本文是 Rust 学习笔记系列第六篇,参考 Rust 指南的第 8 章,涉及向量(vector)、字符串(string)和哈希表(hash map)。
使用向量储存一组数据
首先我们介绍向量类型 Vec<T>
。向量允许我们在一个数据结构中存储多个值,并且多个值存储在邻接的内存中。向量只能存储相同类型的值。
创建新的向量
1 | // 因为无法推断元素的类型,要注明类型 |
修改向量
1 | // 可以从下面的方法调用推导元素类型,所以此处可以省略类型标注 |
销毁向量的同时会销毁元素
和其他的结构体一样,向量在脱离作用域时,会被销毁。
当向量被销毁时,存储在里面的值也会被销毁,当你引用了一些元素时,这会变的比较复杂。
读取向量的元素
1 | let v = vec![1, 2, 3, 4, 5]; |
对向量元素的引用仍然满足之前所介绍的所有权与借用规则。
1 | let mut v = vec![1, 2, 3, 4, 5]; |
这段代码看起来好像没有问题,向向量中添加一个元素,怎么会影响第一个元素呢?这与向量的实现方式有关系,向量添加元素时,可能会需要分配新的内存地址,并且将所有元素都拷贝过去,这时候对任意元素的引用都将会出问题。
遍历向量的元素
使用 for 循环可以获取向量每个元素的不可变引用:
1 | let v = vec![100, 32, 57]; |
也可以对可变向量使用 for 循环,获取可变引用:
1 | // 需要是可变向量 |
配合枚举来存储多种类型的数据
1 | enum SpreadsheetCell { |
使用字符串存储 UTF-8 编码的文本
Rust 新手通常会卡在字符串上,因为:Rust 倾向于暴露所有可能的问题,字符串类型比预期更加复杂,以及 UTF-8 编码,这三个问题揉在一起,会让问题看起来很复杂。
我们在介绍集合类型时介绍字符串,因为字符串被设计成是字节的数组,加上一些以将字节解释为文本为基础来提供功能的方法。
什么是字符串
首先我们需要定义清楚什么是“字符串”。Rust 在语言核心中只有一种字符串类型:字符串切片 str
,通常以借用的形式 &str
出现。之前我们介绍了字符串切片,它是对储存在其他地方的 UTF-8 编码的字符串数据的引用。字符串字面量是字符串切片的一种,存储在程序的二进制文件中。Rust 的标准库中的 String
类型,是一种可变的、有所有权的 UTF-8 编码的字符串类型。Rust 开发者提及字符串时,通常指这两者,而不是其中一种。
Rust 标准库中也有一些其他的字符串类型,比如 OsString
、OsStr
、CString
和 CStr
。其他的库可能会提供更多的字符串存储方案。
创建字符串
1 | // 创建空的可变字符串 |
因为字符串是 UTF-8 编码的,我们可以放入任何正确编码的文本数据,比如中文、Unicode 表情符号等。
修改字符串
使用 push_str
和 push
追加文本
1 | let mut s = String::from("foo"); |
使用 +
操作符或者 format!
宏拼接文本
1 | let s1 = String::from("Hello, "); |
s1 要用移动、s2 要用引用的原因在于 +
操作符使用了字符串的 add
方法,而该方法的签名是:
1 | fn add(self, s: &str) -> String {} |
实际在标准库中,
add
方法是使用范型定义的。
1 | let s1 = String::from("tic"); |
使用下标访问字符串
在很多其他语言中,使用下标访问字符串的字符是有效的且很常见的。但是在 Rust 中,无法使用数字下标获取字符串中的字符。
字符串的内部表示
String
类型是对 Vec<u8>
类型的封装。
1 | // 英文字母由单字节组成,所以看起来还很正常 |
所以对应下标位置的字节并不对应字符。
字节、字符、字形簇
另一个问题是 UTF-8 编码在 Rust 的角度下有三个不同的视图:字节、字符(Unicode 标量)、字形簇(字形簇最接近所谓的字母)。
比如印度的梵文词「नमस्ते」:
1 | // 字节表示 |
而最终 Rust 不允许使用下标访问字符串的原因在于,下标访问应该具有 O(1)
的复杂度。而在字符串中这是不可能保证的,因为必须从头开始遍历字符串来数有多少个有效字符。
字符串切片
虽然不允许下标访问,Rust 还是允许我们使用切片语法获取字符串的切片,只是如果我们的切片的开始或结束位置不在字符边界上,就会造成 panic
(导致程序崩溃)。
遍历字符串的方法
幸运的是,我们仍然可以通过其他方式来遍历字符串:
1 | // 可以遍历所有2个字符 |
使用哈希映射存储关联的键与值
最后一个要介绍的常用集合是哈希映射(hash map)。HashMap<K, V>
类型存储 K
类型的键与 V
类型的值的映射。哈希映射使用哈希函数来决定键与值如何存放在内存中。这种数据结构在很多语言中都有实现,只是名称有所不同:哈希、映射、对象、哈希表、字典、关联数组等。
哈希映射通常用于希望通过一个标记而不是下标来寻找数据的场合。
创建新的哈希映射
1 | // 由于哈希映射并不如字符串或者向量那样常用,所以并没有自动引入到上下文中 |
还可以通过迭代其他集合的方式来创建哈希映射:
1 | use std::collections::HashMap; |
哈希映射与所有权
对于 i32
这种实现了 Copy
特性的类型,值可以复制到哈希映射中。而对于其他的类型,比如String
,值会移动到哈希映射中。
1 | use std::collections::HashMap; |
如果我们将引用插入到哈希映射中,就不会产生移动。但是被引用的数据必须在哈希映射有效时始终保持有效(有关 Rust 的 “生命周期” 特性)。
访问哈希映射中的值
1 | use std::collections::HashMap; |
更新哈希映射中的值
1 | use std::collections::HashMap; |
哈希函数
默认情况下,HashMap
使用一种密码学安全的哈希函数(siphash),可以帮我们抵御 DoS 攻击。这并不是最快的哈希函数,但是它带来的安全性是值得通过一部分性能去换取的。如果觉得默认的哈希函数太慢,可以通过实现 BuildHasher
特性来自定义哈希函数,或者使用成熟的第三方库。
总结
向量、字符串、哈希映射可以提供关于存储、访问、修改数据的大部分功能。我们只了解了其一部分的用法,更多的用法需要通过标准库 API 文档来深入学习。