Rust 编码规范介绍

状态

介绍

据了解,Rust 社区内有些公司和组织都各自维护着自己的编码规范。下面罗列了一些已经公开的:

以上公开的编码规范,除官方Rust API 编写指南和法国国家信息安全局是通用目的公布之外,其他编码规范主要是为了开源项目自身统一贡献者编码风格而制作。

随着 Rust 的普及,各大公司应用 Rust 的需求增多,为了方便 Rust 在公司落地,需要一个统一的、通用的、可通过 Clippy 等工具辅助检查的编码规范。

本规范参考但不限于以上公开规范。本规范并非 Rust 教程,但也可以作为学习参考。

编码规范分类

  1. 安全编码规范。
  2. 领域最佳实践。
  3. 工具链使用指南。
  4. Rust优化指南。
  5. 常用基础库指南。

贡献形式

graph TD
    A{Rust 编码规范}
    A -->|公司A| B[云计算]
    A -->|公司C| C[嵌入式]
    A -->|公司B| D[网络]
    A -->|公司D| E[跨平台组件]
    A -->|公司E| F[WebAssembly]

本规范致力于成为统一的 Rust 编码规范,并联合国内外公司共同维护。

公司可以依赖本规范,结合自己的业务领域和团队习惯,形成自己的编码规范,并可以在日常实践中反哺本规范,让本规范更加完善。

个人开发者也可以参与贡献!

如何参与贡献?

请阅读 贡献说明

贡献者指南

目前项目处于初期,先以 issues 提建议为主,暂不支持 Pull Request。

等项目整体结构确定以后,再开始接受 Pull Request。

贡献名单

Rust 安全编码规范

Rust 语言虽然以类型安全、内存安全、线程安全和高性能等特性著称,但对于开发者而言,还是需要遵循一定的编码规范,更有助于写出地道安全的 Rust 代码。

虽然 Rust 也提供了 Clippy 这样的静态检查工具来帮助开发者识别代码中的坏味道,但是 Clippy 内部为什么要遵循这些规则,以及它们的分类等,对于大部分开发者还是不太透明。这方面也需要一个文档来进行梳理。

另外,Clippy 也有其局限性:无法检测代码中的语义。 语义在 Rust 里对于写出好代码来说,也是比较重要的参考因素。有时候 clippy 的建议反而会产生负面效果。

所以,为了更好地在公司内落地 Rust, 追求更好的代码质量,制定安全编码规范是必须的。

注意:本规范并不是 Rust 教程。

本规范的指导原则:

编码规范 和 Clippy 的关系是相辅相成的。

本规范要做的,就是根据 Clippy 里的lint ,结合社区生态中的一些实践,提炼出一些规则,进一步从语义角度思考这些 lint 的实用性,明确它们的使用边界,帮助开发者在编写代码和使用 Clippy 等工具检查的时候,可以更加有理有据地做出选择。

本规范的目标如下:

  1. 提高 Unsafe Rust 代码编写的安全性。
  2. 提高 代码的健壮性。
  3. 提高 代码的可读性、可维护性和可移植性。
  4. 编程规范条款力争系统化、易应用、易检查。

规范条款分为原则和规则两个类别,

  • 原则,就是编程开发时指导的一个大方向,或是指一类情况,没有那么具体。原则只是一种建议。
  • 规则,是相对原则而言,是更加具体的条目。规则里面有写【建议】还是【必须】遵守。规则一般是可lint检测的。

本规范中的规则也分三种类型:

  1. 大部分默认是Clippy支持lint检测的
  2. 也有一些是当前clippy 无法检测但可以定制 lint 的规则
  3. 编译器可以检测,警告或报错,但是开发者值得了解的一些规则

原则没有办法去检测,所以只能是建议。

通过标题前的编号来标识:

  • 标识为P为原则(Principle)。编号方式为P.Element.Number
  • 标识为G为规则(Guideline)。编号方式为G.Element.Number
  • 当有子目录时。编号方式为 P.Element.SubElement.NumberG.Element.SubElement.Number

Number 从01开始递增。Element为领域知识中关键元素的三位英文字母缩略语。

Element解释Element解释
NAM命名CMT注释
FMT格式TYP数据类型
CNS常量VAR变量
EXP表达式CTL流程控制
RFE引用PTR指针
STR字符串INT整数
MOD模块CAR包管理
MEM内存FUD函数设计
MACSTV静态变量
GEN泛型TRA特质
ASY异步UNS非安全
SafeAbstract安全抽象FFI外部函数调用
LAY内存布局ERR错误处理
CLT集合MTH多线程
EMB嵌入式RustIO输入输出
Security信息安全OTH其他

鸣谢

本指南参考《华为 C 语言编程指南 V 1.0》,感谢华为 开源能力中心 提供编程指南规范协助!

开发环境

编辑器推荐

VSCode + Rust Analyzer 扩展

其他辅助vscode 扩展:

flowistry ,可以帮助开发者理解 Rust 程序。

IDE 推荐

Clion

工具链安装

使用 Rustup。 如需替代安装方式,为了保证安全,最好选择官方推荐的替代安装方式。

Rust 版次(Edition) 说明

Rust从2015开始,每三年发布一个 Edition 版次:

  1. Rust 2015 edition (Rust 0.1.0 ~ Rust 1.0.0)
  2. Rust 2018 edition (Rust 1.0.0 ~ Rust 1.31.0)
  3. Rust 2021 edition (Rust 1.31.0 ~ Rust 1.56.0 )

以此类推。Edition是向前兼容的。Edition 和语义化版本是正交的,不冲突。

关于 Edition 更详细的内容可以查看:https://doc.rust-lang.org/edition-guide/

稳定版、 开发版和测试版工具链

Rust 工具链提供三种不同的发布渠道:

  1. Nightly(开发版),每晚发布(release)一次。
  2. Beta(测试版),每六周发布一次,基于Nightly版本。
  3. Stable(稳定版),每六周发布一次,基于 beta版本。

注意:

  1. 推荐使用 Stable Rust。
  2. 在基于Nightly Rust 开发项目的时候,最好通过在项目中增加 rust-toolchain 文件来指定一个固定的版本,避免因为Nightly Rust 的频繁变更而导致项目编译问题。
  3. 当在稳定版工作的时候,如果需要Nightly工具链,不需要整体上去切换工具链到Nightly,只需要再命令中指明Nightly就可以了。比如 cargo +nightly fmt

包管理器 Cargo

Cargo 是 Rust 项目必不可少的包管理器,除此之外,它也是一种工作流:

  1. 可以用Cargo创建一个项目(bin/lib)
  2. 可以用它编译项目
  3. 可以用它生产项目的文档(依据文档注释)
  4. 可以用它运行单元测试(test)和基准测试(bench)
  5. 可以用它下载和管理crate依赖
  6. 可以用它分发软件包,默认分发到 crates.io 上面
  7. 可以为它编写插件,使用子命令的方式,扩展它的功能。

Cargo 通过 Cargo.toml 配置文件来管理 crate。

Toml 配置文件是一种最小化且无歧义的文件格式,Rust社区最常用Toml。可以通过 toml.io 进一步了解 Toml 的细节。

值得说明的是,在配置文件中如果有 [profile.*] 这种配置,需要引起注意,因为这类配置决定了编译器的调用方式,比如:

  1. debug-assertions ,决定了是否开启debug断言。
  2. 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扩展了三个命令:

  1. Cargo add,在命令行增加新的依赖,而不需要去知道这个依赖的语义版本。
  2. Cargo rm,在命令行删除一个指定依赖。
  3. 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_SERVERRUSTUP_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
}

代码风格

代码风格包含标识符的命名风格、排版与格式风格、注释风格等。一致的编码习惯与风格,可以提高代码可读性和可维护性。

命名

好的命名风格能让我们快速地了解某个名字代表的含义(类型、变量、函数、常量、宏等),甚至能凸显其在整个代码上下文中的语义。命名管理对提升代码的可读性和维护性相当重要。


P.NAM.01 类型名称应该使用统一的词序

【描述】

类型名称都按照 动词-宾语-error 的单词顺序。

具体选择什么样的词序并不重要,但务必要保证同一个 crate 内词序的一致性,以及与标准库相似函数的一致性。

【示例】

【正例】

以下是来自标准库的处理错误的一些类型:

【反例】


#![allow(unused)]
fn main() {
// 应该为 ParseAddrError
struct AddrParseError {}
}

如果增加“解析地址错误”类型,为了保持词性一致,应该使用 ParseAddrError 名称,而不是 AddrParseError

P.NAM.02 cargo feature 名中不应该含有无意义的占位词

【描述】

Cargo feature 命名时,不要带有无实际含义的的词语,比如无需 use-abcwith-abc ,而是直接以 abc 命名。

这条原则经常出现在对 Rust 标准库进行 可选依赖 配置的 crate 上。

【示例】

【正例】

最简洁且正确的做法是:

# In Cargo.toml

[features]
default = ["std"]
std = []
// In lib.rs

#![cfg_attr(not(feature = "std"), no_std)]

【反例】

# In Cargo.toml

// 不要给 feature 取 `use-std` 或者 `with-std` 或者除 `std` 之外另取名字。
[features]
default = ["use-std"]
std = []
// In lib.rs

#![cfg_attr(not(feature = "use-std"), no_std)]

feature 应与 Cargo 在推断可选依赖时隐含的 features 具有一致的名字。

【正例】

假如 x crate 对 Serde 和 标准库具有可选依赖关系:

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

[features]
std = ["serde/std"]

[dependencies]
serde = { version = "1.0", optional = true }

当我们使用 x crate 时,可以使用 features = ["serde"] 开启 Serde 依赖。类似地,也可以使用 features = ["std"] 开启标准库依赖。 Cargo 推断的隐含的 features 应该叫做 serde ,而不是 use-serde 或者 with-serde

【反例】

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

[features]
std = ["serde/std"]
// Cargo 要求 features 应该是叠加的,所以像 `no-abc` 这种负向的 feature 命名实际上并不正确。
no-abc=[]

[dependencies]
serde = { version = "1.0", optional = true }

G.NAM.01 标识符命名应该符合阅读习惯

【级别:必须】

必须严格按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制化参考】

检测错误的英文拼写,检测出后提示;检测拼音,检测出来提示。拼写错误可参考 client9/misspell

【描述】

标识符的命名要清晰、明了,有明确含义,容易理解。符合英文阅读习惯的命名将明显提高代码可读性。

一些好的实践包括但不限于:

  • 使用正确的英文单词并符合英文语法,不要使用拼音
  • 仅使用常见或领域内通用的单词缩写
  • 布尔型变量或函数避免使用否定形式
  • 尽量不要使用 Unicode 标识符。

【正例】


#![allow(unused)]
fn main() {
let first_name: &str = "John";
let last_name: &str = "Smith";
const ERROR_DIRECTORY_NOT_SUPPORTED: u32 = 336;
const ERROR_DRIVER_CANCEL_TIMEOUT: u32 = 594;
}

【反例】


#![allow(unused)]
fn main() {
let ming: &str = "John";
let xing: &str = "Smith";
const ERROR_NO_1: u32 = 336;
const ERROR_NO_2: u32 = 594;
}

G.NAM.02 使用统一的命名风格

【级别:必须】

必须严格按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group
Rustc: non_camel_case_typesnoyesStyle
Rustc: non_snake_casenoyesStyle

【描述】

Rust 命名规范在 RFC 0430 中有描述。总的来说,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)有争议 ,但是一般遵照 [C-FEATURE]

说明 :

  1. UpperCamelCase情况下,由首字母缩写组成的缩略语和 复合词的缩写,算作一个词。比如,应该使用 Uuid 而非 UUID,使用 Usize 而不是 USize,或者是 Stdin 而不是 StdIn
  2. snake_case中,首字母缩写和缩略词是小写的:is_xid_start。
  3. snake_case 或者 SCREAMING_SNAKE_CASE 情况下,每个词不应该由单个字母组成——除非这个字母是最后一个词。比如,使用 btree_map 而不使用 b_tree_map,使用 PI_2 而不使用 PI2

关于包命名:

  • 1: 由于历史问题,包名有两种形式 snake_casekebab-case ,但实际在代码中需要引入包名的时候,Rust 只能识别 snake_case,也会自动将 kebab-case 识别为 kebab_case
  • Crate 的名称通常不应该使用 -rs 或者 -rust 作为后缀或者前缀。 因为每个 crate 都是 Rust 编写的! 没必要一直提醒使用者这一点。但是有些情况下,比如是其他语言移植的同名 Rust 实现,则可以使用 -rs 后缀来表明这是 Rust 实现的版本。

G.NAM.03 作用域越大,命名越精确;反之应简短

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
module_name_repetitionsyesnopedanticallow

【描述】

  1. 对于全局函数、全局变量、宏、类型名、枚举命名,应当精确描述并全局唯一。
  2. 对于函数局部变量,或者结构体、枚举中的成员变量,在其命名能够准确表达含义的前提下,应该尽量简短,避免冗余信息重复描述。

【示例】

全局静态变量示例

【正例】


#![allow(unused)]
fn main() {
static GET_MAX_THREAD_COUNT: i32 = 42;  // 符合
}

【反例】


#![allow(unused)]
fn main() {
static GET_COUNT: i32 = 42;  // 不符合:描述不精确
}

枚举类型的成员命名的例子:

【正例】


#![allow(unused)]
fn main() {
// 符合: 上下文信息已经知道它是 Event
enum WebEvent {
    // An `enum` may either be `unit-like`,
    PageLoad,
    PageUnload,
    // like tuple structs,
    KeyPress(char),
    Paste(String),
    // or c-like structures.
    Click { x: i64, y: i64 },
}
}

【反例】


#![allow(unused)]

fn main() {
// 不符合

enum WebEvent {
    // An `enum` may either be `unit-like`,
    PageLoadEvent,
    PageUnloadEvent,
    // like tuple structs,
    KeyPressEvent(char),
    PasteEvent(String),
    // or c-like structures.
    ClickEvent { x: i64, y: i64 },
}
}

类型别名示例

【正例】


#![allow(unused)]
fn main() {
type Size = u16; 

pub struct HeaderMap {
    // 在使用它的地方自然就知道是描述谁的大小
    mask: Size,
}
}

【反例】


#![allow(unused)]
fn main() {
type MaskSize = u16; 

pub struct HeaderMap {
    // 这样使用就显得有些冗余
    mask: MaskSize,
}
}

G.NAM.04 类型转换函数命名需要遵循所有权语义

【级别:必须】

必须严格按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint GroupLint Level
wrong_self_conventionyesnoStylewarn

【描述】

应该使用带有以下前缀名称方法来进行特定类型转换:

名称前缀内存代价所有权
as_无代价borrowed -> borrowed
to_代价昂贵borrowed -> borrowed
borrowed -> owned (非 Copy 类型)
owned -> owned (Copy 类型)
into_看情况owned -> owned (非 Copy 类型)

【示例】

【正例】

  • 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 ,这可能以很大的代价刷新所有缓存数据。

as_into_ 作为前缀的类型转换通常是 降低抽象层次 ,要么是查看背后的数据 ( as ) ,要么是分解 (deconstructe) 背后的数据 ( into ) 。 相对来说,以 to_ 作为前缀的类型转换处于同一个抽象层次,但是做了更多的工作。

当一个类型用更高级别的语义 (higher-level semantics) 封装 (wraps) 一个与之有关的值时,应该使用 into_inner() 方法名来取出被封装的值。

这适用于以下封装器:

读取缓存 (BufReader) 、编码或解码 (GzDecoder) 、取出原子 (AtomicBool) 、 或者任何相似的语义 (BufWriter)。

如果类型转换方法返回的类型具有 mut 标识符,那么这个方法的名称应如同返回类型组成部分的顺序那样,带有 mut 名。 比如 Vec::as_mut_slice 返回 mut slice 类型,这个方法的功能正如其名称所述,所以这个名称优于 as_slice_mut

// Return type is a mut slice.
fn as_mut_slice(&mut self) -> &mut [T];

更多来自标准库的例子:

G.NAM.05 用于访问或获取数据的 getter/setter 类方法通常不要使用 get_set_ 等前缀

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制化参考】

检测 Struct 实现的方法名是否包含 get_/set_ 前缀,如果包含,则给予警告。

【描述】

因为 Rust 所有权语义的存在,此例子中两个方法的参数分别是共享引用 &self 和 独占引用 &mut self,分别代表了 getter 和 setter 的语义。

【示例】

【正例】


#![allow(unused)]
fn main() {
pub struct First;
pub struct Second;

pub struct S {
    first: First,
    second: Second,
}

impl S {
    // 不建议 `get_first`。
    pub fn first(&self) -> &First {
        &self.first
    }

    // 不建议 `get_first_mut`, `get_mut_first`, or `mut_first`.
    pub fn first_mut(&mut self) -> &mut First {
        &mut self.first
    }
}
}

【反例】


#![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
    }
}
}

【例外】

但也存在例外情况:只有当有一个明显的东西可以通过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 就不对了。

来自标准库的例子:

G.NAM.06 遵循 iter/ iter_mut/ into_iter 规范来生成迭代器

【级别:必须】

必须严格按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制化参考】

检测 iter/iter_mut/into_iter 方法的返回类型是否对应 Iter/IterMut/IntoIter ,如果不是,则给予警告。

【描述】

对于容纳 U 类型的容器 (container) ,其迭代器方法应该这样命名:

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>

此规则适用于在概念上属于同质集合的数据结构的方法,而非函数。例如,第三方库 url 中的 percent_encode 返回一个 URL 编码的字符串片段的迭代器。使用iter/iter_mut/into_iter约定的话,函数名就不会有任何明确的语义了。

【示例】

【正例】

【反例】

标准库中存在一个反例: str 类型是有效 UTF-8 字节的切片(slice),概念上与同质集合略有差别,所以 str 没有提供 iter/iter_mut/into_iter 命名的迭代器方法,而是提供 str::bytes 方法来输出字节迭代器、str::chars 方法来输出字符迭代器。

【参考】

参考 RFC 199

G.NAM.07 迭代器类型名称应该与产生它们的方法相匹配

【级别:必须】

必须严格按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制化参考】

检测返回迭代器的方法,其返回类型应该与方法名相匹配,否则给予警告。

【描述】

一个叫做into_iter()的方法应该返回一个叫做IntoIter的类型,同样,所有其他返回迭代器的方法也是如此。

这条规则主要适用于方法,但通常对函数也有意义。例如,第三方库 url 中的 percent_encode 返回一个PercentEncode 类型的迭代器。

【示例】

【正例】

来自标准库的例子:

G.NAM.8 避免使用语言内置保留字、关键字、内置类型和trait等特殊名称

【级别:必须】

必须严格按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制化参考】

可以检测 标识符 是否通过r#使用了 语言内置的保留字、关键字、内置类型和trait等特殊名称,如果使用,则给予警告。

【描述】

命名必须要避免使用语言内置的保留字、关键字、内置类型和trait等特殊名称。

【反例】

// Sized : Rust 内置了同名 trait 
type Sized = u16; 

fn main() {
    // try 为保留关键字,使用`r#`前缀可以使用它,但要尽力避免
    let r#try = 1;
}

格式

Rust 有自动化格式化工具 rustfmt ,可以帮助开发者摆脱手工调整代码格式的工作,提升生产力。但是,rustfmt 遵循什么样的风格规范,作为开发者应该需要了解,在编写代码的时候可以主动按这样的风格编写。

说明:以下 rustfmt 配置中对应配置项如果 StableNo,则表示该配置项不能用于 Stable Rust 下在 rustfmt.toml 中自定义,但其默认值会在cargo fmt时生效。在 Nightly Rust 下则都可以自定义。

在 Stable Rust 下使用未稳定配置项的方法、了解配置示例及其他全局配置项说明请参阅:Rustfmt 配置相关说明

注意: 以下规则 针对 rustfmt version 1.4.36 版本。


P.FMT.01 代码格式以保证可读性为前提

【描述】

制定统一的编码风格,是为了提升代码的可读性,让日常代码维护和团队之间审查代码更加方便。


G.FMT.01 始终使用 rustfmt 进行自动格式化代码

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制化建议】

通过检测 项目 根目录下是否存在 rustfmt.toml.rustfmt.toml ,如果没有该文件,则发出警告,让开发者使用 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,
    ];
}

G.FMT.02 缩进始终使用空格(space)而非制表符(tab)

【级别:必须】

必须严格按此规范执行。

【rustfmt 配置】

此规则 Clippy 不可检测,由 rustfmt 自动格式化。

rustfmt 配置:

对应选项可选值是否 stable说明
tab_spaces4yes(默认)缩进空格数|
hard_tabsfalseyes(默认)禁止使用tab缩进|

【描述】

  1. 缩进要使用 四个 空格,不要使用制表符(\t)代替。
  2. 通过 IDE/Editor 为缩进默认好设置值。

G.FMT.03 每行最大宽度为 100 个字符

【级别:建议】

建议按此规范执行。

【rustfmt 配置】

此规则 Clippy 不可检测,由 rustfmt 自动格式化。

rustfmt 配置:

对应选项可选值是否 stable说明
max_width100yes(默认)行最大宽度默认值
error_on_line_overflowfalse(默认)No (tracking issue: #3391)如果超过最大行宽设置则报错
use_small_heuristicsDefault(默认)Max(推荐)Yes统一管理宽度设置

【描述】

代码行宽不宜过长,否则不利于阅读。 建议每行字符数不要超过 100 个字符。

rustfmt 还提供很多其他宽度设置:

  • fn_call_width, 函数调用最大宽度设置,其默认值是 max_width60%
  • attr_fn_like_width, 像函数那样使用的属性宏最大宽度,其默认值是 max_width70%
  • struct_lit_width, 结构体字面量最大宽度,其默认值是 max_width18%
  • struct_variant_width, 结构体变量最大宽度,其默认值是 max_width35%
  • array_width, 数组最大宽度,其默认值是 max_width60%
  • chain_width, 链式结构最大宽度,其默认值是 max_width60%
  • single_line_if_else_max_width,单行 if-else 最大宽度,其默认值是 max_width50%

这么多宽度设置管理起来比较麻烦,所以使用 use_small_heuristics 来管理更好。

【示例】

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

【反例】

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

G.FMT.04 行间距最大宽度空一行

【级别:建议】

建议按此规范执行。

【rustfmt 配置】

此规则 Clippy 不可检测,由 rustfmt 自动格式化。

rustfmt 配置:

对应选项可选值是否 stable说明
blank_lines_lower_bound0(默认)No不空行
blank_lines_upper_bound1(默认)No最大空一行

【描述】

代码行之间,最小间隔 0 行,最大间隔1行。

【示例】

【正例】


#![allow(unused)]
fn main() {
fn foo() {
    println!("a");
}
// 1
fn bar() {
    println!("b");
    println!("c");
}
}

或者


#![allow(unused)]
fn main() {
fn foo() {
    println!("a");
}
fn bar() {
    println!("b");
	// 1
    println!("c");
}

}

【反例】


#![allow(unused)]
fn main() {
fn foo() {
    println!("a");
}
// 1
// 2
fn bar() {
    println!("b");
// 1
// 2
    println!("c");
}
}

G.FMT.05 语言项(Item) 定义时花括号(brace)位置应该与语言项保持同一行

【级别:建议】

建议按此规范执行。

【rustfmt 配置】

此规则 Clippy 不可检测,由 rustfmt 自动格式化。

rustfmt 配置:

对应选项可选值是否 stable说明
brace_styleSameLineWhere (默认)No应该与语言项保持同一行,但是 where 语句例外
brace_styleAlwaysNextLineNo应该在语言项的下一行
brace_stylePreferSameLineNo总是优先与语言项保持同一行,where 语句也不例外
where_single_linefalse(默认)No强制将 where 子句放在同一行上
brace_style in control-flowAlwaysSameLine (默认)No总在同一行上,用于控制流程中默认值
[brace_style in control-flowClosingNextLineNo用于控制流程中 else 分支在 if 分支结尾处换行

【描述】

花括号的位置风格默认使用 SameLineWhere,但是也根据不同的语言项略有区别。

【示例】

函数

【正例】


#![allow(unused)]
fn main() {
fn lorem() { // 花括号和fn定义在同一行
    // body
}

fn lorem(ipsum: usize) { // 花括号和fn定义在同一行
    // body
}

// 当有 `where` 子句的时候,花括号换行
// 并且,`where` 子句和 `where` 关键字不在同一行
fn lorem<T>(ipsum: T)
where
    T: Add + Sub + Mul + Div,
{
    // body
}
}

通过配置 where_single_line 为 true,方可设置 where子句在同一行,如下:


#![allow(unused)]
fn main() {
// 当有 `where` 子句的时候,花括号换行
// 设置了 `where_single_line=true` ,则`where` 子句和 `where` 关键字在同一行
fn lorem<T>(ipsum: T)
where T: Add + Sub + Mul + Div,
{
    // body
}
}

【反例】

如果设置 brace_style = "AlwaysNextLine",则:


#![allow(unused)]
fn main() {
fn lorem()
{
    // body
}

fn lorem(ipsum: usize)
{
    // body
}

fn lorem<T>(ipsum: T)
where
    T: Add + Sub + Mul + Div,
{
    // body
}
}

如果设置 brace_style = "PreferSameLine",则:


#![allow(unused)]
fn main() {
fn lorem() {
    // body
}

fn lorem(ipsum: usize) {
    // body
}

fn lorem<T>(ipsum: T)
where
    T: Add + Sub + Mul + Div, { // 注意这里和 `SameLineWhere`的区别
    // body
}
}

结构体与枚举

【正例】


#![allow(unused)]
fn main() {
struct Lorem {
    ipsum: bool,
}

struct Dolor<T>
where
    T: Eq,
{
    sit: T,
}
}

【反例】

如果设置 brace_style = "AlwaysNextLine",则:


#![allow(unused)]
fn main() {
struct Lorem
{
    ipsum: bool,
}

struct Dolor<T>
where
    T: Eq,
{
    sit: T,
}
}

如果设置 brace_style = "PreferSameLine",则:


#![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!");
    }
}

