From 338c63c5e04010c1fdd57db9ed0020a39c6dc278 Mon Sep 17 00:00:00 2001 From: Hector PENG Date: Mon, 9 Jun 2025 16:30:16 +0800 Subject: [PATCH] Updated 'src/io_project/test_driven_dev.md'. --- projects/io_project/Cargo.toml | 6 + projects/io_project/src/lib.rs | 38 ++++++ projects/io_project/src/main.rs | 3 + src/io_project/test_driven_dev.md | 141 ++++++++++++++--------- src/programming_concepts/control_flow.md | 2 +- 5 files changed, 133 insertions(+), 57 deletions(-) create mode 100644 projects/io_project/Cargo.toml create mode 100644 projects/io_project/src/lib.rs create mode 100644 projects/io_project/src/main.rs diff --git a/projects/io_project/Cargo.toml b/projects/io_project/Cargo.toml new file mode 100644 index 0000000..3c4452a --- /dev/null +++ b/projects/io_project/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "io_project" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/projects/io_project/src/lib.rs b/projects/io_project/src/lib.rs new file mode 100644 index 0000000..3275f6d --- /dev/null +++ b/projects/io_project/src/lib.rs @@ -0,0 +1,38 @@ +#[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)); + } +} + + +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 +} + +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(()) +} diff --git a/projects/io_project/src/main.rs b/projects/io_project/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/projects/io_project/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/src/io_project/test_driven_dev.md b/src/io_project/test_driven_dev.md index ea17384..f5a94f9 100644 --- a/src/io_project/test_driven_dev.md +++ b/src/io_project/test_driven_dev.md @@ -1,28 +1,31 @@ -# 以测试驱动的方法,开发这个库的功能 +# 以测试驱动的开发方式,开发库的功能 **Developing the Library's Functionality with Test Driven Development** -既然已将业务逻辑提取到了 `src/lib.rs` 中,而将参数收集与错误处理留在 `src/main.rs` 中,那么编写这里代码核心功能的测试,就容易多了。这里可直接以不同参数来调用那些函数,并在不必从命令行调用这里二进制程序之下,对他们的返回值加以检查。 +现在,我们已将业务逻辑提取到了 `src/lib.rs` 中,并将参数收集和错误处理,留在了 `src/main.rs` 中,这样就更容易为咱们代码的核心功能编写测试。无需在命令行调用我们的二进制文件下,我们即可以不同参数直接调用函数,并检查返回值。 -在本小节中,这里将按照以下步骤,运用测试驱动开发流程(the test-driven development(TDD) process),把搜索逻辑添加到这个 `minigrep` 程序: -1. 编写一个会失败的测试并加以运行,从而确保其会以所设想的原因失败; -2. 编写或修改仅足够的代码,来令到新的测试通过; -3. 对刚添加或修改过的代码加以重构,并确保那些测试继续通过; +在本节中,我们将使用测试驱动的开发,the test-driven development, TDD,流程,将检索逻辑添加到这个 `minigrep` 程序,步骤如下: + +1. 编写一个会失败的测试,并运行他以确保他会以咱们预期的原因失败; +2. 编写或修改足够的代码,使新测试通过; +3. 重构咱们刚刚添加或修改的代码,并确保测试继续通过; 4. 重复步骤 `1` 开始的上述步骤。 -尽管这只是众多编写软件方式之一,TDD 是可以推动代码设计的。在编写令到测试通过的代码之前就编写测试,有助于维持贯穿整个开发过程中,较高程度的测试覆盖面。 -这里将以测试驱动具体完成搜索出文件内容中查询字符串,以及产生出与该查询匹配的行清单两个功能的实现。这里将把此功能,添加在一个叫做 `search` 的函数里。 +尽管测试驱动的开发只是编写软件的众多方法之一,但他有助于推动代码设计。在编写令到测试通过的代码之前先编写测试,有助于在整个过程中,保持较高的测试覆盖率。 + +我们将以测试驱动,将具体完成对文件内容中查询字符串进行检索,并生成与查询匹配行清单的功能实现。我们将把此功能添加到一个名为 `search` 的函数中。 -## 编写一个失效测试 +## 编写一个失败测试 **Writing a Failing Test** -由于不再需要 `src/lib.rs` 与 `src/main.rs` 中的那些,曾用于对该程序行为加以检查的 `println!` 语句,因此这里就要将其移出掉。随后,就要在 `src/lib.rs` 中,添加带有一个测试函数的 `tests` 模组,就跟曾在 [第 11 章](Ch11_Writing_Automated_Tests.md#测试函数剖析) 曾做过的那样。该测试函数指明了这里所打算的这个 `search` 函数要有的行为:他将取得一个查询字串,与要搜索的文本,同时他将只返回搜索文本中,包含了查询字串的那些行。下面清单 12-15 给出了这个测试,该清单尚不会编译。 +因为我们不再需要 `src/lib.rs` 及 `src/main.rs` 中,那些咱们曾用于检查程序行为的 `println!` 语句,咱们来移除掉他们。然后,在 `src/lib.rs` 中,我们将添加带有一个测试函数的 `tests` 模组,就像我们在第 11 章中所做的那样。该测试函数指定了我们希望这个 `search` 函数所具备的行为:他将取一个查询及要检索的文本,并仅返回文本中包含该查询的行。下面清单 12-15 展示了这个测试,目前他还不会编译。 + 文件名:`src/lib.rs` @@ -35,20 +38,22 @@ mod tests { fn one_result() { let query = "duct"; let contents = "\ - Rust: - safe, fast, productive. - Pick three."; +Rust: +safe, fast, productive. +Pick three."; assert_eq! (vec! ["safe, fast, productive."], search(query, contents)); } } ``` -*清单 12-15:创建出一个这里所期望有的那个 `search` 函数的失败测试* +*清单 12-15:为我们希望有的 `search` 函数创建一个失败测试* -这个测试搜索的是字符串 `"duct"`。而这里正搜索的文本是三个行,三个行中只有一行包含了 `"duct"`(请注意那第一个双引号之后的反斜杠`\`,是告诉 Rust 不要把另起一行字符,放在这个字符串字面值内容的开头)。这里就那个 `search` 函数的返回值,包含了这里所预计的那唯一行进行了断言。 -由于这个测试现在甚至不会编译,因此这里尚不能运行这个测试而看到其失败:那个 `search` 函数还不存在!按照 TDD 的各项原则,这里将通过只添加这个 `search` 函数的始终返回某个空矢量值定义,而足够令到这个测试编译并运行的一些代码,如下清单 12-16 中所示。随后该测试将编译,并由于空矢量值不与包含了行 `"safe, fast, productive."` 的矢量匹配而失败。 +这个测试会检索字符串 `"duct"`。我们要检索的文本共有三行,其中仅有一行包含 `"duct"`(请注意,第一个双引号后的反斜杠,告诉 Rust 不要在这个字符串字面值内容的开头添加换行符)。我们断言了这个 `search` 函数所返回的值,仅包含我们预期的那个行。 + +我们目前还无法运行这个测试并观察其失败,因为该测试甚至不会编译:`search` 函数还不存在!遵循 TDD 原则,通过添加始终返回一个空矢量值的 `search` 函数定义,我们将只添加足够使该测试编译并运行的代码,如下清单 12-16 所示。然后这个测试应编译并失败,因为空的矢量值与包含 `"safe, fast, productive"` 这个行的矢量不匹配。 + 文件名:`src/lib.rs` @@ -58,17 +63,17 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str>{ } ``` -*清单 12-16:定义出一个刚好让这里测试编译的那个 `search` 函数来* +*清单 12-16:仅定义 `search` 函数的必要部分,以便我们的测试将会编译* -请注意这里需要在 `search` 的函数签名中,定义一个显式的生命周期 `'a`,并在 `contents` 参数与返回值上,使用那个生命周期。回顾 [第 10 章](Ch10_Generic_Types_Traits_and_Lifetimes.md#使用生命周期对引用加以验证) 中讲到,这些生命周期参数指明了哪个参数生命周期,是与返回值生命周期联系起来的。在这个示例中,这就表示那个返回的矢量,应包含引用了参数 `contents` (而非参数 `query`)的一些切片的字符串切片。 +请注意,我们需要在 `search` 的签名中,定义一个显式的生命周期 `'a`,并对 `contents` 参数及返回值,使用该生命周期。回顾 [第 10 章](..generic_types_traits_and_lifetimes/lifetimes.md),生命周期参数会指定出哪个参数的生命周期,与返回值的生命周期有联系。在这个示例中,我们表明了返回的矢量值,应包含引用了 `contents` 参数切片(而非 `query` 参数的切片)的字符串切片。 -也就是说,这里告诉 Rust,由 `search` 函数返回的数据,将存活到与传递给那个 `search` 函数的、在 `contents` 参数中数据同样长时间。这是相当重要的!*为* 某个切片所引用的数据,需要在该引用有效期间保持有效;若编译器假定这里是在构造 `query` 而非 `contents` 的字符串切片,那么他就会执行错误地安全性检查。 +换句话说,我们告诉 Rust,由 `search` 函数返回的数据,将与 `contents` 参数中传递给 `search` 函数的数据,具有相同生命周期。这一点非常重要!某个切片所引用的数据必须有效,这个引用才会有效;若编译器假定我们构造的是 `query` 而非 `contents` 的字符串切片,那么他将不正确地执行安全检查。 -而在忘掉了这些生命周期注解并尝试编译该函数时,就会得到下面这个错误: +如果我们忘记了这些生命周期注解并尝试编译这个函数,我们将得到以下报错: ```console -$ cargo build lennyp@vm-manjaro +$ cargo build Compiling minigrep v0.1.0 (/home/lennyp/rust-lang/minigrep) error[E0106]: missing lifetime specifier --> src/lib.rs:35:51 @@ -86,14 +91,15 @@ For more information about this error, try `rustc --explain E0106`. error: could not compile `minigrep` due to previous error ``` -Rust 是不可能明白,这里需要的到底是两个参数中哪一个的,因此这里就需要显式地告诉 Rust。而由于 `contents` 正是那个包含了这里全部文本的参数,而这里打算返回的,就是与那个文本匹配的部分,因此这里清楚 `contents` 就应是要运用生命周期语法,将其与返回值联系起来的那个参数。 +Rust 不可能明白我们需要两个参数中的哪个,因此我们需要显式地告诉他。由于 `contents` 是包含所有文本的那个参数,而我们打算返回与该文本匹配的部分,因此我们知道,`contents` 应是要使用生命周期语法,与返回值关联的那个参数。 -别的编程语言并不会要求在函数签名中,将参数与返回值联系起来,但随着时间的推移,这样的实践将变得容易起来。或许你会将这个示例,与第 10 章中的 [“使用生命周期对引用进行验证” 小节](Ch10_Generic_Types_Traits_and_Lifetimes.md#使用生命周期对引用加以验证) 加以比较。 +其他编程语言不会要求咱们在函数签名中,将参数与返回值联系起来,但这种做法将随时间推移而变得愈加容易。咱们可将这个示例,与第10章 [“使用生命周期验证引用”](../generic_types_traits_and_lifetimes/lifetimes.md) 小节中的示例进行比较。 -现在来运行测试: + +现在咱们来运行这个测试: ```console -$ cargo test 12m 0s lennyp@vm-manjaro +$ cargo test Finished test [unoptimized + debuginfo] target(s) in 0.00s Running unittests src/lib.rs (target/debug/deps/minigrep-7d3f5b041202a66e) @@ -117,31 +123,30 @@ test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; error: test failed, to rerun pass '--lib' ``` -相当棒,这个测试失败了,就如这里的预期一样。接下来就要让这个测试通过! +太好了,这个测试失败了,正如我们预期。我们来让这个测试通过吧! -## 编写让测试通过的代码 +## 编写代码让测试通过 **Writing Code to Pass the Test** +目前,我们测试失败是因为我们总是返回一个空的矢量值。为修复这个问题并实现 `search`,我们的程序需要完成以下步骤: -此刻,由于这里始终返回一个空的矢量值,导致这里的测试失败。要修复这个测试失败并实现 `search`,这里的程序就需要遵循下面这些步骤: - -- 对那个内容的各个行加以迭代; -- 检查该行是否包含这里的查询字串; -- 在包含查询字串时,将该行添加到这里正要返回的值清单; -- 在不包含查询字串时,就什么也不做; -- 返回匹配结果的清单。 +1. 遍历内容的每一行; +2. 检查该行是否包含我们的查询字符串; +3. 在包含查询字符串时,将其添加到我们返回的值列表中; +4. 在不包含时,什么也不做; +5. 返回匹配结果清单。 下面就来逐一完成各个步骤,从那些文本行的迭代开始。 -### 使用 `lines` 方法对文本行进行遍历 +### 以 `lines` 方法遍历文本行 **Iterating Through Lines with the `lines` Method** -Rust 有着一个用于处理字符串一行行迭代的有用方法,其被方便地命名为了 `lines`,如下清单 12-17 中所示的那样运作。请注意下面的代码尚不会编译。 +Rust 有个处理字符串逐行迭代的方便方法,该方法名为 `lines`,其工作方式如下清单 12-17 所示。请注意,这段代码还不会编译。 文件名:`src/lib.rs` @@ -155,15 +160,17 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str>{ *清单 12-17:遍历 `contents` 中的各行* -这个 `lines` 方法,返回的是个迭代器(an iterator)。在 [第 13 章](Ch13_Functional_Language_Features_Iterators_and_Closures.md#使用迭代器对条目系列进行处理) 中,就会讲到迭代器,不过回顾一下 [清单 3-5](Ch03_Common_Programming_Concepts.md#使用-for-对集合进行遍历) 中,就曾见过这种用到迭代器的方式,那里曾用到一个 `for` 循环, 就带有一个用于在集合中各个元素上,运行某些代码的迭代器。 +`lines` 方法会返回一个迭代器。我们将在 [第 13 章](../functional_features/iterators.md) 中,详细讨论迭代器,但请回想一下,咱们在 [清单 3-5](../programming_concepts/control_flow.md#listing-3-5) 中就已见过这种使用迭代器的方法,当时我们与迭代器一起使用了 `for` 循环,对集合中的各个元素执行一些代码。 -### 在各行中搜索查询字串 +### 在各行中检索查询字符串 **Searching Each Line for the Query** -接下来,这里就要检查当前行是否包含着这里的查询字串。幸运的是,字符串有着一个为咱们完成这件事的名为 `contains` 的有用方法!在 `search` 函数中添加一个到这个 `contains` 方法的调用,如下清单 12-18 中所示。请注意这仍上不会编译。 +接下来,我们将检查当前行是否包含我们的查询字符串。幸运的是,字符串提供了一个名为 `contains`,帮助我们完成这项任务的有用方法!在 `search` 函数中,添加一个到 `contains` 方法的调用,如下清单 12-18 所示。请注意,此时代码仍不会编译。 + + 文件名:`src/lib.rs` @@ -177,17 +184,17 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str>{ } ``` -*清单 12-18:加入检视该行是否包含 `query` 中字符串的功能* +*清单 12-18:添加检查行中是否包含 `query` 中字符串的功能* -此刻,这里正在构建起功能来。而要让代码编译,就需要从其中的函数体,返回一个在该函数签名中,曾表明的应返回的某个值。 +目前,我们正构建起功能来。为使代码编译,我们需要自函数体返回一个值,正如我们在函数签名中所指明的那样。 -### 存储匹配的那些行 +### 存储匹配的行 **Storing Matching Lines** +要完成这个函数,我们需要一种存储我们打算返回匹配行的方法。为此,我们可在 `for` 循环前,创建一个可变的矢量值,并调用 `push` 方法将某个 `line` 存储在该矢量中。在 `for` 循环后,我们就要返回该矢量值,如下清单 12-19 所示。 -要完成这个函数,就需要某种对这里打算返回的那些匹配行,加以存储的方法。为那个目的,这里可以在其中的 `for` 循环之前,构造出一个可变矢量(a mutable vector),并调用 `push` 方法,来把某个 `line` 存储在该矢量中。在那个 `for` 循环之后,这里就返回那个矢量,如下清单 12-19 中所示。 文件名:`src/lib.rs` @@ -205,12 +212,14 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str>{ } ``` -*清单 12-19:对匹配的那些行进行存储,如此就可以返回他们了* +*清单 12-19:存储匹配行,从而咱们可以返回他们* + + +现在这个 `search` 函数应只返回那些包含 `query` 的行,同时我们的测试应该通过。我们来运行这个测试: -现在这个 `search` 函数就应只返回那些包含了 `query` 的行了,同时这里的测试应通过。下面就来运行该测试: ```console -$ cargo test lennyp@vm-manjaro +$ cargo test Compiling minigrep v0.1.0 (/home/lennyp/rust-lang/minigrep) Finished test [unoptimized + debuginfo] target(s) in 0.49s Running unittests src/lib.rs (target/debug/deps/minigrep-7d3f5b041202a66e) @@ -234,9 +243,10 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; fini ``` -这里的测试通过了,进而咱们就明白 `search` 函数是工作的了! +我们的测试通过了,所以我们知道 `search` 函数可以工作了! -到这里,咱们就会在保持这些测试通过,以维持这同样功能的同时,考虑对这个 `search` 函数的实现,进行重构的一些机会。这个 `search` 函数中的代码虽不怎么差劲,但他并没有利用上迭代器的一些有用特性。在 [第 13 章](Ch13_Functional_Language_Features_Iterators_and_Closures.md#使用迭代器对条目系列进行处理) 中将回到这个示例,那里就会详细探讨到迭代器,进而会看看怎样来改进这个 `search` 函数。 + +此时,我们就可以在确保测试通过以保持相同功能的同时,考虑对 `search` 函数的实现进行重构。这个检索函数中的代码并不算太差,但他并未充分利用迭代器的一些有用特性。我们将在 [第 13 章](../functional_features/iterators.md) 中回到这个示例,那里我们将详细探讨迭代器,并研究如何对其进行改进。 ### 在函数 `run` 中使用 `search` 函数 @@ -244,7 +254,8 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; fini **Using the `search` Function in the `run` Function** -既然 `search` 函数运作起来并被测试过,那么这里就需要在这里的 `run` 函数中,调用 `search` 了。这里需要将那个 `config.query` 值与 `run` 从文件中读取到的 `contents`,传递给这个 `search` 函数。随后 `run` 将打印出从 `search` 返回的各行: +现在这个 `search` 函数已经工作并经过测试,我们需要在咱们的 `run` 函数中调用 `sarch` 了。我们需要将 `config.query` 值,及 `run` 从文件中读取的 `contents`,传递给 `search` 函数。然后 `run` 将打印从 `search` 函数返回的各行: + 文件名:`src/lib.rs` @@ -260,22 +271,22 @@ pub fn run(config: Config) -> Result<(), Box>{ } ``` -这里仍使用一个 `for` 循环,来返回来自 `search` 的各行并将其打印出来。 +我们仍使用了个 `for` 循环,返回 `search` 结果中的各行并打印出来。 -现在这整个程序就应工作了!接下来就要试一下他了,首先以一个应确切地从这首 Emily Dickinson 的诗返回一行的词,“frog”: +现在整个程序就应该可以工作了!我们来试一试,首先以一个应该能从艾米莉·狄金森的诗中,准确返回一行内容的单词:*frog*。 ```console -$ cargo run -- frog poem.txt lennyp@vm-manjaro +$ cargo run -- frog poem.txt Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/minigrep frog poem.txt` 在文件 poem.txt 中检索:frog How public, like a frog ``` -酷!现在来试一个将匹配多行的词,比如 “body”: +太棒了!现在我们来尝试一个可以匹配多行内容的词,比如 *body*: ```console -$ cargo run -- body poem.txt lennyp@vm-manjaro +$ cargo run -- body poem.txt Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/minigrep body poem.txt` 在文件 poem.txt 中检索:body @@ -284,9 +295,27 @@ Are you nobody, too? How dreary to be somebody! ``` -相当棒!这里已经构建了一个经典工具自己的小型版本,并掌握了很多有关如何建构应用程序的知识。这里还了解到有关文件输入输出、生命周期、测试及命令行参数解析等方面的点滴内容。 -而为了完善这个项目,接下来就主要会演示怎样使用环境变量,以及怎样打印到标准错误输出(print to standard error),在编写命令行程序时,这两方面的知识都是有用的。 +最后,我们来确保在咱们检索诗中不存在的单词时,不会出现任何行,比如 *monomorphization*: + + +```console +$ cargo run -- monomorphization poem.txt + Compiling minigrep v0.1.0 (file:///projects/minigrep) + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s + Running `target/debug/minigrep monomorphization poem.txt` +``` + + +太棒了!我们已经构建出了一个经典工具的咱们自己的迷你版本,并掌握了如何设计架构应用的大量知识。此外,我们还掌握了文件输入输出、生命周期、测试以及命令行解析等方面的知识。 + +为完善这个项目,我们将简要演示: + +- 如何使用环境变量; +- 以及如何将输出打印到标准错误。 + +这两者在咱们编写命令行的程序时,都非常有用。 + (End) diff --git a/src/programming_concepts/control_flow.md b/src/programming_concepts/control_flow.md index 68db119..7de8241 100644 --- a/src/programming_concepts/control_flow.md +++ b/src/programming_concepts/control_flow.md @@ -474,7 +474,7 @@ fn main() { } ``` -*清单 3-5:使用 `for` 循环遍历某个集合的各个元素* + *清单 3-5:使用 `for` 循环遍历某个集合的各个元素* 当咱们运行这段代码时,我们将看到与清单 3-4 同样的输出。更重要的是,我们现在提高了代码的安全性,消除了可能由超出数组末尾,或因遍历不足远而遗漏某些项目等所可能导致的错误。