学习资料

  • 英文版
  • 中文版(注意:中文版本有部分翻译使用的还是老版本 Rust,可以对照看一下)

Install

rustup 一个命令行工具,可以用来管理版本,并集成了相关工具

1
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
1
2
3
4
5
6
# 更新
rustup update
# 卸载
rustup self uninstall
# 本地文档
rustup doc

代码风格:

  • 文件名采用 “_” 连接
  • 函数名与 “{” 在同一行,且中间加一个空格
  • 代码采用 4 个空格的缩进方式
  • 使用 rustfmt 可以格式化代码

Cargo

Cargo 可以帮助管理 Rust 项目,同时还可以 build code、管理 libraries

1
2
3
4
5
6
7
8
cargo new project
cargo new --vcs=git # 使用 git 管理 默认

cargo build # 在 target/debug/project 可执行文件,默认以 debug 模式
--release # target/realse
cargo run # 编译并运行
cargo check # 编译,但是不生成可执行文件

Cargo.toml 是 Cargo 的配置文件

1
2
3
4
5
[package]  # 表示下面配置 package
name = "hello_cargo"
version = "0.1.0"
edition = "2021"
[dependencies]

Basic

Comments

1
2
// 单行注释
/// 文档注释,会生成 HTML 文档

Variables and Mutability

变量默认是不可变的,如果需要改变变量,需要显示声明其为 mut。这样做是为了安全性(防止修改不会改变的值,这样的 bug 很难排查),并且提供更简单的并发

变量会与一个 name 绑定,一旦绑定了,就不能修改了

常量与变量的区别

  • 不能使用 mut 修饰常量
  • 常量必须明确类型
  • 常量可以在任何 scope 中声明,包括全局声明
  • 常量只能赋值为常量表达式,不能是运行时计算的值

Shadowing

声明的变量可以和前面的变量一样,the first variable is shadowed by the second,直到第二个变量离开作用域。shadowing 可以改变第二个变量的类型,其实质是重新创建了一个变量

Data Types

Rust 是静态类型语言,必须在编译期间知道所有变量的类型。虽然编译器可以自动推断类型,但是在可能为多个类型的情况下,必须显示地指定类型。比如

1
let guess: u32 = "42".parse().expect("Not a number");

Scalar Types

  • Integers,默认为 i32。
    • 可以在数字字面量后加上类型,可以使用 “_” 作为分隔符,比如 0b1111_0000。
    • 溢出时,debug 下会 panic,release 下采用二进制补码 wrapping
Length Signed Unsigned
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
arch isize usize
  • Floating-Point numbers,f32(64) 表示单(双)进度浮点数,使用 IEE-754 标准
  • Booleans,使用 bool 声明类型,true/false
  • Characters,char 是最原生的字符类型,占 4 个字节,表示 Unicode 标量值

Compound Types

复合类型将多个值组合成一个类型

  • 元组 Tuple Type,let tup: (i32, f64, u8) = (500, 6.4, 1);
    • 一旦声明,长度不会改变 。
    • 可以使用 pattern matching 来解构单个值。也可以使用 ‘.n’ 来访问
    • 没有元素的 tuple 称为 unit,其值和类型都是 (),表示空值或空的返回类型。表达式隐式地返回 unit
  • 数组 Array Type,相同类型元素的集合, let arr = [1, 2, 3, 4]
    • 长度固定
    • 可以使用下标访问数组
1
2
3
4
5
6
7
8
9
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
let i = tup.0 // 500


let months = ["", "January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
let a = [3; 5] // 长度为 5,元素都为 3
let b: [i32, 5] = {1, 2, 3, 4, 5}

Functions

采用 snake 命名

函数参数称之为 parameters,调用函数传递的参数(实参)称之为 arguments。parameters 必须声明类型

函数体以一系列语句构成,可以以表达式结尾

  • 语句 Statements:执行一些操作而不返回值的指令,函数定义也是语句
  • 表达式 Expressions:计算并产生一个值,函数调用也是一个表达式,宏调用也是表达式, { ... } 也是表达式

函数的返回值等同于函数体最后一个表达式的值,可以使用 return 指定返回值,提前返回。如果没有返回值,默认返回最后的表达式

1
2
3
4
5
6
7
8
let y = {
let x = 3;
x + 1
};
println!("The value of expression is {}", {
let x = 3;
x + 1
}); // 4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
fn fib2(n: i32) -> i32 {
if n <= 2 {
return 1;
}
fib(n - 1) + fib(n - 2)
}
let fib3 = fib;

println!("Fibonacci 10 is {}", fib(10));
println!("Fibonacci 10 is {}", fib2(10));
println!("Fibonacci 10 is {}", fib3(10));
}

fn fib(n: i32) -> i32 {
if n <= 2 {
return 1;
}
// return fib(n-1) + fib(n-2);
fib(n - 1) + fib(n - 2)
}

Control Flow

if 是一个表达式 ,因为代码块的值取决于最后一个表达式的值,所以 if 的每个分支的可能的返回值都必须是相同类型

Rust 有三种循环, loop, while, forbreak 可以跳出循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
loop {   // 可以重试可能会失败的操作
// ...
}

while condition {
// ...
}

for e (1...10) {
// ...
}

for e in arr {
// ....
}

Ownership

Rust 使用所有权来管理内存

  • 每个值都有所有者 owner
  • 一个值在任一时刻只有一个所有者
  • 所有者离开作用域时,值会被丢弃(离开作用域时会调用 drop)

Move

String 由三部分构成,指向数据内存的指针、长度(当前使用了多少字节的内存)、容量(分配了多少内存)

1
2
3
let s1 = String::from("hello");
let s2 = s1;
// s1 失效

let s2 = s1 操作拷贝指针、长度、容量,同时还会使 s1 失效,即 s1 不能使用了,这个操作称为移动(move),即 s1 被移动到了 s2 中(这样做是防止多次释放同一段内存,因为多个引用离开作用域后会多次释放)

Clone

可以使用 let s2 = s1.clone() 进行深拷贝,会复制堆上的数据

Copy

编译时就知道大小的类型存储在栈上,可以直接 copy

1
2
3
let x = 5; // 将 5 绑定到 x 上
let y = x; // copy x 的值,绑定到 y 上
// x, y 仍然可以使用

Copy trait 注解可以用在类似整型这样存储在栈上的类型。如果一个类型实现了 Copy trait,那么一个变量 赋值给其他变量后仍然可以使用

Rust 不允许自身或其任何部分实现了 Drop trait 的类型使用 Copy trait

Ownership and Functions

传递参数时,会 move 或 copy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
// literals is immutable
// String allocat at heap
let mut s: String = String::from("Hello");
takes_ownership(s); // s2 move into the function
// s2 can not be used any more
// println!("{}", s2); // error

let x = 5;
makes_copy(x); // x move into the function
// x can be used

println!("{x}");
}