【反例】

如果设置 brace_style = "AlwaysNextLine",则:

fn main() {
    if lorem
    {
        println!("ipsum!");
    }
    else
    {
        println!("dolor!");
    }
}

如果设置 brace_style = "ClosingNextLine",则:

fn main() {
    if lorem {
        println!("ipsum!");
    } // 注意这里 if 分支结尾处,else 换行
    else { 
        println!("dolor!");
    }
}

G.FMT.06 单行规则

【级别:建议】

建议按此规范执行。

【rustfmt 配置】

此规则 Clippy 不可检测,由 rustfmt 自动格式化。

rustfmt 配置:

对应选项默认值是否 stable说明
empty_item_single_linetrue(默认)No当语言项内容为空时,要保持单行
fn_single_linefalse(默认)No当函数中只有一个表达式时,不要保持单行
struct_lit_single_linetrue(默认)No当结构体字面量中只有少量表达式时,要保持单行

【描述】

当语言项内容为空时,即空函数,空结构体,空实现等,要保持单独一行。但是,当函数中只有一个表达式时,请不要保持单行。

【示例】

【正例】

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,
    };
}

G.FMT.07 存在多个标识符时应该保持块状(Block)缩进

【级别:建议】

建议按此规范执行。

【rustfmt 配置】

此规则 Clippy 不可检测,由 rustfmt 自动格式化。

rustfmt 配置:

对应选项可选值是否 stable说明
indent_styleBlock(默认)No多个标识符定义保持块状风格,但看上去可能不太工整
indent_styleVisualNo多个标识符定义保持对齐风格,为了看上去工整

【描述】

当在表达式或语言项定义中出现多个标识符,则应该让其保持块状风格缩进。

【示例】

数组

【正例】

fn main() {
    let lorem = vec![
        "ipsum",
        "dolor",
        "sit",
        "amet",
        "consectetur",
        "adipiscing",
        "elit",
    ];
}

【反例】

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

【反例】

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
}

}

【反例】


#![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",
    );
}

【反例】

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

【反例】


#![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 = Lorem { ipsum: dolor,
                        sit: amet };
}

G.FMT.08 换行样式以文件自动检测为主

【级别:建议】

建议按此规范执行。

【rustfmt 配置】

此规则 Clippy 不可检测,由 rustfmt 自动格式化。

rustfmt 配置:

对应选项可选值是否 stable说明
newline_styleAuto(默认)Yes换行样式以文件自动检测为主

【描述】

换行样式是基于每个文件自动检测的。 具有混合行尾的文件将转换为第一个检测到的行尾样式。

不同平台换行符不同:

  • Windows\r\n结尾。
  • Unix\n 结尾。

G.FMT.09 当有多行表达式操作时,操作符应该置于行首

【级别:建议】

建议按此规范执行。

【rustfmt 配置】

此规则 Clippy 不可检测,由 rustfmt 自动格式化。

rustfmt 配置:

对应选项可选值是否 stable说明
binop_separatorFront(默认)No换行后,操作符置于行首

【描述】

当有多行表达式操作时,操作符应该置于行首。

【示例】

【正例】

操作符置于行首

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

G.FMT.10 枚举变体和结构体字段相互之间默认左对齐

【级别:建议】

建议按此规范执行。

【rustfmt 配置】

此规则 Clippy 不可检测,由 rustfmt 自动格式化。

rustfmt 配置:

对应选项可选值是否 stable说明
enum_discrim_align_threshold0(默认)No具有判别式的枚举变体与其他变体进行垂直对齐的最大长度。没有判别符的变体在对齐时将被忽略。
struct_field_align_threshold0(默认)No结构体字段垂直对齐的最大长度

【描述】

对于自定义了判别式的枚举体,和有字段的结构体而言,默认只需要左对齐就可以。这个宽度可以设置为任意值,但默认是0。此宽度并不是指插入多少空格,而是指需要对齐的字符长度。

【示例】

【正例】


#![allow(unused)]

fn main() {
enum Bar {
    A = 0,
    Bb = 1,
    RandomLongVariantGoesHere = 10,
    Ccc = 71,
}

enum Bar {
    VeryLongVariantNameHereA = 0,
    VeryLongVariantNameHereBb = 1,
    VeryLongVariantNameHereCcc = 2,
}
}

【反例】

enum_discrim_align_threshold = 20 时。


#![allow(unused)]
fn main() {
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,所以它会被对齐
}
}

G.FMT.11 多个函数参数和导入模块的布局

【级别:建议】

建议按此规范执行。

【rustfmt 配置】

此规则 Clippy 不可检测,由 rustfmt 自动格式化。

rustfmt 配置:

对应选项可选值是否 stable说明
fn_args_layoutTall(默认)Yes函数参数五个或以内可以一行,超过五个则使用块状缩进
imports_layoutMixed(默认)No导入模块每行超过四个则换行

【描述】

  1. 五个以内函数参数可以置于一行,超过五个则使用「块」状缩进。
  2. 导入模块每行超过四个,则换行。

【示例】

【正例】


#![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,
};
}

【反例】

fn_args_layoutimports_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,
};

}

G.FMT.12 空格使用规则

【级别:建议】

建议按此规范执行。

【rustfmt 配置】

此规则 Clippy 不可检测,由 rustfmt 自动格式化。

rustfmt 配置:

对应选项可选值是否 stable说明
space_after_colontrue(默认)No在冒号后面要加空格
space_before_colonfalse(默认)No在冒号前面不要加空格
spaces_around_rangesfalse(默认)No....=范围操作符前后不要加空格
type_punctuation_density"Wide"(默认)No+=操作符前后要加空格(此处特指类型签名)

【描述】

总结:

  1. 在冒号之后添加空格,在冒号之前不要加空格。
  2. 在范围(range)操作符(....=)前后不要使用空格。
  3. +=操作符前后要加空格。

【示例】

【正例】


#![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;
}
}

【反例】


#![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;
}
}

G.FMT.13 结尾逗号规则

【级别:建议】

建议按此规范执行。

【rustfmt 配置】

此规则 Clippy 不可检测,由 rustfmt 自动格式化。

rustfmt 配置:

对应选项可选值是否 stable说明
trailing_comma"Vertical"(默认)No当多个字段在不同行时,在最后一个字段结尾添加逗号,如果在同一行,则不加逗号
match_block_trailing_commafalse(默认)No在match分支中,如果包含了块,则不需要加逗号,否则需要加

【描述】

  1. 当多个字段在不同行时,在最后一个字段结尾添加逗号,如果在同一行,则不加逗号。
  2. 在match分支中,如果包含了块,则不需要加逗号,否则需要加。

【示例】

【正例】

// 当 `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"),
    }
}

【反例】

// 当 `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"),
    }
}

G.FMT.14 match 分支格式

【级别:建议】

建议按此规范执行。

【rustfmt 配置】

此规则 Clippy 不可检测,由 rustfmt 自动格式化。

rustfmt 配置:

对应选项可选值是否 stable说明
match_arm_blockstrue(默认)No当match分支右侧代码体太长无法和=>置于同一行需要使用块(block)来包裹
match_arm_leading_pipesNever(默认)No在match分支左侧匹配表达式前不要增加管道符(`

【描述】

  1. 当match分支右侧代码体太长无法和=>置于同一行需要使用块(block)来包裹。
  2. 在match分支左侧匹配表达式前不要增加管道符(|)

【示例】

【正例】

// 当 `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"),
        _ => {}
    }
}



【反例】

// 当 `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"),
        | _ => {}
    }
}

G.FMT.15 导入模块分组规则

【级别:建议】

建议按此规范执行。

【rustfmt 配置】

此规则 Clippy 不可检测,由 rustfmt 自动格式化。

rustfmt 配置:

对应选项可选值是否 stable说明
imports_granularity(Preserve(默认),Crate(推荐))No默认保留开发者的模块导入顺序
reorder_importstrue(默认)No模块分组内根据模块首字母按字典序进行排序
group_imports(Preserve(默认), StdExternalCrate(建议))No默认保留开发者的模块导入分组

【描述】

  1. 导入同一模块的类型,应该置于同一个块内(imports_granularity="Crate")。
  2. 模块导入应该按以下规则进行分组(group_imports="StdExternalCrate"):
    • 导入来自 stdcorealloc的模块需要置于前面一组。
    • 导入来自 第三方库的模块 应该置于中间一组。
    • 导入来自本地 selfsupercrate前缀的模块,置于后面一组。
  3. 分组内使用字典序进行排序(reorder_imports=true)。

说明: 默认 rustfmt 不会对导入的模块自动分组,而是保留开发者的导入顺序。所以,这里需要修改rustfmt 默认配置,但因为这几个配置项暂时未稳定,所以需要在 Nightly 下使用。

【示例】

【正例】


#![allow(unused)]
fn main() {
// 当 `imports_granularity="Crate"`
use foo::{
    a, b,
    b::{f, g},
    c,
    d::e,
};
use qux::{h, i};


// 当 `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;
}

【反例】


#![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};


// 当按默认值设置时,模块导入比较乱,影响可读性
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;
}

G.FMT.16 声明宏分支格式

【级别:建议】

建议按此规范执行。

【rustfmt 配置】

此规则 Clippy 不可检测,由 rustfmt 自动格式化。

rustfmt 配置:

对应选项可选值是否 stable说明
format_macro_matchers(false(默认),true(建议))No声明宏 模式匹配分支(=> 左侧)中要使用紧凑格式
format_macro_bodiestrue(默认)No声明宏分支代码体(=> 右侧) 使用宽松格式

【描述】

  1. 在声明宏中,模式匹配分支(=> 左侧)应该使用紧凑格式(format_macro_matchers=true)。
  2. 而分支代码体(=> 右侧) 使用宽松格式。详细请看示例。

一切都是为了提升可读性。

说明:因为这里需要修改format_macro_matchers的默认值,且该配置项并未 Stable ,所以需要 Nightly 下格式化。

【示例】

【正例】


#![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;
    };
}
}

【反例】


#![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;
    };
}

}

G.FMT.17 具名结构体字段初始化不要省略字段名

【级别:建议】

建议按此规范执行。

【rustfmt 配置】

此规则 Clippy 不可检测,由 rustfmt 自动格式化。

rustfmt 配置:

对应选项可选值是否 stable说明
use_field_init_shorthandfalse(默认)Yes具名结构体字段初始化不能省略字段名

【描述】

具名结构体字段初始化不能省略字段名。

【示例】

【正例】


struct Foo {
    x: u32,
    y: u32,
    z: u32,
}

fn main() {
    let x = 1;
    let y = 2;
    let z = 3;
    let a = Foo { x: x, y: y, z: z };
}

【反例】

struct Foo {
    x: u32,
    y: u32,
    z: u32,
}

fn main() {
    let x = 1;
    let y = 2;
    let z = 3;
    let a = Foo { x, y, z };
}

G.FMT.18 extern 外部函数需要指定 ABI

【级别:建议】

建议按此规范执行。

【rustfmt 配置】

此规则 Clippy 不可检测,由 rustfmt 自动格式化。

rustfmt 配置:

对应选项可选值是否 stable说明
force_explicit_abitrue(默认)Yesextern 外部函数总是要指定 ABI

【描述】

当使用 extern 指定外部函数时,建议显式指定 C-ABIextern 不指定的话默认就是 C-ABI,但是 Rust 语言显式指定是一种约定俗成。如果是 Rust-ABI则不会省略。

【示例】

【正例】


#![allow(unused)]
fn main() {
extern "C" {
    pub static lorem: c_int;
}

extern "Rust" {
    type MyType;
    fn f(&self) -> usize;
}
}

【反例】


#![allow(unused)]
fn main() {
// 省略 ABI 指定,则默认是 C-ABI
extern {
    pub static lorem: c_int;
}

// 非 C-ABI 是无法省略的
extern "Rust" {
    type MyType;
    fn f(&self) -> usize;
}
}

G.FMT.19 解构元组的时候允许使用..来指代剩余元素

【级别:建议】

建议按此规范执行。

【rustfmt 配置】

此规则 Clippy 不可检测,由 rustfmt 自动格式化。

rustfmt 配置:

对应选项可选值是否 stable说明
condense_wildcard_suffixesfalse(默认) true (推荐)No解构元组的时候是否允许使用..来指代剩余元素

【描述】

默认选项是 false,表示不允许 解构元组的时候使用..来指代剩余元素

【示例】

【正例】

设置 condense_wildcard_suffixes = true :

fn main() {
    let (lorem, ipsum, ..) = (1, 2, 3, 4);
}

【反例】

fn main() {
    let (lorem, ipsum, _, _) = (1, 2, 3, 4);
    let (lorem, ipsum, ..) = (1, 2, 3, 4);
}

G.FMT.20 不要将多个 Derive 宏合并为同一行

【级别:建议】

建议按此规范执行。

【rustfmt 配置】

此规则 Clippy 不可检测,由 rustfmt 自动格式化。

rustfmt 配置:

对应选项可选值是否 stable说明
merge_derivestrue(默认) false(推荐)Yes是否将多个 Derive 宏合并为同一行

【描述】

不要将多个 Derive 宏合并为同一行,可以增加代码可读性,明确语义。

【示例】

【正例】

修改默认设置 merge_derives = false


#![allow(unused)]
fn main() {
#[derive(Eq, PartialEq)]
#[derive(Debug)]
#[derive(Copy, Clone)]
pub enum Foo {}
}

【反例】

用默认设置 merge_derives = true


#![allow(unused)]
fn main() {
#[derive(Eq, PartialEq, Debug, Copy, Clone)]
pub enum Foo {}
}

注释与文档

在 Rust 中,注释分为两类:普通注释和文档注释。普通注释使用 ///* ... */,文档注释使用 /////!/** ... **/

在原则和规则中提到「注释」时,包括普通注释和文档注释。当提到「文档」时,特指文档注释。


P.CMT.01 不到必要的时候不要添加注释

【描述】

注释固然很重要, 但最好的代码应当本身就是文档。有意义的类型名、函数名和变量名, 要远胜过要用注释解释的含糊不清的名字。当有意义的类型名、函数名和变量名还不能表达完整的语义时,再使用注释。

不要描述显而易见的现象, 永远不要用自然语言翻译代码作为注释。

P.CMT.02 文档应该始终基于 rustdoc 工具来构建

【描述】

Rust 语言提供 rustdoc 工具来帮助构建文档,所以应该始终围绕rustdoc工具的特性来构建项目文档。

P.CMT.03 文档应该围绕 What 和 How 为核心来构建

【描述】

文档应该始终围绕两个方向来构建:

  1. What : 用于阐述代码为什么而构建。
  2. how : 用于阐述代码如何去使用。

P.CMT.04 注释和文档应该保持简短精干

【描述】

  1. 文档内容用语应该尽量简短精干,不宜篇幅过长。请确保你的代码注释良好并且易于他人理解。
  2. 使用通俗易懂的描述而尽量避免使用专业技术术语。好的注释能够传达上下文关系和代码目的。

P.CMT.05 注释和文档使用的自然语言要保持一致

【描述】

注释和文档尽量使用英文来填写,如果要使用中文,整个项目必须都使用中文。请确保整个项目中文档和注释都使用同一种文本语言,保持一致性。

P.CMT.06 在文档中应该使用 Markdown 格式

【描述】

Rust 文档注释支持 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 }
    }
}

G.CMT.01 注释应该有一定宽度限制

【级别:建议】

建议按此规范执行。

【rustfmt 配置】

此规则 Clippy 不可检测,由 rustfmt 自动格式化。

rustfmt 配置:

对应选项可选值是否 stable说明
comment_width80(默认)No指定一行注释允许的最大宽度
wrap_commentsfalse(默认),true(建议)No运行多行注释按最大宽度自动换成多行注释

【描述】

每行注释的宽度不能过长,需要设置一定的宽度,有助于提升可读性。comment_width可配合 wrap_comments 将超过宽度限制的注释自动分割为多行。

注意:use_small_heuristics配置项并不包括comment_width

【示例】

【正例】

comment_width=80wrap_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.
}

【反例】


#![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.
}

G.CMT.02 使用行注释而避免使用块注释

【级别:建议】

建议按此规范执行。

【rustfmt 配置】

此规则 Clippy 不可检测,由 rustfmt 自动格式化。

rustfmt 配置:

对应选项可选值是否 stable说明
normalize_commentsfalse(默认) true(推荐)No/**/ 注释转为 //
normalize_doc_attributesfalse(默认)No#![doc]#[doc] 注释转为 //!///

【描述】

尽量使用行注释(/////),而非块注释。

对于文档注释,仅在编写模块级文档时使用 //!,在其他情况使用 ///更好。

【示例】

【正例】

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

//! Example documentation

/// Example item documentation
pub enum Foo {}

}

【反例】


#![allow(unused)]
fn main() {
/*
 * Wait for the main task to return, and set the process error code
 * appropriately.
 */

mod tests {
    //! This module contains tests

    // ...
}

#![doc = "Example documentation"]

#[doc = "Example item documentation"]
pub enum Foo {}
}

G.CMT.03 在每一个文件开头加入版权公告

【级别:建议】

建议按此规范执行。

【rustfmt 配置】

此规则 Clippy 不可检测,由 rustfmt 自动格式化。

rustfmt 配置:

对应选项可选值是否 stable说明
license_template_path格式化每一个Rust文件(默认)No指定许可证模版路径

【描述】

每个文件都应该包含许可证引用。为项目选择合适的许可证版本.(比如, Apache 2.0, BSD, LGPL, GPL)。

可以通过 rustfmtlicense_template_path 配置项 和 license.template来自动化此操作。

【示例】

【正例】

来自 TiKV 项目的案例。可以命名为.rustfmt.license-template许可证模版。


#![allow(unused)]
fn main() {
// Copyright {\d+} TiKV Project Authors. Licensed under Apache-2.0.
}

rustfmt.toml 中配置:

license_template_path = ".rustfmt.license-template"

在代码文件中手工添加对应的注释 (自动插入功能还未支持):


#![allow(unused)]
fn main() {
// Copyright 2021 TiKV Project Authors. Licensed under Apache-2.0.
}

G.CMT.04 在注释中使用 FIXMETODO 来帮助任务协作

【级别:建议】

建议按此规范执行。

【rustfmt 配置】

此规则 Clippy 不可检测,由 rustfmt 自动格式化。

rustfmt 配置:

对应选项可选值是否 stable说明
report_fixmeNever(默认),Unnumbered(推荐)No是否报告 FIXME 注释
report_todoNever(默认),Unnumbered(推荐)No是否报告 FIXME 注释

【描述】

通过在注释中开启 FIXMETODO 可以方便协作。rustfmt 默认不开启该项,所以需要配置。

但是配置为 Always 没必要,只需要配置为 Unnumbered 针对编号的 FXIMETODO 报告即可。

这两个配置目前有 Bug ,无法正确识别报告,但不影响这个规则的应用。

【示例】

【正例】


#![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.05 在 公开的返回Result类型返回的函数文档中增加 # Error 注释

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group默认 level
missing_errors_doc yesnoStyleallow

【描述】

在公开(pub)的返回Result类型函数文档中,建议增加 # Error 注释来解释该函数返回的错误类型,方便用户处理错误。

说明: 该规则通过 cargo clippy 来检测。默认不会警告。

【示例】

【正例】


#![allow(unused)]
fn main() {
use std::io;
/// # 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!();
}
}

【反例】


#![allow(unused)]
fn main() {
use std::io;
pub fn read(filename: String) -> io::Result<String> {
    unimplemented!();
}
}

G.CMT.06 在 公开的函数文档中增加 # Panic 注释

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group默认 level
missing_panics_doc yesnoStyleallow

【描述】

在公开(pub)函数文档中,建议增加 # Panic 注释来解释该函数在什么条件下会 Panic,便于使用者进行预处理。

说明: 该规则通过 cargo clippy 来检测。默认不会警告。

【示例】

【正例】


#![allow(unused)]
fn main() {
/// # 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
    }
}
}

【反例】


#![allow(unused)]
fn main() {
pub fn divide_by(x: i32, y: i32) -> i32 {
    if y == 0 {
        panic!("Cannot divide by 0")
    } else {
        x / y
    }
}
}

G.CMT.07 在 文档注释中要使用 空格代替 tab

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group默认 level
tabs_in_doc_comments yesnoStylewarn

【描述】

Rust 代码风格中提倡使用空格代替tab,在文档注释中也应该统一使用空格。

【示例】

【正例】


#![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,
}
}

【反例】

下面文档注释中使用了 tab。


#![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,
}

}

参考

  1. RFC 505: API 注释约定
  2. RFC 1574: API 文档约定
  3. Making Great Docs with Rustdoc
  4. Rust Doc book

编程实践

常量

在 Rust 中,常量有两种用途:

  • 编译时常量(Compile-time constants)
  • 编译时求值 (CTEF, compile-time evaluable functions)

常量命名风格指南请看 编码风格-命名


G.CNS.01 对于科学计算中涉及浮点数近似值的常量要尽量使用预定义常量

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
approx_constantyesnoCorrectnessdeny

该 Lint 默认为 deny,但在某些场景下,可以设置为allow.

【描述】

【正例】


#![allow(unused)]
fn main() {
let x = std::f32::consts::PI;
let y = std::f64::consts::FRAC_1_PI;
}

【反例】


#![allow(unused)]
fn main() {
let x = 3.14;
let y = 1_f64 / x;
}

G.CNS.02 不要断言常量布尔类型

【级别:必须】

必须按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
assertions_on_constantsyesnoStylewarn

【描述】

因为有可能被编译器优化掉。最好直接使用 panic! 代替。

【正例】


#![allow(unused)]
fn main() {
panic!("something");
}

【反例】


#![allow(unused)]
fn main() {
const B: bool = false;
assert!(B);
}

【例外】

该示例需要维护一个常量的不变性,确保它在未来修改时不会被无意中破坏。类似于 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);
}

G.CNS.03 不要将内部可变性容器声明为常量

【级别:必须】

必须按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
borrow_interior_mutable_constyesnoStylewarn
declare_interior_mutable_constyesnoStylewarn

【描述】

【正例】


#![allow(unused)]
fn main() {
use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
const CONST_ATOM: AtomicUsize = AtomicUsize::new(12);

// Good.
static STATIC_ATOM: AtomicUsize = CONST_ATOM;
STATIC_ATOM.store(9, SeqCst);
assert_eq!(STATIC_ATOM.load(SeqCst), 9); // use a `static` item to refer to the same instance
}

【反例】


#![allow(unused)]
fn main() {
use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
const CONST_ATOM: AtomicUsize = AtomicUsize::new(12);

// Bad.
CONST_ATOM.store(6, SeqCst); // the content of the atomic is unchanged
assert_eq!(CONST_ATOM.load(SeqCst), 12); // because the CONST_ATOM in these lines are distinct

}

G.CNS.04 不要在常量定义中增加显式的 'static 生命周期

【级别:必须】

必须按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
redundant_static_lifetimesyesnoStylewarn

【描述】

没必要加。

【正例】


#![allow(unused)]
fn main() {
const FOO: &[(&str, &str, fn(&Bar) -> bool)] = &[...]
 static FOO: &[(&str, &str, fn(&Bar) -> bool)] = &[...]
}

【反例】


