rust-best-practices

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Rust best practices

Rust 最佳实践

Distilled from five sources, with full per-source digests in
references/
: the Google Rust Style Guide, the official Rust API Guidelines, Effective Rust (David Drysdale), Rust Design Patterns (rust-unofficial), and Microsoft's Pragmatic Rust Guidelines. Where the sources disagree, this file says which to follow and why.
提炼自五个来源,完整的逐来源摘要见
references/
:Google Rust Style Guide、官方 Rust API Guidelines、Effective Rust(David Drysdale)、Rust Design Patterns(rust-unofficial),以及 Microsoft's Pragmatic Rust Guidelines。若各来源存在分歧,本文件会说明应遵循哪一方及其原因。

1. Mindset

1. 思维模式

  • Write Rust-shaped code. Don't port C++, Java, C#, or Python constructs one-to-one. Model errors with
    Result
    , resources with ownership and
    Drop
    , polymorphism with traits and enums, and nullability with
    Option
    . Inheritance-flavored tricks (
    Deref
    to a "base" struct,
    Manager
    /
    Factory
    types,
    throw_if_null
    helpers) are smells.
  • Make invalid states unrepresentable. Push invariants into types: enums instead of bool flags and "must be X if Y" comments, newtypes for units and IDs, fallible constructors that validate once. The compiler then enforces what a comment could only request.
  • Application code is not library code. Library code (anything with external users) follows the API-design rules below strictly. Application code should favor simplicity and directness: concrete types,
    anyhow
    -style errors,
    Vec
    return values, no speculative generality.
  • Let the tools do the work. rustfmt owns formatting, clippy owns idiom checks, the compiler owns memory safety. Spend your attention on design.
  • 编写符合 Rust 范式的代码。 不要将 C++、Java、C# 或 Python 的构造方式一对一地移植过来。使用
    Result
    对错误建模,使用所有权和
    Drop
    对资源建模,使用 trait 和 enum 对多态建模,并使用
    Option
    对可空性建模。带有继承思维色彩的技巧(通过
    Deref
    指向一个“基类”struct、使用
    Manager
    /
    Factory
    类型、
    throw_if_null
    辅助函数)都是坏味道。
  • 使无效状态无法表示。 将不变量编码进类型:用 enum 代替 bool 标志和“如果是 Y 就必须是 X”之类的注释,用 newtype 表示单位和 ID,并使用可失败的构造函数一次性完成验证。让编译器强制执行原本只能依靠注释要求的约束。
  • 应用代码不是库代码。 库代码(即任何存在外部用户的代码)应严格遵循下文的 API 设计规则。应用代码应优先考虑简单性和直接性:使用具体类型、
    anyhow
    风格错误、
    Vec
    返回值,不做推测性的泛化。
  • 让工具完成工作。 rustfmt 负责格式化,clippy 负责惯用法检查,编译器负责内存安全。将注意力放在设计上。

2. Before you hand back code

2. 交还代码之前

Run these and fix everything they report:
bash
cargo fmt
cargo clippy --all-targets --all-features -- -D warnings
cargo test
cargo doc --no-deps   # libraries: catches broken doc links and missing docs
  • Fix a lint rather than silencing it. When a lint is genuinely wrong for one item, use
    #[expect(clippy::name, reason = "...")]
    on the narrowest item.
    expect
    warns when the lint stops firing, so stale suppressions get cleaned up. Use
    #[allow]
    only in generated code or macro output.
  • Never put
    #![deny(warnings)]
    in source. New toolchains add new warnings and will break the build. Enforce warnings-as-errors in CI instead (
    cargo clippy -- -D warnings
    or
    RUSTFLAGS="-D warnings"
    ).
  • Configure lints once in
    Cargo.toml
    (
    [lints.rust]
    ,
    [lints.clippy]
    , or
    [workspace.lints]
    ), not with scattered crate attributes. A good baseline:
    unsafe_op_in_unsafe_fn
    ,
    missing_debug_implementations
    ,
    clippy::all
    , and where the project tolerates them
    clippy::undocumented_unsafe_blocks
    and
    clippy::allow_attributes_without_reason
    .
运行以下命令,并修复它们报告的所有问题:
bash
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。当某条 lint 对特定条目确实不适用时,在范围最小的条目上使用
    #[expect(clippy::name, reason = "...")]
    。
    expect
    会在 lint 不再触发时发出警告,因此陈旧的抑制项能够被清理。仅在生成代码或宏输出中使用
    #[allow]
    。
  • 绝不要在源码中放置
    #![deny(warnings)]
    。新工具链会引入新的警告,从而导致构建失败。应在 CI 中启用将警告视为错误的策略(
    cargo clippy -- -D warnings
    或
    RUSTFLAGS="-D warnings"
    )。
  • 在
    Cargo.toml
    中统一配置一次 lint(
    [lints.rust]
    、
    [lints.clippy]
    或
    [workspace.lints]
    ),而不是在各个 crate 中分散添加属性。推荐的基础配置包括:
    unsafe_op_in_unsafe_fn
    、
    missing_debug_implementations
    、
    clippy::all
    ;如果项目能够容忍,也可以加入
    clippy::undocumented_unsafe_blocks
    和
    clippy::allow_attributes_without_reason
    。

3. Ownership and borrowing

3. 所有权与借用

Prefer unique ownership. Reach for
Rc
/
Arc
only when shared ownership is part of the design, and for
RefCell
/
Mutex
only when interior mutability is. They are not failures, but they move checking from compile time to run time, so use them on purpose.
Don't clone to make the borrow checker stop complaining. A clone that exists only to end a borrow produces a silent copy whose later changes are lost. Try these first, in roughly this order:
  1. Extend a temporary with a named
    let
    , or shorten a borrow with a block.
  2. Split a long expression into typed intermediates so the failing step is visible.
  3. Move a field out of
    &mut self
    with
    std::mem::take
    (needs
    Default
    ),
    std::mem::replace
    , or
    Option::take
    instead of cloning it.
  4. Split a large struct into smaller ones so disjoint fields can be borrowed separately. The compiler tracks disjoint field borrows inside one function, but not across method calls.
  5. Clone small, immutable data deliberately, and say so in a comment if it's not obvious.
  6. Use indices into an owning
    Vec
    , or
    Rc<RefCell<_>>
    with
    Weak
    back-pointers, as a last resort. Avoid self-referential structs.
