diff --git a/docs/ch01-00-introduction.html b/docs/ch01-00-introduction.html index e1ba944..73c97b9 100644 --- a/docs/ch01-00-introduction.html +++ b/docs/ch01-00-introduction.html @@ -47,7 +47,7 @@
@@ -71,12 +71,12 @@

ch01-00-introduction.md
-commit c6920d4a2ee0f282addaf8f6945cefe3ef7bdf09

+commit c51c14215d2ee2cb481bc8a942a3769c6d9a2e1a

-

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

+

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

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

-

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

+registry site),crates.io!我们期待看到使用 Rust 进行创作。

+

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

为本书做出贡献

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

diff --git a/docs/ch01-01-installation.html b/docs/ch01-01-installation.html index 6c33108..7991fdb 100644 --- a/docs/ch01-01-installation.html +++ b/docs/ch01-01-installation.html @@ -47,7 +47,7 @@
diff --git a/docs/ch01-02-hello-world.html b/docs/ch01-02-hello-world.html index ce3e873..cb9b895 100644 --- a/docs/ch01-02-hello-world.html +++ b/docs/ch01-02-hello-world.html @@ -47,7 +47,7 @@
@@ -78,7 +78,7 @@ commit aa1801d99cd3b19c96533f00c852b1c4bd5350a6

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

创建项目文件

-

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

+

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

Linux 和 Mac:

$ mkdir ~/projects
 $ cd ~/projects
@@ -117,8 +117,8 @@ Hello, world!
 
    println!("Hello, world!");
 

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

-

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

-

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

+

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

+

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

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

编译和运行是两个步骤

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

@@ -138,10 +138,10 @@ main.rs
$ ./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),不过你只需要一句命令就可以编译和执行程序。这一切都是语言设计的权衡取舍。

+

来自 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)。

+

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

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

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

$ cargo --version
@@ -159,7 +159,7 @@ main.rs
 
$ cargo new hello_cargo --bin
 $ cd hello_cargo
 
-

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

+

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

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

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

Filename: Cargo.toml

diff --git a/docs/ch02-00-guessing-game-tutorial.html b/docs/ch02-00-guessing-game-tutorial.html index d544821..814bf5f 100644 --- a/docs/ch02-00-guessing-game-tutorial.html +++ b/docs/ch02-00-guessing-game-tutorial.html @@ -47,7 +47,7 @@
@@ -76,7 +76,7 @@ commit 77370c073661548dd56bbcb43cc64713585acbba

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

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

准备一个新项目

-

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

+

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

$ cargo new guessing_game --bin
 $ cd guessing_game
 
@@ -146,7 +146,7 @@ println!("Please input your guess.");

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

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

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

+

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

let foo = bar;
 

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

@@ -157,7 +157,7 @@ let mut bar = 5; // mutable

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

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

-

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

+

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

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

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

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

@@ -167,7 +167,7 @@ let mut bar = 5; // mutable

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

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

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

-

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

+

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

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

.expect("Failed to read line");
 
@@ -177,7 +177,7 @@ let mut bar = 5; // mutable

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

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

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

-

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

+

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

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

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

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

@@ -214,7 +214,7 @@ You guessed: 6

生成一个秘密数字

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

使用 crate 来增加更多功能

-

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

+

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

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

Filename: Cargo.toml

[dependencies]
@@ -249,7 +249,7 @@ as a dependency

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

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

更新 crate 到一个新版本

-

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

+

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

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

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

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

    生成一个随机数

    -

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

    +

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

    Filename: src/main.rs
    extern crate rand;
    @@ -366,8 +366,8 @@ fn main() {
     }
     

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

    -

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

    -

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

    +

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

    +

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

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

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

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

    +

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

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

    Filename: src/main.rs

    extern crate rand;
    @@ -422,7 +422,7 @@ fn main() {
     
    let guess: u32 = guess.trim().parse()
         .expect("Please type a number!");
     
    -

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

    +

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

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

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

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

    diff --git a/docs/ch03-00-common-programming-concepts.html b/docs/ch03-00-common-programming-concepts.html index 97ae0ee..93564a6 100644 --- a/docs/ch03-00-common-programming-concepts.html +++ b/docs/ch03-00-common-programming-concepts.html @@ -47,7 +47,7 @@
    @@ -78,7 +78,7 @@ commit 2067b6e2bff990bceb39ae8f35780bd3bed08644

    关键字

    -

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

    +

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

    diff --git a/docs/ch03-01-variables-and-mutability.html b/docs/ch03-01-variables-and-mutability.html index de405a1..731c884 100644 --- a/docs/ch03-01-variables-and-mutability.html +++ b/docs/ch03-01-variables-and-mutability.html @@ -47,7 +47,7 @@
    @@ -73,7 +73,7 @@
    commit b0fab378c9c6a817d4f0080d7001d085017cdef8

    -

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

    +

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

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

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

    Filename: src/main.rs

    @@ -96,8 +96,8 @@ 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 难以跟踪,尤其是当第二部分代码只是有时当变量使不可变时,这意味着一旦一个值被绑定上了一个名称,你就不能改变这个值。

    +

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

    +

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

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

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

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

    @@ -119,14 +119,14 @@ The value of x is: 6

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

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

    变量和常量的区别

    -

    不能改变一个变量的值可能会使你想起另一个大部分编程语言都有的概念:常量constants)。常量也是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。首先,不允许对常量使用mut:常量不光是默认不能改变,它总是不能改变。常量使用const关键字而不是let关键字声明,而且必须注明值的类型。现在我们准备在下一部分,“数据类型”,涉及到类型和类型注解,所以现在无需担心这些细节。常量可以在任何作用域声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。最后一个区别是常量只能用于常量表达式,而不能作为函数调用的结果或任何其他只在运行时使用到的值。

    +

    不能改变一个变量的值可能会使你想起另一个大部分编程语言都有的概念:常量constants)。常量也是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。首先,不允许对常量使用mut:常量不光是默认不能改变,它总是不能改变。常量使用const关键字而不是let关键字声明,而且必须注明值的类型。现在我们准备在下一部分,“数据类型”,涉及到类型和类型注解,所以现在无需担心这些细节。常量可以在任何作用域声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。最后一个区别是常量只能用于常量表达式,而不能作为函数调用的结果或任何其他只在运行时使用到的值。

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

    const MAX_POINTS: u32 = 100_000;
     

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

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

    覆盖

    -

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

    +

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

    Filename: src/main.rs

    fn main() {
         let x = 5;
    diff --git a/docs/ch03-02-data-types.html b/docs/ch03-02-data-types.html
    index 25ce574..ecbb835 100644
    --- a/docs/ch03-02-data-types.html
    +++ b/docs/ch03-02-data-types.html
    @@ -47,7 +47,7 @@
             
     
             
     
             
    @@ -73,8 +73,8 @@
    commit d05b7c63ff50b3f9126bb5533e0ba5dd424b83d1

    -

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

    -

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

    +

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

    +

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

    let guess: u32 = "42".parse().unwrap();
     

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

    @@ -88,9 +88,9 @@ commit d05b7c63ff50b3f9126bb5533e0ba5dd424b83d1

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

    标量类型

    -

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

    +

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

    整型

    -

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

    +

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

    Table 3-1: Integer Types in Rust

    @@ -121,7 +121,7 @@ commit d05b7c63ff50b3f9126bb5533e0ba5dd424b83d1

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

    浮点型

    -

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

    +

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

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

    Filename: src/main.rs

    fn main() {
    @@ -173,7 +173,7 @@ commit d05b7c63ff50b3f9126bb5533e0ba5dd424b83d1

    Rust 的char类型代表了一个 Unicode 变量值(Unicode Scalar Value),这意味着它可以比 ASCII 表示更多内容。拼音字母(Accented letters),中文/日文/汉语等象形文字,emoji(絵文字)以及零长度的空白字符对于 Rust char类型都是有效的。Unicode 标量值包含从 U+0000U+D7FFU+E000U+10FFFF 之间的值。不过,“字符”并不是一个 Unicode 中的概念,所以人直觉上的“字符”可能与 Rust 中的char并不符合。第八章的“字符串”部分将详细讨论这个主题。

    复合类型

    -

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

    +

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

    将值组合进元组

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

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

    @@ -207,14 +207,14 @@ commit d05b7c63ff50b3f9126bb5533e0ba5dd424b83d1

这个程序创建了一个元组,x,并接着使用索引为每个元素创建新变量。跟大多数编程语言一样,元组的第一个索引值是 0。

数组

-

另一个获取一个多个值集合的方式是数组array)。与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,因为 Rust 中的数组是固定长度的:一旦声明,他们的长度不能增长或缩小。

+

另一个获取一个多个值集合的方式是数组array)。与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,因为 Rust 中的数组是固定长度的:一旦声明,他们的长度不能增长或缩小。

Rust 中数组的值位于中括号中的逗号分隔的列表中:

Filename: src/main.rs

fn main() {
     let a = [1, 2, 3, 4, 5];
 }
 
-

数组在想要在栈(stack)而不是在堆(heap)上为数据分配空间时十分有用(第四章将讨论栈与堆的更多内容),或者是想要确保总是有固定数量的元素时。虽然它并不如 vector 类型那么灵活。vector 类型是标准库提供的一个允许增长和缩小长度的类似数组的集合类型。当不确定是应该使用数组还是 vector 的时候,你可能应该使用 vector:第八章会详细讨论 vector。

+

数组在想要在栈(stack)而不是在堆(heap)上为数据分配空间时十分有用(第四章将讨论栈与堆的更多内容),或者是想要确保总是有固定数量的元素时。虽然它并不如 vector 类型那么灵活。vector 类型是标准库提供的一个允许增长和缩小长度的类似数组的集合类型。当不确定是应该使用数组还是 vector 的时候,你可能应该使用 vector:第八章会详细讨论 vector。

一个你可能想要使用数组而不是 vector 的例子是当程序需要知道一年中月份的名字时。程序不大可能回去增加或减少月份,这时你可以使用数组因为我们知道它总是含有 12 个元素:

let months = ["January", "February", "March", "April", "May", "June", "July",
               "August", "September", "October", "November", "December"];
@@ -250,7 +250,7 @@ thread '<main>' panicked at 'index out of bounds: the len is 5 but the ind
 note: Run with `RUST_BACKTRACE=1` for a backtrace.
 error: Process didn't exit successfully: `target/debug/arrays` (exit code: 101)
 
-

编译并没有产生任何错误,不过程序会导致一个运行时runtime)错误并且不会成功退出。当尝试用索引访问一个元素时,Rust 会检查指定的索引是否小于数组的长度。如果索引超出了数组长度,Rust 会panic,这是 Rust 中的术语,它用于程序因为错误而退出的情况。

+

编译并没有产生任何错误,不过程序会导致一个运行时runtime)错误并且不会成功退出。当尝试用索引访问一个元素时,Rust 会检查指定的索引是否小于数组的长度。如果索引超出了数组长度,Rust 会panic,这是 Rust 中的术语,它用于程序因为错误而退出的情况。

这是第一个在实战中遇到的 Rust 安全原则的例子。在很多底层语言中,并没有进行这类检查,这样当提供了一个不正确的索引时,就会访问无效的内存。Rust 通过立即退出而不是允许内存访问并继续执行来使你免受这类错误困扰。第九章会讨论更多 Rust 的错误处理。

diff --git a/docs/ch03-03-how-functions-work.html b/docs/ch03-03-how-functions-work.html index fcfa432..6e0f9e1 100644 --- a/docs/ch03-03-how-functions-work.html +++ b/docs/ch03-03-how-functions-work.html @@ -47,7 +47,7 @@
@@ -97,7 +97,7 @@ Another function.

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

函数参数

-

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

+

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

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

Filename: src/main.rs