#![allow(unused)]
fn main() {
const FOO: &'static [(&'static str, &'static str, fn(&Bar) -> bool)] =
&[...]
static FOO: &'static [(&'static str, &'static str, fn(&Bar) -> bool)] =
&[...]
}

G.CNS.05 对于函数或方法应尽可能地使用 const fn

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
missing_const_for_fnyesnoPerfwarn

【描述】

【正例】


#![allow(unused)]
fn main() {
const fn new() -> Self {
    Self { random_number: 42 }
}
}

【反例】


#![allow(unused)]
fn main() {
fn new() -> Self {
    Self { random_number: 42 }
}
}

G.CNS.06 注意避免将量大的数据结构定义为常量

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制化参考】

这条规则如果需要定制Lint,则需要找出每个定义的常量再判断其空间占用,或可直接排除基础类型以外的数据类型。

【描述】

常量会内联到使用它的地方而静态变量不会内联,它是全局的,且有一个引用地址。 当创建一个很大的常量数组时,应该考虑将其换成静态变量,因为常量会到处内联。

【示例】

fn main() {
    static MONTHS: [&str; 12] = ["January", "Feburary", "March", "April",
                                "May", "June", "July", "August",
                                "September", "October", "November", "December"];
}

【反例】

fn main() {
    const MONTHS: [&str; 12] = ["January", "Feburary", "March", "April",
                                "May", "June", "July", "August",
                                "September", "October", "November", "December"];
}

静态变量


G.STV.01 不要直接使用可变静态变量作为全局变量

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制化参考】

这条规则如果需要定制 Lint,则可以先检查代码是否使用到 FFI,然后再检测代码中是否有已定义为可变的静态变量(static mut),以及其是否用在用于调用外部函数上,若此条件不达标则发出告警。

【描述】

对可变静态变量进行全局修改是 Unsafe 的。在多线程应用中,修改静态变量会导致数据争用(data race),此未定义行为目前并不会被Clippy或Rustc检测出。

【反例】


#![allow(unused)]
fn main() {
static mut NUM_OF_APPLES: i32 = 0;

unsafe fn buy_apple() {
    NUM_OF_APPLES += 1;
}

unsafe fn eat_apple() {
    NUM_OF_APPLES -= 1;
}
}

【例外】

在使用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();
    }
}

变量

这里所说变量是指局部变量。默认情况下,Rust 会强制初始化所有值,以防止使用未初始化的内存。

变量命名风格指南请看 编码风格-命名


P.VAR.01 非必要不要像 C 语言那样先声明可变变量然后再去赋值

【描述】

不要先声明一个可变的变量,然后再后续过程中去改变它的值。一般情况下,声明一个变量的时候,要对其进行初始化。如果后续可能会改变其值,要考虑优先使用变量遮蔽(继承式可变)功能。如果需要在一个子作用域内改变其值,再使用可变绑定或可变引用。

P.VAR.02 避免大量栈分配

【描述】

Rust 默认在栈上存储。局部变量占用过多栈空间,会栈溢出。

P.VAR.03 禁止将局部变量的引用返回函数外

【描述】

局部变量生命周期始于其声明终于其作用域结束。如果在其生命周期之外被引用,则程序的行为是未定义的。当然,Rust 编译器也会阻止你这么干。

P.VAR.04 变量的命名中不需要添加类型标识

【描述】

因为 Rust 语言类型系统崇尚显式的哲学,所以不需要在变量命名中也添加关于类型的标识。

【正例】


#![allow(unused)]
fn main() {
let account: Vec<u8> = read_some_input();   // account 的类型很清楚
let account = String::from_utf8(account)?;  // account 的类型很清楚
let account: Account = account.parse()?;   // account 的类型很清楚
}

【反例】


#![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`
}

P.VAR.05 利用变量遮蔽功能保证变量安全使用

【描述】

在某些场景,可能会临时准备或处理一些数据,但在此之后,数据只用于检查而非修改。

那么可以将其通过变量遮蔽功能,重写绑定为不可变变量,来表明这种 临时可变,但后面不变的意图。

【正例】


#![allow(unused)]
fn main() {
let mut data = get_vec();
data.sort(); // 临时需要排序
let data = data; //  后面就不需要改动了,由编译器可以确保

// Here `data` is immutable.
}

【反例】


#![allow(unused)]
fn main() {
let data = { 
    let mut data = get_vec();
    data.sort();
    data // 虽然后面不再改动,但代码语义上没有表现出来先改变,后不变那种顺序语义
};

// Here `data` is immutable.
}

G.VAR.01 交换两个变量的值应该使用 std::mem::swap 而非赋值

【级别:必须】

必须按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
almost_swappedyesnoCorrectnessdeny

【描述】

【正例】


#![allow(unused)]
fn main() {
let mut a = 1;
let mut b = 2;
std::mem::swap(&mut a, &mut b);
}

【反例】


#![allow(unused)]
fn main() {
let mut a = 1;
let mut b = 2;
a = b;
b = a;  
}

G.VAR.02 使用解构元组方式定义多个变量时不要使用太多单个字符来命名变量

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
many_single_char_namesyesnopedanticallow

【描述】

在解构元组的方式定义多个变量时,有时候变量命名可能是无特别语义的,比如临时值,可以用简单的单个字符来定义变量名,但是不宜太多。

该 lint 对应 clippy.toml 配置项:

# 修改可以绑定的单个字符变量名最大数量。默认为 4
single-char-binding-names-threshold=4

【正例】

超过四个的,就需要起带语义的命名。


#![allow(unused)]
fn main() {
let (a, b, c, d) = (...);
let (width, high, len, shape, color, status) = (...);
}

【反例】


#![allow(unused)]
fn main() {
let (a, b, c, d, e, f, g) = (...);
}

G.VAR.03 通常不要使用非 ASCII 字符作为标识符

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
rustc-lint: non-ascii-identsnoyespedanticallow

【描述】

Rust 语言默认支持 Non ASCII 字符作为合法标识符。但是,为了统一团队代码风格,建议使用最常用的 ASCII 字符作为合法标识符。

另外,只有使用英文的命名才能让命名相关的 Lint 生效。

【正例】

#[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" }

【反例】

#[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" }

G.VAR.04 不要在子作用域中使用变量遮蔽功能

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制化参考】 这条规则如果需要定制Lint,则可以分别扫描当前作用域和子作用域中的变量,判断是否存在同名问题。

【描述】

当两个作用域存在包含关系时,不要使用变量遮蔽功能,即在较小的作用域内定义与较大作用域中相同的变量名,以免引起逻辑Bug。

【示例】 【反例】

fn main(){
    let mut a = 0;
    {
        // do something
        a = 42;
        
        // bug
        // let a = 42;
    }
    
    a; // use a again
}

G.VAR.05 不要在当前作用域使用变量遮蔽功能

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
shadow_reuseyesnorestrictionallow
shadow_sameyesnorestrictionallow
shadow_unrelatedyesnorestrictionallow

【描述】

在同一个作用域中,非必要时不要通过新变量声明遮蔽旧变量声明的方式来修改变量。

【正例】


#![allow(unused)]
fn main() {
let x = 2;
let y = x + 1; // 不改变x的值,声明新的变量y

let y = &x; // 不改变x的绑定,声明新的变量

let w = z; // 使用不同的名字
}

【反例】


#![allow(unused)]
fn main() {
let x = 2;
let x = x + 1; // 将会改变x的值

let x = &x; // 只是改变引用级别

let x = y; // 更早的绑定
let x = z; // 遮蔽了更早的绑定
}

G.VAR.06 避免局部变量导致的大量栈分配

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制化参考】 这条规则如果需要定制Lint,则可以分别检测每个局部变量占用的栈空间,并统计总体占用情况,进行告警。

【描述】

Rust 局部变量默认分配在栈上。当局部变量占用栈空间过大时,可以采用Box使变量在堆上分配

【正例】


#![allow(unused)]
fn main() {
let _: Box<[i32; 8000]> = Box::new([1; 8000]);
}

【反例】


#![allow(unused)]
fn main() {
let _: [i32; 8000] = [1; 8000];
}

G.VAR.07 避免在变量的命名中添加类型标识

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制化参考】 这条规则如果需要定制Lint,则可以获取变量命名的结尾部分和变量类型,进行匹配,判断是否重复。

【描述】

变量的名字需要保持简洁清晰,不需要在变量命名中也添加关于类型的标识。

【正例】


#![allow(unused)]
fn main() {
let count: i32 = 12;   // count 的类型已经很清楚
}

更多例子: P.VAR.04 变量的命名中不需要添加类型标识

【反例】


#![allow(unused)]
fn main() {
let count_int: i32 = 12;   // count 的类型已经很清楚,没必要在命名中加 `_int`
}

更多例子: P.VAR.04 变量的命名中不需要添加类型标识

数据类型

数据类型记录 Rust 标准库提供的 原生类型,以及结构体和枚举体等编码实践。


P.TYP.01 类型转换要尽量使用安全的方式

【描述】

Rust 中的类型转换有多种方式,包括 as 强转、From/Into安全转换函数、Deref、以及 Unsafe 的 std::mem::transmute 等。在使用类型转换的时候,要注意场景,选择合适的方式和安全条件,不要让转换产生未定义行为。

P.TYP.02 对数组和集合容器进行索引要使用 usize 类型

【描述】

Rust 中只允许索引为 usize 类型,因为:

  1. 负索引是无意义的。
  2. usize和 裸指针大小相同,意味着指针算法不会有任何隐藏的强制转换
  3. std::mem::size_of()std::mem::align_of() 的函数返回 usize 类型。
  4. usize 不会因为平台架构的切换而导致索引值被截断的问题,比如 将u32类型的索引 用到 16位大小的嵌入式平台就会出问题。

P.TYP.03 必要时,应该使得类型可以表达更明确的语义,而不是只是直接使用原生类型

【描述】

这样可以增加代码的可读性。

【正例】

struct Years(i64);

fn main() {
    let years = Years(1942);
    let years_as_primitive_1: i64 = years.0; // Tuple
    let Years(years_as_primitive_2) = years; // Destructuring
}

【反例】

fn main() {
    let years = 1942;
}

G.TYP.01 类型转换尽可能使用安全的转换函数代替 as

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
as_conversionsyesnorestrictionallow
cast_losslessyesnopedanticallow
cast_possible_truncationyesnopedanticallow
cast_possible_wrapyesnopedanticallow
cast_precision_lossyesnopedanticallow
cast_sign_lossyesnopedanticallow
fn_to_numeric_castyesnoStylewarn
fn_to_numeric_cast_with_truncationyesnoStylewarn
char_lit_as_u8yesnoComplexitywarn
cast_ref_to_mutyesnocorrectnessdeny
ptr_as_ptryesnopedanticallow

【描述】

Rust 的 as 转换包含了「静默的有损转换(lossy conversion)」。诸如 i32::from 之类的转换函数只会执行无损转换(lossless conversion)。 如果输入表达式的类型发生变化,使用转换函数可以防止转换变成无声的有损转换,并使阅读代码的人更容易知道转换是无损的。

G.TYP.02 数字字面量在使用的时候应该明确标注好类型

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
default_numeric_fallbackyesnorestrictionallow

【描述】

如果数字字面量没有被指定具体类型,那么单靠类型推导,整数类型会被默认绑定为 i32 类型,而浮点数则默认绑定为 f64类型。这可能导致某些运行时的意外。

【正例】


#![allow(unused)]
fn main() {
let i = 10u32;
let f = 1.23f32;
}

【反例】


#![allow(unused)]
fn main() {
let i = 10; // i32
let f = 1.23; // f64
}

G.TYP.03 不要用数字类型边界值去判断能否安全转换,而应该使用 try_from 相关方法

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
checked_conversionsyesnopedanticallow

【描述】

在 Rust 中 From 代表不能失败的转换,而 TryFrom 则允许返回错误。

一般在数字类型转换的时候,不需要防御式地去判断数字大小边界,那样可读性比较差,应该使用 try_from 方法,在无法转换的时候处理错误即可。

【正例】


#![allow(unused)]
fn main() {
let foo: u32 = 5; 
let f = i16::try_from(foo).is_ok(); // 返回 false
}

【反例】


#![allow(unused)]
fn main() {
let foo: u32 = 5;
let _ = foo <= i16::MAX as u32; // 等价于 let _ = foo <= (i32::MAX as u32);
}

单元类型

Rust 中单元类型为零大小类型。其类型签名和值都为 (),它也是一个空元组。


G.TYP.Unit.01 当函数不关心返回值但要处理错误时应使用单元类型

【级别:建议】

建议按此规范执行。

【Lint检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制化参考】

可以检测使用 Option<T> 包含 Error 类型的情况,发出警告。

【描述】

单元类型代表 无返回值。当返回类型无返回值但要处理错误时,应使用Result<(), Error>类型,

而非Option类型。

【正例】


#![allow(unused)]
fn main() {
// 表示该函数要么成功,要么返回各自错误
fn f() -> Result<(), Error> {
    
    // ...
    
    // Error handle
    Ok(())
}
}

【反例】


#![allow(unused)]
fn main() {
fn f() -> Option<Error> {
    
    // ...
    
    None
}
}

布尔

P.TYP.Bool.01 不要使用数字来代替 布尔值

【描述】

Rust 中布尔值就是 truefalse。 不要试图使用数字 10 来代替布尔值。

虽然 布尔值 可以强转为 对应的数字,但是反之则不行。

不要通过判断数字来代替 布尔值,除非是 FFi 场景通过 C-ABI 和其他语言打交道。

【正例】


#![allow(unused)]
fn main() {
let a = true;
let b = false;
assert_eq!(1, a as u32);
assert_eq!(0, b as u32);
}

【反例】


#![allow(unused)]
fn main() {
let a = 1;
let b = 0;
assert_eq!(true, a == 1);  
assert_eq!(false, b == 0);
}

G.TYP.Bool.01 返回为布尔值的表达式或函数值不需要和布尔字面量进行比较

【级别:必须】

必须按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
bool_comparison yesnocomplexitywarn
bool_assert_comparison yesnostylewarn
needless_bool yesnocomplexitywarn
nonminimal_bool yesnocomplexitywarn
needless_bitwise_bool yesnopedanticallow
assertions_on_constants yesnopedanticwarn

【描述】

在 Rust 中,返回为布尔值的表达式或函数值可以直接当作布尔值使用。

总之,使用布尔表达式的时候,要尽可能地简洁明了。

【正例】


#![allow(unused)]
fn main() {
if x {}
if !y {}

assert!(!"a".is_empty());
}

【反例】


#![allow(unused)]
fn main() {
if x == true {}
if y == false {}

assert_eq!("a".is_empty(), false);
assert_ne!("a".is_empty(), true);
}

G.TYP.Bool.02 使用多个布尔表达式条件的时候要避免引入不必要的条件

【级别:必须】

必须按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
logic_bug yesnocorrectnessdeny

【描述】

【反例】


#![allow(unused)]
fn main() {
if a && b || a { ... }
}

该示例中,条件 b 是不需要的,它等价于 if a {...}

G.TYP.Bool.03 如果 match 匹配表达式为布尔类型,建议使用 if 表达式来代替

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
logic_bug yesnocorrectnessdeny
match_boolyesnopedanticallow

【描述】

对于布尔表达式更倾向于使用 if ... else ...,相比较 match 模式匹配更有利于代码可读性。

【正例】


#![allow(unused)]
fn main() {
fn foo() {}
fn bar() {}
let condition: bool = true;
if condition {
    foo();
} else {
    bar();
}
}

【反例】


#![allow(unused)]
fn main() {
fn foo() {}
fn bar() {}
let condition: bool = true;
match condition {
    true => foo(),
    false => bar(),
}
}

G.TYP.Bool.04 不要尝试将数字类型转换为布尔值

【级别:必须】

必须按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
transmute_int_to_bool yesnocomplexitywarn

【描述】

这可能会让布尔值在内存中的表示无效。

【反例】


#![allow(unused)]
fn main() {
let x = 1_u8;
unsafe {
    let _: bool = std::mem::transmute(x); // where x: u8
}

}

G.TYP.Bool.05 不要在 if 表达式条件中使用块(block)结构

【级别:必须】

必须按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
blocks_in_if_conditions yesnostylewarn

【描述】

为了增加可读性。

【正例】


#![allow(unused)]
fn main() {
if true { /* ... */ }

fn somefunc() -> bool { true };
let res = { let x = somefunc(); x };
if res { /* ... */ }
}

【反例】


#![allow(unused)]
fn main() {
if { true } { /* ... */ }

fn somefunc() -> bool { true };
if { let x = somefunc(); x } { /* ... */ }
}

G.TYP.Bool.06 非必要时,布尔运算优先使用 逻辑运算符( &&/||)而非 位运算符 (&/|)

【级别:必须】

必须按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
needless_bitwise_boolyesnopedanticallow

【描述】

位运算不支持短路(short-circuiting),所以会影响性能。逻辑运算符则支持短路。

【正例】


#![allow(unused)]
fn main() {
let (x,y) = (true, false);
if x && !y {} //  逻辑运算符,支持短路
}

【反例】


#![allow(unused)]
fn main() {
let (x,y) = (true, false);
if x & !y {} //  位运算符,不支持短路
}

字符

在 Rust 中,字符是一个合法的 Unicode 标量值(Unicode scalar value),一个字符大小为 4 字节,对应一个 Unicode 码位(CodePoint)。


G.TYP.Char.01 不要将 字符字面量强转为 u8 去使用

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
char_lit_as_u8yesnocomplexitywarn

【描述】

应该使用 字节字面量 去代替 将 字符字面量强转为 u8

【正例】


#![allow(unused)]
fn main() {
b'x'
}

【反例】


#![allow(unused)]
fn main() {
'x' as u8
}

G.TYP.Char.02 字符串方法中如果需要单个字符的值作为参数,最好使用字符而非字符串

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
single_char_patternyesnoperfwarn

【描述】

使用 字符 比 用字符串性能更好一些。

【正例】


#![allow(unused)]
fn main() {
let s = "yxz";
s.split('x');
}

【反例】


#![allow(unused)]
fn main() {
let s = "yxz";
s.split("x");
}

G.TYP.Char.03 当需要将整数转换为字符时,请使用安全转换函数,而非 transmute

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
transmute_int_to_charyesnocomplexitywarn

【描述】

并非每个整数都对应一个合法的 Unicode 标量值,使用 transmute 转换会有未定义行为。

【正例】


#![allow(unused)]
fn main() {
let x = 37_u32;
unsafe {
    let x = std::char::from_u32(x).unwrap(); // 请按情况处理 None
    // let x = std::char::from_u32_unchecked(x);  // 如果确定该整数对应合法的unicode,可以使用 uncheck 方法加速
    assert_eq!('%', x);
}
}

【反例】


#![allow(unused)]
fn main() {
let x = 37_u32;
unsafe {
    let x: char = std::mem::transmute(x); // where x: u32
    assert_eq!('%', x);
}
}

整数

Rust 中有目前有 十二种整数类型:i8/u8, i16/u16, i32/u32, i64/u64, i128/u128, isize/usize


G.TYP.INT.01 在用整数计算的时候需要考虑整数溢出、回绕和截断的风险

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
integer_arithmeticyesnorestrictionallow
manual_saturating_arithmeticyesnostylewarn

【描述】

需要结合场景和业务来考虑如果发生溢出、回绕或截断的时候,是否会引起严重的问题。

比如,对于时间要求精准的系统,如果在计算时间发生整数溢出,或者去计算某个数组的索引等,那可能会发生严重问题。但如果你只是一个简单的计算器,不会被用到具体的业务场合,那溢出也没有关系,因为你只需要在合理的数字范围内计算性能最好。

在 Rust 标准库中,提供 add/ checked_add / saturating_add/overflowing_add / wrapping_add 不同系列方法,返回值不同,根据不同的场合选择适合的方法。

  1. check_*函数返回Option,一旦发生溢出则返回None。
  2. saturating_*系列函数返回类型是整数,如果溢出,则给出该类型可表示范围的“最大/最小”值。
  3. wrapping_*系列函数则是直接抛弃已经溢出的最高位,将剩下的部分返回。