rust
// 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) };
    }
}
Accept the most general borrowed type.
&str
not
&String
,
&[T]
not
&Vec<T>
,
&T
not
&Box<T>
,
Option<&T>
not
&Option<T>
. Deref coercion then lets callers pass owned values, borrows, or literals. Where you only read a path, string, or byte buffer,
impl AsRef<Path>
/
impl AsRef<str>
/
impl AsRef<[u8]>
is fine on parameters (never on struct fields; store
PathBuf
and
String
). Library code that only iterates should take
impl IntoIterator<Item = T>
; application code may simply take
&[T]
.
Take ownership only when you need it, and then take it by value. If a function stores or consumes its argument, take
T
, not
&T
plus an internal
.clone()
. If it only reads, take
&T
; if it mutates in place,
&mut T
. Return multiple values as a tuple or struct, not through
&mut
out-parameters (reusing a caller-owned buffer, as
Read::read
does, is the exception).
Prefer owned data in structs. A struct holding
&'a T
forces a lifetime onto every container. Owning a
String
or
Vec<u8>
is usually simpler and rarely the bottleneck. When a type does carry a lifetime, give it a descriptive name (
'src
,
'conn
);
'_
and
'a
are fine on simple functions.
Freeze after setup. Build with
let mut
, then rebind
let data = data;
so later code cannot mutate it by accident.
Keep
Rc
,
Arc
,
Box
, and
RefCell
out of public signatures.
Take
&T
,
&mut T
, or
T
and let callers decide how to own things. A service that must be shared implements a cheap
Clone
by wrapping its state in
Arc<Inner>
internally.
优先采用唯一所有权。 只有当共享所有权本身就是设计的一部分时,才使用
Rc
/
Arc
;只有当需要内部可变性时,才使用
RefCell
/
Mutex
。它们并非错误,但会将检查从编译期转移到运行期,因此必须有意识地使用。
不要为了让 borrow checker 停止报错而调用
clone
。
仅为结束借用而存在的 clone 会产生静默副本,导致后续修改丢失。尝试采用以下方法,顺序大致如下:
  1. 将临时值绑定到具名
    let
    ,以延长其生命周期,或使用代码块缩短借用范围。
  2. 将长表达式拆分为具有明确类型的中间值,使失败步骤清晰可见。
  3. 使用
    std::mem::take
    (要求类型实现
    Default
    )、
    std::mem::replace
    或
    Option::take
    从
    &mut self
    中移出字段,而不是克隆字段。
  4. 将大型 struct 拆分为更小的 struct,使互不相交的字段可以分别借用。编译器会在单个函数内跟踪不相交字段的借用,但不会跨方法调用进行跟踪。
  5. 有意识地克隆体积较小且不可变的数据;如果并非显而易见,应在注释中说明。
  6. 最后才考虑使用指向拥有型
    Vec
    的索引,或使用带
    Weak
    反向指针的
    Rc<RefCell<_>>
    。避免使用自引用 struct。
rust
// 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>
。随后可通过 Deref coercion 让调用者传入拥有型值、借用或字面量。如果只是读取路径、字符串或字节缓冲区,参数可以使用
impl AsRef<Path>
、
impl AsRef<str>
或
impl AsRef<[u8]>
,但绝不应将其用于 struct 字段;字段应存储
PathBuf
和
String
。只负责迭代的库代码应接收
impl IntoIterator<Item = T>
;应用代码则可以直接接收
&[T]
。
仅在需要时取得所有权,并按值取得。 如果函数会存储或消费参数,应接收
T
,而不是接收
&T
后在内部调用
.clone()
。如果函数只读取参数,应接收
&T
;如果函数会原地修改参数,应接收
&mut T
。多个返回值应使用 tuple 或 struct 表达,而不是通过
&mut
输出参数返回(复用调用者拥有的缓冲区是例外,例如
Read::read
)。
struct 中优先保存拥有型数据。 持有
&'a T
的 struct 会迫使每个容器都携带生命周期。拥有
String
或
Vec<u8>
通常更简单,而且很少会成为性能瓶颈。当类型确实需要携带生命周期时,应为其使用描述性名称(
'src
、
'conn
);简单函数中使用
'_
和
'a
即可。
完成初始化后将其冻结。 使用
let mut
构建数据,然后重新绑定为
let data = data;
,使后续代码无法意外修改它。
不要让
Rc
、
Arc
、
Box
和
RefCell
出现在公共签名中。
应接收
&T
、
&mut T
或
T
,由调用者决定如何拥有数据。如果服务必须共享,可通过在内部包装
Arc<Inner>
来实现低成本的
Clone
。

4. Types and API design

4. 类型与 API 设计

Naming

命名

ItemCaseNotes
Types, traits, enum variants
UpperCamelCase
Acronyms are one word:
Uuid
,
HttpClient
Functions, methods, modules, locals
snake_case
Constants, statics
SCREAMING_SNAKE_CASE
Type parametersshort, from the main bound
T
,
R: Read
,
W: Write
Lifetimesshort lowercasedescriptive when on a type
  • No
    get_
    prefix on getters:
    fn len(&self)
    ,
    fn name(&self) -> &str
    ,
    fn name_mut(&mut self)
    . Use bare
    get
    only when there is one obvious thing to get (
    Cell::get
    ). Related:
    set_foo(T)
    ,
    take_foo()
    ,
    with_foo(|f| ..)
    for closure access, and unsafe
    _unchecked
    siblings for checked accessors.
  • Conversion prefixes signal cost and ownership:
    as_
    (free, borrow to borrow),
    to_
    (expensive, borrow to owned or borrow to borrow),
    into_
    (consumes
    self
    ). Put
    mut
    in type order:
    as_mut_slice
    , not
    as_slice_mut
    .
  • Constructors:
    new
    (primary),
    with_capacity
    /
    new_with_foo
    (variants),
    try_new
    (fallible),
    from_foo
    (conversion; pair important ones with a
    From
    impl). I/O resources use domain verbs:
    File::open
    ,
    TcpStream::connect
    .
  • iter
    /
    iter_mut
    /
    into_iter
    return types named
    Iter
    /
    IterMut
    /
    IntoIter
    ;
    keys()
    returns
    Keys
    .
  • Error types read verb-object-error:
    ParseBoolError
    , not
    BoolParseError
    .
  • Avoid weasel suffixes
    Service
    ,
    Manager
    ,
    Factory
    ,
    Helper
    ,
    Util
    . Name the thing it is (
    Bookings
    ,
    Dispatcher
    ,
    FooBuilder
    ). Don't repeat the module name in the type (
    foo::Id
    , not
    foo::FooId
    ). Avoid abbreviations an outsider wouldn't know.
  • If you implement
    len()
    , implement
    is_empty()
    .
  • Feature names are bare:
    std
    ,
    serde
    ,
    alloc
    , never
    use-std
    or
    no-std
    .
项目大小写说明
类型、trait、enum 变体
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()
    ,用于 closure 访问的
    with_foo(|f| ..)
    ,以及 checked accessor 对应的 unsafe
    _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
    。

