时间轴

2025-10-22

init


函数式语言特性:迭代器和闭包

闭包

Rust 的 闭包closures)是可以保存在一个变量中或作为参数传递给其他函数的匿名函数

  • 匿名函数
  • 保存为变量,作为参数
  • 可以在一个地方创建闭包,然后在另一个上下文中调用闭包来完成运算
  • 可从其定义的作用域捕获值

文件名: src/main.rs

一个用来代替假定计算的函数,它大约会执行两秒钟

1
2
3
4
5
6
7
8
use std::thread;
use std::time::Duration;

fn simulated_expensive_calculation(intensity: u32) -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
intensity
}

文件名: src/main.rs

程序的业务逻辑,它根据输入并调用 simulated_expensive_calculation 函数来打印出健身计划

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn generate_workout(intensity: u32, random_number: u32) {
if intensity < 25 {
println!(
"Today, do {} pushups!",
simulated_expensive_calculation(intensity)
);
println!(
"Next, do {} situps!",
simulated_expensive_calculation(intensity)
);
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
simulated_expensive_calculation(intensity)
);
}
}
}

文件名: src/main.rs

main 函数包含了用于 generate_workout 函数的模拟用户输入和模拟随机数输入

1
2
3
4
5
6
fn main() {
let simulated_user_specified_value = 10;
let simulated_random_number = 7;

generate_workout(simulated_user_specified_value, simulated_random_number);
}

闭包的定义

1
2
3
4
5
let expensive_closure = |num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
  • 闭包定义是 expensive_closure 赋值的 = 之后的部分。闭包的定义以一对竖线(|)开始,在竖线中指定闭包的参数;
  • 如果有多于一个参数,可以使用逗号分隔,比如 |param1, param2|
  • 参数之后是存放闭包体的大括号 —— 如果闭包体只有一行则大括号是可以省略的。大括号之后闭包的结尾,需要用于 let 语句的分号。因为闭包体的最后一行没有分号(正如函数体一样),所以闭包体(num)最后一行的返回值作为调用闭包时的返回值 。

注意:

这个 let 语句意味着 expensive_closure 包含一个匿名函数的 定义,不是调用匿名函数的 返回值

重构代码

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn generate_workout(intensity: u32, random_number: u32) {
let expensive_closure = |num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};

if intensity < 25 {
println!("Today, do {} pushups!", expensive_closure(intensity));
println!("Next, do {} situps!", expensive_closure(intensity));
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
expensive_closure(intensity)
);
}
}
}

闭包的类型推断

  • 闭包不要求标注参数和返回值的类型
  • 闭包通常很短小,只在狭小的上下文中工作,编译器通常能推断出类型
  • 可以手动添加类型
1
2
3
4
5
let expensive_closure = |num: u32| -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};

与函数的比较

1
2
3
4
fn  add_one_v1   (x: u32) -> u32 { x + 1 }//函数
let add_one_v2 = |x: u32| -> u32 { x + 1 };//闭包
let add_one_v3 = |x| { x + 1 };//闭包
let add_one_v4 = |x| x + 1 ;//闭包

src/main.rs

1
2
3
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);

这个闭包在执行第二行代码时,编译器就能确定该闭包的类型是 String 了,而在执行第三行时就会报错

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
   Compiling rust_programming v0.1.0 (/home/zhaohang/repository/rust_programming)
error[E0308]: mismatched types
--> src/main.rs:4:29
|
4 | let n = example_closure(5);
| --------------- ^ expected `String`, found integer
| |
| arguments to this function are incorrect
|
note: expected because the closure was earlier called with an argument of type `String`
--> src/main.rs:3:29
|
3 | let s = example_closure(String::from("hello"));
| --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
| |
| in this closure call
note: closure parameter defined here
--> src/main.rs:2:28
|
2 | let example_closure = |x| x;
| ^
help: try using a conversion method
|
4 | let n = example_closure(5.to_string());
| ++++++++++++

For more information about this error, try `rustc --explain E0308`.

泛型参数闭包

在上面的代码仍然把慢计算闭包调用了比所需更多的次数。解决这个问题的一个方法是在全部代码中的每一个需要多个慢计算闭包结果的地方,可以将结果保存进变量以供复用,这样就可以使用变量而不是再次调用闭包。但是这样就会有很多重复的保存结果变量的地方。

幸运的是,还有另一个可用的方案。可以创建一个存放闭包和调用闭包结果的结构体。该结构体只会在需要结果时执行闭包,并会缓存结果值,这样余下的代码就不必再负责保存结果并可以复用该值。你可能见过这种模式被称 *memoization**lazy evaluation**(惰性求值)*。

如何让 struct 持有闭包

  • struct 的定义需要知道所有字段的类型,即需要指明闭包的类型
  • 每个闭包实例都有自己唯一的匿名类型,即使两个闭包签名完全一样
  • 所以需要使用:泛型和 Trait Bound

Fn Trait

  • 由标准库提供
  • 所有的闭包都至少实现了以下 trait 之一:
    • Fn
    • FnMut
    • FnOnce

注意:函数也都实现了这三个 Fn trait。如果不需要捕获环境中的值,则可以使用实现了 Fn trait 的函数而不是闭包。

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
struct Cacher<T>
where
T: Fn(u32) -> u32,
{
calculation: T,
value: Option<u32>,
}
impl<T> Cacher<T>
where
T: Fn(u32) -> u32,
{
fn new(calculation: T) -> Cacher<T> {
Cacher {
calculation,
value: None,
}
}

fn value(&mut self, arg: u32) -> u32 {
match self.value {
Some(v) => v,
None => {
let v = (self.calculation)(arg);
self.value = Some(v);
v
}
}
}
}

结构体 Cacher 有一个泛型 T 的字段 calculation。

T 的 trait bound 指定了 T 是一个使用 Fn 的闭包。

任何我们希望储存到 Cacher 实例的 calculation 字段的闭包必须有一个 u32 参数(由 Fn 之后的括号的内容指定)并必须返回一个 u32(由 -> 之后的内容)。

字段 value 是 Option 类型的。在执行闭包之前,value 将是 None。如果使用 Cacher 的代码请求闭包的结果,这时会执行闭包并将结果储存在 value 字段的 Some 成员中。接着如果代码再次请求闭包的结果,这时不再执行闭包,而是会返回存放在 Some 成员中的结果

重构代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn generate_workout(intensity: u32, random_number: u32) {
let mut expensive_result = Cacher::new(|num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
});

if intensity < 25 {
println!("Today, do {} pushups!", expensive_result.value(intensity));
println!("Next, do {} situps!", expensive_result.value(intensity));
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
expensive_result.value(intensity)
);
}
}
}

Cacher 实现的限制

  1. 第一个问题是 Cacher 实例假设对于 value 方法的任何 arg 参数值总是会返回相同的值。

解决方案

可以使用 HashMap 代替单个值:

  • key: arg 参数

  • value: 执行闭包的结果

  1. 第二个问题是只能接收一个 u32 类型的参数和 u32 类型的返回值

解决方案:

引入两个或多个泛型参数

闭包会捕获其环境

  • 可以捕获其环境并访问其被定义的作用域的变量,而普通函数则不能
1
2
3
4
5
6
7
8
9
fn main() {
let x = 4;

let equal_to_x = |z| z == x;

let y = 4;

assert!(equal_to_x(y));
}
  • 闭包会产生内存开销
    闭包在 Rust 中是一个编译器生成的匿名结构体,结构体的字段就是捕获的变量。
1
2
3
4
5
6
7
8
9
10
11
12
let x = 10;
let c = || println!("{}", x);

// 编译器内部类似:
struct Closure {
x: i32, // 捕获的变量
}
impl Fn() for Closure {
fn call(&self) {
println!("{}", self.x);
}
}

捕获变量需要存储空间

  • 如果捕获的是,闭包会在自身结构体中存储该值。
  • 如果捕获的是引用,闭包结构体中存储的是指针(引用本身也占空间)。

开销大小

  • 小变量(如 i32, bool):几乎没有额外开销,存储在闭包的结构体里即可。
  • 大变量(如 String, Vec, HashMap)
    • 如果 move 捕获,闭包会拷贝或移动整个对象(堆内存可能被绑定到闭包)。
    • 如果只是引用捕获,闭包内部只存指针,但要保证引用生命周期有效。

闭包从所在环境捕获值的方式

与函数获得参数的三种方式一样:

  • 取得所有权: FnOnce

    • FnOnce 消费从周围作用域捕获的变量,闭包周围的作用域被称为其 环境environment。为了消费捕获到的变量,闭包必须获取其所有权并在定义闭包时将其移动进闭包。其名称的 Once 部分代表了闭包不能多次获取相同变量的所有权的事实,所以它只能被调用一次。
  • 可变借用: FnMut

    • FnMut 获取可变的借用值所以可以改变其环境
  • 不可变借用: Fn

    • Fn 从其环境获取不可变的借用值

创建闭包时,通过闭包对环境值的使用,Rust 推断出具体使用哪个 trait:

  • 所有的闭包都实现了 FnOnce
  • 没有移动捕获变量的实现了 FnMut
  • 无需可变访问捕获变量的闭包实现了 Fn

实际上有一个层级关系,所有实现了 Fn 的都实现了 FnMut,所有实现了 FnMut 的,都实现了 FnOnce

move 关键字

在参数列表前使用 move 关键字,可以强制闭包取得它所使用的环境值得所有权

  • 将闭包传递给新线程以移动数据使其归新线程所有时,此技术最为有用

例子

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let x = vec![1, 2, 3];

let equal_to_x = move |z| z == x;

println!("can't use x here: {:?}", x); // error

let y = vec![1, 2, 3];

assert!(equal_to_x(y));
}

x 被移动进了闭包,因为闭包使用 move 关键字定义。接着闭包获取了 x 的所有权,同时 main 就不再允许在 println! 语句中使用 x 了。去掉 println! 即可修复问题。

最佳实践

当指定 Fn trait bound 之一时,首先用 Fn,基于闭包体里得情况,如果需要 FnOnce 或 FnMut,编译器会再告诉你

迭代器

迭代器模式允许你对一个序列的项进行某些处理。迭代器iterator)负责遍历序列中的每一项和决定序列何时结束的逻辑。当使用迭代器时,我们无需重新实现这些逻辑。

在 Rust 中,迭代器是 惰性的(lazy,这意味着在调用方法使用迭代器之前它都不会有效果。

1
2
3
let v1 = vec![1, 2, 3];

let v1_iter = v1.iter();

Iterator trait

  • 所有迭代器都实现了这个 trait
  • 定义于标准库

这个 trait 的定义看起来像这样:

1
2
3
4
5
6
7
pub trait Iterator {
type Item;

fn next(&mut self) -> Option<Self::Item>;

// 此处省略了方法的默认实现
}

type Item 和 Self::Item,他们定义了 trait 的 关联类型associated type)。

这段代码表明实现 Iterator trait 要求同时定义一个 Item 类型,这个 Item 类型被用作 next 方法的返回值类型。换句话说,Item 类型将是迭代器返回元素的类型。

Iterator trait 仅要求实现一个方法:next

next:

  • 每次返回迭代中的一项
  • 返回结果包裹在 Some 里
  • 迭代结束,返回 None

可直接在迭代器上调用 next 方法

1
2
3
4
5
6
7
8
9
10
11
#[test]
fn iterator_demonstration() {
let v1 = vec![1, 2, 3];

let mut v1_iter = v1.iter();

assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
}
  • v1_iter 需要是可变的:在迭代器上调用 next 方法改变了迭代器中用来记录序列位置的状态。换句话说,代码 消费(consume)了,或使用了迭代器。每一个 next 调用都会从迭代器中消费一个项。
  • 使用 for 循环时无需使 v1_iter 可变因为 for 循环会获取 v1_iter 的所有权并在后台使 v1_iter 可变。

几个迭代方法

  • iter 方法:在不可变引用上创建迭代器(元素的不可变引用)
  • into_iter 方法:创建的迭代器会获得所有权
  • iter_mut 方法: 迭代可变的引用

消耗迭代器的方法

  • 在标准库中,Iterator trait 由一些带默认实现的方法
  • 其中有一些方法会调用 next 方法

实现 Iterator trait 时必须实现 next 方法的原因之一

  • 调用 next 的叫做“消耗型适配器

因为调用它们会把迭代器耗尽

一个消费适配器的例子是 sum 方法。这个方法获取迭代器的所有权并反复调用 next 来遍历迭代器,因而会消费迭代器。当其遍历每一个项时,它将每一个项加总到一个总和并在迭代完成时返回总和。

文件名: src/lib.rs

1
2
3
4
5
6
7
8
9
10
#[test]
fn iterator_sum() {
let v1 = vec![1, 2, 3];

let v1_iter = v1.iter();

let total: i32 = v1_iter.sum();

assert_eq!(total, 6);
}

产生其它迭代器的方法

Iterator trait 中定义了另一类方法,被称为 迭代器适配器iterator adaptors),

  • 他们允许将当前迭代器变为不同类型的迭代器。
  • 可以链式调用多个迭代器适配器。
  • 不过因为所有的迭代器都是惰性的,必须调用一个消费适配器方法以便获取迭代器适配器调用的结果。

文件名: src/main.rs

1
2
3
let v1: Vec<i32> = vec![1, 2, 3];

v1.iter().map(|x| x + 1);

该 map 方法使用闭包来调用每个元素以生成新的迭代器。 这里的闭包创建了一个新的迭代器,对其中 vector 中的每个元素都被加 1。

不过这些代码会产生一个警告:

= note: iterators are lazy and do nothing unless consumed

代码实际上并没有做任何事;所指定的闭包从未被调用过。警告提醒了我们为什么:迭代器适配器是惰性的,而这里我们需要消费迭代器。

文件名: src/main.rs

1
2
3
4
5
let v1: Vec<i32> = vec![1, 2, 3];

let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

assert_eq!(v2, vec![2, 3, 4]);

第二行代码的下划线实际上是让编译器去推断他的类型

collect 方法是一个消耗型适配器,把结果收集到一个集合类型中

因为 map 获取一个闭包,可以指定任何希望在遍历的每个元素上执行的操作。这是一个展示如何使用闭包来自定义行为同时又复用 Iterator trait 提供的迭代行为的绝佳例子。

使用闭包捕获环境

filter 方法过滤器

  • 迭代器的 filter 方法获取一个使用迭代器的每一个项并返回布尔值的闭包。
  • 如果闭包返回 true,其值将会包含在 filter 提供的新迭代器中。
  • 如果闭包返回 false,其值不会包含在结果迭代器中。

文件名: src/lib.rs

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
#[derive(PartialEq, Debug)]
struct Shoe {
size: u32,
style: String,
}

fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes.into_iter().filter(|s| s.size == shoe_size).collect()
//闭包从环境中捕获了 `shoe_size` 变量并使用其值与每一只鞋的大小作比较
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn filters_by_size() {
let shoes = vec![
Shoe {
size: 10,
style: String::from("sneaker"),
},
Shoe {
size: 13,
style: String::from("sandal"),
},
Shoe {
size: 10,
style: String::from("boot"),
},
];

let in_my_size = shoes_in_size(shoes, 10);

assert_eq!(
in_my_size,
vec![
Shoe {
size: 10,
style: String::from("sneaker")
},
Shoe {
size: 10,
style: String::from("boot")
},
]
);
}
}

shoes_in_my_size 函数获取一个鞋子 vector 的所有权和一个鞋子大小作为参数。它返回一个只包含指定大小鞋子的 vector。

shoes_in_my_size 函数体中调用了 into_iter 来创建一个获取 vector 所有权的迭代器。接着调用 filter 将这个迭代器适配成一个只含有那些闭包返回 true 的元素的新迭代器。

闭包从环境中捕获了 shoe_size 变量并使用其值与每一只鞋的大小作比较,只保留指定大小的鞋子。最终,调用 collect 将迭代器适配器返回的值收集进一个 vector 并返回。

创建自定义迭代器

