用 Claude Code 做 Rust 开发:Cargo、所有权、测试与 CLI 重构
面向初学者的 Claude Code Rust 实战:Cargo、所有权、测试、fmt、clippy、错误处理和小型 CLI。
先把任务缩小到可验证的 Rust 项目
Rust 的学习曲线主要来自所有权、借用、生命周期和 Result 错误处理。Claude Code 可以帮你读代码、改文件、运行命令,但在 Rust 项目里,最好的用法不是“把需求全部扔给 AI”,而是让它和编译器一起工作:先解释设计,再编辑小范围文件,最后用测试、格式化和 Clippy 验证。
本文用一个小型 CLI tasknote 作为练习。它读取 tasks.txt 中的 [ ] task 和 [x] task 行,输出完成数和未完成数,也可以只列出未完成任务。参考资料以官方文档为准:Rust Book 所有权章节、Cargo Book 创建项目、cargo test、rustfmt、Clippy 和 Claude Code overview。
如果你还不熟悉 Claude Code,可以先看入门指南。团队使用时,把命令、依赖策略和禁止事项写进 CLAUDE.md 最佳实践,再配合权限设置指南。
flowchart LR
Prompt["说明目标和约束"]
Cargo["用 Cargo 创建小项目"]
Compiler["阅读所有权错误"]
Tests["用测试固定行为"]
Quality["运行 fmt 和 clippy"]
Refactor["小差异安全重构"]
Prompt --> Cargo --> Compiler --> Tests --> Quality --> Refactor
用 Cargo 建立最小项目
Cargo 是 Rust 的标准项目工具,负责创建包、构建、运行、测试和管理依赖。先确定验证命令,再让 Claude Code 写代码,这样任务会更清晰。
cargo new tasknote --bin
cd tasknote
cargo run
这个命令会生成 Cargo.toml 和 src/main.rs。现代 Rust 项目可能使用 edition = "2024",所以不要让 Claude Code 复制旧文章里的 edition;让它读取当前项目的 Cargo.toml。这个例子只需要两个依赖:clap 用于命令行参数,anyhow 用于给错误增加上下文。
[package]
name = "tasknote"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1"
clap = { version = "4", features = ["derive"] }
推荐先用设计提示,而不是直接要求生成全部代码。
在这个 Cargo 项目里实现一个小型 CLI `tasknote`。
它读取 `tasks.txt`,行格式为 `[ ] task` 和 `[x] task`。
请先只给出 `Cargo.toml`、`src/lib.rs`、`src/main.rs` 的设计方案。
暂时不要编辑文件。请说明所有权、借用和错误处理的选择。
这个停顿很重要。Rust 的代码是否简单,往往取决于一开始是否分清“谁拥有数据”和“谁只是读取数据”。
让 Claude Code 解释所有权和借用
所有权可以理解为“这个值由谁负责”。借用是“临时读取或修改,但不拿走所有权”。生命周期是“这个引用能活多久”。初学时不要只背术语,要把问题改写成:这个字符串应该由谁持有?这个函数只是读取,还是要返回新数据?
在 tasknote 中,文件内容先读成 String;parse_tasks 只借用文本,所以参数是 &str;解析结果是 Vec<Task>,每个任务标题由 Task 自己持有;summarize 只读任务列表,所以参数是 &[Task]。
请解释 `parse_tasks(input: &str) -> Vec<Task>` 和 `summarize(tasks: &[Task]) -> String` 的所有权设计。
只使用这个 CLI 的例子,不要泛泛而谈。
请用初学者能懂的中文解释 `String`、`&str`、`Vec<Task>`、`&[Task]`。
不要为了消除借用错误而随意增加 `clone()`。
常见失败是把所有借用错误都用 clone() 压下去。clone() 不是禁忌,但必须有理由。如果函数只是读取数据,借用通常更准确,也更容易被后续维护者理解。
可复制运行的小型 CLI
把核心逻辑放进 src/lib.rs,这样不用启动命令行也能测试。main.rs 越薄,之后让 Claude Code 重构时差异越容易审核。
// src/lib.rs
use anyhow::{Context, Result};
use std::{fs, path::Path};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Task {
pub title: String,
pub done: bool,
}
pub fn parse_tasks(input: &str) -> Vec<Task> {
input.lines().filter_map(parse_task_line).collect()
}
fn parse_task_line(line: &str) -> Option<Task> {
let trimmed = line.trim();
if let Some(title) = trimmed
.strip_prefix("[x] ")
.or_else(|| trimmed.strip_prefix("[X] "))
{
return Some(Task {
title: title.trim().to_string(),
done: true,
});
}
if let Some(title) = trimmed.strip_prefix("[ ] ") {
return Some(Task {
title: title.trim().to_string(),
done: false,
});
}
None
}
pub fn summarize(tasks: &[Task]) -> String {
let total = tasks.len();
let done = tasks.iter().filter(|task| task.done).count();
let open = total.saturating_sub(done);
format!("{total} tasks: {done} done, {open} open")
}
pub fn read_tasks(path: impl AsRef<Path>) -> Result<Vec<Task>> {
let path = path.as_ref();
let content = fs::read_to_string(path)
.with_context(|| format!("failed to read {}", path.display()))?;
Ok(parse_tasks(&content))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_markdown_style_tasks() {
let tasks = parse_tasks("[ ] write parser\n[x] add tests\n[X] run clippy\n");
assert_eq!(
tasks,
vec![
Task {
title: "write parser".to_string(),
done: false,
},
Task {
title: "add tests".to_string(),
done: true,
},
Task {
title: "run clippy".to_string(),
done: true,
},
]
);
}
#[test]
fn ignores_unrecognized_lines() {
let tasks = parse_tasks("# Sprint notes\n- plain bullet\n[ ] keep this\n");
assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0].title, "keep this");
}
#[test]
fn summarizes_counts() {
let tasks = parse_tasks("[ ] one\n[x] two\n[ ] three\n");
assert_eq!(summarize(&tasks), "3 tasks: 1 done, 2 open");
}
}
src/main.rs 只处理参数和输出。
// src/main.rs
use anyhow::Result;
use clap::Parser;
use std::path::PathBuf;
use tasknote::{read_tasks, summarize};
#[derive(Parser, Debug)]
#[command(name = "tasknote", about = "Summarize simple task files")]
struct Cli {
#[arg(short, long, default_value = "tasks.txt")]
file: PathBuf,
#[arg(long)]
only_open: bool,
}
fn main() -> Result<()> {
let args = Cli::parse();
let tasks = read_tasks(&args.file)?;
if args.only_open {
for task in tasks.iter().filter(|task| !task.done) {
println!("- {}", task.title);
}
} else {
println!("{}", summarize(&tasks));
}
Ok(())
}
示例 tasks.txt:
[ ] write parser
[x] add unit tests
[ ] run clippy
验证命令:
cargo fmt
cargo test
cargo clippy --all-targets -- -D warnings
cargo run -- --file tasks.txt
cargo run -- --file tasks.txt --only-open
这里 main 返回 Result<()>。如果文件不存在,anyhow::Context 会显示哪个路径读取失败,而不是用 unwrap() 直接崩溃。对于 CLI,缺少输入文件是可恢复错误,应该给用户可操作的信息。
把测试、fmt、Clippy 写进任务要求
Claude Code 完成代码后,必须运行验证命令。cargo test 用于单元测试、集成测试和文档测试;cargo fmt 统一风格;cargo clippy 发现常见问题。可以这样要求:
请实现 `src/lib.rs` 和 `src/main.rs`。
编辑后运行 `cargo fmt`、`cargo test`、`cargo clippy --all-targets -- -D warnings`。
如果失败,先总结错误,再用最小差异修复。
除测试外不要使用 `unwrap()`。
Claude Code 官方文档把它描述为能读代码库、编辑文件、运行命令的 agentic coding tool。即便如此,你仍然要检查 git diff、确认触碰文件是否在范围内,并在风险较高时自己重新运行命令。
三个以上的实际用例
第一个用例是给现有 Rust CLI 增加小功能。例如添加 --json 输出时,让 Claude Code 保持 summarize 不变,把 JSON 格式化写成新函数,避免简单功能变成大重写。
第二个用例是学习所有权错误。遇到 cannot move out of 或 borrowed value does not live long enough 时,把完整错误、相关函数和你期望的数据生命周期交给 Claude Code,让它解释谁拥有值、谁借用值。
第三个用例是测试先行修 bug。先为大小写 [X]、空行、不合法行写测试,再改解析器。Rust 的类型系统很强,但业务规则仍然需要测试固定。
第四个用例是安全重构。当 main.rs 变厚时,请 Claude Code 把解析、文件 I/O、显示逻辑拆开,同时保持公开函数签名不变。
常见陷阱
不要用大量 clone() 掩盖设计问题。每次看到新 clone(),都问一句:这里真的需要拥有数据吗?
不要在生产 CLI 路径留下 unwrap()。文件、配置、网络输入都会失败,应该使用 Result 和上下文信息。
不要在没有测试的情况下重构。Claude Code 改得很快,但大差异很难审。先固定行为,再让它移动代码。
不要在共享 workspace 中随意运行全局格式化。多人或多 agent 同时工作时,明确包名、文件范围和允许的命令。
安全重构提示词
请安全重构 `tasknote` 的解析器。
约束:
- 保持 `[ ] task` 与 `[x] task` 的语义不变
- 不改变 `parse_tasks`、`summarize`、`read_tasks` 的公开签名
- 只触碰 `src/lib.rs` 和其中的测试
- 不增加没有必要的 `clone()`
- 先给出 3 行计划,等待确认后再编辑
编辑后:
- 运行 `cargo fmt`
- 运行 `cargo test`
- 运行 `cargo clippy --all-targets -- -D warnings`
- 总结差异,并说明所有权相关判断
这种写法给了 Claude Code 明确的工作边界、检查命令和报告要求。Rust 项目尤其适合这样做,因为编译器、测试和 linter 都会对低质量修改进行反馈。
变现导线与后续学习
个人练习可以继续扩展 JSON 输出、CSV 导出、目录扫描或 serde 文件格式。团队使用时,请把 Rust edition、必跑命令、unwrap() 策略、允许依赖和评审清单写进 CLAUDE.md。
ClaudeCodeLab 提供 Claude Code 提示词、设置指南、CLAUDE.md 模板和团队导入咨询。想先免费熟悉命令,可以看免费速查表;需要模板和产品化资料,可以查看产品与模板;如果要把 Rust 开发流程放进真实仓库,请看培训与咨询。后续也可以阅读用 Claude Code 做 TDD 和评审流程清单。
实测经验:先把逻辑放进 src/lib.rs,用 cargo test 固定行为,再让 Claude Code 解释所有权并编辑,得到的差异最容易审核。直接要求“一次写完整 CLI”往往会产生更多不必要的 clone() 和更大的修改范围。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
从Obsidian到CLAUDE.md的Claude Code流程:不再反复解释上下文
把 Obsidian 工作笔记整理成 CLAUDE.md 运行说明,让 Claude Code 每次都带着正确上下文开始。
Claude Code 收入 CTA 路由:从文章分流到 PDF、Gumroad 与咨询
用 Claude Code 按读者意图把文章流量分到免费 PDF、Gumroad 教材或咨询入口。
Claude Code 团队交接规则: 把审查证据、权限、回滚和收入路径一起交付
面向团队的 Claude Code 交接格式: 证据、权限、回滚、免费 PDF、Gumroad 与咨询路径都要可审查。