fn makes_copy(_n: i32) {}

fn takes_ownership(str: String) {
println!("{}", str);
}

返回值与作用域

返回值也可以转移所有权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn func_return() {
let s1 = gives_ownership(); // move its return value into s1

let s2 = String::from("hello world");
let s3 = takes_and_gives_back(s2); // s2 is moved into the function; return value is moved into s3
println!("{}, {}", s1, s3)
}

fn gives_ownership() -> String {
let str = String::from("hello");
str
}

fn takes_and_gives_back(str: String) -> String {
str // return str, and move out to the calling function
}

References and Borrowing

为了能够将变量传递给函数后仍能够继续使用,可以传递一个引用(reference),允许函数访问存储在该地址的数据而不获取其 ownership。创建引用的行为称为借用(Borrowing)

  • 引用必须总是有效的
  • 在任意时间,要么只能有一个可变引用,要么能有多个不可变引用
  • 引用的作用域从声明开始,到使用的最后位置结束

不能修改借用的数据。要想修改借用的值,可以使用可变引用,即将函数的参数声明为可变的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let mut s1 = String::from("hello");
let len = calculate_length(&s1);
println!("{s1}, {len}"); // hello, 5

change(&mut s1);
println!("{s1}, {len}"); // hello, world!, 5
}

fn calculate_length(s: &String) -> usize {
// s.push_str(", world!"); // error
s.len()
}

fn change(s: &mut String) -> usize {
s.push_str(", world!");
s.len()
}

如果创建了一个对某个变量的可变引用,那么就不能有对 该变量其他的引用(防止同一时间对统一数据存在多个可变引用,避免数据竞争)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn ref() {
let mut s1 = String::from("hello");
change(&mut s1); // 引用作用域结束
change(&mut s1); // ok
}

