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);
}
// output
224
164
// --snip--
165
135
哈希
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);
}
// output
Yellow: 50
Blue: 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 的库。