Model the domain with types

使用类型建模领域概念

  • Newtypes for semantic distinctions:
    struct Miles(f64)
    and
    struct Kilometers(f64)
    cannot be mixed, where a type alias can. Add
    #[repr(transparent)]
    when layout matters, and derive or forward the traits you need (
    derive_more
    cuts the boilerplate). A newtype that encodes an invariant (
    Month
    ) validates in a fallible constructor (
    TryFrom
    ,
    FromStr
    ,
    try_new
    ) and never offers an infallible
    From
    from the raw type.
  • No
    bool
    parameters in public or shared signatures.
    report(true)
    is unreadable at the call site and cannot grow a third mode. Use a two-variant enum (
    Verbosity::Summary
    ,
    Verbosity::Full
    ) or two methods (
    report()
    ,
    report_verbose()
    ):
    Widget::new(Size::Small, Shape::Round)
    , not
    Widget::new(true, false)
    . The only exception is a private helper whose call sites sit a few lines away. When refactoring, an existing
    bool
    parameter is one of the smells to remove, not a detail to preserve.
  • Named fields over tuples, except single-field newtypes and very local code.
  • Option
    for absence, never sentinels
    such as
    -1
    or
    ""
    . Data-carrying enums instead of "field X is only meaningful when Y is set".
  • Use the strongest std type:
    PathBuf
    /
    &Path
    for paths,
    Duration
    for spans,
    NonZeroU32
    internally only (keep plain integers at API boundaries).
  • Independent flags use the
    bitflags
    crate; an enum models exactly one choice.
  • 使用 newtype 表达语义差异:
    struct Miles(f64)
    和
    struct Kilometers(f64)
    不能混用,而类型别名可以。内存布局很重要时,添加
    #[repr(transparent)]
    ,并派生或转发所需的 trait(
    derive_more
    可以减少样板代码)。如果 newtype 编码了不变量(
    Month
    ),应通过可失败构造函数(
    TryFrom
    、
    FromStr
    、
    try_new
    )进行验证,并且绝不提供从原始类型进行无损
    From
    转换的 infallible 实现。
  • 公共或共享签名中不要使用
    bool
    参数。
    report(true)
    在调用处难以理解,也无法扩展第三种模式。应使用包含两个变体的 enum(
    Verbosity::Summary
    、
    Verbosity::Full
    ),或提供两个方法(
    report()
    、
    report_verbose()
    ):使用
    Widget::new(Size::Small, Shape::Round)
    ,而不是
    Widget::new(true, false)
    。唯一例外是调用处仅相隔几行的私有辅助函数。重构时,现有
    bool
    参数也是应消除的坏味道,而不是必须保留的细节。
  • 使用具名字段,而不是 tuple,单字段 newtype 和高度局部的代码除外。
  • 使用
    Option
    表示缺失,绝不要使用
    -1
    或
    ""
    等哨兵值。
    使用携带数据的 enum,而不是用“字段 X 仅在字段 Y 被设置时才有意义”的方式建模。
  • 使用最合适的标准类型: 路径使用
    PathBuf
    /
    &Path
    ,时间跨度使用
    Duration
    ,
    NonZeroU32
    仅在内部使用(API 边界仍保留普通整数)。
  • 相互独立的标志应使用
    bitflags
    crate;enum 用于表达恰好只有一种选择的情况。

Derive and implement the standard traits eagerly

尽早派生并实现标准 trait

Downstream crates cannot add std traits to your types (orphan rule), so you must. Derive whenever every field supports it:
Debug
(on every public type; hand-write it to redact secrets),
Clone
(except unique resources and key material),
Copy
(only small bitwise-copyable types, because the compiler copies silently),
PartialEq
/
Eq
,
PartialOrd
/
Ord
(never ship
PartialOrd
without
Ord
when a total order exists),
Hash
(consistent with
Eq
),
Default
. Implement
Display
for anything meant to be read: error types, string-like wrappers. Add
#[must_use]
to pure functions and to types, such as errors and lazy values, whose discarding is a bug.
Implement
From
/
TryFrom
, never
Into
/
TryInto
directly (blanket impls give you those). Prefer
from
/
into
over
as
, which silently allows lossy casts. Implement
FromStr
rather than an ad-hoc
from_str
method,
FromIterator
and
Extend
on collections, and
IntoIterator
for both owned and borrowed forms. Implement
Deref
only on smart pointers and owning collections (
Vec<T>
to
[T]
), never to fake inheritance. Overload operators only for their mathematical meaning.
Don't put derivable trait bounds on struct definitions:
#[derive(Clone)] struct Good<T>
, not
struct Bad<T: Clone + Debug>
. Bounds belong on the
impl
blocks that need them.
If you hand-write
Ord
, hand-write
PartialEq
,
PartialOrd
, and
Hash
to agree with it (compare the same fields, ignore the same fields). A derived
PartialEq
next to a custom
Ord
breaks the std contract and misbehaves in
BTreeMap
and
sort
/
dedup
. Say in the docs which fields equality ignores.
下游 crate 无法为自己的类型添加 std trait(orphan rule),因此你必须完成这些工作。只要每个字段都支持,就应进行派生:公共类型均应派生
Debug
(可手写实现以隐藏秘密信息),
Clone
(唯一资源和密钥材料除外),
Copy
(仅适用于体积小且可按位复制的类型,因为编译器会静默复制),
PartialEq
/
Eq
,
PartialOrd
/
Ord
(存在全序时,绝不要在缺少
Ord
的情况下提供
PartialOrd
),与
Eq
保持一致的
Hash
,以及
Default
。为所有用于展示的内容实现
Display
,例如错误类型和类字符串包装器。对于纯函数,以及丢弃后即构成错误的对象(例如错误和惰性值),应添加
#[must_use]
。
实现
From
/
TryFrom
,不要直接实现
Into
/
TryInto
,因为 blanket impl 会自动提供后两者。优先使用
from
/
into
,而不是可能静默产生有损转换的
as
。实现
FromStr
,而不是临时定义一个
from_str
方法;为集合实现
FromIterator
和
Extend
;为拥有型和借用型形式都实现
IntoIterator
。仅在智能指针和拥有型集合(
Vec<T>
到
[T]
)上实现
Deref
,绝不要借此模拟继承。仅在操作符具有数学含义时才重载操作符。
不要在 struct 定义上添加可通过派生获得的 trait bound:使用
#[derive(Clone)] struct Good<T>
,而不是
struct Bad<T: Clone + Debug>
。trait bound 应仅放在需要它们的
impl
块上。
如果手写
Ord
,则应同时手写与其保持一致的
PartialEq
、
PartialOrd
和
Hash
(比较相同的字段、忽略相同的字段)。在自定义
Ord
旁边使用派生的
PartialEq
会破坏 std 契约,并导致
BTreeMap
以及
sort
/
dedup
出现异常行为。应在文档中说明相等性比较会忽略哪些字段。

