参考:https://kaisery.github.io/trpl-zh-cn/ch08-00-common-collections.html
集合 (collections ):
Rust 标准库中提供的一系列非常有用的数据结构。
大部分其他数据类型都代表一个特定的值,不过集合可以包含多个值。
不同于内建的数组和元组类型,这些集合指向的数据是储存在堆上的,这意味着数据的数量不必在编译时就已知,并且还可以随着程序的运行增长或缩小。
每种集合都有着不同功能和成本,而根据当前情况选择合适的集合,这是一项应当逐渐掌握的技能。
- vector 允许我们一个挨着一个地储存一系列数量可变的值
- 字符串 (string )是字符的集合。我们之前见过
String
类型,不过在本章我们将深入了解。 - 哈希 map (hash map )允许我们将值与一个特定的键(key)相关联。这是一个叫做 map 的更通用的数据结构的特定实现。
对于标准库提供的其他类型的集合,请查看文档。
vec
Vec<T>
,也被称为 vector :
允许我们在一个单独的数据结构中储存多于一个的值,它在内存中彼此相邻地排列所有的值。
只能储存相同类型的值。它们在拥有一系列项的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。
创建 vec 的元素
Vec::new()
+ 类型注解(当完全不知道元素是什么类型时)vec![初始值]
``` // 因为没有向这个 vector 中插入任何值,Rust 并不知道我们想要储存什么类型的元素,所以需要类型注解。 let v1: Vec= Vec::new(); // 利用 vec! 宏,使用初始值来创建一个 Vec let v2 = vec![1, 2, 3];
## 修改 vec 的元素
1. `.push(追加一个元素)` 来追加(向末尾增加)一个元素
// 注意 vec 必须可变 let mut v = Vec::new(); // 追加相同类型的数据,此时类型已知,因此无需注解 v.push(5);
2. `IndexMut` trait (可变索引)使得直接通过索引的方式来修改 Vec 元素的值,就像 python 那样:
let mut vec = vec![1, 2, 3]; // 因为需要修改元素的值,所以必须可变 vec[0] = 0;
## 读取 vec 的一个元素
1. `&v[pos]`pos 从 0 开始,当超出索引(大于 vec 长度)时直接 panic
1. 使用`get`方法以索引作为参数来返回一个`Option<&T>`:`v.get(pos)`
根据“**在相同作用域中不能同时存在可变和不可变引用**”的所有权规则,不能在读取 vec 元素时,同时修改或增加 vec 元素。比如:
fn main() { let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
// error
v.push(6); // 此时 first 依然存在与作用域,未被释放,导致 不可变引用与可变引用并存
println!("The first element is: {}", first);
}
为什么第一个元素的引用会关心 vector 结尾的变化?不能这么做的原因是由于 vector 的工作方式:在 vector 的结尾增加新元素时,在没有足够空间将所有所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。我们可以及时使用不可变引用,让其作用域不与可变引用作用域冲突:
fn main() { let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
println!("The first element is: {}", first); // 此时 first 被使用了,离开作用域,不可变引用已经消失
// 可以运行
v.push(6);
println!("The vec is: {:?}", v);
}
关于 `Vec<T>` 类型的更多实现细节,在 [https://doc.rust-lang.org/stable/nomicon/vec.html](6ba2da622fe0ea14291f0e06e53565c3) 查看 “The Nomicon”
## 读取 vec 连续的元素
"slice":`&[]` + range
fn main() { let v = vec![1, 2, 3, 4, 5]; println!(“{:?}”, &v[0..1]); // [1] println!(“{:?}”, &v[..2]); // [1, 2] println!(“{:?}”, &v[3..]); // [4, 5] println!(“{:?}”, &v[..]); // [1, 2, 3, 4, 5] }
## 遍历 vec 所有元素
使用 `for`
// 遍历 vector 中元素的不可变引用 let v = vec![100, 32, 57]; for i in &v { println!(“{}”, i); } // 遍历 vector 中元素的可变引用 let mut v = vec![100, 32, 57]; for i in &mut v { // 为了修改可变引用所指向的值,在使用 += 运算符之前必须使用解引用运算符()获取 i 中的值 i += 50; }
## 使用枚举来储存多种类型
vector 只能储存相同类型的值。而枚举的成员都被定义为相同的枚举类型(实际上每个成员的值可以是单独的类型),所以当需要在 vector 中储存不同类型值时,我们可以定义并使用一个枚举!
fn main() {
#[derive(Debug)]
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
println!("{:?}", row);
// 同理,也可以使用数组类型
let arr: [SpreadsheetCell; 3] = [
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
println!("{:?}", arr);
}
Rust 在编译时就必须准确的知道 vector 中类型的原因在于它需要知道储存每个元素到底需要多少内存,因此必须知道存储什么类型。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。使用枚举外加 `match` 意味着 Rust 能在编译时就保证总是会处理所有可能的情况。<br />如果在编写程序时不能确切无遗地知道运行时会储存进 vector 的所有类型,枚举技术就行不通了。相反,你可以使用 trait 对象(猜测使用泛型)。
# String
## 说明
字符串就是作为字节的集合外加一些方法实现的,当这些字节被解释为文本时,这些方法提供了实用的功能。<br />当 Rustacean 们谈到 Rust 的 “字符串”时,通常指的是 `String` 和 "字符串 slice" `&str` 类型,而不仅仅是其中之一。`String` 和字符串 slice 都是 UTF-8 编码的。<br />除了这两种类型之外,Rust 标准库中还包含一系列其他字符串类型,比如 `OsString`、`OsStr`、`CString` 和 `CStr`。相关库 crate 甚至会提供更多储存字符串数据的选择。看到这些由 `String` 或是 `Str` 结尾的名字了吗?这对应着它们提供的所有权和可借用的字符串变体,就像是你之前看到的 `String` 和 `str`。举例而言,这些字符串类型能够以不同的编码,或者内存表现形式上以不同的形式,来存储文本内容。更多有关如何使用它们以及各自适合的场景,请参见其API文档。<br />索引 `String` 是很复杂的,由于人和计算机理解 `String` 数据方式的不同。<br />`String` 的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。<br />String 官方文档:[https://doc.rust-lang.org/std/string/struct.String.html#](https://doc.rust-lang.org/std/string/struct.String.html#)
## 创建 String
1.
创建空字符串: `String::new()`
1.
从 str 转换成 String:两种没有区别
1. `let s = "...".to_string();`
1. `let s = String::from("...");`
## 拼接 String
1.
向后追加:`push_str` 和 `push` 方法采用字符串 slice,并不需要获取参数的所有权
1. 字符串 slice:`s.push_str("...");` (注意用双引号)
1. 单个"字符 char":`s.push('a');`、`s.push('中');` (注意用单引号)
1.
从多个字符串拼接:
1.
`format!` 宏,不获取任何参数的所有权:`format!("{}-{}-{}", s1, s2, s3);`
1.
`+` 运算符:`s1 + &s2`
- 注意 `s1` 被移动了,不能继续使用:`+` 在此处的函数签名可以看成:`fn add(self, s: &str) -> String {}`,第一个参数不是 `&self`,因此获取了所有权;第二个是引用,没有获取所有权
- `s2` 类型是 `String`,引用类型是 `&String`,在此由于 Rust 的 **强制多态** (_deref coercion_ ),把 `&s2` 变成了 `&s2[..]`
- 如果继续拼接更多字符串,则继续使用引用 `s1 + &s2 + &s3`
## 不支持索引的原因
从 Rust 的角度来讲,事实上有三种相关方式可以理解字符串:字节 (bytes)、标量值 (scalar values) 和字形簇(grapheme clusters 最接近人们眼中“字母”概念)
1. 从字节角度看:`String` 是一个 `Vec<u8>` 的封装,其长度表示使用 UTF-8 编码所需要的字节数,而不是通常意义上的字符串长度。比如:
// 每一个字母的 UTF-8 编码都占用一个字节,因此返回 4 (字节) let len = String::from(“Hola”).len(); // 首字母是西里尔字母的 Ze 而不是阿拉伯数字 3 // 每个 Unicode 标量值需要两个字节存储,因此返回 24 (字节) let len = String::from(“Здравствуйте”).len();
当我们试图取索引的时候:
let hello = “Здравствуйте”; let answer = &hello[0];
`"З"` 的两个字节 [208, 151] 中,由于第一个字节 208 并不是一个有效的字母,但是它是 Rust 在字节索引 0 位置所能提供的唯一数据(即字节),而用户通常不会想要一个字节值被返回。假设规定按索引返回字节,针对 `"Hola"` ,第一个字节 104 对应了有效字母 `"H"`,而根据假设,索引 0 实际返回没什么用的 104,而不会返回第一个字母。为了避免返回意外的值并造成不能立刻发现的 bug,Rust 根本不会编译这些代码,并在开发过程中及早杜绝了误会的发生。
2. 从标量值角度看:Rust 的 `char` 类型对应了 Unicode 的标量值。
以梵文书写的印度语单词 `नमस्ते` 为例,最终它储存在 vector 中的 `u8` 值看起来像这样:
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, 224, 165, 135]
而拆分成 `char` 类型得到:`['न', 'म', 'स', '्', 'त', 'े']` (python 把这个单词拆开就是得到这样形式),有六个 `char`。不过类似于 [天城文](https://baike.baidu.com/item/%E5%A4%A9%E5%9F%8E%E6%96%87) 的语言文字属于 [元音附标文字](https://baike.baidu.com/item/%E5%85%83%E9%9F%B3%E9%99%84%E6%A0%87%E6%96%87%E5%AD%97/2606169),第四个和第六个都不是字母,它们是发音符号本身并没有任何意义。
3. 从字形簇的角度理解,`नमस्ते` 这个单词就会得到人们所说的构成这个单词的四个字母:`["न", "म", "स्", "ते"]`。从字符串中获取字形簇是很复杂的,所以标准库并没有提供这个功能。crates.io 上有些提供这样功能的 crate。
对于中文、英文等文字,`char` 类型和人们认知的字形簇是一致的。针对 `str` 和 `String` 类型的变量,使用 `char` 或 `char_indices` 方法即可得到 `char` 类型或其索引值。更多参考:[String#method.char_indices](https://doc.rust-lang.org/std/string/struct.String.html#method.char_indices)、[String#method.chars](https://doc.rust-lang.org/std/string/struct.String.html#method.chars)
fn main() { let hello = String::from(“Hello!”); let c = hello.chars(); println!(“{:?}”, c);
let hello = String::from("你好!");
let c = hello.chars();
println!("{:?}", c);
let hello = String::from("Здравствуйте");
let c = hello.chars();
println!("{:?}", c);
}
输出结果:
Chars([‘H’, ‘e’, ‘l’, ‘l’, ‘o’, ‘!’]) Chars([‘你’, ‘好’, ‘!’]) Chars([‘З’, ‘д’, ‘р’, ‘а’, ‘в’, ‘с’, ‘т’, ‘в’, ‘у’, ‘й’, ‘т’, ‘е’])
4. 还有一个原因 Rust 不允许使用索引获取 String 字符:索引操作预期总是需要常数时间 (O(1))。但是对于 `String` 不可能保证这样的性能,因为 Rust 必须从开头到索引位置遍历来确定有多少有效的字符。
## 复杂的字符串 slice
尽管不能通过索引来访问和操作 `String`,但是可以使用 `[]` 和一个 range 来创建含特定字节的字符串 slice。<br />这个 range 必须包含有效的字节,不能从单词中间“劈开”。
// 前面讲了,这个单词每个 unicode 标量值占 2 个字节
let hello = “Здравствуйте”;
// 第 1、2 个字节组合起来得到 З
let s = &hello[0..2];
// 而第 1 个字节并不能得到 char(或者 str ),因此下面的语句会 panic
// let s = &hello[0..1];
## 遍历字符串
使用 `for` 循环结构:
1.
使用 `chars` 方法,遍历 Unicode 的标量值
fn main() { for c in “नमस्ते”.chars() { println!(“{}”, c); } }
打印出:
न म स ् त े
1.
使用 `bytes` 方法,遍历原始字节,有效的 Unicode 标量值可能会由不止一个字节组成
fn main() { for b in “नमस्ते”.bytes() { println!(“{}”, b); } }
打印出组成 String 的 18 个字节:
224 164 // —snip— 165 135
---
总而言之,字符串还是很复杂的。不同的语言选择了不同的向程序员展示其复杂性的方式。Rust 选择了以准确的方式处理 `String` 数据作为所有 Rust 程序的默认行为,这意味着程序员们必须更多的思考如何预先处理 UTF-8 数据。这种权衡取舍相比其他语言更多的暴露出了字符串的复杂性,不过也使你在开发生命周期后期免于处理涉及非 ASCII 字符的错误。
# HashMap
`HashMap<K, V>`:
- 是一种类型,储存了一个键类型`K`对应一个值类型`V`的映射,可以用于需要任何类型作为键来寻找数据的情况,而不是像 vector 那样通过索引。
- 通过一个**哈希函数**(_hashing function_)来实现映射,决定如何将键和值放入内存中(数据储存在堆上)。
- 并没有被 prelude 自动引用,也没有内建的构建宏。
- 是同质的:所有的键必须是相同类型,值也必须都是相同类型。
## 创建 HashMap
1. 直接插入键值对:使用 `new` 创建一个空的 `HashMap`,并使用 `insert(key, value)` 或者 `entry(key).or_insert(value)` 方法增加元素
use std::collections::HashMap;
let mut scores = HashMap::new(); // 必须插入同类型的 key-value // insert:直接添加数据,遇到重复 key 时,新数据覆盖掉旧数据 scores.insert(String::from(“Blue”), 10); scores.insert(String::from(“Blue”), 50); // 插入多条相同 key 的数据,只保留最后一条数据 // entry:key 不存在时添加数据;遇到重复 key 不添加数据 scores.entry(String::from(“Blue”)).or_insert(80); // 添加成功 scores.entry(String::from(“Red”)).or_insert(80); // 不会更新这条数据
- 当 `insert` 或者 `entry` 将拥有所有权的变量的值移动到哈希 map 中后,将获取它们的所有权;而对于具有 Copy trait 的变量,将值拷贝进 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 被 moved
-
如果将值的引用插入哈希 map,这些值本身将不会被移动进哈希 map。但是这些引用指向的值必须至少在哈希 map 有效时也是有效的。#todo:生命周期与引用有效性#
-
`entry` 的 `or_insert` 方法在键对应的值存在时就返回这个值的**可变引用**,如果不存在则将参数作为新值插入并返回新值的可变引用。这比编写自己的逻辑要简明的多,另外也与借用检查器结合得更好。
2. 从列表转成键值对:key 列表使用 `zip` 方法与 value 列表打包在一起形成元组,使用 `collect` 方法转化成 `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();
这里 `HashMap<_, _>` 类型注解是必要的,因为可能 `collect` 很多不同的数据结构(包括之前说的 `vec`),而除非显式指定否则 Rust 无从得知你需要的类型。但是对于键和值的类型参数来说,可以使用下划线占位,而 Rust 能够根据 vector 中数据的类型推断出 `HashMap` 所包含的类型。
## 读取 HashMap 的值
使用 `get(&key)` 或 `get_mut(&key)` 方法获取特定键的值,返回的是 `Option` 类型:
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); // Some(10)
## 修改 HashMap 的值
1. 如果支持 `IndexMut` ,那么我们可以"像 Vec 类型那样"通过索引(HashMap 里面就是 key)来修改值。
然而对于 map 类型的数据,Rust 官方打算实现一个`IndexSet` trait 来做到更加简洁的操作(关注 [Issue: Future-proof indexing on maps: remove IndexMut](https://github.com/rust-lang/rust/pull/23559))。<br />所以目前可以使用 `get_mut(&key)` 获取 HashMap value 的可变引用,利用解引用修改值:
let v = map.get_mut(&key).unwrap(); v = new_value; // 或者等价地 map.get_mut(&key).unwrap() = new_value;
2. 根据 `entry(key).or_insert(value)` " 返回值是可变引用的特殊性 ",所以同样利用可变性和解引用来修改值:
let v = entry(key).or_insert(value); // v 的值是一个可变引用 &mut value_type,所以 v 无需变成 mut v = new_value; // 或者等价地 entry(key).or_insert(value) = new_value;
例子:利用 HasMap 统计单词出现的数量:
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;
// 上下等价
*map.entry(word).or_insert(0) += 1;
}
println!("{:?}", map);
// 简单地修改某个 key 的 value
// *map.get_mut("hello").unwrap() = 0;
// println!("{:?}", map);
}
## 遍历 HashMap
使用 `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 { // 如果不使用引用,则 scores 直接被 borrowed & moved println!(“{}: {}”, key, value); // 以任意顺序打印出每一个键值对 }
```
更换 hash 算法
HashMap 默认使用一种 “密码学安全的”(cryptographically strong) 哈希函数(具体见此处链接),它可以抵抗拒绝服务(Denial of Service, DoS)攻击。然而这并不是可用的最快的算法,不过为了更高的安全性值得付出一些性能的代价。
如果性能监测显示此哈希函数非常慢,以致于你无法接受,你可以指定一个不同的 hasher 来切换为其它函数。hasher 是一个实现了 BuildHasher
trait 的类型。#todo: trait# 你并不需要从头开始实现你自己的 hasher;crates.io 有其他人分享的实现了许多常用哈希算法的 hasher 的库。