Rust 标准库中包含一系列被称为 集合(collections)的非常有用的数据结构。大部分其他数据类型都代表一个特定的值,不过集合可以包含多个值。不同于内建的数组和元组类型,这些集合指向的数据是储存在堆上的,这意味着数据的数量不必在编译时就已知,并且还可以随着程序的运行增长或缩小。每种集合都有着不同功能和成本,而根据当前情况选择合适的集合,这是一项应当逐渐掌握的技能。在这一章里,我们将详细的了解三个在 Rust 程序中被广泛使用的集合:
- vector 允许我们一个挨着一个地储存一系列数量可变的值
- 字符串(string)是字符的集合。我们之前见过
String
类型,不过在本章我们将深入了解。 - 哈希 map(hash map)允许我们将值与一个特定的键(key)相关联。这是一个叫做 map 的更通用的数据结构的特定实现。
一、Vector
第一个类型是 Vec<T>
,也被称为 vector。vector 允许我们在一个单独的数据结构中储存多于一个的值,它在内存中彼此相邻地排列所有的值。vector 只能储存相同类型的值。它们在拥有一系列项的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。
新建vector
为了创建一个新的空 vector,可以调用 Vec::new
函数:
let v: Vec<i32> = Vec::new();
注意这里我们增加了一个类型注解<i32>,因为没有向这个 vector 中插入任何值,Rust 并不知道我们想要储存什么类型的元素。这是一个非常重要的点。vector 是用泛型实现的,后边章节会涉及到如何对你自己的类型使用它们。现在,所有你需要知道的就是 Vec<T>
是一个由标准库提供的类型,它可以存放任何类型,而当 Vec
存放某个特定类型时,那个类型位于尖括号中。
let v = Vec::new(); //错误,未指定vector类型
通常,我们会用初始值来创建一个 Vec<T>
而 Rust 会推断出储存值的类型,所以很少会需要这些类型注解。为了方便 Rust 提供了 vec!
宏,这个宏会根据我们提供的值来创建一个新的 vector。
let v = vec![1, 2, 3];
因为我们提供了 i32
类型的初始值,Rust 可以推断出 v
的类型是 Vec<i32>
,因此类型注解就不是必须的。
更新vector
对于新建一个 vector 并向其增加元素,可以使用 push
方法:
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
如果想要能够改变它的值,必须使用 mut
关键字使其可变。放入其中的所有值都是 i32
类型的,而且 Rust 也根据数据做出如此判断,所以不需要 Vec<i32>
注解。
读取vector元素
有两种方法引用 vector 中储存的值:通过索引或使用 get
方法。在接下来的示例中,为了更加清楚的说明,标注了这些函数返回的值的类型。
访问 vector 中一个值的两种方式,索引语法或者 get
方法:
fn main() {
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
println!("The third element is {third}");
let third: Option<&i32> = v.get(2);
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}
}
这里有几个细节需要注意。我们使用索引值 2
来获取第三个元素,因为索引是从数字 0 开始的。使用 &
和 []
会得到一个索引位置元素的引用。当使用索引作为参数调用 get
方法时,会得到一个可以用于 match
的 Option<&T>
。
Rust 提供了两种引用元素的方法的原因是当尝试使用现有元素范围之外的索引值时可以选择让程序如何运行。举个例子,尝试在当有一个 5 个元素的 vector 接着访问索引 100 位置的元素会发生什么:
let v = vec![1, 2, 3, 4, 5];
let does_not_exist = &v[100];
let does_not_exist = v.get(100);
当运行这段代码,你会发现对于第一个 []
方法,当引用一个不存在的元素时 Rust 会造成 panic。这个方法更适合当程序认为尝试访问超过 vector 结尾的元素是一个严重错误的情况,这时应该使程序崩溃。
当 get
方法被传递了一个数组外的索引时,它不会 panic 而是返回 None
。当偶尔出现超过 vector 范围的访问属于正常情况的时候可以考虑使用它。接着你的代码可以有处理 Some(&element)
或 None
的逻辑。例如,索引可能来源于用户输入的数字。如果它们不慎输入了一个过大的数字那么程序就会得到 None
值,你可以告诉用户当前 vector 元素的数量并再请求它们输入一个有效的值。这就比因为输入错误而使程序崩溃要友好的多!
一旦程序获取了一个有效的引用,借用检查器将会执行所有权和借用规则来确保 vector 内容的这个引用和任何其他引用保持有效。回忆一下不能在相同作用域中同时存在可变和不可变引用的规则。当我们获取了 vector 的一个元素的不可变引用并尝试在 vector 末尾增加一个元素的时候,如果尝试在函数的后面引用这个元素是行不通的:
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {first}");
为什么一个元素的引用会关心 vector 结尾的变化?不能这么做的原因是由于 vector 的工作方式:在 vector 的结尾增加新元素时,在没有足够空间将所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,这个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。
遍历vector中的元素
如果想要依次访问 vector 中的每一个元素,我们可以遍历其所有的元素而无需通过索引一次一个的访问。可以使用 for
循环来获取 i32
值的 vector 中的每一个元素的不可变引用并将其打印:
fn main() {
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
}
我们也可以遍历可变 vector 的每一个元素的可变引用以便能改变他们。示例中的 for
循环会给每一个元素加 50
:
fn main() {
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
for i in &v {
println!("{i}");
}
}
可以看到vector内的元素值确实变了。为了修改可变引用所指向的值,在使用 +=
运算符之前必须使用解引用运算符(*
)获取 i
中的值,后边章节会学习。
使用枚举来储存多种类型
vector 只能储存相同类型的值。这是很不方便的;绝对会有需要储存一系列不同类型的值的用例。幸运的是,枚举的成员都被定义为相同的枚举类型,而枚举可以存放不同类型的值,所以当需要在 vector 中储存不同类型值时,我们可以定义并使用一个枚举!
例如,假如我们想要从电子表格的一行中获取值,而这一行的有些列包含数字,有些包含浮点值,还有些是字符串。我们可以定义一个枚举,其成员会存放这些不同类型的值,同时所有这些枚举成员都会被当作相同类型:那个枚举的类型。接着可以创建一个储存枚举值的 vector,这样最终就能够储存不同类型的值了。
fn main() {
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
}
Rust 在编译时就必须准确的知道 vector 中类型的原因在于它需要知道储存每个元素到底需要多少内存。第二个好处是可以准确的知道这个 vector 中允许什么类型。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。使用枚举外加 match
意味着 Rust 能在编译时就保证总是会处理所有可能的情况。
如果在编写程序时不能确切无遗地知道运行时会储存进 vector 的所有类型,枚举技术就行不通了。相反,你可以使用 trait 对象,在后边章节会学到。
丢弃vector时也会丢弃其他所有元素
类似于任何其他的 struct
,vector 在其离开作用域时会被释放:
{
let v = vec![1, 2, 3, 4];
// do stuff with v
} // <- v goes out of scope and is freed here
当 vector 被丢弃时,所有其内容也会被丢弃,这意味着这里它包含的整数将被清理。借用检查器确保了任何 vector 中内容的引用仅在 vector 本身有效时才可用。
二、String
Rust 的核心语言中只有一种字符串类型:字符串 slice str
,它通常以被借用的形式出现,&str
。前边学到了到了 字符串 slices:它们是一些对储存在别处的 UTF-8 编码字符串数据的引用。举例来说,由于字符串字面值被储存在程序的二进制输出中,因此字符串字面值也是字符串 slices。
新建字符串
很多 Vec
可用的操作在 String
中同样可用,事实上 String
被实现为一个带有一些额外保证、限制和功能的字节 vector 的封装。其中一个同样作用于 Vec<T>
和 String
函数的例子是用来新建一个实例的 new
函数:
let mut s = String::new();
这新建了一个叫做 s
的空的字符串,接着我们可以向其中装载数据。通常字符串会有初始数据,因为我们希望一开始就有这个字符串。为此,可以使用 to_string
方法,它能用于任何实现了 Display
trait 的类型,比如字符串字面值。
fn main() {
let data: &str = "initial contents";
let s: String = data.to_string();
// 该方法也可直接用于字符串字面值:
let s = "initial contents".to_string();
}
这些代码会创建包含 initial contents
的字符串。也可以使用 String::from
函数来从字符串字面值创建 String,
等同于使用 to_string
。
let s = String::from("initial contents");
因为字符串应用广泛,这里有很多不同的用于字符串的通用 API 可供选择。其中一些可能看起来多余,不过都有其用武之地!在这个例子中,String::from
和 .to_string
最终做了完全相同的工作,所以如何选择就是代码风格与可读性的问题了。
记住字符串是 UTF-8 编码的,所以可以包含任何可以正确编码的数据:所有这些都是有效的 String
值。
let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שָׁלוֹם");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");
更新字符串
String
的大小可以增加,其内容也可以改变,就像可以放入更多数据来改变 Vec
的内容一样。另外,可以方便的使用 +
运算符或 format!
宏来拼接 String
值。
可以通过 push_str
方法来附加字符串 slice,从而使 String
变长:
let mut s = String::from("foo");
s.push_str("bar");
执行这两行代码之后,s
将会包含 foobar
。push_str
方法采用字符串 slice,因为我们并不需要获取参数的所有权。
fn main() {
let mut s1 = String::from("foo");
let s2 = "bar";
s1.push_str(s2);
println!("s1 is {s1}");
println!("s2 is {s2}");
}
如果 push_str
方法获得了s2的所有权,那么在后边输出s2肯定会报错的,但是这里没有报错,说明push_str
方法并没有获得所有权。
push
方法被定义为获取一个单独的字符作为参数,并附加到 String
中:
fn main() {
let mut s = String::from("hell");
s.push('o');
println!("s is {s}");
}
除了使用push_str
方法外,还可以使用+运算符或者format!宏来拼接字符串。
fn main() {
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用
println!("s2 is {s2}");
println!("s3 is {s3}");
}
运行结果如下:
如果上述代码尝试输出s1:
fn main() {
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用
println!("s1 is {s1}");
}
s1
在相加后不再有效的原因,和使用 s2
的引用的原因,与使用 +
运算符时调用的函数签名有关。+
运算符使用了 add
函数,这个函数签名看起来像这样:
fn add(self, s: &str) -> String {
首先,s2
使用了 &
,意味着我们使用第二个字符串的 引用 与第一个字符串相加。这是因为 add
函数的 s
参数:只能将 &str
和 String
相加,不能将两个 String
值相加。不过等一下 —— 正如 add
的第二个参数所指定的,&s2
的类型是 &String
而不是 &str
。那么为什么示例还能编译呢?
之所以能够在 add
调用中使用 &s2
是因为 &String
可以被 强转(coerced)成 &str
。当add
函数被调用时,Rust 使用了一个被称为 Deref 强制转换(deref coercion)的技术,你可以将其理解为它把 &s2
变成了 &s2[..]
。因为 add
没有获取参数的所有权,所以 s2
在这个操作后仍然是有效的 String
。
其次,可以发现签名中 add
获取了 self
的所有权,因为 self
没有 使用 &
。这意味着示例中的 s1
的所有权将被移动到 add
调用中,之后就不再有效。所以虽然 let s3 = s1 + &s2;
看起来就像它会复制两个字符串并创建一个新的字符串,而实际上这个语句会获取 s1
的所有权,附加上从 s2
中拷贝的内容,并返回结果的所有权。换句话说,它看起来好像生成了很多拷贝,不过实际上并没有:这个实现比拷贝要更高效。
如果想要级联多个字符串,+
的行为就显得笨重了:
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = s1 + "-" + &s2 + "-" + &s3;
这时 s
的内容会是 “tic-tac-toe”。
对于更为复杂的字符串链接,可以使用 format!
宏:
fn main() {
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{s1}-{s2}-{s3}");
}
这些代码也会将 s
设置为 “tic-tac-toe”。format!
与 println!
的工作原理相同,不过不同于将输出打印到屏幕上,它返回一个带有结果内容的 String
。这个版本就好理解的多,宏 format!
生成的代码使用引用所以不会获取任何参数的所有权。
索引字符串
在 Rust 中,如果你尝试使用索引语法访问 String
的一部分,会出现一个错误:
fn main() {
let s1 = String::from("hello");
let h = s1[0];
}
错误和提示说明了全部问题:Rust 的字符串不支持索引。那么接下来的问题是,为什么不支持呢?为了回答这个问题,我们必须先聊一聊 Rust 是如何在内存中储存字符串的。
String
是一个 Vec<u8>
的封装,在String.rs中:
pub struct String {
vec: Vec<u8>,
}
首先看一个例子:
let hello = String::from("Hola");
在这里,len
的值是 4,这意味着储存字符串 “Hola” 的 Vec
的长度是四个字节:这里每一个字母的 UTF-8 编码都占用一个字节。那下面这个例子又如何呢?(注意这个字符串中的首字母是西里尔字母的 Ze 而不是阿拉伯数字 3。)
let hello = String::from("Здравствуйте");
当问及这个字符是多长的时候有人可能会说是 12。然而,Rust 的回答是 24。这是使用 UTF-8 编码 “Здравствуйте” 所需要的字节数,这是因为每个 Unicode 标量值需要两个字节存储。因此一个字符串字节值的索引并不总是对应一个有效的 Unicode 标量值。作为演示,考虑如下无效的 Rust 代码:
let hello = "Здравствуйте";
let answer = &hello[0];
我们已经知道 answer
的第一个字符不是数字3
。当使用 UTF-8 编码时,第一个字节(西里尔字母的 Ze)З
的编码是 208
,第二个是 151
,所以 answer
实际上应该是 208
,不过 208
自身并不是一个有效的字母。返回 208
可不是一个请求字符串第一个字母的人所希望看到的,不过它是 Rust 在字节索引 0 位置所能提供的唯一数据。用户通常不会想要一个字节值被返回。即使这个字符串只有拉丁字母,如果 &"hello"[0]
是返回字节值的有效代码,它也会返回 104
而不是 h
。
为了避免返回意外的值并造成不能立刻发现的 bug,Rust 根本不会编译这些代码,并在开发过程中及早杜绝了误会的发生。
从 Rust 的角度来讲,事实上有三种相关方式可以理解字符串:字节、标量值和字形簇(最接近人们眼中 字母 的概念)。
比如这个用梵文书写的印度语单词 “नमस्ते”,最终它储存在 vector 中的 u8
值看起来像这样:
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]
这里有 18 个字节,也就是计算机最终会储存的数据。如果从 Unicode 标量值的角度理解它们,也就像 Rust 的 char
类型那样,这些字节看起来像这样:
['न', 'म', 'स', '्', 'त', 'े']
这里有六个 char
,不过第四个和第六个都不是字母,它们是发音符号本身并没有任何意义。最后,如果以字形簇的角度理解,就会得到人们所说的构成这个单词的四个字母:
["न", "म", "स्", "ते"]
Rust 提供了多种不同的方式来解释计算机储存的原始字符串数据,这样程序就可以选择它需要的表现方式,而无所谓是何种人类语言。
最后一个 Rust 不允许使用索引获取 String
字符的原因是,索引操作预期总是需要常数时间(O(1))。但是对于 String
不可能保证这样的性能,因为 Rust 必须从开头到索引位置遍历来确定有多少有效的字符。
字符串slice
索引字符串通常是一个坏点子,因为字符串索引应该返回的类型是不明确的:字节值、字符、字形簇或者字符串 slice。因此,如果你真的希望使用索引创建字符串 slice 时,Rust 会要求你更明确一些。为了更明确索引并表明你需要一个字符串 slice,相比使用 []
和单个值的索引,可以使用 []
和一个 range 来创建含特定字节的字符串 slice:
#![allow(unused)]
fn main() {
let hello = "Здравствуйте";
let s = &hello[0..4];
println!("s is {s}");
}
运行结果如下:
这里,s
会是一个 &str
,它包含字符串的头四个字节。我们提到了这些字母都是两个字节长的,所以这意味着 s
将会是 “Зд”。
如果获取 &hello[0..1]
会发生什么呢?答案是:Rust 在运行时会 panic,就跟访问 vector 中的无效索引时一样:
遍历字符串的方法
操作字符串每一部分的最好的方法是明确表示需要字符还是字节。对于单独的 Unicode 标量值使用 chars
方法。对 “Зд” 调用 chars
方法会将其分开并返回两个 char
类型的值,接着就可以遍历其结果来访问每一个元素了:
fn main() {
for c in "Здравствуйте".chars() {
println!("{c}");
}
}
运行结果:
另外 bytes
方法返回每一个原始字节,这可能会适合你的使用场景:
fn main() {
for c in "Здравствуйте".bytes() {
println!("{c}");
}
}
运行结果:
总而言之,字符串还是很复杂的。不同的语言选择了不同的向程序员展示其复杂性的方式。Rust 选择了以准确的方式处理 String
数据作为所有 Rust 程序的默认行为,这意味着程序员们必须更多的思考如何预先处理 UTF-8 数据。这种权衡取舍相比其他语言更多的暴露出了字符串的复杂性,不过也使你在开发周期后期免于处理涉及非 ASCII 字符的错误。
好消息是标准库提供了很多围绕 String
和 &str
构建的功能,来帮助我们正确处理这些复杂场景。请务必查看这些使用方法的文档,例如 contains
来搜索一个字符串,和 replace
将字符串的一部分替换为另一个字符串。
称作 String
的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。当 Rustacean 们谈到 Rust 的 “字符串”时,它们通常指的是 String
或字符串 slice &str
类型,而不特指其中某一个。虽然本部分内容大多是关于 String
的,不过这两个类型在 Rust 标准库中都被广泛使用,String
和字符串 slices 都是 UTF-8 编码的。
三、Hash Map
最后介绍的常用集合类型是 哈希 map(hash map)。HashMap<K, V>
类型储存了一个键类型 K
对应一个值类型 V
的映射。它通过一个 哈希函数(hashing function)来实现映射,决定如何将键和值放入内存中。
哈希 map 可以用于需要任何类型作为键来寻找数据的情况,而不是像 vector 那样通过索引。例如,在一个游戏中,你可以将每个团队的分数记录到哈希 map 中,其中键是队伍的名字而值是每个队伍的分数。给出一个队名,就能得到他们的得分。
新建一个HashMap
可以使用 new
创建一个空的 HashMap
,并使用 insert
增加元素。
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
}
注意必须首先 use
标准库中集合部分的 HashMap
。在这三个常用集合中,HashMap
是最不常用的,所以并没有被 prelude 自动引用。标准库中对 HashMap
的支持也相对较少,例如,并没有内建的构建宏。
像 vector 一样,哈希 map 将它们的数据储存在堆上,这个 HashMap
的键类型是 String
而值类型是 i32
。类似于 vector,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。
访问Hash Map中的值
可以通过 get
方法并提供对应的键来从哈希 map 中获取值:
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);
}
这里,score
是与蓝队分数相关的值,应为 10
。get
方法返回 Option<&V>
,如果某个键在哈希 map 中没有对应的值,get
会返回 None
。程序中通过调用 copied
方法来获取一个 Option<i32>
而不是 Option<&i32>
,接着调用 unwrap_or
在 score
中没有该键所对应的项时将其设置为零。
可以使用与 vector 类似的方式来遍历哈希 map 中的每一个键值对,也就是 for
循环:
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
for (key, value) in &scores {
println!("{key}: {value}");
}
}
Hash Map和所有权
对于像 i32
这样的实现了 Copy
trait 的类型,其值可以拷贝进哈希 map。对于像 String
这样拥有所有权的值,其值将被移动而哈希 map 会成为这些值的所有者:
fn main() {
use std::collections::HashMap;
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
map.insert(field_name, field_value);
// 这里 field_name 和 field_value 不再有效,
// 尝试使用它们看看会出现什么编译错误!
println!("field name is {field_name}");
}
输出结果:
更新HashMap
尽管键值对的数量是可以增长的,每个唯一的键只能同时关联一个值(反之不一定成立:比如蓝队和黄队的 scores
哈希 map 中都可能存储有 10 这个值)。
当我们想要改变哈希 map 中的数据时,必须决定如何处理一个键已经有值了的情况。可以选择完全无视旧值并用新值代替旧值。可以选择保留旧值而忽略新值,并只在键 没有 对应值时增加新值。或者可以结合新旧两值。让我们看看这分别该如何处理!
覆盖一个值
如果我们插入了一个键值对,接着用相同的键插入一个不同的值,与这个键相关联的旧值将被替换。
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);
println!("{:?}", scores);
}
这会打印出 {"Blue": 25}
。原始的值 10
则被覆盖了。
只在键没有对应的值时插入键值对
我们经常会检查某个特定的键是否已经存在于哈希 map 中并进行如下操作:如果哈希 map 中键已经存在则不做任何操作。如果不存在则连同值一块插入。
为此哈希 map 有一个特有的 API,叫做 entry
,它获取我们想要检查的键作为参数。entry
函数的返回值是一个枚举,Entry
,它代表了可能存在也可能不存在的值。比如说我们想要检查黄队的键是否关联了一个值。如果没有,就插入值 50,对于蓝队也是如此:
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
println!("{:?}", scores);
}
第一个 entry
调用会插入黄队的键和值 50
,因为黄队并没有一个值。第二个 entry
调用不会改变哈希 map 因为蓝队已经有了值 10
。
根据旧值更新一个值
另一个常见的Hash Map 的应用场景是找到一个键对应的值并根据旧的值更新它。例如,写一段代码计数一些文本中每一个单词分别出现了多少次。我们使用哈希 map 以单词作为键并递增其值来记录我们遇到过几次这个单词。如果是第一次看到某个单词,就插入值 0
。
fn main() {
use std::collections::HashMap;
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
println!("{:?}", map);
}
输出结果:
split_whitespace
方法返回一个由空格分隔 text
值子 slice 的迭代器。
or_insert
方法返回这个键的值的一个可变引用(&mut V
)。在这段代码中,or_insert返回值是一个可变的i32类型,然后把这个值赋给count,即count的类型为 &mut i32,然后在下边通过给count递增的方法来改变每个单词键对应的值,即单词出现的次数。这里我们将这个可变引用储存在 count
变量中,所以为了赋值必须首先使用星号(*
)解引用 count
。
哈希函数
HashMap
默认使用一种叫做 SipHash的哈希函数,它可以抵御涉及哈希表(hash table) 的拒绝服务(Denial of Service, DoS)攻击。然而这并不是可用的最快的算法,不过为了更高的安全性值得付出一些性能的代价。如果性能监测显示此哈希函数非常慢,以致于你无法接受,你可以指定一个不同的 hasher 来切换为其它函数。hasher 是一个实现了 BuildHasher
trait 的类型。你并不需要从头开始实现你自己的 hasher;crates.io 有其他人分享的实现了许多常用哈希算法的 hasher 的库。
总结
vector、字符串和哈希 map 会在你的程序需要储存、访问和修改数据时帮助你。这里有一些你应该能够解决的练习问题:
- 给定一系列数字,使用 vector 并返回这个列表的中位数(排列数组后位于中间的值)和众数(mode,出现次数最多的值;这里哈希 map 会很有帮助)。
- 将字符串转换为 Pig Latin,也就是每一个单词的第一个辅音字母被移动到单词的结尾并增加 “ay”,所以 “first” 会变成 “irst-fay”。元音字母开头的单词则在结尾增加 “hay”(“apple” 会变成 “apple-hay”)。牢记 UTF-8 编码!
- 使用哈希 map 和 vector,创建一个文本接口来允许用户向公司的部门中增加员工的名字。例如,“Add Sally to Engineering” 或 “Add Amir to Sales”。接着让用户获取一个部门的所有员工的列表,或者公司每个部门的所有员工按照字典序排列的列表。
Rust语言基础教程学习其他章节【未完待续】:
Rust语言基础教程学习【一】:Rust语言基础教程学习【一】(Rust基础语法、数据类型)_LLLL、的博客-CSDN博客
Rust语言基础教程学习【二】:Rust语言基础教程学习【二】(函数、条件语句、循环)_LLLL、的博客-CSDN博客
Rust语言基础教程学习【三】:Rust语言基础教程学习【三】(所有权)_LLLL、的博客-CSDN博客
Rust语言基础教程学习【四】:Rust语言基础教程学习【四】(结构体)_LLLL、的博客-CSDN博客
Rust语言基础教程学习【五】:Rust语言基础教程学习【五】(枚举和模式匹配)_LLLL、的博客-CSDN博客
Rust语言基础教程学习【六】:Rust语言基础教程学习【六】(包、Crate、模块)_LLLL、的博客-CSDN博客