diff --git a/docs/ch01-00-introduction.html b/docs/ch01-00-introduction.html index 8329572..b817f27 100644 --- a/docs/ch01-00-introduction.html +++ b/docs/ch01-00-introduction.html @@ -73,14 +73,14 @@
commit 62f78bb3f7c222b574ff547d0161c2533691f9b4

-

欢迎阅读“Rust 程序设计语言”,一本关于 Rust 的介绍性书籍。Rust 是一个着用于安全、速度和并发的编程语言。它的设计不仅可以使程序获得性能和对底层语言的控制,并且能够享受高级语言强大的抽象能力。这些特性使得 Rust 适合那些有类似 C 语言经验并正在寻找一个更安全的替代者的程序员,同时也适合那些来自类似 Python 语言背景,正在探索在不牺牲表现力的情况下编写更好性能代码的开发者。

-

Rust 在编译时进行其绝大多数的安全检查和内存管理决策,因此程序的运行时性能没有受到影响。这让其在许多其他语言不擅长的应用场景中得以大显身手:存在可预测空间和时间要求的程序,嵌入到其他语言中,以及编写底层代码,如设备驱动和操作系统。Rust 也很擅长 web 程序:它驱动着 Rust 包注册网站(package -registry site),crates.io!我们期待看到使用 Rust 进行创作。

-

本书是为已经至少了解一门编程语言的读者而写的。读完本书之后,你应该能自如的编写 Rust 程序。我们将通过小而集中并且相互依赖的例子来学习 Rust,并向你展示如何使用 Rust 多样的功能,同时了解它们在幕后是如何执行的。

+

欢迎阅读“Rust 程序设计语言”,一本介绍 Rust 的书。Rust 是一门着眼于安全、速度和并发的编程语言。它的设计兼顾性能与底层控制,以及高级语言强大的抽象能力。适合那些有类 C 语言经验,正在寻找更安全的替代品的开发者;以及有着类 Python 语言背景,寻求在不牺牲表现力的前提下,编写性能更好的代码的开发者。

+

Rust 主要在编译时执行安全检查和内存管理决策,对运行时性能的影响微不足道。这使其在许多语言不擅长的应用场景中得以大显身手:空间和时间需求可预测的程序,嵌入到其他语言中,以及编写底层代码,如设备驱动和操作系统。Rust 也很擅长 web 程序:它驱动着 Rust 包注册网站(package +registry site),crates.io!我们期待使用 Rust 进行创作。

+

本书的目标读者至少应了解一门其它编程语言。读完本书之后,你应该能自如的编写 Rust 程序。我们将通过短小精干、前后呼应的例子来学习 Rust,并展示其多样功能的使用方法,同时了解幕后如何运行。

为本书做出贡献

-

本书是开源的。如果你发现任何错误,请不要犹豫,在 GitHub 上发起 issue 或提交 pull request。请查看CONTRIBUTING.md获取更多信息。

+

本书是开源的。如果你发现任何错误,不要犹豫,在 GitHub 上发起 issue 或提交 pull request。请查看 CONTRIBUTING.md 获取更多信息。

-

译者注:这是本译本的 GitHub 仓库,同样欢迎 Issue 和 PR :)

+

译者注:译本的 GitHub 仓库,同样欢迎 Issue 和 PR :)

diff --git a/docs/ch01-01-installation.html b/docs/ch01-01-installation.html index 76cd7df..ef3d7e0 100644 --- a/docs/ch01-01-installation.html +++ b/docs/ch01-01-installation.html @@ -73,43 +73,43 @@
commit c1b95a18dbcbb06aadf07c03759f27d88ccf62cf

-

使用 Rust 的第一步是安装。你需要网络连接来执行本章的命令,因为我们要从网上下载 Rust。

-

我们将会展示很多使用终端的命令,并且这些代码都以$开头。并不需要真正输入$,它们在这里代表每行指令的开头。在网上会看到很多使用这个惯例的教程和例子:$代表以常规用户运行命令,#代表需要用管理员运行的命令。没有以$(或#)的行通常是之前命令的输出。

+

第一步是安装 Rust。你需要网络连接来执行本章的命令,因为我们要从网上下载 Rust。

+

我们将会展示很多在终端中输入的命令,这些命令均以 $ 开头。你不需要真的输入$,在这里它代表每行命令的起始。网上有很多教程和例子遵循这种惯例:$ 代表以常规用户身份运行命令,# 代表需要用管理员身份运行命令。没有以 $(或 #)起始的行通常是之前命令的输出。

在 Linux 或 Mac 上安装

-

如果你使用 Linux 或 Mac,所有需要做的就是打开一个终端并输入:

+

如果你使用 Linux 或 Mac,你需要做的全部,就是打开一个终端并输入:

$ curl https://sh.rustup.rs -sSf | sh
 
-

这会下载一个脚本并开始安装。你可能被提示要输入密码。如果一切顺利,将会出现如下内容:

+

这会下载一个脚本并开始安装。可能会提示你输入密码,如果一切顺利,将会出现如下内容:

Rust is installed now. Great!
 
-

当然,如果你不赞成curl | sh这种模式,可以随意下载、检查和运行这个脚本。

+

当然,如果你对于 curl | sh 心有疑虑,你可以随意下载、检查和运行这个脚本。

在 Windows 上安装

-

在 Windows 上,前往https://rustup.rs并按照说明下载 rustup-init.exe。运行并遵循其提供的其余指示操作。

-

本书其余 Windows 相关的命令假设你使用cmd作为你的 shell。如果你使用不同的 shell,可能能够执行 Linux 和 Mac 用户相同的命令。如果都不行,请查看所使用的 shell 的文档。

+

如果你使用 Windows,前往 https://rustup.rs,按说明下载 rustup-init.exe,运行并照其指示操作。

+

本书中其余 Windows 相关的命令,假设你使用 cmd 作为 shell。如果你使用其它 shell,也许可以执行与 Linux 和 Mac 用户相同的命令。如果不行,请查看该 shell 的文档。

自定义安装

-

如果有理由倾向于不使用 rustup.rs,请查看Rust 安装页面获取其他选择。

+

无论出于何种理由,如果不愿意使用 rustup.rs,请查看 Rust 安装页面 获取其他选择。

更新

-

一旦安装完 Rust,更新到最新版本是简单的。在 shell 中运行更新脚本:

+

一旦 Rust 安装完,更新到最新版本很简单。在 shell 中执行:

$ rustup update
 

卸载

-

卸载 Rust 同安装一样简单。在 shell 中运行卸载脚本

+

卸载 Rust 同样简单。在 shell 中执行:

$ rustup self uninstall
 

故障排除

-

安装完 Rust 后,打开 shell,输入:

+

安装完 Rust 后,在 shell 中执行:

$ rustc --version
 
-

应该能看到类似这样的版本号、提交 hash 和提交日期,对应你安装时的最新稳定版本:

+

应该能看到类似这样的版本号、提交哈希和提交日期,对应安装时的最新稳定版:

rustc x.y.z (abcabcabc yyyy-mm-dd)
 
-

如果出现这些内容,Rust 就安装成功了!

+

出现这些内容,Rust 就安装成功了!

恭喜入坑!(此处应该有掌声!)

-

如果有问题并且你在使用 Windows,检查 Rust(rustc,cargo 等)是否位于%PATH%系统变量中。

-

如果还是不能运行,有许多可以获取帮助的地方。最简单的是 irc.mozilla.org 上的 #rust IRC 频道 ,可以使用 Mibbit 来访问它。访问这些地址然后就可以和其他能提供帮助的 Rustacean(我们这些人自嘲的绰号)聊天了。其它给力的资源包括用户论坛Stack Overflow

+

如果在 Windows 中使用出现问题,检查 Rust(rustc,cargo 等)是否在 %PATH% 环境变量所包含的路径中。

+

如果还是不能解决,有许多地方可以求助。最简单的是 irc.mozilla.org 上的 #rust IRC 频道 ,可以使用 Mibbit 来访问它。然后就能和其他 Rustacean(Rust 用户的称号,有自嘲意味)聊天并寻求帮助。其它给力的资源包括用户论坛Stack Overflow

本地文档

-

安装程序也包含一份本地文档的拷贝,你可以离线阅读它们。输入rustup doc将在浏览器中打开本地文档。

-

任何你不太确认标准库中提供的类型或函数是干什么的时候,请查看 API 文档!

+

安装程序自带本地文档,可以离线阅读。输入 rustup doc 可以在浏览器中查看。

+

任何时候,如果你拿不准标准库中类型或函数,请查看 API 文档!

diff --git a/docs/ch01-02-hello-world.html b/docs/ch01-02-hello-world.html index 5e55907..5126eed 100644 --- a/docs/ch01-02-hello-world.html +++ b/docs/ch01-02-hello-world.html @@ -73,12 +73,12 @@
commit 4f2dc564851dc04b271a2260c834643dfd86c724

-

现在已经安装好了 Rust,让我们来编写第一个 Rust 程序。当学习一门新语言的时候,编写一个在屏幕上打印 “Hello, world!” 文本的小程序是一个传统,而在这一部分将遵循这个传统。

+

Rust 已安好,让我们来编写第一个程序。当学习一门新语言的时候,使用该语言在屏幕上打印 “Hello, world!” 是一项传统,我们将遵循这个传统。

-

注意:本书假设你熟悉基本的命令行操作。Rust 本身并不对你的编辑器,工具和你的代码存放在何处有什么特定的要求,所以如果你比起命令行更喜欢 IDE,请随意选择你喜欢的 IDE。

+

注意:本书假设你熟悉基本的命令行操作。对于你的编辑器、工具,以及你的代码存在何处,Rust 并没有特殊要求,如果你更喜欢 IDE,请随意。

创建项目文件夹

-

首先,创建一个文件夹来存放 Rust 代码。Rust 并不关心你的代码存放在哪里,不过在本书中,我们建议在你的 home 目录创建一个 projects 目录,并把你的所有项目放在这。打开一个终端并输入如下命令来为这个项目创建一个文件夹:

+

首先,创建一个存放代码的文件夹。Rust 并不关心它的位置,不过在本书中,我们建议你在 home 目录中创建一个 projects 目录,并把你的所有项目放在这。打开一个终端,输入如下命令来创建一个文件夹:

Linux 和 Mac:

$ mkdir ~/projects
 $ cd ~/projects
@@ -92,8 +92,8 @@ $ cd hello_world
 > cd hello_world
 

编写并运行 Rust 程序

-

接下来,新建一个叫做 main.rs 的源文件。Rust 文件总是以 .rs 后缀结尾。如果文件名多于一个单词,使用下划线分隔它们。例如,使用 my_program.rs 而不是 myprogram.rs

-

现在打开刚创建的 main.rs 文件,并输入如下代码:

+

接下来,新建一个叫做 main.rs 的文件。Rust 源代码总是以 .rs 后缀结尾。如果文件名包含多个单词,使用下划线分隔它们。例如 my_program.rs,而不是 myprogram.rs

+

现在打开刚创建的 main.rs 文件,输入如下代码:

Filename: main.rs

fn main() {
     println!("Hello, world!");
@@ -104,28 +104,28 @@ $ cd hello_world
 $ ./main
 Hello, world!
 
-

在 Windows 上,运行.\main.exe而不是./main。不管使用何种系统,你应该在终端看到Hello, world!字符串。如果你做到了,那么恭喜你!你已经正式编写了一个 Rust 程序。你是一名 Rust 程序员了!欢迎入坑。

+

在 Windows 上,运行 .\main.exe,而不是./main。不管使用何种系统,你应该在终端看到 Hello, world! 字样。如果你做到了,恭喜你!你已经正式编写了一个 Rust 程序,成为一名 Rust 程序员!

分析 Rust 程序

-

现在,让我们回过头来仔细看看“Hello, world!”程序到底发生了什么。这里是谜题的第一片:

+

现在,让我们回过头来,仔细看看“Hello, world!”程序到底发生了什么。这是拼图的第一片:

fn main() {
 
 }
 
-

这几行定义了一个 Rust 函数main函数是特殊的:这是每一个可执行的 Rust 程序首先运行的函数(译者注:入口点)。第一行表示“定义一个叫 main 的函数,没有参数也没有返回值。”如果有参数的话,它们应该出现在括号中,()

-

同时注意函数体被包裹在大括号中,{}。Rust 要求所有函数体都位于大括号中(译者注:对比有些语言特定情况可以省略大括号)。将前一个大括号与函数声明置于一行,并留有一个空格被认为是一个好的代码风格。

-

main()函数中:

+

这几行定义了一个 Rust 函数。一个叫 main 的函数,没有参数也没有返回值。如果有参数的话,它们应该出现在括弧中,()之间。main 函数是特殊的:它是每一个可执行的 Rust 程序的入口点。

+

还须注意函数体被包裹在花括号中,{} 之间。所有函数体都要用花括号包裹起来(译者注:有些语言,当函数体只有一行时可以省略花括号,但 Rust 中是不行的)。一般来说,将左花括号与函数声明置于一行,并以空格分隔,是良好的代码风格。

+

main() 函数中:

    println!("Hello, world!");
 
-

这行代码做了这个小程序的所有工作:它在屏幕上打印文本。这里有很多需要注意的细节。第一个是 Rust 代码风格使用 4 个空格缩进,而不是 1 个制表符(tab)。

-

第二个重要的部分是println!()。这叫做 Rust ,是如何进行 Rust 元编程(metaprogramming)的关键所在。相反如果是调用一个函数的话,它应该看起来像这样:println(没有!)。我们将在 21 章 E 小节中更加详细的讨论 Rust 宏,不过现在你只需记住当看到符号!的时候,就代表在调用一个宏而不是一个普通的函数。

+

一行代码完成这个小程序的所有工作:在屏幕上打印文本。这里有很多细节需要注意。首先 Rust 使用 4 个空格的缩进风格,而不是 1 个制表符(tab)。

+

第二个重要的部分是println!()。这是 ,Rust 元编程(metaprogramming)的关键所在。而调用一个函数,则要像这样:println(没有!)。我们将在 21 章 E 小节中更加详细的讨论宏,现在你只需记住,当看到符号 ! 的时候,调用的是宏而不是普通函数。

接下来,"Hello, world!" 是一个 字符串。我们把这个字符串作为一个参数传递给println!,它负责在屏幕上打印这个字符串。轻松加愉快!(⊙o⊙)

-

这一行以一个分号结尾(;)。;代表这个表达式的结束和下一个表达式的开始。大部分 Rust 代码行以;结尾。

+

该行以分号结尾(;)。; 代表一个表达式的结束和下一个表达式的开始。大部分 Rust 代码行以 ; 结尾。

编译和运行是两个步骤

-

在“编写并运行 Rust 程序”部分,展示了如何运行一个新创建的程序。现在我们将拆分并检查每一步操作。

-

在运行一个 Rust 程序之前,必须先编译它。可以输入rustc命令来使用 Rust 编译器并像这样传递源文件的名字:

+

“编写并运行 Rust 程序”部分,展示了如何创建运行程序。现在我们将拆分并检查每一步操作。

+

运行一个 Rust 程序之前,必须先编译它。可以通过 rustc 命令来使用 Rust 编译器,并传递源文件的名字给它,如下:

$ rustc main.rs
 
-

如果你来自 C 或 C++ 背景,就会发现这与gccclang类似。编译成功后,Rust 应该会输出一个二进制可执行文件,在 Linux 或 OSX 上在 shell 中你可以通过ls命令看到如下:

+

如果你有 C 或 C++ 背景,就会发现这与 gccclang 类似。编译成功后,Rust 应该会输出一个二进制可执行文件,在 Linux 或 OSX 上在 shell 中你可以通过ls命令看到如下:

$ ls
 main  main.rs
 
@@ -134,21 +134,21 @@ main main.rs main.exe main.rs -

这表示我们有两个文件:.rs 后缀的源文件,和可执行文件(在 Windows下是 main.exe,其它平台是 main)。这里剩下的操作就只有运行 mainmain.exe 文件了,像这样:

+

这表示我们有两个文件:.rs 后缀的源文件,和可执行文件(在 Windows下是 main.exe,其它平台是 main)。然后运行 mainmain.exe 文件,像这样:

$ ./main  # or .\main.exe on Windows
 

如果 main.rs 是我们的“Hello, world!”程序,它将会在终端上打印Hello, world!

-

来自 Ruby、Python 或 JavaScript 这样的动态类型语言背景的同学,可能不太习惯在分开的步骤编译和执行程序。Rust 是一种 静态提前编译语言ahead-of-time compiled language),这意味着可以编译好程序后,把它给任何人,他们都不需要安装 Rust 就可运行。如果你给他们一个 .rb.py.js 文件,他们需要先分别安装 Ruby,Python,JavaScript 实现(运行时环境,VM),不过你只需要一句命令就可以编译和执行程序。这一切都是语言设计的权衡取舍。

-

仅仅使用rustc编译简单程序是没问题的,不过随着项目的增长,你将想要能够控制你项目拥有的所有选项,并使其易于分享你的代码给别人或别的项目。接下来,我们将介绍一个叫做 Cargo 的工具,它将帮助你编写现实生活中的 Rust 程序。

+

来自 Ruby、Python 或 JavaScript 这样的动态类型语言背景的同学,可能不太习惯将编译和执行分为两个步骤。Rust 是一种 预编译静态类型语言ahead-of-time compiled language),这意味着编译好程序后,把它给任何人,他们不需要安装 Rust 就可运行。如果你给他们一个 .rb.py.js 文件,他们需要先分别安装 Ruby,Python,JavaScript 实现(运行时环境,VM),不过你只需要一句命令就可以编译和执行程序。这一切都是语言设计上的权衡取舍。

+

使用 rustc 编译简单程序是没问题的,不过随着项目的增长,你可能需要控制你项目的方方面面,并且更容易地将代码分享给其它人或项目。所以接下来,我们要介绍一个叫做 Cargo 的工具,它会帮助你编写真实世界中的 Rust 程序。

Hello, Cargo!

-

Cargo 是 Rust 的构建系统和包管理工具,同时 Rustacean 们使用 Cargo 来管理他们的 Rust 项目,因为它使得很多任务变得更轻松。例如,Cargo 负责构建代码、下载代码依赖的库并编译这些库。我们把代码需要的库叫做 依赖dependencies)。

-

最简单的 Rust 程序,例如我们刚刚编写的,并没有任何依赖,所以目前我们只使用了 Cargo 负责构建代码的那一部分。随着编写更加复杂的 Rust 程序,你会想要添加依赖,如果你使用 Cargo 开始的话,这将会变得简单许多。

-

由于绝大部分 Rust 项目使用 Cargo,本书接下来的部分将假设你使用它。如果使用安装章节介绍的官方安装包的话,Rust 自带 Cargo。如果通过其他方式安装 Rust 的话,可以在终端输入如下命令检查是否安装了 Cargo:

+

Cargo 是 Rust 的构建系统和包管理工具,同时 Rustacean 们使用 Cargo 来管理他们的 Rust 项目,它使得很多任务变得更轻松。例如,Cargo 负责构建代码、下载依赖库并编译。我们把代码需要的库叫做 依赖dependencies)。

+

最简单的 Rust 程序,比如我们刚刚编写的,并没有任何依赖,所以我们只使用了 Cargo 构建代码的功能。随着更复杂程序的编写,你会想要添加依赖,如果你使用 Cargo 开始的话,这将会变得简单许多。

+

由于绝大部分 Rust 项目使用 Cargo,本书接下来的部分将假设你使用它。如果使用之前介绍的官方安装包的话,它自带 Cargo。如果通过其他方式安装的话,可以在终端输入如下命令,检查是否安装了 Cargo:

$ cargo --version
 
-

如果出现了版本号,一切 OK!如果出现一个类似“command not found”的错误,那么你应该查看安装方式的文档来确定如何单独安装 Cargo。

+

如果出现了版本号,一切 OK!如果出现类似“command not found”的错误,你应该查看安装文档以确定如何单独安装 Cargo。

使用 Cargo 创建项目

-

让我们使用 Cargo 来创建一个新项目并看看与上面的hello_world项目有什么不同。回到 projects 目录(或者任何你决定放置代码的目录):

+

让我们使用 Cargo 来创建一个新项目,然后看看与上面的hello_world项目有什么不同。回到 projects 目录(或者任何你放置代码的目录):

Linux 和 Mac:

$ cd ~/projects
 
@@ -159,9 +159,9 @@ main.rs
$ cargo new hello_cargo --bin
 $ cd hello_cargo
 
-

我们向cargo new传递了--bin因为我们的目标是生成一个可执行程序,而不是一个库。可执行文件是二进制可执行文件,通常就叫做 二进制文件binaries)。项目的名称被定为hello_cargo,同时 Cargo 在一个同名目录中创建它的文件,接着我们可以进入查看。

-

如果列出 hello_cargo 目录中的文件,将会看到 Cargo 生成了一个文件和一个目录:一个 Cargo.toml 文件和一个 src 目录,main.rs 文件位于目录中。它也在 hello_cargo 目录初始化了一个 git 仓库,以及一个 .gitignore 文件;你可以改为使用不同的版本控制系统(VCS),或者不使用 VCS,通过--vcs参数。

-

使用你选择的文本编辑器(IDE)打开 Cargo.toml 文件。它应该看起来像这样:

+

我们向 cargo new 传递了 --bin,因为我们的目标是生成一个可执行程序,而不是一个库。可执行程序是二进制可执行文件,通常就叫做 二进制文件binaries)。项目的名称被定为hello_cargo,同时 Cargo 在一个同名目录中创建它的文件,接着我们可以进入查看。

+

如果列出 hello_cargo 目录中的文件,将会看到 Cargo 生成了一个文件和一个目录:一个 Cargo.toml 文件和一个 src 目录,main.rs 文件位于目录中。它也在 hello_cargo 目录初始化了一个 git 仓库,以及一个 .gitignore 文件;你可以通过--vcs参数,切换到其它版本控制系统(VCS),或者不使用 VCS。

+

使用文本编辑器(IDE)打开 Cargo.toml 文件。它应该看起来像这样:

Filename: Cargo.toml

[package]
 name = "hello_cargo"
@@ -170,22 +170,22 @@ authors = ["Your Name <you@example.com>"]
 
 [dependencies]
 
-

这个文件使用TOML (Tom's Obvious, Minimal Language) 格式。TOML 类似于 INI,不过有一些额外的改进之处,并且被用作 Cargo 的配置文件的格式。

-

第一行,[package],是一个部分标题表明下面的语句用来配置一个包。随着我们在这个文件增加更多的信息,还将增加其他部分。

-

最后一行,[dependencies],是列出项目依赖的 crates(我们这么称呼 Rust 代码包)的部分的开始,这样 Cargo 也就知道去下载和编译它们了。这个项目并不需要任何其他的 crate,不过在下一章猜猜看教程会需要。

+

这个文件使用 TOML (Tom's Obvious, Minimal Language) 格式。TOML 类似于 INI,不过有一些额外的改进之处,并且被用作 Cargo 的配置文件的格式。

+

第一行,[package],是一个段落标题,表明下面的语句用来配置一个包。随着我们在这个文件增加更多的信息,还将增加其他段落。

+

最后一行,[dependencies],是项目依赖的 crates(Rust 代码包)的段落的开始,这样 Cargo 就知道下载和编译它们了。这个项目并不需要任何其他的 crate,不过在下一章猜猜看教程会需要。

现在看看 src/main.rs

Filename: src/main.rs

fn main() {
     println!("Hello, world!");
 }
 
-

Cargo 为你生成了一个“Hello World!”,正如我们之前编写的那个!目前为止我们所见过的之前项目与 Cargo 生成的项目区别有:

+

Cargo 为你生成了一个“Hello World!”,正如我们之前编写的那个!目前为止,之前项目与 Cargo 生成项目区别有:

-

Cargo 期望源文件位于 src 目录,这样将项目根目录留给 README、license 信息、配置文件和其他跟代码无关的文件。这样,Cargo 帮助你保持项目干净整洁。一切井井有条。

-

如果没有使用 Cargo 开始项目,正如在 hello_world 目录中的项目,可以把它转化为一个使用 Cargo 的项目,通过将代码放入 src 目录并创建一个合适的 Cargo.toml

+

Cargo 期望源文件位于 src 目录,将项目根目录留给 README、license 信息、配置文件和其他跟代码无关的文件。这样,Cargo 帮助你保持项目干净整洁,一切井井有条。

+

如果没有用 Cargo 创建项目,比如 hello_world 目录中的项目,可以通过将代码放入 src 目录,并创建一个合适的 Cargo.toml,将其转化为一个 Cargo 项目。

构建并运行 Cargo 项目

现在让我们看看通过 Cargo 构建和运行 Hello World 程序有什么不同。为此,输入如下命令:

$ cargo build
@@ -196,40 +196,40 @@ authors = ["Your Name <you@example.com>"]
 Hello, world!
 

好的!如果一切顺利,Hello, world!应该再次打印在终端上。

-

第一次运行cargo build的时候也会使 Cargo 在项目根目录创建一个叫做 Cargo.lock 的新文件,它看起来像这样:

+

首次运行 cargo build 的时候,Cargo 会在项目根目录创建一个新文件,Cargo.lock,它看起来像这样:

Filename: Cargo.lock

[root]
 name = "hello_cargo"
 version = "0.1.0"
 
-

Cargo 使用 Cargo.lock 来记录程序的依赖。这个项目并没有依赖,所以内容有一点稀少。事实上,你自己永远也不需要碰这个文件;仅仅让 Cargo 处理它就行了。

-

我们刚刚使用cargo build构建了项目并使用./target/debug/hello_cargo运行了它,不过也可以使用cargo run编译并运行:

+

Cargo 使用 Cargo.lock 来记录程序的依赖。这个项目并没有依赖,所以内容比较少。事实上,你自己永远也不需要碰这个文件,让 Cargo 处理它就行了。

+

我们刚刚使用 cargo build 构建了项目并使用 ./target/debug/hello_cargo 运行了它,也可以使用 cargo run 编译并运行:

$ cargo run
      Running `target/debug/hello_cargo`
 Hello, world!
 
-

注意这一次,并没有出现告诉我们 Cargo 正在编译 hello_cargo 的输出。Cargo 发现文件并没有被改变,所以只是运行了二进制文件。如果修改了源文件的话,将会出现像这样的输出:

+

注意这一次并没有出现“正在编译 hello_cargo”的输出。Cargo 发现文件并没有被改变,直接运行了二进制文件。如果修改了源文件的话,将会出现像这样的输出:

$ cargo run
    Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
      Running `target/debug/hello_cargo`
 Hello, world!
 
-

所以现在又出现一些更多的不同:

+

所以现在又出现更多的不同:

-

Cargo 的另一个优点是不管你使用什么操作系统它的命令都是一样的,所以之后我们将不再为 Linux 和 Mac 以及 Windows 提供特定的命令。

+

Cargo 的另一个优点是,不管你使用什么操作系统,它的命令都是一样的,所以之后我们将不再为 Linux 和 Mac 以及 Windows 提供相应的命令。

发布构建

-

当项目最终准备好发布了,可以使用cargo build --release来优化编译项目。这会在 target/release 下生成可执行文件,而不是 target/debug。这些优化可以让 Rust 代码运行的更快,不过启用他们会让程序花更长的时间编译。这也是为何这是两种不同的配置:一个为了开发,这时你经常想要快速重新构建;另一个构建提供给用户的最终程序,这时并不会重新构建并希望程序能运行得越快越好。如果你在测试代码的运行时间,请确保运行cargo build --release并使用 target/release 下的可执行文件进行测试。

+

当项目最终准备好发布了,可以使用 cargo build --release 来优化编译项目。这会在 target/release 下生成可执行文件,而不是 target/debug。优化可以让 Rust 代码运行的更快,然而也需要更长的编译时间。因此产生了两种不同的配置:一种为了开发,你需要快速重新构建;另一种构建给用户的最终程序,不会重新构建,并且程序运行得越快越好。如果你在测试代码的运行时间,请确保运行 cargo build --release 并使用 target/release 下的可执行文件。

把 Cargo 当作习惯

-

对于简单项目, Cargo 并不能比rustc提供更多的价值,不过随着开发的进行终将体现它的价值。对于拥有多个 crate 的复杂项目,让 Cargo 来协调构建将更简单。有了 Cargo,只需运行cargo build,然后一切将有序运行。即便这个项目很简单,现在也它使用了很多之后你的 Rust 程序生涯将会用得上的实用工具。事实上,无形中你可以使用下面的命令开始所有你想要从事的项目:

+

对于简单项目, Cargo 并不比 rustc 更有价值,不过随着开发的进行终将体现它的价值。对于拥有多个 crate 的复杂项目,让 Cargo 来协调构建将更简单。有了 Cargo,只需运行cargo build,然后一切将有序运行。即便这个项目很简单,也它使用了很多你之后的 Rust 生涯将会用得上的实用工具。其实你可以开始任何你想要从事的项目,使用下面的命令:

$ git clone someurl.com/someproject
 $ cd someproject
 $ cargo build
 
-

注意:如果你想要查看 Cargo 的更多细节,请阅读官方的 Cargo guide,它覆盖了其所有的功能。

+

注意:如果想要了解 Cargo 更多的细节,请阅读官方的 Cargo guide,它覆盖了所有的功能。

diff --git a/docs/ch02-00-guessing-game-tutorial.html b/docs/ch02-00-guessing-game-tutorial.html index a2b0b8a..bda1095 100644 --- a/docs/ch02-00-guessing-game-tutorial.html +++ b/docs/ch02-00-guessing-game-tutorial.html @@ -73,10 +73,10 @@
commit e6d6caab41471f7115a621029bd428a812c5260e

-

让我们通过自己动手的方式一起完成一个项目来快速上手 Rust!本章将介绍一些常用的 Rust 概念,并通过真实的程序来展示如何运用他们。你将会学到letmatch、方法、关联函数、使用外部 crate 等更多的知识!接下来的章节会探索这些概念的细节。在这一章,我们将练习基础。

-

我们会实现一个经典的新手编程问题:猜猜看游戏。它是这么工作的:程序将会随机生成一个 1 到 100 之间的随机整数。接着它会提示玩家输入一个猜测。当输入了一个猜测后,它会提示猜测是太大了还是太小了。如果猜对了,它会打印出祝贺并退出。

+

让我们亲自动手,快速熟悉 Rust!本章将介绍 Rust 中常用的一些概念,并通过真实的程序来展示如何运用。你将会学到更多诸如 letmatch、方法、关联函数、外部 crate 等知识!后继章节会深入探索这些概念的细节。在这一章,我们将练习基础。

+

我们会实现一个经典的新手编程问题:猜猜看游戏。它是这么工作的:程序将会随机生成一个 1 到 100 之间的随机整数。接着它会请玩家猜一个数并输入,然后提示猜测是大了还是小了。如果猜对了,它会在退出前祝贺你。

准备一个新项目

-

要创建一个新项目,进入在第一章创建的项目目录,像这样使用 Cargo 创建它:

+

要创建一个新项目,进入第一章创建的项目目录,使用 Cargo 创建它:

$ cargo new guessing_game --bin
 $ cd guessing_game
 
@@ -91,22 +91,22 @@ authors = ["Your Name <you@example.com>"] [dependencies]

如果 Cargo 从环境中获取的开发者信息不正确,修改这个文件并再次保存。

-

正如第一章那样,cargo new生成了一个“Hello, world!”程序。查看 src/main.rs 文件:

+

正如第一章那样,cargo new 生成了一个“Hello, world!”程序。查看 src/main.rs 文件:

Filename: src/main.rs

fn main() {
     println!("Hello, world!");
 }
 
-

现在让我们使用cargo run在相同的步骤编译并运行这个“Hello, world!”程序:

+

现在让我们使用 cargo run,编译运行一步到位:

$ cargo run
    Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
      Running `target/debug/guessing_game`
 Hello, world!
 
-

run命令在需要快速迭代项目时就派上用场了,而这个游戏就正是这么一个项目:我们需要在进行下一步之前快速测试每次迭代。

-

重新打开 src/main.rs 文件。我们将会在这个文件编写全部的代码。

+

run 命令适合用在需要快速迭代的项目,而这个游戏就是:我们需要在下一步迭代之前快速测试。

+

重新打开 src/main.rs 文件。我们将会在这个文件中编写全部代码。

处理一次猜测

-

程序的第一部分会请求用户输入,处理输入,并检查输入是否为期望的形式。首先,允许玩家输入一个猜测。在 src/main.rs 中输入列表 2-1 中的代码。

+

程序的第一部分请求和处理用户输入,并检查输入是否符合预期。首先,需要有一个让玩家输入猜测的地方。在 src/main.rs 中输入列表 2-1 中的代码。

Filename: src/main.rs

use std::io;
 
@@ -124,59 +124,59 @@ fn main() {
 }
 

Listing 2-1: Code to get a guess from the user and print it out

-

这些代码包含很多信息,所以让我们一点一点地过一遍。为了获取用户输入并接着打印结果作为输出,我们需要将io(输入/输出)库引入作用域中。io库来自于标准库(也被称为std):

+

这些代码包含很多信息,我们一点一点地过一遍。为了获取用户输入并打印结果作为输出,我们需要将 io(输入/输出)库引入当前作用域。io库来自于标准库(也被称为std):

use std::io;
 
-

Rust 默认只在每个程序的 prelude 中引用很少的一些类型。如果想要使用的类型并不在 prelude 中,你必须使用一个use语句显式的将其引入到作用域中。使用std::io库将提供很多io相关的功能,接受用户输入的功能。

-

正如第一章所讲,main函数是程序的入口点:

+

Rust 默认只在每个程序的 prelude 中引入少量类型。如果需要的类型不在 prelude 中,你必须使用一个 use 语句显式的将其引入作用域。std::io 库提供很多 io 相关的功能,比如接受用户输入。

+

如第一章所提及,main 函数是程序的入口点:

fn main() {
 
-

fn语法声明了一个新函数,()表明没有参数,{作为函数体的开始。

-

第一章也讲到了,println!是一个在屏幕上打印字符串的宏:

+

fn 语法声明了一个新函数,() 表明没有参数,{ 作为函数体的开始。

+

第一章也提及,println! 是一个在屏幕上打印字符串的宏:

println!("Guess the number!");
 
 println!("Please input your guess.");
 
-

这些代码仅仅打印一个提示,说明游戏的内容并请求用户输入。

+

这些代码仅仅打印提示,介绍游戏的内容然后请用户输入。

用变量储存值

接下来,创建一个地方储存用户输入,像这样:

-
let mut guess = String::new();
+
let mut guess = String::new();
 
-

现在程序开始变得有意思了!这一小行代码发生了很多事。注意这是一个let语句,用来创建变量。这里是另外一个例子:

-
let foo = bar;
+

现在程序开始变得有意思了!这一小行代码发生了很多事。注意这是一个 let 语句,用来创建变量。这里是另外一个例子:

+
let foo = bar;
 
-

这行代码会创建一个叫做foo的新变量并把它绑定到值bar上。在 Rust 中,变量默认是不可变的。下面的例子展示了如何在变量名前使用mut来使一个变量可变:

+

这行代码会创建一个叫做 foo 的新变量并把它绑定到值 bar 上。在 Rust 中,变量默认是不可变的。下面的例子展示了如何在变量名前使用 mut 来使一个变量可变:

let foo = 5; // immutable
 let mut bar = 5; // mutable
 

注意:// 开始一个注释,它持续到本行的结尾。Rust 忽略注释中的所有内容。

-

现在我们知道了let mut guess会引入一个叫做guess的可变变量。等号(=)的另一边是guess所绑定的值,它是String::new的结果,这个函数会返回一个String的新实例。String是一个标准库提供的字符串类型,它是可增长的、UTF-8 编码的文本块。

-

::new那一行的::语法表明newString类型的一个关联函数associated function)。关联函数是针对类型实现的,在这个例子中是String,而不是String的某个特定实例。一些语言中把它称为静态方法static method)。

-

new函数创建了一个新的空的String,你会在很多类型上发现new函数,因为这是创建某个类型新值的常用函数名。

-

总结一下,let mut guess = String::new();这一行创建了一个可变变量,目前它绑定到一个String新的、空的实例上。哟!

-

回忆一下我们在程序的第一行使用use std::io;从标准库中引用输入/输出功能。现在在io上调用一个关联函数,stdin

+

现在我们知道了 let mut guess 会引入一个叫做 guess 的可变变量。等号(=)的右边是 guess 所绑定的值,它是 String::new 的结果,这个函数会返回一个 String 的新实例。String 是一个标准库提供的字符串类型,它是 UTF-8 编码的可增长文本块。

+

::new 那一行的 :: 语法表明 newString 类型的一个 关联函数associated function)。关联函数是针对类型实现的,在这个例子中是 String,而不是 String 的某个特定实例。一些语言中把它称为静态方法static method)。

+

new 函数创建了一个新的空 String,你会在很多类型上发现new 函数,这是创建类型实例的惯用函数名。

+

总结一下,let mut guess = String::new(); 这一行创建了一个可变变量,绑定到一个新的 String 空实例上。

+

回忆一下,我们在程序的第一行使用 use std::io; 从标准库中引入“输入输出”。现在调用 io 的关联函数 stdin

io::stdin().read_line(&mut guess)
     .expect("Failed to read line");
 
-

如果我们在程序的开头没有use std::io这一行,我们可以把函数调用写成std::io::stdin这样。stdin函数返回一个 std::io::Stdin的实例,这是一个代表终端标准输入句柄的类型。

-

代码的下一部分,.read_line(&mut guess),调用 read_line 方法从标准输入句柄获取用户输入。我们还向read_line()传递了一个参数:&mut guess

-

read_line的工作是把获取任何用户键入到标准输入的字符并放入一个字符串中,所以它获取字符串作为一个参数。这个字符串需要是可变的,这样这个方法就可以通过增加用户的输入来改变字符串的内容。

-

&表明这个参数是一个引用reference),它提供了一个允许多个不同部分的代码访问同一份数据而不需要在内存中多次拷贝的方法。引用是一个复杂的功能,而 Rust 的一大优势就是它是如何安全而优雅操纵引用的。完成这个程序并不需要知道这么多细节:第四章会更全面的解释引用。现在,我们只需知道它像变量一样,默认是不可变的。因此,需要写成&mut guess而不是&guess来使其可变。

