Rust基础拾遗--结构体、枚举与模式、迭代器、集合


前言

   通过Rust程序设计-第二版笔记的形式对Rust相关重点知识进行汇总,读者通读此系列文章就可以轻松的把该语言基础捡起来。


1.结构体

Rust 有 3 种结构体类型:具名字段型结构体元组型结构体单元型结构体

1.1 具名字段型结构体

具名字段型结构体的定义如下所示:

/// 由8位灰度像素组成的矩形
struct GrayscaleMap {
    pixels: Vec<u8>,
    size: (usize, usize)
}

   声明一个 GrayscaleMap 类型,其中包含两个给定类型的字段,分别名为 pixelssize。Rust 约定中所有类型的名称都将每个单词的第一个字母大写(如 GrayscaleMap),这称为大驼峰格式。字段和方法是小写的,单词之间用下划线分隔,这称为蛇形格式snake_case)。

可以使用结构体表达式构造出此类型的值:

let width = 1024;
let height = 576;
let image = GrayscaleMap {
    pixels: vec![0; width * height],
    size: (width, height)
};

   结构体表达式以类型名称(GrayscaleMap)开头,后跟一对花括号,其中列出了每个字段的名称和值。还有用来从与字段同名的局部变量或参数填充字段的简写形式:

fn new_map(size: (usize, usize), pixels: Vec<u8>) -> GrayscaleMap {
    assert_eq!(pixels.len(), size.0 * size.1);
    GrayscaleMap { pixels, size }
}

   结构体表达式 GrayscaleMap { pixels, size }GrayscaleMap { pixels: pixels, size: size } 的简写形式。你可以对某些字段使用 key: value 语法,而对同一结构体表达式中的其他字段使用简写语法。

要访问结构体的字段,可以使用 . 运算符:

assert_eq!(image.size, (1024, 576));
assert_eq!(image.pixels.len(), 1024 * 576);

   与所有其他语法项一样,结构体默认情况下是私有的,仅在声明它们的模块及其子模块中可见。你可以通过在结构体的定义前加上 pub 来使结构体在其模块外部可见。

结构体中的每个字段默认情况下也是私有的:

/// 由8位灰度像素组成的矩形
pub struct GrayscaleMap {
    pub pixels: Vec<u8>,
    pub size: (usize, usize)
}

   其他模块可以使用此结构体及其任何公共的关联函数,但不能按名称访问私有字段或使用结构体表达式来创建新的 GrayscaleMap 值。也就是说,要创建结构体型的值,就需要结构体的所有字段都可见。**这就是为什么不能编写结构体表达式来创建新的 String 或 Vec?**这些标准类型都是结构体,但它们的所有字段都是私有的。如果想创建一个值,就必须使用公共的类型关联函数,比如 Vec::new()

   创建具名字段结构体的值时,可以使用另一个相同类型的结构体为省略的那些字段提供值。在结构体表达式中,如果具名字段后面跟着 … EXPR,则任何未提及的字段都会从 EXPR(必须是相同结构体类型的另一个值)中获取它们的值。假设我们有一个代表游戏中怪物的结构体:

// 在这个游戏中,怪物是一些扫帚。你会看到:
struct Broom {
    name: String,
    height: u32,
    health: u32,
    position: (f32, f32, f32),
    intent: BroomIntent
}

/// `Broom`可以支持的两种用途
#[derive(Copy, Clone)]
enum BroomIntent { FetchWater, DumpWater }

   《魔法师的学徒》:一个新手魔法师对一把扫帚施了魔法,让它为自己工作,但工作完成后不知道如何让它停下来。于是,他用斧头将扫帚砍成了两半,结果一把扫帚变成了两把,虽然每把扫帚的大小只有原始扫帚的一半,但仍然具有和原始扫帚一样的“工作热情”。

// 按值接收输入的Broom(扫帚),并获得所有权
fn chop(b: Broom) -> (Broom, Broom) {
    // 主要从`b`初始化`broom1`,只修改`height`。由于`String`
    // 不是`Copy`类型,因此`broom1`获得了`b`中`name`的所有权
    let mut broom1 = Broom { height: b.height / 2, .. b };
    // 主要从`broom1`初始化`broom2`。由于`String`不是`Copy`类型,
    // 因此我们显式克隆了`name`
    let mut broom2 = Broom { name: broom1.name.clone(), .. broom1 };
    // 为每一半扫帚分别起一个名字
    broom1.name.push_str(" I");
    broom2.name.push_str(" II");
    (broom1, broom2)
}

有了这个定义,我们就可以制作一把扫帚,把它一分为二,然后看看会得到什么:

let hokey = Broom {
    name: "Hokey".to_string(),
    height: 60,
    health: 100,
    position: (100.0, 200.0, 0.0),
    intent: BroomIntent::FetchWater
};

let (hokey1, hokey2) = chop(hokey);
assert_eq!(hokey1.name, "Hokey I");
assert_eq!(hokey1.height, 30);
assert_eq!(hokey1.health, 100);

assert_eq!(hokey2.name, "Hokey II");
assert_eq!(hokey2.height, 30);
assert_eq!(hokey2.health, 100);

新的扫帚 hokey1 和 hokey2 获得了修改后的名字,长度只有原来的一半,但生命值都跟原始扫帚一样。

1.2 元组型结构体

元组型结构体类似于元组:

struct Bounds(usize, usize);

构造此类型的值与构造元组非常相似,只是必须包含结构体名称:

let image_bounds = Bounds(1024, 768);

元组型结构体保存的值称为元素,就像元组的值一样。你可以像访问元组一样访问它们:

assert_eq!(image_bounds.0 * image_bounds.1, 786432);

元组型结构体的单个元素可以是公共的,也可以不是:

pub struct Bounds(pub usize, pub usize);

表达式 Bounds(1024, 768) 看起来像一个函数调用,实际上它确实是,即定义这种类型时也隐式定义了一个函数:

fn Bounds(elem0: usize, elem1: usize) -> Bounds { ... }

   在最基本的层面上,具名字段型结构体和元组型结构体非常相似。选择使用哪一个需要考虑易读性、无歧义性和简洁性。如果你喜欢用 . 运算符来获取值的各个组件,那么用名称来标识字段就能为读者提供更多信息,并且更容易防范拼写错误。如果你通常使用模式匹配来查找这些元素,那么元组型结构体会更好用。

   元组型结构体适用于创造新类型(newtype),即建立一个只包含单组件的结构体,以获得更严格的类型检查。如果你正在使用纯 ASCII 文本,那么可以像下面这样定义一个新类型:

struct Ascii(Vec<u8>);

   将此类型用于 ASCII 字符串比简单地传递 Vec< u8> 缓冲区并在注释中解释它们的内容要好得多。在将其他类型的字节缓冲区传给需要 ASCII 文本的函数时,这种新类型能帮 Rust 捕获错误。

1.3 单元型结构体

单元型结构体声明了一个根本没有元素的结构体类型:

struct Onesuch;

   这种类型的值不占用内存,很像单元类型 ()。Rust 既不会在内存中实际存储单元型结构体的值,也不会生成代码来对它们进行操作,因为仅通过值的类型它就能知道关于值的所有信息。但从逻辑上讲,空结构体是一种可以像其他任何类型一样有值的类型。或者更准确地说,空结构体是一种只有一个值的类型:

let o = Onesuch;

   有关 .. 范围运算符的内容,你已经遇到过单元型结构体。像 3…5 这样的表达式是结构体值 Range { start: 3, end: 5 } 的简写形式,而表达式 …(一个省略两个端点的范围)是单元型结构体值 RangeFull 的简写形式。单元型结构体在处理特型时也很有用。

1.4 结构体布局

   在内存中,具名字段型结构体和元组型结构体是一样的:值(可能是混合类型)的集合以特定方式在内存中布局。例如,在本章前面我们定义了下面这个结构体:

struct GrayscaleMap {
    pixels: Vec<u8>,
    size: (usize, usize)
}

内存中GrayscaleMap结构体
在这里插入图片描述
   Rust 没有具体承诺它将如何在内存中对结构体的字段或元素进行排序,上图 仅展示了一种可能的安排。然而,Rust 确实承诺会将字段的值直接存储在结构体本身的内存块中。 Rust 会将 pixels 值和 size 值直接嵌入 GrayscaleMap 值中。只有由 pixels 向量拥有的在堆上分配的缓冲区才会留在它自己的块中。

1.5 用impl定义方法

   在本书中,我们一直在对各种值调用方法,比如使用 v.push(e) 将元素推送到向量上、使用 v.len() 获取向量的长度、使用 r.expect(“msg”) 检查 Result 值是否有错误,等等。你也可以在自己的结构体类型上定义方法。Rust 方法不会在结构体定义中,而是会出现在单独的 impl 块中。

   impl 块只是 fn 定义的集合,每个定义都会成为块顶部命名的结构体类型上的一个方法。例如,这里我们定义了一个公共的 Queue 结构体,然后为它定义了 push 和 pop 这两个公共方法:

/// 字符的先入先出队列
pub struct Queue {
    older: Vec<char>, // 较旧的元素,最早进来的在后面
    younger: Vec<char> // 较新的元素,最后进来的在后面
}
impl Queue {
    /// 把字符推入队列的最后
    pub fn push(&mut self, c: char) {
        self.younger.push(c);
    }
    /// 从队列的前面弹出一个字符。如果确实有要弹出的字符,
    /// 就返回`Some(c)`;如果队列为空,则返回`None`
    pub fn pop(&mut self) -> Option<char> {
        if self.older.is_empty() {
            if self.younger.is_empty() {
                return None;
            }
            // 将younger中的元素移到older中,并按照所承诺的顺序排列它们
            use std::mem::swap;
            swap(&mut self.older, &mut self.younger);
            self.older.reverse();
        }
        // 现在older能保证有值了。Vec的pop方法已经
        // 返回一个Option,所以可以放心使用了
        self.older.pop()
    }
}

   在 impl 块中定义的函数称为关联函数,因为它们是与特定类型相关联的。与关联函数相对的是自由函数,它是未定义在 impl 块中的语法项。

   Rust 会将调用关联函数的结构体值作为第一个参数传给方法,该参数必须具有特殊名称 self。由于 self 的类型显然就是在 impl 块顶部命名的类型或对该类型的引用,因此 Rust 允许你省略类型,并以 self、&self 或 &mut self 作为 self: Queue、self: &Queue 或 self: &mut Queue 的简写形式。

如果你愿意,也可以使用完整形式,但如前所述,几乎所有 Rust 代码都会使用简写形式。

   在我们的示例中,push 方法和 pop 方法会通过 self.older 和 self.younger 来引用 Queue 的字段。在 C++ 和 Java 中,“this” 对象的成员可以在方法主体中直接可见,不用加 this. 限定符,而 Rust 方法中则必须显式使用 self 来引用调用此方法的结构体值,这类似于 Python 方法中使用 self 以及 JavaScript 方法中使用 this 的方式。

   由于 push 和 pop 需要修改 Queue,因此它们都接受 &mut self 参数。然而,当调用一个方法时,你不需要自己借用可变引用,常规的方法调用语法就已经隐式处理了这一点。因此,有了这些定义,你就可以像下面这样使用 Queue 了:

let mut q = Queue { older: Vec::new(), younger: Vec::new() };

q.push('0');
q.push('1');
assert_eq!(q.pop(), Some('0'));

q.push('∞');
assert_eq!(q.pop(), Some('1'));
assert_eq!(q.pop(), Some('∞'));
assert_eq!(q.pop(), None);

   只需编写 q.push(…) 就可以借入对 q 的可变引用,就好像你写的是 (&mut q).push(…) 一样,因为这是 push 方法的 self 参数所要求的。

