键盘快捷键

按 ← 或 → 在章节间切换

按 S 或 / 搜索本书

按 ? 显示此帮助

按 Esc 隐藏此帮助

使用向量存储值列表

我们要介绍的第一种集合类型是 Vec<T>,也称为向量。向量可以让你在一个数据结构中存储多个值,并将这些值连续地放在内存中。向量只能存储相同类型的值。当你需要保存一个条目列表时,向量就很有用,例如文件中的文本行或购物车中各商品的价格。

创建新向量

要创建一个新的空向量,我们可以调用 Vec::new 函数,如示例 8-1 所示。

fn main() {
    let v: Vec<i32> = Vec::new();
}
示例 8-1:创建一个新的空向量,用于存储 i32 类型的值

请注意,我们在这里添加了类型注解。由于尚未向这个向量插入任何值,Rust 不知道我们打算存储哪种元素。这一点很重要。向量使用泛型实现;我们会在第 10 章介绍如何在自己的类型中使用泛型。目前只需知道,标准库提供的 Vec<T> 类型可以存储任意类型。创建用于存储特定类型的向量时,我们可以在尖括号中指定类型。在示例 8-1 中,我们告诉 Rust,v 中的 Vec<T> 将存储 i32 类型的元素。

更常见的做法是使用初始值创建 Vec<T>,Rust 会推断你想要存储的值的类型,因此通常不需要添加类型注解。Rust 提供了方便的 vec! 宏,可以创建一个包含所给值的新向量。示例 8-2 创建了一个新的 Vec<i32>,其中包含值 1、2 和 3。整数类型是 i32,因为它是默认整数类型,正如我们在第 3 章“数据类型”一节中讨论的那样。

fn main() {
    let v = vec![1, 2, 3];
}
示例 8-2:创建一个包含值的新向量

由于我们提供了初始 i32 值,Rust 可以推断 v 的类型是 Vec<i32>,因此不需要类型注解。接下来,我们来看看如何修改向量。

更新向量

要创建向量并向其中添加元素,我们可以使用 push 方法,如示例 8-3 所示。

fn main() {
    let mut v = Vec::new();

    v.push(5);
    v.push(6);
    v.push(7);
    v.push(8);
}
示例 8-3:使用 push 方法向向量添加值

和其他变量一样,如果我们想要能够修改它的值,就需要使用 mut 关键字将其声明为可变变量,这一点已在第 3 章讨论过。我们放入其中的数字都是 i32 类型,Rust 会根据数据推断出这一点,因此不需要标注 Vec<i32>。

读取向量元素

有两种方式可以引用向量中存储的值:通过索引,或使用 get 方法。为了更加清晰,下面的示例标注了这些函数返回值的类型。

示例 8-4 展示了通过索引语法和 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);
    match third {
        Some(third) => println!("The third element is {third}"),
        None => println!("There is no third element."),
    }
}
示例 8-4:使用索引语法和 get 方法访问向量中的项

这里有几点需要注意。由于向量的索引从 0 开始,因此我们使用索引值 2 来获取第三个元素。使用 & 和 [] 会得到对该索引位置元素的引用。使用 get 方法并将索引作为参数传入时,我们会得到一个可以与 match 一起使用的 Option<&T>。

Rust 提供了这两种引用元素的方式,让你可以选择当尝试使用超出已有元素范围的索引时,程序应如何处理。举例来说,如示例 8-5 所示,假设有一个包含五个元素的向量,我们分别使用这两种方式访问索引 100 处的元素,看看会发生什么。

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let does_not_exist = &v[100];
    let does_not_exist = v.get(100);
}
示例 8-5:尝试访问包含五个元素的向量中索引 100 处的元素

运行这段代码时,第一个 [] 方法会导致程序 panic,因为它引用了一个不存在的元素。当你希望程序在有人尝试访问向量末尾之后的元素时崩溃,这种方式最合适。

如果传给 get 方法的索引超出了向量范围,它会返回 None,而不会触发 panic。如果在正常情况下偶尔可能访问超出向量范围的元素,就可以使用这种方法。此时,代码可以像第 6 章讨论的那样,处理得到 Some(&element) 或 None 的情况。例如,索引可能来自用户输入。如果用户不小心输入了过大的数字,导致程序得到 None,你可以告知用户当前向量中有多少项,并让他们再次输入有效值。与因输入错误而让程序崩溃相比,这样对用户更友好!

当程序持有有效引用时,借用检查器会强制执行所有权和借用规则(第 4 章介绍过),以确保该引用以及指向向量内容的其他引用始终有效。回顾一下这条规则:在同一作用域中,不能同时拥有可变引用和不可变引用。示例 8-6 就涉及这条规则:我们持有向量第一个元素的不可变引用,同时尝试在末尾添加一个元素。如果之后还要在函数中引用该元素,这段程序就无法运行。

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];

    let first = &v[0];

    v.push(6);

    println!("The first element is: {first}");
}
示例 8-6:持有某个元素的引用时,尝试向向量中添加元素