trait Iterator定义中唯一要求提供的方法就是 next 方法。一旦定义了它,就可以使用所有其他由 Iterator trait 提供的拥有默认实现的方法来创建自定义迭代器了!

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
struct Counter {
count: u32,
}

impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}

impl Iterator for Counter {
type Item = u32;
//这里将迭代器的关联类型 Item 设置为 u32,意味着迭代器会返回 u32 值集合。

fn next(&mut self) -> Option<Self::Item> {
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
//如果 count 值小于 6,next 会返回封装在 Some 中的当前值,
//不过如果 count 大于或等于 6,迭代器会返回 None。
}
}

#[test]
fn calling_next_directly() {
let mut counter = Counter::new();

assert_eq!(counter.next(), Some(1));
assert_eq!(counter.next(), Some(2));
assert_eq!(counter.next(), Some(3));
assert_eq!(counter.next(), Some(4));
assert_eq!(counter.next(), Some(5));
assert_eq!(counter.next(), None);
}

通过定义 next 方法实现 Iterator trait,我们现在就可以使用任何标准库定义的拥有默认实现的 Iterator trait 方法了,因为他们都使用了 next 方法的功能。

例如,出于某种原因我们希望获取 Counter 实例产生的值,将这些值与另一个 Counter 实例在省略了第一个值之后产生的值配对,将每一对值相乘只保留那些可以被三整除的结果然后将所有保留的结果相加,这可以如示例 13-23 中的测试这样做:

文件名: src/lib.rs

1
2
3
4
5
6
7
8
9
#[test]
fn using_other_iterator_trait_methods() {
let sum: u32 = Counter::new()
.zip(Counter::new().skip(1))
.map(|(a, b)| a * b)
.filter(|x| x % 3 == 0)
.sum();
assert_eq!(18, sum);
}

示例 13-23:使用自定义的 Counter 迭代器的多种方法,注意Counter本身就是Iterator

注意 zip 只产生四对值;理论上第五对值 (5, None) 从未被产生,因为 zip 在任一输入迭代器返回 None 时也返回 None。

所有这些方法调用都是可能的,因为我们指定了 next 方法如何工作,而标准库则提供了其它调用 next 的方法的默认实现。

改进 I/O 项目

使用迭代器并去掉 clone

文件名: src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}

let query = args[1].clone();
let filename = args[2].clone();

let case_sensitive = env::var("CASE_INSENSITIVE").is_err();

Ok(Config {
query,
filename,
case_sensitive,
})
}
}

直接使用 env::args 返回的迭代器

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
10
fn main() {
//let args: Vec<String> = env::args().collect();

let config = Config::new(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {}", err);
process::exit(1);
});

// --snip--
}

src/lib.rs

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
use std::env;
use std::error::Error;
use std::fs;

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
//读取文件
let contents = fs::read_to_string(&config.filename)?;
// println!("With text:\n{}", contents);
let results = if config.case_sensitive {
search(&config.query, &contents)
} else {
search_case_insensitive(&config.query, &contents)
};
for line in results {
println!("{}", line);
}
Ok(())
}
pub struct Config {
pub query: String,
pub filename: String,
pub case_sensitive: bool,
}
impl Config {
pub fn new(mut args: std::env::Args) -> Result<Config, &'static str> {
// if args.len() < 3 {
// return Err("not enough arguments");
// }
args.next();
let query = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a query string"),
};
let filename = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a file name"),
};
// println!("Search for {}", query);
// println!("In file {}", filename);
let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
Ok(Config {
query,
filename,
case_sensitive,
})
}
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
// let mut result = Vec::new();
// for line in contents.lines() {
// if line.contains(query) {
// result.push(line);
// }
// }

// result

contents
.lines()
.filter(|line| line.contains(query))
.collect()
}
pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
// let mut result = Vec::new();
// let query = query.to_lowercase();
// for line in contents.lines() {
// if line.to_lowercase().contains(&query) {
// result.push(line);
// }
// }

// result
contents
.lines()
.filter(|line| line.to_lowercase().contains(query.to_lowercase().as_str()))
.collect()
}

#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe,fast,productive.
Pick three.";
assert_eq!(vec!["safe,fast,productive."], search(query, contents));
}

#[test]
fn case_insensitive() {
let query = "duct";
let contents = "\
Rust:
safe,fast,productive.
Pick three.";
assert_eq!(
vec!["safe,fast,productive."],
search_case_insensitive(query, contents)
);
}
}

src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use std::env;
use std::process;
use minigrep::Config;

fn main() {
// println!("{:?}",args);
let config = Config::new(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments:{}", err);
process::exit(1);
});
if let Err(e) = minigrep::run(config){
eprintln!("Application error: {}",e);
process::exit(1);
};
}

性能比较 循环/迭代器

代器是 Rust 的 零成本抽象zero-cost abstractions)之一,它意味着抽象并不会引入运行时开销,它与Bjarne Stroustrup(C++ 的设计和实现者)在 “Foundations of C++”(2012) 中所定义的 零开销zero-overhead)如出一辙:

In general, C++ implementations obey the zero-overhead principle: What you don’t use, you don’t pay for. And further: What you do use, you couldn’t hand code any better.

  • Bjarne Stroustrup “Foundations of C++”

从整体来说,C++ 的实现遵循了零开销原则:你不需要的,无需为他们买单。更有甚者的是:你需要的时候,也不可能找到其他更好的代码了。

  • 本贾尼·斯特劳斯特卢普 “Foundations of C++”

Cargo 和 crates.io

采用发布配置自定义构建

release profile

  • 是预定义的
  • 可自定义
  • 每个 profile 配置独立于其它 profile

cargo 主要的两个 profile

  • dev profile: 适用于开发 cargo build
  • release profile: 适用于发布 cargo build —release

自定义 profile

在 Cargo.toml 中添加[profile.xxxx]区域,在里面覆盖某人配置的子集

文件名: Cargo.toml

1
2
3
4
5
[profile.dev]
opt-level = 0

[profile.release]
opt-level = 3

opt-level 设置控制 Rust 会对代码进行何种程度的优化。这个配置的值从 0 到 3。越高的优化级别需要更多的时间编译,所以如果你在进行开发并经常编译,可能会希望在牺牲一些代码性能的情况下编译得快一些。这就是为什么 dev 的 opt-level 默认为 0。

文档注释

  • 生成 HTML 文档
  • 显示公共 Api 的文档注释: 如何使用 API
  • 使用///
  • 支持 Markdown
  • 放置在说明条目前

生成文档

运行 rustdoc 工具

1
cargo doc

把生成的文档放到 target/doc 下

生成文档并浏览

1
cargo doc --open

常用章节

#Examples

其它常用章节

1
2
3
Panics: 函数可能发生panic的场景
Errors: 如果函数返回Result,描述可能的错误种类,以及可导致错误的条件
Safety: 如果函数处于unsafe调用,就应该解释函数unsafe的原因,以及调用者确保的使用前提

文档注释作为测试

运行 cargo test: 把文档注释中的示例代码作为测试来运行

文件名: src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}

尝试 cargo test 运行像示例中 add_one 函数的文档;应该在测试结果中看到像这样的部分:

1
2
3
4
5
6
Doc-tests my_crate

running 1 test
test src/lib.rs - add_one (line 5) ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s

现在尝试改变函数或例子来使例子中的 assert_eq! 产生 panic。再次运行 cargo test,我们将会看到文档测试捕获到了例子与代码不再同步!

为包含注释的项添加文档注释

  • 符号: //!
  • 这类注释通常描述 crate 和模块

crate root (按惯例 src/lib.rs)

一个模块内,将 crate 或模块作为一个整体进行记录

例子:

文件名: src/lib.rs

1
2
3
4
5
6
7
//! # My Crate
//!
//! `my_crate` is a collection of utilities to make performing certain
//! calculations more convenient.

/// Adds one to the number given.
// --snip--

使用 pub use 导出方便使用的公共 API

你开发时候使用的文件架构可能并不方便用户。你的结构可能是一个包含多个层级的分层结构,不过这对于用户来说并不方便。这是因为想要使用被定义在很深层级中的类型的人可能很难发现这些类型的存在。他们也可能会厌烦要使用 use my_crate::some_module::another_module::UsefulType; 而不是 use my_crate::UsefulType; 来使用类型。

使用 pub use 重导出(re-export)项来使公有结构不同于私有结构

src/lib.rs

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
//! # Art
//!
//! A library for modeling artistic concepts.

pub mod kinds {
/// The primary colors according to the RYB color model.
pub enum PrimaryColor {
Red,
Yellow,
Blue,
}

/// The secondary colors according to the RYB color model.
pub enum SecondaryColor {
Orange,
Green,
Purple,
}
}

pub mod utils {
use crate::kinds::*;

/// Combines two primary colors in equal amounts to create
/// a secondary color.
pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
// --snip--
SecondaryColor::Green
}
}

文件名: src/main.rs

1
2
3
4
5
6
7
8
use art::kinds::PrimaryColor;
use art::utils::mix;

fn main() {
let red = PrimaryColor::Red;
let yellow = PrimaryColor::Yellow;
mix(red, yellow);
}

为了从公有 API 中去掉 crate 的内部组织,我们可以采用示例 中的 art crate 并增加 pub use 语句来重导出项到顶层结构,如示例 14-5 所示:

文件名: src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//! # Art
//!
//! A library for modeling artistic concepts.

pub use self::kinds::PrimaryColor;
pub use self::kinds::SecondaryColor;
pub use self::utils::mix;

pub mod kinds {
// --snip--
}

pub mod utils {
// --snip--
}

使用

文件名: src/main.rs

1
2
3
4
5
6
use art::mix;
use art::PrimaryColor;

fn main() {
// --snip--
}

发布 Crate

有了唯一的名称、版本号、由 cargo new 新建项目时增加的作者信息、描述和所选择的 license,已经准备好发布的项目的 Cargo.toml 文件可能看起来像这样:

文件名: Cargo.toml

1
2
3
4
5
6
7
8
9
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"
description = "A fun game where you guess what number the computer has chosen."
license = "MIT OR Apache-2.0"
author = "even629"

[dependencies]

Cargo 的文档 描述了其他可以指定的元信息,他们可以帮助你的 crate 更容易被发现和使用!

发布:

1
$ cargo publish

crate 一旦发布,就是永久性的:该版本无法覆盖,代码无法删除

  • 目的:依赖于该版本的项目可继续正常工作

发布已存在 crate 的新版本

修改 version 重新发布

使用 cargo yank 从 Crates.io 撤回版本

  • 不可以删除 crate 之前的版本

撤回某个版本会阻止新项目开始依赖此版本,不过所有现存此依赖的项目仍然能够下载和依赖这个版本。从本质上说,撤回意味着所有带有 Cargo.lock 的项目的依赖不会被破坏,同时任何新生成的 Cargo.lock 将不能使用被撤回的版本。

为了撤回一个 crate,运行 cargo yank 并指定希望撤回的版本:

1
$ cargo yank --vers 1.0.1

也可以撤销撤回操作,并允许项目可以再次开始依赖某个版本,通过在命令上增加 —undo:

1
$ cargo yank --vers 1.0.1 --undo

撤回 并没有 删除任何代码。举例来说,撤回功能并不意在删除不小心上传的秘密信息。如果出现了这种情况,请立即重新设置这些秘密信息。

Cargo 工作空间(Workspaces)

  • cargo 工作空间: 帮助管理多个相互关联且需要协同开发的 crate
  • cargo 工作空间是一套共享同一个 Cargo.lock 和输出文件夹的包

创建工作空间

为了在顶层 add 目录运行二进制 crate,可以通过 -p 参数和包名称来运行 cargo run 指定工作空间中我们希望使用的包:

1
2
3
4
$ cargo run -p adder
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/adder`
Hello, world! 10 plus one is 11!

这会运行 adder/src/main.rs 中的代码,其依赖 add_one crate

从 CRATES.IO 安装二进制 crate

  • 命令: cargo install
  • 来源 https://crates.io
  • 限制: 只能安装具有二进制目标 (binary target) 的 crate

二进制目标 binary target:是一个可运行的程序

  • 拥有 src/main.rs 或其它被指定为二进制文件的 crate 生成

通常: READEME 里面有关于 crate 的描述:

  • 拥有 library target
  • 拥有 library target
  • 两者兼备

cargo install

cargo install 安装的二进制存放在根目录的 bin 文件夹

使用自定义命令扩展 cargo

  • cargo 被设计成可以使用子命令来扩展
  • 例:如果$PATH 中的某个二进制是 cargo-something,你可以像子命令一样运行:
1
$ cargo something
  • 类似这样的自定义命令可以通过该命令列出: cargo —list
  • 优点: 可以使用 cargo install 来安装扩展,像内置工具一样来运行

智能指针

  • 指针pointer)是一个包含内存地址的变量的通用概念。

这个地址引用,或 “指向”(points at)一些其他数据。

  • Rust 中最常见的指针是 引用reference)。

引用以 & 符号为标志并借用了他们所指向的值。除了引用数据没有任何其他特殊功能。它们也没有任何额外开销,所以应用得最多。

  • 智能指针smart pointers)是一类数据结构,他们的表现类似指针,但是也拥有额外的元数据和功能。

引用和智能指针的其它不同

  • 引用:只借用数据
  • 智能指针:很多时候都拥有它所指向的数据

智能指针的例子:

  • String 和 Vec
  • 都拥有一片内存区域,且允许用户对其操作
  • 还拥有元数据(例如容量等)
  • 提供额外的功能或保障(String 保障其数据是合法的 UTF-8 编码)

智能指针的实现

  • 智能指针通常使用 struct 实现,并且实现了:Deref 和 Drop 这两个 trait

    • Deref trait 允许智能指针 struct 的实例像引用一样使用

    • Drop trait 允许你自定义当智能指针实例走出作用域时的代码

使用 Box指向堆上的数据

  • Box是最简单的智能指针:

    • 允许你在 heap 上存储数据(而不是 stack)
    • stack 上是指向 heap 数据的指针
    • 没有性能开销
    • 没有其它额外功能
  • Box实现了 Deref trait 和 Drop trait

多用于如下场景:

  • 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候
  • 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候
  • 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候
1
2
3
4
fn main() {
let b = Box::new(5);
println!("b = {}",b);
}

使用 Box 赋能递归类型

  • 在编译时,Rust 需要知道一个类型所占的空间大小
  • 而递归类型的大小无法在编译时确定
  • 但 Box 类型的大小确定
  • 在递归类型中使用 Box 就可以解决上述问题
  • 函数式语言的 Cons List

Cons List

cons list 是一个来源于 Lisp 编程语言及其方言的数据结构。在 Lisp 中,cons 函数(“construct function” 的缩写)利用两个参数来构造一个新的列表,他们通常是一个单独的值和另一个列表。

cons 函数的概念涉及到更常见的函数式编程术语;“将 xy 连接” 通常意味着构建一个新的容器而将 x 的元素放在新容器的开头,其后则是容器 y 的元素。

cons list 的每一项都包含两个元素:当前项的值和下一项。其最后一项值包含一个叫做 Nil 的值且没有下一项。cons list 通过递归调用 cons 函数产生。代表递归的终止条件(base case)的规范名称是 Nil,它宣布列表的终止。

代表递归的终止条件(base case)的规范名称是 Nil,它宣布列表的终止。注意这不同于 “null” 或 “nil” 的概念,他们代表无效或缺失的值。

Cons List 并不是 Rust 的常用集合

1
2
3
4
5
6
7
8
9
use crate::List::{Cons,Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}

enum List {
Cons(i32,List),
Nil,
}

运行报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Compiling my_box v0.1.0 (C:\Users\cauchy\Desktop\rust\my_box)
error[E0072]: recursive type `List` has infinite size
--> src\main.rs:6:1
|
6 | enum List {
| ^^^^^^^^^
7 | Cons(i32,List),
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
7 | Cons(i32,Box<List>),
| ++++ +

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

计算非递归类型的大小

Message 枚举:

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

当 Rust 需要知道要为 Message 值分配多少空间时,它可以检查每一个成员并发现

  • Message::Quit 并不需要任何空间,
  • Message::Move 需要足够储存两个 i32 值的空间,依此类推。
  • 因为 enum 实际上只会使用其中的一个成员,所以 Message 值所需的空间等于储存其最大成员的空间大小。

与此相对当 Rust 编译器检查像上例的 List 这样的递归类型时会发生什么呢。编译器尝试计算出储存一个 List 枚举需要多少内存,并开始检查 Cons 成员,那么 Cons 需要的空间等于 i32 的大小加上 List 的大小。为了计算 List 需要多少内存,它检查其成员,从 Cons 成员开始。Cons 成员储存了一个 i32 值和一个 List 值,这样的计算将无限进行下去

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

  • 因为 Box 是一个指针,我们总是知道它需要多少空间

指针的大小并不会根据其指向的数据量而改变。

  • Box
  1. 只提供了”间接”存储和 heap 内存分配的功能
  2. 没有其它额外功能
  3. 没有性能开销
  4. 适用于需要间接存储的场景,例如 Cons List
  5. 实现了 Deref trait 和 Drop trait

Dref Trait

  • 实现 Deref Trait 使我们可以自定义解引用运算符*的行为
  • 通过实现 Deref,智能指针可像引用一样来处理

解引用运算符

文件名: src/main.rs

1
2
3
4
5
6
7
fn main() {
let x = 5;
let y = &x;

assert_eq!(5, x);
assert_eq!(5, *y);
}

把 Box当作引用

文件名: src/main.rs

1
2
3
4
5
6
7
fn main() {
let x = 5;
let y = Box::new(x);

assert_eq!(5, x);
assert_eq!(5, *y);
}

定义自己的智能指针

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^

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

MyBox 类型不能解引用,因为我们尚未在该类型实现这个功能。为了启用 * 运算符的解引用功能,需要实现 Deref trait。

通过实现 Deref trait 将某类型像引用一样处理

  • 标准库中的 Deref trait 要求我们实现一个 deref 方法:

    • 该方法借用 self

    • 返回一个指向内部数据的引用

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
use std::ops::Deref;

impl<T> Deref for MyBox<T> {
type Target = T;

fn deref(&self) -> &Self::Target {
&self.0
}
}

当我们在示例代码

1
2
3
4
5
6
7
fn main() {
let x = 5;
let y = MyBox::new(x);

assert_eq!(5, x);
assert_eq!(5, *y);
}

输入 *y 时,Rust 事实上在底层运行了如下代码:

*(y.deref())

函数和方法的隐式解引用转化(Deref Coercion)

  • Deref Coercion 是为函数和方法提供的一种便捷特性
  • 假设 T 实现了 Deref trait: Deref Coercion 可以把 T 的引用转化为 T 经过 Deref 操作后生成的引用
  • 当把某类型的引用传递给函数或方法时,但它的类型于定义的参数类型不匹配:
  1. Deref Coercion 就会自动发生
  2. 编译器会对 deref 进行一系列调用,来把它转为所需的参数类型
  3. 它在编译时完成,没有额外性能开销

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
10
fn hello(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let m = MyBox::new(String::from("Rust"));
//&m &MyBox<String>
//deref &String
//&String &str
hello(&m);
}
  1. 这里使用 &m 调用 hello 函数,其为 MyBox 值的引用
  2. 因为示例 中在 MyBox 上实现了 Deref trait,Rust 可以通过 deref 调用将 &MyBox 变为 &String。
  3. 标准库中提供了 String 上的 Deref 实现,其会返回字符串 slice,这可以在 Deref 的 API 文档中看到。Rust 再次调用 deref 将 &String 变为 &str,这就符合 hello 函数的定义了。

如果 Rust 没有实现 Deref 强制转换,为了使用 &MyBox 类型的值调用 hello,则不得不编写以下的代码

文件名: src/main.rs

1
2
3
4
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}

解引用与可变性

  • 可使用 DerefMut trait 重载可变引用的*运算符
  • 在类型和 trait 在下列三种情况发生时,Rust 会执行 deref coercion:
  1. 当 T: Deref,允许&T 转换为&U
  2. 当 T: DerefMut,允许&mut T 转换为&mut U
  3. 当 T: Deref,允许&mut T 转换为&U
情况 条件 转换
1 T: Deref<Target=U> &T 自动变 &U
2 T: DerefMut<Target=U> &mut T 自动变 &mut U
3 T: Deref<Target=U> &mut T 可变引用降级为不可变引用 &U

举例:

1
2
3
4
5
6
7
8
fn greet(name: &str) {
println!("Hello {name}");
}

let s = String::from("Rust");
greet(&s);
// 正常情况下 &String 不能传给 &str,
// 但因为 String 实现了 Deref<Target=str>,所以自动变成了 &str

Drop Trait

  • 实现 Drop Trait 可以让我们自定义当值将要离开作用域时发生的动作

    1. 例如:文件,网络资源释放等

    2. 任何类型都可以实现 Drop trait

  • Drop Trait 只要求你实现 drop 方法

    1. 参数: 对 self 的可变引用
  • Drop trait 在预导入模块里

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
$ 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`!

使用 std::mem::drop 来提前 drop 值

  • 很难直接禁用自动的 drop 功能,也没必要
    1. Drop trait 的目的就是进行自动的释放处理逻辑
  • Rust 不允许手动调用 Drop trait 的 drop 方法
  • 可以调用标准库的 std::mem::drop 函数(prelude),来提前 drop 值

文件名: src/main.rs

1
2
3
4
5
6
7
8
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
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/drop-example`
CustomSmartPointer created.
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main.

我们也无需担心意外的清理掉仍在使用的值,这会造成编译器错误:所有权系统确保引用总是有效的,也会确保 drop 只会在值不再被使用时被调用一次。

Rc引用计数智能指针

  • 有时一个值会有多个所有者
  • 为了支持多重所有权: Rt

    1. reference counting(引用计数)

    2. 追踪到值得引用

    3. 0 个引用:该值可以被清理掉
  • 需要在 heap 上分配数据,这些数据被程序的多个部分读取(只读),但在编译时无法确定哪个部分最后使用完这些数据

  • 注意 Rc 只能用于单线程场景

我们希望创建两个共享第三个列表所有权的列表,其概念将会看起来如图所示:

文件名: src/main.rs

不能用两个 Box 的列表尝试共享第三个列表的所有权

1
2
3
4
5
6
7
8
9
10
11
12
enum List {
Cons(i32, Box<List>),
Nil,
}

use crate::List::{Cons, Nil};

fn main() {
let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
let b = Cons(3, Box::new(a));
let c = Cons(4, Box::new(a));
}

报错:

1
2
3
4
5
6
7
8
9
error[E0382]: use of moved value: `a`
--> src/main.rs:11:30
|
9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
| - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 | let b = Cons(3, Box::new(a));
| - value moved here
11 | let c = Cons(4, Box::new(a));
| ^ value used here after move

我们修改 List 的定义为使用 Rc 代替 Box,如列表 所示。现在每一个 Cons 变量都包含一个值和一个指向 List 的 Rc。当创建 b 时,不同于获取 a 的所有权,这里会克隆 a 所包含的 Rc,这会将引用计数从 1 增加到 2 并允许 a 和 b 共享 Rc 中数据的所有权。创建 c 时也会克隆 a,这会将引用计数从 2 增加为 3。每次调用 Rc::clone,Rc 中数据的引用计数都会增加,直到有零个引用之前其数据都不会被清理。

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
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
14
15
16
17
        a (Rc)


Cons(5)


Cons(10)


Nil

b ---> Cons(3) ───┐


a (共享)

c ---> Cons(4) ───┘

也可以调用 a.clone() 而不是 Rc::clone(&a),不过在这里 Rust 的习惯是使用 Rc::clone。

  • Rc::clone 的实现并不像大部分类型的 clone 实现那样对所有数据进行深拷贝。
  • Rc::clone 只会增加引用计数,这并不会花费多少时间。深拷贝可能会花费很长时间

克隆 Rc会增加引用计数

文件名: src/main.rs

Rc::strong_count 获得引用计数

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!("count after creating a = {}", Rc::strong_count(&a));
let b = Cons(3, Rc::clone(&a));
println!("count after creating b = {}", Rc::strong_count(&a));
{
let c = Cons(4, Rc::clone(&a));
println!("count after creating c = {}", Rc::strong_count(&a));
}
println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

这段代码会打印出:

1
2
3
4
5
6
7
8
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished dev [unoptimized + debuginfo] target(s) in 0.45s
Running `target/debug/cons-list`
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

我们能够看到 a 中 Rc 的初始引用计数为 1,接着每次调用 clone,计数会增加 1。当 c 离开作用域时,计数减 1。不必像调用 Rc::clone 增加引用计数那样调用一个函数来减少计数;Drop trait 的实现当 Rc 值离开作用域时自动减少引用计数。

从这个例子我们所不能看到的是,在 main 的结尾当 b 然后是 a 离开作用域时,此处计数会是 0,同时 Rc 被完全清理。使用 Rc 允许一个值有多个所有者,引用计数则确保只要任何所有者依然存在其值也保持有效。

  • Rc通过不可变引用, Rc 允许在程序的多个部分之间只读地共享数据。
  • 如果 Rc 也允许多个可变引用,则会违反第四章讨论的借用规则之一:相同位置的多个可变借用可能造成数据竞争和不一致。

RefCell和内部可变性

内部可变性(interior mutability)

  • interior mutability 是 Rust 的设计模式之一
  • 它允许你在支持有不可变引用的前提下对数据进行修改

数据结构中使用了 unsafe 代码来绕过 Rust 正常的可变性和借用规则

  • 与 Rc不同,RefCell类型代表了其持有数据的唯一所有权

回忆借用规则:

  1. 在任何给定的时间里,你要么只能拥有一个可变引用,要么只能拥有任意数量的不可变引用
  2. 引用总是有效的

RefCell与 Box的区别

Box RefCell
编译阶段强制代码遵守借用规则 只会在运行时检查借用规则
否则出现错误 否则触发 panic

借用规则在不同阶段进行检查的比较

编译阶段 运行时
尽早暴露问题 问题暴露延后,甚至到生产环境
没有任何运行时开销 因借用计数产生些许性能损失
对大多数场景是最佳选择 实现某些特定的内存安全场景(不可变环境中修改自身数据)
是 Rust 的默认行为
  • 与 Rc类似,只能用于单线程场景

选择 Box,Rc,RefCell的依据

Box Rc RefCell
同一数据的所有者 一个 多个 一个
可变性,借用检查 可变,不可变借用(编译时检查) 不可变借用(编译时检查) 可变,不可变借用(运行时检查)

内部可变性:可变的借用一个不可变的值

借用规则的一个推论是当有一个不可变值时,不能可变地借用它。例如,如下代码不能编译:

1
2
3
4
fn main() {
let x = 5;
let y = &mut x;
}

如果尝试编译,会得到如下错误:

1
2
3
4
$ cargo run
Compiling borrowing v0.1.0 (file:///projects/borrowing)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
--> src/main.rs:3:13

如下是一个我们想要测试的场景:

我们在编写一个记录某个值与最大值的差距的库,并根据当前值与最大值的差距来发送消息。例如,这个库可以用于记录用户所允许的 API 调用数量限额。

该库只提供记录与最大值的差距,以及何种情况发送什么消息的功能。使用此库的程序则期望提供实际发送消息的机制:程序可以选择记录一条消息、发送 email、发送短信等等。库本身无需知道这些细节;只需实现其提供的 Messenger trait 即可。示例 15-20 展示了库代码:

文件名: src/lib.rs

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
pub trait Messenger {
fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &T, max: usize) -> LimitTracker<T> {
LimitTracker {
messenger,
value: 0,
max,
}
}

pub fn set_value(&mut self, value: usize) {
self.value = value;

let percentage_of_max = self.value as f64 / self.max as f64;

if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;

struct MockMessenger {
sent_messages: Vec<String>,
}

impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}

impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.push(String::from(message));
}
}

#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

limit_tracker.set_value(80);

assert_eq!(mock_messenger.sent_messages.len(), 1);
}
}

这些代码中一个重要部分是拥有一个方法 send 的 Messenger trait,其获取一个 self 的不可变引用和文本信息。这个 trait 是 mock 对象所需要实现的接口库,这样 mock 就能像一个真正的对象那样使用了。另一个重要的部分是我们需要测试 LimitTracker 的 set_value 方法的行为。可以改变传递的 value 参数的值,不过 set_value 并没有返回任何可供断言的值。也就是说,如果使用某个实现了 Messenger trait 的值和特定的 max 创建 LimitTracker,当传递不同 value 值时,消息发送者应被告知发送合适的消息。

我们所需的 mock 对象是,调用 send 并不实际发送 email 或消息,而是只记录信息被通知要发送了。可以新建一个 mock 对象实例,用其创建 LimitTracker,调用 LimitTracker 的 set_value 方法,然后检查 mock 对象是否有我们期望的消息。示例 15-21 展示了一个如此尝试的 mock 对象实现,不过借用检查器并不允许:

然而,这个测试是有问题的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
--> src/lib.rs:58:13
|
2 | fn send(&self, msg: &str);
| ----- help: consider changing that to be a mutable reference: `&mut self`
...
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable

For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` due to previous error
warning: build failed, waiting for other jobs to finish...
error: build failed

不能修改 MockMessenger 来记录消息,因为 send 方法获取了 self 的不可变引用。我们也不能参考错误文本的建议使用 &mut self 替代,因为这样 send 的签名就不符合 Messenger trait 定义中的签名了(可以试着这么改,看看会出现什么错误信息)。

这正是内部可变性的用武之地!我们将通过 RefCell 来储存 sent_messages,然后 send 将能够修改 sent_messages 并储存消息。

文件名: src/lib.rs

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
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;

struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}

impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}

impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}

#[test]
fn it_sends_an_over_75_percent_warning_message() {
// --snip--

assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}

使用 RefCell在运行时记录借用信息

  • 两个方法(安全接口)

    1. borrow 方法:返回智能指针 Ref,它实现了 Deref

    2. borrow_mut 方法:返回 RefMut,它实现了 Deref

  • RefCell会记录当前存在多少个活跃的 Ref和 RefMut智能指针

    1. 每次调用 borrow:不可变借用计数+1

    2. 任何一个 Ref的值离开作用域被释放时:不可变借用计数-1

    3. 每次调用 borrow_mut: 可变借用计数+1

    4. 任何一个 RefMut的值利开作用域被释放时:可变借用计数-1

  • Rust 以此计数来维护借用检查规则:任何一个给定时间里,只允许拥有多个不可变借用或一个可变借用

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

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}

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

fn main() {
let value = Rc::new(RefCell::new(5));

let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));

*value.borrow_mut() += 10;

