这里继续沿用上次工程rust-demo
Rust的标准库包括许多非常有用的数据结构,称为集合。大多数其他数据类型表示一个特定的值,但是集合可以包含多个值。与内置数组和元组类型不同,这些集合指向的数据存储在堆上,这意味着数据量在编译时不需要知道,并且可以随着程序运行而增长或收缩。每一种收藏都有不同的功能和成本,选择一种适合你当前情况的收藏是一项你需要慢慢培养的技能。我们将讨论Rust程序中经常使用的三个集合:
我们要看的第一个集合类型是Vec
为了创建一个新的空向量,我们调用Vec::new函数
fn main() {
let v: Vec = Vec::new(); // 创建vector
}
注意,我们在这里添加了一个类型注释。因为我们没有在这个向量中插入任何值,所以Rust不知道我们打算存储什么样的元素。这是很重要的一点。向量是使用泛型实现的;现在,我们知道标准库提供的Vec
更常见的是,您将创建一个带有初始值的Vec < T >, Rust将推断出您想要存储的值的类型,因此您很少需要做这种类型注释。Rust方便地提供了vec!宏,它将创建一个新的向量来保存您给它的值。下例中创建了一个新的Vec
fn main() {
let v = vec![1, 2, 3]; // 带有初始值的vector
}
因为我们已经给出了初始i32值,Rust可以推断出v的类型是Vec
要创建一个向量,然后向其中添加元素,我们可以使用push方法,
fn main() {
let mut v = Vec::new(); // mut关键字,创建可修改的向量vector
v.push(5); // 和其他语言类似,传入数据
v.push(6);
v.push(7);
v.push(8);
}
和任何变量一样,如果我们希望能够改变它的值,我们需要使用mut关键字使它可变,如第3章所讨论的。我们放入的数字都是i32类型的,Rust从数据中推断出这一点,所以我们不需要Vec
有两种方法可以引用存储在vector中的值:通过索引或者使用get方法。在下面的例子中,为了更加清晰起见,我们对这些函数返回的值的类型进行了注释。
fn main() {
let v = vec![1, 2, 3, 4, 5]; // 定义向量
let third: &i32 = &v[2]; // 通过索引获取向量中的值
println!("The third element is {}", third);
let third: Option<&i32> = v.get(2); // 通过get接口获取向量中的值
match third {
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element."),
}
}
编译运行
cargo run
请注意这里的一些细节。我们使用索引值2来获取第三个元素,因为向量是由数字索引的,从零开始。使用&和[]给出了对索引值处元素的引用。当我们使用get方法并将索引作为参数传递时,我们得到一个可以与match一起使用的Option< &T>。
Rust提供这两种方法引用元素的原因是,当您试图使用现有元素范围之外的索引值时,您可以选择程序的行为方式。作为一个例子,让我们看看当我们有一个包含五个元素的向量,然后我们尝试用每种技术访问索引为100的元素时会发生什么,
fn main() {
let v = vec![1, 2, 3, 4, 5];
let does_not_exist = &v[100]; // 获取索引为100的值
let does_not_exist = v.get(100);
}
编译运行
cargo run
当我们运行这段代码时,第一个[]方法将导致程序崩溃,因为它引用了一个不存在的元素。如果试图访问超过vector末尾的元素,希望程序崩溃时,最好使用这种方法。
当get方法被传递一个在vector之外的索引时,它返回None而不会死机。如果在正常情况下偶尔会访问vector范围之外的元素,那么可以使用这个方法。然后你的代码将有逻辑来处理有Some(&element)或者None,如之前章节讨论的。例如,索引可能来自输入数字的人。如果他们不小心输入了一个太大的数字,程序得到一个None值,你可以告诉用户当前向量中有多少项,然后再给他们一次输入有效值的机会。这比因为一个打字错误而导致程序崩溃更容易操作!
当程序有一个有效的引用时,借用检查器执行所有权和借用规则以确保这个引用和任何其他对vector内容的引用保持有效。回想一下在同一个作用域中不能有可变和不可变引用的规则。这条规则适用于下例,我们保存了一个对向量中第一个元素的不可变引用,并试图在末尾添加一个元素。如果我们试图在函数的后面引用这个元素,这个程序将无法运行:
fn main() {
let mut v = vec![1, 2, 3, 4, 5]; // 可变的向量
let first = &v[0];
v.push(6); // 改变值
println!("The first element is: {}", first);
}
编译运行
cargo run
编译错误
上例中的代码看起来应该是可行的:为什么对第一个元素的引用要关心向量末尾的变化?这个错误是由vector的工作方式造成的:因为vector将值彼此相邻地放在内存中,所以在vector的末尾添加新元素可能需要分配新的内存,并将旧的元素复制到新的空间,如果没有足够的空间将所有元素彼此相邻地放在vector当前存储的位置。在这种情况下,对第一个元素的引用将指向已释放的内存。借用规则防止程序在这种情况下终止。
为了依次访问vector中的每个元素,我们将遍历所有元素,而不是使用索引一次访问一个元素。下例中显示了如何使用一个for循环来获得对i32值的向量中的每个元素的不可变引用,并打印它们。
fn main() {
let v = vec![100, 32, 57];
for i in &v { // for循环
println!("{}", i);
}
}
编译运行
cargo run
我们还可以迭代可变向量中每个元素的可变引用,以便对所有元素进行更改。下例中中的for循环将每个元素加50。
fn main() {
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50; // 每个数加 50
println!("{}", i);
}
}
编译运行
cargo run
要改变可变引用所引用的值,我们必须在使用+=操作符之前使用* 接触引用操作符来获得I中的值。
由于借用检查器的规则,迭代一个向量,无论是不变的还是可变的,都是安全的。如果我们试图在上例中的for循环体中插入或删除项目,我们会得到一个类似于上述其中代码的编译错误。对for循环持有的向量的引用防止了对整个向量的同时修改。
向量只能存储相同类型的值。这可能不方便;肯定有需要存储不同类型的项目列表的用例。幸运的是,一个枚举的变体是在同一个枚举类型下定义的,所以当我们需要一个类型来表示不同类型的元素时,我们可以定义并使用一个枚举!
例如,假设我们想从电子表格的一行中获取值,该行中的一些列包含整数、一些浮点数和一些字符串。我们可以定义一个枚举,它的变量包含不同的值类型,所有的枚举变量都被认为是相同的类型:枚举的类型。然后我们可以创建一个向量来保存这个枚举,最终保存不同的类型。
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),
];
}
Rust需要知道在编译时vector中有哪些类型,这样它就能准确地知道在堆中需要多少内存来存储每个元素。我们还必须明确在这个向量中允许什么类型。如果Rust允许vector保存任何类型,那么有可能一个或多个类型会导致对vector元素执行的操作出错。使用枚举加match表达式意味着Rust将确保在编译时处理每一种可能的情况。
如果你不知道一个程序在运行时将在vector中存储的所有类型,enum技术就不起作用。
既然我们已经讨论了使用vectors的一些最常见的方法,可以查看Rust API文档,了解标准库在Vec
像任何其他struct一样,向量在超出范围时被释放,
fn main() {
{
let v = vec![1, 2, 3, 4];
// do stuff with v
} // <- v goes out of scope and is freed here // 有效范围
}
当vector被丢弃时,它的所有内容也被丢弃,这意味着它保存的整数将被清除。借用检查器确保仅当向量本身有效时,才使用对向量内容的任何引用。