趣味学RUST基础篇(构建一个命令行程序2重构)

发布于:2025-09-06 ⋅ 阅读:(16) ⋅ 点赞:(0)

太好了!我们的探测器已经能听懂指令了,知道要找“藏宝图在此”,并且知道去“地图.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!

成功了! 我们的探测器不仅能听,还能读!它把整首诗都“看”了一遍,一字不差地复述出来了!

但是…(小问题预警)

虽然现在探测器能读诗了,但它有点“懒”,把整首诗都背下来了。我们真正想要的,是它能精准地找出包含“藏宝图在此”的那一行,而不是把整个卷轴都念一遍。

而且,如果卷轴是湿的、被虫蛀了或者根本不存在,我们的探测器只会说“哎呀,打不开!”,然后直接罢工,这可不行。别担心,这些问题我们接下来就会解决!我们要让它变得更聪明、更健壮!

给你的“寻宝探测器”做个“大手术”!

我们的探测器现在能听指令、能读卷轴,看起来很厉害了。但就像一个刚组装好的机器人,它还有点“笨”:

  1. 它太“贪心”main 函数啥都干,又是听指令,又是读文件,像个“管家婆”。
  2. 它太“乱”query(找啥)和 file_path(在哪找)像两个散落的零件,没有组织。
  3. 它太“暴躁”:文件打不开?直接“死机”(panic),只会说“打不开文件”,像个没耐心的孩子。
  4. 它太“傲娇”:你少给一个参数,它就“发脾气”(panic),也不告诉你为啥。

是时候给它做个“大手术”,也就是重构代码,让它变得更聪明、更专业了!

手术第一步:给机器人“分科”(关注点分离)

医生说:“别让一个函数干所有活!我们来分工!”

  • src/main.rs:变成“总指挥室”。它只负责三件事:
    1. 听指令(收参数)。
    2. 把指令交给“专家部门”(lib.rs)。
    3. 如果专家说“搞不定”,就优雅地关机。
  • src/lib.rs:变成“专家实验室”。所有复杂的逻辑,比如解析指令、读文件、找宝藏,都放这里。这里的东西还能被“测试”,确保它靠谱!

手术第二步:把零件“装盒子”(创建 Config 结构体)

之前,queryfile_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'

用户看了只会一脸懵。现在,我们让它学会“礼貌地报告故障”:

  1. 检查故障:在 Config::new 里加个检查:
    if args.len() < 3 {
        return Err("兄弟,参数不够啊!要‘找啥’和‘在哪找’两个才行!"); // 不再 panic!
    }
    
  2. 返回“诊断报告”new 函数现在返回 Result<Config, 错误信息>,就像医生的诊断单:Ok(工具盒)Err(错误信息)
  3. “总指挥”优雅处理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 })
    }
}

网站公告

今日签到

点亮在社区的每一天
去签到