idiomatic-rust

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Idiomatic Rust

地道 Rust

This skill is about expression: what form does a Rust reader expect to see?
本 skill 关注的是表达式:Rust 读者期望看到的是哪种形式?

The shape of idiomatic Rust

地道 Rust 的形态

Prefer expressions over statements: a
match
that returns a value beats one that assigns to a
let mut
in every arm, and a chain that produces the answer beats a flag variable set in a loop and checked after it — let the type system carry invariants instead of runtime checks.
let ... else
keeps an early return at the top instead of drifting the happy path rightward under a nested
if let
;
matches!
is the boolean test on a pattern. If-let chains compose conditions where the toolchain supports them — a recent edition, so check the repo MSRV first.
优先使用表达式而非语句:返回值的
match
优于在每个分支中赋值给
let mut
的
match
;直接产生答案的链式调用优于在循环中设置标志变量、之后再对其检查——让类型系统承载不变量,而不是在运行时检查。
let ... else
可以让提前返回保持在顶部,而不是在嵌套的
if let
下把正常路径不断向右挤;
matches!
则是对模式进行布尔测试。如果工具链支持 if-let 链,可以使用它们组合条件——这需要较新的 edition,因此应先检查仓库的 MSRV。

Iterators

迭代器

Reach for the iterator pipeline before the index loop —
for i in 0..v.len() { ... v[i] ... }
almost always has an iterator equivalent, and the iterator version fails to compile on bad bounds instead of panicking at runtime. Collect into the type you want, not a
Vec
you then convert:
rust
// Reads like a translation.
let mut names = Vec::new();
for user in &users {
    names.push(user.name.clone());
}

// Reads like Rust.
let names: Vec<String> = users.iter().map(|u| u.name.clone()).collect();
collect
also targets
HashMap
,
HashSet
,
String
, and
Result<Vec<_>, _>
— that last is how
?
composes with iteration.
rust
use std::num::ParseIntError;

fn parse_all(lines: &[&str]) -> Result<Vec<i64>, ParseIntError> {
    lines.iter().map(|line| line.parse::<i64>()).collect::<Result<Vec<_>, _>>()
}
Prefer the plain
for
loop when the body has real side effects or an early exit a combinator would obscure — clarity beats iterator purity.
在索引循环之前,先考虑迭代器管线——
for i in 0..v.len() { ... v[i] ... }
几乎总有等价的迭代器写法,而且迭代器版本在边界错误时无法编译,而不是在运行时 panic。直接 collect 成目标类型,不要先 collect 成
Vec
再转换:
rust
// Reads like a translation.
let mut names = Vec::new();
for user in &users {
    names.push(user.name.clone());
}

// Reads like Rust.
let names: Vec<String> = users.iter().map(|u| u.name.clone()).collect();
collect
也可以用于
HashMap
、
HashSet
、
String
和
Result<Vec<_>, _>
——最后一个正是
?
与迭代组合的方式。
rust
use std::num::ParseIntError;

fn parse_all(lines: &[&str]) -> Result<Vec<i64>, ParseIntError> {
    lines.iter().map(|line| line.parse::<i64>()).collect::<Result<Vec<_>, _>>()
}
如果循环体存在实际副作用,或者组合器会掩盖提前退出,应优先使用普通的
for
循环——清晰性比迭代器的纯粹性更重要。

Conversions

转换

When one type can be built from another, implement
From
—
Into
comes free through the blanket impl, so never write both directions by hand. When the conversion can fail, implement
TryFrom
instead of
From
plus a panic or a sentinel value.
rust
struct Celsius(f64);
struct Fahrenheit(f64);

impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}
当一个类型可以由另一个类型构建时,实现
From
——通过 blanket impl 可以自动获得
Into
,因此绝不要手动实现两个方向。当转换可能失败时,实现
TryFrom
,而不是使用
From
加 panic 或哨兵值。
rust
struct Celsius(f64);
struct Fahrenheit(f64);

impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}

Derives before hand-written impls

优先 derive,而不是手写 impl

Reach for
#[derive(Debug, Clone, PartialEq, Default, Hash)]
before writing it by hand — a derived impl is generated from the actual fields, so it stays in sync as the struct changes. Write the manual version only when the derive would be wrong:
  • A
    Default
    that must preserve an invariant the field-wise default breaks (a
    Percentage
    summing to 100, not zero per field).
  • A
    Debug
    that must redact a secret field (an API key, a password) rather than print it.
  • PartialEq
    where logically-equal values can differ in a field that shouldn't count (a cached hash, a timestamp).