Rust 编译器在编译时默认没有溢出检查(可通过编译参数来引入),但在运行时会有 Rust 内置 lint (#[deny(arithmetic_overflow)])来检查,如果有溢出会 Panic。

无符号整数使用时要注意回绕(wrap around),不同整数类型转换时需注意截断。

【正例】


#![allow(unused)]
fn main() {
assert_eq!((-5i32).checked_abs(), Some(5));
assert_eq!(100i32.saturating_add(1), 101);
}

【反例】


#![allow(unused)]
fn main() {
assert_eq!((-5i32).abs(), 5);
assert_eq!(100i32+1, 101);
}

G.TYP.INT.02 对于大整数字面量使用十六进制表示比十进制更好

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
decimal_literal_representationyesnorestrictionallow

【描述】

【正例】


#![allow(unused)]
fn main() {
let a = `0xFF`
let b = `0xFFFF`
let c = `0xF0F0_F0F0
}

【反例】


#![allow(unused)]
fn main() {
let a = `255` 
let b = `65_535`
let c =`4_042_322_160` 
}

G.TYP.INT.03 避免将有符号整数和无符号整数之间强制转换

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
cast_sign_lossyesnopedanticallow

注意:默认情况下该 lint 是 allow,如果需要检查这种转换,则需要设置为 warndeny

【描述】

当有符号整数被强制转换为无符号整数时,负值会发生回绕,变成更大的正值,这在实际应用时有可能助长缓冲区溢出风险。

【正例】


#![allow(unused)]
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);
}

【反例】


#![allow(unused)]
fn main() {
let y: i8 = -1;
y as u128; // will return 18446744073709551615
}

G.TYP.INT.04 对负数取模计算的时候不要使用 %

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
modulo_arithmeticyesnorestrictionallow

【描述】

Rust 当前的这个 %形式是余数运算符,它的行为与CJava等语言中相同符号的运算符相同。它也类似于PythonHaskell等语言中的模(modulo)运算符,只是它对 负数 的行为不同:余数是基于截断除法,而模运算是基于向下取整(floor)除法。

【正例】

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);
}

【反例】

fn main() {
    let a: i32 = -1;
    let b: i32 = 6;
    // 余数运算符只是返回第一个操作数除以第二个操作数的余数。所以 -1/6 给出 0,余数为 -1
    assert_eq!(a % b, -1);
}

浮点数

Rust 的浮点数包括 f32f64 两种类型。Rust 编译器默认推断的 Float 类型是 f64


G.TYP.Float.01 使用 f32 字面量时,小心被 Rust 编译器截断

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
excessive_precisionyesnostylewarn

【描述】

当指定超过 f32 精度的字面量值时,Rust 会默认截断该值。

【正例】


#![allow(unused)]
fn main() {
let v: f64 = 0.123_456_789_9;
println!("{}", v); //  0.123_456_789_9
}

【反例】


#![allow(unused)]
fn main() {
let v: f32 = 0.123_456_789_9;
println!("{}", v); //  0.123_456_789
}

G.TYP.Float.02 当从任何数字类型转换为 f64类型时需要注意是否会损失精度

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
cast_precision_lossyesnopedanticallow

【描述】

这种转换可能会有值的舍入错误发生。在某些对于精度要求比较高的场景需要注意。

【示例】


#![allow(unused)]
fn main() {
let x = u64::MAX;
x as f64; // 18446744073709551615
}

G.TYP.Float.03 不要对浮点数进行运算和比较

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
float_arithmeticyesnorestrictionallow
float_cmpyesnopedanticallow
float_cmp_constyesnorestrictionallow
float_equality_without_absyesnosuspiciouswarn

【描述】

浮点数计算通常都是不精确的,直接对浮点数进行运算和比较都是自找麻烦。 如何更好地处理浮点数,可以参阅 浮点数指南

【正例】


#![allow(unused)]
fn main() {
let x = 1.2331f64;
let y = 1.2332f64;

let error_margin = f64::EPSILON; // Use an epsilon for comparison
// Or, if Rust <= 1.42, use `std::f64::EPSILON` constant instead.
// let error_margin = std::f64::EPSILON;
if (y - 1.23f64).abs() < error_margin { }
if (y - x).abs() > error_margin { }

// or
pub fn is_roughly_equal(a: f32, b: f32) -> bool {
    (a - b).abs() < f32::EPSILON
}
}

【反例】


#![allow(unused)]
fn main() {
let x = 1.2331f64;
let y = 1.2332f64;

if y == 1.23f64 { }
if y != x {} // where both are floats

// or
pub fn is_roughly_equal(a: f32, b: f32) -> bool {
    (a - b) < f32::EPSILON
}
}

G.TYP.Float.04 尽量使用 Rust 内置方法来处理浮点数计算

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
imprecise_flopsyesnonurseryallow
suboptimal_flopsyesnonurseryallow

【描述】

内置方法会牺牲一定性能,但它可以提升准确性。

【正例】


#![allow(unused)]
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();
}

【反例】


#![allow(unused)]
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
};
}

G.TYP.Float.05 在定义f32浮点数字面量时,要注意它会损失精度,尽量使用 f64 类型

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
imprecise_flopsyesnonurseryallow

【描述】

f32 浮点数字面量在定义时,将会损失精度,应该尽量使用 f64 类型。

【正例】


#![allow(unused)]
fn main() {
let x : f64 = 16_777_217.0;
assert_eq!(16777217.0, x);
}

【反例】


#![allow(unused)]
fn main() {
let x : f32 = 16_777_217.0;
assert_eq!(16777216.0, x);
}

G.TYP.Float.06 浮点数和整数之间转换时不要使用 transmute

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
transmute_float_to_intyesnocomplexitywarn
transmute_int_to_floatyesnocomplexitywarn

【描述】

【正例】


#![allow(unused)]
fn main() {
let _: f32 = f32::from_bits(1_u32);
let _: u32 = 1f32.to_bits();
}

【反例】


#![allow(unused)]
fn main() {
unsafe {
    let _: u32 = std::mem::transmute(1f32);
    let _: f32 = std::mem::transmute(1_u32); // where x: u32
}
}

引用

在 Rust 中,引用就是有借用检查的指针,就像穿着“安全的外衣”。指针,没有借用检查,所以也叫裸指针。

Rust 编译器总是希望引用是非空且对齐的。


P.REF.01 使用引用的时候要注意其生命周期不要重合

【描述】

在使用 引用的时候,要注意分析其生命周期,不可变借用和可变借用之间,以及可变借用之间不要有重叠。

【正例】

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);
    
}

【反例】

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

切片

切片(slice)允许开发者引用集合中连续的元素序列,类型签名用 [T]表示,但因为它是动态大小类型(DST),所以一般用 &[T] 表示切片。

&str 就是一种字符串切片。


P.TYP.Slice.01 利用切片迭代器来代替手工索引

【描述】

在 for 循环中使用索引是比较常见的编程习惯,但是这种方式是最有可能导致边界错误的。

利用 切片自带的方法,并利用迭代器,可以避免这种错误。

【正例】


#![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);
}
}

【反例】


#![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);
]
}

P.TYP.Slice.02 可以利用切片模式来提升代码的可读性

【描述】

切片也支持模式匹配,适当应用切片模式,可以有效提升代码可读性。

【正例】

利用切片模式编写判断回文字符串的函数。代码来自于: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,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn known_palindromes() {
        assert!(word_is_palindrome(""));
        assert!(word_is_palindrome("a"));
        assert!(word_is_palindrome("aba"));
        assert!(word_is_palindrome("abba"));
    }

    #[test]
    fn not_palindromes() {
        assert!(!word_is_palindrome("abc"));
        assert!(!word_is_palindrome("abab"));
    }
}

}

元组

元组是异构复合类型,可以存储多个不同的值。


P.TYP.Tuple.01 可以使用元组解构来同时定义多个变量

【描述】

可以利用元组解构的特性,来更好地精简代码。

【正例】

struct A(i32, i32);

fn hello( A(a, b): A){
    println!("{}, {}", a, b);
}

fn main(){
    let a = A(1, 2) ;
    hello(a);
}

G.TYP.Tuple.01 使用元组时,其元素最多不要超过3个

【级别:建议】

建议按此规范执行。

【Lint检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制化参考】

可以检测元组中元素个数,如果超过 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]


P.TYP.Array.01 当数组长度在编译期就已经确定,应该优先使用固定长度数组,而非动态数组( Vec<T>

【描述】

固定长度数组会根据元素类型,优先选择存储在栈上,可以优化内存分配。

当编译期长度可以确定,但长度并不是唯一确定的,那么可以考虑使用常量泛型。注意:常量泛型特性从 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();
}


G.TYP.Array.01 当创建大的全局数组时应该使用静态变量而非常量

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
large_const_arraysyesnoperfwarn
large_stack_arraysyesnopedanticallow

注意: large_stack_arrays 会检查在栈上分配的大数组,但clippy默认是 allow,根据实际使用场景决定是否针对这种情况发出警告。

【描述】

因为常量会内联,对于大的数组,使用静态变量定义更好。

【正例】


#![allow(unused)]
fn main() {
pub static A: [u32;1_000_000] = [0u32; 1_000_000];
}

【反例】


#![allow(unused)]
fn main() {
pub const A: [u32;1_000_000] = [0u32; 1_000_000];
}

G.TYP.Array.02 使用数组索引时不要越界访问

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
out_of_bounds_indexingyesnocorrectnessdeny

【描述】

越界访问在运行时会 Panic!

【正例】


#![allow(unused)]
fn main() {
let x = [1, 2, 3, 4];
x[0];
x[3];
}

【反例】


#![allow(unused)]
fn main() {
let x = [1, 2, 3, 4];
x[9];
&x[2..9];
}

G.TYP.Array.03 当数组元素为原生数据类型(Primitive)的值时,对其排序应该优先考虑非稳定排序

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
stable_sort_primitiveyesnoperfwarn

当确实需要稳定排序时,需要修改该 lint 的设置为 allow

【描述】

因为稳定排序会消耗更多的内存和 CPU 周期,相对而言,非稳定排序性能更佳。

当然,在必须要稳定排序的场合,不应该使用非稳定排序。

【正例】


#![allow(unused)]
fn main() {
let mut vec = vec![2, 1, 3];
vec.sort_unstable(); // unstable sort
}

【反例】


#![allow(unused)]
fn main() {
let mut vec = vec![2, 1, 3];
vec.sort();  // stable 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(); // The business requirement here is to use stable sort 
        // ...
}
}

动态数组

这里指可以动态增长的数组Vec<T>

在数组一节中有部分原则和规则也适用于这里。


P.TYP.Vec.01 非必要不要使用动态数组

【描述】

相关原则参见数组一节中有对应原则,非必须不要使用 Vec<T>,应该优先尝试使用固定长度数组或常量泛型。

P.TYP.Vec.02 创建动态数组时,可以预先分配大约足够的容量来避免后续操作中产生多次分配

【描述】

预分配足够的容量,避免后续内存分配,可以提升代码性能。

【正例】


#![allow(unused)]
fn main() {
let mut output = Vec::with_capacity(input.len());
}

【反例】


#![allow(unused)]
fn main() {
let mut output = Vec::new();
}

G.TYP.Vec.01 禁止访问还未初始化的数组

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
uninit_vecyesnocorrectnessdeny

【描述】

访问未初始化数组的内存会导致 未定义行为。

【正例】


#![allow(unused)]
fn main() {
let mut vec: Vec<u8> = vec![0; 1000];
reader.read(&mut vec);

// or
let mut vec: Vec<MaybeUninit<T>> = Vec::with_capacity(1000);
vec.set_len(1000);  // `MaybeUninit` can be uninitialized

// or
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
}

【反例】


#![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
}

结构体

Rust 包含了三种结构体: 命名结构体、元组结构体、单元结构体。


P.TYP.Struct.01 为结构体实现构造性方法时,应该避免构造后再初始化的情况

【描述】

跟其他OOP 或 FP 语言不一样, Rust 的惯用方式是构建即初始化。

【正例】


#![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 })
  }
}
}

【反例】


#![allow(unused)]
fn main() {
// 先构建
let mut dict = Dictionary::new();
// 后初始化
dict.load_from_file("./words.txt")?;
}

P.TYP.Struct.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.Struct.03 结构体实例需要默认实现时,建议为其实现 Default

【描述】

为结构体实现 Default 对于简化代码提高可读性很有帮助。

【正例】

use std::{path::PathBuf, time::Duration};

// note that we can simply auto-derive Default here.
#[derive(Default, Debug, PartialEq)]
struct MyConfiguration {
    // Option defaults to None
    output: Option<PathBuf>,
    // Vecs default to empty vector
    search_path: Vec<PathBuf>,
    // Duration defaults to zero time
    timeout: Duration,
    // bool defaults to false
    check: bool,
}

impl MyConfiguration {
    // add setters here
}

fn main() {
    // construct a new instance with default values
    let mut conf = MyConfiguration::default();
    // do something with conf here
    conf.check = true;
    println!("conf = {:#?}", conf);
        
    // partial initialization with default values, creates the same instance
    let conf1 = MyConfiguration {
        check: true,
        ..Default::default()
    };
    assert_eq!(conf, conf1);
}

G.TYP.Struct.01 对外导出的公开的 Struct,建议增加 #[non_exhaustive]属性

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
exhaustive_structsyesnorestrictionallow
manual_non_exhaustiveyesnostylewarn

【描述】

作为对外公开的 结构体,为了保持稳定性,应该使用 #[non_exhaustive]属性,避免因为将来结构体字段发生变化而影响到下游的使用。主要涉及命名结构体和元组结构体。

【正例】


#![allow(unused)]
fn main() {
#[non_exhaustive]
struct Foo {
    bar: u8,
    baz: String,
}
}

【反例】

#[non_exhaustive] 属性稳定之前,社区内还有一种约定俗成的写法来达到防止下游自定义枚举方法。通过 manual_non_exhaustive 可以监控这类写法。


#![allow(unused)]
fn main() {
struct S {
    pub a: i32,
    pub b: i32,
    _priv: (),  // 这里用 下划线作为前缀定义的字段,作为私有字段,不对外公开
}

// 用户无法自定义实现该结构体的方法。
}

【例外】

也有例外情况!

从语义角度看,#[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: (),
        }
    }
}
}

G.TYP.Struct.02 结构体中有超过三个布尔类型的字段,建议将其独立为一个枚举

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
struct_excessive_boolsyesnopedanticallow

该 lint 对应 clippy.toml 配置项:

# 用于配置函数可以拥有的 bool 类型参数最大数量,默认为 3。
max-struct-bools=3 

【描述】

这样有助于提升 代码可读性和 API 。

【正例】


#![allow(unused)]
fn main() {
enum S {
    Pending,
    Processing,
    Finished,
}
}

【反例】


#![allow(unused)]
fn main() {
struct S {
    is_pending: bool,
    is_processing: bool,
    is_finished: bool,
}
}

G.TYP.Struct.03 善用结构体功能更新语法来提升代码可读性

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
field_reassign_with_defaultyesnostylewarn

【描述】

【正例】


#![allow(unused)]
fn main() {
let a = A {
    i: 42,
    .. Default::default()
};
}

【反例】


#![allow(unused)]
fn main() {
let mut a: A = Default::default();
a.i = 42;
}

枚举体

Rust 的枚举是一种带 Tag 的联合体。 一般分为三类:空枚举、无字段(fieldless)枚举和数据承载(data carrying)枚举。

【示例】


#![allow(unused)]
fn main() {
enum Empty {}

enum Fieldless {
    A,
    B,
    C = 42, // 可以自定义判别式
}

enum DataCarrying {
    Foo(i32, i32),
    Bar(String)
}
}

Rust 中枚举体用处很多,你甚至可以将其作为一种接口使用。


P.TYP.Enum.01 修改 Enum 中值的时候建议使用 std::mem::take

【描述】


#![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.TYP.Enum.01 要避免使用and_then而使用map

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
bind_instead_of_map yesnocomplexitywarn

【描述】

为了让代码更加简单明了增强可读性,建议使用 map

【正例】


#![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 });
}

【反例】


#![allow(unused)]
fn main() {
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) });

}

G.TYP.Enum.02 除非必要,不要自己创建空枚举

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
empty_enumyesnopedanticallow

【描述】

在 Rust 中 只有 never 类型(!)才是唯一合法表达 无法被实例化类型 的类型。但目前 never 类型还未稳定,只能在 Nightly 下使用。

【正例】

所以,如果想在 稳定版 Rust 中使用,建议使用std::convert::InfallibleInfallible 枚举是一个合法的 空枚举,常用于错误处理中,表示永远不可能出现的错误。但是目前也可以用于在稳定版中替代 never 类型。


#![allow(unused)]
fn main() {
// 未来 never 类型稳定的话,将会把 Infallible 设置为 never 类型的别名
pub type Infallible = !;
}

【反例】


#![allow(unused)]
fn main() {
enum Test {}
}

【例外】

因为 std::convert::Infallible 默认实现了很多 trait,如果不想依赖其他 trait ,那么可以用 空枚举。


#![allow(unused)]
fn main() {
pub enum NoUserError {}

impl Display for NoUserError {
    fn fmt(&self, _formatter: &mut fmt::Formatter) -> fmt::Result {
        match *self {}
    }
}

}

G.TYP.Enum.03 在使用类似 C 语言的枚举写法且使用repr(isize/usize) 布局时要注意 32位架构上截断的问题

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
enum_clike_unportable_variantyesnocorrectnessdeny

【描述】

在使用类似 C 语言的枚举写法且使用repr(isize/usize) 布局时,在32位架构上会截断变体值,但在64位上工作正常。

但是没有这种风险的时候,可以正常使用。

【正例】

因为当前 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,
}
}

【反例】


#![allow(unused)]
fn main() {
#[cfg(target_pointer_width = "64")]
#[repr(usize)]
enum NonPortable {
    X = 0x1_0000_0000,
    Y = 0,
}
}

G.TYP.Enum.04 一般情况下,不要使用 use 对 Enum 的全部变体(variants)

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
enum_glob_useyesnopedanticallow

【描述】

因为使用 Enum 的类型前缀可以使代码更加可读。

【正例】


#![allow(unused)]
fn main() {
use std::cmp::Ordering;
foo(Ordering::Less)
}

【反例】


#![allow(unused)]
fn main() {
use std::cmp::Ordering::*; // 这里导入了全部变体
foo(Less);
}

【例外】

当枚举体非常多的时候,比如 glutin::event::VirtualKeyCode 这类对应键盘按键的枚举,并且上下文比较明确,都是在处理和 Key 相关的内容时,可以直接全部导入。


#![allow(unused)]
fn main() {
// From:  https://github.com/alacritty/alacritty/blob/master/alacritty/src/config/bindings.rs#L368
#![allow(clippy::enum_glob_use)]

pub fn default_key_bindings() -> Vec<KeyBinding> {
    let mut bindings = bindings!(
        KeyBinding;
        Copy;  Action::Copy;
        Copy,  +BindingMode::VI; Action::ClearSelection;
        Paste, ~BindingMode::VI; Action::Paste;
        L, ModifiersState::CTRL; Action::ClearLogNotice;
        L,    ModifiersState::CTRL,  ~BindingMode::VI, ~BindingMode::SEARCH;
            Action::Esc("\x0c".into());
        Tab,  ModifiersState::SHIFT, ~BindingMode::VI, ~BindingMode::SEARCH;
            Action::Esc("\x1b[Z".into());
        // ...
    }
}

G.TYP.Enum.05 对外导出的公开的 Enum,建议增加 #[non_exhaustive]属性

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
exhaustive_enumsyesnorestrictionallow
manual_non_exhaustiveyesnostylewarn

【描述】

作为对外公开的 Enum,为了保持稳定性,应该使用 #[non_exhaustive]属性,避免因为将来Enum 枚举变体的变化而影响到下游的使用。

【正例】


#![allow(unused)]
fn main() {
#[non_exhaustive]
enum E {
    A,
    B,
}
}

【反例】

#[non_exhaustive] 属性稳定之前,社区内还有一种约定俗成的写法来达到防止下游自定义枚举方法。通过 manual_non_exhaustive 可以监控这类写法。


#![allow(unused)]
fn main() {
enum E {
    A,
    B,
    #[doc(hidden)]
    _C, // 这里用 下划线作为前缀定义的变体,作为隐藏的变体,不对外展示
}
// 用户无法自定义实现该 枚举的方法,达到一种稳定公开枚举的目的。
}

G.TYP.Enum.06 注意 Enum 内变体的大小差异不要过大

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
large_enum_variantyesnoperfwarn

该 lint 可以通过 clippy 配置项 enum-variant-size-threshold = 200 来配置,默认是 200 字节。

【描述】

要注意 Enum 内 变体 的大小差异不要过大,因为 Enum 内存布局是以最大的那个变体进行对齐。根据场景,如果该Enum 实例中小尺寸变体的实例使用很多的话,那内存就会有所浪费。但是如果小尺寸变体的实例使用很少,则无所谓。

解决办法就是把大尺寸变体包到 Box<T>中。

【正例】


#![allow(unused)]
fn main() {
enum Test {
    A(i32),
    B(Box<[i32; 1000]>),
    C(Box<[i32; 8000]>),
}
}

【反例】


#![allow(unused)]
fn main() {
enum Test {
    A(i32),
    B([i32; 1000]),
    C([i32; 8000]),
}
}

表达式

Rust 中几乎一切皆表达式。


G.EXP.01 当需要对表达式求值之后重新赋值时尽量使用复合赋值模式

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
assign_op_patternyesnostylewarn

【描述】

【正例】


#![allow(unused)]
fn main() {
let mut a = 5;
let b = 0;
a += b;
}

【反例】


#![allow(unused)]
fn main() {
let mut a = 5;
let b = 0;
a = a + b;
}

G.EXP.02 避免在比较中使用不兼容的位掩码

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
bad_bit_maskyesnocorrectnessdeny

【描述】

可以对照下面表格进行检查。

ComparisonBit OpExampleis alwaysFormula
== or !=&x & 2 == 3falsec & m != c
< or >=&x & 2 < 3truem < c
> or <=&x & 1 > 1falsem <= c
== or !=x1 == 0
< or >=x1 < 1
<= or >x1 > 0

G.EXP.03 不要使用子表达式调用

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
unnecessary_operationyesnocomplexitywarn

【描述】

这样会影响代码可读性。

【正例】


#![allow(unused)]
fn main() {
let arr = compute_array();
let first = arr[0];
}

【反例】


#![allow(unused)]
fn main() {
let first = compute_array()[0];
}

G.EXP.04 不要使用无效表达式语句

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
no_effectyesnocomplexitywarn

【描述】

无效的表达式语句,虽然会执行,但实际并没有起到什么效果。

也有例外情况存在。

【正例】


#![allow(unused)]
fn main() {
let a = 41;
let a = a+1;
}

【反例】


#![allow(unused)]
fn main() {
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)
    }
	// ...
    
}

}

G.EXP.05 使用 +=/-= 等操作来代替 ++i / --i

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
double_negyesnostylewarn

【描述】

++i 这种操作编译会失败,但是 --i 编译不会出错。有些C/Cpp等其他语言新手容易犯此错误。

【正例】


#![allow(unused)]
fn main() {
let mut x = 3;
x -= 1;
}

【反例】


#![allow(unused)]
fn main() {
let mut x = 3;
--x;
}

G.EXP.06 表达式操作最好使用括号来表达清楚优先级顺序

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
precedenceyesnocomplexitywarn

【描述】

并不是每个人都能记得住优先级,所以最好使用括号把优先级顺序区分出来,增加可读性。

【正例】


#![allow(unused)]
fn main() {
(1 << 2) + 3
(-1i32).abs()
}

【反例】


#![allow(unused)]
fn main() {
1 << 2 + 3
-1i32.abs()
}

G.EXP.07 避免在比较中添加无用的掩码操作

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
ineffective_bit_maskyesnocorrectnessdeny

【描述】

检查比较中的无用位掩码操作,可以在不改变结果的情况下删除该位掩码操作。

可以对照下面表格进行检查。

ComparisonBit OpExampleequals
> / <=| / ^x | 2 > 3x > 3
< / >=| / ^x ^ 1 < 4x < 4

【正例】


#![allow(unused)]
fn main() {
if (x > 3) {  }
}

【反例】


#![allow(unused)]
fn main() {
if (x | 1 > 3) {  }
}

控制流程

Rust 中 流程控制 也是属于 表达式,但在本规范中将其独立出来。


P.CTL.01 不要过度使用迭代器

【描述】

迭代器虽然是 Rust 中比较推崇的方式,但也没必要过度使用它。总之,如果使用迭代器让代码太复杂,就考虑换个非迭代器的方式实现吧。

【正例】

创建一个 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);

    // allocate our output matrix, copying from the input so
    // we don't need to worry about the edge cases.
    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
}
}

【反例】

创建一个 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);

    // Stash away the top and bottom rows so they can be
    // directly copied across later
    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 {
    // stash away the left-most and right-most elements so they can be copied across directly.
    let &first = middle_row.first().unwrap();
    let &last = middle_row.last().unwrap();

    // Get the top, middle, and bottom row of our 3x3 sub-matrix so they can be
    // averaged.
    let top_window = top_row.windows(3);
    let middle_window = middle_row.windows(3);
    let bottom_window = bottom_row.windows(3);

    // slide the 3x3 window across our middle row so we can get the average
    // of everything except the left-most and right-most elements.
    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))
}
}

P.CTL.02 优先使用模式匹配而不是 if 判断值是否相等

【描述】

Rust 中 模式匹配 是惯用法,而不是通过 if 判断值是否相等。

【正例】


#![allow(unused)]
fn main() {
if let Some(value) = opt {
  ...
}
// or
if let [first, ..] = list {
  ...
}
}

【反例】


#![allow(unused)]
fn main() {
let opt: Option<_> = ...;

if opt.is_some() {
  let value = opt.unwrap();
  ...
}

// or
let list: &[f32] = ...;

if !list.is_empty() {
  let first = list[0];
  ...
}

}

G.CTL.01 避免在流程控制分支中使用重复代码

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
branches_sharing_codeyesnonurseryallow

【描述】

【正例】


#![allow(unused)]
fn main() {
println!("Hello World");
let foo = if … {
    13
} else {
    42
};
}

【反例】


#![allow(unused)]
fn main() {
let foo = if … {
    println!("Hello World");
    13
} else {
    println!("Hello World");
    42
};
}

G.CTL.02 控制流程的分支逻辑要保持精炼

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
collapsible_else_ifyesnostylewarn
collapsible_ifyesnostylewarn
collapsible_matchyesnostylewarn
double_comparisonsyesnocomplexitywarn
wildcard_in_or_patternsyesnocomplexitywarn

【描述】

【正例】


#![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" => {},
    _ => {},
}
}

【反例】


#![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" | _ => {},
}

}

G.CTL.03 当需要通过比较大小来区分不同情况时,优先使用matchcmp 来代替 if 表达式

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
comparison_chainyesnostylewarn

【描述】

这种情况下,使用 matchcmp 代替 if 的好处是语义更加明确,而且也能帮助开发者穷尽所有可能性。 但是这里需要注意这里使用 matchcmp 的性能要低于 if表达式,因为 一般的 >< 等比较操作是内联的,而 cmp方法没有内联。

根据实际情况来选择是否设置 comparison_chainallow

【正例】


#![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()
     }
}
}

【反例】


#![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()
    }
}
}

G.CTL.04 if 条件表达式分支中如果包含了 else if 分支也应该包含 else 分支

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
else_if_without_elseyesnorestrictionallow

【描述】

【正例】


#![allow(unused)]
fn main() {
fn a() {}
fn b() {}
let x: i32 = 1;
if x.is_positive() {
    a();
} else if x.is_negative() {
    b();
} else {
    // We don't care about zero.
}
}

【反例】


#![allow(unused)]
fn main() {
fn a() {}
fn b() {}
let x: i32 = 1;
if x.is_positive() {
    a();
} else if x.is_negative() {
    b();
}
}

G.CTL.05 如果要通过 if 条件表达式来判断是否panic,请优先使用断言

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
if_then_panicyesnoStylewarn

【描述】

【正例】


#![allow(unused)]
fn main() {
let sad_people: Vec<&str> = vec![];
assert!(sad_people.is_empty(), "there are sad people: {:?}", sad_people);
}

【反例】


#![allow(unused)]
fn main() {
let sad_people: Vec<&str> = vec![];
if !sad_people.is_empty() {
    panic!("there are sad people: {:?}", sad_people);
}
}

G.CTL.06 善用标准库中提供的迭代器适配器方法来满足自己的需求

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
explicit_counter_loopyesnocomplexitywarn
filter_map_identityyesnocomplexitywarn
filter_nextyesnocomplexitywarn
flat_map_identityyesnocomplexitywarn
flat_map_optionyesnopedanticallow

【描述】

Rust 标准库中提供了很多迭代器方法,要学会使用它们,选择合适的方法来满足自己的需求。

下面示例中,反例中的迭代器适配器方法,都可以用对应的正例中的方法代替。

【正例】


#![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();

}

【反例】


#![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();
}

G.CTL.07 在 Match 分支的 Guard 语句中不要使用带有副作用的条件表达式

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制化参考】

可以检测 match分支中 Guard 的 if 表达式是否使用 {} ,如果是的话,发出警告,不要带有副作用。

【描述】

因为在 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);
}

字符串

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::with_capacity(input.len());
}

【反例】


#![allow(unused)]
fn main() {
let mut output = String::new();
}

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("&lt;"),
                '>' => output.push_str("&gt;"),
                '&' => output.push_str("&amp;"),
                _ => output.push(c)
            }
        }
        // 只有在字符串修改的时候才使用 String
        Cow::Owned(output)
    } else {
        //其他情况使用 &str
        input
    }
}
}

P.STR.04 在使用内建字符串处理函数或方法的时候,应该注意避免隐藏的嵌套迭代 或 多次迭代

【描述】

比如 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("&lt;"),
                '>' => output.push_str("&gt;"),
                '&' => output.push_str("&amp;"),
                _ => output.push(c),
            }
        }

        Cow::Owned(output)
    } else {
        input.into()
    }
}
}

P.STR.05 只有在合适的场景下,才使用第三方库正则表达式regex

【描述】

合适的场景包括:

  1. 不在乎编译文件大小。regex 正则引擎是第三方库,引入它的时候意味着还会引入其他依赖,对编译文件大小有要求可以考虑,是否使用 Cow 和 内建函数方法来替代。
  2. 对字符串查找性能有极致需求。regexfind 实现性能很好,但是 replace 替换就不一定了。对于替换需求,在适合 Cow<str> 的场景下,使用 Cow 和 内建函数方法来替代 regex 可能更好。

P.STR.06 在拼接字符串时,建议使用 format!

【描述】

使用 format! 组合字符串是最简单和直观的方法,尤其是在字符串和非字符串混合的情况下。但追加字符串还是建议使用 push

【正例】


#![allow(unused)]
fn main() {
 let hw = format!("Hello {}!", name)
}

G.STR.01 在实现 Display trait 时不要调用 to_string() 方法

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
to_string_in_displayyesnocorrectnessdeny

【描述】

因为 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.0)
    }
}
}

【反例】


#![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())
    }
}
}

G.STR.02 在追加字符串时使用 push_str方法可读性更强

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
string_add_assignyesnopedanticallow
string_addyesnorestrictionallow

【描述】

【正例】


#![allow(unused)]
fn main() {
let mut x = "Hello".to_owned();

// More readable
x += ", World";
x.push_str(", World");
}

【反例】


#![allow(unused)]
fn main() {
let mut x = "Hello".to_owned();
x = x + ", World";
}

G.STR.03 将只包含 ASCII字符的字符串字面量转为字节序列可以直接使用b"str" 语法代替调用as_bytes方法

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
string_lit_as_bytesyesnonurseryallow

【描述】

这是为了增强可读性,让代码更简洁。

注意,"str".as_bytes() 并不等价于 b"str",而是等价于 &b"str"[..]

【正例】


#![allow(unused)]
fn main() {
let bs = b"a byte string";
}

【反例】


#![allow(unused)]
fn main() {
let bs = "a byte string".as_bytes();
}

G.STR.04 需要判断字符串以哪个字符开头或结尾时,不要按字符迭代比较

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
chars_last_cmpyesnostylewarn
chars_next_cmpyesnostylewarn

【描述】

Rust 语言 核心库 和 标准库都对字符串内置了一些方便的方法来处理这类需求。

迭代字符的性能虽然也很快(对500多个字符迭代转义处理大概需要4.5微秒左右),但这种场景用迭代的话,代码可读性更差一些。

【正例】


#![allow(unused)]
fn main() {
let name = "_";
name.ends_with('_') || name.ends_with('-');

let name = "foo";
if name.starts_with('_') {};
}

【反例】


#![allow(unused)]
fn main() {
let name = "_";
name.chars().last() == Some('_') || name.chars().next_back() == Some('-');

let name = "foo";
if name.chars().next() == Some('_') {};
}

G.STR.05 对字符串按指定位置进行切片的时候需要小心破坏其 UTF-8 编码

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
string_sliceyesnorestrictionallow

【描述】

字符串默认是合法的 UTF-8字节序列,如果通过指定索引位置来对字符串进行切片,有可能破坏其合法 UTF-8 编码,除非这个位置是确定的,比如按 char_indices 方法来定位是合法的。

【正例】


#![allow(unused)]
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};
let sub_s = &s[pos..];
assert_eq!("lkanne", sub_s);
}

【反例】


#![allow(unused)]
fn main() {
let s = "Ölkanne";
// thread 'main' panicked at 'byte index 1 is not a char boundary; 
// it is inside 'Ö' (bytes 0..2) of `Ölkanne`'
let sub_s = &s[1..];
// println!("{:?}", sub_s);
}

集合类型

Rust 中的集合类型包括四大类:


P.CLT.01 根据集合各自的特点选择合适的集合类型

【描述】

Rust 标准库内置的集合类型,在安全和性能方面还是比较靠谱的。需要仔细阅读标准库中各类集合类型的优缺点来选择合适的类型。

下列场景考虑 Vec

  • 你想要一个可动态增长大小(堆分配)的数组
  • 你想要一个栈结构
  • 你想要集合元素按特定顺序排序,并且仅需要在结尾追加新元素
  • 你可能只是想临时收集一些元素,并且不关心它们的实际存储

下列场景考虑 VecDeque

  • 你想要一个可以在头尾两端插入元素的 Vec
  • 你想要一个队列,或双端队列

下列场景考虑LinkedList

  • 你非常确定你真的需要一个双向链表

下列场景考虑 Hashmap

  • 你需要一个 KV 集合
  • 你想要一个缓存

下列场景考虑 BTreeMap

  • 你需要一个可以排序的 HashMap
  • 你希望可以按需获取一系列元素
  • 你对最小或最大的 KV 感兴趣
  • 你想要找到比某物小或大的最大或最小键

下列场景考虑使用 Set 系列

  • 你只是需要一个 集合

下列场景考虑使用 BinaryHeap

  • 你想存储一堆元素,但只想在任何给定时间内处理 最大 或 最重要的元素
  • 你想要一个优先队列

G.CLT.01 非必要情况下,不要使用 LinkedList,而用 VecVecDeque 代替

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
linkedlistyesnopedanticallow

该 lint 对应 clippy.toml 配置项:

# 如果函数是被导出的 API,则该 lint 不会被触发,是防止 lint 建议对 API 有破坏性的改变。默认为 true
avoid-breaking-exported-api=true 

【描述】

一般情况下,有 VecVecDeque 性能更好。LinkedList 存在内存浪费,缓存局部性(Cache Locality)比较差,无法更好地利用CPU 缓存机制,性能很差。

只有在有大量的 列表 拆分 和 合并 操作时,才真正需要链表,因为链表允许你只需操作指针而非复制数据来完成这些操作。

函数与闭包


P.FUD.01 函数参数建议使用借用类型

【描述】

这里是指 借用类型,而非 借用有所有权的类型。比如:&str 优于 &String&[T] 优于&Vec<T>&T 优于 &Box<T> 等。

使用 借用类型 可以利用 Deref 隐式转换让函数参数更加灵活。

【正例】

// 这里的参数可以接受 &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);
        }
    }
}

P.FUD.02 传递到闭包的变量建议单独重新绑定

【描述】

默认情况下,闭包通过借用来捕获环境变量。或者,可以使用 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 num2_cloned = num2.clone();
let num3_borrowed = num3.as_ref();
let closure = move || {
    *num1 + *num2_cloned + *num3_borrowed;
};
}

【反例】


#![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` is moved
    let num2 = num2.clone();  // `num2` is cloned
    let num3 = num3.as_ref();  // `num3` is borrowed
    move || {
        *num1 + *num2 + *num3;
    }
};
}

