Fork me on GitHub

Rust 语言备忘清单 2024-03-02

文中提到的书籍: 《Rust 程序设计语言》BK (中文), 《通过例子学 Rust》EX (中文), 《标准库文档》STD, 《Rust 秘典》NOM (中文), 《Rust 参考手册》REF (中文).

可点击凡例

BK 《Rust 程序设计语言》
EX 《通过例子学 Rust》
STD 《标准库文档》
NOM 《Rust 秘典》(死灵书)
REF 《Rust 参考手册》
RFC 官方 RFC 文档
🔗 外部链接
参见上文
参见下文

其他凡例

🗑️ 基本已废弃
'18 最低版本要求.
🚧 要求Rust nightly (或不完整).
🛑 故意的错误示例缺陷代码.
🝖 略显深奥, 很少使用的高级我.
🔥 常用特性
? 缺少链接或说明
💬 作者见解

字体连字 (..=, =>) 展开所有内容? 夜间模式 💡

语言结构

增强设施

数据布局

附录

标准库

工具

与类型打交道

编码指南

你好, Rust!#

如果你是 Rust 新手, 或者你想试点什么, 可以在下面尝试运行一下:

fn main() {
    println!("Hello, world!");
}
服务由 play.rust-lang.org 🔗 提供

Rust 擅长此事

你可能会遇到的问题

  • 学习曲线陡峭1. 其他场合下的“最佳实践”往往会被编译器教训一通(特别是内存方面).
  • 缺少某些领域的 Rust-native 库, 目标平台(尤其是嵌入式)以及 IDE 功能.1
  • 比起其他语言中“类似的”代码编译时间更长.1
  • 没有正式语言规范, 影响在某些领域(如航空, 医疗等)的合法使用.
  • 内部使用了 unsafe 的第三方库有可能会破坏安全性保证.

1 参见该 Rust 调查结果.

下载

  • rustup.rs 获取安装程序 (推荐用于任何平台)

IDE 编辑器

模块化初学者资源

另可参考常用文档. BK EX STD

作者按 💬 — 如果你从来没用过 Rust 最好还是先看看上面的教程, 本文后续章节对相关概念仅作简要说明, 不会深入讲解.

数据结构#

数据类型和内存位置由关键字定义.

示例说明
struct S {}定义包含命名字段的结构体BK EX STD REF.
     struct S { x: T }定义包含 T 类型命名字段 x 的结构体.
     struct S ​(T);定义 T 类型数字字段 .0 的“元组”结构体.
     struct S;定义一个零大小NOM的单元结构体. 已优化不占用任何空间.
enum E {}定义枚举BK EX REF, 参见数字数据类型标签联合.
     enum E { A, B(), C {} }定义变体枚举; 可以是单元- A, 元组 B ​() 或者结构体风格的 C{}.
     enum E { A = 1 }如果所有变体都是单元值, 则允许判别式值, 例如用于 FFI.
union U {}不安全的 C 风格联合体REF, 用于兼容 FFI. 🝖
static X: T = T();具有 'static 生命周期的全局变量 BK EX REF, 内存位置独立.
const X: T = T();定义常量BK EX REF, 使用时会临时复制一份.
let x: T;在栈1上分配 T 大小的字节并命名为 x. 一旦分配不可修改.
let mut x: T;类似于 let, 但具有可变性BK EX, 允许修改和可变借用2.
     x = y;y 移动到 x, 如果 T 不能 CopySTD, y 将不再可用, 否则会复制一份 y.

1 域变量 BK EX REF是生存在栈上的同步代码. 在async{}中, 这些变量将成为异步状态机的一部分, 最终可能是驻留在堆上.
2 注意术语_可变_和_不可变_并不准确. 不可变绑定或共享引用可能仍然包含 Cell STD, 从而提供_内部可变性_.

 

下面列出了如何构建和访问数据结构; 以及一些_神奇的_类型.

示例说明
S { x: y }构建 struct S {} 或者 useenum E::S {} 字段 x 设置为 y.
S { x }同上, 但字段 x 会被设置为局部变量 x.
S { ..s }s 填充剩余字段, 常配合 Default 一起使用.
S { 0: x }类似下面的 S ​(x), 但是用结构体语法设置字段 .0.
S​ (x)创建 struct S ​(T) 或者 useenum E::S​ () 其中字段 .0 设置为 x.
S表示 struct S; 或以 S 为值创建 use 来的 enum E::S.
E::C { x: y }构建枚举变体 C. 上面的其他方法依然有效.
()空元组, 既是字面量也是类型, 又称单元. STD
(x)括号表达式.
(x,)单元素元组表达式. EX STD REF
(S,)单元素元组类型.
[S]未指明长度的数组类型, 如切片. EX STD REF 不能生存在栈上. *
[S; n]元素类型为 S, 定长为 n数组类型 EX STD.
[x; n]nx 的副本构建的数组实例. REF
[x, y]由给定元素 xy构成的数据实例.
x[0]索引, 下标为 usize 类型. 可重载 IndexIndexMut.
     x[..]范围索引, 这里是_全部范围_, 又见下 x[a..b], x[a..=b], ...
a..b左闭右开区间 STD REF, 如 1..3 表示 1, 2.
..b无起点到右开区间 STD.
a..=b全闭合区间, STD 1..=3 表示 1, 2, 3.
..=b无起点到右闭合区间 STD.
..全包含区间STD, 常表示_整个组合_.
s.x命名 字段访问REF, 如果 x 不是类型 S 的一部分的话则会尝试 Deref.
s.0数字字段访问, 用于元组类型 S ​(T).

* 目前, 可以参考RFC中的已知问题.

引用 & 指针#

为非所有者内存赋予访问权限. 另请参见 泛型 & 约束.

示例说明
&S共享 引用 BK STD NOM REF (用于存储_任意_&s).
     &[S]特殊的切片引用, 包含地址和长度 (address, length).
     &str特殊的字符串引用, 包含地址和长度 (address, length).
     &mut S允许修改的独占引用 (参见 &mut [S], &mut dyn S, …).
     &dyn T特殊的 trait 对象 BK 引用, 包含地址和虚表 (address, vtable).
&s共享借用 BK EX STD ( s 的地址, 长度, 虚表等, 如 0x1234).
     &mut s具有可变性的独占借用. EX
*const S不可变的裸指针类型 BK STD REF, 内存不安全
     *mut S可变的裸指针类型, 内存不安全.
     &raw const s不通过引用创建裸指针, 见 ptr:addr_of!() STD 🚧 🝖
     &raw mut s同上, 但可变. 🚧 裸指针可用于未对齐的包装字段. 🝖
ref s引用绑定, EX 创建绑定的引用类型. 🗑️
     let ref r = s;等价于 let r = &s.
     let S { ref mut x } = s;可变引用绑定 (let x = &mut s.x), 解构的简写.
*r解引用 BK STD NOM 引用 r 以访问指针指向的内容.
     *r = s;如果 r 是一个可变引用, 则将 s 移动或复制到目标内存.
     s = *r;如果 rCopy, 则将 r 引用的内容复制到 s.
     s = *my_box;Box 有一个特例🔗, 即便它不可 Copy, 也仍会从 Box 里面移动出来.
'a生命周期参数, BK EX NOM REF, 为静态分析声明一块代码的持续时间.
     &'a S仅支持生存时间不短于 'a 的地址 s .
     &'a mut S同上, 但允许改变地址指向的内容.
     struct S<'a> {}表明 S 包含一个生命周期为 'a 的地址.由 S 的创建者决定 'a.
     trait T<'a> {}表明一个实现了 impl T for SS 可能会包含地址.
     fn f<'a>(t: &'a T)同上, 用于函数.调用者决定 'a.
'static特殊的生命周期, 生存在程序的整个执行过程中.

函数 & 行为#

定义代码单元及其抽象.

示例说明
trait T {}定义 trait BK EX REF, 它是一系列可被实现的通用行为.
trait T : R {}T父 trait REF R 的子 trait.任何要 impl TS 都必须先 impl R.
impl S {}类型 S 的函数实现 REF, 如方法.
impl T for S {}为类型 S 实现 trait T.
impl !T for S {}禁用自动推导的 auto trait. NOM REF 🚧 🝖
fn f() {}定义一个函数BK EX REF, 或在 impl 里关联一个函数.
     fn f() -> S {}同上, 但会返回一个 S 类型的值.
     fn f(&self) {}定义一个方法.BK EX例如, 在 impl S {} 中.
const fn f() {}编译期常量函数 fn.如 const X: u32 = f(Y). '18
async fn f() {}异步 REF '18 函数转写. 令 f 返回一个 impl Future. STD
     async fn f() -> S {}同上, 但令 f 返回 impl Future<Output=S>.
     async { x }用在函数内部, 使 { x } 变得 impl Future<Output=X>.
fn() -> S函数指针BK STD REF, 内存存放的可调用地址.
Fn() -> S可调用 TraitBK STD(又见 FnMutFnOnce), 可由闭包或函数等实现.
|| {} 闭包 BK EX REF , 将会借用它所有的捕获. REF (如局部变量).
     |x| {}有传入参数 x 的闭包.
     |x| x + x没有块表达式的闭包, 仅可由单个表达式组成.
     move |x| x + y 闭包, 将会获取它所有捕获的所有权.
     return || true 闭包, 看起来像是逻辑或, 但这里表示返回一个闭包.
unsafe不安全代码. BK EX NOM REF . 如果你喜欢在周五晚上调试段错误的话~
     unsafe fn f() {}意味着“调用会导致 UB, 必须检查依赖.”
     unsafe trait T {}意味着“不完善的 impl T 会导致 UB, 必需检查实现.”
     unsafe { f(); }向编译器保证“我已检查过依赖, 请相信我.”
     unsafe impl T for S {}保证“S 的行为确实符合 T”, 在 S 上使用 T 是安全的.

控制流程#

在函数中控制执行.

示例说明
while x {}循环REF, 当表达式 x 为真时运行.
loop {}无限循环REF直到 break. 可以用 break x 来 yield 一个值出来.
for x in iter {}迭代器上循环的语法糖.BK STD REF
if x {} else {}条件分支 REF. 如果表达式为真则...否则...
'label: loop {}循环标签 EX REF, 用于嵌套循环的流程控制.
breakBreak 表达式 REF, 用于退出循环.
     break x同上, 但将 x 作为循环表达式的值(仅在 loop 中有效).
     break 'label不单单退出的是当前循环, 而是最近一个标记有 'label 的循环.
     break 'label x同上, 但返回 x 作为闭包循环 'label 的值.
continue Continue 表达式 REF, 用于继续该循环的下一次迭代.
continue 'label同上, 但继续的是最近标记有 'label 的循环迭代.
x?如果 xErrNone, 返回并向上传播.BK EX STD REF
x.await仅在 async 中可用. yield 当前控制流直到 Future STD 或流 x 已就绪. REF '18
return x从函数中提前返回.然而以表达式结束的方式更惯用.
f()调用 f(如函数, 闭包, 函数指针或 Fn 等).
x.f()调用成员函数(方法), 要求 fself, &self 等作为第一个参数.
     X::f(x)x.f().除非 impl Copy for X {}, 否则 f 仅可调用一次.
     X::f(&x)x.f().
     X::f(&mut x)x.f().
     S::f(&x)x.f(), 仅当 X 实现了对 SDeref.这里 x.f() 会去找 S 的方法.
     T::f(&x)x.f(), 仅当 X impl T. 这里 x.f() 会去找作用域内 T 的方法.
