由 C++ 转向 rust 之:(二)进阶入门

2022/4/18

这一章节的指针并发和 C++ 非常相似故而上手很快。

# 智能指针

指针(pointer)是一个包含内存地址的变量的通用概念。 Rust 中最常见的指针是第四章介绍的引用(reference)。 引用以 & 符号为标志并借用了他们所指向的值。 除了引用数据没有任何其他特殊功能。 它们也没有任何额外开销,所以应用得最多。 另一方面,智能指针(smart pointers)是一类数据结构,他们的表现类似指针,但是也拥有额外的元数据和功能。

在 Rust 中,普通引用和智能指针的一个额外的区别是:引用是一类只借用数据的指针;相反,在大部分情况下,智能指针拥有他们指向的数据。

智能指针通常使用结构体实现。 智能指针区别于常规结构体的显著特性在于其实现了 DerefDrop trait

  • Deref trait 允许智能指针结构体实例表现的像引用一样,这样就可以编写既用于引用、又用于智能指针的代码。
  • Drop trait 允许我们自定义当智能指针离开作用域时运行的代码。

最常用的一些智能指针:

  • Box<T>,用于在堆上分配值
  • Rc<T>,一个引用计数类型,其数据可以有多个所有者
  • Ref<T>RefMut<T>,通过 RefCell<T> 访问。RefCell<T> 是一个在运行时而不是在编译时执行借用规则的类型。

# 使用 Box 指向堆上的数据

box 允许你将一个值放在堆上而不是栈上。 留在栈上的则是指向堆数据的指针。

除了数据被储存在堆上而不是栈上之外,box 没有性能损失。不过也没有很多额外的功能。它们多用于如下场景:

  • 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候
  • 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候
  • 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候

# 使用 Box 在堆上储存数据

fn main() {
    let b = Box::new(5);
    println!("b = {}", b);
}   // 当像 b 这样的 box 在 main 的末尾离开作用域时,它将被释放。
    // 这个释放过程作用于 box 本身(位于栈上)和它所指向的数据(位于堆上)。
1
2
3
4
5

# 计算非递归类型的大小

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}
1
2
3
4
5
6

当 Rust 需要知道要为 Message 值分配多少空间时,它可以检查每一个成员并发现 Message::Quit 并不需要任何空间,Message::Move 需要足够储存两个 i32 值的空间,依此类推。 因为 enum 实际上只会使用其中的一个成员,所以 Message 值所需的空间等于储存其最大成员的空间大小。

# 使用 Box 给递归类型一个已知的大小

因为 Box<T> 是一个指针,我们总是知道它需要多少空间:指针本身的大小并不会根据其指向的数据量而改变。 box 只提供了间接存储和堆分配;他们并没有任何其他特殊的功能。

Box<T> 类型是一个智能指针,因为它实现了 Deref trait,它允许 Box<T> 值被当作引用对待。 当Box<T> 值离开作用域时,由于 Box<T> 类型 Drop trait 的实现,box 所指向的堆数据也会被清除。

# 通过 Deref trait 将智能指针当作常规引用处理

实现 Deref trait 允许我们重载解引用运算符(dereference operator)*(与乘法运算符或通配符相区别)。 通过这种方式实现 Deref trait 的智能指针可以被当作常规引用来对待,可以编写操作引用的代码并用于智能指针。


一种理解指针的方式是将其看成指向储存在其他某处值的箭头

fn main() {
    let x = 5;
    let y = &x;
    assert_eq!(5, x);
    assert_eq!(5, *y);  // 使用解引用运算符来跟踪 i32 值的引用
}
1
2
3
4
5
6

fn main() {
    let x = 5;
    let y = Box::new(x);
    assert_eq!(5, x);
    assert_eq!(5, *y);  // 在 Box<i32> 上使用解引用运算符
}
1
2
3
4
5
6

# 自定义智能指针

将要构建的 MyBox 类型与真正的 Box 有一个很大的区别:我们的版本不会在堆上储存数据。这个例子重点关注 Deref,所以其数据实际存放在何处,相比其类似指针的行为来说不算重要。

use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        &self.0   // .0 用来访问元组结构体的第一个元素
    }
}

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);
    assert_eq!(5, x);
    assert_eq!(5, *y);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

deref 方法体中写入了 &self.0,这样 deref 返回了我希望通过 * 运算符访问的值的引用。

没有 Deref trait 的话,编译器只会解引用 & 引用类型。 deref 方法向编译器提供了获取任何实现了 Deref trait 的类型的值,并且调用这个类型的 deref 方法来获取一个它知道如何解引用的 & 引用的能力。

对于 assert_eq!(5, *y); 中的 *y Rust 事实上在底层运行了如下代码:

*(y.deref())
1

deref 方法返回值的引用,以及 *(y.deref()) 括号外边的普通解引用仍为必须的原因在于所有权。 如果 deref 方法直接返回值而不是值的引用,其值(的所有权)将被移出 self。 在这里以及大部分使用解引用运算符的情况下我们并不希望获取 MyBox<T> 内部值的所有权。

注意,每次当我们在代码中使用 * 时,* 运算符都被替换成了先调用 deref 方法再接着使用 * 解引用的操作,且只会发生一次,不会对 * 操作符无限递归替换,解引用出上面 i32 类型的值就停止了。

# 函数和方法的隐式 Deref 强制转换

  • Deref 强制转换(derefcoercions)是 Rust 在函数或方法传参上的一种便利。
  • Deref 强制转换只能作用于实现了 Deref trait 的类型。
  • Deref 强制转换将这样一个类型的引用转换为另一个类型的引用。

当所涉及到的类型定义了 Deref trait,Rust 会分析这些类型并使用任意多次 Deref::deref 调用以获得匹配参数的类型。 这些解析都发生在编译时,所以利用 Deref 强制转换并没有运行时损耗!

# Deref 强制转换如何与可变性交互

类似于如何使用 Deref trait 重载不可变引用的 * 运算符,Rust 提供了 DerefMut trait 用于重载可变引用的 * 运算符。

Rust 在发现类型和 trait 实现满足三种情况时会进行 Deref 强制转换:

  • T: Deref<Target=U> 时从 &T&U
  • T: DerefMut<Target=U> 时从 &mut T&mut U
  • T: Deref<Target=U> 时从 &mut T&U

# 使用 Drop Trait 运行清理代码

对于智能指针模式来说第二个重要的 trait 是 Drop,其允许我们在值要离开作用域时执行一些代码。

在其他一些语言中,我们不得不记住在每次使用完智能指针实例后调用清理内存或资源的代码。如果忘记的话,运行代码的系统可能会因为负荷过重而崩溃。

Drop trait 要求实现一个叫做 drop 的方法,它获取一个 self 的可变引用。

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer {
        data: String::from("my stuff"),
    };
    let d = CustomSmartPointer {
        data: String::from("other stuff"),
    };
    println!("CustomSmartPointers created.");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ cargo run
    Compiling drop-example v0.1.0 (file:///projects/drop-example)
      Finished dev [unoptimized + debuginfo] target(s) in 0.60s
        Running `target/debug/drop-example` CustomSmartPointers created.

Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!
1
2
3
4
5
6
7

当实例离开作用域 Rust 会自动调用 drop,并调用我们指定的代码。 变量以被创建时相反的顺序被丢弃,所以 dc 之前被丢弃。

# 通过 std::mem::drop 提早丢弃值

整个 Drop trait 存在的意义在于其是自动处理的,通常也不需要禁用 drop。 然而,有时你可能需要提早清理某个值。一个例子是当使用智能指针管理锁时;可能希望强制运行 drop 方法来释放锁以便作用域中的其他代码可以获取锁。 Rust 并不允许我们主动调用 Drop traitdrop 方法;当我们希望在作用域结束之前就强制释放变量的话,我们应该使用的是由标准库提供的 std::mem::drop

因为不能禁用当值离开作用域时自动插入的 drop,并且不能显式调用 drop,如果我们需要强制提早清理值,可以使用 std::mem::drop 函数。 std::mem::drop 函数不同于 Drop trait 中的 drop 方法。 可以通过传递希望提早强制丢弃的值作为参数。 std::mem::drop 位于 prelude

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer {
        data: String::from("some data"),
    };
    println!("CustomSmartPointer created.");
    drop(c);
    println!("CustomSmartPointer dropped before the end of main.");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Drop trait 实现中指定的代码可以用于许多方面,来使得清理变得方便和安全:比如可以用其创建我们自己的内存分配器! 通过 Drop trait 和 Rust 所有权系统,你无需担心之后的代码清理,Rust 会自动考虑这些问题。

# Rc 引用计数智能指针

为了启用多所有权,Rust 有一个叫做 Rc<T> 的类型。其名称为引用计数(reference counting)的缩写。 引用计数意味着记录一个值引用的数量来知晓这个值是否仍在被使用。如果某个值有零个引用,就代表没有任何有效引用并可以被清理。

Rc<T> 用于当我们希望在堆上分配一些内存供程序的多个部分读取,而且无法在编译时确定程序的哪一部分会最后结束使用它的时候。

注意 Rc<T> 只能用于单线程场景;

# 使用 Rc 共享数据

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}
1
2
3
4
5
6
7
8
9
10
11
12
13