如果一个方法不需要修改 self,那么可以将其定义为接受共享引用:

impl Queue {
    pub fn is_empty(&self) -> bool {
        self.older.is_empty() && self.younger.is_empty()
    }
}

   但请注意,由于 split 通过值获取 self,因此这会将 Queue 从 q 中移动出去,使 q 变成未初始化状态。由于 split 的 self 现在拥有此队列,因此它能够将这些单独的向量移出队列并返回给调用者。

有时,像这样通过值或引用获取 self 还是不够的,因此 Rust 还允许通过智能指针类型传递 self。

1.5.1 以Box、Rc或Arc形式传入self

   方法的 self 参数也可以是 Box 类型、Rc 类型或 Arc 类型。这种方法只能在给定的指针类型值上调用。调用该方法会将指针的所有权传给它。你通常不需要这么做。如果一个方法期望通过引用接受 self,那它在任何指针类型上调用时都可以正常工作:

let mut bq = Box::new(Queue::new());

// `Queue::push`需要一个`&mut Queue`,但`bq`是一个`Box<Queue>`
// 这没问题:Rust在调用期间从`Box`借入了`&mut Queue`
bq.push('■');

   对于方法调用和字段访问,Rust 会自动从 Box、Rc、Arc 等指针类型中借入引用,因此 &self 和 &mut self 几乎总是(偶尔也会用一下 self)方法签名里的正确选择。

   但是如果某些方法确实需要获取指向 Self 的指针的所有权,并且其调用者手头恰好有这样一个指针,那么 Rust 也允许你将它作为方法的 self 参数传入。为此,你必须明确写出 self 的类型,就好像它是普通参数一样。

impl Node {
    fn append_to(self: Rc<Self>, parent: &mut Node) {
        parent.children.push(self);
    }
}

1.5.2 类型关联函数

   给定类型的 impl 块还可以定义根本不以 self 为参数的函数。这些函数仍然是关联函数,因为它们在 impl 块中,但它们不是方法,因为它们不接受 self 参数。为了将它们与方法区分开来,我们称其为类型关联函数

它们通常用于提供构造函数,如下所示:

impl Queue {
    pub fn new() -> Queue {
        Queue { older: Vec::new(), younger: Vec::new() }
    }
}

要使用此函数,需要写成 Queue::new,即类型名称 + 双冒号 + 函数名称:

let mut q = Queue::new();

q.push('*');
...

   在 Rust 中,构造函数通常按惯例命名为 new,我们已经见过 Vec::new、Box::new、HashMap::new 等。但是 new 这个名字并没有什么特别之处,它不是关键字。类型通常还有其他关联函数作为构造函数,比如 Vec::with_capacity。

   虽然对于一个类型可以有许多独立的 impl 块,但它们必须都在定义该类型的同一个 crate 中。不过,Rust 确实允许你将自己的方法附加到其他类型中。

如果你习惯了用 C++ 或 Java,那么将类型的方法与其定义分开可能看起来很不寻常,但这样做有几个优点。

  • 找出一个类型的数据成员总是很容易。在大型 C++ 类定义中,你可能需要浏览数百行成员函数的定义才能确保没有遗漏该类的任何数据成员,而在 Rust 中,它们都在同一个地方。
  • 尽管可以把方法放到具名字段型结构体中,但对元组型结构体和单元型结构体来说这看上去不那么简洁。将方法提取到一个 impl 块中可以让所有这 3 种结构体使用同一套语法。事实上,Rust 还使用相同的语法在根本不是结构体的类型(比如 enum 类型和像 i32 这样的原始类型)上定义方法。(任何类型都可以有方法,这是 Rust 很少使用对象这个术语的原因之一,它更喜欢将所有东西都称为值。)
  • 同样的 impl 语法也可以巧妙地用于实现特型。

1.6 关联常量

   Rust 在其类型系统中的另一个特性也采用了类似于 C# 和 Java 的思想,有些值是与类型而不是该类型的特定实例关联起来的。在 Rust 中,这些叫作关联常量

   关联常量是常量值。它们通常用于表示指定类型下的常用值。例如,你可以定义一个用于线性代数的二维向量和一个关联的单位向量:

pub struct Vector2 {
    x: f32,
    y: f32,
}

impl Vector2 {
    const ZERO: Vector2 = Vector2 { x: 0.0, y: 0.0 };
    const UNIT: Vector2 = Vector2 { x: 1.0, y: 0.0 };
}

   这些值是和类型本身相关联的,你可以在不必引用 Vector2 的任一实例的情况下使用它们。这与关联函数非常相似,使用的名字是与其关联的类型名,后面跟着它们自己的名字:

let scaled = Vector2::UNIT.scaled_by(2.0);

   关联常量的类型不必是其所关联的类型,我们可以使用此特性为类型添加 ID 或名称。如果有多种类似于 Vector2 的类型需要写入文件然后加载到内存中,则可以使用关联常量来添加名称或数值 ID,这些名称或数值 ID 可以写在数据旁边以标识其类型。

impl Vector2 {
    const NAME: &'static str = "Vector2";
    const ID: u32 = 18;
}

1.7 泛型结构体

   前面对 Queue 的定义并不令人满意:它是为存储字符而写的,但是它的结构体或方法根本没有任何专门针对字符的内容。如果我们要定义另一个包含 String 值的结构体,那么除了将 char 替换为 String 外,其余代码可以完全相同。这纯属浪费时间。

   Rust 结构体可以是泛型的,这意味着它们的定义是一个模板,你可以在其中插入任何自己喜欢的类型。例如,下面是 Queue 的定义,它可以保存任意类型的值:

pub struct Queue<T> {
    older: Vec<T>,
    younger: Vec<T>
}

   可以把 Queue< T> 中的 读作“对于任意元素类型 T……”。所以上面的定义可以解读为:“对于任意元素类型 T,Queue 有两个 Vec 类型的字段。”例如,在 Queue< String>中,T 是 String,所以 older 和 younger 的类型都是 Vec< String>。而在 Queue< char> 中,T 是 char,我们最终得到的结构体与最初那个针对 char 定义的结构体是一样的。事实上,Vec 本身也是一个泛型结构体,它就是这样定义的。

在泛型结构体定义中,尖括号(<>)中的类型名称叫作类型参数。泛型结构体的 impl 块如下所示:

impl<T> Queue<T> {
    pub fn new() -> Queue<T> {
        Queue { older: Vec::new(), younger: Vec::new() }
    }

    pub fn push(&mut self, t: T) {
        self.younger.push(t);
    }

    pub fn is_empty(&self) -> bool {
        self.older.is_empty() && self.younger.is_empty()
    }

    ...
}

   可以将 impl Queue 这一行解读为“对于任意元素类型 T,这里有一些在 Queue 上可用的关联函数。”然后,你可以使用类型参数 T 作为关联函数定义中的类型。

   语法可能看起来有点儿累赘,但 impl 可以清楚地表明 impl 块能涵盖任意类型 T,这便能将它与为某种特定类型的 Queue 编写的 impl 块区分开来,如下所示:

impl Queue<f64> {
    fn sum(&self) -> f64 {
        ...
    }
}

   这个 impl 块标头表明“这里有一些专门用于 Queue 的关联函数”。这为 Queue 提供了一个 sum 方法,不过该方法在其他类型的 Queue 上不可用。

   我们在前面的代码中使用了 Rust 的 self 参数简写形式,如果到处都写成 Queue,则让人觉得拗口且容易分心。作为另一种简写形式,每个 impl 块,无论是不是泛型,都会将特殊类型的参数 Self(注意这里是大驼峰 CamelCase)定义为我们要为其添加方法的任意类型。对前面的代码来说,Self 就应该是 Queue,因此我们可以进一步缩写 Queue::new 的定义:

pub fn new() -> Self {
    Queue { older: Vec::new(), younger: Vec::new() }
}

   你可能注意到了,在 new 的函数体中,不需要在构造表达式中写入类型参数,简单地写 Queue { … } 就足够了。这是 Rust 的类型推断在起作用:由于只有一种类型适用于该函数的返回值(Queue),因此 Rust 为我们补齐了该类型参数。但是,你始终都要在函数签名和类型定义中提供类型参数。Rust 不会推断这些,相反,它会以这些显式类型为基础,推断函数体内的类型。

   Self 也可以这样使用,我们可以改写成 Self { … }。你觉得哪种写法最容易理解就写成哪种。在调用关联函数时,可以使用 ::<>(比目鱼)表示法显式地提供类型参数:

let mut q = Queue::<char>::new();

但实际上,通常可以让 Rust 帮你推断出来:

let mut q = Queue::new();
let mut r = Queue::new();

q.push("CAD");  // 显然是Queue<&'static str>
r.push(0.74);   // 显然是Queue<f64>

q.push("BTC");   // 2019年6月一比特币值多少美元
r.push(13764.0); // Rust可没能力检测出非理性繁荣

   事实上,我们在本书中经常这样使用另一种泛型结构体类型 Vec。不仅结构体可以是泛型的,枚举同样可以接受类型参数,而且语法也非常相似。

1.8 带生命周期参数的泛型结构体

   如果结构体类型包含引用,则必须为这些引用的生命周期命名。例如,下面这个结构体可能包含对某个切片的最大元素和最小元素的引用:

struct Extrema<'elt> {
    greatest: &'elt i32,
    least: &'elt i32
}

   早些时候,我们建议你把像 struct Queue 这样的声明理解为:给定任意类型 T,都可以创建一个持有该类型的 Queue。同样,可以将 struct Extrema<'elt> 理解为:给定任意生命周期 'elt,都可以创建一个 Extrema<'elt> 来持有对该生命周期的引用。

下面这个函数会扫描切片并返回一个 Extrema 值,这个值的各个字段会引用其中的元素:

fn find_extrema<'s>(slice: &'s [i32]) -> Extrema<'s> {
    let mut greatest = &slice[0];
    let mut least = &slice[0];

    for i in 1..slice.len() {
        if slice[i] < *least { least = &slice[i]; }
        if slice[i] > *greatest { greatest = &slice[i]; }
    }
    Extrema { greatest, least }
}

   在这里,由于 find_extrema 借用了 slice 的元素,而 slice 有生命周期 's,因此我们返回的 Extrema 结构体也使用了 's 作为其引用的生命周期。Rust 总会为各种调用推断其生命周期参数,所以调用 find_extrema 时不需要提及它们:

let a = [0, -3, 0, 15, 48];
let e = find_extrema(&a);
assert_eq!(*e.least, -3);
assert_eq!(*e.greatest, 48);

   因为返回类型的生命周期与参数的生命周期相同是很常见的情况,所以如果有一个显而易见的候选者,那么 Rust 就允许我们省略生命周期。因此也可以把 find_extrema 的签名写成如下形式,意思不变:

fn find_extrema(slice: &[i32]) -> Extrema {
 ...
}

当然,我们的意思也可能是 Extrema<'static>,但这很不寻常。Rust 只为最常见的情况提供了简写形式。

1.9 带常量参数的泛型结构体

泛型结构体也可以接受常量值作为参数。例如,你可以定义一个表示任意次数多项式的类型,如下所示:

/// N - 1 次多项式
struct Polynomial<const N: usize> {
    ///  多项式的系数
    ///
    ///  对于多项式 a + bx + cx<sup>2</sup> + ... + zx<sup>n-1</sup>,其第`i`个元素是 x<sup>i</sup>的系数
    coefficients: [f64; N]
}

   例如,根据这个定义,Polynomial<3> 是一个二次多项式。这里的 子句表示 Polynomial 类型需要一个 usize 值作为它的泛型参数,以此来决定要存储多少个系数。

   与通过字段保存长度和容量而将元素存储在堆中的 Vec 不同,Polynomial 会将其系数(coefficients)直接存储在值中,再无其他字段。长度直接由类型给出。(这里不需要容量的概念,因为 Polynomial 不能动态增长。)

