这篇博客我将详细介绍 Rust 这门语言。从创建本博客至此刻大更新(2024-5),我的 Rust 开发经验已是两年有余了——我认为它是互联网厂商泛工程开发中最合适的语言。根据我的体验,它至少拥有以下优点:

  • 好用的语言特性。可提高安全性的强语言类型、提高开发效率的自动类型推导、成熟的流操作等等。
  • 舒适的开发环境。VSCode+RA,rust-fmt 和 rust-clippy 等工具让代码开发成为一种享受。
  • 良好的异步引擎。不用基于麻烦的回调或 Webflux,像同步代码一样写异步!
  • 高效。据传 Rust 和 C 的代码速度比是 1.07:1。这个特性让 Rust 能用在量化交易系统和链上系统。
  • 安全。这是 Rust 被吹得最多的一点,诸如“内存绝对安全“、”只要过编译就罕有 bug”。我对这个特性的认知比较保守:至少,Rust 拥有一个强大的编译器,借用检查、生命周期等机制能有效降低内存泄漏的可能性。
  • 优秀的包管理系统和良好的生态。Cargo 不能说是最强的包管理系统,但肯定是好用者之一。

Rust 的缺点不多,体感最强烈的是编译速度太慢。一般我敲入编译或测试的命令时就会去茶水间打水

环境安装和资料推荐

Rust 的编译工具是 rustc(2024-04-09 版本 1.77.2),由工具 rustup 进行管理(2024-03-08 版本 1.27.0)。

rustup 按 官网 指示进行下载安装(注意 Windows 环境下没必要用默认的方式安装庞大的 Visual Studio)。安装完成后可以在 ~/.cargo/bin 看到 rustup、rustc、cargo、rustfmt、rust-gdb 等一系列工具。

Rust 依赖 C 的编译环境,所以针对不同的操作系统和 C 语言依赖有 不同的版本。用 rustup toolchain 来管理可能存在的多个版本。Linux 下自然依赖 gnu,而 Windows 推荐依赖 msvc。

Rust 开发环境推荐 VSCode + Rust Analyzer 插件或 Intellij-Rust,我用的是前者。

下面推荐一些 Rust 的教程和资料:

网址 描述
https://crates.io/ Rust 官方的包平台
https://rust.godbolt.org/ 将 rust 代码翻译为汇编的工具
The Rust Progamming LanguageRust 程序设计语言 Rust 官方教程,有中文翻译版
Rust 语言圣经(Rust Course) 中文社区教程,突出贡献者居然是我同事
Rust by Example 带有大量代码示例的教程
Rust Async Book Rust 中的异步编程
The Rustonomicon Rust 前沿特性教程,不推荐初学者

常见的代码测试、格式化等命令参考:

1
2
3
4
5
6
7
8
9
10
11
cargo test [options] [testname] [-- test-options]
cargo test -- --nocapture # print normal messages like logs
cargo test functionname # run all tests filter by name
cargo test --test [filename] -- [functionname] # only run the specific test
cargo test --all-features # activate all available features

cargo clippy
cargo clippy --fix --allow-dirty --allow-staged # check include modified, auto fix
cargo sort [path] # sort toml
cargo +nightly fmt # format code
cargo +nightly udeps # remove unused package

附:何柱指导的一种 Windows 下防止 Rust 编译时候爆内存的方法:

1
2
3
4
5
6
cargo install -f cargo-binutils
rustup component add llvm-tools-preview

# in ~/.cargo/config
[target.x86_64-pc-windows-msvc]
linker = "rust-lld.exe"

包管理

一个 Rust 项目里通常含有以下几项:

路径 功能
Cargo.toml 声明本项目直接依赖的三方库版本,以及一些构建配置。
Cargo.lock 由编译器自动生成,往往很大,记录了直接依赖和间接依赖的三方库信息。
src/* 源码,包括程序入口 main.rs,提供给外界用的 lib.rs 等。
tests/* 测试文件。一般用于集成测试,小型的功能测试可能直接附于源码内。
examples/* 一些典型的使用示例。
bench/* 用于测速。

包(package)是提供一系列功能的若干个 crate。crate 分为库(library crate)和二进制项(binary crate)。src/main.rs 是默认的二进制项,src/lib.rs 是默认的库。src/bin/ 下可添加任意多的二进制项。

模块(mod)可以对一个 crate 里的代码进行分组和嵌套。模块、函数、结构体、结构体成员等默认私有(子模块可以调用父模块内容,父模块无法调用子模块内容,模块之间可以调用),枚举成员默认公有。可以用 pub 关键词修饰公有,修饰后上一层父模块可调用子模块内容(如果想在更上层调用,需要继续添加 pub 关键词)。

调用模块树里的内容时,可以使用绝对路径(crate::...)或相对路径(用 self:: 表示当前模块,也可以直接以同级的某个模块名开始;用 super:: 表示父模块)。

use 导入(创建路径的软链接),用 as 重命名,用 .* 引入全部公有项。一般函数的导入会保留最后一层模块,而结构体直接引用到具体内容。 use 前也可以用 pub 修饰,称为重导出(re-exporting)。

可以用嵌套路径来消除大量的 use 行。如果涉及和子路径合并,可以使用 self 关键词。

1
2
3
4
5
6
use std::cmp::Ordering;
use std::io;
use std::{cmp::Ordering, io};
use std::io;
use std::io::Write;
use std::io::{self, Write};

cargo tree 可以查看已使用的 crate 的树型依赖关系。

基本语法

Rust 支持类型推理。用 let 指定不可变变量,let mut 指定可变变量。

  • const 修饰(全局)常量。需指明类型,没有固定的内存地址,值无法改变(不能加 mut 前缀)。
  • static 修饰(全局)静态变量。需指明类型,具有固定的内存地址。可以加 mut 前缀表示可变的静态变量,但访问和修改可变静态变量是非安全的(只能通过 unsafe 块),因为有多线程下的数据竞争问题。
1
2
3
4
5
6
7
8
9
let x = 5;
let mut y = 5u32;
y = 6_222;
let z = {
let x = 0o77;
x + 1
};
static mut COUNTS: u32 = 0;
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

Rust 支持变量遮蔽(shadow),遮蔽前后类型可以不同,作用域结束后遮蔽自动结束。

1
2
3
4
5
6
let x = 1;
{
let x = x * 2;
println!("The value of x in the inner scope is: {}", x); // 2
}
println!("The value of x is: {}", x); // 1

函数参数和返回值(如果有)必须指定类型。函数定义的位置不作要求。如果没有返回值,默认返回 ()

if 判断的表达式必须返回布尔类型,表达式无须强制套小括号。if 表达式返回的结果必须同类型。

1
let number = if condition { 5 } else { 6 };

loopwhilefor 来表示循环。支持 continuebreakbreak 后可添加返回值来构建表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
while number != 0 {
println!("{}!", number);
number -= 1;
}
for element in (1..4).rev() {
println!("the value is: {}", element);
}

基本数据类型

Rust 是一种强类型(strongly typed)和静态类型(statically typed)语言。

Rust 的原生类型(primitives type)分为标量类型(scalar type)和复合类型(compound type)。

标量类型主要有以下四种:

  • 整型:i8, i16, i32, i64, i128, isize; u8, u16, u32, u64, u128, usize。字面量未声明默认是 i32。整型溢出(integer overflow):debug 模式下报 panic 错误,release 模式下用补码包裹。
  • 浮点类型:f32, f64。字面量未声明默认是 f64。与 Nan 交互都会变成 NaNNaN 参与比较会 panic。
  • 布尔类型:bool。大小为 1 字节。
  • 字符类型:char。大小为 4 字节,Unicode 编码,用单引号表示。用 b'x' 表示字符 x 的 ASCII 值。

序列range)用来生成连续的数值,默认前闭后开。如 1..51..=5。常用于 for...in...

范围表达式 类型 范围
s..t std::ops::Range sx<ts \le x < t
s.. std::ops::RangeFrom sxs \le x
..t std::ops::RangeTo x<tx < t
.. std::ops::RangeFull
s..=t std::ops::RangeInclusive sxts \le x \le t
..=t syd::ops::RangeToInclusive xtx \le t

元组tuple)可以将多个不同类型的值复合在一起,可以用模式匹配或 . 配合下标来索引。

单元类型unit type)是一种特殊的元组,只有一个值 (),称为单元值(unit value),不占用内存。如果表达式或函数不返回任何其他值,就隐式地返回单元值。如果 Map 不关心键值,也可以用 () 占位。

1
2
3
4
5
let tuple = (500, 6.4, 1);
let (x, y, z) = tup;
let six_point_four = tuple.1;
struct Point(i32, i32, i32);
let origin = Point(0, 0, 0);

数组array)只能组合同类型的元素,长度是不可变的常量。数组内容会被分配至栈空间中。

数组切片slice)可以部分或全部地引用数组,前闭后开。用 & 获得指向的地址,不实际拥有数据。切片相比原始数组的好处是长度确定,其本身一个胖指针(fat pointer),包含了地址和长度两个量。

1
2
3
4
5
let months = ["January", "February", "March"];
let a: [i32; 5] = [1, 2, 3, 4, 5]
let a: [3; 5]; // [3, 3, 3, 3, 3]
let first = a[0];
let slice: &[i32] = &a[1..3];

never 类型never type)是一个没有值的类型,表示永远不会完成计算的结果,用 ! 标记。panic! 、一直循环的 loop、match 分支里的 break/return 会被视为 never 类型。never 类型可以被转化成任何类型。

Rust 不提供原生类型之间的隐式类型转换。可以用 as 进行显式转换,原生类型中只支持四种转换:整数和浮点数之间互相转换,枚举类型转成整型,布尔类型或字符类型转成整型,u8 转成字符类型。

结构体和函数

结构体用 struct 定义,用 . 索引内容,包括具名结构体、元组(成员匿名)和单元结构体(没有任何成员)。具名结构体用花括号定义成员字段(单元结构体可以省略花括号)。结构体必须整个可变或整个不可变。

  • 具名结构体在初始化时,若赋值变量和待赋值的成员字段同名,可以简化赋值。
  • 可以用类似初始化的方式将具名结构体里的字段移动出来。这种方式要求该结构体未定义 drop。
  • 把结构体里某个字段的所有权转移出去后,就无法访问该字段,但仍能正常访问其他字段。
  • .. 置于初始化的最后部分,来根据另一个实例创建新实例。
  • impl 块来定义结构体的专属方法。一个结构体可以定义多个 impl 块。用 impl 块定义的函数被称为关联函数(associated functions),用 Selfself 指代关联结构体和其实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct User {
username: String,
sign_in_count: u64,
}
let user1 = User {
username: String::from("someusername123"),
sign_in_count: 1,
};
let user2 = User {
sign_in_count: 2,
..user1
};
let User {sign_in_count, ..} = user2;
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}

函数(function)用关键词 fn 声明,函数体是个块表达式。函数每个输入参数和返回值都要声明类型,若不声明则默认返回 ()。同一模块下的函数名称不能相同(即使参数不同)。

方法(method)是以 self(包括 &selfmut self 等) 作为第一个参数的关联函数。结构体实例调用结构体方法时,会根据方法的定义进行自动引用和解引用(automatic referencing and dereferencing)。

常量函数(constant function)用 const fn 标识,定义在常量上下文(const context)中,编译阶段就能执行:

  1. 常量函数计算出的结果必须是确定的,即不能将随机数生成器编写为常量函数。
  2. 常量函数里不能包含 for 循环(loop 和 while 可以),也不能执行浮点数运算。

函数、结构体和结构体方法都支持泛型,编译时进行单态化(monomorphization),即泛型不影响效率。

1
2
3
4
5
6
7
8
9
10
11
12
struct Point<T, U> {
x: T,
y: U,
}
impl<T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point {
x: self.x,
y: other.y,
}
}
}

枚举类型

枚举类型用 enum 定义,每个成员可以选择绑定一个数据类型。可以像结构体一样用 impl 定义方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
impl Message {
fn call(&self) {
...
}
}
let m = Message::Write(String::from("hello"));
m.call();

枚举的每一个成员都有自己的判别式(discriminant)。

  1. 如果枚举类型的每个成员都没有绑定数据类型,可以通过 Enum::Foo as isize 获取判别式的数值。
  2. 标准库提供了 std::mem::discriminant() 方法来获取判别式的抽象值,其只能进行相等比较和哈希。

repr(T) 指定判别式在存储时实际的整型。当显式指定 repr 或每个成员都没有绑定数据类型时:成员的判别式数值可以手动指定,未指定的字段标号默认是前一位标号+1(第一个是 0)。

1
2
3
4
5
6
#[repr(i16)]
enum Enum {
Foo = 3,
Bar,
L = -1,
}

整个枚举类型的占用空间一般是 size(T) + max(size(field))。如果没有显式指定判别式的存储类型,Rust 编译器可能使用更小的类型或方法来实现判别式(但保证能正常转为 isize)。例如:

  1. Enum{A, B} 默认使用一个字节,而 Option<T> 只需要 size(T)
  2. Trick<T> 只需要等同于 usize 的空间(用 0 表示 Foo,其他值表示堆上 Box 的地址)。
1
2
3
4
5
// the size is same as usize
enum Trick<T> {
Foo,
Bar(Box<T>),
}

Vec 和 String

vec<T> 是可变长数组,数据存储存储在堆上,栈上会保留 pointer, capacity 和 length 三个参数。

  • 下标索引元素时:如果 T 没有实现 copy trait,所有权会移动;越界时会报 panic 错误。
  • .get() 索引,可以得到 Option<&T> 类型。

注意获得某个下标引用的同时禁止对 vector 进行 push,因为 push 要获取整个 vector 的可变引用。

可以用 Vec::new() 新建或 vec![] 宏初始化构造,根据下一次使用进行类型推理。

1
2
3
4
5
6
let v = Vec::<i32>::new();
let mut v = vec![1, 2, 3];
let mut v = vec![0; 5];
v.push(4);
let mut w = Vec::with_capacity(5);
w.resize(5, 0);

可以用 for...in... 简化对 vector 的遍历,包括不可变引用遍历和可变引用遍历。

1
2
3
4
5
6
7
8
let v = vec![100, 32, 57];
for i in &v {
println!("{}", i);
}
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}

可以用枚举类型来变相让 vector 支持不同类型。

1
2
3
4
5
6
7
8
9
10
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];

字符串string)由 UTF-8 编码,本质上是 Vec<u8> 的封装,数据保存在堆里。因为每个元素占用的字节数可能不同,字符串 不支持下标索引 以防止歧义发生。字符串和字符(Unicode)互转时会有性能开销。

  • .chars() 返回字符串的实际字符集合;用 .chars().count() 统计实际字符个数,复杂度线性。
  • .bytes() 返回每一个原始字节信息;用 .len() 返回实际字节数,复杂度 O(1)O(1)
1
2
3
4
5
6
7
8
let mut s1 = String::from("hello");
s1.push_str(", world!"); // push_str 的参数是 slice,即不获取其所有权
s1.push('2'); // push 的参数是单个字符
let end_char = s1.pop(); // 删除最后一个字符并返回
s1.remove(0); // 删除指定位置的字符,索引必须是该字符开头
let mut s2 = s1 + "233"; // '+' 会调用 add,会抢走左边变量的所有权
let s3 = format!("{}{}", s2, s3);
let s4 = String::from_utf8(vec![65, 66, 67, 68]);

字符串切片str)是最原始的字符串类型,能保证内部是有效的 UTF-8 编码。

字符串切片通常以借用的方式(&str)存在。&str 和数组切片一样,是一个包含地址、长度两个量的胖指针。

&str 可以源于对字符串执行切片操作。注意长度按原始字节来处理,切断 utf-8 编码后会 panic。

1
2
3
4
5
6
7
8
9
let s = "Hello, World!";
assert!(s.contains("nana"));
let v: Vec<&str> = hello.split(' ').collect();

let a: &str = "hello\n"; // 自动转义处理。
let b: &str = r"hello\world"; // 原始字面量,不处理转义。
let c: &str = r#"hello"o"#; // 包含双引号的原始字面量,用 #" 和 "# 标识头尾。
let string = String::from("xxx");
let d: &str = &string[0..5];

同时接受 &strString 的函数签名可以写成:

1
2
pub fn parse<S>(s: S) where S: AsRef<str>            // 都视为 &str
pub fn parse<S>(s: S) -> Self where S: Into<String> // 都转成 String

Vec, [u8], String, str 的相互转化

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
use std::str;
fn main() {
// -- FROM: vec of chars --
let src1: Vec<char> = vec!['j','{','"','i','m','m','y','"','}'];
// to String
let string1: String = src1.iter().collect::<String>();
// to str
let str1: &str = &src1.iter().collect::<String>();
// to vec of byte
let byte1: Vec<u8> = src1.iter().map(|c| *c as u8).collect::<Vec<_>>();

// -- FROM: vec of bytes --
// b - byte, r - raw string, br - byte of raw string
let src2: Vec<u8> = br#"e{"ddie"}"#.to_vec();
// to String
// from_utf8 consume the vector of bytes
let string2: String = String::from_utf8(src2.clone()).unwrap();
// to str
let str2: &str = str::from_utf8(&src2).unwrap();
// to vec of chars
let char2: Vec<char> = src2.iter().map(|b| *b as char).collect::<Vec<_>>();

// -- FROM: String --
let src3: String = String::from(r#"o{"livia"}"#);
let str3: &str = &src3;
let char3: Vec<char> = src3.chars().collect::<Vec<_>>();
let byte3: Vec<u8> = src3.as_bytes().to_vec();

// -- FROM: str --
let src4: &str = r#"g{'race'}"#;
let string4 = String::from(src4);
let char4: Vec<char> = src4.chars().collect();
let byte4: Vec<u8> = src4.as_bytes().to_vec();
}

模式匹配

Match 是模式匹配的经典案例,允许将一个值与一系列模式进行比较,并执行对应的流程。

  • 每一种模式的处理结果的返回值必须相同。允许某个分支里使用 continue/return/panic! ,其返回类型会被标注成 !,然后被强制转化成需要的类型。
  • 模式之间范围可以重叠(优先取第一个),但必须穷尽所有情况。可使用 <name>/_ 表示其他情况。
  • 由于编译时要确定每个模式是否非空,当模式使用序列时仅支持表达式是数值类型和字符类型。当前稳定版本的 Rust 仅支持 inclusive 的区间表达式,即必须使用 a..=b..=ba.. 三种形式。
  • | 表示并列;用 @ 将模式内容绑定到变量上供分支流程里使用,如 n @ 1..=12Some(n @ 42)
  • 值的表达式可以表示结构体,用花括号取其中的若干字段,用 .. 表示忽略剩余字段。
  • 匹配守卫match guard)应用于某个分支后,只有当额外的 if 条件成立才会执行对应语句。当同一句模式里匹配守卫和 | 结合时,| 前后所有的可能都会执行 if 条件。

if let 是 Match 的语法糖(没有定义 PartialEq 的 enum 也能使用),可选的 else 相当于 Match 里的 _

1
2
3
4
5
6
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {:?}!", state);
} else {
count += 1;
}

Rust 1.65 支持了 let else 语法。

1
2
3
4
5
6
7
fn get_count_item(s: &str) -> &str {
let mut it = s.split(' ');
let (Some(count_str), Some(item)) = (it.next(), it.next()) else {
panic!("Can't segment count item pair: '{s}'");
};
item
}

letwhile letfor 中也可以使用模式匹配来简化代码。

1
2
3
4
5
6
7
8
9
10
11
12
struct Foo {
x: (u32, u32),
y: u32,
}
let foo = ...
for Point {(a, b), c} in foo {
...
}
let Foo {
x: (a, b),
y: rename_y
} = foo;

模式的可反驳性(refutability):不可反驳的 指能匹配任何传递的值,可反驳的 指会存在匹配失败。

  • 函数参数,letfor 只能接受不可反驳的模式,因为不匹配时是无意义的。
  • if letwhile letlet else 被设计成只能接受可反驳的模式。
  • match 中最多只能有一个分支使用不可反驳模式。

切片模式可以匹配固定大小的数组和动态大小的数组切片。

  • 匹配数组时,只要每个元素都是不可反驳的,切片模式就是不可反驳的。
  • 匹配数组切片时,只有当模式是有标识符和后置的 .. 构成时,才是不可反驳的。
1
2
3
4
5
6
let v = vec![1, 2, 3];
match v[..] {
[a, b] => (),
[a, b, c] => (),
_ => (),
}

切片模式内部如果用范围模式表达某个元素:

  • 和 Match 一样,当前 Rust 稳定版不允许使用 inclusive 的范围模式。
  • 没有上下界的必须用括号括起来,如 (a..);具有上下界的范围模式则无须括号,如 a..=b

Option 和 Result

Option 和 Result 是 Rust 内置的泛型枚举类型。

1
2
3
4
5
6
7
8
enum Result<T, E> {
Ok(T),
Err(E),
}
enum Option<T> {
Some(T),
None,
}

Result<T, E> 处理可恢复的错误。

  • .unwrap() 可以解包 Result 直接获得其返回值,若产生 Error 会转化成 panic。
  • .expect() 与上述类似,只是会把 panic 错误信息替换成给出的字符串参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => panic!("Problem opening the file: {:?}", other_error),
},
};
}

调用函数的末尾加上 ? 后可以简单地起到错误传播的作用。即若 Result 返回值是 Ok,? 不起任何作用;若返回值是 Err,会把值做一个 into() 强制转换并向外传递。? 本质上是所有实现了 try trait 的类型的语法糖。最常见的实现了 try trait 的类型是 Result<Ok, Err>Option<T>ControlFlow<B, C>

1
2
3
4
5
fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}

当开发者遇到不可处理的错误时,Rust 会产生 panic,此时有两种处理方式可选。

  • 栈展开(unwind),默认的处理方式。Rust 会回溯栈上的数据和函数调用,终止出错所在线程,并调用析构函数。栈展开可以设置 RUST_BACKTRACE 环境变量来得到 backtrace。在没有使用 unsafe 代码的情况下,栈展开能保证 panic 时不会导致内存不安全。panic::catch_unwind 用于捕捉栈展开方式下的 panic,但要求传递过来的闭包必须是 UnwindSafe 的。若无标记,可以用 AssertUnWindSafe 这个包装器强制转换。
  • 终止(abort)。程序会不清理数据直接退出,由操作系统来清理。可在配置项里配置 panic = 'abort'

Option<T> 表达可能为空的数据,它和 Result 一样可以在函数里里用 ? 抛出。

如果要用到 Option<T> 的引用,一般来说 Option<&T>&Option<T> 更好

  • 若返回值里返回 Option 可变引用,Option<&mut T>&mut Option<T> 语义不同,酌情选取。
  • 若返回值里返回 Option 不可变引用,前者更优。从使用侧考虑,对 Option 做操作(如 map)时前者的变式更方便。此外在需求变化时,前者的兼容性也更好:当 Option<T> 改为 Option<Box<T>> 时,前者的函数签名不用变化;前者可以在函数里进一步 filter 而后者不行。
  • 若参数里传入 Option 的可变引用,也是前者更优,因为后者在构造时会移动 T。

前者的内存空间似乎比后者多了个标记位,但其实 Rust 做了 Option 套引用的内存优化,举例如下:

  • Option<i32> 会占用两个连续的字节,一个是标记位,一个存具体的 i32。
  • &Option<i32> 会占用一个固定的内存指针的空间,指向 Option<i32> 的地址。
  • Option<&i32> 只需一个内存指针,不再需要标记位:全零地址表示 None,非全零地址指向 i32 地址。

黑科技:If let Some(a) = b 可以写成 for a in b(但是不推荐)。

Option 和 Result 之间可以灵活转化,Rust 提供了很多类似流操作的 API。

trait

trait 类似于其他语言的接口(interfaces),用来定义一系列函数的集合。任何类型都可以是 trait 的实例。

定义 trait 时可为函数实现默认方法。注意,无法从相同方法的重载实现中调用默认方法。

1
2
3
4
5
6
7
8
9
10
11
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}

使用泛型时,可以用 : 指定要求实现的 trait,称为泛型约束(trait bound)。多个 trait 之间用 + 连接。函数参数中可以用 impl 简化声明,也可以用 where 语法把参数里的泛型约束更清晰地提取出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pub fn notify(item1: impl Summary, item2: impl Summary)
pub fn notify<T: Summary, U: Summary>(item1: T, item2: U)

fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32
fn some_function<T, U>(t: T, u: U) -> i32
where T: Display + Clone, U: Clone + Debug

struct Pair<T> {
x: T,
y: T,
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
println!("x = {}, y = {}", self.x, self.y);
}
}

trait 没有继承功能,但可以要求作为若干个 trait 的超集,用 :+ 连接,例如 trait Subtrait: Supertrait1 + Supertrait2,那么实现 Subtrait 时必须同时实现前两个 trait。

如果参数或返回值只有在实现 trait 时才能确定,可以用 type 关键字定义关联类型(associated types)。关联类型和泛型很像,但是在实现 trait 时只需指定一次,不必在每一处声明泛型类型。

1
2
3
4
5
6
7
8
trait Animal {
type food;
fn eat(&self, _: Self::Food);
}
impl Animal for Sheep {
type food = Grass;
fn eat(&self, grass: Grass) {...}
}

如果在关联类型定义中声明泛型参数,被称为泛型关联类型(Generic Associated Types,GAT)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
trait Calculator {
type I<T>: Add<T, Output = Self::I<T>> where T: Add<T, Output = T>;
}
struct Data<T: Calculator> {
data1: T::I<i8>,
data2: T::I<f32>,
}
enum B<T> { Val(T), Err}
impl<T: Add<T, Output = T>> Add<T> for B<T> {
type Output = B<T>;
fn add(self, rhs: T) -> Self::Output {
match self {
Self::Val(val) => Self::Val(val + rhs),
Self::Err => Self::Err,
}
}
}

Rust 定义满足以下条件的 trait 被认为是对象安全object safe)的:

  • 没有任何关联类型,也不包含泛型参数。
  • 所有 super trait 均是对象安全的。
  • 所有函数正好只有第一个参数是涉及 self 的。

当我们希望使用一个 trait 的实例而不确定到底是哪种类型时,可以用 dyn trait 来实现动态绑定。常见的使用方法是 &dyn traitBox<dyn trait>。注意如果 dyn trait 用于非对象安全的 trait,会触发 rust 编译器的报错。可以用 where Self: Sized 来将未满足对象安全条件的函数排除在外(dyn trait 时就无法调用)。

1
2
3
4
5
6
7
trait Animal {
fn noise(&self) -> &'static str;
fn foo(x: i32) where Self: Sized;
}
fn animal_speak(animal: &dyn Animal) -> &'static str {
animal.noise()
}

Rust 不允许为外部类型实现外部 trait,即只有当 trait 或者要实现 trait 的类型位于 crate 的本地作用域时,才能为该类型实现 trait。这个限制被称为孤儿规则(orphan rule),主要有两个目的:防止 crate 间的 trait 互相冲突;实现向前兼容,即不会因为依赖 crate 中 trait 实现的变化造成无法编译。

一个绕开孤儿规则的方法是使用 newtype 模式,对待实现 trait 的类型做一个简单的封装。如下:

1
2
3
4
5
6
7
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(", "))
}
}

当和 trait 的方法出现歧义时,可以用完全限定语法(fully qualified syntax)指定。 如果结构体 Human 定义了 fly(&self) 方法,它又实现了带有 fly(&self) 方法的 Pilot trait,h.fly() 默认使用前者, Pilot::fly(h) 指定使用后者;如果 fly() 方法里不带有 &self,用 <Pilot as Human>::fly() 指定使用后者。

所有权和生命周期

Rust 中的每一个值都有一个对应的所有者(owner)的变量。当变量离开其作用域时,值会被丢弃。

  • 对未实现 copy trait 的类型进行赋值或函数传参时所有权会转移(move),原变量不能再被使用。
  • 对于结构体或元组等编译期间可以确定的复合类型,允许部分成员发生所有权的转移,其他成员仍然可以正常使用,但结构体整体不允许再被使用;而数组、Vec 不允许部分成员转移,因为编译器无法准确统计。

Rust 用 & 表示引用(borrow),不发生所有者的转移;用 &mut 表示可变引用,可以改变值但是不发生所有者的转移。借用检查规定:一个值在同一个作用域内只能有一个可变引用或多个不可变引用。

Rust2018 引入非词法作用域生命周期(Non-Lexical Lifetimes,NLL)特性。如果某个引用在以后的代码中不再被使用,它会在最后一句相关的语句执行之后提前被消除。即以下代码能正确运行。

1
2
3
4
5
6
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
let r3 = &mut s;
println!("{}", r3);

数组元素的借用会使整体被标记成借用。为了对多个元素进行可变操作,有以下几种解决方案:

  • 使用循环或其他迭代结构,这样可以在不同迭代步骤的作用域中分别可变借用不同的元素。
  • 使用切片的 split_at_mut 方法,可以获得指向数组不同部分的两个可变切片。
  • 使用 CellRefCell 这种提供内部可变性的类型,但会造成一些运行时的开销。

生命周期(lifetime)用于表示多个引用的泛型生命周期参数如何相互联系。生命周期参数本质用泛型来标注,参数以 ' 开头,通常是较短的小写字母(如 'a)。返回值引用和某个参数引用如果有相同的标记,意味着其生命周期不得短于该参数。注意使用泛型参数时,生命周期参数始终在类型参数之前。

下面的例子暗示了 longest 返回的引用的生命周期与传入两个引用的较小者保持一致。

1
2
3
4
5
6
7
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}

结构体的每个引用也要标注。下例暗示 ImportantExcerpt 实例不能比其 part 字段引用的字符串存在得更久。

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

结构体方法里的引用也要标注。下例展示了两组(执行生命周期省略规则后)合法的方法标注示例。

1
2
3
4
5
6
7
8
9
10
11
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}

早期 Rust 要求每个引用都标注明确的生命周期,后来编译器会执行生命周期省略规则(lifetime elision rules)。Rust 有一条输入生命周期(input lifetimes)省略规则和两条输出生命周期(output lifetimes)省略规则。它会根据这三条规则对当前代码进行不完整推断,如果推断完后仍然残留未知生命周期的参数就会编译报错。

  1. 默认每一个是引用的参数都有它自己默认的生命周期参数。
  2. 如果输入生命周期参数只有一种,那么它会被赋予给所有输出生命周期参数。
  3. 如果方法有多个输入生命周期参数且其中一个参数是 &self&mut self(说明是对象的方法),那么所有输出生命周期参数会被赋予 self 的生命周期。

根据 RFC 599 和 RFC1156,Trait 的默认生命周期会被加上 'static,显式加上 '_ 才能用于常规的省略规则。

静态生命周期用 'static 标注,其生命周期能够存活于整个程序期间。字符串字面量默认是静态生命周期。

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

Map 和 Iterator

std::collections::HashMapahash::HashMap 更慢但更(密码学)安全。

HashMap 的键和值的类型必须各自相同。

  • .insert(key, value):插入一组元素,若存在则会覆盖。
  • .entry(key).or_insert(value):如果 key 不存在,就插入指定 value。返回 value 的可变索引。

对于基本类型这种实现了 copy trait 的类型,其值会直接拷贝至 map,否则会转移所有权至 map。

1
2
3
4
5
6
7
8
9
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
let teams = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];
let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
let score = scores.get(&team_name);
for (key, value) in &scores {
println!("{}: {}", key, value);
}

迭代器都实现了一个叫做 Iterator 的标准 trait,其定义如下:

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

type ItemSelf::Item 定义了 trait 的关联类型(associated type),表示 next 返回的是元素的类型。

v.iter()/iter_mul()/into_iter() 获取 vector 对应的迭代器。后者会获取 vector 的所有权。

map 能遍历(消费)迭代器并返回一个新迭代器,sum/collect/filter 等能获得迭代器所有权并消费它。

1
2
3
4
5
6
7
8
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
let sum: u32 = Counter::new()
.zip(Counter::new().skip(1))
.map(|(a, b)| a * b)
.filter(|x| x % 3 == 0)
.sum();
}

闭包

所谓闭包,就是能够捕获作用域里变量的匿名函数。

定义闭包时,|| 里定义参数列表,后接 {} 定义函数体。函数参数和返回值的类型可以省略,根据第一次使用的情况自动推理。闭包和函数最大的不同是它可以自动捕捉作用域里的变量。

1
2
3
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;

编译器倾向于优先通过不可变借用来捕获外部变量,其次是可变借用,最后才是移动所有权。

在闭包参数前加上 move 关键词后,闭包会强制获取作用域里所需变量的所有权(copy trait 仍然是拷贝),一般用于跨线程数据传输。这种闭包在调用一次之后无法再次捕获作用域里的变量,所以只能被 FnOnce() 接受。

  • 当 move 闭包使用了数组元素,且数组元素不支持 copy trait:获取了整个数组的所有权。
  • 当 move 闭包使用了结构体、元组、枚举的字段,且该字段不支持 copy trait:如果该结构体未实现 drop trait,则捕获的是单个字段的所有权,否则是整个结构体的所有权。

闭包作为函数参数时,可以用三种 trait 承接:

trait 名 原理
FnOnce(self) 只能调用一次。所有闭包都会实现 FnOnce trait。
FnMut(&mut self) 可多次调用。获取作用域里变量的可变引用,生存期间外界无法获取引用
Fn(&self) 可多次调用。获取作用域里变量的不可变引用,生存期间外界无法获取可变引用

普通函数的 fn 无须捕获作用域里的变量,因此可以随时随地调用。函数指针可将函数作为参数,其同时实现了闭包的三个 trait。不捕获环境中的任何变量的闭包可以通过匹配签名的方式自动转化成函数指针。

1
2
3
4
5
6
7
fn main() {
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
let add = |x: i32| x + 1;
let answer = do_twice(add, 5);
}

闭包返回引用类型的参数时,需要显式指定生命周期。

智能指针

智能指针通常用结构体实现,通过实现 Deref trait 和 Drop trait 来做到自动引用和清理。

最简单的智能指针是 Box<T>,实际数据会被转移到堆上存储,栈上仅保留指针。内存布局取决于 T 本身:如果是 T 是 Sized 栈上就只需记录地址,如果是 Slice 栈上还需记录长度,如果是 dyn trait 栈上还需记录虚表指针。

Box 可以解决递归类型的问题(Rust 不允许直接递归定义结构体,因为这样就无法计算所需空间)。

1
2
3
4
5
6
7
enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}

Box 会实现 Deref 实现自动解引用。对 Box 执行解引用时,会转移指针内部数据的所有权。

1
2
3
4
5
6
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}

Rc<T> 是引用计数指针,支持多所有权机制。用 Rc::clone 克隆时计数器会加一,离开作用域计数器就减一,当计数值为 0 时数据会被清理。 注意 Rc<T> 仅支持多个部分 只读地 共享数据,否则会违反所有权规则。

Rc<T> 可能会引起引用循环导致内存泄漏,因为只有当 strong_count=0 才会清理。可以调用 Rc::downgrade 创建 Weak<T> 。弱引用也会进行计数,区别在于 weak_count 无须计数为 0 就能使 Rc<T> 实例被清理。

Cow 智能指针包含 Borrowed 和 Owned 两种类型,前者是对原始数据的引用,后者拥有所有权。

1
2
3
4
5
6
7
pub enum Cow<'a, B>
where
B: 'a + ToOwned + ?Sized,
{
Borrowed(&'a B),
Owned(<B as ToOwned>::Owned),
}

Cow 支持 clone-on-write 功能,在需要 Clone 时才进行 Clone。下面是官网的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn abs_all(input: &mut Cow<[i32]>) {
for i in 0..input.len() {
let v = input[i];
if v < 0 {
input.to_mut()[i] = -v;
}
}
}

// No clone occurs because `input` doesn't need to be mutated.
let slice = [0, 1, 2];
let mut input = Cow::from(&slice[..]);
abs_all(&mut input);

// Clone occurs because `input` needs to be mutated.
let slice = [-1, 0, 1];
let mut input = Cow::from(&slice[..]);
abs_all(&mut input);

// No clone occurs because `input` is already owned.
let mut input = Cow::from(vec![-1, 0, 1]);
abs_all(&mut input);

还有个经典例子是 B = strCow<&str> 一般用于函数的返回值,取决于是否对参数 &str 做了修改。

1
2
3
4
enum Cow {
Borrowed(&str),
Owned(String),
}

小课堂:size_of::<Cow<str>>() 在 Rust 版本里的变迁。

Unsafe 和内部可变性

unsafe 关键字可以修饰函数、代码块、trait 定义和 impl 块。

  • 用 unsafe 修饰的 trait 在 impl 的时候必须也加上 unsafe,不能和 safe 的混合使用。
  • unsafe 函数以为这个函数在某些场景下安全、某些场景下不安全,其调用必须在 unsafe 上下文中。和其他语言交互的 ffi 函数 extern "C" 会被隐式地带上 unsafe 修饰。
  • unsafe block 指需要使用 unsafe 代码作为内部实现(且当前上下文已经满足安全条件),如解裸指针。

内部可变性(interior mutability)是 Rust 的一个设计模式,需要用 unsafe 标记,以模糊可变性和借用规则。

UnsafeCell<T> 提供了一种在不可变值内部进行修改的方法。不基于其开发的内部可变性都是未定义行为。

1
2
3
4
#[repr(transparent)]
pub struct UnsafeCell<T: ?Sized> {
value: T
}

Cell<T> 基于所有权转移提供内部可变性接口,内部直接基于 UnsafeCell 做了封装,不会导致 panic。

1
2
3
4
5
6
7
8
9
10
11
#[repr(transparent)]
pub struct Cell<T: ?Sized> {
value: UnsafeCell<T>,
}

let a: Cell<u32> = Cell::new(5);
assert_eq!(a.get(), 5); // 返回内部数据拷贝,需要 T:Copy
let b = a.replace(10); // 替换 Cell 里的数据,返回旧数据
a.set(10); // 替换 Cell 里的数据,舍弃旧数据
let b: Cell<u32> = Cell::new(10);
a.swap(&b); // 交换两个相同类型的 Cell 的数据

RefCell<T> 基于引用计数提供内部可变性接口,内部也基于 UnsafeCell 开发。

  • 它有 borrowborrow_mul 两个方法,分别创建一个不可变借用和可变借用。任何时候仅能有多个不可变借用或者一个可变借用,这个检查从编译时推迟到运行时,违反会 panic。
  • 内部维护计数器:初始值为 ;不可变借用时值 +1,释放时值 -1;可变借用时值直接置为 -1。
  • 使用 try_borrow() 可尝试获取不可变借用,若内部计数器值为 -1 则返回 BorrowError。

结合 RcRefCell 来拥有多个可变数据所有者,即 Rc<RefCell<T>>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
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(6)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));
*value.borrow_mut() += 10;
println!("a after = {:?}", a); // a after = Cons(RefCell { value: 15 }, Nil)
println!("b after = {:?}", b); // b after = Cons(RefCell { value: 6 }, Cons(RefCell { value: 15 }, Nil))
println!("c after = {:?}", c); // c after = Cons(RefCell { value: 10 }, Cons(RefCell { value: 15 }, Nil))
}

裸指针

裸指针为基本原生类型,分 *const T*mut T 表示是否能修改指向数据。

  • 没有生命周期,不会自动执行任何内存安全检查,不保证指向有效的内存。
  • 不能实现任何自动清理功能(不能自动 drop)。注意 *ptr = data 会调用旧值的 drop。
  • 允许为空,用 ptr::NonNull<T> 表示非空且协变的可变裸指针。
  • 不支持隐式转换,需要用 as 关键字转换。
  • 只能在 unsafe 上下文中解引用。

裸指针的获取方式:直接获取、Box::into_raw()std::ptr::addr_of!()、从 C 中传递。

1
2
3
4
5
6
7
8
9
10
let my_num: i32 = 10;
let my_num_ptr: *const i32 = &my_num;
let mut my_speed: i32 = 88;
let mut my_speed_ptr: *mut i32 = &mut my_speed;

let my_speed: Box<i32> = Box::new(88);
let my_speed: *mut i32 = Box::into_raw(my_speed);
unsafe {
drop(Box::from_raw(my_speed));
}

ptr 模块常用的安全抽象方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 会按字节对齐的 T 的大小计算偏移量,内部没有做边界检查
pub const unsafe fn offset<T>(self, count: isize) -> *const T / *mut T
pub const unsafe fn add<T>(self, count: usize) -> *const T / *mut T
// 按位复制,复制位数为 count*size_of::<T>()
pub const unsafe fn copy<T>(src: *const T, dst: *mut T, count: usize)
// 在不读取或 Drop 旧值的情况下覆盖内存位置;要求内存地址对 T 对齐,否则用 write_unaligned。
// 如果是敏感信息,建议用 std::ptr::write_volatile() 函数。
pub const unsafe fn write<T>(dst: *mut T, src: T)
// 和 write 类似,额外返回旧值。更推荐使用 std::mem::replace
pub const unsafe fn replace<T>(dst: *mut T, src: T) -> T
// 从指针指向内存读取一个值,但不改变所有权;要求内存地址对 T 对齐,否则用 read_unaligned。
pub const unsafe fn read<T>(src: *const T) -> T
// 要求 x 和 y 都初始化且对 T 对齐,两者指向位置可能重叠。更推荐使用 std::mem::swap
pub const unsafe fn swap<T>(x: *mut T, y: *mut T)

FFI

FFI(Foreign Function Interface)是一种编程技术,允许在一个语言中调用另一个语言便携的函数。Rust 的 FFI 特点是安全和高效。Rust 提供了丰富的类型和抽象来安全地、无额外开销地处理 C 语言的数据和函数。

ABI(Application Binary Interface)定义了函数调用细节(包括参数传递、返回值和寄存器等),调用双方要同时遵守。Rust 中的 FFI 安全性主要依赖于正确的 ABI 和正确的类型转换:函数签名(参数类型、参数顺序和返回类型)需要完全一致;数据布局要保持一致,Rust 结构体用 #[repr(C)] 表示 C 布局;交互时对象的生命周期需要正确地手动管理;Rust 的 Panic 和 C++ 的异常是互相不兼容的,在 ABI 边界要处理好所有异常。

extern <abi> 声明 FFI 时的 ABI,常见的包括:

  • Rust:Rust 自身的函数,可以认为 fn f() 带有 extern "Rust"
  • C:与 C 交互的标准 ABI,extern fn f() 时的默认值。
  • stdcall:部分与 Win32 强相关的与 C 交互的 ABI。
  • system:一般等价于 C,在不同系统里自动转化(如 Win32 系统里相当于 stdcall)。

类型对照表:

C 类型 Rust 类型
void ()
void * *mut c_void
int8_t, uint8_t, int16_t, ..., uint64_t i8, u8, i16, ..., u64
float, double f32, f64
char, unsigned char, signed char c_char, c_uchar, c_schar
(unsigned) short/int/long/long long c_(u)short/int/long/longlong
size_t, ptrdiff_t usize, isize
char *, const char * *mut c_char, *const c_char
struct, union, enum struct, union, enum/i32

Rust 里的 String&str 表示一定有效的 UTF-8 字符串数据。

  • ffi::CString 用于表示传递给 C 的字符串,末尾带有 \0,对应的切片是 CStr
  • ffi::OsString 用于表示平台原生字符串类型,对应的切片是 OsStr:Windows 系统中是非零的 16 位值的任意序列,一般为 UTF-16;Unix 系统中由非零字节组成的 8 位值的任意序列,一般为 UTF-8。

智能指针、宽指针和函数在 Rust 和 C 转换时需要和 *mut c_void 交互,可用 Box 中转。

1
2
3
4
5
6
7
8
9
pub extern "C" fn create_my_struct_for_c(value: i32) -> *mut c_void {
let mu_struct = MyStruct { value };
let boxed: Box<dyn MyTrait> = Box::new(my_struct);
Box::into_raw(boxed) as *mut c_void
}
pub extern "C" fn call_back_from_c(closure: *mut c_void) where F: FnOnce {
let closure = Box<F> = unsafe { Box::from_raw(closure as *mut F) };
closure();
}

MaybeUninit<T> 是 Rust 标准库提供的结构,表示可能没有被初始化的内存,适合和 C 交互。

1
2
3
4
5
let mut x = MaybeUninit::<i32>::uninit();
unsafe {
x.as_mut_ptr().write(5); // 如果不执行这句话,会导致未定义行为。
}
let x_init = unsafe { x.assume_init(); };

ManuallyDrop<T> 是 Rust 标准库提供的包装类型,用于阻止其包装的类型 T 的析构函数的自动调用。

GlobalAlloc 是 Rust 中用于自定义内存分配器的 trait,当前仅能全局替换所有类型的分配方式。

内存布局

如果一个结构体实现了 Sized,就在编译时有了确定的大小和对齐量,可以使用 std::mem::size_of()std::mem::align_of() 计算对应的类型大小和对齐量。用 repr 更换对齐方式。

  • 结构体和元组 布局规则一致:对齐量等于所有成员中对齐量的最大值,成员之间可能会留空隙。
  • 枚举 的 Tag 固定占一个字节,占用空间是每个成员的布局最大值加上一字节(没有优化的情况下)。
  • Option 类型有特殊的优化,可以用零值表示 None,从而压缩掉 Tag 的字节。
repr 规则 含义
default Rust 默认排布风格,不保证成员的排列顺序和稳定性。
C C 风格排布,无优化。成员有序,会根据对齐量填空隙,枚举类型是枚举值和 union 两段
packed(n) 在基本对齐规则上,将结构体的对齐量减少到 n,成员之间的间隙按新对齐量填充。
align(n) 在基本对齐规则上,保证结构体的对齐量至少是 nn 是 2 的幂次)。
<int_type> 声明枚举类型的 tag 的存储类型
transparent 用于单成员的结构体/枚举,表明内外有相同的内存布局。

Union 类型通常只用于和 C 交互,所有字段共享同一段存储,对某个字段的 mut 引用等价于对整个结构体。

Union 类型不能实现 Drop Drait,其内部的成员类型也不能,即其成员类型必须满足以下之一:实现了 Copy Trait 的类型,&T 或者 &mut T 类型,ManuallyDrop<T> 类型,以上类型组成的元组或数组类型。

对 Union 类型的成员变量 读操作 必须 unsafe,包括常见的模式匹配来读取。

宏(macro)分为 声明宏declarative macro)和三种 过程宏procedural macro)。

声明宏用于编写一些类 match 的表达式来生成代码,类似于 C 中的 define。以下是 vec! 简化后的声明宏。#[macro_export] 标注说明:只要将定义了宏的 crate 引入作用域,宏就应当是可用的。

1
2
3
4
5
6
7
8
9
10
11
12
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}

过程宏procedural macros)更像是函数,接收 Rust 代码作为输入,操作后产生另一些代码作为输出。过程宏分为自定义派生(derive)、类属性和类函数三种。

举个例子,我们希望实现一个自定义 derive 宏,使得只要在结构体定义前加上 derive(HelloMacro) 的标记,就能为结构体自动定义一个能够打印结构体名字的 trait 方法。

  • 当用户在一个结构体前指定了 derive(HelloMacro) 标记后,hello_macro_derive 会被调用。
  • syn::parse 负责解析把 rust 代码解析成可操作的数据结构。若失败,会直接 panic。
  • impl_hello_macro() 读入可操作的数据结构,转化成新代码的 TokenStream。quote! 宏可以让我们编写需要返回的 Rust 代码。最后的 into() 方法会将其从原来的中间表示(IR)转化成 TokenStream
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
extern crate proc_macro;
use crate::proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
let ast = syn::parse(input).unwrap();
impl_hello_macro(&ast)
}
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}", stringify!(#name));
}
}
};
gen.into()
}

类属性宏不仅可以为属性生成代码,还可以创建新的属性。类函数宏能比声明宏更灵活地操控函数。

serde 和 serde_json

serde 用于将 Rust 结构体序列化和反序列化,serde_json 则专注于和 Json 的交互。

作用在结构体某个字段上的常见标记:

1
2
3
4
5
6
7
8
9
10
11
#[serde(rename = "name")]    // 将序列化和反序列化的名字重命名成 name
#[serde(rename(serialize = "ser_name", deserialize = "de_name"))]
#[serde(default)] // 若该字段未出现则调用类型的 Default 方法
#[serde(default = "path")] // 若该字段未出现则调用 path 指向的函数
#[serde(borrow)] // 执行 zero-copy 反序列化,经典场景是反序列化成 &str
#[serde(skip)] // 该字段跳过序列化和反序列化
#[serde(skip_deserializing)] // 默认调用 Default,也可以配合 default = "path"
#[serde(skip_skip_serializing_if = "path")] // e.g. "Option::is_none"
#[serde(serialize_with = "path")] // 指定该字段的序列化方法,要求不能已实现 Serialize
#[serde(deserialize_with = "path")] // 指定该字段的反序列方法,要求不能已实现 Deserialize
#[serde(borrow)] // 从原始数据里 borrow 而非新建

作用在结构体或 enum 外层的常见标记:

1
2
3
4
#[serde(rename = "name")]          // 修改整个结构体/enum 的名字
#[serde(rename_all = "...")] // lowercase, UPPERCASE, PascalCase, camelCase, snake_case, SCREAMING_SNAKE_CASE, kebab-case, SCREAMING-KEBAB-CASE
#[serde(rename_all(serialize = "...", deserialize = "..."))])]
#[serde(deny_unknown_fields)] // 遇到未知字段时会报错(暂不支持和 flatten 一起用)

对于 enum,默认使用 externally tagged 进行解析,也就是把每种类型名作为 map 的 key 来解析。

1
2
3
#[serde(tag = "type")]             // internally tagged,类型名放在名为 type 的 key 下。
#[serde(tag = "t", content = "c")] // adjacently tagged,额外把成员内容放在 content 下。
#[serde(untagged)] // 不再序列化 enum 的类型,直接平铺每个成员的内容

有一个关于 &str 进行反序列化的坑。由于转化成 Rust 的 &str 后长度缩小了,str 的表示失败会 unwrap。解决方法是全程使用 String。如果真的想优化内存,可以使用 Cow<&str> + #[serde(borrow)]

1
2
3
4
5
6
#[derive(serde::Deserialize)]
struct Foo<'a> {
a: &'a str,
}
serde_json::from_str::<Foo>(r#"{"a":"123"}"#).unwrap(); // no problem
serde_json::from_str::<Foo>(r#"{"a":"123\n"}"#).unwrap(); // unwrap

serde_json::to_string 对 struct 序列化时结果是唯一的,字段之间会按定义顺序排序。但是如果字段里有无序结构(如 HashMap),则不能保证序列化结果的唯一性。

1
2
3
4
5
6
7
8
let v = json!({
"name": "John",
"phones": ["110", "120"],
}) // Construct Value
let v: Value = serde_json::from_str(data)?; // Json String -> Value
let p: Persion = serde_json::from_str(data)?; // Json String -> Struct
let s = v2.to_string(); // Value -> Json String
let j = serde_json::to_string(&p)?; // Struct -> Json String

测试时,常常需要比较 Value 和某 rust 结构(String 或 Hashmap 等)。可以使用 json!() 包裹后者。

Cargo Insta

Insta 库是 Rust 测试里比较好用的库,能够为结果生成类似快照的内容,系统性地管理测试的输出文件。当第一次测试或结果有更新时会生成 .snap.new 为后缀的文件,归档后会生成 .snap 为后缀的文件。

1
2
3
4
5
6
assert_snapshot!()
assert_debug_snapshot!()
assert_yaml_snapshot!()
assert_ron_snapshot!()
assert_json_snapshot!()
assert_compact_json_snapshot!()

使用 cargo install cargo-insta 安装 insta 的命令行工具,便于管理 snapshots 文件。

1
2
3
4
5
6
cargo-insta review                               # review all pending snapshots
cargo-insta review --snapshot [snapshotname] # review specific snapshot
cargo-insta accept/reject # accept or reject snapshot(s)
cargo-insta pending-snapshorts # list all pending snapshots
cargo-insta test --accept # run tests and accept all snapshorts
cargo-insta test --accept-unseen # run tests and accept new snapshorts

Insta glob! 宏支持一次管理多个文件,用 globset 通配。生成的 snapshots 文件名字包含test_file_pathtest_functioninput_path 三部分,其中 input_path 会取所有读取的文件的 lcp 的后面部分。

1
2
3
4
5
6
use std::fs;

glob!("inputs/*.txt", |path| {
let input = fs::read_to_string(path).unwrap();
assert_json_snapshot!(input.to_uppercase());
});

附录:常见 trait

常见 trait 效果
Add/Sub/… 通过实现对 std::ops 里的运算符 trait,可以实现运算符的重载。
Clone 标记结构体是否能被整个复制。
Copy Clone 超集。标记性质,表示类型可以按位复制,和 Drop trait 不能一起实现。已实现的类型包括:整型,布尔型,浮点型,字符型,成员已实现的元组或数组,不可变引用。
Display 定义当前类型如何支持 Formatter 来打印。实现了 Display 会自动实现 ToString。
Drop 定义方法 drop(),在对象离开作用域时自动调用。析构是有顺序的。
Deref 重载解引用运算符,使得调用 *mb 等价于调用 *(mb.deref())
Into<T> 定义当前类型如何转化成类型 T。
From<T> 定义当前类型如何从类型 T 转化而来。实现了 From 会自动实现 Into。
TryInto<T> 定义当前类型如何转化成类型 T,返回结果是 Result。
TryFrom<T> 定义当前类型如何从类型 T 转化而来,返回结果是 Result。
FromStr<T> 定义如何从 str 类型转化成当前类型,实现后通过 .parse() 触发。
Eq PartialEq 超集,比较同类型的实例,要求满足自反性、对称性和传递性。
Ord PartialOrd 和 Eq 超集,比较同类型的实例,要求比较顺序确定(浮点数未实现 Ord)
PartialEq 比较类型 A 的实例 a 是否和类型 B 的实例 b 是否相同。
PartialOrd PartialEq 超集,比较类型 A 的实例 a 和类型 B 的实例 b 的大小关系。
Send 标记该类型能安全地发送至另一个线程
Sized 表示实例占用的空间大小在编译时确定。函数参数要求必须是 Sized。无长度的参数数组、不含 Sized 约束的 dyn trait 和含有 ?Sized 约束的结构体泛型参数被认为是 Unsized 的。
Sync 标记该类型能够安全地在线程之间共享
ToString 定义当前类型如何转化成 String 类型,实现后通过 .to_string() 触发。
Unpin 表示类型能够安全地在内存中移动
UnwindSafe 标记发生 panic 时的类型是否仍然安全。

Sized

动态大小类型(dynamically sized types,DST)允许我们处理只有在运行时才知道大小的类型。Rust 不允许直接创建 DST 的变量,也不能直接获取 DST 的参数。str 就是一个典型的 DST,我们常用引用的形式使用它(记录地址和长度,所以 &str 的大小是 usize 大小的两倍),或者用 Box<str>Rc<str> 等指针使用它。

为了处理 DST,Rust 有一个特定的 trait Sized 来确定一个类型的大小是否在编译时可知。而且 Rust 隐式地为每一个泛型函数增加了 Sized bound。只有当 T 使用了 T: ?Sized 时才表明 T 的大小可能在编译时无法确定。

Drop

Rust 不允许自发调用 Drop::drop 方法,只能通过 std::mem::drop(x) 显式释放资源。析构顺序如下:

  • 作用域里的变量按定义的逆序析构,函数参数同样按定义的逆序析构。
  • 嵌套的作用域按深度优先的顺序(inside-out order)析构。
  • 结构体、枚举的字段按代码中声明的顺序析构,元组和数组按元素的先后顺序析构。
  • 注意闭包通过移动捕获的变量的析构顺序目前是未定义的。

Deref

如果没有实现 Deref trait,编译器只会解引用 & 引用类型。deref 方法提供了其他解引用场景的功能。

1
2
3
4
pub trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}

Rust 支持以下三种解引用的强制转换(deref coercions),如 &String 能够强转为 &str

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

Pin 和 UnPin

参考资料:Pin ModulePin-Rust Async Book

Unpin 是一种 trait,表示类型能够安全地在内存中移动。Rust 的类型默认会加上 Unpin 这个 trait,所以标准库中绝大部分类型都是 Unpin 的。异步编程里 async/await 产生的 Future 类是典型的 !Unpin

Pin<P> 是一种包裹类,意为 P 指向的地址 T 不会发生移动,常用来解决自引用的场景。注意只有当 T:!UnpinPin<P> 的功能才生效,即对 Unpin 的类型套上 Pin 后依然能正常将其移动。

Pin 的实现原理可从源码中窥得一二。简单来说,Pin 占有了 P 指向地址的可变引用,外界只能通过 unsafe 的方式改变/移动它。如果我们通过把 Pin<&mut T>.as_mut() 传入 std::mem::swap(a, b) 来尝试移动呢?注意到 Pin 不允许 !Unpin 结构进行 get_mut(),而 as_mut() 的返回结果也被包裹成 Pin,所以编译不通过。从源码中也能看出,若 TUnpin 类型,Pin<'a, T> 完全等价于 &'a mut T,能正常进行移动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
impl<P> Pin<P> where P: Deref, <P as Deref>::Target: Unpin
pub fn new(pointer: P) -> Pin<P>
pub fn into_inner(pin: Pin<P>) -> P

impl<P> Pin<P> where P: Deref
pub unsafe fn new_unchecked(pointer: P) -> Pin<P>
pub fn as_ref(&self) -> Pin<&<P as Deref>::Target>

impl<P> Pin<P> where P: DerefMut
pub fn as_mut(&mut self) -> Pin<&mut <P as Deref>::Target>

impl<P> Pin<P> where P: DerefMut, <P as Deref>::Target: Sized
pub fn set(&mut self, value: <P as Deref>::Target)

impl<'a, T> Pin<&'a T> where T: ?Sized
pub fn get_ref(self) -> &'a T

impl<'a, T> Pin<&'a mut T> where T: ?Sized
pub fn get_mut(self) -> &'a mut T where T: Unpin
pub unsafe fn get_unchecked_mut(self) -> &'a mut T

impl<T> Pin<&'static T> where T: ?Sized
pub fn static_ref(r: &'static T) -> Pin<&'static T>

如果想标记一个类型是 !Unpin 的,目前 stable 版本中只能添加 std::marker::PhantomPinned 来达成。

Send 和 Sync

SendSync 用来刻画结构体在多线程模型下的线程安全性的 trait,是 rust 语言层面提供的标记。

Trait 名 定义
Send 该类型能安全地发送至另一个线程
Sync 该类型能够安全地在线程之间共享

两者的关系是: TSync 的当且仅当 &TSend 的。

  • 几乎所有基本类型(裸指针)都是 SendSync 的。
  • UnsafeCell 不是 Sync 的,这也导致 CellRefCell 不是 Sync 的。
  • Rc 既不是 Send 也不是 Sync 的,它本身就被设计成单线程下的多引用,多线程下对应模型是 Arc

这两个 trait 能自动推导:如果一个类型的所有成员都是 SendSync 的,那么它也是 SendSync 的。如果自行给类型标注 Sendsync,需要在前面加上 unsafe 标记。