From 4af4ba6822ab51b704d5dd051f6cba453f8bbfd0 Mon Sep 17 00:00:00 2001 From: yang yue Date: Mon, 27 Feb 2017 23:25:11 +0800 Subject: [PATCH] wip --- docs/ch10-02-traits.html | 188 ++++++++++++++- docs/ch10-03-lifetime-syntax.html | 184 ++++++++++++++- docs/print.html | 370 +++++++++++++++++++++++++++++- src/ch10-02-traits.md | 265 ++++++++++++++++++++- src/ch10-03-lifetime-syntax.md | 258 +++++++++++++++++++++ 5 files changed, 1261 insertions(+), 4 deletions(-) diff --git a/docs/ch10-02-traits.html b/docs/ch10-02-traits.html index d0d6eec..6847d05 100644 --- a/docs/ch10-02-traits.html +++ b/docs/ch10-02-traits.html @@ -133,7 +133,193 @@ impl Summarizable for Tweet { Tweet types

-

在类型上实现 trait 类似与实现与 trait 无关的方法。区别在于impl关键字之后,我们提供需要实现 trait 的名称,接着是for和需要实现 trait 的类型的名称。

+

在类型上实现 trait 类似与实现与 trait 无关的方法。区别在于impl关键字之后,我们提供需要实现 trait 的名称,接着是for和需要实现 trait 的类型的名称。在impl块中,使用 trait 定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现 trait 方法所拥有的行为。

+

一旦实现了 trait,我们就可以用与NewsArticleTweet实例的非 trait 方法一样的方式调用 trait 方法了:

+
let tweet = Tweet {
+    username: String::from("horse_ebooks"),
+    content: String::from("of course, as you probably already know, people"),
+    reply: false,
+    retweet: false,
+};
+
+println!("1 new tweet: {}", tweet.summary());
+
+

这会打印出1 new tweet: horse_ebooks: of course, as you probably already know, people

+

注意因为列表 10-12 中我们在相同的lib.rs力定义了Summarizable trait 和NewsArticleTweet类型,所以他们是位于同一作用域的。如果这个lib.rs是对应aggregator crate 的,而别人想要利用我们 crate 的功能外加为其WeatherForecast结构体实现Summarizable trait,在实现Summarizable trait 之前他们首先就需要将其导入其作用域中,如列表 10-13 所示:

+
+Filename: lib.rs +
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)
+    }
+}
+
+
+

Listing 10-13: Bringing the Summarizable trait from our aggregator crate +into scope in another crate

+
+
+

另外这段代码假设Summarizable是一个公有 trait,这是因为列表 10-11 中trait之前使用了pub关键字。

+

trait 实现的一个需要注意的限制是:只能在 trait 或对应类型位于我们 crate 本地的时候为其实现 trait。换句话说,不允许对外部类型实现外部 trait。例如,不能Vec上实现Display trait,因为DisplayVec都定义于标准库中。允许在像Tweet这样作为我们aggregatorcrate 部分功能的自定义类型上实现标准库中的 trait Display。也允许在aggregatorcrate中为Vec实现Summarizable,因为Summarizable定义与此。这个限制是我们称为 orphan rule 的一部分,如果你感兴趣的可以在类型理论中找到它。简单来说,它被称为 orphan rule 是因为其父类型不存在。没有这条规则的话,两个 crate 可以分别对相同类型是实现相同的 trait,因而这两个实现会相互冲突:Rust 将无从得知应该使用哪一个。因为 Rust 强制执行 orphan rule,其他人编写的代码不会破坏你代码,反之亦是如此。

+

默认实现

+

有时为 trait 中的某些或全部提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。

+

列表 10-14 中展示了如何为Summarize trait 的summary方法指定一个默认的字符串值,而不是像列表 10-11 中那样只是定义方法签名:

+
+Filename: lib.rs +
pub trait Summarizable {
+    fn summary(&self) -> String {
+        String::from("(Read more...)")
+    }
+}
+
+
+

Listing 10-14: Definition of a Summarizable trait with a default +implementation of the summary method

+
+
+

如果想要对NewsArticle实例使用这个默认实现,而不是像列表 10-12 中那样定义一个自己的实现,则可以指定一个空的impl块:

+
impl Summarizable for NewsArticle {}
+
+

即便选择不再直接为NewsArticle定义summary方法了,因为summary方法有一个默认实现而且NewsArticle被指定为实现了Summarizable trait,我们仍然可以对NewsArticle的实例调用summary方法:

+
let article = 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."),
+};
+
+println!("New article available! {}", article.summary());
+
+

这段代码会打印New article available! (Read more...)

+

Summarizable trait 改变为拥有默认summary实现并不要求对列表 10-12 中的Tweet和列表 10-13 中的WeatherForecastSummarizable的实现做任何改变:重载一个默认实现的语法与实现没有默认实现的 trait 方法时完全一样的。

+

默认实现允许调用相同 trait 中的其他方法,哪怕这些方法没有默认实现。通过这种方法,trait 可以实现很多有用的功能而只需实现一小部分特定内容。我们可以选择让Summarizable trait 也拥有一个要求实现的author_summary方法,接着summary方法则提供默认实现并调用author_summary方法:

+
pub trait Summarizable {
+    fn author_summary(&self) -> String;
+
+    fn summary(&self) -> String {
+        format!("(Read more from {}...)", self.author_summary())
+    }
+}
+
+

为了使用这个版本的Summarizable,只需在实现 trait 时定义author_summary即可:

+
impl Summarizable for Tweet {
+    fn author_summary(&self) -> String {
+        format!("@{}", self.username)
+    }
+}
+
+

一旦定义了author_summary,我们就可以对Tweet结构体的实例调用summary了,而summary的默认实现会调用我们提供的author_summary定义。

+
let tweet = Tweet {
+    username: String::from("horse_ebooks"),
+    content: String::from("of course, as you probably already know, people"),
+    reply: false,
+    retweet: false,
+};
+
+println!("1 new tweet: {}", tweet.summary());
+
+

这会打印出1 new tweet: (Read more from @horse_ebooks...)

+

注意在重载过的实现中调用默认实现是不可能的。

+

trait bounds

+

现在我们定义了 trait 并在类型上实现了这些 trait,也可以对泛型类型参数使用 trait。我们可以限制泛型不再适用于任何类型,编译器会确保其被限制为那么实现了特定 trait 的类型,由此泛型就会拥有我们希望其类型所拥有的功能。这被称为指定泛型的 trait bounds

+

例如在列表 10-12 中为NewsArticleTweet类型实现了Summarizable trait。我们可以定义一个函数notify来调用summary方法,它拥有一个泛型类型T的参数item。为了能够在item上调用summary而不出现错误,我们可以在T上使用 trait bounds 来指定item必须是实现了Summarizable trait 的类型:

+
pub fn notify<T: Summarizable>(item: T) {
+    println!("Breaking news! {}", item.summary());
+}
+
+

trait bounds 连同泛型类型参数声明一同出现,位于尖括号中的冒号后面。由于T上的 trait bounds,我们可以传递任何NewsArticleTweet的实例来调用notify函数。列表 10-13 中使用我们aggregator crate 的外部代码也可以传递一个WeatherForecast的实例来调用notify函数,因为WeatherForecast同样也实现了Summarizable。使用任何其他类型,比如Stringi32,来调用notify的代码将不能编译,因为这些类型没有实现Summarizable

+

可以通过+来为泛型指定多个 trait bounds。如果我们需要能够在函数中使用T类型的显示格式的同时也能使用summary方法,则可以使用 trait bounds T: Summarizable + Display。这意味着T可以是任何是实现了SummarizableDisplay的类型。

+

对于拥有多个泛型类型参数的函数,每一个泛型都可以有其自己的 trait bounds。在函数名和参数列表之间的尖括号中指定很多的 trait bound 信息将是难以阅读的,所以有另外一个指定 trait bounds 的语法,它将其移动到函数签名后的where从句中。所以相比这样写:

+
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {
+
+

我们也可以使用where从句:

+
fn some_function<T, U>(t: T, u: U) -> i32
+    where T: Display + Clone,
+          U: Clone + Debug
+{
+
+

这就显得不那么杂乱,同时也使这个函数看起来更像没有很多 trait bounds 的函数。这时函数名、参数列表和返回值类型都离得很近。

+

使用 trait bounds 来修复largest函数

+

所以任何想要对泛型使用 trait 定义的行为的时候,都需要在泛型参数类型上指定 trait bounds。现在我们就可以修复列表 10-5 中那个使用泛型类型参数的largest函数定义了!当我们将其放置不管的时候,它会出现这个错误:

+
error[E0369]: binary operation `>` cannot be applied to type `T`
+  |
+5 |         if item > largest {
+  |            ^^^^
+  |
+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 中所以并不需要手动将其引入作用域。

+
fn largest<T: PartialOrd>(list: &[T]) -> T {
+
+

但是如果编译代码的话,会出现不同的错误:

+
error[E0508]: cannot move out of type `[T]`, a non-copy array
+ --> src/main.rs:4:23
+  |
+4 |     let mut largest = list[0];
+  |         -----------   ^^^^^^^ cannot move out of here
+  |         |
+  |         hint: to prevent move, use `ref largest` or `ref mut largest`
+
+error[E0507]: cannot move out of borrowed content
+ --> src/main.rs:6:9
+  |
+6 |     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函数,我们只尝试了寻找最大的i32char。正如第四章讨论过的,像i32char这样的类型是已知大小的并可以储存在栈上,所以他们实现了Copy trait。当我们将largest函数改成使用泛型后,现在list参数的类型就有可能是没有实现Copy trait 的,这意味着我们可能不能将list[0]的值移动到largest变量中。

+

如果只想对实现了Copy的类型调用这些带啊吗,可以在T的 trait bounds 中增加Copy!列表 10-15 中展示了一个可以编译的泛型版本的largest函数的完整代码,只要传递给largest的 slice 值的类型实现了PartialOrdCopy这两个 trait,例如i32char

+
+Filename: src/main.rs +
use std::cmp::PartialOrd;
+
+fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
+    let mut largest = list[0];
+
+    for &item in list.iter() {
+        if item > largest {
+            largest = item;
+        }
+    }
+
+    largest
+}
+
+fn main() {
+    let numbers = vec![34, 50, 25, 100, 65];
+
+    let result = largest(&numbers);
+    println!("The largest number is {}", result);
+
+    let chars = vec!['y', 'm', 'a', 'q'];
+
+    let result = largest(&chars);
+    println!("The largest char is {}", result);
+}
+
+
+

Listing 10-15: A working definition of the largest function that works on any +generic type that implements the PartialOrd and Copy traits

+
+
+

如果并不希望限制largest函数只能用于实现了Copy trait 的类型,我们可以在T的 trait bounds 中指定Clone而不是Copy,并克隆 slice 的每一个值使得largest函数拥有其所有权。但是使用clone函数潜在意味着更多的堆分配,而且堆分配在涉及大量数据时可能会相当缓慢。另一种largest的实现方式是返回 slice 中一个T值的引用。如果我们将函数返回值从T改为&T并改变函数体使其能够返回一个引用,我们将不需要任何CloneCopy的 trait bounds 而且也不会有任何的堆分配。尝试自己实现这种替代解决方式吧!

+

trait 和 trait bounds 让我们使用泛型类型参数来减少重复,并仍然能够向编译器明确指定泛型类型需要拥有哪些行为。因为我们向编译器提供了 trait bounds 信息,它就可以检查代码中所用到的具体类型是否提供了正确的行为。在动态类型语言中,如果我们尝试调用一个类型并没有实现的方法,会在运行时出现错误。Rust 将这些错误移动到了编译时,甚至在代码能够运行之前就强迫我们修复错误。另外,我们也无需编写运行时检查行为的代码,因为在编译时就已经检查过了,这样相比其他那些不愿放弃泛型灵活性的语言有更好的性能。

+

这里还有一种泛型,我们一直在使用它甚至都没有察觉它的存在,这就是生命周期lifetimes)。不同于其他泛型帮助我们确保类型拥有期望的行为,生命周期则有助于确保引用在我们需要他们的时候一直有效。让我们学习生命周期是如何做到这些的。

diff --git a/docs/ch10-03-lifetime-syntax.html b/docs/ch10-03-lifetime-syntax.html index 97af04a..8069ef0 100644 --- a/docs/ch10-03-lifetime-syntax.html +++ b/docs/ch10-03-lifetime-syntax.html @@ -67,7 +67,189 @@
- +

生命周期与引用有效性

+
+

ch10-03-lifetime-syntax.md +
+commit d7a4e99554da53619dd71044273535ba0186f40a

+
+

当在第四章讨论引用时,我们遗漏了一个重要的细节:Rust 中的每一个引用都有其生命周期,也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以多种不同方式向关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。

+

好吧,这有点不太寻常,而且也不同于其他语言中使用的工具。生命周期,从某种意义上说,是 Rust 最与众不同的功能。

+

生命周期是一个很广泛的话题,本章不可能涉及到它全部的内容,所以这里我们会讲到一些通常你可能会遇到的生命周期语法以便你熟悉这个概念。第十九章会包含生命周期所有功能的更高级的内容。

+

生命周期避免了悬垂引用

+

生命周期的主要目标是避免悬垂引用,它会导致程序引用了并非其期望引用的数据。考虑一下列表 10-16 中的程序,它有一个外部作用域和一个内部作用域,外部作用域声明了一个没有初值的变量r,而内部作用域声明了一个初值为 5 的变量x。在内部作用域中,我们尝试将r的值设置为一个x的引用。接着在内部作用域结束后,尝试打印出r的值:

+
+
{
+    let r;
+
+    {
+        let x = 5;
+        r = &x;
+    }
+
+    println!("r: {}", r);
+}
+
+
+

Listing 10-16: An attempt to use a reference whose value has gone out of scope

+
+
+
+

未初始化变量不能被使用

+

接下来的一些例子中声明了没有初始值的变量,以便这些变量存在于外部作用域。这看起来好像和 Rust 不允许存在空值相冲突。然而这是可以的,如果我们尝试在给它一个值之前使用这个变量,会出现一个编译时错误。请自行尝试!

+
+

当编译这段代码时会得到一个错误:

+
error: `x` does not live long enough
+   |
+6  |         r = &x;
+   |              - borrow occurs here
+7  |     }
+   |     ^ `x` dropped here while still borrowed
+...
+10 | }
+   | - borrowed value needs to live until here
+
+

变量x并没有“存在的足够久”。为什么呢?好吧,x在到达第 7 行的大括号的结束时就离开了作用域,这也是内部作用域的结尾。不过r在外部作用域也是有效的;作用域越大我们就说它“存在的越久”。如果 Rust 允许这段代码工作,r将会引用在x离开作用域时被释放的内存,这时尝试对r做任何操作都会不能正常工作。那么 Rust 是如何决定这段代码是不被允许的呢?

+

借用检查器

+

编译器的这一部分叫做借用检查器borrow checker),它比较作用域来确保所有的借用都是有效的。列表 10-17 展示了与列表 10-16 相同的例子不过带有变量声明周期的注释:

+
+
{
+    let r;         // -------+-- 'a
+                   //        |
+    {              //        |
+        let x = 5; // -+-----+-- 'b
+        r = &x;    //  |     |
+    }              // -+     |
+                   //        |
+    println!("r: {}", r); // |
+                   //        |
+                   // -------+
+}
+
+
+

Listing 10-17: Annotations of the lifetimes of x and r, named 'a and 'b +respectively

+
+
+ + +

我们将r的声明周期标记为'a而将x的生命周期标记为'b。如你所见,内部的'b块要比外部的生命周期'a小得多。在编译时,Rust 比较这两个生命周期的大小,并发现r拥有声明周期'a,不过它引用了一个拥有生命周期'b的对象。程序被拒绝编译,因为生命周期'b比生命周期'a要小:引用者没有比被引用者存在的更久。