也可以在类型的关联函数中使用参数 N:

impl<const N: usize> Polynomial<N> {
    fn new(coefficients: [f64; N]) -> Polynomial<N> {
        Polynomial { coefficients }
    }
    ///  计算`x`处的多项式的值
    fn eval(&self, x: f64) -> f64 {
        //  秦九韶算法在数值计算上稳定、高效且简单:
        // c<sub>0</sub> + x(c<sub>1</sub> + x(c<sub>2</sub> + x(c<sub>3</sub> + ... x(c[n-1] + x c[n]))))
        let mut sum = 0.0;
        for i in (0..N).rev() {
            sum = self.coefficients[i] + x * sum;
        }
        sum
    }
}

   这里,new 函数会接受一个长度为 N 的数组,并将其元素作为新 Polynomial 值的系数。eval 方法将在 0…N 范围内迭代以找到给定点 x 处的多项式值。

与类型参数和生命周期参数一样,Rust 通常也能为常量参数推断出正确的值:

use std::f64::consts::FRAC_PI_2; // π/2

//  用近似法对`sin`函数求值:sin x ≅ x - 1/6 x³ + 1/120 x<sup>5</sup>
//  误差几乎为 0,相当精确!
let sine_poly = Polynomial::new([0.0, 1.0, 0.0, -1.0/6.0, 0.0,
 1.0/120.0]);
assert_eq!(sine_poly.eval(0.0), 0.0);
assert!((sine_poly.eval(FRAC_PI_2) - 1.).abs() < 0.005);

   由于我们向 Polynomial::new 传递了一个包含 6 个元素的数组,因此 Rust 知道必须构造出一个 Polynomial<6>。eval 方法仅通过查询其 Self 类型就知道 for 循环应该运行多少次迭代。由于长度在编译期是已知的,因此编译器可能会用一些顺序执行的代码完全替换循环。

常量泛型参数可以是任意整数类型、char 或 bool。不允许使用浮点数、枚举和其他类型。

   如果结构体还接受其他种类的泛型参数,则生命周期参数必须排在第一位,然后是类型,接下来是任何 const 值。例如,一个包含引用数组的类型可以这样声明:

struct LumpOfReferences<'a, T, const N: usize> {
    the_lump: [&'a T; N]
}

   常量泛型参数是 Rust 的一个相对较新的功能,目前它们的使用受到了一定的限制。例如,像下面这样定义 Polynomial 显然更好:

/// 一个N次多项式
struct Polynomial<const N: usize> {
    coefficients: [f64; N + 1]
}

   虽然 [f64; N] 没问题,但像 [f64; N + 1] 这样的类型显然对 Rust 来说太过激进了。所以 Rust 暂时施加了这个限制,以避免遇到像下面这样的问题:

struct Ketchup<const N: usize> {
    tomayto: [i32; N & !31],
    tomahto: [i32; N - (N % 32)],
}

   通过计算可知,不管 N 取何值,N & !31 和 N - (N % 32) 总是相等的,因此 tomayto 和 tomahto 始终具有相同的类型。例如,应该允许将任何一个赋值给另一个。但是,如果想让 Rust 的类型检查器识别这种位运算,就需要把一些令人困惑的极端情况引入这种本已相当复杂的语言中,而这会带来复杂度失控的风险。当然,支持像 N + 1 这样的简单表达式是没问题的,并且也确实已经有人在努力教 Rust 顺利处理这些问题。

   由于此处关注的是类型检查器的行为,因此这种限制仅适用于出现在类型中的常量参数,比如数组的长度。在普通表达式中,可以随意使用 N:像 N + 1 和 N & !31 这样的写法是完全可以的。

   如果要为 const 泛型参数提供的值不仅仅是字面量或单个标识符,那么就必须将其括在花括号中,就像 Polynomial<> 这样。此规则能让 Rust 更准确地报告语法错误。

1.10 让结构体类型派生自某些公共特型

结构体很容易编写:

struct Point {
    x: f64,
    y: f64
}

   但是,如果你要开始使用这种 Point 类型,很快就会发现它有点儿难用。像这样写的话,Point 不可复制或克隆,不能用 println!(“{:?}”, point); 打印,而且不支持 == 运算符和 != 运算符。

这些特性中的每一个在 Rust 中都有名称——Copy、Clone、Debug 和 PartialEq,它们被称为特型

如何为自己的结构体手动实现特型?

   对于这些标准特型和其他一些特型,无须手动实现,除非你想要某种自定义行为。Rust 可以自动为你实现它们,而且结果准确无误。只需将 #[derive] 属性添加到结构体上即可:

#[derive(Copy, Clone, Debug, PartialEq)]
struct Point {
    x: f64,
    y: f64
}

   这些特型中的每一个都可以为结构体自动实现特型,但前提是结构体的每个字段都实现了该特型。我们可以要求 Rust 为 Point 派生 PartialEq,因为它的两个字段都是 f64 类型,而 f64 类型已经实现了 PartialEq。

   Rust 还可以派生 PartialOrd,这将增加对比较运算符 <、>、<= 和 >= 的支持。我们在这里并没有这样做,因为比较两个点以了解一个点是否“小于”另一个点是一件很奇怪的事情。毕竟点和点之间并没有任何常规意义上的顺序可言。所以我们选择不让 Point 值支持这些运算符。这种特例就是 Rust 让我们自己编写 #[derive] 属性而不会自动为它派生每一个可能特型的原因之一。而另一个原因是,只要实现某个特型就会自动让它成为公共特性,因此可复制性、可克隆性等都会成为该结构体的公共 API 的一部分,应该慎重选择。

1.11 内部可变性

   可变性与其他任何事物一样:过犹不及,而你通常只需要一点点就够了。假设你的蜘蛛机器人控制系统有一个中心结构体 SpiderRobot,其中包含一些设置和 I/O 句柄。该结构体会在机器人启动时设置好,并且值永不改变:

pub struct SpiderRobot {
    species: String,
    web_enabled: bool,
    leg_devices: [fd::FileDesc; 8],
    ...
}

机器人的每个主要系统由不同的结构体处理,它们都有一个指向 SpiderRobot 的指针:

use std::rc::Rc;

pub struct SpiderSenses {
    robot: Rc<SpiderRobot>, // <--指向设置和I/O的指针
    eyes: [Camera; 32],
    motion: Accelerometer,
    ...
}

   织网、捕食、毒液流量控制等结构体也都有一个 Rc 智能指针。回想一下,Rc 代表引用计数(reference counting),并且 Rc 指向的值始终是共享的,因此将始终不可变。

   现在假设你要使用标准 File 类型向 SpiderRobot 结构体添加一点儿日志记录。但有一个问题:File 必须是可变的。所有用于写入的方法都需要一个可变引用。

   这种情况经常发生。我们需要一个不可变值(SpiderRobot 结构体)中的一丁点儿可变数据(一个 File)。这称为内部可变性。Rust 提供了多种可选方案,本节将讨论两种最直观的类型,即 Cell 和 RefCell,它们都在 std::cell 模块中。

Cell 是一个包含类型 T 的单个私有值的结构体。Cell 唯一的特殊之处在于,即使你对 Cell 本身没有 mut 访问权限,也可以获取和设置这个私有值字段。

Cell::new(value)(新建)

创建一个新的 Cell,将给定的 value 移动进去。

cell.get()(获取)

返回 cell 中值的副本。

cell.set(value)(设置)

将给定的 value 存储在 cell 中,丢弃先前存储的值。
此方法接受一个不可变引用型的 self。

fn set(&self, value: T) // 注意:不是`&mut self`

   当然,这对名为 set 的方法来说是相当不寻常的。迄今为止,Rust 一直在告诉我们如果想更改数据,就需要 mut 型访问。但出于同样的原因,这个不寻常的细节正是 Cell 的全部意义所在。Cell 只是改变不变性规则的一种安全方式——一丝不多,一毫不少。

cell 还有其他一些方法,你可以查阅其文档进行了解。

如果你想在 SpiderRobot 中添加一个简单的计数器,那么 Cell 是一个不错的工具。可以写成如下形式:

use std::cell::Cell;

pub struct SpiderRobot {
 ...
 hardware_error_count: Cell<u32>,
 ...
}

   行为与 borrow() 和 borrow_mut() 一样,但会返回一个 Result。如果该值已被以可变的方式借出,那么这两个方法不会 panic,而是返回一个 Err 值。同样,RefCell 也有一些其他的方法,你可以在其文档中进行查找。仅当你试图打破“可变引用必须独占”的 Rust 规则时,这两个 borrow 方法才会 panic。例如,以下代码会引起 panic:

use std::cell::RefCell;

let ref_cell: RefCell<String> = RefCell::new("hello".to_string());

let r = ref_cell.borrow(); // 正确,返回Ref<String>
let count = r.len(); // 正确,返回"hello".len()
assert_eq!(count, 5);

let mut w = ref_cell.borrow_mut(); // panic:已被借出
w.push_str(" world");

为避免 panic,可以将这两个借用放入不同的块中。这样,在你尝试借用 w 之前,r 已经被丢弃了。

   这很像普通引用的工作方式。唯一的区别是,通常情况下,当你借用一个变量的引用时,Rust 会在编译期进行检查,以确保你在安全地使用该引用。如果检查失败,则会出现编译错误。RefCell 会使用运行期检查强制执行相同的规则。因此,如果你违反了规则,就会收到 panic(对于 try_borrow 和 try_borrow_mut 则会显示 Err)。

现在我们已经准备好把 RefCell 用在 SpiderRobot 类型中了:

pub struct SpiderRobot {
    ...
    log_file: RefCell<File>,
    ...
}

impl SpiderRobot {
    /// 往日志文件中写一行消息
    pub fn log(&self, message: &str) {
        let mut file = self.log_file.borrow_mut();
        // `writeln!`很像`println!`,但会把输出发送到给定的文件中
        writeln!(file, "{}", message).unwrap();
    }
}

变量 file 的类型为 RefMut,我们可以像使用 File 的可变引用一样使用它。

   Cell 很容易使用。虽然不得不调用 .get() 和 .set() 或 .borrow() 和 .borrow_mut() 略显尴尬,但这就是我们为违反规则而付出的代价。还有一个缺点虽不太明显但更严重:Cell 以及包含它的任意类型都不是线程安全的。因此 Rust 不允许多个线程同时访问它们。

   无论一个结构体是具名字段型的还是元组型的,它都是其他值的聚合:如果我有一个 SpiderSenses 结构体,那么就有了指向共享 SpiderRobot 结构体的 Rc 指针、有了眼睛、有了陀螺仪,等等。所以结构体的本质是“和”这个字:我有 X 和 Y。但是如果围绕“或”这个字构建另一种类型呢?也就是说,当你拥有这种类型的值时,你就拥有了 X 或 Y。

2.枚举与模式

在 Rust 中,它们被称为枚举。你可以使用它们来定义自己的类型,其值是一组命名常量。

   例如,你可以定义一个名为 Color 的类型,其值为 Red、Orange、Yellow 等。这种枚举也适用于 Rust,但是 Rust 的枚举远不止于此。Rust 枚举还可以包含数据,甚至是不同类型的数据。例如,Rust 的 Result 类型就是一个枚举,这样的值要么是包含 String 型的 Ok 值,要么是包含 io::Error 的 Err 值。Rust 枚举更像是 C 的联合体,但不同之处在于它是类型安全的。

   只要值可能代表多种事物,枚举就很有用。使用枚举的“代价”是你必须通过模式匹配安全地访问数据,这是本章后半部分的主题。

   如果你用过 Python 中的解包或 JavaScript 中的解构,那么应该很熟悉“模式”这个词,但 Rust 的模式不止于此。Rust 模式有点儿像针对所有数据的正则表达式。它们用于测试一个值是否具有特定的目标形态,可以一次从结构体或元组中把多个字段提取到局部变量中。