fn ref2() {
let mut s1 = String::from("hello");
let r1 = &s1;
let r2 = &s1; // ok
// let r3 = &mut s1; // error
}

fn ref3() {
let mut s1 = String::from("hello");
let r1 = &mut s1;
// let r2 = &mut s2; // error
// let r3 = &s1; // error
println!("{}", r1);
let r4 = &mut s1; // ok,因为 r1 没有使用了
}

Slice Type

Slice 允许引用集合中一段连续的元素序列,而不用引用整个集合

1
2
3
4
5
let s = String::from("hello");

let s1 = &s[1:s.len()];
let s2 = &s[:2];
let s3 = &s[:];

Struct and Method

Struct

1
2
3
4
5
6
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}

Rust 不允许将某个字段标记为可变的,只能将实例声明为可变的。如果变量名称与结构体字段名称相同,可以省略字段名称

1
2
3
4
5
6
7
8
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1
}
}

结构体更新语法(Struct Update Syntax)可以快速根据一个结构体创建一个新结构体。.. 语法制定了剩余为显示设置的字段与给定实例对应字段相同(将给定实例的字段 move 到了新实例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let mut user1 = build_user(String::from("rust@email.com"), String::from("rust"));
let user2 = User {
email: String::from("abc@gmail.com"),
username: String::from("abc"),
..user1
};
println!("{}, {}", user1.username, user1.active);

let user3 = User {
email: String::from("abc@gmail.com"),
..user1
};
println!("{}", user1.email);
// println!("{}", user1.username); // error

元组结构体: 没有具体的字段名,只有字段类型。

1
2
3
4
struct Point(i32, i32, i32);

let p = Point(1, 2, 4);
println!("Point: x = {}, y = {}, y = {}", p.0, p.1, p.2);

类单元结构体: 没有任何字段的结构体,称为类单元结构体 (unit-like structs)。常用在想要在某个类型上实现 trait,但不需要再类型中存储数据的时候发挥作用的时候

Method

方法在结构体(或枚举或trait对象)的上下文中被定义,并且第一个参数总是 selfselfself: &Self 的简写,在 impl 块中,Self 是 impl 块的类型的别名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn main() {
let rect1 = Rectangle::new(30, 40);
println!("{}", rect1.area());
}

#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

impl Rectangle {
fn new(width: u32, height: u32) -> Self { // not a method
Self {
width,
height,
}
}
fn area(&self) -> u32 { // method
self.width * self.height
}
}

关联函数: 所有在 impl 块中定义的函数称为关联函数,可以定义不以 self 为第一参数的关联函数(这个函数不是方法,因为不作用于结构体实例)。常用作返回创建实例

Enum and pattern matching

Enum

1
2
3
4
enum IpAddrKind {
V4,
V6,
}

枚举成员可以关联数据

1
2
3
4
5
6
7
8
enum Message {
Quit,
Move {x: i32, y: i32},
Write(String), // 关联一个 String 类型的数据
ChangeColor(i32, i32, i32),
}

let msg = Message::Write(String::from("hello"));

枚举还可以定义方法

1
2
3
4
5
impl Message {
fn call(&self) {
// ......
}
}

Option 是标准库定义的一个枚举,表示一个值要么有值,要么没值 (a value could be something or it could be nothing)(比如从空 list 取值 get nothing,从非空 list 取可以得到 value, 相当于 null 和非 null)

当使用 Option 时才需要考虑是否没值,其他情况都确保有值(反过来说,只要一个值不是 Option<T> 类型就不会为空)。即当一个值可能为空时,需要放到 Option<T> 中,使用这个值是,必须明确地处理值为空的情况。

这样做是为了减少空值的泛滥,以增加代码的安全性

match

match 控制流运算符允许我们将一个值与一系列的模式相比较,并更具相匹配的模式执行相应的代码。

模式可由字面量、变量名、通配符和其他东西构成。编译器检查会确保处理了所有可能的情况

1
2
3
4
match 表达式 {
模式1 => 表达式,
模式2 => 表达式,
}

they can bind to the parts of the values that match the pattern

匹配必须是可穷举的,分支必须覆盖所有可能性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}", state);
25
},
_other => () // 通配模式,表示其他的所有可能
}
}

Containner

