介绍

ch01-00-introduction.md
commit c6920d4a2ee0f282addaf8f6945cefe3ef7bdf09

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

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

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

为本书做出贡献

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

安装

ch01-01-installation.md
commit f828919e62aa542aaaae03c1fb565da42374213e

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

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

在 Linux 或 Mac 上安装

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

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

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

Rust is installed now. Great!

当然,如果你不赞成curl | sh这种模式,可以随意下载、检查和运行这个脚本。

在 Windows 上安装

在 Windows 上,前往https://rustup.rs并按照说明下载rustup-init.exe。运行并遵循它提供的其余指示。

本书其余 Windows 相关的命令假设你使用cmd作为你的 shell。如果你使用不同的 shell,可能能够执行 Linux 和 Mac 用户相同的命令。如果都不行,查看所使用的 shell 的文档。

自定义安装

如果有理由倾向于不使用 rustup.rs,请查看Rust 安装页面获取其他选择。

卸载

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

$ rustup self uninstall

故障排除

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

$ rustc --version

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

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

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

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

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

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

本地文档

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

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

Hello, World!

ch01-02-hello-world.md
commit aa1801d99cd3b19c96533f00c852b1c4bd5350a6

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

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

创建项目文件

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

Linux 和 Mac:

$ mkdir ~/projects
$ cd ~/projects
$ mkdir hello_world
$ cd hello_world

Windows:

> mkdir %USERPROFILE%\projects
> cd %USERPROFILE%\projects
> mkdir hello_world
> cd hello_world

编写并运行 Rust 程序

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

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

Filename: main.rs

fn main() {
    println!("Hello, world!");
}

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

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

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

分析 Rust 程序

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

fn main() {

}

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

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

main()函数中:

    println!("Hello, world!");

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

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

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

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

编译和运行是两个步骤

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

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

$ rustc main.rs

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

$ ls
main  main.rs

在 Windows 上,输入:

> dir /B %= the /B option says to only show the file names =%
main.exe
main.rs

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

$ ./main  # or .\main.exe on Windows

如果 main.rs 是我们的“Hello, world!”程序,它将会在终端上打印Hello, world!

来自 Ruby、Python 或 JavaScript 这样的动态类型语言背景的同学,可能不太习惯在分开的步骤编译和执行程序。Rust 是一种 静态提前编译语言ahead-of-time compiled language),这意味着可以编译好程序后,把它给任何人,他们都不需要安装 Rust 就可运行。如果你给他们一个 .rb.py.js 文件,他们需要先分别安装 Ruby,Python,JavaScript 实现(运行时环境,VM),不过你只需要一句命令就可以编译和执行程序。这一切都是语言设计的权衡取舍。

仅仅使用rustc编译简单程序是没问题的,不过随着项目的增长,你将想要能够控制你项目拥有的所有选项,并使其易于分享你的代码给别人或别的项目。接下来,我们将介绍一个叫做 Cargo 的工具,它将帮助你编写现实生活中的 Rust 程序。

Hello, Cargo!

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

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

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

$ cargo --version

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

使用 Cargo 创建项目

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

Linux 和 Mac:

$ cd ~/projects

Windows:

> cd %USERPROFILE%\projects

并在任何操作系统运行:

$ cargo new hello_cargo --bin
$ cd hello_cargo

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

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

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

Filename: Cargo.toml

[package]
name = "hello_cargo"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]

[dependencies]

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

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

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

现在看看 src/main.rs

fn main() {
    println!("Hello, world!");
}

Cargo 为你生成了一个“Hello World!”,正如我们之前编写的那个!目前为止我们所见过的之前项目与 Cargo 生成的项目区别有:

  • 代码位于 src 目录
  • 项目根目录包含一个 Cargo.toml 配置文件

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

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

构建并运行 Cargo 项目

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

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

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

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

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

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

Filename: Cargo.lock

[root]
name = "hello_cargo"
version = "0.1.0"

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

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

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

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

$ cargo run
   Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
     Running `target/debug/hello_cargo`
Hello, world!

所以又出现一些更多的不同:

  • 使用cargo build构建项目(或使用cargo run一步构建并运行),而不是使用rustc
  • 不同于将构建结果放在源码相同目录,Cargo 会将它放到 target/debug 目录中的文件,我们将会看到

Cargo 的另一个有点是不管你使用什么操作系统它的命令都是一样的,所以之后我们将不再为 Linux 和 Mac 以及 Windows 提供特定的命令。

发布构建

当项目最终准备好发布了,可以使用cargo build --release来优化编译项目。这会在 target/release 下生成可执行文件,而不是 target/debug。这些优化可以让 Rust 代码运行的更快,不过启用他们会让程序花更长的时间编译。这也是为何这是两种不同的配置:一个为了开发,这时你经常想要快速重新构建;另一个构建提供给用户的最终程序,这时并不会重新构建并希望能运行得越快越好。如果你在测试代码的运行时间,请确保运行cargo build --release并使用 target/release 下的可执行文件进行测试。

把 Cargo 当作习惯

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

$ git clone someurl.com/someproject
$ cd someproject
$ carg

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

猜猜看

ch02-00-guessing-game-tutorial.md
commit 77370c073661548dd56bbcb43cc64713585acbba

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

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

准备一个新项目

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

$ cargo new guessing_game --bin
$ cd guessing_game

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

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

Filename: Cargo.toml

[package]
name = "guessing_game"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]

[dependencies]

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

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

Filename: src/main.rs

fn main() {
    println!("Hello, world!");
}

现在让我们使用cargo run在相同的步骤编译并运行这个“Hello, world!”程序:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
     Running `target/debug/guessing_game`
Hello, world!

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

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

处理一次猜测

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

Filename: src/main.rs
use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin().read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {}", guess);
}

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

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

use std::io;

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

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

fn main() {

fn语法声明了一个新函数,()表明没有参数,{作为函数体的开始。

第一章也讲到了,println!是一个在屏幕上打印字符串的宏:

println!("Guess the number!");

println!("Please input your guess.");

这些代码仅仅打印一个提示,说明游戏的内容并请求用户输入。

用变量储存值

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

let mut guess = String::new();

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

let foo = bar;

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

let foo = 5; // immutable
let mut bar = 5; // mutable

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

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

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

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

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

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

io::stdin().read_line(&mut guess)
    .expect("Failed to read line");

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

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

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

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

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

.expect("Failed to read line");

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

io::stdin().read_line(&mut guess).expect("Failed to read line");

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

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

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

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

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

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

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

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
src/main.rs:10:5: 10:39 warning: unused result which must be used,
#[warn(unused_must_use)] on by default
src/main.rs:10     io::stdin().read_line(&mut guess);
                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

使用println!占位符打印值

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

println!("You guessed: {}", guess);

这行代码打印出存储了用户输入的字符串。这对{}是一个在特定位置预留值的占位符。可以使用{}打印多个值:第一个{}对应格式化字符串之后列出的第一个值,第二个对应第二个值,以此类推。用一个println!调用打印多个值应该看起来像这样:

let x = 5;
let y = 10;

println!("x = {} and y = {}", x, y);

这行代码会打印出x = 5 and y = 10

测试第一部分代码

让我们来测试下猜猜看游戏的第一部分。使用cargo run运行它:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6

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

生成一个秘密数字

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

使用 crate 来增加更多功能

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

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

Filename: Cargo.toml

[dependencies]

rand = "0.3.14"

Cargo.toml 文件中,任何标题之后的内容都是属于这个部分的,一直持续到直到另一个部分开始。[dependencies]部分告诉 Cargo 项目依赖了哪个外部 crate 和需要的 crate 版本。在这个例子中,我们使用语义化版本符号0.3.14来指定randcrate。Cargo 理解语义化版本(Semantic Versioning)(有时也称为 SemVer),这是一个编写版本号的标准。版本号0.3.14事实上是^0.3.14的缩写,它的意思是“任何与 0.3.14 版本公有 API 相兼容的版本”。

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

$ cargo build
    Updating registry `https://github.com/rust-lang/crates.io-index`
 Downloading rand v0.3.14
 Downloading libc v0.2.14
   Compiling libc v0.2.14
   Compiling rand v0.3.14
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)

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

可能会出现不同的版本号(不过多亏了语义化版本,它们与代码是兼容的!),同时显示顺序也可能会有所不同。

现在我们有了一个外部依赖,Cargo 从 registryCrates.io)上获取了一份(兼容的)最新版本代码的拷贝。Crates.io 是 Rust 生态环境中的人们向他人贡献他们的开源 Rust 项目的地方。

在更新完 registry (索引)后,Cargo 检查[dependencies]部分并下载还不存在部分。在这个例子中,虽然只列出了rand一个依赖,Cargo 也获取了一份libc的拷贝,因为rand依赖libc来正常工作。在下载他们之后,Rust 编译他们接着用这些依赖编译项目。

如果不做任何修改就立刻再次运行cargo build,则不会有任何输出。Cargo 知道它已经下载并编译了依赖,同时 Cargo.toml 文件中也没有任何相关修改。Cargo 也知道代码没有做任何修改,所以它也不会重新编译代码。因为无事可做,它简单的退出了。如果打开 src/main.rs 文件,并做一些普通的修改,保存并再次构建,只会出现一行输出:

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

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

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

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

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

