当前位置: 首页 > news >正文

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

程序员的“闯关秘籍”:测试驱动开发(TDD)

目前,我们已经开发了“迷你 grep”(一个文本搜索小工具)的程序。但你不想写完代码才发现 bug 满天飞,于是你决定用一种“超前预判”的武功——测试驱动开发(TDD)

TDD 的口诀是:

**先写“失败”的测试,再写代码让它“复活”!**就像先画好靶子,再射箭,确保每一箭都命中红心。


第一关:画靶子(写一个失败的测试)

你先不急着写搜索功能,而是先写一个“测试关卡”

“如果我搜索关键词 'duct',在下面这段文字里……”

// src/lib.rs
....
#[cfg(test)]
mod tests {use super::*;#[test]fn one_result() {let query = "duct";let contents = "\
Rust:
safe, fast, productive.
Pick three.";assert_eq!(vec!["safe, fast, productive."], search(query, contents));}
}

“你必须只返回这一行:safe, fast, productive.

你信心满满地写下这个测试,然后……编译失败了!

为啥?因为 search 这个函数还不存在!你只画了靶子,还没造弓箭呢。

第二关:造一把“空弓”(让代码先能编译)

为了让程序能编译通过,你先造一把“空弓”——一个什么也不干的 search 函数:

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {vec![] //返回一个空列表
}

现在程序能编译了!但测试还是失败,因为返回的是空列表,而不是那句“safe, fast, productive.”。

靶子有了,弓也有了,下一步:射箭!

第三关:射出第一支箭(让测试通过)

你开始写真正的搜索逻辑,分三步走:

  1. 一行一行读:用 .lines() 把文本拆成一行行。
  2. 逐行检查:用 .contains(query) 看这行有没有关键词。
  3. 命中就存:如果有,就把它加入结果列表。

最终代码长这样:

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
}

再次运行测试……叮!通关成功!

running 1 test
test tests::one_result ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sRunning unittests src/main.rs (target/debug/deps/minigrep-a9fd6e1032830f98)running 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests minigreprunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

测试通过了!你搜索 "duct",真的只返回了那一行。

第四关:优化装备(重构)

现在你已经通关了,但你知道——高手不会满足于“能用”的代码。你心想:“这段代码能不能更酷一点?比如用更高级的‘迭代器魔法’?”

于是你开始重构——不改变功能,只让代码更优雅、更高效。

最后:把武器装上(集成到主程序)

现在 search 函数已经通过测试,可以投入使用了!你回到主程序 run 函数调用它,完整代码如下

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {let contents = fs::read_to_string(config.file_path)?;//重点是这行    for line in search(&config.query, &contents) {println!("{line}");}Ok(())
}

然后你开始测试真实场景:

(base) kunliu@MacBook-Pro-4 minigrep % cargo run -- frog poem.txtCompiling minigrep v0.1.0 (/Users/kunliu/project/rust-project/minigrep)Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.22sRunning `target/debug/minigrep frog poem.txt`
How public, like a frog
  • "frog" → 返回 1 行
  • "body" → 返回多行
  • "monomorphization"(根本不存在的词)→ 一行都不返回

完美!你的“迷你 grep”大功告成!

TDD 的三大好处

  1. 目标明确:先写测试,就像先画靶子,避免盲目开发。
  2. 安全感强:每加一行代码,都有测试帮你“兜底”。
  3. 代码更优:先实现功能,再重构优化,步步为营。

给你的搜索工具加个“魔法开关”:环境变量登场!

你已经做出了一个能搜索文本的“迷你 grep”小工具,但它现在有个小问题:它太“死板”了!比如你搜 "rust",它就只认小写的 rust,像 "Rust""rUsT" 这种带大写字母的,它就装作没看见。你心想:“要是能有个开关,让我一键开启‘不区分大小写’模式,那该多酷!”但你不想每次搜索都打一堆参数,比如 --ignore-case,那太麻烦了!

于是你决定用一个更高级的技巧——环境变量(Environment Variable),给你的程序加一个“永久魔法开关”!

第一步:先画个“失败的蓝图”(写测试)

你还是老规矩,先写测试,走 TDD 路线。你新增一个测试,名字叫:“不区分大小写的搜索”

你设定场景:

“我要搜 'rUsT',但在‘不区分大小写’模式下,下面这些句子都应该被找到:”

  • "Rust:"
  • "Trust me."
  • "rUsT is awesome!"
#[test]fn case_insensitive() {let query = "rUsT";let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";assert_eq!(vec!["Rust:", "Trust me."],search_case_insensitive(query, contents));}

然后你运行测试……结果当然是失败的,因为 search_case_insensitive 这个函数还不存在!

第二步:施个“变小写”魔法(实现功能)

你开始写真正的“不区分大小写”搜索函数。

它的原理很简单:把所有东西都变成小写,再比较!

就像你戴上一副“小写眼镜”,不管别人写的是大写还是小写,在你眼里都是小写。

代码长这样:

pub fn search_case_insensitive<'a>(query: &str,contents: &'a str,
) -> Vec<&'a str> {let query = query.to_lowercase();let mut results = Vec::new();for line in contents.lines() {if line.to_lowercase().contains(&query) {results.push(line);}}results
}

**运行测试……叮!魔法生效!**测试通过了!无论是 "Rust" 还是 "rUsT",都能被搜到!

第三步:加个“开关”(集成到主程序)

现在魔法有了,但怎么让用户“打开”它呢?

你决定:用环境变量 IGNORE_CASE 来控制!

  • 如果用户设置了 IGNORE_CASE=1,就开启“不区分大小写”模式。
  • 如果没设置,就默认“区分大小写”。
怎么实现?
  1. 加个配置项:在程序的配置里加一个布尔值 ignore_case
  2. 读取环境变量:用 Rust 的 env::var("IGNORE_CASE") 去检查这个变量有没有被设置。
  3. 自动切换:如果设置了,就调用 search_case_insensitive;否则用原来的 search
pub struct Config {query: String,file_path: String,pub ignore_case: bool, // 添加大小写判断
}pub fn run(config: Config) -> Result<(), Box<dyn Error>> {let contents = fs::read_to_string(config.file_path)?;let results = if config.ignore_case { //加判断search_case_insensitive(&config.query, &contents)} else {search(&config.query, &contents)};for line in results {println!("{line}");}Ok(())
}
use std::env;use std::env;
// --snip--impl Config {pub fn new(args: &[String]) -> Result<Config, &'static str> {if args.len() < 3 {return Err("not enough arguments");}let query = args[1].clone();let file_path = args[2].clone();let ignore_case = env::var("IGNORE_CASE").is_ok(); //设置了就返回trueOk(Config {query,file_path,ignore_case,})}
}

is_ok() 是关键!我们不关心值是什么(1trueyes 都行),只关心“有没有设置”。


试试魔法开关!

先试试默认模式(区分大小写):

cargo run -- to poem.txt

只会找到小写的 to

再试试开启魔法

IGNORE_CASE=1 cargo run -- to poem.txt

结果炸了!所有包含 to 的句子,不管大小写,全都被搜出来了:

(base) kunliu@MacBook-Pro-4 minigrep % IGNORE_CASE=1 cargo run -- to poem.txtFinished `dev` profile [unoptimized + debuginfo] target(s) in 0.01sRunning `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!

太棒了!你的程序现在会“看场合”了!

为什么用环境变量?不直接用命令行参数?

  • 命令行参数:适合每次都可能不同的选项(比如搜什么词)。
  • 环境变量:适合一次性设置,长期生效的偏好(比如“我永远不想区分大小写”)。

就像你设置手机的“暗黑模式”,不用每次打开 App 都点一次。

PowerShell 用户注意!

如果你用的是 Windows 的 PowerShell,设置环境变量的命令稍微不一样:

$Env:IGNORE_CASE=1; cargo run -- to poem.txt

想关掉它?运行:

Remove-Item Env:IGNORE_CASE

程序员的“广播站”:stdout vs stderr

想象你的程序是一个广播电台,它有两种广播频道:

  • 频道 A:标准输出(stdout)
    播放“正常新闻”——比如搜索结果、计算答案、程序成功运行的消息。

  • 频道 B:标准错误(stderr)
    播放“紧急插播”——比如“参数错误!”、“文件找不到!”这类出问题的警报。

目前,你的“迷你 grep”程序有个大问题:它把所有内容,不管是新闻还是警报,全都塞进了频道 A!

这就出问题了!

问题来了:警报被“静音”了!

假设你想把搜索结果保存到一个文件里,比如:

cargo run -- to poem.txt > results.txt

这里的 > 就像是说:“把频道 A 的内容录下来,存到 results.txt 里。

但问题是——你的程序把错误信息也播在频道 A,所以:

如果你输错了参数,比如忘了写文件名……
结果是:错误信息也被录进了 results.txt
而你在屏幕上啥也看不到,还以为程序“卡死了”或“没反应”。

这就像:

火灾警报响了,但广播员却对着录音机说:“着火了!着火了!”
而你正在录音室外面,根本听不到!

修复方案:把警报移到“紧急频道”

Rust 给我们提供了两个“麦克风”:

  • println! → 对着 频道 A(stdout) 说话(正常输出)
  • eprintln! → 对着 频道 B(stderr) 说话(错误信息)

所以,我们只需要把程序里所有“报错”的地方,从 println! 换成 eprintln! 就行了!

比如原来这样:

println!("Problem parsing arguments: not enough arguments");

改成:

eprintln!("Problem parsing arguments: not enough arguments");
//下面是 完整代码
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|{eprintln!("问题:{}", err);process::exit(1);});if let Err(e) = run(config){eprintln!("应用程序出错 :{e}");process::exit(1);}}

测试一下:电台升级成功!

现在我们再试一次:

cargo run > output.txt

结果是:

  • 屏幕上立刻显示:Problem parsing arguments: not enough arguments
    (因为这是 stderr,直接播出来,你立刻就知道出错了!)
  • output.txt 里是空的!(因为 stdout 没有内容,没录到任何东西)

再试一次正确的命令:

cargo run -- to poem.txt > output.txt
  • 屏幕上啥也没有(正常,因为没错误)

  • output.txt 里存下了所有匹配的行:

    Are you nobody, too?
    How dreary to be somebody!

这才是一个合格的命令行程序!

为什么这么重要?

因为用户经常这样做:

我的程序 | grep "关键词" > 结果.txt

如果错误信息混在正常输出里,就会被 grep 处理,甚至被存进文件,导致结果混乱。而用 stderr,错误信息会“绕开”管道和重定向,直接蹦到用户眼前,提醒他们:“嘿!出问题了!”


println!println! 的区别

输出到用途是否会被 > 重定向
println!stdout正常输出会被重定向
eprintln!stderr错误信息不会被重定向

总结:你的程序现在是个“专业电台”了!

  • 正常结果 → 走 stdout → 可以被保存、被处理。
  • 错误信息 → 走 stderr → 直接弹出,不会被“淹没”。

这就像:

一个好的服务员,不会把账单和垃圾一起扔进垃圾桶。
同样,一个好的程序,也不会把错误信息和正常输出混在一起。


文章转载自:

http://5kvoxKGT.hmqmm.cn
http://dgH4oxzE.hmqmm.cn
http://NdmD3RHv.hmqmm.cn
http://rdVfeA0I.hmqmm.cn
http://GDIaQsjM.hmqmm.cn
http://EUgvzQjN.hmqmm.cn
http://oKagOsC8.hmqmm.cn
http://MifOWSL0.hmqmm.cn
http://IRRa17IV.hmqmm.cn
http://809wC8fV.hmqmm.cn
http://SNAinFkO.hmqmm.cn
http://hFxkXEzJ.hmqmm.cn
http://AVY7ZxE2.hmqmm.cn
http://48v0d7IN.hmqmm.cn
http://t5xFHrnu.hmqmm.cn
http://B9X6CjhC.hmqmm.cn
http://atJG4yVM.hmqmm.cn
http://tsqo5XjK.hmqmm.cn
http://qYM8wAJN.hmqmm.cn
http://pHzSpkAD.hmqmm.cn
http://SK2OSgpt.hmqmm.cn
http://WMRAPa9F.hmqmm.cn
http://GVsjGKYJ.hmqmm.cn
http://iSwsHDA2.hmqmm.cn
http://KxpZz1SL.hmqmm.cn
http://0LP9NkGC.hmqmm.cn
http://pKCwj2xq.hmqmm.cn
http://PqSglbUk.hmqmm.cn
http://r8egvuj8.hmqmm.cn
http://pBEn549O.hmqmm.cn
http://www.dtcms.com/a/373184.html

相关文章:

  • 基于STM32的智能宠物看护系统设计与实现
  • 基于SpringBoot的家政保洁预约系统【计算机毕业设计选题 计算机毕业设计项目 计算机毕业论文题目推荐】
  • 幂等性、顺序性保障以及消息积压
  • 第一次使用coze工作流,生成简易行业报告
  • tl;dv:让你的会议更高效
  • 【入门级-算法-6、排序算法: 插入排序】
  • 健康度——设备健康续航条
  • 深入理解Spring Boot的EnvironmentPostProcessor:环境处理的黑科技
  • 面向生产环境的大模型应用开发
  • elastic search 是如何做sum操作的
  • HashMap高频面试题目
  • 李沐深度学习论文精读(二)Transformer + GAN
  • 达梦数据库(DM8)单机数据库安装部署
  • 《sklearn机器学习——特征提取》
  • OnlyOffice的高可用方案如何做
  • 苍穹外卖前端Day1 | vue基础、Axios、路由vue-router、状态管理vuex、TypeScript
  • 【RabbitMQ】----RabbitMQ 的7种工作模式
  • CN2 GIA线路深度解析:阿里云/腾讯云选哪个?(附三网评测)
  • 冰火岛 Tech 传:Apple Foundation Models 心法解密(下集)
  • Gamma AI:高效制作PPT的智能生成工具
  • 云计算学习笔记——HTTP服务、NFS服务篇
  • unity入门:按钮控制横向滚动视窗显示最左最右
  • 大模型为什么会有幻觉?-Why Language Models Hallucinate
  • 数据结构造神计划第三天---数据类型
  • MYSQL集群高可用架构之MHA高可用架构
  • 小麦矩阵系统:让短视频分发实现抖音快手小红书全覆盖
  • 智能高低压地埋线走向探测器如何在多条电缆中查找特定电缆?
  • 【Docker】常见操作
  • Python/JS/Go/Java同步学习(第七篇)四语言“字符串类型验证“对照表: 运维“雏田“白眼审核凭证上传崩溃(附源码/截图/参数表/避坑指南)
  • 深入解析网通核心器件:光模块、巴伦(Balun)与LTCC及其关键参数