Vector

  • Vec::new
  • Vec::push
  • Vec::get
  • Vec[index]
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
fn main() {
// 新建 Vector
let _v: Vec<i32> = Vec::new();
let mut v2 = vec![1, 2, 3];
let mut v3 = Vec::new(); // can bee inferred by line 5
// 更新
v2.push(4);
v2.push(5);
v3.push(1);

let third: &i32 = &v2[2];
// v2.push(6); // error. vector may copy old data to new memory
println!("{}", third);

v2.push(6); // ok

let third: Option<&i32> = v2.get(2);
// v2.push(7); // error
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}

v2.push(7); // ok

for i in &v2 {
// can not delete here
println!("{i}")
}
}

String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn new_string() {
let s = String::new();

let data = "initial contents";

let s2 = data.to_string();

let s3 = String::from("new strign");

println!("{}, {}, {}", s, s2, s3);
}

fn update_string() {
let mut s = String::from("foo");
println!("original string {}", s);
s.push(' ');
println!("after append ' ' {}", s);
s.push_str("bar");
println!("after append bar {}", s);
}

Error Handling

Unrecoverable Errors

在实践中有两种方法造成 panic:

  • 执行会造成代码 panic 的操作(比如访问超过数组结尾的内容)
  • 显式调用 panic! 宏

通常情况下这些 panic 会打印出一个错误信息,展开并清理栈数据,然后退出。通过一个环境变量,也可以让 Rust 在 panic 发生时打印调用堆栈(call stack)以便于定位 panic 的原因。

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

v[99];
}

设置 RUST_BACKTRACE=1 环境变量可以打印出 panic! 所生成的 backtrace 信息

Recoverable Errors

大部分错误并没有严重到需要程序完全停止执行。Result 可以返回成功或者失败的值

1
2
3
4
enum Result<T, E> {
Ok(T),
Err(E),
}

使用 unwrap 可以简化处理过程,如果返回 Ok,那么得到 Ok 中的值,如果返回 Err 会调用 panic!

使用 expect 可以在返回 Err 的时候指定一个错误信息

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
fn _open_file_fail() {
// Ok => value in Ok
// Err => panic!
let _f = File::open("hello.txt").unwrap();

let _f2 = File::open("helo.txt")
.expect("hello.txt should be included in this project");
}

fn _open_file() {
let file_result = File::open("hello.txt");
let mut f = match file_result {
Ok(file) => file,
Err(err) => match err.kind() {
std::io::ErrorKind::NotFound => match File::create("hello.txt") {
Ok(file) => file,
Err(e) => panic!("Problem creating the file: {:?}", e)
},
other_err => {
panic!("Problem opening the file: {:?}", other_err)
}
}
};
let w_result = f.write("hello".as_bytes());
if let Err(err) = w_result {
panic!("Problem writting the file {:?}", err);
}
}

传播错误

可以将错误传播给调用者,因为调用者可能更清楚如何处理错误

1
2
3
4
5
6
7
8
9
10
11
12
// ? return a value from function.
fn _propagating_errors_simplify() -> Result<String, std::io::Error> {

// Ok ==> the value in Ok
// Err ==> return the error(convert to the type of return type)
let mut f = File::open("hello.txt")?;

let mut content = String::new();

f.read_to_string(&mut content)?;
Ok(content)
}

? 运算符被定义为从函数中提早返回一个值,所以? 运算符只能被用于返回值与 ? 作用的值相兼容的函数

Generic

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
struct Point<T, U> {
x: T,
y: U,
}

impl<T,U> Point<T, U> {
fn point_at(x: T, y: U) -> Point<T, U> {
Point {
x, y
}
}
fn move_to<T2,U2>(&self, x: T2, y: U2) -> Point<T2, U2> {
Point {
x, y
}
}
fn move_y(&mut self, y: U) {
self.y = y;
}
}

impl<U> Point<i32, U> {
fn move_x(&mut self, x: i32) {
self.x += x;
}
}

Rust 通过在编译时进行泛型代码的 单态化(monomorphization)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。编译器寻找所有泛型代码被调用的位置并使用泛型代码针对具体类型生成代码。

Trait

trait 定义了某个特定类型拥有可能与其他类型共享的功能。可以通过 trait 以一种抽象的方式定义共享的行为。可以使用 trait bounds 指定泛型是任何拥有特定行为的类型。

可以使用 impl xxx_trait for xxx 来为某个类型实现一个 trait