Constructors, builders, defaults

构造函数、Builder 与默认值

  • Provide
    new()
    even when
    Default
    exists, and make them agree (one delegates to the other). Derive or implement
    Default
    so callers can write
    ..Default::default()
    ; bag-of-options structs with public fields should always have it.
  • Up to two optional parameters: inherent constructors (
    new
    ,
    with_a
    ,
    with_b
    ). Beyond that, or when construction validates or has side effects, add a builder:
    Foo::builder()
    returns
    FooBuilder
    , setters are named after fields (
    timeout(d)
    , not
    set_timeout
    ) and infallible, and
    build() -> Result<Foo, Error>
    does all validation including cross-field checks. Prefer non-consuming setters (
    &mut self -> &mut Self
    ) unless
    build
    must move a value out.
  • A constructor with four or more loose arguments should group them into semantic types:
    Deposit::new(account, amount)
    rather than four
    &str
    /
    u64
    parameters.
  • 即使已经存在
    Default
    ,也应提供
    new()
    ,并确保二者行为一致(其中一个委托给另一个)。应派生或实现
    Default
    ,使调用者可以编写
    ..Default::default()
    ;具有公共字段、且包含多个可选配置的 struct 应始终支持
    Default
    。
  • 最多支持两个可选参数时,使用固有构造函数(
    new
    、
    with_a
    、
    with_b
    )。超过两个参数,或者构造过程需要验证或产生副作用时,应添加 builder:
    Foo::builder()
    返回
    FooBuilder
    ;setter 以字段命名(使用
    timeout(d)
    ,而不是
    set_timeout
    )且 infallible;
    build() -> Result<Foo, Error>
    负责包括跨字段检查在内的全部验证。除非
    build
    必须移出某个值,否则应优先使用不消费自身的 setter(
    &mut self -> &mut Self
    )。
  • 包含四个或更多独立参数的构造函数,应将这些参数组织为具有语义的类型:使用
    Deposit::new(account, amount)
    ,而不是传入四个
    &str
    /
    u64
    参数。

Visibility and evolution

可见性与演进

  • Private by default. Fields stay private unless the struct is passive C-style data. Widen to
    pub(crate)
    or
    pub(super)
    before
    pub
    . Making an item more public later is non-breaking; the reverse is breaking, and every
    pub
    item is a promise.
  • Mark public enums and public-field structs that cross crate boundaries
    #[non_exhaustive]
    unless the set is a stable ontology. When matching one, list the variants you handle and add a wildcard arm.
  • Seal traits you don't want implemented downstream, and say so in the docs:
    rust
    pub trait TheTrait: private::Sealed { /* ... */ }
    mod private {
        pub trait Sealed {}
        impl Sealed for usize {}
    }
  • Hide implementation iterator types behind
    impl Iterator<Item = T>
    (internal APIs) or a newtype (public APIs you must keep stable).
  • Expose each public item at exactly one path. Don't
    pub use
    a foreign crate's items unless you are an umbrella crate. Avoid leaking third-party types into your public API; feature-gate
    serde
    impls behind a feature named
    serde
    . If a dependency's type must appear, re-export it so callers can name the matching version.
  • Mark a function
    const
    only if it will stay
    const
    . Don't expose
    ErrorKind
    enums or other internals that lock in failure modes you consider private.
  • 默认私有。除非 struct 是被动的 C 风格数据,否则字段应保持私有。在扩大可见性时,先使用
    pub(crate)
    或
    pub(super)
    ,再使用
    pub
    。将项目改为公开通常不会破坏兼容性,而反向修改则属于破坏性变更;每个
    pub
    项目都是对使用者的承诺。
  • 对于跨越 crate 边界的公共 enum 和具有公共字段的 struct,除非其变体集合属于稳定的领域概念,否则应标记
    #[non_exhaustive]
    。匹配此类 enum 时,应列出明确处理的变体,并添加通配分支。
  • 对于不希望下游实现的 trait,应将其封闭,并在文档中说明:
    rust
    pub trait TheTrait: private::Sealed { /* ... */ }
    mod private {
        pub trait Sealed {}
        impl Sealed for usize {}
    }
  • 内部 API 应通过
    impl Iterator<Item = T>
    隐藏实现迭代器类型;必须保持稳定的公共 API 应通过 newtype 隐藏。
  • 每个公共项目应仅通过一个路径公开。除非当前 crate 是聚合 crate,否则不要
    pub use
    外部 crate 的项目。避免将第三方类型泄漏到公共 API 中;应将
    serde
    实现置于名为
    serde
    的 feature 之后。如果依赖项的类型必须出现在 API 中,应重新导出该类型,使调用者能够引用匹配的版本。
  • 仅当函数能够持续保持为
    const
    时,才将其标记为
    const
    。不要公开
    ErrorKind
    enum 或其他会将不希望成为公共契约的失败模式固化的内部实现。

Traits and generics