For an enum,
#[derive(Default)]
with
#[default]
on the intended variant is the same rule —
BOILERPLATE.md
in this directory has the pair.
在动手编写之前,先考虑
#[derive(Debug, Clone, PartialEq, Default, Hash)]
——derive 生成的 impl 基于实际字段,因此会随 struct 变化保持同步。仅当 derive 不正确时,才编写手动版本:
  • Default
    必须保留逐字段默认值会破坏的不变量,例如总和为 100 的
    Percentage
    ,而不是每个字段都默认为零。
  • Debug
    必须隐藏机密字段,例如 API key 或 password,而不是将其打印出来。
  • PartialEq
    中,逻辑上相等的值可能包含一个不应参与比较的字段,例如缓存的 hash 或 timestamp。
对于 enum,在目标 variant 上使用
#[default]
并采用
#[derive(Default)]
,遵循的是同一规则——本目录中的
BOILERPLATE.md
提供了对应示例。

Newtypes

Newtype

A newtype costs nothing at runtime and makes a swapped-argument call a type error. Reach for one wherever a bare
u64
,
String
, or
f64
stands in for a domain concept (an ID, a currency amount) rather than a number.
newtype 在运行时没有额外开销,并且能让参数顺序传错成为类型错误。当裸
u64
、
String
或
f64
实际代表某个领域概念,例如 ID 或货币金额,而不是普通数值时,应使用 newtype。

Naming

命名

Names earn every word:
Bookings
, not
BookingService
—
Service
,
Manager
,
Helper
,
Util
,
Data
, and
Info
mark a name nobody finished. The
as_
/
to_
/
into_
prefixes promise a cost — free view, allocation, consumption. No
get_
on a field accessor; acronyms are words (
HttpClient
,
Uuid
); a helper that does not need
Self
is a free function, not an
impl
block used as a namespace. The tiers table and worked pairs live in
NAMING.md
.
每个名称中的词都应有意义:使用
Bookings
,而不是
BookingService
——
Service
、
Manager
、
Helper
、
Util
、
Data
和
Info
往往意味着名称尚未斟酌完成。
as_
、
to_
和
into_
前缀意味着不同的成本——免费视图、分配或消耗。字段访问器不要使用
get_
;缩写应作为完整单词处理,例如
HttpClient
、
Uuid
;如果 helper 不需要
Self
,它应该是普通函数,而不是被当作命名空间使用的
impl
块。层级表和完整示例位于
NAMING.md
。

Boilerplate that is actually a design signal

真正体现设计信号的样板代码

Not all repetition is a
derive
away from disappearing — five near-identical
match
arms, or a wrapper with ten forwarding methods, can point at a missing abstraction. See
BOILERPLATE.md
for the pairs, including the
Deref
trap.
并非所有重复都能通过 derive 消除——五个几乎相同的
match
分支,或者一个包含十个转发方法的 wrapper,都可能表明缺少抽象。相关示例,包括
Deref
陷阱,请参阅
BOILERPLATE.md
。

Deferrals

暂不处理的问题

This skill governs expression, not decision-making: the clone-versus-borrow call is
/ownership-not-clone
, error type shape is
/rust-errors
, and making an illegal state unrepresentable is
/type-driven-design
.
本 skill 负责表达形式,而非决策:clone 与 borrow 之间的选择见
/ownership-not-clone
,错误类型设计见
/rust-errors
,如何让非法状态无法表示见
/type-driven-design
。

Verification

验证

Same behavior, different shape — run the standard trio:
bash
cargo clippy --all-targets   # add -- -D warnings only if the repo has no lint config
cargo fmt --check
cargo test
Run the trio at the lint level the repo configures; treat
clippy::pedantic
findings as proposals, not violations, unless the repo opted into that group.
行为保持相同,只改变代码形态——运行标准三件套:
bash
cargo clippy --all-targets   # add -- -D warnings only if the repo has no lint config
cargo fmt --check
cargo test
按仓库配置的 lint 级别运行这三项;将
clippy::pedantic
的发现视为建议而非违规,除非仓库选择启用了该组。