fn main() {
@@ -115,7 +115,7 @@ fn another_function(x: i32) {
 The value of x is: 5
 

another_function的声明有一个叫做x的参数。x的类型被指定为i32。当5被传递给another_function时,println!宏将5放入格式化字符串中大括号的位置。

-

在函数签名中,必须声明每个参数的类型。这是 Rust 设计中一个经过慎重考虑的决定:要求在函数定义中提供类型注解意味着编译器再也不需要在别的地方要求你注明类型就能知道你的意图。

+

在函数签名中,必须声明每个参数的类型。这是 Rust 设计中一个经过慎重考虑的决定:要求在函数定义中提供类型注解意味着编译器再也不需要在别的地方要求你注明类型就能知道你的意图。

当一个函数有多个参数时,使用逗号隔开他们,像这样:

Filename: src/main.rs

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

函数体

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

语句与表达式

-

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

+

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

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

Filename: src/main.rs diff --git a/docs/ch03-04-comments.html b/docs/ch03-04-comments.html index e9928a3..11665a9 100644 --- a/docs/ch03-04-comments.html +++ b/docs/ch03-04-comments.html @@ -47,7 +47,7 @@
@@ -73,7 +73,7 @@
commit 74d6fc999b986b74bf94edd6dcbb5a08a16c12de

-

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

+

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

这是一个注释的例子:

// Hello, world.
 
@@ -94,8 +94,8 @@ commit 74d6fc999b986b74bf94edd6dcbb5a08a16c12de

// I’m feeling lucky today. let lucky_number = 7; } - -这就是注释的全部。并没有什么特别复杂的。
+
+

这就是注释的全部。并没有什么特别复杂的。

diff --git a/docs/ch03-05-control-flow.html b/docs/ch03-05-control-flow.html index 0a3e18c..23368a5 100644 --- a/docs/ch03-05-control-flow.html +++ b/docs/ch03-05-control-flow.html @@ -47,7 +47,7 @@
@@ -104,7 +104,7 @@ condition was true Running `target/debug/branches` condition was false -

另外值得注意的是代码中的条件必须bool。如果像看看条件不是bool值时会发生什么,尝试运行如下代码:

+

另外值得注意的是代码中的条件必须bool。如果像看看条件不是bool值时会发生什么,尝试运行如下代码:

Filename: src/main.rs

fn main() {
     let number = 3;
@@ -216,7 +216,7 @@ error[E0308]: if and else have incompatible types
 

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

使用循环重复执行

-

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

+

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

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

使用loop重复执行代码

loop关键字告诉 Rust 一遍又一遍的执行一段代码直到你明确要求停止。

@@ -335,6 +335,10 @@ the value is: 50 + +
@@ -345,6 +349,10 @@ the value is: 50 + +
diff --git a/docs/ch04-00-understanding-ownership.html b/docs/ch04-00-understanding-ownership.html new file mode 100644 index 0000000..d836896 --- /dev/null +++ b/docs/ch04-00-understanding-ownership.html @@ -0,0 +1,137 @@ + + + + + Rust 程序设计语言 中文版 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+

认识所有权

+
+

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

+
+

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

+ +
+ + + + + + + + + + +
+ + + + + + + + + +
+ + + + + + + + + + + + + + diff --git a/docs/ch04-01-what-is-ownership.html b/docs/ch04-01-what-is-ownership.html new file mode 100644 index 0000000..9d29f78 --- /dev/null +++ b/docs/ch04-01-what-is-ownership.html @@ -0,0 +1,293 @@ + + + + + Rust 程序设计语言 中文版 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+

什么是所有权

+
+

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

+
+

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

+

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

+

因为所有权对很多程序员都是一个新概念,需要一些时间来适应。好消息是随着你对 Rust 和所有权系统的规则越来越有经验,你就越能自然地编写出安全和高效的代码。持之以恒!

+

当你理解了所有权系统,你就会对这个使 Rust 如此独特的功能有一个坚实的基础。在本章中,你将会通过一些例子来学习所有权,他们关注一个非常常见的数据结构:字符串。

+ +
+

栈(Stack)与堆(Heap)

+

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

+

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

+

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

+

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

+

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

+

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

+

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

+

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

+
+ +

所有权规则

+

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

+
+
    +
  1. Rust 中的每一个值都有一个叫做它的所有者owner)的变量。
  2. +
  3. 同时一次只能有一个所有者
  4. +
  5. 当所有者变量离开作用域,这个值将被丢弃。
  6. +
+
+

变量作用域

+

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

+

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

+
let s = "hello";
+
+

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

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

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

+
+
+

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

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

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

+

String类型

+

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

+

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

+

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

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

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

+

这类字符串可以被修改:

+
let mut s = String::from("hello");
+
+s.push_str(", world!"); // push_str() appends a literal to a String
+
+println!("{}", s); // This will print `hello, world!`
+
+

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

+

内存与分配

+

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

+

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

+
    +
  1. 内存必须在运行时向操作系统请求
  2. +
  3. 需要一个当我们处理完String时将内存返回给操作系统的方法
  4. +
+

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

+

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

+

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

+
{
+    let s = String::from("hello"); // s is valid from this point forward
+
+    // do stuff with s
+}                                  // this scope is now over, and s is no
+                                   // longer valid
+
+

这里是一个将String需要的内存返回给操作系统的很自然的位置:当s离开作用域的时候。当变量离开作用域,Rust 为其调用一个特殊的函数。这个函数叫做 drop,在这里String的作者可以放置释放内存的代码。Rust 在结尾的}处自动调用drop

+
+

注意:在 C++ 中,这种 item 在生命周期结束时释放资源的方法有时被称作资源获取即初始化Resource Acquisition Is Initialization (RAII))。如果你使用过 RAII 模式的话应该对 Rust 的drop函数不陌生。

+
+

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

+

变量与数据交互:移动

+

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

+
+
let x = 5;
+let y = x;
+
+
+

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

+
+
+

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

+

现在看看这个String版本:

+
let s1 = String::from("hello");
+let s2 = s1;
+
+

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

+

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

+
+String in memory +
+

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

+
+
+

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

+

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

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

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

+
+
+

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

+
+s1 and s2 to two places +
+

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

+
+
+

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

+

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

+
let s1 = String::from("hello");
+let s2 = s1;
+
+println!("{}", s1);
+
+

你会得到一个类似如下的错误,因为 Rust 禁止你使用无效的引用。

+
error[E0382]: use of moved value: `s1`
+ --> src/main.rs:4:27
+  |
+3 |     let s2 = s1;
+  |         -- value moved here
+4 |     println!("{}, world!",s1);
+  |                           ^^ value used here after move
+  |
+  = note: move occurs because `s1` has type `std::string::String`,
+which does not implement the `Copy` trait
+
+

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

+
+s1 moved to s2 +
+

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

+
+
+

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

+

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

+

变量与数据交互:克隆

+

如果我们确实需要深度复制String中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做clone

+ +
+ + + + + + + + + + +
+ + + + + + + + + +
+ + + + + + + + + + + + + + diff --git a/docs/ch04-02-references-and-borrowing.html b/docs/ch04-02-references-and-borrowing.html new file mode 100644 index 0000000..de5472a --- /dev/null +++ b/docs/ch04-02-references-and-borrowing.html @@ -0,0 +1,131 @@ + + + + + Rust 程序设计语言 中文版 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+

References & Borrowing

+ +
+ + + + + + + + + + +
+ + + + + + + + + +
+ + + + + + + + + + + + + + diff --git a/docs/ch04-03-slices.html b/docs/ch04-03-slices.html new file mode 100644 index 0000000..ec0c51c --- /dev/null +++ b/docs/ch04-03-slices.html @@ -0,0 +1,123 @@ + + + + + Rust 程序设计语言 中文版 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+

Slices

+ +
+ + + + + + + + +
+ + + + + + + +
+ + + + + + + + + + + + + + diff --git a/docs/img/trpl04-01.svg b/docs/img/trpl04-01.svg new file mode 100644 index 0000000..7f5ee8d --- /dev/null +++ b/docs/img/trpl04-01.svg @@ -0,0 +1,65 @@ + + + + + + +%3 + + +table0 + +s1 + +name + +value + +ptr + + +len + +5 + +capacity + +5 + + +table1 + +index + +value + +0 + +h + +1 + +e + +2 + +l + +3 + +l + +4 + +o + + +table0:pointer:c->table1:pointee + + + + + diff --git a/docs/img/trpl04-02.svg b/docs/img/trpl04-02.svg new file mode 100644 index 0000000..7d3a29a --- /dev/null +++ b/docs/img/trpl04-02.svg @@ -0,0 +1,90 @@ + + + + + + +%3 + + +table0 + +s1 + +name + +value + +ptr + + +len + +5 + +capacity + +5 + + +table1 + +index + +value + +0 + +h + +1 + +e + +2 + +l + +3 + +l + +4 + +o + + +table0:pointer:c->table1:pointee + + + + +table3 + +s2 + +name + +value + +ptr + + +len + +5 + +capacity + +5 + + +table3:pointer:c->table1:pointee + + + + + diff --git a/docs/img/trpl04-03.svg b/docs/img/trpl04-03.svg new file mode 100644 index 0000000..a606851 --- /dev/null +++ b/docs/img/trpl04-03.svg @@ -0,0 +1,117 @@ + + + + + + +%3 + + +table0 + +s2 + +name + +value + +ptr + + +len + +5 + +capacity + +5 + + +table1 + +index + +value + +0 + +h + +1 + +e + +2 + +l + +3 + +l + +4 + +o + + +table0:pointer:c->table1:pointee + + + + +table3 + +s1 + +name + +value + +ptr + + +len + +5 + +capacity + +5 + + +table4 + +index + +value + +0 + +h + +1 + +e + +2 + +l + +3 + +l + +4 + +o + + +table3:pointer:c->table4:pointee + + + + + diff --git a/docs/img/trpl04-04.svg b/docs/img/trpl04-04.svg new file mode 100644 index 0000000..1a17b27 --- /dev/null +++ b/docs/img/trpl04-04.svg @@ -0,0 +1,91 @@ + + + + + + +%3 + + +table0 + + +s1 + +name + +value + +ptr + + +len + +5 + +capacity + +5 + + +table1 + +index + +value + +0 + +h + +1 + +e + +2 + +l + +3 + +l + +4 + +o + + +table0:pointer:c->table1:pointee + + + + +table3 + +s2 + +name + +value + +ptr + + +len + +5 + +capacity + +5 + + +table3:pointer:c->table1:pointee + + + + + diff --git a/docs/img/trpl04-05.svg b/docs/img/trpl04-05.svg new file mode 100644 index 0000000..33e5b49 --- /dev/null +++ b/docs/img/trpl04-05.svg @@ -0,0 +1,82 @@ + + + + + + +%3 + + +table0 + +s + +name + +value + +ptr + + + +table1 + +s1 + +name + +value + +ptr + + +len + +5 + +capacity + +5 + + +table0:borrower:c->table1:borrowee + + + + +table2 + +index + +value + +0 + +h + +1 + +e + +2 + +l + +3 + +l + +4 + +o + + +table1:pointer:c->table2:pointee + + + + + diff --git a/docs/img/trpl04-06.svg b/docs/img/trpl04-06.svg new file mode 100644 index 0000000..1a87522 --- /dev/null +++ b/docs/img/trpl04-06.svg @@ -0,0 +1,110 @@ + + + + + + +%3 + + +table0 + +world + +name + +value + +ptr + + +len + +5 + + +table4 + +index + +value + +0 + +h + +1 + +e + +2 + +l + +3 + +l + +4 + +o + +5 + + + +6 + +w + +7 + +o + +8 + +r + +9 + +l + +10 + +d + + +table0:pointer2:c->table4:pointee2 + + + + +table3 + +s + +name + +value + +ptr + + +len + +11 + +capacity + +11 + + +table3:pointer:c->table4:pointee + + + + + diff --git a/docs/index.html b/docs/index.html index 5406b3c..a872f59 100644 --- a/docs/index.html +++ b/docs/index.html @@ -46,7 +46,7 @@
@@ -70,12 +70,12 @@

ch01-00-introduction.md
-commit c6920d4a2ee0f282addaf8f6945cefe3ef7bdf09

+commit c51c14215d2ee2cb481bc8a942a3769c6d9a2e1a

-

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

+

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

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

-

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

+registry site),crates.io!我们期待看到使用 Rust 进行创作。

+

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

为本书做出贡献

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