Trait 与泛型

  • Define a trait only for generic programming (
    T: Trait
    ,
    impl Trait
    ,
    dyn Trait
    ) or as an extension trait (
    FooExt
    ). Don't use traits merely to organize code. Put essential functionality in inherent methods and have trait impls forward to them, so callers need no import.
  • Prefer, in order: concrete types; generics (
    T: Trait
    ,
    impl Trait
    );
    dyn Trait
    , ideally wrapped in your own struct (
    struct DynStore(Arc<dyn Store>)
    ). Generics give static dispatch and inlining; trait objects give heterogeneous collections and smaller binaries. Don't nest generics on service-level types (
    Service<Backend>
    is fine;
    Service<Backend<Store>>
    is not).
  • Decide early whether a trait must be object-safe. Generic methods or
    Self
    outside the receiver break it; exclude such methods with
    where Self: Sized
    .
  • Keep required trait methods minimal and provide defaults for everything derivable from them, as
    Iterator
    does from
    next
    .
  • If a function has a clear receiver, make it a method. A computation with no receiver is a free function, not
    Database::check(params)
    . Associated functions are for construction.
  • Closures: use the most general bound that works (
    FnOnce
    if called once,
    FnMut
    if it mutates, else
    Fn
    ) and prefer
    Fn*
    bounds to bare
    fn
    pointers. Closure parameters go last; don't take more than one.
  • Keep parameter order consistent across a crate: call-specific first, ubiquitous context (loggers, allocators) last.
  • Reach for a macro only when functions, generics, and traits cannot do the job. Prefer
    macro_rules!
    to proc macros, make macro input look like the output it generates, and never let a macro change a type's kind or a function's signature.
  • 仅在泛型编程(
    T: Trait
    、
    impl Trait
    、
    dyn Trait
    )或扩展 trait(
    FooExt
    )场景中定义 trait。不要仅仅为了组织代码而使用 trait。应通过固有方法提供核心功能,并让 trait 实现转调这些方法,从而避免调用者额外导入 trait。
  • 选择顺序依次为:具体类型;泛型(
    T: Trait
    、
    impl Trait
    );
    dyn Trait
    ,最好包装在自己的 struct 中(
    struct DynStore(Arc<dyn Store>)
    )。泛型提供静态分派和内联;trait object 支持异构集合并可生成更小的二进制文件。不要在服务级类型上嵌套泛型(
    Service<Backend>
    可以,
    Service<Backend<Store>>
    不可以)。
  • 应尽早决定 trait 是否必须支持 object safety。泛型方法或接收者之外的
    Self
    会破坏 object safety;可使用
    where Self: Sized
    排除这些方法。
  • 必需的 trait 方法应保持最少,并为所有可以推导的行为提供默认实现,就像
    Iterator
    从
    next
    获得默认实现一样。
  • 如果函数具有明确的接收者,应将其定义为方法。没有接收者的计算应使用自由函数,而不是
    Database::check(params)
    。关联函数用于构造。
  • Closure:使用能够满足需求的最通用约束(只调用一次时使用
    FnOnce
    ,会修改捕获状态时使用
    FnMut
    ,否则使用
    Fn
    ),并优先使用
    Fn*
    约束,而不是裸
    fn
    指针。Closure 参数应放在最后,且不要接受多个 closure 参数。
  • 在整个 crate 中保持参数顺序一致:与调用场景密切相关的参数放在前面,通用上下文(logger、allocator)放在最后。
  • 仅当函数、泛型和 trait 都无法完成任务时才使用宏。优先选择
    macro_rules!
    而非过程宏;宏输入应看起来像其生成的输出,并且绝不能改变类型的种类或函数的签名。

Refactoring someone else's code

重构他人编写的代码

  • Fix every smell the request is about, not just the cheap ones: clones that only satisfy the borrow checker,
    &String
    /
    &Vec<T>
    parameters,
    unwrap
    on fallible paths,
    Rc<RefCell<_>>
    in signatures,
    get_
    getters,
    bool
    parameters,
    +
    string concatenation,
    #![deny(warnings)]
    , error types without
    Error
    , nondeterministic
    HashMap
    iteration in output.
  • Keep public behavior identical unless changing it is the point: the same operations, the same success and failure semantics, the same output bytes. Turning a panic into an
    Err
    is the change callers want. Dropping an error variant, removing a derive, or changing what a constructor takes is not; if a signature must change, show the before and after in the change list and say why.
  • Preserve quirks you don't understand and flag them, rather than silently "fixing" them.
  • When the module has no tests, pin the current behavior with a few before you touch it.
  • 修复请求涉及的所有坏味道,而不只是容易处理的问题:仅为满足 borrow checker 而存在的 clone、
    &String
    /
    &Vec<T>
    参数、可失败路径中的
    unwrap
    、签名中的
    Rc<RefCell<_>>
    、带
    get_
    的 getter、
    bool
    参数、字符串
    +
    拼接、
    #![deny(warnings)]
    、未实现
    Error
    的错误类型,以及输出中不确定的
    HashMap
    迭代顺序。
  • 除非行为变更本身是任务目标,否则应保持公共行为完全一致:相同的操作、相同的成功与失败语义,以及相同的输出字节。将 panic 转换为
    Err
    往往是调用者期望的变更。删除错误变体、移除派生或更改构造函数参数则不属于此类变更;如果必须修改签名,应在变更列表中展示修改前后内容并说明原因。
  • 保留那些尚未理解但具有重要影响的特殊行为,并将其标记出来,而不是静默地“修复”。如果模块尚无测试,应在修改前通过一些测试锁定当前行为。

5. Errors and panics

5. 错误与 Panic

Pick the mechanism by contract:
SituationUse
Absence is normal (
first()
,
checked_add
)
Option<T>
The caller may need to know what went wrong
Result<T, E>
A bug or violated invariant inside your code
panic!
/
assert!
/
expect("why")
Panics are not exceptions. A panic means "stop now". Never use one to report bad input, never rely on
catch_unwind
for control flow (it does nothing under
panic = "abort"
and can leave state inconsistent), and never catch a panic and continue inside a library. Where a panicking convenience is justified, pair it with a fallible API (
get
/
get_unchecked
,
new
/
try_new
) and document it under
# Panics
.
Every intentional panic carries a message with the relevant values:
assert!(buf.len() >= HEADER, "buffer too small: {} bytes", buf.len())
,
expect("config is loaded before workers start")
. Prefer
expect("reason")
to
unwrap()
. Don't restate the condition as the message.
Pattern-match instead of check-then-unwrap:
if let Some(v) = opt { .. }
or
let Some(v) = opt else { return .. };
, never
if opt.is_some() { opt.unwrap() }
.
Lock poisoning is fatal:
.lock().expect("lock poisoned")
. Don't recover with
into_inner
.
Library error types (any
E
in a public
Result
):
  • Implement
    std::error::Error
    (which requires
    Debug
    +
    Display
    ), be
    Send + Sync + 'static
    , and implement
    source()
    for wrapped causes. Never use
    ()
    ,
    String
    , or
    Box<dyn Error>
    as a library's public error type.
  • Display
    messages are lowercase, concise, and have no trailing punctuation:
    "invalid header length"
    .
  • Implement
    From<Upstream>
    so
    ?
    converts. Use
    map_err
    only for foreign errors or to add context.
  • Two sound shapes: an enum with one variant per cause (
    thiserror
    generates this well), or a struct per situation with a private
    kind
    plus
    is_xxx()
    and accessor methods. Either way, don't make one crate-wide enum that lists every failure mode in the crate, and don't expose details callers cannot act on.
  • If a fallible function consumes an argument, return it inside the error so the caller can retry without cloning (
    FromUtf8Error::into_bytes
    ).
  • Never expose
    anyhow::Error
    in a library's public API.