可以为 trait 中的某些或全部方法提供默认的行为。这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pub trait Summary {
fn summarize(&self) -> String;

fn summarize_more(&self) -> String {
String::from("Read more from...")
}
}

pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}

impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}

在定义函数的时候,可以指定接收实现了某个 trait 的参数。rust 提供了 trait bound 语法糖,在参数很多的情况下可以简化代码。在类型很多的情况下,也可以使用 where 来简化 trait bound

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn notify(item: &(impl Summary + Display)) {
println!("{}", item.summarize_more());
println!("1 new tweet: {}", item.summarize());
}

// trait bound
fn notify2<T: Summary + Display>(item: &T) {
println!("{}", item.summarize_more());
println!("1 new tweet: {}", item.summarize());
}

fn notify3<T>(item: &T) -> i32
where
T: Display + Summary{

println!("{}", item.summarize_more());
println!("1 new tweet: {}", item.summarize());
0
}

通过使用带有 trait bound 的泛型参数的 impl 块,可以有条件地只为那些实现了特定 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::fmt::Display;

struct Pair<T> {
x: T,
y: T,
}

impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}

impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}

也可以对任何实现了特定 trait 的类型有条件地实现 trait。对任何满足特定 trait bound 的类型实现 trait 被称为 blanket implementations,他们被广泛的用于 Rust 标准库中。例如,标准库为任何实现了 Display trait 的类型实现了 ToString trait。这个 impl 块看起来像这样:

1
2
3
impl<T: Display> ToString for T {
// --snip--
}

Lifetime

每个引用都有其生命周期,即引用保持有效的作用域。大部分时候生命周期是可以推断的,但是也有不能推断的时候,这时需要使用生命周期注解来注明关系,以确保运行时实际使用的引用是有效的

生命周期注解并不改变任何生命周期的长短,他们描述了多个引用生命周期的相互关系。通过指定生命周期可以帮助编译器进行 borrow checker(通过比较作用域来确保所有的借用都是有效的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {}", result);
}
// println!("The longest string is {}", result); // error
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}

在结构体中定义生命周期注解

如果一个字段存放的是一个引用,必须声明生命周期参数,以便在结构体定义中使用生命周期参数。(这个注解意味着该结构体实例不能比其内部字段中的引用存在得更久)

1
2
3
4
5
6
7
8
9
10
11
struct ImportantExcerpt<'a> {
part: &'a str
}
fn main() {
let novel = String::from("Call me Ishmael. Some years age...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
println!("{}", i.part);
}

生命周期省略规则:

  • 编译器为每一个引用参数都分配一个生命周期参数
  • 如果只有一个输入生命周期参数,那么它被赋予给所有输出生命周期参数
  • 如果方法有多个输入声明周期参数,并且其中一个参数是 &self&mut self,说明这是个对象的方法,那么所有输出生命周期参数被赋予 self 的生命周期

为带有生命周期结构体实现方法时,给

1
2
3
4
5
6
7
8
9
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 { // 第一条生命周期规则
3
}
fn announce_and_return_part(&self, announcement: &str) -> &str { // 第三条规则
println!("Attention please: {}", announcement);
self.part
}
}

泛型 + trait bounds 标注生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn _longest_with_an_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}

静态生命周期

'static 静态生命周期在程序的生命周期相同,所有字符串字面值都拥有 'static 生命周期

1
let s: &'static str = "I have a static lifetime.";

Closures

Rust 的 闭包(closures)可以保存在一个变量中或作为参数传递给其他函数的匿名函数。可以在一个地方创建闭包,然后在不同的上下文中执行闭包运算。不同于函数,闭包允许捕获被定义时所在作用域中的值

闭包有三种方式捕获环境,对应了三种 Fn trait

  • 所有权,参数列表前使用 move 关键字。对应 FnOnce,该类型的闭包会拿走被捕获变量的所有权,该闭包只能运行一次
  • 不可变引用,对应于 Fn
  • 可变引用,对应于 FnMut

一个闭包并不仅仅实现某一种 Fn 特征,规则如下:

  • 所有的闭包都自动实现了 FnOnce 特征,因此任何一个闭包都至少可以被调用一次
  • 没有移出所捕获变量的所有权的闭包自动实现了 FnMut 特征
  • 不需要对捕获变量进行改变的闭包自动实现了 Fn 特征
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
// 捕获了一个不可变实例到 self
fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
user_preference.unwrap_or_else(|| self.most_stocked())
}