X::f()调用关联函数, 比如 X::new().
     <X as T>::f()调用为 X 实现了的 trait 方法 `T::f().

代码组织#

将项目分割成更小的单元并最大限度地减少依赖关系.

示例说明
mod m {}定义模块BK EX REF, 其中的定义在 {} 内.
mod m;定义模块, 其中的定义在 m.rsm/mod.rs.
a::b命名空间路径EX REF, 表示 a(modenum 等) 里面的元素 b.
     ::b相对于当前 crate 根下搜索 b .🗑️
     crate::b相对于当前 crate 根下搜索 b.'18
     self::b相对于当前模块下搜索 b.
     super::b相对于父级模块下搜索 b.
use a::b;Use EX REF 声明, 将 b 直接引入到当前作用域, 以后就不需要再加 a 前缀了.
use a::{b, c};同上, 但同时将 bc 都引入.
use a::b as x;b 引入作用域但命名为 x. 比如 use std::error::Error as E.
use a::b as _;b 匿名的引入作用域, 用于含有冲突名称的 trait.
use a::*;a 里面的所有元素都引入作用域.仅推荐在 aprelude 的情况下使用.🔗
pub use a::b;a::b 引入作用域, 并再次从当前位置导出.
pub T控制 T可见性 BK.「如果父级路径公开, 我也公开」.
     pub(crate) T可见性仅1在当前 crate 内.
     pub(super) T可见性仅1在父级以下.
     pub(self) T可见性仅1在当前模块内.
     pub(in a::b) T可见性仅1a::b 内.
extern crate a;声明依赖一个外部 crate BK EX REF 🗑️. 换用 use a::b '18.
extern "C" {}声明 FFI 的外部依赖和 ABI(如 "C")BK EX NOM REF.
extern "C" fn f() {}定义 FFI 导出成 ABI(如 "C")的函数.

1 子模块中的总是可以访问所有项目, 与其是否是 pub 无关.

类型别名和转换#

类型名称的简写, 以及转为其他类型的方法.

示例说明
type T = S;创建类型别名BK REF. 这里表示 S 的另一个名字.
Self当前实现类型REF 的别名. 如 fn new() -> Self.
selffn f(self) {} 的方法主体. 同 fn f(self: Self) {}.
     &self同上, 但将借用指向自己的引用. 同 f(self: &Self).
     &mut self同上, 但是可变借用. 同 f(self: &mut Self).
     self: Box<Self>任意自型, 为智能指针增加方法 (my_box.f_of_self()).
S as T消歧义BK REF, 将类型 S 作为 trait T 看待. 比如 <X as T>::f().
S as Ruse 里, 将 S 导入为 R.如 use a::b as x.
x as u32转换EX REF, 会发生截断和一些比特上的意外.1 NOM

1 关于在类型之间转换的所有方法, 请参见下面的类型转换.

宏 & 属性#

实际编译前的代码预展开.

示例说明
m!() BK STD REF 咒语, 也作 m!{}, m![] (取决于宏本身)
#[attr]外部属性EX REF, 注解接下来的内容.
#![attr]内部属性, 注解_上部_, 周边的内容.
 
宏内写法说明
$x:ty宏捕获 (此处表示捕获一个类型), 详见工具链命令.
$x宏替换, 如使用上面的 $x:ty 捕获.
$(x),*宏重复“零次或若干次”.
     $(x),?宏重复“零次或一次”.
     $(x),+宏重复“一次或若干次”.
     $(x)<<+分隔符可以不是逗号“,”. 比如这里用 << 作为分割符.

模式匹配#

函数参数, matchlet 表达式中的构造.

示例说明
match m {}模式匹配BK EX REF, 下面跟匹配分支. 参见下表.
let S(x) = get();显然, let 也和下表的模式匹配类似.
     let S { x } = s;仅将 x 绑定到值 s.x.
     let (_, b, _) = abc;仅将 b 绑定到值 abc.1.
     let (a, ..) = abc;也可以将「剩余的」都忽略掉.
     let (.., a, b) = (1, 2);忽略前面「剩余的」, 这里 a1, b2.
     let s @ S { x } = get();s 绑定到 S 并将 x 绑定到 s.x, 模式绑定, BK EX REF 见下 🝖
     let w @ t @ f = get();存储 3 份 get() 结果的拷贝分别到 w, t, f. 🝖
     let Some(x) = get();不可用🛑, 因为模式可能会不匹配REF. 换用 if let.
if let Some(x) = get() {}如果模式匹配则执行该分支(如某个 enum 变体). 语法糖*.
while let Some(x) = get() {}等效; 这里继续调用 get(), 只要可以分配模式就运行 {}.
fn f(S { x }: S)类似于 let, 模式匹配也可用在函数参数上. 这里 f(s)x 被绑定到 s.x.🝖

* 展开后是 match get() { Some(x) => {}, _ => () }.

 

match 表达式的模式匹配分支. 左列的分支也可用于 let 表达式.

匹配分支说明
E::A => {}匹配枚举变体 A. 参见模式匹配.BK EX REF
E::B ( .. ) => {}匹配枚举元组变体 B, 通配所有下标.
E::C { .. } => {}匹配枚举结构变体 C, 通配所有字段.
S { x: 0, y: 1 } => {}匹配含特定值的结构体(仅匹配 ss.x0s.y1 的情况).
S { x: a, y: b } => {}匹配为任意(!)值的该类型结构体, 并绑定 s.xa, 绑定 s.yb.
     S { x, y } => {}同上, 但将 s.xs.y 分别简写地绑定为 xy.
S { .. } => {}匹配任意值的该类型结构体.
D => {}匹配枚举变体 E::D.仅当 D 已由 use 引入.
D => {}匹配任意事物并绑定到 D.如果 D 没被 use 进来, 怕不是个 E::D 的假朋友.🛑
_ => {}通配所有, 或者所有剩下的.
0 | 1 => {}可选模式列表, 或模式. RFC
     E::A | E::Z 同上, 但为枚举变体.
     E::C {x} | E::D {x}同上, 但如果所有变体都有 x 则绑定.
     Some(A | B)同上, 可以嵌套匹配.
(a, 0) => {}匹配元组, 绑定第一个值到 a, 要求第二个是 0.
[a, 0] => {}切片模式REF 🔗. 绑定第一个值到 a, 要求第二个是 0.
     [1, ..] => {}匹配以 1 开始的数组, 剩下的不管. 子切片模式.?
     [1, .., 5] => {}匹配以 1 开始以 5 结束的数组.
     [1, x @ .., 5] => {}同上, 但将 x 绑定到中间部分的切片上(见匹配绑定)
     [a, x @ .., b] => {}同上, 但可以指定任意上下界 a, b.
1 .. 3 => {}范围模式, BK REF 这里匹配 12. 尚不稳定. 🚧
     1 ..= 3 => {}闭区间范围模式, 匹配 1, 23.
     1 .. => {}开区间范围模式, 匹配 1 和更大的数字.
x @ 1..=5 => {}绑定匹配到 x, 即模式绑定BK EX REF. 这里 x 可以是 1, 2 直到 5.
     Err(x @ Error {..}) => {}嵌套使用, 这里 x 绑定到 Error, 下常跟 if.
S { x } if x > 10 => {}模式匹配条件BK EX REF. 该匹配会要求这个条件也为真.

泛型 & 约束#

泛型使得类型构造, trait 和函数更加可扩展.

示例说明
S<T>泛型BK EX, 类型参数 T 是占位符.
S<T: R>类型短 trait 约束BK EX说明. (R 必须 是个实际的 trait).
     T: R, P: S独立 trait 约束(这里一个对 T, 一个对 P).
     T: R, S编译错误🛑. 可以用下面的 R + S 代替.
     T: R + S合并 trait 约束BK EX. T 必须同时满足 RS.
     T: R + 'a同上, 但有生命周期. T 必须满足 R; 如果 T 有生命周期, 则必须长于 'a.
     T: ?Sized指定一个预定义的 trait 绑定, 如这里是 Sized. ?
     T: 'a类型生命周期约束EX. T 应长于 'a.
     T: 'static同上. 但 意味着值 t 🛑 活在 'static 上, 仅在它可以的时候才行.
     'b: 'a生命周期 'b 必须至少活得和 'a 一样长.
S<const N: usize>泛型常量绑定. ? 使用类型 S 可提供常量值 N.
     S<10>可以指定字面量.
     S<{5+5}>表达式需要用花括号包起来.
S<T> where T: R几乎等同于 S<T: R>, 但对于比较长的约束更容易阅读.
     S<T> where u8: R<T>还允许您创建涉及其他类型的条件语句.
S<T = R>默认参数. BK 保持扩展性的同时更易于使用.
     S<const N: u8 = 0>常量默认参数. 如 f(x: S) {} 中参数 N0.
     S<T = u8>类型默认参数. 如 f(x: S) {} 中参数 Tu8.
S<'_>推断匿名生命周期. 让编译器 “想办法” 明确生命周期.
S<_>推断匿名类型. 比如 let x: Vec<_> = iter.collect()
S::<T>TurbofishSTD 消歧义类型调用. 如 f::<u32>()
trait T<X> {}X 的 trait 泛型.可以有多个 impl T for S(每个 X 一个).
trait T { type X; }定义关联类型BK REF X. 仅可有一个 impl T for S .
     type X = R;设置关联类型. 仅在 impl T for S { type X = R; } 内.
impl<T> S<T> {}实现 S<T> 任意类型 T 的功能.
impl S<T> {}实现确定 S<T> 的功能. 如 S<u32>.
fn f() -> impl TExistential 类型 BK. 返回一个对调用者未知的但 impl TS.
fn f(x: &impl T)Trait 约束, 「impl traitBK.和 fn f<S:T>(x: &S) 有点类似.
fn f(x: &dyn T)动态分发标记BK REF. f 不再单态.
fn f() where Self: Rtrait T {} 中标记 f 仅可由实现了 impl R 的类型访问.
     fn f() where Self: Sized;使用 Sized 可以指定 f 对于 dyn T 的 trait 对象对应的虚表.
     fn f() where Self: R {}Other R useful w. dflt. methods (non dflt. would need be impl'ed anyway).

高阶项目 🝖#

实际的 类型和 trait, 某些事物的抽象以及常用生命周期.

示例说明
for<'a>高阶绑定NOM REF表记. 🝖
     trait T: for<'a> R<'a> {}在任意生命周期下, 任意实现了 impl TS 都应满足 R.
fn(&'a u8)函数指针 类型, 持有可调用 fn 以及指定的生命周期 'a.
for<'a> fn(&'a u8)高阶类型1 🔗 持有可调用 fn 任意 小于 上述生命周期的参数; 上面的子类型.
     fn(&'_ u8)同上, 自动展开为类型 for<'a> fn(&'a u8).
     fn(&u8)同上, 自动展开为类型 for<'a> fn(&'a u8).
dyn for<'a> Fn(&'a u8)高阶 (trait 对象) 类型, 行为如上 fn.
     dyn Fn(&'_ u8)同上, 自动展开为类型 dyn for<'a> Fn(&'a u8).
     dyn Fn(&u8)同上, 自动展开为类型 dyn for<'a> Fn(&'a u8).

1 没错, for<> 是类型的一部分, 这会导致你下面会写出来 impl T for for<'a> fn(&'a u8) 这样的代码.

 
Trait 实现说明
impl<'a> T for fn(&'a u8) {}For fn 指针, 调用接受指定 小于 'a 的参数, impl trait T.
impl T for for<'a> fn(&'a u8) {}For fn 指针, 调用接受任意 小于 的参数, impl trait T.
     impl T for fn(&u8) {}同上, 简写.

字符串 & 字符#

Rust 提供了若干种创建字符串和字符字面量的办法.

示例说明
"..."UTF-8 字符串字面量REF, 1.会将 \n 等看作换行 0xA 等.
r"..."UTF-8 裸字符串字面量REF, 1. 不会处理 \n 等.
r#"..."#UTF-8 裸字符串字面量. 但可以包含 ".
b"..."字节串字面量REF, 1, 由 ASCII [u8] 组成. 并不是字 串.
br"...", br#"..."#裸字节串字面量, ASCII [u8]. 说明见上.
'🦀'字符字面量REF, 固定的 4 字节 Unicode '字符'.STD
b'x'ASCII 字节字面量.REF

1 均支持多行字符串. 但要注意 Debug (例如 dbg!(x)println!("{x:?}")) 会将换行符渲染成 \n, 而 Display (例如 println!("{x}")) 则会输出换行.

文档#

调试器的天敌. 这玩意儿能避免 Bug.

示例说明
///外部行级文档注释, BK EX REF 用于类型, trait, 函数等.
//!内部行级文档注释, 多用于文档模块的文件头部.
//行内注释. 用于文档代码流内或 内部组件.
/*...*/块级注释.
/**...*/外部块级文档注释.
/*!...*/内部块级文档注释.

工具链命令告诉你可以在文档注释中做什么.

杂项#

这些小技巧不属于其他分类但最好了解一下.

示例说明
!永远为空的 never 类型.🚧 BK EX STD REF
_无名变量绑定.如 |x, _| {}.
     let _ = x;匿名赋值等于无操作 (no-op), 不会🛑x 移出当前作用域!
_x变量绑定, 明确标记该变量未使用.
1_234_567为了易读加入的数字分隔符.
1_u8数字字面量的类型说明符.EX REF (又见 i8, u16等).
0xBEEF, 0o777, 0b1001十六进制(0x), 八进制(0o)和二进制(0b) 整型字面量.
r#foo原始标识符 BK EX. 用于版本兼容. 🝖
x;语句REF终止符. 见表达式EX REF.

通用运算符#

Rust 支持大部分其他语言也有的通用操作符(+, *, %, =, ==...). 因为这在 Rust 里没什么太大差别所以这里不列出来了. Rust 也支持运算符重载.STD


增强设施#

可能会导致你的脑子爆炸的神秘知识点, 超级推荐.

抽象层#

C/C++, Rust 基于一个 抽象层.

Rust CPU
🛑 不太优雅
Rust 抽象层 CPU
优雅
 

抽象层 (AM)

  • 并非运行时, 并不会有任何运行时开销, 但它是一个 计算模型的抽象,
  • 包含如内存分配(, ...)和运行语义等概念,
  • 了解看到 你 CPU 并不关心的东西,
  • 构建了程序员到机器之间的一道契约,
  • 并且综合上述内容进行优化.

如果 Rust 直接发送给 CPU, 人们可能会错误地认为他们 应该逃脱惩罚 , 然而 更加正确 的做法是:

 
无抽象层有抽象层
0xffff_ffff 会产生一个有效的 char. 🛑只是内存中的一段比特.
0xff0xff 是相同的指针. 🛑指针可以来自不同的 .
0xff 的任意读写都是可行的. 🛑不能同时读写同一引用.
某些寄存器直接把 0x0 当做 null. 🛑在引用中保存 0x0 简直是克苏鲁.

语法糖#

如果有什么东西让你觉得, “不该能用的啊”, 那可能就是这里的原因.

名称说明
强转 NOM隐式转换 类型以匹配签名, 如 &mut T 转为 &T. 见 类型转换.
解引用 NOM 🔗连续解引用 x: T 直到 *x, **x, … 满足目标类型 S.
Prelude STD自动导入基本项目, 如 Option, drop, ...
重新借用即便 x: &mut T 不能复制, 也可以移动一个新的 &mut *x 代替.
生命周期省略 BK NOM REF自动将 f(x: &T) 标注为 f<'a>(x: &'a T).
方法重解析 REF解引用或借用 x 直到 x.f() 可用.
匹配引用简写 RFC重复应用解引用到各个选择肢上并添加 refref mut 到绑定.
右值静态提升 RFC使引用满足 'static, 如 &42, &None, &mut [].
 

作者按 💬 — 上述功能会让你活得轻松些, 但却会扰乱你的理解. 如果任意有类型相关的操作让你觉得 有些反常, 那可能就是这里的语法糖在作怪了.

内存和生命周期#

移动, 引用和生命周期到底是咋回事.

应用程序内存 S(1) 应用程序内存
  • 应用程序内存在底层就是一个字节数组.
  • 操作系统经常将其划分为若干分区:
    • 栈区 (空间小, 低成本内存,1 多数 变量 都在这里),
    • 堆区 (空间大, 可扩展内存, 但总会由类似 Box<T> 的栈代理来指向),
    • 静态区 (多用于存储组成 &str 的字符串 str),
    • 代码区 (你的函数二进制代码的存储区域).
  • 这里面最富挑战的莫过于栈的增长, 这是我们关注的重点.

1 对于固定大小的值, 栈的管理非常细致: 你需要的时候马上生成, 不需要的时候马上离开. 然而这些 短暂 分配的指针却是导致生命周期存在的 本质 原因, 也是主导了本章后续所有内容.

变量 S(1) S(1) 变量
let t = S(1);
  • 分配内存空间, 名为 t, 类型为 S, 里面存储的值为 S(1).
  • 如果声明了 let 那空间将会分配在栈上. 1
  • 注意语义歧义,2 术语变量可能指的是:
    1. 源文件中定义的名称 (“重命名某变量”),
    2. 已编译程序中的位置, 0x7 (“告诉我某变量的地址”),
    3. 里面包含的, S(1) (“增加某变量”).
  • 特别地, 对于编译器来说 t 指的是 t位置 (这里是 0x7) 和 t 里面的 (这里是 S(1)).

1 上述比较中仅针对于同步代码, 而 async 异步栈帧有可能被运行时放在堆上.

移动语义 S(1) 移动
let a = t;
  • 操作将移动 t 里面的值到 a 的位置, 如果 S 是可 Copy 的则复制一份.
  • t 的位置移动后将会失效且不能再被读取.
    • 技术上该位置的比特位并非完全置为 , 但 未定义.
    • 如果你仍然通过 unsafe 访问 t 的话它仍有可能 看起来 像是个有效的 S, 但任何把它当成有效 S 的操作都是未定义行为 (UB).
  • 这里没有提到 Copy 的影响, 虽然它会轻微影响上述规则:
    • 它们不会被析构.
    • '空'变量的位置永远不会离开作用域.
类型安全 M { ... } 类型安全
let c: S = M::new();
  • 变量的类型指出了许多重要的期望, 它:
    1. 规定了如何解释底层的比特位,
    2. 仅允许被友好定义的操作去操作这些比特位,
    3. 防止其他随机变量或比特写到这个位置.
  • 这里赋值语句将会编译失败, 因为 M::new() 的字节无法被有效地转换为 S 类型.
  • 类型之间的直接转换 总会 失败. 但通常允许某些例外 (强转或 as 转换等).
域 & 析构 S(1) C(2) S(2) S(3) 作用域 & 析构
{
    let mut c = S(2);
    c = S(3);  // <- 赋值前将会对 `c` 进行析构.
    let t = S(1);
    let a = t;
}   // <- 这里退出了 `a`, `t`, `c` 的作用域, 将调用 `a`, `c` 的析构方法.
  • 一旦一个未腾出空间的“变量名”离开其作用域, 其包含的值将被析构.
    • 首要规则: 当变量名离开其定义的 {} 块时执行,
    • 临时变量等尤为如此.
  • 当新值赋值给已有变量位置时也会触发析构.
  • 当调用了值对应位置的 Drop::drop() 时.
    • 上例中 drop()a 上调用了一次, 在 c 上调用了两次, 但没有在 t 上调用过.
  • 多数非 Copy 的值都在此时发生析构, 除非使用了 mem::forget(), Rc 之类或者 abort().
栈帧 S(1) 函数边界
fn f(x: S) { ... }

let a = S(1); // <- 在这里
f(a);
  • 函数被调用, 参数和返回值的内存将会保存在栈上.1
  • 这里当调用 f 之前, a 中的值将会移动到“商量”好的栈位置, 当 f 执行时作为“局部变量” x.

1 实际的位置取决于调用时的转换, 可能根本不会分配在栈上, 但这不影响此处的心智模型.

S(1) 嵌套函数
fn f(x: S) {
    if once() { f(x) } // <- 递归前在这里
}

let a = S(1);
f(a);
  • 函数的递归调用, 或由其他函数调用, 都将会扩展栈帧.
  • 过多的嵌套调用 (比如无限递归) 会使得栈不断增长, 以至于溢出并导致程序终止.
变量有效性 S(1) M { } 内存重定义
fn f(x: S) {
    if once() { f(x) }
    let m = M::new() // <- 递归后在这里
}

let a = S(1);
f(a);
  • 之前保有确定类型的栈将由函数或内部函数重新定义此处的用途.
  • 这里 f 的递归产生的第二个 x, 将会在后续的递归里由 m 重新利用.

关键点在于, 有很多种方法来保证之前保有一个确定类型有效值内存位置在此期间不被使用. 简单来说就是实现了指针.

引用类型   S(1) 0x3 引用作为指针
let a = S(1);
let r: &S = &a;
  • 类似 &S&mut S 这样的引用类型持有某个 s位置.
  • 这里类型 &S 绑定到了名称 r, 持有变量 a (0x3) 的 位置, 它的类型为 S, 通过 &a 获取.
  • 如果你将变量 c 视为 指定位置, 引用 r 就是 位置的接入点.
  • 引用的类型与其他类型类似, 通常可以被推断出来, 所以可以省略不写:
    let r: &S = &a;
    let r = &a;
    
(可变) 引用   S(2) 0x3 S(1) 访问共享内存
let mut a = S(1);
let r = &mut a;
let d = r.clone();  // 从 r 指向目标克隆或拷贝.
*r = S(2);          // 将 r 指向目标设置为新值 S.
  • 引用可以读取自 (&S) 或写入到 (&mut S) 它们指向的位置.
  • 解引用 *r 表示既不使用 r位置 也不使用其包含的 , 而是使用位置 r 指向的目标.
  • 上例中, 克隆 d 是由 *r 创建的, 并且 S(2) 写入到了 *r.
    • 方法 Clone::clone(&T) 期望传入其自身的引用, 这就是我们使用 r 而非 *r 的原因.
    • 赋值语句 *r = ... 中的旧值也会被析构 (图中未说明).
  S(2) 0x3 M { x } 引用对象保护
let mut a = ...;
let r = &mut a;
let d = *r;       // 无法移出值, 否则 `a` 将为空.
*r = M::new();    // 无法存储非 S 类型值, 毫无意义.
  • 当绑定保证总是 持有 有效值时, 引用也总是保证一定 指向 有效数据.
  • 特别是 &mut T 必须和变量一样提供保证, 要知道它们并不能让指向的目标消失:
    • 不允许写无效数据.
    • 不允许移出数据 (否则会留下一个不知道所有者的空目标).
  C(2) 0x3 裸指针
let p: *const S = questionable_origin();
  • 与引用不同, 指针不提供任何保证.
  • 指针有可能指向无效数据或者不存在的数据.
  • 解引用指针是 unsafe 的, 将无效的 *p 作为有效值来操作是未定义行为 (UB).
C(2) 0x3 事物的“生命周期”
  • 程序中的每个实体都有其相关的临时或者长期的空间, 即 生存.
  • 宽松地说, 生存时间 可以是1
    1. 项目可用代码行 (LOC). (如模块名).
    2. 位置初始化到位置被丢弃之间的代码行.
    3. 从位置第一次确定性使用停止使用之间的代码行.
    4. 从创建 到该值被析构之间的代码行 (或实际时间).
  • 本节剩余部分会将上面的项目分别称为:
    1. 项目作用域, 不重要.
    2. 变量或位置的作用域.
    3. 用法的生命周期2.
    4. 值的生命周期, 当和文件描述符打交道时非常有用, 但这里也不重要.
  • 同样地, 代码中的生命周期参数 r: &'a S
    • 任意位置 r 指向 的代码行导致的未确定态都要求可访问或被锁定;
    • r 本身作为代码行的“存在时间”并无关联 (它只要存在得更短就行了).
  • &'static S 意味着地址必须 在所有代码行中有效.

1 文档中的 作用域生命周期 有时存在歧义. 这里尝试作一定的区分, 但如果有更好的意见也欢迎提出.

2 生存行 可能是个更好的说法...

  S(0) S(1) S(2) 0xa r: &'c S 的含义
  • 假设你从哪里看到 r: &'c S, 它表示:
    • r 持有某个 S 的地址,
    • r 指向的任意地址会至少存在在 'c 期间,
    • 变量 r 本身不能活得比 'c 长.
  S(0) S(3) S(2) 0x6 生命周期与类型的相似性
{
    let b = S(3);
    {
        let c = S(2);
        let r: &'c S = &c;      // 不能如愿运行
        {                       // 因为函数体中的局部变量无法命名生命周期
            let a = S(0);       // 函数中的规则也类似

            r = &a;             // `a` 的位置在很多行都不存在 -> 不行.
            r = &b;             // `b` 的位置活在比 `c` 更大的范围 -> 可以.
        }
    }
}
  • 假设你在哪里看到了 mut r: &mut 'c S.
    • 它表示一个可以持有一个可变引用的可变地址.
  • 如上述, 引用必须保证目标内存有效.
  • 'c 部分和类型一样限制了赋值到 r 的操作.
  • &b (0x6) 赋值到 r 是有效的, 但 &a (0x3) 无效, 就因为 &b 生存时间大于等于 &c.
  S(0)   S(2) 0x6 S(4) 借用态
let mut b = S(0);
let r = &mut b;

b = S(4);   // 无效. 因为 `b` 处于借用态.

print_byte(r);
  • 一旦变量地址被 &b&mut b 捕获, 变量就会被标记为已借用.
  • 借用时, 就不能再通过原始绑定 b 修改地址的内容.
  • 一旦捕获了地址的 &b&mut b 在上下文中不再使用, 原始绑定 b 将会恢复可用.
S(0) S(1) S(2) ? 0x6 0xa 函数参数
fn f(x: &S, y:&S) -> &u8 { ... }

let b = S(1);
let c = S(2);

let r = f(&b, &c);
  • 调用函数时会捕获返回的引用, 这里将会发生两件趣事:
    • 用到的局部变量将会置为借用态,
    • 但编译期间并不知道返回值的地址.
S(0) S(1) S(2) ? 0x6 0xa “借用态”传播的问题
let b = S(1);
let c = S(2);

let r = f(&b, &c);

let a = b;   // 这样做可行吗?
let a = c;   // 谁才是真正被借用的?

print_byte(r);
  • 因为 f 只能返回一个地址, 所以并不是所有情况下 bc 都需要保持锁定态.
  • 多数情况下我们是有更好的办法解决这个问题的.
    • 特别是当我们知道一个参数 不能 再被用于返回值的时候.
  S(1) S(1) S(2) y + _ 0x6 0xa 借用态传播生命周期
fn f<'b, 'c>(x: &'b S, y: &'c S) -> &'c u8 { ... }

let b = S(1);
let c = S(2);

let r = f(&b, &c); // 我们知道返回的引用是基于 `c` 的, 它必须保持锁定;
                   // 然而 `b` 却是可以自由移动的.

let a = b;

print_byte(r);
  • 解决这个问题的办法就是签名中的生命周期参数 (比如上面的 'c).
  • 它们的主要作用是:
    • 函数外会用于描述生成的结果基于哪个输入地址和输出地址,
    • 函数内来保证只有生存时间低于 'c 的地址允许被赋值.
  • 实际的生命周期 'b, 'c 会基于开发者给出的借用态变量被编译器透明地指派给调用方.
  • 它并不等同于 bc作用域 (可能是从初始化到结束之间的代码行), 但仅有一个最小子集可以作为该作用域的 生命周期, 即基于 bc 需要借用于该调用和保存结果的最少代码行.
  • 某些如 f'c: 'b 替代, 仍不能区分出来的情况下, 两个都会保持锁定.
S(2) S(1) S(2) y + 1 0x6 0xa 解锁
let mut c = S(2);

let r = f(&c);
let s = r;
                    // <- 不是这里, `s` 会延长 `c` 的锁定时间.

print_byte(s);

let a = c;          // <- 是这里, 不再使用 `r` 和 `s`.


  • 一旦任意引用最后指向了结束, 变量位置将会再次 解锁.

↕️ 点击展开用例

 

数据类型#

通用数据类型的内存表示.

基本类型#

语言核心内建的必要类型.

数字类型 REF#

u8, i8 u16, i16 u32, i32 u64, i64 u128, i128 f32 f64 usize, isize 与平台的 ptr 一致.
 
类型最大值
u8255
u1665_535
u324_294_967_295
u6418_446_744_073_709_551_615
u128340_282_366_920_938_463_463_374_607_431_768_211_455
usize取决于平台指针大小, 可以是 u16, u32, 或 u64.
类型最大值
i8127
i1632_767
i322_147_483_647
i649_223_372_036_854_775_807
i128170_141_183_460_469_231_731_687_303_715_884_105_727
isize取决于平台指针大小, 可以是 i16, i32, 或 i64.
 
类型最小值
i8-128
i16-32_768
i32-2_147_483_648
i64-9_223_372_036_854_775_808
i128-170_141_183_460_469_231_731_687_303_715_884_105_728
isize取决于平台指针大小, 可以是 i16, i32, 或 i64.

f32 的位表示*:

S E E E E E E E E F F F F F F F F F F F F F F F F F F F F F F F
 

说明:

f32S (1)E (8)F (23)
规格化数±1 to 254任意±(1.F)2 * 2E-127
非规格化数±0非零±(0.F)2 * 2-126
±00±0
无穷大±2550±∞
NaN±255非零NaN
 

同样, 对于 f64 类型, 这将类似于:

f64S (1)E (11)F (52)
规格化数±1 to 2046任意±(1.F)2 * 2E-1023
非规格化数±0非零±(0.F)2 * 2-1022
±00±0
无穷大±20470±∞
NaN±2047非零NaN
* 浮点类型遵循 IEEE 754-2008 规范, 并取决于平台大小端序.
转换1结果说明
3.9_f32 as u83截断, 请优先使用 x.round().
314_f32 as u8255采用最接近的可用数字.
f32::INFINITY as u8255同上, 但会把 INFINITY 当做一个 真正的 大数.
f32::NAN as u80-
_314 as u858截断多余的位.
_200 as i856-
_257 as i8-1-
操作1结果说明
200_u8 / 0_u8编译错误.-
200_u8 / _0 dPanic.由于除以 0, 该计算会 panic.
200_u8 / _0 rPanic.同上.
200_u8 + 200_u8编译错误.-
200_u8 + _200 dPanic.考虑换用 checked_, wrapping_ 等方法 STD
200_u8 + _200 r144在 release 模式下会溢出.
1_u8 / 2_u80整数除法会截断.
0.8_f32 + 0.1_f320.90000004-
1.0_f32 / 0.0_f32f32::INFINITY-
0.0_f32 / 0.0_f32f32::NAN-
x < f32::NANfalseNAN 的比较结果永远为假.
x > f32::NANfalse-
f32::NAN == f32::NANfalse-

1表达式 _100 表示可能包含 100 值的任何内容, 例如 100_i32, 但对编译器是不透明的.
d 调试版本.
r 发布版本.

 

文本类型 REF#

char 任意 UTF-8 标量. str ... U T F - 8 ...未指明条目 很少单独见到, 常用 &str 代替.
 
类型描述
char总是为 4 字节, 且仅包含一个 Unicode 标量值🔗.
str未知长度的 u8 数组保证保存 UTF-8 编码的码位.
字符描述
let c = 'a';通常一个 char (Unicode 标量) 就是你直觉上认为的 字符.
let c = '❤';它可以持有很多 Unicode 符号.
let c = '❤️';但并不总是如此. 比如一个 emoji 是由两个 char (参见编码) 组成的, 并不能🛑存在 c 里.1
c = 0xffff_ffff;字符也不允许🛑用一个随便的比特模式就表示了.
1 有趣的是, 零宽连字 (⨝) 会让用户把这些连起来看起来像个字符: 👨‍👩‍👧 实际上是由 👨⨝👩⨝👧 这 5 个字符组成的, 渲染引擎也可以把它们显示成一个字符, 也可以分开显示成三个, 这取决于平台的能力.
 
字符串描述
let s = "a";通常并不会直接使用 str, 而是像这里的 s 一样通过 &str 访问.
let s = "❤❤️";可以存储任意长度的文本, 但很难进行索引.

let s = "I ❤ Rust";
let t = "I ❤️ Rust";

变体内存表示2
s.as_bytes()49 20 e2 9d a4 20 52 75 73 74 3
s.chars()149 00 00 00 20 00 00 00 64 27 00 00 20 00 00 00 52 00 00 00 75 00 00 00 73 00
t.as_bytes()49 20 e2 9d a4 ef b8 8f 20 52 75 73 74 4
t.chars()149 00 00 00 20 00 00 00 64 27 00 00 0f fe 01 00 20 00 00 00 52 00 00 00 75 00
 
1 结果会转为字节数组.
2 在 x86 平台上的十六进制表示.
3 注意 对应一个 Unicode 代码点 (U+2764), 它在 char 中被表示为 64 27 00 00, 但在 str 中则被表示为 UTF-8 编码 e2 9d a4.
4 注意 emoji 红心 ❤️ 其实是由心形 U+FE0F Variation Selector 组成的, 可以看到 ts 拥有更多字符.
 

💬 尽管上面的 st 是不一样的, 但 Safari 和 Edge 都有把脚注 3 和 4 的心形符号渲染错误的 Bug.

 

自定义类型#

用户定义的基本类型. 它实际的内存布局REF取决于表示法REF, 还有对齐.

T T 有大小 T: ?Sized T 也许是 DST [T; n] T T T ... n 次 固定的 n 个元素的数组. [T] ... T T T ...未指定条目 未知多元素的切片类型.
既非 Sized (不携带 len 信息) ,
多数情况下以 &[T] 为参照.
struct S; 零大小 (A, B, C) A B C 或者也可能 B A C 除非强制表示 (例如通过 #[repr(C)])
否则类型布局未指定.
struct S { b: B, c: C } B C 或者也可能 C B 编译器还可以添加填充.

还需注意, 具有完全相同字段的两种类型 A(X, Y)B(X, Y) 仍然可以具有不同的布局. 在没有使用 #[repr()] 限制其布局表示的情况下, 绝不能使用 transmute() 进行类型转换.

 

这些合并类型存有其一种子类型的值:

enum E { A, B, C } 标签 A 排他性或 标签 B 排他性或 标签 C 安全地保存 A, B 或 C.
又名“标签联合”, 尽管编译器会忽略标签.
union { ... } A 不安全或 B 不安全或 C 不安全地以多种方式解释同一块内存.
结果可能是未定义的.

引用 & 指针#

引用授权了对其他内存空间的安全访问. 裸指针则是不安全 unsafe 的访问. 各自的 mut 类型是相同的.

&'a T ptr2/4/8 meta2/4/8 | T 必须定位一些有效 tT,
并且任何这样的目标必须 至少存在'a.
*const T ptr2/4/8 meta2/4/8 没有任何保证.

元指针#

许多引用和指针类型可以携带一个额外的字段, 即元数据指针STD. 它可以是目标的元素长度或字节长度, 也可以是指向 vtable 的指针. 带有元数据的指针称为胖指针, 否则称为瘦指针.

&'a T ptr2/4/8 | T 没有大小目标的元 (瘦指针). &'a T ptr2/4/8 len2/4/8 | T T 是 DST struct (如 S { x: [u8] }),
元字段 len 则是动态大小内容的长度.
&'a [T] ptr2/4/8 len2/4/8 | ... T T ... 通常省略 'a切片引用
(即切片类型 [T] 的引用类型)
常表示为 &[T].
&'a str ptr2/4/8 len2/4/8 | ... U T F - 8 ... 字符串切片引用 (即字符串
类型 str 的引用),
元字段 len 即为字节长度.

&'a dyn Trait ptr2/4/8 ptr2/4/8 | T |
*Drop::drop(&mut T)
size
align
*Trait::f(&T, ...)
*Trait::g(&T, ...)
元指针指向虚表, 其中 *Drop::drop(), *Trait::f() 等是它们各自 impl for T 的指针.

闭包#

闭包是一个临时函数, 定义闭包时, 它会自动管理数据捕获REF环境中访问的内容. 例如:

move |x| x + y.f() + z Y Z 匿名闭包类型 C1 |x| x + y.f() + z ptr2/4/8 ptr2/4/8 匿名闭包类型 C2 | Y | Z

生成匿名函数 fnfc1(C1, X) or fc2(&C2, X). 具体细节取决于捕获类型的属性支持 FnOnce, FnMut 还是 Fn .等.

标准库类型#

Rust 标准库为上面提到的基本类型扩展了更多有用的类型, 并定义了一些特殊的语义. 一些通用类型如下:

UnsafeCell<T> T 魔术类型, 允许
别名可变性.
Cell<T> T 允许 T
移动进出.
RefCell<T> borrowed T 允许 T 的动态借用.
比如 CellSend 的,
但不是 Sync 的.
AtomicUsize usize2/4/8 其他原子类型类似 Result<T, E> 标签 E 或者 标签 T Option<T> 标签 或者 标签 T 对于确定的类型 T 可以省略
标签. 比如 NonNull.
 

通用堆存储器#

Box<T> ptr2/4/8 meta2/4/8 | T 对某些 T 栈代理可能会持有
元数据 (比如 Box<[T]>).
Vec<T> ptr2/4/8 capacity2/4/8 len2/4/8 |
T T ... len
capacity
 

所有字符串#

String ptr2/4/8 capacity2/4/8 len2/4/8 |
U T F - 8 ... len
capacity
观察 String&str 以及 &[char] 的区别.
CString ptr2/4/8 len2/4/8 |
A B C ... len ...
以空字符结束, 但中间没有空字符.
OsString ? 平台定义 |
? ? / ? ?
操作系统对字符串表示的直接封装.
(比如 Windows 上的 UTF-16).
PathBuf ? OsString |
? ? / ? ?
操作系统对路径表示的直接封装.
 

共享所有权#

如果类型 T 不包含 Cell, 那它也会包含以下 Cell 类型的变体以允许共享实际可变性.

Rc<T> ptr2/4/8 meta2/4/8
| strng2/4/8 weak2/4/8 T
在同一个线程上共享 T 的所有权. 需要嵌套 Cell
RefCell 以允许修改. 它既不是 Send 也不是 Sync 的.
Arc<T> ptr2/4/8 meta2/4/8
| strng2/4/8 weak2/4/8 T
同左. 但如果 T 是 SendSync 的, 则允许在线程间共享.

Mutex<T> / RwLock<T> ptr2/4/8 poison2/4/8 T | lock 需要包在 Arc 里以便在线程间共享,
总是 SendSync 的.
可用 parking_lot 替代 (快且不占用堆).

标准库#

基本准则#

这些代码片段很通用但经常容易忘. 详情可以参考 Rust Cookbook 🔗.

用途代码
连接字符串 (任何实现了 Display 的类型). 1 '21format!("{x}{y}")
以给定匹配分割字符串. STD 🔗s.split(pattern)
     ... 以 &strs.split("abc")
     ... 以 chars.split('/')
     ... 以闭包s.split(char::is_numeric)
以空白分割.s.split_whitespace()
以换行分割.s.lines()
以正则表达式分割.2 Regex::new(r"\s")?.split("one two three")

1 会产生内存分配. 如果 x 已经是 String 的情况下可能不是性能的最优解.
2 依赖 regex crate.

用途代码
创建新文件File::create(PATH)?
     同上, 但给出选项OpenOptions::new().create(true).write(true).truncate(true).open(PATH)?
用途代码
具有变量参数的宏macro_rules! var_args { ($($args:expr),*) => {{ }} }
     应用 args, 如多次调用 f.     $( f($args); )*
用途代码
清理闭包捕获wants_closure({ let c = outer.clone(); move || use_clone(c) })
修复 'try' 闭包内的类型推断iter.try_for_each(|x| { Ok::<(), Error>(()) })?;
T 满足 Copy 时, 迭代 修改 &mut [T]Cell::from_mut(mut_slice).as_slice_of_cells()
给定长度的切片&original_slice[offset..][..length]
确保 trait T 是对象安全的写法const _: Option<&dyn T> = None;

线程安全#

Send*!Send
Sync*多数类型 ... Mutex<T>, Arc<T>1,2MutexGuard<T>1, RwLockReadGuard<T>1
!SyncCell<T>2, RefCell<T>2Rc<T>, &dyn Trait, *const T3, *mut T3

* T: Send 表示实例 t 可以移动到另一个线程; T: Sync 表示 &t 可以移动到另一个线程.
1 如果 TSync.
2 如果 TSend.
3 如果你要发送一个裸指针, 建议创建新类型 struct Ptr(*const u8)unsafe impl Send for Ptr {}. 用来保证你 可能 会发送它 (到其他线程).

迭代器#

基础

假设有一个元素类型都为 C 的集合 c:

  • c.into_iter() — 将集合 c 转为一个**Iterator** STD i消费掉* c. 要求实现 CIntoIterator STD, 其元素类型取决于 C. 这是获取迭代器的“标准方式”.
  • c.iter() — 对某些集合更友好的方法, 返回一个借用迭代器而不消费掉 c.
  • c.iter_mut() — 同上, 但返回一个允许修改集合元素的可变借用迭代器.

迭代

一旦你获得了一个 i:

  • i.next() — 如果下一个元素 c 存在则返回 Some(x), 否则返回 None 表示结束.

循环

  • for x in c {} — 语法糖, 相当于调用 c.into_iter() 并且循环 i 直到它变为 None.

* 当类型是 Copy 的时, 迭代器会看起来并没有消费掉 c。比如, 调用 (&c).into_iter() 会在 &c 上调用 .into_iter() (会消费掉该引用并返回一个迭代器), 但本质上并没有去访问 c.

基础

假设有一集合 struct Collection<T> {}.

  • struct IntoIter<T> {} — 创建一个持有自定义迭代状态 (比如下标) 的结构体.
  • impl Iterator for IntoIter {} — 实现能够产生元素的 Iterator::next().
Collection<T> IntoIter<T> Iterator Item = T;

共享和可变迭代器

  • struct Iter<T> {} — 创建一个持有 &Collection<T> 的结构体用于共享迭代.
  • struct IterMut<T> {} — 类似地, 但持有 &mut Collection<T> 用于可变迭代.
  • impl Iterator for Iter<T> {} — 实现共享迭代.
  • impl Iterator for IterMut<T> {} — 实现可变迭代.

另外, 建议实现如下方法以获取对应迭代器:

  • Collection::iter(&self) -> Iter,
  • Collection::iter_mut(&mut self) -> IterMut.
Iter<T> Iterator Item = &T; IterMut<T> Iterator Item = &mut T;

实现循环

  • impl IntoIterator for Collection {} — 使得 for x in c {} 可用.
  • impl IntoIterator for &Collection {} — 使得 for x in &c {} 可用.
  • impl IntoIterator for &mut Collection {} — 使得 for x in &mut c {} 可用.
Collection<T> IntoIterator Item = T; To = IntoIter<T> T 上迭代. &Collection<T> IntoIterator Item = &T; To = Iter<T> &T 上迭代. &mut Collectn<T> IntoIterator Item = &mut T; To = IterMut<T> &mut T 上迭代.

数字转换#

目前正确的数字转换.

↓ 原始 / 目标 →u8i128f32 / f64String
u8i128u8::try_from(x)? 1x as f32 3x.to_string()
f32 / f64x as u8 2x as f32x.to_string()
Stringx.parse::<u8>()?x.parse::<f32>()?x

1 如果是其类型的真子集, from() 将会直接转换, 比如 u32::from(my_u8).
2 见下, 这些转换将会截断 (11.9_f32 as u8 得到 11) 或缩容 (1024_f32 as u8 得到 255).
3 转换后会重新用二进制位表示 (u64::MAX as f32) 或产生无穷大 Inf (u128::MAX as f32).

字符串转换#

下面列出要转换到目标字符串类型的方法:

原始类型 x转换方法
Stringx
CStringx.into_string()?
OsStringx.to_str()?.to_string()
PathBufx.to_str()?.to_string()
Vec<u8> 1String::from_utf8(x)?
&strx.to_string() i
&CStrx.to_str()?.to_string()
&OsStrx.to_str()?.to_string()
&Pathx.to_str()?.to_string()
&[u8] 1String::from_utf8_lossy(x).to_string()
原始类型 x转换方法
StringCString::new(x)?
CStringx
OsString 2CString::new(x.to_str()?)?
PathBufCString::new(x.to_str()?)?
Vec<u8> 1CString::new(x)?
&strCString::new(x)?
&CStrx.to_owned() i
&OsStr 2CString::new(x.to_os_string().into_string()?)?
&PathCString::new(x.to_str()?)?
&[u8] 1CString::new(Vec::from(x))?
*mut c_char 3unsafe { CString::from_raw(x) }
原始类型 x转换方法
StringOsString::from(x) i
CStringOsString::from(x.to_str()?)
OsStringx
PathBufx.into_os_string()
Vec<u8> 1?
&strOsString::from(x) i
&CStrOsString::from(x.to_str()?)
&OsStrOsString::from(x) i
&Pathx.as_os_str().to_owned()
&[u8] 1?
原始类型 x转换方法
StringPathBuf::from(x) i
CStringPathBuf::from(x.to_str()?)
OsStringPathBuf::from(x) i
PathBufx
Vec<u8> 1?
&strPathBuf::from(x) i
&CStrPathBuf::from(x.to_str()?)
&OsStrPathBuf::from(x) i
&PathPathBuf::from(x) i
&[u8] 1?
原始类型 x转换方法
Stringx.into_bytes()
CStringx.into_bytes()
OsString?
PathBuf?
Vec<u8> 1x
&strVec::from(x.as_bytes())
&CStrVec::from(x.to_bytes_with_nul())
&OsStr?
&Path?
&[u8] 1x.to_vec()
原始类型 x转换方法
Stringx.as_str()
CStringx.to_str()?
OsStringx.to_str()?
PathBufx.to_str()?
Vec<u8> 1std::str::from_utf8(&x)?
&strx
&CStrx.to_str()?
&OsStrx.to_str()?
&Pathx.to_str()?
&[u8] 1std::str::from_utf8(x)?
原始类型 x转换方法
StringCString::new(x)?.as_c_str()
CStringx.as_c_str()
OsString 2x.to_str()?
PathBuf?,4
Vec<u8> 1,5CStr::from_bytes_with_nul(&x)?
&str?,4
&CStrx
&OsStr 2?
&Path?
&[u8] 1,5CStr::from_bytes_with_nul(x)?
*const c_char 1unsafe { CStr::from_ptr(x) }
原始类型 x转换方法
StringOsStr::new(&x)
CString?
OsStringx.as_os_str()
PathBufx.as_os_str()
Vec<u8> 1?
&strOsStr::new(x)
&CStr?
&OsStrx
&Pathx.as_os_str()
&[u8] 1?
原始类型 x转换方法
StringPath::new(x) r
CStringPath::new(x.to_str()?)
OsStringPath::new(x.to_str()?) r
PathBufPath::new(x.to_str()?) r
Vec<u8> 1?
&strPath::new(x) r
&CStrPath::new(x.to_str()?)
&OsStrPath::new(x) r
&Pathx
&[u8] 1?
原始类型 x转换方法
Stringx.as_bytes()
CStringx.as_bytes()
OsString?
PathBuf?
Vec<u8> 1&x
&strx.as_bytes()
&CStrx.to_bytes_with_nul()
&OsStrx.as_bytes() 2
&Path?
&[u8] 1x
目标类型原始类型 x转换方法
*const c_charCStringx.as_ptr()

i 如果可以推断出类型则可简写为 x.into().
r 如果可以推断出类型则可简写为 x.as_ref().

1 该调用应当也必然为 unsafe 的, 请确保原始数据是对应字符串类型的有效表示 (比如 String 必须是 UTF-8 编码). 🔗

2 仅在某些平台上 std::os::<your_os>::ffi::OsStrExt 支持通过辅助方法获取底层 OsStr 的原始 &[u8] 表示. 例如:

use std::os::unix::ffi::OsStrExt;
let bytes: &[u8] = my_os_str.as_bytes();
CString::new(bytes)?

3 c_char 必须由前一个 CString 生成. 如果是从 FFI 来的则换用 &CStr.

4 如果没有结尾 0x0 的话是没法简单地转换为 x 的. 最好的办法是通过 CString 转一道.

5 必须保证数组以 0x0 结束.

字符串输出#

将类型转换为 String 或输出出来.

Rust 拥有一系列将类型转化为字符串输出的 API, 统称为 格式化 宏:

输出说明
format!(fmt)String全功能的“转为 String”.
print!(fmt)控制台写到标准输出.
println!(fmt)控制台写到标准输出.
eprint!(fmt)控制台写到标准错误输出.
eprintln!(fmt)控制台写到标准错误输出.
write!(dst, fmt)缓冲区别忘了要引入 use std::io::Write;
writeln!(dst, fmt)缓冲区别忘了要引入 use std::io::Write;
 
方法说明
x.to_string() STD产生 String, 对每个 Display 类型都作了实现.
 

这里 fmt 是个类似于 "hello {}" 字符串字面量, 它可以指定输出 (参见“格式化”) 和附加参数.

这里列出了在 format! 和类似命令中, 通过 trait Display "{}" STDDebug "{:?}" STD 实现的类型转换 (并不全面):

类型实现
StringDebug, Display
CStringDebug
OsStringDebug
PathBufDebug
Vec<u8>Debug
&strDebug, Display
&CStrDebug
&OsStrDebug
&PathDebug
&[u8]Debug
boolDebug, Display
charDebug, Display
u8i128Debug, Display
f32, f64Debug, Display
!Debug, Display
()Debug
 

简而言之, Debug 打印出详细信息; 而 特殊 类型需要特别指定如何转换到 Display.

格式化宏中的各参数指示器可以是 {}, {argument} 或后续下述基本语法:

{ [argument] ':' [[fill] align] [sign] ['#'] [width [$]] ['.' precision [$]] [type] }
元素说明
argument数字 (0, 1, ...), 参数 '21 或名称,'18print!("{x}").
fill当指定 width 时该字符串将用于填充空白 (如 0).
align当指定宽度时表示左 (<), 中 (^), 右 (>).
signCan be + for sign to always be printed.
#增强格式化, 如更美观的 DebugSTD 格式化 ? 或十六进制前导符 0x.
width最小宽度 (≥ 0), 用 fill 填充 (默认为空格). 如果以 0 开头则用 0 填充.
precision数字类型的十进制位数 (≥ 0), 或非数值类型的最大宽度.
$widthprecision 解释为参数标识符以允许动态格式化.
typeDebugSTD (?) 格式化, 十六进制 (x), 二进制 (b), 八进制 (o), 指针 (p), 科学计数法 (e)... 见此.
 
格式举例说明
{}使用 Display 打印下一个参数.STD
{x}同上, 但使用作用域中的 x. '21
{:?}使用 Debug 打印下一个参数.STD
{2:#?}DebugSTD 格式化美观打印第三个参数.
{val:^2$}将参数 val 居中, 其宽度由第三个参数指定.
{:<10.3}以宽度 10 进行左对齐, 小数位数是 3.
{val:#x}用十六进制格式化 val 参数, 并带有前导 0x (x 的增强格式).
 
用法举例说明
println!("{}", x)DisplaySTD 打印 x 到标准输出并换行. '15
println!("{x}")同上, 但使用作用域的 x. '21
format!("{a:.3} {b:?}")PI 转为小数点后 3 位, 中间加一个空格后, 用 Debug STD 打印 b, 返回 String. '21
 

工具链#

项目结构#

基本项目布局, 以及 cargo 常用的文件和目录.

项目代码
📁 .cargo/项目本地 cargo 配置, 可以包含 config.toml. 🔗 🝖
📁 benches/存放该 crate 的性能测试, 通过 cargo bench 运行, 默认要求 nightly. * 🚧
📁 examples/使用该 crate 的例程, 其中的代码视该 crate 层级如用户.
     my_example.rs每个独立的例程可以通过 cargo run --example my_example 来运行.
📁 src/项目实际源代码.
     main.rs应用程序默认入口, 即 cargo run 运行的内容.
     lib.rs库默认入口. 即 my_crate::f() 对应查找的内容.
📁 src/bin/额外的二进制程序, 在库项目中也可以有.
     x.rs二进制程序可通过 cargo run --bin x 来运行.
📁 tests/集成测试, 通过 cargo test 调用. 单元测试则通常直接放在 src/ 的文件里.
.rustfmt.toml自定义 cargo fmt 的运行方式.
.clippy.toml对特定 clippy lint 的特殊配置, 通过 cargo clippy 调用 🝖
build.rs预编译脚本, 🔗 当编译 C/FFI 等时常用.
Cargo.toml项目清单, 🔗 定义了依赖和架构等.
Cargo.lock用于可重复构建的依赖详情, 对于应用程序建议加入 git 版本控制, 但库不需要.
rust-toolchain.toml定义项目的工具链覆盖🔗 (频道, 组件, 目标).

* stable 可以考虑 Criterion.

 

各种不同入口的最简单样例如下:

// src/main.rs (默认应用程序入口点)

fn main() {
    println!("Hello, world!");
}
// src/lib.rs (默认库入口点)

pub fn f() {}      // 根下的公共条目, 可被外部访问.

mod m {
    pub fn g() {}  // 根下非公开 (`m` 不公开), 
}                  // 所以 crate 外不可访问.
// src/my_module.rs (项目中任意文件)

fn f() -> u32 { 0 }

#[cfg(test)]
mod test {
    use super::f;           // 需要从父模块导入.
                            // 可以访问非公共成员.
    #[test]
    fn ff() {
        assert_eq!(f(), 0);
    }
}
// tests/sample.rs (例程测试样例)

#[test]
fn my_sample() {
    assert_eq!(my_crate::f(), 123); // 集成和性能测试对 crate 的依赖
}                                   // 与依赖第三方库是一样的. 因此仅可访问公开项.
// benches/sample.rs (性能测试样例)

#![feature(test)]   // #[bench] 依然是实验性的

extern crate test;  // 出于某些原因在 '18 版本仍需要.
                    // 虽然通常情况下可能不需要.

use test::{black_box, Bencher};

#[bench]
fn my_algo(b: &mut Bencher) {
    b.iter(|| black_box(my_crate::f())); // `black_box` 防止 `f` 被优化掉.
}
// build.rs (预编译脚本样例)

fn main() {
    // 通过 env 环境变量获取编译目标; 也可使用 `#[cfg(...)]`.
    let target_os = env::var("CARGO_CFG_TARGET_OS");
}

