5. rust字符串String详解

一、前言

rust中的String是一个非常常用的crate,它的底层涉及到了rust中的所有权概念,不过这不是本章的内容,如果对rust所有权概念感兴趣的,可以查看前一篇文章:String与所有权

本文的目的是详细、全面的介绍String的基本用法,毕竟它实在是太过常用了,自带了大量的方法。

二、基本概念

字符串,也就是由一系列字符组成的,而在计算机中存储一个字符,用到的字节数量并不完全相同。

比如下面的代码:

fn main() {
    let s1=String::from("h");
    let s2=String::from("你");
    println!("{} {}",s1.len(),s2.len());
}

同样是一个字符,只不过s1是英文字符,s2是中文字符,所使用的空间就不是一样大:

1 3

一个英文字符用1个字节大小,而一个中文字符却要用3个字节大小。

之所以出现这个现象,是因为rust中的字符串String为了更加的通用化,采用的是UTF-8编码,更多介绍可参考文章:编码

正因为utf-8编码的这一特性,导致了我们无法像c语言那样,可以直接遍历字符串中的所有字符:

fn main() {
    let s=String::from("hello 世界");
    for i in s{ //错误,无法直接遍历

    }
}

同样的,你也无法直接用下标取出对应的字符

fn main() {
    let s=String::from("hello 世界");
    let c=s[0]; //错误,不能直接用下标取出字符
}

因为每个字符占用的内存大小不一,所以它不知道你是想要取出字符,还是想要取这个位置上的字节。

三、构造

既然是学习String,那么第一件事就是了解我们应该怎么创建一个String,一般有三个方法:

fn main() {
    let s=String::from("hello 世界");

    let mut s1=String::new();
    s1.push_str("hello 世界");
    //s1+="hello 世界"; //与上面的语句等价,即:追加字符串在后面
    
    let s2="hello 世界".to_string();

    println!("{}", s);
    println!("{}", s1);
    println!("{}", s2);
}

三个方法分别是调用from函数、new函数以及to_string函数。

其中,fromto_string的功能是等价的,只是调用的对象不同而已,作用都是从一个字符串字面量直接构造出一个String来。

new函数则是凭空产生一个String,并且为空,如果想要让它存值就得将他声明为可变的(mut),之后可以用push_str函数或者+=操作符来追加字符串。

最后输出的结果都是一样的:

hello 世界
hello 世界
hello 世界

除此之外,还有一个函数为with_capacity

 let s=String::with_capacity(100);

它的作用与new基本相同,唯一不同之处在于,这个函数需要一个参数来初始化其内部的内存大小,如果你事先知道自己需要多大的内存,那么建议你使用这个函数来构造一个String而不是用new

至于原因,可以参考后文的:长度与容量

四、遍历

接下来,我们首先要看的就是如何对字符串进行遍历,用到的函数为as_byteschars

首先是as_bytes函数,看名字也知道,它的意思就是:作为字节

所以它的功能就是遍历字符串的所有字节,就可以这样写:

fn main() {
    let s=String::from("hello 世界");
    for i in s.as_bytes(){
        print!("{} ",i);
    }
}

它的作用就是遍历所有的字节值,打印结果如下:

104 101 108 108 111 32 228 184 150 231 149 140

其中,hello ,就分别对应着前面的104 101 108 108 111 32,最后一个32,是中间的空格

世界两个字,则分别对应228 184 150231 149 140

由于都是一个字节,所以这个as_bytes返回的是一个字节数组,我们就可以字节通过下标获取第几个字节:

    let cs=s.as_bytes();
    let c=cs[1]; //取出第二个字节(从0算起,第二个字节的下标为1)

或者也可以简写为:

let c=s.as_bytes()[1]; //取出第二个字节(从0算起,第二个字节的下标为1)

除了as_bytes可以返回字节数组外,还可以使用bytes函数返回字符迭代器:

fn main() {
    let s=String::from("hello 世界");
    
    let mut b=s.bytes(); //返回字节迭代器
    println!("{}",b.next().expect("err")); //打印第一个
    println!("{}",b.next().expect("err")); //打印第二个
    println!("{}",b.next().expect("err")); //打印第三个
    println!("{}",b.next().expect("err")); //打印第四个
    //...
    //上面的代码等价于下面的写法:
    for i in s.bytes(){
        println!("{}",i);
    }
}

显然这个函数并不如上面的as_bytes好用,这个就见仁见智了。

不过大多数时候,上面这种遍历的方式并不是我们想要的,我们只想要取出其中的第几个字符而已

这时,我们就可以用到chars函数,作用就是将其作为字符看待:

fn main() {
    let s=String::from("hello 世界");
    for i in s.chars(){
        print!("{} ",i);
    }
}

这时输出的结果就是:

h e l l o   世 界

成功将对应的字符给取了出来。

如果你想要取出第几个字符,就可以用函数nth

fn main() {
    let s=String::from("hello 世界");
    let mut m=s.chars(); //得到迭代器
    let c=m.nth(7); //得到下标为7的Option<char>返回值
    if let Some(t) = c { //取出返回值中携带的字符
        print!("{}",t);
    }
}

这里得到的结果就是字,更加简洁的写法是:

fn main() {
    let s=String::from("hello 世界");
    let c=s.chars().nth(7); //得到下标为7的Option<char>返回值
    if let Some(t) =c { //取出返回值中携带的字符
        print!("{}",t);
    }
}

上面的写法应该还是很好理解的,就是将chars的返回值字节调用nth函数。

但下面这个取值的操作还是有点麻烦了,所以我们还能更简洁:

fn main() {
    let s=String::from("hello 世界");
    let c=s.chars().nth(7).unwrap(); //取出下标7的字符
    print!("{}", c);
}

这里的unwrap函数是Option这个trait的一个函数,等价于:

fn main() {
    let s=String::from("hello 世界");
    let c=s.chars().nth(7);
    let r = match c {
        Some(t) => t,
        None => panic!("")
    };
    print!("{}", r);
}

如果不会这个用法的,可以参考Trait详解

五、长度与容量

说到长度与容量,就不得不提及它的底层原理了。

因为String本质上是在堆上分配的内存,也只有在堆上分配内存,才能满足我们想要动态修改字符串的目的。

与之相对应的是栈,堆与栈的概念在C/C++中听到的应该比较多,同时本套教程也是面对至少了解C/C++的程序员准备的,所以这里不再过多解释,如果不理解的可以自行浏览器搜索相关内存进行了解。

而在你声明一个String后,编译器并不知道你后面会不会再对它进行修改,所以一般来说,它会申请一个比你预料中的要大上一些的内存,如果你后面想要追加、插入数据。就不用重新去开辟内存,而是直接在后面追加。

长度与容量分别对应的函数为:len(), capacity()

比如下面这段代码:

fn main() {
    let s=String::from("hello 世界");
    print!("{} {}", s.len(),s.capacity());
}

因为是直接从一个字面量生成的String,而一般这样的行为大多数都不会再追加数据了,所以其默认行为就是容量与长度同样大:

12 12

但如果你用new的方式:

fn main() {
    let mut s=String::new();
    s.push('c');
    print!("{} {}", s.len(),s.capacity());
}

这里生成了一个String,并用push函数向里面推入一个字符,此时结果为:

1 8

此时,虽然你只用了1个字节,但实际上它有8个字节的容量,这样就保证了你之后如果还想要继续往里面推入数据,就不用重新开辟内存了

重新开辟内容就意味着,要将这块内存上的数据拷贝到新内存,并释放掉原本的内存,这是一个非常影响程序效率的事情。

也正因如此,如果你提前知道需要多大的内存,那就可以用函数with_capacity来创建一个String

let mut s=String::with_capacity(1024); //提前分配好足够大的内存,避免后续出现拷贝

它的唯一的参数就是你需要多大的内存

六、增删改查

对于数据的操作,无非就是增、删、改、查这四种,所以这里我们再尽量详尽的介绍一下这四种操作。

1.增

首先是增,前面我们已经见过了push_strpush这两个函数:

fn main() {
    let mut s=String::new();
    s.push_str("string"); //推入一个字符串
    s.push('c'); //推入一个字符
}

这两个函数的区别就在于,前一个是用来向原字符串后面追加字符串的,而后一个则是用来追加字符的。

除了调用函数,我们还可以方便的使用符号来代替:

fn main() {
    let mut s=String::new();
    s+="string"; //推入一个字符串
    //上面这句,等价于:s=s+"string";

    //s=s+'c';  //错误,对于字符类型的,只能使用push函数
    //或者通过下面这种方式:先将字符转换为String,然后前面添加&符号,代表对字符串的引用
    s+=&'c'.to_string();
}

但这种操作太单一了,只能在后面推入数据,很多时候我们还想要在前面、中间位置插入数据该怎么做呢?

这时候就可以使用insertinsert_str函数了:

fn main() {
    let mut s=String::new();
    s+="string"; //推入一个字符串
    s.insert_str(0, "prestr "); //在0字节位置插入字符串
    s.insert(0,'中'); //在0字节位置插入字符
    println!("{}", s);
}
作者:余识
全部文章:0
会员文章:0
总阅读量:0
c/c++pythonrustJavaScriptwindowslinux