14 KiB
Common Collections
Rust 的标准库包含了许多有用的数据结构,称为集合。大多数其他数据类型表示一个特定的值,但集合可以包含多个值。与内置数组和元组类型不同,这些集合指向的数据存储在堆上,这意味着数据量不需要在编译时知道,可以随着程序运行而增长或缩小。
Rust 程序中经常使用的三种集合:
- 向量允许存储可变数量的值。
- 字符串是字符的集合。我们之前提到过 String 类型,但在本章中我们将深入讨论。
- 哈希映射允许将值与特定的键相关联。它是更一般的数据结构映射的特定实现。
Vectors
Vec 向量。向量允许您在单个数据结构中存储多个值,将所有值放在内存中相邻的位置。向量只能存储相同类型的值。
创建新向量
1, 创建空向量。需指定类型,因为rust不知道我们打算存储什么样的元素。向量是使用泛型实现的。
fn main() {
let v: Vec<i32> = Vec::new();
}
- 创建带有初始值的向量。Rust 会推断出你想要存储的值的类型,所以你很少需要做这种类型注释。
fn main() {
let v = vec![1, 2, 3];
}
// v 的类型是Vec<i32>。整数类型是i32 因为它是默认的整数类型
读取向量元素
访问向量有两种方法,索引或使用get方法。
使用get方法时。将得到一个Option<&T>。
当取值越界时: 1. 使用索引或直接抛出Error;
- 使用get会返回None而不出现panic, 可通过match None处理。
向量的工作方式与所有权
所有权规则:不能在同一范围内拥有可变和不可变引用的规则。对于vec有一个不可变引用时,在未归还所有权之前,无法对vec进行新增和删除和修改工作。
为什么对第一个元素的引用会关心向量末尾的变化呢?这个错误是由于向量工作的方式导致的:因为向量把值放在内存中相邻的位置,在向量末尾添加新元素可能需要分配新的内存并将旧元素复制到新空间,如果当前向量存储位置的相邻位置没有足够的空间。在这种情况下,第一个元素的引用将指向已经释放的内存。借用规则防止程序陷入这种情况。 底层实现
遍历向量
向量没有实现Copy trait 故遍历时需要提供引用,否则后续不能使用此向量。
使用引用
遍历并修改内容。使用可变引用,并使用*
取消引用运算符获取值,
使用枚举存储多种类型
向量只能存储相同类型的值。但一个枚举的变体是在同一个枚举类型下定义的,故需要一个类型来表示不同类型的元素时,可以定义一个枚举。
Rust需要在编译时知道向量中将会有哪些类型,这样它就知道在堆上存储每个元素需要多少内存。我们还必须明确这个向量中允许哪些类型。如果Rust允许一个向量容纳任何类型,就有可能会有一些类型导致对向量元素执行操作时出现错误。
销毁向量时,元素一起被销毁
fn main() {
{
let v = vec![1, 2, 3, 4];
// do stuff with v
} // <- v goes out of scope and is freed here
}
当向量被销毁时,它的所有内容也会被销毁,这意味着它所持有的整数将被清理。借用检查器确保对向量内容的任何引用只在向量本身有效时使用。
String
rust的三个难点:1. Rust喜欢暴露可能的错误,
2.字符串是一种比许多程序员认为的更复杂的数据结构,
3.UTF-8。
什么是字符串
Rust在核心语言中只有一种字符串类型,即通常以其借用形式&str出现的字符串片段str。它们是对存储在其他地方的UTF-8编码字符串数据的引用。
Rust 标准库提供的 String 类型是一种可增长、可变、所有权、UTF-8 编码的字符串类型,而不是编入核心语言中的类型。当 Rustaceans 在 Rust 中提到“字符串”时,他们可能指的是 String 或字符串片段 &str 两种类型之一,而不仅仅是其中一种类型。尽管本节主要讨论 String,但两种类型在 Rust 标准库中都有广泛使用,且两种类型都是 UTF-8 编码的。
创建新字符串
字符串(String)也能使用与 Vec 相同的许多操作,因为实际上字符串是在字节向量的基础上增加了一些额外的保证、限制和功能而实现的(底层是由一个 Vec 来实现的)。
to_string 方法,它可以在实现了 Display trait 的任何类型上使用,就像字符串字面量一样
fn main() {
let data = "initial contents";
let s = data.to_string();
// the method also works on a literal directly: 该方法也可以直接在文字上使用
let s = "initial contents".to_string();
}
使用String::from。代码等同于上面的。字符串使用许多不同的通用 API,为我们提供了很多选择。其中一些看似多余,但它们都有其用武之地!在这种情况下,String::from
做 to_string
同样的事情,所以你选择哪个是风格和可读性的问题。
fn main() {
let s = String::from("initial contents");
}
字符串是 UTF-8 编码的,因此我们可以在其中包含任何正确编码的数据。所有这些都是有效值String
fn main() {
let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שָׁלוֹם");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");
}
更新字符串
字符串可以增加大小并更改其内容,就像 Vec 一样,如果你向其中添加更多数据。此外,还可以方便地使用 + 运算符或 format! 宏来连接字符串值。
push_str和push附加到字符串
分别用于字符串切片和字符。都不会获得所有权(因为是引用)。
当使用字符串时,需取切片
字符串拼接
+运算符和format!宏连接
+ 运算符使用以下add方法
fn add(self, s: &str) -> String {
在调用add时使用&s2是因为编译器可以将**&String参数强制转换为&str**。当我们调用add方法时,Rust使用deref强制转换。
使用+操作符会失去第一个参数的所有权,同时在处理多个字符串拼接时会特别笨拙。以下为使用format!宏进行拼接,不会失去所有权(使用引用)。
字符串索引
Rust 字符串不支持索引、
内部表示
一个字符串是 Vec的封装。当以 UTF-8 编码时,这些字母中的每一个都占用 1 个字节。但当以 Unicode 编码时,每一项占用两个字节。字符串字节的索引并不总是与有效的 Unicode 标量值相关联。 UTF-8 是一种将 Unicode 编码转换成字节序列的转换格式。
因为存储的单元字节大小不同,故而直接通过下标取值有时会取到错误值。为了避免返回意外值并导致可能无法立即发现的错误,Rust 根本不编译此代码,并在开发过程的早期防止误解。
字节和标量值和字形群
关于 UTF-8 的另一点是,从 Rust 的角度来看,实际上有三种相关的方式来查看字符串:作为字节、标量值和字素簇(最接近我们所说的字母)。
印地语单词“नमस्ते”,它存储为一个u8
值向量,如下所示:
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]
18个字节,这就是计算机最终存储此数据的方式。如果我们将它们视为 Unicode 标量值,这就是 Rust 的char
类型,那么这些字节如下所示:
['न', 'म', 'स', '्', 'त', 'े']
这里有六个char
值,但第四个和第六个不是字母:它们是本身没有意义的变音符号。最后,如果我们将它们视为字素簇,我们会得到人们所说的构成印地语单词的四个字母:
["न", "म", "स्", "ते"]
Rust 提供了不同的方式来解释计算机存储的原始字符串数据,这样每个程序都可以选择它需要的解释,而不管数据使用的是哪种人类语言。
Rust 不允许我们通过索引 aString
来获取字符的最后一个原因是索引操作预计总是需要常数时间 (O(1))。但是不能保证 a 的性能String
,因为 Rust 必须遍历从头到索引的内容以确定有多少个有效字符。
字符串切片
可以使用索引创建字符串切片,但需谨慎,会产生意外panic。
当对二个字节字符取一个字节时,会产生如下错误:
迭代字符串
chars
方法返回每个字符,bytes
方法返回每个原始字节。
有效的 Unicode 标量值可能由超过 1 个字节组成。
HashMap<K,V>
创建HashMap
首先从标准库引用HashMap。哈希映射将其数据存储在堆上。哈希映射是同类型的: 所有键必须具有相同的类型,所有值也必须具有相同的类型。
访问HashMap
get方法返回一个Option<&V>。如果哈希映射中该键没有值,get
将返回None
。
unwrap_or()
函数是 Rust 中 Option
类型的一个常用函数,它用于在值为 None
时返回一个默认值。它接受一个参数,在 Option
类型为 None
时返回该参数。
遍历HashMap
以任意顺序打印每一对。
HashMap所有权
对于实现Copy
特征的类型,如i32
,值被复制到哈希映射中。对于拥有的值,如String
,这些值将被移动,哈希映射将成为这些值的所有者。
存储字符串引用(保留所有权)
但是可能会出现这种情况:
推荐使用clone:
更新HashMap的值
- 覆盖,插入了两个相同key
- 仅在键不存在时添加键和值。
如果该键确实存在于哈希映射中,则现有值应保持原样。如果键不存在,则插入它并为其赋值。entry
将您要检查的密钥作为参数。该entry
方法的返回值是一个枚举Entry
,代表一个可能存在也可能不存在的值。
or_insert
方法被定义为如果该键存在,则Entry
返回对相应Entry
键值的可变引用,如果不存在,则将参数作为该键的新值插入,并返回对新值的可变引用。
- 基于旧值更新值
split_whitespace方法返回 中值的子片的迭代器,由空格分隔text。该or_insert
方法返回一个可变引用 ( &mut V
) 到指定键的值。这里我们将可变引用存储在count
变量中,因此为了分配给该值,我们必须首先count
使用星号 (*
) 取消引用。
哈希函数
默认情况下,HashMap使用一种名为SipHash的哈希函数,可以提供对哈希表的拒绝服务(DoS)攻击的抵抗力。这不是最快的哈希算法,但是为了更好的安全性而降低性能的权衡是值得的。如果您对代码进行了分析并发现默认哈希函数对您的目的太慢,您可以通过指定其他函数来切换。哈希器是一种实现BuildHasher特征的类型。