Application error handling:
anyhow
or
eyre
with
.context("opening config")
is the right tool. Use one app-level error type, not several.
main
may return
Result<(), anyhow::Error>
.
Destructors never fail, panic, or block. Provide
close(self) -> Result<()>
for fallible teardown and let
Drop
do best-effort cleanup.
Document failure modes:
# Errors
for any
Result
,
# Panics
for any reachable panic,
# Safety
for every
unsafe fn
.
根据契约选择错误处理机制:
情况使用方式
缺失属于正常情况(
first()
、
checked_add
)
Option<T>
调用者可能需要了解出错原因
Result<T, E>
代码中存在 bug 或内部不变量被破坏
panic!
/
assert!
/
expect("why")
Panic 不是异常。 Panic 意味着“立即停止”。绝不能用 panic 报告错误输入;不要依赖
catch_unwind
控制流程(在
panic = "abort"
下它不起作用,并且可能使状态不一致);在库中捕获 panic 后继续执行也绝不可取。如果确实需要提供会 panic 的便捷 API,应同时提供可失败 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
    。
  • 有两种健全的形态:每种底层原因对应一个变体的 enum(
    thiserror
    可以很好地生成这种代码),或每种情况对应一个 struct,并包含私有
    kind
    、
    is_xxx()
    及访问器方法。无论采用哪种形式,都不要创建一个列举整个 crate 所有失败模式的全局 enum,也不要公开调用者无法据以采取行动的细节。
  • 如果可失败函数会消费参数,应在错误中返回该参数,使调用者无需克隆即可重试(
    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. Everyday idioms

6. 日常惯用法

  • Iterators over hand loops when the pipeline is short and reads top-down:
    values.iter().filter(..).map(..).collect()
    ,
    .any()
    ,
    .sum()
    ,
    .fold()
    . Collect fallible steps into
    Result<Vec<_>, E>
    and apply
    ?
    . Keep an explicit
    for
    loop when the body is large, does several jobs, or needs awkward early returns. Bind a long chain to a named variable rather than putting it in a
    for
    header, and prefer
    for x in it
    to
    .for_each(|x| ..)
    .
  • Option
    /
    Result
    combinators
    (
    map
    ,
    and_then
    ,
    ok_or_else
    ,
    unwrap_or_else
    ,
    as_ref
    ,
    as_deref
    ) when they're clearer than
    match
    , and
    match
    when they're not.
  • format!
    /
    write!
    with inline args (
    format!("{name}: {count}")
    ) for concatenation and substitution, not chained
    +
    or
    str::replace
    .
    write!
    into an existing buffer in hot paths.
  • Explicit types where inference obscures intent: on
    .into()
    ,
    .parse()
    ,
    .collect()
    , and complex iterator results. Prefer
    let x: Vec<u32> = ..
    to a turbofish.
  • Drop guards for cleanup that must run on early return or unwind. Bind them to a name (
    let _guard = ..
    ), because
    let _ = ..
    drops immediately.
  • Closures that capture mixed ownership: rebind in a block, then
    move
    :
    rust
    let job = {
        let cfg = cfg.clone();
        move || run(cfg, data)
    };
  • Import style: one
    use
    per module path (
    use foo::bar::{Bar, Baz};
    ); types and traits imported and used unqualified (
    Regex
    ); free functions qualified by their module (
    mem::take
    ,
    fs::read
    ). Import a trait inside the function that needs it unless the whole file uses it. No glob imports except
    use super::*
    in test modules and
    use Enum::*
    inside a function that matches on it.
  • Magic numbers become named
    const
    s with a doc comment saying where the value comes from and what changing it affects.
  • Logging: structured events through
    tracing
    or
    log
    , not
    println!
    /
    dbg!
    , except for a CLI's own stdout output.
  • 当处理流水线较短且自上而下易于阅读时,优先使用迭代器而不是手写循环:
    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!
    进行拼接和替换,而不是链式使用
    +
    或
    str::replace
    。在热路径中,使用
    write!
    写入已有缓冲区。
  • 在类型推断会模糊意图的位置显式指定类型: 例如
    .into()
    、
    .parse()
    、
    .collect()
    以及复杂的迭代器结果。优先使用
    let x: Vec<u32> = ..
    ,而不是 turbofish。
  • 使用 Drop guard 执行必须在提前返回或 unwind 期间运行的清理。应将其绑定到名称(
    let _guard = ..
    ),因为
    let _ = ..
    会立即将其丢弃。
  • 处理混合所有权的 closure: 在代码块中重新绑定变量,然后使用
    move
    :
    rust
    let job = {
        let cfg = cfg.clone();
        move || run(cfg, data)
    };
  • 导入风格: 每个模块路径使用一条
    use
    (
    use foo::bar::{Bar, Baz};
    );类型和 trait 以非限定名称导入并使用(
    Regex
    );自由函数通过其模块限定(
    mem::take
    、
    fs::read
    )。除非整个文件都需要某个 trait,否则应在需要它的函数内部导入。除测试模块中的
    use super::*
    和函数内部匹配 enum 时的
    use Enum::*
    外,不要使用 glob 导入。
  • 魔法数字应替换为具名
    const
    ,并添加文档注释说明数值来源以及修改它会产生什么影响。
  • 日志: 通过
    tracing
    或
    log
    记录结构化事件,不要使用
    println!
    /
    dbg!
    ,但 CLI 自身的标准输出除外。

7. Concurrency and async

7. 并发与 Async

  • Public types, and the futures they return, should be
    Send
    (and usually
    Sync
    ). Pin it with a compile-time check so a refactor can't silently regress it:
    rust
    const _: () = {
        const fn assert_send<T: Send>() {}
        assert_send::<MyClient>();
    };
    Holding an
    Rc
    or a
    RefCell
    borrow across an
    .await
    makes a future
    !Send
    .
  • Prefer message passing (channels) to shared state. When sharing is necessary: keep data with linked invariants under one lock, keep lock scopes to a few lines, never call user closures while holding a lock, and never return a
    MutexGuard
    from a public method.
  • Write
    async fn
    rather than
    fn -> impl Future
    , except in traits or when setup must run outside the async block for a hot path.
  • Long CPU-bound work inside async must yield (
    tokio::task::yield_now().await
    every tens of microseconds) or move to
    spawn_blocking
    .
  • Library code doesn't spawn raw threads or assume a runtime; take an executor, a spawn function, or
    impl AsyncRead
    /
    impl Read
    from the caller.
公共类型及其返回的 future 应满足
Send
(通常也应满足
Sync
)。可通过编译期检查将其固定下来,防止重构后在不知情的情况下退化:
rust
const _: () = {
    const fn assert_send<T: Send>() {}
    assert_send::<MyClient>();
};
跨
.await
持有
Rc
或
RefCell
借用会使 future 变成
!Send
。
优先使用消息传递(channel),而不是共享状态。必须共享时:将具有关联不变量的数据放在同一把锁下,将锁的作用域限制在几行以内,绝不在持锁时调用用户提供的 closure,并且绝不从公共方法返回
MutexGuard
。
编写
async fn
,而不是
fn -> impl Future
;trait 中的方法,或热路径中必须在 async block 外完成初始化的场景除外。
async 内部的长时间 CPU 密集型工作必须让出执行权(每几十微秒调用一次
tokio::task::yield_now().await
),或者移至
spawn_blocking
。
库代码不应直接创建裸线程,也不应假定存在某个 runtime;应从调用者处接收 executor、spawn 函数,或
impl AsyncRead
/
impl Read
。

8. Unsafe and FFI

8. Unsafe 与 FFI

  • Write
    unsafe
    only for one of three reasons: a novel abstraction safe code can't express, a benchmarked performance need, or FFI and platform calls. Reach for std and well-reviewed crates first; they already contain the audited
    unsafe
    .
  • Every
    unsafe
    block or impl gets a
    // SAFETY:
    comment stating why the invariants hold. Every
    unsafe fn
    gets a
    # Safety
    doc section listing what the caller must uphold. Enable
    unsafe_op_in_unsafe_fn
    .
  • Keep
    unsafe
    in the smallest module that can uphold its invariant and wrap it in a safe API; the module boundary is the soundness boundary. A safe function that can cause undefined behavior through any sequence of safe calls is unsound, and unsound code is never acceptable.
  • Mark a function
    unsafe
    only when misuse can cause undefined behavior, not because it is dangerous (
    delete_database()
    is a safe function).
  • No
    transmute
    to cast enums or dodge lifetimes, and no
    unsafe impl Send
    to silence the compiler.
  • Test unsafe code under Miri and with adversarial inputs.
  • FFI:
    #[repr(C)]
    on shared structs, sized integer types,
    CString
    /
    CStr
    for strings, allocate and free on the same side, wrap C-owned pointers in an RAII type whose
    Drop
    frees them, check for null and return
    Result
    , and stop panics at the boundary with
    catch_unwind
    . Generate bindings with
    bindgen
    in a
    foo-sys
    crate and keep business logic in a safe core crate.
  • 仅应出于以下三种原因之一编写
    unsafe
    :安全代码无法表达某种新型抽象、存在经过基准测试的性能需求,或需要处理 FFI 与平台调用。优先使用 std 和经过充分审查的 crate;它们已经包含经过审计的
    unsafe
    实现。
  • 每个
    unsafe
    block 或 impl 都应带有
    // SAFETY:
    注释,说明其不变量为何成立。每个
    unsafe fn
    都应提供
    # Safety
    文档章节,列出调用者必须满足的条件。启用
    unsafe_op_in_unsafe_fn
    。
  • 将
    unsafe
    限制在能够保证其不变量的最小模块中,并使用安全 API 对外包装;模块边界就是健全性边界。如果一个安全函数可能通过任意安全调用序列导致未定义行为,那么该函数就是不健全的,而不健全代码绝不可接受。
  • 仅当误用可能导致未定义行为时,才将函数标记为
    unsafe
    ,不要仅仅因为函数具有危险性;例如,
    delete_database()
    仍应是安全函数。
  • 不要使用
    transmute
    转换 enum 或规避生命周期,也不要使用
    unsafe impl Send
    来压制编译器。
  • 应在 Miri 下并使用对抗性输入测试 unsafe 代码。
  • FFI:共享 struct 应使用
    #[repr(C)]
    和定长整数类型;字符串应使用
    CString
    /
    CStr
    ;内存应在同一侧分配和释放;应使用 RAII 类型包装 C 所有的指针,并由
    Drop
    负责释放;应检查空指针并返回
    Result
    ;应在边界处使用
    catch_unwind
    阻止 panic 传播。使用
    bindgen
    在
    foo-sys
    crate 中生成绑定,并将业务逻辑保留在安全的核心 crate 中。

9. Crates, modules, dependencies

9. Crate、模块与依赖

  • Prefer small, single-purpose crates in a flat
    crates/
    workspace; never nest a crate inside another crate's
    src/
    . Split when a crate grows a second purpose or a subdirectory. Share versions and lints through
    [workspace.dependencies]
    and
    [workspace.lints]
    .
  • Modules: name a leaf module after its primary item (
    Foo
    lives in
    foo.rs
    ). Non-leaf modules contain only
    mod
    declarations and re-exports. Put the essential items at the crate root and group the rest by use case (
    account
    ,
    network
    ), not by kind (
    traits
    ,
    errors
    ). Don't define a prelude.
  • Re-export individually (
    pub use foo::{A, B};
    ), never with a glob.
  • Dependencies: every one costs compile time, binary size, and supply-chain exposure, including
    build.rs
    and proc macros. Use ranges (
    "1"
    ,
    "1.4"
    ), never
    "*"
    or a needless
    "="
    . Commit
    Cargo.lock
    for binaries. Audit with
    cargo tree --duplicates
    ,
    cargo deny
    ,
    cargo udeps
    , and
    cargo audit
    .
  • Features are additive and bare-named:
    std
    ,
    serde
    ,
    alloc
    ,
    test-util
    . Never
    no-std
    ,
    use-serde
    , or a feature that removes or changes a public item. Don't feature-gate struct fields or trait methods. Put mocks and fakes behind
    test-util
    .
  • Semver: breaking changes include removing or renaming public items, adding a variant to an enum that isn't
    #[non_exhaustive]
    , adding a field to a publicly constructible struct, adding a trait bound, or making a trait non-object-safe. Deprecate in a minor release before removing in a major one. Use the latest stable edition for new crates and set an MSRV deliberately.
  • Don't run code before
    main()
    (no
    ctor
    -style crates). Don't use unstable features on a stable toolchain.
  • 优先使用小型、单一用途的 crate,并采用扁平的
    crates/
    workspace;绝不要在另一个 crate 的
    src/
    中嵌套 crate。当 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。不要通过 feature 控制 struct 字段或 trait 方法。Mock 和 fake 应置于
    test-util
    之后。
  • Semver: 以下属于破坏性变更:移除或重命名公共项目;向未标记
    #[non_exhaustive]
    的 enum 添加变体;向可由外部构造的 struct 添加字段;添加 trait bound;或使 trait 不再支持 object safety。在次要版本中先弃用,再在主要版本中移除。新 crate 应使用最新的稳定 edition,并明确设置 MSRV。
  • 不要在
    main()
    之前运行代码(不要使用
    ctor
    风格的 crate)。不要在稳定工具链上使用不稳定特性。

10. Documentation and comments

10. 文档与注释

  • ///
    on every public item;
    //!
    at the top of every public module and the crate root. The first sentence is a single line of about fifteen words, because it becomes the summary in listings.
  • Sections, in this order, as applicable:
    # Examples
    ,
    # Errors
    ,
    # Panics
    ,
    # Safety
    . Examples use
    ?
    with the
    fn main
    hidden behind
    # 
    lines, never
    unwrap()
    , because users copy them verbatim. Describe parameters in prose, not a table.
  • Document what the signature can't say: preconditions, side effects, ownership transfer, complexity, and why. Don't restate the type or the name;
    /// Removes an element.
    on
    remove_element
    is noise.
  • Link items with
    [`Type`]
    . Hide impls users never trigger (
    From<PrivateError> for PublicError
    ) with
    #[doc(hidden)]
    .
  • //
    comments explain why, not what:
    // SAFETY:
    ,
    // TODO(issue-123):
    , non-obvious magic values, workarounds with a link. No design journals, "why we chose X" essays, or rule-compliance tables in code docs. Document the end state.
  • 每个公共项目都应使用
    ///
    ;每个公共模块和 crate 根目录都应使用
    //!
    。第一句话应为约十五个单词的单行摘要,因为它会成为列表中的摘要。
  • 根据适用情况,按以下顺序组织章节:
    # Examples
    、
    # Errors
    、
    # Panics
    、
    # Safety
    。示例应使用
    ?
    ,并通过
    # 
    行隐藏
    fn main
    ,绝不要使用
    unwrap()
    ,因为用户会原样复制示例。应以文字而非表格描述参数。
  • 应记录签名无法表达的信息:前置条件、副作用、所有权转移、复杂度以及实现原因。不要重复类型或名称;为
    remove_element
    编写
    /// Removes an element.
    只是噪声。
  • 使用
    [`Type`]
    链接项目。应隐藏用户永远不会触发的实现(
    From<PrivateError> for PublicError
    ),方法是添加
    #[doc(hidden)]
    。
  • //
    注释应解释原因,而不是复述代码行为:例如
    // SAFETY:
    、
    // TODO(issue-123):
    、不明显的魔法值,以及带有链接的临时解决方案。代码文档中不应包含设计日志、“我们为何选择 X”之类的长篇文章或规则对照表。只记录最终状态。

11. Tests

11. 测试

  • Unit tests in
    #[cfg(test)] mod tests { use super::*; .. }
    for private internals. Integration tests in
    tests/
    for anything reachable through the public API, and prefer that location when either would do. Doc examples are tests too.
  • Test observable behavior and properties (monotonic, round-trips, idempotent), not restated constants or mirrored branches.
  • Write a failing test before fixing a bug. Cover each feature combination and each platform-specific path.
  • Anything doing I/O, time, randomness, or network must be mockable:
    new_mocked() -> (Self, MockCtrl)
    behind
    test-util
    , not a
    Default
    that performs I/O.
  • Fuzz parsers of untrusted input with
    cargo fuzz
    . Benchmark with
    criterion
    or
    divan
    and
    std::hint::black_box
    . Run Miri over
    unsafe
    .
  • Assertions get context so failures read well:
    assert_eq!(got, want, "input {input:?}")
    .
  • 对于私有内部实现,使用
    #[cfg(test)] mod tests { use super::*; .. }
    编写单元测试。对于任何可通过公共 API 访问的功能,使用
    tests/
    中的集成测试;如果两种方式均可,优先使用集成测试。文档示例同样是测试。
  • 应测试可观察行为和属性(单调性、往返转换、幂等性),而不是复述常量或镜像分支逻辑。
  • 修复 bug 前先编写一个会失败的测试。覆盖每种 feature 组合以及每条平台专用路径。
  • 任何涉及 I/O、时间、随机性或网络的功能都必须可 mock:通过
    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. Performance, in order

12. 性能:按顺序处理

Design for usability first and optimize after measuring. When you do:
Vec::with_capacity
or
collect()
over push loops; reuse caller-owned buffers in hot APIs (
fn read_into(&mut self, buf: &mut Vec<u8>)
);
Box<[T]>
,
Box<str>
,
Arc<str>
for long-lived immutable sequences;
shrink_to_fit
on long-lived collections built without a reservation; a faster hasher (
foldhash
,
FxHash
) only for trusted keys; batch work instead of per-item locking; applications may set a
#[global_allocator]
such as
mimalloc
. Derive
Copy
for small plain types and never for large ones.
首先围绕可用性进行设计,测量之后再进行优化。需要优化时:使用
Vec::with_capacity
或
collect()
,而不是 push 循环;在热路径 API 中复用调用者拥有的缓冲区(
fn read_into(&mut self, buf: &mut Vec<u8>)
);对于长期存在且不可变的序列,使用
Box<[T]>
、
Box<str>
、
Arc<str]
;对于未预留容量而构建的长期集合,使用
shrink_to_fit
;仅在密钥可信时使用更快的 hasher(
foldhash
、
FxHash
);批量处理工作,而不是逐项加锁;应用可以设置
#[global_allocator]
,例如
mimalloc
。为体积较小的普通类型派生
Copy
,但绝不要为大型类型这样做。