+

让我们看看列表 10-18 中这个并没有产生悬垂引用且可以正常编译的例子:

+
+
{
+    let x = 5;            // -----+-- 'b
+                          //      |
+    let r = &x;           // --+--+-- 'a
+                          //   |  |
+    println!("r: {}", r); //   |  |
+                          // --+  |
+}                         // -----+
+
+
+

Listing 10-18: A valid reference because the data has a longer lifetime than +the reference

+
+
+

x拥有生命周期 'b,在这里它比 'a要大。这就意味着r可以引用x:Rust 知道r中的引用在x有效的时候也会一直有效。

+

现在我们已经在一个具体的例子中展示了引用的声明周期位于何处,并讨论了 Rust 如何分析生命周期来保证引用总是有效的,接下来让我们聊聊在函数的上下文中参数和返回值的泛型生命周期。

+

函数中的泛型生命周期

+

让我们来编写一个返回两个字符串 slice 中最长的那一个的函数。我们希望能够通过传递两个字符串 slice 来调用这个函数,并希望返回一个字符串 slice。一旦我们实现了longest函数,列表 10-19 中的代码应该会打印出The longest string is abcd

+
+

Filename: src/main.rs

+
fn main() {
+    let string1 = String::from("abcd");
+    let string2 = "xyz";
+
+    let result = longest(string1.as_str(), string2);
+    println!("The longest string is {}", result);
+}
+
+
+

Listing 10-19: A main function that calls the longest function to find the +longest of two string slices

+
+
+

注意函数期望获取字符串 slice(如第四章所讲到的这是引用)因为我们并不希望longest函数获取其参数的引用。我们希望函数能够接受String的 slice(也就是变量string1的类型)和字符串字面值(也就是变量string2包含的值)。

+ + +

参考之前第四章中的“字符串 slice 作为参数”部分中更多关于为什么上面例子中的参数正是我们想要的讨论。

+

如果尝试像列表 10-20 中那样实现longest函数,它并不能编译:

+
+Filename: src/main.rs +
fn longest(x: &str, y: &str) -> &str {
+    if x.len() > y.len() {
+        x
+    } else {
+        y
+    }
+}
+
+
+

Listing 10-20: An implementation of the longest function that returns the +longest of two string slices, but does not yet compile

+
+
+

将会出现如下有关生命周期的错误:

+
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`
+
+

提示文本告诉我们返回值需要一个泛型生命周期参数,因为 Rust 并不知道将要返回的引用是指向xy。事实上我们也不知道,因为函数体中if块返回一个x的引用而else块返回一个y的引用。

+

虽然我们定义了这个函数,但是并不知道传递给函数的具体值,所以也不知道到底是if还是else会被执行。我们也不知道传入的引用的具体生命周期,所以也就不能像列表 10-17 和 10-18 那样通过观察作用域来确定返回的引用总是有效的。借用检查器自身同样也无法确定,因为它不知道xy的生命周期是如何与返回值的生命周期相关联的。接下来我们将增加泛型生命周期参数来定义引用间的关系以便借用检查器可以进行相关分析。

+

生命周期注解语法

+

生命周期注解并不改变任何引用的生命周期的长短。与当函数签名中指定了泛型类型参数后就可以接受任何类型一样,当指定了泛型生命周期后函数也能接受任何生命周期的引用。生命周期注解所做的就是将多个引用的生命周期联系起来。

+

生命周期注解有着一个不太常见的语法:生命周期参数名称必须以撇号(')开头。生命周期参数的名称通常全是小写,而且类似于泛型类型,其名称通常非常短。'a是大多数人默认使用的名称。生命周期参数注解位于引用的&之后,并有一个空格来将引用类型与生命周期注解分隔开。

+

这里有一些例子:我们有一个没有生命周期参数的i32的引用,一个有叫做'a的生命周期参数的i32的引用,和一个也有的生命周期参数'ai32的可变引用:

+
&i32        // a reference
+&'a i32     // a reference with an explicit lifetime
+&'a mut i32 // a mutable reference with an explicit lifetime
+
+

生命周期注解本身没有多少意义:生命周期注解告诉 Rust 多个引用的泛型生命周期参数如何相互联系。如果函数有一个生命周期'ai32的引用的参数first,还有另一个同样是生命周期'ai32的引用的参数second,这两个生命周期注解有相同的名称意味着firstsecond必须与这相同的泛型生命周期存在得一样久。

+

函数签名中的生命周期注解

+

来看看我们编写的longest函数的上下文中的生命周期。就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的加括号中。这里我们想要告诉 Rust 关于参数中的引用和返回值之间的限制是他们都必须拥有相同的生命周期,就像列表 10-21 中在每个引用中都加上了'a那样:

+
+Filename: src/main.rs +
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
+    if x.len() > y.len() {
+        x
+    } else {
+        y
+    }
+}
+
+
+

Listing 10-21: The longest function definition that specifies all the +references in the signature must have the same lifetime, 'a

+
+
+

这段代码能够编译并会产生我们想要使用列表 10-19 中的main函数得到的结果。

+

现在函数签名表明对于某些生命周期'a,函数会获取两个参数,他们都是与生命周期'a存在的一样长的字符串 slice。函数会返回一个同样也与生命周期'a存在的一样长的字符串 slice。这就是我们告诉 Rust 需要其保证的协议。

+

通过在函数签名中指定生命周期参数,我们不会改变任何参数或返回值的生命周期,不过我们说过任何不坚持这个协议的类型都将被借用检查器拒绝。这个函数并不知道(或需要知道)xy具体会存在多久,不过只需要知道一些可以使用'a替代的作用域将会满足这个签名。

+

当在函数中使用生命周期注解时,这些注解出现在函数签名中,而不存在于函数体中的任何代码中。这是因为 Rust 能够分析函数中代码而不需要任何协助,不过当函数引用或被函数之外的代码引用时,参数或返回值的生命周期可能在每次函数被调用时都不同。这可能会产生惊人的消耗并且对于 Rust 来说经常都是不可能分析的。在这种情况下,我们需要自己标注生命周期。

+
diff --git a/docs/print.html b/docs/print.html index 6d1d60a..7744977 100644 --- a/docs/print.html +++ b/docs/print.html @@ -4988,7 +4988,375 @@ impl Summarizable for Tweet { Tweet types

-

在类型上实现 trait 类似与实现与 trait 无关的方法。区别在于impl关键字之后,我们提供需要实现 trait 的名称,接着是for和需要实现 trait 的类型的名称。

+

在类型上实现 trait 类似与实现与 trait 无关的方法。区别在于impl关键字之后,我们提供需要实现 trait 的名称,接着是for和需要实现 trait 的类型的名称。在impl块中,使用 trait 定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现 trait 方法所拥有的行为。

+

一旦实现了 trait,我们就可以用与NewsArticleTweet实例的非 trait 方法一样的方式调用 trait 方法了:

+
let tweet = Tweet {
+    username: String::from("horse_ebooks"),
+    content: String::from("of course, as you probably already know, people"),
+    reply: false,
+    retweet: false,
+};
+
+println!("1 new tweet: {}", tweet.summary());
+
+

这会打印出1 new tweet: horse_ebooks: of course, as you probably already know, people

+

注意因为列表 10-12 中我们在相同的lib.rs力定义了Summarizable trait 和NewsArticleTweet类型,所以他们是位于同一作用域的。如果这个lib.rs是对应aggregator crate 的,而别人想要利用我们 crate 的功能外加为其WeatherForecast结构体实现Summarizable trait,在实现Summarizable trait 之前他们首先就需要将其导入其作用域中,如列表 10-13 所示:

+
+Filename: lib.rs +
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)
+    }
+}
+
+
+

Listing 10-13: Bringing the Summarizable trait from our aggregator crate +into scope in another crate

+
+
+

另外这段代码假设Summarizable是一个公有 trait,这是因为列表 10-11 中trait之前使用了pub关键字。

+

trait 实现的一个需要注意的限制是:只能在 trait 或对应类型位于我们 crate 本地的时候为其实现 trait。换句话说,不允许对外部类型实现外部 trait。例如,不能Vec上实现Display trait,因为DisplayVec都定义于标准库中。允许在像Tweet这样作为我们aggregatorcrate 部分功能的自定义类型上实现标准库中的 trait Display。也允许在aggregatorcrate中为Vec实现Summarizable,因为Summarizable定义与此。这个限制是我们称为 orphan rule 的一部分,如果你感兴趣的可以在类型理论中找到它。简单来说,它被称为 orphan rule 是因为其父类型不存在。没有这条规则的话,两个 crate 可以分别对相同类型是实现相同的 trait,因而这两个实现会相互冲突:Rust 将无从得知应该使用哪一个。因为 Rust 强制执行 orphan rule,其他人编写的代码不会破坏你代码,反之亦是如此。

+

默认实现

+

有时为 trait 中的某些或全部提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。

+

列表 10-14 中展示了如何为Summarize trait 的summary方法指定一个默认的字符串值,而不是像列表 10-11 中那样只是定义方法签名:

+
+Filename: lib.rs +
pub trait Summarizable {
+    fn summary(&self) -> String {
+        String::from("(Read more...)")
+    }
+}
+
+
+

Listing 10-14: Definition of a Summarizable trait with a default +implementation of the summary method

+
+
+

如果想要对NewsArticle实例使用这个默认实现,而不是像列表 10-12 中那样定义一个自己的实现,则可以指定一个空的impl块:

+
impl Summarizable for NewsArticle {}
+
+

即便选择不再直接为NewsArticle定义summary方法了,因为summary方法有一个默认实现而且NewsArticle被指定为实现了Summarizable trait,我们仍然可以对NewsArticle的实例调用summary方法:

+
let article = 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."),
+};
+
+println!("New article available! {}", article.summary());
+
+

这段代码会打印New article available! (Read more...)

+

Summarizable trait 改变为拥有默认summary实现并不要求对列表 10-12 中的Tweet和列表 10-13 中的WeatherForecastSummarizable的实现做任何改变:重载一个默认实现的语法与实现没有默认实现的 trait 方法时完全一样的。

+

默认实现允许调用相同 trait 中的其他方法,哪怕这些方法没有默认实现。通过这种方法,trait 可以实现很多有用的功能而只需实现一小部分特定内容。我们可以选择让Summarizable trait 也拥有一个要求实现的author_summary方法,接着summary方法则提供默认实现并调用author_summary方法:

+
pub trait Summarizable {
+    fn author_summary(&self) -> String;
+
+    fn summary(&self) -> String {
+        format!("(Read more from {}...)", self.author_summary())
+    }
+}
+
+

为了使用这个版本的Summarizable,只需在实现 trait 时定义author_summary即可:

+
impl Summarizable for Tweet {
+    fn author_summary(&self) -> String {
+        format!("@{}", self.username)
+    }
+}
+
+

一旦定义了author_summary,我们就可以对Tweet结构体的实例调用summary了,而summary的默认实现会调用我们提供的author_summary定义。

+
let tweet = Tweet {
+    username: String::from("horse_ebooks"),
+    content: String::from("of course, as you probably already know, people"),
+    reply: false,
+    retweet: false,
+};
+
+println!("1 new tweet: {}", tweet.summary());
+
+

这会打印出1 new tweet: (Read more from @horse_ebooks...)

+

注意在重载过的实现中调用默认实现是不可能的。

+

trait bounds

+

现在我们定义了 trait 并在类型上实现了这些 trait,也可以对泛型类型参数使用 trait。我们可以限制泛型不再适用于任何类型,编译器会确保其被限制为那么实现了特定 trait 的类型,由此泛型就会拥有我们希望其类型所拥有的功能。这被称为指定泛型的 trait bounds

+

例如在列表 10-12 中为NewsArticleTweet类型实现了Summarizable trait。我们可以定义一个函数notify来调用summary方法,它拥有一个泛型类型T的参数item。为了能够在item上调用summary而不出现错误,我们可以在T上使用 trait bounds 来指定item必须是实现了Summarizable trait 的类型:

+
pub fn notify<T: Summarizable>(item: T) {
+    println!("Breaking news! {}", item.summary());
+}
+
+

trait bounds 连同泛型类型参数声明一同出现,位于尖括号中的冒号后面。由于T上的 trait bounds,我们可以传递任何NewsArticleTweet的实例来调用notify函数。列表 10-13 中使用我们aggregator crate 的外部代码也可以传递一个WeatherForecast的实例来调用notify函数,因为WeatherForecast同样也实现了Summarizable。使用任何其他类型,比如Stringi32,来调用notify的代码将不能编译,因为这些类型没有实现Summarizable

+

可以通过+来为泛型指定多个 trait bounds。如果我们需要能够在函数中使用T类型的显示格式的同时也能使用summary方法,则可以使用 trait bounds T: Summarizable + Display。这意味着T可以是任何是实现了SummarizableDisplay的类型。

+

对于拥有多个泛型类型参数的函数,每一个泛型都可以有其自己的 trait bounds。在函数名和参数列表之间的尖括号中指定很多的 trait bound 信息将是难以阅读的,所以有另外一个指定 trait bounds 的语法,它将其移动到函数签名后的where从句中。所以相比这样写:

+
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {
+
+

我们也可以使用where从句:

+
fn some_function<T, U>(t: T, u: U) -> i32
+    where T: Display + Clone,
+          U: Clone + Debug
+{
+
+

这就显得不那么杂乱,同时也使这个函数看起来更像没有很多 trait bounds 的函数。这时函数名、参数列表和返回值类型都离得很近。

+

使用 trait bounds 来修复largest函数

+

所以任何想要对泛型使用 trait 定义的行为的时候,都需要在泛型参数类型上指定 trait bounds。现在我们就可以修复列表 10-5 中那个使用泛型类型参数的largest函数定义了!当我们将其放置不管的时候,它会出现这个错误:

+
error[E0369]: binary operation `>` cannot be applied to type `T`
+  |
+5 |         if item > largest {
+  |            ^^^^
+  |
+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 中所以并不需要手动将其引入作用域。

+
fn largest<T: PartialOrd>(list: &[T]) -> T {
+
+

但是如果编译代码的话,会出现不同的错误:

+
error[E0508]: cannot move out of type `[T]`, a non-copy array
+ --> src/main.rs:4:23
+  |
+4 |     let mut largest = list[0];
+  |         -----------   ^^^^^^^ cannot move out of here
+  |         |
+  |         hint: to prevent move, use `ref largest` or `ref mut largest`
+
+error[E0507]: cannot move out of borrowed content
+ --> src/main.rs:6:9
+  |
+6 |     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函数,我们只尝试了寻找最大的i32char。正如第四章讨论过的,像i32char这样的类型是已知大小的并可以储存在栈上,所以他们实现了Copy trait。当我们将largest函数改成使用泛型后,现在list参数的类型就有可能是没有实现Copy trait 的,这意味着我们可能不能将list[0]的值移动到largest变量中。

+

如果只想对实现了Copy的类型调用这些带啊吗,可以在T的 trait bounds 中增加Copy!列表 10-15 中展示了一个可以编译的泛型版本的largest函数的完整代码,只要传递给largest的 slice 值的类型实现了PartialOrdCopy这两个 trait,例如i32char

+
+Filename: src/main.rs +
use std::cmp::PartialOrd;
+
+fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
+    let mut largest = list[0];
+
+    for &item in list.iter() {
+        if item > largest {
+            largest = item;
+        }
+    }
+
+    largest
+}
+
+fn main() {
+    let numbers = vec![34, 50, 25, 100, 65];
+
+    let result = largest(&numbers);
+    println!("The largest number is {}", result);
+
+    let chars = vec!['y', 'm', 'a', 'q'];
+
+    let result = largest(&chars);
+    println!("The largest char is {}", result);
+}
+
+
+

Listing 10-15: A working definition of the largest function that works on any +generic type that implements the PartialOrd and Copy traits

+
+
+

如果并不希望限制largest函数只能用于实现了Copy trait 的类型,我们可以在T的 trait bounds 中指定Clone而不是Copy,并克隆 slice 的每一个值使得largest函数拥有其所有权。但是使用clone函数潜在意味着更多的堆分配,而且堆分配在涉及大量数据时可能会相当缓慢。另一种largest的实现方式是返回 slice 中一个T值的引用。如果我们将函数返回值从T改为&T并改变函数体使其能够返回一个引用,我们将不需要任何CloneCopy的 trait bounds 而且也不会有任何的堆分配。尝试自己实现这种替代解决方式吧!

+

trait 和 trait bounds 让我们使用泛型类型参数来减少重复,并仍然能够向编译器明确指定泛型类型需要拥有哪些行为。因为我们向编译器提供了 trait bounds 信息,它就可以检查代码中所用到的具体类型是否提供了正确的行为。在动态类型语言中,如果我们尝试调用一个类型并没有实现的方法,会在运行时出现错误。Rust 将这些错误移动到了编译时,甚至在代码能够运行之前就强迫我们修复错误。另外,我们也无需编写运行时检查行为的代码,因为在编译时就已经检查过了,这样相比其他那些不愿放弃泛型灵活性的语言有更好的性能。

+

这里还有一种泛型,我们一直在使用它甚至都没有察觉它的存在,这就是生命周期lifetimes)。不同于其他泛型帮助我们确保类型拥有期望的行为,生命周期则有助于确保引用在我们需要他们的时候一直有效。让我们学习生命周期是如何做到这些的。

+

生命周期与引用有效性

+
+

ch10-03-lifetime-syntax.md +
+commit d7a4e99554da53619dd71044273535ba0186f40a

+
+

当在第四章讨论引用时,我们遗漏了一个重要的细节:Rust 中的每一个引用都有其生命周期,也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以多种不同方式向关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。

+

好吧,这有点不太寻常,而且也不同于其他语言中使用的工具。生命周期,从某种意义上说,是 Rust 最与众不同的功能。

+

生命周期是一个很广泛的话题,本章不可能涉及到它全部的内容,所以这里我们会讲到一些通常你可能会遇到的生命周期语法以便你熟悉这个概念。第十九章会包含生命周期所有功能的更高级的内容。

+

生命周期避免了悬垂引用

+

生命周期的主要目标是避免悬垂引用,它会导致程序引用了并非其期望引用的数据。考虑一下列表 10-16 中的程序,它有一个外部作用域和一个内部作用域,外部作用域声明了一个没有初值的变量r,而内部作用域声明了一个初值为 5 的变量x。在内部作用域中,我们尝试将r的值设置为一个x的引用。接着在内部作用域结束后,尝试打印出r的值:

+
+
{
+    let r;
+
+    {
+        let x = 5;
+        r = &x;
+    }
+
+    println!("r: {}", r);
+}
+
+
+

Listing 10-16: An attempt to use a reference whose value has gone out of scope

+
+
+
+

未初始化变量不能被使用

+

接下来的一些例子中声明了没有初始值的变量,以便这些变量存在于外部作用域。这看起来好像和 Rust 不允许存在空值相冲突。然而这是可以的,如果我们尝试在给它一个值之前使用这个变量,会出现一个编译时错误。请自行尝试!

+
+

当编译这段代码时会得到一个错误:

+
error: `x` does not live long enough
+   |
+6  |         r = &x;
+   |              - borrow occurs here
+7  |     }
+   |     ^ `x` dropped here while still borrowed
+...
+10 | }
+   | - borrowed value needs to live until here
+
+

变量x并没有“存在的足够久”。为什么呢?好吧,x在到达第 7 行的大括号的结束时就离开了作用域,这也是内部作用域的结尾。不过r在外部作用域也是有效的;作用域越大我们就说它“存在的越久”。如果 Rust 允许这段代码工作,r将会引用在x离开作用域时被释放的内存,这时尝试对r做任何操作都会不能正常工作。那么 Rust 是如何决定这段代码是不被允许的呢?

+

借用检查器

+

编译器的这一部分叫做借用检查器borrow checker),它比较作用域来确保所有的借用都是有效的。列表 10-17 展示了与列表 10-16 相同的例子不过带有变量声明周期的注释:

+
+
{
+    let r;         // -------+-- 'a
+                   //        |
+    {              //        |
+        let x = 5; // -+-----+-- 'b
+        r = &x;    //  |     |
+    }              // -+     |
+                   //        |
+    println!("r: {}", r); // |
+                   //        |
+                   // -------+
+}
+
+
+

Listing 10-17: Annotations of the lifetimes of x and r, named 'a and 'b +respectively

+
+
+ + +

我们将r的声明周期标记为'a而将x的生命周期标记为'b。如你所见,内部的'b块要比外部的生命周期'a小得多。在编译时,Rust 比较这两个生命周期的大小,并发现r拥有声明周期'a,不过它引用了一个拥有生命周期'b的对象。程序被拒绝编译,因为生命周期'b比生命周期'a要小:引用者没有比被引用者存在的更久。

+

让我们看看列表 10-18 中这个并没有产生悬垂引用且可以正常编译的例子:

+
+
{
+    let x = 5;            // -----+-- 'b
+                          //      |
+    let r = &x;           // --+--+-- 'a
+                          //   |  |
+    println!("r: {}", r); //   |  |
+                          // --+  |
+}                         // -----+
+
+
+

Listing 10-18: A valid reference because the data has a longer lifetime than +the reference

+
+
+

x拥有生命周期 'b,在这里它比 'a要大。这就意味着r可以引用x:Rust 知道r中的引用在x有效的时候也会一直有效。

+

现在我们已经在一个具体的例子中展示了引用的声明周期位于何处,并讨论了 Rust 如何分析生命周期来保证引用总是有效的,接下来让我们聊聊在函数的上下文中参数和返回值的泛型生命周期。

+

函数中的泛型生命周期

+

让我们来编写一个返回两个字符串 slice 中最长的那一个的函数。我们希望能够通过传递两个字符串 slice 来调用这个函数,并希望返回一个字符串 slice。一旦我们实现了longest函数,列表 10-19 中的代码应该会打印出The longest string is abcd

+
+

Filename: src/main.rs

+
fn main() {
+    let string1 = String::from("abcd");
+    let string2 = "xyz";
+
+    let result = longest(string1.as_str(), string2);
+    println!("The longest string is {}", result);
+}
+
+
+

Listing 10-19: A main function that calls the longest function to find the +longest of two string slices

+
+
+

注意函数期望获取字符串 slice(如第四章所讲到的这是引用)因为我们并不希望longest函数获取其参数的引用。我们希望函数能够接受String的 slice(也就是变量string1的类型)和字符串字面值(也就是变量string2包含的值)。

+ + +

参考之前第四章中的“字符串 slice 作为参数”部分中更多关于为什么上面例子中的参数正是我们想要的讨论。

+

如果尝试像列表 10-20 中那样实现longest函数,它并不能编译:

+
+Filename: src/main.rs +
fn longest(x: &str, y: &str) -> &str {
+    if x.len() > y.len() {
+        x
+    } else {
+        y
+    }
+}
+
+
+

Listing 10-20: An implementation of the longest function that returns the +longest of two string slices, but does not yet compile

+
+
+

将会出现如下有关生命周期的错误:

+
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`
+
+

