Rust标准库包含了一系列非常有用的被称为集合(collections)的数据结构。大部分的数据结构都代表着某个特定的值,但集合却可以包含多个值。与内置的数组与元组类型不同,这些集合将自己持有的数据存储在堆上。不同的集合类型有着不同的性能特性与开销。
- 动态数组(Vector):可以让你连续地存储任意多个值
- 字符串(String):是字符的集合。
- 哈希映射(Hash map):可以让你将值关联到一个特定的键上,它是另外一个数据结构——映射(map)的特殊表现。
一、动态数组(Vector)
动态数组(Vector)是一个集合类型表现为Vec<T>
。动态数组允许你在单个数据结构中存储多个相同类型的值,这些值会彼此相邻地存储在内存中。动态数组非常适合在需要存储一系列相同类型值地场景中使用。
1、创建动态数据
fn main() {
let v: Vec<i32> = Vec::new();
}
因为Vector是用泛型实现地,所以我们需要告诉Rust
变量v
绑定的Vec<T>
会持有i32
类型的元素。
Rust为了方便提供了vec!
宏,这个宏会根据我们提供的值来创建一个新的Vector,并根据上下文判断变量v
是存储那种类型的键值。
fn main() {
let v = vec![1, 2, 3];
}
2、更新Vector
fn main() {
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
}
对一个新创建的Vector并向其增加新元素,可以使用push
方法。但是必须在该变量v
前使用mut
关键字使其可变并且放入其中的所有值都是i32
类型,而且Rust也根据数据做出判断。
3、丢弃Vector
fn main() {
{
let v = vec![1, 2, 3, 4];
// 处理变量 v
} // <- 这里 v 离开作用域并被丢弃
}
动态数组中的所有内容都会随着动态数组的销毁而销毁,其持有的整数将被自动清理干净。
4、读取Vector元素
fn main() {
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."),
}
}
读取Vector元素中的内容:
- 使用
&
和[]
返回一个引用; - 使用
get
方法以索引作为参数返回一个一个Option<&T>
;
Rust提供了两种引用元素的方法的原因是当尝试使用现有元素之外的索引值时可以选择让程序如何运行。
fn main() {
let v = vec![1, 2, 3, 4, 5];
let does_not_exist = &v[100];
let does_not_exist = v.get(100);
}
当运行这段代码时,[]
方法会因为索引指向了不存在的元素而导致程序触发panic。当你尝试越界访问元素时使程序直接崩溃,那么[]
方法很合适;
get
方法会在检测到索引越越界时简单返回None
,而不是使程序直接崩溃。当偶尔越界访问动态数据组中的元素是一个正常行为时,你应该使用这种get
方法;(例如:索引可能来自一个用户输入的数字。当这个数字意外地超出边界时,程序就会得到一个None值。而我们也应该将这一信息反馈给用户,告诉他们当前动态数组的元素数量,并再度请求用户输入有效的值。这就比因为输入错误而使程序崩溃要友好得多!)
一旦程序获得了一个有效的引用,借用检查器就会执行所有权规则和借用规则,来保证这个引用及其他任何指向这个动态数组的引用始终有效。
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is : {}", first);
}
不能在相同作用域中同时存在可变和不可变引用的规则。当我们获取了vector的第一个元素的不可变引用并尝试在vector末尾增加一个元素的时候。
此处错误是由动态数组的工作原理导致的:当动态数组张的元素时连续存储的,插入新的元素后也许会没有足够多的空间将所有元素依次相邻地放下,这就需要分配新的内存空间,并将旧的元素移动到新的空间上。
5、遍历动态数组中的值
通过for循环来获取动态数组中每一个i32元素的不可变引用。
fn main() {
let v = vec![100, 32, 57];
for i in &v {
println!("{}", i);
}
}
我们也可以遍历可变的动态数组,并获得元素的可变引用,并修改其中的值。
fn main() {
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
}
为了使用+=
运算符来修改可变引用指向的值,我们首先需要使用解引用运算符*
来获取i
的绑定值。
6、使用枚举来存储多个类型的值
当需要在动态数组中存储不同类型的元素时,可以定义并使用枚举来应对这种情况,因为枚举中的所有变体都被定义为了同一种枚举类型。
fn main() {
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
}
二、使用字符串存储UTF-8编码的文本
在Rust中使用字符串时出现错误,可能时由3个因素共同造成的:
- Rust倾向于暴露可能的错误;
- 字符串时一个复杂的数据结构;
- Rust中的字符串使用了UTF-8编码;
字符串本身就是基于字节的集合,并通过功能性的方法将字节解析为文本。
1、字符串是什么?
Rust在语言核心部分只有一种字符串类型,那就是字符串切片str
,它通常以借用的形式(&str
)出现。字符串切片是一些指向存储在别处的UTF-8编码字符串的引用。
String
类型被定义在了Rust标准库中而没有被内置在语言的核心部分。当Rust开发者提到”字符串“时,他们通常指的是String
类型与&str
字符切片。
2、创建一个新的字符串
fn main() {
let mut s = String::new();
}
这行代码创建了一个叫做s的空字符串,我们可以将数据填入字符串。通常字符串会有初始数据。对于这种情况我们可以对实现了Display trait
的类型调用to_string
方法。
fn main() {
let data = "initial contents";
let s = data.to_string();
// 该方法也可直接用于字符串字面值:
let s = "initial contents".to_string();
}
这段代码所创建的字符串会拥有"initial contents"作为内容。
记住,字符串是基于UTF-8编码的,我们可以将任何合法的数据编码进字符串中。
3、更新字符串
String
的大小可以增减,其中的内容也可以修改,正如我们将数据推入其中时Vec<T>
内部数据所发生的变化一样。此外,还可以使用+
运算符或format!
宏拼接String
。
1.使用push_str
或push
向字符串添加内容
使用push_str
方法向String
中添加一段字符串切片:
fn main() {
let mut s = String::from("foo");
s.push_str("bar");
}
上面代码执行完成后,s
中的字符串将被更新为foobar
。由于我们并不需要取得参数的所有权,所以这里的push_str
方法只需要接收一个字符串切片作为参数。
在将字符串切片附加至String后继续使用变量s2
。
fn main() {
let mut s1 = String::from("foo");
let s2 = "bar";
s1.push_str(s2);
println!("s2 is {}", s2);
}
push
方法接收单个字符作为参数,并将它添加到String
中。
fn main() {
let mut s = String::from("lo");
s.push('l');
println!("{}", s);
}
2.使用+
运算符或format!
宏拼接字符串
使用+
运算符将两个String
之合并到一个新的String
值中:
fn main() {
let s1 = String::from("Hello, ");
let s2 = String::from("World!");
let s3 = s1 + &s2;
println!("{}", s3);
}
执行完这段代码后,字符串s3
中的内容会变为Hello, World!
。值得我们注意的是,我们在加法操作中仅对s2采用了引用,而s1
在加法操作之后则不再有效。产生这一现象的原因与使用+
运算符时所调用的方法签名有关。
fn add(self, s: &str) -> String {
标准库中,add
函数使用了泛型来进行定义。此处展示的add
函数将泛型替换为了具体的类型,这是我们使用String
值调用add
时使用的签名。
首先,代码中的s2
使用了&
符号,实际上是将第二个字符串的引用与第一个字符串相加,但是只能将&str与String相加,而不能将两个String相加。我们能够使用&s2
来调用add
函数的原因在于:编译器可以自动将&String
类型的参数强制转换为&str
类型。当我们调用add
函数时,Rust使用了一种被称作解引用强制转换的技术,将&2
转换为了&s2[]..
。
其次,我们可以看到add
函数签名中的self
并没有&
标记,所以add
函数会取得self
的所有权。这一意味着s1
将被移动到add
函数中调用,并在调用后失效。所以,即便let s3 = s1 + &s2;
看起来像是复制两个字符串并创建一个新的字符串,但实际上这条语句会取得s1
的所有权,再将s2
中的内容复制到其中,最后再将s1
的所有权作为结果返回。
4、字符串索引
Rust不允许我们通过索引来获得String
中的字符。在Rust中,我们实际上可以通过3中不同的方式来看待字符串中的数据:字节、标量值和字形簇。
5、字符串切片
为了明确表明需要一个字符串切片,你需要在索引的[]
中填写范围来指定所需的字节内容,而不是在[]
中使用单个数字进行索引:
fn main() {
let hello = "Здравствуйте";
let s = &hello[0..4];
println!("{}", s);
}
s
会是一个 &str
,它包含字符串的头四个字节。早些时候,我们提到了这些字母都占据两个字节,所以这意味着 s
将会是 “Зд”。
6、遍历字符串的方法
如果你想要对每一个Unicode标量值都进行处理,那么最好的办法就是使用chars
方法。
fn main() {
for c in "नमस्ते".chars() {
println!("{}", c);
}
}
另外使用bytes
方法返回每一个原始字节,这可能会适合你的场景:
fn main() {
for b in "नमस्ते".bytes() {
println!("{}", b);
}
}
三、使用Hash Map存储键值对
HashMap<K,V>
存储了从K类型键到V类型值之间的映射关系。哈希映射在内部实现中使用了哈希函数,这同时决定了它在内存中存储键值对的方式。
1、创建一个新的哈希映射
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
}
注意,我们首先需要使用use
将HashMap
从标准库的集合部分引入当前作用域。由于哈希映射没有被包含在预导入模块内,所以标准库对哈希映射的支持也不知另外两个集合。和动态数组一样,哈希映射也将数据存储在堆上。
另外一个构建哈希映射的方法是,在一个由键值对组成的元组动态数组上使用collect
方法。这里的collect
方法可以将数据收集到很多数据结构中,这些数据结构也包括HashMap
。
fn main() {
use std::collections::HashMap;
let teams = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];
let mut scores: HashMap<_,_> =
teams.into_iter().zip(initial_scores.into_iter()).collect();
}
2、哈希映射与所有权
对于那些实现了Copy trait
的类型,它们的值会被简单地复制到哈希映射中,而对于String
这种持有所有权地值,其值将会转移且所有权会转移给哈希映射。
fn main() {
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 不再有效,
// 尝试使用它们看看会出现什么编译错误!
}
在调用insert
方法后,field_name
和field_value
变量被移动到哈希映射中,我们没有办法使用这两个变量了。
3、访问哈希映射中的值
我们可以通过将键传入get
方法来获得哈希映射中的值。
fn main() {
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);
}
上面的代码中的score将会是与蓝队相关联的值,也就是Some(&10)
。因为get
返回的是一个Option<&V>
,所以这里的结果将被封装到了Some
中;假如这个哈希映射中没有键所对应的值,那么get就会返回None。
类似于动态数组,我们同样可以使用一个for
循环来遍历哈希映射中所有的键值对:
fn main() {
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);
}
}
4、更新哈希映射
尽管键值对的数量是可以增长的,但是在任意时刻,每个键都只能对应一个值。
1.覆盖旧值
当我们将一个键值对插入哈希映射后,接着使用同样的键并配以不同的值来继续插入,之前的键所关联的值就会被替换掉。
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);
println!("{:?}", scores);
}
2.只在键没有对应值时插入数据
在实际工作中,我们需要检测一个键是否存在对应值,如果不存在,则为它插入一个值。哈希映射中提供了一个被称为entry
的专用API来处理这种情形,它接收我们想要检测的键作为参数,并返回一个叫做Entry
的枚举作为结果。
fn main() {
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);
}
entry
中的or_insert
方法被定义为返回一个Entry键所指向值的可变引用,假如这个值不存在,就将参数作为新值插入哈希映射中,并把这个新值的可变引用返回。
3.基于旧值来更新值
哈希映射的另一个常见用法是查找某个键值所对应的值,并给予这个值来进行更新。
fn main() {
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;
}
println!("{:?}", map);
}
4.哈希函数
HashMap
默认使用一种叫做SipHash
的哈希函数,它可以抵御设计哈希表的拒绝服务攻击。但是这不是最快的哈希算法,不过为了更高的安全性付出一些性能代价通常是值得的。