P.FUD.03 函数返回值不要使用 return

【描述】

Rust 中函数块会自动返回最后一个表达式的值,不需要显式地指定 Return。

只有在函数过程中需要提前返回的时候再加 Return。

【正例】


#![allow(unused)]
fn main() {
fn foo(x: usize) -> usize {
    if x < 42{
        return x;
    }
    x + 1
}
}

【反例】


#![allow(unused)]
fn main() {
fn foo(x: usize) -> usize {
    if x < 42{
        return x;
    }
    return x + 1;
}
}

G.FUD.01 函数参数最长不要超过 五 个

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
too_many_argumentsyesnocomplexitywarn

该 lint 对应 clippy.toml 配置项:

# 函数参数最长不要超过5个
too-many-arguments-threshold=5

【描述】

为了提升代码可读性,函数的参数最长不宜超过五个。

【正例】

想办法把过长的参数缩短。

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);
}

【反例】


#![allow(unused)]
fn main() {
struct Color;
fn foo(x: f32, y: f32, name: &str, c: Color, w: u32, h: u32, a: u32, b: u32) {
    // ..
}
}

G.FUD.02 当函数参数实现了 Copy,并且是按值传入,如果值可能会太大,则应该考虑按引用传递

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
large_types_passed_by_valueyesnopedanticallow

该 lint 对应 clippy.toml 配置项:

# 如果函数是被导出的 API,则该 lint 不会被触发,是防止 lint 建议对 API 有破坏性的改变。默认为 true
avoid-breaking-exported-api=true 

【描述】

通过值传递的参数可能会导致不必要的 memcpy 拷贝,这可能会造成性能损失。

【正例】


#![allow(unused)]
fn main() {
#[derive(Clone, Copy)]
struct TooLarge([u8; 2048]);

// Good
fn foo(v: &TooLarge) {}
}

【反例】


#![allow(unused)]
fn main() {
#[derive(Clone, Copy)]
struct TooLarge([u8; 2048]);

// Bad
fn foo(v: TooLarge) {}
}

G.FUD.03 当函数参数出现太多 bool 类型的参数时,应该考虑将其封装为自定义的结构体或枚举

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
fn_params_excessive_boolsyesnopedanticallow

该 lint 对应 clippy.toml 配置项:

# 用于配置函数可以拥有的 bool 类型参数最大数量,默认为 3。
max-fn-params-bools=3 

【描述】

布尔类型的参数过多,很难让人记住,容易出错。将其封装为枚举或结构体,可以更好地利用类型系统的检查而避免出错。

【正例】


#![allow(unused)]
fn main() {
enum Shape {
    Round,
    Spiky,
}

enum Temperature {
    Hot,
    IceCold,
}

fn f(shape: Shape, temperature: Temperature) { ... }
}

【反例】


#![allow(unused)]
fn main() {
fn f(is_round: bool, is_hot: bool) { ... }
}

G.FUD.04 当Copy 类型的足够小的值作为函数参数时,应该按值(by-value)传入,而不是引用(by-ref)

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
trivially_copy_pass_by_refyesnopedanticallow

该 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

【描述】

在函数参数为 Copy 类型 且 其值足够小的时候,一般情况下,会避免传引用。因为对于这种小的值,性能上和按引用传递是一样快的,并且在代码更容易编写和可读。包括一些小的 结构体,也推荐按值传递,但要注意【例外】示例所示的情况。

【正例】


#![allow(unused)]
fn main() {
fn foo(v: u32) {}
}

【反例】


#![allow(unused)]
fn main() {
fn foo(v: &u32) {}
}

【例外】

#[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 函数
    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());
}

G.FUD.05 函数参数是不可变借用的时候,返回值不应该是可变借用

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
mut_from_refyesnocorrectnessdeny

【描述】

【正例】


#![allow(unused)]
fn main() {
fn foo(&Foo) -> &Bar { .. }
}

【反例】


#![allow(unused)]
fn main() {
fn foo(&Foo) -> &mut Bar { .. }
}

G.FUD.06 不要为函数指定 inline(always)

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
inline_alwaysyesnopedanticallow

【描述】

inline 虽然可以提升性能,但也会增加编译时间和编译大小。

Rust 中性能、编译时间和编译大小之间需要权衡。根据需要再 inline 即可。

【反例】


#![allow(unused)]
fn main() {
#[inline(always)]
fn not_quite_hot_code(..) { ... }
}

泛型

Rust 中的泛型允许开发人员编写更加简洁、更少重复的代码。但泛型可能会引起编译文件大小膨胀,酌情使用。


P.GEN.01 用泛型来抽象公共语义

【描述】

应该巧用泛型来抽象公共语义,消除重复代码。

【正例】

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

【反例】

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

P.GEN.02 不要随便使用 impl Trait 语法替代泛型限定

【描述】

impl Trait 语法 和 泛型限定,虽然都是静态分发,且效果类似,但是它们的语义是不同的。

在类型系统层面上的语义:

  1. impl Trait 是 存在量化类型。意指,存在某一个被限定的类型。
  2. 泛型限定 是 通用量化类型。意指,所有被限定的类型。

要根据它们的语义来选择不同的写法。

另外,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 限定会显著地增长编译时间。

【正例】

来自于 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)
    }   
}

【反例】

以下写法比上面的写法编译时间要多十倍。


#![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)
    }
}

G.GEN.01 泛型参数必须先声明再使用

【级别:规则】

按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_noyes__

【描述】

泛型参数必须先被声明,才能被使用。如果一个类型中包含泛型类型,也必须为其声明泛型参数。

【正例】


#![allow(unused)]
fn main() {
struct Foo<T> { x: T }

struct Bar<T> { x: Foo<T> }
}

【反例】


#![allow(unused)]
fn main() {
struct Foo<T> { x: T }

struct Bar { x: Foo } // error[E0107]: missing generics for struct `Foo`
}

G.GEN.02 不要在泛型位置上使用内建类型

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
builtin_type_shadowyesnostylewarn

【描述】

【正例】


#![allow(unused)]
fn main() {
impl<T> Foo<T> {
    fn impl_func(&self) -> T {
        42
    }
}
}

【反例】

这里 u32 会被认为是一个类型参数。


#![allow(unused)]
fn main() {
impl<u32> Foo<u32> {
    fn impl_func(&self) -> u32 {
        42
    }
}
}

G.GEN.03 使用 Rust 标准库中某些方法,要注意避免使用其泛型默认实现,而应该使用具体类型的实现

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
inefficient_to_stringyesnopedanticallow

【描述】

Rust 标准库内部某些类型使用了 泛型特化(未稳定特性),比如 ToString trait。

该 trait 有一个泛型默认实现, 并且一些具体类型也实现了它,比如 char/ str / u8/ i8 等。

在实际代码中,应该选择去调用具体类型实现的 to_string() 方法,而非调用泛型的默认实现。

这一规则要求开发者对 Rust 标准库的一些方法实现有一定了解。

【正例】


#![allow(unused)]
fn main() {
// 闭包参数中, s 为 `&&str` 类型,使用 `|&s|` 对参数模式匹配后,闭包体内 `s` 就变成了 `&str` 类型
// 经过这样的转换,直接调用 `&str`的 `to_string()` 方法,而如果是 `&&str` 就会去调用泛型的默认实现。 
["foo", "bar"].iter().map(|&s| s.to_string() );
}

【反例】


#![allow(unused)]
fn main() {
// 闭包参数中, s 为 `&&str` 类型
//  `&&str` 就会去调用泛型的默认实现
["foo", "bar"].iter().map(|s| s.to_string() );
}

G.GEN.04 为泛型类型实现方法时,impl 中声明的泛型类型参数一定要被用到

【级别:规则】

按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_noyes__

【描述】

impl 中被声明的类型参数,至少要满足下面三种形式:

  1. impl<T> Foo<T>T 出现在实现的Self 类型Foo<T> 中 。
  2. impl<T> SomeTrait<T> for FooT出现在要实现的 trait 中 。
  3. impl<T, U> SomeTrait for T where T: AnotherTrait<AssocType=U> , 出现在 T 的 trait 限定的关联类型中。

除此之外,都不算 T 被用到(出现在 Self 类型中)。

有这种限制,主要有两个原因:

  1. 方便 Rust 类型推断。有这些限制才能明确能推断这些泛型参数的行为,避免产生错误。参考 RFC 0447
  2. 避免语义定义不明确的情况。如果 impl 上存在自由的 泛型参数,则无法保证这一点。

【正例】


#![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内的一个关联类型
}

【反例】


#![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
}

G.GEN.05 定义泛型函数时,如果该函数实现用到来自 trait 定义的相关行为,需要为泛型指定相关 trait 的限定

【级别:规则】

按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_noyes__

【描述】

泛型,在 Rust 类型系统中的语义是一种 通用量化l类型(Universally-quantified type),即,泛型类型 T 的所有可能 的单态类型。

在泛型函数内部,如果使用了来自某个 trait 定义的行为,则需要为泛型指定相关的 trait 限定,来排除其他没有实现该trait 的类型。

【正例】

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

【反例】


#![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`
}
}

特质

特质就是指 trait。在 Rust 中, trait 不是具体类型,而是一种抽象接口。但是通过 impl Traitdyn Trait 也可以将 trait 作为类型使用。


G.TRA.01 使用 trait 时要注意 trait 一致性规则

【级别:规则】

按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_noyes__

【描述】

使用 trait 的时候,必须要满足 trait 一致性规则,即,孤儿规则(orphans rule):类型和trait,必须有一个是在本地crate内定义的。

内置 trait

Rust 标准库内置了很多 trait,在使用这些 trait 的时候也需要注意。

P.TRA.Buitin.01 在实现 Borrow trait 的时候,需要注意一致性

【描述】

当你想把不同类型的借用进行统一抽象,或者当你要建立一个数据结构,以同等方式处理自拥有值(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 实现 EqHash 的行为不一致,而 HashMap 则要求 Key 必须 HashEq 的实现一致。这种不一致,编译器无法检查,所以在逻辑上,就不应该为其实现 Borrow。如果强行实现,那可能会出现逻辑 Bug。


G.TRA.Buitin.01 应该具体类型的 default() 方法代替 Default::default() 调用

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
default_trait_accessyesnopedanticallow

【描述】

为了增强可读性。

【正例】


#![allow(unused)]
fn main() {
let s = String::default();
}

【反例】


#![allow(unused)]
fn main() {
let s: String = Default::default();
}

G.TRA.Buitin.02 不要为迭代器实现Copy trait

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
copy_iteratoryesnopedanticallow

【描述】

在 Rust 中,迭代器是不能实现 Copy 的。因为在需要迭代修改的场景,因为 Copy 的存在,而失去效果。

【反例】

比如,对于标准库里的 Range<T> 就不能实现 Copy,因为它也是一个迭代器。


#![allow(unused)]
fn main() {
let mut iter = 0..n;
for i in iter { if i > 2 { break; } }
iter.collect();
}

如果它实现了 Copy,示例中 iter 的值将不会被改变,这样就不符合预期结果。

G.TRA.Buitin.03 能使用derive 自动实现Default trait 就不要用手工实现

【级别:规则】

按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
derivable_implsyesnocomplexitywarn

该lint不能用于检测泛型参数类型的 Default 手工实现。

【描述】

手工实现 Default,代码不精炼。

【正例】


#![allow(unused)]
fn main() {
#[derive(Default)]
struct Foo {
    bar: bool
}
}

【反例】


#![allow(unused)]
fn main() {
struct Foo {
    bar: bool
}

impl std::default::Default for Foo {
    fn default() -> Self {
        Self {
            bar: false
        }
    }
}
}

G.TRA.Buitin.04 在使用#[derive(Hash)] 的时候,避免再手工实现 PartialEq

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
derive_hash_xor_eqyesnocorrectnessdeny

【描述】

实现 Hash 和 Eq 必须要满足下面一个等式:

k1 == k2  -> hash(k1) == hash(k2)

即,当k1k2 相等时,hash(k1)也应该和 hash(k2) 相等。 所以要求 PartialEq / Eq / Hash 的实现必须保持一致。

如果用 #[derive(Hash)] 的时候,搭配了一个手工实现的 PartialEq 就很可能出现不一致的情况。

但也有例外。

【正例】


#![allow(unused)]
fn main() {
#[derive(PartialEq, Eq, Hash)]
struct Foo;
}

【反例】


#![allow(unused)]
fn main() {
#[derive(Hash)]
struct Foo;

impl PartialEq for 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)]
}

G.TRA.Buitin.05 在使用#[derive(Ord)] 的时候,避免再手工实现 PartialOrd

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
derive_ord_xor_partial_ordyesnocorrectnessdeny

【描述】

跟实现 Hash 和 Eq 的要求类似,对于实现 Ord 的类型来说,必须要满足下面一个等式:

k1.cmp(&k2) == k1.partial_cmp(&k2).unwrap()

所以要求与 PartialOrd 的实现必须保持一致,并确保maxminclampcmp一致。

通过#[derive(Ord)] 并手动实现PartialOrd,很容易意外地使cmp和partial_cmp不一致。

但也有例外。

【正例】


#![allow(unused)]
fn main() {
#[derive(Ord, PartialOrd, PartialEq, Eq)]
struct Foo;

// or

#[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 {
    ...
}

}

【反例】


#![allow(unused)]
fn main() {
#[derive(Ord, PartialEq, Eq)]
struct Foo;

impl PartialOrd 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()
    }
}
}

G.TRA.Buitin.06 不要对实现 Copy 或引用类型调用 std::mem::dropstd::mem::forgot

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
drop_copyyesnocorrectnessdeny
drop_refyesnocorrectnessdeny
forget_copyyesnocorrectnessdeny
forget_refyesnocorrectnessdeny
undropped_manually_dropsyesnocorrectnessdeny

【描述】

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); // Get rid of the self reference so we don't use it by mistake.
    // ...
}
}

G.TRA.Buitin.07 对实现 Copy 的可迭代类型来说,要通过迭代器拷贝其所有元素时,应该使用 copied方法,而非cloned

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
cloned_instead_of_copiedyesnopedanticallow

【描述】

copied 方法在语义层面,是针对实现 Copy 的类型,所以应该使用 copied 来增加代码可读性。

【正例】


#![allow(unused)]
fn main() {
let a = [1, 2, 3];

let v_copied: Vec<_> = a.iter().copied().collect();
}

【反例】


#![allow(unused)]
fn main() {
let a = [1, 2, 3];

let v_copied: Vec<_> = a.iter().cloned().collect();
}

G.TRA.Buitin.08 实现 From 而不是 Into

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
from_over_intoyesnostylewarn

【描述】

优先为类型实现 From 而非 Into。因为实现了 FromInto 也会被自动实现。并且在错误处理的时候,? 操作符会通过调用 From 实现自动进行错误类型转换。

但是在泛型限定上,优先 Into

当然,也存在例外。

【正例】


#![allow(unused)]
fn main() {
struct StringWrapper(String);

impl From<String> for StringWrapper {
    fn from(s: String) -> StringWrapper {
        StringWrapper(s)
    }
}
}

【反例】


#![allow(unused)]
fn main() {
struct StringWrapper(String);

impl Into<StringWrapper> for String {
    fn into(self) -> StringWrapper {
        StringWrapper(self)
    }
}
}

【例外】

有两类情况,可以直接实现 Into

  1. Into 不提供 From 实现。在一些场景中,From 自动实现的 Into 并不符合转换需求。
  2. 使用 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
    }
}
}

G.TRA.Buitin.09 一般情况下不要给 Copy 类型手工实现 Clone

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
expl_impl_clone_on_copyyesnopedanticallow

【描述】

手工为 Copy 类型实现 Clone ,并不能改变 Copy 类型的行为。除非你显式地去调用 clone()方法。

【正例】


#![allow(unused)]
fn main() {
#[derive(Copy, Clone)]
struct Foo;
}

【反例】


#![allow(unused)]
fn main() {
#[derive(Copy)]
struct Foo;

impl Clone for 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();
}

G.TRA.Buitin.10 不要随便使用 Deref trait 来模拟继承

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【描述】

Deref trait是专门用于实现自定义指针类型而存在的。虽然可以实现 Deref 来达到某种类似于继承的行为,但 Rust 中不推荐这样做。

这是因为 Rust 语言推崇显式的转换,而 Deref 则是 Rust 中为数不多的隐式行为。如果 Deref 被滥用,那么程序中隐式行为可能会增多,隐式的转换是 Bug 的温床。

trait 对象

trait 对象需要注意 动态安全 (dyn safe),也叫对象安全 (object safe),但官方现在倾向于 动态安全这个术语。


P.TRA.Object.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.Object.02 除非必要,避免自定义虚表

【描述】

trait 对象 dyn Trait 隐藏了复杂而又为危险的虚表实现,为我们提供了简单而又安全的动态分发。手动实现虚表的代码中充斥着大量的 unsafe,稍有不慎,就会引入 bug 。如无必要,不要自定义虚表。