fn capturing_reference() {
let mut list = vec![1, 2, 3];
println!("Before defining closure: {:?}", list);

let only_borrows = || println!("From closure: {:?}", list); // 捕获不可变引用
// let mut borrows_mutable = || list.push(7); // error

println!("Before calling closure: {:?}", list); // 1, 2, 3
only_borrows();
println!("After calling closure: {:?}", list); // 1, 2, 3

let mut borrows_mutable = || list.push(7); // 捕获可变引用
// println!("After calling closure: {:?}", list); // error, because list mut reference hold by closure
borrows_mutable();
println!("After calling closure: {:?}", list); // 1, 2, 3, 7


// move
thread::spawn(move || println!("From thread: {:?}", list))
.join()
.unwrap();
// println!("After calling closure: {:?}", list); // error. ownership has been moved
}

闭包通常很短,并只关联于小范围的上下文而非任意情境。在这些有限制的上下文中,编译器能可靠地推断参数和返回值的类型,类似于它是如何能够推断大部分变量的类型一样。编译器会为闭包定义中的每个参数和返回值推断一个具体类型。但是如果尝试调用闭包两次,第一次使用 String 类型作为参数而第二次使用 u32,则会得到一个错误

1
2
3
4
5
let closure_v1 = |x|             { x };
let closure_v2 = |x| x ;

closure_v1(1);
// closure_v1("abc"); // error

一个闭包实现了哪种 Fn 特征取决于该闭包如何使用被捕获的变量,而不是取决于闭包如何捕获它们

Iterator

迭代器模式允许对一个序列的item进行某些处理。迭代器(iterator)负责遍历序列中的每一项和决定序列何时结束的逻辑。

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

let v1_iter = v1.iter(); // 生成一个不可变引用的迭代器,使用 into_iter() 可以获取所有权

for val in v1_iter { // for 循环会获取 v1_iter 所有权,并使其可变
println!("Got: {val}");
}

let v1_iter2 = v1.iter();
let total: i32 = v1_iter2.sum(); // 调用 sum 后,v1_iter 不能够再使用了,因为 sum 会获取所有权
assert_eq!(total, 6);
}

迭代器都实现了 Iterator 这个 trait,要实现迭代器需要实现 Iterator trait 的 next 方法,用于返回迭代器中的一个 item

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn iter_adaptors() {
let v1: Vec<i32> = vec![1, 2, 3];

let it = v1.iter().map(|x| x + 1);

for v in it { // 2, 3, 4
println!("{v}");
}

for v in v1.iter() { // 1, 2, 3
println!(" {v}");
}

let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);

let v3: Vec<_> = v1.into_iter().filter(|x| *x > 2).collect();
assert_eq!(v3, vec![3]);
}

Smart Pointer

Box

Box<T> 类型是一个智能指针,因为它实现了 Deref trait,它允许 Box 值被当作引用对待。Box 可以让元素存储在堆上,而不是栈上,可以用于以下场景:

  • 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候
  • 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候
  • 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候
1
2
3
4
5
6
7
8
9
10
11
use crate::List::{Cons, Nil};

enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
let y = Box::new(x);
assert_eq!(5, *y);
let _list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}

Deref trait

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct MyBox<T>(T);

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

}

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

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

实现了 Deref trait 的类型在解引用时会调用 Derefderef() 方法,Rust 会分析这些类型,并使用任意多次(解引用的值可能也实现了 Deref trait),这些解析都是在编译时发生的。

1
2
3
4
5
6
7
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m); // &m -> &String -> &str
}

实现 DerefMut 可以重载可变引用的 * 运算符

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

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

Drop trait

Drop trait 其允许我们在值要离开作用域时执行一些代码,这些变量会以被创建的相反顺序被丢弃。实现 Drop trait 需要实现 drop() 方法。

1
2
3
4
5
impl<T> Drop for MyBox<T> {
fn drop(&mut self) {
println!("Drop MyBox...");
}
}

注意:Rust 不允许显示调用 drop() 方法,因为会造成 double free。使用 std::mem::drop 可以提早丢弃一个值,比如提前释放锁

Rc

Rc<T> 类型允许程序在多个部分之间只读地 共享数据,它内部有一个引用计数器,每次调用 Rc::cloneRc 中数据的引用计数都会增加,除非引用计数为 0,否则不会被清理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use crate::List::{Cons, Nil};
use std::rc::Rc;

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

fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!("count after creating a {}", Rc::strong_count(&a)); // 1
let _b = Cons(3, Rc::clone(&a));
println!("count after creating b {}", Rc::strong_count(&a)); // 2
{
let _c = Cons(4, Rc::clone(&a));
println!("count after creating c {}", Rc::strong_count(&a)); // 3
}
println!("count after c goes out of scope {}", Rc::strong_count(&a)); // 2
} // reference count = 0

RefCell

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

RefCell<T> 的不可变性作用于运行时(也就是说编译器不检查,将检查交给用户程序员),如果违反这些规则,会 panic 并退出。

注意: Rc<T>, RefCell<T>都只能用于单线程 的场景

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
use std::cell::RefCell;

trait Sender {
fn send(&self, mail: &str);
}

struct MailServer {
boxes: RefCell<Vec<String>>,
}

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

impl Sender for MailServer {
fn send(&self, mail: &str) {
self.boxes.borrow_mut().push(String::from(mail));
println!("send msg: {}", mail);
}
}

fn main() {
let server = MailServer::new();
server.send("abc");

assert_eq!(server.boxes.borrow().len(), 1);
}
  • borrow() 返回 Ref<T> 类型的智能指针,每次调用将活动的不可变用计数加一,离开作用域时不可变借用计数减一
  • borrow_mut() 返回 RefMut<T> 类型的智能指针
  • 任何时候都只允许有多个不可变借用或一个可变借用

RefCell<T> 的一个常见用法是与 Rc<T> 结合。如果有一个存储了 RefCell<T>Rc<T> 就可以得到有多个所有者并且可以修改的值了。比如可以拥有一个表面上不可变的 List,但是可以使用 RefCell<T> 提供的内部可变性方法在需要时修改数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}

fn ref_cell_ref() {
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; // update

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

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

reference cycle problem

循环引用是引用计数法的老问题了(ABA 问题)

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
#[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)); // 1
// │a next item = Some(RefCell { value: Nil })
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)); // 2
println!("b initial rc count = {}", Rc::strong_count(&b)); // 1
// │b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
println!("b next item = {:?}", b.tail());

if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b); // refrence cycle
}
println!("b rc count after changing a = {}", Rc::strong_count(&b)); // 2
println!("a rc count after changing a = {}", Rc::strong_count(&a)); // 2
// println!("a next item = {:?}", a.tail()); stack overflow
}

TODO:

Concurrency

使用 thread::spawn() 传递一个闭包可以创建一个新线程,可以使用 move 关键字将某个变量的所有权转移到线程内部

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

handle.join().unwrap();
}

Message Passing

Rust 标准库提供了一个 channel,来使线程通过发送消息来互相通信。

这个思想来源于 Go “不要通过共享内存来通信;而是通过通讯来共享内存”

可以用 mpsc::channel 创建一个 channel,返回一个元组 (transmitter, receiver),任何一端出错(发送或者接收),另一端都会返回一个错误。(mpsc 是多个生产者,单个消费者的缩写(multiple producer, single consumer))

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
fn channel_demo() {
let (tx, rx) = mpsc::channel();

let tx1 = tx.clone();
thread::spawn(move || {
let vals = vec![String::from("hello"), String::from("world")];
// let val = String::from("hello");
for val in vals {
// Result<T, E>
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});

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

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

Share State

mutex 在任一时刻只允许一个线程访问某些数据,Mutex::lock() 返回一个智能指针 MutexGuard,实现了 DerefDrop trait,在离开作用域时会自动释放锁

现在的问题:

  • 需要在线程之间传递锁
  • 闭包会捕获 mutex 的所有权
  • Rc, RefCell 均不能用于多线程

在多线程中可以使用 Arc<T> 原子引用计数。

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

Send and Sync

实现了 Send trait 的类型值可以在线程间传递,几乎所有的 Rust 类型都是 Send(也有些例外,比如Rc<T>)。任何完全由 Send 组成的类型也会被自动标记为 Send(也就是说有一个类型不是 Send 就不是 Send)

实现了 Sync trait 的类型值可以安全地在多个线程中拥有其引用,即对于任意类型 T, 如果&TSend 的,那么 TSync 的。Rust 的基本类型是 Sync 的,然全有 Sync 组成的类型也是 Sync