Fork me on GitHub

Rust 语言速查表 9. July 2025

包含可点击链接,指向 The Book、BK Rust by Example、EX 标准库文档、STD NomiconNOM 和 Reference。REF

可点击的符号

BK The Book。
EX Rust by Example。
STD 标准库 (API)。
NOM Nomicon。
REF Reference。
RFC 官方 RFC 文档。
🔗 互联网。
↑ 在此页面上方。
↓ 在此页面下方。

其他符号

🗑️ 大部分已弃用。
'18 有最低版本要求。
🚧 需要 Rust nightly(或不完整)。
🛑 故意的错误示例或陷阱。
🝖 稍微深奥,很少使用或高级。
🔥 具有卓越效用的东西。
↪  父项展开为 …
💬 个人观点。
? 缺少好的链接或解释。

➕
字体连字 (..=, =>) 夜间模式 💡 X-Ray 📈
X-Ray 可视化已启用。这些显示了每个部分的汇总反馈。目前这是实验性的。已知问题:
  • 一些收到大量反馈的部分没有显示(例如,“Hello Rust”)
  • 它没有考虑部分的年龄(有些部分已经存在多年,有些只有几个月)
  • 它既没有考虑反馈爆发(人们连续按同一个按钮3次),也没有考虑“最新反馈”(人们先赞后踩)。
反馈格式为(正面,负面,文本),等同于使用反馈按钮。

语言结构

幕后探秘

内存布局

杂项

标准库

工具链

处理类型

编码指南

你好,Rust!url

如果你是 Rust 新手,或者想尝试下面的内容:

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

Rust 在可衡量方面做得非常好的事情

你可能会遇到的问题

  • 学习曲线陡峭;1 编译器强制执行的规则(尤其是内存规则),在其他语言中可能只是“最佳实践”。
  • 在某些领域、目标平台(尤其是嵌入式)和 IDE 功能方面,缺少 Rust 原生库。1
  • 编译时间比其他语言中的“类似”代码要长。1
  • 库中粗心(使用 unsafe)的代码可能会偷偷破坏安全保证。
  • 没有正式的语言规范,🔗 可能会妨碍在某些领域(航空、医疗等)的合法使用。🔗
  • Rust 基金会可能会滥用其知识产权来影响“Rust”项目(例如,禁止使用名称,强制实施政策)。🔗🔗2

1 比较 Rust 调查。
2 避免使用他们的标记(例如,在你的名称、URL、logo、着装中)可能就足够了。

下载

IDE

模块化入门资源

此外,请考虑 The Book、BK Rust by Example、EX 标准库STD 和 Learn Rust。🔗

个人观点 💬 — 如果你从未见过或使用过任何 Rust,最好在继续之前访问上面的链接之一;否则下一章可能会感觉有点简洁。

数据结构url

通过关键字定义的数据类型和内存位置。

示例解释
struct S {}定义一个结构体 (struct) BK EX STD REF,包含命名字段。
     struct S { x: T }定义带有命名字段 x(类型为 T)的结构体。
     struct S ​(T);定义“元组”结构体,其编号字段 .0 的类型为 T。
     struct S;定义零大小 NOM 的单元结构体。不占空间,会被优化掉。
enum E {}定义一个枚举 (enum),BK EX REF 类似于代数数据类型、带标签的联合体。
     enum E { A, B​(), C {} }定义枚举的变体;可以是单元式 A、元组式 B ​() 和结构体式 C{}。
     enum E { A = 1 }带有显式判别值的枚举,REF 例如用于 FFI。
     enum E {}没有变体的枚举是不可构造的 (uninhabited),REF 无法创建,类似于 'never' ↓ 🝖
union U {}不安全的类 C 联合体 (union) REF,用于 FFI 兼容性。 🝖
static X: T = T();全局变量 BK EX REF,具有 'static 生命周期,单一🛑1内存位置。
const X: T = T();定义常量,BK EX REF 在使用时会复制到一个临时位置。
let x: T;在栈上分配 T 字节2,绑定为 x。可赋值一次,但不可变。
let mut x: T;类似于 let,但允许可变性 (mutability) BK EX 和可变借用。3
     x = y;将 y 移动到 x,如果 T 不是 Copy STD,则 y 失效;否则复制 y。

1 在库中,你可能会秘密地得到多个 X 的实例,这取决于你的 crate 是如何被导入的。🔗
2 绑定变量 BK EX REF 在同步代码中存在于栈上。在 async {} 中,它们成为 async 状态机的一部分,可能驻留在堆上。
3 从技术上讲,可变 (mutable) 和 不可变 (immutable) 是用词不当。不可变绑定或共享引用仍然可以包含 Cell STD,从而提供内部可变性。

 

创建和访问数据结构;以及一些更具符号意义的类型。

示例解释
S { x: y }创建 struct S {} 或 use 引入的 enum E::S {},字段 x 设置为 y。
S { x }同上,但使用局部变量 x 作为字段 x 的值。
S { ..s }从 s 中填充剩余字段,对 Default::default() 尤其有用。STD
S { 0: x }类似于下面的 S ​(x),但使用结构体语法设置字段 .0。
S​ (x)创建 struct S ​(T) 或 use 引入的 enum E::S​ (),字段 .0 设置为 x。
S如果 S 是单元结构体 struct S; 或 use 引入的 enum E::S,则创建 S 类型的值。
E::C { x: y }创建枚举变体 C。上述其他方法也适用。
()空元组,既是字面量也是类型,也称为单元 (unit)。STD
(x)带括号的表达式。
(x,)单元素**元组 (tuple)**表达式。EX STD REF
(S,)单元素元组类型。
[S]未指定长度的数组类型,即切片 (slice)。EX STD REF 不能存在于栈上。*
[S; n]固定长度 n 的数组类型 (array type) EX STD REF,包含 S 类型的元素。
[x; n]包含 n 个 x 副本的数组实例 (array instance) REF(表达式)。
[x, y]包含给定元素 x 和 y 的数组实例。
x[0]集合索引,此处使用 usize。通过 Index、IndexMut 实现。
     x[..]同上,通过范围(此处为全范围),也可是 x[a..b]、x[a..=b] 等,见下文。
a..b创建右侧不包含的范围 (range) STD REF,例如 1..3 表示 1, 2。
..b无起点的右侧不包含的到…的范围 (range to) STD。
..=b无起点的到…的包含范围 (inclusive range to) STD。
a..=b包含范围,STD 1..=3 表示 1, 2, 3。
a..无终点的从…开始的范围 (range from) STD。
..全范围 (full range),STD 通常表示整个集合。
s.x命名字段访问,REF 如果 x 不是 S 类型的一部分,可能会尝试 Deref。
s.0编号字段访问,用于元组类型 S ​(T)。

* 目前是这样,RFC 等待 跟踪问题 完成。

引用与指针url

授予对非自有内存的访问权限。另请参见泛型与约束部分。

示例解释
&S共享引用 (reference) BK STD NOM REF(类型;用于存放任何 &s 的空间)。
     &[S]特殊的切片引用,包含 (地址, 数量)。
     &str特殊的字符串切片引用,包含 (地址, 字节长度)。
     &mut S允许修改的独占引用(也适用于 &mut [S], &mut dyn S 等)。
     &dyn T特殊的trait 对象 (trait object) BK REF 引用,形式为 (地址, 虚函数表);T 必须是对象安全的。 REF
&s共享借用 (borrow) BK EX STD(例如,这个 s 的地址、长度、虚函数表等,如 0x1234)。
     &mut s允许可变性的独占借用。EX
*const S不可变的裸指针类型 (raw pointer type) BK STD REF,不提供内存安全保证。
     *mut S可变的裸指针类型,不提供内存安全保证。
     &raw const s不通过引用直接创建裸指针;类似于 ptr:addr_of!() STD 🝖
     &raw mut s同上,但可变。🚧 用于未对齐、紧凑的字段。🝖
ref s通过引用绑定 (bind by reference),EX 使绑定成为引用类型。🗑️
     let ref r = s;等同于 let r = &s。
     let S { ref mut x } = s;可变引用绑定 (let x = &mut s.x),是解构↓的简写版本。
*r解引用 (dereference) BK STD NOM 一个引用 r 以访问它指向的内容。
     *r = s;如果 r 是一个可变引用,则将 s 移动或复制到目标内存。
     s = *r;如果 *r 是 Copy 类型,则将 s 设为 r 所引用内容的副本。
     s = *r;如果 *r 不是 Copy 类型,则无法工作 🛑,因为这会移动并留下空值。
     s = *my_box;对于**Box**STD的特殊情况🔗,可以移出非 Copy 的借用内容。
'a生命周期参数,BK EX NOM REF 静态分析中一个流程的持续时间。
     &'a S只接受某个 s 的地址;该地址存在时间为 'a 或更长。
     &'a mut S同上,但允许地址内容被改变。
     struct S<'a> {}表明这个 S 将包含一个生命周期为 'a 的地址。S 的创建者决定 'a。
     trait T<'a> {}表明任何 impl T for S 的 S 可能包含一个地址。
     fn f<'a>(t: &'a T)表明此函数处理某个地址。调用者决定 'a。
'static特殊的生命周期,持续整个程序执行期间。

函数与行为url

定义代码单元及其抽象。

示例解释
trait T {}定义一个 trait;BK EX REF 类型可以遵循的共同行为。
trait T : R {}T 是父 trait (supertrait) BK EX REF R 的子 trait。任何 S 必须先 impl R 才能 impl T。
impl S {}为类型 S 实现 (implementation) REF 功能,例如方法。
impl T for S {}为类型 S 实现 trait T;指定 S 究竟如何表现得像 T。
impl !T for S {}禁用一个自动派生的 auto trait。NOM REF 🚧 🝖
fn f() {}定义一个函数 (function);BK EX REF 如果在 impl 内部,则是关联函数。
     fn f() -> S {}同上,返回一个 S 类型的值。
     fn f(&self) {}定义一个方法 (method),BK EX REF 例如在 impl S {} 内部。
struct S ​(T);更晦涩地,也↑定义了一个 fn S(x: T) -> S 的构造函数。 RFC 🝖
const fn f() {}可在编译时使用的常量 fn,例如 const X: u32 = f(Y);。 REF '18
     const { x }在函数内部使用,确保 { x } 在编译期间求值。REF
async fn f() {}异步 (async) REF '18 函数转换,↓ 使 f 返回一个 impl Future。STD
     async fn f() -> S {}同上,但使 f 返回一个 impl Future<Output=S>。
     async { x }在函数内部使用,使 { x } 成为一个 impl Future<Output=X>。REF
     async move { x }将捕获的变量移动到 future 中,类似于 move 闭包。 REF ↓
fn() -> S函数引用 (function references),1 BK STD REF 持有可调用实体地址的内存。
Fn() -> S可调用 trait (callable trait) BK STD(还有 FnMut, FnOnce),由闭包、函数等实现。
AsyncFn() -> S可调用异步 trait (callable async trait) STD(还有 AsyncFnMut, AsyncFnOnce),由异步实体实现。
|| {} 一个闭包 (closure) BK EX REF,它借用其捕获 (captures),↓ REF (例如,一个局部变量)。
     |x| {}接受一个名为 x 的参数的闭包,主体是块表达式。
     |x| x + x同上,但没有块表达式;只能由单个表达式组成。
     move |x| x + y 移动闭包 (move closure) REF,获取所有权;即,y 被转移到闭包中。
     async |x| x + x异步闭包 (async closure)。REF 将其结果转换为一个 impl Future<Output=X>。
     async move |x| x + y异步移动闭包 (async move closure)。以上两者的结合。
     return || true 闭包有时看起来像逻辑或(这里:返回一个闭包)。
unsafe如果你喜欢调试段错误;不安全代码 (unsafe code)。↓ BK EX NOM REF
     unsafe fn f() {}意味着“调用可能导致 UB,↓ 你必须检查前提条件”。
     unsafe trait T {}意味着“对 T 的粗心实现可能导致 UB;实现者必须检查”。
     unsafe { f(); }向编译器保证“我已经检查过前提条件,相信我”。
     unsafe impl T for S {}保证*S 相对于 T 的行为是良好的*;人们可以在 S 上安全地使用 T。
     unsafe extern "abi" {}从 Rust 2024 开始,extern "abi" {} 块↓ 必须是 unsafe 的。
          pub safe fn f();在 unsafe extern "abi" {} 内部,标记 f 实际上可以安全调用。RFC

1 大多数文档称它们为函数指针 (pointers),但函数引用 (references) 可能更合适🔗,因为它们不能是 null,并且必须指向有效的目标。

控制流url

控制函数内的执行流程。

示例解释
while x {}循环 (loop),REF 当表达式 x 为真时运行。
loop {}无限循环 REF 直到遇到 break。可以使用 break x 产生一个值。
for x in collection {}遍历 迭代器 (iterators) 的语法糖。BK STD REF
     ↪  collection.into_iter() 实际上,首先将任何 IntoIterator STD 类型转换为适当的迭代器。
     ↪  iterator.next() 然后在适当的 Iterator STD 上 x = next() 直到耗尽(第一个 None)。
if x {} else {}当表达式为真时的条件分支 REF。
'label: {}块标签 (block label),RFC 可与 break 一起使用以退出此块。1.65+
'label: loop {}类似的循环标签 (loop label),EX REF 在嵌套循环中用于流控制很有用。
breakBreak 表达式 REF,用于退出一个带标签的块或循环。
     break 'label x从名为 'label 的块或循环中断出,并使其值为 x。
     break 'label同上,但不产生任何值。
     break x使 x 成为最内层循环的值(仅在实际的 loop 中)。
continue Continue 表达式 REF,进入此循环的下一次迭代。
continue 'label同上,但不是此循环,而是标记为 'label 的外层循环。
x?如果 x 是 Err 或 None,则返回并传播。BK EX STD REF
x.await获取 future、轮询、让出的语法糖。REF '18 仅在 async 内部使用。
     ↪  x.into_future() 实际上,首先将任何 IntoFuture STD 类型转换为适当的 future。
     ↪  future.poll() 然后在适当的 Future STD 上 poll(),如果为 Poll::Pending,则让出流程。STD
return x从函数提前返回 (early return) REF。更惯用的做法是以表达式结束。
     { return }在普通的 {} 块内,return 会退出外围函数。
     || { return }在闭包内,return 仅退出该闭包,即闭包类似于一个函数。
     async { return }在 async 内部,return 仅REF 🛑 退出该 {}, 即 async {} 类似于一个函数。
f()调用可调用实体 f(例如,函数、闭包、函数指针、Fn 等)。
x.f()调用成员函数,要求 f 的第一个参数是 self、&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 解引用到 S,即 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()。

代码组织url

将项目分割成更小的单元并最小化依赖。

示例解释
mod m {}定义一个模块 (module),BK EX REF 从 {} 内部获取定义。↓
mod m;定义一个模块,从 m.rs 或 m/mod.rs 获取定义。↓
a::b命名空间路径 (path) EX REF 指向 a(mod、enum 等)中的元素 b。
     ::b在crate 根 '15 REF 或外部 prelude 中搜索 b;'18 REF 全局路径。REF 🗑️
     crate::b在 crate 根中搜索 b。'18
     self::b在当前模块中搜索 b。
     super::b在父模块中搜索 b。
use a::b;在此作用域内直接使用 (use) EX REF b,不再需要 a。
use a::{b, c};同上,但将 b 和 c 引入作用域。
use a::b as x;将 b 引入作用域但命名为 x,如 use std::error::Error as E。
use a::b as _;将 b 匿名引入作用域,对于名称冲突的 trait 很有用。
use a::*;引入 a 中的所有内容,仅当 a 是某种prelude时推荐使用。STD 🔗
pub use a::b;将 a::b 引入作用域并从此处重新导出。
pub T“如果父路径是公开的,则公开”的可见性 (visibility) BK REF 作用于 T。
     pub(crate) T最多1在当前 crate 中可见。
     pub(super) T最多1在父模块中可见。
     pub(self) T最多1在当前模块中可见(默认,与没有 pub 相同)。
     pub(in a::b) T最多1在祖先 a::b 中可见。
extern crate a;声明对外部crate的依赖;BK REF 🗑️ 在 '18 中只需 use a::b。
extern "C" {}声明来自 FFI 的外部依赖和 ABI(例如 "C")。BK EX NOM REF
extern "C" fn f() {}定义一个要导出到 FFI 的函数,其 ABI 为 "C"。

1 子模块中的项总是可以访问任何项,无论是否 pub。

类型别名与转换url

类型的简写名称,以及将一种类型转换为另一种类型的方法。

示例解释
type T = S;创建一个类型别名 (type alias),BK REF 即 S 的另一个名称。
Self实现类型的类型别名,REF 例如 fn new() -> Self。
selffn f(self) {} 中的方法主语 (method subject) BK REF,类似于 fn f(self: Self) {}。
     &self同上,但指代被借用的 self,等同于 f(self: &Self)。
     &mut self同上,但可变借用,等同于 f(self: &mut Self)。
     self: Box<Self>任意 self 类型 (arbitrary self type),为智能指针添加方法(my_box.f_of_self())。
<S as T>消除歧义 (disambiguate) BK REF 将类型 S 视为 trait T,例如 <S as T>::f()。
a::b as c在 use 符号时,将 S 导入为 R,例如 use a::S as R。
x as u32原始类型转换 (cast),EX REF 可能会截断并有点出人意料。1 NOM

1 参见下文的类型转换以了解所有类型之间的转换方式。

宏与属性url

在实际编译发生前展开的代码生成结构。

示例解释
m!()宏 (macro) BK STD REF 调用,也可是 m!{}、m![](取决于宏)。
#[attr]外部属性 (attribute),EX REF 用于注解后面的项。
#![attr]内部属性,用于注解上方的、包围的项。
 
宏内部 1解释
$x:ty宏捕获,:ty 片段指定符 (fragment specifier) REF ,2 声明了 $x 可以是什么。
$x宏替换,例如使用上面捕获的 $x:ty。
$(x),*宏重复 (repetition) REF 零次或多次。
     $(x),+同上,但一次或多次。
     $(x)?同上,但零次或一次(分隔符不适用)。
     $(x)<<+实际上,除 , 之外的分隔符也被接受。这里是 <<。

1 适用于 **“示例宏 (macros by example)” **。REF
2 参见下文的工具指令以了解所有片段指定符。

模式匹配url

存在于 match 或 let 表达式,或函数参数中的结构。

示例解释
match m {}启动模式匹配 (pattern matching),BK EX REF 然后使用匹配臂,参见下表。
let S(x) = get();值得注意的是,let 也会解构 (destructures) EX,类似于下表。
     let S { x } = s;只有 x 会绑定到值 s.x。
     let (_, b, _) = abc;只有 b 会绑定到值 abc.1。
     let (a, ..) = abc;忽略“其余部分”也有效。
     let (.., a, b) = (1, 2);特定绑定优先于“其余部分”,这里 a 是 1,b 是 2。
     let s @ S { x } = get();将 s 绑定到 S,同时将 x 绑定到 s.x,模式绑定 (pattern binding),BK EX REF 参见下文 🝖
     let w @ t @ f = get();将 get() 结果的 3 个副本分别存储在 w、t、f 中。🝖
     let (|x| x) = get();病态的或模式,↓ 不是闭包。🛑 与 let x = get(); 相同。🝖
let Ok(x) = f();如果模式可以被反驳 (refuted),则无法工作 🛑,REF 请改用 let else 或 if let。
let Ok(x) = f();但如果替代方案不可构造,则可以工作,例如 f 返回 Result<T, !> 1.82+
let Ok(x) = f() else {};尝试赋值,RFC 如果不匹配则执行 else {},其中必须包含 break、return、panic! 等。1.65+ 🔥
if let Ok(x) = f() {}如果模式可以被赋值(例如 enum 变体),则进入分支,是语法糖。*
if let … && let … { }Let 链,REF 无需嵌套即可使用多个绑定。'24
while let Ok(x) = f() {}等效;此处持续调用 f(),只要模式可以被赋值,就运行 {}。
fn f(S { x }: S)函数参数也像 let 一样工作,此处 x 绑定到 f(s) 的 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 } => {}匹配具有特定值的结构体(仅匹配 s.x 为 0 且 s.y 为 1 的 s)。
S { x: a, y: b } => {}匹配具有任何🛑值的结构体,并将 s.x 绑定到 a,s.y 绑定到 b。
     S { x, y } => {}同上,但是简写,将 s.x 和 s.y 分别绑定为 x 和 y。
S { .. } => {}匹配具有任何值的结构体。
D => {}如果 D 在 use 中,则匹配枚举变体 E::D。
D => {}匹配任何东西,并绑定到 D;如果 D 不在 use 中,则可能是 E::D 的假朋友🛑。
_ => {}正确的通配符,匹配任何东西 / “所有其余的”。
0 | 1 => {}模式替代,或模式 (or-patterns)。RFC
     E::A | E::Z => {}同上,但作用于枚举变体。
     E::C {x} | E::D {x} => {}同上,但如果所有变体都有 x,则绑定 x。
     Some(A | B) => {}同上,也可以匹配深度嵌套的替代项。
     |x| x => {}病态的或模式,↑🛑 前导的 | 被忽略,只是 x | x,因此是 x。🝖
     |x => {}类似地,前导的 | 被忽略。🝖
(a, 0) => {}匹配元组,a 为任何值,第二个为 0。
[a, 0] => {}切片模式,REF 🔗 匹配数组,a 为任何值,第二个为 0。
     [1, ..] => {}匹配以 1 开头的数组,其余为任何值;子切片模式 (subslice pattern)。REF RFC
     [1, .., 5] => {}匹配以 1 开头,以 5 结尾的数组。
     [1, x @ .., 5] => {}同上,但也将 x 绑定到表示中间部分的切片(参见模式绑定)。
     [a, x @ .., b] => {}同上,但匹配任何第一个、最后一个,并分别绑定为 a、b。
1 .. 3 => {}范围模式,BK REF 此处匹配 1 和 2;部分不稳定。🚧
     1 ..= 3 => {}包含范围模式,匹配 1、2 和 3。
     1 .. => {}开放范围模式,匹配 1 和任何更大的数。
x @ 1..=5 => {}将匹配项绑定到 x;模式绑定,BK EX REF 此处 x 将是 1 … 5。
     Err(x @ Error {..}) => {}也适用于嵌套,此处 x 绑定到 Error,在下面与 if 一起使用时特别有用。
S { x } if x > 10 => {}模式匹配守卫 (match guards),BK EX REF 条件也必须为真才能匹配。

泛型与约束url

泛型与类型构造器、trait 和函数相结合,为您的用户提供更大的灵活性。

示例解释
struct S<T> …带有类型参数(此处 T 是占位符)的泛型 (generic) BK EX 类型。
S<T> where T: RTrait 约束 (trait bound),BK EX REF 限制允许的 T,保证 T 具有 trait R。
     where T: R, P: S独立的 trait 约束,此处一个用于 T,一个用于(未显示的)P。
     where T: R, S编译错误,🛑 您可能想要下面的复合约束 R + S。
     where T: R + S复合 trait 约束 (compound trait bound),BK EX T 必须同时满足 R 和 S。
     where T: R + 'a同上,但带有生命周期。T 必须满足 R,如果 T 有生命周期,则必须比 'a 活得长。
     where T: ?Sized选择退出一个预定义的 trait 约束,此处是 Sized。?
     where T: 'a类型生命周期约束 (lifetime bound);EX 如果 T 有引用,它们必须比 'a 活得长。
     where T: 'static同上;并不意味着值 t 将🛑活 'static,只意味着它可以。
     where 'b: 'a生命周期 'b 必须至少活得和(即超过)'a 一样长。
     where u8: R<T>也可以创建涉及其他类型的条件语句。🝖
S<T: R>简写约束,几乎与上面相同,写起来更短。
S<const N: usize>泛型常量约束 (generic const bound);REF 类型 S 的用户可以提供常量值 N。
     S<10>在使用时,常量约束可以作为原始值提供。
     S<{5+5}>表达式必须放在花括号中。
S<T = R>默认参数;BK 使 S 更易于使用,但保持了灵活性。
     S<const N: u8 = 0>常量的默认参数;例如,在 f(x: S) {} 中参数 N 为 0。
     S<T = u8>类型的默认参数,例如,在 f(x: S) {} 中参数 T 为 u8。
S<'_>推断的匿名生命周期;要求编译器在明显的情况下 “自己搞定”。
S<_>推断的匿名类型,例如 let x: Vec<_> = iter.collect()
S::<T>Turbofish STD 调用点类型消歧,例如 f::<u32>()。
     E::<T>::A泛型枚举可以在其类型 E 上接收其类型参数 …
     E::A::<T>… 或在变体上(此处为 A);允许 Ok::<R, E>(r) 等。
trait T<X> {}一个泛型于 X 的 trait。可以有多个 impl T for S(每个 X 一个)。
trait T { type X; }定义关联类型 (associated type) BK REF RFC X。只有一个 impl T for S 是可能的。
trait T { type X<G>; }定义泛型关联类型 (GAT),RFC X 可以是泛型的 Vec<>。
trait T { type X<'a>; }定义一个泛型于生命周期的 GAT。
     type X = R;在 impl T for S { type X = R; } 中设置关联类型。
     type X<G> = R<G>;GAT 同样,例如 impl T for S { type X<G> = Vec<G>; }。
impl<T> S<T> {}泛型地为 S<T> 中的任何 T 实现 fn,REF 此处 T 是类型参数。
impl S<T> {}固有地为精确的 S<T> 实现 fn,REF 此处 T 是特定类型,例如 u8。
fn f() -> impl T存在类型 (existential types)(又名 RPIT),BK 返回一个调用者未知的、impl T 的 S。
     -> impl T + 'a表明隐藏类型至少活得和 'a 一样长。RFC
     -> impl T + use<'a>反而表明隐藏类型捕获了生命周期 'a,使用约束 (use bound)。🔗 ?
     -> impl T + use<'a, R>也表明隐藏类型可能从 R 捕获了生命周期。
     -> S<impl T>impl T 部分也可以在类型参数内部使用。
fn f(x: &impl T)通过“impl traits”的 trait 约束,BK 类似于下面的 fn f<S: T>(x: &S)。
fn f(x: &dyn T)通过 动态分发 (dynamic dispatch) 调用 f,BK REF f 将不会为 x 实例化。
fn f<X: T>(x: X)函数泛型于 X,f 将为每个 X 实例化(‘单态化’)。
fn f() where Self: R;在 trait T {} 中,使 f 仅在已知也 impl R 的类型上可访问。
     fn f() where Self: Sized;使用 Sized 可以使 f 不进入 trait 对象的虚函数表,从而启用 dyn T。
     fn f() where Self: R {}其他 R 在默认函数中有用(非默认的反正也需要实现)。

高阶项 🝖url

实际的类型和 trait,对某些东西进行抽象,通常是生命周期。

示例解释
for<'a>高阶约束 (higher-ranked bounds) 的标记。NOM REF 🝖
     trait T: for<'a> R<'a> {}任何 impl T 的 S 都必须对任何生命周期满足 R。
fn(&'a u8)函数指针类型,持有可使用特定生命周期 'a 调用的函数。
for<'a> fn(&'a u8)高阶类型1 🔗,持有可使用任何生命周期调用的函数;是上述类型的子类型↓。
     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) {}对于函数指针,当调用接受特定生命周期 'a 时,实现 trait T。
impl T for for<'a> fn(&'a u8) {}对于函数指针,当调用接受任何生命周期时,实现 trait T。
     impl T for fn(&u8) {}同上,简写版本。

字符串与字符url

Rust 有多种方式创建文本值。

示例解释
"..."字符串字面量 (string literal),REF, 1 一个 UTF-8 的 &'static str,STD 支持以下转义:
     "\n\r\t\0\\"常见转义 REF,例如 "\n" 变成换行符。
     "\x36"ASCII 转义 REF,最高到 7f,例如 "\x36" 会变成 6。
     "\u{7fff}"Unicode 转义 REF,最多 6 位,例如 "\u{7fff}" 变成 翿。
r"..."原始字符串字面量 (raw string literal)。REF, 1 UTF-8,但不会解释上面的任何转义。
r#"..."#原始字符串字面量,UTF-8,但也可以包含 "。# 的数量可以变化。
c"..."C 字符串字面量 (C string literal),REF 一个以 NUL 结尾的 &'static CStr,STD 用于 FFI。1.77+
cr"...", cr#"..."#原始 C 字符串字面量,是上述的类似组合。
b"..."字节字符串字面量 (byte string literal);REF, 1 构造一个只含 ASCII 的 &'static [u8; N]。
br"...", br#"..."#原始字节字符串字面量,是上述的类似组合。
b'x'ASCII 字节字面量 (byte literal),REF 一个单独的 u8 字节。
'🦀'字符字面量 (character literal),REF 一个固定 4 字节的 Unicode 'char'。STD

1 支持多行。只需记住 Debug↓(例如 dbg!(x) 和 println!("{x:?}"))可能会将其渲染为 \n,而 Display↓(例如 println!("{x}"))会正确地渲染它。

文档url

调试器都恨他。用这个奇怪的技巧避免 bug。

示例解释
///外部行文档注释,1 BK EX REF 在类型、trait、函数等上使用。
//!内部行文档注释,主要用于文件顶部。
//行注释,用于记录代码流程或内部实现。
/* … */块注释。2 🗑️
/** … */外部块文档注释。2 🗑️
/*! … */内部块文档注释。2 🗑️

1 工具指令概述了您可以在文档注释中做什么。
2 由于用户体验不佳,通常不鼓励使用。如果可能,请使用等效的行注释并利用 IDE 支持。

杂项url

这些符号不适合任何其他类别,但了解它们仍然很有用。

示例解释
!总是为空的never 类型。BK EX STD REF
     fn f() -> ! {}永不返回的函数;与任何类型兼容,例如 let x: u8 = f();
     fn f() -> Result<(), !> {}必须返回 Result 但表明它永远不会 Err 的函数。🚧
     fn f(x: !) {}存在但永远不能被调用的函数。不太有用。🝖 🚧
_未命名的通配符REF变量绑定,例如 |x, _| {}。
     let _ = x;未命名赋值是空操作,不会🛑移出 x 或保留作用域!
     _ = x;你可以将任何东西赋值给 _ 而无需 let,即 _ = ignore_rval(); 🔥
_x不会产生未使用变量警告的变量绑定。
1_234_567用于视觉清晰度的数字分隔符。
1_u8数字字面量的类型指定符EX REF(也可是 i8、u16 等)。
0xBEEF, 0o777, 0b1001十六进制(0x)、八进制(0o)和二进制(0b)整数字面量。
12.3e4, 1E-8浮点数字面量的科学记数法。REF
r#foo用于版本兼容性的原始标识符 (raw identifier) BK EX。🝖
'r#a用于版本兼容性的原始生命周期标签 (raw lifetime label) ?。🝖
x;语句 (statement) REF 终止符,区别于表达式 (expressions) EX REF

常见运算符url

Rust 支持您所期望的大多数运算符(+, *, %, =, == 等),包括重载。STD 由于它们在 Rust 中的行为没有不同,我们在此不列出它们。


幕后探秘url

可能会对你的心智造成可怕影响的神秘知识,强烈推荐。

抽象机url

与 C 和 C++ 一样,Rust 基于一个抽象机。

Rust → CPU
🛑 误导性的。
Rust → 抽象机 → CPU
正确的。
 

除了极少数例外,你永远不被“允许”去思考实际的 CPU。你是在为一个抽象的 CPU 编写代码。然后 Rust(在某种程度上)理解你想要什么,并将其转换为实际的 RISC-V / x86 / … 机器码。

 

这个抽象机

  • 不是一个运行时,也没有任何运行时开销,而是一个计算模型抽象,
  • 包含诸如内存区域(栈等)、执行语义等概念,
  • 知道并看到你的 CPU 可能不关心的东西,
  • 实际上是你和编译器之间的契约,
  • 并利用以上所有特性进行优化。

左边是人们可能错误地认为如果 Rust 直接面向 CPU 应该能侥幸成功的事情。右边是如果你违反了抽象机(AM)的契约,实际上会干扰到的事情。

 
没有 AM有 AM
0xffff_ffff 会是一个有效的 char。🛑AM 可能会利用 “无效” 的位模式来打包不相关的数据。
0xff 和 0xff 是相同的指针。🛑AM 的指针可以有来源 (provenance) STD 用于优化。
对指针 0xff 的任何读/写总是没问题。🛑AM 可能会发出缓存友好的操作,因为 “不可能有读取” 。
读取未初始化的内存只会得到随机值。🛑AM “知道” 读取不可能,可能会移除所有相关代码。
数据竞争只会得到随机值。🛑AM 可能会拆分读/写,产生不可能的值。↓
空引用只是某个寄存器中的 0x0。🛑在引用中持有 0x0 会召唤克苏鲁。
 

这个表格只是为了概述 AM 的作用。与 C 或 C++ 不同,Rust 永远不会让你做错事,除非你用 unsafe 强制它。↓

语法糖url

如果有些事情“在你仔细思考后觉得不应该能工作”但它却能工作,那可能就是因为下面这些原因之一。

名称描述
类型强制转换 (Coercions) NOM弱化类型以匹配签名,例如,&mut T 转为 &T;参见类型转换 ↓
解引用 (Deref) NOM 🔗解引用 x: T 直到 *x, **x, … 与某个目标 S 兼容。
预导入 (Prelude) STD自动导入基本项,例如 Option, drop(), …
重新借用 (Reborrow) 🔗由于 x: &mut T 不能被复制;因此移动一个新的 &mut *x 来代替。
生命周期省略 (Lifetime Elision) BK NOM REF允许你写 f(x: &T),而不是 f<'a>(x: &'a T),以求简洁。
生命周期扩展 (Lifetime Extensions) 🔗 REF在 let x = &tmp().f 和类似情况下,将临时值保留到该行之后。
方法解析 (Method Resolution) REF解引用或借用 x 直到 x.f() 工作。
匹配人体工程学 (Match Ergonomics) RFC重复解引用被匹配表达式 (scrutinee) 并向绑定添加 ref 和 ref mut。
右值静态提升 (Rvalue Static Promotion) RFC 🝖使对常量的引用变为 'static,例如 &42, &None, &mut []。
双重定义 (Dual Definitions) RFC 🝖定义一个(例如 struct S(u8))会隐式定义另一个(例如 fn S)。
Drop 隐藏流 (Drop Hidden Flow) REF 🝖在块 { ... } 结束或 _ 赋值时,可能会调用 T::drop()。STD
Drop 不可调用 (Drop Not Callable) STD 🝖编译器禁止显式调用 T::drop(),必须使用 mem::drop()。STD
自动 Trait (Auto Traits) REF如果可能,总是为你的类型、闭包、future 实现。
 

个人观点 💬 — 这些特性让你使用 Rust 更容易,但却阻碍了你学习它。如果你想建立真正的理解,花些额外的时间去探索它们。

内存与生命周期url

一个关于移动、引用和生命周期的图文指南。

应用程序内存 S(1) 应用程序内存
  • 在底层,应用程序内存只是一组字节数组。
  • 运行环境通常会将其分段,其中包括:
    • 栈 (stack)(小而低开销的内存,1 大多数变量放在这里),
    • 堆 (heap)(大而灵活的内存,但总是通过像 Box<T> 这样的栈代理来处理),
    • 静态区 (static)(最常用作 &str 中 str 部分的存放地),
    • 代码区 (code)(你的函数二进制码所在的地方)。
  • 最棘手的部分与栈如何演变有关,这是我们的重点。

1 对于固定大小的值,栈的管理非常简单:在你需要时多拿几个字节,一旦你离开就丢弃。然而,向这些短暂的位置分发指针,正是生命周期存在的本质;也是本章其余部分的主题。

变量 S(1) S(1) 变量
let t = S(1);
  • 保留一个名为 t 的内存位置,其类型为 S,里面存储的值为 S(1)。
  • 如果用 let 声明,该位置存在于栈上。1
  • 注意语言上的模糊性,在术语**变量**中,它可以指:
    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 的尝试都是未定义行为。↓
  • 我们在这里不明确讨论 Copy 类型。它们会稍微改变规则,但不多:
    • 它们不会被 drop。
    • 它们永远不会留下一个“空的”变量位置。
类型安全 M { … } ⛔ 类型安全
let c: S = M::new();
  • 变量的类型有几个重要的作用,它:
    1. 规定了底层位应该如何被解释,
    2. 只允许对这些位进行定义良好的操作,
    3. 防止随机的其他值或位被写入该位置。
  • 这里赋值编译失败,因为 M::new() 的字节不能被转换为 S 类型的形式。
  • 类型之间的转换通常总是会失败,除非有明确的规则允许(强制转换、类型转换等)。
作用域与 Drop S(1)▼ C(2) S(2)▼ S(3) 作用域与 Drop
{
    let mut c = S(2);
    c = S(3);  // <- 在赋值前对 `c` 调用 Drop。
    let t = S(1);
    let a = t;
}   // <- `a`, `t`, `c` 的作用域在此结束,对 `a`, `c` 调用 drop。
  • 一旦一个未被移出的变量的“名称”离开其(drop-)作用域,其包含的值将被drop。
    • 经验法则:执行到达变量名称离开其定义的 {} 块的点。
    • 细节上更复杂,尤其是临时值等。
  • 当新值赋给现有变量位置时,也会调用 Drop。
  • 在这种情况下,Drop::drop() 会在该值的位置上被调用。
    • 在上面的例子中,drop() 在 a 和 c 上被调用了两次,但没有在 t 上调用。
  • 大多数非 Copy 的值在大多数时候都会被 drop;例外包括 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);          // 将新的 S 值设置到 r-目标是有效的。
  • 引用可以读取(&S)也可以写入(&mut S)它们指向的位置。
  • 解引用 *r 意味着使用**r 指向的位置**(不是 r 本身的位置或值)。
  • 在这个例子中,克隆 d 是从 *r 创建的,S(2) 被写入到 *r。
    • 我们假设 S 实现了 Clone,并且 r.clone() 克隆的是 r 的目标,而不是 r 本身。
    • 在赋值 *r = … 时,位置中的旧值也会被 drop(上图中未显示)。
 ▼ 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 当作有效的是未定义行为。↓
C(2) 0x3 事物的“生命周期”
  • 程序中的每个实体都有其相关的(时间/空间)范围,即其存活期间。
  • 粗略地说,这个存活时间可以是1
    1. 一个项可用的代码行(LOC)(例如,一个模块名)。
    2. 一个位置被值初始化和该位置被放弃之间的代码行。
    3. 一个位置首次以某种方式被使用和该使用停止之间的代码行。
    4. 一个值被创建和该值被 drop 之间的代码行(或实际时间)。
  • 在本节的其余部分,我们将上述各项称为:
    1. 该项的作用域,此处不相关。
    2. 该变量或位置的作用域。
    3. 该使用的生命周期2。
    4. 该值的生命周期,在讨论打开的文件描述符时可能有用,但此处也不相关。
  • 同样,代码中的生命周期参数,例如 r: &'a S,
    • 关心的是任何**r 指向的位置**需要被访问或锁定的代码行;
    • 与 r 本身的“存在时间”(作为代码行)无关(当然,它需要存在得更短,就是这样)。
  • &'static S 意味着地址必须在所有代码行期间都有效。

1 文档中有时在区分各种作用域和生命周期时存在模糊。 我们在这里试图 pragmatic,但欢迎提出建议。

2 Live lines 可能是更合适的术语……

 ▼ 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 获取了变量的地址,该变量就被标记为被借用 (borrowed)。
  • 在被借用期间,地址的内容不能再通过原始绑定 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 只能返回一个地址,因此并非在所有情况下 b 和 c 都需要保持锁定。
  • 在许多情况下,我们可以获得生活质量的提升。
    • 特别是,当我们知道某个参数不可能再被用于返回值时。
 ▼ 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 由编译器在调用点根据开发者提供的被借用变量透明地选择。
  • 它们不等于 b 或 c 的作用域(即从初始化到销毁的代码行),而只是其作用域的一个最小子集,称为生命周期,即基于 b 和 c 需要被借用多长时间来执行此调用并使用获得的结果的最小代码行集。
  • 在某些情况下,比如如果 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`。


  • 一旦任何可能指向它的引用的最后一次使用结束,变量位置就会再次被解锁。
 ▼  ▼   S(1) 0x2 0x6 0x2 引用的引用
// 返回短 ('b) 引用
fn f1sr<'b, 'a>(rb: &'b     &'a     S) -> &'b     S { *rb }
fn f2sr<'b, 'a>(rb: &'b     &'a mut S) -> &'b     S { *rb }
fn f3sr<'b, 'a>(rb: &'b mut &'a     S) -> &'b     S { *rb }
fn f4sr<'b, 'a>(rb: &'b mut &'a mut S) -> &'b     S { *rb }

// 返回短 ('b) 可变引用。
// f1sm<'b, 'a>(rb: &'b     &'a     S) -> &'b mut S { *rb } // M
// f2sm<'b, 'a>(rb: &'b     &'a mut S) -> &'b mut S { *rb } // M
// f3sm<'b, 'a>(rb: &'b mut &'a     S) -> &'b mut S { *rb } // M
fn f4sm<'b, 'a>(rb: &'b mut &'a mut S) -> &'b mut S { *rb }

// 返回长 ('a) 引用。
fn f1lr<'b, 'a>(rb: &'b     &'a     S) -> &'a     S { *rb }
// f2lr<'b, 'a>(rb: &'b     &'a mut S) -> &'a     S { *rb } // L
fn f3lr<'b, 'a>(rb: &'b mut &'a     S) -> &'a     S { *rb }
// f4lr<'b, 'a>(rb: &'b mut &'a mut S) -> &'a     S { *rb } // L

// 返回长 ('a) 可变引用。
// f1lm<'b, 'a>(rb: &'b     &'a     S) -> &'a mut S { *rb } // M
// f2lm<'b, 'a>(rb: &'b     &'a mut S) -> &'a mut S { *rb } // M
// f3lm<'b, 'a>(rb: &'b mut &'a     S) -> &'a mut S { *rb } // M
// f4lm<'b, 'a>(rb: &'b mut &'a mut S) -> &'a mut S { *rb } // L

// 现在假设我们有一个 `ra`
let mut ra: &'a mut S = …;

let rval = f1sr(&&*ra);       // OK
let rval = f2sr(&&mut *ra);
let rval = f3sr(&mut &*ra);
let rval = f4sr(&mut ra);

//  rval = f1sm(&&*ra);       // 会很糟,因为 rval 将是从
//  rval = f2sm(&&mut *ra);   // 破碎的可变性链中获得的可变引用。
//  rval = f3sm(&mut &*ra);   //
let rval = f4sm(&mut ra);

let rval = f1lr(&&*ra);
//  rval = f2lr(&&mut *ra);   // 如果这行得通,我们现在就有 `rval` 和 `ra`……
let rval = f3lr(&mut &*ra);
//  rval = f4lr(&mut ra);     // ……(可变地)别名了下面的计算中的 `S`。

//  rval = f1lm(&&*ra);       // 与上面相同,因可变链原因失败。
//  rval = f2lm(&&mut *ra);   //                    "
//  rval = f3lm(&mut &*ra);   //                    "
//  rval = f4lm(&mut ra);     // 与上面相同,因别名原因失败。

// 某个我们使用 `ra` 和 `rval` 的虚构地方,两者都存活。
compute(ra, rval);

此处 (M) 表示因可变性错误编译失败,(L) 表示生命周期错误。 此外,解引用 *rb 并非严格必要,只是为了清晰起见而添加。

  • f_sr 情况总是有效,短引用(只活 'b)总是可以产生。
  • f_sm 情况通常失败,因为需要一个可变链到 S 才能返回 &mut S。
  • f_lr 情况可能失败,因为从 &'a mut S 返回 &'a S 给调用者意味着现在将存在两个对同一 S 的引用(一个可变),这是非法的。
  • f_lm 情况总是因上述原因的组合而失败。

注意:这个例子是关于 f 函数,而不是 compute。你可以假设 它被定义为 fn compute(x: &S, y: &S) {}。在这种情况下,ra 参数 将被自动强制转换↓从 &mut S 到 &S,因为你不能有 一个共享引用和一个可变引用指向同一个目标。

S(1)▼ Drop 和 _
{
    let f = |x, y| (S(x), S(y)); // 返回两个'可 Drop' 的函数。

    let (    x1, y) = f(1, 4);  // S(1) - 作用域   S(4) - 作用域
    let (    x2, _) = f(2, 5);  // S(2) - 作用域   S(5) - 立即
    let (ref x3, _) = f(3, 6);  // S(3) - 作用域   S(6) - 作用域

    println!("…");
}

此处 作用域 意味着包含的值存活到作用域结束,即超过 println!()。

  • 产生可移动值的函数或表达式必须由被调用方处理。
  • 存储在“正常”绑定中的值会保留到作用域结束,然后被 drop。
  • 存储在 _ 绑定中的值通常会立即被 drop。
  • 然而,有时引用(例如 ref x3)可以使值(例如元组 (S(3), S(6)))保留更长时间,因此作为该元组一部分的 S(6) 只能在对其兄弟 S(3) 的引用消失后才能被 drop。

↕️ 点击展开示例。

 

内存布局url

常见类型的字节表示。

基本类型url

语言核心内置的基本类型。

布尔型 REF 与数值类型 REFurl

bool u8, i8 u16, i16 u32, i32 u64, i64 u128, i128 usize, isize 与平台上的 ptr 大小相同。 f16 🚧 f32 f64 f128 🚧
 
类型最大值
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 相同。
类型最大值最小正值最大无损整数1
f16 🚧65504.06.10 ⋅ 10 -52048
f323.40 ⋅ 10 383.40 ⋅ 10 -3816_777_216
f641.79 ⋅ 10 3082.23 ⋅ 10 -3089_007_199_254_740_992
f128 🚧1.19 ⋅ 10 49323.36 ⋅ 10 -49322.07 ⋅ 10 34

1 最大整数 M,使得所有其他整数 0 <= X <= M 都能在该类型中无损表示。换句话说,可能存在更大的整数仍能无损表示(例如 f16 的 65504),但直到该值,无损表示是有保证的。

 

浮点值为视觉清晰度而近似。负数极限是值乘以 -1。

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 到 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 到 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截断多余的位。
_257 as i81截断多余的位。
_200 as i8-56截断多余的位,最高有效位可能也表示负数。
操作1得到注意
200_u8 / 0_u8编译错误。-
200_u8 / _0 d, rPanic。常规数学运算可能 panic;此处:除以零。
200_u8 + 200_u8编译错误。-
200_u8 + _200 dPanic。考虑使用 checked_、wrapping_ 等。STD
200_u8 + _200 r144在 release 模式下会溢出。
-128_i8 * -1编译错误。会溢出(128_i8 不存在)。
-128_i8 * _1neg dPanic。-
-128_i8 * _1neg r-128在 release 模式下溢出回 -128。
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 比较总是返回 false。
x > f32::NANfalseNAN 比较总是返回 false。
f32::NAN == f32::NANfalse使用 f32::is_nan() STD 代替。

1 表达式 _100 表示任何可能包含值 100 的东西,例如 100_i32,但对编译器是不透明的。
d Debug 构建。
r Release 构建。

 

文本类型 REFurl

char 任何 Unicode 标量值。 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;另外,char 不允许🛑持有任意的位模式。
1 有趣的是,由于零宽度连接符 (⨝),用户*感知到的字符*可能变得更加不可预测:👨‍👩‍👧 实际上是 5 个 char 👨⨝👩⨝👧,渲染引擎可以自由地将它们融合成一个,或分开显示为三个,这取决于它们的能力。
 
字符串描述
let s = "a";str 通常不会直接持有,而是作为 &str,如此处的 s。
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
t.as_bytes()49 20 e2 9d a4 ef b8 8f 20 52 75 73 74 4
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.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 变体选择器的组合,因此 `t` 的字符数比 `s` 多。
 

⚠️ 由于似乎是浏览器 bug,Safari 和 Edge 在脚注 3 和 4 中渲染心形有误,尽管它们能在上面的 s 和 t 中正确地区分它们。

 

自定义类型url

用户可定义的基本类型。实际的布局 REF 取决于表示 (representation);REF 可能存在填充。

T T 有大小的类型。 T: ?Sized T 可能无大小。 [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) 仍然可以有不同的布局;在没有表示保证的情况下,切勿使用 transmute() STD。

 

这些和类型 (sum types) 持有其子类型之一的值:

enum E { A, B, C } 标签 A 互斥或 标签 B 互斥或 标签 C 安全地持有 A 或 B 或 C,也
称为‘带标签的联合体’,尽管
编译器可能会将标签压缩
到‘未使用’的位中。
union { … } A 不安全或 B 不安全或 C 可以不安全地重新解释
内存。结果可能
是未定义的。

引用与指针url

引用提供对第三方内存的安全访问,裸指针提供unsafe访问。 相应的 mut 类型与其不可变对应项具有相同的数据布局。

&'a T ptr2/4/8 meta2/4/8 | T 必须指向某个有效的 t(类型为 T),
且任何这样的目标必须至少存在
'a 时间。
*const T ptr2/4/8 meta2/4/8 无保证。

指针元数据url

许多引用和指针类型可以携带一个额外的字段,即指针元数据 (pointer metadata)。STD 它可以是目标的元素或字节长度,或指向一个虚函数表 (vtable)的指针。带元数据的指针称为胖指针 (fat),否则称为瘦指针 (thin)。

&'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 … 常规的切片引用(即切片类型 [T] 的
引用类型)↑
如果 'a 被省略,通常写作 &[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() 等是指向它们各自为 T 的 impl 的指针。

闭包url

具有自动管理数据块的即席函数,该数据块捕获 REF, 1 定义闭包的环境。例如,如果你有:

let y = ...;
let z = ...;

with_closure(move |x| x + y.f() + z); // y 和 z 被移动到闭包实例(类型为 C1)中
with_closure(     |x| x + y.f() + z); // y 和 z 被闭包实例(类型为 C2)引用

那么传递给 with_closure() 的生成的匿名闭包类型 C1 和 C2 将如下所示:

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

也产生匿名的 fn,例如 fc1(C1, X) 或 fc2(&C2, X)。细节取决于支持哪个 FnOnce、FnMut、Fn ...,这基于捕获类型的属性。

1 简单来说,闭包是一个方便编写的“迷你函数”,它接受参数,并且需要一些局部变量来完成其工作。因此,它是一个类型(包含所需的局部变量)和一个函数。“捕获环境”是一种花哨的说法,用来描述闭包类型如何持有这些局部变量,要么是通过移动的值,要么是通过指针。有关各种含义,请参见API 中的闭包 ↓。

标准库类型url

Rust 的标准库将上述原始类型组合成具有特殊语义的有用类型,例如:

Option<T> STD 标签 或 标签 T 对于某些 T,标签可能被省略,
例如 NonNull。STD
Result<T, E> STD 标签 E 或 标签 T 要么是某个错误 E,要么是
T 类型的值。
ManuallyDrop<T> STD T 阻止 T::drop() 被
调用。
AtomicUsize STD usize2/4/8 其他原子类型类似。 MaybeUninit<T> STD 未̼̟̔͛定̥͕͐͞义̛̲͔̦̳̑̓̐ 不安全或 T 未初始化的内存或
某个 T。处理未初始化
数据的唯一合法方式。
PhantomData<T> STD 零大小的辅助类型,用于持有
否则未使用的生命周期。
Pin<P> STD P | 📌 P::Deref 表示 P 的目标被“永远”固定
即使超过 Pin 的生命周期。内部值
不可移出(但可移入新值),
除非是 Unpin。STD

🛑 所有描述仅用于说明目的。 这些字段应存在于最新的 stable 版本中,但 Rust 对其布局不做任何保证,除非文档允许,否则您不得 尝试不安全地访问任何内容。

 

Cell 类型url

UnsafeCell<T> STD T 允许别名可变性的
神奇类型。
Cell<T> STD T 允许 T
移入和
移出。
RefCell<T> STD borrowed T 也支持 T 的动态
借用。与 Cell 一样,
这是 Send,但不是 Sync。
OnceCell<T> STD
标签 或 标签 T
最多初始化一次。
LazyCell<T, F> STD
标签 Uninit<F> 或 标签 Init<T> 或 标签 Poisoned
在首次访问时初始化。
 

保序集合url

Box<T> STD ptr2/4/8 meta2/4/8 | T 对于某些 T,栈代理可能携带
元数据↑(例如 Box<[T]>)。
Vec<T> STD ptr2/4/8 len2/4/8 capacity2/4/8 |
T T … len
← capacity →
常规的可增长数组向量,包含单一类型。
LinkedList<T> STD🝖 head2/4/8 tail2/4/8 len2/4/8 | | next2/4/8 prev2/4/8 T 元素 head 和 tail 要么是 null,要么指向堆上的节点。
每个节点可以指向其 prev 和 next 节点。
吞噬你的缓存(看看这东西!);除非你
显然必须使用,否则不要用。🛑
VecDeque<T> STD head2/4/8 len2/4/8 ptr2/4/8 capacity2/4/8 |
T … 空 … T⁣H
← capacity →
索引 head 在作为环形缓冲区的数组中选择。这意味着内容可能
不连续,并且中间为空,如上所示。
 

其他集合url

HashMap<K, V> STD bmask2/4/8 ctrl2/4/8 left2/4/8 len2/4/8 | K:V K:V … K:V … K:V 过于简化! 根据哈希值将键和值存储在堆上,SwissTable
实现通过 hashbrown。HashSet STD 与 HashMap 相同,
只是类型 V 消失了。堆视图被严重简化了。🛑
BinaryHeap<T> STD ptr2/4/8 capacity2/4/8 len2/4/8 |
T⁣0 T⁣1 T⁣1 T⁣2 T⁣2 … len
← capacity →
堆存储为数组,每层有 2N 个元素。每个 T
在下一层可以有两个子节点。每个 T 都比其
子节点大。

自有字符串url

String STD ptr2/4/8 capacity2/4/8 len2/4/8 |
U T F - 8 … len
← capacity →
注意 String 与 &str 和 &[char] 的区别。
CString STD ptr2/4/8 len2/4/8 |
A B C … len … ∅
以 NUL 结尾,但中间没有 NUL。
OsString STD ptr2/4/8 capacity2/4/8 len2/4/8 |
/
封装了操作系统如何
表示路径。
 

共享所有权url

如果类型不包含用于 T 的 Cell,这些类型通常与上述 Cell 类型之一结合使用,以允许事实上的共享可变性。

Rc<T> STD ptr2/4/8 meta2/4/8
| strng2/4/8 weak2/4/8 T
在同一线程中共享 T 的所有权。需要嵌套 Cell
或 RefCell 来允许修改。既不是 Send 也不是 Sync。
Arc<T> STD ptr2/4/8 meta2/4/8
| strng2/4/8 weak2/4/8 T
同上,但如果包含的
T 本身是 Send 和 Sync,则允许在线程间共享。

Mutex<T> STD / RwLock<T> STD inner poison2/4/8 T 内部字段取决于平台。需要放在
Arc 中才能在解耦的线程间共享,
或者通过 scope() STD 用于作用域线程。
Cow<'a, T> STD 标签 T::Owned 或 标签 ptr2/4/8
| T
持有一个对某个 T 的只读引用,
或者拥有其 ToOwned STD
的等价物。

标准库url

一行代码url

一些常见但容易忘记的代码片段。更多信息请参见 Rust Cookbook 🔗。

意图代码片段
连接字符串(任何实现了 Display↓ 的类型)。STD 1 '21format!("{x}{y}")
追加字符串(任何 Display 到任何 Write)。'21 STDwrite!(x, "{y}")
按分隔符模式分割。STD 🔗s.split(pattern)
     … 使用 &strs.split("abc")
     … 使用 chars.split('/')
     … 使用闭包s.split(char::is_numeric)
按空白分割。STDs.split_whitespace()
按换行符分割。STDs.lines()
按正则表达式分割。🔗 2 Regex::new(r"\s")?.split("one two three")

1 会分配内存;如果 x 或 y 之后不再使用,请考虑使用 write! 或 std::ops::Add。
2 需要 regex crate。

意图代码片段
创建一个新文件 STDFile::create(PATH)?
     同上,通过 OpenOptionsOpenOptions::new().create(true).write(true).truncate(true).open(PATH)?
将文件读取为 String STDread_to_string(path)?
意图代码片段
带可变参数的宏macro_rules! var_args { ($($args:expr),*) => {{ }} }
     使用 args,例如多次调用 f。     $( f($args); )*
起始类型资源
Option<T> -> …参见 基于类型的速查表
Result<T, R> -> …参见 基于类型的速查表
Iterator<Item=T> -> …参见 基于类型的速查表
&[T] -> …参见 基于类型的速查表
Future<T> -> …参见 Futures 速查表
意图代码片段
更清晰的闭包捕获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 是对象安全的金丝雀代码。REFconst _: Option<&dyn T> = None;
用于统一类型的Semver 技巧。🔗Cargo.toml 中的 my_crate = "next.version" + 重新导出类型。
在自己的 crate 内部使用宏。🔗macro_rules! internal_macro {} 与 pub(crate) use internal_macro;

线程安全url

假设你在线程 1 中持有一些变量,并且想要将它们移动到线程 2,或者将它们的引用传递给线程 3。这是否被允许分别由 SendSTD 和 SyncSTD 决定:

 

|
|
|
 Mutex<u32>
|
|
|
 Cell<u32>
|
|
|
 MutexGuard<u32>
|
|
|
 Rc<u32>
线程 1

 Mutex<u32>  Cell<u32>  MutexGuard<u32>  Rc<u32> 线程 2

&Mutex<u32> &Cell<u32> &MutexGuard<u32> &Rc<u32> 线程 3
 
示例解释
Mutex<u32>既是 Send 也是 Sync。你可以安全地将其传递或借给另一个线程。
Cell<u32>Send,但不是 Sync。可移动,但其引用将允许并发的非原子写入。
MutexGuard<u32>Sync,但不是 Send。锁与线程绑定,但其引用的使用不允许数据竞争。
Rc<u32>两者都不是,因为它是一个带有非原子计数器的、易于克隆的堆代理。
 
TraitSend!Send
Sync大多数类型 … Arc<T>1,2, Mutex<T>2MutexGuard<T>1, RwLockReadGuard<T>1
!SyncCell<T>2, RefCell<T>2Rc<T>, &dyn Trait, *const T3

1 如果 T 是 Sync。
2 如果 T 是 Send。
3 如果你需要发送一个裸指针,创建一个新类型 struct Ptr(*const u8) 并 unsafe impl Send for Ptr {}。只需确保你可以发送它。

 
何时 ...... 是 Send?
T所有包含的字段都是 Send,或者 unsafe 实现。
    struct S { ... }所有字段都是 Send,或者 unsafe 实现。
    struct S<T> { ... }所有字段都是 Send 并且 T 是 Send,或者 unsafe 实现。
    enum E { ... }所有变体中的所有字段都是 Send,或者 unsafe 实现。
&T如果 T 是 Sync。
|| {}如果所有捕获都是 Send,则闭包是 Send。
     |x| { }Send,与 x 无关。
     |x| { Rc::new(x) }Send,因为仍然没有捕获任何东西,尽管 Rc 不是 Send。
     |x| { x + y }仅当 y 是 Send 时才是 Send。
async { }如果没有任何 !Send 类型跨越 .await 点,则 Future 是 Send。
     async { Rc::new() }Future 是 Send,因为 !Send 类型 Rc 没有跨越 .await。
     async { rc; x.await; rc; } 1Future 是 !Send,因为 Rc 跨越了 .await 点。
async || { } 🚧异步闭包 Send 如果所有捕获都是 Send,结果的 Future 也是 Send 如果内部也没有 !Send 类型。
     async |x| { x + y } 🚧异步闭包 Send 如果 y 是 Send。Future Send 如果 x 和 y 是 Send。

1 这是一些伪代码,旨在传达要点,其思想是在一个 .await 点之前有一个 Rc,并在该点之后继续使用它。

原子操作与缓存 🝖url

CPU 缓存、内存写入,以及原子操作如何影响它们。

S O M E D R A M D A T A 主内存 S O M E (E) D A T A (S) CPU1 缓存 S R A M (M) D A T A (S) CPU2 缓存

现代 CPU 不直接访问内存,只访问其缓存。每个 CPU 都有自己的缓存,比 RAM 快 100 倍,但小得多。它以缓存行的形式存在,🔗 是一些字节的切片窗口,用于跟踪它是否是主内存的独占 (E)、共享 (S) 或已修改 (M) 🔗 视图。缓存之间相互通信以确保缓存一致性 (coherence),🔗 即,“足够小”的数据将被所有其他 CPU“立即”看到,但这可能会使 CPU 停顿。

S O M E D X T A (M) 周期 1 O3 S O M 4 D X T A 周期 2 23 停顿 1 4 (M) D X T Y (M) 周期 3 23

左:编译器和CPU 都可以自由地重排🔗并拆分读/写内存访问。即使你明确说了 write(1); write(23); write(4),你的编译器也可能认为先写 23 是个好主意;此外,你的 CPU 可能坚持拆分写操作,先做 3 再做 2。这些步骤中的每一个都可能被 CPU2 通过 unsafe 的数据竞争观察到(甚至是不可能的 O3)。重排对于锁也是致命的。右:半相关的,即使两个 CPU 不试图访问彼此的数据(例如,更新 2 个独立的变量),如果底层内存被 2 个缓存行映射(伪共享),它们仍然可能会经历显著的性能损失。🔗

1 2 3 4 S R A M D X T Y 主内存 RA 1 R A M 周期 4 RA 1 2 M 周期 5 23 1 2 3 4 (M) 周期 6 23

原子操作通过做两件事来解决上述问题,它们 - 确保读/写/更新不会通过临时锁定其他 CPU 中的缓存行而被部分观察到, - 强制编译器和 CPU 都不要在其周围重排*“不相关”*的访问(即,充当屏障STD)。确保多个 CPU 对这些其他操作的相对顺序达成一致称为内存模型一致性 (consistency)。🔗 这也带来了错失性能优化的代价。

 

注意 — 上述部分被大大简化了。虽然缓存一致性 (coherence) 和内存模型一致性 (consistency) 的问题是普遍存在的,但 CPU 架构在如何实现缓存和原子操作以及它们的性能影响方面差异很大。

 
     A. 排序解释
Relaxed STD完全重排。不相关的读/写可以在原子操作周围自由地重新排序。
Release STD, 1在写入时,确保由第三方 Acquire 加载的其他数据在此次写入之后可见。
Acquire STD, 1在读取时,确保在第三方 Release 之前写入的其他数据在此次读取之后可见。
SeqCst STD原子操作周围不进行重排。所有不相关的读和写都保持在适当的一侧。

1 需要明确的是,当与 2 个或更多 CPU 同步内存访问时,所有CPU都必须使用 Acquire 或 Release(或更强的)。写入者必须确保它希望释放到内存的所有其他数据都在原子信号之前,而希望获取这些数据的读取者必须确保它们的其他读取仅在原子信号之后完成。

迭代器url

处理集合中的元素。

广义上讲,有四种集合迭代的风格:

风格描述
for x in c { ... }命令式,在有副作用、相互依赖或需要提早中断流程时很有用。
c.iter().map().filter()函数式,当只关心结果时通常更清晰。
c_iter.next()底层,通过显式调用 Iterator::next() STD。🝖
c.get(n)手动,绕过官方的迭代机制。
 

个人观点 💬 — 函数式风格通常最容易理解,但如果你的 .iter() 链变得混乱,不要犹豫使用 for。在实现容器时,迭代器支持是理想的,但当赶时间时,有时只实现 .len() 和 .get() 然后继续生活可能更实用。

基础

假设你有一个类型为 C 的集合 c,你想要使用它:

  • c.into_iter()1 — 将集合 c 转换为一个 Iterator STD i 并消耗2 c。获取迭代器的标准方式。
  • c.iter() — 一些集合提供的便捷方法,返回一个借用的迭代器,不消耗 c。
  • c.iter_mut() — 同上,但返回一个可变借用的迭代器,允许集合被更改。

迭代器

一旦你有了 i:

  • i.next() — 返回 Some(x),即 c 提供的下一个元素,如果完成则返回 None。

For 循环

  • for x in c {} — 语法糖,调用 c.into_iter() 并循环 i 直到 None。

1 要求为 C 实现 IntoIterator STD。项的类型取决于 C 是什么。

2 如果看起来它没有消耗 c,那是因为类型是 Copy。例如,如果你调用 (&c).into_iter(),它将在 &c 上调用 .into_iter()(这将消耗引用的一个副本并将其转换为迭代器),但原始的 c 保持不变。

基本要素

假设你编写了一个 struct Collection<T> {}。你还应该实现:

  • struct IntoIter<T> {} — 创建一个结构体来保存你的迭代状态(例如,一个索引),用于值迭代。
  • impl Iterator for IntoIter<T> {} — 实现 Iterator::next() 以便它可以产生元素。
Collection<T> IntoIter<T> ⌾ Iterator Item = T;
 

此时,你已经有了一个可以作为 Iterator STD 的东西,但还没有办法真正获得它。下一选项卡将介绍通常如何做到这一点。

原生循环支持

许多用户会期望你的集合在 for 循环中就能用。你需要实现:

  • impl IntoIterator for Collection<T> {} — 现在 for x in c {} 可以工作了。
  • impl IntoIterator for &Collection<T> {} — 现在 for x in &c {} 可以工作了。
  • impl IntoIterator for &mut Collection<T> {} — 现在 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。
 

如你所见,IntoIterator STD trait 才是真正将你的集合与你在前一个选项卡中创建的 IntoIter 结构体连接起来的东西。IntoIter 的两个兄弟(Iter 和 IterMut)将在下一个选项卡中讨论。

共享与可变迭代器

此外,如果你希望你的集合在被借用时也很有用,你应该实现:

  • 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;
 

借用迭代器支持的代码基本上只是前几个步骤的重复,只是类型略有不同,例如 &T 对比 T。

迭代器互操作性

要允许第三方迭代器“收集到”你的集合中,请实现:

  • impl FromIterator for Collection<T> {} — 现在 some_iter.collect::<Collection<_>>() 可以工作了。
  • impl Extend for Collection<T> {} — 现在 c.extend(other) 可以工作了。

此外,还应考虑将 std::iter STD 中的额外 trait 添加到你之前的结构体中:

Collection<T> ⌾ FromIterator ⌾ Extend IntoIter<T> ⌾ DoubleEndedIt… ⌾ ExactSizeIt… ⌾ FusedIterator Iter<T> ⌾ DoubleEndedIt… ⌾ ExactSizeIt… ⌾ FusedIterator IterMut<T> ⌾ DoubleEndedIt… ⌾ ExactSizeIt… ⌾ FusedIterator
 

编写集合可能是一项工作。好消息是,如果你遵循了所有这些步骤,你的集合将会感觉像是一等公民。

数字转换url

目前为止最正确的数字转换。

↓ 有 / 想要 →u8 … i128f32 / f64String
u8 … i128u8::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)。

 

另请参见转换和算术陷阱 ↑,了解更多处理数字时可能出错的事情。

字符串转换url

如果你想要一个 … 类型的字符串

如果你有一个 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
OsStringCString::new(x.to_str()?)?
PathBufCString::new(x.to_str()?)?
Vec<u8> 1CString::new(x)?
&strCString::new(x)?
&CStrx.to_owned() i
&OsStrCString::new(x.to_os_string().into_string()?)?
&PathCString::new(x.to_str()?)?
&[u8] 1CString::new(Vec::from(x))?
*mut c_char 2unsafe { CString::from_raw(x) }
如果你有一个 x,类型是 …使用这个 …
StringOsString::from(x) i
CStringOsString::from(x.to_str()?)
OsStringx
PathBufx.into_os_string()
Vec<u8> 1unsafe { OsString::from_encoded_bytes_unchecked(x) }
&strOsString::from(x) i
&CStrOsString::from(x.to_str()?)
&OsStrOsString::from(x) i
&Pathx.as_os_str().to_owned()
&[u8] 1unsafe { OsString::from_encoded_bytes_unchecked(x.to_vec()) }
如果你有一个 x,类型是 …使用这个 …
StringPathBuf::from(x) i
CStringPathBuf::from(x.to_str()?)
OsStringPathBuf::from(x) i
PathBufx
Vec<u8> 1unsafe { PathBuf::from(OsString::from_encoded_bytes_unchecked(x)) }
&strPathBuf::from(x) i
&CStrPathBuf::from(x.to_str()?)
&OsStrPathBuf::from(x) i
&PathPathBuf::from(x) i
&[u8] 1unsafe { PathBuf::from(OsString::from_encoded_bytes_unchecked(x.to_vec())) }
如果你有一个 x,类型是 …使用这个 …
Stringx.into_bytes()
CStringx.into_bytes()
OsStringx.into_encoded_bytes()
PathBufx.into_os_string().into_encoded_bytes()
Vec<u8> 1x
&strVec::from(x.as_bytes())
&CStrVec::from(x.to_bytes_with_nul())
&OsStrVec::from(x.as_encoded_bytes())
&PathVec::from(x.as_os_str().as_encoded_bytes())
&[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()
OsStringx.to_str()?
PathBuf?,3
Vec<u8> 1,4CStr::from_bytes_with_nul(&x)?
&str?,3
&CStrx
&OsStr?
&Path?
&[u8] 1,4CStr::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> 1unsafe { OsStr::from_encoded_bytes_unchecked(&x) }
&strOsStr::new(x)
&CStr?
&OsStrx
&Pathx.as_os_str()
&[u8] 1unsafe { OsStr::from_encoded_bytes_unchecked(x) }
如果你有一个 x,类型是 …使用这个 …
StringPath::new(x) r
CStringPath::new(x.to_str()?)
OsStringPath::new(x.to_str()?) r
PathBufPath::new(x.to_str()?) r
Vec<u8> 1unsafe { Path::new(OsStr::from_encoded_bytes_unchecked(&x)) }
&strPath::new(x) r
&CStrPath::new(x.to_str()?)
&OsStrPath::new(x) r
&Pathx
&[u8] 1unsafe { Path::new(OsStr::from_encoded_bytes_unchecked(x)) }
如果你有一个 x,类型是 …使用这个 …
Stringx.as_bytes()
CStringx.as_bytes()
OsStringx.as_encoded_bytes()
PathBufx.as_os_str().as_encoded_bytes()
Vec<u8> 1&x
&strx.as_bytes()
&CStrx.to_bytes_with_nul()
&OsStrx.as_encoded_bytes()
&Pathx.as_os_str().as_encoded_bytes()
&[u8] 1x
你想要并且有 x使用这个 …
*const c_charCStringx.as_ptr()

i 如果类型可以推断,可以使用简写形式 x.into()。
r 如果类型可以推断,可以使用简写形式 x.as_ref()。
1 你必须确保 x 带有字符串类型的有效表示(例如,对于 String 的 UTF-8 数据)。
2 c_char 必须来自先前的 CString。如果它来自 FFI,请改用 &CStr。
3 没有已知的简写,因为 x 将缺少结尾的 0x0。最好的方法可能是通过 CString。
4 必须确保 x 实际上以 0x0 结尾。

字符串输出url

如何将类型转换为 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 "{}" STD 或 Debug "{:?}" STD 进行转换,不完全列表:

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

简而言之,几乎所有东西都是 Debug;更特殊的类型可能需要特殊处理或转换↑为 Display。

格式化宏中的每个参数指示符要么是空的 {}, {argument},要么遵循基本的 语法:

{ [argument] ':' [[fill] align] [sign] ['#'] [width [$]] ['.' precision [$]] [type] }
元素含义
argument数字(0, 1, …)、变量'21或名称,'18例如print!("{x}")。
fill如果指定了 width,则用于填充空白的字符(例如 0)。
align如果指定了宽度,则为左(<)、中(^)或右(>)对齐。
sign可以是 +,表示总是打印符号。
#替代格式化,例如美化 DebugSTD 格式化器 ? 或为十六进制加前缀 0x。
width最小宽度(≥ 0),用 fill(默认为空格)填充。如果以 0 开头,则用零填充。
precision数值的小数位数(≥ 0),或非数值的最大宽度。
$将 width 或 precision 解释为参数标识符,以允许动态格式化。
typeDebugSTD (?) 格式化、十六进制 (x)、二进制 (b)、八进制 (o)、指针 (p)、指数 (e)……查看更多。
 
格式化示例解释
{}使用 DisplaySTD 打印下一个参数。
{x}同上,但使用作用域中的变量 x。'21
{:?}使用 DebugSTD 打印下一个参数。
{2:#?}使用 DebugSTD 格式化漂亮地打印第 3 个参数。
{val:^2$}居中名为 val 的参数,宽度由第 3 个参数指定。
{:<10.3}左对齐,宽度为 10,精度为 3。
{val:#x}将 val 参数格式化为十六进制,并带前导 0x(x 的替代格式)。
 
完整示例解释
println!("{}", x)使用 DisplaySTD 将 x 打印到标准输出并追加换行符。'15 🗑️
println!("{x}")同上,但使用作用域中的变量 x。'21
format!("{a:.3} {b:?}")将 a 转换为 3 位数,添加空格,使用 DebugSTD 格式化 b,返回 String。'21
 

工具链url

项目结构url

基本的项目布局,以及 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/放置额外二进制文件的地方,即使在库项目中也可以。
          extra.rs额外的二进制文件,用 cargo run --bin extra 运行。
📁 tests/集成测试放在这里,通过 cargo test 调用。单元测试通常留在 src/ 文件中。
.rustfmt.toml如果你想自定义 cargo fmt 的工作方式。
.clippy.toml某些 clippy lints 的特殊配置,通过 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` 不是公共的),所以 `g`
}                  // 不能从 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 版本中,由于某些原因,这也是必需的。
                    // 通常在 '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() {
    // 你需要依赖环境变量来获取目标;`#[cfg(…)]` 是为主机准备的。
    let target_os = env::var("CARGO_CFG_TARGET_OS");
}

*此处是设置的环境变量列表。

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

extern crate proc_macro;  // 显然需要这样导入。

use proc_macro::TokenStream;

#[proc_macro_attribute]   // Crate 现在可以使用 `#[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.rs 或 m/mod.rs。
  • .rs 的路径基于嵌套,例如 mod a { mod b { mod c; }}} 要么是 a/b/c.rs 要么是 a/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();2 →

1 在类型和函数中都算,定义了类型 X 和常量 X。
2 在类型和函数中都算,定义了类型 X 和函数 X。

  • 在任何给定的作用域中,例如在一个模块内,每个命名空间只能存在一个项,例如,
    • enum X {} 和 fn X() {} 可以共存
    • struct X; 和 const X 不能共存
  • 使用 use my_mod::X;,所有名为 X 的项都将被导入。

由于命名约定(例如,fn 和 mod 按惯例是小写的)和常识(大多数开发者不会把所有东西都命名为 X),在大多数情况下你不需要担心这些种类。然而,在设计宏时,它们可能是一个因素。

 

Cargourl

一些值得了解的命令和工具。

命令描述
cargo init为最新版本创建一个新项目。
cargo build在调试模式下构建项目(--release 用于所有优化)。
cargo check检查项目是否会编译(速度快得多)。
cargo test运行项目的测试。
cargo doc --no-deps --open为你的代码在本地生成文档。
cargo run运行你的项目,如果产生了二进制文件(main.rs)。
     cargo run --bin b运行二进制文件 b。统一与其他依赖项的特性(可能会令人困惑)。
     cargo run --package w运行子工作区 w 的 main。更合理地处理特性。
cargo … --timings显示哪些 crate 导致你的构建耗时过长。🔥
cargo tree显示依赖图,项目中使用的所有 crate,递归地。
     cargo tree -i foo反向依赖查找,解释为什么使用了 foo。
cargo info foo显示 foo 的 crate 元数据(默认为此项目使用的版本)。
cargo +{nightly, stable} …对命令使用给定的工具链,例如,用于“仅 nightly”的工具。
cargo +1.85.0 …也直接接受特定版本。
cargo +nightly …一些仅 nightly 的命令(用下面的命令替换 …)
     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 插件。

 

交叉编译url

🔘 检查目标是否受支持。

🔘 通过 rustup target install aarch64-linux-android(例如)安装目标。

🔘 安装原生工具链(需要用于链接,取决于目标)。

从目标供应商处获取(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
…

是否设置它们取决于编译器如何抱怨,不一定都需要。

一些平台/配置对路径的指定方式(例如 \ vs /)和引号极其敏感。

✔️ 使用 cargo build --target=aarch64-linux-android 编译

 

工具指令url

嵌入在源代码中,由工具或预处理器使用的特殊标记。

在声明式 BK 示例宏 BK EX REF macro_rules! 实现中,这些片段指定符 REF 可以工作:

宏内部解释
$x:ty宏捕获(这里 $x 是捕获,ty 表示 x 必须是类型)。
     $x:block一个语句或表达式的块 {},例如 { let x = 5; }
     $x:expr一个表达式,例如 x、1 + 1、String::new() 或 vec![]
     $x:expr_2021一个匹配 Rust '21 行为的表达式 RFC
     $x:ident一个标识符,例如在 let x = 0; 中,标识符是 x。
     $x:item一个项,如函数、结构体、模块等。
     $x:lifetime一个生命周期(例如 'a、'static 等)。
     $x:literal一个字面量(例如 3、"foo"、b"bar" 等)。
     $x:meta一个元项;放在 #[…] 和 #![…] 属性内部的东西。
     $x:pat一个模式,例如 Some(x)、(17, 'a') 或 x|x。
     $x:pat_param模式的子集,不含顶层 |,例如 Some(x) 或 x。
     $x:path一个路径(例如 foo、::std::mem::replace、transmute::<_, int>)。
     $x:stmt一个语句,例如 let x = 1 + 1;、String::new(); 或 vec![];
     $x:tt一个单独的标记树,更多细节请见这里。
     $x:ty一个类型,例如 String、usize 或 Vec<u8>。
     $x:vis一个可见性修饰符;pub、pub(crate) 等。
$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 或应用程序的属性:

Opt-Out's作用于解释
#![no_std]C不(自动)导入 stdSTD;改用 coreSTD。REF
#![no_implicit_prelude]CM不添加 preludeSTD,需要手动导入 None、Vec 等。REF
#![no_main]C如果你自己做,则不在应用程序中发出 main()。REF
 
Opt-In's作用于解释
#![feature(a, b, c)]C依赖可能不会稳定的特性,参见 Unstable Book。🚧
 
构建作用于解释
#![crate_name = "x"]C指定当前 crate 名称,例如在不使用 cargo 时。? REF 🝖
#![crate_type = "bin"]C指定当前 crate 类型(bin、lib、dylib、cdylib 等)。REF 🝖
#![recursion_limit = "123"]C设置解引用、宏等的编译时递归限制。REF 🝖
#![type_length_limit = "456"]C限制类型替换的最大数量。REF 🝖
#![windows_subsystem = "x"]C在 Windows 上,制作一个 console 或 windows 应用程序。REF 🝖
 
处理器作用于解释
#[alloc_error_handler]F将某个 fn(Layout) -> ! 设为分配失败处理器。🔗 🚧
#[global_allocator]S将实现了 GlobalAlloc STD 的静态项设为全局分配器。REF
#[panic_handler]F将某个 fn(&PanicInfo) -> ! 设为应用程序的恐慌处理器。REF

主要控制生成代码的属性:

开发者 UX作用于解释
#[non_exhaustive]T使 struct 或 enum 面向未来;提示它将来可能会增长。REF
#[path = "x.rs"]M从非标准文件获取模块。REF
#[diagnostic::on_unimplemented]X当 trait 未实现时提供更好的错误消息。RFC
 
代码生成作用于解释
#[cold]F提示函数可能不会被调用。REF
#[inline]F友好地建议编译器在调用点内联函数。REF
#[inline(always)]F强烈威胁编译器内联调用,否则…… REF
#[inline(never)]F指示编译器如果仍然内联函数会感到悲伤。REF
#[repr(X)]1T使用另一种表示,而不是默认的 rust REF 表示:
#[target_feature(enable="x")]F为 unsafe fn 的代码启用 CPU 特性(例如 avx2)。REF
#[track_caller]F允许 fn 找到**caller**STD 以获得更好的 panic 消息。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))]。

 
链接作用于解释
#[unsafe(no_mangle)]*直接使用项名作为符号名,而不是进行名称修饰。REF
#[unsafe(export_name = "foo")]FS以不同名称导出一个 fn 或 static。REF
#[unsafe(link_section = ".x")]FS项应放置的目标文件的节名。REF
#[link(name="x", kind="y")]X查找符号时要链接的原生库。REF
#[link_name = "foo"]F解析 extern fn 时要搜索的符号名。REF
#[no_link]X当只需要宏时,不链接 extern crate。REF
#[used]S即使 static 变量看起来未使用,也不要优化掉。REF

Rust 工具用于提高代码质量的属性:

代码模式作用于解释
#[allow(X)]*指示 rustc / clippy 忽略 X 类的可能问题。REF
#[expect(X)] 1*如果 lint 未触发,则发出警告。REF
#[warn(X)] 1*… 发出警告,与 clippy lints 混合使用效果很好。🔥 REF
#[deny(X)] 1*… 编译失败。REF
#[forbid(X)] 1*… 编译失败并阻止后续的 allow 覆盖。REF
#[deprecated = "msg"]*让你的用户知道你犯了一个设计错误。REF
#[must_use = "msg"]FTX使编译器检查返回值是否被调用者处理。🔥 REF

1 💬 关于哪种方法是确保高质量 crate 的最佳方法存在一些争论。积极维护的多开发者 crate 可能从更激进的 deny 或 forbid lints 中受益;不定期更新的 crate 可能更多地从保守使用 warn 中受益(因为未来的编译器或 clippy 更新可能会突然破坏原本工作正常的、有轻微问题的代码)。

 
测试作用于解释
#[test]F将函数标记为测试,用 cargo test 运行。🔥 REF
#[ignore = "msg"]F编译但暂时不执行某个 #[test]。REF
#[should_panic]F测试必须 panic!() 才能真正成功。REF
#[bench]F将 bench/ 中的函数标记为 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生成 Run 按钮并使用给定的服务。🔗
#![doc(html_root_url = "")]C到外部 crate 链接的基本 URL。🔗
#![doc(html_no_source)]C防止源代码被包含在文档中。🔗

与宏的创建和使用相关的属性:

示例宏作用于解释
#[macro_export]!在 crate 级别将 macro_rules! 导出为 pub REF
#[macro_use]MX使宏在模块之外持久化;或从 extern crate 导入。REF
 
过程宏作用于解释
#[proc_macro]F将 fn 标记为函数式过程宏,可作为 m!() 调用。REF
#[proc_macro_derive(Foo)]F将 fn 标记为派生宏,可以 #[derive(Foo)]。REF
#[proc_macro_attribute]F将 fn 标记为属性宏,用于新的 #[x]。REF
 
派生作用于解释
#[derive(X)]T让某个过程宏提供一个相当不错的 trait X 的 impl。🔥 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
 

⚠️ 注意,选项通常可以设置多次,即同一个键可以出现多次,带有不同的值。可以期望 #[cfg(target_feature = "avx")] 和 #[cfg(target_feature = "avx2")] 同时为真。

 
已知选项作用于解释
#[cfg(debug_assertions)]*debug_assert!() 等是否会 panic。REF
#[cfg(feature = "foo")]*当你的 crate 用特性 foo 编译时。🔥 REF
#[cfg(target_arch = "x86_64")]*crate 编译的目标 CPU 架构。REF
#[cfg(target_env = "msvc")]*在操作系统上如何与 DLL 和函数交互。REF
#[cfg(target_endian = "little")]*你的新零成本协议失败的主要原因。REF
#[cfg(target_family = "unix")]*操作系统所属的家族。REF
#[cfg(target_feature = "avx")]*某个特定指令集是否可用。REF
#[cfg(target_os = "macos")]*你的代码将运行的操作系统。REF
#[cfg(target_pointer_width = "64")]*指针、usize 和字的位数。REF
#[cfg(target_vendor = "apple")]*目标的制造商。REF
#[cfg(panic = "unwind")]*panic 时是 unwind 还是 abort。?
#[cfg(proc_macro)]*crate 是否作为过程宏编译。REF
#[cfg(test)]*是否用 cargo test 编译。🔥 REF

与预构建脚本相关的环境变量和输出。考虑使用 build-rs🔗。

输入环境变量解释 🔗
CARGO_FEATURE_X为每个激活的特性 x 设置的环境变量。
     CARGO_FEATURE_SOMETHING如果特性 something 被启用。
     CARGO_FEATURE_SOME_FEATURE如果特性 some-feature 被启用;破折号 - 被转换为 _。
CARGO_CFG_X暴露 cfg;通过 , 连接多个选项,并将 - 转换为 _。
     CARGO_CFG_TARGET_OS=macos如果 target_os 设置为 macos。
     CARGO_CFG_TARGET_FEATURE=avx,avx2如果 target_feature 设置为 avx 和 avx2。
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-cfg=KEY[="VALUE"]发出给定的 cfg 选项,用于后续编译。
cargo::rustc-cdylib-link-arg=FLAG 构建 cdylib 时,传递链接器标志。
cargo::rustc-env=VAR=VALUE 发出可在编译期间通过 env!() 访问的变量。
cargo::rustc-flags=FLAGS向编译器添加特殊标志。?
cargo::rustc-link-lib=[KIND=]NAME链接原生库,如同通过 -l 选项。
cargo::rustc-link-search=[KIND=]PATH搜索原生库的路径,如同通过 -L 选项。
cargo::warning=MESSAGE发出编译器警告。

从 build.rs 通过 println!() 发出。列表不详尽。

对于属性上的作用于列:
C 表示在 crate 级别(通常在顶层文件中以 #![my_attr] 形式给出)。
M 表示在模块上。
F 表示在函数上。
S 表示在静态项上。
T 表示在类型上。
X 表示某些特殊情况。
! 表示在宏上。
* 表示在几乎任何项上。


处理类型url

类型、Trait、泛型url

允许用户自带类型并避免代码重复。

类型
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
    • casts 手动转换类型的值,0_i8 as u8
    • coercions ↑ 在安全的情况下自动转换类型2,let x: &u8 = &mut 0_u8;

1 Casts 和 coercions 将值从一个集合(例如 u8)转换为另一个集合(例如 u16),可能需要添加 CPU 指令来这样做;这与子类型化 (subtyping) 不同,后者意味着类型和子类型是同一集合的一部分(例如,u8 是 u16 的子类型,0_u8 与 0_u16 相同),这种转换将纯粹是编译时检查。Rust 不对常规类型使用子类型化(0_u8 确实不同于 0_u16),但对生命周期则有所不同。🔗

2 此处的安全不仅是物理概念(例如,&u8 不能被强制转换为 &u128),还包括“历史表明这种转换会导致编程错误”。

实现 — impl S { }
u8 impl { … } String impl { … } Port impl { … }
impl Port {
    fn f() { … }
}
  • 类型通常带有固有实现,REF 例如 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 (marker traits)。
  • 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 与接口
👩‍🦰 ⌾ Eat 🧔 Venison ⌾ Eat 🎅 venison.eat()
 

接口

  • 在 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)。
  • 后来某人*决定将 Eat 添加到 Venison 是一个非常好的主意。
  • 在使用 Venison 时,Santa 必须单独导入 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 中。有关详细信息,请参见一致性。?

类型构造器 — 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<> 是类型构造器 (type constructor),一个“模板”或“创建类型的配方”
    • 允许第三方通过参数构造具体的类型,
    • 只有这样,这个 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] 和 S<const N: usize>
[T; n] S<const N>
  • 一些类型构造器不仅接受特定类型,还接受特定常量。
  • [T; n] 构造一个包含 n 次 T 类型的数组类型。
  • 对于自定义类型,声明为 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` 和
// 具体的 usize `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 约束就像“成员资格检查”:
// 类型只能为某个 `T` 构造,如果该
// T 是 `Absolute` 成员列表的一部分。
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<> 添加一个实现块,
  • 包含方法……

你可以将这样的 impl<T> … {} 代码视为抽象地实现了一个行为族。REF 最值得注意的是,它们允许第三方透明地物化实现,类似于类型构造器物化类型的方式:

// 如果编译器遇到这个,它会
// - 检查 `0` 和 `x` 是否满足 `T` 的成员资格要求
// - 创建两个新版本的 `f`,一个用于 `char`,另一个用于 `u32`。
// - 基于提供的“族实现”
s.f(0_u32);
s.f('x');
毯式实现 — impl<T> X for T { … }
 

也可以编写“族实现”,以便它们将 trait 应用于多种类型:

// 如果类型已经实现了 ToHex,则也为任何类型实现 Serialize
impl<T> Serialize for T where T: ToHex { … }

这些被称为毯式实现 (blanket implementations)。

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 { }

// 实现者将族实现 (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 的自定义。

如你所见,术语输入或输出并不(必然)与 I 或 O 是否是实际函数的输入或输出有关!

 

多个输入和输出参数

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 占用的字节数,则 T 是 Sized STD,u8 和 &[u8] 是,[u8] 不是。
  • Sized 意味着 impl Sized for T {} 成立。这是自动发生的,不能由用户实现。
  • 不是 Sized 的类型称为动态大小类型 BK NOM REF (DSTs),有时也称为unsized。
  • 没有数据的类型称为零大小类型 NOM (ZSTs),不占用空间。
示例解释
struct A { x: u8 }类型 A 是 sized 的,即 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 可以通过父 trait ↑ 选择加入 Sized。
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 来实例化类型(编译器会在方法内部提供帮助),
    • S<'p> 和 S<'q> 是不同的类型,就像 Vec<f32> 和 Vec<u8> 一样
    • 这意味着你不能简单地将 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 之外,你不能真正创建一个生命周期,例如“第 80-100 行”,编译器会为你做这件事。🔗

点击展开示例。

外部类型与 Traiturl

你的 crate 和上游 crate 中类型和 trait 的视觉概览。

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> 在上游 crate 中定义的项。 ⌾ Serialize ⌾ Transport ⌾ ShowHex Device ⌾ From<u8> 为本地类型实现外部 trait。 String ⌾ Serialize 为外部类型实现本地 trait。 String ⌾ From<u8> 🛑 非法,为外部类型实现外部 trait。 String ⌾ From<Port> 例外:如果使用的类型是本地的则合法。 Port ⌾ From<u8> ⌾ From<u16> 具有不同输入参数的 trait 的多个 impl。 Container ⌾ Deref Tgt = u8; ⌾ Deref Tgt = f32; 🛑 具有不同输出参数的 trait 的非法 impl。 T T T ⌾ ShowHex 为任何类型毯式实现 trait。 你的 crate。

trait 和类型的示例,以及你可以为哪些类型实现哪些 trait。

类型转换url

当你有了 A,如何得到 B?

fn f(x: A) -> B {
    // 你如何从 A 得到 B?
}
方法解释
恒等简单情况,B 就是 A。
计算通过编写代码转换数据来创建和操作 B 的实例。
转换 (Casts)在建议谨慎的情况下按需在类型之间转换。
强制转换 (Coercions)在*“弱化规则集”*内自动转换。1
子类型化 (Subtyping)在*“相同布局不同生命周期规则集”*内自动转换。1
 

1 虽然两者都将 A 转换为 B,但强制转换通常链接到一个不相关的 B(一个“可以合理地期望有不同方法”的类型), 而子类型化链接到一个仅在生命周期上不同的 B。

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

从 A 得到 B 的常规方法。一些 trait 提供了规范的、用户可计算的类型关系:

Trait示例Trait 意味着…
impl From<A> for B {}a.into()显而易见、总是有效的关系。
impl TryFrom<A> for B {}a.try_into()?显而易见、有时有效的关系。
impl Deref for A {}*aA 是携带 B 的智能指针;也启用强制转换。
impl AsRef<B> for A {}a.as_ref()A 可以被视为 B。
impl AsMut<B> for A {}a.as_mut()A 可以被可变地视为 B。
impl Borrow<B> for A {}a.borrow()A 有一个借用的等价物 B(在 Eq 等下行为相同)。
impl ToOwned for A { … }a.to_owned()A 有一个自有的等价物 B。
fn f(x: A) -> B {
    x as B
}

如果转换相对明显但可能引起问题,则使用关键字 as 转换类型。NOM

AB示例解释
PointerPointerdevice_ptr as *const u8如果 *A, *B 是 Sized。
PointerIntegerdevice_ptr as usize
IntegerPointermy_usize as *const Device
NumberNumbermy_u8 as u16通常有令人惊讶的行为。↑
enum (无字段)IntegerE::A as u8
boolIntegertrue as u8
charInteger'A' as u8
&[T; N]*const Tmy_ref as *const u8
fn(…)Pointerf as *const u8如果 Pointer 是 Sized。
fn(…)Integerf as usize
 

这里 Pointer、Integer、Number 只是为了简洁而使用,实际上意味着:

  • Pointer 任何 *const T 或 *mut T;
  • Integer 任何可数的 u8 … i128;
  • Number 任何 Integer、f32、f64。

个人观点 💬 — 转换,尤其是 Number - Number,很容易出错。 如果你关心正确性,请考虑使用更明确的方法。

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

自动弱化类型 A 为 B;类型可以实质上1不同。NOM

AB解释
&mut T&T指针弱化。
&mut T*mut T-
&T*const T-
*mut T*const T-
&T&U解引用,如果 impl Deref<Target=U> for T。
TU去大小化 (Unsizing),如果 impl CoerceUnsized<U> for T。2 🚧
TV传递性,如果 T 强制转换为 U 且 U 强制转换为 V。
|x| x + xfn(u8) -> u8非捕获闭包,转换为等效的 fn 指针。
 

1 实质上意味着可以常规地期望强制转换结果 B 是一个完全不同的类型(即,具有完全不同的方法)而不是原始类型 A。

2 在上面的示例中不太适用,因为无大小类型不能放在栈上;可以想象 f(x: &A) -> &B。去大小化默认适用于:

  • [T; n] 到 [T]
  • T 到 dyn Trait 如果 impl Trait for T {}。
  • Foo<…, T, …> 到 Foo<…, U, …> 在某些晦涩的🔗情况下。
fn f(x: A) -> B {
    x
}

对于仅在生命周期上不同的类型,自动将 A 转换为 B NOM - 子类型化示例:

A(子类型)B(超类型)解释
&'static u8&'a u8有效,永久指针也是瞬时指针。
&'a u8&'static u8🛑 无效,瞬时不应是永久。
&'a &'b u8&'a &'b u8有效,相同的东西。但现在事情变得有趣了。请继续阅读。
&'a &'static u8&'a &'b u8有效,&'static u8 也是 &'b u8;在 & 内部是协变的。
&'a mut &'static u8&'a mut &'b u8🛑 无效且令人惊讶;在 &mut 内部是不变的。
Box<&'static u8>Box<&'a u8>有效,带有永久的 Box 也是带有瞬时的 Box;协变的。
Box<&'a u8>Box<&'static u8>🛑 无效,带有瞬时的 Box 可能不是带有永久的。
Box<&'a mut u8>Box<&'a u8>🛑 ⚡ 无效,见下表,&mut u8 从来不是 &u8。
Cell<&'static u8>Cell<&'a u8>🛑 无效,Cell 从不是其他东西;不变的。
fn(&'static u8)fn(&'a u8)🛑 如果 fn 需要永久,它可能会在瞬时上窒息;逆变的。
fn(&'a u8)fn(&'static u8)但吃瞬时的东西可以是(!)吃永久的东西。
for<'r> fn(&'r u8)fn(&'a u8)高阶类型 for<'r> fn(&'r u8) 也是 fn(&'a u8)。
 

相比之下,这些不是🛑子类型化的例子:

AB解释
u16u8🛑 显然无效;u16 永远不应自动成为 u8。
u8u16🛑 设计上无效;具有不同数据的类型即使可以,也永远不会是子类型。
&'a mut u8&'a u8🛑 特洛伊木马,不是子类型化;而是强制转换(仍然有效,但不是子类型化)。
 
fn f(x: A) -> B {
    x
}

对于仅在生命周期上不同的类型,自动将 A 转换为 B NOM - 子类型化变性规则:

  • 较长的生命周期 'a 超过较短的 'b,是 'b 的子类型。
  • 这意味着 'static 是所有其他生命周期 'a 的子类型。
  • 具有参数的类型(例如 &'a T)是否是彼此的子类型,使用以下变性表:
构造1'aTU
&'a T协变协变
&'a mut T协变不变
Box<T>协变
Cell<T>不变
fn(T) -> U逆变协变
*const T协变
*mut T不变

协变 (Covariant) 意味着如果 A 是 B 的子类型,那么 T[A] 是 T[B] 的子类型。
逆变 (Contravariant) 意味着如果 A 是 B 的子类型,那么 T[B] 是 T[A] 的子类型。
不变 (Invariant) 意味着即使 A 是 B 的子类型,T[A] 和 T[B] 都不会是对方的子类型。

1 像 struct S<T> {} 这样的复合类型通过其使用的字段获得变性,如果混合了多种变性,通常会变得不变。

💡 换句话说,“常规”类型永远不是彼此的子类型(例如,u8 不是 u16 的子类型), Box<u32> 也永远不会是任何东西的子类型或超类型。 然而,通常情况下,Box<A> 可以是 Box<B> 的子类型(通过协变),如果 A 是 B 的子类型,这只会在 A 和 B 是“仅在生命周期上不同的同一种类型”时发生,例如,A 是 &'static u32,B 是 &'a u32。

 

编码指南url

惯用的 Rusturl

如果你习惯了 Java 或 C,请考虑这些。

惯用法代码
用表达式思考y = if x { a } else { b };
y = loop { break 5 };
fn f() -> u32 { 0 }
用迭代器思考(1..10).map(f).collect()
names.iter().filter(|x| x.starts_with("A"))
用 ? 测试缺失y = try_something()?;
get_option()?.run()?
使用强类型enum E { Invalid, Valid { … } } 而不是 ERROR_INVALID = -1
enum E { Visible, Hidden } 而不是 visible: bool
struct Charge(f32) 而不是 f32
非法状态:不可能my_lock.write().unwrap().guaranteed_at_compile_time_to_be_locked = 10; 1
thread::scope(|s| { /* 线程的存活时间不能超过 scope() */ });
避免全局状态在多个版本中被依赖会秘密地复制静态变量。🛑 🔗
提供构建器 (Builders)Car::new("Model T").hp(20).build();
使其为 Const在可能的情况下将函数标记为 const;在可行的情况下在 const {} 内运行代码。
不要 PanicPanic 不是异常,它们表明进程应立即中止!
仅在编程错误时 panic;否则使用 Option<T>STD 或 Result<T,E>STD。
如果是用户明确请求的,例如调用 obtain() 而不是 try_obtain(),panic 也可以。
在 const { NonZero::new(1).unwrap() } 中,panic 会变成编译错误,也可以。
适度使用泛型一个简单的 <T: Bound>(例如 AsRef<Path>)可以让你的 API 更易于使用。
复杂的约束使其难以理解。如有疑问,不要在泛型上发挥创意。
拆分实现像 Point<T> 这样的泛型可以为每个 T 有单独的 impl 以实现某种特化。
impl<T> Point<T> { /* 在这里添加通用方法 */ }
impl Point<f32> { /* 在这里添加只与 Point<f32> 相关的方法 */ }
Unsafe避免 unsafe {},↓ 通常有更安全、更快的解决方案。
实现 Trait#[derive(Debug, Copy, …)] 以及在需要时自定义 impl。
工具链定期运行 clippy 以显著提高你的代码质量。🔥
用 rustfmt 格式化你的代码以保持一致性。🔥
添加单元测试BK (#[test]) 以确保你的代码能工作。
添加文档测试BK (``` my_api::f() ```) 以确保文档与代码匹配。
文档用文档注释注解你的 API,这些注释会显示在 docs.rs 上。
不要忘记包含一个摘要句和示例标题。
如果适用:Panics, Errors, Safety, Abort 和 Undefined Behavior。

1 在大多数情况下,你应该优先使用 ? 而不是 .unwrap()。然而,在锁的情况下,返回的 PoisonError 表示另一个线程发生了 panic,所以 unwrap 它(从而传播 panic)通常是更好的主意。

 

🔥 我们强烈建议你也遵循 API 指南(清单) 用于任何共享项目!🔥

 

性能提示url

在将微基准测试移植到 Rust 或进行性能分析后,“我的代码很慢”有时会出现。

评级名称描述
🚀🍼Release 模式 BK 🔥总是使用 cargo build --release 来获得巨大的速度提升。
🚀🍼🚀⚠️目标原生 CPU 🔗在 config.toml 中添加 rustflags = ["-Ctarget-cpu=native"]。↑
🚀🍼⚖️代码生成单元 🔗代码生成单元 1 可能会产生更快的代码,但编译更慢。
🚀🍼预留容量 STD预分配集合可以减少分配压力。
🚀🍼回收集合 STD调用 x.clear() 并重用 x 可以防止分配。
🚀🍼追加到字符串 STD使用 write!(&mut s, "{}") 可以防止额外的分配。
🚀🍼⚖️全局分配器 STD在某些平台上,外部分配器(例如 mimalloc 🔗)更快。
Bump 分配 🔗廉价地获取临时的动态内存,尤其是在热循环中。
批量 API设计 API 以一次处理多个相似的元素,例如切片。
🚀🚀⚖️SoA / AoSoA 🔗除此之外,考虑数组的结构体(SoA)等。
🚀🚀⚖️SIMD STD 🚧在(数学密集型)批量 API 内部使用 SIMD 可以提供 2x - 8x 的提升。
减小数据大小小类型(例如 u8 vs u32,niche ?)和数据具有更好的缓存利用率。
保持数据就近 🔗将经常使用的数据存储在附近可以提高内存访问时间。
按大小传递 🔗小的(2-3 个字)结构体最好按值传递,大的按引用传递。
🚀🚀⚖️Async-Await 🔗如果并行等待发生得很多(例如,服务器 I/O),async 是个好主意。
线程 STD线程允许你一次在多个项上执行并行工作。
🚀... 在应用程序中通常对应用程序有好处,因为更短的等待时间意味着更好的用户体验。
🚀🚀⚖️... 在库内部在库内部不透明地使用线程通常不是个好主意,可能过于固执己见。
🚀🚀... 对库调用者然而,允许你的用户并行处理你是个极好的主意。
🚀🚀⚖️避免锁多线程代码中的锁会扼杀并行性。
🚀🚀⚖️避免原子操作不必要的原子操作(例如 Arc vs Rc)会影响其他内存访问。
🚀🚀⚖️避免伪共享 🔗确保不同 CPU 读/写的数据至少相距 64 字节。🔗
🚀🍼缓冲 I/O STD 🔥没有缓冲的原始 File I/O 效率极低。
🚀🍼🚀⚠️更快的哈希器 🔗默认的 HashMap STD 哈希器具有 DoS 攻击弹性但速度慢。
🚀🍼🚀⚠️更快的 RNG如果你使用加密 RNG,考虑换成非加密的。
🚀🚀⚖️避免 Trait 对象 🔗T.O. 减小代码大小,但增加内存间接性。
🚀🚀⚖️延迟 Drop 🔗在转储线程中 drop 重对象可以释放当前线程。
🚀🍼🚀⚠️未经检查的 API STD如果你 100% 自信,unsafe { unchecked_ } 会跳过检查。

标记为 🚀 的条目通常带来巨大的(> 2x)性能提升,🍼 即使事后也很容易实现,⚖️ 可能有昂贵的副作用(例如,内存,复杂性),⚠️ 有特殊风险(例如,安全,正确性)。

 

性能分析提示 💬

性能分析器是识别代码中热点的不可或缺的工具。为了获得最佳体验,请将以下内容添加到你的 Cargo.toml:

[profile.release]
debug = true

然后执行 cargo build --release 并用 Superluminal (Windows) 或 Instruments (macOS) 运行结果。 话虽如此,有许多性能机会是性能分析器找不到的,但需要设计进去。

 

Async-Await 101url

如果你熟悉 C# 或 TypeScript 中的 async / await,这里有一些需要记住的事情:

构造解释
async任何声明为 async 的东西总是返回一个 impl Future<Output=_>。STD
     async fn f() {}函数 f 返回一个 impl Future<Output=()>。
     async fn f() -> S {}函数 f 返回一个 impl Future<Output=S>。
     async { x }将 { x } 转换为一个 impl Future<Output=X>。
let sm = f(); 调用 async 的 f() 不会执行 f,而是产生一个状态机 sm。1 2
     sm = async { g() };同样,不会执行 { g() } 块;而是产生一个状态机。
runtime.block_on(sm);在 async {} 外部,调度 sm 实际运行。会执行 g()。3 4
sm.await在 async {} 内部,运行 sm 直到完成。如果 sm 未就绪,则让给运行时。

1 技术上讲,async 将随后的代码转换为一个匿名的、编译器生成的状态机类型;f() 实例化该机器。
2 状态机总是 impl Future,可能还 impl Send 等,取决于在 async 内部使用的类型。
3 状态机由工作线程直接通过运行时调用 Future::poll() 驱动,或间接通过父 .await 驱动。
4 Rust 不带运行时,需要外部 crate,例如 tokio。另外,futures crate 中有更多辅助工具。

在每个 x.await 处,状态机将控制权传递给下属状态机 x。在某个时刻,通过 .await 调用的低级状态机可能尚未就绪。在这种情况下,工作线程会一直返回到运行时,以便它可以驱动另一个 Future。一段时间后,运行时:

  • 可能会恢复执行。通常会,除非 sm / Future 被 drop。
  • 可能会用前一个工作线程或另一个工作线程恢复(取决于运行时)。

在 async 块内部编写的代码的简化图:

       consecutive_code();           consecutive_code();           consecutive_code();
开始 --------------------> x.await --------------------> y.await --------------------> 就绪
// ^                          ^     ^                               Future<Output=X> 就绪 -^
// 通过运行时或外部 .await 调用   |     |
//                                 |     这可能会在另一个线程上恢复(下一个最好的可用线程),
//                                 |     或者如果 Future 在等待时被 drop,则根本不会恢复。
//                                 |
//                                 执行 `x`。如果就绪:继续执行;否则,将此线程
//                                 返回给运行时。

在 async 结构中编写代码时的一些注意事项:

构造 1解释
sleep_or_block();绝对糟糕 🛑,永远不要暂停当前线程,会堵塞执行器。
set_TL(a); x.await; TL();绝对糟糕 🛑,await 可能会从其他线程返回,线程局部存储无效。
s.no(); x.await; s.go();可能糟糕 🛑,如果 Future 在等待时被 drop,await 将不会返回。2
Rc::new(); x.await; rc();非 Send 类型会阻止 impl Future 成为 Send;兼容性较差。

1 这里我们假设 s 是任何可能暂时处于无效状态的非局部变量; TL 是任何线程局部存储,并且包含该代码的 async {} 的编写 没有假设执行器的具体细节。
2 由于在 Future 被 drop 时,Drop 在任何情况下都会运行,如果必须在 .await 点跨越时将其置于不良状态,请考虑使用 drop guard 来清理/修复应用程序状态。

 

API 中的闭包url

存在一个子 trait 关系 Fn : FnMut : FnOnce。这意味着实现 Fn STD 的闭包也实现了 FnMut 和 FnOnce。同样,实现 FnMut STD 的闭包也实现了 FnOnce。STD

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

签名函数 g 可以调用 …函数 g 接受 …
g<F: FnOnce()>(f: F)… f() 最多一次。Fn, FnMut, FnOnce
g<F: FnMut()>(mut f: F)… f() 多次。Fn, FnMut
g<F: Fn()>(f: F)… f() 多次。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、Undefinedurl

Unsafe 导致 Unsound。Unsound 导致 Undefined。Undefined 导致原力的黑暗面。

安全代码 (Safe Code)

  • Safe 在 Rust 中有狭义的含义,大致是“内在地防止未定义行为 (UB)”。
  • 内在意味着语言不允许你用它自己来导致 UB。
  • 让飞机坠毁或删除你的数据库不是 UB,因此从 Rust 的角度来看是“安全的”。
  • 写入 /proc/[pid]/mem 来自我修改你的代码也是“安全的”,由此产生的 UB 不是内在引起的。
let y = x + x;  // 安全的 Rust 只保证这段代码的执行与
print(y);       // '规范'(说来话长……)一致。它不保证 y 是 2x
                // (X::add 可能实现得很糟糕)也不保证 y 被打印(Y::fmt 可能 panic)。

不安全代码 (Unsafe Code)

  • 标记为 unsafe 的代码具有特殊权限,例如,可以解引用裸指针,或调用其他 unsafe 函数。
  • 随之而来的是作者必须向编译器遵守的特殊承诺,编译器会相信你。
  • unsafe 代码本身并不坏,但很危险,对于 FFI 或奇异的数据结构是必需的。
// `x` 必须总是指向无竞争、有效、对齐、初始化的 u8 内存。
unsafe fn unsafe_f(x: *mut u8) {
    my_native_lib(x);
}

未定义行为 (Undefined Behavior, UB)

  • 如前所述,unsafe 代码意味着对编译器有特殊承诺(否则就不需要 unsafe)。
  • 未能遵守任何承诺都会使编译器产生错误的代码,执行这些代码会导致 UB。
  • 触发未定义行为后,任何事情都可能发生。阴险的是,其影响可能是 1) 微妙的,2) 在远离违规地点的地方显现,或 3) 仅在特定条件下可见。
  • 一个看似能工作的程序(包括任何数量的单元测试)并不能证明 UB 代码不会随时失效。
  • 带有 UB 的代码客观上是危险的、无效的,并且永远不应该存在。
if maybe_true() {
    let r: &u8 = unsafe { &*ptr::null() };   // 一旦这行运行,整个应用程序都是未定义的。即使
} else {                                     // 这行看似没做什么,应用程序现在也可能运行
    println!("the spanish inquisition");     // 两个路径,破坏数据库,或做任何其他事情。
}

不健全代码 (Unsound Code)

  • 任何安全的 Rust 代码,如果可能(即使只是理论上)对任何用户输入产生 UB,那么它总是不健全的 (unsound)。
  • 同样,unsafe 代码如果通过违反上述承诺而自行调用 UB,也是不健全的。
  • 不健全的代码是稳定性和安全性的风险,并违反了许多 Rust 用户的基本假设。
fn unsound_ref<T>(x: &T) -> &u128 {      // 对用户来说,签名看起来是安全的。碰巧
    unsafe { mem::transmute(x) }         // 如果用 &u128 调用就没问题,但对于几乎
}                                        // 其他所有东西都是 UB。
 

负责任地使用 Unsafe 💬

  • 除非绝对必要,否则不要使用 unsafe。
  • 遵循 Nomicon、Unsafe 指南,始终遵守所有安全规则,并绝不调用 UB。
  • 最小化 unsafe 的使用,并将其封装在易于审查的小而健全的模块中。
  • 永远不要创建不健全的抽象;如果你不能正确地封装 unsafe,就不要做。
  • 每个 unsafe 单元都应附有纯文本的理由,概述其安全性。
 

对抗性代码 🝖url

对抗性代码是安全的第三方代码,它能编译但不遵循 API 的期望,并可能干扰你自己的(安全)保证。

你编写用户代码可能会…
fn g<F: Fn()>(f: F) { … }意外地 panic。
struct S<X: T> { … }糟糕地实现 T,例如,滥用 Deref 等。
macro_rules! m { … }做以上所有事情;调用点可能有奇怪的作用域。
 
风险模式描述
#[repr(packed)]紧凑对齐可能会使引用 &s.x 无效。
impl std::… for S {}任何 trait impl,尤其是 std::ops,都可能被破坏。特别是…
     impl Deref for S {}可能随机地 Deref,例如 s.x != s.x,或 panic。
     impl PartialEq for S {}可能违反相等性规则;panic。
     impl Eq for S {}可能导致 s != s;panic;在 HashMap 等中不得使用 s。
     impl Hash for S {}可能违反哈希规则;panic;在 HashMap 等中不得使用 s。
     impl Ord for S {}可能违反排序规则;panic;在 BTreeMap 等中不得使用 s。
     impl Index for S {}可能随机索引,例如 s[x] != s[x];panic。
     impl Drop for S {}可能在作用域 {} 结束时,或在赋值 s = new_s 期间运行代码或 panic。
panic!()用户代码可能在任何时候 panic,导致中止或展开。
catch_unwind(|| s.f(panicky))另外,调用者可能会强制观察 s 的破坏状态。
let … = f();变量名会影响 Drop 执行的顺序。1 🛑

1 值得注意的是,当你将变量从 _x 重命名为 _ 时,你也会改变 Drop 的行为,因为你改变了语义。名为 _x 的变量将在其作用域结束时执行 Drop::drop(),而名为 _ 的变量可以在“表面”赋值时立即执行(“表面”是因为名为 _ 的绑定意味着通配符REF丢弃这个,这会尽快发生,通常是立即)!

 

影响

  • 如果安全性依赖于类型在大多数(std::)trait 方面的合作,那么泛型代码不可能是安全的。
  • 如果需要类型合作,你必须使用 unsafe trait(可能自己实现)。
  • 你必须考虑在意想不到的地方(例如,重新赋值、作用域结束)随机执行代码。
  • 在最坏情况的 panic 之后,你可能仍然是可观察的。

作为推论,安全但致命的代码(例如 airplane_speed<T>())可能也应该遵循这些指南。

 

API 稳定性url

更新 API 时,这些更改可能会破坏客户端代码。RFC 主要更改(🔴)肯定会破坏,而次要更改(🟡)可能会破坏:

 
Crates
🔴 使一个之前在稳定版上编译的 crate 需要nightly。
🔴 移除 Cargo 特性。
🟡 更改现有的 Cargo 特性。
 
模块
🔴 重命名/移动/移除任何公共项。
🟡 添加新的公共项,因为这可能会破坏做 use your_crate::* 的代码。
 
结构体
🔴 当所有当前字段都为公共时,添加私有字段。
🔴 当没有私有字段存在时,添加公共字段。
🟡 当至少有一个私有字段已经存在时(更改前后),添加或移除私有字段。
🟡 从一个所有字段都为私有的元组结构体(至少有一个字段)变为一个普通结构体,反之亦然。
 
枚举
🔴 添加新的变体;可以通过早期使用 #[non_exhaustive] REF 来缓解
🔴 向一个变体添加新的字段。
 
Trait
🔴 添加一个非默认项,会破坏所有现有的 impl T for S {}。
🔴 对项签名的任何非平凡更改,会影响消费者或实现者。
🔴 实现任何“基础”trait,因为不实现一个基础 trait 已经是一个承诺。
🟡 添加一个默认项;可能会与其他现有 trait 引起分发歧义。
🟡 添加一个默认类型参数。
🟡 实现任何非基础 trait;也可能引起分发歧义。
 
固有实现
🟡 添加任何固有项;可能会导致客户端优先选择它而不是 trait 函数并产生编译错误。
 
类型定义中的签名
🔴 收紧约束(例如,<T> 到 <T: Clone>)。
🟡 放松约束。
🟡 添加默认类型参数。
🟡 泛化为泛型。
函数中的签名
🔴 添加/移除参数。
🟡 引入一个新的类型参数。
🟡 泛化为泛型。
 
行为更改
🔴 / 🟡 更改语义可能不会导致编译器错误,但可能会使客户端做错事。
 

杂项url

专业书籍,另请参见 Rust 书籍小集。

主题 ️📚描述
API 指南如何编写惯用且可重用的 Rust。
异步编程 🚧解释 async 代码、Futures 等。
Cargo如何使用 cargo 和编写 Cargo.toml。
命令行工具关于创建命令行工具的信息。
食谱演示良好实践的简单示例集合。
设计模式惯用法、模式、反模式。
版本指南使用 Rust 2015、Rust 2018 及更高版本。
嵌入式使用嵌入式和 #![no_std] 设备。
函数式编程术语 🝖用 Rust 解释的函数式编程术语集合。
Rustc 开发指南 🝖解释编译器内部如何工作。
Rust 宏小书社区关于 Rust 宏的集体知识。
性能提高速度和内存使用的技术。
RFCs 🝖查找已接受的 RFC 以及它们如何改变语言。
Rustdoc关于如何自定义 cargo doc 和 rustdoc 的提示。
不安全代码指南 🚧关于编写 unsafe 代码的简明信息。
不稳定版 🝖关于不稳定项的信息,例如 #![feature(…)]。
 

全面的常见组件查找表。

表格 📋描述
Rust Forge列出发布火车和为编译器工作的人员的链接。
     支持的平台所有支持的平台及其等级。
     组件历史 🚧检查一个平台的各种 Rust 工具的nightly状态。
Clippy Lints你可能感兴趣的所有 clippy lints。
Rustfmt 配置你可以在 .rustfmt.toml 中使用的所有 rustfmt 选项。
 

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

服务 ⚙️描述
Rust Playground尝试和分享 Rust 代码片段。
crates.io所有 Rust 的第三方库。
lib.rs优质 Rust 库和应用程序的非官方概述。
blessed.rs 💬Rust 生态系统的非官方指南,更加固执己见。
std.rsstd 文档的快捷方式。
stdrs.dev 🝖std 文档的快捷方式,包括编译器内部模块。
docs.rs第三方库的文档,从源代码自动生成。
releases.rs之前和即将发布的版本的发布说明。
 

打印与 PDFurl

想要这份 Rust 速查表的 PDF 吗?在这里下载最新的 PDF(A4)和Letter格式。或者,通过文件 > 打印然后“另存为 PDF”自行生成(在 Chrome 中效果很好,在 Firefox 中有些问题)。