Rust 程序中有如下三种被广泛使用的集合:
- vector 允许连续地储存一系列数量可变的值
- 字符串(string)是一个字符的集合。
- 哈希 map(hash map)允许我们将值与一个特定的键(key)相关联。是一个叫做 map 的更通用的数据结构的特定实现。
这些常用的集合类型都是存储在堆上的。本部分内容每一小节的结构都类型,均包括:类型的特点、创建、更新、删除/丢弃、查询/索引、遍历等,当然这部分内容很少,只是皮毛,其余的内容需要在实践中慢慢地接触。
Vector
新建
let v: Vec<i32> = Vec::new();
类型 Vec<T>
,也被称为 vector,i32
是类型注解,在使用初始值创建时,可以推断出类型而不用特意指定。
let v = vec![1, 2, 3];
vec!
宏会根据我们提供的值来创建一个新的 Vec
。
更新
当 vector 变量为 mut
时,可以通过 push()
方法来新增元素,pop()
方法来弹出元素。
销毁
离开作用域后 vector 会自动销毁。
读取
读取 vector 中的值需要使用引用,有两种方法:
&v[i]
索引语法得到一个元素的引用v.get(i)
方法:get
方法以索引作为参数来返回一个Option<&T>
。
注意事项 1:相同作用域中同时存在可变和不可变引用行不通
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
为什么第一个元素的引用会关心 vector 结尾的变化?
不能这么做的原因是由于 vector 的工作方式:在 vector 的结尾增加新元素时,在没有足够空间将所有所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,第一个元素的引用就指向了被释放的内存。
遍历
for i in &v
for i in &mut v
String
字符串就是作为字节的集合外加一些方法实现的。Rust 的核心语言中只有一种字符串类型:str
,字符串 slice,它通常以被借用的形式出现,&str
。
字符串 slice 是一些储存在别处的 UTF-8 编码字符串数据的引用。
称作 String
的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。
新建
// 1 ::new() 空字符串,记得加上 mut
let mut s1 = String::new();
// 2 to_string()
let data = "initial contents";
let s2 = data.to_string();
// 3 ::from()
let mut s3 = String::from("ss");
更新
/// 1 使用 push_str 附加字符串 slice,所以 push_str 并没有获取传入字符串的所有权
let mut s1 = String::from("foo");
let s2 = "bar";
s1.push_str(s2);
/// 2 使用 push 将一个字符加入 String 值中
let mut s = String::from("lo");
s.push('l');
// 3 使用 + 运算符或 format! 宏拼接字符串
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
// 注意 s1 被移动了,不能继续使用,+ 运算符使用了 add 函数
// 所以 s1 被移动及 s2 需要使用引用是必须的
// 之所以能够在 add 调用中使用 &s2 是因为 &String 可以被 强转(coerced)成 &str。
// 当 add 函数被调用时,Rust 使用了一个被称为解引用强制多态(deref coercion)的技术,你可以将其理解为它把 &s2 变成了 &s2[..]
let s3 = s1 + &s2;
/// 3 需要级联多个字符串时,使用 format! 宏
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{}-{}-{}", s1, s2, s3);
索引
Rust 中不支持通过索引 i
来访问字符串中的字符。为什么呢?
String
是一个 Vec<u8>
的封装,且使用 UTF-8 编码,所以一个索引不能对应一个有效的 Unicode 标量值,也就是说 Unicode 标量值可能不只使用一个字节,这与其它 ASCII 编码的语言是不一样的。
从 Rust 的角度来讲,事实上有三种相关方式可以理解字符串:字节、标量值和字形簇(最接近人们眼中 字母 的概念)。
也就是说,Rust 中,字节和有效字符是不能一一对应的,而索引操作预期总是需要常数时间 (O(1)),这对于 String
不可能保证这样的性能,因为 Rust 必须从开头到索引位置遍历来确定有多少有效的字符。
同样,与索引类似,在字符串 slice 中也遇到了同样的问题:无法根据有效字符的索引来创建字符串 slice。还是因为字节和有效字符不是一一对应的,因此 Rust 中使用 []
来创建 slice 时,里面的数字指的是字节。
遍历
如果你需要操作单独的 Unicode 标量值,最好的选择是使用 chars()
方法,而 bytes()
方法则返回字节。
Hash Map
HashMap<K, V>
类型储存了一个键类型 K
对应一个值类型 V
的映射。它通过一个 哈希函数(hashing function)来实现映射,决定如何将键和值放入内存中。
新建
use std::collections::HashMap;
/// 1 new()
let mut scores = HashMap::new();
// insert() 方法插入键值对
// 所有的键必须是相同类型,值也必须都是相同类型。
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
/// 2 用队伍列表和分数列表创建哈希 map
let teams = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];
// 使用 zip() 方法来创建一个元组的 vector,再使用 collect() 方法基于元组创建 map
let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
注意 map 中值的所有权:
对于像
i32
这样的实现了Copy
trait 的类型,其值可以拷贝进哈希 map。对于像String
这样拥有所有权的值,其值将被移动而哈希 map 会成为这些值的所有者。
访问
get
方法并提供对应的键来从哈希 map 中获取值,get
返回 Option<V>
。
遍历
for (key, value) in &scores {
println!("{}: {}", key, value);
}
更新
更新时,主要存在如下三种情况:
替换已存在的值。再调用一次 insert()
即可。
只在键没有对应值时插入。使用 entry()
方法,它的返回值是一个枚举,Entry
,它代表了可能存在也可能不存在的值。
根据旧值更新一个值。entry()
方法会返回存在的值的可变引用,可以据此来更新旧值。
/// 1 替换已存在的值
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);
/// 2 只在键没有对应值时插入,存在时则返回对应的值的可变引用,此时可以对值进行修改
scores.entry(String::from("Yellow")).or_insert(50);
/// 3 根据旧值更新一个值
let score = scores.entry(String::from("Blue")).or_insert(50);
*score += 10;
HashMap
默认使用一种密码学安全的(cryptographically strong)siphash 哈希函数,可以通过指定一个不同的 hasher 来切换为其它函数。hasher 是一个实现了 BuildHasher
trait 的类型。第十章会讨论 trait 和如何实现它们。