From fcd74477e894a6be906e9e0cff20dce50a77b312 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Mon, 24 Apr 2017 23:03:13 +0800 Subject: [PATCH] wip: update ch17-01 --- docs/ch07-01-mod-and-the-filesystem.html | 2 +- ...07-02-controlling-visibility-with-pub.html | 4 +- docs/ch08-02-strings.html | 4 +- docs/ch10-00-generics.html | 5 +- docs/ch10-01-syntax.html | 10 +- docs/ch10-02-traits.html | 6 +- docs/ch10-03-lifetime-syntax.html | 18 +-- docs/ch11-03-test-organization.html | 2 +- ...2-01-accepting-command-line-arguments.html | 2 +- ...proving-error-handling-and-modularity.html | 4 +- ...04-testing-the-librarys-functionality.html | 2 +- ...05-working-with-environment-variables.html | 2 +- ...6-writing-to-stderr-instead-of-stdout.html | 2 +- docs/ch13-01-closures.html | 2 +- docs/ch13-02-iterators.html | 6 +- docs/ch13-04-performance.html | 4 +- docs/ch14-02-publishing-to-crates-io.html | 4 +- docs/ch14-03-cargo-workspaces.html | 2 +- docs/ch17-00-oop.html | 2 +- docs/ch17-01-what-is-oo.html | 54 ++++--- docs/ch17-02-trait-objects.html | 11 +- docs/print.html | 148 +++++++++--------- src/ch17-00-oop.md | 2 +- src/ch17-01-what-is-oo.md | 60 +++---- src/ch17-02-trait-objects.md | 8 +- 25 files changed, 186 insertions(+), 180 deletions(-) diff --git a/docs/ch07-01-mod-and-the-filesystem.html b/docs/ch07-01-mod-and-the-filesystem.html index 1be71ca..7bbfae1 100644 --- a/docs/ch07-01-mod-and-the-filesystem.html +++ b/docs/ch07-01-mod-and-the-filesystem.html @@ -292,7 +292,7 @@ $ mv src/server.rs src/network │ ├── mod.rs │ └── server.rs -

那么,当我们想要提取network::server模块时,为什么也必须将 src/network.rs 文件改名成 src/network/mod.rs 文件呢,还有为什么要将network::server的代码放入 network 目录的 src/network/server.rs 文件中,而不能将network::server模块提取到 src/server.rs 中呢?原因是如果 server.rs 文件在 src 目录中那么 Rust 就不能知道server应当是network的子模块。为了更清除的说明为什么 Rust 不知道,让我们考虑一下有着如下层级的另一个例子,它的所有定义都位于 src/lib.rs 中:

+

那么,当我们想要提取network::server模块时,为什么也必须将 src/network.rs 文件改名成 src/network/mod.rs 文件呢,还有为什么要将network::server的代码放入 network 目录的 src/network/server.rs 文件中,而不能将network::server模块提取到 src/server.rs 中呢?原因是如果 server.rs 文件在 src 目录中那么 Rust 就不能知道server应当是network的子模块。为了更清楚得说明为什么 Rust 不知道,让我们考虑一下有着如下层级的另一个例子,它的所有定义都位于 src/lib.rs 中:

communicator
  ├── client
  └── network
diff --git a/docs/ch07-02-controlling-visibility-with-pub.html b/docs/ch07-02-controlling-visibility-with-pub.html
index 8ffcc2b..4dc602b 100644
--- a/docs/ch07-02-controlling-visibility-with-pub.html
+++ b/docs/ch07-02-controlling-visibility-with-pub.html
@@ -217,7 +217,7 @@ some of which are incorrect

try_me函数位于项目的根模块。叫做outermost的模块是私有的,不过第二条私有性规则说明try_me函数允许访问outermost模块,因为outermost位于当前(根)模块,try_me也是。

outermost::middle_function的调用是正确的。因为middle_function是公有的,而try_me通过其父模块访问middle_functionoutermost。根据上一段的规则我们可以确定这个模块是可访问的。

outermost::middle_secret_function的调用会造成一个编译错误。middle_secret_function是私有的,所以第二条(私有性)规则生效了。根模块既不是middle_secret_function的当前模块(outermost是),也不是middle_secret_function当前模块的子模块。

-

叫做inside的模块是私有的且没有子模块,所以它只能被当前模块访问,outermost。这意味着try_me函数不允许调用outermost::inside::inner_functionoutermost::inside::secret_function任何一个。

+

叫做inside的模块是私有的且没有子模块,所以它只能被当前模块outermost访问。这意味着try_me函数不允许调用outermost::inside::inner_functionoutermost::inside::secret_function任何一个。

修改错误

这里有一些尝试修复错误的代码修改意见。在你尝试他们之前,猜测一下他们哪个能修复错误,接着编译查看你是否猜对了,并结合私有性规则理解为什么。

    @@ -226,7 +226,7 @@ some of which are incorrect

  • 如果在inner_function函数体中调用::outermost::middle_secret_function()?(开头的两个冒号意味着从根模块开始引用模块。)

请随意设计更多的实验并尝试理解他们!

-

接下来,让我们讨论一下使用use关键字来将项引入作用域。

+

接下来,让我们讨论一下使用use关键字将模块项目引入作用域。

diff --git a/docs/ch08-02-strings.html b/docs/ch08-02-strings.html index 8966d8f..9976e83 100644 --- a/docs/ch08-02-strings.html +++ b/docs/ch08-02-strings.html @@ -96,7 +96,7 @@ let s = "initial contents".to_string();

也可以使用String::from函数来从字符串字面值创建String。如下等同于使用to_string

let s = String::from("initial contents");
 
-

因为字符串使用广泛,这里有很多不同的用于字符串的通用 API 可供选择。他们有些可能显得有些多于,不过都有其用武之地!在这个例子中,String::from.to_string最终做了完全相同的工作,所以如何选择就是风格问题了。

+

因为字符串使用广泛,这里有很多不同的用于字符串的通用 API 可供选择。他们有些可能显得有些多余,不过都有其用武之地!在这个例子中,String::from.to_string最终做了完全相同的工作,所以如何选择就是风格问题了。

记住字符串是 UTF-8 编码的,所以可以包含任何可以正确编码的数据:

let hello = "السلام عليكم";
 let hello = "Dobrý den";
@@ -137,7 +137,7 @@ let s3 = s1 + &s2; // Note that s1 has been moved here and can no longer be
 
fn add(self, s: &str) -> String {
 

这并不是标准库中实际的签名;那个add使用泛型定义。这里的签名使用具体类型代替了泛型,这也正是当使用String值调用这个方法会发生的。第十章会讨论泛型。这个签名提供了理解+运算那奇怪的部分的线索。

-

首先,s2使用了&,意味着我们使用第二个字符串的引用与第一个字符串相加。这是因为add函数的s参数:只能将&strString相加,不能将两个String值相加。不过等一下——正如add的第二个参数所指定的,&s2的类型是&String而不是&str。那么为什么代码还能编译呢?之所以能够在add调用中使用&s2是因为&String可以被强转coerced)成 &str——当add函数被调用时,Rust 使用了一个被成为解引用强制多态deref coercion)的技术,你可以将其理解为它把&s2变成了&s2[..]以供add函数使用。第十五章会更深入的讨论解引用强制多态。因为add没有获取参数的所有权,所以s2在这个操作后仍然是有效的String

+

首先,s2使用了&,意味着我们使用第二个字符串的引用与第一个字符串相加。这是因为add函数的s参数:只能将&strString相加,不能将两个String值相加。不过等一下——正如add的第二个参数所指定的,&s2的类型是&String而不是&str。那么为什么代码还能编译呢?之所以能够在add调用中使用&s2是因为&String可以被强转coerced)成 &str——当add函数被调用时,Rust 使用了一个被称为解引用强制多态deref coercion)的技术,你可以将其理解为它把&s2变成了&s2[..]以供add函数使用。第十五章会更深入的讨论解引用强制多态。因为add没有获取参数的所有权,所以s2在这个操作后仍然是有效的String

其次,可以发现签名中add获取了self的所有权,因为self没有使用&。这意味着上面例子中的s1的所有权将被移动到add调用中,之后就不再有效。所以虽然let s3 = s1 + &s2;看起来就像它会复制两个字符串并创建一个新的字符串,而实际上这个语句会获取s1的所有权,附加上从s2中拷贝的内容,并返回结果的所有权。换句话说,它看起来好像生成了很多拷贝不过实际上并没有:这个实现比拷贝要更高效。

如果想要级联多个字符串,+的行为就显得笨重了:

let s1 = String::from("tic");
diff --git a/docs/ch10-00-generics.html b/docs/ch10-00-generics.html
index 87d1fbd..fb66001 100644
--- a/docs/ch10-00-generics.html
+++ b/docs/ch10-00-generics.html
@@ -130,12 +130,11 @@ of numbers

Listing 10-2: Code to find the largest number in two lists of numbers

-

虽然代码能够执行,但是重复的代码是冗余且已于出错的,并且意味着当更新逻辑时需要修改多处地方的代码。

+

虽然代码能够执行,但是重复的代码是冗余且容易出错的,并且意味着当更新逻辑时需要修改多处地方的代码。

-

为了消除重复,我们可以创建一层抽象,在这个例子中将表现为一个获取任意整型列表作为参数并对其进行处理的函数。这将增加代码的简洁性并让我们将表达和推导寻找列表中最大值的这个概念与使用这个概念的特定位置相互独。 -立。

+

为了消除重复,我们可以创建一层抽象,在这个例子中将表现为一个获取任意整型列表作为参数并对其进行处理的函数。这将增加代码的简洁性并让我们将表达和推导寻找列表中最大值的这个概念与使用这个概念的特定位置相互独立。

在列表 10-3 的程序中将寻找最大值的代码提取到了一个叫做largest的函数中。这个程序可以找出两个不同数字列表的最大值,不过列表 10-1 中的代码只存在于一个位置:

Filename: src/main.rs

fn largest(list: &[i32]) -> i32 {
diff --git a/docs/ch10-01-syntax.html b/docs/ch10-01-syntax.html
index f844ee4..6be0c1b 100644
--- a/docs/ch10-01-syntax.html
+++ b/docs/ch10-01-syntax.html
@@ -119,8 +119,8 @@ fn main() {
 

Listing 10-4: Two functions that differ only in their names and the types in their signatures

这里largest_i32largest_char有着完全相同的函数体,所以能够将这两个函数变成一个来减少重复就太好了。所幸通过引入一个泛型参数就能实现!

-

为了参数化要定义的函数的签名中的类型,我们需要像给函数的值参数起名那样为这类型参数起一个名字。这里选择了名称T。任何标识符抖可以作为类型参数名,选择T是因为 Rust 的类型命名规范是骆驼命名法(CamelCase)。另外泛型类型参数的规范也倾向于简短,经常仅仅是一个字母。T作为“type”的缩写是大部分 Rust 程序员的首选。

-

当需要再函数体中使用一个参数时,必须再函数签名中声明这个参数以便编译器能知道函数体中这个名称的意义。同理,当在函数签名中使用一个类型参数时,必须在使用它之前就声明它。类型参数声明位于函数名称与参数列表中间的尖括号中。

+

为了参数化要定义的函数的签名中的类型,我们需要像给函数的值参数起名那样为这类型参数起一个名字。这里选择了名称T。任何标识符都可以作为类型参数名,选择T是因为 Rust 的类型命名规范是骆驼命名法(CamelCase)。另外泛型类型参数的规范也倾向于简短,经常仅仅是一个字母。T作为“type”的缩写是大部分 Rust 程序员的首选。

+

当需要再函数体中使用一个参数时,必须在函数签名中声明这个参数以便编译器能知道函数体中这个名称的意义。同理,当在函数签名中使用一个类型参数时,必须在使用它之前就声明它。类型参数声明位于函数名称与参数列表中间的尖括号中。

我们将要定义的泛型版本的largest函数的签名看起来像这样:

fn largest<T>(list: &[T]) -> T {
 
@@ -185,7 +185,7 @@ fn main() {

Listing 10-6: A Point struct that holds x and y values of type T

其语法类似于函数定义中的泛型应用。首先,必须在结构体名称后面的尖括号中声明泛型参数的名称。接着在结构体定义中可以指定具体数据类型的位置使用泛型类型。

-

注意Point的定义中是使用了要给泛型类型,我们想要表达的是结构体Point对于一些类型T是泛型的,而且无论这个泛型是什么,字段xy都是相同类型的。如果尝试创建一个有不同类型值的Point的实例,像列表 10-7 中的代码就不能编译:

+

注意Point的定义中是使用了要给泛型类型,我们想要表达的是结构体Point对于一些类型T是泛型的,而且字段xy都是相同类型的,无论它具体是何类型。如果尝试创建一个有不同类型值的Point的实例,像列表 10-7 中的代码就不能编译:

Filename: src/main.rs

struct Point<T> {
     x: T,
@@ -233,7 +233,7 @@ fn main() {
     None,
 }
 
-

换句话说Option<T>是一个拥有泛型T的枚举。它有两个成员:Some,它存放了一个类型T的值,和不存在任何值的None。标准库中只有这一个定义来支持创建任何具体类型的枚举值。“一个可能的值”是一个比具体类型的值更抽象的概念,而 Rust 允许我们不引入重复就能表现抽象的概念。

+

换句话说Option<T>是一个拥有泛型T的枚举。它有两个成员:Some,它存放了一个类型T的值,和不存在任何值的None。标准库中只有这一个定义来支持创建任何具体类型的枚举值。“一个可能的值”是一个比具体类型的值更抽象的概念,而 Rust 允许我们不引入重复代码就能表现抽象的概念。

枚举也可以拥有多个泛型类型。第九章使用过的Result枚举定义就是一个这样的例子:

enum Result<T, E> {
     Ok(T),
@@ -241,7 +241,7 @@ fn main() {
 }
 

Result枚举有两个泛型类型,TEResult有两个成员:Ok,它存放一个类型T的值,而Err则存放一个类型E的值。这个定义使得Result枚举能很方便的表达任何可能成功(返回T类型的值)也可能失败(返回E类型的值)的操作。回忆一下列表 9-2 中打开一个文件的场景,当文件被成功打开T被放入了std::fs::File类型而当打开文件出现问题时E被放入了std::io::Error类型。

-

当发现代码中有多个只有存放的值的类型有所不同的结构体或枚举定义时,你就应该像之前的函数定义中那样引入泛型类型来减少重复。

+

当发现代码中有多个只有存放的值的类型有所不同的结构体或枚举定义时,你就应该像之前的函数定义中那样引入泛型类型来减少重复代码。

方法定义中的枚举数据类型

可以像第五章介绍的那样来为其定义中带有泛型的结构体或枚举实现方法。列表 10-9 中展示了列表 10-6 中定义的结构体Point<T>。接着我们在Point<T>上定义了一个叫做x的方法来返回字段x中数据的引用:

Filename: src/main.rs

diff --git a/docs/ch10-02-traits.html b/docs/ch10-02-traits.html index 363e960..73bfab8 100644 --- a/docs/ch10-02-traits.html +++ b/docs/ch10-02-traits.html @@ -137,7 +137,7 @@ the NewsArticle and Tweet types

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 所示:

+

注意因为列表 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;
 
@@ -218,14 +218,14 @@ println!("1 new tweet: {}", tweet.summary());
 

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

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

trait bounds

-

现在我们定义了 trait 并在类型上实现了这些 trait,也可以对泛型类型参数使用 trait。我们可以限制泛型不再适用于任何类型,编译器会确保其被限制为那么实现了特定 trait 的类型,由此泛型就会拥有我们希望其类型所拥有的功能。这被称为指定泛型的 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。如果我们需要能够在函数中使用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 {
 
diff --git a/docs/ch10-03-lifetime-syntax.html b/docs/ch10-03-lifetime-syntax.html index 4f36ba9..e439782 100644 --- a/docs/ch10-03-lifetime-syntax.html +++ b/docs/ch10-03-lifetime-syntax.html @@ -131,7 +131,7 @@ clear --> 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-18 中这个并没有产生悬垂引用且可以正常编译的例子:

{
     let x = 5;            // -----+-- 'b
@@ -211,7 +211,7 @@ compile

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

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

-

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

+

来看看我们编写的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() {
@@ -354,17 +354,17 @@ type are references

这里我们提到一些 Rust 的历史是因为更多的明确的模式将被合并和添加到编译器中是完全可能的。未来将会需要越来越少的生命周期注解。

被编码进 Rust 引用分析的模式被称为生命周期省略规则lifetime elision rules)。这并不是需要程序员遵守的规则;这些规则是一系列特定的场景,此时编译器会会考虑,如果代码符合这些场景,就不需要明确指定生命周期。

这些规则并不提供完整的推断:如果 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 foo<'a>(x: &'a i32),有两个引用参数的函数有两个不同的生命周期参数,fn foo<'a, 'b>(x: &'a i32, y: &'b i32),依此类推。

  2. -

    如果只有一个输入生命周期参数,而且它被赋予所有输出生命周期参数:fn foo<'a>(x: &'a i32) -> &'a i32

    +

    如果只有一个输入生命周期参数,那么它被赋给所有输出生命周期参数:fn foo<'a>(x: &'a i32) -> &'a i32

  3. -

    如果方法有多个输入生命周期参数,不过其中之一是&self&mut self,那么self的生命周期被赋予所有输出生命周期参数。这使得方法看起来更简洁。

    +

    如果方法有多个输入生命周期参数,不过其中之一因为方法的缘故是&self&mut self,那么self的生命周期被赋给所有输出生命周期参数。这使得方法写起来更简洁。

假设我们自己就是编译器并来计算列表 10-25 first_word函数的签名中的引用的生命周期。开始时签名中的引用并没有关联任何生命周期:

@@ -393,7 +393,7 @@ 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-10 中展示的泛型类型参数的语法:包括声明生命周期参数的位置和以及生命周期参数是否与结构体字段或方法的参数与返回值相关联。

+

当为带有生命周期的结构体实现方法时,其语法依然类似列表 10-10 中展示的泛型类型参数的语法:包括声明生命周期参数的位置和生命周期参数是否与结构体字段或方法的参数与返回值相关联。

(实现方法时)结构体字段的生命周期必须总是在impl关键字之后声明并在结构体名称之后被适用,因为这些生命周期是结构体类型的一部分。

impl块里的方法签名中,引用可能与结构体字段中的引用相关联,也可能是独立的。另外,生命周期省略规则也经常让我们无需在方法签名中使用生命周期注解。让我们看看一些使用列表 10-24 中定义的结构体ImportantExcerpt的例子。

首先,这里有一个方法level。其唯一的参数是self的引用,而且返回值只是一个i32,并不引用任何值:

@@ -446,7 +446,7 @@ fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann

这个是列表 10-21 中那个返回两个字符串 slice 中最长者的longest函数,不过带有一个额外的参数annann的类型是泛型T,它可以被放入任何实现了where从句中指定的Display trait 的类型。这个额外的参数会在函数比较字符串 slice 的长度之前被打印出来,这也就是为什么Display trait bound 是必须的。因为生命周期也是泛型,生命周期参数'a和泛型类型参数T都位于函数名后的同一尖括号列表中。

总结

-

这一章介绍了很多的内容!现在你知道了泛型类型参数、trait 和 trait bounds 以及 泛型生命周期类型,你已经准备编写既不重复又能适用于多种场景的代码了。泛型类型参数意味着代码可以适用于不同的类型。trait 和 trait bounds 保证了即使类型是泛型的,这些类型也会拥有所需要的行为。由生命周期注解所指定的引用生命周期之间的关系保证了这些灵活多变的代码不会出现悬垂引用。而所有的这一切,发生在运行时所以不会影响运行时效率!

+

这一章介绍了很多的内容!现在你知道了泛型类型参数、trait 和 trait bounds 以及 泛型生命周期类型,你已经准备编写既不重复又能适用于多种场景的代码了。泛型类型参数意味着代码可以适用于不同的类型。trait 和 trait bounds 保证了即使类型是泛型的,这些类型也会拥有所需要的行为。由生命周期注解所指定的引用生命周期之间的关系保证了这些灵活多变的代码不会出现悬垂引用。而所有的这一切发生在编译时所以不会影响运行时效率!

你可能不会相信,这个领域还有更多需要学习的内容:第十七章会讨论 trait 对象,这是另一种使用 trait 的方式。第十九章会涉及到生命周期注解更复杂的场景。第二十章讲解一些高级的类型系统功能。不过接下来,让我们聊聊如何在 Rust 中编写测试,来确保代码的所有功能能像我们希望的那样工作!

diff --git a/docs/ch11-03-test-organization.html b/docs/ch11-03-test-organization.html index a5391d2..b79ee0c 100644 --- a/docs/ch11-03-test-organization.html +++ b/docs/ch11-03-test-organization.html @@ -73,7 +73,7 @@
commit 55b294f20fc846a13a9be623bf322d8b364cee77

-

正如之前提到的,测试是一个很广泛的学科,而且不同的开发者也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:单元测试unit tests)与集成测试unit tests)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块。他们也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户使用相同的方式使用你的代码,他们只针对共有接口而且每个测试都会测试多个模块。

+

正如之前提到的,测试是一个很广泛的学科,而且不同的开发者也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:单元测试unit tests)与集成测试integration tests)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块。他们也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户使用相同的方式使用你的代码,他们只针对公有接口而且每个测试都会测试多个模块。

这两类测试对于从独立和整体的角度保证你的库符合期望是非常重要的。

单元测试

单元测试的目的是在与其他部分隔离的环境中测试每一个单元的代码,以便于快速而准确的定位何处的代码是否符合预期。单元测试位于 src 目录中,与他们要测试的代码存在于相同的文件中。传统做法是在每个文件中创建包含测试函数的tests模块,并使用cfg(test)标注模块。

diff --git a/docs/ch12-01-accepting-command-line-arguments.html b/docs/ch12-01-accepting-command-line-arguments.html index ae65a5a..65ed301 100644 --- a/docs/ch12-01-accepting-command-line-arguments.html +++ b/docs/ch12-01-accepting-command-line-arguments.html @@ -134,7 +134,7 @@ edited as such, can you check? --> 12-2. We're not "setting" arguments here, we're saving the value in variables. I've hopefully cleared this up without needing to introduce repetition. /Carol--> -

你可能注意到了 vector 的第一个值是"target/debug/greprs",它是二进制我呢见的名称。其原因超出了本章介绍的范围,不过需要记住的是我们保存了所需的两个参数。

+

你可能注意到了 vector 的第一个值是"target/debug/greprs",它是我们二进制文件的名称。其原因超出了本章介绍的范围,不过需要记住的是我们保存了所需的两个参数。

将参数值保存进变量

打印出参数 vector 中的值仅仅展示了可以访问程序中指定为命令行参数的值。但是这并不是我们想要做的,我们希望将这两个参数的值保存进变量这样就可以在程序使用这些值。让我们如列表 12-2 这样做:

-

注意:一些同学将这种当使用符合类型更为合适的时候使用基本类型的反模式称为基本类型偏执primitive obsession)。