13. Final checklist

13. 最终检查清单

Before returning Rust code, confirm:
  • cargo fmt
    ,
    cargo clippy -- -D warnings
    , and
    cargo test
    are clean.
  • No
    unwrap()
    outside tests and
    main
    ; every
    expect
    ,
    assert
    , and
    panic
    has a message with the relevant values.
  • Public functions take
    &str
    ,
    &[T]
    ,
    impl AsRef<_>
    , or
    impl IntoIterator
    , not
    &String
    or
    &Vec<T>
    .
  • Every public type derives
    Debug
    and the applicable common traits; error types implement
    Error + Display + Send + Sync + 'static
    .
  • No
    bool
    parameters in public signatures; no hand-written
    Ord
    without matching
    PartialEq
    and
    Hash
    .
  • No
    .clone()
    whose only job is to satisfy the borrow checker.
  • No glob imports, no preludes, no glob re-exports, no leaked third-party types.
  • Every
    unsafe
    block has a
    // SAFETY:
    comment; every
    unsafe fn
    has
    # Safety
    docs.
  • Public items have docs with a one-line summary and
    # Errors
    /
    # Panics
    where relevant.
  • Application code stayed simple; library code followed the API rules.
返回 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
    参数;手写
    Ord
    时配有匹配的
    PartialEq
    和
    Hash
    。
  • 不存在仅为满足 borrow checker 而调用的
    .clone()
    。
  • 没有 glob 导入、prelude、glob 重新导出或泄漏的第三方类型。
  • 每个
    unsafe
    block 都有
    // SAFETY:
    注释;每个
    unsafe fn
    都有
    # Safety
    文档。
  • 公共项目都有单行摘要文档,并在相关位置提供
    # Errors
    /
    # Panics
    。
  • 应用代码保持简单;库代码遵循 API 规则。

References

参考资料

Read these when you need the reasoning behind a rule or a complete checklist for a task. Each is a condensed digest of one source.
当需要了解某条规则背后的原因或某项任务的完整检查清单时,请阅读以下资料。每份资料都是对应来源的浓缩摘要。