常用的集合
前言
Rust 笔记也做到集合部分了,强烈推荐 rustling 工具,配合官方文档或者本系列笔记作为练习题是相当不错的。
Vector
可以使用 Vector 存储多个值:
Vec<T>
,称为 Vector。- 其由标准库提供。
- 可以存储多个值。
- 只能存储相同类型的数据。
- 值在内存中连续存放。
使用 Vec::new 创建 Vector:
let v: Vec<i32> = Vec::new();
,由于 new 创建的 vec 是没有元素的,需要显式的指定 vec 类型。let v2 = vec![1, 2, 3];
,使用初始值创建 Vector,通过 vec! 宏。
更新 Vector:
- 向 Vector 添加元素,使用 push 方法。
let mut v = Vec::new(); v.push(1); // 添加完元素 编译器可以推断出当前 vector 的类型,可以不再显式声明。
删除 Vector:
- 与任何其他 struct 一样,当 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(num) => println!("The third element is {}", num), None => println!("There is no third element"), }
索引与 get 在处理访问越界时是有区别的:
- 索引会引起 panic。
- get 则会返回 None。
所有权和借用规则
- 不能在同一作用域内同时拥有可变和不可变引用。
let mut v = vec![1, 2, 3, 4, 5]; let third = &v[2]; v.push(6); // 这时存在对 v 对可变与不可变借用(third)会报错 println!("The third element is {}", third); v.push(6); // 这时不可变应用 third 后续没有再进行使用,因此可以正常的可变借用
遍历 Vector:
let mut v = vec![100, 32, 57]; for i in &mut v { println!("{}", i); *i += 50; } for i in v { println!("{}", i); }
Vector 例子
使用 enum 来存储多种数据类型。
- enum 的变体可以附加不同类型的数据。
- enum 的变体定义在同一个 enum 类型下。
enum SpreadssheetCell { Int(i32), Float(f64), Text(String), } fn test_vector_enum() { let _row = vec![ SpreadssheetCell::Int(3), SpreadssheetCell::Text(String::from("blue")), SpreadssheetCell::Float(10.12), ]; }
String
Rust 中的字符串困难点:
- Rust 倾向于暴露可能的错误。
- 字符串数据结构复杂。
- 默认是使用 UTF-8 编码。
在 Rust 的核心语言层面,只提供来一个字符串类型:字符串切片 str(或 &str)。
- 字符串切片:对于存储在其他地方、UTF-8 编码的字符串引用。
- 字符串字面值:存储在二进制文件中,也就是字符串切片。
- 字符串切片:对于存储在其他地方、UTF-8 编码的字符串引用。
String 类型来自标准库,不是核心语言层。其特点是可增长、可修改、可拥有的字符串数据管理结构,也采用 UTF-8 编码。
其他字符串类型
- Rust 的标准库中还包括其他的字符串类型,例如:OsString、OsStr、CString、CStr 等。
- String 或 Str 后缀是指,
拥有
或借用
的变体。 - 可存储不同编码的问题或在内存中以不同的形式展现。
- String 或 Str 后缀是指,
String 使用
创建字符串:
- 很多 Vec
的操作都可以作用于 String。 - String::new() 函数:
let mut s = String::new();
。
- 很多 Vec
使用初始值来创建 String:
to_string()
方法,可用于实现来 Display trait 的类型,包括字符串字面值。
let data = "initial contents"; let _s = data.to_string(); let _s1 = "initial contents".to_string();
String::from()
函数从字面值创建 String。
更新 String:
push_str()
方法:把一个字符串切片附加到 String。
let mut s = String::from("foo"); s.push_str("bar"); //foobar let s1 = "bar2".to_string(); s.push_str(&s1); // 仅借用 s1 不会获取其所有权,后续可继续使用
push()
方法:将单个字符附加到 String。example:s.push('l');
。+
运算符:拼接字符串。
let s1 = "Hello, ".to_string(); let s2 = "World!".to_string(); let s3 = s1 + &s2; // 这里 s1 需要是 String,s2 是要求字符串切片或 String 类型引用。 /* println!("s1 = {}", s1); s1 已经被 moved,因此不能再使用 error[E0382]: borrow of moved value: `s1` --> src/main.rs:25:25 | 20 | let s1 = "Hello, ".to_string(); | -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait ... 23 | let s3 = s1 + &s2; // 这里 s1 需要是 String,s2 是要求字符串切片或 String 类型引用。 | -- value moved here 24 | 25 | println!("s1 = {}", s1); | ^^ value borrowed here after move */ println!("s2 = {}", s2); println!("s3 = {}", s3);
format!
宏:连接多个字符串,它不会获取其他字符串的所有权,会返回一个全新的字符串:
let s1 = String::from("tic"); let s2 = String::from("tac"); let s3 = String::from("toe"); //let s3 = s1 + "-" + &s2 + "-" + &s3; //print!("{}", s3); let s = format!("{}-{}-{}", s1, s2, s3); print!("{}", s);
String 访问
按索引的形式对 String 进行访问是不允许的。example:
s[0]
。String 内部表示,其实是对 Vec
的包装。又一个 len 方法可以返回 String 的长度(所占的字节数)。 let len = String::from("hola").len(); //Unicode 标量值往往不是一个字节表示,对于中文或者其他文字可能两个或三个字节表示一个文字。 // 你好。len() 可能返回 4,这时通过索引进行访问就存在问题了。 println!("{}", len);
字节(Bytes)、标量值(Scalar Values)、字形簇(Grapheme Clusters)
Rust 有三种看待字符串的方式:
- 字节:String.bytes(),最基本的字节数据。
- 标量值:String.Chars()
- 字形簇(最接近单个语言字符的):标准库是不支持直接字形簇。
Rust 不允许对 String 进行索引的最后一个原因:
- 索引操作应消耗一个常量时间(O(1))。
- 而 String 无法保证:需要遍历所有内容,来确定有多少个合法字符。
切割 String
可以使用
[]
和一个范围
来创建字符串的切片。- 注意需要按照字符的边界进行,如果切割在 unicode 中间则会引起 panic。
let hello = "Здравствуйте"; let s = &hello[0..4]; println!("{}", s); //Зд /* thread 'main' panicked at 'byte index 3 is not a char boundary; it is inside 'д' (bytes 2..4) of `Здравствуйте`' , src/main.rs:69:14 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace */ let s = &hello[0..3];
HashMap<K, V>
- 键值对的形式存储数据,一个键(key)对应一个值(Value)。
- 其内部主要使用 Hash 函数:决定如何在内存中存放 K 和 V。
创建 HashMap
创建空 HashMap: new() 函数。
添加数据:insert() 方法。
HashMap 不在预导入模块中,因此需要使用 use 进行导入。标准库对其支持比较少,没有内置宏来进行创建。
HashMap 数据是存储在 Heap 上的。
HashMap 是同构的:
- 所有的 K 必须是同一种类型。
- 所有的 V 必须是同一种类型。
use std::collections::HashMap; fn main() { let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); }
另一种创建 HashMap 的方式:collect 方法。
- 在元素类型为 Tuple 的 Vector 上使用 collect 方法,可以创建一个 HashMap:
- 要求 Tuple 有两个值:一个作为 K,一个作为 V。
- collect 方法可以把数据整合程很多种类型,包括 HashMap,因此返回值需要显式的指明类型。
let teams = vec![String::from("Blue"), String::from("Yellow")]; let intial_scores = vec![10, 50]; // 使用 zip (拉链)来创建一个 tuple let _scores: HashMap<_, _> = teams.iter().zip(intial_scores.iter()).collect();
- 在元素类型为 Tuple 的 Vector 上使用 collect 方法,可以创建一个 HashMap:
HashMap 和所有权
对于实现了 Copy trait 的类型(例如 i32),值会被复制到 HashMap 中。
对于拥有所有权的值(例如 String),值会被移动,所有权会转移给 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); /* error[E0382]: borrow of moved value: `field_name` --> src/main.rs:24:24 | 18 | let field_name = String::from("Favorite color"); | ---------- move occurs because `field_name` has type `String`, which does not implement the `Copy` trait ... 22 | map.insert(field_name, field_value); | ---------- value moved here 23 | 24 | println!("{}: {}", field_name, field_value); | ^^^^^^^^^^ value borrowed here after move | */ //println!("{}: {}", field_name, field_value);
如果将值的引用插入到 HashMap,值本身不会移动。
- 在 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); println!("{}: {}", field_name, field_value);
访问 HashMap 中的值
get 方法:
- 参数:K
- 返回:
Option<&V>
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); match score { Some(s) => println!("{}", s), None => println!("team not exit"), }
遍历 HashMap
- for 循环:
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
for (k, v) in &scores {
println!("{}: {}", k, v);
}
更新 HashMap<K, V>
HashMap 大小可变,但是每个 K 同时只能对应一个 V。
更新 HashMap 中的数据:
- K 已经存在,对应一个 V:
- 替换现有 V。
- 保留现有 V,忽略新的 V。
- 合并现有 V 和新的 V。
- K 不存在:
- 添加一对 K,V。
- K 已经存在,对应一个 V:
覆盖现有的 V:
let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Blue"), 50); println!("{:?}", scores); //{"Blue": 50}
只在 K 不对应任何值的情况下,才插入 V:
entry 方法:检查指定的 K 是否有对应一个 V。
- 参数为 K。
- 返回 enum Entry:代表值是否存在。
entry 的
or_insert()
方法:- 返回:
- 如果 K 存在,返回到对应 V 的一个可变引用。
- 如果 K 不存在,将方法参数作为 K 的新值插入进去,返回到这个值的可变引用。
let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); //scores.entry(String::from("Yellow")).or_insert(50); let e = scores.entry(String::from("Yellow")); println!("{:?}", e); //Entry(VacantEntry("Yellow")) e.or_insert(50); let e = scores.entry(String::from("Blue")); println!("{:?}", e); //Entry(OccupiedEntry { key: "Blue", value: 10, .. }) e.or_insert(50); println!("{:?}", scores); //{"Blue": 10, "Yellow": 50}
基于现有 V 来更新 V。
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); // 如果 K 存在,返回到对应 V 的一个可变引用。 *count += 1; } println!("{:?}", map); //{"wonderful": 1, "world": 2, "hello": 1}
Hash 函数
- 默认情况下,HashMap 使用加密功能强大的 Hash 函数,可以抵抗拒绝服务(DOS)攻击,所以不是可用的最快 Hash 算法。
- 具有更好的安全性。
- 可以指定不同的 hasher 来切换到另一个函数。
- hasher 是实现 BuildHasher trait 类型。