和正则表达式一样,模式很简洁,通常能在一行代码中完成全部工作。

本章从枚举的基础知识讲起,

  • 首先展示数据如何关联到枚举的各个变体,以及枚举如何存储在内存中;
  • 然后展示 Rust 的模式和 match(匹配)语句如何基于枚举、结构体、数组和切片简洁地表达逻辑。
  • 模式中还可以包含引用、移动和 if 条件,来让自己更加强大。

2.1 枚举

Rust 中定义枚举很直观:

enum Ordering {
    Less,
    Equal,
    Greater,
}

   这声明了一个具有 3 个可能值的 Ordering 类型,称为变体或构造器:Ordering::LessOrdering::EqualOrdering::Greater。这个特殊的枚举是标准库的一部分,因此 Rust 代码能够直接导入它:

use std::cmp::Ordering;
fn compare(n: i32, m: i32) -> Ordering {
    if n < m {
        Ordering::Less
    } else if n > m {
        Ordering::Greater
    } else {
        Ordering::Equal
    }
}

要导入当前模块中声明的枚举的构造器,请使用 self:

enum Pet {
    Orca,
    Giraffe,
...
}

use self::Pet::*;

2.1.1 带数据的枚举

   有些程序总是要显示精确到毫秒的完整日期和时间,但对大多数应用程序来说,使用粗略的近似值(比如“两个月前”)对用户更友好。我们可以使用之前定义的枚举来编写一个新的 enum,以帮忙解决此问题:

/// 刻意四舍五入后的时间戳,所以程序会显示“6个月前”
/// 而非“2016年2月9日上午9点49分”
#[derive(Copy, Clone, Debug, PartialEq)]
enum RoughTime {
    InThePast(TimeUnit, u32),
    JustNow,
    InTheFuture(TimeUnit, u32),
}

   此枚举中的两个变体 InThePast 和 InTheFuture 能接受参数。这种变体叫作元组型变体。与元组型结构体一样,这些构造器也是可创建新 RoughTime 值的函数:

let four_score_and_seven_years_ago =
    RoughTime::InThePast(TimeUnit::Years, 4 * 20 + 7);

let three_hours_from_now =
    RoughTime::InTheFuture(TimeUnit::Hours, 3);

枚举还可以有结构体型变体,就像普通结构体一样包含一些具名字段:

enum Shape {
    Sphere { center: Point3d, radius: f32 },
    Cuboid { corner1: Point3d, corner2: Point3d },
}

let unit_sphere = Shape::Sphere {
    center: ORIGIN,
    radius: 1.0,
};

   总而言之,Rust 有 3 种枚举变体,没有数据的变体对应于单元型结构体。元组型变体的外观和功能很像元组型结构体。结构体型变体具有花括号和具名字段。单个枚举中可以同时有 3 种类型的变体:

enum RelationshipStatus {
    Single,
    InARelationship,
    ItsComplicated(Option<String>),
    ItsExtremelyComplicated {
        car: DifferentialEquation,
        cdr: EarlyModernistPoem,
    },
}

枚举的所有构造器和字段都与枚举本身具有相同的可见性。

2.1.2 内存中的枚举
   在内存中,带有数据的枚举会以一个小型整数标签加上足以容纳最大变体中所有字段的内存块的格式进行存储。标签字段供 Rust 内部使用。它会区分由哪个构造器创建了值,进而决定这个值应该有哪些字段。

从 Rust 1.50 开始,RoughTime 会占用 8 字节,如图 所示。

在这里插入图片描述
   不过,为了给将来的优化留下余地,Rust 并没有对枚举的内存布局做出任何承诺。在某些情况下,Rust 可以比图 中展示的布局更有效地打包枚举。例如,有些泛型结构体可以在不需要标签的情况下存储。

2.1.3 用枚举表示富数据结构

   枚举对于快速实现树形数据结构也很有用。假设一个 Rust 程序需要处理任意 JSON 数据。在内存中,任何 JSON 文档都可以表示为这种 Rust 类型的值:

use std::collections::HashMap;
enum Json {
    Null,
    Boolean(bool),
    Number(f64),
    String(String),
    Array(Vec<Json>),
    Object(Box<HashMap<String, Json>>),
}

   用自然语言解释这种数据结构还不如直接看 Rust 代码。JSON 标准指定了可以出现在 JSON 文档中的不同数据类型:null、布尔值、数值、字符串、各种 JSON 值的数组以及具有字符串键名和 JSON 值的对象。这里的 Json 枚举只是简单地列出了这些类型而已。

   这不是一个假想的例子。可以在 serde_json 中找到一个非常相似的枚举,serde_json 是 Rust 的结构体序列化库,是 crates.io 上最常下载的 crate 之一。

   这里在表示 Object 的 HashMap 周围加 Box 只是为了让所有 Json 值更紧凑。在内存中,Json 类型的值占用 4 个机器字。而 String 值和 Vec 值占用 3 个机器字,Rust 又添加了一个标签字节。Null 值和 Boolean 值中没有足够的数据来用完所有空间,但所有 Json 值的大小必须相同。因此,额外的空间就用不上了。图 展示了 Json 值在内存中的实际布局的一些示例。

在这里插入图片描述
   HashMap 则更大。如果必须在每个 Json 值中为它留出空间,那么将会非常大,在 8 个机器字左右。但是 Box 是 1 个机器字:它只是指向堆中分配的数据的指针。我们甚至可以通过装箱更多字段来让 Json 更加紧凑。

这里值得注意的是用 Rust 建立这个结构有多么容易。在 C++ 中,可能要为此编写一个类:

class JSON {
private:
    enum Tag {
        Null, Boolean, Number, String, Array, Object
    };
    union Data {
        bool boolean;
        double number;
        shared_ptr<string> str;
        shared_ptr<vector<JSON>> array;
        shared_ptr<unordered_map<string, JSON>> object;

        Data() {}
        ~Data() {}
        ...
    };

    Tag tag;
    Data data;

public:
    bool is_null() const { return tag == Null; }
    bool is_boolean() const { return tag == Boolean; }
    bool get_boolean() const {
        assert(is_boolean());
        return data.boolean;
    }
    void set_boolean(bool value) {
        this->~JSON();  // 清理string/array/object值
        tag = Boolean;
        data.boolean = value;
    }
    ...
};

   写了 30 行代码,我们才刚开了个头。这个类将需要构造函数、析构函数和赋值运算符。还有一种方法是创建一个具有基类 JSON 和子类 JSONBoolean、JSONString 等的类层次结构。无论采用哪种方法,操作完成时,我们的 C++ 版 JSON 库都将有十几个方法。其他程序员需要阅读一定的内容才能掌握并使用它。而整个 Rust 枚举才 8 行代码。

2.1.4 泛型枚举
枚举可以是泛型的。Rust 标准库中的两个例子是该语言中最常用的数据类型:

enum Option<T> {
    None,
    Some(T),
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

   现在你已经很熟悉这些类型了,泛型枚举的语法与泛型结构体是一样的。

   一个不太明显的细节是,当类型 T 是引用、Box 或其他智能指针类型时,Rust 可以省掉 Option 的标签字段。由于这些指针类型都不允许为 0,因此 Rust 可以将 Option> 表示为单个机器字:0 表示 None,非零表示 Some 指针。这能让 Option 类型的值尽量接近于可能为空的 C 或 C++ 指针。不同之处在于 Rust 的类型系统要求你在使用其内容之前检查 Option 是否为 Some。这有效地避免了对空指针解引用。

只需几行代码就可以构建出泛型数据结构:

// `T`组成的有序集合
enum BinaryTree<T> {
    Empty,
    NonEmpty(Box<TreeNode<T>>),
}

// BinaryTree的部件
struct TreeNode<T> {
    element: T,
    left: BinaryTree<T>,
    right: BinaryTree<T>,
}

这几行代码定义了一个 BinaryTree 类型,它可以存储任意数量的 T 类型的值。

   这两个定义中包含了大量信息,因此我们需要花点儿时间逐字解释这些代码。每个 BinaryTree 值要么是 Empty,要么是 NonEmpty。如果是 Empty,那它根本不含任何数据。如果是 NonEmpty,那它就会有一个 Box,即一个指向堆上分配的 TreeNode 的指针。

   每个 TreeNode 值包含一个实际元素以及另外两个 BinaryTree 值。这意味着树可以包含子树,因此 NonEmpty 树可以有任意数量的后代。

   BinaryTree<&str> 类型值的示意图如图所示。与 Option> 一样,Rust 在这里也省略了标签字段,因此 BinaryTree 值占用一个机器字。

在这里插入图片描述
在此树中构建任何特定节点都很简明直观:

use self::BinaryTree::*;
let jupiter_tree = NonEmpty(Box::new(TreeNode {
    element: "Jupiter",
    left: Empty,
    right: Empty,
}));

较大的树可以基于较小的树来构建:

let mars_tree = NonEmpty(Box::new(TreeNode {
    element: "Mars",
    left: jupiter_tree,
    right: mercury_tree,
}));

   自然,此赋值会将 jupiter_node 和 mercury_node 的所有权转移给它们的新父节点。树的其余部分都遵循同样的模式。根节点与其他节点没有什么区别:

let tree = NonEmpty(Box::new(TreeNode {
    element: "Saturn",
    left: mars_tree,
    right: uranus_tree,
}));

稍后本章将展示如何在 BinaryTree 类型上实现一个 add 方法,以便像下面这样写:

let mut tree = BinaryTree::Empty;
for planet in planets {
    tree.add(planet);
}

   无论你的语言背景如何,在 Rust 中创建像 BinaryTree 这样的数据结构都可能需要做一些练习。起初并不容易看出应该把这些 Box 放在哪里。找到可行设计方案的方法之一是画出图 10-3 那样的图,展示你希望这些数据在内存中如何布局。然后从图片倒推出代码。每组方块都表示一个结构体或元组,每个箭头都是一个 Box 或其他智能指针。弄清楚每个字段的类型虽然有点儿难,但仍然是可以解决的。解决此难题的回报是对程序内存进行了更好的控制。

   现在再来说一下本章开头提过的“代价”。枚举的标签字段会占用一点儿内存,最坏情况下可达 8 字节,但这通常可以忽略不计。枚举的真正缺点(如果一定要算的话)是,虽然这些字段真的存在于值中,但 Rust 代码不允许你直接访问它们:

let r = shape.radius;  // 错误:在`Shape`类型上没有`radius`字段

只能用一种安全的方式来访问枚举中的数据,即使用模式。

2.2 模式

如下是定义的 RoughTime 类型:

enum RoughTime {
    InThePast(TimeUnit, u32),
    JustNow,
    InTheFuture(TimeUnit, u32),
}

   假设你有一个 RoughTime 值并希望把它显示在网页上。你需要访问值内的 TimeUnit 字段和 u32 字段。Rust 不允许你通过编写 rough_time.0 和 rough_time.1 来直接访问它们,因为毕竟 rough_time 也可能是没有字段的,比如 RoughTime::JustNow。那么,怎样才能获得数据呢

你需要一个 match 表达式:

fn rough_time_to_english(rt: RoughTime) -> String {
    match rt {
        RoughTime::InThePast(units, count) =>
            format!("{} {} ago", count, units.plural()),
        RoughTime::JustNow =>
            format!("just now"),
        RoughTime::InTheFuture(units, count) =>
            format!("{} {} from now", count, units.plural()),
    }
}

