commit cf2b9a266ccf560df7c1228b79c711cb37c29072 Author: rust-lang.xfoss.com Date: Mon Mar 27 14:33:48 2023 +0800 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..05ff2e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +book +target diff --git a/README.md b/README.md new file mode 100644 index 0000000..8a0a456 --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +# Rust 编程语言 + + +原著:[The Rust Programming Language](https://doc.rust-lang.org/book/) + +*原作者:Steve Klabnik 与 Carol Nichols, 及 Rust 社区* + + +此版本的教材,假定安装了 Rust `1.67.1` (发布于 2023-02-09)或更新版本。请参阅 [第 1 章的 “安装” 小节](docs/Ch01_Getting_Started.md#Installation) 进行安装,或对已安装的 Rust 进行升级。 + +```console +$ rustc --version +rustc 1.68.0 (2c8cc3432 2023-03-06) +``` + + +## 在线阅读 + +- [rust-lang.xfoss.com](https://rust-lang.xfoss.com) + +- 在 Gitbook 上可阅读此教程:[Rust 编程语言](https://rust.xfoss.com) + +## 简介 + +欢迎来到 *Rust 编程语言*,一本 Rust 的介绍性书籍。Rust 编程语言帮助更快地编写出更可靠软件。在程序语言设计中,上层人机交互与底层控制,通常是不可调和的;Rust 挑战了这对矛盾。经由强力的技术能力与了不起的开发者体验,Rust 带来了对底层细节(譬如内存的使用)控制的同时,免去了传统上底层控制带来的一大堆麻烦。 + +## Rust 适用于哪些人群 + +对于相当多的人群,Rust 因为各种原因,都是理想选择。下面就来看看那些最重要群体中的一些人的情况。 + +### 开发者团队 + +对于有着不同水平系统编程知识的开发者团队的协同来讲,Rust 正被证明是一种生产力工具。底层代码倾向于出现各种细微错误,这样的细微错误,对于其他编程语言,则只能经由广泛测试,和经验老道的开发者细致代码评审才能捕获到。而在 Rust 中,编译器通过拒绝编译这些难以捉摸的错误,包括并发错误,而扮演着看门人角色。通过与编译器一道工作,团队就可以将他们的时间,集中用在程序逻辑,而不是找寻错误上。 + +Rust 还带给了系统编程世界,一些现代开发者工具: + +- `Cargo`,Rust 所包含的依赖管理器与构建工具,让整个 Rust 生态中添加依赖、编译与管理依赖,变得愉快并具一致性(`Cargo`, the included dependency manager and build tool, makes adding, compiling, and managing dependecies painless and consistant across the Rust ecosystem); +- `Rustfmt` 确保了不同开发者之间有着一致的编码风格; +- Rust 语言服务器驱动了用于代码补全与行内错误消息的集成开发环境。 + +通过使用这些开发者工具,及其他一些 Rust 生态中的工具,开发者就可以在编写系统级代码时,颇具生产力了。 + +### 学生 + +Rust 是为学生及那些对掌握系统概念感兴趣的人所准备的。运用 Rust,许多人都掌握了像是操作系统开发这样的知识点。Rust 社区非常欢迎并乐于回答学生们提出的问题。通过像是本书这样的努力,Rust 团队本身是要让更多人,尤其是那些刚开始编程的人们,可获取到系统概念。 + + +### 商业公司 + +已有上千家规模或大或小的商业公司,在生产中,为着不同任务使用着 Rust。这些任务包括了命令行工具、web 服务、运维工具、嵌入式装置、音视频分析与转码、加密货币、生物信息学、搜索引擎、物联网应用、机器学习,甚至Firefox web浏览器的主要部分等等。 + +### 开放源代码开发者 + +Rust 是为着那些想要构建 Rust 编程语言本身、Rust 社区、Rust 开发者工具和库而准备的。我们希望你为 Rust 语言做出贡献。 + +### 看重运行速度与稳定性的人们 + +Rust 是为那些渴求某门语言所提供速度与稳定性的人们准备的。这里说的运行速度,指的是使用 Rust 可创建出程序的运行速度,以及 Rust 所能达到的编写这些程序速度。Rust 编译器的检查,确保了功能补充与重构的稳定性。这稳定性是与那些不具备这些检查语言中的脆弱老旧代码相比得出的,开发者通常害怕去修改那些脆弱老旧代码。通过争取实现零代价的抽象,就有了那些在手动编写时,就立即编译到底层代码的上层特性,Rust 致力于实现在构造安全代码的同时,还取得了快速的代码编写与程序运行。 + +Rust 语言也希望带给众多其他用户以支持;这里提到的只是一些最大的相关群体。总体来讲,Rust 最伟大的抱负,是要消除程序员们在过去数十年来,业已被迫接受的在安全性与生产力、开发和运行速度及人机交互上的妥协。请给 Rust 一个机会,然后看看 Rust 的选择是否适合于你。 + +## 本书读者群体 + +本书假定你曾编写过其他某种编程语言的代码,至于何种编程语言并不重要。本书作者已尽力让其中的教学材料适合到有着宽泛编程背景的读者。这里不会花大量时间来解释编程为何物,以及该怎么来看待编程。若对编程一窍不通,那么最好找一本编程入门的书先看看。 + +## 怎样使用本书 + +大体上,本书假定是要以从前往后的顺序进行阅读。后续章节是建立在较早章节的概念之上,而较早的章节不会深入某个话题的细节;在后续章节通常会回顾到那个话题。 + +本书有两种章节:概念性章节与项目性章节。在概念章节,将对 Rust 某个方面的有所了解。而在项目性章节,就会构建出一些在一起的小程序,这些小程序运用了概念性章节中学到的东西。第 2、12 和 20 章,就是项目性章节;而剩下的,全都是概念性章节。 + +第 1 章讲了怎样安装 Rust、怎样编写出 “Hello, world!” 程序,还有怎样使用 Rust 的包管理器及构建工具 `Cargo`。第 2 章是 Rust 语言的一个实操介绍。这里涵盖了上层的一些概念,而在后续章节则会提供到进一步的细节。若要立即动手编写代码,那么第 2 章就可以开始了。一开始你或许想要跳过第 3 章,这一章涵盖了与那些其他编程语言类似的一些 Rust 特性,而要直接前往到第 4 章,去了解 Rust 的所有权系统。不过若要细致了解 Rust,就应详细掌握 Rust 的每个细节设计之后,在移步到下一章节,或许也会跳过第 2 章直接到第 3 章,然后在想要将学到的细节知识应用到项目时,再回到第 2 章。 + +第 5 章讨论了结构体和方法,同时第 6 章涵盖了枚举、`match` 表达式,和 `if let` 控制流结构。在构造 Rust 中的定制类型时,就会用到这些结构体和枚举。 + +第 7 章将了解到 Rust 的模组系统,以及代码组织的隐私规则,还有 Rust 的公共应用编程接口(Application Programming Interface, API)。第 8 章讨论了一些常用的、由标准库提供的集合数据结构,诸如矢量、字符串及哈希图。第 9 章探索了 Rust 的错误处理思想与技巧。 + +第 10 章涉及了范型、特质(traits) 与生命周期,他们赋予了定义出应用多种类型代码的能力。第 11 章全都是关于测试的内容,即便有着 Rust 的安全性保证,对于确保程序逻辑正确,测试仍是不可缺少的。第 12 章将构建一个我们自己的、用于在文件中搜索文本的 `grep` 命令行工具功能子集的版本。到这里,就会用到先前章节中所讨论的众多概念了。 + +第 13 章对闭包(closures)和迭代进行了探索:闭包和迭代属于 Rust 来自函数式编程语言的特性。第 14 章,将更深入地对 `Cargo` 加以检视,并就如何与他人分享库的最佳实践进行探讨。第 15 章讨论标准库提供的一些灵巧指针,还有实现不同功能的 Rust 特质(traits)。 + +第 16 章,将遍数并发编程的各种不同模型,并探讨 Rust 如何对大胆进行多线程编程的帮助。第 17 章将或许你所熟知面向对象编程的那些原则,与 Rust 下编程的习惯加以对比。 + +第 18 章是模式与模式匹配的一个参考,这在 Rust 程序中,属于是概念表达的强大方式。第 19 章包含了一个诸多感兴趣的话题大杂烩,包括不安全的 Rust、宏,以及更多有关生命周期、特质(traits)、类型、函数与闭包等等的细节。 + +第 20 章,将完成一个其中实现了底层线程化的 web 服务器! + +最后,还有一些包含了这门语言的有用信息的附录,这些附录则更多的像是参考的形式。附录 A 涵盖了 Rust 的关键字,附录 B 涵盖了 Rust 的运算符和符号,附录 C 涵盖了由标准库所提供的那些派生特质(derivable traits),附录 D 涵盖了一些有用的开发工具,还有附录 E 对 Rust 版本进行了解释。 + +阅读本书并无定法:你要跳着去读,也是可以的!在遇到疑惑时,或许就不得不跳回去看看了。你只要怎么有效就行了。 + diff --git a/add/Cargo.toml b/add/Cargo.toml new file mode 100644 index 0000000..1448801 --- /dev/null +++ b/add/Cargo.toml @@ -0,0 +1,6 @@ +[workspace] + +members = [ + "adder", + "add_one", +] diff --git a/add/add_one/Cargo.toml b/add/add_one/Cargo.toml new file mode 100644 index 0000000..66ee46a --- /dev/null +++ b/add/add_one/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "add_one" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rand = "^0.8.4" diff --git a/add/add_one/src/lib.rs b/add/add_one/src/lib.rs new file mode 100644 index 0000000..8eff881 --- /dev/null +++ b/add/add_one/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add_one(x: i32) -> i32 { + x + 1 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add_one(2); + assert_eq!(result, 3); + } +} diff --git a/add/adder/Cargo.toml b/add/adder/Cargo.toml new file mode 100644 index 0000000..5ff08a7 --- /dev/null +++ b/add/adder/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "adder" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +add_one = { path = "../add_one" } +rand = "0.8.4" diff --git a/add/adder/src/main.rs b/add/adder/src/main.rs new file mode 100644 index 0000000..b4153a8 --- /dev/null +++ b/add/adder/src/main.rs @@ -0,0 +1,6 @@ +use add_one::add_one; + +fn main() { + let num = 10; + println!("你好,世界!\n\t{num} 加 1 为 {}!", add_one(num)); +} diff --git a/adder/Cargo.toml b/adder/Cargo.toml new file mode 100644 index 0000000..b7d36d4 --- /dev/null +++ b/adder/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "adder" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/adder/src/lib.rs b/adder/src/lib.rs new file mode 100644 index 0000000..c1d0c22 --- /dev/null +++ b/adder/src/lib.rs @@ -0,0 +1,17 @@ +pub fn add_two(a: i32) -> i32 { + internal_add(a, 2) +} + +fn internal_add(a: i32, b: i32) -> i32 { + a + b +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn internal() { + assert_eq! (4, internal_add(2, 2)); + } +} diff --git a/adder/tests/common/mod.rs b/adder/tests/common/mod.rs new file mode 100644 index 0000000..503f4f8 --- /dev/null +++ b/adder/tests/common/mod.rs @@ -0,0 +1,3 @@ +pub fn setup() { + println! ("特定于库测试的一些设置代码,将放在这里"); +} diff --git a/adder/tests/integration_test.rs b/adder/tests/integration_test.rs new file mode 100644 index 0000000..c56ed2c --- /dev/null +++ b/adder/tests/integration_test.rs @@ -0,0 +1,9 @@ +use adder; + +mod common; + +#[test] +fn it_adds_two() { + common::setup(); + assert_eq! (6, adder::add_two(4)); +} diff --git a/aggregator/Cargo.toml b/aggregator/Cargo.toml new file mode 100644 index 0000000..27d4889 --- /dev/null +++ b/aggregator/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "aggregator" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/aggregator/src/bin/test_case.rs b/aggregator/src/bin/test_case.rs new file mode 100644 index 0000000..3b69746 --- /dev/null +++ b/aggregator/src/bin/test_case.rs @@ -0,0 +1,3 @@ +fn main() { + println! ("媒体聚合器"); +} diff --git a/aggregator/src/lib.rs b/aggregator/src/lib.rs new file mode 100644 index 0000000..16246ae --- /dev/null +++ b/aggregator/src/lib.rs @@ -0,0 +1,56 @@ +pub trait Summary { + fn summarize_author(&self) -> String; + + fn summarize(&self) -> String { + format! ("(了解更多来自 {} ......)", self.summarize_author()) + } +} + +pub struct NewsArticle { + pub headline: String, + pub location: String, + pub author: String, + pub content: String, +} + +impl Summary for NewsArticle { + fn summarize_author(&self) -> String { + format! ("{}", self.author) + } +} + +pub struct Tweet { + pub username: String, + pub content: String, + pub reply: bool, + pub retweet: bool, +} + +impl Summary for Tweet { + fn summarize_author(&self) -> String { + format! ("@{}", self.username) + } +} + +use std::fmt::Display; + +pub struct Pair { + pub x: T, + pub y: T, +} + +impl Pair { + pub fn new(x: T, y: T) -> Self { + Self { x, y } + } +} + +impl Pair { + pub fn cmp_display(&self) { + if self.x >= self.y { + println! ("极大数为 x = {}", self.x); + } else { + println! ("极大数为 y = {}", self.y); + } + } +} diff --git a/aggregator/src/main.rs b/aggregator/src/main.rs new file mode 100644 index 0000000..1828013 --- /dev/null +++ b/aggregator/src/main.rs @@ -0,0 +1,54 @@ +use aggregator::{Summary, Tweet, NewsArticle, Pair}; + +pub fn notify(item: &T) { + println! ("突发新闻!{}", item.summarize()); +} + +fn return_summarizable() -> impl Summary { + Tweet { + username: String::from("horse_ebooks"), + content: String::from( + "当然,如同你或许已经知道的一样,朋友们" + ), + reply: false, + retweet: false, + } +} + +fn main() { + let tweet = Tweet { + username: String::from("horse_ebooks"), + content: String::from( + "当然,跟大家已经清楚的一样了,朋友们", + ), + reply: false, + retweet: false, + }; + + println!("1 条新推文: {}", tweet.summarize()); + notify(&tweet); + + + let article = NewsArticle { + headline: String::from("企鹅队赢得斯坦利杯锦标赛!"), + location: String::from("美国,宾夕法尼亚州,匹兹堡"), + author: String::from("Iceburgh"), + content: String::from( + "匹兹堡企鹅队再度成为美国曲棍球联盟 \ + NHL 中的最佳球队。" + ), + }; + + println! ("有新文章可读!{}", article.summarize()); + notify(&article); + + println! ("1 条旧推文: {}", return_summarizable().summarize()); + + let pair = Pair::new(5, 10); + pair.cmp_display(); + + let pair = Pair::new("这是一个测试", "This is a test."); + pair.cmp_display(); + + println! ("{}", 3.to_string()); +} diff --git a/art/Cargo.toml b/art/Cargo.toml new file mode 100644 index 0000000..4742a18 --- /dev/null +++ b/art/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "art" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/art/src/kinds.rs b/art/src/kinds.rs new file mode 100644 index 0000000..d5a9a30 --- /dev/null +++ b/art/src/kinds.rs @@ -0,0 +1,15 @@ +/// RYB 颜色模型下的主要颜色。 +#[derive(Debug)] +pub enum PrimaryColor { + Red, + Yellow, + Blue, +} + +/// RYB 颜色模型下的次要颜色。 +#[derive(Debug)] +pub enum SecondaryColor { + Orange, + Green, + Purple, +} diff --git a/art/src/lib.rs b/art/src/lib.rs new file mode 100644 index 0000000..1c2ee79 --- /dev/null +++ b/art/src/lib.rs @@ -0,0 +1,10 @@ +//! # art - 美术 +//! +//! 建模诸多美术概念的一个库。 + +pub mod kinds; +pub mod utils; + +pub use self::kinds::PrimaryColor; +pub use self::kinds::SecondaryColor; +pub use self::utils::mix; diff --git a/art/src/utils.rs b/art/src/utils.rs new file mode 100644 index 0000000..8de81af --- /dev/null +++ b/art/src/utils.rs @@ -0,0 +1,9 @@ +use crate::kinds::*; + +/// 结合两种等量的主要颜色,创建出 +/// 某种次要颜色。 +pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor { + // --跳过代码-- + println! ("c1: {:?}, c2: {:?}", c1, c2); + SecondaryColor::Purple +} diff --git a/assert_demo/Cargo.toml b/assert_demo/Cargo.toml new file mode 100644 index 0000000..3e4fdc2 --- /dev/null +++ b/assert_demo/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "assert_demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/assert_demo/src/lib.rs b/assert_demo/src/lib.rs new file mode 100644 index 0000000..4a2898b --- /dev/null +++ b/assert_demo/src/lib.rs @@ -0,0 +1,43 @@ +pub fn add_two(a: i32) -> i32 { + a + 2 +} + +pub fn nth_fibonacci(n: u64) -> u64 { + + if n == 0 || n == 1 { + return n; + } else { + return nth_fibonacci(n - 1) + nth_fibonacci(n - 2); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn add_two_and_two() { + assert_eq! (4, add_two(2)); + } + + #[test] + fn add_three_and_two() { + assert_eq! (5, add_two(3)); + } + + #[test] + fn one_hundred() { + assert_eq! (102, add_two(100)); + } + + #[test] + fn it_works() { + assert_eq! (2 + 2, 4); + } + + #[test] + #[ignore] + fn expensive_test() { + assert_ne! (100, nth_fibonacci(50)); + } +} diff --git a/associated_type/Cargo.lock b/associated_type/Cargo.lock new file mode 100644 index 0000000..8b25c34 --- /dev/null +++ b/associated_type/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "associated_type" +version = "0.1.0" diff --git a/associated_type/Cargo.toml b/associated_type/Cargo.toml new file mode 100644 index 0000000..4e9eec7 --- /dev/null +++ b/associated_type/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "associated_type" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/associated_type/src/main.rs b/associated_type/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/associated_type/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/book.toml b/book.toml new file mode 100644 index 0000000..981eaf0 --- /dev/null +++ b/book.toml @@ -0,0 +1,6 @@ +[book] +authors = ["Lenny Peng"] +language = "zh_CN" +multilingual = false +src = "src" +title = "Yet another Chinese rust-lang book." diff --git a/branches/Cargo.toml b/branches/Cargo.toml new file mode 100644 index 0000000..6934aa4 --- /dev/null +++ b/branches/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "branches" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/branches/src/main.rs b/branches/src/main.rs new file mode 100644 index 0000000..d09ae5c --- /dev/null +++ b/branches/src/main.rs @@ -0,0 +1,7 @@ +fn main() { + let condition = true; + + let number = if condition { 5 } else { "six" }; + + println! ("number 的值为:{}", number); +} diff --git a/cargo_features_demo/Cargo.toml b/cargo_features_demo/Cargo.toml new file mode 100644 index 0000000..2a540a2 --- /dev/null +++ b/cargo_features_demo/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "cargo_features_demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +art = { path = "../art" } diff --git a/cargo_features_demo/src/lib.rs b/cargo_features_demo/src/lib.rs new file mode 100644 index 0000000..98097dd --- /dev/null +++ b/cargo_features_demo/src/lib.rs @@ -0,0 +1,21 @@ +//! # Cargo 特性示例代码箱 +//! +//! `cargo_features_demo` 是令到执行某些确切计算更便利 +//! 的一些工具的集合。 +//! + +/// 将一加到所给数字。 +/// # 示例(examples) +/// +/// ``` +/// let arg = 5; +/// let answer = cargo_features_demo::add_one(arg); +/// +/// assert_eq! (6, answer); +/// ``` +pub fn add_one(x: i32) -> i32 { + x + 2 +} + +#[cfg(test)] +mod tests; diff --git a/cargo_features_demo/src/main.rs b/cargo_features_demo/src/main.rs new file mode 100644 index 0000000..170a783 --- /dev/null +++ b/cargo_features_demo/src/main.rs @@ -0,0 +1,8 @@ +use art::mix; +use art::PrimaryColor; + +fn main() { + let red = PrimaryColor::Red; + let yellow = PrimaryColor::Yellow; + mix(red, yellow); +} diff --git a/cargo_features_demo/src/tests.rs b/cargo_features_demo/src/tests.rs new file mode 100644 index 0000000..b65c49d --- /dev/null +++ b/cargo_features_demo/src/tests.rs @@ -0,0 +1,6 @@ +use super::*; + +#[test] +fn five_plus_one() { + assert_eq! (7, add_one(5)); +} diff --git a/closure-example/Cargo.toml b/closure-example/Cargo.toml new file mode 100644 index 0000000..8085ade --- /dev/null +++ b/closure-example/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "closure-example" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/closure-example/src/main.rs b/closure-example/src/main.rs new file mode 100644 index 0000000..0c514f5 --- /dev/null +++ b/closure-example/src/main.rs @@ -0,0 +1,22 @@ +#[derive(Debug)] +struct Rectangle { + width: u32, + height: u32, +} + +fn main() { + let mut list = [ + Rectangle { width: 10, height: 1 }, + Rectangle { width: 3, height: 5 }, + Rectangle { width: 7, height: 12 }, + ]; + + let mut sort_operations = vec! []; + let value = String::from("按照被调用到的 key"); + + list.sort_by_key(|r| { + sort_operations.push(&value); + r.width + }); + println! ("{:#?}\n{:#?}", list, sort_operations); +} diff --git a/closure_demo/Cargo.toml b/closure_demo/Cargo.toml new file mode 100644 index 0000000..643b4d5 --- /dev/null +++ b/closure_demo/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "closure_demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/closure_demo/src/main.rs b/closure_demo/src/main.rs new file mode 100644 index 0000000..974f605 --- /dev/null +++ b/closure_demo/src/main.rs @@ -0,0 +1,57 @@ +#[derive(Debug, PartialEq, Copy, Clone)] +enum ShirtColor { + Red, + Blue, +} + +struct Inventory { + shirts: Vec, +} + +impl Inventory { + fn giveaway( + &self, + user_preference: Option + ) -> ShirtColor { + user_preference.unwrap_or_else(|| self.most_stocked()) + } + + fn most_stocked(&self) -> ShirtColor { + let mut num_red = 0; + let mut num_blue = 0; + + for color in &self.shirts { + match color { + ShirtColor::Red => num_red += 1, + ShirtColor::Blue => num_blue += 1, + } + } + + if num_red > num_blue { + ShirtColor::Red + } else { + ShirtColor::Blue + } + } +} + +fn main() { + let store = Inventory { + shirts: vec! [ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue], + }; + + let user_pref1 = Some(ShirtColor::Red); + let giveaway1 = store.giveaway(user_pref1); + println! ( + "选项为 {:?} 的用户,得到了 {:?}", + user_pref1, giveaway1 + ); + + let user_pref2 = None; + let giveaway2 = store.giveaway(user_pref2); + println! ( + "选项为 {:?} 的用户得到了 {:?}", + user_pref2, giveaway2 + ); + +} diff --git a/concur_demo/Cargo.toml b/concur_demo/Cargo.toml new file mode 100644 index 0000000..53aebaa --- /dev/null +++ b/concur_demo/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "concur_demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/concur_demo/src/main.rs b/concur_demo/src/main.rs new file mode 100644 index 0000000..886d3ec --- /dev/null +++ b/concur_demo/src/main.rs @@ -0,0 +1,14 @@ +#![allow(dead_code)] +#![allow(unused_variables)] + +use std::thread; + +fn main() { + let v = vec! [1, 2, 3]; + + let handle = thread::spawn(move || { + println! ("这里有个矢量值:{:?}", &v); + }); + + handle.join().unwrap(); +} diff --git a/cons_list_demo/Cargo.toml b/cons_list_demo/Cargo.toml new file mode 100644 index 0000000..d9b7875 --- /dev/null +++ b/cons_list_demo/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "cons_list_demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/cons_list_demo/src/main.rs b/cons_list_demo/src/main.rs new file mode 100644 index 0000000..3a8bc3e --- /dev/null +++ b/cons_list_demo/src/main.rs @@ -0,0 +1,24 @@ +#[derive(Debug)] +enum List { + Cons(Rc>, Rc), + Nil, +} + +use crate::List::{Cons, Nil}; +use std::cell::RefCell; +use std::rc::Rc; + +fn main() { + let value = Rc::new(RefCell::new(5)); + + let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil))); + + let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a)); + let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a)); + + *value.borrow_mut() += 10; + + println! ("之后的 a = {:?}", a); + println! ("之后的 b = {:?}", b); + println! ("之后的 c = {:?}", c); +} diff --git a/declarative_macro/Cargo.lock b/declarative_macro/Cargo.lock new file mode 100644 index 0000000..4f1c856 --- /dev/null +++ b/declarative_macro/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "declarative_macro" +version = "0.1.0" diff --git a/declarative_macro/Cargo.toml b/declarative_macro/Cargo.toml new file mode 100644 index 0000000..aa308b2 --- /dev/null +++ b/declarative_macro/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "declarative_macro" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/declarative_macro/src/main.rs b/declarative_macro/src/main.rs new file mode 100644 index 0000000..65f6504 --- /dev/null +++ b/declarative_macro/src/main.rs @@ -0,0 +1,4 @@ +fn main() { + + println!("Hello, world!"); +} diff --git a/derive_macro_comsumer/Cargo.toml b/derive_macro_comsumer/Cargo.toml new file mode 100644 index 0000000..b7ff214 --- /dev/null +++ b/derive_macro_comsumer/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "derive_macro_comsumer" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +hello_macro = { path = "../hello_macro" } +hello_macro_derive = { path = "../hello_macro/hello_macro_derive" } diff --git a/derive_macro_comsumer/src/main.rs b/derive_macro_comsumer/src/main.rs new file mode 100644 index 0000000..468c30a --- /dev/null +++ b/derive_macro_comsumer/src/main.rs @@ -0,0 +1,9 @@ +use hello_macro::HelloMacro; +use hello_macro_derive::HelloMacro; + +#[derive(HelloMacro)] +struct Pancakes; + +fn main() { + Pancakes::hello_macro(); +} diff --git a/disambiguation/Cargo.toml b/disambiguation/Cargo.toml new file mode 100644 index 0000000..2fc552b --- /dev/null +++ b/disambiguation/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "disambiguation" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/disambiguation/src/main.rs b/disambiguation/src/main.rs new file mode 100644 index 0000000..a715029 --- /dev/null +++ b/disambiguation/src/main.rs @@ -0,0 +1,21 @@ +trait Animal { + fn baby_name() -> String; +} + +struct Dog; + +impl Dog { + fn baby_name() -> String { + String::from("点点") + } +} + +impl Animal for Dog { + fn baby_name() -> String { + String::from("狗崽") + } +} + +fn main() { + println! ("小狗叫做 {}", ::baby_name()); +} diff --git a/dst/Cargo.toml b/dst/Cargo.toml new file mode 100644 index 0000000..5d1f188 --- /dev/null +++ b/dst/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "dst" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/dst/src/main.rs b/dst/src/main.rs new file mode 100644 index 0000000..19bcaab --- /dev/null +++ b/dst/src/main.rs @@ -0,0 +1,4 @@ +fn main() { + let s1: str = "致以问候!"; + let s2: str = "最近过得怎么样?"; +} diff --git a/encapsulation_demo/Cargo.lock b/encapsulation_demo/Cargo.lock new file mode 100644 index 0000000..a420727 --- /dev/null +++ b/encapsulation_demo/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "encapsulation_demo" +version = "0.1.0" diff --git a/encapsulation_demo/Cargo.toml b/encapsulation_demo/Cargo.toml new file mode 100644 index 0000000..d463a97 --- /dev/null +++ b/encapsulation_demo/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "encapsulation_demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/encapsulation_demo/src/lib.rs b/encapsulation_demo/src/lib.rs new file mode 100644 index 0000000..3fc846c --- /dev/null +++ b/encapsulation_demo/src/lib.rs @@ -0,0 +1,34 @@ +#![allow(dead_code)] +#![allow(unused_variables)] + +pub struct AveragedCollection { + list: Vec, + average: f64, +} + +impl AveragedCollection { + pub fn add(&mut self, value: i32) { + self.list.push(value); + self.update_average(); + } + + pub fn remove(&mut self) -> Option { + let result = self.list.pop(); + match result { + Some(value) => { + self.update_average(); + Some(value) + } + None => None, + } + } + + pub fn average(&self) -> f64 { + self.average + } + + fn update_average(&mut self) { + let total: i32 = self.list.iter().sum(); + self.average = total as f64 / self.list.len() as f64; + } +} diff --git a/enum_demo/Cargo.toml b/enum_demo/Cargo.toml new file mode 100644 index 0000000..5cbbd5e --- /dev/null +++ b/enum_demo/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "enum_demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/enum_demo/src/main.rs b/enum_demo/src/main.rs new file mode 100644 index 0000000..0af60c3 --- /dev/null +++ b/enum_demo/src/main.rs @@ -0,0 +1,7 @@ +fn main() { + let config_max: Option = Some(3u8); + + if let Option::Some(max) = (config_max) { + println! ("极大值被设置为了 {}", max); + } +} diff --git a/error_handling_demo/Cargo.toml b/error_handling_demo/Cargo.toml new file mode 100644 index 0000000..3a161fe --- /dev/null +++ b/error_handling_demo/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "error_handling_demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/error_handling_demo/src/main.rs b/error_handling_demo/src/main.rs new file mode 100644 index 0000000..230c217 --- /dev/null +++ b/error_handling_demo/src/main.rs @@ -0,0 +1,9 @@ +fn main () { + use std::net::IpAddr; + + let home: IpAddr = "192.168.0.255" + .parse() + .expect("硬编码的 IP 地址应是有效的"); + + println! ("{}", home); +} diff --git a/extern_code/Cargo.toml b/extern_code/Cargo.toml new file mode 100644 index 0000000..a271a5f --- /dev/null +++ b/extern_code/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "extern_code" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/extern_code/src/main.rs b/extern_code/src/main.rs new file mode 100644 index 0000000..cfbfcce --- /dev/null +++ b/extern_code/src/main.rs @@ -0,0 +1,17 @@ +extern "C" { + fn abs(input: i32) -> i32; + fn sqrt(input: f64) -> f64; +} + + +#[no_mangle] +pub extern "C" fn call_from_c() { + println! ("刚从 C 调用了一个 Rust 函数!"); +} + +fn main() { + unsafe { + println! ("C 语言中 -3 的绝对值为:{},3.0 的平方根为:{}", abs(-3), sqrt(3.0)); + } +} + diff --git a/fah_to_cels/Cargo.toml b/fah_to_cels/Cargo.toml new file mode 100644 index 0000000..a4be573 --- /dev/null +++ b/fah_to_cels/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "fah_to_cels" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/fah_to_cels/src/main.rs b/fah_to_cels/src/main.rs new file mode 100644 index 0000000..af6d21f --- /dev/null +++ b/fah_to_cels/src/main.rs @@ -0,0 +1,67 @@ +use std::io; +use std::process; + +fn fah_to_cels(f: f32) -> f32 { + return (f - 32.0) / 1.8; +} + +fn cels_to_fah(c: f32) -> f32 { + return c * 1.8 + 32.0; +} + +fn main() { + println! ("法式温度与摄氏温度之间的转换"); + + loop { + println! ("\n-----------------\n请选择: + '1'-摄氏温度/'2'-法式温度/'Q'/\"quit\" 退出程序。 + '1'/'2'/'Q'/\"quit\"[1]:"); + + let mut temp_type = String::new(); + + io::stdin() + .read_line(&mut temp_type) + .expect("读取输入失败!"); + + let temp_type = temp_type.trim(); + + if temp_type.eq("Q") || temp_type.eq("quit") { process::exit(0); } + + if ! temp_type.eq("1") && ! temp_type.eq("2") && ! temp_type.eq("") { + println! ("无效输入,请输入 '1'、'2'、'Q'、\"quit\",或直接按下回车键"); + continue; + } + + if temp_type.eq("1") || temp_type.eq("") { + println! ("请输入要转换的摄氏温度:"); + let temp = get_temp_input(); + + println! ("摄氏温度: {:.2}°C,约为法氏温度:{:.2}°F", temp, cels_to_fah(temp)); + } + + if temp_type.eq("2") { + println! ("请输入要转换的法氏温度:"); + let temp = get_temp_input(); + + println! ("法氏温度:{:.2}°F,约为摄氏温度:{:.2}°C", temp, fah_to_cels(temp)); + } + } +} + +fn get_temp_input() -> f32 { + return loop { + let mut tmp = String::new(); + + io::stdin() + .read_line(&mut tmp) + .expect("读取输入失败"); + + match tmp.trim().parse() { + Ok(num) => { break num }, + Err(_) => { + println! ("请输入一个浮点数,比如 -10.0, 15.6"); + continue + } + }; + }; +} diff --git a/fn_pattn_demo/Cargo.toml b/fn_pattn_demo/Cargo.toml new file mode 100644 index 0000000..1e72a95 --- /dev/null +++ b/fn_pattn_demo/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "fn_pattn_demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/fn_pattn_demo/src/main.rs b/fn_pattn_demo/src/main.rs new file mode 100644 index 0000000..0fe2f3b --- /dev/null +++ b/fn_pattn_demo/src/main.rs @@ -0,0 +1,8 @@ +fn print_coordinates(&(x, y): &(i32, i32)) { + println!("当前坐标:({}, {})", x, y); +} + +fn main() { + let point = (3, -5); + print_coordinates(&point); +} diff --git a/for_demo/Cargo.toml b/for_demo/Cargo.toml new file mode 100644 index 0000000..fac497a --- /dev/null +++ b/for_demo/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "for_demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/for_demo/src/main.rs b/for_demo/src/main.rs new file mode 100644 index 0000000..14f624c --- /dev/null +++ b/for_demo/src/main.rs @@ -0,0 +1,7 @@ +fn main() { + let v = vec! ['a', 'b', 'c']; + + for (index, value) in v.iter().enumerate() { + println! ("{} 处于索引 {} 处", value, index); + } +} diff --git a/func_pointer/Cargo.toml b/func_pointer/Cargo.toml new file mode 100644 index 0000000..0758072 --- /dev/null +++ b/func_pointer/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "func_pointer" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/func_pointer/src/main.rs b/func_pointer/src/main.rs new file mode 100644 index 0000000..b17345c --- /dev/null +++ b/func_pointer/src/main.rs @@ -0,0 +1,30 @@ +fn add_one(x: i32) -> i32 { + x + 1 +} + +fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 { + f(arg) + f(arg) +} + + +fn main() { + let answer = do_twice(add_one, 5); + + println! ("答案为:{}", answer); + + let list_of_numbers = vec! [1, 2, 3]; + let list_of_strings: Vec = + list_of_numbers.iter().map(ToString::to_string).collect(); + + println! ("结果为:{:?}", list_of_strings); + + #[derive(Debug)] + enum Status { + Value(u32), + Stop, + } + + let mut list_of_statuses: Vec = (0u32..20).map(Status::Value).collect(); + list_of_statuses.append(&mut vec! [Status::Stop]); + println! ("list_of_statuses: {:?}", list_of_statuses); +} diff --git a/functions/Cargo.toml b/functions/Cargo.toml new file mode 100644 index 0000000..a9b5578 --- /dev/null +++ b/functions/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "functions" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/functions/src/main.rs b/functions/src/main.rs new file mode 100644 index 0000000..a79372c --- /dev/null +++ b/functions/src/main.rs @@ -0,0 +1,9 @@ +fn main() { + let x = plus_one(-1); + + println! ("x 的值为:{}", x); +} + +fn plus_one(x: i32) -> i32 { + x + 1 +} diff --git a/generics_demo/Cargo.toml b/generics_demo/Cargo.toml new file mode 100644 index 0000000..8974a0e --- /dev/null +++ b/generics_demo/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "generics_demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/generics_demo/src/main.rs b/generics_demo/src/main.rs new file mode 100644 index 0000000..d25919a --- /dev/null +++ b/generics_demo/src/main.rs @@ -0,0 +1,14 @@ +enum Option_i32 { + Some(i32), + None, +} + +enum Option_f64 { + Some(f64), + None, +} + +fn main() { + let integer = Option_i32::Some(5); + let float = Option_f64::Some(5.0); +} diff --git a/guessing_game/Cargo.toml b/guessing_game/Cargo.toml new file mode 100644 index 0000000..80f0a08 --- /dev/null +++ b/guessing_game/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "guessing_game-xfossdotcom" +license = "MIT" +version = "0.1.1" +description = "一个在其中猜出计算机所选数字的有趣游戏。" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rand = "0.8.3" diff --git a/guessing_game/README.md b/guessing_game/README.md new file mode 100644 index 0000000..610c66e --- /dev/null +++ b/guessing_game/README.md @@ -0,0 +1,3 @@ +# Readme + +这是一个作为把代码箱上传到 [crates.io](https://crates.io) 示例的 Rust 项目。 diff --git a/guessing_game/src/main.rs b/guessing_game/src/main.rs new file mode 100644 index 0000000..79837d3 --- /dev/null +++ b/guessing_game/src/main.rs @@ -0,0 +1,64 @@ +use rand::Rng; +use std::{cmp::Ordering, io, process}; + +pub struct Guess { + value: i32, +} + +impl Guess { + pub fn new(value: i32) -> Guess { + if value < 1 || value > 100 { + panic! ("Guess 类型值必须在 1 与 100 之间,收到的是 {}", value); + } + + Guess { value } + } + + pub fn value(&self) -> i32 { + self.value + } +} + +fn main() { + loop { + println! ("\n---猜出这个数来!---"); + + let secret_number: i32 = rand::thread_rng().gen_range(1..101); + + // println! ("随机生成的秘密数字为:{}", secret_number); + + loop { + println! ("请输入你猜的数。( ‘Q/quit’ 退出游戏)"); + + let mut guess: String = String::new(); + + io::stdin() + .read_line(&mut guess) + .expect("读取行失败......"); + + if guess.trim().eq("Q") || guess.trim().eq("quit") { process::exit(0); } + + // let guess: u32 = guess.trim().parse().expect("请输入一个数字!"); + let guess: i32 = match guess.trim().parse() { + Ok(num) => num, + Err(_) => { println! ("请输入一个数字!"); continue }, + }; + + if guess < 1 || guess > 100 { + println! ("秘密数字将在 1 和 100 之间"); + continue + } + + println! ("你猜的数为:{}", guess); + + match guess.cmp(&secret_number) { + Ordering::Less => println! ("太小了!"), + Ordering::Greater => println! ("太大了!"), + Ordering::Equal => { + println! ("你赢了!"); + break + }, + } + } + } +} diff --git a/hashmap_demo/Cargo.toml b/hashmap_demo/Cargo.toml new file mode 100644 index 0000000..c948a8c --- /dev/null +++ b/hashmap_demo/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "hashmap_demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/hashmap_demo/src/main.rs b/hashmap_demo/src/main.rs new file mode 100644 index 0000000..dcaf432 --- /dev/null +++ b/hashmap_demo/src/main.rs @@ -0,0 +1,14 @@ +fn main() { + use std::collections::HashMap; + + let text = "hello world wonderful world"; + + let mut map = HashMap::new(); + + for word in text.split_whitespace() { + let count = map.entry(word).or_insert(0); + *count += 1; + } + + println! ("{:?}", map); +} diff --git a/hello/404.html b/hello/404.html new file mode 100644 index 0000000..49c36b1 --- /dev/null +++ b/hello/404.html @@ -0,0 +1,11 @@ + + + + + 你好! + + +

糟糕!

+

抱歉,我不明白你要什么。

+ + diff --git a/hello/Cargo.toml b/hello/Cargo.toml new file mode 100644 index 0000000..fb1ec2c --- /dev/null +++ b/hello/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "hello" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/hello/hello.html b/hello/hello.html new file mode 100644 index 0000000..b3d914e --- /dev/null +++ b/hello/hello.html @@ -0,0 +1,11 @@ + + + + + 你好! + + +

你好!

+

来自 Rust 的问好

+ + diff --git a/hello/src/main.rs b/hello/src/main.rs new file mode 100644 index 0000000..bc7a713 --- /dev/null +++ b/hello/src/main.rs @@ -0,0 +1,35 @@ +#![allow(warnings)] +use std::{ + fs, + io::{prelude::*, BufReader}, + net::{TcpListener, TcpStream}, +}; + +fn main() { + let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); + + for stream in listener.incoming() { + let stream = stream.unwrap(); + + handle_conn(stream); + } +} + +fn handle_conn(mut stream: TcpStream) { + let buf_reader = BufReader::new(&mut stream); + let req_line = buf_reader.lines().next().unwrap().unwrap(); + + let (status_line, filename) = if req_line == "GET / HTTP/1.1" { + ( "HTTP/1.1 200 OK", "hello.html") + } else { + ("HTTP/1.1 404 NOT FOUND", "404.html") + }; + + let contents = fs::read_to_string(filename).unwrap(); + let length = contents.len(); + + let resp = + format! ("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); + + stream.write_all(resp.as_bytes()).unwrap(); +} diff --git a/hello_cargo/Cargo.toml b/hello_cargo/Cargo.toml new file mode 100644 index 0000000..19c7b36 --- /dev/null +++ b/hello_cargo/Cargo.toml @@ -0,0 +1,8 @@ +[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] diff --git a/hello_cargo/src/main.rs b/hello_cargo/src/main.rs new file mode 100644 index 0000000..bfb0e05 --- /dev/null +++ b/hello_cargo/src/main.rs @@ -0,0 +1,4 @@ +fn main() { + // 这是注释。 + println! ("Hello, Cargo!"); +} diff --git a/hello_macro/Cargo.toml b/hello_macro/Cargo.toml new file mode 100644 index 0000000..9b9d479 --- /dev/null +++ b/hello_macro/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "hello_macro" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/hello_macro/hello_macro_derive/Cargo.toml b/hello_macro/hello_macro_derive/Cargo.toml new file mode 100644 index 0000000..1b02fb9 --- /dev/null +++ b/hello_macro/hello_macro_derive/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "hello_macro_derive" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +proc-macro = true + +[dependencies] +syn = "1.0" +quote = "1.0" diff --git a/hello_macro/hello_macro_derive/src/lib.rs b/hello_macro/hello_macro_derive/src/lib.rs new file mode 100644 index 0000000..e45a413 --- /dev/null +++ b/hello_macro/hello_macro_derive/src/lib.rs @@ -0,0 +1,26 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn; + +#[proc_macro_derive(HelloMacro)] +pub fn hello_macro_derive(input: TokenStream) -> TokenStream { + // 以语法树形式,构建出咱们可操作 Rust 代码的表示 + // Construct a representation of Rust code as a syntax tree + // that we can manipulate + let ast = syn::parse(input).unwrap(); + + // 构造出这个特质实现 + impl_hello_macro(&ast) +} + +fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream { + let name = &ast.ident; + let gen = quote! { + impl HelloMacro for #name { + fn hello_macro() { + println! ("你好,宏!我的名字叫 {}!", stringify! (#name)); + } + } + }; + gen.into() +} diff --git a/hello_macro/src/lib.rs b/hello_macro/src/lib.rs new file mode 100644 index 0000000..e747931 --- /dev/null +++ b/hello_macro/src/lib.rs @@ -0,0 +1,3 @@ +pub trait HelloMacro { + fn hello_macro(); +} diff --git a/hello_macro_derive/Cargo.toml b/hello_macro_derive/Cargo.toml new file mode 100644 index 0000000..8fa4119 --- /dev/null +++ b/hello_macro_derive/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "hello_macro_derive" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/hello_macro_derive/src/lib.rs b/hello_macro_derive/src/lib.rs new file mode 100644 index 0000000..7d12d9a --- /dev/null +++ b/hello_macro_derive/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/hello_world/Cargo.toml b/hello_world/Cargo.toml new file mode 100644 index 0000000..624cb06 --- /dev/null +++ b/hello_world/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "hello_world" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/hello_world/src/main.rs b/hello_world/src/main.rs new file mode 100644 index 0000000..96e80b1 --- /dev/null +++ b/hello_world/src/main.rs @@ -0,0 +1,4 @@ +fn main() { + // 这是注释。 + println!("Hello, World!"); +} diff --git a/if_let_demo/Cargo.toml b/if_let_demo/Cargo.toml new file mode 100644 index 0000000..dc397a9 --- /dev/null +++ b/if_let_demo/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "if_let_demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/if_let_demo/src/main.rs b/if_let_demo/src/main.rs new file mode 100644 index 0000000..af9a212 --- /dev/null +++ b/if_let_demo/src/main.rs @@ -0,0 +1,22 @@ +#![allow(dead_code)] +#![allow(unused_variables)] + +fn main() { + let favorite_color: Option<&str> = None; + let is_tuesday = false; + let age: Result = "34".parse(); + + if let Some(color) = favorite_color { + println! ("使用你喜欢的颜色,{color},作为背景"); + } else if is_tuesday { + println! ("周二是绿色的一天!"); + } else if let Ok(age) = age { + if age > 30 { + println! ("使用紫色作为背景色"); + } else { + println! ("使用橙色作为背景色"); + } + } else { + println! ("使用蓝色作为背景色"); + } +} diff --git a/iterator_demo/Cargo.toml b/iterator_demo/Cargo.toml new file mode 100644 index 0000000..ac9fb49 --- /dev/null +++ b/iterator_demo/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "iterator_demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/iterator_demo/src/lib.rs b/iterator_demo/src/lib.rs new file mode 100644 index 0000000..bea2d9c --- /dev/null +++ b/iterator_demo/src/lib.rs @@ -0,0 +1,35 @@ +#[derive(PartialEq, Debug)] +struct Shoe { + size: u32, + style: String, +} + +fn shoes_in_size(shoes: Vec, shoe_size: u32) -> Vec { + shoes.into_iter().filter(|s| s.size == shoe_size).collect() +} + +#[cfg(test)] +mod tests; + +#[test] +fn iterator_demonstration() { + let v1 = vec! [1, 2, 3]; + + let mut v1_iter = v1.iter(); + + assert_eq! (v1_iter.next(), Some(&1)); + assert_eq! (v1_iter.next(), Some(&2)); + assert_eq! (v1_iter.next(), Some(&3)); + assert_eq! (v1_iter.next(), None); +} + +#[test] +fn iterator_sum() { + let v1 = vec! [1, 2, 3]; + + let v1_iter = v1.iter(); + + let total: i32 = v1_iter.sum(); + + assert_eq! (total, 6); +} diff --git a/iterator_demo/src/main.rs b/iterator_demo/src/main.rs new file mode 100644 index 0000000..00097c5 --- /dev/null +++ b/iterator_demo/src/main.rs @@ -0,0 +1,7 @@ +fn main() { + let v1 = vec! [1, 2, 3]; + + let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); + + assert_eq! (v2, vec! [2, 3, 4]); +} diff --git a/iterator_demo/src/tests.rs b/iterator_demo/src/tests.rs new file mode 100644 index 0000000..bbdc376 --- /dev/null +++ b/iterator_demo/src/tests.rs @@ -0,0 +1,35 @@ +use super::*; + +#[test] +fn filter_by_size() { + let shoes = vec! [ + Shoe { + size: 10, + style: String::from("sneaker"), + }, + Shoe { + size: 13, + style: String::from("sandal"), + }, + Shoe { + size: 10, + style: String::from("boot"), + }, + ]; + + let in_my_size = shoes_in_size(shoes, 10); + + assert_eq! ( + in_my_size, + vec! [ + Shoe { + size: 10, + style: String::from("sneaker"), + }, + Shoe { + size: 10, + style: String::from("boot"), + }, + ] + ); +} diff --git a/lifetimes_demo/Cargo.toml b/lifetimes_demo/Cargo.toml new file mode 100644 index 0000000..9383aea --- /dev/null +++ b/lifetimes_demo/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "lifetimes_demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/lifetimes_demo/src/main.rs b/lifetimes_demo/src/main.rs new file mode 100644 index 0000000..d0bc65a --- /dev/null +++ b/lifetimes_demo/src/main.rs @@ -0,0 +1,23 @@ +use std::fmt::Display; + +fn longest_with_an_announcement<'a, T>( + x: &'a str, + y: &'a str, + ann: T, +) -> &'a str +where + T: Display, +{ + println! ("通知!{}", ann); + if x.len() > y.len() { + x + } else { + y + } +} + +fn main() { + let result = longest_with_an_announcement("abc", "测试", "计算结果已出来。"); + + println! ("{}", result); +} diff --git a/limit_tracker/Cargo.toml b/limit_tracker/Cargo.toml new file mode 100644 index 0000000..c7089a1 --- /dev/null +++ b/limit_tracker/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "limit_tracker" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/limit_tracker/src/lib.rs b/limit_tracker/src/lib.rs new file mode 100644 index 0000000..27494ae --- /dev/null +++ b/limit_tracker/src/lib.rs @@ -0,0 +1,77 @@ +pub trait Messenger { + fn send(&self, msg: &str); +} + +pub struct LimitTracker<'a, T: Messenger> { + messenger: &'a T, + value: usize, + max: usize, +} + +impl<'a, T> LimitTracker<'a, T> +where + T: Messenger, +{ + pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> { + LimitTracker { + messenger, + value: 0, + max, + } + } + + pub fn set_value(&mut self, value: usize) { + self.value = value; + + let percentage_of_max = self.value as f64 / self.max as f64; + + if percentage_of_max >= 1.0 { + self.messenger.send("出错:你已超出你的配额!"); + } else if percentage_of_max >= 0.9 { + self.messenger + .send("紧急警告:你已用掉你配额的 90% !"); + } else if percentage_of_max >= 0.75 { + self.messenger + .send("警告:你已用掉你配额的 75% !"); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::cell::RefCell; + + struct MockMessenger { + sent_messages: RefCell>, + } + + impl MockMessenger { + fn new() -> MockMessenger { + MockMessenger { + sent_messages: RefCell::new(vec! []), + } + } + } + + impl Messenger for MockMessenger { + fn send(&self, message: &str) { + let mut borrow_one = self.sent_messages.borrow_mut(); + let mut borrow_two = self.sent_messages.borrow_mut(); + + borrow_one.push(String::from(message)); + borrow_two.push(String::from(message)); + } + } + + #[test] + fn it_sends_an_over_75_percent_waring_message() { + let mock_messenger = MockMessenger::new(); + let mut limit_tracker = LimitTracker::new(&mock_messenger, 100); + + limit_tracker.set_value(80); + println! ("{}", mock_messenger.sent_messages.borrow().iter().next().unwrap()); + + assert_eq! (mock_messenger.sent_messages.borrow().len(), 1); + } +} diff --git a/loops/Cargo.toml b/loops/Cargo.toml new file mode 100644 index 0000000..0d5dad5 --- /dev/null +++ b/loops/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "loops" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/loops/src/main.rs b/loops/src/main.rs new file mode 100644 index 0000000..41b6133 --- /dev/null +++ b/loops/src/main.rs @@ -0,0 +1,7 @@ +fn main() { + for number in (1..4).rev() { + println! ("{}!", number); + } + + println! ("发射!!"); +} diff --git a/lyrics_of_xmas_carol/Cargo.toml b/lyrics_of_xmas_carol/Cargo.toml new file mode 100644 index 0000000..f206dd8 --- /dev/null +++ b/lyrics_of_xmas_carol/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "lyrics_of_xmas_carol" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/lyrics_of_xmas_carol/src/main.rs b/lyrics_of_xmas_carol/src/main.rs new file mode 100644 index 0000000..7dd9c5e --- /dev/null +++ b/lyrics_of_xmas_carol/src/main.rs @@ -0,0 +1,60 @@ +fn main() { + let days = [ + "first", + "second", + "third", + "fourth", + "fifth", + "sixth", + "seventh", + "eighth", + "nineth", + "tenth", + "eleventh", + "twelfth" + ]; + let amounts = [ + "A", + "Two", + "Three", + "Four", + "Five", + "Six", + "Seven", + "Eight", + "Nine", + "Ten", + "Eleven", + "Twelve" + ]; + let things = [ + "partridge in a pear tree", + "turtle doves", + "French hens", + "calling birds", + "golden rings", + "geese-a-laying", + "swans-a-swimming", + "maids-a-milking", + "ladies dancing", + "lords-a-leaping", + "pipers piping", + "drummers drumming", + ]; + + for num in 1..=12 { + println! ("\nOn the {} day of Christmas,\nMy true love gave to me:", + days[num-1]); + for tmp in (0..num).rev() { + if tmp == 0 && num == 1 { + println! ("{} {}.", amounts[tmp], things[tmp]); + } + if tmp == 0 && num != 1 { + println! ("And {} {}.", amounts[tmp].to_lowercase(), things[tmp]); + } + if tmp != 0 { + println! ("{} {},", amounts[tmp], things[tmp]); + } + } + } +} diff --git a/minigrep/Cargo.toml b/minigrep/Cargo.toml new file mode 100644 index 0000000..703598f --- /dev/null +++ b/minigrep/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "minigrep" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +[profile.dev] +opt-level = 1 + +[profile.release] +opt-level = 3 diff --git a/minigrep/poem.txt b/minigrep/poem.txt new file mode 100644 index 0000000..8707527 --- /dev/null +++ b/minigrep/poem.txt @@ -0,0 +1,9 @@ +I'm nobody! Who are you? +Are you nobody, too? +Then there's a pair of us - don't tell! +They'd banish us, you know. + +How dreary to be somebody! +How public, like a frog +To tell your name the livelong day +To an admiring bog! diff --git a/minigrep/src/data_structures.rs b/minigrep/src/data_structures.rs new file mode 100644 index 0000000..5d7b678 --- /dev/null +++ b/minigrep/src/data_structures.rs @@ -0,0 +1,33 @@ +use std::env; + +pub struct Config { + pub query: String, + pub file_path: String, + pub ignore_case: bool, +} + +impl Config { + pub fn build( + mut args: impl Iterator, + ) -> Result { + args.next(); + + let query = match args.next() { + Some(arg) => arg, + None => return Err("未曾获取到查询字串"), + }; + + let file_path = match args.next() { + Some(arg) => arg, + None => return Err("未曾获取到文件路径"), + }; + + let ignore_case = env::var("IGNORE_CASE").is_ok(); + + Ok(Config { + query, + file_path, + ignore_case, + }) + } +} diff --git a/minigrep/src/lib.rs b/minigrep/src/lib.rs new file mode 100644 index 0000000..3b9fd7c --- /dev/null +++ b/minigrep/src/lib.rs @@ -0,0 +1,57 @@ +#![allow(warnings)] +// +// this is to disable warnings. +// Comment it to enable warnings. +// +use std::error::Error; +use std::fs; +use data_structures::Config; +// +// 以下两种写法,也是可以的 +// +// use self::data_structures::Config; +// use crate::data_structures::Config; + +#[cfg(test)] +mod tests; +pub mod data_structures; + +pub fn run( + config: Config +) -> Result<(), Box> { + let contents = fs::read_to_string(config.file_path)?; + + let results: Vec<&str> = if config.ignore_case { + search_insensitive(&config.query, &contents) + } else { + search(&config.query, &contents) + }; + + for line in results { + println! ("{line}"); + } + + Ok(()) +} + +pub fn search<'a>( + query: &str, + contents: &'a str +) -> Vec<&'a str> { + contents + .lines() + .filter(|line| line.contains(query)) + .collect() +} + +pub fn search_insensitive<'a>( + query: &str, + contents: &'a str +) -> Vec<&'a str> { + let query = query.to_lowercase(); + + contents + .lines() + .filter(|line| line.to_lowercase().contains(&query)) + .collect() +} diff --git a/minigrep/src/main.rs b/minigrep/src/main.rs new file mode 100644 index 0000000..7af555f --- /dev/null +++ b/minigrep/src/main.rs @@ -0,0 +1,19 @@ +use std::env; +use std::process; + +use minigrep::data_structures::Config; + +fn main() { + let config = Config::build(env::args()) + .unwrap_or_else(|err| { + eprintln! ("解析参数时遇到问题:{err}"); + process::exit(1); + }); + + println! ("在文件 {} 中检索:{}", config.file_path, config.query); + + if let Err(e) = minigrep::run(config) { + eprintln! ("应用程序错误:{e}"); + process::exit(1); + } +} diff --git a/minigrep/src/tests.rs b/minigrep/src/tests.rs new file mode 100644 index 0000000..5410559 --- /dev/null +++ b/minigrep/src/tests.rs @@ -0,0 +1,28 @@ +use super::*; + +#[test] +fn case_sensitive() { + let query = "duct"; + let contents = "\ +Rust: +safe, fast, productive. +Pick three. +Duct tape."; + + assert_eq! (vec! ["safe, fast, productive."], search(query, contents)); +} + +#[test] +fn case_insensitive() { + let query = "rUst"; + let contents = "\ +Rust: +safe, fast, productive. +Pick three. +Trust me."; + + assert_eq! ( + vec! ["Rust:", "Trust me."], + search_insensitive(query, contents) + ); +} diff --git a/mp_demo/Cargo.lock b/mp_demo/Cargo.lock new file mode 100644 index 0000000..3792509 --- /dev/null +++ b/mp_demo/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "mp_demo" +version = "0.1.0" diff --git a/mp_demo/Cargo.toml b/mp_demo/Cargo.toml new file mode 100644 index 0000000..be7808c --- /dev/null +++ b/mp_demo/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "mp_demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/mp_demo/src/main.rs b/mp_demo/src/main.rs new file mode 100644 index 0000000..066d2e9 --- /dev/null +++ b/mp_demo/src/main.rs @@ -0,0 +1,43 @@ +#![allow(dead_code)] +#![allow(unused_variables)] + +use std::sync::mpsc; +use std::thread; +use std::time::Duration; + +fn main() { + let (tx, rx) = mpsc::channel(); + + let tx1 = tx.clone(); + thread::spawn(move || { + let vals = vec! [ + String::from("你好"), + String::from("自"), + String::from("此"), + String::from("线程"), + ]; + + for val in vals { + tx1.send(val).unwrap(); + thread::sleep(Duration::from_millis(500)); + } + }); + + thread::spawn(move || { + let vals = vec! [ + String::from("给"), + String::from("你"), + String::from("一些别的"), + String::from("消息"), + ]; + + for val in vals { + tx.send(val).unwrap(); + thread::sleep(Duration::from_millis(500)); + } + }); + + for received in rx { + println! ("收到:{}", received); + } +} diff --git a/mutex_demo/Cargo.lock b/mutex_demo/Cargo.lock new file mode 100644 index 0000000..348018e --- /dev/null +++ b/mutex_demo/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "mutex_demo" +version = "0.1.0" diff --git a/mutex_demo/Cargo.toml b/mutex_demo/Cargo.toml new file mode 100644 index 0000000..c15b0f8 --- /dev/null +++ b/mutex_demo/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "mutex_demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/mutex_demo/src/main.rs b/mutex_demo/src/main.rs new file mode 100644 index 0000000..d7ac6a8 --- /dev/null +++ b/mutex_demo/src/main.rs @@ -0,0 +1,27 @@ +#![allow(dead_code)] +#![allow(unused_variables)] + +use std::sync::{Arc, Mutex}; +use std::thread; + +fn main() { + let counter = Arc::new(Mutex::new(0)); + let mut handles = vec! []; + + for _ in 0..10 { + let counter = Arc::clone(&counter); + let handle = thread::spawn(move || { + let mut num = counter.lock().unwrap(); + + *num += 1; + }); + + handles.push(handle); + } + + for handle in handles { + handle.join().unwrap(); + } + + println! ("结果为:{}", *counter.lock().unwrap()); +} diff --git a/neo_simple_blog/Cargo.lock b/neo_simple_blog/Cargo.lock new file mode 100644 index 0000000..a554162 --- /dev/null +++ b/neo_simple_blog/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "neo_simple_blog" +version = "0.1.0" diff --git a/neo_simple_blog/Cargo.toml b/neo_simple_blog/Cargo.toml new file mode 100644 index 0000000..501d90c --- /dev/null +++ b/neo_simple_blog/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "neo_simple_blog" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/neo_simple_blog/src/lib.rs b/neo_simple_blog/src/lib.rs new file mode 100644 index 0000000..38500a6 --- /dev/null +++ b/neo_simple_blog/src/lib.rs @@ -0,0 +1,43 @@ +pub struct Post { + content: String, +} + +pub struct DraftPost { + content: String, +} + +impl Post { + pub fn new() -> DraftPost { + DraftPost { + content: String::new(), + } + } + + pub fn content(&self) -> &str { + &self.content + } +} + +impl DraftPost { + pub fn add_text(&mut self, text: &str) { + self.content.push_str(text); + } + + pub fn request_review(self) -> PendingReviewPost { + PendingReviewPost { + content: self.content, + } + } +} + +pub struct PendingReviewPost { + content: String, +} + +impl PendingReviewPost { + pub fn approve(self) -> Post { + Post { + content: self.content, + } + } +} diff --git a/neo_simple_blog/src/main.rs b/neo_simple_blog/src/main.rs new file mode 100644 index 0000000..4ab39ce --- /dev/null +++ b/neo_simple_blog/src/main.rs @@ -0,0 +1,15 @@ +#![allow(dead_code)] +#![allow(unused_variables)] + +use neo_simple_blog::Post; + +fn main() { + let mut post = Post::new(); + + post.add_text("这是一个博客帖子。"); + + let post = post.request_review(); + let post = post.approve(); + + assert_eq! ("这是一个博客帖子。", post.content()); +} diff --git a/never_type/Cargo.toml b/never_type/Cargo.toml new file mode 100644 index 0000000..7f1d489 --- /dev/null +++ b/never_type/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "never_type" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/never_type/src/main.rs b/never_type/src/main.rs new file mode 100644 index 0000000..2071f77 --- /dev/null +++ b/never_type/src/main.rs @@ -0,0 +1,7 @@ +fn main() { + print! ("永永 "); + + loop { + print! ("远远 "); + } +} diff --git a/newtype/Cargo.toml b/newtype/Cargo.toml new file mode 100644 index 0000000..dd63388 --- /dev/null +++ b/newtype/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "newtype" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/newtype/src/main.rs b/newtype/src/main.rs new file mode 100644 index 0000000..776b9d7 --- /dev/null +++ b/newtype/src/main.rs @@ -0,0 +1,13 @@ +use std::fmt; + +struct Wrapper(Vec); + +impl fmt::Display for Wrapper { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write! (f, "[{}]", self.0.join(", ")) + } +} +fn main() { + let w = Wrapper(vec! [String::from("你好"), String::from("世界")]); + println! ("w = {}", w); +} diff --git a/nth_fibonacci/Cargo.toml b/nth_fibonacci/Cargo.toml new file mode 100644 index 0000000..633d91a --- /dev/null +++ b/nth_fibonacci/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "nth_fibonacci" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +num-format = "0.4.0" diff --git a/nth_fibonacci/src/main.rs b/nth_fibonacci/src/main.rs new file mode 100644 index 0000000..f7a6fd6 --- /dev/null +++ b/nth_fibonacci/src/main.rs @@ -0,0 +1,47 @@ +use std::io; +use num_format::{Locale, ToFormattedString}; +// use std::process; + +fn nth_fibonacci(n: u64) -> u64 { + + if n == 0 || n == 1 { + return n; + } else { + return nth_fibonacci(n - 1) + nth_fibonacci(n - 2); + } +} + +fn main() { + println! ("找出第 n 个斐波拉基数"); + + 'main_loop: loop { + println! ("请输入 n: (Q/quit 退出程序)"); + + let n: u64 = loop { + let mut tmp = String::new(); + + io::stdin() + .read_line(&mut tmp) + .expect("读取输入失败!"); + + let tmp = tmp.trim(); + + if tmp.eq("Q") || tmp.eq("quit") { + // process::exit(0); + break 'main_loop; + } + + match tmp.parse() { + Ok(num) => { break num }, + Err(_) => { + println! ("请输入一个正整数!\n"); + continue; + }, + }; + }; + + println! ("第 {} 个斐波拉基数为:{}", + n, + nth_fibonacci(n).to_formatted_string(&Locale::en)); + } +} diff --git a/operator_overloading/Cargo.toml b/operator_overloading/Cargo.toml new file mode 100644 index 0000000..0a6eb65 --- /dev/null +++ b/operator_overloading/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "operator_overloading" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/operator_overloading/src/main.rs b/operator_overloading/src/main.rs new file mode 100644 index 0000000..7b7b95b --- /dev/null +++ b/operator_overloading/src/main.rs @@ -0,0 +1,45 @@ +use std::ops::Add; + +#[derive(Debug, Copy, Clone, PartialEq)] +struct Point { + x: i32, + y: i32, +} + +impl Add for Point { + type Output = Point; + + fn add(self, other: Point) -> Point { + Point { + x: self.x + other.x, + y: self.y + other.y, + } + } +} + + +#[derive(Debug, Copy, Clone, PartialEq)] +struct Millimeters(u32); + +#[derive(Debug, Copy, Clone, PartialEq)] +struct Meters(u32); + +impl Add for Millimeters { + type Output = Millimeters; + + fn add(self, other: Meters) -> Millimeters { + Millimeters(self.0 + (other.0 * 1000)) + } +} + +fn main() { + assert_eq! ( + Point { x: 1, y: 0 } + Point { x: 2, y: 3}, + Point { x: 3, y: 3 } + ); + + assert_eq! ( + Millimeters(50) + Meters(1), + Millimeters(1050) + ); +} diff --git a/ownership_demo/Cargo.toml b/ownership_demo/Cargo.toml new file mode 100644 index 0000000..b4df443 --- /dev/null +++ b/ownership_demo/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "ownership_demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/ownership_demo/src/main.rs b/ownership_demo/src/main.rs new file mode 100644 index 0000000..7bd91cc --- /dev/null +++ b/ownership_demo/src/main.rs @@ -0,0 +1,42 @@ +fn main() { + let s = String::from("The quick brown fox jumps over the lazy dog."); + + // 函数 first_word 在 String 值的切片上有效,不管是部分还是全部的切片 + let word = first_word(&s[0..6]); + println! ("{}", word); + + let word = first_word(&s[..]); + println! ("{}", word); + + // 函数 first_word 还在 String 变量的引用上有效,而 String 变量的引用 + // 与 String 值的整个切片是等价的 + let word = first_word(&s); + println! ("{}", word); + + let s_string_literal = "hello word"; + + // 函数 first_word 在字符串字面值上有效,不论是部分还是整体 + let word = first_word(&s_string_literal[0..6]); + println! ("{}", word); + + let word = first_word(&s_string_literal[..]); + println! ("{}", word); + + // 由于字符串字面值已经 是 字符串切片,因此无需切片语法,这 + // 也是有效的! + let word = first_word(s_string_literal); + + println! ("{}", word); +} + +fn first_word(s: &str) -> &str { + let bytes = s.as_bytes(); + + for (i, &item) in bytes.iter().enumerate() { + if item == b' ' { + return &s[0..i]; + } + } + + &s[..] +} diff --git a/pattern_syntax_demo/Cargo.toml b/pattern_syntax_demo/Cargo.toml new file mode 100644 index 0000000..240aac9 --- /dev/null +++ b/pattern_syntax_demo/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "pattern_syntax_demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/pattern_syntax_demo/src/main.rs b/pattern_syntax_demo/src/main.rs new file mode 100644 index 0000000..7bbf2eb --- /dev/null +++ b/pattern_syntax_demo/src/main.rs @@ -0,0 +1,17 @@ +fn main() { + enum Message { + Hello { id: u32 }, + } + + let msg = Message::Hello { id: 5 }; + + match msg { + Message::Hello { + id: id_variable @ 3..=7, + } => println! ("找到位于范围内的一个 id: {}", id_variable), + Message::Hello { id: 10..=12 } => { + println! ("找到位于另一范围的一个 {}", id); + }, + Message::Hello { id } => println! ("找到别的一个 id: {}", id), + } +} diff --git a/raw_pointers/Cargo.toml b/raw_pointers/Cargo.toml new file mode 100644 index 0000000..e1b1b9e --- /dev/null +++ b/raw_pointers/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "raw_pointers" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/raw_pointers/src/main.rs b/raw_pointers/src/main.rs new file mode 100644 index 0000000..bff33b0 --- /dev/null +++ b/raw_pointers/src/main.rs @@ -0,0 +1,11 @@ +fn main() { + let mut num = 5; + + let r1 = &num as *const i32; + let r2 = &mut num as *mut i32; + + unsafe { + println! ("r1 为:{}", *r1); + println! ("r2 为:{}", *r2); + } +} diff --git a/rectangles/Cargo.toml b/rectangles/Cargo.toml new file mode 100644 index 0000000..703c9d9 --- /dev/null +++ b/rectangles/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "rectangles" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/rectangles/src/main.rs b/rectangles/src/main.rs new file mode 100644 index 0000000..a7aa76e --- /dev/null +++ b/rectangles/src/main.rs @@ -0,0 +1,52 @@ +#[derive(Debug)] +struct Rectangle { + width: u32, + height: u32, +} + +impl Rectangle { + fn area(&self) -> u32 { + self.width * self.height + } + + fn width(&self) -> bool { + self.width > 0 + } + + fn can_hold(&self, other: &Rectangle) -> bool { + (self.width > other.width && self.height > other.height) || + (self.width > other.height && self.height > other.width) + } + + fn square(size: u32) -> Rectangle { + Rectangle { + width: size, + height: size, + } + } +} + +fn main() { + let rect1 = Rectangle { + width: 30, + height: 50, + }; + + let rect2 = Rectangle { + width: 10, + height: 40, + }; + + let rect3 = Rectangle { + width: 45, + height: 25, + }; + + let sq1 = Rectangle::square(28); + let sq2 = Rectangle::square(35); + + println! ("rect1 可以装下 rect2 吗?{}", rect1.can_hold(&rect2)); + println! ("rect1 可以装下 rect3 吗?{}", rect1.can_hold(&rect3)); + println! ("rect1 可以装下 sq1 吗?{}", rect1.can_hold(&sq1)); + println! ("rect1 可以装下 sq2 吗?{}", rect1.can_hold(&sq2)); +} diff --git a/ref_cycle_demo/Cargo.toml b/ref_cycle_demo/Cargo.toml new file mode 100644 index 0000000..04d3f91 --- /dev/null +++ b/ref_cycle_demo/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "ref_cycle_demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/ref_cycle_demo/src/main.rs b/ref_cycle_demo/src/main.rs new file mode 100644 index 0000000..6fe1657 --- /dev/null +++ b/ref_cycle_demo/src/main.rs @@ -0,0 +1,42 @@ +use std::cell::RefCell; +use std::rc::Rc; +use crate::List::{Cons, Nil}; + +#[derive(Debug)] +enum List { + Cons(i32, RefCell>), + Nil, +} + +impl List { + fn tail(&self) -> Option<&RefCell>> { + match self { + Cons(_, item) => Some(item), + Nil => None, + } + } +} + +fn main() { + let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil)))); + + println! ("a 的初始 rc 计数 = {}", Rc::strong_count(&a)); + println! ("a 的下一条目 = {:?}", a.tail()); + + let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a)))); + + println! ("b 的创建后 a 的 rc 计数 = {}", Rc::strong_count(&a)); + println! ("b 的初始 rc 计数 = {}", Rc::strong_count(&b)); + println! ("b 的下一条目 = {:?}", b.tail()); + + if let Some(link) = a.tail() { + *link.borrow_mut() = Rc::clone(&b); + } + + println! ("在修改 a 之后 b 的 rc 计数 = {}", Rc::strong_count(&b)); + println! ("在修改 a 之后 a 的 rc 计数 = {}", Rc::strong_count(&a)); + + // 取消下面这行注释,就可以看到这里有着循环引用; + // 他将溢出堆栈(it will overflow the stack) + // println! ("a 的下一条目 = {:?}", a.tail()); +} diff --git a/references_demo/Cargo.toml b/references_demo/Cargo.toml new file mode 100644 index 0000000..e30e153 --- /dev/null +++ b/references_demo/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "references_demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/references_demo/src/main.rs b/references_demo/src/main.rs new file mode 100644 index 0000000..d1a400f --- /dev/null +++ b/references_demo/src/main.rs @@ -0,0 +1,11 @@ +fn main() { + let mut s = String::from("你好"); + + let r1 = &s; + { + let r2 = &mut s; + r2.push_str(",世界"); + } + + println! ("s = {}, r2 = {}", s, r1); +} diff --git a/refutable_demo/Cargo.toml b/refutable_demo/Cargo.toml new file mode 100644 index 0000000..c3c0cb6 --- /dev/null +++ b/refutable_demo/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "refutable_demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/refutable_demo/src/main.rs b/refutable_demo/src/main.rs new file mode 100644 index 0000000..8b3c86b --- /dev/null +++ b/refutable_demo/src/main.rs @@ -0,0 +1,5 @@ +fn main() { + if let x = 5 { + println! ("{}", x); + } +} diff --git a/restaurant/Cargo.toml b/restaurant/Cargo.toml new file mode 100644 index 0000000..678b365 --- /dev/null +++ b/restaurant/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "restaurant" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/restaurant/src/front_of_house.rs b/restaurant/src/front_of_house.rs new file mode 100644 index 0000000..d0a8154 --- /dev/null +++ b/restaurant/src/front_of_house.rs @@ -0,0 +1 @@ +pub mod hosting; diff --git a/restaurant/src/front_of_house/hosting.rs b/restaurant/src/front_of_house/hosting.rs new file mode 100644 index 0000000..d65f3af --- /dev/null +++ b/restaurant/src/front_of_house/hosting.rs @@ -0,0 +1 @@ +pub fn add_to_waitlist() {} diff --git a/restaurant/src/lib.rs b/restaurant/src/lib.rs new file mode 100644 index 0000000..639b689 --- /dev/null +++ b/restaurant/src/lib.rs @@ -0,0 +1,28 @@ +mod back_of_house { + pub enum Appetizer { + Soup, + Salad, + } + + pub struct Breakfast { + pub toast: String, + seasonal_fruit: String, + } + + impl Breakfast { + pub fn summer(toast: &str) -> Breakfast { + Breakfast { + toast: String::from(toast), + seasonal_fruit: String::from("peaches"), + } + } + } +} + +mod front_of_house; + +pub use crate::front_of_house::hosting; + +pub fn eat_at_restaurant() { + hosting::add_to_waitlist(); +} diff --git a/returning_closure/Cargo.toml b/returning_closure/Cargo.toml new file mode 100644 index 0000000..543adda --- /dev/null +++ b/returning_closure/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "returning_closure" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/returning_closure/src/main.rs b/returning_closure/src/main.rs new file mode 100644 index 0000000..4b6aff7 --- /dev/null +++ b/returning_closure/src/main.rs @@ -0,0 +1,7 @@ +fn returns_closure() -> Box i32> { + Box::new(|x| x + 1) +} + +fn main() { + println!("Hello, world!"); +} diff --git a/safe_abstraction/Cargo.toml b/safe_abstraction/Cargo.toml new file mode 100644 index 0000000..732d446 --- /dev/null +++ b/safe_abstraction/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "safe_abstraction" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/safe_abstraction/src/main.rs b/safe_abstraction/src/main.rs new file mode 100644 index 0000000..0c2f159 --- /dev/null +++ b/safe_abstraction/src/main.rs @@ -0,0 +1,12 @@ +#![allow(warnings)] + +fn main() { + use std::slice; + + let address = 0x01234usize; + let r = address as *mut i32; + + let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) }; + + println! ("{:?}", values); +} diff --git a/simple_blog/Cargo.lock b/simple_blog/Cargo.lock new file mode 100644 index 0000000..5b1eec6 --- /dev/null +++ b/simple_blog/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "simple_blog" +version = "0.1.0" diff --git a/simple_blog/Cargo.toml b/simple_blog/Cargo.toml new file mode 100644 index 0000000..0cae12c --- /dev/null +++ b/simple_blog/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "simple_blog" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/simple_blog/src/lib.rs b/simple_blog/src/lib.rs new file mode 100644 index 0000000..92deec1 --- /dev/null +++ b/simple_blog/src/lib.rs @@ -0,0 +1,82 @@ +#![allow(dead_code)] +#![allow(unused_variables)] + +pub struct Post { + state: Option>, + content: String, +} + +impl Post { + pub fn new() -> Post { + Post { + state: Some(Box::new(Draft {})), + content: String::new(), + } + } + + pub fn add_text(&mut self, text: &str) { + self.content.push_str(text); + } + + pub fn content(&self) -> &str { + self.state.as_ref().unwrap().content(self) + } + + pub fn request_review(&mut self) { + if let Some(s) = self.state.take() { + self.state = Some(s.request_review()) + } + } + + pub fn approve(&mut self) { + if let Some(s) = self.state.take() { + self.state = Some(s.approve()) + } + } +} + +trait State { + fn request_review(self: Box) -> Box; + fn approve(self: Box) -> Box; + fn content<'a>(&self, post: &'a Post) -> &'a str { "" } +} + +struct Draft {} + +impl State for Draft { + fn request_review(self: Box) -> Box { + Box::new(PendingReview {}) + } + + fn approve(self: Box) -> Box { + self + } +} + +struct PendingReview {} + +impl State for PendingReview { + fn request_review(self: Box) -> Box { + self + } + + fn approve(self: Box) -> Box { + Box::new(Published {}) + } +} + +struct Published {} + +impl State for Published { + fn request_review(self: Box) -> Box { + self + } + + fn approve(self: Box) -> Box { + self + } + + fn content<'a>(&self, post: &'a Post) -> &'a str { + &post.content + } +} diff --git a/simple_blog/src/main.rs b/simple_blog/src/main.rs new file mode 100644 index 0000000..3503690 --- /dev/null +++ b/simple_blog/src/main.rs @@ -0,0 +1,17 @@ +#![allow(dead_code)] +#![allow(unused_variables)] + +use simple_blog::Post; + +fn main() { + let mut post = Post::new(); + + post.add_text("今天午饭我吃了沙拉。"); + assert_eq! ("", post.content()); + + post.request_review(); + assert_eq! ("", post.content()); + + post.approve(); + assert_eq! ("今天午饭我吃了沙拉。", post.content()); +} diff --git a/simple_gui/Cargo.lock b/simple_gui/Cargo.lock new file mode 100644 index 0000000..ea8b9f4 --- /dev/null +++ b/simple_gui/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "simple_gui" +version = "0.1.0" diff --git a/simple_gui/Cargo.toml b/simple_gui/Cargo.toml new file mode 100644 index 0000000..0de16ee --- /dev/null +++ b/simple_gui/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "simple_gui" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/simple_gui/src/lib.rs b/simple_gui/src/lib.rs new file mode 100644 index 0000000..f38f5b4 --- /dev/null +++ b/simple_gui/src/lib.rs @@ -0,0 +1,31 @@ +#![allow(dead_code)] +#![allow(unused_variables)] + +pub trait Draw { + fn draw(&self); +} + +pub struct Screen { + pub components: Vec>, +} + +impl Screen { + pub fn run(&self) { + for component in self.components.iter() { + component.draw(); + } + } +} + +pub struct Button { + pub width: u32, + pub height: u32, + pub label: String, +} + +impl Draw for Button { + fn draw(&self) { + // 具体绘制按钮的代码 + println! ("这是一个大小:{} 像素 x {} 像素,有着 “{}” 标签的按钮;", self.width, self.height, self.label); + } +} diff --git a/simple_gui/src/main.rs b/simple_gui/src/main.rs new file mode 100644 index 0000000..15e9d0a --- /dev/null +++ b/simple_gui/src/main.rs @@ -0,0 +1,27 @@ +#![allow(dead_code)] +#![allow(unused_variables)] + +use simple_gui::Draw; + +pub struct SelectBox { + width: u32, + height: u32, + options: Vec, +} + +impl Draw for SelectBox { + fn draw(&self) { + // 具体绘制复选框的代码 + println! ("这是一个大小为:{} 像素 x {} 像素,有着选项:{:?} 的复选框;", self.width, self.height, self.options); + } +} + +use simple_gui::Screen; + +pub fn main() { + let screen = Screen { + components: vec! [Box::new(String::from("你好"))], + }; + + screen.run(); +} diff --git a/sp_demos/Cargo.toml b/sp_demos/Cargo.toml new file mode 100644 index 0000000..e89ee27 --- /dev/null +++ b/sp_demos/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "sp_demos" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/sp_demos/src/main.rs b/sp_demos/src/main.rs new file mode 100644 index 0000000..8f48d41 --- /dev/null +++ b/sp_demos/src/main.rs @@ -0,0 +1,4 @@ +fn main() { + let x = 5; + let y = &mut x; +} diff --git a/src/Ch00_Forword_and_Introduction.md b/src/Ch00_Forword_and_Introduction.md new file mode 100644 index 0000000..58c1317 --- /dev/null +++ b/src/Ch00_Forword_and_Introduction.md @@ -0,0 +1,100 @@ +# 前言和简介 + +虽然这样说有些含糊其辞,但基本上可说 Rust 编程语言,是一种 *赋能(empowerment)*:不管你当前在用哪种语言编写代码,Rust 都可以赋予你更大能力,在编程之路上走得更远,在你先前的各种领域,更具信心地编写程序。 + +就拿涉及到底层的内存管理、数据表示及并发等 “系统层面” 的工作来讲。传统上,这类编程都被认为是深奥的、只由极少数花费多年时间掌握了避开其间臭名昭著陷阱的程序员来完成。而即使这些人,在写这类代码时,仍然是小心翼翼,以免他们写出的代码留下漏洞利用、崩溃或不当。 + +通过消除原有各种陷阱,以及提供到友好、全新工具集,Rust 破除了编写这类苛刻程序中的诸多障碍。那么那些需要“深入”到底层控制的程序员们,现在就可以运用 Rust,在不必承担一直以来的崩溃或者安全漏洞的情况下,同时还无须去深入细致地掌握那变化无常工具链,就可以达成他们的目标了。更了不起的是,在设计这门语言时,就贯彻了引导使用他的程序员编写出可靠又高效的代码,体现在运行速度及内存使用上。 + +正在进行底层代码编写的程序员们,可运用 Rust 来提升他们的雄心壮志。比如说,在Rust 中引入并行机制,是相对低风险的操作:编译器会为你捕获到那些经典错误。同时还可以在确信不会带来程序崩溃或漏洞利用之下,大胆进行更多的优化。 + +然而 Rust 并非局限于底层系统的编写。对于构造命令行应用、web 服务器及其他类别的代码来说,Rust 的表现力和人机工程设计,也可以让这些编写起来相当愉悦 -- 在本书后面,就会发现这样的示例。运用 Rust 可实现将一个领域中学到的技能,迁移到另一领域;通过编写 web 应用,就可以掌握 Rust, 然后将这同样的技能应用到树梅派 app 的编写上。 + +本书充分接纳到 Rust 给其使用者赋能的潜力。这本书友好而恰当,试图帮助你不光提升有关Rust的知识,还在一般方面提升你编程的水平和信心。那么就请继续阅读下去,欢迎来到 Rust 社区! + +-- *Nicholas Matsakis 与 Aaron Turon* + +## 简介 + +欢迎来到 *Rust 编程语言*,一本 Rust 的介绍性书籍。Rust 编程语言帮助更快地编写出更可靠软件。在程序语言设计中,上层人机交互与底层控制,通常是不可调和的;Rust 挑战了这对矛盾。经由强力的技术能力与了不起的开发者体验,Rust 带来了对底层细节(譬如内存的使用)控制的同时,免去了传统上底层控制带来的一大堆麻烦。 + +## Rust 适用于哪些人群 + +对于相当多的人群,Rust 因为各种原因,都是理想选择。下面就来看看那些最重要群体中的一些人的情况。 + +### 开发者团队 + +对于有着不同水平系统编程知识的开发者团队的协同来讲,Rust 正被证明是一种生产力工具。底层代码倾向于出现各种细微错误,这样的细微错误,对于其他编程语言,则只能经由广泛测试,和经验老道的开发者细致代码评审才能捕获到。而在 Rust 中,编译器通过拒绝编译这些难以捉摸的错误,包括并发错误,而扮演着看门人角色。通过与编译器一道工作,团队就可以将他们的时间,集中用在程序逻辑,而不是找寻错误上。 + +Rust 还带给了系统编程世界,一些现代开发者工具: + +- `Cargo`,Rust 所包含的依赖管理器与构建工具,让整个 Rust 生态中添加依赖、编译与管理依赖,变得愉快并具一致性(`Cargo`, the included dependency manager and build tool, makes adding, compiling, and managing dependecies painless and consistant across the Rust ecosystem); +- `Rustfmt` 确保了不同开发者之间有着一致的编码风格; +- Rust 语言服务器驱动了用于代码补全与行内错误消息的集成开发环境。 + +通过使用这些开发者工具,及其他一些 Rust 生态中的工具,开发者就可以在编写系统级代码时,颇具生产力了。 + +### 学生 + +Rust 是为学生及那些对掌握系统概念感兴趣的人所准备的。运用 Rust,许多人都掌握了像是操作系统开发这样的知识点。Rust 社区非常欢迎并乐于回答学生们提出的问题。通过像是本书这样的努力,Rust 团队本身是要让更多人,尤其是那些刚开始编程的人们,可获取到系统概念。 + + +### 商业公司 + +已有上千家规模或大或小的商业公司,在生产中,为着不同任务使用着 Rust。这些任务包括了命令行工具、web 服务、运维工具、嵌入式装置、音视频分析与转码、加密货币、生物信息学、搜索引擎、物联网应用、机器学习,甚至Firefox web浏览器的主要部分等等。 + +### 开放源代码开发者 + +Rust 是为着那些想要构建 Rust 编程语言本身、Rust 社区、Rust 开发者工具和库而准备的。我们希望你为 Rust 语言做出贡献。 + +### 看重运行速度与稳定性的人们 + +Rust 是为那些渴求某门语言所提供速度与稳定性的人们准备的。这里说的运行速度,指的是使用 Rust 可创建出程序的运行速度,以及 Rust 所能达到的编写这些程序速度。Rust 编译器的检查,确保了功能补充与重构的稳定性。这稳定性是与那些不具备这些检查语言中的脆弱老旧代码相比得出的,开发者通常害怕去修改那些脆弱老旧代码。通过争取实现零代价的抽象,就有了那些在手动编写时,就立即编译到底层代码的上层特性,Rust 致力于实现在构造安全代码的同时,还取得了快速的代码编写与程序运行。 + +Rust 语言也希望带给众多其他用户以支持;这里提到的只是一些最大的相关群体。总体来讲,Rust 最伟大的抱负,是要消除程序员们在过去数十年来,业已被迫接受的在安全性与生产力、开发和运行速度及人机交互上的妥协。请给 Rust 一个机会,然后看看 Rust 的选择是否适合于你。 + +## 本书读者群体 + +本书假定你曾编写过其他某种编程语言的代码,至于何种编程语言并不重要。本书作者已尽力让其中的教学材料适合到有着宽泛编程背景的读者。这里不会花大量时间来解释编程为何物,以及该怎么来看待编程。若对编程一窍不通,那么最好找一本编程入门的书先看看。 + +## 怎样使用本书 + +大体上,本书假定是要以从前往后的顺序进行阅读。后续章节是建立在较早章节的概念之上,而较早的章节不会深入某个话题的细节;在后续章节通常会回顾到那个话题。 + +本书有两种章节:概念性章节与项目性章节。在概念章节,将对 Rust 某个方面的有所了解。而在项目性章节,就会构建出一些在一起的小程序,这些小程序运用了概念性章节中学到的东西。第 2、12 和 20 章,就是项目性章节;而剩下的,全都是概念性章节。 + +第 1 章讲了怎样安装 Rust、怎样编写出 “Hello, world!” 程序,还有怎样使用 Rust 的包管理器及构建工具 `Cargo`。第 2 章是 Rust 语言的一个实操介绍。这里涵盖了上层的一些概念,而在后续章节则会提供到进一步的细节。若要立即动手编写代码,那么第 2 章就可以开始了。一开始你或许想要跳过第 3 章,这一章涵盖了与那些其他编程语言类似的一些 Rust 特性,而要直接前往到第 4 章,去了解 Rust 的所有权系统。不过若要细致了解 Rust,就应详细掌握 Rust 的每个细节设计之后,在移步到下一章节,或许也会跳过第 2 章直接到第 3 章,然后在想要将学到的细节知识应用到项目时,再回到第 2 章。 + +第 5 章讨论了结构体和方法,同时第 6 章涵盖了枚举、`match` 表达式,和 `if let` 控制流结构。在构造 Rust 中的定制类型时,就会用到这些结构体和枚举。 + +第 7 章将了解到 Rust 的模组系统,以及代码组织的隐私规则,还有 Rust 的公共应用编程接口(Application Programming Interface, API)。第 8 章讨论了一些常用的、由标准库提供的集合数据结构,诸如矢量、字符串及哈希图。第 9 章探索了 Rust 的错误处理思想与技巧。 + +第 10 章涉及了范型、特质(traits) 与生命周期,他们赋予了定义出应用多种类型代码的能力。第 11 章全都是关于测试的内容,即便有着 Rust 的安全性保证,对于确保程序逻辑正确,测试仍是不可缺少的。第 12 章将构建一个我们自己的、用于在文件中搜索文本的 `grep` 命令行工具功能子集的版本。到这里,就会用到先前章节中所讨论的众多概念了。 + +第 13 章对闭包(closures)和迭代进行了探索:闭包和迭代属于 Rust 来自函数式编程语言的特性。第 14 章,将更深入地对 `Cargo` 加以检视,并就如何与他人分享库的最佳实践进行探讨。第 15 章讨论标准库提供的一些灵巧指针,还有实现不同功能的 Rust 特质(traits)。 + +第 16 章,将遍数并发编程的各种不同模型,并探讨 Rust 如何对大胆进行多线程编程的帮助。第 17 章将或许你所熟知面向对象编程的那些原则,与 Rust 下编程的习惯加以对比。 + +第 18 章是模式与模式匹配的一个参考,这在 Rust 程序中,属于是概念表达的强大方式。第 19 章包含了一个诸多感兴趣的话题大杂烩,包括不安全的 Rust、宏,以及更多有关生命周期、特质(traits)、类型、函数与闭包等等的细节。 + +第 20 章,将完成一个其中实现了底层线程化的 web 服务器! + +最后,还有一些包含了这门语言的有用信息的附录,这些附录则更多的像是参考的形式。附录 A 涵盖了 Rust 的关键字,附录 B 涵盖了 Rust 的运算符和符号,附录 C 涵盖了由标准库所提供的那些派生特质(derivable traits),附录 D 涵盖了一些有用的开发工具,还有附录 E 对 Rust 版本进行了解释。 + +阅读本书并无定法:你要跳着去读,也是可以的!在遇到疑惑时,或许就不得不跳回去看看了。你只要怎么有效就行了。 + +掌握 Rust 过程中的一个重要部分,就是要学会怎样去读那些编译器给出的错误消息:这些错误消息将引导你得到运作的代码。由此,本书将提供到许多不编译的示例,以及在各种情况下编译器将给出的错误消息。请知悉在进入到某个随机示例并加以运行时,示例代码可能会不编译!请确保要阅读这些示例周围的文字,来了解正尝试运行的示例,是不是有错误。本书中的虚拟人物 `Ferris` 也会帮助你识别代码是否会工作的: + +| Ferris | 意义 | +| :-: | :- | +| ![不会编译](images/Ch00_01.svg) | 此代码不会编译! | +| ![不会运行](images/Ch00_02.svg) | 此代码不会运行! | +| ![不会产生期望的行为](images/Ch00_03.svg) | 此代码不会产生出期望的行为。 | + +*表 1 - Ferris 表情含义* + +多数情况下,这里都会给出不会编译代码的正确版本。 + +## 本书的源码 + +本书所产生的源码,可在 [Github: gnu4cn/rust-lang](https://github.com/gnu4cn/rust-lang) 下载到。 diff --git a/src/Ch01_Getting_Started.md b/src/Ch01_Getting_Started.md new file mode 100644 index 0000000..c5188d8 --- /dev/null +++ b/src/Ch01_Getting_Started.md @@ -0,0 +1,394 @@ +# 入门 + +现在就开始 Rust 之旅!有很多要掌握的东西,不过千里之行,始于足下。本章将讨论: + +- 在 Linux、macOS 及 Windows 上安装 Rust; +- 编写一个打印出 `Hello, world!` 的程序来; +- Rust 的包管理器和构建系统 Cargo 的使用。 + + +## 安装 + +第一步即是安装 Rust。这里将通过 `rustup` 这个用于管理 Rust 版本及相关工具的命令行工具,来下载 Rust。要下载 Rust,就需要互联网连接。 + +> 注意:若由于某些原因而不愿使用 `rustup`,那么请参考 [其他 Rust 安装方式页面](https://forge.rust-lang.org/infra/other-installation-methods.html) 了解更多选项。 + +接下来就是要按照最新的稳定版 Rust 编译器。Rust 的稳定性保证了本书中所有示例都将在较新的 Rust 版本下可持续编译。由于 Rust 经常会改进错误消息和告警,因此在不同版本之间,输出可能会略有不同。也就是说,任何使用以下步骤所安装的较新、稳定版 Rust,都将如本书内容中所期望的那样工作。 + +> 关于**命令行注释** +> 在本章及全书中,都会给出一些在终端中用到的命令。他们是一些应在以 `$` 开始的终端中输入的行。至于这个 `$` 字符,是无需输入的;这个字符表示每条命令的开头。那些不以 `$` 开头的行,通常给出的是上一命令的输出。此外,那些特定于 `PowerShell` 的示例中,将使用 `>` 而不是 `$`。 + + +### 在 Linux 与 macOS 上安装 `rustup` + +若使用的是 Linux 或 macOS,那么请打开一个终端,然后输入下面的命令: + +```console +$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh +``` + +此命令会下载一个脚本并开始 `rustup` 工具的安装,而 `rustup` 将安装最新的稳定版 Rust。可能会提示输入 `sudo` 密码。在安装成功后,就会出现下面这行! + +```console +Rust is isntalled now. Great! +``` + +这里还将需要一个连接器(linker),这是个Rust要用来将其编译好的输出,组合起来形成一个文件的程序。似乎你的电脑上以及有了一个这样的连接器了。若收到连接器错误信息,那么就应安装一个 C 语言编译器,C 编译器通常会包含着连接器的。由于一些常用 Rust 包对 C 代码有依赖且需要 C 编译器,因此 C 编译器也是有用的。 + +在 macOS 上,可通过运行下面的命令,获取到一个 C 编译器: + +```console +$ xcode-select --install +``` + +Linux 用户一般都会安装 GCC 或 Clang,至于具体哪种 C 编译器,则是依据他们所用 Linux 分发版本的文档可以确定。比如若使用的是 Ubuntu,那么就可以安装 `build-essential` 软件包。 + + +### 在 Windows 上安装 `rustup` + +在 Windows 上,请前往 [https://www.rust-lang.org/tools/install](https://www.rust-lang.org/tools/install) 页面,并按照安装 Rust 的指令进行安装。在安装过程的某个时刻,将收到为何需要 Visual Studio 2013 或更新版本的 C++ 构建工具的说明。而最简单的获取到构建工具的方法,则是安装 [Visual Studio 2019 构建工具](https://visualstudio.microsoft.com/visual-cpp-build-tools/)。在询问将要安装何种工作负载(workloads)时,请确保 `C++ build tolls` 被选中,还要确保包含 Windows 10 SDK 及英语语言包。 + +本书接下来用到的命令,在 `cmd.exe` 与 `PowerShell` 中都可工作。若其中有特定区别,本书将会解释要用哪个。 + +## 更新与卸载 + +在通过 `rustup` 安装了 Rust 后,更新到最新版本就容易了。在 `shell` 中运行下面的更新脚本: + +```console +$ rustup update +``` + +而要卸载 Rust 和 `rustup`,只需在 `shell` 中运行下面的卸载脚本: + +```java +$ rustup self uninstall +``` + +## 故障排除 + +要检查当前是否安装了 Rust, 请开启一个 `shell` 并敲入这行命令: + +```console +$ rustc --version +``` + +就会看到版本编号、合并哈希(`commit` hash),以及已发布的该最新稳定版本合并日期,以下面这种格式: + +```console +rustc x.y.z (abcabcadc yyyy-mm-dd) +``` + +若看到这个信息,那么就已成功安装了 Rust!若看不到这个信息,且是在 Windows 上,那么就请在 `%PATH%` 系统变量中检查一下 Rust 在不在里面。若那一点问题都没有而 Rust 仍就不工作,那么可在数个地方需求帮助。其中最便利的就是 [Rust 官方 Discord](https://discord.gg/rust-lang) 上的 `#beginners` 频道了。在那里可与其他 Rust 公民(一种无厘头的自我称呼)聊天,他们可以帮助到你。其他不错的资源包括 [用户论坛](https://users.rust-lang.org/) 和 [Stack Overflow](https://stackoverflow.com/questions/tagged/rust)。 + +## 本地文档 + +Rust 的安装,也包含了一份本地文档,因此可离线阅读到这本地文档。运行 `rustup doc` 即可在浏览器中打开这本地文档。 + +在任何时候遇到标准库所提供的类型或函数,而又确定他做些什么或该怎样使用这类型或函数时,就可以使用 API 文档来搞明白他是怎么回事! + +## `Hello, World!` + +既然已经安装好了 Rust, 那么就来编写第一个 Rust 程序吧。在掌握一门新语言时,传统就是要编写一个小的、打印出文字 `Hello, World!` 到屏幕上的程序,因此这里也会干这同样的事情! + +> 注意:本书假定读者对命令行有着基本的熟悉。Rust 对代码在何处编辑和使用何种工具编辑没有特别要求,因此若优先选择某种集成开发环境,而非命令行,那么使用喜好的 IDE 即可。许多 IDE 都有某种程度的 Rust 支持;请查看 IDE 文档了解有关细节信息。近来,Rust 团队已着手启动良好的IDE支持,且此方面已取得极大进展! + +### 创建一个项目目录 + +这里是以构造一个保存 Rust 代码的目录开始的。对于 Rust 来说,代码位居何处并不重要,不过对于本书中的练习与项目,是建议在主目录下构造一个 `projects` 目录,并把全部项目放在那里的。 + +请打开一个终端,并输入下面的这些命令来构造一个 `projects` 的目录,和一个在 `projects` 下用于 "Hello, World!" 项目的目录。 + +对于 Linux、macOS 和 Windows 上的 `PowerShell`, 请输入: + +```console +$ mkdir ~/rust-lang/projects +$ cd ~/rust-lang/projects +$ mkdir hello_world +$ cd hello_world +``` + +而对于 Windows 的 CMD, 请输入: + +```console +> mkdir "%USERPROFILE%\rust-lang\projects" +> cd /d "%USERPROFILE%\rust-lang\projects" +> mkdir hello_world +> cd hello_world +``` + +### 编写及运行 Rust 程序 + +接下来,就要构造一个源代码文件,并命名为 `main.rs`。Rust 文件总是以 `.rs` 扩展名结束。若要在文件名中是一多个单词,那么请使用下划线来将这些单词隔开。比如,请使用 `hello_world.rs` 而不是 `helloworld.rs`。 + +现在就要打开这个刚创建出的 `main.rs` 文件,并敲入清单 1-1 中的代码。 + +文件名:`main.rs` + +```rust +fn main() { + println!("Hello, World!"); +} +``` + +*清单 1-1:打印`Hello, World!` 的程序* + +保存这个文件并回到终端窗口。在 Linux 或 macOS 上,请输入下面的命令来编译和运行这个文件: + +```console +$ rustc main.rs +$ ./main +Hello, World! +``` + +在 Windows 上,就要输入命令 `.\main.exe` 而不是 `./main`: + +```console +> rustc main.rs +> .\main.exe +Hello, World! +``` + +而不论所在操作系统为何,字符串 `Hello, World!` 都应打印到终端。而若没有看到这个输出,那么请回到安装小节的 [“故障排除”](#troubleshooting) 部分获取帮助。 + +如确实打印出了 `Hello, World!`,那么恭喜你!你已正式编写除了一个 Rust 程序了。那就让你成为了一名 Rust 程序员了 -- 欢迎! + +### Rust 程序解析 + +来仔细回顾一下刚才在 “Hello World!” 程序中发生了什么。这是谜团中第一部分: + +```rust +fn main() { + +} +``` + +这些行定义了 Rust 中的一个函数。这个 `main` 函数比较特殊:在每个可执行的 Rust 程序中,他总是第一个开始运行的代码。这第一行声明了一个名为 `main` 的、没有参数且不返回任何值的参数。若函数有参数,那么参数就应位处圆括号`()`内部。 + +还有就是,请注意函数体是包裹在花括号`{}`中的。Rust 要求将全部函数体都用花括号包裹起来。将开头的花括号与函数声明放在同一行,并在二者之间加上一个空格,是良好的代码风格。 + +若想要在多个 Rust 项目之间保持一种标准的编码风格,那么就可以使用一个名为 `rustfmt` 的自动格式化工具,来以一种特定样式对代码进行格式化。与 `rustc` 一样,Rust 团队已将此工具包含在标准的 Rust 发布中,因此在你的电脑上就应该已经有了这个格式化工具了!请查看在线文档了解更多详情。 + +在这个`main` 函数里头,是下面的代码: + +```rust +println!("Hello, World!"); +``` + +这行代码完成了此小程序的全部工作:他将文字打印到屏幕。这里有四个需要注意的重要细节。 + +首先,Rust 编码风格是缩进四个空格,而非一个制表符; + +其次,`println!` 调用了一个 Rust 的宏(a Rust macro)。若他调用的是个函数,那么就应输入 `println` (是不带 `!` 的)。在后续的第 19 章,将详细讨论 Rust 的宏。而现在,则只需知道 `!` 的使用表示是在调用某个宏而不是普通函数,同时宏不会总是遵循与函数同样的规则; + +第三,就是看到的 `Hello, World!` 这个字符串了。这里时将此字符串作为参数,传递给 `println!` 的,且这个字符串是被打印到屏幕上的; + +最后,这行语句是以分号(`;`)结束的,这表示该表达式结束,同时下一表达式已准备好开始。Rust 代码的多数行,都是以分号结束的。 + + +### 编译和运行是分开的步骤 + +这里刚刚运行了一个新近创建出的程序,那么来检视一下该过程的每个步骤。 + +在运行某个 Rust 程序之前,必须要通过敲入 `rustc` 命令并将源代码文件名字,作为`rustc`的参数加以传入,这样来使用 Rust 编译器对其进行编译,像下面这样: + +```console +$ rustc main.rs +``` + +若你有 C 或 C++ 的背景知识,那么就会注意到这与 `gcc` 或 `clang` 类似。在成功编译后,Rust 就会输出一个二进制可执行文件。 + +在 Linux、macOS 和 Windows 上的 PowerShell 之上,就可以通过在 `shell` 中敲入 `ls` 看到这个可执行文件。在 Linux 与 macOS 上,将看到下面这两个文件。而在 Windows 上的 PowerShell 中,则会看到与使用 CMD 一样的以下三个文件。 + +```console +$ ls +main main.rs +``` + +在 Windows 的 CMD 中,就应输入下面的东西: + +```console +> dir /B %= 这里的 /B 选项表示只显示文件名 =% +main.exe +main.pdb +main.rs +``` + +这显示了带有 `.rs` 扩展名的源代码文件、那个可执行文件(Windows 上的 `main.exe`,对于其他平台则是 `main`),以及,在使用 Windows 时,一个包含了调试信息的、带有 `.pdb` 扩展名的文件。从此处,就像下面这样来运行这里的 `main` 或 `main.exe`: + +```console +$ ./main # 或在 Windows 上的 .\main.exe +``` + +若这里的 `main.rs` 就是那个 “Hello, World!” 程序,那么这行命令就会将 `Hello, World!` 打印到你的终端了。 + +若你对某门动态语言,诸如 Ruby、Python 或者 JavaScript 更为熟悉,那么可能就不习惯于将编译和运行某个程序作为分开的步骤。Rust 是门 *提前编译* 语言(an *ahead-of-time compiled* language),这意味着可对程序进行编译,而将可执行文件交给他人,他们可在未安装 Rust 的情况下运行编译好的可执行文件。而若将某个 `.rb`、`.py`,或者 `.js` 文件交给某人时,他们就需要安装好相应的 Ruby、Python 或 JavaScript 实现。不过在这些语言中,仅需一个命令来编译和运行他们的程序。在编程语言设计中,每件事都有所取舍。 + +对于简单的程序来说,用 `rustc` 编译就足够了,但随着项目的成长,就希望对所有选项进行管理,并令到代码分享更为简便。接下来,就要介绍 Cargo 工具了,这工具将帮助我们编写出实用的 Rust 程序。 + + +## 你好,Cargo! + +Cargo 是 Rust 的构建系统和包管理器。由于 Cargo 处理了很多任务,诸如构建代码、下载代码所依赖的库,以及这些库的构建等等,因此绝大多数 Rust 公民都使用这个工具,来管理他们的 Rust 项目。(我们把这些库叫做代码需要依赖(we call the libraries that your code needs *dependencies*)。) + +对于最简单的那些 Rust 程序,比如才写的那个,是没有任何依赖的。因此若使用 Cargo 来构建这个 `Hello, World!` 项目,那么就只会用到 Cargo 处理代码构建的部分。而随着更为复杂 Rust 程序的编写,就会添加依赖,而在开始一个用到 Cargo 的项目时,完成依赖添加就会容易得多。 + +由于广大 Rust 项目都用到了 Cargo,本书其余部分就假定了也使用 Cargo。若使用了在 [安装](#installation) 小节中提到的官方安装器进行的 Rust 安装,那么Cargo就已与 Rust 一起安装好了。而若是以其他方式安装的 Rust,那么就要通过在终端中敲入下面的命令,来检查 Cargo 是否已安装妥当: + +```console +$ cargo --version +``` + +若能看到版本号,那么就有了这个工具!而若看到错误,诸如 `command not found`,就请查看你的安装方式的文档,找到怎样单独安装 Cargo 的方法。 + + +### 使用 Cargo 创建项目 + +下面来使用 Cargo 创建一个新项目,并看看与原先的 “Hello, World!” 项目有何不同。现在导航至 `projects` 目录(或确定下来的保存代码的其他地方)。然后不论在那个操作系统之上,运行下面的命令: + +```console +$ cargo new hello_cargo +$ cd hello_cargo +``` + +这第一个命令创建出了一个新的名为 `hello_cargo` 目录。这里就已将项目命名为了 `hello_cargo`,然后 Cargo 将其文件创建在了同名的目录里面。 + +进入到 `hello_cargo` 目录并列出那些文件。就会看到 Cargo 已经为我们生成了两个文件和一个目录:一个 `Cargo.toml`文件与一个里头有着 `main.rs` 文件的 `src` 目录。 + +`cargo new` 还初始化了一个新的、带有 `.gitignore` 文件的 Git 代码仓库。若是在一个既有的 Git 代码仓库运行的 `cargo new`,那么就不会生成那些 Git 文件;通过运用 `cargo new --vcs=git` 可重写此行为。 + +> 注意:Git 是种常用的版本控制系统。可通过上面的 `--vcs` 命令行参数,让 `cargo new` 使用其他版本控制系统或不使用版本控制系统。请运行 `cargo new --help`命令来查看所有可用选项。 + + +文件名:`Cargo.toml` + +```toml +[package] +name = "hello_cargo" +version = "0.1.0" +edition = '2021' + +[dependencies] +``` + +*清单 1-2:由 `cargo new` 所生成的 `Cargo.toml` 的内容* + +该文件是 [TOML](https://toml.io/) ( *Tom's Obvious, Minimal Language* ) 格式的,这是 Cargo 的配置格式。 + +该文件的第一行, `[package]`,是个小节标题,表示接下来的语句是在对一个包进行配置。随着往这个文件添加越来越多的信息,就会添加其他小节。 + +接下来的三行,对 Cargo 用于编译程序所需的信息进行了配置:项目名称、版本号及要使用的 Rust 版本。在 [附录 E](Appendix_E.md) 中会讲到这个 `edition` 关键字。 + +`Cargo.toml` 的最后一行,`[dependencies]`,是要列出项目全部依赖小节开始的地方。在 Rust 中,代码包被称为 *包裹(crates)*。此项目无需任何其他包裹,在第 2 章中的头一个项目,就会用到依赖包裹,因此在那时就会用到这个依赖小节。 + +现在打开 `src/main.rs` 然后看看: + +文件名:`src/main.rs` + +```rust +fn main() { + println! ("Hello, World!"); +} +``` + +Cargo 以及为我们生成了一个 “Hello, World!” 的程序,这个自动生成的程序就跟之前在清单 1-1 中的一样!到现在,先前的项目与这个 Cargo 生成的项目的不同之处,就是 Cargo 是将代码放在那个 `src` 目录中的,同时在顶层目录还有了一个 `Cargo.toml` 配置文件。 + +Cargo 希望那些源代码文件,存留在 `src` 目录里头。而顶层的项目目录,只用于 `README` 文件、许可证信息、配置文件及其他与代码无关的东西。使用 Cargo 有助于对项目的组织。一切都有了个地方,且一切都在各自的地方(there's a place for everything, and everything is in its place)。 + +若没有使用 Cargo 来开始项目,就如同先前在 “Hello, World!” 项目中所做那样,那么仍旧可使用 Cargo 将其转换为一个项目。将项目代码移入到 `src` 目录并创建出一个适当的 `Cargo.toml` 文件来: + +```console +$ cd hello_world +$ mkdir src +$ mv main.rs src/ +$ cargo init +``` + +### 构建和运行一个 Cargo 项目 + +现在来看看在使用 Cargo 来构建和运行那个 “Hello, World!” 程序有什么不同之处!在 `hello_cargo` 目录,通过敲入下面的命令,来构建该项目: + +```console +$ cargo build  ✔ + Compiling hello_cargo v0.1.0 (/home/peng/rust-lang/projects/hello_cargo) + Finished dev [unoptimized + debuginfo] target(s) in 0.45s +``` + +此命令创建出在 `target/debug/hello_cargo`(或 Windows 上的`target\debug\hello_cargo.exe`)中,而非当前目录下的一个可执行文件。可使用下面这个命令运行那个可执行程序: + +```console +$ ./target/debug/hello_cargo # 或者在 Windows 上的 .\target\debug\hello_cargo.exe +Hello, world! +``` + +若一切顺利,那么 `Hello, World!` 就会被打印到终端。首次运行 `cargo build`,还会造成 Cargo 在顶层目录创建一个新文件:`Cargo.lock`。该文件会跟踪项目中各个依赖的精确版本。由于这个项目没有依赖,因此该文件有些稀疏。绝无必要手动修改此文件;Cargo 会为我们管理他的内容。 + +文件名:`Cargo.lock` + +```toml +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "hello_cargo" +version = "0.1.0" +``` + +*清单 1-3, `Cargo.lock`* + +这里刚刚使用 `cargo build` 构建了一个项目,并用 `./target/debug/hello_cargo` 运行了这个项目,不过这里还可以将代码编译和运行编译结果,全部在一个命令,`cargo run`,中完成: + +```console +$ cargo run + Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs + Running `target/debug/hello_cargo` +Hello, World! +``` + +请注意这次并未见到表示 Cargo 曾在编译 `hello_cargo` 的输出。Cargo 发现这些文件并未发生改变,因此他就运行了那个二进制文件。若曾修改过源代码,那么 Cargo 就会在运行这个项目之前,重新构建该项目,从而会看到这样的输出: + +```console +$ cargo run  ✔ + Compiling hello_cargo v0.1.0 (/home/peng/rust-lang/projects/hello_cargo) + Finished dev [unoptimized + debuginfo] target(s) in 0.43s + Running `target/debug/hello_cargo` +Hello, Cargo! +``` + +Cargo 还提供了一个叫做 `cargo check` 的命令。此命令会对代码进行快速检查,以确保代码可被编译,但该命令不会产生出可执行程序: + +```console +$ cargo check  ✔ + Checking hello_cargo v0.1.0 (/home/peng/rust-lang/projects/hello_cargo) + Finished dev [unoptimized + debuginfo] target(s) in 0.35s +``` + +这里为何不要一个可执行文件呢?通常,由于 `cargo check` 跳过了产生出可执行程序的步骤,因此他要比 `cargo build` 快得多。但在编写代码时,要持续检查已完成的工作时,那么 `cargo check` 的使用,就会加速工作流程!由于这个原因,许多的 Rust 公民,都会在编写他们的程序时,定期运行 `cargo check`,来确保程序通过编译。而在准备好使用可执行文件的时候,在运行 `cargo build`。 + +来概括一下到现在,已经掌握的有关 Cargo 的内容: + +- 使用 `cargo new` 就可以创建出项目; +- 使用 `cargo build` 就可以构建出项目; +- 使用 `cargo run` 就可以一步完成项目的构建和运行; +- 使用 `cargo check`就可以在不产生出二进制程序的情况下,对项目加以构建以进行错误检查; +- Cargo 是将构建结果保存在 `target/debug` 目录,而不是保存在与源代码同样的目录。 + +使用 Cargo 的一个额外优势,就是不论是在何种操作系统上工作,那些命令都是同样的。基于这个原因,本书后续就不再提供针对 Linux 与 macOS,以及Windows 的特别说明了。 + +### 发布目的的构建 + +在项目最终准备好发布时,就可以使用 `cargo build --release` 来带优化地对其进行编译了。该命令将创建出一个位于 `target/release`,而非 `target/debug` 中的可执行文件。其中的那些优化,会令到项目的 Rust 代码运行得更快,不过开启这些优化,将增加程序编译的时间。这就是为什么有两种不同配置文件的原因:一个配置是为开发目的,在希望快速且频繁地对项目进行重新构建时使用的配置,而另一个,则是为构建要给到用户的、不会反复重新构建的、将尽可能快速运行的最终程序所用到的配置。在要对程序进行性能测试时,就一定要运行 `cargo build --release`,并对 `target/release` 中的可执行程序进行性能测试。 + +### 约定俗成的 Cargo + +对于那些简单项目,相比于使用 `rustc`,Cargo 并未提供到很多价值,然而在程序变得愈加错综复杂时,他就会证明他的价值了。对于那些由多个代码箱(crates) 构成的复杂项目,让 Cargo 来对构建进行协调,就要容易得多。 + +即使这个`hello_cargo` 项目如此,此刻也用到了将在接下来的 Rust 编程生涯中会用到的真正工具。事实上,对于在任何既有的 Rust 项目,都应使用下面这些命令,使用 Git 来检出代码,然后前往到项目目录,进而加以构建: + +```console +$ git clone example.org/someproject +$ cd someproject +$ cargo build +``` + +更多有关 Cargo 的信息,请查看看[Cargo 文档](https://doc.rust-lang.org/cargo/)。 diff --git a/src/Ch02_Programming_a_Guessing_Game.md b/src/Ch02_Programming_a_Guessing_Game.md new file mode 100644 index 0000000..22f98db --- /dev/null +++ b/src/Ch02_Programming_a_Guessing_Game.md @@ -0,0 +1,735 @@ +# 编写一个猜谜游戏 + +现在就要通过一个动手项目,投身到 Rust 中!本章将通过给出如何在真实程序中使用几个常见 Rust 概念,而对他们进行介绍。将了解有关 `let`、`match` 关键字,方法、关联函数(associated functions)、使用外部 Rust 代码盒子(Rust crates),及更多的 Rust 概念!而在接下来的章节,就会对这些概念进行深入探索。在本章中,将会对基础知识进行实操。 + +这里将实现一个经典的新手编程问题:猜谜游戏。他的工作运作机制为:程序将生成一个 1 到 100 之间的随机整数。随后将提示玩家输入猜测的数字。在玩家输入后,程序将表明猜测的数字是小了还是大了。在猜到正确的数字时,游戏就会打印出一条祝贺消息并推出。 + +## 建立一个新项目 + +要建立新项目,就要前往第一章所创建出的 `projects` 目录,并使用 Cargo 构造一个新项目,像下面这样: + +```console +$ cargo new guessing_game +$ cd guessing_game +``` + +第一条命令,`cargo new`,取了项目名称(`guessing_game`)作第一个参数。而第二条命令则是前往到这个新项目的目录下。 + +来看看这个生成的 `Cargo.toml` 文件: + +文件名:`Cargo.toml` + +```toml +[package] +name = "guessing_game" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +``` + +就跟在第 1 章中所看到的那样,`cargo new` 生成了一个 “Hello, world!” 的程序。检视那个 `src/main.rs` 文件: + +文件名:`src/main.rs` + +```rust +fn main() { + println! ("Hello, world!"); +} +``` + +现在就来使用 `cargo run` 命令,在同一个步骤中编译这个 “Hello, world!” 程序并运行他: + +```console +$ cargo run  ✔ + Compiling guessing_game v0.1.0 (/home/peng/rust-lang/projects/guessing_game) + Finished dev [unoptimized + debuginfo] target(s) in 0.44s + Running `target/debug/guessing_game` +Hello, world! +``` + +在需要在项目上进行快速迭代时,这个 `run` 命令用起来就相当顺手了,这正是这里在这个游戏中将要做的,在前往下一迭代之前,对每次迭代进行快速测试。 + +请再次打开这个 `src/main.rs` 文件。即将在这个文件中编写所有的代码。 + +## 处理一次猜数 + +这个猜数游戏的第一部分,将请求用户的输入、处理那个输入,进而检查该输入是否有着正确格式。这里将实现玩家输入一个猜数开始。请敲入清单 2-1 中的代码到 `src/main.rs` 里去。 + +文件名:`src/main.rs` + +```rust +use std::io; + +fn main() { + println! ("猜出这个数来!"); + + println! ("请输入你猜的数。"); + + let mut guess = String::new(); + + io::stdin() + .read_line(&mut guess) + .expect("读取行失败"); + + println! ("你猜的数为:{}", guess); +} +``` + +*清单 2-1,从用户获取到一个猜数并将其打印出来的代码* + +此代码包含了很多信息,那么这里就来一行一行的走一遍。要获取到用户输入并将结果打印出来,就需要将 `io` 输入/输出库带入到作用域中。而 `io` 库则是来自名为 `std` 的标准库: + +```rust +use std::io; +``` + +默认情况下,Rust 只有少数几个定义在标准库中、由标准库带入到每个程序的项目(by default, Rust has a few items defined in the standard library that it brings into the scope of every program)。这个集合被称为 Rust 序曲(`prelude`),在 [标准库文档](https://doc.rust-lang.org/std/prelude/index.html) 中可找到全部的标准库 `prelude` 项目。 + +在要使用的类型,不在 Rust 序曲集合中时,就必须将那个类型,显式地通过 `use` 语句带入到作用域中。`std::io` 库的使用,提供了数个有用特性,包括接收用户输入的能力。 + +就跟在第 1 章所见到的那样,`main` 函数即是这个程序的进入点: + +```rust +fn main() { +``` + +`fn` 语法声明了一个函数,而这个圆括号,`()`,表示这里没有参数,同时那个花括号,`{`,是该函数的函数体的开始。 + +同样与在第 1 章中所了解的那样,`println!` 是个将字符串打印到屏幕的宏(macro): + +```console + println! ("猜出这个数来!"); + + println! ("请输入你猜的数。"); +``` + +这段代码在打印提示消息,表明该游戏是什么及正在请求用户输入。 + +## 使用变量保存那些值 + +接下来,就要创建一个 *变量(variable)* 来存储用户输入,像下面这样: + +```rust + let mut guess = String::new(); +``` + +现在这个程序就变得有趣起来了!这小小一行,可是有很多东西。这里使用了 `let` 语句来创建这个变量。下面是另一个示例: + +```rust +let apples = 5; +``` + +这行代码创建了一个新的名为 `apples` 的变量,并将其绑定到了值 `5`。在 Rust 中,默认变量是不可变的(immutable)。在后续第 3 章的 [变量及可变性](Ch03_Common_Programming_Concepts.md#variables-and-mutability) 小节,将对此概念加以讨论。而要让变量可变,就要将变量名字前加上 `mut` 关键字: + +```rust +let apples = 5; // 不可变(immutable) +let mut bananas = 5; // 可变(mutable) +``` + +> 注意:这里的 `//` 语法,开始了一条持续到那个行结束的代码注释。Rust 会忽略注释中的全部内容。在 [第 3 章](Ch03_Common_Programming_Concepts.md#comments) 将更加详细地讨论代码注释。 + +回到这个猜数游戏程序,那么此刻就明白了那个 `let mut guess` 将引入一个名为 `guess` 的可变变量。而那个等号(`=`),则是告诉 Rust,现在要将某个东西绑定到该变量了。等号右边就是要绑定到 `guess` 的那个值,而这个值则是调用 `String::new` 的结果,这个 `String::new`,则又是一个返回一个 `String` 实例的函数。`String` 是由标准库提供的一个字符串类型,为一个可增大的、经 UTF-8 位编码的文本(a growable, UTF-8 encoded bit of text)。 + +在那个 `::new` 代码行中的 `::` 语法,表示其中的 `new` 是 `String` 类型的一个关联函数(an associated funtion of the `String` type)。至于 *关联函数(associated function)*,指的是应用到某种类型上的函数,在此实例中,类型就是 `String` 了。这个 `new` 函数创建了一个新的、空空的字符串。由于`new` 是个构造某种新值的常见函数,因此在许多类型上,都将找到 `new` 函数。 + +整体上看,这个 `let mut guess = String::new();` 语句,完成了一个当前绑定到新的、`String` 类型空实例的可变变量的创建。总算讲清楚了! + +## 接收用户输入 + +回顾程序第一行上,以 `use std::io;` 从标准库所包含进来的输入/输出功能。那么现在就要调用那个 `io` 模组中的 `stdin` 函数,该函数将实现对用户输入的处理: + +```rust + io:stdin() + .readline(&mut guess) +``` + +若在程序的开头不曾以 `std::io` 方式,将 `io` 库导入,那么仍然可以将该函数写作 `std::io::stdin` 形式,而对其进行使用。`stdin` 函数返回的是 `std::io::Stdin` 的实例, 而 `std::io::Stdin` 则表示终端标准输入句柄的类型(the `stdin` function returns an instance of `std::io::Stdin`, which is a type that represents a handle to the standard input for your terminal)。 + +接下来的代码行 `.readling(&mut guess)` 调用了标准输入句柄类型实例上的 `read_line` 方法,用于获取用户输入。这里还将 `&mut guess` 作为 `read_line` 的参数进行了传递,以告诉 `read_line` 函数,将用户输入存入到哪个字符串中。`read_line` 的整个职能,就要将用户敲入到标准输入的东西,追加到某个字符串(在不覆盖掉这个字符串内容的情况下),因此这里是将那个字符串作为参数传递的。为了这个 `read_line` 方法可以修改其内容,这里的字符串就要是可变的。 + +其中的 `&` 表明该参数是个 *引用(reference)*,而引用则是一种无需将数据多次拷贝到内存中的情况下,就可以实现代码多个部分对该数据进行读写的特性(注:在 C 家族语言中,`&`表示内存地址,因此 Rust 中的引用,与指针有类似之处)。引用是一项复杂特性,同时 Rust 的主要优点之一,就是安全而便利地运用引用的方式。对于完成这个猜数游戏,是不必对这些细节有过多了解的。现在要明白的是,与变量类似,引用默认也是不可变的。因此,这里就要写上 `&mut guess` 而不是 `&guess`,来令到这个到 `guess` 的引用为可变的。(第 4 章将更详细地对引用进行解释。) + +## 处理潜在的带有 `Result` 的程序失效 + +**Handle Potential Failure with the `Result` Type** + +这里还在解析代码行。尽管这里讨论的是代码文本的第三行,但他仍是单个逻辑代码行的一部分。接下来的部分是这个方法: + +```rust + .expect("读取输入失败"); +``` + +这代码本可以写成下面这样: + +```rust +io::stdin().read_line(&mut guess).expect("读取输入失败"); +``` + +不过这样的一个长代码行,难于阅读,因此最好将其分开为多个断行。在以 `.method_name()` 语法调用方法时,通过引入另起一行及缩进,来将长的代码行拆分为短代码行,通常是明智的。下面就来说说这一行完成了什么。 + +前面讲过,`read_line`方法将用户敲入的东西,放入到传递给他的那个字符串中,然而 `read_line` 还会返回一个值 -- 在此实例中,返回的就是一个 `io::Result` 类型值。Rust 在他的标准库中,有着数个名为 `Result` 的类型:这是一个泛型的 `Result`,对于那些子模组都有着特定版本,比如这里的 `io::Result`。`Result` 的那些类型都属于 [枚举(enumerations)](Ch06_Enums_and_Pattern_Matching.md#enums),枚举常被写为 `enums`,枚举有着一套被称作 *变种(variants)* 的可能值。枚举常常是和 `match` 关键字一起使用的,而 `match` 则是一种条件判断,在符合某个条件时,就可以很方便地根据枚举中的哪个变种,来执行不同代码。 + +第 6 章将深入涵盖到枚举数据结构。而这些 `Result` 类型的目的,则是对错误处理信息进行编码。 + +这个 `Result` 的变种,就是 `Ok` 与 `Err`。`Ok` 变种表示该操作是成功的,而在 `Ok` 内部,就是成功生成的值。相反 `Err` 变种,则意味着操作失败了,同时 `Err` 包含了关于操作失败的方式与原因。 + +`Result` 类型的那些值,跟其他任何类型都差不多,在这些值上都定义了一些方法。`io::Result` 实例,就有一个可供调用的 [`expect` 方法](https://doc.rust-lang.org/std/result/enum.Result.html#method.expect)。在这个 `io::Result` 实例是个 `Err` 变种时,那么`expect` 方法就会导致程序崩溃,并将传递给 `expect` 方法的参数显示出来。若 `read_line` 方法返回了一个 `Err`,那很可能是来自所采用操作系统错误的结果(if the `read_line` method returns an `Err`, it would likely be the result of an error coming from the underlying operating system)。而若该 `io::Result` 实例是个 `Ok` 值,那么 `expect` 方法就会取得那个 `Ok` 所保存的返回值,并只将该值返回,从而就可以使用到这个返回值。在此实例中,那个值,就是用户输入中的字节数目。 + +若这里没有对 `expect` 方法进行调用,那么该程序会编译,不过会收到一条告警信息: + +```console +$ cargo build  ✔ + Compiling guessing_game v0.1.0 (/home/peng/rust-lang/projects/guessing_game) +warning: unused `Result` that must be used + --> src/main.rs:10:5 + | +10 | / io::stdin() +11 | | .read_line(&mut guess); + | |_______________________________^ + | + = note: `#[warn(unused_must_use)]` on by default + = note: this `Result` may be an `Err` variant, which should be handled + +warning: `guessing_game` (bin "guessing_game") generated 1 warning + Finished dev [unoptimized + debuginfo] target(s) in 0.48s +``` + +Rust 警告说不曾对返回自 `read_line` 的 `Result` 值进行使用,表示程序没有对可能的错误加以处理。 + +消除该警告信息的正确方式,就是要老老实实地编写错误处理代码,而在这个实例中,则只要在问题发生时,崩溃掉这个程序即可,因此这里就可以使用 `expect`。在 [第 9 章](Ch09_Error_Handling.md#recoverable-errors-with-result) 会掌握到如何从错误中恢复过来。 + +## 使用 `println!` 的占位符将值打印出来 + +**Printing Values with `println!` Placeholders** + +紧接着那个结束花括号前面,就只有剩下的一行代码要讨论了: + +```rust + println! ("你猜的数是:{}", guess); +``` + +这行代码是将此刻包含了用户输入的那个字符串打印出来。其中的那套花括号 `{}` ,就是一个占位符(placeholder):请将`{}`当作是些在那个地方留有一个值的小螃蟹。使用一些这样的花括号,就可以打印出多个值来:第一套花括号保留着在格式化字符串之后列出的第一个值,第二套保留着第二个值,如此等等。一个 `println!` 调用中多个值的打印,看起来会是下面这样: + +```rust +let x = 5; +let y = 10; + +println! ("x = {} 同时 y = {}", x, y); +``` + +此代码将打印出 `x = 5 同时 y = 10`。 + +## 对第一部分的测试 + +下面就来测试一下这猜数游戏的第一部分。用 `cargo run` 运行他: + +```console +$ cargo run  ✔ + Compiling guessing_game v0.1.0 (/home/peng/rust-lang/projects/guessing_game) + Finished dev [unoptimized + debuginfo] target(s) in 0.68s + Running `target/debug/guessing_game` +猜出这个数来! +请输入你猜的数。 +6 +你猜的数为:6 +``` + +此刻,这游戏的第一部分就算完成了:这里正从键盘获取到输入,并随后将输入打印出来。 + + +## 生成秘密数字 + +接下来,就需要生成一个用户将要试着去猜的秘密数字了。生成的秘密数字应每次都不相同,这样这游戏在多次玩的时候才有趣。为了不让这个游戏太难,这里要用一个 `1` 到 `100` 之间的随机数。Rust 在其标准库中尚未包含随机数功能。不过 Rust 团队还真的提供了一个 [`rand` 代码箱](https://crates.io/crates/rand),这里就姑且把这样的代码箱,称之为功能吧。 + +### 运用代码箱(a Crate) 获取到更多功能 + +请记住,所谓代码箱,即为一些 Rust 源代码文件的集合。之前曾构建好的项目,则是一个 *二进制的代码箱(binary crate)*,那是个可执行程序。而 `rand` 代码箱,则是个 *库代码箱(library crate)*,这样的库代码箱,包含了预期将在其他程序中会用到的代码,同时库代码箱自身并不能执行(the `rand` crate is a *library crate*, which contains code intended to be used in other programs, and can't be executed on its own)。 + +Cargo 对外部代码箱的协调能力,正是 Cargo 真正闪耀之处。在能够编写出用到 `rand` 库代码箱的代码之前,先要将 `Cargo.toml` 加以修改,将 `rand` 代码箱作为依赖包含进来。打开那个文件并将下面的行,添加到底部、那个 Cargo 创建出的`[dependencies]` 小节标题之下。要确保像这里一样,带着版本号地精确指明 `rand` 代码箱,否则此教程中的代码示例就不会工作。 + +文件名:`Cargo.toml` + +```toml +rand = "0.8.3" +``` + +在这 `Cargo.toml` 文件中,凡在某个标题之后的东西,都是那个小节的一部分,直到另一小节开始为止。在 `[dependencies]` 小节,告诉 Cargo 的是项目依赖了哪些外部代码箱(external crates),以及所需的这些代码箱版本。在此实例中,就指明了有着语义版本指示符(the semantic version specifier) `0.8.3` 的 `rand` 库代码箱。Cargo 能明白 [语义化版本控制(Sementic Versioning)](http://semver.org/)(有时也叫做 *`SemVer`*),这是编制版本号的标准。数字 `0.8.3` 实际上是 `^0.8.3` 的缩写,表示高于 `0.8.3` 却低于 `0.9.0` 的任何版本。Cargo 认为这些版本有着与 `0.8.3` 兼容的公共 APIs,同时这样的规定,确保了将获取到在本章中代码仍可编译的情况下,最新的补丁发布。那些 `0.9.0` 及更高的版本,无法保证接下来示例用到同样的 API。 + +现在,在不修改任何代码的情况下,来构建一下这个项目,如清单 2-2 所示: + +```console +$ cargo build + Updating crates.io index + Downloaded rand v0.8.3 + Downloaded libc v0.2.86 + Downloaded getrandom v0.2.2 + Downloaded cfg-if v1.0.0 + Downloaded ppv-lite86 v0.2.10 + Downloaded rand_chacha v0.3.0 + Downloaded rand_core v0.6.2 + Compiling rand_core v0.6.2 + Compiling libc v0.2.86 + Compiling getrandom v0.2.2 + Compiling cfg-if v1.0.0 + Compiling ppv-lite86 v0.2.10 + Compiling rand_chacha v0.3.0 + Compiling rand v0.8.3 + Compiling guessing_game v0.1.0 (file:///projects/guessing_game) + Finished dev [unoptimized + debuginfo] target(s) in 2.53s +``` + +*清单 2-2-1:在添加了作为依赖的 `rand` 代码箱后运行 `cargo build` 的输出(书上的输出)* + +```console +$ cargo build  ✔ + Updating crates.io index + Downloaded cfg-if v1.0.0 + Downloaded rand_chacha v0.3.1 + Downloaded rand_core v0.6.3 + Downloaded getrandom v0.2.7 + Downloaded ppv-lite86 v0.2.16 + Downloaded rand v0.8.5 + Downloaded libc v0.2.126 + Downloaded 7 crates (773.8 KB) in 3.41s + Compiling libc v0.2.126 + Compiling cfg-if v1.0.0 + Compiling ppv-lite86 v0.2.16 + Compiling getrandom v0.2.7 + Compiling rand_core v0.6.3 + Compiling rand_chacha v0.3.1 + Compiling rand v0.8.5 + Compiling guessing_game v0.1.0 (/home/peng/rust-lang/projects/guessing_game) + Finished dev [unoptimized + debuginfo] target(s) in 56.66s +``` + +*清单 2-2-2:在添加了作为依赖的 `rand` 代码箱后运行 `cargo build` 的输出(实际输出)* + +这里可能会看到不同的一些版本号(归功于 `SemVer`,这些不同版本号将与示例代码全都兼容!)、不同的输出行(取决于所在的操作系统),以及这些行可能以不同顺序出现。 + +在包含外部依赖时,Cargo 会从 *登记处(registry)* 拉取到那个依赖所需的全部最新版本的代码箱,而所谓登记处,则是 [Crates.io](https://crates.io/) 数据的一份拷贝。Crates.io 是 Rust 生态中的人们,发布给其他人使用的开放源代码项目的地方。 + +在更新了登记处索引之后,Cargo 就对 `[denpendencies]` 小节进行查看,并下载所列代码箱中尚未下载的那些。在此实例中,尽管只列出了依赖 `rand`,Cargo 还抓取了其他 `rand` 赖以运作的一些代码箱。在下载了这些代码箱之后,Rust 会对他们进行了编译,并随后以这些可用的依赖,对这项目进行了编译。 + +若不做任何修改,就立即再次运行 `cargo build`,那么除了那行 `Finished` 输出之外,就再也没有别的输出了。Cargo 明白他以及下载并编译好了那些依赖,还明白尚未对 `Cargo.toml` 文件做任何修改。Cargo 还知道,这里并未对项目代码做任何修改,因此他也没有对项目代码重新编译。既然无事可做,那么他就直接退出了。 + +```console +$ cargo build  ✔ + Finished dev [unoptimized + debuginfo] target(s) in 0.00s +``` + +若此时打开 `src/main.rs` 文件,做个细微修改,然后保存并再次构建,那么就只会看到下面这两行输出: + +```console +cargo build  ✔ + Compiling guessing_game v0.1.0 (/home/peng/rust-lang/projects/guessing_game) + Finished dev [unoptimized + debuginfo] target(s) in 0.50s +``` + +这些行显示 Cargo 只更新了对 `src/main.rs` 文件细微修改的构建。由于依赖不曾改变,因此 Cargo 清除他可以重用那些已经下载和编译好的依赖。 + +### 使用 `Cargo.lock` 文件确保可重现的构建 + +**Ensuring Reproducible Builds with the `Cargo.lock` File** + +Cargo 具备一种不论是自己还是其他要构建代码的人来说,确保每次都可以构建出同样程序组件(the same artifact)的机制:除非另有指定,Cargo 都将只使用在 `[denpendencies]` 小节中所指定的依赖版本。比如说下周 `0.8.4` 版本的 `rand` 就要释出,且那个版本包含了一个重要的错误修复,但也包含了一个会破坏咱们代码的特性撤回。为了应对这样的情况,Rust 在首次运行 `cargo build`时,就创建了 `Cargo.lock` 文件,也就是现在在 `guessing_game` 目录下就有这么个文件。 + +在首次构建项目时,Cargo 会找出那些依赖满足条件的所有版本,并将其写入到这 `Cargo.lock` 文件。在今后对项目进行构建时,Cargo 就会查看是否存在那个 `Cargo.lock` 文件,并使用其中所指定的那些版本,而不会再次完成找出那些版本的工作了。这样就自动实现了可重现的构建。也就是说,得益于这个 `Cargo.lock` 文件,除非显式地升级了 `rand` 的版本号,项目将保持其版本为 `0.8.3`。 + +### 更新代码箱来获取新版本 + +**Updating a Crate to Get a New Version** + +在确实要更新某个代码箱时,Cargo 提供了 `update` 命令,该命令会忽略 `Cargo.lock` 文件,并找出与`Cargo.toml`中的那些规格相适合的全部最新版本。Cargo 随后将把这些版本写入到 `Cargo.lock` 文件。否则的话,默认 Cargo 就会只查找那些高于 `0.8.3` 且低于 `0.9.0` 的版本。在 `rand` 库代码箱已发布了两个新的 `0.8.4` 和 `0.9.0` 版本时,此时若运行 `cargo update`,就会看到下面的输出: + +```console +$ cargo update + Updating crates.io index + Updating rand v0.8.3 -> v0.8.4 +``` + +Cargo 忽略了那个 `0.9.0` 的发布。此刻还会注意到在 `Cargo.lock` 文件中,一处标记现在所用 `rand` 代码箱版本为 `0.8.4` 的改变。要使用版本 `0.9.0` 或任何 `0.9.x` 系列中某个版本的 `rand`,就必须将 `Cargo.toml` 更新为下面这样: + +```toml +[dependencies] +rand = "0.9.0" +``` + +在下次运行 `cargo build` 时,Cargo 就会更新可用代码箱的登记处,并根据所指定的新版本,重新对 `rand` 需求加以评估。 + +关于 [Cargo](http://doc.crates.io/) 及 [Cargo 生态](http://doc.crates.io/crates-io.html),有很多要讲的东西,这些在第 14 章会讨论到,而此时,了解上面这些就够了。Cargo 实现了非常便利的库重用,因此 Rust 公民们就能够编写出,从数个软件包组合而来的那些体量较小的项目。 + +### 生成随机数 + +现在就来开始使用 `rand` 库代码箱,生成用于猜测的数字。接下来的步骤就是更新 `src/main.rs`,如下清单 2-3 所示: + +文件名:`src/main.rs` + +```rust +use std::io; +use rand::Rng; + +fn main() { + println! ("猜出这个数来!"); + + let secret_number = rand::thread_rng().gen_range(1..101); + + println! ("秘密数字为:{}", secret_number); + + println! ("请输入你猜的数。"); + + let mut guess = String::new(); + + io::stdin() + .read_line(&mut guess) + .expect("读取行失败......"); + + println! ("你猜的数为:{}", guess); +} +``` + +*清单 2-3:添加生成随机数的代码* + +首先,这里添加了那行 `use rand::Rng`。这 `Rng` 特质(the `Rng` trait)定义了一些随机数生成器实现的方法,而为了使用这些方法,此特质就必须要在作用域中。第 10 章将详细涵盖到特质(traits)。 + +接下来在中间部分,添加了两行新代码。在第一行代码中,调用了 `rand::thread_rng` 函数,该函数给到了这里即将用到的特定随机数生成器:一个相对于当前执行线程,属于本地的随机数生成器,其用到的种子由操作系统提供。随后在这个随机数生成器实例上的 `gen_range` 方法。该方法是由前面 `use rand::Rng` 语句带入到作用域的 `Rng` 特质定义。这 `gen_range` 方法取的是一个范围表达式,这里用到的范围表达式,所采取的是 `start..end` 形式,该范围表达式包含了左边界,但排除了右边界,因此就要指定 `1..101` 来求得一个 `1` 到 `100` 之间的数字。或者也可以传递范围 `1..=100`,这是等价的。 + +> 注意:对于不知道到底该使用那个 Rust 特质,以及要调用代码箱的那些方法和函数的情况,那么每个代码箱都有着如何使用他的说明文档。Cargo 的另一灵巧特性,便是通过运行 `cargo doc --open` 命令,就会构建出由全部本地依赖提供的文档来,并在浏览器中打开这些文档。比如说若对 `rand` 这个代码箱的其他功能感兴趣,那么运行 `cargo doc --open` 命令然后点击左侧边栏中的 `rand` 即可进一步了解。 + +那第二个新行,则是打印出那个秘密数字。在开发这个程序期间,这是有用的,这样能够对程序进行测试,不过在最终版本那里就会删除这行代码。若程序在一开始就打印出谜底,显然这就算不上是个游戏了。 + +尝试运行几次这个程序: + +```console +$ cargo run  ✔  4s  + Compiling guessing_game v0.1.0 (/home/peng/rust-lang/projects/guessing_game) + Finished dev [unoptimized + debuginfo] target(s) in 0.54s + Running `target/debug/guessing_game` +猜出这个数来! +随机生成的秘密数字为:40 +请输入你猜的数。 +86 +你猜的数为:86 + +$ cargo run  ✔  9s  + Finished dev [unoptimized + debuginfo] target(s) in 0.00s + Running `target/debug/guessing_game` +猜出这个数来! +随机生成的秘密数字为:30 +请输入你猜的数。 +27 +你猜的数为:27 + +``` + +就会得到不同的随机数字,并且他们都应是 `1` 到 `100` 之间的数字。非常棒! + +## 将猜数与秘数相比较 + +既然有了用户输入和随机数,就可以加以比较了。比较的步骤在下面的清单 2-4 中给出了。请注意这个代码还不会编译,原因后面会解释。 + +文件名:`src/main.rs` + +```rust +use rand::Rng; +use std::cmp::Ordering; +use std::io; + +fn main() { + // --跳过前面的代码-- + + println! ("你猜的数为:{}", guess); + + match guess.cmp(&secret_number) { + Ordering::Less => println! ("太小了!"), + Ordering::Greater => println! ("太大了!"), + Ordering::Equal => println! ("你赢了!"), + } +} +``` + +*清单 2-4:对比较两个数可能的返回值进行处理* + +首先这里添加了另一个 `use` 语句,将标准库的一个名为 `std::cmp::Ordering` 的类型,带入到作用域。这 `Ordering` 了新是另一个枚举,且其有着 `Less`、`Greater` 和 `Equal` 共计三个变种。这些就是在对两个值进行比较时,三个可能的输出了。 + +随后在该程序底部,添加了用到这 `Ordering` 类型的五行新代码。其中的 `cmp` 方法是对两个值进行比较,并可在任何被可比较物上进行调用。`cmp` 方法会取一个要与之相比的引用(a reference):这里他是在将 `guess` 与 `secret_number` 相比。随后他就返回了前面用 `use` 语句带入到作用域的 `Ordering` 枚举的一个变种。这里用一个 `match` 表达式,根据以 `guess` 和 `secret_number` 中的值,对 `cmp` 调用所返回具体 `Odering` 变种,而确定出下一步要做什么。 + +`match` 表达式由数个 *支臂(arms)* 构成。每个支臂是由要与之匹配的 *模式(pattern)* ,及在给到 `match` 的值与该支臂的模式符合时,应运行的代码所组成。Rust 取给到 `match` 的值,并以此检视各个支臂的模式。模式及 `match` 结构,是强大的 Rust 特性,实现对代码可能遇到的各种情况的表达,并确保对全部的这些情况进行处理。在第 6 章和第 18 章,相应地将详细涵盖到这些特性。 + +下面就来对这里使用的 `match` 表达式的一个示例走一遍。假设说用户猜的数是 `50`,同时随机生成的秘密数这次是 `38`。在代码将 `50` 与 `38` 作比较时,由于 `50` 比 `38` 大,因此那个 `cmp` 方法就会返回 `Odering::Greater`。于是 `match` 表达式就获取到值 `Odering::Greater` 并开始对各个支臂的模式进行检查。他看了第一个支臂的模式,是 `Ordering::Less`,并发现值 `Ordering::Greater` 与 `Odering::Less` 不匹配,那么他就会忽略第一个支臂中的代码而移步到下一支臂。下一支臂的模式为 `Ordering::Greater`,这正好与 `Odering::Greater` 相匹配!那个支臂中的相关代码就会执行,进而将 `太大了!`打印到屏幕。在此场景中,由于`match` 表达式无需检视那最后的支臂,因此他就结束了。 + +然而清单 2-4 中的代码并不会编译。这里试着编译一下: + +```console +$ cargo build  ✔ + Compiling guessing_game v0.1.0 (/home/peng/rust-lang/projects/guessing_game) +error[E0308]: mismatched types + --> src/main.rs:22:21 + | +22 | match guess.cmp(&secret_number) { + | ^^^^^^^^^^^^^^ expected struct `String`, found `i32` + | + = note: expected reference `&String` + found reference `&i32` + +For more information about this error, try `rustc --explain E0308`. +error: could not compile `guessing_game` due to previous error +``` + +这些错误状态的核心,指向的是存在 *不匹配的类型(mismatched types)*。Rust 有着强静态类型系统(Rust has a strong, static type system)。不过他也有着类型推导(type inference)。在写下 `let mut guess = String::new()` 时,Rust 当时就能推导出 `guess` 应是个 `String`,而没有要求一定要写出该类型i`String`。但对于 `secret_number` 来说,则是一个数字类型。有几种 Rust 数字类型都可以保有一个 `1` 到 `100` 之间的值:`i32`,32 位整数;`u32`,32 位无符号整数;`i64`,64 位整数;还有一些其他的。除非有特别指明,Rust 默认都是个 `i32` 整数,除非在某处给 `secret_number` 添加了引起 Rust 推断出不同数字类型的类型信息,那么 `secret_number` 的类型就会是 `i32`。上面错误的原因,就是 Rust 无法将字符串与数字类型相比较。 + +最后,这里就要将程序以输入形式读取到的 `String`,转换成具体数字类型,如此就可以将其与`secret_number`进行数学上的比较。这里通过将下面这行添加到 `main` 函数体完成的: + +文件名:`src/main.rs` + +```rust + // --跳过前面的代码-- + + let mut guess: String = String::new(); + + io::stdin() + .read_line(&mut guess) + .expect("读取行失败......"); + + let guess: u32 = guess.trim().parse().expect("请输入一个数字!"); + + println! ("你猜的数为:{}", guess); + + match guess.cmp(&secret_number) { + Ordering::Less => println! ("太小了!"), + Ordering::Greater => println! ("太大了!"), + Ordering::Equal => println! ("你赢了!"), + } +``` + +添加的那行就是: + +```rust +let guess: u32 = guess.trim().parse().expect("请输入一个数字!"); +``` + +这里创建了一个名为 `guess` 的变量。不过稍等一下,这个程序不是已经有了一个名为 `guess` 的变量了吗?他确实已经有了个名为 `guess` 的变量,然而好在 Rust 是允许以一个新的 `guess` 变量,对其先前的值进行 *遮蔽(shadow)* 操作的。这样的遮蔽特性,实现了对`guess` 这个变量名的重用,而非强制创建两个诸如 `guess_str` 和 `guess` 这样的独特变量。在第 3 章将对此进行更详细的讲解,此时只要明白,此特性通常用在要将某个值从一种类型转换到另一类型的时候。 + +这里将这个新变量,绑定到了表达式 `guess.trim().parse()`。该表达式中的 `guess` 援引的是原来那个包含着字符串形式输入的 `guess`。而作用在 `String` 实例上的 `trim` 方法,将消除开头和结尾的全部空白,必须要进行这个操作,才能将字符串转换到 `u32` 类型,`u32`只能包含数字数据。为了满足到 `read_line` 并输入他们的猜数,用户必须要按下回车键,这样就会将一个换行字符添加到那个字符串。比如在用户敲入了 `5` 然后按下回车键时,`guess`看起来就会是这样:`5\n`。其中的 `\n` 表示 “换行(newline)”。(在 Windows 上,按下回车键会导致一个回车字符和一个换行字符,即 `\r\n`)。这 `trim` 会将 `\n` 或 `\r\n` 消除,而结果就只是 `5` 了。 + +[字符串上的 `parse` 方法](https://doc.rust-lang.org/std/primitive.str.html#method.parse) 将只会在那些逻辑上可被转换成数字的字符上运作,而因此就很可能引起错误。比如说在字符串包含了 `A👍%` 时,就没有办法将其转换成一个数字。由于 `parse` 方法会失败,因此他返回的是个 `Result` 类型,这与 `read_line` 方法所做的一样(在早先的 [用 `Result` 类型处理潜在失败](#handling-potential-failure-with-the-result-type) 中讨论过)。这里再次使用 `expect` 方法对这个`Result` 进行了同样的处理。在因为 `parse` 无法从字符串创建出一个数字,而返回了一个 `Err` 的 `Result` 变种时,这个 `expect` 就会令到游戏崩溃,并将给他的那条消息打印出来。而在 `parse` 可成功将那个字符串,转换成数字时,`expect` 就会返回 `Result` 的 `Ok` 变种,同时 `expect` 会返回这里想要的、`Ok` 值中的数字。 + +现在来运行一下这个程序! + +```console +$ cargo run  101 ✘  3s  + Finished dev [unoptimized + debuginfo] target(s) in 0.00s + Running `target/debug/guessing_game` +猜出这个数来! +随机生成的秘密数字为:66 +请输入你猜的数。 + 76 +你猜的数为:76 +太大了! +``` + +很棒!尽管在猜数前加了一些空格,程序仍然算出了用户猜的是 `76`。多运行几次这个程序,来验证在各种输入时其不同的表现:猜对一个数、猜个太大的数,以及猜个过小的数。 + +现在这个游戏大致在工作了,然而用户只能猜一次。下面就来通过添加循环对其进行修改! + +## 用循环来实现多次猜数 + +**Allowing Multiple Guesses with Looping** + +关键字 `loop` 创建出无限循环。这里就要添加一个循环,来让用户有更多机会去猜数: + +文件名:`src/main.rs` + +```rust + // --跳过-- + + println! ("随机生成的秘密数字为:{}", secret_number); + + loop { + println! ("请输入你猜的数。"); + + // --跳过-- + + match guess.cmp(&secret_number) { + Ordering::Less => println! ("太小了!"), + Ordering::Greater => println! ("太大了!"), + Ordering::Equal => { println! ("你赢了!"); break }, + } + } +} +``` + +可以看到,这里已将自猜数输入提示开始的全部代码,移入到循环中了。请确保循环中的那些代码行,都另外缩进四个空格,然后再次运行这个程序。现在程序将会一直要求另一猜数,这实际上引入了新的问题。好像是用户无法退出了! + +用户可一直通过键盘快捷键 `Ctrl-C`,来中断这个程序。不过还是有别的方法,来退出这头贪厌的怪兽,就像在 [将猜数与秘密数字比较](#comparing-the-guess-to-the-secret-number)中对 `parse` 方法讨论中提到的那样:在用户输入了非数字的答案时,程序就会崩溃。这里就利用了那个,来实现用户退出,如下所示: + +```console +$ cargo run + Compiling guessing_game v0.1.0 (/home/peng/rust-lang/projects/guessing_game) + Finished dev [unoptimized + debuginfo] target(s) in 0.53s + Running `target/debug/guessing_game` + +---猜出这个数来!--- +请输入你猜的数。( ‘Q/quit’ 退出游戏) +50 +你猜的数为:50 +太小了! +请输入你猜的数。( ‘Q/quit’ 退出游戏) +75 +你猜的数为:75 +太大了! +请输入你猜的数。( ‘Q/quit’ 退出游戏) +62 +你猜的数为:62 +太大了! +太小了! +请输入你猜的数。( ‘Q/quit’ 退出游戏) +55 +你猜的数为:55 +你赢了! + +---猜出这个数来!--- +请输入你猜的数。( ‘Q/quit’ 退出游戏) +quit +thread 'main' panicked at '请输入一个数字!: ParseIntError { kind: InvalidDigit }', src/main.rs:25:51 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +``` + +敲入 `quit` 就会退出这游戏,不过正如所注意到的,这样做将就要敲入别的非数字输入。至少可以是这种做法是次优的;这里想要在猜到了正确数字时,游戏也要停止。 + +## 猜对后的退出 + +下面就来通过添加一条 `break` 语句,将游戏编程为在用户赢了时退出: + +文件名:`src/main.rs` + +```rust + // --跳过-- + + match guess.cmp(&secret_number) { + Ordering::Less => println! ("太小了!"), + Ordering::Greater => println! ("太大了!"), + Ordering::Equal => { + println! ("你赢了!"); + break + }, + } + } +} +``` + +在 `你赢了!` 之后添加上 `break` 代码行,就令到游戏在用户猜中了秘密数字时,退出那个循环。由于该循环是 `main` 函数体的最后部分,因此退出循环也意味着退出这个程序。 + + +## 无效输入的处理 + +为了进一步改进游戏表现,而不要在用户输入了非数字时将程序崩溃掉,那么接下来就要使得游戏忽略非数字,从而用户可以继续猜数。通过把`guess`从 `String` 转换为 `u32` 的那行加以修改,来完成这个目的,如下面的清单 2-5 所示: + +文件名:`src/main.rs` + +```rust + // --跳过-- + + io::stdin() + .read_line(&mut guess) + .expect("读取行失败......"); + + if guess.trim().eq("Q") || guess.trim().eq("quit") { process::exit(0); } + + // let guess: u32 = guess.trim().parse().expect("请输入一个数字!"); + let guess: u32 = match guess.trim().parse() { + Ok(num) => num, + Err(_) => { println! ("请输入一个数字!"); continue }, + }; + + println! ("你猜的数为:{}", guess); + + // --跳过-- +``` + +*清单 2-5:忽略非数字的猜解进而询问另一猜数,而不再是崩溃掉程序* + +这里将原来的 `expect` 调用,转换到了一个 `match` 表达式,而实现了一错误就程序崩溃,到对错误进行处理的转变。请记住 `parse` 返回的是个 `Result` 类型,而 `Result` 则是个枚举,有着变种 `Ok` 和 `Err`。与先前对 `cmp` 方法返回结果 `Ordering` 的处理一样,这里运用了一个 `match` 表达式。 + +在 `parse` 能够成功将那个字符串,转换为数字时,他就会返回一个包含了所得结果数的 `Ok` 值。那 `Ok` 值就会匹配上第一个支臂的模式,而这个 `match` 表达式将值返回 `parse` 产生的、放在`Ok` 值里头的那个 `num` 值。那个数字就会刚好放在这里想要他呆的地方,即这里正在创建的那个新 `guess` 变量了。 + +在 `parse` 无法将那个字符串转换成数字时,他就会返回一个包含了有关该错误详细信息的 `Err` 值。该 `Err` 值不与第一个 `match` 支臂中的 `Ok(num)` 模式匹配,不过却正好匹配第二个支臂中的 `Err(_)` 模式。其中的下划线,`_`,是个收集错误信息的值(a catch-all value);在此示例中,就是要匹配所有 `Err` 值,而不管这些 `Err` 值中包含了什么信息。那么程序就会执行第二支臂的代码,即 `continue`,这是告诉程序前往到那个 `loop` 循环的下一次迭代,进而询问另一个猜数。就这样,有效地方让程序忽略了全部 `parse` 可能会发生的错误了! + +现在程序各方面就应如预期那样工作了。就来试试: + +```console +$ cargo run  ✔ + Compiling guessing_game v0.1.0 (/home/peng/rust-lang/projects/guessing_game) + Finished dev [unoptimized + debuginfo] target(s) in 0.57s + Running `target/debug/guessing_game` + +---猜出这个数来!--- +请输入你猜的数。( ‘Q/quit’ 退出游戏) +50 +你猜的数为:50 +太小了! +请输入你猜的数。( ‘Q/quit’ 退出游戏) +75 +你猜的数为:75 +你赢了! +``` + +非常棒!只需最后一个小的优化,就将完成这个猜数游戏了。没忘记这个程序仍是把秘密数字打印出来的吧。那样做对测试来说没有问题,但却毁掉了这个游戏。这里就来将输出了秘密数字的那个 `prinln!` 给删掉。下面的清单 2-6 给出了最终代码。 + +文件名:`src/main.rs` + +```rust +use rand::Rng; +use std::cmp::Ordering; +use std::io; +use std::process; + +fn main() { + loop { + println! ("\n---猜出这个数来!---"); + + let secret_number: u32 = rand::thread_rng().gen_range(1..101); + + // println! ("随机生成的秘密数字为:{}", secret_number); + + loop { + println! ("请输入你猜的数。( ‘Q/quit’ 退出游戏)"); + + let mut guess: String = String::new(); + + io::stdin() + .read_line(&mut guess) + .expect("读取行失败......"); + + if guess.trim().eq("Q") || guess.trim().eq("quit") { process::exit(0); } + + // let guess: u32 = guess.trim().parse().expect("请输入一个数字!"); + let guess: u32 = match guess.trim().parse() { + Ok(num) => num, + Err(_) => { println! ("请输入一个数字!"); continue }, + }; + + println! ("你猜的数为:{}", guess); + + match guess.cmp(&secret_number) { + Ordering::Less => println! ("太小了!"), + Ordering::Greater => println! ("太大了!"), + Ordering::Equal => { + println! ("你赢了!"); + break + }, + } + } + } +} +``` + +*清单 2-6:完全的猜数游戏代码* + +## 小结 + +到了这里,就成功构建了这个猜数游戏。恭喜! + +该项目以动手的方式,教了许多新的 Rust 概念:`let`,`match` 等关键字,函数、运用外部代码箱及更多。在接下来的几章中,会更深入地掌握这些概念。第 3 章涵盖了大多数编程语言都有的一些概念,诸如变量、数据类型及函数,并展示了如何在 Rust 中使用他们。第 4 章对 Rust 中的所有权(ownership)进行了探索,所有权是一项令到 Rust 不同于其他语言的特性。第 5 章对结构体(structs)和方法语法(method syntax)进行了讨论,而第 6 章解释了枚举的原理。 diff --git a/src/Ch03_Common_Programming_Concepts.md b/src/Ch03_Common_Programming_Concepts.md new file mode 100644 index 0000000..810c084 --- /dev/null +++ b/src/Ch03_Common_Programming_Concepts.md @@ -0,0 +1,1465 @@ +# 常见编程概念 + +本章涵盖了出现在几乎所有编程语言中的一些概念,以及这些概念在 Rust 中运作方式。众多编程语言,在他们各自核心,都有着许多共同的东西。出现在本章中的这些概念,没有一个是 Rust 独有的,然而这里是要就他们在 Rust 语境下进行讨论,并对使用这些概念的相关约定进行解释。 + +具体来讲,本章将要掌握到变量、基本类型、函数、注释及控制流等概念。这些基本概念将出现在全部 Rust 程序中,而早点掌握他们,就会给到一个强大的核心起点。 + +> **关键字(keywords)** +> +> Rust 语言有着一套 *关键字*,他们是保留的,仅由语言使用,在这点上与其他语言没有什么不同。牢记是不可以将这些词汇,用作变量或函数的名称的。多数关键字都有着特殊意义,而会使用他们来完成 Rust 程序中不同任务;其中有少数几个当前并没有关联功能,他们被保留用于未来将添加到 Rust 的功能。在 [附录 A](Ch99_Appendix_A.md) 就能看到关键字清单。 + + +## 变量与可变性 + +**Variables and Mutability** + +就如在之前的 ["用变量保存值"](Ch02_Programming_a_Guessing_Game.md#storing-values-with-variables) 小节中所讲的那样,默认变量是不可变的。这是 Rust 所提供的,推动利用 Rust 赋予的安全和易于并发代码编写方式的众多措施之一(by default variables are immutable, this is one of many nudges Rust gives you to write your code in a way that takes advantage of the safety and easy concurrency that Rust offers)。尽管如此,还是有将变量作为可变的选项。下面就来搞清楚,为何 Rust 会鼓励偏向不可变,以及为何有时会希望不接受 Rust 的建议。 + +在变量为不可变时,一旦值被绑定到某个名字,那么就无法修改那个值了。为对此进行演示,就来通过使用 `cargo new variables` 在 `projects` 目录中生成一个新的名为 `variables` 的项目。 + +然后,在那个新的 `variables` 目录中,打开 `src/main.rs` 并将其代码替换为下面的代码。此代码当然不会被编译,这里首先要对不可变错误加以检视。 + +```rust +fn main() { + let x = 5; + println! ("x 的值为:{}", x); + + x = 6; + println! ("x 的值为:{}", x); +} +``` + +保存并使用 `cargo run` 运行这个程序。就会受到错误消息,如下面这个输出: + +```console +$ cargo run  ✔ + Compiling variables v0.1.0 (/home/peng/rust-lang/projects/variables) +error[E0384]: cannot assign twice to immutable variable `x` + --> src/main.rs:5:5 + | +2 | let x = 5; + | - + | | + | first assignment to `x` + | help: consider making this binding mutable: `mut x` +... +5 | x = 6; + | ^^^^^ cannot assign twice to immutable variable + +For more information about this error, try `rustc --explain E0384`. +error: could not compile `variables` due to previous error +``` + +此示例显示了编译器如何帮助发现程序中的错误。编译器错误可能令人沮丧,但这些编译器错误真的意味着,程序未有安全地执行要程序做的事情;编译器错误并不表示你不是一名良好的程序员!即使那些经验丰富的 Rust 公民,也会收到编译器错误。 + +该错误消息表示错误原因为 `cannot assing twice to immutable variable 'x'`,是因为有尝试将第二个值赋给那个不可变的 `x` 变量。 + +在尝试修改某个被指定为不可变的值时,由于这种情况会导致程序错误,因此这个时候收到编译时错误尤为重要。代码一部分的运作,是建立在值将绝不会改变这种假定上,而代码另一部分却修改了那个值,那么就有可能代码的第一部分未有完成他预计要完成的工作了。此类程序错误的原因,就难于追踪到真相,尤其是在代码第二部分仅 *有的时候* 才对那个值进行修改时。Rust 编译器保证了在表明某个值不会变化时,那么那个值就真的不会变化,如此就不必亲自去紧盯着那个变量了。代码也由此而更易于推演。 + +然而可变则可能会非常有用,并能令到代码更便于编写。变量仅在默认情况下是不可变的;就如同在第 2 章中所做的那样,可通过在变量名字前添加 `mut` 关键字,让变量成为可变。`mut` 的添加,也向将来代码的读者传达了某种意图,表示代码的其他部分,会对这个变量的值进行修改。 + +比如,来将 `src/main.rs` 修改为下面这样: + +文件名:`src/main.rs` + +```rust +fn main() { + let mut x = 5; + println! ("x 的值为:{}", x); + + x = 6; + println! ("x 的值为:{}", x); +} +``` + +在此时运行这个程序时,就会得到这样的输出: + +```rust +$ cargo run  101 ✘ + Compiling variables v0.1.0 (/home/peng/rust-lang/projects/variables) + Finished dev [unoptimized + debuginfo] target(s) in 0.46s + Running `target/debug/variables` +x 的值为:5 +x 的值为:6 +``` + +在使用了 `mut` 关键字时,就被允许将绑定到 `x` 的值从 `5` 修改到 `6` 了。除了防止程序错误之外,还要考虑多种权衡。比如,在使用着大型数据结构时,就地修改其的一个实例,就会比拷贝并返回新近分配的实例要快一些(for example, in cases where you're using large data structures, mutating an instance in place may be faster than copying and returning newly allocated instances)。而对于较小的数据结构,创建其新实例,并以更具函数式编程风格来编写代码,则可能更易于理解,那么由此带来的性能下降,相对所取得的思路清晰,也会是值得的。 + +## 常量 + +与不可变变量类似, *常量(constants)* 是一些绑定到名字且不允许修改的值,但常量与变量之间,有些差异。 + +首先,是不允许在常量上使用 `mut` 关键字的。常量不光是默认不可变的 -- 他们一直都是不可变的。常量的声明用的是 `const` 关键字,而不是 `let` 关键字,同时值的类型 *必须* 被注解(be annotated)。在下一小节,[数据类型](#data-types),就会讲到类型和类型注解了,因此现在不要关心细节。只要明白必须始终对类型进行注解。 + +可在任何作用域,包括全局作用域中声明常量。而当在全局作用域中声明常量时,则会让那些代码中许多部分都需要知悉的值的常量,变得有用起来。 + +常量与不可变变量的最后一个区别,就是常量只能被设置到一个常量表达式,而不能被设置为只能在运行时计算出结果的值。 + +下面是一个常量声明的示例: + +```rust +const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3; +``` + +该常量的名字为 `THREE_HOURS_IN_SECONDS`,而他的值就被设置为了 `60` (即一分钟的秒数)乘以 `60` (即一小时的分钟数)乘以 `3` (此程序中要计数的小时数)。Rust 关于常量命名的约定,即为全部使用大写,在词汇之间用下划线隔开。编译器在运行时,能够执行一套受限的运算,这样就可以选择将常量值,以这种更易于理解和验证的方式写出来,而不是将该常量设置为值 `10,800`。请参阅 [Rust 参考手册有关常量求值的小节](https://doc.rust-lang.org/reference/const_eval.html),了解更多有关在声明常量时可使用那些运算的信息。 + +常量在程序运行的全部时间、在其被声明的作用域内部,都是有效的。常量的这个属性,令到常量对于应用域内的那些、程序多个部分都需要知悉的值来说,变得有用起来,比如某个游戏全部玩家所允许赚到的最大点数,或光速常量。 + +对那些整个程序都要用到的、作为常量的硬编码值进行取名,对于向代码将来的维护者们传达那些值的意义,是相当有用的。对于未来需要更新硬编码值来说,对常量命名就让那些需要修改的代码只有一处要改,而对此带来帮助。 + +## 遮蔽(shadowing) + +如同在第 2 章中的猜数游戏里看到的那样,可声明一个与先前变量同名的新变量。Rust 公民们表示,那第一个变量是被第二个给 *遮蔽* 掉了,这就意味着在用到这个变量是,程序所看到的,会是第二个变量的值。通过使用一样的变量名字,以及重复使用 `let` 关键字,就可对某个变量进行遮蔽,如下所示: + +文件名:`src/main.rs` + +```rust +fn main() { + let x = 5; + + let x = x + 1; + + { + let x = x * 2; + println! ("内部作用域中 x 的值为:{}", x); + } + + println! ("x 的值为:{}", x); +} +``` + +```console +内部作用域中 x 的值为:12 +x 的值为:6 +``` + +> 注意:遮蔽特性的使用,不需要 `mut` 关键字。 + +这个程序首先将 `x` 绑定到值 `5`。随后通过重复 `let x =`,取原来的值并加上 `1`,而对 `x` 做了遮蔽操作,因此 `x` 的值此时就为 `6` 了。之后,在一个内部作用域内,第三个 `let` 语句也对 `x` 进行了遮蔽,将先前的值乘以 `2`,就给到 `x` 一个值 `12`。在那个内部作用域完毕时,那个内部遮蔽就结束了,同时 `x` 回到仍为 `6`。在运行这个程序时,他将输出下面的内容: + + +```console +$ cargo run  ✔ + Compiling variables v0.1.0 (/home/peng/rust-lang/projects/variables) + Finished dev [unoptimized + debuginfo] target(s) in 0.47s + Running `target/debug/variables` +内部作用域中 x 的值为:12 +x 的值为:6 +``` + +由于在不小心而尝试在不带 `let` 关键字而重新赋值给该变量时,会收到编译时错误,因此遮蔽不同于构造一个`mut` 的变量。通过使用 `let` 关键字,就可以在值上执行少量的转换操作,而在这些转换操作完成后又将该变量置入到不可变。 + +`mut` 与遮蔽的另一不同之处,则是由于再次使用`let`关键字时,有效地创建出了一个新变量,因此就可以改变那个值的类型,而仍然重用那同样的变量名字。比如说程序要通过用户输入若干空格字符,来询问用户希望在一些文本之间留多少个空格,而此时又要将用户输入的若干个空格,保存为一个数字: + +```rust +let spaces = " "; +let spaces = spaces.len(); +``` + +第一个 `spaces` 变量是字符串类型,而第二个 `spaces` 变量则是数字类型。遮蔽因此而免于不得不苦苦思索不同的变量名字,诸如 `spaces_str` 及 `spaces_num`;相反,是可以重新较简单的 `spaces` 名称。然而,若尝试对这个变量使用 `mut` 关键字,就会收到一个编译时错误,如下所示: + +```rust +let mut spaces = " "; +spaces = spaces.len(); +``` + +错误是说不允许转变变量类型: + +```console +$ cargo run  ✔ + Compiling variables v0.1.0 (/home/peng/rust-lang/projects/variables) +error[E0308]: mismatched types + --> src/main.rs:14:14 + | +13 | let mut spaces = " "; + | ------ expected due to this value +14 | spaces = spaces.len(); + | ^^^^^^^^^^^^ expected `&str`, found `usize` + +For more information about this error, try `rustc --explain E0308`. +error: could not compile `variables` due to previous error +``` + +现在已经完成变量运行机制的探讨,接卸来就要看看这些变量可以有的那些其余数据类型了。 + + +## 数据类型 + +Rust 的所有值,都属于某种确切的 *数据类型(data type)*,数据类型告诉 Rust 所指定的是何种数据,进而 Rust 才知道该怎样使用那个数据。接下来会看看两个数据类型的子集:标量(scalar)类型与复合(compound)类型。 + +请牢记 Rust 是门 *静态类型(statically typed)* 语言,这就意味着在运行时,他必须清楚所有变量的类型。基于值与对变量的使用方式,编译器通常可以推断出希望变量使用何种类型来。在可能有许多中类型的情况下,就如同第 2 章 [将猜数与秘密数字比较](Ch02_Programming_a_Guessing_Game.md#compring-the-guess-to-the-secret-number) 小节中,使用 `parse` 把一个 `String` 转换成数字类型时,就必须添加一个类型注释,如下面这样: + +```rust +let guess: u32 = "42".parse().expect("这不是个数字!"); +``` + +若这里添加类型注解,那么 Rust 就会给出下面的错误,表示编译器需要更多信息来明白这里想要使用何种类型: + +```console +$ cargo build  101 ✘ + Compiling variables v0.1.0 (/home/peng/rust-lang/projects/variables) +error[E0282]: type annotations needed + --> src/main.rs:19:9 + | +19 | let guess = "42".parse().expect("那不是个数字!"); + | ^^^^^ consider giving `guess` a type + +For more information about this error, try `rustc --explain E0282`. +static HELLO_WORLD: &str = "你好,世界!"; + +fn main() { + println! ("名字为:{}", HELLO_WORLD); +}rror: could not compile `variables` due to previous error +``` + +接下来就会见识到其他数据类型的类型注解。 + + +## 标量类型(Scalar Types) + +*标量* 类型,表示单个值。Rust 有着四个主要的标量类型:整数、浮点数、布尔值与字符。这些类型,其他语言也有。下面就深入看看他们在 Rust 中是怎样工作的。 + +### 整形(Integer Types) + +*整数* 是不带小数部分的数。在第 2 章中就已用到一种整数类型,即 `u32` 类型。这种类型声明表示变量关联的值,应是个无符号的、占据 32 个二进制位空间的整数(有符号整数以 `i` 而不是 `u` 开头)。下面的表 3-1 给出了 Rust 中内建的那些整数类型。可使用这些变种中的任何一个,取声明出某个整数值的类型。 + +*表 3-1:Rust 中的整数类型* + + +| 长度 | 有符号 | 无符号 | +| :-: | :- | :- | +| 8 位 | `i8` | `u8` | +| 16 位 | `i16` | `u16` | +| 32 位 | `i32` | `u32` | +| 64 位 | `i64` | `u64` | +| 128 位 | `i128` | `u128` | +| 架构决定 | `isize` | `usize` | + +这每个变种,都可以是有符号或无符号的,同时有着显式的大小(二进制位数)。 *有符号* 与 *无符号* 是该数字是否可以是负数--也就是说,该数是否需带有符号(即有符号的),或者说他是否将只为正数,而因此仅能被不带符号的表示出来(即无符号)。这与在纸上写数字相像:在符号重要时,那么写下来的数字就会带有加号或减号;不过在可安全地假定数字为正数时,写下的数字就不带符号了。有符号数字,是采用 [二进制补码](https://en.wikipedia.org/wiki/Two%27s_complement) 表示法加以存储的。 + +每个有符号变种,都可存储自 `-(2^n-1)` 到 `2^n-1` 范围的那些数字(包括边界上的两个数字),其中的 `n` 即为变种用到的位数。那么一个 `i8` 就可以存储 从`-(2^7)` 到 `2^7-1` 的那些数字了,相当于 `-128` 到 `127`。 + +无符号变种则可以存储 `0` 到 `2^n - 1` 的数字,因此一个 `u8` 可以存储 `0` 到 `2^8 - 1` 的数字,相当于 `0` 到 `255`。 + +此外,其中的 `isize` 与 `usize` 类型,取决于程序所运行计算机的架构,这在上面的表格中,是以 `arch` 表示的:若在 `64-bit` 机器上那么就是 64 位,而若在 `32-bit` 机器上,那么就是 32 位。 + +可使用上面表 3-2 中的任何形式,来编写整数字面值(integer literals)。请注意数字字面值是可以将多种数字类型,作为类型后缀(a type suffix),而指定出该数字的类型的,比如 `57u8`。数字字面值中还可以使用 `_` 作为视觉分隔符,从而让数字更易于阅读,比如 `1_234_456_789_012`,这与指明 `123456789012` 有着同样的值。 + +*表 3-2:Rust 中的数字字面值* + +| 数字字面值 | 示例 | +| :- | :- | +| 十进制(Decimal) | `98_222` | +| 十六进制(Hex) | `0xff` | +| 八进制(Octal) | `0o77` | +| 二进制(Binary) | `0b1111_0000` | +| 字节(仅限 `u8`,Byte(`u8` only)) | `b'A'` | + +那么怎样知道,该用何种类型的整数呢?在不确定的时候,一般来说 Rust 默认的整数类型,即是不错的开场:整数类型默认为 `i32`。而要用到 `isize` 或 `usize` 的主要场合,则是在对一些类别的集合进行索引的时候(the primary situation in which you'd use `isize` or `usize` is when indexing some sort of collection)。 + +> 关于 **整数溢出** +> +> 比方说有个类型为 `u8` 的、可保存 `0` 到 `255` 之间值的变量。在尝试将该变量修改为超出那个范围的某个值,比如 `256` 时,就会发生 *整型溢出(integer overflow)*,而整型溢出又可导致两种行为之一。在以调试模式进行程序编译时,Rust 就会包含整数溢出的检查,在发生了整数溢出时,就会导致程序进入 *错误(panic)* 状态。对于程序因错误而退出执行这种情况,Rust 使用了 猝死(paniking) 这个词语;在第 9 章中的 [带有 `panic!` 宏的不可恢复性错误](Ch09_Error_Handling.md#unrecoverable-errors-with-panic) 小节,将更深入地讨论到程序因错误而终止运行的情况。 +> +> 在以 `--release` 开关进行发布模式的编译时,Rust 就不会包含对引起程序终止运行的整数溢出的检查。这时若发生了溢出,Rust 就会执行 *二进制补码封装(two's complement wrapping)*。简而言之,对于比那种类型能保存的最大值还要大的值,就会被“回卷(wrap around)”到那种类型所能保存的最小值。以 `u8` 为例,值 `256` 就变成了 `0`,值 `257` 就变成了 `1`,如此等等。这样程序就不会死掉,而那个变量则会有着一个或许不是所期望的值。对整数溢出的回卷行为的依赖,被视为一种错误(Relying on integer overflow's wrapping behavior is considered an error)。 +> +> 要显式地处理可能的溢出,就要用到标准库为原生数字类型所提供的以下方法族(these families of methods provided by the standard library for primitive numeric types): +> +> - 以 `wrapping_*` 这些方法的所有模式的封装,比如 `wrapping_add`(wrap in all modes with the `wrapping_*` methods, such as `wrapping_add`); +> - 存在以 `checked_*` 方法的溢出时,返回 `None` 值(return the `None` value if there is overflow with the `checked_*` methods); +> - 返回该值,以及一个表示是否存在带有 `overflowing_*` 方法的溢出的布尔值(return the value and a boolean indicating whether there was overflow with the `overflow_*` methods); +> - 以 `saturating_*` 方法,实现该值的最小或最大值处饱和(saturate at the value's minimum or maximum values with `saturating_*` methods)。 + + +### 浮点类型 + +Rust 同样有两种原生的 *浮点数* 类型,所谓浮点数,是带有小数点的数字。Rust 的浮点数类型为 `f32` 与 `f64`,分别为 32 位及 64 位大小。由于在现代 CPU 上 `f64` 与 `f32` 处理速度大致一样,不过前者具备了更高的精度,因此默认类型就定为了 `f64`。两种浮点数类型都是有符号的。 + +下面的示例展示了具体的浮点数: + +文件名:`src/main.rs` + +```rust +fn main() { + let x = 2.0; // f64 + let y: f32 = 3.0; // f32 +} +``` + +浮点数的表示,符合 [IEEE-754 标准](https://standards.ieee.org/ieee/754/6210/)。`f32` 类型是单精度浮点数,而 `f64` 则是双精度的。 + +### 数字运算 + +Rust 支持在所有数字类型上、所期望的那些基本数学运算:加法、减法、乘法、除法,及余数。整数除法会向下取到最接近的整数结果。下面的代码展示了在 `let` 语句中,如何运用各种数字运算: + +文件名:`src/main.rs` + +```rust +fn main() { + + // 加法 + let sum = 5 + 10; + + // 减法 + let difference = 95.5 - 4.3; + + // 乘法 + let product = 4 * 30; + + // 除法 + let quotient = 56.7 / 32.2; + let floored = 2 / 3; // 结果为 0 + + // 余数 + let reminder = 43 % 5; + + println! (" + 5 + 10 = {}, + 95.5 - 4.3 = {} + 4 * 30 = {} + 56.7 / 32.2 = {} + 2 / 3 = {} + 43 % 5 = {}", sum, difference, product, quotient, floored, reminder); +} +``` + +这些语句中每个表达式都使用了一个数学运算符,并求到一个单值,该单值随后被绑定到变量。[附录 B](Ch99_Operators.md) 包含了 Rust 所提供的全部运算符。 + +### 布尔值类型 + +与多数其他编程语言中一样,Rust 中的布尔值类型也有两个可能的值:`true` 及 `false`。布尔值大小为一个字节。Rust 中的布尔值类型,指定使用 `bool` 关键字。比如: + +文件名:`src/main.rs` + +```rust +fn main() { + let t = true; + + let f: bool = false; // 带有显式类型注解 +} +``` + +主要通过条件判断,来使用布尔值,比如在 `if` 表达式中。在 [控制流(Control Flow)](#control-flow) 小节,会讲到 Rust 中 `if` 表达式的工作方式。 + +#### 字符类型 + +Rust 的 `char` 类型,是这门语言最为原生的字母类型。下面就是一些声明 `char` 值的示例: + +文件名:`src/main.rs` + +```rust +fn main() { + let c = 'z'; + let z = 'ℤ'; + let heart_eyed_cat = '😻'; + + println! ("c 为 {}, z 为 {}, 爱心猫: {}", c, z, heart_eyed_cat); +} +``` + +请注意,相比使用双引号来给字符串字面值进行值的指定,这里是以单引号来对这些 `char` 的字面值进行指定的。Rust 的 `char` 类型,有着四个字节的大小,而表示了 Unicode 的标量值,这就意味着他可以表示比仅仅 ASCII 要多得多的符号。像是重音字母(accented letters);中文、日语、韩语等等;emoji 符号;以及那些零宽度空格等等,在 Rust 中都是有效的 `char` 取值。Unicode 标量值的范围,是从 `U+0000` 到 `U+D7FF`,及 `U+E000` 到 `U+10FFFF`,包含边界值。不过,“字符(character)” 并非 Unicode 中的概念,因此对 “字符” 为何物的主观认识,可能与 Rust 中 `char` 的本质有所差别。在第 8 章中的 [用字符串存储 UTF-8 编码的文本](Ch08_Strings.md#storing-utf-8-encoded-text-with-strings) 小节,将对此话题进行讨论。 + +## 复合类型 + +*复合类型(compound types)* 可将多个值组合成一个类型。Rust 有着两个原生的复合类型:元组与数组(tuples and arrays)。 + +### 元组类型 + +元组是将数个不同类型的值,组合成一个复合类型的一般方式。元组是固定长度的:一旦被声明出来,他们的大小就无法扩大或缩小了。 + +通过编写放在圆括号里面的、逗号分隔的值清单,来创建元组。元组中每个位置都有着一种类型,同时元组中不同值的类型不必一致。下面的示例中,加上了那些可选的类型注解: + +文件名:`src/main.rs` + +```rust +fn main() { + let tup: (i32, f64, u8) = (500, 6.4, 1); +} +``` + +由于元组被当作是单一复合元素,因此这里的变量 `tup` 绑定到了那整个的元组。要从元组获取到其单个值,就要使用模式匹配,来对元组值进行解构,就像下面这样: + +文件名:`src/main.rs` + +```rust +fn main() { + let tup = (500, 6.4, 1, "元组的最后一个元素"); + + let (x, y, z, a) = tup; + + println! ("a 的值为:{}", a); +} +``` + +这个程序首先创建了一个元组,并将其绑定到了变量 `tup`。随后以 `let` 关键字,使用了一个模式来取得 `tup`,并将其转换为四个独立变量,分别为 `x`、`y`、`z` 与 `a`。由于该操作将单个的元素,打散为了四个部分,因此称之为 *解构(destructuring)*。最后,程序打印出了 `a` 的值,即为 `元组的最后一个元素`。 + +还可以通过使用句点(`.`)后带上想要访问值的索引,还可直接对某个元组元素进行访问。比如: + +文件名:`src/main.rs` + +```rust +fn main() { + let x = (500, 6.4, 1); + + let five_hundred = x.0; + + let six_point_four = x.1; + + let one = x.2; + + println! ("x.0: {}, x.1:{}, x.2:{}", five_hundred, six_point_four, one); +} +``` + +此程序创建了元组 `x`,并随后通过使用各个元素的索引,而构造了他们的新变量。与绝大多数编程语言一样,元组的首个索引为 `0`。 + +没有任何值的元组,`()`,是种仅有一个值的特殊类型,同样写作 `()`。该类型叫做 *单元类型(unit type)*,同时这个值叫做 *单元值(unit value)*。那些不返回任何值的表达式,就会显式地返回这个单元值。 + + +### 数组类型 + +*数组(array)* 是拥有多个值集合的另一种方式。不同于元组,数组中的每个元素,都必须是同一类型。与其他一些语言中的数组不同,Rust 中的数组是定长的。 + +以方括号(`[]`)中逗号分隔的方式,来编写数组中个那些值: + +文件:`src/main.rs` + +```rust +fn main() { + let a = [1, 2, 3, 4, 5]; +} +``` + +在希望数据分配在栈而不是堆(在 [第 4 章](Ch04_Understanding_Ownership.md#what-is-ownership) 将进一步讨论栈与堆)上时,或希望确保一直有着固定数目的元素时,数组就派上用场了。然而,数组不如矢量类型灵活。矢量是标准库所提供的,一种类似的集合类型,其大小 *可以* 变大或缩小。在不确定要使用数组,还是要使用矢量类型时,那多半就应该使用矢量了。[第 8 章](Ch08_Common_Collections.md#vectors) 对矢量类型进行了更详细的讨论。 + +尽管如此,在了解了元素数目无需变化时,数组将更为有用。比如,在程序中正使用着各个月份名字时,由于是知道那将总是包含 12 个元素,因此就要使用数组而非矢量类型: + +```rust +let months = ["January", "February", "March", "April", "May", "June", "July", + "August", "September", "October", "November", "December"]; +``` + +数组类型的编写,是以方括弧(`[]`)括起来的,各个元素的类型,一个分号(`;`),和数组中元素个数的形式,像下面这样: + +```rust +let a: [i32, 5] = [-1, 0, 1, 2, 3]; +``` + +这里,`i32`就是各个元素的类型。在分号之后,数字 `5` 表示该数组包含五个元素。 + +还可以通过在方括弧(`[]`)中,先指定初始值,接着一个分号(`;`),及随后数组长度的方式,初始化出一个包含各个元素为同一个值的数组,如下所示: + +```rust +let a = [3; 5]; +``` + +名叫 `a` 这个这个数组,将包含 `5` 个元素都将被初始化设置为值 `3` 的元素。这与 `let a = [3, 3, 3, 3, 3];` 的写法是一样的,不过是一种更简洁的方式。 + +### 对数组元素的访问 + +一个数组,即是可分配在栈上的、已知及固定大小的一块内存。使用索引,就可以对数组的那些元素进行访问,比如下面这样: + +文件名:`src/main.rs` + +```rust +fn main() { + let a = [1, 2, 3, 4, 5]; + + let first = a[0]; + let last = a[a.len()-1]; + + println! ("数组的第一个元素:{},最后一个元素:{}", first, last); +} +``` + +在这个示例中,由于值 `1` 为该数组中索引为 `[0]` 处的值,因此名为 `first` 的元素将获取到值 `1`。而名为 `last` 的变量,将从数组中的索引 `[4]` 获取到值 `5`。 + +### 无效的数组元素访问 + +下来来看看,在尝试访问超出数组末端的数组元素时,会发生什么。就是说在运行下面这个程序时,与第二章中的猜数游戏类似,要从用户那里获取到一个数组索引: + +文件名:`src/main.rs` + +```rust +use std::io; +use std::process; + +fn main() { + let a = [1, 2, 3, 4, 5]; + + println! ("请输入一个数组索引。"); + + let mut index = String::new(); + + io::stdin() + .read_line(&mut index) + .expect("读取行失败"); + + let index: usize = match index.trim() + .parse() { + Ok(num) => num, + Err(_) => { + println! ("输入的索引并非数字"); + process::exit(0); + } + }; + + let element = a[index]; + + println! ( + "位于索引 {} 处的元素值为:{}", + index, element); +} +``` + +此代码会成功编译。而在使用 `cargo run` 运行此代码,并输入 `0`、`1`、`2`、`3` 或 `4` 时,程序将打印出该数组中对应与那个索引处的值。而若输入了一个超出数组末端的数字,比如 `10`,那么就会看到下面这样的输出: + +```console +thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:24:19 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +``` + +当在索引操作中使用了无效值的时间点,该程序造成了一个 *运行时(runtime)* 错误。该程序以一条错误消息退出,而并未执行那最后的 `println!` 语句。在尝试使用索引访问某个元素时,Rust 会就所指定的索引,小于数组长度进行检查。若该索引大于或等于数组长度,Rust 就会出错。此项检查必须要在运行时进行,尤其是在此示例中,这是因为编译器几无可能知道在用户随后运行此程序时,会输入什么样的值。 + +这是 Rust 内存安全准则的一个活生生的示例。在许多底层语言中,此种检查都未实现,进而在提供了不正确的索引时,就会访问到无效的内存。Rust 通过立即退出而不是允许这种无效内存访问并继续运行,而保护免于此类错误。第 9 章将对 Rust 的错误处理进行过多的讨论。 + +## 函数 + +函数遍布于 Rust 代码中。而那个这门语言中最重要函数之一:`main` 函数,也一早就见到过了,`main` 函数可是许多程序的入口点。通过那个还已见到过的 `fn` 关键字,就可以声明出新的函数来。 + +Rust 代码使用 *蛇形命名法(snake case)*,作为函数与变量命名的约定样式,以这种命名法,函数及变量名称中的全部字母都是小写的,同时用下划线来分隔单词。下面就是一个包含了示例函数定义的程序: + +文件名:`src/main.rs` + +```rust +fn main() { + println!("Hello, world!"); + + another_function(); +} + +fn another_function() { + println! ("另一函数。"); +} +``` + +这里通过敲入 `fn` 关键字,接着的是函数名字,以及一套圆括号(`()`),定义出了一个函数。而那一对花括弧(`{}`),则告诉编译器,函数体在哪里开始和结束。 + +通过敲入函数名字,接上一对圆括号(`()`),就可以对已定义好的函数进行调用。由于 `another_function` 在程序中定义过,因此就可以在 `main` 函数里头对其调用。请注意在源代码中,是在 `main` 函数 *之后* 定义的 `another_function`;原本也可以在 `main` 函数之前定义他。Rust 不会关心在何处定义函数,只要他们在某处有定义即可。 + +为进一步对 Rust 的函数加以探索,就来创建一个新的名为 `functions` 的二进制可执行项目。将这个 `another_function` 示例放在 `src/main.rs` 中并运行。就会看到如下的输出: + +```console +$ cargo run + Compiling functions v0.1.0 (/home/peng/rust-lang/projects/functions) + Finished dev [unoptimized + debuginfo] target(s) in 0.32s + Running `target/debug/functions` +Hello, world! +另一函数。 +``` + +> 注:二进制项目(a binary project),是与库源代码项目相对应的,可生成二进制可执行程序的项目。 + +这些代码行,是以他们出现在 `main` 函数中的顺序执行的。首先打印出的是 `Hello, world!` 消息,而随后 `another_function` 就被调用了,同时他的消息被打印出来。 + +### 参数 + +可将函数定义为有 *参数(parameters)*,所谓参数,就是作为函数签名一部分的一些特殊变量(which are special variables that are part of a function's signature)。在函数有着参数时,就可以提供到函数与这些参数对应的具体值。技术上讲,提供到函数的具体值叫做 *实参(arguments)*,不过在一般聊天中,人们会将 *形参(parameters)* 与 *实参(arguments)* 两个说法互换使用,既指函数定义中的变量,又表示调用函数时所传入的具体值。 + +在下面这个版本的 `another_function` 中,就要添加一个参数: + +文件名:`src/main.rs` + +```rust +fn main() { + another_function(-5); +} + +fn another_function(x: i32) { + println! ("x 的值为:{}", x); +} +``` + +试着运行这个程序;就会得到以下输出: + +```console +$ cargo run  ✔ + Compiling functions v0.1.0 (/home/peng/rust-lang/projects/functions) + Finished dev [unoptimized + debuginfo] target(s) in 0.48s + Running `target/debug/functions` +x 的值为:-5 +``` + +`another_function` 的声明,有着一个名为 `x` 的参数。`x` 的类型被指定为 `i32`。在将 `-5` 传入到 `another_function` 时,那个 `println!` 的宏,就将 `-5` 放在那个格式化字符串中两个花括号所在的地方。 + +在函数签名中,*必须* 声明各个参数的类型。这是 Rust 设计中深思熟虑的决定:在函数定义中要求类型注解,就意味着编译器几近无需在代码中的什么地方使用那些函数的情况下,就能搞清楚是要何种类型(requiring type annotations in function definitions means that the compiler almost never needs you to use them elsewhere in the code to figure out what type you mean)。 + +在定义多个参数时,要用逗号(`,`)将那些参数声明分隔开,像下面这样: + +文件名:`src/main.rs` + +```rust +fn main() { + print_labeled_measurement(5, 'h'); +} + +fn print_labeled_measurement(value: i32, unit_label: char) { + println! ("度量值为:{}{}", value, unit_label); +} +``` + +此示例创建了一个名为 `print_labeled_measurement`的、有两个参数的方法。第一个参数名为 `value`,且类型为 `i32`。第二个名为 `unit_label`,同时类型为 `char`。该函数随后会打印出同时包含 `value` 与 `unit_label` 的文本。 + +来尝试运行此代码。将`functions` 项目中的 `src/main.rs` 中的当前程序,用上面的示例进行替换,并使用 `cargo run` 运行当前程序: + +```console +$ cargo run  ✔ + Compiling functions v0.1.0 (/home/peng/rust-lang/projects/functions) + Finished dev [unoptimized + debuginfo] target(s) in 0.45s + Running `target/debug/functions` +度量值为:5h +``` + +由于这里以 `5` 作为 `value` 的值,以 `h` 作为 `unit_label` 的值,调用了这个函数,因此该程序的输出,就包含了这些值。 + +### 语句及表达式 + +函数体是由一系列语句构成,这些语句可以是表达式结束的,也可以不是。到目前为止,所讲到的函数,都没有包含语句以表达式结束,不过有见到过表达式作为语句一部分的情况。由于 Rust 是基于表达式的语言,那么这一点就很重要,是要掌握的特征。其他语言并无这同样的特征,因此接下来就要看看语句和表达式究竟是何物,以及他们对函数体影响的不同。 + +*语句(statements)* 是一些完成某些操作而不返回值的指令。 *表达式(expressions)* 会求得一个结果值。来看看一些示例。 + +这里事实上已经用到了语句和表达式。创建一个变量,并以 `let` 关键字将一个值指派给他,就是一条语句。下面的清单 3-1 中,`let y = 6;` 就是一条语句。 + +文件名:`src/main.rs` + +```rust +fn main() { + let y = 6; +} +``` + +*清单 3-1:包含一条语句的一个 `main` 函数* + +函数定义也是语句;上面的整个示例本身就是一条语句。 + +语句不会返回值。因此就无法将一条 `let` 语句,指派给另一变量了,就如同下面代码尝试完成的那样;这就会得到一条错误消息: + +文件名:`src/main.rs` + +```rust +fn main() { + let x = (let y = 6); +} +``` + +当运行这个程序时,将收到的错误如下所示: + +```console +$ cargo run  ✔ + Compiling functions v0.1.0 (/home/peng/rust-lang/projects/functions) +error: expected expression, found statement (`let`) + --> src/main.rs:2:14 + | +2 | let x = (let y = 6); + | ^^^^^^^^^ + | + = note: variable declaration using `let` is a statement + +error[E0658]: `let` expressions in this position are unstable + --> src/main.rs:2:14 + | +2 | let x = (let y = 6); + | ^^^^^^^^^ + | + = note: see issue #53667 for more information + +warning: unnecessary parentheses around assigned value + --> src/main.rs:2:13 + | +2 | let x = (let y = 6); + | ^ ^ + | + = note: `#[warn(unused_parens)]` on by default +help: remove these parentheses + | +2 - let x = (let y = 6); +2 + let x = let y = 6; + | + +For more information about this error, try `rustc --explain E0658`. +warning: `functions` (bin "functions") generated 1 warning +error: could not compile `functions` due to 2 previous errors; 1 warning emitted +``` + +其中的 `let y = 6` 语句不会返回值,因此这里就没有任何东西给 `x` 绑定。这不同于其他语言所发生的事情,譬如 C 和 Ruby 等,在其他语言中,赋值操作返回的是所赋的那个值。在那些语言中,就可以写下 `x = y = 6`,而让 `x` 与 `y` 同时有了值 `6`;但在 Rust 中却不是这样的。 + +表达式会求解为一个值,进而构成往后编写的 Rust 代码的绝大部分。设想一个数学运算,比如 `5 + 6`,这就是个将求值为值 `11` 的表达式。 + +表达式可作为语句的一部分:在清单 3-1 中,语句 `let y = 6;` 里的 `6` 就是一个求值到 `6` 的表达式。对函数的调用,同样是个表达式。对宏的调用,也是个表达式。以花括号创建出的新代码块,还是个表达式,比如: + +文件名:`src/main.rs` + +```rust +fn main() { + let y = { + let x = 3; + x + 1 + }; + + println! ("y 的值为:{}", y); +} +``` + +其中的这个表达式: + + ```rust +{ + let x = 3; + x + 1 +} +``` + +在这个示例中,就是一个求值为 `4` 的表达式。其求得的值 `4` 会作为那条 `let` 语句的一部分,被绑定到 `y`。请注意那代码块最后的 `x + 1` 的代码行,并没有分号(`;`),而与到目前为止所见到的大多数代码行不同。表达式并不包含最后的分号。若将分号家到表达式末端,就会将其变成一条语句,进而就不再返回值了。在接下来对函数返回值与表达式的探索过程中,请牢记这一点。 + +> 注:若在上面代码块中的 `x + 1` 后面加上分号,那么 `y` 的值将为 `()` 这一特殊值(类似于 `null`)。进而在接下来的 `println!` 语句中导致出错。 + + +### 有返回值的函数 + +函数可以将值返回给调用他们的代码。在函数有值要返回时,不会就这些返回值命名,但必须在箭头(`->`)后面,声明这些值的类型。在 Rust 中,函数的返回值,与函数体代码块的最后那个表达式的值,二者等价。通过使用 `return` 关键字并指定一个值,即可尽早地给函数返回值,不过大多数函数,都显式地返回最后的那个表达式。下面就是返回值的一个函数示例: + +> 注:关键字 `return` 的使用,标志着函数体的结束,`return` 语句之后的代码,将不再执行。 + +文件名:`src/main.rs` + +```rust +fn five() -> u32 { + 5 +} + +fn main() { + let x = five(); + + println! ("x 的值为:{}", x); +} +``` + +在那个 `five` 函数中,没有任何函数调用、宏、或者甚至 `let` 语句 -- 只是那个数字 `5` 自己。在 Rust 中这是个完全有效的函数。请注意该函数的返回值类型,也是以 `-> u32` 的形式指定了的。尝试运行此代码;输出应像下面这样: + +```console +$ cargo run  ✔ + Compiling functions v0.1.0 (/home/peng/rust-lang/projects/functions) + Finished dev [unoptimized + debuginfo] target(s) in 0.45s + Running `target/debug/functions` +x 的值为:5 +``` + +函数 `five` 中的 `5` 即是该函数的返回值,这就是为何返回值类型为 `u32` 的原因。下面来更深入地检视一下。其中有两个重点:首先,代码行`let x = five();` 表明这里使用了某个函数的返回值,来对一个变量进行初始化。由于函数 `five` 返回了一个 `5`,因此那行代码就跟下面的相同: + +```rust +let x = 5; +``` + +其次,函数 `five` 没有参数,并定义了返回值类型,而其函数体只是个孤零零的、不带分号的 `5`,这是由于这个不带分号的 `5`,是个要将其值加以返回的表达式(注:若加上分号,那么就会变成一个语句,返回的将是特殊值 `()`,返回值类型将不再是 `u32`,从而导致编译时错误......)。 + +下面来看看另一个示例: + +文件名:`src/main.rs` + +```rust +fn main() { + let x = plus_one(-1); + + println! ("x 的值为:{}", x); +} + +fn plus_one(x: i32) -> i32 { + x + 1; +} +``` + +对这段代码进行编译,会产生一条错误,如下所示: + +```console +$ cargo run  ✔ + Compiling functions v0.1.0 (/home/peng/rust-lang/projects/functions) +error[E0308]: mismatched types + --> src/main.rs:7:24 + | +7 | fn plus_one(x: i32) -> i32 { + | -------- ^^^ expected `i32`, found `()` + | | + | implicitly returns `()` as its body has no tail or `return` expression +8 | x + 1; + | - help: consider removing this semicolon + +For more information about this error, try `rustc --explain E0308`. +error: could not compile `functions` due to previous error +``` + +主要错误消息为,“mismatched types,”,该消息表明了此代码的核心问题。函数 `plus_one` 的定义是说他将返回一个 `i32`,然而函数体的语句并未求解到一个值来,求解到的是一个以 `()` 表示的单元类型(the unit type)。因此,就什么也没返回,这是与函数定义相矛盾的,进而导致了一个错误。在此输出中,Rust 提供了一条或许有助于纠正此问题的消息:他建议移除那个分号,那样就会修正该错误。 + +## 注释 + +所有程序员都会致力于让他们的代码易于理解,而有时是需要额外解释的。在这种情况下,程序员们就会在他们的源代码中,留下会被编译器忽略的 *注释(comments)*,而那些阅读到源代码的人会发现有用。 + +下面就是个简单的注释: + +```rust +// hello, world +``` +在 Rust 中,惯用的注释风格,是以双斜杠来开始一条注释,同时该注释会持续到那行的结束。对于那些超过一行的注释,在每行注释就都要包含 `//`,就像这样: + +```rust +// 那么这里要编写一些复杂的注释,这注释长到要用多个行 +// 才能写完!噢!还好,这条注释会解释接下来要做些什么。 +``` + +注释也可以放在那些包含代码行的末尾: + +文件名:`src/main.rs` + +```rust +fn main() { + let lucky_number = 7; // 今天我感受到了好运 +} +``` + +不过更常见的则是以下面这种形式运用的注释,其中注释位处单独的、在其要注解代码之上的行: + +文件名:`src/main.rs` + +```rust +fn main() { + // 今日感到幸运 + let lucky_number = 7; +} +``` + +Rust 还有另外一种注释,叫做文档注释,在第 14 章的 [将代码箱发布到 Crates.io](Ch14_More_about_Cargo_and_Crates.io.md#publishing-a-crate-tocrates-io) 中会对文档注释进行讨论。 + +## 控制流程(Control Flow) + +根据条件是否为真,来运行某些代码,或者在条件为真时重复运行某些代码的能力,是绝大多数语言的根基。实现Rust代码执行流程控制最常见的结构,即是 `if` 表达式和循环。 + +### `if` 表达式 + +`if` 表达式实现了根据条件对代码进行分支。提供到一个条件,然后就表明,“在该条件满足时,运行这个代码块。在条件不满足时,就不要运行这个代码块。” + +请在 `projects` 目录下,创建一个新的、名为 `branches` 的项目,来探索这个 `if` 表达式。在 `src/main.rs` 文件中,输入以下代码: + +文件名:`src/main.rs` + +```rust +fn main() { + let number = 3; + + if number < 5 { + println! ("条件为真"); + } else { + println! ("条件为假"); + } +} +``` + +全部 `if` 表达式,都是以关键字 `if` 开头的,接着的是一个条件。在此示例中,那个条件就变量 `number` 是否小于 `5` 进行检查。是把要在条件为真时立即执行的代码块,放在条件之后、一对花括号里头。`if`表达式中与那些条件相关联的代码块,有时也叫做 *支臂(arms)*,这与在第 2 章的 [将猜数与秘密数字比较](Ch02_Programming_a_Guessing_Game.md#comparing-the-guess-to-the-secret-number) 小节中讨论过的 `match` 表达式中的支臂一样。 + +作为可选项,还可以包含一个 `else` 表达式,即这里做的那样,从而给到程序一个替代性的、将在条件求解结果为 `false` 时执行的代码块。在未提供`else`表达式,且条件为 `false` 时,程序将直接跳过那个 `if` 代码块,而前往接下来的代码处。 + +尝试运行此代码;将看到下面的输出: + +```console +$ cargo run  ✔ + Compiling branches v0.1.0 (/home/peng/rust-lang/projects/branches) + Finished dev [unoptimized + debuginfo] target(s) in 0.48s + Running `target/debug/branches` +条件为真 +``` + +下面来试着将 `number` 的值修改为一个令到该条件为 `false` 的值,看看会发生什么: + +```rust + let number = 7; +``` + +再运行这个程序,然后看看输出: + +```console +$ cargo run  1 ✘ + Compiling branches v0.1.0 (/home/peng/rust-lang/projects/branches) + Finished dev [unoptimized + debuginfo] target(s) in 0.45s + Running `target/debug/branches` +条件为假 +``` + +还值得注意的是,此代码中的条件 *必须* 是个 `bool` 类型。在条件不是 `bool` 类型时,就会收到错误。比如,尝试运行下面的代码: + +文件名:`src/main.rs` + +```rust +fn main() { + let number = 3; + + if number { + println! ("数字是 3"); + } +} +``` + +这次的 `if` 条件求解为一个 `3` 的值,进而 Rust 抛出一个错误: + +```console +$ cargo run  ✔ + Compiling branches v0.1.0 (/home/peng/rust-lang/projects/branches) +error[E0308]: mismatched types + --> src/main.rs:4:8 + | +4 | if number { + | ^^^^^^ expected `bool`, found integer + +For more information about this error, try `rustc --explain E0308`. +error: could not compile `branches` due to previous error +``` + +该错误表明 Rust 期望得到一个 `bool` 值但得到的是个整数。与诸如 Ruby 和 JavaScript 那样的语言不同,Rust 不会自动将非布尔值转换为布尔值。必须显式地且一直提供给 `if` 一个布尔值作为其条件。比如希望那个 `if` 代码块,仅在某个数字不等于 `0` 的时候运行,那么就可以将这个 `if` 表达式修改为下面这样: + +文件名:`src/main.rs` + +```rust +fn main() { + let number = 3; + + if number != 0 { + println! ("数字为非零数"); + } +} +``` + +运行此代码,就会打印出 `数字为非零数`。 + + +### 用 `else if` 来处理多个条件 + +通过在 `else if` 表达式中,结合 `if` 和 `else`,就可以使用多个条件。比如: + +文件名:`src/main.rs` + +```rust +fn main() { + let number = 6; + + if number % 4 == 0 { + println! ("数字可被 4 整除"); + } else if number % 3 == 0 { + println! ("数字可被 3 整除"); + } else if number % 2 == 0 { + println! ("数字可被 2 整除"); + } else { + println! ("数字不可被 4、3 或 2 整除"); + } +} +``` + +此程序有着其可接收的四个可能路径。在运行他时,就会看到下面的输出: + +```console +$ cargo run  101 ✘ + Compiling branches v0.1.0 (/home/peng/rust-lang/projects/branches) + Finished dev [unoptimized + debuginfo] target(s) in 0.45s + Running `target/debug/branches` +数字可被 3 整除 +``` + +在该程序执行时,就会依次检查各个 `if` 表达式,并执行那第一个条件成立的代码体。请注意即便 `6` 是可被 `2` 整除的,却并未看到输出 `数字可被 2 整除`,也没看到那个 `else` 代码块的 `数字不能被 4、3 或 2 整除` 文字。这是由于 Rust 只执行了第一个为真条件下的代码块,而一旦他发现了一个,就在不会检查剩下的那些条件了。 + +使用太多的 `else if` 表达式,就会让代码杂乱无章,因此在有多于一个这样的表达式时,或许就应对代码进行重构了。第 6 章描述了针对这样情况的一种强大的 Rust 分支结构,名为`match` 模式匹配。 + +### 在 `let` 语句中使用 `if` 关键字 + +由于 `if` 是个表达式,那么就可以在 `let` 表达式的右边使用他,来将其结算结果,赋值给某个变量,如下面的清单 3-2 所示: + +文件名:`src/main.rs` + +```rust +fn main() { + let condition = true; + + let number = if condition { 5 } else { 6 }; + + println! ("number 的值为:{}", number); +} +``` + +*清单 3-2:将`if` 表达式的结果赋值给某个变量* + +其中的 `number` 变量,就会被绑定到那个 `if` 表达式的计算结果上。运行此代码看看会发生什么: + +```console +$ cargo run  ✔ + Compiling branches v0.1.0 (/home/peng/rust-lang/projects/branches) + Finished dev [unoptimized + debuginfo] target(s) in 0.45s + Running `target/debug/branches` +number 的值为:5 +``` + +请记住代码块会求解到其中最后一个表达式的值,且数字本身也就是表达式。在此示例中,整个 `if` 表达式的值,是取决于会执行到哪个代码块的。这就意味着那些该 `if` 表达式各个支臂的、具备作为 `if` 表达式运算结果的那些值,必须要是相同类型;在清单 3-2 中,`if` 支臂和 `else` 支臂的运算结果,就都是 `i32` 类型的整数。若这些类型不匹配,就如下面的示例中那样,则会收到错误: + +文件名:`src/main.rs` + +```rust +fn main() { + let condition = true; + + let number = if condition { 5 } else { "six" }; + + println! ("number 的值为:{}", number); +} +``` + +在尝试编译这段代码时,就会收到错误。其中的 `if` 与 `else` 支臂的值类型不兼容,同时 Rust 还准确标明了在程序中何处发现的该问题: + +```console +$ cargo run  ✔ + Compiling branches v0.1.0 (/home/peng/rust-lang/projects/branches) +error[E0308]: `if` and `else` have incompatible types + --> src/main.rs:4:44 + | +4 | let number = if condition { 5 } else { "six" }; + | - ^^^^^ expected integer, found `&str` + | | + | expected because of this + +For more information about this error, try `rustc --explain E0308`. +error: could not compile `branches` due to previous error +``` + +`if` 代码块中的表达式,求解为整数,而`else` 代码块中的表达式求解为了字符串。由于变量必须有着单一类型,且 Rust 需要知道在运行时变量 `number` 的类型是什么,那么显然这代码是不会工作的。清楚 `number` 的类型,就允许编译器在所有用到 `number` 的地方,验证其类型的有效性。而如果只有在运行时才确定出 `number` 的类型,那么 Rust 就无法做到这一点;若编译器务必要对全部变量的多个假定类型进行跟踪,那么编译器就会更为复杂,且做到更少代码保证。 + +## 循环下的重复 + +多次执行某个代码块常常是有用的。对于这类任务,Rust 提供了数种 *循环(loops)*,所谓循环,是指会贯通执行循环体里头的代码到结束,并随后立即回到开头开始执行。首先构造一个名为 `loops` 的新项目,来进行这些循环的实验。 + +Rust 有着三种循环:`loop`、`while` 及 `for`。接下来就要各个进行尝试。 + +### 用 `loop` 关键字对代码进行重复 + +`loop` 关键字告诉 Rust 去一直一遍又一遍执行代码块,抑或直到显式地告诉他停下来为止。 + +作为示例,将 `loops` 目录中的 `src/main.rs` 文件修改为下面这样: + +文件名:`src/main.rs` + +```rust +fn main() { + loop { + println! (”再次!“); + } +} +``` + +在运行这个程序时,就会看到一遍又一遍地持续打印出 `再次!`,知道手动停止这个程序为止。大多数终端程序,都支持键盘快捷键 `ctrl-c` 来中断某个卡在无尽循环中的某个程序。来尝试一下: + +```console +$ cargo run + Compiling loops v0.1.0 (file:///projects/loops) + Finished dev [unoptimized + debuginfo] target(s) in 0.29s + Running `target/debug/loops` +再次! +再次! +再次! +再次! +^C再次! +``` + +其中的符号 `^C` 表示按下 `ctrl-c` 的地方。在那个 `^C` 之后,可能会也可能不会看到 `再次!` 被打印出来,取决于程序接收到中断信号时,代码在循环中的何处。 + +幸运的是,Rust 还提供了一种运用代码来跳出循环的方式。可在循环中放置 `break` 关键字,而告诉程序在何时结束执行这个循环。还记得在第 2 章的 [猜对数字后退出程序](Ch02_Programming_a_Guessing_Game.md#quitting-after-a-correct-guess) 小节,就在那个猜数游戏中这样做了,在通过猜到正确数字而赢得游戏时退出那个程序。 + +在那个猜数游戏中,还使用了 `continue` 关键字,循环中的 `continue` 关键字,告诉程序去跳过循环本次迭代的其余全部代码,而前往下一次迭代。 + +在有着循环里头的循环时,那么 `break` 与 `continue` 就会应用在他们所在点位处的最内层循环(if you have loops within loops, `break` and `continue` apply to the innermost loop at that point)。可选择在某个循环上指定一个 *循环标签(loop label)*,这样就可以与 `break` 或 `continue` 结合使用,来指明这些关键字是要应用到打上标签的循环,而不再是那最里层的循环了。下面就是一个有着两个嵌套循环的示例: + +```rust +fn main() { + let mut count = 0; + + 'counting_up: loop { + println! ("计数 = {}", count); + let mut remaining = 10; + + loop { + println! ("剩余 = {}", remaining); + if remaining == 9 { + break; + } + if count == 2 { + break 'counting_up; + } + remaining -= 1; + } + + count += 1; + } + + println! ("最终计数 = {}", count); +} +``` + +其中的外层循环有着标签 `'counting_up`,同时他将从 `0` 计数到 `2`。而其中的内层循环不带标签,会从 `10` 计数到 `9`。其中的第一个未指定标签的 `break` 语句,将只会退出那个内部循环。而那个 `break 'counting_up;` 语句,则会将外层循环退出。此代码会打印出: + +```console +$ cargo run  INT ✘ + Compiling loops v0.1.0 (/home/peng/rust-lang/projects/loops) + Finished dev [unoptimized + debuginfo] target(s) in 0.18s + Running `target/debug/loops` +计数 = 0 +剩余 = 10 +剩余 = 9 +计数 = 1 +剩余 = 10 +剩余 = 9 +计数 = 2 +剩余 = 10 +最终计数 = 2 +``` + +### 自循环返回值 + +**Returning Values from Loops** + +`loop` 的一个用途,即是对一个明知会失败的操作进行重试,比如检查某个线程是否已完成他的作业。还可能需要将那个操作的结果,从循环传出来给代码的其余部分。要实现这一点,可将想要返回的值,添加在用于停止该循环的 `break` 表达式之后;那个值就会被返回到该循环的外面,进而就可以用到那个值了,如下所示: + +```rust +fn main() { + let mut counter = 0; + + let result = loop { + counter += 1; + + if counter == 10 { + break counter * 2; + } + }; + + println! ("结果为:{}", result); +} +``` + +在这个循环之前,这里声明了一个名为 `counter` 的变量,并将其初始化为 `0`。随后声明了一个名为 `result` 变量,来保存从该循环所返回的值。在该循环的每次迭代上,是要给 `counter` 变量加上 `1` 的,并随后检查那个计数器是否等于 `10`。在计数器等于 `10` 的时候,就使用有着值 `counter * 2` 的 `break` 关键字。在该循环之后,使用了一个分号来结束将值 `counter * 2` 赋值给 `result` 的那个语句。最后,这里打印出了在 `result` 里的值,即这个示例中的 `20`。 + +### 使用 `while` 的条件循环 + +程序经常会对循环里的条件进行求值。当条件为真时,该循环就运行。在条件不再为真时,程序就调用 `break`,把循环停下来。使用 `loop`、`if`、`else` 与 `break` 来实现与此相似的行为,是可能的;若愿意这样做,现在就可以在程序中尝试一下。不过由于这种模式如此常见,以至于 Rust 为此而有了一个内建的语言结构,那就是叫做 `while` 的循环。在下面的清单 3-3 中,就使用了 `while` 来将该程序循环三次,每次都倒计数,并随后在循环结束之后,打印出一条消息而退出。 + +```rust +fn main() { + let mut number = 3; + + while number != 0 { + println! ("{}!", number); + + number -= 1; + } + + println! ("点火!!"); +} +``` + +*清单 3-3:使用 `while` 循环在条件保持为真期间运行代码* + +此代码结构,消除了使用 `loop`、`if`、`else`、及 `break` 实现同样结构时,很多不可缺少的嵌套,且此结构更为清晰。在条件保持为真期间,代码就会运行;否则,他将退出循环。 + + +### 使用 `for` 对集合进行遍历 + +可选择使用 `while` 结构,来对集合,诸如数组,的那些元素进行循环。作为示例,下面清单 3-4 中的循环,将打印出数组 `a` 中的各个元素。 + +文件名:`src/main.rs` + +```rust +fn main() { + let a = [10, 20, 30, 40, 50]; + + let mut index = 0; + + while index < a.len() { + println! ("值为:{}", a[index]); + + index += 1; + } +} +``` + +*清单 3-4:使用 `while` 循环遍历集合的各个元素* + +这个程序里,代码会根据那个数组中的元素,往上计数。是以索引 `0` 开始,然后循环,直到循环到了数组中最后的那个索引(即,在 `index < 5` 不再为 `true` 时)。运行此代码将打印出数组中的所有元素: + +```console +$ cargo run  ✔ + Compiling loops v0.1.0 (/home/peng/rust-lang/projects/loops) + Finished dev [unoptimized + debuginfo] target(s) in 0.17s + Running `target/debug/loops` +值为:10 +值为:20 +值为:30 +值为:40 +值为:50 +``` + +全部五个数组值都会出现在终端里,跟预期一样。尽管 `index` 在某个时间点达到值 `5`,但该循环会在尝试从那个数组获取第六个值之前,就停止执行。 + +但这种方法易于出错;在索引值或测试条件不正确时,就会导致该程序出错。比如,若把数组 `a` 的定义修改为有四个元素,而忘记了将那个条件更新到 `while index < 4`,此代码就会出错。由于编译器增加了在那个循环过程中,每次迭代上用于执行对 `index` 是否在数组边界内的,条件检查时间,因此这种方法还慢。 + +作为一种位为简练的替代,就可使用 `for` 循环而对集合中的各个元素,执行一些代码。`for` 循环看起来就跟下面清单 3-5 中的代码一样: + +文件名:`src/main.rs` + +```rust +fn main() { + let a = [10, 20, 30, 40, 50]; + + for element in a { + println! ("值为:{}", element); + } +} +``` + +*清单 3-5:使用 `for` 循环对结合的各个元素进行遍历* + + +在运行这段代码时,将看到与清单 3-4 中同样的输出。更重要的是,现在业已提升了代码的安全性,并消除了可能因超出那个数组末端,或因索引未足够触及而遗失掉一些数组项目,而导致的代码错误。 + +使用这个 `for` 循环,在更改了那个数组中值的个数时,就无需记得,像清单 3-4 中所使用的方式那样,去修改任何其他代码。 + +`for` 循环的安全与简洁,使得他们成为了 Rust 中最常用的循环结构。即使在那种要以确切次数来运行某些代码的情形下,如同清单 3-3 中用到 `while` 循环的倒计时示例,大多数 Rust 公民也将会使用 `for` 循环。要以确切次数运行某些代码,则要用到由标准库提供的 `Range` 特性了,`Range` 会依序生成自某个数字开始,并在另一数字之前结束,其间的全部数字来。 + +下面就是使用 `for` 循环,和另一个至今还未讲到的、用于逆转那个范围的 `rev` 方法,来实现那个倒计时的样子: + +文件名:`src/main.rs` + +```rust +fn main() { + for number in (1..4).rev() { + println! ("{}!", number); + } + + println! ("发射!!"); +} +``` + +此代码要更好一些,不是吗? + + +## 总结 + +咱们做到了!这第 3 章内容可真不少:在这里掌握了变量、标量与复合数据类型、函数、代码注释、`if`表达式,还有循环!请构建一些程序来完成下面这些事情,从而练习一下本章所讨论的那些概念: + +- 对法式温度和摄氏温度之间互相转换; +- 生成第 n 个斐波拉基数; +- 利用圣诞颂歌 “The Twelve Days of Christmas” 中的重复,而打印出这首颂歌的歌词来; + +在做好了继续新内容的学习后,就将要讨论到 Rust 中的一个在其他编程语言中并不多见的概念:所有权(ownership)。 + +## 练习答案 + + +
+ “法式温度与摄氏温度的转换” + +```rust +use std::io; +use std::process; + +fn fah_to_cels(f: f32) -> f32 { + return (f - 32.0) / 1.8; +} + +fn cels_to_fah(c: f32) -> f32 { + return c * 1.8 + 32.0; +} + +fn main() { + println! ("法式温度与摄氏温度之间的转换"); + + loop { + println! ("\n-----------------\n请选择: + '1'-摄氏温度/'2'-法式温度/'Q'/\"quit\" 退出程序。 + '1'/'2'/'Q'/\"quit\"[1]:"); + + let mut temp_type = String::new(); + + io::stdin() + .read_line(&mut temp_type) + .expect("读取输入失败!"); + + let temp_type = temp_type.trim(); + + if temp_type.eq("Q") || temp_type.eq("quit") { process::exit(0); } + + if ! temp_type.eq("1") && ! temp_type.eq("2") && ! temp_type.eq("") { + println! ("无效输入,请输入 '1'、'2'、'Q'、\"quit\",或直接按下回车键"); + continue; + } + + if temp_type.eq("1") || temp_type.eq("") { + println! ("请输入要转换的摄氏温度:"); + let temp = get_temp_input(); + + println! ("摄氏温度: {:.2}°C,约为法氏温度:{:.2}°F", temp, cels_to_fah(temp)); + } + + if temp_type.eq("2") { + println! ("请输入要转换的法氏温度:"); + let temp = get_temp_input(); + + println! ("法氏温度:{:.2}°F,约为摄氏温度:{:.2}°C", temp, fah_to_cels(temp)); + } + } +} + +fn get_temp_input() -> f32 { + return loop { + let mut tmp = String::new(); + + io::stdin() + .read_line(&mut tmp) + .expect("读取输入失败"); + + match tmp.trim().parse() { + Ok(num) => { break num }, + Err(_) => { + println! ("请输入一个浮点数,比如 -10.0, 15.6"); + continue + } + }; + }; +} +``` + +
+ + +
+ "生成第 n 个斐波拉基数" + + +```rust +use std::io; +use num_format::{Locale, ToFormattedString}; +// use std::process; + +fn nth_fibonacci(n: u64) -> u64 { + + if n == 0 || n == 1 { + return n; + } else { + return nth_fibonacci(n - 1) + nth_fibonacci(n - 2); + } +} + +fn main() { + println! ("找出第 n 个斐波拉基数"); + + 'main_loop: loop { + println! ("请输入 n: (Q/quit 退出程序)"); + + let n: u64 = loop { + let mut tmp = String::new(); + + io::stdin() + .read_line(&mut tmp) + .expect("读取输入失败!"); + + let tmp = tmp.trim(); + + if tmp.eq("Q") || tmp.eq("quit") { + // process::exit(0); + break 'main_loop; + } + + match tmp.parse() { + Ok(num) => { break num }, + Err(_) => { + println! ("请输入一个正整数!\n"); + continue; + }, + }; + }; + + println! ("第 {} 个斐波拉基数为:{}", + n, + nth_fibonacci(n).to_formatted_string(&Locale::en)); + } +} +``` + +
+ + +
+ "打印圣诞颂歌 ‘The Twelve Days of Christmas’ 歌词" + +```rust +fn main() { + let days = [ + "first", + "second", + "third", + "fourth", + "fifth", + "sixth", + "seventh", + "eighth", + "nineth", + "tenth", + "eleventh", + "twelfth" + ]; + let amounts = [ + "A", + "Two", + "Three", + "Four", + "Five", + "Six", + "Seven", + "Eight", + "Nine", + "Ten", + "Eleven", + "Twelve" + ]; + let things = [ + "partridge in a pear tree", + "turtle doves", + "French hens", + "calling birds", + "golden rings", + "geese-a-laying", + "swans-a-swimming", + "maids-a-milking", + "ladies dancing", + "lords-a-leaping", + "pipers piping", + "drummers drumming", + ]; + + for num in 1..=12 { + println! ("\nOn the {} day of Christmas,\nMy true love gave to me:", + days[num-1]); + for tmp in (0..num).rev() { + if tmp == 0 && num == 1 { + println! ("{} {}.", amounts[tmp], things[tmp]); + } + if tmp == 0 && num != 1 { + println! ("And {} {}.", amounts[tmp].to_lowercase(), things[tmp]); + } + if tmp != 0 { + println! ("{} {},", amounts[tmp], things[tmp]); + } + } + } +} +``` + +
diff --git a/src/Ch04_Understanding_Ownership.md b/src/Ch04_Understanding_Ownership.md new file mode 100644 index 0000000..0f3fa1a --- /dev/null +++ b/src/Ch04_Understanding_Ownership.md @@ -0,0 +1,1005 @@ +# 掌握 Rust 中的所有权 + +**Understanding Ownership** + +所有权,ownership 作为 Rust 最为独特的特性,而对这门语言其余部分有着深刻影响。正是所有权,使得 Rust 在无需垃圾收集器的情况下,保证了内存安全,因此掌握所有权的工作原理,就尤为重要。在这一章,将就所有权,以及几个与所有权有关的特性:借用、切片,以及 Rust 在内存中放置数据的方式等,进行讲解。 + +## 何谓所有权 + +*所有权* 是掌管着 Rust 程序管理内存方式的一套规则(*ownership* is a set of rules that governs how a Rust program manages memory)。所有程序在其运行期间,都必须管理其运用计算机内存的方式。一些语言有着伴随着其程序运行,而持续查找不再用到内存的垃圾回收;在别的一些语言中,程序员必须显式地分配和释放内存。Rust 采用了第三条路线:经由带有编译器会加以检查的一套规则的所有权系统,内存便得到了管理。在这些规则的任何一条被违反了时,程序就不会编译。所有权的任何一个特性,都不会在程序运行期间,拖慢程序运行速度。 + +由于对许多程序员来说,所有权都是个新概念,因此要些时间来习惯他。好消息则是随着对 Rust 与那些所有权系统规则的愈加熟练,那么就会发现,顺其自然地开发出安全且高效的代码,会变得越来越容易。请务必坚持下去! + +在掌握了所有权后,就会对那些令到 Rust 成为一门独特编程语言的特性,有扎实掌握。在本章中,将通过完成着重于甚为常见的一些数据结构:字符串的示例,而掌握到所有权。 + +> **内存栈与堆,the Stack and the Heap** +> +> 许多编程语言,都不要求进程考虑内存栈与堆。不过在像是 Rust 这样的系统编程语言中,某个值是在栈上还是在堆上,就会对语言的行为方式,造成影响,还会影响到不得不做出一些明确决定的理由。本章稍后将讲到的所有权的那些部分,是与内存栈和堆有关的,因此这里是关于他们的一点简要说明,作为预备知识。 +> +> 内存栈和堆,都属于在运行时代码可用内存的组成部分,但他们是以不同方式架构组织起来的。栈,the stack 以其收到值的顺序,保存着一些值,并以相反的顺序,将这些值移除。这被成为 *后进先出,last in, first out*。设想有一叠盘子:在添加更多盘子时,就要把新的盘子放在盘子堆顶上,而在要用个盘子时,就要从顶上拿。从底下或中间添加或拿走盘子,都是不行的!添加数据被称为 “压入栈,pushing onto the stack”,而移除数据被称为 *弹出栈,popping off the stack*。保存在栈上的数据,必须要有已知的、固定的大小。相反,那些运行时未知大小,或大小可能会变化的数据,就必须保存在堆上。 +> +> 内存堆的组织程度较低:在将数据放在堆上时,就要请求确切数量的空间。内存分配器会在堆上找到一处足够大的空白位点,将其标记为正在使用中,然后返回一个 *指针,pointer*,即那个点位的地址。此过程被称为 *堆上内存分配,allocating on the heap*,而有时会去掉“堆”,而简称为 *内存分配,allocating* (而将值压入到栈上,则不被视为内存分配)。由于到堆的指针是已知的、固定大小的,因此就可以将该指针存储在栈上,而在想要具体数据时,就必须依循该指针。请设想正坐在某个餐馆里。在进到餐馆时,就要报出跟你们组的人数,进而餐馆员工就会找出一张可以坐下所有人的空桌子,并把你们带过去。在你们组有人迟到时,他们就可以询问是坐在哪张桌子,而找到你们。 +> +> 由于在把数据压到栈上时,内存分配器绝不必搜寻一个位置来存储新数据,因此相比在堆上分配空间,把数据压入栈是要快得多的;存储新数据的地方,始终是在栈顶部。与此相比,在内存堆上分配空间则需要更多工作,由于内存分配器必须先找到一块足够大空间来保存该数据,并随后还要为准备好下一次内存分配,而完成对此次分配的登记。 +> +> 因为必须要循着某个指针去获取到数据,因此访问内存堆上的数据,与访问栈上的数据相比,也要慢一些。当较少地在内存中跳跃时,现代处理器会更快。延续上面的比喻,设想餐馆里的一名服务员,正在接收来自许多台餐桌的点餐。那么一次获取到一个桌子的全部点餐,再去往下一桌,无疑是最高效的。而从餐桌 A 拿到一份点餐,再从餐桌 B 拿到一份点餐,随后又从餐桌 A 拿到一份,然后又从餐桌 B 再拿到一份,这样无疑就是慢得多的过程了。经由同一令牌,如果处理器处理的数据与另一数据靠近(就像在栈上那样),而不是远离另一数据(就像在内存堆上可能的情形),那么处理器无疑会更好地完成他的工作。 +> +> 在代码对某个函数进行调用时,传入到该函数的值(潜在包含了指向内存堆上数据的指针),以及该函数的本地变量,都是被压入到栈上的。在该函数结束运行后,这些值就被从栈上弹出。 +> +> 对代码的哪些部分正在使用内存堆上的哪些数据进行追踪,最小化内存堆上的重复数据数量,以及对内存堆上的未使用数据进行清理而不至于耗尽内存空间等,都是所有权要解决的问题。一旦掌握了所有权,就再也不需要经常考虑栈和堆了,而清楚了所有权主要目的,是为着对内存堆进行管理,则会有助于解释所有权,为何会以他自己的方式运作。 + + +### 所有权规则 + +首先,来看看这些所有权规则。在完成后面用于演示这些规则的示例时,请牢记这些规则: + +- Rust 中的每个值,都有一个名为 *所有者,owner* 的变量; +- 同一时间,只能有一个所有者; +- 在其所有者超出作用域,scope 时,该值就被丢弃。 + + +### 变量作用域(variable scope) + +既然已经学了 Rust 基础语法,接下来就不会在示例中,包含整个的 `fn main() {` 代码了,那么若跟随这些示例,就要确保把接下来的这些示例,自己手动放在 `main` 函数里头。这样的结果就是,这些示例会比较精炼一点,着重于具体细节而不是那些样板代码。 + +作为所有权的首个示例,这里将考察一下一些变量的 *作用域,scope*。作用域是指某个项目在程序中的有效范围。以下面这个变量来说: + +```rust +let s = "hello"; +``` + +这里的变量 `s` 指向一个字符串字面值,其中的字符串的值,则是被硬编码到这个程序的文本。自变量被声明处,到当前 *作用域* 结束处,变量都是有效的。下面清单 4-1 给出了一个带有对变量 `s` 在何处有效,进行注解注释的程序: + +```rust +{ // 变量 s 在这里是无效的,他还没被声明出来 + let s = "hello"; // s 自此往下都是有效的 + + // 对变量 s 执行一些操作 +} // 此时该作用域就结束了,而变量 s 也不再有效 +``` + +*清单 4-1:变量与其间有效的作用域,a variable and the scope in which it is valid* + +换句话说,这里有两个重点: + +- 当变量 `s` 一旦来到作用域,他就有效了,when `s` comes *into scope*, it is valid; +- 他会保持有效,直到 *超出作用域*,it remains valid until it goes *out of scope*。 + + +到这里,作用域和变量何时有效二者之间的关系,与其他语言中的此类关系类似。现在就要通过引入 `String` 类型,在此理解之上建构出所有权的理解,now we'll build on top of this understanding by introducing the `String` type。 + +### `String` 类型 + +为了对所有权的那些规则进行演示,就需要比前面第 3 章的 ["数据类型"](Ch03_Common_Programming_Concepts.md#data-types) 小节中讲到那些类型,更为复杂一些的数据类型。前面讲到的那些类型,都是已知大小、可存储在栈上的,且在他们的作用域结束时会被弹出栈,在代码另一部分需要在不同作用域中用到同一值时,这些类型还可被快速而简单地复制,而构造出新的、独立实例。不过这里要审视的是存储在内存堆上的数据,进而探讨 Rust 是如何知晓,何时要清理这些内存堆上的数据,那么 `String` 类型就是极佳的示例了。 + +这里将着重于 `String` 类型与所有权有关的部分。这些方面同样适用于其他的、不论是由标准库还是自己创建的复合数据类型,complex data types。在 [第 8 章](Ch08_Common_Collections.md#strings) 将深入讲解 `String` 类型。 + +前面咱们已经见到了一些字符串字面值,其中有个硬编码到程序里的字符串值。字符串字面值很方便,但对于那些打算使用文本的全部情形,他们却并不适合。一个原因是字符串字面值为不可变的。另一个原因则是,在编写代码时,并非每个字符串的值都是已知的:比如,假设要获取用户输入并存储下来呢?对于这样的情形,Rust 有着第二种字符串类型,即 `String`。这种类型对分配到内存堆上的数据加以管理,并因此而具备了存储在编译时数量未知文本的能力。使用 `String` 类型的 `from` 函数,就可以从字符串字面值,创建出一个 `String` 类型的值来,如下所示: + +```rust +let s = String::from("hello"); +// 变量 s 的类型为:String, 而此前字面值中的变量 s 的类型为:&str +``` + +其中的双冒号(`::`)运算符,实现了将这个特定 `from` 函数,置于 `String` 类型的命名空间之下,而无需使用类似于 `string_from` 这种名字了。在第 5 章的 [方法语法](Ch05_Using_Structs_to_Structure_Related_Data.md#method-syntax) 小节,并在第 7 章的 [对模组树中的某个项目进行引用的路径](Ch07_Managing_Growing_Projects_with_Packages_Crates_and_Modules.md#paths-for-referring-to-an-item-in-the-module-tree) 小节,对模组命名空间的介绍中,将对这种语法进行更多讲解。 + +这种字符串,*能* 被改变: + +```rust +let mut s = String::from("hello"); +s.push_str(", world!"); // push_str() 方法把字面值追加到某个字符串 +println! ("{}", s); // 这将打印出 `hello, world!` +``` + +那么,到底字面值, `&str` 与 `String` 类型有何不同?为何 `String` 可以被改变,而字面值却不能?区别就在于,这两种类型处理内存的方式,是不同的。 + + +### 内存与内存分配 + +对于字符串字面值这种情况,在编译时咱们就知道其内容,因此该文本就被直接硬编码到了最终的可执行文件。这就是为何字符串字面值快速高效的原因。然而这些属性,只是来源于字符串字面值的不可变性。不幸的是,对于那些在编译时大小未知的,且在运行期间大小可能改变的各个文本,是无法为他们而将某块内存,放入到二进制程序中的(unfortunately, we can't put a blob of memory into the binary for each piece of text whose size is unknown at compile time and whose size might change while running the program)。 + +在 `String` 类型下,为了支持可变、可增长的一段文本,就需要在内存堆上分配某个数量的内存,用来保存文本的那些内容,而这个数量在编译时则是未知的。这就意味着: + +- 该内存必须在运行时向内存分配器请求; +- 在使用完那个 `String` 值之后,需要把这片内存交回给内存分配器的某种途径。 + +其中第一部分是由代码编写者完成的:在调用 `String::from` 时,这个 `from` 方法的实现,就请求了他所需的内存。在各种编程语言中,这是相当通行的做法。 + +然而,这第二部分就有所不同了。在带有 *垃圾收集器,garbage collector, GC* 的那些语言中,对那些不再是正被使用中的内存的追踪和清理,是由垃圾收集器完成的,对此这里无需去考虑。而在大多数不带垃圾收集器的语言,就要靠代码编写者自己,去识别内存在何时不再被使用,并像请求内存时一样,要调用代码来显式地释放他。要正确完成这样的内存释放,早已成为一个历史悠久的编程难题。若忘记了,咱们就将浪费内存。而过早地释放内存,则将造成变量失效。若执行两次,那也同样是程序错误。咱们需要严格地一个 `allocate` 对应一个 `free`。 + +Rust 采取了不同的路线:一旦某个变量超出了作用域,那么该变量所持有的内存空间,就被自动退回。下面是对清单 4-1 那个作用域示例,使用 `String` 而非字符串字面值的一个版本: + +```rust + { + let s = String::from("hello"); // 变量 s 自此往下是有效的 + + // 以变量 s 完成一些操作 + } // 该作用域到此时结束,而变量 s + // 不再有效 +``` + +其中就存在可将那个 `String` 类型的值所需的内存,退回给内存分配器的一个天然时间点:即在变量 `s` 超出作用域时。在变量超出作用域时,Rust 就会主动调用一个特殊函数。该函数名为 `drop`,其正是 `String` 类型的编写者,放置用于内存退回的代码之处。在那个结束花括号处,Rust 会自动调用这个 `drop` 函数。 + +> 注意:在 C++ 中,在某程序中项目生命周期结束时,资源重分配的这种模式,有时被称为 *资源获取即初始化*(in C++, this pattern of deallocating resources at the end of an item's lifetime is sometimes called *Resource Acquisition Is Initialization, RAII*)。若曾用过 RAII 模式,那么 Rust 中的这个 `drop` 函数就会不那么陌生了。 + +这种模式对 Rust 代码编写方式有深远影响。在此刻他可能看起来还算简单,但在想要让多个变量,使用早先在内存堆上分配的数据,这种更为复杂情形时,代码行为就会无法被预见到。现在就来探讨一下一些这样的情况。 + +### 变量与数据互操作方式之一:迁移(所有权) + +在 Rust 中,多个变量可以多种方式,与同一数据进行互操作。来看看下面清单 4-2 中用到整数的示例: + +```rust +let x = 5; +let y = x; +``` + +*清单 4-2:将变量 `x` 的整数值,赋值给变量 `y`* + +或许能猜到这段代码正在完成的事情:“把值 `5` 绑定到变量 `x`;随后构造一份 `x` 中值的拷贝并将其绑定到变量 `y`。” 现在就有了两个变量,`x` 与 `y`,且他们都等于 `5`。由于整数是有着已知的、固定大小的简单值,因此这实际上就是正在发生的事情,且这两个 `5` 的值都是被压入到栈上的。 + +> **注**:这就是下面会讲到的 [栈上数据的拷贝,copy](#stack-only-data-copy) 情形。 + + +那么现在来看看 `String` 的版本: + +```rust +let s1 = String::from("hello"); +let s2 = s1; +``` + +这代码看起来与上面的非常相似,那么这里就可以假定其工作方式也是一样的:那就是,第二行将构造出一个 `s1` 中值的拷贝,并将该拷贝绑定到 `s2`。不过这并非真的是实际发生的样子。 + +> **注**:下面的代码将打印出 `s1 = 你好, s2 = 你好`,表示类型 `&str` (字符串切片)是存储在栈上的。 + +```rust +fn main() { + let s1 = "你好"; + let s2 = s1; + + println! ("s1 = {}, s2 = {}", s1, s2); +} +``` + +请参阅下面的图 4-1,来搞明白在幕后 `String` 到底发生了什么。`String` 类型的值,是由三部分构成,在下图中的左边有给出:一个指向到保存该字符串内容内存的指针、一个长度,和一个该字符串的容量。这样一组数据被保存在栈上。下图的右边,即是内存堆上保存着字符串内容的内存。 + +![Rust 中 `String` 类型的本质](images/Ch04_01.svg) + +*图 4-1:、保存着绑定到变量 `s1` 的值 `hello` 的一个 `String` 类型值在内存中的表示* + +> **注**:`String` 类似属于 [灵巧指针,smart pointer](Ch15_Smart_Pointers.md),他是个包含了指针与其他一些元数据的,带有一些方法的特别能力的结构体。 + +其中的长度,即为以字节计数、该 `String` 值内容正使用着的内存数量。而容量则是该 `String` 值从内存分配器处收到的、以字节计算的内存数量。长度与容量之间的区别,会相当重要,但在此情形下尚不重要,到目前未知,是可以忽略容量这个部分的。 + +在将 `s1` 赋值给 `s2` 时,这个 `String` 值被拷贝了,表示这里拷贝了栈上的指针、长度和容量。这里并未拷贝指针指向的、内存堆上的数据。也就是说,内存中数据的表示,如下图 4-2 所示: + +![有着变量 `s1` 的指针、长度与容量拷贝的变量 `s2` 在内存中的表示](images/Ch04_02.svg) + +*图 4-2:有着变量 `s1` 的指针、长度与容量拷贝的变量 `s2` 在内存中的表示* + +这种表示 *不* 同于下图 4-3,那才是 Rust 对内存堆上的数据进行拷贝时,内存看起来的样子。如果 Rust 像下图 4-3 中那样做,那么当内存堆上的数据较大时, `s2 = s1` 的这个操作,将会在运行时性能开销上代价高昂。 + +![`s2 = s1` 操作的另一种可能:Rust 拷贝内存堆数据](images/Ch04_03.svg) + +*图 4-3:`s2 = s1` 操作的另一种可能:Rust 同时拷贝内存堆数据* + +早先曾讲过,在变量超出作用域后,Rust 会自动调用那个 `drop` 函数,而清理掉那个变量的堆内存。但图 4-2 则给出了两个指针都指向同一位置的情况。这就是个问题了:在 `s2` 与 `s1` 都超出作用域时,他们都将尝试去释放那同样的内存。这被称为 *双重释放,double free* 错误,是先前提到过的内存安全错误之一,one of the memory safety bugs。二次释放内存,可导致内存损坏,而内存损坏则会潜在导致安全漏洞。 + +为确保内存安全,Rust 在代码行 `s2 = s1` 之后,便不再认为 `s1` 是有效的了。因此,在 `s1` 超出作用域后,Rust 便不需要释放任何内存。下面就来检查一下,在 `s2` 创建出来后,去尝试使用 `s1` 会发生什么;这样做是不会工作的: + +```rust + let s1 = String::from("hello"); // 这里 s 的类型为:String + let s2 = s1; + + println! ("{}", s1); +``` + +由于 Rust 阻止了对失效引用变量的使用,因此将收到一个下面这样的错误: + +```console +$ cargo run + Compiling string_demo v0.1.0 (/home/peng/rust-lang/projects/string_demo) +warning: unused variable: `s2` + --> src/main.rs:3:9 + | +3 | let s2 = s1; + | ^^ help: if this is intentional, prefix it with an underscore: `_s2` + | + = note: `#[warn(unused_variables)]` on by default + +error[E0382]: borrow of moved value: `s1` + --> src/main.rs:5:21 + | +2 | let s1 = String::from("hello"); // 这里 s 的类型为:String + | -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait +3 | let s2 = s1; + | -- value moved here +4 | +5 | println! ("{}", s1); + | ^^ value borrowed here after move + | + = note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info) + +For more information about this error, try `rustc --explain E0382`. +warning: `string_demo` (bin "string_demo") generated 1 warning +error: could not compile `string_demo` due to previous error; 1 warning emitted +``` + +若在使用其他编程语言时,曾听说过 *浅拷贝(shallow copy)* 和 *深拷贝(deep copy)* 这两个说法,那么这种对指针、长度与容量的拷贝,而未拷贝数据的概念,或许听起来像是进行了一次浅拷贝。但由于 Rust 还将第一个变量进行了失效处理,因此这里就不叫浅拷贝,而叫做 *迁移(move)*。在这个示例中,就会讲,变量 `s1` 已被 *迁移* 到变量 `s2` 里了。因此真实发生的事情,就是下图 4-4 显示的那样: + +![在变量 `s1` 失效后内存中的表示](images/Ch04_04.svg) + +*图 4-4:在变量 `s1` 失效后内存中的表示* + +这就解决了问题!在只有 `s2` 有效之下,当变量 `s2` 超出作用域后,那么就只有他会释放内存,于是就解决了双重内存释放问题。 + +此外,这种做法背后,还隐含着一种语言设计上的取舍:Rust 绝不会自动创建数据的 “深” 拷贝。由此,任何 *自动* 拷贝,都可认为在运行时性能开销上的影响很小(Therefore, any *automatic* copying can be assumed to be inexpensive in terms of runtime performance)。 + +### 变量与数据交互方式之二:克隆 + +在 *确实* 打算对 `String` 的内存堆数据,而非只是栈数据进行深度拷贝时,就可以使用一个常用的、名为 `clone` 的方法。在第 5 章将讨论到方法语法,而由于在众多编程语言中,方法都是共同特性,那么此前大概率是见到过方法的。 + +下面是一个运作中的 `clone` 方法示例: + +```rust +fn main() { + let s1 = String::from("hello"); // 这里 s 的类型为:String + let s2 = s1.clone(); + + println! ("s1 = {}, s2 = {}", s1, s2); +} +``` + +这段代码工作起来毫无问题,并显式地产生出图 4-3 中给出的行为,其间内存堆数据确实得以拷贝。 + +当看到一个对 `clone` 方法的调用时,那么就明白正有一些任性代码在被执行,且那代码可能开销高昂。对此方法的调用,是某些不寻常事情正在发生的直观指示。 + +### 唯栈数据:拷贝(stack-only data: copy) + +尚有另一个至今还未讲到的小问题。正使用着整数的这段代码 -- 其中一部分在下面的清单 4-2 中给出了 -- 会工作并是有效代码: + +```rust +let x = 5; +let y = x; + +println! ("x = {}, y = {}", x, y); +``` + +然而这段代码,似乎与前面刚刚所掌握的相抵触:这里没有对 `clone` 的调用,但变量 `x` 依然有效,而并未迁移到变量 `y` 中去。 + +原因就在于,诸如整数这样的,在编译时大小已知的类型,都是被整个存储在栈上,那么构造他们具体值的拷贝是迅速的。那就意味着,在构造出变量 `y` 之后,就没有理由要去阻止变量 `x` 一直有效了。换句话说,此时的深拷贝与浅拷贝之间,是没有区别的,因此对 `clone` 进行调用,不会完成与通常的浅拷贝有任何区别的事情,进而就能忽略这个 `clone` 方法。 + +Rust 有着叫做 `Copy` 特质(the `Copy` trait, 在第 10 章将对特质,traits,进行更多的讲解)的,可放在像是整数这样的、存储于栈上的那些类型之上的一个特殊注解,a special annotation。在某个类型实现了 `Copy` 特质时,使用此类型的那些变量,就不会迁移,相反会轻而易举地被复制,从而在赋值给另一变量后,令到他们依然有效。 + +在某个类型或类型的任何部分带有 `Copy` 特质时,Rust 就不会再允许以 `Drop` 特质对其加以注解了。若某个类型需要在其值超出作用域后,还要进行某些特殊处理,而又将 `Copy` 注解添加到了那个类型,那么就会收到编译时错误(if the type needs something special to happen when the value goes out of scope and we add the `Copy` annotation to that type, we'll get a compile-time error)。要了解如何将 `Copy` 注解,添加到自己编写的类型而实现这个 `Copy` 特质,请参阅附录 C 中 [可派生特质(derivable traits)](Ch21_Appendix.md#derivable-traits)。 + +那么到底哪些类型要实现 `Copy` 特质呢?可查阅给定类型的文档,来确定相应类型是否有实现 `Copy` 特质,不过作为一般规则,任何组别的简单标量值,any group of simple scalar values,都可实现 `Copy` 特质,以及不要求分配内存堆分配,或者其他形式资源的类型,也都可以实现 `Copy` 特质(any group of simple scalar values can implement `Copy`, and nothing that requires allocation or is some form of resource can implement `Copy`)。下面就是一些实现 `Copy` 特质的类型: + +- 全部的整型,比如 `u32`; +- 布尔值类型,`bool`,即值 `true` 与 `false`; +- 全部浮点数类型,比如 `f64`; +- 字符类型,`char`; +- 只包含实现 `Copy` 特质类型的元组类型。比如 `(i32, i32)` 这个元组类型,就实现了 `Copy` 特质,而 `(i32, String)` 则没有。 + + +### 所有权与函数 + +将值传递给函数的语法,与将值赋值给变量的语法,是类似的。将变量传递给函数,就会进行迁移或拷贝,这与赋值所做的别无二致。下面的清单 4-3 有着一个带有一些注解的示例,对其中的变量进入和超出作用域,进行了展示。 + +文件名:`src/main.rs` + + +```rust +fn main() { + let s = String::from("hello"); // 变量 s 进到作用域 + + takes_ownership(s); // 变量 s 的值迁移到这个函数里头...... + // ......进而变量 s 因此不再有效 + + let x = 5; // 变量 x 进到作用域 + + makes_copy(x); // 变量 x 迁移到到这个函数里, + // 但由于 i32 实现了 `Copy` 特质,因此 + // 后面在使用变量 x 也是没问题的 +} // 到这里,变量 x 超出了作用域,接着便是变量 s。但由于变量 s 的值已被迁移,因此 + // 这里不会有特别的事情发生。 + +fn takes_ownership(some_string: String) { // 变量 some_string 进到作用域 + println! ("{}", some_string); +} // 到这里,变量 some_string 便超出作用域,而 `drop` 方法就会被调用。some_string 的 + // 内存就被释放了。 + +fn makes_copy(some_integer: i32) { // 变量 some_integer 进到作用域 + println! ("{}", some_integer); +} // 到这里,变量 some_integer 超出作用域。没有特别事情发生。 +``` + +*清单 4-3:带所有权与作用域注解的函数* + +> 注:下面的代码,仍然会报出:`use of moved value: ``some_string```错误: + +```rust +fn takes_ownership(some_string: String) { + println! ("{}", some_string); + another_takes_ownership(some_string); + third_takes_ownership(some_string); +} +``` + +在对 `takes_ownership` 的调用之后,尝试使用变量 `s` 时,Rust 就会抛出一个编译时错误。这样的静态检查,保护咱们免于出错。请将使用变量 `s` 与变量 `x` 的代码,添加到 `main` 函数中,来观察一下在哪些地方可以使用他们,以及所有权规则会怎样阻止这样做。 + +### 返回值与作用域(return value and scope) + +返回值也会转移所有权。下面的清单 4-4 给出了一个返回了某个值的函数示例,该示例有着与清单 4-3 中的那些类似的注释。 + +文件名:`src/main.rs` + +```rust +fn main() { + let s1 = gives_ownership(); // gives_ownership 将其返回值 + // 迁移到变量 s1 中 + + let s2 = String::from("hello"); // 变量 s2 进入作用域 + + let s3 = takes_and_gives_bake(s2); // 变量 s2 被迁移到 takes_and_gives_back + // 中,该函数又将他的返回值迁移到变量 s3 中 + + println! ("{}, {}", s1, s3); +} // 到这里,变量 s3 超出作用域而被丢弃。变量 s2 已被迁移,因此什么也不会发生。而 + // 变量 s1 则超出作用域而被丢弃。 + +fn gives_ownership() -> String { // 函数 gives_ownership 将把他的返回值,迁移 + // 到调用他的函数中(即 main 函数) + String::from("归你了") // 此表达式的值将被返回,并迁出到调用函数 +} + +// 此函数接收一个 String 并要返回一个 String +fn takes_and_gives_bake(a_string: String) -> String { // a_string 进入作用域 + a_string // a_string 被返回,并迁出到调用函数 +} +``` + +*清单 4-4:返回值的所有权转移* + +变量所有权每次都依循同一模式:在将值赋给另一变量时,所有权就会迁移。包含着内存堆上数据的某个变量,在超出作用域时,除非数据所有权已被迁移至另一变量,否则该值就会被 `drop` 给清理掉。 + +而在此模式生效时,每个函数下的取得所有权与随后的交回所有权,就有点乏味了。在要某个函数使用某个值而不占据其所有权时,会怎样呢?如果希望再度使用传入到函数中的全部东西,并还要把他们和那些可能要返回的函数体运算结果,一起再传回来,那样就很烦人了。 + +如下面的清单 4-5 所示,Rust 确实允许使用一个元组,返回多个值: + +文件名:`src/main.rs` + +```rust +fn main() { + let s1 = String::from("hello"); + + let (s2, len): (String, usize) = calculate_length(s1); + + println! ("字符串 {} 的长度为:{}", s2, len); +} + +fn calculate_length(s: String) -> (String, usize) { + let length = s.len(); + + (s, length) +} +``` + +*清单 4-5:返回参数所有权* + +这虽然间接实现了消除变量所有权占据下,函数的使用变量,但对于这种本应常见的概念来说,这样做就过于花哨,且带来了大量工作负担。幸运的是,Rust 有着一项使用某个值而不转移所有权,名为 *引用(references)* 的特性。 + + +## 引用与借用(references and borrowing) + +清单 4-5 中那些元组代码的问题,是因为那个 `String` 值已被迁移到 `calculate_length` 函数中,因此那里就必须将那个 `String` 值,返回给调用函数(the calling funciton, 即清单 4-5 中的 `main` 函数),进而在对 `calculate_length` 的调用之后,仍然可以使用那个 `String` 的堆上数据。相反,咱们可以提供到那个 `String` 值的引用。所谓 *引用,reference*,与指针相似的是,在引用中的是个地址,咱们循着这个地址,就可以访问保存在那个地址处的数据,而这个数据则是为某个别的变量所拥有的。与指针不同的是,在引用存活期间,其保证是指向了特定类型有效值的。 + +以下是应如何定义和使用,将某个对象的引用作为参数,而非占用该值所有权的方式下的 `calculate_length` 函数: + +文件名:`src/main.rs` + +```rust +fn main() { + let s1 = String::from("hello"); + + let length = calculate_length(&s1); + + println! ("字符串 {} 的长度为:{}", s1, length); +} + +fn calculate_length(s: &String) -> usize { + s.len() +} +``` + +首先,注意到变量声明与函数返回值中的全部元组代码都不见了。其次,留意到这里是将 `&s1` 传入到 `calculate_length` 中的,同时在该函数的定义中,采用的是 `&String` 而非 `String`。这些 `&` 符号,these ampersands,表示了 *引用,references*,他们实现了在无需占用某个值所有权的情况下,引用到该值。下图 4-5 对此概念进行了描述。 + +![指向 `String s1` 的 `&String s` 图示](images/Ch04_05.svg) + +*图 4-5:指向 `String s1` 的 `&String s` 图示* + + +> 注意:这种经由使用 `&` (取地址)运算符,而得到的变量引用的反面,即为 *解引用,dereferencing*,解引用是以解引用运算符 `*` 达成的。在第 8 章中就会看到这个 [解引用运算符的使用](Ch08_Common_Collections.md#iterating-over-the-values-in-a-vector),而在第 15 章中,则会对解引用的细节加以讨论。 + +来细看一下这里的函数调用: + +```rust +let s1 = String::from("hello"); +let len = calculate_length(&s1); +``` + +这种 `&s1` 语法,实现了创建出一个 *指向,refers* 到 `s1` 的值,却不占有那个值的引用变量。由于引用不占有那个值,因此在引用停止使用(超出作用域)时,其所指向值就不会被弃用。 + +与此类似,那个函数签名同样使用 `&` 运算符,来表明参数 `s` 的类型是个引用。下面就来添加一些说明性的注解: + +```rust +fn calculate_length(s: &String) -> usize { // 变量 s 为到某个 String 值的引用 + s.len() +} // 到这里,变量 s 超出作用域。但由于他并没有他指向值的所有权,因此什么 + // 也不会发生。 +``` + +变量 `s` 于其间有效的那个作用域,与所有函数参数作用域是相同的,而由于变量 `s` 不拥有经引用而指向的那个值的所有权,因此在变量 `s` 停止被使用时,那个所指向的值就不会被丢弃。在函数以引用变量,而非真实值作为参数时,由于根本就没有拥有过所有权,那么就不再需要为了交回所有权,而将那些值返回了。 + +咱们把这种创建出引用的行为,叫做 *借用,borrowing*。正如日常生活中,当某人拥有某个物件时,咱们就可以把这个物件从那个人那里借用一下。在使用完毕后,咱们必须将其还回。咱们是不拥有该物件的。 + +那么在尝试修改某个正借用的物件时,又会发生什么呢?请尝试下面清单 4-6 中的代码。提前剧透一下:那代码就不会工作! + +文件名:`src/main.rs` + +```rust +fn main() { + let s = String::from("hello"); + + change(&s); +} + +fn change(some_string: &String) { + some_string.push_str(", world!"); +} +``` + +*清单 4-6:尝试修改被借用值,a borrowed value* + +下面就是编译器报错: + +```console +$ cargo run + Compiling ownership_demo v0.1.0 (/home/peng/rust-lang/projects/ownership_demo) +error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference + --> src/main.rs:8:5 + | +7 | fn change(some_string: &String) { + | ------- help: consider changing this to be a mutable reference: `&mut String` +8 | some_string.push_str(", world!"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable + +For more information about this error, try `rustc --explain E0596`. +error: could not compile `ownership_demo` due to previous error +``` + +就跟变量默认是不可变的一样,引用也是默认不可变的。不允许修改所引用的某个物件。 + + +### 可变引用 + +使用 *可变引用,mutable reference*,来取代默认不可变引用,只需一些小小调整,就可将清单 4-6 的代码,修改为允许对借用值,a borrowed value 加以修改: + +文件名:`src/main.rs` + +```rust +fn main() { + let mut s = String::from("hello"); + + change(&mut s); + + println! ("s:{}", s); +} + +fn change(some_string: &mut String) { + some_string.push_str(", world!"); +} +``` + +首先,这里将变量 `s` 改为了 `mut`。随后在调用 `change` 函数处,以 `&mut s` 创建了一个可变的引用变量,并以 `some_string: &mut String`,将那个函数签名,更新为接受一个可变引用变量(a mutable reference)。这样做就很清楚地表明了,那个 `change` 函数将修改他借用的那个值。 + +可变引用变量有个大的限制:在有着到某值的一个可变引用时,就不能有到那个值的其他引用了。下面尝试创建到变量 `s` 两个可变引用的代码,就会失败: + +文件名:`src/main.rs` + +```rust +fn main() { + let mut s = String::from("hello"); + + let r1 = &mut s; + let r2 = &mut s; + + println! ("{}, {}", r1, r2); +} +``` + +下面是编译器报错: + +```console +$ cargo run + Compiling ownership_demo v0.1.0 (/home/peng/rust-lang/projects/ownership_demo) +error[E0499]: cannot borrow `s` as mutable more than once at a time + --> src/main.rs:5:14 + | +4 | let r1 = &mut s; + | ------ first mutable borrow occurs here +5 | let r2 = &mut s; + | ^^^^^^ second mutable borrow occurs here +6 | +7 | println! ("{}, {}", r1, r2); + | -- first borrow later used here + +For more information about this error, try `rustc --explain E0499`. +error: could not compile `ownership_demo` due to previous error +``` + +此错误是说,由于在某个时间,多次将 `s` 借用做可变引用,而因此这段代码是无效的。首次可变借用是在 `r1` 中,而这次借用必须持续到其在那个 `println!` 中被使用为止,但就在那个可变引用的创建与使用中间,这里还尝试了在 `r2` 中,创建另一个借用了与 `r1` 同样数据的可变引用变量。 + +这种阻止在同一时间,到同一数据多重可变引用的限制,是允许修改的,但要在极度受控方式下进行(the restriction preventing multiple mutable references to the same data at the same time allows for mutation but in a very controlled fashion)。由于多数语言都允许随时修改数据,而因此多重可变引用正是一些新晋 Rust 公民们纠结不已的东西。有着这个限制的好处,则是 Rust 可以在编译时,对数据竞争加以阻止。与赛跑情形类似,*数据竞争,data race* 会在下面三种现象发生出现时出现: + +- 同一时间有两个以上的指针访问着同一数据(two or more pointers access the same data at the same time); +- 这些指针中至少有一个,正被用于写那个数据(at least one of the pointers is being used to write to the data); +- 没有使用某种机制,来同步对数据的访问(there's no mechanism being used to synchronize access to the data)。 + +数据竞争导致未定义行为,并在尝试于运行时对其加以追踪的时候,难于排查诊断和修复;Rust 通过拒绝编译带有数据竞争的代码,而防止了这类问题! + +与往常一样,可使用花括号来创建一个新的作用域,而实现多个可变应用变量,只要不是 *同时,simultaneous* 的几个就行: + +```rust + let mut s = String::from("hello"); + + { + let r1 = &mut s; + } // 由于在这里变量 r1 超出了作用域,因此就可以 + // 毫无问题地构造一个新的引用变量了。 + + let r2 = &mut s; +``` + + +对于将可变与不可变引用进行结合的情况,Rust 则会强制执行类似规则。下面的代码会导致错误: + +```rust + let mut s = String::from("hello"); + + let r1 = &s; + let r2 = &s; + let r3 = &mut s; + + println! ("{}, {} 与 {}", r1, r2, r3); +``` + +下面就是那个错误: + +```console +$ cargo run + Compiling ownership_demo v0.1.0 (/home/peng/rust-lang/projects/ownership_demo) +error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable + --> src/main.rs:6:14 + | +4 | let r1 = &s; + | -- immutable borrow occurs here +5 | let r2 = &s; +6 | let r3 = &mut s; + | ^^^^^^ mutable borrow occurs here +7 | +8 | println! ("{}, {} 与 {}", r1, r2, r3); + | -- immutable borrow later used here + +For more information about this error, try `rustc --explain E0502`. +error: could not compile `ownership_demo` due to previous error +``` + +咦!在有着对某个值的不可变引用时,*也是,also* 不可以对其有可变引用的。不可变引用的用户们,并不期望他们所引用的值,在他们眼皮底下突然就变掉!不过由于仅读取数据的不可变引用,对其他读取那个数据的引用,不具备造成影响的能力,因此多个不可变引用倒是可以的。 + +请注意引用变量的作用域,是从引入这个变量的地方开始,而持续到那个引用变量最后一次被使用为止。举例来说,由于那个不可变引用变量最后的使用,即那个 `println!`,是在那个可变引用变量引入之前发生的,因此下面的代码将会编译: + +```rust + let mut s = String::from("hello"); + + let r1 = &s; + let r2 = &s; + println! ("r1 与 r2: {}, {}", r1, r2); + // 变量 r1 与 r2 在此点位之后便不再被使用 + + let r3 = &mut s; // 这就没问题了 + println! ("r3: {}", r3); +``` + +不可变引用变量 `r1` 与 `r2` 的作用域,在 `println!` 语句,即他们最后被使用的地方之后便结束,而这个地方正是那个可变引用变量 `r3` 被创建之前。这些作用域不会重叠,因此这段代码是允许的。识别出引用变量在作用域结束之前的某处,不再被使用的编译器能力,叫做 *非词法性生命周期,Non-Lexical Lifetimes, 简写做 NLL*,在 [版本手册](https://doc.rust-lang.org/edition-guide/rust-2018/ownership-and-lifetimes/non-lexical-lifetimes.html) 里可阅读到更多有关内容。 + +虽然这些所有权借用方面的错误,时常令人沮丧,但请记住这正是 Rust 编译器,于早期阶段(在编译时而非运行时)就在指出潜在错误,并表明问题准确所在。代码编写者这才不必去追踪为何数据不是先前所设想的那样。 + +### 悬空引用,dangling references + +在有着指针的那些语言中,都容易通过在保留了到某些内存的一个指针同时,释放了那些内存,而错误地创建出 *悬空指针,a dangling pointer* -- 引用了内存中,可能已经给了其他指针的某个地址的一个指针。在 Rust 中,与此相对照,编译器会确保引用绝不会成为悬空引用:在有着到某数据的引用时,编译器会确保在到该数据的引用,超出作用域之前,被引用的数据不超出作用域。 + +下面就来创建一个悬空引用,看看 Rust 如何以编译器错误,来阻止悬空引用: + +文件名:`src/main.rs` + + +```rust +fn main() { + let reference_to_nothing = dangle(); +} + +fn dangle() -> &String { + let s = String::from("hello"); + + &s +} +``` + +下面就是报错: + + +```console +$ cargo run + Compiling ownership_demo v0.1.0 (/home/peng/rust-lang/projects/ownership_demo) +error[E0106]: missing lifetime specifier + --> src/main.rs:5:16 + | +5 | fn dangle() -> &String { + | ^ expected named lifetime parameter + | + = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from +help: consider using the `'static` lifetime + | +5 | fn dangle() -> &'static String { + | ~~~~~~~~ + +For more information about this error, try `rustc --explain E0106`. +error: could not compile `ownership_demo` due to previous error +``` + +此错误消息提到了一个这里还没有讲到特性:生命周期(lifetimes)。在第 10 章将 [详细讨论生命周期](Ch10_Generic_Types_and_Lifetimes.md#validating-references-with-lifetimes)。不过,忽略掉生命周期有关的那部分错误,那么该错误消息就真的包含了,这段代码为何是问题代码的关键原因: + +```console +this function's return type contains a borrowed value, but there is no value +for it to be borrowed from +``` + +下面来细看一下,这里的 `dangle` 代码各个阶段到底发生了什么: + +文件名:`src/main.rs` + +```rust +fn dangle() -> &String { // 函数 dangle 返回的是到某个 String 值的引用 + let s = String::from("hello"); // 变量 s 是个新的 String 值 + + &s // 这里返回了一个到该 String,变量 s 的引用 +} // 到这里,变量 s 超出了作用域,进而被丢弃了。他的内存就没了。 + // 危险所在! +``` + +由于变量 `s` 是在函数 `dangle` 内部创建的,那么在函数 `dangle` 的代码执行完毕时,变量 `s` 就将被解除内存分配(deallocated)。而这里还在尝试返回一个到他的引用。那就意味着这个引用,就会指向到一个无效的 `String`。那就不好了!Rust 是不会允许这样干的。 + +这里的解决办法,就是直接返回那个 `String` 值: + +```rust +fn dangle() -> String { + let s = String::from("hello"); + + s +} +``` + +### 引用的规则 + +下面来对前面已经讨论过有关引用的东西,进行一下总结回顾: + +- 在任意给定时间点,都 *要么* 只能有一个可变引用,*要么* 有任意数量的不可变引用(at any given time, you can have *either* one mutable reference *or* any number of immutable references); +- 引用必须一直有效(references must always be valid)。 + +接下来,将看看一种不同类别的引用:切片(slices)。 + + +## 切片类型(the slice type) + +*切片(slices)* 特性,实现了对集合中一个连续元素序列,而非对整个集合的引用。切片是引用的一种类别,因此他不会持有所有权。 + +这里有个小的编程问题:编写一个取得字符串,而返回在那个字符串中找到的第一个单词的函数。在函数在那个字符串中未找到空格时,那么这整个字符串就一定是一个单词,因此就要返回这整个字符串了。 + +下面就要在不使用切片特性的情况下,来看看该怎么编写这个函数的签名,从而搞明白切片要解决的问题: + +```rust +fn first_word(s: &String) -> ? +``` + +这个 `first_word` 函数,有着一个作为参数的 `&String` 类型。这里不想要所有权,因此这是没问题的。不过应该返回什么呢?这里实在没有一种描述字符串 *局部(part)* 的方式。不过,这里可以返回那个单词的、以一个空格表示的结尾的索引。先来试试这个,如下面清单 4-7 所示: + +文件名:`src/main.rs` + +```rust +fn first_word(s: &String) -> usize { + let bytes = s.as_bytes(); + + for (i, &item) in bytes.iter().enumerate() { + if item == b' ' { + return i; + } + } + + s.len() +} +``` + +*清单 4-7:返回那个 `&String` 参数中一个字节索引值的 `first_word` 函数* + +因为这里需要对这个 `String` 值元素挨个遍历,进而挨个检查值是否是个空格,因此这里就将使用 `as_bytes` 方法,把这个 `String` 值转换为字节的数组: + +```rust +let bytes = s.as_bytes(); +``` + +接着,这里使用 `iter` 方法,创建了一个在该字节数组上的迭代器: + +```rust +for (i, &item) in bytes.iter().enumerate() { +``` + +在第 13 章,将讨论到迭代器的更多细节。而现在,明白 `iter` 是个返回集合中各个元素的方法,而那个 `enumerate` 则会将 `iter` 的结果进行封装进而将各个元素作为一个元组的组成部分,进行返回即可。自 `enumerate` 返回的元组第一个元素就是索引值,而第二个元素,则是到 `iter` 返回元素的索引。相比由代码编写者自己计算索引,这就要方便一点。 + +由于 `enumerate` 方法返回了一个元组,因此这里就可以使用模式,来解构那个元组。在 [第 6 章](Ch06_Enums_and_Pattern_Matching.md#patterns-that-bind-to-values),会对模式进行更多讨论。在那个 `for` 循环中,指定了一个有着用于那个元组中索引的 `i`,以及用于那个元组中单个字节的 `&item` 的模式。由于这里获得的是一个到从 `.iter().enumerate()` 获取元素的引用,因此在那个模式中使用了 `&` 运算符。 + +在那个 `for` 循环内部,这里通过使用字节字面值语法(the byte literal syntax),就表示空格的字节进行了搜索。在找到空格时,就返回空格的位置。否则就通过使用 `s.len()` 返回该字符串的长度。 + +```rust + if item == b' ' { + return i; + } + } + + s.len() +``` + +现在就有了一种找出字符串中第一个单词末尾索引的方法了,不过这里有个问题。这里所返回的只是个 `usize`,然而这个返回值只是在 `&String` 的语境下,才是个有意义的数字。也就是说,由于这个返回的 `usize` 类型值,是从那个 `String` 值获取到的孤立值,因此就没办法保证在以后仍然有效。关于这点,可考虑在清单 4-8 中、用到了清单 4-7 中 `first_word` 函数的这么一个程序。 + +文件名:`src/main.rs` + +```rust +fn main() { + let mut s = String::from("The quick brown fox jumps over the lazy dog."); + + let word = first_word(&s); // 变量 word 将获得值 5 + + s.clear(); // 这个语句会清空该字符串,令其等于 "" + + // 到这里变量 word 仍有着值 5,但已经不再有那个可将值 5 有意义的运用 + // 到的字符串了。变量 5 现在完全无用了! +} +``` + +*清单 4-8:将来自调用 `first_word` 函数的结果存储起来,并在随后修改那个 `String` 值的内容* + +该程序会不带任何错误地编译,且同样会在调用了 `s.clear()`后使用变量 `word` 时,其仍会完成后续执行(this program compiles without any errors and would do so if we used `word` after calling `s.clear()`)。由于变量 `word` 完全未被连接到变量 `s` 的状态,因此变量 `word` 仍包含着值 `5`。这里仍可使用那个值 `5` 与变量 `s`,来尝试提取出第一个单词,但由于自将值 `5` 保存在 `word` 中以来,变量 `s` 的内容已被修改,因此这样做将是个程序错误(a bug)。 + +这种不可避免的担心变量 `word` 中的索引,失去与变量 `s` 中的数据同步,就会十分烦人且容易发生错误!而在要编写 `second_word` 函数时,对这些索引的管理,将更加脆弱。`second_word` 的函数签名,将务必看起来像下面这样: + +```rust +fn second_word(s: &String) -> (usize, usize) { +``` + +现在就得对一个开始 *和* 结束索引保持跟踪,同时甚至还有更多的、要从特定状态中的数据计算出的值,而这些值又完全没有与那种状态联系起来。这样就有了三个无关的、需要同步保持的变量漂浮着。 + +幸运的是,Rust 有此问题的解决办法,那就是:字符串切片(string slices)。 + +### 字符串切片 + +字符串切片是到某个 `String` 类型值部分的引用,而看起来像下面这样: + +```rust + let s = String::from("The quick brown fox jumps over the lazy dog."); + + let the = &s[0..3]; + let quick = &s[4..9]; +``` + +与到整个 `String` 值的引用 `&s` 不同,`the` 是到这个 `String` 的,在那个附加 `[0..3]` 中所指明的一部分的引用。通过指定 `[start_index..ending_index]`,而使用了在一对方括号里的一个范围,这里创建出了切片,其中的 `starting_index` 是切片中首个位置,而 `ending_index` 则是比切片中最后位置多一的位置索引。切片数据结构内部,存储着开始位置与该切片的长度,长度即 `ending_index` 减去 `starting_index`。那么在示例 `let quick = &s[4..9];` 中,`quick` 就会包含一个到变量 `s` 的索引 `4` 处字节的指针。 + +下图 4-6 展示对此进行了展示。 + +![指向一个 `String` 数据局部的字符串切片](images/Ch04_06.svg) + +*图 4-6:指向一个 `String` 数据局部的字符串切片* + +在 Rust 的 `..` 范围语法,the `..` range syntax 之下,在希望于索引为零处开始时,那么就可以舍弃那两个点之前的值。也就是说,写开始索引 `0` 与不写,是等价的: + +``` +let s = String::from("hello"); + +let slice = &s[0..2]; +let slice = &s[..2]; +``` + +对同一个字符串令牌,在切片包含了那个 `String` 的最后字节时,那么就可以舍弃那结尾的数字。即意味着下面的语句是等价的: + +```rust +let s = String::from("hello"); + +let len = s.len(); + +let slice = &s[3..len]; +let slice = &s[3..]; +``` + +要取用整个字符串时,还可以把开始与结束索引都舍弃掉。那么下面的语句就是等价的了: + +```rust +let s = String::from("hello"); + +let len = s.len(); + +let slice = &s[0..len]; +let slice = &s[..]; +``` + +> **注意**:这些字符串切片的范围索引值,必须出现于有效的 UTF-8 字符边界处。若在 UTF-8 多字节字符中间,尝试创建字符串切片,那么程序就会以错误退出。这里只是为介绍字符串切片目的,而假定本小节中只使用 ASCII 字符;在第 8 章的 [“以 `String` 类型值存储 UTF-8 编码的文本”](Ch08_Common_Collections.md#storing-utf-8-encoded-text-with-strings) 小节,有着对 UTF-8 字符串的更全面讨论。 + + +对这全部字符串切片的情况了然在胸,那么下面就来将 `first_word` 重写为返回切片。表示 “字符串切片” 的类型,写做 `&str`: + +文件名:`src/main.rs` + +```rust +fn first_word(s: &String) -> &str { + let bytes = s.as_bytes(); + + for (i, &item) in bytes.iter().enumerate() { + if item == b' ' { + return &s[..i]; + } + } + + &s[..] +} +``` + +这里是以前面清单 4-7 中所做的同样方式,即查找首次出现的空格,而获取到该单词结束处的索引。在找到空格时,就运用该字符串的开头,与那个空格的索引,作为字符串切片开始与结束索引,而返回一个字符串切片。 + +现在当调用 `first_word` 函数时,取回的便是与所用 `String` 数据联系起来单个值。这个值是由到切片起点的引用,与切片中元素个数所组成。 + +这样返回切片,对于 `second_word` 函数,也是有效的: + +```rust +fn second_word(s: &String) -> &str { +``` + +由于编译器将确保到那个 `String` 数据中引用保持有效,因此现在就有了一个简单的、相比之前那个不那么容易搞混的 API 了。还记得在清单 4-8 中那个程序里的错误吧,即那个在已经获取到首个单词结束位置的索引,而随后清除了那个字符串,因此得到的索引就不在有效的问题。那段代码虽然逻辑上不正确,但也不会立即给出什么错误来。若继续尝试使用空字符串上的首个单词结束索引,这些问题仍会出现。切片就令到这个代码错误不可能了,并实现了更快发现代码问题。使用切片版本的 `first_word` 函数,就会抛出一个编译时错误: + +文件名:`src/main.rs` + +```rust +fn main() { + let mut s = String::from("The quick brown fox jumps over the lazy dog."); + + let word = first_word(&s); + + s.clear(); + + println! ("首个单词为:{}", word); +} +``` + +下面就是那个编译器错误消息: + +```console +$ cargo run  ✔ + Compiling ownership_demo v0.1.0 (/home/peng/rust-lang/projects/ownership_demo) +error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable + --> src/main.rs:6:5 + | +4 | let word = first_word(&s); + | -- immutable borrow occurs here +5 | +6 | s.clear(); + | ^^^^^^^^^ mutable borrow occurs here +7 | +8 | println! ("首个单词为:{}", word); + | ---- immutable borrow later used here + +For more information about this error, try `rustc --explain E0502`. +error: could not compile `ownership_demo` due to previous error +``` + +回顾借用规则,在有着到某数据的不可变引用时,就不能同时有可变引用。由于 `clear` 方法需要清空那个 `String` 值,那么就需要得到一个可变引用。而在 `clear` 方法调用之后的 `println!`,用到了变量 `word` 里的引用,那么这个不可变引用于那个时刻,就必将仍是活跃的。Rust 不允许 `clear` 中的可变引用,与 `word` 中的不可变引用同时存在,进而编译失败。可以看出,Rust 不光令到这个 `first_word` 的 API 更易于使用,他还在运行时就消除了这一整类错误! + + +### 字符串字面值即切片 + +还记得前面讲到过,那些硬编码的、存储在二进制可执行文件内部的字符串字面值吧。现在了解了切片,那么就可以很好理解字符串字面值了: + +```rust +let s = "Hello, world!"; +``` + +这里的变量 `s` 的类型,即为 `&str`:他是个指向到二进制文件特殊点位的一个切片。这也是为何字符串字面值为不可变的原因;`&str` 类型属于不可变引用。 + + +### 字符串切片作为函数参数 + +了解了咱们可在函数中,取字符串字面值的切片及 `String` 值,就引出了对 `first_word` 函数的又一项改进,而下面就是函数 `first_word` 的签名: + +```rust +fn first_word(s: &String) -> &str { +``` + +更老道的 Rust 公民将把这个函数签名,写着像下面清单 4-9 中所展示的那样,这是因为下面这样写,就实现了在 `&String` 与 `&str` 两种类型值上,可使用同一个函数: + +```rust +fn first_word(s: &str) -> &str { +``` + +*清单 4-9:通过对 `s` 参数的类型使用字符串切片,对 `first_word` 函数进行改进* + +在咱们有着某个字符串切片时,那么就可以直接传递那个字符串切片。而在咱们有着一个 `String` 时,则可传递该 `String` 的切片,或到这个 `String` 的引用。这种灵活性,是利用了 *强制引用解除,deref coercions* 特性,在第 15 章的 [函数与方法下的隐式强制解引用](Ch05_Smart_Pointers.md#implicit-deref-coercions-with-functions-and-methods) 小节,将讲到的一种特性。 + +这样定义出取字符串切片,而非到 `String` 值引用做参数的函数,令到这个 API 在不丢失任何功能的情况下,变得更为通用和有用: + +文件名:`src/main.rs` + +```rust +fn main() { + let s = String::from("The quick brown fox jumps over the lazy dog."); + + // 函数 first_word 在 String 值的切片上有效,不管是部分还是全部的切片 + let word = first_word(&s[0..6]); + println! ("{}", word); + + let word = first_word(&s[..]); + println! ("{}", word); + + // 函数 first_word 还在 String 变量的引用上有效,而 String 变量的引用 + // 与 String 值的整个切片是等价的 + let word = first_word(&s); + println! ("{}", word); + + let s_string_literal = "hello word"; + + // 函数 first_word 在字符串字面值上有效,不论是部分还是整体 + let word = first_word(&s_string_literal[0..6]); + println! ("{}", word); + + let word = first_word(&s_string_literal[..]); + println! ("{}", word); + + // 由于字符串字面值已经 是 字符串切片,因此无需切片语法,这 + // 也是有效的! + let word = first_word(s_string_literal); + + println! ("{}", word); +} +``` + +### 其他切片 + +或许已经想到,字符串切片是特定于字符串的。然而还有更多通用切片类型呢。请看下面这个数组: + +```rust +let a = [1, 2, 3, 4, 5]; +``` + +就跟要引用字符串的部分一样,也可能要引用数组的部分。那么就将像下面这样,来完成对数组一部分的引用: + +```rust + + let a = [1, 2, 3, 4, 5]; + + let slice = &a[1..3]; + + assert_eq! (slice, &[2, 3]); +``` + +这个切片变量 `slice` 的类型为 `&[i32]`。数组切片的原理与字符串切片一样,都是经由存储到首个元素的引用,和切片长度实现的。今后将对所有类别的其他集合,运用到这种切片。在第 8 章讲到各种矢量时,就会对这些集合加以讨论。 + + +## 本章小结 + +所有权、借用及切片等概念,在编译时确保了 Rust 程序中的内存安全。Rust 语言所给到的对内存运用的掌控方式,与别的系统编程语言相同,但会让数据的所有者,在其超出作用域时,自动清理掉其数据,这就意味着咱们不必编写并调试额外代码,来实现这种控制。 + +所有权对 Rust 程序的许多其他部分都有影响,因此在本书其余部分,都将更进一步的涉及到这些所有权的概念。接下来就要移步第 5 章,而在结构体 `struct` 中,如何将小块数据组装起来。 diff --git a/src/Ch05_Using_Structs_to_Structure_Related_Data.md b/src/Ch05_Using_Structs_to_Structure_Related_Data.md new file mode 100644 index 0000000..c076dd3 --- /dev/null +++ b/src/Ch05_Using_Structs_to_Structure_Related_Data.md @@ -0,0 +1,693 @@ +# 运用结构体来组织相关数据 + +**Using Structs to Structure Related Data** + +*结构体(struct)*,或者说 *结构(structure)*,实现了将多个相关值打包在一起,并取个名字,而构成一个有意义的组别。在熟悉面向对象语言的情况下,那么 *结构体* 就像是对象的那些数据属性。在本章中,将把元组与结构体加以比照,从而在既有认识之上,构建出对结构体的认识,并对使用结构体作为一种更佳的数据组织方式的时机,进行演示。这里会对如何定义及初始化结构体进行演示。还会讨论如何定义关联函数,尤其是那种叫做 *方法* 的关联函数,来指明与某个结构体类型相关联的行为。结构体与枚举(将在第 6 章讨论到),这两种数据结构,是充分利用 Rust 的编译时类型检查特性,在程序域中创建新类型的构件。 + +## 结构体的定义及初始化 + +结构体与之前 [元组类型](Ch03_Common_Programming_Concepts.md#the-tuple-type) 小节中讨论过的元组数据结构类似,二者都保存着多个相关数据。和元组一样,结构体的各个数据片段可以是不同类型。与原则不同的是,在结构体中将给各个数据片段命名,如此各个值表示什么就清楚了。加上这些名字,就意味着相比于元组更为灵活了:不必为了给某个实例指定他的那些值,或要访问实例的那些值,而对实例数据的顺序有所依赖了。 + +要定义出一个结构体,就要敲入关键字 `struct`,及整个结构体的名字。结构体名字,应对安排在一起的这些数据片段的意义加以描述。随后,就要这一对花括号里头,定义出各个数据片段的名称与类型,这些数据片段,就叫做 *字段(fields)*。比如,下面的清单 5-1 就给出了一个保存用户账号信息的结构体。 + +```rust +struct User { + active: bool, + username: String, + email: String, + sign_in_count: u64 +} +``` + +*清单 5-1:`User` 结构体的定义* + +在定义出了结构体后,要用上这个结构体,就要通过给各个字段指定具体值,创建出那个结构体的 *实例(instance)* 来。通过指明结构的名字,并随后加上包含了 `key: value` 键值对的一对花括号,这样创建出一个实例来。键值对中的那些键,就是那些字段的名字,而其中的那些值,则是打算保存在这些字段中的数据。不必按照在结构体中声明那些字段的顺序,来对这些字段进行指明(we don't have to specify the fields in the same order in which we declared them in the struct)。也就是说,结构体定义就如同该类型的通用模板,而实例则将特定数据填充到那个木板中,从而创建出这个类型的值来。比如,就可如下面清单 5-2 中所展示的那样,声明出一个特定的用户来: + +```rust +fn main() { + let user1 = User { + email: String::from("rust@xfoss.com"), + username: String::from("unisko"), + active: true, + sign_in_count: 1 + }; +} +``` + +*清单 5-2:创建出结构体 `User` 的一个实例来* + +而要从结构体中获取到指定值,就要使用点表示法(`.`)。在要的仅是该用户的电子邮件地址时,就可以在那些要用到这个值的地方,使用 `user1.email` 。而在该实例为可变时,那么就可以通过使用点表示法,进而给特定字段赋值,而对某个值加以修改。下面的清单 5-3 展示了如何来修改某个可变 `User` 实例 `email` 字段中的值。 + +文件名:`src/main.rs` + +```rust +fn main() { + let mut user1 = User { + email: String::from("rust@xfoss.com"), + username: String::from("unisko"), + active: true, + sign_in_count: 1 + }; + + user1.email = String::from("java@xfoss.com"); +} +``` + +*清单 5-3:对某个 `User` 实例中的 `email` 字段进行修改* + +请注意这整个实例必须是可变的;Rust 不允许仅将一些字段标记为可变。与所有表达式一样,可以函数体中最后的表达式形式,构造出结构体的新实例,来隐式地返回那个新实例(as with any expression, we can construct a new instance of the struct as the last expression in the function body to implicity return that new instance)。 + +下面的清单 5-4,展示了一个以给定电子邮件和用户名,返回一个 `User` 实例的 `build_user` 函数。其中的 `active` 字符会得到值 `true`,而那个 `sign_in_count` 则会得到值 `1`。 + +```rust +fn build_user(email: String, username: String) -> User { + User { + email: email, + username: username, + active: true, + sign_in_count: 1, + } +} +``` + +*清单 5-4:一个取得电子邮件和用户名,并返回一个 `User` 实例的 `build_user` 函数* + +将函数参数命名为与结构体字段同样的名字,是有意义,但由此而不得不重复那 `email` 与 `username` 的字段名字与变量,就有点烦人了。在结构体有更多字段时,这样重复各个名字就会变得更加烦人。幸运的是,有种方便的简写法! + + +### 使用字段初始化简写法 + +由于在清单 5-4 中的参数名字与结构体字段名字完全一样,因此就可以 *字段初始化简写(field init shorthand)* 语法,来重写 `build_user` 方法,如此一来,`build_user` 函数在没有 `email` 与 `username` 重复的情况下,也有与之前版本同样的表现,如下清单 5-5 所示: + +```rust +fn build_user(email: String, username: String) -> User { + User { + email, + username, + active: true, + sign_in_count: 1, + } +} +``` + +*清单 5-5:由于 `email` 与 `username` 参数与结构体字段有着同样名字,而使用了字段初始化简写的 `build_user` 函数* + +在这里,正创建一个 `User` 结构体的新实例,该结构体有一个名为 `email` 的字段。这里打算将 `email` 字段的值,设置为 `build_user` 函数的 `email` 参数中的值。由于 `email` 字段与 `email` 参数有着同样的名字,因此只就需写下 `email`,而非 `email: email`。 + + +### 使用结构体更新语法,从其他实例创建出实例 + +创建出包含另一实例绝大部分值,而修改一些值的新实例,通常是有用的做法。而使用 *结构体更新语法(struct update syntax)* 就能做到这点。 + +首先,在下面的清单 5-6 中展示了如何按常规,不使用更新语法的情况下,创建出在 `user2` 中的一个新 `User` 实例。这里给 `email` 设置了一个新的值,而在其他方面,则使用了来自之前在清单 5-1 中创建的 `user1` 的那些同样值。 + +```rust +fn main() { + // --跳过代码-- + + let user2 = User { + active: user1.active, + username: user1.username, + email: String::from("java@xfoss.com"), + sign_in_count: user1.sign_in_count, + }; +} +``` + +*清单 5-6:使用一个 `user1` 的值创建出一个新的 `User` 实例* + +而使用结构体更新语法,就可以较少代码,达成同样效果,如下面的清单 5-7 中所给出的那样。其中的 `..` 语法,指明了未显式设置的其余字段,将有着与所给实例中的字段同样的值。 + +```rust +fn main() { + // --跳过代码-- + + let user2 = User { + email: String::from("java@xfoss.com"), + ..user1 + }; +} +``` + +*清单 5-7:使用结构体更新语法来设置 `User` 实例的 `email` 字段值,而使用来自 `user1` 的其余值* + +清单 5-7 中的代码同样创建了在变量 `user2` 中,一个有着 `email` 的不同值,但有着来自 `user1` 的 `username`、`active` 及 `sign_in_count` 同样值。其中的 `..user1` 必须要在最后,这样来指明全部剩余字段都应从 `user1` 中的相应字段获取值,但对于其他字段值的指定,则可选择所要的任意字段,以任意顺序进行,而不论在结构体定义中这些字段的顺序为何(the `..user1` must come last to specify that any remaining fields should get their values from the corresponding fields in `user1`, but we can choose to specify values for as many fields as we want in any order, regardless of the order of the fields in the struct's definition)。 + +请注意结构体更新语法,像赋值一样使用了 `=`;这是由于结构体更新语法迁移了数据,就跟在之前的 ["变量与数据互动方式:迁移"](Ch04_Understanding_Ownership.md#ways-variables-and-data-interact-move) 小节中看到的那样。在此示例中,在创建了 `user2` 之后,由于变量 `user1` 中的 `username` 字段中的 `String` 值,已被迁移到 `user2` 中了,因此就再也不能使用变量 `user1` 了。若给到 `user2` 的 `email` 及 `username` 字段都是新的 `String` 值,而因此只使用来自 `user1` 的 `active` 和 `sign_in_count` 值,那么在创建了 `user2` 之后,`user1` 仍将是有效的。因为 `active` 和 `sign_in_count` 的类型,都是实现了 `Copy` 特质的类型,因此就会应用在 [唯栈数据:拷贝](Ch04_Understanding_Ownership.md#stack-only-data-copy) 小节中的行为表现。 + + +### 使用不带命名字段的元组结构体来创建不同类型 + +**Using Tuple Structs without Named Fields to Create Different Types** + +Rust 还支持看起来像元组的结构体,叫做 *元组结构体(tuple structs)*。元组结构体这一类型,多了类型名称中结构体这一部分所提供的意义,却并没有与各字段相关联的名字;而是,元组结构体他们那些字段的类型。在要给予整个元组一个名字,并令到元组成为不同于其他元组的一种类型,且在如同在常规结构体中那样,给各个字段取名字是多余的等等,在这样的情况下,元组结构体就会有用。 + +要定义一个元组结构体,就要以 `struct` 关键字和该结构体的名字开头,接着是一些在元组中的类型。比如,下面分别定义和使用了两个元组结构体 `Color` 与 `Point`: + +```rust +struct Color(i32, i32, i32); +struct Point(i32, i32, i32); + +fn main() { + let black = Color(0, 0, 0); + let white = Color(255, 255, 255); + let origin = Point(0, 0, 0); +} +``` + +请注意,由于这里的 `black` 与 `origin` 两个值是不同元组结构体的实例,因此他们属于不同类型。尽管结构体里的那些字段有着同样类型,对于所定义每个结构体,都是其自身的类型。比如,某个接收类型 `Color` 参数的函数,就无法接收 `Point` 值做参数,尽管这两种类型都是由三个 `i32` 值构成的。除此之外,元组结构体的实例,与元组表现一样:可将他们解构为三个独立部分,可使用 `.` 后面跟上索引,来访问单独值,等等。 + + +### 没有字段的类单元结构 + +**Unit-Like Structs Without Any Fields** + +还可以定义没有任何字段的结构体!由于这些没有任何字段的结构体,与曾在 [元组类型](Ch03_Common_Programming_Concepts.md#the-tuple-type) 小节提到过的单元类型 `()` 表现类似,因此他们叫做 *类单元结构体(unit-like structs)*。当需要在某类型上实现某个特质(trait),却又不希望将任何数据存储在那个类型自身里面时,类单元结构体就就有用(unit-like structs can be useful when you need to implement a trait on some type but don't have any data that you want to store in the type itself)。在第 10 章就会讨论到特质。下面是一个声明和初始化名为 `AlwaysEqual` 的单元结构体的示例: + +```rust +struct AlwaysEqual; + +fn main() { + let subject = AlwaysEqual; +} +``` + +要定义出 `AlwaysEqual`,就要使用 `struct` 关键字、想要的名字,随后一个分号即可。是不需要花括号或圆括号的!随后就可以类似方式,得到一个在 `subject` 变量中的 `AlwaysEqual` 的示例了:使用定义的名字,不带任何花括弧或原括弧。设想稍后就要将此类型的表现,实现为每个 `AlwaysEqual` 的实例,总是等于任何其他类型的每个实例,这样做或许是为测试目的,而要有这样的已知结果(imagine that later we'll implement behavior for this type such that every instance of `AlwaysEqual` is always equal to every instance of any other type, perhaps to have a known result for testing purposes)。对于这样的行为表现,是不需要任何数据的!在第 10 章就会看到怎样定义特质,以及在包括类单元结构体在内的任何类型上,怎样实现特质。 + +> **结构体数据的所有权** +> +> 在前面清单 5-1 中的 `User` 结构体定义里,使用的是带有所有权的 `String` 类型,而非 `&str` 字符串切片类型。由于那里是要该结构体的各个实例拥有他自己的数据,且是要在整个结构体有效期间,实例数据有效,因此那里使用 `String` 类型而非 `&str` 类型就是有意而为之的了。 +> +> 结构体存储到其他变量持有数据的引用,也是可能的,但这样做就需要用到 *生命周期(lifetimes)*,而生命周期则是会在后面的第 10 章会讨论到的一个 Rust 特性。生命周期确保某个结构体引用到的数据,会在该结构体有效期间保持有效。譬如说如同下面这样,在尝试在某个结构体中存储不带生命周期的引用时;这就不会工作: +> +> 文件名:`src/main.rs` + +```rust +struct User { + active: bool, + username: &str, + email: &str, + sign_in_count: u64, +} + +fn main() { + let user1 = User { + email: "someone@example.com", + username: "someusername123", + active: true, + sign_in_count: 1, + }; +} +``` + +> 编译器会抱怨他需要生命周期说明符: + +```console +$ cargo run + Compiling structs_demo v0.1.0 (/home/peng/rust-lang/projects/structs_demo) +error[E0106]: missing lifetime specifier + --> src/main.rs:3:15 + | +3 | username: &str, + | ^ expected named lifetime parameter + | +help: consider introducing a named lifetime parameter + | +1 ~ struct User<'a> { +2 | active: bool, +3 ~ username: &'a str, + | + +error[E0106]: missing lifetime specifier + --> src/main.rs:4:12 + | +4 | email: &str, + | ^ expected named lifetime parameter + | +help: consider introducing a named lifetime parameter + | +1 ~ struct User<'a> { +2 | active: bool, +3 | username: &str, +4 ~ email: &'a str, + | + +For more information about this error, try `rustc --explain E0106`. +error: could not compile `structs_demo` due to 2 previous errors +``` + +> 在第 10 章中,就会讨论怎样来修复这些错误,尔后就可以在结构体中存储引用变量了,而至于现在,则只会使用像是 `String` 这样的具有所有权的类型,而避开使用像是 `&str` 这样的引用,来解决这个问题。 + + +## 一个使用结构体的示例程序 + +为搞明白何时会想要使用结构体,下面就来编写一个计算矩形面积的程序。这里会先从使用单个变量开始,并在随后对这个程序进行重构,直到使用结构体为止。 + +下面就来以 `Cargo` 构造一个名为 `rectangles` 的新二进制项目,该项目将取得以像素指定的矩形宽和高,并计算出该矩形的面积。下面的清单 5-8 给出了一个简短的程序,该程序正是有着在这个项目的 `src/main.rs` 中的做法: + +```rust +fn main() { + let width1 = 30; + let height1 = 50; + + println! ( + "该矩形的面积为 {} 平方像素。", + area(width1, height1) + ); +} + +fn area(width: u32, height: u32) -> u32 { + width * height +} +``` + +*清单 5-8:计算由单独宽和高变量指明的矩形面积* + +现在,使用 `cargo run` 允许这个程序: + +```console +$ cargo run + Compiling rectangles v0.1.0 (/home/peng/rust-lang/projects/rectangles) + Finished dev [unoptimized + debuginfo] target(s) in 0.17s + Running `target/debug/rectangles` +该矩形的面积为 1500 平方像素。 +``` + +这段代码通过以两个边长调用 `area` 函数,而成功计算出了该矩形的面积,不过还可以进一步让这段代码更为清晰已读。 + +这段代码的问题,体现在 `area` 函数签名中: + +```rust +fn area(width: u32, height: u32) -> u32 { +``` + +`area` 函数是要计算某个矩形面积的,但这里编写的该函数,有着两个参数,同时在这个程序中,并未清楚表明那两个参数是有联系的。将宽和高组织在一起,代码就会更具易读性,且更具可管理性。在第 3 章的 [元组类型](Ch03_Common_Programming_Concepts.md#the-tuple-type) 小节,就已讨论过一种可能那样做的方式:使用元组。 + + +### 以元组进行重构 + +下面的清单 5-9 给出了使用了元组的另一版本的这个程序。 + +文件名:`src/main.rs` + +```rust +fn main() { + let rect1 = (30, 50); + + println! ( + "该矩形的面积为 {} 平方像素。", + area(rect1) + ); +} + +fn area(dimensions: (u32, u32)) -> u32 { + dimensions.0 * dimensions.1 +} +``` + +*清单 5-9:以一个元组来对矩形的宽和高进行指定* + +一方面,这个程序更好了。元组实现了一些代码结构的加入,且现在传递的只有了一个参数。但在另一方面,这个版本变得更不清楚了:元组不会给他的各个元素命名,因此就不得不索引到该元组的各部分,从而令到这里的计算不那么直观了。 + +将宽和高混合起来对于面积计算并不重要,但在要将这个矩形绘制在屏幕上时,那就会有影响了!那时就必须要记住元组中索引 `0` 的是 `width`,而 `height` 是索引 `1`。这对那些将要使用到这代码的其他人来说,将会更难。由于没有在代码中传达数据的意义,因此现在更易于引入错误。 + +### 以结构体进行重构:加入更多意义 + +这里要使用结构体,通过给数据打上标签,来加入更多意义。可将这里正在使用的元组,以给整体命名,同时还给那些部分命名,而转换成为一个结构体。如下清单 5-10 所示。 + +文件名:`src/main.rs` + +```rust +struct Rectangle { + width: u32, + height: u32, +} + +fn main() { + let rect1 = Rectangle { + width: 30, + height: 50, + }; + + println! ( + "该矩形的面积为 {} 平方像素。", + area(&rect1) + ); +} + +fn area(rectangle: &Rectangle) -> u32 { + rectangle.width * rectangle.height +} +``` + +*清单 5-10:定义一个 `Rectangle` 结构体* + +这里就已定义了一个结构体,并将其命名为了 `Rectangle`。在那对花括弧内部,以 `width` 和 `height` 定义了两个字段,两个字段都具有 `u32` 类型。随后在 `main` 函数中,创建出了 `Rectangle` 的一个宽为 `30`,高为 `50` 的特定实例。 + +现在的 `area` 函数被定义为带有一个参数,该参数被命名为 `rectangle`,其类型是结构体 `Rectangle` 实例的不可变借用。如同在第 4 章中提到的那样,这里是要借用那个结构体,而非要取得那个结构体的所有权。在此方式下,`main` 函数仍保留着那个结构体实例的所有权,进而可继续使用变量 `rect1`,这就是在函数 `area` 签名与函数调用中,使用 `&` 符号的原因。 + +`area` 函数会访问那个 `Rectangle` 实例的 `width` 和 `height` 字段。`area` 的函数签名现在表达的正是这里想要的了:使用 `Rectangle` 的 `width` 和 `height` 字段,计算出他的面积。这就传达出了这里的宽与高是相互关联,同时这样做还给到了这些值描述性的名称,而非使用之前元组的索引 `0` 和 `1` 了。这在代码清晰上得了一分。 + + +### 使用派生特质加入有用功能 + +**Adding Useful Functionality with Derived Traits** + +如果能在调试程序期间打印出 `Rectangle` 的实例,并查看到所有字段的值,那就会派上用场。下面的清单 5-11 尝试了使用之前各章已经用到 [`println!` 宏](https://doc.rust-lang.org/std/macro.println.html)。不过这段代码不会工作。 + +```rust +struct Rectangle { + width: u32, + height: u32, +} + +fn main() { + let rect1 = Rectangle { + width: 30, + height: 50, + }; + + println! ("rect1 为:{}", rect1); +} +``` + +*清单 5-11:尝试打印出一个 `Rectangle` 实例* + +在编译这段代码时,会得到有着以下核心消息的错误: + +```console +error[E0277]: `Rectangle` doesn't implement `std::fmt::Display` +``` + +`println!` 宏可完成许多种类的格式化,而默认情况下,那对花括号告诉 `println!` 的是,要使用名为 `Display` 的格式化操作:即用于最终用户直接消费的输出(the `println!` macro can do many kinds of formatting, and by default, the curly brackets tell `println!` to use formatting known as `Display`: output intended for direct end user consumption)。因为在要将一个 `1` 或其他任何原生类型,展示给用户时,都只有唯一的一种方式,因此,对于至今为止已见到过的那些原生类型来说,默认都是实现了 `Display` 的。而对于结构体来说,由于存在更多的显示可能:是要逗号还是不要?要打印出那对花括号吗?所有字段都要展示出来吗?因此 `println!` 对输出进行格式化的方式,就不那么清楚了。正是因为这种模棱两可,Rust 于是就不尝试猜测代码编写者想要的样子,而结构体也就没有一个事先提供的、与 `println!` 和 `{}` 占位符一起使用的 `Display` 实现了。 + +在继续阅读该错误消息时,就会发现下面这个有用注解: + +```console + = help: the trait `std::fmt::Display` is not implemented for `Rectangle` + = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead +``` + +来试一下!这个 `println!` 的宏调用,现在看起来是这样 `println! ("rect1 为 {:?}", rect1);`。将说明符 `:?` 放在那对花括号里头,就会告诉 `println!`,这里是要使用一个名为 `Debug` 的输出。而 `Debug` 特质就令到这里可将那个结构体,以对开发者有用的方式打印出来,如此就可以在对代码进行调试时,看到那个结构体的值了。 + +在此改变下,对该代码进行编译。见鬼!还是得到个错误: + +```console +error[E0277]: `Rectangle` doesn't implement `Debug` +``` + +不过编译器再度给到一个帮助性注释: + +```console + = help: the trait `Debug` is not implemented for `Rectangle` + = note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle` +``` + +Rust *确实* 带有打印输出调试信息的功能,不过这里必须显式地选择上那功能,从而使得那功能对这个结构体可用。而要实现这个目的,就要在紧接着结构体定义之前,加上外层属性 `#[derive(Debug)]`(the outer attribute `#[derive(Debug)`),如下面的清单 5-12 所示。 + +文件名:`src/main.rs` + +```rust +#[derive(Debug)] +struct Rectangle { + width: u32, + height: u32, +} + +fn main() { + let rect1 = Rectangle { + width: 30, + height: 50, + }; + + println! ("rect1 为:{:?}", rect1); +} +``` + +*清单 5-12:加入派生 `Debug` 特质的属性,进而运用调试格式化将那个 `Rectangle` 实例打印出来* + +此时在运行这个程序时,就不会收到任何错误了,且会看到下面的输出: + +```console +$ cargo run + Compiling rectangles v0.1.0 (/home/peng/rust-lang/projects/rectangles) + Finished dev [unoptimized + debuginfo] target(s) in 0.20s + Running `target/debug/rectangles` +rect1 为:Rectangle { width: 30, height: 50 } +``` + +很棒!这虽不是最漂亮的输出,但他给出了该实例全部字段的值,这无疑在调试期间会有帮助。在有着较大的结构体时,让输出更容易阅读一点就会有用;对于那些更大结构体的情形,就可在 `println!` 中使用 `{:#?}` 而非 `{:?}`。而在这个示例中,使用 `{:#?}` 样式将输出: + +```console +cargo run + Compiling rectangles v0.1.0 (/home/peng/rust-lang/projects/rectangles) + Finished dev [unoptimized + debuginfo] target(s) in 0.18s + Running `target/debug/rectangles` +rect1 为:Rectangle { + width: 30, + height: 50, +} +``` + +使用 `Debug` 格式化将某个值打印出来的另一种方式,就是使用 [`dbg!` 宏](https://doc.rust-lang.org/std/macro.dbg.html),这个 `dbg!` 宏会占据某个表达式的所有权,而将那个 `dbg!` 宏调用出现在代码中所在的文件与行号,与那个表达式的结果值一并打印出来,同时返回结果值的所有权(another way to print out a value using the [`dbg!` macro](https://doc.rust-lang.org/std/macro.dbg.html), which takes ownership of an expression, prints the file and line number of where that `dbg!` macro call occurs in your code along with the resulting value of that expression, and returns ownership of the value)。 + +> 注意:对 `dbg!` 宏的调用,会打印到标准错误控制台流(the standard error console stream, `stderr`),这与 `println!` 宏打印到标准输出控制台流(the standard output console stream, `stdout`)相反。在第 12 章中的 [将错误消息写到标准错误而非标准输出](Ch12_An_I_O_Project_Building_a_Command_Line_Program.md#writing-error-messages-to-standard-error-instead-of-standard-output) 小节,将讲到更多有关 `stderr` 与 `stdout` 的内容。 + +以下是个其中对赋值给 `width` 字段,以及在变量 `rect1` 中的整个结构体的值感兴趣的示例: + +```rust +#[derive(Debug)] +struct Rectangle { + width: u32, + height: u32, +} + +fn main() { + let scale = 2; + + let rect1 = Rectangle { + width: dbg! (30 * scale), + height: 50, + }; + + dbg! (&rect1); +} +``` + +这里可将 `dbg!` 放在表达式 `30 * scale` 附近,同时由于 `dbg!` 返回了该表达式值的所有权,因此 `width` 字段将获取到与不在此处调用 `dbg!` 同样的值。由于这里不想要 `dbg!` 取得 `rect1` 的所有权,因此在下一个对 `dbg!` 的调用中,使用到到 `rect1` 的引用。下面就是这个示例输出的样子: + +```console +cargo run + Compiling rectangles v0.1.0 (/home/peng/rust-lang/projects/rectangles) + Finished dev [unoptimized + debuginfo] target(s) in 0.22s + Running `target/debug/rectangles` +[src/main.rs:11] 30 * scale = 60 +[src/main.rs:15] &rect1 = Rectangle { + width: 60, + height: 50, +} +``` + +这里就可以看到,输出的第一部分来自 `src/main.rs` 文件的第 10 行,正是对表达式 `30 * scale` 进行调式的地方,而该表达式的结果值即为 `60`(在整数原生值上实现的 `Debug` 格式化只打印他们的值)。在 `src/main.rs` 第 14 行上的 `dbg!` 调用,输出了 `rect1`,即那个 `Rectangle` 结构体的值。这个输出使用了 `Rectangle` 类型的良好 `Debug` 格式化。在尝试搞清楚代码在做什么时,这个 `dbg!` 宏真的会相当有用! + +除 `Debug` 特质外,Rust 业已提供了数个与 `derive` 属性一起使用的其他特质,这些特质把有用的行为表现,添加到那些定制类型。Rust 提供的那些特质及其行为,在 [附录 C](Ch21_Appendix.md#c-derivable-traits) 小节中有列出。在第 10 章中,就会涉及到怎样去实现这些有着定制行为的特质,以及怎样创建自己的特质。除了 `derive` 之外,同样还有许多别的属性;有关属性的更多信息,请参阅 [Rust 参考手册的 “属性” 小节](https://doc.rust-lang.org/reference/attributes.html)。 + +这里的 `area` 函数,是相当专用的:他只会计算矩形的面积。由于 `area` 方法不会在其他任何类型上工作,因此将此行为与这里的 `Rectangle` 结构体更紧密的联系起来,就会变得有帮助。接下来就要看看,怎样通过将这个 `area` 函数,转变成一个定义在这里的 `Rectangle` 类型上的方法,而继续重构这段代码。 + + +## 方法语法 + +*方法* 与函数类似:是以 `fn` 关键字和一个名称,来声明出方法,方法可以有参数和返回值,同时包含了在某个地方方法被调用时,运行的一些代码。与函数不同的地方在于,方法是在结构体(或者枚举或特质对象,关于枚举即特质对象,将分别在第 6 和 17 章讲到)的语境里面定义的,且方法的首个参数将始终是 `self`,这个 `self` 表示方法被调用的那个结构体实例本身。 + + +### 方法的定义 + +下面就来将那个将一个 `Rectangle` 实例作为参数的 `area` 函数,修改为定义在 `Rectangle` 结构体上的 `area` 方法,如下清单 5-13 所示: + +```rust +#[derive(Debug)] +struct Rectangle { + width: u32, + height: u32, +} + +impl Rectangle { + fn area(&self) -> u32 { + self.width * self.height + } +} + +fn main() { + let rect1 = Rectangle { + width: 30, + height: 50, + }; + + println! ("该矩形的面积为 {} 平方像素。", + rect1.area() + ); +} +``` + +*清单 5-13:在 `Rectangle` 结构体上定义一个 `area` 方法* + + +为定义 `Rectangle` 上下文中的函数,这里开启了一个 `Rectangle ` 的 `impl` (implementation)代码块。此 `impl` 代码块里头的所有物件,都会与那个 `Rectangle` 类型相关联。随后这里就把原来那个 `area` 函数,移入到这个 `impl` 的花括弧里,并将函数签名中的首个(而在此情形下,也是唯一的)参数,及函数体中的各处,均修改为 `self`。在 `main` 函数,即原先调用 `area` 函数与将 `rect1` 作为参数传递的地方,现在就可以使用 *方法语法* 来调用那个在 `Rectangle` 实例上的 `area` 方法了。方法语法(the method syntax)是在实例之后:添加一个带着方法名字、括号及全部参数的点。 + +在 `area` 的签名中,使用了 `&self` 而不再是 `rectangle: &Rectangle`。这个 `&self` 实际上是 `self: &Self` 的简写。在 `impl` 代码块内部,类型 `Self` 就是该 `impl` 代码块所针对的类型。方法必定有着这么一个名为 `self` 类型为 `Self` 的参数,作为他们的首个参数,因此 Rust 这才允许将首个参数位置上的该参数,简写为只是他的名称 `self`。请注意这里仍然需要在 `self` 简写前使用 `&` 运算符,来表示此方法借用了 `Self` 类型的实例,这就跟 `rectangle: &Rectangle` 一样。方法可以取得 `self` 的所有权的、不可变地借用 `self` 变量,或者可变地借用 `self` 变量,对于方法的其他参数,也是这样的。 + +> `&self` - 不可变借用;`&mut self` 可变借用;`self` - 取得所有权,发生所有权转移,`self` 所指向的内存堆上的值原来的所有值将失效。 + +这里选择了 `&self`,有着与方法版本中使用 `&Rectangle` 有着同样理由:那就是不打算取得所有权,同时只打算读取结构体中的数据,而不打算写入。在作为方法要执行的一部分,要修改方法调用所在实例时,就要使用 `&mut self` 作为首个参数了。通过仅使用 `self` 作为首个参数,而取得实例所有权的情况,就非常少见了;通常在要将 `self` 转换为其他类型的数据,而要在这样的转换之后,阻止其他调用者使用原先的实例时,会用到这样的技巧。 + +使用方法而不是函数的主要原因,除了提供到方法语法及不必在每个方法签名中重复 `self` 的类型外,那就是为了代码的组织了。这里已将可由某个类型实例完成的事情,放在一个 `impl` 代码块中,而不是要那些后来的代码使用者,在这里提供的库的各个地方去找寻 `Rectangle` 的那些能力。 + +请注意可选择给方法一个与结构体字段相同的名字。比如,这里就可以在 `Rectangle` 上定义一个同样命名为 `width` 的方法: + +文件名:`src/main.rs` + +```rust +impl Rectangle { + fn width(&self) -> bool { + self.width > 0 + } +} + +fn main() { + let rect1 = Rectangle { + width: 30, + height: 50, + }; + + if rect1.width() { + println! ("该矩形的宽不为零;他的宽为 {}", rect1.width); + } +} +``` + +这里,就选择了让 `width` 方法,在实例的 `width` 字段中的值大于 `0` 时返回 `true`,在值为 `0` 时返回 `false`:在名称与某个字段相同的方法里面,可将该字段用于任何目的。在 `main` 方法中,当这里在 `rect1.width` 后跟上一对圆括号时,那么 Rust 就明白这里指的是方法 `width`了。而在没有使用一对圆括号时,Rust 就知道那里表示的是字段 `width`。 + +通常,但并非总是这样,在给到方法与某个字段同样名字时,要的是让那个方法返回与其同名字段中的值,而不会去干别的事情。像这样的方法,就叫做 *获取器(getters)*,而 Rust 并未像其他语言所做的那样,自动实现结构体字段的获取器。由于可将字段构造为私有,而将方法构造为公开,而由此实现对作为类型的公开 API一部分的字段的只读访问。在第 7 章中就会讨论到何为公开与私有,以及怎样将字段或方法指定为公开或私有。 + +#### `->` 操作符(the `->` operator)哪去了呢? +> +> 在 C 和 C++ 中,方法调用会用到两个操作符:直接调用在对象上的方法时,要用到 `.`,而在对象的指针上调用方法时,则要用 `->` 操作符,这时还先要对该指针解除引用。换句话说,在 `object` 是个指针时,`object -> something()` 是类似于 `(*object) -> something()` 的。 +> Rust 并无 `->` 操作符的等价操作符;相反,Rust 有着一项名为 *自动引用与解引用(automatic referencing and dereferencing)* 的特性。而方法调用就是 Rust 中有着这种行为表现的少数几个地方之一。 +> +> 以下就是该特性的工作原理:在以 `object.something()` 调用某个方法时,Rust 会自动加上 `&`、`&mut` 或 `*`,这样 `object` 就会匹配上该方法的签名。换句话说,下面的语句是一致的: +> +```rust +p1.distance(&p2); +(&p1).distance(&p2); +``` +> +> 第一个语句看起来要清楚不少。由于方法有着明确的接收者 -- 即 `self` 的类型,因此这种自动引用的行为会生效。在给定了接收者和方法名字后,Rust 就可明确地确定出该方式到底是在读取(`&self`)、改变(`&mut self`),或者是在消费(`self`)。Rust 实现了方法接收者的隐式借用这一事实,是为实现所有权系统在编程实践中符合人机交互,而所做努力的较大部分(the fact that Rust makes borrowing implicit for method receivers is a big part of making ownership ergonomic in practice)。 + +### 有多个参数的方法 + +下面就来通过在 `Rectangle` 结构体上实现另一个方法,练习一下方法的运用。这次就要 `Rectangle` 的一个实例,去取得另一个 `Rectangle` 的实例,并在第二个 `Rectangle` 完全能放入到 `self` (即第一个 `Rectangle` )里头时返回 `true`;否则这个方法就会返回 `false`。也就是,一旦定义了这个 `can_hold` 方法,就要能够编写下面清单 5-14 中的那个程序。 + +文件名:`src/main.rs` + +```rust +fn main() { + let rect1 = Rectangle { + width: 30, + height: 50, + }; + + let rect2 = Rectangle { + width: 10, + height: 40, + }; + + let rect3 = Rectangle { + width: 60, + height: 45, + }; + + println! ("rect1 可以装下 rect2 吗?{}", rect1.can_hold(&rect2)); + println! ("rect1 可以装下 rect3 吗?{}", rect1.can_hold(&rect3)); +} +``` + +*清单 5-14:对尚未成文的 `can_hold` 方法进行使用* + +由于 `rect2` 的两个边都小于 `rect1` 的两个边,而 `rect3` 的两个边都要长于 `rect1` 的两个边,因此预期的输出将看起来像下面这样: + +```console +rect1 可以装下 rect2 吗?true +rect1 可以装下 rect3 吗?false +``` + +这里知道要定义的是个方法,因此那将会在 `impl Rectangle` 代码块内部。而方法的名称将是 `can_hold`,同时他会取得作为参数的另一 `Rectangle` 值的不可变借用。通过观察调用该方法的代码,就可以得出那个参数的类型了:`rect1.can_hold(&rect2)` 传入的是 `&rect2`,正是到变量 `rect2` 的不可变借用,而 `rect2` 又是 `Rectangle` 的一个实例。由于这里只需要读取 `rect2`(而非写入,那就意味着将需要一个可变借用了),同时这里是想要 `main` 函数保留 `rect2` 的所有权,这样就可以在 `can_hold` 方法调用之后,还可以再度使用 `rect2`,因此这样做是有理由的。`can_hold` 方法的返回值,将是个布尔值,而该方法的实现会检查 `self` 的宽和高,相应地是否都大于另一个 `Rectangle` 的宽和高。下面就把这个新的 `can_hold` 方法,加入到清单 5-13 的 `impl` 代码块,如下清单 5-15 所示。 + +文件名:`src/main.rs` + +```rust +impl Rectangle { + fn area(&self) -> u32 { + self.width * self.height + } + + fn can_hold(&self, other: &Rectangle) -> bool { + (self.width > other.width && self.height > other.height) || + (self.width > other.height && self.height > other.width) + } +} +``` + +*清单 5-15:对在 `Rectangle` 上的、取另一 `Rectangle` 实例作为参数的 `can_hold` 方法进行实现* + +在以清单 5-14 中的 `main` 函数来运行此代码是,就会得到想要的输出。方法可取得在 `self` 参数之后添加到其签名的多个参数,同时这些参数就像函数中的参数一样生效。 + +### 关联函数(associated functions) + + +由于定义在 `impl` 代码块内部的全部函数,都是与那个在 `impl` 关键字之后命名的类型相关联的,因此他们都叫做 *关联函数(associated functions)*。因为一些关联函数不需要用到该类型的实例,因此可把这些函数定义为不将 `self` 作为首个参数的关联函数(而这样的话,这些函数就不是方法了)。前面就已用到过这样的一个关联函数:`String::from` 函数就是定义在 `String` 类型上的。 + +非方法的关联函数,通常用于将会返回一个该结构体新实例的构造函数。比如,这里就可提供有着一维参数,并将该一维参数同时用作宽和高的这么一个关联函数,如此就令到相比于两次指定同样值,而更容易创建除正方形的 `Rectangle`。 + +文件名:`src/main.rs` + +```rust +impl Rectangle { + fn square(size: u32) -> Rectangle { + Rectangle { + width: size, + height: size, + } + } +} +``` + +要调用这个关联函数,就要使用带有结构体名字的 `::` 语法;`let sq = Rectangle::square(3);` 就是一个示例;该函数是是在那个结构体的命名空间之下的:`::` 语法,同时用于关联函数,与由模组创建出的命名空间。在第 7 章会讨论到 Rust 的模组概念。 + +### 多个 `impl` 代码块 + +所有结构体都允许有多个 `impl` 代码块。比如前面的清单 5-15 就与下面清单 5-16 给出的代码等价,其中各个方法都在各自的 `impl` 代码块中: + +```rust +impl Rectangle { + fn area(&self) -> u32 { + self.width * self.height + } +} + +impl Rectangle { + fn can_hold(&self, other: &Rectangle) -> bool { + (self.width > other.width && self.height > other.height) || + (self.width > other.height && self.height > other.width) + } +} +``` + +*清单 5-16:使用多个 `impl` 代码块对清单 5-15 进行重写* + +虽然这里并无将这些方法分开到多个 `impl` 代码块中的理由,不过这样做也是有效的语法。在第 10 章讨论到泛型和特质时,就会看到多个 `impl` 代码块是有用的情形。 + +## 本章小节 + +结构体实现了创建对于特定领域有意义的定制类型。通过运用结构体,就可以将有关联的数据片段相互连接起来,并给各个数据取名字来让代码清晰。在 `impl` 代码块中,可定义与类型关联的函数,而方法则是一类实现了指定结构体实例所拥有行为的关联函数。 + +然而结构体并非能够创建定制类型的唯一方式:加下了就要转向到 Rust 的枚举特性,将另一工具加入到编程工具箱。 diff --git a/src/Ch06_Enums_and_Pattern_Matching.md b/src/Ch06_Enums_and_Pattern_Matching.md new file mode 100644 index 0000000..258a289 --- /dev/null +++ b/src/Ch06_Enums_and_Pattern_Matching.md @@ -0,0 +1,581 @@ +# 枚举与模式匹配 + +**Enums and Pattern Matching** + +在本章,将会对 *枚举(enumerations)* 进行审视,枚举也被当作 *enums*。枚举实现了通过列举出类型可能的 *变种(variants)*,来定义出一个类型。这里会首先定义并使用一个枚举,来展示枚举能如何将意义和数据编码起来。接下来,就会探索一个特别有用、名为 `Option` 的枚举,该枚举表示了某个值既可以是某事物,也可以什么也不是。随后就会看看在 `match` 表达式中的模式匹配,是怎样令到根据枚举中不同的值,而运行各异的代码容易起来的。最后,将会讲到 `if let` 结构是怎样成为另一种处理代码中枚举值的、便利而简洁的习惯用法的。 + +许多语言都有枚举这一特性,不过在各个语言中的枚举能力是不同的。Rust 的枚举与那些函数式语言,诸如 F#、OCaml 及 Haskell 等中的 *代数数据类型(algebraic data types)* 最为相似。 + +## 定义一个枚举 + +枚举是不同于结构体的第二种定义定制数据类型的方式。下面就来看看一种在代码中可能表达的情形,并见识一下为何在此情形下,相比于结构体,枚举是有用且更恰当的。假设说这里需要对 IP 地址进行处理。目前仅有两种用于 IP 地址的标准:版本四和版本六。由于这两个标准是程序将遇到的 IP 地址仅有的可能性,因此就可以 *列举出(enumerate)* 全部可能的变种,这正是枚举(enumeration) 名字得来之处。 + +任何 IP 地址都只能是版本四或版本六的地址,而不会同时两个都是。由于枚举值只能是枚举变种之一,那么 IP 地址的这个属性,令到枚举数据结构(the enum data structure)恰当起来。而版本四和版本六两种地址,从根本上说都是 IP 地址,那么在代码对适用于任意类别 IP 地址的情形加以处理时,版本四和版本六地址都应当作同一类型对待。 + +在代码中,可通过定义一个 `IpAddrKind` 枚举,并列出 IP 地址可能的类别,即 `V4` 和 `V6`,来表达这个概念。下面就是该枚举的变种: + +```rust +enum IpAddrKind { + V4, + V6, +} +``` + +现在 `IpAddrKind` 就是一个可在代码中别的地方使用的定制数据类型了。 + + +### 枚举取值 + +可像下面这样,创建出 `IpAddrKind` 两个变种的实例来: + +```rust + let four = IpAddrKind::V4; + let six = IpAddrKind::V6; +``` + +请注意,该枚举的两个变种,是在其标识符的命名空间之下的,且这里使用了双冒号将标识符和变种分隔开。由于现在这两个值 `IpAddrKind::V4` 与 `IpAddrKind::V6` 都是这同一类型:`IpAddrKind`,因此这就变得有用了。随后就可以,比如,定义一个取任意 `IpAddrKind` 类型值的函数: + +```rust +fn route(ip_kind: IpAddrKind) {} +``` + +进而就能以这两个变种对这个函数进行调用了: + +```rust + route(IpAddrKind::V4); + route(IpAddrKind::V6); +``` + +枚举的使用甚至还有更多好处。在还没有一种存储具体 IP 地址 *数据(data)* 的时候,就要进一步思考一下这里的 IP 地址类型;这是只知道 IP 地址数据为什么 *类别(king)*。根据在第 5 章中掌握的结构体知识,那么可能很想用下面清单 6-1 中的结构体来解决这个问题。 + +```rust +enum IpAddrKind { + V4, + V6, +} + +struct IpAddr { + kind: IpAddrKind, + address: String, +} + +fn main() { + let home = IpAddr { + kind: IpAddrKind::V4, + address: String::from("127.0.0.1"), + }; + + let loopback = IpAddr { + kind: IpAddrKind::V6, + address: String::from("::1"), + }; +} +``` + +*清单 6-1:使用结构体 `struct` 来存储 IP 地址的数据与 `IpAddrKind` 变种* + +这里已定义了有着两个字段的结构体 `IpAddr`:一个类型为 `IpAddrKind` (即先前定义的那个枚举)的 `kind` 字段,以及一个类型为 `String` 的 `address` 字段。这里有该结构体的两个实例。第一个是 `home`,而他有着与地址数据 `127.0.0.1` 关联的 `IpAddrKind::V4` 作为其 `kind` 的值。第二个实例为 `loopback`。这个实例则有不同的 `IpAddrKind` 变种作为其 `kind` 的值,即 `V6`,与 `kind` 关联的是地址 `::1`。由于这里使用了结构体将 `kind` 与 `address` 值捆绑在一起,因此现在这个 `IpAddrKind` 的变种就与那个 `String` 值关联起来了。 + +不过,仅使用一个枚举来表示这同一概念,就会更加简练:与其将枚举放在结构体内部,可将数据直接放在各个枚举变种里头。那么这新的 `IpAddr` 枚举定义,就是说 `V4` 与 `V6` 两个变种,将同时有着关联的 `String` 值: + +```rust +enum IpAddr { + V4(String), + V6(String), +} + +fn main() { + + let home = IpAddr::V4(String::from("127.0.0.1")); + + let loopback = IpAddr::V6(String::from("::1")); +} +``` + +这里把数据直接附加到枚举的各个变种上,因此就无需额外的结构体了。这里还更易于发现枚举工作原理的另一细节:所定义的各个枚举变种的名字,还成为了构造该枚举实例的函数。那就是说,`IpAddr::V4()` 现在是个取 `String` 参数并返回该 `IpAddr` 类型实例的函数调用了。作为定义枚举的结果,这里让这个构造函数自动就定义好了。 + +这里还有另一个使用枚举而非结构体的好处:各个变种可以有不同类型及数量的关联数据。版本四类型的 IP 地址,将始终有着四个会有着 `0` 到 `255` 之间值的数字部分。在希望将 `V4` 地址存储为四个 `u8` 值,而仍然将 `V6` 地址表示为一个 `String` 值时,那就没法用结构体了,而枚举则能轻易处理这样的情况: + +```rust +enum IpAddr { + V4(u8, u8, u8, u8), + V6(String), +} + +fn main() { + + let home = IpAddr::V4(127, 0, 0, 1); + + let loopback = IpAddr::V6(String::from("::1")); +} +``` + +到这里,就已经给出了好几种定义用于存储版本四和版本六 IP 地址的数据结构了。然而事实表明,想要存储 IP 地址,及对这些 IP 地址所属类别进行编码是如此普遍,以致 [标准库就有一个可加以使用的定义](https://doc.rust-lang.org/std/net/enum.IpAddr.html)!下面就来看看,标准库是怎样定义 `IpAddr` 的:他有着与这里曾定义和使用过的相同枚举和变种,不过标准库是将地址数据,以两个不同结构体的形式,嵌入到变种里的,对两个枚举变种,定义了不同的结构体。 + +```rust +struct Ipv4Addr { + // --跳过-- +} + +struct Ipv4Addr { + // --跳过-- +} + +enum IpAddr { + V4(Ipv4Addr), + V6(Ipv6Addr), +} +``` + +这段代码说明可将任何类别的数据放在枚举变种里面:比如字符串、数字类型,或结构体等等。甚至可以包含另一枚举!还说明了,标准库类型,通常也并不比咱们自己编写的代码复杂多少。 + +请注意,由于这里不曾将标准库的 `IpAddr` 定义带入到这里的作用域,因此即使标准库包含了一个 `IpAddr` 的定义,这里也仍然可以毫无冲突地创建与使用自己的 `IpAddr` 定义。在第 7 章就会讲到有关带入类型到作用域的问题。 + +来看看下面清单 6-2 中另一个枚举的示例:这个枚举有着嵌入到其各个变种中的种类繁多的类型。 + +```rust +enum Message { + Quit, + Move { x: i32, y: i32 }, + Write (String), + ChangeColor(i32, i32, i32), +} +``` + +*清单 6-2:每个变种都存储了不同数量和类型值的 `Message` 枚举* + +这个枚举有着四个带有不同类型数据的变种: + +- `Quit` 变种完全没有与其关联的数据; +- `Move` 变种像结构体一样,有着两个命名的字段; +- `Write` 变种包含了单个 `String`; +- `ChangeColor` 编程包含了三个 `i32` 的值。 + +定义一个有着一些如上面清单 6-2 中变种的枚举,与定义不同种类的结构体定义类似,不同在于枚举未使用关键字 `struct`,且所有变种在 `Message` 类型下组织在了一起。下面这些结构体,就可保存之前各个枚举变种所保存的那些同样数据: + +```rust +struct QuitMessage; // 单元结构体 +struct MoveMessage { + x: i32, + y: i32, +} +struct WriteMessage(String); // 元组结构体 +struct ChangeColorMessage(i32, i32, i32); // 元组结构体 +``` + +不过假如这里使用了不同的、有着各自类型的结构体,那么就无法轻易地定义出一个接收原本在清单 6-2 中定义的、单一类型的 `Message` 枚举那样的,接收全部这些类别消息的函数了。 + +枚举与结构体之间,还有另外一个相似点:正如在结构体上使用 `impl` 关键字定义出一些方法,在枚举上定义方法也是可以的。下面就是一个可定义在这里的 `Message` 枚举上、名为 `call` 的方法: + +```rust +enum Message { + Quit, + Move { x: i32, y: i32 }, + Write (String), + ChangeColor(i32, i32, i32), +} + +impl Message { + fn call(&self) { + // 方法体将定义在这里 + } +} + +fn main() { + + let m = Message::Write(String::from("hello")); + m.call(); +} +``` + +方法体将使用 `self`,来获取方法调用所在变种实例值。在此示例中,已创建了一个有着值 `Message::Write(String::from("hello"))` 的变量 `m`,而那就是在 `m.call()` 运行时,`call` 方法体中的那个 `self`。 + +下面就来看看标准库中另一个甚为常见和有用的枚举:`Option`。 + + +### `Option` 枚举及其超越空值的诸多优点 + +**The `Option` Enum and Its Advantages Over Null Values** + +本小节会探讨 `Option` 的案例研究,`Option` 是由标准库定义的另一个枚举。`Option` 类型编码了某个值可能会是某个物件,或可能什么都不属于的这种甚为常见的场景(the `Option` type encodes the very common scenario in which a value could be something or it could be nothing)。比如在请求某个含有一些项目的清单的第一个项目时,就会得到一个值。而在请求某个空清单的第一个项目时,则会什么也得不到。以类型系统字眼,来表达这个概念,就表示编译器能够对,是否已处理了本应处理的全部情形,进行检查;此项功能可阻止那些在其他编程语言中极为常见的代码错误。 + +编程语言的设计,通常要考量包含哪些特性,而要排除哪些特性也至关重要。Rust 没有许多其他语言都有的空值特性。*空值(Null)* 是一个表示此处无值的值。在带有 `null` 的那些语言中,变量总是会处于下面两个状态之一:空值或非空值。 + +在 `null` 的发明人Tony Hoare 于 2009 年的演讲 “空值引用:10 亿美金代价失误(Null Reference: The Billion Dollar Mistake)” 中,就讲了这个问题: + +> 我把他叫做我的10亿美元失误。那个时候我正在设计某门面向对象语言中的首个综合类型系统。目的是要在编译器自动执行的检查之下,确保全部引用使用,都应绝对安全。仅仅因为空值引用变量实现起来很容易,我当时就没能顶住诱惑,把他加入到特性集了。这个举动,业已造成了数不胜数的代码错误、漏洞及系统崩溃等等问题,在过去 40 余年里,这些问题可能已经造成大概 10 亿美金的痛苦和伤害。 + +`null` 值的问题在于,当尝试将 `null` 值用作非 `null` 值时,就会得到某种错误。由于这种 `null` 或非 `null` 的属性遍布各处,因此极容易犯下此类错误。 + +但 `null` 试图表达的概念,还是有用的:`null` 是个因某些原因,而当前为无效或空缺的值。 + +问题不是真的在于这个概念,而在于针对性的实现。由于这些原因,Rust 就没有空值,但他确实有一个可对值存在或空缺这个概念,进行编码的枚举。这个枚举就是 `Option`,而这个枚举 [由标准库定义](https://doc.rust-lang.org/std/option/enum.Option.html) 为下面这样: + +```rust +enum Option { + None, + Some(T), +} +``` + +这个 `Option` 是如此重要,以至于在 Rust 序曲(the prelude)中甚至都包含了;是不需要显式地将其带入到作用域的(注:*原生类型、这里的 `Option`,以及前面的 `String` 类型等等,就是这样的包含在序曲中的类型,无需显式地带入到作用域,就可以直接使用*)。该枚举的变种也已包含在 Rust 序曲中:可直接在不带前缀 `Option::` 的情况下直接使用 `Some` 与 `None`(注:*那么 `Some` 与 `None` 就被列为了 Rust 关键字了*)。`Option` 仍然只是常规枚举,而 `Some` 与 `None` 仍然是类型 `Option` 的变种。 + +这里的 `` 语法,是个到目前为止还未讲到的 Rust 特性。他是个泛型参数,而在第 10 章将更详细的涉及到泛型。至于现在,只需明白 `` 表示 `Option` 枚举的 `Some` 变种,可保存任意类型的一条数据,而在 `T` 位置处用到的各个具体类型,会让整个 `Option` 类型成为各异的类型(for now, all you need to know is that `` means the `Some` variant of the `Option` enum can hold one piece of data of any type, and that each concrete type that gets used in place of `T` makes the overall `Option` type a different type)。以下是使用 `Option` 来保存数字与字符串类型的一些示例: + +```rust + let some_numer = Some(5); + let some_string = Some("一个字符串"); + + let absent_number: Option = None; +``` + +`some_number` 的类型为 `Option`。`some_string` 的类型为 `Option<&str>`,是个不同的类型。由于这里已在 `Some` 变种里面指定了值,因此 Rust 可推导出这些类型来。而对于 `absent_number`,Rust 就要求注释整个 `Option` 类型:编译器无法通过仅查看一个 `None` 值,而推导出相应的 `Some` 变种的类型来。这里告诉了 Rust,这里计划的是 `absent_number` 为类型 `Option`。 + +在有着一个 `Some` 值时,就知道存在着一个值,且该值是保存在 `Some` 内部的。而在有个 `None` 值时,某种意义上讲,这表示了与空值同样的情况:没有一个有效值。那么究竟为什么有着 `Option` 就是要比有着空值 `null` 好呢? + +简而言之,由于 `Option` 和 `T` (其中的 `T` 可以是任意类型) 为不同类型,因此编译器就不会允许将一个 `Option` 值,当作一个必然的有效值来使用。比如,由于下面这段代码是在尝试将一个 `i8` 值,添加到某个 `Option` 上,因此这段代码不会编译: + +```rust + let x: i8 = 5; + let y: Option = Some(5); + + let sum = x + y; +``` + +在运行这段代码时,就会得到下面这样的错误消息: + +```console +$ cargo run  ✔ + Compiling enum_demo v0.1.0 (/home/peng/rust-lang/projects/enum_demo) +error[E0277]: cannot add `Option` to `i8` + --> src/main.rs:24:17 + | +24 | let sum = x + y; + | ^ no implementation for `i8 + Option` + | + = help: the trait `Add>` is not implemented for `i8` + +For more information about this error, try `rustc --explain E0277`. +error: could not compile `enum_demo` due to previous error +``` + +强悍!事实上,这个错误消息表示,由于 `i8` 与 `Option` 属于不同类型,因此 Rust 不知道怎样将一个 `i8` 值与一个 `Option` 值相加。当在 Rust 中有着一个类型好比 `i8` 这样的值时,编译器就会保证始终有个有效值。在对那个值进行使用前,可不必检查他是不是 `null`,而可放心地加以处理。仅当有个 `Option` 类型(或其他任何正在使用的`Option` 枚举类型值)的变量时,才真地必须关心可能并无值,同时编译器将确保在使用该值前,显式地处理无值的情况。 + +也就是说,在对 `Option` 类型转换为 `T` 类型。通常,这样做有助于捕获到 `null` 最常见问题之一:在某个东西实际上是 `null` 时,错误地将其设想为了他不是 `null`。 + +这种消除了不正确的假定某个非 `null` 值的做法,有助于增强代码自信。为了使用一个可能为 `null` 的值,就必须显式地通过将那个值的构造为 `Option`,来带入这个值。在某个类型不为 `Option` 值出现的每个地方,就都可以假定该值不是 `null`。这是 Rust 有意的设计决定,用以限制 `null` 的无处不在,及提升 Rust 代码的安全性。 + +那么在有一个类型为 `Option` 值的时候,该怎么从 `Some` 变种获取到 `T` 这个值,从而就可以用上那个值呢?枚举 `Option` 有着大量的、在不同情形下有用的方法;在 [`Option` 文档](https://doc.rust-lang.org/std/option/enum.Option.html) 便可查看到这些方法。熟悉 `Option` 上的这些方法,将对 Rust 编程生涯极为有用。 + +总的来说,为了使用某个 `Option` 值,就要有将会处理各个变种的代码。要有一些只会在有着一个 `Some` 的值时运行的代码,而此情况下就会允许这代码使用那个内部的 `T` 类型变量。在有着 `None` 值时,则还要有别的代码来允许了,而这代码就没有可用的 `T` 类型值了。在与枚举一起使用的时候,`match` 表达式正是实现此特性的控制流结构:`match` 表达式将依据枚举有着哪些变种,而运行相应的不同代码,以及哪些代码可使用匹配值内部的数据。 + +## `match` 控制流结构 + +Rust 有值一种即为强大的、名为 `match` 的控制流结构,此控制流结构实现了将某个值与一系列模式的比较,并根据所匹配模式而执行相应的代码。模式可由字面值、变量名字、通配符及其他事物等构成;第 18 章会涵盖到全部不同种类的模式及其所完成的事情。`match` 的强大来自模式的表达能力,以及编译器对全部可能情形都被处理进行确认这一事实。 + +请将 `match` 表达式设想为一种类似硬币分选机这样的东西:硬币随一个滑道滚下,沿着这滑道有不同尺寸的洞,那么每个硬币都会在他碰到的第一个大小合适的洞那里掉落。同样道理,所有值都会历经 `match` 表达式中的各个模式,而在值 “适合” 的第一个模式处,那个值就会掉入到相关代码块,而在执行过程中被使用到。既然讲到了硬币,那么下面就来将其用作一个用到 `match` 表达式的示例!这里可以编写一个接收未知硬币,并以与点数机类似方式,判断出该硬币是何硬币而返回以分计的值来的函数,如下面清单 6-3 中所示。 + +```rust +enum Coin { + Penny, + Nickel, + Dime, + Quarter, +} + +fn value_in_cents(coin: Coin) -> u8 { + match coin { + Coin::Penny => 1, + Coin::Nickel => 5, + Coin::Dime => 10, + Coin::Quarter => 25, + } +} +``` + +*清单 6-3:一个枚举与一个将该枚举的那些变种作为其模式的 `match` 表达式* + +这里就来把在 `value_in_cents` 函数中的那个 `match` 拆开讲讲。首先,这里是将 `match` 关键字后面跟上一个表达式,这里也就是 `coin` 后,列出来的。这就跟与 `if` 关键字一起使用的表达式极为相似,然而有个大的区别:在 `if` 之下,表达式需要返回一个布尔值,而在这里,该表达式可返回任意类型。此示例中 `coin` 的类型,即是这里在第一行上所定义的枚举 `Coin`。 + +接下来就是这个 `match` 表达式的各个支臂了。一个支臂有着两个部分:一个模式与一些代码。这里的第一个支臂,有着值为 `Coin::Penny` 的模式,同时其中的箭头运算符 `=>` 将模式与要运行的代码分隔开来。此情形下的代码,就只是值 `1`。各个支臂则是以逗号接着分开的。 + +在这个 `match` 表达式执行时,他就会依序将结果值与各个支臂的模式加以比较。在某个模式与该值匹配时,与那个模式关联的代码就被执行。而在那个模式不与该值匹配时,执行就会继续到下一支臂,就跟硬币分选机是一样的。这里需要多少支臂,就可以有多少支臂:在清单 6-3 中的 `match` 表达式,就有四个支臂。 + +与各个支臂关联的代码,是个表达式,而在匹配支臂中的表达式返回值,就是整个 `match` 表达式所返回的值。 + +正如清单 6-3 中,每个支臂只是返回一个值那样,在 `match` 表达式支臂代码,为简短代码时,就通常不会使用花括号。而在要于某个 `match` 支臂中运行多行代码时,就必须使用花括号。比如下面的代码,在该方法每次以 `Coin::Penny` 被调用时,都会打印出 “幸运便士!”,不过仍会返回该代码块的最后值,`1`: + +```rust +fn value_in_cents(coin: Coin) -> u8 { + match coin { + Coin::Penny => { + println! ("幸运便士!"); + 1 + }, + Coin::Nickel => 5, + Coin::Dime => 10, + Coin::Quarter => 25, + } +} +``` + +### 绑定到值的模式 + +`match` 支臂的另一有用特性,便是这些支臂可绑定到值与模式进行匹配值的多个部分(another useful feature of `match` arms is that they can bind to the parts of the values that match the pattern)。这就是从枚举变种提取出值的原理。 + +作为一个示例,下面就来将这里的枚举变种之一,修改为其内部保存数据。自 1999 年到 2008 年,美国在 25 美分硬币的一面,铸造上 50 个州不同的设计。别的硬币则没有这样的州份设计,因此只有这些 25 美分硬币才有这额外价值。那么就可以通过修改这个 `Quarter` 变种为内部包含一个 `UsState` 值,来将此信息添加到这里的 `enum` 类型,就如同下面清单 6-4 中所做的。 + +```rust +#[derive(Debug)] // 这样就可以很快对州份进行检查 +enum UsState { + Alabama, + Alaska, + // --跳过-- +} + +enum Coin { + Penny, + Nickel, + Dime, + Quarter(UsState), +} +``` + +来设想一下有个朋友是在尝试收集全部 50 个州份的 25 美分硬币。在按照硬币类型对零钱进行分类的同时,还将叫出与每个 25 美分硬币关联的州份名字,如此就可以在发现那个 25 美分硬币,是那位朋友还没有的时候,就可以把那个硬币添加到收藏。 + +而在这个代码的 `match` 表达式中,就要添加一个名为 `state` 的变量到匹配变种 `Coin::Quarter` 的那些值。在有 `Coin::Quarter` 匹配时,这个 `state` 变量就会绑定到那个 25 美分硬币的状态值。随后就可以在那个支臂的代码里,使用 `state` 变量了,如同下面这样: + +```rust +fn value_in_cents(coin: Coin) -> u8 { + match coin { + Coin::Penny => 1, + Coin::Nickel => 5, + Coin::Dime => 10, + Coin::Quarter(state: UsState) => { + println! ("来自 {:?} 州份的 25 美分硬币!", state); + 25 + } + } +} +``` + +这时在调用了 `value_in_cents(Coin::Quarter(UsState::Alaska))` 后,`coin` 就将是 `Coin::Quarter(UsState::Alaska)`。在将该值与各支臂进行比较时,在到达 `Coin::Quarter(state: UsState)` 支臂之前,是不会有任何支臂于其匹配的。而在 `Coin::Quarter(state: UsState)` 支臂处,`state` 所绑定的,将是值 `UsState::Alaska`。这时就可以使用那个 `println!` 表达式中的绑定,进而就从 `Quarter` 的 `Coin` 枚举变种,获取到那个内部 `state` 值了。 + + +### `Option` 下的模式匹配 + +在前一小节,那里是想要在运用 `Option` 时,从 `Some` 情形中获取到那个内部的 `T` 值;像前面对 `Coin` 枚举所做的那样,也可以这样来对 `Option` 加以处理!比较的不再是那些硬币,而是将比较 `Option` 的两个变种,不过那个 `match` 表达式的原理还是一样的。 + +下面就假设说要编写一个取 `Option` 类型值的函数,同时当 `Option` 里面有个值时,就将 `1` 加到那个值上。在 `Option` 里没有值时,该函数则会返回 `None` 值,并不会尝试执行任何运算。 + +归功于 `match` 表达式,这个函数写起来很容易,他将看起来像下面清单 6-5 这样。 + +```rust +fn plus_one(x: Option) -> Option { + match x { + None => None, + Some(n) => Some(n + 1), + } +} + +fn main() { + let five = Some(5); + let none = None; + println! ("{:?}, {:?}", plus_one(five), plus_one(none)); +} +``` + +*清单 6-5:在 `Option` 类型上运用了 `match` 表达式的一个函数* + +下面就来详细检视一下 `plus_one` 函数的首次执行。在调用 `plus_one(five)` 时,`plus_one` 函数体中的变量 `x` 将有着值 `Some(5)`。之后就会与 `match` 表达式的各个支臂进行比较。 + +```rust + None => None, +``` + +该 `Some(5)` 值不与模式 `None` 匹配,因此就会继续到下一支臂。 + +```rust + Some(n) => Some(n + 1), +``` + +`Some(5)` 与 `Some(n)` 匹配吗?当然是匹配的!这里有着同样的变种。这个 `n` 绑定的是包含在 `Some` 中的那个值,因此 `n` 就会取到值 `5`。随后该 `match` 支臂中的代码就会被执行,从而就会将 `1` 加到 `n` 的值,并创建出一个新的、内部有着这里的和 `6` 的 `Some` 值来。 + +现在来看看清单 6-5 中第二个 `plus_one` 的调用,其中 `x` 则是 `None` 了。这里进入到那个 `match` 表达式,并与第一个支臂进行比较。 + +```rust + None => None, +``` + +他是匹配的!就没有要加的值了,因此程序就停下来并返回 `=>` 右侧上的那个 `None` 值。由于第一个支臂已经匹配,因此就不会再比较其他支臂了。 + +在许多场合,将 `match` 表达式与枚举结合都是有用的。在 Rust 代码中将会看到很多这样的模式:对某个枚举的 `match` 操作,将某个变量绑定到内部数据,并随后据此执行代码(`match` against an enum, bind a variable to the data inside, and then execute code based on it)。在刚开始的时候这显得有些难以琢磨,而一旦熟悉了这种模式,就会希望在全部语言中都有这样的模式。这样的模式一直是编程者的最爱。 + + +### 匹配要彻底(Matches Are Exhaustive) + +这里有个需要讨论到的 `match` 表达式的另一方面。想想这个有着代码错误而不会编译的 `plus_one` 版本: + +```rust + fn plus_one(x: Option) -> Option { + match x { + Some(n) => Some(n + 1), + } + } +``` + +这里没有对 `None` 情形加以处理,因此该代码就会引起错误。幸运的是,那是个 Rust 知道怎样取捕获的代码错误。在尝试编译此代码时,就会得到这样的错误: + +```console +$ cargo run + Compiling enum_demo v0.1.0 (/home/peng/rust-lang/projects/enum_demo) +error[E0004]: non-exhaustive patterns: `None` not covered + --> src/main.rs:2:11 + | +2 | match x { + | ^ pattern `None` not covered + | +note: `Option` defined here + = note: the matched value is of type `Option` +help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown + | +3 ~ Some(n) => Some(n + 1), +4 ~ None => todo!(), + | + +For more information about this error, try `rustc --explain E0004`. +error: could not compile `enum_demo` due to previous error +``` + +Rust 是知道这里未曾覆盖到每种可能情形,并甚至清楚这里忘记了那个模式! Rust 中的 `match` 表达式要是 *彻底的(exhaustive)*:为了让代码有效,就必须穷尽所有的可能性。尤其是在 `Option` 这个示例中,在 Rust 阻止这里忘记显式地处理 `None` 这个情形时,在这里可能会有个 `null` 值时,他就保护了避免有个值的错误假设,进而让那个先前讨论到的十亿美金错误成为不可能了。 + +### 捕获所有模式与 `_` 占位符(Catch-all Patterns and the `_` Placeholder) + +运用枚举,还可以对少数特定值采取特别动作,而对所有其他值采取一种默认动作。设想正在实现某个游戏,其中在投中了骰子上的 3 点时,游戏角色就不会移动,而是会收到一顶新的帽子道具。而在投中 7 点时,游戏角色会失去一定道具帽子。对于其他所有点数值,游戏角色都会在游戏板上移动相应数目的格子。下面就是个实现了该逻辑的 `match` 表达式,其中的骰子点数结果,是硬编码而非随机值,至于其他由不带函数体的函数所表示的逻辑,则是由于实现这些函数超出了本示例的范围: + +```rust +let dice_roll = 9; + +match dice_roll { + 3 => add_fancy_hat(), + 7 => remove_fancy_hat(), + other => move_player(other), +} + +fn add_fancy_hat() {} +fn remove_fancy_hat() {} +fn move_player() {} +``` + +对于前两个支臂,模式为字面值 `3` 和 `7`。而那最后的最比,则涵盖了所有其他可能的值,该模式为这里以选为命名为 `other` 的那个变量。为该 `other` 支臂所运行的代码,通过将这个 `other` 变量传递给 `move_player` 函数,而用到了这个变量。 + +由于那最后的模式将匹配到未特别列出的全部值,因此尽管这里并未列出 `u8` 类型变量有的全部可能值,这段代码仍会编译。这种捕获全部的模式,满足了 `match` 表达式务必彻底的要求。请注意由于这些模式是求值的,因此这里必须将那个捕获全部支臂放在最后。若在捕获全部之后,添加了其他支臂,那么 Rust 就会告警,这是由于这些在捕获全部之后的支臂根本不会匹配到! + +Rust 还有一种在不愿使用捕获全部模式中的值时,可使用的一种模式:`_`,这是一种特别的、未与该值绑定的其他所有值。这种模式告诉 Rust 这里将不会使用该值,因此 Rust 就不会发出有关某个未用到变量的告警了(Rust also has a pattern we can use when we don't want to use the value in the catch-all pattern: `_`, which is a special pattern that matches any value and doen't not bind to that value. This tells Rust we aren't going to use the value, so Rust won't warn us about an unused varialbe)。 + +下面就来将那个游戏的规则修改为,在投中骰子的三点和七点之外别的点数时,就必须再投一次骰子。那么这里就不需要用到那个点数值了,因此就可以将这里的代码修改为使用 `_` 而不是那个名为 `other` 的变量: + +```rust + let dice_roll = 9; + + match dice_roll { + 3 => add_fancy_hat(), + 7 => remove_fancy_hat(), + _ => reroll(), + } + + fn add_fancy_hat() {} + fn remove_fancy_hat() {} + fn reroll() {} +``` + +由于这里在最后的支臂中,显式地忽略了全部其他值,因此该示例也是满足 `match` 表达式的穷尽要求的;这里并未忘记掉任何东西。 + +若再一次修改此游戏的规则,修改为在抛出即非三点也非七点的其他点数时,什么也不会发生,那么就可以通过使用单元值(即在 [元组类型](Ch03_Common_Programming_Concepts.md#the-tuple-type) 小节中讲到的那个空元组类型)作为该 `_` 支臂后的代码,来表达这样的游戏规则: + +```rust +let dice_roll = 9; + +match dice_roll { + 3 => add_fancy_hat(), + 7 => remove_fancy_hat(), + _ => (), +} + +fn add_fancy_hat() {} +fn remove_fancy_hat() {} +``` + +这里就显式地告诉 Rust,这里将不使用那些不与先前支臂匹配的全部其他值,且在此情形下这里不要运行任何代码。 + +在 [第 18 章](Ch18_Patterns_and_Matching.md) 将涉及到更多有关模式与匹配的内容。而现在就要移步到 `if let` 语法,在那些使用 `match` 表达式显得多余的情形下,`if let` 语法就会有用。 + +## `if let` 下的简洁控制流 + +`if let` 语法,实现了将 `if` 与 `let` 关键字,结合为不那么冗长的,处理与一个模式相匹配,而忽略其余模式的一些值的处理方式(the `if let` syntax lets you combine `if` and `let` into a less verbose way to handle that match one pattern while ignoring the rest)。设想下面清单 6-6 中的这个程序,该程序是要对 `config_max` 变量中的 `Option` 值进行匹配,而只打算在该值为 `Some` 变种时,才执行代码。 + +```rust + let config_max = Some(3u8); + + match config_max { + Some(max) => println! ("极大值被配置为了 {}", max); + _ => (); + } +``` + +*清单 6-6:一个仅在乎当值为 `Some` 时运行代码的 `match` 表达式* + +在该值为 `Some` 时,这里就通过将那个 `Some` 变种中的值,绑定到这个模式中的变量 `max`,而打印出该值来。这里并不想要对那个 `None` 值做什么操作。为满足 `match` 表达式的要求,这里必须在处理仅仅一个变种之后,添加 `_ => ()`,这就是要添加的恼人样板代码。 + +相反,可使用 `if let` 语法,以较简短方式写出来。下面的代码与清单 6-6 中的 `match` 表达式表现一致: + +```rust + let config_max = Some(3u8); + + if let Some(max) = config_max { + println! ("极大值被设置为了 {}", max); + } +``` + +`if let` 语法会接收由等号分隔的一个模式与一个表达式。他与 `match` 原理相同,其中的表达式被给到 `match` 表达式,而其中的模式就是 `match` 表达式的第一支臂。在此示例中,模式即为 `Some(max)`,而这个 `max` 就绑定到了 `Some` 里面的那个值。由此,这里随后就可以与在相应的 `match` 支臂中使用 `max` 的同样方式,在后面的那个 `if let` 代码块中对 `max` 进行使用。而在该值 `config_max` 不与该模式匹配时,那个 `if let` 代码块中的代码,就不会运行。 + +> ***注***:`if let` 实际上是两部分,其中 `let Some(max) = config_max` 是个 scrutinee expression。 + +使用 `if let` 语法,就意味着较少输入、较少的缩进,以及更少的样板代码。不过会损失 `match` 表达式强制要求的穷尽检查。是根据特定情形下,手头正在做的事情,在 `match` 表达式与 `if let` 语法之间加以选择的,以及考量为收获到简洁,而是否值得损失穷尽性检查。 + +也就是说,可将 `if let` 语法当作,在值与某个模式匹配时运行代码,并在之后忽略所有其他值的 `match` 表达式的语法糖(in other words, you can think of `if let` as syntax sugar for a `match` that runs code when the value matches one pattern and then ignores all other values)。 + +这里可以在 `if let` 之下,包含一个 `else` 关键字。`else` 所带的代码块,与在和 `if let` 及 `else` 等价的 `match` 表达式中, `_` 情形所带代码块相同。回想起清单 6-4 中的那个 `Coin` 枚举定义,其中的 `Quarter` 变种还有一个 `UsState` 值。在要通告出那些 25 美分硬币的州份的同时,还要清点出找到的全部非 25 美分数目,那么就可以使用下面这样的 `match` 表达式: + +```rust +let mut count = 0; + +match coin { + Coin::Quarter(state) => println! ("这是来自州份 {:?} 的 25 美分硬币!", state), + _ => count += 1, +} +``` + +或者这里还可以使用一个像下面这样的 `if let` 与 `else` 的表达式: + +```rust +let mut count = 0; + +if let Coin::Quarter(state) = coin { + println! ("这是来自州份 {:?} 的 25 美分硬币!", state); +} else { + count += 1; +} +``` + +在遇到程序中使用 `match` 显得太过繁复的逻辑这样情形时,就要记住在 Rust 工具箱中还有 `if let`语法呢。 + + + +## 总结 + +本章已经讲过,怎样运用枚举,来创建可作为一套一一列出数值之一的定制类型。这里给出了标准库的 `Option` 类型,是怎样在运用该类型下,防止代码错误的原理。在枚举值有着内部值时,根据所要处理的多少种情况,而可使用 `match` 表达式或 `if let` 语法,来提取并使用这些值。 + +现在的 Rust 程序,就可以使用结构体与枚举,对所在领域的那些概念加以表达了。通过在自己构建的 API 使用的定制类型,而确保了类型安全:Rust 编译器将令到 API 中的那些函数,只获取到这些函数所期望类型的那些值。 + +而为了将可直接使用上的、组织良好的 API 提供到用户,并只暴露 API 的用户所需要部分,那么就要了解一下 Rust 的模组特性了。 diff --git a/src/Ch07_Managing_Growing_Projects_with_Packages_Crates_and_Modules.md b/src/Ch07_Managing_Growing_Projects_with_Packages_Crates_and_Modules.md new file mode 100644 index 0000000..f71024d --- /dev/null +++ b/src/Ch07_Managing_Growing_Projects_with_Packages_Crates_and_Modules.md @@ -0,0 +1,865 @@ +# 用代码包、代码箱与模组来对日益增长的项目进行管理 + +在编写大型程序时,由于在头脑里对整个程序保持追踪已成为不可能,因此代码组织就尤为重要。通过将相关功能分组,并以截然不同的特性而将代码加以分离,就会搞清楚在哪里去找到实现了某个特定特性的代码,以及在哪里去修改某项特性的运作方式。 + +到目前为止,这里所编写的程序,都是在一个模组的一个文件中的。而随着项目的增长,就可以通过将项目分解为多个模组及多个文件,来对代码加以组织。一个代码包,可以包含多个二进制的代码箱,并可有选择地包含一个库代码箱。本章会涵盖到所有的这些技巧。对于那些极为大型、有着一套互相关联而共同演化的项目,Cargo 工具提供了工作区(workspaces)概念,关于工作区,将在第 14 章的 [Cargo 工作区](Ch14_More_about_Cargo_and_Crates_io.md#cargo-workspaces)中讲到。 + +除了实现功能上的分组(grouping functionality)外,对功能实现细节的封装,还实现了更高层次上的代码重用:一旦实现了某个操作,其他代码就可以在无需掌握其实现原理的情况下,通过该代码的公共接口,对该实现代码加以调用。编写代码的方式,就定义了哪些部分是公开给其他代码使用的,哪些部分是私有实现细节而对其修改权力有所保留。这是对那些必须保留在头脑中细节实现数量,而有所限制的另一种方式(in addition to grouping functionality, encapsulating implementation details lets you reuse code at a higher level: once you've implemented an operation, other code can call that code via the code's pulic interface without knowing how the implementation works. The way you write code defines which part are public for other code to use and which parts are private implementation details that you reserve the right to change. This is another way to limit the amount of detail you have to keep in your head)。 + +而与此相关的概念,便是作用域(scope):代码被编写出处的嵌套上下文,有着定义在所谓 “在作用域中(in scope)” 的一套名字。在读、写及编译代码时,程序员与编译器,二者都需要掌握,在某个特定点位处的某个特定名字,是否是指向了某个变量、函数、结构体、枚举、模组、常量或别的项目,以及该名字所指向项目的意义。创建作用域,及将一些名字的在或不在某个作用域加以修改,都是可行的。在同一作用域中,不能有两个名字相同的项目;有一些工具,可用于找出名字冲突。 + +对于包括哪些细节被暴露、哪些细节为私有,以及程序中各个作用域中有哪些名字等的代码组织,Rust 有着数种特性实现对其的管理。Rust 的这些有关代码组织的特性,有时被统称为 *模组系统(module system)*,包括了: + +- **代码包(packages)**:实现代码箱(crates)的构建、测试与分享的 Cargo 特性; +- **代码箱(crates)**:产生出库或可执行文件的模组树(a tree of modules that produces a library or executable); +- **模组(modules)** 与 **`use`关键字**:实现对代码组织、作用域及路径私有的控制(let you control the organization, scope, and privacy of paths); +- **路径(paths)**:对结构体、函数或模组等进行命名的方式(a way of naming an item, such as a struct, function, or module)。 + +在本章中,就要涉及到这些特性,讨论他们之间互动的原理,以及如何运用这些特性,来对作用域加以管理。在本章结束时,就会对 Rust 的模组系统有扎实掌握,并能够像专业 Rust 程序员那样,以作用域来编写程序! + +## 代码包与代码箱 + +这里将讲到的 Rust 模组系统的头几个部分,即为代码包与代码箱。 + + +*代码箱(a crate)* 是 Rust 编译器一次识别到的最低数量的代码(a *crate* is the smallest amount of code that the Rust compiler considers as a time)。即使运行 `rustc` 而非 `cargo`,并传递单个源码文件(就如同在第 1 章 [“编写并运行一个 Rust 程序”](Ch01_Getting_Started.md#writing-and-running-a-rust-program) 小节中曾干过的),编译器也会将那个文件,视为一个代码箱。代码箱可以包含一些模组,而这些模组则会被定义在其他的、与该代码箱一同被编译的一些文件中,就如同在接下来的小节中将看到的那样。 + +代码箱有着两种形式:二进制代码箱(a binary crate),或库代码箱(a library crate)。*二进制代码箱(binary crates)* 是一些可编译为能够运行的可执行程序的一些程序,譬如命令行程序或服务器。二进制代码箱必须有着一个叫做 `main` 的、定义了在可执行文件运行时所发生事情的函数。到目前为止本书中创建的全部代码箱,都是二进制代码箱。 + +*库代码箱* 是没有 `main` 函数的,且他们不会编译到可执行文件。相反,他们定义的是计划在多个项目下共用的功能。比如在 [第二章](Ch02_Programming_a_Guessing_Game.md#generating-a-random-number) 中用到的 `rand` 代码箱,就提供了生成随机数的功能。在多数时候当 Rust 公民提到 “代码箱(crate)” 时,他们指的就是库代码箱,并且他们将 “代码箱(crate)” 与一般编程概念中的 “库(library)” 互换使用。 + +*代码箱根(crate root)* 是个 Rust 编译器开始之处的源文件,并构成了代码箱的根模组(the *crate root* is a source file that the Rust compiler starts from and makes up the root module of your crate. 后面在 [定义控制作用域和私有化的模组](#defining-modules-to-control-scope-and-privacy) 小节,将深入探讨到模组概念)。 + +*包(a package)* 即为提供了一套功能的一个或多个代码箱的捆绑包(a *package* is a bundle of one or more crates that provides a set of functionality)。包,包含了描述如何构建那些代码箱的一个 `Cargo.toml` 文件。Cargo 本身实际上就是,包含了前面曾用于构建代码的命令行工具二进制代码箱的包。Cargo 包还包含了一个该二进制代码箱所依赖的库代码箱。别的项目便可依靠这个 Cargo 库代码箱,来运用与 Cargo 命令行工具,所用到的同样逻辑。 + +代码包能包含些什么,是由数条规则所确定的。一个代码包,可包含尽可能多的二进制代码箱,但却只能包含至多一个的库代码箱。一个代码包必须包含至少一个代码箱,不管是库或二进制代码箱。 + +下面就来看看在创建代码包时,会发生些什么。首先,这里要敲入命令 `cargo new`: + +```console +$ cargo new my-project + Created binary (application) `my-project` package +$ ls my-project  ✔ +Cargo.toml src +$ ls my-project/src  ✔ +main.rs +``` + +在运行了 `cargo new` 之后,这里便使用 `ls` 来查看 Cargo 创建了些什么。在该项目目录下,有着一个 `Cargo.toml` 文件,这就给到咱们一个代码包。其中还有一个包含了 `main.rs` 的 `src` 目录。在文本编辑器中打开 `Cargo.toml` 文件,就会注意到其中并未提及 `src/main.rs`。Cargo 遵循了 `src/main.rs` 即为与该代码包同名二进制代码箱箱根,这样一条约定。与此类似,Cargo 还知道,在代码包目录包含了 `src/lib.rs` 时,那么这个代码包就包含了与该包同名的一个库代码箱,而那个 `src/lib.rs` 就是该库代码箱的箱根。Cargo 会将代码箱根文件,传递给 `rustc`,来构建出相应的库或二进制程序。 + +这里有一个只包含了 `src/main.rs` 的代码包,意味着他只包含了名为 `my-project` 的一个二进制代码箱。而在代码包同时包含了 `src/main.rs` 与 `src/lib.rs` 时,他就会有两个代码箱:一个二进制和一个库代码箱,二者都有着与该代码包同样的名字。通过将一些文件放入到 `src/bin` 目录,Rust 包就可以有多个二进制代码箱:其中的每个文件,都将是单独的二进制代码箱。 + + +## 定义控制作用域和隐私的模组 + +在本小节中,这里会讲到模组与模组系统的其他部分,分别是实现对各种项目(items, 变量、函数、结构体、枚举、模组、常量或别的项目)命名的 *路径(paths)*;将某个路径引入到作用域的 `use` 关键字;以及将那些项目构造为公共项目的 `pub` 关键字。这里还会讨论到 `as` 关键字、外部代码包,以及全局操作符(the glob operator)等等。 + +首先,这里将以今后在对代码进行组织时,易于参考的一个规则列表开始。随后就会对这些规则详细解释。 + +### 模组备忘单(modules cheat sheet) + +下面就是模组、路径、`use` 关键字与 `pub` 关键字在编译器中工作原理的快速参考,以及多数开发者组织他们代码的方式。贯穿这一整章,都将逐一介绍这些规则,而这也是作为理解 Rust 模组工作原理的极佳之处。 + +- **自代码箱根开始(start from the crate root)**:在编译代码箱时,编译器首先看的是代码箱根文件(对于库代码箱,通常为 `src/lib.rs`,或者二进制代码箱的 `src/main.rs`)中,要编译的代码; + ++ **模组的声明(declaring modules)**:在代码箱根文件中,就可声明一些新的模组;比方说,使用 `mod gargen;`,而声明出一个 `garden` 模组。编译器将在以下位置,查找该模组的代码: + - 内联代码(inline),位于紧随 `mod garden` 之后,取代分号的花括号里; + - 文件 `src/garden.rs` 中; + - 文件 `src/garden/mod.rs` 中; + ++ **子模组的声明(declaring submodules)**:在任何非代码箱根文件中,都可声明出一些子模组来。比如,或许就要在 `src/garden.rs` 中,声明出 `mod vegetables;`;编译器将在那个以父模组命名的目录里的以下地方,查找那些子模组的代码: + - 内联代码,直接跟在 `mod vegetables` 之后,位处取代分号的花括号中; + - 文件 `src/garden/vegetables.rs` 中; + - 文件 `src/garden/vegetables/mod.rs` 中。 + +- **模组中代码的路径(paths to code in modules)**:一旦模组成为代码箱的一部分,就可以在这同一个代码箱中的任何地方,在隐私规则允许的情况下,运用代码路径,对那个模组中的代码加以引用。比如,那个 “garden” “vegetables” 模组中的 `Asparagus` 类型,就可在 `crate::garden::vegetables::Asparagus` 处找到。 + +- **私有与公共(private vs public)**:模组里的代码,默认对该模组的父模组是私有的。要令到模组成为公共的,就要使用 `pub mod` 而非 `mod` 来声明该模组。而要令到公共模组里的各个项目也成为公共的,就要在这些项目的声明之前,使用 `pub` 关键字。 + +- **`use` 关键字**:在某个作用域里,`use` 关键字创建出到项目的快捷方式,以减少长路径的重复。在任何能够引用到 `crate::garden::vegetables::Asparagus` 的作用域中,都可以使用 `use crate::garden::vegetables::AspAragus;` 语句,创建出一个快捷方式,并于随后只需写出 `Asparagus`,就可在该作用域中,使用那个类型。 + + +下面是个名为 `backyard`、对这些规则加以演示的二进制代码箱。该代码箱的目录,也叫做 `backyard`,包含了下面这些文件与目录: + +```console +backyard +├── Cargo.lock +├── Cargo.toml +└── src + ├── garden + │   └── vegetables.rs + ├── garden.rs + └── main.rs +``` + +该代码箱的根文件,在此实例中即为 `src/main.rs`,包含下面的代码: + +文件名:`src/main.rs` + +```rust +use crate::garden::vegetables::Asparagus; + +pub mod garden; + +fn main() { + let plant = Asparagus {}; + println! ("I'm growing {:?}!", plant); +} +``` + +语句 `pub mod garden;`,表示编译器会包含他在 `src/garden.rs` 中找到的代码,也就是: + +文件名:`src/garden.rs` + +```rust +pub mod vegetables; +``` + +而语句 `pub mod vegetables;` 表示在 `src/garden/vetables.rs` 中的代码也会被编译器包含: + +```rust +#[derive(Debug)] +pub struct Asparagus {} +``` + +现在就来进入到这些规则的细节,并在实际操作中对他们进行演示吧! + + +### 在模组中把有关联的代码组织起来 + +**Grouping Related Code in Modules** + +*模组(modules)* 实现了为代码可读性与易于重用目的,而将代码,组织在代码箱里。由于模组里的代码,默认是私有的,因此模组还实现了各个项目 *隐私(privacy)* 的控制。私有项目是一些对外部用途不可用的内部实现细节。可将模组及模组中的那些程序项目,构造为公开,这样就把他们暴露出来,从而允许外部代码使用及依赖于他们。 + +举例来说,这里要编写一个提供饭馆功能的库代码箱。那么就会定义出一些函数签名,不过要将这些函数的函数体留作空白,而非在代码中具体实现一个饭馆出来,以专注于代码组织。 + +在餐饮行业,饭馆的一些部分被称作 *前台(front of house)*,而其余部分则被称作 *后厨(back of house)*。前台是顾客们所在的地方;这是饭馆领台给食客安排位置、服务员拿到菜单和买单,以及调酒师制作饮品的地方。而后厨则是大厨和厨师们在厨房做菜、洗碗工做清洁工作,以及经理们完成行政工作的地方。 + +为了以此种方式架构起这里代码,那么就可以将其函数,组织进一些嵌套模组中。通过运行 `cargo new --lib restaurant` 命令,创建出一个新的、名为 `restaurant` 的库;然后把下面清单 7-1 中的代码,敲入到文件 `src/lib.rs` 里,而定义出一些模组与函数签名。下面是饭馆前台部分: + +文件名:`src/lib.rs` + +```rust +mod front_of_house { + mod hosting { + fn add_to_waitlist() {} + + fn seat_at_table() {} + } + + mod serving { + fn take_order() {} + + fn serve_order() {} + + fn take_payment() {} + } +} +``` + +*清单 7-1:包含着别的一些、又包含了一些函数的模组的一个 `front_of_house` 模组(a `front_of_house` module containing other modules that then contain functions)* + +这里使用跟着模组名字(此示例中,即 `front_of_house`)的关键字 `mod`,定义的一个模组。随后就是位处一对花括号中的模组代码体(the body of the module)。在模组内部,可以有其他模组,如同此示例中的 `hosting` 与 `serving` 模组。模组还可以驻留一些别的项目,诸如结构体、枚举、常量、特质(traits),以及 -- 如同在清单 7-1 中那样的 -- 一些函数等等。 + +经由模组的使用,就可以将有关联的一些定义,组织在一起,并以他们因何相关而取个名字。使用此代码的程序员们,就可以根据这些分组,而非通读全部的这些定义,来浏览代码,那么在找到他们想要使用的那些定义时,就会容易一些。而对于要往该代码增加新功能的那些程序员,就清楚在哪里放置代码,来保持程序组织有序。 + +早先曾提到 `src/main.rs` 与 `src/lib.rs` 都叫做代码箱根(crate root)。他们之所以有着这样的名字,是由于这两个文件的内容,都形成了位处该代码箱的模组结构(the root of the crate's module structure),又称为 *模组树(module tree)*根部处,名为 `crate` 的模组。 + +以下清单 7-2 给出了清单 7-1 中结构的模组树(模组结构): + +```console +crate + └── front_of_house + ├── hosting + │ ├── add_to_waitlist + │ └── seat_at_table + └── serving + ├── take_order + ├── serve_order + └── take_payment +``` + +*清单 7-2:清单 7-1 中代码的模组树* + +该树展示了一些模组是怎样嵌套在另一模组内部的(比如,`hosting` 就嵌套在 `front_of_house` 里头)。该树还展示了一些模组与其他模组互为 *姊妹关系(siblings)*,即他们是定义在同一模组中的(`hosting` 与 `serving` 都是定义在 `front_of_house` 模组中)。继续以家族作比喻,那么在模组 A 为包含在模组 B 里头时,就说模组 A 是模组 B 的 *子模组(child)*,而模组 B 即为模组 A 的 *父模组(parent)*。请注意这里的整个模组树,都是以那个隐式的、名为 `crate` 模组,作为的根。 + +模组树或许会令人想到计算机上文件系统的目录树;这可是一个极为恰当的类比!就跟文件系统中的目录一样,使用模组是为对代码进行组织。而正如目录中的那些文件,这里需要一种找到那些模组的方法。 + + +## 用于引用目录树中项目的路径 + +**Paths for Referring to an Item in the Module Tree** + +这里以与在对文件系统进行导航时,所用到路径的同样方式,使用路径来给 Rust 指出,在何处找到模组树中的某个项目。要调用某个函数,那么就需要获悉他的路径。 + +而路径则可以有两种形式: + +- *绝对路径(an absolute path)* 是从代码箱根开始的完整路径。对于外部代码箱的代码,绝对路径是以代码箱名字开头,而对于当前代码箱的代码,则是以字面值 `crate` 开头; +- *相对路径(a relative path)* 是从当前模组开始,并使用了 `self`、`super` 关键字,或当前模组中的某个标识符。 + +绝对与相对路径,后面跟着的都是以双冒号(`::`)分隔的一个或多个标识符。 + +回到清单 7-1 中的示例,比方说这里打算调用那个 `add_to_waitlist` 函数。这就跟问及:“那个 `add_to_waitlist` 函数的路径为何?“ 是同样的。下面的清单 7-3 包含清单 7-1,不过移除了一些模组与函数。 + +这里将给出从定义在该代码箱根部的一个新函数 `eat_at_restaurant`,调用那个 `add_to_waitlist` 函数的两种方式。其中那些路径都是正确的,但由于存在别的问题,而将阻止此示例如往常那样编译。这里会稍加解释为何会这样。 + +其中的 `eat_at_restaurant` 函数,是这里的库代码箱公共 API 的一部分,因此要将其以 `pub` 关键字进行标记。在后面的 [使用 `pub` 关键字对路径进行暴露](#exposing-paths-with-the-pub-keyword) 小节,深入到更多有关 `pub` 关键字的细节。 + +文件名:`src/lib.rs` + +```rust +mod front_of_house { + mod hosting { + fn add_to_waitlist() {} + } +} + +pub fn eat_at_restaurant() { + // 绝对路径方式 + crate::front_of_house::hosting::add_to_waitlist(); + + // 相对路径方式 + front_of_house::hosting::add_to_waitlist(); +} +``` + +*清单 7-3:使用绝对与相对路径两种方式,调用 `add_to_waitlist` 函数* + +在 `eat_at_restaurant` 中,第一次调用那个 `add_to_waitlist` 函数使用的是绝对路径。由于这个 `add_to_waitlist` 函数,是定义在与 `eat_at_restaurant` 同一个代码箱中,这意味着此处可以使用 `crate` 关键字,来开始一个绝对路径。随后这里包括了到那个 `add_to_waitlist` 为止的各个后续模组。可以设想有着这同样结构的一个文件系统:即要指明路径 `/front_of_house/hosting/add_to_waitlist`,来运行那个 `add_to_waitlist` 程序;使用 `crate` 字面值名字,而自该代码箱根部开始,就跟使用 `/` 来从 shell 中文件系统根部开始类似。 + +在 `eat_at_restaurant` 里第二次调用 `add_to_waitlist` 时,这里使用了绝对路径。该路径是以 `front_of_house`,即那个与 `eat_at_restaurant` 定义在模组树的同一级别的模组名字,开始的。此处文件系统的等价物,将是使用路径 `front_of_house/hosting/add_to_waitlist`。以模组名字开始,就意味着该路径是绝对的。 + +至于究竟要选择相对路径,还是绝对路径,是要基于手头项目,而将作出的决定,并取决于是更倾向于把程序项目定义代码迁移到单独的地方,还是要把他们和要用到他们的代码放在一起。比如,在将 `front_of_house` 模组与 `eat_at_restaurant` 函数,移入到一个名为 `customer_experience` 的模组中时,那么就需要更新那个到 `add_to_waitlist` 的绝对路径,但那个相对路径则仍将有效。但如果将 `eat_at_restaurant` 函数单独移入到一个名为 `dining` 的模组,那么到 `add_to_waitlist` 函数的绝对路径就会依旧保持那样,但那个相对路径则需要被更新。由于今后多半要把代码定义和项目调用,迁移为各自独立,因此总体上偏好是要指明绝对路径。 + +接下来就要尝试编译清单 7-3,并找出他为何不编译的原因!所得到的错误如下清单 7-4 所示。 + +```console +$ cargo build  ✔ + Compiling restaurant v0.1.0 (/home/peng/rust-lang/restaurant) +error[E0603]: module `hosting` is private + --> src/lib.rs:9:28 + | +9 | crate::front_of_house::hosting::add_to_waitlist(); + | ^^^^^^^ private module + | +note: the module `hosting` is defined here + --> src/lib.rs:2:5 + | +2 | mod hosting { + | ^^^^^^^^^^^ + +error[E0603]: module `hosting` is private + --> src/lib.rs:12:21 + | +12 | front_of_house::hosting::add_to_waitlist(); + | ^^^^^^^ private module + | +note: the module `hosting` is defined here + --> src/lib.rs:2:5 + | +2 | mod hosting { + | ^^^^^^^^^^^ + +For more information about this error, try `rustc --explain E0603`. +error: could not compile `restaurant` due to 2 previous errors +``` + +*清单 7-4:构建清单 7-3 中代码时的编译器错误* + +该错误消息讲到,模组 `hosting` 是私有的。也就是说,这里的 `hosting` 模组与 `add_to_waitlist` 函数的路径都是正确的,但由于 Rust 没有到私有部分的访问权限,而不会允许咱们使用他们。在 Rust 中,全部程序项目(函数、方法、结构体、枚举、模组,以及常量等),默认对父模组都是私有的。在打算将某个项目,比如函数或结构体,构造为私有时,就要将其放在某个模组里。 + +父模组中的项目,是无法使用子模组内部的那些私有项目的,但子模组中的项目,则可以使用他们祖辈模组中的项目。这是由于子模组封装并隐藏了他们的实现细节,但子模组却可以看到他们被定义处的上下文(the context in which they're defined)。继续之前的那个比喻,请把这些隐私规则,想做是饭馆后台(the back office of a restaurant):那里所发生的事情对饭馆顾客是隐私的,但后台经理们却可以看到并完成他们所运营饭馆里的全部事情。 + +Rust 选择了让模组系统以这种方式发挥作用,从而默认就将内部实现细节给隐藏了。如此一来,就清楚可修改内部代码的哪些部分,而不会破坏外层代码。尽管如此,Rust 还是提供了通过使用 `pub` 关键字,把某个程序项目构造为公共项目,而将子模组代码的内层部分,暴露给外层祖辈模组的选项。 + +### 使用 `pub` 关键字对路径进行暴露 + +下面回到清单 7-4 中,告知 `hosting` 模组为私有的那个错误。这里希望在父模组中的 `eat_at_restaurant` 函数,有着到那个 `hosting` 子模组中的 `add_to_waitlist` 函数的访问权限,因此就要将该模组,以 `pub` 关键字标记起来,如下面清单 7-5 中所示。 + +文件名:`src/lib.rs` + +```rust +mod front_of_house { + pub mod hosting { + fn add_to_waitlist() {} + } +} + +pub fn eat_at_restaurant() { + // 绝对路径 + crate::front_of_house::hosting::add_to_waitlist(); + + // 相对路径 + front_of_house::hosting::add_to_waitlist(); +} +``` + +*清单 7-5:将 `hosting` 模组声明为 `pub` 以在 `eat_at_restaurant` 中使用他* + +不幸的是,清单 7-5 中的代码仍以错误告终,如下清单 7-6 中所示: + +```console +$ cargo build + Compiling restaurant v0.1.0 (/home/peng/rust-lang/restaurant) +error[E0603]: function `add_to_waitlist` is private + --> src/lib.rs:9:37 + | +9 | crate::front_of_house::hosting::add_to_waitlist(); + | ^^^^^^^^^^^^^^^ private function + | +note: the function `add_to_waitlist` is defined here + --> src/lib.rs:3:9 + | +3 | fn add_to_waitlist() {} + | ^^^^^^^^^^^^^^^^^^^^ + +error[E0603]: function `add_to_waitlist` is private + --> src/lib.rs:12:30 + | +12 | front_of_house::hosting::add_to_waitlist(); + | ^^^^^^^^^^^^^^^ private function + | +note: the function `add_to_waitlist` is defined here + --> src/lib.rs:3:9 + | +3 | fn add_to_waitlist() {} + | ^^^^^^^^^^^^^^^^^^^^ + +For more information about this error, try `rustc --explain E0603`. +error: could not compile `restaurant` due to 2 previous errors +``` + +*清单 7-6:构造清单 7-5 中代码时的编译器错误* + + +怎么回事呢?将 `pub` 关键字加在 `mod hosting` 前面,是把该模组构造为公共模组。有了此修改,那么在能够访问 `front_of_house` 时,就能够访问 `hosting`。但 `hosting` 模组的 *内容(contents)* 仍是私有的;将模组构造为公开,并不会将其内容构造为公开。在模组上的 `pub` 关键字,只是让其祖辈模组中的代码可以引用到他,而不是访问其内层代码。由于模组是些容器,因此仅将模组构造为公开,是做不了什么的;这就需要更进一步,而选择将模组里的一个或更多的程序项目,也构造为公开。 + +清单 7-6 中的错误说到,那个 `add_to_waitlist` 函数是私有的。适用于结构体、枚举、函数即方法等的隐私规则,与适用于模组的一样。 + +下面就来通过把 `pub` 关键字,添加在 `add_to_waitlist` 函数定义之前,而将其构造为公开函数,如下清单 7-7 所示。 + +文件名:`src/lib.rs` + +```rust +mod front_of_house { + pub mod hosting { + pub fn add_to_waitlist() {} + } +} + +pub fn eat_at_restaurant() { + // 绝对路径 + crate::front_of_house::hosting::add_to_waitlist(); + + // 相对路径 + front_of_house::hosting::add_to_waitlist(); +} +``` + +*清单 7-7:把 `pub` 关键字添加到 `mod hosting` 与 `fn add_to_waitlist`,实现从 `eat_at_restaurant` 调用该函数* + +现在该代码就会编译了!接下来看看其中绝对与相对路径,以弄明白为何添加 `pub` 关键字,就实现了在遵循隐私规则之下,使用到 `add_to_waitlist` 中的这些路径的原因。 + +在那个绝对路径中,是以这里代码箱模组树的根、字面值 `crate` 开始的。那个 `front_of_house` 模组,即为被定义在该代码箱根中。尽管 `front_of_house` 模组不是公开的,但由于 `eat_at_restaurant` 函数被定义在与 `front_of_house` 同一模组中(即 `eat_at_restaurant` 与 `front_of_house` 是姊妹关系),因此是可以从 `eat_at_restaurant` 函数引用 `front_of_house` 的。接下来就是那个被标记了 `pub` 的 `hosting` 模组了。由于这里可以访问 `hosting` 的父模组,因此就可以访问 `hosting`。最后,由于那个 `add_to_waitlist` 函数被 `pub` 标记过,且这里可以访问他的父模组,因此该函数调用就生效了! + +在那个相对路径中,除了第一步外,其中的逻辑与绝对路径相同:与从代码箱根开始不同,该相对路径是从 `front_of_house` 处开始的。这个 `front_of_house` 模组,是定义在与 `eat_at_restaurant` 函数同样的模组中,那么从 `eat_at_restaurant` 定义所在处的这个模组开始的相对路径,就是有效的。随后由于 `hosting` 与 `add_to_waitlist` 都是以 `pub` 关键字标记过,那么该路径其余部分就都工作了,同时此函数调用就是有效的了! + +在计划分享库代码箱,进而其他项目可使用到其代码时,公开 API 即是与该代码箱用户的合约,定下了与库代码箱代码互动的方式。在管理对公共 API 的修改方面,则有着诸多考量,以让人们更易于依赖到咱们的代码箱。这些考量超出了本书的范围;若对这方面感兴趣,那么请参阅 [Rust API 指南](https://rust-lang.github.io/api-guidelines/)。 + + +> **带有一个二进制与一个库的 Rust 代码包最佳实践(Best Practice for Packages with a Binary and a Library)** +> +> 前面提到过 Rust 包可以同时包含一个 `src/main.rs` 二进制代码箱根,与一个 `src/lib.rs` 库代码箱根,且这两个代码箱都将默认有着该 Rust 包的名字。一般来说,这种同时包含了一个库及二进制代码箱模式下的包,都会在二进制代码箱中,仅有着足够启动一个会调用到库代码箱代码的可执行程序的少量代码。由于库代码箱的代码可被共享,因此这就实现了别的项目,受益于该 Rust 包所提供的绝大部分功能。 +> +> 模组树应定义在 `src/lib.rs` 中。随后,全部的公开程序项目,都可通过以该包名字开头的路径,在那个二进制代码箱中被使用。这个二进制代码箱,就像是个将用到那个库代码箱的完整外部箱,成了库代码箱的一名用户:他只能使用公共 API。这样做有助于设计出良好的 API;你不仅是库代码箱的作者,还是一名库代码箱的客户了! +> +> 在 [第 12 章](Ch12_An_I_O_Project_Building_a_Command_Line_Program.md),将以一个会同时包含二进制代码箱与库代码箱的命令行程序,对这种代码组织方式实践加以演示。 + + +### 使用 `super` 关键字开始相对路径 + +**Starting Relative Paths with `super`** + +通过在路径开头使用 `super` 关键字,就可以构建出在父模组处,而非当前模组或代码箱根处开始的相对路径。这与以 `..` 语法开始的文件系统路径相似。使用 `super` 实现了对已知在父模组中某个程序项目的引用,在模组与其父模组密切相关,但该父模组在某个时候可能会被迁移到模组树中别的地方时,这种使用 `super` 关键字的相对路径,就能让模组树的重新安排更为容易。 + +设想下面清单 7-8 中,建模了一位大厨修正某个不正确点餐,并亲自将其交给顾客的代码。其中定义在 `back_of_house` 模组中的函数 `fix_incorrect_order`,通过以 `super` 作为开头指明的 `deliver_order` 路径,调用了定义在父模组中的该 `deliver_order` 函数: + +文件名:`src/lib.rs` + +```rust +fn deliver_order() {} + +mod back_of_house { + fn fix_incorrect_order() { + cook_order(); + super::deliver_order(); + } + + fn cook_order() {} +} +``` + +*清单 7-8:使用以 `super` 开头的相对路径调用某个函数* + + +这个 `fix_incorrect_order` 函数是在 `back_of_house` 模组中,因此就可以使用 `super` 关键字,前往到 `back_of_house` 的父模组,那就是此示例中的 `crate`,亦即代码箱根。在那里,就会查找 `deliver_order` 进而找到他。大功告成!这里把 `back_of_house` 模组与 `deliver_order` 函数,设想作可能维持这同样关系,并在今后决定要对这个代码箱的模组树,进行重新组织时,他们会一起被移动。因此,这里使用了 `super`,从而今后在此代码被移入到别的模组时,要更新代码的地方就会少一些。 + +### 将结构体与枚举构造为公共项目 + +**Making Structs and Enums Public** + +这里还可以使用 `pub` 关键字,来将结构体与枚举,指定为公开项目,但结构体与枚举下 `pub` 的用法,有着几个额外情况。在结构体定义前使用 `pub` 关键字时,就将该结构体构造为了公开,但该结构体的那些字段,仍将是私有。可根据具体情况,把各个字段构造为公开或不公开。在下面清单 7-9 中,就定义了有着公开 `toast` 字段,和私有 `seasonal_fruit` 字段的一个公开的 `back_of_house::Breakfast` 结构体。这就对在某个饭馆中,顾客可在何处挑选与正餐搭配的面包类型,而主厨则会根据当季及仓库里有些什么,而决定由哪些水果来搭配正餐,这种情形进行了建模。可用的水果变化很快,因此顾客就无法对水果进行选择,甚至他们看不到会得到什么样的水果。 + +文件名:`src/lib.rs` + +```rust +mod back_of_house { + pub struct Breakfast { + pub toast: String, + seasonal_fruit: String, + } + + impl Breakfast { + pub fn summer(toast: &str) -> Breakfast { + Breakfast { + toast: String::from(toast), + seasonal_fruit: String::from("peaches"), + } + } + } +} + +pub fn eat_at_restaurant() { + // 点下一份带有黑麦土司的夏日早餐, rye, US /raɪ/, UK /rai/, n.黑麦, 黑麦粒 + let mut meal = back_of_house::Breakfast::summer("Rye"); + meal.toast = String::from("Wheat"); + println! ("请给我一份 {} 土司", meal.toast); + + // 若不把接下来的行注释掉,那么就不会编译;这里不允许查看或修改 + // 餐食搭配的应季水果 + // meal.seasonal_fruit = String::from("blueberries"); +} +``` + +*清单 7-9:有着一些公共字段与私有字段的一个结构体* + +由于 `back_of_house::Breakfast` 结构体中的 `toast` 字段是公开的,因此在 `eat_at_restaurant` 中就可以使用点符号(`.`),对 `toast` 字段进行写入与读取。请注意由于 `seasonal_fruit` 是私有的,因此这里不能在 `eat_at_restaurant` 中使用那个 `seasonal_fruit` 字段。尝试将那个对 `seasonal_fruit` 字段值进行修改的行解除注释,看看将得到什么样的错误! + + +```console +$ cargo build + Compiling restaurant v0.1.0 (/home/peng/rust-lang/restaurant) +error[E0616]: field `seasonal_fruit` of struct `Breakfast` is private + --> src/lib.rs:25:10 + | +25 | meal.seasonal_fruit = String::from("blueberries"); + | ^^^^^^^^^^^^^^ private field + +For more information about this error, try `rustc --explain E0616`. +error: could not compile `restaurant` due to previous error +``` + +还请留意由于 `back_of_restaurant::Breakfast` 有个私有字段,那么该结构体就需要提供一个公开的、构造出`Breakfast` 实例的关联函数(这里将该函数命名为了 `summer`)。若 `Breakfast` 没有这样一个函数,那么由于在 `eat_at_restaurant` 中无法设置那个私有 `seasonal_fruit` 字段的值,因此就没法在 `eat_at_restaurant` 中创建处一个 `Breakfast` 的实例来。 + +与此相比,在将枚举构造为公开时,该枚举的全部变种此时都是公开的。这里就只需在 `enum` 关键字前的 `pub` 关键字,如下清单 7-10 中所示。 + +文件名:`src/lib.rs` + +```rust +mod back_of_house { + pub enum Appetizer { + Soup, + Salad, + } +} + +pub fn eat_at_restaurant() { + let order1 = back_of_house::Appetizer::Soup; + let order2 = back_of_house::Appetizer::Salad; +} +``` + +> appetizer, US/ˈæpəˌtaɪzər/, UK/ˈæpəˌtaɪzə(r)/ n.(餐前的)开胃品 + +*清单 7-10:将枚举指定为公开,则会将其全部变种构造为公开* + +由于这里将那个 `Appetizer` 枚举构造为了公开,因此就可以在 `eat_at_restaurant` 中使用 `Soup` 与 `Salad` 变种。除非枚举的各个变种是公开的,否则枚举就不是非常有用了;若在所有场合,都必须以 `pub` 关键字来对全部枚举变种进行注解,那就会让人觉得烦恼不已,因此默认枚举变种就是公开的。而结构体则通常无需其字段保持公开就有用处,因此结构体的那些字段,就遵循了除非以 `pub` 关键字注释,而默认全部为私有的一般规则。 + +还有一个尚未讲到的涉及 `pub` 关键字的情况,那也是最后的一项模组系统特性:`use` 关键字。后面会先讲 `use` 本身,然后再给出怎样结合 `pub` 与 `use`。 + +## 使用 `use` 关键字将路径带入作用域 + +为调用一些函数,而不得不写出他们的路径,就会感到不便与重复。比如在清单 7-7 中,对于到 `add_to_waitlist` 函数,无论是选择绝对路径还是相对路径,在每次在打算调用 `add_to_waitlist` 时,都必须还要指明 `front_of_house` 与 `hosting`。幸运的是,有简化此过程的办法:这里可以使用 `use` 关键字,一次性创建出到某个路径的快捷方式,尔后就可以在该作用域中所有地方,使用这个较短名字了。 + +在下面清单 7-11 中,就将 `crate::front_of_house::hosting` 模组,带入到了 `eat_at_restaurant` 函数的作用域,由此就只须指明 `hosting::add_to_wait`,而在 `eat_at_restaurant` 中调用这个 `add_to_waitlist` 函数了。 + +文件名:`src/lib.rs` + +```rust +mod front_of_house { + pub mod hosting { + pub fn add_to_waitlist() {} + } +} + +use crate::front_of_house::hosting; + +pub fn eat_at_restaurant() { + hosting::add_to_waitlist(); +} +``` + +*清单 7-11:使用 `use` 关键字,将模组带入到作用域* + +在作用域中添加 `use` 及某个路径,与在文件系统中创建一个符号链接类似。通过在该代码箱根处,添加 `use crate::front_of_house::hosting`,那么 `hosting` 现在就是一个有效的名字,就如同这个 `hosting` 模组,已在该代码箱根中被定义过一样。使用 `use` 关键字带入到作用域中的那些路径,与任何其他路径一样,同样会检查隐私性。 + +请注意 `use` 关键字只会针对在该 `use` 出现的特定作用域,创建快捷方式。下面清单 7-12 将 `eat_at_restaurant` 移入到了新的名为 `customer` 的子模组中,这个模组就与那个 `use` 语句属于不同作用域了,因此那个函数体就不会编译: + +文件名:`src/lib.rs` + +```rust +mod front_of_house { + pub mod hosting { + pub fn add_to_waitlist() {} + } +} + +use crate::front_of_house::hosting; + +mod customer { + pub fn eat_at_restaurant() { + hosting::add_to_waitlist(); + } +} +``` + +*清单 7-12:`use` 语句只适用于其所在的作用域* + +编译器错误指出,在 `customer` 模组里头,那个快捷方式不再适用: + +```console +$ cargo build + Compiling restaurant v0.1.0 (/home/peng/rust-lang/restaurant) +error[E0433]: failed to resolve: use of undeclared crate or module `hosting` + --> src/lib.rs:33:9 + | +33 | hosting::add_to_waitlist(); + | ^^^^^^^ use of undeclared crate or module `hosting` + +warning: unused import: `crate::front_of_house::hosting` + --> src/lib.rs:28:5 + | +28 | use crate::front_of_house::hosting; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: `#[warn(unused_imports)]` on by default + +For more information about this error, try `rustc --explain E0433`. +warning: `restaurant` (lib) generated 1 warning +error: could not compile `restaurant` due to previous error; 1 warning emitted +``` + +请注意这里还有那个 `use` 在其作用域中已不再被使用的一个告警!为修复此问题,就同时要将那个 `use` 语句,移入到那个 `customer` 模组内部,或者在那个子 `customer` 模组内部,以 `super::hosting` 来引用父模组中的那个快捷方式。 + +### 创建惯用 `use` 路径 + +在上面的清单 7-11 中,你或许会想,为什么那里指定了 `use crate::front_of_house::hosting`,并随后在 `eat_at_restaurant` 函数中调用了 `hosting::add_to_waitlist`,而不是将那个 `use` 路径,指定为一直到那个 `add_to_waitlist` 函数,以达到同样目的,即如下清单 7-13 中那样。 + +文件名:`src/lib.rs` + +```rust +mod front_of_house { + pub mod hosting { + pub fn add_to_waitlist() {} + } +} + +use crate::front_of_house::hosting::add_to_waitlist; + +pub fn eat_at_restaurant() { + add_to_waitlist(); +} +``` + +*清单 7-13:使用 `use` 将 `add_to_waitlist` 带入到作用域,此为非惯用做法* + +尽管清单 7-11 与 7-13 都完成了同样任务,但清单 7-11 则是以 `use` 关键字将函数带入到作用域的惯用方式。以 `use` 关键字将函数的父模组带入到作用域中,就意味着在调用该函数时,必须指明父模组。而在调用函数时指明父模组,就令到该函数是非本地函数,这一事实变得明了,同时仍旧减少了完整路径的重复。而清单 7-13 中的代码,对于 `add_to_waitlist` 在何处创建,则并不清楚。 + +另一方面,在使用 `use` 关键字,将结构体、枚举及其他程序项目带入时,惯用的就是指明完整路径了。下面清单 7-14 给出了将标准库的 `HashMap`,带入到某个二进制代码箱的惯用方式。 + +文件名:`src/lib.rs` + +```rust +use std::collections::HashMap; + +fn main() { + let mut map = HashMap::new(); + map.insert(1, 2); +} +``` + +*清单 7-14:以惯用方式将 `HashMap` 带入到作用域* + +这种惯用语法背后并没有什么有力理由:他不过是业已形成的约定,且人们已经习惯了以这样的方式,阅读和编写 Rust 代码。 + +由于 Rust 不允许使用 `use` ,将两个有着同样名字的程序项目带入到作用域,那么这就正是此惯用语法的例外了。下面清单 7-15 给出了,怎样将两个有着同样名字,但父模组不同的 `Result` 类型带入作用域,及怎样去引用他们。 + +文件名:`src/lib.rs` + +```rust +use std::fmt; +use std::io; + +fn function1() -> fmt::Result { + // --跳过-- +} + +fn function2() -> io::Result { + // --跳过-- +} +``` + +*清单 7-15:将有着同样名字的两种类型带入到同一作用域,就要求使用他们的父模组* + +可以看到,这里使用父模组,就将两个 `Result` 类型区分开了。相反如果指明的是 `use std::fmt::Result;` 与 `use std::io::Result;`,就会得到同一作用域中的两个 `Result` 类型,而 Rust 就不明白在使用 `Result` 时,到底是要哪个了。 + +### 使用 `as` 关键字提供新名字 + +解决以 `use` 关键字将有着同样名字的两个类型,带入到同一作用域的问题,还有另一方法:在路径后面,可指定 `as`,与该类型的一个新本地名字,或者说 *别名(alias)*。下面清单 7-16 给出了通过将那两个 `Result` 类型中的一个,使用 `as` 关键字进行重命名,而编写清单 7-15 中代码的另一种方式。 + +文件名:`src/lib.rs` + +```rust +use std::fmt::Result; +use std::io::Result as IoResult; + +fn function1() -> Result { + // --跳过-- +} + +fn function2() -> IoResult { + // --跳过-- +} +``` + +*清单 7-16:在将某个类型带入作用域时,使用 `as` 关键字对其进行重命名* + + +在第二个 `use` 语句中,选择了 `IoResult` 作为 `std::io::Result` 类型的新名字,这就不会与同时带入到作用域的、来自 `std::fmt` 的 `Result` 冲突了。清单 7-15 与清单 7-16 都被当作惯用方式,因此选择哪个就随你所愿了! + + +### 使用 `pub use` 将名字重新导出 + +**Re-exporting Names with `pub use`** + +在使用 `use` 关键字将某个名字带入到作用域中时,这个在新作用域中可用的名字即为私有的。为了那些会调用到引入作用域代码的其他代码,能够像这个名字是被定义在引入到作用域的代码的作用域中一样,对这个名字进行引用,这时就可以结合上 `pub` 与 `use` 关键字。由于这里是将某个程序项目带入到作用域,而又同时将那个程序项目构造为可被其他代码将其带入他们的作用域,因此该技巧被称为 *重导出(re-exporting)*。 + +下面清单 7-17 给出了将根模组中的 `use` 修改为 `pub use` 后,清单 7-11 中的代码。 + +文件名:`src/lib.rs` + +```rust +mod front_of_house { + pub mod hosting { + pub fn add_to_waitlist() {} + } +} + +pub use crate::front_of_house::hosting; + +pub fn eat_at_restaurant() { + hosting::add_to_waitlist(); +} +``` + +*清单 7-17:使用 `pub use`,于一个新作用域处将某个名字构造为对任意代码可用* + +在此项修改之前,外部代码必须通过使用路径 `restaurant::front_of_house::hosting::add_to_waitlist()`,来调用其中的 `add_to_waitlist` 函数。现在既然这个 `pub use` 已将该 `hosting` 模组,自根模组中重新导出,那么外部代码现在就可以使用 `restaurant::hosting::add_to_waitlist()` 路径了。 + +在所编写代码的内部结构,与调用代码的程序员们对该领域有着不同设想时,重导出就是有用的。比如,在这个饭馆的比喻中,运营该饭馆的人设想的是“前厅”与“后厨”。但造访饭馆的食客,或许不会用这样的词汇,来认识饭馆的这些部位。有了 `pub use`,就可以一种结构来编写代码,而以另一种结构将代码暴露出来。这样做就让这个库,对于在该库上编写代码的程序员,与调用这个库的程序员,均具备良好的组织。在第 14 章的 [“运用 `pub use` 导出便利的公共 API”](Ch14_More_about_Cargo_and_Crates_io.md#exporting-a-convenient-public-api-with-pub-use) 小节,将会看到另一个 `pub use` 的示例,并了解他是怎样影响到代码箱的文档。 + + +### 使用外部 Rust 包 + +在第 2 章中,那里曾编写了用到名为 `rand` 外部包来获取一个随机数的猜数游戏项目。为了在项目中使用 `rand`,那里曾添加下面这行到 `Cargo.toml` 文件: + +文件名:`Cargo.toml` + +```toml +rand = `0.8.3` +``` + +将 `rand` 作为依赖项添加到 `Cargo.toml`,就告诉 Cargo,去 [crates.io](https://crates.io/) 下载那个 `rand` 包和任何的依赖项,而令到 `rand` 对此项目可用。 + +随后为了将 `rand` 的一些定义,带入到所编写的包中,这里添加了以代码箱名字,`rand`,开头,并列出了打算要带入到作用域中的那些条目的一个 `use` 行。回顾第 2 章中的 [“生成一个随机数”](Ch02_Programming_a_Guessing_Game.md#generating-a-random-number) 小节,那里就将那个 `Rng` 特质,带入到了作用域,并调用了 `rand::thread_rng` 函数: + +```rust +use rand::Rng; + +fn main() { + let secret_number = rand::thread_rng().gen_rang(1..=100); +} +``` + +Rust 社群业已构造了可在 [crates.io](https://crates.io/) 上取得的许多 Rust 包,而将任意的这些包,拉取进入自己的包,都涉及到这些同样步骤:将他们列在自己包的 `Cargo.toml` 文件中,并使用 `use` 来将他们代码箱中的条目,带入到作用域中。 + +请注意标准库(`std`)同样是个相对本地包的外部代码箱。由于标准库是 Rust 语言本身附带的,因此就无需修改 `Cargo.toml` 文件为包含 `std`。但为了将 `std` 中的条目带入到本地包作用域,是需要以 `use` 来引用他。比如,以 `HashMap` 来说,就要使用下面这行: + +```rust +use std::collections::HashMap; +``` + +这是一个以 `std`,即标准库代码箱名字,开头的绝对路径。 + +### 运用嵌套路径来清理大量的 `use` 清单 + +**Using Nested Paths to Clean Up Large `use` Lists** + +在用到定义在同一代码箱或同一模组中的多个条目时,若各自行上地列出这些条目,那么就会占据文件中的很多纵向空间。比如,清单 2-4 中的猜数游戏里,就有下面这两个 `use` 语句,他们将 `std` 中的两个条目带入到作用域: + +文件名:`src/main.rs` + +```rust +// --跳过-- +use std::cmp::Ordering; +use std::io; +// --跳过-- +``` + +相反,这里就可以使用嵌套路径,来在一个行中,把来自同一代码箱或包的那些条目,带入到作用域。通过指明路径的共同部分,接上一对冒号,及随后的花括号封闭包围起来的那些路径各异部分的清单,就完成了这一点,如下代码清单 7-18 所示。 + +文件名:`src/main.rs` + +```rust +// --跳过-- +use std::{cmp::Ordering, io}; +// --跳过-- +``` + +*清单 7-18:指定出嵌套路径,来将多个有着同样前缀的程序项目带入到作用域* + +在更为大型的程序中,使用嵌套路径,将许多的程序项目,从同一代码箱或模组带入到作用域,可极大地减少所需的单独 `use` 语句数目! + +在路径中的任何级别,都可使用嵌套路径,在对两个共用了子路径的 `use` 语句进行组合时,这是有用的。比如下面清单 7-19 就给出了两个 `use` 语句:一个将 `std::io` 带入到作用域,而另一个则是将 `std::io::Write` 带入到作用域。 + +文件名:`src/lib.rs` + +```rust +use std::io; +use std::io::Write; +``` + +*清单 7-19:其中一个为另一个子路径的两个 `use` 语句* + +这两个路径的共同部分,即是 `std::io`,且那就是完整的第一个路径。为将这两个路径融合为一个 `use` 语句,这里可在嵌套路径中,使用 `self` 关键字,如下清单 7-20 中所示。 + +文件名:`src/main.rs` + +```rust +use std::io::{self, Write}; +``` + +*清单 7-20:将清单 7-19 中的两个路径组合为一个 `use` 语句* + +这行代码就将 `std::io` 与 `std::io::Write` 带入到了作用域。 + + +### 全局操作符 + +在打算将某个路径中的 *全部(all)* 公开条目,都带入到作用域时,可将那个路径,后面跟上 `*`,即全局操作符,而予以指定: + +```rust +use std::collections::*; +``` + +这个 `use` 语句,将定义在 `std::collections` 中的全部公开项目,都带入到了当前作用域。在使用这个全局操作符时要当心!全局带入,会导致更难于分清哪些名字是作用域中,与在所编写程序中用到的名字,是在何处定义的。 + +通常是在测试时,要将正在测试的全部程序项目带入到 `tests` 模组,才使用这个全局操作符;在第 11 章中的 [怎样编写测试](Ch11_Writing_Automated_Tests.md#how-to-write-tests) 小节,就会讲到这个问题。在序曲模式(the prelude pattern)中,有时也会用到全局操作符:请参阅 [标准库文档](https://doc.rust-lang.org/std/prelude/index.html#other-preludes),了解有关更多序曲模式的知识。 + + +## 将模组拆分为不同文件 + +**Separating Modules into Different Files** + +到目前为止,本章的全部示例,都是将多个模组定义在一个文件中的。在模组变得大起来时,就会打算将他们的定义,迁移到单独文件,从而令到代码易于导览。 + +比如,这里就从清单 7-17 中的代码开始,并将那些模组提取到文件中,而非将所有那些模组,都定义在那个代码箱根文件里。在此情况下,代码箱根文件为 `src/lib.rs`,但这个过程同样对那些根文件为 `src/main.rs` 的二进制代码箱有效。 + +首先,会将那个 `front_of_house` 模组,提取到他自己的文件。要移除 `front_of_house` 模组花括号里头的代码,而仅留下 `mod front_of_house;` 语句声明,这样那个 `src/lib.rs` 就会包含如下清单 7-21 中展示的代码了。请注意在创建出后面清单 7-22 中的 `src/front_of_house.rs` 文件之前,这是不会编译的。 + +文件名:`src/lib.rs` + +```rust +mod front_of_house; + +pub use crate::front_of_house::hosting; + +pub fn eat_at_restaurant() { + hosting::add_to_waitlist(); +} +``` + +*清单 7-21:声明出其模组代码体将在 `src/front_of_house.rs` 中的 `front_of_house` 模组* + +接下来,就要把原先在花括号中的代码,放入到一个新的名为 `src/front_of_house.rs` 文件中,如下清单 7-22 中所示。由于编译器在该代码箱根中,找到了名字 `front_of_house`,因此他就明白要在这个文件中看看。 + +文件名:`src/front_of_house.rs` + +```rust +pub mod hosting { + pub fn add_to_waitlist() {} +} +``` + +*清单 7-22:文件 `src/front_of_house.rs` 中 `front_of_house` 模组内部的定义* + +请注意只需在模组树中的某处,使用一次 `mod` 声明,而将某个文件的内容加载进来。一旦编译器获悉该文件是项目的一部分(且由已将那个 `mod` 语句放置于于何处,而掌握了该代码在模组树中所处的位置),项目中的其他文件,则应如同之前 [用于引用模组树中项目的路径](#paths-for-referring-to-an-item-in-the-module-tree) 小节中,曾讲到的到模组声明处的路径,来引用那个文件中的代码。也就是说,这里的 `mod` *并非* 其他编程语言有的那种 “include” 操作。 + +接下来,就要将那个 `hosting` 模组,提取到他自己的文件了。而由于 `hosting` 是 `front_of_house` ,而非根模组,的子模组,因此过程略有不同。这里将把 `hosting` 模组的文件,放在模组树中以其父辈命名的一个新目录中,此示例中即为 `src/front_of_house`。 + +这里要将 `src/front_of_house.rs` 文件,修改为只包含 `hosting` 模组声明,以开始对 `hosting` 的迁移: + +文件名:`src/front_of_house.rs` + +```rust +pub mod hosting; +``` + +随后就要创建一个 `src/front_of_house` 的目录,和一个文件 `src/front_of_house/hosting.rs`,来包含在 `hosting` 模组中构造的那些定义: + +文件名:`src/front_of_house/hosting.rs` + +```rust +pub fn add_to_waitlist() {} +``` + +相反如果将 `hosting.rs` 放在 `src` 目录,那么编译器就会以为 `hosting.rs` 的代码,是在声明于代码箱根部的 `hosting` 模组中的,而不是那个 `front_of_house` 模组的子模组中的。为了获取模组代码,而要查看那些文件方面的编译器规则,就表明这些目录与文件,甚为紧密地于模组树结构相匹配。 + +#### 备用文件路径 + +> +> 本小节讲的是 Rust 编译器所用到的最惯用的文件路径;但较早的文件路径仍被支持。 +> +> 对于定义在代码箱根部的名为 `front_of_house` 模组,编译器会在下面这些地方查找该模组的代码: + +- `src/front_of_house.rs` (即这里讲到的); +- `src/front_of_house/mod.rs` (较早的,仍被支持的路径)。 + +> 而对于作为 `front_of_house` 的子模组的名为 `hosting` 的模组,编译器会在以下地方查找该模组的代码: + +- `src/front_of_house/hosting.rs` (即这里讲到的); +- `src/front_of_house/hosting/mod.rs` (较早的,仍被支持的路径)。 + +> 对于同一模组,若同时使用这两种文件路径,那么就会得到一个编译器错误。而对同一项目中的不同模组,采用不同方式的文件路径是被允许的,只是会对那些导览项目的人造成困扰。 +> +> 使用名为 `mod.rs` 文件方式的主要缺点,即那样的话,项目最终会以许多名为 `mod.rs` 文件而终结,在代码编辑器中,同时打开这些 `mod.rs` 文件,那么就会感到混乱。 + +将各个模组的代码,移入到单独文件现在就完成了,而模组树还是保持原来那样。尽管模组定义存在于不同文件中,但是无需任何修改,那个在 `eat_at_restaurant` 中的函数调用仍会工作。这种技巧,就实现了在模组大小增长时,将其迁移到新的文件中。 + +请注意 `src/lib.rs` 中的那个 `pub use crate::front_of_house::hosting;` 语句,同样不曾改变,而那个 `use` 也不会对哪些文件作为代码箱的部分,而被编译有任何的影响。`mod` 关键字定义了模组,而 Rust 则会在与该模组有着同样名字的文件中,查找要进到那个模组中的代码。 + +## 总结 + +Rust 实现了包拆分为多个代码箱,进而将代码箱拆分为多个模组,这样就可以从一个模组,对定义在另一模组中的程序项目加以引用。通过指明绝对或相对路径,就可以做到这点。使用 `use` 语句,就可以将这些程序项目的路径,带入到作用域,如此就可以在那个作用域中,多次用到所带入的程序项目时,使用较简短的路径。默认下模组代码是私有的,但可通过添加 `pub` 关键字,而将一些定义构造为公开的。 + +下一章中,就会看看,可在本地组织良好代码中,使用到的标准库中的一些集合数据结构(collection data structures)。 diff --git a/src/Ch08_Common_Collections.md b/src/Ch08_Common_Collections.md new file mode 100644 index 0000000..813e0cb --- /dev/null +++ b/src/Ch08_Common_Collections.md @@ -0,0 +1,782 @@ +# 一些常用的集合 + +**Common Collections** + +Rust 标准库中包含了几种名为 *集合(collections)* 的有用数据结构。大多数其他数据类型,都表示某个特定值,而集合则可包含多个值。与内建的数组和元组类型不同,这些集合所指向的数据,是存储在堆上的,这就意味着在编译时不需要知道数据的数量,进而在程序运行时,这些数据数量可增加或减少。每种集合都有不同能力与开销,针对应用程序当下情况,而选择恰当的一种集合,则是随着时间推移,要发展的一项编程技能。本章中,将讨论在 Rust 程序中,经常被用到的三种集合: + +- *矢量* 允许存储并列的数个值; +- *字符串* 是一些字符的集合。早先曾提到过 `String` 类型,而本章就要深入讨论到他; +- *哈希映射(hash map)* 允许将某个值与特定键进行关联。他是一种更为通用数据结构、名为 *映射(map)* 的一个特定实现。 + +要了解由标准库所提供的其他类别集合,请参阅 [文档](https://doc.rust-lang.org/std/collections/index.html)。 + +这里将讨论怎样创建与更新矢量、字符串与哈希映射,同时会讨论他们因何而变得特殊。 + + +## 使用矢量类型,对值清单进行存储 + +**Storing Lists of Values with Vectors** + +这里要看的第一个集合,便是 `Vec`,也叫做 *矢量(vector)* 类型。矢量类型允许将多个值,存储在单个的、将全部这些值挨个放入内存的数据结构中。矢量类型仅能存储同一类型的这些值。在有着某个数据项目清单,比如某个文件中的那些文本行,或购物车中那些货品价格时,那么矢量类型就是有用的。 + + +### 创建一个新的矢量值 + +要创建出一个新的空矢量值,就要调用 `Vec::new()` 函数,如下清单 8-1 所示: + +```rust + let v: Vec = Vec::new(); +``` + +*清单 8-1:创建一个新的、用于保持一些类型 `i32` 值的空矢量* + +请注意这里添加了个类型注解。由于这里没有往这个矢量插入任何值,因此 Rust 是不清楚这里要存储何种类别元素的。这是个重点。矢量值是使用泛型实现的;在后面第 10 章中,就会讲到怎样在自己的类型中使用泛型。而此刻,就要明白由标准库提供的这个 `Vec` 可以保存任何类型。在创建保存特定类型的矢量时,可在尖括号里头指定那个类型。在清单 8-1 中,就告诉了 Rust,`v` 中的那个 `Vec` 将保存 `i32` 类型的元素。 + +而更为常见的则是,会创建带有初始值的 `Vec`,同时 Rust 就会推断出要存储的值类型,那么就很少会进行这样的类型注解。Rust 贴心地提供了 `vec!` 这个宏,这个宏就会创建出一个新的、保存给到他的那些值的矢量来。下面清单 8-2 就创建了一个新的、保存了值 `1`、`2` 与 `3` 的 `Vec`。之所以那个整数类型为 `i32`,是由于 `i32` 正是默认的整数类型,如同第 3 章的 ["数据类型"](Ch03_Common_Programming_Concepts.md#data-types) 中所讨论的那样。 + +```rust +let v = vec! [1, 2, 3]; +``` + +*清单 8-2:创建一个新的包含了值的矢量* + +由于这里已经给定了一些初始化 `i32` 的值,因此 Rust 就可以推断出 `v` 的类型为 `Vec`,而那个类型注解就不是必要的了。接下来,就要看看怎样修改矢量。 + + +### 更新矢量 + +要创建出一个矢量,并随后将一些元素添加给他,就可以使用 `push` 方法,如下清单 8-3 中所示。 + +```rust + let mut v = Vec::new(); + + v.push(5); + v.push(6); + v.push(7); + v.push(8); +``` + +*清单 8-3:使用 `push` 方法来把一些值添加到某个矢量* + +正如第 3 章中所讨论的,这里与任何变量一样,在想要能修改矢量的值时,就要使用 `mut` 关键字,将其构造为可变。这里在矢量内部的数字,全部都是 `i32` 类型,而 Rust 就会从这些数据,推断出这个类型,因此这里不需要 `Vec` 类型注解。 + +### 丢弃某个矢量,就会丢弃他的元素 + +**Dropping a Vector Drops Its Elements** + +与其他任何 `struct` 一样,矢量在超出作用域时,就会被释放掉,如下清单 8-4 所示。 + +```rust + { + let v = vec! [1, 2, 3, 4]; + + // 对 v 执行一些操作 + } // 这里 v 就超出了作用域,而被释放掉 +``` + +*清单 8-4:对矢量及其元素在何处被丢弃进行展示* + +在这个矢量被丢弃时,那么他所有内容也会被丢弃,即他保存的那些整数将被清理掉。这初一看似乎直接明了,然而在开始触及到一些到该矢量元素的引用时,事情就会变得复杂。接下来就要解决这个问题! + +### 读取矢量的元素 + +引用存储在矢量中某个值的方式有两种:经由索引,或使用 `get` 方法。在接下来的示例中,为讲得更清楚的原因,已经对从这些方法返回的值进行了注释。 + +下面清单 8-5 给出了访问矢量某个值的两种方式,即索引语法与 `get` 方法。 + +```rust + let v = vec! [1, 2, 3, 4]; + + let third: &i32 = &v[2]; + println! ("第三个元素为 {}", third); + + match v.get(2) { + Some(third) => println! ("第三个元素为 {}", third), + None => println! ("没有第三个元素。"), + } +``` + +*清单 8-5:使用索引语法或 `get` 方法访问矢量中的某个元素* + +请留意这里的两个细节。首先,由于矢量是以从零开始的数字进行索引,因此这里使用了索引值 `2` 来获取那第三个元素。其次,这里是通过同时使用 `&` 与 `[]`,获取第三个元素的,这就给到一个引用变量,而使用带有作为参数传递索引的 `get` 方法,给到的却是个 `Option<&T>` 值。 + +Rust 提供这两种引用某个元素方式的原因在于,有了这两种方式,就可以在尝试使用某个超出了既有元素范围的索引值时,对程序此时的表现加以选择。比如,下面就来看看在有着一个五个元素的矢量,而随后尝试以两种技巧,来访问位于索引 `100` 处元素时,会发生什么事情,如下清单 8-6 中所示。 + +```rust + let v = vec! [1, 2, 3, 4, 5]; + + let does_not_exist = &v[100]; + let does_not_exist = v.get(100); +``` + +*清单 8-6:尝试访问包含五个元素矢量中索引 `100` 处的元素* + +在运行此代码时,由于第一种 `[]` 方式引用了不存在的元素,因此将导致程序死机。在有着某个对超出矢量末端的元素进行访问的尝试,而打算将程序崩溃掉时,那么用这种方式是最佳的。 + +而在传递给 `get` 方法的索引,位于矢量外部时,他就会返回不会程序死机的 `None` 值。在寻常情况下,就会时不时出现对超出矢量范围元素的访问时,就应使用这种方式。这时代码就将有如同第 6 章中所讨论的,处理 `Some(&element)` 或 `None` 值的逻辑。比如,那个索引可以是来自某人输入的数字。在他们不小心输入了一个过大的数字时,程序就会得到一个 `None` 值,这时就可以告诉用户在当前矢量中有多少个项目,并给到他们又一次输入有效值的机会。比起由于输入错误而将程序崩溃掉,这将是更加用户友好! + +当程序有了有效引用时,Rust 的借用检查器,就会强制执行所有权与借用规则检查(在第 4 章讲到过),来确保该引用及全部其他的、到这个矢量内容的引用是有效的。请回顾那条表明了不能在同一作用域中,有着多个可变与不可变引用的规则。那条规则就适用于下面清单 8-7,清单中有着一个到矢量首个元素的可变引用,并尝试将一个元素添加到示例末尾。若同时在那个函数中尝试引用那个元素,那么该程序就不会工作: + +```rust + let mut v = vec! [1, 2, 3, 4, 5]; + + let first: &i32 = &v[0]; + + v.push(6); + + println! ("首个元素为:{}", first); +``` + +*清单 8-7:在保留到矢量某个条目的引用同时,尝试将一个元素添加到该矢量* + +对此代码进行编译,将引发下面这个错误: + +```console +$ cargo run  ✔ + Compiling vec_demo v0.1.0 (/home/peng/rust-lang/projects/vec_demo) +error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable + --> src/main.rs:6:5 + | +4 | let first: &i32 = &v[0]; + | - immutable borrow occurs here +5 | +6 | v.push(6); + | ^^^^^^^^^ mutable borrow occurs here +7 | +8 | println! ("首个元素为:{}", first); + | ----- immutable borrow later used here + +For more information about this error, try `rustc --explain E0502`. +error: could not compile `vec_demo` due to previous error +``` + +清单 8-7 中的代码看起来似乎可以工作:为何到矢量首个元素的引用,会牵连到该矢量末尾的变化呢?这个报错是由于矢量工作原理:由于矢量是将他的那些值,挨个放在内存中的,那么将新元素添加到矢量末尾,而在该矢量当前存储处,没有足够场所来挨个放置全部这些元素时,这时就会要求分配新内存,并将那些旧有元素拷贝到新内存空间。在那种情况下,到首个元素的引用就会指向已解分配内存(deallocated memory)。而正是这些 Rust 的借用规则,防止程序已这样的情形而告终。 + +> **请注意**:更多有关 `Vec` 类型的实现细节,请参考 [Rust 专论(The Rustonomicon)](https://doc.rust-lang.org/nomicon/vec/vec.html)。 + + +### 对矢量中那些值的迭代 + +要依次访问矢量中的各个元素,就要迭代全部元素,而非使用那些索引值,一次访问一个了。下面清单 8-8 展示了怎样使用 `for` 循环,来获取到一个 `i32` 矢量值中各个元素的不可变引用,并将这些元素打印出来。 + +```rust + let v = vec! [100, 32, 57]; + + for i in &v { + println! ("{}", i); + } +``` + +*清单 8-8:通过使用 `for` 循环对各个元素进行迭代,而打印出矢量中的每个元素* + +也可以为了对全部元素进行修改,而对可变示例中的各个元素,进行可变引用的迭代。下面清单 8-9 中的 `for` 循环,将把 `50` 添加到各个元素。 + +```rust + let mut v = vec! [100, 32, 57]; + + for i in &mut v { + *i += 50; + } +``` + +*清单 8-9:对实例中各个元素,进行可变引用的迭代* + +要修改可变引用所指向的值,就必须使用 `*` 解引用操作符(the `*` dereference operator),在能够使用 `+=` 运算符之前,获取到 `i` 中的那个值。在后面第 15 章的 [“以解引用操作符,顺着指针找到值”](Ch15_Smart_Pointers.md#following-the-pointer-to-the-value) 小节,就会讲到这个解引用操作符。 + + +### 使用枚举存储多种类型 + +矢量只能存储同一类型的值。这就会不方便;显然是有需要存储不同类型条目清单的使用场景。幸运的是,枚举的那些变种,就是定义在同一枚举类型之下的,那么在需要某种表示那些不同类型元素的类型时,就可以定义并使用一个枚举! + +好比说这里要从电子表格的某行,其中该行的一些列包含了整数,另一些列包含了浮点数,而其他列则包含了字符串,而要从这行获取到这些数据。那么就可以定义这样一个枚举,他的那些变种将保存这些不同值类型,而全部这些变种,就会被看着是同一类型:即为该枚举。随后就可以创建一个矢量,来保存那个枚举,进而最终保存了这些不同类型。下面清单 8-10 中就对此进行了演示。 + +```rust + enum SpreadsheetCell { + Int(i32), + Float(f64), + Text(String), + } + + let row = vec! [ + SpreadsheetCell::Int(3), + SpreadsheetCell::Text(String::from("blue")), + SpreadsheetCell::Float(10.12), + ]; +``` + +*清单 8-10:定义一个 `enum` 来在矢量中存储不同类型的值* + +Rust 需要在编译时了解那个矢量中会有些什么类型,这样他就清楚存储该矢量的每个元素,所需要的内存堆上内存准确数量。同时必须显式声明该矢量中允许哪些类型。若 Rust 允许矢量保存任意类型,那么就会存在一个或多个类型,将引发在该矢量的元素上执行操作错误的可能。运用枚举加上 `match` 表达式,就意味着 Rust 将在编译时,确保所有可能情形都被处理,如同第 6 章中讨论的那样。 + +但若不清楚运行时程序会在矢量中收到的详尽类型集合,那么这个枚举技巧就不会有用。相反,这个时候就可以使用特质对象(a trait object),而在后面的第 17 章就会讲到特质对象。 + +既然这里已经讨论了使用矢量的一些最常用方式,那就一定要看看 [API 文档](https://doc.rust-lang.org/std/vec/struct.Vec.html),了解那些定义在 `Vec` 上,由标准库所定义的全部有用方法。比如,除了 `push` 之外,`pop` 方法会移除并返回矢量的最后一个元素。下面就移步到下一个集合类型:`String` 吧! + +## 使用 `String` 存储 UTF-8 编码的文本 + +在第 4 章中,就曾谈到过字符串,而现在则要深入审视他们。萌新 Rust 公民,通常会由于以下三个搅在一起的原因,而被字符串给卡住:作为 Rust 暴露各种可能错误的选择;相比于许多程序员意识到的复杂度,字符串是一种更具复杂度的数据结构;还有就是 UTF-8。在从别的语言转到 Rust 时,这些因素就会有以看起来有难度的方式,纠缠在一起。 + +由于字符串是作为字节集合,加上一些在将这些字节解析为文本时,提供有用功能的方法,这样来实现的,因此这里就在集合的语境中,来讨论字符串了。在本小节,将谈及在 `String` 类型上的那些所有集合都有的操作,比如创建、更新与读取等等。这里也会讨论 `String` 与其他集合的不同之处,即通过对比人类与机器解读 `String` 类型数据的不同之处,来搞清楚索引进到某个 `String` 有何等复杂。 + +### 何为 `String`? + +这里首先就要定义一下,*字符串(string)* 这个名词指的是什么。Rust 在其核心语言中,只有一种字符串类型,那就是字符串切片类型 `str`,该类型通常是以其被借用的形式 `&str` 而出现。在第 4 章中,就讲到过 *字符串切片(string slices)*,他们是到一些存储在各处的、以 UTF-8 编码的字符串数据的引用。 + +而 `String` 类型,则是由 Rust 标准库所提供,而非编码进核心语言的,一种可增长、可变、(所有权)被持有的、UTF-8 编码的字符串类型(the `String` type, which is provided by Rust's standard library rather than coded into the core lanuage, is a growable, mutable, owned, UTF-8 encoded string type)。在 Rust 公民提到 Rust 中的 “字符串” 时,他们可能指的既是 `String`,也可可能是字符串切片的 `&str` 类型,而不仅仅是这些类型其中之一。虽然这个小节很大部分讲的是 `String`,但在 Rust 标准库中,两种类型都有重度使用,且 `String` 与字符串切片,都是 UTF-8 编码的。 + +Rust 标准库还包含了一些其他字符串类型,比如 `OsString`、`OsStr`、`CString` 及 `CStr` 等等。一些库代码箱则可提供到甚至更多的用于存储字符串数据的选项。发现这些名称都是以 `String` 或 `Str` 结尾的了吧?他们指向的都是是有所有权的与借用的变种,就跟先前所见到的 `String` 与 `str` 类型一样。比如,这些字符串类型就可存储不同编码或在内存中以不同方式表示的文本。本章中不会讨论这些其他字符串类型;请参阅 API 文档,了解更多有关如何使用他们,以及何时哪个是恰当的字符串类型的更多知识。 + + +### 创建一个新的 `String` + +`Vec` 的许多同样操作,对 `String` 也是可用的,这里就以创建一个新字符串的 `new` 函数开始,如下清单 8-11 中所示。 + +```rust + let mut s = String::new(); +``` + +*清单 8-11:创建一个新的空 `String`* + +这行代码就创建了一个新的、名为 `s` 的空字符串,随后就可以将数据加载进这个空字符串了。通常,这里会有一些初始数据作为字符串的开头。为此,就要使用 `to_string` 方法,这个方法在所有实现了 `Display` 特质(the `Display` trait),正如字符串字面值这样的类型上,都是可用的。下面清单 8-12 给出了两个示例。 + +```rust + let data = "初始内容"; + + let s = data.to_string(); + + // 该方法同样直接工作于字面值之上 + let s = "初始内容".to_string(); +``` + +*清单 8-12:使用 `to_string` 方法自字符串字面值创建出一个 `String`* + +此代码传教了一个包含 `初始内容` 的字符串。 + +这里也可以使用函数 `String::from` 来从字符串字面值创建 `String`。下面清单 8-13 众多的代码,与使用 `to_string` 的清单 8-12 中的代码等价。 + +```rust + let s = String::from("初始内容"); +``` + +*清单 8-13:使用 `String::from` 函数,从字符串字面值创建一个 `String`* + +由于字符串有相当多的用途,因此就可以使用字符串的众多不同的通用 API,而赋予到很多选择。这些字符串通用 API 中的一些,可能看起来是重复的,但他们全都有他们的用处!就在这个示例中,`String::from` 与 `to_string` 两个函数完成的是同样的事情,那么选择哪个,就关乎代码风格与可读性了。 + +请记住字符串都是 UTF-8 编码的,因此就可以将任何编码恰当的数据,包含在字符串中,如下清单 8-14 中所示。 + +```rust + let hello = String::from("السلام عليكم"); + let hello = String::from("Dobrý den"); + let hello = String::from("Hello"); + let hello = String::from("שָׁלוֹם"); + let hello = String::from("नमस्ते"); + let hello = String::from("こんにちは"); + let hello = String::from("안녕하세요"); + let hello = String::from("你好"); + let hello = String::from("Olá"); + let hello = String::from("Здравствуйте"); + let hello = String::from("Hola"); + let hello = String::from("👋"); +``` + +*清单 8-14:以不同语言在字符串中存储问候语* + +全部这些都是有效的 `String` 值。 + + +### 更新 `String` + +在将更多数据压入到其中时,`String` 可以增长大小,且就跟 `Vec` 的内容一样,内容可以改变。此外,还可以方便地使用 `+` 运算符或 `format!` 宏,来连接一些 `String` 值。 + +**使用 `push_str` 与 `push`,往 `String` 追加数据** + +通过使用 `push_str` 方法来追加一个字符串切片,就可以增大 `String`,如下清单 8-15 中所示的那样。 + +```rust + let mut s = String::from("foo"); + s.push_str("bar"); + println! ("{}", s); +``` + +*清单 8-15:使用 `push_str` 方法将一个字符串切片追加到某个 `String`* + +在这两行之后,`s` 就会包含 `foobar`。由于这里并非真的想要取得那个参数的所有权,因此这个 `push_str` 方法取的是个字符串切片。而比如在下面 8-16 中的代码里,就打算在将 `s2` 的内容追加到 `s1` 后,能够对 `s2` 进行使用。 + +```rust + let mut s1 = String::from("foo"); + let s2 = "bar"; + s1.push_str(s2); + println! ("s2 为 {}", s2); +``` + +*清单 8-16:在将一个字符串切片的内容追加到某个 `String` 后再对其加以使用* + +若这个 `push_str` 方法取得了 `s2` 的所有权,那么这里就无法在最后一行打印其值。然而,这段代码正如预期那样运作了! + +相比 `push_str` 方法,这个 `push` 方法则会取单个字符作为参数,并将其添加到 `String`。下面清单 8-17 就使用这个 `push` 方法,将字母 "l" 添加到了一个 `String`。 + +```rust + let mut s = String::from("lo"); + s.push('l'); +``` + +*清单 8-17:使用 `push` 方法,将一个字符添加到某个 `String`* + +作为上面代码的结果,`s` 将包含 `lol`。 + + +#### 使用 `+` 运算符或 `format!` 宏的字符串连接 + +通常,会想要将两个既有字符串合在一起。完成此操作的一种方式,就是使用 `+` 运算符,如下清单 8-18 中所示。 + +```rust + let s1 = String::from("Hello, "); + let s2 = String::from("world!"); + let s3 = s1 + &s2; // 请注意这里的 s1 已被迁移,而不再能被使用了 +``` + +*清单 8-18:运用 `+` 运算符来将两个 `String` 值结合为一个新的 `String` 值* + +这个字符串 `s3` 将包含 `Hello, world!`。`s1` 在该字符串加法之后不再有效的原因,以及这里使用到 `s2` 引用的原因,与这里使用 `+` 运算符时,被调用的那个方法的签名有关。这个 `+` 运算符使用了 `add` 方法,而该方法的签名使用了 `add` 方法,`add` 方法的签名,看起来与下面的类似: + +```rust +fn add(self, s: &str) -> String +``` + +在标准库中,就会看到使用泛型定义的 `add` 函数。这里已将泛型的那些参数,用具体类型进行了替换,即在以 `String` 类型值调用是所发生的。在第 10 章就会讨论泛型。这个函数签名,提供了了解那个 `+` 运算符棘手之处所需的线索。 + +首先,这里的 `s2` 有个 `&`,表示这里这里正将第二个字符串的 *引用*,添加到第一个字符串。这是由于 `add` 函数中的那个 `s` 参数的原因:这里只能将一个 `&str` 添加到某个 `String`;这里是无法将两个 `String` 相加在一起的。不过稍等一下 -- `&s2` 的类型是 `&String`,而非在 `add` 函数的第二个参数中所指明的 `&str`。那为何清单 8-18 会编译呢? + +这里之所以能在到 `add` 的调用中使用 `&s2` 的原因,在于编译器可将那个 `&String` 参数,*强制转换* 为 `&str` 类型。在调用 `add` 方法时,Rust 使用了 *解引用强制转换(deref coercion)* 特性,在这里该特性就将 `&s2` 转换为了 `&s2[..]`。在第 15 章就将深入讨论这个解引用强制转换。由于 `add` 方法并未占据那个 `s` 参数的所有权,因此 `s2` 在此运算之后,仍将有效。 + +其次,这里可以看到,在该方法签名中,由于 `self` *没有* `&`,那么 `add` 就取得了 `self` 的所有权。这就意味着清单 8-18 中的 `s1` 将被迁移到那个 `add` 调用中,并在那之后便不再有效。这样看来,尽管 `let s3 = s1 + &s2;` 这个语句看起来将同时拷贝这两个字符串,并创建一个新的字符串,不过此语句实际上是要取得 `s1` 的所有权,追加 `s2` 内容的一份拷贝,进而随后返回该运算结果的所有权。也就是说,看起来这行语句构造了很多拷贝,但并没有;这样的实现比拷贝更为高效。 + +在需要连接多个字符串时,这个 `+` 运算符的行为就变得笨拙了: + +```rust + let s1 = String::from("tic"); + let s2 = String::from("toc"); + let s3 = String::from("toe"); + + let s = s1 + "-" + &s2 + "-" + &s3; +``` + +此处 `s` 将为 `tic-toc-toe`。由于有些全部的 `+` 与 `"` 字符,因此就难于看清发生了什么。对于较复杂的字符串合并,可这个 `format!` 宏: + +```rust + let s1 = String::from("tic"); + let s2 = String::from("toc"); + let s3 = String::from("toe"); + + let s = format! ("{}-{}-{}", s1, s2, s3); +``` + +这段代码同样把 `s` 设置为了 `tic-toc-toe`。这个 `format!` 宏与 `println!` 宏的运作类似,而与将输出打印到屏幕不同,他会将结果内容,以一个 `String` 加以返回。使用 `format!` 这个版本的代码,读起来容易得多,且由于 `format!` 宏所生成的代码,使用的是引用,那么这个调用就不会占据任何一个其参数的所有权。 + + +### 索引到 `String` 内部 + +再许多其他编程语言中,经由通过索引而引用字符串中的一些单独字符,都是有效且常见的操作。不过在 Rust 中,当尝试使用索引来访问某个 `String` 的一些部分时,就会收到错误。请考虑下面清单 8-19 中的无效代码。 + +```rust + let s1 = String::from("hello"); + let h = s1[0]; +``` + +*清单 8-19:尝试在某个字符串上使用索引语法* + +该代码将引发下面的错误: + +```console +$ cargo run + Compiling string_demo v0.1.0 (/home/peng/rust-lang/projects/string_demo) +error[E0277]: the type `String` cannot be indexed by `{integer}` + --> src/main.rs:3:13 + | +3 | let h = s1[0]; + | ^^^^^ `String` cannot be indexed by `{integer}` + | + = help: the trait `Index<{integer}>` is not implemented for `String` + +For more information about this error, try `rustc --explain E0277`. +error: could not compile `string_demo` due to previous error +``` + +这个报错和提示讲清了缘由:Rust 的字符串不支持索引。但为什么不支持呢?要回到这个问题,就要探讨一下 Rust 是怎样在内存中存储字符串的。 + + +**内部表示** + +`String` 是对 `Vec` 的一种封装(a `String` is a wrapper over a `Vec`)。下面来看看清单 8-14 中,那里的一些以 UTF-8 良好编码的示例字符串。首先是这个: + +```rust +let hello = String::from("Hola"); +``` + +在此示例中,`len` 将为 `4`,这表示这个存储着字符串 “Hola” 的矢量长度为 4 个字节。这些字母在以 UTF-8 编码时,每个占用 1 个字节。而接下来的这行,就会惊讶到你了。(请注意这个字符串是以大写西里尔字母 `Ze` 开头,而非阿拉伯数字 `3`。) + +```rust + let hello = String::from("Здравствуйте"); +``` + +在被问及这个字符串有多长时,你可能会讲是 `12`。事实上,Rust 的答案是 `24`:由于那个字符串中的每个 Unicode 标量值,都有占用 2 字节的存储,故那就是以 UTF-8 编码 `Здравствуйте` 所用占用的字节数。由于这个缘故,到该字符串的那些字节的所以,就不会总是对应到某个有效的 Unicode 标量值了。为对此加以演示,请设想下面这段无效的 Rust 代码: + +```rust + let hello = String::from("Здравствуйте"); + let answer = &hello[0]; +``` + +这里当然清楚 `answer` 将不会是那第一个字母 `З`。在以 UTF-8 编码时,`З` 的第一个字节是 `208`,同时第二个字节为 `151`,因此看起来 `answer` 事实上应该是 `208`,但 `208` 本身并不是个有效的字符。在用户要求该字符串的首个字母时,返回 `208` 就不会是他们所想要;然而,那却是 Rust 在字节索引 `0` 处有的唯一数据了。即使字符串只包含拉丁字母,用户也通常不想要那个返回的字节值:即便 `&"hello"[0]` 是返回了字节值的有效代码,他也会返回 `104`,而非 `h`。 + +那么答案就是,为避免返回一个不期望的值,以及避免引发一些可能无法立即发现的程序错误,Rust 就根本不会编译此代码,并阻止了在开发过程早期阶段的这些误解。 + + +**字节、标量值与字素簇!我的天!(Bytes and Scalar Values and Grapheme Clusters! Oh My!)** + +有关 UTF-8 的另一点,即为从 Rust 视角看待字符串,事实上有三种相关方式:视为字节、标量值与字素簇(而字素簇则是与我们称之为 *文字/letters* 的东西的最接近的事物了)。 + +在看到以梵文字书写的印地语词汇 `"नमस्ते"` 时,这个词汇是以看起来像下面这样的一些 `u8` 类型值的矢量存储的: + +```rust +[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, 224, 165, 135] +``` + +这是 18 个字节,也是计算机最终存储该数据的方式。而在将他们视为 Unicode 的标量值,即 Rust 的 `char` 类型时,则这些字节看起来是这样的: + +```rust +['न', 'म', 'स', '्', 'त', 'े'] +``` + +这里就有了六个 `char` 值了,但其中第四与第六个并不是文字(letters):他们是自身并无意义的变音符号。最后,在将这些 Unicode 标量值视为字素簇时,就得到了人类所称呼的、四个构成了那个印地词语的四个文字: + +```rust +["न", "म", "स्", "ते"] +``` + +Rust 提供了解析计算机存储的原始字符串数据的数种不同方式,因此各个程序就可以选择他所需的解析方式,这与该数据为何种人类语言无关。 + +Rust 不允许索引进入 `String` 来获取某个字符的终极原因,即是索引操作,被认为总是消耗固定的时间(即 `O(1)`)。但由于 Rust 将不得不从开头遍历到索引位置,来确定那里有多少个有效字符,由此对于在 `String` 上执行索引操作,所消耗的时间是无法确保持续一致的。 + + +### 对字符串进行切片操作 + +**Slicing Strings** + +由于字符串索引操作的返回值类型不明朗:可能是字节值、字符、字素簇,或者字符串切片,因此索引到字符串中去,通常是个糟糕的主意。而在确实需要使用索引来创建字符串切片时,那么 Rust 就要求提供更具体的索引。 + +与使用带有单个数字的 `[]` 相比,可使用带有范围的 `[]`,来创建包含一些特定字节的字符串切片: + +```rust + let hello = String::from("Здравствуйте"); + + let s = &hello[0..4]; +``` + +这里的 `s` 将是个包含该字符串头 4 个字节的 `&str`。早先曾提到过,每个的这些字符都是 2 字节,那么这就意味着 `s` 将为 `Зд`。 + +而在尝试使用类似 `&hello[0..1]` 这样的操作,来对某个字符的那些字节的一部分进行切片时,Rust 就会在运行时,以与在矢量中访问无效索引时同样的方式终止运行: + +```console +thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`'... +``` + +由于使用范围来创建出字符串切片这样的操作,可能将程序崩溃掉,因此进行这样操作时应小心谨慎。 + +### 对字符串进行迭代的一些方法 + +在字符串的各个片段上进行操作的最好方式,就是显示地指明是要字符还是字节。对于单独的 Unicode 标量值,就使用 `chars` 方法。在 `नमस्ते` 上调用 `chars`,就会分理出并返回六个类型 `char` 的值来,进而就可以对结果进行迭代,而访问到各个元素: + +```rust + for c in "नमस्ते".chars() { + println!("{}", c); + } +``` + +该代码将打印下面的东西: + +```rust +न +म +स + +त + +``` + +此外,`bytes` 方法返回的则是各个原始字节,对与特定领域,这方法可能正好: + +```rust + for b in s.bytes() { + println!("{}", b); + } +``` + +该代码将打印出构成这个 `String` 的 18 个字节来: + +```console +224 +164 +168 +224 +164 +174 +224 +164 +184 +224 +165 +141 +224 +164 +164 +224 +165 +135 +``` + +不过要确保记住,有效的 Unicode 标量值可能有多余 1 个字节组成。 + +而从字符串获取字素簇则是复杂的,因此这项功能并未由标准库提供。在需要该功能时,在 [crates.io](https://crates.io/) 上有一些可用的代码箱。 + +### 字符串并不简单 + +总的来说,字符串是复杂的。不同编程语言,在以何种程度将这种复杂度呈现给编程者上,做出了不同的选择。Rust 选择了将正确处理 `String` 数据,作为所有 Rust 程序的默认行为,这就意味着 Rust 程序员就必须在处理 UTF-9 数据时,要提前投入更多思考。这种权衡暴露了相较于其他编程语言,更多的字符串复杂度,但这防止了在软件开发生命周期后期,将涉及到的非 ASCII 字符的错误处理。 + +接下来就要切换到些许不那么复杂的事情:哈希图! + + +## 在哈希图中存储关联的键与值 + +最后一个常用集合,就是 *哈希图(hash map)* 了。类型 `HashMap`,存储了使用确定如何将这些类型为 `K` 的键,与类型为 `V` 的值放置于内存中的 *散列函数(a hashing function)*,而建立的键与值映射关系。许多编程语言都支持这种数据结构,不过他们通常使用了别的名称,比如哈希、图、对象、哈希表、字典,或者关系数组,这里仅举几例。 + +在打算不使用如同矢量中那样的索引,而是通过使用可为任意类型的键,来查找数据时,哈希图就是有用的了。比如在某个游戏中,就可在各个键为战队名字,值为战队得分的哈希图中,保持对这些战队得分的追踪。在给到战队名字后,就可获取到他的得分。 + +本小节将审视哈希图集合数据结构的基本 API,不过有数不尽的哈希图好处,都是隐藏在由标准库所定义、`HashMap` 上的那些函数里。与往常一样,请查看标准库文档,来了解更多信息。 + +### 创新一个新的哈希图 + +创建空哈希图的一种方式,即为使用 `new` 方法,与使用 `insert` 方法进行元素添加。在下面清单 8-20 中,就要对两个名字分别为 *蓝队(Blue)* 与 *黄队(Yellow)* 的战队得分,进行追踪。蓝队以 10 分开始,而黄队以 50 分开始。 + + +```rust + use std::collections::HashMap; + + let mut scores = HashMap::new(); + + scores.insert(String::from("蓝队"), 10); + scores.insert(String::from("红队"), 50); +``` + +*清单 8-20:创建一个新的哈希图并插入一些键与值* + +请注意这里需要首先 `use` 这个来自标准库集合部分的 `HashMap`。三个常用集合中,这个是最少用到的,因此他就没有包含在那些 Rust 程序前奏(the prelude)中,自动带入的特性里。哈希图受标准库的支持也较少;比如标准库中就没有内建的构造哈希图的宏。 + +与矢量一样,哈希图是将他们的数据存储在内存堆上的。示例中的这个 `HashMap` 键的类型为 `String`,值的类型为 `i32`。与矢量类似,哈希图都是同质的(homogeneous):所有键都必须有着同样类型,且所有值也必须有着同样类型。 + +另一种构造哈希图的方式,即为通过使用迭代器,与元组矢量上的 `collect` 方法,而元组矢量中各个元组,则是由一个键与其值组成。在 [第 13 章的 “使用迭代器处理一系列的条目” 小节](Ch13_Functional_Language_Features_Iterators_and_Closures.md#processing-a-series-of-items-with-iterators),就会深入到迭代器的有关细节及其相关方法。`collect` 方法会将数据收集到包括 `HashMap` 在内数种集合类型中。比如,在将战队名字与初始得分放在两个单独矢量中时,那么就可以使用 `zip` 方法,来创建一个元组的迭代器,其中 `Blue` 就会与 `10` 结对,并以此类推。随后就可以使用 `collect` 方法类将那个元组迭代器,转换到一个哈希图了,如下清单 8-21 中所示。 + + +```rust + use std::collections::HashMap; + + let teams = vec! [String::from("蓝队"), String::from("红队")]; + let initial_scores = vec! [10, 50]; + + let mut scores: HashMap<_, _> = teams + .into_iter() + .zip(initial_scores.into_iter()) + .collect(); +``` + +*清单 8-21:从战队清单与得分清单创建一个哈希图* + +由于有可能 `collect` 进到许多不同数据结构,而除非有指明,那么 Rust 就不清楚所想要的是何种数据结构,因此这里的类型注解 `HashMap<_, _>` 是需要的。不过对于键与值类型的泛型参数,这里使用了下划线(`_`),而 Rust 可基于那两个矢量中数据的类型,而推断出该哈希图的类型。在上面清单 8-21 中,键的类型将是 `String`,而值类型将为 `i32`,就跟清单 8-20 中的一样。 + + +### 哈希图与所有权 + +对于实现了 `Copy` 特质(the `Copy` trait) 的那些类型,比如 `i32`,那么他们的值就被拷贝到哈希图里。而对于像是 `String` 这样的被持有值,他们的所有值就会被迁移,进而哈希图会成为这些值的所有者,如同在下面清单 8-22 中所演示的那样。 + +```rust + use std::collections::HashMap; + + let field_name = String::from("喜好颜色"); + let field_value = String::from("蓝色"); + + let mut map = HashMap::new(); + map.insert(field_name, field_value); + + println! ("{}, {}", field_name, field_value); + // 到这里 field_name 与 field_value 就无效了,请尝试对 + // 他们进行使用,并看看会收到什么样的编译器错误! +``` + +*清单 8-25:对一旦被插入到哈希图,键与值就被哈希图持有的展示* + +```console +$ cargo run  ✔ + Compiling hashmap_demo v0.1.0 (/home/peng/rust-lang/hashmap_demo) +error[E0382]: borrow of moved value: `field_name` + --> src/main.rs:10:25 + | +4 | let field_name = String::from("喜好颜色"); + | ---------- move occurs because `field_name` has type `String`, which does not implement the `Copy` trait +... +8 | map.insert(field_name, field_value); + | ---------- value moved here +9 | +10 | println! ("{}, {}", field_name, field_value); + | ^^^^^^^^^^ value borrowed here after move + | + = note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0382]: borrow of moved value: `field_value` + --> src/main.rs:10:37 + | +5 | let field_value = String::from("蓝色"); + | ----------- move occurs because `field_value` has type `String`, which does not implement the `Copy` trait +... +8 | map.insert(field_name, field_value); + | ----------- value moved here +9 | +10 | println! ("{}, {}", field_name, field_value); + | ^^^^^^^^^^^ value borrowed here after move + | + = note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info) + +For more information about this error, try `rustc --explain E0382`. +error: could not compile `hashmap_demo` due to 2 previous errors +``` + +在对 `insert` 调用而导致 `field_name` 与 `field_value` 被迁移到那个哈希图中之后,这里就无法使用这两个变量了。 + +而在将到值的引用插入进哈希图时,这些值就不会被迁移进哈希图。对于这些引用所指向的值,则只要哈希图尚有效,那么他们便一直有效。在后面第 10 章的 [“以声明周期对引用有效性进行验证”](Ch10_Generic_Types_Traits_and_Lifetimes.md#validating-references-with-lifetimes) 小节中,将进一步讲到这些问题。 + +### 访问哈希图中的值 + +通过将某个值的键提供给 `get` 方法,就可以从哈希图中获取到该值来,如下清单 8-23 中所示。 + +```rust + use std::collections::HashMap; + + let mut scores = HashMap::new(); + + scores.insert(String::from("蓝队"), 10); + scores.insert(String::from("红队"), 50); + + let team_name = String::from("蓝队"); + let score = scores.get(&team_name); +``` + +*清单 8-23:对存储在哈希图中的蓝队得分进行访问* + +这里,`score` 将具有与蓝队关联的取值,同时结果将为 `Some(&10)`。由于 `get` 方法返回的是 `Option<&V>` 类型,因此该结构是封装在 `Some` 中的;当在哈希图中没有那个键的值时,`get` 就会返回 `None`。程序就需要以在第 6 章中所讲到的那些方式之一,对这个返回的 `Option` 加以处理。 + +可以与对矢量进行迭代的类似方式,即使用 `for` 循环,对哈希图中的各个键/值对加以迭代: + +```rust + use std::collections::HashMap; + + let mut scores = HashMap::new(); + + scores.insert(String::from("蓝队"), 10); + scores.insert(String::from("红队"), 50); + + for (key, value) in &scores { + println! ("{}, {}", key, value); + } +``` + +此代码将以任意顺序,打印出各个键值对: + +```console +蓝队, 10 +红队, 50 +``` + +### 更新哈希图 + +虽然键值对数目是可增长的,但每个键在某个时刻,只能有一个与其关联的值。在要修改哈希图中的数据时,就必须定下来怎样处理某个键已经指定了值的情形。可以将原有值替换为新值,而完全忽略原有值。可以保留原有值而忽视掉新值,而在键 *尚未* 有值时,仅将新值进行添加。或者可以将原有值与新值结合在一起。那就来看看怎样处理这些各种情况! + +**重写某个值** + +在将一个键与一个值插入到哈希图,并在随后再插入同样键与一个不同值,那么与那个键关联的值就会被替换掉。尽管下面清单 8-24 中的代码调用了 `insert` 两次,由于这里两次都是插入 “蓝队” 的值,因此那个哈希图将只包含一个键/值对。 + +```rust + use std::collections::HashMap; + + let mut scores = HashMap::new(); + + scores.insert(String::from("蓝队"), 10); + scores.insert(String::from("蓝队"), 25); + + println! ("{:?}", scores); +``` + +*清单 8-24:以特定键对存储的某个值进行替换* + +此代码打印 `{"蓝队": 25}`。原先的值 `10` 已被重写。 + + +**在键无值时而仅插入值** + +检查某个特定键是否有值,并在其没有值时,为其插入一个值,这样的情况是常见的。为此哈希图有个特别的、名为 `entry` 的 API,他会取要检查的键作为参数。`entry` 方法的返回值,是个叫做 `Entry` 的枚举,表示一个可能存在或可能不存在的值。那么下面就假设这里想要检查黄队的键有无与其关联的值。在其没有关联值时,就想要插入值 `50`,对于蓝队也同样操作。运用这个 `entry` API,代码看起来就如同下面清单 8-25 一样。 + +```rust + use std::collections::HashMap; + + let mut scores = HashMap::new(); + scores.insert(String::from("蓝队"), 10); + + scores.entry(String::from("黄队")).or_insert(50); + scores.entry(String::from("蓝队")).or_insert(50); + + println! ("{:?}", scores); +``` + +*清单 8-25:使用 `entry` 方法仅在键尚无值时插入值* + +`Entry` 类型上的 `or_insert` 方法,被定义为在键存在时,返回相应 `Entry` 键的值的可变应用,而若键不存在,那么就会将其参数作为该键的新值插入,并返回到该新值的可变引用。此技巧相比于咱们自己来编写该逻辑,要清楚得多,此外,在以借用规则检查器进行检查时,进行得也更好。 + +运行清单 8-25 中的代码,将打印出 `{"黄队": 50, "蓝队": 10}`。其中首次到 `entry` 的调用,由于黄队尚无值,因此就会插入黄队的键与值 `50`。而第二个到 `entry` 的调用,因为蓝队已经有了值 `10`,因此就不会修改这个哈希图。 + + +**基于原有值而对某个值进行更新** + +哈希图的另一个常见使用情形,即是查找某个键的值,并随后根据原有值对其更新。举例来说,下面清单 8-26 给出了对在一些文字中各个词出现次数进行计数的代码。这里使用了一个以词汇作为键的哈希图,并对值进行增加来追踪已见到那个词了多少次。而在首次见到某个词时,就会首先插入值 `0`。 + +```rust + use std::collections::HashMap; + + let text = "hello world wonderful world"; + + let mut map = HashMap::new(); + + for word in text.split_whitespace() { + let count = map.entry(word).or_insert(0); + *count += 1; + } + + println! ("{:?}", map); +``` + +*清单 8-26:使用存储词汇与计数的哈希图,对词汇出现次数进行计数* + +此代码将打印 `{"wonderful": 1, "world": 2, "hello": 1}`。这个 `split_withespace` 方法会对 `text` 中的那个值的、以空格分隔的子切片进行迭代。而那个 `or_insert` 方法,返回的时到指定键的值的可变引用(`&mut V`)。这里是将那个可变引用存储在变量 `count` 中的,因此为了对那个值进行赋值,这里就必须首先使用星号(`*`)对 `count` 解引用。在那个 `for` 循环结尾,该可变引用就超出了作用域,因此所有这些修改都是安全的,同时为借用规则所允许。 + +### 散列函数(Hashing Functions) + +默认情况下,`HashMap` 使用了一个可提供抵抗哈希表有关的拒绝服务攻击的、名为 *`SipHash`* 的散列函数(参见:[https://en.wikipedia.org/wiki/SipHash](https://en.wikipedia.org/wiki/SipHash))。这并非可用的最快散列算法,不过这种为了更好安全性,而在性能上的舍弃,是值得的。在对自己代码进行推敲,而发现这个默认散列函数对于自己目的太慢时,是可以通过指定别的哈希器,切换到另一函数的。 *哈希器(a hasher)* 是一种实现了 `BuildHasher` 特质(the `BuildHasher` trait)的类型。在第 10 章中就会谈到特质及其如何实现。不必从头实现自己的哈希器;[crates.io](https://crates.io/) 就有由其他 Rust 使用者共享的、提供对许多常用散列算法进行实现的哈希器的库。 + + +## 本章小结 + +矢量、字符串与哈希图,在程序中需要存储、访问与修改数据时,就会提供大量必要功能。下面就是一些现在应有能力解决的练习: + +- 给定一个整数清单,请使用矢量,并返回这些数的中位数(即在这些数排序后,位于中间位置的值)与众数(最常出现的那个值;此时哈希图将有帮助); +- 将字符串(英语)转换为拉丁语式的结尾。每个词汇的第一个常量,会被迁移到该词汇的末尾,同时会加上 “ay”,那么 “first” 就变成了 “irst-fay” 了。以元音开头的词汇,则会将 “hay” 添加到词汇末尾(比如 “apple” 就成了 “apple-hay”)。请牢记有关 UTF-8 编码的那些细节! +- 运用哈希图与矢量,创建一个实现程序用户把员工名字添加到某公司里的某个部门的文本接口。比如,“添加 Sally 到工程部” 或 “添加 Amir 到销售部”。随后让用户获取到某个部门全体人员清单,或以部门字母排序的公司全体人员名单。 + +标准库 API 文档对矢量、字符串及哈希图有着的、对这些练习将有帮助的方法都有说明! + +接下来就要进入到一些其中某些操作可能失败的程序,那么现在就是讨论错误处理的最佳时机。下一章就要来完成对错误处理的讨论了! diff --git a/src/Ch09_Error_Handling.md b/src/Ch09_Error_Handling.md new file mode 100644 index 0000000..e70e1dd --- /dev/null +++ b/src/Ch09_Error_Handling.md @@ -0,0 +1,636 @@ +# 错误处理 + +在软件中,错误是家常便饭,因此 Rust 有数个用于处理某些东西出了岔子情形的特性。在许多情况下,Rust 都要求编程者知晓某种错误的可能性,进而在代码编译之前就采取一些措施。这样的要求通过确保编程者在将其代码部署到生产环境之前,会发现错误并对其进行恰当处理,而使得他们的程序更为健壮! + +Rust 将错误分组为两个主要类别: *可恢复(recoverable)* 与 *不可恢复(unrecoverable)* 错误。对于可恢复错误,比如 *文件未找到* 错误,大多数情况下只要将该故障汇报给用户,并重试该操作。而不可恢复错误则总是代码错误的表征,像是尝试访问超出数组末端的某个位置,进而因此就要立即停止该程序。 + +大多数语言都没有区分这两种错误,而以同样方式,使用诸如异常的机制处理这两种错误。Rust 没有异常。相反,Rust 有着用于可恢复错误的类型 `Result`,以及在程序发生了不可恢复错误时,停止程序执行的 `panic!` 宏(the `panic!` macro)。本章将首先涵盖对 `panic!` 的调用,并在随后讲解那些返回的 `Result` 值。此外,这里会对在决定是否要尝试从错误中恢复,还是要停止程序的执行时的诸多考虑,进行探讨。 + + +## 带 `panic!` 的不可恢复错误 + +**Unrecoverable Errors with `panic!`** + +某些时候,在代码中不好的事情发生了,而对其无计可施。在这些情形下,Rust 有着 `panic!` 宏。在 `panic!` 宏执行时,程序就会打印一条失败消息,释放(unwind)并清理掉栈,并在随后退出。在侦测到某种类别的代码错误,且在编写程序时刻,尚不清楚怎样处理这个故障时,就会触发一个程序中止(invoke a panic)。 + +> **对程序终止进行响应的栈解除或栈终止(Unwinding the Stack or Aborting in Response to a Panic)** +> +> 默认情况下,在程序终止发生时,程序就开始 *解除栈(unwinding)*,这是指 Rust 对栈进行回退,并清理他所遇到的各个函数的数据。然而,这样的回退与清理,是很多的工作量。那么因此 Rust 就允许编程者选择立即 *终止(aborting)* 的替代方案,该替代方案会不加清理的结束程序。程序曾用过的内存,这时就需要由操作系统来清理。如过在项目中,需要将生成的二进制执行文件构造得尽可能小,你们就可以通过把 `panic= 'abort'`,添加到 `Cargo.toml` 文件中恰当的 `[profile]` 小节,而从程序中止的栈解除切换为立即终止。比如,若想要在发布模式中,于程序中止时立即终止,那么就要添加这个: + +```toml +[profile.release] +panic = 'abort' +``` + +下面就来在一个简单程序中,尝试调用 `panic!` 宏: + +文件名:`src/main.rs` + +```rust +fn main() { + panic! ("崩溃并燃烧"); +} +``` + +在运行该程序时,就会看到下面这样的东西: + + +```console +$ cargo run + Compiling error_handling_demo v0.1.0 (/home/lenny/rust-lang/error_handling_demo) + Finished dev [unoptimized + debuginfo] target(s) in 0.40s + Running `target/debug/error_handling_demo` +thread 'main' panicked at '崩溃并燃烧', src/main.rs:2:5 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +``` + +这个到 `panic!` 的调用,引起了包含在最后两行中的错误消息。第一行给出了程序中止消息及源码里程序中止发生的位置:`src/main.rs:2:5` 表示是在这里的 `src/main.rs` 文件的第二行、第五个字符处。 + +在此情况下,所指出的那行,就是这里代码的一部分,而在前往到那行时,就会看到那个 `panic!` 宏调用。在别的情况下,`panic!` 调用可能会在所编写代码调用的代码中,那么由该错误消息报告出的文件名与行号,就会是 `panic!` 被调用所在之处的其他人的代码,而不会是最终引起那个 `panic!` 调用的自己编写的代码行。这里可以使用该 `panic!` 调用来自那些函数的回溯,来弄清楚此处代码的哪个部分导致了该问题。接下来就要详细讨论这种回溯。 + +### 运用 `panic!` 回溯 + +现在来看一下另一个示例,看看由于代码中的编码错误,而非由于在代码中直接调用 `panic!` 宏时,来自库的 `panic!` 调用到底会是什么样子。下面清单 9-1 有一些尝试访问某个矢量中超出了有效索引范围索引的代码。 + +文件名:`src/main.rs` + +```rust +fn main() { + let v = vec! [1, 2, 3]; + + v[99]; +} +``` + +*清单 9-1:尝试访问某个超出了矢量末端的元素,这会导致一个到 `panic!` 的调用* + +这里正尝试访问这里矢量的第 100 个元素(由于索引开始于零处,故那是在索引 99 处),但这个矢量只有 3 个元素。在此情况下,Rust 就会中止。使用 `[]` 被认为是要返回一个元素的,但在传递某个无效索引时,这里就没有 Rust 可返回的正确元素。 + +在 C 语言中,尝试读取超出某种数据结构,属于未定义的行为。那么就可以会得到与该数据结构中元素对应的、内存中那个位置处的任何东西,即便该内存不属于那个数据结构。这就叫做 *缓冲区重读取(a buffer overread)*,并能在攻击者可以这样的方式操作索引,来读取存储在该数据结构之后的、本不应允许他们读取的数据时,导致安全漏洞。 + +Rust 为保护程序免受这类漏洞的危害,就会在尝试位于某个不存在索引处的元素时,停止程序的执行而拒绝继续下去。来尝试运行一下上面的代码看看: + +```console +$ cargo run lenny@vm-manjaro + Compiling error_handling_demo v0.1.0 (/home/lenny/rust-lang/error_handling_demo) + Finished dev [unoptimized + debuginfo] target(s) in 0.47s + Running `target/debug/error_handling_demo` +thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +``` + +此错误指出了,在这里 `main.rs` 的第 4 行,其中尝试访问索引 `99` 处。接下来的注解行,讲到这里可将 `RUST_BACKTRACE` 环境变量,设置为获取究竟发生什么,才导致了这个错误。所谓 *回溯(a backtrace)*,即为到此已调用的全部函数的清单。Rust 中的回溯,与其他语言中的回溯完成的事情一样:阅读回溯的冠军,就是要从顶部开始,一直要读到自己编写的文件为止。那便是该问题缘起之处。在那个点位之上的那些行,就是所编写代码曾调用过的代码;而所编写代码之下的那些行,则是调用所编写代码的代码。这些前前后后的行,就可能包含核心 Rust 代码、 标准库代码,或者正使用着的代码箱。下面就来通过将 `RUST_BACKTRACE` 环境变量,设置为除 `0` 之外的任何值,尝试获取到回溯。下面清单 9-2 展示了与将会看到的类似输出。 + +```console +$ RUST_BACKTRACE=1 cargo run lenny@vm-manjaro + Finished dev [unoptimized + debuginfo] target(s) in 0.02s + Running `target/debug/error_handling_demo` +thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5 +stack backtrace: + 0: rust_begin_unwind + at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/std/src/panicking.rs:584:5 + 1: core::panicking::panic_fmt + at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:142:14 + 2: core::panicking::panic_bounds_check + at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:84:5 + 3: >::index + at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:242:10 + 4: core::slice::index:: for [T]>::index + at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:18:9 + 5: as core::ops::index::Index>::index + at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/alloc/src/vec/mod.rs:2591:9 + 6: error_handling_demo::main + at ./src/main.rs:4:5 + 7: core::ops::function::FnOnce::call_once + at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/ops/function.rs:248:5 +note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace. +``` + +*清单 9-2:在设置了 `RUST_BACKTRACE` 环境变量时,由到 `panic!` 调用生成的回溯被显示了出来* + +那可是很多的输出了!具体看到的输出,可能根据操作系统与 Rust 版本而有所不同。为从此信息中获得回溯,就要开启那些调试符号。在不带 `--release` 标志使用 `cargo build` 或 `cargo run` 时,如同这里这样,这些调试符号默认就是开启的。 + +在上面清单 9-2 里的输出中,回溯所指向到这里项目中行的第 6 行,就是导致问题的行:即 `src/main.rs` 的第 4 行。在不想要这个程序中止时,就应在首个提到了自己所编写文件的行,所指向的那个位置,开始排查。在之前的清单 9-1 中,那里有意编写了会中止的代码,而修正程序中止的办法,就是不要请求某个超出那个矢量索引范围的元素。而在今后代码中止时,就需要搞清楚代码是在对什么值进行何种操作,而导致了中止,以及代码应该怎么做。 + +在本章的 [要 `panic!` 或不要 `panic!`](#to-panic-or-not-to-panic) 小节,将回到 `panic!` 这个话题,并讨论在何时要用 `panic!`,何时不应使用 `panic!` 来处理不同错误情形。接下来,就会看看怎样使用 `Result`,从错误中恢复过来。 + +## 带有 `Result` 的可恢复错误 + +多数错误都没有严重到要求程序整个地停止运行。某些时候,在某个函数失败时,必定是由于某种可易于解释进而加以响应的原因。比如在尝试打开某个文件,而因为要打开的文件不存在,那个操作失败了时,那么可能希望创建该文件,而不是中止这个进程。 + +回顾第二章中的 [处理潜在带有 `Result` 类型的程序失败](Ch02_Programming_a_Guessing_Game.md#handling-potential-failure-with-the-result-type) 小节,其中的 `Result` 枚举被定义为有两个变种,`Ok` 与 `Err`,如下所示: + +```rust +enum Result { + Ok, + Err, +} +``` + +这里的 `T` 与 `E`,都属于泛型参数(generic type parameters):在第 10 章就会更深入讨论泛型。此刻需要明白的是,这里的 `T` 表示在操作成功情形下,那个 `Ok` 变种里返回值的类型,而这里的 `E`,则表示在失效情形下,将返回的在 `Err` 变种里错误的类型。由于 `Result` 有着这些泛型参数,因此就可以在打算返回成功值与错误值有所区别的许多不同情形下,使用到这个 `Result` 及定义在其上的函数。 + +下面就来调用一个由于其会失败,而返回 `Result` 值的函数。在下面清单 9-3 中,是尝试打开一个文件。 + +文件名:`src/main.rs` + +```rust +use std::fs::File; + +fn main() { + let f = File::open("hello.txt"); +} +``` + +*清单 9-3:打开某个文件* + + +怎样知道 `File::open` 会返回一个 `Result` 呢?这里就可以看看 [标准库 API 文档](https://doc.rust-lang.org/std/fs/struct.File.html#method.open),或者可以询问一下编译器!在赋予 `f` 一个明知 *不是* 该函数返回值类型的类型注解,并随后尝试编译该代码时,编译器就会告知,这两个类型不匹配。给出的错误消息,就会告诉 `f` 的类型是什么。来试试吧!这里已知 `File::open` 的返回类型不是 `u32`,因此就把那个 `let f` 语句修改为下面这样: + +```rust +let f: u32 = File::open("hello.txt"); +``` + +现在尝试编译,就会给到接下来的输出: + +```console +$ cargo run lennyp@vm-manjaro + Compiling error_handling_demo v0.1.0 (/home/lennyp/rust-lang/error_handling_demo) +error[E0308]: mismatched types + --> src/main.rs:4:18 + | +4 | let f: u32 = File::open("hello.txt"); + | --- ^^^^^^^^^^^^^^^^^^^^^^^ expected `u32`, found enum `Result` + | | + | expected due to this + | + = note: expected type `u32` + found enum `Result` + +For more information about this error, try `rustc --explain E0308`. +error: could not compile `error_handling_demo` due to previous error +``` + +这就是说,`File::open` 函数的返回类型,是个 `Result`。泛型参数 `T`,在这里已被使用成功值的类型,`std::fs::File`,即一个文件句柄(a file handle)填充。而用于错误值的类型 `E`,则为 `std::io::Error`。 + +这样的返回值类型,表示到 `File::open` 的调用,可能会成功而返回一个能够自该处读取,或写入到该处的文件句柄。该函数调用同样可能失败:比如该文件可能不存在,或可能没有访问该文件的权限。那么这个 `File::open` 函数,就需要具备已知告知其是否成功或失败的方式,与此同时给到一个文件句柄,或者错误信息。这样的信息,正是这个 `Result` 枚举所要表达的。 + +此示例中,在 `File::open` 成功处,变量 `f` 中的值就会是包含了一个文件句柄的 一个 `Ok` 实例。而在其失败的情况下,`f` 中的那个值,就会是包含了有关所发生错误类别的更多信息的一个 `Err` 实例。 + +这里就需要对清单 9-3 中代码进行添加,从而根据 `File::open` 所返回值,而采取不同措施。下面清单 9-4 就给出了一种使用基本工具,即在第 6 章中曾讨论过的 `match` 表达式,对那个 `Result` 进行处理的方法。 + +文件名:`src/main.rs` + +```rust +use std::fs::File; + +fn main() { + let f = File::open("hello.txt"); + + let f = match f { + Ok(file) => file, + Err(e) => panic! ("打开文件出现问题:{:?}", e), + }; +} +``` + +*清单 9-4:运用 `match` 表达式来处理可能返回的各个 `Result` 变种* + +请注意,与 `Option` 枚举类似,这个 `Result` 枚举及其变种,是已由 Rust 前奏(the prelude)带入到作用域中了的,因此这里无需在那两个 `match` 支臂中的 `Ok` 与 `Err` 变种之前,指明 `Result::`。 + +在返回结果为 `Ok` 时,此代码就会返回从 `Ok` 变种抽出的那个内部的 `file` 值,且这里随后就把那个文件句柄值,指派给那个变量 `f`。在这个 `match` 之后,就可以将这个文件句柄,用于读取或写入了。 + +而那个 `match` 的另一支臂,则处理了从 `File::open` 得到一个 `Err` 值的情形。在此示例中,选择了调用 `panic!` 宏。在当前目录中没有名为 `hello.txt` 的文件,并运行此代码时,就会看到来自那个 `panic!` 宏的如下输出: + +```console +$ cargo run lennyp@vm-manjaro + Finished dev [unoptimized + debuginfo] target(s) in 0.00s + Running `target/debug/error_handling_demo` +thread 'main' panicked at '打开文件出现问题:Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:8:19 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +``` + +与往常一样,此输出告知了到底什么出错了。 + +### 匹配各异的错误 + +上面清单 9-4 中的代码,不论 `File::open` 因何而失败,都会 `panic!`。然而,这里是要因应不同失败原因,而采取不同措施:在 `File::open` 因为那个文件不存在而失败时,就要创建该文件并返回到那个新建文件的句柄。在那个 `File::open` 因别的其他原因失败 -- 比如没有打开该文件的权限时,这里仍要该代码以清单 9-4 中所做的同样方式,`panic!` 掉。为此,这里就要添加一个内部的 `match` 表达式,如下清单 9-5 中所示。 + +文件名:`src/main.rs` + +```rust +use std::fs::File; +use std::io::ErrorKind; + +fn main() { + let f = File::open("hello.txt"); + + let f = match f { + Ok(file) => file, + Err(e) => match e.kind() { + ErrorKind::NotFound => match File::create("hello.txt") { + Ok(fc) => fc, + Err(error) => panic! ("创建该文件时出现问题:{:?}", error), + }, + other_error => panic! ("打开文件出现问题:{:?}", other_error), + }, + }; +} +``` + +*清单 9-5:以不同方式处理不同类别的错误* + +`File::open` 所返回的位于 `Err` 变种内部的值的类型为 `io::Error`,他是一个由标准库提供的结构体。该结构体有个可供调用以获取到 `io::ErrorKind` 值的方法 `kind`。而枚举 `io::ErrorKind` 亦是由标准库提供,并有着表示那些可能自某个 `io` 操作而引起的,不同类别错误的一些变种。这里打算使用的变种为 `ErrorKind::NotFound`,表示了正尝试打开的文件尚不存在。因此这里既对 `f` 进行了匹配,而同时还有了在 `e.kind()` 上的一个内层匹配。 + +这里打算检查的那个内层匹配中的条件,则是由 `e.king()` 所返回的那个值,是否为 `ErrorKind` 枚举的 `NotFound` 变种。在 `e.kind()` 返回的值为 `ErrorKind` 的 `NotFound` 变种时,这里就尝试以 `File::create` 来创建该文件。然而由于 `Fiel::create` 仍会失败,因此这里就需要在那个内层 `match` 表达式中的第二个支臂。在该文件无法被创建出来时,就会打印出一条不同的错误消息。外层那个 `match` 表达式的第二支臂保持原样,因此该程序会在除了文件未找到错误之外的其他任何错误时,都会中止运行。 + +> **这种结合`Result` 运用 `match` 表达式的替代方案** +> +> 那可是有好多的 `match` !`match` 表达式是很有用,但同样也是很原始的。在第 13 章,就会了解到闭包(closures),这种与定义在 `Result` 上的众多方法一起使用的特性。在对代码中的 `Result` 值进行处理时,比起使用 `match` 表达式,这样的闭包方式可以简练得多。 +> 比如,下面就是编写与清单 9-5 中同样逻辑,不过却使用了闭包特性与 `unwrap_or_else` 方法的另一种方式。 + +```rust +use std::fs::File; +use std::io::ErrorKind; + +fn main() { + let f = File::open("hello.txt").unwrap_or_else(|e| { + if e.kind() == ErrorKind::NotFound { + File::create("hello.txt").unwrap_or_else(|error| { + panic! ("创建文件时发生问题:{:?}", error); + }) + } else { + panic! ("打开文件时出现问题:{:?}", e); + } + }); + + println! ("{:?}", f); +} +``` + +> 尽管此代码与清单 9-5 有着同样行为,但他并未包含任何的 `match` 表达式,且读起来更清楚。请在读完了第 13 章,并看看标准库文档中的这个 `unwrap_or_else` 方法后,再回到这个示例。在对错误进行处理时,许多别的这些方法,都可以清理掉大量嵌套的 `match` 表达式。 + + +### 因错误而中止的快捷方式:`unwrap` 与 `expect` + +**Shortcuts for Panic on Error: `unwrap` and `expect`** + +运用 `match` 运作足够良好,不过那样可能有点冗长,且不总是良好地传达了意图。这个 `Result` 类型,其上本来就定义了许多用于完成各种各样的、更为具体任务的辅助方法。其中的 `unwrap` 方法,就是一个实现了刚好与前面清单 9-4 中所编写的 `match` 表达式类似的快捷方法。在 `Result` 的值为 `Ok` 变种时,`unwrap` 就会返回那个 `Ok` 内部的值。而在该 `Result` 为 `Err` 变种时,`unwrap` 则会代为调用 `panic!` 宏。下面就是运作中的一个 `unwrap` 示例: + +文件名:`src/main.rs` + +```rust +use std::fs::File; + +fn main() { + let f = File::open("hello.txt").unwrap(); +} +``` + +在没有 `hello.txt` 文件下运行此程序时,就会看到一条来自由这个 `unwrap` 方法做出的 `panic!` 宏调用的错误消息: + +```console +$ cargo run lennyp@vm-manjaro + Finished dev [unoptimized + debuginfo] target(s) in 0.03s + Running `target/debug/error_handling_demo` +thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:37 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +``` + +同样,`Result` 上的 `expect` 方法,则可实现对这条 `panic!` 错误消息的选取。使用 `expect` 而非 `unwrap` 并提供良好的错误消息,就能够传达到自己的意图,进而令到追踪程序中止缘由更为容易。`expect` 方法的语法如下所示: + +文件名:`src/main.rs` + +```rust +use std::fs::File; + +fn main() { + let f = File::open("hello.txt").expect("打开 hello.txt 失败"); +} +``` + +这里以与 `unwrap` 同样方式,使用了 `expect`:用于返回文件句柄,或者对 `panic!` 宏进行调用。而在 `expect` 调用 `panic!` 时用到的错误消息,就将是这里传递给 `expect` 的那个参数,而不再是 `unwrap` 所用到的那个默认 `panic!` 消息了。下面就是该错误消息看起来的样子: + +```console +$ cargo run lennyp@vm-manjaro + Compiling error_handling_demo v0.1.0 (/home/lennyp/rust-lang/error_handling_demo) + Finished dev [unoptimized + debuginfo] target(s) in 1.23s + Running `target/debug/error_handling_demo` +thread 'main' panicked at '打开 hello.txt 失败: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:37 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +``` + +由于此错误消息是以这里所指定的,`打开 hello.txt 失败` 开始,因此就会更易于搞清楚,此错误消息来自代码中的何处。而若在多处使用 `unwrap`,那么在要精准找出到底是那个 `unwrap` 导致了程序中止时,就会因为所有这些调用了 `panic!` 的 `unwrap`,都打印出同样消息,而要耗费更多时间。 + + +### 传递错误(Propagating Errors) + +在某函数实现调用了可能失败的某些东西时,与其在该函数自身里头对错误进行处理,还可以将该错误返回给调用该函数的代码,这样调用该函数的代码就可以自己决定要做些什么。这就叫做 *传递(propagating)* 错误,而将更多的控制,给到调用该函数的代码,相比于当前实现的函数代码,调用代码中可能会有更多决定该错误应如何被处理的信息或逻辑。 + +比如,下面清单 9-6 就给出了一个从某个文件读取用户名的函数。在那个文件不存在或无法读取时,这个函数就会将这些错误返回给调用该函数的代码。 + +```rust +use std::fs::File; +use std::io::{self, Read}; + +fn read_username_from_file() -> Result { + let username_file_result = File::open("hello.txt"); + + let mut username_file = match username_file_result { + Ok(file) => file, + Err(e) => return Err(e), + }; + + let mut username = String::new(); + + match username_file.read_to_string(&mut username) { + Ok(_) => Ok(username), + Err(e) => Err(e), + } +} +``` + +*清单 9-6:使用 `match` 将错误返回给调用代码的一个函数* + +虽然可以简单得多的方式,来重写该函数,不过为了对错误处理进行探索,因此这里就要通过亲自动手完成其大部分代码开头;在结束时,就会给出那更简短的方式。首先来看看该函数的返回值类型:`Result`。这表示该函数要返回一个类型 `Result` 的值,其中的泛型参数 `T` 已被具体类型 `String` 填充,而那个泛型 `E` 则已被具体类型 `io::Error` 填充。若此函数不带任何问题的成功运行,那么调用该函数的代码,就会收到一个保存着一个 `String` 的 `Ok` 值 -- 即该函数从那个文件中读取到的用户名。而在该函数出现任何问题时,那么调用代码就会收到一个,保存着包含了有关所出现问题更多信息的 `io::Error` 示例的 `Err` 值。这里之所以选择 `io::Error` 作为此函数的返回值,是因为在该函数的函数体中所调用的两个都可能失败的操作:`File::open` 与 `read_to_string`,他们所返回的错误值都是这个 `io::Error` 类型。 + +该函数的函数体,是以调用 `File::open` 函数开始的。随后这里就以与清单 9-4 中类似方式,使用了一个 `match` 处理 `File::open` 返回的 `Result`。在 `File::open` 成功时,那么在模式变量 `file` 中的文件句柄,就成为那个可变变量 `f` 中的值,且函数会继续执行。而在 `Err` 情形下,这里使用了 `return` 关键字,早早地就从这个函数 `return` 了出去,同时将来自 `File::open` 的那个错位值,此时是在模式变量 `e` 中,作为该函数的错误值,传回给调用该函数的代码。 + +因此在 `username_file` 有着一个文件句柄时,该函数随后就会创建一个在变量 `username` 中的新 `String`,并调用 `username_file` 中文件句柄上的 `read_to_string` 方法,来将该文件中的内容,读取到 `username` 中。因为即使 `File::open` 运行成功,这个 `read_to_string` 仍可能失败,因此他同样会返回一个 `Result`。那么这里就需要另一个 `match`,来处理这个 `Result`:在 `read_to_string` 成功时,那么接下来这个函数就成功执行了,进而就从这个文件,返回到此时位于封装在一个 `Ok` 中的 `username` 中的用户名来。而在 `read_to_string` 失败时,这里就会以与之前在那个处理 `File::open` 返回值的 `match` 中返回错误值的同样方式,返回现在这个 `read_to_string` 的错误值。不过,由于这是该函数中的最后一个表达式,因此这里无需显示地写下 `return`。 + +调用此代码的代码,随后就会对收到的包含了用户名 `Ok` 值,或者包含了一个 `io::Error` 类型的 `Err` 值进行处理。至于要对这些值做何处理,则取决于调用代码了。在调用代码收到 `Err` 值时,他就可以采取好比调用 `panic!` 并崩溃掉该程序,可以使用某个默认用户名,或者从相比该文件的其他地方,查找该用户名等操作。这里没有关于那个调用代码确切地尝试要做什么的足够信息,因此这里就把全部的成功或错误信息,向上传递给调用代码,让调用代码进行适当处理。 + +由于在 Rust 中这样的传递错误模式是如此普遍,以致于 Rust 提供了问好操作符(the question mark operator, `?`),来令到错误传递更加容易。 + + +### 传递错误的快捷方式:`?` 操作符 + +下面清单 9-7 给出了与清单 9-6 有着同样功能的一个 `read_username_from_file` 实现,只是此实现使用了 `?` 操作符。 + +文件名:`src/main.rs` + +```rust +use std::fs::File; +use std::io::{self, Read}; + +fn read_username_from_file() -> Result { + let mut username_file = File::open("hello.txt")?; + let mut username = String::new(); + username_file.read_to_string(&mut username)?; + Ok(username) +} +``` + +*清单 9-7:一个使用 `?` 操作符将错误返回给调用代码的函数* + +那个放在某个 `Result` 值后面的 `?`,被定义为几乎与之前所定义的那些,用于处理清单 9-5 中那些 `Result` 值的 `match` 表达式,以同样方式运作。在 `Result` 的值为 `Ok` 时,那么那个 `Ok` 内部的值,就会从该表达式得以返回,且程序将继续运行。而在该 `Result` 值为一个 `Err` 时,则会如同之前曾用到的 `return` 关键字一样,将自这整个函数,返回这个 `Err` 值,进而这个错误值,就被传递给了调用代码。 + +清单 9-6 中的 `match` 表达式完成的事情,与这个 `?` 操作符完成的事情有个不同点:调用了这个 `?` 操作符的错误值,会经过定义在标准库中 `From` 特质(the `From` trait in the standard library)中定义的 `from` 函数,而该函数被用于将一种类型的值,转换到另一种类型中。当 `?` 操作符调用 `from` 函数时,被接收到的错误类型,就被转换为了定义在当前函数返回值类型中的类型了(即 `Result`)。在某个函数可能失败,即便该函数的一些部分而不是整个函数,由于许多不同原因而失败,而返回一种表示这些全部失败方式的一种错误类型时,这个不同之处就会有用。 + +比如,这里本可将清单 9-7 中的 `read_username_from_file` 函数,修改为返回一个自己定义的名为 `OurError` 的定制错误类型。而在同时给 `OurError` 定义了 `impl From`,以从 `io::Error` 构造出一个 `OurError` 的实例时,那么随后无需添加任何代码到这个函数,`read_username_from_file` 函数中的这些 `?` 操作符,就会调用 `from` 并对那些错误类型进行转换。 + +在清单 9-7 的语境下,位于 `File::open` 调用末尾的那个 `?`,将返回一个 `Ok` 内部的值给变量 `username_file`。而在有错误发生时,这个 `?` 操作符,就会早早地从整个函数退出,并把任何的 `Err` 值给到调用代码。对于那个 `read_to_string` 调用末尾处的 `?`,适用这同样的情况。 + +`?` 操作符消除了很多样板代码(a lot of boilerplate),并令到此函数的实现更为简单。通过将这些方法调用在整个 `?` 即刻链接起来,甚至可以进一步缩短此代码,如下清单 9-8 中所示。 + +文件名:`src/main.rs` + +```rust +use std::fs::File; +use std::io::{self, Read}; + +fn read_username_from_file() -> Result { + let mut username = String::new(); + + File::open("hello.txt")?.read_to_string(&mut username)?; + + Ok(username) +} +``` + +*清单 9-8:在 `?` 操作符后将方法调用链接起来* + +这里已将那个 `username` 中的新 `String` 的创建,挪到了该函数的开头;整个函数就整个部分未作改动。这里没有了变量 `username_file` 的创建,而是已将到 `read_to_string` 的函数调用,直接链接到了 `File::open("hello.txt")?` 的结果上。在 `read_to_string` 调用的末尾仍有一个 `?`,同时在这两个 `File::open` 与 `read_to_string` 调用都成功,而不返回错误时,这里就会返回一个包含了 `username` 的 `Ok` 值。功能仍旧与清单 9-6 和清单 9-7 中是一样的;这只是一种不同的、更为符合人体工程学的编写方式。 + +下面清单 9-9 给出了使用 `fs::read_to_string` 的一种甚至更加简短的方式。 + +文件名:`src/main.rs` + +```rust +use std::fs; +use std::io; + +fn read_username_from_file() -> Result { + fs::read_to_string("hello.txt") +} +``` + +*清单 9-9:使用 `fs::read_to_string` 而非打开在读取那个文件* + +将某个文件读取到字符串中,是个相当常见的操作,因此标准库提供了便捷的打开文件、创建一个新 `String`、读取文件内容、将内容放入到那个 `String`,并将其返回的 `fs::read_to_string` 函数。当然,`fs::read_to_string` 的使用,并不能赋予到这里对全部错误处理加以解释的机会,因此这里才要走过前面那些常常的过程。 + + + +### 哪些地方可以使用 `?` 操作符 + +`?` 操作符仅可用于那些返回值类型,与这个 `?` 被用于的那个值类型兼容的函数中。这是由于 `?` 操作符被定义为与在清单 9-6 中,所定义的那个 `match` 表达式类似方式,执行一个该函数早期阶段的退出。在清单 9-6 中,那个 `match` 使用的是一个 `Result` 值,同时那个先期返回支臂返回的是一个 `Err(e)` 值。那么那个函数的返回值类型,就必须是个 `Result`,这样才与这个 `return` 兼容。 + +在下面清单 9-10 中,就要看看一个有着与其上使用了 `?` 的类型值不兼容返回值的 `main` 函数中,使用 `?` 操作符会收到的错误: + +文件名:`src/mian.rs` + +```rust +use std::fs::File; + +fn main() { + let greating_file = File::open("hello.txt"); +} +``` + +*清单 9-10:尝试在返回 `()` 的 `main` 函数中使用 `?` 就不会编译* + + +此代码是要打开一个文件,这就可能失败。那个 `?` 操作符接续了由 `File::open` 所返回的 `Return` 值,然而这个 `main` 函数的返回值类型为 `()`,而非 `Result`。那么在编译此代码时,就会得到以下的错误消息: + +```console +$ cargo run lennyp@vm-manjaro + Compiling error_handling_demo v0.1.0 (/home/lennyp/rust-lang/error_handling_demo) +error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`) + --> src/main.rs:4:48 + | +3 | / fn main() { +4 | | let greating_file = File::open("hello.txt")?; + | | ^ cannot use the `?` operator in a function that returns `()` +5 | | } + | |_- this function should return `Result` or `Option` to accept `?` + | + = help: the trait `FromResidual>` is not implemented for `()` + +For more information about this error, try `rustc --explain E0277`. +error: could not compile `error_handling_demo` due to previous error +``` + +此错误指出了只允许在返回 `Result`、`Option` 或别的实现了 `FromResidual` 的类型的函数中,使用 `?` 操作符。 + +而要修正这个错误,则有两个选择。一个选择是在没有修改函数返回值类型的限制时,那么就将其修改为与在其上使用 `?` 操作符的值类型兼容。另一技巧,则是使用一个 `match` 表达式,或某个 `Result` 的那些方法,来以某种恰当方式对这个 `Result` 进行处理了。 + +这个错误消息还提到,`?` 还可与 `Option` 类型的值一同使用。与在 `Result` 上使用 `?` 一样,可在返回一个 `Option` 的函数中的 `Option` 上使用 `?`。在某个 `Option` 上调用 `?` 操作符的行为,与在 `Result` 上其被调用时的行为类似:在该值为 `None` 时,`None` 就会在那个地方及早地从该函数被返回。而在该值为 `Some` 时,那么这个 `Some` 内部的值,就是该表达式的结果值,同时函数会继续执行。下面清单 9-11 有着一个在给定文本中找到第一行最后一个字符的函数示例: + +```rust +fn last_char_of_first_line(text: &str) -> Option { + text.lines().next()?.chars().last() +} +``` + +*清单 9-11:在某个 `Option` 的值上使用 `?` 操作符* + +由于可能那里有个字符,不过同样坑能那里没有字符,因此此函数返回的是 `Option`。这个代码取那个 `text` 字符串切片参数,并在其上调用了 `lines` 方法,该方法返回的是对该字符串中那些文本行的一个迭代器。由于此函数是要对首个文本行进行检查,因此他调用了那个迭代器上的 `next`,来从迭代器上获取头一个值。在 `text` 为空字符串时,那么这个到 `next` 的调用,就会返回 `None`,这也就是这里使用 `?` 来停止这个 `last_char_of_first_line` 函数,并自其返回 `None` 的情形。而在 `text` 不为空字符串时,`next` 就会返回一个包含了在 `text` 中第一行文本的字符串切片的 `Some` 值。 + +此时 `?` 操作符会提取这个字符串切片,进而就可以在那个字符串切片上调用 `chars`,来获取到他那些字符的一个迭代器。这里关心的是第一行文本中的最后一个字符,因此就要调用 `last` 来返回迭代器中的最后一个条目。因为首个文本行为空字符串是可能的,比如在 `text` 以空行开头却在其他行上有一些字符,如同在 `"\nhi"` 中一样,因此 `last` 得到一个就是个 `Option` 值。不过在首行上有最后一个字符时,这个字符就会在 `Some` 变种里被返回。中间的 `?` 操作符,给到了一种表达此逻辑的简洁方式,运行在一个行里来实现该函数。若无法在 `Option` 上运用这个 `?` 操作符,那么就必须使用更多方法调用,或 `match` 表达式来实现此逻辑。 + +注意在返回 `Result` 函数中的 `Result` 上,可以使用 `?` 操作符,而在返回 `Option` 函数中的 `Option` 上,可使用 `?` 操作符,但不能混用及进行匹配。`?` 操作符不会自动将 `Result` 转换为 `Option`,或反过来将 `Option` 转换为 `Result`;在这些情况下,是可以在 `Result` 上使用诸如 `ok`,或在 `Option` 上使用 `ok_or` 这样的方法,来显示地完成转换。 + +到目前为止,这里使用过的所有 `main` 函数,返回的都是 `()`。由于 `main` 函数是可执行程序的进入与退出点,因此他是特殊的,而关于其返回值类型可以是什么,为了程序如预期那样执行,是有一些限制的。 + +幸运的是,`main` 函数同样可以返回 `Result<(), E>`。下面清单 9-12 有着来自 9-10 的代码,不过这里将 `main` 函数的返回值类型,改成了 `Result<(), Box>`,并在最后添加了一个返回值 `Ok(())`。现在该代码就会编译了: + + +```rust +use std::error::Error; +use std::fs::File; + +fn main() -> Result<(), Box> { + let greeting_file = File::open("hello.txt")?; + + Ok(()) +} +``` + +*清单 9-12:将 `main` 修改为返回 `Result<(), E>`,就实现了在 `Result` 值上 `?` 操作符的使用* + +这里的 `Box` 类型,是个 *特质对象(trait object)*,在第 17 章中的 [“运用特质对象实现不同类型的值”](Ch17_Object_Oriented_Programming_Features_of_Rust.md#using-trait-objects-that-allow-for-values-of-different-types) 小节,就会讲到这个特性。而现在,可将 `Box` 理解为表示 “任何类别的错误”。由于 `?` 操作符允许将任何 `Err` 值及早返回,因此将 `?` 用在有着错误类型 `Box` 的 `main` 函数中, 某个 `Result` 值上是允许的。即使这个 `main` 函数的函数体,将只会返回类型 `std::io::Error` 的那些错误,而经由指定 `Box`,即使将返回其他错误的代码添加到 `main` 的函数体,该函数签名 `fn main() -> Result<(), Box>` 仍将无误。 + +在 `main` 函数返回了一个 `Result<(), E>` 时,那么若 `main` 返回的是 `Ok(())`,则该可执行程序就会以值 `0` 退出,并在 `main` 返回 `Err` 值时,以非零值退出。C 语言编写的可执行程序,在退出时返回的是些整数:成功退出的程序返回整数 `0`,而出错的程序返回某些非 `0` 的整数。Rust 从可执行程序返回的也是整数,从而与此约定兼容。 + +`main` 函数可能返回任何实现了 [`std::process::Termination` 特质(the `std::process::Termination`)](https://doc.rust-lang.org/std/process/trait.Termination.html) 的任何类型,该特质包含了返回某个 `ExitCode` 的 `report` 函数。请参考标准库文档,了解更多有关实现自己类型 `Termination` 的信息。 + +现在既然已经讨论了调用 `panic!` 或返回 `Result` 的细节,那么就要回到怎样判断,在何种情形下,使用哪种方式属于恰当的话题了。 + + +## 要 `panic!` 还是不要 `panic!` + + +那么该怎样确定,什么时候应该调用 `panic!`,以及什么时候应该返回 `Result` 呢?在代码中止时,就没有办法恢复了。当然可以在任何错误情形下调用 `panic!`,而不管存不存在可能的恢复方式,不过这个时候就是代码编写者本人,代替代码在做出这种情形为不可恢复的确定了。而在选择了返回某个 `Result` 值是,就赋予了调用代码(the calling code)各种选项。调用代码就可以根据其自身情况,而选择尝试恢复,或者他可以决定在此情形下的某个 `Err` 是不可恢复的,进而他就可以调用 `panic!` 而将可恢复错误,转变为不可恢复错误。这样看来,在对某个可能失败的函数进行定义时,返回一个 `Result` 就是良好的默认选择。 + +而在示例程序、原型代码及测试等中,那么比起返回 `Result`,编写程序中止代码就要更合适。接下来就要探讨一下为何这样讲,随后就要讨论一些编译器无法搞清楚,但作为代码编写者的人类却明白程序失败不可能发生的情形。本章将以一些有关在库代码中,如何确定要不要中止程序的守则结束(in situations such as examples, prototype code, and tests, it's more approciate to write code that panics instead of returning a `Result`. Let's explore why, then discuss situations in which the compiler can't tell that failure is impossible, but you as a human can. The chapter will conclude with some general guidelines on how to decide whether to panic in library code)。 + + +### 示例程序、原型代码与测试 + +在编写用于演示某些概念的示例程序时,若同时包含一些健壮的错误处理代码,就会令到示例程序不那么明晰。在示例程序中,到某个诸如 `unwrap` 这样的可能会中止程序运行方法的调用,确信就表明那是一个是要这个应用程序对错误进行处理的占位符,而根据接下来代码所做的事情,这样的调用会有所不同。 + +与此类似,`unwrap` 与 `expect` 方法在构造原型程序,尚未准备好确定如何处理错误时,是十分方便的。这些方法在代码中留下了清楚的一些记号,这些记号在已做好准备让程序更为健壮时会用到。 + +而当方法调用在测试中失败时,即使那个方法并非正在测试的某些功能,也会希望整个测试失败。由于 `panic!` 正是将测试标记为失败的方式,那么调用 `unwrap` 或 `expect`,则正是要用到的了。 + + +### 相比于编译器,代码编写者掌握了更多信息的情形 + +在有着确保了 `Result` 将有着 `Ok` 值的其他某些逻辑,但编译器对此逻辑却一无所知时,调用 `unwrap` 或 `expect` 也是恰当的。这时将仍然有个需要处理的 `Result` 值:对于不论所调用的什么操作,即使在当前特定情形下,逻辑上失败绝无可能,但所调用的操作原本总体上仍是有失败可能。在经由亲自检查代码,而能确保绝不会有 `Err` 变种时,调用 `unwrap` 就是完美可接受的,且将自己设想的绝不会有 `Err` 变种的原因,在 `expect` 文本中撰写出来,这样做甚至更佳。下面就是一个示例: + +```rust +fn main () { + use std::net::IpAddr; + + let home: IpAddr = "127.0.0.1" + .parse() + .expect("硬编码的 IP 地址应是有效的"); +} +``` + +这里是在通过解析硬编码字符串,创建一个 `IpAddr`。可以看到 `127.0.0.1` 是个有效的 IP 地址,因此这里使用 `expect` 是可接受的。然而,有着一个硬编码的、有效的字符串,并未改变 `parse` 方法返回值类型:这里仍将得到一个 `Result` 类型,同时由于编译器不是足够聪明到发现这个字符串总是个有效的 IP 地址,那么编译器仍将要求,以 `Err` 变种是一种可能性那样,对这个 `Result` 进行处理。在这个 IP 地址为来自用户输入,而非这里的硬编码到程序中,进而 *确实* 有着失败可能时,无疑就要打算对这个 `Result` 进行更为健壮的处理了。这种提及这个 IP 地址为硬编码的假设,将提醒到在将来,需要从其他来源获取这个 IP 地址时,就要把 `expect` 修改为更好的错误处理代码。 + + +### 错误处理守则 + +在代码可能以糟糕状态结束运行时,那么让代码中止运行就是明智的。在这种情形下,所谓 *糟糕状态(a bad state)* 就是在某种假设、保证、合约,或恒值已被破坏,譬如在无效值、矛盾值,或缺失值被传递到所编写代码 -- 加上以下的一项或多项: + +- 糟糕状态是某些不期望的东西,他们与偶发的东西相反,比如用户输入的错误格式数据; +- 在此处之后的代码,需要依赖于不处在这种糟糕状态,而不是在接下来的每一步都检查这个问题; +- 没有以自己所使用的类型,来编码该信息的好办法。在第 17 章的 [“将状态于行为编码为类型”](Ch17_Object_Oriented_Programming_Features_of_Rust.md#encoding-states-and-behavior-as-types) 小节,就会贯穿一个这里所意指的示例。 + +在有人调用到咱们的代码,并传入了无意义的值时,在可以的情况下,最好返回一个错误,这样库用户就可以确定在那样的情况下,他们打算做什么。然而在继续执行下去会不安全或有危害的情形中,那么最佳选择就会时调用 `panic!`,并警醒使用到咱们库的人他们代码中的错误,这样在他们开发过程中就可以修好那个代码错误。与此类似,在调用不在掌控中的外部代码,且该外部代码返回了无法修复的无效状态时,那么 `panic!` 通常就是恰当选择。 + +不过在失败为预期的时,那么相比于构造一个 `panic!` 调用,返回一个 `Result` 则更为恰当。这类示例包括给到解析器错误格式数据,或某个返回了表示已达到访问数限制的 HTTP 请求等。在这些情况下,返回一个 `Result` 就表示失败是一种调用代码必须确定如何处理的预期可能。 + +在所编写代码被使用无效值调用,而执行了某种可能将用户置于危险境地的操作时,那么代码就应首先对这些值进行检查,并在这些值无效时中止运行。这主要是处于安全原因:尝试运行于无效数据,就会将代码暴露于漏洞。这就是在尝试超出边界的内存访问时,标准库会调用 `panic!` 的主要原因:尝试访问不属于当前数据结构的内存,是个常见的安全问题。函数通常有着 *合约(contracts)*:只在输入满足特定要求时,他们的行为才有保证。那么由于合约破坏总是表明调用者侧的代码错误,且这种错误并非是要调用代码必须显式处理的那种错误,因此在合约被破坏时的中止运行就说得通了。实际上,调用代码是没有恢复的合理方法的;调用的 *代码编写者* 需要修复该代码。应在函数的 API 文档中,解释函数的合约,尤其是在合约破坏会导致中止运行时。 + +但是,在全部的函数中,进行大量错误检查,则会显得冗长而烦人。幸运的是,可使用 Rust 的类型系统(并因此由编译器完成类型检查),来完成许多的检查。在函数有着作为参数的特定类型时,就可以在知悉编译器已经确保有着有效值的情况下,着手处理代码的业务逻辑。比如,在有着一个不同于 `Option` 的类型时,程序就期望有 *某个东西(something)* 而非 *什么也没有(nothing)*。代码这时就不必处理 `Some` 与 `None` 变种的两种情形:无疑将只有一种有着某个值的情形。尝试将无值传递给该函数的代码,甚至都不会编译,那么该函数就不必在运行时对那样的情况进行检查了。另一个示例则是使用某个诸如 `u32` 无符号整数,这就确保了参数绝不会是个负数。 + +### 创建用于验证的定制类型 + +接下来将这个运用 Rust 的类型系统,来确保有着有效值的概念,进行进一步拓展,而看看创建一个用于验证的定制类型。回顾在第二章中的猜数游戏,其中的代码要求用户猜出一个 `1` 与 `100` 之间的数字。在将用户猜的数字与那里的秘密数字比对之前,是绝无对用户猜数是否处于 `1` 与 `100` 之间,进行过验证的;那里只验证过猜数为正数。在这个示例中,后果并不是非常可怕:这里的输出 “太大了” 或 “太小了” 仍将正确。但引导用户朝向有效的猜数,并在用户猜出不在该范围的数,与用户敲入了比如一些字母时,而有不同的表现,将是一项有用的功能增强。 + +完成此功能增强的一种方式,将是将猜数解析为一个 `i32` 而非仅仅为一个 `u32`,从而允许潜在的负数,并在随后键入一个该数位于范围中的检查,像下面这样: + +```rust + loop { + // --跳过-- + + let guess: i32 = match guess.trim().parse() { + Ok(num) => num, + Err(_) => { println! ("请输入一个数字!"); continue }, + }; + + if guess < 1 || guess > 100 { + println! ("秘密数字将在 1 和 100 之间"); + continue; + } + + match guess.cmp(&secret_number) { + // --跳过-- + } +``` + +其中的 `if` 表达式,对这里的值是否超出范围进行了检查,告诉用户这个问题,并调用 `continue` 来开始下一次循环迭代而请求另一个猜数。在这个 `if` 表达式之后,就可继续进行 `guess` 与秘密数字之间的比较,获悉 `guess` 是在 `1` 与 `100` 之间。 + +然而这并非一种理想的方案:若程序只运行在 `1` 与 `100` 之间的值这一点至关重要,且程序有着许多有此要求的函数,而在每个函数中都进行这样的一个检查,就会显得冗长乏味(并可能影响性能)。 + +相反,这里可以构造一种新类型,并将那些验证放入某个函数,从而创建出该类型的一个示例,而非在各个地方重复这些验证。那样的话,这些函数就可以在他们的签名中,安全地使用这种新类型,并信心十足地使用他们接收到的那些值了。下面清单 9-13 给出了一种定义 `Guess` 类型的方式,在 `new` 函数接收到一个 `1` 与 `100` 之间的值时,这种方式下将只创建一个 `Guess` 的实例。 + +```rust +pub struct Guess { + value: i32, +} + +impl Guess { + pub fn new(value: i32) -> Guess { + if value < 1 || value > 100 { + panic! ("Guess 类型值必须在 1 与 100 之间,收到的是 {}", value); + } + + Guess { value } + } + + pub fn value(&self) -> i32 { + self.value + } +} +``` + +*清单 9-13:只会在值处于 `1` 与 `100` 之间,才继续执行的一个 `Guess` 类型* + + +首先,这里定义了一个名为 `Guess`,带有一个叫做 `value`、保存了一个 `i32` 值字段的结构体。这就是要存储数字的地方。 + +随后这里在 `Guess` 上实现了一个名为 `new` 的关联函数,其创建出一个 `Guess` 类型的实例。这个 `new` 函数被定义为有着一个名为 `value`、类型为 `i32` 的参数,以及要返回一个 `Guess` 类型值。`new` 函数体中的代码,对 `value` 进行了测试,从而确保 `value` 是在 `1` 与 `100` 之间。若 `value` 未通过此测试,那么就做出一个 `panic!` 调用,由于创建一个超出此范围的 `Guess` 会破坏 `Guess::new` 所依赖的合约,因此这就会警醒到编写调用代码的程序员,他们有个需要修复的代码错误。`Guess::new` 可能中止运行的条件,应在其公开的 API 文档中,进行说明。在第 14 章就会涉及到在所创建的文档中,表示 `panic!` 可能性的一些约定。在 `value` 通过该测试时,这里就会创建一个将 `value` 字段设置为那个 `value` 参数的新 `Guess` 类型值,并返回这个 `Guess` 类型值。 + +接下来,这里实现了一个名为 `value`、借用了 `self`,不带任何其他参数,并返回一个 `i32` 的方法。由于这类方法的目的,是要从一些字段获取数据并加以返回,因此有时就被叫做 *取值方法(getter)*。因为 `Guess` 结构体的这个 `value` 字段是私有的,那么这个公开方法就是必要的。这个 `value` 字段作为私有至关重要,这样使用这个 `Guess` 结构体的代码就不被允许直接设置 `value`:该模组外部的代码,*必须* 使用 `Guess::new` 函数,来创建 `Guess` 的实例,这样就确保了 `Guess` 不会有未经 `Guess::new` 函数中条件检查的 `value`。 + +现在某个有着一个 `1` 与 `100` 之间参数,或只返回 `1` 与 `100` 之间数字的函数,就可以在其函数签名中,声明他所取参数或其返回值为 `Guess` 类型而非 `i32` 类型,而不再需要在其函数体中完成任何额外检查了。 + + +## 小结 + +Rust 的那些错误处理特性,被设计用于帮助编写更为健壮的代码。`panic!` 这个宏,发出了程序处于其无法处理状态的信号,并让咱们告知进程停下来,而不是尝试以无效或不正确的一些值继续运行。而 `Result` 这个枚举则使用了 Rust 的类型系统,来表示以代码可以从中恢复过来的某种方式的一些操作失败(the `Result` enum uses Rust's type system to indicate that operations might fail in a way that your code could recover from)。还可使用 `Result` 来告诉调用了咱们代码的代码,需要处理潜在的成功与失败情形。在一些适当情形下,运用 `panic!` 与 `Result` 就会令到咱们的代码在各种不可避免的问题面前,更加可靠。 + +既然这里已经见识到标准库在 `Option` 与 `Result` 枚举上,运用到泛型的一些有用方式,那么接下来就要谈及泛型的原理,以及怎样在咱们的代码中运用泛型。 diff --git a/src/Ch10_Generic_Types_Traits_and_Lifetimes.md b/src/Ch10_Generic_Types_Traits_and_Lifetimes.md new file mode 100644 index 0000000..34dde2f --- /dev/null +++ b/src/Ch10_Generic_Types_Traits_and_Lifetimes.md @@ -0,0 +1,1409 @@ +# 泛型、特质与生命周期 + +每种编程语言都有用于有效处理重复概念的工具。在 Rust 中,一种这样的工具就是 *泛型(generics)*:将一些具体类型或别的属性的替身抽象出来。无需知悉在编译及运行代码时泛型处有着什么,即可对泛型的行为,以及这些泛型与其他泛型之间的关系,进行表达(abstract stand-ins for concret types or other properties. We can express the bevavior of generics or how they relate to other generics without knowing what will be in their place when compiling and running the code)。 + +以与取未知类型值,来在多种具体类型值上运行同样代码的某个函数同样的方式,函数可取某些泛型的参数,而非像是 `i32` 或 `String` 这样的具体类型。事实上,前面在第 6 章的 `Option`,第 8 章的 `Vec` 和 `HashMap`,还有第 9 章的 `Result` 中,就已经用到了泛型特性。本章中,将探讨怎样定义咱们自己的带有泛型特性的类型、函数及方法! + +首先,这里会回顾怎样对函数进行凝练,从而减少代码重复。随后会使用同样技巧,来将两个只是参数类型不同的函数,构造为一个泛型函数。这里还会讲解怎样在结构体与枚举定义中,运用泛型。 + +接着就会掌握怎样使用 *特质(traits)* 来以泛型方式定义动作行为。可将特质与泛型结合,来将某个泛型只接受有着特定行为的那些类型,而不再是任意类型。 + +最后,这里将讨论 *生命周期(lifetimes)*:给到编译器各个引用相互之间联系信息的各种泛型。生命周期特性允许给到编译器,有关那些被借用值的足够信息,这样编译器就可以在相比与未给到这些信息时,能够在更多不同情形下,确保这些引用的有效性。 + + +### 通过提取出函数,而去除重复 + +泛型特性,实现了以占位符表示多种类型方式,而替换掉那些特定类型,从而消除了代码重复。在进入到泛型语法之前,那么就要首先来看看,怎样以不涉及泛型的,而是通过提取出函数,以将一些特定值用一个表示多个值的占位符加以替换,这样的方式消除重复。随后就会把这同样技巧,应用到提取出泛型函数上!经由了解怎样识别出重复代码,就可以提取出函数来,这样亦将发现那些可使用泛型特性的重复代码。 + +这里会以下面清单 10-1 中,找出清单里极大数的简短程序开始。 + +文件名:`src/main.rs` + +```rust +fn main() { + let number_list = vec! [34, 50, 25, 100, 65]; + + let mut largest = &number_list[0]; + + for number in &number_list { + if number > largest { + largest = number; + } + } + + println! ("极大数为 {}", largest); +} +``` + +*清单 10-1:找出某个数字清单中的极大数* + +这里将一个整数清单,存储在了变量 `number_list` 中,并将到该清单中第一个数字的引用,放在一个名为 `largest` 的变量里。这里随后对那个清单中的全部数字进行迭代,并在当前数字大于存储在 `largest` 中的数字时,替换掉那个变量中的引用。然而,在当前数小于或等于至今所见到的极大数时,那个变量就不会改变,同时代码会继续到清单中的下一个数。在对清单中的全部数字进行考察后,`largest` 就应指向那个极大数,在此示例中即为 `100`。 + +而现在接受了找出两个不同数字清单中极大数的任务。为完成这个任务,就可以选择重复清单 10-1 中的代码,并在程序中两个不同位置使用同样的逻辑,如下清单 10-2 中所示。 + +文件名:`src/main.rs` + +```rust +fn main() { + let number_list = vec! [34, 50, 25, 100, 65]; + + let mut largest = &number_list[0]; + + for number in &number_list { + if number > largest { + largest = number; + } + } + + println! ("极大数为 {}", largest); + + let number_list = vec! [102, 34, 6000, 89, 54, 2, 43, 8]; + + let mut largest = &number_list[0]; + + for number in &number_list { + if number > largest { + largest = number; + } + } + + println! ("极大数为 {}", largest); +} +``` + +*清单 10-2:找出 **两个** 数字清单中最大数的代码* + +尽管此代码工作了,但那些重复代码则是乏味且容易出错的。在修改此代码时,还必须记住在多个地方更新代码。 + +这里将通过定义一个运行在某个参数中,所传入的任意整数清单之上的函数,来消除这种重复。此方案会令到这里的代码更清楚,并实现了找出某个清单中极大数这一概念的抽象表达。 + +在下面的清单 10-3 中,就把找出极大数的代码,提取到了一个名为 `largest` 的函数。随后调用该函数来找出了清单 10-2 的两个清单中的极大数。这里还可以在将来可能遇到的任何其他 `i32` 值清单上,运用这个函数。 + +文件名:`src/main.rs` + +```rust +fn largest(list: &[i32]) -> &i32 { + let mut largest = &list[0]; + + for item in list { + if item > largest { + largest = item; + } + } + + largest +} + +fn main() { + let number_list = vec! [34, 50, 25, 100, 65]; + + let result = largest(&number_list); + println! ("极大数为 {}", result); + + let number_list = vec! [102, 34, 6000, 89, 54, 2, 43, 8]; + + let result = largest(&number_list); + println! ("极大数为 {}", result); +} +``` + +*清单 10-3:抽象后的找出两个清单中极大数的代码* + +这个 `largest` 函数有着一个名为 `list` 的参数,该参数表示了任意的、可能传入到该函数的一些 `i32` 值的切片。那么由此而来,在调用该函数时,该代码就会运行在所传入的那些特定值上。 + +总的来说,以下就是将代码从清单 10-2 修改为清单 10-3 所用的步骤: + +1. 识别出重复代码; +2. 将重复代码提取到目标函数的函数体中,并在函数签名中指定重复代码的输入与输出值; +3. 将重复代码的两个实例,更新为调用这个提取出的函数。 + +接下来,就要在泛型下,使用这些同样步骤来降低代码重复了。与函数体可以在抽象的 `list`, 而非具体值上运作的方式一样,泛型实现了代码在抽象类型上的操作。 + +比如,假设说这里有两个函数:一个时在 `i32` 值的切片中,找出极大项,而另一个是在 `char` 值的切片中,找出极大项。那该怎样消除重复呢?下面就来解决这个问题! + + +## 泛型数据类型(Generic Data Types) + +这里要使用泛型特性,来创建诸如函数前面或结构体等的定义,随后就可以用这些定义,用于许多不同的具体数据类型了。首先来看看,怎样运用泛型特性,来定义函数、结构体、枚举及方法等。接下来就会讨论到,泛型特性如何影响到代码性能。 + + +### 函数定义方面 + +在定义使用到泛型特性的函数时,就要把泛型放在原先通常在其中指明参数与返回值数据类型的函数签名中。这样做就会在防止了代码重复的同时,令到代码更为灵活,同时提供到更多功能给该函数的调用者。 + +继续之前的 `largest` 函数,下面清单 10-4 给出了两个均为找出某个切片中极大值的函数。这随后就要将这两个函数,合并为使用泛型特性的单个函数。 + +文件名:`src/main.rs` + + +```rust +fn largest_i32(list: &[i32]) -> &i32 { + let mut largest = &list[0]; + + for item in list { + if item > largest { + largest = item; + } + } + + largest +} + +fn largest_char(list: &[char]) -> &char { + let mut largest = &list[0]; + + for item in list { + if item > largest { + largest = item; + } + } + + largest +} + +fn main() { + let number_list = vec! [34, 50, 25, 100, 65]; + + let result = largest_i32(&number_list); + println! ("极大数为 {}", result); + + let char_list = vec! ['y', 'm', 'a', 'q']; + + let result = largest_char(&char_list); + println! ("极大字符为 {}", result); +} +``` + +*清单 10-4:两个只是名字与签名中类型不同的函数* + +其中的 `largest_i32` 函数,即为在清单 10-3 中所提取出的那个,找出某个切片中最大的 `i32` 函数。而这里的 `largest_char` 函数则是找出某个切片中的极大 `char`。由于这两个函数体有着同样代码,因此这里就要通过在单个函数中,引入泛型参数来消除重复。 + +为将新单一函数中的类型参数化,就需要给类型参数命名,就如同对函数的取值参数所做的那样。可将任意标识符,用作类型参数名字。不过这里会使用 `T`,这是由于根据约定,Rust 中的参数名字都是简短的,同样只有一个字母,且 Rust 的类型命名约定,即为驼峰式大小写命名规则(CamelCase)。而 “type” 的简写 “T”,就是大多数 Rust 程序员的默认选择了。 + +当在函数体中运用某个参数时,就必须在函数签名中声明这个参数,如此编译器就知道那个名字表示什么。与此类型,在函数签名中使用某个类型参数时,就必须在使用该类型参数之前,对这个类型参数进行声明。要定义这个同样的 `largest` 函数,就要那些类型名字声明,放在尖括号(`<>`)内部,位于函数名字与参数列表之间,如下所示: + +```rust +fn largest(list: &) -> &T { +``` + +要将这个定义读作:函数 `largest` 对某些类型 `T` 通用(the function `largest` is generic over some type `T`)。该函数有着一个名为 `list` 的参数,即类型 `T` 值的一个切片。`largest` 函数将返回一个到同样类型 `T` 值的引用。 + +下面清单 10-5 给出了这个在其签名中,运用通用数据类型的合并 `largest` 函数定义。这个清单还展示了怎样使用 `i32` 值切片,或 `char` 值切片,调用该函数。请注意此代码尚不会编译,但本章后面就会修复他。 + +文件名:`src/main.rs` + +```rust +fn largest(list: &[T]) -> &T { + let mut largest = &list[0]; + + for item in list { + if item > largest { + largest = item; + } + } + + largest +} + +fn main() { + let number_list = vec! [34, 50, 25, 100, 65]; + + let result = largest(&number_list); + println! ("极大数为 {}", result); + + let char_list = vec! ['y', 'm', 'a', 'q']; + + let result = largest(&char_list); + println! ("极大字符为 {}", result); +} +``` + +*清单 10-5:使用泛型参数的 `largest` 函数;此代码尚不会编译* + +现在即刻编译此代码,将得到如下错误信息: + +```console +$ cargo run lennyp@vm-manjaro + Compiling generics_demo v0.1.0 (/home/lennyp/rust-lang/generics_demo) +error[E0369]: binary operation `>` cannot be applied to type `&T` + --> src/main.rs:5:17 + | +5 | if item > largest { + | ---- ^ ------- &T + | | + | &T + | +help: consider restricting type parameter `T` + | +1 | fn largest(list: &[T]) -> &T { + | ++++++++++++++++++++++ + +For more information about this error, try `rustc --explain E0369`. +error: could not compile `generics_demo` due to previous error +``` + +该帮助文本消息,提供了 `std::cmp::PartialOrd`,这是一个 *特质(trait)*,而在下一小节,就会讲到特质。至于现在,明白该错误表示,`largest` 函数体,由于 `T` 可以是的那些全部可能类型而不会工作就行。由于这里是要在该函数体中,比较那些类型 `T` 的值,因此这里就只能使用值可被排序的那些类型。而要让这些值的比较可行,标准库就有着这个可应用在类型上的 `std::cmp::PartialOrd` 特质(请参阅附录 C 了解该特质的更多信息)。按照该帮助信息的建议,这里就要将这些类型限制为对 `T` 有效的、仅那些实现了 `PartialOrd` 的类型,而由于标准库在 `i32` 与 `char` 上,均实现了 `PartialOrd` 特质,那么这个示例就会编译了。 + + +### 在结构体定义中 + +这里还可以将结构体,定义为在其一个或多个字段中,运用 `<>` 语法来使用泛型参数。清单 10-6 定义了一个 `Point` 的结构体,来保存任意类型的 `x` 与 `y` 坐标值。 + +文件名:`src/main.rs` + +```rust +struct Point { + x: T, + y: T, +} + +fn main() { + let integer = Point { x: 5, y: 10 }; + let float = Point { x: 1.0, y: 4.0 }; +} +``` + +*清单 10-6:保存类型 `T` 的 `x` 与 `y` 值的 `Point` 结构体* + +在结构体定义中使用泛型特性的语法,与在函数定义中用到的类型。首先,这里在紧接着结构体名字之后的尖括号内部,声明了类型参数名字。随后在结构体定义中,在哪些原本会指明具体数据类型的地方,使用了该泛型。 + +请注意由于这里之用的一个泛型来定义 `Point`,此定义是说,这个 `Point` 结构体对某些类型 `T` 通用,且字段 `x` 与 `y` *均为* 那种同样类型,而不论那种类型可能是何种类型。在创建一个有着不同类型值的 `Point` ,即下面清单 10-7 中那样,那么这里的代码就不会编译。 + +文件名:`src/main.rs` + +```rust +struct Point { + x: T, + y: T, +} + +fn main() { + let wont_work = Point { x: 5, y: 4.0 }; +} +``` + +*清单 10-7:由于字段 `x` 与 `y` 有着同一泛型数据类型 `T`,因此他们必须为同一类型* + +在此示例中,当这里将整数值 `5` 赋值给 `x` 时,那么就让编译器明白了该 `Point` 实例的泛型 `T`,将是个整数。那么随后在将 `4.0` 指定给那个已被定义为与 `x` 有着同一类型的 `y` 时,就会得到一个像下面这样的类型不匹配错误: + +```console +$ cargo run lennyp@vm-manjaro + Compiling generics_demo v0.1.0 (/home/lennyp/rust-lang/generics_demo) +error[E0308]: mismatched types + --> src/main.rs:7:38 + | +7 | let wont_work = Point { x: 5, y: 4.0 }; + | ^^^ expected integer, found floating-point number + +For more information about this error, try `rustc --explain E0308`. +error: could not compile `generics_demo` due to previous error +``` + +要定义一个其中 `x` 与 `y` 同时为泛型,而又可以有着不同类型的 `Point` 结构体,就可以使用多个泛型参数。比如,在下面清单 10-8 中,就将 `Point` 的定义,修改为了对类型 `T` 与 `U` 通用,其中 `x` 为类型 `T`,而 `y` 则是类型 `U`。 + +文件名:`src/main.rs` + +```rust +struct Point { + x: T, + y: U, +} + +fn main() { + let both_integer = Point { x: 5, y: 10 }; + let both_float = Point { x: 1.0, y: 4.0 }; + let integer_and_float = Point { x: 5, y: 4.0 }; +} +``` + +*清单 10-8:对两种类型通用的 `Point`,进而 `x` 与 `y` 可以是不同类型的值* + +现在上面给出的全部 `Point` 实例,都是允许的了!在某个定义中,可以使用想要的泛型参数个数,不过多使用几个就会令到代码难于阅读。在发现代码中需要很多种泛型时,那就可能表示所编写的代码,需要重构为更小的片段了。 + + +### 在枚举的定义中 + +如同对结构体所做的那样,可将枚举定义为在他们的变种中,保存一些通用数据类型。下面就来换个角度看看,标准库提供的那个曾在第 6 章中用到的 `Option`: + +```rust +enum Option { + Some(T), + None, +} +``` + +这个定义,现在应有着更多意涵了。可以看到,这个 `Option` 枚举对类型 `T` 是通用的,并有两个变种:保存着一个类型 `T` 值的 `Some`,与一个不保存任何值的 `None` 变种。通过使用这个 `Option` 枚举,就可以表达可选值的抽象概念,并由于 `Option` 是通用的,因此就可以在无关乎该可选值为何种类型下,使用这种抽象概念。 + +枚举也可以使用多个泛型。在第 9 章中用到的 `Result` 枚举定义,就是一个示例: + +```rust +enum Result { + Ok(T), + Err(E), +} +``` + +这个 `Result` 对两种类型通用,`T` 与 `E`,并有着两个变种:保存了一个类型 `T` 值的 `Ok`,与保存了一个类型 `E` 值的 `Err`。这个定义使得在某个操作可能成功(便返回某种类型 `T` 的一个值),或失败(便返回一个某种类型 `E` 的值)的地方,使用这个 `Result` 枚举方便起来。事实上,这就是在清单 9-3 中打开某个文件时所用到的,在文件被成功打开时,其中的 `T` 就用 `std::fs::File` 填充上了,而在打开那个文件时存在某些问题时,那么其中的 `E` 就以 `std::io::Error` 填充。 + +在意识到代码中有着多个仅在其所保存值类型上,有区别的结构体或枚举,这样的一些情况时,就可以通过使用泛型,而避免代码重复。 + + +### 在方法定义中 + +在结构体与枚举上,可以实现一些方法(正如在第 5 章中所做的那样),并也可以在这些方法的定义中使用泛型。下面清单 10-9 展示了在清单 10-6 中所定义的那个 `Point` 结构体,其上实现了个名为 `x` 的方法。 + + +文件名:`src/main.rs` + +```rust +struct Point { + x: T, + y: U, +} + +impl Point { + fn x(&self) -> &T { + &self.x + } + + fn y(&self) -> &U { + &self.y + } +} + +fn main() { + let p = Point { x: 5, y: 10 }; + + println! ("{}, {}", p.x(), p.y()); +} +``` + +*清单 10-9:在 `Point` 结构体上实现一个将返回到类型 `T` 的 `x` 字段引用的名为 `x` 的方法* + +这里已在 `Point` 上定义了一个名为 `x` 的、返回到字段 `x` 中数据一个引用的方法。通过在 `impl` 后将 `T` 声明为泛型,Rust 就可以识别到 `Point` 中的尖括号(`<>`) 里的类型为一个泛型,而非具体类型。对于这个泛型参数,这里是可以选择一个不同于前面结构体定义中的泛型参数的,但使用同样的名字在这里是惯例性的。在某个声明了泛型的 `impl` 里头编写的那些方法,不论泛型最终将以何种具体类型所代替,这些方法都将被定义在该类型的任意实例上。 + +在将一些方法定义在类型上时,还可以在这些泛型上指明一些约束条件。比如这里就可以只在 `Point`,而不是有着任意泛型的 `Point` 实例上实现方法。在下面清单 10-10 中,就使用了具体类型 `f32`,即指在 `impl` 之后没有声明任何类型。 + +文件名:`src/main.rs` + +```rust +impl Point { + fn distance_from_origin(&self) -> f32 { + (self.x.powi(2) + self.y.powi(2)).sqrt() + } +} +``` + +*清单 10-10:一个只适用于有着对于泛型参数 `` 的某种特定具体类型结构体的 `impl` 代码块* + +此代码表示类型 `Option` 将有着一个 `distance_from_origin` 方法;别的其中 `T, U` 不是 `f32` 的 `Option` 实例,就不会有这个定义的方法。该方法度量了这个点与坐标 `(0.0, 0.0)` 处点的距离,并使用了只对浮点数类型可以的数学运算。 + +结构体定义中的泛型参数,并不总是与在同一结构体方法签名中所用的那些泛型参数相同。下面清单 10-11 就对 `Point` 结构体使用了泛型 `T` 与 `U`,对 `mixup` 方法签名,则使用了 `X Y`,来让这个示例更明显。该方法以来自 `self` `Point` 的 `x` 值(类型为 `T`),与来自传入的 `Point` 值的 `y` (类型为 `Y`),创建了一个新的 `Point`。 + +文件名:`src/main.rs` + + +```rust +#[derive(Debug)] +struct Point { + x: T, + y: U, +} + +impl Point { + fn mixup(self, other: Point) -> Point { + Point { + x: self.x, + y: other.y, + } + } +} + +fn main() { + let p1 = Point { x: 5, y: 10.4 }; + let p2 = Point { x: "Hello", y: 'c' }; + + let p3 = p1.mixup(p2); + + println! ("p3.x = {}, p3.y = {}", p3.x, p3.y); +} +``` + +*清单 10-11:一个使用了与其结构体定义不同泛型的方法* + +在 `main` 函数中,这里已定义了一个有着 `x` 为 `i32` (值为 `5`),及 `y` 为 `f64` (值为 `10.4`)的 `Point`。其中的变量 `p2` 是个有着 `x` 为字符串切片(值为 `Hello`),同时 `y` 为 `char` (值为 `c`)的 `Point` 结构体。以 参数 `p2` 调用 `p1` 上的 `mixup`,给到了 `p3`,由于 `p3` 的 `x` 来自于 `p1`,因此将有一个 `i32` 的 `x`。而由于这个变量 `p3` 的 `y` 来自于 `p2`, 因此他将有一个 `char` 的 `y`。那个 `println!` 宏调用,将打印 `p3.x = 5, p3.y = c`。 + +此示例的目的,是要对其中有些泛型参数是以 `impl` 来声明,而另一些泛型参数则是以方法定义来声明的一种情形,加以演示。由于这里的泛型参数 `T` 与 `U`与结构体定义在一起,因此他们是在 `impl` 之后声明的。而其中的泛型参数 `X` 与 `Y`,由于他们只与那个方法 `mixup` 有关,因此他们就被声明在了 `fn mixup` 之后。 + + +### 使用泛型参数代码的性能问题 + + +这里或许想了解,在运用了泛型参数时,是否有着运行时的开销。好消息就是,相比于使用具体类型,使用泛型并不会令到程序运行得更慢。 + +Rust 通过在编译时,完成那些使用了泛型代码的单态化(performing monomorphization of the code using generics),实现了这一点。所谓 *单态化(monomorphization)*,即通过将在编译后用到的具体类型填入进去,而将通用代码转换为具体代码的过程。在此过程中,编译器会执行与清单 10-5 中曾创建通用函数相反的步骤:编译器会查看所有泛型代码被调用到的地方,并生成调用到泛型代码的那些具体类型的代码。 + +下面就来通过使用标准库的通用 `Option` 枚举,看看单态化的工作原理: + +```rust +let integer = Some(5); +let float = Some(5.0); +``` + +在 Rust 编译此代码时,他就会执行单态化。在那个过程中,编译器会读取已在这两个 `Option` 实例中用到的值,并识别到两种类型的 `Option`:一个为 `i32`,同时另一个为 `f64`。就这样,编译器会将 `Option` 的通用定义,展开为两个专门用于 `i32` 与 `f64` 的定义,由此就用这些特定类型,对通用定义进行了替换。 + +单态化后版本的该代码,看起来与下面的类似(编译器会使用不同于这里为演示目的而使用的名字): + + +文件名:`src/main.rs` + +```rust +enum Option_i32 { + Some(i32), + None, +} + +enum Option_f64 { + Some(f64), + None, +} + +fn main() { + let integer = Option_i32::Some(5); + let float = Option_f64::Some(5.0); +} +``` + +这个通用的 `Option`,就被以编译器创建的具体定义给替换掉了。由于 Rust 将通用代码,编译为指明了各个实例中类型的代码,那么就不会为运用泛型,而付出运行时代价。在运行的时候,代码就只是会与原本早先手写的各个重复定义一样执行。单态化的过程,令到 Rust 的泛型特性,在运行时极为高效。 + + +## 特质:定义共用行为 + +*特质(a trait)*,定义了某种特定类型所具有的,并可与其他类型共用的功能。使用特质,就可以抽象方式,来定义共用行为。而使用 *特质边界(trait bounds)*,就可以指明具有特定行为的任意类型的泛型(we can use *trait bounds* to specify that a generic type can be any type that has certain behavior)。 + + +> **注意**:特质与其他语言中名为 *接口(interfaces)* 的特性类似,不过有着一些不同之处。 + + +### 定义一个特质 + +某个类型的行为,是由那些可在该类型上调用的方法,所组成的。在能够于不同类型之上,调用一些同样方法时,那么这些不同类型,就共用了同样的行为。特质定义,就是为了完成某种目标,而定义一套必要行为,为此而将一些方法签名组织在一起的一种方式(trait definitions are a way to group method signatures together to define a set of behaviors necessary to accomplish some purpose)。 + +比如说,这里有着保存了多种类别与数量文本的多个结构体:一个 `NewsArticle` 结构体,保存着归档于特定位置的新闻故事,而一个 `Tweet` 结构体,则可以有着最多 280 个字符,并带有表明其是否为一则新 tweet、或 retweet,抑或是到另一 tweet 答复的元数据。 + +这里打算构造一个名为 `aggregator` 的媒体聚合器库代码箱,可以显示出可能存储在某个 `NewsArticle` 或 `Tweet` 实例中数据的一些摘要信息来。要完成这个目的,就需要每种类型的摘要,并将通过调用实例上的 `summarize` 方法,请求到摘要信息。下面清单 10-12 给出了表示此行为的一个公共 `Summary` 特质。 + + +文件名:`src/lib.rs` + + +```rust +pub trait Summary { + fn summarize(&self) -> String; +} +``` + +*清单 10-12:由 `summarize` 方法提供的行为所组成的一个 `Summary` 特质* + +这里使用 `trait` 关键字,及接下来该特质的名字,即此示例中的 `Summary`,声明了一个特质。这里同时将该特质声明为了 `pub`,如此就会跟在接下来将看到的几个示例中那样,那些依赖于此代码箱的其他代码箱,也可利用上这个特质。在花括号里面,就要声明对那些实现了此特质的类型行为加以描述的方法签名,在此示例中,那些方法签名即为 `fn summarize(&self) -> String`。 + +在方法签名之后,与提供出花括号里的方法实现不同,这里使用了一个分号。实现此特质的各个类型,都必须提供其自己的定制行为,作为该方法的方法体。编译器会强制要求,有着这个 `Summary` 特质的任意类型,都将有着与这个以该签名所定义的,完全一致的 `summarize` 方法。 + +特质在其代码体中,可以由多个方法:这些方法签名一行一个地列出来,同时每行都已分号结束。 + + +### 在类型上实现某个特质 + +既然前面已经定义了所需的那个 `Summary` 特质的那些方法的签名,那么就可以将其在此处媒体聚合器中的类型上,加以实现了。下面清单 10-13 就给出了这个 `Summary` 特质,在 `NewsArticle` 结构体上,使用标题、作者以及处所字段来创建 `summaryize` 方法返回值的一种实现。而对于 `Tweet` 结构体,这里是将 `summarize` 定义作在假定推文已被限制为 280 字符情况下,返回用户名加上推文的全部文字。 + +文件名:`src/lib.rs` + +```rust +pub struct NewsArticle { + pub headline: String, + pub location: String, + pub author: String, + pub content: String, +} + +impl Summary for NewsArticle { + fn summarize(&self) -> String { + format! ("{}, by {} ({})", self.headline, self.author, self.location) + } +} + +pub struct Tweet { + pub username: String, + pub content: String, + pub reply: bool, + pub retweet: bool, +} + +impl Summary for Tweet { + fn summarize(&self) -> String { + format! ("{}: {}", self.username, self.content) + } +} +``` + +*清单 10-13:在 `NewsArticle` 与 `Tweet` 上对 `Summary` 进行实现* + + +在某个类型上实现一个特质,与实现常规方法类似。不同之处为,在 `impl` 关键字后面,要放置那个打算实现的特质名字,然后要使用 `for` 关键字,并随后指定那个打算为其实现特质的类型名字。在这个 `impl` 代码块内部,就要放入前面特质定义所定义出的那些方法签名。这里不再是在各个签名之后添加分号,而是使用花括号,并将这里打算要特定类型所具有该特质的方法,填充到该方法的方法体中。 + +既然这个库已经在 `NewsArticle` 与 `Tweet` 上实现了那个 `Summary` 特质,那么该库代码箱的用户,就可以调用常规方法的同样方式,调用 `NewsArticle` 与 `Tweet` 实例上的这些特质方法了。唯一区别就是,用户必须将该特质,与相关类型,同时带入到作用域中。下面就是某个二进制代码箱,如何使用这里的 `aggregator` 库代码箱的示例: + + +```rust +use aggregator::{Summary, Tweet}; + +fn main() { + let tweet = Tweet { + username: String::from("horse_ebooks"), + content: String::from( + "当然,跟大家已经清楚的一样了,朋友们", + ), + reply: false, + retweet: false, + }; + + println!("1 条新推文: {}", tweet.summarize()); +} +``` + +此代码会打印 `1 条推文:horse_ebooks: 当然,跟大家已经清楚的一样了,朋友们`。 + +依赖于 `aggregator` 代码箱的其他代码箱,同样可以将 `Summary` 特质带入其作用域,来将 `Summary` 实现在他们自己的类型上。 +要注意有个限制,就是只有在特质或类型属于本地代码箱时,才能将这个特质应用于这个类型上。比如,由于 `Tweet` 这个类型属于本地代码箱 `aggregator`,那么因此就可以将像是标准库的特质 `Display`,应用于像是定制类型 `Tweet` 上,而作为这个 `aggregator` 代码箱功能的一部分。由于那个 `Summary` 属于 `aggregator` 代码箱本地,因此在这里的 `aggregator` 代码箱中,还可将 `Summary` 应用在 `vec` 上。 + + +但这里是无法将外部特质,应用在外部类型上的。比如,由于 `Display` 特质与 `vec` 类型都是定义在标准库中,而均不属于 `aggregator` 代码箱本地,那么在这里的 `aggregator` 代码箱里头,就不能够将 `Display` 特质,应用在 `vec` 上。这种限制属于一种名为 *内聚性(coherrnce)* 属性的一部分,更具体地讲,就是 *孤儿规则(the orphan rule)*,之所以这样称呼,是由于未父类型缺席了(this restriction is part of a property called *coherence*, and more specifically the *orphan rule*, so named because the parent type is not present)。此规则确保其他人的代码无法破坏自己的代码,反之亦然。在没有这条规则下,两个代码箱就会将同一特质,对同一类型加以实现,那么 Rust 就不清楚要使用那个实现了。 + + +### 默认实现 + +在有些时候,给特质中一些或全部方法以默认行为,而非要求在每种类型上实现全部方法,会是有用的做法。随后,在某个特定类型上实现该特质时,就可以保留或重写各个方法的默认行为。 + +下面清单 10-14 就给那个 `Summary` 特质的 `summarize` 方法,指定了一个默认字符串,而非如同在清单 10-12 中所做的,仅定义该方法签名。 + +文件名:`src/lib.rs` + +```rust +pub trait Summary { + fn summarize(&self) -> String { + String::from("了解更多......") + } +} +``` + +*清单 10-14:定义一个带有 `summarize` 方法默认实现的 `Summary` 特质* + +而要使用默认实现来对 `NewsArticle` 的实例进行摘要,就要以 `impl Summary for NewsArticle {}`,指明一个空的 `impl` 代码块。 + +即便这里不再于 `NewsArticle` 类型上,直接定义那个 `summarize` 方法,这里仍已提供到一个默认实现,并已指明了 `NewsArticle` 类型实现了 `Summary` 特质。由此,这里就可以在某个 `NewsArticle` 实例上,调用这个 `summarize` 方法,如同下面这样: + +```rust + let article = NewsArticle { + headline: String::from("企鹅队赢得斯坦利杯锦标赛!"), + location: String::from("美国,宾夕法尼亚州,匹兹堡"), + author: String::from("Iceburgh"), + content: String::from( + "匹兹堡企鹅队再度成为美国曲棍球联盟 \ + NHL 中的最佳球队。" + ), + }; + + println! ("有新文章可读!{}", article.summarize()); +``` + +此代码会打印出 `有新文章可读!了解更多......`。 + +默认实现的创建,不要求对清单 10-13 中在 `Tweet` 上 `Summary` 的实现,做任何的修改。原因就是对某个默认实现进行重写这种语法,与实现某个不具有默认实现的特质方法的语法,是相同的。 + +默认实现可调用同一特质中的其他方法,即使这些别的方法没有默认实现。以这样的方式,特质就可以提供到很多有用功能,而只要求实现者指明特质的一小部分。比如这里就可以将这个 `Summary` 特质,定义为有着一个要求予以实现的 `summarize_author` 方法,并随后定义了一个有着调用了该 `summarize_author` 方法的一个默认实现 `summarize` 方法: + +```rust +pub trait Summary { + fn summarize_author(&self) -> String; + + fn summarize(&self) -> String { + format! ("(了解更多来自 {} ......)", self.summarize_author()) + } +} +``` + +而要使用此版本的 `Summary`,就只需在某个类型上实现该特质时,对 `summarize_author` 加以定义: + +```rust +impl Summary for Tweet { + fn summarize_author(&self) -> String { + format! ("@{}", self.username) + } +} +``` + +在定义了 `summarize_author` 之后,就可以在 `Tweet` 结构体的实例上调用 `summarize` 方法,同时 `summarize` 的默认实现,将调用这里已经提供到的 `summarize_author` 的定义。由于这里已经实现了 `summarize_author`,那么在无需要求咱们编写任何其他代码之下,这个 `Summary` 特质就已给到 `summarize` 方法的行为了。 + +```rust + let tweet = Tweet { + username: String::from("horse_ebooks"), + content: String::from( + "当然,跟大家已经清楚的一样了,朋友们", + ), + reply: false, + retweet: false, + }; + + println!("1 条新推文: {}", tweet.summarize()); +``` + +此代码会打印出 `1 条新推文: (了解更多来自 @horse_ebooks ......)`。 + +请注意从某个方法的重写实现,是无法访问该同一方法的默认实现的。 + + +### 作为参数的特质 + +既然咱们清楚了怎样定义和实现特质,那么就可以探索一下,怎样使用特质来定义接受不同类型参数的函数了。这里将使用之前清单 10-13 中,定义在 `NewsArticle` 与 `Tweet` 上的那个 `Summary` 特质,来定义一个调用其 `item` 参数上 `summarize` 方法的 `notify` 函数,其中 `item` 参数是某个实现了 `Summary` 特质的类型。要完成这一点,就要使用到 `impl Trait` 语法,如下所示: + +```rust +pub fn notify(item: &impl Summary) { + println! ("突发新闻!{}", item.summarize()); +} +``` + +这里给那个 `item` 参数指定了 `impl` 关键字和特质名字,而不再是具体类型。此参数将接受实现了指定特质的任何类型。在 `notify` 的函数体中,就可以调用 `item` 上来此 `Summary` 特质的全部方法,比如这里的 `summarize`。这里便可以对 `notify` 加以调用,并传入任意的 `NewsArticle` 或 `Tweet` 实例了。以任意其他类型,比如 `String` 或 `i32`,由于这些类型没有实现 `Summary`,那么对该函数进行调用的代码,就不会编译。 + + +### 特质边界语法 + +这种适用于简单案例的 `impl Trait` 语法,实际上是一种被称作 *特质边界(a trait bound)* 较长形式的语法糖;而特质边界看起来像下面这样: + + +```rust +pub fn notify(item: &T) { + println! ("突发新闻!{}", item.summarize()); +} +``` + +这种较长形式与上一小节中的示例是等价的,但要更冗长一些。这里是将特质边界(`Summary`),在一个冒号之后,与泛型参数声明放在了一起,并在一对尖括号里面。 + +`impl Trait` 这种语法,在简单情形下,是方便的,且令到代码更为简洁,而在别的情形下,较完整的特质边界语法,则能够对更高复杂度进行表达。比如,这里可以有着两个实现了 `Summary` 的参数。以 `impl Trait` 语法实现这种情况,看起来就像下面这样: + +```rust +pub fn notify(item1: &impl Summary, item2: &impl Summary) { +``` + +在想要此函数容许 `item1` 与 `item2` 有着不同类型(只要这两种类型都实现了 `Summary` 即可)。而在要强制这两个参数有着同一类型时,那么就必须使用特质边界,像下面这样: + +```rust +pub fn notify(item1: &T, item2: &T) { +``` + +这里被指定为 `item1` 与 `item2` 两个参数类型的泛型 `T`,对该函数进行了约束,进而作为传递给 `item1` 与 `item2` 的参数值具体类型,就必须相同了。 + + +### 使用 `+` 语法,指定多个特质边界 + +这里还可以指定多于一个的特质边界。比方说这里打算的是 `notify` 要在 `item` 上使用 `summarize` 方法的同时,还会用到一些显示的格式化:那么就会在 `notify` 的定义中,指明 `item` 必须同时实现了 `Disply` 与 `Summary` 两个特质。使用 `+` 语法,就可以达到这个目的: + +```rust +pub fn notify(item &(impl Summary + Display)) { +``` + +`+` 语法同样对泛型上的特质边界有效: + + +```rust +pub fn notify(item: &T) { +``` + +在制定了这两个特质下,`notify` 的函数体,就可以调用 `summarize` 函数,并可以使用 `{}` 来对 `item` 进行格式化了。 + + +### 运用 `where` 子句获得更明确的特质边界 + +**Clearer Trait Bounds with `where` Clauses** + +过多特质边界的使用,有着其负面性。每个泛型都有其自己的特质边界,因此带有多个泛型参数的函数,就会在函数名字与其参数列表之间,包含很多的特质边界信息,从而令到该函数签名难于阅读。由于这个原因,Rust 就有了在函数签名之后的一个 `where` 子句里头,指定特质边界的这样一种替代性语法。从而与其像下面这样编写函数签名: + +```rust +fn some_function(t: &T, u: &U) -> i32 { +``` + +就可以使用 `where` 子句,写成下面这样: + +```rust +fn some_function(t: &T, u: &U) -> i32 + where T: Display + Clone, + U: Clone + Debug +{ +``` + + +这样的函数签名,就不那么杂乱无章了:函数名、参数列表与返回值类型紧挨在一起,与未带有很多特质边界的某个函数类似。 + + +### 实现了特质的返回值类型 + +在返回某种实现了某个特质的类型值的返回值处,也可以使用 `impl Trait` 语法,如下所示: + + +```rust +fn return_summarizable() -> impl Summary { + Tweet { + username: String::from("horse_ebooks"), + content: String::from( + "当然,如同你或许已经知道的一样,朋友们" + ), + reply: false, + retweet: false, + } +} +``` + +这里通过在返回值类型上使用 `impl Summary`,在没有命名具体类型下,而指明了这个 `returns_summarizable` 函数,会返回某种实现了 `Summary` 特质的类型。在此示例中,`returns_summarizable` 函数返回的是一个 `Tweet`,而调用此函数的代码,则无需知会这一点。 + +仅由返回值类型所实现的特质,指定处返回值类型的这种能力,在闭包与迭代器,在第 13 章就会涉及到这两个特性,的语境下尤为有用。闭包与迭代器创建了仅编译器知晓的一些类型,或一些长度极大而无法指定的类型。这种 `impl Trait` 语法,实现了简明地指定处返回实现了 `Iterator` 特质的某种类型,而无需编写出那非常长的类型。 + +然而,只能在返回单个类型时,才能使用 `impl Trait`。比如,下面有着将返回值类型值指定为了 `impl Summary`,而既要返回 `NewsArticle` 又要返回 `Tweet` 的代码,就不会工作: + +```rust +fn returns_summarizable(switch: bool) -> impl Summary { + if switch { + NewsArticle { + headline: String::from("企鹅队赢得斯坦利杯锦标赛!"), + location: String::from("美国,宾夕法尼亚州,匹兹堡"), + author: String::from("Iceburgh"), + content: String::from( + "匹兹堡企鹅队再度成为美国曲棍球联盟 \ + NHL 中的最佳球队。" + ), + } + } else { + Tweet { + username: String::from("horse_ebooks"), + content: String::from( + "当然,跟大家已经清楚的一样了,朋友们", + ), + reply: false, + retweet: false, + } + } +} +``` + +这里不允许既返回 `NewsArticle` 又返回 `Tweet`,是由于有关这个 `impl Trait` 语法,在编译器中实现方式的限制。在第 17 章的 [运用允许不同类型值的特质对象](Ch17_Object_Oriented_Programming_Features_of_Rust.md#using-trait-objects-that-allow-for-values-of-different-types) 小节,就会讲到怎样编写有着这种行为的函数。 + + +### 运用特质边界来有条件地实现方法 + +经由运用有着一个使用泛型参数的 `impl` 代码块的特质边界,就可以根据实现了指定特质的类型,而实现不同方法(by using a trait bound with an `impl` block that uses generic type parameters, we can implement methods conditionally for types that implement the specified traits)。比如下面清单 10-15 中的类型 `Pair`,就一直会将那个 `new` 函数,实现为返回 `Pair` 的一个新实例(回顾第 5 章的 [定义方法](Ch05_Using_Structs_to_Structure_Related_Data.md#defining-methods) 小节就知道,`Self` 就是那个 `impl` 代码块的类型别名,在这个示例中即为 `Pair`)。但在接下来的 `impl` 代码块中,在 `Pair` 的内部类型 `T` 实现了启用比较的 `PartialOrd` 特质,*与* 启用打印的 `Display` 特质时,那么 `Pair` 就只会实现 `cmp_display` 方法。 + + +```rust +use std::fmt::Display; + +struct Pair { + x: T, + y: T, +} + +impl Pair { + fn new(x: T, y: T) -> Self { + Self { x, y } + } +} + +impl Pair { + fn cmp_display(&self) { + if self.x >= self.y { + println! ("极大数为 x = {}", self.x); + } else { + println! ("极大数为 y = {}", self.y); + } + } +} +``` + +*清单 10-15:根据特质边界,而有条件地实现泛型上的方法(conditionally implementing methods on a generic type depending on trait bounds)* + +> **注意**:这里的 `new` 是个关联函数,而非方法!只能以 `Pair::new` 形式使用。要作为方法使用,函数就必须要有一个 `self` 参数。 + +还可以对任意实现了另一特质的类型,有条件地实现某个特质。在任意满足某些特质边界的类型上,某个特质的实现,被称作 *一揽子实现(blanket implementations)*,这样的做法在 Rust 标准库中有广泛使用。比如,标准库就在实现了 `Display` 特质的全部类型上,实现了 `ToString` 这个特质。标准库中这个 `impl` 代码快,看起来与下面的类似: + +```rust +impl ToString for T { + // --跳过代码-- +} +``` + +由于标准库有着这样的一揽子实现,那么就可以在实现了 `Display` 特质的全部类型上,调用由 `ToString` 特质所定义的 `to_string` 方法。比如,由于正是实现了 `Display` 特质,那么这里就可以像下面这样,把整数转换为他们对应的 `String`: + +```rust +let s = 3.to_string(); +``` + +一揽子实现,会出现在特质文档的 “相关实现器(Implementors)” 部分。 + + +### 小结 + +特质与特质边界这两个特性,允许咱们编写运用了泛型参数的代码,从而在减少代码重复的同时,还向编译器指出希望该泛型有着特定行为。随后编译器就会使用特质边界信息,来就代码所用到的全部具体类型,是否提供到正确行为进行检查。在一般的动态类型语言(dynamically typed languages)中,若调用某个类型上尚未定义的方法,那么就会在运行时收到错误。但 Rust 将这些错误,移到了编译时,这样就要在代码还不能运行的时候,就被强制要求修复这些问题。此外,由于已在编译时检查过这些问题,那么就不必编写对运行时行为进行检查的代码了。在不必放弃泛型灵活性之下,就提升了程序性能。 + + +## 使用生命周期对引用加以验证 + +生命周期是另一种前面已经用到的泛型。与确保某种类型有着期望行为的特质不同,生命周期确保的是引用在需要他们有效期间,保持有效(lifetimes ensure that references are valid as long as we need them to be)。 + +在第 4 章中的 [引用与借用](Ch04_Understanding_Ownership.md#references-and-borrowing) 小节,未曾讨论到的一个细节,就是在 Rust 中的全部引用都有着 *生命周期(lifetime)*,即引用有效的作用范围。多数时候,声明周期都是隐式的,且是被推导出来的,这正与多数时候类型是被推导出来的一样。在可能有多个类型时,仅务必对类型加以注解即可。与这种注解类型的方式类似,在引用的生命周期与少数几种不同方式相关时,就必须对生命周期加以注解。为确保在运行时用到的具体类型显著有效,Rust 就要求使用泛型生命周期参数,对这些关系加以注解(in a similar way, we must annotate lifetimes when the lifetimes of references could be related in a few different ways. Rust requires us to annotate the relationships using generic lifetime parameters to ensure the actual references used at runtime will definitely be valid)。 + +绝大多数别的编程语言,甚至都没有声明周期注解这个概念,那么这就会感觉陌生了。尽管本章不会涵盖生命周期的全部,这里仍会对可能遇到的生命周期语法的一些常见方式进行讨论,如此就会对此概念感到不那么违和。 + + +### 使用生命周期防止悬空引用 + +**Preventing Dangling References with Lifetimes** + +生命周期的主要目标,就是防止 *悬空引用(dangling references)*,这会导致程序引用到并非其打算要引用的数据。设想下面清单 10-16 中的程序,其有着一个外层作用范围与一个内层作用范围。 + + +```rust +fn main() { + let r; + + { + let x = 5; + r = &x; + } + + println! {"r: {}", r}; +} +``` + +*清单 10-16:一个使用了值已超出作用域引用的尝试* + +> 注意:清单 10-16、10-17 及 10-23 中的示例,都不带初始值地声明了一些变量,那么变量名就存在于外层作用域中。咋一看,这样做似乎与 Rust 的无空值(Rust's having no `null` values)特性相抵触。不过,在尝试于赋予变量值之前使用某个变量,就会得到一个编译器时错误,这样就表示 Rust 实际上是不允许空值(`null` values) 的。 + + +外层作用域声明了一个名为 `r` 不带初始值的变量,同时内层作用域声明了一个名为 `x` 带有初始值 `5` 的变量。在那个内层作用域里头,这里尝试了将 `r` 的值设置为到 `x` 的一个引用。随后那个内层作用域便结束了,而这里尝试打印 `r` 中的值。由于 `r` 所指向的值,在这里尝试使用之前就已超出作用域,因此此代码不会编译。下面就是错误消息: + +```console +$ cargo run lennyp@vm-manjaro + Compiling lifetimes_demo v0.1.0 (/home/lennyp/rust-lang/lifetimes_demo) +error[E0597]: `x` does not live long enough + --> src/main.rs:6:13 + | +6 | r = &x; + | ^^ borrowed value does not live long enough +7 | } + | - `x` dropped here while still borrowed +8 | +9 | println! {"r: {}", r}; + | - borrow later used here + +For more information about this error, try `rustc --explain E0597`. +error: could not compile `lifetimes_demo` due to previous error +``` + +变量 `x` 未 “存活足够长时间。” 原因就是在内层作用域于第 7 行结束处,变量 `x` 便超出了作用域。然而对于外层作用域,变量 `r` 仍是有效的;有望变量 `r` 的作用域要大一些,这里就讲变量 `x` 就要 “存活得长一些”。若 Rust 允许此代码工作,那么变量 `r` 就会引用到在变量 `x` 超出作用域时,已被解除分配的内存,并且任何尝试在变量 `x` 下的操作,都将不会正确工作。那么 Rust 是怎样判定此代码无效的呢?他运用了一种借用检查器。 + + +### 借用检查器 + +Rust 编译器有个对作用域加以比较,而确定出全部借用是否有效的 *借用检查器(a borrow checker)*。下面清单 10-17 就给出了与清单 10-16 同样的代码,不过有着显示其中变量生命周期的注解。 + +```rust +fn main() { + let r; // ---------+-- 'a + // | + { // | + let x = 5; // -+-- 'b | + r = &x; // | | + } // -+ | + // | + println!("r: {}", r); // | +} // ---------+ +``` + +*清单 10-17:变量 `r` 与 `x` 生命周期的注解,各自取名为 `'a` 与 `'b`* + +这里已使用 `'a` 与 `'b` 分别注解了变量 `r` 与 `x` 的生命周期。如同这里所看到的,内层的 `'b` 代码块,相比外层的 `'a` 声明周期代码块要小得多。在编译时,Rust 就会比较这两个生命周期的大小,并发现变量 `r` 有着 `'a` 的生命周期,但他却指向了一个 `'b` 的生命周期。由于生命周期 `'b` 比 `'a` 要短,于是该程序就被拒绝:引用物(the subject of the reference)未与引用变量,存活同样时间。 + +下面清单 10-18 修复了该代码,因此他就没有悬空引用,进而就无误地编译了。 + + +```rust +fn main() { + let x = 5; // ----------+-- 'b + // | + let r = &x; // --+-- 'a | + // | | + println!("r: {}", r); // | | + // --+ | +} // ----------+ +``` + +*清单 10-18:由于被引用数据有着长于引用变量的生命周期,因此这是一个有效的引用* + + +这里变量 `x` 有着生命周期 `'b`,在此示例中是长于声明周期 `'a` 的。这就意味着由于 Rust 清楚在变量 `r` 中的引用,在变量 `x` 有效期间,将始终有效,因此变量 `r` 就可以对变量 `x` 加以引用。 + +既然咱们已经清楚引用的生命周期都在何处,以及 Rust 怎样对生命周期加以分析,来确保引用将始终有效,那么接下来就要探讨,函数语境下的参数与返回值的泛型生命周期了(generic lifetimes of parameters and return values in the context of functions)。 + + +### 函数中的泛型生命周期 + +**Generic Lifetimes in Functions** + + +下面将编写一个返回两个字符串切片中较长者的函数。该函数将取两个字符串切片,并返回单个的字符串切片。在实现了这个 `longest` 函数后,清单 10-19 中的代码,就会打印 `最长的字符串为 abcd`。 + + +文件名:`src/main.rs` + + +```rust +fn main() { + let string1 = String::from("abcd"); + let string2 = "xyz"; + + let result = longest(string1.as_str(), string2); + println! ("最长的字符串为 {}", result); +} +``` + +*清单 10-19:调用 `longest` 函数来找出两个字符串切片中较长的那个的 `main` 函数* + + +留意到由于这里并不打算这个 `longest` 函数取得其参数的所有权,因此这里是要该函数取两个字符串切片,两个都是引用变量,而非字符串。请参考第 4 章中的 [作为函数参数的字符串切片](Ch04_Understanding_Ownership.md#string-slices-as-parameters) 小节,了解更多为何在清单 10-19 中使用的参数,即为这里想要的参数的讨论。 + +在尝试如下面清单 10-20 中所示的那样,对这个 `longest` 函数加以实现时,那将仍不会编译。 + + +文件名:`src/main.rs` + +```rust +fn longest(x: &str, y: &str) -> &str { + if x.len() > y.len() { + x + } else { y } +} +``` + +*清单 10-20:一种尚不会编译的返回两个字符串切片中较长者的 `longest` 函数实现* + + +相反,这里会得到以下的谈及生命周期的错误: + + +```console +$ cargo run lennyp@vm-manjaro + Compiling lifetimes_demo v0.1.0 (/home/lennyp/rust-lang/lifetimes_demo) +error[E0106]: missing lifetime specifier + --> src/main.rs:1:33 + | +1 | fn longest(x: &str, y: &str) -> &str { + | ---- ---- ^ expected named lifetime parameter + | + = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y` +help: consider introducing a named lifetime parameter + | +1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { + | ++++ ++ ++ ++ + +For more information about this error, try `rustc --explain E0106`. +error: could not compile `lifetimes_demo` due to previous error +``` + +该帮助性文字,揭示了由于 Rust 无法弄清返回的引用到底是指向 `x` 还是 `y`,因此返回值类型就需要其上的泛型声明周期函数(a generic lifetime parameter)。事实上,由于在该函数的函数体中,那个 `if` 代码块返回的时到参数 `x` 的引用,而其中的 `else` 代码块返回的是到 `y` 的引用,所以就连咱们也不清楚! + +在对该函数进行定义时,是不清楚要传入到该函数的那些具体值的,因此就不清楚究竟是`if` 条件,还是 `else` 条件会被执行。这里也不清楚要传入的那些引用变量的具体声明周期,进而就无法查看如同清单 10-17 及 10-18 中所看到的那些作用域,来判断返回的引用变量是否始终有效。由于 Rust 的借用检查器不清楚其中 `x` 与 `y` 的生命周期,与返回值的生命周期有怎样的关联,因此借用检查器也无法对此做出判断。要修复这个错误,就要添加对这些引用变量之间关系进行定义的泛型生命周期参数,进而借用检查器就可以完成他的分析。 + + +### 生命周期注解语法 + +**Lifetime Annotation Syntax** + +生命周期注解,一点也不会改变引用变量的存活时长。而是在不影响生命周期指向,对多个引用变量生命周期之间的关系加以描述。正如函数签名指定了泛型参数时,函数可接受任意类型一样,通过在函数签名中指定泛型生命周期参数,函数就可以接受任意生命周期的引用了(just as functions can accept any type when the signatures specifies a generic type parameter, functions can accept with any lifetime by specifying a generic lifetime parameter)。 + +生命周期注解有着些许不同寻常的语法:生命周期参数的名字,必须以撇号(单引号,`'`)开头,通常为全部小写字母,且像泛型一样非常短。多数人会用 `'a` 作为首个生命周期的注解。是将生命周期注解,放在某个引用的 `&` 之后,使用一个空格,来将注解与该引用的类型分隔开。 + +下面就是一些示例:到某个 `i32` 的不带生命周期参数的引用、到某个 `i32` 的有着名为 `'a` 的生命周期参数,以及到某个 `i32` 的同样有着生命周期 `'a` 的可变引用。 + +```rust +&i32 // 某个引用 +&'a i32 // 某个带有显式生命周期的引用 +&'a mut i32 // 某个有着显式生命周期的可变引用 +``` + +由于注解的目的是告诉 Rust (编译器),多个引用的泛型生命周期参数相互之间是怎样关联起来的,因此生命周期本身并没有什么意义。接下来就要在那个 `largest` 函数语境下,对生命周期注解相互之间怎样联系起来加以审视了。 + + +### 函数签名中的生命周期注解 + +要在函数签名中使用生命周期注解,就需要在函数名字与参数列表之间,在尖括号里头对通用 *生命周期(lifetime)* 参数加以声明,正如之前对通用 *类型(type)* 参数所做的那样。 + +这里是要该函数签名,表达以下约束:返回的那个引用,将在这两个参数均为有效时,保持有效。这便是参数与返回值生命周期之间的关系。这里将把该生命周期命名为 `'a`,并在随后将其添加到各个引用,如下清单 10-21 中所示。 + +文件名:`src/main.rs` + +```rust +fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { + if x.len() > y.len() { + x + } else { + y + } +} +``` + +*清单 10-21:指明了签名中全部引用都必须有着同一生命周期 `'a` 的 `longest` 函数* + + +此代码应会编译,并在清单 10-19 的 `main` 函数中使用他时,产生出这里想要的结果来。 + +该函数签名现在会告诉 Rust,对于生命周期 `'a`,该函数会取两个参数,这两个参数都是存活时间至少为 `'a` 的两个字符串切片。该函数签名还会告诉 Rust,从该函数返回的字符串切片,将存活至少生命周期 `'a` 那么长时间。在实践中,这就表示有这个 `longest` 函数返回的引用的生命周期,与该函数参数所引用到的值生命周期中较小的一致。这些关系,就是这里想要 Rust 在分析此代码时,要用到的关系。 + +当于函数中对生命周期进行注解时,这些注解是介入函数签名中,而非函数体中。这些生命周期注解,成为了该函数合约的一部分,这与签名中的类型较为相似。令到函数包含生命周期合约(the lifetime contract),就意味着 Rust 编译器所完成的分析,可以相对简单一些。在某个函数被注解的方式,或其被调用的方式存在问题时,所报出的编译器错误,就可以更精准地指向所编写代码或约束的某个部分。相反,相比于添加了生命周期注解,在 Rust 编译器要做出更多有关这里所预期生命周期关系的推断时,那么编译器可能就只能够指出,在问题原因处许多步之外,代码的某个使用了(if, instead, the Rust compiler made more inferences about what we intended the relationships of the lifetimes to be, the compiler might only be able to point to a use of our code many steps away from the cause of the problem)。 + +在将具体引用传递给 `longest` 时,取代 `'a` 的那个具体生命周期,即为参数 `x` 作用域与参数 `y` 作用域重叠的部分。也就是说,这个泛型生命周期 `'a` 将获取到,与 `x` 与 `y` 生命周期中较小那个相等的具体生命周期。由于这里已将那个返回的引用,注解为了同一生命周期参数 `'a`,那么那个返回的引用,就会在 `x` 与 `y` 的生命周期中较小那个的长度期间有效。 + +下面就来通过传入有着不同具体生命周期的引用,而看看这些生命周期注解,是怎样对这个 `longest` 函数加以限制的。下面清单 10-22 就是一个直观的示例。 + + +文件名:`src/main.rs` + +```rust +fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { + if x.len() > y.len() { + x + } else { + y + } +} + +fn main() { + let string1 = String::from("长字符串就是长"); + + { + let string2 = String::from("xyz"); + let result = longest(string1.as_str(), string2.as_str()); + println! ("最长的字符串为 {}", result); + } +} +``` + +*清单 10-22:在到有着不同具体生命周期 `String` 类型值的引用下,运用这个 `longest` 函数* + + +在此示例中,到外层作用域结束之前,`string1` 都是有效的,而 `string2` 则是到内层作用域结束之前为有效,同时 `result` 引用了某个在内层作用域结束之前有效的东西。运行此代码,就会看到借用检查器通过了检查;此代码将编译并打印 `最长的字符串为 长字符串就是长`。 + +接下来,就要尝试一个展示 `result` 中引用的生命周期,必须为这两个参数生命周期中较小的那个的示例。这里将把那个 `result` 变量的声明,移到内层作用域外面而将到该 `result` 变量的赋值,仍然留在有着 `string2` 变量的作用域里头。随后将把那个用到 `result` 变量的 `println!` 语句,移出到内层作用域外面,在内层作用域结束之后。下面清单 10-23 中的代码就不会编译了。 + + +文件名:`src/main.rs` + + +```rust +fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { + if x.len() > y.len() { + x + } else { + y + } +} + +fn main() { + let string1 = String::from("长字符串就是长"); + let result; + + { + let string2 = String::from("xyz"); + result = longest(string1.as_str(), string2.as_str()); + } + println! ("最长的字符串为 {}", result); +} +``` + +*清单 10-23:尝试在 `string2` 已超出作用域之后对 `result` 加以使用* + + +在尝试编译此代码时,就会得到这样的错误: + + +```console +$ cargo run lennyp@vm-manjaro + Compiling lifetimes_demo v0.1.0 (/home/lennyp/rust-lang/lifetimes_demo) +error[E0597]: `string2` does not live long enough + --> src/main.rs:15:44 + | +15 | result = longest(string1.as_str(), string2.as_str()); + | ^^^^^^^^^^^^^^^^ borrowed value does not live long enough +16 | } + | - `string2` dropped here while still borrowed +17 | println! ("最长的字符串为 {}", result); + | ------ borrow later used here + +For more information about this error, try `rustc --explain E0597`. +error: could not compile `lifetimes_demo` due to previous error +``` + +该错误显示,要让 `result` 对那个 `println!` 语句有效,那么 `string2` 就需要在外层作用域结束之前,保持有效。Rust (编译器)之所以清楚这点,是由于这里使用了同样的生命周期参数 `'a`,对该函数的各个参数与返回值进行了注解。 + +而作为人类,那么就可以看看这段代码,并发现 `string1` 相较于 `string2` 的生命周期要长,进而由此 `result` 就会包含一个到 `string1` 的引用。由于 `string1` 尚未超出作用域,那么到 `string1` 的某个引用,相对 `println!` 语句仍将有效。然而编译器却无法在此示例中,发现该引用是有效的。这里已告知 Rust,有这个 `longest` 函数所返回引用的生命周期,与所传入的那些参数声明周期中较小者相同。那么,由于代码中可能有着无效的引用,故借用检查器是不允许清单 10-23 中代码的。 + + +请尝试设计更多传入到 `longest` 函数不同值与引用生命周期,及返回引用变量使用方式不同的试验。并在编译这些试验代码前,就这些试验是否会通过借用检查器的检查,做出一些假定;随后在看看所做出的假定是否正确! + + +### 从生命周期角度进行思考 + +**Thinking in Terms of Lifetimes** + + +指定生命周期参数的所需方式,取决于函数是在干什么事情。比如在将 `longest` 函数的实现,修改为了始终返回第一个参数,而非那个最长的字符串切片时,那么就不需要在其中的 `y` 参数上,指定生命周期了。以下代码将会编译: + +文件名:`src/main.rs` + +```rust +fn longest<'a>(x: &'a str, y: &str) -> &'a str { + x +} +``` + + +这里已将生命周期参数 `'a` 指定给了参数 `x` 与返回值类型,而由于参数 `y` 的生命周期,与 `x` 或返回值的生命周期,并无任何关系,故这里并未将 `'a` 指定给参数 `y`。 + +当从某个函数返回一个引用时,返回值类型的生命周期参数,就需要与某个参数的生命周期参数相匹配。而在返回的引用,*未* 指向某个参数时,那么他就必须指向在该函数内部创建的某个值。然而,由于这个函数内部创建的值,在函数结束处将超出作用域,因此这就会是个悬空引用了。请设想下面这个不会编译的尝试性 `longest` 函数实现: + +文件名:`src/main.rs` + +```rust +fn longest<'a>(x: &str, y: &str) -> &'a str { + let result = String::from("真正长的字符串"); + result.as_str() +} +``` + +这里尽管已给那个返回类型指定了生命周期参数 `'a`,但由于其中的返回值生命周期,与那些参数的生命周期毫无关系,故该实现将编译失败。下面就是得到的错误消息: + +```console +$ cargo run lennyp@vm-manjaro + Compiling lifetimes_demo v0.1.0 (/home/lennyp/rust-lang/lifetimes_demo) +error[E0515]: cannot return reference to local variable `result` + --> src/main.rs:11:5 + | +11 | result.as_str() + | ^^^^^^^^^^^^^^^ returns a reference to data owned by the current function + +For more information about this error, try `rustc --explain E0515`. +warning: `lifetimes_demo` (bin "lifetimes_demo") generated 2 warnings +error: could not compile `lifetimes_demo` due to previous error; 2 warnings emitted +``` + +问题就是在 `longest` 函数结束处,`result` 就超出了作用域而被清理掉了。这里还在尝试从该函数返回到 `result` 的一个引用。并不存在能够指定一个会改变这个悬空引用的生命周期参数的办法,而 Rust 也不会容许创建悬空引用。在此示例中,最佳修复将是返回一个有着所有权的数据类型,而非某个引用(注:故引用是没有所有权的),进而随后由调用函数(the calling function),来负责清理该值。 + +最后,生命周期语法,是有关将函数各个参数的生命周期,与函数返回值的生命周期连接起来的。一旦他们连接了起来,那么 Rust 就有了足够信息,来放行一些涉及内存安全的操作,以及蓝星那些会创建出悬空指针或其他危及内存安全的操作。 + + +### 结构体定义中的生命周期注解 + +**Lifetime Annotations in Struct Definitions** + + +到目前为止,本书中业已定义的那些结构体,都持有自己的一些类型。这里可以将结构体定义为持有一些引用,但这样的话,就需要在结构体定义中每个引用上,添加生命周期注解了。下面清单 10-24 有着一个名为 `ImportedExcerpt`、保存着一个字符串切片的结构体。 + +文件名:`src/main.rs` + +```rust +struct ImportantExcerpt<'a> { + part: &'a str, +} + +fn main() { + let novel = String::from("请叫我伊萨梅尔。多年以前....."); + let first_sentence = novel.split('。').next().expect("找不到一个 '。'"); + let i = ImportantExcerpt { + part: first_sentence, + }; +} +``` + +*清单 10-24:保存了一个引用的结构体,因此就需要生命周期注解* + +此结构体有着单个的、保存着一个字符串切片,因此是个引用变量的字段 `part`。与通用数据类型(generic data types)之下一样,这里在结构他名字后面,尖括号里头,对其中的通用声明周期参赛进行了声明,进而就可以在这个结构体定义代码体中,使用那个生命周期参数。这样的注解表示,`ImportantExcerpt` 的实例,无法存活过超出保存在其 `part` 字段中引用的生命早期。 + +这里的 `main` 函数创建了该 `ImportantExcerpt` 的一个实例,该实例保存了到其中由变量 `novel` 所拥有所有权的 `String` 第一句话的引用。`novel` 中的数据,在该 `ImportantExcerpt` 实例被创建之前,便存在了。此外,`novel` 在这个 `ImportantExcerpt` 超出作用域之前,并未超出作用域,因此在这个 `ImportantExcerpt` 实例中的引用,将有效。 + + +### 生命周期的省略 + +**Lifetime Elision** + +现在已经了解到每个引用都有生命周期,以及需要给使用到引用的函数与结构体,指明生命周期参数。不过,在第 4 章中,曾有一个清单 4-9 中的函数,这里再次将其展示在下面清单 10-25 中,这是个不带生命周期注解就被编译的函数。 + + +文件名:`src/main.rs` + +```rust +fn first_word(s: &String) -> &str { + let bytes = s.as_bytes(); + + for (i, &item) in bytes.iter().enumerate() { + if item == b' ' { + return &s[..i]; + } + } + + &s[..] +} +``` + +*清单 10-25:在清单 4-9 中曾定义的一个不带生命周期注解即被编译的函数,即使其参数与返回值均为引用变量* + + +此函数不带生命周期注解即会编译的原因,是历史遗留的:在 Rust 早期版本(`pre-1.0`)中,由于每个引用都需要显式生命周期,因此该代码就不会编译。那个时候,该函数签名就会被写成下面这样: + +```rust +fn first_word<'a>(s: &'a str) -> &'a str { +``` + +在编写了许多的 Rust 代码之后,Rust 团队发现,Rust 程序员们在某些特定情形下,会一次又一次地敲入许多同样的生命周期注解。而这些特定情形,是可被预测的,并遵循了少数几种确定性的模式(a few deterministic patterns)。Rust 开发者们于是就将这些模式,编程进了编译器的代码,于是借用检查器,就可以推断出这些情形下的生命周期,而无需显式的注解了。 + +由于将来可能合并更多确定性的模式,并将这些模式添加到编译器,因此 Rust 的这段历史是有联系的。在将来,或许就只要求更少甚至没有生命周期注解了。 + +编程到 Rust 的引用分析中的那些确定性模式,被称为 *生命周期省略规则(lifetime elision rules)*。这些规则并非 Rust 程序员要遵循的规则;他们是一套编译器要考虑的特殊情形,并在咱们编写的代码符合这些情形时,就无需显式地写出生命周期(注解)。 + +这些省略规则,并不提供完全的推断。在 Rust 明确地应用了这些规则,但仍有着哪些引用有何种生命周期方面的模糊性时,那么编译器是不会就其余引用变量应有何种生命周期,加以猜测的。编译器将给到某个可通过添加生命周期注解,而予以消除的错误消息,而非对模糊的引用生命周期胡乱猜测。 + +在函数或方法参数上的生命周期,被称为 *输入生命周期(input lifetimes)*,而在返回值上的生命周期,则被称为 *输出生命周期(output lifetimes)*。 + +在没有显式的生命周期注解时,编译器会用到三条规则,来计算出那些引用的生命周期。首条规则应用于输入生命周期,而第二及第三条规则,则是都应用于输出生命周期。在编译器抵达这三条规则的结尾处,而仍有其未能计算出生命周期的引用时,那么编译器就会以某个错误消息而停止。这三条规则适用于 `fn` 定义,对于 `impl` 代码块也一样适用。 + +首条规则即为,编译器后给那些是引用的各个参数,分别指派一个生命周期参数。也就是说,带有一个参数的函数,就会获得一个生命周期参数:`fn foo<'a>(x: &'a i32)`;而有着两个参数的函数,就会得到两个单独的生命周期参数:`fn foo<'a, 'b>(x: &'a i32, &'b i32)`;如此等等。 + +而第二条规则,则是在确切地只有一个输入生命周期参数时,那个生命周期,就被指派给全部的输出生命周期参数:`fn foo<'a>(x: &'a i32) -> &'a i32`。 + +第三条规则,在有多个输入生命周期参数,但由于这是个方法,而其中之一为 `&self` 或 `&mut self` 时,那么 `self` 的生命周期就会被指派给全部输出生命周期参数。由于只有较少必要符号,因此这第三条规令到方法的阅读与编写体验更佳。 + +下面咱们就来扮着编译器。这里将应用这些规则,来计算出清单 10-25 中,那个 `first_word` 函数签名里各个引用的生命周期。该函数签名以不带任何与其中引用关联的生命周期开始: + +```rust +fn first_word(s: &str) -> &str { +``` + +随后编译器就应用首条规则,这会指定各个参数获取到其各自的生命周期。这里将和平时一样,将该生命周期叫做 `'a`,那么现在该函数签名就是这样的: + +```rust +fn first_word<'a>(s: &'a str) -> &str { +``` + +由于这里确切地是一个输入生命周期,那么第二条规则就应用了。第二条规则指出,这个输入参数的生命周期,会被指派给输出生命周期,那么现在这个函数签名就是这样的: + +```rust +fn first_word<'a>(s: &'a str) -> &'a str { +``` + +现在这个函数签名中的全部引用,都有了生命周期,进而编译器就可以在无需程序员对此函数签名中的生命周期进行注解的情况下,而继续其分析了。 + +接下来就要看看另一个示例,这次要使用在清单 10-20 中,一开始编写时没有生命周期参数的那个 `longest` 函数: + +```rust +fn longest(x: &str, y: &str) -> &str { +``` + +首先来应用第一条规则:各个参数都得到其自己的生命周期。这次有两个而非一个参数,那么这里就有两个生命周期: + +```rust +fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str { +``` + +可以看到,由于这里有多于一个的输入生命周期,因此第二条规则并不使用了。至于第三条规则,由于 `longest` 是个函数而非方法,那么这些参数中没有一个是 `self`,因此第三条规则也不适用。在历经了全部三条规则后,这里仍未计算出返回值类型的生命周期为何物。这就是为何在尝试编译清单 10-20 中代码时收到错误的原因:编译器历经这些生命周期省略规则,而仍未计算出该函数签名中引用的全部生命周期。 + +由于这第三条规则,真的只适用于方法签名中,这里接下来就要看看,在方法语境中的生命周期,从而发现这第三条规则,就意味着不必甚为频繁地在方法签名中对生命周期进行注解。 + + +### 方法定义中的生命周期注解 + +前面在有着生命周期的结构体上实现方法时,就使用了与清单 10-11 中泛型参数同样的语法。其中根据生命周期是否与结构体字段,或与方法的参数及返回值相关,而声明并使用到生命周期参数。 + +由于这些生命周期为结构体类型的部分,因此结构体字段的生命周期名字,总是要声明在 `impl` 关键字之后,并随后要在结构体名字之后使用。 + +在 `impl` 代码块里头的方法签名中,这些引用可能会被捆绑到结构体字段中引用的生命周期,或这些引用也可能是独立的。此外,生命周期省略规则通常会发挥作用,从而在方法签名中,生命周期注解就不是必要的了。下面就来看看一些使用了清单 10-24 中曾定义的名为 `ImportantExcerpt` 结构体的示例。 + +首先,这里将使用一个名为 `level`、其唯一参数是个到 `self` 引用,且返回值是个 `i32` 、不是到任何变量引用的方法: + +```rust +impl<'a> ImportantExcerpt<'a> { + fn level(&self) -> i32 { + 3 + } +} +``` + + +在 `impl` 之后的生命周期参数声明,及在类型名字之后其的使用,都是必须的,但由于那首条省略规则,这里就未要求对到 `self` 引用的生命周期进行注解。 + +下面是个其中适用第三条生命周期省略规则的示例: + +```rust +impl<'a> ImportantExcerpt<'a> { + fn announce_and_return_part(&self, announcement: &str) -> &str { + println! ("请注意:{}", announcement); + self.part + } +} +``` + +这里有两个输入生命周期,那么 Rust 就会适用首条生命周期省略规则,并赋予到 `&self` 与 `announcement` 其各自的生命周期。随后由于其中一个参数是 `&self`,那么返回值类型就会得到 `&self` 的生命周期,进而全部生命周期就都得到了计算。 + + +### 静态生命周期 + +这里需要讨论的一种特俗生命周期,那就是 `'static`,这表示了受其影响的引用,*可以* 存活到程序整个持续时间。所有字符串字面值,都有着 `'static` 的生命周期,可将这种特性注解为下面这样: + +```rust +let s: &'static str = "我有静态的生命周期。"; +``` + +此字符串的文本,是直接存储在该程序二进制数据中的,而程序二进制数据则是一直可用的。因此,所有字符串字面值的生命周期,就是 `'static`。 + +在一些错误消息中,或许会看到使用 `'static` 生命周期的建议。不过在将 `'static` 指定为某个引用的生命周期之前,请想一下手头的这个引用,是不是真的会存活到整个程序的生命周期,以及是否想要这个引用存活到程序的整个生命周期。多数时候,某个建议 `'static` 生命周期的错误消息,都是由尝试创建悬空引用,或可行的生命周期不匹配造成的。在这些情况下,解决办法是修复这些问题,而不是指定这个 `'static` 生命周期。 + + +### 泛型参数、特质边界与生命周期三位一体 + +**Generic Type Parameters, Trait Bounds, and Lifetimes Together** + + +下面就来简要地看看,在一个函数中,同时指定出全部的泛型参数、特质边界与生命周期的语法! + +```rust +use std::fmt::Display; + +fn longest_with_an_announcement<'a, T>( + x: &'a str, + y: &'a str, + ann: T, +) -> &'a str +where + T: Display, +{ + println! ("通知!{}", ann); + if x.len() > y.len() { + x + } else { + y + } +} +``` + +这就是清单 10-21 中,返回两个字符串切片中较长者的那个 `longest` 函数。不过现在他有了个名为 `ann`、泛型 `T` 的额外参数,这个泛型 `T` 可填入任何的、实现了那个由 `where` 子句所指定的 `Display` 特质的类型。这个额外参数,将被使用 `{}` 打印出来,这就是 `Display` 特质作为必要的原因。由于生命周期是一种通用类型,因此这里的生命周期参数 `'a` 与泛型参数 `T`,就处于函数名称之后尖括号内部的同一清单里头。 + + +## 本章小结 + +在这一章中,涉及到了很多东西!既然了解了泛型参数、特质与特质边界,以及通用生命周期参数,那么就算准备好编写在许多不同情形下,不带有重复的工作代码了。泛型参数实现了将代码应用于不同类型。特质与特质边界则确保了即使类型是通用的,他们仍将有着代码所需的行为。咱们还掌握了运用生命周期注解,来保证这样的灵活代码不会有任何的悬空引用。而全部的这种分析,都是发生在编译时,这样的特性未对运行时性能造成影响! + +不论相信与否,本章中讨论到的这些话题,要掌握的东西远不止这些:第 17 章会讨论特质对象(trait objects),那是另一种运用特质的方式。同时则还有更多只会在甚为高级场合,会需要用到生命周期注解的复杂场景;为着这些目的,就要阅读一下 [Rust 指南](https://doc.rust-lang.org/reference/index.html)。不过接下来,就会掌握怎样编写 Rust 中的测试,这样就可以确保所咱们的代码,以其应该有的方式工作。 diff --git a/src/Ch11_Writing_Automated_Tests.md b/src/Ch11_Writing_Automated_Tests.md new file mode 100644 index 0000000..cfec84b --- /dev/null +++ b/src/Ch11_Writing_Automated_Tests.md @@ -0,0 +1,1417 @@ +# 编写自动化测试 + +在 Edsgar W. Dijkstra(迪杰斯特拉) 1972 年论文 [《谦卑的程序员(The Humble Programmer)》](https://www.cs.utexas.edu/users/EWD/transcriptions/EWD03xx/EWD340.html)中,迪杰斯特拉指出 “程序测试可以是一种揭示代码存在的非常有效方式,但对于揭示代码错误存在,程序测试又显得不那么足够(Program testing can be a very effective way to show the presence of bugs, but it is hopelessly inadequate for showing their absensce)。” 这并不意味着咱们就不要尽力进行尽可能多的测试! + +所谓计算机程序正确,即为所编写代码,在多大程度上完成了想要他完成的事情。Rust 是以高度关注程序正确度而设计的,不过正确度是个复杂的问题,而不易于证明。Rust 的类型系统承担了保证正确性的很大部分,但类型系统并不能捕获到所有东西。由于这方面的原因,Rust 包括了编写自动化软件测试的支持。 + +这里假设说编写了一个将 `2` 加到所传入任何数字的一个函数 `add_two`。该函数的签名,会接受某个整数作为参数,并返回一个整数作为计算结果。在实现并编译那个函数时,Rust 会完成至此所掌握的全部类型检查与借用检查,来确保比如这里没有传递某个 `String` 值,或传递某个无效引用到该函数。但 Rust *无法* 就该函数将准确完成咱们所想要的操作,即返回参数加 `2`,而非参数加 `10` 或者参数减去 `50` 进行检查!这正是测试发挥作用的地方。 + +可编写出进行假定的一些测试来,比如,在将 `3` 传递给这个 `add_two` 函数时,返回的值就是 `5`。每当修改了代码时,就都可以运行这些测试,来确保车关系的任何既有正确行为,没有发生变化。 + +测试是门综合技能:尽管这里无法在一章中,涉及到怎样编写良好测试的方方面面,这里还是会对 Rust 各种测试设施的机制进行讨论。这里会讲到在编写测试时,可用的注解与宏,运行测试的默认动作与选项,以及怎样将一些测试,组织为单元测试与集成测试(unit tests and integration tests)。 + + +## 怎样编写测试 + +所谓测试,是指一些验证非测试代码(the non-test code)以预期方式发挥作用的函数(tests are Rust functions that verify that the non-test code is functioning in the expected manner)。测试函数的函数体,通常执行以下三种操作: + +1. 建立起全部所需的数据或状态; + +2. 运行打算测试的代码; + +3. 就运行结果是所期望的结果进行断言(assert the results are what you expect)。 + +下面就来看看,Rust 专为编写进行这些操作的测试,而提供到一些特性,包括 `test` 属性(the `test` attribute)、几个宏,以及 `should_panic` 属性(the `should_panic` attribute)。 + + +### 测试函数剖析 + +**The Anatomy of a Test Function** + +Rust 最简单形态的测试,就是以 `test` 属性注解的一个函数。所谓属性,是指有关 Rust 代码片段的元数据(attributes are metadata about pieces of Rust code);在第 5 章中,[用在结构体上的 `derive` 属性](Ch05_Using_Structs_to_Structure_Related_Data.md#adding-useful-functionality-with-derived-traits),就是一个属性的例子。要将某个函数修改为测试函数,就要把 `#[test]` 添加在 `fn` 之前的行上。在以 `cargo test` 命令运行编写的测试时,Rust 就会构建一个运行这些注解过的函数,并就各个测试函数是否通过或失败进行汇报的测试运行器二进制文件(a test runner binary)。 + +每当用 Cargo 构造了一个新的库项目时,就会自动生成有着一个测试函数的测试模组。该模组给到了编写测试的模板,如此以来,就不必在每次开始新项目时,去找寻确切的测试结构及语法了。而至于要添加多少个额外测试函数与测试模组,则取决于咱们自己! + +在对代码进行具体测试之前,这里将通过进行模板测试(the template test)下的试验,来探索测试工作原理的一些方面。随后就会编写一些对之前曾编写的代码进行调用,并就这些代码有着正确行为进行断言的、真实世界中的测试。 + +先来创建一个名为 `adder`、把两个数字相加的新库项目: + +```console +$ cargo new adder --lib lennyp@vm-manjaro + Created library `adder` package +$ cd adder +``` + + +在 `adder` 库中 `src/lib.rs` 文件的内容,应看起来如清单 11-1 所示。 + + +文件名:`src/lib.rs` + +```rust +#[cfg(test)] +mod tests { + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } + +} +``` + +*清单 11-1:由 `cargo new` 自动生成的测试模组与函数* + + +至于现在,就要忽略顶部的两行,并着重于那个函数。请注意那个 `#[test]` 注解:此属性表示这是个测试函数,由此测试运行器就知道将这个函数,当作一个测试对待。在那个 `tests` 模组中,可能也会有一些非测试函数,来帮助建立一些常见场景或执行一些常见操作,因此就需要表明哪些函数是测试。 + +这个示例函数的函数体,使用了 `assert_eq!` 宏,来对包含了 `2` 加 `2` 的结果 `result` 等于 `4` 进行断言。该断言是作为一个典型测试的格式示例,而提供的。下面就来运行他,来看到该测试会通过。 + +`cargo test` 命令会运行此项目中的全部测试,如下清单 11-2 所示。 + +```console +$ cargo test 1m 48s lennyp@vm-manjaro + Compiling adder v0.1.0 (/home/lennyp/rust-lang/adder) + Finished test [unoptimized + debuginfo] target(s) in 0.58s + Running unittests src/lib.rs (target/debug/deps/adder-3985394b39347736) + +running 1 test +test tests::it_works ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Doc-tests adder + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +``` + +*清单 11-2:运作这个自动生成测试的输出* + + +Cargo 编译并运行了这个测试。这里看到那行 `running 1 test`。接下来的行就给出了那个自动生成测试函数的名字,名为 `it_works`,以及运行那个测试的结果为 `ok`。整体结论 `test result: ok.` 就表示全部测试都通过了,而后面的 `1 passed; 0 failed` 的部分,则对通过与未通过的测试数据,做了合计。 + +将某个测试标记为忽略,进而其在特定实例中不运行,是可能的;在本章后面的 ["忽视某些在特别要求下才运行的测试(Ignoring Some Tests Unless Specifically Requested)"](#ignoring-some-tests-unless-specifically-requested) 小节,就会讲到这个问题。由于这里尚未完成这个问题,因此这里的测试总结,就给出了 `0 ignored`。这里还可以把一个参数,传递给这个 `cargo test` 命令,来只测试那些名字与某个字符串匹配的测试;此特性叫做 *过滤(filtering)*,在 [“通过指定测试名字运行测试子集(Running a Subset of Tests)”](#running-a-subset-of-tests) 小节,就会讲到这个问题。而这里也没有对所运行的测试加以过滤,因此在该测试小结的最后,显示了 `0 filtered out`。 + +其中属于基准测试的 `0 measured` 统计值,对性能进行了测量。所谓基准测试(benchmark tests),就跟其字面意思一样,只在每日构建版的 Rust 中可用。请参阅 [基准测试相关文档](https://doc.rust-lang.org/unstable-book/library-features/test.html) 了解更多信息。 + +测试输出接下来的部分,是以 `Doc-tests adder` 开始的,在有文档测试时,这便是文档测试的输出。虽然目前尚无文档测试,当 Rust 是可以编译在 API 文档中的全部代码示例的。此特性有助于将文档与代码保持同步!在第 14 章的 [“作为测试的文档注释(Documentation Comments as Tests)”](Ch14_More_about_Cargo_and_Crates_io.md#documentation-comments-as-tests) 小节,就会讨论怎样编写文档测试。至于现在,就会这个 `Doc-tests` 的输出加以忽略。 + + +接下来开始将该测试,定制为咱们自己所需的样子。首先将其中的 `it_works` 函数的名字,修改到某个别的名字,比如 `exploration`,像下面这样: + + +文件名:`src/lib.rs` + + +```rust +#[cfg(test)] +mod tests { + + #[test] + fn exploration() { + assert_eq! (2 + 2, 4); + } +} +``` + +随后再度运行 `cargo test`。其输出此时就给出了 `exploration` 而非 `it_works`: + + +```console +$ cargo test lennyp@vm-manjaro + Compiling adder v0.1.0 (/home/lennyp/rust-lang/adder) + Finished test [unoptimized + debuginfo] target(s) in 1.64s + Running unittests src/lib.rs (target/debug/deps/adder-3985394b39347736) + +running 1 test +test tests::exploration ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Doc-tests adder + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +``` + +现在就要添加另一测试,但这次将构造一个会失败的测试!测试是在测试函数中的某个东西发生终止运行时,才失败的。每个测试都是运行在一个新线程中,并在主线程发现某个测试线程死去时,该测试就被标记为失败了。在第 9 章中,就讲到引发代码终止运行的最简单方式,即为调用 `panic!` 这个宏。请敲入一个名为 `another` 函数的新测试,那么这个 `src/lib.rs` 看起来就如同下面清单 11-3 这样。 + + +文件名:`src/lib.rs` + +```rust +#[cfg(test)] +mod tests { + + #[test] + fn exploration() { + assert_eq! (2 + 2, 4); + } + + #[test] + fn another() { + panic! ("令该测试失败"); + } +} +``` + +使用 `cargo test` 再度运行这些测试。其输出看起来应如同清单 11-4 那样,显示这里的 `exploration` 测试通过而 `another` 失败了。 + +```console +$ cargo test lennyp@vm-manjaro + Compiling adder v0.1.0 (/home/lennyp/rust-lang/adder) + Finished test [unoptimized + debuginfo] target(s) in 0.42s + Running unittests src/lib.rs (target/debug/deps/adder-3985394b39347736) + +running 2 tests +test tests::exploration ... ok +test tests::another ... FAILED + +failures: + +---- tests::another stdout ---- +thread 'tests::another' panicked at '令该测试失败', src/lib.rs:15:9 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + tests::another + +test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +error: test failed, to rerun pass '--lib' +``` + +*清单 11-4:在一项测试通过而一项测试失败时的测试输出* + +这里不再是 `ok` 了,`test tests::another` 那行给出了 `FAILED`。在这单独结果与测试小结直接,出现了两个新的部分:第一部分显示各个测试失败的具体原因。在此示例中,就得到 `another` 失败详情,是由于该测试函数在 `src/lib.rs` 文件第 15 行处 `panicked at '令该测试失败'`。接下来的部分,则列出了仅所有失败测试的名字,这在有很多测试,进而有很多详细失败测试输出时,是有用的。随后就可以使用某个失败测试的名字,来只运行该项测试而更容易地对其加以调试;在 [“对测试运行方式进行控制(Controlling How Tests Are Run)”](#controlling-how-tests-are-run) 小节,将对运行测试方式,进行深入讲解。 + + +显示在最后的测试小节行:总体上看,这个测试的结果为 `FAILED`。这里有一个测试通过,以及一个测试失败了。 + +既然现在已经见识了不同场景下测试结果的样子,那么就来看看在测试中,除 `panic!` 之外其他一些有用的宏。 + + +### 以 `assert!` 宏来对测试结果进行检查 + +这个由标准库提供的 `assert!` 宏,在想要确保测试中某些情形求值为 `true` 时,是有用的。要给到这个 `assert!` 宏,一个求值为布尔值的参数。在求得的值为 `true` 时,就什么也不会发生,同时该测试通过。而在求得的值为 `false` 时,那么这个 `assert!` 宏就会调用 `panic!` 来造成该测试失败。使用这个 `assert!` 宏,有助于检查所编写代码,是以所计划方式运作。 + +在第 5 章的清单 5-15 中,用到了一个 `Rectangle` 结构体,以及一个 `can_hold` 方法,下面清单 11-5 中重复了那段代码。下面就将这段代码放在 `src/lib.rs` 文件,随后就要使用 `assert!` 宏为其编写一些测试。 + +文件名:`src/lib.rs` + +```rust +#[derive(Debug)] +struct Rectangle { + width: u32, + height: u32, +} + +impl Rectangle { + fn can_hold(&self, other: &Rectangle) -> bool { + (self.width > other.width && self.height > other.height) || (self.width > other.height && self.height > other.width) + } +} +``` + +*清单 11-5:使用第 5 章的 `Rectangle` 结构体及其 `can_hold` 方法* + +这个 `can_hold` 方法返回的是个布尔值,这就表示他是个 `assert!` 宏的绝佳用例。在下面清单 11-6 中,这里经由创建一个有着宽为 `8` 高为 `7` 的 `Rectangle` 实例,并断言其可装下另一个宽为 `5` 高为 `1` 的 `Rectangle` 实例,而编写了一个对该 `can_hold` 方法进行检查的测试。 + +文件名:`src/lib.rs` + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn larger_can_hold_smaller() { + let larger = Rectangle { + width: 8, + height: 7, + }; + let smaller = Rectangle { + width: 5, + height: 1, + }; + + assert! (larger.can_hold(&smaller)); + } +} +``` + +*清单 11-6:`can_hold` 的一个检查较大矩形是否能够真正包含较小矩形的测试* + +请注意这里在 `tests` 模组里头添加了个新行:`use super::*;`。这个 `tests` 模组是个遵循第 7 章中,[“用于指向模组树中某个项目的路径”](Ch07_Managing_Growing_Projects_with_Packages_Crates_and_Modules.md#paths-for-referring-to-an-item-in-the-module-tree)小节中曾讲到一般可见性规则的常规模组。由于这个 `tests` 模组是个内部模组,因此这里就需要将外层模组中的受测试代码,带入到这个 `tests` 内部模组的作用域。而由于这里使用了一个全局通配符(a glob, `*`),因此所有在外层模组中定义的内容,就对这个 `tests` 模组可用了。 + +这里已将这个测试命名为了 `larger_can_hold_smaller`,并创建除了所需的两个 `Rectanble` 实例。随后就调用了 `assert!` 宏,并将调用 `larger.can_hold(&smaller)` 的结果传递给了他。这个表达式应返回 `true`,因此这个测试将通过。那么就来试试看吧! + +```console +$ cargo test lennyp@vm-manjaro + Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo) + Finished test [unoptimized + debuginfo] target(s) in 0.37s + Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3) + +running 1 test +test tests::larger_can_hold_smaller ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Doc-tests assert_demo + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +``` + +这个测试真的通过了!接下来添加另一个测试,这次就断言某个较小矩形,无法装下一个较大矩形: + +文件名:`src/lib.rs` + + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn larger_can_hold_smaller() { + // --跳过代码-- + } + + #[test] + fn smaller_cannot_hold_larger() { + let larger = Rectangle { + width: 4, + height: 9, + }; + let smaller = Rectangle { + width: 8, + height: 3, + }; + + assert! (!smaller.can_hold(&larger)); + } +} +``` + +由于此情形下的 `can_hold` 正确结果为 `false`,因此就需要在将该结果传递给 `assert!` 宏之前,对其取反。而作为测试结果,在 `can_hold` 返回 `false` 时,这个测试就会通过: + +```console +$ cargo test lennyp@vm-manjaro + Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo) + Finished test [unoptimized + debuginfo] target(s) in 0.37s + Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3) + +running 2 tests +test tests::smaller_cannot_hold_larger ... ok +test tests::larger_can_hold_smaller ... ok + +test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Doc-tests assert_demo + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +``` + +两个测试均通过了!现在来看看在将一个代码错误(a bug)引入这里的代码时,这里的测试结果将发生什么。这里会通过在比较两个矩形宽时,将大于符号替换为小于符号,而对 `can_hold` 方法的实现加以修改: + +```rust +// --跳过代码-- +impl Rectangle { + fn can_hold(&self, other: &Rectangle) -> bool { + (self.width < other.width && self.height > other.height) || + (self.width < other.height && self.height > other.width) + } +} +``` + +现在运行这些测试,就会生成下面的输出: + +```console +$ cargo test lennyp@vm-manjaro + Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo) + Finished test [unoptimized + debuginfo] target(s) in 0.37s + Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3) + +running 2 tests +test tests::larger_can_hold_smaller ... FAILED +test tests::smaller_cannot_hold_larger ... ok + +failures: + +---- tests::larger_can_hold_smaller stdout ---- +thread 'tests::larger_can_hold_smaller' panicked at 'assertion failed: larger.can_hold(&smaller)', src/lib.rs:29:9 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + tests::larger_can_hold_smaller + +test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +error: test failed, to rerun pass '--lib' +``` + +这些测试就捕获到了代码错误(the bug)!由于 `larger.width` 为 `8` 而 `smaller.width` 为 `5`,那么在 `can_hold` 方法中宽的比较现在就会返回 `false`: `8` 不比 `5` 小。 + + +### 使用 `assert_eq!` 与 `assert_ne!` 两个宏测试是否相等 + +对功能进行验证的一种常见方式,便是对测试之前代码的输出结果,与所期望的代码返回值之间是否相等进行测试。使用 `assert!` 宏并将一个使用了 `==` 运算符的表达式传递给他,就可完成这样的测试。然而由于这是一个如此常见的测试,以致标准库提供了一对宏 -- `assert_eq!` 与 `assert_ne!` -- 来更方便地执行这样的测试。这两个宏分别比较两个参数的相等与不相等。在断言失败时,他们还会打印出那两个值,这就令到发现 *为何* 测试失败,更为容易了;与之相反,`assert!` 宏则只表明他收到了那个 `==` 表达式的 `false` 值,而没有将导致那个 `false` 值的两个值打印出来的功能。 + +在下面清单 11-7 中,就编写了一个名为 `add_two`、将 `2` 加到其参数的函数,随后使用 `asset_eq!` 宏对这个函数进行了测试。 + +文件名:`src/lib.rs` + +```rust +pub fn add_two(a: i32) -> i32 { + a + 2 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_adds_two() { + assert_eq! (4, add_two(2)); + } +} +``` + +*清单 11-7:使用 `assert_eq!` 宏对函数 `add_two` 进行测试* + + +下面就来看看,他通过了测试! + + +```console +$ cargo test lennyp@vm-manjaro + Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo) + Finished test [unoptimized + debuginfo] target(s) in 0.56s + Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3) + +running 1 test +test tests::it_adds_two ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Doc-tests assert_demo + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +``` + +这里将 `4` 作为参数传递给了 `assert_eq!`,这与调用 `add_two(2)` 的结果相等。该测试的那一行就是 `tests::it_adds_two ... ok`,而文本 `ok` 就表明这个测试通过了! + +接下来将一个 bug 引入到这里的代码,看看在 `assert_eq!` 失败时,会是什么样子。将这个 `add_two` 函数的实现修改为加 `3`: + +```rust +pub fn add_two(a: i32) -> i32 { + a + 3 +} +``` + +在此运行这些测试(the tests): + +```console +$ cargo test lennyp@vm-manjaro + Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo) + Finished test [unoptimized + debuginfo] target(s) in 0.54s + Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3) + +running 1 test +test tests::it_adds_two ... FAILED + +failures: + +---- tests::it_adds_two stdout ---- +thread 'tests::it_adds_two' panicked at 'assertion failed: `(left == right)` + left: `4`, + right: `5`', src/lib.rs:11:9 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + tests::it_adds_two + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +error: test failed, to rerun pass '--lib' +``` + +这里的测试捕获到了那个 bug!其中的 `it_adds_two` 测试就失败了,同时这些消息讲到,失败的断言为 ``assert failed: `(left == right)` ``,以及其中 `left` 与 `right` 的值分别为何。该消息有助于发起调试:那个 `left` 参数为 `4`,而那个 `right` 参数,即放上 `add_two(2)` 的那个,为 `5`。那么这里就可以联想到,当有很多测试在进行时,这一点就会尤其有帮助了。 + +请注意在某些语言与测试框架中,相等断言函数的那两个参数,分别叫做 `expected` 与 `actual`,且指定这两个参数的顺序是至关重要的。不过在 Rust 中,他们则分别叫做 `left` 与 `right`,且在指定所期望值与代码产生值的顺序,并不重要。这里可将该断言写作 `assert_eq! (add_two(2), 4)`,这仍会导致这个显示出 ``assertion failed: `(left == right)` `` 的同样失败消息。 + +而 `assert_ne!` 宏则将在给到其两个不相等值时通过测试,在两个值相等时测试失败。对于在不确定某个值是什么,但却清楚该值明显不会为何时的各种情形,这个宏就是最有用的。比如,在对某个确切会以某种方式修改其输入的函数进行测试,而修改方式会根据具体每周的哪一天运行该测试发生改变时,那么加以断言的最佳事物,就会是该函数的输出,与其输入不相等。 + +表象之下,`assert_eq!` 与 `assert_ne!` 两个宏,分别使用了运算符 `==` 与 `!=`。在他们的断言失败时,这两个宏就会使用调试格式化(debug formatting),将他们的参数打印出来,这就意味着正被比较的两个值,必须实现了 `PartialEq` 与 `Debug` 特质。全部原生值与绝大多数的标准库类型,都实现了这两个特质。而对于咱们自己定义的结构体与枚举,就需要实现 `PartialEq` 来对这些类型的相等与否进行断言。同样还需要实现 `Debug`,来在断言失败时打印比较的两个值。由于这两个特质都正如第 5 章清单 5-12 中所提到的派生特质(derivable traits),这样就跟将 `#[derive(PartialEq, Debug)]` 注解,添加到所编写的结构体或枚举定义一样直接了。请参阅附录 C,[“可派生特质(derivable traits)”](Ch21_Appdendix.md#c-derivable-traits) 了解更多有关这两个及其他派生特质的详细信息。 + +### 加入定制失败消息 + +**Adding Custom Failure Message** + +还可将与失败消息一同打印的定制消息,作为 `assert!`、`assert_eq!` 及 `assert_ne!` 宏的可选参数加入进来。在必须的两个参数之后指定的全部参数,都被传递给他们中的 `format!` 宏(第 8 章中 [“以 `+` 操作符或 `format!` 宏的字符串连接(Concatenation with the `+` Operator or the `format!` macro)”](Ch08_Common_Collections.md#concatenation-with-the-plus-operator-or-the-format-macro)) 小节曾讲到),因此就可以传递一个包含了 `{}` 占位符的格式化字符串,以及进到这些占位符的值。对于给某个断言表示什么的文档编制,这些定制消息就是有用的;在某个测试失败时,就会有着该代码下那个问题的较好理解。 + +比如说,这里有个按照名字来打招呼的函数,并打算就传入到该函数的名字有出现在输出中进行测试: + +文件名:`src/lib.rs` + +```rust +pub fn greeting(name: &str) -> String { + format! ("你好,{}", name) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn greeting_contains_name() { + let result = greeting("Lenny"); + assert! (result.contains("Lenny")); + } +} +``` + +该程序的各项要求尚未达成一致,同时这里十分肯定问候开始处的 `你好` 文字将会改变。这里已经确定不打算在各项要求改变时,必定要对这个测试加以更新,因此这里将只就输出包含输出参数的文本进行断言,而非对自 `greeting` 函数返回的值,进行精确的相等检查。 + +下面就来通过把 `greeting` 修改未排除 `name`,而将一个 bug 引入到这段代码,来看看这个默认测试失败的样子: + +```rust +pub fn greeting(name: &str) -> String { + String::from("你好!") +} +``` + +运行这个测试,就会产生以下输出: + +```console +$ cargo test lennyp@vm-manjaro + Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo) + Finished test [unoptimized + debuginfo] target(s) in 0.48s + Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3) + +running 1 test +test tests::greeting_contains_name ... FAILED + +failures: + +---- tests::greeting_contains_name stdout ---- +thread 'tests::greeting_contains_name' panicked at 'assertion failed: result.contains(\"Lenny\")', src/lib.rs:12:9 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + tests::greeting_contains_name + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +error: test failed, to rerun pass '--lib' +``` + +这样的结果,正好表明了该断言失败了,以及这个失败断言所在的行。而更有用的失败消息,应会打印出那个 `greeting` 函数的值来。下面就来添加一个,由带有以获取自 `greeting` 函数的具体值所填充的占位符的格式字符串,所构成的定制失败消息: + +```rust + #[test] + fn greeting_contains_name() { + let result = greeting("Lenny"); + assert! ( + result.contains("Lenny"), + "问候语未包含名字,问候语的值为 `{}`", + result + ); + } +``` + +现在运行这个测试,就会得到内容更为的错误消息: + +```console +$ cargo test lennyp@vm-manjaro + Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo) + Finished test [unoptimized + debuginfo] target(s) in 0.42s + Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3) + +running 1 test +test tests::greeting_contains_name ... FAILED + +failures: + +---- tests::greeting_contains_name stdout ---- +thread 'tests::greeting_contains_name' panicked at '问候语未包含名字,问候语的值为 `你好!`', src/lib.rs:12:9 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + tests::greeting_contains_name + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +error: test failed, to rerun pass '--lib' +``` + +现在就可以在测试输出中,看到具体得到的值了,这将有助于对发生的事情,而非期望发生的事情进行调试,有所帮助(we can see the value we actually got in the test output, which would help us debug what happened instead of what we were expecting to happen)。 + + +### 使用 `should_panic` 对运行中止进行检查 + +**Checking for Panics with `should_panic`** + +除了检查返回值外,重要的是检查所编写代码有如预期那样,对各种错误情形进行处理。比如,请考虑在第 9 章清单 9-13 中所创建的那个 `Guess` 类型。使用了 `Guess` 的其他代码,就仰赖于 `Guess` 实例,将包含仅在 `1` 与 `100` 之间的值这一保证。这里就可以编写一个,确保在尝试创建带有那个范围之外值的 `Guess` 实例时,会中止运行的测试。 + +这里是通过将属性 `should_panic` 添加到此处的测试函数,来完成这一点的。在函数内部代码中止运行时,该测试便会通过;若函数中代码没有中止运行,那么该测试就会失败。 + +下面清单 11-8,就给出了一个在预期 `Guess::new` 的各种错误情形发生时,对这些错误情形进行检查的测试。 + +文件名:`src/lib/rs` + +```rust +pub struct Guess { + value: i32, +} + +impl Guess { + pub fn new(value: i32) -> Guess { + if value < 1 || value > 100 { + panic! ("Guess 值必须在 1 与 100 之间,得到的是 {}。", value); + } + + Guess { value } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[should_panic] + fn greater_than_100() { + Guess::new(200); + } +} +``` + +*清单 11:就某个将引发 `panic!` 的情形进行测试* + +这里将那个 `#[should_panic]` 属性,放在了 `#[test]` 属性之后,且在其应用到的函数之前。下面来看看在该测试通过时的样子: + + +```console +$ cargo test lennyp@vm-manjaro + Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo) + Finished test [unoptimized + debuginfo] target(s) in 0.64s + Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3) + +running 1 test +test tests::greater_than_100 - should panic ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Doc-tests assert_demo + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +``` + +看起来不错!现在就来通过移出当其中的值大于 `100` 时,这个 `new` 函数将中止运行的条件,而将一个 bug 引入到这里的代码: + +```rust +// --跳过代码-- +impl Guess { + pub fn new(value: i32) -> Guess { + if value < 1 { + panic! ("Guess 值必须在 1 与 100 之间,得到的是 {}。", value); + } + + Guess { value } + } +} +``` + +此时在运行清单 11-8 中的测试,他就会失败了: + +```console +$ cargo test lennyp@vm-manjaro + Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo) + Finished test [unoptimized + debuginfo] target(s) in 0.42s + Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3) + +running 1 test +test tests::greater_than_100 - should panic ... FAILED + +failures: + +---- tests::greater_than_100 stdout ---- +note: test did not panic as expected + +failures: + tests::greater_than_100 + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +error: test failed, to rerun pass '--lib' + +``` + +在这个示例中,并未获得非常有用的消息,不过在查看那个测试函数时,就会看到其被 `#[should_panic]` 给注解过。这里收到了失败,就表示在这个测试函数中的代码,并未引发运行中止。 + +用到 `should_panic` 的测试,可并不那么精确。即便在该测试由于某个不同于咱们所预期的原因而中止运行了,这个 `should_panic` 测试仍会通过。要令到 `should_panic` 测试更加精确,则可以将某个可选的 `expected` 参数,传递给那个 `should_panic` 属性。这种测试工具,将确保失败消息包含了所提供的文本(the test harneess will make sure that the failure message contains the provided text)。比如,请考虑下面清单 11-9 中修改过的 `Guess` 代码,其中 `new` 函数会根据该值是否过小或过大,而以不同消息中止运行。 + +文件名:`src/lib.rs` + +```rust +pub struct Guess { + value: i32, +} + +impl Guess { + pub fn new(value: i32) -> Guess { + if value < 1 { + panic! ( + "Guess 值必须大于或等于 1, 得到的是 {}。", + value + ); + } else if value > 100 { + panic! ( + "Guess 值必须小于或等于 100, 得到的是 {}。", + value + ); + } + + + Guess { value } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[should_panic(expected = "小于或等于 100")] + fn greater_than_100() { + Guess::new(200); + } +} +``` + +*清单 11-9:对有着包含指定 _子字符串_ 的中止运行消息,的某个 `panic!` 进行测试* + +由于这里放在那个 `should_panic` 属性的 `expected` 参数中的值,正是其中 `Guess::new` 函数中止运行消息的一个子字符串,因此这个测试将通过。这里本可将所预期的整个中止运行消息给指定出来,在此示例中即为 `Guess 值必须小于或等于 100,得到的是 200。` 选择指明什么,是根据中止运行消息,具有何种程度的独特性或动态变化,以及打算要整个测试具有何种级别的准确度。在此示例中,那个中止运行消息的某个子字符串,就足够用于确保该测试函数中代码,执行了 `else if value > 100` 的条件。 + +为看到在某个 `should_panic` 以一个 `expected` 消息失败时,会发生什么,下面就来通过调换 `if value < 1` 与 `else if value > 100` 代码块的代码体,而引入一个 bug 到这里的代码中: + +```rust + if value < 1 { + panic! ( + "Guess 值必须小于或等于 100, 得到的是 {}。", + value + ); + } else if value > 100 { + panic! ( + "Guess 值必须大于或等于 1, 得到的是 {}。", + value + ); + } +``` + +这次在运行这个 `should_panic` 测试时,便会失败了: + +```console +$ cargo test lennyp@vm-manjaro + Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo) + Finished test [unoptimized + debuginfo] target(s) in 0.41s + Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3) + +running 1 test +test tests::greater_than_100 - should panic ... FAILED + +failures: + +---- tests::greater_than_100 stdout ---- +thread 'tests::greater_than_100' panicked at 'Guess 值必须大于或等于 1, 得到的是 200。', src/lib.rs:13:13 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +note: panic did not contain expected string + panic message: `"Guess 值必须大于或等于 1, 得到的是 200。"`, + expected substring: `"小于或等于 100"` + +failures: + tests::greater_than_100 + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +error: test failed, to rerun pass '--lib' +``` + +这样的失败消息就表示,这个测试确实如预期那样中止运行了,但中止运行消息并未包含预期的字符串 `小于或等于 100`。在此示例中,真正得到中止运行消息,为 `Guess 值必须大于或等于 1, 得到的是 200。` 现在就可以开始找出,这里的 bug 在哪了! + + +### 在测试中使用 `Result` + +到目前为止,这里全部的测试在失败时,都会中止运行。这里通用可以编写用到 `Result` 的测试!下面就是清单 11-1 的那个测试,只是被重写为了使用 `Result`,并返回一个 `Err` 而非中止运行: + +```rust +#[cfg(test)] +mod tests { + #[test] + fn it_works() -> Result<(), String> { + if 2 + 2 == 4 { + Ok(()) + } else { + Err(String::from("二加二不等于四")) + } + } +} +``` + +这个 `it_works` 函数现在有了 `Result` 的返回值类型。而在该函数的函数体中,此时在那个 `if` 测试通过时,返回了 `Ok(())`,在测试失败时返回一个带有 `String` 的 `Err`,而不再调用那个 `assert_eq!` 宏了。 + + +编写这样的返回某个 `Return` 的测试,就令到在各个测试的函数体中,使用问号运算符(the question mark operator, `?`)可行了,而在测试函数体中使用 `?`,则可以是编写那些,在其内部返回某个 `Err` 变种时将会失败测试的便利方式。 + +在那些用到 `Result` 的测试上,是不可以使用 `#[should_panic]` 注解的。而要断言某个操作返回的是一个`Result` 枚举的 `Err` 变种,就不要在返回的 `Result` 值上,使用问号操作符。相反,要使用 `assert!(value.is_err())` 这种方式。 + + +既然咱们已经了解了编写测试的几种方式,那么就来看一下,在运行这些编写的测试时会发生什么,并探索一下可与 `cargo test` 一起使用的不同选项。 + + +## 控制测试以何种方式运行 + +就跟 `cargo run` 会编译代码并于随后运行得出的二进制程序一样,`cargo test` 也会以测试模式编译所编写的代码,并会运行得到的测试二进制程序。而由 `cargo test` 产生出的二进制程序默认行为,即是以并行方式运行全部测试,并在测试运行期间捕获输出,阻止输出被显示出来以及令到与测试结果相关的输出,更加易于阅读(the default behavior of the binary produced by `cargo test` is to run all the tests in parallel and capture output generated during test runs, preventing the output from being displayed and making it easier to read the output related to the test results)。不过,这里是可以指定一些命令行选项,来改变这种默认行为的。 + +一些命令行选项是介入到 `cargo test`,而一些则是介入所得到的测试二进制程序。在介入到 `cargo test` 的命令行参数之后,跟上分隔符 `--`,随后才是那些进到测试二进制程序的参数,以这样的方式把这两种类型的命令行参数区分开。运行 `cargo test --help`,就会显示出可在 `cargo test` 下使用的选项,而运行 `cargo test -- --help` 则会显示出可在分隔符之后使用的那些选项。 + +### 并行还是连续地运行测试 + +**Running Tests in Parallel or Consecutively** + +在运行多个测试时,这些测试默认使用线程以并行方式运行,意味着他们会运行得更快,而咱们也会迅速地得到反馈。由于这些测试是在同时运行的,因此就必须确保所编写的测试不会各自依赖,并依赖于任何共用的状态,包括某种共用环境,诸如当前工作目录或环境变量。 + +比如说,所编写的每个测试,都会运行一些在磁盘上创建名为 `test-output.txt` 的文件,并将某些数据写到那个文件的代码。随后各个测试就会读取那个文件中的数据,并就那个包含了某个特定值进行断言,这个断言的特定值在各个测试中是不同的。由于这些测试是在同一时间运行,某个测试就可能会在另一测试写入与读取这个文件期间,对该文件进行覆写。那么第二个测试随后就将并非由于代码不正确,而因为这些测试在并行运行期间,相互之间造成了影响而失败。一种解决办法,是确保各个测试写入到不同文件;另一种办法,就是以一次运行一个的方式,运行这些测试。 + +在不打算并行运行这些测试,或要对所用到线程数有更细粒度掌控时,就可以将 `--test-threads` 这个命令行标志,与打算使用的线程数目,发送给那个测试二进制程序。请看看下面这个示例: + +```console +$ cargo test -- --test-threads=1 +``` + +这里把测试线程数设置为了 `1`,这就告诉了该程序不要使用任何并行机制。使用一个线程运行这些测试,相比以并行方式运行他们,将耗费更长时间,但在这些测试共用了状态时,他们之间不会相互影响。 + + +### 展示函数的输出 + +默认情况下,在某个测试通过时,Rust 的测试库会对任何打印到标准输出的内容加以捕获。比如,当在某个测试中调用 `println!` 且该测试通过时,就不会在终端中看到那个 `println!` 的输出;而只将看到表示该测试通过的那行。而在某个测试失败时,则会与失败消息的其余部分一起,看到任何打印到标准输出的内容,。 + +作为一个示例,下面清单 11-10 有着一个打印其参数值并返回 `10` 的弱智函数,以及一个会通过的测试与一个会失败的测试。 + +文件名:`src/lib.rs` + +```rust +fn prints_and_returns_10(a: i32) -> i32 { + println! ("我得到了一个值 {}", a); + 10 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn this_test_will_pass() { + let value = prints_and_returns_10(4); + assert_eq! (10, value); + } + + #[test] + fn this_test_will_fail() { + let value = prints_and_returns_10(8); + assert_eq! (5, value); + } +} +``` + +*清单 11-10:对一个调用了 `println!` 宏的函数的两个测试* + +在以 `cargo test` 运行这两个测试时,就会看到以下的输出: + +```console +$ cargo test lennyp@vm-manjaro + Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo) + Finished test [unoptimized + debuginfo] target(s) in 0.38s + Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3) + +running 2 tests +test tests::this_test_will_pass ... ok +test tests::this_test_will_fail ... FAILED + +failures: + +---- tests::this_test_will_fail stdout ---- +我得到了一个值 8 +thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)` + left: `5`, + right: `10`', src/lib.rs:19:9 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + tests::this_test_will_fail + +test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +error: test failed, to rerun pass '--lib' +``` + +请留意此输出中没有在哪里看到 `我得到了一个值 4`,这正是在通过的那个测试运行时所打印出的内容。那个输出就已被捕获了。而来自失败了的那个测试的输出,`我得到了一个值 8`,出现在了该测试的总结输出小节中,这个测试总结输出小节,还给出了该测试失败的原因。 + +在想要同样看到已通过测试的那些打印值时,就可以使用 `--show-output` 命令行开关,告诉 Rust 还要显示成功测试的输出。 + + +```console +$ cargo test -- --show-output +``` + +在使用 `--show-output` 命令行开关再次运行清单 11-10 中的那些测试时,就会看到下面的输出: + +```console +$ cargo test -- --show-output lennyp@vm-manjaro + Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo) + Finished test [unoptimized + debuginfo] target(s) in 0.41s + Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3) + +running 2 tests +test tests::this_test_will_fail ... FAILED +test tests::this_test_will_pass ... ok + +successes: + +---- tests::this_test_will_pass stdout ---- +我得到了一个值 4 + + +successes: + tests::this_test_will_pass + +failures: + +---- tests::this_test_will_fail stdout ---- +我得到了一个值 8 +thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)` + left: `5`, + right: `10`', src/lib.rs:19:9 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + tests::this_test_will_fail + +test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +error: test failed, to rerun pass '--lib' +``` + +### 依据测试名称来运行测试的某个子集 + +**Running a Subset of Tests by Name** + +在有的时候,运行一整个的测试套件可能要用很长时间。而当在某个特定方面编写代码时,就会想要只运行与正在编写代码有关的那些测试。通过将想要运行的某个或某些测试的名字,作为参数传递给 `cargo test`,就可以对想要运行哪些测试加以选择。 + +为了演示怎样运行测试子集,这里将首先为所编写的 `add_two` 函数,创建三个测试,如下清单 11-11 中所示,并会选择要运行哪些测试。 + +文件名:`src/lib.rs` + +```rust +pub fn add_two(a: i32) -> i32 { + a + 2 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn add_two_and_two() { + assert_eq! (4, add_two(2)); + } + + #[test] + fn add_three_and_two() { + assert_eq! (5, add_two(3)); + } + + #[test] + fn one_hundred() { + assert_eq! (102, add_two(100)); + } +} +``` + +*清单 11-11:有着不同名字的三个测试* + +如同早先所看到的那样,在不带传递任何参数运行这些测试时,全部这些测试将以并行方式运行: + +```console +$ cargo test lennyp@vm-manjaro + Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo) + Finished test [unoptimized + debuginfo] target(s) in 0.43s + Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3) + +running 3 tests +test tests::add_three_and_two ... ok +test tests::add_two_and_two ... ok +test tests::one_hundred ... ok + +test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Doc-tests assert_demo + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +``` + +**运行单个的测试** + +可将任何测试函数的名字,传递给 `cargo test` 来只运行那个测试: + + +```console +$ cargo test one_hundred lennyp@vm-manjaro + Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo) + Finished test [unoptimized + debuginfo] target(s) in 0.37s + Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3) + +running 1 test +test tests::one_hundred ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s + +``` + +上面就只有那个名字为 `one_hundred` 的测试运行了;其他两个测试并不与那个指定的名字匹配。而这个测试输出,通过末尾出显示出的 `2 filtered out`,而让咱们获悉有更多测试并未运行。 + +以这种方式是没法指定多个测试的名字的;只有给到 `cargo test` 的第一个值,才会被用到。不过是有方法来运行多个测试的。 + + +**使用过滤来运行多个测试** + +这里可指定某个测试函数名字的一部分,那么名字与所指定值匹配的全部测试,就都会被运行。比如,由于上面的那些测试中有两个测试的名字包含了 `add`,因此这里就可以通过运行 `cargo test add`,运行这两个测试: + +```console +$ cargo test add lennyp@vm-manjaro + Finished test [unoptimized + debuginfo] target(s) in 0.00s + Running unittests src/lib.rs (target/debug/deps/assert_demo-9c28057969510af5) + +running 2 tests +test tests::add_three_and_two ... ok +test tests::add_two_and_two ... ok + +test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s + +``` + +此命令运行了名字中有 `add` 字样的全部测试,并将那个名为 `one_hundred` 的测试给过滤掉了。还要留意到,测试所出现在的模组,成为了该测试名字的一部分,因此就可通过以模组名字来过滤,而运行某个模组中的全部测试。 + + +### 在未作特别要求时忽略某些测试 + +**Ignoring Some Tests Unless Specifically Requested** + + +有的时候少数几个特定测试,执行起来可能非常耗费时间,那么就会打算在绝大多数 `cargo test` 运行期间,将这些测试排除掉。与将全部想要运行的测试列为参数不同,这里是可以将那些耗费时间的测试,使用 `ignore` 属性进行注解,而将他们排除,如下所示: + +文件名:`src/lib.rs` + +```rust +pub fn add_two(a: i32) -> i32 { + a + 2 +} + +pub fn nth_fibonacci(n: u64) -> u64 { + + if n == 0 || n == 1 { + return n; + } else { + return nth_fibonacci(n - 1) + nth_fibonacci(n - 2); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn add_two_and_two() { + assert_eq! (4, add_two(2)); + } + + #[test] + fn add_three_and_two() { + assert_eq! (5, add_two(3)); + } + + #[test] + fn one_hundred() { + assert_eq! (102, add_two(100)); + } + + #[test] + fn it_works() { + assert_eq! (2 + 2, 4); + } + + #[test] + #[ignore] + fn expensive_test() { + assert_ne! (100, nth_fibonacci(50)); + } +} +``` + +这里在 `#[test]` 之后,把那行 `#[ignore]` 添加到了打算排除的那个测试之上。此时再运行这些测试时,原来的三个测试会运行,但 `expensive_test` 就不会运行: + +```console +$ cargo test lennyp@vm-manjaro + Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo) + Finished test [unoptimized + debuginfo] target(s) in 0.46s + Running unittests src/lib.rs (target/debug/deps/assert_demo-9c28057969510af5) + +running 5 tests +test tests::expensive_test ... ignored +test tests::add_two_and_two ... ok +test tests::one_hundred ... ok +test tests::it_works ... ok +test tests::add_three_and_two ... ok + +test result: ok. 4 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Doc-tests assert_demo + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +``` + +那个 `expensive_test` 函数就被列为了 `ignored`。而再打算只运行那些忽略的测试时,则可以使用 `cargo test -- --ignored`: + +```console +$ cargo test -- --ignored lennyp@vm-manjaro + Finished test [unoptimized + debuginfo] target(s) in 0.00s + Running unittests src/lib.rs (target/debug/deps/assert_demo-9c28057969510af5) + +running 1 test +test tests::expensive_test has been running for over 60 seconds +test tests::expensive_test ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 124.65s + + Doc-tests assert_demo + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +``` + +经由控制哪些测试运行,就可以确保 `cargo test` 快速得出结果。在对那些 `ignored` 测试结果进行检查是有意义的,且有时间等待他们的结果出来时,那么就可以运行 `cargo test -- --ignored`。在打算运行全部测试,而不管他们有没有被注解为 `ignored`,那么就可以运行 `cargo test -- --include-ignored`。 + + +```console +$ cargo test -- --include-ignored lennyp@vm-manjaro + Finished test [unoptimized + debuginfo] target(s) in 0.00s + Running unittests src/lib.rs (target/debug/deps/assert_demo-9c28057969510af5) + +running 5 tests +test tests::add_two_and_two ... ok +test tests::add_three_and_two ... ok +test tests::it_works ... ok +test tests::one_hundred ... ok +test tests::expensive_test has been running for over 60 seconds +test tests::expensive_test ... ok + +test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 130.68s + + Doc-tests assert_demo + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +``` + + +## 测试的组织 + +如同在本章开头提到的,测试是门复杂的学问,而不同人群会使用不同术语及组织方式(testing is a complex discipline, and different people use different terminology and organization)。Rust 社群认为,测试是由两个大类组成:单元测试与集成测试(unit tests and integration tests)。*单元测试* 是一些小而更为专注的测试,他们一次单独测试一个模组,并可对私有接口进行测试(*unit tests* are small and more focused, testing one module in isolation at a time, and can test private interfaces)。*集成测试* 则是完全在所编写库外部进行,并会像其他外部代码那样,对咱们的代码加以使用,因此就只会对公开接口进行使用,且潜在每个测试会检查多个模组。 + + +这两种类型的测试编写,对于确保代码库的各个部分有在单独及共同完成所预期的事项,都是重要的。 + + +### 单元测试 + +单元测试的目的,是要将各个代码单元孤立于其余代码进行测试,从而快速定位出何处代码有如预期那样工作,以及何处代码未如预期那样工作。应将单元测试,放在 `src` 目录之下,在那些有着他们要测试代码的各个文件中。约定即为要在各个文件中,创建包含那些测试函数的一个名为 `tests` 的模组,并使用 `cfg(test)` 对该模组加以注解。 + + +**测试模组与 `#[cfg(test)]`** + +在测试模组上的那个 `#[cfg(test)]` 注解,告诉 Rust 仅在运行 `cargo test`,而非运行 `cargo build`时,才编译和运行测试代码。在只打算构建该库时,这样就节省了编译时间,并由于在得到的已编译工件中不会包含测试,而在其中节省了空间。后面就会看到,由于集成测试会进到不同目录,他们就不需要这个 `#[cfg(test)]` 注解。不过由于单元测试是在与代码同样的文件中,因此就会使用 `#[cfg(test)]`,来指明他们不应被包含在编译结果中。 + +回顾在本章第一小节中,当那里生成那个新的 `adder` 项目时,Cargo 就为咱们生成了下面的代码: + + +```console +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + let result = 2 + 2; + assert_eq!(result, 4); + } +} +``` + +这段代码就是自动生成的测试模组。其中的属性 `cfg` 是指 *配置(configuration)*,而告诉 Rust 接下来的项目只应在给定的某个配置选项下才被包含。在此示例中,那个配置选项便是 `test`,Rust 提供的这个配置选项,用于测试的编译及运行。通过使用这个 `cfg` 属性,Cargo 就会只在咱们以 `cargo test`,明确表示要运行这些测试时,才对这里的测试代码进行编译。而这些测试代码,包含了可能位于此模组内部的全部辅助函数,以及使用 `#[test]` 注解过的那些函数。 + + +**私有函数的测试(Testing Private Functions)** + +在测试社区,有着私有函数是否应被直接测试的争论,而别的语言让对私有函数的测试,成为困难或不可行的事情。不论所才行的是何种测试理念,Rust 的私有规则,真的实现了对私有函数的测试。请考虑下面清单中,有着私有函数 `internal_adder` 的代码。 + +文件名:`src/lib.rs` + +```rust +pub fn add_two(a: i32) -> i32 { + internal_add(a, 2) +} + +fn internal_add(a: i32, b: i32) -> i32 { + a + b +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn internal() { + assert_eq! (4, internal_add(2, 2)); + } +} +``` + +*清单 11-12:对私有函数进行测试* + + +请注意这个 `internal_adder` 函数,未被标记为 `pub`。其中的那些测试,都只是些 Rust 代码,同时那个 `tests` 模组,只是另一个模组。如同在前面的 [“用于对模组树中某个项目进行引用的路径”](Ch07_Managing_Growing_Projects_with_Packages_Crates_and_Modules.md#paths-for-referring-to-an-item-in-the-module-tree) 小节中所讨论的,子模组中的那些项目,可以使用其祖辈模组中的项目。在这个测试中,就以 `use super::*` 语句,将那个 `tests` 模组父辈的那些项目,带入到了作用域,进而该测试随后就可以调用 `internal_adder` 了。而在不认为私有函数应被测试时,那么 Rust 就没有什么可以迫使你对他们进行测试了(if you don't think private functions should be tested, there's nothing in Rust that will compel you to do so)。 + + +### 集成测试 + +在 Rust 中,集成测试整个都是属于所编写库外部的。他们会以与其他代码同样方式,对咱们编写的库加以使用,这就意味着集成测试只能调用属于库公开 API 一部分的那些函数。集成测试的目的,是要就所编写库的多个部分,是否有正确地一起运作进行测试。这些各自正常工作的代码单元,在被集成在一起时,就可能有问题,因此集成后代码的测试面,也是重要的。要创建集成测试,首先就需要一个 `tests` 目录。 + + +**`tests` 目录** + +这里时在项目目录的顶层,挨着那个 `src` 目录,创建一个 `tests` 目录的。Cargo 就明白要在整个目录下,查找那些集成测试的文件。至于可以构造多少个测试文件,则是想要多少都可以,Cargo 将把这些各个文件,编译为单独的代码箱。 + +下面就来创建一个集成测试。使用清单 11-12 中仍在 `src/lib.rs` 文件中的代码,构造一个 `tests` 目录,并创建一个名为 `tests/integration_test.rs` 的文件。那么现在的目录结构,应像下面这样: + +```console +adder +├── Cargo.lock +├── Cargo.toml +├── src +│   └── lib.rs +└── tests + └── integration_test.rs +``` + +请将下面清单 11-13 中的代码,输入到那个 `tests/integration_test.rs` 文件中: + +文件名:`tests/integration_test.rs` + +```rust +use adder; + +#[test] +fn it_adds_two() { + assert_eq! (4, adder::add_two(2)); +} +``` + +*清单 11-13:一个 `adder` 代码箱中函数的集成测试* + +在 `tests` 目录中的每个文件,都是个单独代码箱,因此这里就需要将所编写的库,带入到各个测试代码箱的作用域。由于这个原因,这里就要在该代码的顶部,添加 `use adder` 语句,这在之前的单元测试中就不需要。 + +这里不需要以 `#[cfg(test)]` 对 `tests/integration_test.rs` 中的任何代码进行注解。Cargo 会特别对待 `tests` 目录,而只在运行 `cargo test` 时,才编译此目录中的文件。现在运行 `cargo test`: + +```console +$ cargo test lennyp@vm-manjaro + Compiling adder v0.1.0 (/home/lennyp/rust-lang/adder) + Finished test [unoptimized + debuginfo] target(s) in 0.52s + Running unittests src/lib.rs (target/debug/deps/adder-7763e46d5dd299a3) + +running 1 test +test tests::internal ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Running tests/integration_test.rs (target/debug/deps/integration_test-d0d0eaf0bad2a59f) + +running 1 test +test it_adds_two ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Doc-tests adder + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +``` + +输出的三个部分,包含了单元测试、集成测试与文档测试。请留意在某个部分的任何测试失败时,接下来的部分就不会运行了。比如,在某个单元测试失败时,由于集成测试与文档测试只会在全部单元测试通过时才运行,因此就不再会有集成与文档测试的任何输出了。 + +其中单元测试的第一部分,与之前曾见到过的一样:每个单元测试一行(那行就是在清单 11-12 中所添加的名为 `internal` 的测试),并随后有个这些单元测试的小结。 + +集成测试部分是以那行 `Running tests/integration_test.rs` 开始的。接下来,集成测试中的每个测试函数都有一行,且在紧接着 `Doc-tests adder` 部分开始之前,就是集成测试的那些结果的一个小结。 + +每个集成测试都有其自己的部分,那么在把更多文件添加到那个 `tests` 目录中时,就会有更多的集成测试部分了。 + +通过将测试函数的名字,指明为 `cargo test` 的命令行参数,这里仍可运行某个特定集成测试函数。而要运行某个特定集成测试文件中的全部测试,则要使用跟上了该文件名字的 `cargo test` 的 `--test` 参数: + +```console +$ cargo test --test integration_test lennyp@vm-manjaro + Compiling adder v0.1.0 (/home/lennyp/rust-lang/adder) + Finished test [unoptimized + debuginfo] target(s) in 0.23s + Running tests/integration_test.rs (target/debug/deps/integration_test-d0d0eaf0bad2a59f) + +running 1 test +test it_adds_two ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +``` + +此命令只运行在 `test/integration_test.rs` 文件中的那些测试。 + + +**集成测试中的子模组** + +随着更多集成测试的添加,就会想要在那个 `tests` 目录下,构造更多文件,来帮助组织这些文件;比如就可以将那些测试函数,按照他们所测试的功能而进行分组。如同早先所提到的,在 `tests` 目录下的各个文件,都作为其自己单独的代码箱而被编译,这一点对于创建独立作用域,来对最终用户将要使用所编写代码箱的方式,进行更紧密模拟是有用的。不过,这将意味着在 `tests` 目录中的那些文件,不会如同在第 7 章中,有关 [如何将代码分离为模组与文件](Ch07_Managing_Growing_Projects_with_Packages_Crates_and_Modules.md#separating-modules-into-different-files) 部分,所掌握的 `src` 中的那些文件那样,共用同样的行为。 + +在有着一套在多个集成测试文件中使用的辅助函数,并尝试遵循第 7 章 [将模组分离为不同文件](Ch07_Managing_Growing_Projects_with_Packages_Crates_and_Modules.md#separating-modules-into-different-files) 中的步骤,把这些辅助函数提取到某个通用模组中时,`tests` 目录的那些文件的不同行为就最为明显了。比如说,在创建出 `tests/common.rs` 并将一个名为 `setup` 的函数放在其中时,就可以将一些要在多个测试文件的多个测试函数调用的代码,添加到 `setup`。 + +文件名:`tests/common.rs` + +```rust +pub fn setup() { + // 特定于库测试的一些设置代码,将放在这里 +} +``` + +当再度运行这些测试时,即使这个 `common.rs` 文件未包含任何测试函数,也没有从任何地方调用这个 `setup` 函数,仍会在测试输出中,发现这个 `common.rs` 文件的一个新部分: + +```console +$ cargo test lennyp@vm-manjaro + Compiling adder v0.1.0 (/home/lennyp/rust-lang/adder) + Finished test [unoptimized + debuginfo] target(s) in 0.47s + Running unittests src/lib.rs (target/debug/deps/adder-7763e46d5dd299a3) + +running 1 test +test tests::internal ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Running tests/common.rs (target/debug/deps/common-82aa4aac16d81562) + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Running tests/integration_test.rs (target/debug/deps/integration_test-d0d0eaf0bad2a59f) + +running 1 test +test it_adds_two ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Doc-tests adder + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +``` + +以显示出他的 `running 0 tests` 方式,让 `common` 出现在测试结果中,并非咱们想要的。这里只是打算在其他集成测试文字之下,共用一些代码。 + +要避开让 `common` 出现在测试输出中,就要创建出 `tests/common/mod.rs`,而非创建出 `tests/common.rs`。该项目目录现在看起来像下面这样: + +```console +adder +├── Cargo.lock +├── Cargo.toml +├── src +│   └── lib.rs +└── tests + ├── common + │   └── mod.rs + └── integration_test.rs +``` + +这是曾在第 7 章 ["替代文件路径"](Ch07_Managing_Growing_Projects_with_Packages_Crates_and_Modules.md#alternate-file-paths) 小节所提到的,Rust 同样明白的较早命名约定。以这种方式命名该文件,就告诉 Rust 不要将那个 `common` 模组,作为一个集成测试文件对待。在将这个 `setup` 函数移入到 `tests/common/mod.rs` 里头,并删除了那个 `tests/common.rs` 文件时,在测试输出中的该部分就不再出现了。`tests` 目录子目录中的那些文件,不会作为单独代码箱而被编译,也不会在测试输出中拥有自己的部分。 + + +在创建出 `tests/common/mod.rs` 之后,就可以从任意的集成测试文件,将其作为模组而加以使用。下面就是一个从 `tests/integration_test.rs` 中的 `it_adds_two` 测试,对这个 `setup` 函数进行调用的示例: + +文件名:`tests/integration_test.rs` + +```rust +use adder; + +mod common; + +#[test] +fn it_adds_two() { + common::setup(); + assert_eq! (6, adder::add_two(4)); +} +``` + +请留意其中的 `mod common;` 声明,与曾在清单 7-21 中演示过的模组声明相同。随后在那个测试函数中,这里既可以调用那个 `common::setup()` 函数了。 + + +**二进制代码箱的集成测试** + +在所编写详细是个仅包含 `src/main.rs` 文件的二进制代码箱,而没有 `src/lib.rs` 文件时,就无法在 `tests` 目录中创建集成测试,以及使用 `use` 语句,将定义在 `src/main.rs` 中的函数带入到作用域。唯有库代码箱将其他代码箱可以使用的函数,给暴露出来;二进制代码箱本来就是由他们自己来运行的(binary crates are meant to be run on their own)。 + +这是那些提供到二进制程序的 Rust 项目,有着一个直接了当的、对存在于 `src/lib.rs` 逻辑进行调用的 `src/main.rs` 文件的原因之一。运用那样的结构,集成测试就 *可以* 使用 `use` 对库代码箱进行测试,从而令到重要功能可用。当重要功能运作时,那么在那个 `src/main.rs` 文件中的少量代码,也将同样工作,同时那少量代码就不需要被测试了。 + +## 本章小结 + +Rust 的这些测试特性,提供到一种指明代码应如何生效,从而确保即使在进行了修改时,其仍继续如预期那样工作的方式。单元测试对库的各个部分进行单独检查,而可对一些私有实现细节进行测试。集成测试则对库的多个部分一起正确运作进行检查,同时他们会使用库的公开 API,以与外部代码使用库的同样方式,对代码进行测试。即使 Rust 的类型系统与所有权规则有助于防止某些种类的代码错误,对于消除与所编写代码预期表现方式有关的逻辑错误,测试仍是必不可少的。 + +下面就来将本章以及前面那些章中所掌握的知识结合起来,在一个项目上练手一下了! diff --git a/src/Ch12_An_IO_Project_Building_a_Command_Line_Program.md b/src/Ch12_An_IO_Project_Building_a_Command_Line_Program.md new file mode 100644 index 0000000..69b522b --- /dev/null +++ b/src/Ch12_An_IO_Project_Building_a_Command_Line_Program.md @@ -0,0 +1,1244 @@ +# 一个文件系统 I/O 项目:构建一个命令行程序 + +这一章是对到目前为止所学到技能的一个回顾,又是对少数几个另外标准库特性的一个探索。这里将构建一个与文件及命令行输入输出进行交互的命令行工具,来练习咱们现在掌握到的一些 Rust 概念。 + +Rust 的速度、安全性、单一二进制可执行程序输出,还有跨平台支持,令其成为创建命令行工具的理想编程语言,那么对于这个项目,这里将构造自己版本的那个经典的命令行搜索工具 `grep` (**g**lobally search a **r**egular **e**xpression and **p**rint,正则表达式全局搜索及打印程序)。在最简单用例中,`grep` 会对某个指定文件,就某个指定字符串而加以搜索。为完成这个目的,`grep` 就会取一个文件路径与一个字符串作为其命令行参数。随后他会读取那个文件,找到那个文件中包含有该字符串参数的那些行,并打印出这些行。 + +在构造这个命令行程序的道路上,这里将展示如何让这个命令行工具,使用到其他命令行工具都会用到的一些终端特性(the terminal features)。这里将读取某个环境变量的值,来允许使用者对这个工具默认行为进行配置。这里还会将错误消息打印到标准错误控制台的流(the standard error console stream, `stderr`),而非打印到标准输出(`stdout`),如此一来,用户就可以将成功的输出重定向到某个文件,而仍能从屏幕上看到错误消息,并有着其他一些好处。 + +名为 Andrew Gallant 的一位 Rust 社区成员,就已经创建了一个特性完整、非常快版本的 `grep`,名叫 [`ripgrep`](https://github.com/BurntSushi/ripgrep)。相比之下,这个版本将相当简单,不过这一章将给到一些掌握诸如 `ripgrep` 这样的真实项目,所需的背景知识。 + +这个 `grep` 项目,将结合至今所掌握的下面几个到目前为止已掌握的概念: + +- 对代码进行组织(使用 [第 7 章](Ch07_Managing_Growing_Projects_with_Packages_Crates_and_Modules.md) 中所掌握的有关模组的知识) +- 对矢量值与字符串的使用(集合,[第 8 章](Ch08_Common_Collections.md)) +- 对错误的处理([第 9 章](Ch09_Error_Handling.md)) +- 在恰当之处使用特质与生命周期([第 10 章](Ch10_Generic_Types_Traits_and_Lifetimes.md)) +- 编写测试([第 11 章](Ch11_Writing_Automated_Tests.md)) + +这里还会简要对闭包、迭代器及特质对象等,进行简要介绍,后面的 [第 13 章](Ch13_Functional_Languages_Features_Iterator_and_Closures.md) 与 [第 17 章](Object_Oriented_Programming_Features_of_Rust.md) 等章节,将详细讲解到这些特性。 + + +## 接收命令行参数 + +现在来与往常一样,使用 `cargo new` 创建一个新的项目。这里将把这个项目,叫做 `minigrep` 来将其区别于或许已在现有系统上有的那个 `grep` 工具。 + +```console +$ cargo new minigrep + Created binary (application) `minigrep` project +$ cd minigrep +``` + +首个任务,即要让 `minigrep` 接收他的两个命令行参数:文件路径与要检索的字符串。那就是,这里打算能够以 `cargo run`,与两个短横线(`--`)来表明接下来的参数,是这个程序的参数,这样的方式,而非 `cargo` 与一个要检索的字符串,及要在其中检索的文件路径的方式来运行这个程序,如下所示: + + +```console +$ cargo run -- searchstring example-filename.txt +``` + +而现在,由 `cargo new` 命令生成的程序,是无法处理给他的参数的。[crates.io](https://crates.io/) 上的一些既有库,可以帮助编写除接收命令行参数的程序,而由于咱们才开始了解这个概念,因此就要咱们自己来实现这项功能。 + +### 读取参数值 + +为开启 `minigrep` 对传给他的命令行参数值的读取,这里将需要在 Rust 标准库中所提供的 `std::env::args` 函数。该函数返回的是那些传递给 `minigrep` 命令行参数的一个迭代器。后面的 [第 13 章](Ch13_Functional_Language_Features_Iterators_and_Closures.md) 就会讲到迭代器。而现在,就只需要知道迭代器的两个细节:迭代器会产生出一些列值,而在某个迭代器上调用 `collect` 方法,就可以将其转换成比如矢量这这样的一个、包含着迭代器产生的全部元素的集合。 + +下面清单 12-1 中的代码,实现了`minigrep` 程序读取全部传递给他的命令行参数,并于随后将这些值收集到一个矢量中。 + +文件名:`src/main.rs` + +```rust +use std::env; + +fn main() { + let args: Vec = env::args().collect(); + dbg! (args); +} +``` + +*清单 12-1:将命令行参数,收集到一个矢量中并把他们打印出来* + + +这里首先使用了一个 `use` 语句,将那个 `std::env` 模组带入到了作用域,如此就可以使用他的 `args` 函数了。请注意这个 `std::env::args` 函数,是嵌套在两个层级的模组中的。如同在 [第 7 章](Ch07_Managing_Growing_Projects_with_Packages_Crates_and_Modules.md#creating-idiomatic-use-path) 处所讨论过的,在那些所需函数是嵌套于多个模组中的情形下,那里就选择将其中的父模组带入到作用域,而非该函数本身。经由这样做,就可以轻易地使用到 `std::env` 中的其他函数了。同时相比于添加 `use std::env::args` 并在随后只使用 `args` 调用这个函数,这样做也不那么含糊其辞,这是由于 `args` 这个名字,可能稍不留意就会被误用为定义在当前模组中的某个函数。 + +> **`args` 函数与无效 Unicode 字符** +> +> 请注意 `std::env::args` 在由任何参数包含了无效 Unicode 字符时,将会中止运行。在程序需要接收包含了无效 Unicode 字符的参数时,就要使用 `std::env::args_os`。那个函数返回的是一个产生出 `OsString` 值,而非 `String` 值的迭代器。由于各个平台上的 `OsString` 值有所区别,且相比使用 `String` 值,`OsString` 使用起来要更为复杂,因此为简化起见,这里使用的是 `std::env::args`。 + +在 `main` 函数的第一行,这里调用了 `env::args`,并立即使用 `collect` 来将其所返回的那个迭代器,转换为一个包含由该迭代器所产生全部值的矢量值。由于使用这个 `collect` 函数,即可创建出许多类别的集合来,因此这里就显示地对 `args` 变量的类型进行了注解,来指明这里要的是一个字符串的矢量。尽管在 Rust 中,极少需要对类型加以注解,不过这个 `collect` 函数就是一个通常需要注解的函数,这是由于 Rust 无法推断出,代码编写者想要的集合类别来。 + +最后,这里使用了调试宏(`dbg!`),打印出那个矢量。下面就来尝试先不带参数运行该代码,尔后再带上两个参数: + +```console +$ cargo run lennyp@vm-manjaro + Compiling minigrep v0.1.0 (/home/lennyp/rust-lang/minigrep) + Finished dev [unoptimized + debuginfo] target(s) in 0.39s + Running `target/debug/minigrep` +[src/main.rs:6] args = [ + "target/debug/minigrep", +] +``` + + +```console +$ cargo run -- 检索字符串😀 demo.txt lennyp@vm-manjaro + Finished dev [unoptimized + debuginfo] target(s) in 0.00s + Running `target/debug/minigrep '检索字符串😀' demo.txt` +[src/main.rs:6] args = [ + "target/debug/minigrep", + "检索字符串😀", + "demo.txt", +] +``` + + +请注意这个矢量中的首个值,即 `"target/debug/minigrep"`,就是这里二进制程序文件的名字。这一点符合了 C 语言中参数清单的行为,让程序运用到其被触发执行的那个名字(this matches the behavior of the arguments list in C, letting programms use the name by which they were invoked in their execution)。在要于消息中打印出程序名字,或根据用于触发该程序运行的何种命令行别名,而对程序行为加以改变这样的情形下,有着对程序名字的访问,通常就比较便利。而对于本章的目的,这里就会忽略这首个参数,而只保存这里所需的那两个参数。 + + +### 将参数值保存在变量中 + + +这个程序此刻就可以访问到被指定为命令行参数的那些值了。现在这里需要将这两个参数的值,保存在变量中,如此就可以在这个程序的整个其余部分,使用到这些值了。在下面清单 12-2 中就要完成这个事情。 + +文件名:`src/main.rs` + +```rust +use std::env; + +fn main() { + let args: Vec = env::args().collect(); + + let query = &args[1]; + let file_path = &args[2]; + + println! ("在文件 {} 中检索:{}", file_path, query); +} +``` + +*清单 12-2:创建两个变量来保存查询参数与文件路径参数* + +与在打印这个矢量时所看到的一样,该程序的名字,占据了那个矢量中 `args[0]` 处的首个值,因此这里是于索引 `1` 处开始参数的。`minigrep` 取的第一个参数,即为这里正检索的字符串,因此这里把到这首个参数的索引,放在了变量 `query` 中。第二个参数将是那个文件路径,因此这里把到那第二个参数的索引,放在了变量 `file_path` 中。 + +这里临时性地将这两个变量的值打印出来,以证实该代码是如打算那样运行。下面就来以参数 `test` 和 `sample.txt`,再次运行这个程序: + +```console +$ cargo run -- test sample.txt lennyp@vm-manjaro + Compiling minigrep v0.1.0 (/home/lennyp/rust-lang/minigrep) + Finished dev [unoptimized + debuginfo] target(s) in 0.35s + Running `target/debug/minigrep test sample.txt` +在文件 sample.txt 中检索:test +``` + +很好,这个程序工作了!所需参数的那些值正被保存到恰当的变量中。后面就要添加一些错误处理,来处理某些潜在的错误情形,诸如在用户未提供参数这样的情况;现在,这里将忽略那样的情况,而是会编写添加文件读取能力的代码。 + +## 读取文件 + +现在就要添加读取那个在 `file_path` 参数中所指定文件的功能了。首先,这里需要一个样本文件来对其进行测试:这里将使用一个有着少量文字、其中多个行均有一些重复文字的文件。下面清单 12-3 这首 Emily Dickinson 的诗歌用起来就会不错!在项目的根目录处创建一个叫做 `poem.txt` 的文件,并敲入这首 “I'm Nobody! Who are you?” 的诗歌。 + +文件名:`poem.txt` + +```txt +I'm nobody! Who are you? +Are you nobody, too? +Then there's a pair of us - don't tell! +They'd banish us, you know. + +How dreary to be somebody! +How public, like a frog +To tell your name the livelong day +To an admiring bog! +``` + +*清单 12-3:一首 Emily Dickinson 的诗歌成就了一个良好的测试用例* + + +有了这个文本后,就要编辑 `src/main.rs` 并添加读取该文件的代码了,如下清单 12-4 中所示。 + +文件名:`src/main.rs` + +```rust +use std::env; +use std::fs; + +fn main() { + // --跳过代码-- + println! ("在文件 {} 中检索:{}", file_path, query); + + let contents = fs::read_to_string(file_path) + .expect("应能读取这个这个文件"); + + println! ("有着文本:\n{}", contents); +} +``` + +*清单 12-4:对由第二个参数所指定的文件内容进行读取* + +首先,这里使用了一个 `use` 语句,将标准库的一个相对部分(a relevant part)带入进来:这里需要 `std::fs` 来对文件进行处理。 + +在 `main` 函数中,那个新的 `fs::read_to_string` 取了其中的 `file_path` 做参数,打开那个文件,并返回一个该文件内容的 `std::io::Result` 类型值。 + +在那之后,这里再次添加了一个临时的、于该文件被读取之后打印 `contents` 值的 `println!` 语句,因此这里就该程序到此在运行而进行检查了。 + +下面就来以任意字符串作为第一个参数(由于这里尚未实现检索的部分),并以那个 `poem.txt` 文件作为第二个参数,运行这段代码: + +```rust +$ cargo run -- the poem.txt lennyp@vm-manjaro + Compiling minigrep v0.1.0 (/home/lennyp/rust-lang/minigrep) + Finished dev [unoptimized + debuginfo] target(s) in 0.36s + Running `target/debug/minigrep the poem.txt` +在文件 poem.txt 中检索:the +有着文本: +I'm nobody! Who are you? +Are you nobody, too? +Then there's a pair of us - don't tell! +They'd banish us, you know. + +How dreary to be somebody! +How public, like a frog +To tell your name the livelong day +To an admiring bog! + +``` + +很好!这代码就读取并于随后打印出了那个文件的内容。但这代码有着少数几个缺陷。此时的这个 `main` 函数,有着多重义务:一般来讲,在每个函数只负责一件事情时,那么他们就是些更为清晰明了,并更易于维护的函数了。另一问题则是这里没有尽可能地对错误进行处理。这个程序还很小,因此这些缺陷就不是什么大问题,不过随着程序变大,就会变得更加难于彻底修复这些缺陷。在开发某个程序时,由于重构数量较少的代码要容易得多,因此尽早开始重构,是一种良好实践。接下来就会干这件事 -- 重构。 + + +## 对代码进行重构来改进模组性与错误处理 + +为改进这个程序,这里就要修复与该程序结构及其处理潜在错误方式有关的四个问题。首先,这里的 `main` 函数现在执行了两个任务:他对参数进行解析并读取文件。随着程序的增长,这个 `main` 函数所处理的独立任务数目将不断增加。而随着函数不断获得其任务,就会变得更加难于推理,更难于对其进行测试,以及更难于在不破坏其各个部分的情况下对其进行修改。那么最后就要将功能拆分,从而每个函数负责一项任务。 + +这个问题同样联系着第二个问题:尽管这里的 `query` 与 `file_path` 属于这个程序的配置性变量,而像 `contents` 这样的变量则被用于执行该程序的逻辑处理。这个 `main` 函数变得越长,那么这里就会将更多的变量引入到作用域;在作用域中的变量越多,那么就会越难对各个变量的目的保持追踪。因此就最好将这些配置变量,分组到某个结构体中,而令到他们的目的明确。 + +第三个问题则是,在读取那个文件失败时,这里使用了 `expect` 将一条错误消息打印处理,而该错误消息只会打印 “应能读取这个这个文件。” 文件读取以多种方式失败:比如那个文件可能没有,或可能没有打开他的权限。此时,无论何种情形,这里都将打印同样的错误消息,这样并不会给到用户任何信息! + +第四,这里重复地使用了 `expect` 来处理不同重复,而在用户未指定足够参数时,他们就会得到一个并不会清楚解释问题原因、 Rust 的 `index out of bounds` 错误。若全部错误处理代码都在一个地方,那么就最好了,这样在错误处理代码需要修改时,那么以后的维护者就只有一个地方来查阅代码。将全部错误处理代码放在一处,还将确保这里打印的消息,是会对终端用户有意义的那些消息。 + +下面就来通过对这里的项目进行重构,来解决这四个问题。 + + +### 二进制程序项目的关注点分离 + +**Separation of Concerns for Binary Projects** + +将多重任务分配给那个 `main` 函数方面的组织性问题,常见于许多二进制项目。由此 Rust 社区业已开发了在 `main` 开始变得大型起来时,将二进制程序单个关注点进行剥离的守则。这个剥离单独关注点的过程,有着以下几个步骤: + +- 将程序剥离为一个 `main.rs` 与一个 `lib.rs`,并将程序逻辑迁移到 `lib.rs`; +- 由于命令行解析逻辑不大,因此他仍然留在 `main.rs` 中; +- 而在命令行解析逻辑开始变得复杂的时候,就要将其从 `main.rs` 提取出来,并将其迁移到 `lib.rs`。 + + +那么在经历了剥离单独关注点这个过程后,留在这个 `main` 函数中的任务就应局限于下面这些了: + +- 以那些参数值,对命令行解析逻辑进行调用; +- 建立起全部其他配置; +- 调用 `lib.rs` 中的某个 `run` 函数; +- 在 `run` 返回了某个错误时,对该错误加以处理。 + + +这种模式,是有关关注点分离的:`main.rs` 对运行程序加以处理,而 `lib.rs` 处理的则是手头任务的全部逻辑。由于无法对 `main` 函数直接进行测试,因此这种结构通过将全部程序逻辑移入到 `lib.rs` 种的函数,而允许对他们进行测试了。保留在 `main.rs` 种的代码,将足够小到通过过目一下,就可以验证其正确性。下面就来依照这些步骤,重制这个程序。 + +### 提取参数解析器 + +这里将把解析参数的功能,提取到一个 `main` 会调用到的函数种,从而把命令行解析逻辑(the command line parsing logic),迁移到 `src/lib.rs`。下面清单 12-5 就给出了调用了一个新函数 `parse_config` 的 `main` 新开头,此刻这里将把这个新函数 `parse_config` 定义在 `src/main.rs` 中。 + +文件名:`src/main.rs` + +```rust +use std::env; +use std::fs; + +fn main() { + let args: Vec = env::args().collect(); + + let (query, file_path) = parse_config(&args); + // --跳过代码-- +} + +fn parse_config(args: &[String]) -> (&str, &str) { + let query = &args[1]; + let file_path = &args[2]; + + (query, file_path) +} +``` + +*清单 12-5:自 `main` 中提取一个 `parse_config` 函数* + +这里仍是将那些命令行参数,收集到一个矢量中,而与在 `main` 函数中,将索引 `1` 处的参数值指派给变量 `query`,及将索引 `2` 处的参数值指派给变量 `file_path` 不同,这里将那整个矢量,传递给了 `parse_config` 函数。这个 `parse_config` 函数随后就持有了确定哪个参数进到哪个变量,及将这些值传回到 `main` 的逻辑。在 `main` 中,这里仍创建了 `query` 与 `file_path` 两个变量,但 `main` 不再具有确定命令行参数与变量如何对应起来的义务了。 + +对于这里的小型程序,这项重制可能看起来矫枉过正了,但这里是正在以小的、渐进式的步骤进行重构。在做出这项修改后,就要再次运行这个程序来验证参数解析仍会运作。频繁检查所取得的进展,从而在有问题发生时,帮助识别出问题的原因,总是不错的做法。 + +### 对配置值进行分组 + +**Grouping Configuration Values** + +这里可以进一步对那个 `parse_config` 函数加以改进。此刻,这里返回的是个元组,然后随后又立即将那个元素,再次拆分为了单独的一些部分。这便是个或许这里尚未有着恰当抽象的表征。 + +有着改进空间的另一指标,便是 `parse_config` 的 `config` 部分,这暗示了这里返回的两个值是有关联的,且他俩都是某个配置值的组成部分。由于这里是将这两个值编组为了元组,而并未以数据结构(the structure of the data)方式分组,因此当前并未揭示出这层意义来;那么这里就要将这两个值,放入到某种结构体中,并分别给到该结构体的两个字段有意义的名字。这样做将让此代码的未来维护者更加容易理解,不同值直接是怎样相互联系起来的,以及他们各自的目的为何。 + +下面清单 12-6 就给出了对这个 `parse_config` 函数的改进。 + +文件名:`src/main.rs` + +```rust +use std::env; +use std::fs; + +fn main() { + let args: Vec = env::args().collect(); + + let config = parse_config(&args); + + println! ("在文件 {} 中检索:{}", config.file_path, config.query); + + let contents = fs::read_to_string(config.file_path) + .expect("应能读取这个这个文件。"); + + // --跳过代码-- +} + +struct Config { + query: String, + file_path: String, +} + +fn parse_config(args: &[String]) -> Config { + let query = args[1].clone(); + let file_path = args[2].clone(); + + Config { query, file_path } +} +``` + +*清单 12-6:将 `parse_config` 重构为返回 `Config` 结构体的实例* + +这里就已添加了一个名为 `Config`、定义为有着名为 `query` 与 `file_path` 字段的一个结构体。现在 `parse_config` 的签名,就表示其返回了一个 `Config` 值。而在那个 `parse_config` 的函数体中,之前曾于其中返回引用了 `args` 中那些 `String` 值的字符串切片,现在则定义了 `Config` 来包含持有所有权的一些 `String` 值。`main` 中的那个 `args` 变量,即为那些参数值的所有者,且仅允许那个 `parse_config` 函数借用那些参数值,这就意味着在 `Config` 尝试取得 `args` 中那些值的所有权时,就会破坏 Rust 的借用规则。 + +对于 `String` 数据的管理,是可以采用多种方式的;而其中最容易的途径,当然虽不那么高效,便是在这些值上调用 `clone` 方法。这将构造出为那个 `Config` 实例所持有的该数据的完整拷贝,相比于存储到那个字符串数据的一个引用,这样做会消耗更多时间与内存。但对数据进行克隆,由于就不必对引用的生命周期加以管理,而也会令到这里的代码相当直接;在这样的情形下,为获得简单性而舍弃一点小小的性能,即是有价值的一种取舍。 + +> **使用 `clone` 上的权衡** +> +> 在相当多 Rust 公民中间,有着由于 `clone` 的运行时开销,而避免使用其来修复所有权问题的这种倾向。在接下来的 [第 13 章](Ch13_Functional_Language_Features_Iterators_and_Closures.md) 中,就会掌握到在这类情形下,怎样使用别的一些高效的方法。而现在,则由于仅会构造这些拷贝一次,且文件路径与查询字串都相当小,那么对少量字符串加以拷贝,以继续进行关注点分离过程,是可以的。相比于在起步阶段就尝试对代码进行超优化(hyperoptimize),更好的选择当然是有一个不那么高效的运行的程序了。而随着对 Rust 日益熟练,就会更容易以最为高效的解决办法开始,而此刻,调用 `clone` 是相当可接受的做法。 + +这里已对 `main` 进行了更新,如此其就把由 `parse_config` 所返回的那个 `Config` 实例,置于一个名为 `config` 的变量中,同时这里更新了之前使用了 `query` 与 `file_path` 两个单独变量的代码,如此该代码现在使用的就是那个 `Config` 结构体上的字段了。 + +现在这里的代码,就更清楚地反应了 `query` 与 `file_path` 二者是相关的,以及他们的目的是要配置该程序将如何运作。任何用到这两个值的代码,就都知道了要在那个 `config` 实例中,在以其目的而取名的字段中找到他们。 + + +### 给 `Config` 创建一个构造器 + +到这里,就已把负责解析命令行参数的逻辑,从 `main` 中提取了出来,而将其放在了那个 `parse_config` 函数中。这样做有助于看出其中 `query` 与 `file_path` 两个值是相关的,而那层联系应在这里的代码中体现出来。随后这里添加了一个 `Config` 的结构体,来命名 `query` 与 `file_path` 这种关联目的,并能够将这些值的名字作为结构体字段,自这个 `parse_config` 函数而加以返回。 + +那么既然这个 `parse_config` 函数的目的是要创建一个 `Config` 的实例,那么就可以将 `parse_config` 从一个普通函数,修改为一个命名为 `new` 的、与 `Config` 结构体关联起来的函数。进行这一修改,将令到代码更加符合 Rust 语言习惯。对于标准库中的那些类型,譬如 `String`, 就可以通过调用 `String::new` 创建出他们的实例来。与此类似,通过将 `parse_config` 修改为与 `Config` 关联起来的 `new` 函数,就可以通过调用 `Config::new` 而创建出 `Config` 的实例来。下面清单 12-7 给出了这里需要做出的修改。 + +文件名:`src/main.rs` + +```rust +use std::env; +use std::fs; + +fn main() { + let args: Vec = env::args().collect(); + + let config = Config::new(&args); + // --跳过代码-- +} + +// --跳过代码-- + +impl Config { + fn new(args: &[String]) -> Config { + let query = args[1].clone(); + let file_path = args[2].clone(); + + Config { query, file_path } + } +} +``` + +*清单 12-7:把 `parse_config` 修改为 `Config::new`* + + +这里已将其中曾对 `parse_config` 进行调用的 `main`,更新为了调用 `Config::new`。已将 `parse_config` 这个名字,修改为了 `new`,并将其移入到了一个 `impl` 代码块里头,而正是这个 `impl` 代码块,把这个 `new` 函数,与 `Config` 关联了起来。请尝试再次编译此代码,来确保其的运作。 + +### 对错误处理进行修复 + +现在就要开始修复这里的错误处理了。回顾到之前在尝试访问 `args` 矢量中索引 `1` 或索引 `2` 处的值,若该矢量包含了少于三个条目,那么就会导致该程序终止运行。请以不带任何参数运行这个程序;他就会看起来像下面这样: + +```console +cargo run lennyp@vm-manjaro + Finished dev [unoptimized + debuginfo] target(s) in 0.00s + Running `target/debug/minigrep` +thread 'main' panicked at 'index out of bounds: the len is 1 but the index is 1', src/main.rs:24:21 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +``` + +其中的行 `index out of bounds: the len is 1 but the index is 1` 是一条留给代码编写者的错误消息。该消息不会帮到终端用户,搞明白又该怎样做。现在就要来修复这个问题。 + + +**改进错误消息** + +下面的清单 12-8 中,这里于那个 `new` 函数中,在访问索引 `1` 与 `2` 之前,添加一个验证那个切片是否足够长的检查。若该切片没有足够长,那么这个程序就会终止运行,并显示出一个更好的错误消息。 + + +文件名:`src/main.rs` + +```rust + // --跳过代码-- + fn new(args: &[String]) -> Config { + if args.len() < 3 { + panic! ("参数数量不足"); + } + // --跳过代码-- +``` + +*清单 12-8:添加一个参数个数的检查* + + +此代码与 [清单 9-13 中曾编写过的 `Guess::new` 函数](Ch09_Error_Handling.md#creating-custom-types-for-validation) 类似,其中在那个 `value` 参数超出有效值边界时,就调用了 `panic!` 宏。这里没有检查值的边界,而是就 `args` 的长度至少为 `3` 进行了检查,进而该函数的其余部分,就可以在此条件已满足的假定下运作了。在 `args` 所拥有的条目少于三个时,此条件便为真,进而这里就会条约那个 `panic!` 宏,来立即结束这个程序。 + +有了`new` 中的这些额外少数几行,下面就不带任何参数地再度运行这个程序,来看看现在错误看起来如何: + +```console +$ cargo run lennyp@vm-manjaro + Compiling minigrep v0.1.0 (/home/lennyp/rust-lang/minigrep) + Finished dev [unoptimized + debuginfo] target(s) in 0.57s + Running `target/debug/minigrep` +thread 'main' panicked at '参数数量不足', src/main.rs:25:13 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +``` + +此输出好了一些:现在这里就有了一个合理的错误消息了。不过,这里还有一些不希望给到用户的无关信息。或许运用曾在清单 9-13 中用到的那种技巧,并非这里要用到的最佳技巧:到 `panic!` 的调用,相比于用法方面的问题,是更适合于编程方面的问题的,如同 [第 9 章中所讨论的那样](Ch09_Error_Handling.md#guidelines-for-error-handling)。相反,这里将使用之前在第 9 章中曾学到的另一项技能 -- [返回一个 `Result`](Ch09_Error_Handling.md#recoverable-errors-with-result),以表示成功执行成功或是出错。 + + +**返回一个 `Result` 值,而非调用 `panic!` 宏** + +与上面调用 `panic!` 相比,这里可返回将包含成功情形下的 `Config` 实例,及在错误情形下对问题进行描述的 `Result` 值。由于许多编程者都期望 `new` 函数绝不失败,因此这里还将把该函数的名字,从 `new` 修改为 `build`。在 `Config::build` 与 `main` 通信时,这里就可以使用这个 `Result` 类型,来发出存在某个问题的信号了。接下来就可以将 `main` 修改为将 `Err` 变种,转换为一个对程序使用者来说更实际的错误消息,而不再带有那些因调用 `panic!` 宏,而引发的前后有关 `thread 'main'` 及 `RUST_BACKTRACE` 的字眼。 + +下面清单 12-9 给出了对现在调用的 `Config::Build` 函数返回值,以及该函数体需要一个返回 `Result` 值,而需要做出的修改。请注意在下一代码清单中,同时更新 `main` 之前,此代码是不会编译的。 + +文件名:`src/main.rs` + +```rust +impl Config { + fn build(args: &[String]) -> Result { + if args.len() < 3 { + return Err("参数数量不足"); + } + + let query = args[1].clone(); + let file_path = args[2].clone(); + + Ok(Config { query, file_path }) + } +} +``` + +*清单 12-9:自 `Config::build` 返回一个 `Result`* + +这里的 `build` 函数,返回的是一个在成功情形下带有 `Config` 实例,在错误情况下有着一个 `&'static str` 的 `Result` 值。这里的错误值将始终是有种 `'static` 生命周期的字符串字面值。 + +在该函数的函数体中,这里完成了两处改变:与在使用者未传递足够参数时调用 `panic!` 宏不同,现在这里返回的是一个 `Err` 值,同时这里已将那个 `Config` 的返回值,封装在了一个 `Ok` 中。这些修改就令到该函数与其新的类型签名相符了。 + +从 `Config::build` 返回一个 `Err` 的值,就允许 `main` 函数对自那个 `build` 函数返回的 `Result` 值加以处理,进而在错误情形下,更明确的退出该程序进程。 + + +**对 `Config::build` 进行调用并对错误进行处理** + +为对错误情形加以处理,并打印出用户友好的消息,这里就需要更新 `main`,以处理由 `Config::build` 所返回的那个 `Result` 值,如下清单 12-10 中所示。这里还将承担在不使用 `panic!` 宏后,以一个非零错误代码退出这个命令行工具的任务,并要亲自实现这个任务。非零的退出状态,是一条用于向调用咱们编写的程序的进程,发出程序以错误状态退出信号的约定。 + +文件名:`src/main.rs` + +```rust +use std::process; + +fn main() { + let args: Vec = env::args().collect(); + + let config = Config::build(&args).unwrap_or_else(|err| { + println! ("解析参数时遇到问题:{err}"); + process::exit(1); + }); + + // --跳过代码-- +``` + +*清单 12-10:在构建一个 `Config` 失败时以一个错误代码退出* + +在此代码清单中,业已使用一个尚未详细讲过的方法:`unwrap_or_else`,这是个由标准库定义在 `Result` 上的方法。使用 `unwrap_or_else`,就允许定义出一些定制的、非 `panic!` 的错误处理。在由 `Config::build` 返回的那个 `Result` 为一个 `Ok` 的值时,该方法的行为就跟 `unwrap` 类似:其返回 `Ok` 所封装的那个内部值。不过在返回的 `Result` 是个 `Err` 时,该方法就会调用那个 *闭包(closure)* 中的代码,而该闭包代码,则是这里所定义、并将其作为一个参数,而传递给 `unwrap_or_else` 的一个匿名函数(an anonymous function)。在 [第 13 章](Ch13_Functional_Language_Features_Iterators_and_Closures.md),会更深入地讲到闭包特性。而此刻,仅需要明白 `unwrap_or_else` 将把那个 `Err` 的内部值,即此示例中的那个此前于清单 12-9 中所添加的静态字符串 `参数数量不足`,传递到这里的闭包中,那个位处于两个竖直管线之间的参数里。那么闭包中的代码,随后就可以在其运行的时候,使用这个 `err` 值了。 + +这里已添加了一个新的、将标准库的 `process` 带入到作用域中的 `use` 代码行。而将在错误情形下运行的那闭包中的代码,则只有两行:这里打印了那个 `err` 值,并于随后对 `process::exit` 进行了调用。这个 `process::exit` 函数,将立即停止该程序,并返回作为推出状态代码传递的那个数字。这与清单 12-8 中曾使用过的基于 `panic!` 的处理类似,只不过这里不在会受到先前全部的那些额外输出了。现在来尝试运行一下: + +```console +$ cargo run  ✔ + Compiling minigrep v0.1.0 (/home/peng/rust-lang/minigrep) + Finished dev [unoptimized + debuginfo] target(s) in 0.66s + Running `target/debug/minigrep` +解析参数时遇到问题:参数数量不足 +``` + +棒极了!这样的输出对于程序使用者来说,就友好多了。 + +### 从 `main` 中提取出逻辑 + +**Extract Logic from `main`** + +既然这里已经完成了对配置解析的重构,那么就来转向该程序的逻辑部分。如同在 [“二进制项目的关注点分离”](#separation-of-concerns-for-binary-projects) 小节中所指出的,这里将提取出一个保有当前在这个 `main` 函数中,不涉及到建立配置与错误处理的全部逻辑的 `run` 函数。在完成此过程时,`main` 就变得简洁而易于经由目测得以验证,并能编写出全部其他逻辑的测试。 + +下面清单 12-11 给出了那个被提取出的 `run` 函数。此时,这里只进行小的、渐进式的提出该函数的改进。此时仍将该函数定义在 `src/main.rs` 中。 + +文件名:`src/main.rs` + +```rust +fn main() { + // --跳过代码-- + println! ("在文件 {} 中检索:{}", config.file_path, config.query); + + run(config); +} + +fn run(config: Config) { + let contents = fs::read_to_string(config.file_path) + .expect("应能读取这个这个文件。"); + + println! ("有着文本:\n{}", contents); +} + +// --跳过代码-- +``` + +*清单 12-11:提取出一个包含了程序逻辑其余部分的 `run` 函数* + +这个 `run` 函数现在就包含了 `main` 中自读取文件开始的全部剩余逻辑。该 `run` 函数取了那个 `Config` 实例,作为一个参数。 + + +### 从那个 `run` 函数返回错误 + +在其余程序逻辑分离到这个 `run` 函数之下,就可以改进错误处理了,就跟在清单 12-9 中对 `Config::build` 所做的那样。与其经由调用 `expect` 而允许该程序终止允许,这个 `run` 函数将在发生某种错误时,返回一个 `Result` 类型的值。这样做就允许咱们进一步把有关错误处理的逻辑,以用户友好的方式整合到 `main` 中。下面清单 12-12 给出了这里需要对 `run` 的签名及函数体做出的修改。 + +文件名:`src/main.rs` + +```rust +// --跳过代码-- +use std::error::Error; + +// --跳过代码-- + +fn run(config: Config) -> Result<(), Box>{ + let contents = fs::read_to_string(config.file_path)?; + + println! ("有着文本:\n{}", contents); + + Ok(()) +} +``` + +**清单 12-12:将 `run` 函数修改为返回 `Result`** + +这里做出了三处显著修改。首先,这里把这个 `run` 函数的返回值类型,修改为了 `Result<(), Box>`。此函数先前返回的是单元类型(the unit type),`()`,而这里则将其保留作了 `Ok` 情形中返回的值。 + +而对于错误类型,这里使用了那个特质对象(the `trait object`) `Box` (且这里已在代码顶部,使用一条 `use` 语句,而已将 `std::error::Error` 带入到了作用域)。这里将在 [第 17 章](Ch17_Object_Oriented_Programming_Features_of_Rust.md) 讲到特质对象。至于现在,则只要了解那个 `Box<(), Error>` 表示该函数将返回一个实现了 `Error` 特质的类型,而这里不必指明该返回值将是何种特定类型。这就给到了在不同错误情形下,返回值可能为不同类型的灵活性。这个 `dyn` 关键字,是 “动态(dynamic)” 的缩写。 + +其次,这里通过使用那个 `?` 运算符,而已将到 `expect` 的调用移除,正如在 [第 9 章](Ch09_Error_Handling.md#a-shortcut-for-propagating-errors-the-question-mark-operator) 中曾讲到过的那样。与在某个错误上 `panic!` 不同,`?` 将返回把当前函数中的错误值,返回给调用者来加以处理。 + +第三,这个 `run` 函数现在会在成功情形下返回一个 `Ok` 值。在函数签名中,这里已将该 `run` 函数的成功类型定义为 `()`,这就意味着需要将那个单元值,封装在 `Ok` 值中。乍一看这个 `Ok(())` 语法或许有点陌生,不过像这样使用 `()`,则正是一种表明这里调用 `run` 只是为了其副作用的方式;他不会返回一个这里所需要的值。 + +在运行此代码时,此代码将编译,不过将显示出一条告警: + +```console +$ cargo run the poem.txt lennyp@vm-manjaro + Compiling minigrep v0.1.0 (/home/lennyp/rust-lang/minigrep) +warning: unused `Result` that must be used + --> src/main.rs:16:5 + | +16 | run(config); + | ^^^^^^^^^^^^ + | + = note: `#[warn(unused_must_use)]` on by default + = note: this `Result` may be an `Err` variant, which should be handled + +warning: `minigrep` (bin "minigrep") generated 1 warning + Finished dev [unoptimized + debuginfo] target(s) in 1.94s + Running `target/debug/minigrep the poem.txt` +在文件 poem.txt 中检索:the +有着文本: +I'm nobody! Who are you? +Are you nobody, too? +Then there's a pair of us - don't tell! +They'd banish us, you know. + +How dreary to be somebody! +How public, like a frog +To tell your name the livelong day +To an admiring bog! + +``` + +Rust 告诉咱们,这里的代码忽略了那个 `Result` 值,而该 `Result` 值可能表示发生了某个错误。而这里没有对到底有无错误进行检查,同时编译器提醒了,这里或许是要有一些错误处理代码!下面就来纠正这个问题。 + +### 在 `main` 中对返回自 `run` 的错误进行处理 + +这类就要对错误加以检查,并是要与代码清单 12-10 中曾用到的类似技巧,不过要以些许不同的方式,对这些错误加以处理: + + +文件名:`src/main.rs` + +```rust +fn main() { + // --跳过代码-- + + println! ("在文件 {} 中检索:{}", config.file_path, config.query); + + if let Err(e) = run(config) { + println! ("应用程序错误:{e}"); + process::exit(1); + } +} +``` + +这里是要了 `if let` 而非 `unwrap_or_else`,来对 `run` 是否返回一个 `Err` 值加以检查,并在 `run` 确实返回了一个 `Err` 值时,调用 `process::exit(1)`。这个 `run` 函数并未返回一个,这里所要以与`Config::build` 返回的那个 `Config` 实例同样方式,而去 `unwrap` 的值,这里只关心的是探测到某个错误,因此这里就不需要 `unwrap_or_else` 来返回那个解封装值,亦即这里的 `()`。 + +其中的 `if let` 与 `unwrap_or_else` 两个函数的函数体,在成功及失败两种情形下是同样的:这里都打印出错误并退出程序。 + + +### 将代码分离到库代码箱 + +到现在这个 `minigrep` 项目看起来就不错了!现在就要拆分这个 `src/main.rs` 文件,并将一些代码放入到 `src/lib.rs` 文件。那样就可以对代码加以测试,并有了一个有着更少职责的 `src/main.rs` 文件。 + +接下来就要将那些非 `main` 函数的代码,从 `src/main.rs` 迁移到 `src/lib.rs`: + +- 那个 `run` 函数的定义; +- 相关的 `use` 语句; +- `Config` 结构体的定义; +- 其中 `Config::build` 函数的定义。 + + +那么 `src/lib.rs` 的内容,就应包含下面清单 12-13 中所显示的那些签名(这里出于简洁考虑,已省略这些函数的函数体)。请注意在清单 12-14 中修改 `src/main.rs` 之前,这还不会编译。 + +文件名:`src/lib.rs` + +```rust +use std::error::Error; +use std::fs; + +pub struct Config { + pub query: String, + pub file_path: String, +} + +impl Config { + pub fn build(args: &[String]) -> Result { + // --跳过代码-- + } +} + +pub fn run(config: Config) -> Result<(), Box>{ + // --跳过代码-- +} +``` + +*清单 12-13:将 `Config` 与 `run` 迁移到 `src/lib.rs` 中* + +这里业已大量使用到那个 `pub` 关键字:在 `Config` 上,在其字段与其 `build` 方法上,以及在那个 `run` 函数上。现在这里就有了一个带有可测试公共 API 的库代码箱了! + +现在就需要把那些已迁移到 `src/lib.rs` 的代码,带入到 `src/main.rs` 中二进制代码箱的作用域中了,如下清单 12-14 中所示。 + +文件名:`src/main.rs` + +```rust +use std::env; +use std::process; + +use minigrep::Config; + +fn main() { + // --跳过代码-- + if let Err(e) = minigrep::run(config) { + // --跳过代码-- + } +} +``` + +*清单 12-14:在 `src/main.rs` 中使用 `minigrep` 库代码箱* + +这里添加了一个 `use minigrep::Config` 的语句行,来将这个 `Config` 类型,从那个库代码箱,带入到这个二进制代码箱的作用域中,同时把这里的代码箱名字,作为了那个 `run` 函数的前缀。那么现在这全部功能,就应联系起来并生效了。使用 `cargo run` 运行这个程序,并确保所有东西都正确运作。 + +咦!这可是干了很多活了,还好现在已经为将来的成功做好了准备。现在处理错误就容易多了,同时令到代码更具模块性。从现在开始,几乎咱们的全部工作,就将在 `src/lib.rs` 完成了。 + +下面就来通过运用现在这种新发现的模组性优势,完成一些对于早先不具模组性代码较难实现,而对这新代码却易于实现的事情。 + + +## 在测试驱动开发方法论下,开发出库的功能 + +既然已将业务逻辑提取到了 `src/lib.rs` 中,而将参数收集与错误处理留在 `src/main.rs` 中,那么编写这里代码核心功能的测试,就容易多了。这里可直接以不同参数来调用那些函数,并在不必从命令行调用这里二进制程序之下,对他们的返回值加以检查。 + +在本小节中,这里将按照以下步骤,运用测试驱动开发流程(the test-driven development(TDD) process),把搜索逻辑添加到这个 `minigrep` 程序: + +1. 编写一个会失败的测试并加以运行,从而确保其会以所设想的原因失败; +2. 编写或修改仅足够的代码,来令到新的测试通过; +3. 对刚添加或修改过的代码加以重构,并确保那些测试继续通过; +4. 重复步骤 `1` 开始的上述步骤。 + +尽管这只是众多编写软件方式之一,TDD 是可以推动代码设计的。在编写令到测试通过的代码之前就编写测试,有助于维持贯穿整个开发过程中,较高程度的测试覆盖面。 + +这里将以测试驱动具体完成搜索出文件内容中查询字符串,以及产生出与该查询匹配的行清单两个功能的实现。这里将把此功能,添加在一个叫做 `search` 的函数里。 + + +### 编写一个失败测试 + +由于不再需要 `src/lib.rs` 与 `src/main.rs` 中的那些,曾用于对该程序行为加以检查的 `println!` 语句,因此这里就要将其移出掉。随后,就要在 `src/lib.rs` 中,添加带有一个测试函数的 `tests` 模组,就跟曾在 [第 11 章](Ch11_Writing_Automated_Tests.md#the-anatomy-of-a-test-function) 曾做过的那样。该测试函数指明了这里所打算的这个 `search` 函数要有的行为:他将取得一个查询字串,与要搜索的文本,同时他将只返回搜索文本中,包含了查询字串的那些行。下面清单 12-15 给出了这个测试,该清单尚不会编译。 + +文件名:`src/lib.rs` + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn one_result() { + let query = "duct"; + let contents = "\ + Rust: + safe, fast, productive. + Pick three."; + + assert_eq! (vec! ["safe, fast, productive."], search(query, contents)); + } +} +``` + +*清单 12-15:创建出一个这里所期望有的那个 `search` 函数的失败测试* + +这个测试搜索的是字符串 `"duct"`。而这里正搜索的文本是三个行,三个行中只有一行包含了 `"duct"`(请注意那第一个双引号之后的反斜杠`\`,是告诉 Rust 不要把另起一行字符,放在这个字符串字面值内容的开头)。这里就那个 `search` 函数的返回值,包含了这里所预计的那唯一行进行了断言。 + +由于这个测试现在甚至不会编译,因此这里尚不能运行这个测试而看到其失败:那个 `search` 函数还不存在!按照 TDD 的各项原则,这里将通过只添加这个 `search` 函数的始终返回某个空矢量值定义,而足够令到这个测试编译并运行的一些代码,如下清单 12-16 中所示。随后该测试将编译,并由于空矢量值不与包含了行 `"safe, fast, productive."` 的矢量匹配而失败。 + +文件名:`src/lib.rs` + +```rust +pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str>{ + vec! [] +} +``` + +*清单 12-16:定义出一个刚好让这里测试编译的那个 `search` 函数来* + +请注意这里需要在 `search` 的函数签名中,定义一个显式的生命周期 `'a`,并在 `contents` 参数与返回值上,使用那个生命周期。回顾 [第 10 章](Ch10_Generic_Types_Traits_and_Lifetimes.md#validating-references-with-lifetimes) 中讲到,这些生命周期参数指明了哪个参数生命周期,是与返回值生命周期联系起来的。在这个示例中,这就表示那个返回的矢量,应包含引用了参数 `contents` (而非参数 `query`)的一些切片的字符串切片。 + +也就是说,这里告诉 Rust,由 `search` 函数返回的数据,将存活到与传递给那个 `search` 函数的、在 `contents` 参数中数据同样长时间。这是相当重要的!*为* 某个切片所引用的数据,需要在该引用有效期间保持有效;若编译器假定这里是在构造 `query` 而非 `contents` 的字符串切片,那么他就会执行错误地安全性检查。 + +而在忘掉了这些生命周期注解并尝试编译该函数时,就会得到下面这个错误: + + +```console +$ cargo build lennyp@vm-manjaro + Compiling minigrep v0.1.0 (/home/lennyp/rust-lang/minigrep) +error[E0106]: missing lifetime specifier + --> src/lib.rs:35:51 + | +35 | pub fn search(query: &str, contents: &str) -> Vec<&str>{ + | ---- ---- ^ expected named lifetime parameter + | + = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `query` or `contents` +help: consider introducing a named lifetime parameter + | +35 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str>{ + | ++++ ++ ++ ++ + +For more information about this error, try `rustc --explain E0106`. +error: could not compile `minigrep` due to previous error +``` + +Rust 是不可能明白,这里需要的到底是两个参数中哪一个的,因此这里就需要显式地告诉 Rust。而由于 `contents` 正是那个包含了这里全部文本的参数,而这里打算返回的,就是与那个文本匹配的部分,因此这里清楚 `contents` 就应是要运用生命周期语法,将其与返回值联系起来的那个参数。 + +别的编程语言并不会要求在函数签名中,将参数与返回值联系起来,但随着时间的推移,这样的实践将变得容易起来。或许你会将这个示例,与第 10 章中的 [“使用生命周期对引用进行验证” 小节](Ch10_Generic_Types_Traits_and_Lifetimes.md#validating-references-with-lifetimes) 加以比较。 + +现在来运行测试: + +```console +$ cargo test 12m 0s lennyp@vm-manjaro + Finished test [unoptimized + debuginfo] target(s) in 0.00s + Running unittests src/lib.rs (target/debug/deps/minigrep-7d3f5b041202a66e) + +running 1 test +test tests::one_result ... FAILED + +failures: + +---- tests::one_result stdout ---- +thread 'tests::one_result' panicked at 'assertion failed: `(left == right)` + left: `["safe, fast, productive."]`, + right: `[]`', src/lib.rs:51:9 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + tests::one_result + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +error: test failed, to rerun pass '--lib' +``` + +相当棒,这个测试失败了,就如这里的预期一样。接下来就要让这个测试通过! + + +### 编写让测试通过的代码 + +此刻,由于这里始终返回一个空的矢量值,导致这里的测试失败。要修复这个测试失败并实现 `search`,这里的程序就需要遵循下面这些步骤: + +- 对那个内容的各个行加以迭代; +- 检查该行是否包含这里的查询字串; +- 在包含查询字串时,将该行添加到这里正要返回的值清单; +- 在不包含查询字串时,就什么也不做; +- 返回匹配结果的清单。 + +下面就来逐一完成各个步骤,从那些文本行的迭代开始。 + + +**使用 `lines` 方法对文本行进行遍历** + +Rust 有着一个用于处理字符串一行行迭代的有用方法,其被方便地命名为了 `lines`,如下清单 12-17 中所示的那样运作。请注意下面的代码尚不会编译。 + +文件名:`src/lib.rs` + +```rust +pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str>{ + for line in contents.lines() { + // 对单个文本行进行一些操作 + } +} +``` + +*清单 12-17:遍历 `contents` 中的各行* + +这个 `lines` 方法,返回的是个迭代器(an iterator)。在 [第 13 章](Ch13_Functional_Language_Features_Iterators_and_Closures.md#processing-a-series-of-items-with-iterators) 中,就会讲到迭代器,不过回顾一下 [清单 3-5](Ch03_Common_Programming_Concepts.md#looping-through-a-collection-with-for) 中,就曾见过这种用到迭代器的方式,那里曾用到一个 `for` 循环, 就带有一个用于在集合中各个元素上,运行某些代码的迭代器。 + + +**在各行中搜索那个查询字串** + +接下来,这里就要检查当前行是否包含着这里的查询字串。幸运的是,字符串有着一个为咱们完成这件事的名为 `contains` 的有用方法!在 `search` 函数中添加一个到这个 `contains` 方法的调用,如下清单 12-18 中所示。请注意这仍上不会编译。 + +文件名:`src/lib.rs` + +```rust +pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str>{ + for line in contents.lines() { + if line.contains(query) { + // 对文本行执行某些操作 + } + } +} +``` + +*清单 12-18:加入检视该行是否包含 `query` 中字符串的功能* + +此刻,这里正在构建起功能来。而要让代码编译,就需要从其中的函数体,返回一个在该函数签名中,曾表明的应返回的某个值。 + + +**对匹配的那些行进行存储** + +要完成这个函数,就需要某种对这里打算返回的那些匹配行,加以存储的方法。为那个目的,这里可以在其中的 `for` 循环之前,构造出一个可变矢量(a mutable vector),并调用 `push` 方法,来把某个 `line` 存储在该矢量中。在那个 `for` 循环之后,这里就返回那个矢量,如下清单 12-19 中所示。 + +文件名:`src/lib.rs` + +```rust +pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str>{ + let mut results = Vec::new(); + + for line in contents.lines() { + if line.contains(query) { + results.push(line); + } + } + + results +} +``` + +*清单 12-19:对匹配的那些行进行存储,如此就可以返回他们了* + +现在这个 `search` 函数就应只返回那些包含了 `query` 的行了,同时这里的测试应通过。下面就来运行该测试: + +```console +$ cargo test lennyp@vm-manjaro + Compiling minigrep v0.1.0 (/home/lennyp/rust-lang/minigrep) + Finished test [unoptimized + debuginfo] target(s) in 0.49s + Running unittests src/lib.rs (target/debug/deps/minigrep-7d3f5b041202a66e) + +running 1 test +test tests::one_result ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Running unittests src/main.rs (target/debug/deps/minigrep-38ae0a181a4574d5) + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Doc-tests minigrep + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +``` + +这里的测试通过了,进而咱们就明白 `search` 函数是工作的了! + +到这里,咱们就会在保持这些测试通过,以维持这同样功能的同时,考虑对这个 `search` 函数的实现,进行重构的一些机会。这个 `search` 函数中的代码虽不怎么差劲,但他并没有利用上迭代器的一些有用特性。在 [第 13 章](Ch13_Functional_Language_Features_Iterators_and_Closures.md#processing-a-series-of-items-with-iterators) 中将回到这个示例,那里就会详细探讨到迭代器,进而会看看怎样来改进这个 `search` 函数。 + + +**在 `run` 函数中使用这个 `search` 函数** + +既然 `search` 函数运作起来并被测试过,那么这里就需要在这里的 `run` 函数中,调用 `search` 了。这里需要将那个 `config.query` 值与 `run` 从文件中读取到的 `contents`,传递给这个 `search` 函数。随后 `run` 将打印出从 `search` 返回的各行: + +文件名:`src/lib.rs` + +```rust +pub fn run(config: Config) -> Result<(), Box>{ + let contents = fs::read_to_string(config.file_path)?; + + for line in search(&config.query, &contents) { + println! ("{line}"); + } + + Ok(()) +} +``` + +这里仍使用一个 `for` 循环,来返回来自 `search` 的各行并将其打印出来。 + +现在这整个程序就应工作了!接下来就要试一下他了,首先以一个应确切地从这首 Emily Dickinson 的诗返回一行的词,“frog”: + +```console +$ cargo run -- frog poem.txt lennyp@vm-manjaro + Finished dev [unoptimized + debuginfo] target(s) in 0.00s + Running `target/debug/minigrep frog poem.txt` +在文件 poem.txt 中检索:frog +How public, like a frog +``` + +酷!现在来试一个将匹配多行的词,比如 “body”: + +```console +$ cargo run -- body poem.txt lennyp@vm-manjaro + Finished dev [unoptimized + debuginfo] target(s) in 0.00s + Running `target/debug/minigrep body poem.txt` +在文件 poem.txt 中检索:body +I'm nobody! Who are you? +Are you nobody, too? +How dreary to be somebody! +``` + +相当棒!这里已经构建了一个经典工具自己的小型版本,并掌握了很多有关如何建构应用程序的知识。这里还了解到有关文件输入输出、生命周期、测试及命令行参数解析等方面的点滴内容。 + +而为了完善这个项目,接下来就主要会演示怎样使用环境变量,以及怎样打印到标准错误输出(print to standard error),在编写命令行程序时,这两方面的知识都是有用的。 + +## 运用环境变量 + +**Working with Environment Variables** + +这里就要通过加入一项额外特性,来改进 `minigrep`:经由使用某个环境变量,用户可以开启与关闭的区分大小写的搜索选项。这里本可以将此特性,构造为一个命令行选项,并在用户打算应该该选项时,要求他们键入该命令行选项,而不是将其构造为一个环境变量,这样就允许用户只设置该环境变量一次,而在那次终端会话中的全部搜索,都是区分大小写的了。 + + +### 为这个区分大小写的 `search` 函数编写出一个失败测试 + +首先这里要加入一个新的、在该环境变量有着某个值时会调用到的 `search_case_insensitive` 函数。这里将继续遵循 TDD 流程,因此第一步就是要再度编写一个失败测试(a failing test)。这里将给这个新的 `search_case_insensitive` 函数,添加一个新的测试,并将其中原来的测试,从 `one_result` 改名为 `case_sensitive`,以区分这里两个测试的不同之处,如下清单 12-20 中所示。 + + +文件名:`src/lib.rs` + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn case_sensitive() { + let query = "duct"; + let contents = "\ +Rust: +safe, fast, productive. +Pick three. +Duct tape."; + + assert_eq! (vec! ["safe, fast, productive."], search(query, contents)); + } + + #[test] + fn case_insensitive() { + let query = "rUsT"; + let contents = "\ +Rust: +safe, fast, productive. +Pick three. +Trust me."; + + assert_eq! ( + vec! ["Rust:", "Trust me."], + search_insensitive(query, contents) + ); + } +} +``` + +*清单 12-20:给那个即将添加的不区分大小写函数添加一个新的失败测试* + +请注意这里也已编辑过原先测试的 `contents` 了。这里添加了一个有着文本 `"Duct tape."` 的新行,其中用到一个在以区分大小写方式进行搜索时,不应与查询字串 `"duct"` 匹配的大写字母 D。以这种方式修改原来的那个测试,有助于确保这里不会意外破坏这里已经实现了的区分大小写检索功能。这个区分大小写的测试,现在应会通过,并应在实现不区分大小写检索过程中继续通过测试。 + +那个新的不区分大小写检索的测试,使用了 `"rUsT"` 作为其查询字串。在那个这里即将添加的 `search_case_insensitive` 函数中,该查询字串 `"rUsT"` 应匹配到包含有着大写字母 R 的 `"Rust:"` 行,并匹配到行 `"Trust me."`,尽管这两行都有着与该查询字串不同的大小写。这就是这里的失败测试,而由于这里尚未定义出那个 `search_case_insensitive` 函数,因此该测试将会失败。请随意添加一个始终返回空矢量值的骨架实现,就跟在清单 12-16 中对 `search` 函数所做的类似,来对测试编译与失败加以检视。 + +### 实现这个 `search_case_insensitive` 函数 + +在下面清单 12-21 中所给出的这个 `search_case_insensitive` 函数,将与那个 `search` 函数几乎完全一样。唯一区别就是,这里将把其中的 `query` 与各个 `line` 做小写的处理,这样一来不论输入的参数是大写还是小写,在就该行是否包含查询字串时,他们都将是同样的拼写。 + +文件名:`src/lib.rs` + +```rust +pub fn search_insensitive<'a>( + query: &str, + contents: &'a str +) -> Vec<&'a str> { + let query = query.to_lowercase(); + let mut results = Vec::new(); + + for line in contents.lines() { + if line.to_lowercase().contains(&query) { + results.push(line); + } + } + + results +} +``` + +*清单 12-21:定义出一个在对查询字串与文本行进行比较前,先对他们进行小写处理的 `search_case_insensitive` 函数* + +首先,这里把那个 `query` 字符串进行小写处理,并将其存储在一个有着同样名字的遮蔽变量中(in a shadowed variable with the same name)。在查询字串上调用 `to_lowercase` 是必要的,如此不用户的查询为 `"rust"`、`"RUST"`、`"Rust"` 还是 `rUsT`,这里都将把查询字串,当作其为 `rust` 处理,而变得不区分大小写。尽管 `to_lowercase` 会处理基本 Unicode 字符,但他并不会 100% 精确。因此在编写真正应用时,这里就要完成些许更多的工作,但由于本小节是有关环境变量,而非 Unicode,因此这里就点到为止了。 + +请注意由于调用 `to_lowercase` 会创建出一个新数据,而非引用既有收据,因此现在的 `query` 就是一个新的 `String` 了。比如说查询字串为 `rUsT`:那个字符串切片并不包含这里要用到的小写字母 `u` 或 `t`,因此这里就不得不分配一个新的包含着 `rust` 的 `String` 变量。现在将 `query` 作为参数,传递给 `contains` 是,由于 `contains` 的函数签名被定义为取一个字符串切片,因此这里就需要添加一个地址符号 `&`。 + +接下来,这里在各个 `line` 上添加了到 `to_lowercase` 的调用,来对全部字符小写处理。现在就已将 `line` 及 `query` 转换成了小写,这里就将找出与查询字串大小写无关的那些匹配了。 + +现在来看看这种实现是否通过那些测试: + +```console +$ cargo test  ✔ + Finished test [unoptimized + debuginfo] target(s) in 0.00s + Running unittests src/lib.rs (target/debug/deps/minigrep-7d3f5b041202a66e) + +running 2 tests +test tests::case_insensitive ... ok +test tests::case_sensitive ... ok + +test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Running unittests src/main.rs (target/debug/deps/minigrep-38ae0a181a4574d5) + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Doc-tests minigrep + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +``` + +很棒!他们都通过了。现在,就要在那个 `run` 函数中,调用这个新的 `search_case_insensitive` 函数了。首先,这里将把一个配置项,添加到其中的 `Config` 结构体,来在区分大小写与不区分大小写检索之间加以切换。由于尚未在任何地方对这个字段进行初始化,因此这个字段的添加,将导致一些编译器错误: + +文件名:`src/lib.rs` + +```rust +pub struct Config { + pub query: String, + pub file_path: String, + pub ignore_case: bool, +} +``` + +这里添加了那个保存了一个布尔值(a Boolean) 的 `ignore_case` 字段。接下来,这里就需要那个 `run` 函数来检查这个 `ignore_case` 字段值,并使用该值来确定是要调用 `search` 函数还是 `search_case_insensitive` 函数,如下清单 12-22 中所示。这代码仍将不会编译。 + +文件名:`src/lib.rs` + + +```rust +pub fn run(config: Config) -> Result<(), Box>{ + let contents = fs::read_to_string(config.file_path)?; + + let results: Vec<&str> = if config.ignore_case { + search_insensitive(&config.query, &contents) + } else { + search(&config.query, &contents) + } + + for line in results { + println! ("{line}"); + } + + Ok(()) +} +``` + +*清单 12-22:依据 `config.ignore_case` 中的值,调用 `search` 还是 `search_case_insensitive`* + +最后,这里需要就环境变量加以检查了。用于处理环境变量的那些函数,位于便准库的 `env` 模组中,因此这里就要在 `src/lib.rs` 的顶部,把那个模组带入到作用域中来。随后这里就会使用 `env` 模组中的 `var` 函数,来检视是否已有给名为 `IGNORE_CASE` 设置某个值,如下清单 12-23 中所示。 + +文件名:`src/lib.rs` + +```rust +use std::env; +// --跳过代码-- + +impl Config { + pub fn build(args: &[String]) -> Result { + if args.len() < 3 { + return Err("参数数量不足"); + } + + let query = args[1].clone(); + let file_path = args[2].clone(); + + let ignore_case = env::var("IGNORE_CASE").is_ok(); + + Ok(Config { + query, + file_path, + ignore_case, + }) + } +} +``` + +*清单 12-23:就一个名为 `IGNORE_CASE` 的环境变量中的值进行检查* + +这里创建了一个新的变量 `ignore_case`。而为设置他的值,这里调用了 `env::var` 函数,并传递给了其那个 `IGNORE_CASE` 环境变量的名字。这个 `env::var` 函数返回的是一个 `Result` 值。在该环境变量有曾被设置为某个值时,其就会返回一个包含了该环境变量值的成功 `Ok` 变种。而在该环境变量未曾被设置时,该函数将返回 `Err` 变种。 + +这里在那个返回的 `Result` 上使用了 `is_ok` 方法,来检查该环境变量是否有被设置,这就意味着该程序应完成一次不区分大小写的检索。在这个 `IGNORE_CASE` 环境变量未被设置为某个值时,那么 `is_ok` 就会返回 `false`,而这个程序就会执行一次区分大小写的检索。这里并不关系那个环境变量的 *值*,而只关心他是否被设置或未设置,因此这里使用的就是 `is_ok`,而非使用 `unwrap`、`expect` 或其他任何已见到过的 `Result` 上的那些方法。 + +这里把在 `ignore_case` 变量中的值,传递给了那个 `Config` 实例,这样一来 `run` 函数就可以读取到那个值,并判定是要调用 `search_case_insensitive` 还是 `search`,就如同在清单 12-22 中所实现的那样。 + +现在就来试着运行一下!首先,这里将在未设置那个环境变量及查询字串为 `to` 之下,运行这个程序,这样应匹配到包含了全部小写单词 “to” 的那些行: + +```rust +$ cargo run -- to poem.txt  ✔ + Compiling minigrep v0.1.0 (/home/peng/rust-lang/minigrep) + Finished dev [unoptimized + debuginfo] target(s) in 0.55s + Running `target/debug/minigrep to poem.txt` +在文件 poem.txt 中检索:to +Are you nobody, too? +How dreary to be somebody! +``` + +看起来那代码仍会工作!现在,就在 `IGNORE_CASE` 被设置为 `1`,而查询字串同样为 `to` 之下,运行这个程序。 + + +```console +$ IGNORE_CASE=1 cargo run -- to poem.txt  ✔ +``` + +在使用的是 PowerShell 时,就需要用单独的命令,来设置该环境变量与运行这个程序: + +```PowerShell +PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt +``` + +这样就会令到 `IGNORE_CASE` 持续到本次 shell 会话终止为止。使用 `Remove-Item` cmdlet 其就可以被清除设置。 + +```PowerShell +PS> Remove-Item Env:IGNORE_CASE +``` + +这里应得到包含了可能有着大写字母 "to" 的那些行: + +```console +Are you nobody, too? +How dreary to be somebody! +To tell your name the livelong day +To an admiring bog! +``` + +非常好,这里还得到了包含着 “To” 的那些行了!这里的 `minigrep` 现在可以完成,由一个环境变量控制的不区分大小写检索了。现在就清楚了怎样运用命令行参数,或是环境变量,来管理程序选项集了。 + +有的程序,同时实现同一配置的命令行参数 *与* 环境变量。在这样的情形下,这些程序就会确定下其中之一有着较高优先级。好比你自己的另一代码练习中,就会尝试经由命令行参数,或同时经由环境变量,对是否区分大小写进行控制。就会在程序在一种设置下区分大小写,而另一种设置下不区分大小写时,对到底命令行参数优先,还是环境变量优先,加以确定。 + +这个 `std::env` 模组,包含了许多用于处理环境变量的其他有用特性:请查看其文档来看看有哪些可用特性。 + + +## 把错误消息写到标准错误而非标准输出 + +**Writing Error Messages to Standard Error Instead of Standard Output** + +- 标准错误输出:standard error +- 标准输出:standard output + +到目前为止,咱们都是在把全部输出,使用那个 `println!` 宏输出到终端。而在绝大多数终端里,都有着两种类型的终端:用于通用信息的 *标准输出* ( *standard output*,`stdout`),及用于错误消息的 *标准错误* ( *standard error*,`stderr`)。这种区别,就可以让用户选择把程序的成功输出,导向某个文件,而仍把错误消息,打印到屏幕上。 + +那个 `println!` 宏,只能打印到标准输出,因此这里就不得不使用其他物件,来打印到标准错误了。 + +### 对错误被写到何处进行检视 + +这里将使用清单 12-24 中的代码,来修改错误消息被打印出的方式。由于本章中早前完成的重构,现在打印错误消息的全部代码,就在一个函数,即 `main` 中了。Rust 标准库提供了打印到标准错误流(the standard error stream)的 `eprintln!` 宏,那么这里就来修改之前曾调用了 `println!` 的两个地方,以使用 `eprintln!` 来打印错误消息。 + +文件名:`src/main.rs` + +```rust +fn main() { + let args: Vec = env::args().collect(); + + let config = Config::build(&args).unwrap_or_else(|err| { + eprintln! ("解析参数时遇到问题:{err}"); + process::exit(1); + }); + + println! ("在文件 {} 中检索:{}", config.file_path, config.query); + + if let Err(e) = minigrep::run(config) { + eprintln! ("应用程序错误:{e}"); + process::exit(1); + } +} +``` + +*清单 12-24:使用 `eprintln!` 将错误消息写到标准错误而非标准输出* + +现在来一同样方式再度运行这个程序,不带任何参数并使用 `>` 对标准输出进行重定向(redirecting standard output with `>`): + +```console +$ cargo run > output.txt  ✔ + Compiling minigrep v0.1.0 (/home/peng/rust-lang/minigrep) + Finished dev [unoptimized + debuginfo] target(s) in 0.49s + Running `target/debug/minigrep` +解析参数时遇到问题:参数数量不足 +``` + +现在就看到了屏幕上的错误消息,同时发现 *output.txt* 中什么也没有,而这正是这所期望的命令行程序的行为了。 + +下面来以一些不会引起错误的参数,再度运行这个程序,不过仍要把标准输出重定向到某个文件,像下面这样: + +```console +$ cargo run -- to poem.txt > output.txt +``` + +这里将不会看见到终端的任何输出,而 *output.txt* 则会包含这里的结果: + +文件名:`output.txt` + +```plaintext +在文件 poem.txt 中检索:to +Are you nobody, too? +How dreary to be somebody! +``` + +这就证明现在正分别对成功输出使用着标准输出,而对错误输出使用着标准错误。 + + +## 本章小节 + +本章回顾了到目前为止曾学过的一些主要概念,并涵盖了在 Rust 中怎样完成常见 I/O 操作。经由使用命令行参数、文件、环境变量,以及那个用于打印错误的 `eprintln!` 宏,现在就已准备好编写命令行应用程序了。结合先前那些章中的概念,咱们所编写的代码将是良好组织、以恰当数据结构有效地存储着数据、对错误加以优美地处理,并被妥善地测试过。 + +接下来,这里将探讨受函数式编程所影响的一些 Rust 特性:闭包与迭代器(closures and iterators)。 diff --git a/src/Ch13_Functional_Language_Features_Iterators_and_Closures.md b/src/Ch13_Functional_Language_Features_Iterators_and_Closures.md new file mode 100644 index 0000000..dfe20f1 --- /dev/null +++ b/src/Ch13_Functional_Language_Features_Iterators_and_Closures.md @@ -0,0 +1,1004 @@ +# 函数式编程语言特性:迭代器与闭包 + +Rust 的设计从许多现有的语言和技术中获得了灵感,而一个显著的影响,便是 *函数式编程(functional programming)*。以函数方式编程,通常包括了通过把函数传入到参数中,或是从其他函数返回他们,以及将函数赋值给变量以便稍后执行,诸如此类的操作,而将函数作为一些值加以运用(programming in a functional style often includes using functions as values by using functions as values by passing them in arguments, returning them from another functions, assigning them to variables for later execution, and so forth)。 + +本章中,这里不会就函数式编程的问题为何或不为何加以分辨,而相反这里会讨论 Rust 的一些,与许多通常被指为函数式编程语言中的特性,相类似的特性。 + +更具体地讲,这里会涉及到: + +- *闭包(closures)*,一种可存储在变量中、类似函数的结构体; +- *迭代器(iterators)*,一种处理元素序列的方式(a way of processing a series of elements); +- 怎样运用闭包与迭代器,来改进第 12 章中的那个 I/O 项目; +- 闭包与迭代器的性能问题(剧透警告:比起可能想到的,他们要快!)。 + +到这里咱们已经涵盖到一些别的 Rust 特性,诸如模式匹配与枚举等,那些特性也是受函数式编程影响的。由于掌握闭包与迭代器,是进行惯用、快速 Rust 代码编写的一个重要方面,因此这里将把这整章,都用来讲解他们。 + + +## 闭包:捕获其环境的一些匿名函数 + +**Closures: Anonymous Functions that Capture Their Environment** + +Rust 的闭包,是一些可保存在变量中,或作为参数传递给另一些函数的匿名函数。可在一处创建出闭包,并于随后在别处调用该闭包,而在不同上下文中执行他(evaluate it)。与函数不同,闭包可以从其被定义的作用域,捕获到一些值。随后这里就会就闭包的这些特性,如何实现代码复用与程序行为的定制,而加以演示(unlike functions, closures can capture values from the scope in which they're defined. We'll demonstrate how these closure features allow for code reuse and behavior customization)。 + + +### 使用闭包对环境进行捕获 + +**Capturing Environment with Closures** + + +这里先要对怎样使用闭包,来捕获闭包被定义所在环境的一些值,加以检视。场景是这样的:每隔一段时间,本地的体恤衫公司,都会以促销方式,送出一些独家的、限量版的体恤衫给邮件列表上的某人。而在邮件列表上那些人,则可选择将他们偏好的颜色,添加到他们的个人资料。在某人选择了设置为其喜好颜色的免费体恤衫时,他就会收到那样颜色的体恤衫。而在某人不曾指定喜好颜色时,那么他们就会收到该公司当前有的数量最多那种颜色的体恤衫。 + +实现这样的业务逻辑,有许多种方式。而对于这个示例,这里就打算使用一个有着变种 `Red` 与 `Blue`(为简化目的而对颜色数目进行限制)、名为 `ShirtColor` 的枚举。这里使用了有着一个包含了表示当前库存种 T 恤衫颜色 `Vec` 的 `shirts` 字段,来表示该公司库存的 `Inventory` 的结构体。其中定义在 `Inventory` 上的方法 `giveaway`,会获取到免费体恤衫获得者的可选体恤衫颜色参数,并返回那个人将得到的体恤衫颜色。下面清单 13-1 给出了这样的设置: + +文件名:`src/main.rs` + +```rust +#[derive(Debug, PartialEq, Copy, Clone)] +enum ShirtColor { + Red, + Blue, +} + +struct Inventory { + shirts: Vec, +} + +impl Inventory { + fn giveaway(&self, user_preference: Option) -> ShirtColor { + user_preference.unwrap_or_else(|| self.most_stocked()) + } + + fn most_stocked(&self) -> ShirtColor { + let mut num_red = 0; + let mut num_blue = 0; + + for color in &self.shirts { + match color { + ShirtColor::Red => num_red += 1, + ShirtColor::Blue => num_blue += 1, + } + } + + if num_red > num_blue { + ShirtColor::Red + } else { + ShirtColor::Blue + } + } +} + +fn main() { + let store = Inventory { + shirts: vec! [ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue], + }; + + let user_pref1 = Some(ShirtColor::Red); + let giveaway1 = store.giveaway(user_pref1); + println! ( + "选项为 {:?} 的用户,得到了 {:?}", + user_pref1, giveaway1 + ); + + let user_pref2 = None; + let giveaway2 = store.giveaway(user_pref2); + println! ( + "选项为 {:?} 的用户得到了 {:?}", + user_pref2, giveaway2 + ); +} +``` + +*清单 13-1:体恤衫公司派发情形* + +定义在 `main` 中的 `store`,有着两件蓝色 T 恤与一件红色 T 恤剩下,用于本次限量版促销活动。这里分别对选项为红色 T 恤及蓝色 T 恤的用户,调用了那个 `giveaway` 方法。 + +再次说明,此代码可以许多方式实现,而这里,为着重于闭包特性,因此除开其中用到闭包的那个 `giveaway` 的函数体,这里仍立足于咱们已经学过的那些概念。在那个 `giveaway` 方法中,这里以一个类型为 `Option` 的参数,而获取到用户选项,并在 `user_preference` 上调用了 `unwrap_or_else` 方法。这个 [`Option` 上的 `unwrap_or_else` 方法](https://doc.rust-lang.org/std/option/enum.Option.html#method.unwrap_or_else) 是定义在标准库中的。他会取两个参数:一个不带任何参数的、返回值 `T` (与 `Option` 的 `Some` 变种中保存的同一类型,此示例中即为 `ShirtColor`)。在 `Option` 为 `Some` 变种时,`unwrap_or_else` 就会返回 `Some` 里的那个值。而在 `Option` 为 `None` 变种时,那么 `unwrap_or_else` 就会调用随后的那个闭包,并返回由该闭包所返回的值。 + +这里是将那个闭包表达式(closure expression) `|| self.most_stocked()`,作为参数传递给 `unwrap_or_else` 的。这是一个本身不取参数的闭包(如该闭包有参数,那么那些参数就会出现在那两条竖线之间)。该闭包的函数体调用了 `self.most_stocked()`。这里于这个地方对该闭包进行定义,进而 `unwrap_or_else` 的实现就会在需要其结果时,执行这个闭包。 + +运行此代码将会打印出: + + +```console +$ cargo run lennyp@vm-manjaro + Compiling closure_demo v0.1.0 (/home/lennyp/rust-lang/closure_demo) + Finished dev [unoptimized + debuginfo] target(s) in 0.47s + Running `target/debug/closure_demo` +选项为 Some(Red) 的用户,得到了 Red +选项为 None 的用户得到了 Blue +``` + +这里有个有趣的地方,即这里曾传递了一个调用了在当前 `Inventory` 实例上的 `self.most_stocked()` 的闭包。标准库并不需要明白有关这里所定义的 `Inventory` 或 `ShirtColor` 的任何事情,或者在此场景下这里要运用的那些逻辑。这个闭包捕获了到 `self` 这个 `Inventory` 实例的一个不可变引用,并将该不可变引用,传递给这里指定给那个 `unwrap_or_else` 方法的代码。与之相反,函数是无法以这种方式,捕获到他们的环境的(the closure captures an immutable reference to the `self` `Inventory` instance and pass it with the code we specify to the `unwrap_or_else` method. Functions, on the other hand, are not able to capture their environment in this way)。 + + +### 闭包的类型推断与类型注解 + +**Closure Type Inference and Annotation** + +函数与闭包之间,还有别的一些区别。闭包通常不要求像 `fn` 函数那样,注解参数或返回值的类型。之所以在函数上要求类型注解,是因为类型是暴露给用户的显式接口的组成部分。严格定义这种接口,对于确保所有人,在某个函数用到与返回的值类型上,达成一致尤为重要。与此相反,闭包则不是用在像这样暴露出的接口中:闭包存储在变量中,并在无需命名及暴露给库用户下而被使用。 + +闭包通常是短小的,并仅在较窄的上下文,而非在任何情况下都有意义。有了执行受限条件,那么编译器就可以推断出参数与返回值的类型,类似于编译器能够推断出绝大多数变量的类型那样(同样有极少情况下,编译器需要闭包的类型注解)。 + +与变量一样,在想要提升明确性与清楚时,是可以添加类型的,只不过要付出相比严格必要更多的繁琐代价。对闭包的类型进行注解,看起来会与下面清单 13-2 中所给出的定义一样。在此示例中,这里定义了一个闭包,并将其存储在一个变量中,而非清单 13-1 中所做的那样,把闭包定义在将其作为参数加以传递的地方。 + +文件名:`src/main.rs` + +```rust +let expensive_closure = |num: u32| -> u32 { + println! ("缓慢计算中......"); + thread::sleep(Duration::from_secs(2)); + num +} +``` + +*清单 13-2:在闭包中加上可选的参数与返回值类型的类型注解* + + +加上类型注解后,闭包的语法就更像是函数的语法了。作为比较,下面定义了一个把 `1` 加到参数的函数,以及一个有着同样行为的闭包。这里为对齐那些对应部分,这里添加了一些空格。这样就表示出了闭包语法与函数语法,除开管道使用及可选语法数量外,是如何的相似。 + +```rust +fn add_one_v1 (x: u32) -> u32 { x + 1 }; +let add_one_v2 = |x: u32| -> u32 { x + 1 }; +let add_one_v3 = |x| { x + 1 }; +let add_one_v4 = |x| x + 1 ; +``` + +第一行给出了一个函数定义,而第二行则给出的是一个完整注解过的闭包定义。在第三行中,这里移除了那个闭包定义的类型注解。在第四行,就移出了那对花括号,由于这个闭包的函数体只有一个表达式,因此这对花括号是可选的。这些全都是有效的定义,在他们被调用时,都会产生出同样的行为。由于 `add_one_v3` 与 `add_one_v4` 中的那些类型将从他们的用法中推断出来,因此这两个行就需要被执行的闭包,能够被编译。这一点与 `let v = Vec::new();` 要么需要类型注解,或是需要有某种类型的值插入到这个 `Vec` 中,Rust 才能够推断出他的类型类相似。 + +对于闭包的定义,编译器将为其各个参数及其返回值,推断出某种具体类型。举个例子,下面清单 13-3 给出仅返回其接收到的、作为参数的值的一个较短闭包定义。此闭包除了这个示例外,并没有什么用处。请注意这里并未添加任何类型注解到这个定义。由于没有类型注解,这里就可以任何类型调用这个闭包,这里第一次是以 `String` 类型调用的。在随后尝试以整数调用 `example_closure` 时,就会得到一个错误。 + +文件名:`src/main.rs` + +```rust + let example_closure = |x| x; + let s = example_closure(String::from(你好)); + let n = example_closure(5); +``` + +*清单 13-3:尝试是以两种不同类型,调用一个类型为推断出的闭包* + +编译器给出如下错误: + +```console +$ cargo run lennyp@vm-manjaro + Compiling closure-example v0.1.0 (/home/lennyp/rust-lang/closure-example) +error[E0308]: mismatched types + --> src/main.rs:4:29 + | +4 | let n = example_closure(5); + | --------------- ^- help: try using a conversion method: `.to_string()` + | | | + | | expected struct `String`, found integer + | arguments to this function are incorrect + | +note: closure defined here + --> src/main.rs:2:27 + | +2 | let example_closure = |x| x; + | ^^^ + +For more information about this error, try `rustc --explain E0308`. +error: could not compile `closure-example` due to previous error +``` + +这里第一次是以 `String` 值调用的 `example_closure`,编译器便推断出了该闭包 `x` 与返回值的类型为 `String`。这些类型随后便被锁定于 `example_closure` 中的那个闭包里了,而在接下来尝试以不同类型,使用这同样的闭包时,就得到了一个类型错误。 + + +### 捕获引用抑或迁移所有权 + +**Capturing Reference or Moving Ownership** + +闭包可以三种方式,捕获到其所在环境的值,而这三种方式,则是直接对应到函数取得参数三种可行方式的:不可变地进行借用、可变地借用,及取得所有权(closures can capture values from their environment in three ways, which directly map to the three ways a function can take a parameter: borrowing immutably, borrowing mutably, and taking ownership)。闭包会根据函数体要对捕获到的值做些什么,而确定出使用何种方式。 + +在下面清单 13-4 中,由于其只需一个不可变引用来打印出值,因此这里就定义了捕获到名为 `list` 矢量值不可变引用的一个闭包: + +文件名:`src/main.rs` + +```rust +fn main() { + let list = vec! [1, 2, 3]; + println! ("在定义闭包之前的:{:?}", list); + + let only_borrows = || println! ("自闭包打印出的:{:?}", list); + + println! ("在调用闭包之前:{:?}", list); + only_borrows(); + println! ("在调用闭包之后:{:?}", list); +} +``` + +*清单 13-4:定义及调用一个捕获不可变引用的闭包* + +此示例还演示了变量可绑定到闭包定义,进而随后即可通过使用变量名字与圆括号对,如同这个变量名是个函数名一样,调用这个闭包, + +由于在同一时间,可以有着到 `list` 的多个不可变引用,对该闭包定义之前、闭包定义之后而在该闭包被调用之前,以及该闭包调用之后的代码中,`list` 都仍是可访问的。此代码会编译、运行及打印出如下输出: + +```console +$ cargo run lennyp@vm-manjaro + Compiling closure-example v0.1.0 (/home/lennyp/rust-lang/closure-example) + Finished dev [unoptimized + debuginfo] target(s) in 0.29s + Running `target/debug/closure-example` +在定义闭包之前的:[1, 2, 3] +在调用闭包之前:[1, 2, 3] +自闭包打印出的:[1, 2, 3] +在调用闭包之后:[1, 2, 3] +``` + +接下来,在下面的清单 13-5 中,这里将修改该闭包的函数体,让其把一个元素添加到这个 `list` 矢量值。这个闭包现在就捕获了一个可变引用: + + +文件名:`src/main.rs` + +```rust +fn main() { + let mut list = vec! [1, 2, 3]; + println! ("在定义闭包之前的:{:?}", list); + + let mut borrows_mutably = || list.push(7); + + borrows_mutably(); + println! ("在调用闭包之后:{:?}", list); +} +``` + +*清单 13-5:定义并调用一个捕获可变引用的闭包* + +此代码会编译、运行,并打印出: + + +```console +$ cargo run lennyp@vm-manjaro + Compiling closure-example v0.1.0 (/home/lennyp/rust-lang/closure-example) + Finished dev [unoptimized + debuginfo] target(s) in 0.47s + Running `target/debug/closure-example` +在定义闭包之前的:[1, 2, 3] +在调用闭包之后:[1, 2, 3, 7] +``` + +请注意在那个 `borrows_mutably` 的定义与调用之间,不再有一个 `println!`:在 `borrows_mutably` 被定义时,他就捕获了到 `list` 的一个可变引用。在闭包被调用之后,这里没有再度使用那个闭包,因此这个可变借用就结束了。由于当存在着一个可变借用(a mutable borrow)时,不允许有其他的借用,因此在该闭包定义与其调用期间,打印那个 `list` 的不可变借用是不被允许的。请尝试在这里添加一个 `println!`,来看看会得到什么消息! + +在即使闭包的函数体并不严格需要所有权,而仍打算强制要求闭包取得他所使用的环境中一些值的所有权时,那么就可以在参数清单前,使用 `move` 关键字。 + +在将闭包传递给某个新线程以迁移数据,从而令到数据为该新线程所掌握时,这种技巧最为有用。在第 16 章中,讲到并发的时候,就会详细讨论到线程及为何要使用线程,而现在,下面就来粗略地探讨一下,使用一个需要 `move` 关键字的闭包,生成一个新线程。下面清单 13-6 给出了修改后在一个新线程,而非这个主线程中,打印出那个矢量值的修改后的清单 13-4: + +文件名:`src/main.rs` + +```rust +use std::thread; + +fn main() { + let mut list = vec! [1, 2, 3]; + println! ("在定义闭包之前的:{:?}", list); + + thread::spawn(move || println! ("从线程打印出的:{:?}", list)) + .join() + .unwrap(); +} +``` + +*清单 13-6:使用 `move` 关键字,强制那个线程的闭包取得 `list` 的所有权* + +这里生成了一个新线程,将一个闭包作为参数给到这个线程来运行(we spawn a new thread, giving the thread a closure to run as an argument)。其中闭包的函数体,打印出这个清单。在代码清单 13-4 中,由于不可变引用是打印那里清单所需的最少量权限,因此那里的闭包只使用了不可变引用对 `list` 加以了捕获。而在这个示例中,即使其中的闭包函数体只需要不可变引用,这里仍需要通过把那个 `move` 关键字放置于闭包定义的开头,指明那个 `list` 应被迁移到该闭包中。这个新线程可能在主线程其余部分执行完毕之前就执行结束,或主线程可能先结束。若主线程仍保有 `list` 的所有权,而在新线程结束之前就结束,而丢弃掉 `list`,那么在那个线程中的 `list` 就会成为无效。因此,编译器就要求其中的 `list` ,被迁移到那个给到新线程的闭包中,如此那个引用就将有效。请尝试去掉这个 `move` 关键字,或在那个闭包被之后使用 `list`,来看看会得到什么编译器错误! + + +### 将捕获到的值迁移出闭包与 `Fn` 特质 + +**Moving Captured Values Out of Closures and the `Fn` Traits** + +一旦闭包捕获了某个引用,或捕获了其被定义处环境中某个值的所有权(从而影响到被迁移 *进* 该闭包的相关项目),该闭包函数体中的代码,就会定义出闭包稍后被执行时,对这些引用或值会发生什么(因此而影响到那些被迁移 *出* 该闭包的相关项目,once a closure has captured a reference or captured ownership of a value from the environment where the closure is defined(thus affecting what, if anything, is moved *into* the closure), the code in the body of the closure defines what happens to the references or values when the closure is evaluated later(thus affecting what, if anything, is moved *out* of the closure))。闭包函数体可执行以下的任意操作:将捕获到的值迁移出闭包、修改捕获到的值、既不迁移也不修改值,或以不从其环境捕获任何东西开始。 + +闭包捕获及处理环境中的值的方式,影响到闭包实现了哪个特质,而特质则是指函数与结构体,能够以怎样的方式,指明他们可以使用的闭包类别。依据闭包函数体处理环境中值的不同方式,闭包会以累加方式,自动实现一个、两个,或全部三个的这些 `Fn` 特质(the way a closure captures and handles values from the environment affects which traits the closure implements, and traits are how functions and structs can specify what kinds of closures they can use. Closures will automatically implement one, two, or all three of these `Fn` traits, in an additive fashion, depending on how the closure's body handles the values): + +1. `FnOnce` 特质适用于那些可被调用一次的闭包。由于全部闭包都可被调用,因此他们都起码实现了这个特质。而由于将捕获到的值迁移出其函数体的闭包,只能被调用一次,因此这样的闭包将只要实现 `FnOnce`,而不会实现其他的那些 `Fn` 特质; + +2. `FnMut` 特质适用于不将捕获值迁移出函数体,但仍会修改捕获值的那些闭包。这些闭包可被多次调用; + +3. `Fn` 则适用于不将捕获值迁移出函数体的闭包,与不修改捕获值的闭包,以及那些不从其环境捕获任何东西的闭包。这些闭包在不会修改其环境之下,可被多次调用,在诸如并发地多次调用某个闭包情形中,这种的调用方式就相当重要。 + +下面来看看清单 13-1 中用到的, `Option` 上那个 `unwrap_or_else` 方法的定义: + +```rust +impl Option { + pub fn unwrap_or_else(self, f: F) -> T + where + F: FnOnce() -> T + { + match self { + Some(x) => x, + None => f(), + } + } +} +``` + +回顾到那个 `T` 即为表示某个 `Optoin` 的 `Some` 变种中值类型的泛型。那个类型 `T` 同样是这个 `unwrap_or_else` 函数的返回值类型:比如,在某个 `Option` 上调用 `unwrap_or_else` 的代码,就会得到一个 `String`。 + +接着,请注意这个 `unwrap_or_else` 函数有个额外的泛型参数 `F`。这个 `F` 类型为名为 `f` 的参数类型,而这个参数 `f`,则正是在调用 `unwrap_or_else` 时,所提供到的那个闭包。 + +在这个泛型 `F` 上所指定的特质边界(the trait bound),为 `FnOnce() -> T`,就表示 `F` 必须能被调用一次、不取参数,并要返回一个 `T` 类型值。在特质边界中使用 `FnOnce`,表示 `unwrap_or_else` 只会调用 `f` 至多一次这样的约束。而在 `unwrap_or_else` 的函数体中,就可以看到在那个 `Option` 为 `Some` 时,`f` 不会被调用。在 `Option` 为 `None` 时,`f` 就会被调用一次。由于所有闭包都实现了 `FnOnce`,因此 `unwrap_or_else` 就会接收最为广泛的闭包,进而有着其最大的灵活性。 + +> **注意**:函数也可以实现全部三个 `Fn` 特质。在打算执行的操作,不需要捕获环境中某个值时,就可以在需要某个实现了 `Fn` 特质处,使用某个函数名字而非闭包。比如,在某个 `Option>` 值上,若这个值为 `None`,那么就可以调用 `unwrap_or_else(Vec::new)` 来获取到一个新的空矢量值。 + +现在来检视一下定义在切片上的那个标准库方法 `sort_by_key`,来看看其与 `unwrap_or_else` 有何区别,以及为何 `sort_by_key` 会使用 `FnMut` 而非 `FnOnce` 作为特质边界。其中的闭包,获得的是一个到正被处理切片中当前元素的引用形式的参数,并返回一个可被排序类型 `K` 的一个值。在想要以各个条目的某种特定属性,对切片进行排序时,此函数是有用的。在下面清单 13-7 中,就有一个 `Rectangle` 实例的清单,进而这里使用了 `sort_by_key`,来以 `width` 属性的升序对这些 `Rectangle` 实例加以排序: + +文件名:`src/main.rs` + +```rust +#[derive(Debug)] +struct Rectangle { + width: u32, + height: u32, +} + +fn main() { + let mut list = [ + Rectangle { width: 10, height: 1 }, + Rectangle { width: 3, height: 5 }, + Rectangle { width: 7, height: 12 }, + ]; + + list.sort_by_key(|r| r.width); + println! ("以宽的升序排序:{:#?}", list); + + list.sort_by_key(|r| r.height); + println! ("以高的升序排序:{:#?}", list); +} +``` + +*清单 13-7:使用 `sort_by_key` 来对矩形以宽和高分别排序* + + +此代码会打印出: + +```console +$ cargo run lennyp@vm-manjaro + Compiling closure-example v0.1.0 (/home/lennyp/rust-lang/closure-example) + Finished dev [unoptimized + debuginfo] target(s) in 0.19s + Running `target/debug/closure-example` +以宽的升序排序: +[ + Rectangle { + width: 3, + height: 5, + }, + Rectangle { + width: 7, + height: 12, + }, + Rectangle { + width: 10, + height: 1, + }, +] +以高的升序排序: +[ + Rectangle { + width: 10, + height: 1, + }, + Rectangle { + width: 3, + height: 5, + }, + Rectangle { + width: 7, + height: 12, + }, +] +``` + +`sort_by_key` 之所以被定义为取一个 `FnMut` 闭包,是由于他多次调用了这个闭包:对那个切片中的每个条目运行一次。那个闭包 `|r| r.width` 并不会捕获、修改,或是从其环境迁移任何东西,因此他就满足了 `FnMut` 这个特质边界的那些要求。 + +作为对照,下面清单 13-8 给出一个仅实现了 `FnOnce` 特质的闭包示例,正是因为该闭包将值迁移出了其环境。编译器不会允许咱们在 `sort_by_key` 下使用这个闭包: + +文件名:`src/main.rs` + +```rust +#[derive(Debug)] +struct Rectangle { + width: u32, + height: u32, +} + +fn main() { + let mut list = [ + Rectangle { width: 10, height: 1 }, + Rectangle { width: 3, height: 5 }, + Rectangle { width: 7, height: 12 }, + ]; + + let mut sort_operations = vec! []; + let value = String::from("按照被调用到的 key"); + + list.sort_by_key(|r| { + sort_operations.push(value); + r.width + }); + println! ("{:#?}", list); +} +``` + +*清单 13-8:尝试在 `sort_by_key` 下使用某个 `FnOnce` 类型的闭包* + +这是一种人为杜撰的、错综复杂(而不会工作)的,来尝试计算 `sort_by_key` 在对 `list` 进行排序时,被调用次数的方法。此代码试图通过将该闭包所处环境中的 `value` -- 这样一个 `String`,压入到那个 `sort_operations` 矢量,而完成这样的计数。该闭包会捕获到 `value`,随后通过将 `value` 的所有权转移给 `sort_operations` 矢量,而将 `value` 迁移出这个闭包。这个闭包是可以调用一次的;由于 `value` 将不再位于那个其被再次压入到 `sort_operations` 的环境中,因此第二次的尝试调用他,就不会工作了。因此这个闭包就只实现了 `FnOnce`。在尝试编译此代码时,就会得到由于那个闭包必须实现 `FnMut`,因此 `value` 无法被迁移出那个闭包的错误: + +```console +$ cargo run lennyp@vm-manjaro + Compiling closure-example v0.1.0 (/home/lennyp/rust-lang/closure-example) +error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure + --> src/main.rs:18:30 + | +15 | let value = String::from("按照被调用到的 key"); + | ----- captured outer variable +16 | +17 | list.sort_by_key(|r| { + | --- captured by this `FnMut` closure +18 | sort_operations.push(value); + | ^^^^^ move occurs because `value` has type `String`, which does not implement the `Copy` trait + +For more information about this error, try `rustc --explain E0507`. +error: could not compile `closure-example` due to previous error +``` + +错误指向的是那个闭包函数体中,将 `value` 迁移出环境的那一行。为了修复这个问题,这里就需要对该闭包的函数体加以修改,从而令其不会将值迁移出环境。为对 `sort_by_key` 被调用次数加以计数,就要在环境中保留一个计数器,并在闭包函数体中增加该计数器的值,就是计算 `sort_by_key` 被调用次数的一种更为直接了当的方式。下面清单 13-9 中的闭包,由于其只捕获了到那个 `num_sort_operations` 计数器的可变引用,进而由此就可以被多次调用,那么这个闭包就会工作: + +文件名:`src/main.rs` + +```rust +#[derive(Debug)] +struct Rectangle { + width: u32, + height: u32, +} + +fn main() { + let mut list = [ + Rectangle { width: 10, height: 1 }, + Rectangle { width: 3, height: 5 }, + Rectangle { width: 7, height: 12 }, + ]; + + let mut num_sort_operations = 0; + + list.sort_by_key(|r| { + num_sort_operations += 1; + r.width + }); + println! ("{:#?}\n 在 {num_sort_operations} 次操作下被排序好的", list); +} +``` + +*清单 13-9:在 `sort_by_key` 下使用一个 `FnMut` 闭包是允许的* + +在定义那些运用到闭包的函数或类型时,这些 `Fn` 特质是相当重要的。在下一小节中,就会讨论到迭代器。许多迭代器的方法,都取的是闭包参数,因此请在继续学习时,牢记这些闭包的细节! + +> **注**:将清单 13-8 的代码,只加入一个地址符号 `&`,而修改成下面这样,也是工作的。这就要想想是为什么了:) + +```rust +#[derive(Debug)] +struct Rectangle { + width: u32, + height: u32, +} + +fn main() { + let mut list = [ + Rectangle { width: 10, height: 1 }, + Rectangle { width: 3, height: 5 }, + Rectangle { width: 7, height: 12 }, + ]; + + let mut sort_operations = vec! []; + let value = String::from("按照被调用到的 key"); + + list.sort_by_key(|r| { + sort_operations.push(&value); + r.width + }); + println! ("{:#?}\n{:#?}", list, sort_operations); +} +``` + + +## 使用迭代器对条目系列进行处理 + +**Processing a Series of Items with Iterators** + +迭代器模式,实现了在条目序列上依次执行某些任务。迭代器负责着对各个条目进行遍历,及判断序列在什么时候结束的逻辑。在运用迭代器时,就不必自己再实现那样的逻辑了。 + +在 Rust 中,迭代器是 *惰性的(lazy)*,即在调用那些消费该迭代器以用完他之前,他们是没有效果的。比如,下面清单 13-10 中的代码就通过调用定义在 `Vec` 上的 `iter` 方法,而创建出了一个在那个矢量 `v1` 中各个条目上的迭代器。此代码本身并不会执行任何有用的事情。 + +```rust + let v1 = vec! [1, 2, 3]; + let v1_iter = v1.iter(); +``` + +*清单 13-10:创建出一个迭代器* + +其中的迭代器,是存储在那个 `v1_iter` 变量中的。一旦创建出了某个迭代器,就可以多种方式对其加以使用了。在第 3 章中的清单 3-5 中,就曾使用了一个 `for` 循环,对一个数组进行过迭代,而在该数组的各个条目上执行一些代码。而在使用 `for` 循环这表象之下,则是隐式地创建出了一个迭代器,并于随后消费了这个迭代器,不过在本小节之前,只是并未提及其确切工作原理。 + +下面清单 13-11 中的示例里,就把迭代器的创建,从那个 `for` 循环中迭代器的使用分离开来了。在使用 `v1_iter` 中的迭代器,调用那个 `for` 循环时,在那个迭代器中的各个元素,就在那个循环的一个个迭代中,被使用了,而这就会打印出各个值。 + +```rust + let v1 = vec! [1, 2, 3]; + + let v1_iter = v1.iter(); + + for val in v1_iter { + println! ("得到了:{}", val); + } +``` + +*清单 13-11:在 `for` 循环中使用迭代器* + +在那些不具备由语言标准库所提供迭代器的编程语言中,就会大概率要通过一个从 `0` 开始的索引变量,而使用那个变量,来进入到该矢量值中,以获取到一个值,并在一个循环中对这个索引变量值进行递增,直到该索引变量达到该矢量中全部条目数为止。 + +迭代器为咱们处理了这全部的逻辑,从而减少了可能潜在会出错的那些重复代码。对于许多不同类别的序列,而不光是那些可以索引进去的数据结构,比如矢量值,迭代器都能给到运用这同样逻辑的更多灵活性。接下来就要检视一下迭代器是怎样做到这样的。 + +### `Iterator` 特质与 `next` 方法 + +所有迭代器都实现了一个定义在标准库中、名为 `Iterator` 的特质。该特质的定义看起来像下面这样: + + +```rust +pub trait Iterator { + type Item; + + fn next(&mut self) -> Option; + + // 这里省略了那些有着默认实现的方法 +} +``` + +请注意此定义使用了一些新的语法:`type Item` and `Self::Item`,他们定义着这个特质的一个 *关联类型(associated type)*。将在后面的第 19 章中,深入讲到关联类型。至于现在,要了解的全部,即此代码表明了这个 `Iterator` 特质的实现,有个还要定义一个 `Item` 类型的前提要求,且这个 `Item` 类型,会被用在其中的 `next` 方法返回值类型中。也就是说,这个 `Item` 类型将是该迭代器所返回的类型。 + +`Iterator` 特质只要求实现者(implementors)定义一个方法:即这个 `next` 方法,他会一次返回一个,封装在 `Some` 中该迭代器的条目,且在迭代完毕时,就返回 `None`。 + +是可以直接调用迭代器上的这个 `next` 方法的;下面清单 13-12 演示了,重复调用创建自该矢量的那个迭代器上的 `next` 方法,会返回些什么样的值。 + +文件名:`src/lib.rs` + +```rust +#[test] +fn iterator_demonstration() { + let v1 = vec! [1, 2, 3]; + + let mut v1_iter = v1.iter(); + + assert_eq! (v1_iter.next(), Some(&1)); + assert_eq! (v1_iter.next(), Some(&2)); + assert_eq! (v1_iter.next(), Some(&3)); + assert_eq! (v1_iter.next(), None); +} +``` + +*清单 13-12:对迭代器上的 `next` 方法进行调用* + + +请注意这里需要将 `v1_iter` 构造为可变的:调用某个迭代器上的 `next` 方法,会修改那个迭代器用于追踪其本身位于其关联序列何处的内部状态。也就是说,此代码 *消费(consumes)*,或用完(use up)了这个迭代器。每次对 `next` 的调用,都会从这个迭代器吃掉一个条目。之所以之前在使用 `for` 循环时,未曾需要将 `v1_iter` 构造为可变,是由于那个循环占据了 `v1_iter` 的所有权,而在幕后将其构造为了可变。 + +还要注意这里从到 `next` 的调用所获取到的那些值,都是到那个矢量中值的不可变引用。`iter` 方法产生出的,是一个不可变引用的迭代器。而在打算创建一个会取得 `v1` 所有权的迭代器,并返回自有数据时,则可以调用 `into_iter` 而非 `iter`。与此类似,当需要可变引用的迭代时,那么就可以调用 `iter_mut` 而非 `iter`。 + + +### 消费迭代器的一些方法 + +`Iterator` 特质有着带有由标准库提供了默认实现的几个方法;通过查阅 `Iterator` 特质的标准库 API 文档,就可以找到这些方法。他们中的一些,就在他们的定义中调用了这个 `next` 方法,这正是在实现这个 `Iterator` 特质时,不要求实现这个 `next` 方法的原因。 + +由于调用这些调用了 `next` 方法,会耗尽迭代器,因此他们被称为 *消费适配器(consuming adaptors)*。一个例子便是 `sum` 方法,他会取得迭代器的所有权,并通过重复调用 `next`,遍历所有条目,由此消费该迭代器。在其遍历期间,他就会把各个条目,加到一个运行中的总和,并在遍历完成时返回这个总和。下面清单 13-13,有着这个 `sum` 方法运用的测试演示: + +文件名:`src/lib.rs` + +```rust +#[test] +fn iterator_sum() { + let v1 = vec! [1, 2, 3]; + + let v1_iter = v1.iter(); + + let total: i32 = v1_iter.sum(); + + assert_eq! (total, 6); +} +``` + +*清单 13-13:调用 `sum` 方法来获取迭代器中全部项目的总和* + +由于 `sum` 会取得那个在其上调用他的迭代器的所有权,因此在到 `sum` 的调用之后,是不允许使用 `v1_iter` 的。 + + +### 产生其他迭代器的方法 + +*迭代器适配器(iterator adaptors)* 是一些定义在 `Iterator` 特质上,不会消费迭代器的方法。相反,他们会通过改变初始迭代器的某些方面,而产生出别的一些迭代器。 + +下面清单 13-17 给出了一个调用迭代器适配器方法 `map` 的示例,该方法会取得在迭代器条目被遍历时在各个条目上调用的闭包。这个 `map` 方法会返回一个产生出修改后的那些条目的一个新迭代器。此示例中的闭包,会创建一个其中原矢量的各个条目被增加 `1` 了的新迭代器: + +文件名:`src/main.rs` + +```rust + let v1 = vec! [1, 2, 3]; + + v1.iter().map(|x| x + 1); +``` + +*清单 13-14:调用迭代器适配器 `map` 来创建出一个新迭代器* + +然而,此代码会产生一条告警: + +```console +$ cargo run lennyp@vm-manjaro + Compiling iterator_demo v0.1.0 (/home/lennyp/rust-lang/iterator_demo) +warning: unused `Map` that must be used + --> src/main.rs:4:5 + | +4 | v1.iter().map(|x| x + 1); + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: `#[warn(unused_must_use)]` on by default + = note: iterators are lazy and do nothing unless consumed + +warning: `iterator_demo` (bin "iterator_demo") generated 1 warning + Finished dev [unoptimized + debuginfo] target(s) in 0.36s + Running `target/debug/iterator_demo` +``` + +清单 13-14 中的代码,并未执行任何事情;那里所指定的闭包不曾被调用过。那条告警提示了缘由:迭代器适配器是惰性的,进而在此就需要消费该迭代器。 + +为修正此告警而消费那个迭代器,这里将使用 `collect` 方法,之前曾在第 12 章的清单 12-1 中,对 `env::args` 用到过该方法。此方法会消费那个迭代器,并将那些结果值,收集到一个集合数据类型中。 + +下面清单 13-15 中,就把在那个自 `map` 的调用所返回的迭代器遍历的结果,收集到了一个矢量值中。这个矢量将以包含原始矢量的各个条目增加 `1` 后的各个条目。 + +文件名:`src/main.rs` + +```rust + let v1 = vec! [1, 2, 3]; + + let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); + + assert_eq! (v2, vec! [2, 3, 4]); +``` + +*清单 13-15:调用 `map` 方法来创建出一个新迭代器,并于随后调用 `collect` 方法来消费该新迭代器而创建出一个矢量值* + +由于 `map` 取了一个闭包,因此这里就可以指定咱们要在各个条目上执行的任何操作了。对于阐明闭包在重用 `Iterator` 特质所提供的遍历行为的同时,怎样实现定制一些行为来讲,这可是一个极佳的示例了。 + +可将多个调用,链接到迭代器适配器,来以可读方式,执行一些复杂操作。但由于所有迭代器都是惰性的,因此就必须调用一个消费适配器方法(one of the consuming apdaptor methods),来获取到迭代器适配器调用的结果。 + + +### 使用捕获了其所在环境的闭包 + +**Using Closures that Capture Their Environment** + +相当多的迭代器适配器,都会将闭包取作参数,且通常作为参数指定给迭代器适配器的那些闭包,都将是些会捕获他们环境的闭包。 + +这里将使用会取一个闭包的 `filter` 方法,来作为这方面的示例。改闭包会从其所在迭代器获取到一个条目,并返回要给 `bool`。在闭包返回 `true` 时,那个条目值就将被包含在由 `filter` 产生出的迭代中。在该闭包返回 `false` 时,那个条目值则不会被包含。 + +下面清单 13-16 中,使用了有着捕获其环境中 `shoe_size` 变量的一个闭包的 `filter` 方法,来对一个 `Shoe` 结构体实例的集合进行迭代。他将只返回特定尺码的那些鞋子。 + +文件名:`src/lib.rs` + +```rust +#[derive(PartialEq, Debug)] +struct Shoe { + size: u32, + style: String, +} + +fn shoes_in_size(shoes: Vec, shoe_size: u32) -> Vec { + shoes.into_iter().filter(|s| s.size == shoe_size).collect() +} + +#[cfg(test)] +mod tests { + #[test] + fn filter_by_size() { + let shoes = vec! [ + Shoe { + size: 10, + style: String::from("sneaker"), + }, + Shoe { + size: 13, + style: String::from("sandal"), + }, + Shoe { + size: 10, + style: String::from("boot"), + }, + ]; + + let in_my_size = shoes_in_size(shoes, 10); + + assert_eq! ( + in_my_size, + vec! [ + Shoe { + size: 10, + style: String::from("sneaker"), + }, + Shoe { + size: 10, + style: String::from("boot"), + }, + ] + ); + } +} +``` + +*清单 13-16:使用带有一个捕获 `shoe_size` 闭包的 `filter` 方法* + +这里的 `shoes_in_size` 函数将一个 `shoes` 矢量的所有权,和一个 `shoe_size` 取作参数。他会返回只包含特定尺码鞋子的一个矢量。 + +而在 `shoes_in_size` 的函数体中,这里调用了 `into_iter`,来创建出取得那个矢量所有权的迭代器。随后这里调用了 `filter` 来将那个迭代器,适配为只包含令到那个闭包返回 `true` 那些元素的一个新迭代器。 + +其中的闭包,就从其环境中捕获到他的 `shoe_size` 参数,并将该值于各鞋子的尺码比较,而只保留特定尺码的那些鞋子。最后,这里调用 `collect` 方法,将由那个已适配出的迭代器返回的 `Shoe` 类型值,收集到一个矢量中,`shoes_in_size` 这个函数返回的,便是这个矢量值。 + +这个测试显示,在调用 `shoes_in_size` 时,取回的仅是那些有着与所指定值同样尺寸的鞋子。 + + +## 改进那个 I/O 项目 + +有了迭代器这方面的新知识,这里就可以经由迭代器,改进第 12 章中的那个 I/O 项目,使得代码各处更清楚并更简洁。下面就来看看,迭代器可以怎样改进其中 `Config::build` 函数与 `search` 函数的实现。 + + +### 使用迭代器消除 `clone` + +代码清单 12-6 中,那里曾添加了取 `String` 值的切片,并通过索引到那个切片中并克隆出一些值,从而创建出一个允许拥有这些值的 `Config` 结构体实例的代码。下面清单 13-17 中,就重现了正如清单 12-23 中,那个 `Config::build` 函数的实现: + +文件名:`src/lib.rs` + +```rust +impl Config { + pub fn build(args: &[String]) -> Result { + if args.len() < 3 { + return Err("参数数量不足"); + } + + let query = args[1].clone(); + let file_path = args[2].clone(); + + let ignore_case = env::var("IGNORE_CASE").is_ok(); + + Ok(Config { + query, + file_path, + ignore_case, + }) + } +} +``` + +*清单 13-17:清单 12-23 中 `Config::build` 函数的重现* + +那个时候,就曾说过不要对其中的那两个低效率 `clone` 调用担心,因为将来会把他们消除掉。那么这个时候就是消除他们的时机! + +之所以这里需要 `clone` 方法,是由于这里在参数 `args` 中,有着由一些 `String` 元素构成的切片,而这个 `build` 函数并不拥有 `args`。为返回某个 `Config` 实例的所有权,那里就不得不克隆 `Config` 结构体的 `query` 与 `filename` 字段,如此该 `Config` 实例就能拥有他的这些值。 + +有了关于迭代器的新知识,那么就可以将这个 `build` 函数,修改为取得某个迭代器的所有权作为其参数,而不再是借用一个切片。与其使用对切片长度加以检查,以及所有进入到特定位置的那些代码,这里将运用迭代器功能。由于迭代器将访问到那些值,那么这样就将厘清这个 `Config::build` 函数,正在完成些什么事情。 + +一旦 `Config::build` 取得那个迭代器的所有权,而停止使用借用到的索引操作,你们这里就可以把那个迭代器中的那些 `String` 值,迁移到 `Config` 里去,而非调用 `clone` 方法并构造一个新的内存分配了。 + + +### 直接使用返回的迭代器 + +请打开之前 I/O 项目的 `src/main.rs` 文件,他看起来应是这样的: + +文件名:`src/main.rs` + + +```rust +fn main() { + let args: Vec = env::args().collect(); + + let config = Config::build(&args).unwrap_or_else(|err| { + eprintln! ("解析参数时遇到问题:{err}"); + process::exit(1); + }); + + // --跳过代码-- +} +``` + +这里将首先把在清单 12-24 中的那个 `main` 函数的开头,修改为下面清单 13-18 中,使用迭代器的样子。同样,在更新 `Config::build` 之前,这不会编译。 + +文件名:`src/main.rs` + +```rust +fn main() { + let config = Config::build(env::args()).unwrap_or_else(|err| { + eprintln! ("解析参数时遇到问题:{err}"); + process::exit(1); + }); + + // --跳过代码-- +} +``` + +*清单 13-18:把 `env::args` 的返回值传递给 `Config::build`* + +这个 `env::args` 函数,返回的是个迭代器!不再是将迭代器的值收集到某个矢量,而在随后把一个切片传递给 `Config::build` 了,现在这里直接吧返回自 `env::args` 的迭代器所有权,传递给 `Config::build`。 + +接下来,这里就需要更新 `Config::build` 的定义。在这个 I/O 项目的 `src/data_structures.rs` 文件中,接下来就要想下面这样,修改 `Config::build` 的函数签名。由于尚需更新该函数的函数体,因此这仍不会编译。 + +文件名:`src/lib.rs` + +```rust +impl Config { + pub fn build( + mut args: impl Iterator, + ) -> Result { + // --跳过代码-- +``` + +*清单 13-19:将 `Config::build` 的函数签名,更新为期待得到一个迭代器* + +`env::args` 函数的标准库文档显示,其所返回的迭代器类型为 `std::env::Args`,而那个类型是实现了 `Iterator` 特质的,同时返回的是一些 `String` 值。 + +这里已经更新了这个 `Config::build` 函数的签名,那么参数 `args`,就有了一个带有特质边界 `impl Iterator` 的泛型,而不再是 `&[String]` 类型了。第 10 章 [作为参数的特质](Ch10_Generic_Types_Traits_and_Lifetimes.md#traits-as-paramters) 小节曾讨论过的这种 `impl Trait` 语法的用法,表示 `args` 可以是任何实现了 `Iterator` 类型,并返回一些 `String` 条目的类型。 + +由于这里取得了 `args` 的所有权,且这里通过对 `args` 进行迭代,而将对其进行修改,因此这里可把 `mut` 关键字,添加到这个 `args` 参数的说明中,来将其构造为可变的。 + + +### 使用 `Iterator` 特质的方法取代原先的索引操作 + +**Using `Iterator` Trait Methods Instead of Indexing** + +接下来,这里将修正 `Config::build` 的函数体。由于 `args` 实现了 `Iterator` 特质,因此就明白这里可以在他上面调用 `next` 方法!下面清单 13-20 将清单 12-23 中的代码,更新为了使用 `next` 方法: + +文件名:`src/data_structures.rs` + +```rust +impl Config { + pub fn build( + mut args: impl Iterator, + ) -> Result { + args.next(); + + let query = match args.next() { + Some(arg) => arg, + None => return Err("未曾获取到查询字串"), + }; + + let file_path = match args.next() { + Some(arg) => arg, + None => return Err("未曾获取到文件路径"), + }; + + let ignore_case = env::var("IGNORE_CASE").is_ok(); + + Ok(Config { + query, + file_path, + ignore_case, + }) + } +} +``` + +*清单 13-20:将 `Config::build` 的函数体,修改为使用迭代器方法* + +请记住 `env::args` 返回值中的第一个,是程序的名字。这里是要忽略那个值,而到下一值处,因此这里首先调用了 `next` 并对该返回值什么也没做。其次,这里调用了 `next` 来获取到这里打算将其放入 `Config` 的 `query` 字段的那个值。在 `next` 返回的是一个 `Some` 时,这里使用了一个 `match` 来提取该值。在其返回的是 `None` 时,就表示没有给到足够的参数,同事这里及早地返回了一个 `Err` 值。对于 `filename` 值,这里进行了同样的处理。 + + +### 使用迭代器适配器令到代码更清晰 + +**Making Code Clearer with Iterator Adaptors** + +在这个 I/O 项目的 `search` 函数中,也可以利用到迭代器的优势,该函数重现于下面清单 13-21 中,如同其曾在清单 12-19 中那样: + +文件名:`src/lib.rs` + +```rust +pub fn search<'a>( + query: &str, + contents: &'a str +) -> Vec<&'a str> { + let mut results = Vec::new(); + + for line in contents.lines() { + if line.contains(query) { + results.push(line); + } + } + + results +} +``` + +*清单 13-21:清单 12-19 中 `search` 函数的实现* + +这里可使用迭代器适配器方法,以更精练方式编写出此代码。这样做还实现了避免使用一个可变的中间 `results` 矢量。函数式编程风格(the functional programming style),偏好于将可变状态的数量最小化,从而令到代码更简明。移除掉这个可变状态,可开启使得搜索以并行方式进行的一项未来功能增强,这是由于这里将不再必须对到这个 `results` 矢量的并发访问加以管理。下面清单 13-22 给出了这一修改: + +文件名:`src/lib.rs` + +```rust +pub fn search<'a>( + query: &str, + contents: &'a str +) -> Vec<&'a str> { + contents + .lines() + .filter(|line| line.contains(query)) + .collect() +} +``` + +*清单 13-22:在 `search` 函数中使用迭代器适配器方法* + +回顾这个 `search` 函数的目的,即为返回 `contents` 中所有包含 `query` 的那些行。与清单 13-16 中的 `filter` 示例类型,此代码使用了这个 `filter` 适配器,来只保留 `line.contains(query)` 返回 `true` 的那些行。这里随后使用 `collect()`,收集那些匹配的行。这就简单多了!请在 `search_case_insensitive` 函数中,也完成这同样的改造。 + +> 函数 `search_case_insenstitive` 修改后如下所示: + + +```rust +pub fn search_insensitive<'a>( + query: &str, + contents: &'a str +) -> Vec<&'a str> { + let query = query.to_lowercase(); + + contents + .lines() + .filter(|line| line.to_lowercase().contains(&query)) + .collect() +} +``` + +### 选择循环还是迭代器 + +**Choosing Between Loops or Iterators** + +接着合乎逻辑的问题,便是在所编写代码中,应选取何种样式以及为什么要选择那种样式:是要清单 13-21 原先的实现,还是要清单 13-22 中用到迭代器的版本。绝大多数 Rust 程序员,都首选使用迭代器的样式。开始的时候要掌握这种诀窍有点难,不过一旦摸清各种迭代器适配器以及他们完成的事情,那么迭代器就能较容易地掌握。与其摆弄循环的各种东西,并构建出一些新的矢量值,运用迭代器的代码,关注的则是循环的高级别目标。那么运用迭代器就把一些常见代码,给抽象了出来,如此就更易于看出,特定于该代码的一些概念了,比如迭代器中的各个元素,所必须通过的那种过滤条件。 + +然而这两种实现真的等价吗?直觉上的假定,可能是其中更低级别的循环,将会更快。那么下面就来讨论性能吧。 + + +## 性能比较:循环与迭代器 + +**Comparing Performance: Loops vs. Iterators** + +为确定出是要是要循环,还是使用迭代器,咱们需要搞清楚以下哪种实现更快:带有显式 `for` 循环的 `search` 函数,还是有着迭代器的那个版本。 + +这里通过加载 Sir Arthur Conan Doyle 写的整部 *福尔摩斯历险记(The Adventures of Sherlock Holmes)* 到一个 `String` 中,并在这些内容里查找单词 *the*,运行了一个基准测试。下面就是这个基准测试,分别在使用 `for` 循环版本,与使用迭代器版本的 `search` 函数上的测试结果: + +```console +test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700) +test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200) +``` + +迭代器版本就要稍微快一些!由于这里关键不是要证实这两个版本等价,而是要对怎样从性能方面对两种实现加以比较,有个粗略认知,因此这里不会对这个基准测试代码加以解释(we won't explain the benchmark code here, because the point is not to prove that the two versions are equivalent but to get a general sense of how these two implementations compare performance-wise)。 + +对于更为全面的基准测试,那么就应使用不同大小的各种文本作为其中的 `contents`,不同单词与不同长度单词作为那个 `query`,以及所有类别的其他差异。关键在于这里:尽管迭代器属于高级别的抽象,但他们会被向下编译为如同咱们自己所编写的低级别代码。迭代器是 Rust 的那些 *无代价抽象(zero-cost abstractions)* 之一,这就意味着这种抽象的使用,并不会承担额外的运行时开销。这与 C++ 最初设计者与实现者 Bjarne Stroustrup 在 “Foundation of C++”(2012) 那本书中,所定义的 *无开销(zero-overhead)* 概念类似: + +> 总的来说,C++ 的众多实现,都遵循了无开销原则:用不到的东西,就无需付出代价。并更进了一步:即使用到,亦不会手搓出更良好的代码(the zero-overhead principle: What you don't use, you don't pay for. And further: What you do use, you couldn't hand code any better)。 + +作为另一个示例,以下代码取自某个音频解码器。其中的解码算法,使用了线性预测的数学运算,来根据先前的一些样本,估算出后面的值。此代码使用了迭代器链(an iterator chain),来完成作用域中三个变量:数据的一个 `buffer` 切片,12 个 `coeffecients` 的一个数组,及存储在 `qlp_shift` 中对数据进行偏移的数量,上的一些数学计算。这里已声明出了该示例中的那些变量,但并未给到他们任何值;尽管此代码在其上下文外部并无多少意义,但他仍不失为 Rust 如何将高级别的一些概念,翻译为低级别代码的一个简练的、真实世界下的示例。 + +```rust +let buffer: &mut [i32]; +let coefficients: [i64; 12]; +let qlp_shift: i16; + +for i in 12..buffer.len() { + let prediction = coefficients.iter() + .zip(&buffer[i - 12..i]) + .map(|&c, &s| c * s as i64) + .sum::() >> qlp_shift; + let delta = buffer[i]; + buffer[i] = prediction as i32 + delta; +} +``` + +为了计算出 `prediction` 的值,此代码对 `coefficients` 中 12 个值的每个都进行了迭代,并使用 `zip` 方法将这些系数值与 `buffer` 中的前 12 个值配对起来。随后对这个每个数值对,这里将他们放一起做乘法,对这些结果求和,并将和数中的那些二进制位,往右偏移了 `qlp_shift` 个二进制位。 + +像是音频解码器这样得应用中的那些计算,通常要将性能放在最高的优先级。这里,就创建出了一个迭代器,使用了两个适配器,并在随后消费了那个值。而这段代码会编译到什么样的汇编代码呢?当然,在这本书编写时,他会向下编译到由手写的同样汇编代码。注意相应于对 `coefficients` 中那些值的迭代,是完全没有循环的:Rust 清楚那里有 12 次迭代,因此他(Rust 编译器)就会 “解开(unrolls)” 其中那个循环。所谓 “解开(unrolling)”,是消除循环控制代码方面的开销,而以生成该循环历次迭代的重复代码予以取代的一种优化(*unrolling* is an optimization that removes the overhead of the loop controlling code and instead generates repetitive code for each iteration of the loop)。 + +全部的这些系数,都是被存储在寄存器中的,这意味着访问这些值是极为快速的。运行时是没有数组上的边界检查的。这些 Rust 所能运用的全部优化,就令到最终代码极为高效。既然现在获悉到这一点,那么就可以毫无顾忌的使用迭代器和闭包了!他们使得代码看起来像是在较高的层级,但这样做并不会造成运行时性能下降。 + + + +## 本章小结 + +闭包与迭代器,是 Rust 的两项受函数式编程概念启发的特性。他们带来了 Rust 的有着底层代码性能、但以高级语言清晰表达概念的能力。闭包与迭代器的实现,不会影响到运行时性能。这正是 Rust 致力于提供到无代价抽象(zero-cost abstractions),这一目标的一个方面。 + +既然现在已经改进了这个 I/O 项目的表现力,那么接下来就要看看,`cargo` 的一些别的、将帮助咱们与外界分享这个项目的一些特性了。 diff --git a/src/Ch14_More_about_Cargo_and_Crates-io.md b/src/Ch14_More_about_Cargo_and_Crates-io.md new file mode 100644 index 0000000..bab4466 --- /dev/null +++ b/src/Ch14_More_about_Cargo_and_Crates-io.md @@ -0,0 +1,879 @@ +# Cargo 的其他方面与 Crates.io + +**More About Cargo and Crates.io** + +到目前为止,咱们曾用到的都是 Cargo 中的一些最基本特性,来构建、运行与测试所编写的代码,但 Cargo 可以完成多得多的事情。本章中,就会讨论他的一些别的、更为先进的特性,来展示如何完成以下的这些事情: + +- 经由不同发布配置文件,对构建加以定制(customize your build through release profiles); +- 在 [crates.io](https://crates.io) 上公开一些库; +- 使用工作区将大型项目组织起来(organize large projects with workspaces); +- 安装 [crates.io](https://crates.io) 上的一些库; +- 使用定制命令对 Cargo 加以扩展。 + + +相比本章讲到的,cargo 甚至能完成更多功能,因此对于 Cargo 的全部特性的完整阐释,请参阅 [他的文档](https://doc.rust-lang.org/cargo/)。 + + +## 使用不同发布配置文件,定制不同构建 + +**Customizing Builds with Release Profiles** + +在 Rust 中,所谓 *发布配置文件*,是带有允许程序员对编译代码,有着更多掌控的一些预定义及可定制的配置文件。每个配置文件相对其他配置文件,被独立配置过。 + +Cargo 有着两个主要的发布配置文件:在运行 `cargo build` 时 Cargo 用到的 `dev` 配置文件,以及在运行 `cargo build --release` 时 Cargo 用到的 `release` 配置文件。`dev` 配置文件被定义为有着用于开发的一些良好默认配置,而 `release` 配置文件则有着用于发布构建的一些良好默认配置。 + +从不同构建的输出中,或许对这些配置文件不那么陌生: + +```console +$ cargo build + Finished dev [unoptimized + debuginfo] target(s) in 0.0s +$ cargo build --release + Finished release [optimized] target(s) in 0.0s +``` + +其中的 `dev` 及 `release`,即由编译器用到的两个不同配置文件。 + +在项目的 `Cargo.toml` 文件中未曾显式添加任何 `[profile.*]` 这样的小节时,Cargo 是有所运用的各个配置文件默认设置的。通过添加用于咱们打算定制的任何配置文件的一些 `[profile.*]` 小节,就会覆盖掉这些默认设置的任何子集。比如,下面就是 `dev` 与 `release` 配置文件分别的 `opt-level` 设置的默认值: + +文件名:`Cargo.toml` + +```toml +[profile.dev] +opt-level = 0 + +[profile.release] +opt-level = 3 +``` + +这个 `opt-level` 设置项,控制了 Rust 适用于咱们所编写代码的优化数目,有着范围 `0` 到 `3` 的取值。运用更多优化,会延长编译时间,因此在开发过程中而频繁编译代码时,就会想要更少的优化,来更快地编译,即使产生出的代码运行较慢。因此这默认的 `opt-level` 就是 `0`。而在已准备好发布时,那么就最好用更多时间来编译。咱们将只以发布模式编译一次,但会多次运行那已编译好的程序,因此发布模式就以较长的编译时间,换取到运行较快的代码。那就是 `release` 配置文件的 `opt-level` 默认为 `3` 的原因。 + +通过在 `Cargo.toml` 中,给某个默认值添加不同的值,就可以覆盖掉这个默认值。比如,在打算于开发配置文件中使用优化级别 `1` 时,就可以把下面这两行,添加到项目的 `Cargo.toml`: + +文件名:`Cargo.toml` + +```toml +[profile.dev] +opt-level = 1 +``` + +此代码就覆盖了默认的设置 `0`。现在运行 `cargo build`,Cargo 将使用 `dev` 配置文件的那些默认设置,加上这里对 `opt-level` 的定制修改。由于这里把 `opt-level` 设置为了 `1`,Cargo 就会应用相比于默认设置更多,但并不如发布构建那样多的优化。 + +若要了解这两个配置文件的完整配置项清单及他们的默认设置,请参阅 [Cargo 文档](https://doc.rust-lang.org/cargo/reference/profiles.html)。 + + +## 将代码箱发布到 Crates.io + +**Publishing a Crate to Crates.io** + +前面在项目中,已经用到了 [crates.io](https://crates.io) 上的一些包,然而通过发布咱们自己的包,还可以与其他人分享咱们自己的代码。位于 [crates.io](https://crates.io) 网站的代码箱登记处,会分发咱们的包,因此 crates.io 主要保存了开放源码的代码。 + +Rust 与 Cargo,均有着令到咱们所发布的包,易于为其他人找到并使用的一些特性。接下来就会聊聊一些这样的特性,并讲解怎样发布某个包(how to release a package)。 + + +### 制作一些有用的文档注释 + +**Making Useful Documentation Comments** + +准确地为咱们的包编写文档,将帮助到其他使用者获悉怎样及何时来使用咱们的包,因此投入时间来编写文档是值得的。第 3 章中,就曾讨论过如何使用双斜杠 `//`,来注释 Rust 代码。Rust 还有用于文档的一种特殊注释,通常被称作 *文档注释(documentation comment)*,此类注释将产生出 HTML 文档。这些生成的 HTML,会将给那些想要了解怎样 *使用(use)* 咱们的代码箱,而不是咱们代码箱如何实现的程序员,准备的公开 API 的文档注释,给显示出来。 + +文档注释用的是三斜杠 `///`,而不是双斜杠,并支持用于格式化文本的 Markdown 写法。要把文档注释,放在他们要注释项目之前,紧接着注释项目。下面清单 14-1 给出了名为 `cargo_features_demo` 代码箱中,一个 `add_one` 函数的文档注释。 + +文件名:`src/lib.rs` + +````rust +/// 将一加到所给数字。 +/// # 示例(examples) +/// +/// ``` +/// let arg = 5; +/// let answer = cargo_features_demo::add_one(arg); +/// +/// assert_eq! (6, answer); +/// ``` +pub fn add_one(x: i32) -> i32 { + x + 1 +} +```` + +*清单 14-1:一个函数的文档注释* + +这里对该 `add_one` 函数完成了什么进行了描述,以标题 `示例` 开始了一个小节,随后提供了演示如何使用这个 `add_one` 函数的代码。通过运行 `cargo doc` 命令,就可以生成该文档注释的 HTML 文档。`cargo doc` 命令会运行与 Rust 一起分发的 `rustdoc` 工具,并将生成的 HTML 文档,放在 `target/doc` 目录中。 + +为方便起见,运行 `cargo doc --open` 将构建出当前代码箱文档(以及全部代码箱依赖的文档)的 HTML,并于随后在 web 浏览器中打开得到的结果。导航到那个 `add_one` 函数,就会看到文档注释中的文本,是如何被渲染的,如下图片 1401 中所示: + +![`add_one` 函数的 HTML 文档](images/14-01.png) + +*图 14-01:`add_one` 函数的 HTML 文档* + +**经常用到的一些小节** + +这里用到了清单 14-1 中 `# 示例(examples)` 的 Markdown 标题,来创建出生成 HTML 中,有着标题 “示例(examples)” 的一个小节。下面是代码箱编写者经常在他们文档中,用到的一些别的小节: + +- **终止运行(Panics)**:正被文档注释的函数可能终止运行的情形。不愿他们的程序终止运行的那些调用者,应确保他们不会在这些情形下调用该函数; +- **报错(Errors)**:在该函数返回的是一个 `Result` 时,那么对可能发生的各种错误及何种条件下会引起这些错误被返回进行描述,就能有效帮助到调用者,以便他们可以编写出以不同方式,处理这些不同类别错误的代码来。 +- **安全性(Safety)**:在该函数属于 `unsafe` 的调用时(在后面第 19 章会讨论到不安全 `unsafe`),就应有解释为何该函数属于不安全,以及对该函数所期望的调用者要坚守哪些不变因素进行说明一个小节(if the funciton is `unsafe` to call(we discuss unsafety in Chapter 19), there should be a section explaining why the function is unsafe and covering the invariants that the function expects callers to uphold)。 + + +多数的文档注释,并不需要全部的这些小节,但这仍不失为提醒咱们,代码使用者将有兴趣了解咱们代码哪些方面的一个不错的检查单。 + + +**作为测试的文档注释(Documentation Comments as Tests)** + +在文档注释中添加一些代码块,可有助于演示怎样使用咱们的库,而这样做有着一项额外收获(an additional bonus):运行 `cargo test` 将以测试方式,运行文档中的那些代码示例!没有什么比带有示例的文档更好的了。然而比起由于在文档写好后,代码已被修改而造成的示例不工作,也没有什么更糟糕的了。在清单 14-1 中 `add_one` 函数的文档下,运行 `cargo test` 时,就会在测试结果中看到这样一个小节: + +```console + Doc-tests cargo_features_demo + +running 1 test +test src/lib.rs - add_one (line 7) ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.15s +``` + +而现在要么修改那个函数,要么修改那个示例,总之要让示例中的 `assert_eq!` 终止运行,并再次运行 `cargo tset`,就会看到文档测试(the doc tests)捕获了那个示例,同时 `add_one` 的实现代码,与文档注释中的代码,便失去了相互的同步! + +> **注**:此状况下的输出为: + +```console + Doc-tests cargo_features_demo + +running 1 test +test src/lib.rs - add_one (line 7) ... FAILED + +failures: + +---- src/lib.rs - add_one (line 7) stdout ---- +Test executable failed (exit status: 101). + +stderr: +thread 'main' panicked at 'assertion failed: `(left == right)` + left: `6`, + right: `7`', src/lib.rs:7:1 +stack backtrace: + 0: 0x5620cf499480 - std::backtrace_rs::backtrace::libunwind::trace::h32eb3e08e874dd27 + at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/std/src/../../backtrace/src/back trace/libunwind.rs:93:5 + // ... + 36: 0x0 - + + + +failures: + src/lib.rs - add_one (line 7) + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.15s + +error: doctest failed, to rerun pass `--doc` +``` + +> **注**:执行 `cargo test --doc`,将只运行文档注释中的示例代码。 + +**对其所在的程序项目进行文档注释(Commenting Contained Items)** + +`//!` 这种样式的文档注释,会把文档添加到包含注释的条目,而非那些跟随这些注释的条目。在代码箱根文件中(依惯例即 `src/lib.rs`),或在对代码箱编写文档的模组,抑或在作为整体的模组中,通常会用到这些文档注释(the style of doc comment `//!` adds documentation to the item contains the comments rather than to the items following the comments. We typically use these doc comments inside the crate root file(`src/lib.rs` by convention) or inside a module to document the crate or the module as a whole)。 + +比如,为添加对包含 `add_one` 函数的这个 `cargo_features_demo` 代码箱的目的加以描述的文档,这里就要把以 `//!` 开头的一些文档注释,添加到那个 `src/lib.rs` 文件的开头,如下清单 14-2 中所示: + +文件:`src/lib.rs` + +```rust +//! # Cargo 特性示例代码箱 +//! +//! `cargo_features_demo` 是令到执行某些确切计算更便利 +//! 的一些工具的集合。 +//! + +/// 将一加到所给数字。 +// --跳过代码-- +``` + +*清单 14-2:整体下 `cargo_features_demo` 代码箱的文档* + +请注意由于这里是以 `//!` 而非 `///` 开始的这些注释,因此在以 `//!` 开始的最后一行之后,是没有任何代码的,这里是在给包含了此注释的程序项目,而非紧接着此注释的程序项目编写文档。在此示例中,那个程序项目就是 `src/lib.rs` 文件,这正是代码箱根。这些注释描述了整个的代码箱。 + +在运行 `cargo doc --open` 时,这些注释就会显示在 `cargo_features_demo` 代码箱文档的首页(the front page),位于该代码箱那些公开项目清单之上,如下图 14-02 中所示: + +![渲染后的 `cargo_features_demo` 代码箱的文档](images/14-02.png) + +*图 14-02:渲染后的 `cargo_features_demo` 代码箱的文档, 包括了将该代码箱作为整体进行描述的注释* + +程序项目里的文档注释,用于对代码箱及模组等进行描述,尤其有用。使用它们来解释该容器(the container) 的整体目标,就有助于咱们代码箱的使用者,理解该代码箱的组织。 + + +### 使用 `pub use` 导出好用的公开 API + +**Exporting a Convinient Public API with `pub use`** + +在发布某个代码箱时,其公开 API 结构是主要的考量。用到咱们代码箱的人们,相比咱们自己,对代码箱结构的没有那么熟悉,进而在咱们的代码箱有着大型模组层次结构时,就会难于找到他们打算使用的部分。 + +在第 7 章中,就曾讲到过怎样使用 `pub` 关键字,把一些程序项目构造为公开,以及使用 `use` 关键字,把一些程序项目带入到某个作用域。尽管如此,在开发某个代码箱过程中,对咱们有意义的组织结构(模组树),对于咱们的用户,就可能不那么便利。咱们可能打算把代码箱结构,组织为包含多个级别的层次,但随后打算使用已被定义在该层次结构深处某个类型的人,就可能在查明那个类型是否存在上,遇到麻烦。他们可能还会对必须敲入 `use cargo_features_demo::some_module::another_module::UsefulType;`,而非敲入 `use cargo_features_demo::UsefulType;` 而感到恼火。 + +可喜的是,在代码箱组织结构 *不* 便于其他人在别的库中使用时,咱们并不必须重新调整代码箱的内部组织:相反,咱们可通过使用 `pub use`,重新导出程序项目,而构造出一种不同于咱们私有组织结构的公开组织结构。重新导出(re-export)会取位于某处的一个公开程序项目,并在另一处将其构造为公开,就跟这个项目是在这另一处被定义过一样。 + +比如,假设这里构造了用于对一些美术概念建模的一个名为 `art` 的库。这个库里头有两个模组:包含了两个分别名为 `PrimaryColor` 与 `SeccondaryColor` 枚举的 `kinds` 模组与包含了一个名为 `mix` 函数的 `utils` 模组,如下清单 14-3 中所示: + +文件名:`src/lib.rs` + +```rust +//! # 美术 +//! +//! 建模诸多美术概念的一个库。 + +pub mod kinds { + /// RYB 颜色模型下的主要颜色。 + pub enum PrimaryColor { + Red, + Yellow, + Blue, + } + + /// RYB 颜色模型下的次要颜色。 + pub enum SecondaryColor { + Orange, + Green, + Purple, + } +} + +pub mod utils { + use crate::kinds::*; + + /// 结合两种等量的主要颜色,创建出 + /// 某种次要颜色。 + pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor { + // --跳过代码-- + SecondaryColor::Purple + } +} +``` + +*清单 14-3:带有组织在 `kinds` 与 `utils` 模组中一些程序项目的 `art` 库* + +下图 14-03 给出了由 `cargo doc` 产生出的该代码箱文档首页的样子: + +![列出 `kinds` 与 `utils` 两个模组的 `art` 代码箱文档首页](images/14-03.png) + +*图 14-3:列出 `kinds` 与 `utils` 两个模组的 `art` 代码箱文档首页* + + +请注意 `PrimaryColor` 与 `SecondaryColor` 两个类型,并未在首页上列出,那个 `mix` 函数也没有。要看到他们,就必须点击 `kinds` 与 `utils`。 + +依赖于这个库的另一代码箱,就需要使用将一些将程序项目从 `art` 带入到作用域的 `use` 语句,指定当前所定义的这种模组结构。下面清单 14-4 给出了用到这个 `art` 代码箱中 `PrimaryColor` 与 `mix` 两个项目的代码箱示例: + +文件名:`src/main.rs` + +```rust +use art::kinds::PrimaryColor; +use art::utils::mix; + +fn main() { + let red = PrimaryColor::Red; + let yellow = PrimaryColor::Yellow; + mix(red, yellow); +} +``` + +*清单 14-4:以内部组织结构导出方式下,使用 `art` 代码箱的程序项目* + +> **注**:使用本地未发布代码箱的方法,是在 `Cargo.toml` 的 `[dependencies]` 小节中,要使用的本地未发布代码箱列出。参见 [How to use a local unpublished crate?](https://stackoverflow.com/a/33025972) + +文件:`Cargo.toml` + +```toml +// --跳过代码-- + +[dependencies] +art = { path = "../art" } +``` + +清单 14-4 中,使用了 `art` 代码箱代码的作者,就必须找出那个 `PrimaryColor` 是在 `kinds` 模组里,以及 `mix` 函数是在 `utils` 模组里。那个 `art` 代码箱的模组结构(即模组树),和要用到该代码箱的开发者相比,与那些在 `art` 代码箱上编写代码的开发者要更为密切。对于那些试图搞清楚怎样使用这个 `art` 代码箱的人来说,他的内部组织结构并未包含任何有用信息,而由于用到他开发者,必须搞明白要在那里去查看,并必须在 `use` 语句中指明那些模组名字,因此他的内部组织结构,反而会造成混乱。 + +要从公开 API 中消除内部的组织结构,咱们可以将清单 14-3 中那个 `art` 代码箱的代码,修改为在其顶层出添加上一些 `pub use` 语句,来重新导出那些程序项目,如下清单 14-5 中所示: + +文件名:`src/lib.rs` + +```rust +//! # 美术 +//! +//! 建模诸多美术概念的一个库。 + +pub use self::kinds::PrimaryColor; +pub use self::kinds::SecondaryColor; +pub use self::utils::mix; + +pub mod kinds; +pub mod utils; +``` + +*清单 14-5:添加一些 `pub use` 语句来重新导出程序项目* + +如下图 14-04 中所示,`cargo doc` 为此代码箱所产生出的 API 文档,现在就会在首页上,列出并链接到那些重导出的程序项目,令到 `PrimaryColor` 与 `SecondaryColor` 两个类型,以及那个 `mix` 函数更易于找到。 + +![列出了重导出项目的 `art` 代码箱文档首页](images/14-04.png) + +*图 14-4:列出了重导出项目的 `art` 代码箱文档首页* + +这个 `art` 代码箱的用户,仍然能象下面清单 14-4 中所演示的那样,看到并使用清单 14-3 中的内部结构,抑或他们可以使用清单 14-5 中那种更为便利的结构,如下清单 14-6 中所示: + +文件名:`src/main.rs` + +```rust +use art::mix; +use art::PrimaryColor; + +fn main() { + let red = PrimaryColor::Red; + let yellow = PrimaryColor::Yellow; + mix(red, yellow); +} +``` + +*清单 14-6:使用 `art` 代码箱中那些重导出程序项目的程序* + +在那些其中有着许多嵌套模组的情形下,于代码箱结构的顶部,使用 `pub use` 重导出一些类型,可在那些用到该代码箱的人体验上,造成显著不同。`pub use` 的另一常见用途,则是重导出当前代码箱中某项依赖的一些定义,而把那个依赖代码箱的一些定义,构造为咱们自己代码箱的公开 API。 + +创建出一种有用的公开 API 结构,与其说是一门科学,不如说是一门艺术,同时可以不断迭代,来找出对于代码箱用户运作最佳的那种 API。而选择 `pub use` 则会给到在内部组织代码箱方式上的灵活性,同时解偶了内部结构,与呈现给代码箱用户的组织结构。请查看曾安装过的一些代码箱的代码,来检视他们内部结构,是否不同于他们的公开 API。 + + +### 建立一个 Crates.io 帐号 + +在能够发布代码箱之前,咱们需要在 [crates.io](https://crates.io) 上创建一个帐号,并获取到一个 API 令牌(an API token)。而要这样做,就要访问 [crates.io](https://crates.io) 处的主页,并经由一个 GitHub 帐号登录。(GitHub 帐号目前是必须的,但该站点后面可能会支持创建帐号的其他途径。)在登录之后,就要访问 [https://crates.io/me/](https://creates.io/me/) 处的帐号设置,并找回自己的 API 密钥(API key)。然后使用这个 API 密钥,运行 `cargo login` 命令,如下: + +```console +$ cargo login abcdefghijklmnopqrstuvwxyz012345 +``` + +此命令将告知 Cargo 咱们的 API 令牌,并将其存储在本地的 `~/.cargo/credentials` 文件中。请注意此令牌是个 *秘密(secret)*:不要与其他任何人分享这个秘密。不论以何种方式,让任何人知道了API 令牌,那么就都应该吊销这个 API 令牌,并在 [crates.io](https://crates.io) 上重新生成一个新的令牌。 + +### 给新的代码箱添加元数据 + +**Adding Metadata to a New Crate** + +好比说有个打算发布的代码箱。在发布之前,就将需要在该代码箱的 `Cargo.toml` 文件的 `[package]` 小节中,添加一些元数据。 + +咱们的代码箱将需要一个独特的名字。尽管在本地编写某个代码箱时,可以给代码箱取任意喜欢的名字。但是,[crates.io](https://crates.io) 上代码箱的名字,则是以先到先得的原则分配的(allocated on a first-come, first-served basis)。一旦某个代码箱名字被使用,其他人就不能发布有着那个名字的代码箱了。在尝试发布某个代码箱之前,就要检索一下咱们打算使用的那个名字。在这个名字已被使用时,就需要找到另一个名字,并编辑 `Cargo.toml` 文件中 `[package]` 小节下的 `name` 字段,以在使用那个用于发布的新名字,就像下面这样: + +文件名:`Cargo.toml` + +```toml +[package] +name = "guessing_game" +``` + +即使咱们以及选出了一个独特的名字,在此时运行 `cargo publish` 来发布这个代码箱时,仍将得到一条告警并接着一个报错: + + +```console +cargo publish lennyp@vm-manjaro + Updating crates.io index +warning: manifest has no description, license, license-file, documentation, homepage or repository. +See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info. + Packaging guessing_game v0.1.0 (/home/lennyp/rust-lang/guessing_game) + Verifying guessing_game v0.1.0 (/home/lennyp/rust-lang/guessing_game) + Compiling libc v0.2.132 + Compiling cfg-if v1.0.0 + Compiling ppv-lite86 v0.2.16 + Compiling getrandom v0.2.7 + Compiling rand_core v0.6.3 + Compiling rand_chacha v0.3.1 + Compiling rand v0.8.5 + Compiling guessing_game v0.1.0 (/home/lennyp/rust-lang/guessing_game/target/package/guessing_game-0.1.0) + Finished dev [unoptimized + debuginfo] target(s) in 3.55s + Uploading guessing_game v0.1.0 (/home/lennyp/rust-lang/guessing_game) +error: failed to publish to registry at https://crates.io + +Caused by: + the remote server responded with an error: missing or empty metadata fields: description, license. Please see https://doc.rust-lang.org/cargo/reference/manifest.html for how to upload metadata +``` + +这些错误是由于咱们缺失了一些重要信息:代码箱的描述及许可证是必须的,由此人们就会明白咱们的代码箱完成的什么,以及在何种条件下他们可以使用咱们的代码箱。在 `Cargo.toml` 中,由于代码箱的描述,会与咱们的代码箱一起呈现在搜索结果中,因此请添加仅仅一两句话的描述。而对于那个 `license` 字段,则需要提供 *某个许可证标识符的值(a licence identifier value)*。[Linux 基金会的软件包数据交换站(Linux Foundation's Software Package Data Exchange, SPDX),spdx.org](http://spdx.org/licenses/) 就列出了可供这个值使用的那些标识符。比如,为指明咱们已使用 MIT 许可证,授权咱们的软件包,那么就要添加那个 `MIT` 的许可证标识符: + + +文件名:`Cargo.toml` + +```toml +[package] +name = "guessing_game" +license = "MIT" +``` + +在打算使用某个未出现于 SPDX 中的许可证时,那么就需要把那种许可证的文本放置于某个文件里,把这个文件包含在咱们的项目中,并于随后使用 `license-file` 而非 `license` 键(the `license` key),来指出那个文件的名字。 + +有关哪种许可证适合于你的项目方面的指南,是不在这本书的范围的。Rust 社区的许多人,都以 Rust 项目同样的方式,即采用 `MIT OR Apache-2.0` 双重许可证,授权他们的项目。这种项目授权的实践,表明咱们是也可以通过 `OR`,来指定多个许可证标识符,从而让咱们的项目有着多种许可证。 + +有了一个独特的名字、版本号、代码箱描述,并添加了某个许可证,那么这个准备好发布项目的 `Cargo.toml`,就会看起来像下面这样的: + +文件名:`Cargo.toml` + +```toml +[package] +name = "guessing_game" +license = "MIT" +version = "0.1.0" +description = "一个在其中猜出计算机所选数字的有趣游戏。" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rand = "0.8.3" +``` + +[Cargo 文档](https://doc.rust-lang.org/cargo/) 介绍了可以指定来确保其他人,能更容易发现并使用你的代码箱的其他元数据。 + + +### 发布到 Crates.io + +既然前面已经创建了账号,保存了 API 令牌,选择了代码箱的名字,并指定了必需的元数据,那么就准备好发布了!发布某个代码箱,就会上传某个特定版本到 [crates.io](https://crates.io),供其他人使用。 + +因为发布是 *永久性的(permanent)*,因此要当心。其中的版本绝无可能被覆盖,同时代码无法删除。[crates.io](https://crates.io) 的一个主要目标,是要充当代码的永久存档,以便依赖 [crates.io](https://crates.io) 处代码箱的全部项目构建,将持续工作。允许版本删除,就会领导实现那个目标几无可能。好在咱们可发布的代码箱版本数目上,没有限制。 + +再度运行这个 `cargo publish` 命令。他现在就应成功了: + +```console +$ cargo publish lennyp@vm-manjaro + Updating crates.io index +warning: manifest has no documentation, homepage or repository. +See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info. + Packaging guessing_game-xfossdotcom v0.1.0 (/home/lennyp/rust-lang/guessing_game) + Verifying guessing_game-xfossdotcom v0.1.0 (/home/lennyp/rust-lang/guessing_game) + Compiling libc v0.2.132 + Compiling cfg-if v1.0.0 + Compiling ppv-lite86 v0.2.16 + Compiling getrandom v0.2.7 + Compiling rand_core v0.6.3 + Compiling rand_chacha v0.3.1 + Compiling rand v0.8.5 + Compiling guessing_game-xfossdotcom v0.1.0 (/home/lennyp/rust-lang/guessing_game/target/package/guessing_game-xfossdotcom-0.1.0) + Finished dev [unoptimized + debuginfo] target(s) in 2.73s + Uploading guessing_game-xfossdotcom v0.1.0 (/home/lennyp/rust-lang/guessing_game) +``` + +祝贺!现在咱们与 Rust 社区,分享了咱们的代码,同时其他人就可以将咱们的代码箱,作为他们项目的一项依赖而加以添加了。 + +> **注**:在 Crates.io 上的账号电子邮箱未验证时,将报出如下错误: + +```console +Caused by: + the remote server responded with an error: A verified email address is required to publish crates to crates.io. Visit https://crates.io/me to set and verify your email address. +``` + +### 发布既有代码箱的新版本 + +在修改了咱们的代码箱,并准备好发表一个新版本时,就要修改那个于 `Cargo.toml` 中所指明的 `version` 值并重新发布。请使用 [语义版本控制规则(Semantic Versioning rules)](http://semver.org/) 来依据曾作出的修改类别,确定出适当的下一版本编号。然后就运行 `cargo publish` 来上传这个新版本。 + + +### 使用 `cargo yank` 弃用 Crates.io 上的一些版本 + +**Depracating Versions from Crates.io with `cargo yank`** + +尽管咱们无法移除某个代码箱的一些先前版本,但咱可以阻止任何今后的项目,将他们添加作新的依赖项。在某个代码箱版本由于某种原因或别的问题而损坏时,这样做是有用的。在诸如此类的情形中,Cargo 是支持将某个代码箱版本 *抽出来* 的(in such situations, Cargo supports *yanking* a crate version)。 + +抽出某个版本,就会在允许所有依赖该版本的项目继续工作的同时,阻止新的项目依赖那个版本。本质上,一次版本抽出,就表示有着 `Cargo.lock` 的全部项目不会破坏,同时任何今后生成的 `Cargo.lock` 文件,都不会使用这个被抽出的版本了。 + +要抽出代码箱的某个版本,就要在先前已发布的那个代码箱目录中,运行 `cargo yank` 并指定要抽出哪个版本。比如,在曾发布了一个名为 `guessing_game` 代码箱的 `0.1.0` 版本,并打算抽出他时,那么就要在 `guessing_game` 的项目目录下,运行下面的命令: + +```console +$ cargo yank --vers 0.1.0 4s lennyp@vm-manjaro + Updating crates.io index + Yank guessing_game-xfossdotcom@0.1.0 +``` + +通过添加 `--undo` 到这个命令,咱们还可以撤销某次抽出,而运行一些项目再度开始依赖于某个版本: + +```console +$ cargo yank --vers 0.1.0 --undo lennyp@vm-manjaro + Updating crates.io index + Unyank guessing_game-xfossdotcom@0.1.0 +``` + +抽出某个版本,*不会* 删除任何代码。比如,此操作就无法删除那些不小心上传的机密信息。若发生了机密信息被上传的情况,那么就必须立即重置这些机密信息。 + + +## Cargo 工作区 + +**Cargo Workspaces** + +在第 12 章中,曾构建了包含一个二进制代码箱和一个库代码箱的包(a package)。随着项目的不可开发,就会发现那个库代码箱会持续变大,而咱们就会想要将咱们的包,进一步拆分为多个库代码箱。Cargo 提供了叫做 *工作区(workspace)* 的特性,可帮助管理多个先后开发的相关包。 + +> ***注***:总结 Rust 开发的层次结构如下:工作区(workspace) -> 包(package) -> 代码箱(crate) -> 模组(module) -> 语句(statement)。 + +### 创建工作区 + +*工作区*(a *workspace*)是共享了同一 `Cargo.lock` 文件及输出目录的一个包集合。下面就来构造一个用到工作区的项目 -- 这里将使用一些简单代码,这样咱们就可以着重于该工作区的结构上。组织工作区有多种方式,因此这里将只给出一种常用的方式。这里将有着包含一个二进制代码箱,及两个库代码箱的工作区。其中的二进制代码箱,将提供依赖于那两个库代码箱的 `main` 功能。其中一个库代码箱,将提供一个 `add_one` 函数,而另一个则会提供 `add_two` 函数。这三个代码箱,都将是同一工作区的组成部分。这里将以创建该工作区的目录开始: + +```console +$ mkdir add +$ cd add +``` + +接着,在那个 `add` 目录中,就要创建一个将对整个工作区加以配置的 `Cargo.toml` 文件了。这个文件不会有 `[package]` 小节。相反,他会以一个 `[workspace]` 小节打头,这将允许咱们通过指定有着这里二进制代码箱的那个包的路径,而把一些成员添加到这个工作区;在此情形下,那个路径就是 `adder`: + +文件名:`Cargo.toml` + +```toml +[workspace] +members = [ + "adder", +] +``` + +再接着,这里将通过在这个 `add` 目录里头,运行 `cargo new` 创建出那个 `adder` 二进制代码箱: + +```console +$ cargo new adder + Created binary (application) `adder` package +``` + +到这里,就可以通过运行 `cargo build`,构造这个工作区了。这个 `add` 目录下的那些文件,看起来应像下面这样: + +```console +. +├── adder +│   ├── Cargo.toml +│   └── src +│   └── main.rs +├── Cargo.lock +├── Cargo.toml +└── target +``` + +这个工作区在顶级有个 `target` 目录,那些编译好的物件(the compiled artifacts)就会放入到其中;那个 `adder` 包则并无其自己的 `target` 目录。即使在 `adder` 目录内部运行 `cargo build`,那些编译出的物件,仍会以位处于 `add/target` 而告终,而不会在 `add/adder/target` 目录里。Cargo 之所以像这样来架构这个 `target` 目录,是因为工作区中的那些代码箱,是为了依赖于彼此。若各个代码箱都有其自己的 `target` 目录,那么各个代码箱为了把编译成的物件放在自己的 `target` 目录中,而不得不重新编译工作区中的各个其他代码箱。通过共用一个 `target` 目录,这些代码箱就可以避免不必要的重构建。 + + +### 创建工作区中的第二个包 + +**Creating the Second Package in the Workspace** + +接下来,就要创建出工作区中的另一个成员包,并将其叫做 `add_one`。请修改顶层的 `Cargo.toml`,在其中的 `members` 清理里指明 `add_one` 的路径: + +文件名:`Cargo.toml` + +```toml +[workspace] + +members = [ + "adder", + "add_one", +] +``` + +随后生成一个名为 `add_one` 的新库代码箱: + +```console +$ cargo new add_one --lib lennyp@vm-manjaro + Created library `add_one` package +``` + +这个 `add` 目录现在应该有这些目录与文件: + +```console +. +├── adder +│   ├── Cargo.toml +│   └── src +│   └── main.rs +├── add_one +│   ├── Cargo.toml +│   └── src +│   └── lib.rs +├── Cargo.lock +├── Cargo.toml +└── target +``` + +在那个 `add_one/src/lib.rs` 文件中,给添加一个 `add_one` 函数: + +文件名:`add_one/src/lib.rs` + +```rust +pub fn add_one(x: i32) -> i32 { + x + 1 +} +``` + +现在咱们就可以让有着这里二进制代码箱的 `adder` 包,依赖于这个有着这里的库的 `add_one` 包了。首先,这里将需要把有关 `add_one` 的路径依赖(a path dependency),添加到 `adder/Cargo.toml`。 + +文件名:`adder/Cargo.toml` + +```toml +[dependencies] +add_one = { path = "../add_one" } +``` + +Cargo 不会假定工作区中的代码箱将各自相互依赖,因此这里需要显示说明这些依赖关系。 + +接下来,就要在 `adder` 代码箱中,使用那个 `add_one` 函数(位于 `add_one` 代码箱中)。打开 `adder/src/main.rs` 文件,并在其顶部使用一行 `use`,带入那个新的 `add_one` 库代码箱到作用域中。随后修改其中的 `main` 函数,来调用那个 `add_one` 函数,如下清单 14-7 中所示。 + +文件名:`adder/src/main.rs` + +```rust +use add_one::add_one; + +fn main() { + let num = 10; + println!("你好,世界!{num} 加一为 {}!", add_one(num)); +} +``` + +*清单 14-7:在 `adder` 代码箱中使用 `add_one` 库代码箱* + +下面就来通过在 `add` 目录的顶层,运行 `cargo build` 构建出这个工作区! + +```console +$ cargo build lennyp@vm-manjaro + Compiling add_one v0.1.0 (/home/lennyp/rust-lang/add/add_one) + Compiling adder v0.1.0 (/home/lennyp/rust-lang/add/adder) + Finished dev [unoptimized + debuginfo] target(s) in 0.40s +``` + +要在 `add` 目录运行这个二进制代码箱,是可以通过使用 `cargo run` 的 `-p` 命令行参数及包名字,指定出要运行工作区中哪个包的: + +```console +$ cargo run -p adder lennyp@vm-manjaro + Compiling adder v0.1.0 (/home/lennyp/rust-lang/add/adder) + Finished dev [unoptimized + debuginfo] target(s) in 0.35s + Running `target/debug/adder` +你好,世界! + 10 加 1 为 11! +``` + +这会运行 `adder/src/main.rs` 中的代码,其依赖于那个 `add_one` 代码箱。 + + +### 于工作区中依赖外部代码箱 + +**Depending on an External Package in a Workspace** + +请注意工作区在其顶层,只有一个 `Cargo.lock` 文件,而非在各个代码箱目录中都有 `Cargo.lock`。这确保了工作区的全部代码箱,都使用着同一版本的所有依赖。若这里把 `rand` 包分别添加到 `adder/Cargo.toml` 及 `add_one/Cargo.toml` 文件,Cargo 就会就这两个版本的 `rand` 加以解析,并将其记录在这一个的 `Cargo.lock` 中。 + +令到工作区中全部代码箱使用同样的那些依赖,就意味着这些代码箱将始终相互兼容。下面就来把 `rand` 代码箱添加到 `add_one/Cargo.toml` 文件的 `[dependencies]` 小节,从而就可以在 `add_one` 代码箱中使用这个 `rand` 代码箱: + +文件名:`add_one/Cargo.toml` + +```toml +rand = "0.8.3" +``` + +现在就可以添加 `use rand;` 到 `add_one/src/lib.rs` 文件了,而通过在 `add` 目录运行 `cargo build` 构建这整个工作区,就会带入并编译那个 `rand` 代码箱。由于这里并未引用那个已带入到作用域中的 `rand`,因此这里会收到一条告警: + +```console +$ cargo build lennyp@vm-manjaro + Updating crates.io index + Downloaded rand_core v0.6.4 + Downloaded ppv-lite86 v0.2.17 + Downloaded getrandom v0.2.8 + Downloaded libc v0.2.137 + Downloaded 4 crates (681.6 KB) in 1.29s + Compiling libc v0.2.137 + Compiling cfg-if v1.0.0 + Compiling ppv-lite86 v0.2.17 + Compiling getrandom v0.2.8 + Compiling rand_core v0.6.4 + Compiling rand_chacha v0.3.1 + Compiling rand v0.8.5 + Compiling add_one v0.1.0 (/home/lennyp/rust-lang/add/add_one) +warning: unused import: `rand` + --> add_one/src/lib.rs:1:5 + | +1 | use rand; + | ^^^^ + | + = note: `#[warn(unused_imports)]` on by default + +warning: `add_one` (lib) generated 1 warning + + Compiling adder v0.1.0 (/home/lennyp/rust-lang/add/adder) + Finished dev [unoptimized + debuginfo] target(s) in 6.76s +``` + +那个顶层的 `Cargo.lock`,现在就包含了有关 `add_one` 对 `rand` 的依赖信息。但是,及时 `rand` 在该工作区中的某处被用到,在未将 `rand` 添加到其他代码箱的 `Cargo.toml` 文件之前,是不能在其他代码箱中使用他的。比如,若这里把 `use rand;` 添加到 `adder` 包的 `adder/src/main.rs` 文件,就会得到一个报错: + +```console +$ cargo build lennyp@vm-manjaro + --跳过前面的告警-- + Compiling adder v0.1.0 (/home/lennyp/rust-lang/add/adder) +error[E0432]: unresolved import `rand` + --> adder/src/main.rs:1:5 + | +1 | use rand; + | ^^^^ no external crate `rand` + +For more information about this error, try `rustc --explain E0432`. +error: could not compile `adder` due to previous error +``` + +要修正整个错误,就要编辑 `adder` 包的 `Cargo.toml` 文件,并也表明 `rand` 是其的一个依赖项。构建这个 `adder` 包就会把 `rand`,添加到 `Cargo.lock` 中 `adder` 的依赖项清单,但不会有额外的 `rand` 拷贝会被下载。Cargo 已确保工作区中用到这个 `rand` 包每个包中的每个代码箱,都将使用同一版本,从而节省了空间,并确保了工作区中的那些代码箱都将兼容于彼此。 + + +### 添加测试到工作区 + +**Adding a Test to a Workspace** + +为说明另一项改进,下面来添加 `add_one` 代码箱里头,`add_one::add_one` 函数的一个测试: + +文件名:`add_one/src/lib.rs` + +```rust +pub fn add_one(x: i32) -> i32 { + x + 1 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add_one(2); + assert_eq!(result, 3); + } +} +``` + +此时在顶层的 `add` 目录运行 `cargo test`。在像这样组织的工作区中运行 `cargo test`,就会运行工作区中全部代码箱的那些测试: + +```console +$ cargo test lennyp@vm-manjaro + Compiling add_one v0.1.0 (/home/lennyp/rust-lang/add/add_one) + Compiling adder v0.1.0 (/home/lennyp/rust-lang/add/adder) + Finished test [unoptimized + debuginfo] target(s) in 0.68s + Running unittests src/lib.rs (target/debug/deps/add_one-837c2ad0efe6b80c) + +running 1 test +test tests::it_works ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Running unittests src/main.rs (target/debug/deps/adder-2277ab1084738161) + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Doc-tests add_one + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +``` + +输出的首个部分,显示 `add_one` 代码箱中那个 `it_works` 测试通过了。接下来的小节显示,在 `adder` 代码箱中找到零个测试,而随后那个最后小节,显示在 `add_one` 代码箱中找到零个文档测试。(*注*:二进制代码箱中不会有文档测试?) + +这里还可以通过使用 `-p` 命令行标志及指定要测试的代码箱名字,在顶层目录处运行工作区中某个特定代码箱的那些测试: + + +```console +$ cargo test -p add_one lennyp@vm-manjaro + Finished test [unoptimized + debuginfo] target(s) in 0.01s + Running unittests src/lib.rs (target/debug/deps/add_one-837c2ad0efe6b80c) + +running 1 test +test tests::it_works ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Doc-tests add_one + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +``` + +此输出展示了只运行那个 `add_one` 代码箱测试,而未运行 `adder` 代码箱测试的 `cargo test`。 + +在将工作区中的代码箱发布到 `crates.io` 时,工作区中的各个代码箱,则将需要被单独发布。与 `cargo test` 类似,可通过使用 `-p` 命令行标志,并指明打算发布的那个代码箱名字,发布工作区中的某个特定代码箱。 + +作为附加练习,请以与 `add_one` 代码箱类似方式,把 `add_two` 添加到这个工作区! + +在项目日渐增长时,就要考虑使用工作区:相比于一大块代码,搞明白较小的、单独的组件,要容易一些。再者,把代码箱保持在一个工作区中,当工作区中的那些代码箱经常同时被修改时,就能令到他们之间的协作更容易。 + + +## 使用 `cargo install` 安装 Crates.io 上的二进制代码箱 + +**Installing Binaries from Crates.io with `cargo install`** + +`cargo install` 命令允许咱们在本地安装和使用二进制的代码箱。这种用法的目的,不是要替换系统包(system packages);其宗旨是为 Rust 开发者提供安装其他人已在 [crates.io](https://crates.io) 上分享工具的一种便利方式。请注意咱们只能安装有着二进制目标的那些包。所谓 *二进制目标*(a *binary target*),即与本身为非可运行,而适合于在其他程序中包含的库目标(a libary target)相反的,在代码箱有着一个 `src/main.rs` 文件,或被指定为二进制的另一文件时,所创建出的那个可以运行的程序。通常,代码箱会在 `README` 文件中,有着关于其是否为库代码箱,还是有着二进制目标,或二者皆具方面的信息。 + +使用 `cargo install` 安装的全部二进制程序文件,都是被存储在安装根的 `bin` 文件中(in the installation root's `bin` folder)。在使用 `rustup.rs` 安装的 Rust,且未有做任何定制配置时,那么这个目录将是 `$HOME/.cargo/bin`。为了能够运行那些使用 `cargo install` 安装的程序,就要确保那个目录是在 `$PATH` 中。 + +> *注*:可在任意位置运行 `cargo install` 命令来安装某个 Crates.io 上的 Rust 二进制程序,这些程序都将被安装在 `$HOME/.cargo/bin` 之下。若已安装了某个 Rust 程序后再安装他,那么就会有如下输出: + +```console +$ cargo install ripgrep 1m 4s lennyp@vm-manjaro + Updating crates.io index + Ignored package `ripgrep v13.0.0` is already installed, use --force to override +``` + +比如,在第 12 章中,曾提到有个名为 `ripgrep` 用于检索文件的 `grep` 的 Rust 实现。要安装 `ripgrep`,就可以运行如下命令: + +```console +$ cargo install ripgrep lennyp@vm-manjaro + Updating crates.io index + Installing ripgrep v13.0.0 + Compiling memchr v2.5.0 + Compiling cfg-if v1.0.0 + Compiling libc v0.2.137 + Compiling log v0.4.17 + Compiling proc-macro2 v1.0.47 + Compiling lazy_static v1.4.0 + Compiling regex-automata v0.1.10 + Compiling quote v1.0.21 + Compiling unicode-ident v1.0.5 + Compiling bstr v0.2.17 + Compiling syn v1.0.103 + Compiling aho-corasick v0.7.20 + Compiling regex-syntax v0.6.28 + Compiling serde_derive v1.0.147 + Compiling encoding_rs v0.8.31 + Compiling serde v1.0.147 + Compiling regex v1.7.0 + Compiling grep-matcher v0.1.5 + Compiling serde_json v1.0.89 + Compiling unicode-width v0.1.10 + Compiling fnv v1.0.7 + Compiling same-file v1.0.6 + Compiling once_cell v1.16.0 + Compiling thread_local v1.1.4 + Compiling globset v0.4.9 + Compiling textwrap v0.11.0 + Compiling encoding_rs_io v0.1.7 + Compiling memmap2 v0.5.8 + Compiling bitflags v1.3.2 + Compiling crossbeam-utils v0.8.14 + Compiling bytecount v0.6.3 + Compiling itoa v1.0.4 + Compiling ryu v1.0.11 + Compiling strsim v0.8.0 + Compiling termcolor v1.1.3 + Compiling clap v2.34.0 + Compiling grep-searcher v0.1.10 + Compiling atty v0.2.14 + Compiling base64 v0.13.1 + Compiling grep-printer v0.1.6 + Compiling grep-cli v0.1.6 + Compiling grep-regex v0.1.10 + Compiling ripgrep v13.0.0 + Compiling walkdir v2.3.2 + Compiling ignore v0.4.18 + Compiling grep v0.2.10 + Compiling num_cpus v1.14.0 + Finished release [optimized + debuginfo] target(s) in 1m 09s + Installing /home/lennyp/.cargo/bin/rg + Installed package `ripgrep v13.0.0` (executable `rg`) +``` + +输出的最后两行,显示了那个已安装二进制 Rust 程序的位置与名字,在 `ripgrep` 这个示例中,名字即为 `rg`。而由于正如前面提到的那样,该安装目录是在 `$PATH` 中,因此随后就可以运行 `rg --help`,进而启动一个用于检索文件的更快、更具 Rust 风格的工具了! + + +## 使用定制命令对 Cargo 进行扩展 + +**Extending Cargo with Custom Commands** + +Cargo 被设计为在无需修改 Cargo 下,就可以使用一些新的子命令,对其加以扩展。当 `$PATH` 中有着一个名为 `cargo-something` 的二进制程序时,那么就可通过运行 `cargo something`,将其作为某个 Cargo 的子命令一样运行他。像这样的定制命令,还会在运行 `cargo --list` 被列出来。这种使用 `cargo install` 来安装扩展,并在随后就跟运行内建的 Cargo 工具一样运行他们,正是 Cargo 之设计的一项超级便利的好处! + + +## 本章小节 + +运用 Cargo 与 [crates.io](https://crates.io) 进行代码的分享,正是令到 Rust 生态对于许多不同任务都有用的一个方面。Rust 的标准库是小型且稳定的,但在不同于语言本身的时间线上,代码箱则是易于共享、运用以及改进的。请不要羞于在 [crates.io](https://crates.io) 上分享对自己有用的代码;那些代码或许同样对其他人也是有用的! diff --git a/src/Ch15_Smart_Pointers.md b/src/Ch15_Smart_Pointers.md new file mode 100644 index 0000000..093558e --- /dev/null +++ b/src/Ch15_Smart_Pointers.md @@ -0,0 +1,1437 @@ +# 灵巧指针 + +**Smart Pointers** + +所谓 *指针* (a *pointer*),即包含了内存中某个地址变量的一般概念。该地址引用了,或者说 “指向” 一个另外数据。Rust 中最常见的指针类别,便是在第 4 章中就曾了解过的引用。引用是以地址符号 `&` 所表示的,并借用了其所指向的那个值。除了对数据进行引用,以及没有开销外,他们并无什么特别的能力。 + +而另一方面的 *灵巧指针* (*smart pointers*),则是些表现为指针,而同时有着额外元数据及能力的一些数据结构。灵巧指针这个概念,并非 Rust 所独有的:灵巧指针起源于 C++,且在其他语言中也存在。Rust 有着定义在标准库中,提供了超出语言参考所提供到功能的各种各样的灵巧指针。这里将检视灵巧指针的几个不同示例,包括一种 *引用计数*(*reference counting*) 的灵巧指针类型,来探索这个一般概念。引用计数这个指针,通过追踪数据所有者的数目,实现了允许数据有着多个所有者,在没有所有者剩下时,就清除该数据。 + +具有所有权和借用概念下的 Rust,在引用与灵巧指针之间,就有了另外的区别:引用只会借用数据,而在许多情形下,灵巧指针则 *拥有(own)* 他们所指向的数据。 + +尽管此时这里尚未像这样调用到他们,但本书中已经遇到过少数几个灵巧指针了,就包括第 8 章中的 `String` 与 `Vec`。这两种类型之所以被算作灵巧指针,是由于他们拥有一些内存,同时允许咱们操纵那些内存。他们还有着元数据与额外能力或一些保证。比如,`String` 就将其容量存储为元数据,并有着确保其数据始终是有效 UTF-8 的额外能力。 + +灵巧指针通常是使用结构体实现的。不同于寻常结构体,灵巧指针实现了 `Deref` 与 `Drop` 特质。`Deref` 特质允许该灵巧指针结构体的实例,像引用那样行事,如此咱们就可以编写出使用引用或灵巧指针的代码。而那个 `Drop` 特质,则允许对在该灵巧指针超出作用域时,要运行的代码加以定制。本章中,就会讨论这两种特质,并演示他们为何对灵巧指针是重要的。 + +有鉴于这种灵巧指针模式,是 Rust 中一种频繁用到的一般设计模式,因此本章不会涵盖每种现有灵巧指针。许多的库都有他们自己的灵巧指针,同时咱们也可以编写自己的灵巧指针。这里将讲到标准库中最常用的一些灵巧指针: + +- 用于在内存堆上分配一些值的 `Box`; +- 实现多重所有权的引用计数类型 `Rc`(`Rc`, a reference counting type that enables multiple ownership); +- 经由那个强制在运行时,而非编译时进行借用规则检查的 `RefCell`,而访问到的 `Ref` 与 `RefMut` (`Ref` and `RefMut`, accessed through `RefCell`, a type that enforces the borrowing rules at runtime instead of compile time)。 + +此外,这里将讲到其间可变类型,暴露出用于修改某个内部值的 API 的 *内部可变* 模式(the *interior mutability* pattern)。这里还会讨论 *引用环*:他们会怎样泄露内存以及如何防止出现引用环(*reference cycles*: how they can leak memory and how to prevent them)。 + +下面就来切入正题吧! + + +## 使用 `Box` 来指向内存堆上的数据 + +最直接了当的灵巧指针,便是 *匣子(box)* 了,其类型写作 `Box`。这些匣子实现了将数据存储在堆上,而非在栈上。留存在栈上的,是到堆数据的指针。请参考第 4 章,回顾一下栈与堆的区别。 + +除了其数据是存储在内存堆而非栈上之外,匣子数据结构并无性能方面的开销。但他们也没有什么额外能力。在下面这些场景中,就会经常用到他们: + +- 当有着在编译时大小未知的类型,而咱们又要在对确切大小有要求的上下文中,使用那种类型的值时(when you have a type whose size can't be known at compile time and you want to use a value of that type in a context that requires an exact size); +- 有着大量数据,并打算在转移所有权的同时,确保这些数据不会被拷贝时(when you have a large amount of data and you want to transfer ownership but ensure the data won't be copied when you do so); +- 在希望拥有某个值,并只关心其为某个实现了特定特质的类型,而非为某个具体类型(when you want to own a value and you care only that it's a type that implements a particular trait rather than being of a specific type)。 + +这第一种情形将在 [使用匣子实现递归类型](#enabling-recursive-tpes-with-boxes) 小节演示。在第二种情形下,大量数据所有权的转移,会由于这些数据在栈上拷来拷去。为改进这种情形下的性能,就可以将这些大量数据存储在内存堆上的一个匣子中。随后,就只有少量的指针数据,在栈上拷贝了,同时其引用的数据,还是呆在堆上的一个地方。第三种情形被称为 *特质对象* (*trait object*),而第 17 章中,用了一整个小节,[“使用实现具有多个类型值的特质对象”](Ch17_Object_Oriented_Programming_Features_of_Rust.md#using-trait-objects-that-allow-for-values-of-different-types),来只讲解那个方面。因此这里掌握的东西,还会在第 17 章中用到。 + +### 使用 `Box` 在内存堆上存储数据 + +在讨论 `Box` 的内存堆存储用例之前,这里将讲解一些其语法即怎样与存储在 `Box` 里头的值交互。 + +下面清单 15-1 给出了怎样使用匣子,把一个 `i32` 的值存储在堆上: + +文件名:`src/main.rs` + +```rust +fn main() { + let b = Box::new(5); + println! ("b = {}", b); +} +``` + +*清单 15-1:使用匣子将一个 `i32` 值存储在堆上* + +这里将变量 `b`,定义为有个指向值 `5` 的 `Box` 类型值,值 `5` 被分配在堆上的。此程序将打印出 `b = 5`;在此示例中,即可以与该数据在栈上类似的方式,访问那个匣子中的数据。就跟所有自有值一样(just like any owned value),在某个匣子超出作用域,即 `b` 不处于 `main` 的最后时,他就会被解除内存分配。这种内存解除分配,既发生于这个匣子(存储在栈上的),同时也发生于匣子所指向的数据(存储于内存堆上)。 + +### 使用匣子数据结构,实现递归数据类型 + +**Enabling Recursive Types with Boxes** + +*递归类型* (*recursive type*)的值,可以拥有作为其一部分、相同类型的另一值。由于 Rust 需要在编译时,清楚某个类型占据多少的内存空间,那么递归类型就引出了一个问题。然而,递归类型这种嵌套值,理论上会无穷尽地持续,因此 Rust 就无法搞清楚该递归类型值,需要多少内存空间。由于匣子都有已知大小,因此通过把一个匣子插入到递归类型的定义中,就可以实现递归类型。 + +作为递归类型的示例,下面就来探讨一下 *构造* 列表(the *cons*(__cons__ tructs) list)。这是在一些函数式编程语言中,通常会发现的一种数据类型。下面将定义的这个构造列表数据类型,除了其中的递归之外,则是很简单的了;因此,这个示例中咱们要解决的那些概念,在今后切入到涉及递归类型更复杂情形的任何时候,都将是有用的。 + + +**构造列表的更多信息** + +所谓 *构造列表* (*cons list*),是来自 Lisp 编程语言及其方言的一种数据结构,并由一些嵌套对值组成,同时就是 Lisp 版本的链表(is made up of nested pairs, and is the Lisp version of a linked list)。他的名字来源于 Lisp 中,从其两个参数构造出一个新数值对的 `cons` 函数(是 “ __cons__ truct” 函数的缩写)。通过在由一个值与另一数值对构成的数值对上调用 `cons`,就可以构造出由一些递归数值对,所组成的构造链表。 + +比如,下面就是一个包含了列表 1、2、3 的构造列表的伪代码表示,其中各个列表分别位于一些圆括号中: + +```lisp +(1, (2, (3, Nil))) +``` + +构造列表中的各个条目,均包含了两个元素:当前条目值与下一条目。列表中最后那个条目,只包含了叫做 `Nil` 的值,而没有下一条目。构造列表是有递归调用 `cons` 函数产生出的。表示该递归之基础的规范名称,是为 `Nil`(the canonical name to denote the base case of the recursion is `Nil`)。请注意这不同于本书第 6 中,指无效或缺失值的 “null” 或 “nil” 概念。 + +在 Rust 中,构造列表不是一种常用数据结构。多数时候咱们在 Rust 中有着一个条目清单时,`Vec` 都是要用到的更佳选择。否则,那些更为复杂的递归数据类型,在一些不同场合,*就会是* 有用处的了,而将构造列表作为本章的开头,咱们就可以心无旁骛地探讨匣子数据结构,怎样实现递归数据类型的定义。 + +下面清单 15-2 包含了构造列表的一种枚举定义。请注意由于其中的 `List` 类型没有已知大小,此代码还不会编译,对此这里将会加以演示。 + +文件名:`src/main.rs` + +```rust +enum List { + Cons(i32, List), + Nil, +} +``` + +*清单 15-2:定义一个枚举来表示 `i32` 值构造列表数据结构的首次尝试* + +> **注意**:这里为此示例目的,而实现的一个仅保存 `i32` 值的构造列表。这里本可以如同第 10 章中所讨论的那样,使用泛型来实现,从而定义出一个可存储任何类型值的构造列表。 + +使用这个 `List` 类型来存储列表 `1, 2, 3`,看起来就会像下面清单 15-3 中的代码: + +文件名:`src/main.rs` + +```rust +use crate::List::{Cons, Nil}; + +fn main() { + let list = Cons(1, Cons(2, Cons(3, Nil))); +} +``` + +*清单 15-3:使用那个 `List` 枚举来存储列表 `1, 2, 3`* + +其中第一个 `Cons` 保存着 `1` 与另一 `List` 值。该 `List` 值,又是另一保存了 `2` 与另一 `List` 的 `Cons` 值。这个 `List` 值则为又一个保存了 `3` 与一个最终为 `Nil` 的 `List` 值的 `Cons` 值,那个非递归变种(`Nil`),标志着这个列表的结束。 + +在尝试编译清单 15-3 中的时候,就跟得到下面清单 15-4 中所给出的报错: + +```console +$ cargo run lennyp@vm-manjaro + Compiling sp_demos v0.1.0 (/home/lennyp/rust-lang/sp_demos) +error[E0072]: recursive type `List` has infinite size + --> src/main.rs:1:1 + | +1 | enum List { + | ^^^^^^^^^ recursive type has infinite size +2 | Cons(i32, List), + | ---- recursive without indirection + | +help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable + | +2 | Cons(i32, Box), + | ++++ + + +For more information about this error, try `rustc --explain E0072`. +error: could not compile `sp_demos` due to previous error +``` + +*清单 15-4:在尝试定义一个递归枚举时收到的报错* + +该错误指出这个类型 “有着无穷大小(has infinite size)。”原因在于这里已把 `List` 定义为了有着一个递归的变种:其直接拥有其自身类型的另一个值。如此一来,Rust 就无法找出他需要多少空间来存储一个 `List` 类型值。接下来就要深究收到这个错误的原因。首先,这里将审视一下,Rust 是怎样确定出,他需要多少内存空间来存储某个非递归类型值。 + +**计算非递归类型的大小** + +回顾在第 6 章中讨论枚举的定义时,于清单 6-2 中曾定义的那个 `Message` 枚举: + +```rust +enum Message { + Quit, + Move { x: i32, y: i32 }, + Write(String), + ChangeColor(i32, i32, i32), +} +``` + +要确定出给一个 `Message` 值分配多少内存空间,Rust 就要遍历其中的各个变种,以发现哪个变种需要最多的空间。Rust 会发现 `Message::Quit` 无需任何空间,`Message::Move` 需要存储两个 `i32` 值的足够空间,并如此等等。由于只会用到一个变种,因此某个 `Message` 值将需要的最大内存空间,就是他要用于存储其变种中最大的那个。 + +请将这个情况与 Rust 尝试确定出诸如清单 15-2 中, `List` 枚举这种递归类型所需内存空间数量对比。编译器以检视那个保存了一个类型 `i32` 值,与一个类型 `List` 值的 `Cons` 变种开始。那么,`Cons` 就需要等同于一个 `i32` 加一个 `List` 大小数量的内存空间。而要计算出 `List` 类型需要多少内存,编译器就会检视这些变种,以其中的 `Cons` 变种开始。这个 `Cons` 变种保存了一个类型 `i32` 的值,与一个类型 `List` 的值,而这个过程会无限持续,如下图 15-1 中所示。 + +![由无限的 `Cons` 变种组成的一个无线 `List`](images/15-01.svg) + +*图 15-01:由无限的 `Cons` 变种组成的一个无线 `List`* + + +**使用 `Box` 获得一个有着已知大小的递归类型** + +由于 Rust 无法计算出,要分配多少内存空间给递归定义的类型,因此编译器就给到有着以下有帮助的一项建议: + +```console +help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable + | +2 | Cons(i32, Box), + | ++++ + +``` + +在此建议中,“间接性的东西(indirection)” 表示与其直接存储某个值,这里应该将这个数据结构,修改为通过存储指向到值的指针,间接地存储那个值。 + +由于 `Box` 是个指针,Rust 就始终清楚一个 `Box` 需要多少内存空间:指针的大小,不会因其指向数据的空间数量而变化。这就意味着这里可把一个 `Box`,而不是直接把另一个 `List`,放在那个 `Cons` 变种里头。这个 `Box` 将指向将位于内存堆上,而非在那个 `Cons` 内部的下一 `List`。从概念上讲,这里仍会有以包含其他列表的一些列表方式,创建出的一个列表,但现在这种实现看起来更像是把那些列表挨个放置,而不是一个列表在另一列表内部。 + +这里可将清单 15-2 中那个 `List` 枚举的定义,与清单 15-3 中该 `List` 的用法,修改为下面清单 15-5 中的代码,这就会编译了: + +文件名:`src/main.rs` + +```rust +#[derive(Debug)] +enum List { + Cons(i32, Box), + Nil, +} + +use crate::List::{Cons, Nil}; + +fn main() { + let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil)))))); + + println! ("list: {:?}", list); +} +``` + +*清单 15-5:为有着已知大小而使用了 `Box` 的 `List` 定义* + +其中的 `Cons` 变种,需要一个 `i32` 的大小加上存储那个匣子指针数据的内存空间。那个 `Nil` 变种未存储值,因此他需要的空间,要少于 `Cons` 变种。现在咱们就指定任何 `List` 值,都会占用一个 `i32` 的大小加上一个匣子指针数据的大小了。通过使用匣子,咱们就破解了那个无限的、递归的链条,因此编译器就可以计算出,他存储一个 `List` 值所需的内存大小。下图 15-2 显示了那个 `Cons` 现在看起来的样子: + +![由于 `Cons` 保存了一个 `Box` 而不在是无限大小的 `List`](images/15-02.svg) + +*图 15-02:由于 `Cons` 保存了一个 `Box` 而不在是无限大小的 `List`* + +匣子数据结构,仅提供了这种间接性的东西与内存堆方面的空间分配;他们不具备像是将在其他灵巧指针类型下,所发现的那些其他特别能力。匣子类型也不会有这些特别能力所招致的性能开销,因此他们在像是构造列表这种,仅需间接性特性的情形下,会是有用的。在第 17 章,还将会检视匣子类型的更多用例。 + +`Box` 类型之所以是灵巧指针,是由于他实现了 `Deref` 特质,这就实现了像引用那样,对待 `Box` 类型的值。在某个 `Box` 值超出作用域时,又由于 `Box` 的那个 `Drop` 实现,那么该匣子所指向的内存堆数据就会被清理。相比本章其余部分将讨论到的由其他灵巧指针所提供到的功能,这两个特质甚至将会更为重要。下面就来更深入地探讨一下这两个特质。 + + +## 在 `Deref` 特质下,像常规引用那样看待灵巧指针 + +**Treating Smart Pointers Like Regular References with `Deref` Trait** + +那个 `Deref` 特质的实现,就允许咱们,对 *解引用运算符, the dereference operator, *️'*' 的行为加以定制(请不要与乘法或全局运算符混淆)。通过以灵巧指针可被像常规引用那样处理这样的方式,实现 `Deref`,咱们就可以编写出运作于引用上的代码,而同时在灵巧指针上使用那些代码。 + +接下来首先会看看,这个解引用运算符,是怎样作用于常规引用的。随后咱们就会尝试定义出像 `Box` 那样行事的已知定制类型,而搞清楚为何解引用运算符,不会像在引用上那样,在这个新定义出的类型上起效。这里将探讨怎样实现,这个令到灵巧指针,以类似与引用类似方式运作的 `Deref` 特质。接着这里将看看 Rust 的 *解引用强制* 特性(*deref coercion* feature),以及他是怎样实现即工作于引用,又工作于灵巧指针之下的。 + +> **注意**:在那个即将建立的 `MyBox` 类型与真实的 `Box`(Rust 标准库提供的)之间,有着一个大差别:这里的版本并未将其数据存储在内存堆上。由于本小节是着重于 `Deref`,因此相比指针方面行为,数据实际存储于何处则没那么重要。 + + +### 跟随指针到其值 + +**Following the Pointer to the Value** + +常规引用即是一种指针,同时设想指针的一种方式,就好比指向存储于别处某个值的一个箭头。在下面清单 15-6 种,就创建了到某个 `i32` 值的引用,并在随后使用解引用运算符,来跟随了到该值的这个引用: + +文件名:`src/main.rs` + +```rust +fn main() { + let x = 5; + let y = &x; + + assert_eq! (5, x); + assert_eq! (5, *y); +} +``` + +*清单 15-6:使用解引用运算符来跟随到一个 `i32` 值的引用* + +变量 `x` 保存着一个 `i32` 值 `5`。这里将 `y` 设置为等于到 `x` 的一个引用。这里当然是可以断言,`x` 是等于 `5` 的。但是在咱们打算做出 `y` 中的断言时,这里就可以使用 `*y`,来跟随该引用到其所指向那个值(因此就叫 *解引用,dereference*),进而编译器就可以比较具体值了。一旦解引用了 `y`,咱们就有了到 `y` 所指向的那个整数的存取,那么就可以与 `5` 相比较了。 + +相反,如果这里尝试编写 `assert_eq! (5, y);`,就会得到这样的编译报错: + +```console +$ cargo run  ✔   + Compiling sp_demos v0.1.0 (/home/peng/rust-lang/sp_demos) +error[E0277]: can't compare `{integer}` with `&{integer}` + --> src/main.rs:6:5 + | +6 | assert_eq! (5, y); + | ^^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}` + | + = help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}` + = help: the following other types implement trait `PartialEq`: + f32 + f64 + i128 + i16 + i32 + i64 + i8 + isize + and 6 others + = note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info) + +For more information about this error, try `rustc --explain E0277`. +error: could not compile `sp_demos` due to previous error +``` + +由于数字与到数字的引用属于不同类型,因此拿他们做比较是不被允许的。咱们必须使用解引用运算符,来跟随引用到其所指向的值。 + + +### 像引用一样使用 `Box` + +这里可将清单 15-6 中的代码,重写为使用 `Box` 而非引用;清单 15-7 中在 `Box` 上用到的解引用运算符,会与清单 15-6 中用在引用上的解引用运算符,以同样的方式其作用: + +文件名:`src/main.rs` + +```rust +fn main() { + let x = 5; + let y = Box::new(x); + + assert_eq! (5, x); + assert_eq! (5, *y); +} +``` + +*清单 15-7:在一个 `Box` 上运用解引用运算符* + +清单 15-7 与清单 15-6 之间的主要区别,就是这里将 `y` 设置为了指向 `x` 的一个拷贝值的匣子实例,而不是清单 15-6 中那样,指向 `x` 值的引用。在最后那个断言里,咱们就可以在 `y` 为引用时,曾做过的同样方式,使用解引用运算符来跟随这个匣子的指针。接下来,将通过定义咱们自己的匣子类型,来探讨到底 `Box` 有何特别之处,来实现在其上使用解引用运算符的。 + + +### 定义咱们自己的灵巧指针 + +接下来就要构建出一个,类似于由标准库所提供的 `Box` 类型的灵巧指针,而感受一下灵巧指针默认情况下,是怎样不同于引用的。随后就将看看,如何添加这种使用解引用运算符的能力。 + +`Box` 最终被定义为有着一个元素的元组结构体(a tuple struct),因此清单 15-8 就以同样方式,定义了一个 `MyBox` 类型。这里还将定义一个 `new` 函数,来与定义在 `Box` 上的那个 `new` 函数相匹配。 + +文件名:`src/main.rs` + +```rust +struct MyBox(T); + +impl MyBox { + fn new(x: T) -> MyBox { + MyBox(x) + } +} +``` + +*清单 15-8:定义出一个 `MyBox` 类型* + +这里定义了一个名为 `MyBox` 的结构体,并由于这里想要这个类型保存任意类型的值,而声明了一个泛型参数 `T`。该 `MyBox` 类型是个有着一个类型 `T` 元素的元组结构体。其中的 `MyBox::new` 函数会取一个类型 `T` 的参数,并返回保持着所传入值的一个 `MyBox` 实例。 + +接下来尝试把清单 15-7 中的 `main` 函数,添加到清单 15-8 并将其修改为,使用这个上面定义的 `MyBox` 类型而非 `Box`。由于 Rust 不清楚怎样解引用 `MyBox`,因此下面清单 15-9 中的代码不会编译。 + +文件名:`src/main.rs` + +```rust +fn main() { + let x = 5; + let y = MyBox::new(x); + + assert_eq! (5, x); + assert_eq! (5, *y); +} +``` + +*清单 15-9:以使用引用及 `Box` 同样方式,尝试使用 `MyBox`* + +下面就是那产生出的编译报错: + +```console +$ cargo run  ✔   + Compiling sp_demos v0.1.0 (/home/peng/rust-lang/sp_demos) +error[E0614]: type `MyBox<{integer}>` cannot be dereferenced + --> src/main.rs:14:20 + | +14 | assert_eq! (5, *y); + | ^^ + +For more information about this error, try `rustc --explain E0614`. +error: could not compile `sp_demos` due to previous error +``` + +由于咱们未曾在这个 `MyBox` 类型上实现过其被解引用的能力,因此他无法被解引用。为实现使用 `*` 运算符的解引用,就要实现 `Deref` 特质。 + + +### 通过实现 `Deref` 特质而像引用那样,对待某个类型 + +**Treating a Type Like a Reference by Implementing the `Deref` Trait** + +正如第 10 章的 ["在类型上实现某个特质"](Ch10_Generic_Types_Traits_and_Lifetimes.md#implementing-a-trait-on-a-type) 小节中所讨论过的,这里需要提供到特质所要求的那些方法的实现。而这个由标准库提供的 `Deref` 特质,要求咱们实现一个会借用到 `self`,并会返回到其内部数据的引用的名为 `deref` 的方法。下面清单 15-10 包含了添加到 `MyBox` 定义的一个 `Deref` 实现: + +文件名:`src/main.rs` + +```rust +use std::ops::Deref; + +struct MyBox(T); + +impl MyBox { + fn new(x: T) -> MyBox { + MyBox(x) + } +} + +impl Deref for MyBox { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} +``` + +*清单 15-10:在 `MyBox` 上实现 `Deref`* + +其中 `type Target = T;` 这种语法,定义出了 `Deref` 特质要用到的一个关联类型(an assiotiated type for the `Deref` trait to use)。关联类型属于与声明泛型参数有些许不同的声明方式,现在无需担心他们;在第 19 张中将更细致地讲到他们。 + +这里填入 `deref` 方法函数体的是 `&self.0`,从而 `deref` 就返回了到咱们打算用 `*` 运算符访问的那个值的一个引用;回顾第 5 章的 [运用不带命名字段的元组结构体来创建出不同类型](Ch05_Using_Structs_to_Structure_Related_Data.md#using-tuple-structs-without-named-fields-to-create-different-types) 小节,那个 `.0` 就是访问了结构体中的首个值。清单 15-9 中在其中 `MyBox` 值上调用了 `*` 的 `main` 函数,现在就会编译了,同时那些断言将通过! + +没有这个 `Deref` 特质,编译器就只能解引用那些 `&` 的引用。那个 `deref` 方法,给到了编译器取得实现了 `Deref` 特质的任意类型值的能力,而调用该特质的 `deref` 方法,就获得了其知道如何解引用的一个 `&` 引用。 + +在于清单 15-9 中敲入 `*y` 时,在幕后 Rust 实际上运行了下面的代码: + +```rust +*(y.deref()) +``` + +Rust 使用到 `deref` 方法的一个调用,以及接着一个普通的解引用,替换了那个 `*` 运算符,如此咱们就不必考虑,这里到底需不需要调用那个 `deref` 方法了。Rust 的这项特性,实现了不论对于常规引用,或是对实现了 `Deref` 特质的类型,以同样方式起作用代码的编写。 + +那个`deref` 返回到某个值的引用,以及 `*(y.deref())` 中括号外的那个普通解引用,二者都有其存在的必要原因,那就是配合了 Rust 的所有权系统。假如这个 `deref` 方法直接返回值,而不是到值的引用,那么该值就会被迁移出 `self`。咱们并未打算取得这个示例,或用到解引用运算符的其他绝大多数用例中,`MyBox` 里头那个内层值的所有权。 + +请注意那个 `*` 运算符被替换为了到 `deref` 方法的一次调用,和随后到 `*` 运算符的一次调用,这替换只有一次,且咱们在代码中用到一次 `*` 运算符,这种替换就会进行一次。由于 `*` 运算符的这种替代不会无限递归,因此这里就会以类型 `i32` 的数据而结束,其正好与清单 15-9 中那个 `assert_eq!` 里的 `5` 匹配。 + + +### 函数与方法下的隐式解引用强制转换 + +**Implicit `Deref` Coercions with Functions and Methods** + +*解引用强制转换,deref coercion* 会将到实现了 `Deref` 特质的某种类型的引用,转换为到另一类型的引用。比如,由于 `String` 实现了 `Deref` 特质,因此对 `&String` 解引用强制转换,就会返回 `&str`,因此就可以把 `&String` 解引用强制转换为 `&str`。解引用强制转换,属于 Rust 在函数与方法的参数上,所执行的一项便利措施,并只在那些实现了 `Deref` 特质的类型上起作用。在将到特定类型值的引用,作为参数传递给某个函数或方法,而所传递的引用,并不与那个函数或方法定义中的参数类型想匹配时,这种解引用强制转换就会发生。这时到 `deref` 方法的一系列调用,就会把所提供的类型,转换为函数或方法定义中那些参数所需的类型。 + +> *注*:在面向对象编程语言 Java 中,类似的特性叫 ["自动装箱"](https://java.xfoss.com/bian-yi-qi-dui-yu-zhan/ch10_numbers_and_statics_numbers_matter#autoboxing)。 + +为了程序员们在编写函数与方法调用时,无需使用 `&` 及 `*` 添加许多的那些显示引用和解引用,解引用强制转换特性就这样被添加到 Rust 了。这种解引用强制转换,还实现更多既可在引用,亦可在灵巧指针上起作用代码的编写。 + +下面就来使用定义在清单 15-8 中的这个 `MyBox` 类型,以及在清单 15-10 中添加的那个 `Deref` 实现,来看看运作中的解引用强制转换。下面清单 15-11 给出了有着一个字符串切片参数的某个函数定义: + +文件名:`src/main.rs` + +```rust +fn hello(name: &str) { + println! ("你好,{name}"); +} +``` + +*清单 15-11:有着类型 `&str` 参数 `name` 的 `hello` 函数* + +这里可使用一个字符串切片作为参数,调用这个 `hello` 函数,譬如 `hello("Rust");`。而解引用强制转换特性,就令到使用到类型 `MyBox` 值的引用,来调用 `hello` 成为可能,如下清单 15-12 中所示: + +文件名:`src/main.rs` + +```rust +fn main() { + hello("Rust"); + + let m = MyBox::new(String::from("Rust")); + hello(&m); +} +``` + +*清单 15-12:使用到某个 `MyBox` 值的引用调用 `hello`,因为有解引用强制转换,这样做是可行的* + +这里使用参数 `&m`,即到某个 `MyBox` 值的引用,调用的那个 `hello` 函数。由于这里曾在清单 15-10 中的 `MyBox` 上实现过 `Deref` 特质,因此 Rust 就能通过调用 `deref`,将 `&MyBox` 转换为 `&String`。标准库提供了在 `&String` 上,返回一个字符串切片的 `Deref` 实现,且这一点就在 `Deref` 的 API 文档中。Rust 就会再度调用 `deref`,来将这个 `&String` 转换为 `&str`,这就与 `hello` 函数定义想吻合了。 + +若 Rust 不曾实现解引用强制转换特性,那么这里就不得不编写出下面清单 15-13 中的代码,而不是清单 15-12 中的代码,来以某个类型 `&MyBox` 值调用 `hello` 了: + +文件名:`src/main.rs` + +```rust +fn main() { + let m = MyBox::new(String::from("Rust")); + hello(&(*m)[..]); +} +``` + +*清单 15-13:若 Rust 没有解引用强制转换特性,而将不得不编写的代码* + +其中的 `(*m)` 将那个 `MyBox` 解引用为了一个 `String`。随后的 `&` 与 `[..]`,则取了与 `hello` 函数签名相匹配,等于整个字符串的该 `String` 的字符串切片。这种不带有解引用强制转换的代码,因为涉及到全部的这些符号,而更难于阅读、编写与理解。正是解引用强制转换,实现了 Rust 为咱们自动处理这些转换。 + +在为这些涉及到的类型定义了 `Deref` 特质时,Rust 将分析这些类型,并运用必要次数的 `Deref::deref`,来获取到与函数或方法定义中参数类型相匹配的一个引用。所需插入的 `Deref::deref` 次数,是在编译时解算出来的,因此解引用强制转换优势的利用,并无运行时的代价。 + + +### 解引用强制转换与可变性的互动方式 + +与使用 `Deref` 特质来覆写不可变引用上的 `*` 运算符的方式类似,咱们可以使用 `DerefMut` 特质,来覆写可变引用上的 `*` 运算符。 + +在以下三种情形下,Rust 会在他发现类型与特质的实现时,执行强制引用转换: + +- 从 `&T` 强制转换为 `&U` 时,`T: Deref` +- 从 `&mut T` 转换为 `&mut U` 时,`T: DerefMut` +- 从 `&mut T` 转换为 `&U` 时,`T: Deref` + +其中前两个情形,除了第二种实现了可变外,他们是同样的。第一种情形指出了在咱们有着一个 `&T`,且 `T` 对某种类型 `U` 实现了 `Deref` 特质,那么就显然能得到一个 `&U`。第二种情形则指出了对可变引用,同样会发生解引用强制转换。 + +那第三中情形就较为复杂了:Rust 还将把某个可变引用,强制转换为一个不可变引用。但反过来则是 *不* 可行的:不可变引用绝不会强制转换为可变引用。由于借用规则的存在,在有着某个可变引用时,那个可变引用必定只会是到那个数据的引用(否则,程序就不会编译)。将一个可变引用转换为一个不可变引用,是绝不会破坏借用规则的。而将不可变引用转换为可变引用,就会要求那个初始不可变引用,为到那个数据的唯一不可变引用,但借用规则却不会确保那一点。因此,Rust 就无法做出将不可变引用,转换为可变引用可行这一假定。 + + +## 使用 `Drop` 特质在清理内存时运行代码 + +**Running Code on Cleanup with `Drop` Trait** + +对于灵巧指针模式来讲,重要的第二个特质便是 `Drop` 了,他允许咱们在某个值即将超出作用域时,对要发生什么加以定制。在任何类型上,咱们都可以提供 `Drop` 特质的一个实现,而那些代码就可被用于释放诸如文件或网络连接等资源。 + +这里之所以在灵巧指针上下文中引入 `Drop` 特质,是由于 `Drop` 特质的功能,几乎总是用在实现某个灵巧指针的时候。比如,在某个 `Box` 被弃用时,`Drop` 特质就会解除该匣子所指向的堆上的内存空间分配。 + +在一些语言中,对于某些类型,编程者就必须在他们每次结束使用这些类型的某个实例时,调用代码来释放内存或其他资源。这类示例包括了文件把手、套接字或一些锁等等(file handles, sockets, or locks)。若他们忘记了这点,那么系统就会变得过载并崩溃。而在 Rust 中,咱们就可以指定出,在每当有某个值超出作用域时,所运行的一些特定代码,而编译器就会自动插入这些代码。结果就是,咱们就不需要小心翼翼地,在程序里某种特定类型的某个示例结束使用的各处,放置那些清理代码了 -- 咱们仍不会泄露各种资源! + +咱们是通过实现 `Drop` 特质,指定出在某个值超出作用域时所运行的那些代码的。`Drop` 特质要求咱们,要实现一个取到 `self` 的可变引用、名为 `drop` 的方法。现在就来实现一个有着数条 `println!` 语句的 `drop` 方法,以发现 Rust 于何时调用这个 `drop` 方法。 + +下面清单 15-14 给出了仅有着一项定制功能,即在其实例超出作用域时打印出 `正在弃用 CustomSmartPointer!` 的一个 `CumstomSmartPointer` 结构体,以展示出 Rust 在何时运行这个 `drop` 函数。 + +文件名:`src/main.rs` + +```rust +struct CustomSmartPointer { + data: String, +} + +impl Drop for CustomSmartPointer { + fn drop(&mut self) { + println! ("正在使用数据 `{}` 弃用 CustomSmartPointer!", self.data); + } +} + +fn main() { + let c = CustomSmartPointer { + data: String::from("我的事情"), + }; + let d = CustomSmartPointer { + data: String::from("其他事情"), + }; + println! ("已创建出一些 CustomSmartPointer 实例"); +} +``` + +*清单 15-14:实现了于其中放置咱们编写的清理代码的 `Drop` 特质的 `CustomSmartPointer` 结构体* + +`Drop` 特质是包含在 Rust 序曲(the prelude)中的,因此这里就无需将其带入作用域。这里在 `CustomSmartPointer` 上实现了 `Drop` 特质,并提供了一个调用了 `println!` 宏的 `drop` 方法的实现。这个 `drop` 函数的函数体,即是咱们将要放置那些,在咱们类型的某个实例超出作用域时,打算运行的全部逻辑的地方。这里咱们就打印出一些文本,来直观地演示 Rust 何时会调用 `drop` 方法。 + +在 `main` 函数中,这里创建出了 `CustomSmartPointer` 的两个实例,并于随后打印了 `已创建出一些 CumstomSmartPointer 实例`。在 `main` 末尾处,这些 `CumstomSmartPointer` 的实例,就将超出作用域,同时 Rust 就会调用咱们放入到`drop` 方法中的那些代码,打印出咱们的最终消息。请注意咱们并不需要显式地调用这个 `drop` 方法。 + +在运行这个程序的时候,就会看到下面的输出: + +```console +$ cargo run lennyp@vm-manjaro + Compiling sp_demos v0.1.0 (/home/lennyp/rust-lang/sp_demos) + Finished dev [unoptimized + debuginfo] target(s) in 0.50s + Running `target/debug/sp_demos` +已创建出一些 CustomSmartPointer 实例 +正在使用数据 `其他事情` 弃用 CustomSmartPointer! +正在使用数据 `我的事情` 弃用 CustomSmartPointer! +``` + +在这些实例超出作用域时,Rust 就自动为咱们调用了 `drop`,进而调用了咱们指定出的那些代码。变量以与他们创建相反的顺序被弃用,因此其中的 `d` 先于 `c` 被启用。这个示例的目的,是要给到 `drop` 方法工作方式的直观说明;通常咱们会指定出咱们的类型所要运行的代码,而非一条打印出的消息。 + + +### 使用 `std::mem::drop` 提前弃用某个值 + +**Drop a Value Early with `std::mem::drop`** + +不幸的是,要关闭这种自动的 `drop` 功能,却并不那么简单。关闭 `drop` 并不常见;`Drop` 特质的全部意义,就在于他是自动的。然而在少数情况下,咱们就会想要提前清理掉某个值。一个这样的例子,便是在运用一些管理锁的灵巧指针时:咱们就可能希望强制运行那个释放锁的 `drop` 方法,从而同一作用域中的其他代码,就可以请求到该锁。Rust 是不允许咱们,手动调用 `Drop` 特质的 `drop` 方法的;相反,在想要在作用域结束之前,强制弃用某个值时,咱们可以调用由标准库提供的 `std::mem::drop` 函数。 + +若咱们通过修改清单 15-14 中那个 `main` 函数,而尝试手动调用 `Drop` 特质的 `drop` 方法,如下清单 15-15 中所示,就会得到一个编译器报错: + +文件名:`src/main.rs` + +```rust +fn main() { + let c = CustomSmartPointer { + data: String::from("一些数据"), + }; + println! ("已创建出一个 CustomSmartPointer 实例。"); + c.drop(); + println! ("在 main 结束之前这个 CustomSmartPointer 已被弃用。") +} +``` + +*清单 15-15:尝试调用 `Drop` 特质中的 `drop` 方法来提前清理* + + +在尝试编译此代码时,就会得到下面这样的错误: + +```console +$ cargo run lennyp@vm-manjaro + Compiling sp_demos v0.1.0 (/home/lennyp/rust-lang/sp_demos) +error[E0040]: explicit use of destructor method + --> src/main.rs:17:7 + | +17 | c.drop(); + | --^^^^-- + | | | + | | explicit destructor calls not allowed + | help: consider using `drop` function: `drop(c)` + +For more information about this error, try `rustc --explain E0040`. +error: could not compile `sp_demos` due to previous error +``` + +此错误消息指出,这里是不允许显式调用 `drop` 方法的。该错误消息用到了术语 *解构器,descructor*,那正是清理掉某个实例的函数的通用编程术语(the general programming term)。*解构器,destructor* 类似于创建出实例的 *构造器,constructor*。Rust 中的 `drop` 函数,就是一个特别的解构器。 + +Rust 之所以不让咱们显式地调用 `drop`,是因为 Rust 仍将在 `main` 函数末尾,自动调用那个值上的 `drop`。由于 Rust 会两次尝试清理同一值,因此这就会导致 *双重释放,double free* 的错误。 + +咱们无法关闭这种在某个值超出作用域时的 `drop` 自动插入,同时又无法显式地调用 `drop` 方法。因此,在咱们需要强制某个值提前被清理掉时,就要使用 `std::mem::drop` 函数。 + +这个 `std::mem::drop` 函数不同于 `Drop` 特质中的那个 `drop` 方法。咱们是通过将要强制弃用的那个值作为参数传递,而调用他的。该函数位于 Rust 序曲中(in the prelude),因此这里就可以把清单 15-15 中的 `main` 函数,修改为如下清单 15-16 中所示的调用那个 `drop` 函数: + +文件名:`src/main.rs` + +```rust +fn main() { + let c = CustomSmartPointer { + data: String::from("我的事情"), + }; + println! ("已创建出一个 CustomSmartPointer 实例。"); + drop(c); + println! ("在 main 结束之前这个 CustomSmartPointer 已被弃用。") +} +``` + +*清单 15-16:调用 `std::mem::drop` 在某个值超出作用域之前,显式地弃用该值* + +运行此代码就会打印出下面的输出: + +```console +$ cargo run lennyp@vm-manjaro + Compiling sp_demos v0.1.0 (/home/lennyp/rust-lang/sp_demos) + Finished dev [unoptimized + debuginfo] target(s) in 0.40s + Running `target/debug/sp_demos` +已创建出一个 CustomSmartPointer 实例。 +正在使用数据 `一些数据` 弃用 CustomSmartPointer! +在 main 结束之前这个 CustomSmartPointer 已被弃用。 +``` + +于 `已创建出一个 CustomSmartPointer 实例。`,与 `在 main 结束之前这个 CustomSmartPointer 已被弃用。` 文本之间打印出的文本,`正在使用数据 `一些数据` 弃用 CustomSmartPointer!` ,显示在那个时间点,`Drop` 特质的 `drop` 方法被调用来弃用 `c`。 + +咱们可以许多种方式,使用 `Drop` 特质实现中所指明的代码,来令到资源清理变成方便且安全:比如就可以使用这种技巧,来创建出咱们自己的内存分配器!有了这个 `Drop` 特质及 Rust 的所有权系统,由于 Rust 会自动完成资源的清理,因此咱们就不必一定要记得清理资源了。 + +咱们还不必担心,意外清理仍在使用中的一些值而导致的问题:确保引用始终有效的所有权系统,会确保 `drop` 只会在该值不再会被使用时才被调用。 + +既然咱们已经检视了 `Box` 及灵巧指针的一些特征,接下来就要看看定义在标准库中的个别其他灵巧指针了。 + + +## `Rc`,引用计数灵巧指针 + +大多数情况下,所有权都是明确的:咱们确切知道,是哪个变量拥有者某个给定值。然而,单个值可能有着多个所有者的情形,也是有的。比如,在图数据结构(graph data structures),多条边就可能指向同一节点,从概念上讲,而那个节点就是被所有指向他的边所拥有的。在已不再有任何边指向节点,进而该节点已无所有者之前,这个节点就不应被清理掉。 + +必须通过使用 Rust 的类型 `Rc`,来显式地启用多重所有权,`Rc` 即 *引用计数,reference counting* 的缩写。`Rc` 类型会追踪某个值的引用数,从而判断出该值是否仍在使用中。在到某个值的引用数为零时,该值就可以在不会有任何引用变成无效的情况下,(安全地)被清理掉。 + +请将 `Rc` 设想为客厅里的一台电视机。在有人进来看电视时,他们就会打开他。其他人是可以进来客厅而看电视的。在最后一人离开客厅时,由于电视已不再被使用,他们便关掉了电视机。而在其他人仍在看电视时,有人关了电视机,那么剩下的那些电视观众,就会哇哇叫的! + +当咱们打算在内存堆上给咱们程序多个部分,分配用来读取的一些数据,且无法确定出,在编译时那些部分将最后用到这些数据时,咱们就会用到这个 `Rc` 类型。若咱们清楚那个部分将最后结束,那么就可以只把那个部分,构造为该数据的所有者,同时在编译时强制用到的一般所有权规则,就能发挥作用。 + +请注意 `Rc` 只适用于单线程的场景(only for use in single-threaded scenarios)。在第 16 章中讨论到并发时,就会讲到多线程程序中,怎样完成引用计数。 + + +### 使用 `Rc` 来共用数据 + +**Using `Rc` to Share Data** + + +下面来回到清单 15-5 中那个构造列表的示例。回顾到咱们曾使用 `Box` 定义出的那个构造列表。这次,咱们将创建出同时共用了第三个列表的两个列表。概念上讲,这看起来与下图 15-3 类似: + +![`b` 与 `c` 两个列表,共用了第三列表 `a` 的所有权](images/15-03.svg) + +*图 15-03:`b` 与 `c` 两个列表,共用了第三列表 `a` 的所有权* + +这里将构造出包含 `5` 与其后 `10` 的列表 `a`。随后这里将构造两个另外的列表:以 `3` 开始的 `b` 和以 `4` 开始的列表 `c`。列表 `b` 与 `c` 都将接着延续到头一个包含着 `5` 及 `10` 的列表 `a`。换句话说,这两个列表将共用那包含了 `5` 与 `10` 的头一个列表。 + +使用之前有着 `Box` 的 `List` 尝试实现这种场景,就不会工作,如下清单 15-17 中所示: + +文件名:`src/main.rs` + +```rust +enum List { + Cons(i32, Box), + Nil, +} + +use crate::List::{Cons, Nil}; + +fn main() { + let a = Cons(5, Box::new(Cons(10, Box::new(Nil)))); + let b = Cons(3, Box::new(a)); + let c = Cons(4, Box::new(a)); +} +``` + +*清单 15-17:对不允许有着两个用到 `Box` 的列表尝试共用第三列表进行演示* + +在编译上面的代码时,就会得到下面的报错: + +```console +$ cargo run  ✔   + Compiling sp_demos v0.1.0 (/home/peng/rust-lang/sp_demos) +error[E0382]: use of moved value: `a` + --> src/main.rs:11:30 + | +9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil)))); + | - move occurs because `a` has type `List`, which does not implement the `Copy` trait +10 | let b = Cons(3, Box::new(a)); + | - value moved here +11 | let c = Cons(4, Box::new(a)); + | ^ value used here after move + +For more information about this error, try `rustc --explain E0382`. +error: could not compile `sp_demos` due to previous error; +``` + +那些 `Cons` 变种,拥有他们所保存的数据,因此在创建出那个 `b` 列表时,`a` 就被迁移进了 `b`,进而 `b` 就拥有了 `a`。随后,在创建出 `c` 而尝试再次使用 `a` 时,因为 `a` 已被迁移,因此这里就不再被允许了。 + +这里原本是可以将 `Cons` 的定义,修改为保存引用的,但随后就必须要指定生命周期参数。经由制定生命周期参数,这里就指出了列表中的每个元素,都将与整个列表有同样的存活时间。这正是清单 15-17 中元素与列表的情形,但并非是所有场景中的情形。 + +相反,这里将把 `List` 的定义,修改为在 `Box` 处运用 `Rc`,如下清单 15-18 中所示。这样各个 `Cons` 变种,现在就将保存一个值与一个指向某个 `List` 的 `Rc` 了。在创建出 `b` 时,就不再是取得 `a` 的所有权,而是将克隆出 `a` 正保存的那个 `Rc`,因此将引用的数据,从一个增加到了两个,实现了 `a` 与 `b` 共用那个 `Rc` 中的数据。在创建出 `c` 时,这里也将克隆 `a`,从而将引用的数据,从两个增加到三个。每次调用 `Rc::clone` 时,到那个 `Rc` 里头数据的引用计数,都将增加,同时除非到其引用为零,该数据便不会被清理掉。 + +文件名:`src/main.rs` + +```rust +#[derive(Debug)] +enum List { + Cons(i32, Rc), + Nil, +} + +use crate::List::{Cons, Nil}; +use std::rc::Rc; + +fn main() { + let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); + let b = Cons(3, Rc::clone(&a)); + let c = Cons(4, Rc::clone(&a)); + + println! ("b 为: {:?}\nc 为: {:?}", b, c); +} +``` + +*清单 15-18:使用了 `Rc` 的 `List` 定义* + +由于 `Rc` 不在 Rust 序曲中(in the prelude),因此就需要添加一条 `use` 语句将其带入到作用域中。在 `main` 函数里,这里创建了那个包含 `5` 与 `10` 的列表,并将其存储在 `a` 中的一个新 `Rc` 里。随后在创建 `b` 与 `c` 时,这里调用了 `Rc::clone` 函数,并将到那个 `Rc` 的引用作为参数加以传入。 + +这里本可以调用 `a.clone()` 而不是 `Rc::clone(&a)`,但在这种情况下,Rust 的约定就是使用 `Rc::clone`。`Rc::clone` 方法的实现,与绝大多数类型的 `clone` 实现方式不同,其并不会构造全部数据的深拷贝。到 `Rc::clone` 的调用,只会增加引用计数,这样做不耗费很多时间。而数据的一些深拷贝,则能耗费很多时间。通过使用 `Rc::clone` 来进行引用计数,咱们就可以直观地区别出深拷贝类别的那些克隆,与那些增加引用计数的克隆类别。在查找代码中的性能问题时,咱们只需要关注那些深拷贝的克隆,而可以不用管那些到 `Rc::clone` 的调用。 + +> **注**:第 4 章 [变量与数据交互方式之二:克隆](Ch04_Understanding_Ownership.md#ways-variables-and-data-interact-clone) 中,曾提到:“当看到一个对 clone 方法的调用时,那么就明白正有一些任性代码在被执行,且那代码可能开销高昂。对此方法的调用,是某些不同寻常事情正在发生的明显标志。”。 + + +### 对某个 `Rc` 进行克隆,就会增加引用计数 + +**Cloning an `Rc` Increases the Reference Count** + +下面就拉修改清单 15-18 中的那个运作中的示例,从而可以发现在创建及弃用到 `a` 中那个 `Rc` 的引用时,引用计数就会改变。 + +在下面清单 15-19 中,这里将修改 `main` 为其有着一个围绕列表 `c` 的内层作用域;随后就会看到在 `c` 超出作用域时,引用计数会怎样变化。 + + +文件名:`src/main.rs` + +```rust +fn main() { + let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); + println! ("在创建出 a 后,引用计数为 {}", Rc::strong_count(&a)); + let b = Cons(3, Rc::clone(&a)); + println! ("在创建出 b 后,引用计数为 {}", Rc::strong_count(&a)); + { + let c = Cons(4, Rc::clone(&a)); + println! ("在创建出 c 后,引用计数为 {}", Rc::strong_count(&a)); + } + println! ("在 c 超出作用域后,引用计数为 {}", Rc::strong_count(&a)); +} +``` + +*清单 15-19:打印出引用计数* + +在程序中引用计数变化的各个点位,咱们都打印出了引用计数,其正是咱们经由调用 `Rc::strong_count` 函数得到的。该函数之所以名为 `strong_count`,而非 `count`,是由于这个 `Rc` 类型,还有一个 `weak_count` 函数;在 [阻止引用的循环:将 `Rc` 转换为 `Weak`](#preventing-reference-cycles-turning-an-rc-t-into-a-weak-t) 小节,就会看到 `weak_count` 的使用。 + +此代码会打印出下面的东西: + + +```console +$ cargo run lennyp@vm-manjaro + Compiling sp_demos v0.1.0 (/home/lennyp/rust-lang/sp_demos) + Finished dev [unoptimized + debuginfo] target(s) in 0.40s + Running `target/debug/sp_demos` +在创建出 a 后,引用计数为 1 +在创建出 b 后,引用计数为 2 +在创建出 c 后,引用计数为 3 +在 c 超出作用域后,引用计数为 2 +``` + +可以看出,变量 `a` 中的 `Rc` 有着初始的引用计数 `1`;随着在每次调用 `clone` 时,该计数就会上升 `1`。在变量 `c` 超出作用域时,该计数降低了 `1`。与必须调用 `Rc::clone` 来提升该引用计数不同,咱们不必调用某个函数,来降低引用计数:在某个 `Rc` 值超出作用域时,`Drop` 特质实现会自动降低引用计数。 + +在这个示例中,咱们无法见到的是,在 `b` 及随后的 `a` 于 `main` 结束处超出作用域时,该计数就会是 `0`,同时这个 `Rc` 就被完全清除掉。使用 `Rc` 就实现了单个的值,有着多个所有者,同时这种计数确保了该值在任意这些所有者存在期间,保持有效。 + +通过不可变引用,`Rc` 实现了程序的各个部分之间,只读地共用数据。在 `Rc` 也实现了有着多个可变引用时,就会违反第 4 章中,曾讨论过的借用规则之一:到同一处所的多个可变借用,会导致数据竞争与不一致问题。然而能够修改数据,是非常有用的!那么在接下来的小节,就会讨论内部可变性模式(the interior mutability pattern),与那个可结合 `Rc` 值,用来解决这种不可变限制问题的 `RefCell` 类型(the `RefCell` type that you can use in conjunction with an `Rc` to work with this immutability restriction)。 + + +## `RefCell` 及内部可变性模式 + +**`RefCell` and the Interior Mutability Pattern** + +*内部可变性,interior mutability* 属于 Rust 中的一种设计模式,他实现了即使在有着到数据的一些不可变引用之下,对数据加以改变;一般情况下,这样的行为是借用规则所不允许的。为了改变数据,这种模式便运用了数据结构内部的一些 `unsafe` 代码,来改变了 Rust 监管可变性与借用的一些一般规则。这些不安全代码向编译器表明,咱们自己在手动检查那些规则,而非依赖于编译器为咱们检查那些规则;在第 19 章将进一步讨论这些不安全代码。 + +咱们可以只在能够确保借用规则在运行时将被遵循,而即使编译器无法保证这一点时,使用那些运用了内部可变性的类型。那么这个时候所涉及的那些 `unsafe` 代码,就会被封装在某个安全的 API 中,而外层的类型仍然是不可变的(we can use types that use the interior mutability pattern only when we can ensure that the borrowing rules will be followed at runtime, even though the compiler can't guarantee that. The `unsafe` code involved is then wrapped in a safe API, and the outer type is still immutable)。 + +接下来就要经由检视这个遵循内部可变性设计模式的 `RefCell` 类型,探讨此概念。 + + +### 使用 `RefCell` 在运行时强制借用规则检查 + +**Enforcing Borrowing Rules at Runtime with `RefCell`** + +不同于 `Rc`,这个 `RefCell` 类型,表示其所保存数据上的单个所有权。那么到底是什么令到 `RefCell` 不同于像 `Box` 这样的类型呢?回顾在第 4 章中所掌握的那些借用规则: + +- 在任何给定时间,咱们都可以有着 *要么* (而非同时) 一个的可变引用,要么任意数量的不可变引用; +- 引用必须始终是有效的。 + +在引用及 `Box` 之下,这些借用规则的那些不变性,在编译时被强制检查(with references and `Box`, the borrowing rules' invariants are enforced at compile time)。而在 `RefCell` 之下,这些不变性是在 *运行时,runtime*,被强制检查的。对于引用,在破坏这些规则时,就会得到编译时错误。而对于 `RecCell`,在破坏这些规则时,程序就会终止运行并退出。 + +在编译时检查借用规则的好处,就是那些错误会在开发过程中被及时捕获到,而因为全部代码分析都是提前完成的,因此在运行时性能上没有影响。由于这些原因,在编译时检查借用规则,即是大多数情形中的最佳实践,也正是 Rust 作为默认项的原因。 + +相反在运行时检查借用规则的优势,在于这个时候明确的内存安全场景是被允许的,这些场景中,他们原本是不被编译时借用规则检查所允许。一些静态分析,好比 Rust 的编译器,本质上是保守的。代码的一些属性,都是不可能通过分析代码侦测到的:其中最有名的示例,便是图灵停机问题,the Halting Problem,这个问题超出了本书的范围,但是个要研究的有趣话题。 + +由于某些分析不可能进行,因此在 Rust 编译器无法确定代码,在所有权规则下会编译时,他就会拒绝某个正确的程序;从这方面讲,他就是保守的了。假如 Rust 编译器接受不正确的程序,那么用户将无法信任 Rust 所做出的那些保证。然而,若 Rust 拒绝某个正确程序,那么编程者就将感到不便,却又不会发生什么灾难性的事情。在咱们确定咱们的代码遵循了借用规则,只不过编译器无法理解并确保那一点时,`RefCell` 类型便是有用的了。 + +与 `Rc` 类似,`RefCell` 仅用于一些单线程场景中,并在咱们尝试将其用于多线程语境中时,将给到一个编译时报错。在第 16 章将讲到怎样在多线程的程序中,获得 `RefCell` 的功能。 + +以下为因何原因而选择 `Box`、`Rc` 或 `RefCell` 的总结: + +- `Rc` 实现了同一数据的多个所有者;`Box` 与 `RefCell` 都有着单个所有者; +- `Box` 实现了编译时的可变或不可变借用检查;`Rc` 仅实现了编译时的不可变借用检查;`RefCell` 则实现了在运行时的可变及不可变借用检查; +- 由于 `RefCell` 实现了运行时的可变借用检查,因此即是某个 `RefCell` 是不可变的,咱们也可以其内部的值。 + +修改某个不可变值内部的值,即为 *内部可变性* 模式(the *interior mutability* pattern)。接下来就要看一个其中内部可变性有用的示例,并检视内部可变性是如何可行的。 + + +### 内部可变性:到不可变值的可变借用 + +**Interior Mutability: A Mutable Borrow to an Immutable Value** + +借用规则的一种后果,便是在有着某个不可变值时,是无法可变地借用他的。比如,下面的代码就不会编译: + +```rust +fn main() { + let x = 5; + let y = &mut x; +} +``` + +在尝试编译此代码时,就会得到以下报错: + +```console +$ cargo run lennyp@vm-manjaro + Compiling sp_demos v0.1.0 (/home/lennyp/rust-lang/sp_demos) +error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable + --> src/main.rs:3:13 + | +2 | let x = 5; + | - help: consider changing this to be mutable: `mut x` +3 | let y = &mut x; + | ^^^^^^ cannot borrow as mutable + +For more information about this error, try `rustc --explain E0596`. +error: could not compile `sp_demos` due to previous error; +``` + +然而,某个值在他的一些方法中对自身加以修改,而对别的代码表现出不可变,这种情况在一些场合是有用的。值那些方法外部的代码,将无法修改该值。使用 `RefCell`,便是获得拥有内部可变性能力的一种途径,但 `RefCell` 并不是完全绕开了借用规则:编译器中的借用检查,放行了这种内部可变性,而取而代之的是,借用规则在运行时得以检查。在违反这些规则时,就会得到一个 `panic!` 而非编译时错误。 + +接下来就要贯穿其中可使用 `RefCell`,来修改某个不可变值,并发现为何这是有用的一个实际例子。 + + +### 内部可变性的一个用例:模拟对象 + +**A Use Case for Interior Mutability: Mock Objects** + +有的时候,在测试期间,编程者为了观察到特定行为,并断言该行为有被正确实现,就会在某个类型处使用另一类型。这样的占位类型,叫做 *测试替身,test double*。请将其设想为电影工业中的 “特技替身,stunt double”,即某人介入进来并代替某名演员完成特别棘手的一个场景。在测试时,测试替身代表了其他类型。所谓模拟对象,就是记录测试过程中,发生了些什么,如此咱们就可以确定出那些正确操作有发生的一些特定类型。 + +Rust 没有如同其他有着对象的语言,同样意义上的那些对象,且 Rust 没有如同一些其他语言那样,内建到标准库中的模拟对象功能。然而,咱们是绝对可以创建出,将起到与模拟对象相同的作用,这样的结构体的。 + +以下就是这里将测试的场景:这里将创建根据最大值而追踪某个值,并根据最大值与当前值的接近程度,发出一些消息的库。这样的库,比如就可被用于追踪用户的允许调用 API 次数配额。 + +这个库将提供对某个值接近最大值程度的追踪,以及在什么时刻发出什么消息的功能。使用这个库的应用,预期将提供发送消息的机制:应用可将某条消息放置于该应用中,或者发出一封电子邮件,或者发出一条手机短信,抑或别的什么。这个库则无需清楚那样的细节。他所需的全部,即是实现一个这里将提供的、名为 `Messenger` 的一个特质。下面清单 15-20 给出了该库的代码: + + +文件名:`src/lib.rs` + +```rust +pub trait Messenger { + fn send(&self, msg: &str); +} + +pub struct LimitTracker<'a, T: Messenger> { + messenger: &'a T, + value: usize, + max: usize, +} + +impl<'a, T> LimitTracker<'a, T> +where + T: Messenger, +{ + pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> { + LimitTracker { + messenger, + value: 0, + max, + } + } + + pub fn set_value(&mut self, value: usize) { + self.value = value; + + let percentage_of_max = self.value as f64 / self.max as f64; + + if percentage_of_max >= 1.0 { + self.messenger.send("出错:你已超出你的配额!"); + } else if percentage_of_max >= 0.9 { + self.messenger + .send("紧急警告:你已用掉你配额的 90% !"); + } else if percentage_of_max >= 0.75 { + self.messenger + .send("警告:你已用掉你配额的 75% !"); + } + } +} +``` + +*清单 15-20:就某个值与最大值接近程度加以追踪,并在该值处于不同水平时发出告警的一个库* + +此代码的一个重要部分,即是有着一个取了到 `self` 的不可变引用与消息文本、名为 `send` 方法的那个 `Messenger` 特质。这个特质就是咱们的模拟对象所需实现的接口(the interface, 借鉴了 Java 语言的叫法,参见 [使用接口来拯救!](https://java.xfoss.com/ji-cheng-he-duo-tai-ji-zhi/ch08_interfaces_and_abstract_classes#interface_rescue)),从而这种模拟就可与真实对象的同样方式,而被使用。至于另一重要部分,则是这里打算测试 `LimitTracker` 上那个 `set_value` 方法的行为(注意:这里 `LimitTracker` 命名方式,同样借鉴了 Java 语言中类的命名约定)。这里可以改变所传入的那个 `value` 参数的值,但 `set_value` 不会返回任何咱们对其做出断言的东西。这里是要能够表达出,在咱们以实现了 `Messenger` 特质的某物,及 `max` 的某个特定值,而创建出一个 `LimitTracker` 下,当咱们传入不同数字的 `value` 时,那个信使方法,就被告知要发送一些恰当的消息。 + +这里所需的模拟对象,在调用 `send` 时,不是发送电子邮件或手机短信,而是将只追踪其被告知要发送的消息。这里可以创建出该模拟对象的一个新实例,然后创建一个用到这个模拟对象的 `LimitTracker`,接着调用 `LimitTracker` 上的那个 `set_value` 方法,并随后检查该模拟对象是否有着咱们期望的消息。下面清单 15-21 给出了实现一个模拟对象,来刚好完成这些步骤的一种尝试,但借用检查器不会放行这个尝试: + +文件名:`src/lib.rs` + +```rust +#[cfg(test)] +mod tests { + use super::*; + + struct MockMessenger { + sent_messages: Vec, + } + + impl MockMessenger { + fn new() -> MockMessenger { + MockMessenger { + sent_messages: vec! [], + } + } + } + + impl Messenger for MockMessenger { + fn send(&self, message: &str) { + self.sent_messages.push(String::from(message)); + } + } + + #[test] + fn it_sends_an_over_75_percent_waring_message() { + let mock_messenger = MockMessenger::new(); + let mut limit_tracker = LimitTracker::new(&mock_messenger, 100); + + limit_tracker.set_value(80); + + assert_eq! (mock_messenger.sent_messages.len(), 1); + } +} +``` + +*清单 15-21:实现不被借用检查器允许的一个 `MockMessenger` 的尝试* + + +此测试代码定义了有着一个 `sent_messages` 字段的 `MockMessenger` 结构体,该字段有着用于跟踪其被告知要发送消息的一些 `String` 值的 `Vec` 类型变量。这里还定义了一个关联函数 `new`,来令到创建出以空消息清单开头的那些新 `MockMessenger` 类型值,便利起来。随后这里就为 `MockMessenger` 实现了那个 `Messenger` 特质,于是就可以将某个 `MockMessenger` 给到一个 `LimitTracker` 了。在那个 `send` 方法的定义中,这里将所传入的消息,取作了参数,并将其存储在 `MockMessenger` 的 `sent_messages` 清单中。 + +在那个测试中,所测试的是,当其中的 `LimitTracker` 被告知要将 `value`,设置为大于其中的 `max` 值的某个值时,会发生什么事情。首先,这里创建出了一个新的 `MockMessage`,他将以一个空的消息清单开始。随后这里创建了一个新的 `LimitTracker`,并给到他了到那个新 `MockMessenger` 的引用,以及 `100` 的 `max` 值。这里以值 `80` 调用了 `LitmitTracker` 上的 `set_value` 方法,而该值是大于 `75` 小于 `100` 的。随后这里断言了 `MockMessenger` 正追踪的那个消息清单,现在应有一条消息在其中。 + +然而,该测试有一个问题,如下所示: + +```console +$ cargo test lennyp@vm-manjaro + Compiling limit_tracker v0.1.0 (/home/lennyp/rust-lang/limit_tracker) +error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference + --> src/lib.rs:58:13 + | +2 | fn send(&self, msg: &str); + | ----- help: consider changing that to be a mutable reference: `&mut self` +... +58 | self.sent_messages.push(String::from(message)); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable + +For more information about this error, try `rustc --explain E0596`. +error: could not compile `limit_tracker` due to previous error +warning: build failed, waiting for other jobs to finish... +``` + +由于其中的 `send` 方法取了一个到 `self` 的不可变引用,因此这里是无法修改那个 `MockMessenger` 值来追踪到那些消息的。这里还不能采取报错文本中,使用 `&mut self` 取代的建议,这是由于随后 `send` 的签名,将不与 `Messenger` 特质定义中的函数签名相匹配(请尽情尝试,并观察会得到什么样的报错消息)。 + +这正是内部可变性可帮到忙的一种情形!下面就将把那个 `sent_messages` 存储于一个 `RefCell` 内部,而接下来那个 `send` 方法,就将能够修改 `sent_messages`,以存储咱们曾见到过的那些消息了。下面清单 15-22 给出了那看起来的样子: + +文件名:`src/lib.rs` + +```rust +#[cfg(test)] +mod tests { + use super::*; + use std::cell::RefCell; + + struct MockMessenger { + sent_messages: RefCell>, + } + + impl MockMessenger { + fn new() -> MockMessenger { + MockMessenger { + sent_messages: RefCell::new(vec! []), + } + } + } + + impl Messenger for MockMessenger { + fn send(&self, message: &str) { + self.sent_messages.borrow_mut().push(String::from(message)); + } + } + + #[test] + fn it_sends_an_over_75_percent_waring_message() { + let mock_messenger = MockMessenger::new(); + let mut limit_tracker = LimitTracker::new(&mock_messenger, 100); + + limit_tracker.set_value(80); + + assert_eq! (mock_messenger.sent_messages.borrow().len(), 1); + } +} +``` + +*清单 15-22:在外层值被视为不可变的同时,使用 `RefCell` 改变内层值* + +那个 `sent_messages` 字段,现在就是类型 `RefCell>`,而非 `Vec` 的了。在其中的 `new` 函数里,这里围绕那个空矢量值,创建了一个新的 `RefCell>` 实例。 + +而对于那个 `send` 方法的实现,首个参数认识 `self` 的一个不可变借用,这与那个特质定义是相匹配的。这里调用了 `self.sent_messages` 值中,那个 `RefCell>` 类型实例上的 `borrow_mut` 方法,来获取了到这个 `RefCell>` 实例内部值,亦即那个矢量值的一个可变引用。随后这里调用了到该矢量值可变引用上的 `push` 方法,来最终测试期间所发送的那些消息。 + +这里必须做出的最后一项修改,是在那个断言中:为了看到在那个内部矢量值中有多少的条目,这里就要调用那个 `RefCell>` 上的 `borrow` 方法,来获取到其中矢量值的一个不可变引用。 + +既然咱们以及看到怎样使用 `RefCell`,接下来就要探究其原理了! + + +### 使用 `RefCell` 在运行时对借用进行追踪 + +**Keeping Track of Borrows at Runtime with `RefCell`** + +在创建不可变及可变引用时,咱们分别用到了 `&` 与 `&mut` 语法。而在 `RefCell` 下,咱们使用的是 `borrow` 与 `borrow_mut` 两个方法,他们均为属于 `RefCell` 那些安全 API 的一部分。其中 `borrow` 方法返回的是灵巧指针类型 `Ref`(the smart pointer type `Ref`),而 `borrow_mut` 则返回的是灵巧指针类型 `RefMut`(the smart pointer type `RefMut`)。这两种返回的类型,都实现了 `Deref` 特质,因此咱们就能向常规引用那样,对待他们。 + +`RefCell` 追踪了有多少个当前活动的 `Ref` 及 `RefMut`。在每次于某个 `RefCell` 值上调用 `borrow` 时,该 `RefCell` 都会增加其有多少个活动不可变借用计数。而在某个 `Ref` 值超出作用域时,该不可变借用计数,就会降低一个。跟编译时借用规则一样,`RefCell` 允许在任何时刻,有着多个不可变借用或一个的可变借用。 + +在咱们尝试违反这些规则时,与之前在引用下得到编译器报错相反,`RefCell` 的实现将在运行时终止运行。下面清单 15-23 就给出了清单 15-22 中那个 `send` 实现的修改版本。其中故意尝试创建出统一作用域的两个活动可变借用,来演示 `RefCell` 会在运行时阻止咱们这样做。 + +文件名:`src/lib.rs` + +```rust + impl Messenger for MockMessenger { + fn send(&self, message: &str) { + let mut borrow_one = self.sent_messages.borrow_mut(); + let mut borrow_two = self.sent_messages.borrow_mut(); + + borrow_one.push(String::from(message)); + borrow_two.push(String::from(message)); + } + } +``` + +*清单 15-23:创建出同一作用域中的两个可变引用,来发现 `RefCell` 将终止运行* + +这里创建了返回自 `borrow_mut` 的一个灵巧指针 `RefCell` 的变量 `borrow_one`。随后这里以同样方式创建了变量 `borrow_two` 中的另一可变借用。这就在同一作用域中构造了两个可变引用,而这是不运行的。在咱们运行这个库的测试时,清单 15-23 中的代码将不带任何报错地编译,但测试将失败: + +```console +$ cargo test  101 ✘   + Compiling limit_tracker v0.1.0 (/home/peng/rust-lang/limit_tracker) + Finished test [unoptimized + debuginfo] target(s) in 0.46s + Running unittests src/lib.rs (target/debug/deps/limit_tracker-98d6159d1b15eb72) + +running 1 test +test tests::it_sends_an_over_75_percent_waring_message ... FAILED + +failures: + +---- tests::it_sends_an_over_75_percent_waring_message stdout ---- +thread 'tests::it_sends_an_over_75_percent_waring_message' panicked at 'already borrowed: BorrowMutError', src/lib.rs:60:53 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + tests::it_sends_an_over_75_percent_waring_message + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +error: test failed, to rerun pass `--lib` +``` + +请注意该代码是以消息 `already borrowed: BorrowMutError` 终止运行的。这正是 `RefCell` 处理运行时违反借用规则的方式。 + +选择在运行时,而非编译时捕获借用报错,正如这里所做的那样,就意味着咱们潜在地会在程序开发过程中晚一点的时候,发现代码中的错误:那么就可能在直到代码部署到生产环境时,也没有发现这些错误。同时,咱们的代码还会因在运行时,而非编译时保持对那些借用的追踪,而遭受由此导致的性能代价。然而,`RefCell` 的使用,令到在只允许使用一些不可变值的上下文中,编写出正使用着的,可对自身加以修改,从而跟踪其所见到的那些消息的模拟对象成为可能。咱们可在权衡了其弊端,及相交常规引用所能提供到的更多功能后,合理使用 `RefCell` 这种灵巧指针。 + + +### 通过结合 `Rc` 与 `RefCell`,实现可变数据的多个所有者 + +**Having Multiple Owners of Mutable Data by Combining `Rc` and `RefCell`** + +使用 `RefCell` 的一种常见方式,便是与 `Rc` 结合运用。回顾 `Rc` 实现了某个数据有着多个所有者,但其只给出到那个数据的不可变访问。在有着保存了一个 `RefCell` 的 `Rc` 时,咱们就可得到,一个可以有着多个所有者,*且* 咱们可以改变的值。 + +比如,回顾清单 15-18 中的那个构造列表示例,其中使用了 `Rc` 来实现多个列表共用另一列表所有权。由于 `Rc` 仅保存着一些不可变值,因此一旦咱们创建出了那些清单,就再不能修改其中的任何值。下面就要加入 `RefCell`,来获得修改列表中那些值的能力。下面清单 15-24 给出了通过在那个 `Cons` 定义中,使用 `RefCell`,咱们就可以修改存储在所有列表中值了。 + +文件名:`src/main.rs` + +```rust +#[derive(Debug)] +enum List { + Cons(Rc>, Rc), + Nil, +} + +use crate::List::{Cons, Nil}; +use std::cell::RefCell; +use std::rc::Rc; + +fn main() { + let value = Rc::new(RefCell::new(5)); + + let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil))); + + let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a)); + let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a)); + + *value.borrow_mut() += 10; + + println! ("之后的 a = {:?}", a); + println! ("之后的 b = {:?}", b); + println! ("之后的 c = {:?}", c); +} +``` + +*清单 15-24:运用 `Rc>` 创建出可改变的 `List`* + +这里创建了为 `Rc>` 类型实例的一个值,并将其存储在名为 `value` 的一个变量中,如此咱们就可以在稍后直接访问他。接着这里在 `a` 中,创建了有着保存了 `value` 的 `Cons` 变种的一个 `List`。这里需要克隆 `value`,这样 `a` 与 `value` 都会有着那个内层值 `5` 的所有权,而非将所有权从 `value` 转移到 `a` 或让 `a` 从 `value` 借用。 + +这里把那个列表 `a`,封装在了一个 `Rc` 中,进而在创建列表 `b` 与 `c` 时,二者都可以引用到 `a`,正如咱们在清单 15-18 中所做的那样。在这里已创建出 `a`、`b` 与 `c` 中的三个列表后,就打算把 `10` 加到 `value` 中的那个值。咱们是通过调用 `value` 上的 `borrow_mut` 方法做到这点的,这用到了第 5 章中曾讨论过的自动解引用特性(参见 [`->` 操作符去哪儿了?](Ch05_Using_Structs_to_Structure_Related_Data.md#where-is-the-arrow-operator)),来将这个 `Rc` 解引用到内层的 `RefCell` 值。这个 `borrow_mut` 方法返回的是一个 `RefMut` 的灵巧指针,而咱们于其上使用了解引用运算符,并修改了那个内层值。 + +在打印 `a`、`b` 与 `c` 时,就可以看到他们都有了修改后的值 `15` 而非 `5`: + +```console +$ cargo run lennyp@vm-manjaro + Compiling cons_list_demo v0.1.0 (/home/lennyp/rust-lang/cons_list_demo) + Finished dev [unoptimized + debuginfo] target(s) in 0.17s + Running `target/debug/cons_list_demo` +之后的 a = Cons(RefCell { value: 15 }, Nil) +之后的 b = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil)) +之后的 c = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil)) +``` + +这样的技巧是相当整洁的!通过使用 `RefCell`,咱们就有了对外不可变的 `List` 值(an outwardly immutable `List` value)。而由于咱们可以使用 `RefCell` 上提供了对其内部可变性访问的那些方法,因此就可以在需要的时候修改一些数据。运行时的借用规则检查,保护了咱们免于数据竞争,而有的时候为了数据结构中的此种灵活性,是值得拿运行速度来换取的。请注意 `RefCell` 对于多线程代码是不生效的!`Mutex` 即为线程安全版本的 `RefCell`,而在第 16 章咱们就会讨论到 `Mutex`。 + + +## 引用循环会泄露内存 + +**Reference Cycles Can Leak Memory** + +Rust 的内存安全,确保的是难于,但并非不可能,意外创建出绝不会被清理的内存(即所谓的 *内存泄露,memory leak*)。完全防止内存泄露,不是 Rust 那些保证之一,这就意味着在 Rust 中,内存泄露即为内存安全的(preventing memory leaks entirely is not one of Rust's gurantees, meaning memory leaks are memory safe in Rust)。通过使用 `Rc` 及 `RefCell` 就能发现,Rust 是允许内存泄露的:创建出其中以循环方式,指向对方的一些引用是有可能的。由于循环中各个引用条目的引用计数,将永远到不了 `0`,而这些值就永远不会被弃用,这就创造了内存泄露。 + + +### 创建出循环引用 + +**Creaing a Reference Cycle** + +下面以清单 15-25 中的 `List` 枚举及一个 `tail` 方法开始,来看看循环引用会怎样发生,以及怎样防止循环引用: + + +文件名:`src/main.rs` + +```rust +use std::cell::RefCell; +use std::rc::Rc; +use crate::List::{Cons, Nil}; + +#[derive(Debug)] +enum List { + Cons(i32, RefCell>), + Nil, +} + +impl List { + fn tail(&self) -> Option<&RefCell>> { + match self { + Cons(_, item) => Some(item), + Nil => None, + } + } +} + +fn main() {} +``` + +*清单 15-25:保存着一个 `RefCell`,从而可修改 `Cons` 变种指向何处的一个构造列表定义* + +这里用的是清单 15-5 中那个 `List` 定义的另一变体。`Cons` 变种中的第二个元素,现在是 `RefCell>`,表示这里不是要如同在清单 15-24 中所做的那样,具备修改那个 `i32` 值的能力,这里是要修改某个 `Cons` 变种所指向的那个 `List` 值。这里还添加了在有着某个 `Cons` 变种时,实现便于访问其第二个项目的 `tail` 方法。 + +在下面清单 15-26 中,咱们添加了用到清单 15-25 中那些定义的 `main` 函数。此代码创建了变量 `a` 中的一个清单,以及变量 `b` 中指向 `a` 中清单的一个清单。随后他将 `a` 中的清单指向了 `b`,这就创建了一个循环引用。其间有着一些 `println!` 语句,来显示此过程中不同点位的那些引用计数。 + + +文件名:`src/main.rs` + +```rust +fn maiN() { + let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil)))); + + println! ("a 的初始 rc 计数 = {}", Rc::strong_count(&a)); + println! ("a 的下一条目 = {:?}", a.tail()); + + let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a)))); + + println! ("b 的创建后 a 的 rc 计数 = {}", Rc::strong_count(&a)); + println! ("b 的初始 rc 计数 = {}", Rc::strong_count(&b)); + println! ("b 的下一条目 = {:?}", b.tail()); + + if let Some(link) = a.tail() { + *link.borrow_mut() = Rc::clone(&b); + } + + println! ("在修改 a 之后 b 的 rc 计数 = {}", Rc::strong_count(&b)); + println! ("在修改 a 之后 a 的 rc 计数 = {}", Rc::strong_count(&a)); + + // 取消下面这行注释,就可以看到这里有着循环引用; + // 他将溢出堆栈(it will overflow the stack) + // println! ("a 的下一条目 = {:?}", a.tail()); +} +``` + +*清单 15-26:创建出相互指向的两个 `List` 的循环引用* + + +这里创建出了保存着变量 `a` 中,初始列表 `5, Nil` 的一个 `Rc` 实例。随后这里又创建了保存着变量 `b` 中包含了值 `10` 并指向了 `a` 中清单的另一个 `Rc` 实例。 + +这里修改了 `a` 从而其指向了 `b` 而非 `Nil`,于是创建了一个循环。咱们是通过是要那个 `tail` 方法,来获得到 `a` 中那个 `RefCell>` 的引用,这里将其放入到了变量 `link` 中。随后这里使用了这个 `RefCell>` 上的 `borrow_mut` 方法,来将保存着 `Nil` 的一个 `Rc`,修改为保存到 `b` 中的那个 `Rc`。 + +在保持那最后一个 `println!` 被注释掉,而运行此代码时,就会得到下面的输出: + +```console +$ cargo run lennyp@vm-manjaro + Compiling ref_cycle_demo v0.1.0 (/home/lennyp/rust-lang/ref_cycle_demo) + Finished dev [unoptimized + debuginfo] target(s) in 1.20s + Running `target/debug/ref_cycle_demo` +a 的初始 rc 计数 = 1 +a 的下一条目 = Some(RefCell { value: Nil }) +b 的创建后 a 的 rc 计数 = 2 +b 的初始 rc 计数 = 1 +b 的下一条目 = Some(RefCell { value: Cons(5, RefCell { value: Nil }) }) +在修改 a 之后 b 的 rc 计数 = 2 +在修改 a 之后 a 的 rc 计数 = 2 +``` + +在将 `a` 中的列表修改为指向 `b` 后,与 `b` 中的两个 `Rc` 实例引用计数均为 `2`。在 `main` 的结尾,Rust 会弃用掉变量 `b`,这会将那个 `b` `Rc` 实例的引用计数,从 `2` 降低到 `1`。因为他的引用计数为 `1` 而不是 `0`,因此那个 `Rc` 在内存堆上的内存,在此刻就不会被弃用。随后 Rust 会弃用 `a`,这同样会将那个 `a` `Rc` 实例的引用计数,从 `2` 降低到 `1`。由于另一 `Rc` 实例仍指向着他,因此该实例的内存也无法被弃用。那么分配给该清单的内存,将永不会被收回。为形象表示这个引用循环,这里创建出了下图 15-4 中的图示。 + +![相互指向的列表 `a` 与 `b` 的一个循环引用](images/15-04.svg) + +*图 15-04:相互指向的列表 `a` 与 `b` 的一个循环引用* + +在取消注释掉其中最后一个 `println!` 而运行该程序时,Rust 就会尝试以 `a` 指向 `b` 指向 `a` 如此往复,打印出这个循环,直到他溢出内存栈为止。 + +相较于真实世界的程序,这个示例中创建出循环引用的那些后果并不算非常残酷:在这里创建出那个循环引用之后,这个程序就立马结束了。然而,在更为复杂的程序在某个循环中分配了大量内存,并在其上耗费较长时间时,那么这个程序就会用到相比于其所需要更多的内存,且可能会是系统不堪重负,造成系统耗尽可用内存。 + +循环引用的创建并非一蹴而就,但也并非是不可能的。在有着包含着 `Rc` 值的一些 `RefCell` 值,或类似的带有内部可变性及引用计数的嵌套类型组合时,咱们就必须确保不会创建出循环引用。咱们不能依靠 Rust 来捕获到循环引用。创建出循环引用来,是属于程序中的逻辑错误,咱们应运用自动化测试、代码审阅,及其他一些软件开发实践来消除。 + +避免循环引用的另一种方案,便是重组咱们的数据结构,从而实现一些引用表达所有权,而一些引用则不是。结果就是,咱们是可以有着由一些所有权关系,与一些非所有权关系构成的循环,而只有所有权关系会影响到某个值是否可被丢弃。在清单 15-25 中,咱们是一直要那些 `Cons` 变种,拥有他们清单的所有权,那么重组其中的数据结构就是不可行的。接下来要看到用到了由一些父节点与子节点组成的图数据结构(graphs made up of parent nodes and child nodes)的示例,来发现在什么时候,非所有权关系是防止循环引用的恰当方式。 + + +### 防止引用循环:将 `Rc` 转变为 `Weak` + +到目前为止,咱们已经证实了调用 `Rc::clone` 会增加某个 `Rc` 示例的 `strong_count`,同时 `Rc` 示例只会在其 `strong_count` 为 `0` 时被清理掉。咱们还可以通过调用 `Rc::downgrade` 并传入一个到某个 `Rc` 的引用,而创建出到该 `Rc` 实例中值的 *弱引用,weak reference*。强引用是咱们可共用某个 `Rc` 实例的方式。弱引用并不表示某种所有权关系,且在某个 `Rc` 实例被清理掉时,他们的计数不会受影响。由于在一旦所涉及到那些值的强引用计数为 `0` 时,涉及到弱引用的全部循环都将被破坏,因此弱引用就不会导致循环引用(weak references don't express an ownership relationship, and their count doesn't affect when an `Rc` instance is cleaned up. They won't cause a reference cycle because any cycle involving some weak references will be broken once the strong reference count of values involved is `0`)。 + +在调用 `Rc::downgrade` 时,就会得到类型 `Weak` 的灵巧指针。调用 `Rc::downgrade` 不是把 `Rc` 实例中的 `strong_count` 加 `1`,而是把 `weak_count` 加 `1`。与 `strong_count` 类似,`Rc` 类型使用 `weak_count` 来追踪存在多少个 `Weak`。不同之处在于,对于 `Rc` 的被清理,是无需 `weak_count` 为 `0` 的。 + +由于 `Weak` 所引用的值,可能已被启用了,因此在以某个 `Weak` 所指向值来完成任何事情时,咱们必须确保那个值仍是存在的。而要确保这一点,是通过在 `Weak` 实例上调用 `upgrade` 方法实现的,该方法将返回一个 `Option>` 值。在那个 `Rc` 值尚未被弃用时,咱们就会得到一个 `Some` 的结果,而若那个 `Rc` 值已被弃用,则就会得到 `None` 的结果。由于 `upgrade` 返回的是一个 `Option>`,Rust 就将确保 `Some` 与 `None` 两种情形都被处理,进而就将不会有无效指针。 + +下面的示例,这里将创建其条目了解各自的子条目 *以及* 各自的父条目的一种树形数据结构,而非之前的其条目仅了解其下一条目的列表数据结构。 + + +**创建一种树形数据结构:有着字节点的节点, Creating a Tree Data Structure: a Node with Child Nodes** + +作为开头,这里将构建有着了解其子节点的一些节点。这里将创建出一个名为 `Node` 的结构体,保存着自身的 `i32` 值,以及到其子 `Node` 值的一些引用。 + +文件名:`src/main.rs` + +```rust +use std::cell::RefCell; +use std::rc::Rc; + +#[derive(Debug)] +struct Node { + value: i32, + children: RefCell>>, +} +``` + +这里要的是某个 `Node` 拥有其子节点,并想要以一些变量,来共用那样的所有权,从而就可以直接访问树中的各个 `Node`(we want a `Node` to own its children, and we want to share that ownership with variables so we can access each `Node` in the tree directly)。为了完成这一点,这里把其中的那些 `Vec` 条目,定义为了类型 `Rc` 的一些值。这里还打算修改哪些节点是另一节点的子节点,因此这里就有一个在 `children` 字段中,包裹着 `Vec>` 的 `RefCell`。 + +接下来,这里就将使用这个结构体定义,并创建出有着值 `3` 而没有子节点的一个名为 `leaf` 的 `Node` 实例,以及另一个有着值 `5` 及将 `leaf` 作为其子节点的 `branch` 实例,如下清单 15-27 中所示: + +文件名:`src/main.rs` + +```rust +fn main() { + let leaf = Rc::new(Node { + value: 3, + children: RefCell::new(vec! []), + }); + + let branch = Rc::new(Node { + value: 5, + children: RefCell::new(vec! [Rc::clone(&leaf)]), + }); +} +``` + +*清单 15-27:创建出没有子节点的一个 `leaf` 节点及将 `leaf` 作为其一个子节点的 `branch` 节点* + + +这里克隆了 `leaf` 中的 `Rc` 并将其存储在了 `branch` 中,表示 `leaf` 中的 `Node` 现在有了两个所有者:`leaf` 与 `branch`。这里就可以经由 `branch.children`,从 `branch` 到达 `leaf`,但并无从 `leaf` 到 `branch` 的途径。原因就在于 `leaf` 没有到 `branch` 的引用,而就不知道他们是相关的。这里想要 `leaf` 明白,`branch` 是其父节点。接下来就要完成这一点。 + + +**在子节点中添加到其父节点的引用,Adding a Reference from a Child to Its Parent** + +要让那个字节点了解他的父节点,这里就需要添加一个 `parent` 字段到这里的 `Node` 结构体定义。麻烦在于确定出 `parent` 字段应为何种类型。咱们清楚他不能包含一个 `Rc`,因为那样就会以 `leaf.parent` 指向 `branch` 且 `branch.children` 指向 `leaf`,而创建出循环引用,这将导致他们的 `strong_count` 值用不为零。 + +以另外一种方式,来设想这样的关系,父节点因拥有他的子节点:在父节点被弃用时,他的那些子节点也应被弃用。然而,子节点则不应拥有他的父节点:在咱们弃用某个子节点时,那个父节点应存在。这正是弱引用的情况! + +因此这里将把 `parent` 字段的类型,构造为使用 `Weak`,具体而言就是 `RefCell>`,而非 `Rc`。现在这个 `Node` 结构体定义看起来像下面这样: + +文件名:`src/main.rs` + +```rust +use std::cell::RefCell; +use std::rc::Rc; + +#[derive(Debug)] +struct Node { + value: i32, + parent: RefCell>, + children: RefCell>>, +} +``` + +节点将能够引用他的副节点,而不拥有父节点。在下面清单 15-28 中,把 `main` 更新为使用这个新定义,进而 `leaf` 节点将有引用其父节点,`branch` 的一种途径: + +文件名:`src/main.rs` + +```rust +fn main() { + let leaf = Rc::new(Node { + value: 3, + parent: RefCell::new(Weak::new()), + children: RefCell::new(vec! []), + }); + + println! ("叶子节点的父节点 = {:?}", leaf.parent.borrow().upgrade()); + + let branch = Rc::new(Node { + value: 5, + parent: RefCell::new(Weak::new()), + children: RefCell::new(vec! [Rc::clone(&leaf)]), + }); + + *leaf.parent.borrow_mut() = Rc::downgrade(&branch); + + println! ("叶子节点的父节点 = {:?}", leaf.parent.borrow().upgrade()); +} +``` + +*清单 15-28:带有到其父节点 `branch` 的弱引用的 `leaf` 节点* + +这个 `leaf` 节点的创建,与清单 15-27 类似,除了其中的 `parent` 字段:`leaf` 以不带父节点开始,因此这里创建了一个新的、空 `Weak` 引用实例。 + +到这里,在咱们尝试通过使用 `upgrade` 方法,获取 `leaf` 的父节点的引用时,就会得到一个 `None` 值。在首个 `println!` 语句的输出中,就看到了这点: + +```console +叶子节点的父节点 = None +``` + +在创建那个 `branch` 节点时,由于 `branch` 没有父节点,他也将有一个 `parent` 字段中的新 `Weak` 引用。这里仍将 `leaf` 作为 `branch` 的子节点之一。一旦咱们有了 `branch` 变量中的那个 `Node` 实例,就可以修改 `leaf`,来给到他一个到其父节点的 `Weak` 引用。这里使用了 `leaf` 的 `parent` 字段中,`RefCell>` 上的 `borrow_mut` 方法,并于随后使用了 `Rc::downgrade` 函数,来子 `branch` 变量中的那个 `Rc`,创建出到 `branch` 的 `Weak` 引用。 + +当咱们再度打印 `leaf` 的父节点时,这次就会得到保存着 `branch` 的一个 `Some` 变种:现在 `leaf` 就可以访问其父节点了!在打印 `leaf` 时,同样避免了清单 15-26 中曾有过的,最终以栈一出而告终的那个循环引用;其中的 `Weak` 引用,是作为 `(Weak)` 被打印出的: + +```console +叶子节点的父节点 = Some(Node { value: 5, parent: RefCell { value: (Weak) }, children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) }, children: RefCell { value: [] } }] } }) +``` + +没有了无限输出,就表示此代码并未创建出循环引用。咱们还可以通过查看从调用 `Rc::strong_count` 与 `Rc::weak_count` 得到的值,来说明这一点。 + + +**`strong_count` 与 `weak_count` 变化的直观表示,Visualizing Changes to `strong_count` and `weak_count`** + +下面来看看这些 `Rc` 实例,是怎样通过创建出新的内层作用域,并将 `branch` 定义迁移到那个作用域而变化的。通过这样做,咱们就可以看到在 `branch` 被创建出,及在其超出作用域而被弃用时,会发生什么。下面清单 15-29 中给出了这些修改: + +文件名:`src/main.rs` + +```rust +fn main() { + let leaf = Rc::new(Node { + value: 3, + parent: RefCell::new(Weak::new()), + children: RefCell::new(vec! []), + }); + + println! ( + "叶子节点的强引用计数:{},弱引用计数:{}\n", + Rc::strong_count(&leaf), + Rc::weak_count(&leaf), + ); + + { + let branch = Rc::new(Node { + value: 5, + parent: RefCell::new(Weak::new()), + children: RefCell::new(vec! [Rc::clone(&leaf)]), + }); + + *leaf.parent.borrow_mut() = Rc::downgrade(&branch); + + println! ( + "枝干节点的强引用计数:{},弱引用计数:{}\n", + Rc::strong_count(&branch), + Rc::weak_count(&branch), + ); + println! ( + "叶子节点的强引用计数:{},弱引用计数:{}\n", + Rc::strong_count(&leaf), + Rc::weak_count(&leaf), + ); + + } + println! ("叶子节点的父节点 = {:?}\n", leaf.parent.borrow().upgrade()); + println! ( + "叶子节点的强引用计数:{},弱引用计数:{}\n", + Rc::strong_count(&leaf), + Rc::weak_count(&leaf), + ); +} +``` + +*清单 15-29:在内层作用域中创建 `branch` 并对那些强弱引用计数进行检查* + +在 `leaf` 节点被创建出了后,其 `Rc` 便有了强引用计数 `1` 及弱引用计数 `0`。在那个内层作用域中,这里创建了 `branch` 并将其与 `leaf` 关联,在打印两种计数的那个时间点,`branch` 中 `Rc` 将有着强计数 `1` 与弱计数 `1`(由于 `leaf.parent` 以一个 `Weak` 指向了 `branch`)。在打印 `leaf` 中的两个计数时,由于 `branch` 现在有着存储在 `branch.childeren` 中,`leaf` 的 `Rc` 的一份克隆,因此咱们就会看到他将有着强引用计数 `2`,而他仍将有着弱引用计数 `0`。 + +在那个内存作用域结束时,`brach` 就超出了作用域,而那个 `Rc` 的强引用计数就会降低到 `0`,因此他的 `Node` 就被丢弃了。源自 `leaf.parent` 的弱引用计数 `1`,与这个 `Node` 是否被弃用无关,因此这里就不会得到任何内存泄露! + +在那个内层作用域结束之后,若咱们尝试访问 `leaf` 的父节点,就将再度得到 `None`。在该程序结束处,由于变量 `leaf` 此时又仅是到那个 `Rc` 的唯一引用,因此他里面的 `Rc`,将有着强引用计数 `1` 与弱引用计数 `0`。 + +管理这两种计数与值的弃用的全部逻辑,都被内建到了 `Rc` 与 `Weak`,以及二者的 `Drop` 特质实现中。通过在 `Node` 定义中,指明某个子节点到其父节点的关系,应为 `Weak` 的引用,咱们就能够在不创建出循环引用与内存泄露之下,让父节点指向子节点,并反过来让子节点也指向父节点。 + + +## 本章小节 + +本章涵盖了怎样运用灵巧指针,来做出相比与 Rust 默认在常规引用下,所做出的不同保证及权衡(this chapter covered how to use smart pointers to make different gurantees and trade-offs from those Rust makes by default with regular references)。其中的 `Box` 类型,有着已知大小,并指向分配在内存堆上的数据。而 `Rc` 类型,则对到内存堆上数据的引用数量加以追踪,因此那个数据变可以有多个所有者。`RefCell` 类型,以其内部可变性,而给到在需要一种不可变类型,却又需要修改那种类型的内层值时,咱们可用的一种类型;这种类型还强制要求在运行时,而非编译时的借用规则检查。 + +本章还讨论了 `Deref` 与 `Drop` 两个特质,他们实现了灵巧指针的很多功能。这里探讨了可导致内存泄露的循环引用,以及怎样运用 `Weak` 来防止他们。 + +若这一章激发了你的兴趣,而打算实现自己的灵巧指针,那么请查看 [The Rustonomicon](https://doc.rust-lang.org/nomicon/index.html) 了解更多有用信息。 + +接下来,咱们就将谈谈 Rust 中的并发问题了。咱们将了解到少数几个新的灵巧指针。 diff --git a/src/Ch16_Fearless_Concurrency.md b/src/Ch16_Fearless_Concurrency.md new file mode 100644 index 0000000..faa6ed7 --- /dev/null +++ b/src/Ch16_Fearless_Concurrency.md @@ -0,0 +1,922 @@ +# 无惧并发 + +**Fearless Concurrency** + +安全并高效地处理并发编程,是 Rust 的另一主要目标。所谓 *并发编程,concurrent programming*,是指其中程序的各部分独立地执行着,而 *并行编程,parallel programming*,则是指程序的不同部分于同一时间执行,随着越来越多的计算机利用了多处理器的优势,这两种编程范式变得日益重要起来。历史上,这两种情景下的编程,曾是有难度且容易出错的:Rust 就有望来改变这种局面。 + +早期阶段,Rust 团队曾认为确保内存安全与防止并发问题,属于要以不同方法来解决的两个单独挑战。随着时间的推移,团队发现所有权与类型系统,正是有助于管理内存安全,*及* 并发问题的一套强有力的工具!经由利用所有权与类型检查,许多的并发错误,就成了 Rust 中的编译时错误,而非运行时错误。因此,就不再是要咱们,在出现运行时并发错误时,花费大量时间尽力重现那些确切情形,而是那些不正确代码,将拒绝编译,并给出代码问题的错误提示。由此,咱们就可以在编写出错误代码时,而非潜在地于交付代码到生产之后,修复好这些代码。这里将 Rust 此方面的特性,亲切地取名为 *无惧并发,fearless concurrency*。无惧并发实现了编写出不带难以察觉错误的代码,且易于在不引入新代码错误之下,对代码加以重构。 + +> **注意**:为简化起见,这里将把许多的这些问题,指为 *并发,concurrency*,而非称作更准确的 *并发及/或并行,concurrency and/or parallel*。若本书是有关并发及/或并行编程的书,那么咱们就会更为具体。对于本章,请在任何提及 *并发* 之处,在内心里将其以 *并发及/或并行* 代换。 + +许多语言在他们所提供的,用于解决并发问题的方案上,都是机械教条主义的。比如,Erlang 有着消息传递方面并发的优雅功能,但在共用线程间状态方面,却只有一些晦涩难懂的的途径,for example, Erlang has elegant functionality for message-passing concurrency, but has only obscure ways to share state between threads。对于这类高级语言来讲,仅支持可行方案的子集,是说得通的一种策略,这是由于高级语言以放弃部分的掌控,而换取到抽象方面的收益。然而,那些底层语言,则被期望在各种情形下,都要提供最具性能的方案,进而在硬件上有着较少抽象。因此,Rust 便提供了用以适合于咱们自己不同情形与需求的各种方式,对问题加以建模的各种工具,therefore, Rust offers a variety of tools for modeling problems in whatever way is appropriate for your situtation and requirements。 + +以下即为本章咱们将涵盖的几个话题: + +- 怎样创建出线程,来在同一时间运行代码的不同片段,how to create threads to run multiple pieces of code at the same time; +- *消息传递,message-passing* 方面的并发,其中有着于线程间发送消息的一些通道; +- *状态共用,shared-state* 方面的并发,其中多个线程均对某个数据加以访问; +- `Sync` 与 `Send` 特质,他们俩把 Rust 并发方面的保证,扩展到 Rust 使用者所定义的类型,以及由标准库所提供的那些类型。 + + +## 运用线程来同步运行代码 + +**Using Threads to Run Code Simutaneously** + +在绝大多数当前的操作系统中,被执行的程序代码,都是运行于 *进程,a process* 中的,而所在的操作系统,则会同时管理多个进程。在程序内部,咱们同样可以有着同步运行的一些独立部分。运行这些独立部分的特性,便被称作 *线程,threads*。比如,web 服务器就可以有多个线程,如此他就可以在同一时间,响应多于一个的请求。 + +将咱们程序的运算,拆分为多个线程,来在同一时间运行多个任务,可以提升性能,但这样也增加了复杂度。由于线程能够同步运行,因此在于不同线程上,将要运行代码哪个部分的顺序方面,就没有了某种固有保证,because threads can run simultaneously, there's no inherent guarantee about the order in which parts of your code on different threads will run。这就会导致一些问题,诸如: + +- 竞争局面,其中线程正以不一致顺序,访问着一些数据或资源; +- 死锁问题,其中两个线程正相互等待,而阻止了他们继续运行下去; +- 只在一些确切情形下才发生,而难于重现并可靠修复的代码错误。 + +Rust 试图消除这些运用线程方面的负面影响,但在多线程情景下的编程,仍要深思熟虑,并要求与运行在单线程下程序,截然不同的代码架构。 + +诸多编程语言,都是以少数几种不同途径,实现的线程,且多数操作系统,均提供了编程语言为可以创建出线程而调用的 API。Rust 标准库使用的是线程实现的 1:1 模型,由此程序就会以一个语言线程,对应使用一个操作系统线程。也有实现了别的线程操作模型的代码箱,对这种 1:1 模型做出了取舍。 + + +### 使用 `spawn` 函数创建出一个新的线程 + +要创建出一个新的线程,咱们就要调用 `thread::spawn` 函数,并传递给他一个包含了打算在这个新线程中运行代码的闭包(在第 13 章中曾谈到过闭包)。下面清单 16-1 中的示例,会打印出来自主线程的一些文本,以及来自新线程的一些文本: + +文件名:`src/main.rs` + +```rust +use std::thread; +use std::time::Duration; + +fn main() { + thread::spawn(|| { + for i in 1..10 { + println! ("\t- 你好,这是来自生成线程的数字 {} !", i); + thread::sleep(Duration::from_millis(20)); + } + }); + + for i in 1..5 { + println! ("- 你好,这是来自主线程的数字 {} !", i); + thread::sleep(Duration::from_millis(20)); + } +} +``` + +*清单 16-1:创建出一个新线程来打印某物件,与此同时主线程也在打印着其他东西* + +请注意在 Rust 程序主线程完毕时,全部生成的线程就被关闭了,而不论他们是否已结束运行。该程序的输出每次都会有些许不同,但其看起来将如下所示: + +```console +- 你好,这是来自主线程的数字 1 ! + - 你好,这是来自生成线程的数字 1 ! +- 你好,这是来自主线程的数字 2 ! + - 你好,这是来自生成线程的数字 2 ! +- 你好,这是来自主线程的数字 3 ! + - 你好,这是来自生成线程的数字 3 ! +- 你好,这是来自主线程的数字 4 ! + - 你好,这是来自生成线程的数字 4 ! + - 你好,这是来自生成线程的数字 5 ! +``` + +到 `thread::sleep` 的调用,强制线程停止其执行短暂的时间,而允许别的线程运行。这些线程可能会轮流运行,但那并无保证:这取决于咱们的操作系统调度线程的方式。在此运行中,主线程就先行打印了,即便生成的线程中的打印语句,首先出现在代码中。而即便这里告诉了生成的线程,打印直到 `i` 为 `9` 的时候,但 `i` 在主线程关闭之前,仍只到了 `5`。 + +若在运行此代码时,只看到主线程的输出,或未看到任何重叠部分,那么就要尝试增加其中那个范围(`1..10`, `1..5`)的数字,来给操作系统创造出,更多的与线程之间切换的机会。 + + +### 使用 `join` 把手,等待全部线程结束 + +**Waiting for All Threads to Finish Using `join` Handles** + +清单 16-1 中的代码,不仅会由于主线程的结束而提前停止生成线程,并因为在线程运行的顺序上没有保证,咱们还根本无法确保其中的生成线程将得到完整运行! + +> **注**:在 `thred::sleep` 为 `1ms` 时,将偶发出现下面的运行结果: + +```console +- 你好,这是来自主线程的数字 1 ! + - 你好,这是来自生成线程的数字 1 ! +- 你好,这是来自主线程的数字 2 ! + - 你好,这是来自生成线程的数字 2 ! + - 你好,这是来自生成线程的数字 3 ! +- 你好,这是来自主线程的数字 3 ! +- 你好,这是来自主线程的数字 4 ! + - 你好,这是来自生成线程的数字 4 ! + - 你好,这是来自生成线程的数字 % +``` + +咱们可以通过将 `thread::spawn` 的返回值,保存在一个变量中,来修复该生成线程不运行或提前结束的问题。`thread::spawn` 的返回值类型为 `JoinHandle`。而 `JoinHandle` 值则是一个自有值,在咱们于其上调用 `join` 方法时,他将等待其线程执行完毕。下面清单 16-2 就给出了怎样使用清单 16-1 中所创建出的那个 `JoinHandle`,来确保该生成线程在 `main` 退出之前执行完毕: + +文件名:`src/main.rs` + +```rust +use std::thread; +use std::time::Duration; + +fn main() { + let handle = thread::spawn(|| { + for i in 1..10 { + println! ("\t- 你好,这是来自生成线程的数字 {} !", i); + thread::sleep(Duration::from_millis(20)); + } + }); + + for i in 1..5 { + println! ("- 你好,这是来自主线程的数字 {} !", i); + thread::sleep(Duration::from_millis(20)); + } + + handle.join().unwrap(); +} +``` + +*清单 16-2:保存一个来自 `thread::spawn` 的 `JoinHandle` 来确保该线程运行完毕* + +> **注**:结合第 9 章中 [因错误而中止的快捷方式:`unwrap` 与 `expect`](Ch09_Error_Handling.md#shortcuts-for-panic-on-error-unwrap-and-expect),表明 `join` 返回的是个 `Result` 类型的枚举值。 + +在这个把手上调用 `join`,就会阻塞那个当前运行的线程,直到由该把手所表示的该线程终止。所谓 *阻塞,blocking* 某个线程,是指那个线程被阻止执行工作或退出,*blocking* a thread means that thread is prevented from performing work or exiting。由于咱们已将到 `join` 的调用,放在了那个主线程的 `for` 循环之后,因此运行清单 16-2 中的代码,应产生出如下类似的输出(注:但每次运行的输出仍然不同): + +```console +- 你好,这是来自主线程的数字 1 ! + - 你好,这是来自生成线程的数字 1 ! +- 你好,这是来自主线程的数字 2 ! + - 你好,这是来自生成线程的数字 2 ! +- 你好,这是来自主线程的数字 3 ! + - 你好,这是来自生成线程的数字 3 ! +- 你好,这是来自主线程的数字 4 ! + - 你好,这是来自生成线程的数字 4 ! + - 你好,这是来自生成线程的数字 5 ! + - 你好,这是来自生成线程的数字 6 ! + - 你好,这是来自生成线程的数字 7 ! + - 你好,这是来自生成线程的数字 8 ! + - 你好,这是来自生成线程的数字 9 ! +``` + +两个线程依旧交替运行,但因为这个到 `handle.join()` 的调用,主线程就会等待,而在生成线程完毕之前不会结束。 + +不过来看看像下面这样,当咱们把 `handle.join()` 移至 `main` 中那个 `for` 循环前面时,会发生什么: + +文件名:`src/main.rs` + +```rust +use std::thread; +use std::time::Duration; + +fn main() { + let handle = thread::spawn(|| { + for i in 1..10 { + println! ("\t- 你好,这是来自生成线程的数字 {} !", i); + thread::sleep(Duration::from_millis(20)); + } + }); + + handle.join().unwrap(); + + for i in 1..5 { + println! ("- 你好,这是来自主线程的数字 {} !", i); + thread::sleep(Duration::from_millis(20)); + } +} +``` + +主线程将等待生成线程运行完毕,并于随后运行他的 `for` 循环,因此输出将不再交错,如下所示: + +```console + - 你好,这是来自生成线程的数字 1 ! + - 你好,这是来自生成线程的数字 2 ! + - 你好,这是来自生成线程的数字 3 ! + - 你好,这是来自生成线程的数字 4 ! + - 你好,这是来自生成线程的数字 5 ! + - 你好,这是来自生成线程的数字 6 ! + - 你好,这是来自生成线程的数字 7 ! + - 你好,这是来自生成线程的数字 8 ! + - 你好,这是来自生成线程的数字 9 ! +- 你好,这是来自主线程的数字 1 ! +- 你好,这是来自主线程的数字 2 ! +- 你好,这是来自主线程的数字 3 ! +- 你好,这是来自主线程的数字 4 ! +``` + +诸如 `join` 于何处被调用这样的细节,均会影响到咱们的线程,是否在同一时间运行。 + + +### 在线程上使用 `move` 闭包 + +**Using `move` Closures with Threads** + +由于传递给 `thread::spawn` 的闭包随后将取得其用到的环境中一些值的所有权,由此就会把这些值的所有权,从一个线程转移到另一线程,因此咱们今后将经常在这些闭包上,使用 `move` 关键字。在第 13 章 [“捕获引用或迁移所有权”](Ch13_Functional_Language_Features_Iterators_and_Closures.md#capturing-reference-or-moving-ownership) 小节,咱们就曾讨论过闭包语境下的 `move` 关键字。现在,咱们将更多地着重于 `move` 与 `thread::spawn` 之间的互动。 + +请注意在清单 16-1 中,传递给 `thread::spawn` 的那个闭包没有取任何参数:咱们没有在生成线程中,使用主线程中的任何数据。为在生成线程中使用主线程中的数据,那么生成线程的闭包就必须捕获其所需的值。下面清单 16-3 给出了在主线程中创建出一个矢量值,并在生成线程中用到这个矢量值的一种尝试。然而,正如即将看到的那样,这将尚不会运作。 + +文件名:`src/main.rs` + +```rust +use std::thread; + +fn main() { + let v = vec! [1, 2, 3]; + + let handle = thread::spawn(|| { + println! ("这里有个矢量值:{:?}", v); + }); + + handle.join().unwrap(); +} +``` + +*清单 16-3:尝试在另一线程中,使用由主线程创建出的一个矢量值* + +这个闭包用到了 `v`,因此他将捕获 `v` 并将其构造为该闭包环境的一部分。由于 `thread::spawn` 是在一个新线程中运行此闭包,因此咱们应能够在那个新线程内部访问 `v`。然而在编译这个示例时,咱们会得到如下报错: + +```console +cargo run lennyp@vm-manjaro + Compiling concur_demo v0.1.0 (/home/lennyp/rust-lang/concur_demo) +error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function + --> src/main.rs:9:32 + | +9 | let handle = thread::spawn(|| { + | ^^ may outlive borrowed value `v` +10 | println! ("这里有个矢量值:{:?}", v); + | - `v` is borrowed here + | +note: function requires argument type to outlive `'static` + --> src/main.rs:9:18 + | +9 | let handle = thread::spawn(|| { + | __________________^ +10 | | println! ("这里有个矢量值:{:?}", v); +11 | | }); + | |______^ +help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword + | +9 | let handle = thread::spawn(move || { + | ++++ + +For more information about this error, try `rustc --explain E0373`. +error: could not compile `concur_demo` due to previous error +``` + +Rust *推断出了,infers* 怎样去捕获 `v`,并由于 `println!` 值需要到 `v` 的一个引用,因此该闭包就尝试借用 `v`。然而,这里有个问题:Rust 无法识别出这个生成线程将运行多久,因此他就不清楚到 `v` 的引用是否将始终有效。 + +下面清单 16-4 提供了更倾向于有着到 `v` 的不将有效引用的一种场景: + +文件名:`src/main.rs` + +```rust +#![allow(dead_code)] +#![allow(unused_variables)] + +use std::thread; + +fn main() { + let v = vec! [1, 2, 3]; + + let handle = thread::spawn(|| { + println! ("这里有个矢量值:{:?}", v); + }); + + drop(v); // 噢,不要啊! + + handle.join().unwrap(); +} +``` + +*清单 16-4:有着尝试从弃用了 `v` 的主线程捕获到 `v` 引用的闭包的一个线程* + +若 Rust 运行咱们运行此代码,那么就有可能在一点也没有运行那个生成线程下,其就会被立即置于后台中,if Rust allowed us to run this code, there's a possibility the spawned thread would be immediately put in the background without running at all。那个生成线程内部有着一个到 `v` 的引用,而主线程则使用第 15 章中曾讨论过的 `drop` 函数,立即弃用了 `v`。随后,在生成线程开始执行时,`v` 就不再有效了,一次到他的引用也失效了。噢,不要! + +要修复清单 16-3 中的编译器错误,咱们可以使用错误消息中的建议: + +```console +help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword + | +9 | let handle = thread::spawn(move || { + | ++++ + +``` + +经由在那个闭包前添加 `move` 关键字,咱们就强制该闭包取得其用到值的所有权,而非让 Rust 来推断出他应借用该值。下面清单 16-5 给出的对清单 16-3 的修改,将如咱们设想的那样编译和运行: + +文件名:`src/main.rs` + +```rust +use std::thread; + +fn main() { + let v = vec! [1, 2, 3]; + + let handle = thread::spawn(move || { + println! ("这里有个矢量值:{:?}", v); + }); + + handle.join().unwrap(); +} +``` + +*清单 16-5:使用 `move` 关键字来强制闭包取得他所用到值的所有权* + +或许也会尝试以同样做法,通过使用 `move` 关键字,去修复清单 16-4 中,主线程调用了 `drop` 的代码。然而,由于清单 16-4 尝试完成的事情,因为一种不同原因而不被允许,那么这样的修复就不会凑效。在咱们把 `move` 添加到闭包时,咱们就会把 `v` 迁移到该闭包的环境中,进而咱们就无法再在主线程中,于其上调用 `drop` 了。这是会得到如下的编译器错误: + +```console +$ cargo run lennyp@vm-manjaro + Compiling concur_demo v0.1.0 (/home/lennyp/rust-lang/concur_demo) +error[E0382]: use of moved value: `v` + --> src/main.rs:13:10 + | +7 | let v = vec! [1, 2, 3]; + | - move occurs because `v` has type `Vec`, which does not implement the `Copy` trait +8 | +9 | let handle = thread::spawn(move || { + | ------- value moved into closure here +10 | println! ("这里有个矢量值:{:?}", &v); + | - variable moved due to use in closure +... +13 | drop(v); + | ^ value used here after move + +For more information about this error, try `rustc --explain E0382`. +error: could not compile `concur_demo` due to previous error +``` + +Rust 的所有权规则,再次挽救了咱们!由于 Rust 一直以来的保守,以及只为那个线程借用了 `v`,就意味着主线程理论上可以令到生成线程的引用失效,而得到了清单 16-3 中代码的报错。通过告知 Rust 将 `v` 的所有权迁移到生成线程,咱们就向 Rust 保证了主线程不会再使用 `v`。而若咱们以同样方式修改清单 16-4,那么随后在咱们于主线程中尝试使用 `v` 时,就破坏了那些所有权规则。这个 `move` 关键字,覆盖了 Rust 借用方面的保守做法;但他并无让咱们破坏所有权规则。 + +有了线程及线程 API 方面的基本认识,接下来就有看看用线程可以 *做,do* 些什么。 + + +## 使用消息传递来在线程间传输数据 + +**Using Message Passing to Transfer Data Between Threads** + +一种日渐流行的确保并发安全的方法,便是 *消息传递,message passing*,其中线程或参与者,通过相互发送包含数据的消息进行通信。下面就是摘自 [Go 语言文档](https://golang.org/doc/effective_go.html#concurrency) 的一句口号:“勿要通过共用内存进行通信;而要经由通信来共用内存。” + +为达成消息发送式的并发,Rust 标准库提供了 *信道,channels* 的一种实现。所谓信道,即数据被从一个线程,发送到另一线程,这种形式的一个通用编程概念。 + +咱们可以把编程中的信道,设想为带流向的水渠,如同一条小溪或一条河。在咱们把像是一只塑胶小黄鸭投入到一条小河中时,他就会顺流而下到达该水路的尽头。 + +信道有着两端:一个发送者和一个接收者。发送端即在将小黄鸭投入到河流中的上游位置,而接收端即为小黄鸭抵达的下游了。咱们代码中一个部分以打算发送的数据,调用发送者上的方法,而另一个部分则会查看接收端的抵达消息。在发射者或接收者端之一被弃用时,就算是信道被 *关闭,closed* 了。 + +下面,咱们将完成有着一个线程生成一些值并将这些值发送到信道,同时有另一线程将接收这些值并将其打印出来的这么一个程序。咱们将在线程间使用信道发送一些简单值,来演示这项特性。一旦咱们熟悉了这项技巧,那么就可以对任何需要相互通讯的线程,比如聊天系统,或其中有许多线程执行着某项计算的各个部分,并把这些部分发送到结果汇总线程的系统等中使用信道。 + +首先,在下面的清单 16-6 中,咱们将创建出一个信道而不使用他来完成任何事情。请注意由于 Rust 无法分辨出咱们打算通过该信道,发送何种类型的值,该代码尚不会编译。 + +文件名:`src/main.rs` + +```rust +use std::sync::mpsc; + +fn main() { + let (tx, rx) = mpsc::channel(); +} +``` + +*清单 16-6:创建出一个信道,并将两端赋值给 `tx` 与 `rx`* + +咱们使用 `mpsc::channel` 函数,创建了一个新的信道;`mpsc` 表示的是 *多生产者,单一消费者,multiple producer, single consumer*。简而言之,Rust 标准库实现信道的方式,表明信道可以有多个生成值的 *发送,sending* 端,但只有一个消费这些值的 *接收,receiving* 端。请设想有多条小溪,汇流到一条大河:那么从任何这些小溪,送下来的东西,都将在最后抵达一条河中。现在咱们将以单个的生产者开始,在令到这个示例工作起来时,就将添加多个生产者。 + +这个 `mpsc::channel` 函数返回的是一个元组,元组的首个元素为发送端 -- 发送器,the transmitter -- 而第二个元素就是接收端 -- 接收器,the receiver。两个缩写 `tx` 与 `rx`,传统上在许多领域,都相应地被用于表示 *transmitter* 与 *receiver*,因此咱们就把这两个变量,如此命名来表示两端。咱们使用了带有模式,a pattern (`(tx, rx)`)的一个 `let` 语句,对这个元组加以解构;在第 18 章中,咱们将讨论这种 `let` 遇见中模式的运用及解构问题。至于现在,请明白以这种方式使用 `let` 语句,是提取由 `mpsc::channel` 返回元组中那些部分的便捷方式。 + +下面就把其中的发射端,移入到一个生成线程,并让其发送一个字符串,从而生成线程就在与主线程通信了,如下清单 16-7 中所示。这就像是在河流上游投入一只小黄鸭,或是在一个线程发送了一条聊天消息给另一线程。 + +文件名:`src/main.rs` + +```rust +use std::sync::mpsc; +use std::thread; + +fn main() { + let (tx, rx) = mpsc::channel(); + + thread::spawn(move || { + let val = String::from("你好"); + tx.send(val).unwrap(); + }); +} +``` + +*清单 16-7:把 `tx` 迁移到一个生成线程并发送 "你好"* + +又一次,咱们使用了 `thread::spawn` 创建出一个新线程,并使用 `move` 把 `tx` 迁移进到那个闭包,于是这个生成的线程,便拥有了 `tx`。该生成线程需要拥有发送器,才能够经由信道发送消息。发送器有着取咱们打算发送值的一个 `send` 方法。而这个 `send` 方法返回的是个 `Result` 类型值,那么在接收器已被弃用,而无处发送值时,那么发送操作就将返回一个错误。在此示例中,咱们调用了 `unwrap` 来在出现错误时终止运行,panic in case of an error。而在真实应用中,咱们应予以恰当处理:请回到第 9 章,回顾那些那些适当的错误处理策略。 + +在下面的清单 16-8 中,咱们将自主线程中的接收器,获取到那个值。这就像是在河流尽头接收到小黄鸭,或是接收到一条聊天消息。 + +文件名:`src/main.rs` + +```rust +use std::sync::mpsc; +use std::thread; + +fn main() { + let (tx, rx) = mpsc::channel(); + + thread::spawn(move || { + let val = String::from("你好"); + tx.send(val).unwrap(); + }); + + let received = rx.recv().unwrap(); + println! ("收到:{}", received); +} +``` + +*清单 16-8:在主进程中接收值 “你好” 并将其打印出来* + +接收器有着两个有用方法:`recv` 与 `try_recv`。这里使用的是 `recv`,是 *receive* 的简写,该方法将阻塞主线程执行,而等待直到某个值被下发到信道。一旦某个值被发出,那么 `recv` 就会在一个 `Result` 中将其返回。在发射器关闭时,`recv` 就会返回一个错误,表明不会再有值到来。 + +`try_recv` 方法则不会阻塞,而相反会立即返回一个 `Result`:在消息可用时的一个保存着消息的 `Ok` 值,同时此刻没有任何消息时的一个 `Err` 值。在该线程在等待消息时,有其他工作要完成的情况下,使用 `try_recv` 便是有用的:咱们可以编写出每隔一段时间就调用 `try_recv` 的循环,在有消息时处理消息,再次检查是否收到消息之前的空隙,完成一些其他工作。 + +这里使用 `recv` 是为了简化;在主线程中,除了等待消息之外并无其他工作要做,因此阻塞主线程是恰当的。 + +当咱们运行清单 16-8 中的代码时,就会看到该值在主线程中被打印出来: + +```console +收到:你好 +``` + +好极了! + + +### 信道与所有权的转移 + +**Channels and Ownship Transference** + +由于所有权规则帮助咱们编写出安全、并行的代码,因此其在消息发送中起着至关重要作用。在并发式编程中,于咱们 Rust 程序通篇考虑所有权的好处,就在于这样可以防止错误。接下来就要完成一项实验,来展示信道与所有权,是怎样一起运作以阻止问题发生的:咱们将在生成线程中,把一个 `val` 值送到信道 *之后,after*,再尝试使用这个值。尝试编译清单 16-9 中的代码,来观察为何该代码是不被允许的: + +文件名:`src/main.rs` + +```rust +use std::sync::mpsc; +use std::thread; + +fn main() { + let (tx, rx) = mpsc::channel(); + + thread::spawn(move || { + let val = String::from("你好"); + tx.send(val).unwrap(); + println! ("val 为 {}", val); + }); + + let received = rx.recv().unwrap(); + println! ("收到:{}", received); +} +``` + +*清单 16-9:在咱们已将 `val` 发送到信道之后在尝试使用他* + +在这里,咱们在经由 `tx.send` 已把 `val` 发出到信道之后,尝试打印出他。允许这样做将是个糟糕的主意:一旦该值已被发送到另一线程,那么在咱们尝试再度使用该值之前,发往的那个线程就可能修改或是弃用掉该值。而潜在地,另一线程的这些改动,就会由于不一致或不存在的数据,而造成错误或未预期结果。不过,在咱们尝试编译清单 16-9 中的代码时,Rust 会给到咱们一个报错: + +```console +$ cargo run  ✔   + Compiling mp_demo v0.1.0 (/home/peng/rust-lang/mp_demo) +error[E0382]: borrow of moved value: `val` + --> src/main.rs:13:31 + | +11 | let val = String::from("你好"); + | --- move occurs because `val` has type `String`, which does not implement the `Copy` trait +12 | tx.send(val).unwrap(); + | --- value moved here +13 | println! ("val 为 {}", val); + | ^^^ value borrowed here after move + | + = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info) + +For more information about this error, try `rustc --explain E0382`. +error: could not compile `mp_demo` due to previous error +``` + +咱们犯下的并发错误就造成一个编译时报错。其中的 `send` 函数取得了其参数的所有权,进而在那个值被迁移时,接收器便取得了他的所有权。这就阻拦了咱们在发送了该值后,无意中地再度使用该值;所有权系统会检查各方面都妥当无虞。 + +### 发送出多个值并观察接收器的等待 + +**Sending Multiple Values and Seeing the Receiver Waiting** + +清单 16-8 中的代码编译并运行了,不过其并未清楚地给出,两个单独线程是怎样通过信道相互交流的。在下面清单 16-10 中,咱们已做出将证实清单 16-8 中代码有在并发运行的一些修订:生成线程现在将发出多条消息,并在每条消息之间暂停一秒钟。 + +文件名:`src/main.rs` + +```rust +use std::sync::mpsc; +use std::thread; +use std::time::Duration; + +fn main() { + let (tx, rx) = mpsc::channel(); + + thread::spawn(move || { + let vals = vec! [ + String::from("你好"), + String::from("自"), + String::from("此"), + String::from("线程"), + ]; + + for val in vals { + tx.send(val).unwrap(); + thread::sleep(Duration::from_millis(500)); + } + }); + + for received in rx { + println! ("收到:{}", received); + } +} +``` + +*清单 16-10:发送多条消息,并在每次发送之间进行暂停* + +这次,其中的生成线程,有着咱们打算发送到主线程的一个字符串矢量值。咱们对其迭代,而分别发送每条消息,同时通过使用 `500` 毫秒的一个 `Duration` 值,调用 `thread::sleep` 在每次消息发送间暂停。 + +在主线程中,咱们未再显式调用 `recv` 函数:相反,咱们将 `rx` 当作了迭代器。对于所接收到的每个值,咱们就将其打印出来。在信道被关闭时,迭代就结束了。 + +在运行清单 16-10 中的代码时,咱们就会看到下面每行之间有着 500ms 暂停的输出: + +```console +收到:你好 +收到:自 +收到:此 +收到:线程 +``` + +由于咱们在主线程中的那个 `for` 循环中,并无任何暂停或延迟的代码,因此咱们就可以说,主线程是在等待接收来自生成线程的那些值。 + +### 通过克隆发射器创建出多个生产者 + +**Creating Multiple Producers by Cloning the Transmitter** + +早前咱们曾提到,`mpsc` 是 *multiple producer, single consumer* 的首字母缩写。接下来就要就要运用上 `mpsc`,并将清单 16-10 中的代码,扩充为创建出均将一些值发送到同一接收器的多个线程。通过克隆发射器,咱们就可以这样做,如下清单 16-11 中所示: + +文件名:`src/main.rs` + +```rust +use std::sync::mpsc; +use std::thread; +use std::time::Duration; + +fn main() { + let (tx, rx) = mpsc::channel(); + + let tx1 = tx.clone(); + thread::spawn(move || { + let vals = vec! [ + String::from("你好"), + String::from("自"), + String::from("此"), + String::from("线程"), + ]; + + for val in vals { + tx1.send(val).unwrap(); + thread::sleep(Duration::from_millis(500)); + } + }); + + thread::spawn(move || { + let vals = vec! [ + String::from("给"), + String::from("你"), + String::from("一些别的"), + String::from("消息"), + ]; + + for val in vals { + tx.send(val).unwrap(); + thread::sleep(Duration::from_millis(500)); + } + }); + + for received in rx { + println! ("收到:{}", received); + } +} +``` + +*清单 16-11:从多个生产者发出多条消息* + +这次在创建出首个生成线程之前,咱们调用了发射器上的 `clone` 方法。这样做将给到咱们可传递给那首个生成线程的一个新发射器。咱们把原先的发射器,传递给了第二个生成线程。这样就给到了咱们两个线程,二者都把不同消息,发送到那一个的接收器。 + +在运行此代码时,咱们的输出看起来应像下面这样: + +```console +收到:你好 +收到:给 +收到:自 +收到:你 +收到:此 +收到:一些别的 +收到:线程 +收到:消息 +``` + +根据咱们所在系统的不同,也可能会看到另外顺序的这些值。这种消息每次出现顺序的不一致,正是令到并发有趣而又有难度的地方。而若带上 `thread::sleep` 加以实验,即在两个不同线程中给到不同睡眠值,这时的每次运行,将更具不确定性,而每次运行都造成不同输出。 + +既然咱们已经看到了信道的工作原理,那么接下来就要看看一种方式迥异的并发了。 + + +## 状态共用的并发 + +**Shared-State Concurrency** + +消息传递是处理并发的一种很好方式,但其并非唯一的一种。另一种方式将是,多个线程访问同一共用数据。请重新考虑一下摘自 Go 语言文档的那句口号的这个部分:“勿要经由共用内存通信。do not communicate by sharing memory.” + +那么经由共用内存的通信,又会是怎样的呢?另外,为何消息传递方式拥趸们,会警告不要使用内存共用方式呢? + +在某种程度上,任何编程语言中的信道,均类似于单一所有权,因为一旦咱们把值传递到信道,那么就不应再使用那个值了。内存共用的并发,则像是多重所有权:多个线程均可在同一时间,访问同一内存位置。正如咱们在第 15 章中曾见到过的,那里的灵巧之中令到多重所有权可行,多重所有权会因为这些不同所有者需要管理,而增加复杂度。Rust 的类型系统与所有权规则,极大地助力了实现这样的管理正确无误。作为一个示例,接下来咱们就要看看作为共用内存的一种更常见并发原语,即所谓的互斥量,for an example, let's look at mutexes, one of the more common concurrency primitives for shared memory。 + + +### 运用互斥量实现一个时间仅允许一个线程访问数据 + +**Using Mutexes to Allow Access to Data from One Thread at a Time** + +*互斥,mutex* 是 *相互排斥,mutual exclusion* 的缩写,正如互斥量在任何给定时间,都只允许一个线程访问某个数据。要访问互斥量中的数据,线程就必须首先通过询问来获取到该互斥量的 *锁,lock*,表明其打算访问。所谓锁,则是保持着当前是谁(哪个线程)有着对该数据排他性访问的追踪,作为该互斥量一部分的一种数据结构,the lock is a data structure that is part of the mutex that keeps track of who currently has exclusive access to the data。因此,所谓互斥量,就被描述为经由这种加锁系统,而 *守护着,guarding* 其所保存着的数据。 + +由于咱们务必要记住以下两条规则,互斥量便有了难以运用的名声: + +- 在使用数据之前,咱们必须尝试获取到锁; +- 在完成互斥量所保护数据的操作时,咱们必须解开该数据,以便其他线程能够获取到锁。 + +至于互斥量的真实世界比喻,请设想在仅有一只麦克风的会议上的一个小组讨论。那么在小组成员能发言之前,他们就不得不请求或表明,他们打算使用麦克风。在他们得到麦克风时,他们便可以想要讲多长时间便讲多长时间,并在随后吧麦克风,递给下一位要求发言的小组成员。在某名小组成员于用完麦克风,却忘记交出麦克风时,就没有人能发言了。在这个共用麦克风的管理出错时,这个小组就将不会如计划那样运作了! + +互斥量的管理非常棘手,难以做到正确无误,这正是许多人热衷于信道的原因。但是,归功于 Rust 的类型系统与所有权规则,咱们就无法在互斥量的加锁与解锁上出错了。 + + +### `Mutex` 的 API + +下面是如何使用互斥量的一个示例,接下来咱们就要如下面清单 16-12 中所给出的那样,通过在单一线程情形下使用互斥量开始: + +文件名:`src/main.rs` + +```rust +use std::sync::Mutex; + +fn main() { + let m = Mutex::new(5); + + { + let mut num = m.lock().unwrap(); + *num = 6; + } + + println! ("m = {:?}", m); +} +``` + +*清单 16-12:为简化目的在单个线程情形下探讨 `Mutex` 的 API* + +与许多类型一样,咱们使用关联函数 `new` 创建出了一个 `Mutex`。而为了访问这个互斥量内部的数据,咱们使用了 `lock` 方法来获取到锁。此调用将阻塞当前线程,从而在轮到咱们拥有锁之前,当前线程就无法完成任何工作。 + +若有另一持有着锁的线程已终止运行,那么到 `lock` 的调用就会失败。在那种情况下,就没人能获得锁了,因此咱们就选择了 `unwrap`,而在咱们陷入到那样的情形时,让这个线程终止运行。 + +在获取到锁后,咱们就可以对此示例中名为 `num` 的返回值,作为到互斥量内部数据的可变引用,而加以处理了。类型系统会确保咱们在使用 `m` 里的值前,获取到锁。`m` 的类型为 `Mutex`,而非 `i32`,因此咱们为了使用那个 `i32` 值, 就 *必须* 调用 `lock`。这是不能忘掉的;否则类型系统就不会让咱们访问那个内层的 `i32`。 + +正如咱们可能怀疑的那样,`Mutex` 是个灵巧指针。更准确地讲,到 `lock` 的调用,*返回的是* 封装在咱们曾以到 `unwrap` 调用处理的 `LockResult` 中,一个叫做 `MutexGuard` 的灵巧指针。`MutexGuard` 灵巧之中实现了 `Deref`,来指向咱们的内层数据;这个灵巧指针还有着在 `MutexGuard` 超出作用域,即清单 16-12 的示例内存作用域结束处所发生时,自动释放锁的一个 `Drop` 实现。而其结果就是,由于锁的释放是自动发生的,因此咱们就不会面临,忘记释放锁而阻塞该互斥量为其他线程使用的风险。 + +在弃用了该所之后,咱们就可以打印出该互斥量的值,并看到咱们是能够把那个内层的 `i32`,修改为 `6` 的。 + + +### 在多个线程间共用 `Mutex` + +现在,咱们就来尝试使用 `Mutex`,在多个线程见共用值。咱们将启动 10 个线程,并让他们分别都把一个计数器增加 `1`,因此那个计数器就会从 `0` 到达 `10`。接下来清单 16-13 中的示例,将有着一个编译器报错,同时咱们将使用那个报错,来掌握更多有关使用 `Mutex`,以及 Rust 如何帮助咱们正确运用他的知识。 + +文件名:`src/main.rs` + +```rust +use std::sync::Mutex; +use std::thread; + +fn main() { + let counter = Mutex::new(0); + let mut handles = vec! []; + + for _ in 0..10 { + let handle = thread::spawn(move || { + let mut num = counter.lock().unwrap(); + + *num += 1; + }); + + handles.push(handle); + } + + for handle in handles { + handle.join().unwrap(); + } + + println! ("结果为:{}", *counter.lock().unwrap()); +} +``` + +*清单 16-13:各自分别对由 `Mutex` 所守护计数器递增的十个线程* + +与在清单 16-12 中一样,咱们创建出了一个在 `Mutex` 内,保存着一个 `i32` 的 `counter` 变量。接下来,咱们通过对数字范围的迭代,创建出了 10 个线程。咱们使用了 `thread::spawn`,并给到全部线程同样闭包:把那个计数器迁移进到线程,通过调用 `lock` 方法取得那个 `Mutex` 上的锁,并于随后加 `1` 到该互斥量的值的这样一个闭包。在线程完成运行其闭包时,`num` 就会超出作用域而释放那把锁,从而另一线程便可以取得该锁。 + +在主线程中,咱们收集起了所有连接把手,collect all the join handles。随后,如同在清单 16-2 中所做的那样,咱们在各个把手上调用了 `join`,来确保所有现场运行完毕。在那个点位处,主线程将取得那把锁,并打印出该程序的结果。 + +咱们曾暗示过此示例不会编译。现在就来找出原因为何! + +```console +$ cargo run  ✔   + Compiling mutex_demo v0.1.0 (/home/peng/rust-lang/mutex_demo) +error[E0382]: use of moved value: `counter` + --> src/main.rs:12:36 + | +8 | let counter = Mutex::new(0); + | ------- move occurs because `counter` has type `Mutex`, which does not implement the `Copy` trait +... +12 | let handle = thread::spawn(move || { + | ^^^^^^^ value moved into closure here, in previous iteration of loop +13 | let mut num = counter.lock().unwrap(); + | ------- use occurs due to use in closure + +For more information about this error, try `rustc --explain E0382`. +error: could not compile `mutex_demo` due to previous error +``` + +这个报错消息指出,其中的 `counter` 值在循环的上一次迭代中已被迁移。Rust 正告诉咱们,不能将锁 `counter` 的所有权,迁移进到多个线程中。下面就来使用第 15 张中曾讨论过的多重所有权方式,修正这个编译器报错。 + +### 多线程下的多重所有权 + +**Multiple Ownership with Multiple Threads** + +在第 15 章中,咱们曾通过使用灵巧指针 `Rc`,来创建出一个引用计数的值,而将一个值赋予到多个所有者。下面就来完成那同样的操作,并看到会发生什么。咱们将在清单 16-14 中,把那个 `Mutex` 封装在 `Rc` 中,并在把所有权迁移到线程之前,克隆这个 `Rc`。 + +文件名:`src/main.rs` + +```rust +use std::rc::Rc; +use std::sync::Mutex; +use std::thread; + +fn main() { + let counter = Rc::new(Mutex::new(0)); + let mut handles = vec! []; + + for _ in 0..10 { + let counter = Rc::clone(&counter); + let handle = thread::spawn(move || { + let mut num = counter.lock().unwrap(); + + *num += 1; + }); + + handles.push(handle); + } + + for handle in handles { + handle.join().unwrap(); + } + + println! ("结果为:{}", *counter.lock().unwrap()); +} +``` + +*清单 16-14:尝试使用 `Rc` 来实现多个线程拥有那个 `Mutex`* + +又一次,咱们编译并得到......一些不同的报错!编译器给了咱们很多指教。 + +```console +$ cargo run  ✔   + Compiling mutex_demo v0.1.0 (/home/peng/rust-lang/mutex_demo) +error[E0277]: `Rc>` cannot be sent between threads safely + --> src/main.rs:14:36 + | +14 | let handle = thread::spawn(move || { + | ------------- ^------ + | | | + | ______________________|_____________within this `[closure@src/main.rs:14:36: 14:43]` + | | | + | | required by a bound introduced by this call +15 | | let mut num = counter.lock().unwrap(); +16 | | +17 | | *num += 1; +18 | | }); + | |_________^ `Rc>` cannot be sent between threads safely + | + = help: within `[closure@src/main.rs:14:36: 14:43]`, the trait `Send` is not implemented for `Rc>` +note: required because it's used within this closure + --> src/main.rs:14:36 + | +14 | let handle = thread::spawn(move || { + | ^^^^^^^ +note: required by a bound in `spawn` + +For more information about this error, try `rustc --explain E0277`. +error: could not compile `mutex_demo` due to previous error +``` + +喔,那报错消息真的非常罗嗦!而这些才是要关注的重要部分:`` `Rc>` cannot be sent between threads safely ``。编译器还告诉咱们了其原因:`` the trait `Send` is not implemented for `Rc>` ``。下一小节咱们就要讲到 `Send` 特质:他是确保咱们用到类型,是意图用于并发情形的特质之一。 + +不幸的是,`Rc` 于跨线程的共用上是不安全的。在 `Rc` 管理着引用计数时,他会增加每次到 `clone` 调用的计数,并在每个克隆被弃用时减去计数。但其并未使用任何并发原语,any concurrency primitives,来确保那些对该计数的改变,不被另一线程中断。这就会导致错误的计数 -- 进而会导致内存泄漏,或在咱们未完成值处理之前,该值就已被启用这样的一些微妙代码缺陷。咱们所需要的,是像极了 `Rc`,但会令到引用计数以线程安全方式得以改变的一种类型。 + +> **注**:简单地说,与各种编程语言中的那些原生数据类型,primitive data types 一样,所谓并发原语,concurrency primitives,指的就是用于并发编程的一些基本设施,the basic facilities for concurrent programming,这样的说法,某种程度上是跨越某个语言家族(比如 C 语言家族)。 +> +> 参考:[What-are-concurrency-primitives-"K Symbol"](https://qr.ae/prtpz6) + +### `Arc` 下的原子引用计数 + +**Atomic Reference Counting with `Arc`** + +幸运的是,`Arc` *正是* 安全用于并发情形下的一个像是 `Rc` 的类型。其中的 `a` 代表着 `原子,atomic`,表示其是一种 *原子的引用计数,atomically reference counted* 类型。原子类型是咱们不会在此详细讨论的一类额外并发原生类型:请参阅 [`std::sync::atomic` 的标准库文档](https://doc.rust-lang.org/std/sync/atomic/index.html),了解更多细节。此刻,咱们只需要知道这些原子类型会像那些原生类型一样运作,只不过他们对于跨线程的共用是安全的。 + +到这里咱们可能想知道,为何全部原生类型不是原子的,以及为何标准库的那些类型,没有默认使用 `Arc` 实现。原因就是线程安全自带了性能损失,而只有在咱们真的需要线程安全时,才会打算付出。在咱们只是在单线程里于一些值上执行操作时,若咱们的代码不必强制实现原子类型所提供的那些保证,那么这些代码就可以运行得快得多。 + +接下来回到那个示例:`Arc` 与 `Rc` 有着同样的 API,因此通过修改其中的 `use` 语句行、到 `new` 的调用,以及到 `clone` 的调用,咱们就可以修复那个程序。清单 16-15 中的代码最终将会编译及运行: + +文件名:`src/main.rs` + +```rust +use std::sync::{Arc, Mutex}; +use std::thread; + +fn main() { + let counter = Arc::new(Mutex::new(0)); + let mut handles = vec! []; + + for _ in 0..10 { + let counter = Arc::clone(&counter); + let handle = thread::spawn(move || { + let mut num = counter.lock().unwrap(); + + *num += 1; + }); + + handles.push(handle); + } + + for handle in handles { + handle.join().unwrap(); + } + + println! ("结果为:{}", *counter.lock().unwrap()); +} +``` + +*清单 16-15:为能够跨越多线程地共用所有权,而使用 `Arc` 来封装那个 `Mutex`* + +此代码将打印出下面的内容: + +```console +结果为:10 +``` + +咱们就做到了!咱们从 `0` 计数到了 `10`,这或许看起来不是非常印象深刻,但他真的教会了咱们很多有关 `Mutex` 与线程安全的东西。咱们也可以运用这个程序的架构,完成相比于增加计数器,一些更为复杂的操作。运用这种策略,咱们可把某项计算,划分为一些独立部分,将这些部分拆解为多个线程,并于随后使用 `Mutex` 来让各个各个线程,使用其自己部分对最终结果加以更新。 + +请注意若咱们是在完成一些简单的数字运算,你们就有由 [标准库的 `std::sync::atomic` 模组](https://doc.rust-lang.org/std/sync/atomic/index.html) 所提供的,相较于 `Mutex` 更简单的一些类型。这些类型提供到原生类型安全、并发、原子的访问。咱们为这个示例而选择带有原生类型的 `Mutex`,目的是可以着重于 `Mutex` 的工作原理。 + + +### `RefCell`/`Rc` 与 `Mutex`/`Arc` 二者之间的相似点 + +**Similarities Between `RefCell`/`Rc` and `Mutex`/`Arc`** + +咱们或许已经留意到,其中那个 `counter` 是不可变的,但咱们却能获取到其内部值的可变引用;这意味着与 `Cell` 家族,the `Cell` family 所做的一样, `Mutex` 提供了内部可变性。与咱们在第 15 章中曾使用 `RefCell` 来实现修改 `Rc` 内部内容同样的方式,咱们使用了 `Mutex` 来修改 `Arc` 内部内容。 + +另一个需要注意的细节,便是在咱们使用 `Mutex` 时,Rust 无法保护咱们免于全部类别的逻辑错误。回顾在第 15 章中,`Rc` 运用就伴随着创建出循环引用风险,其中两个 `Rc` 值相互指向,导致内存泄漏。与此类似,`Mutex` 则附带着创建出 *死锁,deadlocks* 的风险。在某个操作需要锁住两项资源,同时两个线程分别均已请求获取两把锁中的一把时,就造成他们一直等待着对方释放各自所需的锁。若对死锁方面感兴趣,那么请尝试创建出有着死锁的一个 Rust 程序;随后就要研究任何一门语言中,互斥量的死锁消除策略,并试试在 Rust 中实现这些策略。`Mutex` 和 `MutexGuard` 的标准库 API 文档,就提供了一些有用信息。 + +咱们将通过讲解 `Send` 与 `Sync` 两个特质,以及怎样与一些定制类型来运用他们来完结本章。 + + +## `Sync` 与 `Send` 两个特质下的可扩展并发 + +**Extensible Concurrency with the `Sync` and `Send` Traits** + + +有趣的是,Rust 语言并发方面的特性 *非常* 少。本章中到目前为止咱们讲到过的每种并发特性,都已是标准库而非语言本身的一部分。用于处理并发问题的选项,并不局限于这门语言或标准库;咱们可以编写自己的并发特性,或可以使用由其他人编写的并发特性。 + +不过,在这门语言中,是嵌入了两个并发概念的:即 `std::marker` 特质 `Sync` 与 `Send`。 + + +### 使用 `Send` 特质实现线程间所有权转移 + +**Allowing Transference of Ownership Between Threads with `Send`** + + +这个 `Send` 标识符特质,表示实现 `Send` 类型值的所有权,可以在线程间转移。几乎全部 Rust 类型都是 `Send` 类型,但有一些例外,包括 `Rc`:由于在咱们克隆了某个 `Rc` 并尝试将这份克隆的所有权,转移到另一线程时,两个现场可能在同一时间更新引用计数,因此 `Rc` 就不能是 `Send` 类型。由于这个原因,`Rc` 正是为其间咱们不打算付出线程安全方面性能开销的那些单线程情形,而实现的。 + +由此,Rust 的类型系统与特质边界,type system and trait bounds,就确保了咱们绝不会意外地将某个 `Rc`,不安全地跨越线程发送。当咱们在清单 16-14 中尝试这样做时,咱们就曾得到编译器报错 `` the trait `Send` is not implemented for `Rc>` ``。而在咱们切换到 `Arc` 这种 `Send` 类型时,那段代码就编译了。 + +由全部 `Send` 类型所组成的类型,也会被自动标记为 `Send` 类型。除开那些原始指针,raw pointers 外,那么可以说几乎全部原生类型都是 `Send` 的,咱们将在第 19 章中,讲到那些原始指针。 + + +### 使用 `Sync` 实现来自多个线程的访问 + +**Allowing Access from Multiple Threads with `Sync`** + +`Sync` 标识符表示实现 `Sync` 特质的类型,其被从多个线程引用是安全的。换句话说,任何类型 `T` 在 `&T` (即到 `T` 的不可变引用) 为 `Send` 的时,那么其即为 `Sync` 的,表示该引用可以安全地发送到另一线程。与 `Send` 类似,原生类型均为 `Sync` 的,且由全部都是 `Sync` 的类型所组成的类型,也都是 `Sync` 的。 + +灵巧指针 `Rc` 因为其不是 `Send` 的同样原因,其也不是 `Sync` 的。`RefCell` 类型(咱们曾在第 15 章讲过)以及相关的 `Cell` 类型家族,都不是 `Sync` 的。`RefCell` 在运行时所完成的借用检查实现,不是线程安全的。灵巧指针 `Mutex` 是 `Sync` 的,并正如咱们在 [于多个线程间共用 `Mutex`](#sharing-a-mutex-t-between-multiple-threads) 小节中看到的,其可被用于多个线程下共用访问。 + + +### 手动实现 `Send` 与 `Sync` 是不安全的 + +**Implementing `Send` and `Sync` Manually Is Unsafe** + + +由于 `Send` 与 `Sync` 特质构成的类型自动也是 `Send` 与 `Sync` 的,因此咱们大可不必手动实现这两个特质。而作为标记性特质,二者甚至都没有任何要实现的方法。他们只是在执行与并发性有关的不变性方面很有用。 + +手动实现这两个特质,涉及到实现一些不安全 Rust 代码,unsafe Rust code。在第 19 章咱们将讲到运用不安全 Rust 代码;至于现在,要点在于构造不是由一些 `Send` 与 `Sync` 部分组成的新并发类型,需要深思熟虑来维持那些安全保证。[The Rustonomicon](https://doc.rust-lang.org/nomicon/index.html) 有着这些保证的更多信息,以及维持这些保证的方式。 + + +## 本章小节 + +这不会是你在本书中将见到并发的最后一章:第 20 张中的那个项目,就将在相比于这里所讨论过较小示例,而更具现实意义的情形下用到本章中的那些概念。 + +正如早先所提到的,由于只有极少量的 Rust 处理并发方式,属于这门语言的一部分,因此许多并发解决方案,都是作为代码箱实现的。这些方案相比标准库进化更为迅速,那么就要确保在线搜寻当前的、最前沿代码箱,来用于多线程情形中。 + +Rust 标准库提供了用于消息传递的信道,以及诸如 `Mutex` 与 `Arc` 等安全用于并发情景中的一些灵巧指针类型。类型系统与借用检查器,会确保应用了这些方案的代码,不会以数据竞争或无效引用结束。一旦让代码编译了,咱们就可以放下心来,代码将愉快地运行于多线程之上,而不会有在其他语言中常见的那些难于追踪的问题。并发编程自此不再是令人害怕的概念:去吧,让你的程序并发起来,无所畏惧! + +接下来,咱们将讲到,随着咱们的 Rust 程序变得大了起来,建模问题与架构出方案的一些管用做法。此外,咱们将讨论 Rust 的一些习惯说法,这些说法可能与面向对象编程中所熟悉的有关。 diff --git a/src/Ch17_Object_Oriented_Programming_Features_of_Rust.md b/src/Ch17_Object_Oriented_Programming_Features_of_Rust.md new file mode 100644 index 0000000..b638380 --- /dev/null +++ b/src/Ch17_Object_Oriented_Programming_Features_of_Rust.md @@ -0,0 +1,863 @@ +# Rust 的一些面向对象编程特性 + +**Object Oriented Programming Features of Rust** + +面向对象编程方法,object-oriented programming, OOP, 是建模程序的一种方法。对象是在 20 世纪 60 年代,在编程语言 Simula 中所引入的一个程序化概念。正是那些对象,影响了 Alan Kay 的编程架构,其中对象会相互传递消息。为描述这种架构,他在 1967 年创造了面向对象编程这个术语。有许多互相竞争的定义,都描述了 OOP 是什么,而根据其中一些定义,Rust 属于面向对象的,但根据另一些,Rust 则不属于面向对象的。在本章中,咱们将探讨通常被看作是面向对象的一些特征,以及这些特征怎样被转译为 Rust 的习惯说法。随后咱们将给出在 Rust 怎样实现面向对象的设计模式,并讨论在这样做,与相反采用 Rust 的一些长处来实现解决方案,之间的权衡取舍。 + + +## 面向对象语言的特征 + +**Characteristics of Object-Oriented Languages** + + +在编程界,并无关于某门被视为面向对象的,而必须具有哪些特性的共识。Rust 受了许多编程范式,programming paradigms,的影响,其中就包括 OOP;比如在第 13 章中,咱们就曾探讨过,那些来自于函数式编程的特性。可以说,那些 OOP 的语言,确实是共用了一些确切的特征的,那即是对象、封装与继承等。下面就来看看这些特征各自指的是什么,以及 Rust 是否支持他们。 + + +### 对象包含了数据及行为 + +**Objects Contain Data and Behavior** + + +Erich Gamma、Richard Helm、Ralph Johnson 及 John Vlissides 等的合著 *Design Patterns: Elements of Reusable Object-Oriented Software* (Addison-Wesley Professional, 1994),又被通俗地叫做 *The Gang of Four* 书,便是面向对象设计模式的一个目录。该书像下面这样定义了 OOP: + +> 面向对象程序是由对象所组成的。*对象,an object* 同时打包了数据与运行在那数据上的过程。这些过程一般就叫做 *方法,methods* 或 *操作,operations*。 + +运用这个定义,Rust 便是面向对象的:结构体与枚举均有着数据,而 `impl` 块则提供了结构体与枚举上的那些方法。即使有着方法的那些结构体与枚举未*被称作* 对象,根据 The Gang of Four 的对象定义,他们提供了同样的功能。 + + +### 隐藏了实现细节的封装 + +**Encapsulation that Hides Implementation Details** + + +通常与 OOP 相关的另一方面的 *封装,encapsulation*,是指对于用到该对象的代码,对象实现细节是不可访问的。由此,与对象交互的唯一方式,便是经由该对象的公开 API;运用对象的代码,不应具备到达该对象内部,而直接改变数据或行为的能力。这实现了程序员在无需修改用到对象的那些代码之下,修改或重构对象的那些内部代码。 + +在第 7 章中,咱们曾讨论过怎样控制封装:咱们可以使用 `pub` 关键字,来决定咱们代码中,哪些模组、类型、函数与方法等应为公开的,而默认其他所有项目都是私有的。比如,咱们就可以定义有着包含 `i32` 值矢量的一个字段的 `AveragedCollection` 结构体。这个字段也可以有包含着那个矢量中值的平均数的一个字段,表示在有人需要该平均值时,不必按需计算出该平均值。换句话说,`AveragedCollection` 将为咱们缓存这个计算出的平均值。下面清单 17-1 便有着这个 `AveragedCollection` 结构体的定义: + +文件名:`src/lib.rs` + +```rust +pub struct AveragedCollection { + list: Vec, + average: f64, +} +``` + +*清单 17-1:维护着一个整数清单及该集合中项目平均数的 `AveragedCollection` 结构体* + +该结构体被标记为 `pub`,从而其他代码就可以使用他,而该结构体内部的那些字段保持着私有。由于咱们打算不管何时在有某个值被添加到清单,或从清单移除时,其中的平均数也要同时被更新,因此在这个示例中这样的封装就很重要。咱们是通过实现下面清单 17-2 中所给出的 `add`、`remove` 及 `average` 方法,做到这一点的。 + +文件名:`src/lib.rs` + +```rust +impl AveragedCollection { + pub fn add(&mut self, value: i32) { + self.list.push(value); + self.update_average(); + } + + pub fn remove(&mut self) -> Option { + let result = self.list.pop(); + match result { + Some(value) => { + self.update_average(); + Some(value) + } + None => None, + } + } + + pub fn average(&self) -> f64 { + self.average + } + + fn update_average(&mut self) { + let total: i32 = self.list.iter().sum(); + self.average = total as f64 / self.list.len() as f64; + } +} +``` + +*清单 17-2:`AveragedCollection` 上公开方法 `add`、`remove` 与 `average` 的实现* + +这些公开方法 `add`、`remove` 与 `average`,是仅有的访问或修改 `AveragedCollection` 实例中数据的方式。在使用 `add` 方法或 `remove` 方法,添加或移除某个条目时,其各自的实现,就同时会调用处理更新 `average` 字段的私有 `update_average` 方法。 + +咱们把 `list` 与 `average` 自动留着私有,从而外部代码就无法直接添加项目到那个 `list`,或直接从那个 `list` 移除项目;不然的话,在那个`list` 变化时,`average` 字段就可能失去同步。其中的 `average` 方法,返回的是 `average` 字段中的值,这实现了外部代码读取那个 `average` 而不会修改他。 + +由于咱们已封装了结构体 `AveragedCollection` 的实现细节,因此咱们就可以在将来轻易地修改各个方面,诸如数据结构等。比如,咱们可以对其中的 `list` 字段,使用 `HashSet` 而非 `Vec`。只要 `add`、`remove` 及 `average` 三个公开方法的签名保持不变,那些使用 `AveragedCollection` 的代码就无需改变。而相反若咱们把 `list` 构造为公开,就未必如此了:`HashSet` 与 `Vec` 有着添加和一处条目的不同方法,由此在外部代码直接修改 `list` 时,就大概率不得不修改了。 + +若封装是某门语言被视为面向对象的要件,你们 Rust 是满足那种要求的。对代码的不同部分,使用抑或不使用 `pub` 的选项,实现了实现细节的封装。 + + +### 以类型系统及以代码共用的继承 + +**Inheritance as a Type System and as Code Sharing** + + +*继承,inheritance*,乃籍以实现对象从另一对象继承一些元素,从而在不必再度定义这些元素之下,获得父辈对象数据与行为的一种机制。 + +若某们语言务必要有着继承,方能成为一门面向对象语言,那么 Rust 就不算是面向对象语言。在不使用宏,a macro 之下,没有定义出继承父辈结构体字段与方法实现的结构体的方法。 + +然而,若在编程工具箱中惯于使用继承,那么依据咱们将继承作为头等大事的自身理由,是可以运用 Rust 中别的一些办法的。 + +之所以选用继承,大致有两种原因。一个是代码的重用:咱们可以对一个类型实现一些特定行为,而继承就让咱们可以对另一类型重用那些实现。咱们可以使用一些默认的特质方法实现,即咱们曾在清单 10-14 中,将 `summarize` 方法的一个默认实现,添加到 `Summary` 特质上时所见到的那样,在 Rust 代码中以一种受限方式做到这点。任何实现了这个 `Summary` 特质的类型,在无需更多代码之下,都将在其上有着这个 `summarize` 方法。这与父类有着某个方法的实现,同时集成的子类也会有着该方法的实现是类似的。在实现这个 `Summary` 特质时,咱们也可以重写 `summarize` 方法的默认实现,这与子类重新继承自父类的方法实现类似。 + +而使用与类型系统相关继承的另一原因:即为了实现在与父类型的同一地方,使用子类型。这又被成为 *多态,polymorphism*,是指在多个对象共用了一些确切特征时,咱们可以相互替换使用他们。 + +> **关于多态** +> +> 对许多人来讲,多态等同于继承。但他实际上指的是代码可工作于多个类型数据之下的一个宽泛概念。而对于继承,这些类型则是通用的一些子类。 +> +> Rust 则运用了泛型,来对各异的各种可能类型加以抽象,并使用特质边界来强化这些类型所必须提供的那些约束。有时这样的做法,又被叫做 *有边界的参数化多态,bounded parametric polymorphism*。 + +由于继承通常有着共用了超出必要代码的风险,时至今日,其已在许多编程语言中,作为编程设计模式而失宠了。子类本不应共用其父类的全部特征,但在继承父类时却会这样做。这就会造成程序的设计有较低的灵活性。由于子类从父类继承的一些方法并不适用于子类,因此继承还会引入调用子类上无意义或引发错误方法的可能。此外,一些语言还只将运行单一继承(即子类只能从一个类继承),这进一步限制了程序设计的灵活度。 + +由于这些原因,Rust 便采取了运用特质对象,而非继承的方法。接下来就要看看特质对象是如何实现 Rust 中的多态。 + + +## 使用允许不同类型值的特质对象 + +**Using Trait Objects That Allow for Values of Different Types** + +> **注**:这类似于 Java 语言中,解决死亡钻石问题(DDD)的 [接口](https://java.xfoss.com/ji-cheng-he-duo-tai-ji-zhi/ch08_interfaces_and_abstract_classes#interface_rescue)。 + +在第 8 章中,咱们就提到过矢量值的一个局限,便是他们只能存储一种类型的元素。在清单 8-9 中咱们创建出了一种变通方案,其中定义了有着分别保存整数、浮点数与文本变种的 `SpreadsheetCell` 枚举。这就意味着咱们可在各个单元格中存储不同类型的数据,而仍旧有了表示这些单元格所组成行的一个矢量值。这对于在咱们的代码被编译时,就已经清楚这些可交换项目,为固定类型集的情况,这确实是一种相当不错的解决办法。 + +然而,有时咱们会想要咱们库的用户,能够扩展这个于某种特定情形下有效的类型集。为展示咱们将怎样达成这个目的,接下来咱们将创建对一个条目清单加以迭代的示例性图形用户界面,graphical user interface,GUI 工具 -- 对于 GUI 工具来讲这可是一项常见技能。咱们将创建包含 GUI 库架构的名为 `gui` 的一个库代码箱。此代码箱会包含给人类使用的一些类型,比如 `Button` 或 `TextField`。此外,`gui` 的用户将希望创建出他们自己的能被绘制出来的类型:比如,某个程序员要添加一个 `Image`,而另一程序员则要添加一个 `SelectBox`。 + +对于这个示例,咱们不会实现一个完全成熟的 GUI 库,而是会给出这些部分将怎样一起配合起来。在编写这个库时,咱们没法了解而定义出其他那些程序员可能想要创建的全部类型。但咱们肯定清楚 `gui` 需要追踪各种不同类型的许多不同值,同时他还需要调用这些不同类型值上的 `draw` 方法。其无需明白在咱们调用该 `draw` 方法时,具体会发生什么,他只需知道那个值会让那个方法可被咱们调用。 + +在有着继承的某门语言中要做到这点,咱们可能会定义其上有着名为 `draw` 的方法的一个名为 `Component` 类。至于其他类,比如 `Button`、`Image` 与 `SelectBox` 等,将从 `Component` 基础并因此继承这个 `draw` 方法。他们可以分别重写这个 `draw` 方法,来定义他们的定制行为,而框架则可以将全部这些类型,当作 `Component` 的实例对待而调用他们之上的 `draw`。但由于 Rust 并无继承,因此咱们需要另一种方法,来架构这个 `gui` 库,来允许用户以新类型来扩展他。 + + +### 定义用于共同行为的特质 + +**Defining a Trait for Common Behavior** + + +为了实现咱们想要 `gui` 所拥有的行为,咱们将定义将有着一个名为 `draw` 方法的名为 `Draw` 特质。随后咱们就可以定义取 *特质对象,a trait object* 的一个矢量。特质对象会同时指向实现了这个指定特质的某个类型,以及用于在运行时查找那个类型上特质方法的一张表。咱们是通过指定某种指针,比如某个 `&` 的引用,或某个 `Box` 的灵巧指针,接着便是 `dyn` 关键字,以及随后指明相关特质,创建出特质对象。(在第 19 章的 [“动态大小类型与 `Sized` 特质”](Ch19_Advanced_Features.md#dynamically-sized-types-and-the-sized-trait) 小节咱们将讲到特质对象必须使用指针的原因。)在泛型或具体类型处,咱们就可以使用特质对象。而不论在何处使用特质对象,Rust 的类型系统都会确保在编译时,在那样的上下文中的任何值,都将实现该特质对象的特质。于是,咱们就无需掌握编译时的所有可能类型了。 + + +咱们已经提到过,在 Rust 中,咱们避免将结构体与枚举称为 “对象”,是为了将二者与其他语言中的对象区别开来。在结构体或枚举中,结构体字段中的数据,与 `impl` 代码块中的行为是分开的,而在其他语言中,数据与行为被结合为通常被标称为对象的这么一个概念。然而,特质对象由于其结合了数据与行为,而 *真的* 更像其他语言中的对象。但从无法添加数据到特质对象上看,特质对象是不同于传统的对象的。特质对象并不如其他语言中的对象那样普遍的有用:其特定用途为实现共用行为的抽象。 + +下面清单 17-3 给出了怎样定义有着一个名为 `draw` 方法的一个名为 `Draw` 的特质: + + +文件名:`src/lib.rs` + +```rust +pub trait Draw { + fn draw(&self); +} +``` + +*清单 17-3:`Draw` 特质的定义* + +这种语法应与在第 10 章中关于定义特质的方式看起来类似。接下来便有了一种新的语法:下面清单 17-4 定义了保存着一个名为 `components` 矢量的一个名为 `Screen` 的结构体。该矢量为类型 `Box` 的,而 `Box` 便是一个特质对象;`Box` 是 `Box` 里头实现了 `Draw` 特质的全部类型的代名词。 + +文件名:`src/lib.rs` + +```rust +pub struct Screen { + pub components: Vec>, +} +``` + +*清单 17-4:带有保存着一个实现了 `Draw` 特质的特质对象矢量的 `components` 字段的 `Screen` 结构体的定义* + +在这个 `Screen` 结构体上,咱们将定义将调用其 `components` 各条目上 `draw` 方法的一个名为 `run` 的方法,如下清单 17-5 中所示: + +文件名:`src/lib.rs` + +```rust +impl Screen { + pub fn run(&self) { + for component in self.components.iter() { + component.draw(); + } + } +} +``` + +*清单 17-5:`Screen` 上会调用各组件上 `draw` 方法的一个 `run` 方法* + + +这与定义出用到带有特质边界泛型参数的结构体,原理是不同的。泛型参数在某个时间只能用一种具体类型替换,而特质对象则允许在运行时填入多种具体类型。比如,咱们本可以像在下面清单 17-6 中那样,将这个 `Screen` 结构体定义为使用泛型与特质边界: + +文件名:`src/lib.rs` + +```rust +pub struct Screen { + pub components: Vec, +} + +impl Screen +where + T: Draw, +{ + pub fn run(&self) { + for component in self.components.iter() { + component.draw(); + } + } +} +``` + +*清单 17-6:其 `run` 方法用到泛型与特质边界的 `Screen` 结构体的一种替代实现* + +这种写法就会将咱们限制到有着全是类型 `Button` 或全是类型 `TextField` 组件清单的某个 `Screen` 实例。在咱们将仅有着同质集合,homogeneous collections,时,由于那些定义在编译时,为使用具体类型而将被单一化,那么此时使用泛型与特质边界便是更可取的做法。 + +另一方面,有了使用特质对象的方法,一个 `Screen` 实例便可以保存包含着 `Box