+

注意:一些同学将这种拒绝使用相对而言更为合适的复合类型而使用基本类型的模式称为基本类型偏执primitive obsession)。

-

虽然很多人使用多态来描述继承,但是它实际上是一种特殊的多态,称为子类型多态。也有很多种其他形式,在Rust中带有通用的ttait绑定的一个参数 -也是多态——更特殊的类型多态。在多种类型的多态间的细节不是关键的,所以不要过于担心细节,只需要知道Rust有多种多态相关的特色就好,不像很多其他OOP语言。

+

虽然很多人使用“多态”来描述继承,但是它实际上是一种特殊的多态,称为“子类型多态”。也有很多种其他形式的多态,在 Rust 中带有泛型参数的 trait bound 也是多态,更具体的说是“参数多态”。不同类型多态的确切细节在这里并不关键,所以不要过于担心细节,只需要知道 Rust 有多种多态相关的特色就好,不同于很多其他 OOP 语言。

-

为了支持这种样式,Rust有trait对象,这样我们可以指定给任何类型的值,只要值实现了一种特定的trait。

-

继承最近在很多编程语言的设计方案中失宠了。使用继承类实现代码重用需要共享比你需要共享的代码。子类不应该经常共享它们的父类的所有特色,但是继承意味着子类得到了它的父类的数据和行为。这使得一个程序的设计不灵活,创建了无意义的子类的方法被调用的可能性或者由于方法不适用于子类但是必须从父类继承,从而触发错误。另外,很多语言只允许从一个类继承,更加限制了程序设计的灵活性。

-

因为这些原因,Rust选择了一个另外的途径,使用trait替代继承。让我们看一下在Rust中trait对象是如何实现多态的。

+ +

为了支持这种模式,Rust 有 trait 对象trait objects),这样我们可以指定给任何类型的值,只要值实现了一种特定的 trait。

+

继承最近在很多编程语言的设计方案中失宠了。使用继承来实现代码重用需要共享比你需要共享的代码。子类不应该总是共享它们的父类的所有特色,但是继承意味着子类得到了它父类所有的数据和行为。这使得程序的设计更加不灵活,并产生了无意义的方法调用或子类,或者由于方法并不适用于子类不过必需从父类继承而造成错误的可能性。另外,一些语言只允许子类继承一个父类,这进一步限制了程序设计的灵活性。

+

因为这些原因,Rust 选择了一个另外的途径,使用 trait 对象替代继承。让我们看一下在 Rust 中 trait 对象是如何实现多态的。

diff --git a/docs/ch17-02-trait-objects.html b/docs/ch17-02-trait-objects.html index e5b5bd4..8932ea5 100644 --- a/docs/ch17-02-trait-objects.html +++ b/docs/ch17-02-trait-objects.html @@ -67,17 +67,16 @@
-

