minigrep:重构出一个合格的命令行程序 — 第 12 章拆解
minigrep:重构出一个合格的命令行程序
这一章做什么: 写一个 mini 版 grep——
minigrep 关键词 文件名,输出文件里含关键词的行。 真正的课程是过程:同样一个程序被重构四遍,从 40 行的 main 一路拆成 "薄 main + 库逻辑 + 可测试函数"的结构。读完你要能说出 Rust 二进制项目的标准分工: main 只负责启动和报错,逻辑住在 lib.rs。
1. 顶层全景:四遍重构的地图
v1 main 一把梭:读参数 → 读文件 → 打印 → 问题:main 身兼数职,无法测试
v2 抽出 Config::new,配置聚成结构体 → 问题:参数不够时裸奔 index out of bounds
v3 Config::build 返回 Result;run 返回 Result → main 用 unwrap_or_else / if let 统一报错
v4 逻辑搬入 lib.rs(search 函数),TDD 补功能 → 搜索可测、可复用;再加环境变量与 stderr
图说:每一遍都不加新功能,只改结构。这是本章的潜台词:重构先于功能。2. 第一遍:让程序跑起来
读参数:std::env::args() 返回命令行参数的迭代器,.collect() 收成 Vec<String>(collect 的目标类型推不出来,要标注)。向量第 0 个是程序自己的名字(沿用 C 的惯例),所以用户参数从 args[1]、args[2] 取1。一个坑书上点了名:args 遇到无效 Unicode 会 panic,要宽容就用 args_os(返回 OsString,跨平台更复杂)2。
读文件:fs::read_to_string(file_path) 返回 io::Result<String>,先 .expect(...) 顶着。跑通:cargo run -- the poem.txt 能打印狄金森的诗3。
v1 的四宗罪(书列得很清楚):main 干两件事;配置变量散在局部;expect 的错误信息不区分失败原因;参数缺失时抛的是"给程序员看的"越界错误4。
3. 第二、三遍:抽出 Config 与 run,错误改走 Result
分离模式(main.rs/lib.rs 分工),书里给了明确的清单——main 只剩四件事:调参数解析、做配置、调 lib.rs 的 run、处理 run 的错误。理由:main 没法直接测试,把逻辑搬出去才能测;留下的 main 小到"读一遍就知道对"5。
Config 的进化链,三步各解决一个问题:
parse_config(&args) -> (&str, &str):参数解析离开 main。返回元组,但调用处立刻拆包——书说,"刚造出来就拆开"是抽象没到位的信号6。Config { query, file_path }结构体:两个值是"一份配置"这件事终于写进了类型。为了 owned 字段用了.clone()——书里的态度很务实:这里 clone 一两次完全划算,"先有一个略低效但能跑的程序,好过一上来就过度优化"7。Config::new→Config::build(返回Result<Config, &'static str>):参数不足返回Err("not enough arguments")而不是 panic。改名有讲究:程序员默认new不会失败,会失败的就别叫 new8。
main 侧的统一报错:
let config = Config::build(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}unwrap_or_else 首次登场:Ok 取值,Err 时执行你传的闭包(匿名函数,第 13 章展开)9。run 返回 Result<(), Box<dyn Error>>,内部用 ? 传播;Ok(()) 是"只为副作用而调用"的惯用写法。忽略 run 的返回值,编译器警告 unused Result——第 2 章那个"必须表态"的设计再次出现10。
4. 第四遍:逻辑进 lib.rs,TDD 写 search
按第 7 章的双 crate 惯例,search 定义到 lib.rs 并标 pub;binary 里 use minigrep::search;。search 的签名是生命周期在实战里的样子:
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}返回值里的切片借自 contents,而不是 query——所以只有 contents 带 'a。书特意提醒:编译器帮忙提示会把所有参数都标上 'a,那在这里是错的,因为结果跟 query 无关11。
TDD 流程(书给出了完整四步循环):① 先写失败测试(one_result:在 "safe, fast, productive." 里搜 "duct");② 给 unimplemented!() 的实现先返回空 vec,让测试"因错误的原因失败";③ 实现到通过;④ 重构保绿12。
大小写不敏感搜索同样走 TDD:实现是 to_lowercase() 后比较(返回的仍是原始行,不是小写行——值得注意的细节);配置项 ignore_case: bool 进 Config,值来自 env::var("IGNORE_CASE").is_ok()——只关心"设没设",不关心设成什么,所以用 is_ok 而非 unwrap 系13。
5. 收尾:stderr 分流
终端有两路输出:stdout(正常结果)和 stderr(错误信息)。 分列的意义:用户可以把成功的输出重定向进文件(minigrep to poem.txt > output.txt)而不错过屏幕上的报错。println! 只写 stdout;错误要改 eprintln!。改动只有两处(都在 main 里)——这正是"错误处理集中在一处"的红利:改输出通道时不用翻遍代码14。
验证姿势很妙:cargo run > output.txt 不传参数,改之前错误信息混进文件,改之后错误上屏、文件为空15。
6. 作者的判断与证据
- "main 薄到可以目测验证、逻辑必须可测"——结构性主张,贯穿全章5。
- "clone 一点字符串换简单,划算"——务实立场,明说反对过度优化7。
- "会失败的构造函数别叫 new"——命名即契约的社区惯例8。
- "错误处理集中一处"——本章最重要的架构判断,stderr 改造是它的验证14。
判断(我们的,不是书里的): 本章其实教了一种可迁移的审查清单, 拿来 review 任何 Rust 二进制项目:main 能否一眼读完?所有错误是否汇于一处? 每个会失败的构造函数是否返回 Result?核心逻辑是否可脱离 main 测试? 输出是否分流 stdout/stderr?——这五问覆盖的正是"脚本"与"程序"的分界线。 如果错,会错在: 这套结构对小工具是负担——十个参数的一次性脚本按此拆分, 文件数翻三倍;判据是"这个程序会被维护多久、被多少人读", 本章默认答案是"足够久、足够多",这对教学项目成立,对一次性脚本不成立。
7. 边界与局限
to_lowercase的大小写转换明说"对基本 Unicode 够用,不是 100% 准确",真产品要更细致。- search 先把全部结果收进 vector 再打印——大文件上会延迟输出,书里预告第 13 章用迭代器改善。
- 参数解析手写;真实的 CLI 该用 clap 这类 crate(本书不讲)。
8. 可带走的
env::args().collect::<Vec<String>>()收参数;args[0]是程序名。- 结构体化的信号:一对值"总是一起出现、一起传"就该聚成类型。
- 会失败的构造函数命名 build,返回 Result。
- main 的四件事:解析参数、建配置、调 run、处理错误。
unwrap_or_else处理 Err 并保留 Ok 值;if let Err(e)只关心错误时更贴切。Result<(), Box<dyn Error>>+?+Ok(())是 run 函数的标准形态。- TDD 循环:失败测试 → 最简实现 → 通过 → 重构。
- 返回切片借自哪个参数,'a 就标在哪个参数上(help 提示不一定是最佳答案)。
env::var(name).is_ok():检测环境变量"是否设置"。- 错误信息用 eprintln!(stderr),正常结果用 println!(stdout)。
9. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 读命令行参数 | Chapter 12: An I/O Project | text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:73(搜「env::args」) |
| args 与无效 Unicode | 同上 | text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:88(搜「UNICODE」) |
| 读文件 | 同上 | text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:194(搜「fs」) |
| 四宗罪 | 同上 | text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:247(搜「four problems」) |
| main/lib 分工清单 | 同上 | text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:273(搜「Separating Concerns」) |
| clone 的权衡 | 同上 | text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:403(搜「OF USING CLONE」) |
| build 与 Result | 同上 | text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:512(搜「Returning a Result」) |
| unwrap_or_else / exit(1) | 同上 | text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:564(搜「unwrap_or_else」) |
| run 返回 Result | 同上 | text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:648(搜「Box<dyn Error>」) |
| search 的生命周期 | 同上 | text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:734(搜「search<'a>」) |
| TDD 循环 | 同上 | text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:794(搜「test-driven development」) |
| 大小写不敏感与环境变量 | 同上 | text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:1096(搜「to_lowercase」) · text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:1193(搜「env::var」) |
| stdout/stderr 分流 | 同上 | text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:1263(搜「Standard Error」) · text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:1308(搜「eprintln!」) |
Footnotes
-
出处:「Chapter 12: An I/O Project」第 120 段(
text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:120,搜「first value in the vector」)。 ↩ -
出处:「Chapter 12: An I/O Project」第 90 段(
text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:90,搜「invalid Unicode」)。 ↩ -
出处:「Chapter 12: An I/O Project」第 200 段(
text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:200,搜「read_to_string」)。 ↩ -
出处:「Chapter 12: An I/O Project」第 247 段(
text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:247,搜「four problems」)。 ↩ -
出处:「Chapter 12: An I/O Project」第 273 段(
text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:273,搜「Separating Concerns」)。 ↩ ↩2 -
出处:「Chapter 12: An I/O Project」第 338 段(
text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:338,搜「break that tuple」)。 ↩ -
出处:「Chapter 12: An I/O Project」第 403 段(
text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:403,搜「OF USING CLONE」)。 ↩ ↩2 -
出处:「Chapter 12: An I/O Project」第 516 段(
text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:516,搜「expect new functions to never fail」)。 ↩ ↩2 -
出处:「Chapter 12: An I/O Project」第 564 段(
text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:564,搜「unwrap_or_else」)。 ↩ -
出处:「Chapter 12: An I/O Project」第 648 段(
text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:648,搜「Box<dyn Error>」)与第 676 段(text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:676,搜「side effects」)。 ↩ -
出处:「Chapter 12: An I/O Project」第 734 段(
text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:734,搜「search<'a>」)与第 892 段(text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:892,搜「incorrect」)。 ↩ -
出处:「Chapter 12: An I/O Project」第 794 段(
text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:794,搜「test-driven development」)。 ↩ -
出处:「Chapter 12: An I/O Project」第 1096 段(
text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:1096,搜「to_lowercase」)与第 1193 段(text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:1193,搜「env::var」)。 ↩ -
出处:「Chapter 12: An I/O Project」第 1263 段(
text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:1263,搜「Standard Error」)。 ↩ ↩2 -
出处:「Chapter 12: An I/O Project」第 1308 段(
text/19-ch12-chapter-12-an-i-o-project-building-a-command-lin.txt:1308,搜「eprintln!」)。 ↩