mirror of
https://github.com/KaiserY/trpl-zh-cn
synced 2025-02-22 20:22:18 +08:00
Merge branch 'master' of https://github.com/KaiserY/trpl-zh-cn
This commit is contained in:
commit
64e0ccb707
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-2018 KaiserY
|
||||
Copyright (c) 2017-2018 Rust 中文社区
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
@ -4,7 +4,7 @@
|
||||
> <br>
|
||||
> commit 7480e811ab5ad8d53a5b854d9b0c7a5a4f58499f
|
||||
|
||||
让我们一起动手完成一个项目,来快速上手 Rust!本章将介绍 Rust 中常用的一些概念,并通过真实的程序来展示如何运用它们。你将会学到更多诸如 `let`、`match`、方法、关联函数、外部 crate 等很多的知识!后继章节会深入探索这些概念的细节。在这一章,我们将练习基础。
|
||||
让我们一起动手完成一个项目,来快速上手 Rust!本章将介绍 Rust 中一些常用概念,并通过真实的程序来展示如何运用它们。你将会学到 `let`、`match`、方法、关联函数、外部 crate 等知识!后续章节会深入探讨这些概念的细节。在这一章,我们将做基础练习。
|
||||
|
||||
我们会实现一个经典的新手编程问题:猜猜看游戏。它是这么工作的:程序将会随机生成一个 1 到 100 之间的随机整数。接着它会请玩家猜一个数并输入,然后提示猜测是大了还是小了。如果猜对了,它会打印祝贺信息并退出。
|
||||
|
||||
@ -44,22 +44,23 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
现在编译 “Hello, world!” 程序,使用 `cargo run` 编译运行一步到位:
|
||||
现在使用 `cargo run` 命令,一步完成 “Hello, world!” 程序的编译和运行:
|
||||
|
||||
```text
|
||||
$ cargo run
|
||||
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
|
||||
Finished dev [unoptimized + debuginfo] target(s) in 1.50 secs
|
||||
Running `target/debug/guessing_game`
|
||||
Hello, world!
|
||||
```
|
||||
|
||||
`run` 命令适合用于需要快速迭代的项目,而这个游戏便是这样的项目:我们需要在下一步迭代之前快速测试每一步。
|
||||
当你需要在项目中快速迭代时,`run` 命令就能派上用场,正如我们在这个游戏项目中做的,在下一次迭代之前快速测试每一次迭代。
|
||||
|
||||
重新打开 *src/main.rs* 文件。我们将会在这个文件中编写全部的代码。
|
||||
|
||||
## 处理一次猜测
|
||||
|
||||
程序的第一部分请求和处理用户输入,并检查输入是否符合预期的格式。首先,允许用户输入猜测。在 *src/main.rs* 中输入示例 2-1 中的代码。
|
||||
猜猜看程序的第一部分请求和处理用户输入,并检查输入是否符合预期的格式。首先,允许玩家输入猜测。在 *src/main.rs* 中输入示例 2-1 中的代码。
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -82,13 +83,13 @@ fn main() {
|
||||
|
||||
<span class="caption">示例 2-1:获取用户猜测并打印的代码</span>
|
||||
|
||||
这些代码包含很多信息,我们一行一行地过一遍。为了获取用户输入并打印结果作为输出,我们需要将 `io`(输入/输出)库引入当前作用域。`io` 库来自于标准库(也被称为`std`):
|
||||
这些代码包含很多信息,我们一行一行地过一遍。为了获取用户输入并打印结果作为输出,我们需要将 `io`(输入/输出)库引入当前作用域。`io` 库来自于标准库(也被称为 `std`):
|
||||
|
||||
```rust,ignore
|
||||
use std::io;
|
||||
```
|
||||
|
||||
Rust 默认只在每个程序的 [*prelude*][prelude]<!-- ignore --> 中引入少量类型。如果需要的类型不在 prelude 中,你必须使用一个 `use` 语句显式地将其引入作用域。`std::io` 库提供很多 `io` 相关的功能,比如接受用户输入的功能。
|
||||
默认情况下,Rust 将 [*prelude*][prelude]<!-- ignore --> 模块中少量的类型引入到每个程序的作用域中。如果需要的类型不在 prelude 中,你必须使用 `use` 语句显式地将其引入作用域。`std::io` 库提供很多有用的功能,包括接收用户输入的功能。
|
||||
|
||||
[prelude]: https://doc.rust-lang.org/std/prelude/index.html
|
||||
|
||||
@ -112,7 +113,7 @@ println!("Please input your guess.");
|
||||
|
||||
### 使用变量储存值
|
||||
|
||||
接下来,创建一个地方储存用户输入,像这样:
|
||||
接下来,创建一个储存用户输入的地方,像这样:
|
||||
|
||||
```rust,ignore
|
||||
let mut guess = String::new();
|
||||
@ -131,7 +132,7 @@ let foo = 5; // immutable
|
||||
let mut bar = 5; // mutable
|
||||
```
|
||||
|
||||
> 注意:`//` 语法开始一个持续到本行的结尾的注释。Rust 忽略注释中的所有内容。
|
||||
> 注意:`//` 语法开始一个注释,持续到行尾。Rust 忽略注释中的所有内容,将在第三章中详细介绍注释。
|
||||
|
||||
让我们回到猜猜看程序中。现在我们知道了 `let mut guess` 会引入一个叫做 `guess` 的可变变量。等号(`=`)的右边是 `guess` 所绑定的值,它是 `String::new` 的结果,这个函数会返回一个 `String` 的新实例。[`String`][string]<!-- ignore --> 是一个标准库提供的字符串类型,它是 UTF-8 编码的可增长文本块。
|
||||
|
||||
@ -139,7 +140,7 @@ let mut bar = 5; // mutable
|
||||
|
||||
`::new` 那一行的 `::` 语法表明 `new` 是 `String` 类型的一个 **关联函数**(*associated function*)。关联函数是针对类型实现的,在这个例子中是 `String`,而不是 `String` 的某个特定实例。一些语言中把它称为 **静态方法**(*static method*)。
|
||||
|
||||
`new` 函数创建了一个新的空 `String`,你会在很多类型上发现 `new` 函数,这是创建类型实例的惯用函数名。
|
||||
`new` 函数创建了一个新的空字符串,你会发现很多类型上有 `new` 函数,因为它是创建类型实例的惯用函数名。
|
||||
|
||||
总结一下,`let mut guess = String::new();` 这一行创建了一个可变变量,当前它绑定到一个新的 `String` 空实例上。
|
||||
|
||||
@ -160,7 +161,7 @@ io::stdin().read_line(&mut guess)
|
||||
|
||||
`read_line` 的工作是,无论用户在标准输入中键入什么内容,都将其存入一个字符串中,因此它需要字符串作为参数。这个字符串参数应该是可变的,以便 `read_line` 将用户输入附加上去。
|
||||
|
||||
`&` 表示这个参数是一个 **引用**(*reference*),它允许多处代码访问同一处数据,而无需在内存中多次拷贝。引用是一个复杂的特性,Rust 的一个主要优势就是安全而简单的操纵引用。完成当前程序并不需要了解如此多细节。现在,我们只需知道它像变量一样,默认是不可变的,需要写成 `&mut guess` 而不是 `&guess` 来使其可变。(第四章会更全面的解释引用。)
|
||||
`&` 表示这个参数是一个 **引用**(*reference*),它允许多处代码访问同一处数据,而无需在内存中多次拷贝。引用是一个复杂的特性,Rust 的一个主要优势就是安全而简单的操纵引用。完成当前程序并不需要了解如此多细节。现在,我们只需知道它像变量一样,默认是不可变的。因此,需要写成 `&mut guess` 来使其可变,而不是 `&guess`。(第四章会更全面的解释引用。)
|
||||
|
||||
### 使用 `Result` 类型来处理潜在的错误
|
||||
|
||||
@ -170,13 +171,13 @@ io::stdin().read_line(&mut guess)
|
||||
.expect("Failed to read line");
|
||||
```
|
||||
|
||||
当使用 `.foo()` 语法调用方法时,通过换行并缩进来把长行拆开是明智的。我们完全可以这样写:
|
||||
当使用 `.foo()` 语法调用方法时,通过换行加缩进来把长行拆开是明智的。我们完全可以这样写:
|
||||
|
||||
```rust,ignore
|
||||
io::stdin().read_line(&mut guess).expect("Failed to read line");
|
||||
```
|
||||
|
||||
不过,过长的行难以阅读,所以最好拆开来写,两行代码两个方法调用。现在来看看这行代码干了什么。
|
||||
不过,过长的行难以阅读,所以最好拆开来写,两个方法调用占两行。现在来看看这行代码干了什么。
|
||||
|
||||
之前提到了 `read_line` 将用户输入附加到传递给它的字符串中,不过它也返回一个值——在这个例子中是 [`io::Result`][ioresult]<!-- ignore -->。Rust 标准库中有很多叫做 `Result` 的类型。一个 [`Result`][result]<!-- ignore --> 泛型以及对应子模块的特定版本,比如 `io::Result`。
|
||||
|
||||
@ -187,9 +188,9 @@ io::stdin().read_line(&mut guess).expect("Failed to read line");
|
||||
|
||||
[enums]: ch06-00-enums.html
|
||||
|
||||
对于 `Result`,它的成员是 `Ok` 或 `Err`,`Ok` 成员表示操作成功,内部包含成功时产生的值。`Err` 成员则意味着操作失败,并且包含失败的前因后果。
|
||||
`Result` 的成员是 `Ok` 和 `Err`,`Ok` 成员表示操作成功,内部包含成功时产生的值。`Err` 成员则意味着操作失败,并且包含失败的前因后果。
|
||||
|
||||
这些 `Result` 类型的作用是编码错误处理信息。`Result` 类型的值,像其他类型一样,拥有定义于其上的方法。`io::Result` 的实例拥有 [`expect` 方法][expect]<!-- ignore -->。如果 `io::Result` 实例的值是 `Err`,`expect` 会导致程序崩溃,并显示当做参数传递给 `expect` 的信息。如果 `read_line` 方法返回 `Err`,则可能是来源于底层操作系统错误的结果。如果 `io::Result` 实例的值是 `Ok`,`expect` 会获取 `Ok` 中的值并原样返回。在本例中,这个值是用户输入到标准输入中的字节的数量。
|
||||
这些 `Result` 类型的作用是编码错误处理信息。`Result` 类型的值,像其他类型一样,拥有定义于其上的方法。`io::Result` 的实例拥有 [`expect` 方法][expect]<!-- ignore -->。如果 `io::Result` 实例的值是 `Err`,`expect` 会导致程序崩溃,并显示当做参数传递给 `expect` 的信息。如果 `read_line` 方法返回 `Err`,则可能是来源于底层操作系统错误的结果。如果 `io::Result` 实例的值是 `Ok`,`expect` 会获取 `Ok` 中的值并原样返回。在本例中,这个值是用户输入到标准输入中的字节数。
|
||||
|
||||
[expect]: https://doc.rust-lang.org/std/result/enum.Result.html#method.expect
|
||||
|
||||
@ -255,9 +256,9 @@ You guessed: 6
|
||||
|
||||
### 使用 crate 来增加更多功能
|
||||
|
||||
记住 *crate* 是一个 Rust 代码的包。我们正在构建的项目是一个 **二进制 crate**,它生成一个可执行文件。 `rand` crate 是一个 **库 crate**,库 crate 可以包含任意能被其他程序使用的代码。
|
||||
记住,*crate* 是一个 Rust 代码包。我们正在构建的项目是一个 **二进制 crate**,它生成一个可执行文件。 `rand` crate 是一个 **库 crate**,库 crate 可以包含任意能被其他程序使用的代码。
|
||||
|
||||
Cargo 对外部 crate 的运用是其真正闪光的地方。在我们使用 `rand` 编写代码之前,需要编辑 *Cargo.toml* ,声明 `rand` 作为一个依赖。现在打开这个文件并在底部的 `[dependencies]` 部分标题之下添加:
|
||||
Cargo 对外部 crate 的运用是其真正闪光的地方。在我们使用 `rand` 编写代码之前,需要修改 *Cargo.toml* 文件,引入一个 `rand` 依赖。现在打开这个文件并在底部的 `[dependencies]` 片段标题之下添加:
|
||||
|
||||
<span class="filename">文件名: Cargo.toml</span>
|
||||
|
||||
@ -267,7 +268,7 @@ Cargo 对外部 crate 的运用是其真正闪光的地方。在我们使用 `ra
|
||||
rand = "0.3.14"
|
||||
```
|
||||
|
||||
在 *Cargo.toml* 文件中,标题以及之后的内容属同一个部分,直到遇到下一个标题才开始新的部分。`[dependencies]` 部分告诉 Cargo 本项目依赖了哪些外部 crate 及其版本。本例中,我们使用语义化版本 `0.3.14` 来指定 `rand` crate。Cargo 理解[语义化版本(Semantic Versioning)][semver]<!-- ignore -->(有时也称为 *SemVer*),这是一种定义版本号的标准。`0.3.14` 事实上是 `^0.3.14` 的简写,它表示 “任何与 0.3.14 版本公有 API 相兼容的版本”。
|
||||
在 *Cargo.toml* 文件中,标题以及之后的内容属同一个片段,直到遇到下一个标题才开始新的片段。`[dependencies]` 片段告诉 Cargo 本项目依赖了哪些外部 crate 及其版本。本例中,我们使用语义化版本 `0.3.14` 来指定 `rand` crate。Cargo 理解[语义化版本(Semantic Versioning)][semver]<!-- ignore -->(有时也称为 *SemVer*),这是一种定义版本号的标准。`0.3.14` 事实上是 `^0.3.14` 的简写,它表示 “任何与 0.3.14 版本公有 API 相兼容的版本”。
|
||||
|
||||
[semver]: http://semver.org
|
||||
|
||||
@ -284,7 +285,7 @@ $ cargo build
|
||||
Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs
|
||||
```
|
||||
|
||||
<span class="caption">示例 2-2: 增加 rand crate 作为依赖之后运行 `cargo build` 的输出</span>
|
||||
<span class="caption">示例 2-2: 将 rand crate 添加为依赖之后运行 `cargo build` 的输出</span>
|
||||
|
||||
可能会出现不同的版本号(多亏了语义化版本,它们与代码是兼容的!),同时显示顺序也可能会有所不同。
|
||||
|
||||
@ -292,7 +293,7 @@ $ cargo build
|
||||
|
||||
[cratesio]: https://crates.io
|
||||
|
||||
在更新完 registry 后,Cargo 检查 `[dependencies]` 段落并下载缺失的 crate 。本例中,虽然只声明了 `rand` 一个依赖,然而 Cargo 还是额外获取了 `libc` 的拷贝,因为 `rand` 依赖 `libc` 来正常工作。下载完成后,Rust 编译依赖,然后使用这些依赖编译项目。
|
||||
在更新完 registry 后,Cargo 检查 `[dependencies]` 片段并下载缺失的 crate 。本例中,虽然只声明了 `rand` 一个依赖,然而 Cargo 还是额外获取了 `libc` 的拷贝,因为 `rand` 依赖 `libc` 来正常工作。下载完成后,Rust 编译依赖,然后使用这些依赖编译项目。
|
||||
|
||||
如果不做任何修改,立刻再次运行 `cargo build`,则不会看到任何除了 `Finished` 完成提示之外的输出。Cargo 知道它已经下载并编译了依赖,同时 *Cargo.toml* 文件也没有变动。Cargo 还知道代码也没有任何修改,所以它不会重新编译代码。因为无事可做,它简单的退出了。
|
||||
|
||||
@ -308,9 +309,9 @@ $ cargo build
|
||||
|
||||
#### *Cargo.lock* 文件确保构建是可重现的
|
||||
|
||||
Cargo 有一个机制来确保任何人在任何时候重新构建代码,都会产生相同的结果:Cargo 只会使用你指定的依赖的版本,除非你又手动指定了别的。例如,如果下周 `rand` crate 的 `v0.3.15` 版本出来了,它修复了一个重要的 bug,同时也含有一个会破坏代码运行的缺陷,这时会发生什么呢?
|
||||
Cargo 有一个机制来确保任何人在任何时候重新构建代码,都会产生相同的结果:Cargo 只会使用你指定的依赖版本,除非你又手动指定了别的。例如,如果下周 `rand` crate 的 `0.3.15` 版本出来了,它修复了一个重要的 bug,同时也含有一个会破坏代码运行的缺陷,这时会发生什么呢?
|
||||
|
||||
这个问题的答案是 *Cargo.lock* 文件。它在第一次运行 `cargo build` 时创建,并放在 *guessing_game* 目录。当第一次构建项目时,Cargo 计算出所有符合要求的依赖版本并写入 *Cargo.lock* 文件。当将来构建项目时,Cargo 会发现 *Cargo.lock* 存在并使用其中指定的版本,而不是再次计算所有的版本。这使得你拥有了一个自动化的可重现的构建。换句话说,项目会持续使用 `0.3.14` 直到你显式升级,感谢 *Cargo.lock* 文件。
|
||||
这个问题的答案是 *Cargo.lock* 文件。它在第一次运行 `cargo build` 时创建,并放在 *guessing_game* 目录。当第一次构建项目时,Cargo 计算出所有符合要求的依赖版本并写入 *Cargo.lock* 文件。当将来构建项目时,Cargo 会发现 *Cargo.lock* 已存在并使用其中指定的版本,而不是再次计算所有的版本。这使得你拥有了一个自动化的可重现的构建。换句话说,项目会持续使用 `0.3.14` 直到你显式升级,感谢 *Cargo.lock* 文件。
|
||||
|
||||
#### 更新 crate 到一个新版本
|
||||
|
||||
@ -324,7 +325,7 @@ $ cargo update
|
||||
Updating rand v0.3.14 -> v0.3.15
|
||||
```
|
||||
|
||||
这时,你也会注意到的 *Cargo.lock* 文件中的变化无外乎 `rand` crate 现在使用的版本是`0.3.15`
|
||||
这时,你也会注意到的 *Cargo.lock* 文件中的变化无外乎现在使用的 `rand` crate 版本是`0.3.15`
|
||||
|
||||
如果想要使用 `0.4.0` 版本的 `rand` 或是任何 `0.4.x` 系列的版本,必须像这样更新 *Cargo.toml* 文件:
|
||||
|
||||
@ -371,11 +372,11 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 2-3:为了生成随机数而做的修改</span>
|
||||
<span class="caption">示例 2-3:添加生成随机数的代码</span>
|
||||
|
||||
首先,这里在顶部增加一行 `extern crate rand;` 通知 Rust 我们要使用外部依赖。这也会调用相应的 `use rand`,所以现在可以使用 `rand::` 前缀来调用 `rand` crate 中的任何内容。
|
||||
首先,这里在顶部增加一行 `extern crate rand;` 通知 Rust 我们要使用外部依赖 `rand`。这也会调用相应的 `use rand`,所以现在可以使用 `rand::` 前缀来调用 `rand` crate 中的任何内容。
|
||||
|
||||
接下来增加了另一行 `use`:`use rand::Rng`。`Rng` 是一个 trait,它定义了随机数生成器应实现的方法,想使用这些方法的话此 trait 必须在作用域中。第十章会详细介绍 trait。
|
||||
接下来增加了另一行 `use`:`use rand::Rng`。`Rng` 是一个 trait,它定义了随机数生成器应实现的方法,想使用这些方法的话,此 trait 必须在作用域中。第十章会详细介绍 trait。
|
||||
|
||||
另外,中间还新增加了两行。`rand::thread_rng` 函数提供实际使用的随机数生成器:它位于当前执行线程本地,并从操作系统获取 seed。接下来,调用随机数生成器的 `gen_range` 方法。这个方法由刚才引入到作用域的 `Rng` trait 定义。`gen_range` 方法获取两个数字作为参数,并生成一个范围在两者之间的随机数。它包含下限但不包含上限,所以需要指定 `1` 和 `101` 来请求一个 1 和 100 之间的数。
|
||||
|
||||
@ -406,7 +407,7 @@ You guessed: 5
|
||||
|
||||
你应该能得到不同的随机数,同时它们应该都是在 1 和 100 之间的。干得漂亮!
|
||||
|
||||
## 比较猜测与秘密数字
|
||||
## 比较猜测的数字和秘密数字
|
||||
|
||||
现在有了用户输入和一个随机数,我们可以比较它们。这个步骤如示例 2-4 所示。注意这段代码还不能通过编译,我们稍后会解释。
|
||||
|
||||
@ -443,7 +444,7 @@ fn main() {
|
||||
|
||||
一个 `match` 表达式由 **分支(arms)** 构成。一个分支包含一个 **模式**(*pattern*)和表达式开头的值与分支模式相匹配时应该执行的代码。Rust 获取提供给 `match` 的值并挨个检查每个分支的模式。`match` 结构和模式是 Rust 中强大的功能,它体现了代码可能遇到的多种情形,并帮助你确保没有遗漏处理。这些功能将分别在第六章和第十八章详细介绍。
|
||||
|
||||
让我们看看使用 `match` 表达式的例子。假设用户猜了 50,这时随机生成的秘密数字是 38。比较 50 与 38 时,因为 50 比 38 要大,`cmp` 方法会返回 `Ordering::Greater`。`Ordering::Greater` 是 `match` 表达式得到的值。它检查第一个分支的模式,`Ordering::Less` 与 `Ordering::Greater`并不匹配,所以它忽略了这个分支的动作并来到下一个分支。下一个分支的模式是 `Ordering::Greater`,**正确** 匹配!这个分支关联的代码被执行,在屏幕打印出 `Too big!`。`match` 表达式就此终止,因为该场景下没有检查最后一个分支的必要。
|
||||
让我们看看使用 `match` 表达式的例子。假设用户猜了 50,这时随机生成的秘密数字是 38。比较 50 与 38 时,因为 50 比 38 要大,`cmp` 方法会返回 `Ordering::Greater`。`Ordering::Greater` 是 `match` 表达式得到的值。它检查第一个分支的模式,`Ordering::Less` 与 `Ordering::Greater`并不匹配,所以它忽略了这个分支的代码并来到下一个分支。下一个分支的模式是 `Ordering::Greater`,**正确** 匹配!这个分支关联的代码被执行,在屏幕打印出 `Too big!`。`match` 表达式就此终止,因为该场景下没有检查最后一个分支的必要。
|
||||
|
||||
然而,示例 2-4 的代码并不能编译,可以尝试一下:
|
||||
|
||||
@ -463,7 +464,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 之间的值:32 位数字 `i32`;32 位无符号数字 `u32`;64 位数字 `i64` 等等。Rust 默认使用 `i32`,所以它是 `secret_number` 的类型,除非增加类型信息,或任何能让 Rust 推断出不同数值类型的信息。这里错误的原因在于 Rust 不会比较字符串类型和数字类型。
|
||||
错误的核心表明这里有 **不匹配的类型**(*mismatched types*)。Rust 有一个静态强类型系统,同时也有类型推断。当我们写出 `let guess = String::new()` 时,Rust 推断出 `guess` 应该是 `String` 类型,并不需要我们写出类型。另一方面,`secret_number`,是数字类型。几个数字类型拥有 1 到 100 之间的值:32 位数字 `i32`;32 位无符号数字 `u32`;64 位数字 `i64` 等等。Rust 默认使用 `i32`,所以它是 `secret_number` 的类型,除非增加类型信息,或任何能让 Rust 推断出不同数值类型的信息。这里错误的原因在于 Rust 不会比较字符串类型和数字类型。
|
||||
|
||||
所以我们必须把从输入中读取到的 `String` 转换为一个真正的数字类型,才好与秘密数字进行比较。这可以通过在 `main` 函数体中增加如下两行代码来实现:
|
||||
|
||||
@ -497,9 +498,9 @@ let guess: u32 = guess.trim().parse()
|
||||
.expect("Please type a number!");
|
||||
```
|
||||
|
||||
这里创建了一个叫做 `guess` 的变量。不过等等,不是已经有了一个叫做`guess`的变量了吗?确实如此,不过 Rust 允许 **隐藏**(*shadow*),用一个新值来隐藏 `guess` 之前的值。这个功能常用在需要转换值类型之类的场景,它允许我们复用 `guess` 变量的名字,而不是被迫创建两个不同变量,诸如 `guess_str` 和 `guess` 之类。(第三章会介绍 shadowing 的更多细节。)
|
||||
这里创建了一个叫做 `guess` 的变量。不过等等,不是已经有了一个叫做 `guess` 的变量了吗?确实如此,不过 Rust 允许用一个新值来 **隐藏** (*shadow*) `guess` 之前的值。这个功能常用在需要转换值类型之类的场景。它允许我们复用 `guess` 变量的名字,而不是被迫创建两个不同变量,诸如 `guess_str` 和 `guess` 之类。(第三章会介绍 shadowing 的更多细节。)
|
||||
|
||||
`guess` 被绑定到 `guess.trim().parse()` 表达式。表达式中的 `guess` 是包含输入的原始 `String` 类型。`String` 实例的 `trim` 方法会去除字符串开头和结尾的空白。`u32` 只能由数字字符转换,不过用户必须输入 <span class="keystroke">return</span> 键才能让 `read_line` 返回,然而用户按下 <span class="keystroke">return</span> 键时,会在字符串中增加一个换行(newline)符。例如,用户输入 <span class="keystroke">5</span> 并按下 <span class="keystroke">return</span>,`guess` 看起来像这样:`5\n`。`\n` 代表 “换行”,回车键。`trim` 方法消除 `\n`,只留下`5`。
|
||||
我们将 `guess` 绑定到 `guess.trim().parse()` 表达式上。表达式中的 `guess` 是包含输入的原始 `String` 类型。`String` 实例的 `trim` 方法会去除字符串开头和结尾的空白字符。`u32` 只能由数字字符转换,不过用户必须输入 <span class="keystroke">return</span> 键才能让 `read_line` 返回,然而用户按下 <span class="keystroke">return</span> 键时,会在字符串中增加一个换行(newline)符。例如,用户输入 <span class="keystroke">5</span> 并按下 <span class="keystroke">return</span>,`guess` 看起来像这样:`5\n`。`\n` 代表 “换行”,回车键。`trim` 方法消除 `\n`,只留下`5`。
|
||||
|
||||
[字符串的 `parse` 方法][parse]<!-- ignore --> 将字符串解析成数字。因为这个方法可以解析多种数字类型,因此需要告诉 Rust 具体的数字类型,这里通过 `let guess: u32` 指定。`guess` 后面的冒号(`:`)告诉 Rust 我们指定了变量的类型。Rust 有一些内建的数字类型;`u32` 是一个无符号的 32 位整型。对于不大的正整数来说,它是不错的类型,第三章还会讲到其他数字类型。另外,程序中的 `u32` 注解以及与 `secret_number` 的比较,意味着 Rust 会推断出 `secret_number` 也是 `u32` 类型。现在可以使用相同类型比较两个值了!
|
||||
|
||||
@ -522,7 +523,7 @@ You guessed: 76
|
||||
Too big!
|
||||
```
|
||||
|
||||
漂亮!即便是在猜测之前添加了空格,程序依然能判断出用户猜测了 76。多运行程序几次来检验不同类型输入的相应行为:猜一个正确的数字,猜一个过大的数字和猜一个过小的数字。
|
||||
漂亮!即便是在猜测之前添加了空格,程序依然能判断出用户猜测了 76。多运行程序几次,输入不同的数字来检验不同的行为:猜一个正确的数字,猜一个过大的数字和猜一个过小的数字。
|
||||
|
||||
现在游戏已经大体上能玩了,不过用户只能猜一次。增加一个循环来改变它吧!
|
||||
|
||||
@ -551,9 +552,9 @@ Too big!
|
||||
}
|
||||
```
|
||||
|
||||
如上所示,我们将提示用户猜测之后的所有内容放入了循环。确保这些代码额外缩进了一层,再次运行程序。注意这里有一个新问题,因为程序忠实地执行了我们的要求:永远地请求另一个猜测,用户好像没法退出啊!
|
||||
如上所示,我们将提示用户猜测之后的所有内容放入了循环。确保 loop 循环中的代码多缩进四个空格,再次运行程序。注意这里有一个新问题,因为程序忠实地执行了我们的要求:永远地请求另一个猜测,用户好像没法退出啊!
|
||||
|
||||
用户总能使用 <span class="keystroke">ctrl-c</span> 终止程序。不过还有另一个方法跳出无限循环,就是 “比较猜测与秘密数字” 部分提到的 `parse`:如果用户输入一个非数字答案,程序会崩溃。用户可以利用这一点来退出,如下所示:
|
||||
用户总能使用 <span class="keystroke">ctrl-c</span> 终止程序。不过还有另一个方法跳出无限循环,就是 “比较猜测与秘密数字” 部分提到的 `parse`:如果用户输入的答案不是一个数字,程序会崩溃。用户可以利用这一点来退出,如下所示:
|
||||
|
||||
```text
|
||||
$ cargo run
|
||||
@ -707,6 +708,6 @@ fn main() {
|
||||
|
||||
## 总结
|
||||
|
||||
此时此刻,你顺利完成了猜猜看游戏!恭喜!
|
||||
此时此刻,你顺利完成了猜猜看游戏。恭喜!
|
||||
|
||||
这是一个通过动手实践学习 Rust 新概念的项目:`let`、`match`、方法、关联函数、使用外部 crate 等等,接下来的几章,我们将会继续深入。第三章涉及到大部分编程语言都有的概念,比如变量、数据类型和函数,以及如何在 Rust 中使用它们。第四章探索所有权(ownership),这是一个 Rust 同其他语言大不相同的功能。第五章讨论结构体和方法的语法,而第六章侧重解释枚举。
|
||||
本项目通过动手实践,向你介绍了 Rust 新概念:`let`、`match`、方法、关联函数、使用外部 crate 等等,接下来的几章,你会继续深入学习这些概念。第三章介绍大部分编程语言都有的概念,比如变量、数据类型和函数,以及如何在 Rust 中使用它们。第四章探索所有权(ownership),这是一个 Rust 同其他语言大不相同的功能。第五章讨论结构体和方法的语法,而第六章侧重解释枚举。
|
||||
|
@ -2,13 +2,13 @@
|
||||
|
||||
> [ch03-01-variables-and-mutability.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch03-01-variables-and-mutability.md)
|
||||
> <br>
|
||||
> commit 6aad5008b69078a2fc18e6dd7e00ef395170c749
|
||||
> commit f949ff883628db8ed2f2f5f19e146ebf19ed6a6f
|
||||
|
||||
第二章中提到过,变量默认是 **不可变**(*immutable*)的。这是鼓励你利用 Rust 安全和简单并发的优势来编写代码的一大助力。不过,你仍然可以使用可变变量。让我们探讨一下 Rust 拥抱不可变性的原因及方法,以及何时你不想使用不可变性。
|
||||
第二章中提到过,变量默认是不可改变的(immutable)。这是推动你以充分利用 Rust 提供的安全性和简单并发性编写代码的众多方式之一。不过,你仍然可以使用可变变量。让我们探讨一下 Rust 拥抱不可变性的原因及方法,以及何时你不想使用不可变性。
|
||||
|
||||
当变量不可变时,意味着一旦值被绑定上一个名称,你就不能改变这个值。作为说明,通过 `cargo new --bin variables` 在 *projects* 目录生成一个叫做 *variables* 的新项目。
|
||||
当变量不可变时,一旦值被绑定一个名称上,你就不能改变这个值。为了对此进行说明,使用 `cargo new --bin variables` 命令在 *projects* 目录生成一个叫做 *variables* 的新项目。
|
||||
|
||||
接着,在新建的 *variables* 目录,打开 *src/main.rs* 并替换其代码为如下:
|
||||
接着,在新建的 *variables* 目录,打开 *src/main.rs* 并将代码替换为如下代码,这些代码还不能编译:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -21,7 +21,7 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
保存并使用 `cargo run` 运行程序。应该会看到一个错误信息,如下输出所示:
|
||||
保存并使用 `cargo run` 运行程序。应该会看到一条错误信息,如下输出所示:
|
||||
|
||||
```text
|
||||
error[E0384]: cannot assign twice to immutable variable `x`
|
||||
@ -34,17 +34,17 @@ error[E0384]: cannot assign twice to immutable variable `x`
|
||||
| ^^^^^ cannot assign twice to immutable variable
|
||||
```
|
||||
|
||||
这个例子展示了编译器如何帮助你找出程序中的错误。虽然编译错误令人沮丧,那也不过是说程序不能安全的完成你想让它完成的工作;而 **不能** 说明你是不是一个好程序员!有经验的 Rustacean 们一样会遇到编译错误。
|
||||
这个例子展示了编译器如何帮助你找出程序中的错误。虽然编译错误令人沮丧,但那只是表示程序不能安全的完成你想让它完成的工作;并 **不能** 说明你不是一个好程序员!经验丰富的 Rustacean 们一样会遇到编译错误。
|
||||
|
||||
这些错误给出的原因是 `不能对不可变变量二次赋值`(`cannot assign twice to immutable variable x`),因为我们尝试对不可变变量 `x` 赋第二个值。
|
||||
错误信息指出错误的原因是 `不能对不可变变量 x 二次赋值`(`cannot assign twice to immutable variable x`),因为你尝试对不可变变量 `x` 赋第二个值。
|
||||
|
||||
在尝试改变预设为不可变的值的时候产生编译错误是很重要的,因为这种情况可能导致 bug:如果代码的一部分假设一个值永远也不会改变,而另一部分代码改变了它,第一部分代码就有可能以不可预料的方式运行。不得不承认这种 bug 难以跟踪,尤其是第二部分代码只是 **有时** 改变其值的时候。
|
||||
在尝试改变预设为不可变的值时,产生编译时错误是很重要的,因为这种情况可能导致 bug。如果一部分代码假设一个值永远也不会改变,而另一部分代码改变了这个值,第一部分代码就有可能以不可预料的方式运行。不得不承认这种 bug 的起因难以跟踪,尤其是第二部分代码只是 **有时** 会改变值。
|
||||
|
||||
Rust 编译器保证,如果声明一个值不会变,它就真的不会变。这意味着当阅读和编写代码时,不需要追踪一个值如何以及哪里可能会被改变,从而使得代码易于推导。
|
||||
|
||||
不过可变性也是非常有用的。变量只是默认不可变,可以通过在变量名之前加 `mut` 来使其可变。除了使值可以改变之外,它向读者表明了其他代码将会改变这个变量的意图。
|
||||
不过可变性也是非常有用的。变量只是默认不可变;正如在第二章所做的那样,你可以在变量名之前加 `mut` 来使其可变。除了允许改变值之外,`mut` 向读者表明了其他代码将会改变这个变量值的意图。
|
||||
|
||||
例如,改变 *src/main.rs* 并替换其代码为如下:
|
||||
例如,让我们将 *src/main.rs* 修改为如下代码:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -57,7 +57,7 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
当运行这个程序,出现如下:
|
||||
现在运行这个程序,出现如下内容:
|
||||
|
||||
```text
|
||||
$ cargo run
|
||||
@ -68,23 +68,23 @@ The value of x is: 5
|
||||
The value of x is: 6
|
||||
```
|
||||
|
||||
通过 `mut`,允许把绑定到 `x` 的值从 `5` 改成 `6`。在一些情况下,你会想用可变变量,因为这样的代码比起只用不可变变量的实现更容易编写。
|
||||
通过 `mut`,允许把绑定到 `x` 的值从 `5` 改成 `6`。在一些情况下,你会想用可变变量,因为与只用不可变变量相比,它会让代码更容易编写。
|
||||
|
||||
除了避免 bug 外,还有很多地方需要权衡取舍。例如,使用大型数据结构时,适当地使用可变变量,可能比复制和返回新分配的实例更快。对于较小的数据结构,总是创建新实例,采用更偏向函数式的风格编程,可能会使代码更易理解,为可读性而遭受性能惩罚或许值得。
|
||||
除了防止出现 bug 外,还有很多地方需要权衡取舍。例如,使用大型数据结构时,适当地使用可变变量,可能比复制和返回新分配的实例更快。对于较小的数据结构,总是创建新实例,采用更偏向函数式的编程风格,可能会使代码更易理解,为可读性而牺牲性能或许是值得的。
|
||||
|
||||
### 变量和常量的区别
|
||||
|
||||
不允许改变值的变量,可能会使你想起另一个大部分编程语言都有的概念:**常量**(*constants*)。类似于不可变变量,常量也是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。
|
||||
不允许改变值的变量,可能会使你想起另一个大部分编程语言都有的概念:**常量**(*constants*)。类似于不可变变量,常量是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。
|
||||
|
||||
首先,不允许对常量使用 `mut`:常量不光默认不能变,它总是不能变。
|
||||
首先,不允许对常量使用 `mut`。常量不光默认不能变,它总是不能变。
|
||||
|
||||
声明常量使用 `const` 关键字而不是 `let`,并且 *必须* 注明值的类型。在下一部分,“数据类型” 中会涉及到类型和类型注解,现在无需关心这些细节,记住总是标注类型即可。
|
||||
声明常量使用 `const` 关键字而不是 `let`,并且 *必须* 注明值的类型。在下一部分,“数据类型” 中会介绍类型和类型注解,现在无需关心这些细节,记住总是标注类型即可。
|
||||
|
||||
常量可以在任何作用域声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。
|
||||
常量可以在任何作用域中声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。
|
||||
|
||||
最后一个区别是常量只能用于常量表达式,而不能作为函数调用的结果,或任何其他只在运行时计算的值。
|
||||
最后一个区别是,常量只能被设置为常量表达式,而不能是函数调用的结果,或任何其他只能在运行时计算出的值。
|
||||
|
||||
这是一个常量声明的例子,它的名称是 `MAX_POINTS`,值是 100,000。(Rust 的常量使用下划线分隔的大写字母的命名规范):
|
||||
这是一个声明常量的例子,它的名称是 `MAX_POINTS`,值是 100,000。(Rust 常量的命名规范是使用下划线分隔的大写字母):
|
||||
|
||||
```rust
|
||||
const MAX_POINTS: u32 = 100_000;
|
||||
@ -92,11 +92,11 @@ const MAX_POINTS: u32 = 100_000;
|
||||
|
||||
在声明它的作用域之中,常量在整个程序生命周期中都有效,这使得常量可以作为多处代码使用的全局范围的值,例如一个游戏中所有玩家可以获取的最高分或者光速。
|
||||
|
||||
将用于整个程序的硬编码的值声明为常量对后来的维护者了解值的意义很有帮助。同时将硬编码的值汇总于一处,也能为将来修改提供方便。
|
||||
将遍布于应用程序中的硬编码值声明为常量,能帮助后来的代码维护人员了解值的意图。如果将来需要修改硬编码值,也只需修改汇聚于一处的硬编码值。
|
||||
|
||||
### 隐藏(Shadowing)
|
||||
|
||||
如第二章 “猜猜看游戏” 所讲的,我们可以定义一个与之前变量重名的新变量,而新变量会 **隐藏** 之前的变量。Rustacean 们称之为第一个变量被第二个 **隐藏** 了,这意味着使用这个变量时会看到第二个值。可以用相同变量名称来隐藏它自己,以及重复使用 `let` 关键字来多次隐藏,如下所示:
|
||||
正如在第二章猜猜看游戏的 “比较猜测的数字和秘密数字” 中所讲,我们可以定义一个与之前变量同名的新变量,而新变量会 **隐藏** 之前的变量。Rustacean 们称之为第一个变量被第二个 **隐藏** 了,这意味着使用这个变量时会看到第二个值。可以用相同变量名称来隐藏一个变量,以及重复使用 `let` 关键字来多次隐藏,如下所示:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -112,7 +112,7 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
这个程序首先将 `x` 绑定到值 `5` 上。接着通过 `let x =` 隐藏 `x`,获取原始值并加 `1` 这样 `x` 的值就变成 `6` 了。第三个 `let` 语句也隐藏了 `x`,获取之前的值并乘以 `2`,`x` 最终的值是 `12`。运行这个程序,它会有如下输出:
|
||||
这个程序首先将 `x` 绑定到值 `5` 上。接着通过 `let x =` 隐藏 `x`,获取初始值并加 `1`,这样 `x` 的值就变成 `6` 了。第三个 `let` 语句也隐藏了 `x`,将之前的值乘以 `2`,`x` 最终的值是 `12`。运行这个程序,它会有如下输出:
|
||||
|
||||
```text
|
||||
$ cargo run
|
||||
@ -122,23 +122,23 @@ $ cargo run
|
||||
The value of x is: 12
|
||||
```
|
||||
|
||||
这与将变量声明为 `mut` 是有区别的。因为除非再次使用 `let` 关键字,不小心尝试对变量重新赋值会导致编译时错误。我们可以用这个值进行一些计算,不过计算完之后变量仍然是不变的。
|
||||
隐藏与将变量标记为 `mut` 是有区别的。当不小心尝试对变量重新赋值时,如果没有使用 `let` 关键字,就会导致编译时错误。通过使用 `let`,我们可以用这个值进行一些计算,不过计算完之后变量仍然是不变的。
|
||||
|
||||
`mut` 与隐藏的另一个区别是,当再次使用 `let` 时,实际上创建了一个新变量,我们可以改变值的类型,从而复用这个名字。例如,假设程序请求用户输入空格来提供文本间隔,然而我们真正需要的是将输入存储成数字(多少个空格):
|
||||
`mut` 与隐藏的另一个区别是,当再次使用 `let` 时,实际上创建了一个新变量,我们可以改变值的类型,但复用这个名字。例如,假设程序请求用户输入空格字符来说明希望在文本之间显示多少个空格,然而我们真正需要的是将输入存储成数字(多少个空格):
|
||||
|
||||
```rust
|
||||
let spaces = " ";
|
||||
let spaces = spaces.len();
|
||||
```
|
||||
|
||||
这里允许第一个 `spaces` 变量是字符串类型,而第二个 `spaces` 变量,它是一个恰巧与第一个变量同名的崭新变量,是数字类型。隐藏使我们不必使用不同的名字,如 `spaces_str` 和 `spaces_num`;相反,我们可以复用 `spaces` 这个更简单的名字。然而,如果尝试使用 `mut`,如下所示:
|
||||
这里允许第一个 `spaces` 变量是字符串类型,而第二个 `spaces` 变量,它是一个恰巧与第一个变量同名的崭新变量,是数字类型。隐藏使我们不必使用不同的名字,如 `spaces_str` 和 `spaces_num`;相反,我们可以复用 `spaces` 这个更简单的名字。然而,如果尝试使用 `mut`,将会得到一个编译时错误,如下所示:
|
||||
|
||||
```rust,ignore
|
||||
let mut spaces = " ";
|
||||
spaces = spaces.len();
|
||||
```
|
||||
|
||||
会导致一个编译错误,因为改变一个变量的类型是不被允许的:
|
||||
这个错误说明,我们不能改变变量的类型:
|
||||
|
||||
```text
|
||||
error[E0308]: mismatched types
|
||||
@ -151,4 +151,4 @@ error[E0308]: mismatched types
|
||||
found type `usize`
|
||||
```
|
||||
|
||||
现在我们已经了解了变量如何工作,让我们再看看更多变量可以拥有的数据类型。
|
||||
现在我们已经了解了变量如何工作,让我们看看变量可以拥有的更多数据类型。
|
||||
|
@ -2,17 +2,17 @@
|
||||
|
||||
> [ch03-02-data-types.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch03-02-data-types.md)
|
||||
> <br>
|
||||
> commit ec65990849230388e4ce4db5b7a0cb8a0f0d60e2
|
||||
> commit f949ff883628db8ed2f2f5f19e146ebf19ed6a6f
|
||||
|
||||
在 Rust 中,任何值都属于一种明确的 **类型**(*type*),这告诉了 Rust 它被指定为何种数据,以便明确其处理方式。本部分我们将看到一系列内建于语言中的类型。我们将其分为两类:标量(scalar)和复合(compound)。
|
||||
在 Rust 中,每一个值都属于某一个 **数据类型**(*data type*),这告诉 Rust 它被指定为何种数据,以便明确数据处理方式。我们将看到两类数据类型:标量(scalar)和复合(compound)。
|
||||
|
||||
Rust 是 **静态类型**(*statically typed*)语言,也就是说在编译时就必须知道所有变量的类型,这一点将贯穿整个章节。通过值的形式及其使用方式,编译器通常可以推断出我们想要用的类型。多种类型均有可能时,比如第二章中使用 `parse` 将 `String` 转换为数字时,必须增加类型注解,像这样:
|
||||
记住,Rust 是 **静态类型**(*statically typed*)语言,也就是说在编译时就必须知道所有变量的类型。根据值及其使用方式,编译器通常可以推断出我们想要用的类型。当多种类型均有可能时,比如第二章的 “比较猜测的数字和秘密数字” 使用 `parse` 将 `String` 转换为数字时,必须增加类型注解,像这样:
|
||||
|
||||
```rust
|
||||
let guess: u32 = "42".parse().expect("Not a number!");
|
||||
```
|
||||
|
||||
这里如果不添加类型注解,Rust 会显示如下错误,这说明编译器需要更多信息,来了解我们想要的类型:
|
||||
这里如果不添加类型注解,Rust 会显示如下错误,这说明编译器需要我们提供更多信息,来了解我们想要的类型:
|
||||
|
||||
```text
|
||||
error[E0282]: type annotations needed
|
||||
@ -25,37 +25,37 @@ error[E0282]: type annotations needed
|
||||
| consider giving `guess` a type
|
||||
```
|
||||
|
||||
在我们讨论各种数据类型时,你会看到不同的类型注解。
|
||||
你会看到其它数据类型的各种类型注解。
|
||||
|
||||
### 标量类型
|
||||
|
||||
**标量**(*scalar*)类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型。你可能在其他语言中见过它们,不过让我们深入了解它们在 Rust 中是如何工作的。
|
||||
**标量**(*scalar*)类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型。你可能在其他语言中见过它们。让我们深入了解它们在 Rust 中是如何工作的。
|
||||
|
||||
#### 整型
|
||||
|
||||
**整数** 是一个没有小数部分的数字。我们在这一章的前面使用过 `u32` 类型。该类型声明表明,u32 关联的值应该是一个占据 32 比特位的无符号整数(有符号整型类型以 `i` 开头而不是 `u`)。表格 3-1 展示了 Rust 内建的整数类型。每一种变体都有符号和无符号列(例如,*i8*)可以用来声明对应的整数值。
|
||||
**整数** 是一个没有小数部分的数字。我们在第二章使用过 `u32` 整数类型。该类型声明表明,它关联的值应该是一个占据 32 比特位的无符号整数(有符号整数类型以 `i` 开头而不是 `u`)。表格 3-1 展示了 Rust 内建的整数类型。在有符号列和无符号列中的每一个变体(例如,`i16`)都可以用来声明整数值的类型。
|
||||
|
||||
<span class="caption">表格 3-1: Rust 中的整型</span>
|
||||
|
||||
| 长度 | 有符号 | 无符号 |
|
||||
|--------|--------|----------|
|
||||
| 8-bit | i8 | u8 |
|
||||
| 16-bit | i16 | u16 |
|
||||
| 32-bit | i32 | u32 |
|
||||
| 64-bit | i64 | u64 |
|
||||
| arch | isize | usize |
|
||||
| 长度 | 有符号 | 无符号 |
|
||||
|--------|---------|----------|
|
||||
| 8-bit | `i8` | `u8` |
|
||||
| 16-bit | `i16` | `u16` |
|
||||
| 32-bit | `i32` | `u32` |
|
||||
| 64-bit | `i64` | `u64` |
|
||||
| arch | `isize` | `usize` |
|
||||
|
||||
每一种变体都可以是有符号或无符号的,并有一个明确的大小。有符号和无符号代表数字能否为负值;换句话说,数字是否需要有一个符号(有符号数),或者永远为正而不需要符号(无符号数)。这有点像在纸上书写数字:当需要考虑符号的时候,数字以加号或减号作为前缀;然而,可以安全地假设为正数时,加号前缀通常省略。有符号数以二进制补码形式(two’s complement representation)存储(如果你不清楚这是什么,可以在网上搜索;对其的解释超出了本书的范畴)。
|
||||
每一个变体都可以是有符号或无符号的,并有一个明确的大小。**有符号** 和 **无符号** 代表数字能否为负值,换句话说,数字是否需要有一个符号(有符号数),或者永远为正而不需要符号(无符号数)。这有点像在纸上书写数字:当需要考虑符号的时候,数字以加号或减号作为前缀;然而,可以安全地假设为正数时,加号前缀通常省略。有符号数以补码形式(two’s complement representation)存储(如果你不清楚这是什么,可以在网上搜索;对其的解释超出了本书的范畴)。
|
||||
|
||||
每一个有符号的变体可以储存包含从 -(2<sup>n - 1</sup>) 到 2<sup>n - 1</sup> - 1 在内的数字,这里 `n` 是变体使用的位数。所以 `i8` 可以储存从 -(2<sup>7</sup>) 到 2<sup>7</sup> - 1 在内的数字,也就是从 -128 到 127。无符号的变体可以储存从 0 到 2<sup>n</sup> - 1 的数字,所以 `u8` 可以储存从 0 到 2<sup>8</sup> - 1 的数字,也就是从 0 到 255。
|
||||
每一个有符号的变体可以储存包含从 -(2<sup>n - 1</sup>) 到 2<sup>n - 1</sup> - 1 在内的数字,这里 *n* 是变体使用的位数。所以 `i8` 可以储存从 -(2<sup>7</sup>) 到 2<sup>7</sup> - 1 在内的数字,也就是从 -128 到 127。无符号的变体可以储存从 0 到 2<sup>n</sup> - 1 的数字,所以 `u8` 可以储存从 0 到 2<sup>8</sup> - 1 的数字,也就是从 0 到 255。
|
||||
|
||||
另外,`isize` 和 `usize` 类型依赖运行程序的计算机架构:64 位架构上它们是 64 位的, 32 位架构上它们是 32 位的。
|
||||
|
||||
可以使用表格 3-2 中的任何一种形式编写数字字面值。注意除 byte 以外的其它字面值允许使用类型后缀,例如 `57u8`,同时也允许使用 `_` 做为分隔符以方便读数,例如`1_000`。
|
||||
可以使用表格 3-2 中的任何一种形式编写数字字面值。注意除 byte 以外的所有数字字面值允许使用类型后缀,例如 `57u8`,同时也允许使用 `_` 做为分隔符以方便读数,例如`1_000`。
|
||||
|
||||
<span class="caption">表格 3-2: Rust 中的整型字面值</span>
|
||||
|
||||
| 数字字面值 | 例子 |
|
||||
| 数字字面值 | 例子 |
|
||||
|------------------|---------------|
|
||||
| Decimal | `98_222` |
|
||||
| Hex | `0xff` |
|
||||
@ -67,7 +67,7 @@ error[E0282]: type annotations needed
|
||||
|
||||
#### 浮点型
|
||||
|
||||
Rust 同样有两个主要的 **浮点数**(*floating-point numbers*)类型,`f32` 和 `f64`,它们是带小数点的数字,分别占 32 位和 64 位比特。默认类型是 `f64`,因为在现代 CPU 中它与 `f32` 速度几乎一样,不过精度更高。
|
||||
Rust 也有两个原生的 **浮点数**(*floating-point numbers*)类型,它们是带小数点的数字。Rust 的浮点数类型是 `f32` 和 `f64`,分别占 32 位和 64 位。默认类型是 `f64`,因为在现代 CPU 中,它与 `f32` 速度几乎一样,不过精度更高。
|
||||
|
||||
这是一个展示浮点数的实例:
|
||||
|
||||
@ -83,32 +83,32 @@ fn main() {
|
||||
|
||||
浮点数采用 IEEE-754 标准表示。`f32` 是单精度浮点数,`f64` 是双精度浮点数。
|
||||
|
||||
#### 数字运算符
|
||||
#### 数值运算
|
||||
|
||||
Rust 支持所有数字类型常见的基本数学运算操作:加法、减法、乘法、除法以及取余。下面的代码展示了如何在一个 `let` 语句中使用它们:
|
||||
Rust 中的所有数字类型都支持基本数学运算:加法、减法、乘法、除法和取余。下面的代码展示了如何在 `let` 语句中使用它们:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
// addition
|
||||
// 加法
|
||||
let sum = 5 + 10;
|
||||
|
||||
// subtraction
|
||||
// 减法
|
||||
let difference = 95.5 - 4.3;
|
||||
|
||||
// multiplication
|
||||
// 乘法
|
||||
let product = 4 * 30;
|
||||
|
||||
// division
|
||||
// 除法
|
||||
let quotient = 56.7 / 32.2;
|
||||
|
||||
// remainder
|
||||
// 取余
|
||||
let remainder = 43 % 5;
|
||||
}
|
||||
```
|
||||
|
||||
这些语句中的每个表达式使用了一个数学运算符并计算出了一个值,它们绑定到了一个变量。附录 B 包含了一个 Rust 提供的所有运算符的列表。
|
||||
这些语句中的每个表达式使用了一个数学运算符并计算出了一个值,然后绑定给一个变量。附录 B 包含 Rust 提供的所有运算符的列表。
|
||||
|
||||
#### 布尔型
|
||||
|
||||
@ -120,37 +120,37 @@ fn main() {
|
||||
fn main() {
|
||||
let t = true;
|
||||
|
||||
let f: bool = false; // with explicit type annotation
|
||||
let f: bool = false; // 显式指定类型注解
|
||||
}
|
||||
```
|
||||
|
||||
使用布尔值的主要场景是条件表达式,例如 `if` 表达式。在 “控制流”(“Control Flow”)部分将讲到 `if` 表达式在 Rust 中如何工作。
|
||||
使用布尔值的主要场景是条件表达式,例如 `if` 表达式。在 “控制流”(“Control Flow”)部分将介绍 `if` 表达式在 Rust 中如何工作。
|
||||
|
||||
#### 字符类型
|
||||
|
||||
目前为止只使用到了数字,不过 Rust 也支持字符。Rust 的 `char` 类型是大部分语言中基本字母字符类型,如下代码展示了如何使用它。注意 `char` 由单引号指定,不同于字符串使用双引号:
|
||||
目前为止只使用到了数字,不过 Rust 也支持字母。Rust 的 `char` 类型是语言中最原生的字母类型,如下代码展示了如何使用它。(注意 `char` 由单引号指定,不同于字符串使用双引号。)
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let c = 'z';
|
||||
let z = 'ℤ';
|
||||
let heart_eyed_cat = '😻';
|
||||
let c = 'z';
|
||||
let z = 'ℤ';
|
||||
let heart_eyed_cat = '😻';
|
||||
}
|
||||
```
|
||||
|
||||
Rust 的 `char` 类型代表了一个 Unicode 标量值(Unicode Scalar Value),这意味着它可以比 ASCII 表示更多内容。拼音字母(Accented letters),中文/日文/韩文等象形文字,emoji(絵文字)以及零长度的空白字符对于 Rust `char` 类型都是有效的。Unicode 标量值包含从 `U+0000` 到 `U+D7FF` 和 `U+E000` 到 `U+10FFFF` 在内的值。不过,“字符” 并不是一个 Unicode 中的概念,所以人直觉上的 “字符” 可能与 Rust 中的 `char` 并不符合。第八章的 “字符串” 部分将详细讨论这个主题。
|
||||
Rust 的 `char` 类型代表了一个 Unicode 标量值(Unicode Scalar Value),这意味着它可以比 ASCII 表示更多内容。在 Rust 中,拼音字母(Accented letters),中文、日文、韩文等字符,emoji(绘文字)以及零长度的空白字符都是有效的 `char` 值。Unicode 标量值包含从 `U+0000` 到 `U+D7FF` 和 `U+E000` 到 `U+10FFFF` 在内的值。不过,“字符” 并不是一个 Unicode 中的概念,所以人直觉上的 “字符” 可能与 Rust 中的 `char` 并不符合。第八章的 “字符串” 中将详细讨论这个主题。
|
||||
|
||||
### 复合类型
|
||||
|
||||
**复合类型**(*Compound types*)可以将多个其他类型的值组合成一个类型。Rust 有两个原生的复合类型:元组(tuple)和数组(array)。
|
||||
**复合类型**(*Compound types*)可以将多个值组合成一个类型。Rust 有两个原生的复合类型:元组(tuple)和数组(array)。
|
||||
|
||||
#### 将值组合进元组
|
||||
#### 元组类型
|
||||
|
||||
元组是一个将多个其他类型的值组合进一个复合类型的主要方式。
|
||||
|
||||
我们使用一个括号中的逗号分隔的值列表来创建一个元组。元组中的每一个位置都有一个类型,而且这些不同值的类型也不必是相同的。这个例子中使用了额外的可选类型注解:
|
||||
我们使用包含在圆括号中的逗号分隔的值列表来创建一个元组。元组中的每一个位置都有一个类型,而且这些不同值的类型也不必是相同的。这个例子中使用了可选的类型注解:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -160,7 +160,7 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
`tup` 变量绑定了整个元组,因为元组被认为是一个单独的复合元素。为了从元组中获取单个的值,可以使用模式匹配(pattern matching)来解构(destructure)元组,像这样:
|
||||
`tup` 变量绑定到整个元组上,因为元组是一个单独的复合元素。为了从元组中获取单个值,可以使用模式匹配(pattern matching)来解构(destructure)元组值,像这样:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -176,7 +176,7 @@ fn main() {
|
||||
|
||||
程序首先创建了一个元组并绑定到 `tup` 变量上。接着使用了 `let` 和一个模式将 `tup` 分成了三个不同的变量,`x`、`y` 和 `z`。这叫做 **解构**(*destructuring*),因为它将一个元组拆成了三个部分。最后,程序打印出了 `y` 的值,也就是 `6.4`。
|
||||
|
||||
除了使用模式匹配解构之外,也可以使用点号(`.`)后跟值的索引来直接访问它们。例如:
|
||||
除了使用模式匹配解构外,也可以使用点号(`.`)后跟值的索引来直接访问它们。例如:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -194,11 +194,11 @@ fn main() {
|
||||
|
||||
这个程序创建了一个元组,`x`,并接着使用索引为每个元素创建新变量。跟大多数编程语言一样,元组的第一个索引值是 0。
|
||||
|
||||
#### 数组
|
||||
#### 数组类型
|
||||
|
||||
另一个获取一个多个值集合的方式是 **数组**(*array*)。与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,因为 Rust 中的数组是固定长度的:一旦声明,它们的长度不能增长或缩小。
|
||||
另一个包含多个值的方式是 **数组**(*array*)。与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,因为 Rust 中的数组是固定长度的:一旦声明,它们的长度不能增长或缩小。
|
||||
|
||||
Rust 中数组的值位于中括号中的逗号分隔的列表中:
|
||||
Rust 中,数组中的值位于中括号内的逗号分隔的列表中:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -208,9 +208,9 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
当你想要在栈(stack)而不是在堆(heap)上为数据分配空间(第四章将讨论栈与堆的更多内容),或者是想要确保总是有固定数量的元素时,数组非常有用,虽然它并不如 vector 类型那么灵活。vector 类型是标准库提供的一个 **允许** 增长和缩小长度的类似数组的集合类型。当不确定是应该使用数组还是 vector 的时候,你可能应该使用 vector。第八章会详细讨论 vector。
|
||||
当你想要在栈(stack)而不是在堆(heap)上为数据分配空间(第四章将讨论栈与堆的更多内容),或者是想要确保总是有固定数量的元素时,数组非常有用。但是数组并不如 vector 类型灵活。vector 类型是标准库提供的一个 **允许** 增长和缩小长度的类似数组的集合类型。当不确定是应该使用数组还是 vector 的时候,你可能应该使用 vector。第八章会详细讨论 vector。
|
||||
|
||||
一个你可能想要使用数组而不是 vector 的例子是,当程序需要知道一年中月份的名字时,程序不大可能会去增加或减少月份。这时你可以使用数组,因为我们知道它总是含有 12 个元素:
|
||||
一个你可能想要使用数组而不是 vector 的例子是,当程序需要知道一年中月份的名字时。程序不大可能会去增加或减少月份。这时你可以使用数组,因为我们知道它总是包含 12 个元素:
|
||||
|
||||
```rust
|
||||
let months = ["January", "February", "March", "April", "May", "June", "July",
|
||||
@ -236,7 +236,7 @@ fn main() {
|
||||
|
||||
##### 无效的数组元素访问
|
||||
|
||||
如果我们访问数组结尾之后的元素会发生什么呢?比如我们将上面的例子改成下面这样,这可以编译不过在运行时会因错误而退出:
|
||||
如果我们访问数组结尾之后的元素会发生什么呢?比如你将上面的例子改成下面这样,这可以编译不过在运行时会因错误而退出:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -263,6 +263,6 @@ thread '<main>' panicked at 'index out of bounds: the len is 5 but the index is
|
||||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||||
```
|
||||
|
||||
编译并没有产生任何错误,不过程序会导致一个 **运行时**(*runtime*)错误并且不会成功退出。当尝试用索引访问一个元素时,Rust 会检查指定的索引是否小于数组的长度。如果索引超出了数组长度,Rust 会 *panic*,这是 Rust 中的术语,它用于程序因为错误而退出的情况。
|
||||
编译并没有产生任何错误,不过程序会出现一个 **运行时**(*runtime*)错误并且不会成功退出。当尝试用索引访问一个元素时,Rust 会检查指定的索引是否小于数组的长度。如果索引超出了数组长度,Rust 会 *panic*,这是 Rust 术语,它用于程序因为错误而退出的情况。
|
||||
|
||||
这是第一个在实战中遇到的 Rust 安全原则的例子。在很多底层语言中,并没有进行这类检查,这样当提供了一个不正确的索引时,就会访问无效的内存。Rust 通过立即退出而不是允许内存访问并继续执行来使你免受这类错误困扰。第九章会讨论更多 Rust 的错误处理。
|
||||
这是第一个在实战中遇到的 Rust 安全原则的例子。在很多底层语言中,并没有进行这类检查,这样当提供了一个不正确的索引时,就会访问无效的内存。通过立即退出而不是允许内存访问并继续执行,Rust 让你避开此类错误。第九章会讨论更多 Rust 的错误处理。
|
||||
|
@ -1,12 +1,12 @@
|
||||
## 函数如何工作
|
||||
## 函数
|
||||
|
||||
> [ch03-03-how-functions-work.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch03-03-how-functions-work.md)
|
||||
> <br>
|
||||
> commit 6aad5008b69078a2fc18e6dd7e00ef395170c749
|
||||
> commit f949ff883628db8ed2f2f5f19e146ebf19ed6a6f
|
||||
|
||||
函数在 Rust 代码中应用广泛。你已经见过一个语言中最重要的函数:`main` 函数,它是很多程序的入口点。你也见过了 `fn` 关键字,它用来声明新函数。
|
||||
函数遍布于 Rust 代码中。你已经见过语言中最重要的函数之一:`main` 函数,它是很多程序的入口点。你也见过 `fn` 关键字,它用来声明新函数。
|
||||
|
||||
Rust 代码使用 *snake case* 作为函数和变量名称的规范风格。在 snake case 中,所有字母都是小写并使用下划线分隔单词。这里是一个包含函数定义的程序的例子:
|
||||
Rust 代码中的函数和变量名使用 *snake case* 规范风格。在 snake case 中,所有字母都是小写并使用下划线分隔单词。这是一个包含函数定义示例的程序:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -22,11 +22,11 @@ fn another_function() {
|
||||
}
|
||||
```
|
||||
|
||||
Rust 中的函数定义以 `fn` 开始并在函数名后跟一对括号。大括号告诉编译器哪里是函数体的开始和结尾。
|
||||
Rust 中的函数定义以 `fn` 开始并在函数名后跟一对圆括号。大括号告诉编译器哪里是函数体的开始和结尾。
|
||||
|
||||
可以使用定义过的函数名后跟括号来调用任意函数。因为 `another_function` 已经在程序中定义过了,它可以在 `main` 函数中被调用。注意,源码中 `another_function` 在 `main` 函数 **之后** 被定义;也可以在其之前定义。Rust 不关心函数定义于何处,只要它们被定义了。
|
||||
可以使用函数名后跟圆括号来调用我们定义过的任意函数。因为程序中已定义 `another_function` 函数,所以可以在 `main` 函数中调用它。注意,源码中 `another_function` 定义在 `main` 函数 **之后**;也可以定义在之前。Rust 不关心函数定义于何处,只要定义了就行。
|
||||
|
||||
让我们开始一个叫做 *functions* 的新二进制项目来进一步探索函数。将上面的 `another_function` 例子写入 *src/main.rs* 中并运行。你应该会看到如下输出:
|
||||
让我们新建一个叫做 *functions* 的二进制项目来进一步探索函数。将上面的 `another_function` 例子写入 *src/main.rs* 中并运行。你应该会看到如下输出:
|
||||
|
||||
```text
|
||||
$ cargo run
|
||||
@ -37,13 +37,13 @@ Hello, world!
|
||||
Another function.
|
||||
```
|
||||
|
||||
代码在 `main` 函数中按照它们出现的顺序被执行。首先,打印 “Hello, world!” 信息,接着 `another_function` 被调用并打印它的信息。
|
||||
`main` 函数中的代码会按顺序执行。首先,打印 “Hello, world!” 信息,然后调用 `another_function` 函数并打印它的信息。
|
||||
|
||||
### 函数参数
|
||||
|
||||
函数也可以被定义为拥有 **参数**(*parameters*),它们是作为函数签名一部分的特殊变量。当函数拥有参数时,可以为这些参数提供具体的值。技术上讲,这些具体值被称为参数(*arguments*),不过通常的习惯是倾向于在函数定义中的变量和调用函数时传递的具体值都可以用 “parameter” 和 “argument” 而不加区别。
|
||||
函数也可以被定义为拥有 **参数**(*parameters*),参数是特殊变量,是函数签名的一部分。当函数拥有参数(形参)时,可以为这些参数提供具体的值(实参)。技术上讲,这些具体值被称为参数(*arguments*),但是在闲谈时,人们倾向于互换着使用 *parameter* 和 *argument* 来表示函数定义中的变量或调用函数时传入的具体值。
|
||||
|
||||
如下被重写的 `another_function` 版本展示了 Rust 中参数是什么样的:
|
||||
下面被重写的 `another_function` 版本展示了 Rust 中参数是什么样的:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -57,7 +57,7 @@ fn another_function(x: i32) {
|
||||
}
|
||||
```
|
||||
|
||||
尝试运行程序,将会得到如下输出:
|
||||
尝试运行程序,将会输出如下内容:
|
||||
|
||||
```text
|
||||
$ cargo run
|
||||
@ -67,11 +67,11 @@ $ cargo run
|
||||
The value of x is: 5
|
||||
```
|
||||
|
||||
`another_function` 的声明有一个叫做 `x` 的参数。`x` 的类型被指定为 `i32`。当 `5` 被传递给 `another_function` 时,`println!` 宏将 `5` 放入格式化字符串中大括号的位置。
|
||||
`another_function` 的声明中有一个命名为 `x` 的参数。`x` 的类型被指定为 `i32`。当将 `5` 传给 `another_function` 时,`println!` 宏将 `5` 放入格式化字符串中大括号的位置。
|
||||
|
||||
在函数签名中 **必须** 声明每个参数的类型。这是 Rust 设计中一个经过慎重考虑的决定:要求在函数定义中提供类型注解意味着编译器不需要在别的地方要求你注明类型就能知道你的意图。
|
||||
在函数签名中,**必须** 声明每个参数的类型。这是 Rust 设计中一个经过慎重考虑的决定:要求在函数定义中提供类型注解,意味着编译器不需要你在代码的其他地方注明类型来指出你的意图。
|
||||
|
||||
当一个函数有多个参数时,使用逗号隔开它们,像这样:
|
||||
当一个函数有多个参数时,使用逗号分隔,像这样:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -86,7 +86,7 @@ fn another_function(x: i32, y: i32) {
|
||||
}
|
||||
```
|
||||
|
||||
这个例子创建了一个有两个参数的函数,都是 `i32` 类型的。函数打印出了这两个参数的值。注意函数参数并不一定都是相同类型的,这个例子中它们只是碰巧相同罢了。
|
||||
这个例子创建了有两个参数的函数,都是 `i32` 类型。函数打印出了这两个参数的值。注意函数的参数类型并不一定相同,这个例子中只是碰巧相同罢了。
|
||||
|
||||
尝试运行代码。使用上面的例子替换当前 *functions* 项目的 *src/main.rs* 文件,并用 `cargo run` 运行它:
|
||||
|
||||
@ -99,17 +99,15 @@ The value of x is: 5
|
||||
The value of y is: 6
|
||||
```
|
||||
|
||||
因为我们使用 `5` 作为 `x` 的值 `6` 作为 `y` 的值来调用函数,这两个字符串和它们的值被相应打印出来。
|
||||
因为我们使用 `5` 作为 `x` 的值,`6` 作为 `y` 的值来调用函数,因此打印出这两个字符串及相应的值。
|
||||
|
||||
### 函数体
|
||||
### 包含语句和表达式的函数体
|
||||
|
||||
函数体由一系列的语句和一个可选的表达式构成。目前为止,我们只涉及到了没有结尾表达式的函数,不过我们见过表达式作为了语句的一部分。因为 Rust 是一个基于表达式(expression-based)的语言,这是一个需要理解的(不同于其他语言)重要区别。其他语言并没有这样的区别,所以让我们看看语句与表达式有什么区别以及它们是如何影响函数体的。
|
||||
函数体由一系列的语句和一个可选的结尾表达式构成。目前为止,我们只介绍了没有结尾表达式的函数,不过你已经见过作为语句一部分的表达式。因为 Rust 是一门基于表达式(expression-based)的语言,这是一个需要理解的(不同于其他语言)重要区别。其他语言并没有这样的区别,所以让我们看看语句与表达式有什么区别以及这些区别是如何影响函数体的。
|
||||
|
||||
### 语句与表达式
|
||||
实际上,我们已经使用过语句和表达式。**语句**(*Statements*)是执行一些操作但不返回值的指令。表达式(*Expressions*)计算并产生一个值。让我们看一些例子:
|
||||
|
||||
我们已经用过语句与表达式了。**语句**(*Statements*)是执行一些操作但不返回值的指令。表达式(*Expressions*)计算并产生一个值。让我们看一些例子:
|
||||
|
||||
使用 `let` 关键字创建变量并绑定一个值是一个语句。在列表 3-1 中,`let y = 6;` 是一个语句:
|
||||
使用 `let` 关键字创建变量并绑定一个值是一个语句。在列表 3-1 中,`let y = 6;` 是一个语句。
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -123,7 +121,7 @@ fn main() {
|
||||
|
||||
函数定义也是语句,上面整个例子本身就是一个语句。
|
||||
|
||||
语句并不返回值。因此,不能把 `let` 语句赋值给另一个变量,比如下面的例子尝试做的,这会产生一个错误:
|
||||
语句不返回值。因此,不能把 `let` 语句赋值给另一个变量,比如下面的例子尝试做的,会产生一个错误:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -133,7 +131,7 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
当运行这个程序,会得到如下错误:
|
||||
当运行这个程序时,会得到如下错误:
|
||||
|
||||
```text
|
||||
$ cargo run
|
||||
@ -147,9 +145,9 @@ error: expected expression, found statement (`let`)
|
||||
= note: variable declaration using `let` is a statement
|
||||
```
|
||||
|
||||
`let y = 6` 语句并不返回值,所以并没有 `x` 可以绑定的值。这与其他语言不同,例如 C 和 Ruby,它们的赋值语句返回所赋的值。在这些语言中,可以这么写 `x = y = 6` 这样 `x` 和 `y` 的值都是 `6`;这在 Rust 中可不行。
|
||||
`let y = 6` 语句并不返回值,所以没有可以绑定到 `x` 上的值。这与其他语言不同,例如 C 和 Ruby,它们的赋值语句会返回所赋的值。在这些语言中,可以这么写 `x = y = 6`,这样 `x` 和 `y` 的值都是 `6`;Rust 中不能这样写。
|
||||
|
||||
表达式计算出一些值,而且它们组成了其余大部分你将会编写的 Rust 代码。考虑一个简单的数学运算,比如 `5 + 6`,这是一个表达式并计算出值 `11`。表达式可以是语句的一部分:在列表 3-3 中有这个语句 `let y = 6;`,`6` 是一个表达式,它计算出的值是 `6`。函数调用是一个表达式。宏调用是一个表达式。我们用来创建新作用域的大括号(代码块),`{}`,也是一个表达式,例如:
|
||||
表达式会计算出一些值,并且你将编写的大部分 Rust 代码是由表达式组成的。考虑一个简单的数学运算,比如 `5 + 6`,这是一个表达式并计算出值 `11`。表达式可以是语句的一部分:在示例 3-1 中,语句 `let y = 6;` 中的 `6` 是一个表达式,它计算出的值是 `6`。函数调用是一个表达式。宏调用是一个表达式。我们用来创建新作用域的大括号(代码块),`{}`,也是一个表达式,例如:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -175,11 +173,11 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
是一个代码块,它的值是 `4`。这个值作为 `let` 语句的一部分被绑定到 `y` 上。注意结尾没有分号的那一行,与大部分我们见过的代码行不同。表达式并不包含结尾的分号。如果在表达式的结尾加上分号,他就变成了语句,这也就使其不返回一个值。在接下来的探索中记住函数和表达式都返回值就行了。
|
||||
是一个代码块,它的值是 `4`。这个值作为 `let` 语句的一部分被绑定到 `y` 上。注意结尾没有分号的那一行 `x+1`,与你见过的大部分代码行不同。表达式的结尾没有分号。如果在表达式的结尾加上分号,它就变成了语句,而语句不会返回值。在接下来探索具有返回值的函数和表达式时要谨记这一点。
|
||||
|
||||
### 函数的返回值
|
||||
### 具有返回值的函数
|
||||
|
||||
函数可以向调用它的代码返回值。我们并不对返回值命名,不过会在一个箭头(`->`)后声明它的类型。在 Rust 中,函数的返回值等同于函数体最后一个表达式的值。这是一个有返回值的函数的例子:
|
||||
函数可以向调用它的代码返回值。我们并不对返回值命名,但要在箭头(`->`)后声明它的类型。在 Rust 中,函数的返回值等同于函数体最后一个表达式的值。使用 `return` 关键字和指定值,可从函数中提前返回;但大部分函数隐式的返回最后的表达式。这是一个有返回值的函数的例子:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -195,7 +193,7 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
在函数 `five` 中并没有函数调用、宏、甚至也没有 `let` 语句————只有数字 `5` 自身。这在 Rust 中是一个完全有效的函数。注意函数的返回值类型也被指定了,就是 `-> i32`。尝试运行代码;输出应该看起来像这样:
|
||||
在 `five` 函数中没有函数调用、宏、甚至没有 `let` 语句——只有数字 `5`。这在 Rust 中是一个完全有效的函数。注意,也指定了函数返回值的类型,就是 `-> i32`。尝试运行代码;输出应该看起来像这样:
|
||||
|
||||
```text
|
||||
$ cargo run
|
||||
@ -205,13 +203,15 @@ $ cargo run
|
||||
The value of x is: 5
|
||||
```
|
||||
|
||||
函数 `five` 的返回值是 `5`,也就是为什么返回值类型是 `i32`。让我们仔细检查一下这段代码。这有两个重要的部分:首先,`let x = five();` 这一行表明我们使用函数的返回值来初始化了一个变量。因为函数 `five` 返回 `5`,这一行与如下这行相同:
|
||||
`five` 函数的返回值是 `5`,所以返回值类型是 `i32`。让我们仔细检查一下这段代码。有两个重要的部分:首先,`let x = five();` 这一行表明我们使用函数的返回值初始化一个变量。因为 `five` 函数返回 `5`,这一行与如下代码相同:
|
||||
|
||||
```rust
|
||||
let x = 5;
|
||||
```
|
||||
|
||||
其次,函数 `five` 没有参数并定义了返回值类型,不过函数体只有单单一个 `5` 也没有分号,因为这是我们想要返回值的表达式。让我们看看另一个例子:
|
||||
其次,`five` 函数没有参数并定义了返回值类型,不过函数体只有单单一个 `5` 也没有分号,因为这是一个表达式,我们想要返回它的值。
|
||||
|
||||
让我们看看另一个例子:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -227,7 +227,7 @@ fn plus_one(x: i32) -> i32 {
|
||||
}
|
||||
```
|
||||
|
||||
运行代码会打印出 `The value of x is: 6`。如果在包含 `x + 1` 的那一行的结尾加上一个分号,把它从表达式变成语句后会怎样呢?
|
||||
运行代码会打印出 `The value of x is: 6`。但如果在包含 `x + 1` 的行尾加上一个分号,把它从表达式变成语句,我们将看到一个错误。
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -260,4 +260,4 @@ error[E0308]: mismatched types
|
||||
found type `()`
|
||||
```
|
||||
|
||||
主要的错误信息,“mismatched types”(类型不匹配),揭示了代码的核心问题。函数 `plus_one` 的定义说明它要返回一个 `i32`,不过语句并不返回一个值,这由那个空元组 `()` 表明。因此,这个函数返回了空元组 `()`,这与函数定义相矛盾并导致一个错误。在输出中,Rust 提供了一个可能会对修正问题有帮助的信息:它建议去掉分号,这会修复这个错误。
|
||||
主要的错误信息,“mismatched types”(类型不匹配),揭示了代码的核心问题。函数 `plus_one` 的定义说明它要返回一个 `i32` 类型的值,不过语句并不会返回值,使用空元组 `()` 表示不返回值。因为不返回值与函数定义相矛盾,从而出现一个错误。在输出中,Rust 提供了一条信息,可能有助于纠正这个错误:它建议删除分号,这会修复这个错误。
|
||||
|
@ -2,17 +2,17 @@
|
||||
|
||||
> [ch03-04-comments.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch03-04-comments.md)
|
||||
> <br>
|
||||
> commit d4c77666f480edfb960cc9b11a31c42f4b90c745
|
||||
> commit f949ff883628db8ed2f2f5f19e146ebf19ed6a6f
|
||||
|
||||
所有编程语言都力求使其代码易于理解,不过有时需要提供额外的解释。在这种情况下,程序员在源码中留下记录,或者 **注释**(*comments*),编译器会忽略它们不过其他阅读代码的人可能会用得上。
|
||||
所有程序员都力求使其代码易于理解,不过有时还需要提供额外的解释。在这种情况下,程序员在源码中留下记录,或者 **注释**(*comments*),编译器会忽略它们,不过阅读代码的人可能觉得有用。
|
||||
|
||||
这是一个简单的注释的例子:
|
||||
这是一个简单的注释:
|
||||
|
||||
```rust
|
||||
// Hello, world.
|
||||
// hello, world
|
||||
```
|
||||
|
||||
在 Rust 中,注释必须以两道斜杠开始,并持续到本行的结尾。对于超过一行的注释,需要在每一行都加上 `//`,像这样:
|
||||
在 Rust 中,注释必须以两道斜杠开始,并持续到本行的结尾。对于超过一行的注释,需要在每一行前都加上 `//`,像这样:
|
||||
|
||||
```rust
|
||||
// So we’re doing something complicated here, long enough that we need
|
||||
@ -26,17 +26,17 @@
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let lucky_number = 7; // I’m feeling lucky today.
|
||||
let lucky_number = 7; // I’m feeling lucky today
|
||||
}
|
||||
```
|
||||
|
||||
不过你会经常看到它们用于这种格式,也就是位于它所解释的代码行的上面一行:
|
||||
不过你更经常看到以这种格式使用它们,也就是位于它所解释的代码行的上面一行:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
// I’m feeling lucky today.
|
||||
// I’m feeling lucky today
|
||||
let lucky_number = 7;
|
||||
}
|
||||
```
|
||||
|
@ -2,15 +2,15 @@
|
||||
|
||||
> [ch03-05-control-flow.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch03-05-control-flow.md)
|
||||
> <br>
|
||||
> commit ec65990849230388e4ce4db5b7a0cb8a0f0d60e2
|
||||
> commit f949ff883628db8ed2f2f5f19e146ebf19ed6a6f
|
||||
|
||||
通过条件是不是为真来决定是否执行某些代码,或者根据条件是否为真来重复运行一段代码是大部分编程语言的基本组成部分。Rust 代码中最常见的用来控制执行流的结构是 `if` 表达式和循环。
|
||||
根据条件是否为真来决定是否执行某些代码,以及根据条件是否为真来重复运行一段代码是大部分编程语言的基本组成部分。Rust 代码中最常见的用来控制执行流的结构是 `if` 表达式和循环。
|
||||
|
||||
### `if` 表达式
|
||||
|
||||
`if` 表达式允许根据条件执行不同的代码分支。我们提供一个条件并表示 “如果符合这个条件,运行这段代码。如果条件不满足,不运行这段代码。”
|
||||
`if` 表达式允许根据条件执行不同的代码分支。你提供一个条件并表示 “如果条件满足,运行这段代码;如果条件不满足,不运行这段代码。”
|
||||
|
||||
在 *projects* 目录创建一个叫做 *branches* 的新项目来学习 `if` 表达式。在 *src/main.rs* 文件中,输入如下内容:
|
||||
在 *projects* 目录新建一个叫做 *branches* 的项目,来学习 `if` 表达式。在 *src/main.rs* 文件中,输入如下内容:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -28,7 +28,9 @@ fn main() {
|
||||
|
||||
<!-- NEXT PARAGRAPH WRAPPED WEIRD INTENTIONALLY SEE #199 -->
|
||||
|
||||
所有的 `if` 表达式都以 `if` 关键字开头,其后跟一个条件。在这个例子中,条件检查变量 `number` 是否有一个小于 5 的值。在条件为真时希望执行的代码块位于紧跟条件之后的大括号中。`if` 表达式中与条件关联的代码块有时被叫做 *arms*,就像第二章 “比较猜测与秘密数字” 部分中讨论到的 `match` 表达式中分支一样。也可以包含一个可选的 `else` 表达式来提供一个在条件为假时应当执行的代码块,这里我们就这么做了。如果不提供 `else` 表达式并且条件为假时,程序会直接忽略 `if` 代码块并继续执行下面的代码。
|
||||
所有的 `if` 表达式都以 `if` 关键字开头,其后跟一个条件。在这个例子中,条件检查变量 `number` 的值是否小于 5。在条件为真时希望执行的代码块位于紧跟条件之后的大括号中。`if` 表达式中与条件关联的代码块有时被叫做 *arms*,就像第二章 “比较猜测的数字和秘密数字” 部分中讨论到的 `match` 表达式中的分支一样。
|
||||
|
||||
也可以包含一个可选的 `else` 表达式来提供一个在条件为假时应当执行的代码块,这里我们就这么做了。如果不提供 `else` 表达式并且条件为假时,程序会直接忽略 `if` 代码块并继续执行下面的代码。
|
||||
|
||||
尝试运行代码,应该能看到如下输出:
|
||||
|
||||
@ -56,7 +58,7 @@ $ cargo run
|
||||
condition was false
|
||||
```
|
||||
|
||||
另外值得注意的是代码中的条件 **必须** 是 `bool` 值。如果想看看条件不是 `bool` 值时会发生什么,尝试运行如下代码:
|
||||
另外值得注意的是代码中的条件 **必须** 是 `bool` 值。如果条件不是 `bool` 值,我们将得到一个错误。例如,尝试运行以下代码:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -83,7 +85,7 @@ error[E0308]: mismatched types
|
||||
found type `{integer}`
|
||||
```
|
||||
|
||||
这个错误表明 Rust 期望一个 `bool` 不过却得到了一个整型。不像 Ruby 或 JavaScript 这样的语言,Rust 并不会尝试自动地将非布尔值转换为布尔值。必须总是显式地使用布尔值作为 `if` 的条件。例如,如果想要 `if` 代码块只在一个数字不等于 `0` 时执行,可以把 `if` 表达式修改成下面这样:
|
||||
这个错误表明 Rust 期望一个 `bool` 却得到了一个整数。不像 Ruby 或 JavaScript 这样的语言,Rust 并不会尝试自动地将非布尔值转换为布尔值。必须总是显式地使用布尔值作为 `if` 的条件。例如,如果想要 `if` 代码块只在一个数字不等于 `0` 时执行,可以把 `if` 表达式修改成下面这样:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -99,7 +101,7 @@ fn main() {
|
||||
|
||||
运行代码会打印出 `number was something other than zero`。
|
||||
|
||||
#### 使用 `else if` 实现多重条件
|
||||
#### 使用 `else if` 处理多重条件
|
||||
|
||||
可以将 `else if` 表达式与 `if` 和 `else` 组合来实现多重条件。例如:
|
||||
|
||||
@ -131,9 +133,9 @@ $ cargo run
|
||||
number is divisible by 3
|
||||
```
|
||||
|
||||
当执行这个程序,它按顺序检查每个 `if` 表达式并执行第一个条件为真的代码块。注意即使 6 可以被 2 整除,也不会出现 `number is divisible by 2` 的输出,更不会出现 `else` 块中的 `number is not divisible by 4, 3, or 2`。原因是 Rust 只会执行第一个条件为真的代码块,并且一旦它找到一个以后,甚至就不会检查剩下的条件了。
|
||||
当执行这个程序时,它按顺序检查每个 `if` 表达式并执行第一个条件为真的代码块。注意即使 6 可以被 2 整除,也不会输出 `number is divisible by 2`,更不会输出 `else` 块中的 `number is not divisible by 4, 3, or 2`。原因是 Rust 只会执行第一个条件为真的代码块,并且一旦它找到一个以后,甚至都不会检查剩下的条件了。
|
||||
|
||||
使用过多的 `else if` 表达式会使代码显得杂乱无章,所以如果有多于一个 `else if`,最好重构代码。为此第六章会介绍 Rust 中一个叫做 `match` 的强大的分支结构(branching construct)。
|
||||
使用过多的 `else if` 表达式会使代码显得杂乱无章,所以如果有多于一个 `else if` 表达式,最好重构代码。为此,第六章会介绍一个强大的 Rust 分支结构(branching construct),叫做 `match`。
|
||||
|
||||
#### 在 `let` 语句中使用 `if`
|
||||
|
||||
@ -154,9 +156,9 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 3-2:将 `if` 的返回值赋值给一个变量</span>
|
||||
<span class="caption">示例 3-2:将 `if` 表达式的返回值赋给一个变量</span>
|
||||
|
||||
`number` 变量将会绑定到基于 `if` 表达式结果的值。运行这段代码看看会出现什么:
|
||||
`number` 变量将会绑定到表示 `if` 表达式结果的值上。运行这段代码看看会出现什么:
|
||||
|
||||
```text
|
||||
$ cargo run
|
||||
@ -166,7 +168,7 @@ $ cargo run
|
||||
The value of number is: 5
|
||||
```
|
||||
|
||||
还记得代码块的值是其最后一个表达式的值,以及数字本身也是一个表达式吗。在这个例子中,整个 `if` 表达式的值依赖哪个代码块被执行。这意味着 `if` 的每个分支的可能的返回值都必须是相同类型;在示例 3-2 中,`if` 分支和 `else` 分支的结果都是 `i32` 整型。如果它们的类型不匹配,如下面这个例子,则会出现一个错误:
|
||||
记住,代码块的值是其最后一个表达式的值,而数字本身就是一个表达式。在这个例子中,整个 `if` 表达式的值取决于哪个代码块被执行。这意味着 `if` 的每个分支的可能的返回值都必须是相同类型;在示例 3-2 中,`if` 分支和 `else` 分支的结果都是 `i32` 整型。如果它们的类型不匹配,如下面这个例子,则会出现一个错误:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -184,7 +186,7 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
当运行这段代码,会得到一个错误。`if` 和 `else` 分支的值类型是不相容的,同时 Rust 也准确地表明了在程序中的何处发现的这个问题:
|
||||
当编译这段代码时,会得到一个错误。`if` 和 `else` 分支的值类型是不相容的,同时 Rust 也准确地指出在程序中的何处发现的这个问题:
|
||||
|
||||
```text
|
||||
error[E0308]: if and else have incompatible types
|
||||
@ -196,19 +198,19 @@ error[E0308]: if and else have incompatible types
|
||||
6 | | } else {
|
||||
7 | | "six"
|
||||
8 | | };
|
||||
| |_____^ expected integral variable, found reference
|
||||
| |_____^ expected integral variable, found &str
|
||||
|
|
||||
= note: expected type `{integer}`
|
||||
found type `&str`
|
||||
```
|
||||
|
||||
`if` 代码块的表达式返回一个整型,而 `else` 代码块返回一个字符串。这并不可行,因为变量必须只有一个类型。Rust 需要在编译时就确切的知道 `number` 变量的类型,这样它就可以在编译时证明其他使用 `number` 变量的地方它的类型是有效的。Rust 并不能够在 `number` 的类型只能在运行时确定的情况下工作;这样会使编译器变得更复杂而且只能为代码提供更少的保障,因为它不得不记录所有变量的多种可能的类型。
|
||||
`if` 代码块中的表达式返回一个整数,而 `else` 代码块中的表达式返回一个字符串。这不可行,因为变量必须只有一个类型。Rust 需要在编译时就确切的知道 `number` 变量的类型,这样它就可以在编译时验证在每处使用的 `number` 变量的类型是有效的。Rust 并不能够在 `number` 的类型只能在运行时确定的情况下工作;这样会使编译器变得更复杂而且只能为代码提供更少的保障,因为它不得不记录所有变量的多种可能的类型。
|
||||
|
||||
### 使用循环重复执行
|
||||
|
||||
多次执行同一段代码是很常用的,Rust 为此提供了多种 **循环**(*loops*)。一个循环执行循环体中的代码直到结尾并紧接着回到开头继续执行。为了实验一下循环,让我们创建一个叫做 *loops* 的新项目。
|
||||
多次执行同一段代码是很常用的,Rust 为此提供了多种 **循环**(*loops*)。一个循环执行循环体中的代码直到结尾并紧接着回到开头继续执行。为了实验一下循环,让我们新建一个叫做 *loops* 的项目。
|
||||
|
||||
Rust 有三种循环类型:`loop`、`while` 和 `for`。让我们每一个都试试。
|
||||
Rust 有三种循环:`loop`、`while` 和 `for`。我们每一个都试试。
|
||||
|
||||
#### 使用 `loop` 重复执行代码
|
||||
|
||||
@ -226,7 +228,7 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
当执行这个程序,我们会看到 `again!` 被连续的打印直到我们手动停止程序。大部分终端都支持一个键盘快捷键,<span class="keystroke">ctrl-C</span>,来终止一个陷入无限循环的程序。尝试一下:
|
||||
当运行这个程序时,我们会看到连续的反复打印 `again!`,直到我们手动停止程序。大部分终端都支持一个快捷键,<span class="keystroke">ctrl-c</span>,来终止一个陷入无限循环的程序。尝试一下:
|
||||
|
||||
```text
|
||||
$ cargo run
|
||||
@ -240,15 +242,15 @@ again!
|
||||
^Cagain!
|
||||
```
|
||||
|
||||
符号 `^C` 代表你在这按下了<span class="keystroke">ctrl-C</span>。在 `^C` 之后你可能看到也可能看不到 `again!` ,这依赖于在接收到终止信号时代码执行到了循环的何处。
|
||||
符号 `^C` 代表你在这按下了<span class="keystroke">ctrl-c</span>。在 `^C` 之后你可能看到也可能看不到 `again!` ,这取决于在接收到终止信号时代码执行到了循环的何处。
|
||||
|
||||
幸运的是,Rust 提供了另一个更可靠的方式来退出循环。可以使用 `break` 关键字来告诉程序何时停止执行循环。回忆一下在第二章猜猜看游戏的 “猜测正确后退出” 部分使用过它来在用户猜对数字赢得游戏后退出程序。
|
||||
幸运的是,Rust 提供了另一种更可靠的退出循环的方式。可以使用 `break` 关键字来告诉程序何时停止循环。回忆一下在第二章猜猜看游戏的 “猜测正确后退出” 部分使用过它来在用户猜对数字赢得游戏后退出程序。
|
||||
|
||||
#### `while` 条件循环
|
||||
|
||||
在程序中计算循环的条件也很常见。当条件为真,执行循环。当条件不再为真,调用 `break` 停止循环。这个循环类型可以通过组合 `loop`、`if`、`else` 和 `break` 来实现;如果你喜欢的话,现在就可以在程序中试试。
|
||||
|
||||
然而,这个模式太常见了以至于 Rust 为此提供了一个内建的语言结构,它被称为 `while` 循环。下面的例子使用了 `while`:程序循环三次,每次数字都减一。接着,在循环之后,打印出另一个信息并退出:
|
||||
然而,这个模式太常用了,Rust 为此内置了一个语言结构,它被称为 `while` 循环。示例 3-3 使用了 `while`:程序循环三次,每次数字都减一。接着,在循环结束后,打印出另一个信息并退出。
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -266,11 +268,13 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
这个结构消除了很多需要嵌套使用 `loop`、`if`、`else` 和 `break` 的代码,这样显得更加清楚。当条件为真就执行,否则退出循环。
|
||||
<span class="caption">示例 3-3: 当条件为真时,使用 `while` 循环运行代码</span>
|
||||
|
||||
这种结构消除了使用 `loop`、`if`、`else` 和 `break` 时必须有的很多嵌套,代码更加清晰。当条件为真就执行,否则退出循环。
|
||||
|
||||
#### 使用 `for` 遍历集合
|
||||
|
||||
可以使用 `while` 结构来遍历一个元素集合,比如数组。例如,看看如下的示例 3-3:
|
||||
可以使用 `while` 结构来遍历集合中的元素,比如数组。例如,看看示例 3-4。
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -287,9 +291,9 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 3-3:使用 `while` 循环遍历集合中的每一个元素</span>
|
||||
<span class="caption">示例 3-4:使用 `while` 循环遍历集合中的元素</span>
|
||||
|
||||
这里代码对数组中的元素进行计数。它从索引 `0` 开始,并接着循环直到遇到数组的最后一个索引(这时,`index < 5` 不再为真)。运行这段代码会打印出数组中的每一个元素:
|
||||
这里,代码对数组中的元素进行计数。它从索引 `0` 开始,并接着循环直到遇到数组的最后一个索引(这时,`index < 5` 不再为真)。运行这段代码会打印出数组中的每一个元素:
|
||||
|
||||
```text
|
||||
$ cargo run
|
||||
@ -303,11 +307,11 @@ the value is: 40
|
||||
the value is: 50
|
||||
```
|
||||
|
||||
所有数组中的五个元素都如期被打印出来。尽管 `index` 在某一时刻会到达值 `5`,不过循环在其尝试从数组获取第六个值(会越界)之前就停止了。
|
||||
数组中的所有五个元素都如期被打印出来。尽管 `index` 在某一时刻会到达值 `5`,不过循环在其尝试从数组获取第六个值(会越界)之前就停止了。
|
||||
|
||||
不过这个过程是容易出错的;如果索引长度不正确会导致程序 panic。这也使程序更慢,因为编译器增加了运行时代码来对每次循环的每个元素进行条件检查。
|
||||
但这个过程很容易出错;如果索引长度不正确会导致程序 panic。这也使程序更慢,因为编译器增加了运行时代码来对每次循环的每个元素进行条件检查。
|
||||
|
||||
可以使用 `for` 循环来对一个集合的每个元素执行一些代码,来作为一个更有效率的替代。`for` 循环看起来如示例 3-4 所示:
|
||||
作为更简洁的替代方案,可以使用 `for` 循环来对一个集合的每个元素执行一些代码。`for` 循环看起来如示例 3-5 所示:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -321,13 +325,13 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 3-4:使用 `for` 循环遍历集合中的每一个元素</span>
|
||||
<span class="caption">示例 3-5:使用 `for` 循环遍历集合中的元素</span>
|
||||
|
||||
当运行这段代码,将看到与示例 3-3 一样的输出。更为重要的是,我们增强了代码安全性并消除了出现可能会导致超出数组的结尾或遍历长度不够而缺少一些元素这类 bug 的机会。
|
||||
当运行这段代码时,将看到与示例 3-4 一样的输出。更为重要的是,我们增强了代码安全性,并消除了可能由于超出数组的结尾或遍历长度不够而缺少一些元素而导致的 bug。
|
||||
|
||||
例如,在示例 3-3 的代码中,如果从数组 `a` 中移除一个元素但忘记更新条件为 `while index < 4`,代码将会 panic。使用`for`循环的话,就不需要惦记着在更新数组元素数量时修改其他的代码了。
|
||||
例如,在示例 3-4 的代码中,如果从数组 `a` 中移除一个元素但忘记将条件更新为 `while index < 4`,代码将会 panic。使用 `for` 循环的话,就不需要惦记着在改变数组元素个数时修改其他的代码了。
|
||||
|
||||
`for` 循环的安全性和简洁性使得它成为 Rust 中使用最多的循环结构。即使是在想要循环执行代码特定次数时,例如示例 3-3 中使用 `while` 循环的倒计时例子,大部分 Rustacean 也会使用 `for` 循环。这么做的方式是使用 `Range`,它是标准库提供的用来生成从一个数字开始到另一个数字之前结束的所有数字序列的类型。
|
||||
`for` 循环的安全性和简洁性使得它成为 Rust 中使用最多的循环结构。即使是在想要循环执行代码特定次数时,例如示例 3-3 中使用 `while` 循环的倒计时例子,大部分 Rustacean 也会使用 `for` 循环。这么做的方式是使用 `Range`,它是标准库提供的类型,用来生成从一个数字开始到另一个数字之前结束的所有数字的序列。
|
||||
|
||||
下面是一个使用 `for` 循环来倒计时的例子,它还使用了一个我们还未讲到的方法,`rev`,用来反转 range:
|
||||
|
||||
@ -346,10 +350,10 @@ fn main() {
|
||||
|
||||
## 总结
|
||||
|
||||
你做到了!这是一个大章节:你学习了变量,标量和 `if` 表达式,还有循环!如果你想要实践本章讨论的概念,尝试构建如下的程序:
|
||||
你做到了!这是一个大章节:你学习了变量、标量和复合数据类型、函数、注释、 `if` 表达式和循环!如果你想要实践本章讨论的概念,尝试构建如下程序:
|
||||
|
||||
* 相互转换摄氏与华氏温度
|
||||
* 生成 n 阶斐波那契数列
|
||||
* 打印圣诞颂歌 “The Twelve Days of Christmas” 的歌词,并利用歌曲中的重复部分(编写循环)
|
||||
* 相互转换摄氏与华氏温度。
|
||||
* 生成 n 阶斐波那契数列。
|
||||
* 打印圣诞颂歌 “The Twelve Days of Christmas” 的歌词,并利用歌曲中的重复部分(编写循环)。
|
||||
|
||||
当你准备好继续的时候,让我们讨论一个其他语言中 *并不* 常见的概念:所有权(ownership)。
|
||||
|
@ -2,6 +2,6 @@
|
||||
|
||||
> [ch04-00-understanding-ownership.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch04-00-understanding-ownership.md)
|
||||
> <br>
|
||||
> commit 4f2dc564851dc04b271a2260c834643dfd86c724
|
||||
> commit aff4f619c4d6dc138b57b74c3a898ba9bce06649
|
||||
|
||||
所有权(系统)是 Rust 最独特的功能,其令 Rust 无需垃圾回收(garbage collector)即可保障内存安全。因此,理解 Rust 中所有权如何工作是十分重要的。本章我们将讲到所有权以及相关功能:借用、slice 以及 Rust 如何在内存中布局数据。
|
||||
所有权(系统)是 Rust 最独特的功能,其令 Rust 无需垃圾回收(garbage collector)即可保障内存安全。因此,理解 Rust 中所有权如何工作是十分重要的。本章,我们将讲到所有权以及相关功能:借用、slice 以及 Rust 如何在内存中布局数据。
|
@ -2,143 +2,139 @@
|
||||
|
||||
> [ch04-01-what-is-ownership.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch04-01-what-is-ownership.md)
|
||||
> <br>
|
||||
> commit ec65990849230388e4ce4db5b7a0cb8a0f0d60e2
|
||||
> commit f949ff883628db8ed2f2f5f19e146ebf19ed6a6f
|
||||
|
||||
Rust 的核心功能(之一)是 **所有权**(*ownership*)。虽然该功能很容易解释,但它对语言的其他部分有深刻的影响。
|
||||
Rust 的核心功能(之一)是 **所有权**(*ownership*)。虽然该功能很容易解释,但它对语言的其他部分有着深刻的影响。
|
||||
|
||||
所有程序都必须管理其运行时使用计算机内存的方式。一些语言中使用垃圾回收在程序运行过程中来时刻寻找不再被使用的内存;在另一些语言中,程序员必须亲自分配和释放内存。Rust 则选择了第三种方式:内存被一个所有权系统管理,它拥有一系列的规则使编译器在编译时进行检查。任何所有权系统的功能都不会导致运行时开销。
|
||||
所有运行的程序都必须管理其使用计算机内存的方式。一些语言中具有垃圾回收机制,在程序运行时不断地寻找不再使用的内存;在另一些语言中,程序员必须亲自分配和释放内存。Rust 则选择了第三种方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。在运行时,所有权系统的任何功能都不会减慢程序。
|
||||
|
||||
因为所有权对很多程序员来说都是一个新概念,需要一些时间来适应。好消息是随着你对 Rust 和所有权系统的规则越来越有经验,你就越能自然地编写出安全和高效的代码。持之以恒!
|
||||
|
||||
当你理解了所有权,你将有一个坚实的基础来理解使Rust独特的功能。在本章中,你将通过一些常见数据结构的例子来学习所有权:字符串。
|
||||
|
||||
<!-- PROD: START BOX -->
|
||||
当你理解了所有权,你将有一个坚实的基础来理解那些使 Rust 独特的功能。在本章中,你将通过完成一些示例来学习所有权,这些示例基于一个常用的数据结构:字符串。
|
||||
|
||||
> ### 栈(Stack)与堆(Heap)
|
||||
>
|
||||
> 在很多语言中并不经常需要考虑到栈与堆。不过在像 Rust 这样的系统编程语言中,值是位于栈上还是堆上在更大程度上影响了语言的行为以及为何必须做出这样的选择。我们会在本章的稍后部分描述所有权与堆与栈相关的部分,所以这里只是一个用来预热的简要解释。
|
||||
> 在很多语言中,你并不需要经常考虑到栈与堆。不过在像 Rust 这样的系统编程语言中,值是位于栈上还是堆上在更大程度上影响了语言的行为以及为何必须做出这样的抉择。我们会在本章的稍后部分描述所有权与栈和堆相关的内容,所以这里只是一个用来预热的简要解释。
|
||||
>
|
||||
> 栈和堆都是代码在运行时可供使用的内存部分,不过他们以不同的结构组成。栈以放入值的顺序存储并以相反顺序取出值。这也被称作 **后进先出**(*last in, first out*)。想象一下一叠盘子:当增加更多盘子时,把他们放在盘子堆的顶部,当需要盘子时,也从顶部拿走。不能从中间也不能从底部增加或拿走盘子!增加数据叫做 **进栈**(*pushing onto the stack*),而移出数据叫做 **出栈**(*popping off the stack*)。
|
||||
> 栈和堆都是代码在运行时可供使用的内存,但是它们的结构不同。栈以放入值的顺序存储值并以相反顺序取出值。这也被称作 **后进先出**(*last in, first out*)。想象一下一叠盘子:当增加更多盘子时,把它们放在盘子堆的顶部,当需要盘子时,也从顶部拿走。不能从中间也不能从底部增加或拿走盘子!增加数据叫做 **进栈**(*pushing onto the stack*),而移出数据叫做 **出栈**(*popping off the stack*)。
|
||||
>
|
||||
> 栈的操作是十分快速的,这主要是得益于它存取数据的方式:因为数据存取的位置总是在栈顶而不需要寻找一个位置存放或读取数据。另一个栈的属性也让操作栈中数据快速,是因为栈中的所有数据都必须有一个已知且固定的大小。
|
||||
> 栈的操作是十分快速的,这主要是得益于它存取数据的方式:因为数据存取的位置总是在栈顶而不需要寻找一个位置存放或读取数据。另一个让操作栈快速的属性是,栈中的所有数据都必须占用已知且固定的大小。
|
||||
>
|
||||
> 相反对于在编译时未知大小或大小可能变化的数据,可以把他们储存在堆上。堆是缺乏组织的:当向堆放入数据时,我们请求一定大小的空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回给我们一个其位置的 **指针**(*pointer*)。这个过程称作 **在堆上分配内存**(*allocating on the heap*),并且有时这个过程就简称为 “分配”(allocating)。向栈中放入数据并不被认为是分配。因为指针是已知的固定大小的,我们可以将指针储存在栈上,不过当需要实际数据时,必须访问指针。
|
||||
> 在编译时大小未知或大小可能变化的数据,要改为存储在堆上。堆是缺乏组织的:当向堆放入数据时,你要请求一定大小的空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 **指针**(*pointer*)。这个过程称作 **在堆上分配内存**(*allocating on the heap*),有时简称为 “分配”(allocating)。将数据推入栈中并不被认为是分配。因为指针的大小是已知并且固定的,你可以将指针存储在栈上,不过当需要实际数据时,必须访问指针。
|
||||
>
|
||||
> 想象一下去餐馆就坐吃饭。当进入时,你说明有几个人,餐馆员工会找到一个够大的空桌子并领你们过去。如果有人来迟了,他们也可以通过询问来找到你们坐在哪。
|
||||
> 想象一下去餐馆就座吃饭。当进入时,你说明有几个人,餐馆员工会找到一个够大的空桌子并领你们过去。如果有人来迟了,他们也可以通过询问来找到你们坐在哪。
|
||||
>
|
||||
> 访问堆上的数据要比访问栈上的数据要慢因为必须通过指针来访问。现代处理器在内存中跳转越少就越快(缓存)。继续类比,假设有一个服务员在餐厅里处理多个桌子的点菜。在一个桌子报完所有菜后再移动到下一个桌子是最有效率的。从桌子 A 听一个菜,接着桌子 B 听一个菜,然后再桌子 A,然后再桌子 B 这样的流程会更加缓慢。出于同样原因,处理器在处理的数据之间彼此较近的时候(比如在栈上)比较远的时候(比如可能在堆上)能更好的工作。在堆上分配大量的空间也可能消耗时间。
|
||||
> 访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。现代处理器在内存中跳转越少就越快(缓存)。继续类比,假设有一个服务员在餐厅里处理多个桌子的点菜。在一个桌子报完所有菜后再移动到下一个桌子是最有效率的。从桌子 A 听一个菜,接着桌子 B 听一个菜,然后再桌子 A,然后再桌子 B 这样的流程会更加缓慢。出于同样原因,处理器在处理的数据彼此较近的时候(比如在栈上)比较远的时候(比如可能在堆上)能更好的工作。在堆上分配大量的空间也可能消耗时间。
|
||||
>
|
||||
> 当调用一个函数,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。
|
||||
> 当你的代码调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。
|
||||
>
|
||||
> 记录何处的代码在使用堆上的什么数据,最小化堆上的冗余数据的数量以及清理堆上不再使用的数据以致不至于耗尽空间,这些所有的问题正是所有权系统要处理的。一旦理解了所有权,你就不需要经常考虑栈和堆了,不过理解如何管理堆内存可以帮助我们理解所有权为何存在以及为什么要以这种方式工作。
|
||||
|
||||
<!-- PROD: END BOX -->
|
||||
> 跟踪哪部分代码正在使用堆上的哪些数据,最大限度的减少堆上的重复数据的数量,以及清理堆上不再使用的数据确保不会耗尽空间,这些问题正是所有权系统要处理的。一旦理解了所有权,你就不需要经常考虑栈和堆了,不过明白了所有权的存在就是为了管理堆数据,能够帮助解释为什么所有权要以这种方式工作。
|
||||
|
||||
### 所有权规则
|
||||
|
||||
首先,让我们看一下所有权的规则。请记住它们,我们将讲解一些它们的例子:
|
||||
首先,让我们看一下所有权的规则。当我们通过举例说明时,请谨记这些规则:
|
||||
|
||||
> 1. Rust 中每一个值都有一个称之为其 **所有者**(*owner*)的变量。
|
||||
> 2. 值有且只能有一个所有者。
|
||||
> 1. Rust 中的每一个值都有一个被称为其 **所有者**(*owner*)的变量。
|
||||
> 2. 值有且只有一个所有者。
|
||||
> 3. 当所有者(变量)离开作用域,这个值将被丢弃。
|
||||
|
||||
### 变量作用域
|
||||
|
||||
我们已经在第二章完成过一个 Rust 程序的例子。现在我们已经掌握了基本语法,所以不会在之后的例子中包含 `fn main() {` 代码了,所以如果你是一路跟过来的,必须手动将之后例子的代码放入一个 `main` 函数中。为此,例子将显得更加简明,使我们可以关注具体细节而不是样板代码。
|
||||
我们已经在第二章完成一个 Rust 程序示例。既然我们已经掌握了基本语法,将不会在之后的例子中包含 `fn main() {` 代码,所以如果你是一路跟过来的,必须手动将之后例子的代码放入一个 `main` 函数中。这样,例子将显得更加简明,使我们可以关注实际细节而不是样板代码。
|
||||
|
||||
作为所有权的第一个例子,我们看看一些变量的 **作用域**(*scope*)。作用域是一个项(原文:item) 在程序中有效的范围。假设有这样一个变量:
|
||||
在所有权的第一个例子中,我们看看一些变量的 **作用域**(*scope*)。作用域是一个项(item)在程序中有效的范围。假设有这样一个变量:
|
||||
|
||||
```rust
|
||||
let s = "hello";
|
||||
```
|
||||
|
||||
变量 `s` 绑定到了一个字符串字面值,这个字符串值是硬编码进程序代码中的。这个变量从声明的点开始直到当前 **作用域** 结束时都是有效的。示例 4-1 的注释标明了变量 `s` 在何处是有效的:
|
||||
变量 `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
|
||||
{ // s 在这里无效, 它尚未声明
|
||||
let s = "hello"; // 从此处起,s 是有效的
|
||||
|
||||
// do stuff with s
|
||||
} // this scope is now over, and s is no longer valid
|
||||
// 使用 s
|
||||
} // 此作用域已结束,s 不再有效
|
||||
```
|
||||
|
||||
<span class="caption">示例 4-1:一个变量和其有效的作用域</span>
|
||||
|
||||
换句话说,这里有两个重要的点:
|
||||
换句话说,这里有两个重要的时间点:
|
||||
|
||||
1. 当 `s` **进入作用域** 时,它就是有效的。
|
||||
2. 这一直持续到它 **离开作用域** 为止。
|
||||
* 当 `s` **进入作用域** 时,它就是有效的。
|
||||
* 这一直持续到它 **离开作用域** 为止。
|
||||
|
||||
目前为止,变量是否有效与作用域的关系跟其他编程语言是类似的。现在我们在此基础上介绍 `String` 类型。
|
||||
|
||||
### `String` 类型
|
||||
|
||||
为了演示所有权的规则,我们需要一个比第三章讲到的任何一个都要复杂的数据类型。“Data Types” 部分涉及到的数据类型都是储存在栈上的并且当离开作用域时被移出栈,不过我们需要寻找一个储存在堆上的数据来探索 Rust 是如何知道该在何时清理数据的。
|
||||
为了演示所有权的规则,我们需要一个比第三章 “数据类型” 中讲到的都要复杂的数据类型。前面介绍的类型都是存储在栈上的并且当离开作用域时被移出栈,不过我们需要寻找一个存储在堆上的数据来探索 Rust 是如何知道该在何时清理数据的。
|
||||
|
||||
这里使用 `String` 作为例子并专注于 `String` 与所有权相关的部分。这些方面也同样适用于其他标准库提供的或你自己创建的复杂数据类型。在第八章会更深入地讲解 `String`。
|
||||
这里使用 `String` 作为例子,并专注于 `String` 与所有权相关的部分。这些方面也同样适用于标准库提供的或你自己创建的其他复杂数据类型。在第八章会更深入地讲解 `String`。
|
||||
|
||||
我们已经见过字符串字面值了,它被硬编码进程序里。字符串字面值是很方便的,不过他们并不总是适合所有需要使用文本的场景。原因之一就是他们是不可变的。另一个原因是不是所有字符串的值都能在编写代码时就知道:例如,如果想要获取用户输入并储存该怎么办呢?为此,Rust 有第二个字符串类型,`String`。这个类型储存在堆上所以能够储存在编译时未知大小的文本。可以用 `from` 函数从字符串字面值来创建 `String`,如下:
|
||||
我们已经见过字符串字面值,字符串值被硬编码进程序里。字符串字面值是很方便的,不过他们并不适合使用文本的每一种场景。原因之一就是他们是不可变的。另一个原因是并不是所有字符串的值都能在编写代码时就知道:例如,要是想获取用户输入并存储该怎么办呢?为此,Rust 有第二个字符串类型,`String`。这个类型被分配到堆上,所以能够存储在编译时未知大小的文本。可以使用 `from` 函数基于字符串字面值来创建 `String`,如下:
|
||||
|
||||
```rust
|
||||
let s = String::from("hello");
|
||||
```
|
||||
|
||||
这两个冒号(`::`)运算符允许将特定的 `from` 函数置于 `String` 类型的命名空间(namespace)下而不需要使用类似 `string_from` 这样的名字。在第五章的 “方法语法”(“Method Syntax”)部分会着重讲解这个语法而且在第七章会讲到模块的命名空间。
|
||||
这两个冒号(`::`)是运算符,允许将特定的 `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
|
||||
s.push_str(", world!"); // push_str() 在字符串后追加字面值
|
||||
|
||||
println!("{}", s); // This will print `hello, world!`
|
||||
println!("{}", s); // 将打印 `hello, world!`
|
||||
```
|
||||
|
||||
那么这里有什么区别呢?为什么 `String` 可变而字面值却不行呢?区别在于两个类型对内存的处理上。
|
||||
|
||||
### 内存与分配
|
||||
|
||||
对于字符串字面值的情况,我们在编译时就知道其内容所以它直接被硬编码进最终的可执行文件中,这使得字符串字面值快速且高效。不过这些属性都只来源于其不可变性。不幸的是,我们不能为了每一个在编译时未知大小的文本而将一块内存放入二进制文件中而它的大小还可能随着程序运行而改变。
|
||||
就字符串字面值来说,我们在编译时就知道其内容,所以文本被直接硬编码进最终的可执行文件中。这使得字符串字面值快速且高效。不过这些特性都只得益于字符串字面值的不可变性。不幸的是,我们不能为了每一个在编译时大小未知的文本而将一块内存放入二进制文件中,并且它的大小还可能随着程序运行而改变。
|
||||
|
||||
对于 `String` 类型,为了支持一个可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容。这意味着:
|
||||
|
||||
1. 内存必须在运行时向操作系统请求。
|
||||
2. 需要一个当我们处理完 `String` 时将内存返回给操作系统的方法。
|
||||
* 必须在运行时向操作系统请求内存。
|
||||
* 需要一个当我们处理完 `String` 时将内存返回给操作系统的方法。
|
||||
|
||||
第一部分由我们完成:当调用 `String::from` 时,它的实现 (*implementation*) 请求其所需的内存。这在编程语言中是非常通用的。
|
||||
|
||||
然而,第二部分实现起来就各有区别了。在有 **垃圾回收**(*garbage collector*,*GC*)的语言中, GC 记录并清除不再使用的内存,而我们作为程序员,并不需要关心他们。没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们程序员的责任了,正如请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要 `allocate` 和 `free` 一一对应。
|
||||
然而,第二部分实现起来就各有区别了。在有 **垃圾回收**(*garbage collector*,*GC*)的语言中, GC 记录并清除不再使用的内存,而我们并不需要关心它。没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们的责任了,跟请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要精确的为一个 `allocate` 配对一个 `free`。
|
||||
|
||||
Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。下面是示例 4-1 中作用域例子的一个使用 `String` 而不是字符串字面值的版本:
|
||||
|
||||
```rust
|
||||
{
|
||||
let s = String::from("hello"); // s is valid from this point forward
|
||||
let s = String::from("hello"); // 从此处起,s 是有效的
|
||||
|
||||
// do stuff with s
|
||||
} // this scope is now over, and s is no
|
||||
// longer valid
|
||||
// 使用 s
|
||||
} // 此作用域已结束,
|
||||
// s 不再有效
|
||||
```
|
||||
|
||||
这里是一个将 `String` 需要的内存返回给操作系统的很自然的位置:当 `s` 离开作用域的时候。当变量离开作用域,Rust 为其调用一个特殊的函数。这个函数叫做 `drop`,在这里 `String` 的作者可以放置释放内存的代码。Rust 在结尾的 `}` 处自动调用 `drop`。
|
||||
这是一个将 `String` 需要的内存返回给操作系统的很自然的位置:当 `s` 离开作用域的时候。当变量离开作用域,Rust 为我们调用一个特殊的函数。这个函数叫做 `drop`,在这里 `String` 的作者可以放置释放内存的代码。Rust 在结尾的 `}` 处自动调用 `drop`。
|
||||
|
||||
> 注意:在 C++ 中,这种 item 在生命周期结束时释放资源的方法有时被称作 **资源获取即初始化**(*Resource Acquisition Is Initialization (RAII)*)。如果你使用过 RAII 模式的话应该对 Rust 的 `drop` 函数并不陌生。
|
||||
> 注意:在 C++ 中,这种 item 在生命周期结束时释放资源的模式有时被称作 **资源获取即初始化**(*Resource Acquisition Is Initialization (RAII)*)。如果你使用过 RAII 模式的话应该对 Rust 的 `drop` 函数并不陌生。
|
||||
|
||||
这个模式对编写 Rust 代码的方式有着深远的影响。现在它看起来很简单,不过在更复杂的场景下代码的行为可能是不可预测的,比如当有多个变量使用在堆上分配的内存时。现在让我们探索一些这样的场景。
|
||||
|
||||
#### 变量与数据交互的方式(一):移动
|
||||
|
||||
Rust 中的多个变量可以采用一种独特的方式与同一数据交互。让我们看看示例 4-2 中一个使用整型的例子:
|
||||
Rust 中的多个变量可以采用一种独特的方式与同一数据交互。让我们看看示例 4-2 中一个使用整型的例子。
|
||||
|
||||
```rust
|
||||
let x = 5;
|
||||
let y = x;
|
||||
```
|
||||
|
||||
<span class="caption">示例 4-2:将变量 `x` 赋值给 `y`</span>
|
||||
<span class="caption">示例 4-2:将变量 `x` 的整数值赋给 `y`</span>
|
||||
|
||||
根据其他语言的经验我们大致可以猜到这在干什么:“将 `5` 绑定到 `x`;接着生成一个值 `x` 的拷贝并绑定到 `y`”。现在有了两个变量,`x` 和 `y`,都等于 `5`。这也正是事实上发生了的,因为正数是有已知固定大小的简单值,所以这两个 `5` 被放入了栈中。
|
||||
我们大致可以猜到这在干什么:“将 `5` 绑定到 `x`;接着生成一个值 `x` 的拷贝并绑定到 `y`”。现在有了两个变量,`x` 和 `y`,都等于 `5`。这也正是事实上发生了的,因为正数是有已知固定大小的简单值,所以这两个 `5` 被放入了栈中。
|
||||
|
||||
现在看看这个 `String` 版本:
|
||||
|
||||
@ -149,29 +145,29 @@ let s2 = s1;
|
||||
|
||||
这看起来与上面的代码非常类似,所以我们可能会假设他们的运行方式也是类似的:也就是说,第二行可能会生成一个 `s1` 的拷贝并绑定到 `s2` 上。不过,事实上并不完全是这样。
|
||||
|
||||
为了更全面的解释这个问题,让我们看看图 4-1 中 `String` 真正是什么样的。`String` 由三部分组成,如图左侧所示:一个指向存放字符串内容内存的指针,一个长度,和一个容量。这一组数据储存在栈上。右侧则是堆上存放内容的内存部分。
|
||||
看看图 4-1 以了解 `String` 的底层会发生什么。`String` 由三部分组成,如图左侧所示:一个指向存放字符串内容内存的指针,一个长度,和一个容量。这一组数据存储在栈上。右侧则是堆上存放内容的内存部分。
|
||||
|
||||
<img alt="String in memory" src="img/trpl04-01.svg" class="center" style="width: 50%;" />
|
||||
|
||||
<span class="caption">图 4-1:一个绑定到 `s1` 的拥有值 `"hello"` 的 `String` 的内存表现</span>
|
||||
<span class="caption">图 4-1:将值 `"hello"` 绑定给 `s1` 的 `String` 在内存中的表现形式</span>
|
||||
|
||||
长度代表当前 `String` 的内容使用了多少字节的内存。容量是 `String` 从操作系统总共获取了多少字节的内存。长度与容量的区别是很重要的,不过这在目前为止的场景中并不重要,所以可以暂时忽略容量。
|
||||
长度表示 `String` 的内容当前使用了多少字节的内存。容量是 `String` 从操作系统总共获取了多少字节的内存。长度与容量的区别是很重要的,不过在当前上下文中并不重要,所以现在可以忽略容量。
|
||||
|
||||
当我们把 `s1` 赋值给 `s2`,`String` 的数据被复制了,这意味着我们从栈上拷贝了它的指针、长度和容量。我们并没有复制堆上指针所指向的数据。换句话说,内存中数据的表现如图 4-2 所示。
|
||||
当我们将 `s1` 赋值给 `s2`,`String` 的数据被复制了,这意味着我们从栈上拷贝了它的指针、长度和容量。我们并没有复制指针指向的堆上数据。换句话说,内存中数据的表现如图 4-2 所示。
|
||||
|
||||
<img alt="s1 and s2 pointing to the same value" src="img/trpl04-02.svg" class="center" style="width: 50%;" />
|
||||
|
||||
<span class="caption">图 4-2:变量 `s2` 的内存表现,它有一份 `s1` 指针、长度和容量的拷贝</span>
|
||||
|
||||
这个表现形式看起来 **并不像** 图 4-3 中的那样,但是如果 Rust 也拷贝了堆上的数据后内存看起来会是如何呢。如果 Rust 这么做了,那么操作 `s2 = s1` 在堆上数据比较大的时候可能会对运行时性能造成非常大的影响。
|
||||
这个表现形式看起来 **并不像** 图 4-3 中的那样,如果 Rust 也拷贝了堆上的数据,那么内存看起来就是这样的。如果 Rust 这么做了,那么操作 `s2 = s1` 在堆上数据比较大的时候会对运行时性能造成非常大的影响。
|
||||
|
||||
<img alt="s1 and s2 to two places" src="img/trpl04-03.svg" class="center" style="width: 50%;" />
|
||||
|
||||
<span class="caption">图 4-3:另一个 `s2 = s1` 时可能的内存表现,如果 Rust 同时也拷贝了堆上的数据的话</span>
|
||||
|
||||
之前,我们提到过当变量离开作用域后 Rust 自动调用 `drop` 函数并清理变量的堆内存。不过图 4-4 展示了两个数据指针指向了同一位置。这就有了一个问题:当 `s2` 和 `s1` 离开作用域,他们都会尝试释放相同的内存。这是一个叫做 **二次释放**(*double free*)的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。
|
||||
之前,我们提到过当变量离开作用域后,Rust 自动调用 `drop` 函数并清理变量的堆内存。不过图 4-2 展示了两个数据指针指向了同一位置。这就有了一个问题:当 `s2` 和 `s1` 离开作用域,他们都会尝试释放相同的内存。这是一个叫做 **二次释放**(*double free*)的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。
|
||||
|
||||
为了确保内存安全,这种场景下 Rust 的处理有另一个细节值得注意。与其尝试拷贝被分配的内存,Rust 则认为 `s1` 不再有效,因此 Rust 不需要在 `s1` 离开作用域后清理任何东西。看看在 `s2` 被创建之后尝试使用 `s1` 会发生什么:
|
||||
为了确保内存安全,这种场景下 Rust 的处理有另一个细节值得注意。与其尝试拷贝被分配的内存,Rust 则认为 `s1` 不再有效,因此 Rust 不需要在 `s1` 离开作用域后清理任何东西。看看在 `s2` 被创建之后尝试使用 `s1` 会发生什么;它不会工作:
|
||||
|
||||
```rust,ignore
|
||||
let s1 = String::from("hello");
|
||||
@ -196,13 +192,13 @@ error[E0382]: use of moved value: `s1`
|
||||
not implement the `Copy` trait
|
||||
```
|
||||
|
||||
如果你在其他语言中听说过术语 “浅拷贝”(“shallow copy”)和 “深拷贝”(“deep copy”),那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效化了,这个操作被称为 **移动**(*move*),而不是浅拷贝。上面的例子可以解读为 `s1` 被 **移动** 到了 `s2` 中。那么具体发生了什么,如图 4-4 所示。
|
||||
如果你在其他语言中听说过术语 **浅拷贝**(*shallow copy*)和 **深拷贝**(*deep copy*),那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效了,这个操作被称为 **移动**(*move*),而不是浅拷贝。上面的例子可以解读为 `s1` 被 **移动** 到了 `s2` 中。那么具体发生了什么,如图 4-4 所示。
|
||||
|
||||
<img alt="s1 moved to s2" src="img/trpl04-04.svg" class="center" style="width: 50%;" />
|
||||
|
||||
<span class="caption">图 4-4:`s1` 无效化之后的内存表现</span>
|
||||
<span class="caption">图 4-4:`s1` 无效之后的内存表现</span>
|
||||
|
||||
这样就解决了我们的麻烦!因为只有 `s2` 是有效的,当其离开作用域,它就释放自己的内存,完毕。
|
||||
这样就解决了我们的问题!因为只有 `s2` 是有效的,当其离开作用域,它就释放自己的内存,完毕。
|
||||
|
||||
另外,这里还隐含了一个设计选择:Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何 **自动** 的复制可以被认为对运行时性能影响较小。
|
||||
|
||||
@ -219,13 +215,13 @@ let s2 = s1.clone();
|
||||
println!("s1 = {}, s2 = {}", s1, s2);
|
||||
```
|
||||
|
||||
这段代码能正常运行,也是如何显式产生图 4-3 中行为的方式,这里堆上的数据 **确实** 被复制了。
|
||||
这段代码能正常运行,并且明确产生图 4-3 中行为,这里堆上的数据 **确实** 被复制了。
|
||||
|
||||
当出现 `clone` 调用时,你知道一些特定的代码被执行而且这些代码可能相当消耗资源。你很容易察觉到一些不寻常的事情正在发生。
|
||||
|
||||
#### 只在栈上的数据:拷贝
|
||||
|
||||
这里还有一个没有提到的小窍门。这些代码使用了整型并且是有效的,他们是之前示例 4-2 中的一部分:
|
||||
这里还有一个没有提到的小窍门。这些代码使用了整型并且是有效的,他们是示例 4-2 中的一部分:
|
||||
|
||||
```rust
|
||||
let x = 5;
|
||||
@ -234,98 +230,95 @@ let y = x;
|
||||
println!("x = {}, y = {}", x, y);
|
||||
```
|
||||
|
||||
他们似乎与我们刚刚学到的内容相抵触:没有调用 `clone`,不过 `x` 依然有效且没有被移动到 `y` 中。
|
||||
但这段代码似乎与我们刚刚学到的内容相矛盾:没有调用 `clone`,不过 `x` 依然有效且没有被移动到 `y` 中。
|
||||
|
||||
原因是像整型这样的在编译时已知大小的类型被整个储存在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 `y` 后使 `x` 无效。换句话说,这里没有深浅拷贝的区别,所以这里调用 `clone` 并不会与通常的浅拷贝有什么不同,我们可以不用管它。
|
||||
原因是像整型这样的在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 `y` 后使 `x` 无效。换句话说,这里没有深浅拷贝的区别,所以这里调用 `clone` 并不会与通常的浅拷贝有什么不同,我们可以不用管它。
|
||||
|
||||
Rust 有一个叫做 `Copy` trait 的特殊注解,可以用在类似整型这样的储存在栈上的类型(第十章详细讲解 trait)。如果一个类型拥有 `Copy` trait,一个旧的变量在将其赋值给其他变量后仍然可用。Rust 不允许自身或其任何部分实现了 `Drop` trait 的类型使用 `Copy` trait。如果我们对其值离开作用域时需要特殊处理的类型使用 `Copy` 注解,将会出现一个编译时错误。关于如何为你的类型增加 `Copy` 注解,请阅读附录 C 中的可导出 trait。
|
||||
Rust 有一个叫做 `Copy` trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上(第十章详细讲解 trait)。如果一个类型拥有 `Copy` trait,一个旧的变量在将其赋值给其他变量后仍然可用。Rust 不允许自身或其任何部分实现了 `Drop` trait 的类型使用 `Copy` trait。如果我们对其值离开作用域时需要特殊处理的类型使用 `Copy` 注解,将会出现一个编译时错误。要学习如何为你的类型增加 `Copy` 注解,请阅读附录 C 中的 “可派生的 trait”。
|
||||
|
||||
那么什么类型是 `Copy` 的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则,任何简单标量值的组合可以是 `Copy` 的,任何需要分配内存,或者本身就是某种形式资源的类型不会是 `Copy` 的。如下是一些 `Copy` 的类型:
|
||||
那么什么类型是 `Copy` 的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则,任何简单标量值的组合可以是 `Copy` 的,不需要分配内存或某种形式资源的类型是 `Copy` 的。如下是一些 `Copy` 的类型:
|
||||
|
||||
* 所有整数类型,比如 `u32`。
|
||||
* 布尔类型,`bool`,它的值是 `true` 和 `false`。
|
||||
* 所有浮点数类型,比如 `f64`。
|
||||
* 元组,当且仅当其包含的类型也都是 `Copy` 的时候。`(i32, i32)` 是 `Copy` 的,不过 `(i32, String)` 就不是。
|
||||
* 字符类型,`char`。
|
||||
* 元组,当且仅当其包含的类型也都是 `Copy` 的时候。比如,`(i32, i32)` 是 `Copy` 的,但 `(i32, String)` 就不是。
|
||||
|
||||
### 所有权与函数
|
||||
|
||||
将值传递给函数在语义上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。示例 4-7 是一个展示变量何时进入和离开作用域的例子:
|
||||
将值传递给函数在语义上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。示例 4-3 使用注释展示变量何时进入和离开作用域:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let s = String::from("hello"); // s comes into scope.
|
||||
let s = String::from("hello"); // s 进入作用域
|
||||
|
||||
takes_ownership(s); // s's value moves into the function...
|
||||
// ... and so is no longer valid here.
|
||||
takes_ownership(s); // s 的值移动到函数里...
|
||||
// ... 所以到这里不再有效
|
||||
|
||||
let x = 5; // x comes into scope.
|
||||
let x = 5; // x 进入作用域
|
||||
|
||||
makes_copy(x); // x would move into the function,
|
||||
// but i32 is Copy, so it’s okay to still
|
||||
// use x afterward.
|
||||
makes_copy(x); // x 应该移动函数里,
|
||||
// 但 i32 是 Copy 的,所以在后面可继续使用 x
|
||||
|
||||
} // Here, x goes out of scope, then s. But since s's value was moved, nothing
|
||||
// special happens.
|
||||
} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
|
||||
// 所以不会有特殊操作
|
||||
|
||||
fn takes_ownership(some_string: String) { // some_string comes into scope.
|
||||
fn takes_ownership(some_string: String) { // some_string 进入作用域
|
||||
println!("{}", some_string);
|
||||
} // Here, some_string goes out of scope and `drop` is called. The backing
|
||||
// memory is freed.
|
||||
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放
|
||||
|
||||
fn makes_copy(some_integer: i32) { // some_integer comes into scope.
|
||||
fn makes_copy(some_integer: i32) { // some_integer 进入作用域
|
||||
println!("{}", some_integer);
|
||||
} // Here, some_integer goes out of scope. Nothing special happens.
|
||||
} // 这里,some_integer 移出作用域。不会有特殊操作
|
||||
```
|
||||
|
||||
<span class="caption">示例 4-3:带有所有权和作用域标注的函数</span>
|
||||
<span class="caption">示例 4-3:带有所有权和作用域注释的函数</span>
|
||||
|
||||
当尝试在调用 `takes_ownership` 后使用 `s` 时,Rust 会抛出一个编译时错误。这些静态检查使我们免于犯错。试试在 `main` 函数中添加使用 `s` 和 `x` 的代码来看看哪里能使用他们,以及所有权规则会在哪里阻止我们这么做。
|
||||
|
||||
### 返回值与作用域
|
||||
|
||||
返回值也可以转移作用域。这里是一个拥有与示例 4-3 中类似标注的例子:
|
||||
返回值也可以转移所有权。示例 4-4 与示例 4-3 一样带有类似的注释。
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let s1 = gives_ownership(); // gives_ownership moves its return
|
||||
// value into s1.
|
||||
let s1 = gives_ownership(); // gives_ownership 将返回值
|
||||
// 移给 s1
|
||||
|
||||
let s2 = String::from("hello"); // s2 comes into scope.
|
||||
let s2 = String::from("hello"); // s2 进入作用域
|
||||
|
||||
let s3 = takes_and_gives_back(s2); // s2 is moved into
|
||||
// takes_and_gives_back, which also
|
||||
// moves its return value into s3.
|
||||
} // Here, s3 goes out of scope and is dropped. s2 goes out of scope but was
|
||||
// moved, so nothing happens. s1 goes out of scope and is dropped.
|
||||
let s3 = takes_and_gives_back(s2); // s2 被移动到
|
||||
// takes_and_gives_back 中,
|
||||
// 它也将返回值移给 s3
|
||||
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
|
||||
// 所以什么也不会发生。s1 移出作用域并被丢弃
|
||||
|
||||
fn gives_ownership() -> String { // gives_ownership will move its
|
||||
// return value into the function
|
||||
// that calls it.
|
||||
fn gives_ownership() -> String { // gives_ownership 将返回值移动给
|
||||
// 调用它的函数
|
||||
|
||||
let some_string = String::from("hello"); // some_string comes into scope.
|
||||
let some_string = String::from("hello"); // some_string 进入作用域.
|
||||
|
||||
some_string // some_string is returned and
|
||||
// moves out to the calling
|
||||
// function.
|
||||
some_string // 返回 some_string 并移出给调用的函数
|
||||
}
|
||||
|
||||
// takes_and_gives_back will take a String and return one.
|
||||
fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
|
||||
// scope.
|
||||
// takes_and_gives_back 将传入字符串并返回该值
|
||||
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
|
||||
|
||||
a_string // a_string is returned and moves out to the calling function.
|
||||
a_string // 返回 a_string 并移出给调用的函数
|
||||
}
|
||||
```
|
||||
|
||||
变量的所有权总是遵循相同的模式:将值赋值给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 `drop` 被清理掉,除非数据被移动为另一个变量所有。
|
||||
<span class="caption">示例 4-4: 转移返回值的所有权</span>
|
||||
|
||||
在每一个函数中都获取并接着返回所有权可能有些冗余。如果我们想要函数使用一个值但不获取所有权该怎么办呢?如果我们还要接着使用它的话,每次都传递出去再传回来就有点烦人了,如果真的这样做,有时一个函数就需要有多个返回值。
|
||||
变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 `drop` 被清理掉,除非数据被移动为另一个变量所有。
|
||||
|
||||
当然,我们可以使用元组来返回多个值,像这样:
|
||||
在每一个函数中都获取所有权并接着返回所有权有些啰嗦。如果我们想要函数使用一个值但不获取所有权该怎么办呢?如果我们还要接着使用它的话,每次都传进去再返回来就有点烦人了,除此之外,我们也可能想返回函数体中产生的一些数据。
|
||||
|
||||
我们可以使用元组来返回多个值,如示例 4-5 所示。
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -339,10 +332,12 @@ fn main() {
|
||||
}
|
||||
|
||||
fn calculate_length(s: String) -> (String, usize) {
|
||||
let length = s.len(); // len() returns the length of a String.
|
||||
let length = s.len(); // len() 返回字符串的长度
|
||||
|
||||
(s, length)
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 4-5: 返回参数的所有权</span>
|
||||
|
||||
但是这未免有些形式主义,而且这种场景应该很常见。幸运的是,Rust 对此提供了一个功能,叫做 **引用**(*references*)。
|
||||
|
@ -2,11 +2,11 @@
|
||||
|
||||
> [ch04-02-references-and-borrowing.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch04-02-references-and-borrowing.md)
|
||||
> <br>
|
||||
> commit aa493fef8630e3eee865167892666569afbbc2aa
|
||||
> commit f949ff883628db8ed2f2f5f19e146ebf19ed6a6f
|
||||
|
||||
上一部分结尾的元组代码有这样一个问题:我们不得不将 `String` 返回给调用函数,以便仍能在调用 `calculate_length` 后使用 `String`,因为 `String` 被移动到了 `calculate_length` 内。
|
||||
示例 4-5 中的元组代码有这样一个问题:我们必须将 `String` 返回给调用函数,以便在调用 `calculate_length` 后仍能使用 `String`,因为 `String` 被移动到了 `calculate_length` 内。
|
||||
|
||||
下面是如何定义并使用一个(新的)`calculate_length` 函数,它以一个对象的 **引用** 作为参数而不是获取值的所有权:
|
||||
下面是如何定义并使用一个(新的)`calculate_length` 函数,它以一个对象的引用作为参数而不是获取值的所有权:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -26,13 +26,13 @@ fn calculate_length(s: &String) -> usize {
|
||||
|
||||
首先,注意变量声明和函数返回值中的所有元组代码都消失了。其次,注意我们传递 `&s1` 给 `calculate_length`,同时在函数定义中,我们获取 `&String` 而不是 `String`。
|
||||
|
||||
这些 & 符号就是 **引用**,他们允许你使用值但不获取其所有权。图 4-5 展示了一个图解。
|
||||
这些 & 符号就是 **引用**,它们允许你使用值但不获取其所有权。图 4-5 展示了一张示意图。
|
||||
|
||||
<img alt="&String s pointing at String s1" src="img/trpl04-05.svg" class="center" />
|
||||
|
||||
<span class="caption">图 4-8:`&String s` 指向 `String s1`</span>
|
||||
<span class="caption">图 4-5:`&String s` 指向 `String s1` 示意图</span>
|
||||
|
||||
> 注意:与使用 `&` 引用相对的操作是 **解引用**(*dereferencing*),它使用解引用运算符,`*`。我们将会在第八章遇到一些解引用运算符,并在第十五章详细讨论解引用。
|
||||
> 注意:与使用 `&` 引用相反的操作是 **解引用**(*dereferencing*),它使用解引用运算符,`*`。我们将会在第八章遇到一些解引用运算符,并在第十五章详细讨论解引用。
|
||||
|
||||
仔细看看这个函数调用:
|
||||
|
||||
@ -45,22 +45,22 @@ let s1 = String::from("hello");
|
||||
let len = calculate_length(&s1);
|
||||
```
|
||||
|
||||
`&s1` 语法允许我们创建一个 **指向** 值 `s1` 的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域时其指向的值也不会被丢弃。
|
||||
`&s1` 语法让我们创建一个 **指向** 值 `s1` 的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域时其指向的值也不会被丢弃。
|
||||
|
||||
同理,函数签名使用了 `&` 来表明参数 `s` 的类型是一个引用。让我们增加一些解释性的注解:
|
||||
同理,函数签名使用 `&` 来表明参数 `s` 的类型是一个引用。让我们增加一些解释性的注释:
|
||||
|
||||
```rust
|
||||
fn calculate_length(s: &String) -> usize { // s is a reference to a String
|
||||
fn calculate_length(s: &String) -> usize { // s 是对 String 的引用
|
||||
s.len()
|
||||
} // Here, s goes out of scope. But because it does not have ownership of what
|
||||
// it refers to, nothing happens.
|
||||
} // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,
|
||||
// 所以什么也不会发生
|
||||
```
|
||||
|
||||
变量 `s` 有效的作用域与函数参数的作用域一样,不过当引用离开作用域后并不丢弃它指向的数据,因为我们没有所有权。函数使用引用而不是实际值作为参数意味着无需返回值来交还所有权,因为就不曾拥有所有权。
|
||||
变量 `s` 有效的作用域与函数参数的作用域一样,不过当引用离开作用域后并不丢弃它指向的数据,因为我们没有所有权。当函数使用引用而不是实际值作为参数,无需返回值来交还所有权,因为就不曾拥有所有权。
|
||||
|
||||
我们将获取引用作为函数参数称为 **借用**(*borrowing*)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。
|
||||
|
||||
如果我们尝试修改借用的变量呢?尝试示例 4-4 中的代码。剧透:这行不通!
|
||||
如果我们尝试修改借用的变量呢?尝试示例 4-6 中的代码。剧透:这行不通!
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -76,7 +76,7 @@ fn change(some_string: &String) {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 4-9:尝试修改借用的值</span>
|
||||
<span class="caption">示例 4-6:尝试修改借用的值</span>
|
||||
|
||||
这里是错误:
|
||||
|
||||
@ -94,7 +94,7 @@ error[E0596]: cannot borrow immutable borrowed content `*some_string` as mutable
|
||||
|
||||
### 可变引用
|
||||
|
||||
可以通过一个小调整来修复在示例 4-4 代码中的错误:
|
||||
我们通过一个小调整就能修复示例 4-6 代码中的错误:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -110,12 +110,10 @@ fn change(some_string: &mut String) {
|
||||
}
|
||||
```
|
||||
|
||||
首先,必须将 `s` 改为 `mut`。然后必须创建一个可变引用 `&mut s` 和接受一个可变引用,`some_string: &mut String`。
|
||||
首先,必须将 `s` 改为 `mut`。然后必须创建一个可变引用 `&mut s` 和接受一个可变引用 `some_string: &mut String`。
|
||||
|
||||
不过可变引用有一个很大的限制:在特定作用域中的特定数据有且只有一个可变引用。这些代码会失败:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust,ignore
|
||||
let mut s = String::from("hello");
|
||||
|
||||
@ -137,17 +135,17 @@ error[E0499]: cannot borrow `s` as mutable more than once at a time
|
||||
| - first borrow ends here
|
||||
```
|
||||
|
||||
这个限制允许可变性,不过是以一种受限制的方式允许。新 Rustacean 们经常与此作斗争,因为大部分语言中变量任何时候都是可变的。这个限制的好处是 Rust 可以在编译时就避免数据竞争。
|
||||
这个限制允许可变性,不过是以一种受限制的方式允许。新 Rustacean 们经常与此作斗争,因为大部分语言中变量任何时候都是可变的。
|
||||
|
||||
**数据竞争**(*data race*)是一种特定类型的竞争状态,它可由这三个行为造成:
|
||||
这个限制的好处是 Rust 可以在编译时就避免数据竞争。**数据竞争**(*data race*)类似于竞态条件,它可由这三个行为造成:
|
||||
|
||||
1. 两个或更多指针同时访问同一数据。
|
||||
2. 至少有一个这样的指针被用来写入数据。
|
||||
3. 不存在同步数据访问的机制。
|
||||
* 两个或更多指针同时访问同一数据。
|
||||
* 至少有一个指针被用来写入数据。
|
||||
* 没有同步数据访问的机制。
|
||||
|
||||
数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复;Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码!
|
||||
|
||||
一如既往,可以使用大括号来创建一个新的作用域来允许拥有多个可变引用,只是不能 **同时** 拥有:
|
||||
一如既往,可以使用大括号来创建一个新的作用域,以允许拥有多个可变引用,只是不能 **同时** 拥有:
|
||||
|
||||
```rust
|
||||
let mut s = String::from("hello");
|
||||
@ -155,12 +153,12 @@ let mut s = String::from("hello");
|
||||
{
|
||||
let r1 = &mut s;
|
||||
|
||||
} // r1 goes out of scope here, so we can make a new reference with no problems.
|
||||
} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用
|
||||
|
||||
let r2 = &mut s;
|
||||
```
|
||||
|
||||
当结合可变和不可变引用时有一个类似的规则存在。这些代码会导致一个错误:
|
||||
类似的规则也存在于同时使用可变与不可变引用中。这些代码会导致一个错误:
|
||||
|
||||
```rust,ignore
|
||||
let mut s = String::from("hello");
|
||||
@ -186,13 +184,13 @@ immutable
|
||||
| - immutable borrow ends here
|
||||
```
|
||||
|
||||
哇哦!我们 **也** 不能在拥有不可变引用的同时拥有可变引用。不可变引用的用户可不希望在它的眼皮底下值突然就被改变了!然而,多个不可变引用是没有问题的因为没有哪个只能读取数据的人有能力影响其他人读取到的数据。
|
||||
哇哦!我们 **也** 不能在拥有不可变引用的同时拥有可变引用。不可变引用的用户可不希望在他们的眼皮底下值就被意外的改变了!然而,多个不可变引用是可以的,因为没有哪个只能读取数据的人有能力影响其他人读取到的数据。
|
||||
|
||||
尽管这些错误有时使人沮丧,但请牢记这是 Rust 编译器在提早指出一个潜在的 bug(在编译时而不是运行时)并明确告诉你问题在哪,而不是任由你去追踪为何有时数据并不是你想象中的那样。
|
||||
尽管这些错误有时使人沮丧,但请牢记这是 Rust 编译器在提前指出一个潜在的 bug(在编译时而不是在运行时)并精准显示问题所在。这样你就不必去跟踪为何数据并不是你想象中的那样。
|
||||
|
||||
### 悬垂引用(Dangling References)
|
||||
|
||||
在存在指针的语言中,容易通过释放内存时保留指向它的指针而错误地生成一个 **悬垂指针**(*dangling pointer*),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当我们拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
|
||||
在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 **悬垂指针**(*dangling pointer*),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
|
||||
|
||||
让我们尝试创建一个悬垂引用,Rust 会通过一个编译时错误来避免:
|
||||
|
||||
@ -214,7 +212,7 @@ fn dangle() -> &String {
|
||||
|
||||
```text
|
||||
error[E0106]: missing lifetime specifier
|
||||
--> dangle.rs:5:16
|
||||
--> main.rs:5:16
|
||||
|
|
||||
5 | fn dangle() -> &String {
|
||||
| ^ expected lifetime parameter
|
||||
@ -224,7 +222,7 @@ error[E0106]: missing lifetime specifier
|
||||
= help: consider giving it a 'static lifetime
|
||||
```
|
||||
|
||||
错误信息引用了一个我们还未涉及到的功能:**生命周期**(*lifetimes*)。第十章会详细介绍生命周期。不过,如果你不理会生命周期的部分,错误信息确实包含了为什么代码是有问题的关键:
|
||||
错误信息引用了一个我们还未介绍的功能:生命周期(lifetimes)。第十章会详细介绍生命周期。不过,如果你不理会生命周期部分,错误信息中确实包含了为什么这段代码有问题的关键信息:
|
||||
|
||||
```text
|
||||
this function's return type contains a borrowed value, but there is no value
|
||||
@ -233,17 +231,19 @@ for it to be borrowed from.
|
||||
|
||||
让我们仔细看看我们的 `dangle` 代码的每一步到底发生了什么:
|
||||
|
||||
<span class="filename">Filename: src/main.rs</span>
|
||||
|
||||
```rust,ignore
|
||||
fn dangle() -> &String { // dangle returns a reference to a String
|
||||
fn dangle() -> &String { // dangle 返回一个 String 引用
|
||||
|
||||
let s = String::from("hello"); // s is a new String
|
||||
let s = String::from("hello"); // s 是新创建的 String
|
||||
|
||||
&s // we return a reference to the String, s
|
||||
} // Here, s goes out of scope, and is dropped. Its memory goes away.
|
||||
// Danger!
|
||||
&s // 我们返回 String 的引用,s
|
||||
} // 这里,s 移出了作用域,并被丢弃。它的内存被释放
|
||||
// 危险!
|
||||
```
|
||||
|
||||
因为 `s` 是在 `dangle` 函数内创建的,当 `dangle` 的代码执行完毕后,`s` 将被释放。不过我们尝试返回一个它的引用。这意味着这个引用会指向一个无效的 `String`!这可不对。Rust 不会允许我们这么做的。
|
||||
因为 `s` 是在 `dangle` 函数内创建的,当 `dangle` 的代码执行完毕后,`s` 将被释放。不过我们尝试返回它的引用。这意味着这个引用会指向一个无效的 `String`,这可不对!Rust 不会允许我们这么做。
|
||||
|
||||
这里的解决方法是直接返回 `String`:
|
||||
|
||||
@ -255,15 +255,13 @@ fn no_dangle() -> String {
|
||||
}
|
||||
```
|
||||
|
||||
这样就可以没有任何错误的运行了。所有权被移动出去,所以没有值被释放。
|
||||
这样就没有任何错误了。所有权被移动出去,所以没有值被释放。
|
||||
|
||||
### 引用的规则
|
||||
|
||||
让我们简要的概括一下之前对引用的讨论:
|
||||
让我们概括一下之前对引用的讨论:
|
||||
|
||||
1. 在任意给定时间,**只能** 拥有如下中的一个:
|
||||
* 一个可变引用。
|
||||
* 任意数量的不可变引用。
|
||||
2. 引用必须总是有效的。
|
||||
* 在任意给定时间,**要么** 只能有一个可变引用,**要么** 只能有多个不可变引用。
|
||||
* 引用必须总是有效。
|
||||
|
||||
接下来,我们来看看另一种不同类型的引用:slice。
|
||||
|
@ -1,40 +1,19 @@
|
||||
## `Drop` Trait 运行清理代码
|
||||
## 使用 `Drop` Trait 运行清理代码
|
||||
|
||||
> [ch15-03-drop.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-03-drop.md)
|
||||
> <br>
|
||||
> commit 721553e3a7b5ee9430cb548c8699b67be197b3f6
|
||||
> commit 6060440d67759b7c8627b4d97cb69576057f5fa6
|
||||
|
||||
对于智能指针模式来说另一个重要的 trait 是 `Drop`。`Drop` 允许我们在值要离开作用域时执行一些代码。可以为任何类型提供 `Drop` trait 的实现,同时所指定的代码被用于释放类似于文件或网络连接的资源。我们在智能指针上下文中讨论 `Drop` 是因为其功能几乎总是用于实现智能指针。例如,`Box<T>` 自定义了 `Drop` 用来释放 box 所指向的堆空间。
|
||||
|
||||
在其他一些语言中,我们不得不记住在每次使用完智能指针实例后调用清理内存或资源的代码。如果忘记的话,运行代码的系统可能会因为负荷过重而崩溃。在 Rust 中,可以指定一些代码应该在值离开作用域时被执行,而编译器会自动插入这些代码。
|
||||
|
||||
<!-- Are we saying that any code can be run, and that we can use that to clean
|
||||
up, or that this code that can be run is specifically always for clean up? -->
|
||||
<!-- I don't understand what the difference between those two choices are?
|
||||
/Carol -->
|
||||
在其他一些语言中,我们不得不记住在每次使用完智能指针实例后调用清理内存或资源的代码。如果忘记的话,运行代码的系统可能会因为负荷过重而崩溃。在 Rust 中,可以指定一些代码应该在值离开作用域时被执行,而编译器会自动插入这些代码。于是我们就不需要在程序中到处编写在实例结束时清理这些变量的代码——而且还不会泄露资源了。
|
||||
|
||||
这意味着无需记住在所有处理完这些类型实例后调用清理代码,而仍然不会泄露资源!
|
||||
|
||||
指定在值离开作用域时应该执行的代码的方式是实现 `Drop` trait。`Drop` trait 要求实现一个叫做 `drop` 的方法,它获取一个 `self` 的可变引用。为了能够看出 Rust 何时调用 `drop`,让我们暂时使用 `println!` 语句实现 `drop`。
|
||||
|
||||
<!-- Why are we showing this as an example and not an example of it being used
|
||||
for clean up? -->
|
||||
<!-- To demonstrate the mechanics of implementing the trait and showing when
|
||||
this code gets run. It's hard to experience the cleaning up unless we print
|
||||
something. /Carol -->
|
||||
|
||||
示例 15-16 展示了唯一定制功能就是当其实例离开作用域时打印出 `Dropping CustomSmartPointer!` 的结构体 `CustomSmartPointer`。这会演示 Rust 何时运行 `drop` 函数:
|
||||
|
||||
<!-- Is this below just telling us how to adapt it for cleaning up instead?
|
||||
Maybe save it for when we have context for it? Instead of a `println!`
|
||||
statement, you'd fill in `drop` with whatever cleanup code your smart pointer
|
||||
needs to run: -->
|
||||
<!-- This is demonstrating what we need to do to use `Drop`, without getting
|
||||
into the complexities of what "cleaning up" might mean yet, just to give the
|
||||
reader an idea of when this code gets called and that it gets called
|
||||
automatically. We're building up to cleaning up. /Carol -->
|
||||
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust
|
||||
@ -55,16 +34,11 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 15-16:结构体 `CustomSmartPointer`,其实现了放置清理代码的 `Drop` trait</span>
|
||||
<span class="caption">示例 15-14:结构体 `CustomSmartPointer`,其实现了放置清理代码的 `Drop` trait</span>
|
||||
|
||||
`Drop` trait 包含在 prelude 中,所以无需导入它。我们在 `CustomSmartPointer` 上实现了 `Drop` trait,并提供了一个调用 `println!` 的 `drop` 方法实现。`drop` 函数体是放置任何当类型实例离开作用域时期望运行的逻辑的地方。这里选择打印一些文本以展示 Rust 合适调用 `drop`。
|
||||
|
||||
<!-- Where you'd put this code, or where this code would be called? It seems
|
||||
laborious to write this clean up code wherever there's a print call? -->
|
||||
<!-- I'm not sure how you concluded that from what we had here, could you
|
||||
elaborate? /Carol -->
|
||||
|
||||
在 `main` 中,新建了一个 `CustomSmartPointer` 实例并打印出了 `CustomSmartPointer created.`。在 `main` 的结尾,`CustomSmartPointer` 的实例会离开作用域,而 Rust 会调用放置于 `drop` 方法中的代码,打印出最后的信息。注意无需显示调用 `drop` 方法:
|
||||
在 `main` 中,我们新建了两个 `CustomSmartPointer` 实例并打印出了 `CustomSmartPointer created.`。在 `main` 的结尾,`CustomSmartPointer` 的实例会离开作用域,而 Rust 会调用放置于 `drop` 方法中的代码,打印出最后的信息。注意无需显示调用 `drop` 方法:
|
||||
|
||||
当运行这个程序,会出现如下输出:
|
||||
|
||||
@ -74,27 +48,13 @@ Dropping CustomSmartPointer with data `other stuff`!
|
||||
Dropping CustomSmartPointer with data `my stuff`!
|
||||
```
|
||||
|
||||
当实例离开作用域 Rust 会自动调用 `drop`,并调用我们指定的代码。变量以被创创建时相反的顺序被丢弃,所以 `d` 在 `c` 之前被丢弃。这刚好给了我们一个 drop 方法如何工作的可视化指导,不过通常需要指定类型所需执行的清理代码而不是打印信息。
|
||||
|
||||
<!-- Can you wrap this example up by saying what you would actually put in a
|
||||
drop method and why?-->
|
||||
<!-- Done /Carol -->
|
||||
当实例离开作用域 Rust 会自动调用 `drop`,并调用我们指定的代码。变量以被创建时相反的顺序被丢弃,所以 `d` 在 `c` 之前被丢弃。这个例子刚好给了我们一个 drop 方法如何工作的可视化指导,不过通常需要指定类型所需执行的清理代码而不是打印信息。
|
||||
|
||||
#### 通过 `std::mem::drop` 提早丢弃值
|
||||
|
||||
<!-- is this a new method from Drop or the same method? -->
|
||||
<!-- This is a new function. /Carol -->
|
||||
不幸的是,我们并不能直截了当的禁用 `drop` 这个功能。通常也不需要禁用 `drop` ;整个 `Drop` trait 存在的意义在于其是自动处理的。然而,有时你可能需要提早清理某个值。一个例子是当使用智能指针管理锁时;你可能希望强制运行 `drop` 方法来释放锁以便作用域中的其他代码可以获取锁。Rust 并不允许我们主动调用 `Drop` trait 的 `drop` 方法;当我们希望在作用域结束之前就释放变量的话,我们应该使用的是由标准库提供的 `std::mem::drop`。
|
||||
|
||||
Rust 当值离开作用域时自动插入 `drop` 调用,不能直接禁用这个功能。
|
||||
|
||||
|
||||
|
||||
被打印到屏幕上,它展示了 Rust 在实例离开作用域时自动调用了`drop`。通常也不需要禁用 `drop`;整个 `Drop` trait 存在的意义在于其是自动处理的。有时可能需要提早清理某个值。一个例子是当使用智能指针管理锁时;你可能希望强制运行 `drop` 方法来释放锁以便作用域中的其他代码可以获取锁。首先。让我们看看自己调用 `Drop` trait 的 `drop` 方法会发生什么,如示例 15-17 修改示例 15-16 中的 `main` 函数:
|
||||
|
||||
<!-- Above: I'm not following why we are doing this, if it's not necessary and
|
||||
we aren't going to cover it now anyway -- can you lay out why we're discussing
|
||||
this here? -->
|
||||
<!-- Done. /Carol -->
|
||||
如果我们像是示例 15-14 那样尝试调用 `Drop` trait 的 `drop` 方法,就会得到像示例 15-15 那样的编译错误:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -107,15 +67,15 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 15-17:尝试手动调用 `Drop` trait 的 `drop` 方法提早清理</span>
|
||||
<span class="caption">示例 15-15:尝试手动调用 `Drop` trait 的 `drop` 方法提早清理</span>
|
||||
|
||||
如果尝试编译代码会得到如下错误:
|
||||
|
||||
```text
|
||||
error[E0040]: explicit use of destructor method
|
||||
--> src/main.rs:15:7
|
||||
--> src/main.rs:14:7
|
||||
|
|
||||
15 | c.drop();
|
||||
14 | c.drop();
|
||||
| ^^^^ explicit destructor calls not allowed
|
||||
```
|
||||
|
||||
@ -125,7 +85,7 @@ Rust 不允许我们显式调用 `drop` 因为 Rust 仍然会在 `main` 的结
|
||||
|
||||
因为不能禁用当值离开作用域时自动插入的 `drop`,并且不能显示调用 `drop`,如果我们需要提早清理值,可以使用 `std::mem::drop` 函数。
|
||||
|
||||
`std::mem::drop` 函数不同于 `Drop` trait 中的 `drop` 方法。可以通过传递希望提早强制丢弃的值作为参数。`std::mem::drop` 位于 prelude,所以我们可以修改示例 15-16 中的 `main` 来调用 `drop` 函数如示例 15-18 所示:
|
||||
`std::mem::drop` 函数不同于 `Drop` trait 中的 `drop` 方法。可以通过传递希望提早强制丢弃的值作为参数。`std::mem::drop` 位于 prelude,所以我们可以修改示例 15-15 中的 `main` 来调用 `drop` 函数如示例 15-16 所示:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -148,7 +108,7 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 15-18: 在值离开作用域之前调用 `std::mem::drop` 显式清理</span>
|
||||
<span class="caption">示例 15-16: 在值离开作用域之前调用 `std::mem::drop` 显式清理</span>
|
||||
|
||||
运行这段代码会打印出如下:
|
||||
|
||||
@ -158,24 +118,12 @@ Dropping CustomSmartPointer with data `some data`!
|
||||
CustomSmartPointer dropped before the end of main.
|
||||
```
|
||||
|
||||
<!-- What's the destructor code, here? We haven't mentioned that before, not in
|
||||
this chapter in any case -->
|
||||
<!-- I added a definition for destructor a few paragraphs above, the first time
|
||||
we see it in an error message. /Carol -->
|
||||
|
||||
```Dropping CustomSmartPointer with data `some data`!``` 出现在 `CustomSmartPointer created.` 和 `CustomSmartPointer dropped before the end of main.` 之间,表明了 `drop` 方法被调用了并在此丢弃了 `c`。
|
||||
|
||||
<!-- How does this show that the destructor code (is that drop?) is called? Is
|
||||
this correct, above?-->
|
||||
<!-- The order of what gets printed shows that the drop code is called.
|
||||
/Carol-->
|
||||
|
||||
`Drop` trait 实现中指定的代码可以用于许多方面来使得清理变得方便和安全:比如可以用其创建我们自己的内存分配器!通过 `Drop` trait 和 Rust 所有权系统,你无需担心之后清理代码,Rust 会自动考虑这些问题。
|
||||
|
||||
我们也无需担心意外的清理掉仍在使用的值,这会造成编译器错误:所有权系统确保引用总是有效的,也会确保 `drop` 只会在值不再被使用时被调用一次。
|
||||
|
||||
|
||||
|
||||
使用`Drop` trait 实现指定的代码在很多方面都使得清理值变得方便和安全:比如可以使用它来创建我们自己的内存分配器!通过`Drop` trait 和 Rust 所有权系统,就无需担心之后清理代码,因为 Rust 会自动考虑这些问题。如果代码在值仍被使用时就清理它会出现编译错误,因为所有权系统确保了引用总是有效的,这也就保证了`drop`只会在值不再被使用时被调用一次。
|
||||
使用 `Drop` trait 实现指定的代码在很多方面都使得清理值变得方便和安全:比如可以使用它来创建我们自己的内存分配器!通过`Drop` trait 和 Rust 所有权系统,就无需担心之后清理代码,因为 Rust 会自动考虑这些问题。如果代码在值仍被使用时就清理它会出现编译错误,因为所有权系统确保了引用总是有效的,这也就保证了`drop`只会在值不再被使用时被调用一次。
|
||||
|
||||
现在我们学习了 `Box<T>` 和一些智能指针的特性,让我们聊聊一些其他标准库中定义的智能指针。
|
Loading…
Reference in New Issue
Block a user