*环境变量详见该表.

// src/lib.rs (过程宏默认入口)

extern crate proc_macro;  // 需要显式引入.

use proc_macro::TokenStream;

#[proc_macro_attribute]   // 此时可以通过 `#[my_attribute]` 使用
pub fn my_attribute(_attr: TokenStream, item: TokenStream) -> TokenStream {
    item
}
// Cargo.toml

[package]
name = "my_crate"
version = "0.1.0"

[lib]
proc-macro = true
 

模块树和导入规则:

模块BK EX REF源文件行为如下:

  • 模块树要求显式定义, 无法被隐式地从文件系统树中构建. 🔗
  • 模块树根等同于库或应用程序的入口点 (如 lib.rs).

实际的模块定义行为如下:

  • 一个 mod m {} 会定义一个文件内模块, 而当使用 mod m; 时则会读取 m.rsm/mod.rs.
  • .rs 的路径取决于嵌套层级, 如 mod a { mod b { mod c; }}} 指向 a/b/c.rsa/b/c/mod.rs.
  • 某些在模块树根中路径不对应文件的 mod m; 并不会被编译器检查到! 🛑

Rust 有如下三种命名空间:

命名空间 类型 命名空间 函数 命名空间
mod X {} fn X() {} macro_rules! X { ... }
X (crate) const X: u8 = 1;
trait X {} static X: u8 = 1;
enum X {}
union X {}
struct X {}
struct X;1
struct X();1

