# 使用向量存储值列表

我们要看的第一个集合类型是Vec<T>,也称为向量。 向量允许你在单个数据结构中存储多个值,这些值在内存中彼此相邻排列。向量只能存储相同类型的值。当你有一个项目列表时,它们非常有用,例如文件中的文本行或购物车中商品的价格。

# 创建新向量

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

fn main() {
    let v: Vec<i32> = Vec::new();
}

示例 8-1 创建一个新的空向量来保存i32类型的值

注意我们在这里添加了类型注解。因为我们没有向这个向量插入任何值,Rust 不知道我们打算存储什么类型的元素。这一点很重要。向量是使用泛型实现的;我们将在第 10 章讨论如何在自己的类型中使用泛型。现在,只需知道标准库提供的Vec<T>类型可以保存任何类型。当我们创建一个向量来保存特定类型时,我们可以在尖括号中指定类型。在代码示例 8-1 中,我们告诉 Rustv中的Vec<T>将保存i32类型的元素。

更常见的是,你会创建一个带有初始值的Vec<T>,Rust 会推断你想要存储的值的类型,因此你很少需要做这种类型注解。Rust 方便地提供了vec!宏,它将创建一个保存你给定值的新向量。代码示例 8-2 创建了一个新的Vec<i32>,保存值123。整数类型是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方法访问向量中的项目

这里需要注意一些细节。我们使用索引值2来获取第三个元素,因为向量是按数字索引的,从零开始。使用&[]给我们一个对索引值处元素的引用。当我们使用get方法并传入索引作为参数时,我们得到一个Option<&T>,可以与match一起使用。

Rust 提供了这两种引用元素的方法,因此你可以选择当尝试使用超出现有元素范围的索引值时程序的行为方式。例如,让我们看看当我们有一个包含五个元素的向量,然后尝试用每种技术访问索引 100 处的元素时会发生什么,如代码示例 8-5 所示。

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 处的元素

当我们运行这段代码时,第一个[]方法将导致程序崩溃,因为它引用了一个不存在的元素。当你希望程序在尝试访问超出向量末尾的元素时不崩溃,最好使用 get 方法。

get方法传递一个超出向量范围的索引时,它会返回None而不会崩溃。如果在正常情况下偶尔会访问超出向量范围的元素,你可以使用此方法。然后你的代码将有逻辑来处理Some(&element)None,如第 6 章所讨论的。例如,索引可能来自用户输入的数字。如果他们不小心输入了一个太大的数字,程序得到一个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>类型的实现细节的更多信息,请参阅《Rust 秘典》 (opens new window)

# 遍历向量中的值

要依次访问向量中的每个元素,我们将遍历所有元素,而不是使用索引一次访问一个。代码示例 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 定义一个enum以在一个向量中存储不同类型的值

Rust 需要在编译时知道向量中将包含哪些类型,以便确切知道堆上需要多少内存来存储每个元素。我们还必须明确说明此向量中允许哪些类型。如果 Rust 允许向量保存任何类型,那么一种或多种类型可能会导致对向量元素执行的操作出错。使用枚举加上match表达式意味着 Rust 将在编译时确保处理所有可能的情况,如第 6 章所讨论的。

如果你不知道程序在运行时将获得哪些类型的详尽集合来存储在向量中,枚举技术将不起作用。相反,你可以使用 trait 对象,我们将在第 18 章中介绍。

现在我们已经讨论了使用向量的一些最常见方法,请务必查看API 文档 (opens new window)以了解标准库在Vec<T>上定义的所有许多有用方法。例如,除了push之外,pop方法会删除并返回最后一个元素。

# 丢弃向量会丢弃其元素

与任何其他struct一样,当向量超出范围时会被释放,如代码示例 8-10 所示。

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

        // 在此处使用v进行操作
    } // <- v在此处离开作用域并被释放
}

示例 8-10 显示向量及其元素被丢弃的位置

当向量被丢弃时,其所有内容也会被丢弃,这意味着它保存的整数将被清理。借用检查器确保对向量内容的任何引用仅在向量本身有效时使用。

让我们继续讨论下一个集合类型:String