wip ch12-04

This commit is contained in:
KaiserY 2017-03-06 22:56:55 +08:00
parent 8d76b29be3
commit edb7303689
7 changed files with 1898 additions and 2 deletions

View File

@ -353,6 +353,323 @@ impl Config {
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p>现在<code>new</code>函数返回一个<code>Result</code>,在成功时带有一个<code>Config</code>实例而在出现错误时带有一个<code>&amp;'static str</code>。回忆一下第十章“静态声明周期”中讲到<code>&amp;'static str</code>是一个字符串字面值,他也是现在我们的错误信息。</p>
<p><code>new</code>函数体中有两处修改:当没有足够参数时不再调用<code>panic!</code>,而是返回<code>Err</code>值。同时我们将<code>Config</code>返回值包装进<code>Ok</code>成员中。这些修改使得函数符合其新的类型签名。</p>
<a class="header" href="#confignew调用和错误处理" name="confignew调用和错误处理"><h3><code>Config::new</code>调用和错误处理</h3></a>
<p>现在我们需要对<code>main</code>做一些修改,如列表 12-9 所示:</p>
<figure>
<span class="filename">Filename: src/main.rs</span>
<pre><code class="language-rust"># use std::env;
# use std::fs::File;
# use std::io::prelude::*;
// ...snip...
use std::process;
fn main() {
let args: Vec&lt;String&gt; = env::args().collect();
let config = Config::new(&amp;args).unwrap_or_else(|err| {
println!(&quot;Problem parsing arguments: {}&quot;, err);
process::exit(1);
});
println!(&quot;Searching for {}&quot;, config.search);
println!(&quot;In file {}&quot;, config.filename);
// ...snip...
#
# let mut f = File::open(config.filename).expect(&quot;file not found&quot;);
#
# let mut contents = String::new();
# f.read_to_string(&amp;mut contents).expect(&quot;something went wrong reading the file&quot;);
#
# println!(&quot;With text:\n{}&quot;, contents);
# }
#
# struct Config {
# search: String,
# filename: String,
# }
#
# impl Config {
# fn new(args: &amp;[String]) -&gt; Result&lt;Config, &amp;'static str&gt; {
# if args.len() &lt; 3 {
# return Err(&quot;not enough arguments&quot;);
# }
#
# let search = args[1].clone();
# let filename = args[2].clone();
#
# Ok(Config {
# search: search,
# filename: filename,
# })
# }
# }
</code></pre>
<figcaption>
<p>Listing 12-9: Exiting with an error code if creating a new <code>Config</code> fails</p>
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p>新增了一个<code>use</code>行来从标准库中导入<code>process</code>。在<code>main</code>函数中我们将处理<code>new</code>函数返回的<code>Result</code>值,并在其返回<code>Config::new</code>时以一种更加清楚的方式结束进程。</p>
<p>这里使用了一个之前没有讲到的标准库中定义的<code>Result&lt;T, E&gt;</code>的方法:<code>unwrap_or_else</code>。当<code>Result</code><code>Ok</code>时其行为类似于<code>unwrap</code>:它返回<code>Ok</code>内部封装的值。与<code>unwrap</code>不同的是,当<code>Result</code><code>Err</code>时,它调用一个<strong>闭包</strong><em>closure</em>),也就是一个我们定义的作为参数传递给<code>unwrap_or_else</code>的匿名函数。第XX章会更详细的介绍闭包这里需要理解的重要部分是<code>unwrap_or_else</code>会将<code>Err</code>的内部值传递给闭包中位于两道竖线间的参数<code>err</code>。使用<code>unwrap_or_else</code>允许我们进行一些自定义的非<code>panic!</code>的错误处理。</p>
<p>上述的错误处理其实只有两行:我们打印出了错误,接着调用了<code>std::process::exit</code>。这个函数立刻停止程序的执行并将传递给它的数组作为返回码。依照惯例,零代表成功而任何其他数字表示失败。就结果来说这依然类似于列表 12-7 中的基于<code>panic!</code>的错误处理,但是不再会有额外的输出了,让我们试一试:</p>
<pre><code class="language-text">$ cargo run
Compiling greprs v0.1.0 (file:///projects/greprs)
Finished debug [unoptimized + debuginfo] target(s) in 0.48 secs
Running `target\debug\greprs.exe`
Problem parsing arguments: not enough arguments
</code></pre>
<p>非常好!现在输出就友好多了。</p>
<a class="header" href="#run函数中的错误处理" name="run函数中的错误处理"><h3><code>run</code>函数中的错误处理</h3></a>
<p>现在重构完了参数解析部分,让我们再改进一下程序的逻辑。列表 12-10 中展示了在<code>main</code>函数中调用提取出函数<code>run</code>之后的代码。<code>run</code>函数包含之前位于<code>main</code>中的部分代码:</p>
<figure>
<span class="filename">Filename: src/main.rs</span>
<pre><code class="language-rust"># use std::env;
# use std::fs::File;
# use std::io::prelude::*;
# use std::process;
#
fn main() {
# let args: Vec&lt;String&gt; = env::args().collect();
#
# let config = Config::new(&amp;args).unwrap_or_else(|err| {
# println!(&quot;Problem parsing arguments: {}&quot;, err);
# process::exit(1);
# });
// ...snip...
println!(&quot;Searching for {}&quot;, config.search);
println!(&quot;In file {}&quot;, config.filename);
run(config);
}
fn run(config: Config) {
let mut f = File::open(config.filename).expect(&quot;file not found&quot;);
let mut contents = String::new();
f.read_to_string(&amp;mut contents).expect(&quot;something went wrong reading the file&quot;);
println!(&quot;With text:\n{}&quot;, contents);
}
// ...snip...
#
# struct Config {
# search: String,
# filename: String,
# }
#
# impl Config {
# fn new(args: &amp;[String]) -&gt; Result&lt;Config, &amp;'static str&gt; {
# if args.len() &lt; 3 {
# return Err(&quot;not enough arguments&quot;);
# }
#
# let search = args[1].clone();
# let filename = args[2].clone();
#
# Ok(Config {
# search: search,
# filename: filename,
# })
# }
# }
</code></pre>
<figcaption>
<p>Listing 12-10: Extracting a <code>run</code> functionality for the rest of the program logic</p>
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p><code>run</code>函数的内容是之前位于<code>main</code>中的几行,而且<code>run</code>函数获取一个<code>Config</code>作为参数。现在有了一个单独的函数了,我们就可以像列表 12-8 中的<code>Config::new</code>那样进行类似的改进了。列表 12-11 展示了另一个<code>use</code>语句将<code>std::error::Error</code>结构引入了作用域,还有使<code>run</code>函数返回<code>Result</code>的修改:</p>
<figure>
<span class="filename">Filename: src/main.rs</span>
<pre><code class="language-rust">use std::error::Error;
# use std::env;
# use std::fs::File;
# use std::io::prelude::*;
# use std::process;
// ...snip...
# fn main() {
# let args: Vec&lt;String&gt; = env::args().collect();
#
# let config = Config::new(&amp;args).unwrap_or_else(|err| {
# println!(&quot;Problem parsing arguments: {}&quot;, err);
# process::exit(1);
# });
#
# println!(&quot;Searching for {}&quot;, config.search);
# println!(&quot;In file {}&quot;, config.filename);
#
# run(config);
#
# }
fn run(config: Config) -&gt; Result&lt;(), Box&lt;Error&gt;&gt; {
let mut f = File::open(config.filename)?;
let mut contents = String::new();
f.read_to_string(&amp;mut contents)?;
println!(&quot;With text:\n{}&quot;, contents);
Ok(())
}
#
# struct Config {
# search: String,
# filename: String,
# }
#
# impl Config {
# fn new(args: &amp;[String]) -&gt; Result&lt;Config, &amp;'static str&gt; {
# if args.len() &lt; 3 {
# return Err(&quot;not enough arguments&quot;);
# }
#
# let search = args[1].clone();
# let filename = args[2].clone();
#
# Ok(Config {
# search: search,
# filename: filename,
# })
# }
# }
</code></pre>
<figcaption>
<p>Listing 12-11: Changing the <code>run</code> function to return <code>Result</code></p>
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p>这里有三个大的修改。第一个是现在<code>run</code>函数的返回值是<code>Result&lt;(), Box&lt;Error&gt;&gt;</code>类型的。之前,函数返回 unit 类型<code>()</code>,现在它仍然是<code>Ok</code>时的返回值。对于错误类型,我们将使用<code>Box&lt;Error&gt;</code>。这是一个<strong>trait 对象</strong><em>trait object</em>第XX章会讲到。现在可以这样理解它<code>Box&lt;Error&gt;</code>意味着函数返回了某个实现了<code>Error</code> trait 的类型,不过并没有指定具体的返回值类型。这样就比较灵活,因为在不同的错误场景可能有不同类型的错误返回值。<code>Box</code>是一个堆数据的智能指针第YY章将会详细介绍<code>Box</code></p>
<p>第二个改变是我们去掉了<code>expect</code>调用并替换为第9章讲到的<code>?</code>。不同于遇到错误就<code>panic!</code>,这会从函数中返回错误值并让调用者来处理它。</p>
<p>第三个修改是现在成功时这个函数会返回一个<code>Ok</code>值。因为<code>run</code>函数签名中声明成功类型返回值是<code>()</code>,所以需要将 unit 类型值包装进<code>Ok</code>值中。<code>Ok(())</code>一开始看起来有点奇怪,不过这样使用<code>()</code>是表明我们调用<code>run</code>只是为了它的副作用的惯用方式;它并没有返回什么有意义的值。</p>
<p>上述代码能够编译,不过会有一个警告:</p>
<pre><code>warning: unused result which must be used, #[warn(unused_must_use)] on by default
--&gt; src\main.rs:39:5
|
39 | run(config);
| ^^^^^^^^^^^^
</code></pre>
<p>Rust 尝试告诉我们忽略<code>Result</code>,它有可能是一个错误值。让我们现在来处理它。我们将采用类似于列表 12-9 中处理<code>Config::new</code>错误的技巧,不过还有少许不同:</p>
<p><span class="filename">Filename: src/main.rs</span></p>
<pre><code class="language-rust,ignore">fn main() {
// ...snip...
println!(&quot;Searching for {}&quot;, config.search);
println!(&quot;In file {}&quot;, config.filename);
if let Err(e) = run(config) {
println!(&quot;Application error: {}&quot;, e);
process::exit(1);
}
}
fn run(config: Config) -&gt; Result&lt;(), Box&lt;Error&gt;&gt; {
let mut f = File::open(config.filename)?;
let mut contents = String::new();
f.read_to_string(&amp;mut contents)?;
println!(&quot;With text:\n{}&quot;, contents);
Ok(())
}
</code></pre>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p>不同于<code>unwrap_or_else</code>,我们使用<code>if let</code>来检查<code>run</code>是否返回<code>Err</code>,如果是则调用<code>process::exit(1)</code>。为什么呢?这个例子和<code>Config::new</code>的区别有些微妙。对于<code>Config::new</code>我们关心两件事:</p>
<ol>
<li>检测出任何可能发生的错误</li>
<li>如果没有出现错误创建一个<code>Config</code></li>
</ol>
<p>而在这个情况下,因为<code>run</code>在成功的时候返回一个<code>()</code>,唯一需要担心的就是第一件事:检测错误。如果我们使用了<code>unwrap_or_else</code>,则会得到<code>()</code>的返回值。它并没有什么用处。</p>
<p>虽然两种情况下<code>if let</code><code>unwrap_or_else</code>的内容都是一样的:打印出错误并退出。</p>
<a class="header" href="#将代码拆分到库-crate" name="将代码拆分到库-crate"><h3>将代码拆分到库 crate</h3></a>
<p>现在项目看起来好多了!还有一件我们尚未开始的工作:拆分 <em>src/main.rs</em> 并将一些代码放入 <em>src/lib.rs</em> 中。让我们现在就开始吧:将 <em>src/main.rs</em> 中的<code>run</code>函数移动到新建的 <em>src/lib.rs</em> 中。还需要移动相关的<code>use</code>语句和<code>Config</code>的定义,以及其<code>new</code>方法。现在 <em>src/lib.rs</em> 应该如列表 12-12 所示:</p>
<figure>
<span class="filename">Filename: src/lib.rs</span>
<pre><code class="language-rust">use std::error::Error;
use std::fs::File;
use std::io::prelude::*;
pub struct Config {
pub search: String,
pub filename: String,
}
impl Config {
pub fn new(args: &amp;[String]) -&gt; Result&lt;Config, &amp;'static str&gt; {
if args.len() &lt; 3 {
return Err(&quot;not enough arguments&quot;);
}
let search = args[1].clone();
let filename = args[2].clone();
Ok(Config {
search: search,
filename: filename,
})
}
}
pub fn run(config: Config) -&gt; Result&lt;(), Box&lt;Error&gt;&gt;{
let mut f = File::open(config.filename)?;
let mut contents = String::new();
f.read_to_string(&amp;mut contents)?;
println!(&quot;With text:\n{}&quot;, contents);
Ok(())
}
</code></pre>
<figcaption>
<p>Listing 12-12: Moving <code>Config</code> and <code>run</code> into <em>src/lib.rs</em></p>
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p>注意我们还需要使用公有的<code>pub</code>:在<code>Config</code>和其字段、它的<code>new</code>方法和<code>run</code>函数上。</p>
<p>现在在 <em>src/main.rs</em> 中,我们需要通过<code>extern crate greprs</code>来引入现在位于 <em>src/lib.rs</em> 的代码。接着需要增加一行<code>use greprs::Config</code>来引入<code>Config</code>到作用域,并对<code>run</code>函数加上 crate 名称前缀,如列表 12-13 所示:</p>
<figure>
<span class="filename">Filename: src/main.rs</span>
<pre><code class="language-rust,ignore">extern crate greprs;
use std::env;
use std::process;
use greprs::Config;
fn main() {
let args: Vec&lt;String&gt; = env::args().collect();
let config = Config::new(&amp;args).unwrap_or_else(|err| {
println!(&quot;Problem parsing arguments: {}&quot;, err);
process::exit(1);
});
println!(&quot;Searching for {}&quot;, config.search);
println!(&quot;In file {}&quot;, config.filename);
if let Err(e) = greprs::run(config) {
println!(&quot;Application error: {}&quot;, e);
process::exit(1);
}
}
</code></pre>
<figcaption>
<p>Listing 12-13: Bringing the <code>greprs</code> crate into the scope of <em>src/main.rs</em></p>
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p>通过这些重构,所有代码应该都能运行了。运行几次<code>cargo run</code>来确保你没有破坏什么内容。好的!确实有很多的内容,不过已经为将来的成功奠定了基础。我们采用了一种更加优秀的方式来处理错误,并使得代码更模块化了一些。从现在开始几乎所有的工作都将在 <em>src/lib.rs</em> 中进行。</p>
<p>让我们利用这新创建的模块的优势来进行一些在旧代码中难以开开展的工作,他们在新代码中却很简单:编写测试!</p>
</div>

View File

@ -67,7 +67,212 @@
</div>
<div id="content" class="content">
<a class="header" href="#测试库的功能" name="测试库的功能"><h2>测试库的功能</h2></a>
<blockquote>
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-04-testing-the-librarys-functionality.md">ch12-04-testing-the-librarys-functionality.md</a>
<br>
commit 4f2dc564851dc04b271a2260c834643dfd86c724</p>
</blockquote>
<p>现在为项目的核心功能编写测试将更加容易,因为我们将逻辑提取到了 <em>src/lib.rs</em> 中并将参数解析和错误处理都留在了 <em>src/main.rs</em> 里。现在我们可以直接使用多种参数调用代码并检查返回值而不用从命令行运行二进制文件了。</p>
<p>我们将要编写的是一个叫做<code>grep</code>的函数,它获取要搜索的项以及文本并产生一个搜索结果列表。让我们从<code>run</code>中去掉那行<code>println!</code>(也去掉 <em>src/main.rs</em> 中的,因为再也不需要他们了),并使用之前收集的选项来调用新的<code>grep</code>函数。眼下我们只增加一个空的实现,和指定<code>grep</code>期望行为的测试。当然,这个测试对于空的实现来说是会失败的,不过可以确保代码是可以编译的并得到期望的错误信息。列表 12-14 展示了这些修改:</p>
<figure>
<span class="filename">Filename: src/lib.rs</span>
<pre><code class="language-rust"># use std::error::Error;
# use std::fs::File;
# use std::io::prelude::*;
#
# pub struct Config {
# pub search: String,
# pub filename: String,
# }
#
// ...snip...
fn grep&lt;'a&gt;(search: &amp;str, contents: &amp;'a str) -&gt; Vec&lt;&amp;'a str&gt; {
vec![]
}
pub fn run(config: Config) -&gt; Result&lt;(), Box&lt;Error&gt;&gt;{
let mut f = File::open(config.filename)?;
let mut contents = String::new();
f.read_to_string(&amp;mut contents)?;
grep(&amp;config.search, &amp;contents);
Ok(())
}
#[cfg(test)]
mod test {
use grep;
#[test]
fn one_result() {
let search = &quot;duct&quot;;
let contents = &quot;\
Rust:
safe, fast, productive.
Pick three.&quot;;
assert_eq!(
vec![&quot;safe, fast, productive.&quot;],
grep(search, contents)
);
}
}
</code></pre>
<figcaption>
<p>Listing 12-14: Creating a function where our logic will go and a failing test
for that function</p>
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p>注意需要在<code>grep</code>的签名中显式声明声明周期<code>'a</code>并用于<code>contents</code>参数和返回值。记住,生命周期参数用于指定函数参数于返回值的生命周期的关系。在这个例子中,我们表明返回的 vector 将包含引用参数<code>contents</code>的字符串 slice而不是引用参数<code>search</code>的字符串 slice。换一种说法就是我们告诉 Rust 函数<code>grep</code>返回的数据将和传递给它的参数<code>contents</code>的数据存活的同样久。这是非常重要的!考虑为了使引用有效则 slice 引用的数据也需要保持有效,如果编译器认为我们是在创建<code>search</code>而不是<code>contents</code>的 slice那么安全检查将是不正确的。如果尝试不用生命周期编译的话我们将得到如下错误</p>
<pre><code>error[E0106]: missing lifetime specifier
--&gt; src\lib.rs:37:46
|
37 | fn grep(search: &amp;str, contents: &amp;str) -&gt; Vec&lt;&amp;str&gt; {
| ^ expected lifetime parameter
|
= help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `search` or
`contents`
</code></pre>
<p>Rust 不可能知道我们需要的是哪一个参数,所以需要告诉它。因为参数<code>contents</code>包含了所有的文本而且我们希望返回匹配的那部分文本,而我们知道<code>contents</code>是应该要使用生命周期语法来与返回值相关联的参数。</p>
<p>在函数签名中将参数与返回值相关联是其他语言不会让你做的工作,所以不用担心这感觉很奇怪!掌握如何指定生命周期会随着时间的推移越来越容易,熟能生巧。你可能想要重新阅读上一部分或返回与第十章中生命周期语法部分的例子做对比。</p>
<p>现在试试运行测试:</p>
<pre><code>$ cargo test
...warnings...
Finished debug [unoptimized + debuginfo] target(s) in 0.43 secs
Running target/debug/deps/greprs-abcabcabc
running 1 test
test test::one_result ... FAILED
failures:
---- test::one_result stdout ----
thread 'test::one_result' panicked at 'assertion failed: `(left == right)`
(left: `[&quot;safe, fast, productive.&quot;]`, right: `[]`)', src/lib.rs:16
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
test::one_result
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
error: test failed
</code></pre>
<p>好的,测试失败了,这正是我们所期望的。修改代码来让测试通过吧!之所以会失败是因为我们总是返回一个空的 vector。如下是如何实现<code>grep</code>的步骤:</p>
<ol>
<li>遍历每一行文本。</li>
<li>查看这一行是否包含要搜索的字符串。
<ul>
<li>如果有,将这一行加入返回列表中</li>
<li>如果没有,什么也不做</li>
</ul>
</li>
<li>返回匹配到的列表</li>
</ol>
<p>让我们一步一步的来,从遍历每行开始。字符串类型有一个有用的方法来处理这种情况,它刚好叫做<code>lines</code></p>
<p><span class="filename">Filename: src/lib.rs</span></p>
<pre><code class="language-rust,ignore">fn grep&lt;'a&gt;(search: &amp;str, contents: &amp;'a str) -&gt; Vec&lt;&amp;'a str&gt; {
for line in contents.lines() {
// do something with line
}
}
</code></pre>
<!-- Will add wingdings in libreoffice /Carol -->
<p>我们使用了一个<code>for</code>循环和<code>lines</code>方法来依次获得每一行。接下来,让我们看看这些行是否包含要搜索的字符串。幸运的是,字符串类型为此也有一个有用的方法<code>contains</code><code>contains</code>的用法看起来像这样:</p>
<p><span class="filename">Filename: src/lib.rs</span></p>
<pre><code class="language-rust,ignore">fn grep&lt;'a&gt;(search: &amp;str, contents: &amp;'a str) -&gt; Vec&lt;&amp;'a str&gt; {
for line in contents.lines() {
if line.contains(search) {
// do something with line
}
}
}
</code></pre>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p>最终,我们需要一个方法来存储包含要搜索字符串的行。为此可以在<code>for</code>循环之前创建一个可变的 vector 并调用<code>push</code>方法来存放一个<code>line</code>。在<code>for</code>循环之后,返回这个 vector。列表 12-15 中为完整的实现:</p>
<figure>
<span class="filename">Filename: src/lib.rs</span>
<pre><code class="language-rust">fn grep&lt;'a&gt;(search: &amp;str, contents: &amp;'a str) -&gt; Vec&lt;&amp;'a str&gt; {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(search) {
results.push(line);
}
}
results
}
</code></pre>
<figcaption>
<p>Listing 12-15: Fully functioning implementation of the <code>grep</code> function</p>
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p>尝试运行一下:</p>
<pre><code>$ cargo test
running 1 test
test test::one_result ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Running target/debug/greprs-2f55ee8cd1721808
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
Doc-tests greprs
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
</code></pre>
<p>非常好!它可以工作了。现在测试通过了,我们可以考虑一下重构<code>grep</code>的实现并时刻保持其功能不变。这些代码并不坏,不过并没有利用迭代器的一些实用功能。第十三章将回到这个例子并探索迭代器和如何改进代码。</p>
<p>现在<code>grep</code>函数是可以工作的,我们还需在在<code>run</code>函数中做最后一件事:还没有打印出结果呢!增加一个<code>for</code>循环来打印出<code>grep</code>函数返回的每一行:</p>
<p><span class="filename">Filename: src/lib.rs</span></p>
<pre><code class="language-rust,ignore">pub fn run(config: Config) -&gt; Result&lt;(), Box&lt;Error&gt;&gt; {
let mut f = File::open(config.filename)?;
let mut contents = String::new();
f.read_to_string(&amp;mut contents)?;
for line in grep(&amp;config.search, &amp;contents) {
println!(&quot;{}&quot;, line);
}
Ok(())
}
</code></pre>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p>现在程序应该能正常运行了!试试吧:</p>
<pre><code>$ cargo run the poem.txt
Compiling greprs v0.1.0 (file:///projects/greprs)
Finished debug [unoptimized + debuginfo] target(s) in 0.38 secs
Running `target\debug\greprs.exe the poem.txt`
Then there's a pair of us - don't tell!
To tell your name the livelong day
$ cargo run a poem.txt
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target\debug\greprs.exe a poem.txt`
I'm nobody! Who are you?
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!
</code></pre>
<p>好极了!我们创建了一个属于自己的经典工具,并学习了很多如何组织程序的知识。我们还学习了一些文件输入输出、生命周期、测试和命令行解析的内容。</p>
</div>
<!-- Mobile navigation buttons -->

View File

@ -67,7 +67,75 @@
</div>
<div id="content" class="content">
<a class="header" href="#处理环境变量" name="处理环境变量"><h2>处理环境变量</h2></a>
<blockquote>
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-05-working-with-environment-variables.md">ch12-05-working-with-environment-variables.md</a>
<br>
commit 4f2dc564851dc04b271a2260c834643dfd86c724</p>
</blockquote>
<p>让我们再增加一个功能:大小写不敏感搜索。另外,这个设定将不是一个命令行参数:相反它将是一个环境变量。当然可以选择创建一个大小写不敏感的命令行参数,不过用户要求提供一个环境变量这样设置一次之后在整个终端会话中所有的搜索都将是大小写不敏感的了。</p>
<a class="header" href="#实现并测试一个大小写不敏感grep函数" name="实现并测试一个大小写不敏感grep函数"><h3>实现并测试一个大小写不敏感<code>grep</code>函数</h3></a>
<p>首先,让我们增加一个新函数,当设置了环境变量时会调用它。增加一个新测试并重命名已经存在的那个:</p>
<pre><code class="language-rust,ignore">#[cfg(test)]
mod test {
use {grep, grep_case_insensitive};
#[test]
fn case_sensitive() {
let search = &quot;duct&quot;;
let contents = &quot;\
Rust:
safe, fast, productive.
Pick three.
Duct tape.&quot;;
assert_eq!(
vec![&quot;safe, fast, productive.&quot;],
grep(search, contents)
);
}
#[test]
fn case_insensitive() {
let search = &quot;rust&quot;;
let contents = &quot;\
Rust:
safe, fast, productive.
Pick three.
Trust me.&quot;;
assert_eq!(
vec![&quot;Rust:&quot;, &quot;Trust me.&quot;],
grep_case_insensitive(search, contents)
);
}
}
</code></pre>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p>我们将定义一个叫做<code>grep_case_insensitive</code>的新函数。它的实现与<code>grep</code>函数大体上相似,不过列表 12-16 展示了一些小的区别:</p>
<figure>
<span class="filename">Filename: src/lib.rs</span>
<pre><code class="language-rust">fn grep_case_insensitive&lt;'a&gt;(search: &amp;str, contents: &amp;'a str) -&gt; Vec&lt;&amp;'a str&gt; {
let search = search.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&amp;search) {
results.push(line);
}
}
results
}
</code></pre>
<figcaption>
<p>Listing 12-16: Implementing a <code>grep_case_insensitive</code> function by changing the
search string and the lines of the contents to lowercase before comparing them</p>
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p>首先,将<code>search</code>字符串转换为小写,并存放于一个同名的覆盖变量中。注意现在<code>search</code>是一个<code>String</code>而不是字符串 slice所以在将<code>search</code>传递给<code>contains</code>时需要加上 &amp;,因为<code>contains</code>获取一个字符串 slice。</p>
</div>
<!-- Mobile navigation buttons -->

View File

@ -6561,6 +6561,596 @@ impl Config {
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p>现在<code>new</code>函数返回一个<code>Result</code>,在成功时带有一个<code>Config</code>实例而在出现错误时带有一个<code>&amp;'static str</code>。回忆一下第十章“静态声明周期”中讲到<code>&amp;'static str</code>是一个字符串字面值,他也是现在我们的错误信息。</p>
<p><code>new</code>函数体中有两处修改:当没有足够参数时不再调用<code>panic!</code>,而是返回<code>Err</code>值。同时我们将<code>Config</code>返回值包装进<code>Ok</code>成员中。这些修改使得函数符合其新的类型签名。</p>
<a class="header" href="#confignew调用和错误处理" name="confignew调用和错误处理"><h3><code>Config::new</code>调用和错误处理</h3></a>
<p>现在我们需要对<code>main</code>做一些修改,如列表 12-9 所示:</p>
<figure>
<span class="filename">Filename: src/main.rs</span>
<pre><code class="language-rust"># use std::env;
# use std::fs::File;
# use std::io::prelude::*;
// ...snip...
use std::process;
fn main() {
let args: Vec&lt;String&gt; = env::args().collect();
let config = Config::new(&amp;args).unwrap_or_else(|err| {
println!(&quot;Problem parsing arguments: {}&quot;, err);
process::exit(1);
});
println!(&quot;Searching for {}&quot;, config.search);
println!(&quot;In file {}&quot;, config.filename);
// ...snip...
#
# let mut f = File::open(config.filename).expect(&quot;file not found&quot;);
#
# let mut contents = String::new();
# f.read_to_string(&amp;mut contents).expect(&quot;something went wrong reading the file&quot;);
#
# println!(&quot;With text:\n{}&quot;, contents);
# }
#
# struct Config {
# search: String,
# filename: String,
# }
#
# impl Config {
# fn new(args: &amp;[String]) -&gt; Result&lt;Config, &amp;'static str&gt; {
# if args.len() &lt; 3 {
# return Err(&quot;not enough arguments&quot;);
# }
#
# let search = args[1].clone();
# let filename = args[2].clone();
#
# Ok(Config {
# search: search,
# filename: filename,
# })
# }
# }
</code></pre>
<figcaption>
<p>Listing 12-9: Exiting with an error code if creating a new <code>Config</code> fails</p>
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p>新增了一个<code>use</code>行来从标准库中导入<code>process</code>。在<code>main</code>函数中我们将处理<code>new</code>函数返回的<code>Result</code>值,并在其返回<code>Config::new</code>时以一种更加清楚的方式结束进程。</p>
<p>这里使用了一个之前没有讲到的标准库中定义的<code>Result&lt;T, E&gt;</code>的方法:<code>unwrap_or_else</code>。当<code>Result</code><code>Ok</code>时其行为类似于<code>unwrap</code>:它返回<code>Ok</code>内部封装的值。与<code>unwrap</code>不同的是,当<code>Result</code><code>Err</code>时,它调用一个<strong>闭包</strong><em>closure</em>),也就是一个我们定义的作为参数传递给<code>unwrap_or_else</code>的匿名函数。第XX章会更详细的介绍闭包这里需要理解的重要部分是<code>unwrap_or_else</code>会将<code>Err</code>的内部值传递给闭包中位于两道竖线间的参数<code>err</code>。使用<code>unwrap_or_else</code>允许我们进行一些自定义的非<code>panic!</code>的错误处理。</p>
<p>上述的错误处理其实只有两行:我们打印出了错误,接着调用了<code>std::process::exit</code>。这个函数立刻停止程序的执行并将传递给它的数组作为返回码。依照惯例,零代表成功而任何其他数字表示失败。就结果来说这依然类似于列表 12-7 中的基于<code>panic!</code>的错误处理,但是不再会有额外的输出了,让我们试一试:</p>
<pre><code class="language-text">$ cargo run
Compiling greprs v0.1.0 (file:///projects/greprs)
Finished debug [unoptimized + debuginfo] target(s) in 0.48 secs
Running `target\debug\greprs.exe`
Problem parsing arguments: not enough arguments
</code></pre>
<p>非常好!现在输出就友好多了。</p>
<a class="header" href="#run函数中的错误处理" name="run函数中的错误处理"><h3><code>run</code>函数中的错误处理</h3></a>
<p>现在重构完了参数解析部分,让我们再改进一下程序的逻辑。列表 12-10 中展示了在<code>main</code>函数中调用提取出函数<code>run</code>之后的代码。<code>run</code>函数包含之前位于<code>main</code>中的部分代码:</p>
<figure>
<span class="filename">Filename: src/main.rs</span>
<pre><code class="language-rust"># use std::env;
# use std::fs::File;
# use std::io::prelude::*;
# use std::process;
#
fn main() {
# let args: Vec&lt;String&gt; = env::args().collect();
#
# let config = Config::new(&amp;args).unwrap_or_else(|err| {
# println!(&quot;Problem parsing arguments: {}&quot;, err);
# process::exit(1);
# });
// ...snip...
println!(&quot;Searching for {}&quot;, config.search);
println!(&quot;In file {}&quot;, config.filename);
run(config);
}
fn run(config: Config) {
let mut f = File::open(config.filename).expect(&quot;file not found&quot;);
let mut contents = String::new();
f.read_to_string(&amp;mut contents).expect(&quot;something went wrong reading the file&quot;);
println!(&quot;With text:\n{}&quot;, contents);
}
// ...snip...
#
# struct Config {
# search: String,
# filename: String,
# }
#
# impl Config {
# fn new(args: &amp;[String]) -&gt; Result&lt;Config, &amp;'static str&gt; {
# if args.len() &lt; 3 {
# return Err(&quot;not enough arguments&quot;);
# }
#
# let search = args[1].clone();
# let filename = args[2].clone();
#
# Ok(Config {
# search: search,
# filename: filename,
# })
# }
# }
</code></pre>
<figcaption>
<p>Listing 12-10: Extracting a <code>run</code> functionality for the rest of the program logic</p>
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p><code>run</code>函数的内容是之前位于<code>main</code>中的几行,而且<code>run</code>函数获取一个<code>Config</code>作为参数。现在有了一个单独的函数了,我们就可以像列表 12-8 中的<code>Config::new</code>那样进行类似的改进了。列表 12-11 展示了另一个<code>use</code>语句将<code>std::error::Error</code>结构引入了作用域,还有使<code>run</code>函数返回<code>Result</code>的修改:</p>
<figure>
<span class="filename">Filename: src/main.rs</span>
<pre><code class="language-rust">use std::error::Error;
# use std::env;
# use std::fs::File;
# use std::io::prelude::*;
# use std::process;
// ...snip...
# fn main() {
# let args: Vec&lt;String&gt; = env::args().collect();
#
# let config = Config::new(&amp;args).unwrap_or_else(|err| {
# println!(&quot;Problem parsing arguments: {}&quot;, err);
# process::exit(1);
# });
#
# println!(&quot;Searching for {}&quot;, config.search);
# println!(&quot;In file {}&quot;, config.filename);
#
# run(config);
#
# }
fn run(config: Config) -&gt; Result&lt;(), Box&lt;Error&gt;&gt; {
let mut f = File::open(config.filename)?;
let mut contents = String::new();
f.read_to_string(&amp;mut contents)?;
println!(&quot;With text:\n{}&quot;, contents);
Ok(())
}
#
# struct Config {
# search: String,
# filename: String,
# }
#
# impl Config {
# fn new(args: &amp;[String]) -&gt; Result&lt;Config, &amp;'static str&gt; {
# if args.len() &lt; 3 {
# return Err(&quot;not enough arguments&quot;);
# }
#
# let search = args[1].clone();
# let filename = args[2].clone();
#
# Ok(Config {
# search: search,
# filename: filename,
# })
# }
# }
</code></pre>
<figcaption>
<p>Listing 12-11: Changing the <code>run</code> function to return <code>Result</code></p>
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p>这里有三个大的修改。第一个是现在<code>run</code>函数的返回值是<code>Result&lt;(), Box&lt;Error&gt;&gt;</code>类型的。之前,函数返回 unit 类型<code>()</code>,现在它仍然是<code>Ok</code>时的返回值。对于错误类型,我们将使用<code>Box&lt;Error&gt;</code>。这是一个<strong>trait 对象</strong><em>trait object</em>第XX章会讲到。现在可以这样理解它<code>Box&lt;Error&gt;</code>意味着函数返回了某个实现了<code>Error</code> trait 的类型,不过并没有指定具体的返回值类型。这样就比较灵活,因为在不同的错误场景可能有不同类型的错误返回值。<code>Box</code>是一个堆数据的智能指针第YY章将会详细介绍<code>Box</code></p>
<p>第二个改变是我们去掉了<code>expect</code>调用并替换为第9章讲到的<code>?</code>。不同于遇到错误就<code>panic!</code>,这会从函数中返回错误值并让调用者来处理它。</p>
<p>第三个修改是现在成功时这个函数会返回一个<code>Ok</code>值。因为<code>run</code>函数签名中声明成功类型返回值是<code>()</code>,所以需要将 unit 类型值包装进<code>Ok</code>值中。<code>Ok(())</code>一开始看起来有点奇怪,不过这样使用<code>()</code>是表明我们调用<code>run</code>只是为了它的副作用的惯用方式;它并没有返回什么有意义的值。</p>
<p>上述代码能够编译,不过会有一个警告:</p>
<pre><code>warning: unused result which must be used, #[warn(unused_must_use)] on by default
--&gt; src\main.rs:39:5
|
39 | run(config);
| ^^^^^^^^^^^^
</code></pre>
<p>Rust 尝试告诉我们忽略<code>Result</code>,它有可能是一个错误值。让我们现在来处理它。我们将采用类似于列表 12-9 中处理<code>Config::new</code>错误的技巧,不过还有少许不同:</p>
<p><span class="filename">Filename: src/main.rs</span></p>
<pre><code class="language-rust,ignore">fn main() {
// ...snip...
println!(&quot;Searching for {}&quot;, config.search);
println!(&quot;In file {}&quot;, config.filename);
if let Err(e) = run(config) {
println!(&quot;Application error: {}&quot;, e);
process::exit(1);
}
}
fn run(config: Config) -&gt; Result&lt;(), Box&lt;Error&gt;&gt; {
let mut f = File::open(config.filename)?;
let mut contents = String::new();
f.read_to_string(&amp;mut contents)?;
println!(&quot;With text:\n{}&quot;, contents);
Ok(())
}
</code></pre>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p>不同于<code>unwrap_or_else</code>,我们使用<code>if let</code>来检查<code>run</code>是否返回<code>Err</code>,如果是则调用<code>process::exit(1)</code>。为什么呢?这个例子和<code>Config::new</code>的区别有些微妙。对于<code>Config::new</code>我们关心两件事:</p>
<ol>
<li>检测出任何可能发生的错误</li>
<li>如果没有出现错误创建一个<code>Config</code></li>
</ol>
<p>而在这个情况下,因为<code>run</code>在成功的时候返回一个<code>()</code>,唯一需要担心的就是第一件事:检测错误。如果我们使用了<code>unwrap_or_else</code>,则会得到<code>()</code>的返回值。它并没有什么用处。</p>
<p>虽然两种情况下<code>if let</code><code>unwrap_or_else</code>的内容都是一样的:打印出错误并退出。</p>
<a class="header" href="#将代码拆分到库-crate" name="将代码拆分到库-crate"><h3>将代码拆分到库 crate</h3></a>
<p>现在项目看起来好多了!还有一件我们尚未开始的工作:拆分 <em>src/main.rs</em> 并将一些代码放入 <em>src/lib.rs</em> 中。让我们现在就开始吧:将 <em>src/main.rs</em> 中的<code>run</code>函数移动到新建的 <em>src/lib.rs</em> 中。还需要移动相关的<code>use</code>语句和<code>Config</code>的定义,以及其<code>new</code>方法。现在 <em>src/lib.rs</em> 应该如列表 12-12 所示:</p>
<figure>
<span class="filename">Filename: src/lib.rs</span>
<pre><code class="language-rust">use std::error::Error;
use std::fs::File;
use std::io::prelude::*;
pub struct Config {
pub search: String,
pub filename: String,
}
impl Config {
pub fn new(args: &amp;[String]) -&gt; Result&lt;Config, &amp;'static str&gt; {
if args.len() &lt; 3 {
return Err(&quot;not enough arguments&quot;);
}
let search = args[1].clone();
let filename = args[2].clone();
Ok(Config {
search: search,
filename: filename,
})
}
}
pub fn run(config: Config) -&gt; Result&lt;(), Box&lt;Error&gt;&gt;{
let mut f = File::open(config.filename)?;
let mut contents = String::new();
f.read_to_string(&amp;mut contents)?;
println!(&quot;With text:\n{}&quot;, contents);
Ok(())
}
</code></pre>
<figcaption>
<p>Listing 12-12: Moving <code>Config</code> and <code>run</code> into <em>src/lib.rs</em></p>
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p>注意我们还需要使用公有的<code>pub</code>:在<code>Config</code>和其字段、它的<code>new</code>方法和<code>run</code>函数上。</p>
<p>现在在 <em>src/main.rs</em> 中,我们需要通过<code>extern crate greprs</code>来引入现在位于 <em>src/lib.rs</em> 的代码。接着需要增加一行<code>use greprs::Config</code>来引入<code>Config</code>到作用域,并对<code>run</code>函数加上 crate 名称前缀,如列表 12-13 所示:</p>
<figure>
<span class="filename">Filename: src/main.rs</span>
<pre><code class="language-rust,ignore">extern crate greprs;
use std::env;
use std::process;
use greprs::Config;
fn main() {
let args: Vec&lt;String&gt; = env::args().collect();
let config = Config::new(&amp;args).unwrap_or_else(|err| {
println!(&quot;Problem parsing arguments: {}&quot;, err);
process::exit(1);
});
println!(&quot;Searching for {}&quot;, config.search);
println!(&quot;In file {}&quot;, config.filename);
if let Err(e) = greprs::run(config) {
println!(&quot;Application error: {}&quot;, e);
process::exit(1);
}
}
</code></pre>
<figcaption>
<p>Listing 12-13: Bringing the <code>greprs</code> crate into the scope of <em>src/main.rs</em></p>
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p>通过这些重构,所有代码应该都能运行了。运行几次<code>cargo run</code>来确保你没有破坏什么内容。好的!确实有很多的内容,不过已经为将来的成功奠定了基础。我们采用了一种更加优秀的方式来处理错误,并使得代码更模块化了一些。从现在开始几乎所有的工作都将在 <em>src/lib.rs</em> 中进行。</p>
<p>让我们利用这新创建的模块的优势来进行一些在旧代码中难以开开展的工作,他们在新代码中却很简单:编写测试!</p>
<a class="header" href="#测试库的功能" name="测试库的功能"><h2>测试库的功能</h2></a>
<blockquote>
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-04-testing-the-librarys-functionality.md">ch12-04-testing-the-librarys-functionality.md</a>
<br>
commit 4f2dc564851dc04b271a2260c834643dfd86c724</p>
</blockquote>
<p>现在为项目的核心功能编写测试将更加容易,因为我们将逻辑提取到了 <em>src/lib.rs</em> 中并将参数解析和错误处理都留在了 <em>src/main.rs</em> 里。现在我们可以直接使用多种参数调用代码并检查返回值而不用从命令行运行二进制文件了。</p>
<p>我们将要编写的是一个叫做<code>grep</code>的函数,它获取要搜索的项以及文本并产生一个搜索结果列表。让我们从<code>run</code>中去掉那行<code>println!</code>(也去掉 <em>src/main.rs</em> 中的,因为再也不需要他们了),并使用之前收集的选项来调用新的<code>grep</code>函数。眼下我们只增加一个空的实现,和指定<code>grep</code>期望行为的测试。当然,这个测试对于空的实现来说是会失败的,不过可以确保代码是可以编译的并得到期望的错误信息。列表 12-14 展示了这些修改:</p>
<figure>
<span class="filename">Filename: src/lib.rs</span>
<pre><code class="language-rust"># use std::error::Error;
# use std::fs::File;
# use std::io::prelude::*;
#
# pub struct Config {
# pub search: String,
# pub filename: String,
# }
#
// ...snip...
fn grep&lt;'a&gt;(search: &amp;str, contents: &amp;'a str) -&gt; Vec&lt;&amp;'a str&gt; {
vec![]
}
pub fn run(config: Config) -&gt; Result&lt;(), Box&lt;Error&gt;&gt;{
let mut f = File::open(config.filename)?;
let mut contents = String::new();
f.read_to_string(&amp;mut contents)?;
grep(&amp;config.search, &amp;contents);
Ok(())
}
#[cfg(test)]
mod test {
use grep;
#[test]
fn one_result() {
let search = &quot;duct&quot;;
let contents = &quot;\
Rust:
safe, fast, productive.
Pick three.&quot;;
assert_eq!(
vec![&quot;safe, fast, productive.&quot;],
grep(search, contents)
);
}
}
</code></pre>
<figcaption>
<p>Listing 12-14: Creating a function where our logic will go and a failing test
for that function</p>
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p>注意需要在<code>grep</code>的签名中显式声明声明周期<code>'a</code>并用于<code>contents</code>参数和返回值。记住,生命周期参数用于指定函数参数于返回值的生命周期的关系。在这个例子中,我们表明返回的 vector 将包含引用参数<code>contents</code>的字符串 slice而不是引用参数<code>search</code>的字符串 slice。换一种说法就是我们告诉 Rust 函数<code>grep</code>返回的数据将和传递给它的参数<code>contents</code>的数据存活的同样久。这是非常重要的!考虑为了使引用有效则 slice 引用的数据也需要保持有效,如果编译器认为我们是在创建<code>search</code>而不是<code>contents</code>的 slice那么安全检查将是不正确的。如果尝试不用生命周期编译的话我们将得到如下错误</p>
<pre><code>error[E0106]: missing lifetime specifier
--&gt; src\lib.rs:37:46
|
37 | fn grep(search: &amp;str, contents: &amp;str) -&gt; Vec&lt;&amp;str&gt; {
| ^ expected lifetime parameter
|
= help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `search` or
`contents`
</code></pre>
<p>Rust 不可能知道我们需要的是哪一个参数,所以需要告诉它。因为参数<code>contents</code>包含了所有的文本而且我们希望返回匹配的那部分文本,而我们知道<code>contents</code>是应该要使用生命周期语法来与返回值相关联的参数。</p>
<p>在函数签名中将参数与返回值相关联是其他语言不会让你做的工作,所以不用担心这感觉很奇怪!掌握如何指定生命周期会随着时间的推移越来越容易,熟能生巧。你可能想要重新阅读上一部分或返回与第十章中生命周期语法部分的例子做对比。</p>
<p>现在试试运行测试:</p>
<pre><code>$ cargo test
...warnings...
Finished debug [unoptimized + debuginfo] target(s) in 0.43 secs
Running target/debug/deps/greprs-abcabcabc
running 1 test
test test::one_result ... FAILED
failures:
---- test::one_result stdout ----
thread 'test::one_result' panicked at 'assertion failed: `(left == right)`
(left: `[&quot;safe, fast, productive.&quot;]`, right: `[]`)', src/lib.rs:16
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
test::one_result
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
error: test failed
</code></pre>
<p>好的,测试失败了,这正是我们所期望的。修改代码来让测试通过吧!之所以会失败是因为我们总是返回一个空的 vector。如下是如何实现<code>grep</code>的步骤:</p>
<ol>
<li>遍历每一行文本。</li>
<li>查看这一行是否包含要搜索的字符串。
<ul>
<li>如果有,将这一行加入返回列表中</li>
<li>如果没有,什么也不做</li>
</ul>
</li>
<li>返回匹配到的列表</li>
</ol>
<p>让我们一步一步的来,从遍历每行开始。字符串类型有一个有用的方法来处理这种情况,它刚好叫做<code>lines</code></p>
<p><span class="filename">Filename: src/lib.rs</span></p>
<pre><code class="language-rust,ignore">fn grep&lt;'a&gt;(search: &amp;str, contents: &amp;'a str) -&gt; Vec&lt;&amp;'a str&gt; {
for line in contents.lines() {
// do something with line
}
}
</code></pre>
<!-- Will add wingdings in libreoffice /Carol -->
<p>我们使用了一个<code>for</code>循环和<code>lines</code>方法来依次获得每一行。接下来,让我们看看这些行是否包含要搜索的字符串。幸运的是,字符串类型为此也有一个有用的方法<code>contains</code><code>contains</code>的用法看起来像这样:</p>
<p><span class="filename">Filename: src/lib.rs</span></p>
<pre><code class="language-rust,ignore">fn grep&lt;'a&gt;(search: &amp;str, contents: &amp;'a str) -&gt; Vec&lt;&amp;'a str&gt; {
for line in contents.lines() {
if line.contains(search) {
// do something with line
}
}
}
</code></pre>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p>最终,我们需要一个方法来存储包含要搜索字符串的行。为此可以在<code>for</code>循环之前创建一个可变的 vector 并调用<code>push</code>方法来存放一个<code>line</code>。在<code>for</code>循环之后,返回这个 vector。列表 12-15 中为完整的实现:</p>
<figure>
<span class="filename">Filename: src/lib.rs</span>
<pre><code class="language-rust">fn grep&lt;'a&gt;(search: &amp;str, contents: &amp;'a str) -&gt; Vec&lt;&amp;'a str&gt; {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(search) {
results.push(line);
}
}
results
}
</code></pre>
<figcaption>
<p>Listing 12-15: Fully functioning implementation of the <code>grep</code> function</p>
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p>尝试运行一下:</p>
<pre><code>$ cargo test
running 1 test
test test::one_result ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Running target/debug/greprs-2f55ee8cd1721808
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
Doc-tests greprs
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
</code></pre>
<p>非常好!它可以工作了。现在测试通过了,我们可以考虑一下重构<code>grep</code>的实现并时刻保持其功能不变。这些代码并不坏,不过并没有利用迭代器的一些实用功能。第十三章将回到这个例子并探索迭代器和如何改进代码。</p>
<p>现在<code>grep</code>函数是可以工作的,我们还需在在<code>run</code>函数中做最后一件事:还没有打印出结果呢!增加一个<code>for</code>循环来打印出<code>grep</code>函数返回的每一行:</p>
<p><span class="filename">Filename: src/lib.rs</span></p>
<pre><code class="language-rust,ignore">pub fn run(config: Config) -&gt; Result&lt;(), Box&lt;Error&gt;&gt; {
let mut f = File::open(config.filename)?;
let mut contents = String::new();
f.read_to_string(&amp;mut contents)?;
for line in grep(&amp;config.search, &amp;contents) {
println!(&quot;{}&quot;, line);
}
Ok(())
}
</code></pre>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p>现在程序应该能正常运行了!试试吧:</p>
<pre><code>$ cargo run the poem.txt
Compiling greprs v0.1.0 (file:///projects/greprs)
Finished debug [unoptimized + debuginfo] target(s) in 0.38 secs
Running `target\debug\greprs.exe the poem.txt`
Then there's a pair of us - don't tell!
To tell your name the livelong day
$ cargo run a poem.txt
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target\debug\greprs.exe a poem.txt`
I'm nobody! Who are you?
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!
</code></pre>
<p>好极了!我们创建了一个属于自己的经典工具,并学习了很多如何组织程序的知识。我们还学习了一些文件输入输出、生命周期、测试和命令行解析的内容。</p>
<a class="header" href="#处理环境变量" name="处理环境变量"><h2>处理环境变量</h2></a>
<blockquote>
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-05-working-with-environment-variables.md">ch12-05-working-with-environment-variables.md</a>
<br>
commit 4f2dc564851dc04b271a2260c834643dfd86c724</p>
</blockquote>
<p>让我们再增加一个功能:大小写不敏感搜索。另外,这个设定将不是一个命令行参数:相反它将是一个环境变量。当然可以选择创建一个大小写不敏感的命令行参数,不过用户要求提供一个环境变量这样设置一次之后在整个终端会话中所有的搜索都将是大小写不敏感的了。</p>
<a class="header" href="#实现并测试一个大小写不敏感grep函数" name="实现并测试一个大小写不敏感grep函数"><h3>实现并测试一个大小写不敏感<code>grep</code>函数</h3></a>
<p>首先,让我们增加一个新函数,当设置了环境变量时会调用它。增加一个新测试并重命名已经存在的那个:</p>
<pre><code class="language-rust,ignore">#[cfg(test)]
mod test {
use {grep, grep_case_insensitive};
#[test]
fn case_sensitive() {
let search = &quot;duct&quot;;
let contents = &quot;\
Rust:
safe, fast, productive.
Pick three.
Duct tape.&quot;;
assert_eq!(
vec![&quot;safe, fast, productive.&quot;],
grep(search, contents)
);
}
#[test]
fn case_insensitive() {
let search = &quot;rust&quot;;
let contents = &quot;\
Rust:
safe, fast, productive.
Pick three.
Trust me.&quot;;
assert_eq!(
vec![&quot;Rust:&quot;, &quot;Trust me.&quot;],
grep_case_insensitive(search, contents)
);
}
}
</code></pre>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p>我们将定义一个叫做<code>grep_case_insensitive</code>的新函数。它的实现与<code>grep</code>函数大体上相似,不过列表 12-16 展示了一些小的区别:</p>
<figure>
<span class="filename">Filename: src/lib.rs</span>
<pre><code class="language-rust">fn grep_case_insensitive&lt;'a&gt;(search: &amp;str, contents: &amp;'a str) -&gt; Vec&lt;&amp;'a str&gt; {
let search = search.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&amp;search) {
results.push(line);
}
}
results
}
</code></pre>
<figcaption>
<p>Listing 12-16: Implementing a <code>grep_case_insensitive</code> function by changing the
search string and the lines of the contents to lowercase before comparing them</p>
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
<p>首先,将<code>search</code>字符串转换为小写,并存放于一个同名的覆盖变量中。注意现在<code>search</code>是一个<code>String</code>而不是字符串 slice所以在将<code>search</code>传递给<code>contains</code>时需要加上 &amp;,因为<code>contains</code>获取一个字符串 slice。</p>
</div>