也可以调用 a.clone() 而不是 Rc::clone(&a),不过在这里 Rust 的习惯是使用 Rc::cloneRc::clone 只会增加引用计数,并不像大部分类型的 clone 实现那样对所有数据进行深拷贝,故而并不会花费多少时间。 通过使用 Rc::clone 进行引用计数,可以明显的区别深拷贝类的克隆和增加引用计数类的克隆。 当查找代码中的性能问题时,只需考虑深拷贝类的克隆而无需考虑 Rc::clone 调用。

# 克隆 Rc 会增加引用计数

在程序中每个引用计数变化的点,会打印出引用计数,其值可以通过调用 Rc::strong_count 函数获得。 这个函数叫做 strong_count 而不是 count 是因为 Rc<T> 也有 weak_count。 通过不可变引用,Rc<T> 允许在程序的多个部分之间只读地共享数据。 如果 Rc<T> 也允许多个可变引用,则会违反第四章讨论的借用规则之一:相同位置的多个可变借用可能造成数据竞争和不一致。 不过可以修改数据是非常有用的!

# RefCell 和内部可变性模式

内部可变性(Interior mutability)是 Rust 中的一个设计模式,它允许你即使在有不可变引用时也可以改变数据,这通常是借用规则所不允许的。 为了改变数据,该模式在数据结构中使用 unsafe 代码来模糊 Rust 通常的可变性和借用规则。

# 通过 RefCell 在运行时检查借用规则

不同于 Rc<T>RefCell<T> 代表其数据的唯一的所有权。 那么是什么让 RefCell<T> 不同于像 Box<T> 这样的类型呢?

  1. 在任意给定时刻,只能拥有一个可变引用或任意数量的不可变引用之一(而不是两者)。
  2. 引用必须总是有效的。

对于引用和 Box<T>,借用规则的不可变性作用于编译时。 对于 RefCell<T>,这些不可变性作用于运行时。 对于引用,如果违反这些规则,会得到一个编译错误。 而对于 RefCell<T>,如果违反这些规则程序会 panic 并退出。

在编译时检查借用规则的优势是这些错误将在开发过程的早期被捕获,同时对运行时没有性能影响,因为所有的分析都提前完成了。 为此,在编译时检查借用规则是大部分情况的最佳选择,这也正是其为何是 Rust 的默认行为。

相反在运行时检查借用规则的好处则是允许出现特定内存安全的场景,而它们在编译时检查中是不允许的。 静态分析,正如 Rust 编译器,是天生保守的。但代码的一些属性不可能通过分析代码发现:其中最著名的就是 停机问题(Halting Problem),这超出了本书的范畴,不过如果你感兴趣的话这是一个值得研究的有趣主题。

因为一些分析是不可能的,如果 Rust 编译器不能通过所有权规则编译,它可能会拒绝一个正确的程序;从这种角度考虑它是保守的。 如果 Rust 接受不正确的程序,那么用户也就不会相信 Rust 所做的保证了。 然而,如果 Rust 拒绝正确的程序,虽然会给程序员带来不便,但不会带来灾难。 RefCell<T> 正是用于当你确信代码遵守借用规则,而编译器不能理解和确定的时候。

类似于 Rc<T>RefCell<T> 只能用于单线程场景。 如果尝试在多线程上下文中使用 RefCell<T>,会得到一个编译错误。

如下为选择 Box<T>Rc<T>RefCell<T> 的理由:

  • Rc<T> 允许相同数据有多个所有者;Box<T>RefCell<T> 有单一所有者。
  • Box<T> 允许在编译时执行不可变或可变借用检查;Rc<T> 仅允许在编译时执行不可变借用检查;RefCell<T> 允许在运行时执行不可变或可变借用检查。
  • 因为 RefCell<T> 允许在运行时执行可变借用检查,所以我们可以在即便 RefCell<T> 自身是不可变的情况下修改其内部的值。

在不可变值内部改变值就是 内部可变性模式

# 内部可变性:不可变值的可变借用

# RefCell 在运行时记录借用

当创建不可变和可变引用时,我们分别使用 &&mut 语法。对于 RefCell<T> 来说,则是 borrowborrow_mut 方法,这属于 RefCell<T> 安全 API 的一部分。 borrow 方法返回 Ref<T> 类型的智能指针,borrow_mut 方法返回 RefMut 类型的智能指针。 这两个类型都实现了 Deref,所以可以当作常规引用对待。

RefCell<T> 记录当前有多少个活动的 Ref<T>RefMut<T> 智能指针。 每次调用 borrowRefCell<T> 将活动的不可变借用计数加一。 当 Ref<T> 值离开作用域时,不可变借用计数减一。 就像编译时借用规则一样,RefCell<T> 在任何时候只允许有多个不可变借用或一个可变借用。

在运行时捕获借用错误而不是编译时意味着将会在开发过程的后期才会发现错误,甚至有可能发布到生产环境才发现;还会因为在运行时而不是编译时记录借用而导致少量的运行时性能惩罚。 然而,使用 RefCell 使得在只允许不可变值的上下文中编写修改自身以记录消息的 mock 对象成为可能。 虽然有取舍,但是我们可以选择使用 RefCell<T> 来获得比常规引用所能提供的更多的功能。

# 结合 Rc 和 RefCell 来拥有多个可变数据所有者

RefCell<T> 的一个常见用法是与 Rc<T> 结合:Rc< RefCell<T> >。 如果有一个储存了 RefCell<T>Rc<T> 的话,就可以得到有多个所有者并且可以修改的值了。

# 引用循环与内存泄漏

Rust 的内存安全性保证使其难以意外地制造永远也不会被清理的内存(被称为 内存泄漏(memory leak)),但并不是不可能。 与在编译时拒绝数据竞争不同,Rust 并不保证完全地避免内存泄漏,这意味着内存泄漏在 Rust 被认为是内存安全的。 这一点可以通过 Rc<T>RefCell<T> 看出:创建引用循环的可能性是存在的。 这会造成内存泄漏,因为每一项的引用计数永远也到不了 0,其值也永远不会被丢弃。

# 制造引用循环

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
enum List {
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, item) => Some(item),
            Nil => None,
        }
    }
}

fn main() {
    // 创建一个引用循环:两个 List 值互相指向彼此

    let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
    println!("a initial rc count = {}", Rc::strong_count(&a));
    println!("a next item = {:?}", a.tail());

    let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
    println!("a rc count after b creation = {}", Rc::strong_count(&a));
    println!("b initial rc count = {}", Rc::strong_count(&b));
    println!("b next item = {:?}", b.tail());

    if let Some(link) = a.tail() {
        *link.borrow_mut() = Rc::clone(&b);
    }

    println!("b rc count after changing a = {}", Rc::strong_count(&b));
    println!("a rc count after changing a = {}", Rc::strong_count(&a));
    // Uncomment the next line to see that we have a cycle;
    // it will overflow the stack
    // println!("a next item = {:?}", a.tail());
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

编译运行:

$ cargo run
  Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished dev [unoptimized + debuginfo] target(s) in 0.53s
      Running `target/debug/cons-list`
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2
1
2
3
4
5
6
7
8
9
10
11

可以看到将列表 a 修改为指向 b 之后,a 和 b 中的 Rc<List> 实例的引用计数都是 2。 在 main 的结尾,Rust 丢弃 b,这会 b Rc<List> 实例的引用计数从 2 减为 1。 然而,b Rc<List> 不能被回收,因为其引用计数是 1 而不是 0。 接下来 Rust 会丢弃 a 将 a Rc<List> 实例的引用计数从 2 减为 1。 这个实例也不能被回收,因为 b Rc<List> 实例依然引用它,所以其引用计数是 1。 这些列表的内存将永远保持未被回收的状态。

如果取消最后 println! 的注释并运行程序,Rust 会尝试打印出 a 指向 b 指向 a 这样的循环直到栈溢出。

这个特定的例子中,创建了引用循环之后程序立刻就结束了。 这个循环的结果并不可怕。 如果在更为复杂的程序中并在循环里分配了很多内存并占有很长时间,有可能压垮系统并造成没有内存可供使用。

创建引用循环并不容易,但也不是不可能。 如果你有包含 Rc<T>RefCell<T> 值或类似的嵌套结合了内部可变性和引用计数的类型,请务必小心确保你没有形成一个引用循环;你无法指望 Rust 帮你捕获它们。 创建引用循环是一个程序上的逻辑 bug,你应该使用自动化测试、代码评审和其他软件开发最佳实践来使其最小化。

另一个解决方案是重新组织数据结构,使得一部分引用拥有所有权而另一部分没有。 换句话说,循环将由一些拥有所有权的关系和一些无所有权的关系组成,只有所有权关系才能影响值是否可以被丢弃。

# 避免引用循环:将 Rc 变为 Weak

到目前为止,我们已经展示了调用 Rc::clone 会增加 Rc<T> 实例的 strong_count,和只在其 strong_count 为 0 时才会被清理的 Rc<T> 实例。 你也可以通过调用 Rc::downgrade 并传递 Rc<T> 实例的引用来创建其值的 弱引用(weak reference)。 调用 Rc::downgrade 时会得到 Weak<T> 类型的智能指针。 不同于将 Rc<T> 实例的 strong_count 加 1,调用 Rc::downgrade 会将 weak_count 加 1。 Rc<T> 类型使用 weak_count 来记录其存在多少个 Weak<T> 引用,类似于 strong_count。 其区别在于 weak_count 无需计数为 0 就能使 Rc<T> 实例被清理。

强引用代表如何共享 Rc<T> 实例的所有权,但弱引用并不属于所有权关系。 他们不会造成引用循环,因为任何弱引用的循环会在其相关的强引用计数为 0 时被打断。

因为 Weak<T> 引用的值可能已经被丢弃了,为了使用 Weak 所指向的值,我们必须确保其值仍然有效。 为此可以调用 Weak<T> 实例的 upgrade 方法,这会返回 Option<Rc<T>>。 如果 Rc<T> 值还未被丢弃,则结果是 Some;如果 Rc<T> 已被丢弃,则结果是 None。 因为 upgrade 返回一个 Option<Rc<T>>,Rust 会确保处理 Some 和 None 的情况,所以它不会返回非法指针。

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); // leaf parent = None

    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
    // output:
    // leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
    // children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
    // children: RefCell { value: [] } }] } })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

