关于 rust-implement
面向 Rust 开发者的分阶段实现技能,将工作分为设计、实现、简化和验证,并涵盖类型设计、错误处理、所有权、异步行为与清理。
主要功能
- 01先设计类型与函数签名
- 02明确错误处理和所有权规则
- 03规范异步操作与资源清理
- 04通过审查简化实现
- 05运行 lint 并进行手动验证
适用场景
- 01编码前规划 Rust 模块
- 02审查错误处理与异步清理
- 03按实现约定检查代码
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ pproenca/ dot-skills --skill rust-implement安装后在 Claude Code 中输入/rust-implement确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
AGENTS.mdmetadata.jsonscripts/lint.sh详细介绍
Rust 实现规范
分阶段编写代码。专家不会一蹴而就地写出完美代码,而是先设计、实现、审查,再简化。每编写一个模块,都遵循此流程。
第 1 阶段:设计类型和签名
开始编写任何实现之前,只编写类型定义和函数签名,不写函数体,也不写逻辑。
继续之前,先问自己以下问题:
- 是否有任何枚举状态会表示无效组合?如果有,请重新设计,确保无法表示无效状态。
- 所有参数是否都能清楚地表明自身含义?用枚举替换
bool参数(转换 2)。 - 每个要序列化或比较的结构体是否都使用
BTreeMap,而非HashMap(转换 3)? - 配置结构体是否都派生了
Default(转换 6)? - 调用方是否可能混淆 String 参数的顺序?使用新类型。
- 是否有结构体包含 2 个或更多
Option<T>字段,且一次只能有一种组合有效?将其转换为枚举(转换 8)。 - 是否有函数接受 4 个或更多参数?用参数结构体替换(转换 9)。
在类型正确之前,不要进入第 2 阶段。类型就是架构。
第 2 阶段:实现
补全函数体。编写每个函数时,应用以下规则:
- 每个
?都要加上.context("failed to [verb] [noun]"),无一例外(转换 1)。 - 对于库代码:错误枚举使用
thiserror。对于应用代码:使用anyhow。 - 函数有时借用、有时分配时,使用
Cow<str>(转换 7)。 - 任何返回 2 个或更多值的函数,都返回具名结构体,而不是元组(转换 5)。
- 使用穷尽匹配分支,不要使用
_ =>通配分支(转换 4)。
贯穿各层传递字段
添加需要传递到执行位置的新字段时,先追踪完整路径,并在编写代码前明确列出每个中间层。对于每一跳,说明承载该值的模块以及结构体或函数。跳过任何一层,都会导致字段在运行时悄然丢失。
精确处理 Drop/清理
- 在任何
.await之前释放锁守卫;在 await 期间持有MutexGuard会阻塞执行器。 - 在
Drop实现中显式关闭 stdin。短暂等待,然后在必要时强制终止。 - 对不得比其父进程存活更久的子进程,使用
kill_on_drop(true)。 - 按确定顺序清理:清除监听器 -> 排空任务 -> 释放资源。
防御性序列化
- SQLite 写事务使用
BEGIN IMMEDIATE,而非BEGIN(防止SQLITE_BUSY_SNAPSHOT)。 - 将锁获取串行化,而不是添加重试。
- 幂等插入使用
ON CONFLICT DO NOTHING,而非先读后写。
处理孤立事件
当收到的事件属于已不存在的实体(已终止的 Agent、已关闭的线程)时,应显式处理。绝不要悄然丢弃:记录警告并清理过期状态。孤立事件绝不能触发 panic。
所有权决策树
需要共享访问数据时,按顺序遍历此决策树:
- 可以传递引用吗? 使用
&T。零成本、零复杂度。 - 数据可能是自有的,也可能是借用的吗? 使用
Cow<'_, str>。借用时零成本。 - 同步边界之间有多个所有者吗? 使用
Arc<T>。优先使用Arc::clone(&x),而不是x.clone()。 - 有多个所有者,而且需要可变性吗? 使用
Arc<Mutex<T>>或Arc<RwLock<T>>。读取远多于写入时,选择RwLock。 - 需要即发即弃地启动任务吗? 使用带有自有数据的
move闭包。
每个 .clone() 都是一个需要决策的地方。问问自己:“这个克隆有必要吗?还是可以改为借用?”
错误处理原则
| 场景 | 工具 | 原因 |
|---|---|---|
| 应用代码(main、CLI、测试) | anyhow::Result + .context() | 错误链信息丰富,无需样板代码 |
| 库代码(供他人使用的 crate) | thiserror 枚举 | 类型明确、可匹配,调用方可以根据变体分支处理 |
错误枚举设计:面向用户的消息应告诉用户该怎么做,而不是内部出了什么问题。使用 Box<T> 封装较大的负载。在 is_retryable() 中明确分类每个变体——不要使用通配符。
异步决策
何时使用 Box::pin: 当轻量级异步包装器将大型被调用方的 future 内联到自身状态机中,导致栈压力时。封装内部调用:Box::pin(self.inner_method(args)).await。
| 需求 | 通道 | 原因 |
|---|---|---|
| 返回一个响应 | oneshot | 仅传递一个值,随后结束 |
| 事件流 | mpsc | 多个生产者,单个消费者 |
| 仅保留最新值 | watch | 接收方始终能看到最新值 |
| 广播给所有人 | broadcast | 每个接收方都会收到每条消息 |
关闭:使用 CancellationToken 实现分层关闭。切勿依赖 Drop 执行有序的异步清理。切勿跨越 .await 点持有 MutexGuard。
第 3 步:简化
审查代码时,设想自己要删除内容,而不是添加内容。
三个诊断问题:
- 是否有参数只是在调用链中传递? 删除它,改用环境访问。
- 是否有未使用的字段或函数? 删除它。
- 是否有缺乏依据的抽象?(仅使用一次的辅助函数、没有增加任何价值的包装器)将其内联。
“重写,而非改接”原则: 如果错误出在函数的内部逻辑,就用明确的检查重写函数体。不要委托给某个碰巧目前能产生正确结果的现有 API——明确实现的版本更便于审计,也能适应上游变更。
如果你添加的代码超出了任务的严格要求,那就说明有问题。删掉多余的代码。
第 4 步:验证
对代码运行随附的 lint 脚本:
bash ${SKILL_DIR}/scripts/lint.sh <your-file.rs>
修复所有 ERROR。检查每条 WARNING。然后执行手动检查清单:
[ ] Every ? has .context("failed to [verb] [noun]")
[ ] No unwrap() outside #[cfg(test)]
[ ] BTreeMap where output is serialized or compared
[ ] No bool parameters -- use enums
[ ] Match arms exhaustive -- no _ => wildcards
[ ] Config structs derive Default
[ ] No single-use helper functions -- inline if called once
[ ] Module under 500 lines (excluding tests)
[ ] No unnecessary .clone() -- prefer borrowing
[ ] Return structs, not tuples, for multi-value returns
[ ] Cow<str> where allocation is conditional
[ ] serde attrs present: rename_all, default, deny_unknown_fields as needed
[ ] No struct with 2+ Option<T> fields that should be an enum
[ ] No function with 4+ parameters (use args struct)
[ ] Lock guards dropped before any .await
[ ] Events for dead/missing entities handled explicitly
提交代码前修复所有违规项。
快速参考:9 种转换
每种转换都展示从初稿到生产级代码的精确差异。
1. 每个 ? 运算符都使用 .context()
之前:
let content = std::fs::read_to_string(path)?;
let config: Config = toml::from_str(&content)?;
之后:
let content = std::fs::read_to_string(path)
.context("failed to read config file")?;
let config: Config = toml::from_str(&content)
.context("failed to parse TOML config")?;
模式:"failed to [verb] [noun]"。上游错误会说明自身情况——你的任务是指出哪个操作失败了。
2. 用枚举代替布尔值
之前:
fn create_sandbox(network: bool, writable: bool) -> Sandbox {
之后:
fn create_sandbox(network: NetworkMode, access: AccessLevel) -> Sandbox {
foo(true, false) 毫无意义。改用枚举,让调用处写成 NetworkMode::Restricted, AccessLevel::ReadOnly。如果无法更改 API,就在不透明的字面量前添加 /*param_name*/ 注释。
3. 对序列化或比较的数据使用 BTreeMap
之前:HashMap<String, Rule> —— 之后:BTreeMap<String, Rule>
HashMap 的迭代顺序是随机的——差异会变得杂乱,快照测试也会不稳定。对于会被序列化、在测试中比较或展示给用户的数据,使用 BTreeMap。
4. 使用不带通配符的穷尽匹配
之前:
match decision {
PolicyDecision::Allow => true,
_ => false,
}
之后:
match decision {
PolicyDecision::Allow => true,
PolicyDecision::Block { .. } => false,
PolicyDecision::Rewrite { .. } => false,
PolicyDecision::Ask { .. } => false,
}
添加新变体时,编译器会标记所有需要更新的 match。通配符会掩盖这些问题。
5. 返回结构体,而非元组
之前:fn build_command() -> (Vec<String>, Vec<OwnedFd>) —— 调用方写 result.0
之后:fn build_command() -> CommandOutput —— 调用方写 output.args、output.preserved_fds
具名字段能够自我说明。任何返回 2 个或更多值的函数都应定义一个结构体。
6. 在配置结构体上派生 Default
#[derive(Default)]
pub struct ServerConfig {
pub timeout_secs: u64,
pub max_retries: u32,
pub bind_addr: String,
}
let config = ServerConfig { timeout_secs: 30, ..Default::default() };
派生 Default,让调用方只需覆盖重要的配置项。对于非零默认值,手动实现 Default。
7. 使用 Cow<str> 实现有条件的所有权
之前:
fn normalize_path(input: &str) -> String {
if needs_normalization(input) { input.replace('\\', "/") }
else { input.to_string() } // allocates even when unchanged
}
之后:
fn normalize_path(input: &str) -> Cow<'_, str> {
if needs_normalization(input) { Cow::Owned(input.replace('\\', "/")) }
else { Cow::Borrowed(input) } // zero-cost when unchanged
}
Cow<str> 可避免无条件分配,仅在某些路径需要分配时才进行分配。
8. 将枚举用作互斥联合类型
修改前:
struct Permissions {
profile_name: Option<String>, // only for named profiles
sandbox_policy: Option<SandboxPolicy>, // only for legacy
file_paths: Option<Vec<PathBuf>>, // only when sandbox_policy is set
}
修改后:
enum Permissions {
Named { profile_name: String },
Legacy { sandbox_policy: SandboxPolicy, file_paths: Vec<PathBuf> },
}
当结构体包含仅在特定组合下有效的 Option<T> 字段时,类型层面便允许出现无效状态。请改用枚举,让每个变体只携带相关数据。诊断方式:如果看到 2 个或更多带有 is_some()/is_none() 守卫的 Option<T> 字段,就应该改用枚举。
9. 用结构体封装长参数列表
修改前:
fn spawn_process(
cmd: &str,
args: &[String],
env: &BTreeMap<String, String>,
cwd: &Path,
stdin_policy: StdinPolicy,
sandbox: SandboxPolicy,
timeout: Duration,
) -> Result<Child> {
修改后:
struct SpawnArgs<'a> {
cmd: &'a str,
args: &'a [String],
env: &'a BTreeMap<String, String>,
cwd: &'a Path,
stdin_policy: StdinPolicy,
sandbox: SandboxPolicy,
timeout: Duration,
}
fn spawn_process(args: &SpawnArgs<'_>) -> Result<Child> {
参数达到 4 个或更多时,调用点会变得难以阅读,也容易弄错参数顺序。使用参数结构体可以在调用点明确标出每个字段,并让今后添加参数时不破坏现有调用。