如果你的设计不能使用标准的 dyn Trait 结构来表达,那么你首先应该尝试重构你的程序,并参考以下理由来决定是否使用自定义的虚表。

  • 你想要为一类指针对象实现多态,并且无法忍受多级指针解引用造成的性能开销,参考 RawWakerBytes
  • 你想要自定义内存布局,比如像 C++ 中虚表一样紧凑的内存结构(虚表指针位于对象内),参考 RawTask
  • 你的 crate 需要在 no_std 环境中使用动态分发,参考 RawWaker
  • 或者,标准的 trait object 确实无法实现你的需求。

错误处理

Rust 为了保证系统健壮性,将系统中出现的非正常情况划分为三大类:

  1. 失败
  2. 错误
  3. 异常

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 {
        // We replace self[index] with the last element. Note that if the
        // bounds check above succeeds there must be a last element (which
        // can be self[index] itself).
        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>

【描述】

在某些其他语言中,如果函数的返回值 或 结构体字段的值 可能为空时,通常会设置一个 “哨兵值(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.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.ERR.04 当程序中需要处理错误时,应该使用 Result<T, E>? 操作符

【描述】

当需要处理错误时,为了保证 程序的健壮性,应该尽可能处理错误。

【正例】


#![allow(unused)]
fn main() {
let res: Result<usize, ()> = Ok(1);
res?;   // Ok::<(), ()>(())
}

【反例】

在实现原型类项目的时候,可以“快、糙、猛”地使用 expect 。但是要进生产环境,需要合理地处理错误。


#![allow(unused)]
fn main() {
let res: Result<usize, ()> = Ok(1);
res.expect("one"); // 如果有 Err, expect会 Panic !

}

P.ERR.05 在确定 Option<T>Result<T, E>类型的值不可能是 NoneErr 时,请用 expect 代替 unwrap()

【描述】

当需要处理的 Option<T>Result<T, E> 类型的值,永远都不可能是 NoneErr 时,虽然直接 unwrap() 也是可以的,但使用 expect 会有更加明确的语义。

expect 的语义:

我不打算处理 NoneErr 这种可能性,因为我知道这种可能性永远不会发生,或者,它不应该发生。但是 类型系统并不知道它永远不会发生。所以,我需要像类型系统保证,如果它确实发生了,它可以认为是一种错误,并且程序应该崩溃,并带着可以用于跟踪和修复该错误的栈跟踪信息。

所以在指定 expect 输出消息的时候,请使用肯定的描述,而非否定,用于提升可读性。

【正例】

// 这个配置文件默认会跟随源码出现,所以,必定可以读取到
// 这个配置文件不应该没有被提供,如果万一出现了没有提供的情况,需要 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");
}

【反例】

// 这个配置文件默认会跟随源码出现,所以,必定可以读取到
// 这个配置文件不应该没有被提供,如果万一出现了没有提供的情况,需要 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!");
}

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")
    }
}

impl<T: fmt::Debug> std::error::Error for SendError<T> {}
}

G.ERR.01 在处理 Option<T>Result<T, E> 类型时,不要随便使用 unwrap

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
unwrap_usedyesnorestrictionallow

【描述】

Option<T>Result<T, E>类型的值分别是 NoneErr 时,直接对其 unwrap() 会导致程序恐慌!

【正例】


#![allow(unused)]
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 的情况
}
}

【反例】


#![allow(unused)]
fn main() {
fn select(opt: Option<String>) {
    opt.unwrap();  // 可以用 expect 方法来处理 None 的情况
}
// OR
fn select(opt: Result<String, ()>) {
    res.unwrap();  // 可以用 expect 方法来处理 Err 的情况
}
}

G.ERR.02 不要滥用 expect,请考虑用 unwrap_or_ 系列方法代替

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
expect_fun_callyesnoperfwarn
expect_usedyesnorestrictionallow

【描述】

使用 expect 的时候请遵循 expect 的语义,不要滥用。参考 : P.ERR.05

但是对于一些存在“副作用”的函数,在 遇到 NoneErr 时,可能需要返回一些指定的值。这个时候用 expect 就不太符合语义。

如果你的用法完全符合 expect 语义,那么可以设置 #![allow(clippy::expect_fun_call]

【正例】


#![allow(unused)]
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!
}

【反例】


#![allow(unused)]
fn main() {
let foo = Some(String::new());
let err_code = "418";
let err_msg = "I'm a teapot";
foo.expect(&format!("Err {}: {}", err_code, err_msg)); 
// or
foo.expect(format!("Err {}: {}", err_code, err_msg).as_str());  
}

【例外】

完全符合 expect 语义的使用。

#![allow(clippy::expect_fun_call]

// 这个配置文件默认会跟随源码出现,所以,必定可以读取到
// 这个配置文件不应该没有被提供,如果万一出现了没有提供的情况,需要 Panic 并提供错误信息方便调试,或者让使用者知道原因
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");
}

内存

生存期

生存期(lifetime),也被叫做 生命周期。


P.MEM.Lifetime.01 生命周期参数命名尽量简单

【描述】

生命周期参数的命名应该尽量简单,可以使用表达一定语义的缩写。

因为生命周期参数的目的是给编译器使用,用于防止函数中出现悬垂引用。

适当简单的携带语义的缩写,可以最小化对业务代码的干扰。并且在生命周期参数较多的情况下,清晰地表达具体哪个引用属于哪个生命周期。

【正例】


#![allow(unused)]
fn main() {
/// '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>,
}

}

【反例】


#![allow(unused)]

fn main() {
struct ConstraintGeneration<'a, 'b, 'c> {
    infcx: &'cg 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: &'cg Body<'c>,
}

}

P.MEM.Lifetime.02 在需要的时候,最好显式地标注生命周期,而非利用编译器推断

【描述】

编译器可以推断你的代码做了什么,但它不知道你的意图是什么。

编译器对生命周期参数有两种单态化方式(生命周期参数也是一种泛型):

  • Early bound。一般情况下,'a: 'b 以及 impl<'a'> 这种方式是 early bound,意味着这些生命周期参数会在当前作用域单态化生命周期实例。
  • Late bound。默认的 'afor<'a> 是在实际调用它们的地方才单态化生命周期实例。

在不同的场景下,需要指定合适的单态化方式,才能让编译器明白你的意图。

在使用匿名生命周期 '_ 的时候需要注意,如果有多个匿名生命周期,比如 ('_ ,'_) ,每个匿名生命周期都会有自己的单独实例。

Box<T>

Rust 中分配堆内存必须要使用的类型。


P.MEM.Box.01

【描述】


G.MEM.BOX.01 一般情况下,不要直接对 Box<T> 进行借用

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
borrowed_boxyesnocomplexitywarn

也有例外。

【描述】

通常 &T&Box<T> 更常用。

【正例】


#![allow(unused)]
fn main() {
fn foo(bar: &T) { ... }
}

【反例】


#![allow(unused)]
fn main() {
fn foo(bar: &Box<T>) { ... }
}

【例外】


#![allow(unused)]
fn main() {
// https://docs.rs/crate/actix-web-security/0.1.0/source/src/authentication/scheme/authentication_provider.rs

#[async_trait]
pub trait AuthenticationProvider: AuthenticationProviderClone {
    #[allow(clippy::borrowed_box)]
    async fn authenticate(
        &self,
        authentication: &Box<dyn Authentication>,
    ) -> Result<Box<dyn UserDetails>, AuthenticationError>;
}
}

G.MEM.BOX.02 一般情况下,不要直接对已经在堆上分配内存的类型进行 Box 装箱

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
box_vecyesnoperfwarn

也有例外。

【描述】

【正例】


#![allow(unused)]
fn main() {
struct X {
    values: Vec<Foo>,
}
}

【反例】


#![allow(unused)]
fn main() {
struct X {
    values: Box<Vec<Foo>>,
}
}

【例外】


#![allow(unused)]
fn main() {
// https://docs.rs/crate/jex/0.2.0/source/src/jq/query.rs

#[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.
    #[allow(clippy::box_vec)]
    errors: Box<Vec<JVRaw>>,
}

// https://docs.rs/crate/mmtk/0.6.0/source/src/plan/mutator_context.rs

// This struct is part of the Mutator struct.
// We are trying to make it fixed-sized so that VM bindings can easily define a Mutator type to have the exact same layout as our Mutator struct.
#[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_vec)]
    pub space_mapping: Box<SpaceMapping<VM>>,
  
    // ...
}
}

G.MEM.BOX.03 一般情况下,不要直接对不需要堆内存分配就可以正常工作的类型进行 Box 装箱

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
boxed_local yesnoperfwarn

也有例外。

【描述】

【正例】


#![allow(unused)]
fn main() {
fn foo(bar: usize) {}
let x = 1;
foo(x);
println!("{}", x);
}

【反例】


#![allow(unused)]
fn main() {
fn foo(bar: usize) {}
let x = Box::new(1);
foo(*x);
println!("{}", *x);
}

【例外】

当栈变量太大的情况下,需要使用堆分配。或者栈变量需要逃逸的时候。


#![allow(unused)]

fn main() {
// https://docs.rs/crate/aitch/0.1.1/source/src/servers/hyper.rs#:~:text=clippy%3a%3aboxed_local

pub trait ServeFunc {
    fn call_box(self: Box<Self>) -> Result<()>;
}

impl<F> ServeFunc for F
where
    F: FnOnce() -> Result<()>,
{
    #[cfg_attr(feature = "cargo-clippy", allow(boxed_local))]
    fn call_box(self: Box<Self>) -> Result<()> {
        (*self)()
    }
}
}

Drop 析构

在 Safe Rust 中 ,Drop 比较安全。在 Unsafe Rust 中则需要注意更多关于 Drop 的问题。


P.MEM.Drop.01 要注意防范内存泄漏

【描述】

Rust 语言并不保证避免内存泄漏,内存泄漏不属于 Rust 安全职责范围。使用 Rust 的时候需要注意下面情况可能会发生内存泄漏:

  1. 循环引用
  2. 使用 forget / leak 等函数主动跳过析构
  3. 使用 std::mem::ManuallyDrop 构建数据结构而忘记析构
  4. 析构函数内部发生了 panic
  5. 程序中止(abort on panic)

模块

Rust 中一个文件 即一个模块,也可以通过 mod 来创建模块。多个文件放到同一个目录下,也可以成为一个模块。

模块相关有三个概念:

  1. mod是 Rust 代码的“骨架”。
  2. use 则是用来决定使用或导出哪个模块中的具体的类型或方法。
  3. 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描述
}
}

G.MOD.01 使用导入模块中的类型或函数,在某些情况下需要带模块名前缀

【级别:建议】

建议按此规范执行

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【描述】

对于标准库中,很多人都熟知的类型 ,比如 Arc/ Rc/ Cell/ HashMap 等 , 可以导入它们直接使用。

但是对于可能引起困惑的函数,比如 std::ptr::replacestd::mem::replace ,在使用它们的时候,就必须得带上模块前缀。

使用一些第三方库中定义的类型或函数,也建议带上crate或模块前缀。如果太长的话,可以考虑使用 astype 来定义别名。

以上考虑都是为了增强代码的可读性、可维护性。

【正例】


#![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 等

【级别:规则】

按此规范执行

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【描述】

这样使用方在使用的时候,就不需要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 导入模块不要随便使用 通配符*

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
wildcard_importsyesnopedanticallow

该 lint 可以通过 clippy 配置项 warn-on-all-wildcard-imports = false 来配置,用于是否禁用 prelude/ super (测试模块中) 使用通配符导入, 默认是 false

【描述】

使用通配符导入会污染命名空间,比如导入相同命名的函数或类型。

【正例】


#![allow(unused)]
fn main() {
use crate1::foo; // Imports a function named foo
foo(); // Calls crate1::foo
}

【反例】


#![allow(unused)]
fn main() {
use crate2::*; // Has a function named foo
foo(); // Calls crate1::foo
}

【例外】


#![allow(unused)]
fn main() {
use prelude::*;

#[test]
use super::*
}

G.MOD.04 一个项目中应该避免使用不同的模块布局风格

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
self_named_module_filesyesnorestrictionallow
mod_module_filesyesnorestrictionallow

【描述】

Rust 支持两种 模块布局,文件夹内使用 mod.rs 或者是使用跟文件夹同名的文件名,来组织模块。

但是项目里如果混合这两种模块布局,是比较让人困惑的,最好统一为同一种风格。

上面两种 lint ,选择其中一种用于检查是否存在不同的模块布局。

【正例】


#![allow(unused)]
fn main() {
// 使用 `self_named_module_files`,允许下面模块布局
src/
  stuff/
    stuff_files.rs
    mod.rs
  lib.rs

// 使用 `mod_module_files`,允许下面模块布局
src/
  stuff/
    stuff_files.rs
  stuff.rs
  lib.rs
}

【反例】


#![allow(unused)]
fn main() {
// 使用 `self_named_module_files`,不允许下面模块布局
src/
  stuff/
    stuff_files.rs
  stuff.rs
  lib.rs

// 使用 `mod_module_files`,不允许下面模块布局

src/
  stuff/
    stuff_files.rs
    mod.rs
  lib.rs
}

G.MOD.05 不要在私有模块中 设置其内部类型或函数方法 为 pub(crate)

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
redundant_pub_crateyesnonurseryallow

注意:此 lint 为 nursery,意味着有 Bug。

【描述】

如果在私有模块中设置 pub(crate) 可能会让使用者产生误解。建议用 pub 代替。

【正例】


#![allow(unused)]
fn main() {
mod internal {
    pub fn internal_fn() { }
}
}

【反例】


#![allow(unused)]
fn main() {
mod internal {
    pub(crate) fn internal_fn() { }
}
}

包管理

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下的测试覆盖和静态检查情况。


G.CAR.01 当项目是可执行程序而非库时,建议使用 src/main.rssrc/lib.rs 模式

【级别:规则】

按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【描述】

crate 结构类似于:

src/
  -- lib.rs
  -- main.rs

src/
  -- lib.rs
bin/
  -- main.rs

这样的好处有:

  1. 便于单元测试。
  2. 这样拆分有利于面向接口思考,让代码架构和逻辑更加清晰。

如果你编写的可执行程序比较复杂时,在 main.rs里需要依赖太多东西,那就需要创建 Workspace, 把 main.rs 在独立为一个 crate 了,而在这个 crate 内也没有必要再拆分为 mainlib 了。

G.CAR.02 Crate 的 Cargo.toml 中应该包含必要的元信息

【级别:规则】

按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
cargo_common_metadatayesnocargoallow

【描述】

在 Cargo.toml 中应该包含必要的元信息,以便使用者知道它的作用。并且后续上传到crates.io上,这些信息也是必须的。

【正例】

# This `Cargo.toml` includes all common metadata
[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"]

【反例】

# This `Cargo.toml` is missing a description field:
[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"]

G.CAR.03 Feature 命名应该避免否定式或多余的前后缀

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
negative_feature_namesyesnocargoallow
redundant_feature_namesyesnocargoallow

【描述】

Feature 命名应该避免出现 no-not- 之类的否定前缀,或诸如 use-with- 前缀或 -support后缀。Feature 的目的是正向的,可选的特性,使用否定式命名和它的目的背道而驰。

【正例】

[features]
default = ["abc", "def", "ghi"]
abc = []
def = []
ghi = []

【反例】

# The `Cargo.toml` with negative feature names
[features]
default = ["with-def", "ghi-support"]
no-abc = []
with-def = []   // redundant
ghi-support = []   // redundant

G.CAR.04 Cargo.toml 中依赖包版本不要使用通配符

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
wildcard_dependenciesyesnocargoallow

【描述】

【正例】

[dependencies]
regex = "1.5"

【反例】

[dependencies]
regex = "*"

Rust 通过宏来支持元编程。其中宏有很多种,按实现方式可以分为两大类:声明宏(Declarative) 和 过程宏(Procedural)。

按功能效果,过程宏又可以分为三类:

  1. Bang 宏。类似于声明宏那样,像函数调用一样去使用的宏。
  2. Derive 宏。用于为数据类型自动生成一些 语法项(item),比如 trait 、结构体、方法等。
  3. Attrubutes 宏。用于更加通用的代码生成功能。

Rust 语言核心库和标准库,都内置了一些声明宏和过程宏,以方便开发者使用。

内置的属性宏按功能大体又可以分为四类:

  1. 测试属性。#[test] 属性宏用于将某个函数标记为单元测试函数。
  2. 诊断(Diagnostic)属性。用于在编译过程中控制和生成诊断信息。包括:
    1. allow(c)/ warn(c)/ deny(c)/ forbid(c) 等。
    2. #[must_use]
  3. 代码生成属性。包括:inline / cold / \#[target_feature] 等。
  4. 编译时限制属性。包括:recursion_limit / type_length_limit
  5. 类型系统属性。包括: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 社区顶级专家 Dtolnay 写的 宏学习案例

P.MAC.02 实现宏语法的时候,应该尽量贴近 Rust 语法

【描述】

Rust 宏可以让开发者定义自己的DSL,但是,在使用宏的时候,要尽可能贴近Rust的语法。这样可以增强可读性,让其他开发者在使用宏的时候,可以猜测出它的生成的代码。

【正例】


#![allow(unused)]
fn main() {
bitflags! {
    struct S: u32 { /* ... */ }
}

// 也要注意结尾是正确的分号或逗号
bitflags! {
    struct S: u32 {
        const C = 0b000100;
        const D = 0b001000;
    }
}
}

【反例】


#![allow(unused)]
fn main() {
// ...over no keyword...
bitflags! {
    S: u32 { /* ... */ }
}

// ...or some ad-hoc word.
bitflags! {
    flags S: u32 { /* ... */ }
}

// or
bitflags! {
    struct S: u32 {
        const E = 0b010000, // 结尾应该是分号更符合 Rust 语法
        const F = 0b100000,
    }
}
}

G.MAC.01 dbg!() 宏只应该在 Debug 模式下使用

【级别:规则】

按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
dbg_macroyesnorestrictionallow

【描述】

dbg!() 宏是 Rust 内置的宏,其目的是用于调试代码,仅用于 Debug 模式。

将其用在 Release 模式下,调试信息也会被打印出来,不安全。

【正例】


#![allow(unused)]
fn main() {
// Debug 模式编译
let foo = false;
dbg!(foo); 

// Release 模式编译
let foo = false;
// dbg!(foo); 
}

【反例】


#![allow(unused)]
fn main() {
// Release 模式编译
let foo = false;
dbg!(foo); 
}

G.MAC.02 在多个地方使用println!panic! 之类的内置宏 时,可以将其包装到函数内,使用 #[cold]#[inline(never)] 属性避免其内联,从而避免编译文件膨胀

【级别:建议】

建议按此规范执行

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【描述】

因为像 println!panic! 之类的宏,如果到处使用,就会到处展开代码,会导致编译文件大小膨胀。尤其在嵌入式领域需要注意。

【正例】


#![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,
    }
}
}

【反例】


#![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,
    }
}
}

声明宏

声明宏 也被叫做 示例宏(macros by example),或者简单地叫做 宏。目前声明宏使用 macro_rules!来定义。

声明宏的特点是,它只用作 代码替换,而无法进行计算。


P.MAC.Decl.01 不要将声明宏内的变量作为外部变量使用

【描述】

声明宏是半卫生(semi-hygienic)宏,其内部元变量(metavariables)不可作为外部变量去使用。

但是对于泛型参数(包括生命周期参数)是不卫生的,所以要小心使用。

【正例】

macro_rules! using_a {
    ($a:ident, $e:expr) => {{
        let $a = 42;
        $e
    }};
}
fn main() {
    let four = using_a!(a, a / 10);
}

【反例】


#![allow(unused)]
fn main() {
macro_rules! using_a {
    ($e:expr) => {
        {
            let a = 42;
            $e
        }
    }
}

let four = using_a!(a / 10); // build error:  cannot find value `a` in this scope
}

P.MAC.Decl.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.Decl.03 不要在片段分类符(fragment-specifier)跟随它不匹配的符号

【描述】

macro_rules! 定义声明宏时,非终止的元变量匹配必须紧随一个已被决定可以在这种匹配之后安全使用的标记。

具体的规则参见: Follow-set Ambiguity Restrictions

【正例】

该示例中,元变量$e1的 片段分类符expr 是非终止的,所以后面需要跟随一个用于分隔的标记。

Rust 规定在 expr 片段分类符 后面可以合法地跟随 => / , / ;


#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! foo {
    ( $e1:expr, $e2:expr) => {$e1; $e2}; 
}

}

【 反例】

对于 [,] 这样的分隔标记就是非法的。这是为了防止未来 Rust 语法变动导致宏定义失效。


#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! foo {
    ( $e1:expr [,] $e2:expr) => {$e1; $e2}; 
}

}

P.MAC.Decl.04 匹配规则要精准,不要模糊不清

【描述】

匹配规则必须精准,因为宏解析器并不会去执行代码,它无法匹配模糊不清的规则。

【正例】

macro_rules! ambiguity {
    ($i2:ident $($i:ident)* ) => { };
}

// ok
fn main() { ambiguity!(an_identifier  an_identifier2); }

【 反例】

宏解析器无法确定第一次匹配的应该是多少个 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); }

P.MAC.Decl.05 使用宏替换(substitution)元变量的时候要注意选择合适的片段分类符(fragment-specifier

【描述】

使用宏替换(substitution)元变量,就是指把已经进行过宏解析的 token 再次传给宏,需要注意,此时传入的 token,已经被看作是宏解析器解析后的 AST 节点了。

【正例】

满足示例这类正常匹配情况的目前只有 ttident 或者 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

【 反例】

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])

P.MAC.Decl.06 当宏需要接收 self时需要注意

【描述】

self 在 Rust 中属于关键字,它会在代码运行时被替换为具体类型的实例。当它传递给 宏 时,它会被看做一个变量,而宏对于变量而言,是具备卫生性的。而且,声明宏的作用只是替换,而非计算,它并不能计算出 self 的具体类型。

【正例】

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);
}

【反例】

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);
}

P.MAC.Decl.07 确保在宏定义之后再去调用宏

【描述】

Rust 中类型或函数,你可以在定义前后都可以调用它,但是宏不一样。 Rust 查找宏定义是按词法依赖顺序的,必须注意定义和调用的先后顺序。

【正例】

macro_rules! X { () => {}; }
mod a {
    X!(); // defined
}
mod b {
    X!(); // defined
}
mod c {
    X!(); // defined
}
fn main() {}

【反例】

mod a {
    // X!(); // undefined
}
mod b {
    // X!(); // undefined
    macro_rules! X { () => {}; }
    X!(); // defined
}
mod c {
    // X!(); // undefined
}
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.Decl.08 同一个 crate 内定义的宏相互调用时,需要注意卫生性

【描述】

当同一个 crate 内定义的宏相互调用时候,应该使用 $crate 元变量来指代当前被调用宏的路径。

【正例】


#![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!();
}
}

【反例】


#![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!();
}
}

过程宏

过程宏(Procedural macros 允许开发者来创建语法扩展。你可以通过过程宏创建类似函数的宏、派生宏以及属性宏。

广义上的"过程宏"指的是通过 syn/quote(毕竟几乎全部过程宏库都用 syn) 及 syn 生态(例如 darling) 进行代码生成等元编程操作。

syn/quote 不仅能用于过程宏,还广泛用于代码生成(codegen)、静态分析等用途,例如 tonic-build/prost 源码中也用到了 syn/quote 。

因此本过程宏规范不仅适用于过程宏,部分规范(例如 P.MAC.Proc.06 )还适用于 prost 这种代码生成库

过程宏必须被单独定义在一个类型为proc-macro 的 crate 中。

过程宏有两类报告错误的方式:Panic 或 通过 compile_error 宏调用发出错误。

过程宏不具有卫生性(hygiene),这意味着它会受到外部语法项(item)的影响,也会影响到外部导入。

过程宏可以在编译期执行任意代码。


P.MAC.Proc.01 不要使用过程宏来规避静态分析检查

【描述】

不要利用过程宏来定义能规避 Rust 静态分析检查的宏。

【正例】

对于不安全的函数,应该显式地使用 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(); }
}

【反例】

在 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();
}

【相关讨论】

P.MAC.Proc.02 实现过程宏时要对关键特性增加测试

【描述】

实现过程宏的时候,要对关键特性增加测试,这是为了避免出现关键特性遗漏的情况。

【正例】

在第三方库 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] },
        }
    ));
}
}

【反例】

在第三方库 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!()
    }
}
}

P.MAC.Proc.03 保证过程宏的卫生性

【描述】

过程宏生成的代码尽量使用完全限定名,防止命名冲突产生意想不到的后果。

【正例】


#![allow(unused)]
fn main() {
quote!(::std::ToString::to_string(a))
}

#![allow(unused)]
fn main() {
quote! {{
    use ::std::ToString;
    a.to_string()
}}
}

【反例】


#![allow(unused)]
fn main() {
quote!(a.to_string())
}

【测试】

