太好了!我们的探测器已经能听懂指令了,知道要找“藏宝图在此”,并且知道去“地图.txt”里找。但问题来了——它还不会看书!
就像一个拿着探测器的探险家,眼睛一闭,啥也看不见。所以,第二步,我们要教它如何“翻阅卷轴”——读取文件!
找个“练习卷轴”
首先,我们得找个“练习卷轴”来教它。就用这首有趣的诗吧!在你的项目文件夹里创建一个叫 poem.txt
的文件,写上这首诗:
“I’m nobody! Who are you?”
I’m nobody! Who are you?
Are you nobody, too?
Then there’s a pair of us - don’t tell!
They’d banish us, you know.How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
(这首诗讲的是“做个无名小卒多自在”,和我们低调寻宝的主题莫名契合!)
给探测器装上“阅读模块”
现在,打开我们的“探测器设计图”(src/main.rs
),给它加点“知识”:
use std::env;
use std::fs; // 引入“阅读模块”!就像给机器人装上眼睛
然后,在它听懂指令之后,我们让它去“翻卷轴”:
// ...(前面的代码不变)...
println!("正在寻找:{}", query);
println!("在文件中:{}", file_path);
// “翻卷轴”指令!
let contents = fs::read_to_string(file_path)
.expect("哎呀,卷轴打不开!是不是被施了魔法?");
// 让它复述一遍卷轴内容,证明它真的读了!
println!("卷轴上写着:\n{}", contents);
试试“阅读”功能!
现在,运行一下探测器,假装我们要找诗里的单词“the”(虽然还没做搜索功能,先看看它会不会读):
$ cargo run -- the poem.txt
如果一切顺利,你会看到:
正在寻找:the
在文件中:poem.txt
卷轴上写着:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.
How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
成功了! 我们的探测器不仅能听,还能读!它把整首诗都“看”了一遍,一字不差地复述出来了!
但是…(小问题预警)
虽然现在探测器能读诗了,但它有点“懒”,把整首诗都背下来了。我们真正想要的,是它能精准地找出包含“藏宝图在此”的那一行,而不是把整个卷轴都念一遍。
而且,如果卷轴是湿的、被虫蛀了或者根本不存在,我们的探测器只会说“哎呀,打不开!”,然后直接罢工,这可不行。别担心,这些问题我们接下来就会解决!我们要让它变得更聪明、更健壮!
给你的“寻宝探测器”做个“大手术”!
我们的探测器现在能听指令、能读卷轴,看起来很厉害了。但就像一个刚组装好的机器人,它还有点“笨”:
- 它太“贪心”:
main
函数啥都干,又是听指令,又是读文件,像个“管家婆”。 - 它太“乱”:
query
(找啥)和file_path
(在哪找)像两个散落的零件,没有组织。 - 它太“暴躁”:文件打不开?直接“死机”(panic),只会说“打不开文件”,像个没耐心的孩子。
- 它太“傲娇”:你少给一个参数,它就“发脾气”(panic),也不告诉你为啥。
是时候给它做个“大手术”,也就是重构代码,让它变得更聪明、更专业了!
手术第一步:给机器人“分科”(关注点分离)
医生说:“别让一个函数干所有活!我们来分工!”
src/main.rs
:变成“总指挥室”。它只负责三件事:- 听指令(收参数)。
- 把指令交给“专家部门”(
lib.rs
)。 - 如果专家说“搞不定”,就优雅地关机。
src/lib.rs
:变成“专家实验室”。所有复杂的逻辑,比如解析指令、读文件、找宝藏,都放这里。这里的东西还能被“测试”,确保它靠谱!
手术第二步:把零件“装盒子”(创建 Config 结构体)
之前,query
和 file_path
就像两个散落的螺丝钉。现在,我们给它们造一个“工具盒”——Config
结构体!
struct Config {
query: String, // 要找的“关键词”
file_path: String, // 要搜的“文件”
}
现在,main
函数只要拿着这个“工具盒”(config
)就能干活,再也不用记住两个变量名了,整洁多了!
小插曲:为啥要用
.clone()
?
想象args
是一份“原始指令书”,main
是保管员。Config
想要一份副本,但不能直接拿走原件(借用规则)。最简单的方法就是复印一份(.clone()
)。虽然复印费点时间,但胜在简单直接!等以后学了“高级复印术”(生命周期),我们再优化。
手术第三步:让“专家”来解析(Config::new)
我们把解析参数的活,从 main
里拿出来,交给 Config
自己的“工厂”——new
函数。
impl Config {
fn new(args: &[String]) -> Config {
let query = args[1].clone();
let file_path = args[2].clone();
Config { query, file_path }
}
}
现在,main
只需说:“Config::new(指令书)
,给我一个工具盒!”,简单明了。
手术第四步:让它“会说话”,别“发脾气”(错误处理升级)
之前,如果参数不够,机器人就直接“死机”:
thread 'main' panicked at 'index out of bounds: the len is 1 but the index is 1'
用户看了只会一脸懵。现在,我们让它学会“礼貌地报告故障”:
- 检查故障:在
Config::new
里加个检查:if args.len() < 3 { return Err("兄弟,参数不够啊!要‘找啥’和‘在哪找’两个才行!"); // 不再 panic! }
- 返回“诊断报告”:
new
函数现在返回Result<Config, 错误信息>
,就像医生的诊断单:Ok(工具盒)
或Err(错误信息)
。 - “总指挥”优雅处理:
main
收到“诊断单”后,如果是Err
,就打印友好的提示,然后关机:let config = Config::new(&args).unwrap_or_else(|err| { println!("问题:{}", err); process::exit(1); // 用非零状态码关机,表示出错了 });
现在,如果用户输错命令:
$ cargo run
问题:兄弟,参数不够啊!要‘找啥’和‘在哪找’两个才行!
是不是瞬间感觉专业多了?
手术第五步:把“寻宝逻辑”也搬进实验室
读文件、找关键词的活,也不能让“总指挥”干。我们把它搬到 lib.rs
,变成一个叫 run
的“寻宝专家”函数:
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?; // ? 运算符,出错直接返回
println!("卷轴内容:\n{}", contents);
Ok(())
}
main
只需呼叫专家:
if let Err(e) = run(config) {
println!("寻宝失败:{}", e);
process::exit(1);
}
手术成功!
经过这一系列“升级手术”,我们的“寻宝探测器”焕然一新:
- 结构清晰:指挥室(
main.rs
)和实验室(lib.rs
)分工明确。 - 沟通顺畅:用
Result
传递“诊断报告”,不再无脑“死机”。 - 易于维护:所有核心逻辑在
lib.rs
,以后加新功能或改代码,都不会影响“总指挥”。 - 便于测试:
lib.rs
里的函数都可以写测试用例来验证!
现在,它已经不再是玩具,而是一个专业级的探险工具了!
完整代码
//main.rsrust
use std::{env, process};
use minigrep::{run, Config};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err|{
println!("问题:{}", err);
process::exit(1);
});
if let Err(e) = run(config){
println!("应用程序出错 :{e}");
process::exit(1);
}
}
//lib.rs
use std::error::Error;
use std::fs;
pub fn run(config: Config) ->Result<(), Box<dyn Error>>{
let contents = fs::read_to_string(config.file_path)?;
println!("卷轴内容:\n{contents}");
Ok(())
}
pub struct Config {
query: String,
file_path: String,
}
impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len()<3 {
panic!("兄弟,参数不够啊!要‘找啥’和‘在哪找’两个才行!");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}