   match 会执行模式匹配,在此示例中,模式就是第 3 行、第 5 行和第 7 行中出现在 => 符号前面的部分。匹配 RoughTime 值的模式很像用于创建 RoughTime 值的表达式。这是刻意的设计。表达式会生成值,模式会消耗值。两者刻意使用了很多相同的语法。

   我们分步了解一下此 match 表达式在运行期会发生什么。假设 rt 是 RoughTime::InTheFuture(TimeUnit::Months, 1) 的值。Rust 会首先尝试将这个值与第 3 行的模式相匹配。如图 所示,二者不匹配。

在这里插入图片描述

   对于枚举、结构体或元组类型的匹配,Rust 的工作方式就像简单地从左到右进行扫描一样,会检查模式的每个组件以查看该值是否与之匹配。如果不匹配,Rust 就会接着尝试下一个模式。第 3 行和第 5 行的模式都不匹配,但是第 7 行的模式匹配成功了,如图 所示。

在这里插入图片描述   模式中包含的简单标识符(如 units 和 count)会成为模式后面代码中的局部变量。值中存在的任何内容都会复制或移动到新变量中。Rust 会在 units 中存储 TimeUnit::Months,在 count 中存储 1,然后运行第 8 行代码,并返回字符串 “1 months from now”。该输出有一个小小的英语语法问题(未处理复数),可以通过在 match 中添加另一个分支来解决:

RoughTime::InTheFuture(unit, 1) =>
    format!("a {} from now", unit.singular()),

   仅当 count 字段恰好为 1 时,才会匹配此分支。请注意,这行新代码必须添加到第 7 行之前。如果将其添加到末尾,那么 Rust 将永远无法访问它,因为第 7 行的模式会匹配所有 InTheFuture 值。如果你犯了这种错误,那么 Rust 编译器将警告发现了 “unreachable pattern”(无法抵达的模式)。

   即使用了新代码,RoughTime::InTheFuture(TimeUnit::Hours, 1) 仍然存在问题:“a hour from now” 这个结果不太正确。唉,这就是英语啊。这也可以通过在 match 中添加另一个分支来解决。

   如本示例所示,模式匹配可以和枚举协同工作,甚至可以测试它们包含的数据,这让 match 成了 C 的 switch 语句的强大而灵活的替代品。迄今为止,我们只看到了匹配枚举值的模式。

2.2.1 模式中的字面量、变量和通配符
   我们已经展示了如何借助 match 表达式来使用枚举。match 也可用来匹配其他类型。当你需要类似 C 语言的 switch 语句的内容时,可以使用针对整数值的 match。像 0 和 1 这样的整型字面量都可以作为模式使用:

match meadow.count_rabbits() {
    0 => {}  // 无话可说
    1 => println!("A rabbit is nosing around in the clover."),
    n => println!("There are {} rabbits hopping about in the meadow", n),
}

   如果草地上没有兔子,就匹配模式 0;如果只有一只兔子,就匹配模式 1;如果有两只或更多的兔子,就匹配第三个模式,即模式 n。模式 n 只是一个变量名,它可以匹配任何值,匹配的值会移动或复制到一个新的局部变量中。所以在这种情况下,meadow.count_rabbits() 的值会存储在一个新的局部变量 n 中,然后打印出来。其他字面量也可以用作模式,包括布尔值、字符,甚至字符串:

let calendar = match settings.get_string("calendar") {
    "gregorian" => Calendar::Gregorian,
    "chinese" => Calendar::Chinese,
    "ethiopian" => Calendar::Ethiopian,
    other => return parse_error("calendar", other),
};

   在这个例子中,other 就像上个例子中的 n 一样充当了包罗万象的模式。这些模式与 switch 语句中的 default 分支起着相同的作用,用于匹配与任何其他模式都无法匹配的值。如果你需要一个包罗万象的模式,但又不关心匹配到的值,那么可以用单个下划线 _ 作为模式,这就是通配符模式:

let caption = match photo.tagged_pet() {
    Pet::Tyrannosaur => "RRRAAAAAHHHHHH",
    Pet::Samoyed => "*dog thoughts*",
    _ => "I'm cute, love me", // 一般性捕获,对任意Pet都生效
};

   这里的通配符模式能匹配任意值,但不会将其存储到任何地方。由于 Rust 要求每个 match 表达式都必须处理所有可能的值,因此最后往往需要一个通配符模式。即使你非常确定其他情况不会发生,也必须至少添加一个后备分支,也许是 panic 的分支。

// 有很多种形状(Shape),但我们只支持“选中”一些文本框
// 或者矩形区域中的所有内容。不能选择椭圆或梯形
match document.selection() {
    Shape::TextSpan(start, end) => paint_text_selection(start, end),
    Shape::Rectangle(rect) => paint_rect_selection(rect),
    _ => panic!("unexpected selection type"),
}

2.2.2 元组型模式与结构体型模式

元组型模式匹配元组。每当你想要在单次 match 中获取多条数据时,元组型模式都非常有用:

fn describe_point(x: i32, y: i32) -> &'static str {
    use std::cmp::Ordering::*;
    match (x.cmp(&0), y.cmp(&0)) {
        (Equal, Equal) => "at the origin",
        (_, Equal) => "on the x axis",
        (Equal, _) => "on the y axis",
        (Greater, Greater) => "in the first quadrant",
        (Less, Greater) => "in the second quadrant",
        _ => "somewhere else",
    }
}

结构体型模式使用花括号,就像结构体表达式一样。结构体型模式包含每个字段的子模式:

match balloon.location {
    Point { x: 0, y: height } =>
        println!("straight up {} meters", height),
    Point { x: x, y: y } =>
        println!("at ({}m, {}m)", x, y),
}

   在此示例中,如果匹配了第一个分支,则 balloon.location.y 会存储在新的局部变量 height 中。假设 balloon.location 的值是 Point { x: 30, y: 40 }。像往常一样,Rust 会依次检查每个模式的每个组件,如图 所示。

在这里插入图片描述
   这会匹配第二个分支,所以输出是 at (30m, 40m)。像 Point { x: x, y: y } 这样的模式在匹配结构体时很常见,而冗余的名称会造成视觉上的混乱,所以 Rust 对此有一个简写形式:Point 。二者的含义是一样的。Point 仍会将某个点的 x 字段和 y 字段分别存储在新的本地变量 x 和 y 中。即使用了简写形式,当我们只关心几个字段时,匹配大型结构体仍然很麻烦:

match get_account(id) {
    ...
    Some(Account {
            name, language,  // <---这两个变量才是我们关心的
            id: _, status: _, address: _, birthday: _, eye_color: _,
            pet: _, security_question: _, hashed_innermost_secret: _,
            is_adamantium_preferred_customer: _, }) =>
        language.show_custom_greeting(name),
}

为避免这种情况,可以使用 … 告诉 Rust 你不关心任何其他字段。

Some(Account { name, language, .. }) =>
    language.show_custom_greeting(name),

2.2.3 数组型模式与切片型模式

   数组型模式匹配数组。数组型模式通常用于过滤一些特殊情况的值,并且在处理那些不同位置的值具有不同含义的数组时也非常有用。例如,在将 HSL(色相、饱和度和亮度)颜色值转换为 RGB(红色、绿色和蓝色)颜色值时,具有零亮度或全亮度的颜色只会是黑色或白色。可以使用 match 表达式来简单地处理这些情况。

fn hsl_to_rgb(hsl: [u8; 3]) -> [u8; 3] {
    match hsl {
        [_, _, 0] => [0, 0, 0],
        [_, _, 255] => [255, 255, 255],
        ...
    }
}

   切片型模式也与此相似,但与数组不同,切片具有可变长度,因此切片型模式不仅匹配值,还匹配长度。… 在切片型模式中能匹配任意数量的元素。

fn greet_people(names: &[&str]) {
    match names {
        [] => { println!("Hello, nobody.") },
        [a] => { println!("Hello, {}.", a) },
        [a, b] => { println!("Hello, {} and {}.", a, b) },
        [a, .., b] => { println!("Hello, everyone from {} to {}.", a, b) }
    }
}

2.2.4 引用型模式

   Rust 模式提供了两种特性来支持引用。ref 模式会借用已匹配值的一部分。& 模式会匹配引用。我们会先介绍 ref 模式。

匹配不可复制的值会移动该值。继续以 account 为例,以下代码是无效的:

match account {
    Account { name, language, .. } => {
        ui.greet(&name, &language);
        ui.show_settings(&account);  // 错误:借用已移动的值`account`
    }
}

   在这里,字段 account.name 和 account.language 会移动到局部变量 name 和 language 中。account 的其余部分均已丢弃。这就是为什么我们之后不能再借用它的引用。

   如果 name 和 language 都是可复制的值,则 Rust 会复制字段而非移动它们,这时上述代码就是有效的。但假设这些是 String 类型,那我们可以做些什么呢?

我们需要一种借用而非移动匹配值的模式。ref 关键字就是这样做的:

match account {
    Account { ref name, ref language, .. } => {
        ui.greet(name, language);
        ui.show_settings(&account);  // 正确
    }
}

   现在局部变量 name 和 language 是对 account 中相应字段的引用。由于 account 只是被借入而没有被消耗,因此继续调用它的方法是没问题的。还可以使用 ref mut 来借入可变引用:

match line_result {
    Err(ref err) => log_error(err),  // `err`是&Error类型的(共享引用)
    Ok(ref mut line) => {            // `line`是&mut String类型的(可变引用)
        trim_comments(line);         // 就地修改此字符串
        handle(line);
    }
}

   模式 Ok(ref mut line) 能匹配任何成功的结果,并借入其成功值的可变引用。与 ref 模式相对的引用型模式是 & 模式。以 & 开头的模式会匹配引用:

match sphere.center() {
    &Point3d { x, y, z } => ...
}

   在此示例中,假设 sphere.center() 会返回对 sphere 中的私有字段的引用,这是 Rust 中的常见模式。返回的值是 Point3d 的地址。如果中心位于原点,则 sphere.center() 会返回 &Point3d { x: 0.0, y: 0.0, z: 0.0 }。模式匹配过程如图 10-7 所示。

在这里插入图片描述

   这有点儿棘手,因为 Rust 在这里会追踪一个指针,我们通常会将追踪指针的操作与 * 运算符而不是 & 运算符联系起来。但要记住,模式和表达式是恰恰相反的。表达式 (x, y) 会把两个值放入一个新的元组中,而模式 (x, y) 则会匹配一个元组并分解成两个值。& 的逻辑也是如此。在表达式中,& 会创建一个引用。在模式中,& 则会匹配一个引用。

   匹配引用时会遵循我们所期望的一切规则。生命周期规则仍然有效。你不能通过共享引用获得可变访问权限,而且不能将值从引用中移动出去,即使对可变引用也是如此。当我们匹配 &Point3d { x, y, z } 时,变量 x、y 和 z 会接受坐标的副本,而原始 Point3d 的值保持不变。这种写法之所以有效,是因为这些字段都是可复制的。如果试图在具有不可复制字段的结构体上这么做,就会出错:

match friend.borrow_car() {
    Some(&Car { engine, .. }) =>  // 错误:不能把借用的值移动出去
        ...
    None => {}
}

   从借来的汽车上搜刮零件可不是君子所为,Rust 同样不会容忍这么做。你可以使用 ref 模式来借用对部件的引用,但并不拥有它:

Some(&Car { ref engine, .. }) =>  // 正确,engine是一个引用

   再来看一个 & 模式的例子。假设我们有一个遍历字符串中各字符的迭代器 chars,并且它有一个返回 Option<&char>(如果有,则是对下一个字符的引用)的方法 chars.peek()。(Peekable 迭代器实际上会返回 Option<&ItemType>)程序可以使用 & 模式来获取它所指向的字符。

match chars.peek() {
    Some(&c) => println!("coming up: {:?}", c),
    None => println!("end of chars"),
}

2.2.5 匹配守卫

   有时,匹配分支会有一些额外的条件,必须满足这些条件才能视为匹配成功。假设我们正在实现一款棋类游戏,它的棋盘是由六边形组成的,而玩家刚刚通过点击移动了一枚棋子。为了确认点击是有效的,我们可能会做如下尝试:

fn check_move(current_hex: Hex, click: Point) -> game::Result<Hex> {
    match point_to_hex(click) {
        None =>
            Err("That's not a game space."),
        Some(current_hex) =>  // 如果用户单击current_hex,就会尝试匹配
                              //(其实它不起作用:请参见下面的解释)
            Err("You are already there! You must click somewhere else."),
        Some(other_hex) =>
            Ok(other_hex)
    }
}

这失败了,因为模式中的标识符引入了新变量。这里的模式 Some(current_hex) 创建了一个新的局部变量 current_hex,它遮蔽了同名参数 current_hex。Rust 发出了几个关于此代码的警告——特别是,match 的最后一个分支是不可达的。解决此问题的一种简单方式是在匹配分支中使用 if 表达式:

match point_to_hex(click) {
    None => Err("That's not a game space."),
    Some(hex) => {
        if hex == current_hex {
            Err("You are already there! You must click somewhere else")
        } else {
            Ok(hex)
        }
    }
}

但 Rust 还提供了匹配守卫,额外的条件必须为真时才能应用此匹配分支,在模式及其分支的 => 标记之间写上 if CONDITION:

match point_to_hex(click) {
    None => Err("That's not a game space."),
    Some(hex) if hex == current_hex =>
        Err("You are already there! You must click somewhere else"),
    Some(hex) => Ok(hex)
}

如果模式匹配成功,但此条件为假,就会继续尝试匹配下一个分支。

2.2.6 匹配多种可能性

对于形如 pat1 | pat2 的模式,如果能匹配其中的任何一个子模式,则认为匹配成功:

let at_end = match chars.peek() {
    Some(&'\r' | &'\n') | None => true,
    _ => false,
};

   在表达式中,| 是按位或运算符,但在这里,它更像正则表达式中的 | 符号。如果 chars. peek() 为 None,或者是某个持有回车符、换行符的 Some,则把 at_end 设置为 true。使用 …= 匹配整个范围的值。范围型模式包括开始值和结束值,因此 ‘0’ …= ‘9’ 会匹配所有 ASCII 数字:

match next_char {
    '0'..='9' => self.read_number(),
    'a'..='z' | 'A'..='Z' => self.read_word(),
    ' ' | '\t' | '\n' => self.skip_whitespace(),
    _ => self.handle_punctuation(),
}

Rust 中还允许使用像 x… 这样的范围型模式,该模式会匹配从 x 到其类型最大值的任何值。但是,目前模式中还不允许使用其他的开区间范围(如 0…100 或 …100)以及无限范围(如 …)。

2.2.7 使用@模式绑定

   最后,x @ pattern 会与给定的 pattern 精确匹配,但成功时,它不会为匹配到的值的各个部分创建变量,而是会创建单个变量 x 并将整个值移动或复制到其中。假设你有如下代码:

match self.get_selection() {
    Shape::Rect(top_left, bottom_right) => {
        optimized_paint(&Shape::Rect(top_left, bottom_right))
    }
    other_shape => {
        paint_outline(other_shape.get_outline())
    }
}

   请注意,第一个分支解包出一个 Shape::Rect 值,却只是为了在下一行重建一个相同的 Shape::Rect 值。像这种代码可以用 @ 模式重写:

rect @ Shape::Rect(..) => {
    optimized_paint(&rect)
}

@ 模式对于各种范围模式也很有用。

match chars.next() {
    Some(digit @ '0'..='9') => read_number(digit, chars),
    ...
},

2.2.8 模式能用在哪里

   尽管模式在 match 表达式中作用最为突出,但它们也可以出现在其他一些地方,通常用于代替标识符。但无论出现在哪里,其含义都是一样的:Rust 不是要将值存储到单个变量中,而是使用模式匹配来拆分值。这意味着模式可用于:

// 把结构体解包成3个局部变量……
let Track { album, track_number, title, .. } = song;

// ……解包某个作为函数参数传入的元组
fn distance_to((x, y): (f64, f64)) -> f64 { ... }

// ……迭代某个HashMap上的键和值
for (id, document) in &cache_map {
    println!("Document #{}: {}", id, document.title);
}

// ……自动对闭包参数解引用(当其他代码给你传入引用,
// 而你更想要一个副本时会很有用)
let sum = numbers.fold(0, |a, &num| a + num);

上述示例中的每一个都节省了两三行样板代码。同样的概念也存在于其他一些语言中:JavaScript 中叫作解构,而 Python 中叫作解包。请注意,上述 4 个示例中都使用了确保匹配的模式。模式 Point3d { x, y, z } 会匹配 Point3d 结构体类型的每个可能值,(x, y) 会匹配任何一个 (f64, f64) 值对,等等。这种始终都可以匹配的模式在 Rust 中是很特殊的,它们叫作不可反驳模式,是唯一能同时用于此处展示的 4 个位置(let 之后、函数参数中、for 之后,以及闭包参数中)的模式。可反驳模式是一种可能不会匹配的模式,比如 Ok(x) 不会匹配错误结果,而 ‘0’ …= ‘9’ 不会匹配字符 ‘Q’。可反驳模式可以用在 match 的分支中,因为 match 就是为此而设计的:如果一个模式无法匹配,那么很清楚接下来会发生什么。在 Rust 程序中,前面的 4 个示例确实是模式可以派上用场的地方,但在这些地方语言不允许匹配失败。if let 表达式和 while let 表达式中也允许使用可反驳模式,这些模式可用于:

// ……处理只有一个枚举值的特例
if let RoughTime::InTheFuture(_, _) = user.date_of_birth() {
    user.set_time_traveler(true);
}

// ……只有当查表成功时才运行某些代码
if let Some(document) = cache_map.get(&id) {
    return send_cached_response(document);
}

// ……重复尝试某些事,直到成功
while let Err(err) = present_cheesy_anti_robot_task() {
    log_robot_attempt(err);
    // 让用户再试一次(此用户仍然可能是人类)
}

// ……在某个迭代器上手动循环
while let Some(_) = lines.peek() {
    read_paragraph(&mut lines);
}

2.2.9 填充二叉树
展示如何实现方法 BinaryTree::add(),它能将一个节点添加到如下的 BinaryTree 类型中:

// `T`的有序集合
enum BinaryTree<T> {
    Empty,
    NonEmpty(Box<TreeNode<T>>),
}

// BinaryTree的部件
struct TreeNode<T> {
    element: T,
    left: BinaryTree<T>,
    right: BinaryTree<T>,
}

   你现在对模式的了解已经足以写出此方法了。对二叉搜索树的解释超出了本书的范畴,如果你已经很熟悉这个主题,可以自己看看它在 Rust 中的表现。

impl<T: Ord> BinaryTree<T> {
    fn add(&mut self, value: T) {
        match *self {
            BinaryTree::Empty => {
                *self = BinaryTree::NonEmpty(Box::new(TreeNode {
                    element: value,
                    left: BinaryTree::Empty,
                    right: BinaryTree::Empty,
                }))
            }
            BinaryTree::NonEmpty(ref mut node) => {
                if value <= node.element {
                    node.left.add(value);
                } else {
                    node.right.add(value);
                }
            }
        }
    }
}

   第 1 行告诉 Rust 我们正在为有序类型的 BinaryTree 定义一个方法。这与我们在泛型结构体上定义方法的语法是完全相同的,详见 9.5 节。

   如果现有的树 *self 是空的,那就很简单了。运行第 5~9 行代码,将 Empty 树更改为 NonEmpty 树即可。此处对 Box::new() 的调用在堆中分配了一个新的 TreeNode。当完成时,树就会包含一个元素。它的左右子树都是 Empty。

如果 *self 不为空,那么我们就会匹配第 11 行代码的模式:

BinaryTree::NonEmpty(ref mut node) => {

   该模式借用了对 Box> 的可变引用,因此我们可以访问和修改该树节点中的数据。该引用名为 node,位于第 12~16 行代码的作用域内。由于此节点中已经有了一个元素,因此代码必须递归调用 .add() 以将此新元素添加到左子树或右子树中。新方法可以像下面这样使用。

let mut tree = BinaryTree::Empty;
tree.add("Mercury");
tree.add("Venus");
...

3.迭代器

迭代器是一个值,它可以生成一系列值,通常用来执行循环操作。

迭代器本身也提供了一组丰富的方法,比如映射(map)、过滤(filter)、连接(join)、收集(collect)等。

fn triangle(n: i32) -> i32 {
    let mut sum = 0;
    for i in 1..=n {
        sum += i;
    }
    sum
}

   表达式 1…=n 是一个 RangeInclusive 型的值。而 RangeInclusive 是一个迭代器,可以生成其起始值到结束值(包括两者)之间的整数,因此你可以将它用作 for 循环的操作数来对从 1 到 n 的值求和。

fn triangle(n: i32) -> i32 {
    (1..=n).fold(0, |sum, item| sum + item)
}

   开始运行时以 0 作为总和,fold 会获取 1…=n 生成的每个值,并以总和(sum)跟当前值(item)为参数调用闭包 |sum, item| sum + item。闭包的返回值会作为新的总和。它返回的最后一个值就是 fold 自身要返回的值——在这个例子中,也就是整个序列的总和。如果你用惯了 for 循环和 while 循环,这种写法可能看起来很奇怪,但一旦习惯了 fold,你就会发现 fold 的表达方式更加清晰和简洁。

   这就是函数式编程语言的标准风格,非常注重表达能力。在发行版中,Rust 会理解 fold 的定义并将其内联到 triangle 中。接下来是将闭包 |sum, item| sum + item 内联到 triangle 中。最后,Rust 会检查合并后的代码并意识到有一种更简单的方法可以对从 1 到 n 的数值求和:其总和总会等于 n * (n+1) / 2。于是 Rust 将 triangle 的整个函数体,包括循环、闭包和所有内容,翻译成了单个乘法指令和几个算术运算。

在本章中,我们将解释以下核心知识点。

  • Iterator 特型和 IntoIterator 特型,两者是 Rust 迭代器的基础。
  • 一个典型的迭代器流水线通常有 3 个阶段:从某种“值源”创建迭代器,通过选择或处理从中流过的值来将一种迭代器适配成另一种迭代器,然后消耗此迭代器生成的值。
  • 如何为自己的类型实现迭代器?

3.1 Iterator 特型与 IntoIterator 特型

迭代器是实现了 std::iter::Iterator 特型的任意值:

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    …… // 很多默认方法
}

   只要可以用某种自然的方式来迭代某种类型,该类型就可以实现 std::iter::IntoIterator,其 into_iter 方法会接受一个值并返回一个迭代器:

trait IntoIterator where Self::IntoIter: Iterator<Item=Self::Item> {
    type Item;
    type IntoIter: Iterator;
    fn into_iter(self) -> Self::IntoIter;
}

   IntoIter 是迭代器本身的类型,而 Item 是它生成的值的类型。任何实现了 IntoIterator 的类型都称为可迭代者,因为你可以随意迭代它。

