idiomatic-rust
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseIdiomatic 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 that returns a value beats one that
assigns to a 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. keeps an early return at the
top instead of drifting the happy path rightward under a nested ;
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.
matchlet mutlet ... elseif letmatches!优先使用表达式而非语句:返回值的 优于在每个分支中赋值给 的 ;直接产生答案的链式调用优于在循环中设置标志变量、之后再对其检查——让类型系统承载不变量,而不是在运行时检查。 可以让提前返回保持在顶部,而不是在嵌套的 下把正常路径不断向右挤; 则是对模式进行布尔测试。如果工具链支持 if-let 链,可以使用它们组合条件——这需要较新的 edition,因此应先检查仓库的 MSRV。
matchlet mutmatchlet ... elseif letmatches!Iterators
迭代器
Reach for the iterator pipeline before the index loop — 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 you then convert:
for i in 0..v.len() { ... v[i] ... }Vecrust
// 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();collectHashMapHashSetStringResult<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<_>, _>>()
}Prefer the plain loop when the body has real side effects or an early exit a
combinator would obscure — clarity beats iterator purity.
for在索引循环之前,先考虑迭代器管线—— 几乎总有等价的迭代器写法,而且迭代器版本在边界错误时无法编译,而不是在运行时 panic。直接 collect 成目标类型,不要先 collect 成 再转换:
for i in 0..v.len() { ... v[i] ... }Vecrust
// 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();collectHashMapHashSetStringResult<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<_>, _>>()
}如果循环体存在实际副作用,或者组合器会掩盖提前退出,应优先使用普通的 循环——清晰性比迭代器的纯粹性更重要。
forConversions
转换
When one type can be built from another, implement — comes free
through the blanket impl, so never write both directions by hand. When the
conversion can fail, implement instead of plus a panic or a
sentinel value.
FromIntoTryFromFromrust
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)
}
}当一个类型可以由另一个类型构建时,实现 ——通过 blanket impl 可以自动获得 ,因此绝不要手动实现两个方向。当转换可能失败时,实现 ,而不是使用 加 panic 或哨兵值。
FromIntoTryFromFromrust
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 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:
#[derive(Debug, Clone, PartialEq, Default, Hash)]- A that must preserve an invariant the field-wise default breaks (a
Defaultsumming to 100, not zero per field).Percentage - A that must redact a secret field (an API key, a password) rather than print it.
Debug - where logically-equal values can differ in a field that shouldn't count (a cached hash, a timestamp).
PartialEq
For an enum, with on the intended variant is the
same rule — in this directory has the pair.
#[derive(Default)]#[default]BOILERPLATE.md在动手编写之前,先考虑 ——derive 生成的 impl 基于实际字段,因此会随 struct 变化保持同步。仅当 derive 不正确时,才编写手动版本:
#[derive(Debug, Clone, PartialEq, Default, Hash)]- 必须保留逐字段默认值会破坏的不变量,例如总和为 100 的
Default,而不是每个字段都默认为零。Percentage - 必须隐藏机密字段,例如 API key 或 password,而不是将其打印出来。
Debug - 中,逻辑上相等的值可能包含一个不应参与比较的字段,例如缓存的 hash 或 timestamp。
PartialEq
对于 enum,在目标 variant 上使用 并采用 ,遵循的是同一规则——本目录中的 提供了对应示例。
#[default]#[derive(Default)]BOILERPLATE.mdNewtypes
Newtype
A newtype costs nothing at runtime and makes a swapped-argument call a type error.
Reach for one wherever a bare , , or stands in for a domain
concept (an ID, a currency amount) rather than a number.
u64Stringf64newtype 在运行时没有额外开销,并且能让参数顺序传错成为类型错误。当裸 、 或 实际代表某个领域概念,例如 ID 或货币金额,而不是普通数值时,应使用 newtype。
u64Stringf64Naming
命名
Names earn every word: , not — , ,
, , , and mark a name nobody finished. The
// prefixes promise a cost — free view, allocation, consumption.
No on a field accessor; acronyms are words (, ); a helper
that does not need is a free function, not an block used as a
namespace. The tiers table and worked pairs live in .
BookingsBookingServiceServiceManagerHelperUtilDataInfoas_to_into_get_HttpClientUuidSelfimplNAMING.md每个名称中的词都应有意义:使用 ,而不是 ——、、、、 和 往往意味着名称尚未斟酌完成。、 和 前缀意味着不同的成本——免费视图、分配或消耗。字段访问器不要使用 ;缩写应作为完整单词处理,例如 、;如果 helper 不需要 ,它应该是普通函数,而不是被当作命名空间使用的 块。层级表和完整示例位于 。
BookingsBookingServiceServiceManagerHelperUtilDataInfoas_to_into_get_HttpClientUuidSelfimplNAMING.mdBoilerplate that is actually a design signal
真正体现设计信号的样板代码
Not all repetition is a away from disappearing — five near-identical
arms, or a wrapper with ten forwarding methods, can point at a missing
abstraction. See for the pairs, including the trap.
derivematchBOILERPLATE.mdDeref并非所有重复都能通过 derive 消除——五个几乎相同的 分支,或者一个包含十个转发方法的 wrapper,都可能表明缺少抽象。相关示例,包括 陷阱,请参阅 。
matchDerefBOILERPLATE.mdDeferrals
暂不处理的问题
This skill governs expression, not decision-making: the clone-versus-borrow call is
, error type shape is , and making an illegal
state unrepresentable is .
/ownership-not-clone/rust-errors/type-driven-design本 skill 负责表达形式,而非决策:clone 与 borrow 之间的选择见 ,错误类型设计见 ,如何让非法状态无法表示见 。
/ownership-not-clone/rust-errors/type-driven-designVerification
验证
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 testRun the trio at the lint level the repo configures; treat
findings as proposals, not violations, unless the repo opted into that group.
clippy::pedantic行为保持相同,只改变代码形态——运行标准三件套:
bash
cargo clippy --all-targets # add -- -D warnings only if the repo has no lint config
cargo fmt --check
cargo test按仓库配置的 lint 级别运行这三项;将 的发现视为建议而非违规,除非仓库选择启用了该组。
clippy::pedantic