-

我们还没有分析完这行代码。虽然这是单独一行代码,但它只是一个逻辑上代码行(虽然换行了但仍是一个语句)的第一部分。第二部分是这个方法:

-
.expect("Failed to read line");
-
-

当使用.expect()语法调用方法时,明智的选择是换行并留出空白(缩进)来把长的代码行拆开。我们可以把代码写成这样:

-
io::stdin().read_line(&mut guess).expect("Failed to read line");
-
-

不过,过长的代码行难以阅读,所以最好拆开来写,两行代码两个方法调用。现在来看看这行代码干了什么。

-

使用Result类型来处理潜在的错误

-

之前提到过,read_line将用户输入放入到传递给它字符串中,不过它也返回一个值——在这个例子中,一个io::Result。Rust 标准库中有很多叫做Result的类型。一个Result泛型以及对应子模块的特定版本,比如io::Result

-

Result类型是 枚举enumerations,通常也写作 enums。枚举拥有固定值集合的类型,而这些值被称为枚举的成员variants)。第六章会更详细的介绍枚举。

-

对于Result,它的成员是OkErrOk表明操作成功了,同时Ok成员之中包含成功生成的值。Err意味着操作失败,Err之中包含操作是为什么或如何失败的信息。

-

Result类型的作用是编码错误处理信息。Result类型的值,正如其他任何类型,拥有定义于其上的方法。io::Result的实例拥有expect方法可供调用。如果io::Result实例的值是Errexpect会导致程序崩溃并显示显示你作为参数传递给expect的信息。如果io::Result实例的值是Okexpect会获取Ok中的值并原原本本的返回给你。在本例中,这个值是用户输入到标准输入中的字节的数量。

-

如果不使用expect,程序也能编译,不过会出现一个警告:

+

如果程序的开头没有 use std::io 这一行,我们可以把函数调用写成 std::io::stdinstdin 函数返回一个 std::io::Stdin 的实例,这代表终端标准输入句柄的类型。

+

代码的下一部分,.read_line(&mut guess),调用 read_line 方法从标准输入句柄获取用户输入。我们还向 read_line() 传递了一个参数:&mut guess

+

read_line 的工作是,无论用户在标准输入中键入什么内容,都将其存入一个字符串中,因此它需要字符串作为参数。这个字符串参数应该是可变的,以便 read_line 将用户输入附加上去。

+

& 表示这个参数是一个引用reference),它允许多处代码访问同一处数据,而无需在内存中多次拷贝。引用是一个复杂的特性,Rust 的一个主要优势就是安全而简单的操纵引用。完成当前程序并不需要了解如此多细节:第四章会更全面的解释引用。现在,我们只需知道它像变量一样,默认是不可变的,需要写成 &mut guess 而不是 &guess 来使其可变。

+

我们还没有分析完这行代码。虽然这是单独一行代码,但它是一个逻辑行(虽然换行了但仍是一个语句)的第一部分。第二部分是这个方法:

+
.expect("Failed to read line");
+
+

当使用 .expect() 语法调用方法时,通过‘换行并缩进’来把长行拆开,是明智的。我们完全可以这样写:

+
io::stdin().read_line(&mut guess).expect("Failed to read line");
+
+

不过,过长的行难以阅读,所以最好拆开来写,两行代码两个方法调用。现在来看看这行代码干了什么。

+

使用 Result 类型来处理潜在的错误

+

之前提到,read_line 将用户输入附加到传递给它字符串中,不过它也返回一个值——在这个例子中是 io::Result。Rust 标准库中有很多叫做 Result 的类型。一个 Result 泛型以及对应子模块的特定版本,比如 io::Result

+

Result 类型是 枚举enumerations,通常也写作 enums。枚举类型持有固定集合的值,这些值被称为枚举的成员variants)。第六章将介绍枚举的更多细节。

+

对于 Result,它的成员是 OkErrOk 表示操作成功,内部包含产生的值。Err 意味着操作失败,包含失败的前因后果。

+

Result 类型的作用是编码错误处理信息。Result 类型的值,像其他类型一样,拥有定义于其上的方法。io::Result 的实例拥有expect方法,如果实例的值是 Errexpect 会导致程序崩溃,并显示当做参数传递给 expect 的信息;如果实例的值是 Okexpect 会获取 Ok 中的值并原样返回。在本例中,这个值是用户输入到标准输入中的字节的数量。

+

如果不使用 expect,程序也能编译,不过会出现一个警告:

$ cargo build
    Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
 src/main.rs:10:5: 10:39 warning: unused result which must be used,
@@ -184,18 +184,18 @@ src/main.rs:10:5: 10:39 warning: unused result which must be used,
 src/main.rs:10     io::stdin().read_line(&mut guess);
                    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-

Rust 警告我们没有使用read_line返回的值Result,表明程序没有处理一个可能的错误。消除警告的正确方式是老实编写错误处理,不过因为我们仅仅希望程序出现问题就崩溃,可以直接使用expect。第九章会学习从错误中恢复的内容。

-

使用println!占位符打印值

-

除了位于结尾的大括号,目前为止编写的代码就只有一行代码值得讨论一下了,就是这一行:

-
println!("You guessed: {}", guess);
+

Rust 警告我们没有使用 read_line 的返回值 Result,说明有一个可能的错误没处理。想消除警告,就老实的写错误处理,不过我们就是希望程序在出现问题时立即崩溃,所以直接使用 expect。第九章会学习如何从错误中恢复。

+

使用 println! 占位符打印值

+

除了位于结尾的大括号,目前为止就只有一行代码值得讨论一下了,就是这一行:

+
println!("You guessed: {}", guess);
 
-

这行代码打印出存储了用户输入的字符串。这对{}是一个在特定位置预留值的占位符。可以使用{}打印多个值:第一个{}对应格式化字符串之后列出的第一个值,第二个对应第二个值,以此类推。用一个println!调用打印多个值应该看起来像这样:

+

这行代码打印存储用户输入的字符串。第一个参数是格式化字符串,里面的 {} 是预留在特定位置的占位符。使用占位符也可以打印多个值:格式化字符串中第一个占位符对应第二个参数值,第二个占位符对应第三个参数值,以此类推(第一个参数是格式化字符串本身)。调用一次 println! 打印多个值看起来像这样:

let x = 5;
 let y = 10;
 
 println!("x = {} and y = {}", x, y);
 
-

这行代码会打印出x = 5 and y = 10

+

这行代码会打印出 x = 5 and y = 10

测试第一部分代码

让我们来测试下猜猜看游戏的第一部分。使用cargo run运行它:

$ cargo run
@@ -206,19 +206,19 @@ Please input your guess.
 6
 You guessed: 6
 
-

至此为止,游戏的第一部分已经完成:我们从键盘获取了输入并打印了出来。

+

至此为止,游戏的第一部分已经完成:我们从键盘获取输入并打印了出来。

生成一个秘密数字

-

接下来,需要生成一个秘密数字,用户会尝试猜测它。秘密数字应该每次都不同,这样多玩几次才会有意思。生成一个 1 到 100 之间的随机数这样游戏也不会太难。Rust 标准库中还未包含随机数功能。然而,Rust 团队确实提供了一个rand crate

+

接下来,需要生成一个秘密数字,好让用户来猜。秘密数字应该每次都不同,这样重复玩才不会乏味;范围应该在 1 到 100 之间,这样才不会太困难。Rust 标准库中尚未包含随机数功能。然而,Rust 团队还是提供了一个 rand crate

使用 crate 来增加更多功能

-

记住 crate 是一个 Rust 代码的包。我们正在构建的项目是一个二进制 crate,它生成一个可执行文件。 rand crate 是一个 库 crate,它包含意在被其他程序使用的代码。

-

Cargo 对外部 crate 的运用是其真正闪光的地方。在我们可以使用rand编写代码之前,需要编辑 Cargo.toml 来包含rand作为一个依赖。现在打开这个文件并在[dependencies]部分标题(Cargo 为你创建了它)的下面添加如下代码:

+

记住 crate 是一个 Rust 代码的包。我们正在构建的项目是一个二进制 crate,它生成一个可执行文件。 rand crate 是一个 库 crate,库 crate 可以包含任意能被其他程序使用的代码。

+

Cargo 对外部 crate 的运用是亮点。在我们使用 rand 编写代码之前,需要编辑 Cargo.toml ,声明 rand 作为一个依赖。现在打开这个文件并在 [dependencies] 标题(Cargo 为你创建了它)之下添加:

Filename: Cargo.toml

[dependencies]
 
 rand = "0.3.14"
 
-

Cargo.toml 文件中,任何标题之后的内容都是属于这个部分的,一直持续到直到另一个部分开始。[dependencies]部分告诉 Cargo 项目依赖了哪个外部 crate 和需要的 crate 版本。在这个例子中,我们使用语义化版本符号0.3.14来指定randcrate。Cargo 理解语义化版本(Semantic Versioning)(有时也称为 SemVer),这是一个编写版本号的标准。版本号0.3.14事实上是^0.3.14的缩写,它的意思是“任何与 0.3.14 版本公有 API 相兼容的版本”。

-

现在,不用修改任何代码,构建项目,如列表 2-2 所示:

+

Cargo.toml 文件中,标题以及之后的内容属同一个段落,遇到下一个标题则开始新的段落。[dependencies] 部分告诉 Cargo 本项目依赖了哪些外部 crate 及其版本。本例中,我们使用语义化版本 0.3.14 来指定 rand crate。Cargo 理解语义化版本(Semantic Versioning)(有时也称为 SemVer),是一种定义版本号的标准。0.3.14 事实上是 ^0.3.14 的简写,它表示“任何与 0.3.14 版本公有 API 相兼容的版本”。

+

现在,不修改任何代码,构建项目,如列表 2-2 所示:

$ cargo build
     Updating registry `https://github.com/rust-lang/crates.io-index`
  Downloading rand v0.3.14
@@ -229,40 +229,40 @@ rand = "0.3.14"
 

Listing 2-2: The output from running cargo build after adding the rand crate as a dependency

-

可能会出现不同的版本号(不过多亏了语义化版本,它们与代码是兼容的!),同时显示顺序也可能会有所不同。

-

现在我们有了一个外部依赖,Cargo 从 registryCrates.io)上获取了一份(兼容的)最新版本代码的拷贝。Crates.io 是 Rust 生态环境中的开发者们向他人贡献他们的开源 Rust 项目的地方。

-

在更新完 registry (索引)后,Cargo 检查[dependencies]部分并下载还不存在的部分。在这个例子中,虽然只列出了rand一个依赖,Cargo 也获取了一份libc的拷贝,因为rand依赖libc来正常工作。在下载他们之后,Rust 编译他们并接着使用这些依赖编译项目。

-

如果不做任何修改就立刻再次运行cargo build,则不会有任何输出。Cargo 知道它已经下载并编译了依赖,同时 Cargo.toml 文件中也没有任何相关修改。Cargo 也知道代码没有做任何修改,所以它也不会重新编译代码。因为无事可做,它简单的退出了。如果打开 src/main.rs 文件,并做一些普通的修改,保存并再次构建,只会出现一行输出:

+

可能会出现不同的版本号(多亏了语义化版本,它们与代码是兼容的!),同时显示顺序也可能会有所不同。

+

现在我们有了一个外部依赖,Cargo 从 registryCrates.io)上获取了一份(兼容的)最新版本的代码。Crates.io 是 Rust 生态环境中的开发者们向他人贡献 Rust 开源项目的地方。

+

在更新完 registry (索引)后,Cargo 检查 [dependencies] 段落并下载缺失的部分。本例中,只声明了 rand 一个依赖,然而 Cargo 还是额外获取了 libc,因为 rand 依赖 libc 来正常工作。下载完成后,Rust 编译依赖,然后使用这些依赖编译项目。

+

如果不做任何修改,立刻再次运行 cargo build,则不会有任何输出。Cargo 知道它已经下载并编译了依赖,同时 Cargo.toml 文件也没有变动,并且代码也没有任何修改,所以它不会重新编译代码。因为无事可做,它简单的退出了。如果打开 src/main.rs 文件,做一些普通的修改,保存并再次构建,只会出现一行输出:

$ cargo build
    Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
 
-

这一行表明 Cargo 只构建了对 src/main.rs 文件做出的微小修改。依赖没有被修改,所以 Cargo 知道可以复用已经为此下载并编译的代码。它只是重新构建了部分(项目)代码。

+

这一行表示 Cargo 只针对 src/main.rs 文件的微小修改而构建。依赖没有变化,所以 Cargo 会复用已经为此下载并编译的代码。它只是重新构建了部分(项目)代码。

Cargo.lock 文件确保构建是可重现的

-

Cargo 有一个机制来确保每次任何人重新构建代码都会生成相同的结果:Cargo 只会使用你指定的依赖的版本,除非你又手动指定了别的。例如,如果下周rand crate 的v0.3.15版本出来了,而它包含一个重要的 bug 修改并也含有一个会破坏代码运行的缺陷的时候会发生什么呢?

-

这个问题的答案是 Cargo.lock 文件,它在第一次运行cargo build时被创建并位于 guessing_game 目录。当第一次构建项目时,Cargo 计算出所有符合要求的依赖版本并接着写入 Cargo.lock 文件中。当将来构建项目时,Cargo 发现 Cargo.lock 存在就会使用这里指定的版本,而不是重新进行所有版本的计算。这使得你拥有了一个自动的可重现的构建。换句话说,项目会继续使用0.3.14直到你显式升级,多亏了 Cargo.lock 文件。我们将会在这个文件编写全部的代码。

+

Cargo 有一个机制来确保任何人在任何时候重新构建代码,都会产生相同的结果:Cargo 只会使用你指定的依赖的版本,除非你又手动指定了别的。例如,如果下周 rand crate 的 v0.3.15 版本出来了,它修复了一个重要的 bug,同时也含有一个缺陷,会破坏代码的运行,这时会发生什么呢?

+

答案是 Cargo.lock 文件。它在第一次运行 cargo build 时创建,并放在 guessing_game 目录,Cargo 计算出所有符合要求的依赖版本并写入 Cargo.lock 文件。当将来构建项目时,如果 Cargo.lock 存在,Cargo 就使用里面指定的版本,不会重新计算。自动使你拥有了一个可重现的构建。换句话说,项目会继续使用 0.3.14 直到你显式升级,感谢 Cargo.lock

更新 crate 到一个新版本

当你确实需要升级 crate 时,Cargo 提供了另一个命令,update,他会:

    -
  1. 忽略 Cargo.lock 文件并计算出所有符合 Cargo.toml 中规格的最新版本。
  2. +
  3. 忽略 Cargo.lock 文件,并计算出所有符合 Cargo.toml 声明的最新版本。
  4. 如果成功了,Cargo 会把这些版本写入 Cargo.lock 文件。
-

不过,Cargo 默认只会寻找大于0.3.0而小于0.4.0的版本。如果rand crate 发布了两个新版本,0.3.150.4.0,在运行cargo update时会出现如下内容:

+

不过,Cargo 默认只会寻找大于 0.3.0 而小于 0.4.0 的版本。如果 rand crate 发布了两个新版本,0.3.150.4.0,在运行 cargo update 时会出现如下内容:

$ cargo update
     Updating registry `https://github.com/rust-lang/crates.io-index`
     Updating rand v0.3.14 -> v0.3.15
 

这时,值得注意的是 Cargo.lock 文件中的一个改变,rand crate 现在使用的版本是0.3.15

-

如果想要使用0.4.0版本的rand或是任何0.4.x系列的版本,必须像这样更新 Cargo.toml 文件:

+

如果想要使用 0.4.0 版本的 rand 或是任何 0.4.x 系列的版本,必须像这样更新 Cargo.toml 文件:

[dependencies]
 
 rand = "0.4.0"
 
-

下一次运行cargo build时,Cargo 会更新 registry 中可用的 crate 并根据你指定新版本重新计算rand的要求。

-

第十四章会讲到Cargo它的生态系统的更多内容,不过目前你只需要了解这么多。Cargo 使得复用库文件变得非常容易,所以 Rustacean 们能够通过组合很多包来编写出更轻巧的项目。

+

下一次运行 cargo build 时,Cargo 会从 registry 更新,并根据你指定的新版本重新计算。

+

第十四章会讲到 Cargo 及其生态系统的更多内容,不过目前你只需要了解这么多。通过 Cargo 复用库文件非常容易,因此 Rustacean 能够编写出由很多包组装而成的更轻巧的项目。

生成一个随机数

-

让我们开始使用rand。下一步是更新 src/main.rs,如列表 2-3 所示:

+

让我们开始使用 rand。下一步是更新 src/main.rs,如列表 2-3 所示:

Filename: src/main.rs

-
extern crate rand;
+
extern crate rand;
 
 use std::io;
 use rand::Rng;
@@ -286,10 +286,10 @@ fn main() {
 

Listing 2-3: Code changes needed in order to generate a random number

-

我们在顶部增加一行extern crate rand;来让 Rust 知道我们要使用外部依赖。这也会调用相应的use rand,所以现在可以使用rand::前缀来调用rand中的任何内容。

-

接下来,我们增加了另一行useuse rand::RngRng是一个定义了随机数生成器应实现方法的 trait,如果要使用这些方法的话这个 trait 必须在作用域中。第十章会详细介绍 trait。

-

另外,中间还新增加了两行。rand::thread_rng函数会提供具体会使用的随机数生成器:它位于当前执行线程本地并从操作系统获取 seed。接下来,调用随机数生成器的gen_range方法。这个方法由我们使用use rand::Rng语句引入到作用域的Rng trait 定义。gen_range方法获取两个数作为参数并生成一个两者之间的随机数。它包含下限但不包含上限,所以需要指定1101来请求一个1100之间的数。

-

并不是仅仅能够知道该 use 哪个 trait 和该从 crate 中调用哪个方法。如何使用 crate 的说明位于每个 crate 的文档中。Cargo 另一个很棒的功能是可以运行cargo doc --open命令来构建所有本地依赖提供的文档并在浏览器中打开。例如,如果你对rand crate 中的其他功能感兴趣,运行cargo doc --open并点击左侧导航栏中的rand

+

我们在顶部增加一行 extern crate rand; 通知 Rust 我们要使用外部依赖。这也会调用相应的 use rand,所以现在可以使用 rand:: 前缀来调用 rand 中的内容。

+

接下来,我们增加了一行 useuse rand::RngRng 是一个 trait,它定义了随机数生成器应实现的方法 ,想使用这些方法的话此 trait 必须在作用域中。第十章会详细介绍 trait。

+

另外,中间还新增加了两行。rand::thread_rng 函数提供实际使用的随机数生成器:它位于当前执行线程,并从操作系统获取 seed。接下来,调用随机数生成器的 gen_range 方法。这个方法由刚才引入到作用域的 Rng trait 定义。gen_range 方法获取两个数字作为参数,并生成一个范围在两者之间的随机数。它包含下限但不包含上限,所以需要指定1101来请求一个1100之间的数。

+

知道 use 哪个 trait 和该从 crate 中调用哪个方法并不是全部,crate 的说明位于其文档中,Cargo 有一个很棒的功能是:运行 cargo doc --open 命令来构建所有本地依赖提供的文档,并在浏览器中打开。例如,假设你对 rand crate 中的其他功能感兴趣,cargo doc --open 并点击左侧导航栏中的 rand

新增加的第二行代码打印出了秘密数字。这在开发程序时很有用,因为我们可以去测试它,不过在最终版本我们会删掉它。游戏一开始就打印出结果就没什么可玩的了!

尝试运行程序几次:

$ cargo run
@@ -312,7 +312,7 @@ You guessed: 5
 

比较猜测与秘密数字

现在有了用户输入和一个随机数,我们可以比较他们。这个步骤如列表 2-4 所示:

Filename: src/main.rs

-
extern crate rand;
+
extern crate rand;
 
 use std::io;
 use std::cmp::Ordering;
@@ -343,17 +343,17 @@ fn main() {
 

Listing 2-4: Handling the possible return values of comparing two numbers

-

新代码的第一行是另一个use,从标准库引入了一个叫做std::cmp::Ordering的类型到作用域。Ordering是另一个枚举,像Result一样,不过Ordering的成员是LessGreaterEqual。这是你比较两个值时可能出现三种结果。

-

接着在底部的五行新代码使用了Ordering类型:

-
match guess.cmp(&secret_number) {
+

新代码的第一行是另一个 use,从标准库引入了一个叫做 std::cmp::Ordering 的类型。Ordering 是一个像 Result 一样的枚举,不过它的成员是 LessGreaterEqual。这是你做比较时可能出现的三种结果。

+

接着,底部的五行新代码使用了 Ordering 类型:

+
match guess.cmp(&secret_number) {
     Ordering::Less    => println!("Too small!"),
     Ordering::Greater => println!("Too big!"),
     Ordering::Equal   => println!("You win!"),
 }
 
-

cmp方法比较两个值并可以在任何可比较的值上调用。它获取一个任何你想要比较的值的引用:这里是把guesssecret_number做比较。cmp返回一个使用use语句引入作用域的Ordering枚举的成员。我们使用一个match表达式根据对guesssecret_number中的值调用cmp后返回的哪个Ordering枚举成员来决定接下来干什么。

-

一个match表达式由 分支(arms) 构成。一个分支包含一个 模式pattern)和代码,这些代码在match表达式开头给出的值符合分支的模式时将被执行。Rust 获取提供给match的值并挨个检查每个分支的模式。match结构和模式是 Rust 中非常强大的功能,它帮助你体现代码可能遇到的多种情形并帮助你处理全部的可能。这些功能将分别在第六章和第十八章详细介绍。

-

让我们看看一个使用这里的match表达式会发生什么的例子。假设用户猜了 50,这时随机生成的秘密数字是 38。当代码比较 50 与 38 时,cmp方法会返回Ordering::Greater,因为 50 比 38 要大。Ordering::Greatermatch表达式得到的值。它检查第一个分支的模式,Ordering::Less,不过值Ordering::Greater并不匹配Ordering::Less。所以它忽略了这个分支的代码并移动到下一个分支。下一个分支的模式,Ordering::Greater正确匹配了Ordering::Greater!这个分支关联的代码会被执行并在屏幕打印出Too big!match表达式就此终止,因为在这个特定场景下没有检查最后一个分支的必要。

+

cmp 方法用来比较两个值。在任何可比较的值上调用,然后获取另一个被比较值的引用:这里是把 guesssecret_number 做比较,返回一个 Ordering 枚举的成员。再使用一个 match 表达式,根据枚举成员来决定接下来干什么。

+

一个 match 表达式由 分支(arms) 构成。一个分支包含一个 模式pattern)和动作,表达式头的求值结果符合分支的模式时将执行对应的动作。Rust 获取提供给 match 的值并挨个检查每个分支的模式。match 结构和模式是 Rust 的强大功能,它体现了代码可能遇到的多种情形,并帮助你没有遗漏的处理。这些功能将分别在第六章和第十八章详细介绍。

+

让我们看看使用 match 表达式的例子。假设用户猜了 50,这时随机生成的秘密数字是 38。比较 50 与 38 时,因为 50 比 38 要大,cmp 方法会返回 Ordering::Greatermatch 表达式得到该值,然后检查第一个分支的模式,Ordering::LessOrdering::Greater并不匹配,所以它忽略了这个分支的动作并来到下一个分支。下一个分支的模式是 Ordering::Greater正确匹配!这个分支关联的动作被执行,在屏幕打印出 Too big!match 表达式就此终止,因为该场景下没有检查最后一个分支的必要。

然而,列表 2-4 的代码并不能编译,可以尝试一下:

$ cargo build
    Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
@@ -369,10 +369,10 @@ error[E0308]: mismatched types
 error: aborting due to previous error
 Could not compile `guessing_game`.
 
-

错误的核心表明这里有不匹配的类型mismatched types)。Rust 拥有一个静态强类型系统。不过,它也有类型推断。当我们写出let guess = String::new()时,Rust 能够推断出guess应该是一个String,并不需要我们写出类型。另一方面,secret_number,是一个数字类型。一些数字类型拥有 1 到 100 之间的值:i32,一个 32 位的数字;u32,一个 32 位无符号数字;i64,一个 64 位数字;等等。Rust 默认使用i32,所以secret_number的类型就是它,除非增加类型信息或任何能让 Rust 推断出不同数值类型的信息。这里错误的原因在于 Rust 不会比较字符串类型和数字类型。

-

最终我们想要把程序从输入中读取到的String转换为一个真正的数字类型,这样好与秘密数字向比较。可以通过在main函数体中增加如下两行代码来实现:

+

错误的核心表明这里有不匹配的类型mismatched types)。Rust 有一个静态强类型系统,同时也有类型推断。当我们写出 let guess = String::new() 时,Rust 推断出 guess 应该是一个String,不需要我们写出类型。另一方面,secret_number,是一个数字类型。多种数字类型拥有 1 到 100 之间的值:32 位数字 i32;32 位无符号数字 u32;64 位数字 i64 等等。Rust 默认使用 i32,所以它是 secret_number 的类型,除非增加类型信息,或任何能让 Rust 推断出不同数值类型的信息。这里错误的原因在于 Rust 不会比较字符串类型和数字类型。

+

所以我们必须把从输入中读取到的 String 转换为一个真正的数字类型,才好与秘密数字进行比较。可以通过在 main 函数体中增加如下两行代码来实现:

Filename: src/main.rs

-
extern crate rand;
+
extern crate rand;
 
 use std::io;
 use std::cmp::Ordering;
@@ -405,13 +405,13 @@ fn main() {
 }
 

这两行代码是:

-
let guess: u32 = guess.trim().parse()
+
let guess: u32 = guess.trim().parse()
     .expect("Please type a number!");
 
-

这里创建了一个叫做guess的变量。不过等等,难道这个程序不是已经有了一个叫做guess的变量了吗?确实如此,不过 Rust 允许我们通过覆盖shadow) 用一个新值来覆盖guess之前的值。这个功能经常用在类似需要把一个值从一种类型转换到另一种类型的场景。shadowing 允许我们复用guess变量的名字而不是强迫我们创建两个不同变量,比如guess_strguess。(第三章会介绍 shadowing 的更多细节。)

-

guess被绑定到guess.trim().parse()表达式。表达式中的guess对应包含输入的String类型的原始guessString实例的trim方法会消除字符串开头和结尾的空白。u32只能包含数字字符。不过用户必须输入回车键才能让read_line返回。当用户按下回车键时,会在字符串中增加一个换行(newline)字符。例如,如果用户输入 5 并回车,guess看起来像这样:5\n\n代表“换行”,回车键。trim方法消除\n,只留下5

-

字符串的parse方法解析一个字符串成某个数字。因为这个方法可以解析多种数字类型,需要告诉 Rust 我们需要的具体的数字类型,这里通过let guess: u32指定。guess后面的冒号(:)告诉 Rust 我们指明了变量的类型。Rust 有一些内建的数字类型;这里的u32是一个无符号的 32 位整型。它是一个好的较小正整数的默认类型。第三章会讲到其他数字类型。另外,例子程序中的u32注解和与secret_number的比较意味着 Rust 会推断secret_number应该是也是u32类型。现在可以使用相同类型比较两个值了!

-

parse调用容易产生错误。例如,如果字符串包含A👍%,就无法将其转换为一个数字。因为它可能失败,parse方法返回一个Result类型,非常像之前在“使用Result类型来处理潜在的错误”部分讨论的read_line方法。这里再次类似的使用expect方法处理这个Result类型。如果parse因为不能从字符串生成一个数字而返回一个ErrResult成员时,expect会使游戏崩溃并打印提供给它的信息。如果parse能成功地将字符串转换为一个数字,它会返回ResultOk成员,同时expect会返回Ok中我们需要的数字。

+

这里创建了一个叫做 guess 的变量。不过等等,不是已经有了一个叫做guess的变量了吗?确实如此,不过 Rust 允许遮盖shadow),用一个新值来遮盖 guess 之前的值。这个功能常用在需要转换值类型之类的场景,它允许我们复用 guess 变量的名字,而不是被迫创建两个不同变量,诸如 guess_strguess 之类。(第三章会介绍 shadowing 的更多细节。)

+

guess 被绑定到 guess.trim().parse() 表达式。表达式中的 guess 是包含输入的 String 类型。String 实例的 trim 方法会去除字符串开头和结尾的空白。u32 只能由数字字符转换,不过用户必须输入回车键才能让 read_line 返回,然而用户按下回车键时,会在字符串中增加一个换行(newline)符。例如,用户输入 5 并回车,guess 看起来像这样:5\n\n 代表“换行”,回车键。trim 方法消除 \n,只留下5

+

字符串的parse方法 将字符串解析成数字。这个方法可以解析多种数字类型,因此需要告诉 Rust 具体的数字类型,这里通过 let guess: u32 指定。guess 后面的冒号(:)告诉 Rust 我们指定了变量的类型。Rust 有一些内建的数字类型;u32 是一个无符号的 32 位整型。对于不大的正整数来说,它是不错的类型,第三章还会讲到其他数字类型。另外,程序中的 u32 注解以及与 secret_number 的比较,意味着 Rust 会推断出 secret_number 也是 u32 类型。现在可以使用相同类型比较两个值了!

+

parse 调用可能产生错误。例如,字符串中包含 A👍%,就无法将其转换为一个数字。因此,parse 方法返回一个 Result 类型。像之前讨论的 read_line 方法,按部就班的用 expect 方法处理即可。如果 parse 不能从字符串生成一个数字,返回一个 Result::Err 时,expect 会使游戏崩溃并打印附带的信息。如果 parse 成功地将字符串转换为一个数字,它会返回 Result::Ok,然后 expect 会返回 Ok 中的数字。

现在让我们运行程序!

$ cargo run
    Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
@@ -426,9 +426,9 @@ Too big!
 

漂亮!即便是在猜测之前添加了空格,程序依然能判断出用户猜测了 76。多运行程序几次来检验不同类型输入的相应行为:猜一个正确的数字,猜一个过大的数字和猜一个过小的数字。

现在游戏已经大体上能玩了,不过用户只能猜一次。增加一个循环来改变它吧!

使用循环来允许多次猜测

-

loop关键字提供了一个无限循环。增加它后给了用户多次猜测的机会:

+

loop 关键字提供了一个无限循环。将其加入后,用户可以反复猜测:

Filename: src/main.rs

-
extern crate rand;
+
extern crate rand;
 
 use std::io;
 use std::cmp::Ordering;
@@ -462,8 +462,8 @@ fn main() {
     }
 }
 
-

如上所示,我们将提示用户猜测之后的所有内容放入了循环。确保这些代码多缩进了四个空格,并再次运行程序。注意这里有一个新问题,因为程序忠实地执行了我们要求它做的:永远地请求另一个猜测!看起来用户没法退出啊!

-

用户总是可以使用Ctrl-C快捷键来终止程序。不过这里还有另一个逃离这个贪得无厌的怪物的方法,就是在“比较猜测”部分提到的parse:如果用户输入一个非数字回答,程序会崩溃。用户可以利用这一点来退出,如下所示:

+

如上所示,我们将提示用户猜测之后的所有内容放入了循环。确保这些代码额外缩进了一层,再次运行程序。注意这里有一个新问题,因为程序忠实地执行了我们的要求:永远地请求另一个猜测,用户没法退出!

+

用户总能使用 Ctrl-C 终止程序。不过还有另一个方法跳出无限循环,就是“比较猜测”部分提到的 parse:如果用户输入一个非数字答案,程序会崩溃。用户可以利用这一点来退出,如下所示:

$ cargo run
    Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
      Running `target/guessing_game`
@@ -487,11 +487,11 @@ thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidD
 note: Run with `RUST_BACKTRACE=1` for a backtrace.
 error: Process didn't exit successfully: `target/debug/guess` (exit code: 101)
 
-

输入quit确实退出了程序,同时其他任何非数字输入也一样。然而,毫不夸张的说这是不理想的。我们想要当猜测正确的数字时游戏能自动退出。

+

输入 quit 确实退出了程序,同时其他任何非数字输入也一样。然而,这并不理想,我们想要当猜测正确的数字时游戏能自动退出。

猜测正确后退出

-

让我们增加一个break来在用户胜利时退出游戏:

+

让我们增加一个 break,在用户猜对时退出游戏:

Filename: src/main.rs

-
extern crate rand;
+
extern crate rand;
 
 use std::io;
 use std::cmp::Ordering;
@@ -528,18 +528,18 @@ fn main() {
     }
 }
 
-

通过在You win!之后增加一行break,程序在用户猜对了神秘数字后会退出循环。退出循环也就意味着退出程序,因为循环是main的最后一部分。

+

通过在 You win! 之后增加一行 break,用户猜对了神秘数字后会退出循环。退出循环也意味着退出程序,因为循环是 main 的最后一部分。

处理无效输入

-

为了进一步改善游戏性,而不是在用户输入非数字时崩溃,需要让游戏忽略非数字从而用户可以继续猜测。可以通过修改guessString转化为u32那部分代码来实现:

-
let guess: u32 = match guess.trim().parse() {
+

为了进一步改善游戏性,不要在用户输入非数字时崩溃,需要忽略非数字,让用户可以继续猜测。可以通过修改 guessString 转化为 u32 那部分代码来实现:

+
let guess: u32 = match guess.trim().parse() {
     Ok(num) => num,
     Err(_) => continue,
 };
 
-

expect调用切换到match语句是如何从遇到错误就崩溃到真正处理错误的常用手段。记住parse返回一个Result类型,而Result是一个拥有OkErr两个成员的枚举。在这里使用match表达式,就像之前处理cmp方法返回的Ordering一样。

-

如果parse能够成功的将字符串转换为一个数字,它会返回一个包含结果数字Ok值。这个Ok值会匹配第一个分支的模式,这时match表达式仅仅返回parse产生的Ok值之中的num值。这个数字会最终如期变成新创建的guess变量。

-

如果parse能将字符串转换为一个数字,它会返回一个包含更多错误信息的Err值。Err值不能匹配第一个match分支的Ok(num)模式,但是会匹配第二个分支的Err(_)模式。_是一个包罗万象的值;在这个例子中,我们想要匹配所有Err值,不管其中有何种信息。所以程序会执行第二个分支的代码,continue,这意味着进入loop的下一次循环并请求另一个猜测。这样程序就有效地忽略了parse可能遇到的所有错误!

-

现在万事俱备(只欠东风)了。运行cargo run来尝试一下:

+

expect 调用换成 match 语句,是从“立即崩溃”转到真正处理错误的惯用方法。须知 parse 返回一个 Result 类型,而 Result 是一个拥有 OkErr 成员的枚举。这里使用的 match 表达式,和之前处理 cmp 方法返回 Ordering 时用的一样。

+

如果 parse 能够成功的将字符串转换为一个数字,它会返回一个包含结果数字的 Ok。这个 Ok 值与match 第一个分支的模式相匹配,该分支对应的动作返回 Ok 值中的数字 num,最后如愿变成新创建的 guess 变量。

+

如果 parse 能将字符串转换为一个数字,它会返回一个包含更多错误信息的 ErrErr 值不能匹配第一个 match 分支的 Ok(num) 模式,但是会匹配第二个分支的 Err(_) 模式:_ 是一个兜底值,用来匹配所有 Err 值,不管其中有何种信息。所以程序会执行第二个分支的动作,continue 意味着进入 loop 的下一次循环,请求另一个猜测。这样程序就忽略了 parse 可能遇到的所有错误!

+

现在万事俱备,只需运行 cargo run

$ cargo run
    Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
      Running `target/guessing_game`
@@ -560,9 +560,9 @@ Please input your guess.
 You guessed: 61
 You win!
 
-

太棒了!再有最后一个小的修改,就能完成猜猜看游戏了:还记得程序依然会打印出秘密数字。这在测试时还好,但会毁了游戏性。删掉打印秘密数字的println!。列表 2-5 为最终代码:

+

太棒了!再有最后一个小的修改,就能完成猜猜看游戏了:还记得程序依然会打印出秘密数字。在测试时还好,但正式发布时会毁了游戏。删掉打印秘密数字的 println!。列表 2-5 为最终代码:

Filename: src/main.rs

-
extern crate rand;
+
extern crate rand;
 
 use std::io;
 use std::cmp::Ordering;
@@ -602,7 +602,7 @@ fn main() {
 

Listing 2-5: Complete code of the guessing game

总结

此时此刻,你顺利完成了猜猜看游戏!恭喜!

-

这是一个通过动手实践的方式想你介绍许多 Rust 新知识的项目:letmatch、方法、关联函数,使用外部 crate,等等。接下来的几章,我们将会详细学习这些概念。第三章涉及到大部分编程语言都有的概念,比如变量、数据类型和函数,以及如何在 Rust 中使用他们。第四章探索所有权(ownership),这是一个 Rust 同其他语言都不相同的功能。第五章讨论结构体和方法的语法,而第六章侧重解释枚举。

+

这是一个通过动手实践学习 Rust 新概念的项目:letmatch、方法、关联函数、使用外部 crate 等等,接下来的几章,我们将会继续深入。第三章涉及到大部分编程语言都有的概念,比如变量、数据类型和函数,以及如何在 Rust 中使用他们。第四章探索所有权(ownership),这是一个 Rust 同其他语言都不相同的功能。第五章讨论结构体和方法的语法,而第六章侧重解释枚举。

diff --git a/docs/ch03-00-common-programming-concepts.html b/docs/ch03-00-common-programming-concepts.html index b21a412..ad3cb5d 100644 --- a/docs/ch03-00-common-programming-concepts.html +++ b/docs/ch03-00-common-programming-concepts.html @@ -73,11 +73,11 @@
commit 04aa3a45eb72855b34213703718f50a12a3eeec8

-

这一章涉及到几乎出现在所有编程语言中的概念,以及他们在 Rust 中是如何工作的。很多编程语言在核心概念上都是共通的。本章中展示的所有概念没有一个是 Rust 所特有的,不过我们会在 Rust 环境中讨论他们并解释他们的使用习惯。

-

具体的,我们将会学习变量,基本类型,函数,注释和控制流。这些基础知识将会出现在每一个 Rust 程序中,提早学习这些概念会使你在起步时拥有一个强有力的基础。

+

本章涉及一些几乎所有编程语言都有的概念,以及他们在 Rust 中是如何工作的。很多编程语言的核心概念都是共通的,本章中展示的概念都不是 Rust 特有的,不过我们会在 Rust 环境中讨论他们,解释他们的使用习惯。

+

具体的,我们将会学习变量,基本类型,函数,注释和控制流。这些基础知识将会出现在每一个 Rust 程序中,提早学习这些概念会使你拥有坚实的起步。

关键字

-

Rust 语言有一系列被保留为只能被语言使用的关键字keywords),如大部分语言一样。注意你不能使用这些关键字作为变量或函数的名称。大部分关键字有特殊的意义,并将会被用来进行 Rust 程序中的多种任务;一些关键字目前没有相关的功能不过为了将来可能添加进 Rust 的功能而被保留。可以在附录 A 中找到一份关键字的列表

+

Rust 语言有一系列保留的关键字keywords),只能由语言本身使用,像大部分语言一样。你不能使用这些关键字作为变量或函数的名称,大部分关键字有特殊的意义,并被用来完成 Rust 程序中的各种任务;一些关键字目前没有分配,是为将来可能添加的功能保留的。可以在附录 A 中找到关键字的列表。

