由 Steve Klabnik 和 Carol Nichols,以及来自 Rust 社区贡献者撰写
中文版: https://rustwiki.org/zh-CN/book/
简介
底层代码中容易出现种种隐晦的 bug,在其他编程语言中,只能通过大量的测试和经验丰富的开发者细心的代码评审来捕获它们。在 Rust 中,编译器充当了守门员的角色,它拒绝编译存在这些难以捕获的 bug 的代码,这其中包括并发 bug。通过与编译器合作,团队将更多的时间聚焦在程序逻辑上,而不是追踪 bug。
Rust 为系统编程世界带来了现代化的开发工具:
- Cargo,内置的依赖管理器和构建工具,它能轻松增加、编译和管理依赖,并使其在 Rust 生态系统中保持一致。
- Rustfmt 确保开发者遵循一致的代码风格。
- Rust Language Server 为集成开发环境(IDE)提供了强大的代码补全和内联错误信息功能。
入门指南
rustup
rustup
是一个管理 Rust 版本和相关工具的命令行工具。
- 通过
rustup
安装 Rust 后,更新到最新版本
rustup update
- 卸载rust
要卸载 Rust 和 rustup
,在 shell 中运行以下卸载命令:
$ rustup self uninstall
- 查看版本
rustc –version
你应该看到最新发布的稳定版本的版本号、提交哈希值和提交日期,如下所示格式:
rustc 1.70.0 (90c541806 2023-05-31)
rustup doc
让浏览器打开本地文档。
手动创建项目目录管理项目
D:\rustProjects>mkdir hello_world
D:\rustProjects>cd hello_world
D:\rustProjects\hello_world>code main.rs
main.rs
fn main() {
println!("Hello, world!你好,世界!");
}
编译并运行文件:
D:\rustProjects\hello_world>rustc main.rs
D:\rustProjects\hello_world>main
Hello, world!你好,世界!
-
main
函数(也称为主函数)很特殊:它始终是每个可执行 Rust 程序中运行的第一个代码。第一行声明一个名为main
的函数,不带参数也没有返回值。 -
格式化代码:rustfmt main.rs
-
println!
调用 Rust 宏。如果改为调用函数,则应该将其输入为println
(不含!
)。我们将在第 19 章中更详细地讨论 Rust 宏。现在只需要知道,当看到一个!
,则意味着调用的是宏而不是普通的函数。你看到"Hello, world!"
字符串。我们将这个字符串作为参数传递给println!
,接着println!
将字符串打印到屏幕上。
使用 rustc
编译对简单的程序可以轻松胜任,但随着项目的增长,你将会想要管理项目中所有相关内容,并想让其他用户和项目能够容易共享你的代码。接下来,我们将引入 Cargo 工具,这将帮助你学会编写真实开发环境的 Rust 程序。
使用cargo管理项目
Cargo 是 Rust 的构建系统和包管理器。可以使用 Cargo 来管理他们的 Rust 项目,因为它可以为你处理很多任务,比如构建代码、下载依赖库,以及编译这些库。(我们把代码所需要的库叫做依赖(_dependency_))。
创建项目 hello_cargo
D:\rustProjects>cargo new hello_cargo
Created binary (application) `hello_cargo` package
D:\rustProjects>cd hello_cargo
D:\rustProjects\hello_cargo>dir
2023/07/07 14:42 <DIR> .
2023/07/07 14:42 <DIR> ..
2023/07/07 14:42 8 .gitignore
2023/07/07 14:42 180 Cargo.toml
2023/07/07 14:42 <DIR> src
Cargo.toml
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-
第一行,[package],是一个表块(section)标题,表明下面的语句用来配置一个包(package)。随着我们在这个文件增加更多的信息,还将增加其他表块。接下来的三行设置了 Cargo 编译程序所需的配置:项目的名称、版本,以及使用的 Rust 大版本号(edition,区别于 version)。附录 E 会介绍 edition(译注:Rust 的核心版本,即 2015、2018、2021 版等) 的值。
-
最后一行,
[dependencies]
是一个表块的开头,你可以在其中列出你的项目所依赖的任何包。在 Rust 中,代码包被称为 _crate_。这个项目并不需要其他的 crate,不过在第 2 章的第一个项目会用到依赖,那时会用得上这个表块。
hello_word项目与 Cargo 生成项目的区别是,Cargo 将代码放在 _src_ 目录,同时项目根目录包含一个 _Cargo.toml_ 配置文件。
Cargo 期望源文件位于 _src_ 目录中。项目根目录只存放说明文件(README)、许可协议(license)信息、配置文件和其他跟代码无关的文件。使用 Cargo 可帮助你保持项目干净整洁。这里为一切事物所准备,一切都位于正确的位置。
对于没有使用 Cargo 开始的项目,比如我们之前创建的 Hello,world! 项目,你可以将其转化为一个 Cargo 项目。将代码放入 _src_ 目录,并创建一个合适的 _Cargo.toml_ 文件。
构建并运行项目:
D:\rustProjects\hello_cargo>cargo build
Compiling hello_cargo v0.1.0 (D:\rustProjects\hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 1.95s
D:\rustProjects\hello_cargo>target\debug\hello_cargo
Hello, world!
-
cargo build 命令会在 _target/debug/hello_cargo_ 下创建一个可执行文件(在 Windows 上是 _target\debug\hello_cargo.exe_)
-
我们也可以使用
cargo run
命令,一次性完成代码编译和运行的操作 -
Cargo 还提供了一个名为
cargo check
的命令。该命令快速检查代码确保其可以编译,但并不产生可执行文件。通常,cargo check
要比cargo build
快得多,因为它省略了生成可执行文件的步骤。如果你在编写代码时不断检查你的代码,那么使用cargo check
命令可以加快这个过程!为此很多 Rustacean 编写代码时会定期运行cargo check
以确保它们可以编译。当准备好使用可执行文件时才运行cargo build
。 -
当项目最终准备好发布时,可以使用
cargo build --release
来优化编译项目。这会在 _target/release_ 而不是 _target/debug_ 下生成可执行文件。这些优化可以让 Rust 代码运行的更快,不过启用这些优化也需要消耗更长的编译时间。这也就是为什么会有两种不同的配置:一种是为了开发,你需要经常快速重新构建;另一种是为用户构建最终程序,它们不会经常重新构建,并且希望程序运行得越快越好。如果你要对代码运行时间进行基准测试,请确保运行cargo build --release
并使用 _target/release_ 下的可执行文件进行测试。
猜数字游戏
实现一个经典的新手编程问题:猜数字游戏。这是它的工作原理:程序会随机生成一个 1 到 100 之间的整数。接着它会提示玩家猜一个数并输入,然后指出猜测是大了还是小了。如果猜对了,它会打印祝贺信息并退出。
基本输入输出
处理一次猜测:
// 为了获取用户输入并打印结果作为输出,我们需要引入 io 输入/输出库到当前作用域。io 库来自于标准库,标准库也被称为 std
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
// 创建一个储存用户输入的变量
// 使用 let 语句来创建变量
// 在 Rust 中,变量默认是不可变的
// 想要让变量可变,可以在变量名前添加 mut(mutability,可变性)
// 等号(=)告诉 Rust 现在想将某个值绑定在变量上
// 等号的右边是 guess 所绑定的值,它是 String::new 的结果,这个函数会返回一个 String 的新实例
// ::new 那一行的 :: 语法表明 new 是 String 类型的一个关联函数。关联函数(associated function)是实现一种特定类型的函数,在这个例子中类型是 String。这个 new 函数创建了一个新的空字符串。你会在很多类型上找到一个 new 函数,因为它是创建类型实例的惯用函数名。
// 总的来说,let mut guess = String::new(); 这一行创建了一个可变变量,目前它绑定到一个新的 String 空实例上。
let mut guess = String::new();
// 从 io 模块调用 stdin 函数,这将允许我们处理用户输入
// 如果程序的开头没有使用 use std::io 引入 io 库,我们仍可以通过 std::io::stdin 来调用函数。stdin 函数返回一个 std::io::Stdin 的实例,这是一个类型,代表终端标准输入的句柄。
// .read_line(&mut guess) 这一行调用了 read_line 方法,来从标准输入句柄中获取用户输入。
// 我们还将 &mut guess 作为参数传递给 read_line(),以告诉它在哪个字符串存储用户输入。
// read_line 的全部工作是,将用户在标准输入中输入的任何内容都追加到一个字符串中(而不会覆盖其内容),所以它需要字符串作为参数。这个字符串应是可变的,以便该方法可以更改其内容。
// & 表示这个参数是一个引用(reference),这为你提供了一种方法,让代码的多个部分可以访问同一处数据,而无需在内存中多次拷贝。引用是一个复杂的特性,Rust 的一个主要优势就是安全而简单的使用引用。
// 完成当前程序并不需要了解太多细节。现在,我们只需知道就像变量一样,引用默认是不可变的。因此,需要写成 &mut guess 来使其可变,而不是 &guess。
io::stdin()
.read_line(&mut guess)
// 使用 Result 类型来处理潜在的错误
// 一行过长的代码很难阅读,所以最好拆开来写。当你使用 .method_name() 语法调用方法时,用换行和空格来拆分长代码行通常是明智的。
// read_line 将用户输入存储到我们传递给它的字符串中,但它也返回一个值——在这个例子中是 io::Result。
// Rust 标准库中有很多名为 Result 的类型:一个通用的 Result 以及在子模块中的特化版本,比如 io::Result。Result 类型是 枚举(enumerations),通常也写作 enum。枚举类型持有固定集合的值,这些值被称为枚举的成员(variant)。枚举往往与条件表达式 match 一起使用,match 是一种条件语句,在其被执行时,可以方便地匹配不同枚举值来执行不同的代码。第 6 章将更详细地介绍枚举类型。这些 Result 类型的目的是编码错误处理信息。
// Result 的成员是 Ok 和 Err,Ok 成员表示操作成功,且 Ok 内部包含成功生成的值。Err 成员则意味着操作失败,并且包含失败的前因后果。
// Result 类型的值,就像任何类型的值一样,都有为其定义的方法。io::Result 的实例拥有 expect 方法。如果 io::Result 实例的值是 Err,expect 会导致程序崩溃,并显示传递给 expect 的参数。如果 read_line 方法返回 Err,则可能是底层操作系统引起的错误结果。如果 io::Result 实例的值是 Ok,expect 会获取 Ok 中的值并原样返回,以便你可以使用它。在本例中,这个值是用户输入的字节数。
// 如果不调用 expect,程序也能编译,但会出现警告提示.Rust 警告我们没有使用 read_line 的返回值 Result,这表明程序没有处理一个可能发生的错误。
// 消除警告的正确做法是实际编写错误处理代码,但在这个例子中,我们只希望程序在出现问题时立即崩溃,因此我们可以直接使用 expect。
.expect("Failed to read line");
//这行代码现在打印了存储用户输入的字符串。里面的 {} 是预留在特定位置的占位符:把 {} 想象成小蟹钳,可以夹住合适的值。使用 {} 也可以打印多个值:第一对 {} 使用格式化字符串之后的第一个值,第二对则使用第二个值,依此类推。
println!("You guessed: {}", guess);
}
引入代码包crate
Rust 团队提供了一个包含随机数功能的 rand crate。
记住,crate 是一个 Rust 代码包。我们正在构建的项目是一个 二进制 crate,它生成一个可执行文件。 rand crate 是一个 库 crate,库 crate 可以包含任意能被其他程序使用的代码,但是不能独自执行。
Cargo 对外部 crate 的运用是其真正的亮点所在。在我们使用 rand
编写代码之前,需要修改 _Cargo.toml_ 文件,引入一个 rand
依赖。现在打开这个文件并将下面这一行添加到 [dependencies]
表块标题之下。请确保按照我们这里的方式指定 rand
及其这里给出的版本号,否则本教程中的示例代码可能无法工作。
默认设置下,Cargo
就从 crates.io 上下载依赖包,只需要一个包名和版本号即可,我们可以从 https://crates.io/ 查询最新版本号并写入 Cargo.toml
[dependencies]
rand = "0.8.3"
argo 理解语义化版本(Semantic Versioning,有时也称为 _SemVer_),这是一种定义版本号的标准。0.8.3
实际上是 ^0.8.3
的简写,它表示任何至少包含 0.8.3
但低于 0.9.0
的版本。 Cargo 认为这些版本具有与 0.8.3
版本兼容的公有 API, 并且此规范可确保你将获得最新的补丁版本,它仍然可以与本章中的代码正常编译。0.9.0
或更高版本则不再确保 API 和以下示例所使用的 API 相同。
现在可以重新运行: cargo run
-
当我们引入了一个外部依赖后,Cargo 将从 registry 上获取所有依赖所需的最新版本,这是一份来自 Crates.io 的数据拷贝。Crates.io 是 Rust 生态环境中开发者们向他人贡献 Rust 开源项目的地方。
-
在更新完 registry 后,Cargo 检查 [dependencies] 表块并下载缺失的 crate 。本例中,虽然只声明了 rand 一个依赖,然而 Cargo 还是额外获取了 rand 所需的其他 crate,rand 依赖它们来正常工作。下载完成后,Rust 编译依赖,然后使用这些依赖编译项目。
-
Cargo 有一个机制来确保任何人在任何时候重新构建代码,都会产生相同的结果:Cargo 只会使用你指定的依赖版本,除非你又手动指定了别的。例如,如果下周
rand
crate 的0.8.4
版本出来了,它修复了一个重要的 bug,同时也含有一个会破坏代码运行的缺陷。为了处理这个问题,Rust 在你第一次运行cargo build
时建立了 _Cargo.lock_ 文件,我们现在可以在 _guessing_game_ 目录找到它。- 当第一次构建项目时,Cargo 计算出所有符合要求的依赖版本并写入 _Cargo.lock_ 文件。 当将来构建项目时,Cargo 会发现 _Cargo.lock_ 已存在并使用其中指定的版本,而不是再次计算所有的版本。这使得你拥有了一个自动化的可重现的构建。换句话说,项目会持续使用
0.8.3
直到你显式地升级,多亏有了 _Cargo.lock_ 文件。 - 更新 crate 到一个新版本: 当你确实需要升级 crate 时,Cargo 提供了这样一个命令 cargo update,它会忽略 Cargo.lock 文件,并计算出所有符合 Cargo.toml 声明的最新版本。Cargo 接下来会把这些版本写入 Cargo.lock 文件。不过,Cargo 默认只会寻找大于或等于 0.8.3 而小于 0.9.0 的版本。 如果想要 rand 使用 0.9.0 版本或任何 0.9.x 系列的版本,则必须像这样更新 Cargo.toml 文件:
- 当第一次构建项目时,Cargo 计算出所有符合要求的依赖版本并写入 _Cargo.lock_ 文件。 当将来构建项目时,Cargo 会发现 _Cargo.lock_ 已存在并使用其中指定的版本,而不是再次计算所有的版本。这使得你拥有了一个自动化的可重现的构建。换句话说,项目会持续使用
[dependencies]
rand = "0.9.0"
使用 cargo fmt 格式化代码。
使用引用的rand
use std::io;
// Rng 是一个 trait,它定义了随机数生成器应实现的方法,想使用这些方法的话,此 trait 必须在作用域中。第 10 章会详细介绍 trait。
use rand::Rng;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
// 调用 rand::thread_rng 函数来为我们提供将要使用的特定随机数生成器:它位于当前执行线程的本地环境中,并从操作系统获取 seed。
// 然后调用随机数生成器的 gen_range 方法。该方法由我们刚才使用 use rand::Rng 语句引入的 Rng trait 定义。gen_range 方法获得一个区间表达式(range expression)作为参数,并在区间内生成一个随机数。我们在这里使用的区间表达式采用的格式为 start..end。它包括起始端,但排除终止端。所以我们需要指定 1..101 生成一个 1 到 100 之间的数字。
// 或者我们可以传入区间 1..=100,这和前面的表达等价。
let secret_number = rand::thread_rng().gen_range(1..101);
// 打印出了秘密数字。这在开发程序时很有用,因为可以测试它,不过在最终版本中会删掉它。
println!("The secret number is: {}", secret_number);
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
比较数字
use rand::Rng;
use std::io;
// 从标准库引入了一个叫做 std::cmp::Ordering 的类型到作用域中。Ordering 也是一个枚举,不过它的成员是 Less、Greater 和 Equal。这是比较两个值时可能出现的三种结果。
use std::cmp::Ordering;
fn main() {
// Rust 默认使用 i32,这是 secret_number 的类型
let secret_number = rand::thread_rng().gen_range(1..101);
println!("The secret number is: {}", secret_number);
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");
// Rust 允许用一个新值来遮蔽 (shadow) guess 之前的值。这允许我们复用 guess 变量的名字,而不是被迫创建两个不同变量,诸如 guess_str 和 guess 之类。我们会在第 3 章介绍变量遮蔽的更多细节,目前暂时只需要知道这个功能通常用作转换值类型。
// 我们将这个新变量绑定到 guess.trim().parse() 表达式上。表达式中的 guess 是指原始的 guess 变量,其中包含作为字符串的输入。
// String 实例的 trim 方法会去除字符串开头和结尾的空白字符,我们必须执行此方法才能将字符串与 u32 比较,因为 u32 只能包含数值型数据。用户必须输入 enter 键才能让 read_line 返回,并输入他们的猜想,这会在字符串中增加一个换行符。例如,用户输入 5 并按下 enter,guess 看起来像这样:5\n,\n 代表 “换行”(在 Windows 中,按 enter 键会得到一个回车和一个换行符 \r\n)。trim 方法会消除 \n 或 \r\n,只留下 5。
// 字符串的 parse 方法 将字符串解析成数字。因为这个方法可以解析多种数字类型,因此需要告诉 Rust 具体的数字类型,这里通过 let guess: u32 指定。guess 后面的冒号(:)告诉 Rust 我们指定了变量的类型。Rust 有一些内建的数字类型;u32 是一个无符号的 32 位整型。对于不大的正整数来说,它是不错的类型,第 3 章还会讲到其他数字类型。另外,程序中的 u32 标注以及与 secret_number 的比较,意味着 Rust 会推断出 secret_number 也是 u32 类型。现在可以使用相同类型比较两个值了!
// 由于 parse 方法只能用于可以逻辑转换为数字的字符,所以调用它很容易产生错误。因此,parse 方法返回一个 Result 类型。像前面 “使用 Result 类型来处理潜在的错误” 部分讨论的 read_line 方法那样,再次按部就班地用 expect 方法处理即可。如果 parse 不能从字符串生成一个数字,返回一个 Result 的 Err 成员时,expect 会使游戏崩溃并打印附带的信息。如果 parse 成功地将字符串转换为一个数字,它会返回 Result 的 Ok 成员,然后 expect 会返回 Ok 值中的数字。
let guess: u32 = guess.trim().parse().expect("Please type a number!");
// 以下五行新代码使用了 Ordering 类型,cmp 方法用来比较两个值并可以在任何可比较的值上调用。它获取一个被比较值的引用:这里是把 guess 与 secret_number 做比较。 然后它会返回一个刚才通过 use 引入作用域的 Ordering 枚举的成员。
// 使用一个 match 表达式,根据对 guess 和 secret_number 调用 cmp 返回的 Ordering 成员来决定接下来做什么。
// 一个 match 表达式由分支(arm) 构成。一个分支包含一个用于匹配的模式(pattern),给到 match 的值与分支模式相匹配时,应该执行对应分支的代码。
// Rust 获取提供给 match 的值并逐个检查每个分支的模式。模式和 match 结构是 Rust 中强大的功能,它体现了代码可能遇到的多种情形,并帮助你确保没有遗漏处理。这些功能将分别在第 6 章和第 18 章详细介绍。
// 使用 match 表达式的例子。假设用户猜了 50,这时随机生成的秘密数字是 38。比较 50 与 38 时,因为 50 比 38 要大,cmp 方法会返回 Ordering::Greater。Ordering::Greater 是 match 表达式得到的值。它检查第一个分支的模式,Ordering::Less 与 Ordering::Greater并不匹配,所以它忽略了这个分支的代码并来到下一个分支。下一个分支的模式是 Ordering::Greater,正确匹配 Ordering::Greater!这个分支关联的代码被执行,在屏幕打印出 Too big!。match 表达式就此终止,因为该场景下没有检查最后一个分支的必要。
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
println!("You guessed: {}", guess);
}
运行:
D:\rustProjects\guessing_game>cargo run
Compiling guessing_game v0.1.0 (D:\rustProjects\guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.61s
Running `target\debug\guessing_game.exe`
The secret number is: 25
Guess the number!
Please input your guess.
u45
thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidDigit }', src\main.rs:26:43
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\guessing_game.exe` (exit code: 101)
D:\rustProjects\guessing_game>cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running `target\debug\guessing_game.exe`
The secret number is: 68
Guess the number!
Please input your guess.
45
Too small!
You guessed: 45
加入循环和退出机制
Rust可以重复定义同名变量,即使是同一作用域,后定义的变量会覆盖掉前面定义的变量,而且后定义的变量可以与前面的变量不同类型。
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
let secret_number = rand::thread_rng().gen_range(1..101);
println!("The secret number is: {}", secret_number);
println!("Guess the number!");
// loop 关键字创建了一个无限循环。
loop {
// 每次循环创建新的字符串变量guess,否则变量guess会不停追加包含换行的字符串
let mut guess = String::new();
println!("Please input your guess.");
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
// let guess: u32 = guess.trim().parse().expect("Please type a number!");
let guess: u32 = match guess.trim().parse() {
// 将 expect 调用换成 match 语句,从而实现遇到错误就崩溃转换成处理错误。
// parse 返回一个 Result 类型,而 Result 是一个拥有 Ok 或 Err 成员的枚举。这里使用的 match 表达式,和之前处理 cmp 方法返回 Ordering 时用的一样。
// 如果 parse 能够成功的将字符串转换为一个数字,它会返回一个包含结果数字的 Ok。这个 Ok 值与 match 第一个分支的模式相匹配,该分支对应的动作返回 Ok 值中的数字 num,最后如愿变成新创建的 guess 变量。
// 如果 parse 不能将字符串转换为一个数字,它会返回一个包含更多错误信息的 Err。Err 值不能匹配第一个 match 分支的 Ok(num) 模式,但是会匹配第二个分支的 Err(_) 模式:_ 是一个通配符值,本例中用来匹配所有 Err 值,不管其中有何种信息。所以程序会执行第二个分支的动作,continue 意味着进入 loop 的下一次循环,请求另一个猜测。这样程序就有效的忽略了 parse 可能遇到的所有错误!
Ok(num) => num,
Err(_) => continue,
};
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
// 退出循环
break;
}
}
}
}
运行:
D:\rustProjects\guessing_game>cargo run
Compiling guessing_game v0.1.0 (D:\rustProjects\guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.58s
Running `target\debug\guessing_game.exe`
The secret number is: 82
Guess the number!
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
90
You guessed: 90
Too big!
Please input your guess.
fds
You guessed: fds
Please input your guess.
82
You guessed: 82
You win!