没有无限的输出表明这段代码并没有造成引用循环。这一点也可以从观察 Rc::strong_countRc::weak_count 调用的结果看出。

# 无畏并发

注意:出于简洁的考虑,我们将很多问题归类为 并发,而不是更准确的区分 并发和(或)并行。如 果这是一本专注于并发和∕或并行的书,我们肯定会更加精确的。对于本章,当我们谈到 并发时,请 自行替换为 并发和(或)并行。

# 多线程

将程序中的计算拆分进多个线程可以改善性能,因为程序可以同时进行多个任务,不过这也会增加复杂性。 因为线程是同时运行的,所以无法预先保证不同线程中的代码的执行顺序。 这会导致诸如此类的问题:

  • 竞争状态(Race conditions),多个线程以不一致的顺序访问数据或资源
  • 死锁(Deadlocks),两个线程相互等待对方停止使用其所拥有的资源,这会阻止它们继续运行
  • 只会发生在特定情况且难以稳定重现和修复的 bug

编程语言有一些不同的方法来实现线程。 很多操作系统提供了创建新线程的 API。 这种由编程语言调用操作系统 API 创建线程的模型有时被称为 1:1,一个 OS 线程对应一个语言线程。 Rust 标准库只提供了 1:1 线程实现;有一些 crate 实现了其他有着不同取舍的线程模型。

# 使用 spawn 创建新线程

use std::thread;
use std::time::Duration;

fn main() {
    thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

注意这个函数编写的方式,当主线程结束时,新线程也会结束,而不管其是否执行完毕。

# 使用 join 等待所有线程结束

可以通过将 thread::spawn 的返回值储存在变量中来修复新建线程部分没有执行或者完全没有执行的问题。 thread::spawn 的返回值类型是 JoinHandleJoinHandle 是一个拥有所有权的值,当对其调用 join 方法时,它会等待其线程结束。

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }
    handle.join().unwrap();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

通过调用 handlejoin 会阻塞当前线程直到 handle 所代表的线程结束。 **阻塞(Blocking)**线程意味着阻止该线程执行工作或退出。

# 线程与 move 闭包

move 关键字经常用于传递给 thread::spawn 的闭包,因为闭包会获取从环境中取得的值的所有权,因此会将这些值的所有权从一个线程传送到另一个线程。

use std::thread;

fn main() {
    let v = vec![1, 2, 3];
    let handle = thread::spawn(|| {
        println!("Here's a vector: {:?}", v);
        // Rust 会 推断如何捕获 v,因为 println! 只需要 v 的引用,闭包尝试借用 v。
        // 然而这有一个问题:Rust 不知道这个新建线程会执行多久,所以无法知晓 v 的引用是否一直有效。
    });
    handle.join().unwrap();
}
1
2
3
4
5
6
7
8
9
10
11

修复:

use std::thread;

fn main() {
    let v = vec![1, 2, 3];
    let handle = thread::spawn(move || {
        println!("Here's a vector: {:?}", v);
    });
    handle.join().unwrap();
}
1
2
3
4
5
6
7
8
9

# 使用消息传递在线程间传送数据

一个日益流行的确保安全并发的方式是 消息传递(message passing),这里线程或 actor 通过发送包含数据的消息来相互沟通。 这个思想来源于 Go 编程语言文档中 的口号:” 不要通过共享内存来通讯;而是通过通讯来共享内存。”(”Do not communicate by sharing memory; instead, share memory bycommunicating.”)

Rust 中一个实现消息传递并发的主要工具是 通道(channel),编程中的通道有两部分组成,一个发送者(transmitter)和一个接收者(receiver)。 代码中的一部分调用发送者的方法以及希望发送的数据,另一部分则检查接收端收到的消息。 当发送者或接收者任一被丢弃时可以认为通道被 关闭(closed)了。

use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();
    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
    });

    let received = rx.recv().unwrap();
    println!("Got: {}", received);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

这里使用 mpsc::channel 函数创建一个新的通道;mpsc 是 多个生产者,单个消费者(multiple producer, single consumer)的缩写。 简而言之,Rust 标准库实现通道的方式意味着一个通道可以有多个产生值的发送(sending)端,但只能有一个消费这些值的 接收(receiving)端。

mpsc::channel 函数返回一个元组:第一个元素是发送端,而第二个元素是接收端。 由于历史原因,txrx 通常作为 发送者(transmitter)和 接收者(receiver)的缩写,所以这就是我们将用来绑定这两端变量的名字。 这里使用了一个 let 语句和模式来解构了此元组。

通道的接收端有两个有用的方法:recvtry_recv。 这里,我们使用了 recv,它是 receive 的缩写。 这个方法会阻塞主线程执行直到从通道中接收一个值。 一旦发送了一个值,recv 会在一个 Result<T, E> 中返回它。 当通道发送端关闭,recv 会返回一个错误表明不会再有新的值到来了。

try_recv 不会阻塞,相反它立刻返回一个 Result<T, E>Ok 值包含可用的信息,而 Err 值代表此时没有任何消息。 如果线程在等待消息过程中还有其他工作时使用 try_recv 很有用:可以编写一个循环来频繁调用 try_recv,在有可用消息时进行处理,其余时候则处理一会其他工作直到再次检查。

# 通道与所有权转移

send 函数获取其参数的所有权并移动这个值归接收者所有。 这可以防止在发送后再次意外地使用这个值。

# 发送多个值并观察接收者的等待

