2.5 字符串
字符串的本质是一种特殊的容器类型,是由零个或多个字符组成的有限序列。字符串是程序开发中最常用的数据结构。不同于其他容器类型较多关注于容器中的元素,字符串常被作为一个整体来关注和使用。下面介绍字符串最常用的创建、修改、访问等操作。不过,由于字符串的处理涉及迭代器、引用、解引用、所有权和生命周期等概念,对这些概念不了解的读者可先阅读相关内容后再回到本节学习。
2.5.1 字符串的创建
Rust常用的字符串有两种,一种是固定长度的字符串字面量str,另一种是可变长度的字符串对象String。
1. &str的创建
Rust内置的字符串类型是str,它通常以引用的形式&str出现。字符串字面量&str是字符的集合,代表的是不可变的UTF-8编码的字符串的引用,创建后无法再为其追加内容或更改内容。
创建字符串字面量&str有以下两种方式。
1)使用双引号创建字符串字面量,代码如下:
let s1 = "Hello, Rust!";
2)使用as_str方法将字符串对象转换为字符串字面量,代码如下:
1 let str = String::from("Hello, Rust!"); 2 let s2 = str.as_str();
2. String的创建
字符串对象String是由Rust标准库提供的、拥有所有权的UTF-8编码的字符串类型,创建后可以为其追加内容或更改内容。String类型的本质是一个字段为Vec<u8>类型的结构体,它把字符内容存放在堆上,由指向堆上字节序列的指针(as_ptr方法)、记录堆上字节序列的长度(len方法)和堆分配的容量(capacity方法)3部分组成。
创建字符串对象String有以下3种方式。
1)使用String::new函数创建空的字符串对象,代码如下:
let mut s = String::new();
2)使用String::from函数根据指定的字符串字面量创建字符串对象,代码如下
let s = String::from("Hello, Rust!");
3)使用to_string方法将字符串字面值转换为字符串对象,代码如下:
1 let str = "Hello, Rust!"; 2 let s = str.to_string();
2.5.2 字符串的修改
String类型字符串常见的修改操作有追加、插入、连接、替换和删除等。
1)使用push方法在字符串后追加字符,使用push_str方法在字符串后追加字符串字面量。这两个方法都是在原字符串上追加,并不会返回新的字符串。
代码清单2-29的第2行代码中变量s的声明使用了mut关键字,是因为要在字符串后追加字符,该字符串必须是可变的。第3行代码中push方法将字符R追加到s的尾部,s变为“Hello, R”。第4行代码中push_str方法将字符串字面量“ust!”追加到s尾部,s变为“Hello, Rust!”。
代码清单2-29 追加字符串
1 fn main() { 2 let mut s = String::from("Hello, "); 3 s.push('R'); 4 s.push_str("ust!"); 5 6 println!("{}", s); 7 } 8 9 // Hello, Rust!
2)使用insert方法在字符串中插入字符,使用insert_str方法在字符串中插入字符串字面量。这两个方法都接收两个参数,第1个参数是插入位置的索引,第2个参数是插入字符或字符串字面量。同样地,这两个方法都是在原字符串上插入,并不会返回新的字符串。
代码清单2-30中,第3行代码的insert方法在索引为5的位置插入字符“,”,s变为“Hello, World!”。第4行代码的insert_str方法在索引为7的位置插入字符串字面量“Rust”,s变为“Hello, Rust World!”。需要注意的是,insert和insert_str方法是基于字节序列的索引进行操作的,其内部会通过is_char_boundary方法判断插入位置的索引是否在合法边界内,如果索引非法将会导致程序错误。
代码清单2-30 插入字符串
1 fn main() { 2 let mut s = String::from("Hello World!"); 3 s.insert(5, ','); 4 s.insert_str(7, "Rust "); 5 6 println!("{}", s); 7 } 8 9 // Hello, Rust World!
3)使用“+”或“+=”运算符将两个字符串连接成一个新的字符串,要求运算符的右边必须是字符串字面量,但不能对两个String类型字符串使用“+”或“+=”运算符。连接与追加的区别在于,连接会返回新的字符串,而不是在原字符串上的追加。
代码清单2-31中,第6行代码中“+”运算符对4个字符串进行连接,由于s2、s3是String类型,不能出现在“+”运算符的右边,因此需要将s2、s3转换为字符串字面量。&s2为&String类型,但String类型实现了Deref trait,执行连接操作时会自动解引用为&str类型。s3使用as_str方法将String类型转换为&str类型。s4和第7行代码中的字符“!”已是&str类型,可以直接使用“+”或“+=”运算符连接。
代码清单2-31 使用“+”或“+=”运算符连接字符串
1 fn main() { 2 let s1 = String::from("Hello"); 3 let s2 = String::from(", "); 4 let s3 = String::from("Rust "); 5 let s4 = "World"; 6 let mut s = s1 + &s2 + s3.as_str() + s4; 7 s += "!"; 8 9 println!("{}", s); 10 } 11 12 // Hello, Rust World!
对于较为复杂或带有格式的字符串连接,我们可以使用格式化宏format!,它对于String类型和&str类型的字符串都适用,如代码清单2-32所示。
代码清单2-32 使用format!连接字符串
1 fn main() { 2 let s1 = String::from("Hello"); 3 let s2 = String::from("Rust"); 4 let s3 = "World"; 5 let s = format!("{}-{}-{}", s1, s2, s3); 6 7 println!("{}", s); 8 } 9 10 // Hello-Rust-World
4)使用replace和replacen方法将字符串中指定的子串替换为另一个字符串。replace方法接收两个参数,第1个参数是要被替换的字符串子串,第2个参数是新字符串,它会搜索和替换所有匹配到的要被替换的子串。replacen方法除replace方法接收的两个参数外,还接收第3个参数来指定替换的个数。
代码清单2-33中,第3行代码的replace方法将匹配到的两个子串都进行替换,第4行代码的replacen方法指定只替换一个匹配到的子串。
代码清单2-33 替换字符串
1 fn main() { 2 let s = String::from("aaabbbbccaadd"); 3 let s1 = s.replace("aa", "77"); 4 let s2 = s.replacen("aa", "77", 1); 5 6 println!("{}", s1); 7 println!("{}", s2); 8 } 9 10 // 77abbbbcc77dd 11 // 77abbbbccaadd
5)使用pop、remove、truncate和clear方法删除字符串中的字符。
- pop:删除并返回字符串的最后一个字符,返回值类型是Option<char>。如果字符串为空,则返回None。
- remove:删除并返回字符串中指定位置的字符,其参数是该字符的起始索引位置。remove方法是按字节处理字符串的,如果给定的索引位置不是合法的字符边界,将会导致程序错误。
- truncate:删除字符串中从指定位置开始到结尾的全部字符,其参数是起始索引位置。truncate方法也是按字节处理字符串的,如果给定的索引位置不是合法的字符边界,将会导致程序错误。
- clear:等价于将truncate方法的参数指定为0,删除字符串中所有字符。
代码清单2-34中,第4行代码的pop方法删除字符串变量s中的最后一个字符“d”,返回值是Some('d')。第7行代码的remove方法删除s中的字符“虎”,如果把参数改为7就会导致程序错误,原因将在2.5.3节解释。第10行代码的truncate方法删除s中的子串“Léopar”,同样如果把参数改为7就会导致程序错误。第13行代码的clear方法删除s中的所有字符。
代码清单2-34 删除字符串
1 fn main() { 2 let mut s = String::from("Löwe 老虎 Léopard"); 3 4 println!("{:?}", s.pop()); 5 println!("{}", s); 6 7 println!("{:?}", s.remove(9)); 8 println!("{}", s); 9 10 s.truncate(9); 11 println!("{}", s); 12 13 s.clear(); 14 println!("{}", s); 15 } 16 17 // Some('d') 18 // Löwe 老虎 Léopar 19 // '虎' 20 // Löwe 老 Léopar 21 // Löwe 老 22 //
2.5.3 字符串的访问
这里不准备详细介绍字符编码的细节,对Unicode字符集、UTF-8编码感兴趣的读者可以自行搜索相关资料学习。读者只需要了解以下两点,就基本能处理常见的字符串操作。
1)字符串是UTF-8编码的字节序列,不能直接使用索引来访问字符。
2)字符串操作可以分为按字节处理和按字符处理两种方式,按字节处理使用bytes方法返回按字节迭代的迭代器,按字符处理使用chars方法返回按字符迭代的迭代器。
代码清单2-35中,使用len方法获取以字节为单位的字符串长度,即字符串中所有字符的总字节数,而不是直观上看到的字符长度。字符串“Löwe 老虎”的长度是12,其中字母“L”的长度是1,特殊字符“ö”的长度是2,中文“老”的长度是3。由此可知,不同字符的长度是不一样的,如果给定的索引位置不是合法的字符边界就会导致程序错误。
代码清单2-35 使用len方法获取字符串长度
1 fn main() { 2 let s = String::from("Löwe 老虎"); 3 println!("Löwe 老虎: {}", s.len()); 4 5 let s = String::from("L"); 6 println!("L: {}", s.len()); 7 8 let s = String::from("ö"); 9 println!("ö: {}", s.len()); 10 11 let s = String::from("老"); 12 println!("老: {}", s.len()); 13 } 14 15 // Löwe 老虎: 12 16 // L: 1 17 // ö: 2 18 // 老: 3
代码清单2-36中,第3行代码的bytes方法返回Bytes迭代器,第4~6行代码的for循环对Bytes迭代器进行迭代处理,可以看到它是按字节进行迭代的。第9行代码的chars方法返回Chars迭代器,第10~12行代码的for循环对Chars迭代器进行迭代处理,可以看到它是按字符进行迭代的。
代码清单2-36 通过迭代器访问字符串的字符
1 fn main() { 2 let s = String::from("Löwe 老虎"); 3 let bytes = s.bytes(); 4 for b in bytes { 5 print!("{} | ", b); 6 } 7 println!(); 8 9 let chars = s.chars(); 10 for c in chars { 11 print!("{} | ", c); 12 } 13 } 14 15 // 76 | 195 | 182 | 119 | 101 | 32 | 232 | 128 | 129 | 232 | 153 | 142 | 16 // L | ö | w | e | | 老 | 虎 |