5.2 向量(Vec)与字符串
在 Rust 中,向量(Vec<T>)和字符串(String 与 &str)是两种最常用、最基础且功能强大的集合类型。与固定大小的数组和元组不同,它们可以在运行时动态地增长和缩小。理解它们的内部机制、所有权关系以及常用操作,对于编写高效、安全的 Rust 代码至关重要。
5.2.1 向量(Vec<T>)
向量是一种可动态增长的数组,它在堆上分配内存来存储相同类型 T 的元素。Vec<T> 是 Rust 标准库提供的核心数据结构之一。
创建向量
有多种方式可以创建向量:
使用
Vec::new()方法:创建一个空的向量。let mut v: Vec<i32> = Vec::new(); v.push(1); v.push(2); v.push(3);这里我们显式地指定了类型
Vec<i32>,因为Vec::new()无法推断元素类型。使用
vec!宏:这是最常用、最便捷的方式,可以同时创建并初始化向量。let v = vec![1, 2, 3]; // 类型被自动推断为 Vec<i32> let v2 = vec![0; 5]; // 创建一个包含 5 个 0 的向量,等价于 [0, 0, 0, 0, 0]
更新向量
- 添加元素:使用
push方法在向量末尾添加一个元素。 - 移除元素:使用
pop方法移除并返回向量最后一个元素(返回Option<T>)。也可以使用remove方法移除指定索引的元素,但这会导致后续元素向前移动,效率较低。
let mut v = vec![1, 2, 3];
v.push(4); // v 变为 [1, 2, 3, 4]
let last = v.pop(); // last 为 Some(4),v 变为 [1, 2, 3]
let second = v.remove(1); // second 为 2,v 变为 [1, 3]
读取向量元素
访问向量中的元素主要有两种方式:
使用索引和
[]运算符:如果索引越界,程序会直接panic。let v = vec![1, 2, 3, 4, 5]; let third: &i32 = &v[2]; // 获取第三个元素的引用 println!("The third element is {}", third);使用
get方法:返回一个Option<&T>类型。如果索引有效,返回Some(&element);如果索引越界,返回None,不会导致panic。let v = vec![1, 2, 3, 4, 5]; match v.get(2) { Some(third) => println!("The third element is {}", third), None => println!("There is no third element."), }在不确定索引是否有效时,推荐使用
get方法。
遍历向量
可以使用 for 循环来遍历向量中的元素。
let v = vec![100, 32, 57];
for i in &v { // 不可变地遍历引用
println!("{}", i);
}
let mut v = vec![100, 32, 57];
for i in &mut v { // 可变地遍历引用,可以修改元素
*i += 50;
}
向量的所有权
向量拥有其内部的所有元素。当向量被丢弃时,其所有元素也会被丢弃。当你将元素插入向量时,元素的所有权会转移给向量。当你通过索引或 get 获取元素的引用时,你借用了向量,必须遵守借用规则。
5.2.2 字符串(String 与 &str)
Rust 中的字符串处理比许多其他语言要复杂一些,这主要归因于其内存安全和编码方式。Rust 的核心语言层面只有一种字符串类型:字符串切片 &str,它通常以不可变引用的形式存在。而标准库提供的 String 类型是可变的、拥有所有权的、在堆上分配的 UTF-8 编码字符串。
&str (字符串切片)
- 定义:对存储在别处的 UTF-8 编码字符串数据的引用。它通常以
&str类型出现。 - 创建:字符串字面量就是
&str类型。let s = "hello"; // s 的类型是 &str,它指向程序二进制文件中的一段只读内存。 - 特性:不可变,固定大小(指向数据的指针和长度),不拥有数据。
String (拥有所有权的字符串)
- 定义:一个可变的、在堆上分配的、拥有所有权的 UTF-8 编码字符串。
- 创建:
String::new():创建一个空字符串。let mut s = String::new(); s.push_str("hello");to_string()方法:从&str创建String。let s = "hello".to_string();String::from():与to_string()类似。let s = String::from("hello");
- 更新:
push_str(&str):追加一个字符串切片。push(char):追加一个字符。+运算符:连接字符串。注意,+运算符会取得左侧String的所有权,并借用右侧&str。let s1 = String::from("Hello, "); let s2 = String::from("world!"); let s3 = s1 + &s2; // 注意 s1 被移动了,不能再使用format!宏:更灵活、更清晰的字符串连接方式,不会取得任何参数的所有权。let s1 = String::from("tic"); let s2 = String::from("tac"); let s3 = String::from("toe"); let s = format!("{}-{}-{}", s1, s2, s3);
字符串的内部表示
String 和 &str 内部存储的都是 UTF-8 编码的字节序列。这意味着:
- 它们不是字符数组,而是字节数组。
- 一个英文字符占用 1 个字节,一个中文字符占用 3 个字节。
- 不能直接通过索引访问字符串中的字符(例如
s[0]是不允许的),因为索引操作的时间复杂度是 O(1),但 UTF-8 编码的字符是变长的,无法保证 O(1) 的字符访问。
字符串的索引与遍历
由于 UTF-8 编码的特性,Rust 提供了三种不同的方式来看待字符串:
- 字节(Bytes):使用
.bytes()方法。 - 标量值(Scalar Values):使用
.chars()方法,返回char类型。 - 字形簇(Grapheme Clusters):最接近我们所说的“字母”,但 Rust 标准库不提供此功能,需要第三方 crate(如
unicode-segmentation)。
let s = String::from("नमस्ते");
for b in s.bytes() {
println!("{}", b); // 输出 18 个字节
}
for c in s.chars() {
println!("{}", c); // 输出 6 个 char 值
}
5.2.3 向量与字符串的协同
向量和字符串经常一起使用。例如,你可以有一个字符串向量 Vec<String> 或 Vec<&str>。
let mut string_vec: Vec<String> = Vec::new();
string_vec.push(String::from("apple"));
string_vec.push(String::from("banana"));
let str_slice_vec: Vec<&str> = vec!["hello", "world"];
当处理字符串切片和 String 的集合时,需要仔细考虑所有权和借用。例如,一个函数如果接受 &[&str] 参数,可以同时接受 Vec<String> 的引用(通过 & 和 as_slice())和字符串字面量数组。
5.2.4 性能考量
- 容量(Capacity):
Vec和String都有一个capacity属性和一个length属性。length是当前元素/字节数,capacity是已分配内存可以容纳的最大元素/字节数。当length接近capacity时,再添加新元素会触发重新分配,将数据复制到更大的内存块中。频繁的重新分配会影响性能。 - 预分配:如果你能大致知道最终需要存储多少元素,可以使用
Vec::with_capacity(n)或String::with_capacity(n)预分配足够的空间,避免不必要的重新分配。 shrink_to_fit:如果你需要释放多余的内存,可以使用shrink_to_fit方法,但这可能会触发重新分配。
5.2.5 总结
- 向量(
Vec<T>):动态数组,提供高效的随机访问和末尾增删操作。使用vec!宏创建,使用push、pop、get和索引进行读写。 - 字符串(
String与&str):String是拥有所有权的可变 UTF-8 字符串,&str是不可变的字符串切片。使用push_str、push、+或format!来构建String。由于 UTF-8 编码,不能直接通过索引访问字符,应使用.chars()或.bytes()进行遍历。
掌握向量和字符串是 Rust 开发的基础。理解它们的区别、所有权模型以及内部编码方式,将帮助你避免许多常见的陷阱,并编写出更健壮、更高效的代码。
