diff --git a/docs/ch01-00-introduction.html b/docs/ch01-00-introduction.html
index 8329572..b817f27 100644
--- a/docs/ch01-00-introduction.html
+++ b/docs/ch01-00-introduction.html
@@ -73,14 +73,14 @@
commit 62f78bb3f7c222b574ff547d0161c2533691f9b4
欢迎阅读“Rust 程序设计语言”,一本关于 Rust 的介绍性书籍。Rust 是一个着用于安全、速度和并发的编程语言。它的设计不仅可以使程序获得性能和对底层语言的控制,并且能够享受高级语言强大的抽象能力。这些特性使得 Rust 适合那些有类似 C 语言经验并正在寻找一个更安全的替代者的程序员,同时也适合那些来自类似 Python 语言背景,正在探索在不牺牲表现力的情况下编写更好性能代码的开发者。
-Rust 在编译时进行其绝大多数的安全检查和内存管理决策,因此程序的运行时性能没有受到影响。这让其在许多其他语言不擅长的应用场景中得以大显身手:存在可预测空间和时间要求的程序,嵌入到其他语言中,以及编写底层代码,如设备驱动和操作系统。Rust 也很擅长 web 程序:它驱动着 Rust 包注册网站(package -registry site),crates.io!我们期待看到你使用 Rust 进行创作。
-本书是为已经至少了解一门编程语言的读者而写的。读完本书之后,你应该能自如的编写 Rust 程序。我们将通过小而集中并且相互依赖的例子来学习 Rust,并向你展示如何使用 Rust 多样的功能,同时了解它们在幕后是如何执行的。
+欢迎阅读“Rust 程序设计语言”,一本介绍 Rust 的书。Rust 是一门着眼于安全、速度和并发的编程语言。它的设计兼顾性能与底层控制,以及高级语言强大的抽象能力。适合那些有类 C 语言经验,正在寻找更安全的替代品的开发者;以及有着类 Python 语言背景,寻求在不牺牲表现力的前提下,编写性能更好的代码的开发者。
+Rust 主要在编译时执行安全检查和内存管理决策,对运行时性能的影响微不足道。这使其在许多语言不擅长的应用场景中得以大显身手:空间和时间需求可预测的程序,嵌入到其他语言中,以及编写底层代码,如设备驱动和操作系统。Rust 也很擅长 web 程序:它驱动着 Rust 包注册网站(package +registry site),crates.io!我们期待你使用 Rust 进行创作。
+本书的目标读者至少应了解一门其它编程语言。读完本书之后,你应该能自如的编写 Rust 程序。我们将通过短小精干、前后呼应的例子来学习 Rust,并展示其多样功能的使用方法,同时了解幕后如何运行。
本书是开源的。如果你发现任何错误,请不要犹豫,在 GitHub 上发起 issue 或提交 pull request。请查看CONTRIBUTING.md获取更多信息。
+本书是开源的。如果你发现任何错误,不要犹豫,在 GitHub 上发起 issue 或提交 pull request。请查看 CONTRIBUTING.md 获取更多信息。
-diff --git a/docs/ch01-01-installation.html b/docs/ch01-01-installation.html index 76cd7df..ef3d7e0 100644 --- a/docs/ch01-01-installation.html +++ b/docs/ch01-01-installation.html @@ -73,43 +73,43 @@译者注:这是本译本的 GitHub 仓库,同样欢迎 Issue 和 PR :)
+译者注:译本的 GitHub 仓库,同样欢迎 Issue 和 PR :)
使用 Rust 的第一步是安装。你需要网络连接来执行本章的命令,因为我们要从网上下载 Rust。
-我们将会展示很多使用终端的命令,并且这些代码都以$
开头。并不需要真正输入$
,它们在这里代表每行指令的开头。在网上会看到很多使用这个惯例的教程和例子:$
代表以常规用户运行命令,#
代表需要用管理员运行的命令。没有以$
(或#
)的行通常是之前命令的输出。
第一步是安装 Rust。你需要网络连接来执行本章的命令,因为我们要从网上下载 Rust。
+我们将会展示很多在终端中输入的命令,这些命令均以 $
开头。你不需要真的输入$
,在这里它代表每行命令的起始。网上有很多教程和例子遵循这种惯例:$
代表以常规用户身份运行命令,#
代表需要用管理员身份运行命令。没有以 $
(或 #
)起始的行通常是之前命令的输出。
如果你使用 Linux 或 Mac,所有需要做的就是打开一个终端并输入:
+如果你使用 Linux 或 Mac,你需要做的全部,就是打开一个终端并输入:
$ curl https://sh.rustup.rs -sSf | sh
-这会下载一个脚本并开始安装。你可能被提示要输入密码。如果一切顺利,将会出现如下内容:
+这会下载一个脚本并开始安装。可能会提示你输入密码,如果一切顺利,将会出现如下内容:
Rust is installed now. Great!
-当然,如果你不赞成curl | sh
这种模式,可以随意下载、检查和运行这个脚本。
当然,如果你对于 curl | sh
心有疑虑,你可以随意下载、检查和运行这个脚本。
在 Windows 上,前往https://rustup.rs并按照说明下载 rustup-init.exe。运行并遵循其提供的其余指示操作。
-本书其余 Windows 相关的命令假设你使用cmd
作为你的 shell。如果你使用不同的 shell,可能能够执行 Linux 和 Mac 用户相同的命令。如果都不行,请查看所使用的 shell 的文档。
如果你使用 Windows,前往 https://rustup.rs,按说明下载 rustup-init.exe,运行并照其指示操作。
+本书中其余 Windows 相关的命令,假设你使用 cmd
作为 shell。如果你使用其它 shell,也许可以执行与 Linux 和 Mac 用户相同的命令。如果不行,请查看该 shell 的文档。
如果有理由倾向于不使用 rustup.rs,请查看Rust 安装页面获取其他选择。
+无论出于何种理由,如果不愿意使用 rustup.rs,请查看 Rust 安装页面 获取其他选择。
一旦安装完 Rust,更新到最新版本是简单的。在 shell 中运行更新脚本:
+一旦 Rust 安装完,更新到最新版本很简单。在 shell 中执行:
$ rustup update
卸载 Rust 同安装一样简单。在 shell 中运行卸载脚本
+卸载 Rust 同样简单。在 shell 中执行:
$ rustup self uninstall
安装完 Rust 后,打开 shell,输入:
+安装完 Rust 后,在 shell 中执行:
$ rustc --version
-应该能看到类似这样的版本号、提交 hash 和提交日期,对应你安装时的最新稳定版本:
+应该能看到类似这样的版本号、提交哈希和提交日期,对应安装时的最新稳定版:
rustc x.y.z (abcabcabc yyyy-mm-dd)
-如果出现这些内容,Rust 就安装成功了!
+出现这些内容,Rust 就安装成功了!
恭喜入坑!(此处应该有掌声!)
-如果有问题并且你在使用 Windows,检查 Rust(rustc,cargo 等)是否位于%PATH%
系统变量中。
如果还是不能运行,有许多可以获取帮助的地方。最简单的是 irc.mozilla.org 上的 #rust IRC 频道 ,可以使用 Mibbit 来访问它。访问这些地址然后就可以和其他能提供帮助的 Rustacean(我们这些人自嘲的绰号)聊天了。其它给力的资源包括用户论坛和 Stack Overflow。
+如果在 Windows 中使用出现问题,检查 Rust(rustc,cargo 等)是否在 %PATH%
环境变量所包含的路径中。
如果还是不能解决,有许多地方可以求助。最简单的是 irc.mozilla.org 上的 #rust IRC 频道 ,可以使用 Mibbit 来访问它。然后就能和其他 Rustacean(Rust 用户的称号,有自嘲意味)聊天并寻求帮助。其它给力的资源包括用户论坛和 Stack Overflow。
安装程序也包含一份本地文档的拷贝,你可以离线阅读它们。输入rustup doc
将在浏览器中打开本地文档。
任何你不太确认标准库中提供的类型或函数是干什么的时候,请查看 API 文档!
+安装程序自带本地文档,可以离线阅读。输入 rustup doc
可以在浏览器中查看。
任何时候,如果你拿不准标准库中类型或函数,请查看 API 文档!
diff --git a/docs/ch01-02-hello-world.html b/docs/ch01-02-hello-world.html index 5e55907..5126eed 100644 --- a/docs/ch01-02-hello-world.html +++ b/docs/ch01-02-hello-world.html @@ -73,12 +73,12 @@现在已经安装好了 Rust,让我们来编写第一个 Rust 程序。当学习一门新语言的时候,编写一个在屏幕上打印 “Hello, world!” 文本的小程序是一个传统,而在这一部分将遵循这个传统。
+Rust 已安好,让我们来编写第一个程序。当学习一门新语言的时候,使用该语言在屏幕上打印 “Hello, world!” 是一项传统,我们将遵循这个传统。
-注意:本书假设你熟悉基本的命令行操作。Rust 本身并不对你的编辑器,工具和你的代码存放在何处有什么特定的要求,所以如果你比起命令行更喜欢 IDE,请随意选择你喜欢的 IDE。
+注意:本书假设你熟悉基本的命令行操作。对于你的编辑器、工具,以及你的代码存在何处,Rust 并没有特殊要求,如果你更喜欢 IDE,请随意。
首先,创建一个文件夹来存放 Rust 代码。Rust 并不关心你的代码存放在哪里,不过在本书中,我们建议在你的 home 目录创建一个 projects 目录,并把你的所有项目放在这。打开一个终端并输入如下命令来为这个项目创建一个文件夹:
+首先,创建一个存放代码的文件夹。Rust 并不关心它的位置,不过在本书中,我们建议你在 home 目录中创建一个 projects 目录,并把你的所有项目放在这。打开一个终端,输入如下命令来创建一个文件夹:
Linux 和 Mac:
$ mkdir ~/projects
$ cd ~/projects
@@ -92,8 +92,8 @@ $ cd hello_world
> cd hello_world
接下来,新建一个叫做 main.rs 的源文件。Rust 文件总是以 .rs 后缀结尾。如果文件名多于一个单词,使用下划线分隔它们。例如,使用 my_program.rs 而不是 myprogram.rs。
-现在打开刚创建的 main.rs 文件,并输入如下代码:
+接下来,新建一个叫做 main.rs 的文件。Rust 源代码总是以 .rs 后缀结尾。如果文件名包含多个单词,使用下划线分隔它们。例如 my_program.rs,而不是 myprogram.rs。
+现在打开刚创建的 main.rs 文件,输入如下代码:
Filename: main.rs
fn main() {
println!("Hello, world!");
@@ -104,28 +104,28 @@ $ cd hello_world
$ ./main
Hello, world!
-在 Windows 上,运行.\main.exe
而不是./main
。不管使用何种系统,你应该在终端看到Hello, world!
字符串。如果你做到了,那么恭喜你!你已经正式编写了一个 Rust 程序。你是一名 Rust 程序员了!欢迎入坑。
在 Windows 上,运行 .\main.exe
,而不是./main
。不管使用何种系统,你应该在终端看到 Hello, world!
字样。如果你做到了,恭喜你!你已经正式编写了一个 Rust 程序,成为一名 Rust 程序员!
现在,让我们回过头来仔细看看“Hello, world!”程序到底发生了什么。这里是谜题的第一片:
+现在,让我们回过头来,仔细看看“Hello, world!”程序到底发生了什么。这是拼图的第一片:
fn main() {
}
-这几行定义了一个 Rust 函数。main
函数是特殊的:这是每一个可执行的 Rust 程序首先运行的函数(译者注:入口点)。第一行表示“定义一个叫 main
的函数,没有参数也没有返回值。”如果有参数的话,它们应该出现在括号中,(
和)
。
同时注意函数体被包裹在大括号中,{
和}
。Rust 要求所有函数体都位于大括号中(译者注:对比有些语言特定情况可以省略大括号)。将前一个大括号与函数声明置于一行,并留有一个空格被认为是一个好的代码风格。
在main()
函数中:
这几行定义了一个 Rust 函数。一个叫 main
的函数,没有参数也没有返回值。如果有参数的话,它们应该出现在括弧中,(
和)
之间。main
函数是特殊的:它是每一个可执行的 Rust 程序的入口点。
还须注意函数体被包裹在花括号中,{
和}
之间。所有函数体都要用花括号包裹起来(译者注:有些语言,当函数体只有一行时可以省略花括号,但 Rust 中是不行的)。一般来说,将左花括号与函数声明置于一行,并以空格分隔,是良好的代码风格。
在 main()
函数中:
println!("Hello, world!");
-这行代码做了这个小程序的所有工作:它在屏幕上打印文本。这里有很多需要注意的细节。第一个是 Rust 代码风格使用 4 个空格缩进,而不是 1 个制表符(tab)。
-第二个重要的部分是println!()
。这叫做 Rust 宏,是如何进行 Rust 元编程(metaprogramming)的关键所在。相反如果是调用一个函数的话,它应该看起来像这样:println
(没有!
)。我们将在 21 章 E 小节中更加详细的讨论 Rust 宏,不过现在你只需记住当看到符号!
的时候,就代表在调用一个宏而不是一个普通的函数。
一行代码完成这个小程序的所有工作:在屏幕上打印文本。这里有很多细节需要注意。首先 Rust 使用 4 个空格的缩进风格,而不是 1 个制表符(tab)。
+第二个重要的部分是println!()
。这是 宏,Rust 元编程(metaprogramming)的关键所在。而调用一个函数,则要像这样:println
(没有!
)。我们将在 21 章 E 小节中更加详细的讨论宏,现在你只需记住,当看到符号 !
的时候,调用的是宏而不是普通函数。
接下来,"Hello, world!"
是一个 字符串。我们把这个字符串作为一个参数传递给println!
,它负责在屏幕上打印这个字符串。轻松加愉快!(⊙o⊙)
这一行以一个分号结尾(;
)。;
代表这个表达式的结束和下一个表达式的开始。大部分 Rust 代码行以;
结尾。
该行以分号结尾(;
)。;
代表一个表达式的结束和下一个表达式的开始。大部分 Rust 代码行以 ;
结尾。
在“编写并运行 Rust 程序”部分,展示了如何运行一个新创建的程序。现在我们将拆分并检查每一步操作。
-在运行一个 Rust 程序之前,必须先编译它。可以输入rustc
命令来使用 Rust 编译器并像这样传递源文件的名字:
“编写并运行 Rust 程序”部分,展示了如何创建运行程序。现在我们将拆分并检查每一步操作。
+运行一个 Rust 程序之前,必须先编译它。可以通过 rustc
命令来使用 Rust 编译器,并传递源文件的名字给它,如下:
$ rustc main.rs
-如果你来自 C 或 C++ 背景,就会发现这与gcc
和clang
类似。编译成功后,Rust 应该会输出一个二进制可执行文件,在 Linux 或 OSX 上在 shell 中你可以通过ls
命令看到如下:
如果你有 C 或 C++ 背景,就会发现这与 gcc
和 clang
类似。编译成功后,Rust 应该会输出一个二进制可执行文件,在 Linux 或 OSX 上在 shell 中你可以通过ls
命令看到如下:
$ ls
main main.rs
@@ -134,21 +134,21 @@ main main.rs
main.exe
main.rs
-这表示我们有两个文件:.rs 后缀的源文件,和可执行文件(在 Windows下是 main.exe,其它平台是 main)。这里剩下的操作就只有运行 main 或 main.exe 文件了,像这样:
+这表示我们有两个文件:.rs 后缀的源文件,和可执行文件(在 Windows下是 main.exe,其它平台是 main)。然后运行 main 或 main.exe 文件,像这样:
$ ./main # or .\main.exe on Windows
如果 main.rs 是我们的“Hello, world!”程序,它将会在终端上打印Hello, world!
。
来自 Ruby、Python 或 JavaScript 这样的动态类型语言背景的同学,可能不太习惯在分开的步骤编译和执行程序。Rust 是一种 静态提前编译语言(ahead-of-time compiled language),这意味着可以编译好程序后,把它给任何人,他们都不需要安装 Rust 就可运行。如果你给他们一个 .rb
, .py
或 .js
文件,他们需要先分别安装 Ruby,Python,JavaScript 实现(运行时环境,VM),不过你只需要一句命令就可以编译和执行程序。这一切都是语言设计的权衡取舍。
仅仅使用rustc
编译简单程序是没问题的,不过随着项目的增长,你将想要能够控制你项目拥有的所有选项,并使其易于分享你的代码给别人或别的项目。接下来,我们将介绍一个叫做 Cargo 的工具,它将帮助你编写现实生活中的 Rust 程序。
来自 Ruby、Python 或 JavaScript 这样的动态类型语言背景的同学,可能不太习惯将编译和执行分为两个步骤。Rust 是一种 预编译静态类型语言(ahead-of-time compiled language),这意味着编译好程序后,把它给任何人,他们不需要安装 Rust 就可运行。如果你给他们一个 .rb
, .py
或 .js
文件,他们需要先分别安装 Ruby,Python,JavaScript 实现(运行时环境,VM),不过你只需要一句命令就可以编译和执行程序。这一切都是语言设计上的权衡取舍。
使用 rustc
编译简单程序是没问题的,不过随着项目的增长,你可能需要控制你项目的方方面面,并且更容易地将代码分享给其它人或项目。所以接下来,我们要介绍一个叫做 Cargo 的工具,它会帮助你编写真实世界中的 Rust 程序。
Cargo 是 Rust 的构建系统和包管理工具,同时 Rustacean 们使用 Cargo 来管理他们的 Rust 项目,因为它使得很多任务变得更轻松。例如,Cargo 负责构建代码、下载代码依赖的库并编译这些库。我们把代码需要的库叫做 依赖(dependencies)。
-最简单的 Rust 程序,例如我们刚刚编写的,并没有任何依赖,所以目前我们只使用了 Cargo 负责构建代码的那一部分。随着编写更加复杂的 Rust 程序,你会想要添加依赖,如果你使用 Cargo 开始的话,这将会变得简单许多。
-由于绝大部分 Rust 项目使用 Cargo,本书接下来的部分将假设你使用它。如果使用安装章节介绍的官方安装包的话,Rust 自带 Cargo。如果通过其他方式安装 Rust 的话,可以在终端输入如下命令检查是否安装了 Cargo:
+Cargo 是 Rust 的构建系统和包管理工具,同时 Rustacean 们使用 Cargo 来管理他们的 Rust 项目,它使得很多任务变得更轻松。例如,Cargo 负责构建代码、下载依赖库并编译。我们把代码需要的库叫做 依赖(dependencies)。
+最简单的 Rust 程序,比如我们刚刚编写的,并没有任何依赖,所以我们只使用了 Cargo 构建代码的功能。随着更复杂程序的编写,你会想要添加依赖,如果你使用 Cargo 开始的话,这将会变得简单许多。
+由于绝大部分 Rust 项目使用 Cargo,本书接下来的部分将假设你使用它。如果使用之前介绍的官方安装包的话,它自带 Cargo。如果通过其他方式安装的话,可以在终端输入如下命令,检查是否安装了 Cargo:
$ cargo --version
-如果出现了版本号,一切 OK!如果出现一个类似“command not found
”的错误,那么你应该查看安装方式的文档来确定如何单独安装 Cargo。
如果出现了版本号,一切 OK!如果出现类似“command not found
”的错误,你应该查看安装文档以确定如何单独安装 Cargo。
让我们使用 Cargo 来创建一个新项目并看看与上面的hello_world
项目有什么不同。回到 projects 目录(或者任何你决定放置代码的目录):
让我们使用 Cargo 来创建一个新项目,然后看看与上面的hello_world
项目有什么不同。回到 projects 目录(或者任何你放置代码的目录):
Linux 和 Mac:
$ cd ~/projects
@@ -159,9 +159,9 @@ main.rs
$ cargo new hello_cargo --bin
$ cd hello_cargo
-我们向cargo new
传递了--bin
因为我们的目标是生成一个可执行程序,而不是一个库。可执行文件是二进制可执行文件,通常就叫做 二进制文件(binaries)。项目的名称被定为hello_cargo
,同时 Cargo 在一个同名目录中创建它的文件,接着我们可以进入查看。
如果列出 hello_cargo 目录中的文件,将会看到 Cargo 生成了一个文件和一个目录:一个 Cargo.toml 文件和一个 src 目录,main.rs 文件位于目录中。它也在 hello_cargo 目录初始化了一个 git 仓库,以及一个 .gitignore 文件;你可以改为使用不同的版本控制系统(VCS),或者不使用 VCS,通过--vcs
参数。
使用你选择的文本编辑器(IDE)打开 Cargo.toml 文件。它应该看起来像这样:
+我们向 cargo new
传递了 --bin
,因为我们的目标是生成一个可执行程序,而不是一个库。可执行程序是二进制可执行文件,通常就叫做 二进制文件(binaries)。项目的名称被定为hello_cargo
,同时 Cargo 在一个同名目录中创建它的文件,接着我们可以进入查看。
如果列出 hello_cargo 目录中的文件,将会看到 Cargo 生成了一个文件和一个目录:一个 Cargo.toml 文件和一个 src 目录,main.rs 文件位于目录中。它也在 hello_cargo 目录初始化了一个 git 仓库,以及一个 .gitignore 文件;你可以通过--vcs
参数,切换到其它版本控制系统(VCS),或者不使用 VCS。
使用文本编辑器(IDE)打开 Cargo.toml 文件。它应该看起来像这样:
Filename: Cargo.toml
[package]
name = "hello_cargo"
@@ -170,22 +170,22 @@ authors = ["Your Name <you@example.com>"]
[dependencies]
-这个文件使用TOML (Tom's Obvious, Minimal Language) 格式。TOML 类似于 INI,不过有一些额外的改进之处,并且被用作 Cargo 的配置文件的格式。
-第一行,[package]
,是一个部分标题表明下面的语句用来配置一个包。随着我们在这个文件增加更多的信息,还将增加其他部分。
最后一行,[dependencies]
,是列出项目依赖的 crates(我们这么称呼 Rust 代码包)的部分的开始,这样 Cargo 也就知道去下载和编译它们了。这个项目并不需要任何其他的 crate,不过在下一章猜猜看教程会需要。
这个文件使用 TOML (Tom's Obvious, Minimal Language) 格式。TOML 类似于 INI,不过有一些额外的改进之处,并且被用作 Cargo 的配置文件的格式。
+第一行,[package]
,是一个段落标题,表明下面的语句用来配置一个包。随着我们在这个文件增加更多的信息,还将增加其他段落。
最后一行,[dependencies]
,是项目依赖的 crates(Rust 代码包)的段落的开始,这样 Cargo 就知道下载和编译它们了。这个项目并不需要任何其他的 crate,不过在下一章猜猜看教程会需要。
现在看看 src/main.rs:
Filename: src/main.rs
fn main() {
println!("Hello, world!");
}
-Cargo 为你生成了一个“Hello World!”,正如我们之前编写的那个!目前为止我们所见过的之前项目与 Cargo 生成的项目区别有:
+Cargo 为你生成了一个“Hello World!”,正如我们之前编写的那个!目前为止,之前项目与 Cargo 生成项目区别有:
Cargo 期望源文件位于 src 目录,这样将项目根目录留给 README、license 信息、配置文件和其他跟代码无关的文件。这样,Cargo 帮助你保持项目干净整洁。一切井井有条。
-如果没有使用 Cargo 开始项目,正如在 hello_world 目录中的项目,可以把它转化为一个使用 Cargo 的项目,通过将代码放入 src 目录并创建一个合适的 Cargo.toml。
+Cargo 期望源文件位于 src 目录,将项目根目录留给 README、license 信息、配置文件和其他跟代码无关的文件。这样,Cargo 帮助你保持项目干净整洁,一切井井有条。
+如果没有用 Cargo 创建项目,比如 hello_world 目录中的项目,可以通过将代码放入 src 目录,并创建一个合适的 Cargo.toml,将其转化为一个 Cargo 项目。
现在让我们看看通过 Cargo 构建和运行 Hello World 程序有什么不同。为此,输入如下命令:
$ cargo build
@@ -196,40 +196,40 @@ authors = ["Your Name <you@example.com>"]
Hello, world!
好的!如果一切顺利,Hello, world!
应该再次打印在终端上。
第一次运行cargo build
的时候也会使 Cargo 在项目根目录创建一个叫做 Cargo.lock 的新文件,它看起来像这样:
首次运行 cargo build
的时候,Cargo 会在项目根目录创建一个新文件,Cargo.lock,它看起来像这样:
Filename: Cargo.lock
[root]
name = "hello_cargo"
version = "0.1.0"
-Cargo 使用 Cargo.lock 来记录程序的依赖。这个项目并没有依赖,所以内容有一点稀少。事实上,你自己永远也不需要碰这个文件;仅仅让 Cargo 处理它就行了。
-我们刚刚使用cargo build
构建了项目并使用./target/debug/hello_cargo
运行了它,不过也可以使用cargo run
编译并运行:
Cargo 使用 Cargo.lock 来记录程序的依赖。这个项目并没有依赖,所以内容比较少。事实上,你自己永远也不需要碰这个文件,让 Cargo 处理它就行了。
+我们刚刚使用 cargo build
构建了项目并使用 ./target/debug/hello_cargo
运行了它,也可以使用 cargo run
编译并运行:
$ cargo run
Running `target/debug/hello_cargo`
Hello, world!
-注意这一次,并没有出现告诉我们 Cargo 正在编译 hello_cargo
的输出。Cargo 发现文件并没有被改变,所以只是运行了二进制文件。如果修改了源文件的话,将会出现像这样的输出:
注意这一次并没有出现“正在编译 hello_cargo
”的输出。Cargo 发现文件并没有被改变,直接运行了二进制文件。如果修改了源文件的话,将会出现像这样的输出:
$ cargo run
Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
Running `target/debug/hello_cargo`
Hello, world!
-所以现在又出现一些更多的不同:
+所以现在又出现更多的不同:
cargo build
构建项目(或使用cargo run
一步构建并运行),而不是使用rustc
cargo build
构建项目(或使用 cargo run
一步构建并运行),而不是使用rustc
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 并不能比rustc
提供更多的价值,不过随着开发的进行终将体现它的价值。对于拥有多个 crate 的复杂项目,让 Cargo 来协调构建将更简单。有了 Cargo,只需运行cargo build
,然后一切将有序运行。即便这个项目很简单,现在也它使用了很多之后你的 Rust 程序生涯将会用得上的实用工具。事实上,无形中你可以使用下面的命令开始所有你想要从事的项目:
对于简单项目, Cargo 并不比 rustc
更有价值,不过随着开发的进行终将体现它的价值。对于拥有多个 crate 的复杂项目,让 Cargo 来协调构建将更简单。有了 Cargo,只需运行cargo build
,然后一切将有序运行。即便这个项目很简单,也它使用了很多你之后的 Rust 生涯将会用得上的实用工具。其实你可以开始任何你想要从事的项目,使用下面的命令:
$ git clone someurl.com/someproject
$ cd someproject
$ cargo build
-diff --git a/docs/ch02-00-guessing-game-tutorial.html b/docs/ch02-00-guessing-game-tutorial.html index a2b0b8a..bda1095 100644 --- a/docs/ch02-00-guessing-game-tutorial.html +++ b/docs/ch02-00-guessing-game-tutorial.html @@ -73,10 +73,10 @@注意:如果你想要查看 Cargo 的更多细节,请阅读官方的 Cargo guide,它覆盖了其所有的功能。
+注意:如果想要了解 Cargo 更多的细节,请阅读官方的 Cargo guide,它覆盖了所有的功能。
让我们通过自己动手的方式一起完成一个项目来快速上手 Rust!本章将介绍一些常用的 Rust 概念,并通过真实的程序来展示如何运用他们。你将会学到let
、match
、方法、关联函数、使用外部 crate 等更多的知识!接下来的章节会探索这些概念的细节。在这一章,我们将练习基础。
我们会实现一个经典的新手编程问题:猜猜看游戏。它是这么工作的:程序将会随机生成一个 1 到 100 之间的随机整数。接着它会提示玩家输入一个猜测。当输入了一个猜测后,它会提示猜测是太大了还是太小了。如果猜对了,它会打印出祝贺并退出。
+让我们亲自动手,快速熟悉 Rust!本章将介绍 Rust 中常用的一些概念,并通过真实的程序来展示如何运用。你将会学到更多诸如 let
、match
、方法、关联函数、外部 crate 等知识!后继章节会深入探索这些概念的细节。在这一章,我们将练习基础。
我们会实现一个经典的新手编程问题:猜猜看游戏。它是这么工作的:程序将会随机生成一个 1 到 100 之间的随机整数。接着它会请玩家猜一个数并输入,然后提示猜测是大了还是小了。如果猜对了,它会在退出前祝贺你。
要创建一个新项目,进入在第一章创建的项目目录,像这样使用 Cargo 创建它:
+要创建一个新项目,进入第一章创建的项目目录,使用 Cargo 创建它:
$ cargo new guessing_game --bin
$ cd guessing_game
@@ -91,22 +91,22 @@ authors = ["Your Name <you@example.com>"]
[dependencies]
如果 Cargo 从环境中获取的开发者信息不正确,修改这个文件并再次保存。
-正如第一章那样,cargo new
生成了一个“Hello, world!”程序。查看 src/main.rs 文件:
正如第一章那样,cargo new
生成了一个“Hello, world!”程序。查看 src/main.rs 文件:
Filename: src/main.rs
fn main() {
println!("Hello, world!");
}
-现在让我们使用cargo run
在相同的步骤编译并运行这个“Hello, world!”程序:
现在让我们使用 cargo run
,编译运行一步到位:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Running `target/debug/guessing_game`
Hello, world!
-run
命令在需要快速迭代项目时就派上用场了,而这个游戏就正是这么一个项目:我们需要在进行下一步之前快速测试每次迭代。
重新打开 src/main.rs 文件。我们将会在这个文件编写全部的代码。
+run
命令适合用在需要快速迭代的项目,而这个游戏就是:我们需要在下一步迭代之前快速测试。
重新打开 src/main.rs 文件。我们将会在这个文件中编写全部代码。
程序的第一部分会请求用户输入,处理输入,并检查输入是否为期望的形式。首先,允许玩家输入一个猜测。在 src/main.rs 中输入列表 2-1 中的代码。
+程序的第一部分请求和处理用户输入,并检查输入是否符合预期。首先,需要有一个让玩家输入猜测的地方。在 src/main.rs 中输入列表 2-1 中的代码。
Filename: src/main.rs
use std::io;
@@ -124,59 +124,59 @@ fn main() {
}
-
这些代码包含很多信息,所以让我们一点一点地过一遍。为了获取用户输入并接着打印结果作为输出,我们需要将io
(输入/输出)库引入作用域中。io
库来自于标准库(也被称为std
):
这些代码包含很多信息,我们一点一点地过一遍。为了获取用户输入并打印结果作为输出,我们需要将 io
(输入/输出)库引入当前作用域。io
库来自于标准库(也被称为std
):
use std::io;
-Rust 默认只在每个程序的 prelude 中引用很少的一些类型。如果想要使用的类型并不在 prelude 中,你必须使用一个use
语句显式的将其引入到作用域中。使用std::io
库将提供很多io
相关的功能,接受用户输入的功能。
正如第一章所讲,main
函数是程序的入口点:
Rust 默认只在每个程序的 prelude 中引入少量类型。如果需要的类型不在 prelude 中,你必须使用一个 use
语句显式的将其引入作用域。std::io
库提供很多 io
相关的功能,比如接受用户输入。
如第一章所提及,main
函数是程序的入口点:
fn main() {
-fn
语法声明了一个新函数,()
表明没有参数,{
作为函数体的开始。
第一章也讲到了,println!
是一个在屏幕上打印字符串的宏:
fn
语法声明了一个新函数,()
表明没有参数,{
作为函数体的开始。
第一章也提及,println!
是一个在屏幕上打印字符串的宏:
println!("Guess the number!");
println!("Please input your guess.");
-这些代码仅仅打印一个提示,说明游戏的内容并请求用户输入。
+这些代码仅仅打印提示,介绍游戏的内容然后请用户输入。
接下来,创建一个地方储存用户输入,像这样:
-let mut guess = String::new();
+let mut guess = String::new();
-现在程序开始变得有意思了!这一小行代码发生了很多事。注意这是一个let
语句,用来创建变量。这里是另外一个例子:
-let foo = bar;
+现在程序开始变得有意思了!这一小行代码发生了很多事。注意这是一个 let
语句,用来创建变量。这里是另外一个例子:
+let foo = bar;
-这行代码会创建一个叫做foo
的新变量并把它绑定到值bar
上。在 Rust 中,变量默认是不可变的。下面的例子展示了如何在变量名前使用mut
来使一个变量可变:
+这行代码会创建一个叫做 foo
的新变量并把它绑定到值 bar
上。在 Rust 中,变量默认是不可变的。下面的例子展示了如何在变量名前使用 mut
来使一个变量可变:
let foo = 5; // immutable
let mut bar = 5; // mutable
注意://
开始一个注释,它持续到本行的结尾。Rust 忽略注释中的所有内容。
-现在我们知道了let mut guess
会引入一个叫做guess
的可变变量。等号(=
)的另一边是guess
所绑定的值,它是String::new
的结果,这个函数会返回一个String
的新实例。String
是一个标准库提供的字符串类型,它是可增长的、UTF-8 编码的文本块。
-::new
那一行的::
语法表明new
是String
类型的一个关联函数(associated function)。关联函数是针对类型实现的,在这个例子中是String
,而不是String
的某个特定实例。一些语言中把它称为静态方法(static method)。
-new
函数创建了一个新的空的String
,你会在很多类型上发现new
函数,因为这是创建某个类型新值的常用函数名。
-总结一下,let mut guess = String::new();
这一行创建了一个可变变量,目前它绑定到一个String
新的、空的实例上。哟!
-回忆一下我们在程序的第一行使用use std::io;
从标准库中引用输入/输出功能。现在在io
上调用一个关联函数,stdin
:
+现在我们知道了 let mut guess
会引入一个叫做 guess
的可变变量。等号(=
)的右边是 guess
所绑定的值,它是 String::new
的结果,这个函数会返回一个 String
的新实例。String
是一个标准库提供的字符串类型,它是 UTF-8 编码的可增长文本块。
+::new
那一行的 ::
语法表明 new
是 String
类型的一个 关联函数(associated function)。关联函数是针对类型实现的,在这个例子中是 String
,而不是 String
的某个特定实例。一些语言中把它称为静态方法(static method)。
+new
函数创建了一个新的空 String
,你会在很多类型上发现new
函数,这是创建类型实例的惯用函数名。
+总结一下,let mut guess = String::new();
这一行创建了一个可变变量,绑定到一个新的 String
空实例上。
+回忆一下,我们在程序的第一行使用 use std::io;
从标准库中引入“输入输出”。现在调用 io
的关联函数 stdin
:
io::stdin().read_line(&mut guess)
.expect("Failed to read line");
-如果我们在程序的开头没有use std::io
这一行,我们可以把函数调用写成std::io::stdin
这样。stdin
函数返回一个 std::io::Stdin
的实例,这是一个代表终端标准输入句柄的类型。
-代码的下一部分,.read_line(&mut guess)
,调用 read_line
方法从标准输入句柄获取用户输入。我们还向read_line()
传递了一个参数:&mut guess
。
-read_line
的工作是把获取任何用户键入到标准输入的字符并放入一个字符串中,所以它获取字符串作为一个参数。这个字符串需要是可变的,这样这个方法就可以通过增加用户的输入来改变字符串的内容。
-&
表明这个参数是一个引用(reference),它提供了一个允许多个不同部分的代码访问同一份数据而不需要在内存中多次拷贝的方法。引用是一个复杂的功能,而 Rust 的一大优势就是它是如何安全而优雅操纵引用的。完成这个程序并不需要知道这么多细节:第四章会更全面的解释引用。现在,我们只需知道它像变量一样,默认是不可变的。因此,需要写成&mut guess
而不是&guess
来使其可变。
-我们还没有分析完这行代码。虽然这是单独一行代码,但它只是一个逻辑上代码行(虽然换行了但仍是一个语句)的第一部分。第二部分是这个方法:
-.expect("Failed to read line");
-
-当使用.expect()
语法调用方法时,明智的选择是换行并留出空白(缩进)来把长的代码行拆开。我们可以把代码写成这样:
-io::stdin().read_line(&mut guess).expect("Failed to read line");
-
-不过,过长的代码行难以阅读,所以最好拆开来写,两行代码两个方法调用。现在来看看这行代码干了什么。
-使用Result
类型来处理潜在的错误
-之前提到过,read_line
将用户输入放入到传递给它字符串中,不过它也返回一个值——在这个例子中,一个io::Result
。Rust 标准库中有很多叫做Result
的类型。一个Result
泛型以及对应子模块的特定版本,比如io::Result
。
-Result
类型是 枚举(enumerations),通常也写作 enums。枚举拥有固定值集合的类型,而这些值被称为枚举的成员(variants)。第六章会更详细的介绍枚举。
-对于Result
,它的成员是Ok
或Err
,Ok
表明操作成功了,同时Ok
成员之中包含成功生成的值。Err
意味着操作失败,Err
之中包含操作是为什么或如何失败的信息。
-Result
类型的作用是编码错误处理信息。Result
类型的值,正如其他任何类型,拥有定义于其上的方法。io::Result
的实例拥有expect
方法可供调用。如果io::Result
实例的值是Err
,expect
会导致程序崩溃并显示显示你作为参数传递给expect
的信息。如果io::Result
实例的值是Ok
,expect
会获取Ok
中的值并原原本本的返回给你。在本例中,这个值是用户输入到标准输入中的字节的数量。
-如果不使用expect
,程序也能编译,不过会出现一个警告:
+如果程序的开头没有 use std::io
这一行,我们可以把函数调用写成 std::io::stdin
。stdin
函数返回一个 std::io::Stdin
的实例,这代表终端标准输入句柄的类型。
+代码的下一部分,.read_line(&mut guess)
,调用 read_line
方法从标准输入句柄获取用户输入。我们还向 read_line()
传递了一个参数:&mut guess
。
+read_line
的工作是,无论用户在标准输入中键入什么内容,都将其存入一个字符串中,因此它需要字符串作为参数。这个字符串参数应该是可变的,以便 read_line
将用户输入附加上去。
+&
表示这个参数是一个引用(reference),它允许多处代码访问同一处数据,而无需在内存中多次拷贝。引用是一个复杂的特性,Rust 的一个主要优势就是安全而简单的操纵引用。完成当前程序并不需要了解如此多细节:第四章会更全面的解释引用。现在,我们只需知道它像变量一样,默认是不可变的,需要写成 &mut guess
而不是 &guess
来使其可变。
+我们还没有分析完这行代码。虽然这是单独一行代码,但它是一个逻辑行(虽然换行了但仍是一个语句)的第一部分。第二部分是这个方法:
+.expect("Failed to read line");
+
+当使用 .expect()
语法调用方法时,通过‘换行并缩进’来把长行拆开,是明智的。我们完全可以这样写:
+io::stdin().read_line(&mut guess).expect("Failed to read line");
+
+不过,过长的行难以阅读,所以最好拆开来写,两行代码两个方法调用。现在来看看这行代码干了什么。
+使用 Result
类型来处理潜在的错误
+之前提到,read_line
将用户输入附加到传递给它字符串中,不过它也返回一个值——在这个例子中是 io::Result
。Rust 标准库中有很多叫做 Result
的类型。一个 Result
泛型以及对应子模块的特定版本,比如 io::Result
。
+Result
类型是 枚举(enumerations),通常也写作 enums。枚举类型持有固定集合的值,这些值被称为枚举的成员(variants)。第六章将介绍枚举的更多细节。
+对于 Result
,它的成员是 Ok
或 Err
,Ok
表示操作成功,内部包含产生的值。Err
意味着操作失败,包含失败的前因后果。
+Result
类型的作用是编码错误处理信息。Result
类型的值,像其他类型一样,拥有定义于其上的方法。io::Result
的实例拥有expect
方法,如果实例的值是 Err
,expect
会导致程序崩溃,并显示当做参数传递给 expect
的信息;如果实例的值是 Ok
,expect
会获取 Ok
中的值并原样返回。在本例中,这个值是用户输入到标准输入中的字节的数量。
+如果不使用 expect
,程序也能编译,不过会出现一个警告:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
src/main.rs:10:5: 10:39 warning: unused result which must be used,
@@ -184,18 +184,18 @@ src/main.rs:10:5: 10:39 warning: unused result which must be used,
src/main.rs:10 io::stdin().read_line(&mut guess);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Rust 警告我们没有使用read_line
返回的值Result
,表明程序没有处理一个可能的错误。消除警告的正确方式是老实编写错误处理,不过因为我们仅仅希望程序出现问题就崩溃,可以直接使用expect
。第九章会学习从错误中恢复的内容。
-使用println!
占位符打印值
-除了位于结尾的大括号,目前为止编写的代码就只有一行代码值得讨论一下了,就是这一行:
-println!("You guessed: {}", guess);
+Rust 警告我们没有使用 read_line
的返回值 Result
,说明有一个可能的错误没处理。想消除警告,就老实的写错误处理,不过我们就是希望程序在出现问题时立即崩溃,所以直接使用 expect
。第九章会学习如何从错误中恢复。
+使用 println!
占位符打印值
+除了位于结尾的大括号,目前为止就只有一行代码值得讨论一下了,就是这一行:
+println!("You guessed: {}", guess);
-这行代码打印出存储了用户输入的字符串。这对{}
是一个在特定位置预留值的占位符。可以使用{}
打印多个值:第一个{}
对应格式化字符串之后列出的第一个值,第二个对应第二个值,以此类推。用一个println!
调用打印多个值应该看起来像这样:
+这行代码打印存储用户输入的字符串。第一个参数是格式化字符串,里面的 {}
是预留在特定位置的占位符。使用占位符也可以打印多个值:格式化字符串中第一个占位符对应第二个参数值,第二个占位符对应第三个参数值,以此类推(第一个参数是格式化字符串本身)。调用一次 println!
打印多个值看起来像这样:
let x = 5;
let y = 10;
println!("x = {} and y = {}", x, y);
-这行代码会打印出x = 5 and y = 10
。
+这行代码会打印出 x = 5 and y = 10
。
测试第一部分代码
让我们来测试下猜猜看游戏的第一部分。使用cargo run
运行它:
$ cargo run
@@ -206,19 +206,19 @@ Please input your guess.
6
You guessed: 6
-至此为止,游戏的第一部分已经完成:我们从键盘获取了输入并打印了出来。
+至此为止,游戏的第一部分已经完成:我们从键盘获取输入并打印了出来。
生成一个秘密数字
-接下来,需要生成一个秘密数字,用户会尝试猜测它。秘密数字应该每次都不同,这样多玩几次才会有意思。生成一个 1 到 100 之间的随机数这样游戏也不会太难。Rust 标准库中还未包含随机数功能。然而,Rust 团队确实提供了一个rand
crate。
+接下来,需要生成一个秘密数字,好让用户来猜。秘密数字应该每次都不同,这样重复玩才不会乏味;范围应该在 1 到 100 之间,这样才不会太困难。Rust 标准库中尚未包含随机数功能。然而,Rust 团队还是提供了一个 rand
crate。
使用 crate 来增加更多功能
-记住 crate 是一个 Rust 代码的包。我们正在构建的项目是一个二进制 crate,它生成一个可执行文件。 rand
crate 是一个 库 crate,它包含意在被其他程序使用的代码。
-Cargo 对外部 crate 的运用是其真正闪光的地方。在我们可以使用rand
编写代码之前,需要编辑 Cargo.toml 来包含rand
作为一个依赖。现在打开这个文件并在[dependencies]
部分标题(Cargo 为你创建了它)的下面添加如下代码:
+记住 crate 是一个 Rust 代码的包。我们正在构建的项目是一个二进制 crate,它生成一个可执行文件。 rand
crate 是一个 库 crate,库 crate 可以包含任意能被其他程序使用的代码。
+Cargo 对外部 crate 的运用是亮点。在我们使用 rand
编写代码之前,需要编辑 Cargo.toml ,声明 rand
作为一个依赖。现在打开这个文件并在 [dependencies]
标题(Cargo 为你创建了它)之下添加:
Filename: Cargo.toml
[dependencies]
rand = "0.3.14"
-在 Cargo.toml 文件中,任何标题之后的内容都是属于这个部分的,一直持续到直到另一个部分开始。[dependencies]
部分告诉 Cargo 项目依赖了哪个外部 crate 和需要的 crate 版本。在这个例子中,我们使用语义化版本符号0.3.14
来指定rand
crate。Cargo 理解语义化版本(Semantic Versioning)(有时也称为 SemVer),这是一个编写版本号的标准。版本号0.3.14
事实上是^0.3.14
的缩写,它的意思是“任何与 0.3.14 版本公有 API 相兼容的版本”。
-现在,不用修改任何代码,构建项目,如列表 2-2 所示:
+在 Cargo.toml 文件中,标题以及之后的内容属同一个段落,遇到下一个标题则开始新的段落。[dependencies]
部分告诉 Cargo 本项目依赖了哪些外部 crate 及其版本。本例中,我们使用语义化版本 0.3.14
来指定 rand
crate。Cargo 理解语义化版本(Semantic Versioning)(有时也称为 SemVer),是一种定义版本号的标准。0.3.14
事实上是 ^0.3.14
的简写,它表示“任何与 0.3.14 版本公有 API 相兼容的版本”。
+现在,不修改任何代码,构建项目,如列表 2-2 所示:
$ cargo build
Updating registry `https://github.com/rust-lang/crates.io-index`
Downloading rand v0.3.14
@@ -229,40 +229,40 @@ rand = "0.3.14"
-可能会出现不同的版本号(不过多亏了语义化版本,它们与代码是兼容的!),同时显示顺序也可能会有所不同。
-现在我们有了一个外部依赖,Cargo 从 registry (Crates.io)上获取了一份(兼容的)最新版本代码的拷贝。Crates.io 是 Rust 生态环境中的开发者们向他人贡献他们的开源 Rust 项目的地方。
-在更新完 registry (索引)后,Cargo 检查[dependencies]
部分并下载还不存在的部分。在这个例子中,虽然只列出了rand
一个依赖,Cargo 也获取了一份libc
的拷贝,因为rand
依赖libc
来正常工作。在下载他们之后,Rust 编译他们并接着使用这些依赖编译项目。
-如果不做任何修改就立刻再次运行cargo build
,则不会有任何输出。Cargo 知道它已经下载并编译了依赖,同时 Cargo.toml 文件中也没有任何相关修改。Cargo 也知道代码没有做任何修改,所以它也不会重新编译代码。因为无事可做,它简单的退出了。如果打开 src/main.rs 文件,并做一些普通的修改,保存并再次构建,只会出现一行输出:
+可能会出现不同的版本号(多亏了语义化版本,它们与代码是兼容的!),同时显示顺序也可能会有所不同。
+现在我们有了一个外部依赖,Cargo 从 registry (Crates.io)上获取了一份(兼容的)最新版本的代码。Crates.io 是 Rust 生态环境中的开发者们向他人贡献 Rust 开源项目的地方。
+在更新完 registry (索引)后,Cargo 检查 [dependencies]
段落并下载缺失的部分。本例中,只声明了 rand
一个依赖,然而 Cargo 还是额外获取了 libc
,因为 rand
依赖 libc
来正常工作。下载完成后,Rust 编译依赖,然后使用这些依赖编译项目。
+如果不做任何修改,立刻再次运行 cargo build
,则不会有任何输出。Cargo 知道它已经下载并编译了依赖,同时 Cargo.toml 文件也没有变动,并且代码也没有任何修改,所以它不会重新编译代码。因为无事可做,它简单的退出了。如果打开 src/main.rs 文件,做一些普通的修改,保存并再次构建,只会出现一行输出:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
-这一行表明 Cargo 只构建了对 src/main.rs 文件做出的微小修改。依赖没有被修改,所以 Cargo 知道可以复用已经为此下载并编译的代码。它只是重新构建了部分(项目)代码。
+这一行表示 Cargo 只针对 src/main.rs 文件的微小修改而构建。依赖没有变化,所以 Cargo 会复用已经为此下载并编译的代码。它只是重新构建了部分(项目)代码。
Cargo.lock 文件确保构建是可重现的
-Cargo 有一个机制来确保每次任何人重新构建代码都会生成相同的结果:Cargo 只会使用你指定的依赖的版本,除非你又手动指定了别的。例如,如果下周rand
crate 的v0.3.15
版本出来了,而它包含一个重要的 bug 修改并也含有一个会破坏代码运行的缺陷的时候会发生什么呢?
-这个问题的答案是 Cargo.lock 文件,它在第一次运行cargo build
时被创建并位于 guessing_game 目录。当第一次构建项目时,Cargo 计算出所有符合要求的依赖版本并接着写入 Cargo.lock 文件中。当将来构建项目时,Cargo 发现 Cargo.lock 存在就会使用这里指定的版本,而不是重新进行所有版本的计算。这使得你拥有了一个自动的可重现的构建。换句话说,项目会继续使用0.3.14
直到你显式升级,多亏了 Cargo.lock 文件。我们将会在这个文件编写全部的代码。
+Cargo 有一个机制来确保任何人在任何时候重新构建代码,都会产生相同的结果:Cargo 只会使用你指定的依赖的版本,除非你又手动指定了别的。例如,如果下周 rand
crate 的 v0.3.15
版本出来了,它修复了一个重要的 bug,同时也含有一个缺陷,会破坏代码的运行,这时会发生什么呢?
+答案是 Cargo.lock 文件。它在第一次运行 cargo build
时创建,并放在 guessing_game 目录,Cargo 计算出所有符合要求的依赖版本并写入 Cargo.lock 文件。当将来构建项目时,如果 Cargo.lock 存在,Cargo 就使用里面指定的版本,不会重新计算。自动使你拥有了一个可重现的构建。换句话说,项目会继续使用 0.3.14
直到你显式升级,感谢 Cargo.lock。
更新 crate 到一个新版本
当你确实需要升级 crate 时,Cargo 提供了另一个命令,update
,他会:
-- 忽略 Cargo.lock 文件并计算出所有符合 Cargo.toml 中规格的最新版本。
+- 忽略 Cargo.lock 文件,并计算出所有符合 Cargo.toml 声明的最新版本。
- 如果成功了,Cargo 会把这些版本写入 Cargo.lock 文件。
-不过,Cargo 默认只会寻找大于0.3.0
而小于0.4.0
的版本。如果rand
crate 发布了两个新版本,0.3.15
和0.4.0
,在运行cargo update
时会出现如下内容:
+不过,Cargo 默认只会寻找大于 0.3.0
而小于 0.4.0
的版本。如果 rand
crate 发布了两个新版本,0.3.15
和 0.4.0
,在运行 cargo update
时会出现如下内容:
$ cargo update
Updating registry `https://github.com/rust-lang/crates.io-index`
Updating rand v0.3.14 -> v0.3.15
这时,值得注意的是 Cargo.lock 文件中的一个改变,rand
crate 现在使用的版本是0.3.15
。
-如果想要使用0.4.0
版本的rand
或是任何0.4.x
系列的版本,必须像这样更新 Cargo.toml 文件:
+如果想要使用 0.4.0
版本的 rand
或是任何 0.4.x
系列的版本,必须像这样更新 Cargo.toml 文件:
[dependencies]
rand = "0.4.0"
-下一次运行cargo build
时,Cargo 会更新 registry 中可用的 crate 并根据你指定新版本重新计算rand
的要求。
-第十四章会讲到Cargo 和它的生态系统的更多内容,不过目前你只需要了解这么多。Cargo 使得复用库文件变得非常容易,所以 Rustacean 们能够通过组合很多包来编写出更轻巧的项目。
+下一次运行 cargo build
时,Cargo 会从 registry 更新,并根据你指定的新版本重新计算。
+第十四章会讲到 Cargo 及其生态系统的更多内容,不过目前你只需要了解这么多。通过 Cargo 复用库文件非常容易,因此 Rustacean 能够编写出由很多包组装而成的更轻巧的项目。
生成一个随机数
-让我们开始使用rand
。下一步是更新 src/main.rs,如列表 2-3 所示:
+让我们开始使用 rand
。下一步是更新 src/main.rs,如列表 2-3 所示:
Filename: src/main.rs
-extern crate rand;
+extern crate rand;
use std::io;
use rand::Rng;
@@ -286,10 +286,10 @@ fn main() {
-我们在顶部增加一行extern crate rand;
来让 Rust 知道我们要使用外部依赖。这也会调用相应的use rand
,所以现在可以使用rand::
前缀来调用rand
中的任何内容。
-接下来,我们增加了另一行use
:use rand::Rng
。Rng
是一个定义了随机数生成器应实现方法的 trait,如果要使用这些方法的话这个 trait 必须在作用域中。第十章会详细介绍 trait。
-另外,中间还新增加了两行。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
。
+我们在顶部增加一行 extern crate rand;
通知 Rust 我们要使用外部依赖。这也会调用相应的 use rand
,所以现在可以使用 rand::
前缀来调用 rand
中的内容。
+接下来,我们增加了一行 use
:use rand::Rng
。Rng
是一个 trait,它定义了随机数生成器应实现的方法 ,想使用这些方法的话此 trait 必须在作用域中。第十章会详细介绍 trait。
+另外,中间还新增加了两行。rand::thread_rng
函数提供实际使用的随机数生成器:它位于当前执行线程,并从操作系统获取 seed。接下来,调用随机数生成器的 gen_range
方法。这个方法由刚才引入到作用域的 Rng
trait 定义。gen_range
方法获取两个数字作为参数,并生成一个范围在两者之间的随机数。它包含下限但不包含上限,所以需要指定1
和101
来请求一个1
和100
之间的数。
+知道 use 哪个 trait 和该从 crate 中调用哪个方法并不是全部,crate 的说明位于其文档中,Cargo 有一个很棒的功能是:运行 cargo doc --open
命令来构建所有本地依赖提供的文档,并在浏览器中打开。例如,假设你对 rand
crate 中的其他功能感兴趣,cargo doc --open
并点击左侧导航栏中的 rand
。
新增加的第二行代码打印出了秘密数字。这在开发程序时很有用,因为我们可以去测试它,不过在最终版本我们会删掉它。游戏一开始就打印出结果就没什么可玩的了!
尝试运行程序几次:
$ cargo run
@@ -312,7 +312,7 @@ You guessed: 5
比较猜测与秘密数字
现在有了用户输入和一个随机数,我们可以比较他们。这个步骤如列表 2-4 所示:
Filename: src/main.rs
-extern crate rand;
+extern crate rand;
use std::io;
use std::cmp::Ordering;
@@ -343,17 +343,17 @@ fn main() {
-新代码的第一行是另一个use
,从标准库引入了一个叫做std::cmp::Ordering
的类型到作用域。Ordering
是另一个枚举,像Result
一样,不过Ordering
的成员是Less
、Greater
和Equal
。这是你比较两个值时可能出现三种结果。
-接着在底部的五行新代码使用了Ordering
类型:
-match guess.cmp(&secret_number) {
+新代码的第一行是另一个 use
,从标准库引入了一个叫做 std::cmp::Ordering
的类型。Ordering
是一个像 Result
一样的枚举,不过它的成员是 Less
、Greater
和 Equal
。这是你做比较时可能出现的三种结果。
+接着,底部的五行新代码使用了 Ordering
类型:
+match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
-cmp
方法比较两个值并可以在任何可比较的值上调用。它获取一个任何你想要比较的值的引用:这里是把guess
与secret_number
做比较。cmp
返回一个使用use
语句引入作用域的Ordering
枚举的成员。我们使用一个match
表达式根据对guess
和secret_number
中的值调用cmp
后返回的哪个Ordering
枚举成员来决定接下来干什么。
-一个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
表达式就此终止,因为在这个特定场景下没有检查最后一个分支的必要。
+cmp
方法用来比较两个值。在任何可比较的值上调用,然后获取另一个被比较值的引用:这里是把 guess
与 secret_number
做比较,返回一个 Ordering
枚举的成员。再使用一个 match
表达式,根据枚举成员来决定接下来干什么。
+一个 match
表达式由 分支(arms) 构成。一个分支包含一个 模式(pattern)和动作,表达式头的求值结果符合分支的模式时将执行对应的动作。Rust 获取提供给 match
的值并挨个检查每个分支的模式。match
结构和模式是 Rust 的强大功能,它体现了代码可能遇到的多种情形,并帮助你没有遗漏的处理。这些功能将分别在第六章和第十八章详细介绍。
+让我们看看使用 match
表达式的例子。假设用户猜了 50,这时随机生成的秘密数字是 38。比较 50 与 38 时,因为 50 比 38 要大,cmp
方法会返回 Ordering::Greater
。match
表达式得到该值,然后检查第一个分支的模式,Ordering::Less
与 Ordering::Greater
并不匹配,所以它忽略了这个分支的动作并来到下一个分支。下一个分支的模式是 Ordering::Greater
,正确匹配!这个分支关联的动作被执行,在屏幕打印出 Too big!
。match
表达式就此终止,因为该场景下没有检查最后一个分支的必要。
然而,列表 2-4 的代码并不能编译,可以尝试一下:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
@@ -369,10 +369,10 @@ error[E0308]: mismatched types
error: aborting due to previous error
Could not compile `guessing_game`.
-错误的核心表明这里有不匹配的类型(mismatched types)。Rust 拥有一个静态强类型系统。不过,它也有类型推断。当我们写出let guess = String::new()
时,Rust 能够推断出guess
应该是一个String
,并不需要我们写出类型。另一方面,secret_number
,是一个数字类型。一些数字类型拥有 1 到 100 之间的值:i32
,一个 32 位的数字;u32
,一个 32 位无符号数字;i64
,一个 64 位数字;等等。Rust 默认使用i32
,所以secret_number
的类型就是它,除非增加类型信息或任何能让 Rust 推断出不同数值类型的信息。这里错误的原因在于 Rust 不会比较字符串类型和数字类型。
-最终我们想要把程序从输入中读取到的String
转换为一个真正的数字类型,这样好与秘密数字向比较。可以通过在main
函数体中增加如下两行代码来实现:
+错误的核心表明这里有不匹配的类型(mismatched types)。Rust 有一个静态强类型系统,同时也有类型推断。当我们写出 let guess = String::new()
时,Rust 推断出 guess
应该是一个String
,不需要我们写出类型。另一方面,secret_number
,是一个数字类型。多种数字类型拥有 1 到 100 之间的值:32 位数字 i32
;32 位无符号数字 u32
;64 位数字 i64
等等。Rust 默认使用 i32
,所以它是 secret_number
的类型,除非增加类型信息,或任何能让 Rust 推断出不同数值类型的信息。这里错误的原因在于 Rust 不会比较字符串类型和数字类型。
+所以我们必须把从输入中读取到的 String
转换为一个真正的数字类型,才好与秘密数字进行比较。可以通过在 main
函数体中增加如下两行代码来实现:
Filename: src/main.rs
-extern crate rand;
+extern crate rand;
use std::io;
use std::cmp::Ordering;
@@ -405,13 +405,13 @@ fn main() {
}
这两行代码是:
-let guess: u32 = guess.trim().parse()
+let guess: u32 = guess.trim().parse()
.expect("Please type a number!");
-这里创建了一个叫做guess
的变量。不过等等,难道这个程序不是已经有了一个叫做guess
的变量了吗?确实如此,不过 Rust 允许我们通过覆盖(shadow) 用一个新值来覆盖guess
之前的值。这个功能经常用在类似需要把一个值从一种类型转换到另一种类型的场景。shadowing 允许我们复用guess
变量的名字而不是强迫我们创建两个不同变量,比如guess_str
和guess
。(第三章会介绍 shadowing 的更多细节。)
-guess
被绑定到guess.trim().parse()
表达式。表达式中的guess
对应包含输入的String
类型的原始guess
。String
实例的trim
方法会消除字符串开头和结尾的空白。u32
只能包含数字字符。不过用户必须输入回车键才能让read_line
返回。当用户按下回车键时,会在字符串中增加一个换行(newline)字符。例如,如果用户输入 5 并回车,guess
看起来像这样:5\n
。\n
代表“换行”,回车键。trim
方法消除\n
,只留下5
。
-字符串的parse
方法解析一个字符串成某个数字。因为这个方法可以解析多种数字类型,需要告诉 Rust 我们需要的具体的数字类型,这里通过let guess: u32
指定。guess
后面的冒号(:
)告诉 Rust 我们指明了变量的类型。Rust 有一些内建的数字类型;这里的u32
是一个无符号的 32 位整型。它是一个好的较小正整数的默认类型。第三章会讲到其他数字类型。另外,例子程序中的u32
注解和与secret_number
的比较意味着 Rust 会推断secret_number
应该是也是u32
类型。现在可以使用相同类型比较两个值了!
-parse
调用容易产生错误。例如,如果字符串包含A👍%
,就无法将其转换为一个数字。因为它可能失败,parse
方法返回一个Result
类型,非常像之前在“使用Result
类型来处理潜在的错误”部分讨论的read_line
方法。这里再次类似的使用expect
方法处理这个Result
类型。如果parse
因为不能从字符串生成一个数字而返回一个Err
的Result
成员时,expect
会使游戏崩溃并打印提供给它的信息。如果parse
能成功地将字符串转换为一个数字,它会返回Result
的Ok
成员,同时expect
会返回Ok
中我们需要的数字。
+这里创建了一个叫做 guess
的变量。不过等等,不是已经有了一个叫做guess
的变量了吗?确实如此,不过 Rust 允许遮盖(shadow),用一个新值来遮盖 guess
之前的值。这个功能常用在需要转换值类型之类的场景,它允许我们复用 guess
变量的名字,而不是被迫创建两个不同变量,诸如 guess_str
和 guess
之类。(第三章会介绍 shadowing 的更多细节。)
+guess
被绑定到 guess.trim().parse()
表达式。表达式中的 guess
是包含输入的 String
类型。String
实例的 trim
方法会去除字符串开头和结尾的空白。u32
只能由数字字符转换,不过用户必须输入回车键才能让 read_line
返回,然而用户按下回车键时,会在字符串中增加一个换行(newline)符。例如,用户输入 5 并回车,guess
看起来像这样:5\n
。\n
代表“换行”,回车键。trim
方法消除 \n
,只留下5
。
+字符串的parse
方法 将字符串解析成数字。这个方法可以解析多种数字类型,因此需要告诉 Rust 具体的数字类型,这里通过 let guess: u32
指定。guess
后面的冒号(:
)告诉 Rust 我们指定了变量的类型。Rust 有一些内建的数字类型;u32
是一个无符号的 32 位整型。对于不大的正整数来说,它是不错的类型,第三章还会讲到其他数字类型。另外,程序中的 u32
注解以及与 secret_number
的比较,意味着 Rust 会推断出 secret_number
也是 u32
类型。现在可以使用相同类型比较两个值了!
+parse
调用可能产生错误。例如,字符串中包含 A👍%
,就无法将其转换为一个数字。因此,parse
方法返回一个 Result
类型。像之前讨论的 read_line
方法,按部就班的用 expect
方法处理即可。如果 parse
不能从字符串生成一个数字,返回一个 Result::Err
时,expect
会使游戏崩溃并打印附带的信息。如果 parse
成功地将字符串转换为一个数字,它会返回 Result::Ok
,然后 expect
会返回 Ok
中的数字。
现在让我们运行程序!
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
@@ -426,9 +426,9 @@ Too big!
漂亮!即便是在猜测之前添加了空格,程序依然能判断出用户猜测了 76。多运行程序几次来检验不同类型输入的相应行为:猜一个正确的数字,猜一个过大的数字和猜一个过小的数字。
现在游戏已经大体上能玩了,不过用户只能猜一次。增加一个循环来改变它吧!
使用循环来允许多次猜测
-loop
关键字提供了一个无限循环。增加它后给了用户多次猜测的机会:
+loop
关键字提供了一个无限循环。将其加入后,用户可以反复猜测:
Filename: src/main.rs
-extern crate rand;
+extern crate rand;
use std::io;
use std::cmp::Ordering;
@@ -462,8 +462,8 @@ fn main() {
}
}
-如上所示,我们将提示用户猜测之后的所有内容放入了循环。确保这些代码多缩进了四个空格,并再次运行程序。注意这里有一个新问题,因为程序忠实地执行了我们要求它做的:永远地请求另一个猜测!看起来用户没法退出啊!
-用户总是可以使用Ctrl-C
快捷键来终止程序。不过这里还有另一个逃离这个贪得无厌的怪物的方法,就是在“比较猜测”部分提到的parse
:如果用户输入一个非数字回答,程序会崩溃。用户可以利用这一点来退出,如下所示:
+如上所示,我们将提示用户猜测之后的所有内容放入了循环。确保这些代码额外缩进了一层,再次运行程序。注意这里有一个新问题,因为程序忠实地执行了我们的要求:永远地请求另一个猜测,用户没法退出!
+用户总能使用 Ctrl-C
终止程序。不过还有另一个方法跳出无限循环,就是“比较猜测”部分提到的 parse
:如果用户输入一个非数字答案,程序会崩溃。用户可以利用这一点来退出,如下所示:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Running `target/guessing_game`
@@ -487,11 +487,11 @@ thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidD
note: Run with `RUST_BACKTRACE=1` for a backtrace.
error: Process didn't exit successfully: `target/debug/guess` (exit code: 101)
-输入quit
确实退出了程序,同时其他任何非数字输入也一样。然而,毫不夸张的说这是不理想的。我们想要当猜测正确的数字时游戏能自动退出。
+输入 quit
确实退出了程序,同时其他任何非数字输入也一样。然而,这并不理想,我们想要当猜测正确的数字时游戏能自动退出。
猜测正确后退出
-让我们增加一个break
来在用户胜利时退出游戏:
+让我们增加一个 break
,在用户猜对时退出游戏:
Filename: src/main.rs
-extern crate rand;
+extern crate rand;
use std::io;
use std::cmp::Ordering;
@@ -528,18 +528,18 @@ fn main() {
}
}
-通过在You win!
之后增加一行break
,程序在用户猜对了神秘数字后会退出循环。退出循环也就意味着退出程序,因为循环是main
的最后一部分。
+通过在 You win!
之后增加一行 break
,用户猜对了神秘数字后会退出循环。退出循环也意味着退出程序,因为循环是 main
的最后一部分。
处理无效输入
-为了进一步改善游戏性,而不是在用户输入非数字时崩溃,需要让游戏忽略非数字从而用户可以继续猜测。可以通过修改guess
从String
转化为u32
那部分代码来实现:
-let guess: u32 = match guess.trim().parse() {
+为了进一步改善游戏性,不要在用户输入非数字时崩溃,需要忽略非数字,让用户可以继续猜测。可以通过修改 guess
将 String
转化为 u32
那部分代码来实现:
+let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
-从expect
调用切换到match
语句是如何从遇到错误就崩溃到真正处理错误的常用手段。记住parse
返回一个Result
类型,而Result
是一个拥有Ok
或Err
两个成员的枚举。在这里使用match
表达式,就像之前处理cmp
方法返回的Ordering
一样。
-如果parse
能够成功的将字符串转换为一个数字,它会返回一个包含结果数字Ok
值。这个Ok
值会匹配第一个分支的模式,这时match
表达式仅仅返回parse
产生的Ok
值之中的num
值。这个数字会最终如期变成新创建的guess
变量。
-如果parse
不能将字符串转换为一个数字,它会返回一个包含更多错误信息的Err
值。Err
值不能匹配第一个match
分支的Ok(num)
模式,但是会匹配第二个分支的Err(_)
模式。_
是一个包罗万象的值;在这个例子中,我们想要匹配所有Err
值,不管其中有何种信息。所以程序会执行第二个分支的代码,continue
,这意味着进入loop
的下一次循环并请求另一个猜测。这样程序就有效地忽略了parse
可能遇到的所有错误!
-现在万事俱备(只欠东风)了。运行cargo run
来尝试一下:
+将 expect
调用换成 match
语句,是从“立即崩溃”转到真正处理错误的惯用方法。须知 parse
返回一个 Result
类型,而 Result
是一个拥有 Ok
或 Err
成员的枚举。这里使用的 match
表达式,和之前处理 cmp
方法返回 Ordering
时用的一样。
+如果 parse
能够成功的将字符串转换为一个数字,它会返回一个包含结果数字的 Ok
。这个 Ok
值与match
第一个分支的模式相匹配,该分支对应的动作返回 Ok
值中的数字 num
,最后如愿变成新创建的 guess
变量。
+如果 parse
不能将字符串转换为一个数字,它会返回一个包含更多错误信息的 Err
。Err
值不能匹配第一个 match
分支的 Ok(num)
模式,但是会匹配第二个分支的 Err(_)
模式:_
是一个兜底值,用来匹配所有 Err
值,不管其中有何种信息。所以程序会执行第二个分支的动作,continue
意味着进入 loop
的下一次循环,请求另一个猜测。这样程序就忽略了 parse
可能遇到的所有错误!
+现在万事俱备,只需运行 cargo run
:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Running `target/guessing_game`
@@ -560,9 +560,9 @@ Please input your guess.
You guessed: 61
You win!
-太棒了!再有最后一个小的修改,就能完成猜猜看游戏了:还记得程序依然会打印出秘密数字。这在测试时还好,但会毁了游戏性。删掉打印秘密数字的println!
。列表 2-5 为最终代码:
+太棒了!再有最后一个小的修改,就能完成猜猜看游戏了:还记得程序依然会打印出秘密数字。在测试时还好,但正式发布时会毁了游戏。删掉打印秘密数字的 println!
。列表 2-5 为最终代码:
Filename: src/main.rs
-extern crate rand;
+extern crate rand;
use std::io;
use std::cmp::Ordering;
@@ -602,7 +602,7 @@ fn main() {
总结
此时此刻,你顺利完成了猜猜看游戏!恭喜!
-这是一个通过动手实践的方式想你介绍许多 Rust 新知识的项目:let
、match
、方法、关联函数,使用外部 crate,等等。接下来的几章,我们将会详细学习这些概念。第三章涉及到大部分编程语言都有的概念,比如变量、数据类型和函数,以及如何在 Rust 中使用他们。第四章探索所有权(ownership),这是一个 Rust 同其他语言都不相同的功能。第五章讨论结构体和方法的语法,而第六章侧重解释枚举。
+这是一个通过动手实践学习 Rust 新概念的项目:let
、match
、方法、关联函数、使用外部 crate 等等,接下来的几章,我们将会继续深入。第三章涉及到大部分编程语言都有的概念,比如变量、数据类型和函数,以及如何在 Rust 中使用他们。第四章探索所有权(ownership),这是一个 Rust 同其他语言都不相同的功能。第五章讨论结构体和方法的语法,而第六章侧重解释枚举。
diff --git a/docs/ch03-00-common-programming-concepts.html b/docs/ch03-00-common-programming-concepts.html
index b21a412..ad3cb5d 100644
--- a/docs/ch03-00-common-programming-concepts.html
+++ b/docs/ch03-00-common-programming-concepts.html
@@ -73,11 +73,11 @@
commit 04aa3a45eb72855b34213703718f50a12a3eeec8
-这一章涉及到几乎出现在所有编程语言中的概念,以及他们在 Rust 中是如何工作的。很多编程语言在核心概念上都是共通的。本章中展示的所有概念没有一个是 Rust 所特有的,不过我们会在 Rust 环境中讨论他们并解释他们的使用习惯。
-具体的,我们将会学习变量,基本类型,函数,注释和控制流。这些基础知识将会出现在每一个 Rust 程序中,提早学习这些概念会使你在起步时拥有一个强有力的基础。
+本章涉及一些几乎所有编程语言都有的概念,以及他们在 Rust 中是如何工作的。很多编程语言的核心概念都是共通的,本章中展示的概念都不是 Rust 特有的,不过我们会在 Rust 环境中讨论他们,解释他们的使用习惯。
+具体的,我们将会学习变量,基本类型,函数,注释和控制流。这些基础知识将会出现在每一个 Rust 程序中,提早学习这些概念会使你拥有坚实的起步。
关键字
-Rust 语言有一系列被保留为只能被语言使用的关键字(keywords),如大部分语言一样。注意你不能使用这些关键字作为变量或函数的名称。大部分关键字有特殊的意义,并将会被用来进行 Rust 程序中的多种任务;一些关键字目前没有相关的功能不过为了将来可能添加进 Rust 的功能而被保留。可以在附录 A 中找到一份关键字的列表
+Rust 语言有一系列保留的关键字(keywords),只能由语言本身使用,像大部分语言一样。你不能使用这些关键字作为变量或函数的名称,大部分关键字有特殊的意义,并被用来完成 Rust 程序中的各种任务;一些关键字目前没有分配,是为将来可能添加的功能保留的。可以在附录 A 中找到关键字的列表。
diff --git a/docs/ch03-01-variables-and-mutability.html b/docs/ch03-01-variables-and-mutability.html
index ab64150..c9fe257 100644
--- a/docs/ch03-01-variables-and-mutability.html
+++ b/docs/ch03-01-variables-and-mutability.html
@@ -73,8 +73,8 @@
commit 04aa3a45eb72855b34213703718f50a12a3eeec8
-第二章中提到过,变量默认是不可变(immutable)的。这是 Rust 中许多鼓励利用 Rust 提供的安全和简单并发优势编写代码的助力之一。不过,仍然有使变量可变的选项。让我们探索一下如何以及为什么鼓励你拥抱不可变性,还有为什么你可能想要弃之不用。
-当变量是不可变时,这意味着一旦一个值被绑定上了一个名称,你就不能改变这个值。作为说明,通过cargo new --bin variables
在 projects 目录生成一个叫做 variables 的新项目。
+第二章中提到过,变量默认是不可变(immutable)的。这是利用 Rust 安全和简单并发的优势编写代码一大助力。不过,变量仍然有可变的选项。让我们探讨一下,拥抱不可变性的原因及方法,以及何时你不想拥抱。
+当变量不可变时,意味着一旦值被绑定上一个名称,你就不能改变这个值。作为说明,通过cargo new --bin variables
在 projects 目录生成一个叫做 variables 的新项目。
接着,在新建的 variables 目录,打开 src/main.rs 并替换其代码为如下:
Filename: src/main.rs
fn main() {
@@ -96,10 +96,10 @@ error[E0384]: re-assignment of immutable variable `x`
4 | x = 6;
| ^^^^^ re-assignment of immutable variable
-这个例子显示了编译器如何帮助你寻找程序中的错误。即便编译器错误可能是令人沮丧的,他们也仅仅意味着程序不能安全的完成你想让它完成的工作;他们不能说明你不是一个好的程序员!有经验的 Rustacean 们也会遇到编译器错误。这些错误表明错误的原因是对不可变变量重新赋值
(re-assignment of immutable variable
),因为我们尝试对不可变变量x
赋第二个值。
-当尝试去改变之前设计为不可变的值出现编译时错误是很重要的,因为这种情况可能导致 bug。如果代码的一部分假设一个值永远也不会改变而另一部分代码改变了它,这样第一部分代码就有可能不能像它设计的那样运行。不得不承认这种 bug 难以跟踪,尤其是当第二部分代码只是有时当变量使不可变时。
-Rust 编译器保证如果声明一个值不会改变,它就真的不会改变。这意味着当阅读和编写代码时,并不需要记录如何以及在哪可能会被改变,这使得代码易于推导。
-不过可变性也是非常有用的。变量只是默认不可变;可以通过在变量名之前增加mut
来使其可变。它向之后的读者表明了其他部分的代码将会改变这个变量值的意图。
+这个例子展示了编译器如何帮助你找出程序中的错误。即便编译错误令人沮丧,那也不过是说程序不能安全的完成你想让它完成的工作;而不能说明你是不是一个好程序员!有经验的 Rustacean 们一样会遇到编译错误。这些错误给出的原因是对不可变变量重新赋值
(re-assignment of immutable variable
),因为我们尝试对不可变变量x
赋第二个值。
+尝试去改变预设为不可变的值,产生编译错误是很重要的,因为这种情况可能导致 bug:如果代码的一部分假设一个值永远也不会改变,而另一部分代码改变了它,第一部分代码就有可能以不可预料的方式运行。不得不承认这种 bug 难以跟踪,尤其是第二部分代码只是有时改变其值。
+Rust 编译器保证,如果声明一个值不会变,它就真的不会变。这意味着当阅读和编写代码时,不需要厘清如何以及哪里可能会被改变,从而使得代码易于推导。
+不过可变性也是非常有用的。变量只是默认不可变;可以通过在变量名之前加 mut
来使其可变。它向读者表明了其他代码将会改变这个变量的意图。
例如,改变 src/main.rs 并替换其代码为如下:
Filename: src/main.rs
fn main() {
@@ -116,21 +116,21 @@ error[E0384]: re-assignment of immutable variable `x`
The value of x is: 5
The value of x is: 6
-通过mut
,允许把绑定到x
的值从5
改成6
。在一些情况下,你会想要一个变量是可变的,因为这比只使用不可变变量实现的代码更易于编写。
-除了避免 bug 外,这里还有多个需要权衡取舍的地方。例如,有时使用大型数据结构时,适当地使变量可变可能比复制和返回新分配的实例要更快。对于较小的数据结构,总是创建新实例并采用一种更函数式的编程风格可能会使代码更易理解。所以为了可读性而造成的性能惩罚也许是值得的。
+通过 mut
,允许把绑定到 x
的值从 5
改成 6
。在一些情况下,你会想要一个变量可变,因为相对不可变的风格更容易写。
+除了避免 bug 外,还有多处需要权衡取舍。例如,使用大型数据结构时,适当地使变量可变,可能比复制和返回新分配的实例更快。对于较小的数据结构,总是创建新实例,采用更偏向函数式的风格编程,可能会使代码更易理解,为可读性而遭受性能惩罚或许值得。
变量和常量的区别
-不能改变一个变量的值可能会使你想起另一个大部分编程语言都有的概念:常量(constants)。类似于不可变变量,常量也是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。
-首先,不允许对常量使用mut
:常量不光是默认不能改变,它总是不能改变。
-常量使用const
关键字而不是let
关键字声明,而且必须注明值的类型。现在我们准备在下一部分,“数据类型”,涉及到类型和类型注解,所以现在无需担心这些细节,只需记住必须总是注明类型即可。
+不允许改变值的变量,可能会使你想起另一个大部分编程语言都有的概念:常量(constants)。类似于不可变变量,常量也是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。
+首先,不允许对常量使用 mut
:常量不光默认不能变,它总是不能变。
+声明常量使用 const
关键字而不是 let
,而且必须注明值的类型。在下一部分,“数据类型”,涉及到类型和类型注解,现在无需关心这些细节,记住总是标注类型即可。
常量可以在任何作用域声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。
-最后一个区别是常量只能用于常量表达式,而不能作为函数调用的结果或任何其他只在运行时计算的值。
-这是一个常量声明的例子,它的名称是MAX_POINTS
而它的值是 100,000。(Rust 常量的命名规范是使用大写字母和单词间使用下划线):
+最后一个区别是常量只能用于常量表达式,而不能作为函数调用的结果,或任何其他只在运行时计算的值。
+这是一个常量声明的例子,它的名称是 MAX_POINTS
,值是 100,000。(常量使用下划线分隔的大写字母命名):
const MAX_POINTS: u32 = 100_000;
-常量在整个程序生命周期中都有效,位于它声明的作用域之中。这使得常量可以用作多个部分的代码可能需要知道的程序范围的值,例如一个游戏中任何玩家可以获得的最高分或者光速。
-将用于整个程序的硬编码的值声明为为常量(并编写文档)对为将来代码维护者表明值的意义是很有用的。它也能帮助你将硬编码的值至于一处以便将来可能需要修改他们。
-覆盖(Shadowing)
-如第二章猜猜看游戏所讲到的,我们可以定义一个与之前变量名称相同的新变量,而新变量会覆盖之前的变量。Rustacean 们称其为第一个变量被第二个给覆盖了,这意味着第二个变量的值是使用这个变量时会看到的值。可以用相同变量名称来覆盖它自己以及重复使用let
关键字来多次覆盖,如下所示:
+常量在整个程序生命周期中都有效,位于它声明的作用域之中。这使得常量可以作为多处代码使用的全局范围的值,例如一个游戏中所有玩家可以获取的最高分或者光速。
+将作用于整个程序的值,由硬编码改为常量(并编写文档),对后来的维护者了解值的意义很用帮助。它也能将硬编码的值汇总一处,为将来可能的修改提供方便。
+遮盖(Shadowing)
+如第二章“猜猜看游戏”所讲的,我们可以定义一个与之前变量重名的新变量,而新变量会遮盖之前的变量。Rustacean 称之为“第一个变量被第二个遮盖了”,这意味着使用这个变量时会看第二个值。可以用相同变量名称来遮盖它自己,以及重复使用 let
关键字来多次遮盖,如下所示:
Filename: src/main.rs
fn main() {
let x = 5;
@@ -142,22 +142,22 @@ The value of x is: 6
println!("The value of x is: {}", x);
}
-这个程序首先将x
绑定到值5
上。接着通过let x =
覆盖x
,获取原始值并加1
这样x
的值就变成6
了。第三个let
语句也覆盖了x
,获取之前的值并乘以2
,x
的最终值是12
。当运行这个程序,它会有如下输出:
+这个程序首先将 x
绑定到值 5
上。接着通过 let x =
遮盖 x
,获取原始值并加 1
这样 x
的值就变成 6
了。第三个 let
语句也覆盖了 x
,获取之前的值并乘以 2
,x
最终的值是 12
。运行这个程序,它会有如下输出:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Running `target/debug/variables`
The value of x is: 12
-这与将变量声明为mut
是有区别的。因为除非再次使用let
关键字,不小心尝试对变量重新赋值会导致编译时错误。我们可以用这个值进行一些计算,不过计算完之后变量仍然是不变的。
-另一个mut
与覆盖的区别是当再次使用let
关键字时,事实上创建了一个新变量,我们可以改变值的类型。例如,假设程序请求用户输入空格来提供在一些文本之间需要多少空间来分隔,不过我们真正需要的是将输入存储成数字(多少个空格):
+这与将变量声明为 mut
是有区别的。因为除非再次使用 let
关键字,不小心尝试对变量重新赋值会导致编译时错误。我们可以用这个值进行一些计算,不过计算完之后变量仍然是不变的。
+mut
与遮盖的另一个区别是,当再次使用 let
时,实际上创建了一个新变量,我们可以改变值的类型。例如,假设程序请求用户输入空格来提供文本间隔,然而我们真正需要的是将输入存储成数字(多少个空格):
let spaces = " ";
let spaces = spaces.len();
-这里允许第一个spaces
变量是字符串类型,而第二个spaces
变量,它是一个恰巧与第一个变量名字相同的崭新的变量,它是数字类型。因此覆盖使我们不必使用不同的名字,比如spaces_str
和spaces_num
;相反,我们可以复用spaces
这个更简单的名称。然而,如果尝试使用mut
,如下所示:
+这里允许第一个 spaces
变量是字符串类型,而第二个 spaces
变量,它是一个恰巧与第一个变量同名的崭新变量,是数字类型。遮盖使我们不必使用不同的名字,如 spaces_str
和 spaces_num
;相反,我们可以复用 spaces
这个更简单的名字。然而,如果尝试使用mut
,如下所示:
let mut spaces = " ";
spaces = spaces.len();
-会导致一个编译时错误,因为不允许改变一个变量的类型:
+会导致一个编译错误,因为改变一个变量的类型是不被允许的:
error[E0308]: mismatched types
--> src/main.rs:3:14
|
diff --git a/docs/ch03-02-data-types.html b/docs/ch03-02-data-types.html
index fb4f079..ff3c83b 100644
--- a/docs/ch03-02-data-types.html
+++ b/docs/ch03-02-data-types.html
@@ -73,11 +73,11 @@
commit fe4833a8ef2853c55424e7747a4ef8dd64c35b32
-Rust 中的任何值都有一个具体的类型(type),这告诉了 Rust 它被指定了何种数据,这样 Rust 就知道如何处理这些数据了。这一部分将讲到一些语言内建的类型。我们将这些类型分为两个子集:标量(scalar)和复合(compound)。
-贯穿整个部分,请记住 Rust 是一个静态类型(statically typed)语言,也就是说必须在编译时就知道所有变量的类型。编译器通常可以通过值以及如何使用他们来推断出我们想要用的类型。当多个类型都是可能的时候,比如第二章中parse
将String
转换为数字类型,必须增加类型注解,像这样:
+在 Rust 中,任何值都属于一种明确的类型(type),声明它被指定了何种数据,以便明确其处理方式。我们将分两部分探讨一些内建类型:标量(scalar)和复合(compound)。
+Rust 是静态类型(statically typed)语言,也就是说在编译时就需要知道所有变量的类型,这一认知将贯穿整个章节,请在头脑中明确。通过值的形式及其使用方式,编译器通常可以推断出我们想要用的类型。多种类型均有可能时,比如第二章中使用 parse
将 String
转换为数字,必须增加类型注解,像这样:
let guess: u32 = "42".parse().expect("Not a number!");
-如果这里不添加类型注解,Rust 会显示如下错误,它意味着编译器需要我们提供更多我们想要使用哪个可能的类型的信息:
+如果不添加类型注解,Rust 会显示如下错误。这说明编译器需要更多信息,来了解我们想要的类型:
error[E0282]: unable to infer enough type information about `_`
--> src/main.rs:2:9
|
@@ -86,11 +86,11 @@ commit fe4833a8ef2853c55424e7747a4ef8dd64c35b32
|
= note: type annotations or generic parameter binding required
-在我们讨论各种数据类型时会看到不同的类型注解。
+在我们讨论各种数据类型时,你会看到多样的类型注解。
标量类型
标量类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型。你可能在其他语言中见过他们,不过让我们深入了解他们在 Rust 中时如何工作的。
整型
-整数是一个没有小数部分的数字。我们在这一章的前面使用过一个整型,i32
类型。这个类型声明表明在 32 位系统上它关联的值应该是一个有符号整数(因为这个i
,与u
代表的无符号相对)。表格 3-1 展示了 Rust 内建的整数类型。每一个变体的有符号和无符号列(例如,i32)可以用来声明对应的整数值。
+整数是一个没有小数部分的数字。我们在这一章的前面使用过 i32
类型。该类型声明指示,i32 关联的值应该是一个占据32比特位的有符号整数(因为这个i
,与u
代表的无符号相对)。表格 3-1 展示了 Rust 内建的整数类型。每一种变体的有符号和无符号列(例如,i32)可以用来声明对应的整数值。
Length Signed Unsigned
8-bit i8 u8
@@ -99,10 +99,10 @@ commit fe4833a8ef2853c55424e7747a4ef8dd64c35b32
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。
-另外,isize
和usize
类型依赖运行程序的计算机类型(构架):64 位构架他们是 64 位的而 32 位构架他们就是 32 位的。
-可以使用表格 3-2 中的任何一种形式编写数字字面值。注意除了字节字面值以外的数字字面值允许使用类型后缀,例如57u8
,而_
是可视化分隔符(visual separator),例如1_000
位的。
+每一种变体都可以是有符号或无符号的,并有一个明确的大小。有符号和无符号代表数字能否为负值;换句话说,数字是否需要有一个符号(有符号数),或者永远为正而不需要符号(无符号数)。这有点像在纸上书写数字:当需要考虑符号的时候,数字以加号或减号作为前缀;然而,可以安全地假设为正数时,加号前缀通常省略。有符号数以二进制补码形式(two’s complement representation)存储(如果你不清楚这是什么,可以在网上搜索;对其的解释超出了本书的范畴)。
+每一个有符号的变体可以储存包含从 -(2n - 1) 到 2n - 1 - 1 在内的数字,这里 n
是变体使用的位数。所以 i8
可以储存从 -(27) 到 27 - 1 在内的数字,也就是从 -128 到 127。无符号的变体可以储存从 0 到 2n - 1 的数字,所以 u8
可以储存从 0 到 28 - 1 的数字,也就是从 0 到 255。
+另外,isize
和 usize
类型依赖运行程序的计算机架构:64 位架构上他们是 64 位的, 32 位架构上他们是 32 位的。
+可以使用表格 3-2 中的任何一种形式编写数字字面值。除字节以外的其它字面值允许使用类型后缀,例如 57u8
,允许使用 _
做为分隔符以方便读数,例如 1_000
(分隔符的数量与位置并不影响实际的数字)。
Number literals Example
Decimal 98_222
@@ -111,9 +111,9 @@ commit fe4833a8ef2853c55424e7747a4ef8dd64c35b32
Binary 0b1111_0000
Byte (u8
only) b'A'
-那么如何知晓该使用哪种类型的数字呢?如果对此拿不定主意,Rust 的默认类型通常就是一个很好的选择,这个默认数字类型是i32
:它通常是最快的,甚至是在 64 位系统上。使用isize
或usize
的主要场景是索引一些集合。
+那么该使用哪种类型的数字呢?如果拿不定主意,Rust 的默认类型通常就很好,数字类型默认是 i32
:它通常是最快的,甚至在 64 位系统上也是。isize
或 usize
的主要作为集合的索引。
浮点型
-Rust 也有两个主要的浮点数(floating-point numbers)类型,他们是有小数点的数字。Rust 的浮点数类型是f32
和f64
,分别是 32 位 和 64 位大小。默认类型是f64
,因为它基本上与f32
一样快不过精度更高。在 32 位系统上使用f64
是可能的,不过会比f32
要慢。大部分情况,牺牲潜在可能的更低性能来换取更高的精度是一个合理的初始选择,同时如果怀疑浮点数的大小有问题的时候应该先对代码进行性能测试。
+Rust 同样有两个主要的浮点数类型,f32
和 f64
,它们是带小数点的数字,分别占 32 位和 64 位比特。默认类型是 f64
,因为它与 f32
速度差不多,然而精度更高。在 32 位系统上也能够使用 f64
,不过比使用 f32
要慢。多数情况下,以潜在的性能损耗换取更高的精度是合理的;如果觉得浮点数的大小是个麻烦,你应该以性能测试作为决策依据。
这是一个展示浮点数的实例:
Filename: src/main.rs
fn main() {
@@ -168,7 +168,7 @@ commit fe4833a8ef2853c55424e7747a4ef8dd64c35b32
复合类型可以将多个其他类型的值组合成一个类型。Rust 有两个原生的复合类型:元组(tuple)和数组(array)。
将值组合进元组
元组是一个将多个其他类型的值组合进一个复合类型的主要方式。
-我们使用一个括号中的逗号分隔的值列表来创建一个元组。元组中的每一个位置都有一个类型,而且这写不同值的类型也不必是相同的。这个例子中使用了额外的可选类型注解:
+我们使用一个括号中的逗号分隔的值列表来创建一个元组。元组中的每一个位置都有一个类型,而且这些不同值的类型也不必是相同的。这个例子中使用了额外的可选类型注解:
Filename: src/main.rs
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
diff --git a/docs/ch04-01-what-is-ownership.html b/docs/ch04-01-what-is-ownership.html
index 329d79b..9aabbc6 100644
--- a/docs/ch04-01-what-is-ownership.html
+++ b/docs/ch04-01-what-is-ownership.html
@@ -142,7 +142,7 @@ println!("{}", s); // This will print `hello, world!`
需要一个当我们处理完String
时将内存返回给操作系统的方法
第一部分由我们完成:当调用String::from
时,它的实现请求它需要的内存。这在编程语言中是非常通用的。
-然而,第二部分实现起来就各有区别了。在有**垃圾回收(GC)**的语言中, GC 记录并清除不再使用的内存,而我们作为程序员,并不需要关心他们。没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们程序员的责任了,正如请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要allocate
和free
一一对应。
+然而,第二部分实现起来就各有区别了。在有垃圾回收(GC)的语言中, GC 记录并清除不再使用的内存,而我们作为程序员,并不需要关心他们。没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们程序员的责任了,正如请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要allocate
和free
一一对应。
Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。下面是列表 4-1 中作用域例子的一个使用String
而不是字符串字面值的版本:
{
let s = String::from("hello"); // s is valid from this point forward
diff --git a/docs/ch04-02-references-and-borrowing.html b/docs/ch04-02-references-and-borrowing.html
index 508979f..9f8265c 100644
--- a/docs/ch04-02-references-and-borrowing.html
+++ b/docs/ch04-02-references-and-borrowing.html
@@ -203,7 +203,7 @@ immutable
哇哦!我们也不能在拥有不可变引用的同时拥有可变引用。不可变引用的用户可不希望在它的眼皮底下值突然就被改变了!然而,多个不可变引用是没有问题的因为没有哪个读取数据的人有能力影响其他人读取到的数据。
即使这些错误有时是使人沮丧的。记住这是 Rust 编译器在提早指出一个潜在的 bug(在编译时而不是运行时)并明确告诉你问题在哪而不是任由你去追踪为何有时数据并不是你想象中的那样。
悬垂引用
-在存在指针的语言中,容易通过释放内存时保留指向它的指针而错误地生成一个悬垂指针(dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者,。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当我们拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
+在存在指针的语言中,容易通过释放内存时保留指向它的指针而错误地生成一个悬垂指针(dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当我们拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
让我们尝试创建一个悬垂引用:
Filename: src/main.rs
fn main() {
diff --git a/docs/ch16-04-extensible-concurrency-sync-and-send.html b/docs/ch16-04-extensible-concurrency-sync-and-send.html
index 2ab691d..61b488b 100644
--- a/docs/ch16-04-extensible-concurrency-sync-and-send.html
+++ b/docs/ch16-04-extensible-concurrency-sync-and-send.html
@@ -77,11 +77,11 @@ commit 9430a3d28a2121a938d704ce48b15d21062f880e
我们说“几乎所有内容都不属于语言本身”,那么属于语言本身的是什么呢?是两个 trait,都位于std::marker
: Sync
和Send
。
Send
用于表明所有权可能被传送给其他线程
Send
标记 trait 表明类型的所有权可能被在线程间传递。几乎所有的 Rust 类型都是Send
的,不过有一些例外。比如标准库中提供的 Rc<T>
:如果克隆Rc<T>
值,并尝试将克隆的所有权传递给另一个线程,这两个线程可能会同时更新引用计数。正如上一部分提到的,Rc<T>
被实现为用于单线程场景,这时不需要为拥有线程安全的引用计数而付出性能代价。
-因为Rc<T>
没有标记为Send
,Rust 的类型系统和 trait bound 会确保我们不会错误的把一个Rc<T>
值不安全的在线程间传递。列表 16-14 曾尝试这么做,不过得到了一个错误,the trait Send is not implemented for Rc<Mutex<i32>>
。当切换为标记为Send
的Arc<T>
时,就没有问题了。
-任何完全由Send
的类型组成的类型也会自动被标记为Send
。几乎所有基本类型都是Send
的,大部分标准库类型是Send
的,除了Rc<T>
,以及第十九章将会讨论的裸指针(raw pointer)。
-Sync
表明多线程访问是安全的
-Sync
标记 trait 表明一个类型可以安全的在多个线程中拥有其值的引用。换一种方式来说就是,对于任意类型T
,如果&T
(T
的引用)是Send
的话T
就是Sync
的,这样其引用就可以安全的发送到另一个线程。类似于Send
的情况,基本类型是Sync
的,完全由Sync
的类型组成的类型也是Sync
的。
-Rc<T>
也不是Sync
的,出于其不是Send
的相同的原因。RefCell<T>
(第十五章讨论过)和Cell<T>
系列类型不是Sync
的。RefCell<T>
在运行时所进行的借用检查也不是线程安全的。Mutex<T>
是Sync
的,正如上一部分所讲的它可以被用来在多线程中共享访问。
+因为 Rc<T>
没有标记为 Send
,Rust 的类型系统和 trait bound 会确保我们不会错误的把一个 Rc<T>
值不安全的在线程间传递。列表 16-14 曾尝试这么做,不过得到了一个错误,the trait Send is not implemented for Rc<Mutex<i32>>
。而使用标记为 Send
的 Arc<T>
时,就没有问题了。
+任何完全由 Send
的类型组成的类型也会自动被标记为 Send
:几乎所有基本类型都是 Send
的,大部分标准库类型是Send
的,除了Rc<T>
,以及第十九章将会讨论的裸指针(raw pointer)。
+Sync
表明多线程访问是安全的
+Sync
标记 trait 表明一个类型可以安全的在多个线程中拥有其值的引用。换一种方式来说,对于任意类型 T
,如果&T
(T
的引用)是Send
的话T
就是Sync
的,这样其引用就可以安全的发送到另一个线程。类似于 Send
的情况,基本类型是 Sync
的,完全由 Sync
的类型组成的类型也是 Sync
的。
+Rc<T>
也不是 Sync
的,出于其不是Send
的相同的原因。RefCell<T>
(第十五章讨论过)和Cell<T>
系列类型不是Sync
的。RefCell<T>
在运行时所进行的借用检查也不是线程安全的。Mutex<T>
是Sync
的,正如上一部分所讲的它可以被用来在多线程中共享访问。
手动实现Send
和Sync
是不安全的
通常并不需要实现Send
和Sync
trait,由属于Send
和Sync
的类型组成的类型,自动就是Send
和Sync
的。因为他们是标记 trait,甚至都不需要实现任何方法。他们只是用来加强并发相关的不可变性的。
实现这些标记 trait 涉及到编写不安全的 Rust 代码,第十九章将会讲述具体的方法;当前重要的是,在创建新的由不是Send
和Sync
的部分构成的并发类型时需要多加小心,以确保维持其安全保证。The Nomicon 中有更多关于这些保证以及如何维持他们的信息。
diff --git a/docs/ch17-02-trait-objects.html b/docs/ch17-02-trait-objects.html
index 9afdcfd..5c2cda2 100644
--- a/docs/ch17-02-trait-objects.html
+++ b/docs/ch17-02-trait-objects.html
@@ -77,22 +77,22 @@ commit 67876e3ef5323ce9d394f3ea6b08cb3d173d9ba9
-有时,我们希望使用的类型的集合对于使用库的程序员来说是可扩展的。例如,很多图形用户接口(GUI)工具有一个条目列表的概念,它通过遍历列表并对每一个条目调用draw
方法来绘制在屏幕上。我们将要创建一个叫做rust_gui
的包含一个 GUI 库结构的库 crate。GUI 库可以包含一些供开发者使用的类型,比如Button
或TextField
。使用rust_gui
的程序员会想要创建更多可以绘制在屏幕上的类型:一个程序员可能会增加一个Image
,而另一个可能会增加一个SelectBox
。我们不会在本章节实现一个功能完善的 GUI 库,不过会展示各个部分是如何结合在一起的。
+有时,我们希望使用的类型的集合对于使用库的程序员来说是可扩展的。例如,很多图形用户接口(GUI)工具有一个条目列表的概念,它通过遍历列表并对每一个条目调用 draw
方法来绘制在屏幕上。我们将要创建一个叫做 rust_gui
的包含一个 GUI 库结构的库 crate。GUI 库可以包含一些供开发者使用的类型,比如 Button
或 TextField
。使用 rust_gui
的程序员会想要创建更多可以绘制在屏幕上的类型:一个程序员可能会增加一个 Image
,而另一个可能会增加一个 SelectBox
。我们不会在本章节实现一个功能完善的 GUI 库,不过会展示各个部分是如何结合在一起的。
当写 rust_gui
库时,我们不知道其他程序员需要什么类型,所以无法定义一个 enum
来包含所有的类型。然而 rust_gui
需要跟踪所有这些不同类型的值,需要有在每个值上调用 draw
方法能力。我们的 GUI 库不需要确切地知道调用 draw
方法会发生什么,只需要有可用的方法供我们调用。
在可以继承的语言里,我们会定义一个名为 Component
的类,该类上有一个draw
方法。其他的类比如Button
、Image
和SelectBox
会从Component
继承并拥有draw
方法。它们各自覆写draw
方法以自定义行为,但是框架会把所有的类型当作是Component
的实例,并在其上调用draw
。
定义一个带有自定义行为的Trait
不过,在Rust语言中,我们可以定义一个 Draw
trait,包含名为 draw
的方法。我们定义一个由trait对象组成的vector,绑定了某种指针的trait,比如&
引用或者一个Box<T>
智能指针。
-之前提到,我们不会称结构体和枚举为对象,以区分其他语言的结构体和枚举对象。结构体或者枚举成员中的数据和impl
块中的行为是分开的,而其他语言则是数据和行为被组合到一个对象里。Trait 对象更像其他语言的对象,因为他们将其指针指向的具体对象作为数据,将在trait 中定义的方法作为行为,组合在了一起。但是,trait 对象和其他语言是不同的,我们不能向一个 trait 对象增加数据。trait 对象不像其他语言那样有用:它们的目的是允许从公有行为上抽象。
-trait 对象定义了给定情况下应有的行为。当需要具有某种特性的不确定具体类型时,我们可以把 trait 对象当作 trait 使用。Rust 的类型系统会保证我们为 trait 对象带入的任何值会实现 trait 的方法。我们不需要在编译阶段知道所有可能的类型,却可以把所有的实例统一对待。Listing 17-03展示了如何定义一个名为Draw
的带有draw
方法的trait。
-Filename: src/lib.rs
+之前提到,我们不会称结构体和枚举为对象,以区分其他语言的结构体和枚举对象。结构体或者枚举成员中的数据和impl
块中的行为是分开的,而其他语言则是数据和行为被组合到一个对象里。Trait 对象更像其他语言的对象,因为他们将其指针指向的具体对象作为数据,将在 trait 中定义的方法作为行为,组合在了一起。但是,trait 对象和其他语言是不同的,我们不能向一个 trait 对象增加数据。trait 对象不像其他语言那样有用:它们的目的是允许从公有行为上抽象。
+trait 对象定义了给定情况下应有的行为。当需要具有某种特性的不确定具体类型时,我们可以把 trait 对象当作 trait 使用。Rust 的类型系统会保证我们为 trait 对象带入的任何值会实现 trait 的方法。我们不需要在编译阶段知道所有可能的类型,却可以把所有的实例统一对待。列表 17-03 展示了如何定义一个名为Draw
的带有draw
方法的 trait。
+文件名: src/lib.rs
pub trait Draw {
fn draw(&self);
}
-
+
-因为我们已经在第10章讨论过如何定义 trait,你可能比较熟悉。下面是新的定义:Listing 17-4有一个名为 Screen
的结构体,里面有一个名为 components
的 vector,components
的类型是Box。Box<Draw>
是一个 trait 对象:它是 Box
内部任意一个实现了 Draw
trait 的类型的替身。
-Filename: src/lib.rs
+因为我们已经在第十章讨论过如何定义 trait,你可能比较熟悉。下面是新的定义:列表 17-4 有一个名为 Screen
的结构体,里面有一个名为 components
的 vector,components
的类型是 Box<Draw>
。Box<Draw>
是一个 trait 对象:它是 Box
内部任意一个实现了 Draw
trait 的类型的替身。
+文件名: src/lib.rs
# pub trait Draw {
# fn draw(&self);
# }
@@ -101,10 +101,9 @@ pub struct Screen {
pub components: Vec<Box<Draw>>,
}
-
-
-在 Screen
结构体上,我们将要定义一个 run
方法,该方法会在它的 components
上调用 draw
方法,如Listing 17-5所示:
-Filename: src/lib.rs
+
+在 Screen
结构体上,我们将要定义一个 run
方法,该方法会在它的 components
上的每一个元素调用 draw
方法,如列表 17-5 所示:
+文件名: src/lib.rs
# pub trait Draw {
# fn draw(&self);
# }
@@ -121,10 +120,10 @@ impl Screen {
}
}
-
-使用 Rust 的第一步是安装。你需要网络连接来执行本章的命令,因为我们要从网上下载 Rust。
-我们将会展示很多使用终端的命令,并且这些代码都以$
开头。并不需要真正输入$
,它们在这里代表每行指令的开头。在网上会看到很多使用这个惯例的教程和例子:$
代表以常规用户运行命令,#
代表需要用管理员运行的命令。没有以$
(或#
)的行通常是之前命令的输出。
+第一步是安装 Rust。你需要网络连接来执行本章的命令,因为我们要从网上下载 Rust。
+我们将会展示很多在终端中输入的命令,这些命令均以 $
开头。你不需要真的输入$
,在这里它代表每行命令的起始。网上有很多教程和例子遵循这种惯例:$
代表以常规用户身份运行命令,#
代表需要用管理员身份运行命令。没有以 $
(或 #
)起始的行通常是之前命令的输出。
在 Linux 或 Mac 上安装
-如果你使用 Linux 或 Mac,所有需要做的就是打开一个终端并输入:
+如果你使用 Linux 或 Mac,你需要做的全部,就是打开一个终端并输入:
$ curl https://sh.rustup.rs -sSf | sh
-这会下载一个脚本并开始安装。你可能被提示要输入密码。如果一切顺利,将会出现如下内容:
+这会下载一个脚本并开始安装。可能会提示你输入密码,如果一切顺利,将会出现如下内容:
Rust is installed now. Great!
-当然,如果你不赞成curl | sh
这种模式,可以随意下载、检查和运行这个脚本。
+当然,如果你对于 curl | sh
心有疑虑,你可以随意下载、检查和运行这个脚本。
在 Windows 上安装
-在 Windows 上,前往https://rustup.rs并按照说明下载 rustup-init.exe。运行并遵循其提供的其余指示操作。
-本书其余 Windows 相关的命令假设你使用cmd
作为你的 shell。如果你使用不同的 shell,可能能够执行 Linux 和 Mac 用户相同的命令。如果都不行,请查看所使用的 shell 的文档。
+如果你使用 Windows,前往 https://rustup.rs,按说明下载 rustup-init.exe,运行并照其指示操作。
+本书中其余 Windows 相关的命令,假设你使用 cmd
作为 shell。如果你使用其它 shell,也许可以执行与 Linux 和 Mac 用户相同的命令。如果不行,请查看该 shell 的文档。
自定义安装
-如果有理由倾向于不使用 rustup.rs,请查看Rust 安装页面获取其他选择。
+无论出于何种理由,如果不愿意使用 rustup.rs,请查看 Rust 安装页面 获取其他选择。
更新
-一旦安装完 Rust,更新到最新版本是简单的。在 shell 中运行更新脚本:
+一旦 Rust 安装完,更新到最新版本很简单。在 shell 中执行:
$ rustup update
卸载
-卸载 Rust 同安装一样简单。在 shell 中运行卸载脚本
+卸载 Rust 同样简单。在 shell 中执行:
$ rustup self uninstall
故障排除
-安装完 Rust 后,打开 shell,输入:
+安装完 Rust 后,在 shell 中执行:
$ rustc --version
-应该能看到类似这样的版本号、提交 hash 和提交日期,对应你安装时的最新稳定版本:
+应该能看到类似这样的版本号、提交哈希和提交日期,对应安装时的最新稳定版:
rustc x.y.z (abcabcabc yyyy-mm-dd)
-如果出现这些内容,Rust 就安装成功了!
+出现这些内容,Rust 就安装成功了!
恭喜入坑!(此处应该有掌声!)
-如果有问题并且你在使用 Windows,检查 Rust(rustc,cargo 等)是否位于%PATH%
系统变量中。
-如果还是不能运行,有许多可以获取帮助的地方。最简单的是 irc.mozilla.org 上的 #rust IRC 频道 ,可以使用 Mibbit 来访问它。访问这些地址然后就可以和其他能提供帮助的 Rustacean(我们这些人自嘲的绰号)聊天了。其它给力的资源包括用户论坛和 Stack Overflow。
+如果在 Windows 中使用出现问题,检查 Rust(rustc,cargo 等)是否在 %PATH%
环境变量所包含的路径中。
+如果还是不能解决,有许多地方可以求助。最简单的是 irc.mozilla.org 上的 #rust IRC 频道 ,可以使用 Mibbit 来访问它。然后就能和其他 Rustacean(Rust 用户的称号,有自嘲意味)聊天并寻求帮助。其它给力的资源包括用户论坛和 Stack Overflow。
本地文档
-安装程序也包含一份本地文档的拷贝,你可以离线阅读它们。输入rustup doc
将在浏览器中打开本地文档。
-任何你不太确认标准库中提供的类型或函数是干什么的时候,请查看 API 文档!
+安装程序自带本地文档,可以离线阅读。输入 rustup doc
可以在浏览器中查看。
+任何时候,如果你拿不准标准库中类型或函数,请查看 API 文档!
Hello, World!
ch01-02-hello-world.md
commit 4f2dc564851dc04b271a2260c834643dfd86c724
-现在已经安装好了 Rust,让我们来编写第一个 Rust 程序。当学习一门新语言的时候,编写一个在屏幕上打印 “Hello, world!” 文本的小程序是一个传统,而在这一部分将遵循这个传统。
+Rust 已安好,让我们来编写第一个程序。当学习一门新语言的时候,使用该语言在屏幕上打印 “Hello, world!” 是一项传统,我们将遵循这个传统。
-注意:本书假设你熟悉基本的命令行操作。Rust 本身并不对你的编辑器,工具和你的代码存放在何处有什么特定的要求,所以如果你比起命令行更喜欢 IDE,请随意选择你喜欢的 IDE。
+注意:本书假设你熟悉基本的命令行操作。对于你的编辑器、工具,以及你的代码存在何处,Rust 并没有特殊要求,如果你更喜欢 IDE,请随意。
创建项目文件夹
-首先,创建一个文件夹来存放 Rust 代码。Rust 并不关心你的代码存放在哪里,不过在本书中,我们建议在你的 home 目录创建一个 projects 目录,并把你的所有项目放在这。打开一个终端并输入如下命令来为这个项目创建一个文件夹:
+首先,创建一个存放代码的文件夹。Rust 并不关心它的位置,不过在本书中,我们建议你在 home 目录中创建一个 projects 目录,并把你的所有项目放在这。打开一个终端,输入如下命令来创建一个文件夹:
Linux 和 Mac:
$ mkdir ~/projects
$ cd ~/projects
@@ -150,8 +150,8 @@ $ cd hello_world
> cd hello_world
编写并运行 Rust 程序
-接下来,新建一个叫做 main.rs 的源文件。Rust 文件总是以 .rs 后缀结尾。如果文件名多于一个单词,使用下划线分隔它们。例如,使用 my_program.rs 而不是 myprogram.rs。
-现在打开刚创建的 main.rs 文件,并输入如下代码:
+接下来,新建一个叫做 main.rs 的文件。Rust 源代码总是以 .rs 后缀结尾。如果文件名包含多个单词,使用下划线分隔它们。例如 my_program.rs,而不是 myprogram.rs。
+现在打开刚创建的 main.rs 文件,输入如下代码:
Filename: main.rs
fn main() {
println!("Hello, world!");
@@ -162,28 +162,28 @@ $ cd hello_world
$ ./main
Hello, world!
-在 Windows 上,运行.\main.exe
而不是./main
。不管使用何种系统,你应该在终端看到Hello, world!
字符串。如果你做到了,那么恭喜你!你已经正式编写了一个 Rust 程序。你是一名 Rust 程序员了!欢迎入坑。
+在 Windows 上,运行 .\main.exe
,而不是./main
。不管使用何种系统,你应该在终端看到 Hello, world!
字样。如果你做到了,恭喜你!你已经正式编写了一个 Rust 程序,成为一名 Rust 程序员!
分析 Rust 程序
-现在,让我们回过头来仔细看看“Hello, world!”程序到底发生了什么。这里是谜题的第一片:
+现在,让我们回过头来,仔细看看“Hello, world!”程序到底发生了什么。这是拼图的第一片:
fn main() {
}
-这几行定义了一个 Rust 函数。main
函数是特殊的:这是每一个可执行的 Rust 程序首先运行的函数(译者注:入口点)。第一行表示“定义一个叫 main
的函数,没有参数也没有返回值。”如果有参数的话,它们应该出现在括号中,(
和)
。
-同时注意函数体被包裹在大括号中,{
和}
。Rust 要求所有函数体都位于大括号中(译者注:对比有些语言特定情况可以省略大括号)。将前一个大括号与函数声明置于一行,并留有一个空格被认为是一个好的代码风格。
-在main()
函数中:
+这几行定义了一个 Rust 函数。一个叫 main
的函数,没有参数也没有返回值。如果有参数的话,它们应该出现在括弧中,(
和)
之间。main
函数是特殊的:它是每一个可执行的 Rust 程序的入口点。
+还须注意函数体被包裹在花括号中,{
和}
之间。所有函数体都要用花括号包裹起来(译者注:有些语言,当函数体只有一行时可以省略花括号,但 Rust 中是不行的)。一般来说,将左花括号与函数声明置于一行,并以空格分隔,是良好的代码风格。
+在 main()
函数中:
println!("Hello, world!");
-这行代码做了这个小程序的所有工作:它在屏幕上打印文本。这里有很多需要注意的细节。第一个是 Rust 代码风格使用 4 个空格缩进,而不是 1 个制表符(tab)。
-第二个重要的部分是println!()
。这叫做 Rust 宏,是如何进行 Rust 元编程(metaprogramming)的关键所在。相反如果是调用一个函数的话,它应该看起来像这样:println
(没有!
)。我们将在 21 章 E 小节中更加详细的讨论 Rust 宏,不过现在你只需记住当看到符号!
的时候,就代表在调用一个宏而不是一个普通的函数。
+一行代码完成这个小程序的所有工作:在屏幕上打印文本。这里有很多细节需要注意。首先 Rust 使用 4 个空格的缩进风格,而不是 1 个制表符(tab)。
+第二个重要的部分是println!()
。这是 宏,Rust 元编程(metaprogramming)的关键所在。而调用一个函数,则要像这样:println
(没有!
)。我们将在 21 章 E 小节中更加详细的讨论宏,现在你只需记住,当看到符号 !
的时候,调用的是宏而不是普通函数。
接下来,"Hello, world!"
是一个 字符串。我们把这个字符串作为一个参数传递给println!
,它负责在屏幕上打印这个字符串。轻松加愉快!(⊙o⊙)
-这一行以一个分号结尾(;
)。;
代表这个表达式的结束和下一个表达式的开始。大部分 Rust 代码行以;
结尾。
+该行以分号结尾(;
)。;
代表一个表达式的结束和下一个表达式的开始。大部分 Rust 代码行以 ;
结尾。
编译和运行是两个步骤
-在“编写并运行 Rust 程序”部分,展示了如何运行一个新创建的程序。现在我们将拆分并检查每一步操作。
-在运行一个 Rust 程序之前,必须先编译它。可以输入rustc
命令来使用 Rust 编译器并像这样传递源文件的名字:
+“编写并运行 Rust 程序”部分,展示了如何创建运行程序。现在我们将拆分并检查每一步操作。
+运行一个 Rust 程序之前,必须先编译它。可以通过 rustc
命令来使用 Rust 编译器,并传递源文件的名字给它,如下:
$ rustc main.rs
-如果你来自 C 或 C++ 背景,就会发现这与gcc
和clang
类似。编译成功后,Rust 应该会输出一个二进制可执行文件,在 Linux 或 OSX 上在 shell 中你可以通过ls
命令看到如下:
+如果你有 C 或 C++ 背景,就会发现这与 gcc
和 clang
类似。编译成功后,Rust 应该会输出一个二进制可执行文件,在 Linux 或 OSX 上在 shell 中你可以通过ls
命令看到如下:
$ ls
main main.rs
@@ -192,21 +192,21 @@ main main.rs
main.exe
main.rs
-这表示我们有两个文件:.rs 后缀的源文件,和可执行文件(在 Windows下是 main.exe,其它平台是 main)。这里剩下的操作就只有运行 main 或 main.exe 文件了,像这样:
+这表示我们有两个文件:.rs 后缀的源文件,和可执行文件(在 Windows下是 main.exe,其它平台是 main)。然后运行 main 或 main.exe 文件,像这样:
$ ./main # or .\main.exe on Windows
如果 main.rs 是我们的“Hello, world!”程序,它将会在终端上打印Hello, world!
。
-来自 Ruby、Python 或 JavaScript 这样的动态类型语言背景的同学,可能不太习惯在分开的步骤编译和执行程序。Rust 是一种 静态提前编译语言(ahead-of-time compiled language),这意味着可以编译好程序后,把它给任何人,他们都不需要安装 Rust 就可运行。如果你给他们一个 .rb
, .py
或 .js
文件,他们需要先分别安装 Ruby,Python,JavaScript 实现(运行时环境,VM),不过你只需要一句命令就可以编译和执行程序。这一切都是语言设计的权衡取舍。
-仅仅使用rustc
编译简单程序是没问题的,不过随着项目的增长,你将想要能够控制你项目拥有的所有选项,并使其易于分享你的代码给别人或别的项目。接下来,我们将介绍一个叫做 Cargo 的工具,它将帮助你编写现实生活中的 Rust 程序。
+来自 Ruby、Python 或 JavaScript 这样的动态类型语言背景的同学,可能不太习惯将编译和执行分为两个步骤。Rust 是一种 预编译静态类型语言(ahead-of-time compiled language),这意味着编译好程序后,把它给任何人,他们不需要安装 Rust 就可运行。如果你给他们一个 .rb
, .py
或 .js
文件,他们需要先分别安装 Ruby,Python,JavaScript 实现(运行时环境,VM),不过你只需要一句命令就可以编译和执行程序。这一切都是语言设计上的权衡取舍。
+使用 rustc
编译简单程序是没问题的,不过随着项目的增长,你可能需要控制你项目的方方面面,并且更容易地将代码分享给其它人或项目。所以接下来,我们要介绍一个叫做 Cargo 的工具,它会帮助你编写真实世界中的 Rust 程序。
Hello, Cargo!
-Cargo 是 Rust 的构建系统和包管理工具,同时 Rustacean 们使用 Cargo 来管理他们的 Rust 项目,因为它使得很多任务变得更轻松。例如,Cargo 负责构建代码、下载代码依赖的库并编译这些库。我们把代码需要的库叫做 依赖(dependencies)。
-最简单的 Rust 程序,例如我们刚刚编写的,并没有任何依赖,所以目前我们只使用了 Cargo 负责构建代码的那一部分。随着编写更加复杂的 Rust 程序,你会想要添加依赖,如果你使用 Cargo 开始的话,这将会变得简单许多。
-由于绝大部分 Rust 项目使用 Cargo,本书接下来的部分将假设你使用它。如果使用安装章节介绍的官方安装包的话,Rust 自带 Cargo。如果通过其他方式安装 Rust 的话,可以在终端输入如下命令检查是否安装了 Cargo:
+Cargo 是 Rust 的构建系统和包管理工具,同时 Rustacean 们使用 Cargo 来管理他们的 Rust 项目,它使得很多任务变得更轻松。例如,Cargo 负责构建代码、下载依赖库并编译。我们把代码需要的库叫做 依赖(dependencies)。
+最简单的 Rust 程序,比如我们刚刚编写的,并没有任何依赖,所以我们只使用了 Cargo 构建代码的功能。随着更复杂程序的编写,你会想要添加依赖,如果你使用 Cargo 开始的话,这将会变得简单许多。
+由于绝大部分 Rust 项目使用 Cargo,本书接下来的部分将假设你使用它。如果使用之前介绍的官方安装包的话,它自带 Cargo。如果通过其他方式安装的话,可以在终端输入如下命令,检查是否安装了 Cargo:
$ cargo --version
-如果出现了版本号,一切 OK!如果出现一个类似“command not found
”的错误,那么你应该查看安装方式的文档来确定如何单独安装 Cargo。
+如果出现了版本号,一切 OK!如果出现类似“command not found
”的错误,你应该查看安装文档以确定如何单独安装 Cargo。
使用 Cargo 创建项目
-让我们使用 Cargo 来创建一个新项目并看看与上面的hello_world
项目有什么不同。回到 projects 目录(或者任何你决定放置代码的目录):
+让我们使用 Cargo 来创建一个新项目,然后看看与上面的hello_world
项目有什么不同。回到 projects 目录(或者任何你放置代码的目录):
Linux 和 Mac:
$ cd ~/projects
@@ -217,9 +217,9 @@ main.rs
$ cargo new hello_cargo --bin
$ cd hello_cargo
-我们向cargo new
传递了--bin
因为我们的目标是生成一个可执行程序,而不是一个库。可执行文件是二进制可执行文件,通常就叫做 二进制文件(binaries)。项目的名称被定为hello_cargo
,同时 Cargo 在一个同名目录中创建它的文件,接着我们可以进入查看。
-如果列出 hello_cargo 目录中的文件,将会看到 Cargo 生成了一个文件和一个目录:一个 Cargo.toml 文件和一个 src 目录,main.rs 文件位于目录中。它也在 hello_cargo 目录初始化了一个 git 仓库,以及一个 .gitignore 文件;你可以改为使用不同的版本控制系统(VCS),或者不使用 VCS,通过--vcs
参数。
-使用你选择的文本编辑器(IDE)打开 Cargo.toml 文件。它应该看起来像这样:
+我们向 cargo new
传递了 --bin
,因为我们的目标是生成一个可执行程序,而不是一个库。可执行程序是二进制可执行文件,通常就叫做 二进制文件(binaries)。项目的名称被定为hello_cargo
,同时 Cargo 在一个同名目录中创建它的文件,接着我们可以进入查看。
+如果列出 hello_cargo 目录中的文件,将会看到 Cargo 生成了一个文件和一个目录:一个 Cargo.toml 文件和一个 src 目录,main.rs 文件位于目录中。它也在 hello_cargo 目录初始化了一个 git 仓库,以及一个 .gitignore 文件;你可以通过--vcs
参数,切换到其它版本控制系统(VCS),或者不使用 VCS。
+使用文本编辑器(IDE)打开 Cargo.toml 文件。它应该看起来像这样:
Filename: Cargo.toml
[package]
name = "hello_cargo"
@@ -228,22 +228,22 @@ authors = ["Your Name <you@example.com>"]
[dependencies]
-这个文件使用TOML (Tom's Obvious, Minimal Language) 格式。TOML 类似于 INI,不过有一些额外的改进之处,并且被用作 Cargo 的配置文件的格式。
-第一行,[package]
,是一个部分标题表明下面的语句用来配置一个包。随着我们在这个文件增加更多的信息,还将增加其他部分。
-最后一行,[dependencies]
,是列出项目依赖的 crates(我们这么称呼 Rust 代码包)的部分的开始,这样 Cargo 也就知道去下载和编译它们了。这个项目并不需要任何其他的 crate,不过在下一章猜猜看教程会需要。
+这个文件使用 TOML (Tom's Obvious, Minimal Language) 格式。TOML 类似于 INI,不过有一些额外的改进之处,并且被用作 Cargo 的配置文件的格式。
+第一行,[package]
,是一个段落标题,表明下面的语句用来配置一个包。随着我们在这个文件增加更多的信息,还将增加其他段落。
+最后一行,[dependencies]
,是项目依赖的 crates(Rust 代码包)的段落的开始,这样 Cargo 就知道下载和编译它们了。这个项目并不需要任何其他的 crate,不过在下一章猜猜看教程会需要。
现在看看 src/main.rs:
Filename: src/main.rs
fn main() {
println!("Hello, world!");
}
-Cargo 为你生成了一个“Hello World!”,正如我们之前编写的那个!目前为止我们所见过的之前项目与 Cargo 生成的项目区别有:
+Cargo 为你生成了一个“Hello World!”,正如我们之前编写的那个!目前为止,之前项目与 Cargo 生成项目区别有:
- 代码位于 src 目录
- 项目根目录包含一个 Cargo.toml 配置文件
-Cargo 期望源文件位于 src 目录,这样将项目根目录留给 README、license 信息、配置文件和其他跟代码无关的文件。这样,Cargo 帮助你保持项目干净整洁。一切井井有条。
-如果没有使用 Cargo 开始项目,正如在 hello_world 目录中的项目,可以把它转化为一个使用 Cargo 的项目,通过将代码放入 src 目录并创建一个合适的 Cargo.toml。
+Cargo 期望源文件位于 src 目录,将项目根目录留给 README、license 信息、配置文件和其他跟代码无关的文件。这样,Cargo 帮助你保持项目干净整洁,一切井井有条。
+如果没有用 Cargo 创建项目,比如 hello_world 目录中的项目,可以通过将代码放入 src 目录,并创建一个合适的 Cargo.toml,将其转化为一个 Cargo 项目。
构建并运行 Cargo 项目
现在让我们看看通过 Cargo 构建和运行 Hello World 程序有什么不同。为此,输入如下命令:
$ cargo build
@@ -254,40 +254,40 @@ authors = ["Your Name <you@example.com>"]
Hello, world!
好的!如果一切顺利,Hello, world!
应该再次打印在终端上。
-第一次运行cargo build
的时候也会使 Cargo 在项目根目录创建一个叫做 Cargo.lock 的新文件,它看起来像这样:
+首次运行 cargo build
的时候,Cargo 会在项目根目录创建一个新文件,Cargo.lock,它看起来像这样:
Filename: Cargo.lock
[root]
name = "hello_cargo"
version = "0.1.0"
-Cargo 使用 Cargo.lock 来记录程序的依赖。这个项目并没有依赖,所以内容有一点稀少。事实上,你自己永远也不需要碰这个文件;仅仅让 Cargo 处理它就行了。
-我们刚刚使用cargo build
构建了项目并使用./target/debug/hello_cargo
运行了它,不过也可以使用cargo run
编译并运行:
+Cargo 使用 Cargo.lock 来记录程序的依赖。这个项目并没有依赖,所以内容比较少。事实上,你自己永远也不需要碰这个文件,让 Cargo 处理它就行了。
+我们刚刚使用 cargo build
构建了项目并使用 ./target/debug/hello_cargo
运行了它,也可以使用 cargo run
编译并运行:
$ cargo run
Running `target/debug/hello_cargo`
Hello, world!
-注意这一次,并没有出现告诉我们 Cargo 正在编译 hello_cargo
的输出。Cargo 发现文件并没有被改变,所以只是运行了二进制文件。如果修改了源文件的话,将会出现像这样的输出:
+注意这一次并没有出现“正在编译 hello_cargo
”的输出。Cargo 发现文件并没有被改变,直接运行了二进制文件。如果修改了源文件的话,将会出现像这样的输出:
$ cargo run
Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
Running `target/debug/hello_cargo`
Hello, world!
-所以现在又出现一些更多的不同:
+所以现在又出现更多的不同:
-- 使用
cargo build
构建项目(或使用cargo run
一步构建并运行),而不是使用rustc
-- 不同于将构建结果放在源码相同目录,Cargo 会将它放到 target/debug 目录中的文件。
+- 使用
cargo build
构建项目(或使用 cargo run
一步构建并运行),而不是使用rustc
+- 有别于将构建结果放在源码目录,Cargo 将它放到 target/debug 目录。
-Cargo 的另一个优点是不管你使用什么操作系统它的命令都是一样的,所以之后我们将不再为 Linux 和 Mac 以及 Windows 提供特定的命令。
+Cargo 的另一个优点是,不管你使用什么操作系统,它的命令都是一样的,所以之后我们将不再为 Linux 和 Mac 以及 Windows 提供相应的命令。
发布构建
-当项目最终准备好发布了,可以使用cargo build --release
来优化编译项目。这会在 target/release 下生成可执行文件,而不是 target/debug。这些优化可以让 Rust 代码运行的更快,不过启用他们会让程序花更长的时间编译。这也是为何这是两种不同的配置:一个为了开发,这时你经常想要快速重新构建;另一个构建提供给用户的最终程序,这时并不会重新构建并希望程序能运行得越快越好。如果你在测试代码的运行时间,请确保运行cargo build --release
并使用 target/release 下的可执行文件进行测试。
+当项目最终准备好发布了,可以使用 cargo build --release
来优化编译项目。这会在 target/release 下生成可执行文件,而不是 target/debug。优化可以让 Rust 代码运行的更快,然而也需要更长的编译时间。因此产生了两种不同的配置:一种为了开发,你需要快速重新构建;另一种构建给用户的最终程序,不会重新构建,并且程序运行得越快越好。如果你在测试代码的运行时间,请确保运行 cargo build --release
并使用 target/release 下的可执行文件。
把 Cargo 当作习惯
-对于简单项目, Cargo 并不能比rustc
提供更多的价值,不过随着开发的进行终将体现它的价值。对于拥有多个 crate 的复杂项目,让 Cargo 来协调构建将更简单。有了 Cargo,只需运行cargo build
,然后一切将有序运行。即便这个项目很简单,现在也它使用了很多之后你的 Rust 程序生涯将会用得上的实用工具。事实上,无形中你可以使用下面的命令开始所有你想要从事的项目:
+对于简单项目, Cargo 并不比 rustc
更有价值,不过随着开发的进行终将体现它的价值。对于拥有多个 crate 的复杂项目,让 Cargo 来协调构建将更简单。有了 Cargo,只需运行cargo build
,然后一切将有序运行。即便这个项目很简单,也它使用了很多你之后的 Rust 生涯将会用得上的实用工具。其实你可以开始任何你想要从事的项目,使用下面的命令:
$ git clone someurl.com/someproject
$ cd someproject
$ cargo build
-注意:如果你想要查看 Cargo 的更多细节,请阅读官方的 Cargo guide,它覆盖了其所有的功能。
+注意:如果想要了解 Cargo 更多的细节,请阅读官方的 Cargo guide,它覆盖了所有的功能。
猜猜看
@@ -295,10 +295,10 @@ $ cargo build
commit e6d6caab41471f7115a621029bd428a812c5260e
-让我们通过自己动手的方式一起完成一个项目来快速上手 Rust!本章将介绍一些常用的 Rust 概念,并通过真实的程序来展示如何运用他们。你将会学到let
、match
、方法、关联函数、使用外部 crate 等更多的知识!接下来的章节会探索这些概念的细节。在这一章,我们将练习基础。
-我们会实现一个经典的新手编程问题:猜猜看游戏。它是这么工作的:程序将会随机生成一个 1 到 100 之间的随机整数。接着它会提示玩家输入一个猜测。当输入了一个猜测后,它会提示猜测是太大了还是太小了。如果猜对了,它会打印出祝贺并退出。
+让我们亲自动手,快速熟悉 Rust!本章将介绍 Rust 中常用的一些概念,并通过真实的程序来展示如何运用。你将会学到更多诸如 let
、match
、方法、关联函数、外部 crate 等知识!后继章节会深入探索这些概念的细节。在这一章,我们将练习基础。
+我们会实现一个经典的新手编程问题:猜猜看游戏。它是这么工作的:程序将会随机生成一个 1 到 100 之间的随机整数。接着它会请玩家猜一个数并输入,然后提示猜测是大了还是小了。如果猜对了,它会在退出前祝贺你。
准备一个新项目
-要创建一个新项目,进入在第一章创建的项目目录,像这样使用 Cargo 创建它:
+要创建一个新项目,进入第一章创建的项目目录,使用 Cargo 创建它:
$ cargo new guessing_game --bin
$ cd guessing_game
@@ -313,22 +313,22 @@ authors = ["Your Name <you@example.com>"]
[dependencies]
如果 Cargo 从环境中获取的开发者信息不正确,修改这个文件并再次保存。
-正如第一章那样,cargo new
生成了一个“Hello, world!”程序。查看 src/main.rs 文件:
+正如第一章那样,cargo new
生成了一个“Hello, world!”程序。查看 src/main.rs 文件:
Filename: src/main.rs
fn main() {
println!("Hello, world!");
}
-现在让我们使用cargo run
在相同的步骤编译并运行这个“Hello, world!”程序:
+现在让我们使用 cargo run
,编译运行一步到位:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Running `target/debug/guessing_game`
Hello, world!
-run
命令在需要快速迭代项目时就派上用场了,而这个游戏就正是这么一个项目:我们需要在进行下一步之前快速测试每次迭代。
-重新打开 src/main.rs 文件。我们将会在这个文件编写全部的代码。
+run
命令适合用在需要快速迭代的项目,而这个游戏就是:我们需要在下一步迭代之前快速测试。
+重新打开 src/main.rs 文件。我们将会在这个文件中编写全部代码。
处理一次猜测
-程序的第一部分会请求用户输入,处理输入,并检查输入是否为期望的形式。首先,允许玩家输入一个猜测。在 src/main.rs 中输入列表 2-1 中的代码。
+程序的第一部分请求和处理用户输入,并检查输入是否符合预期。首先,需要有一个让玩家输入猜测的地方。在 src/main.rs 中输入列表 2-1 中的代码。
Filename: src/main.rs
use std::io;
@@ -346,59 +346,59 @@ fn main() {
}
-这些代码包含很多信息,所以让我们一点一点地过一遍。为了获取用户输入并接着打印结果作为输出,我们需要将io
(输入/输出)库引入作用域中。io
库来自于标准库(也被称为std
):
+这些代码包含很多信息,我们一点一点地过一遍。为了获取用户输入并打印结果作为输出,我们需要将 io
(输入/输出)库引入当前作用域。io
库来自于标准库(也被称为std
):
use std::io;
-Rust 默认只在每个程序的 prelude 中引用很少的一些类型。如果想要使用的类型并不在 prelude 中,你必须使用一个use
语句显式的将其引入到作用域中。使用std::io
库将提供很多io
相关的功能,接受用户输入的功能。
-正如第一章所讲,main
函数是程序的入口点:
+Rust 默认只在每个程序的 prelude 中引入少量类型。如果需要的类型不在 prelude 中,你必须使用一个 use
语句显式的将其引入作用域。std::io
库提供很多 io
相关的功能,比如接受用户输入。
+如第一章所提及,main
函数是程序的入口点:
fn main() {
-fn
语法声明了一个新函数,()
表明没有参数,{
作为函数体的开始。
-第一章也讲到了,println!
是一个在屏幕上打印字符串的宏:
+fn
语法声明了一个新函数,()
表明没有参数,{
作为函数体的开始。
+第一章也提及,println!
是一个在屏幕上打印字符串的宏:
println!("Guess the number!");
println!("Please input your guess.");
-这些代码仅仅打印一个提示,说明游戏的内容并请求用户输入。
+这些代码仅仅打印提示,介绍游戏的内容然后请用户输入。
用变量储存值
接下来,创建一个地方储存用户输入,像这样:
-let mut guess = String::new();
+let mut guess = String::new();
-现在程序开始变得有意思了!这一小行代码发生了很多事。注意这是一个let
语句,用来创建变量。这里是另外一个例子:
-let foo = bar;
+现在程序开始变得有意思了!这一小行代码发生了很多事。注意这是一个 let
语句,用来创建变量。这里是另外一个例子:
+let foo = bar;
-这行代码会创建一个叫做foo
的新变量并把它绑定到值bar
上。在 Rust 中,变量默认是不可变的。下面的例子展示了如何在变量名前使用mut
来使一个变量可变:
+这行代码会创建一个叫做 foo
的新变量并把它绑定到值 bar
上。在 Rust 中,变量默认是不可变的。下面的例子展示了如何在变量名前使用 mut
来使一个变量可变:
let foo = 5; // immutable
let mut bar = 5; // mutable
注意://
开始一个注释,它持续到本行的结尾。Rust 忽略注释中的所有内容。
-现在我们知道了let mut guess
会引入一个叫做guess
的可变变量。等号(=
)的另一边是guess
所绑定的值,它是String::new
的结果,这个函数会返回一个String
的新实例。String
是一个标准库提供的字符串类型,它是可增长的、UTF-8 编码的文本块。
-::new
那一行的::
语法表明new
是String
类型的一个关联函数(associated function)。关联函数是针对类型实现的,在这个例子中是String
,而不是String
的某个特定实例。一些语言中把它称为静态方法(static method)。
-new
函数创建了一个新的空的String
,你会在很多类型上发现new
函数,因为这是创建某个类型新值的常用函数名。
-总结一下,let mut guess = String::new();
这一行创建了一个可变变量,目前它绑定到一个String
新的、空的实例上。哟!
-回忆一下我们在程序的第一行使用use std::io;
从标准库中引用输入/输出功能。现在在io
上调用一个关联函数,stdin
:
+现在我们知道了 let mut guess
会引入一个叫做 guess
的可变变量。等号(=
)的右边是 guess
所绑定的值,它是 String::new
的结果,这个函数会返回一个 String
的新实例。String
是一个标准库提供的字符串类型,它是 UTF-8 编码的可增长文本块。
+::new
那一行的 ::
语法表明 new
是 String
类型的一个 关联函数(associated function)。关联函数是针对类型实现的,在这个例子中是 String
,而不是 String
的某个特定实例。一些语言中把它称为静态方法(static method)。
+new
函数创建了一个新的空 String
,你会在很多类型上发现new
函数,这是创建类型实例的惯用函数名。
+总结一下,let mut guess = String::new();
这一行创建了一个可变变量,绑定到一个新的 String
空实例上。
+回忆一下,我们在程序的第一行使用 use std::io;
从标准库中引入“输入输出”。现在调用 io
的关联函数 stdin
:
io::stdin().read_line(&mut guess)
.expect("Failed to read line");
-如果我们在程序的开头没有use std::io
这一行,我们可以把函数调用写成std::io::stdin
这样。stdin
函数返回一个 std::io::Stdin
的实例,这是一个代表终端标准输入句柄的类型。
-代码的下一部分,.read_line(&mut guess)
,调用 read_line
方法从标准输入句柄获取用户输入。我们还向read_line()
传递了一个参数:&mut guess
。
-read_line
的工作是把获取任何用户键入到标准输入的字符并放入一个字符串中,所以它获取字符串作为一个参数。这个字符串需要是可变的,这样这个方法就可以通过增加用户的输入来改变字符串的内容。
-&
表明这个参数是一个引用(reference),它提供了一个允许多个不同部分的代码访问同一份数据而不需要在内存中多次拷贝的方法。引用是一个复杂的功能,而 Rust 的一大优势就是它是如何安全而优雅操纵引用的。完成这个程序并不需要知道这么多细节:第四章会更全面的解释引用。现在,我们只需知道它像变量一样,默认是不可变的。因此,需要写成&mut guess
而不是&guess
来使其可变。
-我们还没有分析完这行代码。虽然这是单独一行代码,但它只是一个逻辑上代码行(虽然换行了但仍是一个语句)的第一部分。第二部分是这个方法:
-.expect("Failed to read line");
-
-当使用.expect()
语法调用方法时,明智的选择是换行并留出空白(缩进)来把长的代码行拆开。我们可以把代码写成这样:
-io::stdin().read_line(&mut guess).expect("Failed to read line");
-
-不过,过长的代码行难以阅读,所以最好拆开来写,两行代码两个方法调用。现在来看看这行代码干了什么。
-使用Result
类型来处理潜在的错误
-之前提到过,read_line
将用户输入放入到传递给它字符串中,不过它也返回一个值——在这个例子中,一个io::Result
。Rust 标准库中有很多叫做Result
的类型。一个Result
泛型以及对应子模块的特定版本,比如io::Result
。
-Result
类型是 枚举(enumerations),通常也写作 enums。枚举拥有固定值集合的类型,而这些值被称为枚举的成员(variants)。第六章会更详细的介绍枚举。
-对于Result
,它的成员是Ok
或Err
,Ok
表明操作成功了,同时Ok
成员之中包含成功生成的值。Err
意味着操作失败,Err
之中包含操作是为什么或如何失败的信息。
-Result
类型的作用是编码错误处理信息。Result
类型的值,正如其他任何类型,拥有定义于其上的方法。io::Result
的实例拥有expect
方法可供调用。如果io::Result
实例的值是Err
,expect
会导致程序崩溃并显示显示你作为参数传递给expect
的信息。如果io::Result
实例的值是Ok
,expect
会获取Ok
中的值并原原本本的返回给你。在本例中,这个值是用户输入到标准输入中的字节的数量。
-如果不使用expect
,程序也能编译,不过会出现一个警告:
+如果程序的开头没有 use std::io
这一行,我们可以把函数调用写成 std::io::stdin
。stdin
函数返回一个 std::io::Stdin
的实例,这代表终端标准输入句柄的类型。
+代码的下一部分,.read_line(&mut guess)
,调用 read_line
方法从标准输入句柄获取用户输入。我们还向 read_line()
传递了一个参数:&mut guess
。
+read_line
的工作是,无论用户在标准输入中键入什么内容,都将其存入一个字符串中,因此它需要字符串作为参数。这个字符串参数应该是可变的,以便 read_line
将用户输入附加上去。
+&
表示这个参数是一个引用(reference),它允许多处代码访问同一处数据,而无需在内存中多次拷贝。引用是一个复杂的特性,Rust 的一个主要优势就是安全而简单的操纵引用。完成当前程序并不需要了解如此多细节:第四章会更全面的解释引用。现在,我们只需知道它像变量一样,默认是不可变的,需要写成 &mut guess
而不是 &guess
来使其可变。
+我们还没有分析完这行代码。虽然这是单独一行代码,但它是一个逻辑行(虽然换行了但仍是一个语句)的第一部分。第二部分是这个方法:
+.expect("Failed to read line");
+
+当使用 .expect()
语法调用方法时,通过‘换行并缩进’来把长行拆开,是明智的。我们完全可以这样写:
+io::stdin().read_line(&mut guess).expect("Failed to read line");
+
+不过,过长的行难以阅读,所以最好拆开来写,两行代码两个方法调用。现在来看看这行代码干了什么。
+使用 Result
类型来处理潜在的错误
+之前提到,read_line
将用户输入附加到传递给它字符串中,不过它也返回一个值——在这个例子中是 io::Result
。Rust 标准库中有很多叫做 Result
的类型。一个 Result
泛型以及对应子模块的特定版本,比如 io::Result
。
+Result
类型是 枚举(enumerations),通常也写作 enums。枚举类型持有固定集合的值,这些值被称为枚举的成员(variants)。第六章将介绍枚举的更多细节。
+对于 Result
,它的成员是 Ok
或 Err
,Ok
表示操作成功,内部包含产生的值。Err
意味着操作失败,包含失败的前因后果。
+Result
类型的作用是编码错误处理信息。Result
类型的值,像其他类型一样,拥有定义于其上的方法。io::Result
的实例拥有expect
方法,如果实例的值是 Err
,expect
会导致程序崩溃,并显示当做参数传递给 expect
的信息;如果实例的值是 Ok
,expect
会获取 Ok
中的值并原样返回。在本例中,这个值是用户输入到标准输入中的字节的数量。
+如果不使用 expect
,程序也能编译,不过会出现一个警告:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
src/main.rs:10:5: 10:39 warning: unused result which must be used,
@@ -406,18 +406,18 @@ src/main.rs:10:5: 10:39 warning: unused result which must be used,
src/main.rs:10 io::stdin().read_line(&mut guess);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Rust 警告我们没有使用read_line
返回的值Result
,表明程序没有处理一个可能的错误。消除警告的正确方式是老实编写错误处理,不过因为我们仅仅希望程序出现问题就崩溃,可以直接使用expect
。第九章会学习从错误中恢复的内容。
-使用println!
占位符打印值
-除了位于结尾的大括号,目前为止编写的代码就只有一行代码值得讨论一下了,就是这一行:
-println!("You guessed: {}", guess);
+Rust 警告我们没有使用 read_line
的返回值 Result
,说明有一个可能的错误没处理。想消除警告,就老实的写错误处理,不过我们就是希望程序在出现问题时立即崩溃,所以直接使用 expect
。第九章会学习如何从错误中恢复。
+使用 println!
占位符打印值
+除了位于结尾的大括号,目前为止就只有一行代码值得讨论一下了,就是这一行:
+println!("You guessed: {}", guess);
-这行代码打印出存储了用户输入的字符串。这对{}
是一个在特定位置预留值的占位符。可以使用{}
打印多个值:第一个{}
对应格式化字符串之后列出的第一个值,第二个对应第二个值,以此类推。用一个println!
调用打印多个值应该看起来像这样:
+这行代码打印存储用户输入的字符串。第一个参数是格式化字符串,里面的 {}
是预留在特定位置的占位符。使用占位符也可以打印多个值:格式化字符串中第一个占位符对应第二个参数值,第二个占位符对应第三个参数值,以此类推(第一个参数是格式化字符串本身)。调用一次 println!
打印多个值看起来像这样:
let x = 5;
let y = 10;
println!("x = {} and y = {}", x, y);
-这行代码会打印出x = 5 and y = 10
。
+这行代码会打印出 x = 5 and y = 10
。
测试第一部分代码
让我们来测试下猜猜看游戏的第一部分。使用cargo run
运行它:
$ cargo run
@@ -428,19 +428,19 @@ Please input your guess.
6
You guessed: 6
-至此为止,游戏的第一部分已经完成:我们从键盘获取了输入并打印了出来。
+至此为止,游戏的第一部分已经完成:我们从键盘获取输入并打印了出来。
生成一个秘密数字
-接下来,需要生成一个秘密数字,用户会尝试猜测它。秘密数字应该每次都不同,这样多玩几次才会有意思。生成一个 1 到 100 之间的随机数这样游戏也不会太难。Rust 标准库中还未包含随机数功能。然而,Rust 团队确实提供了一个rand
crate。
+接下来,需要生成一个秘密数字,好让用户来猜。秘密数字应该每次都不同,这样重复玩才不会乏味;范围应该在 1 到 100 之间,这样才不会太困难。Rust 标准库中尚未包含随机数功能。然而,Rust 团队还是提供了一个 rand
crate。
使用 crate 来增加更多功能
-记住 crate 是一个 Rust 代码的包。我们正在构建的项目是一个二进制 crate,它生成一个可执行文件。 rand
crate 是一个 库 crate,它包含意在被其他程序使用的代码。
-Cargo 对外部 crate 的运用是其真正闪光的地方。在我们可以使用rand
编写代码之前,需要编辑 Cargo.toml 来包含rand
作为一个依赖。现在打开这个文件并在[dependencies]
部分标题(Cargo 为你创建了它)的下面添加如下代码:
+记住 crate 是一个 Rust 代码的包。我们正在构建的项目是一个二进制 crate,它生成一个可执行文件。 rand
crate 是一个 库 crate,库 crate 可以包含任意能被其他程序使用的代码。
+Cargo 对外部 crate 的运用是亮点。在我们使用 rand
编写代码之前,需要编辑 Cargo.toml ,声明 rand
作为一个依赖。现在打开这个文件并在 [dependencies]
标题(Cargo 为你创建了它)之下添加:
Filename: Cargo.toml
[dependencies]
rand = "0.3.14"
-在 Cargo.toml 文件中,任何标题之后的内容都是属于这个部分的,一直持续到直到另一个部分开始。[dependencies]
部分告诉 Cargo 项目依赖了哪个外部 crate 和需要的 crate 版本。在这个例子中,我们使用语义化版本符号0.3.14
来指定rand
crate。Cargo 理解语义化版本(Semantic Versioning)(有时也称为 SemVer),这是一个编写版本号的标准。版本号0.3.14
事实上是^0.3.14
的缩写,它的意思是“任何与 0.3.14 版本公有 API 相兼容的版本”。
-现在,不用修改任何代码,构建项目,如列表 2-2 所示:
+在 Cargo.toml 文件中,标题以及之后的内容属同一个段落,遇到下一个标题则开始新的段落。[dependencies]
部分告诉 Cargo 本项目依赖了哪些外部 crate 及其版本。本例中,我们使用语义化版本 0.3.14
来指定 rand
crate。Cargo 理解语义化版本(Semantic Versioning)(有时也称为 SemVer),是一种定义版本号的标准。0.3.14
事实上是 ^0.3.14
的简写,它表示“任何与 0.3.14 版本公有 API 相兼容的版本”。
+现在,不修改任何代码,构建项目,如列表 2-2 所示:
$ cargo build
Updating registry `https://github.com/rust-lang/crates.io-index`
Downloading rand v0.3.14
@@ -451,40 +451,40 @@ rand = "0.3.14"
-可能会出现不同的版本号(不过多亏了语义化版本,它们与代码是兼容的!),同时显示顺序也可能会有所不同。
-现在我们有了一个外部依赖,Cargo 从 registry (Crates.io)上获取了一份(兼容的)最新版本代码的拷贝。Crates.io 是 Rust 生态环境中的开发者们向他人贡献他们的开源 Rust 项目的地方。
-在更新完 registry (索引)后,Cargo 检查[dependencies]
部分并下载还不存在的部分。在这个例子中,虽然只列出了rand
一个依赖,Cargo 也获取了一份libc
的拷贝,因为rand
依赖libc
来正常工作。在下载他们之后,Rust 编译他们并接着使用这些依赖编译项目。
-如果不做任何修改就立刻再次运行cargo build
,则不会有任何输出。Cargo 知道它已经下载并编译了依赖,同时 Cargo.toml 文件中也没有任何相关修改。Cargo 也知道代码没有做任何修改,所以它也不会重新编译代码。因为无事可做,它简单的退出了。如果打开 src/main.rs 文件,并做一些普通的修改,保存并再次构建,只会出现一行输出:
+可能会出现不同的版本号(多亏了语义化版本,它们与代码是兼容的!),同时显示顺序也可能会有所不同。
+现在我们有了一个外部依赖,Cargo 从 registry (Crates.io)上获取了一份(兼容的)最新版本的代码。Crates.io 是 Rust 生态环境中的开发者们向他人贡献 Rust 开源项目的地方。
+在更新完 registry (索引)后,Cargo 检查 [dependencies]
段落并下载缺失的部分。本例中,只声明了 rand
一个依赖,然而 Cargo 还是额外获取了 libc
,因为 rand
依赖 libc
来正常工作。下载完成后,Rust 编译依赖,然后使用这些依赖编译项目。
+如果不做任何修改,立刻再次运行 cargo build
,则不会有任何输出。Cargo 知道它已经下载并编译了依赖,同时 Cargo.toml 文件也没有变动,并且代码也没有任何修改,所以它不会重新编译代码。因为无事可做,它简单的退出了。如果打开 src/main.rs 文件,做一些普通的修改,保存并再次构建,只会出现一行输出:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
-这一行表明 Cargo 只构建了对 src/main.rs 文件做出的微小修改。依赖没有被修改,所以 Cargo 知道可以复用已经为此下载并编译的代码。它只是重新构建了部分(项目)代码。
+这一行表示 Cargo 只针对 src/main.rs 文件的微小修改而构建。依赖没有变化,所以 Cargo 会复用已经为此下载并编译的代码。它只是重新构建了部分(项目)代码。
Cargo.lock 文件确保构建是可重现的
-Cargo 有一个机制来确保每次任何人重新构建代码都会生成相同的结果:Cargo 只会使用你指定的依赖的版本,除非你又手动指定了别的。例如,如果下周rand
crate 的v0.3.15
版本出来了,而它包含一个重要的 bug 修改并也含有一个会破坏代码运行的缺陷的时候会发生什么呢?
-这个问题的答案是 Cargo.lock 文件,它在第一次运行cargo build
时被创建并位于 guessing_game 目录。当第一次构建项目时,Cargo 计算出所有符合要求的依赖版本并接着写入 Cargo.lock 文件中。当将来构建项目时,Cargo 发现 Cargo.lock 存在就会使用这里指定的版本,而不是重新进行所有版本的计算。这使得你拥有了一个自动的可重现的构建。换句话说,项目会继续使用0.3.14
直到你显式升级,多亏了 Cargo.lock 文件。我们将会在这个文件编写全部的代码。
+Cargo 有一个机制来确保任何人在任何时候重新构建代码,都会产生相同的结果:Cargo 只会使用你指定的依赖的版本,除非你又手动指定了别的。例如,如果下周 rand
crate 的 v0.3.15
版本出来了,它修复了一个重要的 bug,同时也含有一个缺陷,会破坏代码的运行,这时会发生什么呢?
+答案是 Cargo.lock 文件。它在第一次运行 cargo build
时创建,并放在 guessing_game 目录,Cargo 计算出所有符合要求的依赖版本并写入 Cargo.lock 文件。当将来构建项目时,如果 Cargo.lock 存在,Cargo 就使用里面指定的版本,不会重新计算。自动使你拥有了一个可重现的构建。换句话说,项目会继续使用 0.3.14
直到你显式升级,感谢 Cargo.lock。
更新 crate 到一个新版本
当你确实需要升级 crate 时,Cargo 提供了另一个命令,update
,他会:
-- 忽略 Cargo.lock 文件并计算出所有符合 Cargo.toml 中规格的最新版本。
+- 忽略 Cargo.lock 文件,并计算出所有符合 Cargo.toml 声明的最新版本。
- 如果成功了,Cargo 会把这些版本写入 Cargo.lock 文件。
-不过,Cargo 默认只会寻找大于0.3.0
而小于0.4.0
的版本。如果rand
crate 发布了两个新版本,0.3.15
和0.4.0
,在运行cargo update
时会出现如下内容:
+不过,Cargo 默认只会寻找大于 0.3.0
而小于 0.4.0
的版本。如果 rand
crate 发布了两个新版本,0.3.15
和 0.4.0
,在运行 cargo update
时会出现如下内容:
$ cargo update
Updating registry `https://github.com/rust-lang/crates.io-index`
Updating rand v0.3.14 -> v0.3.15
这时,值得注意的是 Cargo.lock 文件中的一个改变,rand
crate 现在使用的版本是0.3.15
。
-如果想要使用0.4.0
版本的rand
或是任何0.4.x
系列的版本,必须像这样更新 Cargo.toml 文件:
+如果想要使用 0.4.0
版本的 rand
或是任何 0.4.x
系列的版本,必须像这样更新 Cargo.toml 文件:
[dependencies]
rand = "0.4.0"
-下一次运行cargo build
时,Cargo 会更新 registry 中可用的 crate 并根据你指定新版本重新计算rand
的要求。
-第十四章会讲到Cargo 和它的生态系统的更多内容,不过目前你只需要了解这么多。Cargo 使得复用库文件变得非常容易,所以 Rustacean 们能够通过组合很多包来编写出更轻巧的项目。
+下一次运行 cargo build
时,Cargo 会从 registry 更新,并根据你指定的新版本重新计算。
+第十四章会讲到 Cargo 及其生态系统的更多内容,不过目前你只需要了解这么多。通过 Cargo 复用库文件非常容易,因此 Rustacean 能够编写出由很多包组装而成的更轻巧的项目。
生成一个随机数
-让我们开始使用rand
。下一步是更新 src/main.rs,如列表 2-3 所示:
+让我们开始使用 rand
。下一步是更新 src/main.rs,如列表 2-3 所示:
Filename: src/main.rs
-extern crate rand;
+extern crate rand;
use std::io;
use rand::Rng;
@@ -508,10 +508,10 @@ fn main() {
-我们在顶部增加一行extern crate rand;
来让 Rust 知道我们要使用外部依赖。这也会调用相应的use rand
,所以现在可以使用rand::
前缀来调用rand
中的任何内容。
-接下来,我们增加了另一行use
:use rand::Rng
。Rng
是一个定义了随机数生成器应实现方法的 trait,如果要使用这些方法的话这个 trait 必须在作用域中。第十章会详细介绍 trait。
-另外,中间还新增加了两行。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
。
+我们在顶部增加一行 extern crate rand;
通知 Rust 我们要使用外部依赖。这也会调用相应的 use rand
,所以现在可以使用 rand::
前缀来调用 rand
中的内容。
+接下来,我们增加了一行 use
:use rand::Rng
。Rng
是一个 trait,它定义了随机数生成器应实现的方法 ,想使用这些方法的话此 trait 必须在作用域中。第十章会详细介绍 trait。
+另外,中间还新增加了两行。rand::thread_rng
函数提供实际使用的随机数生成器:它位于当前执行线程,并从操作系统获取 seed。接下来,调用随机数生成器的 gen_range
方法。这个方法由刚才引入到作用域的 Rng
trait 定义。gen_range
方法获取两个数字作为参数,并生成一个范围在两者之间的随机数。它包含下限但不包含上限,所以需要指定1
和101
来请求一个1
和100
之间的数。
+知道 use 哪个 trait 和该从 crate 中调用哪个方法并不是全部,crate 的说明位于其文档中,Cargo 有一个很棒的功能是:运行 cargo doc --open
命令来构建所有本地依赖提供的文档,并在浏览器中打开。例如,假设你对 rand
crate 中的其他功能感兴趣,cargo doc --open
并点击左侧导航栏中的 rand
。
新增加的第二行代码打印出了秘密数字。这在开发程序时很有用,因为我们可以去测试它,不过在最终版本我们会删掉它。游戏一开始就打印出结果就没什么可玩的了!
尝试运行程序几次:
$ cargo run
@@ -534,7 +534,7 @@ You guessed: 5
比较猜测与秘密数字
现在有了用户输入和一个随机数,我们可以比较他们。这个步骤如列表 2-4 所示:
Filename: src/main.rs
-extern crate rand;
+extern crate rand;
use std::io;
use std::cmp::Ordering;
@@ -565,17 +565,17 @@ fn main() {
-新代码的第一行是另一个use
,从标准库引入了一个叫做std::cmp::Ordering
的类型到作用域。Ordering
是另一个枚举,像Result
一样,不过Ordering
的成员是Less
、Greater
和Equal
。这是你比较两个值时可能出现三种结果。
-接着在底部的五行新代码使用了Ordering
类型:
-match guess.cmp(&secret_number) {
+新代码的第一行是另一个 use
,从标准库引入了一个叫做 std::cmp::Ordering
的类型。Ordering
是一个像 Result
一样的枚举,不过它的成员是 Less
、Greater
和 Equal
。这是你做比较时可能出现的三种结果。
+接着,底部的五行新代码使用了 Ordering
类型:
+match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
-cmp
方法比较两个值并可以在任何可比较的值上调用。它获取一个任何你想要比较的值的引用:这里是把guess
与secret_number
做比较。cmp
返回一个使用use
语句引入作用域的Ordering
枚举的成员。我们使用一个match
表达式根据对guess
和secret_number
中的值调用cmp
后返回的哪个Ordering
枚举成员来决定接下来干什么。
-一个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
表达式就此终止,因为在这个特定场景下没有检查最后一个分支的必要。
+cmp
方法用来比较两个值。在任何可比较的值上调用,然后获取另一个被比较值的引用:这里是把 guess
与 secret_number
做比较,返回一个 Ordering
枚举的成员。再使用一个 match
表达式,根据枚举成员来决定接下来干什么。
+一个 match
表达式由 分支(arms) 构成。一个分支包含一个 模式(pattern)和动作,表达式头的求值结果符合分支的模式时将执行对应的动作。Rust 获取提供给 match
的值并挨个检查每个分支的模式。match
结构和模式是 Rust 的强大功能,它体现了代码可能遇到的多种情形,并帮助你没有遗漏的处理。这些功能将分别在第六章和第十八章详细介绍。
+让我们看看使用 match
表达式的例子。假设用户猜了 50,这时随机生成的秘密数字是 38。比较 50 与 38 时,因为 50 比 38 要大,cmp
方法会返回 Ordering::Greater
。match
表达式得到该值,然后检查第一个分支的模式,Ordering::Less
与 Ordering::Greater
并不匹配,所以它忽略了这个分支的动作并来到下一个分支。下一个分支的模式是 Ordering::Greater
,正确匹配!这个分支关联的动作被执行,在屏幕打印出 Too big!
。match
表达式就此终止,因为该场景下没有检查最后一个分支的必要。
然而,列表 2-4 的代码并不能编译,可以尝试一下:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
@@ -591,10 +591,10 @@ error[E0308]: mismatched types
error: aborting due to previous error
Could not compile `guessing_game`.
-错误的核心表明这里有不匹配的类型(mismatched types)。Rust 拥有一个静态强类型系统。不过,它也有类型推断。当我们写出let guess = String::new()
时,Rust 能够推断出guess
应该是一个String
,并不需要我们写出类型。另一方面,secret_number
,是一个数字类型。一些数字类型拥有 1 到 100 之间的值:i32
,一个 32 位的数字;u32
,一个 32 位无符号数字;i64
,一个 64 位数字;等等。Rust 默认使用i32
,所以secret_number
的类型就是它,除非增加类型信息或任何能让 Rust 推断出不同数值类型的信息。这里错误的原因在于 Rust 不会比较字符串类型和数字类型。
-最终我们想要把程序从输入中读取到的String
转换为一个真正的数字类型,这样好与秘密数字向比较。可以通过在main
函数体中增加如下两行代码来实现:
+错误的核心表明这里有不匹配的类型(mismatched types)。Rust 有一个静态强类型系统,同时也有类型推断。当我们写出 let guess = String::new()
时,Rust 推断出 guess
应该是一个String
,不需要我们写出类型。另一方面,secret_number
,是一个数字类型。多种数字类型拥有 1 到 100 之间的值:32 位数字 i32
;32 位无符号数字 u32
;64 位数字 i64
等等。Rust 默认使用 i32
,所以它是 secret_number
的类型,除非增加类型信息,或任何能让 Rust 推断出不同数值类型的信息。这里错误的原因在于 Rust 不会比较字符串类型和数字类型。
+所以我们必须把从输入中读取到的 String
转换为一个真正的数字类型,才好与秘密数字进行比较。可以通过在 main
函数体中增加如下两行代码来实现:
Filename: src/main.rs
-extern crate rand;
+extern crate rand;
use std::io;
use std::cmp::Ordering;
@@ -627,13 +627,13 @@ fn main() {
}
这两行代码是:
-let guess: u32 = guess.trim().parse()
+let guess: u32 = guess.trim().parse()
.expect("Please type a number!");
-这里创建了一个叫做guess
的变量。不过等等,难道这个程序不是已经有了一个叫做guess
的变量了吗?确实如此,不过 Rust 允许我们通过覆盖(shadow) 用一个新值来覆盖guess
之前的值。这个功能经常用在类似需要把一个值从一种类型转换到另一种类型的场景。shadowing 允许我们复用guess
变量的名字而不是强迫我们创建两个不同变量,比如guess_str
和guess
。(第三章会介绍 shadowing 的更多细节。)
-guess
被绑定到guess.trim().parse()
表达式。表达式中的guess
对应包含输入的String
类型的原始guess
。String
实例的trim
方法会消除字符串开头和结尾的空白。u32
只能包含数字字符。不过用户必须输入回车键才能让read_line
返回。当用户按下回车键时,会在字符串中增加一个换行(newline)字符。例如,如果用户输入 5 并回车,guess
看起来像这样:5\n
。\n
代表“换行”,回车键。trim
方法消除\n
,只留下5
。
-字符串的parse
方法解析一个字符串成某个数字。因为这个方法可以解析多种数字类型,需要告诉 Rust 我们需要的具体的数字类型,这里通过let guess: u32
指定。guess
后面的冒号(:
)告诉 Rust 我们指明了变量的类型。Rust 有一些内建的数字类型;这里的u32
是一个无符号的 32 位整型。它是一个好的较小正整数的默认类型。第三章会讲到其他数字类型。另外,例子程序中的u32
注解和与secret_number
的比较意味着 Rust 会推断secret_number
应该是也是u32
类型。现在可以使用相同类型比较两个值了!
-parse
调用容易产生错误。例如,如果字符串包含A👍%
,就无法将其转换为一个数字。因为它可能失败,parse
方法返回一个Result
类型,非常像之前在“使用Result
类型来处理潜在的错误”部分讨论的read_line
方法。这里再次类似的使用expect
方法处理这个Result
类型。如果parse
因为不能从字符串生成一个数字而返回一个Err
的Result
成员时,expect
会使游戏崩溃并打印提供给它的信息。如果parse
能成功地将字符串转换为一个数字,它会返回Result
的Ok
成员,同时expect
会返回Ok
中我们需要的数字。
+这里创建了一个叫做 guess
的变量。不过等等,不是已经有了一个叫做guess
的变量了吗?确实如此,不过 Rust 允许遮盖(shadow),用一个新值来遮盖 guess
之前的值。这个功能常用在需要转换值类型之类的场景,它允许我们复用 guess
变量的名字,而不是被迫创建两个不同变量,诸如 guess_str
和 guess
之类。(第三章会介绍 shadowing 的更多细节。)
+guess
被绑定到 guess.trim().parse()
表达式。表达式中的 guess
是包含输入的 String
类型。String
实例的 trim
方法会去除字符串开头和结尾的空白。u32
只能由数字字符转换,不过用户必须输入回车键才能让 read_line
返回,然而用户按下回车键时,会在字符串中增加一个换行(newline)符。例如,用户输入 5 并回车,guess
看起来像这样:5\n
。\n
代表“换行”,回车键。trim
方法消除 \n
,只留下5
。
+字符串的parse
方法 将字符串解析成数字。这个方法可以解析多种数字类型,因此需要告诉 Rust 具体的数字类型,这里通过 let guess: u32
指定。guess
后面的冒号(:
)告诉 Rust 我们指定了变量的类型。Rust 有一些内建的数字类型;u32
是一个无符号的 32 位整型。对于不大的正整数来说,它是不错的类型,第三章还会讲到其他数字类型。另外,程序中的 u32
注解以及与 secret_number
的比较,意味着 Rust 会推断出 secret_number
也是 u32
类型。现在可以使用相同类型比较两个值了!
+parse
调用可能产生错误。例如,字符串中包含 A👍%
,就无法将其转换为一个数字。因此,parse
方法返回一个 Result
类型。像之前讨论的 read_line
方法,按部就班的用 expect
方法处理即可。如果 parse
不能从字符串生成一个数字,返回一个 Result::Err
时,expect
会使游戏崩溃并打印附带的信息。如果 parse
成功地将字符串转换为一个数字,它会返回 Result::Ok
,然后 expect
会返回 Ok
中的数字。
现在让我们运行程序!
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
@@ -648,9 +648,9 @@ Too big!
漂亮!即便是在猜测之前添加了空格,程序依然能判断出用户猜测了 76。多运行程序几次来检验不同类型输入的相应行为:猜一个正确的数字,猜一个过大的数字和猜一个过小的数字。
现在游戏已经大体上能玩了,不过用户只能猜一次。增加一个循环来改变它吧!
使用循环来允许多次猜测
-loop
关键字提供了一个无限循环。增加它后给了用户多次猜测的机会:
+loop
关键字提供了一个无限循环。将其加入后,用户可以反复猜测:
Filename: src/main.rs
-extern crate rand;
+extern crate rand;
use std::io;
use std::cmp::Ordering;
@@ -684,8 +684,8 @@ fn main() {
}
}
-如上所示,我们将提示用户猜测之后的所有内容放入了循环。确保这些代码多缩进了四个空格,并再次运行程序。注意这里有一个新问题,因为程序忠实地执行了我们要求它做的:永远地请求另一个猜测!看起来用户没法退出啊!
-用户总是可以使用Ctrl-C
快捷键来终止程序。不过这里还有另一个逃离这个贪得无厌的怪物的方法,就是在“比较猜测”部分提到的parse
:如果用户输入一个非数字回答,程序会崩溃。用户可以利用这一点来退出,如下所示:
+如上所示,我们将提示用户猜测之后的所有内容放入了循环。确保这些代码额外缩进了一层,再次运行程序。注意这里有一个新问题,因为程序忠实地执行了我们的要求:永远地请求另一个猜测,用户没法退出!
+用户总能使用 Ctrl-C
终止程序。不过还有另一个方法跳出无限循环,就是“比较猜测”部分提到的 parse
:如果用户输入一个非数字答案,程序会崩溃。用户可以利用这一点来退出,如下所示:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Running `target/guessing_game`
@@ -709,11 +709,11 @@ thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidD
note: Run with `RUST_BACKTRACE=1` for a backtrace.
error: Process didn't exit successfully: `target/debug/guess` (exit code: 101)
-输入quit
确实退出了程序,同时其他任何非数字输入也一样。然而,毫不夸张的说这是不理想的。我们想要当猜测正确的数字时游戏能自动退出。
+输入 quit
确实退出了程序,同时其他任何非数字输入也一样。然而,这并不理想,我们想要当猜测正确的数字时游戏能自动退出。
猜测正确后退出
-让我们增加一个break
来在用户胜利时退出游戏:
+让我们增加一个 break
,在用户猜对时退出游戏:
Filename: src/main.rs
-extern crate rand;
+extern crate rand;
use std::io;
use std::cmp::Ordering;
@@ -750,18 +750,18 @@ fn main() {
}
}
-通过在You win!
之后增加一行break
,程序在用户猜对了神秘数字后会退出循环。退出循环也就意味着退出程序,因为循环是main
的最后一部分。
+通过在 You win!
之后增加一行 break
,用户猜对了神秘数字后会退出循环。退出循环也意味着退出程序,因为循环是 main
的最后一部分。
处理无效输入
-为了进一步改善游戏性,而不是在用户输入非数字时崩溃,需要让游戏忽略非数字从而用户可以继续猜测。可以通过修改guess
从String
转化为u32
那部分代码来实现:
-let guess: u32 = match guess.trim().parse() {
+为了进一步改善游戏性,不要在用户输入非数字时崩溃,需要忽略非数字,让用户可以继续猜测。可以通过修改 guess
将 String
转化为 u32
那部分代码来实现:
+let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
-从expect
调用切换到match
语句是如何从遇到错误就崩溃到真正处理错误的常用手段。记住parse
返回一个Result
类型,而Result
是一个拥有Ok
或Err
两个成员的枚举。在这里使用match
表达式,就像之前处理cmp
方法返回的Ordering
一样。
-如果parse
能够成功的将字符串转换为一个数字,它会返回一个包含结果数字Ok
值。这个Ok
值会匹配第一个分支的模式,这时match
表达式仅仅返回parse
产生的Ok
值之中的num
值。这个数字会最终如期变成新创建的guess
变量。
-如果parse
不能将字符串转换为一个数字,它会返回一个包含更多错误信息的Err
值。Err
值不能匹配第一个match
分支的Ok(num)
模式,但是会匹配第二个分支的Err(_)
模式。_
是一个包罗万象的值;在这个例子中,我们想要匹配所有Err
值,不管其中有何种信息。所以程序会执行第二个分支的代码,continue
,这意味着进入loop
的下一次循环并请求另一个猜测。这样程序就有效地忽略了parse
可能遇到的所有错误!
-现在万事俱备(只欠东风)了。运行cargo run
来尝试一下:
+将 expect
调用换成 match
语句,是从“立即崩溃”转到真正处理错误的惯用方法。须知 parse
返回一个 Result
类型,而 Result
是一个拥有 Ok
或 Err
成员的枚举。这里使用的 match
表达式,和之前处理 cmp
方法返回 Ordering
时用的一样。
+如果 parse
能够成功的将字符串转换为一个数字,它会返回一个包含结果数字的 Ok
。这个 Ok
值与match
第一个分支的模式相匹配,该分支对应的动作返回 Ok
值中的数字 num
,最后如愿变成新创建的 guess
变量。
+如果 parse
不能将字符串转换为一个数字,它会返回一个包含更多错误信息的 Err
。Err
值不能匹配第一个 match
分支的 Ok(num)
模式,但是会匹配第二个分支的 Err(_)
模式:_
是一个兜底值,用来匹配所有 Err
值,不管其中有何种信息。所以程序会执行第二个分支的动作,continue
意味着进入 loop
的下一次循环,请求另一个猜测。这样程序就忽略了 parse
可能遇到的所有错误!
+现在万事俱备,只需运行 cargo run
:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Running `target/guessing_game`
@@ -782,9 +782,9 @@ Please input your guess.
You guessed: 61
You win!
-太棒了!再有最后一个小的修改,就能完成猜猜看游戏了:还记得程序依然会打印出秘密数字。这在测试时还好,但会毁了游戏性。删掉打印秘密数字的println!
。列表 2-5 为最终代码:
+太棒了!再有最后一个小的修改,就能完成猜猜看游戏了:还记得程序依然会打印出秘密数字。在测试时还好,但正式发布时会毁了游戏。删掉打印秘密数字的 println!
。列表 2-5 为最终代码:
Filename: src/main.rs
-extern crate rand;
+extern crate rand;
use std::io;
use std::cmp::Ordering;
@@ -824,18 +824,18 @@ fn main() {
总结
此时此刻,你顺利完成了猜猜看游戏!恭喜!
-这是一个通过动手实践的方式想你介绍许多 Rust 新知识的项目:let
、match
、方法、关联函数,使用外部 crate,等等。接下来的几章,我们将会详细学习这些概念。第三章涉及到大部分编程语言都有的概念,比如变量、数据类型和函数,以及如何在 Rust 中使用他们。第四章探索所有权(ownership),这是一个 Rust 同其他语言都不相同的功能。第五章讨论结构体和方法的语法,而第六章侧重解释枚举。
+这是一个通过动手实践学习 Rust 新概念的项目:let
、match
、方法、关联函数、使用外部 crate 等等,接下来的几章,我们将会继续深入。第三章涉及到大部分编程语言都有的概念,比如变量、数据类型和函数,以及如何在 Rust 中使用他们。第四章探索所有权(ownership),这是一个 Rust 同其他语言都不相同的功能。第五章讨论结构体和方法的语法,而第六章侧重解释枚举。
通用编程概念
ch03-00-common-programming-concepts.md
commit 04aa3a45eb72855b34213703718f50a12a3eeec8
-这一章涉及到几乎出现在所有编程语言中的概念,以及他们在 Rust 中是如何工作的。很多编程语言在核心概念上都是共通的。本章中展示的所有概念没有一个是 Rust 所特有的,不过我们会在 Rust 环境中讨论他们并解释他们的使用习惯。
-具体的,我们将会学习变量,基本类型,函数,注释和控制流。这些基础知识将会出现在每一个 Rust 程序中,提早学习这些概念会使你在起步时拥有一个强有力的基础。
+本章涉及一些几乎所有编程语言都有的概念,以及他们在 Rust 中是如何工作的。很多编程语言的核心概念都是共通的,本章中展示的概念都不是 Rust 特有的,不过我们会在 Rust 环境中讨论他们,解释他们的使用习惯。
+具体的,我们将会学习变量,基本类型,函数,注释和控制流。这些基础知识将会出现在每一个 Rust 程序中,提早学习这些概念会使你拥有坚实的起步。
关键字
-Rust 语言有一系列被保留为只能被语言使用的关键字(keywords),如大部分语言一样。注意你不能使用这些关键字作为变量或函数的名称。大部分关键字有特殊的意义,并将会被用来进行 Rust 程序中的多种任务;一些关键字目前没有相关的功能不过为了将来可能添加进 Rust 的功能而被保留。可以在附录 A 中找到一份关键字的列表
+Rust 语言有一系列保留的关键字(keywords),只能由语言本身使用,像大部分语言一样。你不能使用这些关键字作为变量或函数的名称,大部分关键字有特殊的意义,并被用来完成 Rust 程序中的各种任务;一些关键字目前没有分配,是为将来可能添加的功能保留的。可以在附录 A 中找到关键字的列表。
变量和可变性
@@ -843,8 +843,8 @@ commit 04aa3a45eb72855b34213703718f50a12a3eeec8
commit 04aa3a45eb72855b34213703718f50a12a3eeec8
-第二章中提到过,变量默认是不可变(immutable)的。这是 Rust 中许多鼓励利用 Rust 提供的安全和简单并发优势编写代码的助力之一。不过,仍然有使变量可变的选项。让我们探索一下如何以及为什么鼓励你拥抱不可变性,还有为什么你可能想要弃之不用。
-当变量是不可变时,这意味着一旦一个值被绑定上了一个名称,你就不能改变这个值。作为说明,通过cargo new --bin variables
在 projects 目录生成一个叫做 variables 的新项目。
+第二章中提到过,变量默认是不可变(immutable)的。这是利用 Rust 安全和简单并发的优势编写代码一大助力。不过,变量仍然有可变的选项。让我们探讨一下,拥抱不可变性的原因及方法,以及何时你不想拥抱。
+当变量不可变时,意味着一旦值被绑定上一个名称,你就不能改变这个值。作为说明,通过cargo new --bin variables
在 projects 目录生成一个叫做 variables 的新项目。
接着,在新建的 variables 目录,打开 src/main.rs 并替换其代码为如下:
Filename: src/main.rs
fn main() {
@@ -866,10 +866,10 @@ error[E0384]: re-assignment of immutable variable `x`
4 | x = 6;
| ^^^^^ re-assignment of immutable variable
-这个例子显示了编译器如何帮助你寻找程序中的错误。即便编译器错误可能是令人沮丧的,他们也仅仅意味着程序不能安全的完成你想让它完成的工作;他们不能说明你不是一个好的程序员!有经验的 Rustacean 们也会遇到编译器错误。这些错误表明错误的原因是对不可变变量重新赋值
(re-assignment of immutable variable
),因为我们尝试对不可变变量x
赋第二个值。
-当尝试去改变之前设计为不可变的值出现编译时错误是很重要的,因为这种情况可能导致 bug。如果代码的一部分假设一个值永远也不会改变而另一部分代码改变了它,这样第一部分代码就有可能不能像它设计的那样运行。不得不承认这种 bug 难以跟踪,尤其是当第二部分代码只是有时当变量使不可变时。
-Rust 编译器保证如果声明一个值不会改变,它就真的不会改变。这意味着当阅读和编写代码时,并不需要记录如何以及在哪可能会被改变,这使得代码易于推导。
-不过可变性也是非常有用的。变量只是默认不可变;可以通过在变量名之前增加mut
来使其可变。它向之后的读者表明了其他部分的代码将会改变这个变量值的意图。
+这个例子展示了编译器如何帮助你找出程序中的错误。即便编译错误令人沮丧,那也不过是说程序不能安全的完成你想让它完成的工作;而不能说明你是不是一个好程序员!有经验的 Rustacean 们一样会遇到编译错误。这些错误给出的原因是对不可变变量重新赋值
(re-assignment of immutable variable
),因为我们尝试对不可变变量x
赋第二个值。
+尝试去改变预设为不可变的值,产生编译错误是很重要的,因为这种情况可能导致 bug:如果代码的一部分假设一个值永远也不会改变,而另一部分代码改变了它,第一部分代码就有可能以不可预料的方式运行。不得不承认这种 bug 难以跟踪,尤其是第二部分代码只是有时改变其值。
+Rust 编译器保证,如果声明一个值不会变,它就真的不会变。这意味着当阅读和编写代码时,不需要厘清如何以及哪里可能会被改变,从而使得代码易于推导。
+不过可变性也是非常有用的。变量只是默认不可变;可以通过在变量名之前加 mut
来使其可变。它向读者表明了其他代码将会改变这个变量的意图。
例如,改变 src/main.rs 并替换其代码为如下:
Filename: src/main.rs
fn main() {
@@ -886,21 +886,21 @@ error[E0384]: re-assignment of immutable variable `x`
The value of x is: 5
The value of x is: 6
-通过mut
,允许把绑定到x
的值从5
改成6
。在一些情况下,你会想要一个变量是可变的,因为这比只使用不可变变量实现的代码更易于编写。
-除了避免 bug 外,这里还有多个需要权衡取舍的地方。例如,有时使用大型数据结构时,适当地使变量可变可能比复制和返回新分配的实例要更快。对于较小的数据结构,总是创建新实例并采用一种更函数式的编程风格可能会使代码更易理解。所以为了可读性而造成的性能惩罚也许是值得的。
+通过 mut
,允许把绑定到 x
的值从 5
改成 6
。在一些情况下,你会想要一个变量可变,因为相对不可变的风格更容易写。
+除了避免 bug 外,还有多处需要权衡取舍。例如,使用大型数据结构时,适当地使变量可变,可能比复制和返回新分配的实例更快。对于较小的数据结构,总是创建新实例,采用更偏向函数式的风格编程,可能会使代码更易理解,为可读性而遭受性能惩罚或许值得。
变量和常量的区别
-不能改变一个变量的值可能会使你想起另一个大部分编程语言都有的概念:常量(constants)。类似于不可变变量,常量也是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。
-首先,不允许对常量使用mut
:常量不光是默认不能改变,它总是不能改变。
-常量使用const
关键字而不是let
关键字声明,而且必须注明值的类型。现在我们准备在下一部分,“数据类型”,涉及到类型和类型注解,所以现在无需担心这些细节,只需记住必须总是注明类型即可。
+不允许改变值的变量,可能会使你想起另一个大部分编程语言都有的概念:常量(constants)。类似于不可变变量,常量也是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。
+首先,不允许对常量使用 mut
:常量不光默认不能变,它总是不能变。
+声明常量使用 const
关键字而不是 let
,而且必须注明值的类型。在下一部分,“数据类型”,涉及到类型和类型注解,现在无需关心这些细节,记住总是标注类型即可。
常量可以在任何作用域声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。
-最后一个区别是常量只能用于常量表达式,而不能作为函数调用的结果或任何其他只在运行时计算的值。
-这是一个常量声明的例子,它的名称是MAX_POINTS
而它的值是 100,000。(Rust 常量的命名规范是使用大写字母和单词间使用下划线):
+最后一个区别是常量只能用于常量表达式,而不能作为函数调用的结果,或任何其他只在运行时计算的值。
+这是一个常量声明的例子,它的名称是 MAX_POINTS
,值是 100,000。(常量使用下划线分隔的大写字母命名):
const MAX_POINTS: u32 = 100_000;
-常量在整个程序生命周期中都有效,位于它声明的作用域之中。这使得常量可以用作多个部分的代码可能需要知道的程序范围的值,例如一个游戏中任何玩家可以获得的最高分或者光速。
-将用于整个程序的硬编码的值声明为为常量(并编写文档)对为将来代码维护者表明值的意义是很有用的。它也能帮助你将硬编码的值至于一处以便将来可能需要修改他们。
-覆盖(Shadowing)
-如第二章猜猜看游戏所讲到的,我们可以定义一个与之前变量名称相同的新变量,而新变量会覆盖之前的变量。Rustacean 们称其为第一个变量被第二个给覆盖了,这意味着第二个变量的值是使用这个变量时会看到的值。可以用相同变量名称来覆盖它自己以及重复使用let
关键字来多次覆盖,如下所示:
+常量在整个程序生命周期中都有效,位于它声明的作用域之中。这使得常量可以作为多处代码使用的全局范围的值,例如一个游戏中所有玩家可以获取的最高分或者光速。
+将作用于整个程序的值,由硬编码改为常量(并编写文档),对后来的维护者了解值的意义很用帮助。它也能将硬编码的值汇总一处,为将来可能的修改提供方便。
+遮盖(Shadowing)
+如第二章“猜猜看游戏”所讲的,我们可以定义一个与之前变量重名的新变量,而新变量会遮盖之前的变量。Rustacean 称之为“第一个变量被第二个遮盖了”,这意味着使用这个变量时会看第二个值。可以用相同变量名称来遮盖它自己,以及重复使用 let
关键字来多次遮盖,如下所示:
Filename: src/main.rs
fn main() {
let x = 5;
@@ -912,22 +912,22 @@ The value of x is: 6
println!("The value of x is: {}", x);
}
-这个程序首先将x
绑定到值5
上。接着通过let x =
覆盖x
,获取原始值并加1
这样x
的值就变成6
了。第三个let
语句也覆盖了x
,获取之前的值并乘以2
,x
的最终值是12
。当运行这个程序,它会有如下输出:
+这个程序首先将 x
绑定到值 5
上。接着通过 let x =
遮盖 x
,获取原始值并加 1
这样 x
的值就变成 6
了。第三个 let
语句也覆盖了 x
,获取之前的值并乘以 2
,x
最终的值是 12
。运行这个程序,它会有如下输出:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Running `target/debug/variables`
The value of x is: 12
-这与将变量声明为mut
是有区别的。因为除非再次使用let
关键字,不小心尝试对变量重新赋值会导致编译时错误。我们可以用这个值进行一些计算,不过计算完之后变量仍然是不变的。
-另一个mut
与覆盖的区别是当再次使用let
关键字时,事实上创建了一个新变量,我们可以改变值的类型。例如,假设程序请求用户输入空格来提供在一些文本之间需要多少空间来分隔,不过我们真正需要的是将输入存储成数字(多少个空格):
+这与将变量声明为 mut
是有区别的。因为除非再次使用 let
关键字,不小心尝试对变量重新赋值会导致编译时错误。我们可以用这个值进行一些计算,不过计算完之后变量仍然是不变的。
+mut
与遮盖的另一个区别是,当再次使用 let
时,实际上创建了一个新变量,我们可以改变值的类型。例如,假设程序请求用户输入空格来提供文本间隔,然而我们真正需要的是将输入存储成数字(多少个空格):
let spaces = " ";
let spaces = spaces.len();
-这里允许第一个spaces
变量是字符串类型,而第二个spaces
变量,它是一个恰巧与第一个变量名字相同的崭新的变量,它是数字类型。因此覆盖使我们不必使用不同的名字,比如spaces_str
和spaces_num
;相反,我们可以复用spaces
这个更简单的名称。然而,如果尝试使用mut
,如下所示:
+这里允许第一个 spaces
变量是字符串类型,而第二个 spaces
变量,它是一个恰巧与第一个变量同名的崭新变量,是数字类型。遮盖使我们不必使用不同的名字,如 spaces_str
和 spaces_num
;相反,我们可以复用 spaces
这个更简单的名字。然而,如果尝试使用mut
,如下所示:
let mut spaces = " ";
spaces = spaces.len();
-会导致一个编译时错误,因为不允许改变一个变量的类型:
+会导致一个编译错误,因为改变一个变量的类型是不被允许的:
error[E0308]: mismatched types
--> src/main.rs:3:14
|
@@ -944,11 +944,11 @@ spaces = spaces.len();
commit fe4833a8ef2853c55424e7747a4ef8dd64c35b32
-Rust 中的任何值都有一个具体的类型(type),这告诉了 Rust 它被指定了何种数据,这样 Rust 就知道如何处理这些数据了。这一部分将讲到一些语言内建的类型。我们将这些类型分为两个子集:标量(scalar)和复合(compound)。
-贯穿整个部分,请记住 Rust 是一个静态类型(statically typed)语言,也就是说必须在编译时就知道所有变量的类型。编译器通常可以通过值以及如何使用他们来推断出我们想要用的类型。当多个类型都是可能的时候,比如第二章中parse
将String
转换为数字类型,必须增加类型注解,像这样:
+在 Rust 中,任何值都属于一种明确的类型(type),声明它被指定了何种数据,以便明确其处理方式。我们将分两部分探讨一些内建类型:标量(scalar)和复合(compound)。
+Rust 是静态类型(statically typed)语言,也就是说在编译时就需要知道所有变量的类型,这一认知将贯穿整个章节,请在头脑中明确。通过值的形式及其使用方式,编译器通常可以推断出我们想要用的类型。多种类型均有可能时,比如第二章中使用 parse
将 String
转换为数字,必须增加类型注解,像这样:
let guess: u32 = "42".parse().expect("Not a number!");
-如果这里不添加类型注解,Rust 会显示如下错误,它意味着编译器需要我们提供更多我们想要使用哪个可能的类型的信息:
+如果不添加类型注解,Rust 会显示如下错误。这说明编译器需要更多信息,来了解我们想要的类型:
error[E0282]: unable to infer enough type information about `_`
--> src/main.rs:2:9
|
@@ -957,11 +957,11 @@ commit fe4833a8ef2853c55424e7747a4ef8dd64c35b32
|
= note: type annotations or generic parameter binding required
-在我们讨论各种数据类型时会看到不同的类型注解。
+在我们讨论各种数据类型时,你会看到多样的类型注解。
标量类型
标量类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型。你可能在其他语言中见过他们,不过让我们深入了解他们在 Rust 中时如何工作的。
整型
-整数是一个没有小数部分的数字。我们在这一章的前面使用过一个整型,i32
类型。这个类型声明表明在 32 位系统上它关联的值应该是一个有符号整数(因为这个i
,与u
代表的无符号相对)。表格 3-1 展示了 Rust 内建的整数类型。每一个变体的有符号和无符号列(例如,i32)可以用来声明对应的整数值。
+整数是一个没有小数部分的数字。我们在这一章的前面使用过 i32
类型。该类型声明指示,i32 关联的值应该是一个占据32比特位的有符号整数(因为这个i
,与u
代表的无符号相对)。表格 3-1 展示了 Rust 内建的整数类型。每一种变体的有符号和无符号列(例如,i32)可以用来声明对应的整数值。
Length Signed Unsigned
8-bit i8 u8
@@ -970,10 +970,10 @@ commit fe4833a8ef2853c55424e7747a4ef8dd64c35b32
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。
-另外,isize
和usize
类型依赖运行程序的计算机类型(构架):64 位构架他们是 64 位的而 32 位构架他们就是 32 位的。
-可以使用表格 3-2 中的任何一种形式编写数字字面值。注意除了字节字面值以外的数字字面值允许使用类型后缀,例如57u8
,而_
是可视化分隔符(visual separator),例如1_000
位的。
+每一种变体都可以是有符号或无符号的,并有一个明确的大小。有符号和无符号代表数字能否为负值;换句话说,数字是否需要有一个符号(有符号数),或者永远为正而不需要符号(无符号数)。这有点像在纸上书写数字:当需要考虑符号的时候,数字以加号或减号作为前缀;然而,可以安全地假设为正数时,加号前缀通常省略。有符号数以二进制补码形式(two’s complement representation)存储(如果你不清楚这是什么,可以在网上搜索;对其的解释超出了本书的范畴)。
+每一个有符号的变体可以储存包含从 -(2n - 1) 到 2n - 1 - 1 在内的数字,这里 n
是变体使用的位数。所以 i8
可以储存从 -(27) 到 27 - 1 在内的数字,也就是从 -128 到 127。无符号的变体可以储存从 0 到 2n - 1 的数字,所以 u8
可以储存从 0 到 28 - 1 的数字,也就是从 0 到 255。
+另外,isize
和 usize
类型依赖运行程序的计算机架构:64 位架构上他们是 64 位的, 32 位架构上他们是 32 位的。
+可以使用表格 3-2 中的任何一种形式编写数字字面值。除字节以外的其它字面值允许使用类型后缀,例如 57u8
,允许使用 _
做为分隔符以方便读数,例如 1_000
(分隔符的数量与位置并不影响实际的数字)。
Number literals Example
Decimal 98_222
@@ -982,9 +982,9 @@ commit fe4833a8ef2853c55424e7747a4ef8dd64c35b32
Binary 0b1111_0000
Byte (u8
only) b'A'
-那么如何知晓该使用哪种类型的数字呢?如果对此拿不定主意,Rust 的默认类型通常就是一个很好的选择,这个默认数字类型是i32
:它通常是最快的,甚至是在 64 位系统上。使用isize
或usize
的主要场景是索引一些集合。
+那么该使用哪种类型的数字呢?如果拿不定主意,Rust 的默认类型通常就很好,数字类型默认是 i32
:它通常是最快的,甚至在 64 位系统上也是。isize
或 usize
的主要作为集合的索引。
浮点型
-Rust 也有两个主要的浮点数(floating-point numbers)类型,他们是有小数点的数字。Rust 的浮点数类型是f32
和f64
,分别是 32 位 和 64 位大小。默认类型是f64
,因为它基本上与f32
一样快不过精度更高。在 32 位系统上使用f64
是可能的,不过会比f32
要慢。大部分情况,牺牲潜在可能的更低性能来换取更高的精度是一个合理的初始选择,同时如果怀疑浮点数的大小有问题的时候应该先对代码进行性能测试。
+Rust 同样有两个主要的浮点数类型,f32
和 f64
,它们是带小数点的数字,分别占 32 位和 64 位比特。默认类型是 f64
,因为它与 f32
速度差不多,然而精度更高。在 32 位系统上也能够使用 f64
,不过比使用 f32
要慢。多数情况下,以潜在的性能损耗换取更高的精度是合理的;如果觉得浮点数的大小是个麻烦,你应该以性能测试作为决策依据。
这是一个展示浮点数的实例:
Filename: src/main.rs
fn main() {
@@ -1039,7 +1039,7 @@ commit fe4833a8ef2853c55424e7747a4ef8dd64c35b32
复合类型可以将多个其他类型的值组合成一个类型。Rust 有两个原生的复合类型:元组(tuple)和数组(array)。
将值组合进元组
元组是一个将多个其他类型的值组合进一个复合类型的主要方式。
-我们使用一个括号中的逗号分隔的值列表来创建一个元组。元组中的每一个位置都有一个类型,而且这写不同值的类型也不必是相同的。这个例子中使用了额外的可选类型注解:
+我们使用一个括号中的逗号分隔的值列表来创建一个元组。元组中的每一个位置都有一个类型,而且这些不同值的类型也不必是相同的。这个例子中使用了额外的可选类型注解:
Filename: src/main.rs
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
@@ -1659,7 +1659,7 @@ println!("{}", s); // This will print `hello, world!`
需要一个当我们处理完String
时将内存返回给操作系统的方法
第一部分由我们完成:当调用String::from
时,它的实现请求它需要的内存。这在编程语言中是非常通用的。
-然而,第二部分实现起来就各有区别了。在有**垃圾回收(GC)**的语言中, GC 记录并清除不再使用的内存,而我们作为程序员,并不需要关心他们。没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们程序员的责任了,正如请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要allocate
和free
一一对应。
+然而,第二部分实现起来就各有区别了。在有垃圾回收(GC)的语言中, GC 记录并清除不再使用的内存,而我们作为程序员,并不需要关心他们。没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们程序员的责任了,正如请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要allocate
和free
一一对应。
Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。下面是列表 4-1 中作用域例子的一个使用String
而不是字符串字面值的版本:
{
let s = String::from("hello"); // s is valid from this point forward
@@ -1968,7 +1968,7 @@ immutable
哇哦!我们也不能在拥有不可变引用的同时拥有可变引用。不可变引用的用户可不希望在它的眼皮底下值突然就被改变了!然而,多个不可变引用是没有问题的因为没有哪个读取数据的人有能力影响其他人读取到的数据。
即使这些错误有时是使人沮丧的。记住这是 Rust 编译器在提早指出一个潜在的 bug(在编译时而不是运行时)并明确告诉你问题在哪而不是任由你去追踪为何有时数据并不是你想象中的那样。
悬垂引用
-在存在指针的语言中,容易通过释放内存时保留指向它的指针而错误地生成一个悬垂指针(dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者,。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当我们拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
+在存在指针的语言中,容易通过释放内存时保留指向它的指针而错误地生成一个悬垂指针(dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当我们拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
让我们尝试创建一个悬垂引用:
Filename: src/main.rs
fn main() {
@@ -9647,11 +9647,11 @@ commit 9430a3d28a2121a938d704ce48b15d21062f880e
我们说“几乎所有内容都不属于语言本身”,那么属于语言本身的是什么呢?是两个 trait,都位于std::marker
: Sync
和Send
。
Send
用于表明所有权可能被传送给其他线程
Send
标记 trait 表明类型的所有权可能被在线程间传递。几乎所有的 Rust 类型都是Send
的,不过有一些例外。比如标准库中提供的 Rc<T>
:如果克隆Rc<T>
值,并尝试将克隆的所有权传递给另一个线程,这两个线程可能会同时更新引用计数。正如上一部分提到的,Rc<T>
被实现为用于单线程场景,这时不需要为拥有线程安全的引用计数而付出性能代价。
-因为Rc<T>
没有标记为Send
,Rust 的类型系统和 trait bound 会确保我们不会错误的把一个Rc<T>
值不安全的在线程间传递。列表 16-14 曾尝试这么做,不过得到了一个错误,the trait Send is not implemented for Rc<Mutex<i32>>
。当切换为标记为Send
的Arc<T>
时,就没有问题了。
-任何完全由Send
的类型组成的类型也会自动被标记为Send
。几乎所有基本类型都是Send
的,大部分标准库类型是Send
的,除了Rc<T>
,以及第十九章将会讨论的裸指针(raw pointer)。
-Sync
表明多线程访问是安全的
-Sync
标记 trait 表明一个类型可以安全的在多个线程中拥有其值的引用。换一种方式来说就是,对于任意类型T
,如果&T
(T
的引用)是Send
的话T
就是Sync
的,这样其引用就可以安全的发送到另一个线程。类似于Send
的情况,基本类型是Sync
的,完全由Sync
的类型组成的类型也是Sync
的。
-Rc<T>
也不是Sync
的,出于其不是Send
的相同的原因。RefCell<T>
(第十五章讨论过)和Cell<T>
系列类型不是Sync
的。RefCell<T>
在运行时所进行的借用检查也不是线程安全的。Mutex<T>
是Sync
的,正如上一部分所讲的它可以被用来在多线程中共享访问。
+因为 Rc<T>
没有标记为 Send
,Rust 的类型系统和 trait bound 会确保我们不会错误的把一个 Rc<T>
值不安全的在线程间传递。列表 16-14 曾尝试这么做,不过得到了一个错误,the trait Send is not implemented for Rc<Mutex<i32>>
。而使用标记为 Send
的 Arc<T>
时,就没有问题了。
+任何完全由 Send
的类型组成的类型也会自动被标记为 Send
:几乎所有基本类型都是 Send
的,大部分标准库类型是Send
的,除了Rc<T>
,以及第十九章将会讨论的裸指针(raw pointer)。
+Sync
表明多线程访问是安全的
+Sync
标记 trait 表明一个类型可以安全的在多个线程中拥有其值的引用。换一种方式来说,对于任意类型 T
,如果&T
(T
的引用)是Send
的话T
就是Sync
的,这样其引用就可以安全的发送到另一个线程。类似于 Send
的情况,基本类型是 Sync
的,完全由 Sync
的类型组成的类型也是 Sync
的。
+Rc<T>
也不是 Sync
的,出于其不是Send
的相同的原因。RefCell<T>
(第十五章讨论过)和Cell<T>
系列类型不是Sync
的。RefCell<T>
在运行时所进行的借用检查也不是线程安全的。Mutex<T>
是Sync
的,正如上一部分所讲的它可以被用来在多线程中共享访问。
手动实现Send
和Sync
是不安全的
通常并不需要实现Send
和Sync
trait,由属于Send
和Sync
的类型组成的类型,自动就是Send
和Sync
的。因为他们是标记 trait,甚至都不需要实现任何方法。他们只是用来加强并发相关的不可变性的。
实现这些标记 trait 涉及到编写不安全的 Rust 代码,第十九章将会讲述具体的方法;当前重要的是,在创建新的由不是Send
和Sync
的部分构成的并发类型时需要多加小心,以确保维持其安全保证。The Nomicon 中有更多关于这些保证以及如何维持他们的信息。
@@ -9753,22 +9753,22 @@ commit 67876e3ef5323ce9d394f3ea6b08cb3d173d9ba9
-有时,我们希望使用的类型的集合对于使用库的程序员来说是可扩展的。例如,很多图形用户接口(GUI)工具有一个条目列表的概念,它通过遍历列表并对每一个条目调用draw
方法来绘制在屏幕上。我们将要创建一个叫做rust_gui
的包含一个 GUI 库结构的库 crate。GUI 库可以包含一些供开发者使用的类型,比如Button
或TextField
。使用rust_gui
的程序员会想要创建更多可以绘制在屏幕上的类型:一个程序员可能会增加一个Image
,而另一个可能会增加一个SelectBox
。我们不会在本章节实现一个功能完善的 GUI 库,不过会展示各个部分是如何结合在一起的。
+有时,我们希望使用的类型的集合对于使用库的程序员来说是可扩展的。例如,很多图形用户接口(GUI)工具有一个条目列表的概念,它通过遍历列表并对每一个条目调用 draw
方法来绘制在屏幕上。我们将要创建一个叫做 rust_gui
的包含一个 GUI 库结构的库 crate。GUI 库可以包含一些供开发者使用的类型,比如 Button
或 TextField
。使用 rust_gui
的程序员会想要创建更多可以绘制在屏幕上的类型:一个程序员可能会增加一个 Image
,而另一个可能会增加一个 SelectBox
。我们不会在本章节实现一个功能完善的 GUI 库,不过会展示各个部分是如何结合在一起的。
当写 rust_gui
库时,我们不知道其他程序员需要什么类型,所以无法定义一个 enum
来包含所有的类型。然而 rust_gui
需要跟踪所有这些不同类型的值,需要有在每个值上调用 draw
方法能力。我们的 GUI 库不需要确切地知道调用 draw
方法会发生什么,只需要有可用的方法供我们调用。
在可以继承的语言里,我们会定义一个名为 Component
的类,该类上有一个draw
方法。其他的类比如Button
、Image
和SelectBox
会从Component
继承并拥有draw
方法。它们各自覆写draw
方法以自定义行为,但是框架会把所有的类型当作是Component
的实例,并在其上调用draw
。
定义一个带有自定义行为的Trait
不过,在Rust语言中,我们可以定义一个 Draw
trait,包含名为 draw
的方法。我们定义一个由trait对象组成的vector,绑定了某种指针的trait,比如&
引用或者一个Box<T>
智能指针。
-之前提到,我们不会称结构体和枚举为对象,以区分其他语言的结构体和枚举对象。结构体或者枚举成员中的数据和impl
块中的行为是分开的,而其他语言则是数据和行为被组合到一个对象里。Trait 对象更像其他语言的对象,因为他们将其指针指向的具体对象作为数据,将在trait 中定义的方法作为行为,组合在了一起。但是,trait 对象和其他语言是不同的,我们不能向一个 trait 对象增加数据。trait 对象不像其他语言那样有用:它们的目的是允许从公有行为上抽象。
-trait 对象定义了给定情况下应有的行为。当需要具有某种特性的不确定具体类型时,我们可以把 trait 对象当作 trait 使用。Rust 的类型系统会保证我们为 trait 对象带入的任何值会实现 trait 的方法。我们不需要在编译阶段知道所有可能的类型,却可以把所有的实例统一对待。Listing 17-03展示了如何定义一个名为Draw
的带有draw
方法的trait。
-Filename: src/lib.rs
+之前提到,我们不会称结构体和枚举为对象,以区分其他语言的结构体和枚举对象。结构体或者枚举成员中的数据和impl
块中的行为是分开的,而其他语言则是数据和行为被组合到一个对象里。Trait 对象更像其他语言的对象,因为他们将其指针指向的具体对象作为数据,将在 trait 中定义的方法作为行为,组合在了一起。但是,trait 对象和其他语言是不同的,我们不能向一个 trait 对象增加数据。trait 对象不像其他语言那样有用:它们的目的是允许从公有行为上抽象。
+trait 对象定义了给定情况下应有的行为。当需要具有某种特性的不确定具体类型时,我们可以把 trait 对象当作 trait 使用。Rust 的类型系统会保证我们为 trait 对象带入的任何值会实现 trait 的方法。我们不需要在编译阶段知道所有可能的类型,却可以把所有的实例统一对待。列表 17-03 展示了如何定义一个名为Draw
的带有draw
方法的 trait。
+文件名: src/lib.rs
pub trait Draw {
fn draw(&self);
}
-
+
-因为我们已经在第10章讨论过如何定义 trait,你可能比较熟悉。下面是新的定义:Listing 17-4有一个名为 Screen
的结构体,里面有一个名为 components
的 vector,components
的类型是Box。Box<Draw>
是一个 trait 对象:它是 Box
内部任意一个实现了 Draw
trait 的类型的替身。
-Filename: src/lib.rs
+因为我们已经在第十章讨论过如何定义 trait,你可能比较熟悉。下面是新的定义:列表 17-4 有一个名为 Screen
的结构体,里面有一个名为 components
的 vector,components
的类型是 Box<Draw>
。Box<Draw>
是一个 trait 对象:它是 Box
内部任意一个实现了 Draw
trait 的类型的替身。
+文件名: src/lib.rs
# pub trait Draw {
# fn draw(&self);
# }
@@ -9777,10 +9777,9 @@ pub struct Screen {
pub components: Vec<Box<Draw>>,
}
-
-
-在 Screen
结构体上,我们将要定义一个 run
方法,该方法会在它的 components
上调用 draw
方法,如Listing 17-5所示:
-Filename: src/lib.rs
+
+在 Screen
结构体上,我们将要定义一个 run
方法,该方法会在它的 components
上的每一个元素调用 draw
方法,如列表 17-5 所示:
+文件名: src/lib.rs
# pub trait Draw {
# fn draw(&self);
# }
@@ -9797,10 +9796,10 @@ impl Screen {
}
}
-