1 既可以算作 类型 也算作 函数.

  • 在给定作用域中 (如在某个模块中), 每个命名空间类型下只能有一个项, 如:
    • enum X {}fn X() {} 冲突
    • struct X;const X 不冲突
  • 当使用 use my_mod::X; 时, 所有叫作 X 的项都会被导入.

出于存在名称转换 (如 fnmod 都会被转为小写), 以及 常识 (开发者不太会命名多个 X) 的考虑, 在多数 crate 中通常并不需要担心这些 情况. 但在设计宏的时候, 这是需要考虑的一点.

 

Cargo#

常用命令行工具.

命令说明
cargo init基于最新的版本创建新项目.
cargo build调试模式构建项目 (--release 启用所有优化).
cargo check检查项目是否通过编译 (比构建更快)).
cargo test运行项目测试用例.
cargo doc --open生成并打开代码和相关依赖的本地文档.
cargo run编译出二进制文件并运行 (main.rs).
     cargo run --bin b运行二进制程序 b. 统一 feature 和其他依赖 (可能会产生冲突).
     cargo run -p w在子工作空间中 w 运行主程序. 对待 feature 可能更如你所愿.
cargo tree显示依赖树.
cargo +{nightly, stable} ...命令使用给定工具链, 如对某些 'nightly only' 的工具.
cargo +nightly ...某些 nightly-only 命令 (如下替换 ...)
     build -Z timings显示哪个 crate 导致你编译那么久, 很有用. 🚧 🔥
     rustc -- -Zunpretty=expanded显示宏展开. 🚧
rustup doc打开离线 Rust 文档 (包括官方手册), “云”上编程!

这里 cargo build 表示可以输入 cargo build 或者简写成 cargo b; --release 表示可以简写成 -r.

 

可选的 rustup 组件. 通过 rustup component add [tool] 安装.

工具说明
cargo clippy额外(lints) 检查通用 API 误用和非惯用代码.🔗
cargo fmt自动代码格式化.(rustup component add rustfmt) 🔗
 

更多 cargo 插件可以在这里找到.

 

交叉编译#

🔘 检查目标是否支持.

🔘 安装目标依赖: rustup target install X.

🔘 安装本地工具链(取决于目标可能需要链接).

应从目标供应商(Google, Apple 等)获取这些资源.也可能不支持本地宿主环境(比如, Windows 不支持 iOS 工具链).

某些工具链需要额外的构建步骤 (比如 Android 的 make-standalone-toolchain.sh).

🔘 修改 ~/.cargo/config.toml 如下:

[target.aarch64-linux-android]
linker = "[PATH_TO_TOOLCHAIN]/aarch64-linux-android/bin/aarch64-linux-android-clang"

或者

[target.aarch64-linux-android]
linker = "C:/[PATH_TO_TOOLCHAIN]/prebuilt/windows-x86_64/bin/aarch64-linux-android21-clang.cmd"

🔘 设置环境变量 (可选, 编译器不报错则可以跳过):

set CC=C:\[PATH_TO_TOOLCHAIN]\prebuilt\windows-x86_64\bin\aarch64-linux-android21-clang.cmd
set CXX=C:\[PATH_TO_TOOLCHAIN]\prebuilt\windows-x86_64\bin\aarch64-linux-android21-clang.cmd
set AR=C:\[PATH_TO_TOOLCHAIN]\prebuilt\windows-x86_64\bin\aarch64-linux-android-ar.exe
...

如何设置取决于编辑器提示, 并非所有步骤都是必须的.

某些平台和配置可能对路径表示极其敏感 (比如) \/).

✔️ 通过 cargo build --target=X 编译.

 

工具链命令#

源代码中用于工具链或预处理的内嵌的特殊标识符.

声明BK BK EX REF 使用 macro_rules!:

写法说明
$x:ty宏捕获 (此处捕获一个类型).
     $x:item项, 比如一个函数, 结构体或模块等.
     $x:block语句或表达式块 {}, 如 { let x = 5; }
     $x:stmt语句, 如 let x = 1 + 1;, String::new();vec![];
     $x:expr表达式, 如 x, 1 + 1, String::new()vec![]
     $x:pat模式, 如 Some(t), (17, 'a')_.
     $x:ty类型, 如 String, usizeVec<u8>.
     $x:ident标识符, 比如在 let x = 0; 中标识符是 x.
     $x:path路径 (如 foo, ::std::mem::replace, transmute::<_, int>).
     $x:literal字面量 (如 3, "foo", b"bar" 等.).
     $x:lifetime生命周期 (如 'a, 'static 等.).
     $x:meta元项; 用在 #[...]#![...] 属性声明里.
     $x:vis可见修饰符; pub, pub(crate) 等.
     $x:tt单个 token 树, 详情见此.
$crate特殊保留变量, 宏定义所在的 crate. ?

文档注释BK EX REF中的写法如下:

写法说明
```...```包含一个文档测试 (文档代码通过 cargo test 运行).
```X,Y ...```同上, 但包含可选项 X, Y 如下 ...
     rust明确该测试是由 Rust 编写的; 可以通过 Rust 工具链解析.
     -编译测试. 运行测试. 当 panic 时失败. 默认行为.
     should_panic编译测试. 运行测试. 执行应当 panic. 否则测试失败.
     no_run编译测试. 编译失败则测试失败, 不会运行测试.
     compile_fail编译测试. 但如果代码 能够 通过编译则失败.
     ignore不要编译. 不要运行. 忽略.
     edition2018在 Rust '18 版本下运行; 默认是 '15.
#文档中注释某行 (``` # use x::hidden; ```).
[`S`]创建一个链接指向结构体, 枚举, trait, 函数, … 的 S.
[`S`](crate::S)可以使用 Markdown 语法指定链接路径.

这些属性对整个 crate 或应用程序都生效:

外部可选项作用说明
#![no_std]C不自动引入 stdSTD ; 而使用 coreSTD . REF
#![no_implicit_prelude]CM不添加 preludeSTD, 需要手动引入 None, VecREF
#![no_main]C不触发应用程序中的 main(), 允许自定义启动. REF
 
内部可选项作用说明
#![feature(a, b, c)]C依赖于某个永远无法被稳定下来的特性, 参见 Unstable Book. 🚧
 
构建选项作用说明
#![windows_subsystem = "x"]C在 Windows 上创建 consolewindows 应用程序. REF 🝖
#![crate_name = "x"]C当不使用 cargo 时指定当前 crate 名. ? REF 🝖
#![crate_type = "bin"]C指定当前 crate 类型 (bin, lib, dylib, cdylib, ...). REF 🝖
#![recursion_limit = "123"]C设置解引用和宏展开等的 编译期 递归限制 REF 🝖
#![type_length_limit = "456"]C限制类型替换的最大数量. REF 🝖
 
Handler作用说明
#[panic_handler]F使函数 fn f(&PanicInfo) -> ! 作为 panic handler. REF
#[global_allocator]S标记静态实例. GlobalAlloc STD 全局分配器. REF

这些属性主要用于控制相关代码:

开发者体验作用说明
#[non_exhaustive]T标记 structenum 未来有可能发生变更. REF
#[path = "x.rs"]M从非标准文件中获取模块. REF
 
代码生成作用声明
#[inline]F建议编译器将函数调用编译为内嵌代码. REF
#[inline(always)]F要求编译器必须将此函数调用内嵌. REF
#[inline(never)]F告诉编译器即便该函数可以内嵌也不要这么做. REF
#[cold]F标记该函数可能并不需要被调用. REF
#[target_feature(enable="x")]F启用 unsafe fn 下支持的某些 CPU 特性 (如 avx2). REF
#[track_caller]F允许 fn 追溯调用者 callerSTD 已获得更详细的 panic 信息. REF
#[repr(X)]1T用另一种指定的表示法来替换 rust REF 默认的:
     #[repr(C)]T使用兼容 C (当 FFI) 且可预测的 (当 transmute) 内存布局. REF
     #[repr(C, u8)]enum使得该 enum 变体以指定类型表示. REF
     #[repr(transparent)]T使得单元素类型内存布局与其内部字段一致. REF
     #[repr(packed(1))]T结构体及其字段向低位对齐, 可能会 UB. REF
     #[repr(align(8))]T结构体对齐提升, 比如用于 SIMD 类型. REF

1 某些标识装饰器可以合并在一起写, 如 #[repr(C, packed(1))].

 
链接作用说明
#[no_mangle]*使该项编译后如其名, 不添加乱七八糟的字符. REF
#[no_link]X当仅使用宏时不链接 extern crate. REF
#[link(name="x", kind="y")]X链接本地库, 表明符号表将从这里查找. REF
#[link_name = "foo"]F结息 extern fn 用的符号名. REF
#[link_section = ".sample"]FS指定对象文件的段名. REF
#[export_name = "foo"]FSfnstatic 以别名导出. REF
#[used]S不要优化掉看似未使用过的 static 变量. REF

Rust 工具链利用这些属性提升代码质量:

代码模式作用说明
#[allow(X)]*rustcclippy ... 允许 X 可能导致的警告. REF
#[warn(X)] 1*... 产生警告, 结合 clippy lint. 🔥 REF
#[deny(X)] 1*... 编译失败. REF
#[forbid(X)] 1*... 编译失败并禁用后续的 allow 声明. REF
#[deprecated = "msg"]*让用户知道你曾经犯了个错误. REF
#[must_use = "msg"]FTX让编译器检查返回值确被调用者 处理过 了. 🔥 REF