提示文本告诉我们返回值需要一个泛型生命周期参数,因为 Rust 并不知道将要返回的引用是指向xy。事实上我们也不知道,因为函数体中if块返回一个x的引用而else块返回一个y的引用。

+

虽然我们定义了这个函数,但是并不知道传递给函数的具体值,所以也不知道到底是if还是else会被执行。我们也不知道传入的引用的具体生命周期,所以也就不能像列表 10-17 和 10-18 那样通过观察作用域来确定返回的引用总是有效的。借用检查器自身同样也无法确定,因为它不知道xy的生命周期是如何与返回值的生命周期相关联的。接下来我们将增加泛型生命周期参数来定义引用间的关系以便借用检查器可以进行相关分析。

+

生命周期注解语法

+

生命周期注解并不改变任何引用的生命周期的长短。与当函数签名中指定了泛型类型参数后就可以接受任何类型一样,当指定了泛型生命周期后函数也能接受任何生命周期的引用。生命周期注解所做的就是将多个引用的生命周期联系起来。

+

生命周期注解有着一个不太常见的语法:生命周期参数名称必须以撇号(')开头。生命周期参数的名称通常全是小写,而且类似于泛型类型,其名称通常非常短。'a是大多数人默认使用的名称。生命周期参数注解位于引用的&之后,并有一个空格来将引用类型与生命周期注解分隔开。

+

这里有一些例子:我们有一个没有生命周期参数的i32的引用,一个有叫做'a的生命周期参数的i32的引用,和一个也有的生命周期参数'ai32的可变引用:

+
&i32        // a reference
+&'a i32     // a reference with an explicit lifetime
+&'a mut i32 // a mutable reference with an explicit lifetime
+
+

生命周期注解本身没有多少意义:生命周期注解告诉 Rust 多个引用的泛型生命周期参数如何相互联系。如果函数有一个生命周期'ai32的引用的参数first,还有另一个同样是生命周期'ai32的引用的参数second,这两个生命周期注解有相同的名称意味着firstsecond必须与这相同的泛型生命周期存在得一样久。

+

函数签名中的生命周期注解

+

来看看我们编写的longest函数的上下文中的生命周期。就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的加括号中。这里我们想要告诉 Rust 关于参数中的引用和返回值之间的限制是他们都必须拥有相同的生命周期,就像列表 10-21 中在每个引用中都加上了'a那样:

+
+Filename: src/main.rs +
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
+    if x.len() > y.len() {
+        x
+    } else {
+        y
+    }
+}
+
+
+

