Rust 标准库中包含一系列被称为 集合(collections)的非常有用的数据结构。
大部分其他数据类型都代表一个特定的值,不过集合可以包含多个值。不同于内建的数组和元组类型,这些集合指向的数据是储存在堆上的,这意味着数据的数量不必在编译时就已知,并且还可以随着程序的运行增长或缩小。
- vector 允许我们一个挨着一个地储存一系列数量可变的值
- 字符串(string)是字符的集合。我们之前见过 String 类型,不过在本章我们将深入了解。
- 哈希 map(hash map)允许我们将值与一个特定的键(key)相关联。这是一个叫做 map 的更通用的数据结构的特定实现。
vector
Vec新建 vector
使用初始值来创建一个 Vec
let v: Vec<i32> = Vec::new();// vector 是用泛型实现的// 增加了一个类型注解。// 因为没有向这个 vector 中插入任何值,Rust 并不知道我们想要储存什么类型的元素。
因为我们提供了 i32 类型的初始值,Rust 可以推断出 v 的类型是 Vec
let v = vec![1, 2, 3];
更新 vector
如果想要能够改变它的值,必须使用 mut 关键字使其可变。放入其中的所有值都是 i32 类型的,而且 Rust 也根据数据做出如此判断,所以不需要 Vec
let mut v = Vec::new();v.push(5);v.push(6);v.push(7);v.push(8);
丢弃 vector 时也会丢弃其所有元素
当 vector 被丢弃时,所有其内容也会被丢弃,这意味着这里它包含的整数将被清理。
{let v = vec![1, 2, 3, 4];// 处理变量 v} // <- 这里 v 离开作用域并被丢弃
不过一旦开始使用 vector 元素的引用,情况就变得有些复杂了。
读取 vector 的元素
let v = vec![1, 2, 3, 4, 5];let third: &i32 = &v[2];println!("The third element is {}", third);match v.get(2) {Some(third) => println!("The third element is {}", third),None => println!("There is no third element."),}// get方法以索引作为参数 返回一个 Option<&T>
let v = vec![1, 2, 3, 4, 5];let does_not_exist = &v[100];// error// 这个方法更适合当程序认为尝试访问超过 vector 结尾的元素是一个严重错误的情况// ,这时应该使程序崩溃。let does_not_exist = v.get(100);// return None// 偶尔出现超过 vector 范围的访问属于正常情况的时候可以考虑使用它。// 接着你的代码可以有处理 Some(&element) 或 None 的逻辑
代码看起来应该能够运行:为什么第一个元素的引用会关心 vector 结尾的变化? 不能这么做的原因是由于 vector 的工作方式:在 vector 的结尾增加新元素时,在没有足够空间将所有所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。
let mut v = vec![1, 2, 3, 4, 5];let first = &v[0];v.push(6);println!("The first element is: {}", first);// error
遍历 vector 中的元素
为了修改可变引用所指向的值,在使用 += 运算符之前必须使用解引用运算符(*)获取 i 中的值。
let v = vec![100, 32, 57];for i in &v {println!("{}", i);}let mut v = vec![100, 32, 57]; // v中的值可以发送改变for i in &mut v {*i += 50;}
使用枚举来储存多种类型
Rust 在编译时必须准确的知道 vector 中类型的原因在于它需要知道储存每个元素到底需要多少内存。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。使用枚举外加 match 意味着 Rust 能在编译时就保证总是会处理所有可能的情况。 如果在编写程序时不能确切无遗地知道运行时会储存进 vector 的所有类型,枚举技术就行不通了。相反,你可以使用 trait 对象。
enum SpreadsheetCell {Int(i32),Float(f64),Text(String),}let row = vec![SpreadsheetCell::Int(3),SpreadsheetCell::Text(String::from("blue")),SpreadsheetCell::Float(10.12),];
字符串
什么是字符串?
Rust 的核心语言中只有一种字符串类型:str,字符串 slice,它通常以被借用的形式出现,&str。 String 的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。 Rust 标准库中还包含一系列其他字符串类型,比如 OsString、OsStr、CString 和 CStr。这些字符串类型能够以不同的编码,或者内存表现形式上以不同的形式,来存储文本内容。新建字符串
let mut s = String::new();
let data = "initial contents";let s = data.to_string();// 该方法也可直接用于字符串字面值:let s = "initial contents".to_string();// 等价let s = String::from("initial contents");
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");
更新字符串
String 的大小可以增加,其内容也可以改变,就像可以放入更多数据来改变 Vec 的内容一样。另外,可以方便的使用 + 运算符或 format! 宏来拼接 String 值。使用 push_str 和 push 附加字符串
push 方法被定义为获取一个单独的字符作为参数,并附加到 String 中。
let mut s = String::from("foo");s.push_str("bar");// push_str 方法采用字符串 slice,因为我们并不需要获取参数的所有权。let mut s1 = String::from("foo");let s2 = "bar";s1.push_str(s2);println!("s2 is {}", s2);// success
let mut s = String::from("lo");s.push('l');
使用 + 运算符或 format! 宏拼接字符串
s1 在相加后不再有效的原因,和使用 s2 的引用的原因,与使用 + 运算符时调用的函数签名有关。+ 运算符使用了 add 函数,这个函数签名看起来像这样:
let s1 = String::from("Hello, ");let s2 = String::from("world!");let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用
add 函数的 s 参数:只能将 &str 和 String 相加,不能将两个 String 值相加。 不过等一下 —— 正如 add 的第二个参数所指定的,&s2 的类型是 &String 而不是 &str。那么为什么示例 还能编译呢? 之所以能够在 add 调用中使用 &s2 是因为 &String 可以被 强转(coerced)成 &str。当add函数被调用时,Rust 使用了一个被称为 解引用强制多态(deref coercion)的技术,你可以将其理解为它把 &s2 变成了 &s2[..]。 其次,可以发现签名中 add 获取了 self 的所有权,因为 self 没有 使用 &。
fn add(self, s: &str) -> String {// 这并不是标准库中实际的签名;标准库中的 add 使用泛型定义。
如果想要级联多个字符串,+ 的行为就显得笨重了:
对于更为复杂的字符串链接,可以使用 format! 宏
let s1 = String::from("tic");let s2 = String::from("tac");let s3 = String::from("toe");let s = s1 + "-" + &s2 + "-" + &s3;
format! 与 println! 的工作原理相同,不过不同于将输出打印到屏幕上,它返回一个带有结果内容的 String。
let s1 = String::from("tic");let s2 = String::from("tac");let s3 = String::from("toe");let s = format!("{}-{}-{}", s1, s2, s3);
索引字符串
Rust 的字符串不支持索引。那么接下来的问题是,为什么不支持呢?
let s1 = String::from("hello");let h = s1[0];// error
内部表现
// len的值是4 ,这意味着储存字符串 “Hola” 的 Vec 的长度是四个字节:// 这里每一个字母的 UTF-8 编码都占用一个字节。let len = String::from("Hola").len();// 注意这个字符串中的首字母是西里尔字母的 Ze 而不是阿拉伯数字 3 。// len的值是24不是12 因为这里每个Unicode标量值需要两个字节存储(与中文一样)let len = String::from("Здравствуйте").len();let hello = "Здравствуйте";let answer = &hello[0];// answer的值应该是什么呢?它应该是第一个字符З吗?// 实际并不是,由这里每个字符占2个字符可知
字节、标量值和字形簇
比如这个用梵文书写的印度语单词 “नमस्ते”,最终它储存在 vector 中的 u8 值看起来像这样如果从 Unicode 标量值的角度理解它们,也就像 Rust 的 char 类型那样,这些字节看起来像这样:
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, 224, 165, 135]
这里有六个 char,不过第四个和第六个都不是字母,它们是发音符号本身并没有任何意义。最后,如果以字形簇的角度理解,就会得到人们所说的构成这个单词的四个字母:
['न', 'म', 'स', '्', 'त', 'े']
最后一个 Rust 不允许使用索引获取 String 字符的原因是,索引操作预期总是需要常数时间 (O(1))。但是对于 String 不可能保证这样的性能,因为 Rust 必须从开头到索引位置遍历来确定有多少有效的字符。
["न", "म", "स्", "ते"]
字符串 slice
为了更明确索引并表明你需要一个字符串 slice,相比使用 [] 和单个值的索引,可以使用 [] 和一个 range 来创建含特定字节的字符串 slice:
这里,s 会是一个 &str,它包含字符串的头四个字节。早些时候,我们提到了这些字母都是两个字节长的,所以这意味着 s 将会是 “Зд”。 如果获取 &hello[0..1] 会发生什么呢?答案是:Rust 在运行时会 panic,就跟访问 vector 中的无效索引时一样
let hello = "Здравствуйте";let s = &hello[0..4];
遍历字符串的方法
如果你需要操作单独的 Unicode 标量值,最好的选择是使用 chars 方法。bytes 方法返回每一个原始字节。
for c in "नमस्ते".chars() {println!("{}", c);}// outputनमस्ते
for b in "नमस्ते".bytes() {println!("{}", b);}// output224164// --snip--165135
哈希
HashMap新建一个哈希 map
注意必须首先 use 标准库中集合部分的 HashMap。在这三个常用集合中,HashMap 是最不常用的,所以并没有被 prelude 自动引用。 像 vector 一样,哈希 map 将它们的数据储存在堆上,这个 HashMap 的键类型是 String 而值类型是 i32。类似于 vector,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。
use std::collections::HashMap;let mut scores = HashMap::new();scores.insert(String::from("Blue"), 10);scores.insert(String::from("Yellow"), 50);
这里 HashMap<_, _> 类型注解是必要的,因为可能 collect 很多不同的数据结构,而除非显式指定否则 Rust 无从得知你需要的类型。
use std::collections::HashMap;let teams = vec![String::from("Blue"), String::from("Yellow")];let initial_scores = vec![10, 50];let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();// 使用 collect 方法将这个元组 vector 转换成一个 HashMa
哈希 map 和所有权
对于像 i32 这样的实现了 Copy trait 的类型,其值可以拷贝进哈希 map。对于像 String 这样拥有所有权的值,其值将被移动而哈希 map 会成为这些值的所有者
use std::collections::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);// 这里 field_name 和 field_value 不再有效,// 尝试使用它们看看会出现什么编译错误!
访问哈希 map 中的值
通过 get 方法并提供对应的键来从哈希 map 中获取值score 是与蓝队分数相关的值,应为 Some(10)。因为 get 返回 Option
use std::collections::HashMap;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);
use std::collections::HashMap;let mut scores = HashMap::new();scores.insert(String::from("Blue"), 10);scores.insert(String::from("Yellow"), 50);for (key, value) in &scores {println!("{}: {}", key, value);}// outputYellow: 50Blue: 10
更新哈希 map
当我们想要改变哈希 map 中的数据时,必须决定如何处理一个键已经有值了的情况。可以选择完全无视旧值并用新值代替旧值。可以选择保留旧值而忽略新值,并只在键 没有 对应值时增加新值。或者可以结合新旧两值。覆盖一个值
这会打印出 {“Blue”: 25}。原始的值 10 则被覆盖了。
use std::collections::HashMap;let mut scores = HashMap::new();scores.insert(String::from("Blue"), 10);scores.insert(String::from("Blue"), 25);println!("{:?}", scores);
只在键没有对应值时插入
我们经常会检查某个特定的键是否有值,如果没有就插入一个值。为此哈希 map 有一个特有的 API,叫做 entry,它获取我们想要检查的键作为参数。entry 函数的返回值是一个枚举,Entry,它代表了可能存在也可能不存在的值。Entry 的 or_insert 方法在键对应的值存在时就返回这个值的可变引用,如果不存在则将参数作为新值插入并返回新值的可变引用。
use std::collections::HashMap;let mut scores = HashMap::new();scores.insert(String::from("Blue"), 10);scores.entry(String::from("Yellow")).or_insert(50);scores.entry(String::from("Blue")).or_insert(50);println!("{:?}", scores);// {"Yellow": 50, "Blue": 10}
根据旧值更新一个值
use std::collections::HashMap;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);*count += 1;}// :? 打印#[derive(Debug)] 注解println!("{:?}", map);
哈希函数
HashMap 默认使用一种 “密码学安全的”(“cryptographically strong” )哈希函数,它可以抵抗拒绝服务(Denial of Service, DoS)攻击。 然而这并不是可用的最快的算法,不过为了更高的安全性值得付出一些性能的代价。如果性能监测显示此哈希函数非常慢,以致于你无法接受,你可以指定一个不同的 hasher 来切换为其它函数。hasher 是一个实现了 BuildHasher trait 的类型。crates.io 有其他人分享的实现了许多常用哈希算法的 hasher 的库。