为使用不同类型的值而设计的Trait对象

+

为使用不同类型的值而设计的 trait 对象

ch17-02-trait-objects.md
-commit 872dc793f7017f815fb1e5389200fd208e12792d

+commit 67876e3ef5323ce9d394f3ea6b08cb3d173d9ba9

-

在第8章,我们谈到了vector的局限是vectors只能存储同种类型的元素。我们在Listing 8-1有一个例子,其中定义了一个SpreadsheetCell 枚举类型,可以存储整形、浮点型和text,这样我们就可以在每个cell存储不同的数据类型了,同时还有一个代表一行cell的vector。当我们的代码编译的时候,如果交换地处理的各种东西是固定的类型是已知的,那么这是可行的。

-
<!-- The code example I want to reference did not have a listing number; it's
+

在第八章,我们谈到了 vector 的局限是 vector 只能存储同种类型的元素。在列表 8-1 中有一个例子,其中定义了一个有存放整型、浮点型和文本的成员的枚举类型SpreadsheetCell,这样就可以在每一个单元格储存不同类型的数据并使得 vector 仍让代表一行单元格。这在那类代码被编译时就知晓需要可交换处理的数据的类型是一个固定集合的情况下是可行的。

+

有时,我们想我们使用的类型集合是可扩展的,可以被使用我们的库的程序员扩展。比如很多图形化接口工具有一个条目列表,从这个列表迭代和调用draw方法在每个条目上。我们将要创建一个库crate,包含称为rust_gui的CUI库的结构体。我们的GUI库可以包含一些给开发者使用的类型,比如Button或者TextField。使用rust_gui的程序员会创建更多可以在屏幕绘图的类型:一个程序员可能会增加Image,另外一个可能会增加SelectBox。我们不会在本章节实现一个完善的GUI库,但是我们会展示如何把各部分组合在一起。

当要写一个rust_gui库时,我们不知道其他程序员要创建什么类型,所以我们无法定义一个enum来包含所有的类型。我们知道的是rust_gui需要有能力跟踪所有这些不同类型的大量的值,需要有能力在每个值上调用draw方法。我们的GUI库不需要确切地知道当调用draw方法时会发生什么,只要值有可用的方法供我们调用就可以。

在有继承的语言里,我们可能会定义一个名为Component的类,该类上有一个draw方法。其他的类比如ButtonImageSelectBox会从Component继承并继承draw方法。它们会各自覆写draw方法来自定义行为,但是框架会把所有的类型当作是Component的实例,并在它们上调用draw

diff --git a/docs/print.html b/docs/print.html index 24974f6..e7f3bbf 100644 --- a/docs/print.html +++ b/docs/print.html @@ -3239,7 +3239,7 @@ $ mv src/server.rs src/network │ ├── mod.rs │ └── server.rs
-

那么,当我们想要提取network::server模块时,为什么也必须将 src/network.rs 文件改名成 src/network/mod.rs 文件呢,还有为什么要将network::server的代码放入 network 目录的 src/network/server.rs 文件中,而不能将network::server模块提取到 src/server.rs 中呢?原因是如果 server.rs 文件在 src 目录中那么 Rust 就不能知道server应当是network的子模块。为了更清除的说明为什么 Rust 不知道,让我们考虑一下有着如下层级的另一个例子,它的所有定义都位于 src/lib.rs 中:

+

那么,当我们想要提取network::server模块时,为什么也必须将 src/network.rs 文件改名成 src/network/mod.rs 文件呢,还有为什么要将network::server的代码放入 network 目录的 src/network/server.rs 文件中,而不能将network::server模块提取到 src/server.rs 中呢?原因是如果 server.rs 文件在 src 目录中那么 Rust 就不能知道server应当是network的子模块。为了更清楚得说明为什么 Rust 不知道,让我们考虑一下有着如下层级的另一个例子,它的所有定义都位于 src/lib.rs 中:

communicator
  ├── client
  └── network
@@ -3410,7 +3410,7 @@ some of which are incorrect

try_me函数位于项目的根模块。叫做outermost的模块是私有的,不过第二条私有性规则说明try_me函数允许访问outermost模块,因为outermost位于当前(根)模块,try_me也是。

outermost::middle_function的调用是正确的。因为middle_function是公有的,而try_me通过其父模块访问middle_functionoutermost。根据上一段的规则我们可以确定这个模块是可访问的。

outermost::middle_secret_function的调用会造成一个编译错误。middle_secret_function是私有的,所以第二条(私有性)规则生效了。根模块既不是middle_secret_function的当前模块(outermost是),也不是middle_secret_function当前模块的子模块。

-

叫做inside的模块是私有的且没有子模块,所以它只能被当前模块访问,outermost。这意味着try_me函数不允许调用outermost::inside::inner_functionoutermost::inside::secret_function任何一个。

+

叫做inside的模块是私有的且没有子模块,所以它只能被当前模块outermost访问。这意味着try_me函数不允许调用outermost::inside::inner_functionoutermost::inside::secret_function任何一个。

修改错误

这里有一些尝试修复错误的代码修改意见。在你尝试他们之前,猜测一下他们哪个能修复错误,接着编译查看你是否猜对了,并结合私有性规则理解为什么。

    @@ -3419,7 +3419,7 @@ some of which are incorrect

  • 如果在inner_function函数体中调用::outermost::middle_secret_function()?(开头的两个冒号意味着从根模块开始引用模块。)

请随意设计更多的实验并尝试理解他们!

-

接下来,让我们讨论一下使用use关键字来将项引入作用域。

+

接下来,让我们讨论一下使用use关键字将模块项目引入作用域。

导入命名

ch07-03-importing-names-with-use.md @@ -3730,7 +3730,7 @@ let s = "initial contents".to_string();

也可以使用String::from函数来从字符串字面值创建String。如下等同于使用to_string

let s = String::from("initial contents");
 
-

因为字符串使用广泛,这里有很多不同的用于字符串的通用 API 可供选择。他们有些可能显得有些多于,不过都有其用武之地!在这个例子中,String::from.to_string最终做了完全相同的工作,所以如何选择就是风格问题了。

+

因为字符串使用广泛,这里有很多不同的用于字符串的通用 API 可供选择。他们有些可能显得有些多余,不过都有其用武之地!在这个例子中,String::from.to_string最终做了完全相同的工作,所以如何选择就是风格问题了。

记住字符串是 UTF-8 编码的,所以可以包含任何可以正确编码的数据:

let hello = "السلام عليكم";
 let hello = "Dobrý den";
@@ -3771,7 +3771,7 @@ let s3 = s1 + &s2; // Note that s1 has been moved here and can no longer be
 
fn add(self, s: &str) -> String {
 

这并不是标准库中实际的签名;那个add使用泛型定义。这里的签名使用具体类型代替了泛型,这也正是当使用String值调用这个方法会发生的。第十章会讨论泛型。这个签名提供了理解+运算那奇怪的部分的线索。

-

首先,s2使用了&,意味着我们使用第二个字符串的引用与第一个字符串相加。这是因为add函数的s参数:只能将&strString相加,不能将两个String值相加。不过等一下——正如add的第二个参数所指定的,&s2的类型是&String而不是&str。那么为什么代码还能编译呢?之所以能够在add调用中使用&s2是因为&String可以被强转coerced)成 &str——当add函数被调用时,Rust 使用了一个被成为解引用强制多态deref coercion)的技术,你可以将其理解为它把&s2变成了&s2[..]以供add函数使用。第十五章会更深入的讨论解引用强制多态。因为add没有获取参数的所有权,所以s2在这个操作后仍然是有效的String

+

首先,s2使用了&,意味着我们使用第二个字符串的引用与第一个字符串相加。这是因为add函数的s参数:只能将&strString相加,不能将两个String值相加。不过等一下——正如add的第二个参数所指定的,&s2的类型是&String而不是&str。那么为什么代码还能编译呢?之所以能够在add调用中使用&s2是因为&String可以被强转coerced)成 &str——当add函数被调用时,Rust 使用了一个被称为解引用强制多态deref coercion)的技术,你可以将其理解为它把&s2变成了&s2[..]以供add函数使用。第十五章会更深入的讨论解引用强制多态。因为add没有获取参数的所有权,所以s2在这个操作后仍然是有效的String

其次,可以发现签名中add获取了self的所有权,因为self没有使用&。这意味着上面例子中的s1的所有权将被移动到add调用中,之后就不再有效。所以虽然let s3 = s1 + &s2;看起来就像它会复制两个字符串并创建一个新的字符串,而实际上这个语句会获取s1的所有权,附加上从s2中拷贝的内容,并返回结果的所有权。换句话说,它看起来好像生成了很多拷贝不过实际上并没有:这个实现比拷贝要更高效。

