Rust编程:入门、实战与进阶
上QQ阅读APP看书,第一时间看更新

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 |   | 老 | 虎 |