更新 crate 到一个新版本

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

  1. 忽略 Cargo.lock 文件并计算出所有符合 Cargo.toml 中规格的最新版本。
  2. 如果成功了,Cargo 会把这些版本写入 Cargo.lock 文件。

不过,Cargo 默认只会寻找大于0.3.0而小于0.4.0的版本。如果rand crate 发布了两个新版本,0.3.150.4.0,在运行cargo update时会出现如下内容:

$ cargo update
    Updating registry `https://github.com/rust-lang/crates.io-index`
    Updating rand v0.3.14 -> v0.3.15

这时,值得注意的是 Cargo.lock 文件中的一个改变,rand crate 现在使用的版本是0.3.15

如果想要使用0.4.0版本的rand或是任何0.4.x系列的版本,必须像这样更新 Cargo.toml 文件:

[dependencies]

rand = "0.4.0"

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

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

生成一个随机数

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

Filename: src/main.rs
extern crate rand;

use std::io;
use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1, 101);

    println!("The secret number is: {}", secret_number);

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin().read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {}", guess);
}

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

我们在顶部增加一行extern crate rand;来让 Rust 知道我们要使用外部依赖。这也会调用相应的use rand,所以现在可以使用rand::前缀来调用rand中的任何内容。

接下来,我们增加了另一行useuse rand::RngRng是一个定义了随机数生成器应实现方法的 trait,如果要使用这些方法的话这个 trait 必须在作用域中。第十章会详细介绍 trait。

另外,中间还新增加了两行。rand::thread_rng函数会提供具体会使用的随机数生成器:它位于当前执行线程本地并从操作系统获取 seed。接下来,调用随机数生成器的gen_range方法。这个方法由我们使用use rand::Rng语句引入到作用域的Rng trait 定义。gen_range方法获取两个数作为参数并生成一个两者之间的随机数。它包含下限但不包含上限,所以需要指定1101来请求一个1100之间的数。

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

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

尝试运行程序几次:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4
$ cargo run
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5

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

比较猜测与秘密数字

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

Filename: src/main.rs
extern crate rand;

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1, 101);

    println!("The secret number is: {}", secret_number);

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin().read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {}", guess);

    match guess.cmp(&secret_number) {
        Ordering::Less    => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal   => println!("You win!"),
    }
}

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

新代码的第一行是另一个use,从标准库引入了一个叫做std::cmp::Ordering的类型到作用域。Ordering是另一个枚举,像Result一样,不过Ordering的成员是LessGreaterEqual。这是你比较两个值时可能出现三种结果。

接着在底部的五行新代码使用了Ordering类型:

match guess.cmp(&secret_number) {
    Ordering::Less    => println!("Too small!"),
    Ordering::Greater => println!("Too big!"),
    Ordering::Equal   => println!("You win!"),
}

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

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

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

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

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
  --> src/main.rs:23:21
   |
23 |     match guess.cmp(&secret_number) {
   |                     ^^^^^^^^^^^^^^ expected struct `std::string::String`, found integral variable
   |
   = note: expected type `&std::string::String`
   = note:    found type `&{integer}`

error: aborting due to previous error
Could not compile `guessing_game`.

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

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

Filename: src/main.rs

extern crate rand;

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1, 101);

    println!("The secret number is: {}", secret_number);

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin().read_line(&mut guess)
        .expect("Failed to read line");

    let guess: u32 = guess.trim().parse()
        .expect("Please type a number!");

    println!("You guessed: {}", guess);

    match guess.cmp(&secret_number) {
        Ordering::Less    => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal   => println!("You win!"),
    }
}

这两行代码是:

let guess: u32 = guess.trim().parse()
    .expect("Please type a number!");

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

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

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

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

现在让我们运行程序!

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
     Running `target/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
  76
You guessed: 76
Too big!

漂亮!即便是在猜测之前添加了空格,程序依然能判断出用户猜测了 76。多运行程序几次来检验不同类型输入的相应行为:猜一个正确的数字,猜一个过大的数字和猜一个过小的数字。

现在游戏已经大体上能玩了,不过用户只能猜一次。增加一个循环来改变它吧!

使用循环来允许多次猜测

loop关键字提供了一个无限循环。增加它后给了用户多次猜测的机会:

Filename: src/main.rs

extern crate rand;

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1, 101);

    println!("The secret number is: {}", secret_number);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin().read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = guess.trim().parse()
            .expect("Please type a number!");

        println!("You guessed: {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less    => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal   => println!("You win!"),
        }
    }
}

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

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

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
     Running `target/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit
thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidDigit }', src/libcore/result.rs:785
note: Run with `RUST_BACKTRACE=1` for a backtrace.
error: Process didn't exit successfully: `target/debug/guess` (exit code: 101)

输入quit就会退出程序,同时其他任何非数字输入也一样。然而,毫不夸张的说这是不理想的。我们想要当猜测正确的数字时游戏能自动退出。