如果想要级联多个字符串,+的行为就显得笨重了:

let s1 = String::from("tic");
@@ -4465,12 +4465,11 @@ of numbers

Listing 10-2: Code to find the largest number in two lists of numbers

-

虽然代码能够执行,但是重复的代码是冗余且已于出错的,并且意味着当更新逻辑时需要修改多处地方的代码。

+

虽然代码能够执行,但是重复的代码是冗余且容易出错的,并且意味着当更新逻辑时需要修改多处地方的代码。

-

为了消除重复,我们可以创建一层抽象,在这个例子中将表现为一个获取任意整型列表作为参数并对其进行处理的函数。这将增加代码的简洁性并让我们将表达和推导寻找列表中最大值的这个概念与使用这个概念的特定位置相互独。 -立。

+

为了消除重复,我们可以创建一层抽象,在这个例子中将表现为一个获取任意整型列表作为参数并对其进行处理的函数。这将增加代码的简洁性并让我们将表达和推导寻找列表中最大值的这个概念与使用这个概念的特定位置相互独立。

在列表 10-3 的程序中将寻找最大值的代码提取到了一个叫做largest的函数中。这个程序可以找出两个不同数字列表的最大值,不过列表 10-1 中的代码只存在于一个位置:

Filename: src/main.rs

fn largest(list: &[i32]) -> i32 {
@@ -4562,8 +4561,8 @@ fn main() {
 

Listing 10-4: Two functions that differ only in their names and the types in their signatures

这里largest_i32largest_char有着完全相同的函数体,所以能够将这两个函数变成一个来减少重复就太好了。所幸通过引入一个泛型参数就能实现!

-

为了参数化要定义的函数的签名中的类型,我们需要像给函数的值参数起名那样为这类型参数起一个名字。这里选择了名称T。任何标识符抖可以作为类型参数名,选择T是因为 Rust 的类型命名规范是骆驼命名法(CamelCase)。另外泛型类型参数的规范也倾向于简短,经常仅仅是一个字母。T作为“type”的缩写是大部分 Rust 程序员的首选。

-

当需要再函数体中使用一个参数时,必须再函数签名中声明这个参数以便编译器能知道函数体中这个名称的意义。同理,当在函数签名中使用一个类型参数时,必须在使用它之前就声明它。类型参数声明位于函数名称与参数列表中间的尖括号中。

+

为了参数化要定义的函数的签名中的类型,我们需要像给函数的值参数起名那样为这类型参数起一个名字。这里选择了名称T。任何标识符都可以作为类型参数名,选择T是因为 Rust 的类型命名规范是骆驼命名法(CamelCase)。另外泛型类型参数的规范也倾向于简短,经常仅仅是一个字母。T作为“type”的缩写是大部分 Rust 程序员的首选。

+

当需要再函数体中使用一个参数时,必须在函数签名中声明这个参数以便编译器能知道函数体中这个名称的意义。同理,当在函数签名中使用一个类型参数时,必须在使用它之前就声明它。类型参数声明位于函数名称与参数列表中间的尖括号中。

我们将要定义的泛型版本的largest函数的签名看起来像这样:

fn largest<T>(list: &[T]) -> T {
 
@@ -4628,7 +4627,7 @@ fn main() {

Listing 10-6: A Point struct that holds x and y values of type T

其语法类似于函数定义中的泛型应用。首先,必须在结构体名称后面的尖括号中声明泛型参数的名称。接着在结构体定义中可以指定具体数据类型的位置使用泛型类型。

-

注意Point的定义中是使用了要给泛型类型,我们想要表达的是结构体Point对于一些类型T是泛型的,而且无论这个泛型是什么,字段xy都是相同类型的。如果尝试创建一个有不同类型值的Point的实例,像列表 10-7 中的代码就不能编译:

+

注意Point的定义中是使用了要给泛型类型,我们想要表达的是结构体Point对于一些类型T是泛型的,而且字段xy都是相同类型的,无论它具体是何类型。如果尝试创建一个有不同类型值的Point的实例,像列表 10-7 中的代码就不能编译:

Filename: src/main.rs

struct Point<T> {
     x: T,
@@ -4676,7 +4675,7 @@ fn main() {
     None,
 }
 
-

换句话说Option<T>是一个拥有泛型T的枚举。它有两个成员:Some,它存放了一个类型T的值,和不存在任何值的None。标准库中只有这一个定义来支持创建任何具体类型的枚举值。“一个可能的值”是一个比具体类型的值更抽象的概念,而 Rust 允许我们不引入重复就能表现抽象的概念。

+

换句话说Option<T>是一个拥有泛型T的枚举。它有两个成员:Some,它存放了一个类型T的值,和不存在任何值的None。标准库中只有这一个定义来支持创建任何具体类型的枚举值。“一个可能的值”是一个比具体类型的值更抽象的概念,而 Rust 允许我们不引入重复代码就能表现抽象的概念。

枚举也可以拥有多个泛型类型。第九章使用过的Result枚举定义就是一个这样的例子:

enum Result<T, E> {
     Ok(T),
@@ -4684,7 +4683,7 @@ fn main() {
 }
 

Result枚举有两个泛型类型,TEResult有两个成员:Ok,它存放一个类型T的值,而Err则存放一个类型E的值。这个定义使得Result枚举能很方便的表达任何可能成功(返回T类型的值)也可能失败(返回E类型的值)的操作。回忆一下列表 9-2 中打开一个文件的场景,当文件被成功打开T被放入了std::fs::File类型而当打开文件出现问题时E被放入了std::io::Error类型。

-

当发现代码中有多个只有存放的值的类型有所不同的结构体或枚举定义时,你就应该像之前的函数定义中那样引入泛型类型来减少重复。

+

当发现代码中有多个只有存放的值的类型有所不同的结构体或枚举定义时,你就应该像之前的函数定义中那样引入泛型类型来减少重复代码。

方法定义中的枚举数据类型

可以像第五章介绍的那样来为其定义中带有泛型的结构体或枚举实现方法。列表 10-9 中展示了列表 10-6 中定义的结构体Point<T>。接着我们在Point<T>上定义了一个叫做x的方法来返回字段x中数据的引用:

Filename: src/main.rs

@@ -4835,7 +4834,7 @@ the NewsArticle and Tweet types

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 所示:

+

注意因为列表 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;
 
@@ -4916,14 +4915,14 @@ println!("1 new tweet: {}", tweet.summary());
 

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

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

trait bounds

-

现在我们定义了 trait 并在类型上实现了这些 trait,也可以对泛型类型参数使用 trait。我们可以限制泛型不再适用于任何类型,编译器会确保其被限制为那么实现了特定 trait 的类型,由此泛型就会拥有我们希望其类型所拥有的功能。这被称为指定泛型的 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。如果我们需要能够在函数中使用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 {
 
@@ -5063,7 +5062,7 @@ clear --> 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-18 中这个并没有产生悬垂引用且可以正常编译的例子:

{
     let x = 5;            // -----+-- 'b
@@ -5143,7 +5142,7 @@ compile

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

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

-

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

+

来看看我们编写的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() {
@@ -5286,17 +5285,17 @@ type are references

这里我们提到一些 Rust 的历史是因为更多的明确的模式将被合并和添加到编译器中是完全可能的。未来将会需要越来越少的生命周期注解。

被编码进 Rust 引用分析的模式被称为生命周期省略规则lifetime elision rules)。这并不是需要程序员遵守的规则;这些规则是一系列特定的场景,此时编译器会会考虑,如果代码符合这些场景,就不需要明确指定生命周期。

这些规则并不提供完整的推断:如果 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 foo<'a>(x: &'a i32),有两个引用参数的函数有两个不同的生命周期参数,fn foo<'a, 'b>(x: &'a i32, y: &'b i32),依此类推。

  2. -

    如果只有一个输入生命周期参数,而且它被赋予所有输出生命周期参数:fn foo<'a>(x: &'a i32) -> &'a i32

    +

    如果只有一个输入生命周期参数,那么它被赋给所有输出生命周期参数:fn foo<'a>(x: &'a i32) -> &'a i32

  3. -

    如果方法有多个输入生命周期参数,不过其中之一是&self&mut self,那么self的生命周期被赋予所有输出生命周期参数。这使得方法看起来更简洁。

    +

    如果方法有多个输入生命周期参数,不过其中之一因为方法的缘故是&self&mut self,那么self的生命周期被赋给所有输出生命周期参数。这使得方法写起来更简洁。

假设我们自己就是编译器并来计算列表 10-25 first_word函数的签名中的引用的生命周期。开始时签名中的引用并没有关联任何生命周期:

@@ -5325,7 +5324,7 @@ 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-10 中展示的泛型类型参数的语法:包括声明生命周期参数的位置和以及生命周期参数是否与结构体字段或方法的参数与返回值相关联。

+

当为带有生命周期的结构体实现方法时,其语法依然类似列表 10-10 中展示的泛型类型参数的语法:包括声明生命周期参数的位置和生命周期参数是否与结构体字段或方法的参数与返回值相关联。

(实现方法时)结构体字段的生命周期必须总是在impl关键字之后声明并在结构体名称之后被适用,因为这些生命周期是结构体类型的一部分。

impl块里的方法签名中,引用可能与结构体字段中的引用相关联,也可能是独立的。另外,生命周期省略规则也经常让我们无需在方法签名中使用生命周期注解。让我们看看一些使用列表 10-24 中定义的结构体ImportantExcerpt的例子。

首先,这里有一个方法level。其唯一的参数是self的引用,而且返回值只是一个i32,并不引用任何值:

@@ -5378,7 +5377,7 @@ fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann

这个是列表 10-21 中那个返回两个字符串 slice 中最长者的longest函数,不过带有一个额外的参数annann的类型是泛型T,它可以被放入任何实现了where从句中指定的Display trait 的类型。这个额外的参数会在函数比较字符串 slice 的长度之前被打印出来,这也就是为什么Display trait bound 是必须的。因为生命周期也是泛型,生命周期参数'a和泛型类型参数T都位于函数名后的同一尖括号列表中。

总结

-

这一章介绍了很多的内容!现在你知道了泛型类型参数、trait 和 trait bounds 以及 泛型生命周期类型,你已经准备编写既不重复又能适用于多种场景的代码了。泛型类型参数意味着代码可以适用于不同的类型。trait 和 trait bounds 保证了即使类型是泛型的,这些类型也会拥有所需要的行为。由生命周期注解所指定的引用生命周期之间的关系保证了这些灵活多变的代码不会出现悬垂引用。而所有的这一切,发生在运行时所以不会影响运行时效率!

+

这一章介绍了很多的内容!现在你知道了泛型类型参数、trait 和 trait bounds 以及 泛型生命周期类型,你已经准备编写既不重复又能适用于多种场景的代码了。泛型类型参数意味着代码可以适用于不同的类型。trait 和 trait bounds 保证了即使类型是泛型的,这些类型也会拥有所需要的行为。由生命周期注解所指定的引用生命周期之间的关系保证了这些灵活多变的代码不会出现悬垂引用。而所有的这一切发生在编译时所以不会影响运行时效率!

你可能不会相信,这个领域还有更多需要学习的内容:第十七章会讨论 trait 对象,这是另一种使用 trait 的方式。第十九章会涉及到生命周期注解更复杂的场景。第二十章讲解一些高级的类型系统功能。不过接下来,让我们聊聊如何在 Rust 中编写测试,来确保代码的所有功能能像我们希望的那样工作!

测试

@@ -6085,7 +6084,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
commit 55b294f20fc846a13a9be623bf322d8b364cee77

-

正如之前提到的,测试是一个很广泛的学科,而且不同的开发者也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:单元测试unit tests)与集成测试unit tests)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块。他们也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户使用相同的方式使用你的代码,他们只针对共有接口而且每个测试都会测试多个模块。

+

正如之前提到的,测试是一个很广泛的学科,而且不同的开发者也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:单元测试unit tests)与集成测试integration tests)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块。他们也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户使用相同的方式使用你的代码,他们只针对公有接口而且每个测试都会测试多个模块。

这两类测试对于从独立和整体的角度保证你的库符合期望是非常重要的。

单元测试

单元测试的目的是在与其他部分隔离的环境中测试每一个单元的代码,以便于快速而准确的定位何处的代码是否符合预期。单元测试位于 src 目录中,与他们要测试的代码存在于相同的文件中。传统做法是在每个文件中创建包含测试函数的tests模块,并使用cfg(test)标注模块。

@@ -6355,7 +6354,7 @@ edited as such, can you check? --> 12-2. We're not "setting" arguments here, we're saving the value in variables. I've hopefully cleared this up without needing to introduce repetition. /Carol--> -

你可能注意到了 vector 的第一个值是"target/debug/greprs",它是二进制我呢见的名称。其原因超出了本章介绍的范围,不过需要记住的是我们保存了所需的两个参数。

+

你可能注意到了 vector 的第一个值是"target/debug/greprs",它是我们二进制文件的名称。其原因超出了本章介绍的范围,不过需要记住的是我们保存了所需的两个参数。

将参数值保存进变量

打印出参数 vector 中的值仅仅展示了可以访问程序中指定为命令行参数的值。但是这并不是我们想要做的,我们希望将这两个参数的值保存进变量这样就可以在程序使用这些值。让我们如列表 12-2 这样做:

-

注意:一些同学将这种当使用符合类型更为合适的时候使用基本类型的反模式称为基本类型偏执primitive obsession)。

