【Rust学习】08_1_常见集合_向量

前言

Rust的标准库包含许多非常有用的数据结构,称为集合。大多数其他数据类型代表一个特定的值,但集合可以包含多个值。与内置的数组和元组类型不同,这些集合指向的数据存储在堆上,这意味着数据的数量不需要在编译时知道,并且可以在程序运行时增长或缩小。每种集合都有不同的能力和成本,选择适合当前情况的集合是您会随着时间推移而发展的一项技能。在本章中,我们将讨论 Rust 程序中经常使用的三个集合:

  • 向量允许您将可变数量的值彼此相邻存储。
  • 字符串是字符的集合。我们之前已经提到了 String 类型,但在本章中我们将深入讨论它。
  • 哈希映射允许您将值与特定键关联。它是更通用的数据结构(称为映射)的一种特殊实现

要了解 standard 库提供的其他类型的集合,请参阅文档

现在让我们开始讨论如何创建和更新 vector、字符串和 hash map,以及它们的特殊之处。

内容

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

创建一个向量

要创建一个新的空向量,我们调用Vec::new函数,如下所示:

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

请注意,我们在这里添加了一个类型注解。因为我们没有向这个向量中插入任何值,Rust 不知道我们打算存储什么类型的元素。这是一个重要的观点。向量是使用泛型实现的;我们将在后续的章节中介绍如何将泛型与您自己的类型一起使用。现在,请知道标准库提供的 Vec<T> 类型可以容纳任何类型。当我们创建一个向量来保存特定类型时,我们可以在尖括号内指定类型。如上所示,我们告诉 Rust,Vec<T> 将保存 i32 类型的元素。

更常见的情况是,你会用初始值创建一个Vec<T>,Rust会推断出你想存储的值的类型,所以你很少需要做这种类型注释。Rust方便地提供了 vec!宏,它会创建一个新的向量来保存你给它的值。若下所示,创建了一个新的Vec<i32>,它保存了值1、2、3。整数类型是i32,因为这是默认的整数类型。

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

因为我们已经给出了初始的 i32 值,Rust 可以推断 v的类型是 Vec<i32>,所以类型注释不是必需的。接下来,我们将了解如何修改向量。

更新向量

我们可以使用push方法向向量中添加元素,如下所示:

fn main() {
    // let v: Vec<i32> = Vec::new();
    let mut v = vec![1, 2, 3];
    v.push(5);
    v.push(6);
    v.push(7);
    v.push(8);
    println!("v,{:?}",v);
}

与任何变量一样,如果我们想能够更改其值,则需要使用 mut 关键字使其可变。

读取向量元素

有两种方法可以引用存储在向量中的值:通过索引或使用get方法。在以下示例中,为了更加清晰,我们对从这些函数返回的值的类型进行了注释。

fn main() {
    // let v: Vec<i32> = Vec::new();
    let mut v = vec![1, 2, 3];
    v.push(5);
    v.push(6);
    v.push(7);
    v.push(8);
    println!("v,{:?}",v);
    
    // read vector 
    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."),
    }
}

我们使用索引值 2 来获取第三个元素,因为向量是按数字索引的,从 0 开始。使用 &[] 可以得到对索引值处元素的引用。当我们使用 get方法并将索引作为参数传递时,我们会得到一个 Option<&T> 我们可以将其与 match 一起使用。

现在我们知道了读取向量的两种方法,那么如果我们尝试访问范围外的元素会怎么样呢?让我们一起来试试吧。

fn main() {
    let v = vec![1, 2, 3];
    // read element outside the range of existing elements.
    let does_not_exist = &v[100];
    println!("use Index read outside the range element: {}",does_not_exist);
}

首先看下通过索引来获取,现在我们得到的结果如下,当我们引用一个不存在的元素的时候,程序直接崩溃了。

/Users/wangyang/.cargo/bin/cargo run --color=always --package n08_vectors --bin n08_vectors --profile dev
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/n08_vectors`
thread 'main' panicked at src/main.rs:20:28:
index out of bounds: the len is 7 but the index is 100
stack backtrace:
   0: rust_begin_unwind
             at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/std/src/panicking.rs:662:5
   1: core::panicking::panic_fmt
             at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/panicking.rs:74:14
   2: core::panicking::panic_bounds_check
             at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/panicking.rs:276:5
   3: <usize as core::slice::index::SliceIndex<[T]>>::index
             at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/slice/index.rs:302:10
   4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
             at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/slice/index.rs:16:9
   5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
             at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/alloc/src/vec/mod.rs:2920:9
   6: n08_vectors::main
             at ./src/main.rs:20:28
   7: core::ops::function::FnOnce::call_once
             at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

现在让我们看看使用get引用不存在的元素的时候会怎样。

fn main() {
    let v = vec![1, 2, 3];
    // read element outside the range of existing elements.
    let does_not_exist = v.get(100);
    println!("use get read outside the range element: {:?}",does_not_exist);
}

正如你所看到的那样,get并没有导致程序的崩溃,而是返回了None

/Users/wangyang/.cargo/bin/cargo run --color=always --package n08_vectors --bin n08_vectors --profile dev
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/n08_vectors`
use get read outside the range element: None

