From 8563334d1db753959b199d454679eba91ca8053c Mon Sep 17 00:00:00 2001 From: sunface Date: Fri, 24 Feb 2023 16:05:12 +0800 Subject: [PATCH 1/2] =?UTF-8?q?add=20some=20contents=20to=20=E5=85=A5?= =?UTF-8?q?=E9=97=A8=E5=AE=9E=E6=88=98=20chapter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/SUMMARY.md | 3 + src/basic-practice/envs.md | 1 + src/basic-practice/iterators.md | 5 + src/basic-practice/tests.md | 219 ++++++++++++++++++++++++++++++++ 4 files changed, 228 insertions(+) create mode 100644 src/basic-practice/envs.md create mode 100644 src/basic-practice/iterators.md create mode 100644 src/basic-practice/tests.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 09bd1f58..f5324ea7 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -74,6 +74,9 @@ - [入门实战:构建一个简单命令行程序](basic-practice/intro.md) - [基本功能](basic-practice/base-features.md) - [增加模块化和错误处理](basic-practice/refactoring.md) + - [测试驱动开发](basic-practice/tests.md) + - [使用环境变量](basic-practice/envs.md) + - [使用迭代器来改进程序(可选)](basic-practice/iterators.md) - [Rust 高级进阶](advance/intro.md) - [生命周期](advance/lifetime/intro.md) - [深入生命周期](advance/lifetime/advance.md) diff --git a/src/basic-practice/envs.md b/src/basic-practice/envs.md new file mode 100644 index 00000000..a487c815 --- /dev/null +++ b/src/basic-practice/envs.md @@ -0,0 +1 @@ +# 使用环境变量来增强程序 \ No newline at end of file diff --git a/src/basic-practice/iterators.md b/src/basic-practice/iterators.md new file mode 100644 index 00000000..442360e2 --- /dev/null +++ b/src/basic-practice/iterators.md @@ -0,0 +1,5 @@ +# 使用迭代器来改进我们的程序 + +> 本章节是可选内容,请大家在看完[迭代器章节](https://course.rs/advance/functional-programing/iterator.html)后,再来阅读 + + diff --git a/src/basic-practice/tests.md b/src/basic-practice/tests.md new file mode 100644 index 00000000..9dd1892e --- /dev/null +++ b/src/basic-practice/tests.md @@ -0,0 +1,219 @@ +# 测试驱动开发 + +> 开始之前,推荐大家先了解下[如何在 Rust 中编写测试代码](https://course.rs/test/intro.html),这块儿内容不复杂,先了解下有利于本章的继续阅读 + +在之前的章节中,我们完成了对项目结构的重构,并将进入逻辑代码编程的环节,但在此之前,我们需要先编写一些测试代码,也是最近颇为流行的测试驱动开发模式(TDD, Test Driven Development): + +1. 编写一个注定失败的测试,并且失败的原因和你指定的一样 +2. 编写一个成功的测试 +3. 编写你的逻辑代码,直到通过测试 + +这三个步骤将在我们的开发过程中不断循环,知道所有的代码都开发完成并成功通过所有测试。 + +## 注定失败的测试用例 + +既然要添加测试,那之前的 `println!` 语句将没有大的用处,毕竟 `println!` 存在的目的就是为了让我们看到结果是否正确,而现在测试用例将取而代之。 + +接下来,在 `lib.rs` 文件中,添加 `tests` 模块和 `test` 函数: + +```rust +#[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` 字符串,目测可得:其中有一行内容是包含有目标字符串的。 + +但目前为止,还无法运行该测试用例,更何况还想幸灾乐祸的看其失败,原因是 `search` 函数还没有实现!毕竟是测试驱动、测试先行。 + +```rust +// in lib.rs +pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { + vec![] +} +``` + +先添加一个简单的 `search` 函数实现,非常简单粗暴的返回一个空的数组,显而易见测试用例将成功通过,真是一个居心叵测的测试用例! + +注意这里生命周期 `'a` 的使用,之前的章节有[详细介绍](https://course.rs/basic/lifetime.html#函数签名中的生命周期标注),不太明白的同学可以回头看看。 + +喔,这么复杂的代码,都用上生命周期了!嘚瑟两下试试: + +```shell +$ cargo test + Compiling minigrep v0.1.0 (file:///projects/minigrep) + Finished test [unoptimized + debuginfo] target(s) in 0.97s + Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) + +running 1 test +test tests::one_result ... FAILED + +failures: + +---- tests::one_result stdout ---- +thread 'main' panicked at 'assertion failed: `(left == right)` + left: `["safe, fast, productive."]`, + right: `[]`', src/lib.rs:44:9 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + tests::one_result + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +error: test failed, to rerun pass `--lib` +``` + +太棒了!它失败了... + +## 务必成功的测试用例 + +接着,改进型测试驱动的第二步了:编写注定成功的测试。当然,前提条件是实现我们的 `search` 函数。它包含以下步骤: + +- 遍历迭代 `contents` 的每一行 +- 检查该行内容是否包含我们的目标字符串 +- 若包含,则放入返回值列表中,否则忽略 +- 返回匹配到的返回值列表 + +### 遍历迭代每一行 + +Rust 提供了一个很便利的 `lines` 方法将目标字符串进行按行分割: + +```rust +// in lib.rs +pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { + for line in contents.lines() { + // do something with line + } +} +``` + +这里的 `lines` 返回一个[迭代器](https://course.rs/advance/functional-programing/iterator.html),关于迭代器在后续章节会详细讲解,现在只要知道 `for` 可以遍历取出迭代器中的值即可。 + +### 在每一行中查询目标字符串 + +```rust +// in lib.rs +pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { + for line in contents.lines() { + if line.contains(query) { + // do something with line + } + } +} +``` + +与之前的 `lines` 函数类似,Rust 的字符串还提供了 `contains` 方法,用于检查 `line` 是否包含待查询的 `query`。 + +接下来,只要返回合适的值,就可以完成 `search` 函数的编写。 + + +### 存储匹配到的结果 + +简单,创建一个 `Vec` 动态数组,然后将查询到的每一个 `line` 推进数组中即可: + +```rust +// in lib.rs +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 +} +``` + +至此,`search` 函数已经完成了既定目标,为了检查功能是否正确,运行下我们之前编写的测试用例: + +```shell +$ cargo test + Compiling minigrep v0.1.0 (file:///projects/minigrep) + Finished test [unoptimized + debuginfo] target(s) in 1.22s + Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) + +running 1 test +test tests::one_result ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Doc-tests minigrep + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s +``` + +测试通过,意味着我们的代码也完美运行,接下来就是在 `run` 函数中大显身手了。 + +### 在 run 函数中调用 search 函数 + +```rust +// in src/lib.rs +pub fn run(config: Config) -> Result<(), Box> { + let contents = fs::read_to_string(config.file_path)?; + + for line in search(&config.query, &contents) { + println!("{line}"); + } + + Ok(()) +} +``` + +好,再运行下看看结果,看起来我们距离成功从未如此之近! + +```shell +$ cargo run -- frog poem.txt + Compiling minigrep v0.1.0 (file:///projects/minigrep) + Finished dev [unoptimized + debuginfo] target(s) in 0.38s + Running `target/debug/minigrep frog poem.txt` +How public, like a frog +``` + +酷!成功查询到包含 `frog` 的行,再来试试 `body` : + +```shell +$ cargo run -- body poem.txt + Compiling minigrep v0.1.0 (file:///projects/minigrep) + Finished dev [unoptimized + debuginfo] target(s) in 0.0s + Running `target/debug/minigrep body poem.txt` +I'm nobody! Who are you? +Are you nobody, too? +How dreary to be somebody! +``` + +完美,三行,一行不少,为了确保万无一失,再来试试查询一个不存在的单词: + +```shell +cargo run -- monomorphization poem.txt + Compiling minigrep v0.1.0 (file:///projects/minigrep) + Finished dev [unoptimized + debuginfo] target(s) in 0.0s + Running `target/debug/minigrep monomorphization poem.txt` +``` + +至此,章节开头的目标已经全部完成,接下来思考一个小问题:如果要为程序加上大小写不敏感的控制命令,由用户进行输入,该怎么实现比较好呢?毕竟在实际搜索查询中,同时支持大小写敏感和不敏感还是很重要的。 + +答案留待下一章节揭晓。 From 4516b37ab9d27f2db3f9bb1f0b0eed54d39f7738 Mon Sep 17 00:00:00 2001 From: sunface Date: Fri, 24 Feb 2023 16:48:12 +0800 Subject: [PATCH 2/2] =?UTF-8?q?add=20some=20contents=20to=20=E5=85=A5?= =?UTF-8?q?=E9=97=A8=E5=AE=9E=E6=88=98=20chapter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/basic-practice/envs.md | 200 ++++++++++++++++++++++++++++++++++++- 1 file changed, 199 insertions(+), 1 deletion(-) diff --git a/src/basic-practice/envs.md b/src/basic-practice/envs.md index a487c815..e34bcc99 100644 --- a/src/basic-practice/envs.md +++ b/src/basic-practice/envs.md @@ -1 +1,199 @@ -# 使用环境变量来增强程序 \ No newline at end of file +# 使用环境变量来增强程序 + +在上一章节中,留下了一个悬念,该如何实现用户控制的大小写敏感,其实答案很简单,你在其它程序中肯定也遇到过不少,例如如何控制 `panic` 后的栈展开? Rust 提供的解决方案是通过命令行参数来控制: + +```shell +RUST_BACKTRACE=1 cargo run +``` + +与之类似,我们也可以使用环境变量来控制大小写敏感,例如: + +```shell +IGNORE_CASE=1 cargo run -- to poem.txt +``` + +既然有了目标,那么一起来看看该如何实现吧。 + + +## 编写大小写不敏感的测试用例 + +还是遵循之前的规则:测试驱动,这次是对一个新的大小写不敏感函数进行测试 `search_case_insensitive`。 + +还记得 TDD 的测试步骤嘛?首先编写一个注定失败的用例: + +```rust +// in src/lib.rs +#[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) + ); + } +} +``` + +可以看到,这里新增了一个 `case_insensitive` 测试用例,并对 `search_case_insensitive` 进行了测试,结果显而易见,函数都没有实现,自然会失败。 + +接着来实现这个大小写不敏感的搜索函数: + +```rust +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 +} +``` + +跟之前一样,但是引入了一个新的方法 `to_lowercase`,它会将 `line` 转换成全小写的字符串,类似的方法在其它语言中也差不多,就不再赘述。 + +还要注意的是 `query` 现在是 `String` 类型,而不是之前的 `&str`,因为 `to_lowercase` 返回的是 `String`。 + +修改后,再来跑一次测试,看能否通过。 + +```shell +$ cargo test + Compiling minigrep v0.1.0 (file:///projects/minigrep) + Finished test [unoptimized + debuginfo] target(s) in 1.33s + Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) + +running 2 tests +test tests::case_insensitive ... ok +test tests::case_sensitive ... ok + +test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Doc-tests minigrep + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s +``` + +Ok,TDD的第二步也完成了,测试通过,接下来就是最后一步,在 `run` 中调用新的搜索函数。但是在此之前,要新增一个配置项,用于控制是否开启大小写敏感。 + +```rust +// in lib.rs +pub struct Config { + pub query: String, + pub file_path: String, + pub ignore_case: bool, +} +``` + +接下来就是检查该字段,来判断是否启动大小写敏感: + +```rust +pub fn run(config: Config) -> Result<(), Box> { + 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(()) +} +``` + +现在的问题来了,该如何控制这个配置项呢。这个就要借助于章节开头提到的环境变量,好在 Rust 的 `env` 包提供了相应的方法。 + +```rust +use std::env; +// --snip-- + +impl Config { + pub fn build(args: &[String]) -> Result { + 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(); + + Ok(Config { + query, + file_path, + ignore_case, + }) + } +} +``` + +`env::var` 没啥好说的,倒是 `is_ok` 值得说道下。该方法是 `Result` 提供的,用于检查是否有值,有就返回 `true`,没有则返回 `false`,刚好完美符合我们的使用场景,因为我们并不关心 `Ok` 中具体的值。 + +运行下试试: +```shell +$ cargo run -- to poem.txt + Compiling minigrep v0.1.0 (file:///projects/minigrep) + Finished dev [unoptimized + debuginfo] target(s) in 0.0s + Running `target/debug/minigrep to poem.txt` +Are you nobody, too? +How dreary to be somebody! +``` + +看起来没有问题,接下来测试下大小写不敏感: + +```shell +$ IGNORE_CASE=1 cargo run -- to poem.txt +``` + +```shell +Are you nobody, too? +How dreary to be somebody! +To tell your name the livelong day +To an admiring bog! +``` + +大小写不敏感后,查询到的内容明显多了很多,也很符合我们的预期。 + +最后,给大家留一个小作业:同时使用命令行参数和环境变量的方式来控制大小写不敏感,其中环境变量的优先级更高,也就是两个都设置的情况下,优先使用环境变量的设置。 + + +