println!("a after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}

当我们打印出 a、b 和 c 时,可以看到他们都拥有修改后的值 15 而不是 5:

1
2
3
4
5
6
7
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished dev [unoptimized + debuginfo] target(s) in 0.63s
Running `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))

其它可实现内部可变性的类型

  • Cell:通过复制来访问数据
  • Mutex:用于实现跨线程的情形下的内部可变性模式

循环引用导致内存泄漏

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

文件名: src/main.rs

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
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

// List 是一个链表,每个节点是 Cons(value, next) 或 Nil
#[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() {
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());
}

如果保持最后的 println! 行注释并运行代码,会得到如下输出:

1
2
3
4
5
6
7
8
9
10
11
$ 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

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

在 Rust 中,#[derive(Debug)] 对枚举(比如链表)会生成一个递归打印逻辑:注意:next 也会调用 Debug → 又去打印它的 next → 递归调用,而且这个递归调用没有终止条件,因此最后会导致栈溢出

1
2
3
4
5
6
7
8
9
10
11
impl Debug for List {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Cons(v, next) => {
write!(f, "Cons({:?}, {:?})", v, next)
}
Nil => write!(f, "Nil")
}
}
}

防止内存泄漏的解决办法

  • 依靠开发者来保证,不能依靠 Rust
  • 重新组织数据结构:一些引用来表达所有权,一些引用不表达所有权

    1. 循环引用中的一部分具有所有权关系,另一部分不涉及所有权关系

    2. 而只有所有权关系才影响值的清理

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

  • Rc::clone 为 Rc实例的 strong_count 加 1,Rc的实例只有在 strong_count 为 0 时才会被清理
  • Rc实例通过调用 Rc::downgrade 方法可以创建值的 Weak Reference(弱引用)

    1. 返回类型是 Weak(智能指针)

    2. 调用 Rc::downgrade 会为 weak_count 加 1

  • Rc使用 weak_count 来追踪存在多少 Weak

  • weak_count 不为 0 并不影响 Rc实例的清理

Strong VS Weak

  • Strong Reference 是关于如何分享 Rc实例的所有权
  • Weak Reference 并不表达上述意思
  • 使用 Weak Reference 并不会创建循环引用:

当 Strong Reference 数量为 0 的时候,Weak Reference 会自动断开

  • 在使用 Weak前,需保证它指向的值仍然存在:

在 Weak实例上调用 upgrade 方法,返回 Option>

创建树形数据结构:带有子节点的 Node

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
children: RefCell::new(vec![]),
});

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

这里克隆了 leaf 中的 Rc 并储存在了 branch 中,这意味着 leaf 中的 Node 现在有两个所有者:leaf 和 branch。可以通过 branch.children 从 branch 中获得 leaf,不过无法从 leaf 到 branch。leaf 没有到 branch 的引用且并不知道他们相互关联。我们希望 leaf 知道 branch 是其父节点。稍后我们会这么做

增加从子到父的引用

为了使子节点知道其父节点,需要在 Node 结构体定义中增加一个 parent 字段。问题是 parent 的类型应该是什么。我们知道其不能包含 Rc,因为这样 leaf.parent 将会指向 branch 而 branch.children 会包含 leaf 的指针,这会形成引用循环,会造成其 strong_count 永远也不会为 0。

现在换一种方式思考这个关系:

  • 父节点应该拥有其子节点
  • 如果父节点被丢弃了,其子节点也应该被丢弃
  • 然而子节点不应该拥有其父节点
  • 如果丢弃子节点,其父节点应该依然存在

这正是弱引用的例子!

所以 parent 使用 Weak 类型而不是 Rc,具体来说是 RefCell>。现在 Node 结构体定义看起来像这样:

文件名: src/main.rs

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
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());

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());
}

创建 leaf 节点类似于示例 15-27 中如何创建 leaf 节点的,除了 parent 字段有所不同:leaf 开始时没有父节点,所以我们新建了一个空的 Weak 引用实例。

此时,当尝试使用 upgrade 方法获取 leaf 的父节点引用时,会得到一个 None 值。如第一个 println! 输出所示:

1
leaf parent = None

当创建 branch 节点时,其也会新建一个 Weak 引用,因为 branch 并没有父节点。leaf 仍然作为 branch 的一个子节点。一旦在 branch 中有了 Node 实例,就可以修改 leaf 使其拥有指向父节点的 Weak 引用。这里使用了 leaf 中 parent 字段里的 RefCell> 的 borrow_mut 方法,接着使用了 Rc::downgrade 函数来从 branch 中的 Rc 值创建了一个指向 branch 的 Weak 引用。

当再次打印出 leaf 的父节点时,这一次将会得到存放了 branch 的 Some 值:现在 leaf 可以访问其父节点了!当打印出 leaf 时,我们也避免了如示例 15-26 中最终会导致栈溢出的循环:Weak 引用被打印为 (Weak):

1
2
3
leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
children: RefCell { value: [] } }] } })

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

可视化 strong_count 和 weak_count 的改变让我们通过创建了一个新的内部作用域并将 branch 的创建放入其中,来观察 Rc 实例的 strong_count 和 weak_count 值的变化。这会展示当 branch 创建和离开作用域被丢弃时会发生什么。这些修改如示例所示:

示例 :在内部作用域创建 branch 并检查其强弱引用计数

文件名: src/main.rs

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
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
// leaf strong = 1, weak = 0
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);

{
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);
// branch strong = 1, weak = 1
println!(
"branch strong = {}, weak = {}",
Rc::strong_count(&branch),
Rc::weak_count(&branch),
);
// leaf strong = 2, weak = 0
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}
// leaf parent = None
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

// leaf strong = 1, weak = 0
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}

一旦创建了 leaf,其 Rc 的强引用计数为 1,弱引用计数为 0。在内部作用域中创建了 branch 并与 leaf 相关联,此时 branch 中 Rc 的强引用计数为 1,弱引用计数为 1(因为 leaf.parent 通过 Weak 指向 branch)。这里 leaf 的强引用计数为 2,因为现在 branch 的 branch.children 中储存了 leaf 的 Rc 的拷贝,不过弱引用计数仍然为 0。

当内部作用域结束时,branch 离开作用域,Rc 的强引用计数减少为 0,所以其 Node 被丢弃。来自 leaf.parent 的弱引用计数 1 与 Node 是否被丢弃无关,所以并没有产生任何内存泄漏!

如果在内部作用域结束后尝试访问 leaf 的父节点,会再次得到 None。在程序的结尾,leaf 中 Rc 的强引用计数为 1,弱引用计数为 0,因为现在 leaf 又是 Rc 唯一的引用了。

所有这些管理计数和值的逻辑都内建于 Rc 和 Weak 以及它们的 Drop trait 实现中。通过在 Node 定义中指定从子节点到父节点的关系为一个 Weak引用,就能够拥有父节点和子节点之间的双向引用而不会造成引用循环和内存泄漏。

无畏并发

  • 并发编程Concurrent programming),代表程序的不同部分相互独立的执行,
  • 并行编程parallel programming)代表程序不同部分于同时执行

使用线程同时运行代码

在大部分现代操作系统中,已执行程序的代码在一个 进程process)中运行,操作系统则负责管理多个进程。在程序内部,也可以拥有多个同时运行的独立部分。运行这些独立部分的功能被称为 线程threads)。

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

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

编程语言有一些不同的方法来实现线程。

  • 很多操作系统提供了创建新线程的 API。这种由编程语言调用操作系统 API 创建线程的模型有时被称为 1:1,一个 OS 线程对应一个语言线程。Rust 标准库只提供了 1:1 线程实现;需要较小的运行时(即Rust 不需要额外的线程调度器或复杂机制,只需要跟踪线程句柄(handle),创建和销毁线程时调用 OS API,没有额外的用户态调度)。
  • 有一些 crate 实现了其他有着不同取舍的线程模型,即语言自己实现的线程(绿色线程):M:N 模型。需要较大的运行时

通过 spawn 创建新线程

为了创建一个新线程,需要调用 thread::spawn 函数并传递一个闭包,并在其中包含希望在新线程运行的代码

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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));
}
}

当主线程结束时,新线程也会结束,而不管其是否执行完毕。

通过 join Handle 来等待所有线程的完成

  • thread::spawn 的返回值类型是 JoinHandle。
  • JoinHandle 是一个拥有所有权的值
  • 当对其调用 join 方法时,会阻止当前运行线程的执行,直到 handle 所表示的这些线程的终结。

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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();
}

通过调用 handle 的 join 会阻塞当前线程直到 handle 所代表的线程结束。阻塞Blocking) 线程意味着阻止该线程执行工作或退出。因为我们将 join 调用放在了主线程的 for 循环之后,

使用 move 闭包

  • move 闭包通常和 thread::spawn 函数一起使用,它允许你使用其他线程的数据
  • 创建线程时,把值得所有权从一个线程转移到另一个线程

示例: 尝试在另一个线程使用主线程创建的 vector

文件名: src/main.rs

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(|| {
println!("Here's a vector: {:?}", v);
});

handle.join().unwrap();
}

闭包使用了 v,所以闭包会捕获 v 并使其成为闭包环境的一部分。因为 thread::spawn 在一个新线程中运行这个闭包,所以可以在新线程中访问 v。然而当编译这个例子时,会得到如下错误:

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
$ cargo run
Compiling threads v0.1.0 (file:///projects/threads)
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
--> src/main.rs:6:32
|
6 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `v`
7 | println!("Here's a vector: {:?}", v);
| - `v` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src/main.rs:6:18
|
6 | let handle = thread::spawn(|| {
| __________________^
7 | | println!("Here's a vector: {:?}", v);
8 | | });
| |______^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
|
6 | let handle = thread::spawn(move || {
| ++++

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

Rust 会 推断 如何捕获 v,因为 println! 只需要 v 的引用,闭包尝试借用 v。然而这有一个问题:Rust 不知道这个新建线程会执行多久,所以无法知晓 v 的引用是否一直有效

示例 16-4 展示了一个 v 的引用很有可能不再有效的场景:

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
use std::thread;

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

let handle = thread::spawn(|| {
println!("Here's a vector: {:?}", v);
});

drop(v); // oh no!

handle.join().unwrap();
}

通过在闭包之前增加 move 关键字,我们强制闭包获取其使用的值的所有权,而不是任由 Rust 推断它应该借用值。下面展示对代码的修改,可以按照我们的预期编译并运行:

示例: 使用 move 关键字强制获取它使用的值的所有权

文件名: src/main.rs

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();
}

使用消息传递来跨线程传递数据

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

  • 线程(或 Actor)通过彼此发送消息(数据)来进行通信
  • Rust: Channel(标准库提供)

Channel

  • Channel 包含:发送端接收端
  • 调用发送端的方法,发送数据
  • 接收端会检查和接收到达的数据
  • 如果发送端,接收端中任意一端被丢弃了,那么 Channel 就被“关闭”了

创建 Channel

  • 使用mpsc::channel 函数来创建 Channel
  1. mpsc 表示 multiple producer,single consumer(多个生产者,一个消费者)
  2. 返回一个 tuple(元组):里面元素分别是发送端,接收端
1
pub fn channel<T>() -> (Sender<T>, Receiver<T>)

让我们将发送端移动到一个新建线程中并发送一个字符串,这样新建线程就可以和主线程通讯了

示例: 将 tx 移动到一个新建的线程中并发送 “hi”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
}

这里再次使用 thread::spawn 来创建一个新线程并使用 move 将 tx 移动到闭包中这样新建线程就拥有 tx 了。新建线程需要拥有信道的发送端以便能向信道发送消息。

信道的发送端有一个 send 方法用来获取需要放入信道的值。send 方法返回一个 Result 类型,所以如果接收端已经被丢弃了,将没有发送值的目标,所以发送操作会返回错误。在这个例子中,出错的时候调用 unwrap 产生 panic。不过对于一个真实程序,需要合理地处理它

接收端的 recv 方法

  • 信道的接收端有两个有用的方法:recv 和 try_recv。
  • 这里,我们使用了 recv,它是 receive 的缩写。这个方法会阻塞主线程执行直到从信道中接收一个值。一旦发送了一个值,recv 会在一个 Result 中返回它。当信道发送端关闭,recv 会返回一个错误表明不会再有新的值到来了。
  • try_recv 不会阻塞,相反它立刻返回一个 Result:Ok 值包含可用的信息,而 Err 值代表此时没有任何消息。如果线程在等待消息过程中还有其他工作时使用 try_recv 很有用:可以编写一个循环来频繁调用 try_recv,在有可用消息时进行处理,其余时候则处理一会其他工作直到再次检查。

信道与所有权转移

现在让我们做一个试验来看看信道与所有权如何一同协作以避免产生问题:我们将尝试在新建线程中的信道中发送完 val 值 之后 再使用它。尝试编译下面的示例的代码并看看为何这是不允许的:

示例: 在我们已经发送到信道中后,尝试使用 val 引用

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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();
println!("val is {}", val);
});

let received = rx.recv().unwrap();
println!("Got: {}", received);
}

这里尝试在通过 tx.send 发送 val 到信道中之后将其打印出来。允许这么做是一个坏主意:

一旦将值发送到另一个线程后,那个线程可能会在我们再次使用它之前就将其修改或者丢弃。其他线程对值可能的修改会由于不一致或不存在的数据而导致错误或意外的结果

然而,尝试编译示例 16-9 的代码时,Rust 会给出一个错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ cargo run
Compiling message-passing v0.1.0 (file:///projects/message-passing)
error[E0382]: borrow of moved value: `val`
--> src/main.rs:10:31
|
8 | let val = String::from("hi");
| --- move occurs because `val` has type `String`, which does not implement the `Copy` trait
9 | tx.send(val).unwrap();
| --- value moved here
10 | println!("val is {}", val);
| ^^^ value borrowed here after move

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

我们的并发错误会造成一个编译时错误。send 函数获取其参数的所有权并移动这个值归接收者所有。这可以防止在发送后再次意外地使用这个值;所有权系统检查一切是否合乎规则。

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

示例: 发送多个消息,并在每次发送后暂停一段时间

文件名: src/main.rs

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
use std::sync::mpsc;
use std::thread;
use std::time::Duration;

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

thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];

for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});

for received in rx {
println!("Got: {}", received);
}
}

这一次,在新建线程中有一个字符串 vector 希望发送到主线程。我们遍历他们,单独的发送每一个字符串并通过一个 Duration 值调用 thread::sleep 函数来暂停一秒。

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

当运行示例 16-10 中的代码时,将看到如下输出,每一行都会暂停一秒:

1
2
3
4
Got: hi
Got: from
Got: the
Got: thread

因为主线程中的 for 循环里并没有任何暂停或等待的代码,所以可以说主线程是在等待从新建线程中接收值。

通过克隆发送者来创建多个生产者

示例: 从多个生产者发送多个消息

文件名: src/main.rs

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
// --snip--

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);
}

// --snip--

这一次,在创建新线程之前,我们对信道的发送端调用了 clone 方法。这会给我们一个可以传递给第一个新建线程的发送端句柄。我们会将原始的信道发送端传递给第二个新建线程。这样就会有两个线程,每个线程将向信道的接收端发送不同的消息。

如果运行这些代码,你 可能 会看到这样的输出:

1
2
3
4
5
6
7
8
Got: hi
Got: more
Got: from
Got: messages
Got: for
Got: the
Got: thread
Got: you

虽然你可能会看到这些值以不同的顺序出现;这依赖于你的系统。这也就是并发既有趣又困难的原因。如果通过 thread::sleep 做实验,在不同的线程中提供不同的值,就会发现他们的运行更加不确定,且每次都会产生不同的输出。

共享状态并发

  • 在某种程度上,任何编程语言中的信道都类似于单所有权,因为一旦将一个值传送到信道中,将无法再使用这个值。
  • 共享内存类似于多所有权:多个线程可以同时访问相同的内存位置

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

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

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

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

在 Rust 中,得益于类型系统和所有权,我们不会在锁和解锁上出错。

Mutex 的 API

作为展示如何使用互斥器的例子,让我们从在单线程上下文使用互斥器开始,如示例所示:

示例: 出于简单的考虑,在一个单线程上下文中探索 Mutex 的 API

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
use std::sync::Mutex;

fn main() {
let m = Mutex::new(5);

{
let mut num = m.lock().unwrap();
*num = 6;
}

println!("m = {:?}", m);
}

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

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

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

在线程间共享 Mutex

现在让我们尝试使用 Mutex 在多个线程间共享值。我们将启动十个线程,并在各个线程中对同一个计数器值加一,这样计数器将从 0 变为 10。示例中的例子会出现编译错误,而我们将通过这些错误来学习如何使用 Mutex,以及 Rust 又是如何帮助我们正确使用的。

示例: 程序启动了 10 个线程,每个线程都通过 Mutex 来增加计数器的值

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::sync::Mutex;
use std::thread;

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

for _ in 0..10 {
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());
}

这里创建了一个 counter 变量来存放内含 i32 的 Mutex,类似示例 16-12 那样。接下来遍历 range 创建了 10 个线程。使用了 thread::spawn 并对所有线程使用了相同的闭包:他们每一个都将调用 lock 方法来获取 Mutex 上的锁,接着将互斥器中的值加一。当一个线程结束执行,num 会离开闭包作用域并释放锁,这样另一个线程就可以获取它了。

在主线程中,我们收集了所有的 join 句柄,调用它们的 join 方法来确保所有线程都会结束。这时,主线程会获取锁并打印出程序的结果。

编译失败:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0382]: use of moved value: `counter`
--> src/main.rs:9:36
|
5 | let counter = Mutex::new(0);
| ------- move occurs because `counter` has type `Mutex<i32>`, which does not implement the `Copy` trait
...
9 | let handle = thread::spawn(move || {
| ^^^^^^^ value moved into closure here, in previous iteration of loop
10 | let mut num = counter.lock().unwrap();
| ------- use occurs due to use in closure

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

错误信息表明 counter 值在上一次循环中被移动了。所以 Rust 告诉我们不能将 counter 锁的所有权移动到多个线程中

多线程和多所有权

通过使用智能指针 Rc 来创建引用计数的值,可以拥有多所有者。

示例: 尝试使用 Rc 来允许多个线程拥有 Mutex

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::rc::Rc;
use std::sync::Mutex;
use std::thread;

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

for _ in 0..10 {
let counter = Rc::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());
}

再一次编译并…出现了不同的错误!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0277]: `Rc<Mutex<i32>>` cannot be sent between threads safely
--> src/main.rs:11:22
|
11 | let handle = thread::spawn(move || {
| ______________________^^^^^^^^^^^^^_-
| | |
| | `Rc<Mutex<i32>>` cannot be sent between threads safely
12 | | let mut num = counter.lock().unwrap();
13 | |
14 | | *num += 1;
15 | | });
| |_________- within this `[closure@src/main.rs:11:36: 15:10]`
|
= help: within `[closure@src/main.rs:11:36: 15:10]`, the trait `Send` is not implemented for `Rc<Mutex<i32>>`
= note: required because it appears within the type `[closure@src/main.rs:11:36: 15:10]`
note: required by a bound in `spawn`

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

第一行错误表明 Rc>cannot be sent between threads safely。编译器也告诉了我们原因 the traitSendis not implemented forRc>。下一部分会讲到 Send:这是确保所使用的类型可以用于并发环境的 trait 之一。

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

原子引用计数 Arc

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

为什么不是所有的原始类型都是原子性的?为什么不是所有标准库中的类型都默认使用 Arc 实现?

原因在于线程安全带有性能惩罚,我们希望只在必要时才为此买单。如果只是在单线程中对值进行操作,原子性提供的保证并无必要,代码可以因此运行的更快。

示例: 使用 Arc 包装一个 Mutex 能够实现在多线程之间共享所有权

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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());
}

这会打印出:

1
Result: 10

RefCell/Rc 与 Mutex/Arc 的相似性

  • 因为 counter 是不可变的,不过可以获取其内部值的可变引用;这意味着 Mutex 提供了内部可变性,就像 Cell 系列类型那样。正如使用 RefCell 可以改变 Rc 中的内容那样,同样的可以使用 Mutex 来改变 Arc 中的内容。
  • Rust 不能避免使用 Mutex 的全部逻辑错误。回忆一下使用 Rc 就有造成引用循环的风险,这时两个 Rc 值相互引用,造成内存泄漏。同理,Mutex 也有造成 死锁deadlock) 的风险。这发生于当一个操作需要锁住两个资源而两个线程各持一个锁,这会造成它们永远相互等待。

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

Rust 的并发模型中一个有趣的方面是:语言本身对并发知之 甚少。我们之前讨论的几乎所有内容,都属于标准库,而不是语言本身的内容。由于不需要语言提供并发相关的基础设施,并发方案不受标准库或语言所限:我们可以编写自己的或使用别人编写的并发功能。

然而有两个并发概念是内嵌于语言中的:std::marker 中的 Sync 和 Send trait。

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

  • Send 标记 trait 表明实现了 Send 的类型值的所有权可以在线程间传送。
  • 几乎所有的 Rust 类型都是 Send 的,
  • 不过有一些例外,包括 Rc:这是不能 Send 的,

因为如果克隆了 Rc 的值并尝试将克隆的所有权转移到另一个线程,这两个线程都可能同时更新引用计数。为此,Rc 被实现为用于单线程场景,这时不需要为拥有线程安全的引用计数而付出性能代价。

  • Rust 类型系统和 trait bound 确保永远也不会意外的将不安全的 Rc 在线程间发送。当尝试这么做的时候,会得到错误 the trait Send is not implemented for Rc>。而使用标记为 Send 的 Arc 时,就没有问题了。
  • 任何完全由 Send 的类型组成的类型也会自动被标记为 Send。几乎所有基本类型都是 Send 的,除了裸指针(raw pointer)

Sync 允许多线程访问

  • Sync 标记 trait 表明一个实现了 Sync 的类型可以安全的在多个线程中拥有其值的引用
  • 换一种方式来说,对于任意类型 T,如果 &T(T 的不可变引用)是 Send 的话 T 就是 Sync 的,这意味着其引用就可以安全的发送到另一个线程。
  • 类似于 Send 的情况,基本类型是 Sync 的,完全由 Sync 的类型组成的类型也是 Sync 的。
  • 智能指针 Rc 也不是 Sync 的,出于其不是 Send 相同的原因。RefCell和 Cell 系列类型不是 Sync 的。RefCell 在运行时所进行的借用检查也不是线程安全的。
  • Mutex 是 Sync 的,正如 “在线程间共享 Mutex”部分所讲的它可以被用来在多线程中共享访问。

手动实现 Send 和 Sync 是不安全的

  • 通常并不需要手动实现 Send 和 Sync trait,因为由 Send 和 Sync 的类型组成的类型,自动就是 Send 和 Sync 的。
  • 因为他们是标记 trait,甚至都不需要实现任何方法。他们只是用来加强并发相关的不可变性的。
  • 手动实现这些标记 trait 涉及到编写不安全的 Rust 代码,

当前重要的是,在创建新的由不是 Send 和 Sync 的部分构成的并发类型时需要多加小心,以确保维持其安全保证。“The Rustonomicon” 中有更多关于这些保证以及如何维持他们的信息。

Rust 的面向对象特性

面向对象语言的特点

  • 对象包含数据和行为

在这个定义下,Rust 是面向对象的:结构体和枚举包含数据而 impl 块提供了在结构体和枚举之上的方法。虽然带有方法的结构体和枚举并不被 称为 对象,但是他们提供了与对象相同的功能,

  • 封装隐藏了实现细节

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

Rust 中可以使用 pub 关键字来决定模块、类型、函数和方法是公有的,而默认情况下其他一切都是私有的。

举例:

比如,我们可以定义一个包含一个 i32 类型 vector 的结构体 AveragedCollection。结构体也可以有一个字段,该字段保存了 vector 中所有值的平均值。这样,希望知道结构体中的 vector 的平均值的人可以随时获取它,而无需自己计算。换句话说,AveragedCollection 会为我们缓存平均值结果。下面示例有 AveragedCollection 结构体的定义:

示例: AveragedCollection 结构体维护了一个整型列表和集合中所有元素的平均值。

文件名: src/lib.rs

1
2
3
4
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}

注意,结构体自身被标记为 pub,这样其他代码就可以使用这个结构体,但是在结构体内部的字段仍然是私有的。这是非常重要的,因为我们希望保证变量被增加到列表或者被从列表删除时,也会同时更新平均值。可以通过在结构体上实现 add、remove 和 average 方法来做到这一点,如示例所示:

示例: 在 AveragedCollection 结构体上实现了 add、remove 和 average 公有方法

文件名: src/lib.rs

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
impl AveragedCollection {
pub fn add(&mut self, value: i32) {
self.list.push(value);
self.update_average();
}

pub fn remove(&mut self) -> Option<i32> {
let result = self.list.pop();
match result {
Some(value) => {
self.update_average();
Some(value)
}
None => None,
}
}

pub fn average(&self) -> f64 {
self.average
}

fn update_average(&mut self) {
let total: i32 = self.list.iter().sum();
self.average = total as f64 / self.list.len() as f64;
}
}

公有方法 add、remove 和 average 是修改 AveragedCollection 实例的唯一方式。当使用 add 方法把一个元素加入到 list 或者使用 remove 方法来删除时,这些方法的实现同时会调用私有的 update_average 方法来更新 average 字段。

list 和 average 是私有的,所以没有其他方式来使得外部的代码直接向 list 增加或者删除元素,否则 list 改变时可能会导致 average 字段不同步。average 方法返回 average 字段的值,这使得外部的代码只能读取 average 而不能修改它。

因为我们已经封装好了 AveragedCollection 的实现细节,将来可以轻松改变类似数据结构这些方面的内容。例如,可以使用 HashSet 代替 Vec 作为 list 字段的类型。只要 add、remove 和 average 公有函数的签名保持不变,使用 AveragedCollection 的代码就无需改变。相反如果使得 list 为公有,就未必都会如此了: HashSet 和 Vec 使用不同的方法增加或移除项,所以如果要想直接修改 list 的话,外部的代码可能不得不做出修改。

如果封装是一个语言被认为是面向对象语言所必要的方面的话,那么 Rust 满足这个要求。在代码中不同的部分使用 pub 与否可以封装其实现细节。

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

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

如果一个语言必须有继承才能被称为面向对象语言的话,那么 Rust 就不是面向对象的。无法定义一个结构体继承父结构体的成员和方法。然而,如果你过去常常在你的编程工具箱使用继承,根据你最初考虑继承的原因,Rust 也提供了其他的解决方案。

选择继承有两个主要的原因。

  1. 第一个是为了重用代码:一旦为一个类型实现了特定行为,继承可以对一个不同的类型重用这个实现。相反 Rust 代码可以使用默认 trait 方法实现来进行共享,
  2. 第二个使用继承的原因与类型系统有关:表现为子类型可以用于父类型被使用的地方。这也被称为 多态polymorphism),这意味着如果多种对象共享特定的属性,则可以相互替代使用。

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

不同类型值的 trait 对象

vector 只能存储同种类型元素的局限。我们之前的示例中提供了一个定义 SpreadsheetCell 枚举来储存整型,浮点型和文本成员的替代方案。这意味着可以在每个单元中储存不同类型的数据,并仍能拥有一个代表一排单元的 vector。这在当编译代码时就知道希望可以交替使用的类型为固定集合的情况下是完全可行的。

然而有时我们希望库用户在特定情况下能够扩展有效的类型集合。

为了展示如何实现这一点,这里将创建一个图形用户接口(Graphical User Interface, GUI)工具的例子,它通过遍历列表并调用每一个项目的 draw 方法来将其绘制到屏幕上 —— 此乃一个 GUI 工具的常见技术。我们将要创建一个叫做 gui 的库 crate,它含一个 GUI 库的结构。这个 GUI 库包含一些可供开发者使用的类型,比如 Button 或 TextField。在此之上,gui 的用户希望创建自定义的可以绘制于屏幕上的类型:比如,一个程序员可能会增加 Image,另一个可能会增加 SelectBox。

这个例子中并不会实现一个功能完善的 GUI 库,不过会展示其中各个部分是如何结合在一起的。编写库的时候,我们不可能知晓并定义所有其他程序员希望创建的类型。我们所知晓的是 gui 需要记录一系列不同类型的值,并需要能够对其中每一个值调用 draw 方法。这里无需知道调用 draw 方法时具体会发生什么,只要该值会有那个方法可供我们调用。

在拥有继承的语言中,可以定义一个名为 Component 的类,该类上有一个 draw 方法。其他的类比如 Button、Image 和 SelectBox 会从 Component 派生并因此继承 draw 方法。它们各自都可以覆盖 draw 方法来定义自己的行为,但是框架会把所有这些类型当作是 Component 的实例,并在其上调用 draw。

不过 Rust 并没有继承,我们得另寻出路。

定义通用行为的 trait

为了实现 gui 所期望的行为,让我们定义一个 Draw trait,其中包含名为 draw 的方法。接着可以定义一个存放 trait 对象(trait object) 的 vector。trait 对象指向一个实现了我们指定 trait 的类型的实例,以及一个用于在运行时查找该类型的 trait 方法的表。我们通过指定某种指针来创建 trait 对象,例如 & 引用或 Box 智能指针,还有 dyn keyword, 以及指定相关的 trait( [“动态大小类型和 Sized trait”] 部分会介绍 trait 对象必须使用指针的原因)。我们可以使用 trait 对象代替泛型或具体类型。任何使用 trait 对象的位置,Rust 的类型系统会在编译时确保任何在此上下文中使用的值会实现其 trait 对象的 trait。如此便无需在编译时就知晓所有可能的类型。

Rust 刻意不将结构体与枚举称为 “对象”,以便与其他语言中的对象相区别。在结构体或枚举中,结构体字段中的数据和 impl 块中的行为是分开的,不同于其他语言中将数据和行为组合进一个称为对象的概念中。

trait 对象将数据和行为两者相结合,从这种意义上说 其更类似其他语言中的对象。不过 trait 对象不同于传统的对象,因为不能向 trait 对象增加数据。trait 对象并不像其他语言中的对象那么通用:其(trait 对象)具体的作用是允许对通用行为进行抽象。

下面的示例展示了如何定义一个带有 draw 方法的 trait Draw:

文件名: src/lib.rs

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

下面定义了一个存放了名叫 components 的 vector 的结构体 Screen。这个 vector 的类型是 Box,此为一个 trait 对象:它是 Box 中任何实现了 Draw trait 的类型的替身。

文件名: src/lib.rs

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

示例: 一个 Screen 结构体的定义,它带有一个字段 components,其包含实现了 Draw trait 的 trait 对象的 vector

在 Screen 结构体上,我们将定义一个 run 方法,该方法会对其 components 上的每一个组件调用 draw 方法,如示例所示:

文件名: src/lib.rs

1
2
3
4
5
6
7
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}

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

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

文件名: src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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();
}
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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();
}
}
}

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

实现 trait

现在来增加一些实现了 Draw trait 的类型。我们将提供 Button 类型。真正实现 GUI 库超出了范畴,所以 draw 方法体中不会有任何有意义的实现。为了想象一下这个实现看起来像什么,一个 Button 结构体可能会拥有 width、height 和 label 字段,如示例所示:

文件名: src/lib.rs

示例: 一个实现了 Draw trait 的 Button 结构体

1
2
3
4
5
6
7
8
9
10
11
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
}
}

在 Button 上的 width、height 和 label 字段会和其他组件不同,比如 TextField 可能有 width、height、label 以及 placeholder 字段。每一个我们希望能在屏幕上绘制的类型都会使用不同的代码来实现 Draw trait 的 draw 方法来定义如何绘制特定的类型,像这里的 Button 类型(并不包含任何实际的 GUI 代码,这超出了本章的范畴)。除了实现 Draw trait 之外,比如 Button 还可能有另一个包含按钮点击如何响应的方法的 impl 块。这类方法并不适用于像 TextField 这样的类型。

如果一些库的使用者决定实现一个包含 width、height 和 options 字段的结构体 SelectBox,并且也为其实现了 Draw trait,如示例所示:

示例: 另一个使用 gui 的 crate 中,在 SelectBox 结构体上实现 Draw trait

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
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
}
}

库使用者现在可以在他们的 main 函数中创建一个 Screen 实例。至此可以通过将 SelectBox 和 Button 放入 Box 转变为 trait 对象来增加组件。接着可以调用 Screen 的 run 方法,它会调用每个组件的 draw 方法。下面示例展示了这个实现:

示例: 使用 trait 对象来存储实现了相同 trait 的不同类型的值

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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();
}

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

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

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

例如,示例展示了当创建一个使用 String 做为其组件的 Screen 时发生的情况:

示例: 尝试使用一种没有实现 trait 对象的 trait 的类型

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
use gui::Screen;

fn main() {
let screen = Screen {
components: vec![Box::new(String::from("Hi"))],
};

screen.run();
}

我们会遇到这个错误,因为 String 没有实现 rust_gui::Draw trait:

1
2
3
4
5
6
7
8
9
10
11
12
$ cargo run
Compiling gui v0.1.0 (file:///projects/gui)
error[E0277]: the trait bound `String: Draw` is not satisfied
--> src/main.rs:5:26
|
5 | components: vec![Box::new(String::from("Hi"))],
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not implemented for `String`
|
= note: required for the cast to the object type `dyn Draw`

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

这告诉了我们,要么是我们传递了并不希望传递给 Screen 的类型并应该提供其他类型,要么应该在 String 上实现 Draw 以便 Screen 可以调用其上的 draw。

trait 对象执行动态分发

  • 对泛型使用 trait bound 时编译器所执行的单态化处理:编译器为每一个被泛型类型参数代替的具体类型生成了函数和方法的非泛型实现。单态化产生的代码在执行 静态分发static dispatch)。
  • 静态分发发生于编译器在编译时就知晓调用了什么方法的时候。
  • 动态分发dynamic dispatch 编译器在编译时无法知晓调用了什么方法。
  • 在动态分发的场景下,编译器生成的代码到运行时才能确定调用了什么方法。

当使用 trait 对象时,Rust 必须使用动态分发。编译器无法知晓所有可能用于 trait 对象代码的类型,所以它也不知道应该调用哪个类型的哪个方法实现。为此,Rust 在运行时使用 trait 对象中的指针来知晓需要调用哪个方法。动态分发也阻止编译器有选择的内联方法代码,这会相应的禁用一些优化。尽管在编写示例和可以支持示例中的代码的过程中确实获得了额外的灵活性,但仍然需要权衡取舍。

trait 对象需要类型安全

只有对象安全(object-safe)的 trait 可以实现为 特征(dyn)对象

这里有一些复杂的规则来实现 trait 的对象安全,但在实践中,只有两个相关的规则。

如果一个 trait 中定义的所有方法都符合以下规则,则该 trait 是对象安全的:

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

我们使用triat对象我们实际上是在做 动态分发。Rust 在运行时通过一个叫 vtable 的方法表来找到对应的方法实现。为了做到这一点,trait 的方法必须能够在编译时 完全确定方法签名(包括返回值的类型信息等),不能依赖未知的类型信息。

但是某些 trait 的方法依赖了 Self 或泛型,这样编译器就无法保证动态分发是安全的。

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

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

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

当我们尝试编译一些违反 trait 对象的对象安全规则的代码时,我们会收到编译器的提示。例如,我们想实现的 Screen 结构体来保存一个实现了 Clone trait 而不是 Draw trait 的类型,如下所示

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

我们将会收到如下错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ 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

这个错误意味着我们不能将此 trait 用于 trait 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 使用至少两种方法让代码工作
// 不要添加/删除任何代码行
trait MyTrait {
fn f(&self) -> Self;
}

impl MyTrait for u32 {
fn f(&self) -> Self { 42 }
}

impl MyTrait for String {
fn f(&self) -> Self { self.clone() }
}

fn my_function(x: Box<dyn MyTrait>) {
x.f()
}

fn main() {
my_function(Box::new(13_u32));
my_function(Box::new(String::from("abc")));

println!("Success!")
}

第一种方式修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
trait MyTrait {
fn f(&self) -> Self;
}

impl MyTrait for u32 {
fn f(&self) -> u32 { 42 }
}

impl MyTrait for String {
fn f(&self) -> String { self.clone() }
}

fn my_function(x: impl MyTrait) -> impl MyTrait {
x.f()
}

fn main() {
my_function(13_u32);
my_function(String::from("abc"));
}

第二种方式修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
trait MyTrait {
fn f(&self) -> Box<dyn MyTrait>;
}

impl MyTrait for u32 {
fn f(&self) -> Box<dyn MyTrait> { Box::new(42) }
}

impl MyTrait for String {
fn f(&self) -> Box<dyn MyTrait> { Box::new(self.clone()) }
}

fn my_function(x: Box<dyn MyTrait>) -> Box<dyn MyTrait> {
x.f()
}

fn main() {
my_function(Box::new(13_u32));
my_function(Box::new(String::from("abc")));
}

面向对象设计模式的实现

  • 状态模式state pattern)是一个面向对象设计模式。该模式的关键在于一个值有某些内部状态,体现为一系列的 状态对象,同时值的行为随着其内部状态而改变
  • 状态对象共享功能:在 Rust 中使用结构体和 trait 而不是对象和继承。每一个状态对象负责其自身的行为,以及该状态何时应当转移至另一个状态。持有一个状态对象的值对于不同状态的行为以及何时状态转移毫不知情。
  • 使用状态模式意味着当程序的业务需求改变时,无需改变值持有状态或者使用值的代码。我们只需更新某个状态对象中的代码来改变其规则,或者是增加更多的状态对象。
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
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
fn content<'a>(&self, _post: &'a Post) -> &'a str {
""
}
}

pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}

impl Post {
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}

pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}

pub fn content(&self) -> &str {
// 委托给状态对象决定是否返回内容
self.state.as_ref().unwrap().content(self)
}

pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review());
}
}

pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve());
}
}
}

// ===== 状态实现 =====

struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self // ignore
}
}

struct PendingReview {}
impl State for PendingReview {
fn approve(self: Box<Self>) -> Box<dyn State> {
Box::new(Published {})
}
fn request_review(self: Box<Self>) -> Box<dyn State> {
self // ignore
}
}

struct Published {}
impl State for Published {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self // ignore
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self // ignore
}
fn content<'a>(&self, post: &'a Post) -> &'a str {
&post.content
}
}

// ===== 测试 =====

fn main() {
let mut post = Post::new();

post.add_text("Rust makes systems programming safe!");

assert_eq!("", post.content()); // 还在草稿,不能看内容

post.request_review();
assert_eq!("", post.content()); // 待审核,还是不能看内容

post.approve();
assert_eq!("Rust makes systems programming safe!", post.content()); // 发布了!

println!("Published content: {}", post.content());
}

模式与模式匹配

模式:

  • 模式是 Rust 中的一种特殊语法,用于匹配复杂和简单类型的结构
  • 将模式与匹配表达式和其它构造结合使用,可以更好地控制程序的控制流
  • 模式由以下元素(的一些组合)组成:
    • 字面值
    • 解构的数组,enum,struct 和 tuple
    • 变量
    • 通配符
    • 占位符

match 的 Arm

  • match VALUE{PARTTERN=>EXPRESSION,PARTTERN=>EXPRESSION,PARTTERN=>EXPRESSION,}
  • 表达式的要求:详尽(包含所有的可能性)

  • 一个特殊的模式:_ (下划线):它不会匹配任何东西,不会绑定到变量,通常用于 match 的最后一个 arm,或用于忽略某些值

条件 if let 表达式

  • if let 表达式主要是作为一种简短的方式来等价的替代只有一个匹配项的 match
  • if let 可选的可以拥有 else,包括:
    • else if
    • else if let
  • 但,if let 不会检查穷举性例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let favorite_color: Option<&str> = None;
let is_tuesday = false;
let age: Result<u8,_> = "34".parse();

if let Some(color) = favorite_color{
print!("Using your favorite color,{},as the background",color);
} else if is_tuesday{
println!("Tuesday is green day!");
} else if let Ok(age) = age {
if age > 30 {
println!("Using purple as the background color");
} else {
println!("Using orange as the background color");
}
} else {
println!("Using blue as the background color");
}
}

while let 条件循环

  • 只要模式继续满足匹配的条件,那它允许 while 循环一直运行
1
2
3
4
5
6
7
8
9
10
fn main() {
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);

while let Some(top) = stack.pop() {
println!("{}",top);
}
}

for 循环的模式匹配

  • for 循环是 Rust 中最常见的循环
  • for 循环中,模式就是紧随 for 关键字后的值
1
2
3
4
5
6
fn main() {
let v = vec!['a','b','c'];
for (index,value) in v.iter().enumerate(){
println!("{} is at index {}",value,index);
}
}

iter().enumerate()返回的是一个元组

let 语句的模式匹配

  • let 语句也是模式
  • let PARTTERN = EXPRESSION
1
2
let a = 5;
let (x,y,z) = (1,2,3);

函数参数

  • 函数的参数也可以是模式
1
2
3
4
5
6
7
fn print_coordinates(&(x,y): &(i32,i32)) {
println!("Current location: ({},{})",x,y);
}
fn main() {
let point = (3,5);
print_coordinates(&point);
}

可辨驳性:模式是否会无法匹配

  • 模式的两种形式:可辨驳的,无可辩驳的
  • 能匹配任何可能传值的模式:无可辩驳的 入 let x= 4;
  • 对某些可能的值,无法进行匹配的模式:可辨驳的 例如:if let Some(x) = a_value
  • 函数参数,let 语句,for 循环只接受无可辩驳的模式
  • if let 和 while let 接受可辨驳和无可辩驳的模式
1
2
3
4
fn main() {
let a: Option<i32> = Some(5);
let Some(x) = a;
}

输出

1
2
3
4
5
6
7
8
error[E0005]: refutable pattern in local binding: `None` not covered
--> src\main.rs:3:9
|
3 | let Some(x) = a;
| ^^^^^^^ pattern `None` not covered
|
= note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
= note: for more information, visit https://doc.rust-lang.org/book/ch18-02-refutability.html

无法匹配,因为模式没有覆盖 None 这种情况

修改

1
2
3
4
5
6
fn main() {
let a: Option<i32> = Some(5);
if let Some(x) = a{

};
}

match 除了最后一个分支,其它的分支都是可辨驳的,最后一个分支是不可辩驳的,因为它需要匹配所有剩余的情况

匹配字面值

1
2
3
4
5
6
7
8
9
fn main(){
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
}

匹配命名变量

  • 命名的变量是可匹配任何值的无可辩驳模式
1
2
3
4
5
6
7
8
9
10
fn main(){
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(y) => println!("Matched,y={:?}",y),
_ => println!("Default Case,x={:?}",x),
}
println!("at the end: x={:?},y={:?}",x,y);
}

这里 match 的第二个 arm 中y 是一个新的变量,存在于该 arm 的作用域

匹配一个可变引用

使用模式 &mut V 去匹配一个可变引用时,你需要格外小心,因为匹配出来的 V是一个值,而不是可变引用

1
2
3
4
5
6
7
8
9
fn main() {
let mut v = String::from("hello,");
let r = &mut v;

match r {
// The type of value is &mut String
value => value.push_str(" world!")
}
}

多重模式

  • 在 match 表达式中,使用 | 语法(就是或的意思)可以匹配多种模式
1
2
3
4
5
6
7
8
fn main(){
let x= 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
}

使用..=来匹配某个范围的值

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main(){
let x= 5;
match x{
1..=5 => println!("one through five"),
_ => println!("something else"),
}
let x = 'c';
match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
}

数字或字符都可以

解构以分解值

  • 可以使用模式来结构 struct,enum,tuple,从而引用这些类型值的不同部分

解构赋值

1
2
3
4
5
6
fn main() {
let (x, y);
(x,..) = (3, 4);
[.., y] = [1, 2];
assert_eq!([x,y],[3,2]);
}

解构元组

1
2
3
4
5
6
7
fn main() {
let (mut x, y) = (1, 2);
x += 2;

assert_eq!(x, 3);
assert_eq!(y, 2);
}

解构结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#[derive(Debug)]
struct Point{
x: i32,
y: i32,
}
fn main(){
let p = Point{x:0,y:7};
let Point {x:a,y:b}=p;
assert_eq!(0,a);
assert_eq!(7,b);

//简写形式
let Point{x,y} = p;
assert_eq!(0,x);
assert_eq!(7,y);

match p {
Point {x,y:0}=> println!("On the x axis at {}",x),//要求y必须为0
Point {x:0,y} => println!("On the y axis at {}",y),//要求x必须为0
Point {x,y} => println!("On neither axis:({},{})",x,y),
}
}

解构枚举

注意:下面的代码会触发copy或move

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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!("rgb is ({},{},{})",r,g,b);
}
}
}

运行下面的代码:

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
#[derive(Debug)]
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let msg = Message::Write(String::from("Hello World\n"));
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!("rgb is ({},{},{})", r, g, b);
}
}
println!("{:?}", msg);
}

报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
error[E0382]: borrow of partially moved value: `msg`
--> src/main.rs:24:22
|
17 | Message::Write(text) => {
| ---- value partially moved here
...
24 | println!("{:?}", msg);
| ^^^ value borrowed here after partial move
|
= note: partial move occurs because value has type `String`, which does not implement the `Copy` trait
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: borrow this binding in the pattern to avoid moving the value
|
17 | Message::Write(ref text) => {
| +++

For more information about this error, try `rustc --explain E0382`.
error: could not compile `rust_programming` (bin "rust_programming") due to 1 previous error

可以匹配引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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!("rgb is ({},{},{})", r, g, b);
}
}
}

解构嵌套的结构体和枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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);
}
_ => (),
}
}

解构结构体和元组

1
2
3
4
5
6
7
struct Point{
x: i32,
y: i32,
}
fn main(){
let ((feet,inches),Point{x,y}) = ((3,10),Point{x:3,y:-10});
}

在模式中忽略值

  • 有几种方式可以在模式中忽略整个值或部分值:

_ 忽略整个值

1
2
3
4
5
6
fn foo(_:i32,y:i32){
println!("y is {}",y);
}
fn main(){
foo(3, 4);
}

使用嵌套_忽略值的一部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main(){
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;
}
}
println!("setting is {:?}",setting_value);

let numbers = (3,4,8,16,32);
match numbers {
(first,_,third,_,fifth)=>{
println!("Some numbers:{},{},{}",first,third,fifth);
}
}
}

使用以_开头命名来忽略未使用的变量

1
2
3
4
5
6
7
8

fn main(){
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{:?}",s);
}

模式匹配中_s 是一个新的变量,模式匹配把 s 所有权移动到_s,后面再访问 s 就会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
error[E0382]: borrow of partially moved value: `s`
--> src/main.rs:6:22
|
3 | if let Some(_s) = s {
| -- value partially moved here
...
6 | println!("{:?}", s);
| ^ value borrowed here after partial move
|
= note: partial move occurs because value has type `String`, which does not implement the `Copy` trait
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: borrow this binding in the pattern to avoid moving the value
|
3 | if let Some(ref _s) = s {
| +++

For more information about this error, try `rustc --explain E0382`.
error: could not compile `rust_programming` (bin "rust_programming") due to 1 previous error

使用_

1
2
3
4
5
6
7
fn main(){
let s = Some(String::from("Hello!"));
if let Some(_) = s {
println!("found a string");
}
println!("{:?}",s);
}

_ ,不会发生绑定,不会移动所有权

..(忽略值的剩余部分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Point{
x: i32,
y: i32,
z: i32,
}
fn main(){
let origin = Point{x:0,y:0,z:0};
match origin {
Point {x,..} => println!("x is {}",x),
}

let numbers = (2,4,8,16,32);
match numbers {
(first,..,last)=>{
println!("Some numbers: {},{}",first,last)
}
}
}

要加逗号,

1
2
3
4
5
6
7
8
9
10
fn main() {
let numbers = (2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048);

match numbers {
(first,..,last) => {
assert_eq!(first, 2);
assert_eq!(last, 2048);
}
}
}

使用 match 守卫来提供额外的条件

  • match 守卫就是 match arm 模式后额外的 if 条件,想要匹配该条件也必须满足
  • match 守卫适合更复杂的场景
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
fn main(){
let num = Some(4);
match num {
Some(x) if x < 5 => println!("less than five:{}",x),
Some(x) => println!("{}",x),
None => (),
}
}
fn main(){
let x = Some(5);
let y = 10;

match x {
Some(50) => println!("Got 50"),
Some(n) if n==y => println!("Matched,n = {:?}",n),//这里if n==y不是一个模式,不会引用新的变量
_ =>println!("Default case,x ={:?}",x),
}
println!("at the end:x={:?},y={:?}",x,y);
}
fn main(){
let x= 4;
let y = false;
match x {
4 | 5 | 6 if y=> println!("yes"),
_ => println!("no"),
}
}

@绑定

  • @ 符号可以让我们可以创建一个变量,该变量可以在测试某个值是否与模式匹配的同时保存该值

就相当于一个等号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum Message{
Hello {id:i32},
}
fn main(){
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);
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Point {
x: i32,
y: i32,
}

fn main() {
// fill in the blank to let p match the second arm
let p = Point { x: 2, y: 20 }; // x can be [0, 5], y can be 10 20 or 30

match p {
Point { x, y: 0 } => println!("On the x axis at {}", x),
// second arm
Point { x: 0..=5, y: y@ (10 | 20 | 30) } => 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
enum Message {
Hello { id: i32 },
}

fn main() {
let msg = Message::Hello { id: 5 };

match msg {
Message::Hello {
id: 3..=7,
} => println!("id 值的范围在 [3, 7] 之间: {}", id),//Error cannot find value `id` in this scope
Message::Hello { id: newid@10 | 11 | 12 } => {//Error variable `newid` is not bound in all patterns pattern doesn't bind `newid`
println!("id 值的范围在 [10, 12] 之间: {}", newid)
}
Message::Hello { id } => println!("Found some other id: {}", id),
}
}

修复错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum Message {
Hello { id: i32 },
}

fn main() {
let msg = Message::Hello { id: 5 };

match msg {
Message::Hello {
id: id @3..=7,
} => println!("id 值的范围在 [3, 7] 之间: {}", id),
Message::Hello { id: newid@(10 | 11 | 12) } => {
println!("id 值的范围在 [10, 12] 之间: {}", newid)
}
Message::Hello { id } => println!("Found some other id: {}", id),
}
}

unsafe Rust

  • 隐藏着第二个语言,它没有强制内存安全保证:unsafe Rust(不安全的 Rust)

和普通的 Rust 一样,但提供了额外的超能力

  • Unsafe Rust 存在的原因:
  1. 静态分析是保守的,使用 unsafe rust 就相当于告诉编译器:我知道自己在做什么,并承担相应的风险
  2. 计算机硬件本身就是不安全的,Rust 需要能够进行底层系统编程

unsafe 超能力

  • 使用 unsafe 关键字来切换到 unsafe Rust,开启一个块,里面放着 unsafe 代码
  • unsafe Rust 里执行的四个动作(unsafe 超能力):
  1. 解引用原始指针
  2. 调用 unsafe 函数或方法
  3. 访问或修改可变的静态变量
  4. 实现 unsafe trait
  • 注意:

unsafe 并没有关闭借用检查或停用其它安全的安全检查

任何内存安全相关的而错误必须留在 unsafe 块里

尽可能隔离 unsafe 代码,最好将其封装在安全的抽象里,提供安全的 API

解引用原始指针

  • 原始指针

可变的: *mut T

不可变的: *const T,意味着指针在解引用后不能直接对其进行赋值

注意:这里的*不是解引用符号,它是类型名的一部分

  • 与引用不同,原始指针:
  1. 允许通过同时具有可变和不可变指针或指向同一位置的可变指针来忽略借用规则
  2. 无法保证能指向合理的内存
  3. 允许为 null
  4. 不实现任何自动清理
  • 放弃保证的安全,换取更好的性能/与其它语言或硬件接口的能力
1
2
3
4
5
6
7
fn main(){
let mut num=5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
let address = 0x012345usize;
let r = address as *const i32;
}

可以在安全代码块里创建原始指针,但不能够解引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main(){
let mut num=5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

unsafe{
println!("r1:{}",*r1);
println!("r2:{}",*r2);
}

let address = 0x012345usize;
let r = address as *const i32;
unsafe{
println!("r:{}",*r);
}
}

为什么要使用原始指针?

  • 与 C 语言进行接口
  • 构建借用检查器无法理解的安全抽象

调用 unsafe 函数或方法

  • unsafe 函数或方法:在定义前加上了 unsafe 关键字
    • 调用前需手动满足一些条件(主要靠看文档),因为 Rust 无法对这些条件进行验证
    • 需要在 unsafe 块里进行调用
1
2
3
4
5
6
unsafe fn dangerous(){}
fn main(){
unsafe{
dangerous();
}
}

创建 unsafe 代码的安全抽象

  • 函数包含 unsafe 代码并不意味着需要将整个函数标记为 unsafe
  • 将 unsafe 代码包裹在安全函数中是一个常见的抽象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::vec;

fn split_at_mut(slice:&mut[i32],mid:usize)->(&mut [i32],&mut[i32]){
let len = slice.len();
assert!(mid<=len);
(&mut slice[..mid],&mut slice[mid..])
}
fn main(){
let mut v= vec![1,2,3,4,5,6];
let r = &mut v[..];
let (a,b) = r.split_at_mut(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
error[E0499]: cannot borrow `*slice` as mutable more than once at a time
--> src\main.rs:6:29
|
3 | fn split_at_mut(slice:&mut[i32],mid:usize)->(&mut [i32],&mut[i32]){
| - let's call the lifetime of this reference `'1`
...
6 | (&mut slice[..mid],&mut slice[mid..])
| ------------------------^^^^^--------
| | | |
| | | second mutable borrow occurs here
| | first mutable borrow occurs here
| returning this value requires that `*slice` is borrowed for `'1`

For more information about this error, try `rustc --explain E0499`.

使用 unfase 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 v= vec![1,2,3,4,5,6];
let r = &mut v[..];
let (a,b) = r.split_at_mut(3);
assert_eq!(a,&mut [1,2,3]);
assert_eq!(b,&mut [4,5,6]);
}