编译这段代码会产生以下错误:

$ cargo run
   Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:5
  |
4 |     let first = &v[0];
  |                  - immutable borrow occurs here
5 |
6 |     v.push(6);
  |     ^^^^^^^^^ mutable borrow occurs here
7 |
8 |     println!("The first element is: {first}");
  |                                      ----- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections` (bin "collections") due to 1 previous error

示例 8-6 中的代码看起来似乎应该可以运行:为什么对第一个元素的引用会受向量末尾变化的影响?这是由向量的工作方式导致的:由于向量会将值连续存放在内存中,如果当前位置没有足够空间容纳所有元素,那么在向量末尾添加新元素时可能需要分配新的内存,并将旧元素复制到新位置。在这种情况下,指向第一个元素的引用就会指向已释放的内存。借用规则可以防止程序陷入这种状态。

注意:关于 Vec<T> 类型实现细节的更多信息,请参阅《Rustonomicon》。

遍历向量中的值

要依次访问向量中的每个元素,我们可以遍历所有元素,而不是每次用索引访问一个元素。示例 8-7 展示了如何使用 for 循环获取 i32 向量中每个元素的不可变引用并打印它们。

fn main() {
    let v = vec![100, 32, 57];
    for i in &v {
        println!("{i}");
    }
}
示例 8-7:使用 for 循环遍历向量中的元素并逐个打印

我们也可以遍历可变向量中每个元素的可变引用,从而修改所有元素。示例 8-8 中的 for 循环会给每个元素加上 50。

fn main() {
    let mut v = vec![100, 32, 57];
    for i in &mut v {
        *i += 50;
    }
}
示例 8-8:遍历向量中元素的可变引用

要修改可变引用所指向的值,我们必须先使用 * 解引用运算符取得 i 中的值,然后才能使用 += 运算符。第 15 章的“通过解引用运算符访问指针指向的值”一节会进一步介绍解引用运算符。

无论以不可变还是可变方式遍历向量,由于借用检查器的规则,这样做都是安全的。如果我们尝试在示例 8-7 和示例 8-8 的 for 循环体中插入或移除元素,就会得到与示例 8-6 中类似的编译错误。for 循环持有的向量引用会阻止同时修改整个向量。

使用枚举存储多种类型

向量只能存储相同类型的值。这可能不太方便,因为确实存在需要存储不同类型条目列表的情况。幸运的是,枚举的变体都属于同一个枚举类型,因此当我们需要用一种类型表示不同类型的元素时,可以定义并使用枚举!

例如,假设我们想要获取电子表格某一行中的值,其中有些列包含整数,有些包含浮点数,还有些包含字符串。我们可以定义一个枚举,让它的变体分别存储不同类型的值;所有枚举变体都属于同一种类型,也就是该枚举的类型。然后,我们就可以创建一个存储该枚举的向量,从而存储不同类型的值。示例 8-9 展示了这种做法。

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),
    ];
}
示例 8-9:定义枚举,以便在一个向量中存储不同类型的值

Rust 需要在编译时知道向量中会存储哪些类型,这样才能准确确定在堆上存储每个元素所需的内存量。我们还必须明确指定这个向量允许存储哪些类型。如果 Rust 允许向量存储任意类型,那么对向量元素执行的操作可能会导致其中一种或多种类型出错。像第 6 章讨论的那样,使用枚举和 match 表达式,意味着 Rust 会在编译时确保处理了所有可能的情况。

如果你在运行时才知道程序可能会在向量中存储哪些类型,并且无法穷举这些类型,那么枚举技巧就不可行。此时可以使用 trait 对象,我们会在第 18 章介绍它。

现在我们已经讨论了几种最常见的向量用法,请务必查阅 Vec<T> 的API 文档,了解标准库为它定义的许多实用方法。例如,除了 push 之外,pop 方法还可以移除并返回最后一个元素。

丢弃向量时也会丢弃其元素

和其他 struct 一样,向量离开作用域时也会被释放,如示例 8-10 所示。

fn main() {
    {
        let v = vec![1, 2, 3, 4];

        // do stuff with v
    } // <- v goes out of scope and is freed here
}
示例 8-10:展示向量及其元素被丢弃的位置

向量被丢弃时,其中的所有内容也会被丢弃,这意味着它所持有的整数会被清理。借用检查器会确保只有在向量本身有效期间,才能使用指向其内容的引用。

接下来我们继续介绍下一种集合类型:String!