Rust 的 for 循环会将所有这些部分很好地结合在一起。要遍历向量的元素,你可以这样写:

println!("There's:");
let v = vec!["antimony", "arsenic", "aluminum", "selenium"];

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

下面介绍迭代器的一些术语。

  • 迭代器是实现了 Iterator 的任意类型。
  • 可迭代者是任何实现了 IntoIterator 的类型:你可以通过调用它的 into_iter 方法来获得一个迭代器。在这里,向量引用 &v 就是可迭代者。
  • 迭代器能生成值。
  • 迭代器生成的值是条目。在这里,条目是 “antimony”、“arsenic” 等。
  • 接收迭代器所生成条目的代码是消费者。在这里,for 循环体就是消费者。

   虽然 for 循环总会在其操作数上调用 into_iter,但你也可以直接把迭代器传给 for 循环,比如,在遍历 Range 时就是这样的。所有迭代器都自动实现了 IntoIterator,并带有一个直接返回迭代器的 into_iter 方法。

   如果在返回 None 后再次调用迭代器的 next 方法,则 Iterator 特型没有规定它应该做什么。大多数迭代器只会再次返回 None,但也有例外。(如果这个过程中出了问题,可以参考一下 15.3.7 节中介绍的 fuse 适配器。)

3.2 创建迭代器

iter 方法与 iter_mut 方法

   大多数集合类型提供了 iter(迭代器)方法和 iter_mut(可变迭代器)方法,它们会返回该类型的自然迭代器,为每个条目生成共享引用或可变引用。像 &[T] 和 &mut [T] 这样的数组切片也有 iter 方法和 iter_mut 方法。如果你不打算让 for 循环替你跟迭代器打交道,iter 方法和 iter_mut 方法就是获取迭代器最常用的方法:

IntoIterator 的实现

如果一个类型实现了 IntoIterator,你也可以自行调用它的 into_iter 方法,就像 for 循环一样:

// 大家通常会使用HashSet,但它的迭代顺序是不确定的,
// 因此在这个示例中用了BTreeSet,它的演示效果更好些
use std::collections::BTreeSet;
let mut favorites = BTreeSet::new();
favorites.insert("Lucy in the Sky With Diamonds".to_string());
favorites.insert("Liebesträume No. 3".to_string());
let mut it = favorites.into_iter();
assert_eq!(it.next(), Some("Liebesträume No. 3".to_string()));
assert_eq!(it.next(), Some("Lucy in the Sky With Diamonds".to_string()));
assert_eq!(it.next(), None);

   大多数集合实际上提供了 IntoIterator 的几种实现,用于共享引用(&T)、可变引用(&mut T)和移动(T)。给定一个集合的共享引用,into_iter 会返回一个迭代器,该迭代器会生成对其条目的共享引用。例如,在前面的代码中,(&favorites).into_iter() 会返回一个 Item 类型为 &String 的迭代器。给定对集合的可变引用,into_iter 会返回一个迭代器,该迭代器会生成对其条目的可变引用。如果 vector 是某个 Vec,则调用 (&mut vector).into_iter() 会返回一个 Item 类型为 &mut String 的迭代器。当按值传递集合时,into_iter 会返回一个迭代器,该迭代器会获取集合的所有权并按值返回这些条目,这些条目的所有权会从集合转移给消费者,原始集合在此过程中已被消耗掉了。例如,前面代码中的 favorites.into_iter() 调用返回了一个迭代器,该迭代器会按值生成每个字符串,消费者会获得每个字符串的所有权。当迭代器被丢弃时,BTreeSet 中剩余的所有元素都将被丢弃,并且该集合的空壳也将被丢弃。

from_fn 与 successors

要生成一系列值,有一种简单而通用的方法,那就是提供一个能返回这些值的闭包。

给定返回 Option 的函数,std::iter::from_fn(来自 fn)就会返回一个迭代器,该迭代器会调用 fn 来生成条目。例如:

use rand::random; // 在Cargo.toml中添加dependencies: rand = "0.7"
use std::iter::from_fn;

// 产生1000条端点均匀分布在区间[0, 1]上的随机线段的长度(这并不是
// `rand_distr` crate中能找到的分布类型,但你可以轻易实现一个)
let lengths: Vec<f64> =
 from_fn(|| Some((random::<f64>() - random::<f64>()).abs()))
 .take(1000)
 .collect();

   它会调用 from_fn 来让迭代器产生随机数。由于迭代器总是返回 Some,因此序列永不结束,但我们调用 take(1000) 时会将其限制为前 1000 个元素。然后 collect 会从这 1000 次迭代中构建出向量。这是构造已初始化向量的有效方式,我们会在 15.4.13 节中解释原因。

   如果每个条目都依赖于其前一个条目,那么 std::iter::successors 函数很实用。只需要提供一个初始条目和一个函数,且该函数能接受一个条目并返回下一个条目的 Option。如果返回 None,则迭代结束。例如,下面是编写第 2 章中的曼德博集绘图器的 escape_time 函数的另一种方式:

use num::Complex;
use std::iter::successors;

fn escape_time(c: Complex<f64>, limit: usize) -> Option<usize> {
    let zero = Complex { re: 0.0, im: 0.0 };
    successors(Some(zero), |&z| { Some(z * z + c) })
        .take(limit)
        .enumerate()
        .find(|(_i, z)| z.norm_sqr() > 4.0)
        .map(|(i, _z)| i)
}

   从零开始,successors(后继者)调用会通过反复对最后一个点求平方再加上参数 c 来生成复平面上的一系列点。在绘制曼德博集时,我们想看看这个序列是永远在原点附近打转还是“飞向”无穷远。调用 take(limit) 确定了我们追踪序列的次数限制,然后 enumerate 对每个点进行编号,将每个点 z 变成元组 (i, z)。我们使用 find 来寻找距离原点足够远的第一个点以判断是否逃逸。find 方法会返回一个 Option:如果这样的点存在就返回 Some((i, z)),否则返回 None。调用 Option::map 会将 Some((i, z)) 变成 Some(i),但不会改变 None,因为这正是我们想要的返回值。

   from_fn 和 successors 都接受 FnMut 闭包,因此你的闭包可以捕获和修改周边作用域中的变量。例如,下面的 fibonacci 函数就用 move 闭包来捕获一个变量并将其用作自己的运行状态:

fn fibonacci() -> impl Iterator<Item=usize> {
    let mut state = (0, 1);
    std::iter::from_fn(move || {
        state = (state.1, state.0 + state.1);
        Some(state.0)
    })
}

assert_eq!(fibonacci().take(8).collect::<Vec<_>>(),
           vec![1, 1, 2, 3, 5, 8, 13, 21]);

   需要注意的是,from_fn 方法和 successors 方法非常灵活,你几乎可以将任何对迭代器的使用改写成对其中之一的调用,通过传递复杂的闭包来得到你想要的行为。但这样做浪费了迭代器提供的机会,即使用常见模式的标准名称来更清晰地表达计算中的数据流。在使用这两个方法之前,请确保你已经熟悉本章中的其他迭代器方法,通常其他迭代器是更好的选择。

drain 方法

   有许多集合类型提供了 drain(抽取)方法。drain 会接受一个对集合的可变引用,并返回一个迭代器,该迭代器会将每个元素的所有权传给消费者。然而,与按值获取并消耗掉集合的 into_iter() 方法不同,drain 只会借入对集合的可变引用,当迭代器被丢弃时,它会从集合中移除所有剩余元素以清空集合。

对于可以按范围索引的类型(如 String、向量和 VecDeque),drain 方法可指定要移除的元素范围,而不是“抽干”整个序列:

let mut outer = "Earth".to_string();
let inner = String::from_iter(outer.drain(1..4));

assert_eq!(outer, "Eh");
assert_eq!(inner, "art");

如果确实需要“抽干”整个序列,使用整个范围(…)作为参数即可。

3.3 迭代器适配器

   一旦你手头有了迭代器,迭代器的 Iterator 特型就会提供大量适配器方法(也可以简称为适配器)。适配器会消耗某个迭代器并构建一个实现了特定行为的新迭代器。为了阐明适配器的工作原理,我们将从两个最流行的适配器 map 和 filter 开始,然后介绍其他适配器,涵盖了你能想到的从其他序列生成值序列的几乎所有方式:截断、跳过、组合、反转、连接、重复等。

map 与 filter

Iterator 特型的 map(映射)适配器能针对迭代器的各个条目调用闭包来帮你转换迭代器。filter 适配器能使用闭包来帮你从迭代器中过滤某些条目,由闭包决定保留和丢弃哪些条目。

假设你正在逐行遍历文本并希望去掉每一行的前导空格和尾随空格。标准库的 str::trim 方法能从单个 &str 中丢弃前导空格和尾随空格,返回一个新的、修剪过的 &str 借用。你可以通过 map 适配器将 str::trim 应用于迭代器中的每一行:

let text = " ponies \n giraffes\niguanas \nsquid".to_string();
let v: Vec<&str> = text.lines()
 .map(str::trim)
 .collect();
assert_eq!(v, ["ponies", "giraffes", "iguanas", "squid"]);

text.lines() 调用会返回一个生成字符串中各行的迭代器。在该迭代器上调用 map 会返回第二个迭代器,第二个迭代器会对每一行调用 str::trim 并将生成的结果作为自己的条目。最后,collect 会将这些条目收集到一个向量中。map 返回的迭代器本身当然也可以进一步适配。如果你想将结果中的 iguanas 排除,可以这样写:

filter_map 与 flat_map
flatten
take 与 take_while
skip 与 skip_while
peekable
fuse
可逆迭代器与 rev
inspect
chain
enumerate
zip
by_ref
cloned 与 copied
cycle

3.4 消耗迭代器

简单累加:count、sum 和 product
min 与 max
max_by 与 min_by
max_by_key 与 min_by_key
对条目序列进行比较
any 与 all
position、rposition 和 ExactSizeIterator
fold 与 rfold
try_fold 与 try_rfold
nth 与 nth_back
last
find、rfind 和 find_map
构建集合:collect 与 FromIterator
Extend 特型
partition
for_each 与 try_for_each
实现自己的迭代器

4.集合

4.1 概述

首先,移动和借用无处不在。Rust 使用移动来避免对值做深拷贝。这就是 Vec::push(item) 方法会按值而非按引用来获取参数的原因。这样值就会移动到向量中。第 4 章中的示意图展示了这在实践中是如何实现的:将 Rust String 压入 Vec 中会很快,因为 Rust 不必复制字符串的字符数据,并且字符串的所有权始终是明晰的。

其次,Rust 没有失效型错误,也就是当程序仍持有指向集合内部数据的指针时,集合被重新调整大小或发生其他变化而导致的那种悬空指针错误。失效型错误是 C++ 中未定义行为的另一个来源,即使在内存安全的语言中,它们也会偶尔导致 ConcurrentModificationException。Rust 的借用检查器在编译期就可以排除这些错误。

最后,Rust 没有 null,因此在其他语言使用 null 的地方 Rust 会使用 Option。

集合描述C++
Vec< T >可增长数组vector
VecDeque< T >双端队列(可增长的环形缓冲区)deque
LinkedList< T >双向链表list
BinaryHeap< T > where T: Ord最大堆priority_queue
HashMap<K,V> where K: Eq + Hash键值哈希表unordered_map
BTreeMap<K,V> where K: Ord有序键值表map
HashSet< T > where T: Eq + Hash无序的、基于哈希的集unordered_set
BTreeSet< T > where T: Ord有序集set

Vec< T >、HashMap<K,V> 和 HashSet< T > 是最常用的集合类型。本章会依次讨论每种集合类型。

Vec< T >(普通向量)