使用 extern 函数调用外部代码

  • extern 关键字:简化创建和使用外部函数接口(FFI)的过程
  • 外部函数接口(FFI,Foreign Function Interface) : 它允许一种编程语言定义函数,并让其它编程语言能调用这些函数
  • extern 块中声明的函数在 Rust 代码中总是不安全的。因为其他语言不会强制执行 Rust 的规则且 Rust 无法检查它们,所以确保其安全是程序员的责任
1
2
3
4
5
6
7
8
extern "C"{//"C"指明外部函数应用的二进制接口abi(application binary interface)
fn abs(input: i32) ->i32;//想要调用的外部函数的签名
}
fn main(){
unsafe{
println!("Absolute value of -3 according to C:{}",abs(-3));
}
}
  • 应用二进制接口(ABI,Application Binary Interface):定义函数在汇编层的调用方式
  • “C” ABI 是最常见的 ABI,它遵循 C 语言的 ABI

从其它语言调用 Rust 函数

  • 可以使用 extern 创建接口,其它语言通过它们可以调用 Rust 函数
  • fn 前添加 extern 关键字,并指定 ABI
  • 还需添加#[no_mangle]注解:避免 Rust 在编译时改变它的名称
1
2
3
4
5
6
7
8
#[no_mangle]
pub extern "C" fn call_from_c(){
println!("Just called a Rust function from C!");//编译链接后就可被c语言访问了,extern 的使用无需 unsafe。
}