1 关于在 crate 中什么是 最佳实践 上有过不少争论. 通常多人活跃维护的 crate 可能会提供更激进的 denyforbid lint; 不定期更新的项目则可能只标记一个 warn (不保证未来的编译器或者 clippy 不会突然对此产生警告).

 
测试作用说明
#[test]F标记该函数为测试, 通过 cargo test 运行. 🔥 REF
#[ignore = "msg"]F编译但目前不运行某些 #[test]. REF
#[should_panic]F该测试必须 panic!() 才算成功. REF
#[bench]Fbench/ 中标记该函数为性能测试, 通过 cargo bench 运行. 🚧 REF
 
格式化作用说明
#[rustfmt::skip]*防止 cargo fmt 自动清理该项. 🔗
#![rustfmt::skip::macros(x)]CM... 防止自动清理宏 x. 🔗
#![rustfmt::skip::attributes(x)]CM... 防止自动清理属性 x. 🔗
 
文档作用说明
#[doc = "Explanation"]*/// 文档注释效果相同. 🔗
#[doc(alias = "other")]*让用户用该别名也能在文档中搜索到该项. 🔗
#[doc(hidden)]*在文档中隐藏. 🔗
#![doc(html_favicon_url = "")]C设置文档图标 favicon. 🔗
#![doc(html_logo_url = "")]C设置文档 Logo. 🔗
#![doc(html_playground_url = "")]C用给定服务生成 运行 按钮. 🔗
#![doc(html_root_url = "")]C外部 crate 的基础链接. 🔗
#![doc(html_no_source)]C生成的文档中不要包含源代码. 🔗

这些属性用于创建或者修饰宏:

声明宏作用说明
#[macro_export]!macro_rules! 导出为 pub 到 crate 层级 REF
#[macro_use]MX让宏得以运行在模块里; 或从 extern crate 导入. REF
 
过程宏作用说明
#[proc_macro]F标记 fn函数式 过程宏, 调用方式如 m!(). REF
#[proc_macro_derive(Foo)]F标记 fnDerive 宏, 调用方式如 #[derive(Foo)]. REF
#[proc_macro_attribute]F标记 fn属性宏, 调用方式如一个新的 #[x]. REF
 
Derive作用说明
#[derive(X)]T通过某些过程宏提供 trait Ximpl . 🔥 REF

这些属性用于条件编译:

配置属性作用说明
#[cfg(X)]*如果提供了配置 X 则编译. REF
#[cfg(all(X, Y, Z))]*如果提供了所有配置则编译. REF
#[cfg(any(X, Y, Z))]*如果提供了任意配置则编译. REF
#[cfg(not(X))]*如果未提供 X 则编译. REF
#[cfg_attr(X, foo = "msg")]*如果提供了 X 则标记 #[foo = "msg"]. REF
 

⚠️ Note, options can generally be set multiple times, i.e., the same key can show up with multiple values. One can expect #[cfg(target_feature = "avx")] #[cfg(target_feature = "avx2")] to be true at the same time.

 
已知选项作用说明
#[cfg(target_arch = "x86_64")]*指定编译目标 CPU 架构. REF
#[cfg(target_feature = "avx")]*判断某类指令集是否可用. REF
#[cfg(target_os = "macos")]*运行的目标操作系统. REF
#[cfg(target_family = "unix")]*运行的某一类目标操作系统. REF
#[cfg(target_env = "msvc")]*指定如何让操作系统链接 DLL 和函数. REF
#[cfg(target_endian = "little")]*你优秀的无开销自定义协议失败的主要原因. REF
#[cfg(target_pointer_width = "64")]*指针位数, 即 usize 和 CPU 字长. REF
#[cfg(target_vendor = "apple")]*目标设备制造商. REF
#[cfg(debug_assertions)]*标记为 debug_assert!() 的和类似调试语句将会 panic. REF
#[cfg(proc_macro)]*当 crate 编译为过程宏时. REF
#[cfg(test)]*cargo test 时编译. 🔥 REF
#[cfg(feature = "serde")]*当 crate 启用了编译选项 serde 时. 🔥 REF

预编译脚本可用的环境变量和输出配置.

输入环境说明 🔗
CARGO_FEATURE_X每个启用的 x 都将设置一个这样的环境变量.
     CARGO_FEATURE_SERDE如果启用了 serde 特性.
     CARGO_FEATURE_SOME_FEATURE如果启用了 some-feature 特性; 横线 - 会转为下划线 _.
CARGO_CFG_XExposes cfg's; joins mult. opts. by , and converts - to _.
     CARGO_CFG_TARGET_OS=macos如果 target_osmacos.
     CARGO_CFG_TARGET_FEATURE=avx,avx2如果 target_feature 设置为了 avxavx2.
OUT_DIR输出目录.
TARGET编译结果目录.
HOST指定运行该构建脚本的编译器.
PROFILE可以是 debug 或者 release.

build.rs 通过 env::var()? 可以访问. 列表不完整.

 
输出字符串说明 🔗
cargo:rerun-if-changed=PATH(仅当) PATH 变化时运行 build.rs.
cargo:rerun-if-env-changed=VAR(仅当) 环境 VAR 变化时运行 build.rs.
cargo:rustc-link-lib=[KIND=]NAME通过 -l 选项链接到本地库.
cargo:rustc-link-search=[KIND=]PATH通过 -L 选项设置本地库搜索路径.
cargo:rustc-flags=FLAGS为编译器添加自定义标识. ?
cargo:rustc-cfg=KEY[="VALUE"]声明给定 cfg 选项以用于后续编译.
cargo:rustc-env=VAR=VALUE 声明在 crate 编译期间可以通过 env!() 访问的变量.
cargo:rustc-cdylib-link-arg=FLAG 当构建 cdylib 时的连接器标识.
cargo:warning=MESSAGE产生编译器警告.

build.rs 通过 println!() 调用. 列表不完整.

表格列 作用 说明如下:
C 标识作用在 crate 层级上 (常在顶级文件中声明作 #![my_attr]).
M 标识作用在模块上.
F 标识作用在函数上.
S 标识作用在静态区上.
T 标识作用在类型上.
X 标识某些特殊场景上.
! 标识作用在宏上.
* 标识作用在任意项上.


与类型打交道#

类型, Trait, 泛型#

允许用户 自定义类型 并减少代码重复.

类型
u8 String Device
  • 以给定语句或布局设置值.
类型
u8{ 0u8, 1u8, ..., 255u8 }
char{ 'a', 'b', ... '🦀' }
struct S(u8, char){ (0u8, 'a'), ... (255u8, '🦀') }

样例类型和值

类型等价和转换
u8 &u8 &mut u8 [u8; 1] String
  • 看起来类似却完全不同的   u8,    &u8,    &mut u8 类型.
  • 任意 t: T 仅接受精确类型 T 的值, 如:
    • f(0_u8) 不能以 f(&0_u8) 调用,
    • f(&mut my_u8) 不能以 f(&my_u8) 调用,
    • f(0_u8) 不能以 f(0_i8) 调用.

确实, 作为类型而言, 0 != 0 (在数学层面)! 在语言层面, 并没有为了你愉快地使用而定义了这样一个相等比较 ==(0u8, 0u16).

类型
u8{ 0u8, 1u8, ..., 255u8 }
u16{ 0u16, 1u16, ..., 65_535u16 }
&u8{ 0xffaa&u8, 0xffbb&u8, ... }
&mut u8{ 0xffaa&mut u8, 0xffbb&mut u8, ... }

值和类型的不同

  • 不过在某些情况下, Rust 可能会进行 类型转换1
    • 转换 指的是手动进行类型转换, 0_i8 as u8
    • 强转 将在安全的情况下自动转换 2, let x: &u8 = &mut 0_u8;

1 从一类值 (如 u8) 转换和强转到另一类值 (如 u16) 时有可能会增加 CPU 指令以完成该操作; 一个类型是某一个类型一部分的叫做子类型 (比如 u8u16 的子类型所以 0_u80_u16 形式一致), 这种转换在编译期间就可以完成. Rust 并不为规则类型实现子类型转换 (0_u8 确实 不同于 0_u16), 但生命周期却是有的. 🔗

2 安全并不仅是简单的物理保证 (比如 &u8 无法变为 &u128), 并且告诉我们“历史经验表明这样转换确实会导致编程错误”.

实现 — impl S { }
u8 impl { ... } String impl { ... } Port impl { ... }
impl Port {
    fn f() { ... }
}
  • 类型总要有对应的实现, 比如 impl Port {} 的类型 关联 行为有:
    • 关联函数 Port::new(80)
    • 方法 port.close()

这里的 关联 更像是一个哲学概念而非技术概念, 没什么能阻止你使用 u8::play_sound() (只要你乐意).

Trait — trait T { }
Copy Clone Sized ShowHex
  • Trait
    • 是一种“抽象”行为,
    • trait 作者语义上声明了 该 trait 意为 X,
    • 别人也可以为其他类型实现相关行为 (“特化”).
  • 可以认为 trait 是类型的一种“成员列表”:
Copy Trait
Self
u8
u16
...
Clone Trait
Self
u8
String
...
Sized Trait
Self
char
Port
...

Trait 作为成员列表, Self 指向其包含的类型.

  • 无论谁是该成员列表的一部分, 都将遵守列表的行为.
  • Trait 也可以包含关联方法和函数等.
trait ShowHex {
    // 要求按文档描述实现.
    fn as_hex() -> String;

    // 由 trait 作者提供.
    fn print_hex() {}
}
Copy
trait Copy { }
  • 无方法 Trait 通常叫做 标记 trait.
  • Copy 是一种标记 trait, 表示 内存可以被按位复制.
Sized
  • 某些 trait 完全脱离显式控制.
  • Sized 是由编译器提供用于 已知大小 的类型; 类型大小要么已知, 要么未知.
为类型实现 Trait — impl T for S { }
impl ShowHex for Port { ... }
  • 实现“某些”类型的 Trait.
  • 实现 impl A for B 将添加类型 B 到该 Trait 的成员列表:
ShowHex Trait
Self
Port
  • 你可以认为类型被打上了不同的“标签”:
u8 impl { ... } Sized Clone Copy Device impl { ... } Transport Port impl { ... } Sized Clone ShowHex
Trait vs. 接口
👩‍🦰 Eat 🧔 Venison Eat 🎅 venison.eat()
 

接口 (Interface)

  • Java 中, Alice 创建了接口 Eat.
  • 当 Bob 实现 Venison 时, 他需要决定是否为 Venison 实现 Eat.
  • 换言之, 所有关系必须显式地在类型定义时就表明.
  • 当使用 Venison 时, Santa 才可以使用由 Eat 定义的行为:
// Santa 导入 `Venison` 创建的对象可以 `eat()`.
import food.Venison;

new Venison("rudolph").eat();
 
 
👩‍🦰 Eat 🧔 Venison 👩‍🦰 / 🧔 Venison + Eat 🎅 venison.eat()
 

Trait

  • Rust 中, Alice 创建了 trait Eat.
  • Bob 创建了类型 Venison 并决定暂不实现 Eat (他甚至不知道有 Eat 这么个东西).
  • 某人* 后来觉得为 Venison 添加 Eat 是个好主意.
  • 那么当 Santa 使用 Venison 时需要另外导入 Eat:
// Santa 需要导入 `Venison` 用于创建, 并导入 `Eat` 用于调用 trait 方法.
use food::Venison;
use tasks::Eat;

// 吼吼吼
Venison::new("rudolph").eat();

* 为避免两个人实现不同的 Eat, Rust 限制了 Alice 和 Bob 能做的事情; 即, 一个 impl Eat for Venison 仅能在 Venison 所在的 crate 或 Eat 所在的 crate 中实现. 这被称作 trait 实现的“孤儿原则”. ?

类型构造 — Vec<>
Vec<u8> Vec<char>
  • Vec<u8> 是“字节向量”类型; Vec<char> 是“字符向量”类型, 但 Vec<> 是什么?
构造
Vec<u8>{ [], [1], [1, 2, 3], ... }
Vec<char>{ [], ['a'], ['x', 'y', 'z'], ... }
Vec<>-

类型和类型构造

Vec<>
  • Vec<> 为非类型, 不占内存, 也不会生成代码.
  • Vec<>类型构造器, 是“模板”或者“创建类型的表单”
    • 允许第三方通过参数构造特定类型,
    • 仅当声明 Vec<UserType> 时才成为真正的类型.
泛型参数 — <T>
Vec<T> [T; 128] &T &mut T S<T>
  • Vec<> 的参数常作 T 故有 Vec<T>.
  • T “类型的变量名”可以特化为 Vec<f32>, S<u8>, …
类型构造器产生一类
struct Vec<T> {}Vec<u8>, Vec<f32>, Vec<Vec<u8>>, ...
[T; 128][u8; 128], [char; 128], [Port; 128] ...
&T&u8, &u16, &str, ...

类型和类型构造器

// S<> 是个带有参数 T 的类型构造器; 用户可以提供 T 的任意实际类型.
struct S<T> {
    x: T
}

// 实际使用中必须为 T 指定实际类型.
fn f() {
    let x: S<f32> = S::new(0_f32);
}

常量泛型 — [T; N] and S<const N: usize>
[T; n] S<const N>
  • 某些类型构造器不仅接受指定类型, 还接受 指定常量.
  • [T; n] 构造出一个有 nT 类型元素的数组.
  • 自定义类型可声明为 MyArray<T, const N: usize>.
类型构造器产生一类
[u8; N][u8; 0], [u8; 1], [u8; 2], ...
struct S<const N: usize> {}S<1>, S<6>, S<123>, ...

基于常量的类型构造器

let x: [u8; 4]; // "4 字节数组"
let y: [f32; 16]; // "16 浮点数组"

// `MyArray` 是个需要特定类型 `T` 和特定大小 `N` 的类型构造器.
struct MyArray<T, const N: usize> {
    data: [T; N],
}
约束 (简单) — where T: X
🧔 Num<T> 🎅 Num<u8> Num<f32> Num<Cmplx>   u8 Absolute Dim Mul Port Clone ShowHex
  • 如果 T 可以为任意类型, 那我们怎么确定能为它实现这么一个 Num<T>?
  • 参数约束:
    • 限制了允许什么样的类型 (trait 约束) 或值 (常量约束 ?),
    • 然后就可以应用这些限制了!
  • Trait 约束像是一种“行为检查”:
// 类型仅能由某些实现了 `Absolute` 的 `T` 实现.
struct Num<T> where T: Absolute {
    ...
}

Absolute Trait
Self
u8
u16
...

此处我们为该结构体添加了约束. 实践中则最好在 impl 块中添加约束, 详见下文.

约束 (计算) — where T: X + Y
u8 Absolute Dim Mul f32 Absolute Mul char Cmplx Absolute Dim Mul DirName TwoD Car DirName
struct S<T>
where
    T: Absolute + Dim + Mul + DirName + TwoD
{ ... }
  • 过长的 trait 约束像是一种威胁.
  • 实践中, 每个 + X 声明都会减少这里能用的类型.
实现一类 — impl<>
 
impl<T> S<T> where T: Absolute + Dim + Mul {
    fn f(&self, x: T) { ... };
}

可以读作:

  • 这里是对任意类型 T 一个实现 (即 impl <T> 部分),
  • 该类型必须同时满足 Absolute + Dim + Mul 这些 Trait 约束,
  • 可以添加一个实现块 S<T>,
  • 以及包含更多方法 ...

可以将类如 impl<T> ... {} 的代码看作对一类行为的抽象实现. 尤其使得第三方透明地实例化与类型构造器如何实例化类似:

// 当编译器遇到如下代码时将会
// - 检查 `0` 和 `x` 是否满足 `T` 的要求
// - 创建两个版本的 `f`, 一个给 `char`, 另一个给 `u32`.
// - 并基于“一类实现”来提供
s.f(0_u32);
s.f('x');
一揽子实现 — impl<T> X for T { ... }
 

也可以针对多种类型编写针对某“一类的实现”:

// 为任意已经实现过 ToHex 的类型实现 Serialize
impl<T> Serialize for T where T: ToHex { ... }

这称为一揽子实现.

ToHex
Self
Port
Device
...

→ 左边的类型总能根据该 impl 实现到右边 →

Serialize Trait
Self
u8
Port
...

这样可以用一种模块化的方法为已经实现了其他接口的给定外部类型提供一种优雅的实现.

Trait 参数 — Trait<In> { type Out; }
 

注意某些 Trait 会被“附加”多次, 而有些又只有一次?

Port From<u8> From<u16> Port Deref type u8;
 

为什么?

  • Trait 本身通过两类 参数类型 实现泛型:
    • trait From<I> {}
    • trait Deref { type O; }
  • 还记得我们说过 Trait 是类型的“成员列表”并且通过 Self 调用的吗?
  • 转到现在, 就可以认为参数 I (即输入) 和 O (即输出) 不过是该 Trait 列表的另外一 罢了:
impl From<u8> for u16 {}
impl From<u16> for u32 {}
impl Deref for Port { type O = u8; }
impl Deref for String { type O = str; }
From
SelfI
u16u8
u32u16
...
Deref
SelfO
Portu8
Stringstr
...

输入和输出参数

这里会有点绕,

  • 任意输出 O 参数必须由输入参数 I 唯一确定,
  • (同样地, 关系 X Y 会表现为一个函数),
  • Self 作为输入.

一个更复杂的样例:

trait Complex<I1, I2> {
    type O1;
    type O2;
}
  • 此处创建了一个具有关联类型的 Complex,
  • 它有 3 个输入 (Self 也是输入) 和 2 两个输出, 可以表示为 (Self, I1, I2) => (O1, O2)
Complex
Self [I]I1I2O1O2
Playeru8charf32f32
EvilMonsteru16stru8u8
EvilMonsteru16Stringu8u8
NiceMonsteru16Stringu8u8
NiceMonster🛑u16Stringu8u16

各种 Trait 实现. 最后一个对 (NiceMonster, u16, String) 无效,
因为已经由输出唯一确定了.

Trait 设计准则 (抽象)
👩‍🦰 A<I> 🧔 Car 👩‍🦰 / 🧔 Car A<I> 🎅 car.a(0_u8) car.a(0_f32)
👩‍🦰 B type O; 🧔 Car 👩‍🦰 / 🧔 Car B T = u8; 🎅 car.b(0_u8) car.b(0_f32)
  • 参数的选择 (输入还是输出) 仍然决定了谁会被允许加入成员:
    • I 参数允许“一类实现”转发给用户 (Santa),
    • O 参数必须由 Trait 实现者确定 (Alice 或 Bob).
trait A<I> { }
trait B { type O; }

// 实现者将 (X, u32) 添加到 A.
impl A<u32> for X { }

// 实现者将一类 impl (X, ...) 添加到 A, 用户则可以特化之.
impl<T> A<T> for Y { }

// 实现者必须决定将指定入口 (X, O) 添加到 B.
impl B for X { type O = u32; }
A
SelfI
Xu32
Y...

Santa 通过提供他自己类型为 T 添加更多成员.

B
SelfO
PlayerString
Xu32

给定输入集合 (此处为 Self), 实现者必须预先选择 O.

Trait 设计准则 (举例)
Query vs. Query<I> vs. Query type O; vs. Query<I> type O;
 

参数选择取决于 Trait 的作用.


无额外参数

trait Query {
    fn search(&self, needle: &str);
}

impl Query for PostgreSQL { ... }
impl Query for Sled { ... }

postgres.search("SELECT ...");
👩‍🦰 Query 🧔 PostgreSQL Query Sled Query
 

Trait 作者假设:

  • 实现者和用户都不允许自定义 API.
 

输入参数

trait Query<I> {
    fn search(&self, needle: I);
}

impl Query<&str> for PostgreSQL { ... }
impl Query<String> for PostgreSQL { ... }
impl<T> Query<T> for Sled where T: ToU8Slice { ... }

postgres.search("SELECT ...");
postgres.search(input.to_string());
sled.search(file);
👩‍🦰 Query<I> 🧔 PostgreSQL Query<&str> Query<String> Sled Query<T> where T is ToU8Slice.
 

Trait 作者假设:

  • 实现者可以为相同的 Self 类型提供多个自定义 API 实现,
  • 用户来决定哪种 I 类型行为可用.
 

输出参数

trait Query {
    type O;
    fn search(&self, needle: Self::O);
}

impl Query for PostgreSQL { type O = String; ...}
impl Query for Sled { type O = Vec<u8>; ... }

postgres.search("SELECT ...".to_string());
sled.search(vec![0, 1, 2, 4]);
👩‍🦰 Query type O; 🧔 PostgreSQL Query O = String; Sled Query O = Vec<u8>;
 

Trait 作者假设:

  • 实现者可以为 Self 类型自定义 API (但只有一种办法),
  • 用户不需要也不应该能够影响指定 Self 的自定义.

如你所见, 对于函数而言 输入输出 项都 不一定 (除非有必要) 是 IO!

 

多个输入输出参数

trait Query<I> {
    type O;
    fn search(&self, needle: I) -> Self::O;
}

impl Query<&str> for PostgreSQL { type O = String; ... }
impl Query<CString> for PostgreSQL { type O = CString; ... }
impl<T> Query<T> for Sled where T: ToU8Slice { type O = Vec<u8>; ... }

postgres.search("SELECT ...").to_uppercase();
sled.search(&[1, 2, 3, 4]).pop();
👩‍🦰 Query<I> type O; 🧔 PostgreSQL Query<&str> O = String; Query<CString> O = CString; Sled Query<T> O = Vec<u8>; where T is ToU8Slice.
 

如上例, 通常 Trait 作者假设:

  • 用户来决定哪种 I 类型行为可用,
  • 对于给定的输入, 实现者需要自己确定输出类型.
动态大小 / 零大小类型
MostTypes Sized 普通类型 vs. Z Sized 零大小 vs. str Sized 动态大小 [u8] Sized dyn Trait Sized ... Sized
  • 如果编译期能够知道用多少个字节表示, 那么类型 T 就是 Sized STD 的, u8&[u8] 是有大小的, [u8] 则不是.
  • 有大小 Sized 意味着存在 impl Sized for T {}. 这个会自动实现且也不能由用户实现.
  • Sized 的类型称为 动态大小类型 BK NOM REF (DST), 有时是 无大小的.
  • 无数据的类型称为 零大小类型 NOM (ZST), 不分配空间.
示例说明
struct A { x: u8 }类型 A 有大小, 即存在 impl Sized for A, 是最“规则”的类型.
struct B { x: [u8] }因为 [u8] 是 DST, B 就会变成 DST, 即不存在 impl Sized.
struct C<T> { x: T }类型参数 具有 隐式的 T: Sized 约束, 如 C<A> 有效, C<B> 无效.
struct D<T: ?Sized> { x: T }使用 ?Sized REF 会取消大小约束, 即 D<B> 也是有效的.
struct E;类型 E 是零大小的 (也是有确定大小的 Sized) 且不会耗费内存.
trait F { fn f(&self); }Trait 没有 隐式声明 Sized 约束, 即 impl F for B {} 有效.
     trait F: Sized {}Trait 具有 Sized 父 Trait.
trait G { fn g(self); }Self 类似参数 DST impl 仍无效, 因为参数不能进栈.
?Sized
S<T> S<u8> S<char> S<str>
struct S<T> { ... }
  • T 可为任意确定类型.
  • 然而这里存在隐式约束 T: Sized, 故 S<str> 不可用.
  • 可以改为 T : ?Sized 以取消该默认约束:
S<T> S<u8> S<char> S<str>
struct S<T> where T: ?Sized { ... }
泛型和生命周期 — <'a>
S<'a> &'a f32 &'a mut u8
  • 生命周期与类型参数使用方法*类似:
    • 用户必须为特定类型指定 'a (编译器会在方法中提供帮助),
    • 由于 Vec<f32>Vec<u8> 是不同的类型, 故记为 S<'p>S<'q>,
    • 这意味着你不能仅分配类型 S<'a> 的值而不管 S<'b> (异常: 生命周期的“子类型”关系, 如 'a 长于 'b).
S<'a> S<'auto> S<'static>
  • 'static 是仅有的 类型空间 中的具名生命周期.
// `'a 是这里的自由参数 (用户可以在任意生命周期上使用)
struct S<'a> {
    x: &'a u32
}