use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel();

    let tx1 = tx.clone();

    thread::spawn(move || {
        let vals = vec![
            String::from("hi"),
            String::from("from"),
            String::from("the"),
            String::from("thread"),
        ];
        for val in vals {
            tx1.send(val).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    thread::spawn(move || {
        let vals = vec![
            String::from("more"),
            String::from("messages"),
            String::from("for"),
            String::from("you"),
        ];
        for val in vals {
            tx.send(val).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    for received in rx {
        println!("Got: {}", received);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

在主线程中,不再显式调用 recv 函数:而是将 rx 当作一个迭代器。 对于每一个接收到的值,我们将其打印出来。 当通道被关闭时,迭代器也将结束。

Got: hi
Got: more
Got: from
Got: messages
Got: for
Got: the
Got: thread
Got: you

1
2
3
4
5
6
7
8
9

因为主线程中的 for 循环里并没有任何暂停或等待的代码,所以可以说主线程是在等待从新建线程中接收值。 虽然你可能会看到这些值以不同的顺序出现;这依赖于你的系统。这也就是并发既有趣又困难的原因。

# 共享状态并发

# 互斥器一次只允许一个线程访问数据

互斥器(mutex)是 mutual exclusion 的缩写,也就是说,任意时刻,其只允许一个线程访问某些数据。 为了访问互斥器中的数据,线程首先需要通过获取互斥器的 锁(lock)来表明其希望访问数据。 锁是一个作为互斥器一部分的数据结构,它记录谁有数据的排他访问权。 因此,我们描述互斥器为通过锁系统保护(guarding)其数据。

互斥器以难以使用著称,因为你不得不记住:

  1. 在使用数据之前尝试获取锁。
  2. 处理完被互斥器所保护的数据之后,必须解锁数据,这样其他线程才能够获取锁。

# Mutex 的 API

use std::sync::Mutex;
fn main() {
    let m = Mutex::new(5);
    {
        let mut num = m.lock().unwrap();
        *num = 6;
    }
    println!("m = {:?}", m);  // output: 6
}
1
2
3
4
5
6
7
8
9

像很多类型一样,我们使用关联函数 new 来创建一个 Mutex<T>。 使用 lock 方法获取锁,以访问互斥器中的数据。 这个调用会阻塞当前线程,直到我们拥有锁为止。

如果另一个线程拥有锁,并且那个线程 panic 了,则 lock 调用会失败。 在这种情况下,没人能够再获取锁,所以这里选择 unwrap 并在遇到这种情况时使线程 panic。

一旦获取了锁,就可以将返回值(在这里是 num)视为一个其内部数据的可变引用了。 类型系统确保了我们在使用 m 中的值之前获取锁:Mutex<i32> 并不是一个 i32,所以 必须获取锁才能使用这个 i32 值。

Mutex<T> 是一个智能指针。 更准确的说,lock 调用 返回一个叫做 MutexGuard 的智能指针。 这个智能指针实现了 Deref 来指向其内部数据;其也提供了一个 Drop 实现当 MutexGuard 离开作用域时自动释放锁。 为此,我们不会冒忘记释放锁并阻塞互斥器为其它线程所用的风险,因为锁的释放是自动发生的。

# 在线程间共享 Mutex

直接使用 thread::spawn(move || { //skip }); 创建多个线程无法将统一变量共享,因为所有权无法被移动到多个线程。

同时如果考虑 Rc<T> 也是不可的,因为 Rc<T> 并不能安全的在线程间共享。 当 Rc<T> 管理引用计数时,它必须在每一个 clone 调用时增加计数,并在每一个克隆被丢弃时减少计数。 Rc<T> 并没有使用任何并发原语,来确保改变计数的操作不会被其他线程打断。 在计数出错时可能会导致诡异的 bug,比如可能会造成内存泄漏,或在使用结束之前就丢弃一个值。 我们所需要的是一个完全类似 Rc<T>,又以一种线程安全的方式改变引用计数的类型。

# 原子引用计数 Arc

Arc<T> 正是这么一个类似 Rc<T> 并可以安全的用于并发环境的类型。 字母 ”a” 代表 原子性(atomic),所以这是一个 原子引用计数(atomically reference counted)类型。

你可能会好奇为什么不是所有的原始类型都是原子性的? 为什么不是所有标准库中的类型都默认使用Arc 实现?原因在于线程安全带有性能惩罚,我们希望只在必要时才为此买单。 如果只是在单线程中对值进行操作,原子性提供的保证并无必要,代码可以因此运行的更快。

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("Result: {}", *counter.lock().unwrap());   // Result: 10
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# RefCell∕Rc 与 Mutex∕Arc 的相似性

上面的案例中因为 counter 是不可变的,不过可以获取其内部值的可变引用;这意味着 Mutex<T> 提供了内部可变性,就像 Cell 系列类型那样。 正如使用 RefCell<T> 可以改变 Rc<T> 中的内容那样,同样的可以使用 Mutex<T> 来改变 Arc<T> 中的内容。

另一个值得注意的细节是 Rust 不能避免使用 Mutex<T> 的全部逻辑错误。 Mutex<T> 也有造成 **死锁(deadlock)**的风险。 这发生于当一个操作需要锁住两个资源而两个线程各持一个锁,这会造成它们永远相互等待。

# 使用 Sync 和 Send trait 的可扩展并发

两个并发概念是内嵌于语言中的:std::marker 中的 SyncSend trait。

# 通过 Send 允许在线程间转移所有权

Send 标记 trait 表明实现了 Send 的类型值的所有权可以在线程间传送。 几乎所有的 Rust 类型都是 Send 的,不过有一些例外,例如 Rc<T>:这是不能 Send 的,因为如果克隆了 Rc<T> 的值并尝试将克隆的所有权转移到另一个线程,这两个线程都可能同时更新引用计数(有数据竞争风险)。 为此,Rc<T> 被实现为用于单线程场景,这时不需要为拥有线程安全的引用计数而付出性能代价。

因此,Rust 类型系统和 trait bound 确保永远也不会意外的将不安全的 Rc<T> 在线程间发送。

任何完全由 Send 的类型组成的类型也会自动被标记为 Send。几乎所有基本类型都是 Send 的,除了 裸指针(raw pointer)。

# Sync 允许多线程访问

Sync 标记 trait 表明一个实现了 Sync 的类型可以安全的在多个线程中拥有其值的引用。 换一种方式来说,对于任意类型 T,如果 &TT 的不可变引用)是 Send 的话 T 就是 Sync 的,这意味着其引用就可以安全的发送到另一个线程。 类似于 Send 的情况,基本类型是 Sync 的,完全由 Sync 的类型组成的类型也是 Sync 的。

智能指针 Rc<T> 也不是 Sync 的,出于其不是 Send 相同的原因。 RefCell<T>Cell<T> 系列类型不是 Sync 的。 RefCell<T> 在运行时所进行的借用检查也不是线程安全的。 Mutex<T>Sync 的,可以被用来在多线程中共享访问。

# 手动实现 Send 和 Sync 是不安全的,需要格外注意

通常并不需要手动实现 SendSync trait,因为由 SendSync 的类型组成的类型,自动就是 SendSync 的。 因为他们是标记 trait,甚至都不需要实现任何方法。 他们只是用来加强并发相关的不可变性的。

手动实现这些标记 trait 涉及到编写不安全的 Rust 代码,当前重要的是,在创建新的由不是 Send 和 Sync 的部分构成的并发类型时需要多加小心,以确保维持其安全保证。

# 面向对象

# 面向对象语言的特征

# 对象包含数据和行为

面向对象的程序是由对象组成的。 一个对象包含数据和操作这些数据的过程。 这些过程通常被称为 方法或操作。

# 封装隐藏了实现细节

另一个通常与面向对象编程相关的方面是 封装(encapsulation)的思想:对象的实现细节不能被使用对象的代码获取到。 所以唯一与对象交互的方式是通过对象提供的公有 API;使用对象的代码无法深入到对象内部并直接改变数据或者行为。 封装使得改变和重构对象的内部时无需改变使用对象的代码。

# 继承,作为类型系统与代码共享

继承(Inheritance)是一个很多编程语言都提供的机制,一个对象可以定义为继承另一个对象的定义,这使其可以获得父对象的数据和行为,而无需重新定义。

如果一个语言必须有继承才能被称为面向对象语言的话,那么 Rust 就不是面向对象的。 无法定义一个结构体继承父结构体的成员和方法。

选择继承有两个主要的原因。 第一个是为了重用代码:一旦为一个类型实现了特定行为,继承可以对一个不同的类型重用这个实现。 相反 Rust 代码可以使用默认 trait 方法实现来进行共享。

第二个使用继承的原因与类型系统有关:表现为子类型可以用于父类型被使用的地方。 这也被称为 多态(polymorphism),这意味着如果多种对象共享特定的属性,则可以相互替代使用。

多态(Polymorphism) 多人将多态描述为继承的同义词。不过它是一个有关可以用于多种类型的代码的更广泛的概念。对于继承来说,这些类型通常是子类。Rust 则通过泛型来对不同的可能类型进行抽象,并通过 trait bounds 对这些类型所必须提供的内容施加约束。这有时被称为 bounded parametric polymorphism。

近来继承作为一种语言设计的解决方案在很多语言中失宠了,因为其时常带有共享多于所需的代码的风险。 子类不应总是共享其父类的所有特征,但是继承却始终如此。 如此会使程序设计更为不灵活,并引入无意义的子类方法调用,或由于方法实际并不适用于子类而造成错误的可能性。 某些语言还只允许子类继承一个父类,进一步限制了程序设计的灵活性。

# 为使用不同类型的值而设计的 trait 对象

# 定义通用行为的 trait

任何使用 trait 对象的位置,Rust 的类型系统会在编译时确保任何在此上下文中使用的值会实现其 trait 对象的 trait。 如此便无需在编译时就知晓所有可能的类型。

之前提到过,Rust 刻意不将结构体与枚举称为 “对象”,以便与其他语言中的对象相区别。 在结构体或枚举中,结构体字段中的数据和 impl 块中的行为是分开的,不同于其他语言中将数据和行为组合进一个称为对象的概念中。 trait 对象将数据和行为两者相结合,从这种意义上说 则其更类似其他语言中的对象。 不过 trait 对象不同于传统的对象,因为不能向 trait 对象增加数据。 trait 对象并不像其他语言中的对象那么通用:其(trait 对象)具体的作用是允许对通用行为进行抽象。

定义:

pub trait Draw {
  fn draw(&self);
}

pub struct Screen {
  pub components: Vec<Box<dyn Draw>>,
  // 一个 Screen 结构体的定义,它带有一个字段 components,其包含实现了 Draw trait 的 trait对象的 vector
}

impl Screen {
  pub fn run(&self) {
  // 在 Screen 上实现一个 run 方法,该方法在每个 component 上调用 draw 方法
    for component in self.components.iter() {
      component.draw();
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这与定义使用了带有 trait bound 的泛型类型参数的结构体不同。 泛型类型参数一次只能替代一个具体类型,而 trait 对象则允许在运行时替代多种具体类型。

另一种写法:

一种 Screen 结构体的替代实现,其 run 方法使用泛型和 trait bound

pub trait Draw {
  fn draw(&self);
}

pub struct Screen<T: Draw> {
  pub components: Vec<T>,
}

impl<T> Screen<T>
where
  T: Draw,
{
  pub fn run(&self) {
    for component in self.components.iter() {
      component.draw();
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

这限制了 Screen 实例必须拥有一个全是 Button 类型或者全是 TextField 类型的组件列表。 如果只需要同质(相同类型)集合,则倾向于使用泛型和 trait bound,因为其定义会在编译时采用具体类型进行单态化。

另一方面,通过使用 trait 对象的方法,一个 Screen 实例可以存放一个既能包含 Box<Button>,也能包含 Box<TextField>Vec<T>

# 实现 trait

pub trait Draw {
  fn draw(&self);
}

pub struct Screen {
  pub components: Vec<Box<dyn Draw>>,
}

impl Screen {
  pub fn run(&self) {
    for component in self.components.iter() {
      component.draw();
    }
  }
}

pub struct Button {
  pub width: u32,
  pub height: u32,
  pub label: String,
}

impl Draw for Button {
  fn draw(&self) {
    // code to actually draw a button
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
use gui::Draw;

struct SelectBox {
  width: u32,
  height: u32,
  options: Vec<String>,
}

impl Draw for SelectBox {
  fn draw(&self) {
    // code to actually draw a select box
  }
}

fn main() {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use gui::Draw;

struct SelectBox {
  width: u32,
  height: u32,
  options: Vec<String>,
}

impl Draw for SelectBox {
  fn draw(&self) {
    // code to actually draw a select box
  }
}

use gui::{Button, Screen};

fn main() {
  let screen = Screen {
    components: vec![
      Box::new(SelectBox {
        width: 75,
        height: 10,
        options: vec![
          String::from("Yes"),
          String::from("Maybe"),
          String::from("No"),
        ],
      }),
      Box::new(Button {
        width: 50,
        height: 10,
        label: String::from("OK"),
      }),
    ],
  };

  screen.run();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

当编写库的时候,我们不知道何人会在何时增加 SelectBox 类型,不过 Screen 的实现能够操作并绘制这个新类型,因为 SelectBox 实现了 Draw trait,这意味着它实现了 draw 方法。

这个概念——只关心值所反映的信息而不是其具体类型——类似于动态类型语言中称为 鸭子类型(duck typing)的概念:如果它走起来像一只鸭子,叫起来像一只鸭子,那么它就是一只鸭子! 在示例中 Screen 上的 run 实现中,run 并不需要知道各个组件的具体类型是什么。 它并不检查组件是 Button 或者 SelectBox 的实例。 通过指定 Box 作为 components vector 中值的类型,我们就定义了 Screen 为需要可以在其上调用 draw 方法的值。

使用 trait 对象和 Rust 类型系统来进行类似鸭子类型操作的优势是无需在运行时检查一个值是否实现了特定方法或者担心在调用时因为值没有实现方法而产生错误。 如果值没有实现 trait 对象所需的 trait 则 Rust 不会编译这些代码。

使用 trait 对象和 Rust 类型系统来进行类似鸭子类型操作的优势是无需在运行时检查一个值是否实现了特定方法或者担心在调用时因为值没有实现方法而产生错误。 如果值没有实现 trait 对象所需的 trait 则Rust 不会编译这些代码。

# trait 对象执行动态分发

当对泛型使用 trait bound 时编译器所进行单态化处理:编译器为每一个被泛型类型参数代替的具体类型生成了非泛型的函数和方法实现。 单态化所产生的代码进行 静态分发(static dispatch)。 静态分发发生于编译器在编译时就知晓调用了什么方法的时候。 这与 动态分发(dynamic dispatch)相对,这时编译器在编译时无法知晓调用了什么方法。 在动态分发的情况下,编译器会生成在运行时确定调用了什么方法的代码。 当使用 trait 对象时,Rust 必须使用动态分发。 编译器无法知晓所有可能用于 trait 对象代码的类型,所以它也不知道应该调用哪个类型的哪个方法实现。 为此,Rust 在运行时使用 trait 对象中的指针来知晓需要调用哪个方法。 动态分发也阻止编译器有选择的内联方法代码,这会相应的禁用一些优化。

# trait 对象需要类型安全

只有对象安全(object-safe)的 trait 可以实现为特征对象。 这里有一些复杂的规则来实现 trait 的对象安全,但在实践中,只有两个相关的规则。 如果一个 trait 中定义的所有方法都符合以下规则,则该 trait 是对象安全的:

  • 返回值不是 Self
  • 没有泛型类型的参数

Self 关键字是我们在 trait 与方法上的实现的别称,trait 对象必须是对象安全的,因为一旦使用 trait 对象,Rust 将不再知晓该实现的返回类型。 如果一个 trait 的方法返回了一个 Self 类型,但是该 trait 对象忘记了 Self 的确切类型,那么该方法将不能使用原本的类型。当 trait 使用具体类型填充的泛型类型时也一样:具体类型成为实现 trait 的对象的一部分,当使用 trait 对象却忘了类型是什么时,无法知道应该用什么类型来填充泛型类型。

一个非对象安全的 trait 例子是标准库中的 Clone trait。 Clone trait 中的 clone 方法的声明如下:

pub trait Clone {
  fn clone(&self) -> Self;
}
1
2
3

String 类型实现了 Clone trait,当我们在 String 的实例对象上调用 clone 方法时,我们会得到一个String 类型实例对象。 相似地,如果我们调用 Vec 实例对象上的 clone 方法,我们会得到一个 Vec 类型的实例对象。 clone 方法的标签需要知道哪个类型是 Self 类型,因为 Self 是它的返回类型。

当我们尝试编译一些违反 trait 对象的对象安全规则的代码时,我们会收到编译器的提示。

pub struct Screen {
  pub components: Vec<Box<dyn Clone>>,
}
1
2
3

我们将会收到如下错误:

$ cargo build
  Compiling gui v0.1.0 (file:///projects/gui)

error[E0038]: the trait `Clone` cannot be made into an object
--> src/lib.rs:2:29
  |
2 | pub components: Vec<Box<dyn Clone>>,
  | ^^^^^^^^^ `Clone` cannot be made into an object
  |
  = note: the trait cannot be made into an object because it requires `Self: Sized`
  = note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>

For more information about this error, try `rustc --explain E0038`.
error: could not compile `gui` due to previous error
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 面向对象设计模式的实现

状态模式(state pattern)是一个面向对象设计模式。 该模式的关键在于一个值有某些内部状态,体现为一系列的 状态对象,同时值的行为随着其内部状态而改变。 状态对象共享功能:当然,在 Rust 中使用结构体和 trait 而不是对象和继承。 每一个状态对象负责其自身的行为,以及该状态何时应当转移至另一个状态。 持有一个状态对象的值对于不同状态的行为以及何时状态转移毫不知情。

使用状态模式意味着当程序的业务需求改变时,无需改变值持有状态或者使用值的代码。 我们只需更新某个状态对象中的代码来改变其规则,或者是增加更多的状态对象。

如果要创建一个不使用状态模式的替代实现,则可能会在方法中,或者甚至于在 main 代码中用到 match 语句,来检查状态并在这里改变其行为。 这意味着需要查看很多位置来理解所有逻辑!这在增加更多状态时会变得更糟:每一个 match 语句都会需要另一个分支。 对于状态模式来说,方法和使用位置无需 match 语句,同时增加新状态只涉及到增加一个新 struct 和为其实现 trait 的方法。

状态模式的一个缺点是因为状态实现了状态之间的转换,一些状态会相互联系。 另一个缺点是我们会发现一些重复的逻辑。 为了消除他们,可以尝试为 trait 中返回 self 的方法增加默认实现,不过这会违反对象安全性,因为 trait 不知道 self 具体 是什么。

  • 将状态和行为编码为类型:不同于完全封装状态和状态转移使得外部代码对其毫不知情,我们将状态编码进不同的类型。

  • 实现状态转移为不同类型的转换:状态间的转换不再完全封装在实现中。得益于类型系统和编译时类型检查,我们得到了的是无效状 态是不可能的!这确保了某些特定的 bug 会被发现。

# 模式与模式匹配

模式是 Rust 中特殊的语法,它用来匹配类型中的结构,无论类型是简单还是复杂。 结合使用模式和match 表达式以及其他结构可以提供更多对程序控制流的支配权。 模式由如下一些内容组合而成:

  • 字面值
  • 解构的数组、枚举、结构体或者元组
  • 变量
  • 通配符
  • 占位符

这些部分描述了我们要处理的数据的形状,接着可以用其匹配值来决定程序是否拥有正确的数据来运行特定部分的代码。 我们通过将一些值与模式相比较来使用它。如果模式匹配这些值,我们对值部分进行相应处理。

# 可能会用到模式的位置

# match 分支

一个模式常用的位置是 match 表达式的分支。 在形式上 match 表达式由 match 关键字、用于匹配的值和一个或多个分支构成,这些分支包含一个模式和在值匹配分支的模式时运行的表达式:

match VALUE {
  PATTERN => EXPRESSION,
  PATTERN => EXPRESSION,
  PATTERN => EXPRESSION,
}
1
2
3
4
5

match 表达式必须是 穷尽(exhaustive)的,意为 match 表达式所有可能的值都必须被考虑到。 一个确保覆盖每个可能值的方法是在最后一个分支使用捕获所有的模式:比如,一个匹配任何值的名称永远也不会失败,因此可以覆盖所有匹配剩下的情况。 有一个特定的模式 _ 可以匹配所有情况,不过它从不绑定任何变量。 这在例如希望忽略任何未指定值的情况很有用。

# if let 条件表达式

主要用于编写等同于只关心一个情况的 match 语句简写的。 if let 可以对应一个可选的带有代码的 else 在 if let 中的模式不匹配时运行。

if let Some(color) = favorite_color {
  println!("Using your favorite color, {}, as the background", color);
} else if is_tuesday {
  println!("Tuesday is green day!");
} else {
  println!("Using blue as the background color");
}
1
2
3
4
5
6
7

注意 if let 也可以像 match 分支那样引入覆盖变量:if let Ok(age) = age 引入了一个新的覆盖变量 age,它包含 Ok 成员中的值。 这意味着 if age > 30 条件需要位于这个代码块内部;不能将两个条件组合为 if let Ok(age) = age && age > 30,因为我们希望与 30 进行比较的被覆盖的 age 直到大括号开始的新作用域才是有效的。

if let 表达式的缺点在于其穷尽性没有为编译器所检查,而 match 表达式则检查了。 如果去掉最后的 else 块而遗漏处理一些情况,编译器也不会警告这类可能的逻辑错误。

# while let 条件循环

一个与 if let 结构类似的是 while let 条件循环,它允许只要模式匹配就一直进行 while 循环。

fn main() {
  let mut stack = Vec::new();
  stack.push(1);
  stack.push(2);
  stack.push(3);

  while let Some(top) = stack.pop() {
    println!("{}", top);
  }
}
1
2
3
4
5
6
7
8
9
10

# for 循环

在 for 循环中,模式是 for 关键字直接跟随的值,正如 for x in y 中的 x

fn main() {
  let v = vec!['a', 'b', 'c'];
  
  for (index, value) in v.iter().enumerate() {
    println!("{} is at index {}", value, index);
  }
}
1
2
3
4
5
6
7

这里使用 enumerate 方法适配一个迭代器来产生一个值和其在迭代器中的索引,他们位于一个元组中。

# let 语句

let x = 5;
let PATTERN = EXPRESSION;
let (x, y, z) = (1, 2, 3);
1
2
3

如果希望忽略元组中一个或多个值,也可以使用 _.. 部分所示。

# 函数参数

fn foo(x: i32) {
// code goes here
}

fn main() {}
1
2
3
4
5

x 部分就是一个模式!类似于之前对 let 所做的,可以在函数参数中匹配元组。

# Refutability : 模式是否会匹配失效

模式有两种形式: refutable 和 irrefutable。 能匹配任何传递的可能值的模式被称为是 irrefutable 。 一个例子就是 let x = 5; 语句中的 x,因为 x 可以匹配任何值所以不可能会失败。 对某些可能的值进行匹配会失败的模式被称为是 refutable 。 一个这样的例子便是 if let Some(x) = a_value 表达式中的 Some(x);如果变量 a_value 中的值是 None 而不是 Some,那么 Some(x) 模式不能匹配。

函数参数、let 语句和 for 循环只能接受 irrefutable 的模式,因为通过不匹配的值程序无法进行有意义的工作。 if let 和 while let 表达式被限制为只能接受 refutable 的模式,因为根据定义他们意在处理可能的失败:条件表达式的功能就是根据成功或失败执行不同的操作。

# 所有的模式语法

  1. 匹配字面值
    match x {
      1 => println!("one"),
      2 => println!("two"),
      3 => println!("three"),
      _ => println!("anything"),
    }
    
    1
    2
    3
    4
    5
    6
  2. 匹配命名变量 命名变量是匹配任何值的不可反驳模式,然而当其用于 match 表达式时情况会有些复杂。因为 match 会开始一个新作用域,match 表达式中作为模式的一部分声明的变量会覆盖 match 结构之外的同名变量。
match x {
  let x = Some(5);
  let y = 10;

  Some(50) => println!("Got 50"),
  // 下面的 y 不是上面的 y = 10 的 y,而是一个新的局部的 y
  Some(y) => println!("Matched, y = {:?}", y),
  _ => println!("Default case, x = {:?}", x),
}
1
2
3
4
5
6
7
8
9
  1. 多个模式 在 match 表达式中,可以使用 | 语法匹配多个模式,它代表 **或(or)**的意思。
match x {
  1 | 2 => println!("one or two"),
  3 => println!("three"),
  _ => println!("anything"),
}
1
2
3
4
5
  1. 通过 ..= 匹配值的范围
match x {
  1..=5 => println!("one through five"), // 匹配 1 2 3 4 5
  _ => println!("something else"),
}
1
2
3
4

范围只允许用于数字或 char 值,因为编译器会在编译时检查范围不为空。 5. 解构并分解值

  1. 解构结构体
struct Point {
  x: i32,
  y: i32,
}

fn main() {
  let p = Point { x: 0, y: 7 };
  let Point { x, y } = p;
  assert_eq!(0, x);
  assert_eq!(7, y);

  let p = Point { x: 0, y: 7 };
  match p {
    Point { x, y: 0 } => println!("On the x axis at {}", x),
    Point { x: 0, y } => println!("On the y axis at {}", y),
    Point { x, y } => println!("On neither axis: ({}, {})", x, y),
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  1. 解构枚举
```rust
enum Message {
  Quit,
  Move { x: i32, y: i32 },
  Write(String),
  ChangeColor(i32, i32, i32),
}

fn main() {
  let msg = Message::ChangeColor(0, 160, 255);

  match msg {
    Message::Quit => {
      println!("The Quit variant has no data to destructure.")
    }
    Message::Move { x, y } => {
      println!("Move in the x direction {} and in the y direction {}", x, y);
    }
    Message::Write(text) => println!("Text message: {}", text),
    Message::ChangeColor(r, g, b) => println!(
      "Change the color to red {}, green {}, and blue {}", r, g, b),
  }
}
```
  1. 解构嵌套的结构体和枚举
```rust
enum Color {
  Rgb(i32, i32, i32),
  Hsv(i32, i32, i32),
}

enum Message {
  Quit,
  Move { x: i32, y: i32 },
  Write(String),
  ChangeColor(Color),
}

fn main() {
  let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));

  match msg {
    Message::ChangeColor(Color::Rgb(r, g, b)) => println!(
      "Change the color to red {}, green {}, and blue {}", r, g, b),
    Message::ChangeColor(Color::Hsv(h, s, v)) => println!(
      "Change the color to hue {}, saturation {}, and value {}", h, s, v),
    _ => (),
  }
}
  4. 解构结构体和元组

    ```rust
    fn main() {
      struct Point {
        x: i32,
        y: i32,
      }
    
      let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
    }
    ```
6. 忽略模式中的值
  1. 使用 _ 忽略整个值

    ```rust
    fn foo(_: i32, y: i32) {
      println!("This code only uses the y parameter: {}", y);
    }
    ```
  2. 使用嵌套的 `_` 忽略部分值

    ```rust
    let mut setting_value = Some(5);
    let new_setting_value = Some(10);
    
    match (setting_value, new_setting_value) {
      (Some(_), Some(_)) => {
        println!("Can't overwrite an existing customized value");
      }
      _ => {
        setting_value = new_setting_value;
      }
    }
    
    let numbers = (2, 4, 8, 16, 32);
    
    match numbers {
      (first, _, third, _, fifth) => {
        println!("Some numbers: {}, {}, {}", first, third, fifth)
      }
    }
    ```
  3. 通过在名字前以一个下划线开头来忽略未使用的变量

    ```rust
    fn main() {
      let _x = 5;
      let y = 10;
    }
    ```
  4. 用 `..` 忽略剩余值

    对于有多个部分的值,可以使用 `..` 语法来只使用部分并忽略其它值,同时避免不得不每一个忽略值列出下划线。`..` 模式会忽略模式中剩余的任何没有显式匹配的值部分。
    ```rust
    fn main() {
      struct Point {
        x: i32,
        y: i32,
        z: i32,
      }
    
      let origin = Point { x: 0, y: 0, z: 0 };
    
      match origin {
        Point { x, .. } => println!("x is {}", x),
      }
      match origin {
        (first, .., last) => {
          println!("Some numbers: {}, {}", first, last);
        }
      }
    }
    ```
  5. 匹配守卫提供的额外条件

    匹配守卫(match guard)是一个指定于 match 分支模式之后的额外 if 条件,它也必须被满足才能选择此分支。匹配守卫用于表达比单独的模式所能允许的更为复杂的情况。
    ```rust
    fn main() {
      let num = Some(4);
      
      match num {
        Some(x) if x < 5 => println!("less than five: {}", x),
        Some(x) => println!("{}", x),
        None => (),
      }
    }
    ```
  6. @ 绑定

    at 运算符(@)允许我们在创建一个存放值的变量的同时测试其值是否匹配模式。
    ```rust
    fn main() {
      enum Message {
        Hello { id: i32 },
      }
    
      let msg = Message::Hello { id: 5 };
    
      match msg {
        Message::Hello {
          id: id_variable @ 3..=7,
        } => println!("Found an id in range: {}", id_variable),
        Message::Hello { id: 10..=12 } => {
          println!("Found an id in another range")
        }
        Message::Hello { id } => println!("Found some other id: {}", id),
      }
    }
    // OutPut: Found an id in range: 5
    ```
## 高级特征

### 不安全 Rust

目前为止讨论过的代码都有 Rust 在编译时会强制执行的内存安全保证。
然而,Rust 还隐藏有第二种语言,它不会强制执行这类内存安全保证:这被称为 不安全 Rust(unsafe Rust)。
它与常规 Rust 代码无异,但是会提供额外的能力。

不安全 Rust 之所以存在,是因为静态分析本质上是保守的。
当编译器尝试确定一段代码是否支持某个保证时,拒绝一些合法的程序比接受错误的程序要好一些。
这必然意味着有时代码 可能是合法的,但如果 Rust 编译器没有足够的信息来确定,它将拒绝该代码。

另一个 Rust 存在不安全一面的原因是:底层计算机硬件固有的不安全性。
如果 Rust 不允许进行不安全操作,那么有些任务则根本完成不了。
Rust 需要能够进行像直接与操作系统交互,甚至于编写你自己的操作系统这样的底层系统编程!这也是 Rust 语言的目标之一。

#### 不安全的超能力

可以通过 unsafe 关键字来切换到不安全 Rust,接着可以开启一个新的存放不安全代码的块。
这里有五类可以在不安全 Rust 中进行而不能用于安全 Rust 的操作:

- 解引用裸指针
- 调用不安全的函数或方法
- 访问或修改可变静态变量
- 实现不安全 trait
- 访问 union 的字段

有一点很重要,unsafe 并不会关闭借用检查器或禁用任何其他 Rust 安全检查:如果在不安全代码中使用引用,它仍会被检查。
unsafe 键字只是提供了那五个不会被编译器检查内存安全的功能。
你仍然能在不安全块中获得某种程度的安全。

再者,unsafe 不意味着块中的代码就一定是危险的或者必然导致内存安全问题:其意图在于作为程序员你将会确保 unsafe 块中的代码以有效的方式访问内存。

人是会犯错误的,错误总会发生,不过通过要求这五类操作必须位于标记为 unsafe 的块中,就能够知道任何与内存安全相关的错误必定位于 unsafe 块内。
保持 unsafe 块尽可能小,如此当之后调查内存 bug 时就会感谢你自己了。

为了尽可能隔离不安全代码,将不安全代码封装进一个安全的抽象并提供安全 API 是一个好主意。
标准库的一部分被实现为在被评审过的不安全代码之上的安全抽象。
这个技术防止了 unsafe 泄露到所有你或者用户希望使用由 unsafe 代码实现的功能的地方,因为使用其安全抽象是安全的。

#### 解引用裸指针

和引用一样,裸指针是不可变或可变的,分别写作 `*const T` 和 `*mut T`。
这里的星号不是解引用运算符;它是类型名称的一部分。
在裸指针的上下文中,不可变意味着指针解引用之后不能直接赋值。

裸指针与引用和智能指针的区别在于:

- 允许忽略借用规则,可以同时拥有不可变和可变的指针,或多个指向相同位置的可变指针
- 不保证指向有效的内存
- 允许为空
- 不能实现任何自动清理功能

通过去掉 Rust 强加的保证,你可以放弃安全保证以换取性能或使用另一个语言或硬件接口的能力,此 Rust 的保证并不适用。

```rust
fn main() {
	let mut num = 5;
  // 这里使用 as 将不可变和可变引用强转为对应的裸指针类型。因为直接从保证安全的引用来创建他们,可以知道这些特定的裸指针是有效,但是不能对任何裸指针做出如此假设。
	let r1 = &num as *const i32;
	let r2 = &mut num as *mut i32;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173

注意这里没有引入 unsafe 关键字。 可以在安全代码中创建裸指针,只是不能在不安全块之外解引用裸指针。

fn main() {
  // 创建指向任意内存地址的裸指针
  // 尝试使用任意内存是未定义行为:此地址可能有数据也可能没有,编译器可能会优化掉这个内存访问,或者程序可能会出现段错误(segmentation fault)。
	let address = 0x012345usize;
	let r = address as *const i32;
}
1
2
3
4
5
6

使用:

fn main() {
  let mut num = 5;
  let r1 = &num as *const i32;
  let r2 = &mut num as *mut i32;
  
  unsafe {
  	println!("r1 is: {}", *r1);
  	println!("r2 is: {}", *r2);
  }
}
1
2
3
4
5
6
7
8
9
10

创建一个指针不会造成任何危险;只有当访问其指向的值时才有可能遇到无效的值。

通过裸指针,就能够同时创建同一地址的可变指针和不可变指针,若通过可变指针修改数据,则可能潜在造成数据竞争。

# 调用不安全函数或方法

第二类要求使用不安全块的操作是调用不安全函数。 不安全函数和方法与常规函数方法十分类似,除了其开头有一个额外的 unsafe。 在此上下文中,关键字 unsafe 表示该函数具有调用时需要满足的要求,而 Rust 不会保证满足这些要求。

fn main() {
	unsafe fn dangerous() {}
	
  // 必须在一个单独的 unsafe 块中调用 dangerous 函数
  unsafe {
		dangerous();
	}
}
1
2
3
4
5
6
7
8
# 创建不安全代码的安全抽象

仅仅因为函数包含不安全代码并不意味着整个函数都需要标记为不安全的。 事实上,将不安全代码封装进安全函数是一个常见的抽象。

use std::slice;

fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
	let len = slice.len();
	let ptr = slice.as_mut_ptr();
	assert!(mid <= len);
  unsafe {
    (
    	slice::from_raw_parts_mut(ptr, mid),
    	slice::from_raw_parts_mut(ptr.add(mid), len - mid),
    )
	}
}

fn main() {
  let mut vector = vec![1, 2, 3, 4, 5, 6];
  let (left, right) = split_at_mut(&mut vector, 3);
  
  assert_eq!(a, &mut [1, 2, 3]);
	assert_eq!(b, &mut [4, 5, 6]);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

slice 是一个指向一些数据的指针,并带有该 slice 的长度。 可以使用 len 方法获取 slice 的长度,使用 as_mut_ptr 方法访问 slice 的裸指针。

# 使用 extern 函数调用外部代码

有时你的 Rust 代码可能需要与其他语言编写的代码交互。 为此 Rust 有一个关键字,extern,有助于创建和使用外部函数接口(Foreign Function Interface,FFI)。 外部函数接口是一个编程语言用以定义函数的方式,其允许不同(外部)编程语言调用这些函数。

extern 块中声明的函数在 Rust 代码中总是不安全的。 因为其他语言不会强制执行 Rust 的规则且 Rust 无法检查它们,所以确保其安全是程序员的责任:

extern "C" {
	fn abs(input: i32) -> i32;
}

fn main() {
  unsafe {
  	println!("Absolute value of -3 according to C: {}", abs(-3));
  }
}
1
2
3
4
5
6
7
8
9

”C” 部分定义了外部函数所使用的 应用二进制接口(application binary interface,ABI)—— ABI 定义了如何在汇编语言层面调用此函数。

从其它语言调用 Rust 函数

也可以使用 extern 来创建一个允许其他语言调用 Rust 函数的接口。不同于 extern 块,就在 fn 关键字之前增加 extern 关键字并指定所用到的 ABI。还需增加 #[no_mangle] 注解来告诉 Rust 编译器不要 mangle 此函数的名称。Mangling 发生于当编译器将我们指定的函数名修改为不同的名称时,这会增加用于其他编译过程的额外信息,不过会使其名称更难以阅读。每一个编程语言的编译器都会以稍微不同的方式 mangle 函数名,所以为了使 Rust 函数能在其他语言中指定,必须禁用 Rust 编译器的 name mangling。

#[no_mangle]
pub extern "C" fn call_from_c() {
println!("Just called a Rust function from C!");
}
#[no_mangle]
pub extern "C" fn call_from_c() {
println!("Just called a Rust function from C!");
}
1
2
3
4
5
6
7
8

extern 的使用无需 unsafe。

# 访问或修改可变静态变量

Rust 确实支持他们全局变量(global variables),不过这对于 Rust 的所有权规则来说是有问题的。 如果有两个线程访问相同的可变全局变量,则可能会造成数据竞争。

全局变量在 Rust 中被称为 静态(static)变量。

static HELLO_WORLD: &str = "Hello, world!";

fn main() {
	println!("name is: {}", HELLO_WORLD);
}
1
2
3
4
5

静态(static)变量类似于第三章 ”变量和常量的区别” 部分讨论的常量。 通常静态变量的名称采用 SCREAMING_SNAKE_CASE 写法。 静态变量只能储存拥有 'static 生命周期的引用,这意味着 Rust 编译器可以自己计算出其生命周期而无需显式标注。 访问不可变静态变量是安全的。

常量与不可变静态变量可能看起来很类似,不过一个微妙的区别是静态变量中的值有一个固定的内存地址。 使用这个值总是会访问相同的地址。 另一方面,常量则允许在任何被用到的时候复制其数据。

常量与静态变量的另一个区别在于静态变量可以是可变的。 访问和修改可变静态变量都是不安全的。

拥有可以全局访问的可变数据,难以保证不存在数据竞争,这就是为何 Rust 认为可变静态变量是不安全的。

# 实现不安全 trait

unsafe 的另一个操作用例是实现不安全 trait。 当 trait 中至少有一个方法中包含编译器无法验证的不变式(invariant)时 trait 是不安全的。 可以在 trait 之前增加 unsafe 关键字将 trait 声明为 unsafe,同 时 trait 的实现也必须标记为 unsafe。

unsafe trait Foo {
  // methods go here
}

unsafe impl Foo for i32 {
  // method implementations go here
}

fn main() {}
1
2
3
4
5
6
7
8
9

通过 unsafe impl,我们承诺将保证编译器所不能验证的不变量。

如果实现了一个包含一些不是 Send 或 Sync 的类型,比如裸指针,并希望将此类型标记为 Send 或 Sync,则必须使用 unsafe。 Rust 不能验证我们的类型保证可以安全的跨线程发送或在多线程间访问,所以需要我们自己进行检查并通过 unsafe 表明。

# 访问联合体中的字段

仅适用于 unsafe 的最后一个操作是访问联合体中的字段,union 和 struct 类似,但是在一个实例中同时只能使用一个声明的字段。 联合体主要用于和 C 代码中的联合体交互。 访问联合体的字段是不安全的,因为 Rust 无法保证当前存储在联合体实例中数据的类型。

# 高级 trait

# 关联类型在 trait 定义中指定占位符类型

关联类型(associated types)是一个将类型占位符与 trait 相关联的方式,这样 trait 的方法签名中就可以使用这些占位符类型。 trait 的实现者会针对特定的实现在这个类型的位置指定相应的具体类型。 如此可以定义一个使用多种类型的 trait,直到实现此 trait 时都无需知道这些类型具体是什么。

pub trait Iterator {
type Item;
  fn next(&mut self) -> Option<Self::Item>;
}
1
2
3
4

当 trait 有泛型参数时,可以多次实现这个 trait,每次需改变泛型参数的具体类型。 接着当使 用 Counter 的 next 方法时,必须提供类型注解来表明希望使用 Iterator 的哪一个实现。 通过关联类型,则无需标注类型,因为不能多次实现这个 trait。

# 默认泛型类型参数和运算符重载

当使用泛型类型参数时,可以为泛型指定一个默认的具体类型。 如果默认类型就足够的话,这消除了为具体类型实现 trait 的需要。 为泛型类型指定默认类型的语法是在声明泛型类型时使用。

Rust 并不允许创建自定义运算符或重载任意运算符,不过 std::ops 中所列出的运算符和相应的 trait 可以通过实现运算符相关 trait 来重载。

trait Add<Rhs=Self> {
  type Output;
  fn add(self, rhs: Rhs) -> Self::Output;
}
1
2
3
4

这是一个带有一个方法和一个关联类型的 trait。 比较陌生的部分是尖括号中的 Rhs=Self:这个语法叫做 默认类型参数(default type parameters)。 Rhs 是一个泛型类型参数(”right hand side” 的缩写),它用于定义 add 方法中的 rhs 参数。 如果实现 Add trait 时不指定 Rhs 的具体类型,Rhs 的类型将是默认的 Self 类型,也就是在其上实现 Add 的类型。

默认参数类型主要用于如下两个方面: • 扩展类型而不破坏现有代码。 • 在大部分用户都不需要的特定情况进行自定义。

# 完全限定语法与消歧义:调用相同名称的方法

// 指定我们希望调用哪一个 trait 的 fly 方法

trait Pilot {
  fn fly(&self);
}

trait Wizard {
  fn fly(&self);
}

struct Human;

impl Pilot for Human {
  fn fly(&self) {
    println!("This is your captain speaking.");
  }
}

impl Wizard for Human {
  fn fly(&self) {
    println!("Up!");
  }
}

impl Human {
  fn fly(&self) {
    println!("*waving arms furiously*");
  }
}

fn main() {
  let person = Human;
  Pilot::fly(&person);
  Wizard::fly(&person);
  person.fly();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 完全限定语法
// 使用完全限定语法来指定我们希望调用的是 Dog 上 Animal trait 实现中的 baby_name 函数

trait Animal {
  fn baby_name() -> String;
}
struct Dog;
impl Dog {
  fn baby_name() -> String {
    String::from("Spot")
  }
}

impl Animal for Dog {
  fn baby_name() -> String {
    String::from("puppy")
  }
}

fn main() {
  println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

在尖括号中向 Rust 提供了类型注解,并通过在此函数调用中将 Dog 类型当作 Animal 对待,来指定希望调用的是 Dog 上 Animal trait 实现中的 baby_name 函数。

<Type as Trait>::function(receiver_if_method, next_arg, ...);
1

对于关联函数,其没有一个 receiver,故只会有其他参数的列表。 可以选择在任何函数或方法调用处使用完全限定语法。 然而,允许省略任何 Rust 能够从程序中的其他信息中计算出的部分。 只有当存在多个同名实现而 Rust 需要帮助以便知道我们希望调用哪个实现时,才需要使用这个较为冗长的语法。

# 父 trait 用于在另一个 trait 中使用某 trait 的功能

有时我们可能会需要某个 trait 使用另一个 trait 的功能。在这种情况下,需要能够依赖相关的 trait 也被实现。 这个所需的 trait 是我们实现的 trait 的 父(超)trait(supertrait)。

use std::fmt;
trait OutlinePrint: fmt::Display {
  fn outline_print(&self) {
    let output = self.to_string();
    let len = output.len();
    println!("{}", "*".repeat(len + 4));
    println!("*{}*", " ".repeat(len + 2));
    println!("* {} *", output);
    println!("*{}*", " ".repeat(len + 2));
    println!("{}", "*".repeat(len + 4));
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

因为指定了 OutlinePrint 需要 Display trait,则可以在 outline_print 中使用 to_string,其会为任何实现 Display 的类型自动实现。 如果不在 trait 名后增加 : Display 并尝试在 outline_print 中使用 to_string,则会得到一个错误说在当前作用域中没有找到用于 &Self 类型的方法 to_string

孤儿规则(orphan rule),说明只要 trait 或类型对于当前 crate 是本地的话就可以在此类型上实现该 trait。 一个绕开这个限制的方法是使用 newtype 模式(newtype pattern),它涉及到在一个元组结构体中创建一个新类型。 这个元组结构体带有一个字段作为希望实现 trait 的类型的简单封装。 接着这个封装类型对于 crate 是本地的,这样就可以在这个封装上实现 trait。 Newtype 是一个源自(U.C.0079,逃)Haskell 编程语言的概念。 使用这个模式没有运行时性能惩罚,这个封装类型在编译时就被省略了。

例如,如果想要在 Vec<T> 上实现 Display,而孤儿规则阻止我们直接这么做,因为 Display trait 和 Vec<T> 都定义于我们的 crate 之外。 可以创建一个包含 Vec<T> 实例的 Wrapper 结构体,接着可以在 Wrapper 上实现 Display 并使用 Vec<T> 的值:

use std::fmt;

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    write!(f, "[{}]", self.0.join(", "))
  }
}
1
2
3
4
5
6
7
8
9
Last Updated: 2023-10-29T08:26:04.000Z