参考:https://kaisery.github.io/trpl-zh-cn/ch08-00-common-collections.html
集合collections ):
Rust 标准库中提供的一系列非常有用的数据结构。
大部分其他数据类型都代表一个特定的值,不过集合可以包含多个值。
不同于内建的数组和元组类型,这些集合指向的数据是储存在堆上的,这意味着数据的数量不必在编译时就已知,并且还可以随着程序的运行增长或缩小
每种集合都有着不同功能和成本,而根据当前情况选择合适的集合,这是一项应当逐渐掌握的技能。

  • vector 允许我们一个挨着一个地储存一系列数量可变的值
  • 字符串string )是字符的集合。我们之前见过 String 类型,不过在本章我们将深入了解。
  • 哈希 maphash map )允许我们将值与一个特定的键(key)相关联。这是一个叫做 map 的更通用的数据结构的特定实现。

对于标准库提供的其他类型的集合,请查看文档

vec

Vec<T>,也被称为 vector
允许我们在一个单独的数据结构中储存多于一个的值,它在内存中彼此相邻地排列所有的值。
只能储存相同类型的值。它们在拥有一系列项的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。

创建 vec 的元素

  1. Vec::new()+ 类型注解(当完全不知道元素是什么类型时)
  2. vec![初始值] ``` // 因为没有向这个 vector 中插入任何值,Rust 并不知道我们想要储存什么类型的元素,所以需要类型注解。 let v1: Vec = Vec::new(); // 利用 vec! 宏,使用初始值来创建一个 Vec let v2 = vec![1, 2, 3];
  1. ## 修改 vec 的元素
  2. 1. `.push(追加一个元素)` 来追加(向末尾增加)一个元素

// 注意 vec 必须可变 let mut v = Vec::new(); // 追加相同类型的数据,此时类型已知,因此无需注解 v.push(5);

  1. 2. `IndexMut` trait (可变索引)使得直接通过索引的方式来修改 Vec 元素的值,就像 python 那样:

let mut vec = vec![1, 2, 3]; // 因为需要修改元素的值,所以必须可变 vec[0] = 0;

  1. ## 读取 vec 的一个元素
  2. 1. `&v[pos]`pos 0 开始,当超出索引(大于 vec 长度)时直接 panic
  3. 1. 使用`get`方法以索引作为参数来返回一个`Option<&T>``v.get(pos)`
  4. 根据“**在相同作用域中不能同时存在可变和不可变引用**”的所有权规则,不能在读取 vec 元素时,同时修改或增加 vec 元素。比如:

fn main() { let mut v = vec![1, 2, 3, 4, 5];

  1. let first = &v[0];
  2. // error
  3. v.push(6); // 此时 first 依然存在与作用域,未被释放,导致 不可变引用与可变引用并存
  4. println!("The first element is: {}", first);

}

  1. 为什么第一个元素的引用会关心 vector 结尾的变化?不能这么做的原因是由于 vector 的工作方式:在 vector 的结尾增加新元素时,在没有足够空间将所有所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。我们可以及时使用不可变引用,让其作用域不与可变引用作用域冲突:

fn main() { let mut v = vec![1, 2, 3, 4, 5];

  1. let first = &v[0];
  2. println!("The first element is: {}", first); // 此时 first 被使用了,离开作用域,不可变引用已经消失
  3. // 可以运行
  4. v.push(6);
  5. println!("The vec is: {:?}", v);

}

  1. 关于 `Vec<T>` 类型的更多实现细节,在 [https://doc.rust-lang.org/stable/nomicon/vec.html](6ba2da622fe0ea14291f0e06e53565c3) 查看 “The Nomicon”
  2. ## 读取 vec 连续的元素
  3. "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] }

  1. ## 遍历 vec 所有元素
  2. 使用 `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; }

  1. ## 使用枚举来储存多种类型
  2. vector 只能储存相同类型的值。而枚举的成员都被定义为相同的枚举类型(实际上每个成员的值可以是单独的类型),所以当需要在 vector 中储存不同类型值时,我们可以定义并使用一个枚举!