// 非泛型代码中, 'static 是这里仅有的能使用的具名声明周期.
let a: S<'static>;

// 非泛型代码中我们无需指定 'a 并使得 Rust 通过右值自动推断出 'a.
let b: S;

* 这里有些微妙的不同, 比如显式地创建一个类型为 u32 的实例 0, 但由于 'static 的例外你并不能创建一个生命周期, 比如 "lines 80 - 100", 编译器会自动帮你完成这些工作. 🔗

Note to self and TODO: that analogy seems somewhat flawed, as if S<'a> is to S<'static> like S<T> is to S<u32>, then 'static would be a type; but then what's the value of that type?

点击展开样例.

类型乐园#

A visual overview of types and traits in crates.

u8 u16 f32 bool char File String Builder Vec<T> Vec<T> Vec<T> &'a T &'a T &'a T &mut 'a T &mut 'a T &mut 'a T [T; n] [T; n] [T; n] Vec<T> Vec<T> f<T>() {} drop() {} PI dbg! Copy Deref type Tgt; From<T> From<T> From<T> Items defined in upstream crates. Serialize Transport ShowHex Device From<u8> Foreign trait impl. for local type. String Serialize Local trait impl. for foreign type. String From<u8> 🛑 Illegal, foreign trait for f. type. String From<Port> Exception: Legal if used type local. Port From<u8> From<u16> Mult. impl. of trait with differing IN params. Container Deref Tgt = u8; Deref Tgt = f32; 🛑 Illegal impl. of trait with differing OUT params. T T T ShowHex Blanket impl. of trait for any type. Your crate.

A walk through the jungle of types, traits, and implementations that (might possibly) exist in your application.

Type Conversions#

How to get B when you have A?

fn f(x: A) -> B {
    // How can you obtain B from A?
}
MethodExplanation
IdentityTrivial case, B is exactly A.
ComputationCreate and manipulate instance of B by writing code transforming data.
CastsOn-demand conversion between types where caution is advised.
CoercionsAutomatic conversion within 'weakening ruleset'.1
SubtypingAutomatic conversion within 'same-layout-different-lifetimes ruleset'.1
 

1 While both convert A to B, coercions generally link to an unrelated B (a type "one could reasonably expect to have different methods"), while subtyping links to a B differing only in lifetimes.

fn f(x: A) -> B {
    x.into()
}

Bread and butter way to get B from A. Some traits provide canonical, user-computable type relations:

TraitExampleTrait implies ...
impl From<A> for B {}a.into()Obvious, always-valid relation.
impl TryFrom<A> for B {}a.try_into()?Obvious, sometimes-valid relation.
impl Deref for A {}*aA is smart pointer carrying B; also enables coercions.
impl AsRef<B> for A {}a.as_ref()A can be viewed as B.
impl AsMut<B> for A {}a.as_mut()A can be mutably viewed as B.
impl Borrow<B> for A {}a.borrow()A has borrowed analog B (behaving same under Eq, ...).
impl ToOwned for A { ... }a.to_owned()A has owned analog B.
fn f(x: A) -> B {
    x as B
}

Convert types with keyword as if conversion relatively obvious but might cause issues. NOM

AB示例说明
PtrPtrdevice_ptr as *const u8If *A, *B are Sized.
PtrIntegerdevice_ptr as usize
IntegerPtrmy_usize as *const Device
NumberNumbermy_u8 as u16Often surprising behavior.
enum w/o fieldsIntegerE::A as u8
boolIntegertrue as u8
charInteger'A' as u8
&[T; N]*const Tmy_ref as *const u8
fn(...)Ptrf as *const u8If Ptr is Sized.
fn(...)Integerf as usize
 

Where Ptr, Integer, Number are just used for brevity and actually mean:

  • Ptr any *const T or *mut T;
  • Integer any countable u8 ... i128;
  • Number any Integer, f32, f64.

Opinion 💬 — Casts, esp. Number - Number, can easily go wrong. If you are concerned with correctness, consider more explicit methods instead.

fn f(x: A) -> B {
    x
}

Automatically weaken type A to B; types can be substantially1 different. NOM

ABExplanation
&mut T&TPointer weakening.
&mut T*mut T-
&T*const T-
*mut T*const T-
&T&UDeref, if impl Deref<Target=U> for T.
TUUnsizing, if impl CoerceUnsized<U> for T.2 🚧
TVTransitivity, if T coerces to U and U to V.
|x| x + xfn(u8) -> u8Non-capturing closure, to equivalent fn pointer.
 

1 Substantially meaning one can regularly expect a coercion result B to be an entirely different type (i.e., have entirely different methods) than the original type A.

2 Does not quite work in example above as unsized can't be on stack; imagine f(x: &A) -> &B instead. Unsizing works by default for:

  • [T; n] to [T]
  • T to dyn Trait if impl Trait for T {}.
  • Foo<..., T, ...> to Foo<..., U, ...> under arcane 🔗 circumstances.
fn f(x: A) -> B {
    x
}

Automatically converts A to B for types only differing in lifetimes NOM - subtyping examples:

A(subtype)B(supertype)Explanation
&'static u8&'a u8Valid, forever-pointer is also transient-pointer.
&'a u8&'static u8🛑 Invalid, transient should not be forever.
&'a &'b u8&'a &'b u8Valid, same thing. But now things get interesting. Read on.
&'a &'static u8&'a &'b u8Valid, &'static u8 is also &'b u8; covariant inside &.
&'a mut &'static u8&'a mut &'b u8🛑 Invalid and surprising; invariant inside &mut.
Box<&'a static>Box<&'a u8>Valid, box with forever is also box with transient; covariant.
Box<&'a u8>Box<&'static u8>🛑 Invalid, box with transient may not be with forever.
Box<&'a mut u8>Box<&'a u8>🛑 Invalid, see table below, &mut u8 never was a &u8.
Cell<&'a static>Cell<&'a u8>🛑 Invalid, cells are never something else; invariant.
fn(&'static u8)fn(&'u8 u8)🛑 If fn needs forever it may choke on transients; contravar.
fn(&'a u8)fn(&'static u8)But sth. that eats transients can be(!) sth. that eats forevers.
for<'r> fn(&'r u8)fn(&'a u8)Higher-ranked type for<'r> fn(&'r u8) is also fn(&'a u8).
 

In contrast, these are not🛑 examples of subtyping:

ABExplanation
u16u8🛑 Obviously invalid; u16 should never automatically be u8.
u8u16🛑 Invalid by design; types w. different data still never subtype even if they could.
&'a mut u8&'a u8🛑 Trojan horse, not subtyping; but coercion (still works, just not subtyping).
 
fn f(x: A) -> B {
    x
}

Automatically converts A to B for types only differing in lifetimes NOM - subtyping variance rules:

  • A longer lifetime 'a that outlives a shorter 'b is a subtype of 'b.
  • Implies 'static is subtype of all other lifetimes 'a.
  • Whether types with parameters (e.g., &'a T) are subtypes of each other the following variance table is used:
Construct1'aTU
&'a Tcovariantcovariant
&'a mut Tcovariantinvariant
Box<T>covariant
Cell<T>invariant
fn(T) -> Ucontravariantcovariant
*const Tcovariant
*mut Tinvariant

Covariant means if A is subtype of B, then T[A] is subtype of T[B].
Contravariant means if A is subtype of B, then T[B] is subtype of T[A].
Invariant means even if A is subtype of B, neither T[A] nor T[B] will be subtype of the other.

1 Compounds like struct S<T> {} obtain variance through their used fields, usually becoming invariant if multiple variances are mixed.

💡 In other words, 'regular' types are never subtypes of each other (e.g., u8 is not subtype of u16), and a Box<u32> would never be sub- or supertype of anything. However, generally a Box<A>, can be subtype of Box<B> (via covariance) if A is a subtype of B, which can only happen if A and B are 'sort of the same type that only differed in lifetimes', e.g., A being &'static u32 and B being &'a u32.

 

编码指南#

Rust 惯用法#

Java 或 C 的使用者需要转换下思维:

习语代码
用表达式思考x = if x { a } else { b };
x = loop { break 5 };
fn f() -> u32 { 0 }
用迭代器思考(1..10).map(f).collect()
names.iter().filter(|x| x.starts_with("A"))
? 捕获异常x = try_something()?;
get_option()?.run()?
使用强类型enum E { Invalid, Valid { ... } } 之于 ERROR_INVALID = -1
enum E { Visible, Hidden } 之于 visible: bool
struct Charge(f32) 之于 f32
提供生成器Car::new("Model T").hp(20).run();
分离实现泛型 S<T> 可以对每个 T 都有不同的实现.
Rust 没有面向对象, 但通过 impl 可以实现特化.
Unsafe尽量避免 unsafe {}, 因为总是会有更快更安全的解决方案的.除了 FFI.
实现 Trait#[derive(Debug, Copy, ...)].根据需要实现 impl.
工具链利用 clippy 可以提升代码质量.
rustfmt 格式化可以帮助别人看懂你的代码.
添加单元测试 BK(#[test]), 确保代码正常运行.
添加文档测试 BK(``` my_api::f() ```), 确保文档匹配代码.
文档以文档注解的 API 可显示在 docs.rs 上.
不要忘记在开始加上总结句例程.
如果有这些也加上: Panics, Errors, Safety, Abort未定义行为.
 

🔥 强烈建议对所有共享项目都遵循 API 指南(检查列表)! 🔥

 

Async-Await 101#

类似于 C# 或 TypeScript 的 async / await, 但又有所不同:

语法说明
asyncAnything declared async always returns an impl Future<Output=_>. STD
     async fn f() {}Function f returns an impl Future<Output=()>.
     async fn f() -> S {}Function f returns an impl Future<Output=S>.
     async { x }Transforms { x } into an impl Future<Output=X>.
let sm = f(); Calling f() that is async will not execute f, but produce state machine sm. 1 2
     sm = async { g() };Likewise, does not execute the { g() } block; produces state machine.
runtime.block_on(sm);Outside an async {}, schedules sm to actually run. Would execute g(). 3 4
sm.awaitInside an async {}, run sm until complete. Yield to runtime if sm not ready.

1 Technically async transforms following code into anonymous, compiler-generated state machine type; f() instantiates that machine.
2 The state machine always impl Future, possibly Send & co, depending on types used inside async.
3 State machine driven by worker thread invoking Future::poll() via runtime directly, or parent .await indirectly.
4 Rust doesn't come with runtime, need external crate instead, e.g., async-std or tokio 0.2+. Also, more helpers in futures crate.

对每个 x.await, 状态机将会通过控制转移到状态机 x.有时, 由 .await 调用的低级状态机并未就绪, 此时, 工作线程直接返回到运行时, 以使得它可以驱动另一个 Future.一段时间后, 运行时:

  • 可能恢复执行.常见于此, 除非 sm / Future 已析构.
  • 可能由前一个或另一个工作线程恢复执行(取决于运行时).

async 代码块内部代码的简易流程图如下:

       consecutive_code();           consecutive_code();           consecutive_code();
START --------------------> x.await --------------------> y.await --------------------> READY
// ^                          ^     ^                               Future<Output=X> 就绪 -^
// 由运行时调用                 |     |
// 或由外部 .await 调用         |     会由另一个线程恢复(下一个最佳可用的), 
//                            |     或者当 Future 已析构时根本不会执行.
//                            |
//                            执行 `x`.若已就绪, 则继续执行.
//                            若未就绪, 返回当前线程到运行时.

With the execution flow in mind, some considerations when writing code inside an async construct:

语法 1说明
sleep_or_block();显然不对🛑, 当前线程永不终止, 阻碍执行器.
set_TL(a); x.await; TL();显然不对🛑, await 会由其他线程返回, thread local 无效.
s.no(); x.await; s.go();可能不对🛑, 如果等待时 Future 被析构, 则 await 不会返回.2
Rc::new(); x.await; rc();Send 类型拒绝实现 impl Future.兼容性差.

1 Here we assume s is any non-local that could temporarily be put into an invalid state; TL is any thread local storage, and that the async {} containing the code is written without assuming executor specifics.
2 Since Drop is run in any case when Future is dropped, consider using drop guard that cleans up / fixes application state if it has to be left in bad condition across .await points.

 

闭包 API#

There is a subtrait relationship Fn : FnMut : FnOnce. That means a closure that implements Fn STD also implements FnMut and FnOnce. Likewise a closure that implements FnMut STD also implements FnOnce. STD

从调用者的角度来看这意味着:

SignatureFunction g can call …Function g accepts …
g<F: FnOnce()>(f: F)f() once.Fn, FnMut, FnOnce
g<F: FnMut()>(mut f: F)f() multiple times.Fn, FnMut
g<F: Fn()>(f: F)f() multiple times.Fn

注意, 对调用者来说, 如何确定 Fn 闭包, 是最为严格的.但是一个包含 Fn 的闭包, 对调用者来説, 是对任意函数都最兼容的.

 

站在定义闭包的角度来看:

闭包实现*说明
|| { moved_s; } FnOnce调用者必须放弃 moved_s 的所有权.
|| { &mut s; } FnOnce, FnMut允许 g() 改变调用者的局部状态 s.
|| { &s; } FnOnce, FnMut, Fn可能不会导致状态改变, 但可能会共享和重用 s.

* Rust 偏向于以索引捕获(在调用者视角上最「兼容」 Fn 的闭包), 但也可以用 move || {} 语法通过复制或者移动捕获相关环境变量..

 

这会带来如下优势和劣势:

要求优势劣势
F: FnOnce容易满足调用者.仅用一次, g() 仅会调用 f() 一次.
F: FnMut允许 g() 改变调用者状态.调用者不能在 g() 期间重用捕获.
F: Fn可同时存在多个.最难由调用者生成.
 

Unsafe, Unsound, Undefined#

Unsafe 导致 unsound, unsound 导致 undefined, undefined 是一切原力的阴暗面.

Unsafe 代码

  • 标记为 unsafe 的代码有特权.比如, 解引用裸指针, 或调用其他 unsafe 函数.
  • 这是一份特殊的作者必须给编译器的承诺, 编译器相信你.
  • unsafe 代码自身并非有害, 但危险的是 FFI 使用方或者异常的数据结构.
// `x` must always point to race-free, valid, aligned, initialized u8 memory.
unsafe fn unsafe_f(x: *mut u8) {
    my_native_lib(x);
}

未定义行为 (UB)

  • 如前所述, unsafe 代码意味着对编译器的特殊承诺(否则它就不需要是 unsafe 的了).
  • 不遵守承诺会使编译器产生错误的代码, 执行错误的代码会导致未定义行为.
  • 在触发未定义行为之后, 任何事情都可能发生.这种不知不觉的影响可能1)难以捉摸, 2)明显远离事发现场, 或3)只有在某些条件下才会被发现.
  • 一个表面上可以运行的程序(包括任意数量的单元测试), 并不能证明含有未定义行为的代码不会因为一些偶然原因而失败.
  • 含有未定义行为的代码在客观上是危险的, 无效的, 根本不应该存在.
if should_be_true() {
   let r: &u8 = unsafe { &*ptr::null() };    // 一旦运行, 整个程序都会处于未定义状态.
} else {                                     // 尽管这一行看似什么都没干, 程序可能两条路径
    println!("the spanish inquisition");     // 都运行了, 然后破坏掉数据, 或者发生别的.
}

Unsound 代码

  • 任何会由于用户输入而导致 safe Rust 产生未定义行为的都是 unsound(不健全)的(尽管仅仅可能是理论上的).
  • 比如 unsafe 代码可能违反上述承诺而产生未定义行为.
  • Unsound 代码对稳定性和安全性造成风险, 且违背了大部分 Rust 用户的基本假设.
fn unsound_ref<T>(x: &T) -> &u128 {      // Signature looks safe to users. Happens to be
    unsafe { mem::transmute(x) }         // ok if invoked with an &u128, UB for practically
}                                        // everything else.
 

负责任地使用 unsafe 💬

  • 除非非用不可, 不要使用 unsafe.
  • Follow the Nomicon, Unsafe Guidelines, always uphold all safety invariants, and never invoke UB.
  • 最小化 unsafe 用例, 封装成易于评审的小的, 优雅的模块.
  • Never create unsound abstractions; if you can't encapsulate unsafe properly, don't do it.
  • 每个 unsafe 用例应当同时提供关于其安全性的纯文本理由提要.
 

API 稳定性#

When updating an API, these changes can break client code.RFC Major changes (🔴) are definitely breaking, while minor changes (🟡) might be breaking:

 
Crate
🔴 编写一个 stable 的 crate 但却依赖了 nightly.
🟡 修改了 Cargo 的功能(比如添加或移除功能)
 
模块
🔴 重命名, 移动, 移除任何公开项.
🟡 添加新的公开项, 因为 use your_crate::* 可能会破坏现有代码.
 
结构体
🔴 当所有字段都为公开时添加私有字段.
🔴 当没有私有字段时添加公开字段.
🟡 当至少有一个字段时添加或移除私有字段(在更改前或更改后).
🟡 将有私有字段(至少有一个字段)的元组结构转换到普通结构, 或反之.
 
枚举
🔴 Adding new variants; can be mitigated with early #[non_exhaustive] REF
🔴 Adding new fields to a variant.
 
Trait
🔴 添加非默认项, 将会破坏已有的 impl T for S {}.
🔴 任何不必要的项签名修改, 都会影响到使用者或者实现方.
🟡 添加一个默认项, 可能会和另一个 trait 产生歧义.
🟡 添加默认类型参数.
 
Trait
🔴 实现任何「基本」trait.不去实现一个基本 trait 是一种最基本的承诺.
🟡 实现任何非基本的 trait, 可能会导致歧义.
 
固有实现
🟡 添加内部项, 可能会导致客户端倾向于调用这个 trait 的 fn 而导致编译错误.
 
类型定义签名
🔴 强约束(如 <T><T: Clone>).
🟡 弱约束.
🟡 添加默认类型参数.
🟡 泛型归纳.
函数签名
🔴 添加或移除参数.
🟡 引入新的类型参数.
🟡 泛型归纳.
 
行为更改
🔴 / 🟡 改变语义可能不会导致编译器错误, 但可能会使用户产生错误的逻辑.
 

附录#

这里列出了其他的优秀指南和图表.

备忘清单说明
Rust Learning⭐ (中文)可能是学习 Rust 最好的链接合集.
Functional Jargon in RustRust 的函数式编程术语解释合集.
Periodic Table of Types解释各种类型和引用是如何联系在一起的.
Futures如何使用 Future.
Rust Iterator Cheat Sheetstd::iteritertools 的迭代器相关方法总结.
Type-Based Rust Cheat Sheet常见类型和方法表. 可以打印下来挂墙上.
 

多数 Rust 资料都由社区开发.

书记 ️📚说明
The Rust Programming Language (中文)《Rust 程序设计语言》, 入门必备.
     API Guidelines如何编写符合惯例可复用的 Rust.
     Asynchronous Programming 🚧解释 async 代码, Futures, ...
     Design Patterns惯例, 模式和反模式.
     Edition Guide与 Rust 2015, Rust 2018 等各版本打交道.
     Guide to Rustc Development解释编译器内部如何工作.
     Little Book of Rust Macros社区对 Rust 宏的经验总结.
     Reference 🚧 (中文)《Rust 参考手册》.
     RFC Book已接受的 RFC 文档, 可以看到它们是如何影响语言的.
     Performance Book改进速度与内存使用的技术.
     Rust Cookbook已被证明是良好实践的例程集.
     Rust in Easy English用入门级的英语讲的 Rust, 适合入门, 也适合英语初学者.
     Rust for the Polyglot Programmer经验者的指南.
     Rustdoc Book如何自定义 cargo docrustdoc.
     Rustonomicon (中文)《Rust 秘典》, 旧称《死灵书》, 讲述 Rust unsafe 编程的暗黑艺术.
     Unsafe Code Guidelines 🚧编写 unsafe 代码的简要指南.
     Unstable Book关于 unstable 的信息, 如 #![feature(...)].
The Cargo Book如何使用 cargo 以及修改 Cargo.toml.
The CLI Book如何创建 CLI 工具.
The Embedded Book在嵌入式和 #![no_std] 下工作.
     The Embedonomicon运行在 Cortex-M 的首个 #![no_std] 指南.
The WebAssembly Book和 Web 以及 .wasm 打交道.
     The wasm-bindgen Guide如何生产 Rust 和 JavaScript API 的绑定.

其他非官方书籍参见 Little Book of Rust Books.

 

通用组件的综合查找表.

列表 📋说明
Rust Changelog查找某个特定版本的变更记录.
Rust Forge列出了为编译器奋斗的组织和贡献者.
     Rust Platform Support所有支持的平台和优先级 (Tier).
     Rust Component History检查某个平台上 Rust 工具链在 nightly 能否正常工作.
ALL the Clippy Lints列出了所有你可能感兴趣的 clippy lints.
Configuring Rustfmt列出了所有你可以在 .rustfmt.toml 中设置的 rustfmt 选项.
Compiler Error Index想要知道 E0404 什么意思吗?
 

提供信息或工具的在线服务.

服务 ⚙️说明
crates.ioRust 的所有第三方库.
std.rs标准库 std 文档短链接.
docs.rs第三方库文档, 都是自动从源码构建的.
lib.rsRust 非官方的库和应用.
caniuse.rs检查某个特性在哪个 Rust 版本引入或稳定.
Rust Playground分享一些 Rust 代码片段.
Rust Search Extension用于搜索文档, crate, 属性和书籍的浏览器插件.
 

打印 PDF#

点击这里找到最新的 PDF 文件下载即可. 自己也可以直接通过文件 > 打印并选择“保存成 PDF”(Chrome 和 Edge 是可以的, Firefox 可能有点问题).

Fork me on GitHub