diff --git a/docs/print.html b/docs/print.html index a1b7d63..87752c5 100644 --- a/docs/print.html +++ b/docs/print.html @@ -47,7 +47,7 @@
@@ -71,12 +71,12 @@

ch01-00-introduction.md
-commit c6920d4a2ee0f282addaf8f6945cefe3ef7bdf09

+commit c51c14215d2ee2cb481bc8a942a3769c6d9a2e1a

-

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

+

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

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

-

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

+registry site),crates.io!我们期待看到使用 Rust 进行创作。

+

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

为本书做出贡献

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

安装

@@ -129,7 +129,7 @@ commit aa1801d99cd3b19c96533f00c852b1c4bd5350a6

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

创建项目文件

-

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

+

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

Linux 和 Mac:

$ mkdir ~/projects
 $ cd ~/projects
@@ -168,8 +168,8 @@ Hello, world!
 
    println!("Hello, world!");
 

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

-

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

-

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

+

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

+

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

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

编译和运行是两个步骤

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

@@ -189,10 +189,10 @@ main.rs
$ ./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),不过你只需要一句命令就可以编译和执行程序。这一切都是语言设计的权衡取舍。

+

来自 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)。

+

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

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

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

$ cargo --version
@@ -210,7 +210,7 @@ main.rs
 
$ cargo new hello_cargo --bin
 $ cd hello_cargo
 
-

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

+

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

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

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

Filename: Cargo.toml

@@ -290,7 +290,7 @@ commit 77370c073661548dd56bbcb43cc64713585acbba

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

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

准备一个新项目

-

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

+

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

$ cargo new guessing_game --bin
 $ cd guessing_game
 
@@ -360,7 +360,7 @@ println!("Please input your guess.");

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

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

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

+

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

let foo = bar;
 

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

@@ -371,7 +371,7 @@ let mut bar = 5; // mutable

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

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

-

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

+

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

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

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

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

@@ -381,7 +381,7 @@ let mut bar = 5; // mutable

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

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

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

-

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

+

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

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

.expect("Failed to read line");
 
@@ -391,7 +391,7 @@ let mut bar = 5; // mutable

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

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

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

-

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

+

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

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

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

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

@@ -428,7 +428,7 @@ You guessed: 6

生成一个秘密数字

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

使用 crate 来增加更多功能

-

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

+

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

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

Filename: Cargo.toml

[dependencies]
@@ -463,7 +463,7 @@ as a dependency

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

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

更新 crate 到一个新版本

-

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

+

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

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

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

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

    生成一个随机数

    -

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

    +

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

    Filename: src/main.rs
    extern crate rand;
    @@ -580,8 +580,8 @@ fn main() {
     }
     

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

    -

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

    -

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

    +

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

    +

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

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

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

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

    +

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

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

    Filename: src/main.rs

    extern crate rand;
    @@ -636,7 +636,7 @@ fn main() {
     
    let guess: u32 = guess.trim().parse()
         .expect("Please type a number!");
     
    -

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

    +

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

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

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

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

    @@ -846,7 +846,7 @@ commit 2067b6e2bff990bceb39ae8f35780bd3bed08644

    关键字

    -

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

    +

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

    变量和可变性

    @@ -854,7 +854,7 @@ commit 2067b6e2bff990bceb39ae8f35780bd3bed08644


    commit b0fab378c9c6a817d4f0080d7001d085017cdef8

    -

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

    +

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

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

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

    Filename: src/main.rs

    @@ -877,8 +877,8 @@ 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 难以跟踪,尤其是当第二部分代码只是有时当变量使不可变时,这意味着一旦一个值被绑定上了一个名称,你就不能改变这个值。

    +

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

    +

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

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

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

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

    @@ -900,14 +900,14 @@ The value of x is: 6

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

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

    变量和常量的区别

    -

    不能改变一个变量的值可能会使你想起另一个大部分编程语言都有的概念:常量constants)。常量也是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。首先,不允许对常量使用mut:常量不光是默认不能改变,它总是不能改变。常量使用const关键字而不是let关键字声明,而且必须注明值的类型。现在我们准备在下一部分,“数据类型”,涉及到类型和类型注解,所以现在无需担心这些细节。常量可以在任何作用域声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。最后一个区别是常量只能用于常量表达式,而不能作为函数调用的结果或任何其他只在运行时使用到的值。

    +

    不能改变一个变量的值可能会使你想起另一个大部分编程语言都有的概念:常量constants)。常量也是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。首先,不允许对常量使用mut:常量不光是默认不能改变,它总是不能改变。常量使用const关键字而不是let关键字声明,而且必须注明值的类型。现在我们准备在下一部分,“数据类型”,涉及到类型和类型注解,所以现在无需担心这些细节。常量可以在任何作用域声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。最后一个区别是常量只能用于常量表达式,而不能作为函数调用的结果或任何其他只在运行时使用到的值。

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

    const MAX_POINTS: u32 = 100_000;
     

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

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

    覆盖

    -

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

    +

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

    Filename: src/main.rs

    fn main() {
         let x = 5;
    @@ -951,8 +951,8 @@ spaces = spaces.len();
     
    commit d05b7c63ff50b3f9126bb5533e0ba5dd424b83d1

    -

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

    -

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

    +

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

    +

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

    let guess: u32 = "42".parse().unwrap();
     

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

    @@ -966,9 +966,9 @@ commit d05b7c63ff50b3f9126bb5533e0ba5dd424b83d1

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

    标量类型

    -

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

    +

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

    整型

    -

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

    +

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

    Table 3-1: Integer Types in Rust

    @@ -999,7 +999,7 @@ commit d05b7c63ff50b3f9126bb5533e0ba5dd424b83d1

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

    浮点型

    -

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

    +

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

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

    Filename: src/main.rs

    fn main() {
    @@ -1051,7 +1051,7 @@ commit d05b7c63ff50b3f9126bb5533e0ba5dd424b83d1

    Rust 的char类型代表了一个 Unicode 变量值(Unicode Scalar Value),这意味着它可以比 ASCII 表示更多内容。拼音字母(Accented letters),中文/日文/汉语等象形文字,emoji(絵文字)以及零长度的空白字符对于 Rust char类型都是有效的。Unicode 标量值包含从 U+0000U+D7FFU+E000U+10FFFF 之间的值。不过,“字符”并不是一个 Unicode 中的概念,所以人直觉上的“字符”可能与 Rust 中的char并不符合。第八章的“字符串”部分将详细讨论这个主题。

    复合类型

    -

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

    +

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

    将值组合进元组

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

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

    @@ -1085,14 +1085,14 @@ commit d05b7c63ff50b3f9126bb5533e0ba5dd424b83d1

这个程序创建了一个元组,x,并接着使用索引为每个元素创建新变量。跟大多数编程语言一样,元组的第一个索引值是 0。

数组

-

另一个获取一个多个值集合的方式是数组array)。与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,因为 Rust 中的数组是固定长度的:一旦声明,他们的长度不能增长或缩小。

+

另一个获取一个多个值集合的方式是数组array)。与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,因为 Rust 中的数组是固定长度的:一旦声明,他们的长度不能增长或缩小。

Rust 中数组的值位于中括号中的逗号分隔的列表中:

Filename: src/main.rs

fn main() {
     let a = [1, 2, 3, 4, 5];
 }
 
-

数组在想要在栈(stack)而不是在堆(heap)上为数据分配空间时十分有用(第四章将讨论栈与堆的更多内容),或者是想要确保总是有固定数量的元素时。虽然它并不如 vector 类型那么灵活。vector 类型是标准库提供的一个允许增长和缩小长度的类似数组的集合类型。当不确定是应该使用数组还是 vector 的时候,你可能应该使用 vector:第八章会详细讨论 vector。

+

数组在想要在栈(stack)而不是在堆(heap)上为数据分配空间时十分有用(第四章将讨论栈与堆的更多内容),或者是想要确保总是有固定数量的元素时。虽然它并不如 vector 类型那么灵活。vector 类型是标准库提供的一个允许增长和缩小长度的类似数组的集合类型。当不确定是应该使用数组还是 vector 的时候,你可能应该使用 vector:第八章会详细讨论 vector。

一个你可能想要使用数组而不是 vector 的例子是当程序需要知道一年中月份的名字时。程序不大可能回去增加或减少月份,这时你可以使用数组因为我们知道它总是含有 12 个元素:

let months = ["January", "February", "March", "April", "May", "June", "July",
               "August", "September", "October", "November", "December"];
@@ -1128,7 +1128,7 @@ thread '<main>' panicked at 'index out of bounds: the len is 5 but the ind
 note: Run with `RUST_BACKTRACE=1` for a backtrace.
 error: Process didn't exit successfully: `target/debug/arrays` (exit code: 101)
 
-

编译并没有产生任何错误,不过程序会导致一个运行时runtime)错误并且不会成功退出。当尝试用索引访问一个元素时,Rust 会检查指定的索引是否小于数组的长度。如果索引超出了数组长度,Rust 会panic,这是 Rust 中的术语,它用于程序因为错误而退出的情况。

+

编译并没有产生任何错误,不过程序会导致一个运行时runtime)错误并且不会成功退出。当尝试用索引访问一个元素时,Rust 会检查指定的索引是否小于数组的长度。如果索引超出了数组长度,Rust 会panic,这是 Rust 中的术语,它用于程序因为错误而退出的情况。

这是第一个在实战中遇到的 Rust 安全原则的例子。在很多底层语言中,并没有进行这类检查,这样当提供了一个不正确的索引时,就会访问无效的内存。Rust 通过立即退出而不是允许内存访问并继续执行来使你免受这类错误困扰。第九章会讨论更多 Rust 的错误处理。

函数如何工作

@@ -1160,7 +1160,7 @@ Another function.

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

函数参数

-

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

+

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

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

Filename: src/main.rs