可增长的、分配在堆上的 T 类型值数组。本章会用大约一半的篇幅专门介绍 Vec 及其众多实用方法。

HashMap<K, V>(哈希 Map)

由键 - 值对构成的表。通过键查找值很快。其条目会以任意顺序存储。

HashSet< T >(哈希 Set)

由 T 类型的值组成的 Set。它既能很快地添加值和移除值,也能很快地查询给定值是否在此 Set 中。

4.2 Vec< T >

创建向量的最简单方法是使用 vec! 宏:

// 创建一个空向量
let mut numbers: Vec<i32> = vec![];

// 使用给定内容创建一个向量
let words = vec!["step", "on", "no", "pets"];
let mut buffer = vec![0u8; 1024]; // 1024个内容为0的字节

向量具有 3 个字段:长度、容量和指向用于存储元素的堆分配内存的指针。图 16-1 展示了前面的向量在内存中的布局方式。空向量 numbers 最初的容量为 0。直到添加第一个元素之前,不会为其分配堆内存。

在这里插入图片描述
与所有集合一样,Vec 也实现了 std::iter::FromIterator,所以可以使用迭代器的 .collect() 方法从任意迭代器创建向量。

// 把另一个集合转换成向量
let my_vec = my_set.into_iter().collect::<Vec<String>>();

访问元素

通过索引来获取数组、切片或向量的元素非常简单:

// 获取某个元素的引用
let first_line = &lines[0];

// 获取某个元素的副本
let fifth_number = numbers[4]; // 要求实现了Copy特型
let second_line = lines[1].clone(); // 要求实现了Clone特型

// 获取切片的引用
let my_ref = &buffer[4..12];

// 获取切片的副本
let my_copy = buffer[4..12].to_vec(); // 要求实现了Clone特型

如果索引超出了范围,则所有这些形式都会引发 panic。Rust 向量的长度和索引都是 usize 类型。试图用 u32、u64 或 isize 作为向量索引会导致出错。

Rust 对数值类型很挑剔,对向量也不例外。向量的长度和索引都是 usize 类型。试图用 u32、u64 或 isize 作为向量索引会导致出错。

下面这些方法可以轻松访问向量或切片的特定元素(请注意,所有的切片方法也都适用于数组和向量)。slice.first()(第一个)返回对 slice 的第一个元素的引用(如果有的话)。返回类型为 Option<&T>,所以如果 slice 为空则返回值为 None,如果不为空则返回 Some(&slice[0])。

if let Some(item) = v.first() {
    println!("We got one! {}", item);
}

slice.last()(最后一个)与 first 类似,但会返回对最后一个元素的引用。slice.get(index)(获取)如果其存在,就返回 slice[index] 引用的 Some 值。如果 slice 的元素少于 index+1 个,则返回 None。

let slice = [0, 1, 2, 3];
assert_eq!(slice.get(2), Some(&2));
assert_eq!(slice.get(4), None);

slice.first_mut()(第一个可变)、slice.last_mut()(最后一个可变)和 slice.get_mut(index)(获取可变)这些方法是前述 slice.first() 等方法的变体,但借入的是可变引用。

let mut slice = [0, 1, 2, 3];
{
    let last = slice.last_mut().unwrap();   // last的类型是&mut i32
    assert_eq!(*last, 3);
    *last = 100;
}
assert_eq!(slice, [0, 1, 2, 100]);

因为按值返回 T 就意味着移动它,所以一些需要就地访问元素的方法通常会按引用返回这些元素。.to_vec() 方法是一个例外,它会复制这些元素。slice.to_vec()(转向量)克隆整个切片,返回一个新向量:

let v = [1, 2, 3, 4, 5, 6, 7, 8, 9];
assert_eq!(v.to_vec(),
           vec![1, 2, 3, 4, 5, 6, 7, 8, 9]);
assert_eq!(v[0..6].to_vec(),
           vec![1, 2, 3, 4, 5, 6]);

此方法只能用在元素可以克隆的情况下,也就是需满足 where T: Clone 限界。

迭代

向量、数组和切片是可迭代的,要么按值迭代,要么按引用迭代

  • 遍历 Vec 或数组 [T; N] 会生成 T 类型的条目。这些元素会逐个从向量或数组中移动出来并被消耗掉。
  • 遍历 &[T; N]、&[T] 或 &Vec 类型的值(对数组、切片或向量的引用)会生成 &T 类型的条目,即对单个元素的引用,这些元素不会移动出来。
  • 遍历 &mut [T; N]、&mut [T] 或 &mut Vec 类型的值会生成 &mut T 类型的条目。

数组、切片和向量也有 .iter() 方法和 .iter_mut() 方法,以用于创建一个会生成对其元素的引用的迭代器。

扩大向量与收缩向量

联结

拆分

交换

填充

排序与搜索

比较切片

随机元素

Rust 中不存在失效型错误

4.3 VecDeque< T>

4.4 BinaryHeap< T>

4.5 HashMap<K, V>

Map 是键 - 值对[称为条目(entry)]的集合。任何两个条目都不会有相同的键,并且这些条目会始终按某种数据结构进行组织,从而使你可以通过键在 Map 中高效地查找对应的值。简而言之,Map 就是一个查找表。

Rust 提供了两种 Map 类型:HashMap<K, V> 和 BTreeMap<K, V>。这两种类型共享许多相同的方法,区别在于它们如何组织条目以便进行快速查找。

HashMap 会将键和值存储在哈希表中,因此它需要一个实现了 Hash 和 Eq 的键类型 K,即用来求哈希与判断相等性的标准库特型。

HashMap 在内存中的排列方式。深灰色区域表示未使用。所有键、值和缓存的哈希码都存储在一个分配在堆上的表中。添加条目最终会迫使 HashMap 分配一个更大的表并将所有数据移入其中。

在这里插入图片描述
下面是创建 Map 的几种方法。

HashMap::new()(新建)

创建新的空 Map。

iter.collect()(收集)

可用于从键 - 值对创建和填充新的 HashMap。iter 必须是 Iterator 类型的。

HashMap::with_capacity(n)(自带容量)

创建一个新的空 HashMap,其中至少有 n 个条目的空间。与向量一样,HashMap 会将数据存储在分配在堆上的单块内存中,因此它们有容量及其相关方法

hash_map.capacity()
hash_map.reserve(additional) 
hash_map.shrink_to_fit()

条目

HashMap 和 BTreeMap 都有其对应的 Entry(条目)类型。条目的作用旨在消除冗余的 Map 查找。

对 Map 进行迭代

以下几个方法可以对 Map 进行迭代。

  • 按值迭代(for (k, v) in map)以生成 (K, V) 对。这会消耗 Map。
  • 按共享引用迭代(for (k, v) in &map)以生成 (&K, &V) 对。
  • 按可变引用迭代(for (k, v) in &mut map)以生成 (&K, &mut V) 对。(同样,无法对存储在 Map 中的键进行可变访问,因为这些条目是通过它们的键进行组织的。)

与向量类似,Map 也有 .iter() 方法和 .iter_mut() 方法,它们会返回针对“条目引用”的迭代器,可用来迭代 &map 或 &mut map。

4.6 HashSet

Set 是用于快速进行元素存在性测试的集合:

let b1 = large_vector.contains(&"needle"); // 慢,会检查每一个元素
let b2 = large_hash_set.contains(&"needle"); // 快,会按哈希查找

对 Set 进行迭代

当相等的值不完全相同时

针对整个Set的运算

迄今为止,我们看到的大多数 set 方法专注于单个 Set 中的单个值。Set 还有一些对整个 Set 进行运算的方法。set1.intersection(&set2)(交集)返回同时出现在 set1 和 set2 中的值的迭代器。如果想打印同时参加脑外科和火箭科学课程的所有学生的姓名,可以这样写:

for student in brain_class.intersection(&rocket_class) {
    println!("{}", student);
}

有一个运算符能实现同样的效果。&set1 & &set2 会返回一个新 Set,该 Set 是 set1 和 set2 的交集。这是把“二进制按位与”运算符应用在了两个引用之间。这样就会找到同时存在于 set1 和 set2 中的值。

let overachievers = &brain_class & &rocket_class;

set1.union(&set2)(并集)返回存在于 set1 或 set2 中或者同时存在于两者中的值的迭代器。&set1 | &set2 会返回包含所有这些值的新 Set。它会找出所有存在于 set1 或 set2 中的值。set1.difference(&set2)(差集)返回存在于 set1 但不在于 set2 中的值的迭代器。&set1 - &set2 会返回包含所有此类值的新 Set。set1.symmetric_difference(&set2)(对称差集,异或)返回存在于 set1 或 set2 中但不同时存在于两者中的迭代器。&set1 ^ &set2 会返回包含所有此类值的新 Set。以下是测试 Set 之间关系的 3 个方法。set1.is_disjoint(set2)(有交集?)如果 set1 和 set2 没有共同的值,就返回 true——它们之间的交集为空。set1.is_subset(set2)(是子集?)如果 set1 是 set2 的子集,就返回 true。也就是说,set1 中的所有值都在 set2 中。set1.is_superset(set2)(是超集?)与上一个方法相反:如果 set1 是 set2 的超集,就返回 true。Set 还支持使用 == 和 != 进行相等性测试。如果两个 Set 包含完全相同的一组值,那它们就是相等的。

4.7 哈希

std:#️⃣:Hash 是可哈希类型的标准库特型。HashMap 的键和 HashSet 的元素都必须实现 Hash 和 Eq。

大多数实现了 Eq 的内置类型也会实现 Hash。整数、char 和 String 都是可哈希的。对元组、数组、切片和向量来说,只要它们的元素是可哈希的,它们自身就是可哈希的。标准库的一个设计原则是,无论将值存储在何处或如何指向它,都应具有相同的哈希码。因此,引用与其引用的值具有相同的哈希码,而 Box 与其封装的值也具有相同的哈希码。向量 vec 与包含其所有数据的切片 &vec[…] 具有相同的哈希码。String 与具有相同字符的 &str 具有相同的哈希码。默认情况下,结构体和枚举没有实现 Hash,但可以派生一个实现:

/// 大英博物馆藏品中某件物品的ID号
#[derive(Clone, PartialEq, Eq, Hash)]
enum MuseumNumber {
    ...
}

只要此类型的字段都是可哈希的,就可以这样用。如果为一个类型手动实现了 PartialEq,那么也应该手动实现 Hash。假设我们有一个代表无价历史宝藏的类型:

struct Artifact {
    id: MuseumNumber,
    name: String,
    cultures: Vec<Culture>,
    date: RoughTime,
    ...
}

如果两个 Artifact 具有相同的 ID,那么就认为它们是相等的:

impl PartialEq for Artifact {
    fn eq(&self, other: &Artifact) -> bool {
        self.id == other.id
    }
}

impl Eq for Artifact {}

由于我们仅是根据这些收藏品的 ID 来比较它们,因此也必须以相同的方式对这些收藏品进行哈希处理:

use std::hash::;

impl Hash for Artifact {
    fn hash<H: Hasher>(&self, hasher: &mut H) {
        // 把哈希工作委托给藏品编号
        self.id.hash(hasher);
    }
}

(否则,HashSet 将无法正常工作。与所有哈希表一样,它要求如果 a == b,则必然 hash(a) == hash(b)。)这允许我们创建一个 Artifact 的 HashSet:

let mut collection = HashSet::<Artifact>::new();

如上述代码的前一段代码所示,即使要手动实现 Hash,也不需要了解任何有关哈希算法的知识。.hash() 会接收一个表示哈希算法的 Hasher 引用作为参数。你只需将与 == 运算符相关的所有数据提供给这个 Hasher 即可。Hasher 会根据你提供的任何内容计算哈希码。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值