View File

@ -340,3 +340,386 @@ Listing 12-8: Return a `Result` from `Config::new`
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
现在`new`函数返回一个`Result`,在成功时带有一个`Config`实例而在出现错误时带有一个`&'static str`。回忆一下第十章“静态声明周期”中讲到`&'static str`是一个字符串字面值,他也是现在我们的错误信息。
`new`函数体中有两处修改:当没有足够参数时不再调用`panic!`,而是返回`Err`值。同时我们将`Config`返回值包装进`Ok`成员中。这些修改使得函数符合其新的类型签名。
### `Config::new`调用和错误处理
现在我们需要对`main`做一些修改,如列表 12-9 所示:
<figure>
<span class="filename">Filename: src/main.rs</span>
```rust
# use std::env;
# use std::fs::File;
# use std::io::prelude::*;
// ...snip...
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {}", err);
process::exit(1);
});
println!("Searching for {}", config.search);
println!("In file {}", config.filename);
// ...snip...
#
# let mut f = File::open(config.filename).expect("file not found");
#
# let mut contents = String::new();
# f.read_to_string(&mut contents).expect("something went wrong reading the file");
#
# println!("With text:\n{}", contents);
# }
#
# struct Config {
# search: String,
# filename: String,
# }
#
# impl Config {
# fn new(args: &[String]) -> Result<Config, &'static str> {
# if args.len() < 3 {
# return Err("not enough arguments");
# }
#
# let search = args[1].clone();
# let filename = args[2].clone();
#
# Ok(Config {
# search: search,
# filename: filename,
# })
# }
# }
```
<figcaption>
Listing 12-9: Exiting with an error code if creating a new `Config` fails
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
新增了一个`use`行来从标准库中导入`process`。在`main`函数中我们将处理`new`函数返回的`Result`值,并在其返回`Config::new`时以一种更加清楚的方式结束进程。
这里使用了一个之前没有讲到的标准库中定义的`Result<T, E>`的方法:`unwrap_or_else`。当`Result`是`Ok`时其行为类似于`unwrap`:它返回`Ok`内部封装的值。与`unwrap`不同的是,当`Result`是`Err`时,它调用一个**闭包***closure*),也就是一个我们定义的作为参数传递给`unwrap_or_else`的匿名函数。第XX章会更详细的介绍闭包这里需要理解的重要部分是`unwrap_or_else`会将`Err`的内部值传递给闭包中位于两道竖线间的参数`err`。使用`unwrap_or_else`允许我们进行一些自定义的非`panic!`的错误处理。
上述的错误处理其实只有两行:我们打印出了错误,接着调用了`std::process::exit`。这个函数立刻停止程序的执行并将传递给它的数组作为返回码。依照惯例,零代表成功而任何其他数字表示失败。就结果来说这依然类似于列表 12-7 中的基于`panic!`的错误处理,但是不再会有额外的输出了,让我们试一试:
```text
$ cargo run
Compiling greprs v0.1.0 (file:///projects/greprs)
Finished debug [unoptimized + debuginfo] target(s) in 0.48 secs
Running `target\debug\greprs.exe`
Problem parsing arguments: not enough arguments
```
非常好!现在输出就友好多了。
### `run`函数中的错误处理
现在重构完了参数解析部分,让我们再改进一下程序的逻辑。列表 12-10 中展示了在`main`函数中调用提取出函数`run`之后的代码。`run`函数包含之前位于`main`中的部分代码:
<figure>
<span class="filename">Filename: src/main.rs</span>
```rust
# use std::env;
# use std::fs::File;
# use std::io::prelude::*;
# use std::process;
#
fn main() {
# let args: Vec<String> = env::args().collect();
#
# let config = Config::new(&args).unwrap_or_else(|err| {
# println!("Problem parsing arguments: {}", err);
# process::exit(1);
# });
// ...snip...
println!("Searching for {}", config.search);
println!("In file {}", config.filename);
run(config);
}
fn run(config: Config) {
let mut f = File::open(config.filename).expect("file not found");
let mut contents = String::new();
f.read_to_string(&mut contents).expect("something went wrong reading the file");
println!("With text:\n{}", contents);
}
// ...snip...
#
# struct Config {
# search: String,
# filename: String,
# }
#
# impl Config {
# fn new(args: &[String]) -> Result<Config, &'static str> {
# if args.len() < 3 {
# return Err("not enough arguments");
# }
#
# let search = args[1].clone();
# let filename = args[2].clone();
#
# Ok(Config {
# search: search,
# filename: filename,
# })
# }
# }
```
<figcaption>
Listing 12-10: Extracting a `run` functionality for the rest of the program logic
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
`run`函数的内容是之前位于`main`中的几行,而且`run`函数获取一个`Config`作为参数。现在有了一个单独的函数了,我们就可以像列表 12-8 中的`Config::new`那样进行类似的改进了。列表 12-11 展示了另一个`use`语句将`std::error::Error`结构引入了作用域,还有使`run`函数返回`Result`的修改:
<figure>
<span class="filename">Filename: src/main.rs</span>
```rust
use std::error::Error;
# use std::env;
# use std::fs::File;
# use std::io::prelude::*;
# use std::process;
// ...snip...
# fn main() {
# let args: Vec<String> = env::args().collect();
#
# let config = Config::new(&args).unwrap_or_else(|err| {
# println!("Problem parsing arguments: {}", err);
# process::exit(1);
# });
#
# println!("Searching for {}", config.search);
# println!("In file {}", config.filename);
#
# run(config);
#
# }
fn run(config: Config) -> Result<(), Box<Error>> {
let mut f = File::open(config.filename)?;
let mut contents = String::new();
f.read_to_string(&mut contents)?;
println!("With text:\n{}", contents);
Ok(())
}
#
# struct Config {
# search: String,
# filename: String,
# }
#
# impl Config {
# fn new(args: &[String]) -> Result<Config, &'static str> {
# if args.len() < 3 {
# return Err("not enough arguments");
# }
#
# let search = args[1].clone();
# let filename = args[2].clone();
#
# Ok(Config {
# search: search,
# filename: filename,
# })
# }
# }
```
<figcaption>
Listing 12-11: Changing the `run` function to return `Result`
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
这里有三个大的修改。第一个是现在`run`函数的返回值是`Result<(), Box<Error>>`类型的。之前,函数返回 unit 类型`()`,现在它仍然是`Ok`时的返回值。对于错误类型,我们将使用`Box<Error>`。这是一个**trait 对象***trait object*第XX章会讲到。现在可以这样理解它`Box<Error>`意味着函数返回了某个实现了`Error` trait 的类型,不过并没有指定具体的返回值类型。这样就比较灵活,因为在不同的错误场景可能有不同类型的错误返回值。`Box`是一个堆数据的智能指针第YY章将会详细介绍`Box`。
第二个改变是我们去掉了`expect`调用并替换为第9章讲到的`?`。不同于遇到错误就`panic!`,这会从函数中返回错误值并让调用者来处理它。
第三个修改是现在成功时这个函数会返回一个`Ok`值。因为`run`函数签名中声明成功类型返回值是`()`,所以需要将 unit 类型值包装进`Ok`值中。`Ok(())`一开始看起来有点奇怪,不过这样使用`()`是表明我们调用`run`只是为了它的副作用的惯用方式;它并没有返回什么有意义的值。
上述代码能够编译,不过会有一个警告:
```
warning: unused result which must be used, #[warn(unused_must_use)] on by default
--> src\main.rs:39:5
|
39 | run(config);
| ^^^^^^^^^^^^
```
Rust 尝试告诉我们忽略`Result`,它有可能是一个错误值。让我们现在来处理它。我们将采用类似于列表 12-9 中处理`Config::new`错误的技巧,不过还有少许不同:
<span class="filename">Filename: src/main.rs</span>
```rust,ignore
fn main() {
// ...snip...
println!("Searching for {}", config.search);
println!("In file {}", config.filename);
if let Err(e) = run(config) {
println!("Application error: {}", e);
process::exit(1);
}
}
fn run(config: Config) -> Result<(), Box<Error>> {
let mut f = File::open(config.filename)?;
let mut contents = String::new();
f.read_to_string(&mut contents)?;
println!("With text:\n{}", contents);
Ok(())
}
```
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
不同于`unwrap_or_else`,我们使用`if let`来检查`run`是否返回`Err`,如果是则调用`process::exit(1)`。为什么呢?这个例子和`Config::new`的区别有些微妙。对于`Config::new`我们关心两件事:
1. 检测出任何可能发生的错误
2. 如果没有出现错误创建一个`Config`
而在这个情况下,因为`run`在成功的时候返回一个`()`,唯一需要担心的就是第一件事:检测错误。如果我们使用了`unwrap_or_else`,则会得到`()`的返回值。它并没有什么用处。
虽然两种情况下`if let`和`unwrap_or_else`的内容都是一样的:打印出错误并退出。
### 将代码拆分到库 crate
现在项目看起来好多了!还有一件我们尚未开始的工作:拆分 *src/main.rs* 并将一些代码放入 *src/lib.rs* 中。让我们现在就开始吧:将 *src/main.rs* 中的`run`函数移动到新建的 *src/lib.rs* 中。还需要移动相关的`use`语句和`Config`的定义,以及其`new`方法。现在 *src/lib.rs* 应该如列表 12-12 所示:
<figure>
<span class="filename">Filename: src/lib.rs</span>
```rust
use std::error::Error;
use std::fs::File;
use std::io::prelude::*;
pub struct Config {
pub search: String,
pub filename: String,
}
impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let search = args[1].clone();
let filename = args[2].clone();
Ok(Config {
search: search,
filename: filename,
})
}
}
pub fn run(config: Config) -> Result<(), Box<Error>>{
let mut f = File::open(config.filename)?;
let mut contents = String::new();
f.read_to_string(&mut contents)?;
println!("With text:\n{}", contents);
Ok(())
}
```
<figcaption>
Listing 12-12: Moving `Config` and `run` into *src/lib.rs*
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
注意我们还需要使用公有的`pub`:在`Config`和其字段、它的`new`方法和`run`函数上。
现在在 *src/main.rs* 中,我们需要通过`extern crate greprs`来引入现在位于 *src/lib.rs* 的代码。接着需要增加一行`use greprs::Config`来引入`Config`到作用域,并对`run`函数加上 crate 名称前缀,如列表 12-13 所示:
<figure>
<span class="filename">Filename: src/main.rs</span>
```rust,ignore
extern crate greprs;
use std::env;
use std::process;
use greprs::Config;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {}", err);
process::exit(1);
});
println!("Searching for {}", config.search);
println!("In file {}", config.filename);
if let Err(e) = greprs::run(config) {
println!("Application error: {}", e);
process::exit(1);
}
}
```
<figcaption>
Listing 12-13: Bringing the `greprs` crate into the scope of *src/main.rs*
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
通过这些重构,所有代码应该都能运行了。运行几次`cargo run`来确保你没有破坏什么内容。好的!确实有很多的内容,不过已经为将来的成功奠定了基础。我们采用了一种更加优秀的方式来处理错误,并使得代码更模块化了一些。从现在开始几乎所有的工作都将在 *src/lib.rs* 中进行。
让我们利用这新创建的模块的优势来进行一些在旧代码中难以开开展的工作,他们在新代码中却很简单:编写测试!

