Rust 中常见的集合包括 Vector(列表)、String(字符串)和 Map(键值对)。
Vec<T>
,也被称为 vector。vector 允许我们在一个单独的数据结构中储存多于一个的值,它在内存中彼此相邻地排列所有的值。vector 只能储存相同类型的值。
String
字符串类型由 Rust 标准库提供,而不是编入核心语言,它是一种可增长、可变、可拥有和 UTF-8 编码的字符串类型。当 Rustaceans 提及 Rust 中的 "字符串 "时,他们可能指的是 String
或 string slice &str
类型,而不仅仅是其中一种类型。
介绍 Map 我们主要来学习哈希 map(hash map)。HashMap<K, V>
类型储存了一个键类型 K
对应一个值类型 V
的映射。它通过一个哈希函数来实现映射,决定如何将键和值放入内存中。
一、Vector
1.1 新建 vector
创建一个空的 vector 可以使用 Vec::new
函数。
let v: Vec<i32> = Vec::new();
注意这里我们增加了一个类型注解,vector 是用泛型实现的。因为没有向这个 vector 中插入任何值,Rust 并不知道我们想要储存什么类型的元素。
通常,我们会用初始值来创建一个 Vec<T>
而 Rust 会推断出储存值的类型。为了方便 Rust 提供了 vec!
宏,这个宏会根据我们提供的值来创建一个新的 vector。
let v = vec![1, 2, 3];
因为我们提供了 i32 类型的初始值,Rust 可以推断出 v 的类型是 Vec<i32>
。
1.2 更新 vector
对于新建一个 vector 并向其增加元素,可以使用 push
方法。
let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);
v.push(3);
v.push(4);
v.push(5);
如果想要能够改变它的值,必须使用 mut
关键字使其可变。放入其中的所有值都是 i32 类型的,而且 Rust 也根据数据做出如此判断,所以可以不需要 Vec<i32>
注解。
1.3 读取 vector 的元素
有两种方法引用 vector 中储存的值:通过索引或使用 get
方法。
fn main() {
let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);
v.push(3);
v.push(4);
v.push(5);
let index = 3;
let val = &v[index];
println!("The element is {val}");
let val: Option<&i32> = v.get(index);
match val {
Some(val) => println!("The element is {val}"),
None => println!("Element not found."),
}
}
当使用索引作为参数调用 get
方法时,会得到一个可以用于 match
的 Option<&T>
。
运行结果:
The element is 4
The element is 4
越界访问 vector
调整代码 index 改为 10。
fn main() {
let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);
v.push(3);
v.push(4);
v.push(5);
let index = 10;
let val: Option<&i32> = v.get(index);
match val {
Some(val) => println!("The element is {val}"),
None => println!("Element not found."),
}
let val = &v[index];
println!("The element is {val}");
}
当 get
方法被传递了一个数组外的索引时,它不会 panic 而是返回 None
。所以正常打印出了“Element not found.”。对于 []
方法,当引用一个不存在的元素时 Rust 会造成 panic(thread ‘main’ panicked at src/main.rs:18:17:)。
运行结果:
Exited with status 101
---------------------------------Standard Error---------------------------------
Compiling playground v0.0.1 (/playground)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.39s
Running `target/debug/playground`
thread 'main' panicked at src/main.rs:18:17:
index out of bounds: the len is 5 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
---------------------------------Standard Output---------------------------------
Element not found.
vector 借用规则
一旦程序获取了一个有效的引用,借用检查器将会执行所有权和借用规则来确保 vector 内容的这个引用和任何其他引用保持有效。当我们获取了 vector 的第一个元素的不可变引用并尝试在 vector 末尾增加一个元素的时候,如果尝试在函数的后面引用这个元素是行不通的。
fn main() {
let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);
v.push(3);
v.push(4);
v.push(5);
let first = &v[0];
v.push(6);
println!("The first element is: {first}");
}
运行结果:
Compiling playground v0.0.1 (/playground)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:12:5
|
10 | let first = &v[0];
| - immutable borrow occurs here
11 |
12 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
13 |
14 | println!("The first element is: {first}");
| ------- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `playground` (bin "playground") due to 1 previous error
为什么第一个元素的引用会关心 vector 结尾的变化?不能这么做的原因是由于 vector 的工作方式:在 vector 的结尾增加新元素时,在没有足够空间将所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。
1.4 遍历 vector 中的元素
如果想要依次访问 vector 中的每一个元素,我们可以遍历其所有的元素而无需通过索引一次一个的访问。
fn main() {
let v = vec![1, 2, 3, 4, 5];
for element in &v {
println!("{element}");
}
}
运行结果:
1
2
3
4
5
我们也可以遍历可变 vector 的每一个元素的可变引用以便能改变它们。
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
for element in &mut v {
*element += 10;
}
for element in &v {
println!("{element}");
}
}
运行结果:
11
12
13
14
15
二、String
字符串就是作为字节的集合外加一些方法实现的,当这些字节被解释为文本时,这些方法提供了实用的功能。
2.1 新建字符串
很多 Vec 可用的操作在 String 中同样可用,事实上 String 被实现为一个带有一些额外保证、限制和功能的字节 vector 的封装。
创建一个空的字符串可以使用 String::new
函数。
let mut s = String::new();
通常字符串会有初始数据,因为我们希望一开始就有这个字符串。为此,可以使用 to_string
方法,它能用于任何实现了 Display trait 的类型,比如字符串字面值。
fn main() {
let s = "初始值";
println!("{s}");
let str = s.to_string();
println!("{str}");
// 该方法也可直接用于字符串字面值:
let str1 = "初始值".to_string();
println!("{str1}");
}
运行结果:
初始值
初始值
初始值
也可以使用 String::from
函数来从字符串字面值创建 String。
let s = String::from("初始值");
2.2 更新字符串
String 的大小可以增加,其内容也可以改变。另外,可以方便的使用 +
运算符或 format!
宏来拼接 String 值。
push_str
push_str
方法采用字符串 slice。
fn main() {
let mut s = String::from("初始值");
s.push_str(" 附加部分");
println!("{s}");
let str = " 继续追加所有权未转移";
s.push_str(str);
println!("{s}");
println!("使用 str,其值为:{str}");
}
如果 push_str
方法获取了 str 的所有权,就不能在最后一行(println!("使用 str,其值为:{str}");
)打印出其值了。
运行结果:
初始值 附加部分
初始值 附加部分 继续追加所有权未转移
使用 str,其值为: 继续追加所有权未转移
push
push
方法被定义为获取一个单独的字符作为参数,并附加到 String 中。
let mut s = String::from("hell");
s.push('o');
执行以上代码之后,s
将会包含 “hello”。
使用 +
运算符或 format!
宏拼接字符串
通常我们会希望将两个已知的字符串合并在一起。一种办法是使用 +
运算符;另一种是使用 format! 宏,format! 与 println! 的工作原理相同,不过不同于将输出打印到屏幕上,它返回一个带有结果内容的 String。
另外,宏 format!
生成的代码使用引用所以不会获取任何参数的所有权。
fn main() {
let s1 = String::from("Tom ");
let s2 = String::from("Peter ");
let s3 = String::from("Joy ");
let s = format!("{s1}-{s2}-{s3}");
println!("final str(format!):{s}");
println!("s1:{s1}");
let s = s1 + "-" + &s2 + "-" + &s3;
println!("final str(+):{s}");
//println!("s1:{s1}");//error[E0382]: borrow of moved value: `s1`
}
打开注释的最后一行会报错。s1 在相加(使用 +
号)后不再有效的原因,与使用 +
运算符时调用的函数签名有关。所以虽然 let s = s1 + "-" + &s2 + "-" + &s3;
看起来就像它会复制字符串并创建一个新的字符串,而实际上这个语句会获取 s1
的所有权,附加上从 s2、s3 中拷贝的内容,并返回结果的所有权。
运行结果:
final str(format!):Tom -Peter -Joy
s1:Tom
final str(+):Tom -Peter -Joy
2.3 索引字符串
在很多语言中,通过索引来引用字符串中的单独字符是有效且常见的操作。然而在 Rust 中,如果你尝试使用索引语法访问 String 的一部分,会出现一个错误。Rust 的字符串不支持索引。
最后一个 Rust 不允许使用索引获取 String 字符的原因是,索引操作预期总是需要常数时间(O(1))。但是对于 String 不可能保证这样的性能,因为 Rust 必须从开头到索引位置遍历来确定有多少有效的字符。
2.4 字符串 slice
为了更明确索引并表明你需要一个字符串 slice,相比使用 []
和单个值的索引,可以使用 []
和一个 range 来创建含特定字节的字符串 slice。
fn main() {
let hello = "Здравствуйте";
let s = &hello[0..8];
println!("s:{s}");
}
这里,s
会是一个 &str
,它包含字符串的前八个字节。
运行结果:
s:Здра
如果获取 &hello[0..1]
会发生什么呢?Rust 在运行时会 panic,就跟访问 vector 中的无效索引时一样:
Compiling playground v0.0.1 (/playground)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/playground`
thread 'main' panicked at src/main.rs:4:19:
byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
2.5 遍历字符串
操作字符串每一部分的最好的方法是明确表示需要字符还是字节。对于单独的 Unicode 标量值使用 chars
方法。对 “Зд” 调用 chars
方法会将其分开并返回两个 char
类型的值。
fn main() {
for c in "Здравствуйте".chars() {
println!("{c}");
}
for b in "Здравствуйте".bytes() {
println!("{b}");
}
}
运行结果
З
д
р
а
в
с
т
в
у
й
т
е
208
151
208
180
209
128
208
176
208
178
209
129
209
130
208
178
209
131
208
185
209
130
208
181
有效的 Unicode 标量值可能会由不止一个字节组成。
三、Map
Map 是一种键值对结构,我们主要来学习哈希 Map。哈希 map(hash map)。HashMap<K, V>
类型储存了一个键类型 K
对应一个值类型 V
的映射。它通过一个哈希函数(hashing function)来实现映射,决定如何将键和值放入内存中。
3.1 新建哈希 map
可以使用 new
创建一个空的 HashMap
,并使用 insert
增加元素。
use std::collections::HashMap;
fn main() {
let mut employees = HashMap::new();
employees.insert(10000, String::from("Tom"));
employees.insert(10002, String::from("Peter"));
for (key, value) in &employees {
println!("{key}:{value}");
}
}
运行结果
10000:Tom
10002:Peter
注意必须首先 use
标准库中集合部分的 HashMap
。标准库中对 HashMap
的支持也相对较少,例如,并没有内建的构建宏。
像 vector
一样,哈希 map 将它们的数据储存在堆上,这个 HashMap
的键类型是 i32,值类型是 String。类似于 vector
,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。
3.2 访问哈希 map 中的值
可以通过 get
方法并提供对应的键来从哈希 map 中获取值。get
方法返回 Option<&V>
,如果某个键在哈希 map 中没有对应的值,get
会返回 None
。可以使用与 vector 类似的方式来遍历哈希 map 中的每一个键值对,也就是 for 循环。
use std::collections::HashMap;
fn main() {
let mut employees = HashMap::new();
employees.insert(10000, String::from("Tom"));
employees.insert(10002, String::from("Peter"));
let employee_number = 10003;
let opt_name = employees.get(&employee_number);
match opt_name {
Some(name) => println!("{employee_number}:{name}"),
None => println!("There is no such number!"),
}
for (key, value) in &employees {
println!("{key}:{value}");
}
}
运行结果
There is no such number!
10002:Peter
10000:Tom
3.3 哈希 map 和所有权
对于像 i32
这样的实现了 Copy trait 的类型,其值可以拷贝进哈希 map。对于像 String
这样拥有所有权的值,其值将被移动而哈希 map 会成为这些值的所有者。如果将值的引用插入哈希 map,这些值本身将不会被移动进哈希 map。但是这些引用指向的值必须至少在哈希 map 有效时也是有效的。
use std::collections::HashMap;
fn main() {
let mut employees = HashMap::new();
let employee_number = 10003;
let employee_name = String::from("Tom");
employees.insert(employee_number, employee_name);
println!("{employee_number}");
println!("{employee_name}");
}
编译报错:
Compiling playground v0.0.1 (/playground)
error[E0382]: borrow of moved value: `employee_name`
--> src/main.rs:12:15
|
7 | let employee_name = String::from("Tom");
| ------------- move occurs because `employee_name` has type `String`, which does not implement the `Copy` trait
8 |
9 | employees.insert(employee_number, employee_name);
| ------------- value moved here
...
12 | println!("{employee_name}");
| ^^^^^^^^^^^^^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
9 | employees.insert(employee_number, employee_name.clone());
| ++++++++
For more information about this error, try `rustc --explain E0382`.
error: could not compile `playground` (bin "playground") due to 1 previous error
报错信息提示我们:借用被移动的值:“employee_name”。
3.4 更新哈希 map
当我们想要改变哈希 map 中的数据时,必须决定如何处理一个键已经有值了的情况。可以选择完全无视旧值并用新值代替旧值。可以选择保留旧值而忽略新值,并只在键 没有 对应值时增加新值。或者可以结合新旧两值。
覆盖一个值
如果我们插入了一个键值对,接着用相同的键插入一个不同的值,与这个键相关联的旧值将被替换。
use std::collections::HashMap;
fn main() {
let mut employees = HashMap::new();
employees.insert(10003, String::from("Tom"));
employees.insert(10003, String::from("Peter"));
println!("{:?}", employees);
}
运行结果
{10003: "Peter"}
只在键没有对应值时插入键值对
我们经常会检查某个特定的键是否已经存在于哈希 map 中并进行如下操作:如果哈希 map 中键已经存在则不做任何操作。如果不存在则连同值一块插入。
为此哈希 map 有一个特有的 API,叫做 entry
,它获取我们想要检查的键作为参数。entry
函数的返回值是一个枚举,Entry
,它代表了可能存在也可能不存在的值。Entry
的 or_insert
方法在键对应的值存在时就返回这个值的可变引用,如果不存在则将参数作为新值插入并返回新值的可变引用。
use std::collections::HashMap;
fn main() {
let mut employees = HashMap::new();
employees.insert(10003, String::from("Tom"));
employees.entry(10003).or_insert(String::from("Peter"));
employees.entry(10002).or_insert(String::from("Peter"));
println!("{:?}", employees);
}
运行结果
{10002: "Peter", 10003: "Tom"}
根据旧值更新一个值
另一个常见的哈希 map 的应用场景是找到一个键对应的值并根据旧的值更新它。or_insert
方法返回键对应值的一个可变引用(&mut V
)。
use std::collections::HashMap;
fn main() {
let text = "Hello world hello world test";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let word_count = map.entry(word).or_insert(0);
*word_count += 1;
}
println!("{:?}", map);
}
运行结果
{"hello": 1, "Hello": 1, "world": 2, "test": 1}
split_whitespace
方法返回一个由空格分隔 text
值子 slice
的迭代器。or_insert
方法返回这个键的值的一个可变引用(&mut V
)。这里我们将这个可变引用储存在 word_count
变量中,所以为了赋值必须首先使用星号(*
)解引用 word_count
。这个可变引用在 for
循环的结尾离开作用域,这样所有这些改变都是安全的并符合借用规则。
参考链接
- Rust 官方网站:https://www.rust-lang.org/zh-CN
- Rust 官方文档:https://doc.rust-lang.org/
- Rust Play:https://play.rust-lang.org/
- 《Rust 程序设计语言》