集合
Table of Contents
Rust 标准库中包含一系列被称为 集合 ,非常有用的 数据结构 :
- 大部分其他数据类型都代表一个特定的值,不过集合可以 包含多个值
- 不同于内建的数组和元组类型,这些集合指向的数据是 储存在堆 上的,这意味着数据的数量不必在编译时就已知,并且还可以随着程序的运行增长或缩小
- 每种集合都有着不同功能和成本,而根据当前情况选择合适的集合,这是一项始终成长的技能
这一章将详细的了解三个在 Rust 程序中被广泛使用的集合:
- vector : 允许 一个挨着一个 地储存一系列数量可变的值
- String: 是一个 字符 的集合
- hashMap: 允许将 值 与一个特定的 键 相关联
接下来将讨论如何创建和更新 vector、字符串和哈希 map,以及它们有什么特别之处
vector
第一个类型是 Vec<T> ,也被称为 vector。vector 允许在一个 单独 的数据结构中储存 多于一个 的值:
- 在 内存中彼此相邻 地排列所有的值
- 只能储存 相同类型 的值
在拥有一系列项的场景下非常实用,例如文件中的文本行或是购物车中商品的价格
新建 vector
为了创建一个新的空 vector,可以调用 Vec::new 函数,如下所示:
let v: Vec<i32> = Vec::new();
注意:这里增加了一个 类型 注解。因为没有向这个 vector 中插入任何值,Rust 并不知道想要储存什么类型的元素
这是一个非常重要的点:vector 是用泛型实现的,它可以存放任何类型 而当 Vec 存放某个特定类型时,那个类型位于尖括号中 在示例中,v 这个 Vec 将存放 i32 类型的元素
在更实际的代码中,一旦插入值 Rust 就可以推断出想要存放的类型,所以你很少会需要这些类型注解。更常见的做法是使用 初始值 来创建一个 Vec,而且为了方便 Rust 提供了 vec! 宏。这个宏会根据 提供的值 来创建一个新的 Vec。下面新建一个拥有值 1、2 和 3 的 Vec<i32>:
let v = vec![1, 2, 3];
因为提供了 i32 类型的初始值,Rust 可以推断出 v 的类型是 Vec<i32> ,因此类型注解就不是必须的。接下来让看看如何修改一个 vector
更改 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> 注解
另外除了 push 之外还有一个 pop 方法,它会移除并返回 vector 的最后一个元素
丢弃 vector 时也会丢弃其所有元素
类似于任何其他的 struct,vector 在其 离开作用域时会被释放 ,如下面注释:
{ let v = vec![1, 2, 3, 4]; // 处理变量 v } // <- 这里 v 离开作用域并被丢弃
当 vector 被丢弃时,所有其内容也会被丢弃,这意味着这里它包含的整数将被清理 这可能看起来非常直观,不过一旦开始使用 vector 元素的引用,情况就变得有些复杂了
读取 vector 的元素
访问 vector 中一个值的两种方式, 索引 语法或者 get 方法:
let v = vec![1, 2, 3, 4, 5]; let third: &i32 = &v[2]; println!("The third element is {}", third); match v.get(2) { Some(third) => println!("The third element is {}", third), None => println!("There is no third element."), }
这里有两个需要注意的地方:
- 使用 索引值 2 来获取 第三个 元素,索引是从 0 开始的
- 两个不同的获取第三个元素的方式分别为:
- 使用 & 和 [] 返回一个 引用
- 使用 get 方法以索引作为参数来返回一个 Option<&T>
Rust 有两个引用元素的方法的原因:程序可以选择如何处理当索引值在 vector 中没有对应值的情况
如果有一个有五个元素的 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);
这时候会有编译报错:
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable --> src/main.rs:7:5 | 5 | let first = &v[0]; | - immutable borrow occurs here 6 | 7 | v.push(6); | ^^^^^^^^^ mutable borrow occurs here 8 | 9 | println!("The first element is: {}", first); | ----- immutable borrow later used here
不能这么做的原因是由于 vector 的工作方式: 在 vector 的结尾增加新元素时,在没有足够空间将所有所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中 这时,第一个元素的引用就指向了被释放的内存 借用规则阻止程序陷入这种状况
遍历 vector 中的元素
如果想要依次访问 vector 中的每一个元素,可以遍历其所有的元素而无需通过索引一次一个的访问。下面展示了如何使用 for 循环来获取 i32 值的 vector 中的每一个元素的 不可变引用 并将其打印:
let v = vec![100, 32, 57]; for i in &v { println!("{}", i); }
也可以遍历可变 vector 的每一个元素的 可变引用 以便能改变他们。下面的 for 循环会给每一个元素加 50:
let mut v = vec![100, 32, 57]; for i in &mut v { *i += 50; }
使用枚举来储存多种类型
vector 只能储存相同类型的值。这是很不方便的;绝对会有需要储存一系列不同类型的值的用例
幸运的是, 枚举 的成员都被定义为相同的枚举类型,所以当需要在 vector 中储存不同类型值时,可以定义并使用一个枚举
假如想要从电子表格的一行中获取值,而这一行的有些列包含数字,有些包含浮点值,还有些是字符串 1. 可以定义一个枚举,其成员会存放这些不同类型的值,同时所有这些枚举成员都会被当作相同类型,那个枚举的类型 2. 接着可以创建一个储存枚举值的 vector,这样最终就能够储存不同类型的值了
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 对象
String
字符串是新晋 Rustacean 们通常会被困住的领域,这是由于三方面理由的结合: 1. Rust 倾向于确保暴露出可能的错误 2. 字符串是比很多程序员所想象的要更为复杂的数据结构 3. UTF-8 所有这些要素结合起来对于来自其他语言背景的程序员就可能显得很困难了
字符串就是作为 字节的集合 外加一些方法实现的,当这些字节被解释为文本时,这些方法提供了实用的功能:
- 先会讲述 String 中那些任何集合类型都有的操作,比如 创建 、 更新 和 读取
- 讨论 String 与其他集合不一样的地方,例如由于人和计算机理解 String 数据方式的不同, 索引 String是很复杂的
什么是字符串
Rust 的核心语言中只有一种字符串类型: str ,而 字符串 slice 通常以被 借用 的形式出现 &str
字符串 slice 是一些储存在别处的 UTF-8 编码字符串数据的引用。比如:字符串字面值被储存在程序的二进制输出中
称作 String 的类型是由 标准库 提供的,而没有写进核心语言部分,它是 可增长的 、 可变的 、 有 所有权的 、 UTF-8 编码的字符串类型
当 Rustacean 们谈到 Rust 的 “字符串”时,它们通常指的是 String 和字符串 slice &str 类型,而不仅仅是其中之一 虽然本部分内容大多是关于 String 的,不过这两个类型在 Rust 标准库中都被广泛使用,String 和字符串 slice 都是 UTF-8 编码的
Rust 标准库中还包含一系列其他字符串类型,比如 OsString、OsStr、CString 和 CStr,相关库 crate 甚至会提供更多储存字符串数据的选择 看到这些由 String 或是 Str 结尾的名字了吗?这对应着它们提供的所有权和可借用的字符串变体,就像是你之前看到的 String 和 str 举例而言,这些字符串类型能够以不同的编码,或者内存表现形式上以不同的形式,来存储文本内容
新建字符串
很多 Vec 可用的操作在 String 中同样可用,从以 new 函数创建字符串开始,如下所示:
let mut s = String::new();
这新建了一个叫做 s 的空的字符串,接着可以向其中装载数据
通常字符串会有初始数据,因为希望一开始就有这个字符串
可以使用 to_string 方法,它能用于任何实现了 Display trait 的类型, 字符串字面值 也实现了它:
let data = "initial contents"; let s = data.to_string(); // 该方法也可直接用于字符串字面值: let s = "initial contents".to_string();
也可以使用 String::from 函数来从字符串字面值创建 String。下面的代码等同于使用 to_string :
let s = String::from("initial contents");
因为字符串应用广泛,这里有很多不同的用于字符串的通用 API 可供选择 其中一些可能看起来多余,不过都有其用武之地! 在这个例子中,String::from 和 .to_string 最终做了完全相同的工作,所以如何选择就是风格问题了
字符串是 UTF-8 编码的,所以可以包含任何可以正确编码的数据:
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("Olá"); let hello = String::from("Здравствуйте"); let hello = String::from("Hola");
所有这些都是有效的 String 值
更改字符串
String 的 大小 可以增加,其 内容 也可以改变,就像可以放入更多数据来改变 Vec 的内容一样。另外,可以方便的使用 + 运算符 或 format! 宏 来拼接 String 值
使用 push_str 和 push 附加字符串
可以通过 push_str 方法来附加字符串 slice,从而使 String 变长:
let mut s = String::from("foo"); s.push_str("bar");
执行这两行代码之后,s 将会包含 foobar
push_str 方法采用 字符串 slice ,因为并不需要获取参数的所有权。下面展示了如果将 s2 的内容附加到 s1 之后,自身不能被使用就糟糕了:
let mut s1 = String::from("foo"); let s2 = "bar"; s1.push_str(s2); println!("s2 is {}", s2);
如果 push_str 方法获取了 s2 的所有权,就不能在最后一行打印出其值了 好在代码如期望的那样工作!
push 方法被定义为获取一个 单独的字符 作为参数,并 附加 到 String 中。下面展示了使用 push 方法将字母 l 加入 String 的代码:
let mut s = String::from("lo"); s.push('l');
执行这些代码之后,s 将会包含 “lol”
使用 + 运算符或 format! 宏拼接字符串
通常会希望将两个已知的字符串合并在一起。一种办法是像这样使用 + 运算符 :
let s1 = String::from("Hello, "); let s2 = String::from("world!"); let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用
s1 在 相加后不再有效 的原因,和为啥要使用 s2 的引用 ,都与使用 + 运算符使用了 add 函数有关,这个函数签名看起来像这样:
fn add(self, s: &str) -> String {
这并不是标准库中实际的签名;标准库中的 add 使用泛型定义 这里看到的 add 的签名使用具体类型代替了泛型,这也正是当使用 String 值调用这个方法会发生的
- s2 使用了 &: 使用 第二个字符串的引用 与第一个字符串相加。这是因为 add 函数的 s 参数:只能将 &str 和 String 相加,不能将两个 String 值相加
正如 add 的第二个参数所指定的,&s2 的类型是 &String 而不是 &str。那么为什么还能编译呢? 这是因为 &String 可以被 强转成 &str 当add函数被调用时,Rust 使用了一个被称为”解引用强制多态“的技术,可以将其理解为它把 &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! 宏 :
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
这个版本就好理解的多,并且 不会获取任何参数的所有权
索引字符串
在很多语言中,通过 索引 来引用字符串中的单独字符是有效且常见的操作。然而在 Rust 中,如果尝试使用索引语法访问 String 的一部分,会出现一个错误:
let s1 = String::from("hello"); let h = s1[0];
这会产生下面的编译错误:
error[E0277]: the type `std::string::String` cannot be indexed by `{integer}` --> src/main.rs:3:13 | 3 | let h = s1[0]; | ^^^^^ `std::string::String` cannot be indexed by `{integer}` | = help: the trait `std::ops::Index<{integer}>` is not implemented for `std::string::String`
错误和提示说明了全部问题:Rust 的字符串不支持索引
那么接下来的问题是,为什么不支持呢? 为了回答这个问题,必须先聊一聊 Rust 是如何在内存中储存字符串的
内部实现
String 是一个 Vec<u8> 的封装。先来看一些正确编码的字符串的例子。首先:
let len = String::from("Hola").len();
在这里,len 的值是 4 ,这意味着储存字符串 “Hola” 的 Vec 的长度是四个字节 这里每一个字母的 UTF-8 编码都占用一个字节
那下面这个例子:
let len = String::from("Здравствуйте").len();
当问及这个字符是多长的时候有人可能会说是 12。然而,Rust 的回答是 24 这是使用 UTF-8 编码 “Здравствуйте” 所需要的字节数,而每个 Unicode 标量值需要两个字节存储 注意:这个字符串中的首字母是西里尔字母的 Ze 而不是阿拉伯数字 3
可见一个字符串字节值的索引并不总是对应一个有效的 Unicode 标量值。作为演示,思考下无效的 Rust 代码:
let hello = "Здравствуйте"; let answer = &hello[0];
answer 的值应该是什么呢?它应该是第一个字符 З 吗? 当使用 UTF-8 编码时,З 的第一个字节 208,第二个是 151,所以 answer 实际上应该是 208,不过 208 自身并不是一个有效的字母 返回 208 可不是一个请求字符串第一个字母的人所希望看到的,不过它是 Rust 在字节索引 0 位置所能提供的唯一数据 用户通常不会想要一个字节值被返回,即便这个字符串只有拉丁字母:即便 &"hello"[0] 是返回字节值的有效代码,它也应当返回 104 而不是 h
为了避免返回意外的值并造成不能立刻发现的 bug,Rust 根本不会编译这些代码,并在开发过程中及早杜绝了误会的发生
字节、标量值和字形簇
这引起了关于 UTF-8 的另外一个问题:从 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 会要求你更明确一些。可以使用 [] 和一个 range 来创建含特定字节的字符串 slice:
let hello = "Здравствуйте"; let s = &hello[0..4];
s 会是一个 &str,它包含 字符串的头四个字节
早些时候,提到了这些字母都是两个字节长的,所以这意味着 s 将会是 "Зд"
如果获取 &hello[0..1] 则Rust 在 运行时会 panic ,就跟访问 vector 中的无效索引时一样:
thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`', src/libcore/str/mod.rs:2188:4
必须非常小心谨慎的使用这个操作,因为这么做可能会使程序崩溃
遍历字符串
幸运的是,这里还有其他获取字符串元素的方式
如果需要操作单独的 Unicode 标量值,最好的选择是使用 chars 方法。对 “नमस्ते” 调用 chars 方法会将其分开并返回六个 char 类型的值,接着就可以遍历其结果来访问每一个元素了:
for c in "नमस्ते".chars() { println!("{}", c); }
这会打印出如下内容:
न म स ् त े
另外 bytes 方法返回每一个原始字节:
for b in "नमस्ते".bytes() { println!("{}", b); }
这会打印出:
224 164 // --snip-- 165 135
注意:有效的 Unicode 标量值可能会由不止一个字节组成
从字符串中获取字形簇是很复杂的,所以标准库并没有提供这个功能
总结
Rust 选择了以 准确 的方式处理 String 数据作为所有 Rust 程序的默认行为,这意味着程序员们必须更多的思考如何预先处理 UTF-8 数据
不同的语言选择了不同的向程序员展示其复杂性的方式 Rust 相比其他语言更多的暴露出了字符串的复杂性,不过也使在开发生命周期后期免于处理涉及非 ASCII 字符的错误
hashMap
最后介绍的常用集合类型是 Hashmap :
- HashMap<K, V> 类型储存了一个 键 类型 K 对应一个 值 类型 V 的 映射
- 通过一个 哈希函数 来实现映射,决定如何将键和值放入内存中
很多编程语言支持这种数据结构,不过通常有不同的名字:哈希、map、对象、哈希表或者关联数组 哈希 map 可以用于需要任何类型作为键来寻找数据的情况,而不是像 vector 那样通过索引 例如:在一个游戏中,可以将每个团队的分数记录到哈希 map 中,其中键是队伍的名字而值是每个队伍的分数。给出一个队名,就能得到他们的得分。 这里会介绍哈希 map 的基本 API,不过还有更多吸引人的功能隐藏于标准库在 HashMap<K, V> 上定义的函数中。请查看标准库文档来了解更多信息。
新建一个哈希 map
可以使用 new 创建一个空的 HashMap,并使用 insert 增加元素:
例如:记录两支队伍的分数,分别是蓝队和黄队。蓝队开始有 10 分而黄队开始有 50 分:
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 是 同质 的:所有的键必须是相同类型,值也必须都是相同类型
另一个构建哈希 map 的方法是使用一个元组的 vector 的 collect 方法,其中每个元组包含一个 键值对 :
例如,如果队伍的名字和初始分数分别在两个 vector 中,可以使用 collect 方法将这个元组 vector 转换成一个 HashMap
代码如下所示:
use std::collections::HashMap; let teams = vec![String::from("Blue"), String::from("Yellow")]; let initial_scores = vec![10, 50]; let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
- zip 方法: 创建一个元组的 vector ,比如“蓝队”和“10”
- collect 方法:可以将数据收集进一系列的集合类型,包括 HashMap
HashMap<_, _> 类型注解是必要的,因为可能 collect 很多不同的数据结构,而除非显式指定否则 Rust 无从得知你需要的类型 但是对于键和值的类型参数来说,可以使用下划线占位,而 Rust 能够根据 vector 中数据的类型推断出 HashMap 所包含的类型
哈希 map 和所有权
- 对于像 i32 这样的实现了 Copy trait 的类型,其值可以 拷贝 进哈希 map
- 对于像 String 这样拥有所有权的值,其值将被 移动 而哈希 map 会成为 这些值的所有者
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 不再有效, // 尝试使用它们看看会出现什么编译错误!
当 insert 调用将 field_name 和 field_value 移动到哈希 map 中后,将不能使用这两个绑定
如果将 值的引用 插入哈希 map,这些值本身将不会被移动进哈希 map。但是这些引用指向的值必须至少在哈希 map 有效时也是有效的
访问哈希 map 中的值
可以通过 get 方法并提供对应的键来从哈希 map 中获取值:
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);
get 返回 Option<V> :如果某个键在哈希 map 中没有对应的值,get 会返回 None
这里,score 是与蓝队分数相关的值,应为 Some(10)
可以使用与 vector 类似的方式来 遍历 哈希 map 中的每一个键值对,也就是 for 循环 :
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); }
这会以任意顺序打印出每一个键值对:
Yellow: 50 Blue: 10
更新哈希 map
尽管键值对的数量是可以增长的,不过 任何时候,每个键只能关联一个值 。当想要改变哈希 map 中的数据时,必须决定如何处理一个键已经有值了的情况。可以选择:
- 完全无视旧值并用新值代替旧值
- 可以选择保留旧值而忽略新值,并只在键 没有 对应值时增加新值
- 可以结合新旧两值
覆盖一个值
如果插入了一个键值对,接着用 相同的键 插入一个 不同的值 ,与这个键相关联的 旧值将被替换 。下面代码调用了两次 insert,哈希 map 也只会包含一个键值对,因为两次都是对蓝队的键插入的值:
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 有一个特有的 API,叫做 entry :
- 获取想要 检查的键 作为参数
- 返回值:一个 枚举 ,Entry,它代表了是否这个存在这个键
比如说想要检查黄队的键是否关联了一个值。如果没有,就插入值 50,对于蓝队也是如此。
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 的 or_insert 方法:
- 键对应的值存在:返回 这个值的 Entry
- 如果不存在:将参数作为新值 插入 并返回 修改过的 Entry
这比编写自己的逻辑要简明的多,另外也与借用检查器结合得更好
运行代码会打印出 {"Yellow": 50, "Blue": 10}
第一个 entry 调用会插入黄队的键和值 50,因为黄队并没有一个值 第二个 entry 调用不会改变哈希 map 因为蓝队已经有了值 10
根据旧值更新一个值
另一个常见的哈希 map 的应用场景是找到一个键对应的值并 根据旧的值 更新它
例如,计数一些文本中每一个单词分别出现了多少次 使用哈希 map 以单词作为键并递增其值来记录遇到过几次这个单词 如果是第一次看到某个单词,就插入值 0
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);
这会打印出 {"world": 2, "hello": 1, "wonderful": 1}
or_insert 方法会返回这个键的值的一个 可变引用 ( &mut V ):
- 将这个可变引用储存在 count 变量中
- 为了赋值必须首先使用星号 * 解引用 count
- 这个可变引用在 for 循环的结尾离开作用域,这样所有这些改变都是安全的并符合借用规则
哈希函数
HashMap 默认使用一种 “密码学安全的” 1 哈希函数,它可以抵抗拒绝服务攻击 然而这并不是可用的最快的算法,不过为了更高的安全性值得付出一些性能的代价 如果性能监测显示此哈希函数非常慢,以致于无法接受,可以指定一个不同的 hasher 来切换为其它函数,hasher 是一个实现了 BuildHasher trait 的类型 并不需要从头开始实现你自己的 hasher;crates.io 有其他人分享的实现了许多常用哈希算法的 hasher 的库