猜测正确后退出

让我们增加一个break来在用户胜利时退出游戏:

Filename: src/main.rs

extern crate rand;

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1, 101);

    println!("The secret number is: {}", secret_number);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin().read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = guess.trim().parse()
            .expect("Please type a number!");

        println!("You guessed: {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less    => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal   => {
                println!("You win!");
                break;
            }
        }
    }
}

通过在You win!之后增加一行break,程序在用户猜对了神秘数字后会退出循环。退出循环也就意味着退出程序,因为循环是main的最后一部分。

处理无效输入

为了进一步改善游戏性,而不是在用户输入非数字时崩溃,需要让游戏忽略非数字从而用户可以继续猜测。可以通过修改guessString转化为u32那部分代码来实现:

let guess: u32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => continue,
};

expect调用切换到expect语句是如何从遇到错误就崩溃到真正处理错误的常用手段。记住parse返回一个Result类型,而Result是一个拥有OkErr两个成员的枚举。在这里使用match表达式,就像之前处理cmp方法返回的Ordering一样。

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

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

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

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
     Running `target/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!

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

Filename: src/main.rs
extern crate rand;

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1, 101);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin().read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less    => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal   => {
                println!("You win!");
                break;
            }
        }
    }
}

Listing 2-5: Complete code of the guessing game

总结一下,

此时此刻,你顺利完成了猜猜看游戏!恭喜!

这是一个通过动手实践的方式想你介绍许多 Rust 新知识的项目:letmatch、方法、关联函数,使用外部 crate,等等。接下来的几章,我们将会详细学习这些概念。第三章涉及到大部分编程语言都有的概念,比如变量、数据类型和函数,以及如何在 Rust 中使用他们。第四章探索所有权(ownership),这是一个 Rust 同其他语言都不相同的功能。第五章讨论结构体和方法语法,而第六章侧重解释枚举。

通用编程概念

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

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

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

关键字

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

变量和可变性

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

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

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

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

Filename: src/main.rs

fn main() {
    let x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}

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

$ cargo run
   Compiling variables v0.0.1 (file:///projects/variables)
error[E0384]: re-assignment of immutable variable `x`
 --> src/main.rs:4:5
  |
2 |     let x = 5;
  |         - first assignment to `x`
3 |     println!("The value of x is: {}", x);
4 |     x = 6;
  |     ^^^^^ re-assignment of immutable variable

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

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

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

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

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

Filename: src/main.rs

fn main() {
    let mut x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}

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

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
     Running `target/debug/variables`
The value of x is: 5
The value of x is: 6

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

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

变量和常量的区别

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

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

const MAX_POINTS: u32 = 100_000;

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

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

覆盖

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

Filename: src/main.rs

fn main() {
    let x = 5;

    let x = x + 1;

    let x = x * 2;

    println!("The value of x is: {}", x);
}

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

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
     Running `target/debug/variables`
The value of x is: 12

这与将变量声明为mut是有区别的。因为除非再次使用let关键字,不小心尝试对变量重新赋值会导致编译时错误。我们可以用这个值进行一些计算,不过计算完之后变量仍然是不变的。

另一个mut与覆盖的区别是当再次使用let关键字时,事实上创建了一个新变量,我们可以改变值的类型。例如,假设程序请求用户输入空格来提供在一些文本之间需要多少空间来分隔,不过我们真正需要的是将输入存储成数字(多少个空格):

let spaces = "   ";
let spaces = spaces.len();

这里允许第一个spaces变量是字符串类型,而第二个spaces变量,它是一个恰巧与第一个变量名字相同的崭新的变量,它是数字类型。因此覆盖使我们不必使用不同的名字,比如spaces_strspaces_num;相反,我们可以复用spaces这个更简单的名称。然而,如果尝试使用mut,如下所示:

let mut spaces = "   ";
spaces = spaces.len();

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

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

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

数据类型

ch03-02-data-types.md
commit d05b7c63ff50b3f9126bb5533e0ba5dd424b83d1

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

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

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

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

error[E0282]: unable to infer enough type information about `_`
 --> src/main.rs:2:5
  |
2 | let guess = "42".parse().unwrap();
  |     ^^^^^ cannot infer type for `_`
  |
  = note: type annotations or generic parameter binding required

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

标量类型

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

整型

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

Table 3-1: Integer Types in Rust

Length Signed Unsigned
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
arch isize usize

每一种变体都可以是有符号或无符号的并有一个显式的大小。有符号和无符号代表数字是否能够是正数或负数;换句话说,数字是否需要有一个符号(有符号数)或者永远只需要是正的这样就可以不用符号(无符号数)。

How Functions Work

Comments

Control Flow