From 8eebabaf2c44a7671c4d3d7e14071eb55f7515e4 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Sun, 19 Mar 2017 14:53:41 +0800 Subject: [PATCH] wip check to ch04-03 --- README.md | 8 +- docs/ch01-00-introduction.html | 9 +- docs/ch01-01-installation.html | 20 +- docs/ch01-02-hello-world.html | 79 +-- docs/ch02-00-guessing-game-tutorial.html | 132 ++-- docs/ch03-00-common-programming-concepts.html | 11 +- docs/ch03-01-variables-and-mutability.html | 38 +- docs/ch03-02-data-types.html | 32 +- docs/ch03-03-how-functions-work.html | 50 +- docs/ch03-04-comments.html | 10 +- docs/ch03-05-control-flow.html | 61 +- docs/ch04-00-understanding-ownership.html | 6 +- docs/ch04-01-what-is-ownership.html | 94 +-- docs/ch04-02-references-and-borrowing.html | 32 +- docs/ch04-03-slices.html | 37 +- docs/index.html | 9 +- docs/print.html | 611 ++++++++---------- src/PREFACE.md | 8 +- src/ch01-00-introduction.md | 12 +- src/ch01-01-installation.md | 23 +- src/ch01-02-hello-world.md | 80 +-- src/ch02-00-guessing-game-tutorial.md | 141 ++-- src/ch03-00-common-programming-concepts.md | 12 +- src/ch03-01-variables-and-mutability.md | 42 +- src/ch03-02-data-types.md | 38 +- src/ch03-03-how-functions-work.md | 54 +- src/ch03-04-comments.md | 10 +- src/ch03-05-control-flow.md | 62 +- src/ch04-00-understanding-ownership.md | 6 +- src/ch04-01-what-is-ownership.md | 100 +-- src/ch04-02-references-and-borrowing.md | 35 +- src/ch04-03-slices.md | 38 +- 32 files changed, 825 insertions(+), 1075 deletions(-) diff --git a/README.md b/README.md index e2c0c57..12a3ee9 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,5 @@ # Rust 程序设计语言(第二版) 简体中文版 -还在施工中... +还在施工中:正在翻译第十六章 -## Gitbook 中存在的问题 - -`
`中的 markdown 没有语法高亮QAQ - -[https://github.com/GitbookIO/gitbook/issues/1727](https://github.com/GitbookIO/gitbook/issues/1727) \ No newline at end of file +目前正在解决代码排版问题:已检查到第四章 \ No newline at end of file diff --git a/docs/ch01-00-introduction.html b/docs/ch01-00-introduction.html index 52a6d89..8fbe464 100644 --- a/docs/ch01-00-introduction.html +++ b/docs/ch01-00-introduction.html @@ -73,12 +73,15 @@
commit 4f2dc564851dc04b271a2260c834643dfd86c724

-

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

-

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

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

+

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

-

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

+

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

为本书做出贡献

本书是开源的。如果你发现任何错误,请不要犹豫,在 GitHub 上发起 issue 或提交 pull request。

+
+

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

+
diff --git a/docs/ch01-01-installation.html b/docs/ch01-01-installation.html index 4c0b0f2..51d1e12 100644 --- a/docs/ch01-01-installation.html +++ b/docs/ch01-01-installation.html @@ -73,39 +73,39 @@
commit 4f2dc564851dc04b271a2260c834643dfd86c724

-

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

+

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

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

在 Linux 或 Mac 上安装

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

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

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

-
Rust is installed now. Great!
+
Rust is installed now. Great!
 

当然,如果你不赞成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 安装页面获取其他选择。

卸载

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

-
$ rustup self uninstall
+
$ rustup self uninstall
 

故障排除

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

-
$ rustc --version
+
$ rustc --version
 

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

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

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

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

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

-

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

+

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

本地文档

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

-

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

+

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

diff --git a/docs/ch01-02-hello-world.html b/docs/ch01-02-hello-world.html index 5880888..18155d8 100644 --- a/docs/ch01-02-hello-world.html +++ b/docs/ch01-02-hello-world.html @@ -73,14 +73,14 @@
commit 4f2dc564851dc04b271a2260c834643dfd86c724

-

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

+

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

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

创建项目文件夹

-

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

+

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

Linux 和 Mac:

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

编写并运行 Rust 程序

-

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

+

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

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

Filename: main.rs

fn main() {
@@ -100,33 +100,33 @@ $ cd hello_world
 }
 

保存文件,并回到终端窗口。在 Linux 或 OSX 上,输入如下命令:

-
$ rustc main.rs
+
$ rustc main.rs
 $ ./main
 Hello, world!
 

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

分析 Rust 程序

-

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

+

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

fn main() {
 
 }
 
-

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

+

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

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

main()函数中:

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

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

-

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

+

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

+

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

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

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

编译和运行是两个步骤

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

-

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

-
$ rustc main.rs
+

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

+
$ rustc main.rs
 
-

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

-
$ ls
+

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

+
$ ls
 main  main.rs
 

在 Windows 上,输入:

@@ -135,32 +135,32 @@ main.exe main.rs

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

-
$ ./main  # or .\main.exe on Windows
+
$ ./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 程序。

Hello, Cargo!

-

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

-

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

+

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

+

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

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

-
$ cargo --version
+
$ cargo --version
 
-

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

+

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

使用 Cargo 创建项目

-

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

+

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

Linux 和 Mac:

-
$ cd ~/projects
+
$ cd ~/projects
 

Windows:

> cd %USERPROFILE%\projects
 

并在任何操作系统运行:

-
$ cargo new hello_cargo --bin
+
$ 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参数。

+

我们向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]
@@ -171,9 +171,10 @@ authors = ["Your Name <you@example.com>"]
 [dependencies]
 

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

-

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

-

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

+

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

+

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

现在看看 src/main.rs

+

Filename: src/main.rs

fn main() {
     println!("Hello, world!");
 }
@@ -183,19 +184,19 @@ authors = ["Your Name <you@example.com>"]
 
  • 代码位于 src 目录
  • 项目根目录包含一个 Cargo.toml 配置文件
  • -

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

    -

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

    +

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

    +

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

    构建并运行 Cargo 项目

    -

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

    +

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

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

    这应该创建 target/debug/hello_cargo(或者在 Windows 上是 target\debug\hello_cargo.exe)可执行文件,可以通过这个命令运行:

    -
    $ ./target/debug/hello_cargo # or .\target\debug\hello_cargo.exe on Windows
    +
    $ ./target/debug/hello_cargo # or .\target\debug\hello_cargo.exe on Windows
     Hello, world!
     

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

    -

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

    +

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

    Filename: Cargo.lock

    [root]
     name = "hello_cargo"
    @@ -203,29 +204,29 @@ version = "0.1.0"
     

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

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

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

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

    -
    $ cargo run
    +
    $ 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 会将它放到 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 build,然后一切将有序运行。即便这个项目很简单,现在它使用了很多接下来你 Rust 程序生涯将会用到的实用工具。事实上,无形中你可以使用下面的命令开始所有你想要从事的项目:

    -
    $ git clone someurl.com/someproject
    +

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

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

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

    diff --git a/docs/ch02-00-guessing-game-tutorial.html b/docs/ch02-00-guessing-game-tutorial.html index 2386177..29feb75 100644 --- a/docs/ch02-00-guessing-game-tutorial.html +++ b/docs/ch02-00-guessing-game-tutorial.html @@ -71,17 +71,17 @@

    ch02-00-guessing-game-tutorial.md
    -commit 7c1c935560190fcd64c0851e75dbeabf75fedd19

    +commit e6d6caab41471f7115a621029bd428a812c5260e

    -

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

    -

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

    +

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

    +

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

    准备一个新项目

    -

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

    -
    $ cargo new guessing_game --bin
    +

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

    +
    $ cargo new guessing_game --bin
     $ cd guessing_game
     

    第一个命令,cargo new,获取项目的名称(guessing_game)作为第一个参数。--bin参数告诉 Cargo 创建一个二进制项目,与第一章类似。第二个命令进入到新创建的项目目录。

    -

    看一样生成的 Cargo.toml 文件:

    +

    看看生成的 Cargo.toml 文件:

    Filename: Cargo.toml

    [package]
     name = "guessing_game"
    @@ -90,7 +90,7 @@ authors = ["Your Name <you@example.com>"]
     
     [dependencies]
     
    -

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

    +

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

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

    Filename: src/main.rs

    fn main() {
    @@ -103,12 +103,11 @@ authors = ["Your Name <you@example.com>"]
          Running `target/debug/guessing_game`
     Hello, world!
     
    -

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

    +

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

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

    处理一次猜测

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

    -
    -Filename: src/main.rs +

    Filename: src/main.rs

    use std::io;
     
     fn main() {
    @@ -124,14 +123,11 @@ fn main() {
         println!("You guessed: {}", guess);
     }
     
    -
    -

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

    -
    -
    +

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

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

    use std::io;
     
    -

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

    +

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

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

    fn main() {
     
    @@ -156,7 +152,7 @@ let mut bar = 5; // mutable

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

    -

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

    +

    现在我们知道了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新的、空的实例上。哟!

    @@ -164,11 +160,11 @@ let mut bar = 5; // mutable
    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

    +

    如果我们在程序的开头没有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来使其可变。

    -

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

    +

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

    +

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

    .expect("Failed to read line");
     

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

    @@ -176,19 +172,19 @@ let mut bar = 5; // mutable

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

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

    -

    之前提到过,read_line将用户输入放入到传递给它字符串中,不过它也返回一个值————一个io::Result。Rust 标准库中有很多叫做Result的类型。一个Result泛型以及对应子模块的特定版本,比如io::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中的值并原原本本的返回给你,这样就可以使用它了。在本例中,返回值是用户输入到标准输入的一些字符。

    +

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

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

    -
    $ cargo build
    +
    $ 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,
     #[warn(unused_must_use)] on by default
     src/main.rs:10     io::stdin().read_line(&mut guess);
                        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     
    -

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

    +

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

    使用println!占位符打印值

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

    println!("You guessed: {}", guess);
    @@ -213,8 +209,8 @@ You guessed: 6
     

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

    生成一个秘密数字

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

    -

    使用 crate 来增加更多功能

    -

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

    +

    使用 crate 来增加更多功能

    +

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

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

    Filename: Cargo.toml

    [dependencies]
    @@ -222,9 +218,8 @@ You guessed: 6
     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 build
    +

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

    +
    $ cargo build
         Updating registry `https://github.com/rust-lang/crates.io-index`
      Downloading rand v0.3.14
      Downloading libc v0.2.14
    @@ -232,21 +227,18 @@ rand = "0.3.14"
        Compiling rand v0.3.14
        Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
     
    -
    -

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

    -
    -
    +

    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 从 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 build
    +
    $ cargo build
        Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
     

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

    -

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

    -

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

    +

    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 文件。我们将会在这个文件编写全部的代码。

    更新 crate 到一个新版本

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

    @@ -255,7 +247,7 @@ as a dependency

  • 如果成功了,Cargo 会把这些版本写入 Cargo.lock 文件。
  • 不过,Cargo 默认只会寻找大于0.3.0而小于0.4.0的版本。如果rand crate 发布了两个新版本,0.3.150.4.0,在运行cargo update时会出现如下内容:

    -
    $ cargo update
    +
    $ cargo update
         Updating registry `https://github.com/rust-lang/crates.io-index`
         Updating rand v0.3.14 -> v0.3.15
     
    @@ -268,9 +260,8 @@ rand = "0.4.0"

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

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

    生成一个随机数

    -

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

    -
    -Filename: src/main.rs +

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

    +

    Filename: src/main.rs

    extern crate rand;
     
     use std::io;
    @@ -293,17 +284,15 @@ fn main() {
         println!("You guessed: {}", guess);
     }
     
    -
    -

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

    -
    -
    +

    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

    +

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

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

    尝试运行程序几次:

    -
    $ cargo run
    +
    $ cargo run
        Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
          Running `target/debug/guessing_game`
     Guess the number!
    @@ -321,9 +310,8 @@ You guessed: 5
     

    你应该能得到不同的随机数,同时他们应该都是在 1 和 100 之间的。干得漂亮!

    比较猜测与秘密数字

    -

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

    -
    -Filename: src/main.rs +

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

    +

    Filename: src/main.rs

    extern crate rand;
     
     use std::io;
    @@ -353,10 +341,8 @@ fn main() {
         }
     }
     
    -
    -

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

    -
    -
    +

    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) {
    @@ -366,10 +352,10 @@ fn main() {
     }
     

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

    -

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

    +

    一个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表达式就此终止,因为在这个特定场景下没有检查最后一个分支的必要。

    -

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

    -
    $ cargo build
    +

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

    +
    $ cargo build
        Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
     error[E0308]: mismatched types
       --> src/main.rs:23:21
    @@ -383,7 +369,7 @@ 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 不会比较字符串类型和数字类型。

    +

    错误的核心表明这里有不匹配的类型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函数体中增加如下两行代码来实现:

    Filename: src/main.rs

    extern crate rand;
    @@ -424,10 +410,10 @@ fn main() {
     

    这里创建了一个叫做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类型,非常像之前在 XX 页“使用Result类型来处理潜在的错误”部分讨论的read_line方法。这里再次类似的使用expect方法处理这个Result类型。如果parse因为不能从字符串生成一个数字而返回一个ErrResult成员时,expect会使游戏崩溃并打印提供给它的信息。如果parse能成功地将字符串转换为一个数字,它会返回ResultOk成员,同时expect会返回Ok中我们需要的数字。

    +

    字符串的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中我们需要的数字。

    现在让我们运行程序!

    -
    $ cargo run
    +
    $ cargo run
        Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
          Running `target/guessing_game`
     Guess the number!
    @@ -477,8 +463,8 @@ fn main() {
     }
     

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

    -

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

    -
    $ cargo run
    +

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

    +
    $ cargo run
        Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
          Running `target/guessing_game`
     Guess the number!
    @@ -501,7 +487,7 @@ 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来在用户胜利时退出游戏:

    Filename: src/main.rs

    @@ -554,7 +540,7 @@ fn main() {

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

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

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

    -
    $ cargo run
    +
    $ cargo run
        Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
          Running `target/guessing_game`
     Guess the number!
    @@ -575,8 +561,7 @@ You guessed: 61
     You win!
     

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

    -
    -Filename: src/main.rs +

    Filename: src/main.rs

    extern crate rand;
     
     use std::io;
    @@ -614,13 +599,10 @@ fn main() {
         }
     }
     
    -
    -

    Listing 2-5: Complete code of the guessing game

    -
    -
    -

    总结一下,

    +

    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 00b5f85..215d1ea 100644 --- a/docs/ch03-00-common-programming-concepts.html +++ b/docs/ch03-00-common-programming-concepts.html @@ -69,18 +69,17 @@

    通用编程概念

    -

    ch03-00-common-programming-concepts.md +

    ch03-00-common-programming-concepts.md
    -commit 2067b6e2bff990bceb39ae8f35780bd3bed08644

    +commit 04aa3a45eb72855b34213703718f50a12a3eeec8

    -

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

    -

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

    - +

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

    +

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

    关键字

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

    - +
    diff --git a/docs/ch03-01-variables-and-mutability.html b/docs/ch03-01-variables-and-mutability.html index 66edbf2..72c49f5 100644 --- a/docs/ch03-01-variables-and-mutability.html +++ b/docs/ch03-01-variables-and-mutability.html @@ -69,12 +69,12 @@

    变量和可变性

    -

    ch03-01-variables-and-mutability.md +

    ch03-01-variables-and-mutability.md
    -commit b0fab378c9c6a817d4f0080d7001d085017cdef8

    +commit 04aa3a45eb72855b34213703718f50a12a3eeec8

    -

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

    -

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

    +

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

    +

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

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

    Filename: src/main.rs

    fn main() {
    @@ -85,7 +85,7 @@ commit b0fab378c9c6a817d4f0080d7001d085017cdef8

    }

    保存并使用cargo run运行程序。应该会看到一个错误信息,如下输出所示:

    -
    $ cargo run
    +
    $ cargo run
        Compiling variables v0.0.1 (file:///projects/variables)
     error[E0384]: re-assignment of immutable variable `x`
      --> src/main.rs:4:5
    @@ -97,7 +97,7 @@ error[E0384]: re-assignment of immutable variable `x`
       |     ^^^^^ re-assignment of immutable variable
     

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

    -

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

    +

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

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

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

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

    @@ -110,22 +110,26 @@ error[E0384]: re-assignment of immutable variable `x` }

    当运行这个程序,出现如下:

    -
    $ cargo run
    +
    $ cargo run
        Compiling variables v0.1.0 (file:///projects/variables)
          Running `target/debug/variables`
     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关键字声明,而且必须注明值的类型。现在我们准备在下一部分,“数据类型”,涉及到类型和类型注解,所以现在无需担心这些细节。常量可以在任何作用域声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。最后一个区别是常量只能用于常量表达式,而不能作为函数调用的结果或任何其他只在运行时使用到的值。

    -

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

    +

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

    +

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

    +

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

    +

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

    +

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

    +

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

    const MAX_POINTS: u32 = 100_000;
     
    -

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

    -

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

    -

    覆盖

    +

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

    +

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

    +

    覆盖(Shadowing)

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

    Filename: src/main.rs

    fn main() {
    @@ -139,7 +143,7 @@ The value of x is: 6
     }
     

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

    -
    $ cargo run
    +
    $ cargo run
        Compiling variables v0.1.0 (file:///projects/variables)
          Running `target/debug/variables`
     The value of x is: 12
    @@ -154,7 +158,7 @@ let spaces = spaces.len();
     spaces = spaces.len();
     

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

    -
    error[E0308]: mismatched types
    +
    error[E0308]: mismatched types
      --> src/main.rs:3:14
       |
     3 |     spaces = spaces.len();
    @@ -163,7 +167,7 @@ spaces = spaces.len();
       = note: expected type `&str`
       = note:    found type `usize`
     
    -

    现在我们探索了变量如何工作,让我们看看他们能有多少数据类型。

    +

    现在我们探索了变量如何工作,让我们看看更多的数据类型。

    diff --git a/docs/ch03-02-data-types.html b/docs/ch03-02-data-types.html index a433032..8e706ae 100644 --- a/docs/ch03-02-data-types.html +++ b/docs/ch03-02-data-types.html @@ -69,20 +69,20 @@

    数据类型

    -

    ch03-02-data-types.md +

    ch03-02-data-types.md
    -commit 6436ebee2a84820adf77231cead6b5691c8e2744

    +commit 04aa3a45eb72855b34213703718f50a12a3eeec8

    -

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

    +

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

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

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

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

    -
    error[E0282]: unable to infer enough type information about `_`
    - --> src/main.rs:2:5
    +
    error[E0282]: unable to infer enough type information about `_`
    + --> src/main.rs:2:9
       |
    -2 | let guess = "42".parse().expect("Not a number!");
    -  |     ^^^^^ cannot infer type for `_`
    +2 |     let guess = "42".parse().expect("Not a number!");
    +  |         ^^^^^ cannot infer type for `_`
       |
       = note: type annotations or generic parameter binding required
     
    @@ -91,10 +91,7 @@ commit 6436ebee2a84820adf77231cead6b5691c8e2744

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

    整型

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

    -
    -
    -

    Table 3-1: Integer Types in Rust

    -
    +

    Table 3-1: Integer Types in Rust

    @@ -102,15 +99,11 @@ commit 6436ebee2a84820adf77231cead6b5691c8e2744

    Length Signed Unsigned
    8-bit i8 u8
    16-bit i16 u16
    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位的。

    -
    -
    -

    Table 3-2: Integer Literals in Rust

    -
    +

    Table 3-2: Integer Literals in Rust

    @@ -118,10 +111,9 @@ commit 6436ebee2a84820adf77231cead6b5691c8e2744

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

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

    浮点型

    -

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

    +

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

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

    Filename: src/main.rs

    fn main() {
    @@ -193,7 +185,7 @@ commit 6436ebee2a84820adf77231cead6b5691c8e2744

    }

    程序首先创建了一个元组并绑定到tup变量上。接着使用了let和一个模式将tup分成了三个不同的变量,xyz。这叫做解构destructuring),因为它将一个元组拆成了三个部分。最后,程序打印出了y的值,也就是6.4

    -

    除了使用模式匹配解构之外,也可以使用点号(.)后跟值的索引来直接访问。例如:

    +

    除了使用模式匹配解构之外,也可以使用点号(.)后跟值的索引来直接访问他们。例如:

    Filename: src/main.rs

    fn main() {
         let x: (i32, f64, u8) = (500, 6.4, 1);
    @@ -242,7 +234,7 @@ commit 6436ebee2a84820adf77231cead6b5691c8e2744

    }

    使用cargo run运行代码后会产生如下结果:

    -
    $ cargo run
    +
    $ cargo run
        Compiling arrays v0.1.0 (file:///projects/arrays)
          Running `target/debug/arrays`
     thread '<main>' panicked at 'index out of bounds: the len is 5 but the index is
    diff --git a/docs/ch03-03-how-functions-work.html b/docs/ch03-03-how-functions-work.html
    index fe4d28e..91dd843 100644
    --- a/docs/ch03-03-how-functions-work.html
    +++ b/docs/ch03-03-how-functions-work.html
    @@ -69,9 +69,9 @@
                     

    函数如何工作

    -

    ch03-03-how-functions-work.md +

    ch03-03-how-functions-work.md
    -commit 52b7fcbfdd35915cb21e6d492fb6c86764f53b47

    +commit 04aa3a45eb72855b34213703718f50a12a3eeec8

    函数在 Rust 代码中应用广泛。你已经见过一个语言中最重要的函数:main函数,它时很多程序的入口点。你也见过了fn关键字,它用来声明新函数。

    Rust 代码使用 snake case 作为函数和变量名称的规范风格。在 snake case 中,所有字母都是小写并使用下划线分隔单词。这里是一个函数定义程序的例子:

    @@ -87,9 +87,9 @@ fn another_function() { }

    Rust 中的函数定义以fn开始并在函数名后跟一对括号。大括号告诉编译器哪里是函数体的开始和结尾。

    -

    可以使用定义过的函数名后跟括号来调用任意函数。因为another_function在程序中已经定义过了,它可以在main函数中被调用。注意,源码中another_functionmain函数之后被定义;也可以在之前定义。Rust 不关心函数定义于何处,只要他们被定义了。

    -

    让我们开始一个叫做functions的新二进制项目来进一步探索函数。将上面的another_function例子写入 src/main.rs 中并运行。你应该会看到如下输出:

    -
    $ cargo run
    +

    可以使用定义过的函数名后跟括号来调用任意函数。因为another_function已经在程序中定义过了,它可以在main函数中被调用。注意,源码中another_functionmain函数之后被定义;也可以在其之前定义。Rust 不关心函数定义于何处,只要他们被定义了。

    +

    让我们开始一个叫做 functions 的新二进制项目来进一步探索函数。将上面的another_function例子写入 src/main.rs 中并运行。你应该会看到如下输出:

    +
    $ cargo run
        Compiling functions v0.1.0 (file:///projects/functions)
          Running `target/debug/functions`
     Hello, world!
    @@ -97,7 +97,7 @@ Another function.
     

    代码在main函数中按照他们出现的顺序被执行。首先,打印“Hello, world!”信息,接着another_function被调用并打印它的信息。

    函数参数

    -

    函数也可以被定义为拥有参数parameters),他们是作为函数签名一部分的特殊变量。当函数拥有参数,可以为这些参数提供具体的值。技术上讲,这些具体值被称为参数( arguments),不过通常的习惯是倾向于在函数定义中的变量和调用函数时传递的具体值都可以用 "parameter" 和 "argument" 而不加区别。

    +

    函数也可以被定义为拥有参数parameters),他们是作为函数签名一部分的特殊变量。当函数拥有参数,可以为这些参数提供具体的值。技术上讲,这些具体值被称为参数( arguments),不过通常的习惯是倾向于在函数定义中的变量和调用函数时传递的具体值都可以用“parameter”和“argument”而不加区别。

    如下被重写的another_function版本展示了 Rust 中参数是什么样的:

    Filename: src/main.rs

    fn main() {
    @@ -127,7 +127,7 @@ fn another_function(x: i32, y: i32) {
         println!("The value of y is: {}", y);
     }
     
    -

    这个例子创建了一个有两个参数的函数,都是i32类型的。函数打印出了这两个参数的值。注意函数参数并一定都是相同的————这个例子中他们只是碰巧相同。

    +

    这个例子创建了一个有两个参数的函数,都是i32类型的。函数打印出了这两个参数的值。注意函数参数并不一定都是相同类型的,这个例子中他们只是碰巧相同罢了。

    尝试运行代码。使用上面的例子替换当前 function 项目的 src/main.rs 文件,并cargo run运行它:

    $ cargo run
        Compiling functions v0.1.0 (file:///projects/functions)
    @@ -135,22 +135,18 @@ fn another_function(x: i32, y: i32) {
     The value of x is: 5
     The value of y is: 6
     
    -

    因为我们使用5作为x的值和6作为y的值来调用函数,这两个字符串使用这些值并被打印出来。

    +

    因为我们使用5作为x的值和6作为y的值来调用函数,这两个字符串和他们的值并被打印出来。

    函数体

    函数体由一系列的语句和一个可选的表达式构成。目前为止,我们只涉及到了没有结尾表达式的函数,不过我们见过表达式作为了语句的一部分。因为 Rust 是一个基于表达式(expression-based)的语言,这是一个需要理解的(不同于其他语言)重要区别。其他语言并没有这样的区别,所以让我们看看语句与表达式有什么区别以及他们是如何影响函数体的。

    语句与表达式

    我们已经用过语句与表达式了。语句Statements)是执行一些操作但不返回值的指令。表达式(Expressions)计算并产生一个值。让我们看看一些例子:

    使用let关键字创建变量并绑定一个值是一个语句。在列表 3-3 中,let y = 6;是一个语句:

    -
    -Filename: src/main.rs +

    Filename: src/main.rs

    fn main() {
         let y = 6;
     }
     
    -
    -

    Listing 3-3: A main function declaration containing one statement.

    -
    -
    +

    Listing 3-3: A main function declaration containing one statement.

    函数定义也是语句;上面整个例子本身就是一个语句。

    语句并不返回值。因此,不能把let语句赋值给另一个变量,比如下面的例子尝试做的:

    Filename: src/main.rs

    @@ -159,7 +155,7 @@ The value of y is: 6 }

    当运行这个程序,会得到如下错误:

    -
    $ cargo run
    +
    $ cargo run
        Compiling functions v0.1.0 (file:///projects/functions)
     error: expected expression, found statement (`let`)
      --> src/main.rs:2:14
    @@ -170,7 +166,7 @@ error: expected expression, found statement (`let`)
       = note: variable declaration using `let` is a statement
     

    let y = 6语句并不返回值,所以并没有x可以绑定的值。这与其他语言不同,例如 C 和 Ruby,他们的赋值语句返回所赋的值。在这些语言中,可以这么写x = y = 6这样xy的值都是6;这在 Rust 中可不行。

    -

    表达式进行计算而且他们组成了其余大部分 Rust 代码。考虑一个简单的数学运算,比如5 + 6,这是一个表达式并计算出值11。表达式可以是语句的一部分:在列表 3-3 中有这个语句let y = 6;6是一个表达式它计算出的值是6。函数调用是一个表达式。宏调用是一个表达式。我们用来创新建作用域的大括号(代码块),{},也是一个表达式,例如:

    +

    表达式计算出一些值,而且他们组成了其余大部分你将会编写的 Rust 代码。考虑一个简单的数学运算,比如5 + 6,这是一个表达式并计算出值11。表达式可以是语句的一部分:在列表 3-3 中有这个语句let y = 6;6是一个表达式它计算出的值是6。函数调用是一个表达式。宏调用是一个表达式。我们用来创新建作用域的大括号(代码块),{},也是一个表达式,例如:

    Filename: src/main.rs

    fn main() {
         let x = 5;
    @@ -203,8 +199,8 @@ fn main() {
         println!("The value of x is: {}", x);
     }
     
    -

    在函数five中并没有函数调用、宏、甚至也没有let语句————只有数字5它子集。这在 Rust 中是一个完全有效的函数。注意函数的返回值类型也被指定了,就是-> i32。尝试运行代码;输出应该看起来像这样:

    -
    $ cargo run
    +

    在函数five中并没有函数调用、宏、甚至也没有let语句————只有数字5它自己。这在 Rust 中是一个完全有效的函数。注意函数的返回值类型也被指定了,就是-> i32。尝试运行代码;输出应该看起来像这样:

    +
    $ cargo run
        Compiling functions v0.1.0 (file:///projects/functions)
          Running `target/debug/functions`
     The value of x is: 5
    @@ -212,7 +208,7 @@ The value of x is: 5
     

    函数five的返回值是5,也就是为什么返回值类型是i32。让我们仔细检查一下这段代码。这有两个重要的部分:首先,let x = five();这一行表明我们使用函数的返回值来初始化了一个变量。因为函数five返回5,这一行与如下这行相同:

    let x = 5;
     
    -

    再次,函数five没有参数并定义了返回值类型,不过函数体只有单单一个5也没有分号,因为这是我们想要返回值的表达式。让我们看看另一个例子:

    +

    其次,函数five没有参数并定义了返回值类型,不过函数体只有单单一个5也没有分号,因为这是我们想要返回值的表达式。让我们看看另一个例子:

    Filename: src/main.rs

    fn main() {
         let x = plus_one(5);
    @@ -225,6 +221,7 @@ fn plus_one(x: i32) -> i32 {
     }
     

    运行代码会打印出The value of x is: 6。如果在包含x + 1的那一行的结尾加上一个分号,把它从表达式变成语句后会怎样呢?

    +

    Filename: src/main.rs

    fn main() {
         let x = plus_one(5);
     
    @@ -236,19 +233,24 @@ fn plus_one(x: i32) -> i32 {
     }
     

    运行代码会产生一个错误,如下:

    -
    error[E0269]: not all control paths return a value
    - --> src/main.rs:7:1
    +
    error[E0308]: mismatched types
    + --> src/main.rs:7:28
       |
    -7 | fn plus_one(x: i32) -> i32 {
    -  | ^
    +7 |   fn plus_one(x: i32) -> i32 {
    +  |  ____________________________^ starting here...
    +8 | |     x + 1;
    +9 | | }
    +  | |_^ ...ending here: expected i32, found ()
       |
    +  = note: expected type `i32`
    +             found type `()`
     help: consider removing this semicolon:
      --> src/main.rs:8:10
       |
     8 |     x + 1;
       |          ^
     
    -

    主要的错误信息,“并非所有控制路径都返回一个值”(“not all control paths return a value,”),揭示了代码的核心问题。函数plus_one的定义说明它要返回一个i32,不过语句并不返回一个值。因此,这个函数没有返回任何值,这与函数定义相矛盾并导致一个错误。在输出中,Rust 提供了一个可能会对修正问题有帮助的信息:它建议去掉分号,这会修复这个错误。

    +

    主要的错误信息,“mismatched types,”(类型不匹配),揭示了代码的核心问题。函数plus_one的定义说明它要返回一个i32,不过语句并不返回一个值,这由那个空元组()表明。因此,这个函数没有返回任何值,这与函数定义相矛盾并导致一个错误。在输出中,Rust 提供了一个可能会对修正问题有帮助的信息:它建议去掉分号,这会修复这个错误。

    diff --git a/docs/ch03-04-comments.html b/docs/ch03-04-comments.html index 5b5c7ac..a76a497 100644 --- a/docs/ch03-04-comments.html +++ b/docs/ch03-04-comments.html @@ -69,11 +69,11 @@

    注释

    -

    ch03-04-comments.md +

    ch03-04-comments.md
    -commit 74d6fc999b986b74bf94edd6dcbb5a08a16c12de

    +commit 4f2dc564851dc04b271a2260c834643dfd86c724

    -

    所有编程语言都力求使他们的代码易于理解,不过有时额外的解释需要得到保障。在这种情况下,程序员在源码中留下记录,或者注释comments),编译器会忽略他们不过其他阅读代码的人可能会用得上。

    +

    所有编程语言都力求使他们的代码易于理解,不过有时需要提供额外的解释。在这种情况下,程序员在源码中留下记录,或者注释comments),编译器会忽略他们不过其他阅读代码的人可能会用得上。

    这是一个注释的例子:

    // Hello, world.
     
    @@ -82,13 +82,13 @@ commit 74d6fc999b986b74bf94edd6dcbb5a08a16c12de

    // multiple lines of comments to do it! Whew! Hopefully, this comment will // explain what’s going on.
    -

    注释也可以在放在包含代码的行的结尾:

    +

    注释也可以在放在包含代码的行的末尾:

    Filename: src/main.rs

    fn main() {
         let lucky_number = 7; // I’m feeling lucky today.
     }
     
    -

    不过你会经常看到他们被以这种格式使用,也就是位于它解释的代码行的上面一行:

    +

    不过你会经常看到他们被以这种格式使用,也就是位于它所解释的代码行的上面一行:

    Filename: src/main.rs

    fn main() {
         // I’m feeling lucky today.
    diff --git a/docs/ch03-05-control-flow.html b/docs/ch03-05-control-flow.html
    index 92af582..a9c724f 100644
    --- a/docs/ch03-05-control-flow.html
    +++ b/docs/ch03-05-control-flow.html
    @@ -71,7 +71,7 @@
     

    ch03-05-control-flow.md
    -commit 784a3ec5e8b9c6bff456ab9f0efd4dabcc180dda

    +commit 04aa3a45eb72855b34213703718f50a12a3eeec8

    通过条件是不是真来决定是否某些代码,或者根据条件是否为真来重复运行一段代码是大部分编程语言的基本组成部分。Rust 代码中最常见的用来控制执行流的结构是if表达式和循环。

    if表达式

    @@ -88,9 +88,10 @@ commit 784a3ec5e8b9c6bff456ab9f0efd4dabcc180dda

    } }
    +

    所有if表达式以if关键字开头,它后跟一个条件。在这个例子中,条件检查number是否有一个小于 5 的值。在条件为真时希望执行的代码块位于紧跟条件之后的大括号中。if表达式中与条件关联的代码块有时被叫做 arms,就像第二章“比较猜测与秘密数字”部分中讨论到的match表达式中分支一样。也可以包含一个可选的else表达式,这里我们就这么做了,来提供一个在条件为假时应当执行的代码块。如果不提供else表达式并且条件为假时,程序会直接忽略if代码块并继续执行下面的代码。

    尝试运行代码,应该能看到如下输出:

    -
    $ cargo run
    +
    $ cargo run
        Compiling branches v0.1.0 (file:///projects/branches)
          Running `target/debug/branches`
     condition was true
    @@ -99,7 +100,7 @@ condition was true
     
    let number = 7;
     

    再次运行程序并查看输出:

    -
    $ cargo run
    +
    $ cargo run
        Compiling branches v0.1.0 (file:///projects/branches)
          Running `target/debug/branches`
     condition was false
    @@ -115,18 +116,14 @@ condition was false
     }
     

    这里if条件的值是3,Rust 抛出了一个错误:

    -
       Compiling branches v0.1.0 (file:///projects/branches)
    -error[E0308]: mismatched types
    +
    error[E0308]: mismatched types
      --> src/main.rs:4:8
       |
     4 |     if number {
       |        ^^^^^^ expected bool, found integral variable
       |
       = note: expected type `bool`
    -  = note:    found type `{integer}`
    -
    -error: aborting due to previous error
    -Could not compile `branches`.
    +             found type `{integer}`
     

    这个错误表明 Rust 期望一个bool不过却得到了一个整型。Rust 并不会尝试自动地将非布尔值转换为布尔值,不像例如 Ruby 和 JavaScript 这样的语言。必须总是显式地使用boolean作为if的条件。例如如果想要if代码块只在一个数字不等于0时执行,可以把if表达式修改为如下:

    Filename: src/main.rs

    @@ -157,17 +154,16 @@ Could not compile `branches`. }

    这个程序有四个可能的执行路径。运行后应该能看到如下输出:

    -
    $ cargo run
    +
    $ cargo run
        Compiling branches v0.1.0 (file:///projects/branches)
          Running `target/debug/branches`
     number is divisible by 3
     

    当执行这个程序,它按顺序检查每个if表达式并执行第一个条件为真的代码块。注意即使 6 可以被 2 整除,也不会出现number is divisible by 2的输出,更不会出现else块中的number is not divisible by 4, 3, or 2。原因是 Rust 只会执行第一个条件为真的代码块,并且它一旦找到一个以后,就不会检查剩下的条件了。

    -

    使用过多的else if表达式会使代码显得杂乱无章,所以如果有多于一个else if,最好重构代码。为此第六章介绍了 Rust 一个叫做match的强大的分支结构(branching construct)。

    +

    使用过多的else if表达式会使代码显得杂乱无章,所以如果有多于一个else if,最好重构代码。为此第六章会介绍 Rust 中一个叫做match的强大的分支结构(branching construct)。

    let语句中使用if

    -

    因为if是一个表达式,我们可以在let语句的右侧使用它,例如列表 3-4:

    -
    -Filename: src/main.rs +

    因为if是一个表达式,我们可以在let语句的右侧使用它,例如在列表 3-4 中:

    +

    Filename: src/main.rs

    fn main() {
         let condition = true;
         let number = if condition {
    @@ -179,17 +175,15 @@ number is divisible by 3
         println!("The value of number is: {}", number);
     }
     
    -
    -

    Listing 3-4: Assigning the result of an if expression to a variable

    -
    -
    +

    Listing 3-4: Assigning the result of an if expression +to a variable

    number变量将会绑定到基于if表达式结果的值。运行这段代码看看会出现什么:

    -
    $ cargo run
    +
    $ cargo run
        Compiling branches v0.1.0 (file:///projects/branches)
          Running `target/debug/branches`
     The value of number is: 5
     
    -

    还记得代码块的值是其最后一个表达式的值,以及数字本身也是一个表达式吗。在这个例子中,整个if表达式的值依赖哪个代码块被执行。这意味着if的每个分支的可能的返回值都必须是相同类型;在列表 3-4 中,if分支和else分支的结果都是i32整型。不过如果像下面的例子一样这些类型并不相同会怎么样呢?

    +

    还记得代码块的值是其最后一个表达式的值,以及数字本身也是一个表达式吗。在这个例子中,整个if表达式的值依赖哪个代码块被执行。这意味着if的每个分支的可能的返回值都必须是相同类型;在列表 3-4 中,if分支和else分支的结果都是i32整型。不过如果像下面的例子那样这些类型并不匹配会怎么样呢?

    Filename: src/main.rs

    fn main() {
         let condition = true;
    @@ -204,17 +198,21 @@ The value of number is: 5
     }
     

    当运行这段代码,会得到一个错误。ifelse分支的值类型是不相容的,同时 Rust 也准确地表明了在程序中的何处发现的这个问题:

    -
       Compiling branches v0.1.0 (file:///projects/branches)
    -error[E0308]: if and else have incompatible types
    +
    error[E0308]: if and else have incompatible types
      --> src/main.rs:4:18
       |
    -4 |     let number = if condition {
    -  |                  ^ expected integral variable, found reference
    +4 |       let number = if condition {
    +  |  __________________^ starting here...
    +5 | |         5
    +6 | |     } else {
    +7 | |         "six"
    +8 | |     };
    +  | |_____^ ...ending here: expected integral variable, found reference
       |
       = note: expected type `{integer}`
    -  = note:    found type `&’static str`
    +             found type `&'static str`
     
    -

    if代码块的表达式返回一个整型,而else代码块返回一个字符串。这并不可行因为变量必须只有一个类型。Rust 需要在编译时就确切的知道number变量的类型,这样它就可以在编译时证明其他使用number变量的地方它的类型是有效的。Rust 并不能够在number的类型只能在运行时确定的情况下完成这些功能;这样会使编译器变得更复杂而且只能为代码提供更少的保障,因为它不得不记录所有变量的多种可能的类型。

    +

    if代码块的表达式返回一个整型,而else代码块返回一个字符串。这并不可行,因为变量必须只有一个类型。Rust 需要在编译时就确切的知道number变量的类型,这样它就可以在编译时证明其他使用number变量的地方它的类型是有效的。Rust 并不能够在number的类型只能在运行时确定的情况下工作;这样会使编译器变得更复杂而且只能为代码提供更少的保障,因为它不得不记录所有变量的多种可能的类型。

    使用循环重复执行

    多次执行一段代码是很常用的。为了这个功能,Rust 提供了多种循环loops)。一个循环执行循环体中的代码直到结尾并紧接着从回到开头继续执行。为了实验一下循环,让我们创建一个叫做 loops 的新项目。

    Rust 有三种循环类型:loopwhilefor。让我们每一个都试试。

    @@ -242,7 +240,7 @@ again!

    幸运的是,Rust 提供了另一个更可靠的方式来退出循环。可以使用break关键字来告诉程序何时停止执行循环。还记得我们在第二章猜猜看游戏的“猜测正确后退出”部分使用过它来在用户猜对数字赢得游戏后退出程序吗。

    while条件循环

    在程序中计算循环的条件也很常见。当条件为真,执行循环。当条件不再为真,调用break停止循环。这个循环类型可以通过组合loopifelsebreak来实现;如果你喜欢的话,现在就可以在程序中试试。

    -

    然而,这个模式太常见了所以 Rust 为此提供了一个内建的语言结构,它被称为while循环。下面的例子使用了while:程序循环三次,每次数字都减一。接着,在循环之后,打印出另一个信息并退出:

    +

    然而,这个模式太常见了以至于 Rust 为此提供了一个内建的语言结构,它被称为while循环。下面的例子使用了while:程序循环三次,每次数字都减一。接着,在循环之后,打印出另一个信息并退出:

    Filename: src/main.rs

    fn main() {
         let mut number = 3;
    @@ -259,8 +257,7 @@ again!
     

    这个结构消除了很多需要嵌套使用loopifelsebreak的代码,这样显得更加清楚。当条件为真就执行,否则退出循环。

    使用for遍历集合

    可以使用while结构来遍历一个元素集合,比如数组。例如:

    -
    -Filename: src/main.rs +

    Filename: src/main.rs

    fn main() {
         let a = [10, 20, 30, 40, 50];
         let mut index = 0;
    @@ -272,10 +269,8 @@ again!
         }
     }
     
    -
    -

    Listing 3-5: Looping through each element of a collection using a while loop

    -
    -
    +

    Listing 3-5: Looping through each element of a collection +using a while loop

    这里代码对数组中的元素进行计数。它从索引0开始,并接着循环直到遇到数组的最后一个索引(这时,index < 5不再为真)。运行这段代码会打印出数组中的每一个元素:

    $ cargo run
        Compiling loops v0.1.0 (file:///projects/loops)
    diff --git a/docs/ch04-00-understanding-ownership.html b/docs/ch04-00-understanding-ownership.html
    index 19e73b3..30f9aa0 100644
    --- a/docs/ch04-00-understanding-ownership.html
    +++ b/docs/ch04-00-understanding-ownership.html
    @@ -69,11 +69,11 @@
                     

    认识所有权

    -

    ch04-00-understanding-ownership.md +

    ch04-00-understanding-ownership.md
    -commit 759067b651a48a4a66485fe0876d318d398fb4fe

    +commit 4f2dc564851dc04b271a2260c834643dfd86c724

    -

    所有权(系统)是 Rust 最独特的功能,它令 Rust 可以无需垃圾回收(garbage collector)就能保障内存安全。因此,理解 Rust 中所有权如何工作是十分重要的。本章我们将讲到所有权以及相关功能:借用、slices 以及 Rust 如何在内存中安排数据。

    +

    所有权(系统)是 Rust 最独特的功能,它使得 Rust 可以无需垃圾回收(garbage collector)就能保障内存安全。因此,理解 Rust 中所有权如何工作是十分重要的。本章我们将讲到所有权以及相关功能:借用、slice 以及 Rust 如何在内存中摆放数据。

    diff --git a/docs/ch04-01-what-is-ownership.html b/docs/ch04-01-what-is-ownership.html index c866973..65019a5 100644 --- a/docs/ch04-01-what-is-ownership.html +++ b/docs/ch04-01-what-is-ownership.html @@ -69,9 +69,9 @@

    什么是所有权

    -

    ch04-01-what-is-ownership.md +

    ch04-01-what-is-ownership.md
    -commit cc053d91f41793e54d5321abe027b0c163d735b8

    +commit fae5fa82d728b5965ecbba84060689430345e509

    Rust 的核心功能(之一)是所有权ownership)。虽然这个功能理解起来很直观,不过它对语言的其余部分有着更深层的含义。

    所有程序都必须管理他们运行时使用计算机内存的方式。一些语言中使用垃圾回收在程序运行过程中来时刻寻找不再被使用的内存;在另一些语言中,程序员必须亲自分配和释放内存。Rust 则选择了第三种方式:内存被一个所有权系统管理,它拥有一系列的规则使编译器在编译时进行检查。任何所有权系统的功能都不会导致运行时开销。

    @@ -80,18 +80,18 @@ commit cc053d91f41793e54d5321abe027b0c163d735b8

    栈(Stack)与堆(Heap)

    -

    在很多语言中并不经常需要考虑到栈与堆。不过在像 Rust 这样的系统编程语言中,值是位于栈上还是堆上在更大程度上影响了语言的行为以及为何必须做出特定的选择。我们会在本章的稍后部分描述所有权与堆与栈相关的部分,所以这里只是一个用来预热的简要解释。

    +

    在很多语言中并不经常需要考虑到栈与堆。不过在像 Rust 这样的系统编程语言中,值是位于栈上还是堆上在更大程度上影响了语言的行为以及为何必须做出这样的选择。我们会在本章的稍后部分描述所有权与堆与栈相关的部分,所以这里只是一个用来预热的简要解释。

    栈和堆都是代码在运行时可供使用的内存部分,不过他们以不同的结构组成。栈以放入值的顺序存储并以相反顺序取出值。这也被称作后进先出last in, first out)。想象一下一叠盘子:当增加更多盘子时,把他们放在盘子堆的顶部,当需要盘子时,也从顶部拿走。不能从中间也不能从底部增加或拿走盘子!增加数据叫做进栈pushing onto the stack),而移出数据叫做出栈popping off the stack)。

    操作栈是非常快的,因为它访问数据的方式:永远也不需要寻找一个位置放入新数据或者取出数据因为这个位置总是在栈顶。另一个使得栈快速的性质是栈中的所有数据都必须是一个已知的固定的大小。

    相反对于在编译时未知大小或大小可能变化的数据,可以把他们储存在堆上。堆是缺乏组织的:当向堆放入数据时,我们请求一定大小的空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回给我们一个它位置的指针。这个过程称作在堆上分配内存allocating on the heap),并且有时这个过程就简称为“分配”(allocating)。向栈中放入数据并不被认为是分配。因为指针是已知的固定大小的,我们可以将指针储存在栈上,不过当需要实际数据时,必须访问指针。

    想象一下去餐馆就坐吃饭。当进入时,你说明有几个人,餐馆员工会找到一个够大的空桌子并领你们过去。如果有人来迟了,他们也可以通过询问来找到你们坐在哪。

    访问堆上的数据要比访问栈上的数据要慢因为必须通过指针来访问。现代的处理器在内存中跳转越少就越快。继续类比,假设有一台服务器来处理来自多个桌子的订单。它在处理完一个桌子的所有订单后再移动到下一个桌子是最有效率的。从桌子 A 获取一个订单,接着再从桌子 B 获取一个订单,然后再从桌子 A,然后再从桌子 B 这样的流程会更加缓慢。出于同样原因,处理器在处理的数据之间彼此较近的时候(比如在栈上)比较远的时候(比如可能在堆上)能更好的工作。在堆上分配大量的空间也可能消耗时间。

    当调用一个函数,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。

    -

    记录何处的代码在使用堆上的什么数据,最小化堆上的冗余数据的数量以及清理堆上不再使用的数据以致不至于用完空间,这些所有的问题正是所有权系统要处理的。一旦理解了所有权,你就不需要经常考虑栈和堆了,不过理解如何管理堆内存可以帮助我们理解所有权为什么存在以及为什么以它的方式工作。

    +

    记录何处的代码在使用堆上的什么数据,最小化堆上的冗余数据的数量以及清理堆上不再使用的数据以致不至于耗尽空间,这些所有的问题正是所有权系统要处理的。一旦理解了所有权,你就不需要经常考虑栈和堆了,不过理解如何管理堆内存可以帮助我们理解所有权为何存在以及为什么以这种方式工作。

    所有权规则

    -

    首先,让我们看一下所有权的规则。记住这些规则正如我们将完成一些说明这些规则的例子:

    +

    首先,让我们看一下所有权的规则。请记住这些规则因为我们将讲解一些说明这些规则的例子:

    1. Rust 中的每一个值都有一个叫做它的所有者owner)的变量。
    2. @@ -101,31 +101,28 @@ commit cc053d91f41793e54d5321abe027b0c163d735b8

    变量作用域

    我们在第二章已经完成过一个 Rust 程序的例子了。现在我们已经掌握了基本语法,所以不会在所有的例子中包含fn main() {代码了,所以如果你是一路跟过来的,必须手动将之后例子的代码放入一个main函数中。为此,例子将显得更加具体,使我们可以关注具体细节而不是样板代码。

    -

    作为所有权的第一个例子,我们看看一些变量的作用域scope)。作用域是一个 item 在程序中有效的范围。假如有一个这样的变量:

    +

    作为所有权的第一个例子,我们看看一些变量的作用域scope)。作用域是一个项在程序中有效的范围。假如有一个这样的变量:

    let s = "hello";
     

    变量s绑定到了一个字符串字面值,这个字符串值是硬编码进我们程序代码中的。这个变量从声明的点开始直到当前作用域结束时都是有效的。列表 4-1 的注释标明了变量s在哪里是有效的:

    -
    {                      // s is not valid here, it’s not yet declared
         let s = "hello";   // s is valid from this point forward
     
         // do stuff with s
     }                      // this scope is now over, and s is no longer valid
     
    -
    -

    Listing 4-1: A variable and the scope in which it is valid

    -
    -
    +

    Listing 4-1: A variable and the scope in which it is +valid

    换句话说,这里有两个重要的点:

    1. s进入作用域,它就是有效的。
    2. 这一直持续到它离开作用域为止。
    -

    目前为止,变量是否有效与作用域的关系跟其他变成语言是类似的。现在我们要在此基础上介绍String类型。

    +

    目前为止,变量是否有效与作用域的关系跟其他变成语言是类似的。现在我们在此基础上介绍String类型。

    String类型

    -

    为了演示所有权的规则,我们需要一个比第三章讲到的任何一个都要复杂的数据类型。之前出现的数据类型都是储存在栈上的并且当离开作用域时被移出栈,不过我们需要寻找一个储存在堆上的数据来探索 Rust 如何知道该在何时清理数据。

    -

    这里使用String作为例子并专注于String与所有权相关的部分。这些方面也同样适用于其他标准库提供的或你创建的复杂数据类型。在第八章会更深入地讲解String

    -

    我们已经见过字符串字面值了,它被硬编码进程序里。字符串字面值是很方便,不过他们并不总是适合所有需要使用文本的场景。原因之一就是他们是不可变的。另一个原因是不是所有字符串的值都能在编写代码时就知道:例如,如果想要获取用户输入并储存该怎么办呢?为此,Rust 有第二个字符串类型,String。这个类型储存在堆上所以储存在编译时未知大小的文本。可以用from从字符串字面值来创建String,如下:

    +

    为了演示所有权的规则,我们需要一个比第三章讲到的任何一个都要复杂的数据类型。之前出现的数据类型都是储存在栈上的并且当离开作用域时被移出栈,不过我们需要寻找一个储存在堆上的数据来探索 Rust 如何知道该在何时清理数据的。

    +

    这里使用String作为例子并专注于String与所有权相关的部分。这些方面也同样适用于其他标准库提供的或你自己创建的复杂数据类型。在第八章会更深入地讲解String

    +

    我们已经见过字符串字面值了,它被硬编码进程序里。字符串字面值是很方便的,不过他们并不总是适合所有需要使用文本的场景。原因之一就是他们是不可变的。另一个原因是不是所有字符串的值都能在编写代码时就知道:例如,如果想要获取用户输入并储存该怎么办呢?为此,Rust 有第二个字符串类型,String。这个类型储存在堆上所以储存在编译时未知大小的文本。可以用from从字符串字面值来创建String,如下:

    let s = String::from("hello");
     

    这两个冒号(::)运算符允许将特定的from函数置于String类型的命名空间(namespace)下而不需要使用类似string_from这样的名字。在第五章的“方法语法”(“Method Syntax”)部分会着重讲解这个语法而且在第七章会讲到模块的命名空间。

    @@ -138,7 +135,7 @@ println!("{}", s); // This will print `hello, world!`

    那么这里有什么区别呢?为什么String可变而字面值却不行呢?区别在于两个类型对内存的处理上。

    内存与分配

    -

    字符串字面值的情况,我们在编译时就知道内容所以它直接被硬编码进最终的可执行文件中,这使得字符串字面值快速和高效。不过这些属性都只来源于它的不可变形。不幸的是,我们不能为了每一个在编译时未知大小的文本而将一块内存放入二进制文件中而它的大小还可能随着程序运行而改变。

    +

    对于字符串字面值的情况,我们在编译时就知道内容所以它直接被硬编码进最终的可执行文件中,这使得字符串字面值快速和高效。不过这些属性都只来源于它的不可变形。不幸的是,我们不能为了每一个在编译时未知大小的文本而将一块内存放入二进制文件中而它的大小还可能随着程序运行而改变。

    对于String类型,为了支持一个可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容。这意味着:

    1. 内存必须在运行时向操作系统请求
    2. @@ -146,7 +143,7 @@ println!("{}", s); // This will print `hello, world!`

    第一部分由我们完成:当调用String::from时,它的实现请求它需要的内存。这在编程语言中是非常通用的。

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

    -

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

    +

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

    {
         let s = String::from("hello"); // s is valid from this point forward
     
    @@ -161,14 +158,11 @@ println!("{}", s); // This will print `hello, world!`
     

    这个模式对编写 Rust 代码的方式有着深远的影响。它现在看起来很简单,不过在更复杂的场景下代码的行为可能是不可预测的,比如当有多个变量使用在堆上分配的内存时。现在让我们探索一些这样的场景。

    变量与数据交互:移动

    Rust 中的多个变量以一种独特的方式与同一数据交互。让我们看看列表 4-2 中一个使用整型的例子:

    -
    let x = 5;
     let y = x;
     
    -
    -

    Listing 4-2: Assigning the integer value of variable x to y

    -
    -
    +

    Listing 4-2: Assigning the integer value of variable x +to y

    根据其他语言的经验大致可以猜到这在干什么:“将5绑定到x;接着生成一个值x的拷贝并绑定到y”。现在有了两个变量,xy,都等于5。这也正是事实上发生了的,因为正数是有已知固定大小的简单值,所以这两个5被放入了栈中。

    现在看看这个String版本:

    let s1 = String::from("hello");
    @@ -176,31 +170,19 @@ let s2 = s1;
     

    这看起来与上面的代码非常类似,所以我们可能会假设他们的运行方式也是类似的:也就是说,第二行可能会生成一个s1的拷贝并绑定到s2上。不过,事实上并不完全是这样。

    为了更全面的解释这个问题,让我们看看图 4-3 中String真正是什么样。String由三部分组成,如图左侧所示:一个指向存放字符串内容内存的指针,一个长度,和一个容量。这一组数据储存在栈上。右侧则是堆上存放内容的内存部分。

    -
    -String in memory -
    -

    Figure 4-3: Representation in memory of a String holding the value "hello" -bound to s1

    -
    -
    -

    长度代表当前String的内容使用了多少字节的内存。容量是String从操作系统总共获取了多少字节的内存。长度与容量的区别是很重要的,不过目前为止的场景中并不重要,所以可以暂时忽略容量。

    +

    String in memory

    +

    Figure 4-3: Representation in memory of a String +holding the value "hello" bound to s1

    +

    长度代表当前String的内容使用了多少字节的内存。容量是String从操作系统总共获取了多少字节的内存。长度与容量的区别是很重要的,不过这在目前为止的场景中并不重要,所以可以暂时忽略容量。

    当我们把s1赋值给s2String的数据被复制了,这意味着我们从栈上拷贝了它的指针、长度和容量。我们并没有复制堆上指针所指向的数据。换句话说,内存中数据的表现如图 4-4 所示。

    -
    -s1 and s2 pointing to the same value -
    -

    Figure 4-4: Representation in memory of the variable s2 that has a copy of -the pointer, length, and capacity of s1

    -
    -
    +

    s1 and s2 pointing to the same value

    +

    Figure 4-4: Representation in memory of the variable s2 +that has a copy of the pointer, length, and capacity of s1

    这个表现形式看起来并不像图 4-5 中的那样,它是如果 Rust 也拷贝了堆上的数据后内存看起来是怎么样的。如果 Rust 这么做了,那么操作s2 = s1在堆上数据比较大的时候可能会对运行时性能造成非常大的影响。

    -
    -s1 and s2 to two places -
    -

    Figure 4-5: Another possibility of what s2 = s1 might do if Rust copied the -heap data as well

    -
    -
    -

    之前,我们提到过当变量离开作用域后 Rust 自动调用drop函数并清理变量的堆内存。不过图 4-4 展示了两个数据指针指向了同一位置。这就有了一个问题:当s2s1离开作用域,他们都会尝试释放相同的内存。这是一个叫做 double free 的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致安全漏洞。

    +

    s1 and s2 to two places

    +

    Figure 4-5: Another possibility of what s2 = s1 might +do if Rust copied the heap data as well

    +

    之前,我们提到过当变量离开作用域后 Rust 自动调用drop函数并清理变量的堆内存。不过图 4-4 展示了两个数据指针指向了同一位置。这就有了一个问题:当s2s1离开作用域,他们都会尝试释放相同的内存。这是一个叫做 double free 的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。

    为了确保内存安全,这种场景下 Rust 的处理有另一个细节值得注意。与其尝试拷贝被分配的内存,Rust 则认为s1不再有效,因此 Rust 不需要在s1离开作用域后清理任何东西。看看在s2被创建之后尝试使用s1会发生生么:

    let s1 = String::from("hello");
     let s2 = s1;
    @@ -220,12 +202,9 @@ println!("{}", s1);
     which does not implement the `Copy` trait
     

    如果你在其他语言中听说过术语“浅拷贝”(“shallow copy”)和“深拷贝”(“deep copy”),那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效化了,这个操作被成为移动move),而不是浅拷贝。上面的例子可以解读为s1移动到了s2中。那么具体发生了什么如图 4-6 所示。

    -
    -s1 moved to s2 -
    -

    Figure 4-6: Representation in memory after s1 has been invalidated

    -
    -
    +

    s1 moved to s2

    +

    Figure 4-6: Representation in memory after s1 has been +invalidated

    这样就解决了我们的麻烦!因为只有s2是有效的,当其离开作用域,它就释放自己的内存,完毕。

    另外,这里还隐含了一个设计选择:Rust 永远也不会自动创建数据的“深拷贝”。因此,任何自动的复制可以被认为对运行时性能影响较小。

    变量与数据交互:克隆

    @@ -247,7 +226,7 @@ println!("x = {}, y = {}", x, y);

    他们似乎与我们刚刚学到的内容相抵触:没有调用clone,不过x依然有效且没有被移动到y中。

    原因是像整型这样的在编译时已知大小的类型被整个储存在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量y后使x无效。换句话说,这里没有深浅拷贝的区别,所以调用clone并不会与通常的浅拷贝有什么不同,我们可以不用管它。

    -

    Rust 有一个叫做Copy trait 的特殊注解,可以用在类似整型这样的储存在栈上的类型(第十章详细讲解 trait)。如果一个类型拥有Copy trait,一个旧的变量在(重新)赋值后仍然可用。Rust 不允许自身或其任何部分实现了Drop trait 的类型使用Copy trait。如果我们对其值离开作用域时需要特殊处理的类型使用Copy注解,将会出现一个编译时错误。

    +

    Rust 有一个叫做Copy trait 的特殊注解,可以用在类似整型这样的储存在栈上的类型(第十章详细讲解 trait)。如果一个类型拥有Copy trait,一个旧的变量在(重新)赋值后仍然可用。Rust 不允许自身或其任何部分实现了Drop trait 的类型使用Copy trait。如果我们对其值离开作用域时需要特殊处理的类型使用Copy注解,将会出现一个编译时错误。关于如何为你的类型增加Copy注解,请阅读附录 C 中的 Derivable Trait。

    那么什么类型是Copy的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则,任何简单标量值的组合可以是Copy的,任何不需要分配内存或类似形式资源的类型是Copy的,如下是一些Copy的类型:

    • 所有整数类型,比如u32
    • @@ -257,8 +236,7 @@ println!("x = {}, y = {}", x, y);

    所有权与函数

    将值传递给函数在语言上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。列表 4-7 是一个带有变量何时进入和离开作用域标注的例子:

    -
    -Filename: src/main.rs +

    Filename: src/main.rs

    fn main() {
         let s = String::from("hello");  // s comes into scope.
     
    @@ -282,11 +260,9 @@ fn makes_copy(some_integer: i32) { // some_integer comes into scope.
         println!("{}", some_integer);
     } // Here, some_integer goes out of scope. Nothing special happens.
     
    -
    -

    Listing 4-7: Functions with ownership and scope annotated

    -
    -
    -

    当尝试在调用takes_ownership后使用s时,Rust 会抛出一个编译时错误。这些静态检查使我们免于犯错。试试在main函数中添加使用sx的代码来看看哪里能使用他们,和哪里所有权规则会阻止我们这么做。

    +

    Listing 4-7: Functions with ownership and scope +annotated

    +

    当尝试在调用takes_ownership后使用s时,Rust 会抛出一个编译时错误。这些静态检查使我们免于犯错。试试在main函数中添加使用sx的代码来看看哪里能使用他们,以及哪里所有权规则会阻止我们这么做。

    返回值与作用域

    返回值也可以转移作用域。这里是一个有与列表 4-7 中类似标注的例子:

    Filename: src/main.rs

    diff --git a/docs/ch04-02-references-and-borrowing.html b/docs/ch04-02-references-and-borrowing.html index ac0a3f4..e5abf83 100644 --- a/docs/ch04-02-references-and-borrowing.html +++ b/docs/ch04-02-references-and-borrowing.html @@ -69,9 +69,9 @@

    引用与借用

    -

    ch04-02-references-and-borrowing.md +

    ch04-02-references-and-borrowing.md
    -commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c

    +commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

    在上一部分的结尾处的使用元组的代码是有问题的,我们需要将String返回给调用者函数这样就可以在调用calculate_length后仍然可以使用String了,因为String先被移动到了calculate_length

    下面是如何定义并使用一个(新的)calculate_length函数,它以一个对象的引用作为参数而不是获取值的所有权:

    @@ -90,12 +90,8 @@ fn calculate_length(s: &String) -> usize {

    首先,注意变量声明和函数返回值中的所有元组代码都消失了。其次,注意我们传递&s1calculate_length,同时在函数定义中,我们获取&String而不是String

    这些 & 符号就是引用,他们允许你使用值但不获取它的所有权。图 4-8 展示了一个图解。

    -
    -&String s pointing at String s1 -
    -

    Figure 4-8: &String s pointing at String s1

    -
    -
    +

    &String s pointing at String s1

    +

    Figure 4-8: &String s pointing at String s1

    仔细看看这个函数调用:

    # fn calculate_length(s: &String) -> usize {
     #     s.len()
    @@ -114,8 +110,7 @@ let len = calculate_length(&s1);
     

    变量s有效的作用域与函数参数的作用域一样,不过当引用离开作用域后并不丢弃它指向的数据因为我们没有所有权。函数使用引用而不是实际值作为参数意味着无需返回值来交还所有权,因为就不曾拥有它。

    我们将获取引用作为函数参数称为借用borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从它哪里借来。当你使用完毕,必须还回去。

    那么如果我们尝试修改借用的变量呢?尝试列表 4-9 中的代码。剧透:这行不通!

    -
    -Filename: src/main.rs +

    Filename: src/main.rs

    fn main() {
         let s = String::from("hello");
     
    @@ -126,12 +121,9 @@ fn change(some_string: &String) {
         some_string.push_str(", world");
     }
     
    -
    -

    Listing 4-9: Attempting to modify a borrowed value

    -
    -
    +

    Listing 4-9: Attempting to modify a borrowed value

    这里是错误:

    -
    error: cannot borrow immutable borrowed content `*some_string` as mutable
    +
    error: cannot borrow immutable borrowed content `*some_string` as mutable
      --> error.rs:8:5
       |
     8 |     some_string.push_str(", world");
    @@ -160,7 +152,7 @@ let r1 = &mut s;
     let r2 = &mut s;
     

    具体错误如下:

    -
    error[E0499]: cannot borrow `s` as mutable more than once at a time
    +
    error[E0499]: cannot borrow `s` as mutable more than once at a time
      --> borrow_twice.rs:5:19
       |
     4 |     let r1 = &mut s;
    @@ -178,7 +170,7 @@ let r2 = &mut s;
     
  • 没有被用来同步数据访问的机制。
  • 数据竞争会导致未定义行为并且当在运行时尝试追踪时可能会变得难以诊断和修复;Rust 阻止了这种情况的发生,因为存在数据竞争的代码根本就不能编译!

    -

    一如既往,使用大括号来创建一个新的作用域,允许拥有多个可变引用,只是不能同时拥有:

    +

    一如既往,可以使用大括号来创建一个新的作用域来允许拥有多个可变引用,只是不能同时拥有:

    let mut s = String::from("hello");
     
     {
    @@ -196,7 +188,7 @@ let r2 = &s; // no problem
     let r3 = &mut s; // BIG PROBLEM
     

    错误如下:

    -
    error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
    +
    error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
     immutable
      --> borrow_thrice.rs:6:19
       |
    @@ -266,12 +258,12 @@ for it to be borrowed from.
     
     
    • 一个可变引用。
    • -
    • 任意属性的不可变引用。
    • +
    • 任意数量的不可变引用。
    1. 引用必须总是有效的。
    -

    接下来,我们来看看一种不同类型的引用:slices。

    +

    接下来,我们来看看一种不同类型的引用:slice。

    diff --git a/docs/ch04-03-slices.html b/docs/ch04-03-slices.html index 968fe2b..0a061b8 100644 --- a/docs/ch04-03-slices.html +++ b/docs/ch04-03-slices.html @@ -69,9 +69,9 @@

    Slices

    -

    ch04-03-slices.md +

    ch04-03-slices.md
    -commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c

    +commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

    另一个没有所有权的数据类型是 slice。slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。

    这里有一个小的编程问题:编写一个获取一个字符串并返回它在其中找到的第一个单词的函数。如果函数没有在字符串中找到一个空格,就意味着整个字符串是一个单词,所以整个字符串都应该返回。

    @@ -79,8 +79,7 @@ commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c

    fn first_word(s: &String) -> ?
     

    first_word这个函数有一个参数&String。因为我们不需要所有权,所以这没有问题。不过应该返回什么呢?我们并没有一个真正获取部分字符串的办法。不过,我们可以返回单词结尾的索引。让我们试试如列表 4-10 所示的代码:

    -
    -Filename: src/main.rs +

    Filename: src/main.rs

    fn first_word(s: &String) -> usize {
         let bytes = s.as_bytes();
     
    @@ -93,18 +92,15 @@ commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c

    s.len() }
    -
    -

    Listing 4-10: The first_word function that returns a byte index value into -the String parameter

    -
    -
    +

    Listing 4-10: The first_word function that returns a +byte index value into the String parameter

    让我们将代码分解成小块。因为需要一个元素一个元素的检查String中的值是否是空格,需要用as_bytes方法将String转化为字节数组:

    let bytes = s.as_bytes();
     
    -

    Next, we create an iterator over the array of bytes using the iter method :

    +

    接下来,使用iter方法在字节数据上创建一个迭代器:

    for (i, &item) in bytes.iter().enumerate() {
     
    -

    第十六章将讨论迭代器的更多细节。现在,只需知道iter方法返回集合中的每一个元素,而enumerate包装iter的结果并返回一个元组,其中每一个元素是元组的一部分。返回元组的第一个元素是索引,第二个元素是集合中元素的引用。这比我们自己计算索引要方便一些。

    +

    第十三章将讨论迭代器的更多细节。现在,只需知道iter方法返回集合中的每一个元素,而enumerate包装iter的结果并返回一个元组,其中每一个元素是元组的一部分。返回元组的第一个元素是索引,第二个元素是集合中元素的引用。这比我们自己计算索引要方便一些。

    因为enumerate方法返回一个元组,我们可以使用模式来解构它,就像 Rust 中其他地方一样。所以在for循环中,我们指定了一个模式,其中i是元组中的索引而&item是单个字节。因为从.iter().enumerate()中获取了集合元素的引用,我们在模式中使用了&

    我们通过字节的字面值来寻找代表空格的字节。如果找到了,返回它的位置。否则,使用s.len()返回字符串的长度:

        if item == b' ' {
    @@ -114,8 +110,7 @@ the String parameter

    s.len()

    现在有了一个找到字符串中第一个单词结尾索引的方法了,不过这有一个问题。我们返回了单单一个usize,不过它只在&String的上下文中才是一个有意义的数字。换句话说,因为它是一个与String像分离的值,无法保证将来它仍然有效。考虑一下列表 4-11 中使用了列表 4-10 first_word函数的程序:

    -
    -Filename: src/main.rs +

    Filename: src/main.rs

    # fn first_word(s: &String) -> usize {
     #     let bytes = s.as_bytes();
     #
    @@ -139,11 +134,8 @@ fn main() {
         // we could meaningfully use the value 5 with. word is now totally invalid!
     }
     
    -
    -

    Listing 4-11: Storing the result from calling the first_word function then -changing the String contents

    -
    -
    +

    Listing 4-11: Storing the result from calling the +first_word function then changing the String contents

    这个程序编译时没有任何错误,而且在调用s.clear()之后使用word也不会出错。这时words状态就没有联系了,所以word仍然包含值5。可以尝试用值5来提取变量s的第一个单词,不过这是有 bug 的,因为在我们将5保存到word之后s的内容已经改变。

    不得不担心word的索引与s中的数据不再同步是乏味且容易出错的!如果编写一个second_word函数的话管理索引将更加容易出问题。它的签名看起来像这样:

    fn second_word(s: &String) -> (usize, usize) {
    @@ -160,12 +152,9 @@ let world = &s[6..11];
     

    这类似于获取整个String的引用不过带有额外的[0..5]部分。不同于整个String的引用,这是一个包含String内部的一个位置和所需元素数量的引用。

    我们使用一个 range [starting_index..ending_index]来创建 slice,不过 slice 的数据结构实际上储存了开始位置和 slice 的长度。所以就let world = &s[6..11];来说,world将是一个包含指向s第 6 个字节的指针和长度值 5 的 slice。

    图 4-12 展示了一个图例

    -
    -world containing a pointer to the 6th byte of String s and a length 5 -
    -

    Figure 4-12: String slice referring to part of a String

    -
    -
    +

    world containing a pointer to the 6th byte of String s and a length 5

    +

    Figure 4-12: String slice referring to part of a +String

    对于 Rust 的.. range 语法,如果想要从第一个索引(0)开始,可以不写两个点号之前的值。换句话说,如下两个语句是相同的:

    let s = String::from("hello");
     
    diff --git a/docs/index.html b/docs/index.html
    index 3e311bd..c7abfb0 100644
    --- a/docs/index.html
    +++ b/docs/index.html
    @@ -72,12 +72,15 @@
     
    commit 4f2dc564851dc04b271a2260c834643dfd86c724

    -

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

    -

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

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

    +

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

    -

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

    +

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

    为本书做出贡献

    本书是开源的。如果你发现任何错误,请不要犹豫,在 GitHub 上发起 issue 或提交 pull request。

    +
    +

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

    +
    diff --git a/docs/print.html b/docs/print.html index 5a43c7c..e118f70 100644 --- a/docs/print.html +++ b/docs/print.html @@ -73,65 +73,68 @@
    commit 4f2dc564851dc04b271a2260c834643dfd86c724

    -

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

    -

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

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

    +

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

    -

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

    +

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

    为本书做出贡献

    本书是开源的。如果你发现任何错误,请不要犹豫,在 GitHub 上发起 issue 或提交 pull request。

    +
    +

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

    +

    安装

    ch01-01-installation.md
    commit 4f2dc564851dc04b271a2260c834643dfd86c724

    -

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

    +

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

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

    在 Linux 或 Mac 上安装

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

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

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

    -
    Rust is installed now. Great!
    +
    Rust is installed now. Great!
     

    当然,如果你不赞成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 安装页面获取其他选择。

    卸载

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

    -
    $ rustup self uninstall
    +
    $ rustup self uninstall
     

    故障排除

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

    -
    $ rustc --version
    +
    $ rustc --version
     

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

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

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

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

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

    -

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

    +

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

    本地文档

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

    -

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

    +

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

    Hello, World!

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

    -

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

    +

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

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

    创建项目文件夹

    -

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

    +

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

    Linux 和 Mac:

    -
    $ mkdir ~/projects
    +
    $ mkdir ~/projects
     $ cd ~/projects
     $ mkdir hello_world
     $ cd hello_world
    @@ -143,7 +146,7 @@ $ cd hello_world
     > cd hello_world
     

    编写并运行 Rust 程序

    -

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

    +

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

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

    Filename: main.rs

    fn main() {
    @@ -151,33 +154,33 @@ $ cd hello_world
     }
     

    保存文件,并回到终端窗口。在 Linux 或 OSX 上,输入如下命令:

    -
    $ rustc main.rs
    +
    $ rustc main.rs
     $ ./main
     Hello, world!
     

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

    分析 Rust 程序

    -

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

    +

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

    fn main() {
     
     }
     
    -

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

    +

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

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

    main()函数中:

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

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

    -

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

    +

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

    +

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

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

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

    编译和运行是两个步骤

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

    -

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

    -
    $ rustc main.rs
    +

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

    +
    $ rustc main.rs
     
    -

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

    -
    $ ls
    +

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

    +
    $ ls
     main  main.rs
     

    在 Windows 上,输入:

    @@ -186,32 +189,32 @@ main.exe main.rs

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

    -
    $ ./main  # or .\main.exe on Windows
    +
    $ ./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 程序。

    Hello, Cargo!

    -

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

    -

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

    +

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

    +

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

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

    -
    $ cargo --version
    +
    $ cargo --version
     
    -

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

    +

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

    使用 Cargo 创建项目

    -

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

    +

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

    Linux 和 Mac:

    -
    $ cd ~/projects
    +
    $ cd ~/projects
     

    Windows:

    > cd %USERPROFILE%\projects
     

    并在任何操作系统运行:

    -
    $ cargo new hello_cargo --bin
    +
    $ 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参数。

    +

    我们向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]
    @@ -222,9 +225,10 @@ authors = ["Your Name <you@example.com>"]
     [dependencies]
     

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

    -

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

    -

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

    +

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

    +

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

    现在看看 src/main.rs

    +

    Filename: src/main.rs

    fn main() {
         println!("Hello, world!");
     }
    @@ -234,19 +238,19 @@ authors = ["Your Name <you@example.com>"]
     
  • 代码位于 src 目录
  • 项目根目录包含一个 Cargo.toml 配置文件
  • -

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

    -

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

    +

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

    +

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

    构建并运行 Cargo 项目

    -

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

    +

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

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

    这应该创建 target/debug/hello_cargo(或者在 Windows 上是 target\debug\hello_cargo.exe)可执行文件,可以通过这个命令运行:

    -
    $ ./target/debug/hello_cargo # or .\target\debug\hello_cargo.exe on Windows
    +
    $ ./target/debug/hello_cargo # or .\target\debug\hello_cargo.exe on Windows
     Hello, world!
     

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

    -

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

    +

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

    Filename: Cargo.lock

    [root]
     name = "hello_cargo"
    @@ -254,29 +258,29 @@ version = "0.1.0"
     

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

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

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

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

    -
    $ cargo run
    +
    $ 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 会将它放到 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 build,然后一切将有序运行。即便这个项目很简单,现在它使用了很多接下来你 Rust 程序生涯将会用到的实用工具。事实上,无形中你可以使用下面的命令开始所有你想要从事的项目:

    -
    $ git clone someurl.com/someproject
    +

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

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

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

    @@ -285,17 +289,17 @@ $ carg

    ch02-00-guessing-game-tutorial.md
    -commit 7c1c935560190fcd64c0851e75dbeabf75fedd19

    +commit e6d6caab41471f7115a621029bd428a812c5260e

    -

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

    -

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

    +

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

    +

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

    准备一个新项目

    -

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

    -
    $ cargo new guessing_game --bin
    +

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

    +
    $ cargo new guessing_game --bin
     $ cd guessing_game
     

    第一个命令,cargo new,获取项目的名称(guessing_game)作为第一个参数。--bin参数告诉 Cargo 创建一个二进制项目,与第一章类似。第二个命令进入到新创建的项目目录。

    -

    看一样生成的 Cargo.toml 文件:

    +

    看看生成的 Cargo.toml 文件:

    Filename: Cargo.toml

    [package]
     name = "guessing_game"
    @@ -304,7 +308,7 @@ authors = ["Your Name <you@example.com>"]
     
     [dependencies]
     
    -

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

    +

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

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

    Filename: src/main.rs

    fn main() {
    @@ -317,12 +321,11 @@ authors = ["Your Name <you@example.com>"]
          Running `target/debug/guessing_game`
     Hello, world!
     
    -

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

    +

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

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

    处理一次猜测

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

    -
    -Filename: src/main.rs +

    Filename: src/main.rs

    use std::io;
     
     fn main() {
    @@ -338,14 +341,11 @@ fn main() {
         println!("You guessed: {}", guess);
     }
     
    -
    -

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

    -
    -
    +

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

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

    use std::io;
     
    -

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

    +

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

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

    fn main() {
     
    @@ -370,7 +370,7 @@ let mut bar = 5; // mutable

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

    -

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

    +

    现在我们知道了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新的、空的实例上。哟!

    @@ -378,11 +378,11 @@ let mut bar = 5; // mutable
    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

    +

    如果我们在程序的开头没有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来使其可变。

    -

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

    +

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

    +

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

    .expect("Failed to read line");
     

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

    @@ -390,19 +390,19 @@ let mut bar = 5; // mutable

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

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

    -

    之前提到过,read_line将用户输入放入到传递给它字符串中,不过它也返回一个值————一个io::Result。Rust 标准库中有很多叫做Result的类型。一个Result泛型以及对应子模块的特定版本,比如io::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中的值并原原本本的返回给你,这样就可以使用它了。在本例中,返回值是用户输入到标准输入的一些字符。

    +

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

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

    -
    $ cargo build
    +
    $ 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,
     #[warn(unused_must_use)] on by default
     src/main.rs:10     io::stdin().read_line(&mut guess);
                        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     
    -

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

    +

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

    使用println!占位符打印值

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

    println!("You guessed: {}", guess);
    @@ -427,8 +427,8 @@ You guessed: 6
     

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

    生成一个秘密数字

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

    -

    使用 crate 来增加更多功能

    -

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

    +

    使用 crate 来增加更多功能

    +

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

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

    Filename: Cargo.toml

    [dependencies]
    @@ -436,9 +436,8 @@ You guessed: 6
     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 build
    +

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

    +
    $ cargo build
         Updating registry `https://github.com/rust-lang/crates.io-index`
      Downloading rand v0.3.14
      Downloading libc v0.2.14
    @@ -446,21 +445,18 @@ rand = "0.3.14"
        Compiling rand v0.3.14
        Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
     
    -
    -

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

    -
    -
    +

    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 从 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 build
    +
    $ cargo build
        Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
     

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

    -

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

    -

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

    +

    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 文件。我们将会在这个文件编写全部的代码。

    更新 crate 到一个新版本

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

    @@ -469,7 +465,7 @@ as a dependency

  • 如果成功了,Cargo 会把这些版本写入 Cargo.lock 文件。
  • 不过,Cargo 默认只会寻找大于0.3.0而小于0.4.0的版本。如果rand crate 发布了两个新版本,0.3.150.4.0,在运行cargo update时会出现如下内容:

    -
    $ cargo update
    +
    $ cargo update
         Updating registry `https://github.com/rust-lang/crates.io-index`
         Updating rand v0.3.14 -> v0.3.15
     
    @@ -482,9 +478,8 @@ rand = "0.4.0"

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

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

    生成一个随机数

    -

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

    -
    -Filename: src/main.rs +

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

    +

    Filename: src/main.rs

    extern crate rand;
     
     use std::io;
    @@ -507,17 +502,15 @@ fn main() {
         println!("You guessed: {}", guess);
     }
     
    -
    -

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

    -
    -
    +

    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

    +

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

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

    尝试运行程序几次:

    -
    $ cargo run
    +
    $ cargo run
        Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
          Running `target/debug/guessing_game`
     Guess the number!
    @@ -535,9 +528,8 @@ You guessed: 5
     

    你应该能得到不同的随机数,同时他们应该都是在 1 和 100 之间的。干得漂亮!

    比较猜测与秘密数字

    -

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

    -
    -Filename: src/main.rs +

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

    +

    Filename: src/main.rs

    extern crate rand;
     
     use std::io;
    @@ -567,10 +559,8 @@ fn main() {
         }
     }
     
    -
    -

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

    -
    -
    +

    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) {
    @@ -580,10 +570,10 @@ fn main() {
     }
     

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

    -

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

    +

    一个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表达式就此终止,因为在这个特定场景下没有检查最后一个分支的必要。

    -

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

    -
    $ cargo build
    +

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

    +
    $ cargo build
        Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
     error[E0308]: mismatched types
       --> src/main.rs:23:21
    @@ -597,7 +587,7 @@ 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 不会比较字符串类型和数字类型。

    +

    错误的核心表明这里有不匹配的类型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函数体中增加如下两行代码来实现:

    Filename: src/main.rs

    extern crate rand;
    @@ -638,10 +628,10 @@ fn main() {
     

    这里创建了一个叫做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类型,非常像之前在 XX 页“使用Result类型来处理潜在的错误”部分讨论的read_line方法。这里再次类似的使用expect方法处理这个Result类型。如果parse因为不能从字符串生成一个数字而返回一个ErrResult成员时,expect会使游戏崩溃并打印提供给它的信息。如果parse能成功地将字符串转换为一个数字,它会返回ResultOk成员,同时expect会返回Ok中我们需要的数字。

    +

    字符串的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中我们需要的数字。

    现在让我们运行程序!

    -
    $ cargo run
    +
    $ cargo run
        Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
          Running `target/guessing_game`
     Guess the number!
    @@ -691,8 +681,8 @@ fn main() {
     }
     

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

    -

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

    -
    $ cargo run
    +

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

    +
    $ cargo run
        Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
          Running `target/guessing_game`
     Guess the number!
    @@ -715,7 +705,7 @@ 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来在用户胜利时退出游戏:

    Filename: src/main.rs

    @@ -768,7 +758,7 @@ fn main() {

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

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

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

    -
    $ cargo run
    +
    $ cargo run
        Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
          Running `target/guessing_game`
     Guess the number!
    @@ -789,8 +779,7 @@ You guessed: 61
     You win!
     

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

    -
    -Filename: src/main.rs +

    Filename: src/main.rs

    extern crate rand;
     
     use std::io;
    @@ -828,34 +817,30 @@ fn main() {
         }
     }
     
    -
    -

    Listing 2-5: Complete code of the guessing game

    -
    -
    -

    总结一下,

    +

    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 +

    ch03-00-common-programming-concepts.md
    -commit 2067b6e2bff990bceb39ae8f35780bd3bed08644

    +commit 04aa3a45eb72855b34213703718f50a12a3eeec8

    -

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

    -

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

    - +

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

    +

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

    关键字

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

    -

    变量和可变性

    +

    变量和可变性

    -

    ch03-01-variables-and-mutability.md +

    ch03-01-variables-and-mutability.md
    -commit b0fab378c9c6a817d4f0080d7001d085017cdef8

    +commit 04aa3a45eb72855b34213703718f50a12a3eeec8

    -

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

    -

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

    +

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

    +

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

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

    Filename: src/main.rs

    fn main() {
    @@ -866,7 +851,7 @@ commit b0fab378c9c6a817d4f0080d7001d085017cdef8

    }

    保存并使用cargo run运行程序。应该会看到一个错误信息,如下输出所示:

    -
    $ cargo run
    +
    $ cargo run
        Compiling variables v0.0.1 (file:///projects/variables)
     error[E0384]: re-assignment of immutable variable `x`
      --> src/main.rs:4:5
    @@ -878,7 +863,7 @@ error[E0384]: re-assignment of immutable variable `x`
       |     ^^^^^ re-assignment of immutable variable
     

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

    -

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

    +

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

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

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

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

    @@ -891,22 +876,26 @@ error[E0384]: re-assignment of immutable variable `x` }

    当运行这个程序,出现如下:

    -
    $ cargo run
    +
    $ cargo run
        Compiling variables v0.1.0 (file:///projects/variables)
          Running `target/debug/variables`
     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关键字声明,而且必须注明值的类型。现在我们准备在下一部分,“数据类型”,涉及到类型和类型注解,所以现在无需担心这些细节。常量可以在任何作用域声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。最后一个区别是常量只能用于常量表达式,而不能作为函数调用的结果或任何其他只在运行时使用到的值。

    -

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

    +

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

    +

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

    +

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

    +

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

    +

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

    +

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

    const MAX_POINTS: u32 = 100_000;
     
    -

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

    -

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

    -

    覆盖

    +

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

    +

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

    +

    覆盖(Shadowing)

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

    Filename: src/main.rs

    fn main() {
    @@ -920,7 +909,7 @@ The value of x is: 6
     }
     

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

    -
    $ cargo run
    +
    $ cargo run
        Compiling variables v0.1.0 (file:///projects/variables)
          Running `target/debug/variables`
     The value of x is: 12
    @@ -935,7 +924,7 @@ let spaces = spaces.len();
     spaces = spaces.len();
     

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

    -
    error[E0308]: mismatched types
    +
    error[E0308]: mismatched types
      --> src/main.rs:3:14
       |
     3 |     spaces = spaces.len();
    @@ -944,23 +933,23 @@ spaces = spaces.len();
       = note: expected type `&str`
       = note:    found type `usize`
     
    -

    现在我们探索了变量如何工作,让我们看看他们能有多少数据类型。

    +

    现在我们探索了变量如何工作,让我们看看更多的数据类型。

    数据类型

    -

    ch03-02-data-types.md +

    ch03-02-data-types.md
    -commit 6436ebee2a84820adf77231cead6b5691c8e2744

    +commit 04aa3a45eb72855b34213703718f50a12a3eeec8

    -

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

    +

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

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

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

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

    -
    error[E0282]: unable to infer enough type information about `_`
    - --> src/main.rs:2:5
    +
    error[E0282]: unable to infer enough type information about `_`
    + --> src/main.rs:2:9
       |
    -2 | let guess = "42".parse().expect("Not a number!");
    -  |     ^^^^^ cannot infer type for `_`
    +2 |     let guess = "42".parse().expect("Not a number!");
    +  |         ^^^^^ cannot infer type for `_`
       |
       = note: type annotations or generic parameter binding required
     
    @@ -969,10 +958,7 @@ commit 6436ebee2a84820adf77231cead6b5691c8e2744

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

    整型

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

    -
    -
    -

    Table 3-1: Integer Types in Rust

    -
    +

    Table 3-1: Integer Types in Rust

    @@ -980,15 +966,11 @@ commit 6436ebee2a84820adf77231cead6b5691c8e2744

    Length Signed Unsigned
    8-bit i8 u8
    16-bit i16 u16
    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位的。

    -
    -
    -

    Table 3-2: Integer Literals in Rust

    -
    +

    Table 3-2: Integer Literals in Rust

    @@ -996,10 +978,9 @@ commit 6436ebee2a84820adf77231cead6b5691c8e2744

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

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

    浮点型

    -

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

    +

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

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

    Filename: src/main.rs

    fn main() {
    @@ -1071,7 +1052,7 @@ commit 6436ebee2a84820adf77231cead6b5691c8e2744

    }

    程序首先创建了一个元组并绑定到tup变量上。接着使用了let和一个模式将tup分成了三个不同的变量,xyz。这叫做解构destructuring),因为它将一个元组拆成了三个部分。最后,程序打印出了y的值,也就是6.4

    -

    除了使用模式匹配解构之外,也可以使用点号(.)后跟值的索引来直接访问。例如:

    +

    除了使用模式匹配解构之外,也可以使用点号(.)后跟值的索引来直接访问他们。例如:

    Filename: src/main.rs

    fn main() {
         let x: (i32, f64, u8) = (500, 6.4, 1);
    @@ -1120,7 +1101,7 @@ commit 6436ebee2a84820adf77231cead6b5691c8e2744

    }

    使用cargo run运行代码后会产生如下结果:

    -
    $ cargo run
    +
    $ cargo run
        Compiling arrays v0.1.0 (file:///projects/arrays)
          Running `target/debug/arrays`
     thread '<main>' panicked at 'index out of bounds: the len is 5 but the index is
    @@ -1132,9 +1113,9 @@ error: Process didn't exit successfully: `target/debug/arrays` (exit code: 101)
     

    这是第一个在实战中遇到的 Rust 安全原则的例子。在很多底层语言中,并没有进行这类检查,这样当提供了一个不正确的索引时,就会访问无效的内存。Rust 通过立即退出而不是允许内存访问并继续执行来使你免受这类错误困扰。第九章会讨论更多 Rust 的错误处理。

    函数如何工作

    -

    ch03-03-how-functions-work.md +

    ch03-03-how-functions-work.md
    -commit 52b7fcbfdd35915cb21e6d492fb6c86764f53b47

    +commit 04aa3a45eb72855b34213703718f50a12a3eeec8

    函数在 Rust 代码中应用广泛。你已经见过一个语言中最重要的函数:main函数,它时很多程序的入口点。你也见过了fn关键字,它用来声明新函数。

    Rust 代码使用 snake case 作为函数和变量名称的规范风格。在 snake case 中,所有字母都是小写并使用下划线分隔单词。这里是一个函数定义程序的例子:

    @@ -1150,9 +1131,9 @@ fn another_function() { }

    Rust 中的函数定义以fn开始并在函数名后跟一对括号。大括号告诉编译器哪里是函数体的开始和结尾。

    -

    可以使用定义过的函数名后跟括号来调用任意函数。因为another_function在程序中已经定义过了,它可以在main函数中被调用。注意,源码中another_functionmain函数之后被定义;也可以在之前定义。Rust 不关心函数定义于何处,只要他们被定义了。

    -

    让我们开始一个叫做functions的新二进制项目来进一步探索函数。将上面的another_function例子写入 src/main.rs 中并运行。你应该会看到如下输出:

    -
    $ cargo run
    +

    可以使用定义过的函数名后跟括号来调用任意函数。因为another_function已经在程序中定义过了,它可以在main函数中被调用。注意,源码中another_functionmain函数之后被定义;也可以在其之前定义。Rust 不关心函数定义于何处,只要他们被定义了。

    +

    让我们开始一个叫做 functions 的新二进制项目来进一步探索函数。将上面的another_function例子写入 src/main.rs 中并运行。你应该会看到如下输出:

    +
    $ cargo run
        Compiling functions v0.1.0 (file:///projects/functions)
          Running `target/debug/functions`
     Hello, world!
    @@ -1160,7 +1141,7 @@ Another function.
     

    代码在main函数中按照他们出现的顺序被执行。首先,打印“Hello, world!”信息,接着another_function被调用并打印它的信息。

    函数参数

    -

    函数也可以被定义为拥有参数parameters),他们是作为函数签名一部分的特殊变量。当函数拥有参数,可以为这些参数提供具体的值。技术上讲,这些具体值被称为参数( arguments),不过通常的习惯是倾向于在函数定义中的变量和调用函数时传递的具体值都可以用 "parameter" 和 "argument" 而不加区别。

    +

    函数也可以被定义为拥有参数parameters),他们是作为函数签名一部分的特殊变量。当函数拥有参数,可以为这些参数提供具体的值。技术上讲,这些具体值被称为参数( arguments),不过通常的习惯是倾向于在函数定义中的变量和调用函数时传递的具体值都可以用“parameter”和“argument”而不加区别。

    如下被重写的another_function版本展示了 Rust 中参数是什么样的:

    Filename: src/main.rs

    fn main() {
    @@ -1190,7 +1171,7 @@ fn another_function(x: i32, y: i32) {
         println!("The value of y is: {}", y);
     }
     
    -

    这个例子创建了一个有两个参数的函数,都是i32类型的。函数打印出了这两个参数的值。注意函数参数并一定都是相同的————这个例子中他们只是碰巧相同。

    +

    这个例子创建了一个有两个参数的函数,都是i32类型的。函数打印出了这两个参数的值。注意函数参数并不一定都是相同类型的,这个例子中他们只是碰巧相同罢了。

    尝试运行代码。使用上面的例子替换当前 function 项目的 src/main.rs 文件,并cargo run运行它:

    $ cargo run
        Compiling functions v0.1.0 (file:///projects/functions)
    @@ -1198,22 +1179,18 @@ fn another_function(x: i32, y: i32) {
     The value of x is: 5
     The value of y is: 6
     
    -

    因为我们使用5作为x的值和6作为y的值来调用函数,这两个字符串使用这些值并被打印出来。

    +

    因为我们使用5作为x的值和6作为y的值来调用函数,这两个字符串和他们的值并被打印出来。

    函数体

    函数体由一系列的语句和一个可选的表达式构成。目前为止,我们只涉及到了没有结尾表达式的函数,不过我们见过表达式作为了语句的一部分。因为 Rust 是一个基于表达式(expression-based)的语言,这是一个需要理解的(不同于其他语言)重要区别。其他语言并没有这样的区别,所以让我们看看语句与表达式有什么区别以及他们是如何影响函数体的。

    语句与表达式

    我们已经用过语句与表达式了。语句Statements)是执行一些操作但不返回值的指令。表达式(Expressions)计算并产生一个值。让我们看看一些例子:

    使用let关键字创建变量并绑定一个值是一个语句。在列表 3-3 中,let y = 6;是一个语句:

    -
    -Filename: src/main.rs +

    Filename: src/main.rs

    fn main() {
         let y = 6;
     }
     
    -
    -

    Listing 3-3: A main function declaration containing one statement.

    -
    -
    +

    Listing 3-3: A main function declaration containing one statement.

    函数定义也是语句;上面整个例子本身就是一个语句。

    语句并不返回值。因此,不能把let语句赋值给另一个变量,比如下面的例子尝试做的:

    Filename: src/main.rs

    @@ -1222,7 +1199,7 @@ The value of y is: 6 }

    当运行这个程序,会得到如下错误:

    -
    $ cargo run
    +
    $ cargo run
        Compiling functions v0.1.0 (file:///projects/functions)
     error: expected expression, found statement (`let`)
      --> src/main.rs:2:14
    @@ -1233,7 +1210,7 @@ error: expected expression, found statement (`let`)
       = note: variable declaration using `let` is a statement
     

    let y = 6语句并不返回值,所以并没有x可以绑定的值。这与其他语言不同,例如 C 和 Ruby,他们的赋值语句返回所赋的值。在这些语言中,可以这么写x = y = 6这样xy的值都是6;这在 Rust 中可不行。

    -

    表达式进行计算而且他们组成了其余大部分 Rust 代码。考虑一个简单的数学运算,比如5 + 6,这是一个表达式并计算出值11。表达式可以是语句的一部分:在列表 3-3 中有这个语句let y = 6;6是一个表达式它计算出的值是6。函数调用是一个表达式。宏调用是一个表达式。我们用来创新建作用域的大括号(代码块),{},也是一个表达式,例如:

    +

    表达式计算出一些值,而且他们组成了其余大部分你将会编写的 Rust 代码。考虑一个简单的数学运算,比如5 + 6,这是一个表达式并计算出值11。表达式可以是语句的一部分:在列表 3-3 中有这个语句let y = 6;6是一个表达式它计算出的值是6。函数调用是一个表达式。宏调用是一个表达式。我们用来创新建作用域的大括号(代码块),{},也是一个表达式,例如:

    Filename: src/main.rs

    fn main() {
         let x = 5;
    @@ -1266,8 +1243,8 @@ fn main() {
         println!("The value of x is: {}", x);
     }
     
    -

    在函数five中并没有函数调用、宏、甚至也没有let语句————只有数字5它子集。这在 Rust 中是一个完全有效的函数。注意函数的返回值类型也被指定了,就是-> i32。尝试运行代码;输出应该看起来像这样:

    -
    $ cargo run
    +

    在函数five中并没有函数调用、宏、甚至也没有let语句————只有数字5它自己。这在 Rust 中是一个完全有效的函数。注意函数的返回值类型也被指定了,就是-> i32。尝试运行代码;输出应该看起来像这样:

    +
    $ cargo run
        Compiling functions v0.1.0 (file:///projects/functions)
          Running `target/debug/functions`
     The value of x is: 5
    @@ -1275,7 +1252,7 @@ The value of x is: 5
     

    函数five的返回值是5,也就是为什么返回值类型是i32。让我们仔细检查一下这段代码。这有两个重要的部分:首先,let x = five();这一行表明我们使用函数的返回值来初始化了一个变量。因为函数five返回5,这一行与如下这行相同:

    let x = 5;
     
    -

    再次,函数five没有参数并定义了返回值类型,不过函数体只有单单一个5也没有分号,因为这是我们想要返回值的表达式。让我们看看另一个例子:

    +

    其次,函数five没有参数并定义了返回值类型,不过函数体只有单单一个5也没有分号,因为这是我们想要返回值的表达式。让我们看看另一个例子:

    Filename: src/main.rs

    fn main() {
         let x = plus_one(5);
    @@ -1288,6 +1265,7 @@ fn plus_one(x: i32) -> i32 {
     }
     

    运行代码会打印出The value of x is: 6。如果在包含x + 1的那一行的结尾加上一个分号,把它从表达式变成语句后会怎样呢?

    +

    Filename: src/main.rs

    fn main() {
         let x = plus_one(5);
     
    @@ -1299,26 +1277,31 @@ fn plus_one(x: i32) -> i32 {
     }
     

    运行代码会产生一个错误,如下:

    -
    error[E0269]: not all control paths return a value
    - --> src/main.rs:7:1
    +
    error[E0308]: mismatched types
    + --> src/main.rs:7:28
       |
    -7 | fn plus_one(x: i32) -> i32 {
    -  | ^
    +7 |   fn plus_one(x: i32) -> i32 {
    +  |  ____________________________^ starting here...
    +8 | |     x + 1;
    +9 | | }
    +  | |_^ ...ending here: expected i32, found ()
       |
    +  = note: expected type `i32`
    +             found type `()`
     help: consider removing this semicolon:
      --> src/main.rs:8:10
       |
     8 |     x + 1;
       |          ^
     
    -

    主要的错误信息,“并非所有控制路径都返回一个值”(“not all control paths return a value,”),揭示了代码的核心问题。函数plus_one的定义说明它要返回一个i32,不过语句并不返回一个值。因此,这个函数没有返回任何值,这与函数定义相矛盾并导致一个错误。在输出中,Rust 提供了一个可能会对修正问题有帮助的信息:它建议去掉分号,这会修复这个错误。

    +

    主要的错误信息,“mismatched types,”(类型不匹配),揭示了代码的核心问题。函数plus_one的定义说明它要返回一个i32,不过语句并不返回一个值,这由那个空元组()表明。因此,这个函数没有返回任何值,这与函数定义相矛盾并导致一个错误。在输出中,Rust 提供了一个可能会对修正问题有帮助的信息:它建议去掉分号,这会修复这个错误。

    注释

    -

    ch03-04-comments.md +

    ch03-04-comments.md
    -commit 74d6fc999b986b74bf94edd6dcbb5a08a16c12de

    +commit 4f2dc564851dc04b271a2260c834643dfd86c724

    -

    所有编程语言都力求使他们的代码易于理解,不过有时额外的解释需要得到保障。在这种情况下,程序员在源码中留下记录,或者注释comments),编译器会忽略他们不过其他阅读代码的人可能会用得上。

    +

    所有编程语言都力求使他们的代码易于理解,不过有时需要提供额外的解释。在这种情况下,程序员在源码中留下记录,或者注释comments),编译器会忽略他们不过其他阅读代码的人可能会用得上。

    这是一个注释的例子:

    // Hello, world.
     
    @@ -1327,13 +1310,13 @@ commit 74d6fc999b986b74bf94edd6dcbb5a08a16c12de

    // multiple lines of comments to do it! Whew! Hopefully, this comment will // explain what’s going on.
    -

    注释也可以在放在包含代码的行的结尾:

    +

    注释也可以在放在包含代码的行的末尾:

    Filename: src/main.rs

    fn main() {
         let lucky_number = 7; // I’m feeling lucky today.
     }
     
    -

    不过你会经常看到他们被以这种格式使用,也就是位于它解释的代码行的上面一行:

    +

    不过你会经常看到他们被以这种格式使用,也就是位于它所解释的代码行的上面一行:

    Filename: src/main.rs

    fn main() {
         // I’m feeling lucky today.
    @@ -1345,7 +1328,7 @@ commit 74d6fc999b986b74bf94edd6dcbb5a08a16c12de

    ch03-05-control-flow.md
    -commit 784a3ec5e8b9c6bff456ab9f0efd4dabcc180dda

    +commit 04aa3a45eb72855b34213703718f50a12a3eeec8

    通过条件是不是真来决定是否某些代码,或者根据条件是否为真来重复运行一段代码是大部分编程语言的基本组成部分。Rust 代码中最常见的用来控制执行流的结构是if表达式和循环。

    if表达式

    @@ -1362,9 +1345,10 @@ commit 784a3ec5e8b9c6bff456ab9f0efd4dabcc180dda

    } }
    +

    所有if表达式以if关键字开头,它后跟一个条件。在这个例子中,条件检查number是否有一个小于 5 的值。在条件为真时希望执行的代码块位于紧跟条件之后的大括号中。if表达式中与条件关联的代码块有时被叫做 arms,就像第二章“比较猜测与秘密数字”部分中讨论到的match表达式中分支一样。也可以包含一个可选的else表达式,这里我们就这么做了,来提供一个在条件为假时应当执行的代码块。如果不提供else表达式并且条件为假时,程序会直接忽略if代码块并继续执行下面的代码。

    尝试运行代码,应该能看到如下输出:

    -
    $ cargo run
    +
    $ cargo run
        Compiling branches v0.1.0 (file:///projects/branches)
          Running `target/debug/branches`
     condition was true
    @@ -1373,7 +1357,7 @@ condition was true
     
    let number = 7;
     

    再次运行程序并查看输出:

    -
    $ cargo run
    +
    $ cargo run
        Compiling branches v0.1.0 (file:///projects/branches)
          Running `target/debug/branches`
     condition was false
    @@ -1389,18 +1373,14 @@ condition was false
     }
     

    这里if条件的值是3,Rust 抛出了一个错误:

    -
       Compiling branches v0.1.0 (file:///projects/branches)
    -error[E0308]: mismatched types
    +
    error[E0308]: mismatched types
      --> src/main.rs:4:8
       |
     4 |     if number {
       |        ^^^^^^ expected bool, found integral variable
       |
       = note: expected type `bool`
    -  = note:    found type `{integer}`
    -
    -error: aborting due to previous error
    -Could not compile `branches`.
    +             found type `{integer}`
     

    这个错误表明 Rust 期望一个bool不过却得到了一个整型。Rust 并不会尝试自动地将非布尔值转换为布尔值,不像例如 Ruby 和 JavaScript 这样的语言。必须总是显式地使用boolean作为if的条件。例如如果想要if代码块只在一个数字不等于0时执行,可以把if表达式修改为如下:

    Filename: src/main.rs

    @@ -1431,17 +1411,16 @@ Could not compile `branches`. }

    这个程序有四个可能的执行路径。运行后应该能看到如下输出:

    -
    $ cargo run
    +
    $ cargo run
        Compiling branches v0.1.0 (file:///projects/branches)
          Running `target/debug/branches`
     number is divisible by 3
     

    当执行这个程序,它按顺序检查每个if表达式并执行第一个条件为真的代码块。注意即使 6 可以被 2 整除,也不会出现number is divisible by 2的输出,更不会出现else块中的number is not divisible by 4, 3, or 2。原因是 Rust 只会执行第一个条件为真的代码块,并且它一旦找到一个以后,就不会检查剩下的条件了。

    -

    使用过多的else if表达式会使代码显得杂乱无章,所以如果有多于一个else if,最好重构代码。为此第六章介绍了 Rust 一个叫做match的强大的分支结构(branching construct)。

    +

    使用过多的else if表达式会使代码显得杂乱无章,所以如果有多于一个else if,最好重构代码。为此第六章会介绍 Rust 中一个叫做match的强大的分支结构(branching construct)。

    let语句中使用if

    -

    因为if是一个表达式,我们可以在let语句的右侧使用它,例如列表 3-4:

    -
    -Filename: src/main.rs +

    因为if是一个表达式,我们可以在let语句的右侧使用它,例如在列表 3-4 中:

    +

    Filename: src/main.rs

    fn main() {
         let condition = true;
         let number = if condition {
    @@ -1453,17 +1432,15 @@ number is divisible by 3
         println!("The value of number is: {}", number);
     }
     
    -
    -

    Listing 3-4: Assigning the result of an if expression to a variable

    -
    -
    +

    Listing 3-4: Assigning the result of an if expression +to a variable

    number变量将会绑定到基于if表达式结果的值。运行这段代码看看会出现什么:

    -
    $ cargo run
    +
    $ cargo run
        Compiling branches v0.1.0 (file:///projects/branches)
          Running `target/debug/branches`
     The value of number is: 5
     
    -

    还记得代码块的值是其最后一个表达式的值,以及数字本身也是一个表达式吗。在这个例子中,整个if表达式的值依赖哪个代码块被执行。这意味着if的每个分支的可能的返回值都必须是相同类型;在列表 3-4 中,if分支和else分支的结果都是i32整型。不过如果像下面的例子一样这些类型并不相同会怎么样呢?

    +

    还记得代码块的值是其最后一个表达式的值,以及数字本身也是一个表达式吗。在这个例子中,整个if表达式的值依赖哪个代码块被执行。这意味着if的每个分支的可能的返回值都必须是相同类型;在列表 3-4 中,if分支和else分支的结果都是i32整型。不过如果像下面的例子那样这些类型并不匹配会怎么样呢?

    Filename: src/main.rs

    fn main() {
         let condition = true;
    @@ -1478,17 +1455,21 @@ The value of number is: 5
     }
     

    当运行这段代码,会得到一个错误。ifelse分支的值类型是不相容的,同时 Rust 也准确地表明了在程序中的何处发现的这个问题:

    -
       Compiling branches v0.1.0 (file:///projects/branches)
    -error[E0308]: if and else have incompatible types
    +
    error[E0308]: if and else have incompatible types
      --> src/main.rs:4:18
       |
    -4 |     let number = if condition {
    -  |                  ^ expected integral variable, found reference
    +4 |       let number = if condition {
    +  |  __________________^ starting here...
    +5 | |         5
    +6 | |     } else {
    +7 | |         "six"
    +8 | |     };
    +  | |_____^ ...ending here: expected integral variable, found reference
       |
       = note: expected type `{integer}`
    -  = note:    found type `&’static str`
    +             found type `&'static str`
     
    -

    if代码块的表达式返回一个整型,而else代码块返回一个字符串。这并不可行因为变量必须只有一个类型。Rust 需要在编译时就确切的知道number变量的类型,这样它就可以在编译时证明其他使用number变量的地方它的类型是有效的。Rust 并不能够在number的类型只能在运行时确定的情况下完成这些功能;这样会使编译器变得更复杂而且只能为代码提供更少的保障,因为它不得不记录所有变量的多种可能的类型。

    +

    if代码块的表达式返回一个整型,而else代码块返回一个字符串。这并不可行,因为变量必须只有一个类型。Rust 需要在编译时就确切的知道number变量的类型,这样它就可以在编译时证明其他使用number变量的地方它的类型是有效的。Rust 并不能够在number的类型只能在运行时确定的情况下工作;这样会使编译器变得更复杂而且只能为代码提供更少的保障,因为它不得不记录所有变量的多种可能的类型。

    使用循环重复执行

    多次执行一段代码是很常用的。为了这个功能,Rust 提供了多种循环loops)。一个循环执行循环体中的代码直到结尾并紧接着从回到开头继续执行。为了实验一下循环,让我们创建一个叫做 loops 的新项目。

    Rust 有三种循环类型:loopwhilefor。让我们每一个都试试。

    @@ -1516,7 +1497,7 @@ again!

    幸运的是,Rust 提供了另一个更可靠的方式来退出循环。可以使用break关键字来告诉程序何时停止执行循环。还记得我们在第二章猜猜看游戏的“猜测正确后退出”部分使用过它来在用户猜对数字赢得游戏后退出程序吗。

    while条件循环

    在程序中计算循环的条件也很常见。当条件为真,执行循环。当条件不再为真,调用break停止循环。这个循环类型可以通过组合loopifelsebreak来实现;如果你喜欢的话,现在就可以在程序中试试。

    -

    然而,这个模式太常见了所以 Rust 为此提供了一个内建的语言结构,它被称为while循环。下面的例子使用了while:程序循环三次,每次数字都减一。接着,在循环之后,打印出另一个信息并退出:

    +

    然而,这个模式太常见了以至于 Rust 为此提供了一个内建的语言结构,它被称为while循环。下面的例子使用了while:程序循环三次,每次数字都减一。接着,在循环之后,打印出另一个信息并退出:

    Filename: src/main.rs

    fn main() {
         let mut number = 3;
    @@ -1533,8 +1514,7 @@ again!
     

    这个结构消除了很多需要嵌套使用loopifelsebreak的代码,这样显得更加清楚。当条件为真就执行,否则退出循环。

    使用for遍历集合

    可以使用while结构来遍历一个元素集合,比如数组。例如:

    -
    -Filename: src/main.rs +

    Filename: src/main.rs

    fn main() {
         let a = [10, 20, 30, 40, 50];
         let mut index = 0;
    @@ -1546,10 +1526,8 @@ again!
         }
     }
     
    -
    -

    Listing 3-5: Looping through each element of a collection using a while loop

    -
    -
    +

    Listing 3-5: Looping through each element of a collection +using a while loop

    这里代码对数组中的元素进行计数。它从索引0开始,并接着循环直到遇到数组的最后一个索引(这时,index < 5不再为真)。运行这段代码会打印出数组中的每一个元素:

    $ cargo run
        Compiling loops v0.1.0 (file:///projects/loops)
    @@ -1600,16 +1578,16 @@ the value is: 50
     

    当你准备好继续的时候,让我们讨论一个其他语言中并不常见的概念:所有权(ownership)。

    认识所有权

    -

    ch04-00-understanding-ownership.md +

    ch04-00-understanding-ownership.md
    -commit 759067b651a48a4a66485fe0876d318d398fb4fe

    +commit 4f2dc564851dc04b271a2260c834643dfd86c724

    -

    所有权(系统)是 Rust 最独特的功能,它令 Rust 可以无需垃圾回收(garbage collector)就能保障内存安全。因此,理解 Rust 中所有权如何工作是十分重要的。本章我们将讲到所有权以及相关功能:借用、slices 以及 Rust 如何在内存中安排数据。

    +

    所有权(系统)是 Rust 最独特的功能,它使得 Rust 可以无需垃圾回收(garbage collector)就能保障内存安全。因此,理解 Rust 中所有权如何工作是十分重要的。本章我们将讲到所有权以及相关功能:借用、slice 以及 Rust 如何在内存中摆放数据。

    什么是所有权

    -

    ch04-01-what-is-ownership.md +

    ch04-01-what-is-ownership.md
    -commit cc053d91f41793e54d5321abe027b0c163d735b8

    +commit fae5fa82d728b5965ecbba84060689430345e509

    Rust 的核心功能(之一)是所有权ownership)。虽然这个功能理解起来很直观,不过它对语言的其余部分有着更深层的含义。

    所有程序都必须管理他们运行时使用计算机内存的方式。一些语言中使用垃圾回收在程序运行过程中来时刻寻找不再被使用的内存;在另一些语言中,程序员必须亲自分配和释放内存。Rust 则选择了第三种方式:内存被一个所有权系统管理,它拥有一系列的规则使编译器在编译时进行检查。任何所有权系统的功能都不会导致运行时开销。

    @@ -1618,18 +1596,18 @@ commit cc053d91f41793e54d5321abe027b0c163d735b8

    栈(Stack)与堆(Heap)

    -

    在很多语言中并不经常需要考虑到栈与堆。不过在像 Rust 这样的系统编程语言中,值是位于栈上还是堆上在更大程度上影响了语言的行为以及为何必须做出特定的选择。我们会在本章的稍后部分描述所有权与堆与栈相关的部分,所以这里只是一个用来预热的简要解释。

    +

    在很多语言中并不经常需要考虑到栈与堆。不过在像 Rust 这样的系统编程语言中,值是位于栈上还是堆上在更大程度上影响了语言的行为以及为何必须做出这样的选择。我们会在本章的稍后部分描述所有权与堆与栈相关的部分,所以这里只是一个用来预热的简要解释。

    栈和堆都是代码在运行时可供使用的内存部分,不过他们以不同的结构组成。栈以放入值的顺序存储并以相反顺序取出值。这也被称作后进先出last in, first out)。想象一下一叠盘子:当增加更多盘子时,把他们放在盘子堆的顶部,当需要盘子时,也从顶部拿走。不能从中间也不能从底部增加或拿走盘子!增加数据叫做进栈pushing onto the stack),而移出数据叫做出栈popping off the stack)。

    操作栈是非常快的,因为它访问数据的方式:永远也不需要寻找一个位置放入新数据或者取出数据因为这个位置总是在栈顶。另一个使得栈快速的性质是栈中的所有数据都必须是一个已知的固定的大小。

    相反对于在编译时未知大小或大小可能变化的数据,可以把他们储存在堆上。堆是缺乏组织的:当向堆放入数据时,我们请求一定大小的空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回给我们一个它位置的指针。这个过程称作在堆上分配内存allocating on the heap),并且有时这个过程就简称为“分配”(allocating)。向栈中放入数据并不被认为是分配。因为指针是已知的固定大小的,我们可以将指针储存在栈上,不过当需要实际数据时,必须访问指针。

    想象一下去餐馆就坐吃饭。当进入时,你说明有几个人,餐馆员工会找到一个够大的空桌子并领你们过去。如果有人来迟了,他们也可以通过询问来找到你们坐在哪。

    访问堆上的数据要比访问栈上的数据要慢因为必须通过指针来访问。现代的处理器在内存中跳转越少就越快。继续类比,假设有一台服务器来处理来自多个桌子的订单。它在处理完一个桌子的所有订单后再移动到下一个桌子是最有效率的。从桌子 A 获取一个订单,接着再从桌子 B 获取一个订单,然后再从桌子 A,然后再从桌子 B 这样的流程会更加缓慢。出于同样原因,处理器在处理的数据之间彼此较近的时候(比如在栈上)比较远的时候(比如可能在堆上)能更好的工作。在堆上分配大量的空间也可能消耗时间。

    当调用一个函数,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。

    -

    记录何处的代码在使用堆上的什么数据,最小化堆上的冗余数据的数量以及清理堆上不再使用的数据以致不至于用完空间,这些所有的问题正是所有权系统要处理的。一旦理解了所有权,你就不需要经常考虑栈和堆了,不过理解如何管理堆内存可以帮助我们理解所有权为什么存在以及为什么以它的方式工作。

    +

    记录何处的代码在使用堆上的什么数据,最小化堆上的冗余数据的数量以及清理堆上不再使用的数据以致不至于耗尽空间,这些所有的问题正是所有权系统要处理的。一旦理解了所有权,你就不需要经常考虑栈和堆了,不过理解如何管理堆内存可以帮助我们理解所有权为何存在以及为什么以这种方式工作。

    所有权规则

    -

    首先,让我们看一下所有权的规则。记住这些规则正如我们将完成一些说明这些规则的例子:

    +

    首先,让我们看一下所有权的规则。请记住这些规则因为我们将讲解一些说明这些规则的例子:

    1. Rust 中的每一个值都有一个叫做它的所有者owner)的变量。
    2. @@ -1639,31 +1617,28 @@ commit cc053d91f41793e54d5321abe027b0c163d735b8

    变量作用域

    我们在第二章已经完成过一个 Rust 程序的例子了。现在我们已经掌握了基本语法,所以不会在所有的例子中包含fn main() {代码了,所以如果你是一路跟过来的,必须手动将之后例子的代码放入一个main函数中。为此,例子将显得更加具体,使我们可以关注具体细节而不是样板代码。

    -

    作为所有权的第一个例子,我们看看一些变量的作用域scope)。作用域是一个 item 在程序中有效的范围。假如有一个这样的变量:

    +

    作为所有权的第一个例子,我们看看一些变量的作用域scope)。作用域是一个项在程序中有效的范围。假如有一个这样的变量:

    let s = "hello";
     

    变量s绑定到了一个字符串字面值,这个字符串值是硬编码进我们程序代码中的。这个变量从声明的点开始直到当前作用域结束时都是有效的。列表 4-1 的注释标明了变量s在哪里是有效的:

    -
    {                      // s is not valid here, it’s not yet declared
         let s = "hello";   // s is valid from this point forward
     
         // do stuff with s
     }                      // this scope is now over, and s is no longer valid
     
    -
    -

    Listing 4-1: A variable and the scope in which it is valid

    -
    -
    +

    Listing 4-1: A variable and the scope in which it is +valid

    换句话说,这里有两个重要的点:

    1. s进入作用域,它就是有效的。
    2. 这一直持续到它离开作用域为止。
    -

    目前为止,变量是否有效与作用域的关系跟其他变成语言是类似的。现在我们要在此基础上介绍String类型。

    +

    目前为止,变量是否有效与作用域的关系跟其他变成语言是类似的。现在我们在此基础上介绍String类型。

    String类型

    -

    为了演示所有权的规则,我们需要一个比第三章讲到的任何一个都要复杂的数据类型。之前出现的数据类型都是储存在栈上的并且当离开作用域时被移出栈,不过我们需要寻找一个储存在堆上的数据来探索 Rust 如何知道该在何时清理数据。

    -

    这里使用String作为例子并专注于String与所有权相关的部分。这些方面也同样适用于其他标准库提供的或你创建的复杂数据类型。在第八章会更深入地讲解String

    -

    我们已经见过字符串字面值了,它被硬编码进程序里。字符串字面值是很方便,不过他们并不总是适合所有需要使用文本的场景。原因之一就是他们是不可变的。另一个原因是不是所有字符串的值都能在编写代码时就知道:例如,如果想要获取用户输入并储存该怎么办呢?为此,Rust 有第二个字符串类型,String。这个类型储存在堆上所以储存在编译时未知大小的文本。可以用from从字符串字面值来创建String,如下:

    +

    为了演示所有权的规则,我们需要一个比第三章讲到的任何一个都要复杂的数据类型。之前出现的数据类型都是储存在栈上的并且当离开作用域时被移出栈,不过我们需要寻找一个储存在堆上的数据来探索 Rust 如何知道该在何时清理数据的。

    +

    这里使用String作为例子并专注于String与所有权相关的部分。这些方面也同样适用于其他标准库提供的或你自己创建的复杂数据类型。在第八章会更深入地讲解String

    +

    我们已经见过字符串字面值了,它被硬编码进程序里。字符串字面值是很方便的,不过他们并不总是适合所有需要使用文本的场景。原因之一就是他们是不可变的。另一个原因是不是所有字符串的值都能在编写代码时就知道:例如,如果想要获取用户输入并储存该怎么办呢?为此,Rust 有第二个字符串类型,String。这个类型储存在堆上所以储存在编译时未知大小的文本。可以用from从字符串字面值来创建String,如下:

    let s = String::from("hello");
     

    这两个冒号(::)运算符允许将特定的from函数置于String类型的命名空间(namespace)下而不需要使用类似string_from这样的名字。在第五章的“方法语法”(“Method Syntax”)部分会着重讲解这个语法而且在第七章会讲到模块的命名空间。

    @@ -1676,7 +1651,7 @@ println!("{}", s); // This will print `hello, world!`

    那么这里有什么区别呢?为什么String可变而字面值却不行呢?区别在于两个类型对内存的处理上。

    内存与分配

    -

    字符串字面值的情况,我们在编译时就知道内容所以它直接被硬编码进最终的可执行文件中,这使得字符串字面值快速和高效。不过这些属性都只来源于它的不可变形。不幸的是,我们不能为了每一个在编译时未知大小的文本而将一块内存放入二进制文件中而它的大小还可能随着程序运行而改变。

    +

    对于字符串字面值的情况,我们在编译时就知道内容所以它直接被硬编码进最终的可执行文件中,这使得字符串字面值快速和高效。不过这些属性都只来源于它的不可变形。不幸的是,我们不能为了每一个在编译时未知大小的文本而将一块内存放入二进制文件中而它的大小还可能随着程序运行而改变。

    对于String类型,为了支持一个可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容。这意味着:

    1. 内存必须在运行时向操作系统请求
    2. @@ -1684,7 +1659,7 @@ println!("{}", s); // This will print `hello, world!`

    第一部分由我们完成:当调用String::from时,它的实现请求它需要的内存。这在编程语言中是非常通用的。

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

    -

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

    +

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

    {
         let s = String::from("hello"); // s is valid from this point forward
     
    @@ -1699,14 +1674,11 @@ println!("{}", s); // This will print `hello, world!`
     

    这个模式对编写 Rust 代码的方式有着深远的影响。它现在看起来很简单,不过在更复杂的场景下代码的行为可能是不可预测的,比如当有多个变量使用在堆上分配的内存时。现在让我们探索一些这样的场景。

    变量与数据交互:移动

    Rust 中的多个变量以一种独特的方式与同一数据交互。让我们看看列表 4-2 中一个使用整型的例子:

    -
    let x = 5;
     let y = x;
     
    -
    -

    Listing 4-2: Assigning the integer value of variable x to y

    -
    -
    +

    Listing 4-2: Assigning the integer value of variable x +to y

    根据其他语言的经验大致可以猜到这在干什么:“将5绑定到x;接着生成一个值x的拷贝并绑定到y”。现在有了两个变量,xy,都等于5。这也正是事实上发生了的,因为正数是有已知固定大小的简单值,所以这两个5被放入了栈中。

    现在看看这个String版本:

    let s1 = String::from("hello");
    @@ -1714,31 +1686,19 @@ let s2 = s1;
     

    这看起来与上面的代码非常类似,所以我们可能会假设他们的运行方式也是类似的:也就是说,第二行可能会生成一个s1的拷贝并绑定到s2上。不过,事实上并不完全是这样。

    为了更全面的解释这个问题,让我们看看图 4-3 中String真正是什么样。String由三部分组成,如图左侧所示:一个指向存放字符串内容内存的指针,一个长度,和一个容量。这一组数据储存在栈上。右侧则是堆上存放内容的内存部分。

    -
    -String in memory -
    -

    Figure 4-3: Representation in memory of a String holding the value "hello" -bound to s1

    -
    -
    -

    长度代表当前String的内容使用了多少字节的内存。容量是String从操作系统总共获取了多少字节的内存。长度与容量的区别是很重要的,不过目前为止的场景中并不重要,所以可以暂时忽略容量。

    +

    String in memory

    +

    Figure 4-3: Representation in memory of a String +holding the value "hello" bound to s1

    +

    长度代表当前String的内容使用了多少字节的内存。容量是String从操作系统总共获取了多少字节的内存。长度与容量的区别是很重要的,不过这在目前为止的场景中并不重要,所以可以暂时忽略容量。

    当我们把s1赋值给s2String的数据被复制了,这意味着我们从栈上拷贝了它的指针、长度和容量。我们并没有复制堆上指针所指向的数据。换句话说,内存中数据的表现如图 4-4 所示。

    -
    -s1 and s2 pointing to the same value -
    -

    Figure 4-4: Representation in memory of the variable s2 that has a copy of -the pointer, length, and capacity of s1

    -
    -
    +

    s1 and s2 pointing to the same value

    +

    Figure 4-4: Representation in memory of the variable s2 +that has a copy of the pointer, length, and capacity of s1

    这个表现形式看起来并不像图 4-5 中的那样,它是如果 Rust 也拷贝了堆上的数据后内存看起来是怎么样的。如果 Rust 这么做了,那么操作s2 = s1在堆上数据比较大的时候可能会对运行时性能造成非常大的影响。

    -
    -s1 and s2 to two places -
    -

    Figure 4-5: Another possibility of what s2 = s1 might do if Rust copied the -heap data as well

    -
    -
    -

    之前,我们提到过当变量离开作用域后 Rust 自动调用drop函数并清理变量的堆内存。不过图 4-4 展示了两个数据指针指向了同一位置。这就有了一个问题:当s2s1离开作用域,他们都会尝试释放相同的内存。这是一个叫做 double free 的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致安全漏洞。

    +

    s1 and s2 to two places

    +

    Figure 4-5: Another possibility of what s2 = s1 might +do if Rust copied the heap data as well

    +

    之前,我们提到过当变量离开作用域后 Rust 自动调用drop函数并清理变量的堆内存。不过图 4-4 展示了两个数据指针指向了同一位置。这就有了一个问题:当s2s1离开作用域,他们都会尝试释放相同的内存。这是一个叫做 double free 的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。

    为了确保内存安全,这种场景下 Rust 的处理有另一个细节值得注意。与其尝试拷贝被分配的内存,Rust 则认为s1不再有效,因此 Rust 不需要在s1离开作用域后清理任何东西。看看在s2被创建之后尝试使用s1会发生生么:

    let s1 = String::from("hello");
     let s2 = s1;
    @@ -1758,12 +1718,9 @@ println!("{}", s1);
     which does not implement the `Copy` trait
     

    如果你在其他语言中听说过术语“浅拷贝”(“shallow copy”)和“深拷贝”(“deep copy”),那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效化了,这个操作被成为移动move),而不是浅拷贝。上面的例子可以解读为s1移动到了s2中。那么具体发生了什么如图 4-6 所示。

    -
    -s1 moved to s2 -
    -

    Figure 4-6: Representation in memory after s1 has been invalidated

    -
    -
    +

    s1 moved to s2

    +

    Figure 4-6: Representation in memory after s1 has been +invalidated

    这样就解决了我们的麻烦!因为只有s2是有效的,当其离开作用域,它就释放自己的内存,完毕。

    另外,这里还隐含了一个设计选择:Rust 永远也不会自动创建数据的“深拷贝”。因此,任何自动的复制可以被认为对运行时性能影响较小。

    变量与数据交互:克隆

    @@ -1785,7 +1742,7 @@ println!("x = {}, y = {}", x, y);

    他们似乎与我们刚刚学到的内容相抵触:没有调用clone,不过x依然有效且没有被移动到y中。

    原因是像整型这样的在编译时已知大小的类型被整个储存在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量y后使x无效。换句话说,这里没有深浅拷贝的区别,所以调用clone并不会与通常的浅拷贝有什么不同,我们可以不用管它。

    -

    Rust 有一个叫做Copy trait 的特殊注解,可以用在类似整型这样的储存在栈上的类型(第十章详细讲解 trait)。如果一个类型拥有Copy trait,一个旧的变量在(重新)赋值后仍然可用。Rust 不允许自身或其任何部分实现了Drop trait 的类型使用Copy trait。如果我们对其值离开作用域时需要特殊处理的类型使用Copy注解,将会出现一个编译时错误。

    +

    Rust 有一个叫做Copy trait 的特殊注解,可以用在类似整型这样的储存在栈上的类型(第十章详细讲解 trait)。如果一个类型拥有Copy trait,一个旧的变量在(重新)赋值后仍然可用。Rust 不允许自身或其任何部分实现了Drop trait 的类型使用Copy trait。如果我们对其值离开作用域时需要特殊处理的类型使用Copy注解,将会出现一个编译时错误。关于如何为你的类型增加Copy注解,请阅读附录 C 中的 Derivable Trait。

    那么什么类型是Copy的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则,任何简单标量值的组合可以是Copy的,任何不需要分配内存或类似形式资源的类型是Copy的,如下是一些Copy的类型:

    • 所有整数类型,比如u32
    • @@ -1795,8 +1752,7 @@ println!("x = {}, y = {}", x, y);

    所有权与函数

    将值传递给函数在语言上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。列表 4-7 是一个带有变量何时进入和离开作用域标注的例子:

    -
    -Filename: src/main.rs +

    Filename: src/main.rs

    fn main() {
         let s = String::from("hello");  // s comes into scope.
     
    @@ -1820,11 +1776,9 @@ fn makes_copy(some_integer: i32) { // some_integer comes into scope.
         println!("{}", some_integer);
     } // Here, some_integer goes out of scope. Nothing special happens.
     
    -
    -

    Listing 4-7: Functions with ownership and scope annotated

    -
    -
    -

    当尝试在调用takes_ownership后使用s时,Rust 会抛出一个编译时错误。这些静态检查使我们免于犯错。试试在main函数中添加使用sx的代码来看看哪里能使用他们,和哪里所有权规则会阻止我们这么做。

    +

    Listing 4-7: Functions with ownership and scope +annotated

    +

    当尝试在调用takes_ownership后使用s时,Rust 会抛出一个编译时错误。这些静态检查使我们免于犯错。试试在main函数中添加使用sx的代码来看看哪里能使用他们,以及哪里所有权规则会阻止我们这么做。

    返回值与作用域

    返回值也可以转移作用域。这里是一个有与列表 4-7 中类似标注的例子:

    Filename: src/main.rs

    @@ -1879,9 +1833,9 @@ fn calculate_length(s: String) -> (String, usize) {

    但是这不免有些形式主义,同时这离一个通用的观点还有很长距离。幸运的是,Rust 对此提供了一个功能,叫做引用references)。

    引用与借用

    -

    ch04-02-references-and-borrowing.md +

    ch04-02-references-and-borrowing.md
    -commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c

    +commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

    在上一部分的结尾处的使用元组的代码是有问题的,我们需要将String返回给调用者函数这样就可以在调用calculate_length后仍然可以使用String了,因为String先被移动到了calculate_length

    下面是如何定义并使用一个(新的)calculate_length函数,它以一个对象的引用作为参数而不是获取值的所有权:

    @@ -1900,12 +1854,8 @@ fn calculate_length(s: &String) -> usize {

    首先,注意变量声明和函数返回值中的所有元组代码都消失了。其次,注意我们传递&s1calculate_length,同时在函数定义中,我们获取&String而不是String

    这些 & 符号就是引用,他们允许你使用值但不获取它的所有权。图 4-8 展示了一个图解。

    -
    -&String s pointing at String s1 -
    -

    Figure 4-8: &String s pointing at String s1

    -
    -
    +

    &String s pointing at String s1

    +

    Figure 4-8: &String s pointing at String s1

    仔细看看这个函数调用:

    # fn calculate_length(s: &String) -> usize {
     #     s.len()
    @@ -1924,8 +1874,7 @@ let len = calculate_length(&s1);
     

    变量s有效的作用域与函数参数的作用域一样,不过当引用离开作用域后并不丢弃它指向的数据因为我们没有所有权。函数使用引用而不是实际值作为参数意味着无需返回值来交还所有权,因为就不曾拥有它。

    我们将获取引用作为函数参数称为借用borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从它哪里借来。当你使用完毕,必须还回去。

    那么如果我们尝试修改借用的变量呢?尝试列表 4-9 中的代码。剧透:这行不通!

    -
    -Filename: src/main.rs +

    Filename: src/main.rs

    fn main() {
         let s = String::from("hello");
     
    @@ -1936,12 +1885,9 @@ fn change(some_string: &String) {
         some_string.push_str(", world");
     }
     
    -
    -

    Listing 4-9: Attempting to modify a borrowed value

    -
    -
    +

    Listing 4-9: Attempting to modify a borrowed value

    这里是错误:

    -
    error: cannot borrow immutable borrowed content `*some_string` as mutable
    +
    error: cannot borrow immutable borrowed content `*some_string` as mutable
      --> error.rs:8:5
       |
     8 |     some_string.push_str(", world");
    @@ -1970,7 +1916,7 @@ let r1 = &mut s;
     let r2 = &mut s;
     

    具体错误如下:

    -
    error[E0499]: cannot borrow `s` as mutable more than once at a time
    +
    error[E0499]: cannot borrow `s` as mutable more than once at a time
      --> borrow_twice.rs:5:19
       |
     4 |     let r1 = &mut s;
    @@ -1988,7 +1934,7 @@ let r2 = &mut s;
     
  • 没有被用来同步数据访问的机制。
  • 数据竞争会导致未定义行为并且当在运行时尝试追踪时可能会变得难以诊断和修复;Rust 阻止了这种情况的发生,因为存在数据竞争的代码根本就不能编译!

    -

    一如既往,使用大括号来创建一个新的作用域,允许拥有多个可变引用,只是不能同时拥有:

    +

    一如既往,可以使用大括号来创建一个新的作用域来允许拥有多个可变引用,只是不能同时拥有:

    let mut s = String::from("hello");
     
     {
    @@ -2006,7 +1952,7 @@ let r2 = &s; // no problem
     let r3 = &mut s; // BIG PROBLEM
     

    错误如下:

    -
    error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
    +
    error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
     immutable
      --> borrow_thrice.rs:6:19
       |
    @@ -2076,17 +2022,17 @@ for it to be borrowed from.
     
     
    • 一个可变引用。
    • -
    • 任意属性的不可变引用。
    • +
    • 任意数量的不可变引用。
    1. 引用必须总是有效的。
    -

    接下来,我们来看看一种不同类型的引用:slices。

    +

    接下来,我们来看看一种不同类型的引用:slice。

    Slices

    -

    ch04-03-slices.md +

    ch04-03-slices.md
    -commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c

    +commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

    另一个没有所有权的数据类型是 slice。slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。

    这里有一个小的编程问题:编写一个获取一个字符串并返回它在其中找到的第一个单词的函数。如果函数没有在字符串中找到一个空格,就意味着整个字符串是一个单词,所以整个字符串都应该返回。

    @@ -2094,8 +2040,7 @@ commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c

    fn first_word(s: &String) -> ?
     

    first_word这个函数有一个参数&String。因为我们不需要所有权,所以这没有问题。不过应该返回什么呢?我们并没有一个真正获取部分字符串的办法。不过,我们可以返回单词结尾的索引。让我们试试如列表 4-10 所示的代码:

    -
    -Filename: src/main.rs +

    Filename: src/main.rs

    fn first_word(s: &String) -> usize {
         let bytes = s.as_bytes();
     
    @@ -2108,18 +2053,15 @@ commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c

    s.len() }
    -
    -

    Listing 4-10: The first_word function that returns a byte index value into -the String parameter

    -
    -
    +

    Listing 4-10: The first_word function that returns a +byte index value into the String parameter

    让我们将代码分解成小块。因为需要一个元素一个元素的检查String中的值是否是空格,需要用as_bytes方法将String转化为字节数组:

    let bytes = s.as_bytes();
     
    -

    Next, we create an iterator over the array of bytes using the iter method :

    +

    接下来,使用iter方法在字节数据上创建一个迭代器:

    for (i, &item) in bytes.iter().enumerate() {
     
    -

    第十六章将讨论迭代器的更多细节。现在,只需知道iter方法返回集合中的每一个元素,而enumerate包装iter的结果并返回一个元组,其中每一个元素是元组的一部分。返回元组的第一个元素是索引,第二个元素是集合中元素的引用。这比我们自己计算索引要方便一些。

    +

    第十三章将讨论迭代器的更多细节。现在,只需知道iter方法返回集合中的每一个元素,而enumerate包装iter的结果并返回一个元组,其中每一个元素是元组的一部分。返回元组的第一个元素是索引,第二个元素是集合中元素的引用。这比我们自己计算索引要方便一些。

    因为enumerate方法返回一个元组,我们可以使用模式来解构它,就像 Rust 中其他地方一样。所以在for循环中,我们指定了一个模式,其中i是元组中的索引而&item是单个字节。因为从.iter().enumerate()中获取了集合元素的引用,我们在模式中使用了&

    我们通过字节的字面值来寻找代表空格的字节。如果找到了,返回它的位置。否则,使用s.len()返回字符串的长度:

        if item == b' ' {
    @@ -2129,8 +2071,7 @@ the String parameter

    s.len()

    现在有了一个找到字符串中第一个单词结尾索引的方法了,不过这有一个问题。我们返回了单单一个usize,不过它只在&String的上下文中才是一个有意义的数字。换句话说,因为它是一个与String像分离的值,无法保证将来它仍然有效。考虑一下列表 4-11 中使用了列表 4-10 first_word函数的程序:

    -
    -Filename: src/main.rs +

    Filename: src/main.rs

    # fn first_word(s: &String) -> usize {
     #     let bytes = s.as_bytes();
     #
    @@ -2154,11 +2095,8 @@ fn main() {
         // we could meaningfully use the value 5 with. word is now totally invalid!
     }
     
    -
    -

    Listing 4-11: Storing the result from calling the first_word function then -changing the String contents

    -
    -
    +

    Listing 4-11: Storing the result from calling the +first_word function then changing the String contents

    这个程序编译时没有任何错误,而且在调用s.clear()之后使用word也不会出错。这时words状态就没有联系了,所以word仍然包含值5。可以尝试用值5来提取变量s的第一个单词,不过这是有 bug 的,因为在我们将5保存到word之后s的内容已经改变。

    不得不担心word的索引与s中的数据不再同步是乏味且容易出错的!如果编写一个second_word函数的话管理索引将更加容易出问题。它的签名看起来像这样:

    fn second_word(s: &String) -> (usize, usize) {
    @@ -2175,12 +2113,9 @@ let world = &s[6..11];
     

    这类似于获取整个String的引用不过带有额外的[0..5]部分。不同于整个String的引用,这是一个包含String内部的一个位置和所需元素数量的引用。

    我们使用一个 range [starting_index..ending_index]来创建 slice,不过 slice 的数据结构实际上储存了开始位置和 slice 的长度。所以就let world = &s[6..11];来说,world将是一个包含指向s第 6 个字节的指针和长度值 5 的 slice。

    图 4-12 展示了一个图例

    -
    -world containing a pointer to the 6th byte of String s and a length 5 -
    -

    Figure 4-12: String slice referring to part of a String

    -
    -
    +

    world containing a pointer to the 6th byte of String s and a length 5

    +

    Figure 4-12: String slice referring to part of a +String

    对于 Rust 的.. range 语法,如果想要从第一个索引(0)开始,可以不写两个点号之前的值。换句话说,如下两个语句是相同的:

    let s = String::from("hello");
     
    diff --git a/src/PREFACE.md b/src/PREFACE.md
    index e2c0c57..12a3ee9 100644
    --- a/src/PREFACE.md
    +++ b/src/PREFACE.md
    @@ -1,9 +1,5 @@
     # Rust 程序设计语言(第二版) 简体中文版
     
    -还在施工中...
    +还在施工中:正在翻译第十六章
     
    -## Gitbook 中存在的问题
    -
    -`
    `中的 markdown 没有语法高亮QAQ - -[https://github.com/GitbookIO/gitbook/issues/1727](https://github.com/GitbookIO/gitbook/issues/1727) \ No newline at end of file +目前正在解决代码排版问题:已检查到第四章 \ No newline at end of file diff --git a/src/ch01-00-introduction.md b/src/ch01-00-introduction.md index d51ede2..31a1d15 100644 --- a/src/ch01-00-introduction.md +++ b/src/ch01-00-introduction.md @@ -4,17 +4,21 @@ >
    > commit 4f2dc564851dc04b271a2260c834643dfd86c724 -欢迎阅读“Rust 程序设计语言”,一本关于 Rust 的介绍性书籍。Rust 是一个着用于安全、速度和并发的编程语言。它的设计不仅可以使程序获得性能和对底层语言的控制,并且能够享受高级语言强大的抽象能力。这些特性使得 Rust 适合那些有类似 C 语言经验并正在寻找一个更安全的替代者的程序员,同时也适合那些来自类似 Python 语言背景,正在探索在不牺牲表现力的情况下编写更好性能代码的人们。 +欢迎阅读“Rust 程序设计语言”,一本关于 Rust 的介绍性书籍。Rust 是一个着用于安全、速度和并发的编程语言。它的设计不仅可以使程序获得性能和对底层语言的控制,并且能够享受高级语言强大的抽象能力。这些特性使得 Rust 适合那些有类似 C 语言经验并正在寻找一个更安全的替代者的程序员,同时也适合那些来自类似 Python 语言背景,正在探索在不牺牲表现力的情况下编写更好性能代码的开发者。 -Rust 在编译时进行其绝大多数的安全检查和内存管理决策,因此程序的运行时性能没有受到影响。这让其在许多其他语言不擅长的应用场景中得以大显身手:有可预测空间和时间要求的程序,嵌入到其他语言中,以及编写底层代码,如设备驱动和操作系统。Rust 也很擅长 web 程序:它驱动着 Rust 包注册网站(package +Rust 在编译时进行其绝大多数的安全检查和内存管理决策,因此程序的运行时性能没有受到影响。这让其在许多其他语言不擅长的应用场景中得以大显身手:存在可预测空间和时间要求的程序,嵌入到其他语言中,以及编写底层代码,如设备驱动和操作系统。Rust 也很擅长 web 程序:它驱动着 Rust 包注册网站(package registry site),[crates.io]!我们期待看到**你**使用 Rust 进行创作。 [crates.io]: https://crates.io/ -本书是为已经至少了解一门编程语言的读者而写的。读完本书之后,你应该能自如的编写 Rust 程序。我们将通过小而集中并相互依赖的例子来学习 Rust,并向你展示如何使用 Rust 多样的功能,同时了解它们在后台是如何执行的。 +本书是为已经至少了解一门编程语言的读者而写的。读完本书之后,你应该能自如的编写 Rust 程序。我们将通过小而集中并且相互依赖的例子来学习 Rust,并向你展示如何使用 Rust 多样的功能,同时了解它们在幕后是如何执行的。 ## 为本书做出贡献 本书是开源的。如果你发现任何错误,请不要犹豫,[在 GitHub 上][on GitHub]发起 issue 或提交 pull request。 -[on GitHub]: https://github.com/rust-lang/book \ No newline at end of file +[on GitHub]: https://github.com/rust-lang/book + +> 译者注:这是本译本的 [GitHub 仓库][trpl-zh-cn],同样欢迎 Issue 和 PR :) + +[trpl-zh-cn]: https://github.com/KaiserY/trpl-zh-cn \ No newline at end of file diff --git a/src/ch01-01-installation.md b/src/ch01-01-installation.md index ad4d774..4d95449 100644 --- a/src/ch01-01-installation.md +++ b/src/ch01-01-installation.md @@ -4,7 +4,7 @@ >
    > commit 4f2dc564851dc04b271a2260c834643dfd86c724 -使用 Rust 的第一步是安装。你需要联网来执行本章的命令,因为我们要从网上下载 Rust。 +使用 Rust 的第一步是安装。你需要网络连接来执行本章的命令,因为我们要从网上下载 Rust。 我们将会展示很多使用终端的命令,并且这些代码都以`$`开头。并不需要真正输入`$`,它们在这里代表每行指令的开头。在网上会看到很多使用这个惯例的教程和例子:`$`代表以常规用户运行命令,`#`代表需要用管理员运行的命令。没有以`$`(或`#`)的行通常是之前命令的输出。 @@ -12,13 +12,13 @@ 如果你使用 Linux 或 Mac,所有需要做的就是打开一个终端并输入: -```sh +``` $ curl https://sh.rustup.rs -sSf | sh ``` 这会下载一个脚本并开始安装。你可能被提示要输入密码。如果一切顺利,将会出现如下内容: -```sh +``` Rust is installed now. Great! ``` @@ -26,9 +26,9 @@ Rust is installed now. Great! ### 在 Windows 上安装 -在 Windows 上,前往[https://rustup.rs](https://rustup.rs/)并按照说明下载`rustup-init.exe`。运行并遵循它提供的其余指示。 +在 Windows 上,前往[https://rustup.rs](https://rustup.rs/)并按照说明下载 rustup-init.exe。运行并遵循其提供的其余指示操作。 -本书其余 Windows 相关的命令假设你使用`cmd`作为你的 shell。如果你使用不同的 shell,可能能够执行 Linux 和 Mac 用户相同的命令。如果都不行,查看所使用的 shell 的文档。 +本书其余 Windows 相关的命令假设你使用`cmd`作为你的 shell。如果你使用不同的 shell,可能能够执行 Linux 和 Mac 用户相同的命令。如果都不行,请查看所使用的 shell 的文档。 ### 自定义安装 @@ -38,7 +38,7 @@ Rust is installed now. Great! 卸载 Rust 同安装一样简单。在 shell 中运行卸载脚本 -```sh +``` $ rustup self uninstall ``` @@ -46,13 +46,13 @@ $ rustup self uninstall 安装完 Rust 后,打开 shell,输入: -```sh +``` $ rustc --version ``` 应该能看到类似这样的版本号、提交 hash 和提交日期,对应你安装时的最新稳定版本: -```sh +``` rustc x.y.z (abcabcabc yyyy-mm-dd) ``` @@ -62,11 +62,10 @@ rustc x.y.z (abcabcabc yyyy-mm-dd) 如果有问题并且你在使用 Windows,检查 Rust(rustc,cargo 等)是否位于`%PATH%`系统变量中。 -如果还是不能运行,有许多可以获取帮助的地方。最简单的是 irc.mozilla.org 上的 IRC 频道 [#rust-beginners][irc-beginners] 和供一般讨论之用的 [#rust][irc],我们可以使用 [Mibbit][mibbit] 访问。然后我们就可以和其他能提供帮助的 Rustacean(我们这些人自嘲的绰号)聊天了。其它给力的资源包括[用户论坛][users]和[Stack Overflow][stackoverflow]。 +如果还是不能运行,有许多可以获取帮助的地方。最简单的是 [irc.mozilla.org 上的 #rust IRC 频道][irc] ,可以使用 [Mibbit][mibbit] 来访问它。访问这些地址然后就可以和其他能提供帮助的 Rustacean(我们这些人自嘲的绰号)聊天了。其它给力的资源包括[用户论坛][users]和 [Stack Overflow][stackoverflow]。 -[irc-beginners]: irc://irc.mozilla.org/#rust-beginners [irc]: irc://irc.mozilla.org/#rust -[mibbit]: http://chat.mibbit.com/?server=irc.mozilla.org&channel=%23rust-beginners,%23rust +[mibbit]: http://chat.mibbit.com/?server=irc.mozilla.org&channel=%23rust [users]: https://users.rust-lang.org/ [stackoverflow]: http://stackoverflow.com/questions/tagged/rust @@ -74,4 +73,4 @@ rustc x.y.z (abcabcabc yyyy-mm-dd) 安装程序也包含一份本地文档的拷贝,你可以离线阅读它们。输入`rustup doc`将在浏览器中打开本地文档。 -任何你太确认标准库提供的类型或函数是干什么的时候,使用文档 API 查找! \ No newline at end of file +任何你不太确认标准库中提供的类型或函数是干什么的时候,请查看 API 文档! \ No newline at end of file diff --git a/src/ch01-02-hello-world.md b/src/ch01-02-hello-world.md index 4b62a3c..48cfc05 100644 --- a/src/ch01-02-hello-world.md +++ b/src/ch01-02-hello-world.md @@ -4,17 +4,17 @@ >
    > commit 4f2dc564851dc04b271a2260c834643dfd86c724 -现在你已经安装好了 Rust,让我们来编写你的第一个 Rust 程序。当学习一门新语言的时候,编写一个在屏幕上打印 “Hello, world!” 文本的小程序是一个传统,而在这一部分,我们将遵循这个传统。 +现在已经安装好了 Rust,让我们来编写第一个 Rust 程序。当学习一门新语言的时候,编写一个在屏幕上打印 “Hello, world!” 文本的小程序是一个传统,而在这一部分将遵循这个传统。 > 注意:本书假设你熟悉基本的命令行操作。Rust 本身并不对你的编辑器,工具和你的代码存放在何处有什么特定的要求,所以如果你比起命令行更喜欢 IDE,请随意选择你喜欢的 IDE。 ### 创建项目文件夹 -首先,创建一个文件夹来编写 Rust 代码。Rust 并不关心你的代码存放在哪里,不过在本书中,我们建议在你的 home 目录创建一个**项目**目录,并把你的所有项目放在这。打开一个终端并输入如下命令来为这个项目创建一个文件夹: +首先,创建一个文件夹来存放 Rust 代码。Rust 并不关心你的代码存放在哪里,不过在本书中,我们建议在你的 home 目录创建一个 *projects* 目录,并把你的所有项目放在这。打开一个终端并输入如下命令来为这个项目创建一个文件夹: Linux 和 Mac: -```sh +``` $ mkdir ~/projects $ cd ~/projects $ mkdir hello_world @@ -32,7 +32,7 @@ Windows: ### 编写并运行 Rust 程序 -接下来,创建一个新的叫做 *main.rs* 的源文件。Rust 文件总是以 *.rs* 后缀结尾。如果文件名多于一个单词,使用下划线分隔它们。例如,使用 *my_program.rs* 而不是 *myprogram.rs*。 +接下来,新建一个叫做 *main.rs* 的源文件。Rust 文件总是以 *.rs* 后缀结尾。如果文件名多于一个单词,使用下划线分隔它们。例如,使用 *my_program.rs* 而不是 *myprogram.rs*。 现在打开刚创建的 *main.rs* 文件,并输入如下代码: @@ -46,7 +46,7 @@ fn main() { 保存文件,并回到终端窗口。在 Linux 或 OSX 上,输入如下命令: -```sh +``` $ rustc main.rs $ ./main Hello, world! @@ -56,7 +56,7 @@ Hello, world! ### 分析 Rust 程序 -现在,让我们回过头来仔细看看你的“Hello, world!”程序到底发生了什么。这是谜题的第一片: +现在,让我们回过头来仔细看看“Hello, world!”程序到底发生了什么。这里是谜题的第一片: ```rust fn main() { @@ -64,7 +64,7 @@ fn main() { } ``` -这几行定义了一个 Rust **函数**。`main` 函数是特殊的:这是每一个可执行的 Rust 程序首先运行的函数(译者注:入口点)。第一行表示“定义一个叫 `main` 的函数,没有参数也没有返回值。”如果有参数的话,它们应该出现在括号中,`(`和`)`。 +这几行定义了一个 Rust **函数**。`main`函数是特殊的:这是每一个可执行的 Rust 程序首先运行的函数(译者注:入口点)。第一行表示“定义一个叫 `main` 的函数,没有参数也没有返回值。”如果有参数的话,它们应该出现在括号中,`(`和`)`。 同时注意函数体被包裹在大括号中,`{`和`}`。Rust 要求所有函数体都位于大括号中(译者注:对比有些语言特定情况可以省略大括号)。将前一个大括号与函数声明置于一行,并留有一个空格被认为是一个好的代码风格。 @@ -74,9 +74,9 @@ fn main() { println!("Hello, world!"); ``` -这行代码做了这个小程序的所有工作:它在屏幕上打印文本。有很多需要注意的细节。第一个是 Rust 代码风格使用 4 个空格缩进,而不是 1 个制表符(tab)。 +这行代码做了这个小程序的所有工作:它在屏幕上打印文本。这里有很多需要注意的细节。第一个是 Rust 代码风格使用 4 个空格缩进,而不是 1 个制表符(tab)。 -第二个重要的部分是`println!()`。这叫做 Rust **宏**,是如何进行 Rust 元编程(metaprogramming)的关键所在。相反如果调用一个函数的话,它应该看起来像这样:`println`(没有`!`)。我们将在 24 章更加详细的讨论 Rust 宏,不过现在你只需记住当看到符号 `!` 的时候,就代表在调用一个宏而不是一个普通的函数。 +第二个重要的部分是`println!()`。这叫做 Rust **宏**,是如何进行 Rust 元编程(metaprogramming)的关键所在。相反如果是调用一个函数的话,它应该看起来像这样:`println`(没有`!`)。我们将在 24 章更加详细的讨论 Rust 宏,不过现在你只需记住当看到符号`!`的时候,就代表在调用一个宏而不是一个普通的函数。 接下来,`"Hello, world!"` 是一个 **字符串**。我们把这个字符串作为一个参数传递给`println!`,它负责在屏幕上打印这个字符串。轻松加愉快!(⊙o⊙) @@ -86,15 +86,15 @@ fn main() { 在“编写并运行 Rust 程序”部分,展示了如何运行一个新创建的程序。现在我们将拆分并检查每一步操作。 -在运行一个 Rust 程序之前,必须编译它。可以输入`rustc`命令来使用 Rust 编译器并像这样传递你源文件的名字: +在运行一个 Rust 程序之前,必须先编译它。可以输入`rustc`命令来使用 Rust 编译器并像这样传递源文件的名字: -```sh +``` $ rustc main.rs ``` -如果你来自 C 或 C++ 背景,你会发现这与`gcc`和`clang`类似。编译成功后,Rust 应该会输出一个二进制可执行文件,在 Linux 或 OSX 上在 shell 中你可以通过`ls`命令看到如下: +如果你来自 C 或 C++ 背景,就会发现这与`gcc`和`clang`类似。编译成功后,Rust 应该会输出一个二进制可执行文件,在 Linux 或 OSX 上在 shell 中你可以通过`ls`命令看到如下: -```sh +``` $ ls main main.rs ``` @@ -109,7 +109,7 @@ main.rs 这表示我们有两个文件:*.rs* 后缀的源文件,和可执行文件(在 Windows下是 *main.exe*,其它平台是 *main*)。这里剩下的操作就只有运行 *main* 或 *main.exe* 文件了,像这样: -```sh +``` $ ./main # or .\main.exe on Windows ``` @@ -121,25 +121,25 @@ $ ./main # or .\main.exe on Windows ## Hello, Cargo! -Cargo 是 Rust 的构建系统和包管理工具,同时 Rustacean 们使用 Cargo 来管理它们的 Rust 项目,因为它使得很多任务变得更轻松。例如,Cargo负责构建代码、下载代码依赖的库并编译这些库。我们把代码需要的库叫做 **依赖**(*dependencies*)。 +Cargo 是 Rust 的构建系统和包管理工具,同时 Rustacean 们使用 Cargo 来管理他们的 Rust 项目,因为它使得很多任务变得更轻松。例如,Cargo 负责构建代码、下载代码依赖的库并编译这些库。我们把代码需要的库叫做 **依赖**(*dependencies*)。 -最简单的 Rust 程序,例如我们刚刚编写的,并没有任何依赖,所以目前我们只使用了 Cargo 负责构建代码的部分。随着你编写更加复杂的 Rust 程序,你会想要添加依赖,那么如果你使用 Cargo 开始的话,这将会变得简单许多。 +最简单的 Rust 程序,例如我们刚刚编写的,并没有任何依赖,所以目前我们只使用了 Cargo 负责构建代码的那一部分。随着编写更加复杂的 Rust 程序,你会想要添加依赖,那么如果你使用 Cargo 开始的话,这将会变得简单许多。 由于绝大部分 Rust 项目使用 Cargo,本书接下来的部分将假设你使用它。如果使用安装章节介绍的官方安装包的话,Rust 自带 Cargo。如果通过其他方式安装 Rust 的话,可以在终端输入如下命令检查是否安装了 Cargo: -```sh +``` $ cargo --version ``` -如果看到了版本号,一切 OK!如果出现一个类似“`command not found`”的错误,那么你应该查看安装方式的文档来确定如何单独安装 Cargo。 +如果出现了版本号,一切 OK!如果出现一个类似“`command not found`”的错误,那么你应该查看安装方式的文档来确定如何单独安装 Cargo。 ### 使用 Cargo 创建项目 -让我们使用 Cargo 来创建一个新项目并看看与`hello_world`项目有什么不同。回到项目目录(或者任何你决定放置代码的目录): +让我们使用 Cargo 来创建一个新项目并看看与上面的`hello_world`项目有什么不同。回到 projects 目录(或者任何你决定放置代码的目录): Linux 和 Mac: -```sh +``` $ cd ~/projects ``` @@ -151,14 +151,14 @@ Windows: 并在任何操作系统运行: -```sh +``` $ cargo new hello_cargo --bin $ cd hello_cargo ``` -我们向`cargo new`传递了`--bin`因为我们的目标是生成一个可执行程序,而不是一个库。可执行文件是二进制可执行文件,通常就叫做 **二进制文件**(*binaries*)。项目的名称被定为`hello_cargo`,同时 Cargo 在一个同名(子)目录中创建它的文件,接着我们可以进入查看。 +我们向`cargo new`传递了`--bin`因为我们的目标是生成一个可执行程序,而不是一个库。可执行文件是二进制可执行文件,通常就叫做 **二进制文件**(*binaries*)。项目的名称被定为`hello_cargo`,同时 Cargo 在一个同名目录中创建它的文件,接着我们可以进入查看。 -如果列出 *hello_cargo* 目录中的文件,我们将会看到 Cargo 生成了两个文件和一个目录:一个 *Cargo.toml* 文件和一个 *src* 目录,*main.rs* 文件位于目录中。它也在 *hello_cargo* 目录初始化了一个 git 仓库,以及一个 *.gitignore* 文件;你可以改为使用不同的版本控制系统,或者不使用,通过`--vcs`参数。 +如果列出 *hello_cargo* 目录中的文件,将会看到 Cargo 生成了两个文件和一个目录:一个 *Cargo.toml* 文件和一个 *src* 目录,*main.rs* 文件位于目录中。它也在 *hello_cargo* 目录初始化了一个 git 仓库,以及一个 *.gitignore* 文件;你可以改为使用不同的版本控制系统(VCS),或者不使用 VCS,通过`--vcs`参数。 使用你选择的文本编辑器(IDE)打开 *Cargo.toml* 文件。它应该看起来像这样: @@ -177,12 +177,14 @@ authors = ["Your Name "] [toml]: https://github.com/toml-lang/toml -第一行,`[package]`,是一个部分标题表明下面的语句用来配置一个包。随着我们在这个文件增加更多的信息,我们还会增加其他部分。 +第一行,`[package]`,是一个部分标题表明下面的语句用来配置一个包。随着我们在这个文件增加更多的信息,还将增加其他部分。 -最后一行,`[dependencies]`,是列出项目依赖的 *crates*(我们这么称呼 Rust 代码的包)的部分的开始,这样 Cargo 也就知道去下载和编译它们。这个项目并不需要任何其他的 crate,不过在猜猜看教程章节会需要。 +最后一行,`[dependencies]`,是列出项目依赖的 *crates*(我们这么称呼 Rust 代码包)的部分的开始,这样 Cargo 也就知道去下载和编译它们了。这个项目并不需要任何其他的 crate,不过在下一章猜猜看教程会需要。 现在看看 *src/main.rs*: +Filename: src/main.rs + ```rust fn main() { println!("Hello, world!"); @@ -194,13 +196,13 @@ Cargo 为你生成了一个“Hello World!”,正如我们之前编写的那 - 代码位于 *src* 目录 - 项目根目录包含一个 *Cargo.toml* 配置文件 -Cargo 期望源文件位于 src 目录,这样将项目根目录留给 README、license 信息、配置文件和其他跟代码无关的文件。这样,Cargo 帮助你保持项目干净整洁。一切井井有条。 +Cargo 期望源文件位于 *src* 目录,这样将项目根目录留给 README、license 信息、配置文件和其他跟代码无关的文件。这样,Cargo 帮助你保持项目干净整洁。一切井井有条。 -如果没有使用 Cargo 开始项目,正如我们在 *hello_world* 目录中的项目,可以把它转化为一个 Cargo 使用的项目,通过将代码放入 *src* 目录并创建一个合适的 *Cargo.toml*。 +如果没有使用 Cargo 开始项目,正如在 *hello_world* 目录中的项目,可以把它转化为一个使用 Cargo 的项目,通过将代码放入 *src* 目录并创建一个合适的 *Cargo.toml*。 ### 构建并运行 Cargo 项目 -现在让我们看看通过 Cargo 构建和运行 Hello World 程序有什么不同。为此,我们输入如下命令: +现在让我们看看通过 Cargo 构建和运行 Hello World 程序有什么不同。为此,输入如下命令: ``` $ cargo build @@ -209,14 +211,14 @@ $ cargo build 这应该创建 *target/debug/hello_cargo*(或者在 Windows 上是 *target\debug\hello_cargo.exe*)可执行文件,可以通过这个命令运行: -```sh +``` $ ./target/debug/hello_cargo # or .\target\debug\hello_cargo.exe on Windows Hello, world! ``` 好的!如果一切顺利,`Hello, world!`应该再次打印在终端上。 -第一次运行的时候也会使 Cargo 在项目根目录创建一个叫做 *Cargo.lock* 的新文件,它看起来像这样: +第一次运行`cargo build`的时候也会使 Cargo 在项目根目录创建一个叫做 *Cargo.lock* 的新文件,它看起来像这样: Filename: Cargo.lock @@ -230,7 +232,7 @@ Cargo 使用 *Cargo.lock* 来记录程序的依赖。这个项目并没有依赖 我们刚刚使用`cargo build`构建了项目并使用`./target/debug/hello_cargo`运行了它,不过也可以使用`cargo run`编译并运行: -```sh +``` $ cargo run Running `target/debug/hello_cargo` Hello, world! @@ -238,32 +240,32 @@ Hello, world! 注意这一次,并没有出现告诉我们 Cargo 正在编译 `hello_cargo` 的输出。Cargo 发现文件并没有被改变,所以只是运行了二进制文件。如果修改了源文件的话,将会出现像这样的输出: -```sh +``` $ 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 会将它放到 *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 build`,然后一切将有序运行。即便这个项目很简单,现在它使用了很多接下来你 Rust 程序生涯将会用到的实用工具。事实上,无形中你可以使用下面的命令开始所有你想要从事的项目: +对于简单项目, Cargo 并不能比`rustc`提供更多的价值,不过随着开发的进行终将体现它的价值。对于拥有多个 crate 的复杂项目,让 Cargo 来协调构建将更简单。有了 Cargo,只需运行`cargo build`,然后一切将有序运行。即便这个项目很简单,现在也它使用了很多之后你的 Rust 程序生涯将会用得上的实用工具。事实上,无形中你可以使用下面的命令开始所有你想要从事的项目: -```sh +``` $ git clone someurl.com/someproject $ cd someproject -$ carg +$ cargo build ``` > 注意:如果你想要查看 Cargo 的更多细节,请阅读官方的 [Cargo guide],它覆盖了其所有的功能。 diff --git a/src/ch02-00-guessing-game-tutorial.md b/src/ch02-00-guessing-game-tutorial.md index 9dbdab7..018e873 100644 --- a/src/ch02-00-guessing-game-tutorial.md +++ b/src/ch02-00-guessing-game-tutorial.md @@ -2,24 +2,24 @@ > [ch02-00-guessing-game-tutorial.md](https://github.com/rust-lang/book/blob/master/src/ch02-00-guessing-game-tutorial.md) >
    -> commit 7c1c935560190fcd64c0851e75dbeabf75fedd19 +> commit e6d6caab41471f7115a621029bd428a812c5260e -让我们通过自己动手的方式一起完成一个项目来快速上手 Rust!本章通过展示如何在真实的项目中运用的方式向你介绍一些常用的 Rust 概念。你将会学到`let`、`match`、方法、关联函数、使用外部 crate 等更多的知识!接下来的章节会探索这些概念的细节。在这一章,我们练习基础。 +让我们通过自己动手的方式一起完成一个项目来快速上手 Rust!本章将介绍一些常用的 Rust 概念,并通过真实的程序来展示如何运用他们。你将会学到`let`、`match`、方法、关联函数、使用外部 crate 等更多的知识!接下来的章节会探索这些概念的细节。在这一章,我们将练习基础。 -我们会实现一个经典新手编程问题:猜猜看游戏。它是这么工作的:程序将会随机生成一个 1 到 100 之间的随机整数。接着它会提示玩家输入一个猜测。当输入了一个猜测后,它会告诉提示猜测是太大了还是太小了。猜对了,它会打印出祝贺并退出。 +我们会实现一个经典的新手编程问题:猜猜看游戏。它是这么工作的:程序将会随机生成一个 1 到 100 之间的随机整数。接着它会提示玩家输入一个猜测。当输入了一个猜测后,它会提示猜测是太大了还是太小了。如果猜对了,它会打印出祝贺并退出。 ## 准备一个新项目 -要创建一个新项目,进入你在第一章创建的**项目**目录,并使用 Cargo 创建它,像这样: +要创建一个新项目,进入在第一章创建的**项目**目录,像这样使用 Cargo 创建它: -```sh +``` $ cargo new guessing_game --bin $ cd guessing_game ``` 第一个命令,`cargo new`,获取项目的名称(`guessing_game`)作为第一个参数。`--bin`参数告诉 Cargo 创建一个二进制项目,与第一章类似。第二个命令进入到新创建的项目目录。 -看一样生成的 *Cargo.toml* 文件: +看看生成的 *Cargo.toml* 文件: Filename: Cargo.toml @@ -32,7 +32,7 @@ authors = ["Your Name "] [dependencies] ``` -如果 Cargo 从环境中获取的作者信息不正确,修改这个文件并再次保存。 +如果 Cargo 从环境中获取的开发者信息不正确,修改这个文件并再次保存。 正如第一章那样,`cargo new`生成了一个“Hello, world!”程序。查看 *src/main.rs* 文件: @@ -54,7 +54,7 @@ $ cargo run Hello, world! ``` -`run`命令在你需要快速迭代项目时就派上用场了,而这个游戏就正是这么一个项目:我们需要在进行下一步之前快速测试每次迭代。 +`run`命令在需要快速迭代项目时就派上用场了,而这个游戏就正是这么一个项目:我们需要在进行下一步之前快速测试每次迭代。 重新打开 *src/main.rs* 文件。我们将会在这个文件编写全部的代码。 @@ -62,7 +62,6 @@ Hello, world! 程序的第一部分会请求用户输入,处理输入,并检查输入是否为期望的形式。首先,允许玩家输入一个猜测。在 *src/main.rs* 中输入列表 2-1 中的代码。 -
    Filename: src/main.rs ```rust,ignore @@ -82,12 +81,7 @@ fn main() { } ``` -
    - -Listing 2-1: Code to get a guess from the user and print it out - -
    -
    +Listing 2-1: Code to get a guess from the user and print it out 这些代码包含很多信息,所以让我们一点一点地过一遍。为了获取用户输入并接着打印结果作为输出,我们需要将`io`(输入/输出)库引入作用域中。`io`库来自于标准库(也被称为`std`): @@ -97,7 +91,7 @@ use std::io; Rust 默认只在每个程序的 [*prelude*][prelude] 中引用很少的一些类型。如果想要使用的类型并不在 prelude 中,你必须使用一个`use`语句显式的将其引入到作用域中。使用`std::io`库将提供很多`io`相关的功能,接受用户输入的功能。 -[prelude]: https://doc.rust-lang.org/std/prelude/ +[prelude]: https://doc.rust-lang.org/std/prelude/index.html 正如第一章所讲,`main`函数是程序的入口点: @@ -142,7 +136,7 @@ let mut bar = 5; // mutable 现在我们知道了`let mut guess`会引入一个叫做`guess`的可变变量。等号(`=`)的另一边是`guess`所绑定的值,它是`String::new`的结果,这个函数会返回一个`String`的新实例。[`String`][string]是一个标准库提供的字符串类型,它是可增长的、UTF-8 编码的文本块。 -[string]: ../std/string/struct.String.html +[string]: https://doc.rust-lang.org/std/string/struct.String.html `::new`那一行的`::`语法表明`new`是`String`类型的一个**关联函数**(*associated function*)。关联函数是针对类型实现的,在这个例子中是`String`,而不是`String`的某个特定实例。一些语言中把它称为**静态方法**(*static method*)。 @@ -159,17 +153,17 @@ io::stdin().read_line(&mut guess) 如果我们在程序的开头没有`use std::io`这一行,我们可以把函数调用写成`std::io::stdin`这样。`stdin`函数返回一个 [`std::io::Stdin`][iostdin]的实例,这是一个代表终端标准输入句柄的类型。 -[iostdin]: ../std/io/struct.Stdin.html +[iostdin]: https://doc.rust-lang.org/std/io/struct.Stdin.html 代码的下一部分,`.read_line(&mut guess)`,调用 [`read_line`][read_line] 方法从标准输入句柄获取用户输入。我们还向`read_line()`传递了一个参数:`&mut guess`。 -[read_line]: ../std/io/struct.Stdin.html#method.read_line +[read_line]: https://doc.rust-lang.org/std/io/struct.Stdin.html#method.read_line `read_line`的工作是把获取任何用户键入到标准输入的字符并放入一个字符串中,所以它获取字符串作为一个参数。这个字符串需要是可变的,这样这个方法就可以通过增加用户的输入来改变字符串的内容。 -`&`表明这个参数是一个**引用**(*reference*),它提供了一个允许多个不同部分的代码访问同一份数据而不需要在内存中多次拷贝的方法。引用是一个复杂的功能,而 Rust 的一大优势就是它是安全而优雅操纵引用。完成这个程序并不需要知道这么多细节:第四章会更全面的解释引用。现在,我们只需知道它像变量一样,默认是不可变的。因此,需要写成`&mut guess`而不是`&guess`来使其可变。 +`&`表明这个参数是一个**引用**(*reference*),它提供了一个允许多个不同部分的代码访问同一份数据而不需要在内存中多次拷贝的方法。引用是一个复杂的功能,而 Rust 的一大优势就是它是如何安全而优雅操纵引用的。完成这个程序并不需要知道这么多细节:第四章会更全面的解释引用。现在,我们只需知道它像变量一样,默认是不可变的。因此,需要写成`&mut guess`而不是`&guess`来使其可变。 -这行代码还没有分析完。虽然这是单独一行代码,但它只是一个逻辑上代码行(虽然换行了但仍是一个语句)的第一部分。第二部分是这个方法: +我们还没有分析完这行代码。虽然这是单独一行代码,但它只是一个逻辑上代码行(虽然换行了但仍是一个语句)的第一部分。第二部分是这个方法: ```rust,ignore .expect("Failed to read line"); @@ -185,10 +179,10 @@ io::stdin().read_line(&mut guess).expect("Failed to read line"); ### 使用`Result`类型来处理潜在的错误 -之前提到过,`read_line`将用户输入放入到传递给它字符串中,不过它也返回一个值————一个[`io::Result`][ioresult]。Rust 标准库中有很多叫做`Result`的类型。一个[`Result`][result]泛型以及对应子模块的特定版本,比如`io::Result`。 +之前提到过,`read_line`将用户输入放入到传递给它字符串中,不过它也返回一个值——在这个例子中,一个[`io::Result`][ioresult]。Rust 标准库中有很多叫做`Result`的类型。一个[`Result`][result]泛型以及对应子模块的特定版本,比如`io::Result`。 -[ioresult]: ../std/io/type.Result.html -[result]: ../std/result/enum.Result.html +[ioresult]: https://doc.rust-lang.org/std/io/type.Result.html +[result]: https://doc.rust-lang.org/std/result/enum.Result.html `Result`类型是 [*枚举*(*enumerations*)][enums],通常也写作 *enums*。枚举拥有固定值集合的类型,而这些值被称为枚举的**成员**(*variants*)。第六章会更详细的介绍枚举。 @@ -196,13 +190,13 @@ io::stdin().read_line(&mut guess).expect("Failed to read line"); 对于`Result`,它的成员是`Ok`或`Err`,`Ok`表明操作成功了,同时`Ok`成员之中包含成功生成的值。`Err`意味着操作失败,`Err`之中包含操作是为什么或如何失败的信息。 -`Result`类型的作用是编码错误处理信息。`Result`类型的值,正如其他任何类型,拥有定义于其上的方法。`io::Result`的实例拥有[`expect`方法][expect]可供调用。如果`io::Result`实例的值是`Err`,`expect`会导致程序崩溃并显示显示你作为参数传递给`expect`的信息。如果`io::Result`实例的值是`Ok`,`expect`会获取`Ok`中的值并原原本本的返回给你,这样就可以使用它了。在本例中,返回值是用户输入到标准输入的一些字符。 +`Result`类型的作用是编码错误处理信息。`Result`类型的值,正如其他任何类型,拥有定义于其上的方法。`io::Result`的实例拥有[`expect`方法][expect]可供调用。如果`io::Result`实例的值是`Err`,`expect`会导致程序崩溃并显示显示你作为参数传递给`expect`的信息。如果`io::Result`实例的值是`Ok`,`expect`会获取`Ok`中的值并原原本本的返回给你。在本例中,这个值是用户输入到标准输入中的字节的数量。 -[expect]: ../std/result/enum.Result.html#method.expect +[expect]: https://doc.rust-lang.org/std/result/enum.Result.html#method.expect 如果不使用`expect`,程序也能编译,不过会出现一个警告: -```sh +``` $ 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, @@ -211,7 +205,7 @@ src/main.rs:10 io::stdin().read_line(&mut guess); ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``` -Rust 警告说我们没有使用`read_line`返回的值`Result`,表明程序没有处理一个可能的错误。消除警告的正确方式是老实编写错误处理,不过因为我们仅仅希望程序出现问题就崩溃,可以使用`expect`。你会在第九章学习从错误中恢复。 +Rust 警告我们没有使用`read_line`返回的值`Result`,表明程序没有处理一个可能的错误。消除警告的正确方式是老实编写错误处理,不过因为我们仅仅希望程序出现问题就崩溃,可以直接使用`expect`。第九章会学习从错误中恢复的内容。 ### 使用`println!`占位符打印值 @@ -254,9 +248,9 @@ You guessed: 6 [randcrate]: https://crates.io/crates/rand -## 使用 crate 来增加更多功能 +### 使用 crate 来增加更多功能 -记住 *crate* 是一个 Rust 代码的包。我们正在构建的项目是一个**二进制 crate**,它生成一个可执行文件。 `rand` crate 是一个 *库 crate*,它包含意在被其他程序使用的代码。 +记住 *crate* 是一个 Rust 代码的包。我们正在构建的项目是一个**二进制 crate**,它生成一个可执行文件。 `rand` crate 是一个 **库 crate**,它包含意在被其他程序使用的代码。 Cargo 对外部 crate 的运用是其真正闪光的地方。在我们可以使用`rand`编写代码之前,需要编辑 *Cargo.toml* 来包含`rand`作为一个依赖。现在打开这个文件并在`[dependencies]`部分标题(Cargo 为你创建了它)的下面添加如下代码: @@ -272,11 +266,9 @@ rand = "0.3.14" [semver]: http://semver.org -现在,不用修改任何代码,构建项目,如列表 2-2: +现在,不用修改任何代码,构建项目,如列表 2-2 所示: -
    - -```text +``` $ cargo build Updating registry `https://github.com/rust-lang/crates.io-index` Downloading rand v0.3.14 @@ -286,34 +278,29 @@ $ cargo build Compiling guessing_game v0.1.0 (file:///projects/guessing_game) ``` -
    - -Listing 2-2: The output from running `cargo build` after adding the rand crate -as a dependency - -
    -
    +Listing 2-2: The output from running `cargo build` after +adding the rand crate as a dependency 可能会出现不同的版本号(不过多亏了语义化版本,它们与代码是兼容的!),同时显示顺序也可能会有所不同。 -现在我们有了一个外部依赖,Cargo 从 *registry* ([Crates.io][cratesio])上获取了一份(兼容的)最新版本代码的拷贝。Crates.io 是 Rust 生态环境中的人们向他人贡献他们的开源 Rust 项目的地方。 +现在我们有了一个外部依赖,Cargo 从 *registry* ([Crates.io][cratesio])上获取了一份(兼容的)最新版本代码的拷贝。Crates.io 是 Rust 生态环境中的开发者们向他人贡献他们的开源 Rust 项目的地方。 [cratesio]: https://crates.io -在更新完 registry (索引)后,Cargo 检查`[dependencies]`部分并下载还不存在部分。在这个例子中,虽然只列出了`rand`一个依赖,Cargo 也获取了一份`libc`的拷贝,因为`rand`依赖`libc`来正常工作。在下载他们之后,Rust 编译他们接着用这些依赖编译项目。 +在更新完 registry (索引)后,Cargo 检查`[dependencies]`部分并下载还不存在的部分。在这个例子中,虽然只列出了`rand`一个依赖,Cargo 也获取了一份`libc`的拷贝,因为`rand`依赖`libc`来正常工作。在下载他们之后,Rust 编译他们并接着使用这些依赖编译项目。 如果不做任何修改就立刻再次运行`cargo build`,则不会有任何输出。Cargo 知道它已经下载并编译了依赖,同时 *Cargo.toml* 文件中也没有任何相关修改。Cargo 也知道代码没有做任何修改,所以它也不会重新编译代码。因为无事可做,它简单的退出了。如果打开 *src/main.rs* 文件,并做一些普通的修改,保存并再次构建,只会出现一行输出: -```sh +``` $ cargo build Compiling guessing_game v0.1.0 (file:///projects/guessing_game) ``` 这一行表明 Cargo 只构建了对 *src/main.rs* 文件做出的微小修改。依赖没有被修改,所以 Cargo 知道可以复用已经为此下载并编译的代码。它只是重新构建了部分(项目)代码。 -#### The *Cargo.lock* 文件确保构建是可重现的 +#### *Cargo.lock* 文件确保构建是可重现的 -Cargo 有一个机制来确保每次任何人重新构建代码都会生成相同的成品:Cargo 只会使用你指定的依赖的版本,除非你又手动指定了别的。例如,如果下周`rand` crate 的`v0.3.15`版本出来了,而它包含一个重要的 bug 修改并也含有一个会破坏代码运行的缺陷的时候会发生什么呢? +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* 文件。我们将会在这个文件编写全部的代码。 @@ -326,7 +313,7 @@ Cargo 有一个机制来确保每次任何人重新构建代码都会生成相 不过,Cargo 默认只会寻找大于`0.3.0`而小于`0.4.0`的版本。如果`rand` crate 发布了两个新版本,`0.3.15`和`0.4.0`,在运行`cargo update`时会出现如下内容: -```sh +``` $ cargo update Updating registry `https://github.com/rust-lang/crates.io-index` Updating rand v0.3.14 -> v0.3.15 @@ -351,9 +338,8 @@ rand = "0.4.0" ### 生成一个随机数 -让我们开始**使用**`rand`。下一步是更新 *src/main.rs*,如列表 2-3: +让我们开始**使用**`rand`。下一步是更新 *src/main.rs*,如列表 2-3 所示: -
    Filename: src/main.rs ```rust,ignore @@ -380,12 +366,8 @@ fn main() { } ``` -
    - -Listing 2-3: Code changes needed in order to generate a random number - -
    -
    +Listing 2-3: Code changes needed in order to generate a +random number 我们在顶部增加一行`extern crate rand;`来让 Rust 知道我们要使用外部依赖。这也会调用相应的`use rand`,所以现在可以使用`rand::`前缀来调用`rand`中的任何内容。 @@ -393,13 +375,13 @@ Listing 2-3: Code changes needed in order to generate a random number 另外,中间还新增加了两行。`rand::thread_rng`函数会提供具体会使用的随机数生成器:它位于当前执行线程本地并从操作系统获取 seed。接下来,调用随机数生成器的`gen_range`方法。这个方法由我们使用`use rand::Rng`语句引入到作用域的`Rng` trait 定义。`gen_range`方法获取两个数作为参数并生成一个两者之间的随机数。它包含下限但不包含上限,所以需要指定`1`和`101`来请求一个`1`和`100`之间的数。 -并不仅仅能够知道该 use 哪个 trait 和该从 crate 中调用哪个方法。如何使用 crate 的说明在每个 crate 的文档中。Cargo 另一个很棒的功能是可以运行`cargo doc --open`命令来构建所有本地依赖提供的文档并在浏览器中打开。例如,如果你对`rand` crate 中的其他功能感兴趣,运行`cargo doc --open`并点击左侧导航栏的`rand`。 +并不是仅仅能够**知道**该 use 哪个 trait 和该从 crate 中调用哪个方法。如何使用 crate 的说明位于每个 crate 的文档中。Cargo 另一个很棒的功能是可以运行`cargo doc --open`命令来构建所有本地依赖提供的文档并在浏览器中打开。例如,如果你对`rand` crate 中的其他功能感兴趣,运行`cargo doc --open`并点击左侧导航栏中的`rand`。 新增加的第二行代码打印出了秘密数字。这在开发程序时很有用,因为我们可以去测试它,不过在最终版本我们会删掉它。游戏一开始就打印出结果就没什么可玩的了! 尝试运行程序几次: -```sh +``` $ cargo run Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Running `target/debug/guessing_game` @@ -421,9 +403,8 @@ You guessed: 5 ## 比较猜测与秘密数字 -现在有了用户输入和一个随机数,我们可以比较他们。这个步骤如列表 2-4: +现在有了用户输入和一个随机数,我们可以比较他们。这个步骤如列表 2-4 所示: -
    Filename: src/main.rs ```rust,ignore @@ -457,12 +438,8 @@ fn main() { } ``` -
    - -Listing 2-4: Handling the possible return values of comparing two numbers - -
    -
    +Listing 2-4: Handling the possible return values of +comparing two numbers 新代码的第一行是另一个`use`,从标准库引入了一个叫做`std::cmp::Ordering`的类型到作用域。`Ordering`是另一个枚举,像`Result`一样,不过`Ordering`的成员是`Less`、`Greater`和`Equal`。这是你比较两个值时可能出现三种结果。 @@ -480,13 +457,13 @@ match guess.cmp(&secret_number) { [match]: ch06-02-match.html -一个`match`表达式由 **分支(arms)** 构成。一个分支包含一个 **模式**(*pattern*)和代码,这些代码在`match`表达式开头给出的值符合分支的模式时将被执行。Rust 获取提供给`match`的值并挨个检查每个分支的模式。`match`结构和模式是 Rust 中非常强大的功能,它帮助你体现代码可能遇到的多种情形并帮助你处理全部的可能。这些功能将分别在第六章和第十九章详细介绍。 +一个`match`表达式由 **分支(arms)** 构成。一个分支包含一个 **模式**(*pattern*)和代码,这些代码在`match`表达式开头给出的值符合分支的模式时将被执行。Rust 获取提供给`match`的值并挨个检查每个分支的模式。`match`结构和模式是 Rust 中非常强大的功能,它帮助你体现代码可能遇到的多种情形并帮助你处理全部的可能。这些功能将分别在第六章和第十八章详细介绍。 让我们看看一个使用这里的`match`表达式会发生什么的例子。假设用户猜了 50,这时随机生成的秘密数字是 38。当代码比较 50 与 38 时,`cmp`方法会返回`Ordering::Greater`,因为 50 比 38 要大。`Ordering::Greater`是`match`表达式得到的值。它检查第一个分支的模式,`Ordering::Less`,不过值`Ordering::Greater`并不匹配`Ordering::Less`。所以它忽略了这个分支的代码并移动到下一个分支。下一个分支的模式,`Ordering::Greater`,**正确**匹配了`Ordering::Greater`!这个分支关联的代码会被执行并在屏幕打印出`Too big!`。`match`表达式就此终止,因为在这个特定场景下没有检查最后一个分支的必要。 -然而,列表 2-4 的代码并不能编译,尝试一下: +然而,列表 2-4 的代码并不能编译,可以尝试一下: -```sh +``` $ cargo build Compiling guessing_game v0.1.0 (file:///projects/guessing_game) error[E0308]: mismatched types @@ -502,7 +479,7 @@ 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 不会比较字符串类型和数字类型。 +错误的核心表明这里有**不匹配的类型**(*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`函数体中增加如下两行代码来实现: @@ -555,13 +532,13 @@ let guess: u32 = guess.trim().parse() [字符串的`parse`方法][parse]解析一个字符串成某个数字。因为这个方法可以解析多种数字类型,需要告诉 Rust 我们需要的具体的数字类型,这里通过`let guess: u32`指定。`guess`后面的冒号(`:`)告诉 Rust 我们指明了变量的类型。Rust 有一些内建的数字类型;这里的`u32`是一个无符号的 32 位整型。它是一个好的较小正整数的默认类型。第三章会讲到其他数字类型。另外,例子程序中的`u32`注解和与`secret_number`的比较意味着 Rust 会推断`secret_number`应该是也是`u32`类型。现在可以使用相同类型比较两个值了! -[parse]: ../std/primitive.str.html#method.parse +[parse]: https://doc.rust-lang.org/std/primitive.str.html#method.parse -`parse`调用容易产生错误。例如,如果字符串包含`A👍%`,就无法将其转换为一个数字。因为它可能失败,`parse`方法返回一个`Result`类型,非常像之前在 XX 页“使用`Result`类型来处理潜在的错误”部分讨论的`read_line`方法。这里再次类似的使用`expect`方法处理这个`Result`类型。如果`parse`因为不能从字符串生成一个数字而返回一个`Err`的`Result`成员时,`expect`会使游戏崩溃并打印提供给它的信息。如果`parse`能成功地将字符串转换为一个数字,它会返回`Result`的`Ok`成员,同时`expect`会返回`Ok`中我们需要的数字。 +`parse`调用容易产生错误。例如,如果字符串包含`A👍%`,就无法将其转换为一个数字。因为它可能失败,`parse`方法返回一个`Result`类型,非常像之前在“使用`Result`类型来处理潜在的错误”部分讨论的`read_line`方法。这里再次类似的使用`expect`方法处理这个`Result`类型。如果`parse`因为不能从字符串生成一个数字而返回一个`Err`的`Result`成员时,`expect`会使游戏崩溃并打印提供给它的信息。如果`parse`能成功地将字符串转换为一个数字,它会返回`Result`的`Ok`成员,同时`expect`会返回`Ok`中我们需要的数字。 现在让我们运行程序! -```sh +``` $ cargo run Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Running `target/guessing_game` @@ -621,9 +598,9 @@ fn main() { 如上所示,我们将提示用户猜测之后的所有内容放入了循环。确保这些代码多缩进了四个空格,并再次运行程序。注意这里有一个新问题,因为程序忠实地执行了我们要求它做的:永远地请求另一个猜测!看起来用户没法退出啊! -用户总是可以使用`Ctrl-C`快捷键来终止程序。不过这里还有另一个逃离这个贪得无厌的怪物的方法,就是在 XX 页“比较猜测”部分提到的`parse`:如果用户输入一个非数字回答,程序会崩溃。用户可以利用这一点来退出,如下所示: +用户总是可以使用`Ctrl-C`快捷键来终止程序。不过这里还有另一个逃离这个贪得无厌的怪物的方法,就是在“比较猜测”部分提到的`parse`:如果用户输入一个非数字回答,程序会崩溃。用户可以利用这一点来退出,如下所示: -```sh +``` $ cargo run Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Running `target/guessing_game` @@ -648,7 +625,7 @@ note: Run with `RUST_BACKTRACE=1` for a backtrace. error: Process didn't exit successfully: `target/debug/guess` (exit code: 101) ``` -输入`quit`就会退出程序,同时其他任何非数字输入也一样。然而,毫不夸张的说这是不理想的。我们想要当猜测正确的数字时游戏能自动退出。 +输入`quit`确实退出了程序,同时其他任何非数字输入也一样。然而,毫不夸张的说这是不理想的。我们想要当猜测正确的数字时游戏能自动退出。 ### 猜测正确后退出 @@ -716,7 +693,7 @@ let guess: u32 = match guess.trim().parse() { 现在万事俱备(只欠东风)了。运行`cargo run`来尝试一下: -```sh +``` $ cargo run Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Running `target/guessing_game` @@ -740,7 +717,6 @@ You win! 太棒了!再有最后一个小的修改,就能完成猜猜看游戏了:还记得程序依然会打印出秘密数字。这在测试时还好,但会毁了游戏性。删掉打印秘密数字的`println!`。列表 2-5 为最终代码: -
    Filename: src/main.rs ```rust,ignore @@ -782,15 +758,10 @@ fn main() { } ``` -
    +Listing 2-5: Complete code of the guessing game -Listing 2-5: Complete code of the guessing game - -
    -
    - -## 总结一下, +## 总结 此时此刻,你顺利完成了猜猜看游戏!恭喜! -这是一个通过动手实践的方式想你介绍许多 Rust 新知识的项目:`let`、`match`、方法、关联函数,使用外部 crate,等等。接下来的几章,我们将会详细学习这些概念。第三章涉及到大部分编程语言都有的概念,比如变量、数据类型和函数,以及如何在 Rust 中使用他们。第四章探索所有权(ownership),这是一个 Rust 同其他语言都不相同的功能。第五章讨论结构体和方法语法,而第六章侧重解释枚举。 \ No newline at end of file +这是一个通过动手实践的方式想你介绍许多 Rust 新知识的项目:`let`、`match`、方法、关联函数,使用外部 crate,等等。接下来的几章,我们将会详细学习这些概念。第三章涉及到大部分编程语言都有的概念,比如变量、数据类型和函数,以及如何在 Rust 中使用他们。第四章探索所有权(ownership),这是一个 Rust 同其他语言都不相同的功能。第五章讨论结构体和方法的语法,而第六章侧重解释枚举。 \ No newline at end of file diff --git a/src/ch03-00-common-programming-concepts.md b/src/ch03-00-common-programming-concepts.md index cc07368..a23e9dd 100644 --- a/src/ch03-00-common-programming-concepts.md +++ b/src/ch03-00-common-programming-concepts.md @@ -1,17 +1,13 @@ # 通用编程概念 -> [ch03-00-common-programming-concepts.md](https://github.com/rust-lang/book/blob/master/src/ch03-00-common-programming-concepts.md) +> [ch03-00-common-programming-concepts.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch03-00-common-programming-concepts.md) >
    -> commit 2067b6e2bff990bceb39ae8f35780bd3bed08644 +> commit 04aa3a45eb72855b34213703718f50a12a3eeec8 -这一章涉及到几乎出现在所有编程语言中的概念,以及他们在 Rust 中如何工作。很多编程语言在核心概念上都是共通的。本章中展示的所有概念没有一个是 Rust 所特有的,不过我们会在 Rust 环境中讨论他们并解释他们的使用习惯。 +这一章涉及到几乎出现在所有编程语言中的概念,以及他们在 Rust 中是如何工作的。很多编程语言在核心概念上都是共通的。本章中展示的所有概念没有一个是 Rust 所特有的,不过我们会在 Rust 环境中讨论他们并解释他们的使用习惯。 -具体的,我们将会学习变量,基本类型,函数,注释和控制流。这些基础知识将会出现在每一个 Rust 程序中,提早学习这些概念会使你在起步时拥有一个核心的基础。 - - +具体的,我们将会学习变量,基本类型,函数,注释和控制流。这些基础知识将会出现在每一个 Rust 程序中,提早学习这些概念会使你在起步时拥有一个强有力的基础。 > ### 关键字 > > Rust 语言有一系列被保留为只能被语言使用的**关键字**(*keywords*),如大部分语言一样。注意你不能使用这些关键字作为变量或函数的名称。大部分关键字有特殊的意义,并将会被用来进行 Rust 程序中的多种任务;一些关键字目前没有相关的功能不过为了将来可能添加进 Rust 的功能而被保留。可以在附录 A 中找到一份关键字的列表 - - \ No newline at end of file diff --git a/src/ch03-01-variables-and-mutability.md b/src/ch03-01-variables-and-mutability.md index c84b7d1..e3b543f 100644 --- a/src/ch03-01-variables-and-mutability.md +++ b/src/ch03-01-variables-and-mutability.md @@ -1,12 +1,12 @@ ## 变量和可变性 -> [ch03-01-variables-and-mutability.md](https://github.com/rust-lang/book/blob/master/src/ch03-01-variables-and-mutability.md) +> [ch03-01-variables-and-mutability.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch03-01-variables-and-mutability.md) >
    -> commit b0fab378c9c6a817d4f0080d7001d085017cdef8 +> commit 04aa3a45eb72855b34213703718f50a12a3eeec8 -第二章中提到过,变量默认是**不可变**(*immutable*)的。这是 Rust 中许多鼓励以利用 Rust 提供的安全和简单并发优势编写代码的助力之一。不过,仍然有使变量可变的选项。让我们探索一下为什么以及如何鼓励你拥抱不可变性,还有为什么你可能想要弃之不用。 +第二章中提到过,变量默认是**不可变**(*immutable*)的。这是 Rust 中许多鼓励利用 Rust 提供的安全和简单并发优势编写代码的助力之一。不过,仍然有使变量可变的选项。让我们探索一下如何以及为什么鼓励你拥抱不可变性,还有为什么你可能想要弃之不用。 -当变量使不可变时,这意味着一旦一个值被绑定上了一个名称,你就不能改变这个值。作为说明,通过`cargo new --bin variables`在 *projects* 目录生成一个叫做 *variables* 的新项目。 +当变量是不可变时,这意味着一旦一个值被绑定上了一个名称,你就不能改变这个值。作为说明,通过`cargo new --bin variables`在 *projects* 目录生成一个叫做 *variables* 的新项目。 接着,在新建的 *variables* 目录,打开 *src/main.rs* 并替换其代码为如下: @@ -23,7 +23,7 @@ fn main() { 保存并使用`cargo run`运行程序。应该会看到一个错误信息,如下输出所示: -```sh +``` $ cargo run Compiling variables v0.0.1 (file:///projects/variables) error[E0384]: re-assignment of immutable variable `x` @@ -38,7 +38,7 @@ error[E0384]: re-assignment of immutable variable `x` 这个例子显示了编译器如何帮助你寻找程序中的错误。即便编译器错误可能是令人沮丧的,他们也仅仅意味着程序不能安全的完成你想让它完成的工作;他们**不能**说明你不是一个好的程序员!有经验的 Rustacean 们也会遇到编译器错误。这些错误表明错误的原因是`对不可变变量重新赋值`(`re-assignment of immutable variable`),因为我们尝试对不可变变量`x`赋第二个值。 -当尝试去改变之前设计为不可变的值出现编译时错误是很重要的,因为这种情况可能导致 bug。如果代码的一部分假设一个值永远也不会改变而另一部分代码改变了它,这样第一部分代码就有可能不能像它设计的那样运行。你必须承认这种 bug 难以跟踪,尤其是当第二部分代码只是**有时**当变量使不可变时,这意味着一旦一个值被绑定上了一个名称,你就不能改变这个值。 +当尝试去改变之前设计为不可变的值出现编译时错误是很重要的,因为这种情况可能导致 bug。如果代码的一部分假设一个值永远也不会改变而另一部分代码改变了它,这样第一部分代码就有可能不能像它设计的那样运行。不得不承认这种 bug 难以跟踪,尤其是当第二部分代码只是**有时**当变量使不可变时。 Rust 编译器保证如果声明一个值不会改变,它就真的不会改变。这意味着当阅读和编写代码时,并不需要记录如何以及在哪可能会被改变,这使得代码易于推导。 @@ -59,7 +59,7 @@ fn main() { 当运行这个程序,出现如下: -```sh +``` $ cargo run Compiling variables v0.1.0 (file:///projects/variables) Running `target/debug/variables` @@ -67,25 +67,33 @@ The value of x is: 5 The value of x is: 6 ``` -通过`mut`,允许把绑定到`x`的值从`5`改成`6`。在一些情况下,你会想要使一个变量可变,因为这比只使用不可变变量实现的代码更易于编写。 +通过`mut`,允许把绑定到`x`的值从`5`改成`6`。在一些情况下,你会想要一个变量是可变的,因为这比只使用不可变变量实现的代码更易于编写。 -除了避免 bug 外,这里还有数个需要权衡取舍的地方。例如,有时使用大型数据结构时,适当地使变量可变可能比复制和返回新分配的实例要更快。对于较小的数据结构,总是创建新实例并采用一种更函数式的编程风格可能会使代码更易理解。所以为了可读性而造成的性能惩罚也许使值得的。 +除了避免 bug 外,这里还有多个需要权衡取舍的地方。例如,有时使用大型数据结构时,适当地使变量可变可能比复制和返回新分配的实例要更快。对于较小的数据结构,总是创建新实例并采用一种更函数式的编程风格可能会使代码更易理解。所以为了可读性而造成的性能惩罚也许是值得的。 ### 变量和常量的区别 -不能改变一个变量的值可能会使你想起另一个大部分编程语言都有的概念:**常量**(*constants*)。常量也是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。首先,不允许对常量使用`mut`:常量不光是默认不能改变,它总是不能改变。常量使用`const`关键字而不是`let`关键字声明,而且*必须*注明值的类型。现在我们准备在下一部分,“数据类型”,涉及到类型和类型注解,所以现在无需担心这些细节。常量可以在任何作用域声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。最后一个区别是常量只能用于常量表达式,而不能作为函数调用的结果或任何其他只在运行时使用到的值。 +不能改变一个变量的值可能会使你想起另一个大部分编程语言都有的概念:**常量**(*constants*)。类似于不可变变量,常量也是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。 -这是一个常量声明的例子,它的名称是`MAX_POINTS`而它的值是 100,000。Rust 常量的命名规范是使用大写字母和单词间使用下划线: +首先,不允许对常量使用`mut`:常量不光是默认不能改变,它总是不能改变。 + +常量使用`const`关键字而不是`let`关键字声明,而且*必须*注明值的类型。现在我们准备在下一部分,“数据类型”,涉及到类型和类型注解,所以现在无需担心这些细节,只需记住必须总是注明类型即可。 + +常量可以在任何作用域声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。 + +最后一个区别是常量只能用于常量表达式,而不能作为函数调用的结果或任何其他只在运行时计算的值。 + +这是一个常量声明的例子,它的名称是`MAX_POINTS`而它的值是 100,000。(Rust 常量的命名规范是使用大写字母和单词间使用下划线): ```rust const MAX_POINTS: u32 = 100_000; ``` -常量在整个程序生命周期中都有效,位于它声明的作用域之中。这使得常量可以用作多个部分的代码可能需要知道的程序范围的值,例如一个游戏中任何玩家可以获得的最高分或者一年的秒数。 +常量在整个程序生命周期中都有效,位于它声明的作用域之中。这使得常量可以用作多个部分的代码可能需要知道的程序范围的值,例如一个游戏中任何玩家可以获得的最高分或者光速。 -将用于整个程序的硬编码的值命名为常量(并编写文档)对为将来代码维护者表明值的意义是很有用的。它也能帮助你将硬编码的值至于一处以便将来可能需要修改他们。 +将用于整个程序的硬编码的值声明为为常量(并编写文档)对为将来代码维护者表明值的意义是很有用的。它也能帮助你将硬编码的值至于一处以便将来可能需要修改他们。 -### 覆盖 +### 覆盖(Shadowing) 如第二章猜猜看游戏所讲到的,我们可以定义一个与之前变量名称相同的新变量,而新变量会**覆盖**之前的变量。Rustacean 们称其为第一个变量被第二个**给覆盖**了,这意味着第二个变量的值是使用这个变量时会看到的值。可以用相同变量名称来覆盖它自己以及重复使用`let`关键字来多次覆盖,如下所示: @@ -105,7 +113,7 @@ fn main() { 这个程序首先将`x`绑定到值`5`上。接着通过`let x =`覆盖`x`,获取原始值并加`1`这样`x`的值就变成`6`了。第三个`let`语句也覆盖了`x`,获取之前的值并乘以`2`,`x`的最终值是`12`。当运行这个程序,它会有如下输出: -```sh +``` $ cargo run Compiling variables v0.1.0 (file:///projects/variables) Running `target/debug/variables` @@ -129,7 +137,7 @@ spaces = spaces.len(); 会导致一个编译时错误,因为不允许改变一个变量的类型: -```sh +``` error[E0308]: mismatched types --> src/main.rs:3:14 | @@ -140,4 +148,4 @@ error[E0308]: mismatched types = note: found type `usize` ``` -现在我们探索了变量如何工作,让我们看看他们能有多少数据类型。 \ No newline at end of file +现在我们探索了变量如何工作,让我们看看更多的数据类型。 \ No newline at end of file diff --git a/src/ch03-02-data-types.md b/src/ch03-02-data-types.md index 4ec5967..ee07ff6 100644 --- a/src/ch03-02-data-types.md +++ b/src/ch03-02-data-types.md @@ -1,10 +1,10 @@ ## 数据类型 -> [ch03-02-data-types.md](https://github.com/rust-lang/book/blob/master/src/ch03-02-data-types.md) +> [ch03-02-data-types.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch03-02-data-types.md) >
    -> commit 6436ebee2a84820adf77231cead6b5691c8e2744 +> commit 04aa3a45eb72855b34213703718f50a12a3eeec8 -Rust 中的任何值都有一个具体的**类型**(*type*),这告诉了 Rust 它被指定为何种数据这样 Rust 就知道如何处理这些数据了。这一部分将讲到一些语言内建的类型。我们将这些类型分为两个子集:标量(scalar)和复合(compound)。 +Rust 中的任何值都有一个具体的**类型**(*type*),这告诉了 Rust 它被指定了何种数据,这样 Rust 就知道如何处理这些数据了。这一部分将讲到一些语言内建的类型。我们将这些类型分为两个子集:标量(scalar)和复合(compound)。 贯穿整个部分,请记住 Rust 是一个**静态类型**(*statically typed*)语言,也就是说必须在编译时就知道所有变量的类型。编译器通常可以通过值以及如何使用他们来推断出我们想要用的类型。当多个类型都是可能的时候,比如第二章中`parse`将`String`转换为数字类型,必须增加类型注解,像这样: @@ -14,12 +14,12 @@ let guess: u32 = "42".parse().expect("Not a number!"); 如果这里不添加类型注解,Rust 会显示如下错误,它意味着编译器需要我们提供更多我们想要使用哪个可能的类型的信息: -```sh +``` error[E0282]: unable to infer enough type information about `_` - --> src/main.rs:2:5 + --> src/main.rs:2:9 | -2 | let guess = "42".parse().expect("Not a number!"); - | ^^^^^ cannot infer type for `_` +2 | let guess = "42".parse().expect("Not a number!"); + | ^^^^^ cannot infer type for `_` | = note: type annotations or generic parameter binding required ``` @@ -34,12 +34,7 @@ error[E0282]: unable to infer enough type information about `_` **整数**是一个没有小数部分的数字。我们在这一章的前面使用过一个整型,`i32`类型。这个类型声明表明在 32 位系统上它关联的值应该是一个有符号整数(因为这个`i`,与`u`代表的无符号相对)。表格 3-1 展示了 Rust 内建的整数类型。每一个变体的有符号和无符号列(例如,*i32*)可以用来声明对应的整数值。 -
    -
    - -Table 3-1: Integer Types in Rust - -
    +Table 3-1: Integer Types in Rust | Length | Signed | Unsigned | |--------|--------|----------| @@ -49,8 +44,6 @@ Table 3-1: Integer Types in Rust | 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。 @@ -59,12 +52,7 @@ Table 3-1: Integer Types in Rust 可以使用表格 3-2 中的任何一种形式编写数字字面值。注意除了字节字面值以外的数字字面值允许使用类型后缀,例如`57u8`,而`_`是可视化分隔符(visual separator),例如`1_000`位的。 -
    -
    - -Table 3-2: Integer Literals in Rust - -
    +Table 3-2: Integer Literals in Rust | Number literals | Example | |------------------|---------------| @@ -74,13 +62,11 @@ Table 3-2: Integer Literals in Rust | Binary | `0b1111_0000` | | Byte (`u8` only) | `b'A'` | -
    - 那么如何知晓该使用哪种类型的数字呢?如果对此拿不定主意,Rust 的默认类型通常就是一个很好的选择,这个默认数字类型是`i32`:它通常是最快的,甚至是在 64 位系统上。使用`isize`或`usize`的主要场景是索引一些集合。 #### 浮点型 -Rust 也有两个主要的**浮点数**(*floating-point numbers*)类型,他们是有小数点的数字。Rust 的浮点数类型是`f32`和`f64`,分别是 32 位 和 64 位大小。默认类型是`f64`,因为它基本上与`f32`一样快不过精度更高。在 32 位系统上使用`f64`是可能的,不过会比`f32`要慢。大部分情况,牺牲潜在可能的更低性能来换取更高的精度是一个合理的首要选择,同时如果怀疑浮点数的大小有问题的时候应该对代码进行性能测试。 +Rust 也有两个主要的**浮点数**(*floating-point numbers*)类型,他们是有小数点的数字。Rust 的浮点数类型是`f32`和`f64`,分别是 32 位 和 64 位大小。默认类型是`f64`,因为它基本上与`f32`一样快不过精度更高。在 32 位系统上使用`f64`是可能的,不过会比`f32`要慢。大部分情况,牺牲潜在可能的更低性能来换取更高的精度是一个合理的初始选择,同时如果怀疑浮点数的大小有问题的时候应该先对代码进行性能测试。 这是一个展示浮点数的实例: @@ -190,7 +176,7 @@ fn main() { 程序首先创建了一个元组并绑定到`tup`变量上。接着使用了`let`和一个模式将`tup`分成了三个不同的变量,`x`、`y`和`z`。这叫做*解构*(*destructuring*),因为它将一个元组拆成了三个部分。最后,程序打印出了`y`的值,也就是`6.4`。 -除了使用模式匹配解构之外,也可以使用点号(`.`)后跟值的索引来直接访问。例如: +除了使用模式匹配解构之外,也可以使用点号(`.`)后跟值的索引来直接访问他们。例如: Filename: src/main.rs @@ -266,7 +252,7 @@ fn main() { 使用`cargo run`运行代码后会产生如下结果: -```sh +``` $ cargo run Compiling arrays v0.1.0 (file:///projects/arrays) Running `target/debug/arrays` diff --git a/src/ch03-03-how-functions-work.md b/src/ch03-03-how-functions-work.md index f577fe1..5a2596d 100644 --- a/src/ch03-03-how-functions-work.md +++ b/src/ch03-03-how-functions-work.md @@ -1,8 +1,8 @@ ## 函数如何工作 -> [ch03-03-how-functions-work.md](https://github.com/rust-lang/book/blob/master/src/ch03-03-how-functions-work.md) +> [ch03-03-how-functions-work.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch03-03-how-functions-work.md) >
    -> commit 52b7fcbfdd35915cb21e6d492fb6c86764f53b47 +> commit 04aa3a45eb72855b34213703718f50a12a3eeec8 函数在 Rust 代码中应用广泛。你已经见过一个语言中最重要的函数:`main`函数,它时很多程序的入口点。你也见过了`fn`关键字,它用来声明新函数。 @@ -24,11 +24,11 @@ fn another_function() { Rust 中的函数定义以`fn`开始并在函数名后跟一对括号。大括号告诉编译器哪里是函数体的开始和结尾。 -可以使用定义过的函数名后跟括号来调用任意函数。因为`another_function`在程序中已经定义过了,它可以在`main`函数中被调用。注意,源码中`another_function`在`main`函数*之后*被定义;也可以在之前定义。Rust 不关心函数定义于何处,只要他们被定义了。 +可以使用定义过的函数名后跟括号来调用任意函数。因为`another_function`已经在程序中定义过了,它可以在`main`函数中被调用。注意,源码中`another_function`在`main`函数*之后*被定义;也可以在其之前定义。Rust 不关心函数定义于何处,只要他们被定义了。 -让我们开始一个叫做*functions*的新二进制项目来进一步探索函数。将上面的`another_function`例子写入 *src/main.rs* 中并运行。你应该会看到如下输出: +让我们开始一个叫做 *functions* 的新二进制项目来进一步探索函数。将上面的`another_function`例子写入 *src/main.rs* 中并运行。你应该会看到如下输出: -```sh +``` $ cargo run Compiling functions v0.1.0 (file:///projects/functions) Running `target/debug/functions` @@ -40,7 +40,7 @@ Another function. ### 函数参数 -函数也可以被定义为拥有**参数**(*parameters*),他们是作为函数签名一部分的特殊变量。当函数拥有参数,可以为这些参数提供具体的值。技术上讲,这些具体值被称为参数( *arguments*),不过通常的习惯是倾向于在函数定义中的变量和调用函数时传递的具体值都可以用 "parameter" 和 "argument" 而不加区别。 +函数也可以被定义为拥有**参数**(*parameters*),他们是作为函数签名一部分的特殊变量。当函数拥有参数,可以为这些参数提供具体的值。技术上讲,这些具体值被称为参数( *arguments*),不过通常的习惯是倾向于在函数定义中的变量和调用函数时传递的具体值都可以用“parameter”和“argument”而不加区别。 如下被重写的`another_function`版本展示了 Rust 中参数是什么样的: @@ -85,7 +85,7 @@ fn another_function(x: i32, y: i32) { } ``` -这个例子创建了一个有两个参数的函数,都是`i32`类型的。函数打印出了这两个参数的值。注意函数参数并一定都是相同的————这个例子中他们只是碰巧相同。 +这个例子创建了一个有两个参数的函数,都是`i32`类型的。函数打印出了这两个参数的值。注意函数参数并不一定都是相同类型的,这个例子中他们只是碰巧相同罢了。 尝试运行代码。使用上面的例子替换当前 *function* 项目的 *src/main.rs* 文件,并`cargo run`运行它: @@ -97,20 +97,18 @@ The value of x is: 5 The value of y is: 6 ``` -因为我们使用`5`作为`x`的值和`6`作为`y`的值来调用函数,这两个字符串使用这些值并被打印出来。 +因为我们使用`5`作为`x`的值和`6`作为`y`的值来调用函数,这两个字符串和他们的值并被打印出来。 ### 函数体 函数体由一系列的语句和一个可选的表达式构成。目前为止,我们只涉及到了没有结尾表达式的函数,不过我们见过表达式作为了语句的一部分。因为 Rust 是一个基于表达式(expression-based)的语言,这是一个需要理解的(不同于其他语言)重要区别。其他语言并没有这样的区别,所以让我们看看语句与表达式有什么区别以及他们是如何影响函数体的。 - ### 语句与表达式 我们已经用过语句与表达式了。**语句**(*Statements*)是执行一些操作但不返回值的指令。表达式(*Expressions*)计算并产生一个值。让我们看看一些例子: 使用`let`关键字创建变量并绑定一个值是一个语句。在列表 3-3 中,`let y = 6;`是一个语句: -
    Filename: src/main.rs ```rust @@ -119,12 +117,7 @@ fn main() { } ``` -
    - -Listing 3-3: A `main` function declaration containing one statement. - -
    -
    +Listing 3-3: A `main` function declaration containing one statement. 函数定义也是语句;上面整个例子本身就是一个语句。 @@ -140,7 +133,7 @@ fn main() { 当运行这个程序,会得到如下错误: -```sh +``` $ cargo run Compiling functions v0.1.0 (file:///projects/functions) error: expected expression, found statement (`let`) @@ -154,7 +147,7 @@ error: expected expression, found statement (`let`) `let y = 6`语句并不返回值,所以并没有`x`可以绑定的值。这与其他语言不同,例如 C 和 Ruby,他们的赋值语句返回所赋的值。在这些语言中,可以这么写`x = y = 6`这样`x`和`y`的值都是`6`;这在 Rust 中可不行。 -表达式进行计算而且他们组成了其余大部分 Rust 代码。考虑一个简单的数学运算,比如`5 + 6`,这是一个表达式并计算出值`11`。表达式可以是语句的一部分:在列表 3-3 中有这个语句`let y = 6;`,`6`是一个表达式它计算出的值是`6`。函数调用是一个表达式。宏调用是一个表达式。我们用来创新建作用域的大括号(代码块),`{}`,也是一个表达式,例如: +表达式计算出一些值,而且他们组成了其余大部分你将会编写的 Rust 代码。考虑一个简单的数学运算,比如`5 + 6`,这是一个表达式并计算出值`11`。表达式可以是语句的一部分:在列表 3-3 中有这个语句`let y = 6;`,`6`是一个表达式它计算出的值是`6`。函数调用是一个表达式。宏调用是一个表达式。我们用来创新建作用域的大括号(代码块),`{}`,也是一个表达式,例如: Filename: src/main.rs @@ -200,9 +193,9 @@ fn main() { } ``` -在函数`five`中并没有函数调用、宏、甚至也没有`let`语句————只有数字`5`它子集。这在 Rust 中是一个完全有效的函数。注意函数的返回值类型也被指定了,就是`-> i32`。尝试运行代码;输出应该看起来像这样: +在函数`five`中并没有函数调用、宏、甚至也没有`let`语句————只有数字`5`它自己。这在 Rust 中是一个完全有效的函数。注意函数的返回值类型也被指定了,就是`-> i32`。尝试运行代码;输出应该看起来像这样: -```sh +``` $ cargo run Compiling functions v0.1.0 (file:///projects/functions) Running `target/debug/functions` @@ -215,7 +208,7 @@ The value of x is: 5 let x = 5; ``` -再次,函数`five`没有参数并定义了返回值类型,不过函数体只有单单一个`5`也没有分号,因为这是我们想要返回值的表达式。让我们看看另一个例子: +其次,函数`five`没有参数并定义了返回值类型,不过函数体只有单单一个`5`也没有分号,因为这是我们想要返回值的表达式。让我们看看另一个例子: Filename: src/main.rs @@ -233,6 +226,8 @@ fn plus_one(x: i32) -> i32 { 运行代码会打印出`The value of x is: 6`。如果在包含`x + 1`的那一行的结尾加上一个分号,把它从表达式变成语句后会怎样呢? +Filename: src/main.rs + ```rust,ignore fn main() { let x = plus_one(5); @@ -247,13 +242,18 @@ fn plus_one(x: i32) -> i32 { 运行代码会产生一个错误,如下: -```sh -error[E0269]: not all control paths return a value - --> src/main.rs:7:1 +``` +error[E0308]: mismatched types + --> src/main.rs:7:28 | -7 | fn plus_one(x: i32) -> i32 { - | ^ +7 | fn plus_one(x: i32) -> i32 { + | ____________________________^ starting here... +8 | | x + 1; +9 | | } + | |_^ ...ending here: expected i32, found () | + = note: expected type `i32` + found type `()` help: consider removing this semicolon: --> src/main.rs:8:10 | @@ -261,4 +261,4 @@ help: consider removing this semicolon: | ^ ``` -主要的错误信息,“并非所有控制路径都返回一个值”(“not all control paths return a value,”),揭示了代码的核心问题。函数`plus_one`的定义说明它要返回一个`i32`,不过语句并不返回一个值。因此,这个函数没有返回任何值,这与函数定义相矛盾并导致一个错误。在输出中,Rust 提供了一个可能会对修正问题有帮助的信息:它建议去掉分号,这会修复这个错误。 \ No newline at end of file +主要的错误信息,“mismatched types,”(类型不匹配),揭示了代码的核心问题。函数`plus_one`的定义说明它要返回一个`i32`,不过语句并不返回一个值,这由那个空元组`()`表明。因此,这个函数没有返回任何值,这与函数定义相矛盾并导致一个错误。在输出中,Rust 提供了一个可能会对修正问题有帮助的信息:它建议去掉分号,这会修复这个错误。 \ No newline at end of file diff --git a/src/ch03-04-comments.md b/src/ch03-04-comments.md index 601b3d8..829a919 100644 --- a/src/ch03-04-comments.md +++ b/src/ch03-04-comments.md @@ -1,10 +1,10 @@ ## 注释 -> [ch03-04-comments.md](https://github.com/rust-lang/book/blob/master/src/ch03-04-comments.md) +> [ch03-04-comments.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch03-04-comments.md) >
    -> commit 74d6fc999b986b74bf94edd6dcbb5a08a16c12de +> commit 4f2dc564851dc04b271a2260c834643dfd86c724 -所有编程语言都力求使他们的代码易于理解,不过有时额外的解释需要得到保障。在这种情况下,程序员在源码中留下记录,或者**注释**(*comments*),编译器会忽略他们不过其他阅读代码的人可能会用得上。 +所有编程语言都力求使他们的代码易于理解,不过有时需要提供额外的解释。在这种情况下,程序员在源码中留下记录,或者**注释**(*comments*),编译器会忽略他们不过其他阅读代码的人可能会用得上。 这是一个注释的例子: @@ -20,7 +20,7 @@ // explain what’s going on. ``` -注释也可以在放在包含代码的行的结尾: +注释也可以在放在包含代码的行的末尾: Filename: src/main.rs @@ -30,7 +30,7 @@ fn main() { } ``` -不过你会经常看到他们被以这种格式使用,也就是位于它解释的代码行的上面一行: +不过你会经常看到他们被以这种格式使用,也就是位于它所解释的代码行的上面一行: Filename: src/main.rs diff --git a/src/ch03-05-control-flow.md b/src/ch03-05-control-flow.md index 054e6ef..0c17184 100644 --- a/src/ch03-05-control-flow.md +++ b/src/ch03-05-control-flow.md @@ -2,7 +2,7 @@ > [ch03-05-control-flow.md](https://github.com/rust-lang/book/blob/master/src/ch03-05-control-flow.md) >
    -> commit 784a3ec5e8b9c6bff456ab9f0efd4dabcc180dda +> commit 04aa3a45eb72855b34213703718f50a12a3eeec8 通过条件是不是真来决定是否某些代码,或者根据条件是否为真来重复运行一段代码是大部分编程语言的基本组成部分。Rust 代码中最常见的用来控制执行流的结构是`if`表达式和循环。 @@ -26,11 +26,13 @@ fn main() { } ``` + + 所有`if`表达式以`if`关键字开头,它后跟一个条件。在这个例子中,条件检查`number`是否有一个小于 5 的值。在条件为真时希望执行的代码块位于紧跟条件之后的大括号中。`if`表达式中与条件关联的代码块有时被叫做 *arms*,就像第二章“比较猜测与秘密数字”部分中讨论到的`match`表达式中分支一样。也可以包含一个可选的`else`表达式,这里我们就这么做了,来提供一个在条件为假时应当执行的代码块。如果不提供`else`表达式并且条件为假时,程序会直接忽略`if`代码块并继续执行下面的代码。 尝试运行代码,应该能看到如下输出: -```sh +``` $ cargo run Compiling branches v0.1.0 (file:///projects/branches) Running `target/debug/branches` @@ -45,7 +47,7 @@ let number = 7; 再次运行程序并查看输出: -```sh +``` $ cargo run Compiling branches v0.1.0 (file:///projects/branches) Running `target/debug/branches` @@ -68,8 +70,7 @@ fn main() { 这里`if`条件的值是`3`,Rust 抛出了一个错误: -```sh - Compiling branches v0.1.0 (file:///projects/branches) +``` error[E0308]: mismatched types --> src/main.rs:4:8 | @@ -77,10 +78,7 @@ error[E0308]: mismatched types | ^^^^^^ expected bool, found integral variable | = note: expected type `bool` - = note: found type `{integer}` - -error: aborting due to previous error -Could not compile `branches`. + found type `{integer}` ``` 这个错误表明 Rust 期望一个`bool`不过却得到了一个整型。Rust 并不会尝试自动地将非布尔值转换为布尔值,不像例如 Ruby 和 JavaScript 这样的语言。必须总是显式地使用`boolean`作为`if`的条件。例如如果想要`if`代码块只在一个数字不等于`0`时执行,可以把`if`表达式修改为如下: @@ -123,7 +121,7 @@ fn main() { 这个程序有四个可能的执行路径。运行后应该能看到如下输出: -```sh +``` $ cargo run Compiling branches v0.1.0 (file:///projects/branches) Running `target/debug/branches` @@ -132,13 +130,12 @@ number is divisible by 3 当执行这个程序,它按顺序检查每个`if`表达式并执行第一个条件为真的代码块。注意即使 6 可以被 2 整除,也不会出现`number is divisible by 2`的输出,更不会出现`else`块中的`number is not divisible by 4, 3, or 2`。原因是 Rust 只会执行第一个条件为真的代码块,并且它一旦找到一个以后,就不会检查剩下的条件了。 -使用过多的`else if`表达式会使代码显得杂乱无章,所以如果有多于一个`else if`,最好重构代码。为此第六章介绍了 Rust 一个叫做`match`的强大的分支结构(branching construct)。 +使用过多的`else if`表达式会使代码显得杂乱无章,所以如果有多于一个`else if`,最好重构代码。为此第六章会介绍 Rust 中一个叫做`match`的强大的分支结构(branching construct)。 #### 在`let`语句中使用`if` -因为`if`是一个表达式,我们可以在`let`语句的右侧使用它,例如列表 3-4: +因为`if`是一个表达式,我们可以在`let`语句的右侧使用它,例如在列表 3-4 中: -
    Filename: src/main.rs ```rust @@ -154,23 +151,19 @@ fn main() { } ``` -
    - -Listing 3-4: Assigning the result of an `if` expression to a variable - -
    -
    +Listing 3-4: Assigning the result of an `if` expression +to a variable `number`变量将会绑定到基于`if`表达式结果的值。运行这段代码看看会出现什么: -```sh +``` $ cargo run Compiling branches v0.1.0 (file:///projects/branches) Running `target/debug/branches` The value of number is: 5 ``` -还记得代码块的值是其最后一个表达式的值,以及数字本身也是一个表达式吗。在这个例子中,整个`if`表达式的值依赖哪个代码块被执行。这意味着`if`的每个分支的可能的返回值都必须是相同类型;在列表 3-4 中,`if`分支和`else`分支的结果都是`i32`整型。不过如果像下面的例子一样这些类型并不相同会怎么样呢? +还记得代码块的值是其最后一个表达式的值,以及数字本身也是一个表达式吗。在这个例子中,整个`if`表达式的值依赖哪个代码块被执行。这意味着`if`的每个分支的可能的返回值都必须是相同类型;在列表 3-4 中,`if`分支和`else`分支的结果都是`i32`整型。不过如果像下面的例子那样这些类型并不匹配会怎么样呢? Filename: src/main.rs @@ -190,19 +183,23 @@ fn main() { 当运行这段代码,会得到一个错误。`if`和`else`分支的值类型是不相容的,同时 Rust 也准确地表明了在程序中的何处发现的这个问题: -```sh - Compiling branches v0.1.0 (file:///projects/branches) +``` error[E0308]: if and else have incompatible types --> src/main.rs:4:18 | -4 | let number = if condition { - | ^ expected integral variable, found reference +4 | let number = if condition { + | __________________^ starting here... +5 | | 5 +6 | | } else { +7 | | "six" +8 | | }; + | |_____^ ...ending here: expected integral variable, found reference | = note: expected type `{integer}` - = note: found type `&’static str` + found type `&'static str` ``` -`if`代码块的表达式返回一个整型,而`else`代码块返回一个字符串。这并不可行因为变量必须只有一个类型。Rust 需要在编译时就确切的知道`number`变量的类型,这样它就可以在编译时证明其他使用`number`变量的地方它的类型是有效的。Rust 并不能够在`number`的类型只能在运行时确定的情况下完成这些功能;这样会使编译器变得更复杂而且只能为代码提供更少的保障,因为它不得不记录所有变量的多种可能的类型。 +`if`代码块的表达式返回一个整型,而`else`代码块返回一个字符串。这并不可行,因为变量必须只有一个类型。Rust 需要在编译时就确切的知道`number`变量的类型,这样它就可以在编译时证明其他使用`number`变量的地方它的类型是有效的。Rust 并不能够在`number`的类型只能在运行时确定的情况下工作;这样会使编译器变得更复杂而且只能为代码提供更少的保障,因为它不得不记录所有变量的多种可能的类型。 ### 使用循环重复执行 @@ -247,7 +244,7 @@ again! 在程序中计算循环的条件也很常见。当条件为真,执行循环。当条件不再为真,调用`break`停止循环。这个循环类型可以通过组合`loop`、`if`、`else`和`break`来实现;如果你喜欢的话,现在就可以在程序中试试。 -然而,这个模式太常见了所以 Rust 为此提供了一个内建的语言结构,它被称为`while`循环。下面的例子使用了`while`:程序循环三次,每次数字都减一。接着,在循环之后,打印出另一个信息并退出: +然而,这个模式太常见了以至于 Rust 为此提供了一个内建的语言结构,它被称为`while`循环。下面的例子使用了`while`:程序循环三次,每次数字都减一。接着,在循环之后,打印出另一个信息并退出: Filename: src/main.rs @@ -271,7 +268,6 @@ fn main() { 可以使用`while`结构来遍历一个元素集合,比如数组。例如: -
    Filename: src/main.rs ```rust @@ -287,12 +283,8 @@ fn main() { } ``` -
    - -Listing 3-5: Looping through each element of a collection using a `while` loop - -
    -
    +Listing 3-5: Looping through each element of a collection +using a `while` loop 这里代码对数组中的元素进行计数。它从索引`0`开始,并接着循环直到遇到数组的最后一个索引(这时,`index < 5`不再为真)。运行这段代码会打印出数组中的每一个元素: diff --git a/src/ch04-00-understanding-ownership.md b/src/ch04-00-understanding-ownership.md index 89eaca9..fe56937 100644 --- a/src/ch04-00-understanding-ownership.md +++ b/src/ch04-00-understanding-ownership.md @@ -1,7 +1,7 @@ # 认识所有权 -> [ch04-00-understanding-ownership.md](https://github.com/rust-lang/book/blob/master/src/ch04-00-understanding-ownership.md) +> [ch04-00-understanding-ownership.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch04-00-understanding-ownership.md) >
    -> commit 759067b651a48a4a66485fe0876d318d398fb4fe +> commit 4f2dc564851dc04b271a2260c834643dfd86c724 -所有权(系统)是 Rust 最独特的功能,它令 Rust 可以无需垃圾回收(garbage collector)就能保障内存安全。因此,理解 Rust 中所有权如何工作是十分重要的。本章我们将讲到所有权以及相关功能:借用、slices 以及 Rust 如何在内存中安排数据。 \ No newline at end of file +所有权(系统)是 Rust 最独特的功能,它使得 Rust 可以无需垃圾回收(garbage collector)就能保障内存安全。因此,理解 Rust 中所有权如何工作是十分重要的。本章我们将讲到所有权以及相关功能:借用、slice 以及 Rust 如何在内存中摆放数据。 \ No newline at end of file diff --git a/src/ch04-01-what-is-ownership.md b/src/ch04-01-what-is-ownership.md index 667d8b0..4fac918 100644 --- a/src/ch04-01-what-is-ownership.md +++ b/src/ch04-01-what-is-ownership.md @@ -1,8 +1,8 @@ ## 什么是所有权 -> [ch04-01-what-is-ownership.md](https://github.com/rust-lang/book/blob/master/src/ch04-01-what-is-ownership.md) +> [ch04-01-what-is-ownership.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch04-01-what-is-ownership.md) >
    -> commit cc053d91f41793e54d5321abe027b0c163d735b8 +> commit fae5fa82d728b5965ecbba84060689430345e509 Rust 的核心功能(之一)是**所有权**(*ownership*)。虽然这个功能理解起来很直观,不过它对语言的其余部分有着更深层的含义。 @@ -16,7 +16,7 @@ Rust 的核心功能(之一)是**所有权**(*ownership*)。虽然这个 > ### 栈(Stack)与堆(Heap) > -> 在很多语言中并不经常需要考虑到栈与堆。不过在像 Rust 这样的系统编程语言中,值是位于栈上还是堆上在更大程度上影响了语言的行为以及为何必须做出特定的选择。我们会在本章的稍后部分描述所有权与堆与栈相关的部分,所以这里只是一个用来预热的简要解释。 +> 在很多语言中并不经常需要考虑到栈与堆。不过在像 Rust 这样的系统编程语言中,值是位于栈上还是堆上在更大程度上影响了语言的行为以及为何必须做出这样的选择。我们会在本章的稍后部分描述所有权与堆与栈相关的部分,所以这里只是一个用来预热的简要解释。 > > 栈和堆都是代码在运行时可供使用的内存部分,不过他们以不同的结构组成。栈以放入值的顺序存储并以相反顺序取出值。这也被称作**后进先出**(*last in, first out*)。想象一下一叠盘子:当增加更多盘子时,把他们放在盘子堆的顶部,当需要盘子时,也从顶部拿走。不能从中间也不能从底部增加或拿走盘子!增加数据叫做**进栈**(*pushing onto the stack*),而移出数据叫做**出栈**(*popping off the stack*)。 > @@ -30,13 +30,13 @@ Rust 的核心功能(之一)是**所有权**(*ownership*)。虽然这个 > > 当调用一个函数,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。 > -> 记录何处的代码在使用堆上的什么数据,最小化堆上的冗余数据的数量以及清理堆上不再使用的数据以致不至于用完空间,这些所有的问题正是所有权系统要处理的。一旦理解了所有权,你就不需要经常考虑栈和堆了,不过理解如何管理堆内存可以帮助我们理解所有权为什么存在以及为什么以它的方式工作。 +> 记录何处的代码在使用堆上的什么数据,最小化堆上的冗余数据的数量以及清理堆上不再使用的数据以致不至于耗尽空间,这些所有的问题正是所有权系统要处理的。一旦理解了所有权,你就不需要经常考虑栈和堆了,不过理解如何管理堆内存可以帮助我们理解所有权为何存在以及为什么以这种方式工作。 ### 所有权规则 -首先,让我们看一下所有权的规则。记住这些规则正如我们将完成一些说明这些规则的例子: +首先,让我们看一下所有权的规则。请记住这些规则因为我们将讲解一些说明这些规则的例子: > 1. Rust 中的每一个值都有一个叫做它的**所有者**(*owner*)的变量。 > 2. 同时一次只能有一个所有者 @@ -46,7 +46,7 @@ Rust 的核心功能(之一)是**所有权**(*ownership*)。虽然这个 我们在第二章已经完成过一个 Rust 程序的例子了。现在我们已经掌握了基本语法,所以不会在所有的例子中包含`fn main() {`代码了,所以如果你是一路跟过来的,必须手动将之后例子的代码放入一个`main`函数中。为此,例子将显得更加具体,使我们可以关注具体细节而不是样板代码。 -作为所有权的第一个例子,我们看看一些变量的**作用域**(*scope*)。作用域是一个 item 在程序中有效的范围。假如有一个这样的变量: +作为所有权的第一个例子,我们看看一些变量的**作用域**(*scope*)。作用域是一个项在程序中有效的范围。假如有一个这样的变量: ```rust let s = "hello"; @@ -54,8 +54,6 @@ let s = "hello"; 变量`s`绑定到了一个字符串字面值,这个字符串值是硬编码进我们程序代码中的。这个变量从声明的点开始直到当前*作用域*结束时都是有效的。列表 4-1 的注释标明了变量`s`在哪里是有效的: -
    - ```rust { // s is not valid here, it’s not yet declared let s = "hello"; // s is valid from this point forward @@ -64,27 +62,23 @@ let s = "hello"; } // this scope is now over, and s is no longer valid ``` -
    - -Listing 4-1: A variable and the scope in which it is valid - -
    -
    +Listing 4-1: A variable and the scope in which it is +valid 换句话说,这里有两个重要的点: 1. 当`s`**进入作用域**,它就是有效的。 2. 这一直持续到它**离开作用域**为止。 -目前为止,变量是否有效与作用域的关系跟其他变成语言是类似的。现在我们要在此基础上介绍`String`类型。 +目前为止,变量是否有效与作用域的关系跟其他变成语言是类似的。现在我们在此基础上介绍`String`类型。 ### `String`类型 -为了演示所有权的规则,我们需要一个比第三章讲到的任何一个都要复杂的数据类型。之前出现的数据类型都是储存在栈上的并且当离开作用域时被移出栈,不过我们需要寻找一个储存在堆上的数据来探索 Rust 如何知道该在何时清理数据。 +为了演示所有权的规则,我们需要一个比第三章讲到的任何一个都要复杂的数据类型。之前出现的数据类型都是储存在栈上的并且当离开作用域时被移出栈,不过我们需要寻找一个储存在堆上的数据来探索 Rust 如何知道该在何时清理数据的。 -这里使用`String`作为例子并专注于`String`与所有权相关的部分。这些方面也同样适用于其他标准库提供的或你创建的复杂数据类型。在第八章会更深入地讲解`String`。 +这里使用`String`作为例子并专注于`String`与所有权相关的部分。这些方面也同样适用于其他标准库提供的或你自己创建的复杂数据类型。在第八章会更深入地讲解`String`。 -我们已经见过字符串字面值了,它被硬编码进程序里。字符串字面值是很方便,不过他们并不总是适合所有需要使用文本的场景。原因之一就是他们是不可变的。另一个原因是不是所有字符串的值都能在编写代码时就知道:例如,如果想要获取用户输入并储存该怎么办呢?为此,Rust 有第二个字符串类型,`String`。这个类型储存在堆上所以储存在编译时未知大小的文本。可以用`from`从字符串字面值来创建`String`,如下: +我们已经见过字符串字面值了,它被硬编码进程序里。字符串字面值是很方便的,不过他们并不总是适合所有需要使用文本的场景。原因之一就是他们是不可变的。另一个原因是不是所有字符串的值都能在编写代码时就知道:例如,如果想要获取用户输入并储存该怎么办呢?为此,Rust 有第二个字符串类型,`String`。这个类型储存在堆上所以储存在编译时未知大小的文本。可以用`from`从字符串字面值来创建`String`,如下: ```rust let s = String::from("hello"); @@ -106,7 +100,7 @@ println!("{}", s); // This will print `hello, world!` ### 内存与分配 -字符串字面值的情况,我们在编译时就知道内容所以它直接被硬编码进最终的可执行文件中,这使得字符串字面值快速和高效。不过这些属性都只来源于它的不可变形。不幸的是,我们不能为了每一个在编译时未知大小的文本而将一块内存放入二进制文件中而它的大小还可能随着程序运行而改变。 +对于字符串字面值的情况,我们在编译时就知道内容所以它直接被硬编码进最终的可执行文件中,这使得字符串字面值快速和高效。不过这些属性都只来源于它的不可变形。不幸的是,我们不能为了每一个在编译时未知大小的文本而将一块内存放入二进制文件中而它的大小还可能随着程序运行而改变。 对于`String`类型,为了支持一个可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容。这意味着: @@ -117,7 +111,7 @@ println!("{}", s); // This will print `hello, world!` 然而,第二部分实现起来就各有区别了。在有**垃圾回收(GC)**的语言中, GC 记录并清除不再使用的内存,而我们作为程序员,并不需要关心他们。没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们程序员的责任了,正如请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要`allocate`和`free`一一对应。 -Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。下面是列表 4-1 作用域例子的一个使用`String`而不是字符串字面值的版本: +Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。下面是列表 4-1 中作用域例子的一个使用`String`而不是字符串字面值的版本: ```rust { @@ -138,19 +132,13 @@ Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域 Rust 中的多个变量以一种独特的方式与同一数据交互。让我们看看列表 4-2 中一个使用整型的例子: -
    - ```rust let x = 5; let y = x; ``` -
    - -Listing 4-2: Assigning the integer value of variable `x` to `y` - -
    -
    +Listing 4-2: Assigning the integer value of variable `x` +to `y` 根据其他语言的经验大致可以猜到这在干什么:“将`5`绑定到`x`;接着生成一个值`x`的拷贝并绑定到`y`”。现在有了两个变量,`x`和`y`,都等于`5`。这也正是事实上发生了的,因为正数是有已知固定大小的简单值,所以这两个`5`被放入了栈中。 @@ -165,46 +153,28 @@ let s2 = s1; 为了更全面的解释这个问题,让我们看看图 4-3 中`String`真正是什么样。`String`由三部分组成,如图左侧所示:一个指向存放字符串内容内存的指针,一个长度,和一个容量。这一组数据储存在栈上。右侧则是堆上存放内容的内存部分。 -
    String in memory -
    +Figure 4-3: Representation in memory of a `String` +holding the value `"hello"` bound to `s1` -Figure 4-3: Representation in memory of a `String` holding the value `"hello"` -bound to `s1` - -
    -
    - -长度代表当前`String`的内容使用了多少字节的内存。容量是`String`从操作系统总共获取了多少字节的内存。长度与容量的区别是很重要的,不过目前为止的场景中并不重要,所以可以暂时忽略容量。 +长度代表当前`String`的内容使用了多少字节的内存。容量是`String`从操作系统总共获取了多少字节的内存。长度与容量的区别是很重要的,不过这在目前为止的场景中并不重要,所以可以暂时忽略容量。 当我们把`s1`赋值给`s2`,`String`的数据被复制了,这意味着我们从栈上拷贝了它的指针、长度和容量。我们并没有复制堆上指针所指向的数据。换句话说,内存中数据的表现如图 4-4 所示。 -
    s1 and s2 pointing to the same value -
    - -Figure 4-4: Representation in memory of the variable `s2` that has a copy of -the pointer, length, and capacity of `s1` - -
    -
    +Figure 4-4: Representation in memory of the variable `s2` +that has a copy of the pointer, length, and capacity of `s1` 这个表现形式看起来**并不像**图 4-5 中的那样,它是如果 Rust 也拷贝了堆上的数据后内存看起来是怎么样的。如果 Rust 这么做了,那么操作`s2 = s1`在堆上数据比较大的时候可能会对运行时性能造成非常大的影响。 -
    s1 and s2 to two places -
    +Figure 4-5: Another possibility of what `s2 = s1` might +do if Rust copied the heap data as well -Figure 4-5: Another possibility of what `s2 = s1` might do if Rust copied the -heap data as well - -
    -
    - -之前,我们提到过当变量离开作用域后 Rust 自动调用`drop`函数并清理变量的堆内存。不过图 4-4 展示了两个数据指针指向了同一位置。这就有了一个问题:当`s2`和`s1`离开作用域,他们都会尝试释放相同的内存。这是一个叫做 *double free* 的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致安全漏洞。 +之前,我们提到过当变量离开作用域后 Rust 自动调用`drop`函数并清理变量的堆内存。不过图 4-4 展示了两个数据指针指向了同一位置。这就有了一个问题:当`s2`和`s1`离开作用域,他们都会尝试释放相同的内存。这是一个叫做 *double free* 的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。 为了确保内存安全,这种场景下 Rust 的处理有另一个细节值得注意。与其尝试拷贝被分配的内存,Rust 则认为`s1`不再有效,因此 Rust 不需要在`s1`离开作用域后清理任何东西。看看在`s2`被创建之后尝试使用`s1`会发生生么: @@ -232,15 +202,10 @@ which does not implement the `Copy` trait 如果你在其他语言中听说过术语“浅拷贝”(“shallow copy”)和“深拷贝”(“deep copy”),那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效化了,这个操作被成为**移动**(*move*),而不是浅拷贝。上面的例子可以解读为`s1`被**移动**到了`s2`中。那么具体发生了什么如图 4-6 所示。 -
    s1 moved to s2 -
    - -Figure 4-6: Representation in memory after `s1` has been invalidated - -
    -
    +Figure 4-6: Representation in memory after `s1` has been +invalidated 这样就解决了我们的麻烦!因为只有`s2`是有效的,当其离开作用域,它就释放自己的内存,完毕。 @@ -278,7 +243,7 @@ println!("x = {}, y = {}", x, y); 原因是像整型这样的在编译时已知大小的类型被整个储存在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量`y`后使`x`无效。换句话说,这里没有深浅拷贝的区别,所以调用`clone`并不会与通常的浅拷贝有什么不同,我们可以不用管它。 -Rust 有一个叫做`Copy` trait 的特殊注解,可以用在类似整型这样的储存在栈上的类型(第十章详细讲解 trait)。如果一个类型拥有`Copy` trait,一个旧的变量在(重新)赋值后仍然可用。Rust 不允许自身或其任何部分实现了`Drop` trait 的类型使用`Copy` trait。如果我们对其值离开作用域时需要特殊处理的类型使用`Copy`注解,将会出现一个编译时错误。 +Rust 有一个叫做`Copy` trait 的特殊注解,可以用在类似整型这样的储存在栈上的类型(第十章详细讲解 trait)。如果一个类型拥有`Copy` trait,一个旧的变量在(重新)赋值后仍然可用。Rust 不允许自身或其任何部分实现了`Drop` trait 的类型使用`Copy` trait。如果我们对其值离开作用域时需要特殊处理的类型使用`Copy`注解,将会出现一个编译时错误。关于如何为你的类型增加`Copy`注解,请阅读附录 C 中的 Derivable Trait。 那么什么类型是`Copy`的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则,任何简单标量值的组合可以是`Copy`的,任何不需要分配内存或类似形式资源的类型是`Copy`的,如下是一些`Copy`的类型: @@ -291,7 +256,6 @@ Rust 有一个叫做`Copy` trait 的特殊注解,可以用在类似整型这 将值传递给函数在语言上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。列表 4-7 是一个带有变量何时进入和离开作用域标注的例子: -
    Filename: src/main.rs ```rust @@ -319,14 +283,10 @@ fn makes_copy(some_integer: i32) { // some_integer comes into scope. } // Here, some_integer goes out of scope. Nothing special happens. ``` -
    +Listing 4-7: Functions with ownership and scope +annotated -Listing 4-7: Functions with ownership and scope annotated - -
    -
    - -当尝试在调用`takes_ownership`后使用`s`时,Rust 会抛出一个编译时错误。这些静态检查使我们免于犯错。试试在`main`函数中添加使用`s`和`x`的代码来看看哪里能使用他们,和哪里所有权规则会阻止我们这么做。 +当尝试在调用`takes_ownership`后使用`s`时,Rust 会抛出一个编译时错误。这些静态检查使我们免于犯错。试试在`main`函数中添加使用`s`和`x`的代码来看看哪里能使用他们,以及哪里所有权规则会阻止我们这么做。 ### 返回值与作用域 diff --git a/src/ch04-02-references-and-borrowing.md b/src/ch04-02-references-and-borrowing.md index 6605e31..a61293b 100644 --- a/src/ch04-02-references-and-borrowing.md +++ b/src/ch04-02-references-and-borrowing.md @@ -1,8 +1,8 @@ ## 引用与借用 -> [ch04-02-references-and-borrowing.md](https://github.com/rust-lang/book/blob/master/src/ch04-02-references-and-borrowing.md) +> [ch04-02-references-and-borrowing.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch04-02-references-and-borrowing.md) >
    -> commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c +> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 在上一部分的结尾处的使用元组的代码是有问题的,我们需要将`String`返回给调用者函数这样就可以在调用`calculate_length`后仍然可以使用`String`了,因为`String`先被移动到了`calculate_length`。 @@ -28,16 +28,9 @@ fn calculate_length(s: &String) -> usize { 这些 & 符号就是**引用**,他们允许你使用值但不获取它的所有权。图 4-8 展示了一个图解。 - -
    &String s pointing at String s1 -
    - -Figure 4-8: `&String s` pointing at `String s1` - -
    -
    +Figure 4-8: `&String s` pointing at `String s1` 仔细看看这个函数调用: @@ -67,7 +60,6 @@ fn calculate_length(s: &String) -> usize { // s is a reference to a String 那么如果我们尝试修改借用的变量呢?尝试列表 4-9 中的代码。剧透:这行不通! -
    Filename: src/main.rs ```rust,ignore @@ -82,16 +74,11 @@ fn change(some_string: &String) { } ``` -
    - -Listing 4-9: Attempting to modify a borrowed value - -
    -
    +Listing 4-9: Attempting to modify a borrowed value 这里是错误: -```sh +``` error: cannot borrow immutable borrowed content `*some_string` as mutable --> error.rs:8:5 | @@ -134,7 +121,7 @@ let r2 = &mut s; 具体错误如下: -```text +``` error[E0499]: cannot borrow `s` as mutable more than once at a time --> borrow_twice.rs:5:19 | @@ -156,7 +143,7 @@ error[E0499]: cannot borrow `s` as mutable more than once at a time 数据竞争会导致未定义行为并且当在运行时尝试追踪时可能会变得难以诊断和修复;Rust 阻止了这种情况的发生,因为存在数据竞争的代码根本就不能编译! -一如既往,使用大括号来创建一个新的作用域,允许拥有多个可变引用,只是不能**同时**拥有: +一如既往,可以使用大括号来创建一个新的作用域来允许拥有多个可变引用,只是不能**同时**拥有: ```rust let mut s = String::from("hello"); @@ -171,7 +158,6 @@ let r2 = &mut s; 当结合可变和不可变引用时有一个类似的规则存在。这些代码会导致一个错误: - ```rust,ignore let mut s = String::from("hello"); @@ -182,7 +168,7 @@ let r3 = &mut s; // BIG PROBLEM 错误如下: -```sh +``` error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable --> borrow_thrice.rs:6:19 @@ -206,7 +192,6 @@ immutable 让我们尝试创建一个悬垂引用: - Filename: src/main.rs ```rust,ignore @@ -276,7 +261,7 @@ fn no_dangle() -> String { 1. 在任意给定时间,**只能**拥有如下中的一个: * 一个可变引用。 - * 任意属性的不可变引用。 + * 任意数量的不可变引用。 2. 引用必须总是有效的。 -接下来,我们来看看一种不同类型的引用:slices。 \ No newline at end of file +接下来,我们来看看一种不同类型的引用:slice。 \ No newline at end of file diff --git a/src/ch04-03-slices.md b/src/ch04-03-slices.md index e201d3c..9949491 100644 --- a/src/ch04-03-slices.md +++ b/src/ch04-03-slices.md @@ -1,8 +1,8 @@ ## Slices -> [ch04-03-slices.md](https://github.com/rust-lang/book/blob/master/src/ch04-03-slices.md) +> [ch04-03-slices.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch04-03-slices.md) >
    -> commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c +> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 另一个没有所有权的数据类型是 *slice*。slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。 @@ -16,7 +16,6 @@ fn first_word(s: &String) -> ? `first_word`这个函数有一个参数`&String`。因为我们不需要所有权,所以这没有问题。不过应该返回什么呢?我们并没有一个真正获取**部分**字符串的办法。不过,我们可以返回单词结尾的索引。让我们试试如列表 4-10 所示的代码: -
    Filename: src/main.rs ```rust @@ -33,13 +32,8 @@ fn first_word(s: &String) -> usize { } ``` -
    - -Listing 4-10: The `first_word` function that returns a byte index value into -the `String` parameter - -
    -
    +Listing 4-10: The `first_word` function that returns a +byte index value into the `String` parameter 让我们将代码分解成小块。因为需要一个元素一个元素的检查`String`中的值是否是空格,需要用`as_bytes`方法将`String`转化为字节数组: @@ -47,13 +41,13 @@ the `String` parameter let bytes = s.as_bytes(); ``` -Next, we create an iterator over the array of bytes using the `iter` method : +接下来,使用`iter`方法在字节数据上创建一个迭代器: ```rust,ignore for (i, &item) in bytes.iter().enumerate() { ``` -第十六章将讨论迭代器的更多细节。现在,只需知道`iter`方法返回集合中的每一个元素,而`enumerate`包装`iter`的结果并返回一个元组,其中每一个元素是元组的一部分。返回元组的第一个元素是索引,第二个元素是集合中元素的引用。这比我们自己计算索引要方便一些。 +第十三章将讨论迭代器的更多细节。现在,只需知道`iter`方法返回集合中的每一个元素,而`enumerate`包装`iter`的结果并返回一个元组,其中每一个元素是元组的一部分。返回元组的第一个元素是索引,第二个元素是集合中元素的引用。这比我们自己计算索引要方便一些。 因为`enumerate`方法返回一个元组,我们可以使用模式来解构它,就像 Rust 中其他地方一样。所以在`for`循环中,我们指定了一个模式,其中`i`是元组中的索引而`&item`是单个字节。因为从`.iter().enumerate()`中获取了集合元素的引用,我们在模式中使用了`&`。 @@ -69,7 +63,6 @@ s.len() 现在有了一个找到字符串中第一个单词结尾索引的方法了,不过这有一个问题。我们返回了单单一个`usize`,不过它只在`&String`的上下文中才是一个有意义的数字。换句话说,因为它是一个与`String`像分离的值,无法保证将来它仍然有效。考虑一下列表 4-11 中使用了列表 4-10 `first_word`函数的程序: -
    Filename: src/main.rs ```rust @@ -97,13 +90,8 @@ fn main() { } ``` -
    - -Listing 4-11: Storing the result from calling the `first_word` function then -changing the `String` contents - -
    -
    +Listing 4-11: Storing the result from calling the +`first_word` function then changing the `String` contents 这个程序编译时没有任何错误,而且在调用`s.clear()`之后使用`word`也不会出错。这时`word`与`s`状态就没有联系了,所以`word`仍然包含值`5`。可以尝试用值`5`来提取变量`s`的第一个单词,不过这是有 bug 的,因为在我们将`5`保存到`word`之后`s`的内容已经改变。 @@ -134,16 +122,10 @@ let world = &s[6..11]; 图 4-12 展示了一个图例 - -
    world containing a pointer to the 6th byte of String s and a length 5 -
    - -Figure 4-12: String slice referring to part of a `String` - -
    -
    +Figure 4-12: String slice referring to part of a +`String` 对于 Rust 的`..` range 语法,如果想要从第一个索引(0)开始,可以不写两个点号之前的值。换句话说,如下两个语句是相同的: