包含可点击链接,指向 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 📈
语言结构
幕后探秘
内存布局
杂项
标准库
工具链
处理类型
编码指南
如果你是 Rust 新手,或者想尝试下面的内容:
Rust 在可衡量方面做得非常好的事情
cargo(构建就是好用)、clippy(700+ 代码质量检查)、rustup(轻松管理工具链)。模块化入门资源
此外,请考虑 The Book、BK Rust by Example、EX 标准库STD 和 Learn Rust。🔗
个人观点 💬 — 如果你从未见过或使用过任何 Rust,最好在继续之前访问上面的链接之一;否则下一章可能会感觉有点简洁。
通过关键字定义的数据类型和内存位置。
| 示例 | 解释 |
|---|---|
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)。 |
授予对非自有内存的访问权限。另请参见泛型与约束部分。
| 示例 | 解释 |
|---|---|
&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 | 特殊的生命周期,持续整个程序执行期间。 |
定义代码单元及其抽象。
| 示例 | 解释 |
|---|---|
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,并且必须指向有效的目标。
控制函数内的执行流程。
| 示例 | 解释 |
|---|---|
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 在嵌套循环中用于流控制很有用。 |
break | Break 表达式 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()。 |
将项目分割成更小的单元并最小化依赖。
| 示例 | 解释 |
|---|---|
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。
类型的简写名称,以及将一种类型转换为另一种类型的方法。
| 示例 | 解释 |
|---|---|
type T = S; | 创建一个类型别名 (type alias),BK REF 即 S 的另一个名称。 |
Self | 实现类型的类型别名,REF 例如 fn new() -> Self。 |
self | fn 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 参见下文的类型转换以了解所有类型之间的转换方式。
在实际编译发生前展开的代码生成结构。
| 示例 | 解释 |
|---|---|
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 参见下文的工具指令以了解所有片段指定符。
存在于 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 条件也必须为真才能匹配。 |
泛型与类型构造器、trait 和函数相结合,为您的用户提供更大的灵活性。
| 示例 | 解释 |
|---|---|
struct S<T> … | 带有类型参数(此处 T 是占位符)的泛型 (generic) BK EX 类型。 |
S<T> where T: R | Trait 约束 (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 在默认函数中有用(非默认的反正也需要实现)。 |
实际的类型和 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) {} | 同上,简写版本。 |
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}"))会正确地渲染它。
调试器都恨他。用这个奇怪的技巧避免 bug。
| 示例 | 解释 |
|---|---|
/// | 外部行文档注释,1 BK EX REF 在类型、trait、函数等上使用。 |
//! | 内部行文档注释,主要用于文件顶部。 |
// | 行注释,用于记录代码流程或内部实现。 |
/* … */ | 块注释。2 🗑️ |
/** … */ | 外部块文档注释。2 🗑️ |
/*! … */ | 内部块文档注释。2 🗑️ |
1 工具指令概述了您可以在文档注释中做什么。
2 由于用户体验不佳,通常不鼓励使用。如果可能,请使用等效的行注释并利用 IDE 支持。
这些符号不适合任何其他类别,但了解它们仍然很有用。
| 示例 | 解释 |
|---|---|
! | 总是为空的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 |
Rust 支持您所期望的大多数运算符(+, *, %, =, == 等),包括重载。STD 由于它们在 Rust 中的行为没有不同,我们在此不列出它们。
可能会对你的心智造成可怕影响的神秘知识,强烈推荐。
与 C 和 C++ 一样,Rust 基于一个抽象机。
→
→
→
除了极少数例外,你永远不被“允许”去思考实际的 CPU。你是在为一个抽象的 CPU 编写代码。然后 Rust(在某种程度上)理解你想要什么,并将其转换为实际的 RISC-V / x86 / … 机器码。
这个抽象机
左边是人们可能错误地认为如果 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强制它。↓
如果有些事情“在你仔细思考后觉得不应该能工作”但它却能工作,那可能就是因为下面这些原因之一。
| 名称 | 描述 |
|---|---|
| 类型强制转换 (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 更容易,但却阻碍了你学习它。如果你想建立真正的理解,花些额外的时间去探索它们。
一个关于移动、引用和生命周期的图文指南。
Box<T> 这样的栈代理来处理),&str 中 str 部分的存放地),1 对于固定大小的值,栈的管理非常简单:在你需要时多拿几个字节,一旦你离开就丢弃。然而,向这些短暂的位置分发指针,正是生命周期存在的本质;也是本章其余部分的主题。
let t = S(1);
t 的内存位置,其类型为 S,里面存储的值为 S(1)。let 声明,该位置存在于栈上。10x7(“告诉我那个变量的地址”),S(1)(“给那个变量加一”)。t 可以表示 t 的位置,这里是 0x7,和 t 内的值,这里是 S(1)。1 参见上文,↑ 对于完全同步的代码是真的,但 async 栈帧可能会通过运行时将其放在堆上。
let a = t;
t 内的值到 a 的位置,如果 S 是 Copy 类型,则会复制它。t 无效,不能再被读取。
unsafe)访问 t,它们可能仍然看起来像有效的 S,但
任何试图将它们用作有效 S 的尝试都是未定义行为。↓Copy 类型。它们会稍微改变规则,但不多:
let c: S = M::new();
M::new() 的字节不能被转换为 S 类型的形式。{
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() 在 a 和 c 上被调用了两次,但没有在 t 上调用。Copy 的值在大多数时候都会被 drop;例外包括 mem::forget()、Rc 循环、abort()。fn f(x: S) { … }
let a = S(1); // <- 我们在这里
f(a);
f 被调用前,a 中的值被移动到栈上一个“约定好的”位置,在 f 期间,它就像一个“局部变量” x 一样工作。1 实际位置取决于调用约定,实际上可能根本不会在栈上,但这不改变心智模型。
fn f(x: S) {
if once() { f(x) } // <- 我们在这里(递归前)
}
let a = S(1);
f(a);
fn f(x: S) {
if once() { f(x) }
let m = M::new() // <- 我们在这里(递归后)
}
let a = S(1);
f(a);
f 的递归产生了第二个 x,在递归之后,这块内存被部分地用于 m。到目前为止的关键要点是,有多种方式可以使之前持有某个类型有效值的内存位置在此期间不再持有。 正如我们稍后将看到的,这对指针有影响。
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;
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(上图中未显示)。let mut a = …;
let r = &mut a;
let d = *r; // 移出值无效,`a` 会变空。
*r = M::new(); // 存储非 S 值无效,没有意义。
&mut T 必须提供与变量相同的保证,甚至更多,因为它们不能溶解目标:
let p: *const S = questionable_origin();
unsafe 的,将无效的 *p 当作有效的是未定义行为。↓r: &'a S,
r 指向的位置**需要被访问或锁定的代码行;r 本身的“存在时间”(作为代码行)无关(当然,它需要存在得更短,就是这样)。&'static S 意味着地址必须在所有代码行期间都有效。1 文档中有时在区分各种作用域和生命周期时存在模糊。 我们在这里试图 pragmatic,但欢迎提出建议。
2 Live lines 可能是更合适的术语……
r: &'c S 的含义r: &'c S,这意味着:
r 持有一个 S 的地址,r 指向的任何地址必须并且将至少存在 'c 这么久,r 本身的寿命不能长于 'c。{
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。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 就可以再次使用了。fn f(x: &S, y:&S) -> &u8 { … }
let b = S(1);
let c = S(2);
let r = f(&b, &c);
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 都需要保持锁定。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,我们仍然无法区分,两者都需要保持锁定。let mut c = S(2);
let r = f(&c);
let s = r;
// <- 不是这里,`s` 延长了 `c` 的锁定。
print_byte(s);
let a = c; // <- 但是在这里,不再使用 `r` 或 `s`。
// 返回短 ('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,因为你不能有 一个共享引用和一个可变引用指向同一个目标。
_{
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。ref x3)可以使值(例如元组 (S(3), S(6)))保留更长时间,因此作为该元组一部分的 S(6) 只能在对其兄弟 S(3) 的引用消失后才能被 drop。↕️ 点击展开示例。
常见类型的字节表示。
语言核心内置的基本类型。
boolu8, i8u16, i16u32, i32u64, i64u128, i128usize, isizeptr 大小相同。
f16 🚧 f32f64f128 🚧 | 类型 | 最大值 |
|---|---|
u8 | 255 |
u16 | 65_535 |
u32 | 4_294_967_295 |
u64 | 18_446_744_073_709_551_615 |
u128 | 340_282_366_920_938_463_463_374_607_431_768_211_455 |
usize | 取决于平台指针大小,与 u16、u32 或 u64 相同。 |
| 类型 | 最大值 |
|---|---|
i8 | 127 |
i16 | 32_767 |
i32 | 2_147_483_647 |
i64 | 9_223_372_036_854_775_807 |
i128 | 170_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.0 | 6.10 ⋅ 10 -5 | 2048 |
f32 | 3.40 ⋅ 10 38 | 3.40 ⋅ 10 -38 | 16_777_216 |
f64 | 1.79 ⋅ 10 308 | 2.23 ⋅ 10 -308 | 9_007_199_254_740_992 |
f128 🚧 | 1.19 ⋅ 10 4932 | 3.36 ⋅ 10 -4932 | 2.07 ⋅ 10 34 |
1 最大整数 M,使得所有其他整数 0 <= X <= M 都能在该类型中无损表示。换句话说,可能存在更大的整数仍能无损表示(例如 f16 的 65504),但直到该值,无损表示是有保证的。
浮点值为视觉清晰度而近似。负数极限是值乘以 -1。
f32 的示例位表示*:
SEEEEEEEEFFFFFFFFFFFFFFFFFFFFFFF解释:
| f32 | S (1) | E (8) | F (23) | 值 |
|---|---|---|---|---|
| 规格化数 | ± | 1 到 254 | 任意 | ±(1.F)2 * 2E-127 |
| 非规格化数 | ± | 0 | 非零 | ±(0.F)2 * 2-126 |
| 零 | ± | 0 | 0 | ±0 |
| 无穷大 | ± | 255 | 0 | ±∞ |
| NaN | ± | 255 | 非零 | NaN |
同样,对于 f64 类型,它看起来像这样:
| f64 | S (1) | E (11) | F (52) | 值 |
|---|---|---|---|---|
| 规格化数 | ± | 1 到 2046 | 任意 | ±(1.F)2 * 2E-1023 |
| 非规格化数 | ± | 0 | 非零 | ±(0.F)2 * 2-1022 |
| 零 | ± | 0 | 0 | ±0 |
| 无穷大 | ± | 2047 | 0 | ±∞ |
| NaN | ± | 2047 | 非零 | NaN |
| 转换1 | 得到 | 注意 |
|---|---|---|
3.9_f32 as u8 | 3 | 截断,考虑先用 x.round()。 |
314_f32 as u8 | 255 | 取最接近的可用数。 |
f32::INFINITY as u8 | 255 | 同上,将 INFINITY 视为非常大的数。 |
f32::NAN as u8 | 0 | - |
_314 as u8 | 58 | 截断多余的位。 |
_257 as i8 | 1 | 截断多余的位。 |
_200 as i8 | -56 | 截断多余的位,最高有效位可能也表示负数。 |
| 操作1 | 得到 | 注意 |
|---|---|---|
200_u8 / 0_u8 | 编译错误。 | - |
200_u8 / _0 d, r | Panic。 | 常规数学运算可能 panic;此处:除以零。 |
200_u8 + 200_u8 | 编译错误。 | - |
200_u8 + _200 d | Panic。 | 考虑使用 checked_、wrapping_ 等。STD |
200_u8 + _200 r | 144 | 在 release 模式下会溢出。 |
-128_i8 * -1 | 编译错误。 | 会溢出(128_i8 不存在)。 |
-128_i8 * _1neg d | Panic。 | - |
-128_i8 * _1neg r | -128 | 在 release 模式下溢出回 -128。 |
1_u8 / 2_u8 | 0 | 其他整数除法会截断。 |
0.8_f32 + 0.1_f32 | 0.90000004 | - |
1.0_f32 / 0.0_f32 | f32::INFINITY | - |
0.0_f32 / 0.0_f32 | f32::NAN | - |
x < f32::NAN | false | NAN 比较总是返回 false。 |
x > f32::NAN | false | NAN 比较总是返回 false。 |
f32::NAN == f32::NAN | false | 使用 f32::is_nan() STD 代替。 |
1 表达式 _100 表示任何可能包含值 100 的东西,例如 100_i32,但对编译器是不透明的。
d Debug 构建。
r Release 构建。
charstrUTF-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 不允许🛑持有任意的位模式。 |
| 字符串 | 描述 |
|---|---|
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()1 | 49 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()1 | 49 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 … |
❤,其 Unicode 代码点为 (U+2764),在 char 内部表示为 64 27 00 00,但在 str 中被 UTF-8 编码为 e2 9d a4。❤️ 是 ❤ 和 U+FE0F 变体选择器的组合,因此 `t` 的字符数比 `s` 多。
⚠️ 由于似乎是浏览器 bug,Safari 和 Edge 在脚注 3 和 4 中渲染心形有误,尽管它们能在上面的
s和t中正确地区分它们。
用户可定义的基本类型。实际的布局 REF 取决于表示 (representation);REF 可能存在填充。
TTT: ?SizedT[T; n]TTTn 个元素的固定数组。[T]TTTSized(也不携带 len 信息),并且最&[T] 的形式存在。↓struct S;;
(A, B, C)ABCBAC#[repr(C)]),否则类型布局struct S { b: B, c: C } BCC↦B另请注意,两个具有完全相同字段的类型
A(X, Y)和B(X, Y)仍然可以有不同的布局;在没有表示保证的情况下,切勿使用transmute()STD。
这些和类型 (sum types) 持有其子类型之一的值:
enum E { A, B, C }标签A
标签B
标签C
union { … }A
B
C
引用提供对第三方内存的安全访问,裸指针提供unsafe访问。
相应的 mut 类型与其不可变对应项具有相同的数据布局。
&'a Tptr2/4/8
meta2/4/8
Tt(类型为 T),'a 时间。*const Tptr2/4/8
meta2/4/8
许多引用和指针类型可以携带一个额外的字段,即指针元数据 (pointer metadata)。STD 它可以是目标的元素或字节长度,或指向一个虚函数表 (vtable)的指针。带元数据的指针称为胖指针 (fat),否则称为瘦指针 (thin)。
&'a Tptr2/4/8
T&'a Tptr2/4/8
len2/4/8
TT 是 DST struct,例如S { x: [u8] },
元数据字段 len 是&'a [T]ptr2/4/8
len2/4/8
TT[T] 的'a 被省略,通常写作 &[T]。&'a strptr2/4/8
len2/4/8
UTF-8str 的len 是字节长度。&'a dyn Traitptr2/4/8
ptr2/4/8
T*Drop::drop(&mut T) |
size |
align |
*Trait::f(&T, …) |
*Trait::g(&T, …) |
*Drop::drop()、*Trait::f() 等是指向它们各自为 T 的 impl 的指针。具有自动管理数据块的即席函数,该数据块捕获 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() + zYZ|x| x + y.f() + zptr2/4/8
ptr2/4/8
YZ也产生匿名的
fn,例如fc1(C1, X)或fc2(&C2, X)。细节取决于支持哪个FnOnce、FnMut、Fn...,这基于捕获类型的属性。
1 简单来说,闭包是一个方便编写的“迷你函数”,它接受参数,并且需要一些局部变量来完成其工作。因此,它是一个类型(包含所需的局部变量)和一个函数。“捕获环境”是一种花哨的说法,用来描述闭包类型如何持有这些局部变量,要么是通过移动的值,要么是通过指针。有关各种含义,请参见API 中的闭包 ↓。
Rust 的标准库将上述原始类型组合成具有特殊语义的有用类型,例如:
Option<T> STD标签标签T
NonNull。STDResult<T, E> STD标签E
标签T
E,要么是T 类型的值。ManuallyDrop<T> STDTT::drop() 被AtomicUsize STDusize2/4/8
MaybeUninit<T> STD未̼̟̔͛定̥͕͐͞义̛̲͔̦̳̑̓̐
T
T。处理未初始化PhantomData<T> STD
Pin<P> STDP
P::DerefP 的目标被“永远”固定Pin 的生命周期。内部值Unpin。STD🛑 所有描述仅用于说明目的。 这些字段应存在于最新的
stable版本中,但 Rust 对其布局不做任何保证,除非文档允许,否则您不得 尝试不安全地访问任何内容。
UnsafeCell<T> STDTCell<T> STDTTRefCell<T> STDborrowedTT 的动态Cell 一样,Send,但不是 Sync。OnceCell<T> STD标签标签T
LazyCell<T, F> STD标签Uninit<F>
标签Init<T>
标签Poisoned
Box<T> STDptr2/4/8
meta2/4/8
TT,栈代理可能携带Box<[T]>)。Vec<T> STDptr2/4/8
len2/4/8
capacity2/4/8
TTLinkedList<T> STD🝖 head2/4/8
tail2/4/8
len2/4/8
next2/4/8
prev2/4/8
Thead 和 tail 要么是 null,要么指向堆上的节点。prev 和 next 节点。VecDeque<T> STDhead2/4/8
len2/4/8
ptr2/4/8
capacity2/4/8
TTHhead 在作为环形缓冲区的数组中选择。这意味着内容可能HashMap<K, V> STDbmask2/4/8
ctrl2/4/8
left2/4/8
len2/4/8
K:VK:VK:VK:VHashSet STD 与 HashMap 相同,V 消失了。堆视图被严重简化了。🛑 BinaryHeap<T> STDptr2/4/8
capacity2/4/8
len2/4/8
T0T1T1T2T22N 个元素。每个 T T 都比其String STDptr2/4/8
capacity2/4/8
len2/4/8
UTF-8String 与 &str 和 &[char] 的区别。CString STDptr2/4/8
len2/4/8
ABC∅OsString STDptr2/4/8
capacity2/4/8
len2/4/8
如果类型不包含用于 T 的 Cell,这些类型通常与上述 Cell 类型之一结合使用,以允许事实上的共享可变性。
Rc<T> STDptr2/4/8
meta2/4/8
strng2/4/8weak2/4/8TT 的所有权。需要嵌套 Cell
RefCell 来允许修改。既不是 Send 也不是 Sync。Arc<T> STDptr2/4/8
meta2/4/8
strng2/4/8weak2/4/8TT 本身是 Send 和 Sync,则允许在线程间共享。Mutex<T> STD / RwLock<T> STDinnerpoison2/4/8TArc 中才能在解耦的线程间共享,scope() STD 用于作用域线程。
Cow<'a, T> STD标签T::Owned
标签ptr2/4/8
TT 的只读引用,ToOwned STD 一些常见但容易忘记的代码片段。更多信息请参见 Rust Cookbook 🔗。
| 意图 | 代码片段 |
|---|---|
连接字符串(任何实现了 Display↓ 的类型)。STD 1 '21 | format!("{x}{y}") |
追加字符串(任何 Display 到任何 Write)。'21 STD | write!(x, "{y}") |
| 按分隔符模式分割。STD 🔗 | s.split(pattern) |
… 使用 &str | s.split("abc") |
… 使用 char | s.split('/') |
| … 使用闭包 | s.split(char::is_numeric) |
| 按空白分割。STD | s.split_whitespace() |
| 按换行符分割。STD | s.lines() |
| 按正则表达式分割。🔗 2 | Regex::new(r"\s")?.split("one two three") |
1 会分配内存;如果 x 或 y 之后不再使用,请考虑使用 write! 或 std::ops::Add。
2 需要 regex crate。
| 意图 | 代码片段 |
|---|---|
| 带可变参数的宏 | 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 是对象安全的金丝雀代码。REF | const _: Option<&dyn T> = None; |
| 用于统一类型的Semver 技巧。🔗 | Cargo.toml 中的 my_crate = "next.version" + 重新导出类型。 |
| 在自己的 crate 内部使用宏。🔗 | macro_rules! internal_macro {} 与 pub(crate) use internal_macro; |
假设你在线程 1 中持有一些变量,并且想要将它们移动到线程 2,或者将它们的引用传递给线程 3。这是否被允许分别由 SendSTD 和 SyncSTD 决定:
| 示例 | 解释 |
|---|---|
Mutex<u32> | 既是 Send 也是 Sync。你可以安全地将其传递或借给另一个线程。 |
Cell<u32> | Send,但不是 Sync。可移动,但其引用将允许并发的非原子写入。 |
MutexGuard<u32> | Sync,但不是 Send。锁与线程绑定,但其引用的使用不允许数据竞争。 |
Rc<u32> | 两者都不是,因为它是一个带有非原子计数器的、易于克隆的堆代理。 |
| Trait | Send | !Send |
|---|---|---|
Sync | 大多数类型 … Arc<T>1,2, Mutex<T>2 | MutexGuard<T>1, RwLockReadGuard<T>1 |
!Sync | Cell<T>2, RefCell<T>2 | Rc<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; } 1 | Future 是 !Send,因为 Rc 跨越了 .await 点。 |
async || { } 🚧 | 异步闭包 Send 如果所有捕获都是 Send,结果的 Future 也是 Send 如果内部也没有 !Send 类型。 |
async |x| { x + y } 🚧 | 异步闭包 Send 如果 y 是 Send。Future Send 如果 x 和 y 是 Send。 |
1 这是一些伪代码,旨在传达要点,其思想是在一个 .await 点之前有一个 Rc,并在该点之后继续使用它。
CPU 缓存、内存写入,以及原子操作如何影响它们。
现代 CPU 不直接访问内存,只访问其缓存。每个 CPU 都有自己的缓存,比 RAM 快 100 倍,但小得多。它以缓存行的形式存在,🔗 是一些字节的切片窗口,用于跟踪它是否是主内存的独占 (E)、共享 (S) 或已修改 (M) 🔗 视图。缓存之间相互通信以确保缓存一致性 (coherence),🔗 即,“足够小”的数据将被所有其他 CPU“立即”看到,但这可能会使 CPU 停顿。
左:编译器和CPU 都可以自由地重排🔗并拆分读/写内存访问。即使你明确说了 write(1); write(23); write(4),你的编译器也可能认为先写 23 是个好主意;此外,你的 CPU 可能坚持拆分写操作,先做 3 再做 2。这些步骤中的每一个都可能被 CPU2 通过 unsafe 的数据竞争观察到(甚至是不可能的 O3)。重排对于锁也是致命的。右:半相关的,即使两个 CPU 不试图访问彼此的数据(例如,更新 2 个独立的变量),如果底层内存被 2 个缓存行映射(伪共享),它们仍然可能会经历显著的性能损失。🔗
原子操作通过做两件事来解决上述问题,它们 - 确保读/写/更新不会通过临时锁定其他 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(或更强的)。写入者必须确保它希望释放到内存的所有其他数据都在原子信号之前,而希望获取这些数据的读取者必须确保它们的其他读取仅在原子信号之后完成。
处理集合中的元素。
基础
假设你有一个类型为 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>IteratorItem = 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>IntoIteratorItem = T;To = IntoIter<T>T。&Collection<T>IntoIteratorItem = &T;To = Iter<T>&T。&mut Collectn<T>IntoIteratorItem = &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>IteratorItem = &T;IterMut<T>IteratorItem = &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>FromIteratorExtendIntoIter<T>DoubleEndedIt… ExactSizeIt… FusedIterator Iter<T>DoubleEndedIt… ExactSizeIt… FusedIterator IterMut<T>DoubleEndedIt… ExactSizeIt… FusedIterator 编写集合可能是一项工作。好消息是,如果你遵循了所有这些步骤,你的集合将会感觉像是一等公民。
目前为止最正确的数字转换。
| ↓ 有 / 想要 → | u8 … i128 | f32 / f64 | String |
|---|---|---|---|
u8 … i128 | u8::try_from(x)? 1 | x as f32 3 | x.to_string() |
f32 / f64 | x as u8 2 | x as f32 | x.to_string() |
String | x.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,类型是 … | 使用这个 … |
|---|---|
String | x |
CString | x.into_string()? |
OsString | x.to_str()?.to_string() |
PathBuf | x.to_str()?.to_string() |
Vec<u8> 1 | String::from_utf8(x)? |
&str | x.to_string() i |
&CStr | x.to_str()?.to_string() |
&OsStr | x.to_str()?.to_string() |
&Path | x.to_str()?.to_string() |
&[u8] 1 | String::from_utf8_lossy(x).to_string() |
如果你有一个 x,类型是 … | 使用这个 … |
|---|---|
String | CString::new(x)? |
CString | x |
OsString | CString::new(x.to_str()?)? |
PathBuf | CString::new(x.to_str()?)? |
Vec<u8> 1 | CString::new(x)? |
&str | CString::new(x)? |
&CStr | x.to_owned() i |
&OsStr | CString::new(x.to_os_string().into_string()?)? |
&Path | CString::new(x.to_str()?)? |
&[u8] 1 | CString::new(Vec::from(x))? |
*mut c_char 2 | unsafe { CString::from_raw(x) } |
如果你有一个 x,类型是 … | 使用这个 … |
|---|---|
String | OsString::from(x) i |
CString | OsString::from(x.to_str()?) |
OsString | x |
PathBuf | x.into_os_string() |
Vec<u8> 1 | unsafe { OsString::from_encoded_bytes_unchecked(x) } |
&str | OsString::from(x) i |
&CStr | OsString::from(x.to_str()?) |
&OsStr | OsString::from(x) i |
&Path | x.as_os_str().to_owned() |
&[u8] 1 | unsafe { OsString::from_encoded_bytes_unchecked(x.to_vec()) } |
如果你有一个 x,类型是 … | 使用这个 … |
|---|---|
String | PathBuf::from(x) i |
CString | PathBuf::from(x.to_str()?) |
OsString | PathBuf::from(x) i |
PathBuf | x |
Vec<u8> 1 | unsafe { PathBuf::from(OsString::from_encoded_bytes_unchecked(x)) } |
&str | PathBuf::from(x) i |
&CStr | PathBuf::from(x.to_str()?) |
&OsStr | PathBuf::from(x) i |
&Path | PathBuf::from(x) i |
&[u8] 1 | unsafe { PathBuf::from(OsString::from_encoded_bytes_unchecked(x.to_vec())) } |
如果你有一个 x,类型是 … | 使用这个 … |
|---|---|
String | x.into_bytes() |
CString | x.into_bytes() |
OsString | x.into_encoded_bytes() |
PathBuf | x.into_os_string().into_encoded_bytes() |
Vec<u8> 1 | x |
&str | Vec::from(x.as_bytes()) |
&CStr | Vec::from(x.to_bytes_with_nul()) |
&OsStr | Vec::from(x.as_encoded_bytes()) |
&Path | Vec::from(x.as_os_str().as_encoded_bytes()) |
&[u8] 1 | x.to_vec() |
如果你有一个 x,类型是 … | 使用这个 … |
|---|---|
String | x.as_str() |
CString | x.to_str()? |
OsString | x.to_str()? |
PathBuf | x.to_str()? |
Vec<u8> 1 | std::str::from_utf8(&x)? |
&str | x |
&CStr | x.to_str()? |
&OsStr | x.to_str()? |
&Path | x.to_str()? |
&[u8] 1 | std::str::from_utf8(x)? |
如果你有一个 x,类型是 … | 使用这个 … |
|---|---|
String | Path::new(x) r |
CString | Path::new(x.to_str()?) |
OsString | Path::new(x.to_str()?) r |
PathBuf | Path::new(x.to_str()?) r |
Vec<u8> 1 | unsafe { Path::new(OsStr::from_encoded_bytes_unchecked(&x)) } |
&str | Path::new(x) r |
&CStr | Path::new(x.to_str()?) |
&OsStr | Path::new(x) r |
&Path | x |
&[u8] 1 | unsafe { Path::new(OsStr::from_encoded_bytes_unchecked(x)) } |
如果你有一个 x,类型是 … | 使用这个 … |
|---|---|
String | x.as_bytes() |
CString | x.as_bytes() |
OsString | x.as_encoded_bytes() |
PathBuf | x.as_os_str().as_encoded_bytes() |
Vec<u8> 1 | &x |
&str | x.as_bytes() |
&CStr | x.to_bytes_with_nul() |
&OsStr | x.as_encoded_bytes() |
&Path | x.as_os_str().as_encoded_bytes() |
&[u8] 1 | x |
| 你想要 | 并且有 x | 使用这个 … |
|---|---|---|
*const c_char | CString | x.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 结尾。
如何将类型转换为 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 进行转换,不完全列表:
| 类型 | 实现 | |
|---|---|---|
String | Debug, Display | |
CString | Debug | |
OsString | Debug | |
PathBuf | Debug | |
Vec<u8> | Debug | |
&str | Debug, Display | |
&CStr | Debug | |
&OsStr | Debug | |
&Path | Debug | |
&[u8] | Debug | |
bool | Debug, Display | |
char | Debug, Display | |
u8 … i128 | Debug, Display | |
f32, f64 | Debug, 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 解释为参数标识符,以允许动态格式化。 |
type | DebugSTD (?) 格式化、十六进制 (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 的替代格式)。 |
基本的项目布局,以及 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
模块树和导入:
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),在大多数情况下你不需要担心这些种类。然而,在设计宏时,它们可能是一个因素。
一些值得了解的命令和工具。
| 命令 | 描述 |
|---|---|
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 插件。
🔘 检查目标是否受支持。
🔘 通过 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 编译
嵌入在源代码中,由工具或预处理器使用的特殊标记。
在声明式 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。? |
| 文档注释内 | 解释 |
|---|---|
```…``` | 包含一个文档测试(在 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)]1 | T | 使用另一种表示,而不是默认的 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 表示某些特殊情况。
! 表示在宏上。
* 表示在几乎任何项上。
允许用户自带类型并避免代码重复。
u8StringDevice| 类型 | 值 |
|---|---|
u8 | { 0u8, 1u8, …, 255u8 } |
char | { 'a', 'b', … '🦀' } |
struct S(u8, char) | { (0u8, 'a'), … (255u8, '🦀') } |
u8&u8&mut u8[u8; 1]Stringu8、 &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, … } |
0_i8 as u8let 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 { }u8impl { … }Stringimpl { … }Portimpl { … }impl Port {
fn f() { … }
}
impl Port {},与类型相关的行为:
Port::new(80)port.close()什么被认为是相关的,更多是哲学上的而非技术上的,没有什么(除了良好的品味)可以阻止
u8::play_sound()的发生。
trait T { }CopyCloneSizedShowHex| Copy Trait |
|---|
Self |
u8 |
u16 |
… |
| Clone Trait |
|---|
Self |
u8 |
String |
… |
| Sized Trait |
|---|
Self |
char |
Port |
… |
Self 指的是包含的类型。
trait ShowHex {
// 必须根据文档实现。
fn as_hex() -> String;
// 由 trait 作者提供。
fn print_hex() {}
}
Copytrait Copy { }
Copy 是一个标记 trait 的例子,表示内存可以按位复制。SizedSized 由编译器为具有已知大小的类型提供;要么是,要么不是impl T for S { }impl ShowHex for Port { … }
impl A for B 将类型 B 添加到 trait 成员列表中:| ShowHex Trait |
|---|
Self |
Port |
u8impl { … }SizedCloneCopyDeviceimpl { … }TransportPortimpl { … }SizedCloneShowHexEatVenisonEat
venison.eat()
接口
Eat。Venison 时,他必须决定 Venison 是否实现 Eat。Venison 时,Santa 可以利用 Eat 提供的行为:// Santa 导入 `Venison` 来创建它,如果他想,可以 `eat()`。
import food.Venison;
new Venison("rudolph").eat();
EatVenisonVenison+
Eatvenison.eat()
Trait
Eat。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 TS<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: XNum<T>→
Num<u8>Num<f32>Num<Cmplx>
u8AbsoluteDimMulPortCloneShowHexT 可以是任何类型,我们如何对这样的 Num<T> 进行推理(编写代码)?// 类型只能为某个 `T` 构造,如果该
// T 是 `Absolute` 成员列表的一部分。
struct Num<T> where T: Absolute {
…
}
| Absolute Trait |
|---|
Self |
u8 |
u16 |
… |
我们在这里为结构体添加约束。实际上,最好在各自的 impl 块中添加约束,详见本节后面。
where T: X + Yu8AbsoluteDimMulf32AbsoluteMulcharCmplxAbsoluteDimMulDirNameTwoDCarDirNamestruct S<T>
where
T: Absolute + Dim + Mul + DirName + TwoD
{ … }
+ 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<In> { type Out; } 注意有些 trait 可以“附加”多次,而另一些只能附加一次吗?
PortFrom<u8>From<u16>PortDereftype u8;为什么会这样?
trait From<I> {}trait Deref { type O; }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 | |
|---|---|
Self | I |
u16 | u8 |
u32 | u16 |
… | |
| Deref | |
|---|---|
Self | O |
Port | u8 |
String | str |
… | |
这里的关键是,
O 参数必须由输入参数 I 唯一确定,X Y 表示函数的方式相同),Self 算作一个输入。一个更复杂的例子:
trait Complex<I1, I2> {
type O1;
type O2;
}
Complex 的类型关系,Self 总是其中之一)和 2 个输出,并且它持有 (Self, I1, I2) => (O1, O2)| Complex | ||||
|---|---|---|---|---|
Self [I] | I1 | I2 | O1 | O2 |
Player | u8 | char | f32 | f32 |
EvilMonster | u16 | str | u8 | u8 |
EvilMonster | u16 | String | u8 | u8 |
NiceMonster | u16 | String | u8 | u8 |
NiceMonster🛑 | u16 | String | u8 | u16 |
(NiceMonster, u16, String) 已经
唯一确定了输出。
A<I>CarCarA<I>car.a(0_u8)
car.a(0_f32)
Btype O;CarCarBT = 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 | |
|---|---|
Self | I |
X | u32 |
Y | … |
T 来添加更多成员。
| B | |
|---|---|
Self | O |
Player | String |
X | u32 |
Self),实现者必须预先选择 O。
Queryvs.
Query<I>vs.
Querytype 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→
PostgreSQLQuerySledQueryTrait 作者假设:
输入参数
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>→
PostgreSQLQuery<&str>Query<String>SledQuery<T>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]);
Querytype O;→
PostgreSQLQueryO = String;SledQueryO = 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;→
PostgreSQLQuery<&str>O = String;Query<CString>O = CString;SledQuery<T>O = Vec<u8>;T is ToU8Slice.与上述示例类似,特别是 trait 作者假设:
I 类型应该具有该能力,MostTypesSizedvs.
ZSizedvs.
strSized[u8]Sizeddyn TraitSized…SizedT 占用的字节数,则 T 是 Sized STD,u8 和 &[u8] 是,[u8] 不是。Sized 意味着 impl Sized for T {} 成立。这是自动发生的,不能由用户实现。Sized 的类型称为动态大小类型 BK NOM REF (DSTs),有时也称为unsized。| 示例 | 解释 |
|---|---|
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 可能仍然失败,因为参数不能放在栈上。 |
?SizedS<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 行”,编译器会为你做这件事。🔗
点击展开示例。
你的 crate 和上游 crate 中类型和 trait 的视觉概览。
u8u16f32boolcharFileStringBuilderVec<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() {}PIdbg!CopyDereftype Tgt;From<T>From<T>From<T>SerializeTransportShowHexDeviceFrom<u8>StringSerializeStringFrom<u8>StringFrom<Port>PortFrom<u8>From<u16>ContainerDerefTgt = u8;DerefTgt = f32;TTTShowHextrait 和类型的示例,以及你可以为哪些类型实现哪些 trait。
当你有了 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 {} | *a | A 是携带 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
| A | B | 示例 | 解释 |
|---|---|---|---|
Pointer | Pointer | device_ptr as *const u8 | 如果 *A, *B 是 Sized。 |
Pointer | Integer | device_ptr as usize | |
Integer | Pointer | my_usize as *const Device | |
Number | Number | my_u8 as u16 | 通常有令人惊讶的行为。↑ |
enum (无字段) | Integer | E::A as u8 | |
bool | Integer | true as u8 | |
char | Integer | 'A' as u8 | |
&[T; N] | *const T | my_ref as *const u8 | |
fn(…) | Pointer | f as *const u8 | 如果 Pointer 是 Sized。 |
fn(…) | Integer | f 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
| A | B | 解释 |
|---|---|---|
&mut T | &T | 指针弱化。 |
&mut T | *mut T | - |
&T | *const T | - |
*mut T | *const T | - |
&T | &U | 解引用,如果 impl Deref<Target=U> for T。 |
T | U | 去大小化 (Unsizing),如果 impl CoerceUnsized<U> for T。2 🚧 |
T | V | 传递性,如果 T 强制转换为 U 且 U 强制转换为 V。 |
|x| x + x | fn(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)。 |
相比之下,这些不是🛑子类型化的例子:
| A | B | 解释 |
|---|---|---|
u16 | u8 | 🛑 显然无效;u16 永远不应自动成为 u8。 |
u8 | u16 | 🛑 设计上无效;具有不同数据的类型即使可以,也永远不会是子类型。 |
&'a mut u8 | &'a u8 | 🛑 特洛伊木马,不是子类型化;而是强制转换(仍然有效,但不是子类型化)。 |
fn f(x: A) -> B {
x
}
对于仅在生命周期上不同的类型,自动将 A 转换为 B NOM - 子类型化变性规则:
'a 超过较短的 'b,是 'b 的子类型。'static 是所有其他生命周期 'a 的子类型。&'a T)是否是彼此的子类型,使用以下变性表:| 构造1 | 'a | T | U |
|---|---|---|---|
&'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。
如果你习惯了 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 {} 内运行代码。 |
| 不要 Panic | Panic 不是异常,它们表明进程应立即中止! |
仅在编程错误时 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)通常是更好的主意。
在将微基准测试移植到 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) 运行结果。 话虽如此,有许多性能机会是性能分析器找不到的,但需要设计进去。
如果你熟悉 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 来清理/修复应用程序状态。
存在一个子 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。Unsound 导致 Undefined。Undefined 导致原力的黑暗面。
安全代码 (Safe Code)
/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)。if maybe_true() {
let r: &u8 = unsafe { &*ptr::null() }; // 一旦这行运行,整个应用程序都是未定义的。即使
} else { // 这行看似没做什么,应用程序现在也可能运行
println!("the spanish inquisition"); // 两个路径,破坏数据库,或做任何其他事情。
}
不健全代码 (Unsound Code)
unsafe 代码如果通过违反上述承诺而自行调用 UB,也是不健全的。fn unsound_ref<T>(x: &T) -> &u128 { // 对用户来说,签名看起来是安全的。碰巧
unsafe { mem::transmute(x) } // 如果用 &u128 调用就没问题,但对于几乎
} // 其他所有东西都是 UB。
负责任地使用 Unsafe 💬
对抗性代码是安全的第三方代码,它能编译但不遵循 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 方面的合作,那么泛型代码不可能是安全的。- 如果需要类型合作,你必须使用
unsafetrait(可能自己实现)。- 你必须考虑在意想不到的地方(例如,重新赋值、作用域结束)随机执行代码。
- 在最坏情况的 panic 之后,你可能仍然是可观察的。
作为推论,安全但致命的代码(例如
airplane_speed<T>())可能也应该遵循这些指南。
更新 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>)。 |
| 🟡 放松约束。 |
| 🟡 添加默认类型参数。 |
| 🟡 泛化为泛型。 |
| 函数中的签名 |
|---|
| 🔴 添加/移除参数。 |
| 🟡 引入一个新的类型参数。 |
| 🟡 泛化为泛型。 |
| 行为更改 |
|---|
| 🔴 / 🟡 更改语义可能不会导致编译器错误,但可能会使客户端做错事。 |
专业书籍,另请参见 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.rs | std 文档的快捷方式。 |
| stdrs.dev 🝖 | std 文档的快捷方式,包括编译器内部模块。 |
| docs.rs | 第三方库的文档,从源代码自动生成。 |
| releases.rs | 之前和即将发布的版本的发布说明。 |
想要这份 Rust 速查表的 PDF 吗?在这里下载最新的 PDF(A4)和Letter格式。或者,通过文件 > 打印然后“另存为 PDF”自行生成(在 Chrome 中效果很好,在 Firefox 中有些问题)。