mcpField
R
rust-best-practices

rust-best-practices

github.com/morganlinton开发1.6k559 次安装2 天前更新
代码仓库

关于 rust-best-practices

这项 Rust 编码技能汇总了五种风格、API、设计与实践指南,涵盖惯用写法、所有权、API 设计,以及交付代码前应运行的检查。

主要功能

  1. 01采用 Rust 惯用写法与所有权模式
  2. 02围绕类型和不变量设计 API
  3. 03使用标准 Trait 与错误处理
  4. 04运行格式化、Lint、测试和文档检查

适用场景

  1. 01检查 Rust 代码是否符合惯用模式
  2. 02设计公开库 API
  3. 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.yaml
prompt.md
evals/log-summary-cli/graders/
01-never-panics-on-bad-input.md
02-errors-name-the-file.md
03-malformed-lines-counted.md
04-paths-as-path-types.md
05-sorted-and-skipped-count.md
06-dependency-ranges.md
07-map-based-counting.md
08-aggregation-unit-tested.md
evals/refactor-inventory-module/
case.yaml
place-files.sh
prompt.md
evals/refactor-inventory-module/files/
inventory.rs
evals/refactor-inventory-module/graders/
01-deny-warnings-removed.md
02-no-rc-refcell-in-api.md
03-borrowed-param-types.md
04-error-implements-error.md
05-no-unwrap-on-fallible-paths.md
06-getter-renamed.md
07-bool-param-replaced.md
08-deterministic-report.md
09-format-over-concat.md
10-no-check-then-unwrap.md
11-changes-listed-with-reasons.md
12-signature-changes-called-out.md
evals/semver-library-api/
case.yaml
evals/semver-library-api/graders/
01-implements-fromstr.md
02-implements-display.md
03-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。它们并非错误,但会将检查从编译时移到运行时,因此应有意识地使用。

不要为了让借用检查器不再报错而克隆。 如果克隆仅仅是为了结束一次借用,那么产生的副本会悄无声息地丢失后续更改。请先按大致顺序尝试以下方法:

  1. 用命名的 let 延长临时值的生命周期,或用代码块缩短借用。
  2. 将较长的表达式拆分为带类型的中间变量,以便看清失败的步骤。
  3. 使用 std::mem::take(需要 Default)、std::mem::replace 或 Option::take 从 &mut self 中移出字段,而不是克隆。
  4. 将大型结构体拆分成较小的结构体,以便分别借用互不重叠的字段。编译器会在一个函数内跟踪互不重叠字段的借用,但不会跨方法调用跟踪。
  5. 有意克隆少量不可变数据;如果意图不明显,请在注释中说明。
  6. 作为最后手段,使用指向拥有数据的 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 边界处保留普通整数)。
  • 独立标志使用 bitflags crate;枚举表示恰好一个选项。
尽早派生并实现标准 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-sys crate 中生成绑定,并将业务逻辑保留在安全的核心 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 规则。

参考资料

需要了解某条规则背后的原因或某项任务的完整检查清单时,请阅读以下资料。 每份资料都是对一个来源的精简摘要。

评论

支持 Markdown