根据这两种情况,所以当我们希望程序在尝试访问超出向量末尾的元素时崩溃时,最好使用索引访问。如果我们不希望程序崩溃而且有相应的逻辑处理None的话,那我们可以使用get方法。

当程序有一个有效的引用时,借用检查器会执行所有权和借用规则以确保这个引用以及任何其他对向量内容的引用保持有效。不能在同一个作用域内同时拥有可变和不可变的引用。这个规则适用于下面代码的情况,我们持有一个对向量中第一个元素的不可变引用,并尝试在末尾添加一个元素。如果我们还试图在函数后面引用该元素,这个程序将无法工作。

fn main() {
    let mut v = vec![1, 2, 3];
    let first = &v[0];
    v.push(4);
    println!("The first element is: {first}");
}

当我们尝试编译的时候,我们将得到下面的错误:

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
  --> src/main.rs:31:5
   |
29 |     let first = &v[0];
   |                  - immutable borrow occurs here
30 |
31 |     v.push(4);
   |     ^^^^^^^^^ mutable borrow occurs here
32 |
33 |     println!("The first element is: {first}");
   |                                     ------- immutable borrow later used here

为什么对第一个元素的引用需要关心向量末尾的变化呢?这个错误是由于向量的工作方式造成的:因为向量在内存中将值相邻放置,所以在向量的末尾添加一个新元素可能需要分配新的内存,并将旧元素复制到新的空间,如果当前存储向量的地方没有足够的空间将所有元素相邻放置的话。在这种情况下,对第一个元素的引用将指向已释放的内存。借用规则防止程序陷入这种情况。

遍历向量中的值

要依次访问向量中的每个元素,我们将遍历所有元素,而不是使用索引一次访问一个。现在让我们使用 for 循环来获取向量中元素并打印他们。

fn main() {
    let v = vec![1, 2, 3];
    for i in &v {
        println!("{i}");
    }
}

现在我们迭代向量用的元素,并进行可变引用,对元素进行更改。

fn main() {
    let mut v = vec![1, 2, 3];
    for i in &mut v {
        *i += 50;
    }
}

要更改可变引用所指向的值,我们必须使用*解引用操作符来获取i中的值,然后才能使用+=操作符。

遍历一个向量,无论是不可变的还是可变的,都是安全的,因为借用检查器的规则。如果我们尝试在for循环体中插入或删除项目,我们将得到一个编译器的错误。for循环包含的向量引用也阻止了对整个向量的同时修改。

使用枚举存储多种类型

向量只能存储相同类型的值。这肯定是不方便的;绝对有需要存储不同类型项目列表的使用场景。幸运的是,枚举的成员是在相同的枚举类型下定义的,所以当我们需要一个类型来表示不同类型的元素时,我们可以定义并使用枚举!

例如,假设我们想从电子表格中的一行中获取值,其中行中的一些列包含整数、一些浮点数和一些字符串。我们可以定义一个枚举,其变体将包含不同的值类型,并且所有枚举变体都将被视为相同的类型:枚举的类型。然后我们可以创建一个 vector 来保存该枚举,因此,最终,保存不同的类型。我们已经在示例 8-9 中演示了这一点。

例如,假设我们想从一个电子表格的某一行中获取值,该行的某些列包含整数、一些浮点数和一些字符串。我们可以定义一个枚举,其成员将包含不同的值类型,所有枚举成员将被视为相同的类型:即枚举的类型。然后我们可以创建一个向量来保存该枚举,从而最终保存不同的类型,如下所示:

fn main() {
    #[derive(Debug)]
    #[allow(dead_code)]
    enum SpreadsheetCell {
        Int(i32),
        Float(f64),
        Text(String),
    }

    let row = vec![
        SpreadsheetCell::Int(3),
        SpreadsheetCell::Text(String::from("blue")),
        SpreadsheetCell::Float(10.12),
    ];

    println!("row:{:?}", row);
}

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

如果你不知道程序在运行时将获取哪些详尽的类型集合以存储在向量中,那么枚举就不起作用。相反,你可以使用一个trait对象。

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

删除向量

像任何其他结构体一样,当向量超出范围时会被释放,如下所示:

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

        // do stuff with v
    } // <- v goes out of scope and is freed here
}

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

posted @ 2024-11-26 16:22:56 王洋 阅读(43) 评论(0)
发表评论
昵称
邮箱
网址