mirror of
https://github.com/KaiserY/trpl-zh-cn
synced 2024-11-09 00:43:59 +08:00
fix mistakes
This commit is contained in:
commit
bbb6722c13
@ -11,7 +11,7 @@ registry site),[crates.io]!我们由衷期待**你**使用 Rust 进行创
|
||||
|
||||
[crates.io]: https://crates.io/
|
||||
|
||||
本书的目标读者至少应了解一门其它编程语言。阅读本书之后,你应该能自如的编写 Rust 程序。我们将通过短小精干、前后呼应的例子来学习 Rust,并展示其多样功能的使用方法,同时了解他们幕后如何运行。
|
||||
本书的目标读者至少应了解一门其它编程语言。阅读本书之后,你应该能自如的编写 Rust 程序。我们将通过短小精干、前后呼应的例子来学习 Rust,并展示其多样功能的使用方法,同时了解它们幕后如何运行。
|
||||
|
||||
## 为本书做出贡献
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
使用 Rust 的第一步是安装。你需要网络连接来执行本章的命令,因为将要从网上下载 Rust。
|
||||
|
||||
这里将会展示很多使用终端的命令,这些命令均以 `$` 开头。不需要真的输入`$`,在这里他们代表每行命令的起始。网上有很多教程和例子遵循这种惯例:`$` 代表以常规用户身份运行命令,`#` 代表需要用管理员身份运行命令。没有以 `$`(或 `#`)起始的行通常是之前命令的输出。
|
||||
这里将会展示很多使用终端的命令,这些命令均以 `$` 开头。不需要真的输入`$`,在这里它们代表每行命令的起始。网上有很多教程和例子遵循这种惯例:`$` 代表以常规用户身份运行命令,`#` 代表需要用管理员身份运行命令。没有以 `$`(或 `#`)起始的行通常是之前命令的输出。
|
||||
|
||||
### 在 Linux 或 Mac 上安装
|
||||
|
||||
|
@ -73,7 +73,7 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
这几行定义了一个 Rust **函数**。`main` 函数是特殊的:它是每个可执行的 Rust 程序首先执行的。第一行代码表示 “我声明了一个叫做 `main` 的函数,它没有参数也没有返回值。” 如果有参数的话,他们的名称应该出现在括号中,`(`和`)`之间。
|
||||
这几行定义了一个 Rust **函数**。`main` 函数是特殊的:它是每个可执行的 Rust 程序首先执行的。第一行代码表示 “我声明了一个叫做 `main` 的函数,它没有参数也没有返回值。” 如果有参数的话,它们的名称应该出现在括号中,`(`和`)`之间。
|
||||
|
||||
还须注意函数体被包裹在花括号中,`{`和`}` 之间。Rust 要求所有函数体都要用花括号包裹起来(译者注:有些语言,当函数体只有一行时可以省略花括号,但在 Rust 中是不行的)。一般来说,将左花括号与函数声明置于同一行并以空格分隔,是良好的代码风格。
|
||||
|
||||
@ -124,13 +124,13 @@ $ ./main # or .\main.exe on Windows
|
||||
|
||||
如果 *main.rs* 是上文所述的 “Hello, world!” 程序,它将会在终端上打印 `Hello, world!`。
|
||||
|
||||
来自 Ruby、Python 或 JavaScript 这样的动态类型语言背景的同学,可能不太习惯将编译和执行分为两个单独的步骤。Rust 是一种 **预编译静态类型语言**(*ahead-of-time compiled language*),这意味着你可以编译程序并将其交与他人,他们不需要安装 Rust 即可运行。相反如果你给他们一个 `.rb`、`.py` 或 `.js` 文件,他们需要先分别安装 Ruby,Python,JavaScript 实现(运行时环境,VM),不过你只需要一句命令就可以编译和执行程序。这一切都是语言设计上的权衡取舍。
|
||||
来自 Ruby、Python 或 JavaScript 这样的动态类型语言背景的同学,可能不太习惯将编译和执行分为两个单独的步骤。Rust 是一种 **预编译静态类型语言**(*ahead-of-time compiled language*),这意味着你可以编译程序并将其交与他人,它们不需要安装 Rust 即可运行。相反如果你给他们一个 `.rb`、`.py` 或 `.js` 文件,他们需要先分别安装 Ruby,Python,JavaScript 实现(运行时环境,VM),不过你只需要一句命令就可以编译和执行程序。这一切都是语言设计上的权衡取舍。
|
||||
|
||||
使用 `rustc` 编译简单程序是没问题的,不过随着项目的增长,你可能需要控制你项目的方方面面,并且更容易地将代码分享给其它人或项目。接下来,我们要介绍一个叫做 Cargo 的工具,它会帮助你编写真实世界中的 Rust 程序。
|
||||
|
||||
## Hello, Cargo!
|
||||
|
||||
Cargo 是 Rust 的构建系统和包管理工具,同时 Rustacean 们使用 Cargo 来管理他们的 Rust 项目,它使得很多任务变得更轻松。例如,Cargo 负责构建代码、下载依赖库并编译他们。我们把代码需要的库叫做 **依赖**(*dependencies*)。
|
||||
Cargo 是 Rust 的构建系统和包管理工具,同时 Rustacean 们使用 Cargo 来管理他们的 Rust 项目,它使得很多任务变得更轻松。例如,Cargo 负责构建代码、下载依赖库并编译它们。我们把代码需要的库叫做 **依赖**(*dependencies*)。
|
||||
|
||||
最简单的 Rust 程序,比如我们刚刚编写的,并没有任何依赖,所以我们只使用了 Cargo 构建代码的功能。随着编写的程序更加复杂,你会想要添加依赖,如果你使用 Cargo 开始的话,这将会变得简单许多。
|
||||
|
||||
@ -188,9 +188,9 @@ authors = ["Your Name <you@example.com>"]
|
||||
|
||||
第一行,`[package]`,是一个段落标题,表明下面的语句用来配置一个包。随着我们在这个文件增加更多的信息,还将增加其他段落。
|
||||
|
||||
接下来的三行设置了三个 Cargo 所需的配置,他们告诉 Cargo 需要编译这个项目:名称、版本和作者。Cargo 从环境中获取你的名称和 email 信息。如果不正确,请修改并保存此文件。
|
||||
接下来的三行设置了三个 Cargo 所需的配置,项目的名称、版本和作者,它们告诉 Cargo 需要编译这个项目。Cargo 从环境中获取你的名称和 email 信息。如果不正确,请修改并保存此文件。
|
||||
|
||||
最后一行,`[dependencies]`,是项目依赖的 *crates* 列表(我们这样称呼 Rust 代码包)段落的开始,这样 Cargo 就知道下载和编译它们了。这个项目并不需要任何其他的 crate,不过在下一章猜猜看教程会用得上。
|
||||
最后一行,`[dependencies]`,是项目依赖的 *crates* 列表(我们称呼 Rust 代码包为 crate)段落的开始,这样 Cargo 就知道应该下载和编译它们了。这个项目并不需要任何其他的 crate,不过在下一章猜猜看教程会用得上。
|
||||
|
||||
现在看看 *src/main.rs*:
|
||||
|
||||
@ -213,21 +213,21 @@ Cargo 期望源文件位于 *src* 目录,将项目根目录留给 README、lic
|
||||
|
||||
### 构建并运行 Cargo 项目
|
||||
|
||||
现在让我们看看通过 Cargo 构建和运行 Hello World 程序有什么不同。为此,输入如下命令:
|
||||
现在让我们看看通过 Cargo 构建和运行 Hello World 程序有什么不同。为此输入下面的命令:
|
||||
|
||||
```text
|
||||
$ cargo build
|
||||
Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
|
||||
```
|
||||
|
||||
这应该会创建 *target/debug/hello_cargo*(或者在 Windows 上是 *target\debug\hello_cargo.exe*)可执行文件,可以通过这个命令运行:
|
||||
这应该会创建 *target/debug/hello_cargo*可执行文件(或者在 Windows 上是 *target\debug\hello_cargo.exe*),可以通过这个命令运行:
|
||||
|
||||
```text
|
||||
$ ./target/debug/hello_cargo # or .\target\debug\hello_cargo.exe on Windows
|
||||
Hello, world!
|
||||
```
|
||||
|
||||
好的!如果一切顺利,`Hello, world!`应该再次打印在终端上。
|
||||
很好!如果一切顺利,`Hello, world!`应该再次打印在终端上。
|
||||
|
||||
首次运行 `cargo build` 的时候,Cargo 会在项目根目录创建一个新文件,*Cargo.lock*,它看起来像这样:
|
||||
|
||||
@ -267,7 +267,7 @@ Cargo 的另一个优点是,不管你使用什么操作系统其命令都是
|
||||
|
||||
### 发布(release)构建
|
||||
|
||||
当项目最终准备好发布了,可以使用 `cargo build --release` 来优化编译项目。这会在 *target/release* 而不是 *target/debug* 下生成可执行文件。这些优化可以让 Rust 代码运行的更快,不过启用这些优化也需要消耗更长的编译时间。这也就是为什么会有两种不同的配置:一种为了开发,你需要经常快速重新构建;另一种为了构建给用户最终程序,他们不会重新构建,并且希望程序运行得越快越好。如果你在测试代码的运行时间,请确保运行 `cargo build --release` 并使用 *target/release* 下的可执行文件进行测试。
|
||||
当项目最终准备好发布了,可以使用 `cargo build --release` 来优化编译项目。这会在 *target/release* 而不是 *target/debug* 下生成可执行文件。这些优化可以让 Rust 代码运行的更快,不过启用这些优化也需要消耗更长的编译时间。这也就是为什么会有两种不同的配置:一种为了开发,你需要经常快速重新构建;另一种为了构建给用户最终程序,它们不会重新构建,并且希望程序运行得越快越好。如果你在测试代码的运行时间,请确保运行 `cargo build --release` 并使用 *target/release* 下的可执行文件进行测试。
|
||||
|
||||
### 把 Cargo 当作习惯
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
> <br>
|
||||
> commit 2e269ff82193fd65df8a87c06561d74b51ac02f7
|
||||
|
||||
让我们一起动手完成一个项目,来快速上手 Rust!本章将介绍 Rust 中常用的一些概念,并通过真实的程序来展示如何运用他们。你将会学到更多诸如 `let`、`match`、方法、关联函数、外部 crate 等很多的知识!后继章节会深入探索这些概念的细节。在这一章,我们将练习基础。
|
||||
让我们一起动手完成一个项目,来快速上手 Rust!本章将介绍 Rust 中常用的一些概念,并通过真实的程序来展示如何运用它们。你将会学到更多诸如 `let`、`match`、方法、关联函数、外部 crate 等很多的知识!后继章节会深入探索这些概念的细节。在这一章,我们将练习基础。
|
||||
|
||||
我们会实现一个经典的新手编程问题:猜猜看游戏。它是这么工作的:程序将会随机生成一个 1 到 100 之间的随机整数。接着它会请玩家猜一个数并输入,然后提示猜测是大了还是小了。如果猜对了,它会打印祝贺信息并退出。
|
||||
|
||||
@ -396,11 +396,11 @@ Please input your guess.
|
||||
You guessed: 5
|
||||
```
|
||||
|
||||
你应该能得到不同的随机数,同时他们应该都是在 1 和 100 之间的。干得漂亮!
|
||||
你应该能得到不同的随机数,同时它们应该都是在 1 和 100 之间的。干得漂亮!
|
||||
|
||||
## 比较猜测与秘密数字
|
||||
|
||||
现在有了用户输入和一个随机数,我们可以比较他们。这个步骤如示例 2-4 所示:
|
||||
现在有了用户输入和一个随机数,我们可以比较它们。这个步骤如示例 2-4 所示:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -762,4 +762,4 @@ fn main() {
|
||||
|
||||
此时此刻,你顺利完成了猜猜看游戏!恭喜!
|
||||
|
||||
这是一个通过动手实践学习 Rust 新概念的项目:`let`、`match`、方法、关联函数、使用外部 crate 等等,接下来的几章,我们将会继续深入。第三章涉及到大部分编程语言都有的概念,比如变量、数据类型和函数,以及如何在 Rust 中使用他们。第四章探索所有权(ownership),这是一个 Rust 同其他语言大不相同的功能。第五章讨论结构体和方法的语法,而第六章侧重解释枚举。
|
||||
这是一个通过动手实践学习 Rust 新概念的项目:`let`、`match`、方法、关联函数、使用外部 crate 等等,接下来的几章,我们将会继续深入。第三章涉及到大部分编程语言都有的概念,比如变量、数据类型和函数,以及如何在 Rust 中使用它们。第四章探索所有权(ownership),这是一个 Rust 同其他语言大不相同的功能。第五章讨论结构体和方法的语法,而第六章侧重解释枚举。
|
||||
|
@ -4,10 +4,10 @@
|
||||
> <br>
|
||||
> commit 04aa3a45eb72855b34213703718f50a12a3eeec8
|
||||
|
||||
本章涉及一些几乎所有编程语言都有的概念,以及他们在 Rust 中是如何工作的。很多编程语言的核心概念都是共通的,本章中展示的概念都不是 Rust 特有的,不过我们会在 Rust 环境中讨论他们,解释他们的使用习惯。
|
||||
本章涉及一些几乎所有编程语言都有的概念,以及它们在 Rust 中是如何工作的。很多编程语言的核心概念都是共通的,本章中展示的概念都不是 Rust 特有的,不过我们会在 Rust 环境中讨论它们,解释它们的使用习惯。
|
||||
|
||||
具体的,我们将会学习变量,基本类型,函数,注释和控制流。这些基础知识将会出现在每一个 Rust 程序中,提早学习这些概念会使你拥有坚实的起步基础。
|
||||
具体地,我们将会学习变量,基本类型,函数,注释和控制流。这些基础知识将会出现在每一个 Rust 程序中,提早学习这些概念会为你奠定坚实的起步基础。
|
||||
|
||||
> ### 关键字
|
||||
>
|
||||
> Rust 语言有一系列保留的 **关键字**(*keywords*),只能由语言本身使用,像大部分语言一样。你不能使用这些关键字作为变量或函数的名称,大部分关键字有特殊的意义,并被用来完成 Rust 程序中的各种任务;一些关键字目前没有相应的功能,是为将来可能添加的功能保留的。可以在附录 A 中找到关键字的列表。
|
||||
> Rust 语言有一系列保留的 **关键字**(*keywords*),就像大部分语言一样,它们只能由语言本身使用,你不能使用这些关键字作为变量或函数的名称。大部分关键字有特殊的意义,并被用来完成 Rust 程序中的各种任务;一些关键字目前没有相应的功能,是为将来可能添加的功能保留的。可以在附录 A 中找到关键字的列表。
|
||||
|
@ -4,7 +4,7 @@
|
||||
> <br>
|
||||
> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9
|
||||
|
||||
第二章中提到过,变量默认是 **不可变**(*immutable*)的。这是利用 Rust 安全和简单并发的优势编写代码一大助力。不过,变量仍然有可变的选项。让我们探讨一下 Rust 拥抱不可变性的原因及方法,以及何时你不想使用不可变性。
|
||||
第二章中提到过,变量默认是 **不可变**(*immutable*)的。这是利用 Rust 安全和简单并发的优势编写代码一大助力。不过,你仍然可以使用可变变量。让我们探讨一下 Rust 拥抱不可变性的原因及方法,以及何时你不想使用不可变性。
|
||||
|
||||
当变量不可变时,意味着一旦值被绑定上一个名称,你就不能改变这个值。作为说明,通过 `cargo new --bin variables` 在 *projects* 目录生成一个叫做 *variables* 的新项目。
|
||||
|
||||
@ -34,13 +34,15 @@ error[E0384]: re-assignment of immutable variable `x`
|
||||
| ^^^^^ re-assignment of immutable variable
|
||||
```
|
||||
|
||||
这个例子展示了编译器如何帮助你找出程序中的错误。虽然编译错误令人沮丧,那也不过是说程序不能安全的完成你想让它完成的工作;而 **不能** 说明你是不是一个好程序员!有经验的 Rustacean 们一样会遇到编译错误。这些错误给出的原因是 `对不可变变量重新赋值`(`re-assignment of immutable variable`),因为我们尝试对不可变变量 `x` 赋第二个值。
|
||||
这个例子展示了编译器如何帮助你找出程序中的错误。虽然编译错误令人沮丧,那也不过是说程序不能安全的完成你想让它完成的工作;而 **不能** 说明你是不是一个好程序员!有经验的 Rustacean 们一样会遇到编译错误。
|
||||
|
||||
尝试去改变预设为不可变的值,产生编译错误是很重要的,因为这种情况可能导致 bug:如果代码的一部分假设一个值永远也不会改变,而另一部分代码改变了它,第一部分代码就有可能以不可预料的方式运行。不得不承认这种 bug 难以跟踪,尤其是第二部分代码只是 **有时** 改变其值。
|
||||
这些错误给出的原因是 `对不可变变量重新赋值`(`re-assignment of immutable variable`),因为我们尝试对不可变变量 `x` 赋第二个值。
|
||||
|
||||
在尝试改变预设为不可变的值的时候产生编译错误是很重要的,因为这种情况可能导致 bug:如果代码的一部分假设一个值永远也不会改变,而另一部分代码改变了它,第一部分代码就有可能以不可预料的方式运行。不得不承认这种 bug 难以跟踪,尤其是第二部分代码只是 **有时** 改变其值。
|
||||
|
||||
Rust 编译器保证,如果声明一个值不会变,它就真的不会变。这意味着当阅读和编写代码时,不需要记住如何以及哪里可能会被改变,从而使得代码易于推导。
|
||||
|
||||
不过可变性也是非常有用的。变量只是默认不可变;可以通过在变量名之前加 `mut` 来使其可变。除了使值可以改变之外,它向读者表明了其他代码将会改变这个变量的意图。
|
||||
不过可变性也是非常有用的。变量只是默认不可变,可以通过在变量名之前加 `mut` 来使其可变。除了使值可以改变之外,它向读者表明了其他代码将会改变这个变量的意图。
|
||||
|
||||
例如,改变 *src/main.rs* 并替换其代码为如下:
|
||||
|
||||
@ -65,9 +67,9 @@ The value of x is: 5
|
||||
The value of x is: 6
|
||||
```
|
||||
|
||||
通过 `mut`,允许把绑定到 `x` 的值从 `5` 改成 `6`。在一些情况下,你会想要一个变量可变,因为相对只有不可变的风格更容易编写。
|
||||
通过 `mut`,允许把绑定到 `x` 的值从 `5` 改成 `6`。在一些情况下,你会想用可变变量,因为这样的代码比起只用不可变变量的更容易编写。
|
||||
|
||||
除了避免 bug 外,还有多处需要权衡取舍。例如,使用大型数据结构时,适当地使变量可变,可能比复制和返回新分配的实例更快。对于较小的数据结构,总是创建新实例,采用更偏向函数式的风格编程,可能会使代码更易理解,为可读性而遭受性能惩罚或许值得。
|
||||
除了避免 bug 外,还有很多地方需要权衡取舍。例如,使用大型数据结构时,适当地使用可变变量,可能比复制和返回新分配的实例更快。对于较小的数据结构,总是创建新实例,采用更偏向函数式的风格编程,可能会使代码更易理解,为可读性而遭受性能惩罚或许值得。
|
||||
|
||||
### 变量和常量的区别
|
||||
|
||||
@ -87,7 +89,7 @@ The value of x is: 6
|
||||
const MAX_POINTS: u32 = 100_000;
|
||||
```
|
||||
|
||||
常量在整个程序生命周期中都有效,位于它声明的作用域之中。这使得常量可以作为多处代码使用的全局范围的值,例如一个游戏中所有玩家可以获取的最高分或者光速。
|
||||
在声明它的作用域之中,常量在整个程序生命周期中都有效,这使得常量可以作为多处代码使用的全局范围的值,例如一个游戏中所有玩家可以获取的最高分或者光速。
|
||||
|
||||
将用于整个程序的硬编码的值声明为常量对后来的维护者了解值的意义很有帮助。它也能将硬编码的值汇总一处,为将来可能的修改提供方便。
|
||||
|
||||
@ -147,4 +149,4 @@ error[E0308]: mismatched types
|
||||
found type `usize`
|
||||
```
|
||||
|
||||
现在我们探索了变量如何工作,让我们看看更多的数据类型。
|
||||
现在我们已经了解了变量如何工作,让我们再看看更多的数据类型。
|
||||
|
@ -4,9 +4,9 @@
|
||||
> <br>
|
||||
> commit f4bce88a0f4c09aaf0c996021729c6d42907bc2a
|
||||
|
||||
在 Rust 中,任何值都属于一种明确的 **类型**(*type*),这告诉了 Rust 它被指定了何种数据,以便明确其处理方式。我们将分两部分探讨一些内建类型:标量(scalar)和复合(compound)。
|
||||
在 Rust 中,任何值都属于一种明确的 **类型**(*type*),这告诉了 Rust 它被指定为何种数据,以便明确其处理方式。我们将分两部分探讨一些内建类型:标量(scalar)和复合(compound)。
|
||||
|
||||
Rust 是 **静态类型**(*statically typed*)语言,也就是说在编译时就必须知道所有变量的类型,这一认知将贯穿整个章节,请在头脑中明确。通过值的形式及其使用方式,编译器通常可以推断出我们想要用的类型。多种类型均有可能时,比如第二章中使用 `parse` 将 `String` 转换为数字时,必须增加类型注解,像这样:
|
||||
Rust 是 **静态类型**(*statically typed*)语言,也就是说在编译时就必须知道所有变量的类型,这一点将贯穿整个章节。通过值的形式及其使用方式,编译器通常可以推断出我们想要用的类型。多种类型均有可能时,比如第二章中使用 `parse` 将 `String` 转换为数字时,必须增加类型注解,像这样:
|
||||
|
||||
```rust
|
||||
let guess: u32 = "42".parse().expect("Not a number!");
|
||||
@ -28,7 +28,7 @@ error[E0282]: unable to infer enough type information about `_`
|
||||
|
||||
### 标量类型
|
||||
|
||||
**标量**(*scalar*)类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型。你可能在其他语言中见过他们,不过让我们深入了解他们在 Rust 中时如何工作的。
|
||||
**标量**(*scalar*)类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型。你可能在其他语言中见过它们,不过让我们深入了解它们在 Rust 中时如何工作的。
|
||||
|
||||
#### 整型
|
||||
|
||||
@ -48,9 +48,9 @@ error[E0282]: unable to infer enough type information about `_`
|
||||
|
||||
每一个有符号的变体可以储存包含从 -(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 位的。
|
||||
另外,`isize` 和 `usize` 类型依赖运行程序的计算机架构:64 位架构上它们是 64 位的, 32 位架构上它们是 32 位的。
|
||||
|
||||
可以使用表格 3-2 中的任何一种形式编写数字字面值。注意除字节以外的其它字面值允许使用类型后缀,例如 `57u8`,同时也允许使用 `_` 做为分隔符以方便读数,例如`1_000`。
|
||||
可以使用表格 3-2 中的任何一种形式编写数字字面值。注意除 byte 以外的其它字面值允许使用类型后缀,例如 `57u8`,同时也允许使用 `_` 做为分隔符以方便读数,例如`1_000`。
|
||||
|
||||
<span class="caption">表格 3-2: Rust 中的整型字面值</span>
|
||||
|
||||
@ -84,7 +84,7 @@ fn main() {
|
||||
|
||||
#### 数字运算符
|
||||
|
||||
Rust 支持所有数字类型常见的基本数学运算操作:加法、减法、乘法、除法以及余数。如下代码展示了如何使用一个 `let` 语句来使用他们:
|
||||
Rust 支持所有数字类型常见的基本数学运算操作:加法、减法、乘法、除法以及取余。下面的代码展示了如何在一个 `let` 语句中使用它们:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -107,7 +107,7 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
这些语句中的每个表达式使用了一个数学运算符并计算出了一个值,他们绑定到了一个变量。附录 B 包含了一个 Rust 提供的所有运算符的列表。
|
||||
这些语句中的每个表达式使用了一个数学运算符并计算出了一个值,它们绑定到了一个变量。附录 B 包含了一个 Rust 提供的所有运算符的列表。
|
||||
|
||||
#### 布尔型
|
||||
|
||||
@ -139,7 +139,7 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
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 表示更多内容。拼音字母(Accented letters),中文/日文/韩文等象形文字,emoji(絵文字)以及零长度的空白字符对于 Rust `char` 类型都是有效的。Unicode 标量值包含从 `U+0000` 到 `U+D7FF` 和 `U+E000` 到 `U+10FFFF` 之间的值。不过,“字符” 并不是一个 Unicode 中的概念,所以人直觉上的 “字符” 可能与 Rust 中的 `char` 并不符合。第八章的 “字符串” 部分将详细讨论这个主题。
|
||||
|
||||
### 复合类型
|
||||
|
||||
@ -173,9 +173,9 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
程序首先创建了一个元组并绑定到 `tup` 变量上。接着使用了 `let` 和一个模式将 `tup` 分成了三个不同的变量,`x`、`y` 和 `z`。这叫做 *解构*(*destructuring*),因为它将一个元组拆成了三个部分。最后,程序打印出了 `y` 的值,也就是 `6.4`。
|
||||
程序首先创建了一个元组并绑定到 `tup` 变量上。接着使用了 `let` 和一个模式将 `tup` 分成了三个不同的变量,`x`、`y` 和 `z`。这叫做 **解构**(*destructuring*),因为它将一个元组拆成了三个部分。最后,程序打印出了 `y` 的值,也就是 `6.4`。
|
||||
|
||||
除了使用模式匹配解构之外,也可以使用点号(`.`)后跟值的索引来直接访问他们。例如:
|
||||
除了使用模式匹配解构之外,也可以使用点号(`.`)后跟值的索引来直接访问它们。例如:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -195,7 +195,7 @@ fn main() {
|
||||
|
||||
#### 数组
|
||||
|
||||
另一个获取一个多个值集合的方式是 **数组**(*array*)。与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,因为 Rust 中的数组是固定长度的:一旦声明,他们的长度不能增长或缩小。
|
||||
另一个获取一个多个值集合的方式是 **数组**(*array*)。与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,因为 Rust 中的数组是固定长度的:一旦声明,它们的长度不能增长或缩小。
|
||||
|
||||
Rust 中数组的值位于中括号中的逗号分隔的列表中:
|
||||
|
||||
@ -207,9 +207,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",
|
||||
@ -235,7 +235,7 @@ fn main() {
|
||||
|
||||
##### 无效的数组元素访问
|
||||
|
||||
如果我们访问数组结尾之后的元素会发生什么呢?比如我们将上面的例子改为如下:
|
||||
如果我们访问数组结尾之后的元素会发生什么呢?比如我们将上面的例子改成下面这样:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
|
@ -24,7 +24,7 @@ fn another_function() {
|
||||
|
||||
Rust 中的函数定义以 `fn` 开始并在函数名后跟一对括号。大括号告诉编译器哪里是函数体的开始和结尾。
|
||||
|
||||
可以使用定义过的函数名后跟括号来调用任意函数。因为 `another_function` 已经在程序中定义过了,它可以在 `main` 函数中被调用。注意,源码中 `another_function` 在 `main` 函数 **之后** 被定义;也可以在其之前定义。Rust 不关心函数定义于何处,只要他们被定义了。
|
||||
可以使用定义过的函数名后跟括号来调用任意函数。因为 `another_function` 已经在程序中定义过了,它可以在 `main` 函数中被调用。注意,源码中 `another_function` 在 `main` 函数 **之后** 被定义;也可以在其之前定义。Rust 不关心函数定义于何处,只要它们被定义了。
|
||||
|
||||
让我们开始一个叫做 *functions* 的新二进制项目来进一步探索函数。将上面的 `another_function` 例子写入 *src/main.rs* 中并运行。你应该会看到如下输出:
|
||||
|
||||
@ -36,11 +36,11 @@ 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 中参数是什么样的:
|
||||
|
||||
@ -67,9 +67,9 @@ The value of x is: 5
|
||||
|
||||
`another_function` 的声明有一个叫做 `x` 的参数。`x` 的类型被指定为 `i32`。当 `5` 被传递给 `another_function` 时,`println!` 宏将 `5` 放入格式化字符串中大括号的位置。
|
||||
|
||||
在函数签名中,**必须** 声明每个参数的类型。这是 Rust 设计中一个经过慎重考虑的决定:要求在函数定义中提供类型注解意味着编译器再也不需要在别的地方要求你注明类型就能知道你的意图。
|
||||
在函数签名中 **必须** 声明每个参数的类型。这是 Rust 设计中一个经过慎重考虑的决定:要求在函数定义中提供类型注解意味着编译器不需要在别的地方要求你注明类型就能知道你的意图。
|
||||
|
||||
当一个函数有多个参数时,使用逗号隔开他们,像这样:
|
||||
当一个函数有多个参数时,使用逗号隔开它们,像这样:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -84,9 +84,9 @@ fn another_function(x: i32, y: i32) {
|
||||
}
|
||||
```
|
||||
|
||||
这个例子创建了一个有两个参数的函数,都是 `i32` 类型的。函数打印出了这两个参数的值。注意函数参数并不一定都是相同类型的,这个例子中他们只是碰巧相同罢了。
|
||||
这个例子创建了一个有两个参数的函数,都是 `i32` 类型的。函数打印出了这两个参数的值。注意函数参数并不一定都是相同类型的,这个例子中它们只是碰巧相同罢了。
|
||||
|
||||
尝试运行代码。使用上面的例子替换当前 *function* 项目的 *src/main.rs* 文件,并 `cargo run` 运行它:
|
||||
尝试运行代码。使用上面的例子替换当前 *function* 项目的 *src/main.rs* 文件,并用 `cargo run` 运行它:
|
||||
|
||||
```text
|
||||
$ cargo run
|
||||
@ -96,11 +96,11 @@ 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)的语言,这是一个需要理解的(不同于其他语言)重要区别。其他语言并没有这样的区别,所以让我们看看语句与表达式有什么区别以及它们是如何影响函数体的。
|
||||
|
||||
### 语句与表达式
|
||||
|
||||
@ -118,7 +118,7 @@ fn main() {
|
||||
|
||||
<span class="caption">列表 3-3:包含一个语句的 `main` 函数定义</span>
|
||||
|
||||
函数定义也是语句;上面整个例子本身就是一个语句。
|
||||
函数定义也是语句,上面整个例子本身就是一个语句。
|
||||
|
||||
语句并不返回值。因此,不能把`let`语句赋值给另一个变量,比如下面的例子尝试做的:
|
||||
|
||||
@ -144,9 +144,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-3 中有这个语句 `let y = 6;`,`6` 是一个表达式,它计算出的值是 `6`。函数调用是一个表达式。宏调用是一个表达式。我们用来创建新作用域的大括号(代码块),`{}`,也是一个表达式,例如:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -172,11 +172,11 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
这个代码块的值是 `4`。这个值作为 `let` 语句的一部分被绑定到 `y` 上。注意结尾没有分号的那一行,与大部分我们见过的代码行不同。表达式并不包含结尾的分号。如果在表达式的结尾加上分号,他就变成了语句,这也就使其不返回一个值。在接下来的探索中记住函数和表达式都返回值就行了。
|
||||
是一个代码块,它的值是 `4`。这个值作为 `let` 语句的一部分被绑定到 `y` 上。注意结尾没有分号的那一行,与大部分我们见过的代码行不同。表达式并不包含结尾的分号。如果在表达式的结尾加上分号,他就变成了语句,这也就使其不返回一个值。在接下来的探索中记住函数和表达式都返回值就行了。
|
||||
|
||||
### 函数的返回值
|
||||
|
||||
可以向调用它的代码返回值。并不对返回值命名,不过会在一个箭头(`->`)后声明它的类型。在 Rust 中,函数的返回值等同于函数体最后一个表达式的值。这是一个有返回值的函数的例子:
|
||||
函数可以向调用它的代码返回值。我们并不对返回值命名,不过会在一个箭头(`->`)后声明它的类型。在 Rust 中,函数的返回值等同于函数体最后一个表达式的值。这是一个有返回值的函数的例子:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -260,4 +260,4 @@ help: consider removing this semicolon:
|
||||
| ^
|
||||
```
|
||||
|
||||
主要的错误信息,“mismatched types,”(类型不匹配),揭示了代码的核心问题。函数`plus_one` 的定义说明它要返回一个 `i32`,不过语句并不返回一个值,这由那个空元组 `()` 表明。因此,这个函数返回了空元组 `()`,这与函数定义相矛盾并导致一个错误。在输出中,Rust 提供了一个可能会对修正问题有帮助的信息:它建议去掉分号,这会修复这个错误。
|
||||
主要的错误信息,“mismatched types”(类型不匹配),揭示了代码的核心问题。函数 `plus_one` 的定义说明它要返回一个 `i32`,不过语句并不返回一个值,这由那个空元组 `()` 表明。因此,这个函数返回了空元组 `()`,这与函数定义相矛盾并导致一个错误。在输出中,Rust 提供了一个可能会对修正问题有帮助的信息:它建议去掉分号,这会修复这个错误。
|
||||
|
@ -4,7 +4,7 @@
|
||||
> <br>
|
||||
> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9
|
||||
|
||||
所有编程语言都力求使他们的代码易于理解,不过有时需要提供额外的解释。在这种情况下,程序员在源码中留下记录,或者 **注释**(*comments*),编译器会忽略他们不过其他阅读代码的人可能会用得上。
|
||||
所有编程语言都力求使它们的代码易于理解,不过有时需要提供额外的解释。在这种情况下,程序员在源码中留下记录,或者 **注释**(*comments*),编译器会忽略它们不过其他阅读代码的人可能会用得上。
|
||||
|
||||
这是一个注释的例子:
|
||||
|
||||
@ -30,7 +30,7 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
不过你会经常看到他们被以这种格式使用,也就是位于它所解释的代码行的上面一行:
|
||||
不过你会经常看到它们被以这种格式使用,也就是位于它所解释的代码行的上面一行:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -41,4 +41,4 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
这就是注释的全部。并没有什么特别复杂的。
|
||||
Rust 还有另一种注释,称为文档注释,我们将在 14 章讨论它。
|
||||
|
@ -28,7 +28,7 @@ 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` 代码块并继续执行下面的代码。
|
||||
|
||||
尝试运行代码,应该能看到如下输出:
|
||||
|
||||
@ -81,7 +81,7 @@ error[E0308]: mismatched types
|
||||
found type `{integer}`
|
||||
```
|
||||
|
||||
这个错误表明 Rust 期望一个 `bool` 不过却得到了一个整型。Rust 并不会尝试自动地将非布尔值转换为布尔值,不像例如 Ruby 和 JavaScript 这样的语言。必须总是显式地使用 `boolean` 作为 `if` 的条件。例如如果想 要`if` 代码块只在一个数字不等于 `0` 时执行,可以把 `if` 表达式修改为如下:
|
||||
这个错误表明 Rust 期望一个 `bool` 不过却得到了一个整型。不像 Ruby 或 JavaScript 这样的语言,Rust 并不会尝试自动地将非布尔值转换为布尔值。必须总是显式地使用布尔值作为 `if` 的条件。例如,如果想要 `if` 代码块只在一个数字不等于 `0` 时执行,可以把 `if` 表达式修改成下面这样:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -202,13 +202,13 @@ error[E0308]: if and else have incompatible types
|
||||
|
||||
### 使用循环重复执行
|
||||
|
||||
多次执行同一段代码是很常用的。为了这个功能,Rust 提供了多种 **循环**(*loops*)。一个循环执行循环体中的代码直到结尾并紧接着回到开头继续执行。为了实验一下循环,让我们创建一个叫做 *loops* 的新项目。
|
||||
多次执行同一段代码是很常用的,Rust 为此提供了多种 **循环**(*loops*)。一个循环执行循环体中的代码直到结尾并紧接着回到开头继续执行。为了实验一下循环,让我们创建一个叫做 *loops* 的新项目。
|
||||
|
||||
Rust 有三种循环类型:`loop`、`while` 和 `for`。让我们每一个都试试。
|
||||
|
||||
#### 使用 `loop` 重复执行代码
|
||||
|
||||
`loop` 关键字告诉 Rust 一遍又一遍的执行一段代码直到你明确要求停止。
|
||||
`loop` 关键字告诉 Rust 一遍又一遍地执行一段代码直到你明确要求停止。
|
||||
|
||||
作为一个例子,将 *loops* 目录中的 *src/main.rs* 文件修改为如下:
|
||||
|
||||
@ -321,7 +321,7 @@ fn main() {
|
||||
|
||||
例如,在示例 3-5 的代码中,如果从数组 `a` 中移除一个元素但忘记更新条件为 `while index < 4`,代码将会 panic。使用`for`循环的话,就不需要惦记着在更新数组元素数量时修改其他的代码了。
|
||||
|
||||
`for` 循环的安全性和简洁性使得它在成为 Rust 中使用最多的循环结构。即使是在想要循环执行代码特定次数时,例如示例 3-5 中使用 `while` 循环的倒计时例子,大部分 Rustacean 也会使用 `for` 循环。这么做的方式是使用 `Range`,它是标准库提供的用来生成从一个数字开始到另一个数字结束的所有数字序列的类型。
|
||||
`for` 循环的安全性和简洁性使得它在成为 Rust 中使用最多的循环结构。即使是在想要循环执行代码特定次数时,例如示例 3-5 中使用 `while` 循环的倒计时例子,大部分 Rustacean 也会使用 `for` 循环。这么做的方式是使用 `Range`,它是标准库提供的用来生成从一个数字开始到另一个数字之前结束的所有数字序列的类型。
|
||||
|
||||
下面是一个使用 `for` 循环来倒计时的例子,它还使用了一个我们还未讲到的方法,`rev`,用来反转 range:
|
||||
|
||||
@ -340,7 +340,7 @@ fn main() {
|
||||
|
||||
## 总结
|
||||
|
||||
你做到了!这是一个相当可观的章节:你学习了变量,标量和 `if` 表达式,还有循环!如果你想要实践本章讨论的概念,尝试构建如下的程序:
|
||||
你做到了!这是一个大章节:你学习了变量,标量和 `if` 表达式,还有循环!如果你想要实践本章讨论的概念,尝试构建如下的程序:
|
||||
|
||||
* 相互转换摄氏与华氏温度
|
||||
* 生成 n 阶斐波那契数列
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
我们在第三章讨论过,结构体与元组类似。就像元组,结构体的每一部分可以是不同类型。不同于元组,结构体需要命名各部分数据以便能清楚的表明其值的意义。由于有了这些名字使得结构体比元组更灵活:不需要依赖顺序来指定或访问实例中的值。
|
||||
|
||||
定义结构体,需要使用 `struct` 关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字,他们被称作 **字段**(*field*),并定义字段类型。例如,示例 5-1 展示了一个储存用户账号信息的结构体:
|
||||
定义结构体,需要使用 `struct` 关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字,它们被称作 **字段**(*field*),并定义字段类型。例如,示例 5-1 展示了一个储存用户账号信息的结构体:
|
||||
|
||||
```rust
|
||||
struct User {
|
||||
@ -19,7 +19,7 @@ struct User {
|
||||
|
||||
<span class="caption">示例 5-1:`User` 结构体定义</span>
|
||||
|
||||
一旦定义了结构体后为了使用它,通过为每个字段指定具体值来创建这个结构体的 **实例**。创建一个实例需要以结构体的名字开头,接着在大括号中使用 `key: value` 对的形式提供字段,其中 key 是字段的名字而 value 是需要储存在字段中的数据值。这时字段的顺序并不必要与在结构体中声明他们的顺序一致。换句话说,结构体的定义就像一个这个类型的通用模板,而实例则会在这个模板中放入特定数据来创建这个类型的值。例如,可以像示例 5-2 这样来声明一个特定的用户:
|
||||
一旦定义了结构体后为了使用它,通过为每个字段指定具体值来创建这个结构体的 **实例**。创建一个实例需要以结构体的名字开头,接着在大括号中使用 `key: value` 对的形式提供字段,其中 key 是字段的名字,value 是需要储存在字段中的数据值。实例中具体说明字段的顺序不需要和它们在结构体中声明的顺序一致。换句话说,结构体的定义就像一个类型的通用模板,而实例则会在这个模板中放入特定数据来创建这个类型的值。例如,可以像示例 5-2 这样来声明一个特定的用户:
|
||||
|
||||
```rust
|
||||
# struct User {
|
||||
@ -83,7 +83,7 @@ fn build_user(email: String, username: String) -> User {
|
||||
|
||||
<span class="caption">示例 5-4:`build_user` 函数获取 email 和用户名并返回 `User` 实例</span>
|
||||
|
||||
不过,重复 `email` 字段与 `email` 变量的名字,同样的对于`username`,感觉有一点无趣。将函数参数起与结构体字段相同的名字是可以理解的,但是如果结构体有更多字段,重复他们是十分烦人的。幸运的是,这里有一个方便的语法!
|
||||
不过,重复 `email` 字段与 `email` 变量的名字,同样的对于`username`,感觉有一点无趣。将函数参数起与结构体字段相同的名字是可以理解的,但是如果结构体有更多字段,重复它们是十分烦人的。幸运的是,这里有一个方便的语法!
|
||||
|
||||
### 变量与字段同名时的字段初始化语法
|
||||
|
||||
@ -178,11 +178,11 @@ let black = Color(0, 0, 0);
|
||||
let origin = Point(0, 0, 0);
|
||||
```
|
||||
|
||||
注意 `black` 和 `origin` 变量是不同的类型,因为他们是不同的元组结构体的实例。我们定义的每一个结构体有着自己的类型,即使结构体中的字段有相同的类型。在其他方面,元组结构体类似我们在第三章提到的元组。
|
||||
注意 `black` 和 `origin` 变量是不同的类型,因为它们是不同的元组结构体的实例。我们定义的每一个结构体有着自己的类型,即使结构体中的字段有相同的类型。在其他方面,元组结构体类似我们在第三章提到的元组。
|
||||
|
||||
### 没有任何字段的类单元结构体
|
||||
|
||||
我们也可以定义一个没有任何字段的结构体!他们被称为 **类单元结构体**(*unit-like structs*)因为他们类似于 `()`,即 unit 类型。类单元结构体常常在你想要在某个类型上实现 trait 但不需要在类型内存储数据的时候发挥作用。我们将在第十章介绍 trait。
|
||||
我们也可以定义一个没有任何字段的结构体!它们被称为 **类单元结构体**(*unit-like structs*)因为它们类似于 `()`,即 unit 类型。类单元结构体常常在你想要在某个类型上实现 trait 但不需要在类型内存储数据的时候发挥作用。我们将在第十章介绍 trait。
|
||||
|
||||
> ## 结构体数据的所有权
|
||||
>
|
||||
|
@ -191,4 +191,4 @@ rect1 is Rectangle {
|
||||
|
||||
Rust 为我们提供了很多可以通过 `derive` 注解来使用的 trait,他们可以为我们的自定义类型增加实用的行为。这些 trait 和行为在附录 C 中列出。第十章会涉及到如何通过自定义行为来实现这些 trait,同时还有如何创建你自己的 trait。
|
||||
|
||||
我们的 `area` 函数是非常特化的————它只是计算了长方形的面积。如果这个行为与 `Rectangle` 结构体再结合得更紧密一些就更好了,因为它不能用于其他类型。现在让我们看看如何继续重构这些代码,来将 `area` 函数协调进 `Rectangle` 类型定义的`area` **方法** 中。
|
||||
我们的 `area` 函数是非常特化的,它只是计算了长方形的面积。如果这个行为与 `Rectangle` 结构体再结合得更紧密一些就更好了,因为它不能用于其他类型。现在让我们看看如何继续重构这些代码,来将 `area` 函数协调进 `Rectangle` 类型定义的`area` **方法** 中。
|
||||
|
@ -4,11 +4,11 @@
|
||||
> <br>
|
||||
> commit 44bf3afd93519f8b0f900f21a5f2344d36e13448
|
||||
|
||||
**方法** 与函数类似:他们使用 `fn` 关键和名字声明,可以拥有参数和返回值,同时包含一些代码会在某处被调用时执行。不过方法与函数是不同的,因为他们在结构体(或者枚举或者 trait 对象,将分别在第六章和第十七章讲解)的上下文中被定义,并且他们第一个参数总是` self`,它代表方法被调用的结构体的实例。
|
||||
**方法** 与函数类似:它们使用 `fn` 关键字和名字声明,可以拥有参数和返回值,同时包含一段该方法在某处被调用时会执行的代码。不过方法与函数是不同的,因为它们在结构体的上下文中被定义(或者是枚举或 trait 对象的上下文,将分别在第六章和第十七章讲解),并且它们第一个参数总是 `self`,它代表调用该方法的结构体实例。
|
||||
|
||||
### 定义方法
|
||||
|
||||
让我们将获取一个 `Rectangle` 实例作为参数的 `area` 函数改写成一个定义于 `Rectangle` 结构体上的 `area` 方法,如示例 5-13 所示:
|
||||
让我们把前面实现的获取一个 `Rectangle` 实例作为参数的 `area` 函数,改写成一个定义于 `Rectangle` 结构体上的 `area` 方法,如示例 5-13 所示:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -37,13 +37,13 @@ fn main() {
|
||||
|
||||
<span class="caption">示例 5-13:在 `Rectangle` 结构体上定义 `area` 方法</span>
|
||||
|
||||
为了使函数定义于 `Rectangle` 的上下文中,我们开始了一个 `impl` 块(`impl` 是 *implementation* 的缩写)。接着将函数移动到 `impl` 大括号中,并将签名中的第一个(在这里也是唯一一个)参数和函数体中其他地方的对应参数改成 `self`。然后在`main` 中将我们调用 `area` 方法并传递 `rect1` 作为参数的地方,改成使用 **方法语法**(*method syntax*)在 `Rectangle` 实例上调用 `area` 方法。方法语法获取一个实例并加上一个点号后跟方法名、括号以及任何参数。
|
||||
为了使函数定义于 `Rectangle` 的上下文中,我们开始了一个 `impl` 块(`impl` 是 *implementation* 的缩写)。接着将函数移动到 `impl` 大括号中,并将签名中的第一个(在这里也是唯一一个)参数和函数体中其他地方的对应参数改成 `self`。然后在 `main` 中将我们先前调用 `area` 方法并传递 `rect1` 作为参数的地方,改成使用 **方法语法**(*method syntax*)在 `Rectangle` 实例上调用 `area` 方法。方法语法获取一个实例并加上一个点号,后跟方法名、括号以及任何参数。
|
||||
|
||||
在 `area` 的签名中,开始使用 `&self` 来替代 `rectangle: &Rectangle`,因为该方法位于 `impl Rectangle` 上下文中所以 Rust 知道 `self` 的类型是 `Rectangle`。注意仍然需要在 `self` 前面加上 `&`,就像 `&Rectangle` 一样。方法可以选择获取 `self` 的所有权,像我们这里一样不可变的借用 `self`,或者可变的借用 `self`,就跟其他别的参数一样。
|
||||
在 `area` 的签名中,开始使用 `&self` 来替代 `rectangle: &Rectangle`,因为该方法位于 `impl Rectangle` 上下文中所以 Rust 知道 `self` 的类型是 `Rectangle`。注意仍然需要在 `self` 前面加上 `&`,就像 `&Rectangle` 一样。方法可以选择获取 `self` 的所有权,或者像我们这里一样不可变地借用 `self`,或者可变地借用 `self`,就跟其他别的参数一样。
|
||||
|
||||
这里选择 `&self` 跟在函数版本中使用 `&Rectangle` 出于同样的理由:我们并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。如果想要能够在方法中改变调用方法的实例的话,需要将第一个参数改为 `&mut self`。很少见到通过仅仅使用 `self` 作为第一个参数来使方法获取实例的所有权;这种技术通常用在当方法将 `self` 转换成别的实例的时候,这时我们想要防止调用者在转换之后使用原始的实例。
|
||||
这里选择 `&self` 跟在函数版本中使用 `&Rectangle` 出于同样的理由:我们并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。如果想要在方法中改变调用方法的实例,需要将第一个参数改为 `&mut self`。很少见到通过仅仅使用 `self` 作为第一个参数来使方法获取实例的所有权;这种技术通常用在当方法将 `self` 转换成别的实例的时候,这时我们想要防止调用者在转换之后使用原始的实例。
|
||||
|
||||
使用方法而不是函数,除了使用了方法语法和不需要在每个函数签名中重复` self` 类型之外,其主要好处在于组织性。我将某个类型实例能做的所有事情都一起放入 `impl` 块中,而不是让将来的用户在我们的代码中到处寻找 `Rectangle` 的功能。
|
||||
尽量使用方法替代函数,除了使用了方法语法和不需要在每个函数签名中重复 `self` 类型之外,其主要好处在于组织性。我们将某个类型实例能做的所有事情都一起放入 `impl` 块中,而不是让将来的用户在我们的代码中到处寻找 `Rectangle` 的功能。
|
||||
|
||||
> ### `->`运算符到哪去了?
|
||||
>
|
||||
@ -74,11 +74,11 @@ fn main() {
|
||||
> (&p1).distance(&p2);
|
||||
> ```
|
||||
>
|
||||
> 第一行看起来简洁的多。这种自动解引用的行为之所以能行得通是因为方法有一个明确的接收者————`self` 类型。在给出接收者和方法名的前提下,Rust 可以明确的计算出方法是仅仅读取(`&self`),做出修改(`&mut self`)或者是获取所有权(`self`)。Rust 这种使得借用对方法接收者来说是隐式的做法是其所有权系统程序员友好性实践的一大部分。
|
||||
> 第一行看起来简洁的多。这种自动解引用的行为之所以能行得通是因为方法有一个明确的接收者————`self` 类型。在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(`&self`),做出修改(`&mut self`)或者是获取所有权(`self`)。Rust 这种使得借用对方法接收者来说是隐式的做法是其所有权系统程序员友好性实践的一大部分。
|
||||
|
||||
### 带有更多参数的方法
|
||||
|
||||
让我们更多的实践一下方法,通过为 `Rectangle` 结构体实现第二个方法。这回,我们让一个 `Rectangle` 的实例获取另一个 `Rectangle` 实例并返回 `self` 能否完全包含第二个长方形,如果能返回 `true` 若不能则返回 `false`。一旦定义了 `can_hold` 方法,就可以运行示例 5-14 中的代码了:
|
||||
让我们更多的实践一下方法,通过为 `Rectangle` 结构体实现第二个方法。这回,我们让一个 `Rectangle` 的实例获取另一个 `Rectangle` 实例并返回 `self` 能否完全包含第二个长方形,如果能则返回 `true` ,如果不能则返回 `false`。一旦定义了 `can_hold` 方法,就可以运行示例 5-14 中的代码了:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -130,7 +130,7 @@ impl Rectangle {
|
||||
|
||||
### 关联函数
|
||||
|
||||
`impl` 块的另一个有用的功能是:允许在 `impl` 块中定义 **不** 以 `self` 作为参数的函数。这被称为 **关联函数**(*associated functions*),因为他们与结构体相关联。即便如此他们仍是函数而不是方法,因为他们并不作用于一个结构体的实例。你已经使用过一个关联函数了:`String::from`。
|
||||
`impl` 块的另一个有用的功能是:允许在 `impl` 块中定义 **不** 以 `self` 作为参数的函数。这被称为 **关联函数**(*associated functions*),因为它们与结构体相关联。即便如此它们仍是函数而不是方法,因为它们并不作用于一个结构体的实例。你已经使用过一个关联函数了:`String::from`。
|
||||
|
||||
关联函数经常被用作返回一个结构体新实例的构造函数。例如我们可以提供一个关联函数,它接受一个维度参数并且同时用来作为长和宽,这样可以更轻松的创建一个正方形 `Rectangle` 而不必指定两次同样的值:
|
||||
|
||||
@ -150,7 +150,7 @@ impl Rectangle {
|
||||
}
|
||||
```
|
||||
|
||||
使用结构体名和 `::` 语法来调用这个关联函数:比如 `let sq = Rectangle::square(3);`。这个方法位于结构体的命名空间中:`::` 语法用于关联函数和模块创建的命名空间,第七章会讲到后者。
|
||||
使用结构体名和 `::` 语法来调用这个关联函数:比如 `let sq = Rectangle::square(3);`。这个方法位于结构体的命名空间中:`::` 语法用于关联函数和模块创建的命名空间,第七章会讲到模块。
|
||||
|
||||
### 多个 `impl` 块
|
||||
|
||||
@ -180,6 +180,6 @@ impl Rectangle {
|
||||
|
||||
## 总结
|
||||
|
||||
结构体让我们可以在自己的范围内创建有意义的自定义类型。通过结构体,我们可以将相关联的数据片段联系起来并命名他们来使得代码更清晰。方法允许为结构体实例指定行为,而关联函数将特定功能置于结构体的命名空间中并且无需一个实例。
|
||||
结构体让我们可以在自己的范围内创建有意义的自定义类型。通过结构体,我们可以将相关联的数据片段联系起来并命名它们,这样可以使得代码更加清晰。方法允许为结构体实例指定行为,而关联函数将特定功能置于结构体的命名空间中并且无需一个实例。
|
||||
|
||||
结构体并不是创建自定义类型的唯一方法;让我们转向 Rust 的枚举功能并为自己的工具箱再添一个工具。
|
||||
|
@ -4,6 +4,6 @@
|
||||
> <br>
|
||||
> commit 4f2dc564851dc04b271a2260c834643dfd86c724
|
||||
|
||||
本章介绍 **枚举**(*enumerations*),也被称作 *enums*。枚举允许你通过列举可能的值来定义一个类型。首先,我们会定义并使用一个枚举来展示它是如何连同数据一起编码信息的。接下来,我们会探索一个特别有用的枚举,叫做 `Option`,它代表一个值要么是一些值要么什么都不是。然后会讲到 `match` 表达式中的模式匹配如何使对枚举不同的值运行不同的代码变得容易。最后会涉及到 `if let`,另一个简洁方便处理代码中枚举的结构。
|
||||
本章介绍 **枚举**(*enumerations*),也被称作 *enums*。枚举允许你通过列举可能的值来定义一个类型。首先,我们会定义并使用一个枚举来展示它是如何连同数据一起编码信息的。接下来,我们会探索一个特别有用的枚举,叫做 `Option`,它代表一个值要么是某个值要么什么都不是。然后会讲到在 `match` 表达式中用模式匹配,针对不同的枚举值编写相应要执行的代码。最后会涉及到 `if let`,另一个简洁方便处理代码中枚举的结构。
|
||||
|
||||
枚举是一个很多语言都有的功能,不过不同语言中的功能各不相同。Rust 的枚举与像 F#、OCaml 和 Haskell 这样的函数式编程语言中的 **代数数据类型**(*algebraic data types*)最为相似。
|
||||
枚举是一个很多语言都有的功能,不过不同语言中的功能各不相同。Rust 的枚举与 F#、OCaml 和 Haskell 这样的函数式编程语言中的 **代数数据类型**(*algebraic data types*)最为相似。
|
||||
|
@ -4,9 +4,9 @@
|
||||
> <br>
|
||||
> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9
|
||||
|
||||
让我们通过一用代码来表现的场景,来看看为什么这里枚举是有用的而且比结构体更合适。比如我们要处理 IP 地址。目前被广泛使用的两个主要 IP 标准:IPv4(version four)和 IPv6(version six)。这是我们的程序可能会遇到的 IP 地址的所有可能性:所以可以 **枚举** 出所有可能的值,这也正是它名字的由来。
|
||||
让我们在一个实际场景中看看在这个特定场景下,使用枚举比使用结构体更合适。假设我们要处理 IP 地址。目前被广泛使用的两个主要 IP 标准:IPv4(version four)和 IPv6(version six)。这是我们的程序可能会遇到的所有可能的 IP 地址类型:所以可以 **枚举** 出所有可能的值,这也正是它名字的由来。
|
||||
|
||||
任何一个 IP 地址要么是 IPv4 的要么是 IPv6 的而不能两者都是。IP 地址的这个特性使得枚举数据结构非常适合这个场景,因为枚举值只可能是其中一个成员。IPv4 和 IPv6 从根本上讲仍是 IP 地址,所以当代码在处理申请任何类型的 IP 地址的场景时应该把他们当作相同的类型。
|
||||
任何一个 IP 地址要么是 IPv4 的要么是 IPv6 的,而且不能两者都是。IP 地址的这个特性使得枚举数据结构非常适合这个场景,因为枚举值只可能是其中一个成员。IPv4 和 IPv6 从根本上讲仍是 IP 地址,所以当代码在处理申请任何类型的 IP 地址的场景时应该把它们当作相同的类型。
|
||||
|
||||
可以通过在代码中定义一个 `IpAddrKind` 枚举来表现这个概念并列出可能的 IP 地址类型,`V4` 和 `V6`。这被称为枚举的 **成员**(*variants*):
|
||||
|
||||
@ -33,7 +33,7 @@ let four = IpAddrKind::V4;
|
||||
let six = IpAddrKind::V6;
|
||||
```
|
||||
|
||||
注意枚举的成员位于其标识符的命名空间中,并使用两个冒号分开。这么设计的益处是现在 `IpAddrKind::V4` 和 `IpAddrKind::V6` 是相同类型的:`IpAddrKind`。例如,接着可以定义一个函数来获取任何 `IpAddrKind`:
|
||||
注意枚举的成员位于其标识符的命名空间中,并使用两个冒号分开。这么设计的益处是现在 `IpAddrKind::V4` 和 `IpAddrKind::V6` 都是 `IpAddrKind` 类型的。例如,接着可以定义一个函数来获取任何 `IpAddrKind`:
|
||||
|
||||
```rust
|
||||
# enum IpAddrKind {
|
||||
@ -101,7 +101,7 @@ let loopback = IpAddr::V6(String::from("::1"));
|
||||
|
||||
我们直接将数据附加到枚举的每个成员上,这样就不需要一个额外的结构体了。
|
||||
|
||||
使用枚举而不是结构体还有另外一个优势:每个成员可以处理不同类型和数量的数据。IPv4 版本的 IP 地址总是含有四个值在 0 和 255 之间的数字部分。如果我们想要将 `V4` 地址储存为四个 `u8` 值而 `V6` 地址仍然表现为一个 `String`,这就不能使用结构体了。枚举可以轻易处理的这个情况:
|
||||
用枚举替代结构体还有另一个优势:每个成员可以处理不同类型和数量的数据。IPv4 版本的 IP 地址总是含有四个值在 0 和 255 之间的数字部分。如果我们想要将 `V4` 地址储存为四个 `u8` 值而 `V6` 地址仍然表现为一个 `String`,这就不能使用结构体了。枚举可以轻易处理的这个情况:
|
||||
|
||||
```rust
|
||||
enum IpAddr {
|
||||
@ -114,7 +114,7 @@ let home = IpAddr::V4(127, 0, 0, 1);
|
||||
let loopback = IpAddr::V6(String::from("::1"));
|
||||
```
|
||||
|
||||
这些代码展示了使用枚举来储存两种不同 IP 地址的几种可能的选择。然而,事实证明储存和编码 IP 地址实在是太常见了[以致标准库提供了一个可供使用的定义!][IpAddr]<!-- ignore -->让我们看看标准库如何定义 `IpAddr` 的:它正有着跟我们定义和使用的一样的枚举和成员,不过它将成员中的地址数据嵌入到了两个不同形式的结构体中,他们对不同的成员的定义是不同的:
|
||||
这些代码展示了使用枚举来储存两种不同 IP 地址的几种可能的选择。然而,事实证明储存和编码 IP 地址实在是太常见了[以致标准库提供了一个可供使用的定义!][IpAddr]<!-- ignore -->让我们看看标准库如何定义 `IpAddr` 的:它正有着跟我们定义和使用的一样的枚举和成员,不过它将成员中的地址数据嵌入到了两个不同形式的结构体中,它们对不同的成员的定义是不同的:
|
||||
|
||||
[IpAddr]: https://doc.rust-lang.org/std/net/enum.IpAddr.html
|
||||
|
||||
@ -169,7 +169,7 @@ struct WriteMessage(String); // tuple struct
|
||||
struct ChangeColorMessage(i32, i32, i32); // tuple struct
|
||||
```
|
||||
|
||||
不过如果我们使用不同的结构体,他们都有不同的类型,将不能轻易的定义一个获取任何这些信息类型的函数,正如可以使用示例 6-2 中定义的 `Message` 枚举那样,因为他们是一个类型的。
|
||||
不过如果我们使用不同的结构体,它们都有不同的类型,将不能轻易的定义一个获取任何这些信息类型的函数,正如可以使用示例 6-2 中定义的 `Message` 枚举那样,因为它们是一个类型的。
|
||||
|
||||
结构体和枚举还有另一个相似点:就像可以使用 `impl` 来为结构体定义方法那样,也可以在枚举上定义方法。这是一个定义于我们 `Message` 枚举上的叫做 `call` 的方法:
|
||||
|
||||
@ -197,7 +197,7 @@ m.call();
|
||||
|
||||
### `Option` 枚举和其相对于空值的优势
|
||||
|
||||
在之前的部分,我们看到了 `IpAddr` 枚举如何利用 Rust 的类型系统编码更多信息而不单单是程序中的数据。这一部分探索一个 `Option` 的案例分析,它是另一个标准库定义的枚举。`Option` 类型应用广泛因为它编码了一个非常普遍的场景,就是一个值可能是某个值或者什么都不是。从类型系统的角度来表达这个概念就意味着编译器需要检查是否处理了所有应该处理的情况,这样就可以避免在其他编程语言中非常常见的 bug。
|
||||
在之前的部分,我们看到了 `IpAddr` 枚举如何利用 Rust 的类型系统编码更多信息而不单单是程序中的数据。接下来我们分析一个 `Option` 的案例,`Option` 是标准库定义的另一个枚举。`Option` 类型应用广泛因为它编码了一个非常普遍的场景,即一个值要么是某个值要么什么都不是。从类型系统的角度来表达这个概念就意味着编译器需要检查是否处理了所有应该处理的情况,这样就可以避免在其他编程语言中非常常见的 bug。
|
||||
|
||||
编程语言的设计经常从其包含功能的角度考虑问题,但是从其所排除在外的功能的角度思考也很重要。Rust 并没有很多其他语言中有的空值功能。**空值**(*Null* )是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。
|
||||
|
||||
@ -212,7 +212,7 @@ m.call();
|
||||
> crashes, which have probably caused a billion dollars of pain and damage in
|
||||
> the last forty years.
|
||||
>
|
||||
> 我称之为我万亿美元的错误。当时,我在为一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的使用都应该是绝对安全的。不过我未能抗拒引入一个空引用的诱惑,仅仅是因为它是这么的容易实现。这引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数以万计美元的苦痛和伤害。
|
||||
> 我称之为我十亿美元的错误。当时,我在为一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的使用都应该是绝对安全的。不过我未能抗拒引入一个空引用的诱惑,仅仅是因为它是这么的容易实现。这引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数十亿美元的苦痛和伤害。
|
||||
|
||||
空值的问题在于当你尝试像一个非空值那样使用一个空值,会出现某种形式的错误。因为空和非空的属性是无处不在的,非常容易出现这类错误。
|
||||
|
||||
@ -229,7 +229,7 @@ enum Option<T> {
|
||||
}
|
||||
```
|
||||
|
||||
`Option<T>` 是如此有用以至于它甚至被包含在了 prelude 之中:不需要显式导入它。另外,它的成员也是如此:可以不需要 `Option::` 前缀来直接使用 `Some` 和 `None`。即便如此 `Option<T>` 也仍是常规的枚举,`Some(T)` 和 `None` 仍是 `Option<T>` 的成员。
|
||||
`Option<T>` 是如此有用以至于它甚至被包含在了 prelude 之中,这意味着我们不需要显式导入它。另外,它的成员也是如此,可以不需要 `Option::` 前缀来直接使用 `Some` 和 `None`。即便如此 `Option<T>` 也仍是常规的枚举,`Some(T)` 和 `None` 仍是 `Option<T>` 的成员。
|
||||
|
||||
`<T>` 语法是一个我们还未讲到的 Rust 功能。它是一个泛型类型参数,第十章会更详细的讲解泛型。目前,所有你需要知道的就是 `<T>` 意味着 `Option` 枚举的 `Some` 成员可以包含任意类型的数据。这里是一些包含数字类型和字符串类型 `Option` 值的例子:
|
||||
|
||||
|
@ -4,11 +4,11 @@
|
||||
> <br>
|
||||
> commit 01dd4248621c2f510947592e47d16bdab9b14cf0
|
||||
|
||||
Rust 有一个叫做 `match` 的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较并根据匹配的模式执行相应代码。模式可由字面值、变量、通配符和许多其他内容构成;第十八章会涉及到所有不同种类的模式以及他们的作用。`match` 的力量来源于模式的表现力以及编译器检查,它确保了所有可能的情况都得到处理。
|
||||
Rust 有一个叫做 `match` 的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较并根据匹配的模式执行相应代码。模式可由字面值、变量、通配符和许多其他内容构成;第十八章会涉及到所有不同种类的模式以及它们的作用。`match` 的力量来源于模式的表现力以及编译器检查,它确保了所有可能的情况都得到处理。
|
||||
|
||||
可以把 `match` 表达式想象成某种硬币分类器:硬币滑入有着不同大小孔洞的轨道,每一个硬币都会掉入符合它大小的孔洞。同样地,值也会检查 `match` 的每一个模式,并且在遇到第一个 “符合” 的模式时,值会进入相关联的代码块并在执行中被使用。
|
||||
|
||||
因为刚刚提到了硬币,让我们用他们来作为一个使用 `match` 的例子!我们可以编写一个函数来获取一个未知的(美帝)硬币,并以一种类似验钞机的方式,确定它是何种硬币并返回它的美分值,如示例 6-3 中所示:
|
||||
因为刚刚提到了硬币,让我们用它们来作为一个使用 `match` 的例子!我们可以编写一个函数来获取一个未知的(美帝)硬币,并以一种类似验钞机的方式,确定它是何种硬币并返回它的美分值,如示例 6-3 中所示:
|
||||
|
||||
```rust
|
||||
enum Coin {
|
||||
@ -155,7 +155,7 @@ None => None,
|
||||
Some(i) => Some(i + 1),
|
||||
```
|
||||
|
||||
`Some(5)` 与 `Some(i)` 匹配吗?为什么不呢!他们是相同的成员。`i` 绑定了 `Some` 中包含的值,所以 `i` 的值是 `5`。接着匹配分支的代码被执行,所以我们将 `i` 的值加一并返回一个含有值 `6` 的新 `Some`。
|
||||
`Some(5)` 与 `Some(i)` 匹配吗?为什么不呢!它们是相同的成员。`i` 绑定了 `Some` 中包含的值,所以 `i` 的值是 `5`。接着匹配分支的代码被执行,所以我们将 `i` 的值加一并返回一个含有值 `6` 的新 `Some`。
|
||||
|
||||
#### 匹配 `None`
|
||||
|
||||
@ -192,7 +192,7 @@ error[E0004]: non-exhaustive patterns: `None` not covered
|
||||
```
|
||||
|
||||
|
||||
Rust 知道我们没有覆盖所有可能的情况甚至知道那些模式被忘记了!Rust 中的匹配是 **穷尽的**(*exhaustive):必须穷举到最后的可能性来使代码有效。特别的在这个 `Option<T>` 的例子中,Rust 防止我们忘记明确的处理 `None` 的情况,这使我们免于假设拥有一个实际上为空的值,这造成了之前提到过的价值亿万的错误。
|
||||
Rust 知道我们没有覆盖所有可能的情况甚至知道那些模式被忘记了!Rust 中的匹配是 **穷尽的**(*exhaustive*):必须穷举到最后的可能性来使代码有效。特别的在这个 `Option<T>` 的例子中,Rust 防止我们忘记明确的处理 `None` 的情况,这使我们免于假设拥有一个实际上为空的值,这造成了之前提到过的价值亿万的错误。
|
||||
|
||||
### `_` 通配符
|
||||
|
||||
|
@ -4,12 +4,12 @@
|
||||
> <br>
|
||||
> commit b707dc664960f0ffc495c373900d6b13e434927d
|
||||
|
||||
在你刚开始编写 Rust 程序时,代码可能仅仅位于 `main` 函数里。随着代码数量的增长,最终你会将功能移动到其他函数中,为了复用也为了更好的组织。通过将代码分隔成更小的块,每一个块代码自身就更易于理解。不过当你发现自己有太多的函数了该怎么办呢?Rust 有一个模块系统可以有组织的复用代码。
|
||||
在你刚开始编写 Rust 程序时,代码可能仅仅位于 `main` 函数里。随着代码数量的增长,为了复用和更好地组织代码,最终你会将功能移动到其他函数中。通过将代码分隔成更小的块,每一个块代码自身就更易于理解。不过当你发现自己有太多的函数了该怎么办呢?Rust 有一个模块系统可以有组织地复用代码。
|
||||
|
||||
就跟你将代码行提取到一个函数中一样,也可以将函数(和其他类似结构体和枚举的代码)提取到不同模块中。**模块**(*module*)是一个包含函数或类型定义的命名空间,你可以选择这些定义是能(公有)还是不能(私有)在其模块外可见。这是一个模块如何工作的概括:
|
||||
就跟你将代码行提取到一个函数中一样,也可以将函数(和其他类似结构体和枚举的代码)提取到不同模块中。**模块**(*module*)是一个包含函数或类型定义的命名空间,你可以选择这些定义能(公有)或不能(私有)在其模块外可见。下面是一个模块如何工作的梗概:
|
||||
|
||||
* 使用 `mod` 关键字声明新模块。此模块的代码要么直接位于声明之后的大括号中,要么位于另一个文件。
|
||||
* 函数、类型、常量和模块默认都是私有的。可以使用 `pub` 关键字将其变成公有并在命名空间之外可见。
|
||||
* `use` 关键字引入模块、或模块中的定义到作用域中以便于引用他们。
|
||||
* `use` 关键字将模块或模块中的定义引入到作用域中以便于引用它们。
|
||||
|
||||
我们会逐一了解这每一部分并学习如何将他们结合在一起。
|
||||
我们会逐一了解这每一部分并学习如何将它们结合在一起。
|
||||
|
@ -4,7 +4,7 @@
|
||||
> <br>
|
||||
> commit c6a9e77a1b1ed367e0a6d5dcd222589ad392a8ac
|
||||
|
||||
我们将通过使用 Cargo 创建一个新项目来开始我们的模块之旅,不过不再创建一个二进制 crate,而是创建一个库 crate:一个其他人可以作为依赖导入的项目。第二章猜猜看游戏中作为依赖使用的 `rand` 就是这样的 crate。
|
||||
我们将通过使用 Cargo 创建一个新项目来开始我们的模块之旅,不过这次不再创建一个二进制 crate,而是创建一个库 crate:一个其他人可以作为依赖导入的项目。第二章猜猜看游戏中作为依赖使用的 `rand` 就是这样的 crate。
|
||||
|
||||
我们将创建一个提供一些通用网络功能的项目的骨架结构;我们将专注于模块和函数的组织,而不担心函数体中的具体代码。这个项目叫做 `communicator`。Cargo 默认会创建一个库 crate 除非指定其他项目类型,所以如果不像一直以来那样加入 `--bin` 参数则项目将会是一个库:
|
||||
|
||||
@ -22,15 +22,16 @@ $ cd communicator
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Cargo 创建了一个空的测试来帮助我们开始库项目,不像使用 `--bin` 参数那样创建一个 “Hello, world!” 二进制项目。在本章之后的 “使用 `super` 访问父模块” 部分会介绍 `#[]` 和 `mod tests` 语法,目前只需确保他们位于 *src/lib.rs* 底部即可。
|
||||
Cargo 创建了一个空的测试来帮助我们开始库项目,不像使用 `--bin` 参数那样创建一个 “Hello, world!” 二进制项目。在本章之后的 “使用 `super` 访问父模块” 部分会介绍 `#[]` 和 `mod tests` 语法,目前只需确保它们位于 *src/lib.rs* 底部即可。
|
||||
|
||||
因为没有 *src/main.rs* 文件,所以没有可供 Cargo 的 `cargo run` 执行的东西。因此,我们将使用 `cargo build` 命令只是编译库 crate 的代码。
|
||||
|
||||
我们将学习根据编写代码的意图来选择不同的织库项目代码组织来适应多种场景。
|
||||
我们将学习根据编写代码的意图来选择不同的库项目代码组织来适应多种场景。
|
||||
|
||||
### 模块定义
|
||||
|
||||
@ -65,7 +66,7 @@ mod client {
|
||||
|
||||
<span class="caption">示例 7-1:`network` 模块和 `client` 一同定义于 *src/lib.rs*</span>
|
||||
|
||||
现在我们有了 `network::connect` 函数和 `client::connect` 函数。他们可能有着完全不同的功能,同时他们也不会彼此冲突,因为他们位于不同的模块。
|
||||
现在我们有了 `network::connect` 函数和 `client::connect` 函数。它们可能有着完全不同的功能,同时它们也不会彼此冲突,因为它们位于不同的模块。
|
||||
|
||||
在这个例子中,因为我们构建的是一个库,作为库入口点的文件是 *src/lib.rs*。然而,对于创建模块来说,*src/lib.rs* 并没有什么特殊意义。也可以在二进制 crate 的 *src/main.rs* 中创建模块,正如在库 crate 的 *src/lib.rs* 创建模块一样。事实上,也可以将模块放入其他模块中。这有助于随着模块的增长,将相关的功能组织在一起并又保持各自独立。如何选择组织代码依赖于如何考虑代码不同部分之间的关系。例如,对于库的用户来说,`client` 模块和它的函数 `connect` 可能放在 `network` 命名空间里显得更有道理,如示例 7-2 所示:
|
||||
|
||||
@ -85,7 +86,7 @@ mod network {
|
||||
|
||||
<span class="caption">示例 7-2:将 `client` 模块移动到 `network` 模块中</span>
|
||||
|
||||
在 *src/lib.rs* 文件中,将现有的 `mod network` 和 `mod client` 的定义替换为示例 7-2 中的定义,这里将 `client` 模块作为 `network` 的一个内部模块。现在我们有了 `network::connect` 和 `network::client::connect` 函数:同样的,这两个 `connect` 函数也不相冲突,因为他们在不同的命名空间中。
|
||||
在 *src/lib.rs* 文件中,将现有的 `mod network` 和 `mod client` 的定义替换为示例 7-2 中的定义,这里将 `client` 模块作为 `network` 的一个内部模块。现在我们有了 `network::connect` 和 `network::client::connect` 函数:同样的,这两个 `connect` 函数也不相冲突,因为它们在不同的命名空间中。
|
||||
|
||||
这样,模块之间形成了一个层次结构。*src/lib.rs* 的内容位于最顶层,而其子模块位于较低的层次。如下是示例 7-1 中的例子以层次的方式考虑的结构:
|
||||
|
||||
@ -95,7 +96,7 @@ communicator
|
||||
└── client
|
||||
```
|
||||
|
||||
而这是示例 7-2 中例子的的层次结构:
|
||||
而这是示例 7-2 中例子的层次结构:
|
||||
|
||||
```text
|
||||
communicator
|
||||
@ -103,7 +104,7 @@ communicator
|
||||
└── client
|
||||
```
|
||||
|
||||
可以看到示例 7-2 中,`client` 是 `network` 的子模块,而不是它的同级模块。更为复杂的项目可以有很多的模块,所以他们需要符合逻辑地组合在一起以便记录他们。在项目中 “符合逻辑” 的意义全凭你的理解和库的用户对你项目领域的认识。利用我们这里讲到的技术来创建同级模块和嵌套的模块,总有一个会是你会喜欢的结构。
|
||||
可以看到示例 7-2 中,`client` 是 `network` 的子模块,而不是它的同级模块。更为复杂的项目可以有很多的模块,所以它们需要符合逻辑地组合在一起以便记录它们。在项目中 “符合逻辑” 的意义全凭你的理解和库的用户对你项目领域的认识。利用我们这里讲到的技术来创建同级模块和嵌套的模块,总有一个会是你会喜欢的结构。
|
||||
|
||||
### 将模块移动到其他文件
|
||||
|
||||
@ -128,7 +129,7 @@ mod network {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 7-3:三个模块,`client`、`network` 和 `network::server`,他们都定义于 *src/lib.rs*</span>
|
||||
<span class="caption">示例 7-3:三个模块,`client`、`network` 和 `network::server`,它们都定义于 *src/lib.rs*</span>
|
||||
|
||||
*src/lib.rs* 文件有如下层次结构:
|
||||
|
||||
@ -139,7 +140,7 @@ communicator
|
||||
└── server
|
||||
```
|
||||
|
||||
如果这些模块有很多函数,而这些函数又很长,将难以在文件中寻找我们需要的代码。因为这些函数被嵌套进一个或多个模块中,同时函数中的代码也会开始变长。这就有充分的理由将`client`、`network` 和 `server`每一个模块从 *src/lib.rs* 抽出并放入他们自己的文件中。
|
||||
如果这些模块有很多函数,而这些函数又很长,将难以在文件中寻找我们需要的代码。因为这些函数被嵌套进一个或多个模块中,同时函数中的代码也会开始变长。这就有充分的理由将`client`、`network` 和 `server`每一个模块从 *src/lib.rs* 抽出并放入它们自己的文件中。
|
||||
|
||||
首先,将 `client` 模块的代码替换为只有 `client` 模块声明,这样 *src/lib.rs* 看起来应该像这样:
|
||||
|
||||
@ -205,7 +206,7 @@ warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||||
| ^
|
||||
```
|
||||
|
||||
这些警告提醒我们有从未被使用的函数。目前不用担心这些警告;在本章后面的 “使用 `pub` 控制可见性” 部分会解决他们。好消息是,他们仅仅是警告;我们的项目能够被成功编译。
|
||||
这些警告提醒我们有从未被使用的函数。目前不用担心这些警告,在本章后面的 “使用 `pub` 控制可见性” 部分会解决它们。好消息是,它们仅仅是警告,我们的项目能够被成功编译。
|
||||
|
||||
下面使用相同的模式将 `network` 模块提取到自己的文件中。删除 *src/lib.rs* 中 `network` 模块的内容并在声明后加上一个分号,像这样:
|
||||
|
||||
|
@ -26,9 +26,9 @@ warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||||
| ^
|
||||
```
|
||||
|
||||
那么为什么会出现这些错误信息呢?我们构建的是一个库,它的函数的目的是被 **用户** 使用,而不一定要被项目自身使用,所以不应该担心这些 `connect` 函数是未使用的。创建他们的意义就在于被另一个项目而不是被我们自己使用。
|
||||
那么为什么会出现这些错误信息呢?我们构建的是一个库,它的函数的目的是被 **用户** 使用,而不一定要被项目自身使用,所以不应该担心这些 `connect` 函数是未使用的。创建它们的意义就在于被另一个项目而不是被我们自己使用。
|
||||
|
||||
为了理解为什么这个程序出现了这些警告,尝试作为另一个项目来使用这个 `connect` 库,从外部调用他们。为此,通过创建一个包含这些代码的 *src/main.rs* 文件,在与库 crate 相同的目录创建一个二进制 crate:
|
||||
为了理解为什么这个程序出现了这些警告,尝试作为另一个项目来使用这个 `connect` 库,从外部调用它们。为此,通过创建一个包含这些代码的 *src/main.rs* 文件,在与库 crate 相同的目录创建一个二进制 crate:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -109,7 +109,7 @@ warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||||
|
||||
编译通过了,关于 `client::connect` 未被使用的警告消失了!
|
||||
|
||||
未被使用的代码并不总是意味着他们需要被设为公有的:如果你 **不** 希望这些函数成为公有 API 的一部分,未被使用的代码警告可能是在警告你这些代码不再需要并可以安全的删除他们。这也可能是警告你出 bug 了,如果你刚刚不小心删除了库中所有这个函数的调用。
|
||||
未被使用的代码并不总是意味着它们需要被设为公有的:如果你 **不** 希望这些函数成为公有 API 的一部分,未被使用的代码警告可能是在警告你这些代码不再需要并可以安全的删除它们。这也可能是警告你出 bug 了,如果你刚刚不小心删除了库中所有这个函数的调用。
|
||||
|
||||
当然我们的情况是,**确实** 希望另外两个函数也作为 crate 公有 API 的一部分,所以让我们也将其标记为 `pub` 并去掉剩余的警告。修改 *src/network/mod.rs* 为:
|
||||
|
||||
@ -138,7 +138,7 @@ warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||||
| ^
|
||||
```
|
||||
|
||||
恩,虽然将 `network::connect` 设为 `pub` 了我们仍然得到了一个未被使用函数的警告。这是因为模块中的函数是公有的,不过函数所在的 `network` 模块却不是公有的。这回我们是自内向外修改库文件的,而 `client::connect` 的时候是自外向内修改的。我们需要修改 *src/lib.rs* 让 `network` 也是公有的:
|
||||
虽然将 `network::connect` 设为 `pub` 了我们仍然得到了一个未被使用函数的警告。这是因为模块中的函数是公有的,不过函数所在的 `network` 模块却不是公有的。这回我们是自内向外修改库文件的,而 `client::connect` 的时候是自外向内修改的。我们需要修改 *src/lib.rs* 让 `network` 也是公有的:
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
@ -210,12 +210,12 @@ fn try_me() {
|
||||
|
||||
#### 修改错误
|
||||
|
||||
这里有一些尝试修复错误的代码修改意见。在你尝试他们之前,猜测一下他们哪个能修复错误,接着编译查看你是否猜对了,并结合私有性规则理解为什么。
|
||||
这里有一些尝试修复错误的代码修改意见。在你尝试它们之前,猜测一下它们哪个能修复错误,接着编译查看你是否猜对了,并结合私有性规则理解为什么。
|
||||
|
||||
* 如果 `inside` 模块是公有的?
|
||||
* 如果 `outermost` 是公有的而 `inside` 是私有的?
|
||||
* 如果在 `inner_function` 函数体中调用 `::outermost::middle_secret_function()`?(开头的两个冒号意味着从根模块开始引用模块。)
|
||||
|
||||
请随意设计更多的实验并尝试理解他们!
|
||||
请随意设计更多的实验并尝试理解它们!
|
||||
|
||||
接下来,让我们讨论一下使用 `use` 关键字将模块项目引入作用域。
|
@ -72,7 +72,7 @@ fn main() {
|
||||
|
||||
这使得我们可以忽略所有的模块并直接引用函数。
|
||||
|
||||
因为枚举也像模块一样组成了某种命名空间,也可以使用 `use` 来导入枚举的成员。对于任何类型的 `use` 语句,如果从一个命名空间导入多个项,可以使用大括号和逗号来列举他们,像这样:
|
||||
因为枚举也像模块一样组成了某种命名空间,也可以使用 `use` 来导入枚举的成员。对于任何类型的 `use` 语句,如果从一个命名空间导入多个项,可以使用大括号和逗号来列举它们,像这样:
|
||||
|
||||
```rust
|
||||
enum TrafficLight {
|
||||
@ -110,7 +110,7 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
`*` 被称为 **全局导入**(*glob*),它会导入命名空间中所有可见的项。全局导入应该保守的使用:他们是方便的,但是也可能会引入多于你预期的内容从而导致命名冲突。
|
||||
`*` 被称为 **全局导入**(*glob*),它会导入命名空间中所有可见的项。全局导入应该保守的使用:它们是方便的,但是也可能会引入多于你预期的内容从而导致命名冲突。
|
||||
|
||||
### 使用 `super` 访问父模块
|
||||
|
||||
@ -127,6 +127,7 @@ pub mod network;
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -167,7 +168,7 @@ error[E0433]: failed to resolve. Use of undeclared type or module `client`
|
||||
| ^^^^^^^^^^^^^^^ Use of undeclared type or module `client`
|
||||
```
|
||||
|
||||
编译失败了,不过为什么呢?并不需要像 *src/main.rs* 那样将 `communicator::` 置于函数前,因为这里肯定是在 `communicator` 库 crate 之内的。之所以失败的原因是路径是相对于当前模块的,在这里就是 `tests`。唯一的例外就是 `use` 语句,它默认是相对于 crate 根模块的。我们的 `tests` 模块需要 `client` 模块位于其作用域中!
|
||||
编译失败了,不过为什么呢?并不需要像 *src/main.rs* 那样将 `communicator::` 置于函数前,因为这里肯定是在 `communicator` 库 crate 之内的。失败的原因是路径是相对于当前模块的,在这里就是 `tests`。唯一的例外就是 `use` 语句,它默认是相对于 crate 根模块的。我们的 `tests` 模块需要 `client` 模块位于其作用域中!
|
||||
|
||||
那么如何在模块层次结构中回退一级模块,以便在 `tests` 模块中能够调用 `client::connect`函数呢?在 `tests` 模块中,要么可以在开头使用双冒号来让 Rust 知道我们想要从根模块开始并列出整个路径:
|
||||
|
||||
@ -216,6 +217,6 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||||
|
||||
## 总结
|
||||
|
||||
现在你掌握了组织代码的核心科技!利用他们将相关的代码组合在一起、防止代码文件过长并将一个整洁的公有 API 展现给库的用户。
|
||||
现在你掌握了组织代码的核心科技!利用它们将相关的代码组合在一起、防止代码文件过长并将一个整洁的公有 API 展现给库的用户。
|
||||
|
||||
接下来,让我们看看一些标准库提供的集合数据类型,你可以利用他们编写出漂亮整洁的代码。
|
||||
接下来,让我们看看一些标准库提供的集合数据类型,你可以利用它们编写出漂亮整洁的代码。
|
||||
|
@ -14,4 +14,4 @@ Rust 标准库中包含一系列被称为 **集合**(*collections*)的非常
|
||||
|
||||
[collections]: https://doc.rust-lang.org/std/collections
|
||||
|
||||
我们将讨论如何创建和更新 vector、字符串和哈希 map,以及他们有什么不同。
|
||||
我们将讨论如何创建和更新 vector、字符串和哈希 map,以及它们有什么不同。
|
||||
|
@ -4,17 +4,17 @@
|
||||
> <br>
|
||||
> commit 6c24544ba718bce0755bdaf03423af86280051d5
|
||||
|
||||
我们要讲到的第一个类型是`Vec<T>`,也被称为 *vector*。vector 允许我们在一个单独的数据结构中储存多于一个值,它在内存中彼此相邻的排列所有的值。vector 只能储存相同类型的值。他们在拥有一系列项的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。
|
||||
我们要讲到的第一个类型是`Vec<T>`,也被称为 *vector*。vector 允许我们在一个单独的数据结构中储存多于一个值,它在内存中彼此相邻地排列所有的值。vector 只能储存相同类型的值。它们在拥有一系列项的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。
|
||||
|
||||
### 新建 vector
|
||||
|
||||
为了创建一个新的,空的 vector,可以调用 `Vec::new` 函数:
|
||||
为了创建一个新的空 vector,可以调用 `Vec::new` 函数:
|
||||
|
||||
```rust
|
||||
let v: Vec<i32> = Vec::new();
|
||||
```
|
||||
|
||||
注意这里我们增加了一个类型注解。因为没有向这个 vector 中插入任何值,Rust 并不知道我们想要储存什么类型的元素。这是一个非常重要的点。vector 是同质的(homogeneous):他们可以储存很多值,不过这些值必须都是相同类型的。vector 是用泛型实现的,第十章会涉及到如何对你自己的类型使用他们。现在,所有你需要知道的就是 `Vec` 是一个由标准库提供的类型,它可以存放任何类型,而当 `Vec` 存放某个特定类型时,那个类型位于尖括号中。这里我们告诉 Rust `v` 这个 `Vec` 将存放 `i32` 类型的元素。
|
||||
注意这里我们增加了一个类型注解。因为没有向这个 vector 中插入任何值,Rust 并不知道我们想要储存什么类型的元素。这是一个非常重要的点。vector 是同质的(homogeneous):它们可以储存很多值,不过这些值必须都是相同类型的。vector 是用泛型实现的,第十章会涉及到如何对你自己的类型使用它们。现在,所有你需要知道的就是 `Vec` 是一个由标准库提供的类型,它可以存放任何类型,而当 `Vec` 存放某个特定类型时,那个类型位于尖括号中。这里我们告诉 Rust `v` 这个 `Vec` 将存放 `i32` 类型的元素。
|
||||
|
||||
在实际的代码中,一旦插入值 Rust 就可以推断出想要存放的类型,所以你很少会需要这些类型注解。更常见的做法是使用初始值来创建一个 `Vec`,而且为了方便 Rust 提供了 `vec!` 宏。这个宏会根据我们提供的值来创建一个新的 `Vec`。如下代码会新建一个拥有值 `1`、`2` 和 `3` 的 `Vec<i32>`:
|
||||
|
||||
@ -56,7 +56,7 @@ v.push(8);
|
||||
|
||||
### 读取 vector 的元素
|
||||
|
||||
现在你知道如何创建、更新和销毁 vector 了,接下来的一步最好了解一下如何读取他们的内容。有两种方法引用 vector 中储存的值。为了更加清楚的说明这个例子,我们标注这些函数返回的值的类型。
|
||||
现在你知道如何创建、更新和销毁 vector 了,接下来的一步最好了解一下如何读取它们的内容。有两种方法引用 vector 中储存的值。为了更加清楚的说明这个例子,我们标注这些函数返回的值的类型。
|
||||
|
||||
这个例子展示了访问 vector 中一个值的两种方式,索引语法或者 `get` 方法:
|
||||
|
||||
@ -80,7 +80,7 @@ let does_not_exist = v.get(100);
|
||||
|
||||
当运行这段代码,你会发现对于第一个 `[]` 方法,当引用一个不存在的元素时 Rust 会造成 `panic!`。这个方法更适合当程序认为尝试访问超过 vector 结尾的元素是一个严重错误的情况,这时应该使程序崩溃。
|
||||
|
||||
当 `get` 方法被传递了一个数组外的索引时,它不会 panic 而是返回 `None`。当偶尔出现超过 vector 范围的访问属于正常情况的时候可以考虑使用它。接着你的代码可以有处理 `Some(&element)` 或 `None` 的逻辑,如第六章讨论的那样。例如,索引可能来源于用户输入的数字。如果他们不慎输入了一个过大的数字那么程序就会得到 `None` 值,你可以告诉用户 `Vec` 当前元素的数量并再请求他们输入一个有效的值。这就比因为输入错误而使程序崩溃要友好的多!
|
||||
当 `get` 方法被传递了一个数组外的索引时,它不会 panic 而是返回 `None`。当偶尔出现超过 vector 范围的访问属于正常情况的时候可以考虑使用它。接着你的代码可以有处理 `Some(&element)` 或 `None` 的逻辑,如第六章讨论的那样。例如,索引可能来源于用户输入的数字。如果它们不慎输入了一个过大的数字那么程序就会得到 `None` 值,你可以告诉用户 `Vec` 当前元素的数量并再请求它们输入一个有效的值。这就比因为输入错误而使程序崩溃要友好的多!
|
||||
|
||||
#### 无效引用
|
||||
|
||||
@ -137,6 +137,6 @@ let row = vec![
|
||||
|
||||
Rust 在编译时就必须准确的知道 vector 中类型的原因在于它需要知道储存每个元素到底需要多少内存。第二个好处是可以准确的知道这个 vector 中允许什么类型。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。使用枚举外加 `match` 意味着 Rust 能在编译时就保证总是会处理所有可能的情况,正如第六章讲到的那样。
|
||||
|
||||
如果在编写程序时不能确切无遗的知道运行时会储存进 vector 的所有类型,枚举技术就行不通了。相反,你可以使用 trait 对象,第十七章会讲到它。
|
||||
如果在编写程序时不能确切无遗地知道运行时会储存进 vector 的所有类型,枚举技术就行不通了。相反,你可以使用 trait 对象,第十七章会讲到它。
|
||||
|
||||
现在我们了解了一些使用 vector 的最常见的方式,请一定去看看标准库中 `Vec` 定义的很多其他实用方法的 API 文档。例如,除了 `push` 之外还有一个 `pop` 方法,它会移除并返回 vector 的最后一个元素。让我们继续下一个集合类型:`String`!
|
@ -10,11 +10,11 @@
|
||||
|
||||
### 什么是字符串?
|
||||
|
||||
在开始深入这些方面之前,我们需要讨论一下术语 **字符串** 的具体意义。Rust 的核心语言中事实上就只有一种字符串类型:`str`,字符串 slice,它通常以被借用的形式出现,`&str`。第四章讲到了 **字符串 slice**:他们是一些储存在别处的 UTF-8 编码字符串数据的引用。比如字符串字面值被储存在程序的二进制输出中,字符串 slice 也是如此。
|
||||
在开始深入这些方面之前,我们需要讨论一下术语 **字符串** 的具体意义。Rust 的核心语言中事实上就只有一种字符串类型:`str`,字符串 slice,它通常以被借用的形式出现,`&str`。第四章讲到了 **字符串 slice**:它们是一些储存在别处的 UTF-8 编码字符串数据的引用。比如字符串字面值被储存在程序的二进制输出中,字符串 slice 也是如此。
|
||||
|
||||
称作 `String` 的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。当 Rustacean 们谈到 Rust 的 “字符串”时,他们通常指的是 `String` 和字符串 slice `&str`类型,而不是其中一个。这一部分大部分是关于 `String` 的,不过这些类型在 Rust 标准库中都被广泛使用。`String` 和字符串 slice 都是 UTF-8 编码的。
|
||||
称作 `String` 的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。当 Rustacean 们谈到 Rust 的 “字符串”时,它们通常指的是 `String` 和字符串 slice `&str`类型,而不是其中一个。这一部分大部分是关于 `String` 的,不过这些类型在 Rust 标准库中都被广泛使用。`String` 和字符串 slice 都是 UTF-8 编码的。
|
||||
|
||||
Rust 标准库中还包含一系列其他字符串类型,比如 `OsString`、`OsStr`、`CString` 和 `CStr`。相关库 crate 甚至会提供更多储存字符串数据的选择。与 `*String`/`*Str` 的命名类似,他们通常也提供有所有权和可借用的变体,就比如说 `String`/`&str`。这些字符串类型在储存的编码或内存表现形式上可能有所不同。本章将不会讨论其他这些字符串类型;查看 API 文档来更多的了解如何使用他们以及各自适合的场景。
|
||||
Rust 标准库中还包含一系列其他字符串类型,比如 `OsString`、`OsStr`、`CString` 和 `CStr`。相关库 crate 甚至会提供更多储存字符串数据的选择。与 `*String`/`*Str` 的命名类似,它们通常也提供有所有权和可借用的变体,就比如说 `String`/`&str`。这些字符串类型在储存的编码或内存表现形式上可能有所不同。本章将不会讨论其他这些字符串类型;查看 API 文档来更多的了解如何使用它们以及各自适合的场景。
|
||||
|
||||
### 新建字符串
|
||||
|
||||
@ -26,7 +26,7 @@ let mut s = String::new();
|
||||
|
||||
这新建了一个叫做 `s` 的空的字符串,接着我们可以向其中装载数据。
|
||||
|
||||
通常字符串会有初始数据因为我们希望一开始就有这个字符串。为此,使用 `to_string` 方法,它能用于任何实现了 `Display` trait 的类型,对于字符串字面值是这样:
|
||||
通常字符串会有初始数据,因为我们希望一开始就有这个字符串。为此,使用 `to_string` 方法,它能用于任何实现了 `Display` trait 的类型,对于字符串字面值是这样:
|
||||
|
||||
```rust
|
||||
let data = "initial contents";
|
||||
@ -39,13 +39,13 @@ let s = "initial contents".to_string();
|
||||
|
||||
这会创建一个包含 `initial contents` 的字符串。
|
||||
|
||||
也可以使用 `String::from` 函数来从字符串字面值创建 `String`。如下等同于使用 `to_string`:
|
||||
也可以使用 `String::from` 函数来从字符串字面值创建 `String`。下面代码等同于使用 `to_string`:
|
||||
|
||||
```rust
|
||||
let s = String::from("initial contents");
|
||||
```
|
||||
|
||||
因为字符串使用广泛,这里有很多不同的用于字符串的通用 API 可供选择。他们有些可能显得有些多余,不过都有其用武之地!在这个例子中,`String::from` 和 `.to_string` 最终做了完全相同的工作,所以如何选择就是风格问题了。
|
||||
因为字符串使用广泛,这里有很多不同的用于字符串的通用 API 可供选择。它们有些可能显得有些多余,不过都有其用武之地!在这个例子中,`String::from` 和 `.to_string` 最终做了完全相同的工作,所以如何选择就是风格问题了。
|
||||
|
||||
记住字符串是 UTF-8 编码的,所以可以包含任何可以正确编码的数据:
|
||||
|
||||
@ -195,13 +195,13 @@ let answer = &hello[0];
|
||||
224, 165, 135]
|
||||
```
|
||||
|
||||
这里有 18 个字节,也就是计算机最终会储存的数据。如果从 Unicode 标量值的角度理解他们,也就像 Rust 的 `char` 类型那样,这些字节看起来像这样:
|
||||
这里有 18 个字节,也就是计算机最终会储存的数据。如果从 Unicode 标量值的角度理解它们,也就像 Rust 的 `char` 类型那样,这些字节看起来像这样:
|
||||
|
||||
```text
|
||||
['न', 'म', 'स', '्', 'त', 'े']
|
||||
```
|
||||
|
||||
这里有六个 `char`,不过第四个和第六个都不是字母,他们是发音符号本身并没有任何意义。最后,如果以字形簇的角度理解,就会得到人们所说的构成这个单词的四个字母:
|
||||
这里有六个 `char`,不过第四个和第六个都不是字母,它们是发音符号本身并没有任何意义。最后,如果以字形簇的角度理解,就会得到人们所说的构成这个单词的四个字母:
|
||||
|
||||
```text
|
||||
["न", "म", "स्", "ते"]
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
最后介绍的常用集合类型是 **哈希 map**(*hash map*)。`HashMap<K, V>` 类型储存了一个键类型 `K` 对应一个值类型 `V` 的映射。它通过一个 **哈希函数**(*hashing function*)来实现映射,决定如何将键和值放入内存中。很多编程语言支持这种数据结构,不过通常有不同的名字:哈希、map、对象、哈希表或者关联数组,仅举几例。
|
||||
|
||||
哈希 map 可以用于需要任何类型作为键来寻找数据的情况,而不是像 vector 那样通过索引。例如,在一个游戏中,你可以将每个团队的分数记录到哈希 map 中,其中键是队伍的名字而值是每个队伍的分数。给出一个队名,就能得到他们的得分。
|
||||
哈希 map 可以用于需要任何类型作为键来寻找数据的情况,而不是像 vector 那样通过索引。例如,在一个游戏中,你可以将每个团队的分数记录到哈希 map 中,其中键是队伍的名字而值是每个队伍的分数。给出一个队名,就能得到它们的得分。
|
||||
|
||||
本章我们会介绍哈希 map 的基本 API,不过还有更多吸引人的功能隐藏于标准库中 `HashMap` 定义的函数中。请一如既往地查看标准库文档来了解更多信息。
|
||||
|
||||
@ -24,7 +24,7 @@ scores.insert(String::from("Yellow"), 50);
|
||||
```
|
||||
|
||||
注意必须首先 `use` 标准库中集合部分的 `HashMap`。在这三个常用集合中,`HashMap` 是最不常用的,所以并没有被 prelude 自动引用。标准库中对 `HashMap` 的支持也相对较少,例如,并没有内建的构建宏。
|
||||
像 vector 一样,哈希 map 将他们的数据储存在堆上,这个 `HashMap` 的键类型是 `String` 而值类型是 `i32`。同样类似于 vector,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。
|
||||
像 vector 一样,哈希 map 将它们的数据储存在堆上,这个 `HashMap` 的键类型是 `String` 而值类型是 `i32`。同样类似于 vector,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。
|
||||
|
||||
另一个构建哈希 map 的方法是使用一个元组的 vector 的 `collect` 方法,其中每个元组包含一个键值对。`collect` 方法可以将数据收集进一系列的集合类型,包括 `HashMap`。例如,如果队伍的名字和初始分数分别在两个 vector 中,可以使用 `zip` 方法来创建一个元组的 vector,其中 “Blue” 与 10 是一对,依此类推。接着就可以使用 `collect` 方法将这个元组 vector 转换成一个 `HashMap`:
|
||||
|
||||
@ -163,7 +163,7 @@ println!("{:?}", map);
|
||||
|
||||
### 哈希函数
|
||||
|
||||
`HashMap` 默认使用一种密码学安全的哈希函数,它可以抵抗拒绝服务(Denial of Service, DoS)攻击。然而并不是最快的,不过为了更高的安全性值得付出一些性能的代价。如果性能监测显示此哈希函数非常慢,以致于你无法接受,你可以指定一个不同的 *hasher* 来切换为其它函数。hasher 是一个实现了 `BuildHasher` trait 的类型。第十章会讨论 trait 和如何实现他们。你并不需要从头开始实现你自己的 hasher;crates.io 有其他人分享的实现了许多常用哈希算法的 hasher 的库。
|
||||
`HashMap` 默认使用一种密码学安全的哈希函数,它可以抵抗拒绝服务(Denial of Service, DoS)攻击。然而并不是最快的,不过为了更高的安全性值得付出一些性能的代价。如果性能监测显示此哈希函数非常慢,以致于你无法接受,你可以指定一个不同的 *hasher* 来切换为其它函数。hasher 是一个实现了 `BuildHasher` trait 的类型。第十章会讨论 trait 和如何实现它们。你并不需要从头开始实现你自己的 hasher;crates.io 有其他人分享的实现了许多常用哈希算法的 hasher 的库。
|
||||
|
||||
## 总结
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
> <br>
|
||||
> commit 4f2dc564851dc04b271a2260c834643dfd86c724
|
||||
|
||||
Rust 对可靠性的执着也扩展到了错误处理。错误对于软件来说是不可避免的,所以 Rust 有很多功能来处理当现错误的情况。在很多情况下,Rust 要求你承认出错的可能性并在编译代码之前就采取行动。通过确保不会只有在将代码部署到生产环境之后才会发现错误来使得程序更可靠。
|
||||
Rust 对可靠性的执着也扩展到了错误处理。错误对于软件来说是不可避免的,所以 Rust 有很多功能来处理出现错误的情况。在很多情况下,Rust 要求你承认出错的可能性并在编译代码之前就采取行动。通过确保不会只有在将代码部署到生产环境之后才会发现错误来使得程序更可靠。
|
||||
|
||||
Rust 将错误组合成两个主要类别:**可恢复错误**(*recoverable*)和 **不可恢复错误**(*unrecoverable*)。可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件。不可恢复错误通常是 bug 的同义词,比如尝试访问超过数组结尾的位置。
|
||||
|
||||
|
@ -205,11 +205,11 @@ fn read_username_from_file() -> Result<String, io::Error> {
|
||||
|
||||
首先让我们看看函数的返回值:`Result<String, io::Error>`。这意味着函数返回一个 `Result<T, E>` 类型的值,其中泛型参数 `T` 的具体类型是 `String`,而 `E` 的具体类型是 `io::Error`。如果这个函数没有出任何错误成功返回,函数的调用者会收到一个包含 `String` 的 `Ok` 值————函数从文件中读取到的用户名。如果函数遇到任何错误,函数的调用者会收到一个 `Err` 值,它储存了一个包含更多这个问题相关信息的 `io::Error` 实例。这里选择 `io::Error` 作为函数的返回值是因为它正好是函数体中那两个可能会失败的操作的错误返回值:`File::open` 函数和 `read_to_string` 方法。
|
||||
|
||||
函数体以 `File::open` 函数开头。接着使用 `match` 处理返回值 `Result`,类似于示例 9-3 中的 `match`,唯一的区别是不再当 `Err` 时调用 `panic!`,而是提早返回并将 `File::open` 返回的错误值作为函数的错误返回值传递给调用者。如果 `File::open` 成功了,我们将文件句柄储存在变量 `f` 中并继续。
|
||||
函数体以 `File::open` 函数开头。接着使用 `match` 处理返回值 `Result`,类似于示例 9-3 中的 `match`,唯一的区别是当 `Err` 时不再调用 `panic!`,而是提早返回并将 `File::open` 返回的错误值作为函数的错误返回值传递给调用者。如果 `File::open` 成功了,我们将文件句柄储存在变量 `f` 中并继续。
|
||||
|
||||
接着我们在变量 `s` 中创建了一个新 `String` 并调用文件句柄 `f` 的 `read_to_string` 方法来将文件的内容读取到 `s` 中。`read_to_string` 方法也返回一个 `Result` 因为它也可能会失败:哪怕是 `File::open` 已经成功了。所以我们需要另一个 `match` 来处理这个 `Result`:如果 `read_to_string` 成功了,那么这个函数就成功了,并返回文件中的用户名,它现在位于被封装进 `Ok` 的 `s` 中。如果`read_to_string` 失败了,则像之前处理 `File::open` 的返回值的 `match` 那样返回错误值。并不需要显式的调用 `return`,因为这是函数的最后一个表达式。
|
||||
|
||||
调用这个函数的代码最终会得到一个包含用户名的 `Ok` 值,或者一个包含 `io::Error` 的 `Err` 值。我们无从得知调用者会如何处理这些值。例如,如果他们得到了一个 `Err` 值,他们可能会选择 `panic!` 并使程序崩溃、使用一个默认的用户名或者从文件之外的地方寻找用户名。我们没有足够的信息知晓调用者具体会如何尝试,所以将所有的成功或失败信息向上传播,让他们选择合适处理方法。
|
||||
调用这个函数的代码最终会得到一个包含用户名的 `Ok` 值,或者一个包含 `io::Error` 的 `Err` 值。我们无从得知调用者会如何处理这些值。例如,如果他们得到了一个 `Err` 值,他们可能会选择 `panic!` 并使程序崩溃、使用一个默认的用户名或者从文件之外的地方寻找用户名。我们没有足够的信息知晓调用者具体会如何尝试,所以将所有的成功或失败信息向上传播,让他们选择合适的处理方法。
|
||||
|
||||
这种传播错误的模式在 Rust 是如此的常见,以至于有一个更简便的专用语法:`?`。
|
||||
|
||||
@ -234,7 +234,7 @@ fn read_username_from_file() -> Result<String, io::Error> {
|
||||
|
||||
`Result` 值之后的 `?` 被定义为与示例 9-5 中定义的处理 `Result` 值的 `match` 表达式有着完全相同的工作方式。如果 `Result` 的值是 `Ok`,这个表达式将会返回 `Ok` 中的值而程序将继续执行。如果值是 `Err`,`Err` 中的值将作为整个函数的返回值,就好像使用了 `return` 关键字一样,这样错误值就被传播给了调用者。
|
||||
|
||||
在示例 9-6 的上下文中,`File::open` 调用结尾的 `?` 将会把 `Ok` 中的值返回给变量 `f`。如果出现了错误,`?` 会提早返回整个函数并将任何 `Err` 值传播给调用者。同理也适用于 `read_to_string` 调用结尾的 `?`。
|
||||
在示例 9-6 的上下文中,`File::open` 调用结尾的 `?` 将会把 `Ok` 中的值返回给变量 `f`。如果出现了错误,`?` 会提早返回整个函数并将一些 `Err` 值传播给调用者。同理也适用于 `read_to_string` 调用结尾的 `?`。
|
||||
|
||||
`?` 消除了大量样板代码并使得函数的实现更简单。我们甚至可以在 `?` 之后直接使用链式方法调用来进一步缩短代码:
|
||||
|
||||
@ -252,7 +252,7 @@ fn read_username_from_file() -> Result<String, io::Error> {
|
||||
}
|
||||
```
|
||||
|
||||
在 `s` 中创建新的 `String` 被放到了函数开头;这没有什么变化。我们对 `File::open("hello.txt")?` 的结果直接链式调用了 `read_to_string`,而不再创建变量 `f`。仍然需要 `read_to_string` 调用结尾的 `?`,而且当 `File::open` 和 `read_to_string` 都成功没有失败时返回包含用户名 `s` 的 `Ok` 值。其功能再一次与示例 9-5 和示例 9-5 保持一致,不过这是一个与众不同且更符合工程学的写法。
|
||||
在 `s` 中创建新的 `String` 被放到了函数开头;这没有什么变化。我们对 `File::open("hello.txt")?` 的结果直接链式调用了 `read_to_string`,而不再创建变量 `f`。仍然需要 `read_to_string` 调用结尾的 `?`,而且当 `File::open` 和 `read_to_string` 都成功没有失败时返回包含用户名 `s` 的 `Ok` 值。其功能再一次与示例 9-5 和示例 9-6 保持一致,不过这是一个与众不同且更符合工程学的写法。
|
||||
|
||||
### `?` 只能被用于返回 `Result` 的函数
|
||||
|
||||
|
@ -40,7 +40,7 @@ let home = "127.0.0.1".parse::<IpAddr>().unwrap();
|
||||
|
||||
无论代码编写的多么好,当有害状态是预期会出现时,返回 `Result` 仍要比调用 `panic!` 更为合适。这样的例子包括解析器接收到错误数据,或者 HTTP 请求返回一个表明触发了限流的状态。在这些例子中,应该通过返回 `Result` 来表明失败预期是可能的,这样将有害状态向上传播,这样调用者就可以决定该如何处理这个问题。使用 `panic!` 来处理这些情况就不是最好的选择。
|
||||
|
||||
当代码对值进行操作时,应该首先验证值是有效的,并在其无效时 `panic!`。这主要是出于安全的原因:尝试操作无效数据会暴露代码漏洞,这就是标准库在尝试越界访问数组时会 `panic!` 的主要原因:尝试访问不属于当前数据结构的内存是一个常见的安全隐患。函数通常都遵循 **契约**(*contracts*):他们的行为只有在输入满足特定条件时才能得到保证。当违反契约时 panic 是有道理的,因为这这通常代表调用方的 bug,而且这也不是那种你希望调用方必须处理的错误。事实上也没有合理的方式来恢复调用方的代码:调用方的 **程序员** 需要修复他的代码。函数的契约,尤其是当违反它会造成 panic 的契约,应该在函数的 API 文档中得到解释。
|
||||
当代码对值进行操作时,应该首先验证值是有效的,并在其无效时 `panic!`。这主要是出于安全的原因:尝试操作无效数据会暴露代码漏洞,这就是标准库在尝试越界访问数组时会 `panic!` 的主要原因:尝试访问不属于当前数据结构的内存是一个常见的安全隐患。函数通常都遵循 **契约**(*contracts*):他们的行为只有在输入满足特定条件时才能得到保证。当违反契约时 panic 是有道理的,因为这通常代表调用方的 bug,而且这也不是那种你希望调用方必须处理的错误。事实上也没有合理的方式来恢复调用方的代码:调用方的 **程序员** 需要修复他的代码。函数的契约,尤其是当违反它会造成 panic 的契约,应该在函数的 API 文档中得到解释。
|
||||
|
||||
虽然在所有函数中都拥有许多错误检查是冗长而烦人的。幸运的是,可以利用 Rust 的类型系统(以及编译器的类型检查)为你进行很多检查。如果函数有一个特定类型的参数,可以在知晓编译器已经确保其拥有一个有效值的前提下进行你的代码逻辑。例如,如果你使用了一个不同于 `Option` 的类型,而且程序期望它是 **有值** 的并且不是 **空值**。你的代码无需处理 `Some` 和 `None` 这两种情况,它只会有一种情况就是绝对会有一个值。尝试向函数传递空值的代码甚至根本不能编译,所以你的函数在运行时没有必要判空。另外一个例子是使用像 `u32` 这样的无符号整型,也会确保它永远不为负。
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
每一个编程语言都有高效的处理重复概念的工具;在 Rust 中工具之一就是 **泛型**(*generics*)。泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性,比如他们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道他们在这里实际上代表什么。
|
||||
|
||||
同理为了编写一份可以用于多种具体值的代码,函数并不知道其参数为何值,这时就可以让函数获取泛型而不是像 `i32` 或 `String` 这样的具体值。我们已经使用过第六章的 `Option<T>`,第八章的 `Vec<T>` 和 `HashMap<K, V>`,以及第九章的 `Result<T, E>` 这些泛型了。本章会探索如何使用泛型定义我们自己自己的类型、函数和方法!
|
||||
同理为了编写一份可以用于多种具体值的代码,函数并不知道其参数为何值,这时就可以让函数获取泛型而不是像 `i32` 或 `String` 这样的具体值。我们已经使用过第六章的 `Option<T>`,第八章的 `Vec<T>` 和 `HashMap<K, V>`,以及第九章的 `Result<T, E>` 这些泛型了。本章会探索如何使用泛型定义我们自己的类型、函数和方法!
|
||||
|
||||
首先,我们将回顾一下提取函数以减少代码重复的机制。接着使用一个只在参数类型上不同的泛型函数来实现相同的功能。我们也会讲到结构体和枚举定义中的泛型。
|
||||
|
||||
|
@ -253,7 +253,7 @@ fn main() {
|
||||
|
||||
<span class="caption">示例 10-9:在 `Point<T>` 结构体上实现方法 `x`,它返回 `T` 类型的字段 `x` 的引用</span>
|
||||
|
||||
注意必须在 `impl` 后面声明 `T`,这样就可以在 `Point<T>` 上实现的方法中使用它了。在 `impl` 之后声明泛型 `T` ,这样 Rust 就知道 `Point` 的加括号中的类型是泛型而不是具体类型。例如,可以选择为 `Point<f32>` 实例实现方法,而不是为泛型 `Point` 实例。示例 10-10 展示了一个没有在 `impl` 之后(的尖括号)声明泛型的例子,这里使用了一个具体类型,`f32`:
|
||||
注意必须在 `impl` 后面声明 `T`,这样就可以在 `Point<T>` 上实现的方法中使用它了。在 `impl` 之后声明泛型 `T` ,这样 Rust 就知道 `Point` 的尖括号中的类型是泛型而不是具体类型。例如,可以选择为 `Point<f32>` 实例实现方法,而不是为泛型 `Point` 实例。示例 10-10 展示了一个没有在 `impl` 之后(的尖括号)声明泛型的例子,这里使用了一个具体类型,`f32`:
|
||||
|
||||
```rust
|
||||
# struct Point<T> {
|
||||
|
@ -115,7 +115,7 @@ impl Summarizable for WeatherForecast {
|
||||
|
||||
另外这段代码假设 `Summarizable` 是一个公有 trait,这是因为示例 10-12 中 `trait` 之前使用了 `pub` 关键字。
|
||||
|
||||
trait 实现的一个需要注意的限制是:只能在 trait 或对应类型位于我们 crate 本地的时候为其实现 trait。换句话说,不允许对外部类型实现外部 trait。例如,不能在 `Vec` 上实现 `Display` trait,因为 `Display` 和 `Vec` 都定义于标准库中。允许在像 `Tweet` 这样作为我们 `aggregator`crate 部分功能的自定义类型上实现标准库中的 trait `Display`。也允许在 `aggregator`crate 中为 `Vec` 实现 `Summarizable`,因为 `Summarizable` 定义于此。这个限制是我们称为 **孤儿规则**(*orphan rule*)的一部分,如果你感兴趣的可以在类型理论中找到它。简单来说,它被称为 orphan rule 是因为其父类型不存在。没有这条规则的话,两个 crate 可以分别对相同类型是实现相同的 trait,因而这两个实现会相互冲突:Rust 将无从得知应该使用哪一个。因为 Rust 强制执行 orphan rule,其他人编写的代码不会破坏你代码,反之亦是如此。
|
||||
trait 实现的一个需要注意的限制是:只能在 trait 或对应类型位于我们 crate 本地的时候为其实现 trait。换句话说,不允许对外部类型实现外部 trait。例如,不能在 `Vec` 上实现 `Display` trait,因为 `Display` 和 `Vec` 都定义于标准库中。允许在像 `Tweet` 这样作为我们 `aggregator`crate 部分功能的自定义类型上实现标准库中的 trait `Display`。也允许在 `aggregator`crate 中为 `Vec` 实现 `Summarizable`,因为 `Summarizable` 定义于此。这个限制是我们称为 **孤儿规则**(*orphan rule*)的一部分,如果你感兴趣的可以在类型理论中找到它。简单来说,它被称为 orphan rule 是因为其父类型不存在。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,因而这两个实现会相互冲突:Rust 将无从得知应该使用哪一个。因为 Rust 强制执行 orphan rule,其他人编写的代码不会破坏你代码,反之亦是如此。
|
||||
|
||||
### 默认实现
|
||||
|
||||
|
@ -97,7 +97,7 @@ looking arrows and labels? /Carol -->
|
||||
|
||||
这里 `x` 拥有生命周期 `'b`,比 `'a` 要大。这就意味着 `r` 可以引用 `x`:Rust 知道 `r` 中的引用在 `x` 有效的时候也总是有效的。
|
||||
|
||||
现在我们已经在一个具体的例子中展示了引用的声明周期位于何处,并讨论了 Rust 如何分析生命周期来保证引用总是有效的,接下来让我们聊聊在函数的上下文中参数和返回值的泛型生命周期。
|
||||
现在我们已经在一个具体的例子中展示了引用的生命周期位于何处,并讨论了 Rust 如何分析生命周期来保证引用总是有效的,接下来让我们聊聊在函数的上下文中参数和返回值的泛型生命周期。
|
||||
|
||||
### 函数中的泛型生命周期
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
程序的正确性意味着代码如我们期望的那样运行。Rust 是一个非常注重正确性的编程语言,不过正确性是一个难以证明的复杂主题。Rust 的类型系统在此问题上下了很大的功夫,不过它不可能捕获所有种类的错误。为此,Rust 也在语言本身包含了编写软件测试的支持。
|
||||
|
||||
例如,我们可以编写一个叫做 `add_two` 的将传递给它的值加二的函数。它的签名有一个整型参数并返回一个整型值。当实现和编译这个函数时,Rust 会进行所有目前我们已经见过的的类型检查和借用检查,例如,这些检查会确保我们不会传递 `String` 或无效的引用给这个函数。Rust 所 **不能** 检查的是这个函数是否会准确的完成我们期望的工作:返回参数加二后的值,而不是比如说参数加 10 或减 50 的值!这也就是测试出场的地方。
|
||||
例如,我们可以编写一个叫做 `add_two` 的将传递给它的值加二的函数。它的签名有一个整型参数并返回一个整型值。当实现和编译这个函数时,Rust 会进行所有目前我们已经见过的类型检查和借用检查,例如,这些检查会确保我们不会传递 `String` 或无效的引用给这个函数。Rust 所 **不能** 检查的是这个函数是否会准确的完成我们期望的工作:返回参数加二后的值,而不是比如说参数加 10 或减 50 的值!这也就是测试出场的地方。
|
||||
|
||||
我们可以编写测试断言,比如说,当传递 `3` 给 `add_two` 函数时,应该得到 `5`。当对代码进行修改时可以运行测试来确保任何现存的正确行为没有被改变。
|
||||
|
||||
|
@ -135,7 +135,7 @@ error: test failed
|
||||
|
||||
<span class="caption">示例 11-4:一个测试通过和一个测试失败的测试结果</span>
|
||||
|
||||
`test tests::another` 这一行是 `FAILED` 而不是 `ok` 了。在单独测试结果和总结之间多了两个新的部分:第一个部分显示了测试失败的详细原因。在这个例子中,`another` 因为 `panicked at 'Make this test fail'` 而失败,这位于 *src/lib.rs* 的第 9 行。下一部分仅仅列出了所有失败的测试,这在很有多测试和很多失败测试的详细输出时很有帮助。可以使用失败测试的名称来只运行这个测试,这样比较方便调试;下一部分会讲到更多运行测试的方法。
|
||||
`test tests::another` 这一行是 `FAILED` 而不是 `ok` 了。在单独测试结果和总结之间多了两个新的部分:第一个部分显示了测试失败的详细原因。在这个例子中,`another` 因为 `panicked at 'Make this test fail'` 而失败,这位于 *src/lib.rs* 的第 9 行。下一部分仅仅列出了所有失败的测试,这在有很多测试和很多失败测试的详细输出时很有帮助。可以使用失败测试的名称来只运行这个测试,这样比较方便调试;下一部分会讲到更多运行测试的方法。
|
||||
|
||||
最后是总结行:总体上讲,一个测试结果是 `FAILED` 的。有一个测试通过和一个测试失败。
|
||||
|
||||
@ -462,7 +462,7 @@ mod tests {
|
||||
|
||||
<span class="caption">示例 11-8:测试会造成 `panic!` 的条件</span>
|
||||
|
||||
`#[should_panic]` 属性位于 `#[test]` 之后和对应的测试函数之前。让我们看看测试通过时它时什么样子:
|
||||
`#[should_panic]` 属性位于 `#[test]` 之后和对应的测试函数之前。让我们看看测试通过时它是什么样子:
|
||||
|
||||
```text
|
||||
running 1 test
|
||||
|
@ -45,7 +45,7 @@ fn main() {
|
||||
>
|
||||
> 注意 `std::env::args` 在其任何参数包含无效 Unicode 字符时会 panic。如果你需要接受包含无效 Unicode 字符的参数,使用 `std::env::args_os` 代替。这个函数返回 `OsString` 值而不是 `String` 值。这里出于简单考虑使用了 `std::env::args`,因为 `OsString` 值每个平台都不一样而且比 `String` 值处理起来更复杂。
|
||||
|
||||
在 `main` 函数的第一行,我们调用了 `env::args`,并立即使用 `collect` 来创建了一个包含迭代器所有值的 vector。`collect` 可以被用来创建很多类型的集合,所以这里显式注明的 `args` 类型来指定我们需要一个字符串 vector。虽然在 Rust 中我们很少会需要注明类型,`collect` 就是一个经常需要注明类型的函数,因为 Rust 不能推断出你想要什么类型的集合。
|
||||
在 `main` 函数的第一行,我们调用了 `env::args`,并立即使用 `collect` 来创建了一个包含迭代器所有值的 vector。`collect` 可以被用来创建很多类型的集合,所以这里显式注明 `args` 的类型来指定我们需要一个字符串 vector。虽然在 Rust 中我们很少会需要注明类型,`collect` 就是一个经常需要注明类型的函数,因为 Rust 不能推断出你想要什么类型的集合。
|
||||
|
||||
最后,我们使用调试格式 `:?` 打印出 vector。让我们尝试不用参数运行代码,接着用两个参数:
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
第一,`main` 现在进行了两个任务:它解析了参数并打开了文件。对于一个这样的小函数,这并不是一个大问题。然而如果 `main` 中的功能持续增加,`main` 函数处理的独立的任务也会增加。当函数承担了更多责任,它就更难以推导,更难以测试,并且更难以在不破坏其他部分的情况下做出修改。最好能分离出功能以便每个函数就负责一个任务。
|
||||
|
||||
这同时也关系到第二个问题:`search` 和 `filename` 是程序中的配置变量,而像 `f` 和 `contents` 则用来执行程序逻辑。随着 `main` 函数的增长,就需要引入更多的变量到作用域中,而当作用域中有更多的变量时,将更难以追踪每个变量的目的。最好能将将配置变量组织进一个结构这样就能使他们的目的更明确了。
|
||||
这同时也关系到第二个问题:`search` 和 `filename` 是程序中的配置变量,而像 `f` 和 `contents` 则用来执行程序逻辑。随着 `main` 函数的增长,就需要引入更多的变量到作用域中,而当作用域中有更多的变量时,将更难以追踪每个变量的目的。最好能将配置变量组织进一个结构这样就能使他们的目的更明确了。
|
||||
|
||||
第三个问题是如果打开文件失败我们使用 `expect` 来打印出错误信息,不过这个错误信息只是说 `file not found`。除了缺少文件之外还有很多打开文件可能失败的方式:例如,文件可能存在,不过可能没有打开它的权限。如果我们现在就出于这种情况,打印出的 `file not found` 错误信息就给了用户错误的建议!
|
||||
|
||||
|
@ -214,7 +214,7 @@ fn generate_workout(intensity: i32, random_number: i32) {
|
||||
|
||||
### 闭包类型推断和注解
|
||||
|
||||
闭包与由 `fn` 函数定义的函数有一些区别。第一是不要求像 `fn` 函数那样在参数和返回值上注明类型。
|
||||
闭包与由 `fn` 关键字定义的函数有一些区别。第一是不要求像 `fn` 函数那样在参数和返回值上注明类型。
|
||||
|
||||
函数中需要类型注解是因为他们是暴露给用户的显式接口的一部分。严格的定义这些接口对于保证所有人都认同函数使用和返回值的类型来说是很重要的。但是闭包并不用于这样暴露在外的接口:他们储存在变量中并被使用,不用命名他们或暴露给库的用户调用。
|
||||
|
||||
@ -237,7 +237,7 @@ let expensive_closure = |num: i32| -> i32 {
|
||||
|
||||
<span class="caption">示例 13-7:为闭包的参数和返回值增加可选的类型注解</span>
|
||||
|
||||
有了类型注解闭包的语法就更类似函数了。如下是一个对其参数加一的函数的定义与拥有相同行为闭包语法的纵向对比。这里增加了一些空格来对其相应部分。这展示了闭包语法如何类似于函数语法,除了使用竖线而不是括号以及几个可选的语法:
|
||||
有了类型注解闭包的语法就更类似函数了。如下是一个对其参数加一的函数的定义与拥有相同行为闭包语法的纵向对比。这里增加了一些空格来对齐相应部分。这展示了闭包语法如何类似于函数语法,除了使用竖线而不是括号以及几个可选的语法:
|
||||
|
||||
```rust,ignore
|
||||
fn add_one_v1 (x: i32) -> i32 { x + 1 }
|
||||
@ -248,7 +248,7 @@ let add_one_v4 = |x| x + 1 ;
|
||||
|
||||
第一行展示了一个函数定义,而第二行展示了一个完整标注的闭包定义。第三行闭包定义中省略了类型注解,而第四行去掉了可选的大括号,因为闭包体只有一行。
|
||||
|
||||
闭包定义会为每个参数和返回值推断一个具体类型。例如,示例 13-8 中展示了仅仅将参数作为返回值的简短的闭包定义。除了作为示例的目的这个闭包并不是很实用。注意其定义并没有增加任何类型注解:如果尝试调用闭包两次,第一次使用 `String` 类型作为参数而第一次使用 `i32`,则会得到一个错误:
|
||||
闭包定义会为每个参数和返回值推断一个具体类型。例如,示例 13-8 中展示了仅仅将参数作为返回值的简短的闭包定义。除了作为示例的目的这个闭包并不是很实用。注意其定义并没有增加任何类型注解:如果尝试调用闭包两次,第一次使用 `String` 类型作为参数而第二次使用 `i32`,则会得到一个错误:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -279,7 +279,7 @@ error[E0308]: mismatched types
|
||||
|
||||
### 使用带有泛型和 `Fn` trait 的闭包
|
||||
|
||||
回到我们的健身计划生成 app ,在示例 13-6 中的代码仍然调用了多于需要的慢计算闭包。在全部代码中的每一个需要多个慢计算闭包结果的地方,可以将将结果保存进变量以供复用,这样就可以使用变量而不是再次调用闭包。但是这样就会有很多重复的保存结果变量的地方。
|
||||
回到我们的健身计划生成 app ,在示例 13-6 中的代码仍然调用了多于需要的慢计算闭包。在全部代码中的每一个需要多个慢计算闭包结果的地方,可以将结果保存进变量以供复用,这样就可以使用变量而不是再次调用闭包。但是这样就会有很多重复的保存结果变量的地方。
|
||||
|
||||
然而,因为拥有一个慢计算的闭包,我们还可以采取另一个解决方案。可以创建一个存放闭包和调用闭包结果的结构体。该结构体只会在需要结果时执行闭包,并会缓存结果值,这样余下的代码就不必再负责保存结果并可以复用该值。你可能见过这种模式被称 *memoization* 或 *lazy evaluation*。
|
||||
|
||||
|
@ -99,7 +99,7 @@ fn iterator_sum() {
|
||||
|
||||
### `Iterator` trait 中产生其他迭代器的方法
|
||||
|
||||
`Iterator` trait 中定义的另一类方法会产生其他的迭代器。这些方法被称为 **迭代器适配器**(*iterator adaptors*),他们允许我们将当前迭代器变为不同类型的迭代器。可以链式调用多个迭代器适配器。不过因为所有的迭代器都是惰性的,必须调用一个消费适配器方法以便获取迭代器适配器调用的结果。示例 13-17 展示了一个调用迭代器适配器方法 `map` 的例子,它会获取一个 `map` 会在每一个项上调用的闭包来产生一个新迭代器,它的每一项为 vector 中每一项加一。不过这些代码会产生一个警告:
|
||||
`Iterator` trait 中定义的另一类方法会产生其他的迭代器。这些方法被称为 **迭代器适配器**(*iterator adaptors*),他们允许我们将当前迭代器变为不同类型的迭代器。可以链式调用多个迭代器适配器。不过因为所有的迭代器都是惰性的,必须调用一个消费适配器方法以便获取迭代器适配器调用的结果。示例 13-17 展示了一个调用迭代器适配器方法 `map` 的例子,该 `map` 方法使用闭包来调用每个元素以生成新的迭代器。 这里的闭包创建了一个新的迭代器,其中 `vector` 中的每个元素都被加1。不过这些代码会产生一个警告:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
|
@ -102,7 +102,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||||
|
||||
第七章介绍了如何使用 `mod` 关键字来将代码组织进模块中,如何使用 `pub` 关键字将项变为公有,和如何使用 `use` 关键字将项引入作用域。然而对你开发来说很有道理的结果可能对用户来说就不太方便了。你可能希望将结构组织进有多个层次的层级中,不过想要使用被定义在很深层级中的类型的人可能很难发现这些类型是否存在。他们也可能会厌烦 `use my_crate::some_module::another_module::UsefulType;` 而不是 `use my_crate::UsefulType;` 来使用类型。
|
||||
|
||||
公有 API 的结构是你发布 crate 时主要需要考虑的。crate 用户没有你那么熟悉其结构,并且如何模块层级过大他们可能会难以找到所需的部分。
|
||||
公有 API 的结构是你发布 crate 时主要需要考虑的。crate 用户没有你那么熟悉其结构,并且如果模块层级过大他们可能会难以找到所需的部分。
|
||||
|
||||
好消息是,如果结果对于用户来说 **不是** 很方便,你也无需重新安排内部组织:你可以选择使用 `pub use` 重导出(re-export)项来使公有结构不同于私有结构。重导出获取位于一个位置的公有项并将其公开到另一个位置,好像它就定义在这个新位置一样。
|
||||
|
||||
@ -309,7 +309,7 @@ Uploading guessing_game v0.1.0 (file:///projects/guessing_game)
|
||||
|
||||
### 发布现存 crate 的新版本
|
||||
|
||||
当你修改了 crate 并准备好发布新版本时,改变 *Cargo.toml* 中 `version` 所指定的值。请使用 [语义化版本规则][semver] 来根据修改的类型决定下一个版本呢号。接着运行 `cargo publish` 来上传新版本。
|
||||
当你修改了 crate 并准备好发布新版本时,改变 *Cargo.toml* 中 `version` 所指定的值。请使用 [语义化版本规则][semver] 来根据修改的类型决定下一个版本号。接着运行 `cargo publish` 来上传新版本。
|
||||
|
||||
[semver]: http://semver.org/
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
||||
|
||||
对于引用和`Box<T>`,借用规则的不可变性作用于编译时。对于`RefCell<T>`,这些不可变性作用于**运行时**。对于引用,如果违反这些规则,会得到一个编译错误。而对于`RefCell<T>`,违反这些规则会`panic!`。
|
||||
|
||||
Rust 编译器执行的静态分析天生是保守的。代码的一些属性则不可能通过分析代码发现:其中最著名的就是停机问题(停机问题),这超出了本书的范畴,不过如果你感兴趣的话这是一个值得研究的有趣主题。
|
||||
Rust 编译器执行的静态分析天生是保守的。代码的一些属性则不可能通过分析代码发现:其中最著名的就是停机问题(Halting Problem),这超出了本书的范畴,不过如果你感兴趣的话这是一个值得研究的有趣主题。
|
||||
|
||||
因为一些分析是不可能的,Rust 编译器在其不确定的时候甚至都不尝试猜测,所以说它是保守的而且有时会拒绝事实上不会违反 Rust 保证的正确的程序。换句话说,如果 Rust 接受不正确的程序,那么人们也就不会相信 Rust 所做的保证了。如果 Rust 拒绝正确的程序,会给程序员带来不便,但不会带来灾难。`RefCell<T>`正是用于当你知道代码遵守借用规则,而编译器不能理解的时候。
|
||||
|
||||
|
@ -323,7 +323,7 @@ impl State for Published {
|
||||
|
||||
#### 将状态和行为编码为类型
|
||||
|
||||
我们将展示如何稍微反思状态模式来进行一系列不同的权衡取舍。不同于完全封装状态和状态转移使得外部代码对其毫不知情,我们将将状态编码进不同的类型。当状态是类型时,Rust 的类型检查就会使任何在只能使用发布的博文的地方使用草案博文的尝试变为编译时错误。
|
||||
我们将展示如何稍微反思状态模式来进行一系列不同的权衡取舍。不同于完全封装状态和状态转移使得外部代码对其毫不知情,我们将状态编码进不同的类型。当状态是类型时,Rust 的类型检查就会使任何在只能使用发布的博文的地方使用草案博文的尝试变为编译时错误。
|
||||
|
||||
让我们考虑一下列表 17-11 中 `main` 的第一部分:
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user