使用矢量值存储值的清单

Storing Lists of Values with Vectors

这里要看的第一个集合,便是 Vec<T>,也叫做 矢量(vector) 类型。矢量类型允许将多个值,存储在单个的、将全部这些值挨个放入内存的数据结构中。矢量类型仅能存储同一类型的这些值。在有着某个数据项目清单,比如某个文件中的那些文本行,或购物车中那些货品价格时,那么矢量类型就是有用的。

创建一个新的矢量值

要创建出一个新的空矢量值,就要调用 Vec::new() 函数,如下清单 8-1 所示:

#![allow(unused)]
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 就创建了一个新的、保存了值 123Vec<i32>。之所以那个整数类型为 i32,是由于 i32 正是默认的整数类型,如同第 3 章的 "数据类型" 中所讨论的那样。

#![allow(unused)]
fn main() {
let v = vec! [1, 2, 3];
}

清单 8-2:创建一个新的包含了值的矢量

由于这里已经给定了一些初始化 i32 的值,因此 Rust 就可以推断出 v 的类型为 Vec<i32>,而那个类型注解就不是必要的了。接下来,就要看看怎样修改矢量。

更新某个矢量值

要创建出一个矢量,并随后将一些元素添加给他,就可以使用 push 方法,如下清单 8-3 中所示。

#![allow(unused)]
fn main() {
    let mut v = Vec::new();

    v.push(5);
    v.push(6);
    v.push(7);
    v.push(8);
}

清单 8-3:使用 push 方法来把一些值添加到某个矢量

正如第 3 章中所讨论的,这里与任何变量一样,在想要能修改矢量的值时,就要使用 mut 关键字,将其构造为可变。这里在矢量内部的数字,全部都是 i32 类型,而 Rust 就会从这些数据,推断出这个类型,因此这里不需要 Vec<i32> 类型注解。

读取矢量的元素

Reading Elements of Vectors

引用存储在矢量中某个值的方式有两种:经由索引,或使用 get 方法。在接下来的示例中,为讲得更清楚的原因,已经对从这些方法返回的值进行了注释。

下面清单 8-5 给出了访问矢量某个值的两种方式,即索引语法与 get 方法。

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

    let third: &i32 = &v[2];
    println! ("第三个元素为 {}", third);

    match v.get(2) {
        Some(third) => println! ("第三个元素为 {}", third),
        None => println! ("没有第三个元素。"),
    }
}

清单 8-5:使用索引语法或 get 方法访问矢量中的某个元素

请留意这里的两个细节。首先,由于矢量是以从零开始的数字进行索引,因此这里使用了索引值 2 来获取那第三个元素。其次,这里是通过同时使用 &[],获取第三个元素的,这就给到一个引用变量,而使用带有作为参数传递索引的 get 方法,给到的却是个 Option<&T> 值。

Rust 提供这两种引用某个元素方式的原因在于,有了这两种方式,就可以在尝试使用某个超出了既有元素范围的索引值时,对程序此时的表现加以选择。比如,下面就来看看在有着一个五个元素的矢量,而随后尝试以两种技巧,来访问位于索引 100 处元素时,会发生什么事情,如下清单 8-6 中所示。

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

    let does_not_exist = &v[100];
    let does_not_exist = v.get(100);
}

清单 8-6:尝试访问包含五个元素矢量中索引 100 处的元素

在运行此代码时,由于第一种 [] 方式引用了不存在的元素,因此将导致程序死机。在有着某个对超出矢量末端的元素进行访问的尝试,而打算将程序崩溃掉时,那么用这种方式是最佳的。

而在传递给 get 方法的索引,位于矢量外部时,他就会返回不会程序死机的 None 值。在寻常情况下,就会时不时出现对超出矢量范围元素的访问时,就应使用这种方式。这时代码就将有如同第 6 章中所讨论的,处理 Some(&element)None 值的逻辑。比如,那个索引可以是来自某人输入的数字。在他们不小心输入了一个过大的数字时,程序就会得到一个 None 值,这时就可以告诉用户在当前矢量中有多少个项目,并给到他们又一次输入有效值的机会。比起由于输入错误而将程序崩溃掉,这将是更加用户友好!

当程序有了有效引用时,Rust 的借用检查器,就会强制执行所有权与借用规则检查(在第 4 章讲到过),来确保该引用及全部其他的、到这个矢量内容的引用是有效的。请回顾那条表明了不能在同一作用域中,有着多个可变与不可变引用的规则。那条规则就适用于下面清单 8-7,清单中有着一个到矢量首个元素的可变引用,并尝试将一个元素添加到示例末尾。若同时在那个函数中尝试引用那个元素,那么该程序就不会工作:

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

    let first: &i32 = &v[0];

    v.push(6);

    println! ("首个元素为:{}", first);
}

清单 8-7:在保留到矢量某个条目的引用同时,尝试将一个元素添加到该矢量

对此代码进行编译,将引发下面这个错误:

$ cargo run                     ✔
   Compiling vec_demo v0.1.0 (/home/peng/rust-lang/projects/vec_demo)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:5
  |
4 |     let first: &i32 = &v[0];
  |                        - immutable borrow occurs here
5 |
6 |     v.push(6);
  |     ^^^^^^^^^ mutable borrow occurs here
7 |
8 |     println! ("首个元素为:{}", first);
  |                                 ----- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `vec_demo` due to previous error

清单 8-7 中的代码看起来似乎可以工作:为何到矢量首个元素的引用,会牵连到该矢量末尾的变化呢?这个报错是由于矢量工作原理:由于矢量是将他的那些值,挨个放在内存中的,那么将新元素添加到矢量末尾,而在该矢量当前存储处,没有足够场所来挨个放置全部这些元素时,这时就会要求分配新内存,并将那些旧有元素拷贝到新内存空间。在那种情况下,到首个元素的引用就会指向已解分配内存(deallocated memory)。而正是这些 Rust 的借用规则,防止程序已这样的情形而告终。

请注意:更多有关 Vec<T> 类型的实现细节,请参考 Rust 专论(The Rustonomicon)

迭代矢量中的值

Iterating over the Values in a Vector

要依次访问矢量中的各个元素,就要迭代全部元素,而非使用那些索引值,一次访问一个了。下面清单 8-8 展示了怎样使用 for 循环,来获取到一个 i32 矢量值中各个元素的不可变引用,并将这些元素打印出来。

#![allow(unused)]
fn main() {
    let v = vec! [100, 32, 57];

    for i in &v {
        println! ("{}", i);
    }
}

清单 8-8:通过使用 for 循环对各个元素进行迭代,而打印出矢量中的每个元素

也可以为了对全部元素进行修改,而对可变示例中的各个元素,进行可变引用的迭代。下面清单 8-9 中的 for 循环,将把 50 添加到各个元素。

#![allow(unused)]
fn main() {
    let mut v = vec! [100, 32, 57];

    for i in &mut v {
        *i += 50;
    }
}

清单 8-9:对实例中各个元素,进行可变引用的迭代

要修改可变引用所指向的值,就必须使用 * 解引用操作符(the * dereference operator),在能够使用 += 运算符之前,获取到 i 中的那个值。在后面第 15 章的 “以解引用操作符,顺着指针找到值” 小节,就会讲到这个解引用操作符。

运用枚举存储多种类型

Using an Enum to Store Multiple Types

矢量只能存储同一类型的值。这就会不方便;显然是有需要存储不同类型条目清单的使用场景。幸运的是,枚举的那些变种,就是定义在同一枚举类型之下的,那么在需要某种表示那些不同类型元素的类型时,就可以定义并使用一个枚举!

好比说这里要从电子表格的某行,其中该行的一些列包含了整数,另一些列包含了浮点数,而其他列则包含了字符串,而要从这行获取到这些数据。那么就可以定义这样一个枚举,他的那些变种将保存这些不同值类型,而全部这些变种,就会被看着是同一类型:即为该枚举。随后就可以创建一个矢量,来保存那个枚举,进而最终保存了这些不同类型。下面清单 8-10 中就对此进行了演示。

#![allow(unused)]
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-10:定义一个 enum 来在矢量中存储不同类型的值

Rust 需要在编译时了解那个矢量中会有些什么类型,这样他就清楚存储该矢量的每个元素,所需要的内存堆上内存准确数量。同时必须显式声明该矢量中允许哪些类型。若 Rust 允许矢量保存任意类型,那么就会存在一个或多个类型,将引发在该矢量的元素上执行操作错误的可能。运用枚举加上 match 表达式,就意味着 Rust 将在编译时,确保所有可能情形都被处理,如同第 6 章中讨论的那样。

但若不清楚运行时程序会在矢量中收到的详尽类型集合,那么这个枚举技巧就不会有用。相反,这个时候就可以使用特质对象(a trait object),而在后面的第 17 章就会讲到特质对象。

既然这里已经讨论了使用矢量的一些最常用方式,那就一定要看看 API 文档,了解那些定义在 Vec<T> 上,由标准库所定义的全部有用方法。比如,除了 push 之外,pop 方法会移除并返回矢量的最后一个元素。

丢弃某个矢量,就会丢弃他的元素

Dropping a Vector Drops Its Elements

与其他任何 struct 一样,矢量在超出作用域时,就会被释放掉,如下清单 8-4 所示。

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

        // 对 v 执行一些操作
    } // 这里 v 就超出了作用域,而被释放掉
}

清单 8-4:对矢量及其元素在何处被丢弃进行展示

在这个矢量被丢弃时,那么他所有内容也会被丢弃,即他保存的那些整数将被清理掉。这初一看似乎直接明了,然而在开始触及到一些到该矢量元素的引用时,事情就会变得复杂。接下来就要解决这个问题!

下面就移步到下一种集合类型:String 吧!

Last change: 2023-11-30, commit: 6021b30