fn main() {

  1. #[derive(Debug)]
  2. enum SpreadsheetCell {
  3. Int(i32),
  4. Float(f64),
  5. Text(String),
  6. }
  7. let row = vec![
  8. SpreadsheetCell::Int(3),
  9. SpreadsheetCell::Text(String::from("blue")),
  10. SpreadsheetCell::Float(10.12),
  11. ];
  12. println!("{:?}", row);
  13. // 同理,也可以使用数组类型
  14. let arr: [SpreadsheetCell; 3] = [
  15. SpreadsheetCell::Int(3),
  16. SpreadsheetCell::Text(String::from("blue")),
  17. SpreadsheetCell::Float(10.12),
  18. ];
  19. println!("{:?}", arr);

}

  1. Rust 在编译时就必须准确的知道 vector 中类型的原因在于它需要知道储存每个元素到底需要多少内存,因此必须知道存储什么类型。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。使用枚举外加 `match` 意味着 Rust 能在编译时就保证总是会处理所有可能的情况。<br />如果在编写程序时不能确切无遗地知道运行时会储存进 vector 的所有类型,枚举技术就行不通了。相反,你可以使用 trait 对象(猜测使用泛型)。
  2. # String
  3. ## 说明
  4. 字符串就是作为字节的集合外加一些方法实现的,当这些字节被解释为文本时,这些方法提供了实用的功能。<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#)
  5. ## 创建 String
  6. 1.
  7. 创建空字符串: `String::new()`
  8. 1.
  9. str 转换成 String:两种没有区别
  10. 1. `let s = "...".to_string();`
  11. 1. `let s = String::from("...");`
  12. ## 拼接 String
  13. 1.
  14. 向后追加:`push_str` `push` 方法采用字符串 slice,并不需要获取参数的所有权
  15. 1. 字符串 slice`s.push_str("...");` (注意用双引号)
  16. 1. 单个"字符 char"`s.push('a');``s.push('中');` (注意用单引号)
  17. 1.
  18. 从多个字符串拼接:
  19. 1.
  20. `format!` 宏,不获取任何参数的所有权:`format!("{}-{}-{}", s1, s2, s3);`
  21. 1.
  22. `+` 运算符:`s1 + &s2`
  23. - 注意 `s1` 被移动了,不能继续使用:`+` 在此处的函数签名可以看成:`fn add(self, s: &str) -> String {}`,第一个参数不是 `&self`,因此获取了所有权;第二个是引用,没有获取所有权
  24. - `s2` 类型是 `String`,引用类型是 `&String`,在此由于 Rust **强制多态** _deref coercion_ ),把 `&s2` 变成了 `&s2[..]`
  25. - 如果继续拼接更多字符串,则继续使用引用 `s1 + &s2 + &s3`
  26. ## 不支持索引的原因
  27. Rust 的角度来讲,事实上有三种相关方式可以理解字符串:字节 (bytes)、标量值 (scalar values) 和字形簇(grapheme clusters 最接近人们眼中“字母”概念)
  28. 1. 从字节角度看:`String` 是一个 `Vec<u8>` 的封装,其长度表示使用 UTF-8 编码所需要的字节数,而不是通常意义上的字符串长度。比如:

// 每一个字母的 UTF-8 编码都占用一个字节,因此返回 4 (字节) let len = String::from(“Hola”).len(); // 首字母是西里尔字母的 Ze 而不是阿拉伯数字 3 // 每个 Unicode 标量值需要两个字节存储,因此返回 24 (字节) let len = String::from(“Здравствуйте”).len();

  1. 当我们试图取索引的时候:

let hello = “Здравствуйте”; let answer = &hello[0];

  1. `"З"` 的两个字节 [208, 151] 中,由于第一个字节 208 并不是一个有效的字母,但是它是 Rust 在字节索引 0 位置所能提供的唯一数据(即字节),而用户通常不会想要一个字节值被返回。假设规定按索引返回字节,针对 `"Hola"` ,第一个字节 104 对应了有效字母 `"H"`,而根据假设,索引 0 实际返回没什么用的 104,而不会返回第一个字母。为了避免返回意外的值并造成不能立刻发现的 bugRust 根本不会编译这些代码,并在开发过程中及早杜绝了误会的发生。
  2. 2. 从标量值角度看:Rust `char` 类型对应了 Unicode 的标量值。
  3. 以梵文书写的印度语单词 `नमस्ते` 为例,最终它储存在 vector 中的 `u8` 值看起来像这样:

[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, 224, 165, 135]

  1. 而拆分成 `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),第四个和第六个都不是字母,它们是发音符号本身并没有任何意义。
  2. 3. 从字形簇的角度理解,`नमस्ते` 这个单词就会得到人们所说的构成这个单词的四个字母:`["न", "म", "स्", "ते"]`。从字符串中获取字形簇是很复杂的,所以标准库并没有提供这个功能。crates.io 上有些提供这样功能的 crate
  3. 对于中文、英文等文字,`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);

  1. let hello = String::from("你好!");
  2. let c = hello.chars();
  3. println!("{:?}", c);
  4. let hello = String::from("Здравствуйте");
  5. let c = hello.chars();
  6. println!("{:?}", c);

}

  1. 输出结果:

Chars([‘H’, ‘e’, ‘l’, ‘l’, ‘o’, ‘!’]) Chars([‘你’, ‘好’, ‘!’]) Chars([‘З’, ‘д’, ‘р’, ‘а’, ‘в’, ‘с’, ‘т’, ‘в’, ‘у’, ‘й’, ‘т’, ‘е’])

  1. 4. 还有一个原因 Rust 不允许使用索引获取 String 字符:索引操作预期总是需要常数时间 (O(1))。但是对于 `String` 不可能保证这样的性能,因为 Rust 必须从开头到索引位置遍历来确定有多少有效的字符。
  2. ## 复杂的字符串 slice
  3. 尽管不能通过索引来访问和操作 `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];

  1. ## 遍历字符串
  2. 使用 `for` 循环结构:
  3. 1.
  4. 使用 `chars` 方法,遍历 Unicode 的标量值

fn main() { for c in “नमस्ते”.chars() { println!(“{}”, c); } }

  1. 打印出:

न म स ् त े

  1. 1.
  2. 使用 `bytes` 方法,遍历原始字节,有效的 Unicode 标量值可能会由不止一个字节组成

fn main() { for b in “नमस्ते”.bytes() { println!(“{}”, b); } }

  1. 打印出组成 String 18 个字节:

224 164 // —snip— 165 135

  1. ---
  2. 总而言之,字符串还是很复杂的。不同的语言选择了不同的向程序员展示其复杂性的方式。Rust 选择了以准确的方式处理 `String` 数据作为所有 Rust 程序的默认行为,这意味着程序员们必须更多的思考如何预先处理 UTF-8 数据。这种权衡取舍相比其他语言更多的暴露出了字符串的复杂性,不过也使你在开发生命周期后期免于处理涉及非 ASCII 字符的错误。
  3. # HashMap
  4. `HashMap<K, V>`
  5. - 是一种类型,储存了一个键类型`K`对应一个值类型`V`的映射,可以用于需要任何类型作为键来寻找数据的情况,而不是像 vector 那样通过索引。
  6. - 通过一个**哈希函数**(_hashing function_)来实现映射,决定如何将键和值放入内存中(数据储存在堆上)。
  7. - 并没有被 prelude 自动引用,也没有内建的构建宏。
  8. - 是同质的:所有的键必须是相同类型,值也必须都是相同类型。
  9. ## 创建 HashMap
  10. 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); // 不会更新这条数据

  1. - `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

  1. -
  2. 如果将值的引用插入哈希 map,这些值本身将不会被移动进哈希 map。但是这些引用指向的值必须至少在哈希 map 有效时也是有效的。#todo:生命周期与引用有效性#
  3. -
  4. `entry` `or_insert` 方法在键对应的值存在时就返回这个值的**可变引用**,如果不存在则将参数作为新值插入并返回新值的可变引用。这比编写自己的逻辑要简明的多,另外也与借用检查器结合得更好。
  5. 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();

  1. 这里 `HashMap<_, _>` 类型注解是必要的,因为可能 `collect` 很多不同的数据结构(包括之前说的 `vec`),而除非显式指定否则 Rust 无从得知你需要的类型。但是对于键和值的类型参数来说,可以使用下划线占位,而 Rust 能够根据 vector 中数据的类型推断出 `HashMap` 所包含的类型。
  2. ## 读取 HashMap 的值
  3. 使用 `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)

  1. ## 修改 HashMap 的值
  2. 1. 如果支持 `IndexMut` ,那么我们可以"像 Vec 类型那样"通过索引(HashMap 里面就是 key)来修改值。
  3. 然而对于 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;

  1. 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;

  1. 例子:利用 HasMap 统计单词出现的数量:

fn main() { use std::collections::HashMap;

  1. let text = "hello world wonderful world";
  2. let mut map = HashMap::new();
  3. for word in text.split_whitespace() {
  4. // let count = map.entry(word).or_insert(0);
  5. // *count += 1;
  6. // 上下等价
  7. *map.entry(word).or_insert(0) += 1;
  8. }
  9. println!("{:?}", map);
  10. // 简单地修改某个 key 的 value
  11. // *map.get_mut("hello").unwrap() = 0;
  12. // println!("{:?}", map);

}

  1. ## 遍历 HashMap
  2. 使用 `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 的库。