fn main(){

}

访问或修改一个可变的静态变量

  • Rust 支持全局变量,但因为所有权机制可能产生某些问题,例如数据竞争
  • 在 Rust 里,全局变量叫做静态(static)变量
1
2
3
4
5
static HELLO_WORLD: &str = "Hello, world!";

fn main() {
println!("name is: {}", HELLO_WORLD);
}
  • 静态(static)变量类似常量。
  • 通常静态变量的名称采用 SCREAMING_SNAKE_CASE 写法。
  • 静态变量只能储存拥有 ‘static 生命周期的引用,这意味着 Rust 编译器可以自己计算出其生命周期而无需显式标注。
  • 访问不可变静态变量是安全的。

静态变量和常量的区别:

  • 静态变量中的值有一个固定的内存地址。使用这个值总是会访问相同的地址。常量则允许在任何被用到的时候复制其数据
  • 静态变量可以是可变的。访问和修改可变静态变量都是 不安全 的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}

fn main() {
add_to_count(3);

unsafe {
println!("COUNTER: {}", COUNTER);
}
}

任何读写 COUNTER 的代码都必须位于 unsafe 块中。这段代码可以编译并如期打印出 COUNTER: 3,因为这是单线程的。拥有多个线程访问 COUNTER 则可能导致数据竞争

