关于 idiomatic-rust
专注于 Rust 惯用表达的编码技能,涵盖迭代器、类型转换、派生实现、新类型和命名等常见选择,适合整理 Rust 代码,并附有标准验证清单。
主要功能
- 01表达式导向的 Rust 模式
- 02迭代器流水线指南
- 03转换 Trait 的选择
- 04派生实现与新类型指南
- 05命名与验证建议
适用场景
- 01将循环重构为迭代器流水线
- 02为类型选择转换 Trait
- 03用派生实现简化样板代码
- 04检查 Rust 命名与代码风格
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ rewrite-rs/ skills --skill idiomatic-rust安装后在 Claude Code 中输入/idiomatic-rust确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
BOILERPLATE.mdNAMING.mdagents/openai.yaml详细介绍
地道的 Rust
这项技能关注的是表达方式:Rust 读者期待看到什么形式?
地道 Rust 的风格
优先使用表达式而非语句:返回值的 match 胜过在每个分支中都赋值给 let mut;能直接得出结果的链式操作胜过在循环中设置标志变量、再于循环后检查——让类型系统维护不变量,而不是依赖运行时检查。let ... else 能让提前返回保持在顶部,避免在嵌套的 if let 下方把正常路径越推越靠右;matches! 是用于模式的布尔测试。如果工具链支持,if-let 链可以组合条件——这需要较新的 edition,因此请先检查仓库的 MSRV。
迭代器
优先考虑迭代器流水线,而不是索引循环——for i in 0..v.len() { ... v[i] ... } 几乎总有对应的迭代器写法,而且迭代器版本会在边界不正确时编译失败,而不是在运行时 panic。直接收集为所需类型,而不是先收集到 Vec 再转换:
// 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<_>, _>——最后一种正是 ? 与迭代组合的方式。
use std::num::ParseIntError;
fn parse_all(lines: &[&str]) -> Result<Vec<i64>, ParseIntError> {
lines.iter().map(|line| line.parse::<i64>()).collect::<Result<Vec<_>, _>>()
}
如果循环体有实际副作用,或存在组合器会掩盖的提前退出,就优先使用普通 for 循环——清晰胜过对迭代器的执着。
类型转换
当一种类型可以由另一种类型构造时,实现 From——通过通用 impl 会自动获得 Into,因此不要手动实现两个方向。当转换可能失败时,实现 TryFrom,而不是实现 From 后再 panic 或返回哨兵值。
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)
}
}
优先派生,而非手写 impl
手写之前先考虑 #[derive(Debug, Clone, PartialEq, Default, Hash)]——派生的 impl 根据实际字段生成,因此会随着结构体的变化保持同步。只有派生实现不正确时,才手动编写:
Default必须保留字段逐个取默认值会破坏的不变量(例如,Percentage的总和必须为 100,而不是各字段都为零)。Debug必须隐藏敏感字段(如 API 密钥、密码),而不是将其打印出来。PartialEq中,逻辑上相等的值可能在不应参与比较的字段上有所不同(如缓存的哈希值、时间戳)。
对于枚举,使用 #[derive(Default)] 并在预期变体上标注 #[default],遵循的也是同一原则——本目录中的 BOILERPLATE.md 提供了这两种写法。
Newtype
Newtype 不产生运行时开销,还能让参数顺序错误的调用变成类型错误。只要裸 u64、String 或 f64 代表某个领域概念(如 ID、货币金额),而不只是一个数字,就应考虑使用 newtype。
命名
名称中的每个词都应有意义:用 Bookings,而不是 BookingService——Service、Manager、Helper、Util、Data 和 Info 都表明名称还没斟酌完成。as_/to_/into_ 前缀表示有成本——免费视图、分配、消费。字段访问器不使用 get_;缩写按普通单词的方式书写(HttpClient、Uuid);不需要 Self 的辅助函数应是自由函数,而不是把 impl 块当作命名空间。分层表和示例对照见 NAMING.md。
实际上反映设计问题的样板代码
并非所有重复代码都能通过 derive 消除——五个几乎相同的 match 分支,或一个有十个转发方法的包装类型,都可能表明缺少抽象。有关这些示例对照(包括 Deref 陷阱),请参阅 BOILERPLATE.md。
延后处理
这项技能规范的是表达方式,而非决策:clone 还是 borrow 的选择见 /ownership-not-clone;错误类型的形式见 /rust-errors;如何让非法状态无法表示见 /type-driven-design。
验证
行为不变,形式不同——运行标准三项检查:
cargo clippy --all-targets # add -- -D warnings only if the repo has no lint config
cargo fmt --check
cargo test
按照仓库配置的 lint 级别运行这三项检查;除非仓库启用了 clippy::pedantic,否则应将其发现视为建议,而非违规。