diff --git a/docs/ch03-01-variables-and-mutability.html b/docs/ch03-01-variables-and-mutability.html index ab64150..c9fe257 100644 --- a/docs/ch03-01-variables-and-mutability.html +++ b/docs/ch03-01-variables-and-mutability.html @@ -73,8 +73,8 @@
commit 04aa3a45eb72855b34213703718f50a12a3eeec8

-

第二章中提到过,变量默认是不可变immutable)的。这是 Rust 中许多鼓励利用 Rust 提供的安全和简单并发优势编写代码的助力之一。不过,仍然有使变量可变的选项。让我们探索一下如何以及为什么鼓励你拥抱不可变性,还有为什么你可能想要弃之不用。

-

当变量是不可变时,这意味着一旦一个值被绑定上了一个名称,你就不能改变这个值。作为说明,通过cargo new --bin variablesprojects 目录生成一个叫做 variables 的新项目。

+

第二章中提到过,变量默认是不可变immutable)的。这是利用 Rust 安全和简单并发的优势编写代码一大助力。不过,变量仍然有可变的选项。让我们探讨一下,拥抱不可变性的原因及方法,以及何时你不想拥抱。

+

当变量不可变时,意味着一旦值被绑定上一个名称,你就不能改变这个值。作为说明,通过cargo new --bin variablesprojects 目录生成一个叫做 variables 的新项目。

接着,在新建的 variables 目录,打开 src/main.rs 并替换其代码为如下:

Filename: src/main.rs

fn main() {
@@ -96,10 +96,10 @@ error[E0384]: re-assignment of immutable variable `x`
 4 |     x = 6;
   |     ^^^^^ re-assignment of immutable variable
 
-

这个例子显示了编译器如何帮助你寻找程序中的错误。即便编译器错误可能是令人沮丧的,他们也仅仅意味着程序不能安全的完成你想让它完成的工作;他们不能说明你不是一个好的程序员!有经验的 Rustacean 们也会遇到编译器错误。这些错误表明错误的原因是对不可变变量重新赋值re-assignment of immutable variable),因为我们尝试对不可变变量x赋第二个值。

-

当尝试去改变之前设计为不可变的值出现编译时错误是很重要的,因为这种情况可能导致 bug。如果代码的一部分假设一个值永远也不会改变而另一部分代码改变了它,这样第一部分代码就有可能不能像它设计的那样运行。不得不承认这种 bug 难以跟踪,尤其是当第二部分代码只是有时当变量使不可变时。

-

Rust 编译器保证如果声明一个值不会改变,它就真的不会改变。这意味着当阅读和编写代码时,并不需要记录如何以及在哪可能会被改变,这使得代码易于推导。

-

不过可变性也是非常有用的。变量只是默认不可变;可以通过在变量名之前增加mut来使其可变。它向之后的读者表明了其他部分的代码将会改变这个变量值的意图。

+

这个例子展示了编译器如何帮助你找出程序中的错误。即便编译错误令人沮丧,那也不过是说程序不能安全的完成你想让它完成的工作;而不能说明你是不是一个好程序员!有经验的 Rustacean 们一样会遇到编译错误。这些错误给出的原因是对不可变变量重新赋值re-assignment of immutable variable),因为我们尝试对不可变变量x赋第二个值。

+

尝试去改变预设为不可变的值,产生编译错误是很重要的,因为这种情况可能导致 bug:如果代码的一部分假设一个值永远也不会改变,而另一部分代码改变了它,第一部分代码就有可能以不可预料的方式运行。不得不承认这种 bug 难以跟踪,尤其是第二部分代码只是有时改变其值。

+

Rust 编译器保证,如果声明一个值不会变,它就真的不会变。这意味着当阅读和编写代码时,不需要厘清如何以及哪里可能会被改变,从而使得代码易于推导。

+

不过可变性也是非常有用的。变量只是默认不可变;可以通过在变量名之前加 mut 来使其可变。它向读者表明了其他代码将会改变这个变量的意图。

例如,改变 src/main.rs 并替换其代码为如下:

Filename: src/main.rs

fn main() {
@@ -116,21 +116,21 @@ error[E0384]: re-assignment of immutable variable `x`
 The value of x is: 5
 The value of x is: 6
 
-

通过mut,允许把绑定到x的值从5改成6。在一些情况下,你会想要一个变量是可变的,因为这比只使用不可变变量实现的代码更易于编写。

-

除了避免 bug 外,这里还有多个需要权衡取舍的地方。例如,有时使用大型数据结构时,适当地使变量可变可能比复制和返回新分配的实例要更快。对于较小的数据结构,总是创建新实例并采用一种更函数式的编程风格可能会使代码更易理解。所以为了可读性而造成的性能惩罚也许是值得的。

+

通过 mut,允许把绑定到 x 的值从 5 改成 6。在一些情况下,你会想要一个变量可变,因为相对不可变的风格更容易写。

+

除了避免 bug 外,还有多处需要权衡取舍。例如,使用大型数据结构时,适当地使变量可变,可能比复制和返回新分配的实例更快。对于较小的数据结构,总是创建新实例,采用更偏向函数式的风格编程,可能会使代码更易理解,为可读性而遭受性能惩罚或许值得。

变量和常量的区别

-

不能改变一个变量的值可能会使你想起另一个大部分编程语言都有的概念:常量constants)。类似于不可变变量,常量也是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。

-

首先,不允许对常量使用mut:常量不光是默认不能改变,它总是不能改变。

-

常量使用const关键字而不是let关键字声明,而且必须注明值的类型。现在我们准备在下一部分,“数据类型”,涉及到类型和类型注解,所以现在无需担心这些细节,只需记住必须总是注明类型即可。

+

不允许改变值的变量,可能会使你想起另一个大部分编程语言都有的概念:常量constants)。类似于不可变变量,常量也是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。

+

首先,不允许对常量使用 mut:常量不光默认不能变,它总是不能变。

+

声明常量使用 const 关键字而不是 let,而且必须注明值的类型。在下一部分,“数据类型”,涉及到类型和类型注解,现在无需关心这些细节,记住总是标注类型即可。

常量可以在任何作用域声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。

-

最后一个区别是常量只能用于常量表达式,而不能作为函数调用的结果或任何其他只在运行时计算的值。

-

这是一个常量声明的例子,它的名称是MAX_POINTS而它的值是 100,000。(Rust 常量的命名规范是使用大写字母和单词间使用下划线):

+

最后一个区别是常量只能用于常量表达式,而不能作为函数调用的结果,或任何其他只在运行时计算的值。

+

这是一个常量声明的例子,它的名称是 MAX_POINTS,值是 100,000。(常量使用下划线分隔的大写字母命名):

const MAX_POINTS: u32 = 100_000;
 
-

常量在整个程序生命周期中都有效,位于它声明的作用域之中。这使得常量可以用作多个部分的代码可能需要知道的程序范围的值,例如一个游戏中任何玩家可以获得的最高分或者光速。

-

将用于整个程序的硬编码的值声明为为常量(并编写文档)对为将来代码维护者表明值的意义是很有用的。它也能帮助你将硬编码的值至于一处以便将来可能需要修改他们。

-

覆盖(Shadowing)

-

如第二章猜猜看游戏所讲到的,我们可以定义一个与之前变量名称相同的新变量,而新变量会覆盖之前的变量。Rustacean 们称其为第一个变量被第二个给覆盖了,这意味着第二个变量的值是使用这个变量时会看到的值。可以用相同变量名称来覆盖它自己以及重复使用let关键字来多次覆盖,如下所示:

+

常量在整个程序生命周期中都有效,位于它声明的作用域之中。这使得常量可以作为多处代码使用的全局范围的值,例如一个游戏中所有玩家可以获取的最高分或者光速。

+

将作用于整个程序的值,由硬编码改为常量(并编写文档),对后来的维护者了解值的意义很用帮助。它也能将硬编码的值汇总一处,为将来可能的修改提供方便。

+

遮盖(Shadowing)

+

如第二章“猜猜看游戏”所讲的,我们可以定义一个与之前变量重名的新变量,而新变量会遮盖之前的变量。Rustacean 称之为“第一个变量被第二个遮盖了”,这意味着使用这个变量时会看第二个值。可以用相同变量名称来遮盖它自己,以及重复使用 let 关键字来多次遮盖,如下所示:

Filename: src/main.rs

fn main() {
     let x = 5;
@@ -142,22 +142,22 @@ The value of x is: 6
     println!("The value of x is: {}", x);
 }
 
-

这个程序首先将x绑定到值5上。接着通过let x =覆盖x,获取原始值并加1这样x的值就变成6了。第三个let语句也覆盖了x,获取之前的值并乘以2x的最终值是12。当运行这个程序,它会有如下输出:

+

这个程序首先将 x 绑定到值 5 上。接着通过 let x = 遮盖 x,获取原始值并加 1 这样 x 的值就变成 6 了。第三个 let 语句也覆盖了 x,获取之前的值并乘以 2x 最终的值是 12。运行这个程序,它会有如下输出:

$ cargo run
    Compiling variables v0.1.0 (file:///projects/variables)
      Running `target/debug/variables`
 The value of x is: 12
 
-

这与将变量声明为mut是有区别的。因为除非再次使用let关键字,不小心尝试对变量重新赋值会导致编译时错误。我们可以用这个值进行一些计算,不过计算完之后变量仍然是不变的。

-

另一个mut与覆盖的区别是当再次使用let关键字时,事实上创建了一个新变量,我们可以改变值的类型。例如,假设程序请求用户输入空格来提供在一些文本之间需要多少空间来分隔,不过我们真正需要的是将输入存储成数字(多少个空格):

+

这与将变量声明为 mut 是有区别的。因为除非再次使用 let 关键字,不小心尝试对变量重新赋值会导致编译时错误。我们可以用这个值进行一些计算,不过计算完之后变量仍然是不变的。

+

mut 与遮盖的另一个区别是,当再次使用 let 时,实际上创建了一个新变量,我们可以改变值的类型。例如,假设程序请求用户输入空格来提供文本间隔,然而我们真正需要的是将输入存储成数字(多少个空格):

let spaces = "   ";
 let spaces = spaces.len();
 
-

这里允许第一个spaces变量是字符串类型,而第二个spaces变量,它是一个恰巧与第一个变量名字相同的崭新的变量,它是数字类型。因此覆盖使我们不必使用不同的名字,比如spaces_strspaces_num;相反,我们可以复用spaces这个更简单的名称。然而,如果尝试使用mut,如下所示:

+

这里允许第一个 spaces 变量是字符串类型,而第二个 spaces 变量,它是一个恰巧与第一个变量同名的崭新变量,是数字类型。遮盖使我们不必使用不同的名字,如 spaces_strspaces_num;相反,我们可以复用 spaces 这个更简单的名字。然而,如果尝试使用mut,如下所示:

let mut spaces = "   ";
 spaces = spaces.len();
 
-

会导致一个编译时错误,因为不允许改变一个变量的类型:

+

会导致一个编译错误,因为改变一个变量的类型是不被允许的:

error[E0308]: mismatched types
  --> src/main.rs:3:14
   |
diff --git a/docs/ch03-02-data-types.html b/docs/ch03-02-data-types.html
index fb4f079..ff3c83b 100644
--- a/docs/ch03-02-data-types.html
+++ b/docs/ch03-02-data-types.html
@@ -73,11 +73,11 @@
 
commit fe4833a8ef2853c55424e7747a4ef8dd64c35b32

-

Rust 中的任何值都有一个具体的类型type),这告诉了 Rust 它被指定了何种数据,这样 Rust 就知道如何处理这些数据了。这一部分将讲到一些语言内建的类型。我们将这些类型分为两个子集:标量(scalar)和复合(compound)。

-

贯穿整个部分,请记住 Rust 是一个静态类型statically typed)语言,也就是说必须在编译时就知道所有变量的类型。编译器通常可以通过值以及如何使用他们来推断出我们想要用的类型。当多个类型都是可能的时候,比如第二章中parseString转换为数字类型,必须增加类型注解,像这样:

+

在 Rust 中,任何值都属于一种明确的类型type),声明它被指定了何种数据,以便明确其处理方式。我们将分两部分探讨一些内建类型:标量(scalar)和复合(compound)。

+

Rust 是静态类型statically typed)语言,也就是说在编译时就需要知道所有变量的类型,这一认知将贯穿整个章节,请在头脑中明确。通过值的形式及其使用方式,编译器通常可以推断出我们想要用的类型。多种类型均有可能时,比如第二章中使用 parseString 转换为数字,必须增加类型注解,像这样:

let guess: u32 = "42".parse().expect("Not a number!");
 
-

如果这里不添加类型注解,Rust 会显示如下错误,它意味着编译器需要我们提供更多我们想要使用哪个可能的类型的信息:

+

如果不添加类型注解,Rust 会显示如下错误。这说明编译器需要更多信息,来了解我们想要的类型:

error[E0282]: unable to infer enough type information about `_`
  --> src/main.rs:2:9
   |
@@ -86,11 +86,11 @@ commit fe4833a8ef2853c55424e7747a4ef8dd64c35b32

| = note: type annotations or generic parameter binding required
-

在我们讨论各种数据类型时会看到不同的类型注解。

+

在我们讨论各种数据类型时,你会看到多样的类型注解。

标量类型

标量类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型。你可能在其他语言中见过他们,不过让我们深入了解他们在 Rust 中时如何工作的。

整型

-

整数是一个没有小数部分的数字。我们在这一章的前面使用过一个整型,i32类型。这个类型声明表明在 32 位系统上它关联的值应该是一个有符号整数(因为这个i,与u代表的无符号相对)。表格 3-1 展示了 Rust 内建的整数类型。每一个变体的有符号和无符号列(例如,i32)可以用来声明对应的整数值。

+

整数是一个没有小数部分的数字。我们在这一章的前面使用过 i32 类型。该类型声明指示,i32 关联的值应该是一个占据32比特位的有符号整数(因为这个i,与u代表的无符号相对)。表格 3-1 展示了 Rust 内建的整数类型。每一种变体的有符号和无符号列(例如,i32)可以用来声明对应的整数值。

Table 3-1: Integer Types in Rust

@@ -99,10 +99,10 @@ commit fe4833a8ef2853c55424e7747a4ef8dd64c35b32

Length Signed Unsigned
8-bit i8 u8
64-bit i64 u64
arch isize usize
-

每一种变体都可以是有符号或无符号的并有一个显式的大小。有符号和无符号代表数字是否能够是正数或负数;换句话说,数字是否需要有一个符号(有符号数)或者永远只需要是正的这样就可以不用符号(无符号数)。这有点像在纸上书写数字:当需要考虑符号的时候,数字前面会加上一个加号或减号;然而,当可以安全地假设为正数时,可以不带符号(加号)。有符号数以二进制补码形式(two’s complement representation)存储(如果你不清楚这是什么,可以在网上搜索;对其的解释超出了本书的范畴)。

-

每一个有符号的变体可以储存包含从 -(2n - 1) 到 2n - 1 - 1 在内的数字,这里n是变体使用的位数。所以i8可以储存从 -(27) 到 27 - 1 在内的数字,也就是从 -128 到 127。无符号的变体可以储存从 0 到 2n - 1 的数字,所以u8可以储存从 0 到 28 - 1 的数字,也就是从 0 到 255。

-

另外,isizeusize类型依赖运行程序的计算机类型(构架):64 位构架他们是 64 位的而 32 位构架他们就是 32 位的。

-

可以使用表格 3-2 中的任何一种形式编写数字字面值。注意除了字节字面值以外的数字字面值允许使用类型后缀,例如57u8,而_是可视化分隔符(visual separator),例如1_000位的。

+

每一种变体都可以是有符号或无符号的,并有一个明确的大小。有符号和无符号代表数字能否为负值;换句话说,数字是否需要有一个符号(有符号数),或者永远为正而不需要符号(无符号数)。这有点像在纸上书写数字:当需要考虑符号的时候,数字以加号或减号作为前缀;然而,可以安全地假设为正数时,加号前缀通常省略。有符号数以二进制补码形式(two’s complement representation)存储(如果你不清楚这是什么,可以在网上搜索;对其的解释超出了本书的范畴)。

+

每一个有符号的变体可以储存包含从 -(2n - 1) 到 2n - 1 - 1 在内的数字,这里 n 是变体使用的位数。所以 i8 可以储存从 -(27) 到 27 - 1 在内的数字,也就是从 -128 到 127。无符号的变体可以储存从 0 到 2n - 1 的数字,所以 u8 可以储存从 0 到 28 - 1 的数字,也就是从 0 到 255。

+

另外,isizeusize 类型依赖运行程序的计算机架构:64 位架构上他们是 64 位的, 32 位架构上他们是 32 位的。

+

可以使用表格 3-2 中的任何一种形式编写数字字面值。除字节以外的其它字面值允许使用类型后缀,例如 57u8,允许使用 _ 做为分隔符以方便读数,例如 1_000 (分隔符的数量与位置并不影响实际的数字)。

Table 3-2: Integer Literals in Rust

@@ -111,9 +111,9 @@ commit fe4833a8ef2853c55424e7747a4ef8dd64c35b32

Number literals Example
Decimal 98_222
Binary 0b1111_0000
Byte (u8 only) b'A'
-

那么如何知晓该使用哪种类型的数字呢?如果对此拿不定主意,Rust 的默认类型通常就是一个很好的选择,这个默认数字类型是i32:它通常是最快的,甚至是在 64 位系统上。使用isizeusize的主要场景是索引一些集合。

+

那么该使用哪种类型的数字呢?如果拿不定主意,Rust 的默认类型通常就很好,数字类型默认是 i32:它通常是最快的,甚至在 64 位系统上也是。isizeusize 的主要作为集合的索引。

浮点型

-

Rust 也有两个主要的浮点数floating-point numbers)类型,他们是有小数点的数字。Rust 的浮点数类型是f32f64,分别是 32 位 和 64 位大小。默认类型是f64,因为它基本上与f32一样快不过精度更高。在 32 位系统上使用f64是可能的,不过会比f32要慢。大部分情况,牺牲潜在可能的更低性能来换取更高的精度是一个合理的初始选择,同时如果怀疑浮点数的大小有问题的时候应该先对代码进行性能测试。

+

Rust 同样有两个主要的浮点数类型,f32f64,它们是带小数点的数字,分别占 32 位和 64 位比特。默认类型是 f64,因为它与 f32 速度差不多,然而精度更高。在 32 位系统上也能够使用 f64,不过比使用 f32 要慢。多数情况下,以潜在的性能损耗换取更高的精度是合理的;如果觉得浮点数的大小是个麻烦,你应该以性能测试作为决策依据。

这是一个展示浮点数的实例:

Filename: src/main.rs