拥有可以全局访问的可变数据,难以保证不存在数据竞争,这就是为何 Rust 认为可变静态变量是不安全的。任何可能的情况,优先使用智能指针,这样编译器就能检测不同线程间的数据访问是否是安全的。

实现不安全 trait

  • 当 trait 中至少有一个方法中包含编译器无法验证的不变式(invariant)时 trait 是不安全的。
  • 可以在 trait 之前增加 unsafe 关键字将 trait 声明为 unsafe,同时 trait 的实现也必须标记为 unsafe
1
2
3
4
5
6
7
8
9
unsafe trait Foo {
// methods go here
}

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

fn main() {}

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

访问联合体中的字段

仅适用于 unsafe 的最后一个操作是访问 联合体 中的字段,union 和 struct 类似,但是在一个实例中同时只能使用一个声明的字段。联合体主要用于和 C 代码中的联合体交互。访问联合体的字段是不安全的,因为 Rust 无法保证当前存储在联合体实例中数据的类型。可以查看 (https://doc.rust-lang.org/reference/items/unions.html) 了解有关联合体的更多信息。

何时使用不安全的代码

使用 unsafe 来进行这五个操作(超能力)之一是没有问题的,甚至是不需要深思熟虑的,不过使得 unsafe 代码正确也实属不易,因为编译器不能帮助保证内存安全。当有理由使用 unsafe 代码时,是可以这么做的,通过使用显式的 unsafe 标注可以更容易地在错误发生时追踪问题的源头。

高级 trait

在 trait 定义中使用关联类型来指定占位类型

  • 关联类型(associate type)是 trait 中的类型占位符,它可以用于 trait 的方法签名中:

可以定义出包含某些类型的 trait,而在实现前无需知道这些类型是什么

1
2
3
4
5
6
7
pub trait Iterator {
type Item;
fn next(&mut self)->Option<Self::Item>;
}
fn main(){
println!("Hello World");
}

关联类型与泛型的区别

泛型 关联类型
每次实现 Trait 时标注类型 无需标注类型
可以为一个类型多次实现某个 Trait(不同的泛型参数) 无法为单个类型多次实现某个 Trait
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
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
pub trait Iterator2<T>{
fn next(&mut self) -> Option<T>;
}
struct Counter{

}
impl Iterator for Counter{//只能实现一次
type Item=u32;
fn next(&mut self) -> Option<Self::Item> {
None
}
}
// impl Iterator for Counter{//只能实现一次,第二次为String实现报错
// type Item=String;
// fn next(&mut self) -> Option<Self::Item> {
// None
// }
// }

impl Iterator2<String> for Counter {
fn next(&mut self) -> Option<String> {
None
}
}

impl Iterator2<u32> for Counter {//可以为不同的类型实现多次
fn next(&mut self) -> Option<u32> {
None
}
}

fn main(){

}

关联类型主要用于提升代码的可读性,例如以下代码 :

1
2
3
4
pub trait CacheableItem: Clone + Default + fmt::Debug + Decodable + Encodable {
type Address: AsRef<[u8]> + Clone + fmt::Debug + Eq + Hash;
fn is_null(&self) -> bool;
}

相比 AsRef<[u8]> + Clone + fmt::Debug + Eq + Hash, Address 的使用可以极大的减少其它类型在实现该特征时所需的模版代码.

例子:使用关联类型:

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
struct Container(i32, i32);

// 使用关联类型实现重新实现以下特征
// trait Contains {
// type A;
// type B;

trait Contains<A, B> {
fn contains(&self, _: &A, _: &B) -> bool;
fn first(&self) -> i32;
fn last(&self) -> i32;
}

impl Contains<i32, i32> for Container {
fn contains(&self, number_1: &i32, number_2: &i32) -> bool {
(&self.0 == number_1) && (&self.1 == number_2)
}
// Grab the first number.
fn first(&self) -> i32 { self.0 }

// Grab the last number.
fn last(&self) -> i32 { self.1 }
}

fn difference<A, B, C: Contains<A, B>>(container: &C) -> i32 {
container.last() - container.first()
}

fn main() {
let number_1 = 3;
let number_2 = 10;

let container = Container(number_1, number_2);

println!("Does container contain {} and {}: {}",
&number_1, &number_2,
container.contains(&number_1, &number_2));
println!("First number: {}", container.first());
println!("Last number: {}", container.last());

println!("The difference is: {}", difference(&container));
}

实现

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
struct Container(i32, i32);

// A trait which checks if 2 items are stored inside of container.
// Also retrieves first or last value.
trait Contains {
// Define generic types here which methods will be able to utilize.
type A;
type B;

fn contains(&self, _: &Self::A, _: &Self::B) -> bool;
fn first(&self) -> i32;
fn last(&self) -> i32;
}

impl Contains for Container {
// Specify what types `A` and `B` are. If the `input` type
// is `Container(i32, i32)`, the `output` types are determined
// as `i32` and `i32`.
type A = i32;
type B = i32;

// `&Self::A` and `&Self::B` are also valid here.
fn contains(&self, number_1: &i32, number_2: &i32) -> bool {
(&self.0 == number_1) && (&self.1 == number_2)
}
// Grab the first number.
fn first(&self) -> i32 { self.0 }

// Grab the last number.
fn last(&self) -> i32 { self.1 }
}

fn difference<C: Contains>(container: &C) -> i32 {
container.last() - container.first()
}

fn main() {
let number_1 = 3;
let number_2 = 10;

let container = Container(number_1, number_2);

println!("Does container contain {} and {}: {}",
&number_1, &number_2,
container.contains(&number_1, &number_2));
println!("First number: {}", container.first());
println!("Last number: {}", container.last());

println!("The difference is: {}", difference(&container));
}

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

  • 可以在使用泛型参数时为泛型指定一个默认的具体类型
  • 语法:
  • 这种结束常用于运算符重载(operator overloading)
  • Rust 不允许创建自己的运算符及重载任意的运算符
  • 可以通过实现 std::ops 中列出的那些 trait 来重载一部分相应的运算符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::ops::Add;

#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
assert_eq!(
Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
Point { x: 3, y: 3 }
);
}

这里使用的是 add 的默认泛型参数 self

1
2
3
4
5
6
7
8
9
10
11
12
use std::ops::Add;
struct Millimeters(u32);
struct Meter(u32);
impl Add<Meter> for Millimeters {
type Output = Millimeters;
fn add(self, rhs: Meter) -> Self::Output {
Millimeters(self.0+(other.0*1000))
}
}
fn main(){

}

这里指定泛型参数

默认泛型参数的主要应用场景

  • 扩展一个类型而不破坏现有的代码
  • 允许在大部分用户都不需要的特定场景下进行自定义

完全限定语法(Fully Qualified Syntax)

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
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;
person.fly();//调用本身的方法
Pilot::fly(&person);//调用Pilot trait中的方法
Wizard::fly(&person);//调用Wizard trait中的方法
}

无参的形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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::baby_name());
println!("A baby dog is called a{}",Animal::baby_name());//报错
}

这里的 baby_name 没有参数,编译器不知道是哪个 Dog 调用

  • 完全限定语法:::function(receiver_if_method,netx_arg,..) ;
  • 可以在任何调用函数或方法的地方使用
  • 允许忽略那些从其他上下文能推导出来的部分
  • 当 Rust 无法区分你期望调用哪个具体实现的时候,才需要使用这种语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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::baby_name());
println!("A baby dog is called a{}",<Dog as Animal>::baby_name());
}

使用 supertrait 来要求 trait 附带其它 trait 的功能

  • 需要在一个 trait 中使用其它 trait 的功能
    • 需要被依赖的 trait 也被实现
    • 那个被间接以来的 trait 就是当前 trait 的 supertrait
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 std::fmt::{self, write};
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));
}
}
struct Point{
x:i32,
y:i32,
}
impl OutlinePrint for Point {
}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({},{})",self.x,self.y)
}
}

fn main(){

}

