关于 rust-best-practices
这项 Rust 编码技能汇总了五种风格、API、设计与实践指南,涵盖惯用写法、所有权、API 设计,以及交付代码前应运行的检查。
主要功能
- 01采用 Rust 惯用写法与所有权模式
- 02围绕类型和不变量设计 API
- 03使用标准 Trait 与错误处理
- 04运行格式化、Lint、测试和文档检查
适用场景
- 01检查 Rust 代码是否符合惯用模式
- 02设计公开库 API
- 03准备交付 Rust 代码变更
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ morganlinton/ beautifulrust --skill rust-best-practices安装后在 Claude Code 中输入/rust-best-practices确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/log-summary-cli/case.yamlprompt.mdevals/log-summary-cli/graders/01-never-panics-on-bad-input.md02-errors-name-the-file.md03-malformed-lines-counted.md04-paths-as-path-types.md05-sorted-and-skipped-count.md06-dependency-ranges.md07-map-based-counting.md08-aggregation-unit-tested.mdevals/refactor-inventory-module/case.yamlplace-files.shprompt.mdevals/refactor-inventory-module/files/inventory.rsevals/refactor-inventory-module/graders/01-deny-warnings-removed.md02-no-rc-refcell-in-api.md03-borrowed-param-types.md04-error-implements-error.md05-no-unwrap-on-fallible-paths.md06-getter-renamed.md07-bool-param-replaced.md08-deterministic-report.md09-format-over-concat.md10-no-check-then-unwrap.md11-changes-listed-with-reasons.md12-signature-changes-called-out.mdevals/semver-library-api/case.yamlevals/semver-library-api/graders/01-implements-fromstr.md02-implements-display.md03-dedicated-error-type.md详细介绍
Rust 最佳实践
综合了 5 个来源,references/ 中提供了每个来源的完整摘要:Google
Rust 风格指南、官方 Rust API 指南、Effective Rust(David Drysdale)、
Rust Design Patterns(rust-unofficial)以及 Microsoft 的 Pragmatic Rust 指南。
如果各来源存在分歧,本文会说明应遵循哪种做法及其原因。
1. 思维方式
- 编写符合 Rust 风格的代码。 不要照搬 C++、Java、C# 或 Python 的构造。
用
Result表示错误,用所有权和Drop管理资源,用 trait 和枚举实现 多态,用Option表示可空性。带有继承意味的技巧(对“基类”结构体使用Deref、Manager/Factory类型、throw_if_null辅助函数)都值得警惕。 - 让无效状态无法表示。 将不变量融入类型:用枚举代替布尔标记和“若 Y 则必须为 X”之类的注释,用 newtype 表示单位和 ID,用可失败的构造函数一次性 完成验证。这样,编译器就能强制执行原本只能靠注释提出的要求。
- 应用代码不是库代码。 库代码(任何有外部用户的代码)应严格遵循下文的
API 设计规则。应用代码应优先考虑简单直接:使用具体类型、
anyhow风格的 错误、返回Vec,不要追求尚无需求的通用性。 - 让工具代劳。 格式由 rustfmt 负责,惯用写法检查由 clippy 负责,内存安全由 编译器负责。把精力放在设计上。
2. 提交代码前
运行以下命令并修复所有报告的问题:
cargo fmt
cargo clippy --all-targets --all-features -- -D warnings
cargo test
cargo doc --no-deps # libraries: catches broken doc links and missing docs
- 修复 lint,而不是屏蔽它。如果某个 lint 确实不适用于某一项,请在最小范围的
项目上使用
#[expect(clippy::name, reason = "...")]。当 lint 不再触发时,expect会发出警告,因此过时的抑制会被清理。仅在生成的代码或宏输出中使用#[allow]。 - 不要在源代码中使用
#![deny(warnings)]。新工具链会引入新的警告,导致构建 失败。应在 CI 中将警告视为错误(cargo clippy -- -D warnings或RUSTFLAGS="-D warnings")。 - 在
Cargo.toml中统一配置 lint([lints.rust]、[lints.clippy]或[workspace.lints]),不要在各处零散地添加 crate 属性。BeautifulRust 仓库在lints/lints.toml和lints/clippy.toml中提供了一套可直接使用的配置 (https://github.com/morganlinton/BeautifulRust);如果项目尚无配置,可以推荐这套。 一套良好的基础配置包括:unsafe_op_in_unsafe_fn、missing_debug_implementations、clippy::all,以及在项目能够接受的情况下加入clippy::undocumented_unsafe_blocks和clippy::allow_attributes_without_reason。
3. 所有权与借用
优先采用唯一所有权。 仅当设计需要共享所有权时才使用 Rc/Arc,仅当设计需要内部可变性时才使用 RefCell/Mutex。它们并非错误,但会将检查从编译时移到运行时,因此应有意识地使用。
不要为了让借用检查器不再报错而克隆。 如果克隆仅仅是为了结束一次借用,那么产生的副本会悄无声息地丢失后续更改。请先按大致顺序尝试以下方法:
- 用命名的
let延长临时值的生命周期,或用代码块缩短借用。 - 将较长的表达式拆分为带类型的中间变量,以便看清失败的步骤。
- 使用
std::mem::take(需要Default)、std::mem::replace或Option::take从&mut self中移出字段,而不是克隆。 - 将大型结构体拆分成较小的结构体,以便分别借用互不重叠的字段。编译器会在一个函数内跟踪互不重叠字段的借用,但不会跨方法调用跟踪。
- 有意克隆少量不可变数据;如果意图不明显,请在注释中说明。
- 作为最后手段,使用指向拥有数据的
Vec的索引,或使用带Weak反向指针的Rc<RefCell<_>>。避免自引用结构体。
// Move out of a &mut enum without cloning.
fn a_to_b(e: &mut MyEnum) {
if let MyEnum::A { name, x: 0 } = e {
*e = MyEnum::B { name: std::mem::take(name) };
}
}
接受最通用的借用类型。 使用 &str 而不是 &String,&[T] 而不是 &Vec<T>,&T 而不是 &Box<T>,Option<&T> 而不是 &Option<T>。这样,解引用强制转换就能让调用方传入拥有所有权的值、借用值或字面量。对于只读取路径、字符串或字节缓冲区的场景,参数可以使用 impl AsRef<Path> / impl AsRef<str> / impl AsRef<[u8]>(绝不要用于结构体字段;字段应存储 PathBuf 和 String)。只需迭代的库代码应接受 impl IntoIterator<Item = T>;应用代码可以直接接受 &[T]。
只有在需要取得所有权时才取得,并按值取得。 如果函数会存储或消耗参数,就接受 T,而不是接受 &T 后在函数内部调用 .clone()。如果只读取,就接受 &T;如果要原地修改,就接受 &mut T。多个返回值应通过元组或结构体返回,而不是通过 &mut 输出参数返回(像 Read::read 那样复用调用方拥有的缓冲区是例外)。
结构体中优先使用拥有所有权的数据。 持有 &'a T 的结构体会迫使每个容器都带上生命周期参数。拥有一个 String 或 Vec<u8> 通常更简单,而且很少会成为性能瓶颈。类型确实需要携带生命周期时,请使用描述性名称('src、'conn);简单函数中使用 '_ 和 'a 即可。
初始化后冻结数据。 用 let mut 构建数据,然后重新绑定为 let data = data;,这样后续代码就无法意外修改它。
避免在公开签名中使用 Rc、Arc、Box 和 RefCell。 接受 &T、&mut T 或 T,让调用方决定如何管理所有权。必须共享的服务应在内部用 Arc<Inner> 封装状态,并实现开销很低的 Clone。
4. 类型与 API 设计
命名
| 项目 | 格式 | 说明 |
|---|---|---|
| 类型、trait、枚举变体 | UpperCamelCase | 首字母缩写视为一个单词:Uuid、HttpClient |
| 函数、方法、模块、局部变量 | snake_case | |
| 常量、静态变量 | SCREAMING_SNAKE_CASE | |
| 类型参数 | 简短,取自主边界 | T、R: Read、W: Write |
| 生命周期 | 简短小写 | 用于类型时应具有描述性 |
- getter 不加
get_前缀:fn len(&self)、fn name(&self) -> &str、fn name_mut(&mut self)。只有在获取的内容显而易见且只有一种时才使用单独的get(Cell::get)。相关命名:set_foo(T)、take_foo()、用于闭包访问的with_foo(|f| ..),以及供已检查访问器对应使用的不安全_unchecked版本。 - 转换前缀表示开销和所有权:
as_(无开销,借用转借用)、to_(开销较大,借用转拥有值或借用转借用)、into_(消耗self)。在类型名中按顺序放置mut:用as_mut_slice,而非as_slice_mut。 - 构造函数:
new(主要构造函数)、with_capacity/new_with_foo(变体)、try_new(可能失败)、from_foo(转换;重要的转换应配套实现From)。I/O 资源使用领域动词:File::open、TcpStream::connect。 iter/iter_mut/into_iter的返回类型分别命名为Iter/IterMut/IntoIter;keys()返回Keys。- 错误类型按“动词-对象-错误”的顺序命名:用
ParseBoolError,不用BoolParseError。 - 避免使用含糊的后缀
Service、Manager、Factory、Helper、Util。直接命名事物本身 (Bookings、Dispatcher、FooBuilder)。不要在类型名中重复模块名(用foo::Id, 不用foo::FooId)。避免使用外行难以理解的缩写。 - 如果实现了
len(),也要实现is_empty()。 - Feature 名称直接使用:
std、serde、alloc,不要用use-std或no-std。
用类型建模领域
- 使用 Newtype 表示语义上的区别:
struct Miles(f64)和struct Kilometers(f64)不能混用,而类型别名可以。布局很重要时添加#[repr(transparent)],并派生或转发所需的 trait (derive_more可减少样板代码)。编码不变量的 Newtype(Month)应在可能失败的构造函数 (TryFrom、FromStr、try_new)中进行验证,且绝不提供从原始类型转换而来的无失败From实现。 - 公共或共享签名中不使用
bool参数。 调用处的report(true)难以理解,也无法扩展出第三种模式。应使用包含两个变体的枚举 (Verbosity::Summary、Verbosity::Full)或两个方法(report()、report_verbose()):用Widget::new(Size::Small, Shape::Round),不要用Widget::new(true, false)。唯一例外是调用处仅相隔几行的私有辅助函数。重构时,现有的bool参数是 应移除的问题之一,不是应保留的细节。 - 优先使用具名字段,而非元组,单字段 Newtype 和非常局部的代码除外。
- 用
Option表示缺失,绝不使用哨兵值,例如-1或""。用携带数据的枚举, 而不是“仅当 Y 已设置时,字段 X 才有意义”。 - **使用最合适的 std 类型:**路径使用
PathBuf/&Path,时间跨度使用Duration,NonZeroU32仅在内部使用(API 边界处保留普通整数)。 - 独立标志使用
bitflagscrate;枚举表示恰好一个选项。
尽早派生并实现标准 trait
下游 crate 无法为你的类型添加 std trait(孤儿规则),所以你必须自己添加。只要每个字段都支持,就应派生:Debug(所有公共类型都要实现;如需隐藏机密信息,则手动实现)、Clone(唯一资源和密钥材料除外)、Copy(仅用于可按位复制的小型类型,因为编译器会静默复制)、PartialEq / Eq、PartialOrd / Ord(存在全序时,绝不要在未实现 Ord 的情况下提供 PartialOrd)、Hash(与 Eq 保持一致)、Default。为所有需要供人阅读的内容实现 Display,例如错误类型和类字符串包装类型。为纯函数,以及丢弃后会导致错误的类型(例如错误和惰性值)添加 #[must_use]。
实现 From / TryFrom,不要直接实现 Into / TryInto(通用实现会为你提供这些 trait)。优先使用 from / into,而非 as,因为后者会悄悄允许有损转换。实现 FromStr,而不是另设 from_str 方法;为集合实现 FromIterator 和 Extend;为拥有所有权和借用两种形式都实现 IntoIterator。仅在智能指针和拥有所有权的集合(Vec<T> 到 [T])上实现 Deref,绝不要用它来伪造继承。只有在运算符符合其数学含义时才重载。
不要在结构体定义上添加可派生 trait 的约束:写成 #[derive(Clone)] struct Good<T>,而不是 struct Bad<T: Clone + Debug>。约束应放在需要它们的 impl 块上。
如果手动实现 Ord,也要手动实现 PartialEq、PartialOrd 和 Hash,使它们与 Ord 保持一致(比较相同的字段,忽略相同的字段)。在自定义 Ord 旁边使用派生的 PartialEq 会违反 std 契约,并导致 BTreeMap 和 sort / dedup 行为异常。应在文档中说明相等性判断会忽略哪些字段。
构造函数、构建器和默认值
- 即使已有
Default,也要提供new(),并确保两者行为一致(其中一个委托给另一个)。派生或实现Default,以便调用方可以写..Default::default();带有公共字段、用于收集选项的结构体应始终实现它。 - 最多两个可选参数时,使用固有构造函数(
new、with_a、with_b)。超过两个参数,或构造过程需要验证或产生副作用时,添加构建器:Foo::builder()返回FooBuilder,setter 以字段命名(timeout(d),而非set_timeout),且不应失败;build() -> Result<Foo, Error>负责完成所有验证,包括跨字段检查。优先使用不消耗所有权的 setter(&mut self -> &mut Self),除非build必须移出某个值。 - 对于包含四个或更多零散参数的构造函数,应将参数归并为语义明确的类型:使用
Deposit::new(account, amount),而不是四个&str/u64参数。
可见性和演进
-
默认设为私有。除非结构体是被动的 C 风格数据,否则字段应保持私有。应先扩大为
pub(crate)或pub(super),再扩大为pub。之后提高可见性不会造成破坏性变更;反过来则会,且每个pub项目都是一项承诺。 -
对于跨 crate 边界的公共枚举和带公共字段的结构体,除非其取值集合是稳定的本体,否则应标记为
#[non_exhaustive]。匹配此类类型时,列出你处理的变体,并添加通配分支。 -
对不希望下游实现的 trait 进行封闭,并在文档中说明:
pub trait TheTrait: private::Sealed { /* ... */ } mod private { pub trait Sealed {} impl Sealed for usize {} } -
使用
impl Iterator<Item = T>(内部 API)或 newtype(必须保持稳定的公共 API)隐藏实现所用的迭代器类型。 -
每个公共项目只通过一个路径暴露。除非你构建的是聚合 crate,否则不要
pub use外部 crate 的项目。避免在公共 API 中泄露第三方类型;将serde实现放在名为serde的 feature 之后,按需启用。如果必须公开依赖项的类型,应重新导出该类型,以便调用方能够引用匹配的版本。 -
仅当某个函数将始终保持为
const时,才将其标记为const。不要公开ErrorKind枚举或其他会锁定你希望保持私有的失败模式的内部细节。
Trait 与泛型
- 仅在用于泛型编程(
T: Trait、impl Trait、dyn Trait)或作为扩展 Trait(FooExt)时定义 Trait。不要只为组织代码而使用 Trait。将核心功能放在固有方法中,并让 Trait 实现转发到这些方法,这样调用方无需导入任何内容。 - 按以下顺序优先选择:具体类型;泛型(
T: Trait、impl Trait);dyn Trait,最好将其封装在自定义结构体中(struct DynStore(Arc<dyn Store>))。泛型支持静态分发和内联;Trait 对象则支持异构集合并减小二进制文件。不要在服务级类型中嵌套泛型(Service<Backend>可以;Service<Backend<Store>>不行)。 - 尽早确定 Trait 是否必须满足对象安全。泛型方法或接收者以外的
Self会破坏对象安全;使用where Self: Sized排除这类方法。 - 尽量减少 Trait 必需方法,并为所有可从这些方法推导出的功能提供默认实现,就像
Iterator根据next提供默认实现一样。 - 如果函数有明确的接收者,就将其设为方法。没有接收者的计算应使用自由函数,而不是
Database::check(params)。关联函数用于构造。 - 闭包:使用满足需求的最通用约束(调用一次则用
FnOnce,会修改状态则用FnMut,否则用Fn),并优先使用Fn*约束,而非裸fn指针。闭包参数放在最后;不要接收多个闭包参数。 - 在整个 crate 中保持参数顺序一致:调用特定参数在前,通用上下文(日志记录器、分配器)在后。
- 只有在函数、泛型和 Trait 都无法胜任时才使用宏。优先使用
macro_rules!而非过程宏;让宏输入的形式与其生成的输出相似;绝不要让宏改变类型种类或函数签名。
重构他人的代码
- 修复请求涉及的所有代码异味,而不只是容易处理的部分:仅为满足借用检查器而进行的克隆、
&String/&Vec<T>参数、可失败路径上的unwrap、签名中的Rc<RefCell<_>>、get_getter、bool参数、使用+拼接字符串、#![deny(warnings)]、未实现Error的错误类型,以及输出中非确定性的HashMap遍历。 - 除非修改行为本身就是目的,否则保持公共行为完全一致:操作相同,成功和失败语义相同,输出字节相同。将 panic 转为
Err是调用方想要的修改。删除错误变体、移除 derive,或更改构造函数的参数则不是;如果必须更改签名,请在变更列表中列出修改前后的签名并说明原因。 - 保留你不理解的特殊行为,并将其标记出来,而不是悄悄“修复”。
- 如果模块没有测试,请在修改前添加几个测试,固定当前行为。
5. 错误和 panic
根据约定选择机制:
| 情况 | 使用 |
|---|---|
缺失属于正常情况(first()、checked_add) | Option<T> |
| 调用方可能需要了解出错原因 | Result<T, E> |
| 代码内部存在缺陷或违反不变量 | panic! / assert! / expect("why") |
panic 不是异常。 panic 表示“立即停止”。绝不要用它报告无效输入;绝不要依赖 catch_unwind 控制流程(在 panic = "abort" 时它不起作用,而且可能导致状态不一致);也绝不要捕获 panic 后在库内部继续执行。在有理由提供可能 panic 的便捷接口时,应同时提供一个可返回错误的 API(get / get_unchecked、new / try_new),并在 # Panics 下记录说明。
每个有意触发的 panic 都要附带包含相关值的消息:
assert!(buf.len() >= HEADER, "buffer too small: {} bytes", buf.len())、
expect("config is loaded before workers start")。优先使用 expect("reason"),而不是
unwrap()。不要在消息中复述条件。
**使用模式匹配,而不是先检查再 unwrap:**使用 if let Some(v) = opt { .. } 或
let Some(v) = opt else { return .. };,绝不要写 if opt.is_some() { opt.unwrap() }。
锁中毒属于致命错误:.lock().expect("lock poisoned")。不要使用
into_inner 恢复。
库的错误类型(公共 Result 中的任何 E):
- 实现
std::error::Error(这要求实现Debug+Display),满足Send + Sync + 'static,并为包装的错误原因实现source()。绝不要将()、String或Box<dyn Error>用作库的公共错误类型。 Display消息应使用小写、简洁,且不带结尾标点:"invalid header length"。- 实现
From<Upstream>,以便?能进行转换。仅对外部错误或需要补充上下文时使用map_err。 - 两种合适的结构:为每种错误原因设置一个变体的枚举(
thiserror很适合生成这种枚举),或为每种情况定义一个结构体,其中包含私有的kind字段以及is_xxx()和访问器方法。无论采用哪种结构,都不要创建一个罗列 crate 中所有失败模式的全局枚举,也不要暴露调用方无法据此采取行动的细节。 - 如果可返回错误的函数会消耗参数,应在错误中返回该参数,这样调用方无需克隆即可重试(
FromUtf8Error::into_bytes)。 - 绝不要在库的公共 API 中暴露
anyhow::Error。
**应用程序的错误处理:**使用 anyhow 或 eyre 搭配 .context("opening config") 是合适的做法。使用一种应用级错误类型,而不是多种。main 可以返回
Result<(), anyhow::Error>。
**析构函数绝不能失败、panic 或阻塞。**对于可能失败的清理,提供
close(self) -> Result<()>,并让 Drop 执行尽力而为的清理。
**记录失败模式:**任何 Result 都要有 # Errors,任何可触发的 panic 都要有
# Panics,每个 unsafe fn 都要有 # Safety。
6. 日常惯用法
-
当处理流程较短且从上到下易于阅读时,优先使用迭代器,而非手写循环:
values.iter().filter(..).map(..).collect()、.any()、.sum()、.fold()。将可能失败的步骤收集为Result<Vec<_>, E>,并使用?。如果循环体较长、要处理多个任务或需要不太方便的提前返回, 则保留显式的for循环。将较长的链式调用绑定到命名变量,而不要放进for头部;优先使用for x in it, 而非.for_each(|x| ..)。 -
当
Option/Result组合器(map、and_then、ok_or_else、unwrap_or_else、as_ref、as_deref)比match更清晰时,使用组合器;否则使用match。 -
使用
format!/write!和内联参数(format!("{name}: {count}"))进行 拼接和替换,不要使用链式+或str::replace。在性能关键路径中,使用write!写入 已有缓冲区。 -
**在类型推断会模糊意图时显式指定类型:**用于
.into()、.parse()、.collect()和复杂的迭代器结果。优先使用let x: Vec<u32> = ..,而非 turbofish 语法。 -
使用 Drop 守卫执行清理,确保提前返回或展开时也会运行。将其绑定到名称(
let _guard = ..), 因为let _ = ..会立即丢弃。 -
**闭包捕获了不同所有权的值时:**在代码块中重新绑定,然后使用
move:let job = { let cfg = cfg.clone(); move || run(cfg, data) }; -
**导入风格:**每个模块路径使用一条
use语句(use foo::bar::{Bar, Baz};);导入类型和 trait,并以非限定名称使用(Regex);通过模块限定名称调用自由函数 (mem::take、fs::read)。除非整个文件都会使用某个 trait,否则在需要它的函数内导入该 trait。 不使用 glob 导入,测试模块中的use super::*和函数内用于匹配枚举的use Enum::*除外。 -
将魔法数字改为命名的
const,并添加文档注释,说明该值的来源以及修改它会影响什么。 -
**日志记录:**通过
tracing或log记录结构化事件,不要使用println!/dbg!, 但 CLI 自身的标准输出除外。
7. 并发与异步
-
公共类型及其返回的 future 应实现
Send(通常也应实现Sync)。使用编译时检查固定这一要求, 避免重构在不知不觉中导致其退化:const _: () = { const fn assert_send<T: Send>() {} assert_send::<MyClient>(); };在
.await期间持有Rc或RefCell借用会使 future 不再实现Send。 -
优先使用消息传递(通道),而非共享状态。确实需要共享时:将具有相关不变量的数据放在同一把锁下, 将锁的作用域控制在几行内,持锁时绝不调用用户闭包,也绝不从公共方法返回
MutexGuard。 -
优先编写
async fn,而非fn -> impl Future;trait 中,或热路径必须在异步代码块之外执行初始化时除外。 -
异步函数中的长时间 CPU 密集型工作必须让出执行权(每隔几十微秒调用一次
tokio::task::yield_now().await),或移至spawn_blocking。 -
库代码不创建原始线程,也不假定存在运行时;应从调用方接收执行器、spawn 函数,或
impl AsyncRead/impl Read。
8. Unsafe 和 FFI
- 仅在以下三种情况下使用
unsafe:安全代码无法表达的新型抽象、经基准测试确认的性能需求,或 FFI 和平台调用。优先使用标准库和经过充分审查的 crate;它们已经包含经过审计的unsafe。 - 每个
unsafe代码块或 impl 都要添加// SAFETY:注释,说明不变量为何成立。每个unsafe fn都要添加# Safety文档章节,列出调用方必须满足的条件。启用unsafe_op_in_unsafe_fn。 - 将
unsafe保留在能够维护其不变量的最小模块中,并将其封装为安全 API;模块边界就是健全性边界。任何一系列安全调用都可能导致未定义行为的安全函数都是不健全的;不健全的代码绝不可接受。 - 仅当误用可能导致未定义行为时,才将函数标记为
unsafe,不要仅因其有危险就这样做(delete_database()是安全函数)。 - 不要用
transmute转换枚举或规避生命周期检查,也不要用unsafe impl Send来绕过编译器。 - 使用 Miri 和对抗性输入测试不安全代码。
- FFI:共享结构体使用
#[repr(C)],使用定宽整数类型,字符串使用CString/CStr,在同一侧分配和释放;将 C 所有的指针封装为由Drop释放的 RAII 类型;检查空指针并返回Result;在边界处使用catch_unwind阻止 panic 越界。使用bindgen在foo-syscrate 中生成绑定,并将业务逻辑保留在安全的核心 crate 中。
9. Crate、模块和依赖项
- 优先在扁平的
crates/工作区中使用小型、单一用途的 crate;切勿将一个 crate 嵌套在另一个 crate 的src/中。当 crate 出现第二种用途或子目录时,就应拆分。通过[workspace.dependencies]和[workspace.lints]共享版本和 lint 配置。 - **模块:**叶子模块以其主要条目命名(
Foo放在foo.rs中)。非叶子模块只包含mod声明和重新导出。将核心条目放在 crate 根目录,其余内容按用途(account、network)而非类型(traits、errors)分组。不要定义 prelude。 - 逐项重新导出(
pub use foo::{A, B};),切勿使用 glob。 - **依赖项:**每个依赖项都会增加编译时间、二进制大小和供应链风险,包括
build.rs和过程宏。使用范围("1"、"1.4"),切勿使用"*"或不必要的"="。为二进制提交Cargo.lock。使用cargo tree --duplicates、cargo deny、cargo udeps和cargo audit进行审计。 - Feature 应当是增量式且使用裸名称:
std、serde、alloc、test-util。切勿使用no-std、use-serde,也不要使用会移除或更改公共条目的 feature。不要对结构体字段或 trait 方法设置 feature 门控。将 mock 和 fake 放在test-util后面。 - **Semver:**破坏性变更包括移除或重命名公共条目、向未标记为
#[non_exhaustive]的枚举添加变体、向可公开构造的结构体添加字段、添加 trait 约束,或使 trait 不再支持对象安全。在移除条目前,先在一个次要版本中将其弃用,再在主要版本中移除。新 crate 使用最新的稳定版 edition,并审慎设定 MSRV。 - 不要在
main()之前运行代码(不要使用ctor风格的 crate)。不要在稳定版工具链上使用不稳定功能。
10. 文档和注释
- 每个公开项都要有
///;每个公开模块和 crate 根目录顶部都要有//!。 首句应为约十五个词的单行句子,因为它会成为列表中的摘要。 - 按以下顺序编写适用的章节:
# Examples、# Errors、# Panics、# Safety。示例使用?,并通过#行隐藏fn main,绝不使用unwrap(), 因为用户会原样复制这些示例。用文字描述参数,不要使用表格。 - 说明签名无法表达的内容:前置条件、副作用、所有权转移、复杂度及其原因。不要
复述类型或名称;在
remove_element上写/// Removes an element.就是无用内容。 - 使用
[`Type`]链接项。用#[doc(hidden)]隐藏用户不会触发的 impl (From<PrivateError> for PublicError)。 //注释解释原因,而非内容:// SAFETY:、// TODO(issue-123):、不明显的 魔法值、带链接的变通方法。不要在代码文档中写设计日志、“为何选择 X”的长文或 规则合规性表格。记录最终状态。
11. 测试
- 私有内部实现的单元测试放在
#[cfg(test)] mod tests { use super::*; .. }中。 任何可通过公开 API 访问的内容都应编写集成测试,放在tests/中;两种位置都 可行时,优先放在此处。文档示例也属于测试。 - 测试可观察行为和性质(单调性、往返转换、幂等性),而非复述常量或镜像分支。
- 修复 bug 前先编写失败测试。覆盖每种功能组合和每条平台专属路径。
- 任何执行 I/O、涉及时间、随机性或网络的内容都必须可模拟:
在
test-util后配置new_mocked() -> (Self, MockCtrl),不要使用会执行 I/O 的Default。 - 使用
cargo fuzz对不可信输入的解析器进行模糊测试。使用criterion或divan和std::hint::black_box进行基准测试。对unsafe运行 Miri。 - 断言应提供上下文,让失败信息易于理解:
assert_eq!(got, want, "input {input:?}")。
12. 性能,按优先顺序
首先针对易用性进行设计,测量后再优化。优化时:使用 Vec::with_capacity
或 collect(),而不是循环调用 push;在热点 API 中复用调用方所有的缓冲区
(fn read_into(&mut self, buf: &mut Vec<u8>));对长期存活的不可变序列使用
Box<[T]、Box<str>、Arc<str>;对未预留容量而构建的长期存活集合调用
shrink_to_fit;仅对可信键使用更快的哈希器(foldhash、FxHash);批量处理,
而不是逐项加锁;应用可设置 #[global_allocator],例如 mimalloc。为小型普通
类型派生 Copy,大型类型绝不要这样做。
13. 最终检查清单
返回 Rust 代码前,请确认:
cargo fmt、cargo clippy -- -D warnings和cargo test均无问题。- 测试和
main之外没有unwrap();每个expect、assert和panic都有包含 相关值的消息。 - 公开函数接受
&str、&[T]、impl AsRef<_>或impl IntoIterator,而非&String或&Vec<T>。 - 每个公开类型都派生
Debug和适用的常见 trait;错误类型实现Error + Display + Send + Sync + 'static。 - 公开签名中没有
bool参数;没有未配套实现PartialEq和Hash的手写Ord。 - 没有仅为满足借用检查器而调用的
.clone()。 - 没有 glob 导入、prelude、glob 再导出或泄漏的第三方类型。
- 每个
unsafe块都有// SAFETY:注释;每个unsafe fn都有# Safety文档。 - 公开项都有文档,包含单行摘要,并在适用时包含
# Errors/# Panics。 - 应用代码保持简单;库代码遵循 API 规则。
参考资料
需要了解某条规则背后的原因或某项任务的完整检查清单时,请阅读以下资料。 每份资料都是对一个来源的精简摘要。
references/rust-api-guidelines.md(https://rust-lang.github.io/api-guidelines/): 设计公共 API 或 crate;包含全部 54 项检查清单及相关约定。references/effective-rust.md(https://www.lurklurk.org/effective-rust/):在不同方案间做选择:泛型与dyn、Result与 panic、迭代器与循环、 借用检查器技巧、依赖项、CI。references/rust-design-patterns.md(https://rust-unofficial.github.io/patterns/): 惯用法、模式和反模式,并附有代码。references/microsoft-rust-guidelines.md(https://microsoft.github.io/rust-guidelines/): 错误结构体设计、panic 策略、unsafe 策略、异步、工作区布局、性能。references/google-rust-style-guide.md(https://google.github.io/styleguide/rustguide.html): 格式、导入、模块布局、访问器命名、应用与库。