fn main() {
@@ -1178,7 +1178,7 @@ fn another_function(x: i32) {
 The value of x is: 5
 

another_function的声明有一个叫做x的参数。x的类型被指定为i32。当5被传递给another_function时,println!宏将5放入格式化字符串中大括号的位置。

-

在函数签名中,必须声明每个参数的类型。这是 Rust 设计中一个经过慎重考虑的决定:要求在函数定义中提供类型注解意味着编译器再也不需要在别的地方要求你注明类型就能知道你的意图。

+

在函数签名中,必须声明每个参数的类型。这是 Rust 设计中一个经过慎重考虑的决定:要求在函数定义中提供类型注解意味着编译器再也不需要在别的地方要求你注明类型就能知道你的意图。

当一个函数有多个参数时,使用逗号隔开他们,像这样:

Filename: src/main.rs

fn main() {
@@ -1202,7 +1202,7 @@ The value of y is: 6
 

函数体

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

语句与表达式

-

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

+

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

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

Filename: src/main.rs @@ -1318,7 +1318,7 @@ help: consider removing this semicolon:
commit 74d6fc999b986b74bf94edd6dcbb5a08a16c12de

-

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

+

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

这是一个注释的例子:

// Hello, world.
 
@@ -1339,8 +1339,8 @@ commit 74d6fc999b986b74bf94edd6dcbb5a08a16c12de

// I’m feeling lucky today. let lucky_number = 7; } - -这就是注释的全部。并没有什么特别复杂的。
+
+

这就是注释的全部。并没有什么特别复杂的。

控制流

ch03-05-control-flow.md @@ -1378,7 +1378,7 @@ condition was true Running `target/debug/branches` condition was false -

另外值得注意的是代码中的条件必须bool。如果像看看条件不是bool值时会发生什么,尝试运行如下代码:

+

另外值得注意的是代码中的条件必须bool。如果像看看条件不是bool值时会发生什么,尝试运行如下代码:

Filename: src/main.rs

fn main() {
     let number = 3;
@@ -1490,7 +1490,7 @@ error[E0308]: if and else have incompatible types
 

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

使用循环重复执行

-

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

+

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

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

使用loop重复执行代码

loop关键字告诉 Rust 一遍又一遍的执行一段代码直到你明确要求停止。

@@ -1598,6 +1598,178 @@ the value is: 50
  • 打印圣诞颂歌“The Twelve Days of Christmas”的歌词,并利用歌曲中的重复部分(编写循环)
  • 当你准备好继续的时候,让我们讨论一个其他语言中并不常见的概念:所有权(ownership)。

    +

    认识所有权

    +
    +

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

    +
    +

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

    +

    什么是所有权

    +
    +

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

    +
    +

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

    +

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

    +

    因为所有权对很多程序员都是一个新概念,需要一些时间来适应。好消息是随着你对 Rust 和所有权系统的规则越来越有经验,你就越能自然地编写出安全和高效的代码。持之以恒!

    +

    当你理解了所有权系统,你就会对这个使 Rust 如此独特的功能有一个坚实的基础。在本章中,你将会通过一些例子来学习所有权,他们关注一个非常常见的数据结构:字符串。

    + +
    +

    栈(Stack)与堆(Heap)

    +

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

    +

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

    +

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

    +

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

    +

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

    +

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

    +

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

    +

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

    +
    + +

    所有权规则

    +

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

    +
    +
      +
    1. Rust 中的每一个值都有一个叫做它的所有者owner)的变量。
    2. +
    3. 同时一次只能有一个所有者
    4. +
    5. 当所有者变量离开作用域,这个值将被丢弃。
    6. +
    +
    +

    变量作用域

    +

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

    +

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

    +
    let s = "hello";
    +
    +

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

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

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

    +
    +
    +

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

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

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

    +

    String类型

    +

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

    +

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

    +

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

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

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

    +

    这类字符串可以被修改:

    +
    let mut s = String::from("hello");
    +
    +s.push_str(", world!"); // push_str() appends a literal to a String
    +
    +println!("{}", s); // This will print `hello, world!`
    +
    +

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

    +

    内存与分配

    +

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

    +

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

    +
      +
    1. 内存必须在运行时向操作系统请求
    2. +
    3. 需要一个当我们处理完String时将内存返回给操作系统的方法
    4. +
    +

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

    +

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

    +

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

    +
    {
    +    let s = String::from("hello"); // s is valid from this point forward
    +
    +    // do stuff with s
    +}                                  // this scope is now over, and s is no
    +                                   // longer valid
    +
    +

    这里是一个将String需要的内存返回给操作系统的很自然的位置:当s离开作用域的时候。当变量离开作用域,Rust 为其调用一个特殊的函数。这个函数叫做 drop,在这里String的作者可以放置释放内存的代码。Rust 在结尾的}处自动调用drop

    +
    +

    注意:在 C++ 中,这种 item 在生命周期结束时释放资源的方法有时被称作资源获取即初始化Resource Acquisition Is Initialization (RAII))。如果你使用过 RAII 模式的话应该对 Rust 的drop函数不陌生。

    +
    +

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

    +

    变量与数据交互:移动

    +

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

    +
    +
    let x = 5;
    +let y = x;
    +
    +
    +

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

    +
    +
    +

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

    +

    现在看看这个String版本:

    +
    let s1 = String::from("hello");
    +let s2 = s1;
    +
    +

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

    +

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

    +
    +String in memory +
    +

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

    +
    +
    +

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

    +

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

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

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

    +
    +
    +

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

    +
    +s1 and s2 to two places +
    +

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

    +
    +
    +

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

    +

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

    +
    let s1 = String::from("hello");
    +let s2 = s1;
    +
    +println!("{}", s1);
    +
    +

    你会得到一个类似如下的错误,因为 Rust 禁止你使用无效的引用。

    +
    error[E0382]: use of moved value: `s1`
    + --> src/main.rs:4:27
    +  |
    +3 |     let s2 = s1;
    +  |         -- value moved here
    +4 |     println!("{}, world!",s1);
    +  |                           ^^ value used here after move
    +  |
    +  = note: move occurs because `s1` has type `std::string::String`,
    +which does not implement the `Copy` trait
    +
    +

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

    +
    +s1 moved to s2 +
    +

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

    +
    +
    +

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

    +

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

    +

    变量与数据交互:克隆

    +

    如果我们确实需要深度复制String中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做clone

    +

    References & Borrowing

    +

    Slices

    diff --git a/src/SUMMARY.md b/src/SUMMARY.md index a045c03..ff115d6 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -13,4 +13,9 @@ - [数据类型](ch03-02-data-types.md) - [函数如何工作](ch03-03-how-functions-work.md) - [注释](ch03-04-comments.md) - - [控制流](ch03-05-control-flow.md) \ No newline at end of file + - [控制流](ch03-05-control-flow.md) + +- [认识所有权](ch04-00-understanding-ownership.md) + - [什么是所有权](ch04-01-what-is-ownership.md) + - [引用 & 借用](ch04-02-references-and-borrowing.md) + - [Slices](ch04-03-slices.md) \ No newline at end of file diff --git a/src/ch01-00-introduction.md b/src/ch01-00-introduction.md index 07a6e04..2101b8d 100644 --- a/src/ch01-00-introduction.md +++ b/src/ch01-00-introduction.md @@ -2,14 +2,16 @@ > [ch01-00-introduction.md](https://github.com/rust-lang/book/blob/master/src/ch01-00-introduction.md) >
    -> commit c6920d4a2ee0f282addaf8f6945cefe3ef7bdf09 +> commit c51c14215d2ee2cb481bc8a942a3769c6d9a2e1a -欢迎阅读“Rust 程序设计语言”。一本关于 Rust 的介绍性书籍。Rust 是一个关注安全、速度和并发的编程语言。它的设计可以使程序获得性能和对底层语言的控制,并享受高级语言强大的抽象能力。这些特性使得 Rust 适合那些有类似 C 语言经验并正在寻找一个更安全的替代者的程序员,同时也适合那些来自类似 Python 语言背景,正在探索在不牺牲表现力的情况下编写更好性能代码的人们。 +欢迎阅读“Rust 程序设计语言”,一本关于 Rust 的介绍性书籍。Rust 是一个着用于安全、速度和并发的编程语言。它的设计不仅可以使程序获得性能和对底层语言的控制,并且能够享受高级语言强大的抽象能力。这些特性使得 Rust 适合那些有类似 C 语言经验并正在寻找一个更安全的替代者的程序员,同时也适合那些来自类似 Python 语言背景,正在探索在不牺牲表现力的情况下编写更好性能代码的人们。 Rust 在编译时进行其绝大多数的安全检查和内存管理决策,因此程序的运行时性能没有受到影响。这让其在许多其他语言不擅长的应用场景中得以大显身手:有可预测空间和时间要求的程序,嵌入到其他语言中,以及编写底层代码,如设备驱动和操作系统。Rust 也很擅长 web 程序:它驱动着 Rust 包注册网站(package -registry site),crates.io!我们期待看到*你*使用 Rust 进行创作。 +registry site),[crates.io]!我们期待看到**你**使用 Rust 进行创作。 -本书的编排面向已经了解如何使用至少一门编程语言编程的读者。读完本书之后,你应该能自如的编写 Rust 程序。我们将通过小的,专注的并相互依赖的例子来学习 Rust,并向你展示如何使用 Rust 多样的功能,同时了解它们在后台是如何执行的。 +[crates.io]: https://crates.io/ + +本书是为已经至少了解一门编程语言的读者而写的。读完本书之后,你应该能自如的编写 Rust 程序。我们将通过小而集中并相互依赖的例子来学习 Rust,并向你展示如何使用 Rust 多样的功能,同时了解它们在后台是如何执行的。 ## 为本书做出贡献 diff --git a/src/ch01-02-hello-world.md b/src/ch01-02-hello-world.md index 104cf7b..a9147ab 100644 --- a/src/ch01-02-hello-world.md +++ b/src/ch01-02-hello-world.md @@ -10,7 +10,7 @@ ### 创建项目文件 -首先,创建一个文件来编写 Rust 代码。Rust 并不关心你的代码存放在哪里,不过在本书中,我们建议在你的 home 目录创建一个*项目*目录,并把你的所有项目放在这。打开一个终端并输入如下命令来为这个项目创建一个文件夹: +首先,创建一个文件来编写 Rust 代码。Rust 并不关心你的代码存放在哪里,不过在本书中,我们建议在你的 home 目录创建一个**项目**目录,并把你的所有项目放在这。打开一个终端并输入如下命令来为这个项目创建一个文件夹: Linux 和 Mac: @@ -76,9 +76,9 @@ fn main() { 这行代码做了这个小程序的所有工作:它在屏幕上打印文本。有很多需要注意的细节。第一个是 Rust 代码风格使用 4 个空格缩进,而不是 1 个制表符(tab)。 -第二个重要的部分是`println!()`。这叫做 Rust *宏*,是如何进行 Rust 元编程(metaprogramming)的关键所在。相反如果调用一个函数的话,它应该看起来像这样:`println`(没有`!`)。我们将在 24 章更加详细的讨论 Rust 宏,不过现在你只需记住当看到符号 `!` 的时候,就代表在调用一个宏而不是一个普通的函数。 +第二个重要的部分是`println!()`。这叫做 Rust **宏**,是如何进行 Rust 元编程(metaprogramming)的关键所在。相反如果调用一个函数的话,它应该看起来像这样:`println`(没有`!`)。我们将在 24 章更加详细的讨论 Rust 宏,不过现在你只需记住当看到符号 `!` 的时候,就代表在调用一个宏而不是一个普通的函数。 -接下来,`"Hello, world!"` 是一个 *字符串*。我们把这个字符串作为一个参数传递给`println!`,它负责在屏幕上打印这个字符串。轻松加愉快!(⊙o⊙) +接下来,`"Hello, world!"` 是一个 **字符串**。我们把这个字符串作为一个参数传递给`println!`,它负责在屏幕上打印这个字符串。轻松加愉快!(⊙o⊙) 这一行以一个分号结尾(`;`)。`;`代表这个表达式的结束和下一个表达式的开始。大部分 Rust 代码行以`;`结尾。 @@ -115,13 +115,13 @@ $ ./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),不过你只需要一句命令就可以编译和执行程序。这一切都是语言设计的权衡取舍。 +来自 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*)。 +Cargo 是 Rust 的构建系统和包管理工具,同时 Rustacean 们使用 Cargo 来管理它们的 Rust 项目,因为它使得很多任务变得更轻松。例如,Cargo负责构建代码、下载代码依赖的库并编译这些库。我们把代码需要的库叫做 **依赖**(*dependencies*)。 最简单的 Rust 程序,例如我们刚刚编写的,并没有任何依赖,所以目前我们只使用了 Cargo 负责构建代码的部分。随着你编写更加复杂的 Rust 程序,你会想要添加依赖,那么如果你使用 Cargo 开始的话,这将会变得简单许多。 @@ -156,7 +156,7 @@ $ cargo new hello_cargo --bin $ cd hello_cargo ``` -我们向`cargo new`传递了`--bin`因为我们的目标是生成一个可执行程序,而不是一个库。可执行文件是二进制可执行文件,通常就叫做 *二进制文件*(*binaries*)。项目的名称被定为`hello_cargo`,同时 Cargo 在一个同名(子)目录中创建它的文件,接着我们可以进入查看。 +我们向`cargo new`传递了`--bin`因为我们的目标是生成一个可执行程序,而不是一个库。可执行文件是二进制可执行文件,通常就叫做 **二进制文件**(*binaries*)。项目的名称被定为`hello_cargo`,同时 Cargo 在一个同名(子)目录中创建它的文件,接着我们可以进入查看。 如果列出 *hello_cargo* 目录中的文件,我们将会看到 Cargo 生成了两个文件和一个目录:一个 *Cargo.toml* 文件和一个 *src* 目录,*main.rs* 文件位于目录中。它也在 *hello_cargo* 目录初始化了一个 git 仓库,以及一个 *.gitignore* 文件;你可以改为使用不同的版本控制系统,或者不使用,通过`--vcs`参数。 diff --git a/src/ch02-00-guessing-game-tutorial.md b/src/ch02-00-guessing-game-tutorial.md index 32ff799..06a633b 100644 --- a/src/ch02-00-guessing-game-tutorial.md +++ b/src/ch02-00-guessing-game-tutorial.md @@ -10,7 +10,7 @@ ## 准备一个新项目 -要创建一个新项目,进入你在第一章创建的*项目*目录,并使用 Cargo 创建它,像这样: +要创建一个新项目,进入你在第一章创建的**项目**目录,并使用 Cargo 创建它,像这样: ```sh $ cargo new guessing_game --bin @@ -125,7 +125,7 @@ println!("Please input your guess."); let mut guess = String::new(); ``` -现在程序开始变得有意思了!这一小行代码发生了很多事。注意这是一个`let`语句,用来创建 *变量*。这里是另外一个例子: +现在程序开始变得有意思了!这一小行代码发生了很多事。注意这是一个`let`语句,用来创建**变量**。这里是另外一个例子: ```rust,ignore let foo = bar; @@ -144,7 +144,7 @@ let mut bar = 5; // mutable [string]: ../std/string/struct.String.html -`::new`那一行的`::`语法表明`new`是`String`类型的一个 *关联函数*(*associated function*)。关联函数是针对类型实现的,在这个例子中是`String`,而不是`String`的某个特定实例。一些语言中把它称为 *静态方法*(*static method*)。 +`::new`那一行的`::`语法表明`new`是`String`类型的一个**关联函数**(*associated function*)。关联函数是针对类型实现的,在这个例子中是`String`,而不是`String`的某个特定实例。一些语言中把它称为**静态方法**(*static method*)。 `new`函数创建了一个新的空的`String`,你会在很多类型上发现`new`函数,因为这是创建某个类型新值的常用函数名。 @@ -167,7 +167,7 @@ io::stdin().read_line(&mut guess) `read_line`的工作是把获取任何用户键入到标准输入的字符并放入一个字符串中,所以它获取字符串作为一个参数。这个字符串需要是可变的,这样这个方法就可以通过增加用户的输入来改变字符串的内容。 -`&`表明这个参数是一个 *引用*(*reference*),它提供了一个允许多个不同部分的代码访问同一份数据而不需要在内存中多次拷贝的方法。引用是一个复杂的功能,而 Rust 的一大优势就是它是安全而优雅操纵引用。完成这个程序并不需要知道这么多细节:第四章会更全面的解释引用。现在,我们只需知道它像变量一样,默认是不可变的。因此,需要写成`&mut guess`而不是`&guess`来使其可变。 +`&`表明这个参数是一个**引用**(*reference*),它提供了一个允许多个不同部分的代码访问同一份数据而不需要在内存中多次拷贝的方法。引用是一个复杂的功能,而 Rust 的一大优势就是它是安全而优雅操纵引用。完成这个程序并不需要知道这么多细节:第四章会更全面的解释引用。现在,我们只需知道它像变量一样,默认是不可变的。因此,需要写成`&mut guess`而不是`&guess`来使其可变。 这行代码还没有分析完。虽然这是单独一行代码,但它只是一个逻辑上代码行(虽然换行了但仍是一个语句)的第一部分。第二部分是这个方法: @@ -190,7 +190,7 @@ io::stdin().read_line(&mut guess).expect("Failed to read line"); [ioresult]: ../std/io/type.Result.html [result]: ../std/result/enum.Result.html -`Result`类型是 [*枚举*(*enumerations*)][enums],通常也写作 *enums*。枚举拥有固定值集合的类型,而这些值被称为枚举的 *成员*(*variants*)。第六章会更详细的介绍枚举。 +`Result`类型是 [*枚举*(*enumerations*)][enums],通常也写作 *enums*。枚举拥有固定值集合的类型,而这些值被称为枚举的**成员**(*variants*)。第六章会更详细的介绍枚举。 [enums]: ch06-00-enums.html @@ -256,7 +256,7 @@ You guessed: 6 ## 使用 crate 来增加更多功能 -记住 *crate* 是一个 Rust 代码的包。我们正在构建的项目是一个 *二进制 crate*,它生成一个可执行文件。 `rand` crate 是一个 *库 crate*,它包含意在被其他程序使用的代码。 +记住 *crate* 是一个 Rust 代码的包。我们正在构建的项目是一个**二进制 crate**,它生成一个可执行文件。 `rand` crate 是一个 *库 crate*,它包含意在被其他程序使用的代码。 Cargo 对外部 crate 的运用是其真正闪光的地方。在我们可以使用`rand`编写代码之前,需要编辑 *Cargo.toml* 来包含`rand`作为一个依赖。现在打开这个文件并在`[dependencies]`部分标题(Cargo 为你创建了它)的下面添加如下代码: @@ -319,7 +319,7 @@ Cargo 有一个机制来确保每次任何人重新构建代码都会生成相 #### 更新 crate 到一个新版本 -当你*确实*需要升级 crate 时,Cargo 提供了另一个命令,`update`,他会: +当你**确实**需要升级 crate 时,Cargo 提供了另一个命令,`update`,他会: 1. 忽略 *Cargo.lock* 文件并计算出所有符合 *Cargo.toml* 中规格的最新版本。 2. 如果成功了,Cargo 会把这些版本写入 *Cargo.lock* 文件。 @@ -351,7 +351,7 @@ rand = "0.4.0" ### 生成一个随机数 -让我们开始*使用*`rand`。下一步是更新 *src/main.rs*,如列表 2-3: +让我们开始**使用**`rand`。下一步是更新 *src/main.rs*,如列表 2-3:
    Filename: src/main.rs @@ -480,9 +480,9 @@ match guess.cmp(&secret_number) { [match]: ch06-02-match.html -一个`match`表达式由 *分支(arms)* 构成。一个分支包含一个 *模式*(*pattern*)和代码,这些代码在`match`表达式开头给出的值符合分支的模式时将被执行。Rust 获取提供给`match`的值并挨个检查每个分支的模式。`match`结构和模式是 Rust 中非常强大的功能,它帮助你体现代码可能遇到的多种情形并帮助你处理全部的可能。这些功能将分别在第六章和第十九章详细介绍。 +一个`match`表达式由 **分支(arms)** 构成。一个分支包含一个 **模式**(*pattern*)和代码,这些代码在`match`表达式开头给出的值符合分支的模式时将被执行。Rust 获取提供给`match`的值并挨个检查每个分支的模式。`match`结构和模式是 Rust 中非常强大的功能,它帮助你体现代码可能遇到的多种情形并帮助你处理全部的可能。这些功能将分别在第六章和第十九章详细介绍。 -让我们看看一个使用这里的`match`表达式会发生什么的例子。假设用户猜了 50,这时随机生成的秘密数字是 38。当代码比较 50 与 38 时,`cmp`方法会返回`Ordering::Greater`,因为 50 比 38 要大。`Ordering::Greater`是`match`表达式得到的值。它检查第一个分支的模式,`Ordering::Less`,不过值`Ordering::Greater`并不匹配`Ordering::Less`。所以它忽略了这个分支的代码并移动到下一个分支。下一个分支的模式,`Ordering::Greater`,*正确*匹配了`Ordering::Greater`!这个分支关联的代码会被执行并在屏幕打印出`Too big!`。`match`表达式就此终止,因为在这个特定场景下没有检查最后一个分支的必要。 +让我们看看一个使用这里的`match`表达式会发生什么的例子。假设用户猜了 50,这时随机生成的秘密数字是 38。当代码比较 50 与 38 时,`cmp`方法会返回`Ordering::Greater`,因为 50 比 38 要大。`Ordering::Greater`是`match`表达式得到的值。它检查第一个分支的模式,`Ordering::Less`,不过值`Ordering::Greater`并不匹配`Ordering::Less`。所以它忽略了这个分支的代码并移动到下一个分支。下一个分支的模式,`Ordering::Greater`,**正确**匹配了`Ordering::Greater`!这个分支关联的代码会被执行并在屏幕打印出`Too big!`。`match`表达式就此终止,因为在这个特定场景下没有检查最后一个分支的必要。 然而,列表 2-4 的代码并不能编译,尝试一下: @@ -502,7 +502,7 @@ error: aborting due to previous error Could not compile `guessing_game`. ``` -错误的核心表明这里有*不匹配的类型*(*mismatched types*)。Rust 拥有一个静态强类型系统。不过,它也有类型推断。当我们写出`let guess = String::new()`时,Rust 能够推断出`guess`应该是一个`String`,并不需要我们写出类型。另一方面,`secret_number`,是一个数字类型。一些数字类型拥有 1 到 100 之间的值:`i32`,一个 32 位的数字;`u32`,一个 32 位无符号数字;`i64`,一个 64 位数字;等等。Rust 默认使用`i32`,所以`secret_number`的类型就是它,除非增加类型信息或任何能让 Rust 推断出不同数值类型的信息。这里错误的原因是 Rust 不会比较字符串类型和数字类型。 +错误的核心表明这里有**不匹配的类型**(*mismatched types*)。Rust 拥有一个静态强类型系统。不过,它也有类型推断。当我们写出`let guess = String::new()`时,Rust 能够推断出`guess`应该是一个`String`,并不需要我们写出类型。另一方面,`secret_number`,是一个数字类型。一些数字类型拥有 1 到 100 之间的值:`i32`,一个 32 位的数字;`u32`,一个 32 位无符号数字;`i64`,一个 64 位数字;等等。Rust 默认使用`i32`,所以`secret_number`的类型就是它,除非增加类型信息或任何能让 Rust 推断出不同数值类型的信息。这里错误的原因是 Rust 不会比较字符串类型和数字类型。 最终我们想要把程序从输入中读取到的`String`转换为一个真正的数字类型,这样好与秘密数字向比较。可以通过在`main`函数体中增加如下两行代码来实现: @@ -549,7 +549,7 @@ let guess: u32 = guess.trim().parse() .expect("Please type a number!"); ``` -这里创建了一个叫做`guess`的变量。不过等等,难道这个程序不是已经有了一个叫做`guess`的变量了吗?确实如此,不过 Rust 允许我们通过 *覆盖*(*shadow*) 用一个新值来覆盖`guess`之前的值。这个功能经常用在类似需要把一个值从一种类型转换到另一种类型的场景。shadowing 允许我们复用`guess`变量的名字而不是强迫我们创建两个不同变量,比如`guess_str`和`guess`。(第三章会介绍 shadowing 的更多细节。) +这里创建了一个叫做`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`。 diff --git a/src/ch03-00-common-programming-concepts.md b/src/ch03-00-common-programming-concepts.md index 4611fc4..cc07368 100644 --- a/src/ch03-00-common-programming-concepts.md +++ b/src/ch03-00-common-programming-concepts.md @@ -12,6 +12,6 @@ > ### 关键字 > -> Rust 语言有一系列被保留为只能被语言使用的*关键字*(*keywords*),如大部分语言一样。注意你不能使用这些关键字作为变量或函数的名称。大部分关键字有特殊的意义,并将会被用来进行 Rust 程序中的多种任务;一些关键字目前没有相关的功能不过为了将来可能添加进 Rust 的功能而被保留。可以在附录 A 中找到一份关键字的列表 +> Rust 语言有一系列被保留为只能被语言使用的**关键字**(*keywords*),如大部分语言一样。注意你不能使用这些关键字作为变量或函数的名称。大部分关键字有特殊的意义,并将会被用来进行 Rust 程序中的多种任务;一些关键字目前没有相关的功能不过为了将来可能添加进 Rust 的功能而被保留。可以在附录 A 中找到一份关键字的列表 \ No newline at end of file diff --git a/src/ch03-01-variables-and-mutability.md b/src/ch03-01-variables-and-mutability.md index 810c0b3..c84b7d1 100644 --- a/src/ch03-01-variables-and-mutability.md +++ b/src/ch03-01-variables-and-mutability.md @@ -4,7 +4,7 @@ >
    > commit b0fab378c9c6a817d4f0080d7001d085017cdef8 -第二章中提到过,变量默认是*不可变*(*immutable*)的。这是 Rust 中许多鼓励以利用 Rust 提供的安全和简单并发优势编写代码的助力之一。不过,仍然有使变量可变的选项。让我们探索一下为什么以及如何鼓励你拥抱不可变性,还有为什么你可能想要弃之不用。 +第二章中提到过,变量默认是**不可变**(*immutable*)的。这是 Rust 中许多鼓励以利用 Rust 提供的安全和简单并发优势编写代码的助力之一。不过,仍然有使变量可变的选项。让我们探索一下为什么以及如何鼓励你拥抱不可变性,还有为什么你可能想要弃之不用。 当变量使不可变时,这意味着一旦一个值被绑定上了一个名称,你就不能改变这个值。作为说明,通过`cargo new --bin variables`在 *projects* 目录生成一个叫做 *variables* 的新项目。 @@ -36,9 +36,9 @@ error[E0384]: re-assignment of immutable variable `x` | ^^^^^ re-assignment of immutable variable ``` -这个例子显示了编译器如何帮助你寻找程序中的错误。即便编译器错误可能是令人沮丧的,他们也仅仅意味着程序不能安全的完成你想让它完成的工作;他们*不能*说明你不是一个好的程序员!有经验的 Rustacean 们也会遇到编译器错误。这些错误表明错误的原因是`对不可变变量重新赋值`(`re-assignment of immutable variable`),因为我们尝试对不可变变量`x`赋第二个值。 +这个例子显示了编译器如何帮助你寻找程序中的错误。即便编译器错误可能是令人沮丧的,他们也仅仅意味着程序不能安全的完成你想让它完成的工作;他们**不能**说明你不是一个好的程序员!有经验的 Rustacean 们也会遇到编译器错误。这些错误表明错误的原因是`对不可变变量重新赋值`(`re-assignment of immutable variable`),因为我们尝试对不可变变量`x`赋第二个值。 -当尝试去改变之前设计为不可变的值出现编译时错误是很重要的,因为这种情况可能导致 bug。如果代码的一部分假设一个值永远也不会改变而另一部分代码改变了它,这样第一部分代码就有可能不能像它设计的那样运行。你必须承认这种 bug 难以跟踪,尤其是当第二部分代码只是*有时*当变量使不可变时,这意味着一旦一个值被绑定上了一个名称,你就不能改变这个值。 +当尝试去改变之前设计为不可变的值出现编译时错误是很重要的,因为这种情况可能导致 bug。如果代码的一部分假设一个值永远也不会改变而另一部分代码改变了它,这样第一部分代码就有可能不能像它设计的那样运行。你必须承认这种 bug 难以跟踪,尤其是当第二部分代码只是**有时**当变量使不可变时,这意味着一旦一个值被绑定上了一个名称,你就不能改变这个值。 Rust 编译器保证如果声明一个值不会改变,它就真的不会改变。这意味着当阅读和编写代码时,并不需要记录如何以及在哪可能会被改变,这使得代码易于推导。 @@ -73,7 +73,7 @@ The value of x is: 6 ### 变量和常量的区别 -不能改变一个变量的值可能会使你想起另一个大部分编程语言都有的概念:*常量*(*constants*)。常量也是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。首先,不允许对常量使用`mut`:常量不光是默认不能改变,它总是不能改变。常量使用`const`关键字而不是`let`关键字声明,而且*必须*注明值的类型。现在我们准备在下一部分,“数据类型”,涉及到类型和类型注解,所以现在无需担心这些细节。常量可以在任何作用域声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。最后一个区别是常量只能用于常量表达式,而不能作为函数调用的结果或任何其他只在运行时使用到的值。 +不能改变一个变量的值可能会使你想起另一个大部分编程语言都有的概念:**常量**(*constants*)。常量也是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。首先,不允许对常量使用`mut`:常量不光是默认不能改变,它总是不能改变。常量使用`const`关键字而不是`let`关键字声明,而且*必须*注明值的类型。现在我们准备在下一部分,“数据类型”,涉及到类型和类型注解,所以现在无需担心这些细节。常量可以在任何作用域声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。最后一个区别是常量只能用于常量表达式,而不能作为函数调用的结果或任何其他只在运行时使用到的值。 这是一个常量声明的例子,它的名称是`MAX_POINTS`而它的值是 100,000。Rust 常量的命名规范是使用大写字母和单词间使用下划线: @@ -87,7 +87,7 @@ const MAX_POINTS: u32 = 100_000; ### 覆盖 -如第二章猜猜看游戏所讲到的,我们可以定义一个与之前变量名称相同的新变量,而新变量会*覆盖*之前的变量。Rustacean 们称其为第一个变量被第二个*给覆盖*了,这意味着第二个变量的值是使用这个变量时会看到的值。可以用相同变量名称来覆盖它自己以及重复使用`let`关键字来多次覆盖,如下所示: +如第二章猜猜看游戏所讲到的,我们可以定义一个与之前变量名称相同的新变量,而新变量会**覆盖**之前的变量。Rustacean 们称其为第一个变量被第二个**给覆盖**了,这意味着第二个变量的值是使用这个变量时会看到的值。可以用相同变量名称来覆盖它自己以及重复使用`let`关键字来多次覆盖,如下所示: Filename: src/main.rs diff --git a/src/ch03-02-data-types.md b/src/ch03-02-data-types.md index b445ec8..b5f24c5 100644 --- a/src/ch03-02-data-types.md +++ b/src/ch03-02-data-types.md @@ -4,9 +4,9 @@ >
    > commit d05b7c63ff50b3f9126bb5533e0ba5dd424b83d1 -Rust 中的任何值都有一个具体的*类型*(*type*),这告诉了 Rust 它被指定为何种数据这样 Rust 就知道如何处理这些数据了。这一部分将讲到一些语言内建的类型。我们将这些类型分为两个子集:标量(scalar)和复合(compound)。 +Rust 中的任何值都有一个具体的**类型**(*type*),这告诉了 Rust 它被指定为何种数据这样 Rust 就知道如何处理这些数据了。这一部分将讲到一些语言内建的类型。我们将这些类型分为两个子集:标量(scalar)和复合(compound)。 -贯穿整个部分,请记住 Rust 是一个*静态类型*(*statically typed*)语言,也就是说必须在编译时就知道所有变量的类型。编译器通常可以通过值以及如何使用他们来推断出我们想要用的类型。当多个类型都是可能的时候,比如第二章中`parse`将`String`转换为数字类型,必须增加类型注解,像这样: +贯穿整个部分,请记住 Rust 是一个**静态类型**(*statically typed*)语言,也就是说必须在编译时就知道所有变量的类型。编译器通常可以通过值以及如何使用他们来推断出我们想要用的类型。当多个类型都是可能的时候,比如第二章中`parse`将`String`转换为数字类型,必须增加类型注解,像这样: ```rust let guess: u32 = "42".parse().unwrap(); @@ -28,11 +28,11 @@ error[E0282]: unable to infer enough type information about `_` ### 标量类型 -*标量*类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型。你可能在其他语言中见过他们,不过让我们深入了解他们在 Rust 中时如何工作的。 +**标量**类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型。你可能在其他语言中见过他们,不过让我们深入了解他们在 Rust 中时如何工作的。 #### 整型 -*整数*是一个没有小数部分的数字。我们在这一章的前面使用过一个整型,`i32`类型。这个类型声明表明在 32 位系统上它关联的值应该是一个有符号整数(因为这个`i`,与`u`代表的无符号相对)。表格 3-1 展示了 Rust 内建的整数类型。每一个变体的有符号和无符号列(例如,*i32*)可以用来声明对应的整数值。 +**整数**是一个没有小数部分的数字。我们在这一章的前面使用过一个整型,`i32`类型。这个类型声明表明在 32 位系统上它关联的值应该是一个有符号整数(因为这个`i`,与`u`代表的无符号相对)。表格 3-1 展示了 Rust 内建的整数类型。每一个变体的有符号和无符号列(例如,*i32*)可以用来声明对应的整数值。
    @@ -80,7 +80,7 @@ Table 3-2: Integer Literals in Rust #### 浮点型 -Rust 也有两个主要的*浮点数*(*floating-point numbers*)类型,他们是有小数点的数字。Rust 的浮点数类型是`f32`和`f64`,分别是 32 位 和 64 位大小。默认类型是`f64`,因为它基本上与`f32`一样快不过精度更高。在 32 位系统上使用`f64`是可能的,不过会比`f32`要慢。大部分情况,牺牲潜在可能的更低性能来换取更高的精度是一个合理的首要选择,同时如果怀疑浮点数的大小有问题的时候应该对代码进行性能测试。 +Rust 也有两个主要的**浮点数**(*floating-point numbers*)类型,他们是有小数点的数字。Rust 的浮点数类型是`f32`和`f64`,分别是 32 位 和 64 位大小。默认类型是`f64`,因为它基本上与`f32`一样快不过精度更高。在 32 位系统上使用`f64`是可能的,不过会比`f32`要慢。大部分情况,牺牲潜在可能的更低性能来换取更高的精度是一个合理的首要选择,同时如果怀疑浮点数的大小有问题的时候应该对代码进行性能测试。 这是一个展示浮点数的实例: @@ -158,7 +158,7 @@ Rust 的`char`类型代表了一个 Unicode 变量值(Unicode Scalar Value) ### 复合类型 -*复合类型*可以将多个其他类型的值组合成一个类型。Rust 有两个原生的复合类型:元组(tuple)和数组(array)。 +**复合类型**可以将多个其他类型的值组合成一个类型。Rust 有两个原生的复合类型:元组(tuple)和数组(array)。 #### 将值组合进元组 @@ -210,7 +210,7 @@ fn main() { #### 数组 -另一个获取一个多个值集合的方式是*数组*(*array*)。与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,因为 Rust 中的数组是固定长度的:一旦声明,他们的长度不能增长或缩小。 +另一个获取一个多个值集合的方式是**数组**(*array*)。与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,因为 Rust 中的数组是固定长度的:一旦声明,他们的长度不能增长或缩小。 Rust 中数组的值位于中括号中的逗号分隔的列表中: @@ -222,7 +222,7 @@ fn main() { } ``` -数组在想要在栈(stack)而不是在堆(heap)上为数据分配空间时十分有用(第四章将讨论栈与堆的更多内容),或者是想要确保总是有固定数量的元素时。虽然它并不如 vector 类型那么灵活。vector 类型是标准库提供的一个*允许*增长和缩小长度的类似数组的集合类型。当不确定是应该使用数组还是 vector 的时候,你可能应该使用 vector:第八章会详细讨论 vector。 +数组在想要在栈(stack)而不是在堆(heap)上为数据分配空间时十分有用(第四章将讨论栈与堆的更多内容),或者是想要确保总是有固定数量的元素时。虽然它并不如 vector 类型那么灵活。vector 类型是标准库提供的一个**允许**增长和缩小长度的类似数组的集合类型。当不确定是应该使用数组还是 vector 的时候,你可能应该使用 vector:第八章会详细讨论 vector。 一个你可能想要使用数组而不是 vector 的例子是当程序需要知道一年中月份的名字时。程序不大可能回去增加或减少月份,这时你可以使用数组因为我们知道它总是含有 12 个元素: @@ -276,6 +276,6 @@ note: Run with `RUST_BACKTRACE=1` for a backtrace. error: Process didn't exit successfully: `target/debug/arrays` (exit code: 101) ``` -编译并没有产生任何错误,不过程序会导致一个*运行时*(*runtime*)错误并且不会成功退出。当尝试用索引访问一个元素时,Rust 会检查指定的索引是否小于数组的长度。如果索引超出了数组长度,Rust 会*panic*,这是 Rust 中的术语,它用于程序因为错误而退出的情况。 +编译并没有产生任何错误,不过程序会导致一个**运行时**(*runtime*)错误并且不会成功退出。当尝试用索引访问一个元素时,Rust 会检查指定的索引是否小于数组的长度。如果索引超出了数组长度,Rust 会*panic*,这是 Rust 中的术语,它用于程序因为错误而退出的情况。 这是第一个在实战中遇到的 Rust 安全原则的例子。在很多底层语言中,并没有进行这类检查,这样当提供了一个不正确的索引时,就会访问无效的内存。Rust 通过立即退出而不是允许内存访问并继续执行来使你免受这类错误困扰。第九章会讨论更多 Rust 的错误处理。 \ No newline at end of file diff --git a/src/ch03-03-how-functions-work.md b/src/ch03-03-how-functions-work.md index 6a25a22..f577fe1 100644 --- a/src/ch03-03-how-functions-work.md +++ b/src/ch03-03-how-functions-work.md @@ -40,7 +40,7 @@ Another function. ### 函数参数 -函数也可以被定义为拥有*参数*(*parameters*),他们是作为函数签名一部分的特殊变量。当函数拥有参数,可以为这些参数提供具体的值。技术上讲,这些具体值被称为参数( *arguments*),不过通常的习惯是倾向于在函数定义中的变量和调用函数时传递的具体值都可以用 "parameter" 和 "argument" 而不加区别。 +函数也可以被定义为拥有**参数**(*parameters*),他们是作为函数签名一部分的特殊变量。当函数拥有参数,可以为这些参数提供具体的值。技术上讲,这些具体值被称为参数( *arguments*),不过通常的习惯是倾向于在函数定义中的变量和调用函数时传递的具体值都可以用 "parameter" 和 "argument" 而不加区别。 如下被重写的`another_function`版本展示了 Rust 中参数是什么样的: @@ -68,7 +68,7 @@ The value of x is: 5 `another_function`的声明有一个叫做`x`的参数。`x`的类型被指定为`i32`。当`5`被传递给`another_function`时,`println!`宏将`5`放入格式化字符串中大括号的位置。 -在函数签名中,*必须*声明每个参数的类型。这是 Rust 设计中一个经过慎重考虑的决定:要求在函数定义中提供类型注解意味着编译器再也不需要在别的地方要求你注明类型就能知道你的意图。 +在函数签名中,**必须**声明每个参数的类型。这是 Rust 设计中一个经过慎重考虑的决定:要求在函数定义中提供类型注解意味着编译器再也不需要在别的地方要求你注明类型就能知道你的意图。 当一个函数有多个参数时,使用逗号隔开他们,像这样: @@ -106,7 +106,7 @@ The value of y is: 6 ### 语句与表达式 -我们已经用过语句与表达式了。*语句*(*Statements*)是执行一些操作但不返回值的指令。表达式(*Expressions*)计算并产生一个值。让我们看看一些例子: +我们已经用过语句与表达式了。**语句**(*Statements*)是执行一些操作但不返回值的指令。表达式(*Expressions*)计算并产生一个值。让我们看看一些例子: 使用`let`关键字创建变量并绑定一个值是一个语句。在列表 3-3 中,`let y = 6;`是一个语句: diff --git a/src/ch03-04-comments.md b/src/ch03-04-comments.md index ede2b44..601b3d8 100644 --- a/src/ch03-04-comments.md +++ b/src/ch03-04-comments.md @@ -4,7 +4,7 @@ >
    > commit 74d6fc999b986b74bf94edd6dcbb5a08a16c12de -所有编程语言都力求使他们的代码易于理解,不过有时额外的解释需要得到保障。在这种情况下,程序员在源码中留下记录,或者*注释*(*comments*),编译器会忽略他们不过其他阅读代码的人可能会用得上。 +所有编程语言都力求使他们的代码易于理解,不过有时额外的解释需要得到保障。在这种情况下,程序员在源码中留下记录,或者**注释**(*comments*),编译器会忽略他们不过其他阅读代码的人可能会用得上。 这是一个注释的例子: @@ -39,5 +39,6 @@ fn main() { // I’m feeling lucky today. let lucky_number = 7; } +``` 这就是注释的全部。并没有什么特别复杂的。 \ No newline at end of file diff --git a/src/ch03-05-control-flow.md b/src/ch03-05-control-flow.md index 44a6e5d..054e6ef 100644 --- a/src/ch03-05-control-flow.md +++ b/src/ch03-05-control-flow.md @@ -52,7 +52,7 @@ $ cargo run condition was false ``` -另外值得注意的是代码中的条件*必须*是`bool`。如果像看看条件不是`bool`值时会发生什么,尝试运行如下代码: +另外值得注意的是代码中的条件**必须**是`bool`。如果像看看条件不是`bool`值时会发生什么,尝试运行如下代码: Filename: src/main.rs @@ -206,7 +206,7 @@ error[E0308]: if and else have incompatible types ### 使用循环重复执行 -多次执行一段代码是很常用的。为了这个功能,Rust 提供了多种*循环*(*loops*)。一个循环执行循环体中的代码直到结尾并紧接着从回到开头继续执行。为了实验一下循环,让我们创建一个叫做 *loops* 的新项目。 +多次执行一段代码是很常用的。为了这个功能,Rust 提供了多种**循环**(*loops*)。一个循环执行循环体中的代码直到结尾并紧接着从回到开头继续执行。为了实验一下循环,让我们创建一个叫做 *loops* 的新项目。 Rust 有三种循环类型:`loop`、`while`和`for`。让我们每一个都试试。 diff --git a/src/ch04-00-understanding-ownership.md b/src/ch04-00-understanding-ownership.md new file mode 100644 index 0000000..89eaca9 --- /dev/null +++ b/src/ch04-00-understanding-ownership.md @@ -0,0 +1,7 @@ +# 认识所有权 + +> [ch04-00-understanding-ownership.md](https://github.com/rust-lang/book/blob/master/src/ch04-00-understanding-ownership.md) +>
    +> commit 759067b651a48a4a66485fe0876d318d398fb4fe + +所有权(系统)是 Rust 最独特的功能,它令 Rust 可以无需垃圾回收(garbage collector)就能保障内存安全。因此,理解 Rust 中所有权如何工作是十分重要的。本章我们将讲到所有权以及相关功能:借用、slices 以及 Rust 如何在内存中安排数据。 \ No newline at end of file diff --git a/src/ch04-01-what-is-ownership.md b/src/ch04-01-what-is-ownership.md new file mode 100644 index 0000000..4c9890b --- /dev/null +++ b/src/ch04-01-what-is-ownership.md @@ -0,0 +1,251 @@ +## 什么是所有权 + +> [ch04-01-what-is-ownership.md](https://github.com/rust-lang/book/blob/master/src/ch04-01-what-is-ownership.md) +>
    +> commit cc053d91f41793e54d5321abe027b0c163d735b8 + +Rust 的核心功能(之一)是**所有权**(*ownership*)。虽然这个功能理解起来很直观,不过它对语言的其余部分有着更深层的含义。 + +所有程序都必须管理他们运行时使用计算机内存的方式。一些语言中使用垃圾回收在程序运行过程中来时刻寻找不再被使用的内存;在另一些语言中,程序员必须亲自分配和释放内存。Rust 则选择了第三种方式:内存被一个所有权系统管理,它拥有一系列的规则使编译器在编译时进行检查。任何所有权系统的功能都不会导致运行时开销。 + +因为所有权对很多程序员都是一个新概念,需要一些时间来适应。好消息是随着你对 Rust 和所有权系统的规则越来越有经验,你就越能自然地编写出安全和高效的代码。持之以恒! + +当你理解了所有权系统,你就会对这个使 Rust 如此独特的功能有一个坚实的基础。在本章中,你将会通过一些例子来学习所有权,他们关注一个非常常见的数据结构:字符串。 + + + +> ### 栈(Stack)与堆(Heap) +> +> 在很多语言中并不经常需要考虑到栈与堆。不过在像 Rust 这样的系统变成语言中,值是位于栈上还是堆上在更大程度上影响了语言的行为以及为何必须做出特定的选择。我们会在本章的稍后部分描述所有权与堆与栈相关的部分,所以这里只是一个用来预热的简要解释。 +> +> 栈和堆都是代码在运行时可供使用的内存部分,不过他们以不同的结构组成。栈以放入值的顺序存储并以相反顺序取出值。这也被称作**后进先出**(*last in, first out*)。想象一下一叠盘子:当增加更多盘子时,把他们放在盘子堆的顶部,当需要盘子时,也从顶部拿走。不能从中间也不能从底部增加或拿走盘子!增加数据叫做**进栈**(*pushing onto the stack*),而移出数据叫做**出栈**(*popping off the stack*)。 +> +> 操作栈是非常快的,因为它访问数据的方式:永远也不需要寻找一个位置放入新数据或者取出数据因为这个位置总是在栈顶。另一个使得栈快速的性质是栈中的所有数据都必须是一个已知的固定的大小。 +> +> 相反对于在编译时未知大小或大小可能变化的数据,可以把他们储存在堆上。堆是缺乏组织的:当向堆放入数据时,我们请求一定大小的空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回给我们一个它位置的指针。这个过程称作**在堆上分配内存**(*allocating on the heap*),并且有时这个过程就简称为“分配”(allocating)。向栈中放入数据并不被认为是分配。因为指针是已知的固定大小的,我们可以将指针储存在栈上,不过当需要实际数据时,必须访问指针。 +> +> 想象一下去餐馆就坐吃饭。当进入时,你说明有几个人,餐馆员工会找到一个够大的空桌子并领你们过去。如果有人来迟了,他们也可以通过询问来找到你们坐在哪。 +> +> 访问堆上的数据要比访问栈上的数据要慢因为必须通过指针来访问。现代的处理器在内存中跳转越少就越快。继续类比,假设有一台服务器来处理来自多个桌子的订单。它在处理完一个桌子的所有订单后再移动到下一个桌子是最有效率的。从桌子 A 获取一个订单,接着再从桌子 B 获取一个订单,然后再从桌子 A,然后再从桌子 B 这样的流程会更加缓慢。出于同样原因,处理器在处理的数据之间彼此较近的时候(比如在栈上)比较远的时候(比如可能在堆上)能更好的工作。在堆上分配大量的空间也可能消耗时间。 +> +> 当调用一个函数,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。 +> +> 记录何处的代码在使用堆上的什么数据,最小化堆上的冗余数据的数量以及清理堆上不再使用的数据以致不至于用完空间,这些所有的问题正是所有权系统要处理的。一旦理解了所有权,你就不需要经常考虑栈和堆了,不过理解如何管理堆内存可以帮助我们理解所有权为什么存在以及为什么以它的方式工作。 + + + +### 所有权规则 + +首先,让我们看一下所有权的规则。记住这些规则正如我们将完成一些说明这些规则的例子: + +> 1. Rust 中的每一个值都有一个叫做它的**所有者**(*owner*)的变量。 +> 2. 同时一次只能有一个所有者 +> 3. 当所有者变量离开作用域,这个值将被丢弃。 + +### 变量作用域 + +我们在第二章已经完成过一个 Rust 程序的例子了。现在我们已经掌握了基本语法,所以不会在所有的例子中包含`fn main() {`代码了,所以如果你是一路跟过来的,必须手动将之后例子的代码放入一个`main`函数中。为此,例子将显得更加具体,使我们可以关注具体细节而不是样板代码。 + +作为所有权的第一个例子,我们看看一些变量的**作用域**(*scope*)。作用域是一个 item 在程序中有效的范围。假如有一个这样的变量: + +```rust +let s = "hello"; +``` + +变量`s`绑定到了一个字符串字面值,这个字符串值是硬编码进我们程序代码中的。这个变量从声明的点开始直到当前*作用域*结束时都是有效的。列表 4-1 的注释标明了变量`s`在哪里是有效的: + +
    + +```rust +{ // s is not valid here, it’s not yet declared + let s = "hello"; // s is valid from this point forward + + // do stuff with s +} // this scope is now over, and s is no longer valid +``` + +
    + +Listing 4-1: A variable and the scope in which it is valid + +
    +
    + +换句话说,这里有两个重要的点: + +1. 当`s`**进入作用域**,它就是有效的。 +2. 这一直持续到它**离开作用域**为止。 + +目前为止,变量是否有效与作用域的关系跟其他变成语言是类似的。现在我们要在此基础上介绍`String`类型。 + +### `String`类型 + +为了演示所有权的规则,我们需要一个比第三章讲到的任何一个都要复杂的数据类型。之前出现的数据类型都是储存在栈上的并且当离开作用域时被移出栈,不过我们需要寻找一个储存在堆上的数据来探索 Rust 如何知道该在何时清理数据。 + +这里使用`String`作为例子并专注于`String`与所有权相关的部分。这些方面也同样适用于其他标准库提供的或你创建的复杂数据类型。在第八章会更深入地讲解`String`。 + +我们已经见过字符串字面值了,它被硬编码进程序里。字符串字面值是很方便,不过他们并不总是适合所有需要使用文本的场景。原因之一就是他们是不可变的。另一个原因是不是所有字符串的值都能在编写代码时就知道:例如,如果想要获取用户输入并储存该怎么办呢?为此,Rust 有第二个字符串类型,`String`。这个类型储存在堆上所以储存在编译时未知大小的文本。可以用`from`从字符串字面值来创建`String`,如下: + +```rust +let s = String::from("hello"); +``` + +这两个冒号(`::`)运算符允许将特定的`from`函数置于`String`类型的命名空间(namespace)下而不需要使用类似`string_from`这样的名字。在第五章的“方法语法”(“Method Syntax”)部分会着重讲解这个语法而且在第七章会讲到模块的命名空间。 + +这类字符串*可以*被修改: + +```rust +let mut s = String::from("hello"); + +s.push_str(", world!"); // push_str() appends a literal to a String + +println!("{}", s); // This will print `hello, world!` +``` + +那么这里有什么区别呢?为什么`String`可变而字面值却不行呢?区别在于两个类型对内存的处理上。 + +### 内存与分配 + +字符串字面值的情况,我们在编译时就知道内容所以它直接被硬编码进最终的可执行文件中,这使得字符串字面值快速和高效。不过这些属性都只来源于它的不可变形。不幸的是,我们不能为了每一个在编译时未知大小的文本而将一块内存放入二进制文件中而它的大小还可能随着程序运行而改变。 + +对于`String`类型,为了支持一个可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容。这意味着: + +1. 内存必须在运行时向操作系统请求 +2. 需要一个当我们处理完`String`时将内存返回给操作系统的方法 + +第一部分由我们完成:当调用`String::from`时,它的实现请求它需要的内存。这在编程语言中是非常通用的。 + +然而,第二部分实现起来就各有区别了。在有**垃圾回收(GC)**的语言中, GC 记录并清除不再使用的内存,而我们作为程序员,并不需要关心他们。没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们程序员的责任了,正如请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要`allocate`和`free`一一对应。 + +Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。下面是列表 4-1 作用域例子的一个使用`String`而不是字符串字面值的版本: + +```rust +{ + let s = String::from("hello"); // s is valid from this point forward + + // do stuff with s +} // this scope is now over, and s is no + // longer valid +``` + +这里是一个将`String`需要的内存返回给操作系统的很自然的位置:当`s`离开作用域的时候。当变量离开作用域,Rust 为其调用一个特殊的函数。这个函数叫做 `drop`,在这里`String`的作者可以放置释放内存的代码。Rust 在结尾的`}`处自动调用`drop`。 + +> 注意:在 C++ 中,这种 item 在生命周期结束时释放资源的方法有时被称作**资源获取即初始化**(*Resource Acquisition Is Initialization (RAII)*)。如果你使用过 RAII 模式的话应该对 Rust 的`drop`函数不陌生。 + +这个模式对编写 Rust 代码的方式有着深远的影响。它现在看起来很简单,不过在更复杂的场景下代码的行为可能是不可预测的,比如当有多个变量使用在堆上分配的内存时。现在让我们探索一些这样的场景。 + +#### 变量与数据交互:移动 + +Rust 中的多个变量以一种独特的方式与同一数据交互。让我们看看列表 4-2 中一个使用整型的例子: + +
    + +```rust +let x = 5; +let y = x; +``` + +
    + +Listing 4-2: Assigning the integer value of variable `x` to `y` + +
    +
    + +根据其他语言的经验大致可以猜到这在干什么:“将`5`绑定到`x`;接着生成一个值`x`的拷贝并绑定到`y`”。现在有了两个变量,`x`和`y`,都等于`5`。这也正是事实上发生了的,因为正数是有已知固定大小的简单值,所以这两个`5`被放入了栈中。 + +现在看看这个`String`版本: + +```rust +let s1 = String::from("hello"); +let s2 = s1; +``` + +这看起来与上面的代码非常类似,所以我们可能会假设他们的运行方式也是类似的:也就是说,第二行可能会生成一个`s1`的拷贝并绑定到`s2`上。不过,事实上并不完全是这样。 + +为了更全面的解释这个问题,让我们看看图 4-3 中`String`真正是什么样。`String`由三部分组成,如图左侧所示:一个指向存放字符串内容内存的指针,一个长度,和一个容量。这一组数据储存在栈上。右侧则是堆上存放内容的内存部分。 + +
    +String in memory + +
    + +Figure 4-3: Representation in memory of a `String` holding the value `"hello"` +bound to `s1` + +
    +
    + +长度代表当前`String`的内容使用了多少字节的内存。容量是`String`从操作系统总共获取了多少字节的内存。长度与容量的区别是很重要的,不过目前为止的场景中并不重要,所以可以暂时忽略容量。 + +当我们把`s1`赋值给`s2`,`String`的数据被复制了,这意味着我们从栈上拷贝了它的指针、长度和容量。我们并没有复制堆上指针所指向的数据。换句话说,内存中数据的表现如图 4-4 所示。 + +
    +s1 and s2 pointing to the same value + +
    + +Figure 4-4: Representation in memory of the variable `s2` that has a copy of +the pointer, length, and capacity of `s1` + +
    +
    + +这个表现形式看起来**并不像**图 4-5 中的那样,它是如果 Rust 也拷贝了堆上的数据后内存看起来是怎么样的。如果 Rust 这么做了,那么操作`s2 = s1`在堆上数据比较大的时候可能会对运行时性能造成非常大的影响。 + +
    +s1 and s2 to two places + +
    + +Figure 4-5: Another possibility of what `s2 = s1` might do if Rust copied the +heap data as well + +
    +
    + +之前,我们提到过当变量离开作用域后 Rust 自动调用`drop`函数并清理变量的堆内存。不过图 4-4 展示了两个数据指针指向了同一位置。这就有了一个问题:当`s2`和`s1`离开作用域,他们都会尝试释放相同的内存。这是一个叫做 *double free* 的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致安全漏洞。 + +为了确保内存安全,这种场景下 Rust 的处理有另一个细节值得注意。与其尝试拷贝被分配的内存,Rust 则认为`s1`不再有效,因此 Rust 不需要在`s1`离开作用域后清理任何东西。看看在`s2`被创建之后尝试使用`s1`会发生生么: + +```rust,ignore +let s1 = String::from("hello"); +let s2 = s1; + +println!("{}", s1); +``` + +你会得到一个类似如下的错误,因为 Rust 禁止你使用无效的引用。 + +```sh +error[E0382]: use of moved value: `s1` + --> src/main.rs:4:27 + | +3 | let s2 = s1; + | -- value moved here +4 | println!("{}, world!",s1); + | ^^ value used here after move + | + = note: move occurs because `s1` has type `std::string::String`, +which does not implement the `Copy` trait +``` + +如果你在其他语言中听说过术语“浅拷贝”(“shallow copy”)和“深拷贝”(“deep copy”),那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效化了,这个操作被成为**移动**(*move*),而不是浅拷贝。上面的例子可以解读为`s1`被**移动**到了`s2`中。那么具体发生了什么如图 4-6 所示。 + +
    +s1 moved to s2 + +
    + +Figure 4-6: Representation in memory after `s1` has been invalidated + +
    +
    + +这样就解决了我们的麻烦!因为只有`s2`是有效的,当其离开作用域,它就释放自己的内存,完毕。 + +另外,这里还隐含了一个设计选择:Rust 永远也不会自动创建数据的“深拷贝”。因此,任何**自动**的复制可以被认为对运行时性能影响较小。 + +#### 变量与数据交互:克隆 + +如果我们**确实**需要深度复制`String`中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做`clone` \ No newline at end of file diff --git a/src/ch04-02-references-and-borrowing.md b/src/ch04-02-references-and-borrowing.md new file mode 100644 index 0000000..5f0552e --- /dev/null +++ b/src/ch04-02-references-and-borrowing.md @@ -0,0 +1 @@ +# References & Borrowing diff --git a/src/ch04-03-slices.md b/src/ch04-03-slices.md new file mode 100644 index 0000000..26d2b55 --- /dev/null +++ b/src/ch04-03-slices.md @@ -0,0 +1 @@ +# Slices diff --git a/src/img/trpl04-01.svg b/src/img/trpl04-01.svg new file mode 100644 index 0000000..7f5ee8d --- /dev/null +++ b/src/img/trpl04-01.svg @@ -0,0 +1,65 @@ + + + + + + +%3 + + +table0 + +s1 + +name + +value + +ptr + + +len + +5 + +capacity + +5 + + +table1 + +index + +value + +0 + +h + +1 + +e + +2 + +l + +3 + +l + +4 + +o + + +table0:pointer:c->table1:pointee + + + + + diff --git a/src/img/trpl04-02.svg b/src/img/trpl04-02.svg new file mode 100644 index 0000000..7d3a29a --- /dev/null +++ b/src/img/trpl04-02.svg @@ -0,0 +1,90 @@ + + + + + + +%3 + + +table0 + +s1 + +name + +value + +ptr + + +len + +5 + +capacity + +5 + + +table1 + +index + +value + +0 + +h + +1 + +e + +2 + +l + +3 + +l + +4 + +o + + +table0:pointer:c->table1:pointee + + + + +table3 + +s2 + +name + +value + +ptr + + +len + +5 + +capacity + +5 + + +table3:pointer:c->table1:pointee + + + + + diff --git a/src/img/trpl04-03.svg b/src/img/trpl04-03.svg new file mode 100644 index 0000000..a606851 --- /dev/null +++ b/src/img/trpl04-03.svg @@ -0,0 +1,117 @@ + + + + + + +%3 + + +table0 + +s2 + +name + +value + +ptr + + +len + +5 + +capacity + +5 + + +table1 + +index + +value + +0 + +h + +1 + +e + +2 + +l + +3 + +l + +4 + +o + + +table0:pointer:c->table1:pointee + + + + +table3 + +s1 + +name + +value + +ptr + + +len + +5 + +capacity + +5 + + +table4 + +index + +value + +0 + +h + +1 + +e + +2 + +l + +3 + +l + +4 + +o + + +table3:pointer:c->table4:pointee + + + + + diff --git a/src/img/trpl04-04.svg b/src/img/trpl04-04.svg new file mode 100644 index 0000000..1a17b27 --- /dev/null +++ b/src/img/trpl04-04.svg @@ -0,0 +1,91 @@ + + + + + + +%3 + + +table0 + + +s1 + +name + +value + +ptr + + +len + +5 + +capacity + +5 + + +table1 + +index + +value + +0 + +h + +1 + +e + +2 + +l + +3 + +l + +4 + +o + + +table0:pointer:c->table1:pointee + + + + +table3 + +s2 + +name + +value + +ptr + + +len + +5 + +capacity + +5 + + +table3:pointer:c->table1:pointee + + + + + diff --git a/src/img/trpl04-05.svg b/src/img/trpl04-05.svg new file mode 100644 index 0000000..33e5b49 --- /dev/null +++ b/src/img/trpl04-05.svg @@ -0,0 +1,82 @@ + + + + + + +%3 + + +table0 + +s + +name + +value + +ptr + + + +table1 + +s1 + +name + +value + +ptr + + +len + +5 + +capacity + +5 + + +table0:borrower:c->table1:borrowee + + + + +table2 + +index + +value + +0 + +h + +1 + +e + +2 + +l + +3 + +l + +4 + +o + + +table1:pointer:c->table2:pointee + + + + + diff --git a/src/img/trpl04-06.svg b/src/img/trpl04-06.svg new file mode 100644 index 0000000..1a87522 --- /dev/null +++ b/src/img/trpl04-06.svg @@ -0,0 +1,110 @@ + + + + + + +%3 + + +table0 + +world + +name + +value + +ptr + + +len + +5 + + +table4 + +index + +value + +0 + +h + +1 + +e + +2 + +l + +3 + +l + +4 + +o + +5 + + + +6 + +w + +7 + +o + +8 + +r + +9 + +l + +10 + +d + + +table0:pointer2:c->table4:pointee2 + + + + +table3 + +s + +name + +value + +ptr + + +len + +11 + +capacity + +11 + + +table3:pointer:c->table4:pointee + + + + +