fn main() {
@@ -168,7 +168,7 @@ commit fe4833a8ef2853c55424e7747a4ef8dd64c35b32

复合类型可以将多个其他类型的值组合成一个类型。Rust 有两个原生的复合类型:元组(tuple)和数组(array)。

将值组合进元组

元组是一个将多个其他类型的值组合进一个复合类型的主要方式。

-

我们使用一个括号中的逗号分隔的值列表来创建一个元组。元组中的每一个位置都有一个类型,而且这写不同值的类型也不必是相同的。这个例子中使用了额外的可选类型注解:

+

我们使用一个括号中的逗号分隔的值列表来创建一个元组。元组中的每一个位置都有一个类型,而且这些不同值的类型也不必是相同的。这个例子中使用了额外的可选类型注解:

Filename: src/main.rs

fn main() {
     let tup: (i32, f64, u8) = (500, 6.4, 1);
diff --git a/docs/ch04-01-what-is-ownership.html b/docs/ch04-01-what-is-ownership.html
index 329d79b..9aabbc6 100644
--- a/docs/ch04-01-what-is-ownership.html
+++ b/docs/ch04-01-what-is-ownership.html
@@ -142,7 +142,7 @@ println!("{}", s); // This will print `hello, world!`
 
  • 需要一个当我们处理完String时将内存返回给操作系统的方法
  • 第一部分由我们完成:当调用String::from时,它的实现请求它需要的内存。这在编程语言中是非常通用的。

    -

    然而,第二部分实现起来就各有区别了。在有**垃圾回收(GC)**的语言中, GC 记录并清除不再使用的内存,而我们作为程序员,并不需要关心他们。没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们程序员的责任了,正如请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要allocatefree一一对应。

    +

    然而,第二部分实现起来就各有区别了。在有垃圾回收GC)的语言中, GC 记录并清除不再使用的内存,而我们作为程序员,并不需要关心他们。没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们程序员的责任了,正如请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要allocatefree一一对应。

    Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。下面是列表 4-1 中作用域例子的一个使用String而不是字符串字面值的版本:

    {
         let s = String::from("hello"); // s is valid from this point forward
    diff --git a/docs/ch04-02-references-and-borrowing.html b/docs/ch04-02-references-and-borrowing.html
    index 508979f..9f8265c 100644
    --- a/docs/ch04-02-references-and-borrowing.html
    +++ b/docs/ch04-02-references-and-borrowing.html
    @@ -203,7 +203,7 @@ immutable
     

    哇哦!我们不能在拥有不可变引用的同时拥有可变引用。不可变引用的用户可不希望在它的眼皮底下值突然就被改变了!然而,多个不可变引用是没有问题的因为没有哪个读取数据的人有能力影响其他人读取到的数据。

    即使这些错误有时是使人沮丧的。记住这是 Rust 编译器在提早指出一个潜在的 bug(在编译时而不是运行时)并明确告诉你问题在哪而不是任由你去追踪为何有时数据并不是你想象中的那样。

    悬垂引用

    -

    在存在指针的语言中,容易通过释放内存时保留指向它的指针而错误地生成一个悬垂指针dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者,。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当我们拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。

    +

    在存在指针的语言中,容易通过释放内存时保留指向它的指针而错误地生成一个悬垂指针dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当我们拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。

    让我们尝试创建一个悬垂引用:

    Filename: src/main.rs

    fn main() {
    diff --git a/docs/ch16-04-extensible-concurrency-sync-and-send.html b/docs/ch16-04-extensible-concurrency-sync-and-send.html
    index 2ab691d..61b488b 100644
    --- a/docs/ch16-04-extensible-concurrency-sync-and-send.html
    +++ b/docs/ch16-04-extensible-concurrency-sync-and-send.html
    @@ -77,11 +77,11 @@ commit 9430a3d28a2121a938d704ce48b15d21062f880e

    我们说“几乎所有内容都不属于语言本身”,那么属于语言本身的是什么呢?是两个 trait,都位于std::markerSyncSend

    Send用于表明所有权可能被传送给其他线程

    Send标记 trait 表明类型的所有权可能被在线程间传递。几乎所有的 Rust 类型都是Send的,不过有一些例外。比如标准库中提供的 Rc<T>:如果克隆Rc<T>值,并尝试将克隆的所有权传递给另一个线程,这两个线程可能会同时更新引用计数。正如上一部分提到的,Rc<T>被实现为用于单线程场景,这时不需要为拥有线程安全的引用计数而付出性能代价。

    -

    因为Rc<T>没有标记为Send,Rust 的类型系统和 trait bound 会确保我们不会错误的把一个Rc<T>值不安全的在线程间传递。列表 16-14 曾尝试这么做,不过得到了一个错误,the trait Send is not implemented for Rc<Mutex<i32>>。当切换为标记为SendArc<T>时,就没有问题了。

    -

    任何完全由Send的类型组成的类型也会自动被标记为Send。几乎所有基本类型都是Send的,大部分标准库类型是Send的,除了Rc<T>,以及第十九章将会讨论的裸指针(raw pointer)。

    -

    Sync表明多线程访问是安全的

    -

    Sync标记 trait 表明一个类型可以安全的在多个线程中拥有其值的引用。换一种方式来说就是,对于任意类型T,如果&TT的引用)是Send的话T就是Sync的,这样其引用就可以安全的发送到另一个线程。类似于Send的情况,基本类型是Sync的,完全由Sync的类型组成的类型也是Sync的。

    -

    Rc<T>也不是Sync的,出于其不是Send的相同的原因。RefCell<T>(第十五章讨论过)和Cell<T>系列类型不是Sync的。RefCell<T>在运行时所进行的借用检查也不是线程安全的。Mutex<T>Sync的,正如上一部分所讲的它可以被用来在多线程中共享访问。

    +

    因为 Rc<T> 没有标记为 Send,Rust 的类型系统和 trait bound 会确保我们不会错误的把一个 Rc<T> 值不安全的在线程间传递。列表 16-14 曾尝试这么做,不过得到了一个错误,the trait Send is not implemented for Rc<Mutex<i32>>。而使用标记为 SendArc<T> 时,就没有问题了。

    +

    任何完全由 Send 的类型组成的类型也会自动被标记为 Send:几乎所有基本类型都是 Send 的,大部分标准库类型是Send的,除了Rc<T>,以及第十九章将会讨论的裸指针(raw pointer)。

    +

    Sync 表明多线程访问是安全的

    +

    Sync 标记 trait 表明一个类型可以安全的在多个线程中拥有其值的引用。换一种方式来说,对于任意类型 T,如果&TT的引用)是Send的话T就是Sync的,这样其引用就可以安全的发送到另一个线程。类似于 Send 的情况,基本类型是 Sync 的,完全由 Sync 的类型组成的类型也是 Sync 的。

    +

    Rc<T> 也不是 Sync 的,出于其不是Send的相同的原因。RefCell<T>(第十五章讨论过)和Cell<T>系列类型不是Sync的。RefCell<T>在运行时所进行的借用检查也不是线程安全的。Mutex<T>Sync的,正如上一部分所讲的它可以被用来在多线程中共享访问。

    手动实现SendSync是不安全的

    通常并不需要实现SendSync trait,由属于SendSync的类型组成的类型,自动就是SendSync的。因为他们是标记 trait,甚至都不需要实现任何方法。他们只是用来加强并发相关的不可变性的。

    实现这些标记 trait 涉及到编写不安全的 Rust 代码,第十九章将会讲述具体的方法;当前重要的是,在创建新的由不是SendSync的部分构成的并发类型时需要多加小心,以确保维持其安全保证。The Nomicon 中有更多关于这些保证以及如何维持他们的信息。

    diff --git a/docs/ch17-02-trait-objects.html b/docs/ch17-02-trait-objects.html index 9afdcfd..5c2cda2 100644 --- a/docs/ch17-02-trait-objects.html +++ b/docs/ch17-02-trait-objects.html @@ -77,22 +77,22 @@ commit 67876e3ef5323ce9d394f3ea6b08cb3d173d9ba9

    -

    有时,我们希望使用的类型的集合对于使用库的程序员来说是可扩展的。例如,很多图形用户接口(GUI)工具有一个条目列表的概念,它通过遍历列表并对每一个条目调用draw方法来绘制在屏幕上。我们将要创建一个叫做rust_gui的包含一个 GUI 库结构的库 crate。GUI 库可以包含一些供开发者使用的类型,比如ButtonTextField。使用rust_gui的程序员会想要创建更多可以绘制在屏幕上的类型:一个程序员可能会增加一个Image,而另一个可能会增加一个SelectBox。我们不会在本章节实现一个功能完善的 GUI 库,不过会展示各个部分是如何结合在一起的。

    +

    有时,我们希望使用的类型的集合对于使用库的程序员来说是可扩展的。例如,很多图形用户接口(GUI)工具有一个条目列表的概念,它通过遍历列表并对每一个条目调用 draw 方法来绘制在屏幕上。我们将要创建一个叫做 rust_gui 的包含一个 GUI 库结构的库 crate。GUI 库可以包含一些供开发者使用的类型,比如 ButtonTextField。使用 rust_gui 的程序员会想要创建更多可以绘制在屏幕上的类型:一个程序员可能会增加一个 Image,而另一个可能会增加一个 SelectBox。我们不会在本章节实现一个功能完善的 GUI 库,不过会展示各个部分是如何结合在一起的。

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

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

    定义一个带有自定义行为的Trait

    不过,在Rust语言中,我们可以定义一个 Draw trait,包含名为 draw 的方法。我们定义一个由trait对象组成的vector,绑定了某种指针的trait,比如&引用或者一个Box<T>智能指针。

    -

    之前提到,我们不会称结构体和枚举为对象,以区分其他语言的结构体和枚举对象。结构体或者枚举成员中的数据和impl块中的行为是分开的,而其他语言则是数据和行为被组合到一个对象里。Trait 对象更像其他语言的对象,因为他们将其指针指向的具体对象作为数据,将在trait 中定义的方法作为行为,组合在了一起。但是,trait 对象和其他语言是不同的,我们不能向一个 trait 对象增加数据。trait 对象不像其他语言那样有用:它们的目的是允许从公有行为上抽象。

    -

    trait 对象定义了给定情况下应有的行为。当需要具有某种特性的不确定具体类型时,我们可以把 trait 对象当作 trait 使用。Rust 的类型系统会保证我们为 trait 对象带入的任何值会实现 trait 的方法。我们不需要在编译阶段知道所有可能的类型,却可以把所有的实例统一对待。Listing 17-03展示了如何定义一个名为Draw的带有draw方法的trait。

    -

    Filename: src/lib.rs

    +

    之前提到,我们不会称结构体和枚举为对象,以区分其他语言的结构体和枚举对象。结构体或者枚举成员中的数据和impl块中的行为是分开的,而其他语言则是数据和行为被组合到一个对象里。Trait 对象更像其他语言的对象,因为他们将其指针指向的具体对象作为数据,将在 trait 中定义的方法作为行为,组合在了一起。但是,trait 对象和其他语言是不同的,我们不能向一个 trait 对象增加数据。trait 对象不像其他语言那样有用:它们的目的是允许从公有行为上抽象。

    +

    trait 对象定义了给定情况下应有的行为。当需要具有某种特性的不确定具体类型时,我们可以把 trait 对象当作 trait 使用。Rust 的类型系统会保证我们为 trait 对象带入的任何值会实现 trait 的方法。我们不需要在编译阶段知道所有可能的类型,却可以把所有的实例统一对待。列表 17-03 展示了如何定义一个名为Draw的带有draw方法的 trait。

    +

    文件名: src/lib.rs

    pub trait Draw {
         fn draw(&self);
     }
     
    -

    Listing 17-3:Draw trait的定义

    +

    列表 17-3:Draw trait 的定义

    -

    因为我们已经在第10章讨论过如何定义 trait,你可能比较熟悉。下面是新的定义:Listing 17-4有一个名为 Screen 的结构体,里面有一个名为 components 的 vector,components 的类型是BoxBox<Draw> 是一个 trait 对象:它是 Box 内部任意一个实现了 Draw trait 的类型的替身。

    -

    Filename: src/lib.rs

    +

    因为我们已经在第十章讨论过如何定义 trait,你可能比较熟悉。下面是新的定义:列表 17-4 有一个名为 Screen 的结构体,里面有一个名为 components 的 vector,components 的类型是 Box<Draw>Box<Draw> 是一个 trait 对象:它是 Box 内部任意一个实现了 Draw trait 的类型的替身。

    +

    文件名: src/lib.rs

    # pub trait Draw {
     #     fn draw(&self);
     # }
    @@ -101,10 +101,9 @@ pub struct Screen {
         pub components: Vec<Box<Draw>>,
     }
     
    -

    Listing 17-4: 定义一个 Screen 结构体,带有一个含有实现了 Draw trait 的 components vector 成员

    -

    -

    Screen 结构体上,我们将要定义一个 run 方法,该方法会在它的 components 上调用 draw 方法,如Listing 17-5所示:

    -

    Filename: src/lib.rs

    +

    列表 17-4: 一个 Screen 结构体的定义,它带有一个字段components,其包含实现了 Draw trait 的 trait 对象的 vector

    +

    Screen 结构体上,我们将要定义一个 run 方法,该方法会在它的 components 上的每一个元素调用 draw 方法,如列表 17-5 所示:

    +

    文件名: src/lib.rs

    # pub trait Draw {
     #     fn draw(&self);
     # }
    @@ -121,10 +120,10 @@ impl Screen {
         }
     }
     
    -

    Listing 17-5:在 Screen 上实现一个 run 方法,该方法在每个组件上调用 draw 方法 +

    列表 17-5:在 Screen 上实现一个 run 方法,该方法在每个 component 上调用 draw 方法

    -

    这与带 trait 约束的泛型结构体不同(trait 约束泛型参数)。泛型参数一次只能被一个具体类型替代,而 trait 对象可以在运行时允许多种具体类型填充 trait 对象。比如,我们已经定义了 Screen 结构体使用泛型和一个 trait 约束,如Listing 17-6所示:

    -

    Filename: src/lib.rs

    +

    这与带 trait 约束的泛型结构体不同(trait 约束泛型参数)。泛型参数一次只能被一个具体类型替代,而 trait 对象可以在运行时允许多种具体类型填充 trait 对象。比如,我们已经定义了 Screen 结构体使用泛型和一个 trait 约束,如列表 17-6 所示:

    +

    文件名: src/lib.rs

    # pub trait Draw {
     #     fn draw(&self);
     # }
    @@ -142,13 +141,13 @@ impl<T> Screen<T>
         }
     }
     
    -

    Listing 17-6: 一种 Screen 结构体的替代实现,它的 run 方法使用通用类型和 trait 绑定 +

    列表 17-6: 一种 Screen 结构体的替代实现,它的 run 方法使用通用类型和 trait 绑定

    这个例子中,Screen 实例所有组件类型必需全是 Button,或者全是 TextField。如果你的组件集合是单一类型的,那么可以优先使用泛型和 trait 约束,因为其使用的具体类型在编译阶段即可确定。

    Screen 结构体内部的 Vec<Box<Draw>> trait 对象列表,则可以同时包含 Box<Button>Box<TextField>。我们看它是怎么工作的,然后讨论运行时性能。

    来自我们或者库使用者的实现

    -

    现在,我们增加一些实现了 Draw trait 的类型,再次提供 Button。实现一个 GUI 库实际上超出了本书的范围,因此 draw 方法留空。为了想象实现可能的样子,Button 结构体有 widthheightlabel字段,如Listing 17-7所示:

    -

    Filename: src/lib.rs

    +

    现在,我们增加一些实现了 Draw trait 的类型,再次提供 Button。实现一个 GUI 库实际上超出了本书的范围,因此 draw 方法留空。为了想象实现可能的样子,Button 结构体有 widthheightlabel字段,如列表 17-7 所示:

    +

    文件名: src/lib.rs

    # pub trait Draw {
     #     fn draw(&self);
     # }
    @@ -165,11 +164,11 @@ impl Draw for Button {
         }
     }
     
    -

    Listing 17-7: 实现了Draw trait的Button 结构体

    +

    列表 17-7: 实一个现了Draw trait 的 Button 结构体

    Button 上的 widthheightlabel 会和其他组件不同,比如 TextField 可能有 widthheight, label 以及 placeholder 字段。每个我们可以在屏幕上绘制的类型都会实现 Draw trait,在 draw 方法中使用不同的代码,定义了如何绘制 Button。除了 Draw trait,Button 也可能有一个 impl 块,包含按钮被点击时的响应方法。这类方法不适用于 TextField 这样的类型。

    -

    假定我们的库的用户相要实现一个包含 widthheightoptionsSelectBox 结构体。同时也在 SelectBox 类型上实现了 Draw trait,如 Listing 17-8所示:

    -

    Filename: src/main.rs

    +

    假定我们的库的用户相要实现一个包含 widthheightoptionsSelectBox 结构体。同时也在 SelectBox 类型上实现了 Draw trait,如 列表 17-8 所示:

    +

    文件名: src/main.rs

    extern crate rust_gui;
     use rust_gui::Draw;
     
    @@ -185,10 +184,10 @@ impl Draw for SelectBox {
         }
     }
     
    -

    Listing 17-8: 另外一个 crate 中,在 SelectBox 结构体上使用 rust_gui 和实现了Draw trait +

    列表 17-8: 另外一个 crate 中,在 SelectBox 结构体上使用 rust_gui 和实现了Draw trait

    -

    库的用户现在可以在他们的 main 函数中创建一个 Screen 实例,然后把自身放入 Box<T> 变成 trait 对象,向 screen 增加 SelectBoxButton。他们可以在这个 Screen 实例上调用 run 方法,这又会调用每个组件的 draw 方法。 Listing 17-9 展示了实现:

    -

    Filename: src/main.rs

    +

    库的用户现在可以在他们的 main 函数中创建一个 Screen 实例,然后把自身放入 Box<T> 变成 trait 对象,向 screen 增加 SelectBoxButton。他们可以在这个 Screen 实例上调用 run 方法,这又会调用每个组件的 draw 方法。 列表 17-9 展示了实现:

    +

    文件名: src/main.rs

    use rust_gui::{Screen, Button};
     
     fn main() {
    @@ -214,13 +213,13 @@ fn main() {
         screen.run();
     }
     
    -

    Listing 17-9: 使用 trait 对象来存储实现了相同 trait 的不同类型 +

    列表 17-9: 使用 trait 对象来存储实现了相同 trait 的不同类型

    虽然我们不知道哪一天会有人增加 SelectBox 类型,但是我们的 Screen 能够操作 SelectBox 并绘制它,因为 SelectBox 实现了 Draw 类型,这意味着它实现了 draw 方法。

    只关心值的响应,而不关心其具体类型,这类似于动态类型语言中的 duck typing:如果它像鸭子一样走路,像鸭子一样叫,那么它就是只鸭子!在 Listing 17-5 Screenrun 方法实现中,run 不需要知道每个组件的具体类型。它也不检查组件是 Button 还是 SelectBox 的实例,只管调用组件的 draw 方法。通过指定 Box<Draw> 作为 components 列表中元素的类型,我们约束了 Screen 需要这些实现了 draw 方法的值。

    Rust 类型系统使用 trait 对象来支持 duck typing 的好处是,我们无需在运行时检查一个值是否实现了特定方法,或是担心调用了一个值没有实现的方法。如果值没有实现 trait 对象需要的 trait(方法),Rust 不会编译。

    -

    比如,Listing 17-10 展示了当我们创建一个使用 String 做为其组件的 Screen 时发生的情况:

    -

    Filename: src/main.rs

    +

    比如,列表 17-10 展示了当我们创建一个使用 String 做为其组件的 Screen 时发生的情况:

    +

    文件名: src/main.rs

    extern crate rust_gui;
     use rust_gui::Draw;
     
    @@ -234,10 +233,10 @@ fn main() {
         screen.run();
     }
     
    -

    Listing 17-10: 尝试使用一种没有实现 trait 对象的类型

    +

    列表 17-10: 尝试使用一种没有实现 trait 对象的类型

    我们会遇到这个错误,因为 String 没有实现 Draw trait:

    -
    error[E0277]: the trait bound `std::string::String: Draw` is not satisfied
    +
    error[E0277]: the trait bound `std::string::String: Draw` is not satisfied
       -->
        |
      4 |             Box::new(String::from("Hi")),
    @@ -248,8 +247,8 @@ fn main() {
     

    这个错误告诉我们,要么传入 Screen 需要的类型,要么在 String 上实现 Draw,以便 Screen 调用它的 draw 方法。

    Trait 对象执行动态分发

    -

    回忆一下第10章我们讨论过的,当我们在泛型上使用 trait 约束时,编译器按单态类型处理:在需要使用范型参数的地方,编译器为每个具体类型生成非泛型的函数和方法实现。单态类型处理产生的代码实际就是做 static dispatch:方法的代码在编译阶段就已经决定了,当调用时,寻找那段代码非常快速。

    -

    当我们使用 trait 对象,编译器不能按单态类型处理,因为无法知道使用代码的所有可能类型。而是调用方法的时候,Rust 跟踪可能被使用的代码,在运行时找出调用该方法时应使用的代码。这也是我们熟知的 dynamic dispatch,查找过程会产生运行时开销。动态分发也会阻止编译器内联函数,失去一些优化途径。尽管获得了额外的灵活性,但仍然需要权衡取舍。

    +

    回忆一下第十章我们讨论过的,当我们在泛型上使用 trait 约束时,编译器按单态类型处理:在需要使用范型参数的地方,编译器为每个具体类型生成非泛型的函数和方法实现。单态类型处理产生的代码实际就是做 static dispatch:方法的代码在编译阶段就已经决定了,当调用时,寻找那段代码非常快速。

    +

    当我们使用 trait 对象,编译器不能按单态类型处理,因为无法知道使用代码的所有可能类型。而是调用方法的时候,Rust 跟踪可能被使用的代码,在运行时找出调用该方法时应使用的代码。这也是我们熟知的 dynamic dispatch,查找过程会产生运行时开销。动态分发也会阻止编译器内联函数,失去一些优化途径。尽管获得了额外的灵活性,但仍然需要权衡取舍。

    Trait 对象需要对象安全

    -

    Not all traits can be made into trait objects; only object safe traits can. A -trait is object safe as long as both of the following are true:

    +

    不是所有的 trait 都可以被放进 trait 对象中; 只有对象安全的object safe)trait 才可以这样做. 一个 trait 只有同时满足如下两点时才被认为是对象安全的:

      -
    • The trait does not require Self to be Sized
    • -
    • All of the trait's methods are object safe.
    • +
    • 该 trait 要求 Self 不是 Sized;
    • +
    • 该 trait 的所有方法都是对象安全的;
    -

    Self is a keyword that is an alias for the type that we're implementing -traits or methods on. Sized is a marker trait like the Send and Sync -traits that we talked about in Chapter 16. Sized is automatically implemented -on types that have a known size at compile time, such as i32 and references. -Types that do not have a known size include slices ([T]) and trait objects.

    -

    Sized is an implicit trait bound on all generic type parameters by default. -Most useful operations in Rust require a type to be Sized, so making Sized -a default requirement on trait bounds means we don't have to write T: Sized -with most every use of generics. If we want to be able to use a trait on -slices, however, we need to opt out of the Sized trait bound, and we can do -that by specifying T: ?Sized as a trait bound.

    -

    Traits have a default bound of Self: ?Sized, which means that they can be -implemented on types that may or may not be Sized. If we create a trait Foo -that opts out of the Self: ?Sized bound, that would look like the following:

    +

    Self 是一个类型的别名关键字,它表示当前正被实现的 trait 类型或者是方法所属的类型. Sized是一个像在第十六章中介绍的SendSync那样的标记 trait, 在编译时它会自动被放进大小确定的类型里,比如i32和引用. 大小不确定的类型有 slice([T])和 trait 对象.

    +

    Sized 是一个默认会被绑定到所有常规类型参数的内隐 trait. Rust 中要求一个类型是Sized的最具可用性的用法是让Sized成为一个默认的 trait 绑定,这样我们就可以在大多数的常规的用法中不去写 T: Sized 了. 如果我们想在切片(slice)中使用一个 trait, 我们需要取消对Sized的 trait 绑定, 我们只需制定T: ?Sized作为 trait 绑定.

    +

    默认绑定到 Self: ?Sized 的 trait 可以被实现到是 Sized 或非 Sized 的类型上. 如果我们创建一个不绑定 Self: ?Sized 的 trait Foo,它看上去应该像这样:

    trait Foo: Sized {
         fn some_method(&self);
     }
     
    -

    The trait Sized is now a super trait of trait Foo, which means trait -Foo requires types that implement Foo (that is, Self) to be Sized. -We're going to talk about super traits in more detail in Chapter 19.

    -

    The reason a trait like Foo that requires Self to be Sized is not allowed -to be a trait object is that it would be impossible to implement the trait -Foo for the trait object Foo: trait objects aren't sized, but Foo -requires Self to be Sized. A type can't be both sized and unsized at the -same time!

    -

    For the second object safety requirement that says all of a trait's methods -must be object safe, a method is object safe if either:

    +

    Trait Sized现在就是 trait Foo的一个超级 traitsupertrait), 也就是说 trait Foo 需要实现了 Foo 的类型(即Self)是Sized. 我们将在第十九章中更详细的介绍超 trait(supertrait).

    +

    Foo那样要求SelfSized的 trait 不允许成为 trait 对象的原因是不可能为 trait 对象Foo实现 trait Foo: trait 对象是无确定大小的,但是 Foo 要求 SelfSized. 一个类型不可能同时既是有大小的又是无确定大小的.

    +

    第二点说对象安全要求一个 trait 的所有方法必须是对象安全的. 一个对象安全的方法满足下列条件:

      -
    • It requires Self to be Sized or
    • -
    • It meets all three of the following: +
    • 它要求 SelfSized 或者
    • +
    • 它符合下面全部三点:
        -
      • It must not have any generic type parameters
      • -
      • Its first argument must be of type Self or a type that dereferences to -the Self type (that is, it must be a method rather than an associated -function and have self, &self, or &mut self as the first argument)
      • -
      • It must not use Self anywhere else in the signature except for the -first argument
      • +
      • 它不包含任意类型的常规参数
      • +
      • 它的第一个参数必须是类型 Self 或一个引用到 Self 的类型(也就是说它必须是一个方法而非关联函数并且以 self&self&mut self 作为第一个参数)
      • +
      • 除了第一个参数外它不能在其它地方用 Self 作为方法的参数签名
    -

    Those rules are a bit formal, but think of it this way: if your method requires -the concrete Self type somewhere in its signature, but an object forgets the -exact type that it is, there's no way that the method can use the original -concrete type that it's forgotten. Same with generic type parameters that are -filled in with concrete type parameters when the trait is used: the concrete -types become part of the type that implements the trait. When the type is -erased by the use of a trait object, there's no way to know what types to fill -in the generic type parameters with.

    -

    An example of a trait whose methods are not object safe is the standard -library's Clone trait. The signature for the clone method in the Clone -trait looks like this:

    +

    虽然这些规则有一点形式化, 但是换个角度想一下: 如果你的方法在它的参数签名的其它地方也需要具体的 Self 类型参数, 但是一个对象又忘记了它的具体类型是什么, 这时该方法就无法使用被它忘记的原先的具体类型. 当该 trait 被使用时, 被具体类型参数填充的常规类型参数也是如此: 这个具体的类型就成了实现该 trait 的类型的某一部分, 如果使用一个 trait 对象时这个类型被抹掉了, 就没有办法知道该用什么类型来填充这个常规类型参数.

    +

    一个 trait 的方法不是对象安全的一个例子是标准库中的 Clone trait. Clone trait 的 clone 方法的参数签名是这样的:

    pub trait Clone {
         fn clone(&self) -> Self;
     }
     
    -

    String implements the Clone trait, and when we call the clone method on -an instance of String we get back an instance of String. Similarly, if we -call clone on an instance of Vec, we get back an instance of Vec. The -signature of clone needs to know what type will stand in for Self, since -that's the return type.

    -

    If we try to implement Clone on a trait like the Draw trait from Listing -17-3, we wouldn't know whether Self would end up being a Button, a -SelectBox, or some other type that will implement the Draw trait in the -future.

    -

    The compiler will tell you if you're trying to do something that violates the -rules of object safety in regards to trait objects. For example, if we had -tried to implement the Screen struct in Listing 17-4 to hold types that -implement the Clone trait instead of the Draw trait, like this:

    -
    pub struct Screen {
    +

    String 实现了 Clone trait, 当我们在一个 String 实例上调用 clone 方法时, 我们会得到一个 String 实例. 同样地, 如果我们在一个 Vec 实例上调用 clone 方法, 我们会得到一个 Vec 实例. clone 的参数签名需要知道 Self 是什么类型, 因为它需要返回这个类型.

    +

    如果我们像列表 17-3 中列出的 Draw trait 那样的 trait 上实现 Clone, 我们就不知道 Self 将会是一个 Button, 一个 SelectBox, 或者是其它的在将来要实现 Draw trait 的类型.

    +

    如果你做了违反 trait 对象的对象安全性规则的事情, 编译器将会告诉你. 比如, 如果你实现在列表 17-4 中列出的 Screen 结构, 你想让该结构像这样持有实现了 Clone trait 的类型而不是 Draw trait:

    +
    pub struct Screen {
         pub components: Vec<Box<Clone>>,
     }
     
    -

    We'll get this error:

    +

    我们将会得到下面的错误:

    error[E0038]: the trait `std::clone::Clone` cannot be made into an object
      -->
       |
    diff --git a/docs/index.html b/docs/index.html
    index 1c1ac38..022b846 100644
    --- a/docs/index.html
    +++ b/docs/index.html
    @@ -72,14 +72,14 @@
     
    commit 62f78bb3f7c222b574ff547d0161c2533691f9b4

    -

    欢迎阅读“Rust 程序设计语言”,一本关于 Rust 的介绍性书籍。Rust 是一个着用于安全、速度和并发的编程语言。它的设计不仅可以使程序获得性能和对底层语言的控制,并且能够享受高级语言强大的抽象能力。这些特性使得 Rust 适合那些有类似 C 语言经验并正在寻找一个更安全的替代者的程序员,同时也适合那些来自类似 Python 语言背景,正在探索在不牺牲表现力的情况下编写更好性能代码的开发者。

    -

    Rust 在编译时进行其绝大多数的安全检查和内存管理决策,因此程序的运行时性能没有受到影响。这让其在许多其他语言不擅长的应用场景中得以大显身手:存在可预测空间和时间要求的程序,嵌入到其他语言中,以及编写底层代码,如设备驱动和操作系统。Rust 也很擅长 web 程序:它驱动着 Rust 包注册网站(package -registry site),crates.io!我们期待看到使用 Rust 进行创作。

    -

    本书是为已经至少了解一门编程语言的读者而写的。读完本书之后,你应该能自如的编写 Rust 程序。我们将通过小而集中并且相互依赖的例子来学习 Rust,并向你展示如何使用 Rust 多样的功能,同时了解它们在幕后是如何执行的。

    +

    欢迎阅读“Rust 程序设计语言”,一本介绍 Rust 的书。Rust 是一门着眼于安全、速度和并发的编程语言。它的设计兼顾性能与底层控制,以及高级语言强大的抽象能力。适合那些有类 C 语言经验,正在寻找更安全的替代品的开发者;以及有着类 Python 语言背景,寻求在不牺牲表现力的前提下,编写性能更好的代码的开发者。

    +

    Rust 主要在编译时执行安全检查和内存管理决策,对运行时性能的影响微不足道。这使其在许多语言不擅长的应用场景中得以大显身手:空间和时间需求可预测的程序,嵌入到其他语言中,以及编写底层代码,如设备驱动和操作系统。Rust 也很擅长 web 程序:它驱动着 Rust 包注册网站(package +registry site),crates.io!我们期待使用 Rust 进行创作。

    +

    本书的目标读者至少应了解一门其它编程语言。读完本书之后,你应该能自如的编写 Rust 程序。我们将通过短小精干、前后呼应的例子来学习 Rust,并展示其多样功能的使用方法,同时了解幕后如何运行。

    为本书做出贡献

    -

    本书是开源的。如果你发现任何错误,请不要犹豫,在 GitHub 上发起 issue 或提交 pull request。请查看CONTRIBUTING.md获取更多信息。

    +

    本书是开源的。如果你发现任何错误,不要犹豫,在 GitHub 上发起 issue 或提交 pull request。请查看 CONTRIBUTING.md 获取更多信息。

    -

    译者注:这是本译本的 GitHub 仓库,同样欢迎 Issue 和 PR :)

    +

    译者注:译本的 GitHub 仓库,同样欢迎 Issue 和 PR :)

    diff --git a/docs/print.html b/docs/print.html index 7d09bc6..7c0ce79 100644 --- a/docs/print.html +++ b/docs/print.html @@ -73,14 +73,14 @@
    commit 62f78bb3f7c222b574ff547d0161c2533691f9b4

    -

    欢迎阅读“Rust 程序设计语言”,一本关于 Rust 的介绍性书籍。Rust 是一个着用于安全、速度和并发的编程语言。它的设计不仅可以使程序获得性能和对底层语言的控制,并且能够享受高级语言强大的抽象能力。这些特性使得 Rust 适合那些有类似 C 语言经验并正在寻找一个更安全的替代者的程序员,同时也适合那些来自类似 Python 语言背景,正在探索在不牺牲表现力的情况下编写更好性能代码的开发者。

    -

    Rust 在编译时进行其绝大多数的安全检查和内存管理决策,因此程序的运行时性能没有受到影响。这让其在许多其他语言不擅长的应用场景中得以大显身手:存在可预测空间和时间要求的程序,嵌入到其他语言中,以及编写底层代码,如设备驱动和操作系统。Rust 也很擅长 web 程序:它驱动着 Rust 包注册网站(package -registry site),crates.io!我们期待看到使用 Rust 进行创作。

    -

    本书是为已经至少了解一门编程语言的读者而写的。读完本书之后,你应该能自如的编写 Rust 程序。我们将通过小而集中并且相互依赖的例子来学习 Rust,并向你展示如何使用 Rust 多样的功能,同时了解它们在幕后是如何执行的。

    +

    欢迎阅读“Rust 程序设计语言”,一本介绍 Rust 的书。Rust 是一门着眼于安全、速度和并发的编程语言。它的设计兼顾性能与底层控制,以及高级语言强大的抽象能力。适合那些有类 C 语言经验,正在寻找更安全的替代品的开发者;以及有着类 Python 语言背景,寻求在不牺牲表现力的前提下,编写性能更好的代码的开发者。

    +

    Rust 主要在编译时执行安全检查和内存管理决策,对运行时性能的影响微不足道。这使其在许多语言不擅长的应用场景中得以大显身手:空间和时间需求可预测的程序,嵌入到其他语言中,以及编写底层代码,如设备驱动和操作系统。Rust 也很擅长 web 程序:它驱动着 Rust 包注册网站(package +registry site),crates.io!我们期待使用 Rust 进行创作。

    +

    本书的目标读者至少应了解一门其它编程语言。读完本书之后,你应该能自如的编写 Rust 程序。我们将通过短小精干、前后呼应的例子来学习 Rust,并展示其多样功能的使用方法,同时了解幕后如何运行。

    为本书做出贡献

    -

    本书是开源的。如果你发现任何错误,请不要犹豫,在 GitHub 上发起 issue 或提交 pull request。请查看CONTRIBUTING.md获取更多信息。

    +

    本书是开源的。如果你发现任何错误,不要犹豫,在 GitHub 上发起 issue 或提交 pull request。请查看 CONTRIBUTING.md 获取更多信息。

    -

    译者注:这是本译本的 GitHub 仓库,同样欢迎 Issue 和 PR :)

    +

    译者注:译本的 GitHub 仓库,同样欢迎 Issue 和 PR :)

    安装

    @@ -88,55 +88,55 @@ registry site),crates.io!我们期待看
    commit c1b95a18dbcbb06aadf07c03759f27d88ccf62cf

    -

    使用 Rust 的第一步是安装。你需要网络连接来执行本章的命令,因为我们要从网上下载 Rust。

    -

    我们将会展示很多使用终端的命令,并且这些代码都以$开头。并不需要真正输入$,它们在这里代表每行指令的开头。在网上会看到很多使用这个惯例的教程和例子:$代表以常规用户运行命令,#代表需要用管理员运行的命令。没有以$(或#)的行通常是之前命令的输出。

    +

    第一步是安装 Rust。你需要网络连接来执行本章的命令,因为我们要从网上下载 Rust。

    +

    我们将会展示很多在终端中输入的命令,这些命令均以 $ 开头。你不需要真的输入$,在这里它代表每行命令的起始。网上有很多教程和例子遵循这种惯例:$ 代表以常规用户身份运行命令,# 代表需要用管理员身份运行命令。没有以 $(或 #)起始的行通常是之前命令的输出。

    在 Linux 或 Mac 上安装

    -

    如果你使用 Linux 或 Mac,所有需要做的就是打开一个终端并输入:

    +

    如果你使用 Linux 或 Mac,你需要做的全部,就是打开一个终端并输入:

    $ curl https://sh.rustup.rs -sSf | sh
     
    -

    这会下载一个脚本并开始安装。你可能被提示要输入密码。如果一切顺利,将会出现如下内容:

    +

    这会下载一个脚本并开始安装。可能会提示你输入密码,如果一切顺利,将会出现如下内容:

    Rust is installed now. Great!
     
    -

    当然,如果你不赞成curl | sh这种模式,可以随意下载、检查和运行这个脚本。

    +

    当然,如果你对于 curl | sh 心有疑虑,你可以随意下载、检查和运行这个脚本。

    在 Windows 上安装

    -

    在 Windows 上,前往https://rustup.rs并按照说明下载 rustup-init.exe。运行并遵循其提供的其余指示操作。

    -

    本书其余 Windows 相关的命令假设你使用cmd作为你的 shell。如果你使用不同的 shell,可能能够执行 Linux 和 Mac 用户相同的命令。如果都不行,请查看所使用的 shell 的文档。

    +

    如果你使用 Windows,前往 https://rustup.rs,按说明下载 rustup-init.exe,运行并照其指示操作。

    +

    本书中其余 Windows 相关的命令,假设你使用 cmd 作为 shell。如果你使用其它 shell,也许可以执行与 Linux 和 Mac 用户相同的命令。如果不行,请查看该 shell 的文档。

    自定义安装

    -

    如果有理由倾向于不使用 rustup.rs,请查看Rust 安装页面获取其他选择。

    +

    无论出于何种理由,如果不愿意使用 rustup.rs,请查看 Rust 安装页面 获取其他选择。

    更新

    -

    一旦安装完 Rust,更新到最新版本是简单的。在 shell 中运行更新脚本:

    +

    一旦 Rust 安装完,更新到最新版本很简单。在 shell 中执行:

    $ rustup update
     

    卸载

    -

    卸载 Rust 同安装一样简单。在 shell 中运行卸载脚本

    +

    卸载 Rust 同样简单。在 shell 中执行:

    $ rustup self uninstall
     

    故障排除

    -

    安装完 Rust 后,打开 shell,输入:

    +

    安装完 Rust 后,在 shell 中执行:

    $ rustc --version
     
    -

    应该能看到类似这样的版本号、提交 hash 和提交日期,对应你安装时的最新稳定版本:

    +

    应该能看到类似这样的版本号、提交哈希和提交日期,对应安装时的最新稳定版:

    rustc x.y.z (abcabcabc yyyy-mm-dd)
     
    -

    如果出现这些内容,Rust 就安装成功了!

    +

    出现这些内容,Rust 就安装成功了!

    恭喜入坑!(此处应该有掌声!)

    -

    如果有问题并且你在使用 Windows,检查 Rust(rustc,cargo 等)是否位于%PATH%系统变量中。

    -

    如果还是不能运行,有许多可以获取帮助的地方。最简单的是 irc.mozilla.org 上的 #rust IRC 频道 ,可以使用 Mibbit 来访问它。访问这些地址然后就可以和其他能提供帮助的 Rustacean(我们这些人自嘲的绰号)聊天了。其它给力的资源包括用户论坛Stack Overflow

    +

    如果在 Windows 中使用出现问题,检查 Rust(rustc,cargo 等)是否在 %PATH% 环境变量所包含的路径中。

    +

    如果还是不能解决,有许多地方可以求助。最简单的是 irc.mozilla.org 上的 #rust IRC 频道 ,可以使用 Mibbit 来访问它。然后就能和其他 Rustacean(Rust 用户的称号,有自嘲意味)聊天并寻求帮助。其它给力的资源包括用户论坛Stack Overflow

    本地文档

    -

    安装程序也包含一份本地文档的拷贝,你可以离线阅读它们。输入rustup doc将在浏览器中打开本地文档。

    -

    任何你不太确认标准库中提供的类型或函数是干什么的时候,请查看 API 文档!

    +

    安装程序自带本地文档,可以离线阅读。输入 rustup doc 可以在浏览器中查看。

    +

    任何时候,如果你拿不准标准库中类型或函数,请查看 API 文档!

    Hello, World!

    ch01-02-hello-world.md
    commit 4f2dc564851dc04b271a2260c834643dfd86c724

    -

    现在已经安装好了 Rust,让我们来编写第一个 Rust 程序。当学习一门新语言的时候,编写一个在屏幕上打印 “Hello, world!” 文本的小程序是一个传统,而在这一部分将遵循这个传统。

    +

    Rust 已安好,让我们来编写第一个程序。当学习一门新语言的时候,使用该语言在屏幕上打印 “Hello, world!” 是一项传统,我们将遵循这个传统。

    -

    注意:本书假设你熟悉基本的命令行操作。Rust 本身并不对你的编辑器,工具和你的代码存放在何处有什么特定的要求,所以如果你比起命令行更喜欢 IDE,请随意选择你喜欢的 IDE。

    +

    注意:本书假设你熟悉基本的命令行操作。对于你的编辑器、工具,以及你的代码存在何处,Rust 并没有特殊要求,如果你更喜欢 IDE,请随意。

    创建项目文件夹

    -

    首先,创建一个文件夹来存放 Rust 代码。Rust 并不关心你的代码存放在哪里,不过在本书中,我们建议在你的 home 目录创建一个 projects 目录,并把你的所有项目放在这。打开一个终端并输入如下命令来为这个项目创建一个文件夹:

    +

    首先,创建一个存放代码的文件夹。Rust 并不关心它的位置,不过在本书中,我们建议你在 home 目录中创建一个 projects 目录,并把你的所有项目放在这。打开一个终端,输入如下命令来创建一个文件夹:

    Linux 和 Mac:

    $ mkdir ~/projects
     $ cd ~/projects
    @@ -150,8 +150,8 @@ $ cd hello_world
     > cd hello_world
     

    编写并运行 Rust 程序

    -

    接下来,新建一个叫做 main.rs 的源文件。Rust 文件总是以 .rs 后缀结尾。如果文件名多于一个单词,使用下划线分隔它们。例如,使用 my_program.rs 而不是 myprogram.rs

    -

    现在打开刚创建的 main.rs 文件,并输入如下代码:

    +

    接下来,新建一个叫做 main.rs 的文件。Rust 源代码总是以 .rs 后缀结尾。如果文件名包含多个单词,使用下划线分隔它们。例如 my_program.rs,而不是 myprogram.rs

    +

    现在打开刚创建的 main.rs 文件,输入如下代码:

    Filename: main.rs

    fn main() {
         println!("Hello, world!");
    @@ -162,28 +162,28 @@ $ cd hello_world
     $ ./main
     Hello, world!
     
    -

    在 Windows 上,运行.\main.exe而不是./main。不管使用何种系统,你应该在终端看到Hello, world!字符串。如果你做到了,那么恭喜你!你已经正式编写了一个 Rust 程序。你是一名 Rust 程序员了!欢迎入坑。

    +

    在 Windows 上,运行 .\main.exe,而不是./main。不管使用何种系统,你应该在终端看到 Hello, world! 字样。如果你做到了,恭喜你!你已经正式编写了一个 Rust 程序,成为一名 Rust 程序员!

    分析 Rust 程序

    -

    现在,让我们回过头来仔细看看“Hello, world!”程序到底发生了什么。这里是谜题的第一片:

    +

    现在,让我们回过头来,仔细看看“Hello, world!”程序到底发生了什么。这是拼图的第一片:

    fn main() {
     
     }
     
    -

    这几行定义了一个 Rust 函数main函数是特殊的:这是每一个可执行的 Rust 程序首先运行的函数(译者注:入口点)。第一行表示“定义一个叫 main 的函数,没有参数也没有返回值。”如果有参数的话,它们应该出现在括号中,()

    -

    同时注意函数体被包裹在大括号中,{}。Rust 要求所有函数体都位于大括号中(译者注:对比有些语言特定情况可以省略大括号)。将前一个大括号与函数声明置于一行,并留有一个空格被认为是一个好的代码风格。

    -

    main()函数中:

    +

    这几行定义了一个 Rust 函数。一个叫 main 的函数,没有参数也没有返回值。如果有参数的话,它们应该出现在括弧中,()之间。main 函数是特殊的:它是每一个可执行的 Rust 程序的入口点。

    +

    还须注意函数体被包裹在花括号中,{} 之间。所有函数体都要用花括号包裹起来(译者注:有些语言,当函数体只有一行时可以省略花括号,但 Rust 中是不行的)。一般来说,将左花括号与函数声明置于一行,并以空格分隔,是良好的代码风格。

    +

    main() 函数中:

        println!("Hello, world!");
     
    -

    这行代码做了这个小程序的所有工作:它在屏幕上打印文本。这里有很多需要注意的细节。第一个是 Rust 代码风格使用 4 个空格缩进,而不是 1 个制表符(tab)。

    -

    第二个重要的部分是println!()。这叫做 Rust ,是如何进行 Rust 元编程(metaprogramming)的关键所在。相反如果是调用一个函数的话,它应该看起来像这样:println(没有!)。我们将在 21 章 E 小节中更加详细的讨论 Rust 宏,不过现在你只需记住当看到符号!的时候,就代表在调用一个宏而不是一个普通的函数。

    +

    一行代码完成这个小程序的所有工作:在屏幕上打印文本。这里有很多细节需要注意。首先 Rust 使用 4 个空格的缩进风格,而不是 1 个制表符(tab)。

    +

    第二个重要的部分是println!()。这是 ,Rust 元编程(metaprogramming)的关键所在。而调用一个函数,则要像这样:println(没有!)。我们将在 21 章 E 小节中更加详细的讨论宏,现在你只需记住,当看到符号 ! 的时候,调用的是宏而不是普通函数。

    接下来,"Hello, world!" 是一个 字符串。我们把这个字符串作为一个参数传递给println!,它负责在屏幕上打印这个字符串。轻松加愉快!(⊙o⊙)

    -

    这一行以一个分号结尾(;)。;代表这个表达式的结束和下一个表达式的开始。大部分 Rust 代码行以;结尾。

    +

    该行以分号结尾(;)。; 代表一个表达式的结束和下一个表达式的开始。大部分 Rust 代码行以 ; 结尾。

    编译和运行是两个步骤

    -

    在“编写并运行 Rust 程序”部分,展示了如何运行一个新创建的程序。现在我们将拆分并检查每一步操作。

    -

    在运行一个 Rust 程序之前,必须先编译它。可以输入rustc命令来使用 Rust 编译器并像这样传递源文件的名字:

    +

    “编写并运行 Rust 程序”部分,展示了如何创建运行程序。现在我们将拆分并检查每一步操作。

    +

    运行一个 Rust 程序之前,必须先编译它。可以通过 rustc 命令来使用 Rust 编译器,并传递源文件的名字给它,如下:

    $ rustc main.rs
     
    -

    如果你来自 C 或 C++ 背景,就会发现这与gccclang类似。编译成功后,Rust 应该会输出一个二进制可执行文件,在 Linux 或 OSX 上在 shell 中你可以通过ls命令看到如下:

    +

    如果你有 C 或 C++ 背景,就会发现这与 gccclang 类似。编译成功后,Rust 应该会输出一个二进制可执行文件,在 Linux 或 OSX 上在 shell 中你可以通过ls命令看到如下:

    $ ls
     main  main.rs
     
    @@ -192,21 +192,21 @@ main main.rs main.exe main.rs
    -

    这表示我们有两个文件:.rs 后缀的源文件,和可执行文件(在 Windows下是 main.exe,其它平台是 main)。这里剩下的操作就只有运行 mainmain.exe 文件了,像这样:

    +

    这表示我们有两个文件:.rs 后缀的源文件,和可执行文件(在 Windows下是 main.exe,其它平台是 main)。然后运行 mainmain.exe 文件,像这样:

    $ ./main  # or .\main.exe on Windows
     

    如果 main.rs 是我们的“Hello, world!”程序,它将会在终端上打印Hello, world!

    -

    来自 Ruby、Python 或 JavaScript 这样的动态类型语言背景的同学,可能不太习惯在分开的步骤编译和执行程序。Rust 是一种 静态提前编译语言ahead-of-time compiled language),这意味着可以编译好程序后,把它给任何人,他们都不需要安装 Rust 就可运行。如果你给他们一个 .rb.py.js 文件,他们需要先分别安装 Ruby,Python,JavaScript 实现(运行时环境,VM),不过你只需要一句命令就可以编译和执行程序。这一切都是语言设计的权衡取舍。

    -

    仅仅使用rustc编译简单程序是没问题的,不过随着项目的增长,你将想要能够控制你项目拥有的所有选项,并使其易于分享你的代码给别人或别的项目。接下来,我们将介绍一个叫做 Cargo 的工具,它将帮助你编写现实生活中的 Rust 程序。

    +

    来自 Ruby、Python 或 JavaScript 这样的动态类型语言背景的同学,可能不太习惯将编译和执行分为两个步骤。Rust 是一种 预编译静态类型语言ahead-of-time compiled language),这意味着编译好程序后,把它给任何人,他们不需要安装 Rust 就可运行。如果你给他们一个 .rb.py.js 文件,他们需要先分别安装 Ruby,Python,JavaScript 实现(运行时环境,VM),不过你只需要一句命令就可以编译和执行程序。这一切都是语言设计上的权衡取舍。

    +

    使用 rustc 编译简单程序是没问题的,不过随着项目的增长,你可能需要控制你项目的方方面面,并且更容易地将代码分享给其它人或项目。所以接下来,我们要介绍一个叫做 Cargo 的工具,它会帮助你编写真实世界中的 Rust 程序。

    Hello, Cargo!

    -

    Cargo 是 Rust 的构建系统和包管理工具,同时 Rustacean 们使用 Cargo 来管理他们的 Rust 项目,因为它使得很多任务变得更轻松。例如,Cargo 负责构建代码、下载代码依赖的库并编译这些库。我们把代码需要的库叫做 依赖dependencies)。

    -

    最简单的 Rust 程序,例如我们刚刚编写的,并没有任何依赖,所以目前我们只使用了 Cargo 负责构建代码的那一部分。随着编写更加复杂的 Rust 程序,你会想要添加依赖,如果你使用 Cargo 开始的话,这将会变得简单许多。

    -

    由于绝大部分 Rust 项目使用 Cargo,本书接下来的部分将假设你使用它。如果使用安装章节介绍的官方安装包的话,Rust 自带 Cargo。如果通过其他方式安装 Rust 的话,可以在终端输入如下命令检查是否安装了 Cargo:

    +

    Cargo 是 Rust 的构建系统和包管理工具,同时 Rustacean 们使用 Cargo 来管理他们的 Rust 项目,它使得很多任务变得更轻松。例如,Cargo 负责构建代码、下载依赖库并编译。我们把代码需要的库叫做 依赖dependencies)。

    +

    最简单的 Rust 程序,比如我们刚刚编写的,并没有任何依赖,所以我们只使用了 Cargo 构建代码的功能。随着更复杂程序的编写,你会想要添加依赖,如果你使用 Cargo 开始的话,这将会变得简单许多。

    +

    由于绝大部分 Rust 项目使用 Cargo,本书接下来的部分将假设你使用它。如果使用之前介绍的官方安装包的话,它自带 Cargo。如果通过其他方式安装的话,可以在终端输入如下命令,检查是否安装了 Cargo:

    $ cargo --version
     
    -

    如果出现了版本号,一切 OK!如果出现一个类似“command not found”的错误,那么你应该查看安装方式的文档来确定如何单独安装 Cargo。

    +

    如果出现了版本号,一切 OK!如果出现类似“command not found”的错误,你应该查看安装文档以确定如何单独安装 Cargo。

    使用 Cargo 创建项目

    -

    让我们使用 Cargo 来创建一个新项目并看看与上面的hello_world项目有什么不同。回到 projects 目录(或者任何你决定放置代码的目录):

    +

    让我们使用 Cargo 来创建一个新项目,然后看看与上面的hello_world项目有什么不同。回到 projects 目录(或者任何你放置代码的目录):

    Linux 和 Mac:

    $ cd ~/projects
     
    @@ -217,9 +217,9 @@ main.rs
    $ cargo new hello_cargo --bin
     $ cd hello_cargo
     
    -

    我们向cargo new传递了--bin因为我们的目标是生成一个可执行程序,而不是一个库。可执行文件是二进制可执行文件,通常就叫做 二进制文件binaries)。项目的名称被定为hello_cargo,同时 Cargo 在一个同名目录中创建它的文件,接着我们可以进入查看。

    -

    如果列出 hello_cargo 目录中的文件,将会看到 Cargo 生成了一个文件和一个目录:一个 Cargo.toml 文件和一个 src 目录,main.rs 文件位于目录中。它也在 hello_cargo 目录初始化了一个 git 仓库,以及一个 .gitignore 文件;你可以改为使用不同的版本控制系统(VCS),或者不使用 VCS,通过--vcs参数。

    -

    使用你选择的文本编辑器(IDE)打开 Cargo.toml 文件。它应该看起来像这样:

    +

    我们向 cargo new 传递了 --bin,因为我们的目标是生成一个可执行程序,而不是一个库。可执行程序是二进制可执行文件,通常就叫做 二进制文件binaries)。项目的名称被定为hello_cargo,同时 Cargo 在一个同名目录中创建它的文件,接着我们可以进入查看。

    +

    如果列出 hello_cargo 目录中的文件,将会看到 Cargo 生成了一个文件和一个目录:一个 Cargo.toml 文件和一个 src 目录,main.rs 文件位于目录中。它也在 hello_cargo 目录初始化了一个 git 仓库,以及一个 .gitignore 文件;你可以通过--vcs参数,切换到其它版本控制系统(VCS),或者不使用 VCS。

    +

    使用文本编辑器(IDE)打开 Cargo.toml 文件。它应该看起来像这样:

    Filename: Cargo.toml

    [package]
     name = "hello_cargo"
    @@ -228,22 +228,22 @@ authors = ["Your Name <you@example.com>"]
     
     [dependencies]
     
    -

    这个文件使用TOML (Tom's Obvious, Minimal Language) 格式。TOML 类似于 INI,不过有一些额外的改进之处,并且被用作 Cargo 的配置文件的格式。

    -

    第一行,[package],是一个部分标题表明下面的语句用来配置一个包。随着我们在这个文件增加更多的信息,还将增加其他部分。

    -

    最后一行,[dependencies],是列出项目依赖的 crates(我们这么称呼 Rust 代码包)的部分的开始,这样 Cargo 也就知道去下载和编译它们了。这个项目并不需要任何其他的 crate,不过在下一章猜猜看教程会需要。

    +

    这个文件使用 TOML (Tom's Obvious, Minimal Language) 格式。TOML 类似于 INI,不过有一些额外的改进之处,并且被用作 Cargo 的配置文件的格式。

    +

    第一行,[package],是一个段落标题,表明下面的语句用来配置一个包。随着我们在这个文件增加更多的信息,还将增加其他段落。

    +

    最后一行,[dependencies],是项目依赖的 crates(Rust 代码包)的段落的开始,这样 Cargo 就知道下载和编译它们了。这个项目并不需要任何其他的 crate,不过在下一章猜猜看教程会需要。

    现在看看 src/main.rs

    Filename: src/main.rs

    fn main() {
         println!("Hello, world!");
     }
     
    -

    Cargo 为你生成了一个“Hello World!”,正如我们之前编写的那个!目前为止我们所见过的之前项目与 Cargo 生成的项目区别有:

    +

    Cargo 为你生成了一个“Hello World!”,正如我们之前编写的那个!目前为止,之前项目与 Cargo 生成项目区别有:

    • 代码位于 src 目录
    • 项目根目录包含一个 Cargo.toml 配置文件
    -

    Cargo 期望源文件位于 src 目录,这样将项目根目录留给 README、license 信息、配置文件和其他跟代码无关的文件。这样,Cargo 帮助你保持项目干净整洁。一切井井有条。

    -

    如果没有使用 Cargo 开始项目,正如在 hello_world 目录中的项目,可以把它转化为一个使用 Cargo 的项目,通过将代码放入 src 目录并创建一个合适的 Cargo.toml

    +

    Cargo 期望源文件位于 src 目录,将项目根目录留给 README、license 信息、配置文件和其他跟代码无关的文件。这样,Cargo 帮助你保持项目干净整洁,一切井井有条。

    +

    如果没有用 Cargo 创建项目,比如 hello_world 目录中的项目,可以通过将代码放入 src 目录,并创建一个合适的 Cargo.toml,将其转化为一个 Cargo 项目。

    构建并运行 Cargo 项目

    现在让我们看看通过 Cargo 构建和运行 Hello World 程序有什么不同。为此,输入如下命令:

    $ cargo build
    @@ -254,40 +254,40 @@ authors = ["Your Name <you@example.com>"]
     Hello, world!
     

    好的!如果一切顺利,Hello, world!应该再次打印在终端上。

    -

    第一次运行cargo build的时候也会使 Cargo 在项目根目录创建一个叫做 Cargo.lock 的新文件,它看起来像这样:

    +

    首次运行 cargo build 的时候,Cargo 会在项目根目录创建一个新文件,Cargo.lock,它看起来像这样:

    Filename: Cargo.lock

    [root]
     name = "hello_cargo"
     version = "0.1.0"
     
    -

    Cargo 使用 Cargo.lock 来记录程序的依赖。这个项目并没有依赖,所以内容有一点稀少。事实上,你自己永远也不需要碰这个文件;仅仅让 Cargo 处理它就行了。

    -

    我们刚刚使用cargo build构建了项目并使用./target/debug/hello_cargo运行了它,不过也可以使用cargo run编译并运行:

    +

    Cargo 使用 Cargo.lock 来记录程序的依赖。这个项目并没有依赖,所以内容比较少。事实上,你自己永远也不需要碰这个文件,让 Cargo 处理它就行了。

    +

    我们刚刚使用 cargo build 构建了项目并使用 ./target/debug/hello_cargo 运行了它,也可以使用 cargo run 编译并运行:

    $ cargo run
          Running `target/debug/hello_cargo`
     Hello, world!
     
    -

    注意这一次,并没有出现告诉我们 Cargo 正在编译 hello_cargo 的输出。Cargo 发现文件并没有被改变,所以只是运行了二进制文件。如果修改了源文件的话,将会出现像这样的输出:

    +

    注意这一次并没有出现“正在编译 hello_cargo”的输出。Cargo 发现文件并没有被改变,直接运行了二进制文件。如果修改了源文件的话,将会出现像这样的输出:

    $ cargo run
        Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
          Running `target/debug/hello_cargo`
     Hello, world!
     
    -

    所以现在又出现一些更多的不同:

    +

    所以现在又出现更多的不同:

      -
    • 使用cargo build构建项目(或使用cargo run一步构建并运行),而不是使用rustc
    • -
    • 不同于将构建结果放在源码相同目录,Cargo 会将它放到 target/debug 目录中的文件。
    • +
    • 使用 cargo build 构建项目(或使用 cargo run 一步构建并运行),而不是使用rustc
    • +
    • 有别于将构建结果放在源码目录,Cargo 将它放到 target/debug 目录。
    -

    Cargo 的另一个优点是不管你使用什么操作系统它的命令都是一样的,所以之后我们将不再为 Linux 和 Mac 以及 Windows 提供特定的命令。

    +

    Cargo 的另一个优点是,不管你使用什么操作系统,它的命令都是一样的,所以之后我们将不再为 Linux 和 Mac 以及 Windows 提供相应的命令。

    发布构建

    -

    当项目最终准备好发布了,可以使用cargo build --release来优化编译项目。这会在 target/release 下生成可执行文件,而不是 target/debug。这些优化可以让 Rust 代码运行的更快,不过启用他们会让程序花更长的时间编译。这也是为何这是两种不同的配置:一个为了开发,这时你经常想要快速重新构建;另一个构建提供给用户的最终程序,这时并不会重新构建并希望程序能运行得越快越好。如果你在测试代码的运行时间,请确保运行cargo build --release并使用 target/release 下的可执行文件进行测试。

    +

    当项目最终准备好发布了,可以使用 cargo build --release 来优化编译项目。这会在 target/release 下生成可执行文件,而不是 target/debug。优化可以让 Rust 代码运行的更快,然而也需要更长的编译时间。因此产生了两种不同的配置:一种为了开发,你需要快速重新构建;另一种构建给用户的最终程序,不会重新构建,并且程序运行得越快越好。如果你在测试代码的运行时间,请确保运行 cargo build --release 并使用 target/release 下的可执行文件。

    把 Cargo 当作习惯

    -

    对于简单项目, Cargo 并不能比rustc提供更多的价值,不过随着开发的进行终将体现它的价值。对于拥有多个 crate 的复杂项目,让 Cargo 来协调构建将更简单。有了 Cargo,只需运行cargo build,然后一切将有序运行。即便这个项目很简单,现在也它使用了很多之后你的 Rust 程序生涯将会用得上的实用工具。事实上,无形中你可以使用下面的命令开始所有你想要从事的项目:

    +

    对于简单项目, Cargo 并不比 rustc 更有价值,不过随着开发的进行终将体现它的价值。对于拥有多个 crate 的复杂项目,让 Cargo 来协调构建将更简单。有了 Cargo,只需运行cargo build,然后一切将有序运行。即便这个项目很简单,也它使用了很多你之后的 Rust 生涯将会用得上的实用工具。其实你可以开始任何你想要从事的项目,使用下面的命令:

    $ git clone someurl.com/someproject
     $ cd someproject
     $ cargo build
     
    -

    注意:如果你想要查看 Cargo 的更多细节,请阅读官方的 Cargo guide,它覆盖了其所有的功能。

    +

    注意:如果想要了解 Cargo 更多的细节,请阅读官方的 Cargo guide,它覆盖了所有的功能。

    猜猜看

    @@ -295,10 +295,10 @@ $ cargo build
    commit e6d6caab41471f7115a621029bd428a812c5260e

    -

    让我们通过自己动手的方式一起完成一个项目来快速上手 Rust!本章将介绍一些常用的 Rust 概念,并通过真实的程序来展示如何运用他们。你将会学到letmatch、方法、关联函数、使用外部 crate 等更多的知识!接下来的章节会探索这些概念的细节。在这一章,我们将练习基础。

    -

    我们会实现一个经典的新手编程问题:猜猜看游戏。它是这么工作的:程序将会随机生成一个 1 到 100 之间的随机整数。接着它会提示玩家输入一个猜测。当输入了一个猜测后,它会提示猜测是太大了还是太小了。如果猜对了,它会打印出祝贺并退出。

    +

    让我们亲自动手,快速熟悉 Rust!本章将介绍 Rust 中常用的一些概念,并通过真实的程序来展示如何运用。你将会学到更多诸如 letmatch、方法、关联函数、外部 crate 等知识!后继章节会深入探索这些概念的细节。在这一章,我们将练习基础。

    +

    我们会实现一个经典的新手编程问题:猜猜看游戏。它是这么工作的:程序将会随机生成一个 1 到 100 之间的随机整数。接着它会请玩家猜一个数并输入,然后提示猜测是大了还是小了。如果猜对了,它会在退出前祝贺你。

    准备一个新项目

    -

    要创建一个新项目,进入在第一章创建的项目目录,像这样使用 Cargo 创建它:

    +

    要创建一个新项目,进入第一章创建的项目目录,使用 Cargo 创建它:

    $ cargo new guessing_game --bin
     $ cd guessing_game
     
    @@ -313,22 +313,22 @@ authors = ["Your Name <you@example.com>"] [dependencies]

    如果 Cargo 从环境中获取的开发者信息不正确,修改这个文件并再次保存。

    -

    正如第一章那样,cargo new生成了一个“Hello, world!”程序。查看 src/main.rs 文件:

    +

    正如第一章那样,cargo new 生成了一个“Hello, world!”程序。查看 src/main.rs 文件:

    Filename: src/main.rs

    fn main() {
         println!("Hello, world!");
     }
     
    -

    现在让我们使用cargo run在相同的步骤编译并运行这个“Hello, world!”程序:

    +

    现在让我们使用 cargo run,编译运行一步到位:

    $ cargo run
        Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
          Running `target/debug/guessing_game`
     Hello, world!
     
    -

    run命令在需要快速迭代项目时就派上用场了,而这个游戏就正是这么一个项目:我们需要在进行下一步之前快速测试每次迭代。

    -

    重新打开 src/main.rs 文件。我们将会在这个文件编写全部的代码。

    +

    run 命令适合用在需要快速迭代的项目,而这个游戏就是:我们需要在下一步迭代之前快速测试。

    +

    重新打开 src/main.rs 文件。我们将会在这个文件中编写全部代码。

    处理一次猜测

    -

    程序的第一部分会请求用户输入,处理输入,并检查输入是否为期望的形式。首先,允许玩家输入一个猜测。在 src/main.rs 中输入列表 2-1 中的代码。

    +

    程序的第一部分请求和处理用户输入,并检查输入是否符合预期。首先,需要有一个让玩家输入猜测的地方。在 src/main.rs 中输入列表 2-1 中的代码。

    Filename: src/main.rs

    use std::io;
     
    @@ -346,59 +346,59 @@ fn main() {
     }
     

    Listing 2-1: Code to get a guess from the user and print it out

    -

    这些代码包含很多信息,所以让我们一点一点地过一遍。为了获取用户输入并接着打印结果作为输出,我们需要将io(输入/输出)库引入作用域中。io库来自于标准库(也被称为std):

    +

    这些代码包含很多信息,我们一点一点地过一遍。为了获取用户输入并打印结果作为输出,我们需要将 io(输入/输出)库引入当前作用域。io库来自于标准库(也被称为std):

    use std::io;
     
    -

    Rust 默认只在每个程序的 prelude 中引用很少的一些类型。如果想要使用的类型并不在 prelude 中,你必须使用一个use语句显式的将其引入到作用域中。使用std::io库将提供很多io相关的功能,接受用户输入的功能。

    -

    正如第一章所讲,main函数是程序的入口点:

    +

    Rust 默认只在每个程序的 prelude 中引入少量类型。如果需要的类型不在 prelude 中,你必须使用一个 use 语句显式的将其引入作用域。std::io 库提供很多 io 相关的功能,比如接受用户输入。

    +

    如第一章所提及,main 函数是程序的入口点:

    fn main() {
     
    -

    fn语法声明了一个新函数,()表明没有参数,{作为函数体的开始。

    -

    第一章也讲到了,println!是一个在屏幕上打印字符串的宏:

    +

    fn 语法声明了一个新函数,() 表明没有参数,{ 作为函数体的开始。

    +

    第一章也提及,println! 是一个在屏幕上打印字符串的宏:

    println!("Guess the number!");
     
     println!("Please input your guess.");
     
    -

    这些代码仅仅打印一个提示,说明游戏的内容并请求用户输入。

    +

    这些代码仅仅打印提示,介绍游戏的内容然后请用户输入。

    用变量储存值

    接下来,创建一个地方储存用户输入,像这样:

    -
    let mut guess = String::new();
    +
    let mut guess = String::new();
     
    -

    现在程序开始变得有意思了!这一小行代码发生了很多事。注意这是一个let语句,用来创建变量。这里是另外一个例子:

    -
    let foo = bar;
    +

    现在程序开始变得有意思了!这一小行代码发生了很多事。注意这是一个 let 语句,用来创建变量。这里是另外一个例子:

    +
    let foo = bar;
     
    -

    这行代码会创建一个叫做foo的新变量并把它绑定到值bar上。在 Rust 中,变量默认是不可变的。下面的例子展示了如何在变量名前使用mut来使一个变量可变:

    +

    这行代码会创建一个叫做 foo 的新变量并把它绑定到值 bar 上。在 Rust 中,变量默认是不可变的。下面的例子展示了如何在变量名前使用 mut 来使一个变量可变:

    let foo = 5; // immutable
     let mut bar = 5; // mutable
     

    注意:// 开始一个注释,它持续到本行的结尾。Rust 忽略注释中的所有内容。

    -

    现在我们知道了let mut guess会引入一个叫做guess的可变变量。等号(=)的另一边是guess所绑定的值,它是String::new的结果,这个函数会返回一个String的新实例。String是一个标准库提供的字符串类型,它是可增长的、UTF-8 编码的文本块。

    -

    ::new那一行的::语法表明newString类型的一个关联函数associated function)。关联函数是针对类型实现的,在这个例子中是String,而不是String的某个特定实例。一些语言中把它称为静态方法static method)。

    -

    new函数创建了一个新的空的String,你会在很多类型上发现new函数,因为这是创建某个类型新值的常用函数名。

    -

    总结一下,let mut guess = String::new();这一行创建了一个可变变量,目前它绑定到一个String新的、空的实例上。哟!

    -

    回忆一下我们在程序的第一行使用use std::io;从标准库中引用输入/输出功能。现在在io上调用一个关联函数,stdin

    +

    现在我们知道了 let mut guess 会引入一个叫做 guess 的可变变量。等号(=)的右边是 guess 所绑定的值,它是 String::new 的结果,这个函数会返回一个 String 的新实例。String 是一个标准库提供的字符串类型,它是 UTF-8 编码的可增长文本块。

    +

    ::new 那一行的 :: 语法表明 newString 类型的一个 关联函数associated function)。关联函数是针对类型实现的,在这个例子中是 String,而不是 String 的某个特定实例。一些语言中把它称为静态方法static method)。

    +

    new 函数创建了一个新的空 String,你会在很多类型上发现new 函数,这是创建类型实例的惯用函数名。

    +

    总结一下,let mut guess = String::new(); 这一行创建了一个可变变量,绑定到一个新的 String 空实例上。

    +

    回忆一下,我们在程序的第一行使用 use std::io; 从标准库中引入“输入输出”。现在调用 io 的关联函数 stdin

    io::stdin().read_line(&mut guess)
         .expect("Failed to read line");
     
    -

    如果我们在程序的开头没有use std::io这一行,我们可以把函数调用写成std::io::stdin这样。stdin函数返回一个 std::io::Stdin的实例,这是一个代表终端标准输入句柄的类型。

    -

    代码的下一部分,.read_line(&mut guess),调用 read_line 方法从标准输入句柄获取用户输入。我们还向read_line()传递了一个参数:&mut guess

    -

    read_line的工作是把获取任何用户键入到标准输入的字符并放入一个字符串中,所以它获取字符串作为一个参数。这个字符串需要是可变的,这样这个方法就可以通过增加用户的输入来改变字符串的内容。

    -

    &表明这个参数是一个引用reference),它提供了一个允许多个不同部分的代码访问同一份数据而不需要在内存中多次拷贝的方法。引用是一个复杂的功能,而 Rust 的一大优势就是它是如何安全而优雅操纵引用的。完成这个程序并不需要知道这么多细节:第四章会更全面的解释引用。现在,我们只需知道它像变量一样,默认是不可变的。因此,需要写成&mut guess而不是&guess来使其可变。

    -

    我们还没有分析完这行代码。虽然这是单独一行代码,但它只是一个逻辑上代码行(虽然换行了但仍是一个语句)的第一部分。第二部分是这个方法:

    -
    .expect("Failed to read line");
    -
    -

    当使用.expect()语法调用方法时,明智的选择是换行并留出空白(缩进)来把长的代码行拆开。我们可以把代码写成这样:

    -
    io::stdin().read_line(&mut guess).expect("Failed to read line");
    -
    -

    不过,过长的代码行难以阅读,所以最好拆开来写,两行代码两个方法调用。现在来看看这行代码干了什么。

    -

    使用Result类型来处理潜在的错误

    -

    之前提到过,read_line将用户输入放入到传递给它字符串中,不过它也返回一个值——在这个例子中,一个io::Result。Rust 标准库中有很多叫做Result的类型。一个Result泛型以及对应子模块的特定版本,比如io::Result

    -

    Result类型是 枚举enumerations,通常也写作 enums。枚举拥有固定值集合的类型,而这些值被称为枚举的成员variants)。第六章会更详细的介绍枚举。

    -

    对于Result,它的成员是OkErrOk表明操作成功了,同时Ok成员之中包含成功生成的值。Err意味着操作失败,Err之中包含操作是为什么或如何失败的信息。

    -

    Result类型的作用是编码错误处理信息。Result类型的值,正如其他任何类型,拥有定义于其上的方法。io::Result的实例拥有expect方法可供调用。如果io::Result实例的值是Errexpect会导致程序崩溃并显示显示你作为参数传递给expect的信息。如果io::Result实例的值是Okexpect会获取Ok中的值并原原本本的返回给你。在本例中,这个值是用户输入到标准输入中的字节的数量。

    -

    如果不使用expect,程序也能编译,不过会出现一个警告:

    +

    如果程序的开头没有 use std::io 这一行,我们可以把函数调用写成 std::io::stdinstdin 函数返回一个 std::io::Stdin 的实例,这代表终端标准输入句柄的类型。

    +

    代码的下一部分,.read_line(&mut guess),调用 read_line 方法从标准输入句柄获取用户输入。我们还向 read_line() 传递了一个参数:&mut guess

    +

    read_line 的工作是,无论用户在标准输入中键入什么内容,都将其存入一个字符串中,因此它需要字符串作为参数。这个字符串参数应该是可变的,以便 read_line 将用户输入附加上去。

    +

    & 表示这个参数是一个引用reference),它允许多处代码访问同一处数据,而无需在内存中多次拷贝。引用是一个复杂的特性,Rust 的一个主要优势就是安全而简单的操纵引用。完成当前程序并不需要了解如此多细节:第四章会更全面的解释引用。现在,我们只需知道它像变量一样,默认是不可变的,需要写成 &mut guess 而不是 &guess 来使其可变。

    +

    我们还没有分析完这行代码。虽然这是单独一行代码,但它是一个逻辑行(虽然换行了但仍是一个语句)的第一部分。第二部分是这个方法:

    +
    .expect("Failed to read line");
    +
    +

    当使用 .expect() 语法调用方法时,通过‘换行并缩进’来把长行拆开,是明智的。我们完全可以这样写:

    +
    io::stdin().read_line(&mut guess).expect("Failed to read line");
    +
    +

    不过,过长的行难以阅读,所以最好拆开来写,两行代码两个方法调用。现在来看看这行代码干了什么。

    +

    使用 Result 类型来处理潜在的错误

    +

    之前提到,read_line 将用户输入附加到传递给它字符串中,不过它也返回一个值——在这个例子中是 io::Result。Rust 标准库中有很多叫做 Result 的类型。一个 Result 泛型以及对应子模块的特定版本,比如 io::Result

    +

    Result 类型是 枚举enumerations,通常也写作 enums。枚举类型持有固定集合的值,这些值被称为枚举的成员variants)。第六章将介绍枚举的更多细节。

    +

    对于 Result,它的成员是 OkErrOk 表示操作成功,内部包含产生的值。Err 意味着操作失败,包含失败的前因后果。

    +

    Result 类型的作用是编码错误处理信息。Result 类型的值,像其他类型一样,拥有定义于其上的方法。io::Result 的实例拥有expect方法,如果实例的值是 Errexpect 会导致程序崩溃,并显示当做参数传递给 expect 的信息;如果实例的值是 Okexpect 会获取 Ok 中的值并原样返回。在本例中,这个值是用户输入到标准输入中的字节的数量。

    +

    如果不使用 expect,程序也能编译,不过会出现一个警告:

    $ cargo build
        Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
     src/main.rs:10:5: 10:39 warning: unused result which must be used,
    @@ -406,18 +406,18 @@ src/main.rs:10:5: 10:39 warning: unused result which must be used,
     src/main.rs:10     io::stdin().read_line(&mut guess);
                        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     
    -

    Rust 警告我们没有使用read_line返回的值Result,表明程序没有处理一个可能的错误。消除警告的正确方式是老实编写错误处理,不过因为我们仅仅希望程序出现问题就崩溃,可以直接使用expect。第九章会学习从错误中恢复的内容。

    -

    使用println!占位符打印值

    -

    除了位于结尾的大括号,目前为止编写的代码就只有一行代码值得讨论一下了,就是这一行:

    -
    println!("You guessed: {}", guess);
    +

    Rust 警告我们没有使用 read_line 的返回值 Result,说明有一个可能的错误没处理。想消除警告,就老实的写错误处理,不过我们就是希望程序在出现问题时立即崩溃,所以直接使用 expect。第九章会学习如何从错误中恢复。

    +

    使用 println! 占位符打印值

    +

    除了位于结尾的大括号,目前为止就只有一行代码值得讨论一下了,就是这一行:

    +
    println!("You guessed: {}", guess);
     
    -

    这行代码打印出存储了用户输入的字符串。这对{}是一个在特定位置预留值的占位符。可以使用{}打印多个值:第一个{}对应格式化字符串之后列出的第一个值,第二个对应第二个值,以此类推。用一个println!调用打印多个值应该看起来像这样:

    +

    这行代码打印存储用户输入的字符串。第一个参数是格式化字符串,里面的 {} 是预留在特定位置的占位符。使用占位符也可以打印多个值:格式化字符串中第一个占位符对应第二个参数值,第二个占位符对应第三个参数值,以此类推(第一个参数是格式化字符串本身)。调用一次 println! 打印多个值看起来像这样:

    let x = 5;
     let y = 10;
     
     println!("x = {} and y = {}", x, y);
     
    -

    这行代码会打印出x = 5 and y = 10

    +

    这行代码会打印出 x = 5 and y = 10

    测试第一部分代码

    让我们来测试下猜猜看游戏的第一部分。使用cargo run运行它:

    $ cargo run
    @@ -428,19 +428,19 @@ Please input your guess.
     6
     You guessed: 6
     
    -

    至此为止,游戏的第一部分已经完成:我们从键盘获取了输入并打印了出来。

    +

    至此为止,游戏的第一部分已经完成:我们从键盘获取输入并打印了出来。

    生成一个秘密数字

    -

    接下来,需要生成一个秘密数字,用户会尝试猜测它。秘密数字应该每次都不同,这样多玩几次才会有意思。生成一个 1 到 100 之间的随机数这样游戏也不会太难。Rust 标准库中还未包含随机数功能。然而,Rust 团队确实提供了一个rand crate

    +

    接下来,需要生成一个秘密数字,好让用户来猜。秘密数字应该每次都不同,这样重复玩才不会乏味;范围应该在 1 到 100 之间,这样才不会太困难。Rust 标准库中尚未包含随机数功能。然而,Rust 团队还是提供了一个 rand crate

    使用 crate 来增加更多功能

    -

    记住 crate 是一个 Rust 代码的包。我们正在构建的项目是一个二进制 crate,它生成一个可执行文件。 rand crate 是一个 库 crate,它包含意在被其他程序使用的代码。

    -

    Cargo 对外部 crate 的运用是其真正闪光的地方。在我们可以使用rand编写代码之前,需要编辑 Cargo.toml 来包含rand作为一个依赖。现在打开这个文件并在[dependencies]部分标题(Cargo 为你创建了它)的下面添加如下代码:

    +

    记住 crate 是一个 Rust 代码的包。我们正在构建的项目是一个二进制 crate,它生成一个可执行文件。 rand crate 是一个 库 crate,库 crate 可以包含任意能被其他程序使用的代码。

    +

    Cargo 对外部 crate 的运用是亮点。在我们使用 rand 编写代码之前,需要编辑 Cargo.toml ,声明 rand 作为一个依赖。现在打开这个文件并在 [dependencies] 标题(Cargo 为你创建了它)之下添加:

    Filename: Cargo.toml

    [dependencies]
     
     rand = "0.3.14"
     
    -

    Cargo.toml 文件中,任何标题之后的内容都是属于这个部分的,一直持续到直到另一个部分开始。[dependencies]部分告诉 Cargo 项目依赖了哪个外部 crate 和需要的 crate 版本。在这个例子中,我们使用语义化版本符号0.3.14来指定randcrate。Cargo 理解语义化版本(Semantic Versioning)(有时也称为 SemVer),这是一个编写版本号的标准。版本号0.3.14事实上是^0.3.14的缩写,它的意思是“任何与 0.3.14 版本公有 API 相兼容的版本”。

    -

    现在,不用修改任何代码,构建项目,如列表 2-2 所示:

    +

    Cargo.toml 文件中,标题以及之后的内容属同一个段落,遇到下一个标题则开始新的段落。[dependencies] 部分告诉 Cargo 本项目依赖了哪些外部 crate 及其版本。本例中,我们使用语义化版本 0.3.14 来指定 rand crate。Cargo 理解语义化版本(Semantic Versioning)(有时也称为 SemVer),是一种定义版本号的标准。0.3.14 事实上是 ^0.3.14 的简写,它表示“任何与 0.3.14 版本公有 API 相兼容的版本”。

    +

    现在,不修改任何代码,构建项目,如列表 2-2 所示:

    $ cargo build
         Updating registry `https://github.com/rust-lang/crates.io-index`
      Downloading rand v0.3.14
    @@ -451,40 +451,40 @@ rand = "0.3.14"
     

    Listing 2-2: The output from running cargo build after adding the rand crate as a dependency

    -

    可能会出现不同的版本号(不过多亏了语义化版本,它们与代码是兼容的!),同时显示顺序也可能会有所不同。

    -

    现在我们有了一个外部依赖,Cargo 从 registryCrates.io)上获取了一份(兼容的)最新版本代码的拷贝。Crates.io 是 Rust 生态环境中的开发者们向他人贡献他们的开源 Rust 项目的地方。

    -

    在更新完 registry (索引)后,Cargo 检查[dependencies]部分并下载还不存在的部分。在这个例子中,虽然只列出了rand一个依赖,Cargo 也获取了一份libc的拷贝,因为rand依赖libc来正常工作。在下载他们之后,Rust 编译他们并接着使用这些依赖编译项目。

    -

    如果不做任何修改就立刻再次运行cargo build,则不会有任何输出。Cargo 知道它已经下载并编译了依赖,同时 Cargo.toml 文件中也没有任何相关修改。Cargo 也知道代码没有做任何修改,所以它也不会重新编译代码。因为无事可做,它简单的退出了。如果打开 src/main.rs 文件,并做一些普通的修改,保存并再次构建,只会出现一行输出:

    +

    可能会出现不同的版本号(多亏了语义化版本,它们与代码是兼容的!),同时显示顺序也可能会有所不同。

    +

    现在我们有了一个外部依赖,Cargo 从 registryCrates.io)上获取了一份(兼容的)最新版本的代码。Crates.io 是 Rust 生态环境中的开发者们向他人贡献 Rust 开源项目的地方。

    +

    在更新完 registry (索引)后,Cargo 检查 [dependencies] 段落并下载缺失的部分。本例中,只声明了 rand 一个依赖,然而 Cargo 还是额外获取了 libc,因为 rand 依赖 libc 来正常工作。下载完成后,Rust 编译依赖,然后使用这些依赖编译项目。

    +

    如果不做任何修改,立刻再次运行 cargo build,则不会有任何输出。Cargo 知道它已经下载并编译了依赖,同时 Cargo.toml 文件也没有变动,并且代码也没有任何修改,所以它不会重新编译代码。因为无事可做,它简单的退出了。如果打开 src/main.rs 文件,做一些普通的修改,保存并再次构建,只会出现一行输出:

    $ cargo build
        Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
     
    -

    这一行表明 Cargo 只构建了对 src/main.rs 文件做出的微小修改。依赖没有被修改,所以 Cargo 知道可以复用已经为此下载并编译的代码。它只是重新构建了部分(项目)代码。

    +

    这一行表示 Cargo 只针对 src/main.rs 文件的微小修改而构建。依赖没有变化,所以 Cargo 会复用已经为此下载并编译的代码。它只是重新构建了部分(项目)代码。

    Cargo.lock 文件确保构建是可重现的

    -

    Cargo 有一个机制来确保每次任何人重新构建代码都会生成相同的结果:Cargo 只会使用你指定的依赖的版本,除非你又手动指定了别的。例如,如果下周rand crate 的v0.3.15版本出来了,而它包含一个重要的 bug 修改并也含有一个会破坏代码运行的缺陷的时候会发生什么呢?

    -

    这个问题的答案是 Cargo.lock 文件,它在第一次运行cargo build时被创建并位于 guessing_game 目录。当第一次构建项目时,Cargo 计算出所有符合要求的依赖版本并接着写入 Cargo.lock 文件中。当将来构建项目时,Cargo 发现 Cargo.lock 存在就会使用这里指定的版本,而不是重新进行所有版本的计算。这使得你拥有了一个自动的可重现的构建。换句话说,项目会继续使用0.3.14直到你显式升级,多亏了 Cargo.lock 文件。我们将会在这个文件编写全部的代码。

    +

    Cargo 有一个机制来确保任何人在任何时候重新构建代码,都会产生相同的结果:Cargo 只会使用你指定的依赖的版本,除非你又手动指定了别的。例如,如果下周 rand crate 的 v0.3.15 版本出来了,它修复了一个重要的 bug,同时也含有一个缺陷,会破坏代码的运行,这时会发生什么呢?

    +

    答案是 Cargo.lock 文件。它在第一次运行 cargo build 时创建,并放在 guessing_game 目录,Cargo 计算出所有符合要求的依赖版本并写入 Cargo.lock 文件。当将来构建项目时,如果 Cargo.lock 存在,Cargo 就使用里面指定的版本,不会重新计算。自动使你拥有了一个可重现的构建。换句话说,项目会继续使用 0.3.14 直到你显式升级,感谢 Cargo.lock

    更新 crate 到一个新版本

    当你确实需要升级 crate 时,Cargo 提供了另一个命令,update,他会:

      -
    1. 忽略 Cargo.lock 文件并计算出所有符合 Cargo.toml 中规格的最新版本。
    2. +
    3. 忽略 Cargo.lock 文件,并计算出所有符合 Cargo.toml 声明的最新版本。
    4. 如果成功了,Cargo 会把这些版本写入 Cargo.lock 文件。
    -

    不过,Cargo 默认只会寻找大于0.3.0而小于0.4.0的版本。如果rand crate 发布了两个新版本,0.3.150.4.0,在运行cargo update时会出现如下内容:

    +

    不过,Cargo 默认只会寻找大于 0.3.0 而小于 0.4.0 的版本。如果 rand crate 发布了两个新版本,0.3.150.4.0,在运行 cargo update 时会出现如下内容:

    $ cargo update
         Updating registry `https://github.com/rust-lang/crates.io-index`
         Updating rand v0.3.14 -> v0.3.15
     

    这时,值得注意的是 Cargo.lock 文件中的一个改变,rand crate 现在使用的版本是0.3.15

    -

    如果想要使用0.4.0版本的rand或是任何0.4.x系列的版本,必须像这样更新 Cargo.toml 文件:

    +

    如果想要使用 0.4.0 版本的 rand 或是任何 0.4.x 系列的版本,必须像这样更新 Cargo.toml 文件:

    [dependencies]
     
     rand = "0.4.0"
     
    -

    下一次运行cargo build时,Cargo 会更新 registry 中可用的 crate 并根据你指定新版本重新计算rand的要求。

    -

    第十四章会讲到Cargo它的生态系统的更多内容,不过目前你只需要了解这么多。Cargo 使得复用库文件变得非常容易,所以 Rustacean 们能够通过组合很多包来编写出更轻巧的项目。

    +

    下一次运行 cargo build 时,Cargo 会从 registry 更新,并根据你指定的新版本重新计算。

    +

    第十四章会讲到 Cargo 及其生态系统的更多内容,不过目前你只需要了解这么多。通过 Cargo 复用库文件非常容易,因此 Rustacean 能够编写出由很多包组装而成的更轻巧的项目。

    生成一个随机数

    -

    让我们开始使用rand。下一步是更新 src/main.rs,如列表 2-3 所示:

    +

    让我们开始使用 rand。下一步是更新 src/main.rs,如列表 2-3 所示:

    Filename: src/main.rs

    -
    extern crate rand;
    +
    extern crate rand;
     
     use std::io;
     use rand::Rng;
    @@ -508,10 +508,10 @@ fn main() {
     

    Listing 2-3: Code changes needed in order to generate a random number

    -

    我们在顶部增加一行extern crate rand;来让 Rust 知道我们要使用外部依赖。这也会调用相应的use rand,所以现在可以使用rand::前缀来调用rand中的任何内容。

    -

    接下来,我们增加了另一行useuse rand::RngRng是一个定义了随机数生成器应实现方法的 trait,如果要使用这些方法的话这个 trait 必须在作用域中。第十章会详细介绍 trait。

    -

    另外,中间还新增加了两行。rand::thread_rng函数会提供具体会使用的随机数生成器:它位于当前执行线程本地并从操作系统获取 seed。接下来,调用随机数生成器的gen_range方法。这个方法由我们使用use rand::Rng语句引入到作用域的Rng trait 定义。gen_range方法获取两个数作为参数并生成一个两者之间的随机数。它包含下限但不包含上限,所以需要指定1101来请求一个1100之间的数。

    -

    并不是仅仅能够知道该 use 哪个 trait 和该从 crate 中调用哪个方法。如何使用 crate 的说明位于每个 crate 的文档中。Cargo 另一个很棒的功能是可以运行cargo doc --open命令来构建所有本地依赖提供的文档并在浏览器中打开。例如,如果你对rand crate 中的其他功能感兴趣,运行cargo doc --open并点击左侧导航栏中的rand

    +

    我们在顶部增加一行 extern crate rand; 通知 Rust 我们要使用外部依赖。这也会调用相应的 use rand,所以现在可以使用 rand:: 前缀来调用 rand 中的内容。

    +

    接下来,我们增加了一行 useuse rand::RngRng 是一个 trait,它定义了随机数生成器应实现的方法 ,想使用这些方法的话此 trait 必须在作用域中。第十章会详细介绍 trait。

    +

    另外,中间还新增加了两行。rand::thread_rng 函数提供实际使用的随机数生成器:它位于当前执行线程,并从操作系统获取 seed。接下来,调用随机数生成器的 gen_range 方法。这个方法由刚才引入到作用域的 Rng trait 定义。gen_range 方法获取两个数字作为参数,并生成一个范围在两者之间的随机数。它包含下限但不包含上限,所以需要指定1101来请求一个1100之间的数。

    +

    知道 use 哪个 trait 和该从 crate 中调用哪个方法并不是全部,crate 的说明位于其文档中,Cargo 有一个很棒的功能是:运行 cargo doc --open 命令来构建所有本地依赖提供的文档,并在浏览器中打开。例如,假设你对 rand crate 中的其他功能感兴趣,cargo doc --open 并点击左侧导航栏中的 rand

    新增加的第二行代码打印出了秘密数字。这在开发程序时很有用,因为我们可以去测试它,不过在最终版本我们会删掉它。游戏一开始就打印出结果就没什么可玩的了!

    尝试运行程序几次:

    $ cargo run
    @@ -534,7 +534,7 @@ You guessed: 5
     

    比较猜测与秘密数字

    现在有了用户输入和一个随机数,我们可以比较他们。这个步骤如列表 2-4 所示:

    Filename: src/main.rs

    -
    extern crate rand;
    +
    extern crate rand;
     
     use std::io;
     use std::cmp::Ordering;
    @@ -565,17 +565,17 @@ fn main() {
     

    Listing 2-4: Handling the possible return values of comparing two numbers

    -

    新代码的第一行是另一个use,从标准库引入了一个叫做std::cmp::Ordering的类型到作用域。Ordering是另一个枚举,像Result一样,不过Ordering的成员是LessGreaterEqual。这是你比较两个值时可能出现三种结果。

    -

    接着在底部的五行新代码使用了Ordering类型:

    -
    match guess.cmp(&secret_number) {
    +

    新代码的第一行是另一个 use,从标准库引入了一个叫做 std::cmp::Ordering 的类型。Ordering 是一个像 Result 一样的枚举,不过它的成员是 LessGreaterEqual。这是你做比较时可能出现的三种结果。

    +

    接着,底部的五行新代码使用了 Ordering 类型:

    +
    match guess.cmp(&secret_number) {
         Ordering::Less    => println!("Too small!"),
         Ordering::Greater => println!("Too big!"),
         Ordering::Equal   => println!("You win!"),
     }
     
    -

    cmp方法比较两个值并可以在任何可比较的值上调用。它获取一个任何你想要比较的值的引用:这里是把guesssecret_number做比较。cmp返回一个使用use语句引入作用域的Ordering枚举的成员。我们使用一个match表达式根据对guesssecret_number中的值调用cmp后返回的哪个Ordering枚举成员来决定接下来干什么。

    -

    一个match表达式由 分支(arms) 构成。一个分支包含一个 模式pattern)和代码,这些代码在match表达式开头给出的值符合分支的模式时将被执行。Rust 获取提供给match的值并挨个检查每个分支的模式。match结构和模式是 Rust 中非常强大的功能,它帮助你体现代码可能遇到的多种情形并帮助你处理全部的可能。这些功能将分别在第六章和第十八章详细介绍。

    -

    让我们看看一个使用这里的match表达式会发生什么的例子。假设用户猜了 50,这时随机生成的秘密数字是 38。当代码比较 50 与 38 时,cmp方法会返回Ordering::Greater,因为 50 比 38 要大。Ordering::Greatermatch表达式得到的值。它检查第一个分支的模式,Ordering::Less,不过值Ordering::Greater并不匹配Ordering::Less。所以它忽略了这个分支的代码并移动到下一个分支。下一个分支的模式,Ordering::Greater正确匹配了Ordering::Greater!这个分支关联的代码会被执行并在屏幕打印出Too big!match表达式就此终止,因为在这个特定场景下没有检查最后一个分支的必要。

    +

    cmp 方法用来比较两个值。在任何可比较的值上调用,然后获取另一个被比较值的引用:这里是把 guesssecret_number 做比较,返回一个 Ordering 枚举的成员。再使用一个 match 表达式,根据枚举成员来决定接下来干什么。

    +

    一个 match 表达式由 分支(arms) 构成。一个分支包含一个 模式pattern)和动作,表达式头的求值结果符合分支的模式时将执行对应的动作。Rust 获取提供给 match 的值并挨个检查每个分支的模式。match 结构和模式是 Rust 的强大功能,它体现了代码可能遇到的多种情形,并帮助你没有遗漏的处理。这些功能将分别在第六章和第十八章详细介绍。

    +

    让我们看看使用 match 表达式的例子。假设用户猜了 50,这时随机生成的秘密数字是 38。比较 50 与 38 时,因为 50 比 38 要大,cmp 方法会返回 Ordering::Greatermatch 表达式得到该值,然后检查第一个分支的模式,Ordering::LessOrdering::Greater并不匹配,所以它忽略了这个分支的动作并来到下一个分支。下一个分支的模式是 Ordering::Greater正确匹配!这个分支关联的动作被执行,在屏幕打印出 Too big!match 表达式就此终止,因为该场景下没有检查最后一个分支的必要。

    然而,列表 2-4 的代码并不能编译,可以尝试一下:

    $ cargo build
        Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    @@ -591,10 +591,10 @@ error[E0308]: mismatched types
     error: aborting due to previous error
     Could not compile `guessing_game`.
     
    -

    错误的核心表明这里有不匹配的类型mismatched types)。Rust 拥有一个静态强类型系统。不过,它也有类型推断。当我们写出let guess = String::new()时,Rust 能够推断出guess应该是一个String,并不需要我们写出类型。另一方面,secret_number,是一个数字类型。一些数字类型拥有 1 到 100 之间的值:i32,一个 32 位的数字;u32,一个 32 位无符号数字;i64,一个 64 位数字;等等。Rust 默认使用i32,所以secret_number的类型就是它,除非增加类型信息或任何能让 Rust 推断出不同数值类型的信息。这里错误的原因在于 Rust 不会比较字符串类型和数字类型。

    -

    最终我们想要把程序从输入中读取到的String转换为一个真正的数字类型,这样好与秘密数字向比较。可以通过在main函数体中增加如下两行代码来实现:

    +

    错误的核心表明这里有不匹配的类型mismatched types)。Rust 有一个静态强类型系统,同时也有类型推断。当我们写出 let guess = String::new() 时,Rust 推断出 guess 应该是一个String,不需要我们写出类型。另一方面,secret_number,是一个数字类型。多种数字类型拥有 1 到 100 之间的值:32 位数字 i32;32 位无符号数字 u32;64 位数字 i64 等等。Rust 默认使用 i32,所以它是 secret_number 的类型,除非增加类型信息,或任何能让 Rust 推断出不同数值类型的信息。这里错误的原因在于 Rust 不会比较字符串类型和数字类型。

    +

    所以我们必须把从输入中读取到的 String 转换为一个真正的数字类型,才好与秘密数字进行比较。可以通过在 main 函数体中增加如下两行代码来实现:

    Filename: src/main.rs

    -
    extern crate rand;
    +
    extern crate rand;
     
     use std::io;
     use std::cmp::Ordering;
    @@ -627,13 +627,13 @@ fn main() {
     }
     

    这两行代码是:

    -
    let guess: u32 = guess.trim().parse()
    +
    let guess: u32 = guess.trim().parse()
         .expect("Please type a number!");
     
    -

    这里创建了一个叫做guess的变量。不过等等,难道这个程序不是已经有了一个叫做guess的变量了吗?确实如此,不过 Rust 允许我们通过覆盖shadow) 用一个新值来覆盖guess之前的值。这个功能经常用在类似需要把一个值从一种类型转换到另一种类型的场景。shadowing 允许我们复用guess变量的名字而不是强迫我们创建两个不同变量,比如guess_strguess。(第三章会介绍 shadowing 的更多细节。)

    -

    guess被绑定到guess.trim().parse()表达式。表达式中的guess对应包含输入的String类型的原始guessString实例的trim方法会消除字符串开头和结尾的空白。u32只能包含数字字符。不过用户必须输入回车键才能让read_line返回。当用户按下回车键时,会在字符串中增加一个换行(newline)字符。例如,如果用户输入 5 并回车,guess看起来像这样:5\n\n代表“换行”,回车键。trim方法消除\n,只留下5

    -

    字符串的parse方法解析一个字符串成某个数字。因为这个方法可以解析多种数字类型,需要告诉 Rust 我们需要的具体的数字类型,这里通过let guess: u32指定。guess后面的冒号(:)告诉 Rust 我们指明了变量的类型。Rust 有一些内建的数字类型;这里的u32是一个无符号的 32 位整型。它是一个好的较小正整数的默认类型。第三章会讲到其他数字类型。另外,例子程序中的u32注解和与secret_number的比较意味着 Rust 会推断secret_number应该是也是u32类型。现在可以使用相同类型比较两个值了!

    -

    parse调用容易产生错误。例如,如果字符串包含A👍%,就无法将其转换为一个数字。因为它可能失败,parse方法返回一个Result类型,非常像之前在“使用Result类型来处理潜在的错误”部分讨论的read_line方法。这里再次类似的使用expect方法处理这个Result类型。如果parse因为不能从字符串生成一个数字而返回一个ErrResult成员时,expect会使游戏崩溃并打印提供给它的信息。如果parse能成功地将字符串转换为一个数字,它会返回ResultOk成员,同时expect会返回Ok中我们需要的数字。

    +

    这里创建了一个叫做 guess 的变量。不过等等,不是已经有了一个叫做guess的变量了吗?确实如此,不过 Rust 允许遮盖shadow),用一个新值来遮盖 guess 之前的值。这个功能常用在需要转换值类型之类的场景,它允许我们复用 guess 变量的名字,而不是被迫创建两个不同变量,诸如 guess_strguess 之类。(第三章会介绍 shadowing 的更多细节。)

    +

    guess 被绑定到 guess.trim().parse() 表达式。表达式中的 guess 是包含输入的 String 类型。String 实例的 trim 方法会去除字符串开头和结尾的空白。u32 只能由数字字符转换,不过用户必须输入回车键才能让 read_line 返回,然而用户按下回车键时,会在字符串中增加一个换行(newline)符。例如,用户输入 5 并回车,guess 看起来像这样:5\n\n 代表“换行”,回车键。trim 方法消除 \n,只留下5

    +

    字符串的parse方法 将字符串解析成数字。这个方法可以解析多种数字类型,因此需要告诉 Rust 具体的数字类型,这里通过 let guess: u32 指定。guess 后面的冒号(:)告诉 Rust 我们指定了变量的类型。Rust 有一些内建的数字类型;u32 是一个无符号的 32 位整型。对于不大的正整数来说,它是不错的类型,第三章还会讲到其他数字类型。另外,程序中的 u32 注解以及与 secret_number 的比较,意味着 Rust 会推断出 secret_number 也是 u32 类型。现在可以使用相同类型比较两个值了!

    +

    parse 调用可能产生错误。例如,字符串中包含 A👍%,就无法将其转换为一个数字。因此,parse 方法返回一个 Result 类型。像之前讨论的 read_line 方法,按部就班的用 expect 方法处理即可。如果 parse 不能从字符串生成一个数字,返回一个 Result::Err 时,expect 会使游戏崩溃并打印附带的信息。如果 parse 成功地将字符串转换为一个数字,它会返回 Result::Ok,然后 expect 会返回 Ok 中的数字。

    现在让我们运行程序!

    $ cargo run
        Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    @@ -648,9 +648,9 @@ Too big!
     

    漂亮!即便是在猜测之前添加了空格,程序依然能判断出用户猜测了 76。多运行程序几次来检验不同类型输入的相应行为:猜一个正确的数字,猜一个过大的数字和猜一个过小的数字。

    现在游戏已经大体上能玩了,不过用户只能猜一次。增加一个循环来改变它吧!

    使用循环来允许多次猜测

    -

    loop关键字提供了一个无限循环。增加它后给了用户多次猜测的机会:

    +

    loop 关键字提供了一个无限循环。将其加入后,用户可以反复猜测:

    Filename: src/main.rs

    -
    extern crate rand;
    +
    extern crate rand;
     
     use std::io;
     use std::cmp::Ordering;
    @@ -684,8 +684,8 @@ fn main() {
         }
     }
     
    -

    如上所示,我们将提示用户猜测之后的所有内容放入了循环。确保这些代码多缩进了四个空格,并再次运行程序。注意这里有一个新问题,因为程序忠实地执行了我们要求它做的:永远地请求另一个猜测!看起来用户没法退出啊!

    -

    用户总是可以使用Ctrl-C快捷键来终止程序。不过这里还有另一个逃离这个贪得无厌的怪物的方法,就是在“比较猜测”部分提到的parse:如果用户输入一个非数字回答,程序会崩溃。用户可以利用这一点来退出,如下所示:

    +

    如上所示,我们将提示用户猜测之后的所有内容放入了循环。确保这些代码额外缩进了一层,再次运行程序。注意这里有一个新问题,因为程序忠实地执行了我们的要求:永远地请求另一个猜测,用户没法退出!

    +

    用户总能使用 Ctrl-C 终止程序。不过还有另一个方法跳出无限循环,就是“比较猜测”部分提到的 parse:如果用户输入一个非数字答案,程序会崩溃。用户可以利用这一点来退出,如下所示:

    $ cargo run
        Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
          Running `target/guessing_game`
    @@ -709,11 +709,11 @@ thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidD
     note: Run with `RUST_BACKTRACE=1` for a backtrace.
     error: Process didn't exit successfully: `target/debug/guess` (exit code: 101)
     
    -

    输入quit确实退出了程序,同时其他任何非数字输入也一样。然而,毫不夸张的说这是不理想的。我们想要当猜测正确的数字时游戏能自动退出。

    +

    输入 quit 确实退出了程序,同时其他任何非数字输入也一样。然而,这并不理想,我们想要当猜测正确的数字时游戏能自动退出。

    猜测正确后退出

    -

    让我们增加一个break来在用户胜利时退出游戏:

    +

    让我们增加一个 break,在用户猜对时退出游戏:

    Filename: src/main.rs

    -
    extern crate rand;
    +
    extern crate rand;
     
     use std::io;
     use std::cmp::Ordering;
    @@ -750,18 +750,18 @@ fn main() {
         }
     }
     
    -

    通过在You win!之后增加一行break,程序在用户猜对了神秘数字后会退出循环。退出循环也就意味着退出程序,因为循环是main的最后一部分。

    +

    通过在 You win! 之后增加一行 break,用户猜对了神秘数字后会退出循环。退出循环也意味着退出程序,因为循环是 main 的最后一部分。

    处理无效输入

    -

    为了进一步改善游戏性,而不是在用户输入非数字时崩溃,需要让游戏忽略非数字从而用户可以继续猜测。可以通过修改guessString转化为u32那部分代码来实现:

    -
    let guess: u32 = match guess.trim().parse() {
    +

    为了进一步改善游戏性,不要在用户输入非数字时崩溃,需要忽略非数字,让用户可以继续猜测。可以通过修改 guessString 转化为 u32 那部分代码来实现:

    +
    let guess: u32 = match guess.trim().parse() {
         Ok(num) => num,
         Err(_) => continue,
     };
     
    -

    expect调用切换到match语句是如何从遇到错误就崩溃到真正处理错误的常用手段。记住parse返回一个Result类型,而Result是一个拥有OkErr两个成员的枚举。在这里使用match表达式,就像之前处理cmp方法返回的Ordering一样。

    -

    如果parse能够成功的将字符串转换为一个数字,它会返回一个包含结果数字Ok值。这个Ok值会匹配第一个分支的模式,这时match表达式仅仅返回parse产生的Ok值之中的num值。这个数字会最终如期变成新创建的guess变量。

    -

    如果parse能将字符串转换为一个数字,它会返回一个包含更多错误信息的Err值。Err值不能匹配第一个match分支的Ok(num)模式,但是会匹配第二个分支的Err(_)模式。_是一个包罗万象的值;在这个例子中,我们想要匹配所有Err值,不管其中有何种信息。所以程序会执行第二个分支的代码,continue,这意味着进入loop的下一次循环并请求另一个猜测。这样程序就有效地忽略了parse可能遇到的所有错误!

    -

    现在万事俱备(只欠东风)了。运行cargo run来尝试一下:

    +

    expect 调用换成 match 语句,是从“立即崩溃”转到真正处理错误的惯用方法。须知 parse 返回一个 Result 类型,而 Result 是一个拥有 OkErr 成员的枚举。这里使用的 match 表达式,和之前处理 cmp 方法返回 Ordering 时用的一样。

    +

    如果 parse 能够成功的将字符串转换为一个数字,它会返回一个包含结果数字的 Ok。这个 Ok 值与match 第一个分支的模式相匹配,该分支对应的动作返回 Ok 值中的数字 num,最后如愿变成新创建的 guess 变量。

    +

    如果 parse 能将字符串转换为一个数字,它会返回一个包含更多错误信息的 ErrErr 值不能匹配第一个 match 分支的 Ok(num) 模式,但是会匹配第二个分支的 Err(_) 模式:_ 是一个兜底值,用来匹配所有 Err 值,不管其中有何种信息。所以程序会执行第二个分支的动作,continue 意味着进入 loop 的下一次循环,请求另一个猜测。这样程序就忽略了 parse 可能遇到的所有错误!

    +

    现在万事俱备,只需运行 cargo run

    $ cargo run
        Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
          Running `target/guessing_game`
    @@ -782,9 +782,9 @@ Please input your guess.
     You guessed: 61
     You win!
     
    -

    太棒了!再有最后一个小的修改,就能完成猜猜看游戏了:还记得程序依然会打印出秘密数字。这在测试时还好,但会毁了游戏性。删掉打印秘密数字的println!。列表 2-5 为最终代码:

    +

    太棒了!再有最后一个小的修改,就能完成猜猜看游戏了:还记得程序依然会打印出秘密数字。在测试时还好,但正式发布时会毁了游戏。删掉打印秘密数字的 println!。列表 2-5 为最终代码:

    Filename: src/main.rs

    -
    extern crate rand;
    +
    extern crate rand;
     
     use std::io;
     use std::cmp::Ordering;
    @@ -824,18 +824,18 @@ fn main() {
     

    Listing 2-5: Complete code of the guessing game

    总结

    此时此刻,你顺利完成了猜猜看游戏!恭喜!

    -

    这是一个通过动手实践的方式想你介绍许多 Rust 新知识的项目:letmatch、方法、关联函数,使用外部 crate,等等。接下来的几章,我们将会详细学习这些概念。第三章涉及到大部分编程语言都有的概念,比如变量、数据类型和函数,以及如何在 Rust 中使用他们。第四章探索所有权(ownership),这是一个 Rust 同其他语言都不相同的功能。第五章讨论结构体和方法的语法,而第六章侧重解释枚举。

    +

    这是一个通过动手实践学习 Rust 新概念的项目:letmatch、方法、关联函数、使用外部 crate 等等,接下来的几章,我们将会继续深入。第三章涉及到大部分编程语言都有的概念,比如变量、数据类型和函数,以及如何在 Rust 中使用他们。第四章探索所有权(ownership),这是一个 Rust 同其他语言都不相同的功能。第五章讨论结构体和方法的语法,而第六章侧重解释枚举。

    通用编程概念

    ch03-00-common-programming-concepts.md
    commit 04aa3a45eb72855b34213703718f50a12a3eeec8

    -

    这一章涉及到几乎出现在所有编程语言中的概念,以及他们在 Rust 中是如何工作的。很多编程语言在核心概念上都是共通的。本章中展示的所有概念没有一个是 Rust 所特有的,不过我们会在 Rust 环境中讨论他们并解释他们的使用习惯。

    -

    具体的,我们将会学习变量,基本类型,函数,注释和控制流。这些基础知识将会出现在每一个 Rust 程序中,提早学习这些概念会使你在起步时拥有一个强有力的基础。

    +

    本章涉及一些几乎所有编程语言都有的概念,以及他们在 Rust 中是如何工作的。很多编程语言的核心概念都是共通的,本章中展示的概念都不是 Rust 特有的,不过我们会在 Rust 环境中讨论他们,解释他们的使用习惯。

    +

    具体的,我们将会学习变量,基本类型,函数,注释和控制流。这些基础知识将会出现在每一个 Rust 程序中,提早学习这些概念会使你拥有坚实的起步。

    关键字

    -

    Rust 语言有一系列被保留为只能被语言使用的关键字keywords),如大部分语言一样。注意你不能使用这些关键字作为变量或函数的名称。大部分关键字有特殊的意义,并将会被用来进行 Rust 程序中的多种任务;一些关键字目前没有相关的功能不过为了将来可能添加进 Rust 的功能而被保留。可以在附录 A 中找到一份关键字的列表

    +

    Rust 语言有一系列保留的关键字keywords),只能由语言本身使用,像大部分语言一样。你不能使用这些关键字作为变量或函数的名称,大部分关键字有特殊的意义,并被用来完成 Rust 程序中的各种任务;一些关键字目前没有分配,是为将来可能添加的功能保留的。可以在附录 A 中找到关键字的列表。

    变量和可变性

    @@ -843,8 +843,8 @@ commit 04aa3a45eb72855b34213703718f50a12a3eeec8


    commit 04aa3a45eb72855b34213703718f50a12a3eeec8

    -

    第二章中提到过,变量默认是不可变immutable)的。这是 Rust 中许多鼓励利用 Rust 提供的安全和简单并发优势编写代码的助力之一。不过,仍然有使变量可变的选项。让我们探索一下如何以及为什么鼓励你拥抱不可变性,还有为什么你可能想要弃之不用。

    -

    当变量是不可变时,这意味着一旦一个值被绑定上了一个名称,你就不能改变这个值。作为说明,通过cargo new --bin variablesprojects 目录生成一个叫做 variables 的新项目。

    +

    第二章中提到过,变量默认是不可变immutable)的。这是利用 Rust 安全和简单并发的优势编写代码一大助力。不过,变量仍然有可变的选项。让我们探讨一下,拥抱不可变性的原因及方法,以及何时你不想拥抱。

    +

    当变量不可变时,意味着一旦值被绑定上一个名称,你就不能改变这个值。作为说明,通过cargo new --bin variablesprojects 目录生成一个叫做 variables 的新项目。

    接着,在新建的 variables 目录,打开 src/main.rs 并替换其代码为如下:

    Filename: src/main.rs

    fn main() {
    @@ -866,10 +866,10 @@ error[E0384]: re-assignment of immutable variable `x`
     4 |     x = 6;
       |     ^^^^^ re-assignment of immutable variable
     
    -

    这个例子显示了编译器如何帮助你寻找程序中的错误。即便编译器错误可能是令人沮丧的,他们也仅仅意味着程序不能安全的完成你想让它完成的工作;他们不能说明你不是一个好的程序员!有经验的 Rustacean 们也会遇到编译器错误。这些错误表明错误的原因是对不可变变量重新赋值re-assignment of immutable variable),因为我们尝试对不可变变量x赋第二个值。

    -

    当尝试去改变之前设计为不可变的值出现编译时错误是很重要的,因为这种情况可能导致 bug。如果代码的一部分假设一个值永远也不会改变而另一部分代码改变了它,这样第一部分代码就有可能不能像它设计的那样运行。不得不承认这种 bug 难以跟踪,尤其是当第二部分代码只是有时当变量使不可变时。

    -

    Rust 编译器保证如果声明一个值不会改变,它就真的不会改变。这意味着当阅读和编写代码时,并不需要记录如何以及在哪可能会被改变,这使得代码易于推导。

    -

    不过可变性也是非常有用的。变量只是默认不可变;可以通过在变量名之前增加mut来使其可变。它向之后的读者表明了其他部分的代码将会改变这个变量值的意图。

    +

    这个例子展示了编译器如何帮助你找出程序中的错误。即便编译错误令人沮丧,那也不过是说程序不能安全的完成你想让它完成的工作;而不能说明你是不是一个好程序员!有经验的 Rustacean 们一样会遇到编译错误。这些错误给出的原因是对不可变变量重新赋值re-assignment of immutable variable),因为我们尝试对不可变变量x赋第二个值。

    +

    尝试去改变预设为不可变的值,产生编译错误是很重要的,因为这种情况可能导致 bug:如果代码的一部分假设一个值永远也不会改变,而另一部分代码改变了它,第一部分代码就有可能以不可预料的方式运行。不得不承认这种 bug 难以跟踪,尤其是第二部分代码只是有时改变其值。

    +

    Rust 编译器保证,如果声明一个值不会变,它就真的不会变。这意味着当阅读和编写代码时,不需要厘清如何以及哪里可能会被改变,从而使得代码易于推导。

    +

    不过可变性也是非常有用的。变量只是默认不可变;可以通过在变量名之前加 mut 来使其可变。它向读者表明了其他代码将会改变这个变量的意图。

    例如,改变 src/main.rs 并替换其代码为如下:

    Filename: src/main.rs

    fn main() {
    @@ -886,21 +886,21 @@ error[E0384]: re-assignment of immutable variable `x`
     The value of x is: 5
     The value of x is: 6
     
    -

    通过mut,允许把绑定到x的值从5改成6。在一些情况下,你会想要一个变量是可变的,因为这比只使用不可变变量实现的代码更易于编写。

    -

    除了避免 bug 外,这里还有多个需要权衡取舍的地方。例如,有时使用大型数据结构时,适当地使变量可变可能比复制和返回新分配的实例要更快。对于较小的数据结构,总是创建新实例并采用一种更函数式的编程风格可能会使代码更易理解。所以为了可读性而造成的性能惩罚也许是值得的。

    +

    通过 mut,允许把绑定到 x 的值从 5 改成 6。在一些情况下,你会想要一个变量可变,因为相对不可变的风格更容易写。

    +

    除了避免 bug 外,还有多处需要权衡取舍。例如,使用大型数据结构时,适当地使变量可变,可能比复制和返回新分配的实例更快。对于较小的数据结构,总是创建新实例,采用更偏向函数式的风格编程,可能会使代码更易理解,为可读性而遭受性能惩罚或许值得。

    变量和常量的区别

    -

    不能改变一个变量的值可能会使你想起另一个大部分编程语言都有的概念:常量constants)。类似于不可变变量,常量也是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。

    -

    首先,不允许对常量使用mut:常量不光是默认不能改变,它总是不能改变。

    -

    常量使用const关键字而不是let关键字声明,而且必须注明值的类型。现在我们准备在下一部分,“数据类型”,涉及到类型和类型注解,所以现在无需担心这些细节,只需记住必须总是注明类型即可。

    +

    不允许改变值的变量,可能会使你想起另一个大部分编程语言都有的概念:常量constants)。类似于不可变变量,常量也是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。

    +

    首先,不允许对常量使用 mut:常量不光默认不能变,它总是不能变。

    +

    声明常量使用 const 关键字而不是 let,而且必须注明值的类型。在下一部分,“数据类型”,涉及到类型和类型注解,现在无需关心这些细节,记住总是标注类型即可。

    常量可以在任何作用域声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。

    -

    最后一个区别是常量只能用于常量表达式,而不能作为函数调用的结果或任何其他只在运行时计算的值。

    -

    这是一个常量声明的例子,它的名称是MAX_POINTS而它的值是 100,000。(Rust 常量的命名规范是使用大写字母和单词间使用下划线):

    +

    最后一个区别是常量只能用于常量表达式,而不能作为函数调用的结果,或任何其他只在运行时计算的值。

    +

    这是一个常量声明的例子,它的名称是 MAX_POINTS,值是 100,000。(常量使用下划线分隔的大写字母命名):

    const MAX_POINTS: u32 = 100_000;
     
    -

    常量在整个程序生命周期中都有效,位于它声明的作用域之中。这使得常量可以用作多个部分的代码可能需要知道的程序范围的值,例如一个游戏中任何玩家可以获得的最高分或者光速。

    -

    将用于整个程序的硬编码的值声明为为常量(并编写文档)对为将来代码维护者表明值的意义是很有用的。它也能帮助你将硬编码的值至于一处以便将来可能需要修改他们。

    -

    覆盖(Shadowing)

    -

    如第二章猜猜看游戏所讲到的,我们可以定义一个与之前变量名称相同的新变量,而新变量会覆盖之前的变量。Rustacean 们称其为第一个变量被第二个给覆盖了,这意味着第二个变量的值是使用这个变量时会看到的值。可以用相同变量名称来覆盖它自己以及重复使用let关键字来多次覆盖,如下所示:

    +

    常量在整个程序生命周期中都有效,位于它声明的作用域之中。这使得常量可以作为多处代码使用的全局范围的值,例如一个游戏中所有玩家可以获取的最高分或者光速。

    +

    将作用于整个程序的值,由硬编码改为常量(并编写文档),对后来的维护者了解值的意义很用帮助。它也能将硬编码的值汇总一处,为将来可能的修改提供方便。

    +

    遮盖(Shadowing)

    +

    如第二章“猜猜看游戏”所讲的,我们可以定义一个与之前变量重名的新变量,而新变量会遮盖之前的变量。Rustacean 称之为“第一个变量被第二个遮盖了”,这意味着使用这个变量时会看第二个值。可以用相同变量名称来遮盖它自己,以及重复使用 let 关键字来多次遮盖,如下所示:

    Filename: src/main.rs

    fn main() {
         let x = 5;
    @@ -912,22 +912,22 @@ The value of x is: 6
         println!("The value of x is: {}", x);
     }
     
    -

    这个程序首先将x绑定到值5上。接着通过let x =覆盖x,获取原始值并加1这样x的值就变成6了。第三个let语句也覆盖了x,获取之前的值并乘以2x的最终值是12。当运行这个程序,它会有如下输出:

    +

    这个程序首先将 x 绑定到值 5 上。接着通过 let x = 遮盖 x,获取原始值并加 1 这样 x 的值就变成 6 了。第三个 let 语句也覆盖了 x,获取之前的值并乘以 2x 最终的值是 12。运行这个程序,它会有如下输出:

    $ cargo run
        Compiling variables v0.1.0 (file:///projects/variables)
          Running `target/debug/variables`
     The value of x is: 12
     
    -

    这与将变量声明为mut是有区别的。因为除非再次使用let关键字,不小心尝试对变量重新赋值会导致编译时错误。我们可以用这个值进行一些计算,不过计算完之后变量仍然是不变的。

    -

    另一个mut与覆盖的区别是当再次使用let关键字时,事实上创建了一个新变量,我们可以改变值的类型。例如,假设程序请求用户输入空格来提供在一些文本之间需要多少空间来分隔,不过我们真正需要的是将输入存储成数字(多少个空格):

    +

    这与将变量声明为 mut 是有区别的。因为除非再次使用 let 关键字,不小心尝试对变量重新赋值会导致编译时错误。我们可以用这个值进行一些计算,不过计算完之后变量仍然是不变的。

    +

    mut 与遮盖的另一个区别是,当再次使用 let 时,实际上创建了一个新变量,我们可以改变值的类型。例如,假设程序请求用户输入空格来提供文本间隔,然而我们真正需要的是将输入存储成数字(多少个空格):

    let spaces = "   ";
     let spaces = spaces.len();
     
    -

    这里允许第一个spaces变量是字符串类型,而第二个spaces变量,它是一个恰巧与第一个变量名字相同的崭新的变量,它是数字类型。因此覆盖使我们不必使用不同的名字,比如spaces_strspaces_num;相反,我们可以复用spaces这个更简单的名称。然而,如果尝试使用mut,如下所示:

    +

    这里允许第一个 spaces 变量是字符串类型,而第二个 spaces 变量,它是一个恰巧与第一个变量同名的崭新变量,是数字类型。遮盖使我们不必使用不同的名字,如 spaces_strspaces_num;相反,我们可以复用 spaces 这个更简单的名字。然而,如果尝试使用mut,如下所示:

    let mut spaces = "   ";
     spaces = spaces.len();
     
    -

    会导致一个编译时错误,因为不允许改变一个变量的类型:

    +

    会导致一个编译错误,因为改变一个变量的类型是不被允许的:

    error[E0308]: mismatched types
      --> src/main.rs:3:14
       |
    @@ -944,11 +944,11 @@ spaces = spaces.len();
     
    commit fe4833a8ef2853c55424e7747a4ef8dd64c35b32

    -

    Rust 中的任何值都有一个具体的类型type),这告诉了 Rust 它被指定了何种数据,这样 Rust 就知道如何处理这些数据了。这一部分将讲到一些语言内建的类型。我们将这些类型分为两个子集:标量(scalar)和复合(compound)。

    -

    贯穿整个部分,请记住 Rust 是一个静态类型statically typed)语言,也就是说必须在编译时就知道所有变量的类型。编译器通常可以通过值以及如何使用他们来推断出我们想要用的类型。当多个类型都是可能的时候,比如第二章中parseString转换为数字类型,必须增加类型注解,像这样:

    +

    在 Rust 中,任何值都属于一种明确的类型type),声明它被指定了何种数据,以便明确其处理方式。我们将分两部分探讨一些内建类型:标量(scalar)和复合(compound)。

    +

    Rust 是静态类型statically typed)语言,也就是说在编译时就需要知道所有变量的类型,这一认知将贯穿整个章节,请在头脑中明确。通过值的形式及其使用方式,编译器通常可以推断出我们想要用的类型。多种类型均有可能时,比如第二章中使用 parseString 转换为数字,必须增加类型注解,像这样:

    let guess: u32 = "42".parse().expect("Not a number!");
     
    -

    如果这里不添加类型注解,Rust 会显示如下错误,它意味着编译器需要我们提供更多我们想要使用哪个可能的类型的信息:

    +

    如果不添加类型注解,Rust 会显示如下错误。这说明编译器需要更多信息,来了解我们想要的类型:

    error[E0282]: unable to infer enough type information about `_`
      --> src/main.rs:2:9
       |
    @@ -957,11 +957,11 @@ commit fe4833a8ef2853c55424e7747a4ef8dd64c35b32

    | = note: type annotations or generic parameter binding required
    -

    在我们讨论各种数据类型时会看到不同的类型注解。

    +

    在我们讨论各种数据类型时,你会看到多样的类型注解。

    标量类型

    标量类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型。你可能在其他语言中见过他们,不过让我们深入了解他们在 Rust 中时如何工作的。

    整型

    -

    整数是一个没有小数部分的数字。我们在这一章的前面使用过一个整型,i32类型。这个类型声明表明在 32 位系统上它关联的值应该是一个有符号整数(因为这个i,与u代表的无符号相对)。表格 3-1 展示了 Rust 内建的整数类型。每一个变体的有符号和无符号列(例如,i32)可以用来声明对应的整数值。

    +

    整数是一个没有小数部分的数字。我们在这一章的前面使用过 i32 类型。该类型声明指示,i32 关联的值应该是一个占据32比特位的有符号整数(因为这个i,与u代表的无符号相对)。表格 3-1 展示了 Rust 内建的整数类型。每一种变体的有符号和无符号列(例如,i32)可以用来声明对应的整数值。

    Table 3-1: Integer Types in Rust

    @@ -970,10 +970,10 @@ commit fe4833a8ef2853c55424e7747a4ef8dd64c35b32

    Length Signed Unsigned
    8-bit i8 u8
    64-bit i64 u64
    arch isize usize
    -

    每一种变体都可以是有符号或无符号的并有一个显式的大小。有符号和无符号代表数字是否能够是正数或负数;换句话说,数字是否需要有一个符号(有符号数)或者永远只需要是正的这样就可以不用符号(无符号数)。这有点像在纸上书写数字:当需要考虑符号的时候,数字前面会加上一个加号或减号;然而,当可以安全地假设为正数时,可以不带符号(加号)。有符号数以二进制补码形式(two’s complement representation)存储(如果你不清楚这是什么,可以在网上搜索;对其的解释超出了本书的范畴)。

    -

    每一个有符号的变体可以储存包含从 -(2n - 1) 到 2n - 1 - 1 在内的数字,这里n是变体使用的位数。所以i8可以储存从 -(27) 到 27 - 1 在内的数字,也就是从 -128 到 127。无符号的变体可以储存从 0 到 2n - 1 的数字,所以u8可以储存从 0 到 28 - 1 的数字,也就是从 0 到 255。

    -

    另外,isizeusize类型依赖运行程序的计算机类型(构架):64 位构架他们是 64 位的而 32 位构架他们就是 32 位的。

    -

    可以使用表格 3-2 中的任何一种形式编写数字字面值。注意除了字节字面值以外的数字字面值允许使用类型后缀,例如57u8,而_是可视化分隔符(visual separator),例如1_000位的。

    +

    每一种变体都可以是有符号或无符号的,并有一个明确的大小。有符号和无符号代表数字能否为负值;换句话说,数字是否需要有一个符号(有符号数),或者永远为正而不需要符号(无符号数)。这有点像在纸上书写数字:当需要考虑符号的时候,数字以加号或减号作为前缀;然而,可以安全地假设为正数时,加号前缀通常省略。有符号数以二进制补码形式(two’s complement representation)存储(如果你不清楚这是什么,可以在网上搜索;对其的解释超出了本书的范畴)。

    +

    每一个有符号的变体可以储存包含从 -(2n - 1) 到 2n - 1 - 1 在内的数字,这里 n 是变体使用的位数。所以 i8 可以储存从 -(27) 到 27 - 1 在内的数字,也就是从 -128 到 127。无符号的变体可以储存从 0 到 2n - 1 的数字,所以 u8 可以储存从 0 到 28 - 1 的数字,也就是从 0 到 255。

    +

    另外,isizeusize 类型依赖运行程序的计算机架构:64 位架构上他们是 64 位的, 32 位架构上他们是 32 位的。

    +

    可以使用表格 3-2 中的任何一种形式编写数字字面值。除字节以外的其它字面值允许使用类型后缀,例如 57u8,允许使用 _ 做为分隔符以方便读数,例如 1_000 (分隔符的数量与位置并不影响实际的数字)。

    Table 3-2: Integer Literals in Rust

    @@ -982,9 +982,9 @@ commit fe4833a8ef2853c55424e7747a4ef8dd64c35b32

    Number literals Example
    Decimal 98_222
    Binary 0b1111_0000
    Byte (u8 only) b'A'
    -

    那么如何知晓该使用哪种类型的数字呢?如果对此拿不定主意,Rust 的默认类型通常就是一个很好的选择,这个默认数字类型是i32:它通常是最快的,甚至是在 64 位系统上。使用isizeusize的主要场景是索引一些集合。

    +

    那么该使用哪种类型的数字呢?如果拿不定主意,Rust 的默认类型通常就很好,数字类型默认是 i32:它通常是最快的,甚至在 64 位系统上也是。isizeusize 的主要作为集合的索引。

    浮点型

    -

    Rust 也有两个主要的浮点数floating-point numbers)类型,他们是有小数点的数字。Rust 的浮点数类型是f32f64,分别是 32 位 和 64 位大小。默认类型是f64,因为它基本上与f32一样快不过精度更高。在 32 位系统上使用f64是可能的,不过会比f32要慢。大部分情况,牺牲潜在可能的更低性能来换取更高的精度是一个合理的初始选择,同时如果怀疑浮点数的大小有问题的时候应该先对代码进行性能测试。

    +

    Rust 同样有两个主要的浮点数类型,f32f64,它们是带小数点的数字,分别占 32 位和 64 位比特。默认类型是 f64,因为它与 f32 速度差不多,然而精度更高。在 32 位系统上也能够使用 f64,不过比使用 f32 要慢。多数情况下,以潜在的性能损耗换取更高的精度是合理的;如果觉得浮点数的大小是个麻烦,你应该以性能测试作为决策依据。

    这是一个展示浮点数的实例:

    Filename: src/main.rs

    fn main() {
    @@ -1039,7 +1039,7 @@ commit fe4833a8ef2853c55424e7747a4ef8dd64c35b32

    复合类型可以将多个其他类型的值组合成一个类型。Rust 有两个原生的复合类型:元组(tuple)和数组(array)。

    将值组合进元组

    元组是一个将多个其他类型的值组合进一个复合类型的主要方式。

    -

    我们使用一个括号中的逗号分隔的值列表来创建一个元组。元组中的每一个位置都有一个类型,而且这写不同值的类型也不必是相同的。这个例子中使用了额外的可选类型注解:

    +

    我们使用一个括号中的逗号分隔的值列表来创建一个元组。元组中的每一个位置都有一个类型,而且这些不同值的类型也不必是相同的。这个例子中使用了额外的可选类型注解:

    Filename: src/main.rs

    fn main() {
         let tup: (i32, f64, u8) = (500, 6.4, 1);
    @@ -1659,7 +1659,7 @@ println!("{}", s); // This will print `hello, world!`
     
  • 需要一个当我们处理完String时将内存返回给操作系统的方法
  • 第一部分由我们完成:当调用String::from时,它的实现请求它需要的内存。这在编程语言中是非常通用的。

    -

    然而,第二部分实现起来就各有区别了。在有**垃圾回收(GC)**的语言中, GC 记录并清除不再使用的内存,而我们作为程序员,并不需要关心他们。没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们程序员的责任了,正如请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要allocatefree一一对应。

    +

    然而,第二部分实现起来就各有区别了。在有垃圾回收GC)的语言中, GC 记录并清除不再使用的内存,而我们作为程序员,并不需要关心他们。没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们程序员的责任了,正如请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要allocatefree一一对应。

    Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。下面是列表 4-1 中作用域例子的一个使用String而不是字符串字面值的版本:

    {
         let s = String::from("hello"); // s is valid from this point forward
    @@ -1968,7 +1968,7 @@ immutable
     

    哇哦!我们不能在拥有不可变引用的同时拥有可变引用。不可变引用的用户可不希望在它的眼皮底下值突然就被改变了!然而,多个不可变引用是没有问题的因为没有哪个读取数据的人有能力影响其他人读取到的数据。

    即使这些错误有时是使人沮丧的。记住这是 Rust 编译器在提早指出一个潜在的 bug(在编译时而不是运行时)并明确告诉你问题在哪而不是任由你去追踪为何有时数据并不是你想象中的那样。

    悬垂引用

    -

    在存在指针的语言中,容易通过释放内存时保留指向它的指针而错误地生成一个悬垂指针dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者,。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当我们拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。

    +

    在存在指针的语言中,容易通过释放内存时保留指向它的指针而错误地生成一个悬垂指针dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当我们拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。

    让我们尝试创建一个悬垂引用:

    Filename: src/main.rs

    fn main() {
    @@ -9647,11 +9647,11 @@ commit 9430a3d28a2121a938d704ce48b15d21062f880e

    我们说“几乎所有内容都不属于语言本身”,那么属于语言本身的是什么呢?是两个 trait,都位于std::markerSyncSend

    Send用于表明所有权可能被传送给其他线程

    Send标记 trait 表明类型的所有权可能被在线程间传递。几乎所有的 Rust 类型都是Send的,不过有一些例外。比如标准库中提供的 Rc<T>:如果克隆Rc<T>值,并尝试将克隆的所有权传递给另一个线程,这两个线程可能会同时更新引用计数。正如上一部分提到的,Rc<T>被实现为用于单线程场景,这时不需要为拥有线程安全的引用计数而付出性能代价。

    -

    因为Rc<T>没有标记为Send,Rust 的类型系统和 trait bound 会确保我们不会错误的把一个Rc<T>值不安全的在线程间传递。列表 16-14 曾尝试这么做,不过得到了一个错误,the trait Send is not implemented for Rc<Mutex<i32>>。当切换为标记为SendArc<T>时,就没有问题了。

    -

    任何完全由Send的类型组成的类型也会自动被标记为Send。几乎所有基本类型都是Send的,大部分标准库类型是Send的,除了Rc<T>,以及第十九章将会讨论的裸指针(raw pointer)。

    -

    Sync表明多线程访问是安全的

    -

    Sync标记 trait 表明一个类型可以安全的在多个线程中拥有其值的引用。换一种方式来说就是,对于任意类型T,如果&TT的引用)是Send的话T就是Sync的,这样其引用就可以安全的发送到另一个线程。类似于Send的情况,基本类型是Sync的,完全由Sync的类型组成的类型也是Sync的。

    -

    Rc<T>也不是Sync的,出于其不是Send的相同的原因。RefCell<T>(第十五章讨论过)和Cell<T>系列类型不是Sync的。RefCell<T>在运行时所进行的借用检查也不是线程安全的。Mutex<T>Sync的,正如上一部分所讲的它可以被用来在多线程中共享访问。

    +

    因为 Rc<T> 没有标记为 Send,Rust 的类型系统和 trait bound 会确保我们不会错误的把一个 Rc<T> 值不安全的在线程间传递。列表 16-14 曾尝试这么做,不过得到了一个错误,the trait Send is not implemented for Rc<Mutex<i32>>。而使用标记为 SendArc<T> 时,就没有问题了。

    +

    任何完全由 Send 的类型组成的类型也会自动被标记为 Send:几乎所有基本类型都是 Send 的,大部分标准库类型是Send的,除了Rc<T>,以及第十九章将会讨论的裸指针(raw pointer)。

    +

    Sync 表明多线程访问是安全的

    +

    Sync 标记 trait 表明一个类型可以安全的在多个线程中拥有其值的引用。换一种方式来说,对于任意类型 T,如果&TT的引用)是Send的话T就是Sync的,这样其引用就可以安全的发送到另一个线程。类似于 Send 的情况,基本类型是 Sync 的,完全由 Sync 的类型组成的类型也是 Sync 的。

    +

    Rc<T> 也不是 Sync 的,出于其不是Send的相同的原因。RefCell<T>(第十五章讨论过)和Cell<T>系列类型不是Sync的。RefCell<T>在运行时所进行的借用检查也不是线程安全的。Mutex<T>Sync的,正如上一部分所讲的它可以被用来在多线程中共享访问。

    手动实现SendSync是不安全的

    通常并不需要实现SendSync trait,由属于SendSync的类型组成的类型,自动就是SendSync的。因为他们是标记 trait,甚至都不需要实现任何方法。他们只是用来加强并发相关的不可变性的。

    实现这些标记 trait 涉及到编写不安全的 Rust 代码,第十九章将会讲述具体的方法;当前重要的是,在创建新的由不是SendSync的部分构成的并发类型时需要多加小心,以确保维持其安全保证。The Nomicon 中有更多关于这些保证以及如何维持他们的信息。

    @@ -9753,22 +9753,22 @@ commit 67876e3ef5323ce9d394f3ea6b08cb3d173d9ba9

    -

    有时,我们希望使用的类型的集合对于使用库的程序员来说是可扩展的。例如,很多图形用户接口(GUI)工具有一个条目列表的概念,它通过遍历列表并对每一个条目调用draw方法来绘制在屏幕上。我们将要创建一个叫做rust_gui的包含一个 GUI 库结构的库 crate。GUI 库可以包含一些供开发者使用的类型,比如ButtonTextField。使用rust_gui的程序员会想要创建更多可以绘制在屏幕上的类型:一个程序员可能会增加一个Image,而另一个可能会增加一个SelectBox。我们不会在本章节实现一个功能完善的 GUI 库,不过会展示各个部分是如何结合在一起的。

    +

    有时,我们希望使用的类型的集合对于使用库的程序员来说是可扩展的。例如,很多图形用户接口(GUI)工具有一个条目列表的概念,它通过遍历列表并对每一个条目调用 draw 方法来绘制在屏幕上。我们将要创建一个叫做 rust_gui 的包含一个 GUI 库结构的库 crate。GUI 库可以包含一些供开发者使用的类型,比如 ButtonTextField。使用 rust_gui 的程序员会想要创建更多可以绘制在屏幕上的类型:一个程序员可能会增加一个 Image,而另一个可能会增加一个 SelectBox。我们不会在本章节实现一个功能完善的 GUI 库,不过会展示各个部分是如何结合在一起的。

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

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

    定义一个带有自定义行为的Trait

    不过,在Rust语言中,我们可以定义一个 Draw trait,包含名为 draw 的方法。我们定义一个由trait对象组成的vector,绑定了某种指针的trait,比如&引用或者一个Box<T>智能指针。

    -

    之前提到,我们不会称结构体和枚举为对象,以区分其他语言的结构体和枚举对象。结构体或者枚举成员中的数据和impl块中的行为是分开的,而其他语言则是数据和行为被组合到一个对象里。Trait 对象更像其他语言的对象,因为他们将其指针指向的具体对象作为数据,将在trait 中定义的方法作为行为,组合在了一起。但是,trait 对象和其他语言是不同的,我们不能向一个 trait 对象增加数据。trait 对象不像其他语言那样有用:它们的目的是允许从公有行为上抽象。

    -

    trait 对象定义了给定情况下应有的行为。当需要具有某种特性的不确定具体类型时,我们可以把 trait 对象当作 trait 使用。Rust 的类型系统会保证我们为 trait 对象带入的任何值会实现 trait 的方法。我们不需要在编译阶段知道所有可能的类型,却可以把所有的实例统一对待。Listing 17-03展示了如何定义一个名为Draw的带有draw方法的trait。

    -

    Filename: src/lib.rs

    +

    之前提到,我们不会称结构体和枚举为对象,以区分其他语言的结构体和枚举对象。结构体或者枚举成员中的数据和impl块中的行为是分开的,而其他语言则是数据和行为被组合到一个对象里。Trait 对象更像其他语言的对象,因为他们将其指针指向的具体对象作为数据,将在 trait 中定义的方法作为行为,组合在了一起。但是,trait 对象和其他语言是不同的,我们不能向一个 trait 对象增加数据。trait 对象不像其他语言那样有用:它们的目的是允许从公有行为上抽象。

    +

    trait 对象定义了给定情况下应有的行为。当需要具有某种特性的不确定具体类型时,我们可以把 trait 对象当作 trait 使用。Rust 的类型系统会保证我们为 trait 对象带入的任何值会实现 trait 的方法。我们不需要在编译阶段知道所有可能的类型,却可以把所有的实例统一对待。列表 17-03 展示了如何定义一个名为Draw的带有draw方法的 trait。

    +

    文件名: src/lib.rs

    pub trait Draw {
         fn draw(&self);
     }
     
    -

    Listing 17-3:Draw trait的定义

    +

    列表 17-3:Draw trait 的定义

    -

    因为我们已经在第10章讨论过如何定义 trait,你可能比较熟悉。下面是新的定义:Listing 17-4有一个名为 Screen 的结构体,里面有一个名为 components 的 vector,components 的类型是BoxBox<Draw> 是一个 trait 对象:它是 Box 内部任意一个实现了 Draw trait 的类型的替身。

    -

    Filename: src/lib.rs

    +

    因为我们已经在第十章讨论过如何定义 trait,你可能比较熟悉。下面是新的定义:列表 17-4 有一个名为 Screen 的结构体,里面有一个名为 components 的 vector,components 的类型是 Box<Draw>Box<Draw> 是一个 trait 对象:它是 Box 内部任意一个实现了 Draw trait 的类型的替身。

    +

    文件名: src/lib.rs

    # pub trait Draw {
     #     fn draw(&self);
     # }
    @@ -9777,10 +9777,9 @@ pub struct Screen {
         pub components: Vec<Box<Draw>>,
     }
     
    -

    Listing 17-4: 定义一个 Screen 结构体,带有一个含有实现了 Draw trait 的 components vector 成员

    -

    -

    Screen 结构体上,我们将要定义一个 run 方法,该方法会在它的 components 上调用 draw 方法,如Listing 17-5所示:

    -

    Filename: src/lib.rs

    +

    列表 17-4: 一个 Screen 结构体的定义,它带有一个字段components,其包含实现了 Draw trait 的 trait 对象的 vector

    +

    Screen 结构体上,我们将要定义一个 run 方法,该方法会在它的 components 上的每一个元素调用 draw 方法,如列表 17-5 所示:

    +

    文件名: src/lib.rs

    # pub trait Draw {
     #     fn draw(&self);
     # }
    @@ -9797,10 +9796,10 @@ impl Screen {
         }
     }
     
    -

    Listing 17-5:在 Screen 上实现一个 run 方法,该方法在每个组件上调用 draw 方法 +

    列表 17-5:在 Screen 上实现一个 run 方法,该方法在每个 component 上调用 draw 方法

    -

    这与带 trait 约束的泛型结构体不同(trait 约束泛型参数)。泛型参数一次只能被一个具体类型替代,而 trait 对象可以在运行时允许多种具体类型填充 trait 对象。比如,我们已经定义了 Screen 结构体使用泛型和一个 trait 约束,如Listing 17-6所示:

    -

    Filename: src/lib.rs

    +

    这与带 trait 约束的泛型结构体不同(trait 约束泛型参数)。泛型参数一次只能被一个具体类型替代,而 trait 对象可以在运行时允许多种具体类型填充 trait 对象。比如,我们已经定义了 Screen 结构体使用泛型和一个 trait 约束,如列表 17-6 所示:

    +

    文件名: src/lib.rs

    # pub trait Draw {
     #     fn draw(&self);
     # }
    @@ -9818,13 +9817,13 @@ impl<T> Screen<T>
         }
     }
     
    -

    Listing 17-6: 一种 Screen 结构体的替代实现,它的 run 方法使用通用类型和 trait 绑定 +

    列表 17-6: 一种 Screen 结构体的替代实现,它的 run 方法使用通用类型和 trait 绑定

    这个例子中,Screen 实例所有组件类型必需全是 Button,或者全是 TextField。如果你的组件集合是单一类型的,那么可以优先使用泛型和 trait 约束,因为其使用的具体类型在编译阶段即可确定。

    Screen 结构体内部的 Vec<Box<Draw>> trait 对象列表,则可以同时包含 Box<Button>Box<TextField>。我们看它是怎么工作的,然后讨论运行时性能。

    来自我们或者库使用者的实现

    -

    现在,我们增加一些实现了 Draw trait 的类型,再次提供 Button。实现一个 GUI 库实际上超出了本书的范围,因此 draw 方法留空。为了想象实现可能的样子,Button 结构体有 widthheightlabel字段,如Listing 17-7所示:

    -

    Filename: src/lib.rs

    +

    现在,我们增加一些实现了 Draw trait 的类型,再次提供 Button。实现一个 GUI 库实际上超出了本书的范围,因此 draw 方法留空。为了想象实现可能的样子,Button 结构体有 widthheightlabel字段,如列表 17-7 所示:

    +

    文件名: src/lib.rs

    # pub trait Draw {
     #     fn draw(&self);
     # }
    @@ -9841,11 +9840,11 @@ impl Draw for Button {
         }
     }
     
    -

    Listing 17-7: 实现了Draw trait的Button 结构体

    +

    列表 17-7: 实一个现了Draw trait 的 Button 结构体

    Button 上的 widthheightlabel 会和其他组件不同,比如 TextField 可能有 widthheight, label 以及 placeholder 字段。每个我们可以在屏幕上绘制的类型都会实现 Draw trait,在 draw 方法中使用不同的代码,定义了如何绘制 Button。除了 Draw trait,Button 也可能有一个 impl 块,包含按钮被点击时的响应方法。这类方法不适用于 TextField 这样的类型。

    -

    假定我们的库的用户相要实现一个包含 widthheightoptionsSelectBox 结构体。同时也在 SelectBox 类型上实现了 Draw trait,如 Listing 17-8所示:

    -

    Filename: src/main.rs

    +

    假定我们的库的用户相要实现一个包含 widthheightoptionsSelectBox 结构体。同时也在 SelectBox 类型上实现了 Draw trait,如 列表 17-8 所示:

    +

    文件名: src/main.rs

    extern crate rust_gui;
     use rust_gui::Draw;
     
    @@ -9861,10 +9860,10 @@ impl Draw for SelectBox {
         }
     }
     
    -

    Listing 17-8: 另外一个 crate 中,在 SelectBox 结构体上使用 rust_gui 和实现了Draw trait +

    列表 17-8: 另外一个 crate 中,在 SelectBox 结构体上使用 rust_gui 和实现了Draw trait

    -

    库的用户现在可以在他们的 main 函数中创建一个 Screen 实例,然后把自身放入 Box<T> 变成 trait 对象,向 screen 增加 SelectBoxButton。他们可以在这个 Screen 实例上调用 run 方法,这又会调用每个组件的 draw 方法。 Listing 17-9 展示了实现:

    -

    Filename: src/main.rs

    +

    库的用户现在可以在他们的 main 函数中创建一个 Screen 实例,然后把自身放入 Box<T> 变成 trait 对象,向 screen 增加 SelectBoxButton。他们可以在这个 Screen 实例上调用 run 方法,这又会调用每个组件的 draw 方法。 列表 17-9 展示了实现:

    +

    文件名: src/main.rs

    use rust_gui::{Screen, Button};
     
     fn main() {
    @@ -9890,13 +9889,13 @@ fn main() {
         screen.run();
     }
     
    -

    Listing 17-9: 使用 trait 对象来存储实现了相同 trait 的不同类型 +

    列表 17-9: 使用 trait 对象来存储实现了相同 trait 的不同类型

    虽然我们不知道哪一天会有人增加 SelectBox 类型,但是我们的 Screen 能够操作 SelectBox 并绘制它,因为 SelectBox 实现了 Draw 类型,这意味着它实现了 draw 方法。

    只关心值的响应,而不关心其具体类型,这类似于动态类型语言中的 duck typing:如果它像鸭子一样走路,像鸭子一样叫,那么它就是只鸭子!在 Listing 17-5 Screenrun 方法实现中,run 不需要知道每个组件的具体类型。它也不检查组件是 Button 还是 SelectBox 的实例,只管调用组件的 draw 方法。通过指定 Box<Draw> 作为 components 列表中元素的类型,我们约束了 Screen 需要这些实现了 draw 方法的值。

    Rust 类型系统使用 trait 对象来支持 duck typing 的好处是,我们无需在运行时检查一个值是否实现了特定方法,或是担心调用了一个值没有实现的方法。如果值没有实现 trait 对象需要的 trait(方法),Rust 不会编译。

    -

    比如,Listing 17-10 展示了当我们创建一个使用 String 做为其组件的 Screen 时发生的情况:

    -

    Filename: src/main.rs

    +

    比如,列表 17-10 展示了当我们创建一个使用 String 做为其组件的 Screen 时发生的情况:

    +

    文件名: src/main.rs

    extern crate rust_gui;
     use rust_gui::Draw;
     
    @@ -9910,10 +9909,10 @@ fn main() {
         screen.run();
     }
     
    -

    Listing 17-10: 尝试使用一种没有实现 trait 对象的类型

    +

    列表 17-10: 尝试使用一种没有实现 trait 对象的类型

    我们会遇到这个错误,因为 String 没有实现 Draw trait:

    -
    error[E0277]: the trait bound `std::string::String: Draw` is not satisfied
    +
    error[E0277]: the trait bound `std::string::String: Draw` is not satisfied
       -->
        |
      4 |             Box::new(String::from("Hi")),
    @@ -9924,8 +9923,8 @@ fn main() {
     

    这个错误告诉我们,要么传入 Screen 需要的类型,要么在 String 上实现 Draw,以便 Screen 调用它的 draw 方法。

    Trait 对象执行动态分发

    -

    回忆一下第10章我们讨论过的,当我们在泛型上使用 trait 约束时,编译器按单态类型处理:在需要使用范型参数的地方,编译器为每个具体类型生成非泛型的函数和方法实现。单态类型处理产生的代码实际就是做 static dispatch:方法的代码在编译阶段就已经决定了,当调用时,寻找那段代码非常快速。

    -

    当我们使用 trait 对象,编译器不能按单态类型处理,因为无法知道使用代码的所有可能类型。而是调用方法的时候,Rust 跟踪可能被使用的代码,在运行时找出调用该方法时应使用的代码。这也是我们熟知的 dynamic dispatch,查找过程会产生运行时开销。动态分发也会阻止编译器内联函数,失去一些优化途径。尽管获得了额外的灵活性,但仍然需要权衡取舍。

    +

    回忆一下第十章我们讨论过的,当我们在泛型上使用 trait 约束时,编译器按单态类型处理:在需要使用范型参数的地方,编译器为每个具体类型生成非泛型的函数和方法实现。单态类型处理产生的代码实际就是做 static dispatch:方法的代码在编译阶段就已经决定了,当调用时,寻找那段代码非常快速。

    +

    当我们使用 trait 对象,编译器不能按单态类型处理,因为无法知道使用代码的所有可能类型。而是调用方法的时候,Rust 跟踪可能被使用的代码,在运行时找出调用该方法时应使用的代码。这也是我们熟知的 dynamic dispatch,查找过程会产生运行时开销。动态分发也会阻止编译器内联函数,失去一些优化途径。尽管获得了额外的灵活性,但仍然需要权衡取舍。

    Trait 对象需要对象安全

    -

    Not all traits can be made into trait objects; only object safe traits can. A -trait is object safe as long as both of the following are true:

    +

    不是所有的 trait 都可以被放进 trait 对象中; 只有对象安全的object safe)trait 才可以这样做. 一个 trait 只有同时满足如下两点时才被认为是对象安全的:

      -
    • The trait does not require Self to be Sized
    • -
    • All of the trait's methods are object safe.
    • +
    • 该 trait 要求 Self 不是 Sized;
    • +
    • 该 trait 的所有方法都是对象安全的;
    -

    Self is a keyword that is an alias for the type that we're implementing -traits or methods on. Sized is a marker trait like the Send and Sync -traits that we talked about in Chapter 16. Sized is automatically implemented -on types that have a known size at compile time, such as i32 and references. -Types that do not have a known size include slices ([T]) and trait objects.

    -

    Sized is an implicit trait bound on all generic type parameters by default. -Most useful operations in Rust require a type to be Sized, so making Sized -a default requirement on trait bounds means we don't have to write T: Sized -with most every use of generics. If we want to be able to use a trait on -slices, however, we need to opt out of the Sized trait bound, and we can do -that by specifying T: ?Sized as a trait bound.

    -

    Traits have a default bound of Self: ?Sized, which means that they can be -implemented on types that may or may not be Sized. If we create a trait Foo -that opts out of the Self: ?Sized bound, that would look like the following:

    +

    Self 是一个类型的别名关键字,它表示当前正被实现的 trait 类型或者是方法所属的类型. Sized是一个像在第十六章中介绍的SendSync那样的标记 trait, 在编译时它会自动被放进大小确定的类型里,比如i32和引用. 大小不确定的类型有 slice([T])和 trait 对象.

    +

    Sized 是一个默认会被绑定到所有常规类型参数的内隐 trait. Rust 中要求一个类型是Sized的最具可用性的用法是让Sized成为一个默认的 trait 绑定,这样我们就可以在大多数的常规的用法中不去写 T: Sized 了. 如果我们想在切片(slice)中使用一个 trait, 我们需要取消对Sized的 trait 绑定, 我们只需制定T: ?Sized作为 trait 绑定.

    +

    默认绑定到 Self: ?Sized 的 trait 可以被实现到是 Sized 或非 Sized 的类型上. 如果我们创建一个不绑定 Self: ?Sized 的 trait Foo,它看上去应该像这样:

    trait Foo: Sized {
         fn some_method(&self);
     }
     
    -

    The trait Sized is now a super trait of trait Foo, which means trait -Foo requires types that implement Foo (that is, Self) to be Sized. -We're going to talk about super traits in more detail in Chapter 19.

    -

    The reason a trait like Foo that requires Self to be Sized is not allowed -to be a trait object is that it would be impossible to implement the trait -Foo for the trait object Foo: trait objects aren't sized, but Foo -requires Self to be Sized. A type can't be both sized and unsized at the -same time!

    -

    For the second object safety requirement that says all of a trait's methods -must be object safe, a method is object safe if either:

    +

    Trait Sized现在就是 trait Foo的一个超级 traitsupertrait), 也就是说 trait Foo 需要实现了 Foo 的类型(即Self)是Sized. 我们将在第十九章中更详细的介绍超 trait(supertrait).

    +

    Foo那样要求SelfSized的 trait 不允许成为 trait 对象的原因是不可能为 trait 对象Foo实现 trait Foo: trait 对象是无确定大小的,但是 Foo 要求 SelfSized. 一个类型不可能同时既是有大小的又是无确定大小的.

    +

    第二点说对象安全要求一个 trait 的所有方法必须是对象安全的. 一个对象安全的方法满足下列条件:

      -
    • It requires Self to be Sized or
    • -
    • It meets all three of the following: +
    • 它要求 SelfSized 或者
    • +
    • 它符合下面全部三点:
        -
      • It must not have any generic type parameters
      • -
      • Its first argument must be of type Self or a type that dereferences to -the Self type (that is, it must be a method rather than an associated -function and have self, &self, or &mut self as the first argument)
      • -
      • It must not use Self anywhere else in the signature except for the -first argument
      • +
      • 它不包含任意类型的常规参数
      • +
      • 它的第一个参数必须是类型 Self 或一个引用到 Self 的类型(也就是说它必须是一个方法而非关联函数并且以 self&self&mut self 作为第一个参数)
      • +
      • 除了第一个参数外它不能在其它地方用 Self 作为方法的参数签名
    -

    Those rules are a bit formal, but think of it this way: if your method requires -the concrete Self type somewhere in its signature, but an object forgets the -exact type that it is, there's no way that the method can use the original -concrete type that it's forgotten. Same with generic type parameters that are -filled in with concrete type parameters when the trait is used: the concrete -types become part of the type that implements the trait. When the type is -erased by the use of a trait object, there's no way to know what types to fill -in the generic type parameters with.

    -

    An example of a trait whose methods are not object safe is the standard -library's Clone trait. The signature for the clone method in the Clone -trait looks like this:

    +

    虽然这些规则有一点形式化, 但是换个角度想一下: 如果你的方法在它的参数签名的其它地方也需要具体的 Self 类型参数, 但是一个对象又忘记了它的具体类型是什么, 这时该方法就无法使用被它忘记的原先的具体类型. 当该 trait 被使用时, 被具体类型参数填充的常规类型参数也是如此: 这个具体的类型就成了实现该 trait 的类型的某一部分, 如果使用一个 trait 对象时这个类型被抹掉了, 就没有办法知道该用什么类型来填充这个常规类型参数.

    +

    一个 trait 的方法不是对象安全的一个例子是标准库中的 Clone trait. Clone trait 的 clone 方法的参数签名是这样的:

    pub trait Clone {
         fn clone(&self) -> Self;
     }
     
    -

    String implements the Clone trait, and when we call the clone method on -an instance of String we get back an instance of String. Similarly, if we -call clone on an instance of Vec, we get back an instance of Vec. The -signature of clone needs to know what type will stand in for Self, since -that's the return type.

    -

    If we try to implement Clone on a trait like the Draw trait from Listing -17-3, we wouldn't know whether Self would end up being a Button, a -SelectBox, or some other type that will implement the Draw trait in the -future.

    -

    The compiler will tell you if you're trying to do something that violates the -rules of object safety in regards to trait objects. For example, if we had -tried to implement the Screen struct in Listing 17-4 to hold types that -implement the Clone trait instead of the Draw trait, like this:

    -
    pub struct Screen {
    +

    String 实现了 Clone trait, 当我们在一个 String 实例上调用 clone 方法时, 我们会得到一个 String 实例. 同样地, 如果我们在一个 Vec 实例上调用 clone 方法, 我们会得到一个 Vec 实例. clone 的参数签名需要知道 Self 是什么类型, 因为它需要返回这个类型.

    +

    如果我们像列表 17-3 中列出的 Draw trait 那样的 trait 上实现 Clone, 我们就不知道 Self 将会是一个 Button, 一个 SelectBox, 或者是其它的在将来要实现 Draw trait 的类型.

    +

    如果你做了违反 trait 对象的对象安全性规则的事情, 编译器将会告诉你. 比如, 如果你实现在列表 17-4 中列出的 Screen 结构, 你想让该结构像这样持有实现了 Clone trait 的类型而不是 Draw trait:

    +
    pub struct Screen {
         pub components: Vec<Box<Clone>>,
     }
     
    -

    We'll get this error:

    +

    我们将会得到下面的错误:

    error[E0038]: the trait `std::clone::Clone` cannot be made into an object
      -->
       |
    diff --git a/src/ch17-02-trait-objects.md b/src/ch17-02-trait-objects.md
    index edc481f..f110314 100644
    --- a/src/ch17-02-trait-objects.md
    +++ b/src/ch17-02-trait-objects.md
    @@ -10,7 +10,7 @@
     the one with SpreadsheetCell. I will go back and add Listing 8-1 next time I
     get Chapter 8 for editing. /Carol -->
     
    -有时,我们希望使用的类型的集合对于使用库的程序员来说是可扩展的。例如,很多图形用户接口(GUI)工具有一个条目列表的概念,它通过遍历列表并对每一个条目调用`draw`方法来绘制在屏幕上。我们将要创建一个叫做`rust_gui`的包含一个 GUI 库结构的库 crate。GUI 库可以包含一些供开发者使用的类型,比如`Button`或`TextField`。使用`rust_gui`的程序员会想要创建更多可以绘制在屏幕上的类型:一个程序员可能会增加一个`Image`,而另一个可能会增加一个`SelectBox`。我们不会在本章节实现一个功能完善的 GUI 库,不过会展示各个部分是如何结合在一起的。
    +有时,我们希望使用的类型的集合对于使用库的程序员来说是可扩展的。例如,很多图形用户接口(GUI)工具有一个条目列表的概念,它通过遍历列表并对每一个条目调用 `draw` 方法来绘制在屏幕上。我们将要创建一个叫做 `rust_gui` 的包含一个 GUI 库结构的库 crate。GUI 库可以包含一些供开发者使用的类型,比如 `Button` 或 `TextField`。使用 `rust_gui` 的程序员会想要创建更多可以绘制在屏幕上的类型:一个程序员可能会增加一个 `Image`,而另一个可能会增加一个 `SelectBox`。我们不会在本章节实现一个功能完善的 GUI 库,不过会展示各个部分是如何结合在一起的。
     
     当写 `rust_gui` 库时,我们不知道其他程序员需要什么类型,所以无法定义一个 `enum` 来包含所有的类型。然而 `rust_gui` 需要跟踪所有这些不同类型的值,需要有在每个值上调用 `draw` 方法能力。我们的 GUI 库不需要确切地知道调用 `draw` 方法会发生什么,只需要有可用的方法供我们调用。
     
    @@ -20,11 +20,11 @@ get Chapter 8 for editing. /Carol -->
     
     不过,在Rust语言中,我们可以定义一个 `Draw` trait,包含名为 `draw` 的方法。我们定义一个由*trait对象*组成的vector,绑定了某种指针的trait,比如`&`引用或者一个`Box`智能指针。
     
    -之前提到,我们不会称结构体和枚举为对象,以区分其他语言的结构体和枚举对象。结构体或者枚举成员中的数据和`impl`块中的行为是分开的,而其他语言则是数据和行为被组合到一个对象里。Trait 对象更像其他语言的对象,因为他们将其指针指向的具体对象作为数据,将在trait 中定义的方法作为行为,组合在了一起。但是,trait 对象和其他语言是不同的,我们不能向一个 trait 对象增加数据。trait 对象不像其他语言那样有用:它们的目的是允许从公有行为上抽象。
    +之前提到,我们不会称结构体和枚举为对象,以区分其他语言的结构体和枚举对象。结构体或者枚举成员中的数据和`impl`块中的行为是分开的,而其他语言则是数据和行为被组合到一个对象里。Trait 对象更像其他语言的对象,因为他们将其指针指向的具体对象作为数据,将在 trait 中定义的方法作为行为,组合在了一起。但是,trait 对象和其他语言是不同的,我们不能向一个 trait 对象增加数据。trait 对象不像其他语言那样有用:它们的目的是允许从公有行为上抽象。
     
    -trait 对象定义了给定情况下应有的行为。当需要具有某种特性的不确定具体类型时,我们可以把 trait 对象当作 trait 使用。Rust 的类型系统会保证我们为 trait 对象带入的任何值会实现 trait 的方法。我们不需要在编译阶段知道所有可能的类型,却可以把所有的实例统一对待。Listing 17-03展示了如何定义一个名为`Draw`的带有`draw`方法的trait。
    +trait 对象定义了给定情况下应有的行为。当需要具有某种特性的不确定具体类型时,我们可以把 trait 对象当作 trait 使用。Rust 的类型系统会保证我们为 trait 对象带入的任何值会实现 trait 的方法。我们不需要在编译阶段知道所有可能的类型,却可以把所有的实例统一对待。列表 17-03 展示了如何定义一个名为`Draw`的带有`draw`方法的 trait。
     
    -Filename: src/lib.rs
    +文件名: src/lib.rs
     
     ```rust
     pub trait Draw {
    @@ -32,13 +32,13 @@ pub trait Draw {
     }
     ```
     
    -Listing 17-3:`Draw` trait的定义
    +列表 17-3:`Draw` trait 的定义
     
     
     
    -因为我们已经在第10章讨论过如何定义 trait,你可能比较熟悉。下面是新的定义:Listing 17-4有一个名为 `Screen` 的结构体,里面有一个名为 `components` 的 vector,`components` 的类型是Box。`Box` 是一个 trait 对象:它是 `Box` 内部任意一个实现了 `Draw` trait 的类型的替身。
    +因为我们已经在第十章讨论过如何定义 trait,你可能比较熟悉。下面是新的定义:列表 17-4 有一个名为 `Screen` 的结构体,里面有一个名为 `components` 的 vector,`components` 的类型是 `Box`。`Box` 是一个 trait 对象:它是 `Box` 内部任意一个实现了 `Draw` trait 的类型的替身。
     
    -Filename: src/lib.rs
    +文件名: src/lib.rs
     
     ```rust
     # pub trait Draw {
    @@ -50,13 +50,11 @@ pub struct Screen {
     }
     ```
     
    -Listing 17-4: 定义一个 `Screen` 结构体,带有一个含有实现了 `Draw` trait 的 `components` vector 成员
    +列表 17-4: 一个 `Screen` 结构体的定义,它带有一个字段`components`,其包含实现了 `Draw` trait 的 trait 对象的 vector
     
    -
    -
    -在 `Screen` 结构体上,我们将要定义一个 `run` 方法,该方法会在它的 `components` 上调用 `draw` 方法,如Listing 17-5所示:
    +在 `Screen` 结构体上,我们将要定义一个 `run` 方法,该方法会在它的 `components` 上的每一个元素调用 `draw` 方法,如列表 17-5 所示:
     
    -Filename: src/lib.rs
    +文件名: src/lib.rs
     
     ```rust
     # pub trait Draw {
    @@ -76,12 +74,12 @@ impl Screen {
     }
     ```
     
    -Listing 17-5:在 `Screen` 上实现一个 `run` 方法,该方法在每个组件上调用 `draw` 方法
    +列表 17-5:在 `Screen` 上实现一个 `run` 方法,该方法在每个 component 上调用 `draw` 方法
     
     
    -这与带 trait 约束的泛型结构体不同(trait 约束泛型参数)。泛型参数一次只能被一个具体类型替代,而 trait 对象可以在运行时允许多种具体类型填充 trait 对象。比如,我们已经定义了 `Screen` 结构体使用泛型和一个 trait 约束,如Listing 17-6所示:
    +这与带 trait 约束的泛型结构体不同(trait 约束泛型参数)。泛型参数一次只能被一个具体类型替代,而 trait 对象可以在运行时允许多种具体类型填充 trait 对象。比如,我们已经定义了 `Screen` 结构体使用泛型和一个 trait 约束,如列表 17-6 所示:
     
    -Filename: src/lib.rs
    +文件名: src/lib.rs
     
     ```rust
     # pub trait Draw {
    @@ -102,7 +100,7 @@ impl Screen
     }
     ```
     
    -Listing 17-6: 一种 `Screen` 结构体的替代实现,它的 `run` 方法使用通用类型和 trait 绑定
    +列表 17-6: 一种 `Screen` 结构体的替代实现,它的 `run` 方法使用通用类型和 trait 绑定
     
     
     这个例子中,`Screen` 实例所有组件类型必需全是 `Button`,或者全是 `TextField`。如果你的组件集合是单一类型的,那么可以优先使用泛型和 trait 约束,因为其使用的具体类型在编译阶段即可确定。
    @@ -111,9 +109,9 @@ impl Screen
     
     ### 来自我们或者库使用者的实现
     
    -现在,我们增加一些实现了 `Draw` trait 的类型,再次提供 `Button`。实现一个 GUI 库实际上超出了本书的范围,因此 `draw` 方法留空。为了想象实现可能的样子,`Button` 结构体有 `width`、`height` 和 `label`字段,如Listing 17-7所示:
    +现在,我们增加一些实现了 `Draw` trait 的类型,再次提供 `Button`。实现一个 GUI 库实际上超出了本书的范围,因此 `draw` 方法留空。为了想象实现可能的样子,`Button` 结构体有 `width`、`height` 和 `label`字段,如列表 17-7 所示:
     
    -Filename: src/lib.rs
    +文件名: src/lib.rs
     
     ```rust
     # pub trait Draw {
    @@ -133,14 +131,14 @@ impl Draw for Button {
     }
     ```
     
    -Listing 17-7: 实现了`Draw` trait的`Button` 结构体
    +列表 17-7: 实一个现了`Draw` trait 的 `Button` 结构体
     
     在 `Button` 上的 `width`、`height` 和 `label` 会和其他组件不同,比如 `TextField` 可能有 `width`、`height`,
     `label` 以及 `placeholder` 字段。每个我们可以在屏幕上绘制的类型都会实现 `Draw` trait,在 `draw` 方法中使用不同的代码,定义了如何绘制 `Button`。除了 `Draw` trait,`Button` 也可能有一个 `impl` 块,包含按钮被点击时的响应方法。这类方法不适用于 `TextField` 这样的类型。
     
    -假定我们的库的用户相要实现一个包含 `width`、`height` 和 `options` 的 `SelectBox` 结构体。同时也在 `SelectBox` 类型上实现了 `Draw` trait,如 Listing 17-8所示:
    +假定我们的库的用户相要实现一个包含 `width`、`height` 和 `options` 的 `SelectBox` 结构体。同时也在 `SelectBox` 类型上实现了 `Draw` trait,如 列表  17-8 所示:
     
    -Filename: src/main.rs
    +文件名: src/main.rs
     
     ```rust
     extern crate rust_gui;
    @@ -159,12 +157,12 @@ impl Draw for SelectBox {
     }
     ```
     
    -Listing 17-8: 另外一个 crate 中,在 `SelectBox` 结构体上使用 `rust_gui` 和实现了`Draw` trait
    +列表 17-8: 另外一个 crate 中,在 `SelectBox` 结构体上使用 `rust_gui` 和实现了`Draw` trait
     
     
    -库的用户现在可以在他们的 `main` 函数中创建一个 `Screen` 实例,然后把自身放入 `Box` 变成 trait 对象,向 screen 增加 `SelectBox` 和 `Button`。他们可以在这个 `Screen` 实例上调用 `run` 方法,这又会调用每个组件的 `draw` 方法。 Listing 17-9 展示了实现:
    +库的用户现在可以在他们的 `main` 函数中创建一个 `Screen` 实例,然后把自身放入 `Box` 变成 trait 对象,向 screen 增加 `SelectBox` 和 `Button`。他们可以在这个 `Screen` 实例上调用 `run` 方法,这又会调用每个组件的 `draw` 方法。 列表 17-9 展示了实现:
     
    -Filename: src/main.rs
    +文件名: src/main.rs
     
     ```rust
     use rust_gui::{Screen, Button};
    @@ -193,7 +191,7 @@ fn main() {
     }
     ```
     
    -Listing 17-9: 使用 trait 对象来存储实现了相同 trait 的不同类型
    +列表 17-9: 使用 trait 对象来存储实现了相同 trait 的不同类型
     
     
     虽然我们不知道哪一天会有人增加 `SelectBox` 类型,但是我们的 `Screen` 能够操作 `SelectBox` 并绘制它,因为 `SelectBox` 实现了 `Draw` 类型,这意味着它实现了 `draw` 方法。
    @@ -202,9 +200,9 @@ fn main() {
     
     Rust 类型系统使用 trait 对象来支持 duck typing 的好处是,我们无需在运行时检查一个值是否实现了特定方法,或是担心调用了一个值没有实现的方法。如果值没有实现 trait 对象需要的 trait(方法),Rust 不会编译。
     
    -比如,Listing 17-10 展示了当我们创建一个使用 `String` 做为其组件的 `Screen` 时发生的情况:
    +比如,列表 17-10 展示了当我们创建一个使用 `String` 做为其组件的 `Screen` 时发生的情况:
     
    -Filename: src/main.rs
    +文件名: src/main.rs
     
     ```rust
     extern crate rust_gui;
    @@ -221,13 +219,13 @@ fn main() {
     }
     ```
     
    -Listing 17-10: 尝试使用一种没有实现 trait 对象的类型
    +列表 17-10: 尝试使用一种没有实现 trait 对象的类型
     
     
     
     我们会遇到这个错误,因为 `String` 没有实现 `Draw` trait:
     
    -```text
    +```
     error[E0277]: the trait bound `std::string::String: Draw` is not satisfied
       -->
        |
    @@ -242,9 +240,9 @@ error[E0277]: the trait bound `std::string::String: Draw` is not satisfied
     
     ### Trait 对象执行动态分发
     
    -回忆一下第10章我们讨论过的,当我们在泛型上使用 trait 约束时,编译器按单态类型处理:在需要使用范型参数的地方,编译器为每个具体类型生成非泛型的函数和方法实现。单态类型处理产生的代码实际就是做 *static dispatch*:方法的代码在编译阶段就已经决定了,当调用时,寻找那段代码非常快速。
    +回忆一下第十章我们讨论过的,当我们在泛型上使用 trait 约束时,编译器按单态类型处理:在需要使用范型参数的地方,编译器为每个具体类型生成非泛型的函数和方法实现。单态类型处理产生的代码实际就是做 *static dispatch*:方法的代码在编译阶段就已经决定了,当调用时,寻找那段代码非常快速。
     
    -当我们使用 trait 对象,编译器不能按单态类型处理,因为无法知道使用代码的所有可能类型。而是调用方法的时候,Rust 跟踪可能被使用的代码,在运行时找出调用该方法时应使用的代码。这也是我们熟知的 *dynamic dispatch*,查找过程会产生运行时开销。动态分发也会阻止编译器内联函数,失去一些优化途径。尽管获得了额外的灵活性,但仍然需要权衡取舍。
    +当我们使用 trait 对象,编译器不能按单态类型处理,因为无法知道使用代码的所有可能类型。而是调用方法的时候,Rust 跟踪可能被使用的代码,在运行时找出调用该方法时应使用的代码。这也是我们熟知的 *dynamic dispatch*,查找过程会产生运行时开销。动态分发也会阻止编译器内联函数,失去一些优化途径。尽管获得了额外的灵活性,但仍然需要权衡取舍。
     
     ### Trait 对象需要对象安全
     
    @@ -258,16 +256,16 @@ objects. Clone is an example of one. You'll get errors that will let you know
     if a trait can't be a trait object, look up object safety if you're interested
     in the details"? Thanks! /Carol -->
     
    -不是所有的trait都可以被放进trait对象中; 只有*对象安全的*trait才可以这样做. 一个trait只有同时满足如下两点时才被认为是对象安全的:
    +不是所有的 trait 都可以被放进 trait 对象中; 只有*对象安全的*(*object safe*)trait 才可以这样做. 一个 trait 只有同时满足如下两点时才被认为是对象安全的:
     
    -* 该trait要求`Self`不是`Sized`;
    -* 该trait的所有方法都是对象安全的;
    +* 该 trait 要求 `Self` 不是 `Sized`;
    +* 该 trait 的所有方法都是对象安全的;
     
    -`Self`是一个类型的别名关键字,它表示当前正被实现的trait类型或者是方法所属的类型. `Sized`是一个像在第16章中介绍的`Send`和`Sync`那样的标记trait, 在编译时它会自动被放进大小确定的类型里,比如`i32`和引用. 大小不确定的类型有切片(`[T]`)和trait对象.
    +`Self` 是一个类型的别名关键字,它表示当前正被实现的 trait 类型或者是方法所属的类型. `Sized`是一个像在第十六章中介绍的`Send`和`Sync`那样的标记 trait, 在编译时它会自动被放进大小确定的类型里,比如`i32`和引用. 大小不确定的类型有 slice(`[T]`)和 trait 对象.
     
    -`Sized`是一个默认会被绑定到所有常规类型参数的内隐trait. Rust中要求一个类型是`Sized`的最具可用性的用法是让`Sized`成为一个默认的trait绑定,这样我们就可以在大多数的常规的用法中不去写`T: Sized`了. 如果我们想在切片(slice)中使用一个trait, 我们需要取消对`Sized`的trait绑定, 我们只需制定`T: ?Sized`作为trait绑定.
    +`Sized` 是一个默认会被绑定到所有常规类型参数的内隐 trait. Rust 中要求一个类型是`Sized`的最具可用性的用法是让`Sized`成为一个默认的 trait 绑定,这样我们就可以在大多数的常规的用法中不去写 `T: Sized` 了. 如果我们想在切片(slice)中使用一个 trait, 我们需要取消对`Sized`的 trait 绑定, 我们只需制定`T: ?Sized`作为 trait 绑定.
     
    -默认绑定到`Self: ?Sized`的trait可以被实现到是`Sized`或非`Sized`的类型上. 如果我们创建一个不绑定`Self: ?Sized`的trait`Foo`,它看上去应该像这样: 
    +默认绑定到 `Self: ?Sized` 的 trait 可以被实现到是 `Sized` 或非 `Sized` 的类型上. 如果我们创建一个不绑定 `Self: ?Sized` 的 trait `Foo`,它看上去应该像这样: 
     
     ```rust
     trait Foo: Sized {
    @@ -275,21 +273,21 @@ trait Foo: Sized {
     }
     ```
     
    -Trait`Sized`现在就是trait`Foo`的一个*超级trait*, 也就是说trait`Foo`需要实现了`Foo`的类型(即`Self`)是`Sized`. 我们将在第19章中更详细的介绍超trait(supertrait).
    +Trait `Sized`现在就是 trait `Foo`的一个*超级 trait*(*supertrait*), 也就是说 trait `Foo` 需要实现了 `Foo` 的类型(即`Self`)是`Sized`. 我们将在第十九章中更详细的介绍超 trait(supertrait).
     
    -像`Foo`那样要求`Self`是`Sized`的trait不允许成为trait对象的原因是不可能为trait对象`Foo`实现trait`Foo`: trait对象是无确定大小的,但是`Foo`要求`Self`是`Sized`. 一个类型不可能同时既是有大小的又是无确定大小的.
    +像`Foo`那样要求`Self`是`Sized`的 trait 不允许成为 trait 对象的原因是不可能为 trait 对象`Foo`实现 trait `Foo`: trait 对象是无确定大小的,但是 `Foo` 要求 `Self` 是 `Sized`. 一个类型不可能同时既是有大小的又是无确定大小的.
     
    -第二点说对象安全要求一个trait的所有方法必须是对象安全的. 一个对象安全的方法满足下列条件:
    +第二点说对象安全要求一个 trait 的所有方法必须是对象安全的. 一个对象安全的方法满足下列条件:
     
    -* 它要求`Self`是`Sized`或者
    +* 它要求 `Self` 是 `Sized` 或者
     * 它符合下面全部三点:
         * 它不包含任意类型的常规参数
    -    * 它的第一个参数必须是类型`Self`或一个引用到`Self`的类型(也就是说它必须是一个方法而非关联函数并且以`self`、`&self`或`&mut self`作为第一个参数)
    -    * 除了第一个参数外它不能在其它地方用`Self`作为方法的参数签名
    +    * 它的第一个参数必须是类型 `Self` 或一个引用到 `Self` 的类型(也就是说它必须是一个方法而非关联函数并且以 `self`、`&self` 或 `&mut self` 作为第一个参数)
    +    * 除了第一个参数外它不能在其它地方用 `Self` 作为方法的参数签名
     
    -虽然这些规则有一点形式化, 但是换个角度想一下: 如果你的方法在它的参数签名的其它地方也需要具体的`Self`类型参数, 但是一个对象又忘记了它的具体类型是什么, 这时该方法就无法使用被它忘记的原先的具体类型. 当该trait被使用时, 被具体类型参数填充的常规类型参数也是如此: 这个具体的类型就成了实现该trait的类型的某一部分, 如果使用一个trait对象时这个类型被抹掉了, 就没有办法知道该用什么类型来填充这个常规类型参数.
    +虽然这些规则有一点形式化, 但是换个角度想一下: 如果你的方法在它的参数签名的其它地方也需要具体的 `Self` 类型参数, 但是一个对象又忘记了它的具体类型是什么, 这时该方法就无法使用被它忘记的原先的具体类型. 当该 trait 被使用时, 被具体类型参数填充的常规类型参数也是如此: 这个具体的类型就成了实现该 trait 的类型的某一部分, 如果使用一个 trait 对象时这个类型被抹掉了, 就没有办法知道该用什么类型来填充这个常规类型参数.
     
    -一个trait的方法不是对象安全的一个例子是标准库中的`Clone`trait. `Clone`trait的`clone`方法的参数签名是这样的:
    +一个 trait 的方法不是对象安全的一个例子是标准库中的 `Clone` trait. `Clone` trait 的 `clone` 方法的参数签名是这样的:
     
     ```rust
     pub trait Clone {
    @@ -297,13 +295,13 @@ pub trait Clone {
     }
     ```
     
    -`String`实现了`Clone` trait, 当我们在一个`String实例上调用`clone`方法时, 我们会得到一个`String`实例. 同样地, 如果我们在一个`Vec`实例上调用`clone`方法, 我们会得到一个`Vec`实例. `clone`的参数签名需要知道`Self`是什么类型, 因为它需要返回这个类型.
    +`String` 实现了 `Clone` trait, 当我们在一个 `String` 实例上调用 `clone` 方法时, 我们会得到一个 `String` 实例. 同样地, 如果我们在一个 `Vec` 实例上调用 `clone` 方法, 我们会得到一个 `Vec` 实例. `clone` 的参数签名需要知道 `Self` 是什么类型, 因为它需要返回这个类型.
     
    -如果我们想在像17-3中列出的`Draw`trait那样的trait上实现`Clone`, 我们就不知道`Self`将会是一个`Button`, 一个`SelectBox`, 或者是其它的在将来要实现`Draw`trait的类型.
    +如果我们像列表 17-3 中列出的 `Draw` trait 那样的 trait 上实现 `Clone`, 我们就不知道 `Self` 将会是一个 `Button`, 一个 `SelectBox`, 或者是其它的在将来要实现 `Draw` trait 的类型.
     
    -如果你做了违反trait对象的对象安全性规则的事情, 编译器将会告诉你. 比如, 如果你实现在17-4中列出的`Screen`结构, 你想让该结构像这样持有实现了`Clone`trait的类型而不是`Draw`trait:
    +如果你做了违反 trait 对象的对象安全性规则的事情, 编译器将会告诉你. 比如, 如果你实现在列表 17-4 中列出的 `Screen` 结构, 你想让该结构像这样持有实现了 `Clone` trait 的类型而不是 `Draw` trait:
     
    -```rust,ignore
    +```rust
     pub struct Screen {
         pub components: Vec>,
     }
    diff --git a/src/ch17-03-oo-design-patterns.md b/src/ch17-03-oo-design-patterns.md
    index 5e65df5..4b07051 100644
    --- a/src/ch17-03-oo-design-patterns.md
    +++ b/src/ch17-03-oo-design-patterns.md
    @@ -1,36 +1,23 @@
     ## 面向对象设计模式的实现
     
    -让我们看一下状态设计模式和怎样在Rust中来使用它的例子. *状态模式*就是当一个值有多个内部状态时,值的行为改变基于内部状态. Let's look at an example of the state design pattern and how to use it in Rust.
    -The *state pattern* is when a value has some internal state, and the value's
    -behavior changes based on the internal state. The internal state is represented
    -by a set of objects that inherit shared functionality (we'll use structs and
    -traits since Rust doesn't have objects and inheritance). Each state object is
    -responsible for its own behavior and the rules for when it should change into
    -another state. The value that holds one of these state objects doesn't know
    -anything about the different behavior of the states or when to transition
    -between states. In the future when requirements change, we won't need to change
    -the code of the value holding the state or the code that uses the value. We'll
    -only need to update the code inside one of the state objects to change its
    -rules, or perhaps add more state objects.
    -
    -In order to explore this idea, we're going to implement a blog post workflow in
    -an incremental way. The workflow that we want our blog posts to follow, once
    -we're done with the implementation, is:
    -
    -1. A blog post starts as an empty draft.
    -2. Once the draft is done, we request a review of the post.
    -3. Once the post is approved, it gets published.
    -4. Only published blog posts return content to print so that we can't
    -   accidentally print the text of a post that hasn't been approved.
    -
    -Any other changes attempted on a post should have no effect. For example, if we
    -try to approve a draft blog post before we've requested a review, the post
    -should stay an unpublished draft.
    -
    -Listing 17-11 shows this workflow in code form. This is an example usage of the
    -API we're going to implement in a library crate named `blog`:
    -
    -Filename: src/main.rs
    +> [ch17-03-oo-design-patterns.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch17-03-oo-design-patterns.md)
    +> 
    +> commit 67737ff868e3347588cc832eceb8fc237afc5895 + +让我们看看一个状态设计模式的例子以及如何在 Rust 中使用他们。**状态模式**(*state pattern*)是指一个值有某些内部状态,而它的行为随着其内部状态而改变。内部状态由一系列继承了共享功能的对象表现(我们使用结构体和 trait 因为 Rust 没有对象和继承)。每一个状态对象负责它自身的行为和当需要改变为另一个状态时的规则。持有任何一个这种状态对象的值对于不同状态的行为以及何时状态转移毫不知情。当将来需求改变时,无需改变值持有状态或者使用值的代码。我们只需更新某个状态对象中的代码来改变它的规则,或者是增加更多的状态对象。 + +为了探索这个概念,我们将实现一个增量式的发布博文的工作流。这个我们希望发布博文时所应遵守的工作流,一旦完成了它的实现,将为如下: + +1. 博文从空白的草案开始。 +2. 一旦草案完成,请求审核博文。 +3. 一旦博文过审,它将被发表。 +4. 只有被发表的博文的内容会被打印,这样就不会意外打印出没有被审核的博文的文本。 + +任何其他对博文的修改尝试都是没有作用的。例如,如果尝试在请求审核之前通过一个草案博文,博文应该保持未发布的状态。 + +列表 17-11 展示这个工作流的代码形式。这是一个我们将要在一个叫做 `blog` 的库 crate 中实现的 API 的使用示例: + +文件名: src/main.rs ```rust,ignore extern crate blog; @@ -50,44 +37,19 @@ fn main() { } ``` -Listing 17-11: Code that demonstrates the desired -behavior we want our `blog` crate to have - -We want to be able to create a new draft blog post with `Post::new`. Then, we -want to add some text to the blog post while we're in the draft state. If we -try to print out the post's content immediately, though, we shouldn't get any -text, since the post is still a draft. We've added an `assert_eq!` here for -demonstration purposes. Asserting that a draft blog post returns an empty -string from the `content` method would make an excellent unit test in our -library, but we're not going to write tests for this example. - -Next, we want to be able to request a review of our post, and `content` should -still return an empty string while waiting for a review. Lastly, when we -approve the blog post, it should get published, which means the text we added -will be returned when we call `content`. - -Notice that the only type we're interacting with from the crate is the `Post` -type. The various states a post can be in (draft, waiting for review, -published) are managed internally to the `Post` type. The states change due to -the methods we call on the `Post` instance, but we don't have to manage the -state changes directly. This also means we won't make a mistake with the -states, like forgetting to request a review before publishing. - -### Defining `Post` and Creating a New Instance in the Draft State - -Let's get started on the implementation of the library! We know we want to have -a public `Post` struct that holds some content, so let's start with the -definition of the struct and an associated public `new` function to create an -instance of `Post` as shown in Listing 17-12. We're also going to have a -private trait `State`. `Post` will hold a trait object of `Box` inside -an `Option` in a private field named `state`. We'll see why the `Option` is -necessary in a bit. The `State` trait defines all the behavior different post -states share, and the `Draft`, `PendingReview`, and `Published` states will all -implement the `State` trait. For now, the trait does not have any methods, and -we're going to start by defining just the `Draft` state since that's the state -we want to start in: - -Filename: src/lib.rs +列表 17-11: 展示了 `blog` crate 期望行为的代码 + +我们希望能够使用 `Post::new` 创建一个新的博文草案。接着希望能在草案阶段为博文编写一些文本。如果尝试立即打印出博文的内容,将不会得到任何文本,因为博文仍然是草案。这里增加的 `assert_eq!` 用于展示目的。断言草案博文的 `content` 方法返回空字符串将能作为库的一个非常好的单元测试,不过我们并不准备为这个例子编写单元测试。 + +接下来,我们希望能够请求审核博文,而在等待审核的阶段 `content` 应该仍然返回空字符串,当博文审核通过,它应该被发表,这意味着当调用 `content` 时我们编写的文本将被返回。 + +注意我们与 crate 交互的唯一的类型是 `Post`。博文可能处于的多种状态(草案,等待审核和发布)由 `Post` 内部管理。博文状态依我们在`Post`调用的方法而改变,但不必直接管理状态改变。这也意味着不会在状态上犯错,比如忘记了在发布前请求审核。 + +### 定义 `Post` 并新建一个草案状态的实例 + +让我们开始实现这个库吧!我们知道需要一个公有 `Post` 结构体来存放一些文本,所以让我们从结构体的定义和一个创建 `Post` 实例的公有关联函数 `new` 开始,如列表 17-12 所示。我们还需定义一个私有 trait `State`。`Post` 将在私有字段 `state` 中存放一个 `Option` 中的 trait 对象 `Box`。稍后将会看到为何 `Option` 是必须的。`State` trait 定义了所有不同状态的博文所共享的行为,同时 `Draft`、`PendingReview` 和 `Published` 状态都会实现`State` 状态。现在这个 trait 并没有任何方法,同时开始将只定义`Draft`状态因为这是我们希望开始的状态: + +文件名: src/lib.rs ```rust pub struct Post { @@ -111,28 +73,16 @@ struct Draft {} impl State for Draft {} ``` -Listing 17-12: Definition of a `Post` struct and a `new` -function that creates a new `Post` instance, a `State` trait, and a `Draft` -struct that implements `State` +列表 17-12: `Post`结构体的定义和新建 `Post` 实例的 `new`函数,`State` trait 和实现了 `State` 的结构体 `Draft` -When we create a new `Post`, we set its `state` field to a `Some` value holding -a `Box` pointing to a new instance of the `Draft` struct. This ensures whenever -we create a new instance of `Post`, it'll start out as a draft. Because the -`state` field of `Post` is private, there's no way to create a `Post` in any -other state! +当创建新的 `Post` 时,我们将其 `state` 字段设置为一个 `Some` 值,它存放了指向一个 `Draft` 结构体新实例的 `Box`。这确保了无论何时新建一个 `Post` 实例,它会从草案开始。因为 `Post` 的 `state` 字段是私有的,也就无法创建任何其他状态的 `Post` 了!。 -### Storing the Text of the Post Content +### 存放博文内容的文本 -In the `Post::new` function, we set the `content` field to a new, empty -`String`. In Listing 17-11, we showed that we want to be able to call a method -named `add_text` and pass a `&str` to it to add that text to the content of the -blog post. We're choosing to implement this as a method rather than exposing -the `content` field as `pub` because we want to be able to control how the -`content` field's data is read by implementing a method later. The `add_text` -method is pretty straightforward though, let's add the implementation in -Listing 17-13 to the `impl Post` block: +在 `Post::new` 函数中,我们设置 `content` 字段为新的空 `String`。在列表 17-11 中,展示了我们希望能够调用一个叫做 `add_text` 的方法并向其传递一个 `&str` 来将文本增加到博文的内容中。选择实现为一个方法而不是将 `content` 字段暴露为 `pub` 是因为我们希望能够通过之后实现的一个方法来控制 `content` 字段如何被读取。`add_text` 方法是非常直观的,让我们在列表 17-13 的 `impl Post` 块中增加一个实现: -Filename: src/lib.rs + +文件名: src/lib.rs ```rust # pub struct Post { @@ -147,30 +97,15 @@ impl Post { } ``` -Listing 17-13: Implementing the `add_text` method to add -text to a post's `content` +列表 17-13: 实现方法 `add_text` 来向博文的 `content` 增加文本 -`add_text` takes a mutable reference to `self`, since we're changing the `Post` -instance that we're calling `add_text` on. We then call `push_str` on the -`String` in `content` and pass the `text` argument to add to the saved -`content`. This isn't part of the state pattern since its behavior doesn't -depend on the state that the post is in. The `add_text` method doesn't interact -with the `state` field at all, but it is part of the behavior we want to -support. +`add_text` 获取一个 `self` 的可变引用,因为需要改变调用 `add_text` 的 `Post`。接着调用 `content` 中的 `String` 的 `push_str` 并传递 `text` 参数来保存到 `content` 中。这不是状态模式的一部分,因为它的行为并不依赖博文所处的状态。`add_text` 方法完全不与 `state` 状态交互,不过这是我们希望支持的行为的一部分。 -### Content of a Draft Post is Empty +### 博文草案的内容是空的 -After we've called `add_text` and added some content to our post, we still want -the `content` method to return an empty string slice since the post is still in -the draft state, as shown on line 8 of Listing 17-11. For now, let's implement -the `content` method with the simplest thing that will fulfill this requirement: -always returning an empty string slice. We're going to change this later once -we implement the ability to change a post's state to be published. With what we -have so far, though, posts can only be in the draft state, which means the post -content should always be empty. Listing 17-14 shows this placeholder -implementation: +调用 `add_text` 并像博文增加一些内容之后,我们仍然希望 `content` 方法返回一个空字符串 slice,因为博文仍然处于草案状态,如列表 17-11 的第 8 行所示。现在让我们使用能满足要求的最简单的方式来实现 `content` 方法 总是返回一个空字符 slice。当实现了将博文状态改为发布的能力之后将改变这一做法。但是现在博文只能是草案状态,这意味着其内容总是空的。列表 17-14 展示了这个占位符实现: -Filename: src/lib.rs +文件名: src/lib.rs ```rust # pub struct Post { @@ -185,27 +120,15 @@ impl Post { } ``` -Listing 17-14: Adding a placeholder implementation for -the `content` method on `Post` that always returns an empty string slice +列表 17-14: 增加一个 `Post` 的 `content` 方法的占位实现,它总是返回一个空字符串 slice -With this added `content` method, everything in Listing 17-11 up to line 8 -works as we intend. +通过增加这个 `content`方法,列表 17-11 中直到第 8 行的代码能如期运行。 -### Requesting a Review of the Post Changes its State +### 请求审核博文来改变其状态 -Next up is requesting a review of a post, which should change its state from -`Draft` to `PendingReview`. We want `post` to have a public method named -`request_review` that will take a mutable reference to `self`. Then we're going -to call an internal `request_review` method on the state that we're holding, and -this second `request_review` method will consume the current state and return a -new state. In order to be able to consume the old state, the second `request_review` -method needs to take ownership of the state value. This is where the `Option` comes -in: we're going to `take` the `Some` value out of the `state` field and leave a -`None` in its place since Rust doesn't let us have unpopulated fields in -structs. Then we'll set the post's `state` value to the result of this -operation. Listing 17-15 shows this code: +接下来是请求审核博文,这应当将其状态由 `Draft` 改为 `PendingReview`。我们希望 `post` 有一个获取 `self` 可变引用的公有方法 `request_review`。接着将调用内部存放的状态的 `request_review` 方法,而这第二个 `request_review` 方法会消费当前的状态并返回要一个状态。为了能够消费旧状态,第二个 `request_review` 方法需要能够获取状态值的所有权。这就是 `Option` 的作用:我们将 `take` 字段 `state` 中的 `Some` 值并留下一个 `None` 值,因为 Rust 并不允许结构体中有空字段。接着将博文的 `state` 设置为这个操作的结果。列表 17-15 展示了这些代码: -Filename: src/lib.rs +文件名: src/lib.rs ```rust # pub struct Post { @@ -243,451 +166,6 @@ impl State for PendingReview { } ``` -Listing 17-15: Implementing `request_review` methods on -`Post` and the `State` trait - -We've added the `request_review` method to the `State` trait; all types that -implement the trait will now need to implement the `request_review` method. -Note that rather than having `self`, `&self`, or `&mut self` as the first -parameter of the method, we have `self: Box`. This syntax means the -method is only valid when called on a `Box` holding the type. This syntax takes -ownership of `Box`, which is what we want because we're transforming the -old state into a new state, and we want the old state to no longer be valid. - -The implementation for the `request_review` method on `Draft` is to return a -new, boxed instance of the `PendingReview` struct, which is a new type we've -introduced that represents the state when a post is waiting for a review. The -`PendingReview` struct also implements the `request_review` method, but it -doesn't do any transformations. It returns itself since requesting a review on -a post that's already in the `PendingReview` state should stay in the -`PendingReview` state. - -Now we can start seeing the advantages of the state pattern: the -`request_review` method on `Post` is the same no matter what its `state` value -is. Each state is responsible for its own rules. - -We're going to leave the `content` method on `Post` as it is, returning an -empty string slice. We can now have a `Post` in the `PendingReview` state, not -just the `Draft` state, but we want the same behavior in the `PendingReview` -state. Listing 17-11 now works up until line 11! - -### Approving a Post Changes the Behavior of `content` - -The `approve` method on `Post` will be similar to that of the `request_review` -method: it will set the `state` to the value that the current state says it -should have when that state is approved. We'll need to add the `approve` method -to the `State` trait, and we'll add a new struct that implements `State`, the -`Published` state. Listing 17-16 shows the new code: - -Filename: src/lib.rs - -```rust -# pub struct Post { -# state: Option>, -# content: String, -# } -# -impl Post { - // ...snip... - pub fn approve(&mut self) { - if let Some(s) = self.state.take() { - self.state = Some(s.approve()) - } - } -} - -trait State { - fn request_review(self: Box) -> Box; - fn approve(self: Box) -> Box; -} - -struct Draft {} - -impl State for Draft { -# fn request_review(self: Box) -> Box { -# Box::new(PendingReview {}) -# } -# - // ...snip... - fn approve(self: Box) -> Box { - self - } -} - -struct PendingReview {} - -impl State for PendingReview { -# fn request_review(self: Box) -> Box { -# Box::new(PendingReview {}) -# } -# - // ...snip... - fn approve(self: Box) -> Box { - Box::new(Published {}) - } -} - -struct Published {} - -impl State for Published { - fn request_review(self: Box) -> Box { - self - } - - fn approve(self: Box) -> Box { - self - } -} -``` - -Listing 17-16: Implementing the `approve` method on -`Post` and the `State` trait - -Similarly to `request_review`, if we call the `approve` method on a `Draft`, it -will have no effect since it will return `self`. When we call `approve` on -`PendingReview`, it returns a new, boxed instance of the `Published` struct. -The `Published` struct implements the `State` trait, and for both the -`request_review` method and the `approve` method, it returns itself since the -post should stay in the `Published` state in those cases. - -Now for updating the `content` method on `Post`: we want to return the value in -the post's `content` field if its state is `Published`, otherwise we want to -return an empty string slice. Because the goal is to keep all the rules like -this in the structs that implement `State`, we're going to call a `content` -method on the value in `state` and pass the post instance (that is, `self`) as -an argument. Then we'll return the value returned from the `content` method on -the `state` value as shown in Listing 17-17: - -Filename: src/lib.rs - -```rust -# trait State { -# fn content<'a>(&self, post: &'a Post) -> &'a str; -# } -# pub struct Post { -# state: Option>, -# content: String, -# } -# -impl Post { - // ...snip... - pub fn content(&self) -> &str { - self.state.as_ref().unwrap().content(&self) - } - // ...snip... -} -``` - -Listing 17-17: Updating the `content` method on `Post` to -delegate to a `content` method on `State` - -We're calling the `as_ref` method on the `Option` because we want a reference -to the value inside the `Option`. We're then calling the `unwrap` method, which -we know will never panic because all the methods on `Post` ensure that the -`state` value will have a `Some` value in it when those methods are done. This -is one of the cases we talked about in Chapter 12 where we know that a `None` -value is never possible even though the compiler isn't able to understand that. - -The `content` method on the `State` trait is where the logic for what content -to return will be. We're going to add a default implementation for the -`content` method that returns an empty string slice. That lets us not need to -implement `content` on the `Draft` and `PendingReview` structs. The `Published` -struct will override the `content` method and will return the value in -`post.content`, as shown in Listing 17-18: - -Filename: src/lib.rs - -```rust -# pub struct Post { -# content: String -# } -trait State { - // ...snip... - fn content<'a>(&self, post: &'a Post) -> &'a str { - "" - } -} - -// ...snip... -struct Published {} - -impl State for Published { - // ...snip... - fn content<'a>(&self, post: &'a Post) -> &'a str { - &post.content - } -} -``` - -Listing 17-18: Adding the `content` method to the `State` -trait - -Note that we need lifetime annotations on this method, like we discussed in -Chapter 10. We're taking a reference to a `post` as an argument, and we're -returning a reference to a part of that `post`, so the lifetime of the returned -reference is related to the lifetime of the `post` argument. - -### Tradeoffs of the State Pattern - -We've shown that Rust is capable of implementing the object-oriented state -pattern in order to encapsulate the different kinds of behavior that a post -should have that depends on the state that the post is in. The methods on -`Post` don't know anything about the different kinds of behavior. The way this -code is organized, we have one place to look in order to find out all the -different ways that a published post behaves: the implementation of the `State` -trait on the `Published` struct. - -An alternative implementation that didn't use the state pattern might have -`match` statements in the methods on `Post` or even in the code that uses -`Post` (`main` in our case) that checks what the state of the post is and -changes behavior in those places instead. That would mean we'd have a lot of -places to look in order to understand all the implications of a post being in -the published state! This would get worse the more states we added: each of -those `match` statements would need another arm. With the state pattern, the -`Post` methods and the places we use `Post` don't need `match` statements and -adding a new state only involves adding a new `struct` and implementing the -trait methods on that one struct. - -This implementation is easy to extend to add more functionality. Here are some -changes you can try making to the code in this section to see for yourself what -it's like to maintain code using this pattern over time: - -- Only allow adding text content when a post is in the `Draft` state -- Add a `reject` method that changes the post's state from `PendingReview` back - to `Draft` -- Require two calls to `approve` before changing the state to `Published` - -A downside of the state pattern is that since the states implement the -transitions between the states, some of the states are coupled to each other. -If we add another state between `PendingReview` and `Published`, such as -`Scheduled`, we would have to change the code in `PendingReview` to transition -to `Scheduled` instead. It would be nicer if `PendingReview` wouldn't need to -change because of the addition of a new state, but that would mean switching to -another design pattern. - -There are a few bits of duplicated logic that are a downside of this -implementation in Rust. It would be nice if we could make default -implementations for the `request_review` and `approve` methods on the `State` -trait that return `self`, but this would violate object safety since the trait -doesn't know what the concrete `self` will be exactly. We want to be able to -use `State` as a trait object, so we need its methods to be object safe. - -The other duplication that would be nice to get rid of is the similar -implementations of the `request_review` and `approve` methods on `Post`. They -both delegate to the implementation of the same method on the value in the -`Option` in the `state` field, and set the new value of the `state` field to -the result. If we had a lot of methods on `Post` that followed this pattern, we -might consider defining a macro to eliminate the repetition (see Appendix E on -macros). - -A downside of implementing this object-oriented pattern exactly as it's defined -for object-oriented languages is that we're not taking advantage of Rust's -strengths as much as we could be. Let's take a look at some changes we can make -to this code that can make invalid states and transitions into compile time -errors. - -#### Encoding States and Behavior as Types - -We're going to show how to rethink the state pattern a bit in order to get a -different set of tradeoffs. Rather than encapsulating the states and -transitions completely so that outside code has no knowledge of them, we're -going to encode the states into different types. When the states are types, -Rust's type checking will make any attempt to use a draft post where we should -only use published posts into a compiler error. - -Let's consider the first part of `main` from Listing 17-11: - -Filename: src/main.rs - -```rust,ignore -fn main() { - let mut post = Post::new(); - - post.add_text("I ate a salad for lunch today"); - assert_eq!("", post.content()); -} -``` - -We still want to create a new post in the draft state using `Post::new`, and we -still want to be able to add text to the post's content. But instead of having -a `content` method on a draft post that returns an empty string, we're going to -make it so that draft posts don't have the `content` method at all. That way, -if we try to get a draft post's content, we'll get a compiler error that the -method doesn't exist. This will make it impossible for us to accidentally -display draft post content in production, since that code won't even compile. -Listing 17-19 shows the definition of a `Post` struct, a `DraftPost` struct, -and methods on each: - -Filename: src/lib.rs - -```rust -pub struct Post { - content: String, -} - -pub struct DraftPost { - content: String, -} - -impl Post { - pub fn new() -> DraftPost { - DraftPost { - content: String::new(), - } - } - - pub fn content(&self) -> &str { - &self.content - } -} - -impl DraftPost { - pub fn add_text(&mut self, text: &str) { - self.content.push_str(text); - } -} -``` - -Listing 17-19: A `Post` with a `content` method and a -`DraftPost` without a `content` method - -Both the `Post` and `DraftPost` structs have a private `content` field that stores the -blog post text. The structs no longer have the `state` field since we're moving -the encoding of the state to the types of the structs. `Post` will represent a -published post, and it has a `content` method that returns the `content`. - -We still have a `Post::new` function, but instead of returning an instance of -`Post`, it returns an instance of `DraftPost`. It's not possible to create an -instance of `Post` right now since `content` is private and there aren't any -functions that return `Post`. `DraftPost` has an `add_text` method defined on -it so that we can add text to `content` as before, but note that `DraftPost` -does not have a `content` method defined! So we've enforced that all posts -start as draft posts, and draft posts don't have their content available for -display. Any attempt to get around these constraints will be a compiler error. - -#### Implementing Transitions as Transformations into Different Types - -So how do we get a published post then? The rule we want to enforce is that a -draft post has to be reviewed and approved before it can be published. A post -in the pending review state should still not display any content. Let's -implement these constraints by adding another struct, `PendingReviewPost`, -defining the `request_review` method on `DraftPost` to return a -`PendingReviewPost`, and defining an `approve` method on `PendingReviewPost` to -return a `Post` as shown in Listing 17-20: - -Filename: src/lib.rs - -```rust -# pub struct Post { -# content: String, -# } -# -# pub struct DraftPost { -# content: String, -# } -# -impl DraftPost { - // ...snip... - - pub fn request_review(self) -> PendingReviewPost { - PendingReviewPost { - content: self.content, - } - } -} - -pub struct PendingReviewPost { - content: String, -} - -impl PendingReviewPost { - pub fn approve(self) -> Post { - Post { - content: self.content, - } - } -} -``` - -Listing 17-20: A `PendingReviewPost` that gets created by -calling `request_review` on `DraftPost`, and an `approve` method that turns a -`PendingReviewPost` into a published `Post` - -The `request_review` and `approve` methods take ownership of `self`, thus -consuming the `DraftPost` and `PendingReviewPost` instances and transforming -them into a `PendingReviewPost` and a published `Post`, respectively. This way, -we won't have any `DraftPost` instances lingering around after we've called -`request_review` on them, and so forth. `PendingReviewPost` doesn't have a -`content` method defined on it, so attempting to read its content is a compiler -error like it is with `DraftPost`. Because the only way to get a published -`Post` instance that does have a `content` method defined is to call the -`approve` method on a `PendingReviewPost`, and the only way to get a -`PendingReviewPost` is to call the `request_review` method on a `DraftPost`, -we've now encoded the blog post workflow into the type system. - -This does mean we have to make some small changes to `main`. Because -`request_review` and `approve` return new instances rather than modifying the -struct they're called on, we need to add more `let post = ` shadowing -assignments to save the returned instances. We also can't have the assertions -about the draft and pending review post's contents being empty string anymore, -nor do we need them: we can't compile code that tries to use the content of -posts in those states any longer. The updated code in `main` is shown in -Listing 17-21: - -Filename: src/main.rs - -```rust,ignore -extern crate blog; -use blog::Post; - -fn main() { - let mut post = Post::new(); - - post.add_text("I ate a salad for lunch today"); - - let post = post.request_review(); - - let post = post.approve(); - - assert_eq!("I ate a salad for lunch today", post.content()); -} -``` +列表 17-15: 实现 `Post` 和 `State` trait 的 `request_review` 方法 -Listing 17-21: Modifications to `main` to use the new -implementation of the blog post workflow - -Having to change `main` to reassign `post` is what makes this implementation -not quite following the object-oriented state pattern anymore: the -transformations between the states are no longer encapsulated entirely within -the `Post` implementation. However, we've gained the property of having invalid -states be impossible because of the type system and type checking that happens -at compile time! This ensures that certain bugs, such as displaying the content -of an unpublished post, will be discovered before they make it to production. - -Try the tasks suggested that add additional requirements that we mentioned at -the start of this section to see how working with this version of the code -feels. - -Even though Rust is capable of implementing object-oriented design patterns, -there are other patterns like encoding state into the type system that are -available in Rust. These patterns have different tradeoffs than the -object-oriented patterns do. While you may be very familiar with -object-oriented patterns, rethinking the problem in order to take advantage of -Rust's features can give benefits like preventing some bugs at compile-time. -Object-oriented patterns won't always be the best solution in Rust, since Rust -has features like ownership that object-oriented languages don't have. - -## Summary - -No matter whether you think Rust is an object-oriented language or not after -reading this chapter, you've now seen that trait objects are a way to get some -object-oriented features in Rust. Dynamic dispatch can give your code some -flexibility in exchange for a bit of runtime performance. This flexibility can -be used to implement object-oriented patterns that can help with the -maintainability of your code. Rust also has different features, like ownership, -than object-oriented languages. An object-oriented pattern won't always be the -best way to take advantage of Rust's strengths. - -Next, let's look at another feature of Rust that enables lots of flexibility: -patterns. We've looked at them briefly throughout the book, but haven't seen -everything they're capable of yet. Let's go! +这里给 `State` trait 增加了 `request_review` 方法;所有实现了这个 trait 的类型现在都需要实现 `request_review` 方法。注意不用于使用`self`、 `&self` 或者 `&mut self` 作为方法的第一个参数,这里使用了 `self: Box`。这个语法意味着 \ No newline at end of file