1. 概述
状态
- 《Rust 编码规范》目前为 V 1.0 beta 试行版,改进内容参考 Changelog
详细
介绍
Rust 语言社区内其实分散着很多编码规范,下面罗列一部分公开信息:
- 官方|Rust API 编写指南
- 官方 | Rust Style Guide
- Rust's Unsafe Code Guidelines Reference
- 法国国家信息安全局 | Rust 安全(Security)规范
- Apache Teaclave 安全计算平台 | Rust 开发规范
- PingCAP | 编码风格指南(包括 Rust 和 Go 等)
- Google Fuchsia 操作系统 Rust 开发指南
- RustAnalyzer 编码风格指南
- 使用 Rust 设计优雅的 API
- Rust FFI 指南
上面这些除了 Rust 官方和法国国家信息安全局的编码规范之外,其他开源项目的编码规范主要是为了规范贡献者们遵循一个统一的编码风格。
所以,一个通用的,覆盖编码风格和具体编码实践的全面的编码规范,更有助于社区各个开源项目和各大公司参考去制定自己的编码规范。
本规范致力于成为统一的 Rust 编码规范,各大公司可以依赖本规范,结合自己的业务领域和团队习惯,形成自己的编码规范,并可以在日常实践中反哺本规范,让本规范更加完善。
1.1 为什么需要 Rust 编码规范
前言
在刚学 Rust 的时候,我赞叹于 Rust 提供的工具之先进性。比如 rustfmt,可以自动格式化代码,clippy 可以帮助你规范代码中写的不地道的地方。它们确实是非常优秀的工具。当时我也认为 Rust 根本不需要像其他语言那样制定编码规范。
但随着对 Rust 越来越深入了解的过程中,我也逐渐发现这些工具的很多不足之处,覆盖的并不全面。比如 rustfmt 配置和使用不当会导致代码错误,而且无法识别 Rust 代码中各种命名的语义;clippy存在一些误报或lint不合理,以及无法覆盖到 Unsafe Rust 等问题。开发者,尤其是新手们,如果长期像使用一个黑盒一样去依赖rustfmt和clippy,但并不去了解其lint背后的原因,只是知其然而无法知其所以然,那在代码质量有一定要求的前提下是无法提升开发效率的。
所以,rutfmt和clippy并不是万能的。我们还需要一个全面且通用的编码规范,并且也能覆盖到像 rustfmt 和 clippy 这样的工具,让广大 Rust 团队通过规范化的原则和规则去了解编写地道 Rust 代码的基本框架,就可以快速落地 Rust ,增强团队间的协作与信任。
Rustfmt 和 Clippy 的局限性
Rustfmt 局限性
Rust 有自动化格式化工具 rustfmt ,可以帮助开发者摆脱手工调整代码格式的工作,提升生产力。但它并不能代替编码规范对 Rust 代码的编码风格进行规范。
rustfmt主要存在以下缺陷:
- Rust 语言是一门非常注重语义的语言。Rust 中的变量、类型和函数等命名是非常讲究语义的,尤其是所有权语义。rustfmt 工具无法判断代码中命名的语义。这方面利用 Clippy 可以满足部分需求,但是对于开发者来说比较片面。
- Rustfmt 如果使用不当或配置不当,会导致问题。因为rustfmt是自动格式化工具,它会自动修改代码,但是它修改的时候并不会编译代码。如果开发者配置自动保存以后自动执行rustfmt,就会导致代码被修改错误,或者,有一些rustfmt 配置选项配置错误,也可能导致代码修改错误。
- rustfmt 工具中的配置项都比较零散,大部分开发者不会去了解其每一个配置项的含义。
- rustfmt 没有覆盖到代码注释和文档注释的编码规范。
综上所述,需要通过提供一个通用的编码规范,让开发者明确地从命名、格式和注释三方面整体上了解 Rust 遵循什么样的编码风格。其中会覆盖 rustfmt 的内容,但并不是机械地将 rustfmt 的规则都一一提取出来,而是对 rustfmt 的规则进行了统一的归类和梳理,方便开发者去理解 rustfmt 中制定的规则,方便团队去制定适合自己的代码风格。
Clippy 的局限性
Clippy是 Rust 的 linter,是 Rust 生态系统中的主要组件之一。它对已开发的代码执行额外的静态检查,报告发现的问题并解释如何修复它们(有时它甚至可以自动修复它们)。使用它能对 Rust 初学者甚至专业人士都带来好处。
但使用 Clippy 并不是意味着它能代替编码规范,它也存在很多缺陷:
- Clippy 缺乏很多 Unsafe Rust 相关的 lint 检测。Unsafe Rust 是 Rust 非常重要的一部分,需要一个完整的编码规范来覆盖,帮助开发者编写安全的 Unsafe 代码。
- Clippy 中的 lint 截止目前有 500多条,而且还有不断增长的趋势,开发者不可能一条条去了解每个 lint,所以需要一个编码规范帮助开发者对lint进行一个梳理归类。
- Clippy 中的lint 的建议和分级 (allow/warning/deny)有些争议。其中有些 lint 默认是 allow,但不代表在一些场景下,它就是合理的写法;同样,有些 lint 是 warning,但不代表在一些场景下是不合理的。为此,dtolnay 还特意创造了这个仓库:https://github.com/dtolnay/noisy-clippy ,用于分析社区中crate有多少 Clippy lint 的建议并不符合实际场景,从而达到改进 Clippy 的目的。
综上所述,Clippy 虽然是一个十分有用的工具,但它无法替代编码规范。
编码规范作用
Rust 编码规范的作用主要是如下方面:
- 遵循 Rust 语言特性,提高代码的可读性、可维护性、健壮性和可移植性。
- 提高 Unsafe Rust 代码编写的规范性和安全性。
- 编程规范条款力争系统化、易应用、易检查,帮助开发者提升开发效率。
- 给开发者一个明确的且全局的视野,在其开发代码的过程中就能遵循好的代码规范,而非等写完代码以后再通过rustfmt和clippy这类的工具,一条一条去修改warning。
- 规范不等于教程,但是开发人员水平参差不齐,对于一些因为知识盲点而可能导致程序错误的地方,规范也将覆盖到。
1.2 编码规范基本约定
编码规范的内容组织说明
编程规范绝不是为了增加开发者的负担而编写的,目的是为了帮助开发者写出高质量的 Rust 代码。
为了达成这个目的,规范条款分为原则和规则两个类别:原则 与 规则。
-
原则,就是编程开发时指导的一个大方向,或是指一类情况。也有少部分原则是 Rust 编译器可检测的情况,但是因为编译器诊断信息比较迷惑,所以增加了原则,帮助开发者去避免这类情况。
-
规则,相对原则来说,更加具体,包含正例和反例来进一步说明。有些规则也会增加例外的情况。规则基本都是可以通过 lint 进行检测的。
规则内容 与 rustfmt 和 clippy 的关系
规范主要分为两大部分内容:代码风格 和 代码实践。
代码风格
在代码风格中包含代码命名、格式和注释:
- 命名部分,主要是通过 clippy lint 来检查,有些命名规则 clippy lint未提供检测,则需要自定义lint来支持。
- 格式部分,主要用 rustfmt 来自动修改,编码规范中的规则对 rustfmt 的大部分配置项进行了分类描述,为了方便开发者进行参考,制定自己的配置项。编码规范中也提供了配置模版。
- 注释部分,其中包括普通注释和文档注释,规则条目通过 rustfmt 和 clippy 合作来进行规范。
代码实践
代码实践的内容是按照 Rust 语言特性进行分类,每个语言特性都尽量针对日常编码最佳实践进行总结,提取为一条条的原则和规则,方便开发者进行参考。其中大部分规则都是建议,涉及要求的规则基本都是和安全相关。
这部分内容的规则条目基本都依赖 Clippy lint 去检测,但并非是把 Clippy 500 多条 lint 都一一对应为规则。Clippy lint 中涉及很多技巧类的lint,就没有放到规范中。
规则主要是侧重于通用场景下,代码可读性、维护性、安全性、性能这四方面的考量,它仅仅覆盖一小部分(不到 1/5)clippy lint。另外还有一些规则是clippy lint没有的,需要自定义lint。
代码实践内容的重点在于 Unsafe Rust 的编码规范,其中编码原则多于规则,并且 Clippy lint 也很少能检测这部分内容。其中要求类的规则更多一些。
希望通过这部分内容,让开发者在编写 Rust 代码过程中,避开一些常见的坑。
编码规范内容约定
通过标题前的编号来标识:
- 标识为
P
为原则(Principle)。编号方式为P.Element.Number
。 - 标识为
G
为规则(Guideline)。编号方式为G.Element.Number
。 - 当有子目录时。编号方式为
P.Element.SubElement.Number
或G.Element.SubElement.Number
。
Number 从01
开始递增。其中 Element
为领域知识中关键元素(本规范中对应的二级目录)的三位英文字母缩略语。(术语参考: SEI CERT C Coding Standard)
Element | 解释 | Element | 解释 |
---|---|---|---|
NAM | 命名 (Naming) | CMT | 注释 (Comment) |
FMT | 格式 (Format) | TYP | 数据类型 (Data Type) |
CNS | 常量 (Const) | VAR | 变量 (Variables) |
EXP | 表达式 (Expression) | CTF | 控制流程 (Control Flow) |
REF | 引用 (Reference) | PTR | 指针 (Pointer) |
STR | 字符串 (String) | INT | 整数 (Integer) |
MOD | 模块 (Module) | CAR | 包管理 (Cargo) |
MEM | 内存 (Memory) | FUD | 函数设计 (Function Design) |
MAC | 宏 (Macro) | STV | 静态变量 (Static Variables) |
GEN | 泛型 (Generic) | TRA | 特质 (Trait) |
ASY | 异步 (Async) | UNS | 非安全 (Unsafe Rust) |
SAS | 安全抽象 (Safety Abstract) | FFI | 外部函数调用接口 ( Foreign Function Interface ) |
LAY | 内存布局 (Layout) | ERR | 错误处理 (Error Handle) |
CLT | 集合 (Collection) | MTH | 多线程 (Multi Threads) |
EMB | 嵌入式Rust (Embedded Rust) | FIO | 输入输出 (In/Out) |
SEC | 信息安全 (Security) | SPT | 智能指针 (Smart Pointer) |
UNT | 单元类型 (Unit) | BOL | 布尔 (Bool) |
CHR | 字符类型 (Char) | FLT | 浮点数 (Float) |
SLC | 切片类型 (Slice) | TUP | 元组 (Tuple) |
ARR | 固定长度数组类型 (Array) | VEC | 动态长度数组 (Vector) |
SCT | 结构体 (Struct) | ENM | 枚举体 (Enum) |
UNI | 联合体 (Union) | BLN | 标准库内置(BuiltIn) |
OBJ | Trait 对象 (Trait Object) | LFT | 生命周期 (Lifetime) |
BOX | Box<T> 类型 | DRP | 析构函数 (Drop) |
DCL | 声明宏 (Declarative) | PRO | 过程宏 (Procedural) |
LCK | 锁同步 (Lock) | LKF | 无锁 (Lock Free) |
CGN | 代码生成(Code Generation) | OTH | 其他 (Ohters) |
引用代码开源许可证说明
本规范中引用外部代码,均满足 MIT/Apache/Mozilla public licenses
开源许可证!
特别鸣谢
本指南参考《华为 C 语言编程指南 V 1.0》,感谢华为 开源能力中心 提供编程指南规范协助!
2. 代码风格
代码风格包含标识符的命名风格、排版与格式风格、注释风格等。一致的编码习惯与风格,可以提高代码可读性和可维护性。
2.1 命名
好的命名风格能让我们快速地了解某个名字代表的含义(类型、变量、函数、常量、宏等),甚至能凸显其在整个代码上下文中的语义。命名管理对提升代码的可读性和维护性相当重要。
P.NAM.01 同一个crate中标识符的命名规则应该使用统一的词序
【描述】
具体选择什么样的词序并不重要,但务必要保证同一个 crate 内词序的一致性。 若提供与标准库中相似功能的东西时,也要与标准库名称的词性顺序一致.
拿错误类型来举个例子:
当crate中类型名称都按照 动词-宾语-error 这样的顺序来命名错误类型时,如果要增加新的错误类型,则也需要按同样的词序来增加。
以下是来自标准库的处理错误的一些类型示例:
JoinPathsError
ParseBoolError
ParseCharError
ParseFloatError
ParseIntError
RecvTimeoutError
StripPrefixError
如果你想新增和标准库相似的错误类型,比如“解析地址错误”类型,为了保持词性一致,应该使用ParseAddrError
名称,而不是AddrParseError
。
说明:现在标准库文档中 net模块解析地址错误类型是
AddrParseError
,其实和标准库中大部分错误类型遵循的 "动-宾-Error" 词序没有保持一致,所以它是一个特例。
【反例】
#![allow(unused)] fn main() { // 不符合:与标准库错误类型词序 "动-宾-Error" 不一致,应该为 ParseAddrError struct AddrParseError {} }
【正例】
#![allow(unused)] fn main() { // 符合: 与标准库错误类型一致 struct ParseAddrError{} }
P.NAM.02 为 cargo feature 命名时不应含有无意义的占位词
【描述】
给 Cargo feature 命名时,不应带有无实际含义的的词语,比如使用abc
命名来替代 use-abc
或 with-abc
。
这条原则经常出现在对 Rust 标准库进行 可选依赖(optional-dependency) 配置的 crate 上。
并且 Cargo 要求 features 应该是相互叠加的,所以像 no-abc
这种负向的 feature 命名实际上并不正确。
【反例】
# In Cargo.toml
[features]
// 不符合
default = ["use-std"]
std = []
// 不符合
no-abc=[]
// In lib.rs
#![cfg_attr(not(feature = "use-std"), no_std)]
【正例】
最简洁且正确的做法是:
# In Cargo.toml
[features]
// 符合
default = ["std"]
std = []
// In lib.rs
#![cfg_attr(not(feature = "std"), no_std)]
P.NAM.03 标识符命名应该符合阅读习惯
【描述】
标识符的命名要清晰、明了,有明确含义,容易理解。符合英文阅读习惯的命名将明显提高代码可读性。
一些好的实践包括但不限于:
- 使用正确的英文单词并符合英文语法,不要使用拼音
- 仅使用常见或领域内通用的单词缩写
- 布尔型变量或函数避免使用否定形式,双重否定不利于理解
- 不要使用 Unicode 标识符
【反例】
#![allow(unused)] fn main() { // 不符合: 使用拼音 let ming: &str = "John"; let xing: &str = "Smith"; // 不符合: 含义不明确 const ERROR_NO_1: u32 = 336; const ERROR_NO_2: u32 = 594; // 不符合:函数名字表示的函数作用不明了 fn not_number(s:&str) -> bool {/* ... */} }
【正例】
#![allow(unused)] fn main() { // 符合 let first_name: &str = "John"; let last_name: &str = "Smith"; const ERR_DIR_NOT_SUPPORTED: u32 = 336; const ERR_DVER_CANCEL_TIMEOUT: u32 = 594; // 符合 fn is_number(s:&str) -> bool {/* ... */} }
P.NAM.04 作用域越大命名越精确,反之应简短
【描述】
- 对于全局函数、全局变量、宏、类型名、枚举命名,应当精确描述并全局唯一。
- 对于函数局部变量,或者结构体、枚举中的成员变量,在其命名能够准确表达含义的前提下,应该尽量简短,避免冗余信息重复描述。
【反例】
#![allow(unused)] fn main() { // 不符合:描述不精确 static GET_COUNT: i32 = 42; // 不符合:信息冗余 enum WebEvent { PageLoadEvent, PageUnloadEvent, KeyPressEvent(char), PasteEvent(String), ClickEvent { x: i64, y: i64 }, } // 不符合:信息冗余 type MaskSize = u16; pub struct HeaderMap { mask: MaskSize, } }
【正例】
#![allow(unused)] fn main() { // 符合 static MAX_THREAD_COUNT: i32 = 42; // 符合: 上下文信息已经知道它是 Event enum WebEvent { PageLoad, PageUnload, KeyPress(char), Paste(String), Click { x: i64, y: i64 }, } // 符合:在使用它的地方自然就知道是描述谁的大小 type Size = u16; pub struct HeaderMap { mask: Size, } }
P.NAM.05 用于访问或获取数据的 getter
类方法通常不要使用 get_
前缀
【描述】
因为 Rust 所有权语义的存在,此例子中两个方法的参数分别是共享引用 &self
和 独占引用 &mut self
,分别代表了 getter 的语义。
也存在一些例外情况可以用 get_
前缀。
【反例】
#![allow(unused)] fn main() { pub struct First; pub struct Second; pub struct S { first: First, second: Second, } impl S { // 不符合:访问成员函数名字不用get_前缀。 pub fn get_first(&self) -> &First { &self.first } // 不符合: // 同样不建议 `get_mut_first`, or `mut_first`. pub fn get_first_mut(&mut self) -> &mut First { &mut self.first } // set_前缀是可以的 pub fn set_first(&mut self, f: First) -> &mut First { self.first = f; } } }
【正例】
#![allow(unused)] fn main() { pub struct First; pub struct Second; pub struct S { first: First, second: Second, } impl S { // 符合 pub fn first(&self) -> &First { &self.first } // 符合 pub fn first_mut(&mut self) -> &mut First { &mut self.first } // set_前缀是可以的 pub fn set_first(&mut self, f: First) { self.first = f; } } }
【例外】
但也存在例外情况:只有当需要显式的语义来通过getter
获取某种数据,才会使用get
命名。例如,Cell::get
可以访问一个Cell
的内容。
对于进行运行时验证的getter,例如边界检查,可以考虑添加一个 Unsafe 的_unchecked
配套方法。一般来说,会有以下签名。
#![allow(unused)] fn main() { // 进行一些运行时验证,例如边界检查 fn get(&self, index: K) -> Option<&V>; fn get_mut(&mut self, index: K) -> Option<&mut V>; // 没有运行时验证,用于在某些情况下提升性能。比如,在当前运行环境中不可能发生越界的情况。 unsafe fn get_unchecked(&self, index: K) -> &V; unsafe fn get_unchecked_mut(&mut self, index: K) -> &mut V; }
getter 和类型转换 (G.NAM.02) 之间的区别很小,大部分时候不那么清晰可辨。比如 TempDir::path
可以被理解为临时目录的文件系统路径的 getter ,而 TempDir::into_path
负责把删除临时目录时转换的数据传给调用者。
因为 path
方法是一个 getter ,如果用 get_path
或者 as_path
会造成信息冗余。
来自标准库的例子:
std::io::Cursor::get_mut
std::ptr::Unique::get_mut
std::sync::PoisonError::get_mut
std::sync::atomic::AtomicBool::get_mut
std::collections::hash_map::OccupiedEntry::get_mut
<[T]>::get_unchecked
P.NAM.06 遵循 iter/ iter_mut/ into_iter
规范来生成迭代器
【描述】
此规则包含两条基本子规则:
- 对于容纳
U
类型的容器 (container) ,其迭代器方法应该遵循iter/ iter_mut/ into_iter
这三种命名方式。 - 返回的迭代器类型名称也应该和其方法名保持一致,如一个叫做
into_iter()
的方法应该返回一个叫做IntoIter
的类型。
说明:
- 规则一适用于在概念上属于同质集合的数据结构的方法,而非函数。例如,第三方库
url
中的 percent_encode 返回一个 URL 编码的字符串片段的迭代器,使用iter/iter_mut/into_iter
约定的话,函数名就会失去明确的语义。 - 规则二同样主要适用于方法,但通常对函数也有意义。例如,第三方库
url
中的 percent_encode 返回一个PercentEncode
类型的迭代器。
【反例】
// 不符合:没必要加 `to_` 前缀
fn to_iter(&self) -> Iter // Iter 实现 Iterator<Item = &U>
fn to_iter_mut(&mut self) -> IterMut // IterMut 实现 Iterator<Item = &mut U>
fn to_into_iter(self) -> IntoIter // IntoIter 实现 Iterator<Item = U>
【正例】
// 符合
fn iter(&self) -> Iter // Iter 实现 Iterator<Item = &U>
fn iter_mut(&mut self) -> IterMut // IterMut 实现 Iterator<Item = &mut U>
fn into_iter(self) -> IntoIter // IntoIter 实现 Iterator<Item = U>
【例外】
标准库中存在一个例外: str
类型是有效 UTF-8 字节的切片(slice),概念上与同质集合略有差别,所以 str
没有提供 iter
/iter_mut
/into_iter
命名的迭代器方法,而是提供 str::bytes
方法来输出字节迭代器、 str::chars
方法来输出字符迭代器。
【参考】
参考 [RFC 199]: https://github.com/rust-lang/rfcs/blob/master/text/0199-ownership-variants.md
还有有一些来自标准库的例子可参考:
Vec::iter
Vec::iter_mut
Vec::into_iter
BTreeMap::iter
BTreeMap::iter_mut
BTreeMap::keys
返回 [Keys
]btree_map::KeysBTreeMap::values
返回 [Values
]btree_map::Values
P.NAM.07 避免使用语言内置保留字、关键字、内置类型和trait
等特殊名称
【描述】
命名必须要避免使用语言内置的保留字、关键字、内置类型和trait
等特殊名称。 具体可以参考The Rust Reference-Keywords。
【反例】
// 不符合:Rust 内置了 Sized trait type Sized = u16; fn main() { // 不符合:try 为保留关键字 let try = 1; }
【正例】
// 符合 type Size = u16; fn main() { // 符合 let tried = 1; }
【例外】
在一些特定场合,比如对接遗留数据库中的字段和Rust关键字冲突:
#![allow(unused)] fn main() { struct SomeTable{ // 使用 `r#`+type 来解决这种问题 r#type: String } }
或者当序列化为 json 或 proto 时,存在成员为关键字,则可以通过相关库提供的功能来使用:
#![allow(unused)] fn main() { pub struct UserRepr { // ... #[serde(rename="self")] pub self_: Option<String>, // ... } }
P.NAM.08 避免在变量的命名中添加类型标识
【描述】
因为 Rust 语言类型系统崇尚显式的哲学,所以不需要在变量命名中也添加关于类型的标识。
【反例】
#![allow(unused)] fn main() { let account_bytes: Vec<u8> = read_some_input(); // 不符合:account 的类型很清楚,没必要在命名中加 `_bytes` let account_str = String::from_utf8(account_bytes)?; // 不符合:account 的类型很清楚,没必要在命名中加 `_str` let account: Account = account_str.parse()?; // 不符合:account 的类型很清楚,没必要在命名中加 `_str` }
【正例】
#![allow(unused)] fn main() { let account: Vec<u8> = read_some_input(); // 符合 let account = String::from_utf8(account)?; // 符合 let account: Account = account.parse()?; // 符合 }
P.NAM.09 定义全局静态变量时需加前缀G_
以便和常量有所区分
【描述】
为了提升代码可读性和可维护性,有必要将常量的命名和全局静态变量加以区分。所以在定义全局静态变量时,需要以前缀G_
命名。
【反例】
#![allow(unused)] fn main() { // 不符合: 无法通过命名直接区分常量和静态变量 static EVENT: [i32;5]=[1,2,3,4,5]; const MAGIC_NUM: i32 = 65 ; }
【正例】
#![allow(unused)] fn main() { // 符合 static G_EVENT: [i32;5]=[1,2,3,4,5]; const MAGIC_NUM: i32 = 65 ; }
G.NAM.01 使用统一的命名风格
【级别】 要求
【描述】
Rust 倾向于在“类型”级的结构中使用驼峰( UpperCamelCase
) 命名风格,在 “变量、值(实例)、函数名”等结构中使用蛇形( snake_case
)命名风格。
下面是汇总信息:
Item | 规范 |
---|---|
包(Crates) | 通常使用 snake_case 1 |
模块(Modules) | snake_case |
类型(Types) | UpperCamelCase |
特质(Traits) | UpperCamelCase |
枚举体(Enum variants) | UpperCamelCase |
函数(Functions) | snake_case |
方法(Methods) | snake_case |
通用构造函数(General constructors) | new 或者 with_more_details |
转换构造函数(Conversion constructors) | from_some_other_type |
宏(Macros) | snake_case! |
本地变量(Local variables) | snake_case |
静态变量(Statics) | SCREAMING_SNAKE_CASE |
常量(Constants) | SCREAMING_SNAKE_CASE |
类型参数(Type parameters) | 简明的 UpperCamelCase ,通常使用单个大写字母: T |
生存期(Lifetimes) | 简短的 lowercase ,通常使用单个小写字母 'a , 'de , 'src ,尽量保持语义 |
特性(Features) | snake_case |
说明 :
- 在
UpperCamelCase
情况下,由首字母缩写组成的缩略语和 复合词的缩写,算作单个词。比如,应该使用Uuid
而非UUID
,使用Usize
而不是USize
,或者是Stdin
而不是StdIn
。 - 在
snake_case
中,首字母缩写和缩略词是小写的is_xid_start
。 - 在
snake_case
或者SCREAMING_SNAKE_CASE
情况下,每个词不应该由单个字母组成——除非这个字母是最后一个词。比如,使用btree_map
而不使用b_tree_map
,使用PI_2
而不使用PI2
。
关于包命名:
- 由于历史问题,包名有两种形式
snake_case
或kebab-case
,但实际在代码中需要引入包名的时候,Rust 只能识别snake_case
,也会自动将kebab-case
识别为kebab_case
。所以建议使用snake_case
。 - Crate 的名称通常不应该使用
-rs
或者-rust
作为后缀或者前缀。但是有些情况下,比如是其他语言移植的同名 Rust 实现,则可以使用-rs
后缀来表明这是 Rust 实现的版本。
【参考】
Rust 命名规范在 RFC 0430 中有也描述。
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group |
---|---|---|---|
Rustc: non_camel_case_types | no | yes | Style |
Rustc: non_snake_case | no | yes | Style |
G.NAM.02 类型转换函数命名需要遵循所有权语义
【级别】 建议
【描述】
进行特定类型转换的方法名应该包含以下前缀:
名称前缀 | 内存代价 | 所有权 |
---|---|---|
as_ | 无代价 | borrowed -> borrowed |
to_ | 代价昂贵 | borrowed -> borrowed borrowed -> owned (非 Copy 类型) owned -> owned (Copy 类型) |
into_ | 看情况 | owned -> owned (非 Copy 类型) |
以 as_
和 into_
作为前缀的类型转换通常是 降低抽象层次 ,要么是查看背后的数据 ( as
) ,要么是分解 (deconstructe) 背后的数据 ( into
) 。
相对来说,以 to_
作为前缀的类型转换处于同一个抽象层次,但是底层会做更多工作,比如多了内存拷贝等操作。
当一个类型用更高级别的语义 (higher-level semantics) 封装 (wraps) 一个内部类型时,应该使用 into_inner()
方法名来取出被封装类型的值。
这适用于以下封装器:
读取缓存 (BufReader
) 、编码或解码 (GzDecoder
) 、取出原子 (AtomicBool
、
或者任何相似的语义封装 (BufWriter
)。
【正例】
标准库 API 命名有如下示例:
as_
str::as_bytes()
用于查看 UTF-8 字节的str
切片,这是无内存代价的(不会产生内存分配)。 传入值是&str
类型,输出值是&[u8]
类型。
to_
Path::to_str
对操作系统路径进行 UTF-8 字节检查,开销昂贵。 虽然输入和输出都是借用,但是这个方法对运行时产生不容忽视的代价, 所以不应使用as_str
名称。str::to_lowercase()
生成正确的 Unicode 小写字符, 涉及遍历字符串的字符,可能需要分配内存。 输入值是&str
类型,输出值是String
类型。f64::to_radians()
把浮点数的角度制转换成弧度制。 输入和输出都是f64
。没必要传入&f64
,因为复制f64
花销很小。 但是使用into_radians
名称就会具有误导性,因为输入数据没有被消耗。
into_
String::into_bytes()
从String
提取出背后的Vec<u8>
数据,这是无代价的。 它转移了String
的所有权,然后返回具有所有权的Vec<u8>
。BufReader::into_inner()
转移了 buffered reader 的所有权,取出其背后的 reader ,这是无代价的。 存于缓冲区的数据被丢弃了。BufWriter::into_inner()
转移了 buffered writer 的所有权,取出其背后的 writer ,这可能以很大的代价刷新所有缓存数据。
如果类型转换方法返回的类型具有 mut
修饰,那么这个方法的名称应如同返回类型组成部分的顺序那样,带有 mut
。
比如 Vec::as_mut_slice
返回 &mut [T]
类型,这个方法的功能正如其名称所述,所以这个名称优于 as_slice_mut
。
其他参考示例:
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | Lint Level |
---|---|---|---|---|
wrong_self_convention | yes | no | Style | warn |
2.2 格式
制定统一的编码风格,是为了提升代码的可读性,让日常代码维护和团队之间审查代码更加方便。
Rust 有自动化格式化工具 rustfmt ,可以帮助开发者摆脱手工调整代码格式的工作,提升生产力。但是,rustfmt 遵循什么样的风格规范,作为开发者需要了解,在编写代码的时候可以主动按这样的风格编写。
说明:
对于 rustfmt
中未稳定的配置项(Stable
为No
),则表示该配置项不能在稳定版(Stable)Rust 中更改配置,但其默认值会在cargo fmt
时生效。在 Nightly Rust 下则都可以自定义配置。
如需了解在稳定版 Rust 中使用未稳定配置项的方法、配置示例及其他全局配置项说明,请参阅:Rustfmt 配置相关说明 。
【注意事项】
因为 rustfmt 工具会自动修改代码,为了确保 rustfmt 不会因为意外而改错代码,所以在使用 rustfmt 时应该注意下面两项描述:
- 务必保证在全部把代码修改完毕且编译通过之后再执行 rustfmt 命令。 因为 rustfmt 执行过程中不会对代码进行编译,所以就不会有静态检查保护。
- 如果是使用 IDE 或 编辑器的时候开启了自动保护功能,就不要开启自动执行 rustfmt 功能。
P.FMT.01 使用 rustfmt 进行自动格式化代码
【描述】
应该总是在项目中添加 rustfmt.toml
或 .rustfmt.toml
文件。即使它是空文件,这是向潜在的合作者表明你希望代码是自动格式化的。
【例外】
在特殊的情况下,可以通过条件编译属性 #[cfg_attr(rustfmt, rustfmt_skip)]
或 #[rustfmt::skip]
来关闭自动格式化。
比如下面示例:
vec!
中的元素排布是固定格式,这样有助于开发的便利。
fn main() { let got = vec![ 0x00, 0x05, 0x01, 0x00, 0xff, 0x00, 0x00, 0x01, 0x0c, 0x02, 0x00, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, b'd', b'e', b'a', b'd', b'b', b'e', b'e', b'f', 0x00, 0x00, 127, 0x06, 0x03, 0x00, 0x01, 0x02, b'a', b'b', b'c', b'd', 0x00, b'1', b'2', b'3', b'4', 0x00, 0x00, ]; }
如果使用 自动格式化,会变成:
fn main() { let got = vec![ 0x00, 0x05, 0x01, 0x00, 0xff, 0x00, 0x00, 0x01, 0x0c, 0x02, 0x00, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, b'd', b'e', b'a', b'd', b'b', b'e', b'e', b'f', 0x00, 0x00, 127, 0x06, 0x03, 0x00, 0x01, 0x02, b'a', b'b', b'c', b'd', 0x00, b'1', b'2', b'3', b'4', 0x00, 0x00, ]; }
但是加上 #[rustfmt::skip]
就不会被自动格式化影响:
fn main() { #[rustfmt::skip] let got = vec![ 0x00, 0x05, 0x01, 0x00, 0xff, 0x00, 0x00, 0x01, 0x0c, 0x02, 0x00, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, b'd', b'e', b'a', b'd', b'b', b'e', b'e', b'f', 0x00, 0x00, 127, 0x06, 0x03, 0x00, 0x01, 0x02, b'a', b'b', b'c', b'd', 0x00, b'1', b'2', b'3', b'4', 0x00, 0x00, ]; }
P.FMT.02 缩进使用空格而非制表符
【描述】
缩进要使用四个空格,不要使用制表符(\t
)代替。可以通过 IDE 或编辑器把缩进设置为四个空格。
【rustfmt 配置】
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
tab_spaces | 4 | yes(默认) | 缩进空格数| |
hard_tabs | false | yes(默认) | 禁止使用tab缩进| |
P.FMT.03 行间距最大宽度空一行
【描述】
代码行之间,最小间隔 0
行,最大间隔1
行。
【反例】
#![allow(unused)] fn main() { fn foo() { println!("a"); } // 不符合:空两行 // 不符合:空两行 fn bar() { println!("b"); // 不符合:空两行 // 不符合:空两行 println!("c"); } }
【正例】
#![allow(unused)] fn main() { fn foo() { println!("a"); } // 符合:空一行 fn bar() { println!("b"); println!("c"); } }
或者
#![allow(unused)] fn main() { fn foo() { println!("a"); } fn bar() { println!("b"); // 符合:空一行 println!("c"); } }
【rustfmt 配置】
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
blank_lines_lower_bound | 0(默认) | No | 不空行 |
blank_lines_upper_bound | 1(默认) | No | 最大空一行 |
P.FMT.04 语言项(Item) 定义时左花括号(brace)位置应该与语言项保持同一行
【描述】
为了保持代码结构的良好可读性,Rust 中定义各种语言项,包括控制结构(if / match
等)、函数、结构体、枚举等,要求左花括号与其定义保持同一行。
但是如果携带 where
语句,则要求换行,并且where
子句和 where
关键字不在同一行。
rustfmt
提供三个配置项对不同的语言项进行格式化:
brace_style
配置项对应于大部分语言项,包括函数、结构体等,但是控制结构(if / match
等)除外,默认值为SameLineWhere
,代表左花括号与语言项定义保持同一行。where_single_line
配置项对应于where
语句,默认值是false
,表示where
语句的花括号是换行。control_brace_style
配置项对应于控制结构(if / match
等),默认值为AlwaysSameLine
,表示左花括号与语言项定义保持同一行。
所以,只需要使用 rustfmt
默认配置即可。
【反例】
如果设置 brace_style = "AlwaysNextLine"
,则不符合。
#![allow(unused)] fn main() { // 不符合: 左花括号与函数语言项定义未保持同一行 fn lorem() { // body } }
如果设置 brace_style = "PreferSameLine"
,则符合:
#![allow(unused)] fn main() { // 不符合: 左花括号与 where 语句 应该换行 fn lorem<T>(ipsum: T) where T: Add + Sub + Mul + Div, { // 注意这里和 `SameLineWhere`的区别 // body } }
结构体与枚举:
如果设置 brace_style = "AlwaysNextLine"
,则不符合:
#![allow(unused)] fn main() { // 不符合: 左花括号与结构体语言项定义未保持同一行 struct Lorem { ipsum: bool, } }
如果设置 brace_style = "PreferSameLine"
,则符合:
#![allow(unused)] fn main() { // 不符合: 左花括号与 where 语句应该换行 struct Dolor<T> where T: Eq, { sit: T, } }
流程控制倾向于默认使用 AlwaysSameLine
,即,总在同一行。因为流程控制没有where
子句。
如果设置 brace_style = "AlwaysNextLine"
,则不符合:
fn main() { // 不符合: 左花括号与控制结构未保持同一行 if lorem { println!("ipsum!"); } else { println!("dolor!"); } }
【正例】
函数:
#![allow(unused)] fn main() { // 符合: 左花括号和 函数语言项定义在同一行 fn lorem() { // body } fn lorem<T>(ipsum: T) where // 符合:`where` 子句和 `where` 关键字不在同一行 T: Add + Sub + Mul + Div, { // 符合:当有 `where` 子句的时候,花括号换行 // body } }
结构体与枚举
#![allow(unused)] fn main() { // 符合 struct Lorem { ipsum: bool, } // 符合 struct Dolor<T> where T: Eq, { sit: T, } }
流程控制倾向于默认使用 AlwaysSameLine
,即,总在同一行。因为流程控制没有where
子句。
// 符合 // "AlwaysSameLine" (default) fn main() { if lorem { println!("ipsum!"); } else { println!("dolor!"); } }
【rustfmt 配置】
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
brace_style | SameLineWhere (默认) | No | 应该与语言项保持同一行,但是 where 语句例外 |
brace_style | AlwaysNextLine | No | 应该在语言项的下一行 |
brace_style | PreferSameLine | No | 总是优先与语言项保持同一行,where 语句也不例外 |
where_single_line | false(默认) | No | 强制将 where 子句放在同一行上 |
control_brace_style in control-flow | AlwaysSameLine (默认) | No | 总在同一行上,用于控制流程中默认值 |
control_brace_style in control-flow | ClosingNextLine | No | 用于控制流程中 else 分支在 if 分支结尾处换行 |
P.FMT.05 存在多个标识符时应该保持块状(Block)缩进
【描述】
当在表达式或语言项定义中出现多个标识符,则应该让其保持块状风格缩进。
【反例】
fn main() { // 不符合:缩进不符合标准,只是为了对齐 let lorem = vec!["ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit"]; }
流程控制
fn main() { // 不符合:缩进不符合标准,只是为了对齐 if lorem_ipsum && dolor_sit // 注意:这里缩进只是三个空格,仅仅是和前一行 `lorem_ipsum`对齐 && amet_consectetur && lorem_sit && dolor_consectetur && amet_ipsum && lorem_consectetur { // ... } }
函数参数
#![allow(unused)] fn main() { fn lorem() {} fn lorem(ipsum: usize) {} // 不符合 fn lorem(ipsum: usize, dolor: usize, sit: usize, amet: usize, consectetur: usize, adipiscing: usize, elit: usize) { // body } }
函数调用
fn main() { // 不符合 lorem("lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit"); }
泛型
#![allow(unused)] fn main() { // 不符合 fn lorem<Ipsum: Eq = usize, Dolor: Eq = usize, Sit: Eq = usize, Amet: Eq = usize, Adipiscing: Eq = usize, Consectetur: Eq = usize, Elit: Eq = usize>( ipsum: Ipsum, dolor: Dolor, sit: Sit, amet: Amet, adipiscing: Adipiscing, consectetur: Consectetur, elit: Elit) -> T { // body } }
结构体
fn main() { let lorem = Lorem { ipsum: dolor, sit: amet }; }
【正例】
数组:
fn main() { // 符合: 缩进四个空格 let lorem = vec![ "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit", ]; }
流程控制
fn main() { // 符合: 缩进四个空格 if lorem_ipsum && dolor_sit && amet_consectetur && lorem_sit && dolor_consectetur && amet_ipsum && lorem_consectetur { // ... } }
函数参数
#![allow(unused)] fn main() { fn lorem() {} fn lorem(ipsum: usize) {} // 符合: 缩进四个空格 fn lorem( ipsum: usize, dolor: usize, sit: usize, amet: usize, consectetur: usize, adipiscing: usize, elit: usize, ) { // body } }
函数调用
fn main() { // 符合: 缩进四个空格 lorem( "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit", ); }
泛型
#![allow(unused)] fn main() { // 符合: 缩进四个空格 fn lorem< Ipsum: Eq = usize, Dolor: Eq = usize, Sit: Eq = usize, Amet: Eq = usize, Adipiscing: Eq = usize, Consectetur: Eq = usize, Elit: Eq = usize, >( ipsum: Ipsum, dolor: Dolor, sit: Sit, amet: Amet, adipiscing: Adipiscing, consectetur: Consectetur, elit: Elit, ) -> T { // body } }
结构体
fn main() { let lorem = Lorem { ipsum: dolor, sit: amet, }; }
【rustfmt 配置】
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
indent_style | Block(默认) | No | 多个标识符定义保持块状风格,缩进符合标准 |
indent_style | Visual | No | 多个标识符定义保持对齐风格,但不符合缩进标准 |
P.FMT.06 当有多行表达式操作时,操作符应该置于行首
【描述】
当有多行表达式操作时,操作符应该置于行首,这样有利于代码的可读性和可维护性。
【反例】
操作符置于行尾
fn main() { // 不符合 let or = foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo || barbarbarbarbarbarbarbarbarbarbarbarbarbarbarbar; // 不符合 let sum = 123456789012345678901234567890 + 123456789012345678901234567890 + 123456789012345678901234567890; // 不符合 let range = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.. bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb; }
【正例】
操作符置于行首
fn main() { // 符合 let or = foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo || barbarbarbarbarbarbarbarbarbarbarbarbarbarbarbar; // 符合 let sum = 123456789012345678901234567890 + 123456789012345678901234567890 + 123456789012345678901234567890; // 符合 let range = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ..bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb; }
【rustfmt 配置】
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
binop_separator | Front(默认) | No | 换行后,操作符置于行首 |
P.FMT.07 枚举变体和结构体字段都应左对齐
【描述】
对于自定义了判别式的枚举体,和有字段的结构体而言,默认只需要左对齐就可以。
这个宽度可以设置为任意值,但默认是0
。
说明:此宽度并不是指插入多少空格,而是指需要对齐的字符长度。
【反例】
当 enum_discrim_align_threshold = 20
时。
#![allow(unused)] fn main() { // 不符合: 设置了变体长度最大是20 enum Foo { A = 0, Bb = 1, RandomLongVariantGoesHere = 10, // 注意,该变体长度已经超过了20,所以它不会被对齐 Ccc = 2, } enum Bar { VeryLongVariantNameHereA = 0, // 注意,该变体长度已经超过了20,所以它不会被对齐 VeryLongVariantNameHereBb = 1, // 注意,该变体长度已经超过了20,所以它不会被对齐 VeryLongVariantNameHereCcc = 2,// 注意,该变体长度已经超过了20,所以它不会被对齐 } }
当 enum_discrim_align_threshold = 50
时。
#![allow(unused)] fn main() { // 不符合: 因为通过更改配置值填充了空格 enum Foo { A = 0, Bb = 1, RandomLongVariantGoesHere = 10, // 注意,该变体长度未超过50,所以它会被对齐 Ccc = 2, } enum Bar { VeryLongVariantNameHereA = 0, // 注意,该变体长度未超过50,所以它会被对齐 VeryLongVariantNameHereBb = 1, // 注意,该变体长度未超过50,所以它会被对齐 VeryLongVariantNameHereCcc = 2, // 注意,该变体长度未超过50,所以它会被对齐 } }
【正例】
#![allow(unused)] fn main() { // 符合: 无论变体长度多长,都左对齐 enum Bar { A = 0, Bb = 1, RandomLongVariantGoesHere = 10, Ccc = 71, } // 符合 enum Bar { VeryLongVariantNameHereA = 0, VeryLongVariantNameHereBb = 1, VeryLongVariantNameHereCcc = 2, } }
【rustfmt 配置】
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
enum_discrim_align_threshold | 0(默认) | No | 具有判别式的枚举变体与其他变体进行垂直对齐的最大长度 |
struct_field_align_threshold | 0(默认) | No | 结构体字段垂直对齐的最大长度 |
P.FMT.08 函数参数超过五个或导入模块个数超过四个需换行
【描述】
- 五个以内函数参数可以置于一行,超过五个则使用「块」状缩进。
- 导入模块每行超过四个,则换行。
【反例】
当 rustfmt 配置型 fn_args_layout
和 imports_layout
未使用默认值:
#![allow(unused)] fn main() { trait Lorem { fn lorem(ipsum: Ipsum, dolor: Dolor, sit: Sit, amet: Amet); fn lorem(ipsum: Ipsum, dolor: Dolor, sit: Sit, amet: Amet) { // body } // 不符合: 超过五个参数未使用块状缩进 fn lorem( ipsum: Ipsum, dolor: Dolor, sit: Sit, amet: Amet, consectetur: Consectetur, adipiscing: Adipiscing, elit: Elit, ); fn lorem( ipsum: Ipsum, dolor: Dolor, sit: Sit, amet: Amet, consectetur: Consectetur, adipiscing: Adipiscing, elit: Elit, ) { // body } } use foo::{xxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyy, zzzzzzzzzzzzzzzzzz}; // 不符合: 模块换行即可,无需使用块状缩进 use foo::{ aaaaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbbbbb, cccccccccccccccccc, dddddddddddddddddd, eeeeeeeeeeeeeeeeee, ffffffffffffffffff, }; }
【正例】
当 rustfmt 配置项 fn_args_layout
和 imports_layout
使用默认值时:
#![allow(unused)] fn main() { trait Lorem { fn lorem(ipsum: Ipsum, dolor: Dolor, sit: Sit, amet: Amet, consectetur: Consectetur); fn lorem(ipsum: Ipsum, dolor: Dolor, sit: Sit, amet: Amet) { // body } // 符合 fn lorem( ipsum: Ipsum, dolor: Dolor, sit: Sit, amet: Amet, consectetur: Consectetur, adipiscing: Adipiscing, elit: Elit, ); // 符合 fn lorem( ipsum: Ipsum, dolor: Dolor, sit: Sit, amet: Amet, consectetur: Consectetur, adipiscing: Adipiscing, elit: Elit, ) { // body } } use foo::{xxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyy, zzzzzzzzzzzzzzzzzz}; // 符合 use foo::{ aaaaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbbbbb, cccccccccccccccccc, dddddddddddddddddd, eeeeeeeeeeeeeeeeee, }; }
【rustfmt 配置】
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
fn_args_layout | Tall(默认) | Yes | 函数参数五个或以内可以一行,超过五个则使用块状缩进 |
imports_layout | Mixed(默认) | No | 导入模块每行超过四个则换行 |
P.FMT.09 不同的场景,使用不同的空格风格
【描述】
- 在冒号之后添加空格,在冒号之前不要加空格。
- 在范围(range)操作符(
..
和..=
)前后不要使用空格。 - 在
+
或=
操作符前后要加空格。
【反例】
#![allow(unused)] fn main() { // 不符合: 冒号之后未加空格 // 当 `space_after_colon=false` fn lorem<T:Eq>(t:T) { let lorem:Dolor = Lorem { ipsum:dolor, sit:amet, }; } // 不符合: 冒号之前加空格 // 当 `space_before_colon=true` fn lorem<T : Eq>(t : T) { let lorem : Dolor = Lorem { ipsum : dolor, sit : amet, }; } // 不符合: `..`前后加空格 // 当 `spaces_around_ranges=true` let lorem = 0 .. 10; let ipsum = 0 ..= 10; // 不符合: `+`和`=`前后加空格 // 当 `type_punctuation_density="Compressed"` fn lorem<Ipsum: Dolor+Sit=Amet>() { // body let answer = 1 + 2; } }
【正例】
#![allow(unused)] fn main() { // 符合 // 当 `space_after_colon=true` fn lorem<T: Eq>(t: T) { let lorem: Dolor = Lorem { ipsum: dolor, sit: amet, }; } // 符合 // 当 `space_before_colon=false` fn lorem<T: Eq>(t: T) { let lorem: Dolor = Lorem { ipsum: dolor, sit: amet, }; } // 符合 // 当 `spaces_around_ranges=false` let lorem = 0..10; let ipsum = 0..=10; // 符合 // 当 `type_punctuation_density="Wide"` fn lorem<Ipsum: Dolor + Sit = Amet>() { // body let answer = 1 + 2; } }
【rustfmt 配置】
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
space_after_colon | true(默认) | No | 在冒号后面要加空格 |
space_before_colon | false(默认) | No | 在冒号前面不要加空格 |
spaces_around_ranges | false(默认) | No | 在.. 和..= 范围操作符前后不要加空格 |
type_punctuation_density | "Wide"(默认) | No | 在 + 或= 操作符前后要加空格(此处特指类型签名) |
P.FMT.10 match
分支应该具有良好的可读性
【描述】
- 当match分支右侧代码体太长无法和
=>
置于同一行需要使用块(block)来包裹。 - 在match分支左侧匹配表达式前不要增加管道符(
|
)
【反例】
// 不符合: 与 `=>` 不同行应该用块来包裹 // 当 `match_arm_blocks=false` fn main() { match lorem { ipsum => foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo(x), dolor => println!("{}", sit), sit => foo( "foooooooooooooooooooooooo", "baaaaaaaaaaaaaaaaaaaaaaaarr", "baaaaaaaaaaaaaaaaaaaazzzzzzzzzzzzz", "qqqqqqqqquuuuuuuuuuuuuuuuuuuuuuuuuuxxx", ), } } // 当 `match_arm_leading_pipes="Alaways"` fn foo() { match foo { // 不符合: 分支左侧匹配表达式前不要加管道符 | "foo" | "bar" => {} | "baz" | "something relatively long" | "something really really really realllllllllllllly long" => println!("x"), | "qux" => println!("y"), | _ => {} } }
【正例】
// 当 `match_arm_blocks=true` fn main() { match lorem { // 符合 ipsum => { foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo(x) } dolor => println!("{}", sit), // 符合 sit => foo( "foooooooooooooooooooooooo", "baaaaaaaaaaaaaaaaaaaaaaaarr", "baaaaaaaaaaaaaaaaaaaazzzzzzzzzzzzz", "qqqqqqqqquuuuuuuuuuuuuuuuuuuuuuuuuuxxx", ), } } // 当 `match_arm_leading_pipes="Never"` fn foo() { match foo { // 符合 "foo" | "bar" => {} "baz" | "something relatively long" | "something really really really realllllllllllllly long" => println!("x"), "qux" => println!("y"), _ => {} } }
【rustfmt 配置】
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
match_arm_blocks | true(默认) | No | 当match分支右侧代码体太长无法和=> 置于同一行需要使用块(block)来包裹 |
match_arm_leading_pipes | Never(默认) | No | 在match分支左侧匹配表达式前不要增加管道符(` |
P.FMT.11 导入模块分组应该具有良好的可读性
【描述】
- 导入同一模块的类型,应该置于同一个块内(
imports_granularity="Crate"
)。 - 模块导入应该按以下规则进行分组(
group_imports="StdExternalCrate"
):- 导入来自
std
、core
和alloc
的模块需要置于前面。 - 导入来自 第三方库的模块 应该置于中间。
- 导入来自本地
self
、super
和crate
前缀的模块,置于后面。
- 导入来自
- 分组内使用字典序进行排序(
reorder_imports=true
)。
说明: 默认 rustfmt 不会对导入的模块自动分组,而是保留开发者的导入顺序。所以,这里需要修改 rustfmt 默认配置,才能让rustfmt应用此规则,但因为这几个配置项暂时未稳定,所以需要在 Nightly 下使用。
【反例】
例1:
#![allow(unused)] fn main() { // 不符合: 同一模块类型应该置于同一个块内 // 当 `imports_granularity="Preserve"` use foo::b; use foo::b::{f, g}; use foo::{a, c, d::e}; use qux::{h, i}; }
例2:
#![allow(unused)] fn main() { // 不符合:当按默认值设置时,模块导入比较乱,影响可读性 use super::update::convert_publish_payload; use chrono::Utc; use alloc::alloc::Layout; use juniper::{FieldError, FieldResult}; use uuid::Uuid; use std::sync::Arc; use broker::database::PooledConnection; use super::schema::{Context, Payload}; use crate::models::Event; use core::f32; }
【正例】
例1:
#![allow(unused)] fn main() { // 符合 // 当 `imports_granularity="Crate"` use foo::{ a, b, b::{f, g}, c, d::e, }; use qux::{h, i}; }
例2:
#![allow(unused)] fn main() { // 符合 // 当 `group_imports="StdExternalCrate` 且 `reorder_imports=true` use alloc::alloc::Layout; use core::f32; use std::sync::Arc; use broker::database::PooledConnection; use chrono::Utc; use juniper::{FieldError, FieldResult}; use uuid::Uuid; use super::schema::{Context, Payload}; use super::update::convert_publish_payload; use crate::models::Event; }
【rustfmt 配置】
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
imports_granularity | (Preserve(默认),Crate(推荐)) | No | 默认保留开发者的模块导入顺序 |
reorder_imports | true(默认) | No | 模块分组内根据模块首字母按字典序进行排序 |
group_imports | (Preserve(默认), StdExternalCrate(建议)) | No | 默认保留开发者的模块导入分组 |
P.FMT.12 声明宏分支应该具有良好的可读性
【描述】
- 在声明宏中,模式匹配分支(
=>
左侧)应该使用紧凑格式(format_macro_matchers=true
)。 - 而分支代码体(
=>
右侧) 使用宽松格式。
说明:因为这里需要修改
format_macro_matchers
的默认值,且该配置项并未 Stable ,所以需要在 Nightly 下修改配置项的值以便使用。
【反例】
#![allow(unused)] fn main() { // 不符合: 匹配分支使用了宽松格式 // 当 `format_macro_matchers=false`且 `format_macro_bodies=true` macro_rules! foo { ($a: ident : $b: ty) => { $a(42): $b; }; ($a: ident $b: ident $c: ident) => { $a = $b + $c; }; } // 不符合: 分支代码体使用了紧凑格式 // 当 `format_macro_matchers=false`且 `format_macro_bodies=false` macro_rules! foo { ($a: ident : $b: ty) => { $a(42):$b; }; ($a: ident $b: ident $c: ident) => { $a=$b+$c; }; } }
【正例】
#![allow(unused)] fn main() { // 当 `format_macro_matchers=true` 且 `format_macro_bodies=true` macro_rules! foo { // 符合:匹配分支紧凑格式, `$a:ident` 和 `$b:ty` 各自配对 ($a:ident : $b:ty) => { $a(42): $b; // 在代码体内,则宽松一点 }; // 符合 ($a:ident $b:ident $c:ident) => { $a = $b + $c; }; } }
【rustfmt 配置】
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
format_macro_matchers | (false(默认),true(建议)) | No | 声明宏 模式匹配分支(=> 左侧)中要使用紧凑格式 |
format_macro_bodies | true(默认) | No | 声明宏分支代码体(=> 右侧) 使用宽松格式 |
P.FMT.13 具名结构体字段初始化时不要省略字段名
【描述】
因为本规则依赖于rustfmt,而rustfmt会根据相应配置项对代码进行自动更改,为了确保不会因为rustfmt配置项的更改而导致代码错误,请在遵循rustfmt使用注意事项的基础上遵循本规则:
- 省略字段名的时候需要注意变量名和字段名保持一致。
- 变量名和字段名不一致的情况下,不要省略字段名。
注意:如果将 rustfmt 默认配置
use_field_init_shorthand
改为true
时,有可能会发生代码被修改错误的情况。
【反例】
struct Foo { a: u32, // 注意这里是 a y: u32, z: u32, } fn main() { let x = 1; let y = 2; let z = 3; // 不符合: 如果允许省略字段名,并且rustfmt 配置 `use_field_init_shorthand`改为`true`时, // 下面代码中字段`a`就会被rustfmt删除,变为 `Foo{x, y, z}`,从而造成错误 // rustfmt 无法检查这个错误,但是编译时能检查出来,所以要遵循rustfmt使用注意事项就不会出问题 let a = Foo { a: x, y, z }; }
【正例】
struct Foo { a: u32, y: u32, z: u32, } fn main() { let x = 1; let y = 2; let z = 3; // 符合 let a = Foo { a: x, y: y, z: z }; }
【rustfmt 配置】
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
use_field_init_shorthand | false(默认) | Yes | 具名结构体字段初始化不能省略字段名 |
P.FMT.14 extern 外部函数需要显式指定 C-ABI
【描述】
当使用 extern
指定外部函数时,建议显式指定 C-ABI
。
虽然 extern
不指定的话默认就是 C-ABI
,但是 Rust 语言显式指定是一种约定俗成。
【反例】
#![allow(unused)] fn main() { // 不符合:不要省略 C-ABI 指定 extern { pub static lorem: c_int; } }
【正例】
#![allow(unused)] fn main() { // 符合 extern "C" { pub static lorem: c_int; } extern "Rust" { type MyType; fn f(&self) -> usize; } }
【rustfmt 配置】
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
force_explicit_abi | true(默认) | Yes | extern 外部函数总是要指定 ABI |
P.FMT.15 解构元组的时候允许使用..
来指代剩余元素
【描述】
rustfmt
可以由 condense_wildcard_suffixes
配置项来格式化此规则,其默认选项是 false,表示不允许 解构元组的时候使用..
来指代剩余元素,所以需要修改默认配置项的值为 true
才符合规范。
【反例】
默认情况下,rustfmt 不会自动更改代码,会保留原来的写法。
fn main() { // 不符合: 应该使用`..` let (lorem, ipsum, _, _) = (1, 2, 3, 4); let (lorem, _,ipsum, _, _) = (1, 2, 3, 4, 5); }
【正例】
设置 condense_wildcard_suffixes = true
时,会强行更改代码为下面形式。
fn main() { // 符合 let (lorem, ipsum, ..) = (1, 2, 3, 4); let (lorem, _,ipsum, ..) = (1, 2, 3, 4, 5); }
【rustfmt 配置】
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
condense_wildcard_suffixes | false(默认) true (推荐) | No | 解构元组的时候是否允许使用.. 来指代剩余元素 |
P.FMT.16 不要将派生宏中多个不相关的特质合并为同一行
【描述】
不要将派生宏(Derive)中多个特质(trait)合并为同一行,这样可以增加代码可读性,明确语义。
rustfmt
配置项 merge_derives
用于匹配此格式,其默认值是让派生宏中多个特质在同一行,所以需要修改其默认值。
说明: rustfmt
并不会识别哪些特质相关,所以需要开发者手工指定好。
【反例】
当使用默认设置 merge_derives = true
时,不符合。
#![allow(unused)] fn main() { // 不符合:不相关的特质放到一行 #[derive(Eq, PartialEq, Debug, Copy, Clone)] pub enum Foo {} }
【正例】
修改默认设置 merge_derives = false
,符合。
#![allow(unused)] fn main() { // 符合 #[derive(Eq, PartialEq)] #[derive(Debug)] #[derive(Copy, Clone)] pub enum Foo {} }
【rustfmt 配置】
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
merge_derives | true(默认) false(推荐) | Yes | 是否将多个 Derive 宏合并为同一行 |
2.3 注释与文档
在 Rust 中,注释分为两类:普通注释和文档注释。普通注释使用 //
或 /* ... */
,文档注释使用 ///
、//!
或 /** ... **/
。
在原则和规则中提到「注释」时,包括普通注释和文档注释。当提到「文档」时,特指文档注释。
参考
P.CMT.01 代码能做到自注释,文档要干练简洁
【描述】
一、代码能够做到自注释,避免冗余的普通代码注释。
注释固然很重要, 但最好的代码应当本身就是文档。有意义的类型名、函数名和变量名, 要远胜过要用注释解释的含糊不清的名字。当有意义的类型名、函数名和变量名还不能表达完整的语义时,再使用注释。
不要描述显而易见的现象, 永远不要用自然语言翻译代码作为注释。
二、文档注释要干练简洁:
- 文档注释中内容用语应该尽量简短精干,不宜篇幅过长。请确保你的代码注释良好并且易于他人理解,好的注释能够传达上下文关系和代码目的。
- 注释内容始终围绕两个关键点来构建:
- What : 用于阐述代码为了什么而实现。
- how : 用于阐述代码如何去使用。
- 注释和文档注释使用的自然语言要保持一致。
- Rust 项目文档应该始终基于
rustdoc
工具来构建,rustdoc
支持 Markdown 格式,为了使得文档更加美观易读,文档注释应该使用 Markdown 格式。
【正例】
模块级文档,来自于 Rust 标准库std::vec
:
#![allow(unused)] fn main() { // 符合 //! # The Rust core allocation and collections library //! //! This library provides smart pointers and collections for managing //! heap-allocated values. //! //! This library, like libcore, normally doesn’t need to be used directly //! since its contents are re-exported in the [`std` crate](../std/index.html). //! Crates that use the `#![no_std]` attribute however will typically //! not depend on `std`, so they’d use this crate instead. //! //! ## Boxed values //! //! The [`Box`] type is a smart pointer type. There can only be one owner of a //! [`Box`], and the owner can decide to mutate the contents, which live on the //! heap. }
普通文档注释示例,来自标准库Vec::new
方法:
#![allow(unused)] fn main() { // 符合 /// Constructs a new, empty `Vec<T>`. /// /// The vector will not allocate until elements are pushed onto it. /// /// # Examples /// /// ``` /// # #![allow(unused_mut)] /// let mut vec: Vec<i32> = Vec::new(); /// ``` #[inline] #[rustc_const_stable(feature = "const_vec_new", since = "1.39.0")] #[stable(feature = "rust1", since = "1.0.0")] pub const fn new() -> Self { Vec { buf: RawVec::NEW, len: 0 } } }
P.CMT.02 注释应该有宽度限制
【描述】
每行注释的宽度不能过长,需要设置一定的宽度,不超过120,有助于提升可读性。
rustfmt
中通过comment_width
配合 wrap_comments
配置项,可将超过宽度限制的注释自动分割为多行。
注意:rustfmt
的 use_small_heuristics
配置项并不包括comment_width
。
【反例】
#![allow(unused)] fn main() { // 不符合 // Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. }
【正例】
当 comment_width=80
且 wrap_comments=true
时。
注意:这里 wrap_comments
并未使用默认值,需要配置为 true。
#![allow(unused)] fn main() { // 符合 // Lorem ipsum dolor sit amet, consectetur adipiscing elit, // sed do eiusmod tempor incididunt ut labore et dolore // magna aliqua. Ut enim ad minim veniam, quis nostrud // exercitation ullamco laboris nisi ut aliquip ex ea // commodo consequat. }
【rustfmt 配置】
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
comment_width | 80(默认) | No | 指定一行注释允许的最大宽度 |
wrap_comments | false(默认),true(建议) | No | 运行多行注释按最大宽度自动换成多行注释 |
P.CMT.03 使用行注释而避免使用块注释
【描述】
尽量使用行注释(//
或 ///
),而非块注释。这是Rust社区的约定俗成。
对于文档注释,仅在编写模块级文档时使用 //!
,在其他情况使用 ///
更好。
说明: #![doc]
和 #[doc]
对于简化文档注释有特殊作用,没有必要通过 rustfmt
将其强制转化为 //!
或 ///
。
【反例】
#![allow(unused)] fn main() { // 不符合 /* * Wait for the main task to return, and set the process error code * appropriately. */ mod tests { //! This module contains tests // ... } }
【正例】
当 normalize_comments = true
时:
#![allow(unused)] fn main() { // 符合 // Wait for the main task to return, and set the process error code // appropriately. // 符合 // 在使用 `mod` 关键字定义模块时,在 `mod`之上使用 `///` 更好。 /// This module contains tests mod tests { // ... } // 符合 #[doc = "Example item documentation"] pub enum Foo {} }
【rustfmt 配置】
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
normalize_comments | false(默认) true(推荐) | No | 将 /**/ 注释转为 // |
normalize_doc_attributes | false(默认) | No | 将 #![doc] 和 #[doc] 注释转为 //! 和 /// |
P.CMT.04 文件头注释包含版权说明
【描述】
文件头(即,模块级)注释应先包含版权说明。如果文件头注释需要增加其他内容,可以在版权说明下面补充。
可以包括:
- 文件功能说明。
- 作者。
- 创建日期 和 最后修改日期。
- 注意事项。
- 开源许可证(比如, Apache 2.0, BSD, LGPL, GPL)。
- 其他。
版权说明格式如下:
- 中文版:
版权所有(c)XXX 技术有限公司 2015-2022
。 - 英文版:
Copyright (c) XXX Technologies Co.Ltd. 2015-2022. All rights reserved. Licensed under Apache-2.0.
其内容可以进行调整,参加下面详细说明:
2015-2022
根据实际需要可以修改。2015是文件首次创建年份,2022是文件最后修改年份。可以只写一个创建年份,后续如果经常修改则无需修改版权声明。- 如果是内部使用,则无需增加
All rights reserved
。 Licensed under Apache-2.0.
,如果是开源则可以增加许可证声明。
编写版权注释时注意事项:
- 版权注释应该从文件头顶部开始写。
- 文件头注释首先包含“版权说明”,然后紧跟其他内容。
- 可选内容应按需添加,避免空有格式没有内容的情况。
- 保持统一格式,具体格式由项目或更大的范围统一制定。
- 保持版面工整,换行注意对齐。
【正例】
#![allow(unused)] fn main() { // 符合 // 版权所有(c)XXX 技术有限公司 2015-2022。 // Or // 符合 // Copyright (c) XXX Technologies Co.Ltd. 2015-2022. // All rights reserved. Licensed under Apache-2.0. }
P.CMT.05 在注释中使用 FIXME
和 TODO
来帮助任务协作
【描述】
通过在注释中开启 FIXME
和 TODO
可以方便协作。正式发布版本可以不做此类标注。
注意:此条目不适于使用 rustfmt
相关配置项 report_fixme
和 report_todo
,在 rustfmt
v2.0 中已经移除这两项配置。
【正例】
#![allow(unused)] fn main() { // 符合 // TODO(calebcartwright): consider enabling box_patterns feature gate fn annotation_type_for_level(level: Level) -> AnnotationType { match level { Level::Bug | Level::Fatal | Level::Error => AnnotationType::Error, Level::Warning => AnnotationType::Warning, Level::Note => AnnotationType::Note, Level::Help => AnnotationType::Help, // FIXME(#59346): Not sure how to map these two levels Level::Cancelled | Level::FailureNote => AnnotationType::Error, Level::Allow => panic!("Should not call with Allow"), } } }
G.CMT.01 在公开的返回Result
类型的函数文档中增加 Error 注释
【级别】 建议
【描述】
在公开(pub)的返回Result
类型的函数文档中,建议增加 # Error
注释来解释什么场景下该函数会返回什么样的错误类型,方便用户处理错误。
说明: 该规则可以通过 cargo clippy 来检测,但默认不会警告。
【反例】
#![allow(unused)] #![warn(clippy::missing_errors_doc)] fn main() { use std::io; // 不符合: Clippy 会警告 "warning: docs for function returning `Result` missing `# Errors` section" pub fn read(filename: String) -> io::Result<String> { unimplemented!(); } }
【正例】
#![allow(unused)] #![warn(clippy::missing_errors_doc)] fn main() { use std::io; // 符合:增加了规范的 Errors 文档注释 /// # Errors /// /// Will return `Err` if `filename` does not exist or the user does not have /// permission to read it. pub fn read(filename: String) -> io::Result<String> { unimplemented!(); } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | 默认 level |
---|---|---|---|---|
missing_errors_doc | yes | no | Style | allow |
G.CMT.02 如果公开的API在某些情况下会发生Panic,则相应文档中需增加 Panic 注释
【级别】 要求
【描述】
在公开(pub)函数文档中,建议增加 # Panic
注释来解释该函数在什么条件下会 Panic,便于使用者进行预处理。
说明: 该规则通过 cargo clippy 来检测。默认不会警告。
【反例】
#![allow(unused)] #![warn(clippy::missing_panics_doc)] fn main() { // 不符合:没有添加 Panic 相关的文档注释,Clippy会报错 "warning: docs for function which may panic missing `# Panics` section"。 pub fn divide_by(x: i32, y: i32) -> i32 { if y == 0 { panic!("Cannot divide by 0") } else { x / y } } }
【正例】
#![allow(unused)] #![warn(clippy::missing_panics_doc)] fn main() { // 符合:增加了规范的 Panic 注释 /// # Panics /// /// Will panic if y is 0 pub fn divide_by(x: i32, y: i32) -> i32 { if y == 0 { panic!("Cannot divide by 0") } else { x / y } } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | 默认 level |
---|---|---|---|---|
missing_panics_doc | yes | no | Style | allow |
默认为 allow
,但是此规则需要设置#![warn(clippy::missing_panics_doc)]
。
G.CMT.03 在文档注释中要使用空格代替 tab
【级别】 建议
【描述】
Rust 代码风格中提倡使用四个空格代替tab,在文档注释中也应该统一使用四个空格。
【反例】
下面文档注释中使用了 tab。
#![allow(unused)] fn main() { // 不符合:文档注释中使用了 tab 缩进 /// /// Struct to hold two strings: /// - first one /// - second one pub struct DoubleString { /// /// - First String: /// - needs to be inside here first_string: String, /// /// - Second String: /// - needs to be inside here second_string: String, } }
【正例】
#![allow(unused)] fn main() { // 符合:文档注释中使用了四个空格缩进 /// /// Struct to hold two strings: /// - first one /// - second one pub struct DoubleString { /// /// - First String: /// - needs to be inside here first_string: String, /// /// - Second String: /// - needs to be inside here second_string: String, } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | 默认 level |
---|---|---|---|---|
tabs_in_doc_comments | yes | no | Style | warn |
3. 编程实践
编码实践相关原则和规则,有助于编写更地道更安全的 Rust 代码。
3.1 常量
在 Rust 中,常量有两种用途:
- 编译时常量(Compile-time constants)
- 编译时求值 (CTEF, compile-time evaluable functions)
常量命名风格指南请看 编码风格-命名
G.CNS.01 对于科学计算中涉及浮点数近似值的常量宜使用预定义常量
【级别】 建议
【描述】
Rust标准库中已经提供了一些特殊常量的定义,其精确度通常会比开发者自行定义的高,所以若考虑数值精确度时则宜使用标准库已定义的特殊常量。
这些特殊常量都可以在标准库中找到,例如std::f32::consts
【反例】
#![allow(unused)] fn main() { let x = 3.14; // 不符合:自定义 Pi let y = 1_f64 / x; // 不符合 }
【正例】
#![allow(unused)] fn main() { let x = std::f32::consts::PI; // 符合 let y = std::f64::consts::FRAC_1_PI; // 符合 }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
approx_constant | yes | no | Correctness | deny |
该 Lint 默认为 deny
,但在某些场景下,可以设置为allow
,#![allow(clippy::approx_constant)]
。
G.CNS.02 不应断言常量布尔类型
【级别】 建议
【描述】
此类语句会被编译器优化掉。最好直接使用 panic!
或 unreachable!
代替。
【反例】
#![allow(unused)] fn main() { // 不符合 const B: bool = false; assert!(B); assert!(true); }
【正例】
#![allow(unused)] fn main() { // 符合 panic!("something"); }
【例外】
该示例需要维护一个常量的不变性,确保它在未来修改时不会被无意中破坏。类似于 static_assertions 的作用。
#![allow(unused)] #![allow(clippy::assertions_on_constants)] fn main() { const MIN_OVERFLOW: usize = 8192; const MAX_START: usize = 2048; const MAX_END: usize = 2048; const MAX_PRINTED: usize = MAX_START + MAX_END; assert!(MAX_PRINTED < MIN_OVERFLOW); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
assertions_on_constants | yes | no | Style | warn |
G.CNS.03 不应将内部可变性容器声明为常量
【级别】 要求
【描述】
由于常量有内联的特性。若将一个内容可变容器声明为常量,那么在引用它的时候同样会新建一个实例,这样会破坏内容可变容器的使用目的, 所以需要将它的值存储为静态(static)或者直接将其定义为静态。
【反例】
#![allow(unused)] fn main() { use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; const CONST_ATOM: AtomicUsize = AtomicUsize::new(12); // 不符合 CONST_ATOM.store(6, SeqCst); // 此处相当于新建了一个atomic实例,所以原容器内容并未改变 assert_eq!(CONST_ATOM.load(SeqCst), 12); // 仍为12,因为这两行的CONST_ATOM为不同实例 }
【正例】
#![allow(unused)] fn main() { use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; const CONST_ATOM: AtomicUsize = AtomicUsize::new(12); // 符合 static STATIC_ATOM: AtomicUsize = CONST_ATOM; STATIC_ATOM.store(9, SeqCst); assert_eq!(STATIC_ATOM.load(SeqCst), 9); // 使用`static`, 故上下文的STATIC_ATOM皆指向同一个实例 // 符合: 或直接声明为static static ANOTHER_STATIC_ATOM: AtomicUsize = AtomicUsize::new(15); ANOTHER_STATIC_ATOM.store(9, SeqCst); assert_eq!(ANOTHER_STATIC_ATOM.load(SeqCst), 9); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
borrow_interior_mutable_const | yes | no | Style | warn |
declare_interior_mutable_const | yes | no | Style | warn |
G.CNS.04 不应在常量定义中增加显式的 'static
生命周期
【级别】 要求
【描述】
在常量和静态变量声明时已经默认含有隐式的'static
生命周期,所以不需要额外增加显式'static
。
【反例】
#![allow(unused)] fn main() { // 不符合 const FOO: &'static [(&'static str, &'static str, fn(&Bar) -> bool)] = &[...] static FOO: &'static [(&'static str, &'static str, fn(&Bar) -> bool)] = &[...] }
【正例】
#![allow(unused)] fn main() { // 符合 const FOO: &[(&str, &str, fn(&Bar) -> bool)] = &[...] static FOO: &[(&str, &str, fn(&Bar) -> bool)] = &[...] }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
redundant_static_lifetimes | yes | no | Style | warn |
G.CNS.05 对于适用 const fn
的函数或方法宜尽可能地使用 const fn
【级别】 建议
【描述】
函数或方法缺失const
关键词时无法被指派给常量。
但是要注意不是所有函数都能使用const fn
,因为相比一般函数或方法,const fn
在使用时会有限制,必须满足const 安全,如果不满足,编译器会报告错误信息。
【反例】
#![allow(unused)] fn main() { fn foo() -> usize { 10 } // 不符合:必须是 constant 函数才能用于声明 const 常量 const BAZ: usize = foo(); }
【正例】
#![allow(unused)] fn main() { const fn foo() -> usize { 10 } const BAZ: usize = foo(); // 符合 }
【例外】
#![allow(unused)] fn main() { const fn foo() -> bool { for _i in 0..5 {} // ERROR, 因为for loop默认不能用在const fn内(需要注明#![feature(const_for)]) false } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
missing_const_for_fn | yes | no | Perf | warn |
3.2 静态变量
静态变量是用 static
关键字定义的全局静态变量。
G.STV.01 不宜直接使用可变静态变量作为全局变量
【级别】 建议
【描述】
对可变静态变量直接进行全局修改是 Unsafe 的。在多线程应用中,修改静态变量会导致数据竞争(data race)。
【反例】
#![allow(unused)] fn main() { // 不符合 static mut NUM_OF_APPLES: usize = 0; unsafe fn buy_apples(count: usize) { NUM_OF_APPLES += count; } unsafe fn eat_apple() { NUM_OF_APPLES -= 1; } }
【正例】
如果必须使用的话,可以通过 thread_local!
宏在本地线程中使用内部可变性容器:
#![allow(unused)] fn main() { thread_local!{ // 符合 static NEXT_USER_ID: Cell<u64> = Cell::new(0); } }
若需要变更的值的类型为整数或布尔时,可直接使用 atomic。
#![allow(unused)] fn main() { use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; // 符合 static NUM_OF_APPLES: AtomicUsize = AtomicUsize::new(0); fn buy_apple(count: usize) { NUM_OF_APPLES.fetch_add(count, SeqCst); } fn eat_apple() { NUM_OF_APPLES.fetch_sub(1, SeqCst); } }
补充说明:
若需修改整数或布尔之外的数据类型时,可考虑使用 Mutex 或 Rwlock 配合 once_cell 对全局变量进行变更。
(注: once_cell 目前已经被引入到 Nightly 版本的标准库中但还不稳定, 可参考 std::lazy。若要在 Stable 版本下使用,则需要引入第三方库 once_cell。)
#![feature(once_cell)] // 需要nightly compiler use std::sync::Mutex; use std::lazy::SyncLazy; // 若使用stable版本则需要将之替换为once_cell::sync::Lazy static GLOBAL_MESSAGE: SyncLazy<Mutex<String>> = SyncLazy::new(|| { Mutex::new(String::from("I'm hungry")) }); fn update_msg(msg: &str) { let mut old_msg = GLOBAL_MESSAGE.lock().unwrap(); *old_msg = msg.to_string(); } fn main() { println!("{}", GLOBAL_MESSAGE.lock().unwrap()); // I'm hungry update_msg("I'm not hungry anymore!"); println!("{}", GLOBAL_MESSAGE.lock().unwrap()); // I'm not hungry anymore! }
上述示例亦可通过使用第三方库 lazy_static 的方式实现。
#![allow(unused)] fn main() { use std::sync::Mutex; use lazy_static::lazy_static; lazy_static! { static ref GLOBAL_MESSAGE: Mutex<String> = Mutex::new(String::from("I'm hungry")); } fn update_msg(msg: &str) { ... } ... }
【例外】
在使用FFI引用外部,例如C的函数时,其本身有可能会返回全局变量。当 rust 接入这些函数时需要指定输入的变量类型为静态(static),而若要改变它们的值的时候就需要将其定义为可变静态变量(static mut)。
use std::ffi::CString; use std::ptr; #[link(name = "readline")] extern { static mut rl_prompt: *const libc::c_char; } fn main() { let prompt = CString::new("[my-awesome-shell] $").unwrap(); unsafe { rl_prompt = prompt.as_ptr(); println!("{:?}", rl_prompt); rl_prompt = ptr::null(); } }
通常情况下直接修改 static mut 会有线程安全风险,但若配合使用 std::sync::Once 则可保证该变量只初始化一次,不会产生线程安全风险。
(注:此用法在功能上等同于 once_cell::sync::OnceCell 或 Nightly 版本中的 std::lazy::SyncOnceCell。但在使用 Stable 版本编译器并且不使用第三方库的条件下此写法完全合规,故算作例外情况。)
use std::sync::{Mutex, Once}; static mut SOUND: Option<Mutex<String>> = None; static SOUND_ONCE: Once = Once::new(); fn make_sound() -> &'static Mutex<String> { unsafe { SOUND_ONCE.call_once(|| { SOUND = Some(Mutex::new("Oh! Apple! nom nom nom...".to_string())); }); SOUND.as_ref().unwrap() } } fn main() { println!("{}", *make_sound().lock().unwrap()); // Oh! Apple! nom nom nom... }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | 是否可定制 |
---|---|---|---|---|
_ | no | no | _ | yes |
【定制化参考】
这条规则如果需要定制 Lint,则应考虑两种情况:
- 代码中定义为 static mut 的变量是否仅被用于 FFI
- 代码中定义为 static mut 的变量是否经过 call_once 初始化
3.3 本地变量
这里所说的变量单指局部变量而不包括全局变量。 默认情况下,Rust 会强制初始化所有变量的值,以防止使用未初始化的内存。
P.VAR.01 一般情况下避免先声明可变变量再赋值
【描述】
一般情况下,不要先声明一个可变的变量,然后在后续过程中再去改变它的值。声明一个变量的时候,要对其进行初始化。如果后续可能会改变其值,要考虑优先使用变量遮蔽(继承式可变)功能。如果需要在一个子作用域内改变其值,再使用可变绑定或可变引用。
【反例】
#![allow(unused)] fn main() { // 不符合 let mut base : u8; if cfg!(not(USB_PROTOCOL_NEW_ARCH)) { base = other_instance.base; } else { base = 42u8; } }
【正例】
#![allow(unused)] fn main() { // 符合 let base : u8 = if cfg!(not(USB_PROTOCOL_NEW_ARCH)) { other_instance.base } else { 42u8 } }
P.VAR.02 利用变量遮蔽功能保证变量安全使用
【描述】
在某些场景,可能会临时准备或处理一些数值,但在此之后,数据只用于检查而非修改。
那么可以将其通过变量遮蔽功能,重新绑定为不可变变量,来表明这种临时可变,但后面不变的意图。
【反例】
#![allow(unused)] fn main() { // 不符合:代码语义上没有表现出来先改变,后不变那种顺序语义 let data = { let mut data = get_vec(); data.sort(); data } // `data` 在后面不会再被改变 }
【正例】
#![allow(unused)] fn main() { // 符合 let mut data = get_vec(); data.sort(); //临时需要排序 let data = data; // 符合: 后面就不需要改动了,由编译器可以确保 // `data` 在后面不会再被改变 }
G.VAR.01 以解构元组方式定义超过四个变量时不应使用太多无意义变量名
【级别】 建议
【描述】
在以解构元组的方式定义超过四个变量时,变量名可能是无特别语义的,如用单个字符表示的临时变量。但是不宜使用过多无意义变量名。
【反例】
#![allow(unused)] #![warn(clippy::many_single_char_names)] fn main() { // 不符合 let (a, b, c, d, e, f, g) = (...); }
【正例】
元组元素超过四个的,建议使用包含语义的变量命。
#![allow(unused)] #![warn(clippy::many_single_char_names)] fn main() { // 符合 let (width, high, len, shape, color, status) = (...); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
many_single_char_names | yes | no | pedantic | allow |
该 lint 对应 clippy.toml
配置项:
# 修改可以绑定的单个字符变量名最大数量。默认为 4
single-char-binding-names-threshold=4
G.VAR.02 不应使用非 ASCII 字符作为标识符
【级别】 要求
【描述】
Rust 语言默认支持 Non ASCII 字符作为合法标识符。但是,为了统一团队代码风格,建议使用最常用的 ASCII 字符作为合法标识符。
此外,通常命名相关的 Clippy Lint 检查只支持英文命名。
【反例】
// 不符合 #[derive(Debug)] struct 人 { /// 普通话 名字: String, /// 廣東話 屋企: String, } fn main () { let 我的名字 = "मनीष".to_string(); let 我嘅屋企 = "Berkeley".to_string(); // मराठी let मनीष = 人 { 名字: 我的名字, 屋企: 我嘅屋企, }; // हिंदी let उसका_नाम = "مصطفى".to_string(); let 他的家 = "Oakland".to_string(); // اردو let مصطفى = 人 { 名字: उसका_नाम, 屋企: 他的家, }; println!("मी: {:?}", मनीष); println!("माझा मित्र: {:?}", مصطفى); } // 输出: // मी: 人 { 名字: "मनीष", 屋企: "Berkeley" } // माझा मित्र: 人 { 名字: "مصطفى", 屋企: "Oakland" }
【正例】
// 符合 #[derive(Debug)] struct People { name: String, addr: String, } fn main () { let name = "मनीष".to_string(); let addr = "Berkeley".to_string(); // मराठी let me = People { name: name, addr: addr, }; // हिंदी let name = "مصطفى".to_string(); let addr = "Oakland".to_string(); // اردو let he = People { name: name, addr: addr, }; println!("my name: {:?}", me); println!("his name: {:?}", he); } // 输出 // my name: People { name: "मनीष", addr: "Berkeley" } // his name: People { name: "مصطفى", addr: "Oakland" }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
rustc-lint: non-ascii-idents | no | yes | pedantic | allow |
G.VAR.03 变量遮蔽功能应当合理使用
【级别】 建议
【描述】
变量遮蔽功能在功能上属于一种继承式可变。他会覆盖之前的变量绑定,而创建一个新的同名的变量绑定。
- 在同一个作用域中,非必要时不宜通过新变量声明遮蔽旧变量声明的方式来修改变量。
- 在子作用域内修改“哨兵变量”时,应该避免使用变量遮蔽功能,防止引起逻辑bug。
- 如果使用变量遮蔽,禁止用不同类型的变量遮蔽前一个变量,如果实现同一个
trait
的可以例外。
【反例】
#![warn(clippy::shadow_reuse, clippy::shadow_same, clippy::shadow_unrelated)] fn main() { let mut a = 0; { // 不符合:这里使用变量遮蔽,代码逻辑已经被改变 // clippy::shadow_unrelated let a = 42; } a; // use a again let x = 2; // 不符合: 将会改变x的值 // clippy::shadow_reuse let x = x + 1; // 不符合:只是改变引用级别 // clippy::shadow_same let x = &x; let y = 1; // 不符合:这里使用变量遮蔽逻辑已经被改变 // clippy::shadow_unrelated let x = y; // 更早的绑定 let z = 2; // 不符合:这里使用变量遮蔽逻辑已经被改变 // clippy::shadow_unrelated let x = z; // 遮蔽了更早的绑定 }
【正例】
#![warn(clippy::shadow_reuse, clippy::shadow_same, clippy::shadow_unrelated)] fn main() { let mut a = 0; { // 符合 a = 42; } a;// use a again let x = 2; let y = x + 1; // 符合: 不改变x的值,声明新的变量y let ref_x = &x; // 符合:不改变x的绑定,声明新的变量 let z = 2; let w = z; // 符合: 使用不同的名字 }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
shadow_reuse | yes | no | restriction | allow |
shadow_same | yes | no | restriction | allow |
shadow_unrelated | yes | no | restriction | allow |
G.VAR.04 避免因局部变量过大而导致的大量栈分配
【级别】 建议
【描述】
Rust 局部变量默认分配在栈上。当局部变量占用栈空间过大时,会栈溢出。
采用Box<T>
分配也可能出现栈溢出,参见issues #53827,因为目前 Box<T>
的行为是先在栈上分配然后再复制到堆上。
Rust 默认栈分配空间为:
- 主线程默认
8MiB
。 - 运行中代码创建的子线程默认是
2MiB
。
也可以自行配置栈分配内存大小。
所以,局部变量占用多少空间才算过大,这个需要开发者根据具体的场景根据栈大小配置情况做出合适的预判,一般以 512 KiB为宜。
【反例】
fn main() { // 不符合:运行时会栈溢出 let a = [-1; 3000000]; // or // 不符合:运行时会栈溢出 let a = Box::new([-1; 3000000]); }
【正例】
#![allow(unused)] fn main() { // 符合:栈大小适中 let _: [i32; 8000] = [1; 8000]; }
3.4 数据类型
数据类型记录 Rust 标准库提供的 原生类型,以及结构体和枚举体等编码实践。
P.TYP.01 必要时,应使类型可以表达更明确的语义,而不是只是直接使用原生类型
【描述】
在类型中表达语义,可以增加代码的可读性。
【反例】
fn main() { // 不符合 let years = 1942; }
【正例】
// 符合: 语义更明确 struct Years(i64); fn main() { let years = Years(1942); let years_as_primitive_1: i64 = years.0; let Years(years_as_primitive_2) = years; }
G.TYP.01 类型转换尽可能使用安全的转换函数代替 as
【级别】 建议
【描述】
当在数字类型之间转换时,需要注意的是,如果要确保不会存在有损转换(lossy conversion),就不要使用 as
,而应该使用 From::from
。因为 From 只实现了无损转换。例如,可以用From从i32转换到i64,但反过来转换却不允许。
对于指针类型,尽量使用 cast
方法来代替 as
直接转换。
【反例】
#![warn( clippy::as_conversions, clippy::cast_lossless, clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_precision_loss, clippy::ptr_as_ptr )] fn as_u64(x: u8) -> u64 { // 不符合 x as u64 } // or // 不符合 fn main() { // or let ptr: *const u32 = &42_u32; let mut_ptr: *mut u32 = &mut 42_u32; let _ = ptr as *const i32; // 不符合 let _ = mut_ptr as *mut i32; // 不符合 }
【正例】
#![warn( clippy::as_conversions, clippy::cast_lossless, clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_precision_loss, clippy::ptr_as_ptr )] fn as_u64(x: u8) -> u64 { // 符合 u64::from(x) } fn main() { let ptr: *const u32 = &42_u32; let mut_ptr: *mut u32 = &mut 42_u32; let _ = ptr.cast::<i32>(); // 符合 let _ = mut_ptr.cast::<i32>(); // 符合 }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
as_conversions | yes | no | restriction | allow |
cast_lossless | yes | no | pedantic | allow |
cast_possible_truncation | yes | no | pedantic | allow |
cast_possible_wrap | yes | no | pedantic | allow |
cast_precision_loss | yes | no | pedantic | allow |
cast_sign_loss | yes | no | pedantic | allow |
fn_to_numeric_cast | yes | no | Style | warn |
fn_to_numeric_cast_with_truncation | yes | no | Style | warn |
char_lit_as_u8 | yes | no | Complexity | warn |
cast_ref_to_mut | yes | no | correctness | deny |
ptr_as_ptr | yes | no | pedantic | allow |
G.TYP.02 数字字面量在使用的时候应该明确标注类型
【级别】 建议
【描述】
如果数字字面量没有被指定具体类型,那么单靠类型推导,整数类型会被默认绑定为 i32
类型,而浮点数则默认绑定为 f64
类型。这可能导致某些运行时的意外。
【反例】
#![allow(unused)] #![warn(clippy::default_numeric_fallback)] fn main() { // 不符合 let i = 10; // i32 let f = 1.23; // f64 }
【正例】
#![allow(unused)] #![warn(clippy::default_numeric_fallback)] fn main() { // 符合 let i = 10u32; let f = 1.23f32; }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
default_numeric_fallback | yes | no | restriction | allow |
G.TYP.03 不要用数字类型边界值判断能否安全转换,而应使用 try_from
方法
【级别】 建议
【描述】
在 Rust 中 From
代表不能失败的转换,而 TryFrom
则允许返回错误。
一般在数字类型转换的时候,不需要防御式地去判断数字大小边界,那样可读性比较差,应该使用 try_from
方法,在无法转换的时候处理错误即可。
【反例】
#![allow(unused)] #![warn(clippy::checked_conversions)] fn main() { // 不符合 let foo: u32 = 5; let _ = foo <= i16::MAX as u32; // 等价于 let _ = foo <= (i32::MAX as u32); }
【正例】
#![allow(unused)] #![warn(clippy::checked_conversions)] fn main() { // 符合 let foo: u32 = 5; let f = i16::try_from(foo).is_ok(); // 返回 false }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
checked_conversions | yes | no | pedantic | allow |
布尔
Rust 中的布尔类型有 true
和false
两种值。
G.TYP.BOL.01 不应将布尔值和布尔字面量进行比较
【级别】 要求
【描述】
在 Rust 中,返回为布尔值的表达式或函数值可以直接当作布尔值使用。
总之,使用布尔表达式的时候,要尽可能地简洁明了。
【反例】
#![allow(unused)] fn main() { // 不符合 if x == true {} if y == false {} assert_eq!("a".is_empty(), false); assert_ne!("a".is_empty(), true); }
【正例】
#![allow(unused)] fn main() { // 符合 if x {} if !y {} assert!(!"a".is_empty()); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
bool_comparison | yes | no | complexity | warn |
bool_assert_comparison | yes | no | style | warn |
needless_bool | yes | no | complexity | warn |
nonminimal_bool | yes | no | complexity | warn |
needless_bitwise_bool | yes | no | pedantic | allow |
assertions_on_constants | yes | no | pedantic | warn |
G.TYP.BOL.02 如果 match 匹配表达式为布尔类型,宜使用 if
表达式来代替
【级别】 建议
【描述】
对于布尔表达式更倾向于使用 if ... else ...
,相比 match
模式匹配更有利于代码可读性。
【反例】
#![allow(unused)] #![warn(clippy::match_bool)] fn main() { fn foo() {} fn bar() {} let condition: bool = true; // 不符合 match condition { true => foo(), false => bar(), } }
【正例】
#![allow(unused)] #![warn(clippy::match_bool)] fn main() { fn foo() {} fn bar() {} let condition: bool = true; // 符合 if condition { foo(); } else { bar(); } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
logic_bug | yes | no | correctness | deny |
match_bool | yes | no | pedantic | allow |
G.TYP.BOL.03 不应将数字类型转换为布尔值
【级别】 要求
【描述】
这可能会让布尔值在内存中的表示无效。
【反例】
#![allow(unused)] fn main() { let x = 1_u8; unsafe { // 不符合 let _: bool = std::mem::transmute(x); // where x: u8 } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
transmute_int_to_bool | yes | no | complexity | warn |
G.TYP.BOL.04 禁止在if表达式条件中使用块结构
【级别】 要求
【描述】
为了增加可读性。
【反例】
#![allow(unused)] fn main() { // 不符合 if { true } { /* ... */ } fn somefunc() -> bool { true }; // 不符合 if { let x = somefunc(); x } { /* ... */ } }
【正例】
#![allow(unused)] fn main() { // 符合 if true { /* ... */ } fn somefunc() -> bool { true }; let res = { let x = somefunc(); x }; // 符合 if res { /* ... */ } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
blocks_in_if_conditions | yes | no | style | warn |
G.TYP.BOL.05 非必要时,布尔运算应使用逻辑运算符( &&/||
)而非位运算符 (&/|
)
【级别】 建议
【描述】
位运算不支持短路(short-circuiting),所以会影响性能。逻辑运算符则支持短路。
【反例】
#![allow(unused)] #![warn(clippy::needless_bitwise_bool)] fn main() { let (x,y) = (true, false); if x & !y {} // 不符合:位运算符,不支持短路 }
【正例】
#![allow(unused)] #![warn(clippy::needless_bitwise_bool)] fn main() { let (x,y) = (true, false); if x && !y {} // 符合:逻辑运算符,支持短路 }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
needless_bitwise_bool | yes | no | pedantic | allow |
G.TYP.BOL.06 不应使用数字代替布尔值
【级别】 要求
【描述】
Rust 中布尔值就是 true
和 false
。 不要试图使用数字 1
和 0
来代替布尔值。
虽然 布尔值 可以强转为 对应的数字,但是反之则不行。
不要通过判断数字来代替 布尔值,除非是 FFi 场景通过 C-ABI 和其他语言打交道。
【反例】
#![allow(unused)] fn main() { // 不符合 let a = 1; let b = 0; assert_eq!(true, a == 1); assert_eq!(false, b == 0); }
【正例】
#![allow(unused)] fn main() { // 符合 let a = true; let b = false; assert_eq!(true, a ); assert_eq!(false, b); }
G.TYP.BOL.07 使用 .not()
方法代替逻辑取反运算符 (!
)
【级别】 建议
【描述】
逻辑取反运算符 (!
) 是前缀一元运算符,相对较长的逻辑表达式来说很不显眼。
理解业务逻辑时,容易忽略取反符号,并且需要回头看。
使用 .not()
后缀方法 (std::ops::Not
) 可以吸引注意力,视觉上更为连续。
【反例】
#![allow(unused)] fn main() { assert!(!self.map.contains(&key)); if !cache.contains(&key) { // ... } // 不符合:容易忽略取反符号 }
【正例】
#![allow(unused)] fn main() { use std::ops::Not; assert!(self.map.contains(&key).not()); if cache.contains(&key).not() { // ... } // 符合:`.not()` 更容易吸引注意力 }
字符
在 Rust 中,字符是一个合法的 Unicode 标量值(Unicode scalar value),一个字符大小为 4 字节,对应一个 Unicode 码位(CodePoint)。
G.TYP.CHR.01 不应将字符字面量强制转换为 u8
【级别】 建议
【描述】
应该使用字节字面量,而不应使用字符字面量强转为 u8
。
【反例】
#![allow(unused)] fn main() { // 不符合 'x' as u8 }
【正例】
#![allow(unused)] fn main() { // 符合 b'x' }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
char_lit_as_u8 | yes | no | complexity | warn |
G.TYP.CHR.02 字符串方法中如果需要单个字符的值作为参数,宜使用字符而非字符串
【级别】 建议
【描述】
大部分情况下,使用字符比用字符串性能更好。
【反例】
#![allow(unused)] fn main() { // 不符合 let s = "yxz"; s.split("x"); }
【正例】
#![allow(unused)] fn main() { // 符合 let s = "yxz"; s.split('x'); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
single_char_pattern | yes | no | perf | warn |
G.TYP.CHR.03 需要将整数转换为字符时,应使用安全转换函数,而非 transmute
【级别】 要求
【描述】
并非每个整数都对应一个合法的 Unicode 标量值,使用 transmute
转换会有未定义行为。
【反例】
#![allow(unused)] fn main() { let x = 37_u32; unsafe { // 不符合 let x: char = std::mem::transmute(x); // where x: u32 assert_eq!('%', x); } }
【正例】
#![allow(unused)] fn main() { let x = 37_u32; // 符合:x 会返回一个 Result 类型,开发者可以进行错误处理 let x = std::char::from_u32(x); assert_eq!('%', x); // 符合:如果确定该整数对应合法的 unicode,可以使用 uncheck 方法加速 let x = unsafe {std::char::from_u32_unchecked(x) }; assert_eq!('%', x); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
transmute_int_to_char | yes | no | complexity | warn |
整数
Rust 中有目前有十二种整数类型:i8/u8
, i16/u16
, i32/u32
, i64/u64
, i128/u128
, isize/usize
。
G.TYP.INT.01 在用整数计算的时候需要考虑整数溢出、回绕和截断的风险
【级别】 建议
【描述】
如果从代码上下文的逻辑来看,该计算不可能产生溢出,则可以不进行校验。
比如,对于时间要求精准的系统,如果在计算时间发生整数溢出,或者去计算某个数组的索引等,那可能会发生严重问题。但如果你只是一个简单的计算器,不会被用到具体的业务场合,那溢出也没有关系,因为你只需要在合理的数字范围内计算性能最好。
在 Rust 标准库中,提供 add
/ checked_add
/ saturating_add
/overflowing_add
/ wrapping_add
不同系列方法,返回值不同,根据不同的场合选择适合的方法。
check_*
函数返回Option
,一旦发生溢出则返回None。saturating_*
系列函数返回类型是整数,如果溢出,则给出该类型可表示范围的“最大/最小”值。wrapping_*
系列函数则是直接抛弃已经溢出的最高位,将剩下的部分返回。即,返回直接二进制补码结果。overflowing_*
系列函数返回二进制补码结果以及指示是否发生溢出的布尔值。
Rust 编译器在编译时默认没有溢出检查(可通过编译参数来引入),但在运行时会有 Rust 内置 lint (#[deny(arithmetic_overflow)]
)来检查,如果有溢出会 Panic。
无符号整数使用时要注意回绕(wrap around),不同整数类型转换时需注意截断。
【反例】
#![allow(unused)] #![warn(clippy::integer_arithmetic)] fn main() { // 不符合 assert_eq!((-5i32).abs(), 5); assert_eq!(100i32+1, 101); fn test_integer_overflow() { // 不符合:这种写法 debug 与 release 编译时会有溢出检查 let mut a: u8 = 255 + 1; // 不符合:这种写法,Rust 编译器不检查,但 Clippy可以检查到 // debug模式,运行 panic;release模式,x = 0 let mut x: u8 = 255; x += 1; println!("x={}", x); } }
【正例】
#![allow(unused)] #![warn(clippy::integer_arithmetic)] fn main() { // 符合 assert_eq!((-5i32).checked_abs(), Some(5)); assert_eq!(100i32.saturating_add(1), 101); // 符合 fn add_num(a: u8) -> u8 { a.wrapping_add(255) } fn test_integer_overflow() { // 符合: 对于字面量或常量表达式,debug 与 release 编译模式都会有溢出检查 let mut a: u8 = 255 + 1; // 符合 // debug模式,运行会Panic // release模式,x 会等于 0 let mut x: u8 = 255; x = add_num(x); println!("x={}", x); } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
integer_arithmetic | yes | no | restriction | allow |
manual_saturating_arithmetic | yes | no | style | warn |
G.TYP.INT.02 避免在有符号整数和无符号整数之间进行强制转换
【级别】 建议
【描述】
当有符号整数被强制转换为无符号整数时,负值会发生回绕(wrap around),变成更大的正值,这在实际应用时有可能助长缓冲区溢出风险。
注意:在 Rust 中整数溢出属于 未指定(unspecified)行为,而非未定义行为 (见 RFC 560)。
【反例】
#![warn(clippy::cast_sign_loss)] fn main(){ let y: i8 = -1; y as u128; // will return 18446744073709551615 }
【正例】
#![warn(clippy::cast_sign_loss)] fn main(){ let y : i8 = -1; // Error: // the trait `From<i8>` is not implemented for `u128` // the trait bound `u128: From<i8>` is not satisfied let z = u128::from(y); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
cast_sign_loss | yes | no | pedantic | allow |
注意:默认情况下该 lint 是 allow
,如果需要检查这种转换,则需要设置为 warn
或 deny
。
参考
- https://huonw.github.io/blog/2016/04/myths-and-legends-about-integer-overflow-in-rust/
- https://github.com/rust-lang/rfcs/blob/master/text/0560-integer-overflow.md
G.TYP.INT.03 对负数取模计算的时候不应使用%
【级别】 建议
【描述】
Rust 中的 %
符号为余数运算符,它的行为与C
或Java
等语言中相同符号的运算符相同。它也类似于Python
或Haskell
等语言中的模(modulo)运算符,只是它对负数的行为不同:余数是基于截断除法,而模运算是基于向下取整(floor)除法。
【反例】
#![warn(clippy::modulo_arithmetic)] fn main() { let a: i32 = -1; let b: i32 = 6; // 余数运算符只是返回第一个操作数除以第二个操作数的余数。所以 -1/6 给出 0,余数为 -1 assert_eq!(a % b, -1); }
【正例】
#![warn(clippy::modulo_arithmetic)] fn main() { let a: i32 = -1; let b: i32 = 6; // 取模是严格低于第二个操作数的自然数(所以是非负数),与第二个操作数的最大倍数相加,也低于或等于第一个操作数,则为第一个操作数。 // 6的最大倍数低于或等于-1 是 -6(6*-1),模数是5,因为-6+5=-1。 assert_eq!(a.rem_euclid(b), 5); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
modulo_arithmetic | yes | no | restriction | allow |
浮点数
Rust 的浮点数包括 f32
和 f64
两种类型。Rust 编译器默认推断的 Float 类型是 f64
。
G.TYP.FLT.01 使用浮点数字面量时,要警惕是否存在被Rust编译器截断的风险
【级别】 建议
【描述】
当指定超过类型精度(f32
或 f64
)的字面量值时,Rust 会默认截断该值。
【反例】
#![allow(unused)] fn main() { // 不符合 let v: f32 = 0.123_456_789_9; println!("{}", v); // 0.123_456_789 }
【正例】
#![allow(unused)] fn main() { // 符合 let v: f64 = 0.123_456_789_9; println!("{}", v); // 0.123_456_789_9 }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
excessive_precision | yes | no | style | warn |
G.TYP.FLT.02 从任何数字类型转换为浮点类型时注意避免损失精度
【级别】 建议
【描述】
开发者了解发生精度损失的位置,会对解决因为转换而损失精度的问题更加有好处。
【反例】
#![warn(clippy::cast_precision_loss)] fn main(){ // 不符合 let x = u64::MAX; x as f64; // 不符合 let x: f32 = 16_777_219.0 ; // 该数字转换为 f64 后会表示为 16_777_220.0 x as f64; }
【正例】
#![warn(clippy::cast_precision_loss)] fn main(){ // 符合 let x = i32::MAX; let y = f64::from(x); // 如果 x 为 u64 类型,则编译会出错,不接受这类转换 // 符合 let x: f32 = 16_777_219.0 ; let y = f64::from(x); // 该数字转换为 f32 后会表示为 16_777_220.0 println!("{y:?}") }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
cast_precision_loss | yes | no | pedantic | allow |
G.TYP.FLT.03 对精度高要求的场景下,使用浮点数进行运算和比较时需要注意精度损失
【级别】 建议
【描述】
浮点数计算通常都是不精确的,直接对浮点数进行运算和比较可能造成数据错误。 如何更好地处理浮点数,可以参阅 浮点数指南 。
但是对精度要求不高的场合,比如机器学习中某些场景,对此不做要求。
【反例】
#![warn(clippy::float_arithmetic, clippy::float_cmp, clippy::float_cmp_const)] fn main(){ let x = 1.2331f64; let y = 1.2332f64; payment(x, y); } fn payment(x: f64, y: f64) -> f64{ // 不符合: 浮点数计算有精度损失 y - x }
【正例】
推荐使用精度更高的类型,比如 Decimal 类型(需要第三方库支持)。
#![warn(clippy::float_arithmetic, clippy::float_cmp, clippy::float_cmp_const)] fn main(){ let x = 1.2331f64; let y = 1.2332f64; payment(x, y); } fn payment(x: f64, y: f64) -> Result<f64, PaymentErr>{ let z = y - x; let error_margin = f64::EPSILON; // 符合:浮点数的差异绝对值在允许范围内 if z.abs() < error_margin { return Ok(z); } else { return PaymentErr(e); } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
float_arithmetic | yes | no | restriction | allow |
float_cmp | yes | no | pedantic | allow |
float_cmp_const | yes | no | restriction | allow |
float_equality_without_abs | yes | no | suspicious | warn |
G.TYP.FLT.04 宜使用Rust内置方法处理浮点数计算
【级别】 建议
【描述】
内置方法可能会牺牲一定性能,但可以提升准确性。
【反例】
#![warn(clippy::imprecise_flops, clippy::suboptimal_flops)] // 不符合 fn main() { let a = 3f32; let _ = a.powf(1.0 / 3.0); let _ = (1.0 + a).ln(); let _ = a.exp() - 1.0; use std::f32::consts::E; let a = 3f32; let _ = (2f32).powf(a); let _ = E.powf(a); let _ = a.powf(1.0 / 2.0); let _ = a.log(2.0); let _ = a.log(10.0); let _ = a.log(E); let _ = a.powf(2.0); let _ = a * 2.0 + 4.0; let _ = if a < 0.0 { -a } else { a }; let _ = if a < 0.0 { a } else { -a }; }
【正例】
#![warn(clippy::imprecise_flops, clippy::suboptimal_flops)] // 符合 fn main(){ let a = 3f32; let _ = a.cbrt(); let _ = a.ln_1p(); let _ = a.exp_m1(); use std::f32::consts::E; let a = 3f32; let _ = a.exp2(); let _ = a.exp(); let _ = a.sqrt(); let _ = a.log2(); let _ = a.log10(); let _ = a.ln(); let _ = a.powi(2); let _ = a.mul_add(2.0, 4.0); let _ = a.abs(); let _ = -a.abs(); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
imprecise_flops | yes | no | nursery | allow |
suboptimal_flops | yes | no | nursery | allow |
G.TYP.FLT.05 禁止在浮点数和整数相互转换时使用 transmute
【级别】 要求
【描述】
使用 transmute
转换容易产生未定义行为,建议使用 to_bites
这样转换更加安全。
【反例】
#![allow(unused)] fn main() { // 不符合 unsafe { let _: u32 = std::mem::transmute(1f32); let _: f32 = std::mem::transmute(1_u32); } }
【正例】
#![allow(unused)] fn main() { //符合 let _: u32 = 1f32.to_bits(); let _: f32 = f32::from_bits(1_u32); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
transmute_float_to_int | yes | no | complexity | warn |
transmute_int_to_float | yes | no | complexity | warn |
切片
切片(slice)允许开发者引用集合中连续的元素序列,类型签名用 [T]
表示,但因为它是动态大小类型(DST),所以一般用 &[T]
表示切片。
&str
就是一种字符串切片。
P.TYP.SLC.01 宜使用切片迭代器来代替手工索引
【描述】
在 for 循环中使用索引是比较常见的编程习惯,但是这种方式是最有可能导致边界错误的。
利用 切片自带的方法,并利用迭代器,可以避免这种错误。
【反例】
#![allow(unused)] fn main() { let points: Vec<Coordinate> = ...; let differences = Vec::new(); // 不符合:人工计算长度选择范围很可能会出错 for i in 1..points.len() [ let current = points[i]; let previous = points[i-1]; differences.push(current - previous); ] }
【正例】
#![allow(unused)] fn main() { let points: Vec<Coordinate> = ...; let mut differences = Vec::new(); // 符合:切片提供 windows 或 array_windows 方法返回迭代器 for [previous, current] in points.array_windows().copied() { differences.push(current - previous); } }
P.TYP.SLC.02 宜使用切片模式来提升代码的可读性
【描述】
切片也支持模式匹配,适当应用切片模式,可以有效提升代码可读性。
【正例】
利用切片模式编写判断回文字符串(如"aba"、"abba"之类)的函数。代码来自于:Daily Rust: Slice Patterns,还有更多用例。
#![allow(unused)] fn main() { pub fn word_is_palindrome(word: &str) -> bool { let letters: Vec<_> = word.chars().collect(); is_palindrome(&letters) } // 符合:利用切片模式匹配来判断是否回文字符串 fn is_palindrome(items: &[char]) -> bool { match items { [first, middle @ .., last] => first == last && is_palindrome(middle), [] | [_] => true, } } }
元组
元组是异构复合类型,可以存储多个不同类型的元素。
G.TYP.TUP.01 使用元组时,其元素不宜超过3个
【级别】 建议
【描述】
元组是异构复合类型,元素过多,其表达力会下降,影响代码可读性和可维护性。
尤其是利用元组作为函数返回值时,不宜过多。
【反例】
// 不符合:超过3个元组参数 fn convert(x: i8) -> (i8, i16, i32, i64, f32, f64) { (x as i8, x as i16, x as i32, x as i64, x as f32, x as f64) } fn main(){ let _ = convert(3); }
数组
这里指固定长度数组。注意,不同长度的数组,被视为不同的类型。比如 [T;1]
和 [T;3]
是两种不同的类型。
从 Rust 1.51 版本开始,稳定了常量泛型(const generics)功能,形如 [T;1]
和 [T;3]
这种不同的类型可以统一为 [T; N]
。
G.TYP.ARR.01 创建大全局数组时宜使用静态变量而非常量
【级别】 建议
【描述】
因为常量会内联,对于大的数组,通常情况下,会使用其引用,使用静态变量定义更好。
栈上的数组大小以不超过 512KiB 为宜。
虽然常量本质上是会内联,但 Rust 支持复制消除(Copy Elision)优化(非强制),而且在不断改进完善中,对于这种大的数据应该会有相关优化。
【反例】
#![allow(unused)] #![warn(clippy::large_stack_arrays)] fn main() { // 不符合 pub const A: [u32;1_000_000] = [0u32; 1_000_000]; }
【正例】
#![allow(unused)] #![warn(clippy::large_stack_arrays)] fn main() { // 符合 pub static A: [u32;1_000_000] = [0u32; 1_000_000]; }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
large_const_arrays | yes | no | perf | warn |
large_stack_arrays | yes | no | pedantic | allow |
注意: large_stack_arrays
会检查在栈上分配的大数组,但clippy默认是 allow,根据实际使用场景决定是否针对这种情况发出警告。
G.TYP.ARR.02 使用数组索引时禁止越界访问
【级别】 要求
【描述】
越界访问在运行时会 Panic!
【反例】
#![allow(unused)] fn main() { // 不符合 let x = [1, 2, 3, 4]; x[9]; &x[2..9]; }
【正例】
#![allow(unused)] fn main() { // 符合 let x = [1, 2, 3, 4]; x[0]; x[3]; }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
out_of_bounds_indexing | yes | no | correctness | deny |
G.TYP.ARR.03 当数组元素为原生数据类型(Primitive),排序时优先选用非稳定排序
【级别】 建议
【描述】
稳定排序会消耗更多的内存和 CPU 周期,相对而言,非稳定排序性能更佳。
当然,在必须要稳定排序的场合,不应该使用非稳定排序。
注: Vec<T>
动态数组也适用此规则
【反例】
#![allow(unused)] fn main() { // 不符合 let mut vec = vec![2, 1, 3]; vec.sort(); // stable sort }
【正例】
#![allow(unused)] fn main() { // 符合 let mut vec = vec![2, 1, 3]; vec.sort_unstable(); // unstable sort }
【例外】
#![allow(unused)] fn main() { // https://docs.rs/crate/solana-runtime/1.7.11/source/src/accounts_db.rs#:~:text=clippy%3a%3astable_sort_primitive pub fn generate_index(&self, limit_load_slot_count_from_snapshot: Option<usize>) { let mut slots = self.storage.all_slots(); #[allow(clippy::stable_sort_primitive)] slots.sort(); // 商业需求这里需要稳定排序 // ... } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
stable_sort_primitive | yes | no | perf | warn |
当确实需要稳定排序时,需要修改该 lint 的设置为 allow
。
动态数组
这里指可以动态增长的数组Vec<T>
。
在数组一节中有部分原则和规则也适用于这里。
P.TYP.VEC.01 非必要时不宜使用动态数组
【描述】
非必须不宜使用 Vec<T>
,应该优先尝试使用固定长度数组或常量泛型。
或者可以参考第三方库,诸如 smallvec
,在元素比较少量的时候,可以放到栈上进行管理,如果超过一定元素才会选择堆内存。
【反例】
fn main() { // 不符合 let v: Vec<i32> = vec![1, 2, 3]; println!("{:#}", v); }
【正例】
fn main() { // 符合 let v = [1, 2, 3]; println!("{:#?}", v); }
P.TYP.VEC.02 创建动态数组时,宜预先分配足够容量,避免后续操作中产生多次分配
【描述】
预分配足够的容量,避免后续内存分配,可以提升代码性能。
【反例】
#![allow(unused)] fn main() { // 不符合 let mut output = Vec::new(); }
【正例】
#![allow(unused)] fn main() { // 符合 let mut output = Vec::with_capacity(input.len()); }
G.TYP.VEC.01 禁止访问未初始化的数组
【级别】 建议
【描述】
访问未初始化数组的内存会导致未定义行为。
【反例】
#![allow(unused)] fn main() { let mut vec: Vec<u8> = Vec::with_capacity(1000); unsafe { vec.set_len(1000); } // 不符合 reader.read(&mut vec); // error: Undefined Behavior: using uninitialized data, but this operation requires initialized memory }
【正例】
#![allow(unused)] fn main() { // 符合 let mut vec: Vec<u8> = vec![0; 1000]; reader.read(&mut vec); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
uninit_vec | yes | no | correctness | deny |
结构体
Rust 包含了三种结构体: 命名结构体、元组结构体、单元结构体。
P.TYP.SCT.01 为结构体实现构造性方法时,避免构造后再初始化的情况
【描述】
跟其他OOP 或 FP 语言不一样, Rust 的惯用方式是构建即初始化。
【反例】
#![allow(unused)] fn main() { // 不符合 // 先构建 let mut dict = Dictionary::new(); // 后初始化 dict.load_from_file("./words.txt")?; }
【正例】
#![allow(unused)] fn main() { // 符合 // 构建即初始化 let dict = Dictionary::from_file("./words.txt")?; impl Dictionary { fn from_file(filename: impl AsRef<Path>) -> Result<Self, Error> { let text = std::fs::read_to_string(filename)?; // 不会去存储空状态 let mut words = Vec::new(); for line in text.lines() { words.push(line); } Ok(Dictionary { words }) } } }
P.TYP.SCT.02 结构体实例需要默认实现时,宜使用Default
特质
【描述】
为结构体实现 Default
对于简化代码提高可读性很有帮助。
【示例】
use std::{path::PathBuf, time::Duration}; #[derive(Default, Debug, PartialEq)] struct MyConfiguration { output: Option<PathBuf>, search_path: Vec<PathBuf>, timeout: Duration, check: bool, } fn main() { // 使用 default 方法创建实例 let mut conf = MyConfiguration::default(); conf.check = true; println!("conf = {:#?}", conf); // 创建新实例的时候,使用局部更新更加方便 let conf1 = MyConfiguration { check: true, ..Default::default() }; assert_eq!(conf, conf1); }
G.TYP.SCT.01 对外导出的公开的 Struct,宜添加#[non_exhaustive]
属性
【级别】 建议
【描述】
作为对外公开的 结构体,为了保持稳定性,应该使用 #[non_exhaustive]
属性,避免因为将来结构体字段发生变化而影响到下游的使用。主要涉及命名结构体和元组结构体。
【反例】
在 #[non_exhaustive]
属性稳定之前,社区内还有一种约定俗成的写法来达到防止下游自定义枚举方法。通过 manual_non_exhaustive
可以监控这类写法。
#![allow(unused)] #![warn(clippy::exhaustive_structs)] fn main() { struct S { pub a: i32, pub b: i32, _priv: (), // 不符合:这里用 下划线作为前缀定义的字段,作为私有字段,不对外公开 } // 用户无法自定义实现该结构体的方法。 }
【正例】
#![allow(unused)] #![warn(clippy::exhaustive_structs)] fn main() { // 符合 #[non_exhaustive] struct Foo { bar: u8, baz: String, } }
【例外】
也有例外情况!
从语义角度看,#[non_exhaustive]
只是代表未穷尽的字段或枚举变体,是为了表达“未来可能有变化”这种语义。
但是当要表达 “这个结构体不允许对方实例化” 的语义时,通过自定义的 _priv
字段就可以更好地表达这个语义。
而使用 #[non_exhaustive]
虽然也能达到 “不允许对方实例化的目的”,但是在代码可读性层面,却无法表达出这个语义。
#![allow(unused)] fn main() { // From: https://github.com/tokio-rs/tokio/blob/master/tokio-util/src/codec/framed.rs #[allow(clippy::manual_non_exhaustive)] pub struct FramedParts<T, U> { pub io: T, pub codec: U, pub read_buf: BytesMut, pub write_buf: BytesMut, /// This private field allows us to add additional fields in the future in a /// backwards compatible way. _priv: (), } impl<T, U> FramedParts<T, U> { /// Create a new, default, `FramedParts` pub fn new<I>(io: T, codec: U) -> FramedParts<T, U> where U: Encoder<I>, { FramedParts { io, codec, read_buf: BytesMut::new(), write_buf: BytesMut::new(), _priv: (), } } } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
exhaustive_structs | yes | no | restriction | allow |
manual_non_exhaustive | yes | no | style | warn |
G.TYP.SCT.02 当结构体中有超过三个布尔类型的字段,宜将其独立为新的枚举类
【级别】 建议
【描述】
这样有助于提升 代码可读性和 API 。
【反例】
#![allow(unused)] #![warn(clippy::struct_excessive_bools)] fn main() { // 不符合 struct S { name: String, is_pending: bool, is_processing: bool, is_finished: bool, } }
【正例】
#![allow(unused)] #![warn(clippy::struct_excessive_bools)] fn main() { // 符合 struct S { name: String, state: State, } enum State { Pending, Processing, Finished, } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
struct_excessive_bools | yes | no | pedantic | allow |
该 lint 对应 clippy.toml
配置项:
# 用于配置函数可以拥有的 bool 类型参数最大数量,默认为 3。
max-struct-bools=3
G.TYP.SCT.03 宜使用结构体功能更新语法来提升代码可读性
【级别】 建议
【描述】
更改结构体最好提供结构体方法进行更改,而不是直接构建结构体内容。
但是需要多次修改结构体内容的时候,宜使用结构体更新语法来提升代码可读性。
【反例】
#![allow(unused)] fn main() { // 不符合 impl<'a> Colorize for &'a str { fn red(self) -> ColoredString { ColoredString { fgcolor: String::from("31"), input: String::from(self), // 该方法只更新 fgcolor 和 input bgcolor: String::default(); // 如果该结构体字段比较多的话,此处就需要指派很多字段,不太方便 } } fn on_yellow(self) -> ColoredString { ColoredString { bgcolor: String::from("43"), input: String::from(self), fgcolor: String::default(); } } } }
【正例】
#![allow(unused)] fn main() { // 符合 impl<'a> Colorize for &'a str { fn red(self) -> ColoredString { ColoredString { fgcolor: String::from("31"), input: String::from(self), ..ColoredString::default() // 通过该语法,开发者可以快速了解该方法只更新 fgcolor 和 input,也不需要指派其他不需要更新的字段,更加方便 } } fn on_yellow(self) -> ColoredString { ColoredString { bgcolor: String::from("43"), input: String::from(self), ..ColoredString::default() } } } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
field_reassign_with_default | yes | no | style | warn |
枚举体
Rust 的枚举是一种带 Tag 的联合体。 一般分为三类:空枚举、无字段(fieldless)枚举和数据承载(data carrying)枚举。
G.TYP.ENM.01 合理使用map
和and_then
方法
【级别】 建议
【描述】
在标准库中内置的一些 Enum 类型中提供了一些方便的组合算子,比如 map
和 and_then
。
map
,函数签名是fn map<U, F>(self, f: F) -> Option<U> where F: FnOnce(T) -> U
。and_then
,函数签名是fn and_then<U, F>(self, f: F) -> Option<U> where F: FnOnce(T) -> U
。
Result
中实现的 map/and_then
函数签名也和 Option
一致。这两个方法之间的区别在于传入的闭包参数的返回值类型不同。
这意味着:
- 当你通过
F
对U
进行map
转换的时候,意味着这个转换是一定会成功的。 - 当你通过
F
对U
进行and_then
转换的时候,意味着这个转换是不一定会成功的,需要在F
调用之后对其结果Option<U>/Result<U>
进行处理。
在合适的场景中选择合适的组合算子,可以让代码更加简洁,提升可读性和可维护性。
【反例】
#![allow(unused)] fn main() { // 不符合: 当前这种情况是一定会成功的情况,应该使用 map fn opt() -> Option<&'static str> { Some("42") } fn res() -> Result<&'static str, &'static str> { Ok("42") } let _ = opt().and_then(|s| Some(s.len())); let _ = res().and_then(|s| if s.len() == 42 { Ok(10) } else { Ok(20) }); let _ = res().or_else(|s| if s.len() == 42 { Err(10) } else { Err(20) }); }
【正例】
#![allow(unused)] fn main() { // 符合 fn opt() -> Option<&'static str> { Some("42") } fn res() -> Result<&'static str, &'static str> { Ok("42") } let _ = opt().map(|s| s.len()); let _ = res().map(|s| if s.len() == 42 { 10 } else { 20 }); let _ = res().map_err(|s| if s.len() == 42 { 10 } else { 20 }); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
bind_instead_of_map | yes | no | complexity | warn |
G.TYP.ENM.02 不应自行创建空枚举
【级别】 建议
【描述】
在 Rust 中 只有 never
类型(!
)才是唯一合法表达 无法被实例化类型 的类型。但目前 never
类型还未稳定,只能在 Nightly 下使用。
【反例】
#![allow(unused)] #![feature(never_type)] // 当启用该功能的时候,下面的clippy才会生效 #![warn(clippy::empty_enum)] fn main() { // 不符合 enum Test {} }
【正例】
所以,如果想在稳定版 Rust 中使用,建议使用std::convert::Infallible
。 Infallible
枚举是一个合法的空枚举,常用于错误处理中,表示永远不可能出现的错误。但是目前也可以用于在稳定版中替代 never
类型。
#![allow(unused)] #![feature(never_type)] // 当启用该功能的时候,下面的clippy才会生效 #![warn(clippy::empty_enum)] fn main() { //符合: 未来 never 类型稳定的话,将会把 Infallible 设置为 never 类型的别名 pub type Infallible = !; }
【例外】
因为 std::convert::Infallible
默认实现了很多 trait,如果不想依赖其他 trait ,那么可以用 空枚举。
#![allow(unused)] #![feature(never_type)] // 当启用该功能的时候,下面的clippy才会生效 #![allow(clippy::empty_enum)] fn main() { pub enum NoUserError {} impl Display for NoUserError { fn fmt(&self, _formatter: &mut fmt::Formatter) -> fmt::Result { match *self {} } } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
empty_enum | yes | no | pedantic | allow |
G.TYP.ENM.03 在使用类似 C 语言的枚举写法且使用repr(isize/usize)
布局时注意 32位架构上截断的问题
【级别】 建议
【描述】
在使用类似 C 语言的枚举写法且使用repr(isize/usize)
布局时,在32位架构上会截断变体值,但在64位上工作正常。
【反例】
#![allow(unused)] fn main() { #[repr(usize)] enum NonPortable { X = 0x1_0000_0000, // 不符合:如果在 32位架构上会截断变体值,导致该指针地址变化 Y = 0, } }
【正例】
因为当前 lint 默认是 deny
,所以需要将其配置为 allow
。
#![allow(unused)] #![allow(clippy::enum_clike_unportable_variant)] fn main() { #[repr(isize)] pub enum ZBarColor { ZBarSpace = 0, // 符合:因为值足够小,没有截断风险 ZBarBar = 1, } // 符合:没有指定 repr(isize/usize) #[allow(clippy::enum_clike_unportable_variant)] pub(crate) enum PropertyType { ActionItemSchemaVersion = 0x0C003473, ActionItemStatus = 0x10003470, ActionItemType = 0x10003463, Author = 0x1C001D75, } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
enum_clike_unportable_variant | yes | no | correctness | deny |
G.TYP.ENM.04 不宜在use
语句中引入Enum的全部变体(variants)
【级别】 建议
【描述】
使用 Enum 的类型前缀可以使代码更加可读。
【反例】
#![allow(unused)] #![warn(clippy::enum_glob_use)] fn main() { // 不符合 use std::cmp::Ordering::*; // 这里导入了全部变体 foo(Less); }
【正例】
#![allow(unused)] #![warn(clippy::enum_glob_use)] fn main() { // 符合 use std::cmp::Ordering; foo(Ordering::Less) }
【例外】
当枚举体非常多的时候,比如 oci_spec::Arch 中对应平台架构的枚举值,直接用 *
导入会更加方便。
#![allow(unused)] fn main() { // From: https://github.com/alacritty/alacritty/blob/master/alacritty/src/config/bindings.rs#L368 #![allow(clippy::enum_glob_use)] use oci_spec::Arch::*; pub enum Arch { /// The native architecture. ScmpArchNative = 0x00000000, /// The x86 (32-bit) architecture. ScmpArchX86 = 0x40000003, /// The x86-64 (64-bit) architecture. ScmpArchX86_64 = 0xc000003e, // ... more } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
enum_glob_use | yes | no | pedantic | allow |
G.TYP.ENM.05 对外导出的公开Enum,宜添加#[non_exhaustive]
属性
【级别】 建议
【描述】
作为对外公开的 Enum,为了保持稳定性,应该使用 #[non_exhaustive]
属性,避免因为将来Enum 枚举变体的变化而影响到下游的使用。
【反例】
在 #[non_exhaustive]
属性稳定之前,社区内还有一种约定俗成的写法来达到防止下游自定义枚举方法。
#![allow(unused)] #![warn(clippy::exhaustive_enums)] fn main() { enum E { A, B, #[doc(hidden)] _C, // 不符合: 这里用 下划线作为前缀定义的变体,作为隐藏的变体,不对外展示 } }
【正例】
#![allow(unused)] fn main() { // 符合 #[non_exhaustive] enum E { A, B, } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
exhaustive_enums | yes | no | restriction | allow |
manual_non_exhaustive | yes | no | style | warn |
G.TYP.ENM.06 Enum内变体的大小差异不宜过大
【级别】 建议
【描述】
要注意 Enum 内变体的大小差异不要过大,因为 Enum 内存布局是以最大的变体进行对齐。根据场景,如果该Enum 实例中小尺寸变体的实例使用很多的话,内存就会有所浪费。如果小尺寸变体的实例使用很少,则影响不大。
解决办法之一为把大尺寸变体包含到 Box<T>
中。
【反例】
#![allow(unused)] fn main() { // 不符合 enum Test { A(i32), B([i32; 1000]), C([i32; 8000]), } }
【正例】
#![allow(unused)] fn main() { // 符合 enum Test { A(i32), B(Box<[i32; 1000]>), C(Box<[i32; 8000]>), } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
large_enum_variant | yes | no | perf | warn |
该 lint 可以通过 clippy 配置项 enum-variant-size-threshold = 200
来配置,默认是 200
字节。
G.TYP.ENM.07 如需依赖 Enum 中变体的序数,则应为变体设置明确的数值
【级别】 要求
【描述】
在日常开发中,有时需要产生出有独立名称,且为连续或是有规律的数值,用来当作接口的参数数值,一般采用枚举(Enum)来实现。
Rust 语言的枚举变体的序数(ordinal)依赖于它的定义顺序。在开发过程中,很有可能需要新增变体。一般情况下,都是从尾部追加变体,但不排除有人会从中间新增变体,或者,依赖于某些库将变体自动按字典序排序,这样就有可能打乱枚举变体本来到顺序,导致程序中依赖变体序数的代码产生逻辑错误。
所以,在这种情况下,我们需要为变体设置明确的数值
【反例】
// 不符合 enum Mode { Mode0, // 0 Mode1, // 1 Mode3, // 2 Mode2, // 3 } fn main() { // 不符合:报错,此处 Mode::Mode3 对应值为 2 ,而不是 3 assert_eq!(3, Mode::Mode3 as u8); }
【正例】
// 符合 enum Mode { Mode0 = 0, Mode1 = 1, Mode3 = 3, Mode2 = 2, } fn main() { // 符合 assert_eq!(3, Mode::Mode3 as u8); }
3.5 表达式
Rust 中几乎一切皆表达式。
G.EXP.01 当需要对表达式求值后重新赋值时,宜使用复合赋值模式
【级别】 建议
【描述】
略
【反例】
#![allow(unused)] fn main() { let mut a = 5; let b = 0; a = a + b; // 不符合 }
【正例】
#![allow(unused)] fn main() { let mut a = 5; let b = 0; a += b; // 符合 }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
assign_op_pattern | yes | no | style | warn |
G.EXP.02 不宜在比较中使用不兼容的位掩码
【级别】 要求
【描述】
如果比较的位总是被位掩码设置为零或一,则比较是常量true或 false(取决于掩码、比较值和运算符),这种代码是有误导性的,可能是故意这么写用于赢得一场性能竞赛或者是通过一个测试用例。
可以对照下面表格进行检查。
Comparison | Bit Op | Example | is always | Formula |
---|---|---|---|---|
== or != | & | x & 2 == 3 | false | c & m != c |
< or >= | & | x & 2 < 3 | true | m < c |
> or <= | & | x & 1 > 1 | false | m <= c |
== or != | | | x | 1 == 0 | false | c | m != c |
< or >= | | | x | 1 < 1 | false | m >= c |
<= or > | | | x | 1 > 0 | true | m > c |
【反例】
#![allow(unused)] fn main() { let x = 2; // 不符合:该表达式会永远是 false if (x & 1 == 2) { } }
【正例】
#![allow(unused)] fn main() { let x = 2; // 符合 if (x == 2) { } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
bad_bit_mask | yes | no | correctness | deny |
G.EXP.03 不应利用数组表达式的边界检查来 Panic,而应使用断言
【级别】 建议
【描述】
这样会影响代码可读性。使用断言可以更好的描述代码的意图。
【反例】
fn main(){ // 不符合 [42, 55][get_usize()]; compute_array()[0]; } fn get_usize() -> usize { 6 } fn compute_array() -> [i32; 3] { [1,2,3] }
【正例】
fn main(){ // 符合 assert!([42, 55].len() > get_usize()); assert!(compute_array().len() > 0); } fn get_usize() -> usize { 6 } fn compute_array() -> [i32; 3] { [1,2,3] }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
unnecessary_operation | yes | no | complexity | warn |
G.EXP.04 自增或自减运算使用+=
或-=
【级别】 建议
【描述】
C/Cpp 等编程语言常用的自增自减操作,如 ++i
、i++
、i--
等不是合法的 Rust 表达式, --i
虽然是合法的 Rust 表达式,但是表达对i取反两次,而不是自减语义。
【反例】
#![allow(unused)] fn main() { let mut x = 3; --x; // 不符合:x 的值还是 3 }
【正例】
#![allow(unused)] fn main() { let mut x = 3; x -= 1; // 符合 }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
double_neg | yes | no | style | warn |
G.EXP.05 使用括号来清楚表示表达式的计算顺序
【级别】 建议
【描述】
并不是每个人都能记得住优先级,所以最好使用括号把优先级顺序区分出来,增加可读性。
【反例】
#![allow(unused)] fn main() { 1 << 2 + 3 // 不符合 -1i32.abs() // 不符合 }
【正例】
#![allow(unused)] fn main() { (1 << 2) + 3 // 符合 (-1i32).abs() // 符合 }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
precedence | yes | no | complexity | warn |
G.EXP.06 避免在比较中添加无用的掩码操作
【级别】 要求
【描述】
检查比较中的无用位掩码操作,可以在不改变结果的情况下删除该位掩码操作。
请对照下面表格进行检查。
Comparison | Bit Op | Example | equals |
---|---|---|---|
> / <= | | / ^ | x | 2 > 3 | x > 3 |
< / >= | | / ^ | x ^ 1 < 4 | x < 4 |
【反例】
#![allow(unused)] fn main() { // 不符合 if (x | 1 > 3) { } }
【正例】
#![allow(unused)] fn main() { // 符合 if (x > 3) { } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
ineffective_bit_mask | yes | no | correctness | deny |
3.6 控制流程
Rust中流程控制也是属于表达式,但在本规范中将其独立出来。
P.CTF.01 避免滥用迭代器
【描述】
迭代器虽然是 Rust 中比较推崇的方式,但也没必要过度使用它。总之,如果使用迭代器让代码太复杂,就考虑换个非迭代器的方式实现吧。
【反例】
创建一个 Matrix变换的函数,但是这种迭代器的方式,代码可读性相比于命令式更困难。
#![allow(unused)] fn main() { // From : https://adventures.michaelfbryan.com/posts/rust-best-practices/bad-habits/#overusing-iterators pub fn functional_blur(input: &Matrix) -> Matrix { assert!(input.width >= 3); assert!(input.height >= 3); let mut rows = input.rows(); let first_row = rows.next().unwrap(); let last_row = rows.next_back().unwrap(); let top_row = input.rows(); let middle_row = input.rows().skip(1); let bottom_row = input.rows().skip(2); let blurred_elements = top_row .zip(middle_row) .zip(bottom_row) .flat_map(|((top, middle), bottom)| blur_rows(top, middle, bottom)); let elements: Vec<f32> = first_row .iter() .copied() .chain(blurred_elements) .chain(last_row.iter().copied()) .collect(); Matrix::new_row_major(elements, input.width, input.height) } fn blur_rows<'a>( top_row: &'a [f32], middle_row: &'a [f32], bottom_row: &'a [f32], ) -> impl Iterator<Item = f32> + 'a { // 不符合: 使用迭代器处理矩阵变换,代码不直观 let &first = middle_row.first().unwrap(); let &last = middle_row.last().unwrap(); let top_window = top_row.windows(3); let middle_window = middle_row.windows(3); let bottom_window = bottom_row.windows(3); let averages = top_window .zip(middle_window) .zip(bottom_window) .map(|((top, middle), bottom)| top.iter().chain(middle).chain(bottom).sum::<f32>() / 9.0); std::iter::once(first) .chain(averages) .chain(std::iter::once(last)) } }
【正例】
创建一个 Matrix变换的函数,使用命令式风格,代码功能比较明确,更加直观。
#![allow(unused)] fn main() { // From: https://adventures.michaelfbryan.com/posts/rust-best-practices/bad-habits/#overusing-iterators pub fn imperative_blur(input: &Matrix) -> Matrix { assert!(input.width >= 3); assert!(input.height >= 3); let mut output = input.clone(); for y in 1..(input.height - 1) { for x in 1..(input.width - 1) { let mut pixel_value = 0.0; // 符合: 直接使用数组计算坐标更加直观方便 pixel_value += input[[x - 1, y - 1]]; pixel_value += input[[x, y - 1]]; pixel_value += input[[x + 1, y - 1]]; pixel_value += input[[x - 1, y]]; pixel_value += input[[x, y]]; pixel_value += input[[x + 1, y]]; pixel_value += input[[x - 1, y + 1]]; pixel_value += input[[x, y + 1]]; pixel_value += input[[x + 1, y + 1]]; output[[x, y]] = pixel_value / 9.0; } } output } }
P.CTF.02 优先使用模式匹配而非判断后再取值
【描述】
Rust 中 模式匹配 是惯用法,而不是通过 if
判断值是否相等。
【反例】
#![allow(unused)] fn main() { let opt: Option<_> = ...; // 不符合 if opt.is_some() { let value = opt.unwrap(); ... } // 不符合 let list: &[f32] = ...; if !list.is_empty() { let first = list[0]; ... } }
【正例】
#![allow(unused)] fn main() { // 符合 if let Some(value) = opt { ... } // 符合 if let [first, ..] = list { ... } }
G.CTF.01 当需要通过多个if
判断来比较大小来区分不同情况时,优先使用match
和cmp
来代替if
表达式
【级别】 建议
【描述】
在使用多个if-else
来对不同情况进行区分时,使用 match
和 cmp
代替 if
的好处是语义更加明确,而且也能帮助开发者穷尽所有可能性。
但是这里需要注意这里使用 match
和 cmp
的性能要低于 if
表达式,因为 一般的 >
或 <
等比较操作是内联的,而 cmp
方法没有内联。
【反例】
#![allow(unused)] fn main() { fn a() {} fn b() {} fn c() {} fn f(x: u8, y: u8) { // 不符合 if x > y { a() } else if x < y { b() } else { c() } } }
【正例】
#![allow(unused)] fn main() { use std::cmp::Ordering; fn a() {} fn b() {} fn c() {} fn f(x: u8, y: u8) { // 符合 match x.cmp(&y) { Ordering::Greater => a(), Ordering::Less => b(), Ordering::Equal => c() } } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
comparison_chain | yes | no | style | warn |
G.CTF.02 if
条件表达式分支中如果包含了else if
分支也应该包含else
分支
【级别】 建议
【描述】
这样做有助于代码逻辑更加健壮清晰,在一些要求严格的编码规范中要求这么做,比如《MISRA-C:2004 Rule 14.10》编码规范。
【反例】
#[warn(clippy::else_if_without_else)] fn a() {} fn b() {} fn main(){ let x: i32 = 1; if x.is_positive() { a(); } else if x.is_negative() { b(); } // 不符合:没有 else 分支 }
【正例】
#[warn(clippy::else_if_without_else)] fn a() {} fn b() {} fn main(){ let x: i32 = 1; if x.is_positive() { a(); } else if x.is_negative() { b(); } else { // 符合 } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
else_if_without_else | yes | no | restriction | allow |
G.CTF.03 如果要通过 if
条件表达式来判断是否 Panic,请优先使用断言
【级别】 建议
【描述】
略
【反例】
#![allow(unused)] fn main() { let sad_people: Vec<&str> = vec![]; // 不符合 if !sad_people.is_empty() { panic!("there are sad people: {:?}", sad_people); } }
【正例】
#![allow(unused)] fn main() { let sad_people: Vec<&str> = vec![]; // 符合 assert!(sad_people.is_empty(), "there are sad people: {:?}", sad_people); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
if_then_panic | yes | no | Style | warn |
G.CTF.04 在 Match 分支的 Guard 语句中不要使用带有副作用的条件表达式
【级别】 建议
【描述】
因为在 mactch 分支中, 匹配几次就会执行 Guard 几次。如果携带副作用,会产生意料之外的情况。
【反例】
// 不符合:下面代码会输出两次 "ha" fn main() { use std::cell::Cell; let i: Cell<i32> = Cell::new(0); match 1 { 1 | _ // 这里匹配两次 if { // 这个 Guard 条件表达式带有副作用:打印,因为匹配两次,所以会执行两次 println!("ha"); i.set(i.get() + 1); false } => {} _ => {} } assert_eq!(i.get(), 2); }
3.7 字符串
Rust 中字符串是有效的 UTF-8 编码的字节数组。
Rust 字符串类型众多,但本节内容主要围绕 :String
/ &str
P.STR.01 处理字符串元素时优先按字节处理而非字符
【描述】
处理字符串有两种方式,一种是按字符处理,即把字符串转为字符数组[char]
,另一种是直接按字节处理[u8]
。
两者之间的一些区别:
[char]
保证是有效的 Unicode,但不一定是有效的 UTF-8,一般将其看作是 UTF-32 。将字符数组转换为字符串需要注意。[u8]
不一定是有效的字符串,它比[char]
节省内存。将其转换为字符串需要检查UTF-8
编码。
P.STR.02 创建字符串时,宜预先分配大约足够的容量来避免后续操作中产生多次分配
【描述】
预分配足够的容量,避免后续内存分配,可以提升代码性能。
【反例】
#![allow(unused)] fn main() { let mut output = String::new(); }
【正例】
#![allow(unused)] fn main() { let mut output = String::with_capacity(input.len()); }
P.STR.03 在使用内建字符串处理函数或方法的时候,应注意避免隐藏的嵌套迭代或多次迭代
【描述】
比如 contains
函数的实现就是按字符遍历字符串,但是如果你将它用于一个字符串的迭代处理中,就会产生嵌套迭代,时间复杂度从你以为的 O(n)
变成了 O(n^2)
。没有将其用于迭代中,也有可能产生多次迭代,O(n)
变为 O(n+m)
。 为了避免这个问题,我们可以用 find
来代替 contains
。
所以,在使用内建函数的时候要注意它的实现,选择合适的函数或方法,来避免这类问题。
【示例】
#![allow(unused)] fn main() { // 对输入的字符串进行转义 pub fn find<'a, S: Into<Cow<'a, str>>>(input: S) -> Cow<'a, str> { let input = input.into(); fn is_trouble(c: char) -> bool { c == '<' || c == '>' || c == '&' } // 使用 find 而非 contains // find 使用模式查找,可以返回匹配字符的位置信息 let first = input.find(is_trouble); // 利用 find 的位置信息,避免第二次遍历 if let Some(first) = first { let mut output = String::from(&input[0..first]); output.reserve(input.len() - first); let rest = input[first..].chars(); for c in rest { match c { '<' => output.push_str("<"), '>' => output.push_str(">"), '&' => output.push_str("&"), _ => output.push(c), } } Cow::Owned(output) } else { input.into() } } }
P.STR.04 在使用 Cow<'a, B>
时要注意选择合理场景以便最大化地优化性能
【描述】
Cow<'a, B>
可以减少不必要的内存拷贝。Cow 代表 Clone-On-Write,意味着,使用它可以只在必要的时候再进行拷贝。
但它并不是万能的,只有在需要大量读取数据但仅有少量情况需要修改时,Cow<'a, B>
才能真正起到优化性能的作用。
如果不在意依赖库过多,编译文件更大,也可以使用第三方库 regex
来处理大数据的搜索匹配和替换等需求,性能更佳。
【反例】
假设场景一:要处理的大文件中,至少一半数据包含转义符号
#![allow(unused)] fn main() { // 不符合:这种情况在输入不包含转义符号的数据时,也需要对所有字符进行匹配处理,性能较低 pub fn naive(input: &str) -> String { let mut output = String::new(); for c in input.chars() { match c { '<' => output.push_str("<"), '>' => output.push_str(">"), '&' => output.push_str("&"), _ => output.push(c) } } output } }
假设场景二:要处理的大文件中,几乎所有数据都包含转义符号。这种场景下,使用 Cow<'a, B>
对性能也无法起到太大的优化作用,此时宜尝试使用 regex
之类的高性能第三方库处理。
【正例】
假设场景一:要处理的大文件中,至少一半数据包含转义符号。
#![allow(unused)] fn main() { // 对输入的字符串进行转义 // 符合: 性能提升大概 1 倍 pub fn naive<'a, S: Into<Cow<'a, str>>>(input: S) -> Cow<'a, str> { let input = input.into(); fn is_trouble(c: char) -> bool { c == '<' || c == '>' || c == '&' } if input.contains(is_trouble) { let mut output = String::with_capacity(input.len()); for c in input.chars() { match c { '<' => output.push_str("<"), '>' => output.push_str(">"), '&' => output.push_str("&"), _ => output.push(c) } } // 只有在字符串修改的时候才使用 String Cow::Owned(output) } else { //其他情况使用 &str input } } }
P.STR.05 在拼接字符串时,优先使用format!
【描述】
在Rust中有很多方法可以连接字符串,不同的连接方法适用于不同的场景,性能也会有所差别。
【示例】
// 组合字符串是最简单和直观的方法,尤其是在字符串和非字符串混合的情况下。 fn main() { let name = "world!"; let hw = format!("Hello {}!", name); println!("{:#?}", hw); } // 在追加字符串的时候,可以使用`push_str`方法,`push_str`性能比`format!`更好 fn main() { let mut hw = String::new(); hw.push_str("hello"); hw.push_str(" world!"); println!("{:#?}", hw); } // 通过`concat()`方法将字符串数组拼接成一个字符串 fn main() { let hw = ["hello", " ", "world!"].concat(); println!("{:#?}", hw); } // 通过`join()`方法将字符串数组拼接成一个字符串 fn main() { let hw_1 = ["hello", "world!"].join(""); println!("{:#?}", hw_1); // 输出: // helloworld! // 使用`join()`方法在拼接字符串时添加或指定字符 let hw_2 = ["hello", "world!"].join("+"); println!("{:#?}", hw_2); // 输出: // hello+world! } // 使用`collect()`方式对数组中的字符串进行拼接 fn main() { let hw = ["hello", " ", "world!"]; let res: String = hw.iter().map(|x| *x).collect(); println!("{:#?}", res); } // 使用符号`+`进行字符串拼接 fn main() { let hw_1 = &(String::from("hello") + &String::from(" ") + &String::from("world!")); println!("{:#?}", hw_1); let hw_2 = &(String::from("hello") + " " + "world!"); println!("{:#?}", hw_2); let hw_3 = &("hello".to_owned() + " " + "world!"); println!("{:#?}", hw_3); let hw_4 = &("hello".to_string() + " " + "world!"); println!("{:#?}", hw_4); }
G.STR.01 在实现Display
特质时不应调用to_string()
方法
【级别】 要求
【描述】
因为 to_string
是间接通过 Display
来实现的,如果实现 Display
的时候再使用 to_tring
的话,将会无限递归。
【反例】
#![allow(unused)] fn main() { use std::fmt; struct Structure(i32); impl fmt::Display for Structure { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.to_string()) // 不符合 } } }
【正例】
#![allow(unused)] fn main() { use std::fmt; struct Structure(i32); impl fmt::Display for Structure { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.0) // 符合 } } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
to_string_in_display | yes | no | correctness | deny |
G.STR.02 在追加字符串时使用push_str
方法
【级别】 建议
【描述】
增强代码的可读性
【反例】
#![allow(unused)] #![warn(clippy::string_add_assign, clippy::string_add)] fn main() { let mut x = "Hello".to_owned(); x = x + ", World"; // 不符合 }
【正例】
#![allow(unused)] #![warn(clippy::string_add_assign, clippy::string_add)] fn main() { let mut x = "Hello".to_owned(); // More readable x += ", World"; x.push_str(", World"); // 符合 }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
string_add_assign | yes | no | pedantic | allow |
string_add | yes | no | restriction | allow |
G.STR.03 将只包含 ASCII
字符的字符串字面量转为字节序列可以直接使用b"str"
语法代替调用as_bytes
方法
【级别】 建议
【描述】
这是为了增强可读性,让代码更简洁。
注意,"str".as_bytes()
并不等价于 b"str"
,而是等价于 &b"str"[..]
。
【反例】
#![allow(unused)] #![warn(clippy::string_lit_as_bytes)] fn main() { // 不符合 let bs = "a byte string".as_bytes(); }
【正例】
#![allow(unused)] #![warn(clippy::string_lit_as_bytes)] fn main() { // 符合 let bs = b"a byte string"; }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
string_lit_as_bytes | yes | no | nursery | allow |
G.STR.04 需要辨别字符串的字符开头或结尾字符时,不应按字符迭代比较
【级别】 建议
【描述】
Rust 语言核心库和标准库都对字符串内置了一些方便的方法来处理这类需求。
迭代字符的性能虽然也很快(对500多个字符迭代转义处理大概需要4.5微秒左右),但这种场景用迭代的话,代码可读性更差一些。
【反例】
#![allow(unused)] fn main() { let name = "_"; // 不符合 name.chars().last() == Some('_') || name.chars().next_back() == Some('-'); let name = "foo"; // 不符合 if name.chars().next() == Some('_') {}; }
【正例】
#![allow(unused)] fn main() { let name = "_"; // 符合 name.ends_with('_') || name.ends_with('-'); let name = "foo"; // 符合 if name.starts_with('_') {}; }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
chars_last_cmp | yes | no | style | warn |
chars_next_cmp | yes | no | style | warn |
G.STR.05 对字符串按指定位置进行切片的时候需要小心破坏其 UTF-8 编码
【级别】 建议
【描述】
字符串默认是合法的 UTF-8
字节序列,如果通过指定索引位置来对字符串进行切片,有可能破坏其合法 UTF-8
编码,除非这个位置是确定的,比如按 char_indices
方法来定位是合法的。
【反例】
#![warn(clippy::string_slice)] fn main(){ let s = "Ölkanne"; // 不符合 // 字节索引 1 不是字符的边界,所以程序会 panic // `Ölkanne` 的 'Ö' 是 字节 `0..2` let sub_s = &s[1..]; // println!("{:?}", sub_s); }
【正例】
#![allow(clippy::string_slice)] fn main(){ let s = "Ölkanne"; let mut char_indices = s.char_indices(); assert_eq!(Some((0, 'Ö')), char_indices.next()); // assert_eq!(Some((2, 'l')), char_indices.next()); let pos = if let Some((pos, _)) = char_indices.next(){ pos } else {0}; // 符合:计算出了正确的字符位置 // 注意,这里 lint 检查工具可能误报,但这里是合法的,所以将lint设置为 allow let sub_s = &s[pos..]; assert_eq!("lkanne", sub_s); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
string_slice | yes | no | restriction | allow |
3.8 集合类型
Rust 中的集合类型包括四大类:
- 线性序列:
Vec
,VecDeque
,LinkedList
- 映射集:
HashMap
,BTreeMap
- 集合:
HashSet
,BTreeSet
- 其他:
BinaryHeap
P.CLT.01 创建HashMap、VecDeque时,可以预先分配大约足够的容量来避免后续操作中产生多次分配
【描述】
预分配足够的容量,避免后续内存分配,可以提升代码性能。
【反例】
use std::collections::HashMap; use std::collections::VecDeque; fn main() { // 不符合 let mut map = HashMap::new(); map.insert("a", 1); map.insert("b", 2); map.insert("c", 3); println!("{:#?}", map); // 不符合 let mut deque = VecDeque::new(); deque.push_back(1); deque.push_back(2); deque.push_back(3); println!("{:#?}", deque); }
【正例】
use std::collections::HashMap; use std::collections::VecDeque; fn main() { // 符合 let mut map = HashMap::with_capacity(3); map.insert("a", 1); map.insert("b", 2); map.insert("c", 3); println!("{:#?}", map); // 符合 let mut deque = VecDeque::with_capacity(3); deque.push_back(1); deque.push_back(2); deque.push_back(3); println!("{:#?}", deque); }
G.CLT.01 非必要情况下,不要使用LinkedList
,而用Vec
或VecDeque
代替
【级别】 建议
【描述】
一般情况下,有 Vec
和VecDeque
性能更好。LinkedList
存在内存浪费,缓存局部性(Cache Locality)比较差,无法更好地利用CPU 缓存机制,性能很差。
只有在有大量的 列表 拆分 和 合并 操作时,才真正需要链表,因为链表允许你只需操作指针而非复制数据来完成这些操作。
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
linkedlist | yes | no | pedantic | allow |
该 lint 对应 clippy.toml
配置项:
# 如果函数是被导出的 API,则该 lint 不会被触发,是防止 lint 建议对 API 有破坏性的改变。默认为 true
avoid-breaking-exported-api=true
3.9 函数设计
创建函数或使用闭包时需要注意的地方。
P.FUD.01 传递到闭包的变量建议单独重新绑定
【描述】
默认情况下,闭包通过借用来捕获环境变量。或者,可以使用 move
关键字来移动环境变量到闭包中。
将这些要在闭包内用的变量,重新进行分组绑定,可读性更好。
【反例】
#![allow(unused)] fn main() { use std::rc::Rc; let num1 = Rc::new(1); let num2 = Rc::new(2); let num3 = Rc::new(3); let closure = { // `num1` 所有权已经转移 let num2 = num2.clone(); let num3 = num3.as_ref(); move || { *num1 + *num2 + *num3; // 不符合 } }; }
【正例】
#![allow(unused)] fn main() { use std::rc::Rc; let num1 = Rc::new(1); let num2 = Rc::new(2); let num3 = Rc::new(3); // 符合: 单独对要传递到闭包的变量重新绑定 let num2_cloned = num2.clone(); let num3_borrowed = num3.as_ref(); let closure = move || { *num1 + *num2_cloned + *num3_borrowed; // 符合 }; }
P.FUD.02 函数返回值不要使用 return
【描述】
Rust 中函数块会自动返回最后一个表达式的值,不需要显式地指定 return
。
只有在函数过程中需要提前返回的时候再加 Return。
【反例】
#![allow(unused)] fn main() { fn foo(x: usize) -> usize { if x < 42{ return x; } return x + 1; // 不符合 } }
【正例】
#![allow(unused)] fn main() { fn foo(x: usize) -> usize { if x < 42{ return x; } x + 1 // 符合 } }
G.FUD.01 函数参数最长不要超过五个
【级别】 建议
【描述】
为了提升代码可读性,函数的参数最长不宜超过五个。
【反例】
#![allow(unused)] fn main() { struct Color; // 不符合 fn foo(x: f32, y: f32, name: &str, c: Color, w: u32, h: u32, a: u32, b: u32) { // .. } }
【正例】
想办法把过长的参数缩短。
struct Color; // 符合:此处使用 常量泛型(const generic) 来接收后面多个 u32 类型的参数 // 使用元组 缩短 2~3 个参数为一个参数 fn foo<T, const N: usize>(x: (f32, f32), name: &str, c: Color, last: [T; N]) { ; } fn main(){ let arr = [1u32, 2u32]; foo((1.0f32, 2.0f32), "hello", Color, arr); let arr = [1.0f32, 2.0f32, 3.0f32]; foo((1.0f32, 2.0f32), "hello", Color, arr); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
too_many_arguments | yes | no | complexity | warn |
该 lint 对应 clippy.toml
配置项:
# 函数参数最长不要超过5个
too-many-arguments-threshold=5
G.FUD.02 当函数参数实现了 Copy,并且是按值传入,如果值可能会太大,则宜考虑按引用传递
【级别】 建议
【描述】
通过值传递的参数可能会导致不必要的 memcpy
拷贝,这可能会造成性能损失。
【反例】
#![allow(unused)] #![warn(clippy::large_types_passed_by_value)] fn main() { #[derive(Clone, Copy)] struct TooLarge([u8; 2048]); // 不符合 fn foo(v: TooLarge) {} }
【正例】
#![allow(unused)] #![warn(clippy::large_types_passed_by_value)] fn main() { #[derive(Clone, Copy)] struct TooLarge([u8; 2048]); // 符合 fn foo(v: &TooLarge) {} }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
large_types_passed_by_value | yes | no | pedantic | allow |
该 lint 对应 clippy.toml
配置项:
# 如果函数是被导出的 API,则该 lint 不会被触发,是防止 lint 建议对 API 有破坏性的改变。默认为 true
avoid-breaking-exported-api=true
G.FUD.03 当函数参数出现太多 bool 类型的参数时,应该考虑将其封装为自定义的结构体或枚举
【级别】 建议
【描述】
布尔类型的参数过多,很难让人记住,容易出错。将其封装为枚举或结构体,可以更好地利用类型系统的检查而避免出错。 其他类型参数过多时,也可以考虑是否可以用自定义结构体或枚举进行封装。
【反例】
#![allow(unused)] #![warn(clippy::fn_params_excessive_bools)] fn main() { // 不符合 fn f(is_round: bool, is_hot: bool) { ... } }
【正例】
#![allow(unused)] #![warn(clippy::fn_params_excessive_bools)] fn main() { enum Shape { Round, Spiky, } enum Temperature { Hot, IceCold, } // 符合 fn f(shape: Shape, temperature: Temperature) { ... } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
fn_params_excessive_bools | yes | no | pedantic | allow |
该 lint 对应 clippy.toml
配置项:
# 用于配置函数可以拥有的 bool 类型参数最大数量,默认为 3。
max-fn-params-bools=3
G.FUD.04 当Copy 类型的足够小的值作为函数参数时,应该按值(by-value)传入,而不是引用(by-ref)
【级别】 建议
【描述】
在函数参数为 Copy 类型 且 其值足够小的时候,一般情况下,会避免传引用。因为对于这种小的值,性能上和按引用传递是一样快的,并且在代码更容易编写和可读。包括一些小的 结构体,也推荐按值传递,但要注意【例外】示例所示的情况。
【反例】
#![allow(unused)] #![warn(clippy::trivially_copy_pass_by_ref)] fn main() { fn foo(v: &u32) {} }
【正例】
#![allow(unused)] #![warn(clippy::trivially_copy_pass_by_ref)] fn main() { fn foo(v: u32) {} }
【例外】
需要注意这种情况下,lint 检查工具会误报。
#[derive(Clone, Copy)] struct RawPoint { pub x: u8, } #[derive(Clone, Copy)] struct Point { pub raw: RawPoint, } impl Point { pub fn raw(&self) -> *const RawPoint { &self.raw } // 如果听信 lint 的建议,将上面的 raw 函数参数 self 的引用去掉就是 raw_linted 函数, 这在没有编译优化的情况下(如只是 cargo build 运行在debug模式下)的时候不会出错,但在有编译优化的场景下(如 cargo build --release 运行在release模式)就会出现以下问题。 pub fn raw_linted(self) -> *const RawPoint { &self.raw } } fn main() { let p = Point { raw: RawPoint { x: 10 } }; // This passes assert_eq!(p.raw(), p.raw()); // This fails // 事实上,如果去掉那个 self 的引用,该函数的行为就变了 // 因为 结构体 Point 是 Copy 的,每次调用 raw_linted 方法,结构体实例就会被复制一次,得到的结果就不一样了 assert_eq!(p.raw_linted(), p.raw_linted()); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
trivially_copy_pass_by_ref | yes | no | pedantic | allow |
该 lint 对应 clippy.toml
配置项:
# 如果函数是被导出的 API,则该 lint 不会被触发,是防止 lint 建议对 API 有破坏性的改变。默认为 true
avoid-breaking-exported-api=true
# 考虑Copy按值而不是按引用传递的类型的最大大小(以字节为单位)。默认是None
trivial-copy-size-limit=None
注意,该 lint 没有考虑指针相关的情况,见例外示例。需要酌情考虑使用。例外示例来自 rust-clippy/issues/5953 。
G.FUD.05 不要总是为函数指定 inline(always)
【级别】 建议
【描述】
inline
虽然可以提升性能,但也会增加编译时间和编译大小。
Rust 中性能、编译时间和编译大小之间需要权衡。根据需要再 inline
即可。
【反例】
#![allow(unused)] #![warn(clippy::inline_always)] fn main() { // 不符合 #[inline(always)] fn not_quite_hot_code(..) { ... } }
【例外】
根据需要再inline即可,比如明确知道某个函数被调用次数非常频繁,这个时候为了性能考虑要为其手工指定内联。
#![allow(unused)] fn main() { // 符合:实现内存回收功能,调用非常频繁。性能优先。 #[inline(always)] pub fn buf_recycle(buf_id: usize) { // ... } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
inline_always | yes | no | pedantic | allow |
G.FUD.06 函数参数应该考虑兼容多种类型
【级别】 建议
【描述】
这样的好处是参数可以灵活兼容更多类型,代码方便扩展。
【反例】
// 不符合 fn three_vowels(word: &String) -> bool { let mut vowel_count = 0; for c in word.chars() { match c { 'a' | 'e' | 'i' | 'o' | 'u' => { vowel_count += 1; if vowel_count >= 3 { return true } } _ => volweld_count = 0 } } false } fn main() { let sentence_string = "Once upon a time, there was a friendly curious crab named Ferris".to_string(); for word in sentence_string.split(' ') { if three_vowels(word.to_string()) { println!("{} has three consecutive vowels!", word); } } }
【正例】
// 符合:这里的参数可以接受 &String / &'str/ &'static str 三种类型参数 fn three_vowels(word: &str) -> bool { let mut vowel_count = 0; for c in word.chars() { match c { 'a' | 'e' | 'i' | 'o' | 'u' => { vowel_count += 1; if vowel_count >= 3 { return true } } _ => vowel_count = 0 } } false } fn main() { let sentence_string = "Once upon a time, there was a friendly curious crab named Ferris".to_string(); for word in sentence_string.split(' ') { if three_vowels(word) { println!("{} has three consecutive vowels!", word); } } }
3.10 泛型
Rust 中的泛型允许开发人员编写更加简洁、更少重复的代码。但泛型可能会引起编译文件大小膨胀,酌情使用。
P.GEN.01 用泛型来抽象公共语义
【描述】
应该巧用泛型来抽象公共语义,消除重复代码。
【反例】
use std::ops::Add; #[derive(Debug, Clone, Copy)] struct Meter { value: f64 } impl Meter { fn new(value: f64) -> Self { Self { value } } } impl Add for Meter { type Output = Meter; fn add(self, another: Meter) -> Self::Output { let value = self.value + another.value; Meter { value } } } #[derive(Debug, Clone, Copy)] struct Kilogram { value: f64 } impl Kilogram { fn new(value: f64) -> Self { Self { value } } } impl Add for Kilogram { type Output = Kilogram; fn add(self, another: Kilogram) -> Self::Output { let value = self.value + another.value; Kilogram { value } } } fn main() { // 不符合:如果要再新增新的单位,还需要实现很多重复代码 let one_meter = Meter::new(1.0); let two_kilograms = Kilogram::new(2.0); let two_meters = one_meter + one_meter; }
【正例】
use std::ops::Add; use std::marker::PhantomData; #[derive(Debug, Clone, Copy)] struct Unit<T> { value: f64, unit_type: PhantomData<T>, } impl<T> Unit<T> { fn new(value: f64) -> Self { Self { value, unit_type: PhantomData, } } } impl<T> Add for Unit<T> { type Output = Unit<T>; fn add(self, another: Unit<T>) -> Self::Output { let new_value = self.value + another.value; Unit::new(new_value) } } #[derive(Debug, Clone, Copy)] struct MeterType; #[derive(Debug, Clone, Copy)] struct KilogramType; type Meter = Unit<MeterType>; type Kilogram = Unit<KilogramType>; fn main() { // 符合:如果要再新增新的单位,就方便很多了 let one_meter = Meter::new(1.0); let two_kilograms = Kilogram::new(2.0); let two_meters = one_meter + one_meter; }
P.GEN.02 不要随便使用 impl Trait
语法替代泛型限定
【描述】
impl Trait
语法 和 泛型限定,虽然都是静态分发,且效果类似,但是它们的语义是不同的。
在类型系统层面上的语义:
impl Trait
是 存在量化类型。意指,存在某一个被限定的类型。- 泛型限定 是 通用量化类型。意指,所有被限定的类型。
要根据它们的语义来选择不同的写法。
另外,impl Trait
可以用在函数参数位置和返回值位置,但是不同位置意义不同。
函数参数位置
等价于 泛型参数。
但要注意:
#![allow(unused)] fn main() { fn f(b1: impl Bar, b2: impl Bar) -> usize }
等价于:
#![allow(unused)] fn main() { fn f<B1: Bar, B2: Bar>(b1: B1, b2: B2) -> usize }
而不是
#![allow(unused)] fn main() { fn f<B: Bar>(b1: B, b2: B) -> usize }
证明示例:
use std::fmt::Display; // 函数参数可以传入 整数,但是函数返回值是 String fn func(arg: impl Display) -> impl Display { format!("Hay! I am not the same as \"{}\"", arg) } // 很明显不等价于下面这类 // fn somefunc2<T: Display>(arg: T) -> T { // // 需要指定同一个类型 T 的行为 // } fn main(){ let a = 42; let a = func(42); }
函数返回值
在返回值位置上,如果是泛型参数,则是由调用者来选择具体类型,比如 parse::<i32>("32")
; 如果是 impl Trait
,则是由被调用者来决定具体类型,但只能有一种类型。
在返回值位置上的 impl Trait
会根据函数体的返回值自动推断实现了哪些 auto trait。这意味着你不必在 impl Trait
后面再 加 Sync + Send
这种auto trait。
注意下面代码:
#![allow(unused)] fn main() { // Error: 这里只允许有同一种具体类型,Foo 和 Baz 都实现了 Bar 也是错的。 fn f(a: bool) -> impl Bar { if a { Foo { ... } } else { Baz { ... } } } }
P.GEN.03 不要使用太多泛型参数和 trait 限定,否则会增长编译时间
【描述】
为泛型函数添加详细的 trait 限定,可以在一定程度上增强用户使用体验,但使用过多的泛型参数和 trait 限定会显著地增长编译时间。
【反例】
此写法比正例的写法编译时间要多十倍。
#![allow(unused)] fn main() { // From: https://github.com/tokio-rs/axum/pull/198 fn handle_error<ReqBody, ResBody, F, Res, E>( self, f: F, ) -> HandleError<Self, F, ReqBody, HandleErrorFromRouter> where Self: Service<Request<ReqBody>, Response = Response<ResBody>>, F: FnOnce(Self::Error) -> Result<Res, E>, Res: IntoResponse, ResBody: http_body::Body<Data = Bytes> + Send + Sync + 'static, ResBody::Error: Into<BoxError> + Send + Sync + 'static, { HandleError::new(self, f) } }
【正例】
来自于 Web 框架 Axum 的代码:
#![allow(unused)] fn main() { // From: https://github.com/tokio-rs/axum/pull/198 fn handle_error<ReqBody, F>( self, f: F, ) -> HandleError<Self, F, ReqBody, HandleErrorFromRouter> { HandleError::new(self, f) } }
P.GEN.04 为泛型类型实现方法时,impl
中声明的泛型类型参数一定要被用到
【描述】
在 impl
中被声明的类型参数,至少要满足下面三种形式:
impl<T> Foo<T>
,T
出现在实现的Self
类型Foo<T>
中 。impl<T> SomeTrait<T> for Foo
,T
出现在要实现的 trait 中 。impl<T, U> SomeTrait for T where T: AnotherTrait<AssocType=U>
, 出现在T
的 trait 限定的关联类型中。
除此之外,都不算 T
被用到(出现在 Self 类型中)。
有这种限制,主要有两个原因:
- 方便 Rust 类型推断。有这些限制才能明确能推断这些泛型参数的行为,避免产生错误。参考 RFC 0447 。
- 避免语义定义不明确的情况。如果
impl
上存在自由的 泛型参数,则无法保证这一点。
【反例】
#![allow(unused)] fn main() { // case 1 struct Foo; impl<T: Default> Foo { // error: the type parameter `T` is not constrained by the impl trait, self // type, or predicates [E0207] fn get(&self) -> T { <T as Default>::default() } } // case 2 trait Maker { type Item; fn make(&mut self) -> Self::Item; } struct Foo<T> { foo: T } struct FooMaker; impl<T: Default> Maker for FooMaker { // error: the type parameter `T` is not constrained by the impl trait, self // type, or predicates [E0207] type Item = Foo<T>; fn make(&mut self) -> Foo<T> { Foo { foo: <T as Default>::default() } } } // error: the type parameter `A` is not constrained by the impl trait, self type, or predicates trait Foo {} impl<F, A> Foo for F where F: Fn(A) {} // error }
【正例】
#![allow(unused)] fn main() { // case 1 struct Foo; // Move the type parameter from the impl to the method impl Foo { fn get<T: Default>(&self) -> T { <T as Default>::default() } } // case 2 use std::marker::PhantomData; trait Maker { type Item; fn make(&mut self) -> Self::Item; } struct Foo<T> { foo: T } // Add a type parameter to `FooMaker` struct FooMaker<T> { phantom: PhantomData<T>, } impl<T: Default> Maker for FooMaker<T> { type Item = Foo<T>; fn make(&mut self) -> Foo<T> { Foo { foo: <T as Default>::default(), } } } // closue : 相关 issue: https://github.com/rust-lang/rust/issues/25041 trait Foo {} impl<F, A> Foo for F where F: Fn() -> A {} // 此处 A 是 闭包trait内的一个关联类型 }
P.GEN.05 定义泛型函数时,如果该函数实现用到来自 trait 定义的相关行为,需要为泛型指定相关 trait 的限定
【描述】
泛型,在 Rust 类型系统中的语义是一种 通用量化类型(Universally-quantified type),即,泛型类型 T
的所有可能 的单态类型。
在泛型函数内部,如果使用了来自某个 trait 定义的行为,则需要为泛型指定相关的 trait 限定,来排除其他没有实现该trait 的类型。
注:Rust编译器可以检测这种情况,但是编译错误比较晦涩,本原则用来提示开发者注意这种情况。
【反例】
#![allow(unused)] fn main() { use std::fmt; // println! 中 `{:?}` 为 Debug triat 定义行为 fn some_func<T>(foo: T) { println!("{:?}", foo); // error[E0277]: `T` doesn't implement `Debug` } }
【正例】
use std::fmt; // 为泛型类型 T 指派 Debug triat 限定 fn some_func<T: fmt::Debug>(foo: T) { println!("{:?}", foo); } struct A; fn main() { some_func(5i32); // A 没有实现 Debug trait,会被排除掉 some_func(A); // error[E0277]: `A` doesn't implement `Debug` }
G.GEN.01 不要在泛型位置上使用内建类型
【级别】 建议
【描述】
这样做虽然会导致编译错误,但是这种错误会使开发者感到困惑,反而无法找到问题所在。
【反例】
这里 u32
会被认为是一个类型参数。
#![allow(unused)] fn main() { // 不符合 impl<u32> Foo<u32> { fn impl_func(&self) -> u32 { 42 } } }
【正例】
#![allow(unused)] fn main() { // 符合 impl<T> Foo<T> { fn impl_func(&self) -> T { 42 } } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
builtin_type_shadow | yes | no | style | warn |
G.GEN.02 使用 Rust 标准库中某些方法,要注意避免使用其泛型默认实现,而应该使用具体类型的实现
【级别】 建议
【描述】
Rust 标准库内部某些类型使用了 泛型特化(未稳定特性),比如 ToString
trait。
该 trait 有一个泛型默认实现, 并且一些具体类型也实现了它,比如 char
/ str
/ u8
/ i8
等。
在实际代码中,应该选择去调用具体类型实现的 to_string()
方法,而非调用泛型的默认实现。
这一规则要求开发者对 Rust 标准库的一些方法实现有一定了解。
【反例】
#![allow(unused)] #![warn(clippy::inefficient_to_string)] fn main() { // 不符合 // 闭包参数中, s 为 `&&str` 类型 // `&&str` 就会去调用泛型的默认实现 ["foo", "bar"].iter().map(|s| s.to_string() ); }
【正例】
#![allow(unused)] #![warn(clippy::inefficient_to_string)] fn main() { // 符合 // 闭包参数中, s 为 `&&str` 类型,使用 `|&s|` 对参数模式匹配后,闭包体内 `s` 就变成了 `&str` 类型 // 经过这样的转换,直接调用 `&str`的 `to_string()` 方法,而如果是 `&&str` 就会去调用泛型的默认实现。 ["foo", "bar"].iter().map(|&s| s.to_string() ); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
inefficient_to_string | yes | no | pedantic | allow |
3.11 特质
特质就是指 trait。在 Rust 中, trait 不是具体类型,而是一种抽象接口。但是通过 impl Trait
和 dyn Trait
也可以将 trait 作为类型使用。
P.TRA.01 使用 trait 时要注意 trait 一致性规则
【描述】
使用 trait 的时候,必须要满足 trait 一致性规则,即,孤儿规则(orphans rule):类型和trait,必须有一个是在本地crate内定义的。
当不满足孤儿规则时,可以考虑使用NewType
模式来解决问题。
【正例】
#![allow(unused)] fn main() { // String 和 FromStr都在标准库中被定义 // 如果想给String实现FromStr,则编译器会报错,告诉你这违反孤儿规则 // (虽然标准库内已经为string实现了FromStr,这里只是示例) // 但是通过使用NewType,我们可以间接的达成目标 // 使用这种单个元素的元组结构体包装一个类型就叫NewType模式。 pub struct PhoneNumber(String); use std::str::FromStr; impl FromStr for PhoneNumber { type Err = Box<dyn std::error::Error>; fn from_str(s: &str) -> Result<Self, Self::Err> { Ok(PhoneNumber(s.to_string())) } } }
内置 trait
Rust 标准库内置了很多 trait,在使用这些 trait 的时候也需要注意。
列表
- P.TRA.BLN.01 在实现
Borrow
特质时,需要注意一致性 - G.TRA.BLN.01 应该具体类型的
default()
方法代替Default::default()
调用 - G.TRA.BLN.02 不要为迭代器实现Copy特质
- G.TRA.BLN.03 能使用派生宏(Derive)自动实现Default特质就不要用手工实现
- G.TRA.BLN.04 在使用
#[derive(Hash)]
的时候,避免再手工实现PartialEq
- G.TRA.BLN.05 在使用
#[derive(Ord)]
的时候,避免再手工实现PartialOrd
- G.TRA.BLN.06 不要对实现
Copy
或引用类型调用std::mem::drop
和std::mem::forgot
- G.TRA.BLN.07 对实现
Copy
的可迭代类型来说,要通过迭代器拷贝其所有元素时,应该使用copied
方法,而非cloned
- G.TRA.BLN.08 实现
From
而不是Into
- G.TRA.BLN.09 一般情况下不要给
Copy
类型手工实现Clone
- G.TRA.BLN.10 不要随便使用Deref特质来模拟继承
P.TRA.BLN.01 在实现Borrow
特质时,需要注意一致性
【描述】
当你想把不同类型的借用进行统一抽象,或者当你要建立一个数据结构,以同等方式处理自拥有值(ownered)和借用值(borrowed)时,例如散列(hash)和比较(compare)时,选择 Borrow
。当把某个类型直接转换为引用,选择 AsRef
。
但是使用 Borrow
的时候,需要注意一致性问题。具体请看示例。
【反例】
#![allow(unused)] fn main() { // 这个结构体能不能作为 HashMap 的 key? pub struct CaseInsensitiveString(String); // 它实现 Eq 没有问题 impl PartialEq for CaseInsensitiveString { fn eq(&self, other: &Self) -> bool { // 但这里比较是要求忽略了 ascii 大小写 self.0.eq_ignore_ascii_case(&other.0) } } impl Eq for CaseInsensitiveString { } // 实现 Hash 没有问题 // 但因为 eq 忽略大小写,那么 hash 计算也必须忽略大小写 impl Hash for CaseInsensitiveString { fn hash<H: Hasher>(&self, state: &mut H) { for c in self.0.as_bytes() { // 不符合:没有忽略大小写 c.to_ascii_lowercase().hash(state) } } } }
这种情况下,就不能为 CaseInsensitiveString
实现 Borrow
,并非编译不通过,而是在逻辑上不应该为其实现 Borrow
,因为 CaseInsensitiveString
实现 Eq
和 Hash
的行为不一致,而 HashMap
则要求 Key
必须 Hash
和 Eq
的实现一致。这种不一致,编译器无法检查,所以在逻辑上,就不应该为其实现 Borrow
。如果强行实现,那可能会出现逻辑 Bug。
G.TRA.BLN.01 应该具体类型的 default()
方法代替 Default::default()
调用
【级别】 建议
【描述】
为了增强可读性。
【反例】
#![allow(unused)] #![warn(clippy::default_trait_access)] fn main() { // 不符合 let s: String = Default::default(); }
【正例】
#![allow(unused)] #![warn(clippy::default_trait_access)] fn main() { // 符合 let s = String::default(); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
default_trait_access | yes | no | pedantic | allow |
G.TRA.BLN.02 不要为迭代器实现Copy
特质
【级别】 要求
【描述】
不要为迭代器实现 Copy
特质,因为一般会存在有改变状态的迭代器,如果实现 Copy
,则可能会被意外隐式复制,违反 Rust 编译器可变借用独占原则,可能会导致一些意外行为。
所以,通常可以只为迭代器实现 Clone
,需要复制的时候显式拷贝,如果出现问题也容易排查。
【反例】
#![warn(clippy::copy_iterator)] use std::marker::PhantomData; #[derive(Debug)] struct ABC { a: [i32; 3], b: [i32; 3], c: [i32; 3], } impl ABC { fn iter_mut(&mut self) -> ABCIterMut { ABCIterMut { abc: self, index: 0, _phantom: PhantomData::default(), } } } // 不符合:这里为迭代器 ABCIterMut<'a> 实现 Copy #[derive(Copy, Clone)] struct ABCIterMut<'a> { abc: *mut ABC, index: u8, _phantom: PhantomData<&'a mut ABC>, } impl<'a> Iterator for ABCIterMut<'a> { type Item = &'a mut [i32; 3]; fn next(&mut self) -> Option<Self::Item> { let value = unsafe { match self.index { 0 => &mut (*self.abc).a, 1 => &mut (*self.abc).b, 2 => &mut (*self.abc).c, _ => return None } }; self.index += 1; Some(value) } } fn main(){ let mut abc = ABC{a: [1; 3], b: [2;3], c: [3;3]}; let mut abc_iter_mut = abc.iter_mut(); // 因为实现 Copy,迭代器这里会隐式复制,返回结构体内部字段可变借用 for i in abc_iter_mut { // Do Something } // 因为实现 Copy,迭代器这里会隐式复制,返回结构体内部字段可变借用 // 存在意外风险而不容易被发现 for i in abc_iter_mut { // Do Something } }
另外,对于标准库里的 Range<T>
就不能实现 Copy,因为它也是一个迭代器。
细节可以参考来自官方讨论: https://github.com/rust-lang/rust/pull/27186#issuecomment-123390413
【正例】
#![warn(clippy::copy_iterator)] use std::marker::PhantomData; #[derive(Debug)] struct ABC { a: [i32; 3], b: [i32; 3], c: [i32; 3], } impl ABC { fn iter_mut(&mut self) -> ABCIterMut { ABCIterMut { abc: self, index: 0, _phantom: PhantomData::default(), } } } // 符合:不实现 Copy // 在需要的时候只实现 Clone #[derive(Clone)] struct ABCIterMut<'a> { abc: *mut ABC, index: u8, _phantom: PhantomData<&'a mut ABC>, } impl<'a> Iterator for ABCIterMut<'a> { type Item = &'a mut [i32; 3]; fn next(&mut self) -> Option<Self::Item> { let value = unsafe { match self.index { 0 => &mut (*self.abc).a, 1 => &mut (*self.abc).b, 2 => &mut (*self.abc).c, _ => return None } }; self.index += 1; Some(value) } } fn main(){ let mut abc = ABC{a: [1; 3], b: [2;3], c: [3;3]}; let mut abc_iter_mut = abc.iter_mut(); // 因为只实现了 Clone // 需要用多次迭代器的时候,显式调用 clone() 方法, // 开发者可以明确地知道自己在做什么 for i in abc_iter_mut.clone() { println!("{i:?}"); } for i in abc_iter_mut { println!("{i:?}") } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
copy_iterator | yes | no | pedantic | allow |
G.TRA.BLN.03 能使用派生宏(Derive)自动实现Default
特质就不要用手工实现
【级别】 建议
【描述】
手工实现 Default,代码不精炼。
【反例】
#![allow(unused)] fn main() { struct Foo { bar: bool } // 不符合 impl std::default::Default for Foo { fn default() -> Self { Self { bar: false } } } }
【正例】
#![allow(unused)] fn main() { // 符合 #[derive(Default)] struct Foo { bar: bool } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
derivable_impls | yes | no | complexity | warn |
该lint不能用于检测泛型参数类型的 Default 手工实现。
G.TRA.BLN.04 在使用#[derive(Hash)]
的时候,避免再手工实现 PartialEq
【级别】 要求
【描述】
实现 Hash 和 Eq 必须要满足下面一个等式:
k1 == k2 -> hash(k1) == hash(k2)
即,当k1
和 k2
相等时,hash(k1)
也应该和 hash(k2)
相等。 所以要求 PartialEq
/ Eq
/ Hash
的实现必须保持一致。
如果用 #[derive(Hash)]
的时候,搭配了一个手工实现的 PartialEq
就很可能出现不一致的情况。
但也有例外。
【反例】
#![allow(unused)] fn main() { #[derive(Hash)] struct Foo; // 不符合 impl PartialEq for Foo { ... } }
【正例】
#![allow(unused)] fn main() { // 符合 #[derive(PartialEq, Eq, Hash)] struct Foo; }
【例外】
#![allow(unused)] fn main() { // From: https://docs.rs/crate/blsttc/3.3.0/source/src/lib.rs // Clippy warns that it's dangerous to derive `PartialEq` and explicitly implement `Hash`, but the // `pairing::bls12_381` types don't implement `Hash`, so we can't derive it. #![allow(clippy::derive_hash_xor_eq)] }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
derive_hash_xor_eq | yes | no | correctness | deny |
G.TRA.BLN.05 在使用#[derive(Ord)]
的时候,避免再手工实现 PartialOrd
【级别】 要求
【描述】
跟实现 Hash 和 Eq 的要求类似,对于实现 Ord
的类型来说,必须要满足下面一个等式:
k1.cmp(&k2) == k1.partial_cmp(&k2).unwrap()
所以要求与 PartialOrd
的实现必须保持一致,并确保max
、min
和clamp
与cmp
一致。
通过#[derive(Ord)]
并手动实现PartialOrd
,很容易意外地使cmp和partial_cmp不一致。
但也有例外。
【反例】
#![allow(unused)] fn main() { #[derive(Ord, PartialEq, Eq)] struct Foo; // 不符合 impl PartialOrd for Foo { ... } }
【正例】
#![allow(unused)] fn main() { // 符合 #[derive(Ord, PartialOrd, PartialEq, Eq)] struct Foo; // 符合 #[derive(PartialEq, Eq)] struct Foo; impl PartialOrd for Foo { fn partial_cmp(&self, other: &Foo) -> Option<Ordering> { Some(self.cmp(other)) } } impl Ord for Foo { ... } }
【例外】
使用 #[derive(PartialOrd)]
自动实现 PartialOrd
,然后再手工实现 Ord
的时候在内部调用自动实现的partial_cmp
,应该是满足 k1.cmp(&k2) == k1.partial_cmp(&k2).unwrap()
了。
#![allow(unused)] fn main() { // From: https://docs.rs/crate/adventjson/0.1.1/source/src/lib.rs #[derive(Clone, Debug, PartialEq, PartialOrd)] pub enum JsonObject { /// An array of objects (e.g.: \[1,2,3\]) Array(Vec<Self>), /// Key-value pairs (e.g.: {\"first\": 10, \"other\": 15}) Obj(Vec<(String, Self)>), /// A number (e.g.: -0.08333) Number(f64), /// A string (e.g.: \"Test: \\\"\") JsonStr(String), /// A boolean (e.g. true) Bool(bool), /// The null-value Null, } /// Save because no not-number values are allowed in json impl Eq for JsonObject {} /// Save because no not-number values are allowed in json #[allow(clippy::derive_ord_xor_partial_ord)] impl Ord for JsonObject { fn cmp(&self, other: &Self) -> Ordering { self.partial_cmp(other).unwrap() } } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
derive_ord_xor_partial_ord | yes | no | correctness | deny |
G.TRA.BLN.06 不要对实现 Copy
或引用类型调用 std::mem::drop
和 std::mem::forgot
【级别】 要求
【描述】
std::mem::drop
函数只是利用 Rust 所有权的一个技巧,对于 实现了 Copy 的类型 或引用,是无效的。如果使用它,对导致代码可读方便产生误导作用。
另外std::mem::drop
也无法 Drop 掉 ManuallyDrop
类型。
std::mem::forgot
同理。
但是也存在例外的情况。
【反例】
#![allow(unused)] fn main() { // 不符合 let x: i32 = 42; // i32 implements Copy std::mem::drop(x) // A copy of x is passed to the function, leaving the // original unaffected }
【例外】
在某些情况下,虽然不会有实际效果,但是为了提升语义,也可以使用。
下面代码中,为了防止自引用的问题,使用 drop(self)
,提升了代码语义,但实际并不会 drop。
#![allow(unused)] fn main() { // From: https://docs.rs/crate/dhall/0.10.1/source/src/error/builder.rs #[allow(clippy::drop_ref)] pub fn format(&mut self) -> String { if self.consumed { panic!("tried to format the same ErrorBuilder twice") } let this = std::mem::take(self); self.consumed = true; drop(self); // 显式 drop self,避免后面误用它 // ... } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
drop_copy | yes | no | correctness | deny |
drop_ref | yes | no | correctness | deny |
forget_copy | yes | no | correctness | deny |
forget_ref | yes | no | correctness | deny |
undropped_manually_drops | yes | no | correctness | deny |
G.TRA.BLN.07 对实现 Copy
的可迭代类型来说,要通过迭代器拷贝其所有元素时,应该使用 copied
方法,而非cloned
【级别】 建议
【描述】
copied
方法在语义层面,是针对实现 Copy
的类型,所以应该使用 copied
来增加代码可读性。
【反例】
#![allow(unused)] #![warn(clippy::cloned_instead_of_copied)] fn main() { let a = [1, 2, 3]; // 不符合 let v_copied: Vec<_> = a.iter().cloned().collect(); }
【正例】
#![allow(unused)] #![warn(clippy::cloned_instead_of_copied)] fn main() { let a = [1, 2, 3]; // 符合 let v_copied: Vec<_> = a.iter().copied().collect(); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
cloned_instead_of_copied | yes | no | pedantic | allow |
G.TRA.BLN.08 实现 From
而不是 Into
【级别】 建议
【描述】
优先为类型实现 From
而非 Into
。因为实现了 From
,Into
也会被自动实现。并且在错误处理的时候,?
操作符会通过调用 From
实现自动进行错误类型转换。
但是在泛型限定上,优先 Into
。
当然,也存在例外。
【反例】
#![allow(unused)] fn main() { struct StringWrapper(String); // 不符合 impl Into<StringWrapper> for String { fn into(self) -> StringWrapper { StringWrapper(self) } } }
【正例】
#![allow(unused)] fn main() { struct StringWrapper(String); // 符合 impl From<String> for StringWrapper { fn from(s: String) -> StringWrapper { StringWrapper(s) } } }
【例外】
有两类情况,可以直接实现 Into
。
Into
不提供From
实现。在一些场景中,From
自动实现的Into
并不符合转换需求。- 使用
Into
来跳过孤儿规则。
#![allow(unused)] fn main() { // 第一种情况。 // From: https://github.com/apache/arrow-datafusion/blob/master/ballista/rust/core/src/serde/scheduler/from_proto.rs #[allow(clippy::from_over_into)] impl Into<PartitionStats> for protobuf::PartitionStats { fn into(self) -> PartitionStats { PartitionStats::new( foo(self.num_rows), foo(self.num_batches), foo(self.num_bytes), ) } } // From: https://github.com/apache/arrow-datafusion/blob/master/ballista/rust/core/src/serde/scheduler/to_proto.rs #[allow(clippy::from_over_into)] impl Into<protobuf::PartitionStats> for PartitionStats { fn into(self) -> protobuf::PartitionStats { let none_value = -1_i64; protobuf::PartitionStats { num_rows: self.num_rows.map(|n| n as i64).unwrap_or(none_value), num_batches: self.num_batches.map(|n| n as i64).unwrap_or(none_value), num_bytes: self.num_bytes.map(|n| n as i64).unwrap_or(none_value), column_stats: vec![], } } } // 第二种情况 // 根据孤儿规则,trait 和 类型必须有一个在本地定义,所以不能为 Vec<T> 实现 From trait struct Wrapper<T>(Vec<T>); impl<T> From<Wrapper<T>> for Vec<T> { fn from(w: Wrapper<T>) -> Vec<T> { w.0 } } // 但是通过 Into<Vec<T>> ,就可以绕过这个规则 struct Wrapper<T>(Vec<T>); impl<T> Into<Vec<T>> for Wrapper<T> { fn into(self) -> Vec<T> { self.0 } } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
from_over_into | yes | no | style | warn |
G.TRA.BLN.09 一般情况下不要给 Copy 类型手工实现 Clone
【级别】 建议
【描述】
手工为 Copy 类型实现 Clone ,并不能改变 Copy 类型的行为。除非你显式地去调用 clone()
方法。
【反例】
#![allow(unused)] #![warn(clippy::expl_impl_clone_on_copy)] fn main() { #[derive(Copy)] struct Foo; // 不符合 impl Clone for Foo { // .. } }
【正例】
#![allow(unused)] #![warn(clippy::expl_impl_clone_on_copy)] fn main() { // 符合 #[derive(Copy, Clone)] struct Foo; }
【例外】
在有些情况下,需要手动实现 Copy 和 Clone 。 相关 issues : https://github.com/rust-lang/rust/issues/26925
use std::marker::PhantomData; struct Marker<A>(PhantomData<A>); // 如果使用 Derive 自动实现的话,会要求 Marker<A> 里的 A 也必须实现 Clone // 这里通过手工给 Marker<A> 实现 Copy 和 Clone 可以避免这种限制 impl<A> Copy for Marker<A> {} impl<A> Clone for Marker<A> { fn clone(&self) -> Self { *self } } // 不需要给 NoClone 实现 Clone struct NoClone; fn main() { let m: Marker<NoClone> = Marker(PhantomData); let m2 = m.clone(); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
expl_impl_clone_on_copy | yes | no | pedantic | allow |
G.TRA.BLN.10 不要随便使用Deref
特质来模拟继承
【级别】 建议
【描述】
Deref
trait是专门用于实现自定义指针类型而存在的。虽然可以实现 Deref
来达到某种类似于继承的行为,但 Rust 中不推荐这样做。
这是因为 Rust 语言推崇显式的转换,而 Deref
则是 Rust 中为数不多的隐式行为。如果 Deref
被滥用,那么程序中隐式行为可能会增多,隐式的转换是 Bug 的温床。
【反例】
不要像下面这样用Deref
来模拟继承。
use std::ops::Deref; struct Foo {} impl Foo { fn m(&self) { // ... } } struct Bar { f: Foo } impl Deref for Bar { type Target = Foo; fn deref(&self) -> &Foo { &self.f } } fn main() { let bar = Bar { f: Foo {} }; bar.m(); }
trait 对象
trait 对象需要注意 动态安全 (dyn safe),也叫对象安全 (object safe),但官方现在倾向于 动态安全这个术语。
列表
P.TRA.OBJ.01 根据场景合理选择使用trait对象或泛型静态分发
【描述】
trait对象存在一定运行时开销,除非必要,不要滥用,但triat对象也可以避免编译文件大小膨胀。
在性能有严格要求的情况下,可以考虑Enum
或泛型静态分发代替。
【正例】
使用 Enum 代替 trait 对象。 示例来自于 enum_dispatch
trait KnobControl { fn set_position(&mut self, value: f64); fn get_value(&self) -> f64; } struct LinearKnob { position: f64, } struct LogarithmicKnob { position: f64, } impl KnobControl for LinearKnob { fn set_position(&mut self, value: f64) { self.position = value; } fn get_value(&self) -> f64 { self.position } } impl KnobControl for LogarithmicKnob { fn set_position(&mut self, value: f64) { self.position = value; } fn get_value(&self) -> f64 { (self.position + 1.).log2() } } fn main() { // 这里使用 trait 对象 let v: Vec<Box<dyn KnobControl>> = vec![ //set the knobs ]; //use the knobs }
用 Enum 代替:
#![allow(unused)] fn main() { enum Knob { Linear(LinearKnob), Logarithmic(LogarithmicKnob), } impl KnobControl for Knob { fn set_position(&mut self, value: f64) { match self { Knob::Linear(inner_knob) => inner_knob.set_position(value), Knob::Logarithmic(inner_knob) => inner_knob.set_position(value), } } fn get_value(&self) -> f64 { match self { Knob::Linear(inner_knob) => inner_knob.get_value(), Knob::Logarithmic(inner_knob) => inner_knob.get_value(), } } } }
性能有显著提高,但是牺牲了维护成本。可以借助于宏来自动生成相关代码,参见: enum_dispatch
P.TRA.OBJ.02 除非必要,避免自定义虚表
【描述】
trait 对象 dyn Trait
隐藏了复杂而又为危险的虚表实现,为我们提供了简单而又安全的动态分发。手动实现虚表的代码中充斥着大量的 unsafe
,稍有不慎,就会引入 bug 。如无必要,不要自定义虚表。
如果你的设计不能使用标准的 dyn Trait
结构来表达,那么你首先应该尝试重构你的程序,并参考以下理由来决定是否使用自定义的虚表。
- 你想要为一类指针对象实现多态,并且无法忍受多级指针解引用造成的性能开销,参考 RawWaker 与 Bytes。
- 你想要自定义内存布局,比如像 C++ 中虚表一样紧凑的内存结构(虚表指针位于对象内),参考 RawTask。
- 你的 crate 需要在
no_std
环境中使用动态分发,参考 RawWaker 。 - 或者,标准的 trait object 确实无法实现你的需求。
【正例】
来自标准库中 RawWaker 的定义。
#![allow(unused)] fn main() { pub struct RawWaker { /// A data pointer, which can be used to store arbitrary data as required /// by the executor. This could be e.g. a type-erased pointer to an `Arc` /// that is associated with the task. /// The value of this field gets passed to all functions that are part of /// the vtable as the first parameter. data: *const (), /// Virtual function pointer table that customizes the behavior of this waker. vtable: &'static RawWakerVTable, } #[derive(PartialEq, Copy, Clone, Debug)] pub struct RawWakerVTable { /// This function will be called when the [`RawWaker`] gets cloned, e.g. when /// the [`Waker`] in which the [`RawWaker`] is stored gets cloned. /// /// The implementation of this function must retain all resources that are /// required for this additional instance of a [`RawWaker`] and associated /// task. Calling `wake` on the resulting [`RawWaker`] should result in a wakeup /// of the same task that would have been awoken by the original [`RawWaker`]. clone: unsafe fn(*const ()) -> RawWaker, /// This function will be called when `wake` is called on the [`Waker`]. /// It must wake up the task associated with this [`RawWaker`]. /// /// The implementation of this function must make sure to release any /// resources that are associated with this instance of a [`RawWaker`] and /// associated task. wake: unsafe fn(*const ()), /// This function will be called when `wake_by_ref` is called on the [`Waker`]. /// It must wake up the task associated with this [`RawWaker`]. /// /// This function is similar to `wake`, but must not consume the provided data /// pointer. wake_by_ref: unsafe fn(*const ()), /// This function gets called when a [`RawWaker`] gets dropped. /// /// The implementation of this function must make sure to release any /// resources that are associated with this instance of a [`RawWaker`] and /// associated task. drop: unsafe fn(*const ()), } }
来自 Bytes 的示例。
#![allow(unused)] fn main() { pub struct Bytes { ptr: *const u8, len: usize, // inlined "trait object" data: AtomicPtr<()>, vtable: &'static Vtable, } pub(crate) struct Vtable { /// fn(data, ptr, len) pub clone: unsafe fn(&AtomicPtr<()>, *const u8, usize) -> Bytes, /// fn(data, ptr, len) pub drop: unsafe fn(&mut AtomicPtr<()>, *const u8, usize), } }
3.12 错误处理
Rust 为了保证系统健壮性,将系统中出现的非正常情况划分为三大类:
- 失败
- 错误
- 异常
Rust 语言针对这三类非正常情况分别提供了专门的处理方式,让开发者可以分情况去选择。
- 对于失败的情况,可以使用断言工具。
- 对于错误,Rust 提供了基于返回值的分层错误处理方式,比如 Option 可以用来处理可能存在空值的情况,而 Result 就专门用来处理可以被合理解决并需要传播的错误。
- 对于异常,Rust 将其看作无法被合理解决的问题,提供了线程恐慌机制,在发生异常的时候,线程可以安全地退出。
P.ERR.01 当传入函数的参数值因为超出某种限制可能会导致函数调用失败,应该使用断言
【描述】
当传入函数的某个参数值可能因为超出某种限制,比如超出数组长度的索引、字符串是否包含某个字符、数组是否为空等,应该使用断言。
【正例】
#![allow(unused)] fn main() { // From: std::vec::Vec::swap_remove #[stable(feature = "rust1", since = "1.0.0")] pub fn swap_remove(&mut self, index: usize) -> T { #[cold] #[inline(never)] fn assert_failed(index: usize, len: usize) -> ! { panic!("swap_remove index (is {}) should be < len (is {})", index, len); } let len = self.len(); if index >= len { // 此处使用断言方法,虽然不是标准库内置断言宏,但也是一种断言 assert_failed(index, len); } unsafe { let last = ptr::read(self.as_ptr().add(len - 1)); let hole = self.as_mut_ptr().add(index); self.set_len(len - 1); ptr::replace(hole, last) } } }
P.ERR.02 在确定 Option<T>
和 Result<T, E>
类型的值不可能是 None
或 Err
时,请用 expect
代替 unwrap()
【描述】
当需要处理的 Option<T>
和 Result<T, E>
类型的值,永远都不可能是 None
或 Err
时,虽然直接 unwrap()
也是可以的,但使用 expect
会有更加明确的语义。
expect
的语义:我不打算处理
None
或Err
这种可能性,因为我知道这种可能性永远不会发生,或者,它不应该发生。但是 类型系统并不知道它永远不会发生。所以,我需要像类型系统保证,如果它确实发生了,它可以认为是一种错误,并且程序应该崩溃,并带着可以用于跟踪和修复该错误的栈跟踪信息。
所以在指定 expect
输出消息的时候,请使用肯定的描述,而非否定,用于提升可读性。
【反例】
// 这个配置文件默认会跟随源码出现,所以,必定可以读取到 // 这个配置文件不应该没有被提供,如果万一出现了没有提供的情况,需要 Panic ,但这里并没有提供错误信息,对于调试或使用都没有帮助 let config = Config::read("some_config.toml").unwrap(); // or // expect 的输出描述使用否定式内容,可读性不好 let config = Config::read("some_config.toml").expect("No configuration file provided"); // or fn main() { use std::net::IpAddr; let _home: IpAddr = "127.0.0.1".parse().unwrap(); } // or // expect 的输出描述使用否定式内容,可读性不好 fn main() { use std::net::IpAddr; let _home: IpAddr = "127.0.0.1".parse().expect("IP addr parse failed!"); }
【正例】
// 这个配置文件默认会跟随源码出现,所以,必定可以读取到 // 这个配置文件不应该没有被提供,如果万一出现了没有提供的情况,需要 Panic 并提供错误信息方便调试,或者让使用者知道原因 // expect 里输出的描述信息,使用肯定的内容,整体代码可读性更高,更能突出 expect 的语义 let config = Config::read("some_config.toml").expect("Provide the correct configuration file"); // or fn main() { use std::net::IpAddr; let _home: IpAddr = "127.0.0.1".parse().expect("Provide the correct Ip addr"); }
G.ERR.01 在处理 Option<T>
和 Result<T, E>
类型时,不要随便使用 unwrap
【级别】 建议
【描述】
当 Option<T>
和 Result<T, E>
类型的值分别是 None
或 Err
时,直接对其 unwrap()
会导致程序恐慌!
【反例】
#![allow(unused)] #![warn(clippy::unwrap_used)] fn main() { fn select(opt: Option<String>) { opt.unwrap(); // 不符合 } // OR fn select(opt: Result<String, ()>) { res.unwrap(); // 不符合 } }
【正例】
#![allow(unused)] #![warn(clippy::unwrap_used)] fn main() { fn select(opt: Option<String>) { opt.expect("more helpful message"); // 符合:可以用 expect 方法来处理 None 的情况 } // OR fn select(opt: Result<String, ()>) { res.expect("more helpful message"); // 符合:可以用 expect 方法来处理 Err 的情况 } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
unwrap_used | yes | no | restriction | allow |
G.ERR.02 不要滥用 expect
,请考虑用 unwrap_or_
系列方法代替
【级别】 建议
【描述】
使用 expect
的时候请遵循 expect
的语义,不要滥用。
expect
的语义:我不打算处理
None
或Err
这种可能性,因为我知道这种可能性永远不会发生,或者,它不应该发生。但是 类型系统并不知道它永远不会发生。所以,我需要像类型系统保证,如果它确实发生了,它可以认为是一种错误,并且程序应该崩溃,并带着可以用于跟踪和修复该错误的栈跟踪信息。
但是对于一些存在“副作用”的函数,在 遇到 None
或 Err
时,可能需要返回一些指定的值。这个时候用 expect
就不太符合语义。
如果你的用法完全符合 expect
语义,那么可以设置 #![allow(clippy::expect_fun_call]
【反例】
#![warn(clippy::expect_used)] fn main(){ let foo = Some(String::new()); let err_code = "418"; let err_msg = "I'm a teapot"; // 不符合:因为要返回指定的错误码等信息,不适合 expect 语义 foo.expect(&format!("Err {}: {}", err_code, err_msg)); }
【正例】
#![warn(clippy::expect_used)] fn main(){ let foo = Some(String::new()); let err_code = "418"; let err_msg = "I'm a teapot"; // 符合 foo.unwrap_or_else(|| panic!("Err {}: {}", err_code, err_msg)); // 你可以根据场景选择性使用 panic! 或者 不 panic! }
【例外】
完全符合 expect
语义的使用。
#![allow(unused)] #![allow(clippy::expect_fun_call] fn main() { // 这个配置文件默认会跟随源码出现,所以,必定可以读取到 // 这个配置文件不应该没有被提供,如果万一出现了没有提供的情况,需要 Panic 并提供错误信息方便调试,或者让使用者知道原因 let config = Config::read("some_config.toml").expect("Provide the correct configuration file"); // 或者 use std::net::IpAddr; let _home: IpAddr = "127.0.0.1".parse().expect("Provide the correct Ip addr"); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
expect_fun_call | yes | no | perf | warn |
expect_used | yes | no | restriction | allow |
3.13 内存管理
生命周期
生命周期(lifetime),也被叫做 生存期。可以理解为引用的有效范围。
列表
P.MEM.LFT.01 生命周期参数命名尽量有意义且简洁
【描述】
生命周期参数的命名应该尽量简单,可以使用表达一定语义的缩写。
因为生命周期参数的目的是给编译器使用,用于防止函数中出现悬垂引用。
适当简单的携带语义的缩写,可以最小化对业务代码的干扰。并且在生命周期参数较多的情况下,清晰地表达具体哪个引用属于哪个生命周期。
【反例】
#![allow(unused)] fn main() { struct ConstraintGeneration<'a, 'b, 'c> { infcx: &'c InferCtxt<'b, 'c>, all_facts: &'a mut Option<AllFacts>, location_table: &'a LocationTable, liveness_constraints: &'a mut LivenessValues<RegionVid>, borrow_set: &'a BorrowSet<'c>, body: &'c Body<'c>, } }
【正例】
#![allow(unused)] fn main() { // 增加 'cg 意义的文档注释 /// 'cg = the duration of the constraint generation process itself. struct ConstraintGeneration<'cg, 'cx, 'tcx> { infcx: &'cg InferCtxt<'cx, 'tcx>, all_facts: &'cg mut Option<AllFacts>, location_table: &'cg LocationTable, liveness_constraints: &'cg mut LivenessValues<RegionVid>, borrow_set: &'cg BorrowSet<'tcx>, body: &'cg Body<'tcx>, } }
P.MEM.Lifetime.02 通常需要显式地标注生命周期,而非利用编译器推断
【描述】
编译器对生命周期参数有两种单态化方式(生命周期参数也是一种泛型):
- Early bound。一般情况下,
'a: 'b
以及impl<'a>
这种方式是 early bound,意味着这些生命周期参数会在当前作用域单态化生命周期实例。 - Late bound。默认的
'a
或for<'a>
是在实际调用它们的地方才单态化生命周期实例。
在不同的场景下需要指定合适的单态化方式,才能让编译器明白开发者的意图。
在使用匿名生命周期 '_
的时候需要注意,如果有多个匿名生命周期,比如 ('_,'_)
,每个匿名生命周期都会有自己的单独实例。
【反例】
fn main() { let v = vec![1, 2, 3, 4, 5, 6]; let mut buf = Buffer::new(&v); // error[E0499]: cannot borrow `buf` as mutable more than once at a time let b1 = buf.read_bytes(); let b2 = buf.read_bytes(); println!("{:#?} {:#?}", b1, b2); } struct Buffer<'a> { buf: &'a [u8], pos: usize, } impl<'a> Buffer<'a> { fn new(b: &'_ [u8]) -> Buffer { Buffer { buf: b, pos: 0 } } // 不符合:此处依赖编译器推断的生命周期将导致main函数中该方法调用编译错误 fn read_bytes(&'_ mut self) -> &'_ [u8] { self.pos += 3; &self.buf[self.pos - 3..self.pos] } }
【正例】
fn main() { let v = vec![1, 2, 3, 4, 5, 6]; let mut buf = Buffer::new(&v); let b1 = buf.read_bytes(); let b2 = buf.read_bytes(); println!("{:#?} {:#?}", b1, b2); } struct Buffer<'a> { buf: &'a [u8], pos: usize, } // 符合:明确标示清楚生命周期,向编译器传达开发者意图,则可正常编译 impl<'b, 'a: 'b> Buffer<'a> { fn new(b: &'_ [u8]) -> Buffer { Buffer { buf: b, pos: 0 } } // 符合:明确标示清楚输入引用和输出引用的生命周期关系是 `'a: 'b` fn read_bytes(&'b mut self) -> &'a [u8] { self.pos += 3; &self.buf[self.pos - 3..self.pos] } }
智能指针
智能指针,在 Rust 中参与自动管理堆内容、引用计数、抽象指针语义等功能。一般实现了 Deref
trait 或 Drop
trait 的类型都可以看作是一种智能指针。
Box<T>
就是一个典型的智能指针,但是因为其在 Rust 中有特殊地位,所以为其单独罗列规则。
Rust 中常见的智能指针包括:
- 自动管理堆内存:
Box<T>
- 引用计数:
Rc<T> / Arc<T>
- 内部可变性容器:
Cell<T> / RefCell<T>
列表
P.MEM.SPT.01 使用 RefCell<T>
时宜使用 try_borrow/try_borrow_mut
方法
【描述】
Rust 的 RefCell<T>
在运行时会对通过 borrow/borrow_mut
方法借用出去的不可变借用和可变借用进行检查。如果发现违反了借用规则的情况,则会 Panic。
所以在一些多线程场景下,开发者可能对细粒度的操作加了锁同步,但是没有对 RefCell<T>
进行加锁,此时宜用 try_borrow/try_borrow_mut
来代替
borrow/borrow_mut
,以避免在运行时因为违反借用检查规则而出现 Panic。
【反例】
#![allow(unused)] fn main() { // 不符合 // 以下两个函数会让 C 函数在多线程下调用 // 运行过程中有一定几率会出现 Panic pub extern "C" fn nic_udrv_suspend() { NIC_ENTITY.borrow_mut().suspend(); // suspend()需要可变引用 } pub extern "C" fn nic_udrv_buf_recycle(buf_id: usize) { NIC_ENTITY.borrow().buf_recycle(buf_id); // buf_recycle()内有锁可以避免多线程竞争 } }
【正例】
#![allow(unused)] fn main() { // 符合 // 以下两个函数会让 C 函数在多线程下调用 // 使用 try_borrow 或 try_borrow_mut 可以避免运行过程中出现 Panic pub extern "C" fn nic_udrv_suspend() { if let Ok(entity) = NIC_ENTITY.try_borrow_mut() { entity.suspend(); // suspend()需要可变引用 } } pub extern "C" fn nic_udrv_buf_recycle(buf_id: usize) { if let Ok(entity) = NIC_ENTITY.try_borrow() { entity.buf_recycle(buf_id); // buf_recycle()内有锁可以避免多线程竞争 } } }
Box 类型
Rust 中分配堆内存必须要使用的类型,类型签名为 Box<T>
。
列表
- G.MEM.BOX.01 一般情况下,不应直接对
Box<T>
进行借用 - G.MEM.BOX.02 一般情况下,不应直接对已经在堆上分配内存的类型进行 Box 装箱
- G.MEM.BOX.03 一般情况下,不应直接对栈分配类型进行 Box 装箱
G.MEM.BOX.01 一般情况下,不应直接对 Box<T>
进行借用
【级别】 建议
【描述】
借用 Box<T>
等同于直接借用 T
,而 &T
要比 &Box<T>
更常用。
【反例】
#![allow(unused)] fn main() { // 不符合 fn foo(bar: &Box<T>) { ... } }
【正例】
#![allow(unused)] fn main() { // 符合 fn foo(bar: &T) { ... } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
borrowed_box | yes | no | complexity | warn |
G.MEM.BOX.02 一般情况下,不应直接对已经在堆上分配内存的类型进行 Box
装箱
【级别】 建议
【描述】
像 Vec<T> / String
这样的类型(数据集)已经在堆上分配了内存,就没有必要再使用 Box
对其进行装箱操作。当然也存在例外情况。
【反例】
#![allow(unused)] fn main() { struct X { // 不符合:Vec已在堆上分配了内存 values: Box<Vec<Foo>>, } }
【正例】
#![allow(unused)] fn main() { struct X { // 符合 values: Vec<Foo>, } }
【例外】
用例来源:jex
#![allow(unused)] fn main() { #[derive(Debug)] pub struct JQ { ptr: *mut jq_state, // We want to make sure the vec pointer doesn't move, so we can keep pushing to it. // 这里不想把 Vec 的指针 Move 掉,所以用 Box 装箱可以达到这个效果 // 注:clippy::box_vec 以更名为 clippy::box_collection #[allow(clippy::box_vec)] errors: Box<Vec<JVRaw>>, } }
或以下情况。来源 mmtk-core
#![allow(unused)] fn main() { #[repr(C)] pub struct MutatorConfig<VM: VMBinding> { // ... /// Mapping between allocator selector and spaces. Each pair represents a mapping. /// Put this behind a box, so it is a pointer-sized field. // 这里是为了让字段拥有指针一样的大小,所以装箱了 #[allow(clippy::box_collection)] pub space_mapping: Box<SpaceMapping<VM>>, // ... } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
box_collection | yes | no | perf | warn |
G.MEM.BOX.03 一般情况下,不应直接对栈分配类型进行 Box
装箱
【级别】 建议
【描述】
此举会对性能造成不必要的影响。只有当某个栈变量太大,需要使用堆分配的情况下,或是栈变量需要逃逸的时候,才需要考虑是否对其使用 Box
装箱。
【反例】
#![allow(unused)] fn main() { fn foo(bar: usize) {} // 不符合 let x = Box::new(1); foo(*x); println!("{}", *x); }
【正例】
#![allow(unused)] fn main() { fn foo(bar: usize) {} // 符合 let x = 1; foo(x); println!("{}", x); }
【例外】
用例来源:aitch
#![allow(unused)] fn main() { pub trait ServeFunc { fn call_box(self: Box<Self>) -> Result<()>; } impl<F> ServeFunc for F where F: FnOnce() -> Result<()>, { // 特殊情况,F 是泛型,且要匹配 trait定义 #[cfg_attr(feature = "cargo-clippy", allow(boxed_local))] fn call_box(self: Box<Self>) -> Result<()> { (*self)() } } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
boxed_local | yes | no | perf | warn |
Drop 析构
在 Safe Rust 中,Drop 比较安全。在 Unsafe Rust 中则需要注意更多关于 Drop 的问题。
列表
P.MEM.DRP.01 要注意防范内存泄漏
【描述】
Rust 语言并不保证避免内存泄漏,内存泄漏不属于 Rust 安全职责范围。使用 Rust 的时候需要注意下面情况可能会发生内存泄漏:
- 循环引用导致没有正常调用析构函数
- 使用
forget
/leak
等函数主动跳过析构 - 使用
std::mem::ManuallyDrop
构建数据结构而忘记析构 - 析构函数内部发生了 panic
- 程序中止(abort on panic)
3.14 模块
Rust 中一个文件 即一个模块,也可以通过 mod
来创建模块。多个文件放到同一个目录下,也可以成为一个模块。
模块相关有三个概念:
mod
是 Rust 代码的“骨架”。use
则是用来决定使用或导出哪个模块中的具体的类型或方法。Path
,则是一个命名系统,类似于命名空间。
P.MOD.01 合理控制对外接口和模块之间的可见性
【描述】
Rust提供强大的模块(module)系统,并且可以管理这些模块之间的可见性(公有(public)或私有(private))。
1、对于提供给其他crate使用的对外函数、结构体、trait等类型需要严格控制对外pub的范围,避免将内部成员对外提供。
2、对于crate内部,mod之间可见的类型,需要添加上pub(crate)
。
3、对于mod内部私有的类型,不要添加pub(crate)
或者pub
。
【正例】
#![allow(unused)] fn main() { // lib.rs pub mod sha512; pub use sha512::Sha512; // sha512.rs pub struct Sha512 { inner: Sha512Inner, // inner作为内部结构体,不添加pub描述 } }
P.MOD.02 将模块的测试移动到单独的文件,有助于增加编译速度
【描述】
将模块的测试代码 移到一个单独的文件中,并且用 `#[cfg(test)] 来条件编译 tests 的mod,这样可以减少rebuild和编译时间,在大型项目中很重要。
【正例】
#![allow(unused)] fn main() { src/ |--codes.rs |--codes/test.rs }
G.MOD.01 使用导入模块中的类型或函数,在某些情况下需要带模块名前缀
【级别】 建议
【描述】
对于标准库中,很多人都熟知的类型 ,比如 Arc
/ Rc
/ Cell
/ HashMap
等 , 可以导入它们直接使用。
但是对于可能引起困惑的函数,比如 std::ptr::replace
和 std::mem::replace
,在使用它们的时候,就必须得带上模块前缀。
使用一些第三方库中定义的类型或函数,也建议带上crate或模块前缀。如果太长的话,可以考虑使用 as
或 type
来定义别名。
以上考虑都是为了增强代码的可读性、可维护性。
【正例】
#![allow(unused)] fn main() { use std::sync::Arc; let foo = Arc::new(vec![1.0, 2.0, 3.0]); // 直接使用 Arc let a = foo.clone(); // 需要带上 ptr 前缀 use std::ptr; let mut rust = vec!['b', 'u', 's', 't']; // `mem::replace` would have the same effect without requiring the unsafe // block. let b = unsafe { // 符合 ptr::replace(&mut rust[0], 'r') }; }
G.MOD.02 如果是作为库供别人使用,在 lib.rs
中重新导出对外类型、函数和 trait 等
【级别】 建议
【描述】
这样使用方在使用的时候,就不需要use crate::mod::mod::struct
,可以直接使用use crate::struct
,好处是使用方use
的时候会比较方便和直观。
【正例】
#![allow(unused)] fn main() { // 符合 // From syn crate pub use crate::data::{ Field, Fields, FieldsNamed, FieldsUnnamed, Variant, VisCrate, VisPublic, VisRestricted, Visibility, }; }
G.MOD.03 导入模块不要随便使用 通配符*
【级别】 建议
【描述】
使用通配符导入会污染命名空间,比如导入相同命名的函数或类型。
【反例】
#![allow(unused)] #![warn(clippy::wildcard_imports)] fn main() { use crate2::*; // Has a function named foo foo(); // Calls crate1::foo }
【正例】
#![allow(unused)] #![warn(clippy::wildcard_imports)] fn main() { use crate1::foo; // Imports a function named foo foo(); // Calls crate1::foo }
【例外】
#![allow(unused)] fn main() { use prelude::*; #[test] use super::* }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
wildcard_imports | yes | no | pedantic | allow |
该 lint 可以通过 clippy 配置项 warn-on-all-wildcard-imports = false
来配置,用于是否禁用 prelude
/ super
(测试模块中) 使用通配符导入, 默认是 false
。
G.MOD.04 一个项目中应该避免使用不同的模块布局风格
【级别】 建议
【描述】
Rust 支持两种 模块布局,文件夹内使用 mod.rs
或者是使用跟文件夹同名的文件名,来组织模块。
但是项目里如果混合这两种模块布局,是比较让人困惑的,最好统一为同一种风格。
上面两种 lint ,选择其中一种用于检查是否存在不同的模块布局。
【反例】
#![allow(unused)] #![warn(clippy::self_named_module_files, clippy::mod_module_files)] fn main() { // 不符合:使用 `self_named_module_files`,不允许下面模块布局 #![warn(clippy::self_named_module_files)] src/ stuff/ stuff_files.rs stuff.rs lib.rs // 不符合:使用 `mod_module_files`,不允许下面模块布局 #![warn(clippy::mod_module_files)] src/ stuff/ stuff_files.rs mod.rs lib.rs }
【正例】
#![allow(unused)] fn main() { // 符合:使用 `self_named_module_files`,允许下面模块布局 #![warn(clippy::self_named_module_files)] src/ stuff/ stuff_files.rs mod.rs lib.rs // 符合:使用 `mod_module_files`,允许下面模块布局 #![warn(clippy::mod_module_files)] src/ stuff/ stuff_files.rs stuff.rs lib.rs }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
self_named_module_files | yes | no | restriction | allow |
mod_module_files | yes | no | restriction | allow |
G.MOD.05 不要在私有模块中设置其内部类型或函数方法为 pub(crate)
【级别】 建议
【描述】
如果在私有模块中设置 pub(crate)
可能会让使用者产生误解。建议用 pub
代替。
【反例】
#![allow(unused)] fn main() { mod internal { // 不符合 pub(crate) fn internal_fn() { } } }
【正例】
#![allow(unused)] fn main() { // 符合 mod internal { // 此函数在模块外部不可见,可以使用 pub 或 继续保持私有 pub fn internal_fn() { } } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
redundant_pub_crate | yes | no | nursery | allow |
3.15 包管理
Cargo 不仅仅是包管理,它还是一个 Workflow 工具。这一节包含 Cargo 和 Crate 相关内容。
P.CAR.01 应该尽量把项目划分为合理的 crate 组合
【描述】
将整个项目按一定逻辑划分为合理的 crate,在工程方面有利于组件化。并且 crate 是 Rust 的编译单元,也有助于提升编译速度。
但需要注意,crate 之间的依赖关系应该是单向的,避免相互依赖的情况。
但 Rust 中编译时间、性能、编译大小之间,在考虑优化的时候也是需要权衡的。
内联是优化的关键,当编译单元越大,内联优化效果就越好。所以需要权衡 crate 划分的粒度。
P.CAR.02 不要滥用 Features
【描述】
Rust 的 features 提供了方便的条件编译功能。从软件工程来说,features 适合应用于可选功能。
在使用 features 的时候,应该考虑到底是不是真的需要 features。
滥用 features 会带来额外的测试和静态检查的难度,需要保证不同 features 下的测试覆盖和静态检查情况。
P.CAR.03 使用 cargo features
来代替 --cfg
条件编译参数
【描述】
cargo features
为 Rust 原生的条件编译,可用于代替 --cfg
参数且兼容性更好。
P.CAR.04 宜使用 cfg!
来代替 #[cfg]
【描述】
cfg!
和正常代码一样,会检查全部函数逻辑,而 #[cfg]
是条件编译,则会跳过一些 Dead Code。
G.CAR.01 当项目是可执行程序而非库时,建议使用 src/main.rs
和 src/lib.rs
模式
【级别】 建议
【描述】
crate
结构类似于:
src/
-- lib.rs
-- main.rs
或
src/
-- lib.rs
bin/
-- main.rs
这样的好处有:
- 便于单元测试。
- 有利于面向接口思考,让代码架构和逻辑更加清晰。
若编写的可执行程序比较复杂,在 main.rs
里需要依赖太多东西时,那就需要创建 Workspace 把 main.rs
独立为一个 crate,而在这个 crate 内也没有必要再拆分为 main
和 lib
了。
G.CAR.02 Crate 的 Cargo.toml 中应该包含必要的元信息
【级别】 建议
【描述】
在 Cargo.toml 中应该包含必要的元信息,以便使用者知道它的作用。
此外,若要将 crate
发布到 crates.io 上的话,这些信息也是必须的。可参考 The Cargo Book 中的相关介绍。
【反例】
# 不符合:此 `Cargo.toml` 缺失介绍(description)项。无法发布到 crates.io。
[package]
name = "clippy"
version = "0.0.212"
repository = "https://github.com/rust-lang/rust-clippy"
readme = "README.md"
license = "MIT OR Apache-2.0"
keywords = ["clippy", "lint", "plugin"]
categories = ["development-tools", "development-tools::cargo-plugins"]
【正例】
# 符合:此 `Cargo.toml` 包含必要元信息。
[package]
name = "clippy"
version = "0.0.212"
description = "A bunch of helpful lints to avoid common pitfalls in Rust"
repository = "https://github.com/rust-lang/rust-clippy"
readme = "README.md"
license = "MIT OR Apache-2.0"
keywords = ["clippy", "lint", "plugin"]
categories = ["development-tools", "development-tools::cargo-plugins"]
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
cargo_common_metadata | yes | no | cargo | allow |
G.CAR.03 Feature 命名应该避免否定式或多余的前后缀
【级别】 建议
【描述】
Feature 命名应该避免出现 no-
或 not-
之类的否定前缀,或诸如 use-
,with-
前缀或 -support
后缀。Feature 的目的是正向的,可选的特性,使用否定式命名和它的目的背道而驰。
【反例】
[features]
default = ["no-abc", "with-def", "ghi-support"]
no-abc = [] # 不符合:命名否定式
with-def = [] # 不符合:多余前缀
ghi-support = [] # 不符合:多余后缀
【正例】
# 符合
[features]
default = ["abc", "def", "ghi"]
abc = []
def = []
ghi = []
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
negative_feature_names | yes | no | cargo | allow |
redundant_feature_names | yes | no | cargo | allow |
G.CAR.04 Cargo.toml 中依赖包版本不应使用通配符
【级别】 要求
【描述】
依赖的包必须指定具体的语义版本。关于语义版本说明参见:The Cargo Book: SemVer Compatibility。
使用 Clippy 需要设置
#[warn(clippy::wildcard_dependencies)]
。
【反例】
[dependencies]
regex = "*" # 不符合:避免项目依赖因为上游更新而自动更新
【正例】
[dependencies]
regex = "1.5" # 符合
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
wildcard_dependencies | yes | no | cargo | allow |
3.16 宏
Rust 通过宏来支持元编程。其中宏有很多种,按实现方式可以分为两大类:声明宏(Declarative)和 过程宏(Procedural)。
按功能效果,过程宏又可以分为三类:
- Function-like 宏。类似于声明宏那样,像函数调用一样去使用的宏。
- Derive 宏。用于为数据类型自动生成一些 语法项(item),比如 trait 、结构体、方法等。
- Attribute 宏。用于更加通用的代码生成功能。
Rust 语言核心库和标准库都内置了一些声明宏和过程宏,以方便开发者使用。
内置的属性宏按功能大体又可以分为四类:
注:属性宏固定语法为
#[attr]
或#![attr]
,以下使用用例均已简化为attr
的形式。即test
,allow(c)
代表其在 Rust 内的实现可分别表现为#[test]
及#[allow(c)]
。
- 测试属性。
test
属性宏用于将某个函数标记为单元测试函数。 - 诊断(Diagnostic)属性。用于在编译过程中控制和生成诊断信息。包括:
allow(c)
/warn(c)
/deny(c)
/forbid(c)
等。must_use
。
- 代码生成属性。包括:
inline
/cold
/target_feature
等。 - 编译时限制属性。包括:
recursion_limit
/type_length_limit
。 - 类型系统属性。包括:
non_exhaustive
。
宏编程规范:
使用宏时,需要从 声明宏
和 过程宏
各自的特性为出发点,来安全使用它。
宏展开命令:
# 对单个 rs 文件
rustc -Z unstable-options --pretty expanded hello.rs
# 对项目里的二进制 rs 文件
cargo rustc --bin hello -- -Z unstable-options --pretty=expanded
P.MAC.01 不要轻易使用宏
【描述】
当一个开发者想要能写出强大且用户友好的宏API时,不仅需要掌握如何用宏去实现,更需要掌握宏之外关于 Rust 的一切。
宏设计的重点在于宏生成什么样的代码,而不是宏如何生成代码。
宏只是将 Rust 语言特性以一种有趣的方式组合在一起能自动生成代码的创造力。
尤其是过程宏,它有一定复杂性,且很难调试,不卫生,也容易出错,不适用于新手。
"卫生" 这个词表示,宏展开后,不会污染原来的词法作用域。
【参考】
Rust 社区顶级专家 David Tolnay 写的宏学习案例
P.MAC.02 实现宏语法的时候,应该尽量贴近 Rust 语法
【描述】
Rust 宏可以让开发者定义自己的 DSL,但是在使用宏的时候,要尽可能贴近 Rust 的语法。这样可以增强可读性,让其他开发者在使用宏的时候,可以猜测出它生成的代码。
【反例】
#![allow(unused)] fn main() { // 不符合:无关键词 bitflags! { S: u32 { /* ... */ } } // 不符合:或使用一些自定义的特定用途关键词 bitflags! { flags S: u32 { /* ... */ } } // 或 bitflags! { struct S: u32 { const E = 0b010000, // 不符合:结尾应该是分号更符合 Rust 语法 const F = 0b100000, } } }
【正例】
#![allow(unused)] fn main() { bitflags! { struct S: u32 { /* ... */ } // 符合 } // 符合:结尾是正确的分号 bitflags! { struct S: u32 { const C = 0b000100; const D = 0b001000; } } }
G.MAC.01 dbg!()
宏只应该用于调试代码
【级别】 建议
【描述】
dbg!()
宏是 Rust 内置的宏,其目的是用于调试代码。 不要将含有 dbg! 宏的代码加入到版本控制下。
注意:不管在 Debug 模式还是 Release 模式下,调试信息都会被打印出来。
【反例】
#![allow(unused)] #![warn(clippy::dbg_macro)] fn main() { // 不符合:代码加入版本控制时还保留着 dbg! 代码 let foo = false; dbg!(foo); }
【正例】
#![allow(unused)] #![warn(clippy::dbg_macro)] fn main() { // 符合:代码加入版本控制时注释掉 dbg! 代码 let foo = false; // dbg!(foo); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
dbg_macro | yes | no | restriction | allow |
G.MAC.02 使用宏时应该考虑宏展开会让编译文件膨胀的影响
【级别】 建议
【描述】
在多个地方使用 println!
或 panic!
之类的内置宏时,可以将其包装到函数内,使用 #[cold]
和 #[inline(never)]
属性避免其内联,从而避免编译文件膨胀。
因为像 println!
或 panic!
之类的宏,如果到处使用,就会到处展开代码,会导致编译文件大小膨胀。尤其在嵌入式领域需要注意。
【反例】
#![allow(unused)] fn main() { pub fn expect(self, msg: &str) -> T { match self { Ok(t) => t, Err(e) => panic!("{}: {:?}", msg, &e), // 不符合 } } pub fn unwrap_err(self) -> E { match self { Ok(t) => panic!("{}: {:?}", "called `Result::unwrap_err()` on an `Ok` value", &t), // 不符合 Err(e) => e, } } }
【正例】
#![allow(unused)] fn main() { #[inline(never)] #[cold] #[track_caller] // 为了定位 panic 发生时的调用者的位置 fn unwrap_failed(msg: &str, error: &dyn fmt::Debug) -> ! { panic!("{}: {:?}", msg, error) } pub fn expect(self, msg: &str) -> T { match self { Ok(t) => t, Err(e) => unwrap_failed(msg, &e), // 符合 } } pub fn unwrap_err(self) -> E { match self { Ok(t) => unwrap_failed("called `Result::unwrap_err()` on an `Ok` value", &t), // 符合 Err(e) => e, } } }
声明宏
声明宏 也被叫做 示例宏(macros by example),或者简单地叫做 宏。目前声明宏使用 macro_rules!
来定义。
声明宏的特点是:它只用作代码替换,而无法进行计算。
列表
- P.MAC.DCL声明宏内的变量作为外部变量使用
- P.MAC.DCL.02 在编写多个宏规则时,应该先从匹配粒度最小的开始写
- P.MAC.DCL.03 不要在片段分类符跟随它不匹配的符号
- P.MAC.DCL.04 匹配规则要精准,不要模糊不清
- P.MAC.DCL.05 使用宏替换(substitution)元变量的时候要注意选择合适的片段分类符
- P.MAC.DCL.06 当宏需要接收 self 时需要注意
- P.MAC.DCL.07 确保在宏定义之后再去调用宏
- P.MAC.DCL.08 同一个 crate 内定义的宏相互调用时,需要注意卫生性
P.MAC.DCL.01 不要将声明宏内的变量作为外部变量使用
【描述】
声明宏是半卫生(semi-hygienic)宏,其内部元变量(metavariables)不可作为外部变量去使用。但是对于泛型参数(包括生命周期参数)是不卫生的,所以要小心使用。
【反例】
下面为卫生场景示例:
macro_rules! using_a { ($e:expr) => { { let a = 42; $e } } } fn main() { let four = using_a!(a / 10); // build error: cannot find value `a` in this scope }
下面为不卫生场景示例:
trait FooTrait { fn get(&self) -> i32; } // 使用宏为带生命周期的类型实现 FooTrait macro_rules! impl_FooTrait { ($name:ty) => { // 这里使用的 'a 是宏内部定义 impl<'a> $crate::FooTrait for $name { fn get(&self) -> i32 { *self.0 } } }; } struct Baz<'a>(&'a i32); impl_FooTrait!(Baz<'a>); // 这里的 'a 是宏外部 // 整个程序正常编译运行,说明宏内外的 'a 被共用,不卫生 fn main() { let val = 20; let baz = Baz(&val); method(&baz); } // 测试实现 FooTrait 的方法 fn method(foo: &dyn FooTrait) { println!("{:?}", foo.get()); }
【正例】
下面为卫生场景示例:
macro_rules! using_a { ($a:ident, $e:expr) => {{ let $a = 42; $e }}; } fn main() { let four = using_a!(a, a / 10); }
下面为不卫生场景示例:
trait FooTrait { fn get(&self) -> i32; } // 使用宏为带生命周期的类型实现 FooTrait macro_rules! impl_FooTrait { // 这里不直接使用宏内部定义的 'a ,而从外面通过 $lifetime 传入 // 是为了避免不卫生可能引发的问题 ($name:ty, $lifetime:tt) => { impl<$lifetime> $crate::FooTrait for $name { fn get(&self) -> i32 { *self.0 } } }; } struct Baz<'a>(&'a i32); impl_FooTrait!(Baz<'a>, 'a); // 这里的 'a 是从宏外部传入到宏内 // 整个程序正常编译运行 fn main() { let val = 20; let baz = Baz(&val); method(&baz); } // 测试实现 FooTrait 的方法 fn method(foo: &dyn FooTrait) { println!("{:?}", foo.get()); }
P.MAC.DCL.02 在编写多个宏规则时,应该先从匹配粒度最小的开始写
【描述】
因为声明宏中,是按规则的编写顺序来匹配的。当第一个规则被匹配到,后面的规则将永远不会匹配到。所以,编写声明宏规则时,需要先写匹配粒度最小的,最具体的规则,然后逐步编写匹配范围更广泛的规则。
【正例】
#![allow(unused)] fn main() { #[macro_export] macro_rules! foo { (@as_expr $e:expr) => {$e}; // expr 比 tt 匹配更加具体 ($($tts:tt)*) => { foo!(@as_expr $($tts)*) }; } }
P.MAC.DCL.03 不要在片段分类符跟随它不匹配的符号
【描述】
macro_rules!
定义声明宏时,非终止的元变量匹配必须紧随一个已被决定能在这种匹配之后安全使用的标记。
具体的规则参见:Follow-set Ambiguity Restrictions
片段分类符(fragment-specifier)的说明参见附录B:术语解释
【反例】
对于 [,]
这样的分隔标记就是非法的。这是为了防止未来 Rust 语法变动导致宏定义失效。
#![allow(unused)] fn main() { #[macro_export] macro_rules! foo { ($e1:expr [,] $e2:expr) => {$e1; $e2}; } }
【正例】
该示例中,元变量 $e1
的 片段分类符 expr
是非终止的,所以后面需要跟随一个用于分隔的标记。
Rust 规定在 expr
片段分类符 后面可以合法地跟随 =>
/ ,
/ ;
。
#![allow(unused)] fn main() { #[macro_export] macro_rules! foo { ( $e1:expr, $e2:expr) => {$e1; $e2}; } }
P.MAC.DCL.04 匹配规则要精准,不要模糊不清
【描述】
匹配规则必须精准,因为宏解析器并不会去执行代码,它无法匹配模糊不清的规则。
【反例】
宏解析器无法确定第一次匹配的应该是多少个 ident
。
macro_rules! ambiguity { ($($i:ident)* $i2:ident) => { }; } // error: // local ambiguity: multiple parsing options: built-in NTs ident ('i') or ident ('i2'). fn main() { ambiguity!(an_identifier); }
【正例】
macro_rules! ambiguity { ($i2:ident $($i:ident)* ) => { }; } // ok fn main() { ambiguity!(an_identifier an_identifier2); }
P.MAC.DCL.05 使用宏替换(substitution)元变量的时候要注意选择合适的片段分类符
【描述】
使用宏替换(substitution)元变量,就是指把已经进行过宏解析的 token 再次传给宏,需要注意此时传入的 token 已经被看作是宏解析器解析后的 AST 节点了。
片段分类符介绍(fragment-specifier)
【反例】
macro_rules! capture_then_what_is { (#[$m:meta]) => {what_is!(#[$m])}; // 这里片段分类符用的是 meta } macro_rules! what_is { (#[no_mangle]) => {"no_mangle attribute"}; (#[inline]) => {"inline attribute"}; ($($tts:tt)*) => {concat!("something else (", stringify!($($tts)*), ")")}; } fn main() { println!( "{}\n{}\n{}\n{}", what_is!(#[no_mangle]), what_is!(#[inline]), capture_then_what_is!(#[no_mangle]), // 被 capture_then_what_is 宏 解析过的token,不会再二次被 what_is 宏解析,所以按 tt 规则处理 capture_then_what_is!(#[inline]), // 被 capture_then_what_is 宏 解析过的token,不会再二次被 what_is 宏解析,所以按 tt 规则处理 ); } // 输出: // no_mangle attribute // inline attribute // something else (#[no_mangle]) // something else (#[inline])
【正例】
满足示例这类正常匹配情况的目前只有 tt
、ident
或者 lifetime
分类符。
macro_rules! capture_then_what_is { (#[$m:tt]) => {what_is!(#[$m])}; // 这里片段分类符用的是 tt } macro_rules! what_is { (#[no_mangle]) => {"no_mangle attribute"}; (#[inline]) => {"inline attribute"}; ($($tts:tt)*) => {concat!("something else (", stringify!($($tts)*), ")")}; } fn main() { println!( "{}\n{}\n{}\n{}", what_is!(#[no_mangle]), what_is!(#[inline]), capture_then_what_is!(#[no_mangle]), // 被 capture_then_what_is 宏 解析过的token,还会被 what_is 二次处理 capture_then_what_is!(#[inline]), // 被 capture_then_what_is 宏 解析过的token,还会被 what_is 二次处理 ); } // 输出: // no_mangle attribute // inline attribute // no_mangle attribute // inline attribute
P.MAC.DCL.06 当宏需要接收 self
时需要注意
【描述】
self
在 Rust 中属于关键字,它会在代码运行时被替换为具体类型的实例。当它传递给 宏 时会被看做为一个变量,而宏对于变量而言是具备卫生性的。而且,声明宏的作用只是替换而非计算,它并不能计算出 self 的具体类型。
【反例】
macro_rules! make_mutable { ($i:ident) => {let mut $i = $i;}; } struct Dummy(i32); impl Dummy { fn double(self) -> Dummy { make_mutable!(self); // 这里传入的 self 和宏内部 let 定义的 self 不同 self.0 *= 2; self } } fn main() { println!("{:?}", Dummy(4).double().0); }
【正例】
macro_rules! double_method { ($self_:ident, $body:expr) => { fn double(mut $self_) -> Dummy { $body } }; } struct Dummy(i32); impl Dummy { double_method! {self, { self.0 *= 2; self }} } fn main() { println!("{:?}", Dummy(4).double().0); }
P.MAC.DCL.07 确保在宏定义之后再去调用宏
【描述】
Rust 中类型或函数在定义前后都可以调用,但是宏不一样。Rust 查找宏定义是按词法依赖顺序的,必须注意定义和调用的先后顺序。
【反例】
mod a { // X!(); // undefined } mod b { // X!(); // undefined macro_rules! X { () => {}; } X!(); // defined } mod c { // X!(); // undefined } fn main() {}
【正例】
macro_rules! X { () => {}; } mod a { X!(); // defined } mod b { X!(); // defined } mod c { X!(); // defined } fn main() {}
【例外】
宏与宏之间相互调用,不受词法顺序的限制。
mod a { // X!(); // undefined } macro_rules! X { () => { Y!(); }; } // 注意:这里的 Y! 宏是在定义前被调用的,代码正常执行 mod b { // X!(); // defined, but Y! is undefined } macro_rules! Y { () => {}; } // Y! 宏被定义在 X! 宏后面 mod c { X!(); // defined, and so is Y! } fn main() {}
P.MAC.DCL.08 同一个 crate 内定义的宏相互调用时,需要注意卫生性
【描述】
当同一个 crate 内定义的宏相互调用时候,应该使用 $crate
元变量来指代当前被调用宏的路径。
【反例】
#![allow(unused)] fn main() { #[macro_export] macro_rules! helped { () => { helper!() } // This might lead to an error due to 'helper' not being in scope. } #[macro_export] macro_rules! helper { () => { () } } //// 在另外的 crate 中使用这两个宏 // 注意:`helper_macro::helper` 并没有导入进来 use helper_macro::helped; fn unit() { // Error! 这个宏会出现问题,因为其内部调用的 helper 宏的路径会被编译器认为是当前调用crate 的路径 helped!(); } }
【正例】
#![allow(unused)] fn main() { #[macro_export] macro_rules! helped { () => { $crate::helper!() } } #[macro_export] macro_rules! helper { () => { () } } //// 在另外的 crate 中使用这两个宏 // 注意:`helper_macro::helper` 并没有导入进来 use helper_macro::helped; fn unit() { // OK! 这个宏能运行通过,因为 `$crate` 正确地展开成 `helper_macro` crate 的路径(而不是使用者的路径) helped!(); } }
过程宏
过程宏(Procedural macros) 允许开发者来创建语法扩展。你可以通过过程宏创建类似函数的宏、派生宏以及属性宏。
广义上的"过程宏"指的是通过 syn/quote(毕竟几乎全部过程宏库都用 syn) 及 syn 生态(例如 darling) 进行代码生成等元编程操作。
syn/quote 不仅能用于过程宏,还广泛用于代码生成(codegen)、静态分析等用途,例如 tonic-build/prost 源码中也用到了 syn/quote 。
因此本过程宏规范不仅适用于过程宏,部分规范(例如 P.MAC.PRO.06) 还适用于 prost 这种代码生成库
过程宏必须被单独定义在一个类型为proc-macro
的 crate 中。
过程宏有两类报告错误的方式:panic
或 通过 compile_error
宏调用发出错误。
过程宏不具有卫生性(hygiene),这意味着它会受到外部语法项(item)的影响,也会影响到外部导入。
过程宏可以在编译期执行任意代码。
列表
- P.MAC.PRO.01 不要使用过程宏来规避静态分析检查
- P.MAC.PRO.02 实现过程宏时要对关键特性增加测试
- P.MAC.PRO.03 保证过程宏的卫生性
- P.MAC.PRO.04 给出正确的错误位置
- P.MAC.PRO.05 代码生成要按情况选择使用过程宏还是 build.rs
- P.MAC.PRO.06 build.rs 生成的代码要保证没有任何警告
P.MAC.PRO.01 不要使用过程宏来规避静态分析检查
【描述】
不要利用过程宏来定义能规避 Rust 静态分析检查的宏。
【反例】
在 Rust 生态中有一个库 plutonium
,该库利用了过程宏来消除代码中直接的 unsafe
块的使用,从而规避了编译器对 unsafe
关键字的静态检查。
该库会通过#[safe]
过程宏在自动生成代码的时候为函数体添加 unsafe
块,但这会影响到 unsafe
调用链依赖静态检查传播,从而进一步打断 unsafe 调用链路,影响后续通过 unsafe
关键字来定位问题。
use plutonium::safe; #[safe] fn super_safe(x: f32) -> i32 { std::mem::transmute::<f32, i32>(x) } #[safe] unsafe fn deref_null() { *std::ptr::null::<u8>(); } fn main(){ println!("{:?}", super_safe(1.0)); deref_null(); }
【正例】
对于不安全的函数,应该显式地使用 unsafe
。这样做的好处是利用 Rust 编译器静态检查传播 unsafe 调用链条,以达到可以全局查找 unsafe 使用来消除一些代码隐患,方便定位问题。
unsafe fn super_safe(x: f32) -> i32 { unsafe { std::mem::transmute::<f32, i32>(x) } } unsafe fn deref_null() { unsafe { *std::ptr::null::<u8>(); } } fn main(){ println!("{:?}", unsafe{super_safe(1.0f32)}); // 1065353216 // error[E0133]: call to unsafe function is unsafe and requires unsafe function or block // deref_null(); // 如果调用 unsafe 函数不加 unsafe 块,编译器就会报错。 unsafe{ deref_null(); } }
【相关讨论】
- RUSTSEC-2020-0011
- https://github.com/RustSec/advisory-db/issues/275
- https://github.com/rust-lang/unsafe-code-guidelines/issues/278
P.MAC.PRO.02 实现过程宏时要对关键特性增加测试
【描述】
实现过程宏的时候,要对关键特性增加测试,这是为了避免出现关键特性遗漏的情况。
【反例】
在第三方库 zeroize 中,曾经因为过程宏中对枚举类型没有实现 Drop 而引起问题。参见:RUSTSEC-2021-0115
#![allow(unused)] fn main() { #[derive(Zeroize)] #[zeroize(drop)] pub enum Fails { Variant(Vec<u8>), } // This does compile with zeroize_derive version 1.1, meaning `#[zeroize(drop)]` didn't implement `Drop`. impl Drop for Fails { fn drop(&mut self) { todo!() } } }
【正例】
在第三方库 zeroize 中,曾经因为过程宏中对枚举类型没有实现 Drop 而引起问题。增加关键性测试可以避免这类问题。
#![allow(unused)] fn main() { #[test] fn zeroize_on_struct() { parse_zeroize_test(stringify!( #[zeroize(drop)] struct Z { a: String, b: Vec<u8>, c: [u8; 3], } )); } #[test] fn zeroize_on_enum() { parse_zeroize_test(stringify!( #[zeroize(drop)] enum Z { Variant1 { a: String, b: Vec<u8>, c: [u8; 3] }, } )); } }
P.MAC.PRO.03 保证过程宏的卫生性
【描述】
过程宏生成的代码尽量使用完全限定名,防止命名冲突产生意想不到的后果。
可以使用 #![no_implicit_prelude]
属性来验证过程宏的卫生性。
#![allow(unused)] #![no_implicit_prelude] fn main() { #[derive(MyMacro)] struct A; }
【反例】
#![allow(unused)] fn main() { quote!(a.to_string()) }
【正例】
#![allow(unused)] fn main() { quote!(std::string::ToString::to_string(a)) }
#![allow(unused)] fn main() { quote! {{ use std::string::ToString; a.to_string() }} }
P.MAC.PRO.04 给出正确的错误位置
【描述】
过程宏发生错误时,返回的错误应该有正确的位置信息。
【反例】
#![allow(unused)] fn main() { // 直接用Span::call_site() Error::new(Span::call_site(), "requires unit variant") .to_compile_error() .into() }
【正例】
#![allow(unused)] fn main() { #[proc_macro_derive(MyMacro)] pub fn derive_my_macro(input: TokenStream) -> TokenStream { let derive_input: DeriveInput = syn::parse_macro_input!(input as DeriveInput); if let Data::Enum(e) = &derive_input.data { for variant in &e.variants { if !variant.fields.is_empty() { // 使用variant的span return syn::Error::new_spanned(&variant, "must be a unit variable.") .to_compile_error() .into(); } } } todo!() } }
代码生成
Rust 中代码生成的方式包括宏 与 build.rs
两种方式。关于宏,有独立的规范章节,本章节规范内容包括:
build.rs
使用规范- 代码生成相关其他规范
P.CGN.01 代码生成要按情况选择使用过程宏还是 build.rs
【描述】
用过程宏生进行代码生成,比如生成新类型或函数,有一个缺点就是:IDE 无法识别它们,影响开发体验。
但是使用 build.rs
生成的代码,对 IDE 更友好。
不过随着 IDE 的增强,过程宏以后应该也能变得更加 IDE 友好。
建议按应用场景选择:
build.rs
一般用于根据外部文件生成代码的场景。比如根据C
头文件生成 Rust 绑定,或者根据proto
文件生成相应的 Rust 类型等,供开发者直接使用。- 过程宏一般用于消除样例式代码,提升库使用者的开发体验。
【正例】
build.rs
把 tonic
生成的代码直接放在 src
目录 (生成的代码文件应该在 .gitignore 中忽略版本管理),这样 IDE 能够识别它们使自动完成能够工作,提高开发效率。
fn main() -> Result<(), Box<dyn std::error::Error>> { tonic_build::configure() .out_dir("src") .compile( &["proto/helloworld/helloworld.proto"], &["proto/helloworld"], )?; println!("cargo:rerun-if-changed=proto"); }
tarpc
的service
宏会生成一个新的WorldClient
类型,IDE完全无法识别。
#![allow(unused)] fn main() { #[tarpc::service] trait World { async fn hello(name: String) -> String; } let (client_transport, server_transport) = tarpc::transport::channel::unbounded(); let mut client = WorldClient::new(client::Config::default(), client_transport).spawn(); }
P.CGN.02 build.rs
生成的代码要保证没有任何警告
【描述】
build.rs
生成的代码(codegen),要通过或忽略 clippy 检查,不要让库的使用者或应用用户自行忽略
codegen 库要保证生成的代码应该非常干净没有任何警告,不应该让库的使用者去处理生成代码中的警告。
【反例】
lalrpop v0.19.6 生成的代码有几百个 clippy 警告,"淹没"了用户自己代码的 clippy 警告
warning: using `clone` on type `usize` which implements the `Copy` trait
--> /home/w/temp/my_parser/target/debug/build/my_parser-dd96f436ee76c58d/out/my_parser.rs:182148:21
|
182148 | let __end = __start.clone();
| ^^^^^^^^^^^^^^^ help: try removing the `clone` call: `__start`
使得 lalrpop 库的使用者必须手动给生成的模块代码加上 allow clippy,给使用者带来不便
#![allow(unused)] fn main() { lalrpop_mod!( #[allow(clippy::all)] my_parser ); }
【正例】
tonic-build 生成的 rs 会通过 allow 忽略掉 clippy 警告
#![allow(unused)] fn main() { pub mod peer_communication_client { #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] use tonic::codegen::*; }
3.17 多线程
Rust 天生可以有效消除数据竞争。
锁同步
Rust 中多线程并发使用锁来进行线程同步。
列表
- P.MTH.LCK.01 多线程下要注意识别锁争用的情况,避免死锁
- G.MTH.LCK.01 对布尔或引用并发访问应该使用原子类型而非互斥锁
- G.MTH.LCK.02 建议使用
Arc<str> / Arc<[T]>
来代替Arc<String> / Arc<Vec<T>>
- G.MTH.LCK.03 尽量避免直接使用标准库
std::sync
模块中的同步原语,替换为parking_lot
- G.MTH.LCK.04 尽量避免直接使用标准库
std::sync::mpsc
模块中的channel
,替换为crossbeam
P.MTH.LCK.01 多线程下要注意识别锁争用的情况,避免死锁
【描述】
Rust 并不能保证没有死锁,要注意 LockResult<MutexGuard<'_, T>>
的生命周期,以防止出现死锁的情况。
【反例】
下面代码有一定的几率会触发死锁。
// 触发死锁时,只会输出: // Thread 1 holds a lock and starts waiting b lock // Thread 2 hodls a lock and starts waiting a lock use std::sync::{Arc, Mutex}; use std::thread; fn main() { let a = Arc::new(Mutex::new(0)); let b = Arc::new(Mutex::new(0)); let mut handles = vec![]; { let a = Arc::clone(&a); let b = Arc::clone(&b); let handle = thread::spawn(move || { let mut a_num = a.lock().unwrap(); *a_num += 1; println!("Thread 1 holds a lock and starts waiting b lock"); let mut b_num = b.lock().unwrap(); *b_num += 1; }); handles.push(handle); } { let a = Arc::clone(&a); let b = Arc::clone(&b); let handle = thread::spawn(move || { let mut b_num = b.lock().unwrap(); *b_num += 1; println!("Thread 2 holds b lock and starts waiting a lock"); let mut a_num = a.lock().unwrap(); *a_num += 1; println!("Thread 2"); }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Done {}", *a.lock().unwrap()); // never reach here }
【正例】
// 无死锁发生,正常输出 // Thread 1 holds a lock and starts waiting b lock // Thread 2 hodls a lock and starts waiting a lock // Thread 2 // Done 2 use std::sync::{Arc, Mutex}; use std::thread; fn main() { let a = Arc::new(Mutex::new(0)); let b = Arc::new(Mutex::new(0)); let mut handles = vec![]; { let a = Arc::clone(&a); let b = Arc::clone(&b); let handle = thread::spawn(move || { { // <- 这里增加显示作用域,确保 lock 之后可以自动解锁 // 即 LockResult<MutexGuard<'_, T>> 在作用域之外会自动释放 let mut a_num = a.lock().unwrap(); *a_num += 1; println!("Thread 1 holds a lock and starts waiting b lock"); } { // <- 这里增加显示作用域,确保 lock 之后可以自动解锁 // 即 LockResult<MutexGuard<'_, T>> 在作用域之外会自动释放 let mut b_num = b.lock().unwrap(); *b_num += 1; } }); handles.push(handle); } { let a = Arc::clone(&a); let b = Arc::clone(&b); let handle = thread::spawn(move || { { // <- 这里增加显示作用域,确保 lock 之后可以自动解锁 // 即 LockResult<MutexGuard<'_, T>> 在作用域之外会自动释放 let mut b_num = b.lock().unwrap(); *b_num += 1; println!("Thread 2 holds b lock and starts waiting a lock"); } { // <- 这里增加显示作用域,确保 lock 之后可以自动解锁 // 即 LockResult<MutexGuard<'_, T>> 在作用域之外会自动释放 let mut a_num = a.lock().unwrap(); *a_num += 1; } println!("Thread 2"); }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Done {}", *a.lock().unwrap()); }
G.MTH.LCK.01 对布尔或引用并发访问应该使用原子类型而非互斥锁
【级别】 建议
【描述】
使用原子类型性能更好。但要注意指定合理的内存顺序。
【反例】
#![allow(unused)] fn main() { // 不符合 let x = Mutex::new(&y); }
【正例】
#![allow(unused)] fn main() { // 符合 let x = AtomicBool::new(y); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
mutex_atomic | yes | no | perf | warn |
G.MTH.LCK.02 宜使用 Arc<str> / Arc<[T]>
来代替 Arc<String> / Arc<Vec<T>>
【级别】 建议
【描述】
Arc<str> / Arc<[T]>
的性能比 Arc<String> / Arc<Vec<T>>
更好。
因为 :
Arc<String> / Arc<Vec<T>>
有一层中间层:arc -> String len/Vec<T> len -> text/data
,它是一个薄指针(thin pointer)。Arc<str>/ Arc<[T]>
则没有中间层:arc & string len / [T] len -> text/data
,它是一个胖指针(fat pointer)。
【反例】
use std::rc::Rc; use std::sync::Arc; fn main() { let a = "hello world".to_string(); let b: Rc<String> = Rc::from(a); // 不符合 println!("{}", b); let a = "hello world".to_string(); let b: Rc<String> = a.into(); // 不符合 println!("{}", b); let a = "hello world".to_string(); let b: Arc<String> = Arc::from(a); // 不符合 println!("{}", b); }
【正例】
use std::rc::Rc; use std::sync::Arc; fn main() { let a: &str = "hello world"; let b: Rc<str> = Rc::from(a); // 符合 println!("{}", b); let b: Rc<str> = a.into(); // 符合 println!("{}", b); let a: &str = "hello world"; let b: Arc<str> = Arc::from(a); // 符合 println!("{}", b); }
【例外】
参考:https://github.com/rust-lang/rust-clippy/pull/6044#issuecomment-699565080
#![allow(unused)] fn main() { // From: https://github.com/Fishrock123/surf/blob/master/src/client.rs#L33 pub struct Client { http_client: Arc<dyn HttpClient>, /// Holds the middleware stack. // 业务上必须要求持有一个 Vec 才能保证用户正常添加中间件 middleware: Arc<Vec<Arc<dyn Middleware>>>, } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
rc_buffer | yes | no | restriction | allow |
G.MTH.LCK.03 尽量避免直接使用标准库 std::sync
模块中的同步原语,替换为 parking_lot
【级别】 建议
【描述】
尽量避免对标准库 std::sync
模块中锁同步原语的使用,建议使用 parking_lot
的实现。
【反例】
来源于 std标准库文档
#![allow(unused)] fn main() { // 不符合 use std::sync::{Arc, Mutex}; use std::thread; use std::sync::mpsc::channel; const N: usize = 10; let data = Arc::new(Mutex::new(0)); let (tx, rx) = channel(); for _ in 0..N { let (data, tx) = (Arc::clone(&data), tx.clone()); thread::spawn(move || { let mut data = data.lock().unwrap(); *data += 1; if *data == N { tx.send(()).unwrap(); } }); } rx.recv().unwrap(); }
【正例】
例子来源于 parking_lot 文档
相比std::sync::Mutex
,使用 parking_lot::Mutex
能实现'无中毒',锁在 panic 时正常释放,更少的空间占用等优势。
#![allow(unused)] fn main() { // 符合 use parking_lot::Mutex; use std::sync::{Arc, mpsc::channel}; use std::thread; const N: usize = 10; let data = Arc::new(Mutex::new(0)); let (tx, rx) = channel(); for _ in 0..10 { let (data, tx) = (Arc::clone(&data), tx.clone()); thread::spawn(move || { let mut data = data.lock(); *data += 1; if *data == N { tx.send(()).unwrap(); } }); } rx.recv().unwrap(); }
G.MTH.LCK.04 尽量避免直接使用标准库 std::sync::mpsc
模块中的 channel
,替换为 crossbeam
【级别】 建议
【描述】
尽量避免使用 std::sync::mpsc::channel
,建议使用 crossbeam
【反例】
例子来源于 std::sync::mpsc
文档
#![allow(unused)] fn main() { use std::thread; use std::sync::mpsc::channel; // 不符合 let (tx, rx) = channel(); for i in 0..10 { let tx = tx.clone(); thread::spawn(move|| { tx.send(i).unwrap(); }); } for _ in 0..10 { let j = rx.recv().unwrap(); assert!(0 <= j && j < 10); } }
【正例】
#![allow(unused)] fn main() { use crossbeam_channel::unbounded; // 符合 let (tx, rx) = unbounded(); for i in 0..10 { let tx = tx.clone(); thread::spawn(move|| { tx.send(i).unwrap(); }); } for _ in 0..10 { let j = rx.recv().unwrap(); assert!(0 <= j && j < 10); } }
无锁并发
Rust 也支持原子类型,其内存顺序模型与 C++20
相同。
列表
P.MTH.LKF.01 除非必要,否则建议使用同步锁
【描述】
无锁编程性能不一定比同步锁高。
使用无锁编程时需要注意的地方比使用同步锁多,比如指令重排、ABA 问题、内存顺序是否指定正确等。 正确实现无锁编程比使用同步锁要困难很多。所以,除非必要,否则直接使用同步锁就可以。
也有一些 性能测试 作为参考,原子类型的性能比互斥锁的性能大概要好四倍左右。所以,当在同一个临界区内要有超过四次原子操作,也许使用互斥锁更加简单一些。
P.MTH.LKF.02 使用无锁编程时,需要合理选择内存顺序
【描述】
Rust 原子类型使用 C++20
的内存顺序模型 来指定原子操作的内存同步方式,但也不是完全采用此模型。
目前 Rust 引入五种内存顺序:Relaxed / Release / Acquire / AcqRel / SeqCst
。
在无锁编程中,指定正确的内存顺序是很重要很复杂的一件事,这里有一些建议:
- 如果对程序中的原子类型同步方式的判断没有太多信息,建议使用
SeqCst
,它表示顺序一致性,会强制所有线程都同意程序指令以单一全局线性的方式来执行。这样可以保证安全性,但性能有一定损失。 - 如果对无锁实现中线程间发生的数据竞争带来的后果不是特别关心,则可以放心使用
Relaxed
,因为它性能最好。 - 当多个线程之间操作内存中同一个位置有因果关系时,适合使用
Acquire / Release / AcqRel
来配对。比如,线程 A 写 (Release
) 内存中的一个位置,然后线程 B 随后读 (Acquire
) 内存中一个相同的位置,就会产生一个因果关系,所以为了保证 A 的每次写入都能在 B 读取之前被观察到。如果 A 和 B 访问不同内存位置,则没有因果关系。
【正例】
自旋锁中 Acquire / Release
搭配使用的简易示例:
use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::thread; fn main() { let lock = Arc::new(AtomicBool::new(false)); // 这个原子类型的值用来表示 "是否获取到锁" // ... distribute lock to threads somehow ... // 线程 A 尝试通过设置为 ture 来获取锁 // // Acquire 内存顺序: // 当与 load 结合使用时,若 load 的值是由具有 Release (或更强) 排序的 store 操作写入的,则所有后续操作 // 都将在该 store 之后进行排序。特别是,所有后续load都将看到在 store 之前写入的数据。 while lock.compare_and_swap(false, true, Ordering::Acquire) { } // 在循环外,意味着已经拿到了锁! // ...访问/操作数据... // 线程A完成了数据操作,释放锁。 // 此处用 Release 内存顺序可以确保线程B在获取锁时能看到线程A释放了锁(对内存写入 false) // // Release 内存顺序: // 当与 store 结合使用时,所有先前的操作都会在使用 Acquire(或更强)排序的任何 load 此值之前排序。 // 特别是,所有先前的写入对执行 Acquire 此值(或更强)load 的线程都可见。 lock.store(false, Ordering::Release); }
3.18 异步编程
async / await
是 Rust 语言用于编写像同步代码一样的异步函数的内置工具。async
将一个代码块转化为一个实现了名为 Future
的特质 (trait)
的状态机。虽然在同步方法中调用阻塞函数会阻塞整个线程,但阻塞的 Future
将让出线程控制权,允许其他 Future
运行。
Rust 异步编程需要依赖于异步运行时,生产环境中比较推荐的开源异步运行时是 Tokio。
P.ASY.01 异步编程并不适合所有场景,计算密集型场景应该考虑同步编程
【描述】
异步编程适合 I/O 密集型应用,如果是计算密集型场景应该考虑使用同步编程。
G.ASY.01 在 async
块或函数中调用 async
函数或闭包请不要忘记添加.await
【级别】 建议
【描述】
在此条件下 .await
语句通常为必须的。
【反例】
#![allow(unused)] fn main() { async fn foo() {} fn bar() { let x = async { foo() // 不符合 }; } }
【正例】
#![allow(unused)] fn main() { async fn foo() {} fn bar() { let x = async { foo().await // 符合 }; } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
async_yields_async | yes | no | correctness | deny |
G.ASY.02 在跨 await
调用中,需要对其持有的同步互斥锁进行处理
【级别】 建议
【描述】
同步互斥锁本来就不是为异步上下文跨 await
调用而设计的,在这种场景中使用同步互斥锁容易造成死锁。当同步互斥锁被跨 await
时,有可能很长时间都不会返回这个调用点,在其他任务中再次用到这个互斥锁的时候,容易造成死锁。
这里有两种解决方案:
- 使用异步互斥锁。但是异步互斥锁的开销要大于同步互斥锁。
- 确保同步互斥锁在调用
await
之前已经释放。
【反例】
#![allow(unused)] #![warn(clippy::await_holding_lock)] fn main() { use std::sync::Mutex; async fn foo(x: &Mutex<u32>) { let mut guard = x.lock().unwrap(); *guard += 1; baz().await; // 不符合 } }
【正例】
#![warn(clippy::await_holding_lock)] use std::sync::Mutex; // 使用同步互斥锁 async fn foo(x: &Mutex<u32>) { { let guard = x.lock().unwrap(); *guard += 1; } // 符合:await 之前先释放锁 bar.await; } // 使用异步互斥锁 use tokio::sync::Mutex; use std::sync::Arc; #[tokio::main] async fn main() { // 使用 Arc 允许跨线程共享 Mutex let count = Arc::new(Mutex::new(0)); for i in 0..5 { let my_count = Arc::clone(&count); tokio::spawn(async move { for j in 0..10 { // 符合:这里的 lock 在每次迭代后都会被释放 let mut lock = my_count.lock().await; *lock += 1; println!("{} {} {}", i, j, lock); } }); } loop { // 符合:这里的 lock 在每次迭代后都会被释放 if *count.lock().await >= 50 { break; } } println!("Count hit 50."); }
【例外】
用例来源:kludgine
#![allow(unused)] fn main() { // Launch a thread pool std::thread::spawn(|| { let (signal, shutdown) = flume::unbounded::<()>(); easy_parallel::Parallel::new() // Run four executor threads. .each(0..4, |_| { #[allow(clippy::await_holding_lock)] // 这里是 读写锁,不是互斥锁 futures::executor::block_on(async { let guard = GLOBAL_THREAD_POOL.read(); // 获取读写锁的读锁,不会出现锁争用情况,所以是线程安全的 let executor = guard.as_ref().unwrap(); executor.run(shutdown.recv_async()).await }) }) // Run the main future on the current thread. .finish(|| {}); drop(signal); }); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
await_holding_lock | yes | no | pedantic | allow |
G.ASY.03 在跨 await
调用中,需要对其持有 RefCell
的引用进行处理
【级别】 建议
【描述】
使用 RefCell
的独占(可变)借用会导致 Panic。因为 RefCell
是运行时检查独占的可变访问,如果跨 await
持有一个可变引用则可能会因为共享的可变引用而引起 Panic。
这种共享可变在编译期是无法被检查出来的。
【反例】
#![allow(unused)] #![warn(clippy::await_holding_refcell_ref)] fn main() { use std::cell::RefCell; async fn foo(x: &RefCell<u32>) { let mut y = x.borrow_mut(); *y += 1; baz().await; // 不符合 } }
【正例】
#![allow(unused)] #![warn(clippy::await_holding_refcell_ref)] fn main() { use std::cell::RefCell; async fn foo(x: &RefCell<u32>) { { let mut y = x.borrow_mut(); *y += 1; } baz().await; // 符合 } }
【例外】
跨 await
持有 RefCell
的可变借用,但是当前场景确信永远不会 Panic,则可以使用。
用例来源:wasm-streams
#![allow(unused)] fn main() { pub fn pull(&mut self, controller: sys::ReadableByteStreamController) -> Promise { let inner = self.inner.clone(); let fut = async move { // 这个可变借用永远不会恐慌,因为 ReadableStream 对底层源的每个操作总是有序的。 let mut inner = inner.try_borrow_mut().unwrap_throw(); inner.pull(controller).await }; // ... } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
await_holding_refcell_ref | yes | no | pedantic | allow |
G.ASY.04 避免定义不必要的异步函数
【级别】 建议
【描述】
如果一个异步函数内部没有任何异步代码,相比一个同步函数,它会产生额外的调用成本。
【反例】
#![allow(unused)] fn main() { // 不符合 #[warn(clippy::unused_async)] async fn add(value: i32) -> i32 { value + 1 } }
【正例】
#![allow(unused)] fn main() { // 符合 #[warn(clippy::unused_async)] fn add(value: i32) -> i32 { value + 1 } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
unused_async | yes | no | pedantic | allow |
G.ASY.05 避免在异步处理过程中包含阻塞操作
【级别】 建议
【描述】
避免在异步编程中使用阻塞操作。
【反例】
不要在异步流程中使用阻塞操作函数
#![allow(unused)] fn main() { use std::error::Error; use std::{fs, io}; async fn read_file() -> Result<String, std::io::Error> { fs::read_to_string("test.txt") // 不符合 } }
【正例】
使用异步运行时,如tokio提供的非阻塞函数
#![allow(unused)] fn main() { use tokio::fs; async fn read_file() -> std::io::Result<()> { let _ = fs::read_to_string("test.txt").await?; // 符合 Ok(()) } }
3.19 Unsafe Rust
Unsafe Rust 是 Safe Rust 的超集,意味着在 Unsafe Rust 中也会有 Safe Rust的安全检查。但是 Unsafe Rust 中下面五件事是Safe Rust 的检查鞭长莫及的地方:
- 解引用裸指针
- 调用
unsafe
函数(C函数,编译器内部函数或原始分配器) - 实现
unsafe
trait - 可变静态变量
- 访问
union
的字段
使用 Unsafe Rust 的时候,需要遵守一定的规范,这样可以避免未定义行为的发生。
关于 Unsafe Rust 下的一些专用术语可以查看 Unsafe 代码术语指南 。
Unsafe Rust 的语义:这是编译器无法保证安全的地方,需要程序员来保证安全。
P.UNS.01 不要为了逃避编译器安全检查而滥用 Unsafe Rust
【描述】
Unsafe Rust 有其应用范围和目标,不要为了逃避 编译器安全检查而随便滥用 Unsafe Rust,否则很可能引起未定义行为(UB)。
【反例】
// 该函数为滥用 unsafe 来跳过 Rust 借用检查 // 强行返回本地变量的引用,最终引起 UB 未定义行为 fn abuse_unsafe_return_local_ref<'a>() -> &'a String { let s = "hello".to_string(); let ptr_s_addr = &s as *const String as usize; unsafe{ &*(ptr_s_addr as *const String) } } fn main() { let s = abuse_unsafe_return_local_ref(); // error: Undefined Behavior: encountered a dangling reference (use-after-free) }
P.UNS.02 不要为了提升性能而盲目使用 Unsafe Rust
【描述】
对比 Safe 代码的性能看是否够用,就可以减少不必要的 Unsafe。
G.UNS.01 不宜为带有 unsafe
命名的类型或方法创建别名
【级别】 建议
【描述】
Rust 里 unsafe
字样用于提醒开发者在编写代码的时候注意保证安全。如果修改别名,隐藏了这种提醒,不利于展示这种信息。
不利于开发者去保证安全。
【反例】
#![allow(unused)] fn main() { use std::cell::{UnsafeCell as TotallySafeCell}; extern crate crossbeam; use crossbeam::{spawn_unsafe as spawn}; }
【正例】
#![allow(unused)] fn main() { use std::cell::{UnsafeCell}; extern crate crossbeam; use crossbeam::{spawn_unsafe}; }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
unsafe_removed_from_name | yes | no | style | warn |
安全抽象规范
使用 Unsafe Rust 的一种方式是将 Unsafe 的方法或函数进行安全抽象,将其变成安全的方法或函数。
Unsafe Rust 中 API 的安全性设计通常有两种方式:
- 将内部的 unsafe API 直接暴露给 API 的使用者,并且使用
unsafe
关键字来声明该 API 是非安全的,同时也需要对安全边界条件添加注释。 - 对 API 进行安全封装,即,安全抽象。在内部使用断言来保证当越过安全边界时可以 Panic,从而避免 UB 产生。
第二种方式,对 Unsafe 代码进行安全抽象,是 Rust 生态的一种约定俗成。
P.UNS.SAS.01 代码中要注意是否会因为 Panic 发生而导致内存安全问题
【描述】
Panic 一般在程序达到不可恢复的状态才用,当然在 Rust 中也可以对一些实现了UnwindSafe
trait 的类型捕获Panic。
当 Panic 发生时,会引发栈回退(stack unwind),调用栈分配对象的析构函数,并将控制流转移给 Panic 处理程序中。所以,当 Panic 发生的时候,当前存活变量的析构函数将会被调用,从而导致一些内存安全问题,比如释放已经释放过的内存。
通常, 封装的 Unsafe 代码可能会暂时绕过所有权检查,而且,安全封装的 API 在内部 unsafe 代码的值返回之前,会根据安全边界条件确保它不会违反安全规则。但是,假如封装的 Unsafe 代码发生了Panic,则其外部安全检查可能不会执行。这很可能导致类似 C/C++ 中 未初始化(Uninitialized )或双重释放(Double Free)的内存不安全问题。
想要正确的推理在 Unsafe 代码中的恐慌安全,是非常困难且易于出错的。即便如此,在编写代码的时候也要刻意注意此类问题发生的可能性。
【正例】
// 标准库 `String::retain()` 曝出的 CVE-2020-36317 Panic safety bug pub fn retain<F>(&mut self, mut f: F) where F: FnMut(char) -> bool { let len = self.len(); let mut del_bytes = 0; let mut idx = 0; unsafe { self.vec.set_len(0); } // + 修复bug 的代码 while idx < len { let ch = unsafe { self.get_unchecked(idx..len).chars().next().unwrap() }; let ch_len = ch.len_utf8(); // self is left in an inconsistent state if f() panics // 此处如果 f() 发生了 Panic,self 的长度就会不一致 if !f(ch) { del_bytes += ch_len; } else if del_bytes > 0 { unsafe { ptr::copy(self.vec.as_ptr().add(idx), self.vec.as_mut_ptr().add(idx - del_bytes), ch_len); } } idx += ch_len; // point idx to the next char } unsafe { self.vec.set_len(len - del_bytes); } // + 修复bug 的代码 ,如果 while 里发生 Panic,则将返回长度设置为 0 } fn main(){ // PoC: creates a non-utf-8 string in the unwinding path // 此处传入一个 非 UTF-8 编码字符串引发 Panic "0è0".to_string().retain(|_| { match the_number_of_invocation() { 1 => false, 2 => true, _ => panic!(), } }); }
P.UNS.SAS.02 Unsafe 代码编写者有义务检查代码是否满足安全不变式
【描述】
安全不变式(见 Unsafe 代码术语指南 )是 Rust 里的安全函数,在任何有效输入的情况下,都不应该发生任何未定义行为。
可以从以下三个方面来检查:
- 逻辑一致性。
- 纯洁性。相同的输入总是要返回相同的输出。
- 语义约束。传入的参数要合法,满足数据类型。
【正例】
该代码是为 Borrow<str>
实现 join 方法内部调用的一个函数 join_generic_copy
的展示。 在 join_generic_copy
内部,会对 slice
进行两次转换,而在 spezialize_for_lengths!
宏内部,调用了 .borrow()
方法,如果第二次转换和第一次不一样,而会返回一个未初始化字节的字符串。
这里, Borrow<B>
是高阶类型,它内部 borrow
的一致性其实并没有保证,可能会返回不同的 slice,如果不做处理,很可能会暴露出未初始化的字节给调用者。
#![allow(unused)] fn main() { // CVE-2020-36323: a higher-order invariant bug in join() fn join_generic_copy<B, T, S>(slice: &[S], sep: &[T]) -> Vec<T> where T: Copy, B: AsRef<[T]> + ?Sized, S: Borrow<B> { let mut iter = slice.iter(); // `slice`is converted for the first time // during the buffer size calculation. let len = ...; // `slice` 在这里第一次被转换 let mut result = Vec::with_capacity(len); // ... unsafe { let pos = result.len(); let target = result.get_unchecked_mut(pos..len); // `slice`is converted for the second time in macro // while copying the rest of the components. spezialize_for_lengths!(sep, target, iter; // `slice` 第二次被转换 0, 1, 2, 3, 4); // Indicate that the vector is initialized result.set_len(len); } result } // PoC: a benign join() can trigger a memory safety issue impl Borrow<str> for InconsistentBorrow { fn borrow(&self) -> &str { if self.is_first_time() { "123456" } else { "0" } } } let arr: [InconsistentBorrow; 3] = Default::default(); arr.join("-"); }
P.UNS.SAS.03 不要随便在公开的 API 中暴露未初始化内存
【描述】
在公开的API中暴露未初始化内存可能导致 未定义行为。 关于未定义行为,可以参考Unsafe 代码术语指南。
【反例】
#![allow(unused)] fn main() { // 以下是有安全风险的代码示例: impl<R> BufRead for GreedyAccessReader<R> where R: Read, { fn fill_buf(&mut self) -> IoResult<&[u8]> { if self.buf.capacity() == self.consumed { self.reserve_up_to(self.buf.capacity() + 16); } let b = self.buf.len(); let buf = unsafe { // safe because it's within the buffer's limits // and we won't be reading uninitialized memory // 这里虽然没有读取未初始化内存,但是会导致用户读取 std::slice::from_raw_parts_mut( self.buf.as_mut_ptr().offset(b as isize), self.buf.capacity() - b) }; match self.inner.read(buf) { Ok(o) => { unsafe { // reset the size to include the written portion, // safe because the extra data is initialized self.buf.set_len(b + o); } Ok(&self.buf[self.consumed..]) } Err(e) => Err(e), } } fn consume(&mut self, amt: usize) { self.consumed += amt; } } // 另外一个漏洞代码 fn read_vec(&mut self) -> Result<Vec<u8>> { let len: u32 = de::Deserialize::deserialize(&mut *self)?; // 创建了未初始化buf let mut buf = Vec::with_capacity(len as usize); unsafe { buf.set_len(len as usize) } self.read_size(u64::from(len))?; // 将其传递给了用户提供的`Read`实现 self.reader.read_exact(&mut buf[..])?; Ok(buf) } }
【正例】
#![allow(unused)] fn main() { // 修正以后的代码示例,去掉了未初始化的buf: impl<R> BufRead for GreedyAccessReader<R> where R: Read, { fn fill_buf(&mut self) -> IoResult<&[u8]> { if self.buf.capacity() == self.consumed { self.reserve_up_to(self.buf.capacity() + 16); } let b = self.buf.len(); self.buf.resize(self.buf.capacity(), 0); let buf = &mut self.buf[b..]; let o = self.inner.read(buf)?; // truncate to exclude non-written portion self.buf.truncate(b + o); Ok(&self.buf[self.consumed..]) } fn consume(&mut self, amt: usize) { self.consumed += amt; } } // 另外一个已修正漏洞的代码 fn read_vec(&mut self) -> Result<Vec<u8>> { let len: u32 = de::Deserialize::deserialize(&mut *self)?; // 创建了未初始化buf let mut buf = Vec::with_capacity(len as usize); // 初始化为 0; buf.resize(len as usize, 0); self.read_size(u64::from(len))?; // 将其传递给了用户提供的`Read`实现 self.reader.read_exact(&mut buf[..])?; Ok(buf) } }
P.UNS.SAS.04 避免因为 Panic Safety 而导致双重释放
【描述】
要注意 Panic Safety 的情况,避免双重释放(double free)的问题发生。
在使用 std::ptr
模块中接口需要注意,容易产生 UB 问题,要多多查看 API 文档。
【反例】
#![allow(unused)] fn main() { //case 1 macro_rules! from_event_option_array_into_event_list( ($e:ty, $len:expr) => ( impl<'e> From<[Option<$e>; $len]> for EventList { fn from(events: [Option<$e>; $len]) -> EventList { let mut el = EventList::with_capacity(events.len()); for idx in 0..events.len() { // 这个 unsafe 用法在 `event.into()`调用 panic 的时候会导致双重释放 let event_opt = unsafe { ptr::read(events.get_unchecked(idx)) }; if let Some(event) = event_opt { el.push::<Event>(event.into()); } } // 此处 mem::forget 就是为了防止 `dobule free`。 // 因为 `ptr::read` 也会制造一次 drop。 // 所以上面如果发生了 panic,那就相当于注释了 `mem::forget`,导致 `dobule free` mem::forget(events); el } } ) ); }
【正例】
#![allow(unused)] fn main() { macro_rules! from_event_option_array_into_event_list( ($e:ty, $len:expr) => ( impl<'e> From<[Option<$e>; $len]> for EventList { fn from(events: [Option<$e>; $len]) -> EventList { let mut el = ManuallyDrop::new( EventList::with_capacity(events.len()) ); for idx in 0..events.len() { let event_opt = unsafe { ptr::read(events.get_unchecked(idx)) }; if let Some(event) = event_opt { // Use `ManuallyDrop` to guard against // potential panic within `into()`. // 当 into 方法发生 panic 当时候,这里 ManuallyDrop 可以保护其不会`double free` let event = ManuallyDrop::into_inner( ManuallyDrop::new(event) .into() ); el.push(event); } } mem::forget(events); ManuallyDrop::into_inner(el) } } ) ); }
P.UNS.SAS.05 手动实现 auto trait 时要充分考虑其安全性
【描述】
所谓 auto trait 是指 Safe Rust中由编译器自动实现的 trait,比如 Send/Sync
。在 Unsafe Rust中就需要手动实现这俩 trait 了。
所以,在手动实现的时候要充分考虑其安全性。
【正例】
Rust futures 库中发现的问题,错误的手工 Send/Sync
实现 破坏了线程安全保证。
受影响的版本中,MappedMutexGuard
的 Send/Sync
实现只考虑了 T
上的差异,而 MappedMutexGuard
则取消了对 U
的引用。
当 MutexGuard::map()
中使用的闭包返回与 T
无关的 U
时,这可能导致安全 Rust 代码中的数据竞争。
这个问题通过修正 Send/Sync
的实现,以及在 MappedMutexGuard
类型中添加一个 PhantomData<&'a mut U>
标记来告诉编译器,这个防护也是在 U 之上。
#![allow(unused)] fn main() { // CVE-2020-35905: incorrect uses of Send/Sync on Rust's futures pub struct MappedMutexGuard<'a, T: ?Sized, U: ?Sized> { mutex: &'a Mutex<T>, value: *mut U, _marker: PhantomData<&'a mut U>, // + 修复代码 } impl<'a, T: ?Sized> MutexGuard<'a, T> { pub fn map<U: ?Sized, F>(this: Self, f: F) -> MappedMutexGuard<'a, T, U> where F: FnOnce(&mut T) -> &mut U { let mutex = this.mutex; let value = f(unsafe { &mut *this.mutex.value.get() }); mem::forget(this); // MappedMutexGuard { mutex, value } MappedMutexGuard { mutex, value, _marker: PhantomData } // + 修复代码 } } // unsafe impl<T: ?Sized + Send, U: ?Sized> Send unsafe impl<T: ?Sized + Send, U: ?Sized + Send> Send // + 修复代码 for MappedMutexGuard<'_, T, U> {} //unsafe impl<T: ?Sized + Sync, U: ?Sized> Sync unsafe impl<T: ?Sized + Sync, U: ?Sized + Sync> Sync // + 修复代码 for MappedMutexGuard<'_, T, U> {} // PoC: this safe Rust code allows race on reference counter * MutexGuard::map(guard, |_| Box::leak(Box::new(Rc::new(true)))); }
P.UNS.SAS.06 不要随便在公开的 API 中暴露裸指针
【描述】
在公开的API中暴露裸指针,可能会被用户修改为空指针,从而有段错误风险。
【正例】
use cache; /** `cache crate` 内部代码: ```rust pub enum Cached<'a, V: 'a> { /// Value could not be put on the cache, and is returned in a box /// as to be able to implement `StableDeref` Spilled(Box<V>), /// Value resides in cache and is read-locked. Cached { /// The readguard from a lock on the heap guard: RwLockReadGuard<'a, ()>, /// A pointer to a value on the heap // 漏洞风险 ptr: *const ManuallyDrop<V>, }, /// A value that was borrowed from outside the cache. Borrowed(&'a V), } **/ fn main() { let c = cache::Cache::new(8, 4096); c.insert(1, String::from("test")); let mut e = c.get::<String>(&1).unwrap(); match &mut e { cache::Cached::Cached { ptr, .. } => { // 将 ptr 设置为 空指针,导致段错误 *ptr = std::ptr::null(); }, _ => panic!(), } // 输出:3851,段错误 println!("Entry: {}", *e); }
P.UNS.SAS.07 在抽象安全方法的同时,也建议为性能考虑而增加相应的 Unsafe 方法
【描述】
在 Rust 标准库中有很多后缀有 _unchecked
的方法,都对应一个没有该后缀的同名方法,比如 get() / get_unchecked()
。
【正例】
#![allow(unused)] fn main() { /// 假如调用环境可以保证地址是非空,那么可以使用这个 "_unchecked" 的函数 #[inline(always)] unsafe fn io_read_u32_unchecked(ioaddr: usize) -> u32 { let val = ptr::read_volatile(ioaddr as *const u32); trace!("io_read_u32 {:#x}={:#x}", ioaddr, val); val } /// 安全抽象版本 #[inline(always)] fn io_read_u32() -> Result<u32, MyError> { let ioaddr = ioaddr as * const u32; if ioaddr.is_null() { return Err(MyError::Content("io_read_u32 addr is null!")); } unsafe { let val = ptr::read_volatile(ioaddr); trace!("io_read_u32 {:#x}={:#x}", ioaddr, val); ok(val) } } }
P.UNS.SAS.08 函数参数是不可变借用的时候,返回值不应该是可变借用
【描述】
即便函数签名中没有 unsafe
,但开发者无法保证它函数头部实现中不含 Unsafe代码。
当通过 Unsafe 安全抽象一个函数的时候,要注意符合规则中描述的签名约定:不能输入一个不可变借用,返回一个可变的,这是违反 Rust安全准则的。
当然,当函数被标识为 unsafe
时,是允许这种情况的。
【反例】
这个来自 Rust 官方的一个示例,这样的签名导致 Rust 出现了一个严重 bug,来源
#![allow(unused)] fn main() { // 该函数未加 unsafe,被认为是安全的 // 但是函数签名违反了 Rust 的安全规则,不应该不可变借用进去,可变借用返回 pub fn as_mut_slice(&self) -> &mut [T] { unsafe { slice::from_raw_parts_mut(self.ptr as *mut T, self.len()) } } }
【正例】
#![allow(unused)] fn main() { // 修正以后的代码:https://github.com/rust-lang/rust/pull/39466/files pub fn as_mut_slice(&mut self) -> &mut [T] { unsafe { slice::from_raw_parts_mut(self.ptr as *mut T, self.len()) } } }
【例外】
#![allow(unused)] fn main() { // From: https://docs.rs/crate/wasmer/2.0.0/source/src/externals/memory.rs /// Retrieve a mutable slice of the memory contents. /// /// # Safety /// /// This method provides interior mutability without an UnsafeCell. Until /// the returned value is dropped, it is undefined behaviour to read or /// write to the pointed-to memory in any way except through this slice, /// including by calling a wasm function that reads the memory contents or /// by resizing this Memory. // 这里为 unsafe 函数,则允许这种情况 #[allow(clippy::mut_from_ref)] pub unsafe fn data_unchecked_mut(&self) -> &mut [u8] { let definition = self.vm_memory.from.vmmemory(); let def = definition.as_ref(); slice::from_raw_parts_mut(def.base, def.current_length.try_into().unwrap()) } }
P.UNS.SAS.09 在任何 Unsafe 块之前都应该加 SAFETY
注释
【描述】
在任何 unsafe 块(即 unsafe {...}
)之前都应该加 # SAFETY
注释,用于表明 Unsafe 块里面的代码为什么是正确的。并且 # SAFETY
是全大写,注意区分与文档注释中 # Safety
的用法。
虽然有时原因可能看起来微不足道或很明显,但编写这些注释不仅是记录所考虑内容的好方法,而且最重要的是,它提供了一种知道没有额外隐含约束的方法。
【反例】
未在 unsafe 块之前增加 # SAFETY
注释。
#![allow(unused)] fn main() { // 注意这里 Safety 是用于文档注释,用于说明 unsafe 方法或函数的 安全边界 /// Returns the contained [`Some`] value, consuming the `self` value, /// without checking that the value is not [`None`]. /// /// # Safety /// /// Calling this method on [`None`] is *[undefined behavior]*. /// /// [undefined behavior]: https://doc.rust-lang.org/reference/behavior-considered-undefined.html /// /// # Examples /// /// ``` /// let x = Some("air"); /// assert_eq!(unsafe { x.unwrap_unchecked() }, "air"); /// ``` pub unsafe fn unwrap_unchecked(self) -> T { match self { Some(val) => val, // 这里存在一个 Unsafe block ,应该为其增加 SAFETY 注释来说明其 // 但是现在没有相关注释 None => unsafe { hint::unreachable_unchecked() }, } } }
【正例】
已在 unsafe 块之前增加 # SAFETY
注释。
#![allow(unused)] fn main() { // 注意这里 Safety 是用于文档注释,用于说明 unsafe 方法或函数的 安全边界 /// Returns the contained [`Some`] value, consuming the `self` value, /// without checking that the value is not [`None`]. /// /// # Safety /// /// Calling this method on [`None`] is *[undefined behavior]*. /// /// [undefined behavior]: https://doc.rust-lang.org/reference/behavior-considered-undefined.html /// /// # Examples /// /// ``` /// let x = Some("air"); /// assert_eq!(unsafe { x.unwrap_unchecked() }, "air"); /// ``` pub unsafe fn unwrap_unchecked(self) -> T { match self { Some(val) => val, // 这里存在一个 Unsafe block ,为其增加 SAFETY 注释来说明其 // SAFETY: The safety contract must be upheld by the caller. None => unsafe { hint::unreachable_unchecked() }, } } }
G.UNS.SAS.01 在公开的 unsafe 函数的文档中必须增加 Safety 注释
【级别】 要求
【描述】
在公开(pub)的 unsafe 函数文档中,必须增加 # Safety
注释来解释该函数的安全边界,这样使用该函数的用户才可以安全地使用它。
说明: 该规则通过 cargo clippy 来检测。默认会发出警告。
【反例】
#![allow(unused)] fn main() { // 不符合 /// Creates a `Vec<T>` directly from the raw components of another vector. pub unsafe fn from_raw_parts(ptr: *mut T, length: usize, capacity: usize) -> Self { unsafe { Self::from_raw_parts_in(ptr, length, capacity, Global) } } }
【正例】
示例来自于标准库文档: https://doc.rust-lang.org/stable/src/alloc/vec/mod.rs.html#1167
#![allow(unused)] fn main() { // 符合 /// Creates a `Vec<T>` directly from the raw components of another vector. /// /// # Safety /// /// This is highly unsafe, due to the number of invariants that aren't /// checked: /// /// * `ptr` needs to have been previously allocated via [`String`]/`Vec<T>` /// (at least, it's highly likely to be incorrect if it wasn't). /// * `T` needs to have the same size and alignment as what `ptr` was allocated with. /// (`T` having a less strict alignment is not sufficient, the alignment really /// needs to be equal to satisfy the [`dealloc`] requirement that memory must be /// allocated and deallocated with the same layout.) /// * `length` needs to be less than or equal to `capacity`. /// * `capacity` needs to be the capacity that the pointer was allocated with. pub unsafe fn from_raw_parts(ptr: *mut T, length: usize, capacity: usize) -> Self { unsafe { Self::from_raw_parts_in(ptr, length, capacity, Global) } } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | 默认 level |
---|---|---|---|---|
missing_safety_doc | yes | no | Style | warn |
G.UNS.SAS.02 在 Unafe 函数中应使用 assert!
而非 debug_assert!
去校验边界条件
【级别】 要求
【描述】
assert!
宏在 Release 和 Debug 模式下都会被检查,并且不能被禁用。它通常用来在 unsafe 函数中判断传入的参数是否满足某种边界条件,以此来防止不合法的参数传入导致未定义行为。
但是 debug_assert!
则可以通过配置 -C debug-assertions
来禁用它, 而且 debug_assert!
在 Release 模式下也会被编译器优化。所以,一旦使用了 debug_assert!
在 unsafe 函数中用来防范不合法参数,那有可能会失效。
【反例】
#![allow(unused)] #![warn(clippy::debug_assert_with_mut_call)] fn main() { // 不符合 // 使用了 debug_assert! 那就说明这个校验在 Release 模式不一定有效 // 那么该函数就要被标记为 unsafe pub unsafe fn split_at_mut(&mut self, mid: usize) -> (&mut [T], &mut [T]) { debug_assert!(mid <= self.len()); // 注意,这里是 debug_assert! // SAFETY: `[ptr; mid]` and `[mid; len]` are inside `self`, which // fulfills the requirements of `from_raw_parts_mut`. unsafe { self.split_at_mut_unchecked(mid) } } // 不符合 // 在 debug_assert_eq! 中包含可变引用的调用, // 也会因为 debug_assert_ 系列的断言宏在 Release 下产生不可预料的结果,它是 unsafe 的 debug_assert_eq!(vec![3].pop(), Some(3)); }
【正例】
来自标准库 slice
的代码示例。
#![allow(unused)] #![warn(clippy::debug_assert_with_mut_call)] fn main() { // 符合 pub fn split_at_mut(&mut self, mid: usize) -> (&mut [T], &mut [T]) { assert!(mid <= self.len()); // 判断边界条件,杜绝非法参数 // SAFETY: `[ptr; mid]` and `[mid; len]` are inside `self`, which // fulfills the requirements of `from_raw_parts_mut`. unsafe { self.split_at_mut_unchecked(mid) } } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | 默认 level |
---|---|---|---|---|
debug_assert_with_mut_call | yes | no | nursery | allow |
注意该 lint 当前是 Nursery Group,意味着可能会产生误报 Bug。
裸指针操作
Rust提供了*const T
(不变)和*mut T
(可变)两种指针类型。因为这两种指针和C语言中的指针十分相近,所以叫其原生指针(Raw Pointer)。
原生指针具有以下特点:
- 并不保证指向合法的内存。比如很可能是一个空指针。
- 不能像智能指针那样自动清理内存。需要像 C 语言那样手动管理内存。
- 没有生命周期的概念,也就是说,编译器不会对其提供借用检查。
- 不能保证线程安全。
可见,原生指针并不受Safe Rust提供的那一层“安全外衣”保护,所以也被称为“裸指针”。
P.UNS.PTR.01 不要将裸指针在多线程间共享
【描述】
裸指针在 Rust 中不是线程安全的,将裸指针在多线程传递编译器也会编译出错。如果需要在多线程间共享裸指针,则考虑使用 NewType
模式来包装它。
【正例】
#![allow(unused)] fn main() { struct MyBox(*mut u8); unsafe impl Send for MyBox {} unsafe impl Sync for MyBox {} }
P.UNS.PTR.02 建议使用 NonNull<T>
来替代 *mut T
【描述】
尽量使用 NonNull
来包装 *mut T
。
NonNull
的优势:
- 非空指针。会自动检查包装的指针是否为空。
- 协变。方便安全抽象。如果用裸指针,则需要配合
PhantomData
类型来保证协变。
【正例】
#![allow(unused)] fn main() { use std::ptr::NonNull; let mut x = 0u32; let ptr = NonNull::<u32>::new(&mut x as *mut _).expect("ptr is null!"); if let Some(ptr) = NonNull::<u32>::new(std::ptr::null_mut()) { unreachable!(); } }
P.UNS.PTR.03 使用指针类型构造泛型结构体时,需要使用 PhantomData<T>
来指定 T
上的协变和所有权
【描述】
PhantomData<T>
是经常被用于 Unsafe Rust 中配合裸指针来指定协变和所有权的,为裸指针构建的类型保证安全性和有效性。否则,可能会产生未定义行为。
参考: PhantomData<T>
的型变(variance)模式表
【反例】
#![allow(unused)] fn main() { // Vec<T> 不拥有类型 T,并且 data 字段的裸指针不支持协变 // 这样的话,是有风险的。 // 为 Vec<T> 实现的 Drop 可能导致 UB struct Vec<T> { data: *const T, len: usize, cap: usize, } }
【正例】
#![allow(unused)] fn main() { use std::marker; struct Vec<T> { data: *const T, // *const for variance! len: usize, cap: usize, _marker: marker::PhantomData<T>, // 让 Vec<T> 拥有 T,并且让 指针有了协变 } }
G.UNS.PTR.01 当指针类型被强转为和当前内存对齐不一致的指针类型时,禁止对其解引用
【级别】 建议
【描述】
对于指针类型被强转为和当前内存对齐不一致的指针类型的情况,要注意不要对这类强转后的指针进行解引用操作,否则会有未定义行为。
【反例】
fn main() { let a = (&1u8 as *const u8) as *const u16; // 不符合 let b = (&mut 1u8 as *mut u8) as *mut u16; // 不符合 let c = (&1u8 as *const u8).cast::<u16>(); // 不符合 // Undefined Behavior: dereferencing pointer failed: alloc1411 has size 1, so pointer to 2 bytes starting at offset 0 is out-of-bounds unsafe { *a }; // Undefined Behavior: dereferencing pointer failed: alloc1411 has size 1, so pointer to 2 bytes starting at offset 0 is out-of-bounds unsafe { *b }; // Undefined Behavior: dereferencing pointer failed: alloc1411 has size 1, so pointer to 2 bytes starting at offset 0 is out-of-bounds unsafe { *c }; }
【正例】
fn main() { let a = (&1u8 as *const u8) as *const u8; // 符合 let b = (&mut 1u8 as *mut u8) as *mut u8; // 符合 let c = (&1u8 as *const u8).cast::<u8>(); // 符合 // safe unsafe { *a }; // safe unsafe { *b }; // safe unsafe { *c }; }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
cast_ptr_alignment | yes | no | style | warn |
G.UNS.PTR.02 禁止将不可变指针手工转换为可变指针
【级别】 要求
【描述】
因为将不可变指针手工转换为可变指针可能会引发未定义行为。通常有这种需求,合法的手段是使用 UnsafeCell<T>
。
【 反例】
#![allow(unused)] fn main() { fn x(r: &i32) { unsafe { *(r as *const _ as *mut _) += 1; // 不符合 } } }
【正例】
#![allow(unused)] fn main() { use std::cell::UnsafeCell; // 符合 fn x(r: &UnsafeCell<i32>) { unsafe { *r.get() += 1; } } }
【例外】
也有例外情况,当明确知道这种转换会出现什么风险的时候,可以使用,或者在找到合适的解决办法之前 作为一种临时方案,但要加上注释。
#![allow(unused)] fn main() { // https://docs.rs/crate/solana-runtime/1.7.11/source/src/append_vec.rs #[allow(clippy::cast_ref_to_mut)] fn set_data_len_unsafe(&self, new_data_len: u64) { // UNSAFE: cast away & (= const ref) to &mut to force to mutate append-only (=read-only) AppendVec unsafe { *(&self.meta.data_len as *const u64 as *mut u64) = new_data_len; } } // https://docs.rs/crate/mmtk/0.6.0/source/src/policy/space.rs // This is a temporary solution to allow unsafe mut reference. We do not want several occurrence // of the same unsafe code. // FIXME: We need a safe implementation. #[allow(clippy::cast_ref_to_mut)] #[allow(clippy::mut_from_ref)] unsafe fn mut_self(&self) -> &mut Self { &mut *(self as *const _ as *mut _) } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
cast_ref_to_mut | yes | no | correctness | deny |
G.UNS.PTR.03 尽量使用 pointer::cast
来代替 使用 as
强转指针
【级别】 要求
【描述】
使用 pointer::cast
方法转换更加安全,它不会意外地改变指针的可变性,也不会将指针转换为其他类型。
【反例】
#![allow(unused)] fn main() { let ptr: *const u32 = &42_u32; let mut_ptr: *mut u32 = &mut 42_u32; let _ = ptr as *const i32; // 不符合 let _ = mut_ptr as *mut i32; // 不符合 }
【正例】
#![allow(unused)] fn main() { let ptr: *const u32 = &42_u32; let mut_ptr: *mut u32 = &mut 42_u32; let _ = ptr.cast::<i32>(); // 符合 let _ = mut_ptr.cast::<i32>(); // 符合 }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
ptr_as_ptr | yes | no | correctness | deny |
联合体(Union)
Union 是没有 tag 的 Enum,Enum 是有 tag 的Union 。
内存布局 Union 和 Enum 相似。
正因为没有 tag,Rust 编译器无法检查当前使用的正是哪个变体,所以,访问 Union 的变体是 Unsafe 的。
P.UNS.UNI.01 除了与 C 交互,尽量不要使用 Union
【描述】
Rust 支持 Union 就是为了调用 C 接口。如果不是 FFi ,就避免使用 Union。
一般情况下请使用 枚举 或 结构体代替。
使用 Copy 类型的值和 ManuallyDrop
来初始化 Union 的变体,不需要使用 Unsafe 块。
【反例】
#![allow(unused)] fn main() { union MyUnion { f1: u32, f2: f32, } }
【正例】
#![allow(unused)] fn main() { #[repr(C)] union MyUnion { f1: u32, f2: f32, } }
P.UNS.UNI.02 不要把联合体的不同变体用在不同生命周期内
【描述】
对联合体的变体进行借用的时候,要注意其他变体也将在同一个生命周期内。抛开内存布局、安全性和所有权之外,联合体的行为和结构体完全一致,你可以将联合体当做结构体来进行判断。
【反例】
#![allow(unused)] fn main() { // ERROR: cannot borrow `u` (via `u.f2`) as mutable more than once at a time fn test() { let mut u = MyUnion { f1: 1 }; unsafe { let b1 = &mut u.f1; // ---- first mutable borrow occurs here (via `u.f1`) let b2 = &mut u.f2; // ^^^^ second mutable borrow occurs here (via `u.f2`) *b1 = 5; } // - first borrow ends here assert_eq!(unsafe { u.f1 }, 5); } }
内存
这里指 Unsafe Rust 下的数据布局、内存管理和使用相关规范。
P.UNS.MEM.01 要注意选择合适的结构体、元组、枚举的数据布局
【描述】
对于 Rust 中结构体和元组,编译器会随意重排其字段来优化布局。请根据具体的场景来选择合适的数据布局。
可以通过以下 #[repr]
属性来控制结构体和元组的数据布局:
#[repr(Rust)]
,默认 Rust 数据布局#[repr(C)]
,与 C 兼容的布局#[repr(align(N))]
,指定对齐方式#[repr(packed)]
,指定字段将不在内部对齐#[repr(transparent)]
,让包含单个字段的结构体布局和其字段相同
可以通过以下 #[repr]
属性来控制枚举体的数据布局:
- 特定整数类型
#[repr(u8)]
#[repr(u16)]
#[repr(u32)]
#[repr(u64)]
#[repr(i8)]
#[repr(i16)]
#[repr(i32)]
#[repr(i64)]
- C 兼容布局
#[repr(C)]
- 指定判别式大小的 C 兼容布局
#[repr(C, u8)]
#[repr(C, u16)]
- 以此类推
枚举需要注意的地方:
- 枚举不允许通过
#[repr(align)]
手动指定对齐方式。 - 空枚举不能使用
repr
属性 - 无字段枚举不允许指定判别式大小的 C 兼容布局,比如
[repr(C, Int)]
- 数据承载(有字段)枚举则允许所有类型的
repr
属性
P.UNS.MEM.02 不能修改其它进程或动态库的内存变量
【描述】
除非调用合法的API,否则不要尝试修改其它进程/动态库的内存数据,否则会出现内存段错误(SIGSEGV)。
【反例】
sqlite3_libversion()
返回的 sqlite 版本信息指针指向 /usr/lib/libsqlite3.so
动态库的 static 字符串。
libsqlite3.so 中分配的静态字符串不属于进程的内存范围中。
当进程尝试修改 sqlite 动态库的静态字符串内容,操作系统就会发送 SIGSEGV 信号终止进程,以保证 sqlite 动态库的内存数据安全。
#![allow(unused)] fn main() { #[link(name = "sqlite3")] extern "C" { fn sqlite3_libversion() -> *mut std::os::raw::c_char; } fn edit_sqlite_version() { unsafe { let mut sqlite_version = sqlite3_libversion(); // SIGSEGV: invalid memory reference *sqlite_version = 3; } } }
P.UNS.MEM.03 不能让 String/Vec
自动 Drop 其它进程或动态库的内存数据
【描述】
使用 String/Vec
指向其它进程/动态库的内存数据时,一定要手动禁止 String/Vec
的 Drop 方法(析构函数)的调用,避免 free 其它进程/动态库的内存数据。
【反例】
sqlite3_libversion()
返回的 sqlite 版本信息指针指向 /usr/lib/libsqlite3.so
动态库的 static 字符串。
当进程在 String
drop 的时候尝试释放 sqlite 动态库的静态字符串内存时,操作系统就会发送 SIGABRT 信号终止进程,以保证 sqlite 动态库的内存数据安全。
#![allow(unused)] fn main() { #[link(name = "sqlite3")] extern "C" { fn sqlite3_libversion() -> *mut std::os::raw::c_char; } fn print_sqlite_version() { unsafe { let ptr = sqlite3_libversion(); let len = libc::strlen(ptr); let version = String::from_raw_parts(ptr.cast(), len, len); println!("found sqlite3 version={}", version); // SIGABRT: invalid free } } }
【正例】
除了用 mem::forget
或者 ManualDrop
禁止 String
drop 其它动态库的内存,也可以用标准库 ptr/slice
的 copy
或者 libc::strdup
将 sqlite 的版本信息字符串复制到当前进程的内存空间再进行操作
#![allow(unused)] fn main() { fn print_sqlite_version() { unsafe { let ptr = sqlite3_libversion(); let len = libc::strlen(ptr); let version = String::from_raw_parts(ptr.cast(), len, len); println!("found sqlite3 version={}", version); // 手动禁止 String 的析构函数调用 std::mem::forget(version); } } }
P.UNS.MEM.04 尽量用可重入(reentrant)版本的 C-API 或系统调用
【描述】
以 Linux 系统为例,在 glibc(/usr/lib/libc.so) 等知名 C 语言库中,很多 API 会既提供不可重入版本和**可重入(reentrant)**版本,例如 ctime 和 ctime_r 这对系统调用。可重入版本的函数命名一般带 _r 的后缀,_r 也就是单词可重入 reentrant 的缩写。
libc 中不可重入函数的执行过程一般是将函数的输出写到动态库的某个 static 命令内,然后再返回指向该 static 变量的指针返回给调用方,因此是一种「有状态」的函数,多线程环境下可能有线程安全问题。
使用不可重入函数的风险会导致开发人员带来很大的心智负担,需要耗费人力进行代码安全评审确保没有线程安全和内存安全问题,因此必须尽量使用可重入版本的函数。
【反例】
ctime
, gmtime
, localtime
, gethostbyname
【正例】
chrono
库中用 libc::localtime_r
获取本地时间而不用 libc::localtime
。
还有诸如 ctime_r
, gmtime_r
, localtime_r
, gethostbyname_r
等。
P.UNS.MEM.05 如果需要使用位域,推荐使用第三方库
【描述】
位域(或称“位段”, Bit field)为一种数据结构,可以把数据以位的形式紧凑的存储,并允许程序员对此结构的位进行寻址和操作。
这种数据结构的好处:
- 可以使数据单元节省存储空间,当程序需要成千上万个数据单元时,这种方法就显得尤为重要。
- 位域可以很方便的访问一个整数值的部分内容从而可以简化程序源代码。
而位域这种数据结构的缺点在于,其内存分配与内存对齐的实现方式依赖于具体的机器和系统,在不同的平台可能有不同的结果,这导致了位域在本质上是不可移植的。所以目前 Rust 并未在语言内置位域支持。
Rust 中使用位域时推荐使用的第三方库:
- bitvec,是 Cpp 中
std::vector<bool>
和std::bitset
数据结构的 Rust 实现。 在性能、功能性、API完整性和表现力方面,该库都非常优秀。 - bitflags,提供了方便的宏语法来定义和处理位域。
- modular-bitfield,提供了一些宏来定义和处理位域,并且是完全基于 Safe Rust。
以上三个库都支持 no-std
。
【正例】
以 bitvec
为例。 示例参考:https://myrrlyn.net/blog/misc/bitfields-in-rust
C 语言中定义位域。
Struct SixFlags {
uint16_t eins : 3;
uint16_t zwei : 2;
uint16_t drei : 3;
uint16_t vier : 3;
uint16_t funf : 2;
uint16_t seid : 3;
}
type SixFlagsBits = BitSlice<Local, u16>; #[repr(C)] #[derive(Copy, Clone, Default)] pub struct SixFlags { inner: u16, }; impl SixFlags { pub fn eins(&self) -> &SixFlagsBits { &self.inner.bits()[0 .. 3] } pub fn eins_mut(&mut self) -> &mut SixFlagsBits { &mut self.inner.bits()[0 .. 3] } pub fn zwei(&self) -> &SixFlagsBits { &self.inner.bits()[3 .. 5] } pub fn zwei_mut(&mut self) -> &mut SixFlagsBits { &mut self.inner.bits()[3 .. 5] } } fn main() { let mut flags = SixFlags::default(); flags.eins_mut().store(2u8); flags.zwei_mut().store(0u8); flags.drei_mut().store(4u8); flags.vier_mut().store(5u8); flags.funf_mut().store(1u8); flags.seis_mut().store(7u8); }
G.UNS.MEM.01 使用 MaybeUninit<T>
来处理未初始化的内存
【级别】 要求
【描述】
Rust 编译器要求变量要根据其类型正确初始化。
比如引用类型的变量必须对齐且非空。这是一个必须始终坚持的不变量,即使在 Unsafe 代码中也是如此。因此,零初始化引用类型的变量会导致立即未定义行为,无论该引用是否访问过内存。
编译器利用这一点,进行各种优化,并且可以省略运行时检查。
使用前请仔细查看 MaybeUninit<T>
相关文档。
【反例】
由调用者来保证MaybeUninit<T>
确实处于初始化状态。当内存尚未完全初始化时调用 assume_init()
会导致立即未定义的行为。
#![allow(unused)] fn main() { use std::mem::{self, MaybeUninit}; // 不符合:零初始化引用 let x: &i32 = unsafe { mem::zeroed() }; // undefined behavior! ⚠️ // 等价于 `MaybeUninit<&i32>`: let x: &i32 = unsafe { MaybeUninit::zeroed().assume_init() }; // undefined behavior! // 不符合:布尔值必须初始化 let b: bool = unsafe { mem::uninitialized() }; // undefined behavior! ⚠️ // 等价于 `MaybeUninit<bool>`: let b: bool = unsafe { MaybeUninit::uninit().assume_init() }; // undefined behavior! // 不符合:整数类型也必须初始化 let x: i32 = unsafe { mem::uninitialized() }; // undefined behavior! ⚠️ // 等价于 `MaybeUninit<i32>`: let x: i32 = unsafe { MaybeUninit::uninit().assume_init() }; // 不符合:Vec未初始化内存使用 set_len 是未定义行为 let mut vec: Vec<u8> = Vec::with_capacity(1000); unsafe { vec.set_len(1000); } reader.read(&mut vec); // undefined behavior! }
【正例】
#![allow(unused)] fn main() { use std::mem::MaybeUninit; let mut x = MaybeUninit::<bool>::uninit(); x.write(true); // 符合:这里正确进行了初始化 let x_init = unsafe { x.assume_init() }; // 通过 assume_init 对 MaybeUninit 的内存取值 assert_eq!(x_init, true); // 符合:下面数组应该是可以的 let _: [MaybeUninit<bool>; 5] = unsafe { MaybeUninit::uninit().assume_init() }; // 符合:Vec 未初始化内存正确处理 let mut vec: Vec<u8> = vec![0; 1000]; reader.read(&mut vec); // 符合 let mut vec: Vec<MaybeUninit<T>> = Vec::with_capacity(1000); vec.set_len(1000); // `MaybeUninit` can be uninitialized // 符合: let mut vec: Vec<u8> = Vec::with_capacity(1000); let remaining = vec.spare_capacity_mut(); // `&mut [MaybeUninit<u8>]` // perform initialization with `remaining` vec.set_len(...); // Safe to call `set_len()` on initialized part }
【例外】
在能保证 MaybeUninit
不需要初始化的情况下使用 assume_init
是安全的。
#![allow(unused)] fn main() { pub unsafe trait Array: Sized { /// Same array but item is wrapped with /// [`MaybeUninit<_>`](core::mem::MaybeUninit). /// ``` /// # use arraylib::Array; fn dummy<T>() where /// [T; 4]: Array<Item = T, Maybe = [core::mem::MaybeUninit<T>; 4]> /// # {} /// ``` type Maybe: Array<Item = MaybeUninit<Self::Item>>; /// [`MaybeUninit<T>`]: core::mem::MaybeUninit #[inline] // Initializing generic type with uninitialized state seems insane, but is // unsafe trait and `Array` guarantees that it's an array. And `Array::Maybe` // is an array of `MaybeUninit` that doesn't require initialization, so // everything is ok // 这里是一个数组,可以保证不需要去初始化 #[allow(clippy::uninit_assumed_init)] fn uninit() -> Self::Maybe { unsafe { // ## Safety // // Completely safe as `MaybeUninit` don't require initialization MaybeUninit::uninit().assume_init() } } } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
uninit_assumed_init | yes | no | correctness | deny |
uninit_vec | yes | no | correctness | deny |
FFi 规范
Rust 可以通过C-ABI无缝与C语言打交道,也可以通过暴露 C-ABI 接口供其他语言调用。但是跨边界本质上是不安全的。
一般来说,FFi 是指在其他语言中调用 Rust 代码,Rust代码会按 C-ABI 来暴露接口。这类 Rust crate或模块,常以 -ffi
后缀结尾。
另一类是 Rust 去调用 C-ABI 接口,相关代码通常被封装到以 -sys
为后缀命名的 crate 或 模块中。
本小节内容,包含以上两种情况。
P.UNS.FFI.01 避免从公开的 Rust API 直接传字符串到 C 中
【描述】
在跨越 C 边界的时候,应该对 字符串进行边界检查,避免传入一些非法字符串。
【正例】
这个示例中,从公开的 Rust API
传入非法字符串到 C
,导致字符串格式化漏洞。
// From: https://github.com/RustSec/advisory-db/issues/106 extern crate pancurses; use pancurses::{initscr, endwin}; fn main() { let crash = "!~&@%+ S"; // 特意构造非法字符串 let window = initscr(); window.printw(crash); // 通过该函数跨 C 边界传入非法字符串,引起字符串格式化漏洞 window.refresh(); window.getch(); endwin(); }
P.UNS.FFI.02 在使用标准库 std::ffi
模块提供的类型时需要仔细查看其文档
【描述】
因为该模块中提供了用于和其他语言类 C 字符串打交道的 FFi 绑定和类型,在使用前务必要看清楚它们的文档,否则会因为所有权管理不当而导致无效内存访问、内存泄漏和其他内存错误。
P.UNS.FFI.03 当使用来自 C 的指针时,如果该指针需要管理内存,则需要为包装该指针的 Rust 类型实现 Drop 特质
【描述】
Rust 里通过结构体包装该指针,并且为该结构体实现 Drop 来保证相关资源可以安全释放。
【正例】
下面示例中 *mut sys::VMContext
是来自于外部的 C-ABI
指针,它需要管理内存,所以在 Rust 这边使用结构体包装该指针,并实现 Drop
,通过 Drop
来调用 C-ABI
回调函数来释放内存。
#![allow(unused)] fn main() { pub struct Vm { pub(crate) ctx: *mut sys::VMContext, } impl Drop for Vm { fn drop(&mut self) { if !self.ctx.is_null() { unsafe { sys::VMDelete(self.ctx) }; } } } }
P.UNS.FFI.04 如果一个函数正在跨越 FFi 边界,那么需要处理 Panic
【描述】
如果让 Panic 在跨越 FFi 边界时发生,可能会产生未定义行为。
处理 Panic 可以使用 catch_unwind
,但是它只对实现了 UnwindSafe
trait 的类型起作用。另外一种方法就是避免 Panic,而返回错误码。
【正例】
use std::panic::catch_unwind; #[no_mangle] pub extern fn oh_no() -> i32 { let result = catch_unwind(|| { panic!("Oops!"); // 这里会发生 Panic,需要处理 }); match result { Ok(_) => 0, Err(_) => 1, } } fn main() {}
P.UNS.FFI.05 建议使用诸如标准库或 libc
crate 所提供的可移植类型别名,而不是特定平台的类型
【描述】
当与外部(如 C 或 C++)接口交互时,通常需要使用平台相关的类型,如 C 的 int
、long
等。除了 std::ffi
(或 core::ffi
)中的 c void 外,标准库还在 std:os::raw
(或 core::os::raw
)中提供了可移植类型别名。libc
crate 基本覆盖了所有的 C 标准库中的 C 兼容类型。
这样有助于编写跨平台的代码。
P.UNS.FFI.06 Rust 和 C 之间传递字符或字符串时需要注意字符串要符合 C-ABI 以及 字符串的编码
【描述】
注意要使用 c_char
对应 C 语言的字符。libc::c_char
和 std::os::raw::c_char
在大多数 64位 linux 上都是相同的。
FFi 接口使用的字符串要符合 C 语言约定,即使用 \0
结尾且中间不要包含 \0
字符的字符串。
Rust 中字符串要求 utf-8
编码,而 C 字符串则没有这个要求。所以需要注意编码。
【反例】
#![allow(unused)] fn main() { let f = libc::fopen("/proc/uptime".as_ptr().cast(), "r".as_ptr().cast()); // 即使 /proc/uptime 文件存在,fopen 系统调用也会返回 NULL // 并且将错误码 errno 标记为 2 ("No such file or directory") }
【正例】
#![allow(unused)] fn main() { let f = libc::fopen("/proc/uptime\0".as_ptr().cast(), "r\0".as_ptr().cast()); }
P.UNS.FFI.07 不要为任何传出外部的类型实现 Drop
【描述】
因为有可能在传出去之前被析构。需要明确是由哪种语言负责分配和释放内存,谁分配内存,谁来释放。
P.UNS.FFI.08 FFi 中要进行合理的错误处理
【描述】
不同类型的错误代码,需要不同的处理方式:
- 无字段枚举,应该转换为数字并且作为返回码。
- 数据承载(有字段)枚举,应该转换为携带错误信息的整数码。
- 自定义错误类型应该使用兼容 C 的布局
【正例】
#![allow(unused)] fn main() { // 无字段枚举 enum DatabaseError { IsReadOnly = 1, // user attempted a write operation IOError = 2, // user should read the C errno() for what it was FileCorrupted = 3, // user should run a repair tool to recover it } impl From<DatabaseError> for libc::c_int { fn from(e: DatabaseError) -> libc::c_int { (e as i8).into() } } // 数据承载(有字段)枚举 pub mod errors { enum DatabaseError { IsReadOnly, IOError(std::io::Error), FileCorrupted(String), // message describing the issue } impl From<DatabaseError> for libc::c_int { fn from(e: DatabaseError) -> libc::c_int { match e { DatabaseError::IsReadOnly => 1, DatabaseError::IOError(_) => 2, DatabaseError::FileCorrupted(_) => 3, } } } } pub mod c_api { use super::errors::DatabaseError; #[no_mangle] pub extern "C" fn db_error_description( e: *const DatabaseError ) -> *mut libc::c_char { let error: &DatabaseError = unsafe { // SAFETY: pointer lifetime is greater than the current stack frame &*e }; let error_str: String = match error { DatabaseError::IsReadOnly => { format!("cannot write to read-only database"); } DatabaseError::IOError(e) => { format!("I/O Error: {}", e); } DatabaseError::FileCorrupted(s) => { format!("File corrupted, run repair: {}", &s); } }; let c_error = unsafe { // SAFETY: copying error_str to an allocated buffer with a NUL // character at the end let mut malloc: *mut u8 = libc::malloc(error_str.len() + 1) as *mut _; if malloc.is_null() { return std::ptr::null_mut(); } let src = error_str.as_bytes().as_ptr(); std::ptr::copy_nonoverlapping(src, malloc, error_str.len()); std::ptr::write(malloc.add(error_str.len()), 0); malloc as *mut libc::c_char }; c_error } } // 自定义错误类型 struct ParseError { expected: char, line: u32, ch: u16 } impl ParseError { /* ... */ } /* Create a second version which is exposed as a C structure */ #[repr(C)] pub struct parse_error { pub expected: libc::c_char, pub line: u32, pub ch: u16 } impl From<ParseError> for parse_error { fn from(e: ParseError) -> parse_error { let ParseError { expected, line, ch } = e; parse_error { expected, line, ch } } } }
P.UNS.FFI.09 当 Rust 调用外部 C 函数时,如果可以确认安全,可以通过引用来代替裸指针
【描述】
在确认安全的前提下,在声明外部 C 函数时可以直接使用引用形式, C 语言可以使用正确绑定。
P.UNS.FFI.10 当 Rust 函数导出外部函数时,必须从设计上保证被跨线程调用的安全性
【描述】
当 Rust 函数被导出为外部函数接口时,要保证其被跨线程调用的安全性。除非调用它的环境是单线程。
【正例】
#![allow(unused)] fn main() { #[no_mangle] pub extern "C" fn nic_udrv_suspend() { NIC_ENTITY.try_borrow_mut().suspend(); // suspend()需要可变引用 } // 对外被 C 调用的接口 #[no_mangle] pub extern "C" fn nic_udrv_buf_recycle(buf_id: usize) { NIC_ENTITY.try_borrow().buf_recycle(buf_id); // buf_recycle()内有锁可以避免多线程竞争 } }
P.UNS.FFI.11 如需引用指定为 #[repr(packed)]
内存布局的结构体成员字段要注意合理规避未定义行为
【级别】 要求
【描述】
Rust 中的引用类型要求必须是类型对齐的,当结构体使用了 #[repr(packed)]
设置内部对齐方式,而外部成员的地址又不是类型对齐时,对其成员的引用会引发编译期 unaligned_references
的警告,该警告在将来发布的 Rust 版本中会演变为错误。
注: 类型对齐是指一个类型的起始地址应该是其类型大小的整数倍,如 u32 的大小是4,则 u32 所在地址 addr 需要满足 addr % 4 == 0。
推荐解决方法:
- 使用
raw pointer
代替引用,且使用ptr::read_unaligned
/ptr::write_unaligned
函数。 - 可以复制结构体字段内容给一个本地变量,然后使用本地变量的引用。但是这个要求结构体必须实现
Copy
trait。
对第二种方法的进一步说明:
- 结构体使用
packed
通常是为了匹配某种标准定义或二进制形式,在这种场景下结构体需要#[repr(C)]
来稳定内存布局,通常也会通过#[derive(Copy)]
来实现Copy
trait,这种情况不存在该问题;否则需要手动实现Copy
trait,或者通过增加中间变量,显示地发生 move,使用结束后再 move 回原结构体; - 通过该方法访问的是临时拷贝的数据,那如果需要改变原数据怎么做?
- 若通过某个引用修改其指向的数据,则该引用一定已在某个时间点被创建,那么在创建时,若其合法,则可正常对其操作,否则在创建时就已报
unaligned_references
,又回到了该问题,对其操作可能会造成Undefined Behavior
; - 若结构体的成员地址不对齐,又需要进行修改,建议通过结构体来操作,如
foo.baz = 4
,编译器能够完成该工作。
- 若通过某个引用修改其指向的数据,则该引用一定已在某个时间点被创建,那么在创建时,若其合法,则可正常对其操作,否则在创建时就已报
【反例】
#![deny(unaligned_references)] #[repr(packed)] pub struct Foo { field1: u64, field2: u8, } fn main() { unsafe { let foo = Foo { field1: 0, field2: 0 }; let _ = &foo.field1; // UB Error. triggering the lint. println!("{}", foo.field1); // UB Error. An implicit `&` is added here, triggering the lint. } }
【正例】
使用第二种方法。
#![deny(unaligned_references)] #[derive(Copy, Clone)] #[repr(packed)] pub struct Foo { field1: u64, field2: u8, } fn main() { unsafe { let foo = Foo { field1: 0, field2: 0 }; let field1 = foo.field1; // 此处会 Copy field1 let _ = &field1; println!("{}", field1); } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | 是否可定制 |
---|---|---|---|---|
_ | no | yes | _ | _ |
【参考】
https://doc.rust-lang.org/stable/nightly-rustc/rustc_lint/builtin/static.UNALIGNED_REFERENCES.html
P.UNS.FFI.12 当依赖 C 端传入参数时,需要在文档注释中不变性声明,根据不同的调用场景选择合适的安全抽象方式
【级别】 建议
【描述】
当 Rust 代码中依赖 C 接口传入的参数时,比如指针或其他类型的参数,需要在文档注释中不变性声明。
如果在调用场景可以确定所传入的参数都是有效的,比如指针不会是空指针等情况时,我们可以通过文档注释中对该接口做不变性声明(type invariant),来强调对依赖的 C 代码的信任,这样做的好处有两点:
- 可以避免检查,而达到零成本安全抽象的目的。
- 可以减少 unsafe 函数声明。
具体可以参考【正例】的代码注释。
【正例】
场景一:如果在调用场景上能确定所传入的参数都是有效时。
#![allow(unused)] fn main() { /// # Invariants (不变性说明) /// /// 这个指针来自 C语言 端,这里默认 C 端来的是有效指针,信任 C 端 /// 这种信任对性能有益:零成本(没有检查开销) pub struct CPtr(*mut bindings::cptr); impl CPtr { /// # Safety /// /// 这里使用unsafe 函数,是因为确实无法保证传入的指针是否有效, /// 这是构造 CPtr结构体实例的入口方法,所以有必要声明为 unsafe pub unsafe fn new(cptr: *mut bindings::cptr) -> Self { Self(cptr) } // 安全性说明:这个指针已经有上面结构体定义时注释中的不变性声明来承诺安全了 // 所以这是个方法没必要加 unsafe,因为已经有 new 方法标记为 unsafe 足以 // 这种做法可以消除很多 unsafe 函数了 // 同时,也不需要去检查 self.0 这个指针是不是空指针了 pub fn get(&self) -> usize { // SAFETY: The pointer is valid by the type invariant. unsafe { bindings::cptr_get(self.0) as usize } } // 同上 pub fn enable(self) -> Result<EnabledCPtr> { // SAFETY: The pointer is valid by the type invariant. to_result(|| unsafe { bindings::cptrenable(self.0) })?; Ok(EnabledCPtr(self)) } impl Drop for CPtr { // 同上 fn drop(&mut self) { // SAFETY: The pointer is valid by the type invariant. unsafe { bindings::clk_put(self.0) }; } } } }
场景二:如果在调用场景上能无法确定所传入的参数都是有效时。
#![allow(unused)] fn main() { /// # Invariants (不变性说明) /// /// 这个指针来自 C语言 端,这里默认 C 端来的是有效指针,信任 C 端 /// 这种信任对性能有益:零成本(没有检查开销) pub struct CPtr(*mut bindings::cptr); impl CPtr { // 这里对空指针进行判断 // 虽然抽象为了安全方法,但是如果不能panic的情况就比较麻烦,比如用 Rust 写 Linux 内核驱动 // 或者,还需要进行错误处理,增加针对空指针的错误类型,增加复杂性 pub fn new(cptr: *mut bindings::cptr) -> Self { if cptr.is_null() { panic!("CPtr should not be null!") } Self(cptr) } // 因为 new 已经安全了,所以这个方法也安全 pub fn get(&self) -> usize { // SAFETY: The pointer is valid by the type invariant. unsafe { bindings::cptr_get(self.0) as usize } } // 同上 pub fn enable(self) -> Result<EnabledCPtr> { // SAFETY: The pointer is valid by the type invariant. to_result(|| unsafe { bindings::cptrenable(self.0) })?; Ok(EnabledCPtr(self)) } impl Drop for CPtr { // 同上 fn drop(&mut self) { // SAFETY: The pointer is valid by the type invariant. unsafe { bindings::clk_put(self.0) }; } } } }
P.UNS.FFI.13 自定义数据类型要保证一致的数据布局
【描述】
Rust 编译器为了优化内存布局,会对结构体字段进行重排。所以在 FFi 边界,应该注意结构体内存布局和 C 语言的一致。
关于 如何选择合适的 repr
属性可参考:P.UNS.MEM.01
以下是不适合用于和 C 语言交互的类型:
- 没有使用任何
#[repr( )]
属性修饰的自定义类型 - 动态大小类型 (dynamic sized type)
- 指向动态大小类型对象的指针或引用 (fat pointers)
- str 类型、tuple 元组、闭包类型
【正例】
#![allow(unused)] fn main() { #[repr(C)] struct Data { a: u32, b: u16, c: u64, } #[repr(C, packed)] struct PackedData { a: u32, b: u16, c: u64, } }
P.UNS.FFI.14 在 FFi 中使用的类型应该拥有稳定布局
【描述】
FFi-Safe: 通过 FFi 外部传递的结构体类型都要满足内存布局的稳定性。
为结构体添加 #[repr(C)]
或 #[repr(transparent)]
可以让结构体拥有稳定的布局。
零大小类型在 C 中是无效的。也不要把 Rust 中的单元类型 ()
和 C 中的 void
混为一谈。所以不应该在 FFi 中使用零大小类型(ZST)。
【反例】
#![allow(unused)] fn main() { // Foo 为零大小类型 // No FFi Safe #[repr(C)] pub struct Foo; extern { fn get_some_instance() -> *mut Foo; } }
【正例】
#![allow(unused)] fn main() { // 如果 C 函数需要 opaque 类型,可以使用 libc::c_void 解决 extern crate libc; extern "C" { pub fn foo(arg: *mut libc::c_void); pub fn bar(arg: *mut libc::c_void); } // 如果一定要使用零大小类型,比如 C 函数中返回一个结构体指针 // 可以按下面这种方式 #[repr(C)] pub struct Foo { _unused: [u8; 0]} // 理论上上面结构体应该是下面空枚举的一种等价模拟,因为现在 Rust 编译器还不支持给空枚举设置布局 // #[repr(C)] pub enmu Foo{}; extern { fn get_some_instance() -> *mut Foo; } }
P.UNS.FFI.15 从外部传入的不健壮类型的外部值要进行检查
【描述】
Safe Rust 会保证类型的有效性和安全性,但是 Unsafe Rust 中,特别是编写 FFi 的时候,很容易从外部传入无效值。
Rust 中很多类型都不太健壮:
- 布尔类型。外部传入的布尔类型可能是数字也可能是字符串。
- 引用类型。Rust 中的引用仅允许执行有效的内存对象,但是在Unsafe 中使用引用,任何偏差都可能引起未定义行为。
- 函数指针。跨越 FFi 边界的函数指针可能导致任意代码执行。
- Enum。 跨 FFi 边界两端的 枚举值要经过合法转换。
- 浮点数。
- 包含上述类型的复合类型
P.UNS.FFI.16 给 C 接口传递 Rust 闭包时需要将数据和代码进行分离,并要保证其安全性
【描述】
在 Rust 中,闭包只是一种语法糖,其实质是由编译器生成的匿名结构体和一些 call
方法组成。而 C 语言中只支持函数指针,并不支持 Rust 这种闭包。
因此为 C 接口传递 Rust 闭包的思路是将闭包“拆分”为数据(匿名结构体的实例)和函数(call()
方法)部分来将闭包传递给 C 接口,并且要从以下三方面保证安全性:
- Rust 闭包中捕获变量引用的生命周期有效性。
- 传入的Rust 闭包要实现
std::panic::UnwindSafe
,这样可以保证异常安全 - 传入的 Rust 闭包要实现
Send
,这样可以保证线程安全
【正例】
#![allow(unused)] fn main() { // Safety: 此处需要保证以下不变性来进行安全抽象 // - widget 必须是一个有效的指针 // - 这里因为使用 Rust 引用,所以它一定是有效的 // - 数据必须在其被析构之前有效 // - 这里增加了`'static` 限定来确保它有效 // - 增加 `std::panic::UnwindSafe` 和 `Send` 限定,确保其异常安全和线程安全 fn register_c_callback<F>(widget: &mut ffi::widget_t, callback: F) where F: FnMut(ffi::event_t) + 'static + std::panic::UnwindSafe + Send, { // 需要闭包实例数据保留一定时间,所以将其放到堆上,使用`Box::into_raw`防止其被析构 let data = Box::into_raw(Box::new(callback)); unsafe { // 分别将 数据 和 闭包调用代码 传入 C 接口 ffi::widget_register_callback( widget, data as *mut _, // 数据 call_closure::<F>, // 代码 drop_box::<F>, ); } } // Safety: 传入该函数的指针必须是 `F` 类型的非空指针 // 这里不需要 `#[no_mangle]`,是因为它会以函数指针的方式直接传递给 C ,而不需要通过函数名称调用 unsafe extern "C" fn call_closure<F>( data: *mut libc::c_void, event: ffi::event_t, ) where F: FnMut(ffi::event_t) + 'static + std::panic::UnwindSafe + Send, { let callback_ptr = data as *mut F; let callback = &mut *callback_ptr; callback(event); // 调用闭包 } // 在 C 端手动调用的析构函数 unsafe extern "C" fn drop_box<T>(data: *mut libc::c_void) { Box::from_raw(data as *mut T); } }
P.UNS.FFI.17 当Rust绑定C-API不透明(Opaque)类型时,应该使用指向专用不透明类型的指针而不是c_void
指针
【描述】
使用专门构建的不透明类型相比于直接使用 c_void
可以提供一定程度的类型安全性。
【正例】
C 库中包含了一个不透明类型的 foo 指针和 bar 指针:
void foo(void *arg);
void bar(void *arg);
通过包含私有字段_private
且不包含构造函数,创建了两个无法在此模块之外实例化的不透明类型。空数组既是零大小又可设置布局为#[repr(C)]
。
#![allow(unused)] fn main() { #[repr(C)] pub struct Foo {_private: [u8; 0]} #[repr(C)] pub struct Bar {_private: [u8; 0]} // SAFETY: // 因为 Foo 和 Bar类型不同,所以将在它们两者之间获得类型安全,这样就不可能意外地传递一个指向 `bar()` 的`Foo`指针。 extern "C" { fn foo(arg: *mut Foo); fn bar(arg: *mut Bar); } }
P.UNS.FFI.18 避免将 trait 对象传递给 C 接口
【描述】
Rust 中的多态性主要由 trait 来提供。但是在 FFi 时,将 Rust trait 对象传递给 C 接口,并不能保证 FFi 安全。因为 Rust trait 对象没有稳定的 ABI,所以我们不能通过 Box<dyn Trait>
值传递越过 FFI 边界。
所以,最好的方式是不要在 FFi 时通过传递 trait对象来使用多态性。
如果必须要在 FFi 中使用多态性,有以下几种方式:
- 使用枚举。像 C 传递一个指向枚举的指针。
- 使用
thin_trait_object
模式,是 FFi 安全的。
Unsafe I/O
Rust 标准库提供了 I/O 安全性,保证程序持有私有的原始句柄(raw handle),其他部分无法访问它。但是 FromRawFd::from_raw_fd
是 Unsafe 的,所以在 Safe Rust中无法做到 File::from_raw(7)
这种事。 在这个文件描述符上面进行 I/O
操作,而这个文件描述符可能被程序的其他部分私自持有。
P.UNS.FIO.01 在使用原始句柄的时候,要注意 I/O 安全性
【描述】
很多 API 通过接受原始句柄来进行 I/O 操作:
#![allow(unused)] fn main() { pub fn do_some_io<FD: AsRawFd>(input: &FD) -> io::Result<()> { some_syscall(input.as_raw_fd()) } }
AsRawFd
并没有限制as_raw_fd
的返回值,所以do_some_io
最终可以在任意的RawFd
值上进行 I/O
操作。甚至可以写do_some_io(&7)
,因为RawFd
本身实现了AsRawFd
。这可能会导致程序访问错误的资源。甚至通过创建在其他部分私有的句柄别名来打破封装边界,导致一些诡异的 远隔作用(Action at a distance)。
远隔作用(Action at a distance)是一种程式设计中的反模式,是指程式某一部分的行为会广泛的受到程式其他部分指令的影响,而且要找到影响其他程式的指令很困难,甚至根本无法进行。
在一些特殊的情况下,违反 I/O 安全甚至会导致内存安全。
Unsafe 代码术语指南
来自于:Unsafe Code Guidelines Reference | Glossary
术语 | 中文 | 意义 |
---|---|---|
Alias | 别名 | 当一个指针或引用指向的内存范围(Span)和另一个指针或引用指向的内存区域重叠时,就会产生别名 |
(Pointer) Provenance | 指针来源 | 用于区分指向相同内存地址的指针 |
Interior mutability | 内部可变性 | 意味着一块可变的内存,同时还拥有一个共享引用,并且对其执行内部可变还不会引起 UB |
Validity and safety invariant | 有效性和安全性不变量 | 数据必须是有效的,但它只在安全的代码中才能保证安全 |
Undefined Behavior | 未定义行为 | 最终程序在实际硬件上的表现与源程序根据Rust抽象机的表现不同 |
Soundness | 健全性 | 意味着类型系统是正确的,健全性是类型良好的程序所需的属性 |
Layout | 数据布局 | 用于定义类型的大小和对齐方式,以及它的子对象的偏移量 |
Zero-sized type / ZST | 零大小类型 | 不占用空间的类型,即对齐要求为 1 的类型 |
Niche | 利基 | 一个类型的利基决定了布局优化将使用的 无效位模式(bit-pattern) |
Padding | 填充 | 指编译器在结构体或枚举变体的字段之间填充空间,以满足对齐要求 |
Place | 位置 | 位置是对位置表达式的求值结果(C 语言中叫"左值,lvalue",C++中叫 "广义左值,glvalue") |
Value | 值 | 值是对值表达式的求值结果 (其他语言叫 “右值,rvalue”) |
Representation | 表征 | 用于描述一个类型的值和内存中存储这个值的字节序列之间的关系 |
别名(Alias)
当一个指针或引用指向的内存区域(Span)和另一个指针或引用指向的内存区域重叠时,就会产生别名。
对零大小类型(ZST)的引用和指针从不互相别名,因为它们的内存范围长度总是0
字节。
【示例】
fn main() { let u: u64 = 7_u64; let r: &u64 = &u; let s: &[u8] = unsafe { core::slice::from_raw_parts(&u as *const u64 as *const u8, 8) }; let (head, tail) = s.split_first().unwrap(); }
该示例中,r
和 s
是互为别名,因为它们都指向 u
的内存。
然而,head
和 tail
不是互为别名,head
指向 u
的第一个字节,tail
指向其余字节。但是 head
和 tail
共同与 s
互为别名。
内存范围(Span)是指 引用或指针 指向值(Value)的大小,主要依赖于类型,按以下方式确定:
- 对于一个是
Sized
的类型T
,用size_of::<T>()
可以获取T
的引用或指针的 内存范围长度。 - 当
T
不是Sized
时,就有点麻烦:- 如果你有一个引用
r
,你可以使用size_of_val(r)
来确定该引用的内存范围。 - 如果你有一个指针
p
,你必须在使用size_of_val
之前不安全地(unsafely)将其转换为一个引用。目前还没有一个安全的方法来确定一个非Sized
类型的指针的内存范围。
- 如果你有一个引用
指针来源((Pointer) Provenance)
用于区分指向相同内存地址的指针,即,当强转为 usize
时,会比较是否相等。指针来源只存在于 Rust 的抽象层,在转译以后的二进制文件中,将无法区分指针来源,但是它可以影响编译器对程序的转译。
#![allow(unused)] fn main() { // 我们假设这里的两个分配的基本地址是 0x100 和 0x200 // 我们把指针的出处写成`@N`,其中`N`是某种唯一的ID,用来识别该分配 let raw1 = Box::into_raw(Box::new(13u8)); let raw2 = Box::into_raw(Box::new(42u8)); let raw2_wrong = raw1.wrapping_add(raw2.wrapping_sub(raw1 as usize) as usize); // 这些指针现在有以下值: // raw1指向地址 0x100,其出处为 @1 // raw2指向地址 0x200,并有出处 @2 // raw2_wrong 指向地址 0x200,并有出处 @1 // 换句话说,raw2和raw2_wrong有相同的地址 assert_eq!(raw2 as usize, raw2_wrong as usize); // ...但是对 raw2_wrong 解引用将是不合法的,因为它有错误的来源(provenance): // 它指向地址 0x200,这是在分配 @2中,但这个指针的出处是 @1 }
内部可变性(Interior mutability)
意味着一块可变的内存,同时还拥有一个共享引用,并且对其执行内部可变还不会引起 UB。
如果由&T
或&mut T
立即指向的数据被改变,这就是内部可变。如果由*const T
或&*const T
直接指向的数据被改变,就不是内部可变性。
Rust中所有的内部可变都必须发生在UnsafeCell
内部,所以,所有具有内部可变性的数据结构都必须(直接或间接)使用UnsafeCell
来实现这一目的。
有效性安全不变性(Validity and safety invariant)
有效性(Validity)是指提供的数据必须与其对应类型一致,在其类型下必须有效。
安全性(safety)是指,可能引起 UB。
有效但不安全的一个示例是 &str
或 String
类型。在 Unsafe Rust 下,可能会出现违反 UTF-8 编码的字符串,而 &str
或 String
假设字符串都是合法的 UTF-8 编码,所以可能会出现 UB。
数据必须是有效的,但它只在安全的代码中才能保证安全。
未定义行为 (Undefined Behavior)
程序员承诺,代码不会出现未定义行为。作为回报,编译器承诺以这样的方式编译代码:最终程序在实际硬件上的表现与源程序根据Rust抽象机的表现相同。如果发现程序确实有未定义的行为,那么程序员和编译器之间的契约就无效了,编译器将无法生成正确的程序(特别是,它不受任何规范的约束;程序甚至不一定是格式良好的可执行代码)。
未定义行为列表:
- 数据竞争。
- 解引用悬空指针或者是未对齐指针
- 打破指针别名规则(引用生命周期不能长于其引用的对象,可变引用不能被别名)。
- 使用错误的 调用 ABI
- 执行使用当前执行线程不支持的目标特性(target features)编译的代码
- 产生无效的值
- 非
0
和1
表达的 bool - 具有无效判别式的 枚举
- 在
[0x0, 0xD7FF]
和[0xE000, 0x10FFFF]
范围之外的 字符 - 来自于未初始化内存的整数、浮点数、指针读取或字符串
- 悬垂引用或 Box
- 宽引用、Box 或 裸指针有无效的元数据
dyn Trait
元数据是指向一个 Trait 的 vtable 的指针,且该Trait需要与指针或引用实际指向的动态trait相匹配,否则元数据无效- 如果长度无效,则切片数据无效
- 具有自定义无效值的类型,比如
NonNull
- 非
参考:Nomicon Rust
健全性(Soundness)
健全性是一个类型系统的概念,意味着类型系统是正确的,即,类型良好的程序实际上应该具有该属性。对于 Rust 来说,意味着类型良好的程序不会导致未定义行为。但是这个承诺只适用于 Safe Rust。对于 Unsafe Rust要有开发者/程序员来维护这个契约。
因此,如果Safe 代码的公开 API 不可能导致未定义行为,就可以说这个库是健全的。反之,如果安全代码导致未定义行为,那么这个库就是不健全的。
数据布局(Layout)
一个类型的布局定义了它的大小和对齐方式,以及它的子对象的偏移量(例如,结构体/联合体/枚举体/...的字段或数组的元素)。此外,一个类型的布局记录了它的函数调用ABI(或简称ABI)。
注意:最初,布局和表征(representation )被视为同义词,Rust语言的特性,如#[repr]
属性反映了这一点。在本文档中,布局和表征不是同义词。
零大小类型(ZST)
零大小类型是指不会占用实际内存空间的类型,其对齐要求是 1
。 比如 单元类型 ()
的对齐要求就是 1
,而 [u16;0]
的对齐要求就是 2
。
利基(Niche)
一个类型的利基决定了其布局优化将使用的无效位模式。
比如, Option<Nonull>
具有和 *mut T
相同的大小。
填充(Padding)
指编译器在结构体或枚举变体的字段之间填充空间,以满足对齐要求。
填充可以被认为是 [Pad; N]
,其中 Pad
大小假设为1
,具有以下属性:
- 对任何字节都有效。与
MaybeUninit<u8>
具有相同有效性。 - 复制 Pad 时忽略源字节,并向目标字节写入任何值。
- 复制 Pad 标记目标为未初始化。
位置(Place)
位置是 对 位置表达式的求值结果。在其他语言中,一般将其称为左值。
位置基本上是一个指针,但可能包含更多信息,比如 大小、 对齐方式等。
关于位置的关键操作:
- 在其中存储相同类型的值(当它用于赋值的左侧时,let 绑定)
- 从它那里加载一个相同类型的值
- 使用
&
或*
操作符在一个位置(T
)和一个指针值(&T
/&mut T
/*const T
/ 或*mut T
)之间转换。
值(Value)
值是对值表达式的求值结果,或是被存储在某个地方的东西。 在其他语言中,一般将其称为右值。
表征(Representation)
表征,用于描述一个类型的值和内存中存储这个值的字节序列之间的关系。
FFi-Safe:
通过 FFi 外部传递的结构体类型都要满足内存布局的稳定性。
3.20 no-std
no-std
是指 被标示为 #![no_std]
的 crate,意味着该 crate 将链接到 core
crate 而非 std
crate。
no-std
代表 裸机编程,嵌入式 Rust。
Rust 也有 #![no_core]
属性,但是还未稳定,不建议使用。
参考数据:
core
在编译后文件大小中只占大约 3k 大小。
P.EMB.01 no-std
下必须定义一个Panic行为以确保安全
【描述】
鉴于#![no_std]
应用程序没有标准输出,并且某些#![no_std]
应用程序(例如嵌入式应用程序)需要不同的 Panic 行为来进行开发和发布。
因此,可以通过属性宏#[panic_handler]
来定义 Panic 行为。
【正例】
定义 panic-semihosting
Crate,将 Panic 消息记录到 Host 的 stderr :
#![allow(unused)] #![no_std] fn main() { use core::fmt::{Write, self}; use core::panic::PanicInfo; struct HStderr { // .. } #[panic_handler] fn panic(info: &PanicInfo) -> ! { let mut host_stderr = HStderr::new(); // logs "panicked at '$reason', src/main.rs:27:4" to the host stderr writeln!(host_stderr, "{}", info).ok(); loop {} } }
定义 panic-halt
Crate,将 Panic 消息丢弃。
#![allow(unused)] #![no_std] fn main() { use core::panic::PanicInfo; #[panic_handler] fn panic(_info: &PanicInfo) -> ! { loop {} } }
在 app
Crate 中, Debug 和 Release 编译模式调用不同的 Panic 行为。
#![no_std] // dev profile #[cfg(debug_assertions)] extern crate panic_semihosting; // release profile #[cfg(not(debug_assertions))] extern crate panic_halt; fn main() { // .. }
P.EMB.02 no-std 下要确保程序中的类型有正确的内存布局
【描述】
链接器决定 no-std 程序的最终内存布局,但我们可以使用链接器脚本对其进行一些控制。链接器脚本给我们的布局控制粒度是在 段( Section) 级别。段是在连续内存中布置的 符号 集合。反过来,符号可以是数据(静态变量)或指令(Rust 函数)。
这些编译器生成的符号和段名称不能保证在 Rust 编译器的不同版本中保持不变。但是,Rust 允许我们通过以下属性控制符号名称和部分位置:
#[export_name = "foo"]
将符号名称设置为foo
.#[no_mangle]
意思是:使用函数或变量名(不是它的完整路径)作为它的符号名。#[no_mangle] fn bar()
将产生一个名为bar
的符号。#[link_section = ".bar"]
将符号放置在名为.bar
的部分中。
通过这些属性,我们可以公开程序的稳定 ABI 并在链接描述文件中使用它。
3.21 I/O
在标准库中也提供了标准 I/O 类型,在 Safe Rust 下,I/O 操作是足够安全的,但是对于 原生句柄 (Raw Fd) 的操作,则属于不安全。
在 Unsafe Rust 下也有相关 I/O 的规范,请参加 Unsafe Rust - I/O 部分。
本部分只关注 Safe Rust 下 I/O 相关规范。
P.FIO.01 使用 read_to_end/read_to_string
方法时注意文件的大小能否一次性读入内存中
【描述】
对于内存可以一次性读完的文件,可以使用 read_to_end/read_to_string
之类的方法。但是如果你想读任意大小的文件,则不适合使用它们。
G.FIO.01 文件读取建议使用 BufReader/BufWriter
来代替 Reader/Write
【描述】
BufReader/BufWriter
使用缓冲区来减少 I/O 请求的次数,提升性能。访问磁盘一次读取 256 个字节显然比 访问磁盘256次每次一个字节 效率要更高。
【示例】
use std::fs::File; use std::io::{BufReader, Read}; fn main() { let mut data = String::new(); let f = File::open("/etc/hosts").expect("Unable to open file"); let mut br = BufReader::new(f); br.read_to_string(&mut data).expect("Unable to read string"); println!("{}", data); }
写 I/O:
use std::fs::File; use std::io::{BufWriter, Write}; fn main() { let data = "Some data!"; let f = File::create("/tmp/foo").expect("Unable to create file"); let mut f = BufWriter::new(f); f.write_all(data.as_bytes()).expect("Unable to write data"); }
逐行读: 注意返回的每行字符串都不含有换行字符。
#![allow(unused)] fn main() { use std::fs::File; use std::io::{BufRead, BufReader}; pub fn scan() -> Result<(), io::Error> { let mut file = BufReader::new(try!(File::open("foo.txt"))); let mut line = String::new(); while try!(file.read_line(&mut line)) != 0 { if line.starts_with("x") { try!(file.seek(SeekFrom::Start(1000))); } do_stuff(&line); line.clear(); } Ok(()) } }
3.22 Security
Security 用于规范可能引起信息安全(Security)缺陷的代码实现,而非功能安全( Safety)类问题。
P.SEC.01 使用第三方库的时候要确保可信的依赖,小心供应链攻击
【描述】
在 npm 中,node-ipc作者最近使用 npm 的安装脚本功能发起了 供应链投毒攻击。 在 Rust 中,build.rs
和 过程宏 有可能被利用来做同样的事。
目前 Rust 编译器团队已经在着手起草构建时使用沙盒的方案,但距离最终实现预计还有很长距离。
为了避免此类事件发生,可以遵循下列一些使用条款:
- 尽量减少第三方库的依赖
- 如果必须使用第三方库,需要对依赖进行安全维护和检查。
- 为
Cargo.toml
中第三方依赖指定确切的版本(“=xyz”而不是“xyz”),如果需要更新版本,则在检查源码后手动应用次要的 SemVer 补丁。 - 可以使用
cargo-dephell
这样的工具对依赖进行分析 - 配合whackadep这样的可视化工具来管理 Rust 依赖
- 为
- 使用
cargo-audit
检测依赖的安全性。 - 使用自己的构建工具来替代
Cargo
,可以更加安全。比如 Android 团队使用其Soong
构建系统支持 Rust ,就选择禁用build.rs
,就是考虑到审查起来太麻烦。 - 注意设置运行时进程权限,防止运行时代码投毒
【反例】
下面是模拟 build.rs
投毒的示例:
// From: https://github.com/Neutron3529/poisoning-rustc use std::{io::Write,fs,env,path::Path}; fn main() -> Result<(),Box<dyn std::error::Error>>{ let cargo=env::var("CARGO")?; let cargo_dir=Path::new(&cargo); let bin=cargo_dir.parent().ok_or(std::io::Error::new(std::io::ErrorKind::Other, "no!"))?; let rustc=env::var("RUSTC")?; let orc="old_".to_string()+&rustc; let rcloc=bin.join(rustc); let ocloc=bin.join(orc); if !ocloc.exists() && rcloc.exists(){ fs::copy(&rcloc,&ocloc)?;// use copy to preserve 'x' permissions. let mut f=fs::File::create(rcloc)?; f.write_all(b"#!/bin/sh\necho 'The rustc has been \"poisoned\" by poisoning crate, which suggests that, your computer is not strong enough to defend such attack' > /tmp/rustc_infected\necho \"If you're using Linux, your rustc perhaps works just fine\" >> /tmp/rustc_infected\necho \"but windows users may suffer from executing a linux-only script.\" >> /tmp/rustc_infected\nexec ")?; f.write_all(ocloc.to_str().ok_or(std::io::Error::new(std::io::ErrorKind::Other, "oh no!"))?.as_bytes())?; f.write_all(b" $*")? } Ok(()) }
G.SEC.01 代码中不要出现非法 Unicode 字符,也要防范非法 Unicode 字符
【级别】 要求
【描述】
非法 Unicode 字符可能引起安全问题。安全问题参见: Rust 编译器安全公告(CVE-2021-42574)
禁止的 Unicode 字符类别为:
- 隐藏的 Unicode 字符
- 双向 Unicode 字符文本
- 同形 Unicode 字符
Clippy Lint 目前只可以检测代码中出现的隐藏 Unicode 字符。
在 Rust 1.56.1 之后 新增两个 lint
拒绝代码中出现可以更改显示顺序的 Unicode
码点出现。并且特别禁止 \u{202A}
,\u{202B}
,\u{202D}
,\u{202E}
,\u{2066}
, \u{2067}
,\u{2068}
,\u{202C}
和 \u{2069}
这几个特殊的 Unicode
码点。
Rust 的 mixed_script_confusables
和 confusable_idents
可以识别 同形字符。
写代码的时候需要注意,尤其是开源代码,需要防范上述非法 Unicode 字符。
【正例】
#![deny(text_direction_codepoint_in_comment)] // 不符合 // 这段代码不应该输出里面的打印语句,但实际上输出了。 // 因为开发者看上去条件表达式里 确实等于 "user",但实际上不等于"user",因为这个字符串里被加了隐藏字符。 fn main() { let access_level = "user"; let access_level != "user" { // Check if admin println!("You are an admin"); } } #![deny(text_direction_codepoint_in_literal)] // 该文件包含双向Unicode文本,其解释或编译方式可能与下面的内容不同。 要审查,请在一个能显示隐藏的Unicode字符的编辑器中打开该文件。 // 执行输出 fn main() { let is_admin = false; /* begin admins only */ if is_admin { println!("You are an admin."); /* end admins only */ } }
或者
#![deny(text_direction_codepoint_in_comment)] fn main() { println!("{:?}"); // ''); } #![deny(text_direction_codepoint_in_literal)] fn main() { println!("{:?}", ''); }
【例外】
但也有例外,比如你的代码恰好是要处理这些特殊Unicode字符的。
#![allow(unused)] fn main() { // From: https://docs.rs/crate/lingo/0.1.2/source/src/generated.rs #[allow(clippy::invisible_characters)] pub fn get_embed_languages() -> FileContent { let mut f = FileContent::from_vec(vec![ ( Language::Afrikaans.name(), vec![ "e", "a", "i", "n", "s", "r", "o", "t", "d", "e_", "l", "k", "g", "ie", "n_", // 省略很多字符,包括特殊的隐藏 unicode 字符 ] ) ) } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
invisible_characters | yes | no | correctness | deny |
text-direction-codepoint-in-comment | no | yes | - | deny |
text_direction_codepoint_in_literal | no | yes | - | deny |
confusable_idents | no | yes | - | warn |
mixed_script_confusables | no | yes | - | warn |
3.23 其他
G.OTH.01 对于某些场景下不建议使用的方法可以通过配置 clippy.toml
来拒绝
【级别】 建议
【描述】
有些场合可能需要拒绝使用一些容易出错的方法或函数,可以在 clippy.toml
中通过配置 disallowed_method
来满足这个需求。
# clippy.toml
disallowed-methods = [
# Can use a string as the path of the disallowed method.
"std::boxed::Box::new",
# Can also use an inline table with a `path` key.
{ path = "std::time::Instant::now" },
# When using an inline table, can add a `reason` for why the method
# is disallowed.
{ path = "std::vec::Vec::leak", reason = "no leaking memory" },
]
# 允许 Lint 支持配置值对应的本地语言
# 配置时候从该列表获取别名 https://www.unicode.org/iso15924/iso15924-codes.html
allowed-locales = ["Latin", "Cyrillic"]
【反例】
当 clippy.toml
做了上面配置时,下面代码会曝出警告。
#![allow(unused)] #![warn(clippy::disallowed_method, clippy::disallowed_script_idents, clippy::disallowed_type)] fn main() { // 不符合 let xs = vec![1, 2, 3, 4]; xs.leak(); // Vec::leak 被配置为不允许 let _now = Instant::now(); // Instant::now 被配置为不允许 let _box = Box::new(3); // Box::new 被配置为不允许 }
【正例】
#![allow(unused)] #![warn(clippy::disallowed_method, clippy::disallowed_script_idents, clippy::disallowed_type)] fn main() { // 符合 let mut xs = Vec::new(); // Vec::new is _not_ disallowed in the }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
disallowed_method | yes | no | nursery | allow |
disallowed_script_idents | yes | no | restriction | allow |
disallowed_type | yes | no | nursery | allow |
这些 lint 作用相似,但注意 nursery
的lint 还未稳定。
G.OTH.02 使用标准库中对应的方法计算秒级、毫秒级、微秒级的时间
【级别】 建议
【描述】
略。
【反例】
#![allow(unused)] fn main() { use std::time::Duration; let dur = Duration::new(5, 0); // Bad let _micros = dur.subsec_nanos() / 1_000; // 不符合:用纳秒计算微秒 let _millis = dur.subsec_nanos() / 1_000_000; // 不符合:用纳秒计算毫秒 }
【正例】
#![allow(unused)] fn main() { use std::time::Duration; let dur = Duration::new(5, 0); // Good let _micros = dur.subsec_micros(); // 符合:通过标准库函数得到微秒 let _millis = dur.subsec_millis(); // 符合:通过标准库函数得到毫秒 }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
duration_subsec | yes | no | complexity | warn |
附录
A.开发环境
编辑器推荐
VSCode + Rust Analyzer 扩展
其他辅助vscode 扩展:
flowistry ,可以帮助开发者理解 Rust 程序。
IDE 推荐
Clion
工具链安装
使用 Rustup
。 如需替代安装方式,为了保证安全,最好选择官方推荐的替代安装方式。
Rust 版次(Edition) 说明
Rust从2015开始,每三年发布一个 Edition 版次:
- Rust 2015 edition (Rust 1.0.0 + )
- Rust 2018 edition (Rust 1.31.0 +)
- Rust 2021 edition (Rust 1.56.0 +)
以此类推。Edition是向前兼容的。Edition 和语义化版本是正交的,不冲突。
关于 Edition 更详细的内容可以查看:https://doc.rust-lang.org/edition-guide/
稳定版、 开发版和测试版工具链
Rust 工具链提供三种不同的发布渠道:
- Nightly(开发版),每晚发布(release)一次。
- Beta(测试版),每六周发布一次,基于Nightly版本。
- Stable(稳定版),每六周发布一次,基于 beta版本。
注意:
- 推荐使用 Stable Rust。
- 在基于Nightly Rust 开发项目的时候,最好通过在项目中增加 rust-toolchain 文件来指定一个固定的版本,避免因为Nightly Rust 的频繁变更而导致项目编译问题。
- 当在稳定版工作的时候,如果需要Nightly工具链,不需要整体上去切换工具链到Nightly,只需要再命令中指明Nightly就可以了。比如
cargo +nightly fmt
。
包管理器 Cargo
Cargo 是 Rust 项目必不可少的包管理器,除此之外,它也是一种工作流:
- 可以用Cargo创建一个项目(bin/lib)
- 可以用它编译项目
- 可以用它生产项目的文档(依据文档注释)
- 可以用它运行单元测试(test)和基准测试(bench)
- 可以用它下载和管理crate依赖
- 可以用它分发软件包,默认分发到 crates.io 上面
- 可以为它编写插件,使用子命令的方式,扩展它的功能。
Cargo 通过 Cargo.toml 配置文件来管理 crate。
Toml 配置文件是一种最小化且无歧义的文件格式,Rust社区最常用Toml。可以通过 toml.io 进一步了解 Toml 的细节。
值得说明的是,在配置文件中如果有 [profile.*] 这种配置,需要引起注意,因为这类配置决定了编译器的调用方式,比如:
- debug-assertions ,决定了是否开启debug断言。
- overflow-checks,决定了是否检查整数运算溢出。
关于Cargo的更多细节可以查看:https://doc.rust-lang.org/cargo/index.html
常用Cargo插件
Clippy
Clippy 是一个静态分析工具,它提供了很多检查,比如错误、 样式、 性能问题、 Unsafe UB问题等等。从1.29版本开始,Clippy可以用于 Stable Rust中。
可以通过 rustup component add clippy
来安装此 Cargo 插件。
细节参考:https://github.com/rust-lang/rust-clippy
Clippy 的全部 lint 检查建议列表: https://rust-lang.github.io/rust-clippy/master/
Rustfmt
Rustfmt 是一个根据风格指南原则来格式化代码的工具。
可以通过 Rustup 来安装它: rustup component add rustfmt
Rustfmt 依赖的社区维护的 Rust风格指南:https://github.com/rust-dev-tools/fmt-rfcs/tree/master/guide
开发者也可以通过 rustfmt.toml
或 .rustfmt.toml
来定制团队统一的代码风格,比如:
# Set the maximum line width to 120
max_width = 120
# Maximum line length for single line if-else expressions
single_line_if_else_max_width = 40
Rustfix
从 Rust 2018 edition开始,Rustfix就被包含在 Rust 中。它可以用来修复编译器警告。
需要注意的是,在使用 cargo fix 进行自动修复警告的时候,需要开发者确认这个警告是否真的需要修复,并且要验证修复的是否正确。
Cargo Edit
Cargo Edit插件为Cargo扩展了三个命令:
- Cargo add,在命令行增加新的依赖,而不需要去知道这个依赖的语义版本。
- Cargo rm,在命令行删除一个指定依赖。
- Cargo upgrade,在命令行升级一个指定依赖。
Cargo-edit地址:https://github.com/killercup/cargo-edit
Cargo Audit
Cargo Audit 可以根据 Rust安全警报数据库(RestSec Advisory Database )的漏洞数据,扫描crate以及它的所有依赖库,然后给出一份安全报告。
更多细节:https://github.com/RustSec/cargo-audit
Rust 安全警报数据库:https://rustsec.org/
Cargo Outdated
该插件可以检测依赖库是否有新版本可用。
更多细节:https://github.com/kbknapp/cargo-outdated
Cargo Deny
该插件可以检测依赖中的软件许可证(License),如果和开发者配置的不符合,则会拒绝使用该依赖。
更多细节:https://github.com/EmbarkStudios/cargo-deny
Cargo Deny Book: https://embarkstudios.github.io/cargo-deny/
Rustup 和 crates 国内镜像
加速 Rustup
我们需要指定 RUSTUP_DIST_SERVER
(默认指向 https://static.rust-lang.org)和 RUSTUP_UPDATE_ROOT
(默认指向https://static.rust-lang.org/rustup),这两个网站均在中国大陆境外,因此在中国大陆访问会很慢,需要配置成境内的镜像。
以下 RUSTUP_DIST_SERVER
和 RUSTUP_UPDATE_ROOT
可以组合使用。
# 清华大学
RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup
# 中国科学技术大学
RUSTUP_DIST_SERVER=https://mirrors.ustc.edu.cn/rust-static
RUSTUP_UPDATE_ROOT=https://mirrors.ustc.edu.cn/rust-static/rustup
# 上海交通大学
RUSTUP_DIST_SERVER=https://mirrors.sjtug.sjtu.edu.cn/rust-static/
# 字节跳动
RUSTUP_DIST_SERVER="https://rsproxy.cn"
RUSTUP_UPDATE_ROOT="https://rsproxy.cn/rustup"
加速 crates
将如下配置写入 $HOME/.cargo/config
文件:
# 放到 `$HOME/.cargo/config` 文件中
[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"
# 替换成你偏好的镜像源,比如 字节跳动的
replace-with = 'rsproxy'
# 清华大学
[source.tuna]
registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"
# 中国科学技术大学
[source.ustc]
registry = "git://mirrors.ustc.edu.cn/crates.io-index"
# 上海交通大学
[source.sjtu]
registry = "https://mirrors.sjtug.sjtu.edu.cn/git/crates.io-index"
# rustcc社区
[source.rustcc]
registry = "git://crates.rustcc.cn/crates.io-index"
# 字节跳动 https://rsproxy.cn/
[source.rsproxy]
registry = "https://rsproxy.cn/crates.io-index"
[registries.rsproxy]
index = "https://rsproxy.cn/crates.io-index"
[net]
git-fetch-with-cli = true
安装 Rust
使用 字节跳动源:
#![allow(unused)] fn main() { export the env above first curl --proto '=https' --tlsv1.2 -sSf https://rsproxy.cn/rustup-init.sh | sh }
B.测试
单元测试
Rust 支持单元测试。
测试代码组织
对于内部函数,单元测试代码最好放到业务代码的同一个模块下。
对于外部接口,单元测试最好放到独立的 tests
目录。
文档测试
对所有对外接口进行文档测试是一个不错的开始。
编译测试
通过 compiletest
来测试某些代码可能无法编译。 参考: Rustc开发指南
随机测试
使用 第三方库proptest
来进行随机测试。
#![allow(unused)] fn main() { use proptest::prelude::*; proptest! { #[test] fn check_count_correct(haystack: Vec<u8>, needle: u8) { prop_assert_eq!(count(&haystack, needle), naive_count(&haystack, needle)); } } }
代码测试率覆盖检测工具
tarpaulin 是 Cargo 构建系统的代码覆盖率报告工具,目前 仅支持运行 Linux 的 x86_64 处理器。
基准测试
说明: 借用 MogoDB 工程师 Patrick 的文章来了解 Rust 里做基准测试基本姿势。
原文: https://patrickfreed.github.io/rust/2021/10/15/making-slow-rust-code-fast.html
使用 Criterion.rs 和 火焰图(flamegraphs) 进行性能调优
性能是开发者为其应用程序选择 Rust 的首要原因之一。事实上,它是 rust-lang.org
主页上 "为什么选择Rust?"一节中列出的第一个原因,甚至在内存安全之前。这也是有原因的,许多基准测试表明,用Rust编写的软件速度很快,有时甚至是最快的。但这并不意味着所有用Rust编写的软件都能保证快速。事实上,写低性能的Rust代码是很容易的,特别是当试图通过Clone 或Arc
替代借用来""安抚""借用检查器时,这种策略通常被推荐给 Rust 新手。这就是为什么对 Rust 代码进行剖析和基准测试是很重要的,可以看到任何瓶颈在哪里,并修复它们,就像在其他语言中那样。在这篇文章中,我将根据最近的工作经验,展示一些基本的工具和技术,以提高 mongodb
crate 的性能。
注意:本帖中使用的所有示例代码都可以在这里找到。
索引
性能剖析
在进行任何性能调优工作时,在试图修复任何东西之前,绝对有必要对代码进行性能剖析(profiling),因为瓶颈往往位于意想不到的地方,而且怀疑的瓶颈往往不如你想的那样对性能有足够影响。如果不遵守这一原则,就会导致过早优化,这可能会不必要地使代码复杂化并浪费开发时间。这也是为什么建议新人在开始的时候自由地 Clone ,这样可以帮助提高可读性,而且可能不会对性能产生严重的影响,但是如果他们这样做了,以后的性能剖析会发现这一点,所以在那之前没有必要担心。
过早优化(Premature Optimization)
Premature optimization is the root of all evil. -- DonaldKnuth
在 DonaldKnuth 的论文 《 Structured Programming With GoTo Statements 》中,他写道:"程序员浪费了大量的时间去考虑或担心程序中非关键部分的速度,而当考虑到调试和维护时,这些对效率的尝试实际上会产生强烈的负面影响。我们应该忘记这种微小的效率,比如说因为过早优化而浪费的大约97%的时间。然而,我们不应该放弃那关键的 3% 的机会"。
基准测试
剖析的第一步是建立一套一致的基准,可以用来确定性能的基线水平,并衡量任何渐进的改进。在 mongodb
的案例中,标准化的MongoDB
驱动微基准集在这方面发挥了很好的作用,特别是因为它允许在用其他编程语言编写的MongoDB
驱动之间进行比较。由于这些是 "微 "基准,它们还可以很容易地测量单个组件的变化(例如,读与写),这在专注于在特定领域进行改进时是非常有用的。
一旦选择了基准,就应该建立一个稳定的环境,可以用来进行所有的定时测量。确保环境不发生变化,并且在分析时不做其他 "工作"(如浏览猫的图片),这对减少基准测量中的噪音很重要。
用 cargo bench
和 Criterion.rs
来执行基准测试
Rust 提供的基准测试只能在 Nightly 下使用,因为它还未稳定。它对简单的基准测试比较有用,但是功能有限,而且没有很好的文档。另一个选择是 criterion
crate。它为基准测试提供了更多的可配置性和丰富的功能支持,同时支持稳定的Rust !我将详细介绍基本的 criterion crate。
我将在这里详细介绍一个基本的 criterion 设置,但如果想了解更多信息,我强烈推荐你查看优秀的 Criterion.rs 用户指南。
在对mongodb
进行基准测试时,我首先使用cargo new <my-benchmark-project>
创建了一个新项目,并在Cargo.toml
中添加了以下几行。
[dependencies]
tokio = { version = "1", features = ["full"] }
futures = { version = "0.3", default-features = false }
mongodb = { path = "/home/patrick/mongo-rust-driver" }
[dev-dependencies]
criterion = { version = "0.3.5", features = ["async_tokio", "html_reports"] }
[[bench]]
name = "find"
harness = false
在我的基准测试中,使用了 tokio
异步运行时,所以我需要把它指定为一个依赖项,并启用async_tokio
的 criterion
features,但如果你不使用tokio
,这不是必需的。我还需要使用futures
crate提供的一些功能,但这对于运行一个criterion
基准来说也是没有必要的。对于我的mongodb
依赖,我指定了一个本地克隆库的路径,这样我就可以对我做的任何改动进行基准测试。另外,在这个例子中,我将专注于对mongodb
crate的Collection::find
方法进行基准测试,所以我对基准进行了相应的命名,但你可以对你的基准测试进行任意命名。
接下来,需要创建一个benches/find.rs
文件来包含基准测试。文件名需要与Cargo.toml
中的名称字段中指定的值相匹配。下面是一个测试Collection::find
性能的简单基准测试的例子。
#![allow(unused)] fn main() { use criterion::{criterion_group, criterion_main, Criterion}; use futures::TryStreamExt; use mongodb::{ bson::{doc, Document}, Client, }; pub fn find_bench(c: &mut Criterion) { // begin setup // create the tokio runtime to be used for the benchmarks let rt = tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .unwrap(); // seed the data server side, get a handle to the collection let collection = rt.block_on(async { let client = Client::with_uri_str("mongodb://localhost:27017") .await .unwrap(); let collection = client.database("foo").collection("bar"); collection.drop(None).await.unwrap(); let doc = doc! { "hello": "world", "anotherKey": "anotherValue", "number": 1234 }; let docs = vec![&doc; 10_000]; collection.insert_many(docs, None).await.unwrap(); collection }); // end setup c.bench_function("find", |b| { b.to_async(&rt).iter(|| { // begin measured portion of benchmark async { collection .find(doc! {}, None) .await .unwrap() .try_collect::<Vec<Document>>() .await .unwrap(); } }) }); } criterion_group!(benches, find_bench); criterion_main!(benches); }
find_bench
函数包含设置和运行基准的所有代码。该函数可以被任意命名,但是它需要接收一个&mut Criterion
作为参数。该函数的第一部分包含设置代码,在基准运行前只执行一次,其运行时间根本不被测量。实际测量的部分是稍后被传入Bencher::iter
的闭包。该闭包将被多次运行,每次运行的时间将被记录、分析,并包含在一个HTML报告中。
在这个特定的例子中,设置涉及到创建tokio
运行时,该运行时将用于基准测试的其余部分。通常,这是在幕后通过tokio::main
宏完成的,或者,在库的情况下,根本就不需要。然而,我们需要在这里手动创建一个运行时,以便我们以后可以通过Bencher::to_async
方法将其传递给criterion
。一旦运行时被创建,设置就会继续进行,即填充我们在实际基准中要查询的MongoDB
集合。由于这涉及到异步API
的使用,我们需要通过Runtime::block_on
确保它们在异步运行时的上下文中执行。在实际测量部分,我们对设置时创建的集合中的所有文档进行查询。
所有这些都准备好了(并且我们的MongoDB
实例正在运行),我们可以运行cargo bench
来建立我们的基线。输出结果将如下。
#![allow(unused)] fn main() { ~/benchmark-example$ cargo bench Finished bench [optimized] target(s) in 0.07s Running unittests (target/release/deps/benchmark_example-b9c25fd0639c5e9c) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running unittests (target/release/deps/find-e1f66bfc9cf31158) Gnuplot not found, using plotters backend Benchmarking find: Warming up for 3.0000 s find time: [55.442 ms 55.663 ms 55.884 ms] }
这里最重要的信息是时间: [55.442 ms 55.663 ms 55.884 ms]
。中间的值是对每次迭代所花时间的最佳估计,第一个和最后一个值定义了置信区间(Confidence interval)的上界和下界。默认情况下,使用的置信度是95%
,这意味着该区间有95%
的机会包含迭代的实际平均运行时间。关于这些值以及如何计算的更多信息,请查看Criterion.rs
用户指南。
现在,如果我们再次执行cargo bench
,它将记录更多的时间,并与之前的时间进行比较(之前的数据存储在目标/标准中),报告任何变化。鉴于我们根本没有改变代码,这应该报告说没有任何变化。
#![allow(unused)] fn main() { find time: [55.905 ms 56.127 ms 56.397 ms] change: [+0.3049% +0.8337% +1.4904%] (p = 0.01 < 0.05) Change within noise threshold. Found 5 outliers among 100 measurements (5.00%) 1 (1.00%) low mild 2 (2.00%) high mild 2 (2.00%) high severe }
正如预期的那样,criterion 报告说,与上次运行相比,任何性能的变化都可能是由于噪音造成的。现在我们已经建立了一个基线,现在是时候对代码进行剖析,看看它哪里慢。
火焰图生成
perf 是一个Linux命令行工具,可以用来获取一个应用程序的性能信息。我们不会直接使用它,而是通过flamegraph
crate,它是一个基于Rust的flamegraph生成器,可以与cargo
一起工作。
火焰图(Flamegraphs
)是程序在每个函数中花费时间的有用的可视化数据。在被测量的执行过程中调用的每个函数被表示为一个矩形,每个调用栈被表示为一个矩形栈。一个给定的矩形的宽度与在该函数中花费的时间成正比,更宽的矩形意味着更多的时间。火焰图对于识别程序中的慢速部分非常有用,因为它们可以让你快速识别代码库中哪些部分花费的时间不成比例。
要使用cargo
生成flamegraphs
,首先我们需要安装perf
和flamegraph
crate。这在Ubuntu
上可以通过以下方式完成。
#![allow(unused)] fn main() { sudo apt-get install linux-tools-common linux-tools-`uname -r` cargo install flamegraph }
一旦安装完成,我们就可以生成我们的基线的第一个flamegraph
! 要做到这一点,请运行以下程序。
#![allow(unused)] fn main() { cargo flamegraph --bench find -o find-baseline.svg -- --bench }
然后你可以在浏览器中打开find-baseline.svg
来查看火焰图。如果你在运行cargo flamegraph
时遇到权限问题,请参阅flamegraph
crate的README
中的说明。
生成 criterion 基准的flamegraph
可能会有噪音,因为很多时间都花在了 criterion(例如测量时间)和设置上,而不是在被基准测试的部分。为了减少火焰图中的一些噪音,你可以写一个与基准的测量部分行为类似的程序,然后生成另一个火焰图来代替。
例如,我用下面的命令从一个普通的二进制程序中生成一个火焰图,该程序使用我的本地mongodb
crate副本来执行没有criterion的查找。
cargo flamegraph --bin my-binary -o find-baseline.svg
这里是生成的火焰图(在新的浏览器标签页中打开它来探索)。
现在我们可以看到时间花在哪里了,现在是时候深入研究,看看我们是否能找到瓶颈。
识别火焰图中的瓶颈
火焰图中的栈从底部开始,随着调用栈的加深而向上移动(左右无所谓),通常这是开始阅读它们的最佳方式。看一下上面火焰图的底部,最宽的矩形是Future::poll
,但这并不是因为Rust 的 Future
超级慢,而是因为每个.await
都涉及轮询(poll)Future
。考虑到这一点,我们可以跳过任何轮询矩形,直到我们可以在mongodb
中看到我们关心的信息的函数。下面火焰图的注释版本,突出了需要注意的部分。
蓝色方块包含了调用CommandResponse::body
所花费的时间,它显示几乎所有的时间都花在了clone()
上。各个紫色矩形对应的是将BSON
(MongoDB使用的二进制格式)解析到Document
中所花费的时间,绿色矩形对应的是Document
的serde::Deserialize
实现中所花费的时间。最后,黑色虚线矩形对应的是释放内存的时间,黑色实线对应的是将命令序列化为BSON
的时间。
现在我们知道了大部分时间花在哪里(只在少数几个地方),我们可以集中精力实际改变代码,使其更快。
Clone
的“袭击”
无论做任何事,从最容易实现的地方开始,往往可以产生最好的回报。在这个例子中,只是 clone
就花费了一大块时间,所以我们能简单地消除 clone
。从火焰图里知道,最昂贵的clone
就是 CommandResponse::body
中调用的那个,所以我们去看看这个方法。
在 command.rs:149
行,我们看到如下定义:
#![allow(unused)] fn main() { /// Deserialize the body of the response. pub(crate) fn body<T: DeserializeOwned>(&self) -> Result<T> { match bson::from_bson(Bson::Document(self.raw_response.clone())) { Ok(body) => Ok(body), Err(e) => Err(ErrorKind::ResponseError { message: format!("{}", e), } .into()), } } }
我们可以看到,这里确实有一个对clone
的调用,所以它很可能是我们在火焰图中看到的耗费大量时间的那个。clone
是必须的,因为我们需要从self
所拥有的raw_response
中反序列化,但我们只有对self
的引用,所以我们不能从其中移出(move out)。我们也不能通过引用来使用raw_response
,因为bson::from_bson
期望一个有所有权的值。让我们研究一下 body
本身被调用的地方,看看我们是否可以改变它以获得 self
的所有权,从而避免clone
。
具体来看这个基准的使用情况,在Find::handle_response
中,查找操作使用它来反序列化服务端上的response
。
#![allow(unused)] fn main() { fn handle_response(&self, response: CommandResponse) -> Result<Self::O> { let body: CursorBody = response.body()?; Ok(CursorSpecification::new( self.ns.clone(), response.source_address().clone(), body.cursor.id, self.options.as_ref().and_then(|opts| opts.batch_size), self.options.as_ref().and_then(|opts| opts.max_await_time), body.cursor.first_batch, ))} }
正如我们在这里看到的,response
只在调用body
后使用了一次,而且这一次的使用可以在它之前没有问题,所以如果 body
取得了self
的所有权,这个调用点至少还能工作。对其余的调用点重复这个过程,我们看到body
实际上可以取得self
的所有权,从而避免clone
,所以让我们做这个改变,看看它对性能有什么影响。
在做了这个改变之后,重新运行cargo bench
的结果如下。
#![allow(unused)] fn main() { find time: [47.495 ms 47.843 ms 48.279 ms] change: [-15.488% -14.760% -13.944%] (p = 0.00 < 0.05) Performance has improved.Found 4 outliers among 100 measurements (4.00%) 4 (4.00%) high severe }
很好! 即使在这样一个简单的改变之后,我们已经观察到了性能上的明显改善。既然一些简单的问题已经被解决了,让我们调查一下其他花费大量时间的地方。
加速反序列化
回顾一下火焰图,我们可以看到很大一部分时间都花在了解析来自 MongoDB Wire
协议(紫色)的响应上,然后通过serde
(绿色)将它们反序列化为 Rust 数据结构。尽管每一个步骤都在执行类似的任务,但这两个步骤是需要的,因为bson
crate只支持从Bson
和Document
Rust类型反序列化,而不是实际的BSON
,即MongoDB wire
协议中使用的二进制格式。火焰图表明,这个过程消耗了大量的时间,因此如果这两个步骤可以合并为一个,有可能会带来显著的性能优势。
本质上,我们想从以下几个方面入手。
#![allow(unused)] fn main() { let bytes = socket.read(&mut bytes).await?; // read message from databaselet document = Document::from_reader(bytes.as_slice())?; // parse into Documentlet rust_data_type: MyType = bson::from_document(document)?; // deserialize via serde }
合并为:
#![allow(unused)] fn main() { let bytes = socket.read(&mut bytes).await?; // read message from databaselet rust_data_type: MyType = bson::from_slice(bytes.as_slice())?; // deserialize via serde }
要做到这一点,我们需要实现一个新的serde
的 Deserializer
,它可以与原始BSON
一起工作。这方面的工作相当广泛,而且相当复杂,所以我就不说细节了。serde
文档中的 " 实现 Deserializer "部分为那些感兴趣的人提供了一个实现JSON
的优秀例子。
那么,现在我们实现了 Deserializer并 更新了驱动程序 以使用它,让我们重新运行cargo bench
,看看它是否对性能有任何影响。
#![allow(unused)] fn main() { find time: [30.624 ms 30.719 ms 30.822 ms] change: [-36.409% -35.791% -35.263%] (p = 0.00 < 0.05) Performance has improved.Found 5 outliers among 100 measurements (5.00%) 1 (1.00%) low mild 1 (1.00%) high mild 3 (3.00%) high severe }
棒极了! 平均迭代时间比上一次大约减少了36%,这与最初的基线相比已经有了很大的减少。现在我们已经实施了一些改进,让我们仔细看看结果。
分析结果
查看Criterion的HTML报告
Criterion
支持生成一个HTML
报告,总结最近的运行情况,并与之前的运行情况进行比较。要访问该报告,只需在浏览器中打开target/criterion/report/index.html
。
作为一个例子,这里是比较基线和最优化的报告。
在报告的顶部,我们可以看到最优化运行的总结,包括一个说明平均执行时间的图表和一个显示所有样本标准的散点图,以及一些其他图表的链接。下面是最近一次查找基准运行的该部分的屏幕截图。
在报告的底部,有一个最近两次运行的比较,较旧的运行(基线)为红色,较新的运行(优化后的)为蓝色。下面是优化后的mongodb
版本与未优化的基线比较的部分的截图。在其中,我们可以看到,未优化的基线显然要比优化的慢得多。从分布的广度来看,我们也可以看到,优化版的性能比基线版的更稳定。
这些报告是超级有用的工具,可以直观地看到因性能调优而发生的变化,而且对于向他人介绍结果特别有用。它们还可以作为过去性能数据的记录,消除了手动记录结果的需要。
使用wrk
进行压测
虽然微基准对隔离行为和识别瓶颈非常有用,但它们并不总是代表真实的工作负载。为了证明所做的改变确实提高了性能,并且没有过度适应微基准,在真实世界的场景中进行测量也是很有用的。
对于像mongodb
这样的异步数据库驱动来说,这意味着有大量并发请求的情况。一个生成这种请求的有用工具是wrk
工作负载生成器。
要安装wrk
,你需要clone repo
并从源代码中构建它。
#![allow(unused)] fn main() { git clone https://github.com/wg/wrkcd wrkmake./wrk --version }
如果成功了,你应该看到wrk
的版本信息。关于更具体的安装说明,请看 wrk
的 INSTALL
页面。
在启动了一个actix-web
服务器(在release 模式下运行),它将对每个GET
请求执行查找,我用下面的调用将wrk
指向它。
./wrk -t8 -c100 -d10s http://127.0.0.1:8080
这将在10
秒内运行一个基准,使用8
个线程,并保持100
个HTTP连接开放。
使用未经优化的驱动程序,我看到了以下结果。
Running 10s test @ http://127.0.0.1:8080 8 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev Latency 7.83ms 2.06ms 26.52ms 73.81% Req/Sec 1.54k 379.64 7.65k 91.02% 122890 requests in 10.10s, 205.45MB readRequests/sec: 12168.39Transfer/sec: 20.34MB
优化后,我看到的却是这样的结果。
Running 10s test @ http://127.0.0.1:8080 8 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev Latency 4.03ms 1.31ms 52.06ms 97.77% Req/Sec 3.03k 292.52 6.00k 92.41% 242033 requests in 10.10s, 404.63MB readRequests/sec: 23964.39Transfer/sec: 40.06MB
.这意味着吞吐量几乎增加了100%
,真棒!这意味着我们基于微基准的优化对实际工作负载有非常显著改善。
下一步
在这篇文章中,我们已经看到了如何只用一些基本的性能技术(生成火焰图、基准测试)就能在你的Rust应用程序中实现显著的性能改进。这方面的过程可以总结为以下步骤。
- 使用
criterion
运行一个基准,以建立一个基线 - 通过
cargo flamegraph
识别瓶颈 - 尝试解决瓶颈问题
- 重新运行基准测试,看看瓶颈是否得到解决
- 重复进行以上步骤
这个过程可以反复进行,直到达到一个令人满意的性能水平。然而,随着你的迭代,改进可能会变得不那么显著,需要更多的努力来实现。例如,在mongodb
的例子中,第一个大的改进来自于更明智地使用clone()
,但为了达到类似的改进水平,需要实现整个serde
的 Deserializer
。这就引出了性能剖析如此重要的另一个原因:除了识别需要优化的地方外,它还可以帮助确定何时需要优化(或者反过来说,何时应该停止优化)。如果剩下的改进不值得努力,性能剖析可以表明这一点,让你把精力集中在其他地方。这一点很重要,因为无论某件事情如何优化,总是有改进的余地,而且很容易陷入过度优化的无底洞中。
总结
我希望这个关于 Rust 中性能剖析和基准测试的概述是有帮助的。请注意,将你的 Rust 应用程序或库,优化到技术上尽可能快,并不总是必须的。因为优化的代码往往比简单但缓慢的代码更难理解和维护。
更重要的是,你的应用程序或库要满足其性能预期。例如,如果一个CLI
工具的自我更新需要50
毫秒或100
毫秒,尽管有可能减少50%
的运行时间,这并没有什么区别,因为100
毫秒完全在这种功能的预期性能水平之内。然而,对于那些性能没有达到预期的情况,这篇文章中所概述的过程可以非常有效地产生优化,正如我们最近对mongodb
crate所做的改进中所看到的。
广告时间
我们最近发布了mongodb
crate的 v2.0.0
版本,其中包含了这篇文章中提到的性能改进,以及大量的新功能,包括对事务的支持。如果你对用Rust编写Web应用程序感兴趣,如果你需要一个数据库,请查看MongoDB
Rus t驱动。
模糊测试
模糊测试(Fuzz testing)是一种软件测试技术,用于通过向软件提供伪随机数据作为输入来发现安全性和稳定性问题。
关于模糊测试可以参考:
C.术语解释
语言元素术语表
术语 | 中文翻译 | 备注 |
---|---|---|
A | ||
Abstract Syntax Tree | 抽象语法树 | |
ABI | 应用程序二进制接口 | Application Binary Interface 缩写 |
accumulator | 累加器 | |
accumulator variable | 累加器变量 | |
ahead-of-time compiled | 预编译 | |
ahead-of-time compiled language | 预编译语言 | |
alias | 别名 | |
aliasing | 别名使用 | 参见 Wikipedia |
angle brackets | 尖括号,“<”和“>” | |
annotate | 标注,注明(动词) | |
annotation | 标注,注明(名词) | |
Arc | 原子引用计数器 | Atomic Referecne Counter |
anonymity | 匿名 | |
argument | 参数,实参,实际参数 | 不严格区分的话, argument(参数)和 parameter(参量)可以互换地使用 |
argument type | 参数类型 | |
assignment | 赋值 | |
associated functions | 关联函数 | |
associated items | 关联项 | |
associated types | 关联类型 | |
asterisk | 星号(*) | |
atomic | 原子的 | |
attribute | 属性 | |
automated building | 自动构建 | |
automated test | 自动测试,自动化测试 | |
B | ||
benchmark | 基准 | |
binary | 二进制的 | |
binary executable | 二进制的可执行文件 | |
bind | 绑定 | |
block | 语句块,代码块 | |
boolean | 布尔型,布尔值 | |
borrow check | 借用检查 | |
borrower | 借用者,借入者 | |
borrowing | 借用 | |
bound | 约束,限定,限制 | 此词和 constraint 意思相近, constraint 在 C# 语言中翻译成“约束” |
box | 箱子,盒子,装箱类型 | 一般不译,作动词时翻译成“装箱”, 具有所有权的智能指针 |
boxed | 装箱,装包 | |
boxing | 装箱,装包 | |
brace | 大括号,“{”或“}” | |
buffer | 缓冲,缓冲区,缓冲器,缓存 | |
build | 构建 | |
builder pattern | 创建者模式 | |
C | ||
call | 调用 | |
caller | 调用者 | |
capacity | 容器 | |
capture | 捕获 | |
cargo | (Rust 包管理器,不译) | 该词作名词时意思是“货物”, 作动词时意思是“装载货物” |
cargo-fy | Cargo 化,使用 Cargo 创建项目 | |
case analysis | 事例分析 | |
cast | 类型转换,转型 | |
casting | 类型转换 | |
chaining method call | 链式方法调用 | |
channel | 信道,通道 | |
closure | 闭包 | |
coercion | 强制类型转换,强制转换 | coercion 原意是“强制,胁迫” |
collection | 集合 | 参见 Wikipedia |
combinator | 组合算子,组合器 | |
comma | 逗号,“,” | |
command | 命令 | |
command line | 命令行 | |
comment | 注释 | |
compile | 编译(动词) | |
compile time | 编译期,编译期间,编译时 | |
compilation | 编译(名词) | |
compilation unit | 编译单元 | |
compiler | 编译器 | |
compiler intrinsics | 编译器固有功能 | |
compound | 复合(类型,数据) | |
concurrency | 并发 | |
conditional compilation | 条件编译 | |
configuration | 配置 | |
constructor | 构造器 | |
consumer | 消费者 | |
container | 容器 | |
container type | 容器类型 | |
convert | 转换,转化,转 | |
copy | 复制,拷贝 | |
crate | 包,包装箱,装包 | 一般不译,crate 是 Rust 的基本编译单元 |
curly braces | 大括号,包含“{”和“}” | |
custom type | 自定义类型 | |
D | ||
dangling pointer | 悬垂指针 | use after free 在释放后使用 |
data race | 数据竞争 | |
dead code | 死代码,无效代码,不可达代码 | |
deallocate | 释放,重新分配 | |
declare | 声明 | |
deep copy | 深拷贝,深复制 | |
dependency | 依赖 | |
deref coercions | 强制多态 | |
dereference | 解引用 | Rust 文章中有时简写为 Deref |
derive | 派生 | |
designator | 指示符 | |
destruction | 销毁,毁灭 | |
destructor | 析构器,析构函数 | |
destructure | 解构 | |
destructuring | 解构,解构赋值 | |
desugar | 脱糖 | |
diverge function | 发散函数 | |
device drive | 设备驱动 | |
directory | 目录 | |
dispatch | 分发 | |
diverging functions | 发散函数 | |
documentation | 文档 | |
dot operator | 点运算符 | |
DST | 动态大小类型 | dynamic sized type,一般不译, 使用英文缩写形式 |
dynamic language | 动态类型语言 | |
dynamic trait type | 动态特质类型 | |
declarative macros | 声明宏 | |
E | ||
enumeration | 枚举 | |
encapsulation | 封装 | |
equality test | 相等测试 | |
elision | 省略 | |
exhaustiveness checking | 穷尽性检查,无遗漏检查 | |
expression | 表达式 | |
expression-oriented language | 面向表达式的语言 | |
explicit | 显式 | |
explicit discriminator | 显式的辨别值 | |
explicit type conversion | 显式类型转换 | |
extension | 扩展名 | |
extern | 外,外部 | 作关键字时不译 |
F | ||
fat pointer | 胖指针 | |
feature gate | 功能开关 | |
field | 字段 | |
field-level mutability | 字段级别可变性 | |
file | 文件 | |
fmt | 格式化,是 format 的缩写 | |
formatter | 格式化程序,格式化工具,格式器 | |
floating-point number | 浮点数 | |
flow control | 流程控制 | |
Foreign Function Interface(FFI) | 外部语言函数接口 | |
fragment specifier | 片段分类符 | |
free variables | 自由变量 | |
freeze | 冻结 | |
function | 函数 | |
function declaration | 函数声明 | |
functional | 函数式 | |
G | ||
garbage collector | 垃圾回收 | |
generalize | 泛化,泛型化 | |
generator | 生成器 | |
generic | 泛型 | |
generic type | 泛型类型 | |
growable | 可增长的 | |
guard | 守卫 | |
H | ||
handle error | 句柄错误 | |
hash | 哈希,哈希值,散列 | |
hash map | 散列映射,哈希表 | |
heap | 堆 | |
hierarchy | 层次,分层,层次结构 | |
higher rank lifetime | 高阶生命周期 | |
higher rank trait bound | 高阶特质约束 | |
higher tank type | 高阶类型 | |
hygiene | 卫生 | |
hygienic macro system | 卫生宏系统 | |
I | ||
ICE | 编译内部错误 | internal comppiler error 的缩写 |
immutable | 不可变的 | |
implement | 实现 | |
implementor | 实现者 | |
implicit | 隐式 | |
implicit discriminator | 隐式的辨别值 | |
implicit type conversion | 隐式类型转换 | |
import | 导入 | |
in assignment | 在赋值(语句) | |
index | 索引 | 英语复数形式:indices |
infer | 推导(动词) | |
inference | 推导(名词) | |
inherited mutability | 承袭可变性 | |
inheritance | 继承 | |
integrated development environment(IDE) | 集成开发环境 | 中文著作中通常直接写成 IDE |
integration-style test | 集成测试 | |
interior mutability | 内部可变性 | |
installer | 安装程序,安装器 | |
instance | 实例 | |
instance method | 实例方法 | |
integer | 整型,整数 | |
interact | 相互作用,相互影响 | |
interior mutability | 内部可变性 | |
intrinsic | 固有的 | |
invoke | 调用 | |
item | 项,条目,项目 | |
iterate | 重复 | |
iteration | 迭代 | |
iterator | 迭代器 | |
iterator adaptors | 迭代器适配器 | |
iterator invalidation | 迭代器失效 | |
L | ||
LHS | 左操作数 | left-hand side 的非正式缩写, 与 RHS 相对 |
lender | 借出者 | |
library | 库 | |
lifetime | 生存期/ 寿命 / 生命周期 | |
lifetime elision | 生命周期省略 | |
link | 链接 | |
linked-list | 链表 | |
lint | (不译) | lint 英文本义是“纱布,绒毛”,此词在 计算机领域中表示程序代码中可疑和 不具结构性的片段,参见 Wikipedia |
list | 列表 | |
listener | 监听器 | |
literal | 数据,常量数据,字面值,字面量, 字面常量,字面上的 | 英文意思:字面意义的(内容) |
LLVM | (不译) | Low Level Virtual Machine 的缩写, 是构建编译器的系统 |
loop | 循环 | 作关键字时不译 |
low-level code | 底层代码 | |
low-level language | 底层语言 | |
l-value | 左值 | |
M | ||
main function | main 函数,主函数 | |
macro | 宏 | |
map | 映射 | 一般不译 |
match guard | 匹配守卫 | |
memory | 内存 | |
memory leak | 内存泄露 | |
memory safe | 内存安全 | |
meta | 原则,元 | |
metadata | 元数据 | |
metaprogramming | 元编程 | |
metavariable | 元变量 | |
method call syntax | 方法调用语法 | |
method chaining | 方法链 | |
method definition | 方法定义 | |
modifier | 修饰符 | |
module | 模块 | |
monomorphization | 单态 | mono: one, morph: form |
move | 移动,转移 | 按照 Rust 所规定的内容, 英语单词 transfer 的意思 比 move 更贴合实际描述 参考:Rust by Example |
move semantics | 移动语义 | |
mutability | 可变性 | |
mutable | 可变 | |
mutable reference | 可变引用 | |
multiple bounds | 多重约束 | |
mutiple patterns | 多重模式 | |
N | ||
nest | 嵌套 | |
Nightly Rust | Rust 开发版 | nightly本意是“每夜,每天晚上”, 指代码每天都更新 |
NLL | 非词法生命周期 | non lexical lifetime 的缩写, 一般不译 |
non-copy type | 非复制类型 | |
non-generic | 非泛型 | |
no-op | 空操作,空运算 | (此词出现在类型转换章节中) |
non-commutative | 非交换的 | |
non-scalar cast | 非标量转换 | |
notation | 符号,记号 | |
numeric | 数值,数字 | |
O | ||
optimization | 优化 | |
out-of-bounds accessing | 越界访问 | |
orphan rule | 孤儿规则 | |
overflow | 溢出,越界 | |
own | 占有,拥有 | |
owner | 所有者,拥有者 | |
ownership | 所有权 | |
P | ||
package manager | 包管理器,软件包管理器 | |
panic | (不译) | 此单词直接翻译是“恐慌”, 在 Rust 中用于不可恢复的错误处理 |
parameter | 参量,形参,形式参量 | 不严格区分的话, argument(参数)和 parameter(参量)可以互换地使用 |
parametric polymorphism | 参数多态 | |
parent scope | 父级作用域 | |
parentheses | 小括号,包括“(”和“)” | |
parse | 分析,解析 | |
parser | (语法)分析器,解析器 | |
pattern | 模式 | |
pattern match | 模式匹配 | |
phantom type | 虚类型,虚位类型 | phantom 相关的专有名词: phantom bug 幻影指令 phantom power 幻象电源 参见:Haskell、Haskell/Phantom_type、 Rust/Phantom、stdlib/PhantomData |
platform | 平台 | |
polymorphism | 多态 | |
powershell | (不译) | Windows 系统的一种命令行外壳程序 和脚本环境 |
possibility of absence | 不存在的可能性 | |
precede | 预先?,在...发生(或出现) | |
prelude | (不译) | 预先导入模块,英文本意:序曲,前奏 |
primitive types | 原生类型,基本类型,简单类型 | |
打印 | ||
process | 进程 | |
procedural macros | 过程宏,程序宏 | |
project | 项目,工程 | |
prototype | 原型 | |
R | ||
race condition | 竞态条件 | |
RAII | 资源获取即初始化(一般不译) | resource acquisition is initialization 的缩写 |
range | 区间,范围 | |
range expression | 区间表达式 | |
raw identifier | 原始标识符 | |
raw pointer | 原始指针,裸指针 | |
RC | 引用计数 | reference counted |
Reader | 读取器 | |
recursive macro | 递归宏 | |
reference | 引用 | |
reference cycle | 引用循环 | |
release | 发布 | |
resource | 资源 | |
resource leak | 资源泄露 | |
RHS | 右操作数 | right-hand side 的非正式缩写, 与 LHS 相对 |
root directory | 根目录 | |
runtime | 运行时 | |
runtime behavior | 运行时行为 | |
runtime overhead | 运行时开销 | |
Rust | (不译) | 一种编程语言 |
Rustacean | (不译) | 编写 Rust 的程序员或爱好者的通称 |
rustc | (不译) | Rust 语言编译器 |
r-value | 右值 | |
S | ||
scalar | 标量,数量 | |
schedule | 调度 | |
scope | 作用域 | |
screen | 屏幕 | |
script | 脚本 | |
semicolon | 分号,“;” | |
self | 自身,作关键字时不译 | |
shadow | 遮蔽,隐蔽,隐藏,覆盖 | |
shallow copy | 浅拷贝,浅复制 | |
signature | 标记 | |
slice | 切片 | |
snake case | 蛇形命名 | 参见:Snake case |
source file | 源文件 | |
source code | 源代码 | |
specialization | 泛型特化 | |
square | 平方,二次方,二次幂 | |
square brackets | 中括号,“[”和“]” | |
src | (不译) | source 的缩写,指源代码 |
stack | 栈 | |
stack unwind | 栈解开、栈展开 | |
statement | 语句 | |
statically allocated | 静态分配 | |
statically allocated string | 静态分配的字符串 | |
statically dispatch | 静态分发 | |
static method | 静态方法 | |
string | 字符串 | |
string literal | 字符串常量 | |
string slices | 字符串片段 | |
stringify | 字符串化 | |
subscript notation | 下标 | |
sugar | 糖 | |
super | 父级,作关键字时不译 | |
syntax context | 语法上下文 | |
systems programming language | 系统级编程语言 | |
T | ||
tagged union | 标记联合 | |
target triple | 多层次指标,三层/重 指标/目标 | triple 本义是“三”,但此处虚指“多”, 此词翻译需要更多讨论 |
terminal | 终端 | |
testing | 测试 | |
testsuit | 测试套件 | |
the least significant bit (LSB) | 最低数字位 | |
the most significant bit (MSB) | 最高数字位 | |
thread | 线程 | |
TOML | (不译) | Tom's Obvious, Minimal Language 的缩写,一种配置语言 |
token tree | 令牌树? | 待进一步斟酌 |
trait | 特质 | 其字面上有“特性,特征”之意 |
trait bound | 特质约束 | bound 有“约束,限制,限定”之意 |
trait object | 特质对象 | |
transmute | (不译) | 其字面上有“变化,变形,变异”之意, 不作翻译 |
trivial | 平凡的 | |
troubleshooting | 疑难解答,故障诊断, 故障排除,故障分析 | |
tuple | 元组 | |
two's complement | 补码,二补数 | |
two-word object | 双字对象 | |
type annotation | 类型标注 | |
type erasure | 类型擦除 | |
type inference | 类型推导 | |
type inference engine | 类型推导引擎 | |
type parameter | 类型参量 | |
type placeholder | 类型占位符 | |
type signature | 类型标记 | |
U | ||
undefined behavior | 未定义行为 | |
uninstall | 卸载 | |
unit-like struct | 类单元结构体 | |
unit struct | 单元结构体 | |
"unit-style" tests | 单元测试 | |
unit test | 单元测试 | |
unit type | 单元类型 | |
universal function call syntax (UFCS) | 通用函数调用语法 | |
unsized types | 不定长类型 | |
unwind | 展开 | |
unwrap | 解包 | 暂译! |
V | ||
variable binding | 变量绑定 | |
variable shadowing | 变量遮蔽,变量隐蔽, 变量隐藏,变量覆盖 | |
variable capture | 变量捕获 | |
variant | 变量 | |
vector | (动态数组,一般不译) | vector 本义是“向量” |
visibility | 可见性 | |
vtable | 虚表 | |
W | ||
where clause | where 子句,where 从句,where 分句 | 在数据库的官方手册中多翻译成“子句”,英语语法中翻译成“从句” |
wrap | 包裹 | 暂译! |
wrapped | 装包 | |
wrapper | 装包 | |
Y | ||
yield | 产生(收益、效益等),产出,提供 | |
Z | ||
zero-cost abstractions | 零开销抽象 | |
zero-width space(ZWSP) | 零宽空格 |
参考
编译器相关术语表
术语 | 中文 | 意义 |
---|---|---|
arena/arena allocation | 竞技场分配 | arena 是一个大内存缓冲区,从中可以进行其他内存分配,这种分配方式称为竞技场分配。 |
AST | 抽象语法树 | 由rustc_ast crate 产生的抽象语法树。 |
binder | 绑定器 | 绑定器是声明变量和类型的地方。例如,<T> 是fn foo<T>(..) 中泛型类型参数 T 的绑定器,以及 |a | ... 是 参数a 的绑定器。 |
BodyId | 主体ID | 一个标识符,指的是crate 中的一个特定主体(函数或常量的定义)。 |
bound variable | 绑定变量 | "绑定变量 "是在表达式/术语中声明的变量。例如,变量a 被绑定在闭包表达式中|a | a * 2 。 |
codegen | 代码生成 | 由 MIR 转译为 LLVM IR。 |
codegen unit | 代码生成单元 | 当生成LLVM IR时,编译器将Rust代码分成若干个代码生成单元(有时缩写为CGU)。这些单元中的每一个都是由LLVM独立处理的,实现了并行化。它们也是增量编译的单位。 |
completeness | 完整性 | 类型理论中的一个技术术语,它意味着每个类型安全的程序也会进行类型检查。同时拥有健全性(soundness)和完整性(completeness)是非常困难的,而且通常健全性(soundness)更重要。 |
control-flow graph | 控制流图 | 程序的控制流表示。 |
CTFE | 编译时函数求值 | 编译时函数求值(Compile-Time Function Evaluation)的简称,是指编译器在编译时计算 "const fn "的能力。这是编译器常量计算系统的一部分。 |
cx | 上下文 | Rust 编译器内倾向于使用 "cx "作为上下文的缩写。另见 "tcx"、"infcx "等。 |
ctxt | 上下文(另一个缩写) | 我们也使用 "ctxt "作为上下文的缩写,例如, TyCtxt ,以及 cx 或 tcx。 |
DAG | 有向无环图 | 在编译过程中,一个有向无环图被用来跟踪查询之间的依赖关系 |
data-flow analysis | 数据流分析 | 静态分析,找出程序控制流中每一个点的属性。 |
DeBruijn Index | 德布鲁因索引 | 一种只用整数来描述一个变量被绑定的绑定器的技术。它的好处是,在变量重命名下,它是不变的。 |
DefId | 定义Id | 一个识别定义的索引(见rustc_middle/src/hir/def_id.rs )。DefPath 的唯一标识。 |
discriminant | 判别式 | 与枚举变体或生成器状态相关的基础值,以表明它是 "激活的(avtive)"(但不要与它的"变体索引"混淆)。在运行时,激活变体的判别值被编码在tag中。 |
double pointer | 双指针 | 一个带有额外元数据的指针。同指「胖指针」。 |
drop glue | drop胶水 | (内部)编译器生成的指令,处理调用数据类型的析构器(Drop )。 |
DST | DST | Dynamically-Sized Type的缩写,这是一种编译器无法静态知道内存大小的类型(例如:str'或 [u8])。这种类型没有实现 Sized,不能在栈中分配。它们只能作为结构中的最后一个字段出现。它们只能在指针后面使用(例如: &str或 &[u8]`)。 |
early-bound lifetime | 早绑定生存期 | 一个在其定义处被替换的生存期区域(region)。绑定在一个项目的Generics'中,并使用 Substs'进行替换。与late-bound lifetime形成对比。 |
empty type | 空类型 | 参考 "uninhabited type". |
fat pointer | 胖指针 | 一个两字(word)的值,携带着一些值的地址,以及一些使用该值所需的进一步信息。Rust包括两种 "胖指针":对切片(slice)的引用和特质(trait)对象。对切片的引用带有切片的起始地址和它的长度。特质对象携带一个值的地址和一个指向适合该值的特质实现的指针。"胖指针 "也被称为 "宽指针",和 "双指针"。 |
free variable | 自由变量 | 自由变量 是指没有被绑定在表达式或术语中的变量; |
generics | 泛型 | 通用类型参数集。 |
HIR | 高级中间语言 | 高级中间语言,通过对AST进行降级(lowering)和去糖(desugaring)而创建。 |
HirId | HirId | 通过结合“def-id”和 "intra-definition offset"来识别HIR中的一个特定节点。 |
HIR map | HIR map | 通过tcx.hir() 访问的HIR Map,可以让你快速浏览HIR并在各种形式的标识符之间进行转换。 |
ICE | ICE | 内部编译器错误的简称,这是指编译器崩溃的情况。 |
ICH | ICH | 增量编译哈希值的简称,它们被用作HIR和crate metadata等的指纹,以检查是否有变化。这在增量编译中是很有用的,可以查看crate的一部分是否发生了变化,应该重新编译。 |
infcx | 类型推导上下文 | 类型推导上下文(InferCtxt )。 |
inference variable | 推导变量 | 在进行类型或区域推理时,"推导变量 "是一种特殊的类型/区域,代表你试图推理的内容。想想代数中的X。例如,如果我们试图推断一个程序中某个变量的类型,我们就创建一个推导变量来代表这个未知的类型。 |
intern | intern | intern是指存储某些经常使用的常量数据,如字符串,然后用一个标识符(如`符号')而不是数据本身来引用这些数据,以减少内存的使用和分配的次数。 |
intrinsic | 内部函数 | 内部函数是在编译器本身中实现的特殊功能,但向用户暴露(通常是不稳定的)。它们可以做神奇而危险的事情。 |
IR | IR | Intermediate Representation的简称,是编译器中的一个通用术语。在编译过程中,代码被从原始源码(ASCII文本)转换为各种IR。在Rust中,这些主要是HIR、MIR和LLVM IR。每种IR都适合于某些计算集。例如,MIR非常适用于借用检查器,LLVM IR非常适用于codegen,因为LLVM接受它。 |
IRLO | IRLO | IRLO 或irlo 有时被用作internals.rust-lang.org的缩写。 |
item | 语法项 | 语言中的一种 "定义",如静态、常量、使用语句、模块、结构等。具体来说,这对应于 "item"类型。 |
lang item | 语言项 | 代表语言本身固有的概念的项目,如特殊的内置特质,如同步 和发送 ;或代表操作的特质,如添加 ;或由编译器调用的函数。 |
late-bound lifetime | 晚绑定生存期 | 一个在其调用位置被替换的生存期区域。绑定在HRTB中,由编译器中的特定函数替代,如liberate_late_bound_regions 。与早绑定的生存期形成对比。 |
local crate | 本地crate | 目前正在编译的crate。这与 "上游crate"相反,后者指的是本地crate的依赖关系。 |
LTO | LTO | 链接时优化(Link-Time Optimizations)的简称,这是LLVM提供的一套优化,在最终二进制文件被链接之前进行。这些优化包括删除最终程序中从未使用的函数,例如。_ThinLTO_是LTO的一个变种,旨在提高可扩展性和效率,但可能牺牲了一些优化。 |
LLVM | LLVM | (实际上不是一个缩写 :P) 一个开源的编译器后端。它接受LLVM IR并输出本地二进制文件。然后,各种语言(例如Rust)可以实现一个编译器前端,输出LLVM IR,并使用LLVM编译到所有LLVM支持的平台。 |
memoization | memoization | 储存(纯)计算结果(如纯函数调用)的过程,以避免在未来重复计算。这通常是执行速度和内存使用之间的权衡。 |
MIR | 中级中间语言 | 在类型检查后创建的中级中间语言,供borrowck和codegen使用。 |
miri | mir解释器 | MIR的一个解释器,用于常量计算。 |
monomorphization | 单态化 | 采取类型和函数的通用实现并将其与具体类型实例化的过程。例如,在代码中可能有Vec<T> ,但在最终的可执行文件中,将为程序中使用的每个具体类型有一个Vec 代码的副本(例如,Vec<usize> 的副本,Vec<MyStruct> 的副本,等等)。 |
normalize | 归一化 | 转换为更标准的形式的一般术语,但在rustc的情况下,通常指的是关联类型归一化。 |
newtype | newtype | 对其他类型的封装(例如,struct Foo(T) 是T 的一个 "新类型")。这在Rust中通常被用来为索引提供一个更强大的类型。 |
niche | 利基 | 一个类型的无效位模式可用于布局优化。有些类型不能有某些位模式。例如,"非零*"整数或引用"&T "不能用0比特串表示。这意味着编译器可以通过利用无效的 "利基值 "来进行布局优化。这方面的一个应用实例是Discriminant elision on Option -like enums,它允许使用一个类型的niche作为一个enum 的"标签",而不需要一个单独的字段。 |
NLL | NLL | 这是非词法作用域生存期的简称,它是对Rust的借用系统的扩展,使其基于控制流图。 |
node-id or NodeId | node-id or NodeId | 识别AST或HIR中特定节点的索引;逐渐被淘汰,被HirId 取代。 |
obligation | obligation | 必须由特质系统证明的东西。 |
placeholder | placeholder | 注意:skolemization被placeholder废弃一种处理围绕 "for-all "类型的子类型的方法(例如,for<'a> fn(&'a u32) ),以及解决更高等级的trait边界(例如,for<'a> T: Trait<'a> )。 |
point | point | 在NLL分析中用来指代MIR中的某个特定位置;通常用来指代控制流图中的一个节点。 |
polymorphize | 多态化 | 一种避免不必要的单态化的优化。 |
projection | 投影 | 一个 "相对路径 "的一般术语,例如,x.f 是一个 "字段投影",而T::Item 是一个"关联类型投影" |
promoted constants | 常量提升 | 从函数中提取的常量,并提升到静态范围 |
provider | provider | 执行查询的函数。 |
quantified | 量化 | 在数学或逻辑学中,存在量词和普遍量词被用来提出诸如 "是否有任何类型的T是真的?"或 "这对所有类型的T都是真的吗?"这样的问题 |
query | 查询 | 编译过程中的一个子计算。查询结果可以缓存在当前会话中,也可以缓存到磁盘上,用于增量编译。 |
recovery | 恢复 | 恢复是指在解析过程中处理无效的语法(例如,缺少逗号),并继续解析AST。这可以避免向用户显示虚假的错误(例如,当结构定义包含错误时,显示 "缺少字段 "的错误)。 |
region | 区域 | 和生存期精彩使用的另一个术语。 |
rib | rib | 名称解析器中的一个数据结构,用于跟踪名称的单一范围。 |
scrutinee | 审查对象 | 审查对象是在match 表达式和类似模式匹配结构中被匹配的表达式。例如,在match x { A => 1, B => 2 } 中,表达式x 是被审查者。 |
sess | sess | 编译器会话,它存储了整个编译过程中使用的全局数据 |
side tables | side tables | 由于AST和HIR一旦创建就不可改变,我们经常以哈希表的形式携带关于它们的额外信息,并以特定节点的ID为索引。 |
sigil | 符号 | 就像一个关键词,但完全由非字母数字的标记组成。例如,& 是引用的标志。 |
soundness | 健全性 | 类型理论中的一个技术术语。粗略的说,如果一个类型系统是健全的,那么一个进行类型检查的程序就是类型安全的。也就是说,人们永远不可能(在安全的Rust中)把一个值强加到一个错误类型的变量中。 |
span | span | 用户的源代码中的一个位置,主要用于错误报告。这就像一个文件名/行号/列的立体元组:它们携带一个开始/结束点,也跟踪宏的扩展和编译器去糖。所有这些都被装在几个字节里(实际上,它是一个表的索引)。 |
substs | 替换 | 给定的通用类型或项目的替换(例如,HashMap<i32, u32> 中的i32'、 u32')。 |
sysroot | sysroot | 用于编译器在运行时加载的构建工件的目录。 |
tag | tag | 枚举/生成器的 "标签 "编码激活变体/状态的判别式(discriminant)。 标签可以是 "直接的"(简单地将判别式存储在一个字段中)或使用"利基"。 |
tcx | tcx | "类型化上下文"(TyCtxt ),编译器的主要数据结构。 |
'tcx | 'tcx | TyCtxt'所使用的分配区域的生存期。在编译过程中,大多数数据都会使用这个生存期,但HIR数据除外,它使用 'hir`生存期。 |
token | 词条 | 解析的最小单位。词条是在词法运算后产生的 |
TLS | TLS | 线程本地存储。变量可以被定义为每个线程都有自己的副本(而不是所有线程都共享该变量)。这与LLVM有一些相互作用。并非所有平台都支持TLS。 |
trait reference | trait 引用 | 一个特质的名称,以及一组合适的输入类型/生存期。 |
trans | trans | 是 "转译"的简称,是将MIR转译成LLVM IR的代码。已经重命名为codegen。 |
Ty | Ty | 一个类型的内部表示。 |
TyCtxt | TyCtxt | 在代码中经常被称为tcx的数据结构,它提供对会话数据和查询系统的访问。 |
UFCS | UFCS | 通用函数调用语法(Universal Function Call Syntax)的简称,这是一种调用方法的明确语法。 |
uninhabited type | 孤类型 | 一个没有值的类型。这与ZST不同,ZST正好有一个值。一个孤类型的例子是enum Foo {} ,它没有变体,所以,永远不能被创建。编译器可以将处理孤类型的代码视为死代码,因为没有这样的值可以操作。! (从未出现过的类型)是一个孤类型。孤类型也被称为 "空类型"。 |
upvar | upvar | 一个闭合体从闭合体外部捕获的变量 |
variance | 型变 | 确定通用类型/寿命参数的变化如何影响子类型;例如,如果T 是U 的子类型,那么Vec<T> 是Vec<U> 的子类型,因为Vec 在其通用参数中是协变的。 |
variant index | 变体索引 | 在一个枚举中,通过给它们分配从0开始的索引来识别一个变体。这纯粹是内部的,不要与"判别式"相混淆,后者可以被用户覆盖(例如,enum Bool { True = 42, False = 0 } )。 |
wide pointer | 宽指针 | 一个带有额外元数据的指针。 |
ZST | ZST | 零大小类型。这种类型,其值的大小为0字节。由于2^0 = 1 ,这种类型正好有一个值。例如,() (单位)是一个ZST。struct Foo; 也是一个ZST。编译器可以围绕ZST做一些很好的优化。 |
参考
D.模版
这里记录一些 rustfmt 和 clippy 等相关工具等配置文件模版。
Rustfmt 模板
为了方便 Rust 开发者,这里提供一个 Rustfmt 的模板,以供参考。
以下内容可以放到 rustfmt.toml
或 .rustfmt.toml
文件中。因为部分选项还未稳定,所以要使用 cargo +nightly fmt
执行。
很多选项都是默认的,无需配置。以下配置的都不是默认值。
【只包含 Stable 的选项】
# 万一你要使用 rustfmt 2.0 就需要指定这个·
version = "Two"
# 统一管理宽度设置,但不包含 comment_width
use_small_heuristics="MAX"
# 在match分支中,如果包含了块,也需要加逗号以示分隔
match_block_trailing_comma=true
# 当使用 extern 指定外部函数时,不需要显式指定 C-ABI ,默认就是 C-ABI
force_explicit_abi=false
# 如果项目只在 Unix 平台下跑,可以设置该项为 Unix,表示换行符只依赖 Unix
newline_style="Unix"
# 不要将多个 Derive 宏合并为同一行
merge_derives = false
# 指定 fmt 忽略的目录
ignore = [
"src/test",
"test",
"docs",
]
【也包含还未 Stable 的选项】
未稳定,代表该选项还有一些 issue 没有解决,待解决以后就会稳定。
# 万一你要使用 rustfmt 2.0 就需要指定这个·
version = "Two"
# 统一管理宽度设置,但不包含 comment_width
use_small_heuristics="MAX"
# 使多个标识符定义保持对齐风格,代码看上去可以非常工整
indent_style="Visual" # 未稳定
# 设置让自定义具有判别式的枚举体按等号对齐的宽度
enum_discrim_align_threshold = 10 # 未稳定
# 在match分支中,如果包含了块,也需要加逗号以示分隔
match_block_trailing_comma=true
# 自动将同一个 crate 的模块导入合并到一起
imports_granularity="Crate" # 未稳定
# StdExternalCrate 导入模块分组规则
# 1. 导入来自 std、core 和 alloc 的模块需要置于前面一组。
# 2. 导入来自 第三方库的模块 应该置于中间一组。
# 3. 导入来自本地 self、super和crate前缀的模块,置于后面一组。
group_imports="StdExternalCrate" # 未稳定
# format_macro_matchers 规则说明:
# 声明宏 模式匹配分支(=> 左侧)中要使用紧凑格式
# 默认声明宏分支代码体(=> 右侧) 使用宽松格式
format_macro_matchers=true # 未稳定
# 当使用 extern 指定外部函数时,不需要显式指定 C-ABI ,默认就是 C-ABI
force_explicit_abi=false
# 指定一行注释允许的最大宽度
comment_width=100 # 未稳定
# wrap_comments 配合 comment_width 使用,自动将一行超过宽带限制的注释切分为多行注释
wrap_comments=true # 未稳定
# 将 /**/ 注释转为 //
normalize_comments=true # 未稳定
# 元组模式匹配的时候允许使用 `..` 来匹配剩余元素
condense_wildcard_suffixes=true # 未稳定
# 如果项目只在 Unix 平台下跑,可以设置该项为 Unix,表示换行符只依赖 Unix
newline_style="Unix"
# 不要将多个 Derive 宏合并为同一行
merge_derives = false
# 指定 fmt 忽略的目录
ignore = [
"src/test",
"test",
"docs",
]
Clippy 模板
有些 Clippy 的 Lint,依赖于一些配置项,如果不想要默认值,可以在 clippy.toml
中进行设置。
# for `disallowed_method`:
# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_method
disallowed-methods = []
# 函数参数最长不要超过5个
too-many-arguments-threshold=5
Clippy lint 配置模板
#![allow(unused)] fn main() { // 参考: https://github.com/serde-rs/serde/blob/master/serde/src/lib.rs #![allow(unknown_lints, bare_trait_objects, deprecated)] #![cfg_attr(feature = "cargo-clippy", allow(renamed_and_removed_lints))] #![cfg_attr(feature = "cargo-clippy", deny(clippy, clippy_pedantic))] // Ignored clippy and clippy_pedantic lints #![cfg_attr( feature = "cargo-clippy", allow( // clippy bug: https://github.com/rust-lang/rust-clippy/issues/5704 unnested_or_patterns, // clippy bug: https://github.com/rust-lang/rust-clippy/issues/7768 semicolon_if_nothing_returned, // not available in our oldest supported compiler checked_conversions, empty_enum, redundant_field_names, redundant_static_lifetimes, // integer and float ser/de requires these sorts of casts cast_possible_truncation, cast_possible_wrap, cast_sign_loss, // things are often more readable this way cast_lossless, module_name_repetitions, option_if_let_else, single_match_else, type_complexity, use_self, zero_prefixed_literal, // correctly used enum_glob_use, let_underscore_drop, map_err_ignore, result_unit_err, wildcard_imports, // not practical needless_pass_by_value, similar_names, too_many_lines, // preference doc_markdown, unseparated_literal_suffix, // false positive needless_doctest_main, // noisy missing_errors_doc, must_use_candidate, ) )] // Rustc lints. #![deny(missing_docs, unused_imports)] }
Embark Studios 的标准 Lint 配置
#![allow(unused)] fn main() { // BEGIN - Embark standard lints v5 for Rust 1.55+ // do not change or add/remove here, but one can add exceptions after this section // for more info see: <https://github.com/EmbarkStudios/rust-ecosystem/issues/59> #![deny(unsafe_code)] #![warn( clippy::all, clippy::await_holding_lock, clippy::char_lit_as_u8, clippy::checked_conversions, clippy::dbg_macro, clippy::debug_assert_with_mut_call, clippy::disallowed_method, clippy::disallowed_type, clippy::doc_markdown, clippy::empty_enum, clippy::enum_glob_use, clippy::exit, clippy::expl_impl_clone_on_copy, clippy::explicit_deref_methods, clippy::explicit_into_iter_loop, clippy::fallible_impl_from, clippy::filter_map_next, clippy::flat_map_option, clippy::float_cmp_const, clippy::fn_params_excessive_bools, clippy::from_iter_instead_of_collect, clippy::if_let_mutex, clippy::implicit_clone, clippy::imprecise_flops, clippy::inefficient_to_string, clippy::invalid_upcast_comparisons, clippy::large_digit_groups, clippy::large_stack_arrays, clippy::large_types_passed_by_value, clippy::let_unit_value, clippy::linkedlist, clippy::lossy_float_literal, clippy::macro_use_imports, clippy::manual_ok_or, clippy::map_err_ignore, clippy::map_flatten, clippy::map_unwrap_or, clippy::match_on_vec_items, clippy::match_same_arms, clippy::match_wild_err_arm, clippy::match_wildcard_for_single_variants, clippy::mem_forget, clippy::mismatched_target_os, clippy::missing_enforced_import_renames, clippy::mut_mut, clippy::mutex_integer, clippy::needless_borrow, clippy::needless_continue, clippy::needless_for_each, clippy::option_option, clippy::path_buf_push_overwrite, clippy::ptr_as_ptr, clippy::rc_mutex, clippy::ref_option_ref, clippy::rest_pat_in_fully_bound_structs, clippy::same_functions_in_if_condition, clippy::semicolon_if_nothing_returned, clippy::single_match_else, clippy::string_add_assign, clippy::string_add, clippy::string_lit_as_bytes, clippy::string_to_string, clippy::todo, clippy::trait_duplication_in_bounds, clippy::unimplemented, clippy::unnested_or_patterns, clippy::unused_self, clippy::useless_transmute, clippy::verbose_file_reads, clippy::zero_sized_map_values, future_incompatible, nonstandard_style, rust_2018_idioms )] // END - Embark standard lints v0.5 for Rust 1.55+ // crate-specific exceptions: #![allow()] }
Clippy 配置的相关问题
目前 Clippy 不支持配置文件来配置Lint ,目前 像 Embark 公司有两种解决方法:
- 将 lint 放到一个统一文件中,然后复制粘贴到使用的地方。
- 通过
.cargo/config.toml
来配置rustflags
,参考: lints.toml
Embark 也在跟踪和推动在 Cargo 中支持 Lint 配置的功能,相关 issues:
- Be able to disable/enable Clippy lints globally
- Support defining enabled and disabled lints in a configuration file
- [Roadmap] Configuration file for lints
代码生成相关 clippy 配置
和 C 语言绑定代码生成,避免clippy 警告,相关配置可参考:
#![allow(unused)] fn main() { // Generated by gir (https://github.com/gtk-rs/gir @ 5bbf6cb) // from ../gir-files (@ 8e47c67) // DO NOT EDIT #![allow(non_camel_case_types, non_upper_case_globals, non_snake_case)] #![allow(clippy::approx_constant, clippy::type_complexity, clippy::unreadable_literal, clippy::upper_case_acronyms)] }
Cargo Deny 配置模板
cargo-deny 是检查 Cargo 依赖的一个 Lint 工具。它检查的范围包括:
- Licenses,检查依赖crate许可证是否合规。
- Bans, 检查被禁止使用的依赖 crate。
- Advisories ,检查有安全缺陷漏洞或停止维护的 依赖 crate。
- Source,检查依赖crate 的来源,确保只来自于可信任的来源。
以下是模板(参考 vectordotdev/vector 的 deny.toml):
[licenses]
allow = [
"MIT",
"CC0-1.0",
"ISC",
"OpenSSL",
"Unlicense",
"BSD-2-Clause",
"BSD-3-Clause",
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"Zlib",
]
unlicensed = "warn"
default = "warn"
private = { ignore = true }
[[licenses.clarify]]
name = "ring"
version = "*"
expression = "MIT AND ISC AND OpenSSL"
license-files = [
{ path = "LICENSE", hash = 0xbd0eed23 }
]
[advisories]
ignore = [
# term is looking for a new maintainer
# https://github.com/timberio/vector/issues/6225
"RUSTSEC-2018-0015",
# `net2` crate has been deprecated; use `socket2` instead
# https://github.com/timberio/vector/issues/5582
"RUSTSEC-2020-0016",
# Type confusion if __private_get_type_id__ is overriden
# https://github.com/timberio/vector/issues/5583
"RUSTSEC-2020-0036",
# stdweb is unmaintained
# https://github.com/timberio/vector/issues/5585
"RUSTSEC-2020-0056",
]
E.工具链
这里介绍一些检测工具,比如 Cargo fmt 和 Cargo Clippy.
参考资料
- https://doc.rust-lang.org/rustc/lints/groups.html
- https://rust-lang.github.io/rust-clippy/master/index.html
- https://rust-lang.github.io/rust-clippy/master/index.html
- Dtolnay 对 crates.io 中 clippy lint 应用统计
Rustfmt 配置相关说明
在 Stable Rust 下使用未稳定配置项的方法
- CI Job 可以分为
Stable
和Nightly
。在Stable CI
下进行编译,在Nightly CI
下执行cargo fmt
和cargo clippy
。 - 在项目本地可以使用
cargo +nightly fmt
代替cargo fmt
。
注意: 一定要在文件保存之后再运行 rustfmt`,否则容易出错。
真实项目中的配置案例
- 来自 Rust 语言自身项目。
# Run rustfmt with this config (it should be picked up automatically).
version = "Two"
use_small_heuristics = "Max"
merge_derives = false
# by default we ignore everything in the repository
# tidy only checks files which are not ignored, each entry follows gitignore style
ignore = [
"/build/",
"/*-build/",
"/build-*/",
"/vendor/",
# tests for now are not formatted, as they are sometimes pretty-printing constrained
# (and generally rustfmt can move around comments in UI-testing incompatible ways)
"src/test",
# do not format submodules
"library/backtrace",
"library/stdarch",
"compiler/rustc_codegen_cranelift",
"src/doc/book",
"src/doc/edition-guide",
"src/doc/embedded-book",
"src/doc/nomicon",
"src/doc/reference",
"src/doc/rust-by-example",
"src/doc/rustc-dev-guide",
"src/llvm-project",
"src/tools/cargo",
"src/tools/clippy",
"src/tools/miri",
"src/tools/rls",
"src/tools/rust-analyzer",
"src/tools/rustfmt",
"src/tools/rust-installer",
]
# Fuchsia Format Style
# last reviewed: Jan 29, 2019
# Fuchsia uses 2018 edition only
edition = "2018"
# The "Default" setting has a heuristic which splits lines too aggresively.
# We are willing to revisit this setting in future versions of rustfmt.
# Bugs:
# * https://github.com/rust-lang/rustfmt/issues/3119
# * https://github.com/rust-lang/rustfmt/issues/3120
use_small_heuristics = "Max"
# Prevent carriage returns
newline_style = "Unix"
- 来自 Tikv 。
version = "Two"
unstable_features = true
condense_wildcard_suffixes = true
license_template_path = "etc/license.template"
newline_style = "Unix"
use_field_init_shorthand = true
use_try_shorthand = true
edition = "2018"
newline_style = "unix"
# comments
normalize_comments=true
wrap_comments=true
# imports
imports_granularity="Crate"
group_imports="StdExternalCrate"
一些全局配置项
rustfml 格式化版本
【描述】
Version::One
向后兼容 Rustfmt 1.0。 其他版本仅在主要版本号内向后兼容。目前 version
可选值只有 One
和 Two
。
【对应配置项】
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
version | One(默认) | No | 指定 rustfmlt 格式化版本 |
【示例】
# Run rustfmt with this config (it should be picked up automatically).
version = "Two"
指定文件或目录跳过格式化
【描述】
跳过与指定模式匹配的格式化文件和目录。 模式格式与 .gitignore
相同。 一定要使用 Unix/forwardslash/style
路径,此路径样式适用于所有平台。 不支持带有反斜杠 \
的 Windows 样式路径。
【对应配置项】
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
ignore | 格式化每一个Rust文件(默认) | No | 指定文件或目录跳过格式化 |
【示例】
#![allow(unused)] fn main() { // 跳过指定文件 ignore = [ "src/types.rs", "src/foo/bar.rs", ] // 跳过指定目录 ignore = [ "examples", ] // 跳过项目内所有文件 ignore = ["/"] }
禁用格式化
【描述】
可以通过 disable_all_formatting=true
配置来禁用格式化。默认是开启的。
【对应配置项】
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
disable_all_formatting | false(默认) | No | 禁止格式化 |
配置 edition 版次
【描述】
如果通过 Cargo 的格式化工具 cargo fmt 执行,Rustfmt 能够通过读取 Cargo.toml 文件来获取使用的版本。 否则,需要在配置文件中指定版本。
【对应配置项】
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
edition | 2015(默认) | No | 配置 edition 版次 |
【示例】
edition = "2018"
开启未稳定特性
【描述】
默认未启用,但是可以通过配置此功能在 Nightly 上启用此功能。
【对应配置项】
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
unstable_features | false(默认) | No | 开启未稳定特性 |
每行最大宽度为 100 个字符
【级别】 建议
【描述】
代码行宽不宜过长,否则不利于阅读。 建议每行字符数不要超过 100 个字符。
rustfmt
还提供很多其他宽度设置:
- fn_call_width, 函数调用最大宽度设置,其默认值是
max_width
的60%
。 - attr_fn_like_width, 像函数那样使用的属性宏最大宽度,其默认值是
max_width
的70%
。 - struct_lit_width, 结构体字面量最大宽度,其默认值是
max_width
的18%
。 - struct_variant_width, 结构体变量最大宽度,其默认值是
max_width
的35%
。 - array_width, 数组最大宽度,其默认值是
max_width
的60%
。 - chain_width, 链式结构最大宽度,其默认值是
max_width
的60%
。 - single_line_if_else_max_width,单行
if-else
最大宽度,其默认值是max_width
的50%
。
这么多宽度设置管理起来比较麻烦,所以使用 use_small_heuristics
来管理更好。
【反例】
当use_small_heuristics
配置为 Off
:
enum Lorem { Ipsum, Dolor(bool), Sit { amet: Consectetur, adipiscing: Elit, }, } fn main() { lorem("lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing"); let lorem = Lorem { ipsum: dolor, sit: amet, }; let lorem = if ipsum { dolor } else { sit }; }
当use_small_heuristics
配置为 Max
:
enum Lorem { Ipsum, Dolor(bool), Sit { amet: Consectetur, adipiscing: Elit }, } fn main() { lorem("lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing"); let lorem = Lorem { ipsum: dolor, sit: amet }; let lorem = if ipsum { dolor } else { sit }; }
【正例】
use_small_heuristics
默认配置示例。
enum Lorem { Ipsum, Dolor(bool), Sit { amet: Consectetur, adipiscing: Elit }, } fn main() { lorem( "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", ); let lorem = Lorem { ipsum: dolor, sit: amet, }; let lorem = Lorem { ipsum: dolor }; let lorem = if ipsum { dolor } else { sit }; }
【rustfmt 配置】
此规则 Clippy 不可检测,由 rustfmt 自动格式化。
rustfmt 配置:
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
max_width | 100 | yes(默认) | 行最大宽度默认值 |
error_on_line_overflow | false(默认) | No (tracking issue: #3391) | 如果超过最大行宽设置则报错 |
use_small_heuristics | Default(默认)Max(推荐) | Yes | 统一管理宽度设置 |
单行规则
【级别】 建议
【描述】
当语言项内容为空时,即空函数,空结构体,空实现等,要保持单独一行。但是,当函数中只有一个表达式时,请不要保持单行。
【反例】
fn lorem() { } impl Lorem { } fn lorem() -> usize { 42 } fn main() { let lorem = Lorem { foo: bar, baz: ofo, }; }
【正例】
fn lorem() {} impl Lorem {} fn lorem() -> usize { 42 } fn main() { let lorem = Lorem { foo: bar, baz: ofo }; }
【rustfmt 配置】
此规则 Clippy 不可检测,由 rustfmt 自动格式化。
rustfmt 配置:
对应选项 | 默认值 | 是否 stable | 说明 |
---|---|---|---|
empty_item_single_line | true(默认) | No | 当语言项内容为空时,要保持单行 |
fn_single_line | false(默认) | No | 当函数中只有一个表达式时,不要保持单行 |
struct_lit_single_line | true(默认) | No | 当结构体字面量中只有少量表达式时,要保持单行 |
换行样式以文件自动检测为主
【级别】 建议
【描述】
换行样式是基于每个文件自动检测的。 具有混合行尾的文件将转换为第一个检测到的行尾样式。
不同平台换行符不同:
Windows
以\r\n
结尾。Unix
以\n
结尾。
【rustfmt 配置】
此规则 Clippy 不可检测,由 rustfmt 自动格式化。
rustfmt 配置:
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
newline_style | Auto(默认) | Yes | 换行样式以文件自动检测为主 |
结尾逗号规则
【级别】 建议
【描述】
- 当多个字段在不同行时,在最后一个字段结尾添加逗号,如果在同一行,则不加逗号。
- 在match分支中,如果包含了块,则不需要加逗号,否则需要加。
【反例】
// 当 `trailing_comma="Always"` fn main() { let Lorem { ipsum, dolor, sit, } = amet; let Lorem { ipsum, dolor, sit, amet, consectetur, adipiscing, } = elit; } // 当 `trailing_comma="Never"` fn main() { let Lorem { ipsum, dolor, sit } = amet; let Lorem { ipsum, dolor, sit, amet, consectetur, adipiscing } = elit; } // 当 `match_block_trailing_comma=true` fn main() { match lorem { Lorem::Ipsum => { println!("ipsum"); }, Lorem::Dolor => println!("dolor"), } }
【正例】
// 当 `trailing_comma="Vertical"` fn main() { let Lorem { ipsum, dolor, sit } = amet; let Lorem { ipsum, dolor, sit, amet, consectetur, adipiscing, } = elit; } // 当 `match_block_trailing_comma=false` fn main() { match lorem { Lorem::Ipsum => { println!("ipsum"); } Lorem::Dolor => println!("dolor"), } }
【rustfmt 配置】
此规则 Clippy 不可检测,由 rustfmt 自动格式化。
rustfmt 配置:
对应选项 | 可选值 | 是否 stable | 说明 |
---|---|---|---|
trailing_comma | "Vertical"(默认) | No | 当多个字段在不同行时,在最后一个字段结尾添加逗号,如果在同一行,则不加逗号 |
match_block_trailing_comma | false(默认) | No | 在match分支中,如果包含了块,则不需要加逗号,否则需要加 |
在 Rust 生态中被拒绝的一些默认开启的lint
来源:https://github.com/dtolnay/noisy-clippy
以下按字母顺序排列。
absurd_extreme_comparisons
https://rust-lang.github.io/rust-clippy/master/index.html#absurd_extreme_comparisons
【描述】
默认为 Deny
,但在实际应用中,多被设置为 allow
。
blacklisted_name
https://rust-lang.github.io/rust-clippy/master/index.html#blacklisted_name
【描述】
该 lint 不允许代码中出现 「内置黑名单」中定义的命名,比如 foo
、baz
。
默认为 Warn
,但在实际应用中,可能被设置为allow
,因为在某些样板代码、文档或测试代码中可能需要使用 foo
。
blanket_clippy_restriction_lints
https://rust-lang.github.io/rust-clippy/master/index.html#blanket_clippy_restriction_lints
【描述】
用于检查针对整个 clippy::restriction
类别的警告/拒绝/禁止属性。Restriction lint 有时与其他 lint 形成对比,甚至与惯用的 Rust 背道而驰。 这些 lint 应仅在逐个 lint 的基础上启用并仔细考虑。
默认为 suspicious/warn
,但实际有些项目中会将其设置为 allow
。
Cargo Udeps
cargo-udeps 检查 Cargo.toml
中未使用的依赖。
cargo udeps
对标的是 rustc
的 unused_crate_dependencies lint
虽然 rustc 也能检查一些未使用依赖,但是在 lib 和 bin 混合的项目中误报率高
RUSTFLAGS="-Dunused_crate_dependencies" cargo c
cargo udeps
的最大优点就是几乎没有误报。
但是检查力度不如rustc unused_crate_dependencies lint
仔细,建议二者搭配使用
F.Cheat Sheet
这里用于梳理 Rust 相关的 Cheat Sheet。
资源
Float Cheat Sheet
浮点数类型
Rust 有 IEEE 754 单精度 (32-bit) 和 双精度 (64-bit) 类型:
#![allow(unused)] fn main() { let x: f32 = 0.1; // 32-bit float let y: f64 = 0.1; // 64-bit float }
默认的浮点数类型是 f64
:
#![allow(unused)] fn main() { let z = 0.1; // 64-bit float }
Decimal Types
Rust 没有内建的 Decimal 类型,但是有第三方库 rust_decimal 来支持 Decimal 类型。该库实现了 128-bit 有限精度的(limited-precision) 关键字 Decimal
表示 Decimal 类型:
#![allow(unused)] fn main() { use rust_decimal::prelude::*; let a = Decimal::new(1, 1); // second param is the number of fractional digits let b = Decimal::new(2, 1); // a Decimal representing exactly 0.2 let c = a + b; // a Decimal representing exactly 0.3 }
如何四舍五入
生成字符串:
#![allow(unused)] fn main() { format!("{:.2}", 1.2399); // returns "1.24" format!("{:.3}", 1.2399); // returns "1.240" format!("{:.2}", 1.2); // returns "1.20" }
打印标准输出:
#![allow(unused)] fn main() { println!("{:.2}", 1.2399); // prints "1.24" }
这个 round
方法返回与数字最接近的整数。它使用 四舍五入模式(rounding mode) ,"从零开始四舍五入",并且对f32
和f64
类型都有效。
#![allow(unused)] fn main() { let f: f64 = 3.3; let g: f64 = -3.3; f.round(); // returns 3.0 g.round(); // returns -3.0 }
rust_decimal
crate 包含round_dp
方法,它使用Banker的舍入模式。
#![allow(unused)] fn main() { let pi = Decimal::from_str("3.1415926535897932384626433832").unwrap(); println!("{}", pi.round_dp(2).to_string()); // prints "3.14" }
rust_decimal
crate 还包含round_dp_with_strategy
方法,允许你指定一个四舍五入策略。
#![allow(unused)] fn main() { let i = Decimal::from_str("1.25").unwrap(); println!( "{}", i.round_dp_with_strategy(1, RoundingStrategy::RoundDown) .to_string() ) // prints "1.2" }
Resources
G.优化指南
内容介绍
- Rust 性能优化总则
- Rust 性能优化准备工作
- Rust 性能剖析工具介绍
- 日常 Rust 开发性能优化的技巧总结
- Rust 编译大小和编译时间优化技巧
本次分享将围绕 Rust 性能评估和调优主题,比较系统地介绍 Rust 代码的性能优化经验。先从大的总原则出发,介绍在编写 Rust 过程中应该遵循哪些原则对后续优化有帮助。接下来会分享一些代码优化的方法和技巧,然后介绍可以用于 Rust 代码性能评估的工具,也会包括 Rust专用的一些异步并发测试工具介绍。
引子
Rust 语言天生为并发和安全而设计,并且借鉴了面向过程/面向对象/函数式等语言的特点。Rust 的目标在性能方面对标 C 语言,但在安全和生产力方面则比 C 更胜一筹。
虽说 Rust 语言性能对标 C 语言,但开发者写出的Rust 代码如果不经任何优化,也有可能比 Python 更慢。导致 Rust 代码性能慢的因素有很多种,本文就是尝试来梳理这些情况,并且给出一套方法论和一些工具集,来帮助开发者编写高性能的 Rust 代码。
Rust 性能优化总则
原则一: 不要过早优化性能
过早优化(Premature Optimization)
Premature optimization is the root of all evil. -- DonaldKnuth
在 DonaldKnuth 的论文 《 Structured Programming With GoTo Statements 》中,他写道:"程序员浪费了大量的时间去考虑或担心程序中非关键部分的速度,而当考虑到调试和维护时,这些对效率的尝试实际上会产生强烈的负面影响。我们应该忘记这种微小的效率,比如说因为过早优化而浪费的大约97%的时间。然而,我们不应该放弃那关键的 3% 的机会"。
想把代码优化到最佳,需要花很多精力。不应该在开发的时候去想着优化的事情,不需要一步到位。先完成再完美。
但是并非所有优化过早。在编写代码的过程中,优化代码的可读性是你持续要做的。Rust 是一门讲究显式语义的语言,在命名上体现出类型的语义,对于提升可读性非常重要。
原则二: 不要过度优化性能
RustConf 2021 一个演讲就举了一个过度优化例子:
某个用户只是想写一些比 Python 程序性能更好的代码。第一版 Rust 实现的代码已经达到了这个要求,比 Python 代码快 20倍。但是他们花了九牛二虎之力写的第二个 Rust 版本,和第一个版本差距并不大。
性能够用就好,否则就容易浪费不必要的时间。
原则三: Rust 代码的性能、安全、编译速度和编译大小之间需要权衡
Rust 是同时注重安全和性能的语言。但是在优化性能的同时,是有可能损失安全性的。比如使用 Unsafe Rust 来提升性能,而忽略安全检查在某些调用环境比较安全的地方是允许的,但是并非通用的做法。所以在优化性能之前,要考虑是否要牺牲安全性。
另外 Rust 优化性能的同时,可能会导致编译速度变慢 和 编译文件大小膨胀。这也是需要权衡的地方。
Rust 优化准备工作
在性能优化之前,你还需要做一些准备工作,用于测量你的优化是否有效。
基准测试
第一步是建立一套一致的基准,可以用来确定性能的基线水平,并衡量任何渐进的改进。
参考:
mongodb
的案例中,标准化的MongoDB
驱动微基准集在这方面发挥了很好的作用,特别是因为它允许在用其他编程语言编写的MongoDB
驱动之间进行比较。由于这些是 "微 "基准,它们还可以很容易地测量单个组件的变化(例如,读与写),这在专注于在特定领域进行改进时是非常有用的。
一旦选择了基准,就应该建立一个稳定的环境,可以用来进行所有的定时测量。确保环境不发生变化,并且在分析时不做其他 "工作"(如浏览猫的图片),这对减少基准测量中的噪音很重要。
推荐工具:
使用 cargo bench 和 criterion
来进行基准测试
[dev-dependencies]
criterion = { version = "0.3.5", features = ["async_tokio", "html_reports"] }
[[bench]]
name = "find"
harness = false
因为 Rust 自带的基准测试只能用于Nightly Rust ,所以需要使用这个第三方库 criterion 在 Stable Rust 下进行基准测试。
Criterion 会将每次运行的时间记录、分析到一个 HTML 报告中。
在报告的底部,有两个最近的运行之间的比较,较早的运行(基线)为红色,最近的运行(优化的)为蓝色。这些报告是非常有用的工具,用于可视化由于性能调整而发生的变化,并且它们对于向其他人展示结果特别有用。
它们还可以作为过去性能数据的记录,无需手动记录结果。如果有性能回归的情况,也会得到及时的反映。
压力/负载测试
基准测试是开发过程中对程序性能的一种预判。而项目最终发布之后,还需要在实际环境对其进行真正的负载测试,来判断系统的延时和吞吐量。
常用的负载测试工具基本都可以使用,比如 locust,wrk之类。这里介绍一个 Rust 基金会成员公司的一个用 Rust 实现的开源分布式负载测试工具 :goose。
Goose 每 CPU 核产生的流量至少是 Locust 的 11 倍,对于更复杂的负载测试(例如使用第三方库抓取表单内容的负载测试),收益甚至更大。虽然 Locust 要求您管理分布式负载测试,只是为了在单个服务器上使用多个 CPU 内核,但 Goose 使用单个进程利用所有可用的 CPU 内核,从而大大简化了运行更大负载测试的过程。对代码库的持续改进继续带来新功能和更快的性能。Goose 的扩展性远远优于 Locust,可以有效地利用可用资源来实现其目标。它还支持异步流程,使更多的同步流程能够轻松且一致地从单个服务器上增加数千名用户。
Goose 拥有许多其他负载测试工具所没有的独特调试和日志记录机制,简化了负载测试的编写和结果的分析。Goose 还通过对数据的多个简单视图提供了更全面的指标,并且可以轻松地确认负载测试在您按比例放大或缩小时按照您的预期执行。它公开了用于分配任务和任务集的算法,对操作的顺序和一致性进行更精细的控制,这对于易于重复的测试很重要。
明白高性能系统的标准
在进行性能剖析之前,还应该明白高性能系统的一个标准。
性能 = 产出 / 资源消耗
产出 = 事务次数(比如,qps)和 吞吐的数据量
消耗资源 = cpu时间片,磁盘/网络 I/O 次数、流量 等
而高性能的系统是要求在固定资源消耗之下来提高产出。
对于高性能系统的设计一般遵循两个标准:
- 最大化地利用资源。
- 使用流水线技术减少程序中任务总耗时。比如 Rust 编译器优化编译时间,也使用了流水线技术来对crate进行并行编译。
常见瓶颈类型:
- CPU :
- CPU 占用过高,那么就需要减少计算的开销。
- CPU 负载过高,那么就需要查看是否线程过多,以及多个线程的切换太过频繁,多线程交互是否有必要。
- I/O:
- 磁盘 IOPS(Input/Output Operations Per Second) 达到了上限。那么需要减少读写次数,提高 cache命中率。
- IO 带宽(bandwidth) 上限。那么就需要减少磁盘的读写流量,比如使用更紧凑的数据存储格式,更小的读写放大(本来只需要读取100字节,结果触发了好多个page的读写,产生了放大的效果)。
- I/O 并发达到上限。那么就需要考虑使用 异步I/O。
- 锁、计时器、分页/交换等被阻塞。
Rust 性能剖析工具介绍
在做好准备工作之后,就可以开启我们的性能剖析工作了。
性能剖析,就是要发现程序中真正存在的性能瓶颈。而不是你自以为的想象中的性能瓶颈。如果不遵守这点,就会导致过早优化或过度优化。
因为常见的性能瓶颈一般都是两类,CPU 和 I/O 。所以工具也基本面向这两类。
On-CPU 性能剖析
使用 Perf 寻找“热点”
做cpu性能剖析有很多常用的 Linux 命令行工具,比如 linux 命令行工具 perf。它功能强大:它可以检测 CPU 性能计数器、跟踪点、kprobes 和 uprobes(动态跟踪)。
你可以使用 perf 工具对 CPU 进行采样分析。以一个指定的频率对CPU进行采样,进而拿到正在CPU上运行的指令乃至整个函数调用栈的快照,最后对采样的数据分析。比如说在100次采样中有20次在运行A指令或者A函数,那么perf
就会认为A函数的CPU使用率为20%。
可以在 Cargo.toml 中加入:
[profile.release]
debug = true
然后执行:
#![allow(unused)] fn main() { $ cargo build --release $ perf record -g target/release/perf-test $ perf report }
就可以看到报告了。
火焰图工具
但我们 Rust 程序中要通过flamegraph
crate,来生成 火焰图(flamegraph),它可以与cargo
一起工作,非常方便。
因为火焰图有助于阅读源码,它以可视化的图案非常明确地展示调用栈之间的关系。火焰图可以让开发者从整体上看出各个线程的开销比例和子函数占有的比例,指引我们从整体上找到优化的优先级。
火焰图中,在被测量的执行过程中调用的每个函数会被表示为一个矩形,每个调用栈被表示为一个矩形栈。一个给定的矩形的宽度与在该函数中花费的时间成正比,更宽的矩形意味着更多的时间。火焰图对于识别程序中的慢速部分非常有用,因为它们可以让你快速识别代码库中哪些部分花费的时间不成比例。
用 Mongodb 调优的示例来说:
火焰图中的栈从底部开始,随着调用栈的加深而向上移动(左右无所谓),通常这是开始阅读它们的最佳方式。看一下上面火焰图的底部,最宽的矩形是Future::poll
,但这并不是因为Rust 的 Future
超级慢,而是因为每个.await
都涉及轮询(poll)Future
。考虑到这一点,我们可以跳过任何轮询矩形,直到我们在mongodb
中看到我们关心的信息的函数。
蓝色方块包含了调用CommandResponse::body
所花费的时间,它显示几乎所有的时间都花在了clone()
上。各个紫色矩形对应的是将BSON
(MongoDB使用的二进制格式)解析到Document
中所花费的时间,绿色矩形对应的是Document
的serde::Deserialize
实现中所花费的时间。最后,黑色虚线矩形对应的是释放内存的时间,黑色实线对应的是将命令序列化为BSON
的时间。
所以从火焰图中反映出性能瓶颈在于:
- Clone 过多。
- 序列化 bson 耗费更多时间
修复完这些性能瓶颈之后,再使用基准测试测试一次。
如果可能的话,再使用 goose 这样的压测工具进行一次负载测试更好。
perf 适合测试 Rust 异步代码
对于异步 Rust 程序而言,火焰图的效果可能并不是很好,因为异步调度器和执行器几乎会出现在火焰图中每一块地方,看不出瓶颈所在。这个时候使用 perf 工具会更加清晰。
检查内存泄露和不必要的内存分配
可以使用 Valgrind 工具来检查程序是否存在内存泄露,或者在关键的调用路径上存在不必要的内存分配。
不仅仅要考察堆分配,也需要考虑栈上的分配,特别是异步操作时。
有一个非常有用的 Rust 编译标志(仅在 Rust nightly 中可用)来验证数据结构有多大及其缓存对齐。
#![allow(unused)] fn main() { $ RUSTFLAGS=-Zprint-type-sizes cargo build --release }
除了通常的 Cargo 输出之外,包括异步 Future 在内的每个数据结构都以相应的大小和缓存对齐方式打印出来。比如:
#![allow(unused)] fn main() { print-type-size type: `net::protocol::proto::msg::Data`: 304 bytes, alignment: 8 bytes print-type-size field `.key`: 40 bytes print-type-size field `.data_info`: 168 bytes print-type-size field `.payload`: 96 bytes }
Rust 异步编程非常依赖栈空间,异步运行时和库需要把所有东西放到栈上来保证执行的正确性。如果你的异步程序占用了过多的栈空间,可以考虑将其进行优化为 平衡的同步和异步代码组合,把特定的异步代码隔离出来也是一种优化手段。
其他性能剖析/监控工具
如果允许,可以使用 英特尔出品的 VTune 工具进行 CPU 性能剖析。
或者使用在线的性能监控平台,比如 Logrocket,支持 Rust 程序,可以监控应用程序的性能,报告客户端 CPU 负载、客户端内存使用等指标。
也可以使用开源的链路追踪工具来监控你自己的 Rust 项目:使用 OpenTelemetry 标准。OpenTelemetry 也支持 Rust 。
opentelemetry是一款数据收集中间件。我们可以使用它来生成,收集和导出监测数据(Metrics,Logs and traces),这些数据可供支持OpenTelemetry的中间件存储,查询和显示,用以实现数据观测,性能分析,系统监控,服务告警等能力。
PingCAP 也开源了一款高性能的 tracing 库 : minitrace-rust
Off-CPU 性能剖析
Off-CPU 是指在 I/O、锁、计时器、分页/交换等被阻塞的同时等待的时间。
Off-CPU 的性能剖析通常可以在程序运行过程中进行采用链路跟踪来进行分析。
还有就是使用 offcpu 火焰图进行可视化观察。
这里推荐的工具是 eBPF
的前端工具包bcc中的offcputime-bpfcc
工具。
这个工具的原理是在每一次内核调用finish_task_switch()
函数完成任务切换的时候记录上一个进程被调度离开CPU
的时间戳和当前进程被调度到CPU
的时间戳,那么一个进程离开CPU
到下一次进入CPU
的时间差即为Off-CPU
的时间。
比如这里一段代码:
use std::io::Read; fn test1() { std::thread::sleep(std::time::Duration::from_nanos(200)); } fn test2() { let mut f = std::fs::File::open("./1.txt").unwrap(); let mut buffer = Vec::new(); f.read_to_end(&mut buffer).unwrap(); } fn main() { loop { test1(); test2(); } }
程序中一共有两种会导致进程被调度出CPU
的任务,一个是test1()
函数中的sleep()
,一个是在test2()
函数中的读文件操作。
这里需要使用debug编译,因为offcputime-bpfcc
依赖于frame pointer
来进行栈展开,所以我们需要开启RUSTFLAGS="-C force-frame-pointers=yes"
的编译选项以便打印出用户态的函数栈。我们使用如下的命令获取Off-CPU
的分析数据。
#![allow(unused)] fn main() { $ ./target/debug/mytest & sudo offcputime-bpfcc -p `pgrep -nx mytest` 5 }
然后使用 火焰图工具将其生成 off-cpu 火焰图:
#![allow(unused)] fn main() { $ git clone https://github.com/brendangregg/FlameGraph $ cd FlameGraph $ sudo offcputime-bpfcc -df -p `pgrep -nx mytest` 3 > out.stacks $ ./flamegraph.pl --color=io --title="Off-CPU Time Flame Graph" --countname=us < out.stacks > out.svg }
得到下面火焰图:
与On-CPU
的火焰图相同,纵轴代表了函数调用栈,横轴代表了Off-CPU
时间的比例,跨度越大代表Off-CPU
的时间越长。
其他适合 Rust 性能剖析的工具介绍
除了 perf 和 火焰图 工具,下面还有一些 Rust 程序适用的工具。
- Hotspot和Firefox Profiler是查看perf记录的数据的好工具。
- Cachegrind和Callgrind给出了全局的、每个函数的、每个源线的指令数以及模拟的缓存和分支预测数据。
- DHAT可以很好的找到代码中哪些部分会造成大量的分配,并对峰值内存使用情况进行深入了解。
- heaptrack是另一个堆分析工具。
counts
支持即席(Ad Hoc)剖析,它将eprintln!
语句的使用与基于频率的后处理结合起来,这对于了解代码中特定领域的部分内容很有帮助。- Coz执行因果分析以衡量优化潜力。它通过coz-rs支持Rust。因果分析技术可以找到程序的瓶颈并显示对其进行优化的效果。
日常 Rust 开发性能优化技巧总结
虽然我们需要通过完善的性能测试方法来剖析系统中存在的瓶颈,保证不会过早优化和过度优化。但是在日常编码过程中,Rust 社区内也总结出来一些优化技巧来供参考:
1. 对于只被调用一次的函数可能并不需要进行优化。
比如读取配置文件,这种多慢都没有关系。
不要只优化程序中最慢的函数,要优化占用大部分运行时间的函数。
在一个被调用 1000 次的函数上得到 2 毫秒的改进,那比在一个被调用一次的函数上获得 1 秒的改进要好。
2. 优先改进你的算法
很多时候性能不佳,很可能是由于算法不佳而不是实现不佳。请检查你的代码中循环的使用,只需尝试尽可能少的循环。
- 记住每次使用
collect
必须至少会迭代整个集合一次,所以最好只 collect 一次。 - 警惕你使用的标准库方法和第三方库方法内部实现中隐藏的循环。
3. 要充分理解 Rust 中数据结构的内存布局
要学会区分 Rust 中数据类型的内存布局,它们在栈上和堆上如何分配的。
比如 String
,Vec
,HashMap
和Box<Trait>
/Box<[T]>
所有分配都在堆上。
在栈上分配的数据,移动的时候只能是 按位复制的方式。所以即便内存是在栈上分配,也要考虑这个 Copy 的成本。
堆上的数据,要尽可能地避免深拷贝(显式 Clone) 。
并且要尽可能地缓存数据,而避免频繁的内存分配发生。比如可以使用诸如 slab 之类的第三方库,可以合理复用内存。
4. 避免 Box<Trait>
动态分发
创建 trait 对象的规范方法是Box<Trait>
,但大多数代码都可以使用&mut Trait
,它也具有动态分派但节省了分配。如果您绝对需要所有权,请使用Box
,但大多数用例都可以使用&Trait
或&mut Trait
。
有些场景也可以使用 Enum 来代替 trait 对象。参见 enum_dispatch
。
5. 使用基于栈的可变长度数据类型
定长度的数据类型可以简单地存储在堆栈上,但对于动态大小的数据,它并不是那么简单。但是,smallvec
, smallstring
和tendril
都是可变长度数据类型,允许在栈上存储少量元素。像smallvec
这样的库非常适合缓存局部性,可以减少分配。
#![allow(unused)] fn main() { // This is a gross oversimplification of how this type is implemented in the// crate, but it's enough to explain how it works.enum SmallVec<T> { Small([T; 4]), Big(Vec<T>),}type Matrix<T> = SmallVec<SmallVec<T>>; }
6. 合理使用断言避免数组越界检查
Safe Rust 会被编译器自动塞入数组越界检查,比如下面代码:
#![allow(unused)] fn main() { fn do_something_with_array(array: &[u8]) -> u8 { array[0] + array[1] + array[2] + array[3] + array[4] + array[5]} }
可以通过编译输出 MIR 看到,编译器会给数组索引访问插入断言检查:
#![allow(unused)] fn main() { assert(move _9, "index out of bounds: the length is {} but the index is {}", move _8, _7) }
有几个数组索引访问就会被插入几次,上面的代码会被插入 6 次,这极大影响性能。
所以我们可以手工插入一次断言检查,就可以消除编译器的自动插入。
#![allow(unused)] fn main() { fn do_something_with_array(array: &[u8]) -> u8 { assert!(array.len >= 5); array[0] + array[1] + array[2] + array[3] + array[4] + array[5]} }
这一条也是可以举一反三的,比如 Rust 也会为普通的加法操作添加防止计算溢出的断言,但是你如何手工使用了 wrapped_add 之类的方法,那就可以避免编译器自动插入这类断言。
7. 使用链接时优化(LTO)
链接时优化允许编译器跨 crate 进行内联,但是这样做的代价是减慢编译时间。但我认为,编译时间如何能换取性能提升,那么这个时间值得牺牲。
8. 不要使用 #[inline(always)]
Rust 编译器自身的优化可以计算出何时需要内联一些函数,不需要你手工明确指定。除非这个函数调用十分频繁。
因为这种显式的指定会导致编译大小的膨胀,如果你的硬件资源不受限可能不太重要。但是对于资源受限的环境,比如嵌入式,则需要进行权衡。
对于一些小的函数,如果没有使用 LTO,但是需要跨 crate 内联的话,也可以显式指定 #[inline]
。
9. 避免显式 Clone
尽可能地使用引用,避免过多的 Clone 。因为Clone 可能伴随内存分配。
10. 使用 Unsafe 方法消除一些不必要的安全检查
在 Rust 标准库中,你可以看到很多 _unchecked
后缀的方法。
比如 String::from_utf8
和 String::from_utf8_unchecked
,是一对 Safe 和 Unsafe 的方法。
一般情况下,应该使用 String::from_utf8
将 u8
序列转换为合法的字符串,这个方法对 u8
序列进行了合法 utf8编码的检查。但是这个检查也会有一定开销。
如果开发者能确保调用环境的 u8
序列来源是完全合法的 utf8 编码,那么这个安全检查就完全可以忽略。此时就可以使用 String::from_utf8_unchecked
来替换 String::from_utf8
用来提升性能。
#![allow(unused)] fn main() { pub fn from_utf8(vec: Vec<u8>) -> Result<String, FromUtf8Error> { match str::from_utf8(&vec) { Ok(..) => Ok(String { vec }), Err(e) => Err(FromUtf8Error { bytes: vec, error: e }), }}pub unsafe fn from_utf8_unchecked(bytes: Vec<u8>) -> String { String { vec: bytes }} }
11. 并发/并行化你的程序
用 Rust 写多线程和异步并发程序是非常便利的。
推荐的库有很多:
12. 并发程序中,合理使用锁,或替换无锁数据结构
在某些场景中,可能读并发访问要比写并发更频繁,那么可以用 读写锁来替换互斥锁。另外,使用第三方库 parking_lot 中定义的并发锁来代替标准库中的锁。
或者合理选择无锁数据结构来替换用锁来同步的数据结构,并不是说无锁一定比锁同步性能更好,也是需要看场景和选择高质量的第三方实现。
13. 使用 Clippy
使用 Clippy 工具对代码进行静态分析,它可以针对性能改进提供一些建议。
关于 Clippy 性能改进 lint 可以在这里找到:https://rust-lang.github.io/rust-clippy/master/index.html
同样可以遵循 Rust 编码规范 中的一些规范,也会包括 Clippy 的一些建议。如果你有什么性能优化的小技巧,欢迎提交贡献。
编译大小和编译时间的优化总结
1. 优化编译大小
- 设置 codegen-units=1 ,codegen-units 叫做代码生成单元,Rust 编译器会把crate 生成的 LLVMIR进行分割,默认分割为16个单元,每个单元就叫 codegen-units,如果分割的太多,就不利于 Rust编译器使用内联优化一些函数调用,分割单元越大,才越容易判断需要内联的地方。但是这也有可能增大编译文件大小,需要大小和性能间寻找平衡。
- 设置panic=abort。可以缩减编译文件的大小。
- 设置编译优化等级为
z
,意为最小二进制体积。编译器的优化级别对应的是LLVM
函数内联的阈值,z
对应的是 25,而 级别3
则对应 275 。 - 评估代码中泛型和宏的使用,是否可以精简
- 其他参考:https://github.com/johnthagen/min-sized-rust
2. 优化编译大小的一些技巧
-
使用 cargo check 代替 cargo build
-
使用最新 Rust 工具链
-
使用 Rust Analyzer 而不是 Rust Language Server (RLS)
-
删除未使用的依赖项
-
替换依赖过多的第三方库
-
使用 workspace,将项目拆分为多个crate,方便并行编译
-
将针对模块的测试单独拆分为一个测试文件
-
将所有集成测试组合在一个文件中
-
禁止 crate 依赖未使用功能
-
使用 ssd或Ramdisk(虚拟内存盘) 进行编译
-
使用 sccache 缓存依赖项
-
切换到更快的链接器:mold (Linux)/ zld (MacOS) / 🤷 (Windows),可以使用以下命令检查链接所花时间:
#![allow(unused)] fn main() { cargo cleancargo +nightly rustc --bin <your_binary_name> -- -Z time-passes }
-
Rust 针对 MacOS 用户也提升了增量编译性能,在 Cargo.toml 中进行以下配置:
-
#![allow(unused)] fn main() { [profile.dev]split-debuginfo = "unpacked" }
-
调整更多 Codegen 选项/编译器标志。这是完整的 codegen 选项列表 。为了获得灵感,这里是bevy 的用于更快编译的配置。
-
剖析文件编译时间。使用
cargo rustc -- -Zself-profile
生成的跟踪文件可以使用火焰图或 Chromium 分析器进行可视化。还有一个cargo -Z timings
功能可以提供有关每个编译步骤需要多长时间的一些信息,并随着时间的推移跟踪并发信息。 -
避免过程宏 Crates,主要是因为使用了 syn 。过程宏是 Rust 开发的热点:它们会消耗 CPU 周期,因此请谨慎使用。serde 库中包含了过程宏,它在很多地方都用到,所以需要注意是否一定需要serde 进行序列化和反序列化。
-
避免过多的泛型。过多的泛型单态化也会导致编译时间增加。
-
提升你的硬件,或者在云端(比如Gitpod.io,可免费使用 16 核 Intel Xeon 2.80GHz,60GB RAM的主机)使用更好的硬件环境进行编译。
-
下载所有的依赖 crate。编译过程中有很大一部分时间用于下载,提前下载好crate是有帮助的。参考 https://github.com/the-lean-crate/criner
-
使用 docker 进行编译。
cargo-chef
可用于充分利用 Docker 层缓存,从而大大加快 Rust 项目的 Docker 构建。 -
超频 cpu ?谨慎。
-
优化 CI 构建速度。参考 https://matklad.github.io/2021/09/04/fast-rust-builds.html。
-
你自己开发 crate 的时候尽量保持精简,利人利己。
参考
-
https://bheisler.github.io/criterion.rs/book/getting_started.html
-
https://gist.github.com/jFransham/369a86eff00e5f280ed25121454acec1
-
https://rustmagazine.github.io/rust_magazine_2021/chapter_11/rust-profiling.html?search=
-
https://rustmagazine.github.io/rust_magazine_2021/chapter_7/paper-rust-vs-c.html
-
https://blues-star.github.io/perf-book-zh/benchmarking_zh.html
-
https://en.pingcap.com/blog/how-we-trace-a-kv-database-with-less-than-5-percent-performance-impact/
-
https://rust-coding-guidelines.github.io/rust-coding-guidelines-zh/
-
https://docs.rust-embedded.org/book/unsorted/speed-vs-size.html
-
https://fasterthanli.me/articles/why-is-my-rust-build-so-slow
H.Rust编译器编译参数说明
Rustc 说明
通过以下命令可以打印编译器相关参数选项:
#![allow(unused)] fn main() { $ rustc -h // 或 $ rustc --help // 在 Nightly Rust 下,比 Stable Rust 多一个 `-Z` 参数,用于传递 unstable compiler options }
参数功能
-h
/ --help
用于打印 rustc
的帮助信息
--cfg
用于打开或关闭 #[cfg]
变量中的各种条件编译设置。
#![allow(unused)] fn main() { --cfg 'verbose' // 对应 #[cfg(verbose)] // or --cfg 'feature="serde"' // 对应 #[cfg(feature = "serde")] }
-L
-L
用于将一个目录添加到外部搜索路径。命令格式如下:
#![allow(unused)] fn main() { -L [KIND=]PATH }
当 KIND
是以下情况之一时,可以使用 -L KIND=PATH
这种形式指定搜索路径的类型:
dependency
— 仅在该目录中搜索传递依赖项。crate
— 仅在该目录中搜索此 crate 的直接依赖项。native
— 仅在该目录中搜索本地库。framework
— 仅用于在该目录中搜索 macOS 框架。all
— 搜索此目录中的所有库类型。如果 KIND 没有指定,这将是默认值.
-l
用于将生成的 crate 链接到本地库。
#![allow(unused)] fn main() { -l [KIND[:MODIFIERS]=]NAME[:RENAME] }
当 KIND
是以下情况之一时,库的种类可以使用 -l KIND=lib
这种形式指定:
dylib
— 本地动态库static
— 本地静态库 (例如 .a archive 文件)framework
— macOS 框架
可以用#[link]
属性指定库的种类。 如果未在 link
属性或命令行中指定 KIND
,它将链接到可用动态库,否则将使用静态库。 如果在命令行中指定了库类型,其将会覆盖 link
属性指定的库类型。
link
属性中使用的名称可以使用形如 -l ATTR_NAME:LINK_NAME
形式覆盖,其中 ATTR_NAME
是 link
属性中的名称,LINK_NAME
是将要链接到的实际库的名称。
--crate-type
这将指示 rustc 以何种 crate type 去构建。该 Flag 接收逗号分隔的值列表,也可以多次指定。有效的 crate type 如下:
lib
— 编译器生成的首选库类型, 目前默认为 rlib。rlib
— Rust 静态库。staticlib
— 本地静态库。dylib
— Rust 动态库。cdylib
— 本地动态库。bin
— 可执行程序。proc-macro
— 生成格式化且编译器可加载的过程宏库。
可以使用 crate_type
属性来指定 crate 类型,注意 --crate-type
命令行的值会覆盖 crate_type 属性的值。更多细节可以参阅 reference 中的 链接章节。
--crate-name
用于指定正在构建的 crate 名称
--edition
用于指定编译器使用哪个 版次(edition),可选项 2015|2018|2021
。
--emit
#![allow(unused)] fn main() { --emit [asm|llvm-bc|llvm-ir|obj|metadata|link|dep-info|mir] }
该 Flag 控制编译器生成的输出文件的类型。其接收以逗号分隔的值列表,也可以多次指定。有效的生成类型有:
asm
— 生成在 crate 中的一个汇编代码文件。 默认的输出文件是 CRATE_NAME.s。dep-info
— 生成一个包含 Makefile 语法的文件,指示加载以生成 crate 的所有源文件。 默认输出文件是 CRATE_NAME.d。link
— 生成由 --crate-type 指定的 crates 。 默认输出文件取决于平台和 crate 类型。 如果未指定 --emit 这将是默认值。llvm-bc
— 生成一个包含 LLVM bitcode 的二进制文件。默认输出文件是 CRATE_NAME.bc。llvm-ir
— 生成一个包含 LLVM IR( LLVM 中间语言)的文件。默认的输出文件是 CRATE_NAME.ll。metadata
— 生成一个关于该 crate 的元数据的文件。 默认输出文件是 CRATE_NAME.rmeta。mir
— 生成一个包含 Rust 中级中间表示(即中级中间语言)的文件。默认输出文件名是CRATE_NAME.mir
。obj
— 生成一个本地对象文件,默认输出文件是CRATE_NAME.o
。
输出文件名可以用 -o flag
进行设置。使用 -C extra-filename
。Flag 可以添加文件名后缀。文件将被写入当前目录除非使用--out-dir flag
标签。 每一个生成类型也可以使用 KIND=PATH
的形式指定输出文件名,它优先于 -o
标签。
--print
用于打印有关编译器的各种信息。指定--print
标签通常会禁用 --emit
步骤且只打印请求的信息。打印的有效值类型为:
crate-name
— crate 的名称。file-names
— 文件名由 link 命令执行的种类所决定。sysroot
— 系统根目录路径,即.rustup
下的toolchains
文件夹。target-libdir
- 目标lib
文件夹路径(同上)。cfg
— 条件编译值列表。 了解更多条件编译值信息,请参阅 条件编译。target-list
— 已知目标列表。 可以使用--target
标签选择目标。target-cpus
— 当前目标的可用 CPU 值列表。可以使用-C target-cpu=val
Flag 选择目标。target-features
— 当前目标的可用 目标features
列表。目标features
可以使用-C target-feature=val
flag 启用。该Flag是不安全( unsafe )的。relocation-models
— 重定位模型列表。重定位模型可以用-C relocation-model=val
Flag 选择。code-models
— 代码模型列表。代码模型可以用-C code-model=val
flag 进行设置。tls-models
— 支持的线程本地存储模型列表。 模型可以用-Z tls-model=val
Flag 来选择(仅限 Nightly Rust)。native-static-libs
— 当创建一个staticlib crate
类型时可以使用此选项。 如果这是唯一的标志,它将执行一个完整的编译,并包含一个指出链接生成静态库时要使用的链接器 Flag 的诊断说明。该说明以文本native-static-libs:
开始,以便更容易获取输出信息。
-g
等价于 -C debuginfo=2
,用于输出调试信息。
调试信息说明:
0
:根本没有调试信息(默认)。1
: 仅行表。2
:完整的调试信息。
-O
(大写的字母o,优化)
等价于 -C opt-level=2
,用于编译优化,level为2。
优化级别说明:
0
:没有优化,也打开cfg(debug_assertions)
(默认)。1
: 基本优化。2
: 一些优化。3
: 所有优化。s
: 优化二进制大小。z
:优化二进制大小,但也关闭循环向量化。
默认值为0
.
-o
用于控制输出的文件名。
--out-dir DIR
用于指定输出目录位置。
--explain OPT
用于提供错误消息的详细说明。rustc
对于每一个(检测到的)错误都会返回一个错误码,这将打印给定错误的更详细说明。
--test
构建测试工具。
--target TARGET
指定编译的 target triple
。
设置 lint
-W
等价于 --warn LINT
,设置给定 lint 为 warning 级别。
-A
等价于 --allow LINT
,设置给定 lint 为 allow 级别。
-D
等价于 --deny LINT
,设置给定 lint 为 deny 级别。
-F
等价于 --forbid LINT
,设置给定 lint 为 forbid 级别。
-C
用于设置 代码生成 选项。可以通过 rustc -C help
进一步查看其选项。
已弃用参数
ar=val
,废弃。no-stack-check=val
,废弃。
code-model
(优化相关)
code-model=val
,支持的值为:
tiny
- 微小的代码模型。small
- 小代码模型。这是大多数受支持目标的默认模型。kernel
- 内核代码模型。medium
- 中等代码模型。large
- 大型代码模型。比如 x86 平台上,告诉编译器不要进行任何假设,使用64位绝对取址模型访问代码及数据。
可以通过命令 rustc --print code-models
查看。
代码模型对程序及其符号可能使用的地址范围施加了限制。对于较小的地址范围,机器指令可能能够使用更紧凑的寻址模式。具体范围取决于目标架构和可用的寻址模式。
代码模型是程序员与编译器间的一个正式的协议,其中程序员表达其对最终程序将进入的当前正在编译的目标文件大小的意愿。比如,“不要担心,这个对象只会进入不那么大的程序,因此你可以使用快速的RIP相对取址模式”,相反,“这个对象期望链接进巨大的程序,因此请使用慢但安全的,带有完整64位偏移的绝对取址模式”。
codegen-units
(优化: 性能 vs 编译速度)
codegen-units=val
,这个值表示控制 crate 分成多少个代码生成单元(codegen units),它需要一个大于 0 的整数。
当一个 crate 被拆分为多个代码生成单元时,LLVM 能够并行处理它们。增加并行度可能会加快编译时间,但也可能会产生更慢的代码,因为可能会影响内联(inline)优化。
将此设置为 1
可能会提高生成代码的性能,但编译速度可能会变慢。
如果未指定,则默认值为 16,用于非增量构建。对于增量构建,默认值为 256,这允许缓存更细粒度。
control-flow-guard
(安全)
此Flag 控制 LLVM 是否启用 Windows 控制流保护(Control Flow Guard) 平台安全功能。对于非 Windows 目标,此标志当前被忽略。它采用以下值之一:
y
,yes
,on
,checks
, 或无值:表示启用控制流防护。nochecks
:在没有运行时强制检查的情况下发出控制流保护元数据(这应该只用于测试目的,因为它不提供安全强制)。n
,no
,off
: 不启用控制流保护(默认)。
关于 Rust 和 CFG 的更详细说明参考:
debug-assertions
debug-assertions=val
,用于打开或关闭 cfg(debug_assertions)
条件编译。采用以下的值:
y
,yes
,on
, 或无值:启用调试断言。n
,no
, oroff
: 禁用调试断言。
如果无指定,调试断言会在 opt-level 为 0 的优化级别下默认开启。所以这里需要注意优化级别,debug_assertions 在 release 编译时最好不要开启。这个和 debug_assert!
宏相关。
default-linker-libraries
default-linker-libraries=val
,用于设置链接器是否可以链接它的默认库,可以设置的值为:
y
,yes
,on
, 或无值:包括默认库(默认)。n
,no
, oroff
: 排除默认库。
embed-bitcode
(优化:编译大小 && 编译时间)
embed-bitcode=val
,控制编译器是否将 LLVM 位码嵌入到目标文件中。它采用以下值之一:
y
,yes
,on
, 或无值:将位码放入 rlibs(默认)。n
,no
, oroff
: 从 rlibs 中省略位码。
rustc
执行链接时优化 (LTO) 时需要 LLVM 位码(bitcode)。嵌入的位码将出现在 rustc 生成的目标文件中,该文件的名称由目标平台定义。大多数时候是这样 .llvmbc
。
如果你的编译实际不需要位码,使用-C embed-bitcode=no
可以显著提高编译时间并减少生成的文件大小(例如,如果你不为iOS编译或者你不执行LTO)。由于这些原因,Cargo尽可能地使用-C embed-bitcode=no
。同样地,如果你直接用 rustc 构建,我们建议在不使用 LTO 时使用 -C embed-bitcode=no
。
如果与-C lto
结合,-C embed-bitcode=no
将导致rustc
在启动时中止,因为这种结合是无效的。
bitcode是由LLVM引入的一种中间代码(Intermediate Representation,简称IR),它是源代码被编译为二进制机器码过程中的中间表示形态,它既不是源代码,也不是机器码。从代码组织结构上看它比较接近机器码,但是在函数和指令层面使用了很多高级语言的特性。LLVM 优化器负责对bitcode进行各种类型的优化,将bitcode代码进行一些逻辑等价的转换,使得代码的执行效率更高,体积更小,比如DeadStrip/SimplifyCFG,LLVM 后端负责把优化后的bitcode编译为指定目标架构的机器码。
来源:https://xelz.info/blog/2018/11/24/all-you-need-to-know-about-bitcode/
force-frame-pointers
(优化:性能)
force-frame-pointers=val
,用于设置是否强制启用 帧指针(frame-pointers)。相当于 Clang 的-fno-omit-frame-pointer
,
它采用以下值之一:
y
,yes
,on
, 或无值:强制启用帧指针。n
,no
, oroff
: 不要强制启用帧指针。这并不一定意味着将删除帧指针。
如果未强制启用帧指针,则默认行为取决于 target。
一般情况下,如果设置 force-frame-pointers=no
是一种帧指针省略优化。它造成的弊大于利,默认情况下不应启用。
force-unwind-tables
(优化:编译大小)
force-unwind-tables=val
,强制生成 unwind 表。它采用以下值之一:
y
,yes
,on
, 或无值:强制生成 unwind 表。n
,no
, oroff
: 不强制生成 unwind 表。如果目标需要 unwind 表,则会发出错误。
如果未指定,则默认值取决于 target 。
打开 force-unwind-tables=on
可能会导致二进制编译大小膨胀,对于移动和嵌入式这种二进制大小很重要的场景下,建议启用该选项。
incremental
(优化:编译时间)
incremental=val
,用于启用增量编译。
inline-threshold
(优化:性能)
inline-threshold=val
, 允许您设置内联函数的默认阈值。它接受一个无符号整数作为值。内联基于成本模型(cost model),其中较高的阈值将允许更多内联。
默认值取决于opt-level:
选择级别 | 临界点 |
---|---|
0 | 不适用,仅内联始终内联函数 |
1 | 不适用,仅内联始终内联函数和 LLVM 生命周期内在函数 |
2 | 225 |
3 | 275 |
s | 75 |
z | 25 |
链接相关参数
link-arg=val
,将单个额外参数附加到链接器调用。link-args=val
,将多个额外参数附加到链接器调用。选项应该用空格分隔。link-dead-code=val
,控制链接器是否保留死代码,尝试构建代码覆盖率指标时,此标志可能有用。它采用以下值之一:y
,yes
,on
, 或无值:保留死代码。n
,no
, oroff
: 删除死代码(默认)。
link-self-contained=val
,控制链接器是使用 Rust 附带的库和对象还是系统中的库和对象。linker=val
,控制链接器rustc
调用哪个链接器来链接您的代码。它采用链接器可执行文件的路径。如果未指定此标志,则将根据目标推断链接器。选择特定的链接器,有助于优化编译时间。linker-flavor=val
,链接器带有-C linker
标志,则链接器风格是从提供的值中推断出来的。如果没有给出链接器,则使用链接器风格来确定要使用的链接器。每个rustc
目标都默认为某种链接器风格。选项参考:linker-flavor 。linker-plugin-lto=val
,允许将 LTO 优化推迟到实际的链接步骤,如果所有被链接的目标文件都是由基于 LLVM 的工具链创建的,那么这反过来又允许跨编程语言边界执行过程间优化。详细介绍参见Linker-plugin-LTO。它采用以下值之一:y
,yes
,on
, 或无值:启用链接器插件 LTO。n
,no
, oroff
: 禁用链接器插件 LTO(默认)。- 链接器插件的路径。
lto
(优化:性能)
lto=val
,这个标志控制LLVM是否使用链接时优化(link-time optimizations),以产生更好的优化代码,使用整个程序分析,代价是延长链接时间,所以,会减慢编译时间。它取以下值之一:
y
,yes
,on
,fat
, orno
值:执行 "fat "LTO,试图在依赖图中的所有crate中进行优化。n
,no
,of
f: 禁用LTO。thin
:执行 "Thin "LTO。这与 "fat "类似,但运行时间大大减少,同时仍能实现与 "fat "类似的性能提升。
如果没有指定-C lto
,那么编译器将尝试执行 "thin local LTO",它只在本地的crate上对其代码单元执行 "thin "LTO。当没有指定-C lto
时,如果代码单元是1
或者优化被禁用(-C opt-level=0
),LTO将被禁用。
即:
- 当没有指定
-C lto
时:codegen-units=1
: 禁用LTOopt-level=0
: 禁用LTO。
- 当
-C lto
被指定时:lto
:16个代码生成单元,在整个crate 中执行 fat LTO。codegen-units=1
+lto
:1
个编码生成单元,跨 crate 进行fat LTO。
跨语言的LTO 参见Linker-plugin-LTO 。
passes
(优化:性能)
passes=val
,设置额外的 LLVM passes 列表,使用空格分隔。
LLVM Pass 是LLVM代码优化(optimization)中的一个重要组成部分。为便于理解,可以将Pass看作一个又一个的模块,各个Pass可以通过IR获取信息为下一个Pass做好准备,又或者直接对中间代码进行优化。
总的来说,所有的pass大致可以分为两类:
- 分析(
analysis
)和转换分析类的pass以提供信息为主- 转换类(
transform
)的pass优化中间代码详细参考:代码优化与LLVM IR pass
相关选项:no-prepopulate-passes=val
,使用空的 Pass 列表。
no-redzone
no-redzone=val
,允许禁用红区。它采用以下值之一:
y
,yes
,on
, 或无值:禁用红色区域。n
,no
, 或off
: 启用红色区域。
红区(redzone)是System V ABI提供的一种优化的产物,它允许函数无需调整栈指针(stack pointer),便能临时使用其栈帧(stack frame)下方的128个字节。红区被定义为调整过的栈指针下方128个字节的区域——函数将会使用这个区域,来存放一些无需跨越函数调用的临时数据。因此,在一些情况下,比如在小的叶函数(leaf function)[1]中,我们可以优化掉调整栈指针所需的两条指令。(
叶函数(leaf function)指的是不调用其它函数的函数;可以理解为,是函数调用图的叶子节点。特别地,尾递归函数(tail recursive function)的尾部可以看作是叶函数。
然而,当异常(exception)或硬件中断(hardware interrupt)发生时,这种优化却可能产生严重的问题。
no-vectorize-loops
(优化:性能)
no-vectorize-loops=val
,禁用循环矢量化(loop vectorization optimization passes)。等价于 Clang 的 -fno-vectorize
。
LLVM Loop Vectorizer 具有许多功能,可以对复杂的循环进行矢量化(向量化即“批量操作”,数据并行)。循环矢量化器使用成本模型来决定最佳矢量化因子和展开因子。但是,矢量化器的用户可以强制矢量化器使用特定值。许多循环无法向量化,包括具有复杂控制流、不可向量化类型和不可向量化调用的循环。对于更复杂的情况,我们则需要手动进行SIMD编程。
是否禁用,取决于你的使用场景。
相关:no-vectorize-slp=val
,禁用 SLP 向量化。
overflow-checks
(安全)
overflow-checks=val
,控制运行时整数溢出检查,启用溢出检查时,溢出时会发生恐慌。取值为:
采用以下值之一:
y
,yes
,on
, 或无值:启用溢出检查。n
,no
, oroff
: 禁用溢出检查。
如果未指定,则在启用 调试断言时启用溢出检查,否则禁用。
panic
控制代码恐慌时发生的情况:
abort
: 恐慌时终止进程,不能执行析构函数。unwind
: 恐慌时展开堆栈,可以执行析构函数,以及 catch_unwind 。
如果未指定,则默认值取决于目标。
prefer-dynamic
prefer-dynamic=val
,默认情况下,rustc
更喜欢静态链接依赖项。如果库的静态和动态版本都可用,此选项将指示应尽可能使用动态链接。有一个内部算法用于确定是否可以静态或动态地与依赖项链接。例如,cdylib
crate类型可能只使用静态链接。此标志采用以下值之一:
y
,yes
,on
, 或无值:使用动态链接。n
,no
, oroff
: 使用静态链接(默认)。
profile-generate
(优化)
profile-generate=val
,允许创建检测二进制文件,这些二进制文件将收集分析数据以用于 Profile Guided Optimization (PGO)。该Flag 采用一个可选参数,该参数是已检测二进制文件将向其中发出收集的数据的目录的路径。有关详细信息,请参阅 profile-guided optimization 。
相关:profile-use=val
。
split-debuginfo
(调试)
split-debuginfo=val
,控制rustc
生成的调试信息的“split debuginfo”的发射。此选项的默认行为是特定于平台的,并非此选项的所有可能值都适用于所有平台。可能的值为:
off
- 这是具有 ELF 二进制文件和 windows-gnu(不是 Windows MSVC 和 macOS)的平台的默认设置。这通常意味着可以在可执行文件部分的最终工件中找到 DWARF 调试信息。Windows MSVC 不支持此选项。在 macOS 上,此选项可防止最终执行dsymutil
生成调试信息。packed
- 这是 Windows MSVC 和 macOS 的默认设置。这里的“packed”一词意味着所有调试信息都打包到与主可执行文件不同的文件中。在 Windows MSVC 上这是一个*.pdb
文件,在 macOS 上这是一个*.dSYM
文件夹,而在其他平台上这是一个*.dwp
文件。unpacked
- 这意味着调试信息将在每个编译单元(目标文件)的单独文件中找到。Windows MSVC 不支持此功能。在 macOS 上,这意味着原始目标文件将包含调试信息。在其他 Unix 平台上,这意味着*.dwo
文件将包含调试信息。
请注意,此时packed
和unpacked
在-Z unstable-options
非 macOS 平台上被关闭。
strip
(优化:编译大小)
strip=val
,控制在链接期间从二进制文件中剥离调试信息和类似的辅助数据。可用于减少编译文件大小。
此选项支持的值为:
none
- 根据目标将调试信息和符号(如果存在)复制到生成的二进制文件或单独的文件中(例如.pdb
,MSVC 中的文件)。debuginfo
- debuginfo 部分和符号表部分中的 debuginfo 符号在链接时被剥离,并且不会复制到生成的二进制文件或单独的文件中。symbols
- 与 相同debuginfo
,但如果链接器支持,符号表部分的其余部分也会被剥离。
-Z
-Z
Flag 只允许在 Nightly Rust 下使用,因为它包含了 未稳定的编译器选项。
在该 Flag 下面有许多未稳定的子参数,这里就不一一列举。只挑选几个和安全相关的展示一下:
stack-protector=val
,用于控制栈粉碎保护策略,用于缓冲溢出保护。通过命令rustc --print stack-protector-strategies
可以看到详细设置值。sanitizer=val
,sanitizers(CFI 除外)的实现几乎完全依赖于 LLVM,将来增加 GCC 后端应该可以多一些支持。用于支持 内存错误检测器(AddressSanitizer)、LLVM 控制流完整性 (CFI) 提供前沿控制流保护(ControlFlowIntegrity )、运行时内存泄漏检测器(LeakSanitizer)、未初始化读取的检测器(MemorySanitizer)、数据竞争检测器(ThreadSanitizer)等。
参考
https://doc.rust-lang.org/rustc/command-line-arguments.html
https://doc.rust-lang.org/rustc/codegen-options/index.html
I.最佳实践
列表
初学者常见问题Q&A
Rust 编程技巧
设计模式之构建者模式
当需要很多构造函数,或构造含有很多可选配置项时,宜使用构建者模式
【描述】
Rust 中没有默认的构造函数,都是自定义构造函数。
如果需要多个构造函数,或者构造时需要很多可选配置的复杂场景,那么构建者模式是适合你的选择。
【示例】
#![allow(unused)] fn main() { #[derive(Debug, PartialEq)] pub struct Foo { // Lots of complicated fields. bar: String, } impl Foo { // This method will help users to discover the builder pub fn builder() -> FooBuilder { FooBuilder::default() } } #[derive(Default)] pub struct FooBuilder { // Probably lots of optional fields. bar: String, } impl FooBuilder { pub fn new(/* ... */) -> FooBuilder { // Set the minimally required fields of Foo. FooBuilder { bar: String::from("X"), } } pub fn name(mut self, bar: String) -> FooBuilder { // Set the name on the builder itself, and return the builder by value. self.bar = bar; self } // If we can get away with not consuming the Builder here, that is an // advantage. It means we can use the FooBuilder as a template for constructing // many Foos. pub fn build(self) -> Foo { // Create a Foo from the FooBuilder, applying all settings in FooBuilder // to Foo. Foo { bar: self.bar } } } #[test] fn builder_test() { let foo = Foo { bar: String::from("Y"), }; let foo_from_builder: Foo = FooBuilder::new().name(String::from("Y")).build(); assert_eq!(foo, foo_from_builder); } }
善用标准库中提供的迭代器适配器方法来满足自己的需求
【描述】
Rust 标准库中提供了很多迭代器方法,要学会使用它们,选择合适的方法来满足自己的需求。
下面示例中,反例中的迭代器适配器方法,都可以用对应的正例中的方法代替。
【反例】
#![allow(unused)] fn main() { // explicit_counter_loop let v = vec![1]; fn bar(bar: usize, baz: usize) {} let mut i = 0; for item in &v { bar(i, *item); i += 1; } // filter_map_identity let iter = vec![Some(1)].into_iter(); iter.filter_map(|x| x); // filter_next let vec = vec![1]; vec.iter().filter(|x| **x == 0).next(); // flat_map_identity let iter = vec![vec![0]].into_iter(); iter.flat_map(|x| x); // flat_map_option let nums: Vec<i32> = ["1", "2", "whee!"].iter().flat_map(|x| x.parse().ok()).collect(); }
【正例】
#![allow(unused)] fn main() { // explicit_counter_loop let v = vec![1]; fn bar(bar: usize, baz: usize) {} for (i, item) in v.iter().enumerate() { bar(i, *item); } // filter_map_identity let iter = vec![Some(1)].into_iter(); iter.flatten(); // filter_next let vec = vec![1]; vec.iter().find(|x| **x == 0); // flat_map_identity let iter = vec![vec![0]].into_iter(); iter.flatten(); // flat_map_option let nums: Vec<i32> = ["1", "2", "whee!"].iter().filter_map(|x| x.parse().ok()).collect(); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
explicit_counter_loop | yes | no | complexity | warn |
filter_map_identity | yes | no | complexity | warn |
filter_next | yes | no | complexity | warn |
flat_map_identity | yes | no | complexity | warn |
flat_map_option | yes | no | pedantic | allow |
可以使用Cow<str>
来代替直接使用字符串,它可以减少Copy
【描述】
使用 Cow<str>
作为字符串处理函数参数和返回值,可以尽可能地减少数据Copy 和 内存分配。当字符串没有修改的时候,实际使用的是 &'a str
,只有当数据修改的时候才会使用String
。对于读操作大于写操作的场景,使用 Cow<str>
比较合适。
【示例】
#![allow(unused)] fn main() { // 对输入的字符串进行转义 pub fn naive<'a, S: Into<Cow<'a, str>>>(input: S) -> Cow<'a, str> { let input = input.into(); fn is_trouble(c: char) -> bool { c == '<' || c == '>' || c == '&' } if input.contains(is_trouble) { let mut output = String::with_capacity(input.len()); for c in input.chars() { match c { '<' => output.push_str("<"), '>' => output.push_str(">"), '&' => output.push_str("&"), _ => output.push(c) } } // 只有在字符串修改的时候才使用 String Cow::Owned(output) } else { //其他情况使用 &str input } } }
错误处理:根据应用还是库来选择不同的错误处理方式
【描述】
如果编写应用,建议使用 Error
trait对象;如果编写库,则建议返回自定义错误类型,方便下游处理
【正例】
#![allow(unused)] fn main() { // 对于应用使用 Error trait 对象更加方便 pub fn print(&self, languages: &Languages) -> Result<String, Box<dyn Error>> { // do something } // 对于库,暴露自定义错误类型更加方便下游处理错误 #[derive(Debug)] pub struct SendError<T>(pub T); impl<T> fmt::Display for SendError<T> { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { write!(fmt, "channel closed") } } }
嵌入式(no-std): 将一些公用的类型、函数、宏等集中到一个自定义的 baremetal-std
【描述】
虽然 no-std
下不能用Rust 的标准库,但是可以自定义 no-std
下的标准库 baremetal-std
,用于积累 no-std
下常用的公共库。
J.贡献说明
欢迎直接提交 Issues 或 PR 直接参与评审和完善(包括精简、增补新的规则)编码规范。
K.淘汰的规则
常量
G.CNS.03 不宜将量大的数据结构定义为常量
淘汰原因
虽然常量本质上是会内联,但Rust 支持类似于复制消除(Copy Elision)的优化(非强制),而且在不断改进中,对于这种大的数据应该会有相关优化。这里建议用静态变量来代替常量,也许会引入使用的复杂性,所以这条规则被淘汰。只保留对固定长度数组相关的规则。
相应修改:[G.TYP.ARR.01] 的描述也有相对修改。
【级别】 建议
【描述】
因为常量会到处内联,即复制到各个使用到它的地方。而静态变量不会内联,它是全局的且有一个引用地址。 所以当要创建一个很大的常量数组时,应该考虑将其换成静态变量以提高程序运行效率。(详情可见:const-vs-static)
相关:[G.TYP.Array.01 ]
【反例】
fn main() { const MONTHS: [&str; 12] = ["January", "Feburary", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; }
【正例】
fn main() { static MONTHS: [&str; 12] = ["January", "Feburary", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | 是否可定制 |
---|---|---|---|---|
_ | no | no | _ | yes |
【定制化参考】
这条规则如果需要定制Lint,则需要找出每个定义的常量再判断其空间占用,或可直接排除基础类型以外的数据类型。
变量
P.VAR.02 禁止将局部变量的引用返回函数外
淘汰原因
Rust 编译器可以检测到这种情况,之前考虑到编译器错误比较晦涩,列出该规则,但是进一步考虑到这个应该是 Rust 开发者最基本的认知,顾淘汰此规则。
【描述】
局部变量生命周期始于其声明终于其作用域结束。如果在其生命周期之外被引用,则程序的行为是未定义的。当然,Rust 编译器也会阻止你这么干。
*注: Rust 编译器可以检测到这种情况,但是编译器错误比较晦涩,本原则用来提示开发者注意这种情况。 *
【反例】
fn makestr() -> &String { let a = "test".to_string(); &a } pub fn main() { let a = makestr(); }
G.VAR.01 交换两个变量的值应使用 swap
而非赋值
淘汰原因
该条规则属于教程向,这应该是 Rust 开发者的基本认知。也不应该引导开发者使用swap。
【级别】 建议
【描述】
对于包含 swap
方法的类型,如 ptr
、slice
、Cell
、RefCell
、VecDeque
等建议使用该类型的 swap
方法进行交换。
对其他类型可以使用函数 std::mem::swap
进行变量值的交换。
【反例】
#![allow(unused)] fn main() { let mut a = 1; let mut b = 2; let mut c = 0; // 辅助交换的变量 c = a; a = b; b = c; }
【正例】
#![allow(unused)] fn main() { let mut a = 1; let mut b = 2; std::mem::swap(&mut a, &mut b); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | 是否可定制 |
---|---|---|---|---|
_ | no | no | _ | yes |
【定制化参考】
这条规则如果需要定制Lint,则可以检测变量赋值操作,识别交换语义,推荐用户使用 swap
函数。
数据类型
P.TYP.01 类型转换要尽量使用安全的方式
淘汰原因
和 G.TYP.01 有点重复。
【描述】
Rust 中的类型转换有多种方式,其中 as
强转、Unsafe 的 std::mem::transmute
为不安全转换。From/Into
安全转换函数为安全转换。在使用类型转换时,应优先使用安全转换方式。
【反例】
#![allow(unused)] fn main() { // 产生数据截断 let a = i32::MAX; let b = a as u16; println!("{}, {}", a, b); // 2147483647, 65535 // 产生精度损失 let a = std::f64::consts::PI; let b = a as f32; println!("{}, {}", a, b); // 3.141592653589793, 3.1415927 // 结果不正确 let a: f64 = 123456.0; let b: i64 = unsafe { std::mem::transmute(a) }; println!("{}, {}", a, b); // 123456, 4683220244930494464 }
【正例】
#![allow(unused)] fn main() { let a: f32 = 123456.0; let b: f64 = a.try_into().expect("trans failed"); println!("{}, {}", a, b); // 123456, 123456 }
P.TYP.02 对数组和集合容器进行索引要使用 usize
类型
淘汰原因
这属于 Rust 开发者必备基本认知,有点偏教程向,故淘汰。
【描述】
Rust 中只允许索引为 usize
类型,因为:
- 负索引是无意义的。
usize
和 裸指针大小相同,意味着指针算法不会有任何隐藏的强制转换std::mem::size_of()
和std::mem::align_of()
的函数返回usize
类型。usize
不会因为平台架构的切换而导致索引值被截断的问题,比如 将u32
类型的索引 用到 16位大小的嵌入式平台就会出问题。
G.TYP.UNT.01 当函数不关心返回值但要处理错误时应使用单元类型
淘汰原因
这属于 Rust 开发者必备基本认知,有点偏教程向,故淘汰。
【级别】 建议
【定制化参考】
可以检测使用 Option<T>
包含 Error
类型的情况,发出警告。
【描述】
单元类型代表 无返回值
。当返回类型无返回值但要处理错误时,应使用Result<(), Error>类型,
而非Option类型。
【反例】
#![allow(unused)] fn main() { fn f() -> Option<Error> { // ... None } }
【正例】
#![allow(unused)] fn main() { // 表示该函数要么成功,要么返回各自错误 fn f() -> Result<(), Error> { // ... // Error handle Ok(()) } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | 是否可定制 |
---|---|---|---|---|
_ | no | no | _ | yes |
G.TYP.INT.02 对于大整数字面量宜使用十六进制表示
淘汰原因
这个不做限制了,因人而异。
【级别】 建议
【描述】
略
【反例】
#![allow(unused)] fn main() { let a = `255` let b = `65_535` let c =`4_042_322_160` }
【正例】
#![allow(unused)] fn main() { let a = `0xFF` let b = `0xFFFF` let c = `0xF0F0_F0F0 }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
decimal_literal_representation | yes | no | restriction | allow |
P.TYP.REF.01 使用引用的时候要注意其生命周期不要重合
淘汰原因
偏教程向,这应该是 Rust 开发者基本认知,而且编译器会报错。
【描述】
在使用 引用的时候,要注意分析其生命周期,不可变借用和可变借用之间,以及可变借用之间不要有重叠。
【反例】
fn main(){ let mut s = String::from("hello"); // r1 是不可变借用,其生命周期和 可变借用 r3 重叠,所以会出问题 let r1 = &s; // no problem ---------------- lifetime r1 start let r2 = &mut s; // no problem let r3 = &mut s; // BIG PROBLEM -------------- lifetime r3 start println!("{}, {}, and {}", r1, r2, r3); // lifetime r1, r2, r3 end; }
【正例】
fn main(){ let mut s = String::from("hello"); let r1 = &s; // no problem let r2 = &mut s; // no problem let r3 = &mut s; // no PROBLEM // println!("{}, {}, and {}", r1, r2, r3); }
P.TYP.TUP.01 宜使用元组解构来同时定义多个变量
淘汰原因
偏教程向,这应该是 Rust 开发者基本认知。
【描述】
可以利用元组解构的特性,来更好地精简代码。
【示例】
struct A(i32, i32); fn hello( A(a, b): A){ println!("{}, {}", a, b); } fn main(){ let a = A(1, 2) ; hello(a); }
P.TYP.ARR.01 当数组长度在编译期就已经确定,应优先使用固定长度数组,而非动态数组( Vec<T>
)
淘汰原因
偏教程向,这应该是 Rust 开发者基本认知。
【描述】
固定长度数组会根据元素类型,优先选择存储在栈上,可以优化内存分配。当然,过大的数组可以酌情考虑放到堆内存,这个依据具体场景来决定。
当编译期长度可以确定,但长度并不是唯一确定的,那么可以考虑使用常量泛型。注意:常量泛型特性从 Rust 1.51版稳定。
【示例】
#![allow(unused)] fn main() { pub struct Grid { array: [u32; 5], width: usize, height: usize, } }
常量泛型:
pub struct Grid<T, const W: usize, const H: usize> where { array: [[T; W]; H], } impl<T, const W: usize, const H: usize> Default for Grid<T, W, H> where T: Default + Copy, { fn default() -> Self { Self { array: [[T::default(); W ]; H], } } } const WIDTH: usize = 300; const HEIGHT: usize = 200; fn main(){ let _g = Grid::<usize, 3, 4>::default(); let _h = Grid::<usize, WIDTH, HEIGHT>::default(); }
注意,常量泛型目前还有一些特性并未完善,比如下面示例中的 #![feature(generic_const_exprs)]
特性,需要在 Nightly Rust 下使用。
#![feature(generic_const_exprs)] pub struct Grid<T, const W: usize, const H: usize> where [(); W * H]: Sized, { array: [T; W * H], } impl<T, const W: usize, const H: usize> Default for Grid<T, W, H> where [(); W * H]: Sized, T: Default + Copy, { fn default() -> Self { Self { array: [T::default(); W * H], } } } const WIDTH: usize = 300; const HEIGHT: usize = 200; fn main(){ let _g = Grid::<usize, 3, 4>::default(); let _h = Grid::<usize, WIDTH, HEIGHT>::default(); }
P.TYP.SCT.02 当需要很多构造函数,或构造含有很多可选配置项时,宜使用构建者模式
淘汰原因
这条属于编程最佳实践,放到规范中有点臃肿,独立到最佳实践中。
【描述】
Rust 中没有默认的构造函数,都是自定义构造函数。
如果需要多个构造函数,或者构造时需要很多可选配置的复杂场景,那么构建者模式是适合你的选择。
【示例】
#![allow(unused)] fn main() { #[derive(Debug, PartialEq)] pub struct Foo { // Lots of complicated fields. bar: String, } impl Foo { // This method will help users to discover the builder pub fn builder() -> FooBuilder { FooBuilder::default() } } #[derive(Default)] pub struct FooBuilder { // Probably lots of optional fields. bar: String, } impl FooBuilder { pub fn new(/* ... */) -> FooBuilder { // Set the minimally required fields of Foo. FooBuilder { bar: String::from("X"), } } pub fn name(mut self, bar: String) -> FooBuilder { // Set the name on the builder itself, and return the builder by value. self.bar = bar; self } // If we can get away with not consuming the Builder here, that is an // advantage. It means we can use the FooBuilder as a template for constructing // many Foos. pub fn build(self) -> Foo { // Create a Foo from the FooBuilder, applying all settings in FooBuilder // to Foo. Foo { bar: self.bar } } } #[test] fn builder_test() { let foo = Foo { bar: String::from("Y"), }; let foo_from_builder: Foo = FooBuilder::new().name(String::from("Y")).build(); assert_eq!(foo, foo_from_builder); } }
P.TYP.ENM.01 需要取出 Enum 值的时候宜使用 std::mem::take/swap/replace
淘汰原因
偏教程向。
【描述】
需要取出 Enum 中值的时候,可能会遇到所有权的限制,此时可以使用 std::men::take
获取当前的值,而将默认值替换原值,这样可以避免所有权的限制。
但是 std::men::take
只适合实现 Default
的类型,这样就有默认实现可以替换了。
如果没有实现 Default
的类型,可以使用 std::men::swap
或 std::mem::replace
用给定的值来替换原值。
【正例】
#![allow(unused)] fn main() { use std::mem; enum MultiVariateEnum { A { name: String }, B { name: String }, C, D } fn swizzle(e: &mut MultiVariateEnum) { use MultiVariateEnum::*; *e = match e { // Ownership rules do not allow taking `name` by value, but we cannot // take the value out of a mutable reference, unless we replace it: A { name } => B { name: mem::take(name) }, B { name } => A { name: mem::take(name) }, C => D, D => C } } }
表达式
G.EXP.04 不应使用无效表达式语句
淘汰原因
属于代码逻辑问题,不应放到规范中。这类问题交给 Clippy 这类工具即可。
【级别】 建议
【描述】
无效的表达式语句,虽然会执行,但实际并没有起到什么效果。
也有例外情况存在。
【反例】
#![allow(unused)] fn main() { a+1; }
【正例】
#![allow(unused)] fn main() { let a = 41; let a = a+1; }
【例外】
像在下面代码中,为了确保常量函数 new
可以在输入参数超出 MAX 限制的情况下 panic,使用了一个数组的技巧: ["tag number out of range"][(byte > Self::MAX) as usize];
。因为目前 在常量上下文中还无法直接使用 panic!
,等 const_panic
功能稳定就可以了。
如果不加 #[allow(clippy::no_effect)]
,Clippy 会有警告。
#![allow(unused)] fn main() { // From: https://docs.rs/crate/der/0.4.1/source/src/tag/number.rs #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] pub struct TagNumber(pub(super) u8); impl TagNumber { /// Maximum tag number supported (inclusive). pub const MAX: u8 = 30; /// Create a new tag number (const-friendly). /// /// Panics if the tag number is greater than [`TagNumber::MAX`]. For a fallible /// conversion, use [`TryFrom`] instead. #[allow(clippy::no_effect)] pub const fn new(byte: u8) -> Self { // TODO(tarcieri): hax! use const panic when available ["tag number out of range"][(byte > Self::MAX) as usize]; Self(byte) } // ... } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
no_effect | yes | no | complexity | warn |
控制流程
G.CTF.01 避免在流程控制分支中使用重复代码
淘汰原因
属于代码逻辑问题,不应放到规范中。这类问题交给 Clippy 这类工具即可。
【级别】 建议
【描述】
略
【反例】
#![allow(unused)] fn main() { let foo = if … { println!("Hello World"); 13 } else { println!("Hello World"); 42 }; }
【正例】
#![allow(unused)] fn main() { println!("Hello World"); let foo = if … { 13 } else { 42 }; }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
branches_sharing_code | yes | no | nursery | allow |
G.CTF.02 控制流程的分支逻辑要保持精炼
淘汰原因
属于代码逻辑问题,不应放到规范中。这类问题交给 Clippy 这类工具即可。
【级别】 建议
【描述】
略
【反例】
#![allow(unused)] fn main() { if x { … } else { // collapsible_else_if if y { … } } if x { // collapsible_if if y { … } } // collapsible_match fn func(opt: Option<Result<u64, String>>) { let n = match opt { Some(n) => match n { Ok(n) => n, _ => return, } None => return, }; } // double_comparisons let x = 1; let y = 2; if x == y || x < y {} // wildcard_in_or_patterns match "foo" { "a" => {}, "bar" | _ => {}, } }
【正例】
#![allow(unused)] fn main() { // else if if x { … } else if y { … } // Merge multiple conditions if x && y { … } // match fn func(opt: Option<Result<u64, String>>) { let n = match opt { Some(Ok(n)) => n, _ => return, }; } // comparisons let x = 1; let y = 2; if x <= y {} // wildcard_in_or_patterns match "foo" { "a" => {}, _ => {}, } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
collapsible_else_if | yes | no | style | warn |
collapsible_if | yes | no | style | warn |
collapsible_match | yes | no | style | warn |
double_comparisons | yes | no | complexity | warn |
wildcard_in_or_patterns | yes | no | complexity | warn |
G.CTF.06 善用标准库中提供的迭代器适配器方法来满足自己的需求
淘汰原因
属于最佳实践编程技巧类。
【级别】 建议
【描述】
Rust 标准库中提供了很多迭代器方法,要学会使用它们,选择合适的方法来满足自己的需求。
下面示例中,反例中的迭代器适配器方法,都可以用对应的正例中的方法代替。
【反例】
#![allow(unused)] fn main() { // explicit_counter_loop let v = vec![1]; fn bar(bar: usize, baz: usize) {} let mut i = 0; for item in &v { bar(i, *item); i += 1; } // filter_map_identity let iter = vec![Some(1)].into_iter(); iter.filter_map(|x| x); // filter_next let vec = vec![1]; vec.iter().filter(|x| **x == 0).next(); // flat_map_identity let iter = vec![vec![0]].into_iter(); iter.flat_map(|x| x); // flat_map_option let nums: Vec<i32> = ["1", "2", "whee!"].iter().flat_map(|x| x.parse().ok()).collect(); }
【正例】
#![allow(unused)] fn main() { // explicit_counter_loop let v = vec![1]; fn bar(bar: usize, baz: usize) {} for (i, item) in v.iter().enumerate() { bar(i, *item); } // filter_map_identity let iter = vec![Some(1)].into_iter(); iter.flatten(); // filter_next let vec = vec![1]; vec.iter().find(|x| **x == 0); // flat_map_identity let iter = vec![vec![0]].into_iter(); iter.flatten(); // flat_map_option let nums: Vec<i32> = ["1", "2", "whee!"].iter().filter_map(|x| x.parse().ok()).collect(); }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
explicit_counter_loop | yes | no | complexity | warn |
filter_map_identity | yes | no | complexity | warn |
filter_next | yes | no | complexity | warn |
flat_map_identity | yes | no | complexity | warn |
flat_map_option | yes | no | pedantic | allow |
字符串
P.STR.03 可以使用Cow<str>
来代替直接使用字符串,它可以减少Copy
淘汰原因
最佳实践编程技巧类。
【描述】
使用 Cow<str>
作为字符串处理函数参数和返回值,可以尽可能地减少数据Copy 和 内存分配。当字符串没有修改的时候,实际使用的是 &'a str
,只有当数据修改的时候才会使用String
。对于读操作大于写操作的场景,使用 Cow<str>
比较合适。
【示例】
#![allow(unused)] fn main() { // 对输入的字符串进行转义 pub fn naive<'a, S: Into<Cow<'a, str>>>(input: S) -> Cow<'a, str> { let input = input.into(); fn is_trouble(c: char) -> bool { c == '<' || c == '>' || c == '&' } if input.contains(is_trouble) { let mut output = String::with_capacity(input.len()); for c in input.chars() { match c { '<' => output.push_str("<"), '>' => output.push_str(">"), '&' => output.push_str("&"), _ => output.push(c) } } // 只有在字符串修改的时候才使用 String Cow::Owned(output) } else { //其他情况使用 &str input } } }
集合容器
P.CLT.01 根据集合各自的特点选择合适的集合类型
淘汰原因
文档教程向,不该放到规范中。
【描述】
Rust 标准库内置的集合类型,在安全和性能方面还是比较靠谱的。需要仔细阅读标准库中各类集合类型的优缺点来选择合适的类型。
下列场景考虑 Vec
- 你想要一个可动态增长大小(堆分配)的数组
- 你想要一个栈结构
- 你想要集合元素按特定顺序排序,并且仅需要在结尾追加新元素
- 你可能只是想临时收集一些元素,并且不关心它们的实际存储
下列场景考虑 VecDeque
- 你想要一个可以在头尾两端插入元素的
Vec
- 你想要一个队列,或双端队列
下列场景考虑LinkedList
- 你非常确定你真的需要一个双向链表
下列场景考虑 Hashmap
- 你需要一个 KV 集合
- 你想要一个缓存
下列场景考虑 BTreeMap
- 你需要一个可以排序的
HashMap
- 你希望可以按需获取一系列元素
- 你对最小或最大的 KV 感兴趣
- 你想要寻找比某个值更大或更小的键
下列场景考虑使用 Set
系列
- 你只是需要一个 Set 集合,而不需要键值对。
下列场景考虑使用 BinaryHeap
- 你想存储一堆元素,但只想在任何给定时间内处理 最大 或 最重要的元素
- 你想要一个优先队列
错误处理
P.ERR.02 当函数的返回值或者结构体字段的值可能为空时,请使用Option<T>
淘汰原因
教程向,不该放到规范中。
【描述】
在某些其他语言中,如果函数的返回值 或 结构体字段的值 可能为空时,通常会设置一个 “哨兵值(Sentinel Value)” 来应对这种问题,比如使用一个 nil
或 -1
等特殊值来判断这类情况。
但是,在 Rust 中不需要这样,Rust 提供了 Option<T>
类型就是专门用于应对这类情况。
【正例】
struct Config { must: String, opt: Option<String>, } // OR fn main() { let sentence = "The fox jumps over the dog"; let index = sentence.find("fox"); if let Some(fox) = index { let words_after_fox = &sentence[fox..]; println!("{}", words_after_fox); } }
P.ERR.04 当程序中需要处理错误时,应该使用 Result<T, E>
和 ?
操作符
淘汰原因
教程向,不该放到规范中。
【描述】
当需要处理错误时,为了保证 程序的健壮性,应该尽可能处理错误。
【反例】
在实现原型类项目的时候,可以“快、糙、猛”地使用 expect
。但是要进生产环境,需要合理地处理错误。
#![allow(unused)] fn main() { let res: Result<usize, ()> = Ok(1); res.expect("one"); // 如果有 Err, expect会 Panic ! }
【正例】
#![allow(unused)] fn main() { let res: Result<usize, ()> = Ok(1); res?; // Ok::<(), ()>(()) }
P.ERR.06 根据应用还是库来选择不同的错误处理方式
淘汰原因
最佳实践向,不该放到规范中。
【描述】
如果编写应用,建议使用 Error
trait对象;如果编写库,则建议返回自定义错误类型,方便下游处理
【正例】
#![allow(unused)] fn main() { // 对于应用使用 Error trait 对象更加方便 pub fn print(&self, languages: &Languages) -> Result<String, Box<dyn Error>> { // do something } // 对于库,暴露自定义错误类型更加方便下游处理错误 #[derive(Debug)] pub struct SendError<T>(pub T); impl<T> fmt::Display for SendError<T> { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { write!(fmt, "channel closed") } } }
P.ERR.03 当程序中有不可恢复的错误时,应该让其 Panic
淘汰原因
教程向,不该放到规范中。
【描述】
如果遇到无法恢复的错误,则需要让程序 Panic。
相关 Clippy Lint: if_then_panic
【正例】
#![allow(unused)] fn main() { fn boot(ptr: *const usize) { if ptr.is_null() { panic!("ptr is null! boot failed!") } // or assert!(ptr.is_null(), "ptr is null! boot failed!"); } }
多线程
P.MTH.LOK.01 根据场景选择使用互斥锁还是 Channel
淘汰原因
教程向,不该放到规范中。
【描述】
不要从哪种方式更快的角度来考虑,而应该从使用场景。性能取决于开发者如何使用它们。
一个简单的指南:
Channel 适用于 | Mutex 适用于 |
---|---|
传递数据所有权 分发工作单元 传递异步结果 | 修改共享缓存 修改共享状态 |
G.MTH.LOK.02 多线程环境下宜使用 Arc
代替 Rc
淘汰原因
教程向,不该放到规范中。
【级别】 建议
【描述】
Rc
是专门用于单线程的,多线程下应该用 Arc
。
【反例】
#![allow(unused)] fn main() { use std::rc::Rc; use std::sync::Mutex; fn foo(interned: Rc<Mutex<i32>>) { ... } }
【正例】
#![allow(unused)] fn main() { use std::rc::Rc; use std::sync::Arc; use std::cell::RefCell fn foo(interned: Rc<RefCell<i32>>) { ... } // or fn foo(interned: Arc<Mutex<i32>>) { ... } }
【Lint 检测】
lint name | Clippy 可检测 | Rustc 可检测 | Lint Group | level |
---|---|---|---|---|
rc_mutex | yes | no | restriction | allow |
no-std
P.EMB.03 将一些公用的类型、函数、宏等集中到一个自定义的 baremetal-std
淘汰原因
最佳实践向,不该放到规范中。
【描述】
虽然 no-std
下不能用Rust 的标准库,但是可以自定义 no-std
下的标准库 baremetal-std
,用于积累 no-std
下常用的公共库。