+

注意:一些同学将这种拒绝使用相对而言更为合适的复合类型而使用基本类型的模式称为基本类型偏执primitive obsession)。

-

虽然很多人使用多态来描述继承,但是它实际上是一种特殊的多态,称为子类型多态。也有很多种其他形式,在Rust中带有通用的ttait绑定的一个参数 -也是多态——更特殊的类型多态。在多种类型的多态间的细节不是关键的,所以不要过于担心细节,只需要知道Rust有多种多态相关的特色就好,不像很多其他OOP语言。

+

虽然很多人使用“多态”来描述继承,但是它实际上是一种特殊的多态,称为“子类型多态”。也有很多种其他形式的多态,在 Rust 中带有泛型参数的 trait bound 也是多态,更具体的说是“参数多态”。不同类型多态的确切细节在这里并不关键,所以不要过于担心细节,只需要知道 Rust 有多种多态相关的特色就好,不同于很多其他 OOP 语言。

-

为了支持这种样式,Rust有trait对象,这样我们可以指定给任何类型的值,只要值实现了一种特定的trait。

-

继承最近在很多编程语言的设计方案中失宠了。使用继承类实现代码重用需要共享比你需要共享的代码。子类不应该经常共享它们的父类的所有特色,但是继承意味着子类得到了它的父类的数据和行为。这使得一个程序的设计不灵活,创建了无意义的子类的方法被调用的可能性或者由于方法不适用于子类但是必须从父类继承,从而触发错误。另外,很多语言只允许从一个类继承,更加限制了程序设计的灵活性。

-

因为这些原因,Rust选择了一个另外的途径,使用trait替代继承。让我们看一下在Rust中trait对象是如何实现多态的。

-

为使用不同类型的值而设计的Trait对象

+ +

为了支持这种模式,Rust 有 trait 对象trait objects),这样我们可以指定给任何类型的值,只要值实现了一种特定的 trait。

+

继承最近在很多编程语言的设计方案中失宠了。使用继承来实现代码重用需要共享比你需要共享的代码。子类不应该总是共享它们的父类的所有特色,但是继承意味着子类得到了它父类所有的数据和行为。这使得程序的设计更加不灵活,并产生了无意义的方法调用或子类,或者由于方法并不适用于子类不过必需从父类继承而造成错误的可能性。另外,一些语言只允许子类继承一个父类,这进一步限制了程序设计的灵活性。

+

因为这些原因,Rust 选择了一个另外的途径,使用 trait 对象替代继承。让我们看一下在 Rust 中 trait 对象是如何实现多态的。

+

为使用不同类型的值而设计的 trait 对象

ch17-02-trait-objects.md
-commit 872dc793f7017f815fb1e5389200fd208e12792d

+commit 67876e3ef5323ce9d394f3ea6b08cb3d173d9ba9

-

在第8章,我们谈到了vector的局限是vectors只能存储同种类型的元素。我们在Listing 8-1有一个例子,其中定义了一个SpreadsheetCell 枚举类型,可以存储整形、浮点型和text,这样我们就可以在每个cell存储不同的数据类型了,同时还有一个代表一行cell的vector。当我们的代码编译的时候,如果交换地处理的各种东西是固定的类型是已知的,那么这是可行的。

-
<!-- The code example I want to reference did not have a listing number; it's
+

在第八章,我们谈到了 vector 的局限是 vector 只能存储同种类型的元素。在列表 8-1 中有一个例子,其中定义了一个有存放整型、浮点型和文本的成员的枚举类型SpreadsheetCell,这样就可以在每一个单元格储存不同类型的数据并使得 vector 仍让代表一行单元格。这在那类代码被编译时就知晓需要可交换处理的数据的类型是一个固定集合的情况下是可行的。

+

