💻博主现有专栏:
C51单片机(STC89C516),c语言,c++,离散数学,算法设计与分析,数据结构,Python,Java基础,MySQL,linux,基于HTML5的网页设计及应用,Rust(官方文档重点总结),jQuery,前端vue.js,Javaweb开发,Python机器学习等
🥏主页链接:Y小夜-CSDN博客
目录
🎯使用TDD(测试驱动开发)开发库功能
🎃编写失败测试
🎃编写测试通过的代码
✨使用lines方法遍历每一行
✨用查询字符串搜索每一行
✨存储匹配的行
🎃在run函数中使用search函数
🎯使用环境变量
🎃编写一个大小写不敏感的search函数的失败测试
🎃实现search_case_insensitive函数
🎯将错误信息输出到标准错误而不是标准输出
🎃检查错误应该写入何处
🎃将错误打印到标准错误
🎯使用TDD(测试驱动开发)开发库功能
现在我们将逻辑提取到了 src/lib.rs 并将所有的参数解析和错误处理留在了 src/main.rs 中,为代码的核心功能编写测试将更加容易。我们可以直接使用多种参数调用函数并检查返回值而无需从命令行运行二进制文件了。
在这一部分,我们将遵循测试驱动开发(Test Driven Development, TDD)的模式来逐步增加
编写一个失败的测试,并运行它以确保它失败的原因是你所期望的。编写或修改足够的代码来使新的测试通过。重构刚刚增加或修改的代码,并确保测试仍然能通过。从步骤 1 开始重复!minigrep
的搜索逻辑。它遵循如下步骤:虽然这只是众多编写软件的方法之一,不过 TDD 有助于驱动代码的设计。在编写能使测试通过的代码之前编写测试有助于在开发过程中保持高测试覆盖率。
我们将测试驱动实现实际在文件内容中搜索查询字符串并返回匹配的行示例的功能。我们将在一个叫做
search
的函数中增加这些功能。
🎃编写失败测试
去掉 src/lib.rs 和 src/main.rs 中用于检查程序行为的
println!
语句,因为不再真正需要它们了。#[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)); }}
这里选择使用
"duct"
作为这个测试中需要搜索的字符串。用来搜索的文本有三行,其中只有一行包含"duct"
。(注意双引号之后的反斜杠,这告诉 Rust 不要在字符串字面值内容的开头加入换行符)我们断言search
函数的返回值只包含期望的那一行。pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { vec![]}
🎃编写测试通过的代码
目前测试之所以会失败是因为我们总是返回一个空的 vector。为了修复并实现
遍历内容的每一行文本。查看这一行是否包含要搜索的字符串。如果有,将这一行加入列表返回值中。如果没有,什么也不做。返回匹配到的结果列表search
,我们的程序需要遵循如下步骤:让我们一步一步的来,从遍历每行开始。
✨使用lines方法遍历每一行
Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被命名为
lines
,它如示例这样工作。注意这还不能编译:pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { for line in contents.lines() { // do something with line }}
✨用查询字符串搜索每一行
接下来将会增加检查当前行是否包含查询字符串的功能。幸运的是,字符串类型为此也有一个叫做
contains
的实用方法!pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { for line in contents.lines() { if line.contains(query) { // 对文本行进行操作 } }}
✨存储匹配的行
为了完成这个函数,我们还需要一个方法来存储包含查询字符串的行。为此可以在
for
循环之前创建一个可变的 vector 并调用push
方法在 vector 中存放一个line
。在for
循环之后,返回这个 vectorpub 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}
🎃在run函数中使用search函数
现在
search
函数是可以工作并测试通过了的,我们需要实际在run
函数中调用search
。需要将config.query
值和run
从文件中读取的contents
传递给search
函数。接着run
会打印出search
返回的每一行: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(())}
这里仍然使用了
for
循环获取了search
返回的每一行并打印出来。
🎯使用环境变量
我们将增加一个额外的功能来改进
minigrep
:用户可以通过设置环境变量来设置搜索是否是大小写敏感的。当然,我们也可以将其设计为一个命令行参数并要求用户每次需要时都加上它,不过在这里我们将使用环境变量。这允许用户设置环境变量一次之后在整个终端会话中所有的搜索都将是大小写不敏感的。
🎃编写一个大小写不敏感的search函数的失败测试
首先我们希望增加一个新函数
search_case_insensitive
,并将会在环境变量有值时调用它。这里将继续遵循 TDD 过程,其第一步是再次编写一个失败测试。我们将为新的大小写不敏感搜索函数新增一个测试函数,并将老的测试函数从one_result
改名为case_sensitive
来更清楚的表明这两个测试的区别:#[cfg(test)]mod tests { use super::*; #[test] fn case_sensitive() { let query = "duct"; let contents = "\Rust:safe, fast, productive.Pick three.Duct tape."; assert_eq!(vec!["safe, fast, productive."], search(query, contents)); } #[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) ); }}
注意我们也改变了老测试中
contents
的值。
🎃实现search_case_insensitive函数
唯一的区别是它会将
query
变量和每一line
都变为小写,这样不管输入参数是大写还是小写,在检查该行是否包含查询字符串时都会是小写。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}
首先我们将
query
字符串转换为小写,并将其覆盖到同名的变量中。对查询字符串调用to_lowercase
是必需的,这样不管用户的查询是"rust"
、"RUST"
、"Rust"
或者"rUsT"
,我们都将其当作"rust"
处理并对大小写不敏感。虽然to_lowercase
可以处理基本的 Unicode,但它不是 100% 准确。如果编写真实的程序的话,我们还需多做一些工作,不过这一部分是关于环境变量而不是 Unicode 的,所以这样就够了。注意
query
现在是一个String
而不是字符串 slice,因为调用to_lowercase
是在创建新数据,而不是引用现有数据。如果查询字符串是"rUsT"
,这个字符串 slice 并不包含可供我们使用的小写的u
或t
,所以必需分配一个包含"rust"
的新String
。现在当我们将query
作为一个参数传递给contains
方法时,需要增加一个 & 因为contains
的签名被定义为获取一个字符串 slice。
🎯将错误信息输出到标准错误而不是标准输出
目前为止,我们将所有的输出都通过
println!
写到了终端。大部分终端都提供了两种输出:标准输出(standard output,stdout
)对应一般信息,标准错误(standard error,stderr
)则用于错误信息。这种区别允许用户选择将程序正常输出定向到一个文件中并仍将错误信息打印到屏幕上。
🎃检查错误应该写入何处
首先,让我们观察一下目前
minigrep
打印的所有内容是如何被写入标准输出的,包括那些应该被写入标准错误的错误信息。可以通过将标准输出流重定向到一个文件同时有意产生一个错误来做到这一点。我们没有重定向标准错误流,所以任何发送到标准错误的内容将会继续显示在屏幕上。命令行程序被期望将错误信息发送到标准错误流,这样即便选择将标准输出流重定向到文件中时仍然能看到错误信息。目前我们的程序并不符合期望;相反我们将看到它将错误信息输出保存到了文件中!
我们通过
>
和文件路径 output.txt 来运行程序,我们期望重定向标准输出流到该文件中。在这里,我们没有传递任何参数,所以会产生一个错误:$ cargo run > output.txt
>
语法告诉 shell 将标准输出的内容写入到 output.txt 文件中而不是屏幕上。我们并没有看到期望的错误信息打印到屏幕上,所以这意味着它一定被写入了文件中。如下是 output.txt 所包含的:Problem parsing arguments: not enough arguments
是的,错误信息被打印到了标准输出中。像这样的错误信息被打印到标准错误中将会有用得多,将使得只有成功运行所产生的输出才会写入文件。我们接下来就修改。
🎃将错误打印到标准错误
得益于早些时候的重构,所有打印错误信息的代码都位于
main
一个函数中。标准库提供了eprintln!
宏来打印到标准错误流,所以将两个调用println!
打印错误信息的位置替换为eprintln!
:fn main() { let args: Vec<String> = env::args().collect(); let config = Config::build(&args).unwrap_or_else(|err| { eprintln!("Problem parsing arguments: {err}"); process::exit(1); }); if let Err(e) = minigrep::run(config) { eprintln!("Application error: {e}"); process::exit(1); }}
现在我们再次尝试用同样的方式运行程序,不使用任何参数并通过
>
重定向标准输出:$ cargo run > output.txtProblem parsing arguments: not enough arguments