View File

@ -0,0 +1,250 @@
## 测试库的功能
> [ch12-04-testing-the-librarys-functionality.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-04-testing-the-librarys-functionality.md)
> <br>
> commit 4f2dc564851dc04b271a2260c834643dfd86c724
现在为项目的核心功能编写测试将更加容易,因为我们将逻辑提取到了 *src/lib.rs* 中并将参数解析和错误处理都留在了 *src/main.rs* 里。现在我们可以直接使用多种参数调用代码并检查返回值而不用从命令行运行二进制文件了。
我们将要编写的是一个叫做`grep`的函数,它获取要搜索的项以及文本并产生一个搜索结果列表。让我们从`run`中去掉那行`println!`(也去掉 *src/main.rs* 中的,因为再也不需要他们了),并使用之前收集的选项来调用新的`grep`函数。眼下我们只增加一个空的实现,和指定`grep`期望行为的测试。当然,这个测试对于空的实现来说是会失败的,不过可以确保代码是可以编译的并得到期望的错误信息。列表 12-14 展示了这些修改:
<figure>
<span class="filename">Filename: src/lib.rs</span>
```rust
# use std::error::Error;
# use std::fs::File;
# use std::io::prelude::*;
#
# pub struct Config {
# pub search: String,
# pub filename: String,
# }
#
// ...snip...
fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
vec![]
}
pub fn run(config: Config) -> Result<(), Box<Error>>{
let mut f = File::open(config.filename)?;
let mut contents = String::new();
f.read_to_string(&mut contents)?;
grep(&config.search, &contents);
Ok(())
}
#[cfg(test)]
mod test {
use grep;
#[test]
fn one_result() {
let search = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(
vec!["safe, fast, productive."],
grep(search, contents)
);
}
}
```
<figcaption>
Listing 12-14: Creating a function where our logic will go and a failing test
for that function
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
注意需要在`grep`的签名中显式声明声明周期`'a`并用于`contents`参数和返回值。记住,生命周期参数用于指定函数参数于返回值的生命周期的关系。在这个例子中,我们表明返回的 vector 将包含引用参数`contents`的字符串 slice而不是引用参数`search`的字符串 slice。换一种说法就是我们告诉 Rust 函数`grep`返回的数据将和传递给它的参数`contents`的数据存活的同样久。这是非常重要的!考虑为了使引用有效则 slice 引用的数据也需要保持有效,如果编译器认为我们是在创建`search`而不是`contents`的 slice那么安全检查将是不正确的。如果尝试不用生命周期编译的话我们将得到如下错误
```
error[E0106]: missing lifetime specifier
--> src\lib.rs:37:46
|
37 | fn grep(search: &str, contents: &str) -> Vec<&str> {
| ^ expected lifetime parameter
|
= help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `search` or
`contents`
```
Rust 不可能知道我们需要的是哪一个参数,所以需要告诉它。因为参数`contents`包含了所有的文本而且我们希望返回匹配的那部分文本,而我们知道`contents`是应该要使用生命周期语法来与返回值相关联的参数。
在函数签名中将参数与返回值相关联是其他语言不会让你做的工作,所以不用担心这感觉很奇怪!掌握如何指定生命周期会随着时间的推移越来越容易,熟能生巧。你可能想要重新阅读上一部分或返回与第十章中生命周期语法部分的例子做对比。
现在试试运行测试:
```
$ cargo test
...warnings...
Finished debug [unoptimized + debuginfo] target(s) in 0.43 secs
Running target/debug/deps/greprs-abcabcabc
running 1 test
test test::one_result ... FAILED
failures:
---- test::one_result stdout ----
thread 'test::one_result' panicked at 'assertion failed: `(left == right)`
(left: `["safe, fast, productive."]`, right: `[]`)', src/lib.rs:16
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
test::one_result
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
error: test failed
```
好的,测试失败了,这正是我们所期望的。修改代码来让测试通过吧!之所以会失败是因为我们总是返回一个空的 vector。如下是如何实现`grep`的步骤:
1. 遍历每一行文本。
2. 查看这一行是否包含要搜索的字符串。
* 如果有,将这一行加入返回列表中
* 如果没有,什么也不做
3. 返回匹配到的列表
让我们一步一步的来,从遍历每行开始。字符串类型有一个有用的方法来处理这种情况,它刚好叫做`lines`
<span class="filename">Filename: src/lib.rs</span>
```rust,ignore
fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
// do something with line
}
}
```
<!-- Will add wingdings in libreoffice /Carol -->
我们使用了一个`for`循环和`lines`方法来依次获得每一行。接下来,让我们看看这些行是否包含要搜索的字符串。幸运的是,字符串类型为此也有一个有用的方法`contains``contains`的用法看起来像这样:
<span class="filename">Filename: src/lib.rs</span>
```rust,ignore
fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
if line.contains(search) {
// do something with line
}
}
}
```
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
最终,我们需要一个方法来存储包含要搜索字符串的行。为此可以在`for`循环之前创建一个可变的 vector 并调用`push`方法来存放一个`line`。在`for`循环之后,返回这个 vector。列表 12-15 中为完整的实现:
<figure>
<span class="filename">Filename: src/lib.rs</span>
```rust
fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(search) {
results.push(line);
}
}
results
}
```
<figcaption>
Listing 12-15: Fully functioning implementation of the `grep` function
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
尝试运行一下:
```
$ cargo test
running 1 test
test test::one_result ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Running target/debug/greprs-2f55ee8cd1721808
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
Doc-tests greprs
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
```
非常好!它可以工作了。现在测试通过了,我们可以考虑一下重构`grep`的实现并时刻保持其功能不变。这些代码并不坏,不过并没有利用迭代器的一些实用功能。第十三章将回到这个例子并探索迭代器和如何改进代码。
现在`grep`函数是可以工作的,我们还需在在`run`函数中做最后一件事:还没有打印出结果呢!增加一个`for`循环来打印出`grep`函数返回的每一行:
<span class="filename">Filename: src/lib.rs</span>
```rust,ignore
pub fn run(config: Config) -> Result<(), Box<Error>> {
let mut f = File::open(config.filename)?;
let mut contents = String::new();
f.read_to_string(&mut contents)?;
for line in grep(&config.search, &contents) {
println!("{}", line);
}
Ok(())
}
```
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
现在程序应该能正常运行了!试试吧:
```
$ cargo run the poem.txt
Compiling greprs v0.1.0 (file:///projects/greprs)
Finished debug [unoptimized + debuginfo] target(s) in 0.38 secs
Running `target\debug\greprs.exe the poem.txt`
Then there's a pair of us - don't tell!
To tell your name the livelong day
$ cargo run a poem.txt
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target\debug\greprs.exe a poem.txt`
I'm nobody! Who are you?
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!
```
好极了!我们创建了一个属于自己的经典工具,并学习了很多如何组织程序的知识。我们还学习了一些文件输入输出、生命周期、测试和命令行解析的内容。

View File

@ -0,0 +1,83 @@
## 处理环境变量
> [ch12-05-working-with-environment-variables.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-05-working-with-environment-variables.md)
> <br>
> commit 4f2dc564851dc04b271a2260c834643dfd86c724
让我们再增加一个功能:大小写不敏感搜索。另外,这个设定将不是一个命令行参数:相反它将是一个环境变量。当然可以选择创建一个大小写不敏感的命令行参数,不过用户要求提供一个环境变量这样设置一次之后在整个终端会话中所有的搜索都将是大小写不敏感的了。
### 实现并测试一个大小写不敏感`grep`函数
首先,让我们增加一个新函数,当设置了环境变量时会调用它。增加一个新测试并重命名已经存在的那个:
```rust,ignore
#[cfg(test)]
mod test {
use {grep, grep_case_insensitive};
#[test]
fn case_sensitive() {
let search = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(
vec!["safe, fast, productive."],
grep(search, contents)
);
}
#[test]
fn case_insensitive() {
let search = "rust";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
grep_case_insensitive(search, contents)
);
}
}
```
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
我们将定义一个叫做`grep_case_insensitive`的新函数。它的实现与`grep`函数大体上相似,不过列表 12-16 展示了一些小的区别:
<figure>
<span class="filename">Filename: src/lib.rs</span>
```rust
fn grep_case_insensitive<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
let search = search.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&search) {
results.push(line);
}
}
results
}
```
<figcaption>
Listing 12-16: Implementing a `grep_case_insensitive` function by changing the
search string and the lines of the contents to lowercase before comparing them
</figcaption>
</figure>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
首先,将`search`字符串转换为小写,并存放于一个同名的覆盖变量中。注意现在`search`是一个`String`而不是字符串 slice所以在将`search`传递给`contains`时需要加上 &,因为`contains`获取一个字符串 slice。