使用#![no_implicit_prelude]属性来验证过程宏的卫生性。


#![allow(unused)]
#![no_implicit_prelude]

fn main() {
#[derive(MyMacro)]
struct A;
}

P.MAC.Proc.04 给出正确的错误位置

【描述】

过程宏发生错误时,返回的错误应该有正确的位置信息。

【正例】


#![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!()
}
}

【反例】


#![allow(unused)]
fn main() {
// 直接用Span::call_site()
Error::new(Span::call_site(), "requires unit variant")
    .to_compile_error()
    .into()
}

P.MAC.Proc.05 代码生成要按情况选择使用过程宏还是build.rs

【描述】

用过程宏生进行代码生成,比如生成新类型或函数,有一个缺点就是:IDE无法识别它们,影响开发体验。

但是使用build.rs生成的代码,对 IDE 更友好。

不过随着 IDE 的增强,过程宏以后应该也能变得更加 IDE 友好。

建议按应用场景选择:

  • build.rs 一般用于根据外部文件生成代码的场景。比如根据 C 头文件生成 Rust 绑定,或者根据 proto 文件生成相应的 Rust 类型等,供开发者直接使用。
  • 过程宏一般用于消除样例式代码,提升库使用者的开发体验。

【正例】

build.rstonic 生成的代码直接放在 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");
}

tarpcservice宏会生成一个新的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.MAC.Proc.06 build.rs 生成的代码要保证没有任何警告

【描述】

build.rs 生成的代码(codegen),要通过或忽略 clippy 检查,不要让用户/库的使用者自行忽略

codegen 库要保证生成的代码应该非常干净没有任何警告,不应该让库的使用者去处理生成代码中的警告

【正例】

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::*;
}

【反例】

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
);
}

多线程

Rust 天生线程安全,可以有效消除数据竞争。


多线程并发可以分为两类:

锁同步

Rust 中多线程并发使用锁来进行线程同步。


P.MTH.lock.01 首选 parking_lot 中定义的 同步原语,而非标准库 std::sync 模块

【描述】

标准库中 std::sync 模块中实现的锁同步原语,存在一些问题,比如需要使用 Box<T> 将操作系统锁原语维持在同一个内存位置,这点浪费内存。而 parking_lot 的实现则更加轻量和正确,性能也更好,比如 parking_lotMutexRwlock 都支持 最终公平性,在不失性能的基础上保证公平。 目前官方正在推动 parking_lot 进入标准库中。在使用 parking_lot 时注意和标准库的区别。

parking_lot 也提供了一些有用的 feature,比如 死锁检测(deadlock detection),在使用 Mutex 的时候,可以打开这个特性,可以在编译期发现死锁,没准可以节省你很多时间。

P.MTH.lock.02 根据场景选择使用互斥锁还是 Channel

【描述】

不要从哪种方式更快的角度来考虑,而应该从使用场景。性能取决于你如何使用它们。

一个简单的指南:

Channel 适用于Mutex 适用于
传递数据所有权
分发工作单元
传递异步结果
修改共享缓存
修改共享状态

P.MTH.lock.03 如果要使用 Channel 建议使用 crossbeamflume

【描述】

标准库中的 channel 实现并不好,也许会被移使用 crossbeamflume 目前是约定俗成。

P.MTH.lock.04 多线程下要注意识别锁争用的情况,避免死锁

【描述】

Rust 并不能保证没有死锁,要注意 LockResult<MutexGuard<'_, T>> 的生命周期,以防止出现锁争用的情况。


G.MTH.lock.01 对 布尔 或 引用 并发访问应该使用原子类型而非互斥锁

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
mutex_atomicyesnoperfwarn

【描述】

使用原子类型性能更好。但要注意指定合理的内存顺序。

【正例】


#![allow(unused)]
fn main() {
let x = AtomicBool::new(y);
}

【反例】


#![allow(unused)]
fn main() {
let x = Mutex::new(&y);
}

G.MTH.lock.02 多线程环境下要使用 Arc 代替 Rc

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
rc_mutexyesnorestrictionallow

【描述】

Rc 是专门用于单线程的,多线程下应该用 Arc

【正例】


#![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>>) { ... }
}

【反例】


#![allow(unused)]
fn main() {
use std::rc::Rc;
use std::sync::Mutex;
fn foo(interned: Rc<Mutex<i32>>) { ... }
}

G.MTH.lock.03 建议使用 Arc<str>/ Arc<[T]> 来代替 Arc<String> / Arc<Vec<T>>

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
rc_bufferyesnorestrictionallow

【描述】

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: &str = "hello world";
    let b: Rc<str> = Rc::from(a);
    println!("{}", b);

    // or equivalently:
    let b: Rc<str> = a.into();
    println!("{}", b);

    // we can also do this for Arc,
    let a: &str = "hello world";
    let b: Arc<str> = Arc::from(a);
    println!("{}", b);
}

【反例】

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

    // or equivalently:
    let a = "hello world".to_string();
    let b: Rc<String> = a.into();
    println!("{}", b);

    // we can also do this for Arc,
    let a = "hello world".to_string();
    let b: Arc<String> = Arc::from(a);
    println!("{}", b);
}

G.MTH.lock.04 尽量避免直接使用标准库 std::sync 模块中的同步原语,替换为 parking_lot

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制化参考】 这条规则如果需要定制 Lint,则可以扫描 std::sync 锁同步原语的使用,推荐优先选择 crate parking_lot 中对应的同步原语。

【描述】

尽量避免对标准库 std::sync 模块中锁同步原语的使用,建议使用 parking_lot 的实现。

【正例】

例子来源于 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();
}

【反例】

来源于 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();
}

G.MTH.lock.05 尽量避免直接使用标准库 std::sync::mpsc 模块中的channel,替换为 crossbeam

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制化参考】 这条规则如果需要定制 Lint,则可以扫描对 std::sync::mpsc::channel 的使用,推荐优先选择 crate crossbeam

【描述】

尽量避免使用 std::sync::mpsc::channel ,建议使用 crossbeam

【正例】


#![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);
}
}

【反例】

例子来源于 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);
}
}

无锁并发

Rust 也支持原子类型,其内存顺序模型与 C++ 20 相同。


P.MTH.lockfree.01 除非必要,否则建议使用同步锁

【描述】

不要认为无锁编程性能就一定高,并且需要注意的地方比使用同步锁都多,比如 指令重排 、ABA 问题、 内存顺序是否指定正确等。

正确实现无锁编程比使用同步锁要困难很多。所以,除非有必要,否则直接使用同步锁就可以。

也有一些 性能测试 作为参考,原子类型的性能比互斥锁的性能大概要好四倍左右。所以,当在同一个临界区内要有超过四次原子操作,也许使用互斥锁更加简单一些。

P.MTH.lockfree.02 如有必要使用无锁编程,那么内存顺序可以默认使用 Ordering::SeqCst

【描述】

使用 Ordering::SeqCst 内存顺序更加安全一些,并且性能在一般情况下够用,除非性能不够用,想进一步压榨性能的时候,再去考虑合理使用其他内存顺序。

异步编程


P.ASY.01 异步编程并不适合所有场景,计算密集型场景应该考虑同步编程

【描述】

异步编程适合 I/O 密集型应用,如果是计算密集型场景应该考虑使用同步编程。

P.ASY.02 异步编程中要避免阻塞操作

【描述】

异步编程中如果出现阻塞,则会阻止同一线程上其他异步任务的执行,从而导致很大的延迟,或者死锁。


G.ASY.01 在 async 块/函数中调用 async 函数/闭包请不要忘记添加.await

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
async_yields_asyncyesnocorrectnessdeny

也有例外情况。

【描述】

【正例】


#![allow(unused)]
fn main() {
async fn foo() {}

fn bar() {
  let x = async {
    foo().await
  };
}
}

【反例】


#![allow(unused)]
fn main() {
async fn foo() {}

fn bar() {
  let x = async {
    foo()
  };
}
}

【例外】


#![allow(unused)]
fn main() {
// https://docs.rs/crate/fishrock_lambda_runtime/0.3.0-patched.1/source/src/lib.rs#:~:text=clippy%3a%3aasync_yields_async

#[allow(clippy::async_yields_async)]
let task = tokio::spawn(async move { handler.call(body, ctx) });

let req = match task.await {
    // ...
}

}

G.ASY.02 在 跨await 调用中持有同步互斥锁需要进行处理

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
await_holding_lockyesnopedanticallow

【描述】

同步互斥锁本来就不是为异步上下文跨 await 调用而设计的,在这种场景中使用同步互斥锁容易造成死锁。当同步互斥锁被跨 await 时,有可能很长时间都不会返回这个调用点,在其他任务中再次用到这个互斥锁的时候,容易造成死锁。

这里有三种解决方案:

  1. 使用异步互斥锁。但是异步互斥锁的开销要大于同步互斥锁。
  2. 确保同步互斥锁在调用 await 之前已经释放。

【正例】

use std::sync::Mutex;
// 使用同步互斥锁
async fn foo(x: &Mutex<u32>) {
  {
    let guard = x.lock().unwrap();
    *guard += 1;
  }
  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.");
}

【反例】


#![allow(unused)]
fn main() {
use std::sync::Mutex;

async fn foo(x: &Mutex<u32>) {
  let guard = x.lock().unwrap();
  *guard += 1;
  bar.await;
}
}

【例外】


#![allow(unused)]
fn main() {
    // FROM: https://github.com/khonsulabs/kludgine/blob/main/app/src/runtime/smol.rs#L31
    // 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);
    });
}

G.ASY.03 在 跨await 调用持有RefCell的引用需要进行处理

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
await_holding_refcell_refyesnopedanticallow

【描述】

跟不要在异步上下文中跨 await 使用 同步互斥锁类似,使用 RefCell 的独占(可变)借用会导致 Panic。因为 RefCell 是运行时检查独占的可变访问,如果 跨 await 持有一个可变引用则可能会因为共享的可变引用而引起 Panic。

这种共享可变在编译期是无法被检查出来的。

【正例】


#![allow(unused)]
fn main() {
use std::cell::RefCell;

async fn foo(x: &RefCell<u32>) {
  {
     let mut y = x.borrow_mut();
     *y += 1;
  }
  bar.await;
}
}

【反例】


#![allow(unused)]
fn main() {
use std::cell::RefCell;

async fn foo(x: &RefCell<u32>) {
  let mut y = x.borrow_mut();
  *y += 1;
  bar.await;
}
}

【例外】

await 持有 RefCell 的可变借用,但是当前场景确信永远不会 Panic,则可以使用。


#![allow(unused)]
fn main() {
// From : https://github.com/MattiasBuelens/wasm-streams/blob/master/src/readable/into_underlying_byte_source.rs#L65
let fut = async move {
    // This mutable borrow can never panic, since the ReadableStream always queues
    // each operation on the underlying source.
    //  这个可变借用永远不会恐慌,因为 ReadableStream 对底层源的每个操作总是有序的。
    let mut inner = inner.try_borrow_mut().unwrap_throw();
    inner.pull(controller).await
};
}

G.ASY.04 避免定义不必要的异步函数

【描述】

如果一个异步函数内部没有任何异步代码,相比一个同步函数,它会产生额外的调用成本。

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
unused_asyncyesnopedanticallow

【正例】


#![allow(unused)]
fn main() {
fn add(value: i32) -> i32 {
    value + 1
}
}

【反例】


#![allow(unused)]
fn main() {
async fn add(value: i32) -> i32 {
    value + 1
}
}

G.ASY.05 避免在异步处理过程中包含阻塞操作

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制化参考】 这条规则如果需要定制Lint,则可以扫描异步过程,找到黑名单定义的阻塞操作调用,进行告警。

【描述】

避免在异步编程中使用阻塞操作。

【正例】

使用异步运行时,如tokio提供的非阻塞函数


#![allow(unused)]
fn main() {
use tokio::fs;

async fn read_file() -> std::io::Result<()> {
    let _ = fs::read_to_string("test.txt").await?;
    Ok(())
}
}

【反例】

不要在异步流程中使用阻塞操作函数


#![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")
}
}

Unsafe Rust

Unsafe Rust 是 Safe Rust 的超集,意味着在 Unsafe Rust 中也会有 Safe Rust的安全检查。但是 Unsafe Rust 中下面五件事是Safe Rust 的检查鞭长莫及的地方:

  1. 解引用裸指针
  2. 调用 unsafe函数(C函数,编译器内部函数或原始分配器)
  3. 实现 unsafe trait
  4. 可变静态变量
  5. 访问 union 的字段

使用 Unsafe Rust 的时候,需要遵守一定的规范,这样可以避免未定义行为的发生。

关于 Unsafe Rust 下的一些专用术语可以查看 Unsafe 代码术语指南

Unsafe Rust 的语义:这是编译器无法保证安全的地方,需要程序员来保证安全。


本节包含内容如下:


P.UNS.01 不要为了逃避 编译器安全检查而滥用 Unsafe Rust

【描述】

Unsafe Rust 有其应用范围和目标,不要为了逃避 编译器安全检查而随便滥用 Unsafe Rust。

P.UNS.02 不要为了提升性能而盲目使用 Unsafe Rust

【描述】

对比 Safe 代码的性能看是否够用,就可以减少不必要的 Unsafe。


G.UNS.01 不要随便为 带有 unsafe命名的 类型或方法创建别名

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
unsafe_removed_from_nameyesnostylewarn

【描述】

Rust 里 unsafe 字样用于提醒开发者在编写代码的时候注意保证安全。如果修改别名,隐藏了这种提醒,不利于展示这种信息。

不利于开发者去保证安全。

【正例】


#![allow(unused)]
fn main() {
use std::cell::{UnsafeCell  };

extern crate crossbeam;
use crossbeam::{spawn_unsafe  };
}

【反例】


#![allow(unused)]
fn main() {
use std::cell::{UnsafeCell as TotallySafeCell};

extern crate crossbeam;
use crossbeam::{spawn_unsafe as spawn};
}

安全抽象规范

使用 Unsafe Rust 的一种方式是将 Unsafe 的方法或函数进行安全抽象,将其变成安全的方法或函数。

Unsafe Rust 中 API 的安全性设计通常有两种方式:

  1. 将内部的 unsafe API 直接暴露给 API 的使用者,并且使用 unsafe 关键字来声明该 API 是非安全的,同时也需要对安全边界条件添加注释。
  2. 对 API 进行安全封装,即,安全抽象。在内部使用断言来保证当越过安全边界时可以 Panic,从而避免 UB 产生。

第二种方式,对 Unsafe 代码进行安全抽象,是 Rust 生态的一种约定俗成。


P.UNS.SafeAbstract.01 代码中要注意是否会因为 Panic 发生而导致内存安全问题

【描述】

Panic 一般在程序达到不可恢复的状态才用,当然在 Rust 中也可以对一些实现了 UnwindSafe trait 的类型捕获恐慌。

当 Panic 发生时,会引发栈回退(stack unwind),调用栈分配对象的析构函数,并将控制流转移给恐慌处理程序中。所以,当恐慌发生的时候,当前存活变量的析构函数将会被调用,从而导致一些内存安全问题,比如释放已经释放过的内存。

通常, 封装的Unsafe 代码可能会暂时绕过所有权检查,而且,安全封装的 API 在内部unsafe 代码的值返回之前,会根据安全边界条件确保它不会违反安全规则。但是,假如封装的Unsafe 代码发生了恐慌,则其外部安全检查可能不会执行。这很可能导致类似 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() 发生了恐慌,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 编码字符串引发恐慌
    "0è0".to_string().retain(|_| {
        match the_number_of_invocation() {
            1 => false,
            2 => true,
            _ => panic!(),
        }
    });
}

P.UNS.SafeAbstract.02 Unsafe 代码编写者有义务检查代码是否满足安全不变式

【描述】

安全不变式(见 Unsafe 代码术语指南 )是 Rust 里的安全函数,在任何有效输入的情况下,都不应该发生任何未定义行为。

可以从以下三个方面来检查:

  1. 逻辑一致性。
  2. 纯洁性。相同的输入总是要返回相同的输出。
  3. 语义约束。传入的参数要合法,满足数据类型。

【示例】

该代码是为 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.SafeAbstract.03 不要随便在公开的 API 中暴露未初始化内存

【描述】

在公开的API中暴露未初始化内存可能导致 UB。

【正例】


#![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)
}
}

【反例】


#![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)
}
}

P.UNS.SafeAbstract.04 要考虑 Panic Safety 的情况

【描述】

要注意 Panic Safety 的情况,避免双重释放(double free)的问题发生。

在使用 std::ptr 模块中接口需要注意,容易产生 UB 问题,要多多查看 API 文档。

【正例】


#![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)
            }
        }
    )
);
}

【反例】


#![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
                }
        }
    )
);
}

G.UNS.SafeAbstract.01 在 公开的 unsafe 函数的文档中必须增加 # Safety 注释

【级别:必须】

必须按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group默认 level
missing_safety_docyesnoStylewarn

【描述】

在公开(pub)的 unsafe 函数文档中,必须增加 # Safety 注释来解释该函数的安全边界,这样使用该函数的用户才可以安全地使用它。

说明: 该规则通过 cargo clippy 来检测。默认会发出警告。

【示例】

【正例】

示例来自于标准库文档: 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) }
    }
}

【反例】


#![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) }
    }
}

G.UNS.SafeAbstract.02 在 Unafe 函数中应该使用 assert! 而非 debug_assert! 去校验边界条件

【级别:必须】

必须按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group默认 level
debug_assert_with_mut_callyesnonurseryallow

注意该 lint 当前是 Nursery Group,意味着可能会产生误报 Bug。

【描述】

assert! 宏 在 Release 和 Debug 模式下都会被检查,并且不能被禁用。它通常用来在 unsafe 函数中判断传入的参数是否满足某种边界条件,以此来防止不合法的参数传入导致未定义行为。

但是 debug_assert! 则可以通过配置 -C debug-assertions 来禁用它, 而且 debug_assert! 在 Release 模式下也会被编译器优化。所以,一旦使用了 debug_assert! 在 unsafe 函数中用来防范不合法参数,那有可能会失效。

【正例】

来自标准库 slice 的代码示例。


#![allow(unused)]
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) }
    }
}

【反例】


#![allow(unused)]
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) }
    }

   // or
   // 在 debug_assert_eq! 中包含可变引用的调用,
   // 也会因为 debug_assert_ 系列的断言宏在 Release 下产生不可预料的结果,它是 unsafe 的
   debug_assert_eq!(vec![3].pop(), Some(3));
}

G.UNS.SafeAbstract.03 Unsafe 代码中手动实现 auto trait 需要注意

【级别:必须】

必须严格按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制参考】

Lint 需要检测 手工实现 auto trait 的行为,比如 Sync/Send,对开发者发出警告,要注意考虑其安全性

【描述】

所谓 auto trait 是指 Safe Rust中由编译器自动实现的 trait,比如 Send/Sync 。在 Unsafe Rust中就需要手动实现这俩 trait 了。

所以,在手动实现的时候要充分考虑其安全性。

【示例】

Rust futures 库中发现的问题,错误的手工 Send/Sync实现 破坏了线程安全保证。

受影响的版本中,MappedMutexGuardSend/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))));
}

G.UNS.SafeAbstract.04 不要随便在公开的 API 中暴露裸指针

【级别:必须】

必须严格按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制参考】

Lint需要检测在 pub 的结构体、枚举等类型中有裸指针字段或变体,对开发者发出警告,要注意考虑其安全性

【描述】

在公开的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);
}

裸指针操作

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

G.UNS.PTR.01 当指针类型被强转为和当前内存对齐不一致的指针类型时,禁止对其解引用

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
cast_ptr_alignmentyesnostylewarn

【描述】

该 Lint 会检查是否出现 指针类型被强转为和当前内存对齐不一致的指针类型 的情况,要注意不要对这类强转后的指针进行解引用操作,否则会有未定义行为。

【正例】

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

【反例】

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

G.UNS.PTR.02 禁止将不可变指针手工转换为可变指针

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
cast_ref_to_mutyesnocorrectnessdeny

【描述】

因为将不可变指针手工转换为可变指针可能会引发未定义行为。通常有这种需求,合法的手段是使用 UnsafeCell<T>

【正例】


#![allow(unused)]
fn main() {
use std::cell::UnsafeCell;

fn x(r: &UnsafeCell<i32>) {
   unsafe {
       *r.get() += 1;
   }
}
}

【 反例】


#![allow(unused)]
fn main() {
fn x(r: &i32) {
    unsafe {
        *(r as *const _ as *mut _) += 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 _)
}
}

G.UNS.PTR.03 尽量使用 pointer::cast 来代替 使用 as 强转指针

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
ptr_as_ptryesnocorrectnessdeny

【描述】

使用 pointer::cast 方法转换更加安全,它不会意外地改变指针的可变性,也不会将指针转换为其他类型。

【正例】


#![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>();
}

【反例】


#![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;
}

G.UNS.PTR.04 建议使用 NonNull<T> 来替代 *mut T

【级别:必须】

必须严格按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制参考】

检测到包含 *mut T类型的结构体,应该给予开发者警告或建议去使用 NonNull

【描述】

尽量使用 NonNull 来包装 *mut T

NonNull 的优势:

  1. 非空指针。会自动检查包装的指针是否为空。
  2. 协变。方便安全抽象。如果用裸指针,则需要配合 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!();
}
}

G.UNS.PTR.05 使用指针类型构造泛型结构体时,需要使用 PhantomData<T> 来指定 T上的协变和所有权

【级别:必须】

必须严格按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制参考】

检测使用指针类型构造泛型结构体时,如果没有 PhantomData<T> 类型的字段,则需要警告开发者,要考虑 为裸指针配合PhantomData<T>来指定协变和所有权

【描述】

PhantomData<T> 是经常被用于 Unsafe Rust 中配合裸指针来指定协变和所有权的,为裸指针构建的类型保证安全性和有效性。否则,可能会产生未定义行为。

参考: PhantomData<T> 的型变(variance)模式表

【正例】


#![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,并且让 指针有了协变
}
}

【反例】


#![allow(unused)]
fn main() {
// Vec<T> 不拥有类型 T,并且 data 字段的裸指针不支持协变
// 这样的话,是有风险的。
// 为 Vec<T> 实现的 Drop 可能导致 UB
struct Vec<T> {
    data: *const T, 
    len: usize,
    cap: usize,
}
}

联合体(Union)

Union 是没有 tag 的 Enum,Enum 是有 tag 的Union 。

内存布局 Union 和 Enum 相似。

正因为没有 tag,Rust 编译器无法检查当前使用的正是哪个变体,所以,访问 Union 的变体是 Unsafe 的。


G.UNS.Union.01 除了与 C 打交道,尽量不要使用 Union

【级别:必须】

必须严格按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制化参考】

这条规则如果需要定制 Lint,则可以检测 Union 联合体上方是否有 #[repr(C)]属性定义与C兼容的数据布局,如果没有则给予警告。

【描述】

Rust 支持 Union 就是为了和 C 打交道。如果不是 FFi ,就避免使用 Union。

一般情况下请使用 枚举 或 结构体代替。

使用 Copy 类型的值和 ManuallyDrop 来初始化 Union 的变体,不需要使用 Unsafe 块。

【正例】


#![allow(unused)]
fn main() {
#[repr(C)]
union MyUnion {
    f1: u32,
    f2: f32,
}
}

【反例】


#![allow(unused)]
fn main() {
union MyUnion {
    f1: u32,
    f2: f32,
}
}

G.UNS.Union.02 不要把联合体的不同变体用在不同生命周期内

【级别:必须】

必须严格按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制化参考】

检测函数内同一个联合体实例的不同变体被用于不同的生命周期内。

【描述】

对联合体的变体进行借用的时候,要注意其他变体也将在同一个生命周期内。抛开内存布局、安全性和所有权之外,联合体的行为和结构体完全一致,你可以将联合体当中结构体来进行判断。

【反例】


#![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 不能修改其它进程/动态库的存变量

【级别:必须】

必须按此规范执行。

【描述】

不要尝试修改其它进程/动态库的内存数据,否则会出现内存段错误(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 变量的指针返回给调用方,因此是一种「有状态」的函数,多线程环境下可能有线程安全问题

例如线程 A 正在将 glibc 动态库的 gmtime 数据逐个复制回来,结果复制到一半线程 B 调用 gmtime 把后半部分的 gmtime 输出数据给更新掉了导致线程 A 得到的数据有误。

而无重入版本例如 libc::localtime_r 会比 libc::localtime 多一个入参叫 result,

允许调用方进程的内存空间内分配内存,再将调用方进程的可变指针传入到 glibc 中让 glibc 修改可知指针指向的数据。

应当通过工具搜索动态库的函数符号查找可重入版本的函数,或者通过 man 文档查询自己所用函数有没有可重入的版本。

[w@ww repos]$ nm -D /usr/lib/libc.so.6 | grep "_r@"
00000000000bb030 W asctime_r@@GLIBC_2.2.5
00000000000bb100 T ctime_r@@GLIBC_2.2.5
0000000000040a30 T drand48_r@@GLIBC_2.2.5

使用不可重入函数的危害例如 P.UNS.MEM.02 和 P.UNS.MEM.03 规范的反例中的 sqlite3_libversion() 会导致开发人员带来很大的心智负担,需要人工 code review 确保没有线程安全和内存安全问题,因此必须尽量使用可重入版本的函数。

【正例】

chrono 库中用 libc::localtime_r 获取本地时间而不用 libc::localtime

ctime_r, gmtime_r, localtime_r, gethostbyname_r

【反例】

ctime, gmtime, localtime, gethostbyname


G.UNS.MEM.01 使用 MaybeUninit<T> 来处理未初始化的内存

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
uninit_assumed_inityesnocorrectnessdeny
uninit_vecyesnocorrectnessdeny

【描述】

Rust 编译器要求变量要根据其类型正确初始化。

比如引用类型的变量必须对齐且非空。这是一个必须始终坚持的不变量,即使在 Unsafe 代码中也是如此。因此,零初始化引用类型的变量会导致立即未定义行为,无论该引用是否访问过内存。

编译器利用这一点,进行各种优化,并且可以省略运行时检查。

使用前请仔细查看 MaybeUninit<T> 相关文档。

【正例】


#![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);
// or
let mut vec: Vec<MaybeUninit<T>> = Vec::with_capacity(1000);
vec.set_len(1000);  // `MaybeUninit` can be uninitialized
// or
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<T>确实处于初始化状态。当内存尚未完全初始化时调用 assume_init() 会导致立即未定义的行为。


#![allow(unused)]
fn main() {
use std::mem::{self, MaybeUninit};
// 零初始化引用
let x: &i32 = unsafe { mem::zeroed() }; // undefined behavior! ⚠️
// The equivalent code with `MaybeUninit<&i32>`:
let x: &i32 = unsafe { MaybeUninit::zeroed().assume_init() }; // undefined behavior! 
// 布尔值必须初始化
let b: bool = unsafe { mem::uninitialized() }; // undefined behavior! ⚠️
// The equivalent code with `MaybeUninit<bool>`:
let b: bool = unsafe { MaybeUninit::uninit().assume_init() }; // undefined behavior! 
// 整数类型也必须初始化
let x: i32 = unsafe { mem::uninitialized() }; // undefined behavior! ⚠️
// The equivalent code with `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!
}

【例外】

在能保证 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()
        }
    }
}
}

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 trait

【描述】

Rust 里通过结构体包装该指针,并且为该结构体实现 Drop 来保证相关资源可以安全释放。

P.UNS.FFi.04 如果一个函数正在跨越 FFi 边界,那么需要处理恐慌

【描述】

如果让恐慌在跨越 FFi 边界时发生,可能会产生未定义行为。

处理恐慌可以使用 catch_unwind,但是它只对实现了 UnwindSafe trait 的类型起作用。另外一种方法就是避免恐慌,而返回错误码。

【示例】

use std::panic::catch_unwind;

#[no_mangle]
pub extern fn oh_no() -> i32 {
    let result = catch_unwind(|| {
        panic!("Oops!"); // 这里会发生恐慌,需要处理
    });
    match result {
        Ok(_) => 0,
        Err(_) => 1,
    }
}

fn main() {}

P.UNS.FFi.05 建议使用诸如标准库或 libc crate所提供的可移植类型别名,而不是特定平台的类型

【描述】

当与外部(如C或c++)接口交互时,通常需要使用平台相关的类型,如C的intlong等。除了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_charstd::os::raw::c_char 在大多数 64位 linux 上都是相同的。

FFi 接口使用的字符串要符合 C 语言约定,即使用 \0 结尾且中间不要包含 \0字符的字符串。

Rust 中字符串要求 utf-8 编码,而 C 字符串则没有这个要求。所以需要注意编码。

【正例】


#![allow(unused)]
fn main() {
let f = libc::fopen("/proc/uptime\0".as_ptr().cast(), "r\0".as_ptr().cast());
}

【反例】


#![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")
}

P.UNS.FFi.07 不要为任何传入到外部的类型实现 Drop

【描述】

因为有可能在传出去之前被析构。需要明确是由哪种语言负责分配和释放内存,谁分配内存,谁来释放。

P.UNS.FFi.08 FFi 中要进行合理的错误处理

【描述】

不同类型的错误代码,需要不同的处理方式:

  1. 无字段枚举,应该转换为数字并且作为返回码。
  2. 数据承载(有字段)枚举,应该转换为携带错误信息的整数码。
  3. 自定义错误类型应该使用 兼容 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 }
    }
}
}

G.UNS.FFi.01 自定义数据类型要保证一致的数据布局

【级别:必须】

必须严格按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制化参考】

检测 -sys-ffi 后缀的crate 或 模块内的自定义结构体、enum、union有没有指定 repr 布局

【描述】

Rust 编译器为了优化内存布局,会对结构体字段进行重排。所以在 FFi 边界,应该注意结构体内存布局和 C 的一致。

关于 如何选择合适的repr 属性可参考:P.UNS.MEM.01

以下是不合适用于和 C 语言交互的类型:

  1. 没有使用任何 #[repr( )] 属性修饰的自定义类型
  2. 动态大小类型 (dynamic sized type)
  3. 指向动态大小类型对象的指针或引用 (fat pointers)
  4. 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,
}
}

G.UNS.FFi.02 不要在 FFi 中使用 任何零大小类型

【级别:必须】

必须严格按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制参考】

Lint 可检测 -sys-ffi 后缀的 crate 或 模块内有使用零大小类型,对其产生警告

【描述】

零大小类型在 C 中是无效的。也不要把 Rust 中的单元类型 () 和 C 中的 void 混为一谈。

G.UNS.FFi.03 从外部传入的不健壮类型的外部值要进行检查

【级别:必须】

必须严格按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制参考】

lint 可检测 extern fn 函数参数类型,如果是 布尔类型、引用类型、函数指针、枚举、浮点数、或包含前面类型的复合类型,则需要警告开发者注意对这些类型的健壮性检查。

【描述】

Safe Rust 会保证类型的有效性和安全性,但是 Unsafe Rust 中,特别是编写 FFi 的时候,很容易从外部传入无效值。

Rust 中很多类型都不太健壮:

  • 布尔类型。外部传入的 布尔类型可能是数字也可能是字符串。
  • 引用类型。Rust 中的引用仅允许执行有效的内存对象,但是在Unsafe 中使用引用,任何偏差都可能引起未定义行为。
  • 函数指针。跨越 FFi 边界的函数指针可能导致任意代码执行。
  • Enum。 跨 FFi 边界两端的 枚举值要经过合法转换。
  • 浮点数。
  • 包含上述类型的复合类型

Unsafe I/O

Rust 标准库提供了 I/O 安全性,保证程序持有私有的原始句柄(raw handle),其他部分无法访问它。但是 FromRawFd::from_raw_fd 是 Unsafe 的,所以在 Safe Rust中无法做到 File::from_raw(7) 这种事。 在这个文件描述符上面进行 I/O 操作,而这个文件描述符可能被程序的其他部分私自持有。


G.UNS.IO.01 在使用原始句柄的时候,要注意 I/O 安全性

【级别:必须】

必须严格按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Group是否可定制
_nono_yes

【定制参考】

检测在 IO 时使用 as_raw_fd 调用时,警告开发者这是 Unsafe 的,要对传入的原始文件描述符的安全性进行考察。

【描述】

很多 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();
}

该示例中,rs 是互为别名,因为它们都指向 u 的内存。

然而,headtail 不是互为别名,head 指向 u 的第一个字节,tail 指向其余字节。但是 headtail 共同与 s 互为别名。

内存范围(Span)是指 引用或指针 指向值(Value)的大小,主要依赖于类型,按以下方式确定:

  1. 对于一个是 Sized 的类型T,用 size_of::<T>() 可以获取 T的引用或指针的 内存范围长度。
  2. T不是 Sized 时,就有点麻烦:
    1. 如果你有一个引用r,你可以使用size_of_val(r)来确定该引用的内存范围。
    2. 如果你有一个指针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。

有效但不安全的一个示例是 &strString 类型。在 Unsafe Rust 下,可能会出现违反 UTF-8 编码的字符串,而 &strString 假设字符串都是合法的 UTF-8 编码,所以可能会出现 UB。

数据必须是有效的,但它只在安全的代码中才能保证安全。

未定义行为 (Undefined Behavior)

程序员承诺,代码不会出现未定义行为。作为回报,编译器承诺以这样的方式编译代码:最终程序在实际硬件上的表现与源程序根据Rust抽象机的表现相同。如果发现程序确实有未定义的行为,那么程序员和编译器之间的契约就无效了,编译器产生的程序基本上是垃圾(特别是,它不受任何规范的约束;程序甚至不一定是格式良好的可执行代码)。

未定义行为列表:

  • 数据竞争。
  • 解引用悬空指针或者是未对齐指针
  • 打破指针别名规则(引用生命周期不能长于其引用的对象,可变引用不能被别名)。
  • 使用错误的 调用 ABI
  • 执行使用当前执行线程不支持的目标特性(target features)编译的代码
  • 产生无效的值
    • 01 表达的 bool
    • 具有无效判别式的 枚举
    • [0x0, 0xD7FF] [0xE000, 0x10FFFF] 范围之外的 字符
    • 来自于未初始化内存的整数、浮点数、指针读取或字符串
    • 悬垂引用或 Box
    • 宽引用、Box 或 裸指针有无效的元数据
      • dyn Trait 如果元数据不是指向, Trait 与指针或引用指向的实际动态 trait 匹配的 vtable,的指针,则元数据无效
      • 如果长度无效,则切片数据无效
    • 具有自定义无效值的类型,比如 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,具有以下属性:

  1. 对任何字节都有效。与 MaybeUninit<u8>具有相同有效性。
  2. 复制 Pad 时忽略源字节,并向目标字节写入任何值。
  3. 复制 Pad 标记目标为未初始化。

位置(Place)

位置是 对 位置表达式的求值结果。在其他语言中,一般将其称为左值。

位置基本上是一个指针,但可能包含更多信息,比如 大小、 对齐方式等。

关于位置的关键操作:

  1. 在其中存储相同类型的值(当它用于赋值的左侧时,let 绑定)
  2. 从它那里加载一个相同类型的值
  3. 使用 &* 操作符在一个位置(T)和一个指针值(&T / &mut T/ *const T/ 或 *mut T )之间转换。

值(Value)

值是对值表达式的求值结果,或是被存储在某个地方的东西。 在其他语言中,一般将其称为右值。

表征(Representation)

表征,用于描述一个类型的值和内存中存储这个值的字节序列之间的关系。

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_handler])以确保安全

【描述】

恐慌处理程序的编写应该非常谨慎,以确保程序的安全。

P.EMB.02 要确保程序中的类型有正确的内存布局

【描述】

链接器决定 no-std 程序的最终内存布局,但我们可以使用链接器脚本对其进行一些控制。链接器脚本给我们的布局控制粒度是在 段( Section) 级别。段是在连续内存中布置的 符号 集合。反过来,符号可以是数据(静态变量)或指令(Rust 函数)。

这些编译器生成的符号和段名称不能保证在 Rust 编译器的不同版本中保持不变。但是,Rust 允许我们通过以下属性控制符号名称和部分位置:

  • #[export_name = "foo"]将符号名称设置为foo.
  • #[no_mangle]意思是:使用函数或变量名(不是它的完整路径)作为它的符号名。 #[no_mangle] fn bar()将产生一个名为 的符号bar
  • #[link_section = ".bar"]将符号放置在名为 的部分中.bar

通过这些属性,我们可以公开程序的稳定 ABI 并在链接描述文件中使用它。

P.EMB.03 将一些公用的类型、函数、宏等集中到一个自定义的 baremetal-std

【描述】

虽然 no-std 下不能用Rust 的标准库,但是可以自定义 no-std 下的标准库 baremetal-std,用于积累 no-std 下常用的公共库。

I/O

在标准库中也提供了标准 I/O 类型,在 Safe Rust 下,I/O 操作是足够安全的,但是对于 原生句柄 (Raw Fd) 的操作,则属于不安全。

在 Unsafe Rust 下也有相关 I/O 的规范,请参加 Unsafe Rust - I/O 部分。

本部分只关注 Safe Rust 下 I/O 相关规范。


P.IO.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(())
}
}

P.IO.02 使用 read_to_end/read_to_string方法时注意文件的大小能否一次性读入内存中

【描述】

对于内存可以一次性读完的文件,可以使用 read_to_end/read_to_string之类的方法。但是如果你想读任意大小的文件,则不适合使用它们。

Security

Security 用于规范可能引起信息安全(Security)缺陷的代码实现,而非功能安全( Safety)类问题。


G.Security.01 代码中不要出现非法 Unicode 字符,也要防范非法 Unicode 字符

【级别: 必须】

必须按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
invisible_charactersyesnocorrectnessdeny
text-direction-codepoint-in-commentnoyes-deny
text_direction_codepoint_in_literalnoyes-deny
confusable_identsnoyes-warn
mixed_script_confusablesnoyes-warn

【描述】

非法 Unicode 字符可能引起安全问题。安全问题参见: Rust 编译器安全公告(CVE-2021-42574)

禁止的 Unicode 字符类别为:

  1. 隐藏的 Unicode 字符
  2. 双向 Unicode 字符文本
  3. 同形 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_confusablesconfusable_idents 可以识别 同形字符。

写代码的时候需要注意,尤其是开源代码,需要防范上述非法 Unicode 字符。

【正例】

#![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 字符
            ]
        )
    )
 }
}

其他

G.OTH.01 对于某些场景下不建议使用的方法可以通过配置 clippy.toml 来拒绝

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
disallowed_methodyesnonurseryallow
disallowed_script_identsyesnorestrictionallow
disallowed_typeyesnonurseryallow

这些lint 作用相似,但注意nursery 的lint 还未稳定。

【描述】

有些场合可能需要拒绝使用一些容易出错的方法或函数,可以在 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)]
fn main() {
// Example code where clippy issues a warning
let xs = vec![1, 2, 3, 4];
xs.leak(); // Vec::leak is disallowed in the config.
// The diagnostic contains the message "no leaking memory".

let _now = Instant::now(); // Instant::now is disallowed in the config.

let _box = Box::new(3); // Box::new is disallowed in the config.
}

【正例】


#![allow(unused)]
fn main() {
// Example code which does not raise clippy warning
let mut xs = Vec::new(); // Vec::new is _not_ disallowed in the
}

G.OTH.02 【标准库】 计算秒级、毫秒级、微秒级的时间请使用对应的方法

【级别:建议】

建议按此规范执行。

【Lint 检测】

lint nameClippy 可检测Rustc 可检测Lint Grouplevel
duration_subsecyesnocomplexitywarn

【描述】

【正例】


#![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(); // 得到毫秒
}

【反例】


#![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;  // 用纳秒 计算

}

附录

测试

单元测试

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 benchCriterion.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_tokiocriterion 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,首先我们需要安装perfflamegraph 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中看到我们关心的信息的函数。下面火焰图的注释版本,突出了需要注意的部分。

2

蓝色方块包含了调用CommandResponse::body所花费的时间,它显示几乎所有的时间都花在了clone()上。各个紫色矩形对应的是将BSON(MongoDB使用的二进制格式)解析到Document中所花费的时间,绿色矩形对应的是Documentserde::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只支持从BsonDocument 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
}

要做到这一点,我们需要实现一个新的serdeDeserializer,它可以与原始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

作为一个例子,这里是比较基线和最优化的报告。

在报告的顶部,我们可以看到最优化运行的总结,包括一个说明平均执行时间的图表和一个显示所有样本标准的散点图,以及一些其他图表的链接。下面是最近一次查找基准运行的该部分的屏幕截图。

3

在报告的底部,有一个最近两次运行的比较,较旧的运行(基线)为红色,较新的运行(优化后的)为蓝色。下面是优化后的mongodb版本与未优化的基线比较的部分的截图。在其中,我们可以看到,未优化的基线显然要比优化的慢得多。从分布的广度来看,我们也可以看到,优化版的性能比基线版的更稳定。

4

这些报告是超级有用的工具,可以直观地看到因性能调优而发生的变化,而且对于向他人介绍结果特别有用。它们还可以作为过去性能数据的记录,消除了手动记录结果的需要。

使用wrk进行压测

虽然微基准对隔离行为和识别瓶颈非常有用,但它们并不总是代表真实的工作负载。为了证明所做的改变确实提高了性能,并且没有过度适应微基准,在真实世界的场景中进行测量也是很有用的。

对于像mongodb这样的异步数据库驱动来说,这意味着有大量并发请求的情况。一个生成这种请求的有用工具是wrk工作负载生成器。

要安装wrk,你需要clone repo并从源代码中构建它。


#![allow(unused)]
fn main() {
git clone https://github.com/wg/wrkcd wrkmake./wrk --version
}

如果成功了,你应该看到wrk的版本信息。关于更具体的安装说明,请看 wrkINSTALL 页面。

在启动了一个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应用程序中实现显著的性能改进。这方面的过程可以总结为以下步骤。

  1. 使用 criterion 运行一个基准,以建立一个基线
  2. 通过cargo flamegraph识别瓶颈
  3. 尝试解决瓶颈问题
  4. 重新运行基准测试,看看瓶颈是否得到解决
  5. 重复进行以上步骤

这个过程可以反复进行,直到达到一个令人满意的性能水平。然而,随着你的迭代,改进可能会变得不那么显著,需要更多的努力来实现。例如,在mongodb的例子中,第一个大的改进来自于更明智地使用clone(),但为了达到类似的改进水平,需要实现整个serdeDeserializer 。这就引出了性能剖析如此重要的另一个原因:除了识别需要优化的地方外,它还可以帮助确定何时需要优化(或者反过来说,何时应该停止优化)。如果剩下的改进不值得努力,性能剖析可以表明这一点,让你把精力集中在其他地方。这一点很重要,因为无论某件事情如何优化,总是有改进的余地,而且很容易陷入过度优化的无底洞中。

总结

我希望这个关于 Rust 中性能剖析和基准测试的概述是有帮助的。请注意,将你的 Rust 应用程序或库,优化到技术上尽可能快,并不总是必须的。因为优化的代码往往比简单但缓慢的代码更难理解和维护。

更重要的是,你的应用程序或库要满足其性能预期。例如,如果一个CLI工具的自我更新需要50毫秒或100毫秒,尽管有可能减少50%的运行时间,这并没有什么区别,因为100毫秒完全在这种功能的预期性能水平之内。然而,对于那些性能没有达到预期的情况,这篇文章中所概述的过程可以非常有效地产生优化,正如我们最近对mongodb crate所做的改进中所看到的。

广告时间

我们最近发布了mongodb crate的 v2.0.0版本,其中包含了这篇文章中提到的性能改进,以及大量的新功能,包括对事务的支持。如果你对用Rust编写Web应用程序感兴趣,如果你需要一个数据库,请查看MongoDB Rus t驱动。

模糊测试

模糊测试(Fuzz testing)是一种软件测试技术,用于通过向软件提供伪随机数据作为输入来发现安全性和稳定性问题。

关于模糊测试可以参考:

术语解释

语言元素术语表

术语中文翻译备注
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
baroque macro巴洛克宏
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-fyCargo 化,使用 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动态特质类型
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 functionmain 函数,主函数
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 RustRust 开发版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 幻象电源
参见:HaskellHaskell/Phantom_type
Rust/Phantomstdlib/PhantomData
platform平台
polymorphism多态
powershell(不译)Windows 系统的一种命令行外壳程序
和脚本环境
possibility of absence不存在的可能性
precede预先?,在...发生(或出现)
prelude(不译)预先导入模块,英文本意:序曲,前奏
primitive types原生类型,基本类型,简单类型
print打印
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 clausewhere 子句,where 从句,where 分句在数据库的官方手册中多翻译成“子句”,英语语法中翻译成“从句”
wrap包裹暂译!
wrapped装包
wrapper装包
Y
yield产生(收益、效益等),产出,提供
Z
zero-cost abstractions零开销抽象
zero-width space(ZWSP)零宽空格

参考

Rust 语言术语中英文对照表

编译器相关术语表

术语中文意义
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,以及 cxtcx
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  IRLOirlo有时被用作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  型变  确定通用类型/寿命参数的变化如何影响子类型;例如,如果TU的子类型,那么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做一些很好的优化。

参考

Rust 编译器内部术语中英文对照表

模版

这里记录一些 rustfmt 和 clippy 等相关工具等配置文件模版。

Rustfmt 模板

为了方便 Rust 开发者,这里提供一个 Rustfmt 的模板,以供参考。

以下内容可以放到 rustfmt.toml.rustfmt.toml 文件中。因为部分选项还未稳定,所以要使用 cargo +nightly fmt 执行。

很多选项都是默认的,无需配置。以下配置的都不是默认值。

# 万一你要使用 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
# 会报告注释中的 FIXIME
report_fixme="Unnumbered"
# 元组模式匹配的时候允许使用 `..` 来匹配剩余元素
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 公司有两种解决方法:

  1. 将 lint 放到一个统一文件中,然后复制粘贴到使用的地方。
  2. 通过 .cargo/config.toml 来配置 rustflags ,参考: lints.toml

Embark 也在跟踪和推动在 Cargo 中支持 Lint 配置的功能,相关 issues:

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",
]

介绍

这里介绍一些检测工具,比如 Cargo fmt 和 Cargo Clippy.

参考资料

  1. https://doc.rust-lang.org/rustc/lints/groups.html
  2. https://rust-lang.github.io/rust-clippy/master/index.html
  3. https://rust-lang.github.io/rust-clippy/master/index.html
  4. Dtolnay 对 crates.io 中 clippy lint 应用统计

Rustfmt 配置相关说明

在 Stable Rust 下使用未稳定配置项的方法

  1. CI Job 可以分为 StableNightly。在 Stable CI 下进行编译,在Nightly CI下执行cargo fmtcargo clippy
  2. 在项目本地可以使用 cargo +nightly fmt 代替 cargo fmt

注意: 一定要在文件保存之后再运行 rustfmt`,否则容易出错。

真实项目中的配置案例

  1. 来自 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",
]

  1. 来自 Google Fuchsia 操作系统
# 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"
  1. 来自 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 可选值只有 OneTwo

【对应配置项】

对应选项可选值是否 stable说明
versionOne(默认)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_formattingfalse(默认)No禁止格式化

配置 edition 版次

【描述】

如果通过 Cargo 的格式化工具 cargo fmt 执行,Rustfmt 能够通过读取 Cargo.toml 文件来获取使用的版本。 否则,需要在配置文件中指定版本。

【对应配置项】

对应选项可选值是否 stable说明
edition2015(默认)No配置 edition 版次

【示例】

edition = "2018"

开启未稳定特性

【描述】

默认未启用,但是可以通过配置此功能在 Nightly 上启用此功能。

【对应配置项】

对应选项可选值是否 stable说明
unstable_featuresfalse(默认)No开启未稳定特性

其他

在 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 不允许代码中出现 「内置黑名单」中定义的命名,比如 foobaz

默认为 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仔细,建议二者搭配使用

最佳实践

嵌入式 Rust

数据库

游戏

Cli App

GUI

Web 开发

网络服务

WebAssembly

Cheat Sheet

这里用于梳理 Rust 相关的 Cheat Sheet。

资源

https://cheats.rs/

介绍

优化包括性能优化、编译文件大小优化和编译时间优化。

参考资料

Rust 性能优化:

  1. https://nnethercote.github.io/perf-book/

编译文件大小裁剪:

  1. https://github.com/johnthagen/min-sized-rust

  2. https://docs.rust-embedded.org/book/unsorted/speed-vs-size.html

  3. https://arusahni.net/blog/2020/03/optimizing-rust-binary-size.html

  4. https://mender.io/blog/building-mender-rust-in-yocto-and-minimizing-the-binary-size

  5. https://github.com/RazrFalcon/cargo-bloat

  6. https://github.com/orf/cargo-bloat-action CI 里构建 cargo bloat

  7. https://oknozor.github.io/blog/optimize-rust-binary-size/

编译时间优化:

  1. https://jondot.medium.com/8-steps-for-troubleshooting-your-rust-build-times-2ffc965fd13e

CookBook 介绍

这里是对 Rust 生态系统中常用库或框架的使用介绍。