The Rust Programming Language:Common Collections
Storing Lists of Values with Vectors
Creating a New Vector
1 | let v: Vec<i32> = Vec::new(); |
这里需要显式写出元素类型,因为里面还没有元素以供类型推断。
1 | let v = vec![1, 2, 3]; |
这里的整数类型是i32
因为它是默认类型。
Updating a Vector
1 | let mut v = Vec::new(); |
Reading Elements of Vectors
1 | let v = vec![1, 2, 3, 4, 5]; |
以下代码无法编译通过:
1 | let mut v = vec![1, 2, 3, 4, 5]; |
运行结果为:
1 | $ cargo run |
因为 push 可能发生内存拷贝,在内存不够的情况下,会销毁掉原来的内存空间并将 vector 中的内容复制到一块新的空间中。因此 first 就可能会指向一块被销毁掉的内存。Borrowing rules 会阻止程序发生这种情况。
Iterating Over the Values in a Vector
1 | let v = vec![100, 32, 57]; |
Using an Enum to Store Multiple Types
1 | enum SpreadsheetCell { |
Dropping a Vector Drops Its Elements
1 | { |
Storing UTF-8 Encoded Text with Strings
What Is a String?
Rust 在核心语言中只有一种字符串类型,即字符串切片 str,通常以借用形式 &str 出现。String
类型,是在Rust 的标准库中提供的,而不是被编码在核心语言中。它是可增长的、可变的、独占的、UTF-8 编码的字符串类型。
Creating a New String
1 | // create a new, empty string |
Updating a String
可以方便地用 +
操作符和 format!
宏来拼接字符串。
Appending to a String with push_str
and push
1 | let mut s = String::from("foo"); |
push_str
方法接受一个字符串切片,因为我们不一定想要对参数进行所有权转移。例如下面这个场景:
1 | let mut s1 = String::from("foo"); |
如果 push_str
抢占了 s2
的所有权,我们就不能在最后一行打印它的值了,这显然不是我们想要的。
push
方法接收一个字符作为参数并将其加入到String
中:
1 | let mut s = String::from("lo"); |
Concatenation with the +
Operator or the format!
Macro
1 | let s1 = String::from("Hello, "); |
这里其实调用的是 add 方法,这个方法接收了一个引用类型,所以这里用了 &s2。这个 add 方法是个泛型方法,对于 String 类型来说大概长这样:
1 | fn add(self, s: &str) -> String { |
但是这里 add 方法参数中的 s 是&str类型,我们传的是 &String 类型,那为什么可以编译成功呢?因为编译器会将 &String 参数强制转换为 &str,调用add 方法时,Rust 使用解引用强制类型转换,将 &s2 转换为了&s2[…]。
在复杂的字符串拼接场景下,我们可以使用format!
宏:
1 | let s1 = String::from("tic"); |
它返回一个 String
,并且它的参数使用引用,因此不会抢占所有权。
Indexing into Strings
1 | let s1 = String::from("hi"); |
这段代码不能编译通过:
1 | $ cargo run |
Rust 中的字符串不支持索引。但是为什么?为了回答这个问题,我们就需要讨论 Rust 在内存中是如何存储字符串的。
Internal Representation
一个字符串是对 Vec<u8>
的包装。一些正确编码的 UTF-8 的字符串:
1 | let hello = String::from("Hola"); |
这里的len
是 4,意味着存储字符串 Hola
的 vector 长度为 4 字节。这里面的每个字符在用 UTF-8 编码时都占用一个字节。
但下面这一行在 Rust 中的长度为 24 个字节:
1 | let hello = String::from("Здравствуйте"); |
因为该字符串中的每个 Unicode 标量值都需要 2 个字节来进行存储。因此,对字符串字节的索引并不总是与有效的 Unicode 标量值相关。例如:
1 | let hello = "Здравствуйте"; |
当首字母 3 被编码为 UTF-8 时,3 的第一个 byte 是 208,第二个是 151,因此实际上返回值应该是 208,但 208 本身并不是一个有效的字符。并且,返回 208 并不是用户想要的,虽然这就是在字节索引 0 处拥有的唯一数据。
因此,为了避免返回意外值并导致可能不会立即发现的错误,Rust 根本不编译此代码,从而在开发过程中尽早防止误解。
Bytes and Scalar Values and Grapheme Clusters! Oh My!
关于 UTF-8 的另一个要点是,从 Rust 的角度看,对于字符串实际上有三种相关的观察方式:作为字节、Unicode标量值和字形集。如果我们看一下印地语单词“नमस्ते”,它存储为一个 u8值的向量,看起来就会像这样:
1 | [224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, |
这里的 18 个字节就是计算机最终存储数据的形态。如果我们把它们看成Unicode标量值,也就是 Rust 的 char 类型的话,这些字节看起来是这样的:
1 | ['न', 'म', 'स', '्', 'त', 'े'] |
这里有六个字符值,但第四个和第六个不是字母:它们是单独没有意义的变音符号。最后,如果我们将它们视为字形集,我们就得到口语化的构成印地语单词的四个字母:
1 | ["न", "म", "स्", "ते"] |
Rust 提供了不同的方式来解释计算机存储的原始字符串数据,以便每个程序可以选择它所需的解释,无论数据使用哪种语言。
Rust 不允许我们通过索引访问字符串以获取字符的最终原因是,索引操作被期望始终具有恒定的时间复杂度(O(1))。但是,对于字符串来说,无法保证这种性能,因为 Rust 必须从头开始遍历内容到达索引,以确定有多少个有效字符。
Slicing Strings
对字符串进行索引通常是一个坏主意,因为不清楚字符串索引操作的返回类型应该是什么:一个字符值、一个字符、一个字形集还是一个字符串切片。因此如果你真的需要使用索引来创建字符串切片,Rust 要求你更加具体:
1 | let hello = "Здравствуйте"; |
如果我们尝试用类似 &hello[0..1]
的方式来切片字符的字节的一部分,Rust 会在运行时 panic,就像访问了向量中的无效索引一样:
1 | $ cargo run |
Methods for Iterating Over Strings
操作字符串片段的最佳方式是明确你想要字符还是字节。对于单个 Unicode 标量值,使用 chars 方法。调用”Зд”的chars会分离并返回两个类型为char的值,你可以遍历结果以访问每个元素:
1 | for c in "Зд".chars() { |
输出:
1 | З |
类似地,bytes
方法返回每个字节:
1 | for b in "Зд".bytes() { |
输出:
1 | 208 |
Storing Keys with Associated Values in Hash Maps
Creating a New Hash Map
1 | use std::collections::HashMap; |
Accessing Values in a Hash Map
1 | use std::collections::HashMap; |
这里通过调用copied
来处理Option
以获得Option<i32>
而不是Option<&i32>
,然后调用unwrap_or
用来将score
设置为 0 如果scores
中不存在这个键值对。
1 | use std::collections::HashMap; |
Hash Maps and Ownership
对于 i32 这种实现了 Copy
特性的类型,值会被复制到 hash map 中。但对于独占类型,例如String
,值就会被移动到 hash map 中,hash map 会变成这些值的 owner:
1 | use std::collections::HashMap; |
Updating a Hash Map
Overwriting a Value
1 | use std::collections::HashMap; |
输出 {"Blue": 25}
Adding a Key and Value Only If a Key Isn’t Present
1 | use std::collections::HashMap; |
Updating a Value Based on the Old Value
1 | use std::collections::HashMap; |