Listing 10-21: The longest function definition that specifies all the +references in the signature must have the same lifetime, 'a

+
+
+

这段代码能够编译并会产生我们想要使用列表 10-19 中的main函数得到的结果。

+

现在函数签名表明对于某些生命周期'a,函数会获取两个参数,他们都是与生命周期'a存在的一样长的字符串 slice。函数会返回一个同样也与生命周期'a存在的一样长的字符串 slice。这就是我们告诉 Rust 需要其保证的协议。

+

通过在函数签名中指定生命周期参数,我们不会改变任何参数或返回值的生命周期,不过我们说过任何不坚持这个协议的类型都将被借用检查器拒绝。这个函数并不知道(或需要知道)xy具体会存在多久,不过只需要知道一些可以使用'a替代的作用域将会满足这个签名。

+

当在函数中使用生命周期注解时,这些注解出现在函数签名中,而不存在于函数体中的任何代码中。这是因为 Rust 能够分析函数中代码而不需要任何协助,不过当函数引用或被函数之外的代码引用时,参数或返回值的生命周期可能在每次函数被调用时都不同。这可能会产生惊人的消耗并且对于 Rust 来说经常都是不可能分析的。在这种情况下,我们需要自己标注生命周期。

diff --git a/src/ch10-02-traits.md b/src/ch10-02-traits.md index 7514e42..f92e3b6 100644 --- a/src/ch10-02-traits.md +++ b/src/ch10-02-traits.md @@ -84,4 +84,267 @@ Listing 10-12: Implementing the `Summarizable` trait on the `NewsArticle` and -在类型上实现 trait 类似与实现与 trait 无关的方法。区别在于`impl`关键字之后,我们提供需要实现 trait 的名称,接着是`for`和需要实现 trait 的类型的名称。 \ No newline at end of file +在类型上实现 trait 类似与实现与 trait 无关的方法。区别在于`impl`关键字之后,我们提供需要实现 trait 的名称,接着是`for`和需要实现 trait 的类型的名称。在`impl`块中,使用 trait 定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现 trait 方法所拥有的行为。 + +一旦实现了 trait,我们就可以用与`NewsArticle`和`Tweet`实例的非 trait 方法一样的方式调用 trait 方法了: + +```rust,ignore +let tweet = Tweet { + username: String::from("horse_ebooks"), + content: String::from("of course, as you probably already know, people"), + reply: false, + retweet: false, +}; + +println!("1 new tweet: {}", tweet.summary()); +``` + +这会打印出`1 new tweet: horse_ebooks: of course, as you probably already know, people`。 + +注意因为列表 10-12 中我们在相同的`lib.rs`力定义了`Summarizable` trait 和`NewsArticle`与`Tweet`类型,所以他们是位于同一作用域的。如果这个`lib.rs`是对应`aggregator` crate 的,而别人想要利用我们 crate 的功能外加为其`WeatherForecast`结构体实现`Summarizable` trait,在实现`Summarizable` trait 之前他们首先就需要将其导入其作用域中,如列表 10-13 所示: + +
+Filename: lib.rs + +```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) + } +} +``` + +
+ +Listing 10-13: Bringing the `Summarizable` trait from our `aggregator` crate +into scope in another crate + +
+
+ +另外这段代码假设`Summarizable`是一个公有 trait,这是因为列表 10-11 中`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 中的某些或全部提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。 + +列表 10-14 中展示了如何为`Summarize` trait 的`summary`方法指定一个默认的字符串值,而不是像列表 10-11 中那样只是定义方法签名: + +
+Filename: lib.rs + +```rust +pub trait Summarizable { + fn summary(&self) -> String { + String::from("(Read more...)") + } +} +``` + +
+ +Listing 10-14: Definition of a `Summarizable` trait with a default +implementation of the `summary` method + +
+
+ +如果想要对`NewsArticle`实例使用这个默认实现,而不是像列表 10-12 中那样定义一个自己的实现,则可以指定一个空的`impl`块: + +```rust,ignore +impl Summarizable for NewsArticle {} +``` + +即便选择不再直接为`NewsArticle`定义`summary`方法了,因为`summary`方法有一个默认实现而且`NewsArticle`被指定为实现了`Summarizable` trait,我们仍然可以对`NewsArticle`的实例调用`summary`方法: + +```rust,ignore +let article = 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."), +}; + +println!("New article available! {}", article.summary()); +``` + +这段代码会打印`New article available! (Read more...)`。 + +将`Summarizable` trait 改变为拥有默认`summary`实现并不要求对列表 10-12 中的`Tweet`和列表 10-13 中的`WeatherForecast`对`Summarizable`的实现做任何改变:重载一个默认实现的语法与实现没有默认实现的 trait 方法时完全一样的。 + +默认实现允许调用相同 trait 中的其他方法,哪怕这些方法没有默认实现。通过这种方法,trait 可以实现很多有用的功能而只需实现一小部分特定内容。我们可以选择让`Summarizable` trait 也拥有一个要求实现的`author_summary`方法,接着`summary`方法则提供默认实现并调用`author_summary`方法: + +```rust +pub trait Summarizable { + fn author_summary(&self) -> String; + + fn summary(&self) -> String { + format!("(Read more from {}...)", self.author_summary()) + } +} +``` + +为了使用这个版本的`Summarizable`,只需在实现 trait 时定义`author_summary`即可: + + +```rust,ignore +impl Summarizable for Tweet { + fn author_summary(&self) -> String { + format!("@{}", self.username) + } +} +``` + +一旦定义了`author_summary`,我们就可以对`Tweet`结构体的实例调用`summary`了,而`summary`的默认实现会调用我们提供的`author_summary`定义。 + +```rust,ignore +let tweet = Tweet { + username: String::from("horse_ebooks"), + content: String::from("of course, as you probably already know, people"), + reply: false, + retweet: false, +}; + +println!("1 new tweet: {}", tweet.summary()); +``` + +这会打印出`1 new tweet: (Read more from @horse_ebooks...)`。 + +注意在重载过的实现中调用默认实现是不可能的。 + +### trait bounds + +现在我们定义了 trait 并在类型上实现了这些 trait,也可以对泛型类型参数使用 trait。我们可以限制泛型不再适用于任何类型,编译器会确保其被限制为那么实现了特定 trait 的类型,由此泛型就会拥有我们希望其类型所拥有的功能。这被称为指定泛型的 *trait bounds*。 + +例如在列表 10-12 中为`NewsArticle`和`Tweet`类型实现了`Summarizable` trait。我们可以定义一个函数`notify`来调用`summary`方法,它拥有一个泛型类型`T`的参数`item`。为了能够在`item`上调用`summary`而不出现错误,我们可以在`T`上使用 trait bounds 来指定`item`必须是实现了`Summarizable` trait 的类型: + +```rust,ignore +pub fn notify(item: T) { + println!("Breaking news! {}", item.summary()); +} +``` + +trait bounds 连同泛型类型参数声明一同出现,位于尖括号中的冒号后面。由于`T`上的 trait bounds,我们可以传递任何`NewsArticle`或`Tweet`的实例来调用`notify`函数。列表 10-13 中使用我们`aggregator` crate 的外部代码也可以传递一个`WeatherForecast`的实例来调用`notify`函数,因为`WeatherForecast`同样也实现了`Summarizable`。使用任何其他类型,比如`String`或`i32`,来调用`notify`的代码将不能编译,因为这些类型没有实现`Summarizable`。 + +可以通过`+`来为泛型指定多个 trait bounds。如果我们需要能够在函数中使用`T`类型的显示格式的同时也能使用`summary`方法,则可以使用 trait bounds `T: Summarizable + Display`。这意味着`T`可以是任何是实现了`Summarizable`和`Display`的类型。 + +对于拥有多个泛型类型参数的函数,每一个泛型都可以有其自己的 trait bounds。在函数名和参数列表之间的尖括号中指定很多的 trait bound 信息将是难以阅读的,所以有另外一个指定 trait bounds 的语法,它将其移动到函数签名后的`where`从句中。所以相比这样写: + +```rust,ignore +fn some_function(t: T, u: U) -> i32 { +``` + +我们也可以使用`where`从句: + +```rust,ignore +fn some_function(t: T, u: U) -> i32 + where T: Display + Clone, + U: Clone + Debug +{ +``` + +这就显得不那么杂乱,同时也使这个函数看起来更像没有很多 trait bounds 的函数。这时函数名、参数列表和返回值类型都离得很近。 + +### 使用 trait bounds 来修复`largest`函数 + +所以任何想要对泛型使用 trait 定义的行为的时候,都需要在泛型参数类型上指定 trait bounds。现在我们就可以修复列表 10-5 中那个使用泛型类型参数的`largest`函数定义了!当我们将其放置不管的时候,它会出现这个错误: + +``` +error[E0369]: binary operation `>` cannot be applied to type `T` + | +5 | if item > largest { + | ^^^^ + | +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 中所以并不需要手动将其引入作用域。 + +```rust,ignore +fn largest(list: &[T]) -> T { +``` + +但是如果编译代码的话,会出现不同的错误: + +```text +error[E0508]: cannot move out of type `[T]`, a non-copy array + --> src/main.rs:4:23 + | +4 | let mut largest = list[0]; + | ----------- ^^^^^^^ cannot move out of here + | | + | hint: to prevent move, use `ref largest` or `ref mut largest` + +error[E0507]: cannot move out of borrowed content + --> src/main.rs:6:9 + | +6 | 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`变量中。 + +如果只想对实现了`Copy`的类型调用这些带啊吗,可以在`T`的 trait bounds 中增加`Copy`!列表 10-15 中展示了一个可以编译的泛型版本的`largest`函数的完整代码,只要传递给`largest`的 slice 值的类型实现了`PartialOrd`和`Copy`这两个 trait,例如`i32`和`char`: + +
+Filename: src/main.rs + +```rust +use std::cmp::PartialOrd; + +fn largest(list: &[T]) -> T { + let mut largest = list[0]; + + for &item in list.iter() { + if item > largest { + largest = item; + } + } + + largest +} + +fn main() { + let numbers = vec![34, 50, 25, 100, 65]; + + let result = largest(&numbers); + println!("The largest number is {}", result); + + let chars = vec!['y', 'm', 'a', 'q']; + + let result = largest(&chars); + println!("The largest char is {}", result); +} +``` + +
+ +Listing 10-15: A working definition of the `largest` function that works on any +generic type that implements the `PartialOrd` and `Copy` traits + +
+
+ +如果并不希望限制`largest`函数只能用于实现了`Copy` trait 的类型,我们可以在`T`的 trait bounds 中指定`Clone`而不是`Copy`,并克隆 slice 的每一个值使得`largest`函数拥有其所有权。但是使用`clone`函数潜在意味着更多的堆分配,而且堆分配在涉及大量数据时可能会相当缓慢。另一种`largest`的实现方式是返回 slice 中一个`T`值的引用。如果我们将函数返回值从`T`改为`&T`并改变函数体使其能够返回一个引用,我们将不需要任何`Clone`或`Copy`的 trait bounds 而且也不会有任何的堆分配。尝试自己实现这种替代解决方式吧! + +trait 和 trait bounds 让我们使用泛型类型参数来减少重复,并仍然能够向编译器明确指定泛型类型需要拥有哪些行为。因为我们向编译器提供了 trait bounds 信息,它就可以检查代码中所用到的具体类型是否提供了正确的行为。在动态类型语言中,如果我们尝试调用一个类型并没有实现的方法,会在运行时出现错误。Rust 将这些错误移动到了编译时,甚至在代码能够运行之前就强迫我们修复错误。另外,我们也无需编写运行时检查行为的代码,因为在编译时就已经检查过了,这样相比其他那些不愿放弃泛型灵活性的语言有更好的性能。 + +这里还有一种泛型,我们一直在使用它甚至都没有察觉它的存在,这就是**生命周期**(*lifetimes*)。不同于其他泛型帮助我们确保类型拥有期望的行为,生命周期则有助于确保引用在我们需要他们的时候一直有效。让我们学习生命周期是如何做到这些的。 \ No newline at end of file diff --git a/src/ch10-03-lifetime-syntax.md b/src/ch10-03-lifetime-syntax.md index e69de29..bac853f 100644 --- a/src/ch10-03-lifetime-syntax.md +++ b/src/ch10-03-lifetime-syntax.md @@ -0,0 +1,258 @@ +## 生命周期与引用有效性 + +> [ch10-03-lifetime-syntax.md](https://github.com/rust-lang/book/blob/master/src/ch10-03-lifetime-syntax.md) +>
+> commit d7a4e99554da53619dd71044273535ba0186f40a + +当在第四章讨论引用时,我们遗漏了一个重要的细节:Rust 中的每一个引用都有其**生命周期**,也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以多种不同方式向关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。 + +好吧,这有点不太寻常,而且也不同于其他语言中使用的工具。生命周期,从某种意义上说,是 Rust 最与众不同的功能。 + +生命周期是一个很广泛的话题,本章不可能涉及到它全部的内容,所以这里我们会讲到一些通常你可能会遇到的生命周期语法以便你熟悉这个概念。第十九章会包含生命周期所有功能的更高级的内容。 + +### 生命周期避免了悬垂引用 + +生命周期的主要目标是避免悬垂引用,它会导致程序引用了并非其期望引用的数据。考虑一下列表 10-16 中的程序,它有一个外部作用域和一个内部作用域,外部作用域声明了一个没有初值的变量`r`,而内部作用域声明了一个初值为 5 的变量`x`。在内部作用域中,我们尝试将`r`的值设置为一个`x`的引用。接着在内部作用域结束后,尝试打印出`r`的值: + +
+ +```rust,ignore +{ + let r; + + { + let x = 5; + r = &x; + } + + println!("r: {}", r); +} +``` + +
+ +Listing 10-16: An attempt to use a reference whose value has gone out of scope + +
+
+ +> ### 未初始化变量不能被使用 +> +> 接下来的一些例子中声明了没有初始值的变量,以便这些变量存在于外部作用域。这看起来好像和 Rust 不允许存在空值相冲突。然而这是可以的,如果我们尝试在给它一个值之前使用这个变量,会出现一个编译时错误。请自行尝试! + +当编译这段代码时会得到一个错误: + +``` +error: `x` does not live long enough + | +6 | r = &x; + | - borrow occurs here +7 | } + | ^ `x` dropped here while still borrowed +... +10 | } + | - borrowed value needs to live until here +``` + +变量`x`并没有“存在的足够久”。为什么呢?好吧,`x`在到达第 7 行的大括号的结束时就离开了作用域,这也是内部作用域的结尾。不过`r`在外部作用域也是有效的;作用域越大我们就说它“存在的越久”。如果 Rust 允许这段代码工作,`r`将会引用在`x`离开作用域时被释放的内存,这时尝试对`r`做任何操作都会不能正常工作。那么 Rust 是如何决定这段代码是不被允许的呢? + +#### 借用检查器 + +编译器的这一部分叫做**借用检查器**(*borrow checker*),它比较作用域来确保所有的借用都是有效的。列表 10-17 展示了与列表 10-16 相同的例子不过带有变量声明周期的注释: + +
+ +```rust,ignore +{ + let r; // -------+-- 'a + // | + { // | + let x = 5; // -+-----+-- 'b + r = &x; // | | + } // -+ | + // | + println!("r: {}", r); // | + // | + // -------+ +} +``` + +
+ +Listing 10-17: Annotations of the lifetimes of `x` and `r`, named `'a` and `'b` +respectively + +
+
+ + + + +我们将`r`的声明周期标记为`'a`而将`x`的生命周期标记为`'b`。如你所见,内部的`'b`块要比外部的生命周期`'a`小得多。在编译时,Rust 比较这两个生命周期的大小,并发现`r`拥有声明周期`'a`,不过它引用了一个拥有生命周期`'b`的对象。程序被拒绝编译,因为生命周期`'b`比生命周期`'a`要小:引用者没有比被引用者存在的更久。 + +让我们看看列表 10-18 中这个并没有产生悬垂引用且可以正常编译的例子: + +
+ +```rust +{ + let x = 5; // -----+-- 'b + // | + let r = &x; // --+--+-- 'a + // | | + println!("r: {}", r); // | | + // --+ | +} // -----+ +``` + +
+ +Listing 10-18: A valid reference because the data has a longer lifetime than +the reference + +
+
+ +`x`拥有生命周期 `'b`,在这里它比 `'a`要大。这就意味着`r`可以引用`x`:Rust 知道`r`中的引用在`x`有效的时候也会一直有效。 + +现在我们已经在一个具体的例子中展示了引用的声明周期位于何处,并讨论了 Rust 如何分析生命周期来保证引用总是有效的,接下来让我们聊聊在函数的上下文中参数和返回值的泛型生命周期。 + +### 函数中的泛型生命周期 + +让我们来编写一个返回两个字符串 slice 中最长的那一个的函数。我们希望能够通过传递两个字符串 slice 来调用这个函数,并希望返回一个字符串 slice。一旦我们实现了`longest`函数,列表 10-19 中的代码应该会打印出`The longest string is abcd`: + +
+ +Filename: src/main.rs + +```rust +fn main() { + let string1 = String::from("abcd"); + let string2 = "xyz"; + + let result = longest(string1.as_str(), string2); + println!("The longest string is {}", result); +} +``` + +
+ +Listing 10-19: A `main` function that calls the `longest` function to find the +longest of two string slices + +
+
+ +注意函数期望获取字符串 slice(如第四章所讲到的这是引用)因为我们并不希望`longest`函数获取其参数的引用。我们希望函数能够接受`String`的 slice(也就是变量`string1`的类型)和字符串字面值(也就是变量`string2`包含的值)。 + + + + +参考之前第四章中的“字符串 slice 作为参数”部分中更多关于为什么上面例子中的参数正是我们想要的讨论。 + +如果尝试像列表 10-20 中那样实现`longest`函数,它并不能编译: + +
+Filename: src/main.rs + +```rust,ignore +fn longest(x: &str, y: &str) -> &str { + if x.len() > y.len() { + x + } else { + y + } +} +``` + +
+ +Listing 10-20: An implementation of the `longest` function that returns the +longest of two string slices, but does not yet compile + +
+
+ +将会出现如下有关生命周期的错误: + +``` +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` +``` + +提示文本告诉我们返回值需要一个泛型生命周期参数,因为 Rust 并不知道将要返回的引用是指向`x`或`y`。事实上我们也不知道,因为函数体中`if`块返回一个`x`的引用而`else`块返回一个`y`的引用。 + +虽然我们定义了这个函数,但是并不知道传递给函数的具体值,所以也不知道到底是`if`还是`else`会被执行。我们也不知道传入的引用的具体生命周期,所以也就不能像列表 10-17 和 10-18 那样通过观察作用域来确定返回的引用总是有效的。借用检查器自身同样也无法确定,因为它不知道`x`和`y`的生命周期是如何与返回值的生命周期相关联的。接下来我们将增加泛型生命周期参数来定义引用间的关系以便借用检查器可以进行相关分析。 + +### 生命周期注解语法 + +生命周期注解并不改变任何引用的生命周期的长短。与当函数签名中指定了泛型类型参数后就可以接受任何类型一样,当指定了泛型生命周期后函数也能接受任何生命周期的引用。生命周期注解所做的就是将多个引用的生命周期联系起来。 + +生命周期注解有着一个不太常见的语法:生命周期参数名称必须以撇号(`'`)开头。生命周期参数的名称通常全是小写,而且类似于泛型类型,其名称通常非常短。`'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 +``` + +生命周期注解本身没有多少意义:生命周期注解告诉 Rust 多个引用的泛型生命周期参数如何相互联系。如果函数有一个生命周期`'a`的`i32`的引用的参数`first`,还有另一个同样是生命周期`'a`的`i32`的引用的参数`second`,这两个生命周期注解有相同的名称意味着`first`和`second`必须与这相同的泛型生命周期存在得一样久。 + +### 函数签名中的生命周期注解 + +来看看我们编写的`longest`函数的上下文中的生命周期。就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的加括号中。这里我们想要告诉 Rust 关于参数中的引用和返回值之间的限制是他们都必须拥有相同的生命周期,就像列表 10-21 中在每个引用中都加上了`'a`那样: + +
+Filename: src/main.rs + +```rust +fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { + if x.len() > y.len() { + x + } else { + y + } +} +``` + +
+ +Listing 10-21: The `longest` function definition that specifies all the +references in the signature must have the same lifetime, `'a` + +
+
+ +这段代码能够编译并会产生我们想要使用列表 10-19 中的`main`函数得到的结果。 + +现在函数签名表明对于某些生命周期`'a`,函数会获取两个参数,他们都是与生命周期`'a`存在的一样长的字符串 slice。函数会返回一个同样也与生命周期`'a`存在的一样长的字符串 slice。这就是我们告诉 Rust 需要其保证的协议。 + +通过在函数签名中指定生命周期参数,我们不会改变任何参数或返回值的生命周期,不过我们说过任何不坚持这个协议的类型都将被借用检查器拒绝。这个函数并不知道(或需要知道)`x`和`y`具体会存在多久,不过只需要知道一些可以使用`'a`替代的作用域将会满足这个签名。 + +当在函数中使用生命周期注解时,这些注解出现在函数签名中,而不存在于函数体中的任何代码中。这是因为 Rust 能够分析函数中代码而不需要任何协助,不过当函数引用或被函数之外的代码引用时,参数或返回值的生命周期可能在每次函数被调用时都不同。这可能会产生惊人的消耗并且对于 Rust 来说经常都是不可能分析的。在这种情况下,我们需要自己标注生命周期。 \ No newline at end of file