有时,我们想我们使用的类型集合是可扩展的,可以被使用我们的库的程序员扩展。比如很多图形化接口工具有一个条目列表,从这个列表迭代和调用draw方法在每个条目上。我们将要创建一个库crate,包含称为rust_gui的CUI库的结构体。我们的GUI库可以包含一些给开发者使用的类型,比如Button或者TextField。使用rust_gui的程序员会创建更多可以在屏幕绘图的类型:一个程序员可能会增加Image,另外一个可能会增加SelectBox。我们不会在本章节实现一个完善的GUI库,但是我们会展示如何把各部分组合在一起。

当要写一个rust_gui库时,我们不知道其他程序员要创建什么类型,所以我们无法定义一个enum来包含所有的类型。我们知道的是rust_gui需要有能力跟踪所有这些不同类型的大量的值,需要有能力在每个值上调用draw方法。我们的GUI库不需要确切地知道当调用draw方法时会发生什么,只要值有可用的方法供我们调用就可以。

在有继承的语言里,我们可能会定义一个名为Component的类,该类上有一个draw方法。其他的类比如ButtonImageSelectBox会从Component继承并继承draw方法。它们会各自覆写draw方法来自定义行为,但是框架会把所有的类型当作是Component的实例,并在它们上调用draw

diff --git a/src/ch17-00-oop.md b/src/ch17-00-oop.md index 00513c0..6927445 100644 --- a/src/ch17-00-oop.md +++ b/src/ch17-00-oop.md @@ -4,4 +4,4 @@ >
> commit 759801361bde74b47e81755fff545c66020e6e63 -面向对象编程是一种起源于20世纪60年代Simula的模式化编程的方式,然后在90年代在C++语言开始流行。为了描述OOP有很多种复杂的定义:在一些定义下,Rust是面向对象的;在其他定义下,Rust不是。在本章节中,我们会探索一些被普遍认为是面向对象的特性和这些特性是如何转换为Rust的方言的。 \ No newline at end of file +面向对象编程(Object-Oriented Programming)是一种起源于 20 世纪 60 年代的 Simula 编程语言的模式化编程方式,然后在 90 年代随着 C++ 语言开始流行。为了描述 OOP 有很多种复杂的定义:在一些定义下,Rust 是面向对象的;在其他定义下,Rust 不是。在本章节中,我们会探索一些被普遍认为是面向对象的特性和这些特性是如何转换为 Rust 方言的。 \ No newline at end of file diff --git a/src/ch17-01-what-is-oo.md b/src/ch17-01-what-is-oo.md index dac9067..a54e378 100644 --- a/src/ch17-01-what-is-oo.md +++ b/src/ch17-01-what-is-oo.md @@ -2,43 +2,44 @@ > [ch17-01-what-is-oo.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch17-01-what-is-oo.md) >
-> commit 46334522e22d6217b392451cff8b4feca2d69d79 +> commit 2a9b2a1b019ad6d4832ff3e56fbcba5be68b250e -关于一门语言是否需要是面向对象,在编程社区内并达成一致意见。Rust被很多不同的编程模式影响,我们探索了13章提到的函数式编程的特性。面向对象编程语言的一些特性往往是对象、封装和继承。我们看一下每个的含义和Rust是否支持它们。 +关于一门语言是否需要是面向对象,在编程社区内并未达成一致意见。Rust 被很多不同的编程模式影响,我们探索了十三章提到的函数式编程的特性。面向对象编程语言的一些特性往往是对象、封装和继承。我们看一下这每一个概念的含义以及 Rust 是否支持他们。 ## 对象包含数据和行为 `Design Patterns: Elements of Reusable Object-Oriented Software`这本书被俗称为`The Gang of Four book`,是面向对象编程模式的目录。它这样定义面向对象编程: -> 面向对象的程序是由对象组成的。一个对象包数据和操作这些数据的程序。程序通常被称为方法或操作。 - -在这个定一下,Rust是面向对象的:结构体和枚举包含数据和impl块提供了在结构体和枚举上的方法。虽然带有方法的结构体和枚举不称为对象,但是他们提供了和对象相同的功能,使用了` Gang of Four`定义的对象。 +> Object-oriented programs are made up of objects. An *object* packages both +> data and the procedures that operate on that data. The procedures are +> typically called *methods* or *operations*. +> +> 面向对象的程序是由对象组成的。一个**对象**包数据和操作这些数据的过程。这些过程通常被称为**方法**或**操作**。 +在这个定义下,Rust 是面向对象的:结构体和枚举包含数据而 impl 块提供了在结构体和枚举之上的方法。虽然带有方法的结构体和枚举并不被**称为**对象,但是他们提供了与对象相同的功能,参考 Gang of Four 所定义的对象。 ## 隐藏了实现细节的封装 -通常与面向对象编程相关的另一个方面是封装的思想:对象的实现细节不能被使用对象的代码获取到。唯一与对象交互的方式是通过对象提供的public API,使用对象的代码无法深入到对象内部并直接改变数据或者行为。封装使得改变和重构对象的内部,无需改变使用对象的代码。 +另一个通常与面向对象编程相关的方面是**封装**的思想:对象的实现细节不能被使用对象的代码获取到。唯一与对象交互的方式是通过对象提供的公有 API;使用对象的代码无法深入到对象内部并直接改变数据或者行为。封装使得改变和重构对象的内部时无需改变使用对象的代码。 -就像我们在第7张讨论的那样,我们可以使用pub关键字来决定模块、类型函数和方法是public的(默认情况下一切都是private)。比如,我们可以定义一个结构体`AveragedCollection `包含一个`i32`类型的vector。结构体也可以有一个字段,该字段保存了vector中所有值的平均值。这样,希望知道结构体中的vector的平均值的人可以随着获取到,而无需自己计算。`AveragedCollection` 会为我们缓存平均值结果。 Listing 17-1有`AveragedCollection` 结构体的定义。 +就像我们在第七章讨论的那样,可以使用`pub`关键字来决定模块、类型函数和方法是公有的,而默认情况下一切都是私有的。比如,我们可以定义一个包含一个`i32`类型的 vector 的结构体`AveragedCollection `。结构体也可以有一个字段,该字段保存了 vector 中所有值的平均值。这样,希望知道结构体中的 vector 的平均值的人可以随时获取它,而无需自己计算。`AveragedCollection`会为我们缓存平均值结果。列表 17-1 有`AveragedCollection`结构体的定义: -Filename: src/lib.rs +文件名: src/lib.rs -``` +```rust pub struct AveragedCollection { list: Vec, average: f64, } ``` -`AveragedCollection`结构体维护了一个Integer列表和集合中所有元素的平均值。 +列表 17-1: `AveragedCollection`结构体维护了一个整型列表和集合中所有元素的平均值。 +注意,结构体自身被标记为`pub`,这样其他代码可以使用这个结构体,但是在结构体内部的字段仍然是私有的。这是非常重要的,因为我们希望保证变量被增加到列表或者被从列表删除时,也会同时更新平均值。我们通过在结构体上实现`add`、`remove`和`average`方法来做到这一点,如列表 17-2 所示: -注意,结构体本身被标记为pub,这样其他代码可以使用这个结构体,但是在结构体内部的字段仍然是private。这是非常重要的,因为我们希望保证变量被增加到列表或者被从列表删除时,也会同时更新平均值。我们通过在结构体上实现add、remove和average方法来做到这一点( Listing 17-2:): +文件名: src/lib.rs -Filename: src/lib.rs - - -``` +```rust # pub struct AveragedCollection { # list: Vec, # average: f64, @@ -71,29 +72,32 @@ impl AveragedCollection { } ``` -Listing 17-2:在`AveragedCollection`结构体上实现了add、remove和average public方法 +列表 17-2: 在`AveragedCollection`结构体上实现了`add`、`remove`和`average`公有方法 -public方法`add`、`remove`和`average`是修改`AveragedCollection`实例的唯一方式。当使用add方法把一个元素加入到`list`或者使用`remove`方法来删除它,这些方法的实现同时会调用私有的`update_average`方法来更新`average`成员变量。因为`list`和`average`是私有的,没有其他方式来使得外部的代码直接向`list`增加或者删除元素,直接操作`list`可能会引发`average`字段不同步。`average`方法返回`average`字段的值,这指的外部的代码只能读取`average`而不能修改它。 +公有方法`add`、`remove`和`average`是修改`AveragedCollection`实例的唯一方式。当使用`add`方法把一个元素加入到`list`或者使用`remove`方法来删除它时,这些方法的实现同时会调用私有的`update_average`方法来更新`average`字段。因为`list`和`average`是私有的,没有其他方式来使得外部的代码直接向`list`增加或者删除元素,直接操作`list`可能会引发`average`字段不同步。`average`方法返回`average`字段的值,这使得外部的代码只能读取`average`而不能修改它。 -因为我们已经封装好了`AveragedCollection`的实现细节,所以我们也可以像使用`list`一样使用的一个不同的数据结构,比如用`HashSet`代替`Vec`。只要签名`add`、`remove`和`average`公有函数保持相同,使用`AveragedCollection`的代码无需改变。如果我们暴露`List`给外部代码时,未必都是这样,因为`HashSet`和`Vec`使用不同的函数增加元素,所以如果要想直接修改`list`的话,外部的代码可能还得修改。 +因为我们已经封装好了`AveragedCollection`的实现细节,将来可以轻松改变类似数据结构这些方面的内容。例如,可以使用`HashSet`代替`Vec`作为`list`字段的类型。只要`add`、`remove`和`average`公有函数的签名保持不变,使用`AveragedCollection`的代码就无需改变。如果将`List`暴露给外部代码时,未必都是这样,因为`HashSet`和`Vec`使用不同的方法增加或移除项,所以如果要想直接修改`list`的话,外部的代码可能不得不修改。 -如果封装是一个语言被认为是面向对象语言必要的方面的话,那么Rust满足要求。在代码中不同的部分使用或者不使用`pub`决定了实现细节的封装。 +如果封装是一个语言被认为是面向对象语言所必要的方面的话,那么 Rust 就满足这个要求。在代码中不同的部分使用或者不使用`pub`决定了实现细节的封装。 ## 作为类型系统的继承和作为代码共享的继承 -继承是一个很多编程语言都提供的机制,一个对象可以从另外一个对象的定义继承,这使得可以获得父对象的数据和行为,而不用重新定义。很多人定义面向对象语言时,认为继承是一个特色。 +**继承**(*Inheritance*)是一个很多编程语言都提供的机制,一个对象可以定义为继承另一个对象的定义,这使其可以获得父对象的数据和行为,而不用重新定义。一些人定义面向对象语言时,认为继承是一个特色。 -如果一个语言必须有继承才能被称为面向对象的语言,那么Rust就不是面向对象的。没有办法定义一个结构体继承自另外一个结构体,从而获得父结构体的成员和方法。然而,如果你过去常常在你的编程工具箱使用继承,依赖于你要使用继承的原因,在Rust中有其他的方式。 +如果一个语言必须有继承才能被称为面向对象的语言的话,那么 Rust 就不是面向对象的。无法定义一个结构体继承自另外一个结构体,从而获得父结构体的成员和方法。然而,如果你过去常常在你的编程工具箱使用继承,根据你希望使用继承的原因,Rust 提供了其他的解决方案。 -使用继承有两个主要的原因。第一个是为了重用代码:一旦一个特殊的行为从一个类型继承,继承可以在另外一个类型实现代码重用。Rust代码可以被共享通过使用默认的trait方法实现,可以在Listing 10-14看到,我们增加一个`summary`方法到`Summarizable`trait。任何继承了`Summarizable`trait的类型上会有`summary`方法,而无需任何的父代码。这类似于父类有一个继承的方法,一个从父类继承的子类也因为继承有了继承的方法。当实现`Summarizable`trait时,我们也可以选择覆写默认的`summary`方法,这类似于子类覆写了从父类继承的实现方法。 +使用继承有两个主要的原因。第一个是为了重用代码:一旦为一个类型实现了特定行为,继承可以对一个不同的类型重用这个实现。相反 Rust 代码可以使用默认 trait 方法实现来进行共享,在列表 10-14 中我们见过在`Summarizable` trait 上增加的`summary`方法的默认实现。任何实现了`Summarizable` trait 的类型都可以使用`summary`方法而无须进一步实现。这类似于父类有一个方法的实现,而通过继承子类也拥有这个方法的实现。当实现`Summarizable` trait 时也可以选择覆盖`summary`的默认实现,这类似于子类覆盖从父类继承的方法实现。 -第二个使用继承的原因是,使用类型系统:子类型可以在父类型被使用的地方使用。这也称为多态,意味着如果多种对象有一个相同的shape,它们可以被其他替代。 +第二个使用继承的原因与类型系统有关:用来表现子类型可以在父类型被使用的地方使用。这也被称为**多态**(*polymorphism*),意味着如果多种对象有一个相同的形态大小,它们可以替代使用。 ->虽然很多人使用多态来描述继承,但是它实际上是一种特殊的多态,称为子类型多态。也有很多种其他形式,在Rust中带有通用的ttait绑定的一个参数 ->也是多态——更特殊的类型多态。在多种类型的多态间的细节不是关键的,所以不要过于担心细节,只需要知道Rust有多种多态相关的特色就好,不像很多其他OOP语言。 + -为了支持这种样式,Rust有trait对象,这样我们可以指定给任何类型的值,只要值实现了一种特定的trait。 +>虽然很多人使用“多态”来描述继承,但是它实际上是一种特殊的多态,称为“子类型多态”。也有很多种其他形式的多态,在 Rust 中带有泛型参数的 trait bound 也是多态,更具体的说是“参数多态”。不同类型多态的确切细节在这里并不关键,所以不要过于担心细节,只需要知道 Rust 有多种多态相关的特色就好,不同于很多其他 OOP 语言。 -继承最近在很多编程语言的设计方案中失宠了。使用继承类实现代码重用需要共享比你需要共享的代码。子类不应该经常共享它们的父类的所有特色,但是继承意味着子类得到了它的父类的数据和行为。这使得一个程序的设计不灵活,创建了无意义的子类的方法被调用的可能性或者由于方法不适用于子类但是必须从父类继承,从而触发错误。另外,很多语言只允许从一个类继承,更加限制了程序设计的灵活性。 + -因为这些原因,Rust选择了一个另外的途径,使用trait替代继承。让我们看一下在Rust中trait对象是如何实现多态的。 +为了支持这种模式,Rust 有 **trait 对象**(*trait objects*),这样我们可以指定给任何类型的值,只要值实现了一种特定的 trait。 + +继承最近在很多编程语言的设计方案中失宠了。使用继承来实现代码重用需要共享比你需要共享的代码。子类不应该总是共享它们的父类的所有特色,但是继承意味着子类得到了它父类所有的数据和行为。这使得程序的设计更加不灵活,并产生了无意义的方法调用或子类,或者由于方法并不适用于子类不过必需从父类继承而造成错误的可能性。另外,一些语言只允许子类继承一个父类,这进一步限制了程序设计的灵活性。 + +因为这些原因,Rust 选择了一个另外的途径,使用 trait 对象替代继承。让我们看一下在 Rust 中 trait 对象是如何实现多态的。 diff --git a/src/ch17-02-trait-objects.md b/src/ch17-02-trait-objects.md index 97e8949..6368c27 100644 --- a/src/ch17-02-trait-objects.md +++ b/src/ch17-02-trait-objects.md @@ -1,16 +1,14 @@ -## 为使用不同类型的值而设计的Trait对象 +## 为使用不同类型的值而设计的 trait 对象 > [ch17-02-trait-objects.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch17-02-trait-objects.md) >
-> commit 872dc793f7017f815fb1e5389200fd208e12792d +> commit 67876e3ef5323ce9d394f3ea6b08cb3d173d9ba9 - 在第8章,我们谈到了vector的局限是vectors只能存储同种类型的元素。我们在Listing 8-1有一个例子,其中定义了一个`SpreadsheetCell` 枚举类型,可以存储整形、浮点型和text,这样我们就可以在每个cell存储不同的数据类型了,同时还有一个代表一行cell的vector。当我们的代码编译的时候,如果交换地处理的各种东西是固定的类型是已知的,那么这是可行的。 + 在第八章,我们谈到了 vector 的局限是 vector 只能存储同种类型的元素。在列表 8-1 中有一个例子,其中定义了一个有存放整型、浮点型和文本的成员的枚举类型`SpreadsheetCell`,这样就可以在每一个单元格储存不同类型的数据并使得 vector 仍让代表一行单元格。这在那类代码被编译时就知晓需要可交换处理的数据的类型是一个固定集合的情况下是可行的。 -``` -``` 有时,我们想我们使用的类型集合是可扩展的,可以被使用我们的库的程序员扩展。比如很多图形化接口工具有一个条目列表,从这个列表迭代和调用draw方法在每个条目上。我们将要创建一个库crate,包含称为`rust_gui`的CUI库的结构体。我们的GUI库可以包含一些给开发者使用的类型,比如`Button`或者`TextField`。使用`rust_gui`的程序员会创建更多可以在屏幕绘图的类型:一个程序员可能会增加`Image`,另外一个可能会增加`SelectBox`。我们不会在本章节实现一个完善的GUI库,但是我们会展示如何把各部分组合在一起。