mirror of
https://github.com/KaiserY/trpl-zh-cn
synced 2025-02-23 12:42:26 +08:00
check to ch12-06
This commit is contained in:
parent
12d24ab7f0
commit
9a9b361e06
@ -1,10 +1,10 @@
|
||||
## trait:定义共享的行为
|
||||
|
||||
> [ch10-02-traits.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch10-02-traits.md)
|
||||
> [ch10-02-traits.md](https://github.com/rust-lang/book/blob/master/src/ch10-02-traits.md)
|
||||
> <br>
|
||||
> commit 131859023a0a6be67168d36dcdc8e2aa43f806fd
|
||||
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
|
||||
|
||||
trait 允许我们进行另一种抽象:他们让我们可以抽象类型所通用的行为。*trait* 告诉 Rust 编译器某个特定类型拥有可能与其他类型共享的功能。在使用泛型类型参数的场景中,可以使用 *trait bounds* 在编译时指定泛型可以是任何实现了某个 trait 的类型,并由此在这个场景下拥有我们希望的功能。
|
||||
*trait* 告诉 Rust 编译器某个特定类型拥有可能与其他类型共享的功能。可以通过 trait 以一种抽象的方式定义共享的行为。可以使用 *trait bounds* 指定泛型是任何拥有特定行为的类型。
|
||||
|
||||
> 注意:*trait* 类似于其他语言中的常被称为 **接口**(*interfaces*)的功能,虽然有一些不同。
|
||||
|
||||
@ -12,33 +12,35 @@ trait 允许我们进行另一种抽象:他们让我们可以抽象类型所
|
||||
|
||||
一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。
|
||||
|
||||
例如,这里有多个存放了不同类型和属性文本的结构体:结构体 `NewsArticle` 用于存放发生于世界各地的新闻故事,而结构体 `Tweet` 最多只能存放 140 个字符的内容,以及像是否转推或是否是对推友的回复这样的元数据。
|
||||
例如,这里有多个存放了不同类型和属性文本的结构体:结构体 `NewsArticle` 用于存放发生于世界各地的新闻故事,而结构体 `Tweet` 最多只能存放 280 个字符的内容,以及像是否转推或是否是对推友的回复这样的元数据。
|
||||
|
||||
我们想要创建一个多媒体聚合库用来显示可能储存在 `NewsArticle` 或 `Tweet` 实例中的数据的总结。每一个结构体都需要的行为是他们是能够被总结的,这样的话就可以调用实例的 `summary` 方法来请求总结。示例 10-12 中展示了一个表现这个概念的 `Summarizable` trait 的定义:
|
||||
我们想要创建一个多媒体聚合库用来显示可能储存在 `NewsArticle` 或 `Tweet` 实例中的数据的总结。每一个结构体都需要的行为是他们是能够被总结的,这样的话就可以调用实例的 `summarize` 方法来请求总结。示例 10-12 中展示了一个表现这个概念的 `Summary` trait 的定义:
|
||||
|
||||
<span class="filename">文件名: lib.rs</span>
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
pub trait Summarizable {
|
||||
fn summary(&self) -> String;
|
||||
pub trait Summary {
|
||||
fn summarize(&self) -> String;
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 10-12:`Summarizable` trait 定义,它包含由 `summary` 方法提供的行为</span>
|
||||
<span class="caption">示例 10-12:`Summarizable` trait 定义,它包含由 `summarize` 方法提供的行为</span>
|
||||
|
||||
使用 `trait` 关键字来声明一个 trait,后面是 trait 的名字,在这个例子中是 `Summarizable`。在大括号中声明描述实现这个 trait 的类型所需要的行为的方法签名,在这个例子中是 `fn summary(&self) -> String`。在方法签名后跟分号,而不是在大括号中提供其实现。接着每一个实现这个 trait 的类型都需要提供其自定义行为的方法体,编译器也会确保任何实现 `Summarizable` trait 的类型都拥有与这个签名的定义完全一致的 `summary` 方法。
|
||||
这里使用 `trait` 关键字来声明一个 trait,后面是 trait 的名字,在这个例子中是 `Summary`。在大括号中声明描述实现这个 trait 的类型所需要的行为的方法签名,在这个例子中是 `fn summarize(&self) -> String`。
|
||||
|
||||
trait 体中可以有多个方法,一行一个方法签名且都以分号结尾。
|
||||
在方法签名后跟分号,而不是在大括号中提供其实现。接着每一个实现这个 trait 的类型都需要提供其自定义行为的方法体,编译器也会确保任何实现 `Summary` trait 的类型都拥有与这个签名的定义完全一致的 `summarize` 方法。
|
||||
|
||||
trait 体中可以有多个方法:一行一个方法签名且都以分号结尾。
|
||||
|
||||
### 为类型实现 trait
|
||||
|
||||
现在我们定义了 `Summarizable` trait,接着就可以在多媒体聚合库中需要拥有这个行为的类型上实现它了。示例 10-13 中展示了 `NewsArticle` 结构体上 `Summarizable` trait 的一个实现,它使用标题、作者和创建的位置作为 `summary` 的返回值。对于 `Tweet` 结构体,我们选择将 `summary` 定义为用户名后跟推文的全部文本作为返回值,并假设推文内容已经被限制为 140 字符以内。
|
||||
现在我们定义了 `Summary` trait,接着就可以在多媒体聚合库中需要拥有这个行为的类型上实现它了。示例 10-13 中展示了 `NewsArticle` 结构体上 `Summary` trait 的一个实现,它使用标题、作者和创建的位置作为 `summarize` 的返回值。对于 `Tweet` 结构体,我们选择将 `summarize` 定义为用户名后跟推文的全部文本作为返回值,并假设推文内容已经被限制为 280 字符以内。
|
||||
|
||||
<span class="filename">文件名: lib.rs</span>
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
# pub trait Summarizable {
|
||||
# fn summary(&self) -> String;
|
||||
# pub trait Summary {
|
||||
# fn summarize(&self) -> String;
|
||||
# }
|
||||
#
|
||||
pub struct NewsArticle {
|
||||
@ -48,8 +50,8 @@ pub struct NewsArticle {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
impl Summarizable for NewsArticle {
|
||||
fn summary(&self) -> String {
|
||||
impl Summary for NewsArticle {
|
||||
fn summarize(&self) -> String {
|
||||
format!("{}, by {} ({})", self.headline, self.author, self.location)
|
||||
}
|
||||
}
|
||||
@ -61,14 +63,14 @@ pub struct Tweet {
|
||||
pub retweet: bool,
|
||||
}
|
||||
|
||||
impl Summarizable for Tweet {
|
||||
fn summary(&self) -> String {
|
||||
impl Summary for Tweet {
|
||||
fn summarize(&self) -> String {
|
||||
format!("{}: {}", self.username, self.content)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 10-13:在 `NewsArticle` 和 `Tweet` 类型上实现 `Summarizable` trait</span>
|
||||
<span class="caption">示例 10-13:在 `NewsArticle` 和 `Tweet` 类型上实现 `Summary` trait</span>
|
||||
|
||||
在类型上实现 trait 类似于实现与 trait 无关的方法。区别在于 `impl` 关键字之后,我们提供需要实现 trait 的名称,接着是 `for` 和需要实现 trait 的类型的名称。在 `impl` 块中,使用 trait 定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现 trait 方法所拥有的行为。
|
||||
|
||||
@ -82,66 +84,38 @@ let tweet = Tweet {
|
||||
retweet: false,
|
||||
};
|
||||
|
||||
println!("1 new tweet: {}", tweet.summary());
|
||||
println!("1 new tweet: {}", tweet.summarize());
|
||||
```
|
||||
|
||||
这会打印出 `1 new tweet: horse_ebooks: of course, as you probably already know, people`。
|
||||
|
||||
注意因为示例 10-13 中我们在相同的 `lib.rs` 里定义了 `Summarizable` trait 和 `NewsArticle` 与 `Tweet` 类型,所以他们是位于同一作用域的。如果这个 `lib.rs` 是对应 `aggregator` crate 的,而别人想要利用我们 crate 的功能外加为其 `WeatherForecast` 结构体实现 `Summarizable` trait,在实现 `Summarizable` trait 之前他们首先就需要将其导入其作用域中,如示例 10-14 所示:
|
||||
注意因为示例 10-13 中我们在相同的 *lib.rs* 里定义了 `Summary` trait 和 `NewsArticle` 与 `Tweet` 类型,所以他们是位于同一作用域的。如果这个 *lib.rs* 是对应 `aggregator` crate 的,而别人想要利用我们 crate 的功能为其自己的库作用域中的结构体实现 `Summary` trait。首先他们需要将 trait 引入作用域。这可以通过指定 `use aggregator::Summary;` 实现,这样就可以为其类型实现 `Summary` trait 了。`Summary` 还必须是公有 trait 使得其他 crate 可以实现它,这也是为什么实例 10-12 中将 `pub` 置于 `trait` 之前。
|
||||
|
||||
<span class="filename">文件名: lib.rs</span>
|
||||
一个实现 trait 时需要注意的限制是只有要么 tait 或者类型是位于 crate 作用域本地时才能为其实现该 trait。例如,可以为像 `aggregator` crate 的 `Tweet` 这样的自定义类型实现如标准库中的 `Display` 这样 trait,因为 `Tweet` 类型位于 `aggregator` crate 本地。也可以在 `aggregator` crate 中为 `Vec<T>` 实现 `Summary`,因为 `Summary` trait 位于 `aggregator` crate 本地。
|
||||
|
||||
```rust,ignore
|
||||
extern crate aggregator;
|
||||
|
||||
use aggregator::Summarizable;
|
||||
|
||||
struct WeatherForecast {
|
||||
high_temp: f64,
|
||||
low_temp: f64,
|
||||
chance_of_precipitation: f64,
|
||||
}
|
||||
|
||||
impl Summarizable for WeatherForecast {
|
||||
fn summary(&self) -> String {
|
||||
format!("The high will be {}, and the low will be {}. The chance of
|
||||
precipitation is {}%.", self.high_temp, self.low_temp,
|
||||
self.chance_of_precipitation)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 10-14:在另一个 crate 中将 `aggregator` crate 的 `Summarizable` trait 引入作用域</span>
|
||||
|
||||
另外这段代码假设 `Summarizable` 是一个公有 trait,这是因为示例 10-12 中 `trait` 之前使用了 `pub` 关键字。
|
||||
|
||||
trait 实现的一个需要注意的限制是:只能在 trait 或对应类型位于我们 crate 本地的时候为其实现 trait。换句话说,不允许对外部类型实现外部 trait。例如,不能在 `Vec` 上实现 `Display` trait,因为 `Display` 和 `Vec` 都定义于标准库中。允许在像 `Tweet` 这样作为我们 `aggregator`crate 部分功能的自定义类型上实现标准库中的 trait `Display`。也允许在 `aggregator`crate 中为 `Vec` 实现 `Summarizable`,因为 `Summarizable` 定义于此。这个限制是我们称为 **孤儿规则**(*orphan rule*)的一部分,如果你感兴趣的可以在类型理论中找到它。简单来说,它被称为 orphan rule 是因为其父类型不存在。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,因而这两个实现会相互冲突:Rust 将无从得知应该使用哪一个。因为 Rust 强制执行 orphan rule,其他人编写的代码不会破坏你代码,反之亦是如此。
|
||||
但是不能在外部类型上实现外部 trait。例如,不能在 `aggregator` crate 中为 `Vec<T>` 实现 `Display` trait。因为 `Display` 和 `Vec<T>` 都定义于标准库并不位于 `aggregator` crate 本地。这个限制是被称为 **相干性**(*coherence*) 的程序属性的一部分,或者更具体的说是 **孤儿规则**(*orphan rule*),其得名于不存在父类型。这条规则确保了其他人编写的代码不会破坏你代码,反之亦然。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,而Rust 将无从得知应该使用哪一个实现。
|
||||
|
||||
### 默认实现
|
||||
|
||||
有时为 trait 中的某些或全部方法提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。
|
||||
|
||||
示例 10-15 中展示了如何为 `Summarize` trait 的 `summary` 方法指定一个默认的字符串值,而不是像示例 10-12 中那样只是定义方法签名:
|
||||
示例 10-14 中展示了如何为 `Summary` trait 的 `summarize` 方法指定一个默认的字符串值,而不是像示例 10-12 中那样只是定义方法签名:
|
||||
|
||||
<span class="filename">文件名: lib.rs</span>
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
pub trait Summarizable {
|
||||
fn summary(&self) -> String {
|
||||
pub trait Summary {
|
||||
fn summarize(&self) -> String {
|
||||
String::from("(Read more...)")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 10-15:`Summarizable` trait 的定义,带有一个 `summary` 方法的默认实现</span>
|
||||
<span class="caption">示例 10-14:`Summary` trait 的定义,带有一个 `summarize` 方法的默认实现</span>
|
||||
|
||||
如果想要对 `NewsArticle` 实例使用这个默认实现,而不是像示例 10-13 中那样定义一个自己的实现,则可以指定一个空的 `impl` 块:
|
||||
如果想要对 `NewsArticle` 实例使用这个默认实现,而不是定义一个自己的实现,则可以通过 impl Summary for NewsArticle {} 指定一个空的 `impl` 块。
|
||||
|
||||
```rust,ignore
|
||||
impl Summarizable for NewsArticle {}
|
||||
```
|
||||
|
||||
即便选择不再直接为 `NewsArticle` 定义 `summary` 方法了,因为 `summary` 方法有一个默认实现而且 `NewsArticle` 被指定为实现了 `Summarizable` trait,我们仍然可以对 `NewsArticle` 的实例调用 `summary` 方法:
|
||||
即便选择不再直接为 `NewsArticle` 定义 `summarize` 方法了,因为我们提供了一个默认实现而且 `NewsArticle` 被指定为实现了 `Summary` trait。为此我们仍然可以像这样对 `NewsArticle` 的实例调用 `summarize` 方法:
|
||||
|
||||
```rust,ignore
|
||||
let article = NewsArticle {
|
||||
@ -152,36 +126,38 @@ let article = NewsArticle {
|
||||
hockey team in the NHL."),
|
||||
};
|
||||
|
||||
println!("New article available! {}", article.summary());
|
||||
println!("New article available! {}", article.summarize());
|
||||
```
|
||||
|
||||
这段代码会打印 `New article available! (Read more...)`。
|
||||
|
||||
为 `summarize` 创建默认实现并不要求对示例 10-13 中 `Tweet` 上的 `Summary` 实现做任何改变。其原因是重载一个默认实现的语法与实现没有默认实现的 trait 方法一样。
|
||||
|
||||
将 `Summarizable` trait 改变为拥有默认 `summary` 实现并不要求对示例 10-13 中 `Tweet` 和示例 10-14 中 `WeatherForecast` 的 `Summarizable` 实现做任何改变:重载一个默认实现的语法与实现没有默认实现的 trait 方法时完全一样的。
|
||||
|
||||
默认实现允许调用相同 trait 中的其他方法,哪怕这些方法没有默认实现。通过这种方法,trait 可以实现很多有用的功能而只需实现一小部分特定内容。我们可以选择让`Summarizable` trait 也拥有一个要求实现的`author_summary` 方法,接着 `summary` 方法则提供默认实现并调用 `author_summary` 方法:
|
||||
默认实现允许调用相同 trait 中的其他方法,哪怕这些方法没有默认实现。如此,trait 可以实现很多有用的功能而只需实现一小部分特定内容。我们可以选择让`Summary` trait 也拥有一个要求实现的`summarize_author` 方法,接着 `summarize` 方法则提供默认实现并调用 `summarize_author` 方法:
|
||||
|
||||
```rust
|
||||
pub trait Summarizable {
|
||||
fn author_summary(&self) -> String;
|
||||
pub trait Summary {
|
||||
fn summarize_author(&self) -> String;
|
||||
|
||||
fn summary(&self) -> String {
|
||||
format!("(Read more from {}...)", self.author_summary())
|
||||
fn summarize(&self) -> String {
|
||||
format!("(Read more from {}...)", self.summarize_author())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
为了使用这个版本的 `Summarizable`,只需在实现 trait 时定义 `author_summary` 即可:
|
||||
为了使用这个版本的 `Summary`,只需在实现 trait 时定义 `summarize_author` 即可:
|
||||
|
||||
```rust,ignore
|
||||
impl Summarizable for Tweet {
|
||||
fn author_summary(&self) -> String {
|
||||
impl Summary for Tweet {
|
||||
fn summarize_author(&self) -> String {
|
||||
format!("@{}", self.username)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
一旦定义了 `author_summary`,我们就可以对 `Tweet` 结构体的实例调用 `summary` 了,而 `summary` 的默认实现会调用我们提供的 `author_summary` 定义。
|
||||
一旦定义了 `summarize_author`,我们就可以对 `Tweet` 结构体的实例调用 `summarize` 了,而 `summary` 的默认实现会调用我们提供的 `summarize_author` 定义。因为实现了 `summarize_author`,`Summary` trait 就提供了 `summarize` 方法的功能,且无需编写更多的代码。
|
||||
|
||||
```rust,ignore
|
||||
let tweet = Tweet {
|
||||
@ -191,37 +167,74 @@ let tweet = Tweet {
|
||||
retweet: false,
|
||||
};
|
||||
|
||||
println!("1 new tweet: {}", tweet.summary());
|
||||
println!("1 new tweet: {}", tweet.summarize());
|
||||
```
|
||||
|
||||
这会打印出 `1 new tweet: (Read more from @horse_ebooks...)`。
|
||||
|
||||
注意在重载过的实现中调用默认实现是不可能的。
|
||||
注意无法从相同方法的重载实现中调用默认方法。
|
||||
|
||||
### Trait Bounds
|
||||
### trait 作为参数
|
||||
|
||||
现在我们定义了 trait 并在类型上实现了这些 trait,也可以对泛型类型参数使用 trait。我们可以限制泛型不再适用于任何类型,编译器会确保其被限制为那些实现了特定 trait 的类型,由此泛型就会拥有我们希望其类型所拥有的功能。这被称为指定泛型的 *trait bounds*。
|
||||
知道了如何定义 trait 和在类型上实现这些 trait 之后,我们可以探索一下如何使用 trait 来接受多种不同类型的参数。
|
||||
|
||||
例如在示例 10-13 中为 `NewsArticle` 和 `Tweet` 类型实现了 `Summarizable` trait。我们可以定义一个函数 `notify` 来调用 `summary` 方法,它拥有一个泛型类型 `T` 的参数 `item`。为了能够在 `item` 上调用 `summary` 而不出现错误,我们可以在 `T` 上使用 trait bounds 来指定 `item` 必须是实现了 `Summarizable` trait 的类型:
|
||||
例如在示例 10-13 中为 `NewsArticle` 和 `Tweet` 类型实现了 `Summary` trait。我们可以定义一个函数 `notify` 来调用其参数 `item` 上的 `summarize` 方法,该参数为一些实现了 `Summary` trait 的方法。为此可以使用 ‘`impl Trait`’ 语法,像这样:
|
||||
|
||||
```rust,ignore
|
||||
pub fn notify<T: Summarizable>(item: T) {
|
||||
println!("Breaking news! {}", item.summary());
|
||||
pub fn notify(item: impl Summary) {
|
||||
println!("Breaking news! {}", item.summarize());
|
||||
}
|
||||
```
|
||||
|
||||
trait bounds 连同泛型类型参数声明一同出现,位于尖括号中的冒号后面。由于 `T` 上的 trait bounds,我们可以传递任何 `NewsArticle` 或 `Tweet` 的实例来调用 `notify` 函数。示例 10-14 中使用我们 `aggregator` crate 的外部代码也可以传递一个 `WeatherForecast` 的实例来调用 `notify` 函数,因为 `WeatherForecast` 同样也实现了 `Summarizable`。使用任何其他类型,比如 `String` 或 `i32`,来调用 `notify` 的代码将不能编译,因为这些类型没有实现 `Summarizable`。
|
||||
在 `notify` 函数体中,可以调用任何来自 `Summary` trait 的方法,比如 `summarize`。
|
||||
|
||||
可以通过 `+` 来为泛型指定多个 trait bounds。如果我们需要能够在函数中使用 `T` 类型的显示格式的同时也能使用 `summary` 方法,则可以使用 trait bounds `T: Summarizable + Display`。这意味着 `T` 可以是任何实现了 `Summarizable` 和 `Display` 的类型。
|
||||
#### Trait Bounds
|
||||
|
||||
对于拥有多个泛型类型参数的函数,每一个泛型都可以有其自己的 trait bounds。在函数名和参数列表之间的尖括号中指定很多的 trait bound 信息将是难以阅读的,所以有另外一个指定 trait bounds 的语法,它将其移动到函数签名后的 `where` 从句中。所以相比这样写:
|
||||
`impl Trait` 语法适用于短小的例子,它不过是一个较长形式的语法糖。这被称为 *trait bound*,这看起来像:
|
||||
|
||||
```rust,ignore
|
||||
pub fn notify<T: Summary>(item: T) {
|
||||
println!("Breaking news! {}", item.summarize());
|
||||
}
|
||||
```
|
||||
|
||||
这与之前的例子相同,不过稍微冗长了一些。trait bound 与泛型参数声明在一起,位于尖括号中分号的后面。因为 `T` 的 trait bound,我们可以传递任何 `NewsArticle` 或 `Tweet` 的实例调用 `notify`。用任何其他类型,比如 `String` 或 `i32`,调用该函数的代码将不能编译,因为这些类型没有实现 `Summary`。
|
||||
|
||||
何时应该使用这种形式而不是 `impl Trait` 呢?虽然 `impl Trait` 适用于短小的例子,trait bound 则适用于更复杂的场景。例如,比如需要获取两个实现了 `Summary` 的类型:
|
||||
|
||||
```rust,ignore
|
||||
pub fn notify(item1: impl Summary, item2: impl Summary) {
|
||||
```
|
||||
|
||||
这适用于 `item1` 和 `item2` 允许是不同类型的情况(只要它们都实现了 `Summary`)。不过如果你希望强制它们都是相同类型呢?这只有在使用 trait bound 时才有可能:
|
||||
|
||||
```rust,ignore
|
||||
pub fn notify<T: Summary>(item1: T, item2: T) {
|
||||
```
|
||||
|
||||
#### 通过 `+` 指定多个 trait
|
||||
|
||||
如果 `notify` 需要显示 `item` 的格式化形式,同时也要使用 `summarize` 方法,那么 `item` 就需要同时实现两个不同的 trait:`Display` 和 `Summary`。这可以通过 `+` 语法实现:
|
||||
|
||||
```rust,ignore
|
||||
pub fn notify(item: impl Summary + Display) {
|
||||
```
|
||||
|
||||
这个语法也适用于泛型的 trait bound:
|
||||
|
||||
```rust,ignore
|
||||
pub fn notify<T: Summary + Display>(item: T) {
|
||||
```
|
||||
|
||||
#### 通过 `where` 简化代码
|
||||
|
||||
然而,使用过多的 trait bound 也有缺点。每个泛型有其自己的 trait bound,所以有多个泛型参数的函数在名称和参数列表之间会有很长的 trait bound 信息,这使得函数签名难以阅读。为此,Rust 有另一个在函数签名之后的 `where` 从句中指定 trait bound 的语法。所以除了这么写:
|
||||
|
||||
```rust,ignore
|
||||
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {
|
||||
```
|
||||
|
||||
我们也可以使用 `where` 从句:
|
||||
还可以像这样使用 `where` 从句:
|
||||
|
||||
```rust,ignore
|
||||
fn some_function<T, U>(t: T, u: U) -> i32
|
||||
@ -230,51 +243,97 @@ fn some_function<T, U>(t: T, u: U) -> i32
|
||||
{
|
||||
```
|
||||
|
||||
这就显得不那么杂乱,同时也使这个函数看起来更像没有很多 trait bounds 的函数。这时函数名、参数列表和返回值类型都离得很近。
|
||||
这个函数签名就显得不那么杂乱,函数名、参数列表和返回值类型都离得很近,看起来类似没有很多 trait bounds 的函数。
|
||||
|
||||
### 返回 trait
|
||||
|
||||
也可以在返回值中使用 `impl Trait` 语法,来返回实现了某个 trait 的类型:
|
||||
|
||||
```rust,ignore
|
||||
fn returns_summarizable() -> impl Summary {
|
||||
Tweet {
|
||||
username: String::from("horse_ebooks"),
|
||||
content: String::from("of course, as you probably already know, people"),
|
||||
reply: false,
|
||||
retweet: false,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这个签名表明,“我要返回某个实现了 `Summary` trait 的类型,但是不确定其具体的类型”。在例子中返回了一个 `Tweet`,不过调用方并不知情。
|
||||
|
||||
这有什么用呢?在第十三章中,我们会学些两个大量依赖 trait 的功能:闭包和迭代器。这些功能创建只有编译器知道的类型,或者是非常非常长的类型。`impl Trait` 允许你简单的说 “返回一个 `Iterator`” 而无需写出实际的冗长的类型。
|
||||
|
||||
不过这只适用于返回单一类型的情况。例如,这样就 **不行**:
|
||||
|
||||
```rust,ignore,does_not_compile
|
||||
fn returns_summarizable(switch: bool) -> impl Summary {
|
||||
if switch {
|
||||
NewsArticle {
|
||||
headline: String::from("Penguins win the Stanley Cup Championship!"),
|
||||
location: String::from("Pittsburgh, PA, USA"),
|
||||
author: String::from("Iceburgh"),
|
||||
content: String::from("The Pittsburgh Penguins once again are the best
|
||||
hockey team in the NHL."),
|
||||
}
|
||||
} else {
|
||||
Tweet {
|
||||
username: String::from("horse_ebooks"),
|
||||
content: String::from("of course, as you probably already know, people"),
|
||||
reply: false,
|
||||
retweet: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这里尝试返回 `NewsArticle` 或 `Tweet`。这不能编译,因为 `impl Trait` 工作方式的限制。为了编写这样的代码,你不得不等到第十七章的 “为使用不同类型的值而设计的 trait 对象” 部分。
|
||||
|
||||
### 使用 trait bounds 来修复 `largest` 函数
|
||||
|
||||
所以任何想要对泛型使用 trait 定义的行为的时候,都需要在泛型参数类型上指定 trait bounds。现在我们就可以修复示例 10-5 中那个使用泛型类型参数的 `largest` 函数定义了!当我们将其放置不管的时候,它会出现这个错误:
|
||||
现在你知道了如何使用泛型参数 trait bound 来指定所需的行为。让我们回到实例 10-5 修复使用泛型类型参数的 `largest` 函数定义!最后尝试代时出现的错误是:
|
||||
|
||||
```text
|
||||
error[E0369]: binary operation `>` cannot be applied to type `T`
|
||||
--> src/main.rs:5:12
|
||||
|
|
||||
5 | if item > largest {
|
||||
| ^^^^
|
||||
| ^^^^^^^^^^^^^^
|
||||
|
|
||||
note: an implementation of `std::cmp::PartialOrd` might be missing for `T`
|
||||
= note: an implementation of `std::cmp::PartialOrd` might be missing for `T`
|
||||
```
|
||||
|
||||
在 `largest` 函数体中我们想要使用大于运算符比较两个 `T` 类型的值。这个运算符被定义为标准库中 trait `std::cmp::PartialOrd` 的一个默认方法。所以为了能够使用大于运算符,需要在 `T` 的 trait bounds 中指定 `PartialOrd`,这样 `largest` 函数可以用于任何可以比较大小的类型的 slice。因为 `PartialOrd` 位于 prelude 中所以并不需要手动将其引入作用域。
|
||||
在 `largest` 函数体中我们想要使用大于运算符(`>`)比较两个 `T` 类型的值。这个运算符被定义为标准库中 trait `std::cmp::PartialOrd` 的一个默认方法。所以需要在 `T` 的 trait bound 中指定 `PartialOrd`,这样 `largest` 函数可以用于任何可以比较大小的类型的 slice。因为 `PartialOrd` 位于 prelude 中所以并不需要手动将其引入作用域。将 `largest` 的签名修改为如下:
|
||||
|
||||
```rust,ignore
|
||||
fn largest<T: PartialOrd>(list: &[T]) -> T {
|
||||
```
|
||||
|
||||
但是如果编译代码的话,会出现不同的错误:
|
||||
但是如果编译代码的话,会出现一些不同的错误:
|
||||
|
||||
```text
|
||||
error[E0508]: cannot move out of type `[T]`, a non-copy array
|
||||
--> src/main.rs:4:23
|
||||
error[E0508]: cannot move out of type `[T]`, a non-copy slice
|
||||
--> src/main.rs:2:23
|
||||
|
|
||||
4 | let mut largest = list[0];
|
||||
| ----------- ^^^^^^^ cannot move out of here
|
||||
| |
|
||||
| hint: to prevent move, use `ref largest` or `ref mut largest`
|
||||
2 | let mut largest = list[0];
|
||||
| ^^^^^^^
|
||||
| |
|
||||
| cannot move out of here
|
||||
| help: consider using a reference instead: `&list[0]`
|
||||
|
||||
error[E0507]: cannot move out of borrowed content
|
||||
--> src/main.rs:6:9
|
||||
--> src/main.rs:4:9
|
||||
|
|
||||
6 | for &item in list.iter() {
|
||||
4 | for &item in list.iter() {
|
||||
| ^----
|
||||
| ||
|
||||
| |hint: to prevent move, use `ref item` or `ref mut item`
|
||||
| cannot move out of borrowed content
|
||||
```
|
||||
|
||||
错误的核心是 `cannot move out of type [T], a non-copy array`,对于非泛型版本的 `largest` 函数,我们只尝试了寻找最大的 `i32` 和 `char`。正如第四章讨论过的,像 `i32` 和 `char` 这样的类型是已知大小的并可以储存在栈上,所以他们实现了 `Copy` trait。当我们将 `largest` 函数改成使用泛型后,现在 `list` 参数的类型就有可能是没有实现 `Copy` trait 的,这意味着我们可能不能将 `list[0]` 的值移动到 `largest` 变量中。
|
||||
错误的核心是 `cannot move out of type [T], a non-copy slice`,对于非泛型版本的 `largest` 函数,我们只尝试了寻找最大的 `i32` 和 `char`。正如第四章 “只在栈上的数据:拷贝” 部分讨论过的,像 `i32` 和 `char` 这样的类型是已知大小的并可以储存在栈上,所以他们实现了 `Copy` trait。当我们将 `largest` 函数改成使用泛型后,现在 `list` 参数的类型就有可能是没有实现 `Copy` trait 的。这意味着我们可能不能将 `list[0]` 的值移动到 `largest` 变量中,这导致了上面的错误。
|
||||
|
||||
如果只想对实现了 `Copy` 的类型调用这些代码,可以在 `T` 的 trait bounds 中增加 `Copy`!示例 10-16 中展示了一个可以编译的泛型版本的 `largest` 函数的完整代码,只要传递给 `largest` 的 slice 值的类型实现了 `PartialOrd` 和 `Copy` 这两个 trait,例如 `i32` 和 `char`:
|
||||
为了只对实现了 `Copy` 的类型调用这些代码,可以在 `T` 的 trait bounds 中增加 `Copy`!示例 10-15 中展示了一个可以编译的泛型版本的 `largest` 函数的完整代码,只要传递给 `largest` 的 slice 值的类型实现了 `PartialOrd` **和** `Copy` 这两个 trait,例如 `i32` 和 `char`:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -304,13 +363,15 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 10-16:一个可以用于任何实现了 `PartialOrd` 和 `Copy` trait 的泛型的 `largest` 函数</span>
|
||||
<span class="caption">示例 10-15:一个可以用于任何实现了 `PartialOrd` 和 `Copy` trait 的泛型的 `largest` 函数</span>
|
||||
|
||||
如果并不希望限制 `largest` 函数只能用于实现了 `Copy` trait 的类型,我们可以在 `T` 的 trait bounds 中指定 `Clone` 而不是 `Copy`,并克隆 slice 的每一个值使得 `largest` 函数拥有其所有权。但是使用 `clone` 函数潜在意味着更多的堆分配,而且堆分配在涉及大量数据时可能会相当缓慢。另一种 `largest` 的实现方式是返回 slice 中一个 `T` 值的引用。如果我们将函数返回值从 `T` 改为 `&T` 并改变函数体使其能够返回一个引用,我们将不需要任何 `Clone` 或 `Copy` 的 trait bounds 而且也不会有任何的堆分配。尝试自己实现这种替代解决方式吧!
|
||||
如果并不希望限制 `largest` 函数只能用于实现了 `Copy` trait 的类型,我们可以在 `T` 的 trait bounds 中指定 `Clone` 而不是 `Copy`。并克隆 slice 的每一个值使得 `largest` 函数拥有其所有权。使用 `clone` 函数意味着对于类似 `String` 这样拥有堆上数据的类型,会潜在的分配更多堆上空间,而堆分配在涉及大量数据时可能会相当缓慢。
|
||||
|
||||
另一种 `largest` 的实现方式是返回在 slice 中 `T` 值的引用。如果我们将函数返回值从 `T` 改为 `&T` 并改变函数体使其能够返回一个引用,我们将不需要任何 `Clone` 或 `Copy` 的 trait bounds 而且也不会有任何的堆分配。尝试自己实现这种替代解决方式吧!
|
||||
|
||||
### 使用 trait bound 有条件的实现方法
|
||||
|
||||
通过使用带有 trait bound 的泛型 `impl` 块,可以有条件的只为实现了特定 trait 的类型实现方法。例如,示例 10-17 中的类型 `Pair<T>` 总是实现了 `new` 方法,不过只有 `Pair<T>` 内部的 `T` 类型实现了 `PartialOrd` trait 来允许比较和 `Display` trait 来启用打印,才会实现 `cmp_display`:
|
||||
通过使用带有 trait bound 的泛型参数的 `impl` 块,可以有条件的只为实现了特定 trait 的类型实现方法。例如,示例 10-16 中的类型 `Pair<T>` 总是实现了 `new` 方法,不过只有 `Pair<T>` 内部的 `T` 类型实现了 `PartialOrd` trait 来允许比较 **和** `Display` trait 来启用打印,才会实现 `cmp_display` 方法:
|
||||
|
||||
```rust
|
||||
use std::fmt::Display;
|
||||
|
@ -1,20 +1,18 @@
|
||||
## 生命周期与引用有效性
|
||||
|
||||
> [ch10-03-lifetime-syntax.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch10-03-lifetime-syntax.md)
|
||||
> [ch10-03-lifetime-syntax.md](https://github.com/rust-lang/book/blob/master/src/ch10-03-lifetime-syntax.md)
|
||||
> <br>
|
||||
> commit fa0e4403f8350287b034c5b64af752f647ebb5a2
|
||||
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
|
||||
|
||||
当在第四章讨论引用时,我们遗漏了一个重要的细节:Rust 中的每一个引用都有其 **生命周期**(*lifetime*),也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。
|
||||
当在第四章讨论 “引用和借用” 部分时,我们遗漏了一个重要的细节:Rust 中的每一个引用都有其 **生命周期**(*lifetime*),也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。
|
||||
|
||||
好吧,这有点不太寻常,而且也不同于其他语言中使用的工具。生命周期,从某种意义上说,是 Rust 最与众不同的功能。
|
||||
|
||||
生命周期是一个很广泛的话题,本章不可能涉及到它全部的内容,所以这里我们会讲到一些通常你可能会遇到的生命周期语法以便你熟悉这个概念。第十九章会包含生命周期所有功能的更高级的内容。
|
||||
生命周期的概念某种程度上说不同于其他语言中类似的工具,毫无疑问这是 Rust 最与众不同的功能。虽然本章不可能涉及到它全部的内容,我们会讲到一些通常你可能会遇到的生命周期语法以便你熟悉这个概念。查看第十九章 “高级生命周期” 部分了解更多的细节。
|
||||
|
||||
### 生命周期避免了悬垂引用
|
||||
|
||||
生命周期的主要目标是避免悬垂引用,它会导致程序引用了非预期引用的数据。考虑一下示例 10-18 中的程序,它有一个外部作用域和一个内部作用域,外部作用域声明了一个没有初值的变量 `r`,而内部作用域声明了一个初值为 5 的变量`x`。在内部作用域中,我们尝试将 `r` 的值设置为一个 `x` 的引用。接着在内部作用域结束后,尝试打印出 `r` 的值:
|
||||
生命周期的主要目标是避免悬垂引用,它会导致程序引用了非预期引用的数据。考虑一下示例 10-17 中的程序,它有一个外部作用域和一个内部作用域.
|
||||
|
||||
```rust,ignore
|
||||
```rust,ignore,does_not_compile
|
||||
{
|
||||
let r;
|
||||
|
||||
@ -27,16 +25,15 @@
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 10-18:尝试使用离开作用域的值的引用</span>
|
||||
<span class="caption">示例 10-17:尝试使用离开作用域的值的引用</span>
|
||||
|
||||
> #### 未初始化变量不能被使用
|
||||
>
|
||||
> 接下来的一些例子中声明了没有初始值的变量,以便这些变量存在于外部作用域。这看起来好像和 Rust 不允许存在空值相冲突。然而这是可以的,如果我们尝试在给它一个值之前使用这个变量,会出现一个编译时错误。请自行尝试!
|
||||
> 注意:示例 10-17、10-18 和 10-24 中声明了没有初始值的变量,所以这些变量存在于外部作用域。这乍看之下好像和 Rust 不允许存在空值相冲突。然而如果尝试在给它一个值之前使用这个变量,会出现一个编译时错误,这就说明了 Rust 确实不允许空值。
|
||||
|
||||
当编译这段代码时会得到一个错误:
|
||||
外部作用域声明了一个没有初值的变量 `r`,而内部作用域声明了一个初值为 5 的变量`x`。在内部作用域中,我们尝试将 `r` 的值设置为一个 `x` 的引用。接着在内部作用域结束后,尝试打印出 `r` 的值。这段代码不能编译因为 `r` 引用的值在尝试使用之前就离开了作用域。如下是错误信息:
|
||||
|
||||
```text
|
||||
error: `x` does not live long enough
|
||||
error[E0597]: `x` does not live long enough
|
||||
--> src/main.rs:7:5
|
||||
|
|
||||
6 | r = &x;
|
||||
| - borrow occurs here
|
||||
@ -47,51 +44,43 @@ error: `x` does not live long enough
|
||||
| - borrowed value needs to live until here
|
||||
```
|
||||
|
||||
变量 `x` 并没有 “存在的足够久”。为什么呢?好吧,`x` 在到达第 7 行的大括号的结束时就离开了作用域,这也是内部作用域的结尾。不过 `r` 在外部作用域也是有效的;作用域越大我们就说它 “存在的越久”。如果 Rust 允许这段代码工作,`r` 将会引用在 `x` 离开作用域时被释放的内存,这时尝试对 `r` 做任何操作都会不能正常工作。那么 Rust 是如何决定这段代码是不被允许的呢?
|
||||
变量 `x` 并没有 “存在的足够久”。其原因是 `x` 在到达第 7 行内部作用域结束时就离开了作用域。不过 `r` 在外部作用域仍是有效的;作用域越大我们就说它 “存在的越久”。如果 Rust 允许这段代码工作,`r` 将会引用在 `x` 离开作用域时被释放的内存,这时尝试对 `r` 做任何操作都不能正常工作。那么 Rust 是如何决定这段代码是不被允许的呢?这得益于借用检查器。
|
||||
|
||||
#### 借用检查器
|
||||
|
||||
编译器的这一部分叫做 **借用检查器**(*borrow checker*),它比较作用域来确保所有的借用都是有效的。示例 10-19 展示了与示例 10-18 相同的例子不过带有变量生命周期的注释:
|
||||
Rust 编译器有一个 **借用检查器**(*borrow checker*),它比较作用域来确保所有的借用都是有效的。示例 10-18 展示了与示例 10-17 相同的例子不过带有变量生命周期的注释:
|
||||
|
||||
```rust,ignore
|
||||
```rust,ignore,does_not_compile
|
||||
{
|
||||
let r; // -------+-- 'a
|
||||
// |
|
||||
{ // |
|
||||
let x = 5; // -+-----+-- 'b
|
||||
r = &x; // | |
|
||||
} // -+ |
|
||||
// |
|
||||
println!("r: {}", r); // |
|
||||
} // -------+
|
||||
let r; // ---------+-- 'a
|
||||
// |
|
||||
{ // |
|
||||
let x = 5; // -+-- 'b |
|
||||
r = &x; // | |
|
||||
} // -+ |
|
||||
// |
|
||||
println!("r: {}", r); // |
|
||||
} // ---------+
|
||||
```
|
||||
|
||||
<span class="caption">示例 10-19:`r` 和 `x` 的生命周期注解,分别叫做 `'a` 和 `'b`</span>
|
||||
<span class="caption">示例 10-18:`r` 和 `x` 的生命周期注解,分别叫做 `'a` 和 `'b`</span>
|
||||
|
||||
<!-- Just checking I'm reading this right: the inside block is the b lifetime,
|
||||
correct? I want to leave a note for production, make sure we can make that
|
||||
clear -->
|
||||
<!-- Yes, the inside block for the `'b` lifetime starts with the `let x = 5;`
|
||||
line and ends with the first closing curly brace on the 7th line. Do you think
|
||||
the text art comments work or should we make an SVG diagram that has nicer
|
||||
looking arrows and labels? /Carol -->
|
||||
这里将 `r` 的生命周期标记为 `'a` 并将 `x` 的生命周期标记为 `'b`。如你所见,内部的 `'b` 块要比外部的生命周期 `'a` 小得多。在编译时,Rust 比较这两个生命周期的大小,并发现 `r` 拥有生命周期 `'a`,不过它引用了一个拥有生命周期 `'b` 的对象。程序被拒绝编译,因为生命周期 `'b` 比生命周期 `'a` 要小:被引用的对象比它的引用者存在的时间更短。
|
||||
|
||||
我们将 `r` 的生命周期标记为 `'a` 并将 `x` 的生命周期标记为 `'b`。如你所见,内部的 `'b` 块要比外部的生命周期 `'a` 小得多。在编译时,Rust 比较这两个生命周期的大小,并发现 `r` 拥有生命周期 `'a`,不过它引用了一个拥有生命周期 `'b` 的对象。程序被拒绝编译,因为生命周期 `'b` 比生命周期 `'a` 要小:被引用的对象比它的引用者存在的时间更短。
|
||||
|
||||
让我们看看示例 10-20 中这个并没有产生悬垂引用且可以正确编译的例子:
|
||||
让我们看看示例 10-19 中这个并没有产生悬垂引用且可以正确编译的例子:
|
||||
|
||||
```rust
|
||||
{
|
||||
let x = 5; // -----+-- 'b
|
||||
// |
|
||||
let r = &x; // --+--+-- 'a
|
||||
// | |
|
||||
println!("r: {}", r); // | |
|
||||
// --+ |
|
||||
} // -----+
|
||||
let x = 5; // ----------+-- 'b
|
||||
// |
|
||||
let r = &x; // --+-- 'a |
|
||||
// | |
|
||||
println!("r: {}", r); // | |
|
||||
// --+ |
|
||||
} // ----------+
|
||||
```
|
||||
|
||||
<span class="caption">示例 10-20:一个有效的引用,因为数据比引用有着更长的生命周期</span>
|
||||
<span class="caption">示例 10-19:一个有效的引用,因为数据比引用有着更长的生命周期</span>
|
||||
|
||||
这里 `x` 拥有生命周期 `'b`,比 `'a` 要大。这就意味着 `r` 可以引用 `x`:Rust 知道 `r` 中的引用在 `x` 有效的时候也总是有效的。
|
||||
|
||||
@ -99,7 +88,7 @@ looking arrows and labels? /Carol -->
|
||||
|
||||
### 函数中的泛型生命周期
|
||||
|
||||
让我们来编写一个返回两个字符串 slice 中较长者的函数。我们希望能够通过传递两个字符串 slice 来调用这个函数,并希望返回一个字符串 slice。一旦我们实现了 `longest` 函数,示例 10-21 中的代码应该会打印出 `The longest string is abcd`:
|
||||
让我们来编写一个返回两个字符串 slice 中较长者的函数。这个函数获取两个字符串 slice 并返回一个字符串 slice。一旦我们实现了 `longest` 函数,示例 10-20 中的代码应该会打印出 `The longest string is abcd`:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -113,34 +102,15 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 10-21:`main` 函数调用 `longest` 函数来寻找两个字符串 slice 中较长的一个</span>
|
||||
|
||||
注意函数期望获取字符串 slice(如第四章所讲到的这是引用)因为我们并不希望`longest` 函数获取其参数的所有权。我们希望函数能够接受 `String` 的 slice(也就是变量 `string1` 的类型)以及字符串字面值(也就是变量 `string2` 包含的值)。
|
||||
|
||||
<!-- why is `a` a slice and `b` a literal? You mean "a" from the string "abcd"? -->
|
||||
<!-- I've changed the variable names to remove ambiguity between the variable
|
||||
name `a` and the "a" from the string "abcd". `string1` is not a slice, it's a
|
||||
`String`, but we're going to pass a slice that refers to that `String` to the
|
||||
`longest` function (`string1.as_str()` creates a slice that references the
|
||||
`String` stored in `string1`). We chose to have `string2` be a literal since
|
||||
the reader might have code with both `String`s and string literals, and the way
|
||||
most readers first get into problems with lifetimes is involving string slices,
|
||||
so we wanted to demonstrate the flexibility of taking string slices as
|
||||
arguments but the issues you might run into because string slices are
|
||||
references.
|
||||
All of the `String`/string slice/string literal concepts here are covered
|
||||
thoroughly in Chapter 4, which is why we put two back references here (above
|
||||
and below). If these topics are confusing you in this context, I'd be
|
||||
interested to know if rereading Chapter 4 clears up that confusion.
|
||||
/Carol -->
|
||||
<span class="caption">示例 10-20:`main` 函数调用 `longest` 函数来寻找两个字符串 slice 中较长的一个</span>
|
||||
|
||||
参考之前第四章中的 “字符串 slice 作为参数” 部分中更多关于为什么上面例子中的参数正符合我们期望的讨论。
|
||||
|
||||
如果尝试像示例 10-22 中那样实现 `longest` 函数,它并不能编译:
|
||||
如果尝试像示例 10-21 中那样实现 `longest` 函数,它并不能编译:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust,ignore
|
||||
```rust,ignore,does_not_compile
|
||||
fn longest(x: &str, y: &str) -> &str {
|
||||
if x.len() > y.len() {
|
||||
x
|
||||
@ -150,43 +120,44 @@ fn longest(x: &str, y: &str) -> &str {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 10-22:一个 `longest` 函数的实现,它返回两个字符串 slice 中较长者,现在还不能编译</span>
|
||||
<span class="caption">示例 10-21:一个 `longest` 函数的实现,它返回两个字符串 slice 中较长者,现在还不能编译</span>
|
||||
|
||||
将会出现如下有关生命周期的错误:
|
||||
相应地会出现如下有关生命周期的错误:
|
||||
|
||||
```text
|
||||
error[E0106]: missing lifetime specifier
|
||||
|
|
||||
1 | fn longest(x: &str, y: &str) -> &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 `x` or `y`
|
||||
--> src/main.rs:1:33
|
||||
|
|
||||
1 | fn longest(x: &str, y: &str) -> &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 `x` or `y`
|
||||
```
|
||||
|
||||
提示文本告诉我们返回值需要一个泛型生命周期参数,因为 Rust 并不知道将要返回的引用是指向 `x` 或 `y`。事实上我们也不知道,因为函数体中 `if` 块返回一个 `x` 的引用而 `else` 块返回一个 `y` 的引用。
|
||||
提示文本揭示了返回值需要一个泛型生命周期参数,因为 Rust 并不知道将要返回的引用是指向 `x` 或 `y`。事实上我们也不知道,因为函数体中 `if` 块返回一个 `x` 的引用而 `else` 块返回一个 `y` 的引用!
|
||||
|
||||
虽然我们定义了这个函数,但是并不知道传递给函数的具体值,所以也不知道到底是 `if` 还是 `else` 会被执行。我们也不知道传入的引用的具体生命周期,所以也就不能像示例 10-19 和 10-20 那样通过观察作用域来确定返回的引用是否总是有效。借用检查器自身同样也无法确定,因为它不知道 `x` 和 `y` 的生命周期是如何与返回值的生命周期相关联的。接下来我们将增加泛型生命周期参数来定义引用间的关系以便借用检查器可以进行分析。
|
||||
当我们定义这个函数的时候,并不知道传递给函数的具体值,所以也不知道到底是 `if` 还是 `else` 会被执行。我们也不知道传入的引用的具体生命周期,所以也就不能像示例 10-18 和 10-19 那样通过观察作用域来确定返回的引用是否总是有效。借用检查器自身同样也无法确定,因为它不知道 `x` 和 `y` 的生命周期是如何与返回值的生命周期相关联的。为了修复这个错误,我们将增加泛型生命周期参数来定义引用间的关系以便借用检查器可以进行分析。
|
||||
|
||||
### 生命周期注解语法
|
||||
|
||||
生命周期注解并不改变任何引用的生命周期的长短。与当函数签名中指定了泛型类型参数后就可以接受任何类型一样,当指定了泛型生命周期后函数也能接受任何生命周期的引用。生命周期注解所做的就是将多个引用的生命周期联系起来。
|
||||
生命周期注解并不改变任何引用的生命周期的长短。与当函数签名中指定了泛型类型参数后就可以接受任何类型一样,当指定了泛型生命周期后函数也能接受任何生命周期的引用。生命周期注解描述了多个引用生命周期相互的关系,而不影响其生命周期。
|
||||
|
||||
生命周期注解有着一个不太常见的语法:生命周期参数名称必须以撇号(`'`)开头。生命周期参数的名称通常全是小写,而且类似于泛型类型,其名称通常非常短。`'a` 是大多数人默认使用的名称。生命周期参数注解位于引用的 `&` 之后,并有一个空格来将引用类型与生命周期注解分隔开。
|
||||
生命周期注解有着一个不太常见的语法:生命周期参数名称必须以撇号(`'`)开头,其名称通常全是小写,类似于泛型其名称非常短。`'a` 是大多数人默认使用的名称。生命周期参数注解位于引用的 `&` 之后,并有一个空格来将引用类型与生命周期注解分隔开。
|
||||
|
||||
这里有一些例子:我们有一个没有生命周期参数的 `i32` 的引用,一个有叫做 `'a` 的生命周期参数的 `i32` 的引用,和一个生命周期也是 `'a` 的 `i32` 的可变引用:
|
||||
|
||||
```rust,ignore
|
||||
&i32 // a reference
|
||||
&'a i32 // a reference with an explicit lifetime
|
||||
&'a mut i32 // a mutable reference with an explicit lifetime
|
||||
&i32 // 引用
|
||||
&'a i32 // 带有显式生命周期的引用
|
||||
&'a mut i32 // 带有显式生命周期的可变引用
|
||||
```
|
||||
|
||||
单个的生命周期注解本身没有多少意义:生命周期注解告诉 Rust 多个引用的泛型生命周期参数如何相互联系。如果函数有一个生命周期 `'a` 的 `i32` 的引用的参数 `first`,还有另一个同样是生命周期 `'a` 的 `i32` 的引用的参数 `second`,这两个生命周期注解有相同的名称意味着 `first` 和 `second` 必须与这相同的泛型生命周期存在得一样久。
|
||||
单个的生命周期注解本身没有多少意义,因为生命周期注解告诉 Rust 多个引用的泛型生命周期参数如何相互联系的。例如如果函数有一个生命周期 `'a` 的 `i32` 的引用的参数 `first`。还有另一个同样是生命周期 `'a` 的 `i32` 的引用的参数 `second`。这两个生命周期注解意味着引用 `first` 和 `second` 必须与这泛型生命周期存在得一样久。
|
||||
|
||||
### 函数签名中的生命周期注解
|
||||
|
||||
来看看我们编写的 `longest` 函数的上下文中的生命周期。就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的尖括号中。这里我们想要告诉 Rust 关于参数中的引用和返回值之间的限制是他们都必须拥有相同的生命周期,就像示例 10-23 中在每个引用中都加上了 `'a` 那样:
|
||||
现在来看看 `longest` 函数的上下文中的生命周期。就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的尖括号中。这里我们想要告诉 Rust 关于参数中的引用和返回值之间的限制是他们都必须拥有相同的生命周期,就像示例 10-22 中在每个引用中都加上了 `'a` 那样:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -200,19 +171,17 @@ fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 10-23:`longest` 函数定义指定了签名中所有的引用必须有相同的生命周期 `'a`</span>
|
||||
<span class="caption">示例 10-22:`longest` 函数定义指定了签名中所有的引用必须有相同的生命周期 `'a`</span>
|
||||
|
||||
这段代码能够编译并会产生我们希望得到的示例 10-21 中的 `main` 函数的结果。
|
||||
这段代码能够编译并会产生我们希望得到的示例 10-20 中的 `main` 函数的结果。
|
||||
|
||||
现在函数签名表明对于某些生命周期 `'a`,函数会获取两个参数,他们都是与生命周期 `'a` 存在的一样长的字符串 slice。函数会返回一个同样也与生命周期 `'a` 存在的一样长的字符串 slice。这就是我们告诉 Rust 需要其保证的契约。
|
||||
现在函数签名表明对于某些生命周期 `'a`,函数会获取两个参数,他们都是与生命周期 `'a` 存在的一样长的字符串 slice。函数会返回一个同样也与生命周期 `'a` 存在的一样长的字符串 slice。这就是我们告诉 Rust 需要其保证的契约。记住通过在函数签名中指定生命周期参数时,我们并没有改变任何传入后返回的值的生命周期。而是指出任何不遵守这个协议的传入值都将被借用检查器拒绝。注意 `longest` 函数并不需要知道 `x` 和 `y` 具体会存在多久,而只需要知道有某个可以被 `'a` 替代的作用域将会满足这个签名。
|
||||
|
||||
通过在函数签名中指定生命周期参数,我们并没有改变任何传入后返回的值的生命周期,而是指出任何不遵守这个协议的传入值都将被借用检查器拒绝。这个函数并不知道(或需要知道)`x` 和 `y` 具体会存在多久,而只需要知道有某个可以被 `'a` 替代的作用域将会满足这个签名。
|
||||
当在函数中使用生命周期注解时,这些注解出现在函数签名中,而不存在于函数体中的任何代码中。这是因为 Rust 能够分析函数中代码而不需要任何协助,不过当函数引用或被函数之外的代码引用时,让 Rust 自身分析出参数或返回值的生命周期几乎是不可能的。这些生命周期在每次函数被调用时都可能不同。这也就是为什么我们需要手动标记生命周期。
|
||||
|
||||
当在函数中使用生命周期注解时,这些注解出现在函数签名中,而不存在于函数体中的任何代码中。这是因为 Rust 能够分析函数中代码而不需要任何协助,不过当函数引用或被函数之外的代码引用时,参数或返回值的生命周期可能在每次函数被调用时都不同。这可能会产生惊人的消耗并且对于 Rust 来说通常是不可能分析的。在这种情况下,我们需要自己标注生命周期。
|
||||
当具体的引用被传递给 `longest` 时,被 `'a` 所替代的具体生命周期是 `x` 的作用域与 `y` 的作用域相重叠的那一部分。换一种说法就是泛型生命周期 `'a` 的具体生命周期等同于 `x` 和 `y` 的生命周期中较小的那一个。因为我们用相同的生命周期参数 `'a` 标注了返回的引用值,所以返回的引用值就能保证在 `x` 和 `y` 中较短的那个生命周期结束之前保持有效。
|
||||
|
||||
当具体的引用被传递给 `longest` 时,被 `'a` 所替代的具体生命周期是 `x` 的作用域与 `y` 的作用域相重叠的那一部分。因为作用域总是嵌套的,所以换一种说法就是泛型生命周期 `'a` 的具体生命周期等同于 `x` 和 `y` 的生命周期中较小的那一个。因为我们用相同的生命周期参数 `'a` 标注了返回的引用值,所以返回的引用值就能保证在 `x` 和 `y` 中较短的那个生命周期结束之前保持有效。
|
||||
|
||||
让我们看看如何通过传递拥有不同具体生命周期的引用来限制 `longest` 函数的使用。示例 10-24 是一个应该在任何编程语言中都很直观的例子:`string1` 直到外部作用域结束都是有效的,`string2` 则在内部作用域中是有效的,而 `result` 则引用了一些直到内部作用域结束都是有效的值。借用检查器认可这些代码;它能够编译和运行,并打印出 `The longest string is long string is long`:
|
||||
让我们看看如何通过传递拥有不同具体生命周期的引用来限制 `longest` 函数的使用。示例 10-23 是一个很直观的例子。
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -236,13 +205,15 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 10-24:通过拥有不同的具体生命周期的 `String` 值调用 `longest` 函数</span>
|
||||
<span class="caption">示例 10-23:通过拥有不同的具体生命周期的 `String` 值调用 `longest` 函数</span>
|
||||
|
||||
接下来,让我们尝试一个 `result` 的引用的生命周期肯定比两个参数的要短的例子。将 `result` 变量的声明从内部作用域中移动出来,但是将 `result` 和 `string2` 变量的赋值语句一同留在内部作用域里。接下来,我们将使用 `result` 的 `println!` 移动到内部作用域之外,就在其结束之后。注意示例 10-25 中的代码不能编译:
|
||||
在这个例子中,`string1` 直到外部作用域结束都是有效的,`string2` 则在内部作用域中是有效的,而 `result` 则引用了一些直到内部作用域结束都是有效的值。借用检查器认可这些代码;它能够编译和运行,并打印出 `The longest string is long string is long`。
|
||||
|
||||
接下来,让我们尝试一个 `result` 的引用的生命周期肯定比两个参数的要短的例子。将 `result` 变量的声明从内部作用域中移动出来,但是将 `result` 和 `string2` 变量的赋值语句一同留在内部作用域里。接下来,我们将使用 `result` 的 `println!` 移动到内部作用域之外,就在其结束之后。注意示例 10-24 中的代码不能编译:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust,ignore
|
||||
```rust,ignore,does_not_compile
|
||||
fn main() {
|
||||
let string1 = String::from("long string is long");
|
||||
let result;
|
||||
@ -254,25 +225,26 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 10-25:在 `string2` 离开作用域之后使用 `result` 的尝试不能编译</span>
|
||||
<span class="caption">示例 10-24:在 `string2` 离开作用域之后使用 `result` 的尝试不能编译</span>
|
||||
|
||||
如果尝试编译会出现如下错误:
|
||||
|
||||
```text
|
||||
error: `string2` does not live long enough
|
||||
error[E0597]: `string2` does not live long enough
|
||||
--> src/main.rs:15:5
|
||||
|
|
||||
6 | result = longest(string1.as_str(), string2.as_str());
|
||||
14 | result = longest(string1.as_str(), string2.as_str());
|
||||
| ------- borrow occurs here
|
||||
7 | }
|
||||
15 | }
|
||||
| ^ `string2` dropped here while still borrowed
|
||||
8 | println!("The longest string is {}", result);
|
||||
9 | }
|
||||
16 | println!("The longest string is {}", result);
|
||||
17 | }
|
||||
| - borrowed value needs to live until here
|
||||
```
|
||||
|
||||
错误表明为了保证 `println!` 中的 `result` 是有效的,`string2` 需要直到外部作用域结束都是有效的。Rust 知道这些是因为(`longest`)函数的参数和返回值都使用了相同的生命周期参数 `'a`。
|
||||
|
||||
以人类的理解 `string1` 更长,因此 `result` 会包含指向 `string1` 的引用。因为 `string1` 尚未离开作用域,对于 `println!` 来说 `string1` 的引用仍然是有效的。然而,我们通过生命周期参数告诉 Rust 的是 `longest` 函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器不允许示例 10-25 中的代码,因为它可能会存在无效的引用。
|
||||
以人类的理解 `string1` 更长,因此 `result` 会包含指向 `string1` 的引用。因为 `string1` 尚未离开作用域,对于 `println!` 来说 `string1` 的引用仍然是有效的。然而,我们通过生命周期参数告诉 Rust 的是 `longest` 函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器不允许示例 10-24 中的代码,因为它可能会存在无效的引用。
|
||||
|
||||
请尝试更多采用不同的值和不同生命周期的引用作为 `longest` 函数的参数和返回值的实验。并在开始编译前猜想你的实验能否通过借用检查器,接着编译一下看看你的理解是否正确!
|
||||
|
||||
@ -294,7 +266,7 @@ fn longest<'a>(x: &'a str, y: &str) -> &'a str {
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust,ignore
|
||||
```rust,ignore,does_not_compile
|
||||
fn longest<'a>(x: &str, y: &str) -> &'a str {
|
||||
let result = String::from("really long string");
|
||||
result.as_str()
|
||||
@ -304,18 +276,23 @@ fn longest<'a>(x: &str, y: &str) -> &'a str {
|
||||
即便我们为返回值指定了生命周期参数 `'a`,这个实现却编译失败了,因为返回值的生命周期与参数完全没有关联。这里是会出现的错误信息:
|
||||
|
||||
```text
|
||||
error: `result` does not live long enough
|
||||
error[E0597]: `result` does not live long enough
|
||||
--> src/main.rs:3:5
|
||||
|
|
||||
3 | result.as_str()
|
||||
| ^^^^^^ does not live long enough
|
||||
4 | }
|
||||
| - borrowed value only lives until here
|
||||
|
|
||||
note: borrowed value must be valid for the lifetime 'a as defined on the block
|
||||
at 1:44...
|
||||
note: borrowed value must be valid for the lifetime 'a as defined on the
|
||||
function body at 1:1...
|
||||
--> src/main.rs:1:1
|
||||
|
|
||||
1 | fn longest<'a>(x: &str, y: &str) -> &'a str {
|
||||
| ^
|
||||
1 | / fn longest<'a>(x: &str, y: &str) -> &'a str {
|
||||
2 | | let result = String::from("really long string");
|
||||
3 | | result.as_str()
|
||||
4 | | }
|
||||
| |_^
|
||||
```
|
||||
|
||||
出现的问题是 `result` 在 `longest` 函数的结尾将离开作用域并被清理,而我们尝试从函数返回一个 `result` 的引用。无法指定生命周期参数来改变悬垂引用,而且 Rust 也不允许我们创建一个悬垂引用。在这种情况,最好的解决方案是返回一个有所有权的数据类型而不是一个引用,这样函数调用者就需要负责清理这个值了。
|
||||
@ -324,7 +301,7 @@ at 1:44...
|
||||
|
||||
### 结构体定义中的生命周期注解
|
||||
|
||||
目前为止,我们只定义过有所有权类型的结构体。也可以定义存放引用的结构体,不过需要为结构体定义中的每一个引用添加生命周期注解。示例 10-26 中有一个存放了一个字符串 slice 的结构体 `ImportantExcerpt`:
|
||||
目前为止,我们只定义过有所有权类型的结构体。也可以定义存放引用的结构体,不过需要为结构体定义中的每一个引用添加生命周期注解。示例 10-25 中有一个存放了一个字符串 slice 的结构体 `ImportantExcerpt`:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -342,15 +319,15 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 10-26:一个存放引用的结构体,所以其定义需要生命周期注解</span>
|
||||
<span class="caption">示例 10-25:一个存放引用的结构体,所以其定义需要生命周期注解</span>
|
||||
|
||||
这个结构体有一个字段,`part`,它存放了一个字符串 slice,这是一个引用。类似于泛型参数类型,必须在结构体名称后面的尖括号中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数。
|
||||
这个结构体有一个字段,`part`,它存放了一个字符串 slice,这是一个引用。类似于泛型参数类型,必须在结构体名称后面的尖括号中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数。这个注解意味着 `ImportantExcerpt` 的示例不能比其 `part` 字段中的引用存在的更久。
|
||||
|
||||
这里的 `main` 函数创建了一个 `ImportantExcerpt` 的实例,它存放了变量 `novel` 所拥有的 `String` 的第一个句子的引用。
|
||||
这里的 `main` 函数创建了一个 `ImportantExcerpt` 的实例,它存放了变量 `novel` 所拥有的 `String` 的第一个句子的引用。`novel` 的数据在 `ImportantExcerpt` 实例创建之前就存在。另外,直到 `ImportantExcerpt` 离开作用域之后 `novel` 都不会离开作用域,所以 `ImportantExcerpt` 实例中的引用是有效的
|
||||
|
||||
### 生命周期省略(Lifetime Elision)
|
||||
|
||||
在这一部分,我们知道了每一个引用都有一个生命周期,而且需要为使用了引用的函数或结构体指定生命周期。然而,第四章的 “字符串 slice” 部分有一个函数,我们在示例 10-27 中再次展示出来,它没有生命周期注解却能成功编译:
|
||||
在这一部分,我们知道了每一个引用都有一个生命周期,而且需要为使用了引用的函数或结构体指定生命周期。然而,第四章的示例 4-9 中有一个函数,我们在示例 10-26 中再次展示出来,它没有生命周期注解却能成功编译:
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
@ -368,9 +345,9 @@ fn first_word(s: &str) -> &str {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 10-27:第四章定义了一个没有使用生命周期注解的函数,即便其参数和返回值都是引用</span>
|
||||
<span class="caption">示例 10-27:示例 4-9 定义了一个没有使用生命周期注解的函数,即便其参数和返回值都是引用</span>
|
||||
|
||||
这个函数没有生命周期注解却能编译是由于一些历史原因:在早期 pre-1.0 版本的 Rust 中,这的确是不能编译的。每一个引用都必须有明确的生命周期。那时的函数签名将会写成这样:
|
||||
这个函数没有生命周期注解却能编译是由于一些历史原因:在早期版本(pre-1.0)的 Rust 中,这的确是不能编译的。每一个引用都必须有明确的生命周期。那时的函数签名将会写成这样:
|
||||
|
||||
```rust,ignore
|
||||
fn first_word<'a>(s: &'a str) -> &'a str {
|
||||
@ -384,23 +361,25 @@ fn first_word<'a>(s: &'a str) -> &'a str {
|
||||
|
||||
省略规则并不提供完整的推断:如果 Rust 在明确遵守这些规则的前提下变量的生命周期仍然是模棱两可的话,它不会猜测剩余引用的生命周期应该是什么。在这种情况,编译器会给出一个错误,这可以通过增加对应引用之间相联系的生命周期注解来解决。
|
||||
|
||||
首先,介绍一些定义:函数或方法的参数的生命周期被称为 **输入生命周期**(*input lifetimes*),而返回值的生命周期被称为 **输出生命周期**(*output lifetimes*)。
|
||||
函数或方法的参数的生命周期被称为 **输入生命周期**(*input lifetimes*),而返回值的生命周期被称为 **输出生命周期**(*output lifetimes*)。
|
||||
|
||||
现在介绍编译器用于判断引用何时不需要明确生命周期注解的规则。第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。
|
||||
编译器采用三条规则来判断引用何时不需要明确的注解。第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。
|
||||
|
||||
1. 每一个是引用的参数都有它自己的生命周期参数。换句话说就是,有一个引用参数的函数有一个生命周期参数:`fn foo<'a>(x: &'a i32)`,有两个引用参数的函数有两个不同的生命周期参数,`fn foo<'a, 'b>(x: &'a i32, y: &'b i32)`,依此类推。
|
||||
这些规则适用于 `fn` 定义,以及 `impl` 块。
|
||||
|
||||
2. 如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数:`fn foo<'a>(x: &'a i32) -> &'a i32`。
|
||||
第一条规则是每一个是引用的参数都有它自己的生命周期参数。换句话说就是,有一个引用参数的函数有一个生命周期参数:`fn foo<'a>(x: &'a i32)`,有两个引用参数的函数有两个不同的生命周期参数,`fn foo<'a, 'b>(x: &'a i32, y: &'b i32)`,依此类推。
|
||||
|
||||
3. 如果方法有多个输入生命周期参数,不过其中之一因为方法的缘故为 `&self` 或 `&mut self`,那么 `self` 的生命周期被赋给所有输出生命周期参数。这使得方法编写起来更简洁。
|
||||
第二条规则是如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数:`fn foo<'a>(x: &'a i32) -> &'a i32`。
|
||||
|
||||
假设我们自己就是编译器并来计算示例 10-25 `first_word` 函数的签名中的引用的生命周期。开始时签名中的引用并没有关联任何生命周期:
|
||||
第三条规则是如果方法有多个输入生命周期参数,不过其中之一因为方法的缘故为 `&self` 或 `&mut self`,那么 `self` 的生命周期被赋给所有输出生命周期参数。这使得方法更容易读写,因为只需更少的符号。
|
||||
|
||||
假设我们自己就是编译器。并应用这些规则来计算示例 10-26 中 `first_word` 函数签名中的引用的生命周期。开始时签名中的引用并没有关联任何生命周期:
|
||||
|
||||
```rust,ignore
|
||||
fn first_word(s: &str) -> &str {
|
||||
```
|
||||
|
||||
接着我们(作为编译器)应用第一条规则,也就是每个引用参数都有其自己的生命周期。我们像往常一样称之为 `'a`,所以现在签名看起来像这样:
|
||||
接着编译器应用第一条规则,也就是每个引用参数都有其自己的生命周期。我们像往常一样称之为 `'a`,所以现在签名看起来像这样:
|
||||
|
||||
```rust,ignore
|
||||
fn first_word<'a>(s: &'a str) -> &str {
|
||||
@ -414,7 +393,7 @@ fn first_word<'a>(s: &'a str) -> &'a str {
|
||||
|
||||
现在这个函数签名中的所有引用都有了生命周期,如此编译器可以继续它的分析而无须程序员标记这个函数签名中的生命周期。
|
||||
|
||||
让我们再看看另一个例子,这次我们从示例 10-22 中没有生命周期参数的 `longest` 函数开始:
|
||||
让我们再看看另一个例子,这次我们从示例 10-21 中没有生命周期参数的 `longest` 函数开始:
|
||||
|
||||
```rust,ignore
|
||||
fn longest(x: &str, y: &str) -> &str {
|
||||
@ -426,25 +405,17 @@ fn longest(x: &str, y: &str) -> &str {
|
||||
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
|
||||
```
|
||||
|
||||
再来应用第二条规则,它并不适用因为存在多于一个输入生命周期。再来看第三条规则,它同样也不适用因为没有 `self` 参数。然后我们就没有更多规则了,不过还没有计算出返回值的类型的生命周期。这就是为什么在编译示例 10-22 的代码时会出现错误的原因:编译器使用所有已知的生命周期省略规则,不过仍不能计算出签名中所有引用的生命周期。
|
||||
再来应用第二条规则,它并不适用因为存在多于一个输入生命周期。再来看第三条规则,它同样也不适用因为没有 `self` 参数。然后我们就没有更多规则了,不过还没有计算出返回值的类型的生命周期。这就是为什么在编译示例 10-21 的代码时会出现错误的原因:编译器使用所有已知的生命周期省略规则,不过仍不能计算出签名中所有引用的生命周期。
|
||||
|
||||
因为第三条规则真正能够适用的就只有方法签名,现在就让我们看看那种情况中的生命周期,并看看为什么这条规则意味着我们经常不需要在方法签名中标注生命周期。
|
||||
|
||||
### 方法定义中的生命周期注解
|
||||
|
||||
<!-- Is this different to the reference lifetime annotations, or just a
|
||||
finalized explanation? -->
|
||||
<!-- This is about lifetimes on references in method signatures, which is where
|
||||
the 3rd lifetime elision rule kicks in. It can also be confusing where lifetime
|
||||
parameters need to be declared and used since the lifetime parameters could go
|
||||
with the struct's fields or with references passed into or returned from
|
||||
methods. /Carol -->
|
||||
|
||||
当为带有生命周期的结构体实现方法时,其语法依然类似示例 10-11 中展示的泛型类型参数的语法:声明和使用生命周期参数的位置依赖于生命周期参数是否同结构体字段或方法参数和返回值相关。
|
||||
当为带有生命周期的结构体实现方法时,其语法依然类似示例 10-11 中展示的泛型类型参数的语法。声明和使用生命周期参数的位置依赖于生命周期参数是否同结构体字段或方法参数和返回值相关。
|
||||
|
||||
(实现方法时)结构体字段的生命周期必须总是在 `impl` 关键字之后声明并在结构体名称之后被使用,因为这些生命周期是结构体类型的一部分。
|
||||
|
||||
`impl` 块里的方法签名中,引用可能与结构体字段中的引用相关联,也可能是独立的。另外,生命周期省略规则也经常让我们无需在方法签名中使用生命周期注解。让我们看看一些使用示例 10-26 中定义的结构体 `ImportantExcerpt` 的例子。
|
||||
`impl` 块里的方法签名中,引用可能与结构体字段中的引用相关联,也可能是独立的。另外,生命周期省略规则也经常让我们无需在方法签名中使用生命周期注解。让我们看看一些使用示例 10-25 中定义的结构体 `ImportantExcerpt` 的例子。
|
||||
|
||||
首先,这里有一个方法 `level`。其唯一的参数是 `self` 的引用,而且返回值只是一个 `i32`,并不引用任何值:
|
||||
|
||||
@ -481,7 +452,7 @@ impl<'a> ImportantExcerpt<'a> {
|
||||
|
||||
### 静态生命周期
|
||||
|
||||
这里有 **一种** 特殊的生命周期值得讨论:`'static`。`'static` 生命周期存活于整个程序期间。所有的字符串字面值都拥有 `'static` 生命周期,我们也可以选择像下面这样标注出来:
|
||||
这里有一种特殊的生命周期值得讨论:`'static`,其生命周期存活于整个程序期间。所有的字符串字面值都拥有 `'static` 生命周期,我们也可以选择像下面这样标注出来:
|
||||
|
||||
```rust
|
||||
let s: &'static str = "I have a static lifetime.";
|
||||
@ -489,10 +460,7 @@ let s: &'static str = "I have a static lifetime.";
|
||||
|
||||
这个字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的。因此所有的字符串字面值都是 `'static` 的。
|
||||
|
||||
<!-- How would you add a static lifetime (below)? -->
|
||||
<!-- Just like you'd specify any lifetime, see above where it shows `&'static str`. /Carol -->
|
||||
|
||||
你可能在错误信息的帮助文本中见过使用 `'static` 生命周期的建议,不过将引用指定为 `'static` 之前,思考一下这个引用是否真的在整个程序的生命周期里都有效(或者哪怕你希望它一直有效,如果可能的话)。大部分情况,代码中的问题是尝试创建一个悬垂引用或者可用的生命周期不匹配,请解决这些问题而不是指定一个 `'static` 的生命周期。
|
||||
你可能在错误信息的帮助文本中见过使用 `'static` 生命周期的建议,不过将引用指定为 `'static` 之前,思考一下这个引用是否真的在整个程序的生命周期里都有效。你可能会考虑希望它一直有效,如果可能的话。大部分情况,代码中的问题是尝试创建一个悬垂引用或者可用的生命周期不匹配,请解决这些问题而不是指定一个 `'static` 的生命周期。
|
||||
|
||||
### 结合泛型类型参数、trait bounds 和生命周期
|
||||
|
||||
@ -513,7 +481,7 @@ fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a st
|
||||
}
|
||||
```
|
||||
|
||||
这个是示例 10-23 中那个返回两个字符串 slice 中较长者的 `longest` 函数,不过带有一个额外的参数 `ann`。`ann` 的类型是泛型 `T`,它可以被放入任何实现了 `where` 从句中指定的 `Display` trait 的类型。这个额外的参数会在函数比较字符串 slice 的长度之前被打印出来,这也就是为什么 `Display` trait bound 是必须的。因为生命周期也是泛型,所以生命周期参数 `'a` 和泛型类型参数 `T` 都位于函数名后的同一尖括号列表中。
|
||||
这个是示例 10-22 中那个返回两个字符串 slice 中较长者的 `longest` 函数,不过带有一个额外的参数 `ann`。`ann` 的类型是泛型 `T`,它可以被放入任何实现了 `where` 从句中指定的 `Display` trait 的类型。这个额外的参数会在函数比较字符串 slice 的长度之前被打印出来,这也就是为什么 `Display` trait bound 是必须的。因为生命周期也是泛型,所以生命周期参数 `'a` 和泛型类型参数 `T` 都位于函数名后的同一尖括号列表中。
|
||||
|
||||
## 总结
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
# 测试
|
||||
# 编写自动化测试
|
||||
|
||||
> [ch11-00-testing.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-00-testing.md)
|
||||
> [ch11-00-testing.md](https://github.com/rust-lang/book/blob/master/src/ch11-00-testing.md)
|
||||
> <br>
|
||||
> commit 4464eab0892297b83db7134b7ace12762a89b389
|
||||
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
|
||||
|
||||
> Program testing can be a very effective way to show the presence of bugs, but it is hopelessly inadequate for showing their absence.
|
||||
>
|
||||
@ -12,7 +12,9 @@
|
||||
>
|
||||
> Edsger W. Dijkstra,【谦卑的程序员】(1972)
|
||||
|
||||
这并不意味着我们不该尽可能测试软件!程序的正确性意味着代码如我们期望的那样运行。Rust 是一个相当注重正确性的编程语言,不过正确性是一个难以证明的复杂主题。Rust 的类型系统在此问题上下了很大的功夫,不过它不可能捕获所有种类的错误。为此,Rust 也在语言本身包含了编写软件测试的支持。
|
||||
Edsger W. Dijkstra 在其 1972 年的文章【谦卑的程序员】(“The Humble Programmer”)中说到 “软件测试是证明 bug 存在的有效方法,而证明其不存在时则显得令人绝望的不足。”(“Program testing can be a very effective way to show the presence of bugs, but it is hopelessly inadequate for showing their absence.”)这并不意味着我们不该尽可能地测试软件!
|
||||
|
||||
程序的正确性意味着代码如我们期望的那样运行。Rust 是一个相当注重正确性的编程语言,不过正确性是一个难以证明的复杂主题。Rust 的类型系统在此问题上下了很大的功夫,不过它不可能捕获所有种类的错误。为此,Rust 也在语言本身包含了编写软件测试的支持。
|
||||
|
||||
例如,我们可以编写一个叫做 `add_two` 的将传递给它的值加二的函数。它的签名有一个整型参数并返回一个整型值。当实现和编译这个函数时,Rust 会进行所有目前我们已经见过的类型检查和借用检查,例如,这些检查会确保我们不会传递 `String` 或无效的引用给这个函数。Rust 所 **不能** 检查的是这个函数是否会准确的完成我们期望的工作:返回参数加二后的值,而不是比如说参数加 10 或减 50 的值!这也就是测试出场的地方。
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
## 如何编写测试
|
||||
|
||||
> [ch11-01-writing-tests.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-01-writing-tests.md)
|
||||
> [ch11-01-writing-tests.md](https://github.com/rust-lang/book/blob/master/src/ch11-01-writing-tests.md)
|
||||
> <br>
|
||||
> commit 4464eab0892297b83db7134b7ace12762a89b389
|
||||
> commit 820ac357f6cf0e866e5a8e7a9c57dd3e17e9f8ca
|
||||
|
||||
Rust 中的测试函数是用来验证非测试代码是否按照期望的方式运行的。测试函数体通常执行如下三种操作:
|
||||
|
||||
@ -19,21 +19,23 @@ Rust 中的测试函数是用来验证非测试代码是否按照期望的方式
|
||||
|
||||
第七章当使用 Cargo 新建一个库项目时,它会自动为我们生成一个测试模块和一个测试函数。这有助于我们开始编写测试,因为这样每次开始新项目时不必去查找测试函数的具体结构和语法了。当然你也可以额外增加任意多的测试函数以及测试模块!
|
||||
|
||||
为了理清测试是如何工作的,我们将通过观察那些自动生成的测试模版——尽管它们实际上没有测试任何代码。接着,我们会写一些真正的测试,调用我们编写的代码并断言他们的行为的正确性。
|
||||
我们会通过实验那些自动生成的测试模版而不是实际编写测试代码来探索测试如何工作的一些方面。接着,我们会写一些真正的测试,调用我们编写的代码并断言他们的行为的正确性。
|
||||
|
||||
让我们创建一个新的库项目 `adder`:
|
||||
|
||||
```text
|
||||
$ cargo new adder
|
||||
$ cargo new adder --lib
|
||||
Created library `adder` project
|
||||
$ cd adder
|
||||
```
|
||||
|
||||
|
||||
adder 库中 `src/lib.rs` 的内容应该看起来如示例 11-1 所示:
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
# fn main() {}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
@ -75,15 +77,18 @@ Cargo 编译并运行了测试。在 `Compiling`、`Finished` 和 `Running` 这
|
||||
|
||||
因为之前我们并没有将任何测试标记为忽略,所以摘要中会显示 `0 ignored`。我们也没有过滤需要运行的测试,所以摘要中会显示`0 filtered out`。在下一部分 “控制测试如何运行” 会讨论忽略和过滤测试。
|
||||
|
||||
`0 measured` 统计是针对性能测试的。性能测试(benchmark tests)在编写本书时,仍只能用于 Rust 开发版(nightly Rust)。请查看第一章来了解更多 Rust 开发版的信息。
|
||||
`0 measured` 统计是针对性能测试的。性能测试(benchmark tests)在编写本书时,仍只能用于 Rust 开发版(nightly Rust)。请查看 [性能测试的文档][bench] 了解更多。
|
||||
|
||||
测试输出中以 `Doc-tests adder` 开头的这一部分是所有文档测试的结果。我们现在并没有任何文档测试,不过 Rust 会编译任何在 API 文档中的代码示例。这个功能帮助我们使文档和代码保持同步!在第十四章的 “文档注释” 部分会讲到如何编写文档测试。现在我们将忽略 `Doc-tests` 部分的输出。
|
||||
[bench]: https://doc.rust-lang.org/unstable-book/library-features/test.html
|
||||
|
||||
测试输出的中以 `Doc-tests adder` 开头的这一部分是所有文档测试的结果。我们现在并没有任何文档测试,不过 Rust 会编译任何在 API 文档中的代码示例。这个功能帮助我们使文档和代码保持同步!在第十四章的 “文档注释” 部分会讲到如何编写文档测试。现在我们将忽略 `Doc-tests` 部分的输出。
|
||||
|
||||
让我们改变测试的名称并看看这如何改变测试的输出。给 `it_works` 函数起个不同的名字,比如 `exploration`,像这样:
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
# fn main() {}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
@ -106,7 +111,8 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
```rust,panics
|
||||
# fn main() {}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
@ -123,7 +129,6 @@ mod tests {
|
||||
|
||||
<span class="caption">示例 11-3:增加第二个因调用了 `panic!` 而失败的测试</span>
|
||||
|
||||
|
||||
再次 `cargo test` 运行测试。输出应该看起来像示例 11-4,它表明 `exploration` 测试通过了而 `another` 失败了:
|
||||
|
||||
```text
|
||||
@ -162,6 +167,7 @@ error: test failed
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
# fn main() {}
|
||||
#[derive(Debug)]
|
||||
pub struct Rectangle {
|
||||
length: u32,
|
||||
@ -182,6 +188,7 @@ impl Rectangle {
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
# fn main() {}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@ -198,7 +205,7 @@ mod tests {
|
||||
|
||||
<span class="caption">示例 11-6:一个 `can_hold` 的测试,检查一个较大的矩形确实能放得下一个较小的矩形</span>
|
||||
|
||||
注意在 `tests` 模块中新增加了一行:`use super::*;`。`tests` 是一个普通的模块,它遵循第七章 “私有性规则” 部分介绍的可见性规则。因为这是一个内部模块,要测试外部模块中的代码,需要将其引入到内部模块的作用域中。这里选择使用全局导入,以便在 `tests` 模块中使用所有在外部模块定义的内容。
|
||||
注意在 `tests` 模块中新增加了一行:`use super::*;`。`tests` 是一个普通的模块,它遵循第七章 “私有性规则” 部分介绍的可见性规则。因为这是一个内部模块,要测试外部模块中的代码,需要将其引入到内部模块的作用域中。这里选择使用 glob 全局导入,以便在 `tests` 模块中使用所有在外部模块定义的内容。
|
||||
|
||||
我们将测试命名为 `larger_can_hold_smaller`,并创建所需的两个 `Rectangle` 实例。接着调用 `assert!` 宏并传递 `larger.can_hold(&smaller)` 调用的结果作为参数。这个表达式预期会返回 `true`,所以测试应该通过。让我们拭目以待!
|
||||
|
||||
@ -214,6 +221,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
# fn main() {}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@ -245,7 +253,8 @@ test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
|
||||
两个通过的测试!现在让我们看看如果引入一个 bug 的话测试结果会发生什么。将 `can_hold` 方法中比较长度时本应使用大于号的地方改成小于号:
|
||||
|
||||
```rust
|
||||
```rust,not_desired_behavior
|
||||
# fn main() {}
|
||||
# #[derive(Debug)]
|
||||
# pub struct Rectangle {
|
||||
# length: u32,
|
||||
@ -284,13 +293,14 @@ test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
|
||||
### 使用 `assert_eq!` 和 `assert_ne!` 宏来测试相等
|
||||
|
||||
测试功能的一个常用方法是将需要测试代码的值与期望值做比较,并检查是否相等。可以通过向 `assert!` 宏传递一个使用 `==` 运算符的表达式来做到。不过这个操作实在是太常见了,以至于标准库提供了一对宏来更方便的处理这些操作:`assert_eq!` 和 `assert_ne!`。这两个宏分别比较两个值是相等还是不相等。当断言失败时他们也会打印出这两个值具体是什么,以便于观察测试 **为什么** 失败,而 `assert!` 只会打印出它从 `==` 表达式中得到了 `false` 值,而不是导致 `false` 的两个值。
|
||||
测试功能的一个常用方法是将需要测试代码的值与期望值做比较,并检查是否相等。可以通过向 `assert!` 宏传递一个使用 `==` 运算符的表达式来做到。不过这个操作实在是太常见了,以至于标准库提供了一对宏来更方便的处理这些操作 —— `assert_eq!` 和 `assert_ne!`。这两个宏分别比较两个值是相等还是不相等。当断言失败时他们也会打印出这两个值具体是什么,以便于观察测试 **为什么** 失败,而 `assert!` 只会打印出它从 `==` 表达式中得到了 `false` 值,而不是导致 `false` 的两个值。
|
||||
|
||||
示例 11-7 中,让我们编写一个对其参数加二并返回结果的函数 `add_two`。接着使用 `assert_eq!` 宏测试这个函数:
|
||||
示例 11-7 中,让我们编写一个对其参数加二并返回结果的函数 `add_two`。接着使用 `assert_eq!` 宏测试这个函数。
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
# fn main() {}
|
||||
pub fn add_two(a: i32) -> i32 {
|
||||
a + 2
|
||||
}
|
||||
@ -321,7 +331,8 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
|
||||
在代码中引入一个 bug 来看看使用 `assert_eq!` 的测试失败是什么样的。修改 `add_two` 函数的实现使其加 3:
|
||||
|
||||
```rust
|
||||
```rust,not_desired_behavior
|
||||
# fn main() {}
|
||||
pub fn add_two(a: i32) -> i32 {
|
||||
a + 3
|
||||
}
|
||||
@ -353,17 +364,18 @@ test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
|
||||
`assert_ne!` 宏在传递给它的两个值不相等时通过,而在相等时失败。在代码按预期运行,我们不确定值 **会** 是什么,不过能确定值绝对 **不会** 是什么的时候,这个宏最有用处。例如,如果一个函数保证会以某种方式改变其输出,不过这种改变方式是由运行测试时是星期几来决定的,这时最好的断言可能就是函数的输出不等于其输入。
|
||||
|
||||
`assert_eq!` 和 `assert_ne!` 宏在底层分别使用了 `==` 和 `!=`。当断言失败时,这些宏会使用调试格式打印出其参数,这意味着被比较的值必需实现了 `PartialEq` 和 `Debug` trait。所有的基本类型和大部分标准库类型都实现了这些 trait。对于自定义的结构体和枚举,需要实现 `PartialEq` 才能断言他们的值是否相等。需要实现 `Debug` 才能在断言失败时打印他们的值。因为这两个 trait 都是派生 trait,如第五章示例 5-12 所提到的,通常可以直接在结构体或枚举上添加 `#[derive(PartialEq, Debug)]` 注解。附录 C 中有更多关于这些和其他派生 trait 的详细信息。
|
||||
`assert_eq!` 和 `assert_ne!` 宏在底层分别使用了 `==` 和 `!=`。当断言失败时,这些宏会使用调试格式打印出其参数,这意味着被比较的值必需实现了 `PartialEq` 和 `Debug` trait。所有的基本类型和大部分标准库类型都实现了这些 trait。对于自定义的结构体和枚举,需要实现 `PartialEq` 才能断言他们的值是否相等。需要实现 `Debug` 才能在断言失败时打印他们的值。因为这两个 trait 都是派生 trait,如第五章示例 5-12 所提到的,通常可以直接在结构体或枚举上添加 `#[derive(PartialEq, Debug)]` 注解。附录 C “可派生 trait” 中有更多关于这些和其他派生 trait 的详细信息。
|
||||
|
||||
### 自定义失败信息
|
||||
|
||||
你也可以向 `assert!`、`assert_eq!` 和 `assert_ne!` 宏传递一个可选的失败信息参数,可以在测试失败时将自定义失败信息一同打印出来。任何在 `assert!` 的一个必需参数和 `assert_eq!` 和 `assert_ne!` 的两个必需参数之后指定的参数都会传递给 `format!` 宏(在第八章的“使用 `+` 运算符或 `format!` 宏连接字符串”部分讨论过),所以可以传递一个包含 `{}` 占位符的格式字符串和需要放入占位符的值。自定义信息有助于记录断言的意义;当测试失败时就能更好的理解代码出了什么问题。
|
||||
你也可以向 `assert!`、`assert_eq!` 和 `assert_ne!` 宏传递一个可选的失败信息参数,可以在测试失败时将自定义失败信息一同打印出来。任何在 `assert!` 的一个必需参数和 `assert_eq!` 和 `assert_ne!` 的两个必需参数之后指定的参数都会传递给 `format!` 宏(在第八章的“使用 `+` 运算符或 `format!` 宏拼接字符串”部分讨论过),所以可以传递一个包含 `{}` 占位符的格式字符串和需要放入占位符的值。自定义信息有助于记录断言的意义;当测试失败时就能更好的理解代码出了什么问题。
|
||||
|
||||
例如,比如说有一个根据人名进行问候的函数,而我们希望测试将传递给函数的人名显示在输出中:
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
# fn main() {}
|
||||
pub fn greeting(name: &str) -> String {
|
||||
format!("Hello {}!", name)
|
||||
}
|
||||
@ -384,7 +396,8 @@ mod tests {
|
||||
|
||||
让我们通过将 `greeting` 改为不包含 `name` 来在代码中引入一个 bug 来测试失败时是怎样的:
|
||||
|
||||
```rust
|
||||
```rust,not_desired_behavior
|
||||
# fn main() {}
|
||||
pub fn greeting(name: &str) -> String {
|
||||
String::from("Hello!")
|
||||
}
|
||||
@ -407,7 +420,7 @@ failures:
|
||||
tests::greeting_contains_name
|
||||
```
|
||||
|
||||
这仅仅告诉了我们断言失败了和失败的行号。一个更有用的失败信息应该打印出 `greeting` 函数的值。让我们为测试函数增加一个自定义失败信息参数:带占位符的格式字符串,以及 `greeting` 函数的值:
|
||||
结果仅仅告诉了我们断言失败了和失败的行号。一个更有用的失败信息应该打印出 `greeting` 函数的值。让我们为测试函数增加一个自定义失败信息参数:带占位符的格式字符串,以及 `greeting` 函数的值:
|
||||
|
||||
```rust,ignore
|
||||
#[test]
|
||||
@ -422,7 +435,6 @@ fn greeting_contains_name() {
|
||||
|
||||
现在如果再次运行测试,将会看到更有价值的信息:
|
||||
|
||||
|
||||
```text
|
||||
---- tests::greeting_contains_name stdout ----
|
||||
thread 'tests::greeting_contains_name' panicked at 'Greeting did not
|
||||
@ -434,7 +446,7 @@ note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||||
|
||||
### 使用 `should_panic` 检查 panic
|
||||
|
||||
除了检查代码是否返回期望的正确的值之外,检查代码是否按照期望处理错误也是很重要的。例如,考虑第九章示例 9-9 创建的 `Guess` 类型。其他使用 `Guess` 的代码都是基于 `Guess` 实例仅有的值范围在 1 到 100 的前提。可以编写一个测试来确保创建一个超出范围的值的 `Guess` 实例会 panic。
|
||||
除了检查代码是否返回期望的正确的值之外,检查代码是否按照期望处理错误也是很重要的。例如,考虑第九章示例 9-10 创建的 `Guess` 类型。其他使用 `Guess` 的代码都是基于 `Guess` 实例仅有的值范围在 1 到 100 的前提。可以编写一个测试来确保创建一个超出范围的值的 `Guess` 实例会 panic。
|
||||
|
||||
可以通过对函数增加另一个属性 `should_panic` 来实现这些。这个属性在函数中的代码 panic 时会通过,而在其中的代码没有 panic 时失败。
|
||||
|
||||
@ -443,12 +455,13 @@ note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
# fn main() {}
|
||||
pub struct Guess {
|
||||
value: u32,
|
||||
value: i32,
|
||||
}
|
||||
|
||||
impl Guess {
|
||||
pub fn new(value: u32) -> Guess {
|
||||
pub fn new(value: i32) -> Guess {
|
||||
if value < 1 || value > 100 {
|
||||
panic!("Guess value must be between 1 and 100, got {}.", value);
|
||||
}
|
||||
@ -484,15 +497,16 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
|
||||
看起来不错!现在在代码中引入 bug,移除 `new` 函数在值大于 100 时会 panic 的条件:
|
||||
|
||||
```rust
|
||||
```rust,not_desired_behavior
|
||||
# fn main() {}
|
||||
# pub struct Guess {
|
||||
# value: u32,
|
||||
# value: i32,
|
||||
# }
|
||||
#
|
||||
// --snip--
|
||||
|
||||
impl Guess {
|
||||
pub fn new(value: u32) -> Guess {
|
||||
pub fn new(value: i32) -> Guess {
|
||||
if value < 1 {
|
||||
panic!("Guess value must be between 1 and 100, got {}.", value);
|
||||
}
|
||||
@ -525,14 +539,15 @@ test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
# fn main() {}
|
||||
# pub struct Guess {
|
||||
# value: u32,
|
||||
# value: i32,
|
||||
# }
|
||||
#
|
||||
// --snip--
|
||||
|
||||
impl Guess {
|
||||
pub fn new(value: u32) -> Guess {
|
||||
pub fn new(value: i32) -> Guess {
|
||||
if value < 1 {
|
||||
panic!("Guess value must be greater than or equal to 1, got {}.",
|
||||
value);
|
||||
@ -565,7 +580,7 @@ mod tests {
|
||||
|
||||
为了观察带有 `expected` 信息的 `should_panic` 测试失败时会发生什么,让我们再次引入一个 bug,将 `if value < 1` 和 `else if value > 100` 的代码块对换:
|
||||
|
||||
```rust,ignore
|
||||
```rust,ignore,not_desired_behavior
|
||||
if value < 1 {
|
||||
panic!("Guess value must be less than or equal to 100, got {}.", value);
|
||||
} else if value > 100 {
|
||||
@ -596,4 +611,24 @@ test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
|
||||
失败信息表明测试确实如期望 panic 了,不过 panic 信息中并没有包含 `expected` 信息 `'Guess value must be less than or equal to 100'`。而我们得到的 panic 信息是 `'Guess value must be greater than or equal to 1, got 200.'`。这样就可以开始寻找 bug 在哪了!
|
||||
|
||||
### 将 `Result<T, E>` 用于测试
|
||||
|
||||
目前为止,我们编写的测试在失败时就会 panic。也可以使用 `Result<T, E>` 编写测试!这里是第一个例子采用了 Result:
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() -> Result<(), String> {
|
||||
if 2 + 2 == 4 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(String::from("two plus two does not equal four"))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这里我们将 `it_works` 改为返回 Result。同时在函数体中,在成功时返回 `Ok(())` 而不是 `assert_eq!`,而失败时返回带有 `String` 的 `Err`。跟之前一样,这个测试可能成功或失败,不过不再通过 panic,可以通过 `Result<T, E>` 来判断结果。为此不能在对这些函数使用 `#[should_panic]`;而是应该返回 `Err`!
|
||||
|
||||
现在你知道了几种编写测试的方法,让我们看看运行测试时会发生什么,和可以用于 `cargo test` 的不同选项。
|
||||
|
@ -1,8 +1,8 @@
|
||||
## 控制测试如何运行
|
||||
|
||||
> [ch11-02-running-tests.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-02-running-tests.md)
|
||||
> [ch11-02-running-tests.md](https://github.com/rust-lang/book/blob/master/src/ch11-02-running-tests.md)
|
||||
> <br>
|
||||
> commit 550c8ea6f74060ff1f7b67e7e1878c4da121682d
|
||||
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
|
||||
|
||||
就像 `cargo run` 会编译代码并运行生成的二进制文件一样,`cargo test` 在测试模式下编译代码并运行生成的测试二进制文件。可以指定命令行参数来改变 `cargo test` 的默认行为。例如,`cargo test` 生成的二进制文件的默认行为是并行的运行所有测试,并截获测试运行过程中产生的输出,阻止他们被显示出来,使得阅读测试结果相关的内容变得更容易。
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
$ cargo test -- --test-threads=1
|
||||
```
|
||||
|
||||
这里将测试线程设置为 1,告诉程序不要使用任何并行机制。这也会比并行运行花费更多时间,不过在有共享的状态时,测试就不会潜在的相互干扰了。
|
||||
这里将测试线程设置为 `1`,告诉程序不要使用任何并行机制。这也会比并行运行花费更多时间,不过在有共享的状态时,测试就不会潜在的相互干扰了。
|
||||
|
||||
### 显示函数输出
|
||||
|
||||
@ -30,7 +30,7 @@ $ cargo test -- --test-threads=1
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
```rust,panics
|
||||
fn prints_and_returns_10(a: i32) -> i32 {
|
||||
println!("I got the value {}", a);
|
||||
10
|
||||
|
@ -1,8 +1,8 @@
|
||||
## 测试的组织结构
|
||||
|
||||
> [ch11-03-test-organization.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-03-test-organization.md)
|
||||
> [ch11-03-test-organization.md](https://github.com/rust-lang/book/blob/master/src/ch11-03-test-organization.md)
|
||||
> <br>
|
||||
> commit b3eddb8edc0c3f83647143673d18efac0a44083a
|
||||
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
|
||||
|
||||
本章一开始就提到,测试是一个复杂的概念,而且不同的开发者也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:**单元测试**(*unit tests*)与 **集成测试**(*integration tests*)。单元测试倾向于更小而更集中,在隔离的环境中一次测试一个模块,或者是测试私有接口。而集成测试对于你的库来说则完全是外部的。它们与其他外部代码一样,通过相同的方式使用你的代码,只测试公有接口而且每个测试都有可能会测试多个模块。
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
|
||||
测试模块的 `#[cfg(test)]` 注解告诉 Rust 只在执行 `cargo test` 时才编译和运行测试代码,而在运行 `cargo build` 时不这么做。这在只希望构建库的时候可以节省编译时间,并且因为它们并没有包含测试,所以能减少编译产生的文件的大小。与之对应的集成测试因为位于另一个文件夹,所以它们并不需要 `#[cfg(test)]` 注解。然而单元测试位于与源码相同的文件中,所以你需要使用 `#[cfg(test)]` 来指定他们不应该被包含进编译结果中。
|
||||
|
||||
还记得本章第一部分新建的 `adder` 项目吗?Cargo 为我们生成了如下代码:
|
||||
回忆本章第一部分新建的 `adder` 项目吗,Cargo 为我们生成了如下代码:
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
@ -39,6 +39,8 @@ mod tests {
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
# fn main() {}
|
||||
|
||||
pub fn add_two(a: i32) -> i32 {
|
||||
internal_adder(a, 2)
|
||||
}
|
||||
@ -75,7 +77,7 @@ mod tests {
|
||||
<span class="filename">文件名: tests/integration_test.rs</span>
|
||||
|
||||
```rust,ignore
|
||||
extern crate adder;
|
||||
use adder;
|
||||
|
||||
#[test]
|
||||
fn it_adds_two() {
|
||||
@ -147,7 +149,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
|
||||
```rust
|
||||
pub fn setup() {
|
||||
// setup code specific to your library's tests would go here
|
||||
// 编写特定库测试所需的代码
|
||||
}
|
||||
```
|
||||
|
||||
@ -188,7 +190,7 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
<span class="filename">文件名: tests/integration_test.rs</span>
|
||||
|
||||
```rust,ignore
|
||||
extern crate adder;
|
||||
use adder;
|
||||
|
||||
mod common;
|
||||
|
||||
@ -199,7 +201,7 @@ fn it_adds_two() {
|
||||
}
|
||||
```
|
||||
|
||||
注意 `mod common;` 声明与示例 7-4 中展示的模块声明相同。接着在测试函数中就可以调用 `common::setup()` 了。
|
||||
注意 `mod common;` 声明与示例 7-25 中展示的模块声明相同。接着在测试函数中就可以调用 `common::setup()` 了。
|
||||
|
||||
#### 二进制 crate 的集成测试
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
# 一个 I/O 项目:构建一个命令行程序
|
||||
|
||||
> [ch12-00-an-io-project.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-00-an-io-project.md)
|
||||
> [ch12-00-an-io-project.md](https://github.com/rust-lang/book/blob/master/src/ch12-00-an-io-project.md)
|
||||
> <br>
|
||||
> commit 97e60b3cb623d4a5b85419212b085ade8a11cbe1
|
||||
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
|
||||
|
||||
本章既是一个目前所学的很多技能的概括,也是一个更多标准库功能的探索。我们将构建一个与文件和命令行输入/输出交互的命令行工具来练习现在一些你已经掌握的 Rust 技能。
|
||||
|
||||
Rust 的运行速度、安全性、**单二进制文件** 输出和跨平台支持使其成为创建命令行程序的绝佳选择,所以我们的项目将创建一个我们自己版本的经典命令行工具:`grep`。grep 是 “**G**lobally search a **R**egular **E**xpression and **P**rint.” 的首字母缩写。`grep` 最简单的使用场景是在特定文件中搜索指定字符串。为此,`grep` 获取一个文件名和一个字符串作为参数,接着读取文件并找到其中包含字符串参数的行,然后打印出这些行。
|
||||
Rust 的运行速度、安全性、单二进制文件输出和跨平台支持使其成为创建命令行程序的绝佳选择,所以我们的项目将创建一个我们自己版本的经典命令行工具:`grep`。grep 是 “**G**lobally search a **R**egular **E**xpression and **P**rint.” 的首字母缩写。`grep` 最简单的使用场景是在特定文件中搜索指定字符串。为此,`grep` 获取一个文件名和一个字符串作为参数,接着读取文件并找到其中包含字符串参数的行,然后打印出这些行。
|
||||
|
||||
在这个过程中,我们会展示如何让我们的命令行工具利用很多命令行工具中用到的终端功能。读取环境变量来使得用户可以配置工具的行为。打印到标准错误控制流(`stderr`) 而不是标准输出(`stdout`),例如这样用户可以选择将成功输出重定向到文件中的同时仍然在屏幕上显示错误信息。
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
## 接受命令行参数
|
||||
|
||||
> [ch12-01-accepting-command-line-arguments.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-01-accepting-command-line-arguments.md)
|
||||
> [ch12-01-accepting-command-line-arguments.md](https://github.com/rust-lang/book/blob/master/src/ch12-01-accepting-command-line-arguments.md)
|
||||
> <br>
|
||||
> commit 97e60b3cb623d4a5b85419212b085ade8a11cbe1
|
||||
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
|
||||
|
||||
一如之前使用 `cargo new` 新建一个项目,我们称之为 `minigrep` 以便与可能已经安装在系统上的`grep`工具相区别:
|
||||
一如既往使用 `cargo new` 新建一个项目,我们称之为 `minigrep` 以便与可能已经安装在系统上的 `grep` 工具相区别:
|
||||
|
||||
```text
|
||||
$ cargo new --bin minigrep
|
||||
$ cargo new minigrep
|
||||
Created binary (application) `minigrep` project
|
||||
$ cd minigrep
|
||||
```
|
||||
@ -37,7 +37,6 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
<span class="caption">示例 12-1:将命令行参数收集到一个 vector 中并打印出来</span>
|
||||
|
||||
首先使用 `use` 语句来将 `std::env` 模块引入作用域以便可以使用它的 `args` 函数。注意 `std::env::args` 函数被嵌套进了两层模块中。正如第七章讲到的,当所需函数嵌套了多于一层模块时,通常将父模块引入作用域,而不是其自身。这便于我们利用 `std::env` 中的其他函数。这比增加了 `use std::env::args;` 后仅仅使用 `args` 调用函数要更明确一些,因为 `args` 容易被错认成一个定义于当前模块的函数。
|
||||
|
@ -1,18 +1,18 @@
|
||||
## 读取文件
|
||||
|
||||
> [ch12-02-reading-a-file.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-02-reading-a-file.md)
|
||||
> [ch12-02-reading-a-file.md](https://github.com/rust-lang/book/blob/master/src/ch12-02-reading-a-file.md)
|
||||
> <br>
|
||||
> commit 97e60b3cb623d4a5b85419212b085ade8a11cbe1
|
||||
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
|
||||
|
||||
接下来我们将读取由命令行文件名参数指定的文件。首先,需要一个用来测试的示例文件——用来确保 `minigrep` 正常工作的最好的文件是拥有多行少量文本且有一些重复单词的文件。示例 12-3 是一首艾米莉·狄金森(Emily Dickinson)的诗,它正适合这个工作!在项目根目录创建一个文件 `poem.txt`,并输入诗 "I'm nobody! Who are you?":
|
||||
现在我们要增加读取由 `filename` 命令行参数指定的文件的功能。首先,需要一个用来测试的示例文件:用来确保 `minigrep` 正常工作的最好的文件是拥有多行少量文本且有一些重复单词的文件。示例 12-3 是一首艾米莉·狄金森(Emily Dickinson)的诗,它正适合这个工作!在项目根目录创建一个文件 `poem.txt`,并输入诗 "I'm nobody! Who are you?":
|
||||
|
||||
<span class="filename">文件名: poem.txt</span>
|
||||
|
||||
```text
|
||||
I’m nobody! Who are you?
|
||||
I'm nobody! Who are you?
|
||||
Are you nobody, too?
|
||||
Then there’s a pair of us — don’t tell!
|
||||
They’d banish us, you know.
|
||||
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
|
||||
@ -28,8 +28,7 @@ To an admiring bog!
|
||||
|
||||
```rust,should_panic
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::fs;
|
||||
|
||||
fn main() {
|
||||
# let args: Vec<String> = env::args().collect();
|
||||
@ -41,11 +40,8 @@ fn main() {
|
||||
// --snip--
|
||||
println!("In file {}", filename);
|
||||
|
||||
let mut f = File::open(filename).expect("file not found");
|
||||
|
||||
let mut contents = String::new();
|
||||
f.read_to_string(&mut contents)
|
||||
.expect("something went wrong reading the file");
|
||||
let contents = fs::read_to_string(filename)
|
||||
.expect("Something went wrong reading the file");
|
||||
|
||||
println!("With text:\n{}", contents);
|
||||
}
|
||||
@ -53,9 +49,9 @@ fn main() {
|
||||
|
||||
<span class="caption">示例 12-4:读取第二个参数所指定的文件内容</span>
|
||||
|
||||
首先,我们增加了更多的 `use` 语句来引入标准库中的相关部分:需要 `std::fs::File` 来处理文件,而 `std::io::prelude::*` 则包含许多对于 I/O(包括文件 I/O)有帮助的 trait。类似于 Rust 有一个通用的 prelude 来自动引入特定内容,`std::io` 也有其自己的 prelude 来引入处理 I/O 时所需的通用内容。不同于默认的 prelude,必须显式 `use` 位于 `std::io` 中的 prelude。
|
||||
首先,我们增加了更多的 `use` 语句来引入标准库中的相关部分:需要 `std::fs` 来处理文件。
|
||||
|
||||
在 `main` 中,我们增加了三点内容:第一,通过传递变量 `filename` 的值调用 `File::open` 函数来获取文件的可变句柄。创建了叫做 `contents` 的变量并将其设置为一个可变的,空的 `String`。它将会存放之后读取的文件的内容。第三,对文件句柄调用 `read_to_string` 并传递 `contents` 的可变引用作为参数。
|
||||
在 `main` 中新增了一行语句:`fs::read_to_string` 接受 `filename`,打开文件,接着返回包含其内容的 `Result<String>`。
|
||||
|
||||
在这些代码之后,我们再次增加了临时的 `println!` 打印出读取文件后 `contents` 的值,这样就可以检查目前为止的程序能否工作。
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
## 重构改进模块性和错误处理
|
||||
|
||||
> [ch12-03-improving-error-handling-and-modularity.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-03-improving-error-handling-and-modularity.md)
|
||||
> [ch12-03-improving-error-handling-and-modularity.md](https://github.com/rust-lang/book/blob/master/src/ch12-03-improving-error-handling-and-modularity.md)
|
||||
> <br>
|
||||
> commit c1fb695e6c9091c9a5145320498ef80a649af33c
|
||||
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
|
||||
|
||||
为了改善我们的程序这里有四个问题需要修复,而且他们都与程序的组织方式和如何处理潜在错误有关。
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
|
||||
第三个问题是如果打开文件失败我们使用 `expect` 来打印出错误信息,不过这个错误信息只是说 `file not found`。除了缺少文件之外还有很多打开文件可能失败的方式:例如,文件可能存在,不过可能没有打开它的权限。如果我们现在就出于这种情况,打印出的 `file not found` 错误信息就给了用户错误的建议!
|
||||
|
||||
第四,我们不停的使用 `expect` 来处理不同的错误,如果用户没有指定足够的参数来运行程序,他们会从 Rust 得到 "index out of bounds" 错误,而这并不能明确的解释问题。如果所有的错误处理都位于一处这样将来的维护者在需要修改错误处理逻辑时就只需要考虑这一处代码。将所有的错误处理都放在一处也有助于确保我们打印的错误信息对终端用户来说是有意义的。
|
||||
第四,我们不停的使用 `expect` 来处理不同的错误,如果用户没有指定足够的参数来运行程序,他们会从 Rust 得到 `index out of bounds` 错误,而这并不能明确的解释问题。如果所有的错误处理都位于一处这样将来的维护者在需要修改错误处理逻辑时就只需要考虑这一处代码。将所有的错误处理都放在一处也有助于确保我们打印的错误信息对终端用户来说是有意义的。
|
||||
|
||||
让我们通过重构项目来解决这些问题。
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
2. 当命令行解析逻辑比较小时,可以保留在 *main.rs* 中。
|
||||
3. 当命令行解析开始变得复杂时,也同样将其从 *main.rs* 提取到 *lib.rs* 中。
|
||||
4. 经过这些过程之后保留在 `main` 函数中的责任应该被限制为:
|
||||
|
||||
* 使用参数值调用命令行解析逻辑
|
||||
* 设置任何其他的配置
|
||||
* 调用 *lib.rs* 中的 `run` 函数
|
||||
@ -74,7 +75,7 @@ fn parse_config(args: &[String]) -> (&str, &str) {
|
||||
|
||||
```rust,should_panic
|
||||
# use std::env;
|
||||
# use std::fs::File;
|
||||
# use std::fs;
|
||||
#
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
@ -84,7 +85,8 @@ fn main() {
|
||||
println!("Searching for {}", config.query);
|
||||
println!("In file {}", config.filename);
|
||||
|
||||
let mut f = File::open(config.filename).expect("file not found");
|
||||
let contents = fs::read_to_string(config.filename)
|
||||
.expect("Something went wrong reading the file");
|
||||
|
||||
// --snip--
|
||||
}
|
||||
@ -174,7 +176,7 @@ note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||||
|
||||
#### 改善错误信息
|
||||
|
||||
在示例 12-8 中,在 `new` 函数中增加了一个检查在访问索引 1 和 2 之前检查 slice 是否足够长。如果 slice 不够长,我们使用一个更好的错误信息 panic 而不是 `index out of bounds` 信息:
|
||||
在示例 12-8 中,在 `new` 函数中增加了一个检查在访问索引 `1` 和 `2` 之前检查 slice 是否足够长。如果 slice 不够长,我们使用一个更好的错误信息 panic 而不是 `index out of bounds` 信息:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -189,7 +191,7 @@ fn new(args: &[String]) -> Config {
|
||||
|
||||
<span class="caption">示例 12-8:增加一个参数数量检查</span>
|
||||
|
||||
这类似于示例 9-9 中的 `Guess::new` 函数,那里如果 `value` 参数超出了有效值的范围就调用 `panic!`。不同于检查值的范围,这里检查 `args` 的长度至少是 3,而函数的剩余部分则可以在假设这个条件成立的基础上运行。如果
|
||||
这类似于示例 9-9 中的 `Guess::new` 函数,那里如果 `value` 参数超出了有效值的范围就调用 `panic!`。不同于检查值的范围,这里检查 `args` 的长度至少是 `3`,而函数的剩余部分则可以在假设这个条件成立的基础上运行。如果
|
||||
`args` 少于 3 个项,则这个条件将为真,并调用 `panic!` 立即终止程序。
|
||||
|
||||
有了 `new` 中这几行额外的代码,再次不带任何参数运行程序并看看现在错误看起来像什么:
|
||||
@ -203,13 +205,13 @@ thread 'main' panicked at 'not enough arguments', src/main.rs:30:12
|
||||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||||
```
|
||||
|
||||
这个输出就好多了,现在有了一个合理的错误信息。然而,还是有一堆额外的信息我们不希望提供给用户。所以在这里使用示例 9-9 中的技术可能不是最好的;正如第九章所讲到的一样,`panic!` 的调用更趋向于程序上的问题而不是使用上的问题。相反我们可以使用第九章学习的另一个技术:返回一个可以表明成功或错误的 `Result`。
|
||||
这个输出就好多了,现在有了一个合理的错误信息。然而,还是有一堆额外的信息我们不希望提供给用户。所以在这里使用示例 9-9 中的技术可能不是最好的;正如第九章所讲到的一样,`panic!` 的调用更趋向于程序上的问题而不是使用上的问题。相反我们可以使用第九章学习的另一个技术 —— 返回一个可以表明成功或错误的 `Result`。
|
||||
|
||||
#### 从 `new` 中返回 `Result` 而不是调用 `panic!`
|
||||
|
||||
我们可以选择返回一个 `Result` 值,它在成功时会包含一个 `Config` 的实例,而在错误时会描述问题。当 `Config::new` 与 `main` 交流时,可以使用 `Result` 类型来表明这里存在问题。接着修改 `main` 将 `Err` 成员转换为对用户更友好的错误,而不是 `panic!` 调用产生的关于 `thread 'main'` 和 `RUST_BACKTRACE` 的文本。
|
||||
|
||||
示例 12-9 展示了为了返回 `Result` 在 `Config::new` 的返回值和函数体中所需的改变:
|
||||
示例 12-9 展示了为了返回 `Result` 在 `Config::new` 的返回值和函数体中所需的改变。注意这还不能编译,直到下一个示例同时也更新了 `main` 之后。
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -291,10 +293,7 @@ fn main() {
|
||||
}
|
||||
|
||||
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)
|
||||
let contents = fs::read_to_string(config.filename)
|
||||
.expect("something went wrong reading the file");
|
||||
|
||||
println!("With text:\n{}", contents);
|
||||
@ -318,11 +317,8 @@ use std::error::Error;
|
||||
|
||||
// --snip--
|
||||
|
||||
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)?;
|
||||
fn run(config: Config) -> Result<(), Box<dyn Error>> {
|
||||
let contents = fs::read_to_string(config.filename)?;
|
||||
|
||||
println!("With text:\n{}", contents);
|
||||
|
||||
@ -334,9 +330,9 @@ fn run(config: Config) -> Result<(), Box<Error>> {
|
||||
|
||||
这里我们做出了三个明显的修改。首先,将 `run` 函数的返回类型变为 `Result<(), Box<Error>>`。之前这个函数返回 unit 类型 `()`,现在它仍然保持作为 `Ok` 时的返回值。
|
||||
|
||||
对于错误类型,使用了 **trait 对象** `Box<Error>`(在开头使用了 `use` 语句将 `std::error::Error` 引入作用域)。第十七章会涉及 trait 对象。目前只需知道 `Box<Error>` 意味着函数会返回实现了 `Error` trait 的类型,不过无需指定具体将会返回的值的类型。这提供了在不同的错误场景可能有不同类型的错误返回值的灵活性。
|
||||
对于错误类型,使用了 **trait 对象** `Box<dyn Error>`(在开头使用了 `use` 语句将 `std::error::Error` 引入作用域)。第十七章会涉及 trait 对象。目前只需知道 `Box<dyn Error>` 意味着函数会返回实现了 `Error` trait 的类型,不过无需指定具体将会返回的值的类型。这提供了在不同的错误场景可能有不同类型的错误返回值的灵活性。这也就是 `dyn`,它是 “动态”(“dynamic”)的缩写。
|
||||
|
||||
第二个改变是去掉了 `expect` 调用并替换为第九章讲到的 `?`。不同于遇到错误就 `panic!`,这会从函数中返回错误值并让调用者来处理它。
|
||||
第二个改变是去掉了 `expect` 调用并替换为第九章讲到的 `?`。不同于遇到错误就 `panic!`,`?` 会从函数中返回错误值并让调用者来处理它。
|
||||
|
||||
第三个修改是现在成功时这个函数会返回一个 `Ok` 值。因为 `run` 函数签名中声明成功类型返回值是 `()`,这意味着需要将 unit 类型值包装进 `Ok` 值中。`Ok(())` 一开始看起来有点奇怪,不过这样使用 `()` 是表明我们调用 `run` 只是为了它的副作用的惯用方式;它并没有返回什么有意义的值。
|
||||
|
||||
@ -351,7 +347,7 @@ warning: unused `std::result::Result` which must be used
|
||||
= note: #[warn(unused_must_use)] on by default
|
||||
```
|
||||
|
||||
Rust 提示我们的代码忽略了 `Result` 值,它可能表明这里存在一个错误。虽然我们没有检查这里是否有一个错误,而编译器提醒我们这里应该有一些错误处理代码!现在就让我们修正他们。
|
||||
Rust 提示我们的代码忽略了 `Result` 值,它可能表明这里存在一个错误。虽然我们没有检查这里是否有一个错误,而编译器提醒我们这里应该有一些错误处理代码!现在就让我们修正这个问题。
|
||||
|
||||
#### 处理 `main` 中 `run` 返回的错误
|
||||
|
||||
@ -395,8 +391,7 @@ fn main() {
|
||||
|
||||
```rust,ignore
|
||||
use std::error::Error;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::fs;
|
||||
|
||||
pub struct Config {
|
||||
pub query: String,
|
||||
@ -409,25 +404,24 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(config: Config) -> Result<(), Box<Error>> {
|
||||
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
|
||||
// --snip--
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 12-13:将 `Config` 和 `run` 移动到 *src/lib.rs*</span>
|
||||
|
||||
这里使用了公有的 `pub`:在 `Config`、其字段和其 `new` 方法,以及 `run` 函数上。现在我们有了一个拥有可以测试的公有 API 的库 crate 了。
|
||||
这里使用了公有的 `pub` 关键字:在 `Config`、其字段和其 `new` 方法,以及 `run` 函数上。现在我们有了一个拥有可以测试的公有 API 的库 crate 了。
|
||||
|
||||
现在需要在 *src/main.rs* 中将移动到 *src/lib.rs* 的代码引入二进制 crate 的作用域中,如示例 12-14 所示:
|
||||
|
||||
<span class="filename">Filename: src/main.rs</span>
|
||||
|
||||
```rust,ignore
|
||||
extern crate minigrep;
|
||||
|
||||
use std::env;
|
||||
use std::process;
|
||||
|
||||
use minigrep;
|
||||
use minigrep::Config;
|
||||
|
||||
fn main() {
|
||||
|
@ -1,8 +1,8 @@
|
||||
## 采用测试驱动开发完善库的功能
|
||||
|
||||
> [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)
|
||||
> [ch12-04-testing-the-librarys-functionality.md](https://github.com/rust-lang/book/blob/master/src/ch12-04-testing-the-librarys-functionality.md)
|
||||
> <br>
|
||||
> commit 1fe78a83f37ecc69b840fdc8dcfc727f88a3a3d4
|
||||
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
|
||||
|
||||
现在我们将逻辑提取到了 *src/lib.rs* 并将所有的参数解析和错误处理留在了 *src/main.rs* 中,为代码的核心功能编写测试将更加容易。我们可以直接使用多种参数调用函数并检查返回值而无需从命令行运行二进制文件了。如果你愿意的话,请自行为 `Config::new` 和 `run` 函数的功能编写一些测试。
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
# }
|
||||
#
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@ -50,14 +50,14 @@ Pick three.";
|
||||
|
||||
<span class="caption">示例 12-15:创建一个我们期望的 `search` 函数的失败测试</span>
|
||||
|
||||
这里选择使用 "duct" 作为这个测试中需要搜索的字符串。用来搜索的文本有三行,其中只有一行包含 "duct"。我们断言 `search` 函数的返回值只包含期望的那一行。
|
||||
这里选择使用 `"duct"` 作为这个测试中需要搜索的字符串。用来搜索的文本有三行,其中只有一行包含 `"duct"`。我们断言 `search` 函数的返回值只包含期望的那一行。
|
||||
|
||||
我们还不能运行这个测试并看到它失败,因为它甚至都还不能编译!我们将增加足够的代码来使其能够编译:一个总是会返回空 vector 的 `search` 函数定义,如示例 12-16 所示。然后这个测试应该能够编译并因为空 vector 并不匹配一个包含一行 `"safe, fast, productive."` 的 vector 而失败。
|
||||
我们还不能运行这个测试并看到它失败,因为它甚至都还不能编译:`search` 函数还不存在呢!我们将增加足够的代码来使其能够编译:一个总是会返回空 vector 的 `search` 函数定义,如示例 12-16 所示。然后这个测试应该能够编译并因为空 vector 并不匹配一个包含一行 `"safe, fast, productive."` 的 vector 而失败。
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||||
fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||||
vec![]
|
||||
}
|
||||
```
|
||||
@ -74,7 +74,7 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||||
error[E0106]: missing lifetime specifier
|
||||
--> src/lib.rs:5:51
|
||||
|
|
||||
5 | pub fn search(query: &str, contents: &str) -> Vec<&str> {
|
||||
5 | fn search(query: &str, contents: &str) -> Vec<&str> {
|
||||
| ^ expected lifetime
|
||||
parameter
|
||||
|
|
||||
@ -84,7 +84,7 @@ parameter
|
||||
|
||||
Rust 不可能知道我们需要的是哪一个参数,所以需要告诉它。因为参数 `contents` 包含了所有的文本而且我们希望返回匹配的那部分文本,所以我们知道 `contents` 是应该要使用生命周期语法来与返回值相关联的参数。
|
||||
|
||||
其他语言中并不需要你在函数签名中将参数与返回值相关联,所以这么做可能仍然感觉有些陌生,随着时间的推移这将会变得越来越容易。你可能想要将这个例子与第十章中生命周期语法部分做对比。
|
||||
其他语言中并不需要你在函数签名中将参数与返回值相关联。所以这么做可能仍然感觉有些陌生,随着时间的推移这将会变得越来越容易。你可能想要将这个例子与第十章中生命 “生命周期与引用有效性” 部分做对比。
|
||||
|
||||
现在运行测试:
|
||||
|
||||
@ -96,12 +96,12 @@ $ cargo test
|
||||
Running target/debug/deps/minigrep-abcabcabc
|
||||
|
||||
running 1 test
|
||||
test test::one_result ... FAILED
|
||||
test tests::one_result ... FAILED
|
||||
|
||||
failures:
|
||||
|
||||
---- test::one_result stdout ----
|
||||
thread 'test::one_result' panicked at 'assertion failed: `(left ==
|
||||
---- tests::one_result stdout ----
|
||||
thread 'tests::one_result' panicked at 'assertion failed: `(left ==
|
||||
right)`
|
||||
left: `["safe, fast, productive."]`,
|
||||
right: `[]`)', src/lib.rs:48:8
|
||||
@ -109,7 +109,7 @@ note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||||
|
||||
|
||||
failures:
|
||||
test::one_result
|
||||
tests::one_result
|
||||
|
||||
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
|
||||
@ -137,7 +137,7 @@ Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust,ignore
|
||||
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||||
fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||||
for line in contents.lines() {
|
||||
// do something with line
|
||||
}
|
||||
@ -146,7 +146,7 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||||
|
||||
<span class="caption">示例 12-17:遍历 `contents` 的每一行</span>
|
||||
|
||||
`lines` 方法返回一个迭代器。第十三章会深入了解迭代器,不过我们已经在示例 3-4 中见过使用迭代器的方法了,在那里使用了一个 `for` 循环和迭代器在一个集合的每一项上运行了一些代码。
|
||||
`lines` 方法返回一个迭代器。第十三章会深入了解迭代器,不过我们已经在示例 3-5 中见过使用迭代器的方法了,在那里使用了一个 `for` 循环和迭代器在一个集合的每一项上运行了一些代码。
|
||||
|
||||
#### 用查询字符串搜索每一行
|
||||
|
||||
@ -155,7 +155,7 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust,ignore
|
||||
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||||
fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||||
for line in contents.lines() {
|
||||
if line.contains(query) {
|
||||
// do something with line
|
||||
@ -173,7 +173,7 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust,ignore
|
||||
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||||
fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
for line in contents.lines() {
|
||||
@ -194,7 +194,7 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||||
$ cargo test
|
||||
--snip--
|
||||
running 1 test
|
||||
test test::one_result ... ok
|
||||
test tests::one_result ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
```
|
||||
@ -210,11 +210,8 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
<span class="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)?;
|
||||
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
|
||||
let contents = fs::read_to_string(config.filename)?;
|
||||
|
||||
for line in search(&config.query, &contents) {
|
||||
println!("{}", line);
|
||||
|
@ -1,8 +1,8 @@
|
||||
## 处理环境变量
|
||||
|
||||
> [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)
|
||||
> [ch12-05-working-with-environment-variables.md](https://github.com/rust-lang/book/blob/master/src/ch12-05-working-with-environment-variables.md)
|
||||
> <br>
|
||||
> commit 1fe78a83f37ecc69b840fdc8dcfc727f88a3a3d4
|
||||
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
|
||||
|
||||
我们将增加一个额外的功能来改进 `minigrep`:一个通过环境变量启用的大小写不敏感搜索的选项。可以将其设计为一个命令行参数并要求用户每次需要时都加上它,不过相反我们将使用环境变量。这允许用户设置环境变量一次之后在整个终端会话中所有的搜索都将是大小写不敏感的。
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@ -51,9 +51,9 @@ Trust me.";
|
||||
|
||||
<span class="caption">示例 12-20:为准备添加的大小写不敏感函数新增失败测试</span>
|
||||
|
||||
注意我们也改变了老测试中 `contents` 的值。还新增了一个含有文本 "Duct tape" 的行,它有一个大写的 D,这在大小写敏感搜索时不应该匹配 "duct"。我们修改这个测试以确保不会意外破坏已经实现的大小写敏感搜索功能;这个测试现在应该能通过并在处理大小写不敏感搜索时应该能一直通过。
|
||||
注意我们也改变了老测试中 `contents` 的值。还新增了一个含有文本 `"Duct tape."` 的行,它有一个大写的 D,这在大小写敏感搜索时不应该匹配 "duct"。我们修改这个测试以确保不会意外破坏已经实现的大小写敏感搜索功能;这个测试现在应该能通过并在处理大小写不敏感搜索时应该能一直通过。
|
||||
|
||||
大小写 **不敏感** 搜索的新测试使用 "rUsT" 作为其查询字符串。在我们将要增加的 `search_case_insensitive` 函数中,“rUsT” 查询应该包含 “Rust:” 包含一个大写的 R 还有 “Trust me.” 这两行,即便他们与查询的大小写都不同。这个测试现在会编译失败因为还没有定义 `search_case_insensitive` 函数。请随意增加一个总是返回空 vector 的骨架实现,正如示例 12-16 中 `search` 函数为了使测试编译并失败时所做的那样。
|
||||
大小写 **不敏感** 搜索的新测试使用 `"rUsT"` 作为其查询字符串。在我们将要增加的 `search_case_insensitive` 函数中,`"rUsT"` 查询应该包含带有一个大写 R 的 `"Rust:"` 还有 `"Trust me."` 这两行,即便他们与查询的大小写都不同。这个测试现在会编译失败因为还没有定义 `search_case_insensitive` 函数。请随意增加一个总是返回空 vector 的骨架实现,正如示例 12-16 中 `search` 函数为了使测试编译并失败时所做的那样。
|
||||
|
||||
### 实现 `search_case_insensitive` 函数
|
||||
|
||||
@ -78,9 +78,9 @@ fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||||
|
||||
<span class="caption">示例 12-21:定义 `search_case_insensitive` 函数,它在比较查询和每一行之前将他们都转换为小写</span>
|
||||
|
||||
首先我们将 `query` 字符串转换为小写,并将其覆盖到同名的变量中。对查询字符串调用 `to_lowercase` 是必需的,这样不管用户的查询是 “rust”、“RUST”、“Rust” 或者 “rUsT”,我们都将其当作 “rust” 处理并对大小写不敏感。
|
||||
首先我们将 `query` 字符串转换为小写,并将其覆盖到同名的变量中。对查询字符串调用 `to_lowercase` 是必需的,这样不管用户的查询是 `"rust"`、`"RUST"`、`"Rust"` 或者 `"rUsT"`,我们都将其当作 `"rust"` 处理并对大小写不敏感。
|
||||
|
||||
注意 `query` 现在是一个 `String` 而不是字符串 slice,因为调用 `to_lowercase` 是在创建新数据,而不是引用现有数据。如果查询字符串是 “rUsT”,这个字符串 slice 并不包含可供我们使用的小写的 “u” 或 “t”,所以必需分配一个包含 “rust” 的新 `String`。现在当我们将 `query` 作为一个参数传递给 `contains` 方法时,需要增加一个 & 因为 `contains` 的签名被定义为获取一个字符串 slice。
|
||||
注意 `query` 现在是一个 `String` 而不是字符串 slice,因为调用 `to_lowercase` 是在创建新数据,而不是引用现有数据。如果查询字符串是 `"rUsT"`,这个字符串 slice 并不包含可供我们使用的小写的 `u` 或 `t`,所以必需分配一个包含 `"rust"` 的新 `String`。现在当我们将 `query` 作为一个参数传递给 `contains` 方法时,需要增加一个 & 因为 `contains` 的签名被定义为获取一个字符串 slice。
|
||||
|
||||
接下来在检查每个 `line` 是否包含 `search` 之前增加了一个 `to_lowercase` 调用将他们都变为小写。现在我们将 `line` 和 `query` 都转换成了小写,这样就可以不管查询的大小写进行匹配了。
|
||||
|
||||
@ -88,13 +88,13 @@ fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||||
|
||||
```text
|
||||
running 2 tests
|
||||
test test::case_insensitive ... ok
|
||||
test test::case_sensitive ... ok
|
||||
test tests::case_insensitive ... ok
|
||||
test tests::case_sensitive ... ok
|
||||
|
||||
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
```
|
||||
|
||||
好的!现在,让我们在 `run` 函数中实际调用新 `search_case_insensitive` 函数。首先,我们将在 `Config` 结构体中增加一个配置项来切换大小写敏感和大小写不敏感搜索:
|
||||
好的!现在,让我们在 `run` 函数中实际调用新 `search_case_insensitive` 函数。首先,我们将在 `Config` 结构体中增加一个配置项来切换大小写敏感和大小写不敏感搜索。增加这些字段会导致编译错误,因为我们还没有在任何地方初始化这些字段:
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
@ -112,7 +112,7 @@ pub struct Config {
|
||||
|
||||
```rust
|
||||
# use std::error::Error;
|
||||
# use std::fs::File;
|
||||
# use std::fs::{self, File};
|
||||
# use std::io::prelude::*;
|
||||
#
|
||||
# fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||||
@ -123,17 +123,14 @@ pub struct Config {
|
||||
# vec![]
|
||||
# }
|
||||
#
|
||||
# struct Config {
|
||||
# pub struct Config {
|
||||
# query: String,
|
||||
# filename: String,
|
||||
# case_sensitive: bool,
|
||||
# }
|
||||
#
|
||||
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)?;
|
||||
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
|
||||
let contents = fs::read_to_string(config.filename)?;
|
||||
|
||||
let results = if config.case_sensitive {
|
||||
search(&config.query, &contents)
|
||||
@ -189,7 +186,7 @@ impl Config {
|
||||
|
||||
我们将变量 `case_sensitive` 的值传递给 `Config` 实例,这样 `run` 函数可以读取其值并决定是否调用 `search` 或者示例 12-22 中实现的 `search_case_insensitive`。
|
||||
|
||||
让我们试一试吧!首先不设置环境变量并使用查询 “to” 运行程序,这应该会匹配任何全小写的单词 “to” 的行:
|
||||
让我们试一试吧!首先不设置环境变量并使用查询 `to` 运行程序,这应该会匹配任何全小写的单词 “to” 的行:
|
||||
|
||||
```text
|
||||
$ cargo run to poem.txt
|
||||
@ -200,7 +197,16 @@ Are you nobody, too?
|
||||
How dreary to be somebody!
|
||||
```
|
||||
|
||||
看起来程序仍然能够工作!现在将 `CASE_INSENSITIVE` 设置为 1 并仍使用相同的查询 “to”,这回应该得到包含可能有大写字母的 “to” 的行:
|
||||
看起来程序仍然能够工作!现在将 `CASE_INSENSITIVE` 设置为 `1` 并仍使用相同的查询 `to`。
|
||||
|
||||
如果你使用 PowerShell,则需要用两句命令而不是一句来设置环境变量并运行程序:
|
||||
|
||||
```text
|
||||
$ $env:CASE_INSENSITIVE=1
|
||||
$ cargo run to poem.txt
|
||||
```
|
||||
|
||||
这回应该得到包含可能有大写字母的 “to” 的行:
|
||||
|
||||
```text
|
||||
$ CASE_INSENSITIVE=1 cargo run to poem.txt
|
||||
@ -212,13 +218,6 @@ To tell your name the livelong day
|
||||
To an admiring bog!
|
||||
```
|
||||
|
||||
如果你使用 PowerShell,则需要用两句命令而不是一句来设置环境变量并运行程序:
|
||||
|
||||
```text
|
||||
$ $env:CASE_INSENSITIVE=1
|
||||
$ cargo run to poem.txt
|
||||
```
|
||||
|
||||
好极了,我们也得到了包含 “To” 的行!现在 `minigrep` 程序可以通过环境变量控制进行大小写不敏感搜索了。现在你知道了如何管理由命令行参数或环境变量设置的选项了!
|
||||
|
||||
一些程序允许对相同配置同时使用参数 **和** 环境变量。在这种情况下,程序来决定参数和环境变量的优先级。作为一个留给你的测试,尝试通过一个命令行参数或一个环境变量来控制大小写不敏感搜索。并在运行程序时遇到矛盾值时决定命令行参数和环境变量的优先级。
|
||||
|
@ -1,8 +1,8 @@
|
||||
## 将错误信息输出到标准错误而不是标准输出
|
||||
|
||||
> [ch12-06-writing-to-stderr-instead-of-stdout.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-06-writing-to-stderr-instead-of-stdout.md)
|
||||
> [ch12-06-writing-to-stderr-instead-of-stdout.md](https://github.com/rust-lang/book/blob/master/src/ch12-06-writing-to-stderr-instead-of-stdout.md)
|
||||
> <br>
|
||||
> commit 1fe78a83f37ecc69b840fdc8dcfc727f88a3a3d4
|
||||
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
|
||||
|
||||
目前为止,我们将所有的输出都 `println!` 到了终端。大部分终端都提供了两种输出:**标准输出**(*standard output*,`stdout`)对应通用信息,**标准错误**(*standard error*,`stderr`)则用于错误信息。这种区别允许用户选择将程序正常输出定向到一个文件中并仍将错误信息打印到屏幕上。
|
||||
|
||||
@ -60,7 +60,7 @@ $ cargo run > output.txt
|
||||
Problem parsing arguments: not enough arguments
|
||||
```
|
||||
|
||||
现在我们看到了屏幕上的错误信息,同时 `output.txt` 里什么也没有,这正是命令行程序所期望的行为。
|
||||
现在我们看到了屏幕上的错误信息,同时 *output.txt* 里什么也没有,这正是命令行程序所期望的行为。
|
||||
|
||||
如果使用不会造成错误的参数再次运行程序,不过仍然将标准输出重定向到一个文件,像这样:
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user