使用 newtype 模式在外部类型上实现外部 trait

  • 孤儿类型:只有当 trait 或类型定义在本地包时,才能为该类型实现这个 trait
  • 可以通过 newtype 模式来绕过这一规则
    • 利用 tuple struct(元组结构体)创建一个新的类型

(例子)

1
2
3
4
5
6
7
8
9
10
11
12
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(", "))
}
}
fn main(){
let w = Wrapper(vec![String::from("hello"),String::from("world")]);
println!("w={}",w);
}

高级类型

使用 newtype 模式实现类型安全和抽象

  • newtype 模式可以:
    • 用来静态的保证各种值之间不会混淆并表明值的单位
    • 为类型的某些细节提供抽象能力
    • 通过轻量级的封装来隐藏内部实现细节

使用类型别名创建类型同义词

  • Rust 提供了类型别名的功能:——为现有的类型生产另外的名称(同义词)——并不是一个独立的类型——使用 type 关键字
  • 主要用途:减少代码的字符重复
  • 类似于C的typedef
1
2
3
4
5
6
7
8
9
fn takes_long_type(f: Box<dyn Fn()+Send+'static>){
//snip
}
fn returns_long_type()->Box<dyn Fn()+Send+'static>{
Box::new(|| println!("hi"))
}
fn main(){
let f:Box<dyn Fn()+Send+'static> = Box::new(|| println!("hi"));
}

使用类型别名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Thunk = Box<dyn Fn()+Send+'static>;

fn takes_long_type(f: Thunk){
//snip
}
fn returns_long_type()->Thunk{
Box::new(|| println!("hi"))
}
fn main(){
let f:Thunk = Box::new(|| println!("hi"));
}
use std::io::Error;
use std::fmt;
pub trait Write {
fn write(&mut self,buf: &[u8])->Result<usize,Error>;
fn flush(&mut self)->Result<(),Error>;
fn write_all(&mut self,buf: &[u8])->Result<(),Error>;
fn write_fmt(&mut self,fmt: fmt::Arguments)->Result<(),Error>;
}
fn main(){

}

使用类型别名

1
2
3
4
5
6
7
8
9
10
11
12
13
use std::io::Error;
use std::fmt;
// type Result<T> = Result<T,std::io::Error>;标准库中定义了这个
type Result<T> = std::io::Result<T>;
pub trait Write {
fn write(&mut self,buf: &[u8])->Result<usize>;
fn flush(&mut self)->Result<()>;
fn write_all(&mut self,buf: &[u8])->Result<()>;
fn write_fmt(&mut self,fmt: fmt::Arguments)->Result<()>;
}
fn main(){

}

never 类型

  • 有一个名为!的特殊类型:
    • 它没有任何值,行话称为空类型(empty type)
    • 我们倾向于叫它 never 类型,因为它在不返回的函数中充当返回类型
  • 不返回值的函数也被称作发散函数(diverging fuction)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn bar() -> !{
//return (),返回了单元类型,但是不可能创建出返回!类型的函数
}
fn main(){

}
fn main(){
let guess = "";
loop{
let guess:u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
}
}

match 表达式要求各个分支返回的类型是相同的,而 continue 会返回 never 类型,该类型可以安全地强制转换为 num 所对应的类型

1
2
3
4
5
6
7
8
impl<T> Option<T>{
pub fn unwrap(self) -> T{
match self{
Some(val) => val,
None=>panic!("called `Option::unwrap()` on a `None` value"),
}
}
}

panic!返回 never 类型

动态大小和 Sized Trait

  • Rust 需要在编译时确定为一个特定类型的值分配多少空间
  • 动态大小的类型(Dynamically Sized Types,DST)的概念:
    • 编写代码时使用只有在运行时才能确定大小的值
  • str 是动态大小的类型(注意不是&str):只有运行时才能确定字符串的长度
    以下代码无法正常工作:
1
2
let s1:str = "Hello therel";
let s2:str = "How is it going";

因为他们都是同一个类型,需要的空间应该一样,但是这里声明时没有确定共同的空间

解决办法: 使用&str 字符串切片类型

Rust 使用动态大小类型的通用方式

  • 附带一些额外的元数据来存储动态信息的大小
    • 使用动态大小类型时总会把它的值放在某种指针后面

另外一种动态大小的类型:trait

  • 每个 trait 都是一个动态大小的类型,可以通过名称对其进行引用
  • 为了将 trait 用作 trait 对象,必须将它放置在某种指针之后
    • 例如 &dyn Trait 或 Box (Rc) 之后

Sized trait

  • 为了处理动态大小的类型,Rust 提供了一个 Sized trait 来确定一个类型的大小在编译时是否已知——编译时可计算出大小的类型会自动实现这一 trait
  • Rust 还会为每一个泛型函数隐式的添加 Sized 约束
1
2
3
4
5
6
7
8
fn generic<T>(t:T){

}
fn generic<T: Sized>(t:T){
}
fn main(){

}
  • 默认情况下,泛型函数只能被用于编译时已经知道大小的类型,可以通过特殊语法来解除这一限制

?Sized trait 约束

1
2
3
fn generic<T: ?Sized>(t:&T){

}
  • T 可能是也可能不是 Sized
  • 这个语法只能用在 Sized 上面,不能被用于其它 trait

高级函数和闭包

函数指针

  • 可以将函数传递给其它函数
  • 函数在传递过程中会被强制转换为 fn 类型
  • fn 类型就是 函数指针(function pointer)
1
2
3
4
5
6
7
8
9
10
fn add_one(x:i32)->i32{
x+1
}
fn do_twice(f: fn(i32)->i32,arg:i32)->i32{
f(arg) + f(arg)
}
fn main(){
let answer = do_twice(add_one, 5);
println!("The answer is:{}",answer);
}

函数指针与闭包的不同

  • fn 是一个类型而不是一个 trait

    • 可以直接指定 fn 为参数类型,不用声明一个以 Fn trait 为约束的泛型参数
  • 函数指针实现了全部 3 种闭包 trait(Fn,FnMut,FnOnce):

    • 总是可以把函数指针用作参数传递给一个接受闭包的参数
    • 所以,倾向于搭配闭包 trait 的泛型来编写函数:可以同时接收闭包和普通函数
  • 某些情景,只想接收 fn 而不接收闭包
    • 与外部不支持闭包的代码交互:C 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main(){
let list_of_numbers = vec![1,2,3];
let list_of_strings:Vec<String> = list_of_numbers
.iter()
.map(|i| i.to_string())
.collect();

let list_of_numbers = vec![1,2,3];
let list_of_strings: Vec<String> = list_of_numbers
.iter()
.map(ToString::to_string)
.collect();
}
fn main(){
enum Status{
Value(u32),
Stop,
}
let v = Status::Value(3);
let list_of_statuses:Vec<Status> = (0u32..20).map(Status::Value).collect();
}

返回闭包

  • 闭包使用 trait 进行表达,无法在函数中直接返回一个闭包,可以将一个实现了该 trait 的具体类型作为返回值
1
2
3
4
5
6
7
8
9
10
11
// fn returns_closure()->Fn(i32)->i32{//返回类型大小不固定
// |x| x+1
// }

fn returns_closure() -> Box<dyn Fn(i32)->i32>{
Box::new(|x| x+1)
}

fn main(){

}

参考:

官方文档:

Rust语言圣经

Rust宏小册子

异步编程

参考:

Rust异步编程

Rust语言圣经

格式化输出

位置参数

1
2
3
4
5
6
fn main() {
println!("{0}, this is {1}. {1}, this is {0}", "Alice", "Bob");// => Alice, this is Bob. Bob, this is Alice
assert_eq!(format!("{1}{0}", 1, 2), "21");
assert_eq!(format!("{1}{}{0}{}", 1, 2), "2112");
println!("Success!")
}

具名参数

1
2
3
4
5
6
7
8
9
10
11
fn main() {
println!("{argument}", argument = "test"); // => "test"

assert_eq!(format!("{name}{}", 1, name = 2), "21");
assert_eq!(format!("{a} {c} {b}",a = "a", b = 'b', c = 3 ), "a 3 b");

// named argument must be placed after other arguments
println!("{abc} {0}", 2, abc = "def");

println!("Success!")
}

字符串对齐

默认情况下,通过空格来填充字符串

1
2
3
4
5
6
7
8
9
10
fn main() {
// the following two are padding with 5 spaces
println!("Hello {:5}!", "x"); // => "Hello x !"
println!("Hello {:1$}!", "x", 5); // => "Hello x !"

assert_eq!(format!("Hello {1:0$}!", 5, "x"), "Hello x !");
assert_eq!(format!("Hello {:width$}!", "x", width = 5), "Hello x !");

println!("Success!")
}

左对齐, 右对齐, 使用指定的字符填充

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
// left align
println!("Hello {:<5}!", "x"); // => Hello x !
// right align
assert_eq!(format!("Hello {:>5}!", "x"), "Hello x!");
// center align
assert_eq!(format!("Hello {:^5}!", "x"), "Hello x !");

// left align, pad with '&'
assert_eq!(format!("Hello {:&<5}!", "x"), "Hello x&&&&!");

println!("Success!")
}

我们还能使用 0 来填充数字

1
2
3
4
5
6
7
8
9
10
fn main() {
println!("Hello {:5}!", 5); // => Hello 5!
println!("Hello {:+}!", 5); // => Hello +5!
println!("Hello {:05}!", 5); // => Hello 00005!
println!("Hello {:05}!", -5); // => Hello -0005!

assert!(format!("{number:0>width$}", number=1, width=6) == "000001");

println!("Success!")
}

精度

浮点数精度

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let v = 3.1415926;

println!("{:.1$}", v, 4); // same as {:.4} => 3.1416

assert_eq!(format!("{:.2}", v), "3.14");
assert_eq!(format!("{:+.2}", v), "+3.14");
assert_eq!(format!("{:.0}", v), "3");

println!("Success!")
}

字符串长度

1
2
3
4
5
6
7
8
9
fn main() {
let s = "Hello, world!";

println!("{0:.5}", s); // => Hello

assert_eq!(format!("Hello {1:.0$}!", 3, "abcdefg"), "Hello abc!");

println!("Success!")
}

二进制,八进制,十六进制

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
assert_eq!(format!("{:#b}", 27), "0b11011");
assert_eq!(format!("{:#o}", 27), "0o33");
assert_eq!(format!("{:#x}", 27), "0x1b");
assert_eq!(format!("{:#X}", 27), "0x1B");

println!("{:x}!", 27); // hex with no prefix => 1b

println!("{:#010b}", 27); // pad binary with 0, width = 10, => 0b00011011

println!("Success!")
}

捕获环境中的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn get_person() -> String {
String::from("sunface")
}

fn get_format() -> (usize, usize) {
(4, 1)
}


fn main() {
let person = get_person();
println!("Hello, {person}!");

let (width, precision) = get_format();
let scores = [("sunface", 99.12), ("jack", 60.34)];
/* Make it print:
sunface: 99.1
jack: 60.3
*/
for (name, score) in scores {
println!("{name}: {score:width$.precision$}");
}
}

指数,指针地址,转义

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
// 指数
println!("{:2e}", 1000000000); // => 1e9
println!("{:2E}", 1000000000); // => 1E9

// 指针地址
let v= vec![1, 2, 3];
println!("{:p}", v.as_ptr()); // => 0x600002324050

// 转义
println!("Hello {{}}"); // => Hello {}
}

Other

错误处理:

unreachable!()

这是标记程序不应输入的路径的标准宏。如果程序进入这些路径,程序将 panicked 并返回”‘internal error: entered unreachable code’”错误消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
let level = 22;
let stage = match level {
1...5 => "beginner",
6...10 => "intermediate",
11...20 => "expert",
_ => unreachable!(),
};

println!("{}", stage);
}


// -------------- Compile time error --------------
thread 'main' panicked at 'internal error: entered unreachable code', src/main.rs:7:20

我们也可以为此设置自定义错误消息。

1
2
3
4
5
6
7
8
9
10
// --- with a custom message ---
_ => unreachable!("Custom message"),
// -------------- Compile time error --------------
thread 'main' panicked at 'internal error: entered unreachable code: Custom message', src/main.rs:7:20


// --- with debug data ---
_ => unreachable!("level is {}", level),
// -------------- Compile time error --------------
thread 'main' panicked at 'internal error: entered unreachable code: level is 22', src/main.rs:7:14

misconception corollaries

if T: 'static then T must be valid for the entire program

Misconception Corollaries

  • T: 'static should be read as T has a 'static lifetime”
  • &'static T and T: 'static are the same thing
  • if T: 'static then T must be immutable
  • if T: 'static then T can only be created at compile time

Most Rust beginners get introduced to the 'static lifetime for the first time in a code example that looks something like this:

1
2
3
fn main() {
let str_literal: &'static str = "str literal";
}

They get told that "str literal" is hardcoded into the compiled binary and is loaded into read-only memory at run-time so it’s immutable and valid for the entire program and that’s what makes it 'static. These concepts are further reinforced by the rules surrounding defining static variables using the static keyword.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Note: This example is purely for illustrative purposes.
// Never use `static mut`. It's a footgun. There are
// safe patterns for global mutable singletons in Rust but
// those are outside the scope of this article.

static BYTES: [u8; 3] = [1, 2, 3];
static mut MUT_BYTES: [u8; 3] = [1, 2, 3];

fn main() {
MUT_BYTES[0] = 99; // ❌ - mutating static is unsafe

unsafe {
MUT_BYTES[0] = 99;
assert_eq!(99, MUT_BYTES[0]);
}
}

Regarding static variables

  • they can only be created at compile-time
  • they should be immutable, mutating them is unsafe
  • they’re valid for the entire program

The 'static lifetime was probably named after the default lifetime of static variables, right? So it makes sense that the 'static lifetime has to follow all the same rules, right?

Well yes, but a type with a 'static lifetime is different from a type bounded by a 'static lifetime. The latter can be dynamically allocated at run-time, can be safely and freely mutated, can be dropped, and can live for arbitrary durations.

It’s important at this point to distinguish &'static T from T: 'static.

&'static T is an immutable reference to some T that can be safely held indefinitely long, including up until the end of the program. This is only possible if T itself is immutable and does not move after the reference was created. T does not need to be created at compile-time. It’s possible to generate random dynamically allocated data at run-time and return 'static references to it at the cost of leaking memory, e.g.

1
2
3
4
5
6
7
use rand;

// generate random 'static str refs at run-time
fn rand_str_generator() -> &'static str {
let rand_string = rand::random::<u64>().to_string();
Box::leak(rand_string.into_boxed_str())
}

T: 'static is some T that can be safely held indefinitely long, including up until the end of the program. T: 'static includes all &'static T however it also includes all owned types, like String, Vec, etc. The owner of some data is guaranteed that data will never get invalidated as long as the owner holds onto it, therefore the owner can safely hold onto the data indefinitely long, including up until the end of the program. T: 'static should be read as T is bounded by a 'static lifetime” not T has a 'static lifetime”. A program to help illustrate these concepts:

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
use rand;

fn drop_static<T: 'static>(t: T) {
std::mem::drop(t);
}

fn main() {
let mut strings: Vec<String> = Vec::new();
for _ in 0..10 {
if rand::random() {
// all the strings are randomly generated
// and dynamically allocated at run-time
let string = rand::random::<u64>().to_string();
strings.push(string);
}
}

// strings are owned types so they're bounded by 'static
for mut string in strings {
// all the strings are mutable
string.push_str("a mutation");
// all the strings are droppable
drop_static(string); // ✅
}

// all the strings have been invalidated before the end of the program
println!("I am the end of the program");
}

Key Takeaways

  • T: 'static should be read as T is bounded by a 'static lifetime”

  • if T: 'static then T can be a borrowed type with a 'static lifetime or an owned type

  • since

    1
    T: 'static

    includes owned types that means

    1
    T
    • can be dynamically allocated at run-time
    • does not have to be valid for the entire program
    • can be safely and freely mutated
    • can be dynamically dropped at run-time
    • can have lifetimes of different durations