Rust-标准库秘籍-全-
Rust 标准库秘籍(全)
原文:
annas-archive.org/md5/f3e1f43f23141e4adef27926c224cf83译者:飞龙
前言
Mozilla 的 Rust 正在逐渐获得关注,它拥有惊人的特性和强大的库。本书将带你通过各种食谱,教你如何利用标准库来实现高效的解决方案。
本书首先简要介绍了标准库和集合的基本模块。从那里开始,食谱将涵盖支持文件/目录处理和(反)序列化的常见数据格式的 crate。你将了解与高级数据结构、错误处理和网络相关的 crate。你还将学习如何使用 futures 和实验性的夜间功能。本书还涵盖了 Rust 中最相关的外部 crate。你将能够使用库的标准模块来编写自己的算法。
到本书结束时,你将能够熟练地使用《Rust 标准库食谱》。
本书面向的对象
这本书是为那些想要探索 Rust 的力量并学习如何使用标准库来实现各种功能的开发者而写的。假设你具备基本的 Rust 编程知识和终端技能。
本书涵盖的内容
第一章,学习基础知识,构建了一个强大的基础,包括在所有各种情况下都很有用的基本原理和技术。它还将向你展示如何在命令行上接受用户输入,这样你就可以以交互式的方式编写所有后续章节,如果你愿意的话。
第二章,使用集合,为你提供了 Rust 中所有主要数据集合的全面概述,其中包括了如何在你的 RAM 中存储数据,以便你准备好为正确的任务选择正确的集合。
第三章,处理文件和文件系统,展示了如何将你的现有工具与文件系统连接起来,让你能够在计算机上存储、读取和搜索文件。我们还将学习如何压缩数据,以便高效地通过互联网发送。
第四章,序列化,介绍了当今最常见的数据格式以及如何在 Rust 中(反)序列化它们,使你能够通过跨工具通信将你的程序连接到许多服务。
第五章,高级数据结构,通过提供有关通用模块和 Rust 智能指针的有用信息,为后续章节构建了基础。你还将学习如何通过使用自定义#[derive()]语句在编译时为注解结构生成代码。
第六章,处理错误,向您介绍 Rust 的错误处理概念,以及如何通过为您的用例创建自定义错误和记录器来无缝与之交互。我们还将探讨如何设计结构,以便在用户未察觉的情况下清理其资源。
第七章,并行和 Rayon,带您进入多线程的世界,并证明 Rust 确实为您提供了无畏的并发性。您将学习如何轻松地调整您的算法,以充分利用您 CPU 的全部功能。
第八章,使用未来,向您介绍程序中的异步概念,并为您准备所有与 futures(Rust 中在程序后台运行的任务的版本)一起工作的库。
第九章,网络,通过教授您如何设置低级服务器、响应不同协议中的请求以及与世界万维网进行通信,帮助您连接到互联网。
第十章,使用实验性夜间功能,确保您在 Rust 知识上保持领先。它将通过向您展示最期待的功能(这些功能仍被视为不稳定)来展示编程的明天之路。
要充分利用本书
本书是为 rustc 1.24.1 和 rustc 1.26.0-nightly 版本的 Rust 编写并测试的;然而,Rust 强大的向后兼容性应使您能够使用除最后一章之外的所有章节的新版本。第十章使用实验性夜间功能正在与前沿技术合作,预计将通过突破性的变化得到改进。
要下载最新的 Rust 版本,请访问 rustup.rs/,在那里您可以下载适用于您操作系统的 Rust 安装程序。将其保留在标准设置上是可以的。在开始第十章,使用实验性夜间功能之前,请确保调用 rustup default nightly。不用担心,当时候您还会再次被提醒。
许多配方需要活跃的互联网连接,因为我们将与 crates 进行密集型工作。这些是 Rust 在互联网上分发库的方式,它们托管在 crates.io/。
您可能会想知道,一本关于 Rust 标准库(简称 std)的书为什么使用了这么多来自 std 之外的语言。那是因为与大多数其他系统语言不同,Rust 从一开始就被设计为具有强大的依赖管理。将 crate 拉入代码中非常容易,因此很多特定功能已经外包给了官方推荐的 crate。这有助于与 Rust 一起分发的核心标准库保持简单和非常稳定。
在 std 之后,最官方的 crate 组是nursery (github.com/rust-lang-nursery?language=rust)。这些 crate 是许多操作的标准,它们几乎足够稳定或通用,可以包含在 std 中。
如果我们在 nursery 中找不到某个菜谱的 crate,我们会查看 Rust 核心团队成员的 crate (github.com/orgs/rust-lang/people),他们投入了大量精力提供标准库中缺失的功能。这些 crate 不在 nursery 中,因为它们通常足够具体,不值得投入太多资源来积极维护它们。
本书中的所有代码都已使用最新的 rustfmt(rustfmt-nightly v0.4.0)格式化,您可以选择使用 rustup component add rustfmt-preview 进行下载,并使用 cargo fmt 运行。GitHub (github.com/jnferner/rust-standard-library-cookbook)上的代码将积极维护,并使用 rustfmt 的新版本进行格式化,如果可用。在某些情况下,这意味着源代码行标记可能会过时。然而,由于这种变化通常不超过两到三行,因此应该不难找到代码。
所有代码都已通过 Rust 官方的代码检查工具 clippy (github.com/rust-lang-nursery/rust-clippy)进行过检查,使用版本 0.0.187。如果您愿意,可以使用 cargo +nightly install clippy 进行安装,并使用 cargo +nightly clippy 运行它。不过,最新版本经常会出现问题,所以如果它不能直接运行,请不要感到惊讶。
我们故意在代码中留下了某些 clippy 和 rustc 警告。其中大部分要么是死代码,这种情况发生在我们为了说明一个概念而给一个变量赋值,然后不再需要使用这个变量时;要么是使用占位符名称,如 foo、bar 或 baz,当变量的确切目的与配方无关时使用。
下载示例代码文件
您可以从www.packtpub.com的账户下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
在www.packtpub.com登录或注册。
-
选择 SUPPORT 标签。
-
点击代码下载与勘误表。
-
在搜索框中输入书籍名称,并遵循屏幕上的说明。
-
下载文件后,请确保您使用最新版本解压缩或提取文件夹:
-
Windows 版的 WinRAR/7-Zip
-
Mac 版的 Zipeg/iZip/UnRarX
-
Linux 版的 7-Zip/PeaZip
书籍的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Rust-Standard-Library-Cookbook/。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有其他来自我们丰富的书籍和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!
使用的约定
本书使用了多种文本约定。
CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“在bin文件夹中,创建一个名为dynamic_json.rs的文件。”
代码块设置如下:
let s = "Hello".to_string();
println!("s: {}", s);
let s = String::from("Hello");
println!("s: {}", s);
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
let alphabet: Vec<_> = (b'A' .. b'z' + 1) // Start as u8
.map(|c| c as char) // Convert all to chars
.filter(|c| c.is_alphabetic()) // Filter only alphabetic chars
.collect(); // Collect as Vec<char>
任何命令行输入或输出应如下编写:
name abraham
age 49
fav_colour red
hello world
(press 'Ctrl Z' on Windows or 'Ctrl D' on Unix)
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。
警告或重要注意事项如下所示。
小贴士和技巧如下所示。
部分
在本书中,您将找到一些频繁出现的标题(准备工作、如何做…、它是如何工作的…、更多内容…和参见)。
为了清楚地说明如何完成食谱,请按照以下方式使用这些部分:
准备工作
本节将告诉您在食谱中可以期待什么,并描述如何设置任何软件或食谱所需的任何初步设置。
如何做…
本节包含遵循食谱所需的步骤。
它是如何工作的…
本节通常包含对上一节发生情况的详细解释。
更多内容…
本节包含有关食谱的附加信息,以便您对食谱有更多的了解。
参见
本节提供了对食谱其他有用信息的链接。
联系我们
我们读者的反馈总是受欢迎的。
一般反馈:请发送电子邮件至feedback@packtpub.com,并在邮件主题中提及书籍标题。如果您对本书的任何方面有疑问,请通过questions@packtpub.com给我们发送电子邮件。
勘误表:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在此书中发现错误,我们将不胜感激,如果您能向我们报告此错误。请访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误表提交表单链接,并输入详细信息。
盗版:如果您在互联网上遇到任何形式的我们作品的非法副本,我们将不胜感激,如果您能向我们提供位置地址或网站名称。请通过 copyright@packtpub.com 联系我们,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com。
评论
请留下评论。一旦您阅读并使用过这本书,为何不在您购买书籍的网站上留下评论?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
如需了解更多关于 Packt 的信息,请访问 packtpub.com。
免责声明
在明确声明的情况下,本书包含 Rust 编程语言([https://doc.rust-lang.org/stable/book/](https://doc.rust-lang.org/stable/book/)),第一版和第二版中的代码和摘录,这些代码和摘录均在以下条款下以 MIT 许可证分发:
"版权所有 © 2010 Rust 项目开发者 允许任何获得此软件及其相关文档文件(“软件”)副本的人(“用户”)免费使用该软件,不受限制地处理该软件,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售软件副本,并允许向用户提供软件的人这样做,前提是遵守以下条件:上述版权声明和本许可声明应包含在软件的所有副本或主要部分中。软件按“原样”提供,不提供任何形式的保证,无论是明示的还是暗示的,包括但不限于适销性、特定用途的适用性和非侵权性。在任何情况下,作者或版权所有者均不对任何索赔、损害或其他责任负责,无论该责任是基于合同、侵权或其他原因,无论该责任是否源于、源于或与软件或其使用或其他方式有关。"
第一章:学习基础知识
在本章中,我们将介绍以下食谱:
-
字符串连接
-
使用 format! 宏
-
提供默认实现
-
使用构造器模式
-
使用构建器模式
-
通过简单线程实现并行
-
生成随机数
-
使用正则表达式查询
-
访问命令行
-
与环境变量交互
-
从 stdin 读取
-
接受可变数量的参数
简介
有些代码片段和思维模式一次又一次地证明是某种编程语言的基础。我们将从查看 Rust 中的一些此类技术开始这本书。它们对于编写优雅和灵活的代码至关重要,你几乎会在你处理的任何项目中使用其中的一些。
接下来的章节将在此基础上构建,并与 Rust 的零成本抽象协同工作,这些抽象与高级语言中的抽象一样强大。我们还将探讨标准库的复杂内部方面,并在无畏并发和谨慎使用 unsafe 块的帮助下实现我们自己的类似结构,这使我们能够以与某些系统语言(如 C 语言)相同的低级别工作。
字符串连接
在系统编程语言中,字符串操作通常比在脚本语言中要复杂一些,Rust 也不例外。有多种方法可以实现,所有这些方法都以不同的方式管理涉及的资源。
准备工作
我们将假设在本书的其余部分,你已打开编辑器,准备就绪的最新 Rust 编译器,以及可用的命令行。截至写作时,最新版本是 1.24.1。由于 Rust 对向后兼容性的强大保证,你可以放心,所有展示的食谱(除第十章 使用实验性夜间功能外)都将始终以相同的方式工作。你可以通过 www.rustup.rs 下载最新的编译器和其命令行工具。
如何做到...
-
使用
cargo new chapter-one创建一个 Rust 项目以在本章中工作 -
导航到新创建的
chapter-one文件夹。在本章的其余部分,我们将假设你的命令行当前位于此目录下 -
在
src文件夹内,创建一个名为bin的新文件夹 -
删除生成的
lib.rs文件,因为我们没有创建库 -
在
src/bin文件夹中,创建一个名为concat.rs的文件 -
添加以下代码,并使用
cargo run --bin concat运行它:
1 fn main() {
2 by_moving();
3 by_cloning();
4 by_mutating();
5 }
6
7 fn by_moving() {
8 let hello = "hello ".to_string();
9 let world = "world!";
10
11 // Moving hello into a new variable
12 let hello_world = hello + world;
13 // Hello CANNOT be used anymore
14 println!("{}", hello_world); // Prints "hello world!"
15 }
16
17 fn by_cloning() {
18 let hello = "hello ".to_string();
19 let world = "world!";
20
21 // Creating a copy of hello and moving it into a new variable
22 let hello_world = hello.clone() + world;
23 // Hello can still be used
24 println!("{}", hello_world); // Prints "hello world!"
25 }
26
27 fn by_mutating() {
28 let mut hello = "hello ".to_string();
29 let world = "world!";
30
31 // hello gets modified in place
32 hello.push_str(world);
33 // hello is both usable and modifiable
34 println!("{}", hello); // Prints "hello world!"
35 }
它是如何工作的...
在所有函数中,我们首先为可变长度的字符串分配内存。
我们通过创建一个字符串切片 (&str) 并对其应用 to_string 函数来实现这一点 [8, 18 和 28]。
Rust 中连接字符串的第一种方法,如 by_moving 函数所示,是通过将分配的内存以及一个额外的字符串切片移动到一个新的变量 [12] 中。这有几个优点:
-
它非常直接且清晰,因为它遵循常见的编程约定,即使用
+操作符进行连接 -
它只使用不可变数据。请记住,始终尝试编写尽可能少的状态行为代码,因为它会导致更健壮和可重用的代码库
-
它重用了
hello分配的内存 [8],这使得它非常高效
因此,在可能的情况下,应该优先选择这种方式进行连接。
那么,为什么我们还要列出其他连接字符串的方法呢?好吧,我很高兴你问了,亲爱的读者。虽然这种方法很优雅,但它有两个缺点:
-
在第 [12] 行之后,
hello就不再可用,因为它已经被移动。这意味着你不能再以任何方式读取它 -
有时候,你可能实际上更喜欢可变数据,以便在小型、封闭的环境中使用状态
剩下的两个函数分别解决一个关注点。
by_cloning[17] 几乎与第一个函数相同,但它将分配的字符串 [22] 克隆到一个临时对象中,在这个过程中分配新的内存,然后将其移动,这样原始的 hello 就不会被修改,仍然可访问。当然,这会带来运行时冗余内存分配的代价。
by_mutating[27] 是解决我们问题的有状态方法。它就地执行涉及的内存管理,这意味着性能应该与 by_moving 相同。最后,它将 hello 设置为可变,以便进行进一步更改。你可能注意到这个函数看起来不像其他函数那么优雅,因为它没有使用 + 操作符。这是故意的,因为 Rust 尝试通过其设计推动你通过移动数据来创建新变量,而不是修改现有变量。如前所述,你只有在真正需要可变数据或想在非常小且可管理的环境中引入状态时才应该这样做。
使用格式化宏
还有另一种组合字符串的方法,也可以用来与其他数据类型(如数字)组合。
如何操作...
-
在
src/bin文件夹中,创建一个名为format.rs的文件 -
添加以下代码,并使用
cargo run --bin format运行它
1 fn main() {
2 let colour = "red";
3 // The '{}' it the formatted string gets replaced by the
parameter
4 let favourite = format!("My favourite colour is {}", colour);
5 println!("{}", favourite);
6
7 // You can add multiple parameters, which will be
8 // put in place one after another
9 let hello = "hello ";
10 let world = "world!";
11 let hello_world = format!("{}{}", hello, world);
12 println!("{}", hello_world); // Prints "hello world!"
13
14 // format! can concatenate any data types that
15 // implement the 'Display' trait, such as numbers
16 let favourite_num = format!("My favourite number is {}", 42);
17 println!("{}", favourite_num); // Prints "My favourite number
is 42"
18
19 // If you want to include certain parameters multiple times
20 // into the string, you can use positional parameters
21 let duck_duck_goose = format!("{0}, {0}, {0}, {1}!", "duck",
"goose");
22 println!("{}", duck_duck_goose); // Prints "duck, duck, duck,
goose!"
23
24 // You can even name your parameters!
25 let introduction = format!(
26 "My name is {surname}, {forename} {surname}",
27 surname="Bond",
28 forename="James"
29 );
30 println!("{}", introduction) // Prints "My name is Bond, James
Bond"
31 }
它是如何工作的...
format! 宏通过接受一个填充有格式化参数(例如,{}、{0} 或 {foo})的格式字符串和一组参数来组合字符串,然后这些参数被插入到占位符中。
我们现在将在第 [16] 行的示例中展示这一点:
format!("My favourite number is {}", 42);
让我们分解一下前面的代码行:
-
"My favourite number is {}"是格式字符串 -
{}是格式化参数 -
42是参数
如所示,format!不仅与字符串一起工作,还与数字一起工作。实际上,它与实现Display特质的任何struct一起工作。这意味着,通过你自己提供这样的实现,你可以轻松地使你的数据结构以任何你想要的方式可打印。
默认情况下,format!会依次替换一个参数。如果你想覆盖这种行为,你可以使用位置参数,如{0}[21]。了解位置是从零开始的,这里的操作非常直接,{0}被第一个参数替换,{1}被第二个参数替换,以此类推。
有时,当使用大量参数时,这可能会变得有些难以控制。为此,你可以使用命名参数[26],就像在 Python 中一样。记住,你所有的未命名参数都必须放在你的命名参数之前。例如,以下是不合法的:
format!("{message} {}", message="Hello there,", "friendo")
应该重写为:
format!("{message} {}", "friendo", message="Hello there,")
// Returns "hello there, friendo"
还有更多...
你可以将位置参数与普通参数结合使用,但这可能不是一个好主意,因为它很容易变得难以理解。在这种情况下,行为如下——想象一下format!在内部使用一个计数器来确定下一个要放置的参数。每当format!遇到一个没有位置的{}时,这个计数器就会增加。这个规则导致以下结果:
format!("{1} {} {0} {}", "a", "b") // Returns "b a a b"
如果你想要以不同的格式显示你的数据,还有很多额外的格式化选项。{:?}打印出相应参数的Debug特质的实现,通常会产生更冗长的输出。{:.*}允许你通过参数指定浮点数的十进制精度,如下所示:
format!("{:.*}", 2, 1.234567) // Returns "1.23"
要获取完整列表,请访问doc.rust-lang.org/std/fmt/。
这个配方中的所有信息都适用于println!和print!,因为它们本质上是一个宏。唯一的区别是println!不返回其处理后的字符串,而是直接打印出来!
提供默认实现
通常,当处理表示配置的结构时,你不需要关心某些值,只想默默地给它们分配一个标准值。
如何做...
-
在
src/bin文件夹中,创建一个名为default.rs的文件 -
添加以下代码,并用
cargo run --bin default运行它:
1 fn main() {
2 // There's a default value for nearly every primitive type
3 let foo: i32 = Default::default();
4 println!("foo: {}", foo); // Prints "foo: 0"
5
6
7 // A struct that derives from Default can be initialized like
this
8 let pizza: PizzaConfig = Default::default();
9 // Prints "wants_cheese: false
10 println!("wants_cheese: {}", pizza.wants_cheese);
11
12 // Prints "number_of_olives: 0"
13 println!("number_of_olives: {}", pizza.number_of_olives);
14
15 // Prints "special_message: "
16 println!("special message: {}", pizza.special_message);
17
18 let crust_type = match pizza.crust_type {
19 CrustType::Thin => "Nice and thin",
20 CrustType::Thick => "Extra thick and extra filling",
21 };
22 // Prints "crust_type: Nice and thin"
23 println!("crust_type: {}", crust_type);
24
25
26 // You can also configure only certain values
27 let custom_pizza = PizzaConfig {
28 number_of_olives: 12,
29 ..Default::default()
30 };
31
32 // You can define as many values as you want
33 let deluxe_custom_pizza = PizzaConfig {
34 number_of_olives: 12,
35 wants_cheese: true,
36 special_message: "Will you marry me?".to_string(),
37 ..Default::default()
38 };
39
40 }
41
42 #[derive(Default)]
43 struct PizzaConfig {
44 wants_cheese: bool,
45 number_of_olives: i32,
46 special_message: String,
47 crust_type: CrustType,
48 }
49
50 // You can implement default easily for your own types
51 enum CrustType {
52 Thin,
53 Thick,
54 }
55 impl Default for CrustType {
56 fn default() -> CrustType {
57 CrustType::Thin
58 }
59 }
它是如何工作的...
几乎 Rust 中的每个类型都有一个Default实现。当你定义自己的struct,且它只包含已经具有Default的元素时,你可以选择从Default派生[42]。在枚举或复杂结构体的情况下,你可以轻松地编写自己的Default实现[55],因为只需提供一个方法。之后,Default::default()返回的struct会隐式推断为你的类型,前提是你告诉编译器你的类型实际是什么。这就是为什么在行[3]中我们必须写foo: i32,否则 Rust 不知道默认对象实际上应该变成什么类型。
如果你只想指定一些元素并让其他元素保持默认值,你可以使用行[29]中的语法。记住,你可以配置和跳过你想要的任意多个值,如行[33 到 37]所示。
使用构造函数模式
你可能已经问过自己如何在 Rust 中惯用方式初始化复杂结构体,考虑到它没有构造函数。答案是简单的,有一个构造函数,它只是一种约定而不是规则。Rust 的标准库经常使用这种模式,所以如果我们想有效地使用 std,我们需要理解它。
准备工作
在这个菜谱中,我们将讨论用户如何与struct交互。当我们在这个上下文中说“用户”时,我们不是指点击你编写应用程序 GUI 的最终用户。我们指的是实例化和操作struct的程序员。
如何做到这一点...
-
在
src/bin文件夹中,创建一个名为constructor.rs的文件 -
添加以下代码,并用
cargo run --bin constructor运行它:
1 fn main() {
2 // We don't need to care about
3 // the internal structure of NameLength
4 // Instead, we can just call it's constructor
5 let name_length = NameLength::new("John");
6
7 // Prints "The name 'John' is '4' characters long"
8 name_length.print();
9 }
10
11 struct NameLength {
12 name: String,
13 length: usize,
14 }
15
16 impl NameLength {
17 // The user doesn't need to setup length
18 // We do it for him!
19 fn new(name: &str) -> Self {
20 NameLength {
21 length: name.len(),
22 name,
23 }
24 }
25
26 fn print(&self) {
27 println!(
28 "The name '{}' is '{}' characters long",
29 self.name,
30 self.length
31 );
32 }
33 }
它是如何工作的...
如果一个struct提供了一个返回Self的new方法,那么struct的使用者将不会配置或依赖于struct的成员,因为它们被认为是处于内部隐藏状态。
换句话说,如果你看到一个具有new函数的struct,总是使用它来创建结构体。
这有一个很好的效果,就是允许你更改结构体的任意多个成员,而用户不会注意到任何变化,因为他们本来就不应该查看它们。
使用这种模式的另一个原因是引导用户以正确的方式实例化struct。如果有一个只有一大堆成员需要填充值的列表,用户可能会感到有些迷茫。然而,如果有一个只有几个自文档化参数的方法,感觉会更有吸引力。
还有更多...
你可能已经注意到,对于我们的示例,我们实际上并不需要一个length成员,我们可以在打印时随时计算长度。我们仍然使用这种模式,以说明它在隐藏实现方面的有用性。它的另一个很好的用途是,当struct的成员本身有自己的构造函数,并且需要级联构造函数调用时。例如,当我们有一个Vec作为成员时,就像我们将在本书后面的使用向量部分中看到的那样,在第二章,处理集合部分。
有时,你的结构体可能需要多种初始化方式。当这种情况发生时,请尝试仍然提供一个new()方法作为你的默认构造方式,并将其他选项根据它们与默认方式的差异进行命名。一个很好的例子又是向量,它不仅提供了一个Vec::new()构造函数,还提供了一个Vec::with_capacity(10),它为10个元素初始化了足够的空间。更多关于这一点的内容,请参阅第二章,处理集合部分。
当接受一种字符串类型(无论是&str,即借用字符串切片,还是String,即拥有字符串)并计划将其存储在你的struct中,就像我们在示例中所做的那样,同时考虑一个Cow。不,不是那些大牛奶动物朋友。在 Rust 中,Cow是一个围绕类型的写时复制包装器,这意味着它将尽可能尝试借用类型,只有在绝对必要时才会创建数据的拥有副本,这发生在第一次变异时。这种实际效果是,如果我们以以下方式重写我们的NameLength结构体,它将不会关心调用者传递给它的是&str还是String,而会尝试以最有效的方式工作:
use std::borrow::Cow;
struct NameLength<'a> {
name: Cow<'a, str>,
length: usize,
}
impl<'a> NameLength<'a> {
// The user doesn't need to setup length
// We do it for him!
fn new<S>(name: S) -> Self
where
S: Into<Cow<'a, str>>,
{
let name: Cow<'a, str> = name.into();
NameLength {
length: name.len(),
name,
}
}
fn print(&self) {
println!(
"The name '{}' is '{}' characters long",
self.name, self.length
);
}
}
如果你想了解更多关于Cow的信息,请查看 Joe Wilm 的这篇易于理解的博客文章:jwilm.io/blog/from-str-to-cow/。
在Cow代码中使用的Into特质将在第五章,高级数据结构部分的将类型转换为彼此部分中进行解释。
参见
-
使用向量配方在第二章,处理集合
-
将类型转换为彼此配方在第五章,高级数据结构
使用构建器模式
有时你需要介于构造函数的定制和默认实现的隐式性之间的某种东西。构建器模式就出现了,这是 Rust 标准库中经常使用的一种技术,因为它允许调用者流畅地连接他们关心的配置,并让他们忽略他们不关心的细节。
如何实现...
-
在
src/bin文件夹中,创建一个名为builder.rs的文件 -
添加以下所有代码并使用
cargo run --bin builder运行它:
1 fn main() {
2 // We can easily create different configurations
3 let normal_burger = BurgerBuilder::new().build();
4 let cheese_burger = BurgerBuilder::new()
.cheese(true)
.salad(false)
.build();
5 let veggie_bigmac = BurgerBuilder::new()
.vegetarian(true)
.patty_count(2)
.build();
6
7 if let Ok(normal_burger) = normal_burger {
8 normal_burger.print();
9 }
10 if let Ok(cheese_burger) = cheese_burger {
11 cheese_burger.print();
12 }
13 if let Ok(veggie_bigmac) = veggie_bigmac {
14 veggie_bigmac.print();
15 }
16
17 // Our builder can perform a check for
18 // invalid configurations
19 let invalid_burger = BurgerBuilder::new()
.vegetarian(true)
.bacon(true)
.build();
20 if let Err(error) = invalid_burger {
21 println!("Failed to print burger: {}", error);
22 }
23
24 // If we omit the last step, we can reuse our builder
25 let cheese_burger_builder = BurgerBuilder::new().cheese(true);
26 for i in 1..10 {
27 let cheese_burger = cheese_burger_builder.build();
28 if let Ok(cheese_burger) = cheese_burger {
29 println!("cheese burger number {} is ready!", i);
30 cheese_burger.print();
31 }
32 }
33 }
这是可配置的对象:
35 struct Burger {
36 patty_count: i32,
37 vegetarian: bool,
38 cheese: bool,
39 bacon: bool,
40 salad: bool,
41 }
42 impl Burger {
43 // This method is just here for illustrative purposes
44 fn print(&self) {
45 let pretty_patties = if self.patty_count == 1 {
46 "patty"
47 } else {
48 "patties"
49 };
50 let pretty_bool = |val| if val { "" } else { "no " };
51 let pretty_vegetarian = if self.vegetarian { "vegetarian "
}
else { "" };
52 println!(
53 "This is a {}burger with {} {}, {}cheese, {}bacon and
{}salad",
54 pretty_vegetarian,
55 self.patty_count,
56 pretty_patties,
57 pretty_bool(self.cheese),
58 pretty_bool(self.bacon),
59 pretty_bool(self.salad)
60 )
61 }
62 }
这就是构建器本身。它用于配置和创建一个 Burger:
64 struct BurgerBuilder {
65 patty_count: i32,
66 vegetarian: bool,
67 cheese: bool,
68 bacon: bool,
69 salad: bool,
70 }
71 impl BurgerBuilder {
72 // in the constructor, we can specify
73 // the standard values
74 fn new() -> Self {
75 BurgerBuilder {
76 patty_count: 1,
77 vegetarian: false,
78 cheese: false,
79 bacon: false,
80 salad: true,
81 }
82 }
83
84 // Now we have to define a method for every
85 // configurable value
86 fn patty_count(mut self, val: i32) -> Self {
87 self.patty_count = val;
88 self
89 }
90
91 fn vegetarian(mut self, val: bool) -> Self {
92 self.vegetarian = val;
93 self
94 }
95 fn cheese(mut self, val: bool) -> Self {
96 self.cheese = val;
97 self
98 }
99 fn bacon(mut self, val: bool) -> Self {
100 self.bacon = val;
101 self
102 }
103 fn salad(mut self, val: bool) -> Self {
104 self.salad = val;
105 self
106 }
107
108 // The final method actually constructs our object
109 fn build(&self) -> Result<Burger, String> {
110 let burger = Burger {
111 patty_count: self.patty_count,
112 vegetarian: self.vegetarian,
113 cheese: self.cheese,
114 bacon: self.bacon,
115 salad: self.salad,
116 };
117 // Check for invalid configuration
118 if burger.vegetarian && burger.bacon {
119 Err("Sorry, but we don't server vegetarian bacon
yet".to_string())
120 } else {
121 Ok(burger)
122 }
123 }
124 }
它是如何工作的...
哇,这是一大堆代码!让我们先把它拆分开来。
在第一部分,我们展示了如何使用这种模式轻松地配置一个复杂的对象。我们通过依赖合理的标准值,只指定我们真正关心的内容来实现这一点:
let normal_burger = BurgerBuilder::new().build();
let cheese_burger = BurgerBuilder::new()
.cheese(true)
.salad(false)
.build();
let veggie_bigmac = BurgerBuilder::new()
.vegetarian(true)
.patty_count(2)
.build();
代码读起来相当清晰,不是吗?
在我们的构建器模式版本中,我们返回一个包裹在 Result 中的对象,以便告诉世界存在某些无效配置,并且我们的构建器可能无法始终生成有效的产品。正因为如此,我们必须在访问它之前检查汉堡的有效性[7, 10 和 13]。
我们的无效配置是 vegetarian(true) 和 bacon(true)。不幸的是,我们的餐厅还没有提供素食培根!当你启动程序时,你会看到以下行会打印出一个错误:
if let Err(error) = invalid_burger {
println!("Failed to print burger: {}", error);
}
如果我们省略最后的 build 步骤,我们可以重复使用构建器来构建我们想要的任意数量的对象。[25 到 32]
让我们看看我们是如何实现所有这些的。在 main 函数之后的第一件事是我们 Burger 结构体的定义。这里没有惊喜,它只是普通的老数据。print 方法只是在这里提供一些在运行时的一些漂亮的输出。如果你想的话可以忽略它。
真正的逻辑在 BurgerBuilder[64] 中。它应该为每个你想要配置的值有一个成员。由于我们想要配置汉堡的每个方面,我们将有与 Burger 完全相同的成员。在构造函数 [74] 中,我们可以指定一些默认值。然后我们为每个配置创建一个方法。最后,在 build() [109] 中,我们首先执行一些错误检查。如果配置是正确的,我们返回由所有成员组成的 Burger[121]。否则,我们返回一个错误[119]。
还有更多...
如果你希望你的对象可以在没有构建器的情况下构建,你也可以为 Burger 提供一个 Default 实现。BurgerBuilder::new() 然后可以简单地返回 Default::default()。
在 build() 中,如果你的配置本质上不能是无效的,你当然可以直接返回对象,而无需将其包裹在 Result 中。
通过简单线程实现并行
每年,随着处理器物理核心数量的不断增加,并行性和并发性变得越来越重要。在大多数语言中,编写并行代码是棘手的。非常棘手。但在 Rust 中不是这样,因为它从一开始就被设计为围绕 无惧并发 原则。
如何做到这一点...
-
在
src/bin文件夹中,创建一个名为parallelism.rs的文件 -
添加以下代码并使用
cargo run --bin parallelism运行它
1 use std::thread;
2
3 fn main() {
4 // Spawning a thread lets it execute a lambda
5 let child = thread::spawn(|| println!("Hello from a new
thread!"));
6 println!("Hello from the main thread!");
7 // Joining a child thread with the main thread means
8 // that the main thread waits until the child has
9 // finished it's work
10 child.join().expect("Failed to join the child thread");
11
12 let sum = parallel_sum(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
13 println!("The sum of the numbers 1 to 10 is {}", sum);
14 }
15
16 // We are going to write a function that
17 // sums the numbers in a slice in parallel
18 fn parallel_sum(range: &[i32]) -> i32 {
19 // We are going to use exactly 4 threads to sum the numbers
20 const NUM_THREADS: usize = 4;
21
22 // If we have less numbers than threads,
23 // there's no point in multithreading them
24 if range.len() < NUM_THREADS {
25 sum_bucket(range)
26 } else {
27 // We define "bucket" as the amount of numbers
28 // we sum in a single thread
29 let bucket_size = range.len() / NUM_THREADS;
30 let mut count = 0;
31 // This vector will keep track of our threads
32 let mut threads = Vec::new();
33 // We try to sum as much as possible in other threads
34 while count + bucket_size < range.len() {
35 let bucket = range[count..count +
bucket_size].to_vec();
36 let thread = thread::Builder::new()
37 .name("calculation".to_string())
38 .spawn(move || sum_bucket(&bucket))
39 .expect("Failed to create the thread");
40 threads.push(thread);
41
42 count += bucket_size
43 }
44 // We are going to sum the rest in the main thread
45 let mut sum = sum_bucket(&range[count..]);
46
47 // Time to add the results up
48 for thread in threads {
49 sum += thread.join().expect("Failed to join thread");
50 }
51 sum
52 }
53 }
54
55 // This is the function that will be executed in the threads
56 fn sum_bucket(range: &[i32]) -> i32 {
57 let mut sum = 0;
58 for num in range {
59 sum += *num;
60 }
61 sum
62 }
它是如何工作的...
你可以通过调用thread::spawn来创建一个新的线程,然后它将开始执行提供的 lambda 表达式。这将返回一个JoinHandle,你可以用它来,嗯,连接线程。连接一个线程意味着等待线程完成其工作。如果你不连接一个线程,你无法保证它实际上会完成。然而,当设置线程来执行永远不会完成的任务时,比如监听传入的连接,这可能是有效的。
请记住,你不能预知你的线程完成任何工作的顺序。在我们的例子中,无法预测是来自新线程的问候!还是来自主线程的问候!将被首先打印出来,尽管大多数时候可能是主线程,因为操作系统需要付出一些努力来创建一个新的线程。这就是为什么小型算法在不并行执行时可能会更快。有时,让操作系统创建和管理新线程的开销可能根本不值得。
如行[49]所示,连接一个线程将返回一个包含你的 lambda 返回值的Result。
线程也可以被赋予名称。根据你的操作系统,在崩溃的情况下,将显示负责线程的名称。在行[37]中,我们将我们的新求和线程命名为calculation。如果其中任何一个崩溃,我们就能快速识别问题。自己试试,在sum_bucket的开始处插入对panic!();的调用,以故意崩溃程序并运行它。如果你的操作系统支持命名线程,你现在将被告知你的线程calculation因为显式恐慌而崩溃。
parallel_sum是一个函数,它接受一个整数切片并在四个线程上并行地将它们相加。如果你在处理并行算法方面经验有限,这个函数一开始可能很难理解。我邀请你手动将其复制到你的文本编辑器中,并对其进行操作,以便掌握它。如果你仍然感到有些迷茫,不要担心,我们稍后会再次讨论并行性。
将算法调整为并行运行通常伴随着数据竞争的风险。数据竞争被定义为系统中的行为,其输出依赖于外部事件的随机时间。在我们的例子中,存在数据竞争意味着多个线程试图同时访问和修改一个资源。通常,程序员必须分析他们资源的用法并使用外部工具来捕捉所有的数据竞争。相比之下,Rust 的编译器足够智能,可以在编译时捕捉到数据竞争,并在发现一个时停止。这就是为什么我们不得不在行[35]中调用.to_vec()的原因:
let bucket = range[count..count + bucket_size].to_vec();
我们将在后面的配方中介绍向量(第二章 Chapter 2,与集合一起工作中的使用向量部分),所以如果你对此感兴趣,请随时跳转到第二章 Chapter 2,与集合一起工作,然后再回来。其本质是我们将数据复制到 bucket 中。如果我们在新线程中将引用传递给 sum_bucket,我们就会遇到问题,range 引用的内存只保证在 parallel_sum 内部存在,但我们的线程允许比父线程存活更久。这意味着理论上,如果我们没有在正确的时间 join 线程,sum_bucket 可能会不幸地太晚被调用,以至于 range 已经无效。
这样就会产生数据竞争,因为我们的函数的结果将取决于操作系统决定启动线程的不可控序列。
但不要只听我的话,自己试试。只需将上述行替换为 let bucket = &range[count..count + bucket_size]; 并尝试编译它。
还有更多...
如果你熟悉并行处理,你可能已经注意到我们这里的算法不是很优化。这是故意的,因为编写 parallel_sum 的优雅和高效方式需要使用我们尚未讨论的技术。我们将在第七章 Chapter 7,并行性和 Rayon中重新审视这个算法,并以专业的方式重写它。在第七章,我们还将学习如何使用锁并发修改资源。
参见
- 使用 RwLocks 并行访问资源,请参考第七章 Chapter 7 中的配方,并行性和 Rayon
生成随机数
如前言所述,Rust 核心团队有意将一些功能从标准库中移除,并将其放入自己的外部 crate 中。生成伪随机数就是这样一种功能。
如何实现...
-
打开之前为你生成的
Cargo.toml文件 -
在
[dependencies]下添加以下行:
rand = "0.3"
-
如果你想,你可以访问 rand 的 crates.io 页面(
crates.io/crates/rand)来检查最新版本,并使用那个版本。 -
在
bin文件夹中,创建一个名为rand.rs的文件 -
添加以下代码,并用
cargo run --bin rand运行它:
1 extern crate rand;
2
3 fn main() {
4 // random_num1 will be any integer between
5 // std::i32::MIN and std::i32::MAX
6 let random_num1 = rand::random::<i32>();
7 println!("random_num1: {}", random_num1);
8 let random_num2: i32 = rand::random();
9 println!("random_num2: {}", random_num2);
10 // The initialization of random_num1 and random_num2
11 // is equivalent.
12
13 // Every primitive data type can be randomized
14 let random_char = rand::random::<char>();
15 // Altough random_char will probably not be
16 // representable on most operating systems
17 println!("random_char: {}", random_char);
18
19
20 use rand::Rng;
21 // We can use a reusable generator
22 let mut rng = rand::thread_rng();
23 // This is equivalent to rand::random()
24 if rng.gen() {
25 println!("This message has a 50-50 chance of being
printed");
26 }
27 // A generator enables us to use ranges
28 // random_num3 will be between 0 and 9
29 let random_num3 = rng.gen_range(0, 10);
30 println!("random_num3: {}", random_num3);
31
32 // random_float will be between 0.0 and 0.999999999999...
33 let random_float = rng.gen_range(0.0, 1.0);
34 println!("random_float: {}", random_float);
35
36 // Per default, the generator uses a uniform distribution,
37 // which should be good enough for nearly all of your
38 // use cases. If you require a particular distribution,
39 // you specify it when creating the generator:
40 let mut chacha_rng = rand::ChaChaRng::new_unseeded();
41 let random_chacha_num = chacha_rng.gen::<i32>();
42 println!("random_chacha_num: {}", random_chacha_num);
43 }
它是如何工作的...
在你能够使用 rand 之前,你必须告诉 Rust 你正在使用 crate,方法是编写:
extern crate rand;
之后,rand 将提供随机数生成器。我们可以通过调用 rand::random(); [6] 或直接使用 rand::thread_rng(); [22] 来访问它。
如果我们选择第一条路线,生成器需要被告知要生成哪种类型。你可以在方法调用中明确指定类型[6]或者注释结果的变量类型[8]。两者都是等效的,并且会产生完全相同的结果。你使用哪一种取决于你。在这本书中,我们将使用第一种约定。
如你在行[29 和 33]中看到的,如果你在调用上下文中类型是明确的,你不需要if。
生成的值将在其类型的MIN和MAX常量之间。对于i32来说,这将是从std::i32::MIN和std::i32::MAX,或者具体数字是-2147483648 和 2147483647。你可以通过调用以下内容来轻松验证这些数字:
println!("min: {}, max: {}", std::i32::MIN, std::i32::MAX);
如你所见,这些数字非常大。对于大多数用途,你可能需要定义自定义限制。你可以走之前讨论的第二条路线,并使用rand::Rng来实现[22]。它有一个gen方法,实际上这个方法被rand::random()隐式调用,还有一个gen_range()方法,它接受最小值和最大值。记住,这个范围是非包含的,这意味着最大值永远不会被达到。这就是为什么在行[29]中,rng.gen_range(0, 10)只会生成数字 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,而不包括 10。
所述的所有生成随机值的方法都使用均匀分布,这意味着范围内的每个数字都有相同的机会被生成。在某些上下文中,使用其他分布是有意义的。你可以在创建生成器时指定生成器的分布[40]。截至出版时,rand crate 支持 ChaCha 和 ISAAC 分布。
还有更多...
如果你想要随机填充整个struct,你可以使用rand_derive辅助 crate 来从Rand派生它。然后你可以生成自己的struct,就像生成任何其他类型一样。
使用正则表达式查询
在解析简单数据格式时,编写正则表达式(或简称regex)通常比使用解析器更容易。Rust 通过其regexcrate 提供了相当不错的支持。
准备工作
为了真正理解这一章,你应该熟悉正则表达式。网上有无数免费资源,比如 regexone (www.regexone.com/)。
这个配方不会符合 clippy,因为我们故意使正则表达式过于简单,因为我们想将配方的重点放在代码上,而不是正则表达式上。一些显示的示例可以被重写以使用.contains()。
如何做到这一点...
-
打开之前为你生成的
Cargo.toml文件。 -
在
[dependencies]下添加以下行:
regex = "0.2"
-
如果你愿意,你可以访问正则表达式的 crates.io 页面(
crates.io/crates/regex)来检查最新版本并使用它。 -
在
bin文件夹中,创建一个名为regex.rs的文件。 -
添加以下代码,并使用
cargo run --bin regex运行它:
1 extern crate regex;
2
3 fn main() {
4 use regex::Regex;
5 // Beginning a string with 'r' makes it a raw string,
6 // in which you don't need to escape any symbols
7 let date_regex =
Regex::new(r"^\d{2}.\d{2}.\d{4}$").expect("Failed
to create regex");
8 let date = "15.10.2017";
9 // Check for a match
10 let is_date = date_regex.is_match(date);
11 println!("Is '{}' a date? {}", date, is_date);
12
13 // Let's use capture groups now
14 let date_regex = Regex::new(r"(\d{2}).(\d{2})
.(\d{4})").expect("Failed to create regex");
15 let text_with_dates = "Alan Turing was born on 23.06.1912 and
died on 07.06.1954\. \
16 A movie about his life called 'The Imitation Game' came out
on 14.11.2017";
17 // Iterate over the matches
18 for cap in date_regex.captures_iter(text_with_dates) {
19 println!("Found date {}", &cap[0]);
20 println!("Year: {} Month: {} Day: {}", &cap[3], &cap[2],
&cap[1]);
21 }
22 // Replace the date format
23 println!("Original text:\t\t{}", text_with_dates);
24 let text_with_indian_dates =
date_regex.replace_all(text_with_dates, "$1-$2-$3");
25 println!("In indian format:\t{}", text_with_indian_dates);
26
27 // Replacing groups is easier when we name them
28 // ?P<somename> gives a capture group a name
29 let date_regex = Regex::new(r"(?P<day>\d{2}).(?P<month>\d{2})
.(?P<year>\d{4})")
30 .expect("Failed to create regex");
31 let text_with_american_dates =
date_regex.replace_all(text_with_dates,
"$month/$day/$year");
32 println!("In american format:\t{}",
text_with_american_dates);
33 let rust_regex = Regex::new(r"(?i)rust").expect("Failed to
create regex");
34 println!("Do we match RuSt? {}",
rust_regex.is_match("RuSt"));
35 use regex::RegexBuilder;
36 let rust_regex = RegexBuilder::new(r"rust")
37 .case_insensitive(true)
38 .build()
39 .expect("Failed to create regex");
40 println!("Do we still match RuSt? {}",
rust_regex.is_match("RuSt"));
41 }
它是如何工作的...
你可以通过调用Regex::new()并传递一个有效的正则表达式字符串来构建一个正则表达式对象[7]。大多数情况下,你将想要传递一个形式为r"..."的原始字符串。原始字符串意味着字符串中的所有符号都被当作字面值处理,而不需要转义。这很重要,因为正则表达式使用反斜杠(\)字符来表示一些重要概念,例如数字(\d)或空白(\s)。然而,Rust 已经使用反斜杠来转义特殊不可打印符号,例如换行符(\n)或制表符(\t)[23]。如果我们想在普通字符串中使用反斜杠,我们必须通过重复它来转义(\\)。或者第[14]行的正则表达式必须被重写为:
"(\\d{2}).(\\d{2}).(\\d{4})"
更糟糕的是,如果我们想匹配反斜杠本身,我们也必须因为正则表达式而转义它!在普通字符串中,我们必须四次转义它!(\\\\)
我们可以使用原始字符串来避免丢失可读性和混淆,并正常编写我们的正则表达式。实际上,在每个正则表达式中使用原始字符串被认为是一种良好的风格,即使它没有反斜杠[33]。这有助于你未来的自己,如果你发现你实际上确实想使用需要反斜杠的功能。
我们可以遍历我们的正则表达式的结果[18]。每次匹配时我们得到的对象是我们捕获组的集合。请注意,零索引始终是整个捕获[19]。第一个索引然后是我们第一个捕获组的字符串,第二个索引是第二个捕获组的字符串,依此类推。[20]。不幸的是,我们没有在索引上获得编译时检查,所以如果我们访问&cap[4],我们的程序会编译,但在运行时崩溃。
在替换时,我们遵循相同的概念:$0是整个匹配,$1是第一个捕获组的结果,依此类推。为了使我们的生活更轻松,我们可以通过从?P<somename>开始给捕获组命名,然后在替换时使用这个名称[29][31]。
你可以指定许多标志,形式为(?flag),用于微调,例如i,它使匹配不区分大小写[33],或者x,它在正则表达式中忽略空白。如果你想了解更多,请访问它们的文档(doc.rust-lang.org/regex/regex/index.html)。不过,大多数情况下,你可以通过使用正则表达式 crate 中的RegexBuilder来获得相同的结果[36]。我们在第[33]行和第[36]行生成的两个rust_regex对象是等效的。虽然第二个版本确实更冗长,但它一开始就更容易理解。
还有更多...
正则表达式通过在创建时将它们的字符串编译成等效的 Rust 代码来工作。出于性能考虑,建议你重用正则表达式,而不是每次使用时都重新创建它们。一个很好的方法是使用lazy_static包,我们将在本书的“创建懒静态对象”部分(第五章,[6b8b0c3c-2644-4684-b1f4-b1e08d62450c.xhtml],高级数据结构)中稍后讨论。
注意不要过度使用正则表达式。正如他们所说,“当你只有一把锤子时,一切看起来都像钉子。”如果你解析复杂的数据,正则表达式可以迅速变得难以置信地复杂。当你注意到你的正则表达式变得太大,以至于一眼看不过来时,尝试将其重写为解析器。
参见
- 创建懒静态对象配方在第五章,高级数据结构中
访问命令行
总有一天,你将想以某种方式与用户交互。最基本的方法是在通过命令行调用应用程序时让用户传递参数。
如何做到...
-
在
bin文件夹中,创建一个名为cli_params.rs的文件 -
添加以下代码,并用
cargo run --bin cli_params some_option some_other_option运行它:
1 use std::env;
2
3 fn main() {
4 // env::args returns an iterator over the parameters
5 println!("Got following parameters: ");
6 for arg in env::args() {
7 println!("- {}", arg);
8 }
9
10 // We can access specific parameters using the iterator API
11 let mut args = env::args();
12 if let Some(arg) = args.nth(0) {
13 println!("The path to this program is: {}", arg);
14 }
15 if let Some(arg) = args.nth(1) {
16 println!("The first parameter is: {}", arg);
17 }
18 if let Some(arg) = args.nth(2) {
19 println!("The second parameter is: {}", arg);
20 }
21
22 // Or as a vector
23 let args: Vec<_> = env::args().collect();
24 println!("The path to this program is: {}", args[0]);
25 if args.len() > 1 {
26 println!("The first parameter is: {}", args[1]);
27 }
28 if args.len() > 2 {
29 println!("The second parameter is: {}", args[2]);
30 }
31 }
它是如何工作的...
调用env::args()返回一个提供参数的迭代器[6]。按照惯例,大多数操作系统上的第一个命令行参数是可执行文件的路径[12]。
我们可以通过两种方式访问特定的参数:将它们保持为迭代器[11]或collect到像Vec[23]这样的集合中。不用担心,我们将在第二章,与集合一起工作中详细讨论它们。现在,你只需要知道:
-
访问迭代器会强制你在编译时检查元素是否存在,例如,使用
if let绑定[12] -
访问向量时在运行时检查其有效性
这意味着我们可以在[25]和[28]中首先检查它们的有效性之前执行[26]和[29]行。试试看,在程序末尾添加&args[3];行并运行它。
我们无论如何都会检查长度,因为这被认为是一种良好的风格,以确保预期的参数已被提供。使用迭代器方式访问参数时,你不必担心忘记检查,因为它会强制你这么做。另一方面,通过使用向量,你可以在程序开始时检查一次参数,之后就不必再担心了。
还有更多...
如果你正在以*nix 工具的风格构建一个严肃的命令行工具,你将不得不解析很多不同的参数。与其重新发明轮子,你应该看看第三方库,例如 clap (crates.io/crates/clap)。
与环境变量交互
根据《十二要素应用》(12factor.net/),你应该将配置存储在环境中(12factor.net/config)。这意味着你应该将可能在部署之间改变值的值,如端口、域名或数据库句柄,作为环境变量传递。许多程序也使用环境变量来相互通信。
如何做...
-
在
bin文件夹中,创建一个名为env_vars.rs的文件 -
添加以下代码,并使用
cargo run --bin env_vars运行它:
1 use std::env;
2
3 fn main() {
4 // We can iterate over all the env vars for the current
process
5 println!("Listing all env vars:");
6 for (key, val) in env::vars() {
7 println!("{}: {}", key, val);
8 }
9
10 let key = "PORT";
11 println!("Setting env var {}", key);
12 // Setting an env var for the current process
13 env::set_var(key, "8080");
14
15 print_env_var(key);
16
17 // Removing an env var for the current process
18 println!("Removing env var {}", key);
19 env::remove_var(key);
20
21 print_env_var(key);
22 }
23
24 fn print_env_var(key: &str) {
25 // Accessing an env var
26 match env::var(key) {
27 Ok(val) => println!("{}: {}", key, val),
28 Err(e) => println!("Couldn't print env var {}: {}", key, e),
29 }
30 }
它是如何工作的...
使用 env::vars(),我们可以访问在执行时为当前进程设置的 env var 的迭代器 [6]。这个列表相当庞大,正如你运行代码时将看到的,大部分对我们来说都是不相关的。
使用 env::var() [26] 访问单个 env var 更为实用,它如果请求的变量不存在或包含无效的 Unicode,则返回一个 Err。我们可以在第 [21] 行看到这一点,在那里我们尝试打印一个刚刚删除的变量。
因为你的 env::var 返回一个 Result,你可以通过使用 unwrap_or_default 来轻松地为它们设置默认值。一个涉及运行中的流行 Redis (redis.io/) 键值存储地址的现实生活例子如下:
redis_addr = env::var("REDIS_ADDR")
.unwrap_or_default("localhost:6379".to_string());
请记住,使用 env::set_var() [13] 创建 env var 和使用 env::remove_var() [19] 删除它都只改变我们当前进程的 env var。这意味着创建的 env var 不会被其他程序读取。这也意味着如果我们不小心删除了一个重要的 env var,操作系统的其他部分不会关心,因为它仍然可以访问它。
还有更多...
在这个菜谱的开始,我写了关于将你的配置存储在环境中的内容。做这件事的行业标准方式是创建一个名为 .env 的文件,其中包含键值对形式的配置,并在构建过程中某个时刻将其加载到进程中。在 Rust 中这样做的一个简单方法是通过使用 dotenv (crates.io/crates/dotenv) 第三方包。
从 stdin 读取
如果你想要创建一个交互式应用程序,使用命令行来原型化你的功能很容易。对于 CLI 程序,这将是所有需要的交互。
如何做...
-
在
src/bin文件夹中,创建一个名为stdin.rs的文件 -
添加以下代码,并使用
cargo run --bin stdin运行它:
1 use std::io;
2 use std::io::prelude::*;
3
4 fn main() {
5 print_single_line("Please enter your forename: ");
6 let forename = read_line_iter();
7
8 print_single_line("Please enter your surname: ");
9 let surname = read_line_buffer();
10
11 print_single_line("Please enter your age: ");
12 let age = read_number();
13
14 println!(
15 "Hello, {} year old human named {} {}!",
16 age, forename, surname
17 );
18 }
19
20 fn print_single_line(text: &str) {
21 // We can print lines without adding a newline
22 print!("{}", text);
23 // However, we need to flush stdout afterwards
24 // in order to guarantee that the data actually displays
25 io::stdout().flush().expect("Failed to flush stdout");
26 }
27
28 fn read_line_iter() -> String {
29 let stdin = io::stdin();
30 // Read one line of input iterator-style
31 let input = stdin.lock().lines().next();
32 input
33 .expect("No lines in buffer")
34 .expect("Failed to read line")
35 .trim()
36 .to_string()
37 }
38
39 fn read_line_buffer() -> String {
40 // Read one line of input buffer-style
41 let mut input = String::new();
42 io::stdin()
43 .read_line(&mut input)
44 .expect("Failed to read line");
45 input.trim().to_string()
46 }
47
48 fn read_number() -> i32 {
49 let stdin = io::stdin();
50 loop {
51 // Iterate over all lines that will be inputted
52 for line in stdin.lock().lines() {
53 let input = line.expect("Failed to read line");
54 // Try to convert a string into a number
55 match input.trim().parse::<i32>() {
56 Ok(num) => return num,
57 Err(e) => println!("Failed to read number: {}", e),
58 }
59 }
60 }
61 }
它是如何工作的...
为了从标准控制台输入stdin读取,我们首先需要获取它的句柄。我们通过调用io::stdin()[29]来完成这个操作。想象一下返回的对象是一个全局stdin对象的引用。这个全局缓冲区由一个Mutex管理,这意味着一次只有一个线程可以访问它(关于这一点,本书将在第七章的使用 Mutex 并行访问资源部分中进一步讨论,并行性和 Rayon)。我们通过锁定(使用lock())缓冲区来获取这个访问权限,这会返回一个新的句柄[31]。完成这个操作后,我们可以在它上面调用lines方法,该方法返回用户将写入的行的迭代器[31 和 52]。关于迭代器的更多信息,请参阅第二章的作为迭代器访问集合部分,处理集合。
最后,我们可以迭代提交的任意多行,直到达到某种中断条件,否则迭代将永远进行下去。在我们的例子中,一旦输入了有效数字,我们就中断数字检查循环[56]。
如果我们对输入不太挑剔,只想获取下一行,我们有两个选择:
-
我们可以继续使用
lines()提供的无限迭代器,但只需简单地调用它的next方法来获取第一个元素。这附带了一个额外的错误检查,因为一般来说,我们无法保证存在下一个元素。 -
我们可以使用
read_line来填充现有的缓冲区[43]。这不需要我们先lock处理程序,因为它被隐式地完成了。
虽然它们都产生了相同的效果,但你应该选择第一个选项。它更符合惯例,因为它使用迭代器而不是可变状态,这使得它更易于维护和阅读。
顺便提一下,我们在这个配方的一些地方使用print!而不是println!是出于美观原因[22]。如果你更喜欢在用户输入前显示换行符的外观,你可以避免使用它们。
还有更多...
这个配方是基于你希望使用 stdin 进行实时交互的cli编写的。如果你计划将一些数据通过管道输入(例如,cat foo.txt | stdin.rs在*nix 上),你可以停止将lines()返回的迭代器视为无限,并检索单独的行,这与你在上一个配方中检索单独参数的方式类似。
在我们的配方中有各种对trim()的调用[35, 45 和 55]。此方法删除前导和尾随空白,以提高我们程序的易用性。我们将在第二章的使用字符串部分中详细探讨它,处理集合。
参见
-
第一章中的与环境变量交互配方,学习基础知识
-
第二章中的使用字符串和将集合作为迭代器访问配方,与集合一起工作
-
第七章中的使用 Mutex 并行访问资源配方,并行性和 Rayon
接受可变数量的参数
大多数时候,当你想要对一个数据集进行操作时,你会设计一个接受集合的函数。然而,在某些情况下,拥有只接受未绑定数量参数的函数会更好,就像 JavaScript 的剩余参数。这个概念被称为可变参数函数,在 Rust 中不受支持。但是,我们可以通过定义递归宏来实现它。
入门
这个配方中的代码可能很小,但如果你不熟悉宏,它看起来可能像是乱码。如果你还没有学习关于宏的内容或者需要复习,我建议你快速查看官方 Rust 书籍中的相关章节(doc.rust-lang.org/stable/book/first-edition/macros.html)。
如何做...
-
在
src/bin文件夹中,创建一个名为variadic.rs的文件 -
添加以下代码,并使用
cargo run --bin variadic运行它:
1 macro_rules! multiply {
2 // Edge case
3 ( $last:expr ) => { $last };
4
5 ( $head:expr, $($tail:expr), +) => {
6 // Recursive call
7 $head * multiply!($($tail),+)
8 };
9 }
10
11 fn main() {
12 // You can call multiply! with
13 // as many parameters as you want
14 let val = multiply!(2, 4, 8);
15 println!("2*4*8 = {}", val)
16 }
它是如何工作的...
让我们从我们的意图开始:我们想要创建一个名为 multiply 的宏,它可以接受未定义数量的参数并将它们全部相乘。在宏中,这是通过递归实现的。我们每次递归定义都以边缘情况开始,即递归应该停止的参数。大多数时候,这是函数调用不再有意义的地方。在我们的例子中,这是一个单独的参数。想想看,multiply!(3)应该返回什么?由于我们没有其他参数与之相乘,所以将它与任何东西相乘都没有意义。我们最好的反应就是简单地返回未修改的参数。
我们的另一个条件是匹配多个参数,一个$head和一个用逗号分隔的参数列表,位于$tail内部。在这里,我们只定义返回值为$head与$tail乘积的结果。这将调用multiply!与$tail和没有$head,这意味着在每次调用中我们处理一个更少的参数,直到我们最终达到边缘情况,一个单独的参数。
还有更多...
请记住,你应该谨慎使用这种技术。大多数时候,直接接受并操作一个切片会更清晰。然而,与其他宏和更高层次的概念结合使用是有意义的,在这些情况下,“可触摸的事物列表”的类比就失效了。找到一个好的例子很难,因为它们往往非常具体。不过,你可以在书的末尾找到一个例子。
参见
- 第十章中的函数组合配方,使用实验性 Nightly 功能
第二章:与集合一起工作
在本章中,我们将介绍以下食谱:
-
使用向量
-
使用字符串
-
将集合作为迭代器访问
-
使用
VecDeque -
使用
HashMap -
使用
HashSet -
创建自己的迭代器
-
使用 slab
简介
Rust提供了一组非常广泛的集合供使用。我们将查看大多数集合,了解它们的使用方法,讨论它们的实现方式,以及何时使用和选择它们。本章的大部分内容都集中在迭代器上。Rust 的许多灵活性都来自于它们,因为所有集合(以及更多!)都可以用作迭代器。学习如何使用它们是至关重要的。
在本章中,我们将使用大 O 表示法来展示某些算法的有效性。如果您还不知道,这是一种描述算法在处理更多元素时所需时间增长的方式。让我们简要地看看它。
表示无论在集合中存储多少数据,算法所需的时间都将相同。它并没有告诉我们它有多快,只是它不会随着大小的增加而变慢。这是函数的现实理想。一个实际的例子是访问一个无限数字列表中的第一个数字:无论有多少数字,您总是能够立即挑选出第一个。
表示算法将按相同程度减慢每个元素。这并不好,但还可以接受。一个例子是在for循环中打印所有数据。
非常糟糕。它告诉我们算法将随着每个元素的加入而变得越来越慢。一个例子是在另一个for循环中访问相同数据的数据。
使用向量
最基本的集合是向量,或简称为Vec。它本质上是一个具有非常低开销的变长数组。因此,它是在您将使用的大部分时间中的集合。
如何做到这一点...
-
在命令行中,使用
cd ..向上跳一个文件夹,这样您就不再在chapter-one中。在接下来的章节中,我们将假设您总是从这一步开始。 -
使用
cargo new chapter-two创建一个 Rust 项目,在本章中对其进行工作。 -
导航到新创建的
chapter-two文件夹。在本章的其余部分,我们将假设您的命令行当前位于此目录。 -
在
src文件夹内,创建一个名为bin的新文件夹。 -
删除生成的
lib.rs文件,因为我们不是创建一个库。 -
在
src/bin文件夹中创建一个名为vector.rs的文件。 -
将以下代码块添加到文件中,并使用
cargo run --bin vector运行它们:
1 fn main() {
2 // Create a vector with some elements
3 let fruits = vec!["apple", "tomato", "pear"];
4 // A vector cannot be directly printed
5 // But we can debug-print it
6 println!("fruits: {:?}", fruits);
7
8 // Create an empty vector and fill it
9 let mut fruits = Vec::new();
10 fruits.push("apple");
11 fruits.push("tomato");
12 fruits.push("pear");
13 println!("fruits: {:?}", fruits);
14
15 // Remove the last element
16 let last = fruits.pop();
17 if let Some(last) = last {
18 println!("Removed {} from {:?}", last, fruits);
19 }
20
21 // Insert an element into the middle of the vector
22 fruits.insert(1, "grape");
23 println!("fruits after insertion: {:?}", fruits);
24
25 // Swap two elements
26 fruits.swap(0, 1);
27 println!("fruits after swap: {:?}", fruits);
- 这就是您如何访问向量中的单个元素:
29 // Access the first and last elements
30 let first = fruits.first();
31 if let Some(first) = first {
32 println!("First fruit: {}", first);
33 }
34 let last = fruits.last();
35 if let Some(last) = last {
36 println!("Last fruit: {}", last);
37 }
38
39 // Access arbitrary elements
40 let second = fruits.get(1);
41 if let Some(second) = second {
42 println!("Second fruit: {}", second);
43 }
44 // Access arbitrary elements without bonds checking
45 let second = fruits[1];
46 println!("Second fruit: {}", second);
- 下面的几个方法适用于整个向量:
50 // Initialize the vector with a value
51 // Here, we fill our vector with five zeroes
52 let bunch_of_zeroes = vec![0; 5];
53 println!("bunch_of_zeroes: {:?}", bunch_of_zeroes);
54
55 // Remove some item and shift all that come after
56 // into place
57 let mut nums = vec![1, 2, 3, 4];
58 let second_num = nums.remove(1);
59 println!("Removed {} from {:?}", second_num, nums);
60
61 // Filter the vector in place
62 let mut names = vec!["Aaron", "Felicia", "Alex", "Daniel"];
63 // Only keep names starting with 'A'
64 names.retain(|name| name.starts_with('A'));
65 println!("Names starting with A: {:?}", names);
66
67 // Check if the vector contains an element
68 println!("Does 'names' contain \"Alex\"? {}",
names.contains(&"Alex"));
69
70
71
72 // Remove consecutive(!) duplicates
73 let mut nums = vec![1, 2, 2, 3, 4, 4, 4, 5];
74 nums.dedup();
75 println!("Deduped, pre-sorted nums: {:?}", nums);
76
77 // Be careful if your data is not sorted!
78 let mut nums = vec![2, 1, 4, 2, 3, 5, 1, 2];
79 nums.dedup();
80 // Doens't print what you might expect
81 println!("Deduped, unsorted nums: {:?}", nums);
82
83 // Sort a vector
84 nums.sort();
85 println!("Manually sorted nums: {:?}", nums);
86 nums.dedup();
87 println!("Deduped, sorted nums: {:?}", nums);
88
89 // Reverse a vector
90 nums.reverse();
91 println!("nums after being reversed: {:?}", nums);
92
93 // Create a consuming iterator over a range
94 let mut alphabet = vec!['a', 'b', 'c'];
95 print!("The first two letters of the alphabet are: ");
96 for letter in alphabet.drain(..2) {
97 print!("{} ", letter);
98 }
99 println!();
100 // The drained elements are no longer in the vector
101 println!("alphabet after being drained: {:?}", alphabet);
102
103
104 // Check if a vector is empty
105 let mut fridge = vec!["Beer", "Leftovers", "Mayonaise"];
106 println!("Is the fridge empty {}", fridge.is_empty());
107 // Remove all elements
108 fridge.clear();
109 println!("Is the fridge now empty? {}", fridge.is_empty());
- 我们可以将一个向量分成两个,然后再将它们合并:
111 // Split a vector into two pieces
112 let mut colors = vec!["red", "green", "blue", "yellow"];
113 println!("colors before splitting: {:?}", colors);
114 let half = colors.len() / 2;
115 let mut second_half = colors.split_off(half);
116 println!("colors after splitting: {:?}", colors);
117 println!("second_half: {:?}", second_half);
118
119 // Put two vectors together
120 colors.append(&mut second_half);
121 println!("colors after appending: {:?}", colors);
122 // This empties the second vector
123 println!("second_half after appending: {:?}", second_half);
- 您可能还记得 JavaScript 中的
splice方法:
127 let mut stuff = vec!["1", "2", "3", "4", "5", "6"];
128 println!("Original stuff: {:?}", stuff);
129 let stuff_to_insert = vec!["a", "b", "c"];
130 let removed_stuff: Vec<_> = stuff.splice(1..4,
stuff_to_insert).collect();
131 println!("Spliced stuff: {:?}", stuff);
132 println!("Removed stuff: {:?}", removed_stuff);
- 如果您正在处理非常大的数据集,您可以优化您向量的性能:
136 // Initialize the vector with a certain capacity
137 let mut large_vec: Vec<i32> = Vec::with_capacity(1_000_000);
138 println!("large_vec after creation:");
139 println!("len:\t\t{}", large_vec.len());
140 println!("capacity:\t{}", large_vec.capacity());
141
142 // Shrink the vector as close as possible to its length
143 large_vec.shrink_to_fit();
144 println!("large_vec after shrinking:");
145 println!("len:\t\t{}", large_vec.len());
146 println!("capacity:\t{}", large_vec.capacity());
147
148 // Remove some item, replacing it with the last
149 let mut nums = vec![1, 2, 3, 4];
150 let second_num = nums.swap_remove(1);
151 // This changes the order, but works in O(1)
152 println!("Removed {} from {:?}", second_num, nums);
153 }
它是如何工作的...
这个菜谱将比其他菜谱长一些,因为:
-
向量是最重要的集合
-
许多其核心原则,如预分配,也适用于其他集合
-
它包括用于切片的方法,这些方法也可由许多其他集合使用
让我们从一开始。
向量可以通过我们之前提到的构造函数模式 [9] 创建,并通过对每个我们想要存储的元素调用push来填充 [10]。因为这是一个非常常见的模式,Rust 为您提供了一个方便的宏,称为vec![3]。虽然其最终效果相同,但该宏通过一些性能优化来实现。
由于vec!提供的便利性,其他 Rustacians 已经为其他集合实现了类似的宏,您可以在以下链接中找到:crates.io/crates/maplit.
如果您想要通过重复一个元素来初始化向量,您可以使用第 [52] 行中描述的特殊调用语法来这样做。
push的反面是pop:它移除向量的最后一个元素,如果向量在之前不为空,则返回它。由于Vec的内存布局,我们将在下一节中讨论,此操作以
复杂度完成。如果您不知道这意味着什么,让我重新表述一下:它非常快。这就是为什么向量可以很好地用作先进后出(FILO)栈。
如果您需要修改向量的内容,insert[22]、remove [58] 和 swap [26] 应该是显而易见的。有时,尽管如此,您可能想要访问向量中的特定元素。您可以使用get来借用索引 [40] 处的元素,并使用get_mut来修改它。两者都返回一个只包含Some元素的Option,如果索引是有效的。然而,大多数时候,这种精细的错误检查对于向量访问是不必要的,因为越界索引通常无法恢复,并且将仅通过解包Option来处理。因此,Rust 允许您在Vec上调用Index运算符,[]。这将自动推断其可变性并为您执行解包。
有许多方法可以帮助我们一次性处理整个向量。retain是一个非常实用的方法,也被大多数其他集合 [64] 实现。它接受一个所谓的谓词,这是一个返回true或false的函数。它将谓词应用于每个元素,并且只保留返回true的元素。
dedup移除所有连续的重复项 [74]。这意味着对于向量 [1, 2, 2, 3, 2, 3],dedup的结果将是 [1, 2, 3, 2, 3],因为只有重复的 2 是连续的。使用它时始终记住这一点,因为它可能会引起难以发现的错误。如果你想移除所有重复项,你需要通过首先对向量进行排序来使它们连续。如果你的元素是可比较的,这就像调用.sort() [84]一样简单。
使用drain创建一个消费迭代器,在访问所有元素的同时移除它们,使你的向量变为空 [96]。这在你需要处理你的数据并在之后再次使用空向量来收集更多工作时有用。
如果你从未在其他语言中见过splice,你一开始可能会对它做什么感到有些困惑。让我们来看看它,好吗?
splice做三件事:
-
它需要一个范围。这个范围将被从向量中移除。
-
它需要一个迭代器。这个迭代器将被插入到上一步移除后留下的空间中。
-
它返回移除的元素作为迭代器。
如何处理返回的迭代器将是访问集合作为迭代器部分中食谱的主题。
还有更多...
向量应该是你首选的集合。在内部,它被实现为存储在堆上的连续内存块:

重要的关键字是连续的,这意味着内存非常缓存友好。换句话说,向量相当快!向量甚至分配了一些额外的内存,以防你想扩展它。但是,当在向量的开头插入大量数据时要小心:整个堆栈将不得不移动。
最后,你可以看到一点额外容量。这是因为Vec和许多其他集合在每次需要移动块并且它变得太大时都会预先分配一些额外的内存。这是为了尽可能减少重新分配。你可以通过调用capacity [140]来检查向量的确切总空间量。你可以通过使用with_capacity [137]来影响预分配。当你大致知道你打算存储多少元素时使用它。当处理大量数据时,这可能会在容量上产生很大差异。
在缩短向量时,额外的容量并不会消失。如果你有一个长度为 10,000,容量为 100,000 的向量,并在其上调用clear,你仍然会有 100,000 预先分配的容量。当在内存受限的系统上工作时,如微控制器,这可能会成为一个问题。解决方案是定期在这些向量上调用shrink_to_fit [143]。这将使容量尽可能接近长度,但仍允许留下一小部分预先分配的空间。
另一种优化非常大的向量的方法是调用 swap_remove [150]。通常,当你从一个向量中移除一个元素时,它后面的所有元素都会向左移动以保持连续的内存。当在一个大向量中移除第一个元素时,这是一项大量工作。如果你不关心向量中元素的精确顺序,你可以调用 swap_remove 而不是 remove。它通过交换要移除的元素和最后一个元素,并调整长度来实现。这很好,因为你不需要通过移动来填充需要填充的“空洞”,并且因为交换内存是当今处理器中非常快速的操作。
使用字符串
Rust 为其字符串提供了异常大的功能。了解它可以在处理原始用户输入时节省你很多麻烦。
如何做到这一点...
-
在
src/bin文件夹中创建一个名为string.rs的文件。 -
添加以下代码,并使用
cargo run --bin string运行它:
1 fn main() {
2 // As a String is a kind of vector,
3 // you can construct them the same way
4 let mut s = String::new();
5 s.push('H');
6 s.push('i');
7 println!("s: {}", s);
8
9 // The String however can also be constructed
10 // from a string slice (&str)
11 // The next two ways of doing to are equivalent
12 let s = "Hello".to_string();
13 println!("s: {}", s);
14 let s = String::from("Hello");
15 println!("s: {}", s);
16
17 // A String in Rust will always be valid UTF-8
18 let s = " Þjóðhildur ".to_string();
19 println!("s: {}", s);
20
21 // Append strings to each other
22 let mut s = "Hello ".to_string();
23 s.push_str("World");
24
25 // Iterate over the character
26 // A "character" is defined here as a
27 // Unicode Scalar Value
28 for ch in "Tubular".chars() {
29 print!("{}.", ch);
30 }
31 println!();
32 // Be careful though, a "character" might not
33 // always be what you expect
34 for ch in "y̆".chars() {
35 // This does NOT print y̆
36 print!("{} ", ch);
37 }
38 println!();
使用以下代码以各种方式拆分字符串:
42 // Split a string slice into two halves
43 let (first, second) = "HelloThere".split_at(5);
44 println!("first: {}, second: {}", first, second);
45
46 // Split on individual lines
47 let haiku = "\
48 she watches\n\
49 satisfied after love\n\
50 he lies\n\
51 looking up at nothing\n\
52 ";
53 for line in haiku.lines() {
54 println!("\t{}.", line);
55 }
56
57 // Split on substrings
58 for s in "Never;Give;Up".split(';') {
59 println!("{}", s);
60 }
61 // When the splitted string is at the beginning or end,
62 // it will result in the empty string
63 let s: Vec<_> = "::Hi::There::".split("::").collect();
64 println!("{:?}", s);
65
66 // If you can eliminate the empty strings at the end
67 // by using split_termitor
68 let s: Vec<_> = "Mr. T.".split_terminator('.').collect();
69 println!("{:?}", s);
70
71 // char has a few method's that you can use to split on
72 for s in "I'm2fast4you".split(char::is_numeric) {
73 println!("{}", s);
74 }
75
76 // Split only a certain amount of times
77 for s in "It's not your fault, it's mine".splitn(3,
char::is_whitespace) {
78 println!("{}", s);
79 }
80
81 // Get only the substrings that match a pattern
82 // This is the opposite of splitting
83 for c in "The Dark Knight rises".matches(char::is_uppercase) {
84 println!("{}", c);
85 }
86
87 // Check if a string starts with something
88 let saying = "The early bird gets the worm";
89 let starts_with_the = saying.starts_with("The");
90 println!(
"Does \"{}\" start with \"The\"?: {}",
saying,
starts_with_the
);
91 let starts_with_bird = saying.starts_with("bird");
92 println!(
"Does \"{}\" start with \"bird\"?: {}",
saying,
starts_with_bird
);
93
94 // Check if a string ends with something
95 let ends_with_worm = saying.ends_with("worm");
96 println!("Does \"{}\" end with \"worm\"?: {}", saying,
ends_with_worm);
97
98 // Check if the string contains something somewhere
99 let contains_bird = saying.contains("bird");
100 println!("Does \"{}\" contain \"bird\"?: {}", saying,
contains_bird);
移除空白:
105 // Splitting on whitespace might not result in what you expect
106 let a_lot_of_whitespace = " I love spaaace ";
107 let s: Vec<_> = a_lot_of_whitespace.split(' ').collect();
108 println!("{:?}", s);
109 // Use split_whitespace instead
110 let s: Vec<_> =
a_lot_of_whitespace.split_whitespace().collect();
111 println!("{:?}", s);
112
113 // Remove leading and trailing whitespace
114 let username = " P3ngu1n\n".trim();
115 println!("{}", username);
116 // Remove only leading whitespace
117 let username = " P3ngu1n\n".trim_left();
118 println!("{}", username);
119 // Remove only trailing whitespace
120 let username = " P3ngu1n\n".trim_right();
121 println!("{}", username);
122
123
124 // Parse a string into another data type
125 // This requires type annotation
126 let num = "12".parse::<i32>();
127 if let Ok(num) = num {
128 println!("{} * {} = {}", num, num, num * num);
129 }
修改字符串:
133 // Replace all occurences of a pattern
134 let s = "My dad is the best dad";
135 let new_s = s.replace("dad", "mom");
136 println!("new_s: {}", new_s);
137
138 // Replace all characters with their lowercase
139 let lowercase = s.to_lowercase();
140 println!("lowercase: {}", lowercase);
141
142 // Replace all characters with their uppercase
143 let uppercase = s.to_uppercase();
144 println!("uppercase: {}", uppercase);
145
146 // These also work with other languages
147 let greek = "ὈΔΥΣΣΕΎΣ";
148 println!("lowercase greek: {}", greek.to_lowercase());
149
150 // Repeat a string
151 let hello = "Hello! ";
152 println!("Three times hello: {}", hello.repeat(3));
153 }
它是如何工作的...
实质上,作为一种向量,字符串可以通过组合 new 和 push 来以相同的方式创建;然而,因为这样做非常不方便,所以可以从字符串切片(&str)创建一个 string,它可以是借用字符串或字面量。这两种在本文档中展示的方法都是等效的:
let s = "Hello".to_string();
println!("s: {}", s);
let s = String::from("Hello");
println!("s: {}", s);
出于纯粹的个人偏好,我们将使用第一个变体。
在 Rust 1.9 之前,to_owned() 是创建字符串最快的方式。现在,to_string() 的性能同样出色,应该优先使用,因为它提供了对所做操作的更多清晰度。我们提到这一点是因为许多旧的教程和指南自那时起就没有更新,仍然使用 to_owned()。
Rust 中的所有字符串在 UTF-8 编码中都是有效的 Unicode。这可能会带来一些惊喜,因为我们所知的“字符”本质上是一种拉丁发明。例如,看看那些有字母修饰符的语言——ä 是一个独立的字符,还是 a 的变体?对于允许极端组合的语言呢?那样的键盘会是什么样子?因此,Unicode 允许你从不同的 Unicode 标量值中组合你的“字符”。使用 .chars(),你可以创建一个迭代器,遍历这些标量 [28]。如果你处理非拉丁字符,当你访问组合字符时可能会感到惊讶——y̆ 不是单个标量,而是两个标量,y 和 ̆ [36]。你可以通过使用支持遍历图形的 Unicode-segmentation crate 来解决这个问题:crates.io/crates/unicode-segmentation.
当在开始处、结束处或连续多次出现模式时分割字符串,每个实例都会分割成空字符串 ""[107]。当在空格(' ')上分割时,这尤其令人讨厌。在这种情况下,你应该使用 split_whitespace[110]。否则,split_terminator 将从字符串末尾删除空字符串[68]。
顺便说一句,当我们在这个菜谱中提到模式时,我们指的是以下三种情况之一:
-
一个字符
-
一个字符串
-
一个接受一个
char的谓词
还有更多...
String 的实现不应该令人惊讶——它只是一种向量:

将集合作为迭代器访问
欢迎来到 Rust 标准库中最灵活的部分之一。正如其名所示,迭代器是一种对集合中项目应用操作的方式。如果你来自 C#,你可能会因为 Linq 而对迭代器已经很熟悉了。Rust 的迭代器在某种程度上是相似的,但它们采用了一种更函数式的方法来处理事物。
由于它们是标准库中极其基础的部分,我们将把这个菜谱完全奉献给展示你可以独立使用它们的各种不同方式。对于实际用例,你可以简单地继续阅读这本书,因为其他大部分菜谱以某种方式或另一种方式使用迭代器。
如何做到这一点...
-
在
src/bin文件夹中,创建一个名为iterator.rs的文件。 -
添加以下代码,并使用
cargo run --bin iterator运行它:
1 fn main() {
2 let names = vec!["Joe", "Miranda", "Alice"];
3 // Iterators can be accessed in many ways.
4 // Nearly all collections implement .iter() for this purpose
5 let mut iter = names.iter();
6 // A string itself is not iterable, but its characters are
7 let mut alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".chars();
8 // Ranges are also (limited) iterators
9 let nums = 0..10;
10 // You can even create infinite iterators!
11 let all_nums = 0..;
12
13 // As the name says, you can iterate over iterators
14 // This will consume the iterator
15 for num in nums {
16 print!("{} ", num);
17 }
18 // nums is no longer usable
19 println!();
20
21 // Get the index of the current item
22 for (index, letter) in "abc".chars().enumerate() {
23 println!("#{}. letter in the alphabet: {}", index + 1,
letter);
24 }
- 访问单个项:
26 // going through an iterator, step by step
27 if let Some(name) = iter.next() {
28 println!("First name: {}", name);
29 }
30 if let Some(name) = iter.next() {
31 println!("Second name: {}", name);
32 }
33 if let Some(name) = iter.next() {
34 println!("Third name: {}", name);
35 }
36 if iter.next().is_none() {
37 println!("No names left");
38 }
39
40 // Arbitrary access to an item in the iterator
41 let letter = alphabet.nth(3);
42 if let Some(letter) = letter {
43 println!("the fourth letter in the alphabet is: {}",
letter);
44 }
45 // This works by consuming all items up to a point
46 let current_first = alphabet.nth(0);
47 if let Some(current_first) = current_first {
48 // This will NOT print 'A'
49 println!(
50 "The first item in the iterator is currently: {}",
51 current_first
52 );
53 }
54 let current_first = alphabet.nth(0);
55 if let Some(current_first) = current_first {
56 println!(
57 "The first item in the iterator is currently: {}",
58 current_first
59 );
60 }
61
62 // Accessing the last item; This will
63 // consume the entire iterator
64 let last_letter = alphabet.last();
65 if let Some(last_letter) = last_letter {
66 println!("The last letter of the alphabet is: {}",
last_letter);
67 }
- 将迭代器收集到集合中:
69 // Collect iterators into collections
70 // This requires an anotation of which collection we want
71 // The following two are equivalent:
72 let nums: Vec<_> = (1..10).collect();
73 println!("nums: {:?}", nums);
74 let nums = (1..10).collect::<Vec<_>>();
75 println!("nums: {:?}", nums)
- 更改正在迭代的项:
79 // Taking only the first n items
80 // This is often used to make an infinite iterator finite
81 let nums: Vec<_> = all_nums.take(5).collect();
82 println!("The first five numbers are: {:?}", nums);
83
84 // Skip the first few items
85 let nums: Vec<_> = (0..11).skip(2).collect();
86 println!("The last 8 letters in a range from zero to 10:
{:?}", nums);
87
88 // take and skip accept predicates in the form of
89 // take_while and skip_while
90 let nums: Vec<_> = (0..).take_while(|x| x * x <
50).collect();
91 println!(
92 "All positive numbers that are less than 50 when squared:
93 {:?}", nums
94 );
95
96 // This is useful to filter an already sorted vector
97 let names = ["Alfred", "Andy", "Jose", "Luke"];
98 let names: Vec<_> = names.iter().skip_while(|x|
x.starts_with('A')).collect();
99 println!("Names that don't start with 'A': {:?}", names);
100
101 // Filtering iterators
102 let countries = [
103 "U.S.A.",
"Germany",
"France",
"Italy",
"India",
"Pakistan",
"Burma",
104 ];
105 let countries_with_i: Vec<_> = countries
106 .iter()
107 .filter(|country| country.contains('i'))
108 .collect();
109 println!(
110 "Countries containing the letter 'i': {:?}",
111 countries_with_i
112 );
- 检查迭代器是否包含元素:
116 // Find the first element that satisfies a condition
117 if let Some(country) = countries.iter().find(|country|
118 country.starts_with('I')) {
119 println!("First country starting with the letter 'I':
{}", country);
}
120
121 // Don't get the searched item but rather its index
122 if let Some(pos) = countries
123 .iter()
124 .position(|country| country.starts_with('I'))
125 {
126 println!("It's index is: {}", pos);
127 }
128
129 // Check if at least one item satisfies a condition
130 let are_any = countries.iter().any(|country| country.len() ==
5);
131 println!(
132 "Is there at least one country that has exactly five
letters? {}",
133 are_any
134 );
135
136 // Check if ALL items satisfy a condition
137 let are_all = countries.iter().all(|country| country.len() ==
5);
138 println!("Do all countries have exactly five letters? {}",
are_all);
- 用于数值项的有用操作:
141 let sum: i32 = (1..11).sum();
142 let product: i32 = (1..11).product();
143 println!(
144 "When operating on the first ten positive numbers\n\
145 their sum is {} and\n\
146 their product is {}.",
147 sum, product
148 );
149
150 let max = (1..11).max();
151 let min = (1..11).min();
152 if let Some(max) = max {
153 println!("They have a highest number, and it is {}", max);
154 }
155 if let Some(min) = min {
156 println!("They have a smallest number, and it is {}", min);
157 }
- 组合迭代器:
161 // Combine an iterator with itself, making it infinite
162 // When it reaches its end, it starts again
163 let some_numbers: Vec<_> = (1..4).cycle().take(10).collect();
164 // Reader exercise: Try to guess what this will print
165 println!("some_numbers: {:?}", some_numbers);
166
167 // Combine two iterators by putting them after another
168 let some_numbers: Vec<_> = (1..4).chain(10..14).collect();
169 println!("some_numbers: {:?}", some_numbers);
170
171 // Zip two iterators together by grouping their first items
172 // together, their second items together, etc.
173 let swiss_post_codes = [8957, 5000, 5034];
174 let swiss_towns = ["Spreitenbach", "Aarau", "Suhr"];
175 let zipped: Vec<_> =
swiss_post_codes.iter().zip(swiss_towns.iter()).collect();
176 println!("zipped: {:?}", zipped);
177
178 // Because zip is lazy, you can use two infine ranges
179 let zipped: Vec<_> = (b'A'..)
180 .zip(1..)
181 .take(10)
182 .map(|(ch, num)| (ch as char, num))
183 .collect();
184 println!("zipped: {:?}", zipped);
- 将函数应用于所有项:
188 // Change the items' types
189 let numbers_as_strings: Vec<_> = (1..11).map(|x|
x.to_string()).collect();
190 println!("numbers_as_strings: {:?}", numbers_as_strings);
191
192 // Access all items
193 println!("First ten squares:");
194 (1..11).for_each(|x| print!("{} ", x));
195 println!();
196
197 // filter and map items at the same time!
198 let squares: Vec<_> = (1..50)
199 .filter_map(|x| if x % 3 == 0 { Some(x * x) } else { None })
200 .collect();
201 println!(
202 "Squares of all numbers under 50 that are divisible by 3:
203 {:?}", squares
204 );
- 迭代器的真正优势来自于它们的组合:
208 // Retrieve the entire alphabet in lower and uppercase:
209 let alphabet: Vec<_> = (b'A' .. b'z' + 1) // Start as u8
210 .map(|c| c as char) // Convert all to chars
211 .filter(|c| c.is_alphabetic()) // Filter only alphabetic chars
212 .collect(); // Collect as Vec<char>
213 println!("alphabet: {:?}", alphabet);
214 }
它是如何工作的...
这个方法非常重要。无论你做什么,或者使用哪个库,它都将在某个地方使用迭代器。所有展示的操作都可以用于任何集合和实现了 iterator 特性的所有类型。
在第一部分,我们探讨了创建迭代器的不同方法。我提到范围是有限的,因为为了可迭代性,范围类型必须实现 Step。char 不实现,所以你不能将 'A'..'D' 作为迭代器使用。因此,在 [209] 行中,我们以字节的形式迭代字符:
let alphabet: Vec<_> = (b'A' .. b'z' + 1) // Start as u8
.map(|c| c as char) // Convert all to chars
.filter(|c| c.is_alphabetic()) // Filter only alphabetic chars
.collect(); // Collect as Vec<char>
我们必须将范围的上限设置为 b'z' + 1,因为范围是非包含的。你可能已经注意到,这个事实有时会使使用范围变得令人困惑。这就是为什么在夜间编译器上,你可以使用包含范围(第十章,使用实验性夜间功能,迭代包含范围)。
尽管如此,让我们回到我们的配方。在迭代过程中,你可以选择使用enumerate来获取一个迭代计数器 [22]。这就是为什么 Rust 可以避开不支持传统的 C 风格for循环语法。你很可能在其他语言中看到过以下 C 代码的某种变体:
for (int i = 0; i < some_length; i++) {
...
}
Rust 禁止这样做,因为基于范围的for循环几乎总是更简洁,如果你来自 Python 背景,你将会知道这一点,因为它开创了这种限制。事实上,大多数编程语言都已经将它们的范式转向促进基于范围的循环。在极少数情况下,如果你真的实际上想了解你的迭代计数,你可以使用enumerate来模拟这种行为。
当使用nth[41]访问单个项时,你必须注意两件事:
-
它通过遍历所有项目直到找到你想要的项目来访问一个项。在最坏的情况下,这是一种
访问。如果你可以的话,使用你收藏的本地访问方法(大多数情况下,这将是通过.get())。 -
它消耗迭代器直到指定的索引。这意味着使用相同参数两次调用
nth将导致两个不同的返回值 [54]。不要对此感到惊讶。
在使用迭代器的各种访问器时,要注意的另一件事是,它们都返回一个Option,如果迭代器没有更多项目,它将是None。
当将迭代器收集到集合中时,以下两种形式的注解是完全等价的:
let nums: Vec<_> = (1..10).collect();
let nums = (1..10).collect::<Vec<_>>();
只使用你最喜欢的一种。在这本书中,我们坚持使用第一种形式,因为这是个人偏好的原因。顺便说一下,第二种形式被称为turbofish。这是因为::<>看起来像某种鱼类的家族。可爱,不是吗?两种形式也能自动推断出它们的确切类型,所以你不需要写Vec<i32>。内部类型可以用下划线(_)省略,如下所示。
cycle [163] 接受一个迭代器,并无限重复它。[1, 2, 3] 将变成 [1, 2, 3, 1, 2, 3, 1, ...]。
zip [175] 接受两个迭代器,通过将相同索引的项放入元组中,然后链接它们来创建一个。如果迭代器的大小不同,它将忽略较长迭代器的额外项。例如,[1, 2, 3] 与 ['a', 'b', 'c', 'd'] 进行zip将变成 [(1, 'a'), (2, 'b'), (3, 'c')],因为 'd' 将被丢弃,因为它没有可以与之zip的伙伴。如果你将两个无限范围zip在一起,你不会有问题,因为zip是惰性的,这意味着它只有在真正需要时才会开始zip你的迭代器;例如,当使用take提取前几个元组 [81]。
如果你需要修改所有项,可以使用 map。它也可以用来更改迭代器的底层类型,如第 [182] 行所示。for_each与之非常相似,但有一个很大的区别:它不返回任何内容。它基本上就是在迭代器上手动使用 for 循环。它的预期用例是在你有很多链式方法调用迭代器的情况下,将 for_each 链接起来会更优雅,作为一种 消费者。
迭代器经常被链在一起,以编织复杂的转换。如果你发现自己在一个迭代器上调用很多方法,不要担心,因为这正是你应该做的。另一方面,当你发现自己在一个 for 循环中做了很多复杂的事情时,你可能需要用迭代器重写那段代码。
当使用 filter_map[199] 时,你可以通过返回一个包裹在 Some 中的项来保留一个项。如果你想过滤掉它,就返回 None。在此之前,你可以以任何你想要的方式更改项,这就是交易中的 map 部分。
还有更多...
iter() 创建了一个 借用 项的迭代器。如果你想创建一个 消费 项的迭代器——例如,通过移动它们来获取所有权——你可以使用 into_iter()。
参见
- 在第十章 “遍历包含范围”,使用实验性夜间功能 中的配方
使用 VecDeque
当你需要定期在向量的开始处插入或移除元素时,你的性能可能会受到很大的影响,因为这将迫使向量重新分配其后的所有数据。在实现队列时,这尤其令人烦恼。因此,Rust 为你提供了 VecDeque。
如何做到这一点...
-
在
src/bin文件夹中创建一个名为vecdeque.rs的文件。 -
添加以下代码,并使用
cargo run --bin vecdeque运行它:
1 use std::collections::VecDeque;
2
3 fn main() {
4 // A VecDeque is best thought of as a
5 // First-In-First-Out (FIFO) queue
6
7 // Usually, you will use it to push_back data
8 // and then remove it again with pop_front
9 let mut orders = VecDeque::new();
10 println!("A guest ordered oysters!");
11 orders.push_back("oysters");
12
13 println!("A guest ordered fish and chips!");
14 orders.push_back("fish and chips");
15
16 let prepared = orders.pop_front();
17 if let Some(prepared) = prepared {
18 println!("{} are ready", prepared);
19 }
20
21 println!("A guest ordered mozarella sticks!");
22 orders.push_back("mozarella sticks");
23
24 let prepared = orders.pop_front();
25 if let Some(prepared) = prepared {
26 println!("{} are ready", prepared);
27 }
28
29 println!("A guest ordered onion rings!");
30 orders.push_back("onion rings");
31
32 let prepared = orders.pop_front();
33 if let Some(prepared) = prepared {
34 println!("{} are ready", prepared);
35 }
36
37 let prepared = orders.pop_front();
38 if let Some(prepared) = prepared {
39 println!("{} are ready", prepared);
40 }
41
42 // You can freely switch your pushing
43 // from front to back and vice versa
44 let mut sentence = VecDeque::new();
45 sentence.push_back("a");
46 sentence.push_front("had");
47 sentence.push_back("little");
48 sentence.push_front("Mary");
49 sentence.push_back("Lamb");
50 println!("sentence: {:?}", sentence);
51
52 // The same applies to popping data
53 sentence.pop_front();
54 sentence.push_front("Jimmy");
55 sentence.pop_back();
56 sentence.push_back("Cat");
57 println!("sentence: {:?}", sentence);
58
59
60 // The rest of the VecDeque's methods are
61 // pretty much the same as the vector's
62 // However, the VecDeque has additional options
63 // when swap removing!
64 let mut some_queue = VecDeque::with_capacity(5);
65 some_queue.push_back("A");
66 some_queue.push_back("B");
67 some_queue.push_back("C");
68 some_queue.push_back("D");
69 some_queue.push_back("E");
70 println!("some_queue: {:?}", some_queue);
71
72 // This is the same as Vec's swap_remove
73 some_queue.swap_remove_back(2);
74 println!("some_quere after swap_remove_back: {:?}",
some_queue);
75
76 // This is the nearly the same, but swaps the removed
77 // element with the first one instead of the last one
78 some_queue.swap_remove_front(2);
79 println!("some_quere after swap_remove_front: {:?}",
some_queue);
80 }
它是如何工作的...
VecDeque 的大多数接口与 Vec 相同。你甚至可以用 with_capacity 和其 swap_remove 等效方法以相同的方式优化它们。差异来自于 VecDeque 更倾向于从两端进行访问。因此,Vec 中影响最后一个元素的多个方法在 VecDeque 中有两个等效方法:一个用于前端,一个用于后端。这些是:
-
push变成了push_front[46] 和push_back[11] -
pop变成了pop_front[16] 和pop_back[55] -
swap_remove变成了remove_front[78] 和remove_back[73]
VecDeque 有能力以高效的方式从两端自由地追加或移除元素,这使得它成为 先进先出(FIFO)队列 [24] 的理想候选者。实际上,它几乎总是这样使用的。
当你发现自己处于一个想要按到达顺序响应任何请求并在之后再次移除它们的情境中时,VecDeque 是完成这项工作的理想工具。
还有更多...
内部,VecDeque 被实现为一个 环形缓冲区,也称为 循环缓冲区。之所以这样称呼,是因为它的行为就像一个圆圈:末端触及起始端。
它通过分配一个连续的内存块来工作,就像 Vec 一样;然而,Vec 总是在块的末尾留下额外的容量,而 VecDeque 对在块内部留下空位没有任何异议。因此,当你移除第一个元素时,VecDeque 并不会将所有元素向左移动,而是简单地留下第一个空位。如果你然后通过 push_front 在开始处推入一个元素,它将占用之前释放的位置,而不会触及它后面的元素。
故事中的循环陷阱在于,如果你在使用 push_back 时在块的前端有一些容量,但在后端没有,VecDeque 将会简单地使用那个空间来分配额外的元素,导致以下情况:

这很好,因为在使用它的时候,你根本不必担心这一点,因为它的迭代方法总是显示给你 正确的 顺序!
就像向量一样,当 VecDeque 的容量耗尽时,它会自动调整大小并将所有元素移动到一个新的块中。
使用 HashMap
如果你想象一下 Vec 是一个将索引(0、1、2 等等)分配给数据的集合,那么 HashMap 就是一个将任何数据分配给任何数据的集合。它允许你将任意可哈希的数据映射到其他任意数据。哈希和映射,这就是名字的由来!
如何做到这一点...
-
在
src/bin文件夹中,创建一个名为hashmap.rs的文件。 -
添加以下代码,并使用
cargo run --bin hashmap运行它:
1 use std::collections::HashMap;
2
3 fn main() {
4 // The HashMap can map any hashable type to any other
5 // The first type is called the "key"
6 // and the second one the "value"
7 let mut tv_ratings = HashMap::new();
8 // Here, we are mapping &str to i32
9 tv_ratings.insert("The IT Crowd", 8);
10 tv_ratings.insert("13 Reasons Why", 7);
11 tv_ratings.insert("House of Cards", 9);
12 tv_ratings.insert("Stranger Things", 8);
13 tv_ratings.insert("Breaking Bad", 10);
14
15 // Does a key exist?
16 let contains_tv_show = tv_ratings.contains_key("House of
Cards");
17 println!("Did we rate House of Cards? {}", contains_tv_show);
18 let contains_tv_show = tv_ratings.contains_key("House");
19 println!("Did we rate House? {}", contains_tv_show);
20
21 // Access a value
22 if let Some(rating) = tv_ratings.get("Breaking Bad") {
23 println!("I rate Breaking Bad {} out of 10", rating);
24 }
25
26 // If we insert a value twice, we overwrite it
27 let old_rating = tv_ratings.insert("13 Reasons Why", 9);
28 if let Some(old_rating) = old_rating {
29 println!("13 Reasons Why's old rating was {} out of 10",
old_rating);
30 }
31 if let Some(rating) = tv_ratings.get("13 Reasons Why") {
32 println!("But I changed my mind, it's now {} out of 10",
rating);
33 }
34
35 // Remove a key and its value
36 let removed_value = tv_ratings.remove("The IT Crowd");
37 if let Some(removed_value) = removed_value {
38 println!("The removed series had a rating of {}",
removed_value);
39 }
40
41 // Iterating accesses all keys and values
42 println!("All ratings:");
43 for (key, value) in &tv_ratings {
44 println!("{}\t: {}", key, value);
45 }
46
47 // We can iterate mutably
48 println!("All ratings with 100 as a maximum:");
49 for (key, value) in &mut tv_ratings {
50 *value *= 10;
51 println!("{}\t: {}", key, value);
52 }
53
54 // Iterating without referencing the HashMap moves its
contents
55 for _ in tv_ratings {}
56 // tv_ratings is not usable anymore
如果你不需要同时访问键和值,你可以单独迭代任何一个:
58 // Like with the other collections, you can preallocate a size
59 // to gain some performance
60 let mut age = HashMap::with_capacity(10);
61 age.insert("Dory", 8);
62 age.insert("Nemo", 5);
63 age.insert("Merlin", 10);
64 age.insert("Bruce", 9);
65
66 // Iterate over all keys
67 println!("All names:");
68 for name in age.keys() {
69 println!("{}", name);
70 }
71
72 // Iterate over all values
73 println!("All ages:");
74 for age in age.values() {
75 println!("{}", age);
76 }
77
78 // Iterate over all values and mutate them
79 println!("All ages in 10 years");
80 for age in age.values_mut() {
81 *age += 10;
82 println!("{}", age);
83 }
84
你可以使用 entry API 为不在 HashMap 中的键分配默认值:
87 {
88 let age_of_coral = age.entry("coral").or_insert(11);
89 println!("age_of_coral: {}", age_of_coral);
90 }
91 let age_of_coral = age.entry("coral").or_insert(15);
92 println!("age_of_coral: {}", age_of_coral);
93 }
它是如何工作的...
如前所述,HashMap 是一个将一种类型的数据映射到另一种类型的集合。你是通过调用 insert 并传递你的键及其值 [9] 来做到这一点的。如果键已经有一个值,它将被覆盖。这就是为什么 insert 返回一个 Option:如果之前有一个值,它返回旧值 [27],否则返回 None。如果你想确保你没有覆盖任何东西,确保在插入你的值之前检查 contains_key 的结果 [16]。
当使用无效键调用 get 和 remove 时,它们都不会崩溃。相反,它们返回一个 Result。在 remove 的情况下,这个 Result 包含被移除的值。
与大多数集合一样,你可以选择迭代你的数据,通过借用键值对[43]、在修改值的同时借用键[49],或者移动它们全部[55]。由于其性质,HashMap 还允许你另外三个选项:借用所有值[74]、修改所有值[80]或借用所有键[68]。你可能已经注意到,有一个组合是缺失的:你不能修改键。永远不能。这是你使用 HashMap 时签订的合同的一部分。在下面解释 HashMap 的实现时,你会看到,因为键的哈希实际上是索引,修改键相当于删除条目并重新创建它。这一点在设计选择中得到了很好的体现,即不允许你修改键。
最后但同样重要的是,Entry API 允许你访问可能存在或不存在的一个值的抽象。大多数时候,它与 or_insert 配合使用,以便在找不到键时插入一个默认值 [88]。如果你想根据闭包插入一个默认值,可以使用 or_insert_with。entry 对象的另一个用途是将其与其变体匹配:Occupied 或 Vacant。这相当于直接在键上调用 get。请注意,在我们的例子中,我们必须这样限定 entry 的访问范围:
{
let age_of_coral = age.entry("coral").or_insert(11);
println!("age_of_coral: {}", age_of_coral);
}
let age_of_coral = age.entry("coral").or_insert(15);
println!("age_of_coral: {}", age_of_coral);
这是因为 or_insert 返回对值的可变引用。如果我们省略了范围,entry 的第二次调用就会在存在可变引用的同时借用我们的 age 对象,这在 Rust 的借用概念中是一个错误,以确保对资源的无数据竞争访问。
如果你需要针对性能微调你的 HashMap,你可以调用你的常用朋友——with_capacity [60]、shrink_to_fit 和 reserve 也适用于它,并且以与其他集合相同的方式工作。
还有更多...
在内部,你可以想象 HashMap 是作为两个向量实现的:一个表和一个缓冲区。当然,我们在这里简化了;实际上实现中并没有向量。但这个类比足够准确。
如果你想查看实际的实现,请随意查看,因为 Rust 是完全开源的:github.com/rust-lang/rust/blob/master/src/libstd/collections/hash/table.rs.
在后台,缓冲区以顺序方式存储我们的值。在前端,我们有一个表,存储着不做什么更多事情的桶,它们仅仅指向它们所代表的元素。当你插入一个键值对时,发生的情况是:
-
值被放入缓冲区。
-
键会通过一个哈希函数并成为索引。
-
表在指定索引处创建一个指向实际值的桶:

Rust 的哈希算法实际上并不生成唯一的索引,出于性能原因。相反,Rust 使用一种巧妙的方式来处理哈希冲突,称为 罗宾汉桶偷窃 (codecapsule.com/2013/11/11/robin-hood-hashing/)。
标准库的默认哈希算法被特别选择来保护你免受 HashDoS 攻击(cryptanalysis.eu/blog/2011/12/28/effective-dos-attacks-against-web-application-plattforms-hashdos/)。如果你想榨取每一分性能,你可以做到,在你的 HashMap 中不考虑这个特定的风险,或者你可以通过使用 with_hasher 构造函数指定一个自定义的哈希器。
许多人已经在 crates.io 上实现了各种哈希器,所以在自己动手实现解决方案之前,请务必检查它们。
使用 HashSet
描述 HashSet 的最好方法就是描述它的实现方式:HashMap<K, ()>。它只是一个没有值的 HashMap!
选择 HashSet 的两个最佳理由是:
-
你根本不想处理重复的值,因为它甚至不包括它们。
-
你计划进行大量的(我指的是大量)项目查找——这是问题,我的集合中是否包含这个特定的项目? 在向量中,这是通过
完成的,而 HashSet可以通过
完成它。
如何做到...
-
在
src/bin文件夹中创建一个名为hashset.rs的文件。 -
添加以下代码,并使用
cargo run --bin hashset运行它:
1 use std::collections::HashSet;
2
3 fn main() {
4 // Most of the interface of HashSet
5 // is the same as HashMap, just without
6 // the methods that handle values
7 let mut books = HashSet::new();
8 books.insert("Harry Potter and the Philosopher's Stone");
9 books.insert("The Name of the Wind");
10 books.insert("A Game of Thrones");
11
12 // A HashSet will ignore duplicate entries
13 // but will return if an entry is new or not
14 let is_new = books.insert("The Lies of Locke Lamora");
15 if is_new {
16 println!("We've just added a new book!");
17 }
18
19 let is_new = books.insert("A Game of Thrones");
20 if !is_new {
21 println!("Sorry, we already had that book in store");
22 }
23
24 // Check if it contains a key
25 if !books.contains("The Doors of Stone") {
26 println!("We sadly don't have that book yet");
27 }
28
29 // Remove an entry
30 let was_removed = books.remove("The Darkness that comes
before");
31 if !was_removed {
32 println!("Couldn't remove book; We didn't have it to begin
with");
33 }
34 let was_removed = books.remove("Harry Potter and the
Philosopher's Stone");
35 if was_removed {
36 println!("Oops, we lost a book");
37 }
- 比较不同的
HashSet:
41 let one_to_five: HashSet<_> = (1..6).collect();
42 let five_to_ten: HashSet<_> = (5..11).collect();
43 let one_to_ten: HashSet<_> = (1..11).collect();
44 let three_to_eight: HashSet<_> = (3..9).collect();
45
46 // Check if two HashSets have no elements in common
47 let is_disjoint = one_to_five.is_disjoint(&five_to_ten);
48 println!(
49 "is {:?} disjoint from {:?}?: {}",
50 one_to_five,
51 five_to_ten,
52 is_disjoint
53 );
54 let is_disjoint = one_to_five.is_disjoint(&three_to_eight);
55 println!(
56 "is {:?} disjoint from {:?}?: {}",
57 one_to_five,
58 three_to_eight,
59 is_disjoint
60 );
61
62 // Check if a HashSet is fully contained in another
63 let is_subset = one_to_five.is_subset(&five_to_ten);
64 println!(
65 "is {:?} a subset of {:?}?: {}",
66 one_to_five,
67 five_to_ten,
68 is_subset
69 );
70 let is_subset = one_to_five.is_subset(&one_to_ten);
71 println!(
72 "is {:?} a subset of {:?}?: {}",
73 one_to_five,
74 one_to_ten,
75 is_subset
76 );
77
78 // Check if a HashSet fully contains another
79 let is_superset = three_to_eight.is_superset(&five_to_ten);
80 println!(
81 "is {:?} a superset of {:?}?: {}",
82 three_to_eight,
83 five_to_ten,
84 is_superset
85 );
86 let is_superset = one_to_ten.is_superset(&five_to_ten);
87 println!(
88 "is {:?} a superset of {:?}?: {}",
89 one_to_ten,
90 five_to_ten,
91 is_superset
92 );
- 以各种方式连接两个
HashSet:
96 // Get the values that are in the first HashSet
97 // but not in the second
98 let difference = one_to_five.difference(&three_to_eight);
99 println!(
100 "The difference between {:?} and {:?} is {:?}",
101 one_to_five,
102 three_to_eight,
103 difference
104 );
105
106 // Get the values that are in either HashSets, but not in both
107 let symmetric_difference =
one_to_five.symmetric_difference(&three_to_eight);
108 println!(
109 "The symmetric difference between {:?} and {:?} is {:?}",
110 one_to_five,
111 three_to_eight,
112 symmetric_difference
113 );
114
115 // Get the values that are in both HashSets
116 let intersection = one_to_five.intersection(&three_to_eight);
117 println!(
118 "The intersection difference between {:?} and {:?} is {:?}",
119 one_to_five,
120 three_to_eight,
121 intersection
122 );
123
124 // Get all values in both HashSets
125 let union = one_to_five.union(&three_to_eight);
126 println!(
127 "The union difference between {:?} and {:?} is {:?}",
128 one_to_five,
129 three_to_eight,
130 union
131 );
132 }
它是如何工作的...
由于 HashSet 是一种 HashMap,所以它的接口大部分都很相似。主要区别在于,在 HashMap 中会返回键的值的方法,在 HashSet 中只是简单地返回一个 bool 来告诉是否已经存在该键 [14]。
此外,HashSet 还提供了一些用于分析两个集合 [46 到 92] 和合并它们 [96 到 131] 的方法。如果你曾经听说过集合论或文氏图,或者做过一些 SQL,你将能够识别所有这些。否则,我建议你运行示例,并结合相关的注释研究输出结果。
一些插图可能有助于你。对于分析方法,深绿色部分是参考对象:

对于选择方法,深绿色部分是返回的内容:

还有更多...
HashSet 的实现没有太多惊喜,因为它与 HashMap 完全相同,只是没有任何值!

创建自己的迭代器
当你创建一个无限适用的算法或类似集合的结构时,拥有迭代器提供的数十种方法是非常有用的。为此,你必须知道如何告诉 Rust 为你实现它们。
如何做...
-
在
src/bin文件夹中,创建一个名为own_iterator.rs的文件。 -
添加以下代码,并使用
cargo run --bin own_iterator运行它:
1 fn main() {
2 let fib: Vec<_> = fibonacci().take(10).collect();
3 println!("First 10 numbers of the fibonacci sequence: {:?}",
fib);
4
5 let mut squared_vec = SquaredVec::new();
6 squared_vec.push(1);
7 squared_vec.push(2);
8 squared_vec.push(3);
9 squared_vec.push(4);
10 for (index, num) in squared_vec.iter().enumerate() {
11 println!("{}² is {}", index + 1, num);
12 }
13 }
14
15
16 fn fibonacci() -> Fibonacci {
17 Fibonacci { curr: 0, next: 1 }
18 }
19 struct Fibonacci {
20 curr: u32,
21 next: u32,
22 }
23 // A custom iterator has to implement
24 // only one method: What comes next
25 impl Iterator for Fibonacci {
26 type Item = u32;
27 fn next(&mut self) -> Option<u32> {
28 let old = self.curr;
29 self.curr = self.next;
30 self.next += old;
31 Some(old)
32 }
33 }
34
35
36 use std::ops::Mul;
37 struct SquaredVec<T>
38 where
39 T: Mul + Copy,
40 {
41 vec: Vec<T::Output>,
42 }
43 impl<T> SquaredVec<T>
44 where
45 T: Mul + Copy,
46 {
47 fn new() -> Self {
48 SquaredVec { vec: Vec::new() }
49 }
50 fn push(&mut self, item: T) {
51 self.vec.push(item * item);
52 }
53 }
54
55 // When creating an iterator over a collection-like struct
56 // It's best to just allow it to be convertible into
57 // a slice of your underlying type.
58 // This way you automatically implemented a bunch of methods
59 // and are flexible enough to change your implementation later
on
60 use std::ops::Deref;
61 impl<T> Deref for SquaredVec<T>
62 where
63 T: Mul + Copy,
64 {
65 type Target = [T::Output];
66 fn deref(&self) -> &Self::Target {
67 &self.vec
68 }
69 }
它是如何工作的...
在我们的小例子中,我们将探讨迭代器的两种不同用途:
-
fibonacci(),它返回斐波那契数列的无穷范围 -
SquaredVec,它实现了一个(非常)小的Vec子集,有一个特点:它将所有项目平方
斐波那契数列被定义为一系列数字,从 0 和 1 开始,下一个数字是前两个数字的和。它开始如下:0, 1, 1, 2, 3, 5, 8, 13, 21,等等。
根据定义,前两个数是 0 和 1。下一个数是它们的和——0 + 1 = 1。然后是1 + 1 = 2。然后2 + 1 = 3。3 + 2 = 5。无限重复。
通过实现Iterator特质,可以将一个算法转换为一个迭代器。这很简单,因为它只期望你提供正在迭代的类型和一个单一的方法next,该方法获取下一个项目。如果迭代器没有剩余的项目,它应该返回None,否则返回Some。我们的斐波那契迭代器总是返回Some项目,这使得它成为一个无穷迭代器[31]。
相反,我们的SquaredVec更像是一个集合而不是一个算法。在第[37]行到[53]行,我们包装了Vec接口的最小值——我们可以创建一个SquaredVec,并且可以填充它。我们的类型约束Mul + Copy意味着用户想要存储的项目必须能够被复制并且能够被乘。我们需要这样做以便平方它,但对于迭代器来说并不相关。T::Output只是一个乘法会返回的类型,通常情况下它将是T本身。
我们可以再次实现Iterator特质,但有一个更简单的方法可以为你提供更多方法。我们可以允许我们的结构体隐式转换为切片[T],这将不仅为你实现Iterator,还会实现一大堆其他方法。因为Vec已经实现了它,所以你可以直接返回它[67]。如果你的底层集合没有提供切片转换,你仍然可以像以前一样手动实现Iterator特质。
还有更多...
如果你需要在迭代器中执行很多复杂的逻辑,并且想要将它们与集合稍微分离,你可以通过为你的集合提供IntoIterator特质来实现这一点。这将允许你返回一个专门为迭代制作的 struct,它本身提供了Iterator特质。
使用一块板
一些算法要求你持有可能存在或不存在的数据的访问令牌。在 Rust 中,可以通过使用Vec<Option<T>>并把你数据索引当作令牌来解决。但我们可以做得更好!slab正是这个概念的优化抽象。
虽然它不是一种通用集合,但如果使用得当,slab可以为你提供很多帮助。
如何做到这一点...
-
打开之前为你生成的
Cargo.toml文件。 -
在
[dependencies]下添加以下行:
slab = "0.4.0"
-
如果你想,你可以访问 slab 的 crates.io 页面(
crates.io/crates/slab)来检查最新版本,并使用那个版本。 -
在
bin文件夹中创建一个名为slab.rs的文件。 -
添加以下代码,并使用
cargo run --bin slab运行它:
1 extern crate slab;
2 use slab::{Slab, VacantEntry};
3
4 fn main() {
5 // A slab is meant to be used as a limited buffer
6 // As such, you should initialize it with a pre-
7 // defined capacity
8 const CAPACITY: usize = 1024;
9 let mut slab = Slab::with_capacity(CAPACITY);
10
11 // You cannot simply access a slab's entry by
12 // index or by searching it. Instead, every
13 // insert gives you a key that you can use to
14 // access its entry
15 let hello_key = slab.insert("hello");
16 let world_key = slab.insert("world");
17
18 println!("hello_key -> '{}'", slab[hello_key],);
19 println!("world_key -> '{}'", slab[world_key],);
20
21
22 // You can pass an "empty spot" around
23 // in order to be filled
24 let data_key = {
25 let entry = slab.vacant_entry();
26 fill_some_data(entry)
27 };
28 println!("data_key -> '{}'", slab[data_key],);
29
30 // When iterating, you get a key-value pair
31 for (key, val) in &slab {
32 println!("{} -> {}", key, val);
33 }
34
35 // If you want to keep your slab at a constant
36 // capacity, you have to manually check its
37 // length before inserting data
38 if slab.len() != slab.capacity() {
39 slab.insert("the slab is not at capacity yet");
40 }
41 }
42
43
44 fn fill_some_data(entry: VacantEntry<&str>) -> usize {
45 let data = "Some data";
46 // insert() consumes the entry
47 // so we need to get the key before
48 let key = entry.key();
49 entry.insert(data);
50 key
51 }
它是如何工作的...
块非常类似于向量,有一个本质的区别:你无法选择你的索引。相反,在插入数据[15]时,你会收到数据的索引作为一种密钥,你可以用它再次访问它。这是你的责任,将这个密钥存储在某个地方;否则,检索你的数据的唯一方法就是遍历你的块。另一方面,你也不需要提供任何密钥。与HashMap不同,你根本不需要任何可哈希的对象。
这种情况在连接池中很有用:如果你有多个客户端想要访问单个资源,你可以在块中存储这些资源,并为客户提供其密钥作为令牌。
这个例子非常适合块的第二种用途。假设你只在特定时间内接受一定数量的连接。在建立连接时,你并不关心确切的索引或存储方式。相反,你只关心它以可检索的方式存储,并且不超过你的限制。这非常适合块,这也是为什么大多数时候你不会使用Slab::new()创建块,而是使用with_capacity,将其设置为常数上限[9]。
然而,块本身并不通过这种方式设置限制,因为它在处理容量方面与向量表现得完全一样:一旦长度超过容量,块就会将所有对象重新分配到更大的内存块中,并提高容量。这就是为什么在处理上限时,你应该使用某种变体的行[38]插入你的数据:
if slab.len() != slab.capacity() {
slab.insert("the slab is not at capacity yet");
}
其他有效的方法包括将插入操作包裹在一个返回Result或Option的函数中。
还有更多...
一个块由一个Vec<Entry>支持。你可能还记得我们之前关于HashMap的配方中的Entry。它与Option相同,区别在于其变体不称为Some(...)和None,而是Occupied(...)和Vacant。这意味着简而言之,一个块被实现为一个带有空位的向量:

此外,为了保证快速占用空位,该板保留了一个所有空条目的链表。
第三章:处理文件和文件系统
在本章中,我们将涵盖以下菜谱:
-
处理文本文件
-
处理字节
-
处理二进制文件
-
压缩和解压缩数据
-
遍历文件系统
-
使用 glob 模式查找文件
简介
在大数据、机器学习和云服务时代,你不能依赖你的所有数据始终在内存中。相反,你需要能够有效地检查和遍历文件系统,并在你方便的时候操作其内容。
在阅读了这一章之后,你将能够做到的事情包括在子目录中配置具有不同命名变体的文件、以高效的二进制格式保存你的数据、读取其他程序生成的协议,以及压缩你的数据以便以快速的速度通过互联网发送。
处理文本文件
在这个菜谱中,我们将学习如何读取、写入、创建、截断和追加文本文件。掌握了这些知识,你将能够将所有其他菜谱应用于文件,而不是内存中的字符串。
如何去做...
-
使用
cargo new chapter-three创建一个在本章中工作的 Rust 项目。 -
导航到新创建的
chapter-three文件夹。在本章的其余部分,我们将假设你的命令行当前位于这个目录。 -
在
src文件夹内,创建一个名为bin的新文件夹。 -
删除生成的
lib.rs文件,因为我们不是创建一个库。 -
在
src/bin文件夹中,创建一个名为text_files.rs的文件。 -
添加以下代码,并用
cargo run --bin text_files运行它:
1 use std::fs::{File, OpenOptions};
2 use std::io::{self, BufReader, BufWriter, Lines, Write};
3 use std::io::prelude::*;
4
5 fn main() {
6 // Create a file and fill it with data
7 let path = "./foo.txt";
8 println!("Writing some data to '{}'", path);
9 write_file(path, "Hello World!\n").expect("Failed to write to
file");
10 // Read entire file as a string
11 let content = read_file(path).expect("Failed to read file");
12 println!("The file '{}' contains:", path);
13 println!("{}", content);
14
15 // Overwrite the file
16 println!("Writing new data to '{}'", path);
17 write_file(path, "New content\n").expect("Failed to write to
file");
18 let content = read_file(path).expect("Failed to read file");
19 println!("The file '{}' now contains:", path);
20 println!("{}", content);
21
22 // Append data to the file
23 println!("Appending data to '{}'", path);
24 append_file(path, "Some more content\n").expect("Failed to
append to file");
25 println!("The file '{}' now contains:", path);
26 // Read file line by line as an iterator
27 let lines = read_file_iterator(path).expect("Failed to read
file");
28 for line in lines {
29 println!("{}", line.expect("Failed to read line"));
30 }
31
32 append_and_read(path, "Last line in the file,
goodbye").expect("Failed to read and write file");
}
- 这些是
main()函数调用的函数:
37 fn read_file(path: &str) -> io::Result<String> {
38 // open() opens the file in read-only mode
39 let file = File::open(path)?;
40 // Wrap the file in a BufReader
41 // to read in an efficient way
42 let mut buf_reader = BufReader::new(file);
43 let mut content = String::new();
44 buf_reader.read_to_string(&mut content)?;
45 Ok(content)
46 }
47
48 fn read_file_iterator(path: &str) ->
io::Result<Lines<BufReader<File>>> {
49 let file = File::open(path)?;
50 let buf_reader = BufReader::new(file);
51 // lines() returns an iterator over lines
52 Ok(buf_reader.lines())
53 }
54
55
56 fn write_file(path: &str, content: &str) -> io::Result<()> {
57 // create() opens a file with the standard options
58 // to create, write and truncate a file
59 let file = File::create(path)?;
60 // Wrap the file in a BufReader
61 // to read in an efficient way
62 let mut buf_writer = BufWriter::new(file);
63 buf_writer.write_all(content.as_bytes())?;
64 Ok(())
65 }
66
67 fn append_file(path: &str, content: &str) -> io::Result<()> {
68 // OpenOptions lets you set all options individually
69 let file = OpenOptions::new().append(true).open(path)?;
70 let mut buf_writer = BufWriter::new(file);
71 buf_writer.write_all(content.as_bytes())?;
72 Ok(())
73 }
- 在同一个句柄上进行读取和写入:
76 fn append_and_read(path: &str, content: &str) -> io::Result<()
{
let file =
77 OpenOptions::new().read(true).append(true).open(path)?;
78 // Passing a reference of the file will not move it
79 // allowing you to create both a reader and a writer
80 let mut buf_reader = BufReader::new(&file);
81 let mut buf_writer = BufWriter::new(&file);
82
83 let mut file_content = String::new();
84 buf_reader.read_to_string(&mut file_content)?;
85 println!("File before appending:\n{}", file_content);
86
87 // Appending will shift your positional pointer
88 // so you have to save and restore it
89 let pos = buf_reader.seek(SeekFrom::Current(0))?;
90 buf_writer.write_all(content.as_bytes())?;
91 // Flushing forces the write to happen right now
92 buf_writer.flush()?;
93 buf_reader.seek(SeekFrom::Start(pos))?;
94
95 buf_reader.read_to_string(&mut file_content)?;
96 println!("File after appending:\n{}", file_content);
97
98 Ok(())
99 }
它是如何工作的...
我们的 main 函数分为三个部分:
-
创建一个文件。
-
覆盖文件,在这个上下文中被称为 截断。
-
向文件中追加。
在前两部分中,我们将整个文件内容加载到一个单独的 String 中并显示它 [11 和 18]。在最后一部分中,我们遍历文件中的单个行并打印它们 [28]。
File::open() 以只读模式打开文件,并返回对该文件的句柄 [39]。因为这个句柄实现了 Read 特性,我们现在可以直接使用 read_to_string 将其读入字符串。然而,在我们的示例中,我们首先将其包装在一个 BufReader [42] 中。这是因为专门的读取器可以通过收集读取指令来大大提高其资源访问的性能,这被称为 缓冲,并且可以批量执行它们。对于第一个读取示例 read_file [37],这根本没有任何区别,因为我们无论如何都是一次性读取的。我们仍然使用它,因为这是一种良好的实践,它允许我们在不担心性能的情况下,灵活地更改函数的精确读取机制。如果你想看到一个 BufReader 确实做了些事情的函数,可以向下看一点,到 read_file_iterator [48]。它看起来是逐行读取文件的。当处理大文件时,这将是一个非常低效的操作,这就是为什么 BufReader 实际上是一次性读取大块文件,然后逐行返回该段的原因。结果是优化了文件读取,我们甚至没有注意到或关心后台发生了什么,这非常方便。
File::create() 如果文件不存在则创建新文件,否则截断文件。在任何情况下,它都返回与 File::open() 之前相同的 File 处理符。另一个相似之处在于我们围绕它包装的 BufWriter。就像 BufReader 一样,即使没有它我们也能访问底层文件,但使用它来优化未来的访问也是有益的。
除了以只读或截断模式打开文件之外,还有更多选项。我们可以通过使用 OpenOptions [69] 创建文件句柄来使用它们,这使用了我们在第一章 学习基础知识 中探索的 使用构建器模式 部分中的构建器模式。在我们的示例中,我们对 append 模式感兴趣,它允许我们在每次访问时而不是覆盖它来向文件添加内容。
要查看所有可用选项的完整列表,请参阅 OpenOption 文档:
doc.rust-lang.org/std/fs/struct.OpenOptions.html。
我们可以在同一个文件处理符上进行读写操作。为此,在创建 ReadBuf 和 WriteBuf 时,我们传递文件的一个引用而不是移动它,因为否则缓冲区会消耗处理符,使得共享变得不可能:
let mut buf_reader = BufReader::new(&file);
let mut buf_writer = BufWriter::new(&file);
在进行此操作时,请注意在同一个句柄上进行追加和读取。当追加时,存储当前读取位置的内部指针可能会偏移。如果你想先读取,然后追加,然后继续读取,你应该在写入之前保存当前位置,然后在之后恢复它。
我们可以通过调用seek(SeekFrom::Current(0))[89]来访问文件中的当前位置。seek通过一定数量的字节移动我们的指针并返回其新位置。SeekFrom::Current(0)意味着我们想要移动的距离正好是当前位置的零字节远。由于这个原因,因为我们根本不移动,所以seek将返回我们的当前位置。
然后,我们使用flush[92]来追加我们的数据。我们必须调用这个方法,因为BufWriter通常会等待实际写入直到它被丢弃,也就是说,它不再在作用域内。由于我们想在发生之前读取,我们使用flush来强制写入。
最后,我们通过从之前的位置恢复,再次进行定位,准备好再次读取:
buf_reader.seek(SeekFrom::Start(pos))?;
我邀请您运行代码,查看结果,然后与注释掉此行后的输出进行比较。
还有更多...
我们可以在程序开始时打开一个单独的文件句柄,并将其传递给需要它的每个函数,而不是在每个函数中打开一个新的文件句柄。这是一个权衡——如果我们不反复锁定和解锁文件,我们会获得更好的性能。反过来,我们不允许其他进程在我们程序运行时访问我们的文件。
另请参阅
- 使用构建者模式的配方在第一章,学习基础知识
处理字节
当您设计自己的协议或使用现有的协议时,您必须能够舒适地移动和操作二进制缓冲区。幸运的是,扩展的标准库生态系统提供了byteorder包,以满足您所有二进制需求的各种读写功能。
准备就绪
在本章中,我们将讨论端序性。这是描述缓冲区中值排列顺序的一种方式。有两种方式来排列它们:
-
首先放最小的数字(小端序)
-
首先放最大的一个(大端序)
让我们尝试一个例子。假设我们想要保存十六进制值0x90AB12CD。我们首先必须将其分成0x90、0xAB、0x12和0xCD的位。现在我们可以按最大的值首先存储它们(大端序),0x90 - 0xAB - 0x12 - 0xCD,或者我们可以先写最小的数字(小端序),0xCD - 0x12 - 0xAB - 0x90。
如您所见,这是一组完全相同的值,但顺序相反。如果这个简短的说明让您感到困惑,我建议您查看马里兰大学计算机科学系的这篇优秀解释:web.archive.org/web/20170808042522/http://www.cs.umd.edu/class/sum2003/cmsc311/Notes/Data/endian.html.
没有更好的字节序。它们在不同的领域中被使用:例如,Intel 这样的微处理器使用 Little Endian,而 TCP、IPv4、IPv6 和 UDP 这样的互联网协议使用 Big Endian。这不是规则,而是一种为了保持向后兼容而维护的约定。因此,存在例外。
当设计您自己的协议时,请根据类似协议的字节序来定位,选择一个并简单地坚持使用它。
如何做到这一点...
按照以下步骤:
-
打开之前为您生成的
Cargo.toml文件。 -
在
[dependencies]下,添加以下行:
byteorder = "1.1.0"
-
如果您愿意,可以访问
byteorder的 crates.io 页面 (crates.io/crates/byteorder) 检查最新版本,并使用该版本。 -
在
bin文件夹中,创建一个名为bytes.rs的文件。 -
添加以下代码,并使用
cargo run --bin bytes运行它:
1 extern crate byteorder;
2 use std::io::{Cursor, Seek, SeekFrom};
3 use byteorder::{BigEndian, LittleEndian, ReadBytesExt,
WriteBytesExt};
4
5 fn main() {
6 let binary_nums = vec![2, 3, 12, 8, 5, 0];
7 // Wrap a binary collection in a cursor
8 // to provide seek functionality
9 let mut buff = Cursor::new(binary_nums);
10 let first_byte = buff.read_u8().expect("Failed to read
byte");
11 println!("first byte in binary: {:b}", first_byte);
12
13 // Reading advances the internal position,
14 // so now we read the second
15 let second_byte_as_int = buff.read_i8().expect("Failed to
read byte as int");
16 println!("second byte as int: {}", second_byte_as_int);
17
18 // Overwrite the current position
19 println!("Before: {:?}", buff);
20 buff.write_u8(123).expect("Failed to overwrite a byte");
21 println!("After: {:?}", buff);
22
23
24 // Set and get the current position
25 println!("Old position: {}", buff.position());
26 buff.set_position(0);
27 println!("New position: {}", buff.position());
28
29 // This also works using the Seek API
30 buff.seek(SeekFrom::End(0)).expect("Failed to seek end");
31 println!("Last position: {}", buff.position());
32
33 // Read and write in specific endianness
34 buff.set_position(0);
35 let as_u32 = buff.read_u32::<LittleEndian>()
36 .expect("Failed to read bytes");
37 println!(
38 "First four bytes as u32 in little endian order:\t{}",
39 as_u32
40 );
41
42 buff.set_position(0);
43 let as_u32 = buff.read_u32::<BigEndian>().expect("Failed to
read bytes");
44 println!("First four bytes as u32 in big endian order:\t{}",
as_u32);
45
46 println!("Before appending: {:?}", buff);
47 buff.seek(SeekFrom::End(0)).expect("Failed to seek end");
48 buff.write_f32::<LittleEndian>(-33.4)
49 .expect("Failed to write to end");
50 println!("After appending: {:?}", buff);
51
52 // Read a sequence of bytes into another buffer
53 let mut read_buffer = [0; 5];
54 buff.set_position(0);
55 buff.read_u16_into::<LittleEndian>(&mut read_buffer)
56 .expect("Failed to read all bytes");
57 println!(
58 "All bytes as u16s in little endian order: {:?}",
59 read_buffer
60 );
61 }
它是如何工作的...
首先,我们需要一个二进制源。在我们的例子中,我们简单地使用一个向量。然后,我们将它包装到一个 Cursor 中,因为它为我们提供了一个 Seek 实现和一些方便的方法。
Cursor 有一个内部位置计数器,用于跟踪我们此刻正在访问的字节。正如预期的那样,它从零开始。使用 read_u8 和 read_i8,我们可以将当前字节读取为无符号或带符号的数字。这将使位置前进一个字节。这两个函数做的是同一件事,但返回的类型不同。
您注意到我们没有使用 {:b} 作为格式化参数 [11] 来打印返回的字节吗?
println!("first byte in binary: {:b}", first_byte);
通过这样做,我们告诉底层的 format! 宏将我们的字节解释为二进制,这就是为什么它会打印 10 而不是 2。如果您愿意,尝试在我们的其他打印调用中将 {} 替换为 {:b} 并比较结果。
当前位置可以通过 position() [25] 读取,并通过 set_position() 设置。您还可以使用我们在上一道菜谱中介绍的更详细的 Seek API 来操作您的位置 [30]。当使用 SeekFrom::End 时,请记住这不会从末尾开始倒计数。例如,SeekFrom::End(1) 将指向缓冲区末尾之后的一个字节,而不是之前。这种行为是这样定义的,因为,也许有些令人惊讶,越过缓冲区是合法的。这在写入时可能很有用,因为它将简单地用零填充缓冲区末尾和光标位置之间的空间。
当处理多个字节时,您需要通过类型注解指定字节序。读取或写入将根据读取或写入的字节数前进位置,这就是为什么在我们的示例代码中,我们需要频繁地使用 set_position(0) 重置位置。请注意,当您写入到末尾时,您总是会简单地扩展缓冲区 [48]。
如果您知道您想要读取一个非常特定的字节数,比如在解析一个定义良好的协议时,您可以通过提供一个固定大小的数组并通过在 read 后添加 _into 来填充它来实现这一点,如下所示:
// Read exactly five bytes
let mut read_buffer = [0; 5];
buff.read_u16_into::<LittleEndian>(&mut read_buffer).expect("Failed to fill buffer");
在这样做的时候,如果缓冲区没有完全填满,读取将返回一个错误,在这种情况下,其内容是未定义的。
还有更多...
在字节序 crate 中有各种别名,可以简化您的字节序注释。BE别名,表示大端,和LE别名,表示小端,如果您不想输入太多,它们很有用。另一方面,如果您经常忘记在哪里使用哪种字节序,您可以使用NativeEndian,它会设置为操作系统的默认字节序,以及NetworkEndian,用于大端。
要使用它们,您必须像这样将它们拖入作用域:
use byteorder::{BE, LE, NativeEndian, NetworkEndian};
处理二进制文件
我们现在将结合在前两章中学到的知识,以便解析和编写二进制文件。当你计划实现自定义、手动处理文件类型,如 PDF、种子文件和 ZIP 文件时,这将变得至关重要。在设计适用于您自己用例的自定义文件类型时,它也会派上用场。
如何做...
-
如果您在上一个章节中还没有做,请打开之前为您生成的
Cargo.toml文件。 -
在
[dependencies]部分下,添加以下行:
byteorder = "1.1.0"
-
如果您愿意,您可以访问 byteorder 的 crates.io 页面(
crates.io/crates/byteorder),检查最新版本,并使用那个版本。 -
在
bin文件夹中,创建一个名为binary_files.rs的文件。 -
添加以下代码,并用
cargo run --bin binary_files运行它:
1 extern crate byteorder;
2 use byteorder::{ByteOrder, ReadBytesExt, WriteBytesExt, BE,
LE};
3 use std::fs::File;
4 use std::io::{self, BufReader, BufWriter, Read};
5 use std::io::prelude::*;
6
7
8 fn main() {
9 let path = "./bar.bin";
10 write_dummy_protocol(path).expect("Failed write file");
11 let payload = read_protocol(path).expect("Failed to read
file");
12 print!("The protocol contained the following payload: ");
13 for num in payload {
14 print!("0x{:X} ", num);
15 }
16 println!();
17 }
- 创建一个二进制文件:
19 // Write a simple custom protocol
20 fn write_dummy_protocol(path: &str) -> io::Result<()> {
21 let file = File::create(path)?;
22 let mut buf_writer = BufWriter::new(file);
23
24 // Let's say our binary file starts with a magic string
25 // to show readers that this is our protocoll
26 let magic = b"MyProtocol";
27 buf_writer.write_all(magic)?;
28
29 // Now comes another magic value to indicate
30 // our endianness
31 let endianness = b"LE";
32 buf_writer.write_all(endianness)?;
33
34 // Let's fill it with two numbers in u32
35 buf_writer.write_u32::<LE>(0xDEAD)?;
36 buf_writer.write_u32::<LE>(0xBEEF)?;
37
38 Ok(())
39 }
- 读取和解析文件:
42 fn read_protocol(path: &str) -> io::Result<Vec<u32>> {
43 let file = File::open(path)?;
44 let mut buf_reader = BufReader::new(file);
45
46 // Our protocol has to begin with a certain string
47 // Namely "MyProtocol", which is 10 bytes long
48 let mut start = [0u8; 10];
49 buf_reader.read_exact(&mut start)?;
50 if &start != b"MyProtocol" {
51 return Err(io::Error::new(
52 io::ErrorKind::Other,
53 "Protocol didn't start with the expected magic string",
54 ));
55 }
56
57 // Now comes the endianness indicator
58 let mut endian = [0u8; 2];
59 buf_reader.read_exact(&mut endian)?;
60 match &endian {
61 b"LE" => read_protocoll_payload::<LE, _>(&mut buf_reader),
62 b"BE" => read_protocoll_payload::<BE, _>(&mut buf_reader),
63 _ => Err(io::Error::new(
64 io::ErrorKind::Other,
65 "Failed to parse endianness",
66 )),
67 }
68 }
69
70 // Read as much of the payload as possible
71 fn read_protocoll_payload<E, R>(reader: &mut R) ->
io::Result<Vec<u32>>
72 where
73 E: ByteOrder,
74 R: ReadBytesExt,
75 {
76 let mut payload = Vec::new();
77 const SIZE_OF_U32: usize = 4;
78 loop {
79 let mut raw_payload = [0; SIZE_OF_U32];
80 // Read the next 4 bytes
81 match reader.read(&mut raw_payload)? {
82 // Zero means we reached the end
83 0 => return Ok(payload),
84 // SIZE_OF_U32 means we read a complete number
85 SIZE_OF_U32 => {
86 let as_u32 = raw_payload.as_ref().read_u32::<E>()?;
87 payload.push(as_u32)
88 }
89 // Anything else means the last element was not
90 // a valid u32
91 _ => {
92 return Err(io::Error::new(
93 io::ErrorKind::UnexpectedEof,
94 "Payload ended unexpectedly",
95 ))
96 }
97 }
98 }
99 }
它是如何工作的...
为了演示如何读取和写入二进制文件,我们将创建一个简单的自定义二进制协议。它将以所谓的魔数开始,即某个硬编码的值。我们的魔数将是MyProtocol字符串的二进制表示。我们可以在字符串前加上b来告诉 Rust,我们希望文本以二进制切片(&[u8])的形式表示,而不是字符串切片(&str) [26]。
许多协议和文件以魔数开始,以指示它们是什么。例如,.zip文件的内部头以魔数十六进制数0x50和0x4B开始。这些代表 ASCII 中的首字母PH,是创建者 Phil Katz 名字的缩写。另一个例子是 PDF;它以0x25、0x50、0x44和0x46开始,代表PDF%,后面跟着版本号。
之后,我们接着使用LE或BE的二元表示来告诉读取者其余数据的字节序 [31]。最后,我们有有效载荷,它只是任意数量的u32数字,按照上述字节序编码 [35 和 36]。通过在我们的数字前加上0x,我们告诉 Rust 将其视为十六进制数,并将其转换为十进制。因此,Rust 将0xDEAD视为与57005相同的值。
让我们把所有这些都放在一起,并写入一个包含MyProtocolLE5700548879的二进制文件。根据我们的协议,我们还可以创建其他文件,例如MyProtocolBE92341739241842518425或MyProtocolLE0000等。
如果你阅读了之前的食谱,write_dummy_protocol应该很容易理解。我们使用标准库中的古老的好write_all来写入我们的二进制文本,以及byteorder中的write_u32来写入需要端序的值。
协议的读取被分为read_protocol和read_protocol_payload函数。第一个通过读取魔数来验证协议的有效性,然后调用后者,后者读取剩余的数字作为有效载荷。
我们如下验证魔数:
-
正如我们所知,我们知道了用于魔数的确切大小,因此准备相应大小的缓冲区。
-
用同样数量的字节填充它们。
-
将字节与预期的魔数进行比较。
-
如果它们不匹配,则返回错误。
解析完两个魔数后,我们可以解析有效载荷中包含的实际数据。记住,我们将其定义为任意数量的32位(= 4字节)长,无符号数。为了解析它们,我们将反复读取最多四个字节到名为raw_payload的缓冲区中。然后我们将检查实际读取的字节数。在我们的情况下,这个数字可以有以下三种形式,正如我们的match所优雅地展示的那样。
我们感兴趣的第一个值是零,这意味着没有更多的字节可以读取,也就是说,我们已经到达了末尾。在这种情况下,我们可以返回我们的有效载荷:
// Zero means we reached the end
0 => return Ok(payload),
第二个值是SIZE_OF_U32,我们之前将其定义为四。接收到这个值意味着我们的读取器已经成功地将四个字节读入一个四字节长的缓冲区中。这意味着我们已经成功读取了一个值!让我们将其解析为u32并将其推送到我们的payload向量中:
// SIZE_OF_U32 means we read a complete number
SIZE_OF_U32 => {
let as_u32 = raw_payload.as_ref().read_u32::<E>()?;
payload.push(as_u32)
}
我们必须在我们的缓冲区上调用as_ref(),因为固定大小的数组没有实现Read。由于切片实现了该特性和数组引用隐式可转换为切片,我们工作在raw_payload的引用上。
我们可以预期的第三个和最后一个值是除了零或四之外的所有值。在这种情况下,读取器无法读取四个字节,这意味着我们的缓冲区在u32之外结束,并且格式不正确。我们可以通过返回错误来对此做出反应:
// Anything else means the last element was not
// a valid u32
_ => {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"Payload ended unexpectedly",
))
}
}
还有更多...
当读取格式不正确的协议时,我们重用io::ErrorKind来显示到底出了什么问题。在第六章的食谱中,处理错误,你将学习如何提供自己的错误以更好地分离你的失败区域。如果你想,你可以现在阅读它们,然后返回这里来改进我们的代码。
需要推送到自己的变体的错误是:
-
InvalidStart -
InvalidEndianness -
UnexpectedEndOfPayload
代码的另一个改进是将我们所有的字符串,即MyProtocol、LE和BE,放入它们自己的常量中,如下行所示:
const PROTOCOL_START: &[u8] = b"MyProtocol";
在这个配方和某些其他配方中提供的代码不使用很多常量,因为它们在打印形式中证明是有些难以理解的。然而,在实际代码库中,务必始终将你发现自己复制粘贴的字符串放入自己的常量中!
参见
- 在第六章中提供用户定义的错误类型配方,处理错误
压缩和解压缩数据
在今天这个网站臃肿和每天出现新的 Web 框架的时代,许多网站感觉比它们实际使用(和应该使用)的要慢得多。减轻这种情况的一种方法是在发送资源之前压缩它们,然后在收到时解压缩。这已经成为(通常被忽视的)网络标准。为此,这个配方将教你如何使用不同的算法压缩和解压缩任何类型的数据。
如何做到...
按照以下步骤操作:
-
打开之前为你生成的
Cargo.toml文件。 -
在
[dependencies]下添加以下行:flate2 = "0.2.20"如果你想,你可以去
flate2的 crates.io 页面(crates.io/crates/flate2)查看最新版本,并使用那个版本。 -
在
bin文件夹中,创建一个名为compression.rs的文件。 -
添加以下代码,并用
cargo run --bin compression运行它:
1 extern crate flate2;
2
3 use std::io::{self, SeekFrom};
4 use std::io::prelude::*;
5
6 use flate2::{Compression, FlateReadExt};
7 use flate2::write::ZlibEncoder;
8 use flate2::read::ZlibDecoder;
9
10 use std::fs::{File, OpenOptions};
11 use std::io::{BufReader, BufWriter, Read};
12
13 fn main() {
14 let bytes = b"I have a dream that one day this nation will
rise up, \
15 and live out the true meaning of its creed";
16 println!("Original: {:?}", bytes.as_ref());
17 // Conpress some bytes
18 let encoded = encode_bytes(bytes.as_ref()).expect("Failed to
encode bytes");
19 println!("Encoded: {:?}", encoded);
20 // Decompress them again
21 let decoded = decode_bytes(&encoded).expect("Failed to decode
bytes");
22 println!("Decoded: {:?}", decoded);
23
24 // Open file to compress
25 let original = File::open("ferris.png").expect("Failed to
open file");
26 let mut original_reader = BufReader::new(original);
27
28 // Compress it
29 let data = encode_file(&mut original_reader).expect("Failed
to encode file");
30
31 // Write compressed file to disk
32 let encoded = OpenOptions::new()
33 .read(true)
34 .write(true)
35 .create(true)
36 .open("ferris_encoded.zlib")
37 .expect("Failed to create encoded file");
38 let mut encoded_reader = BufReader::new(&encoded);
39 let mut encoded_writer = BufWriter::new(&encoded);
40 encoded_writer
41 .write_all(&data)
42 .expect("Failed to write encoded file");
43
44
45 // Jump back to the beginning of the compressed file
46 encoded_reader
47 .seek(SeekFrom::Start(0))
48 .expect("Failed to reset file");
49
50 // Decompress it
51 let data = decode_file(&mut encoded_reader).expect("Failed to
decode file");
52
53 // Write the decompressed file to disk
54 let mut decoded =
File::create("ferris_decoded.png").expect("Failed to create
decoded file");
55 decoded
56 .write_all(&data)
57 .expect("Failed to write decoded file");
58 }
- 这些是执行实际编码和解码的函数:
61 fn encode_bytes(bytes: &[u8]) -> io::Result<Vec<u8>> {
62 // You can choose your compression algorithm and it's
efficiency
63 let mut encoder = ZlibEncoder::new(Vec::new(),
Compression::Default);
64 encoder.write_all(bytes)?;
65 encoder.finish()
66 }
67
68 fn decode_bytes(bytes: &[u8]) -> io::Result<Vec<u8>> {
69 let mut encoder = ZlibDecoder::new(bytes);
70 let mut buffer = Vec::new();
71 encoder.read_to_end(&mut buffer)?;
72 Ok(buffer)
73 }
74
75
76 fn encode_file(file: &mut Read) -> io::Result<Vec<u8>> {
77 // Files have a built-in encoder
78 let mut encoded = file.zlib_encode(Compression::Best);
79 let mut buffer = Vec::new();
80 encoded.read_to_end(&mut buffer)?;
81 Ok(buffer)
82 }
83
84 fn decode_file(file: &mut Read) -> io::Result<Vec<u8>> {
85 let mut buffer = Vec::new();
86 // Files have a built-in decoder
87 file.zlib_decode().read_to_end(&mut buffer)?;
88 Ok(buffer)
89 }
它是如何工作的...
大多数main函数的工作只是重复你从上一章学到的内容。真正的操作发生在下面。在encode_bytes中,你可以看到如何使用编码器。你可以尽可能多地写入它,并在完成后调用finish。
flate2为你提供了几个压缩选项。你可以通过传递的Compression实例来选择你的压缩强度:
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::Default);
Default是在速度和大小之间的折衷。你的其他选项是Best、Fast和None。此外,你可以指定使用的编码算法。flate2支持 zlib,我们在本配方中使用,gzip 和平滑的 deflate。如果你想使用除 zlib 之外的算法,只需将所有提及它的地方替换为另一个支持的算法。例如,如果你想将前面的代码重写为使用 gzip,它看起来会像这样:
use flate2::write::GzEncoder;
let mut encoder = GzEncoder::new(Vec::new(), Compression::Default);
有关特定编码器如何调用的完整列表,请访问flate2的文档docs.rs/flate2/。
由于人们通常会倾向于压缩或解压缩整个文件而不是字节缓冲区,因此有一些方便的方法可以做到这一点。实际上,它们是在实现了Read接口的每个类型上实现的,这意味着你也可以在BufReader和许多其他类型上使用它们。encode_file和decode_file使用以下行中的zlib:
let mut encoded = file.zlib_encode(Compression::Best);
file.zlib_decode().read_to_end(&mut buffer)?;
这同样适用于 gzip 和 deflate 算法。
在我们的例子中,我们正在压缩和解压缩 ferris.png,这是 Rust 面具的图片:

你可以在 GitHub 仓库 github.com/SirRade/rust-standard-library-cookbook 中找到它,或者你可以使用任何你想要的文件。如果你想要验证压缩效果,你可以查看原始文件、压缩文件和解压缩文件,以检查压缩文件有多小,以及原始文件和解压缩文件是否相同。
还有更多...
当前的 encode_something 和 decode_something 函数被设计得尽可能简单易用。然而,尽管我们可以直接将数据管道输入到写入器中,它们仍然通过分配 Vec<u8> 来浪费一些性能。当编写库时,通过这种方式添加方法,为用户提供两种可能性会很好:
use std::io::Write;
fn encode_file_into(file: &mut Read, target: &mut Write) -> io::Result<()> {
// Files have a built-in encoder
let mut encoded = file.zlib_encode(Compression::Best);
io::copy(&mut encoded, target)?;
Ok(())
}
用户可以像这样调用它们:
// Compress it
encode_file_into(&mut original_reader, &mut encoded_writer)
.expect("Failed to encode file");
遍历文件系统
到目前为止,我们总是为我们的代码提供某个文件的静态位置。然而,现实世界很少如此可预测,当处理散布在不同文件夹中的数据时,需要进行一些挖掘。
walkdir 通过将操作系统的文件系统表示的复杂性和不一致性统一到一个公共 API 下,帮助我们抽象化这些问题,我们将在本配方中学习这个 API。
准备工作
这个配方大量使用了迭代器来操作数据流。如果你还不熟悉它们或需要快速复习,你应该在继续之前阅读第二章 使用集合 中的 将集合作为迭代器访问 部分。
如何做...
-
打开为你生成的
Cargo.toml文件。 -
在
[dependencies]下,添加以下行:
walkdir = "2.0.1"
-
如果你想,你可以去
walkdir的 crates.io 页面(crates.io/crates/walkdir)查看最新版本,并使用那个版本。 -
在
bin文件夹中,创建一个名为traverse_files.rs的文件。 -
添加以下代码,并使用
cargo run --bin traverse_files运行它:
1 extern crate walkdir;
2 use walkdir::{DirEntry, WalkDir};
3
4 fn main() {
5 println!("All file paths in this directory:");
6 for entry in WalkDir::new(".") {
7 if let Ok(entry) = entry {
8 println!("{}", entry.path().display());
9 }
10 }
11
12 println!("All non-hidden file names in this directory:");
13 WalkDir::new("../chapter_three")
14 .into_iter()
15 .filter_entry(|entry| !is_hidden(entry)) // Look only at
non-hidden enthries
16 .filter_map(Result::ok) // Keep all entries we have access to
17 .for_each(|entry| {
18 // Convert the name returned by theOS into a Rust string
19 // If there are any non-UTF8 symbols in it, replace them
with placeholders
20 let name = entry.file_name().to_string_lossy();
21 println!("{}", name)
22 });
23
24 println!("Paths of all subdirectories in this directory:");
25 WalkDir::new(".")
26 .into_iter()
27 .filter_entry(is_dir) // Look only at directories
28 .filter_map(Result::ok) // Keep all entries we have
access to
29 .for_each(|entry| {
30 let path = entry.path().display();
31 println!("{}", path)
32 });
33
34 let are_any_readonly = WalkDir::new("..")
35 .into_iter()
36 .filter_map(Result::ok) // Keep all entries we have
access to
37 .filter(|e| has_file_name(e, "vector.rs")) // Get the
ones with a certain name
38 .filter_map(|e| e.metadata().ok()) // Get metadata if the
OS allows it
39 .any(|e| e.permissions().readonly()); // Check if at
least one entry is readonly
40 println!(
41 "Are any the files called 'vector.rs' readonly? {}",
42 are_any_readonly
43 );
44
45 let total_size = WalkDir::new(".")
46 .into_iter()
47 .filter_map(Result::ok) // Keep all entries we have access
to
48 .filter_map(|entry| entry.metadata().ok()) // Get metadata
if supported
49 .filter(|metadata| metadata.is_file()) // Keep all files
50 .fold(0, |acc, m| acc + m.len()); // Accumulate sizes
51
52 println!("Size of current directory: {} bytes", total_size);
53 }
- 现在,让我们来看看这个配方中使用的谓词:
55 fn is_hidden(entry: &DirEntry) -> bool {
56 entry
57 .file_name()
58 .to_str()
59 .map(|s| s.starts_with('.'))
60 .unwrap_or(false) // Return false if the filename is
invalid UTF8
61 }
62
63 fn is_dir(entry: &DirEntry) -> bool {
64 entry.file_type().is_dir()
65 }
66
67 fn has_file_name(entry: &DirEntry, name: &str) -> bool {
68 // Check if file name contains valid unicode
69 match entry.file_name().to_str() {
70 Some(entry_name) => entry_name == name,
71 None => false,
72 }
73 }
它是如何工作的...
walkdir 由三个重要的类型组成:
-
WalkDir:一个构建器(参见第一章 使用构建器模式 部分,学习基础知识),用于你的目录遍历器 -
IntoIter:由构建器创建的迭代器 -
DirEntry:表示单个文件夹或文件
如果你只想操作根文件夹下的所有条目列表,例如在行 [6] 的第一个例子中,你可以隐式地直接将 WalkDir 作为 DirEntry 不同实例的迭代器使用:
for entry in WalkDir::new(".") {
if let Ok(entry) = entry {
println!("{}", entry.path().display());
}
}
如你所见,迭代器并不直接给你一个 DirEntry,而是一个 Result。这是因为有些情况下访问文件或文件夹可能会很困难。例如,操作系统可能禁止你读取文件夹的内容,隐藏其中的文件。或者一个符号链接,你可以通过在 WalkDir 实例上调用 follow_links(true) 来启用它,它可能指向父目录,这可能导致无限循环。
对于这个配方中的错误,我们的解决方案策略很简单——我们只是忽略它们,并继续处理没有报告任何问题的其他条目。
当你提取实际的条目时,它可以告诉你很多关于它自己的信息。其中之一就是它的路径。记住,尽管 .path() [8] 并不仅仅返回路径作为一个字符串。实际上,它返回一个本地的 Rust Path 结构体,可以用于进一步分析。例如,你可以通过在它上面调用 .extension() 来读取文件路径的扩展名。或者你可以通过调用 .parent() 来获取它的父目录。你可以通过探索 doc.rust-lang.org/std/path/struct.Path.html 上的 Path 文档来自由探索可能性。在我们的案例中,我们只是通过调用 .display() 来将其显示为一个简单的字符串。
当我们使用 into_iter() 显式地将 WalkDir 转换为迭代器时,我们可以访问一个其他迭代器没有的特殊方法:filter_entry。它是对 filter 的优化,因为它在遍历期间被调用。当它的谓词对一个目录返回 false 时,遍历者根本不会进入该目录!这样,在遍历大型文件系统时,你可以获得很多性能提升。在配方中,我们在寻找非隐藏文件 [15] 时使用了它。如果你只需要操作文件而永远不操作目录,你应该使用普通的 filter。
我们根据 Unix 习惯定义 隐藏文件 为所有以点开头的目录和文件。因此,它们有时也被称为 点文件。
在这两种情况下,你的过滤都需要一个谓词。它们通常被放在自己的函数中,以保持简单性和可重用性。
注意,walkdir 并不仅仅给我们一个普通的字符串形式的文件名。相反,它返回一个 OsStr。这是一种特殊的字符串,当 Rust 直接与操作系统交互时,Rust 会使用这种字符串。这种类型的存在是因为一些操作系统允许文件名中包含无效的 UTF-8。在 Rust 中查看这类文件时,你有两种选择——让 Rust 尝试将它们转换为 UTF-8,并用 Unicode 替换字符(�)替换所有无效字符,或者自行处理错误。你可以通过在 OsStr 上调用 to_string_lossy 来选择第一种方法 [20]。第二种方法可以通过调用 to_str 并检查返回的 Option 来访问,就像我们在 has_file_name 中做的那样,我们只是简单地丢弃无效的名称。
在这道菜谱中,你可以看到一个精彩的例子,说明何时选择for_each方法调用(在第一章的将集合作为迭代器访问部分讨论过,学习基础知识,处理集合)而不是for循环——我们的大部分迭代器调用都是链在一起的,因此for_each调用可以自然地链接到迭代器中。
还有更多...
如果你计划仅在 Unix 上发布你的应用程序,你可以通过.metadata().unwrap().permissions()调用在条目上访问额外的权限。具体来说,你可以通过调用.mode()来查看确切的st_mode位,并通过调用set_mode()使用一组新的位来更改它们。
相关内容
使用 glob 模式查找文件
如你所注意到的,使用walkdir根据文件名过滤文件有时可能有点笨拙。幸运的是,你可以通过使用globcrate 来大大简化这一点,它将 Unix 中的标题模式直接带入 Rust。
如何做到这一点...
按以下步骤操作:
-
打开之前为你生成的
Cargo.toml文件。 -
在
[dependencies]下添加以下行:
glob = "0.2.11"
-
如果你愿意,你可以访问 glob 的 crates.io 页面(
crates.io/crates/glob),查看最新版本并使用它。 -
在
bin文件夹中,创建一个名为glob.rs的文件。 -
添加以下代码,并使用
cargo run --bin glob运行它:
1 extern crate glob;
2 use glob::{glob, glob_with, MatchOptions};
3
4 fn main() {
5 println!("All all Rust files in all subdirectories:");
6 for entry in glob("**/*.rs").expect("Failed to read glob
pattern") {
7 match entry {
8 Ok(path) => println!("{:?}", path.display()),
9 Err(e) => println!("Failed to read file: {:?}", e),
10 }
11 }
12
13 // Set the glob to be case insensitive and ignore hidden
files
14 let options = MatchOptions {
15 case_sensitive: false,
16 require_literal_leading_dot: true,
17 ..Default::default()
18 };
19
20
21 println!(
22 "All files that contain the word \"ferris\" case
insensitive \
23 and don't contain an underscore:"
24 );
25 for entry in glob_with("*Ferris[!_]*",
&options).expect("Failed to read glob pattern") {
26 if let Ok(path) = entry {
27 println!("{:?}", path.display())
28 }
29 }
30 }
它是如何工作的...
这个 crate 相当小且简单。使用glob(...),你可以通过指定一个glob模式来创建一个迭代器,遍历所有匹配的文件。如果你不熟悉它们,但记得之前(在第一章的使用正则表达式查询部分中)的 regex 菜谱(学习基础知识),可以将它们视为非常简化的正则表达式,主要用于文件名。其语法在维基百科上有很好的描述:en.wikipedia.org/wiki/Glob_(programming)。
与之前的WalkDir一样,glob迭代器返回一个Result,因为程序可能没有权限读取文件系统条目。在Result中包含一个Path,我们也在上一道菜谱中提到了它。如果你想读取文件内容,请参考本章的第一道菜谱,它涉及文件操作。
使用glob_with,你可以指定一个MatchOptions实例来更改glob搜索文件的方式。你可以切换的最有用的选项是:
-
case_sensitive:默认情况下是启用的,它控制是否应该将小写字母(abcd)和大写字母(ABCD)视为不同或相同。 -
require_literal_leading_dot:默认情况下是禁用的,当设置时,禁止通配符匹配文件名中的前导点。这用于您想忽略用户的隐藏文件时。
您可以在MatchOption的文档中查看其余选项:doc.rust-lang.org/glob/glob/struct.MatchOptions.html.
如果您已经设置了您关心的选项,您可以通过使用在第一章学习基础知识中提供默认实现部分讨论的..Default::default()更新语法,将剩余的选项保留为默认值。
参见
- 在第一章学习基础知识中的使用正则表达式查询和提供默认实现食谱
第四章:序列化
在本章中,我们将介绍以下食谱:
-
处理 CSV
-
使用 Serde 进行序列化基础
-
处理 TOML
-
处理 JSON
-
动态构建 JSON
简介
重新发明轮子是没有意义的。许多程序已经提供了大量的功能,它们很乐意与您的程序交互。当然,如果您无法与他们通信,这种提供就毫无价值。
在本章中,我们将探讨 Rust 生态系统中最重要的格式,以便您能够轻松与其他服务进行交流。
处理 CSV
存储简单且小型数据集的一个好方法就是 CSV。如果您正在使用像 Microsoft Excel 这样的电子表格应用程序,这个格式也很感兴趣,因为它们对导入和导出各种 CSV 格式有很好的支持。
入门
您可能已经知道 CSV 是什么,但稍微复习一下也无妨。
该格式的想法是将值表的所有行都写下来作为记录。在记录内部,每个列项都写下来,并用逗号分隔。这就是该格式名称的由来——逗号分隔值。
让我们做一个例子。在下面的代码中,我们将编写一个 CSV 文件,比较太阳系中各种行星与我们的地球。半径、距离太阳和重力为1表示与地球完全相同。以表格形式写出,我们的值看起来是这样的:
| name | radius | distance_from_sun | gravity |
|---|---|---|---|
| 水星 | 0.38 | 0.47 | 0.38 |
| 金星 | 0.95 | 0.73 | 0.9 |
| 地球 | 1 | 1 | 1 |
| 火星 | 0.53 | 1.67 | 0.38 |
| 木星 | 11.21 | 5.46 | 2.53 |
| 土星 | 9.45 | 10.12 | 1.07 |
| 天王星 | 4.01 | 20.11 | 0.89 |
| 海王星 | 3.88 | 30.33 | 1.14 |
将每一行取出来,用逗号分隔值,将它们各自放在单独的一行上,您就得到了 CSV 文件:
name,radius,distance_from_sun,gravity
Mercury,0.38,0.47,0.38
Venus,0.95,0.73,0.9
Earth,1,1,1
Mars,0.53,1.67,0.38
Jupiter,11.21,5.46,2.53
Saturn,9.45,10.12,1.07
Uranus,4.01,20.11,0.89
Neptune,3.88,30.33,1.14
如您所见,标题(planet,radius,distance_from_sun,gravity)简单地写成第一条记录。
如何操作...
-
使用
cargo new chapter_four创建一个 Rust 项目,在本章中对其进行操作。 -
导航到新创建的
chapter_four文件夹。在本章的剩余部分,我们将假设您的命令行当前位于此目录。 -
在
src文件夹内,创建一个名为bin的新文件夹。 -
删除生成的
lib.rs文件,因为我们不是创建一个库。 -
打开之前为您生成的
Cargo.toml文件。 -
在
[dependencies]下添加以下行:
csv = "1.0.0-beta.5"
-
如果您愿意,可以访问
csv的 crates.io 页面(crates.io/crates/csv),检查最新版本并使用它。 -
在
src/bin文件夹中,创建一个名为csv.rs的文件。 -
添加以下代码,并使用
cargo run --bin csv运行它:
1 extern crate csv;
2
3 use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom,
Write};
4 use std::fs::OpenOptions;
5
6 fn main() {
7 let file = OpenOptions::new()
8 .read(true)
9 .write(true)
10 .create(true)
11 .open("solar_system_compared_to_earth.csv")
12 .expect("failed to create csv file");
13
14 let buf_writer = BufWriter::new(&file);
15 write_records(buf_writer).expect("Failed to write csv");
16
17 let mut buf_reader = BufReader::new(&file);
18 buf_reader
19 .seek(SeekFrom::Start(0))
20 .expect("Failed to jump to the beginning of the csv");
21 read_records(buf_reader).expect("Failed to read csv");
22 }
23
24 fn write_records<W>(writer: W) -> csv::Result<()>
25 where
26 W: Write,
27 {
28 let mut wtr = csv::Writer::from_writer(writer);
29
30 // The header is just a normal record
31 wtr.write_record(&["name", "radius", "distance_from_sun",
32 "gravity"])?;
33
34 wtr.write_record(&["Mercury", "0.38", "0.47", "0.38"])?;
35 wtr.write_record(&["Venus", "0.95", "0.73", "0.9"])?;
36 wtr.write_record(&["Earth", "1", "1", "1"])?;
37 wtr.write_record(&["Mars", "0.53", "1.67", "0.38"])?;
38 wtr.write_record(&["Jupiter", "11.21", "5.46", "2.53"])?;
39 wtr.write_record(&["Saturn", "9.45", "10.12", "1.07"])?;
40 wtr.write_record(&["Uranus", "4.01", "20.11", "0.89"])?;
41 wtr.write_record(&["Neptune", "3.88", "30.33", "1.14"])?;
42 wtr.flush()?;
43 Ok(())
44 }
45
46 fn read_records<R>(reader: R) -> csv::Result<()>
47 where
48 R: Read,
49 {
50 let mut rdr = csv::Reader::from_reader(reader);
51 println!("Comparing planets in the solar system with the
52 earth");
53 println!("where a value of '1' means 'equal to earth'");
54 for result in rdr.records() {
55 println!("-------");
56 let record = result?;
57 if let Some(name) = record.get(0) {
58 println!("Name: {}", name);
59 }
60 if let Some(radius) = record.get(1) {
61 println!("Radius: {}", radius);
62 }
63 if let Some(distance) = record.get(2) {
64 println!("Distance from sun: {}", distance);
65 }
66 if let Some(gravity) = record.get(3) {
67 println!("Surface gravity: {}", gravity);
68 }
69 }
70 Ok(())
71 }
它是如何工作的...
首先,我们准备我们的文件[9]及其OpenOptions,以便我们可以在文件上同时拥有read和write访问权限。您会记得这一点来自第三章,处理文件和文件系统;处理文本文件。
然后我们写入 CSV。我们通过将任何类型的Write包装在csv::Writer[30]中来完成此操作。然后您可以使用write_record来写入任何可以表示为&[u8]迭代器的数据类型。大多数情况下,这只是一个字符串数组。
在读取时,我们同样将一个Read包装在csv::Read中。records()方法返回一个Result的StringRecord迭代器。这样,您可以决定如何处理格式不正确的记录。在我们的例子中,我们简单地跳过它。最后,我们在一个记录上调用get()以获取某个字段。如果没有在指定的索引处有条目,或者它超出了范围,这将返回None。
还有更多...
如果您需要读取或写入自定义 CSV 格式,例如使用制表符而不是逗号作为分隔符的格式,您可以使用WriterBuilder和ReaderBuilder来自定义预期的格式。如果您计划使用 Microsoft Excel,请务必记住这一点,因为它在选择分隔符方面具有令人烦恼的区域不一致性(stackoverflow.com/questions/10140999/csv-with-comma-or-semicolon)。
当处理 CSV 和 Microsoft Excel 时,请小心,并在将其交给 Excel 之前清理您的数据。尽管 CSV 被定义为没有控制标识符的纯数据,但 Excel 在导入 CSV 时会解释并执行宏。关于由此可能打开的可能攻击向量,请参阅georgemauer.net/2017/10/07/csv-injection.html。
如果 Windows 应用程序拒绝接受csv默认使用的\n终止符,这也会很有用。在这种情况下,只需在构建者中指定以下代码即可使用 Windows 本地的\r\n终止符:
.terminator(csv::Terminator::CRLF)
csvcrate 允许您比本配方中显示的更灵活地操作您的数据。例如,您可以在StringRecord中动态插入新字段。我们故意不详细探讨这些可能性,因为 CSV 格式并不适用于这类数据操作。如果您需要做比简单的导入/导出更多的事情,您应该使用更合适的格式,例如 JSON,我们将在本章中也探讨这一点。
参见
-
使用构建者模式的配方在第一章,学习基础知识
-
处理文本文件的配方在第三章,处理文件和文件系统
使用 Serde 进行序列化基础知识
Rust 中所有序列化事物的事实标准是 Serde 框架。本章中的其他所有食谱都将部分使用它。为了让你熟悉 Serde 的做事方式,我们将使用它重写上一个食谱。在本章的后面部分,我们将详细了解 Serde 的工作原理,以便实现将数据反序列化到自定义格式。
如何做到...
-
打开之前为你生成的
Cargo.toml文件。 -
在
[dependencies]下,添加以下行:
serde = "1.0.24"
serde_derive = "1.0.24"
- 如果你还没有在上一个食谱中这样做,请添加以下行:
csv = "1.0.0-beta.5"
-
如果你想,可以去 Serde 的 crates.io 网页(
crates.io/crates/serde)、serde_derive(crates.io/crates/serde_derive)和 CSV(crates.io/crates/csv)查看最新版本,并使用这些版本。 -
在
bin文件夹中,创建一个名为serde_csv.rs的文件。 -
添加以下代码,并用
cargo run --bin serde_csv运行它:
1 extern crate csv;
2 extern crate serde;
3 #[macro_use]
4 extern crate serde_derive;
5
6 use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom,
Write};
7 use std::fs::OpenOptions;
8
9 #[derive(Serialize, Deserialize)]
10 struct Planet {
11 name: String,
12 radius: f32,
13 distance_from_sun: f32,
14 gravity: f32,
15 }
16
17 fn main() {
18 let file = OpenOptions::new()
19 .read(true)
20 .write(true)
21 .create(true)
22 .open("solar_system_compared_to_earth.csv")
23 .expect("failed to create csv file");
24
25 let buf_writer = BufWriter::new(&file);
26 write_records(buf_writer).expect("Failed to write csv");
27
28 let mut buf_reader = BufReader::new(&file);
29 buf_reader
30 .seek(SeekFrom::Start(0))
31 .expect("Failed to jump to the beginning of the csv");
32 read_records(buf_reader).expect("Failed to read csv");
33 }
34
35 fn write_records<W>(writer: W) -> csv::Result<()>
36 where
37 W: Write,
38 {
39 let mut wtr = csv::Writer::from_writer(writer);
40
41 // No need to specify a header; Serde creates it for us
42 wtr.serialize(Planet {
43 name: "Mercury".to_string(),
44 radius: 0.38,
45 distance_from_sun: 0.47,
46 gravity: 0.38,
47 })?;
48 wtr.serialize(Planet {
49 name: "Venus".to_string(),
50 radius: 0.95,
51 distance_from_sun: 0.73,
52 gravity: 0.9,
53 })?;
54 wtr.serialize(Planet {
55 name: "Earth".to_string(),
56 radius: 1.0,
57 distance_from_sun: 1.0,
58 gravity: 1.0,
59 })?;
60 wtr.serialize(Planet {
61 name: "Mars".to_string(),
62 radius: 0.53,
63 distance_from_sun: 1.67,
64 gravity: 0.38,
65 })?;
66 wtr.serialize(Planet {
67 name: "Jupiter".to_string(),
68 radius: 11.21,
69 distance_from_sun: 5.46,
70 gravity: 2.53,
71 })?;
72 wtr.serialize(Planet {
73 name: "Saturn".to_string(),
74 radius: 9.45,
75 distance_from_sun: 10.12,
76 gravity: 1.07,
77 })?;
78 wtr.serialize(Planet {
79 name: "Uranus".to_string(),
80 radius: 4.01,
81 distance_from_sun: 20.11,
82 gravity: 0.89,
83 })?;
84 wtr.serialize(Planet {
85 name: "Neptune".to_string(),
86 radius: 3.88,
87 distance_from_sun: 30.33,
88 gravity: 1.14,
89 })?;
90 wtr.flush()?;
91 Ok(())
92 }
93
94 fn read_records<R>(reader: R) -> csv::Result<()>
95 where
96 R: Read,
97 {
98 let mut rdr = csv::Reader::from_reader(reader);
99 println!("Comparing planets in the solar system with the
earth");
100 println!("where a value of '1' means 'equal to earth'");
101 for result in rdr.deserialize() {
102 println!("-------");
103 let planet: Planet = result?;
104 println!("Name: {}", planet.name);
105 println!("Radius: {}", planet.radius);
106 println!("Distance from sun: {}", planet.distance_from_sun);
107 println!("Surface gravity: {}", planet.gravity);
108 }
109 Ok(())
110 }
它是如何工作的...
本食谱中的代码读取和写入与上一个食谱完全相同的 CSV。唯一的区别是我们如何处理单个记录。Serde 通过允许我们使用普通的 Rust 结构体来帮助我们。我们唯一需要做的是从 Serialize 和 Deserialize [9] 中派生我们的 Planet 结构体。其余的将由 Serde 自动处理。
由于我们现在使用实际的 Rust 结构体来表示一个行星,我们通过调用 serialize 并传入一个结构体来创建记录,而不是像以前那样使用 write_record [42]。看起来是不是更易于阅读了?如果你认为示例变得有点冗长,你可以像在第一章中描述的那样,将实际对象创建隐藏在构造函数后面,学习基础知识;使用构造函数模式。
当读取 CSV 时,我们也不再需要手动访问 StringRecord 的字段。相反,deserialize() 返回一个已反序列化 Planet 对象的 Result 迭代器。再次,看看这变得多么易于阅读。
如你所猜,你应该尽可能使用 Serde,因为它通过提供可读性和编译时类型安全来帮助你尽早捕获可能的错误。
还有更多...
Serde 允许你通过注释你的字段来稍微调整序列化过程。例如,如果你无法通过写入 #[serde(default)] 在其声明上方为其提供一个标准值。在一个结构体中,它看起来像这样:
#[derive(Serialize, Deserialize)]
struct Foo {
bar: i32,
#[serde(default)]
baz: i32,
}
如果 baz 没有被解析,它的 Default::default 值(参见 第一章,学习基础知识;提供默认实现)将被使用。你可以用注解做的另一件有用的事情是改变期望的大小写约定。默认情况下,Serde 会期望 Rust 的 snake_case,但是,你可以通过用 #[serde(rename_all = "PascalCase")] 注解一个 struct 或 enum 来改变这一点。你可以在这样的 struct 上使用它:
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct Stats {
number_of_clicks: i32,
total_time_played: i32,
}
这将,而不是解析 number_of_clicks 和 total_time_played,期望 NumberOfClicks 和 TotalTimePlayed 键被调用。Serde 支持的其他可能的比 PascalCase 更多的案例约定包括小写、camelCase 和 SCREAMING_SNAKE_CASE。
有许多不同且有用的属性。如果你想,你可以在 serde.rs/attributes.html 上熟悉它们。
你可以使用 Serde 提供惯用的序列化和反序列化,然而,讨论所有最佳实践将涵盖一个单独章节。如果你想深入了解这些内容,Serde 在 serde.rs/data-format.html 上有一个很好的关于如何做到这一点的说明。
参见
-
使用构造函数模式 和 提供默认实现 的配方在 第一章,学习基础知识
-
在第三章的“处理文本文件”中工作,处理文件和文件系统*
与 TOML 一起工作
你喜欢 INI 文件的简洁性,但希望它们有正式的规范并且有更多功能吗?汤姆·普雷斯顿-沃纳(GitHub 和 Gravatar 等服务的创始人)也是这样想的。他创建了汤姆的明显、最小化语言,简称 TOML。这种相对较新的格式在新项目中越来越受欢迎。实际上,你现在已经多次使用它了:Cargo 的依赖关系在每一个项目的 Cargo.toml 文件中指定!
入门
在其核心,TOML 完全是关于 键值 对。这是你可以创建的最简单的 TOML 文件:
message = "Hello World"
在这里,键信息具有 "Hello World" 值。值也可以是一个数组:
messages: ["Hello", "World", "out", "there"]
一组键值被称为 表. 下面的 TOML 允许 smileys 表包含 happy 键和 ":)" 值,以及 sad 键和 ":(" 值:
[smileys]
happy = ":)"
sad = ":("
一个特别小的表可以 内联,也就是说,在一行中写出来。最后一个例子与以下内容完全相同:
smileys = { happy = ":)", sad = ":(" }
表可以通过用点分隔它们的名称来嵌套:
[servers]
[servers.production]
ip = "192.168.0.1"
[servers.beta]
ip = "192.169.0.2"
[servers.testing]
ip = "192.169.0.3"
TOML 的一个优点是,如果你需要指定更多信息,你可以将任何键转换为表。例如,Cargo 本身期望在声明依赖版本时这样做。例如,如果你想使用 rocket_contrib,这是流行的 Rust rocket 网络框架的辅助包,版本为 0.3.3,你会这样写:
[dependencies]
rocket_contrib = 0.3.3
然而,如果你想要指定 rocket_contrib 中要包含的确切功能,你需要将其写成 dependencies 的子表。以下 TOML 会告诉 Cargo 使用其 JSON 序列化功能:
[dependencies]
[dependencies.rocket_contrib]
version = "0.3.3"
default-features = false
features = ["json"]
TOML 带来的另一个优点是它的空白不重要,也就是说,你可以按任何方式缩进文件。你甚至可以通过以下方式添加注释:从行的开头开始:
# some comment
如果你想要进一步探索格式,TOML 语法的全部内容在 github.com/toml-lang/toml 中指定。
如何做到...
-
打开为你生成的
Cargo.toml文件 -
在
[dependencies]下添加以下行:
toml = "0.4.5"
- 如果你还没有这样做,也请添加以下行:
serde = "1.0.24"
serde_derive = "1.0.24"
-
如果你愿意,你可以访问 TOML (
crates.io/crates/toml)、Serde (crates.io/crates/serde) 和serde_derive(crates.io/crates/serde_derive) 的 crates.io 网页,检查最新版本并使用这些版本。 -
在
bin文件夹中创建一个名为toml.rs的文件 -
添加以下代码并使用
cargo run --bin toml运行它:
1 #[macro_use]
2 extern crate serde_derive;
3 extern crate toml;
4
5 use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom,
Write};
6 use std::fs::OpenOptions;
- 这些是我们将在整个配方中使用的结构:
8 #[derive(Serialize, Deserialize)]
9 struct Preferences {
10 person: Person,
11 language: Language,
12 privacy: Privacy,
13 }
14
15 #[derive(Serialize, Deserialize)]
16 struct Person {
17 name: String,
18 email: String,
19 }
20
21 #[derive(Serialize, Deserialize)]
22 struct Language {
23 display: String,
24 autocorrect: Option<Vec<String>>,
25 }
26
27 #[derive(Serialize, Deserialize)]
28 struct Privacy {
29 share_anonymous_statistics: bool,
30 public_name: bool,
31 public_email: bool,
32 }
- 准备一个新的文件并调用其他函数:
34 fn main() {
35 let file = OpenOptions::new()
36 .read(true)
37 .write(true)
38 .create(true)
39 .open("preferences.toml")
40 .expect("failed to create TOML file");
41
42 let buf_writer = BufWriter::new(&file);
43 write_toml(buf_writer).expect("Failed to write TOML");
44
45 let mut buf_reader = BufReader::new(&file);
46 buf_reader
47 .seek(SeekFrom::Start(0))
48 .expect("Failed to jump to the beginning of the TOML
file");
49 read_toml(buf_reader).expect("Failed to read TOML");
50 }
- 将我们的结构保存为 TOML 文件:
52 type SerializeResult<T> = Result<T, toml::ser::Error>;
53 fn write_toml<W>(mut writer: W) -> SerializeResult<()>
54 where
55 W: Write,
56 {
57 let preferences = Preferences {
58 person: Person {
59 name: "Jan Nils Ferner".to_string(),
60 email: "jn_ferner@hotmail.de".to_string(),
61 },
62 language: Language {
63 display: "en-GB".to_string(),
64 autocorrect: Some(vec![
65 "en-GB".to_string(),
66 "en-US".to_string(),
67 "de-CH".to_string(),
68 ]),
69 },
70 privacy: Privacy {
71 share_anonymous_statistics: false,
72 public_name: true,
73 public_email: true,
74 },
75 };
76
77 let toml = toml::to_string(&preferences)?;
78 writer
79 .write_all(toml.as_bytes())
80 .expect("Failed to write file");
81 Ok(())
82 }
读取我们刚刚创建的 TOML 文件:
84 type DeserializeResult<T> = Result<T, toml::de::Error>;
85 fn read_toml<R>(mut reader: R) -> DeserializeResult<()>
86 where
87 R: Read,
88 {
89 let mut toml = String::new();
90 reader
91 .read_to_string(&mut toml)
92 .expect("Failed to read TOML");
93 let preferences: Preferences = toml::from_str(&toml)?;
94
95 println!("Personal data:");
96 let person = &preferences.person;
97 println!(" Name: {}", person.name);
98 println!(" Email: {}", person.email);
99
100 println!("\nLanguage preferences:");
101 let language = &preferences.language;
102 println!(" Display language: {}", language.display);
103 println!(" Autocorrect priority: {:?}",
language.autocorrect);
104
105
106 println!("\nPrivacy settings:");
107 let privacy = &preferences.privacy;
108 println!(
109 " Share anonymous usage statistics: {}",
110 privacy.share_anonymous_statistics
111 );
112 println!(" Display name publically: {}",
privacy.public_name);
113 println!(" Display email publically: {}",
privacy.public_email);
114
115 Ok(())
116 }
它是如何工作的...
如往常一样,使用 Serde,我们首先需要声明我们计划使用的结构 [8 to 32]。
在序列化过程中,我们可以直接调用 Serde 的 to_string 方法,因为 TOML 重新导出它们 [77]。这返回一个 String,然后我们可以将其写入文件 [79]。Serde 的 from_str 也是如此,当类型注解时,它接受一个 &str 并将其转换为结构体。
还有更多...
你可能已经注意到,在阅读或编写此配方时我们没有使用 try-运算符(?)。这是因为函数期望的错误类型,se::Error [77] 和 de::Error[93],与 std::io::Error 不兼容。在第六章,处理错误;提供用户定义的错误类型中,我们将探讨如何通过返回包含其他提到的错误类型的自己的错误类型来避免这种情况。
在此配方中使用的 TOML crate 与 Cargo 本身使用的相同。如果你对 Cargo 如何解析其自己的 Cargo.toml 文件感兴趣,可以查看 github.com/rust-lang/cargo/blob/master/src/cargo/util/toml/mod.rs。
参见
-
在第三章,处理文件和文件系统中的处理文本文件配方
-
提供用户定义的错误类型 配方在 第六章,处理错误
处理 JSON
大多数 Web API 和许多本地 API 现在都使用 JSON。当设计供其他程序消费的数据时,它应该是你的首选格式,因为它轻量级、简单、易于使用和理解,并且在各种编程语言中都有出色的库支持,尤其是 JavaScript。
准备工作
JSON 是在大多数 Web 通信通过发送 XML 通过 Java 或 Flash 等浏览器插件完成的时候被创建的。这很麻烦,并且使得交换的信息相当庞大。JSLint 的创造者、著名作品《JavaScript: The Good Parts》的作者 Douglas Crockford 在 2000 年初决定,是时候有一个轻量级且易于与 JavaScript 集成的格式了。他将自己定位在 JavaScript 的小子集上,即它定义对象的方式,并在此基础上稍作扩展,形成了 JavaScript 对象表示法或 JSON。是的,你没有看错;JSON 不是 JavaScript 的子集,因为它接受 JavaScript 不接受的事物。你可以在 timelessrepo.com/json-isnt-a-javascript-subset. 上了解更多关于这一点。
这个故事悲哀的讽刺之处在于,今天我们已经回到了原点:我们最好的 Web 开发实践包括一个错综复杂的任务运行器、框架和转换器迷宫,这些在理论上都很不错,但最终却变成了一个庞大的混乱。但这又是另一个故事了。
JSON 建立在两种结构之上:
-
由
{和}包围的一组键值对,这被称为 对象,它本身也可以是一个值 -
被称为 数组 的由
[和]包围的值列表
这可能会让你想起我们之前讨论的 TOML 语法,你可能会问自己在什么情况下应该选择其中一个。答案是,当你的数据将要被工具自动读取时,JSON 是一个好的格式,而当你的数据旨在由人类手动读取和修改时,TOML 是出色的。
注意,JSON 不允许 注释。这很有道理,因为注释无论如何都是不可读的。
一个包含值、子对象和数组的 JSON 对象示例可能是以下内容:
{
name: "John",
age: 23,
pets: [
{
name: "Sparky",
species: "Dog",
age: 2
},
{
name: "Speedy",
species: "Turtle",
age: 47,
colour: "Green"
},
{
name: "Meows",
species: "Cat",
colour: "Orange"
}
]
}
我们将在下面的代码中编写和读取这个精确的示例。宠物定义的不一致性是有意为之的,因为许多 Web API 在某些情况下会省略某些键。
如何做到这一点...
-
打开之前为你生成的
Cargo.toml文件 -
在
[dependencies]下,添加以下行:
serde_json = "1.0.8"
- 如果你还没有这样做,也请添加以下行:
serde = "1.0.24"
serde_derive = "1.0.24"
-
如果你想的话,你可以访问
serde_json(crates.io/crates/serde_json)、Serde (crates.io/crates/serde) 和serde_derive(crates.io/crates/serde_derive) 的 crates.io 网页,检查最新版本并使用这些版本。 -
在
bin文件夹中,创建一个名为json.rs的文件 -
添加以下代码,并用
cargo run --bin json运行:
1 extern crate serde;
2 extern crate serde_json;
3
4 #[macro_use]
5 extern crate serde_derive;
6
7 use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom,
Write};
8 use std::fs::OpenOptions;
- 这些是我们将在整个食谱中使用的结构:
10 #[derive(Serialize, Deserialize)]
11 struct PetOwner {
12 name: String,
13 age: u8,
14 pets: Vec<Pet>,
15 }
16
17 #[derive(Serialize, Deserialize)]
18 struct Pet {
19 name: String,
20 species: AllowedSpecies,
21 // It is usual for many JSON keys to be optional
22 age: Option<u8>,
23 colour: Option<String>,
24 }
25
26 #[derive(Debug, Serialize, Deserialize)]
27 enum AllowedSpecies {
28 Dog,
29 Turtle,
30 Cat,
31 }
- 准备一个新的文件并调用其他函数:
33 fn main() {
34 let file = OpenOptions::new()
35 .read(true)
36 .write(true)
37 .create(true)
38 .open("pet_owner.json")
39 .expect("failed to create JSON file");
40
41 let buf_writer = BufWriter::new(&file);
42 write_json(buf_writer).expect("Failed to write JSON");
43
44 let mut buf_reader = BufReader::new(&file);
45 buf_reader
46 .seek(SeekFrom::Start(0))
47 .expect("Failed to jump to the beginning of the JSON
file");
48 read_json(buf_reader).expect("Failed to read JSON");
49 }
- 将我们的结构保存为 JSON 文件:
52 fn write_json<W>(mut writer: W) -> serde_json::Result<()>
53 where
54 W: Write,
55 {
56 let pet_owner = PetOwner {
57 name: "John".to_string(),
58 age: 23,
59 pets: vec![
60 Pet {
61 name: "Waldo".to_string(),
62 species: AllowedSpecies::Dog,
63 age: Some(2),
64 colour: None,
65 },
66 Pet {
67 name: "Speedy".to_string(),
68 species: AllowedSpecies::Turtle,
69 age: Some(47),
70 colour: Some("Green".to_string()),
71 },
72 Pet {
73 name: "Meows".to_string(),
74 species: AllowedSpecies::Cat,
75 age: None,
76 colour: Some("Orange".to_string()),
77 },
78 ],
79 };
80
81 let json = serde_json::to_string(&pet_owner)?;
82 writer
83 .write_all(json.as_bytes())
84 .expect("Failed to write file");
85 Ok(())
86 }
- 读取我们刚刚创建的 JSON 文件:
88 fn read_json<R>(mut reader: R) -> serde_json::Result<()>
89 where
90 R: Read,
91 {
92 let mut json = String::new();
93 reader
94 .read_to_string(&mut json)
95 .expect("Failed to read TOML");
96 let pet_owner: PetOwner = serde_json::from_str(&json)?;
97
98 println!("Pet owner profile:");
99 println!(" Name: {}", pet_owner.name);
100 println!(" Age: {}", pet_owner.age);
101
102 println!("\nPets:");
103 for pet in pet_owner.pets {
104 println!(" Name: {}", pet.name);
105 println!(" Species: {:?}", pet.species);
106 if let Some(age) = pet.age {
107 println!(" Age: {}", age);
108 }
109 if let Some(colour) = pet.colour {
110 println!(" Colour: {}", colour);
111 }
112 println!();
113 }
114 Ok(())
115 }
它是如何工作的...
注意这个食谱看起来几乎和上一个完全一样?除了结构,唯一的显著区别是我们调用了serde_json::to_string() [81]而不是toml::to_string(),以及serde_json::from_str() [96]而不是toml::from_str()。这正是像 Serde 这样的精心设计的框架的美丽之处:自定义序列化和反序列化代码隐藏在特质定义之后,我们可以使用相同的 API 而不必关心内部实现细节。
除了这些,没有其他需要说的,因为之前已经说过了,所以我们不会介绍其他格式。所有重要的格式都支持 Serde,所以你可以像使用本章中的其他格式一样使用它们。有关所有支持格式的完整列表,请参阅docs.serde.rs/serde/index.html。
还有更多...
JSON 没有类似enum的概念。然而,由于许多语言确实使用它们,多年来已经出现了多种处理从 JSON 到enum转换的约定。Serde 允许你通过枚举上的注解来支持这些约定。有关支持的转换的完整列表,请访问serde.rs/enum-representations.html。
参见
-
在第三章的处理文本文件食谱中,处理文件和文件系统
-
在第六章的处理错误食谱中,提供用户定义的错误类型
动态构建 JSON
当一个 JSON API 使用一个考虑不周到的模式和不一致的对象设计时,你可能会得到一个大多数成员都是Option的巨大结构。如果你发现自己只向这样的服务发送数据,那么动态地逐个构建你的 JSON 属性可能会容易一些。
如何操作...
-
打开之前为你生成的
Cargo.toml文件 -
在
[dependencies]部分,如果你还没有这样做,请添加以下行:
serde_json = "1.0.8"
-
如果你想,你可以访问
serde_json的 crates.io 网页(crates.io/crates/serde_json)来检查最新版本,并使用那个版本 -
在
bin文件夹中,创建一个名为dynamic_json.rs的文件 -
添加以下代码,并用
cargo run --bin dynamic_json运行:
1 #[macro_use]
2 extern crate serde_json;
3
4 use std::io::{self, BufRead};
5 use std::collections::HashMap;
6
7 fn main() {
8 // A HashMap is the same as a JSON without any schema
9 let mut key_value_map = HashMap::new();
10 let stdin = io::stdin();
11 println!("Enter a key and a value");
12 for input in stdin.lock().lines() {
13 let input = input.expect("Failed to read line");
14 let key_value: Vec<_> = input.split_whitespace().collect();
15 let key = key_value[0].to_string();
16 let value = key_value[1].to_string();
17
18 println!("Saving key-value pair: {} -> {}", key, value);
19 // The json! macro lets us convert a value into its JSON
representation
20 key_value_map.insert(key, json!(value));
21 println!(
22 "Enter another pair or stop by pressing '{}'",
23 END_OF_TRANSMISSION
24 );
25 }
26 // to_string_pretty returns a JSON with nicely readable
whitespace
27 let json =
28 serde_json::to_string_pretty(&key_value_map).expect("Failed
to convert HashMap into JSON");
29 println!("Your input has been made into the following
JSON:");
30 println!("{}", json);
31 }
32
33 #[cfg(target_os = "windows")]
34 const END_OF_TRANSMISSION: &str = "Ctrl Z";
35
36 #[cfg(not(target_os = "windows"))]
37 const END_OF_TRANSMISSION: &str = "Ctrl D";
它是如何工作的...
在这个例子中,用户可以输入任意数量的键值对,直到他们决定停止,此时他们会以 JSON 的形式收到他们的输入。你可以输入的一些示例输入包括:
name abraham
age 49
fav_colour red
hello world
(press 'Ctrl Z' on Windows or 'Ctrl D' on Unix)
使用 #[cfg(target_os = "some_operating_system")] 来处理特定于操作系统的环境。在这个配方中,我们使用它来有条件地编译 END_OF_TRANSMISSION 常量,在 Windows 上与 Unix 上不同。这个键组合告诉操作系统停止当前输入流。
这个程序的想法是,一个没有明确定义模式的 JSON 对象不过是一个 HashMap<String, String>[9]。现在,serde_json 不接受 String 作为值,因为这不够通用。相反,它需要一个 serde_json::Value,你可以通过在几乎任何类型上调用 json! 宏来轻松构建 [20]。
当我们完成时,我们不再像以前那样调用 serde_json::to_string(),而是使用 serde_json::to_string_pretty() [28],因为这会产生效率较低但可读性更强的 JSON。记住,JSON 并不主要是供人类阅读的,这就是为什么 Serde 序列化它的默认方式是完全不带任何空白的。如果你对确切差异感到好奇,可以随意将 to_string_pretty() 改为 to_string() 并比较结果。
参见
-
从标准输入读取 的配方在 第一章,学习基础知识
-
作为迭代器访问集合 和 使用 HashMap 的配方在 第二章,与集合一起工作
第五章:高级数据结构
在本章中,我们将介绍以下食谱:
-
创建延迟静态对象
-
位字段操作
-
提供自定义 derives
-
类型之间的转换
-
数据装箱
-
与智能指针共享所有权
-
处理内部可变性
简介
到目前为止,我们主要关注了所有都有其自身用途的技术。当然,我们正在继续这一趋势,但本章中展示的食谱在与其他代码结合使用时真正闪耀。您可以想象它们是胶水,将一个漂亮的 Rust 程序粘合在一起,因为它们主要面向新的使不同组件能够协同工作的方式。
创建延迟静态对象
大对象,尤其是常量对象,应该重用而不是重建。lazy_static!宏通过扩展 Rust 的正常static功能,帮助您实现这一点,正常情况下,您的对象需要在编译时构造,现在有了在运行时创建延迟对象的 capability。
如何做...
-
使用
cargo new chapter_five创建一个 Rust 项目,在本章中进行工作。 -
导航到新创建的
chapter_five文件夹。在本章的剩余部分,我们将假设您的命令行当前位于此目录中。 -
在
src文件夹中,创建一个名为bin的新文件夹。 -
删除生成的
lib.rs文件,因为我们没有创建库。 -
打开之前为您生成的
Cargo.toml文件。 -
在
[dependencies]下添加以下行:
lazy_static = "1.0"
regex = "0.2"
如果您愿意,您可以访问lazy_static(crates.io/crates/lazy_static)和正则表达式(crates.io/crates/regex)的 crates.io 网页,检查最新版本并使用该版本。
-
在
src/bin文件夹中,创建一个名为lazy_static.rs的文件。 -
添加以下代码,并使用
cargo run --bin lazy_static运行它:
1 #[macro_use]
2 extern crate lazy_static;
3 extern crate regex;
4
5 use regex::Regex;
6 use std::collections::HashMap;
7 use std::sync::RwLock;
8
9 // Global immutable static
10 lazy_static! {
11 static ref CURRENCIES: HashMap<&'static str, &'static str> =
{
12 let mut m = HashMap::new();
13 m.insert("EUR", "Euro");
14 m.insert("USD", "U.S. Dollar");
15 m.insert("CHF", "Swiss Francs");
16 m
17 };
18 }
19
20 // Global mutable static
21 lazy_static! {
22 static ref CLIENTS: RwLock<Vec<String>> =
RwLock::new(Vec::new());
23 }
24
25 // Local static
26 fn extract_day(date: &str) -> Option<&str> {
27 // lazy static objects are perfect for
28 // compiling regexes only once
29 lazy_static! {
30 static ref RE: Regex =
31 Regex::new(r"(\d{2}).(\d{2}).(\d{4})")
32 .expect("Failed to create regex");
33 }
34 RE.captures(date)
35 .and_then(|cap| cap.get(1).map(|day| day.as_str()))
36 }
37
38 fn main() {
39 // The first access to CURRENCIES initializes it
40 let usd = CURRENCIES.get("USD");
41 if let Some(usd) = usd {
42 println!("USD stands for {}", usd);
43 }
44
45 // All accesses will now refer to the same,
46 // already constructed object
47 if let Some(chf) = CURRENCIES.get("CHF") {
48 println!("CHF stands for {}", chf);
49 }
50
51 // Mutable the global static
52 CLIENTS
53 .write()
54 .expect("Failed to unlock clients for writing")
55 .push("192.168.0.1".to_string());
56
57 // Get an immutable reference to the global static
58 let clients = CLIENTS
59 .read()
60 .expect("Failed to unlock clients for reading");
61 let first_client = clients.get(0).expect("CLIENTS is
empty");
62 println!("The first client is: {}", first_client);
63
64 let date = "12.01.2018";
65 // The static object is nicely hidden inside
66 // the definition of extract_day()
67 if let Some(day) = extract_day(date) {
68 println!("The date \"{}\" contains the day \"{}\"", date,
day);
69 }
70 }
它是如何工作的...
通过调用lazy_static!宏[10, 21 和 29],我们在当前作用域中定义了一个延迟初始化的对象。这里的延迟意味着仅在第一次使用时创建。
与let绑定不同,其作用域也可以是全局作用域[10]。一个现实生活中的例子是创建一个具有已知内容且被许多函数使用的集合,因为另一种选择是创建一次并无限传递。
如果您的lazy_static包含一个在编译时已知内容的Vec,您可以使用一个const数组,因为其构造是常量的。从代码的角度来看,这意味着您不需要使用以下内容:
lazy_static!{
static ref FOO: Vec<&'static str> = vec!["a", "b", "c"];
}
相反,您可以使用以下方法:
const FOO: [&str; 3] = ["a", "b", "c"];
记得在第一章,学习基础知识;使用正则表达式查询时,我们讨论了编译正则表达式是昂贵的,应该避免吗?lazy_static! 是理想的选择。事实上,在函数中创建局部 static 正则表达式的模式非常普遍,以至于我们将其包含在这个例子 [29] 中:
fn extract_day(date: &str) -> Option<&str> {
lazy_static! {
static ref RE: Regex =
Regex::new(r"(\d{2}).(\d{2}).(\d{4})")
.expect("Failed to create regex");
}
RE.captures(date)
.and_then(|cap| cap.get(1).map(|day| day.as_str()))
}
最后,您还可以使用 lazy_static 对象创建全局可变状态 [21]。如前几章所述,过多的状态是软件开发中许多问题的根源,应该谨慎处理。很少有情况可以证明拥有这样的对象是合理的,因为几乎总是更好的选择是将对象传递出去。然而,也有一些例外。有时一个程序围绕对内存中一个特定数据集的操作展开,所有涉及的参与者都希望访问它。在这些情况下,将对象传递到代码中的每个函数可能会非常繁琐。一个可能但仍然非常罕见的例子是当仅处理活动连接列表 [21] 时:
lazy_static! {
static ref CLIENTS: RwLock<Vec<String>> = RwLock::new(Vec::new());
}
注意,由于借用检查器对具有 'static 生命周期的对象是禁用的(参见以下 还有更多... 部分),我们需要将我们的 static 包裹在一个并行锁中,例如 RwLock 或 Mutex,以保证线程安全。您可以在第七章,并行与 Rayon;使用 RwLock 并行访问资源中了解更多。
还有更多...
来自其他语言的人可能会想知道 lazy_static 提供了什么,这是普通 static 对象无法做到的。它们之间的区别如下。
在 Rust 中,static 变量是一个在整个程序运行期间存在的变量,这就是为什么它们有自己的、特殊的生命周期 'static'。问题是变量必须以恒定的方式构建,即在编译时已知的方式。在我们的例子中,我们不能用正常的 static 替换 CURRENCIES [11],因为 HashMap::new() 返回一个在运行时内存中某处的新构造的 HashMap。由于这要求它存在于内存中,因此不可能在编译时构建 HashMap,所以它的构造器不是 constant。
static 变量的另一个问题是,由于它们具有全局生命周期,借用检查器不能确保它们的访问是线程安全的。因此,对 static mut 变量的任何访问都将始终是 unsafe。
static 变量的约定是使用全大写字母书写,就像 const 变量一样。这是因为它们非常紧密地相关联。事实上,const 仅仅是一个内联的 static,它永远不会是 mut。
lazy_static 通过将你的对象包装在一个新创建的 struct 中来绕过这些限制,该 struct 可以隐式解引用到你的对象。这意味着你实际上从未直接访问过你的对象。lazy_static 通过要求你在 static 声明期间写 ref 来强调这一点,因为这让你在心理上将变量视为引用而不是实际的对象:
lazy_static! {
static ref foo: Foo = Foo::new();
}
在解引用时,包装器 struct 使用你在动态创建的对象中的 static mut 指针。它所做的只是以安全的方式包装 unsafe 调用。
如果你来自现代 C++背景,你可以将正常的 Rust static 视为 C++的 static constexpr,将 lazy_static Rust 视为 C++的 static 局部。
参见
-
使用正则表达式进行查询 在第一章,学习基础知识中的配方
-
使用 RwLocks 并行访问资源 在第七章,并行性和 Rayon中的配方
位字段的工作方式
用 C 编写的程序没有使用 Builder 模式(第一章,学习基础知识;使用 builder 模式)来为用户提供可组合选项的可能性。相反,它们必须依赖于位字段。由于 C 在历史上已经成为系统语言的通用语言,如果你计划在 Rust 接口中包装现有程序或反之亦然,你将不得不与大量的 C 代码交互。因此,你迟早会接触到位字段。由于 Rust 的 enum 比 C 的 enum 复杂得多,你必须依赖 bitflags crate 来提供你舒适地处理位字段所需的所有功能。
开始
本章假设你已经知道什么是位字段。在这里解释它没有意义,因为它还会涉及到二进制算术的解释,并且在你日常的 Rust 使用中并不那么相关。要了解位字段的好方法,请查看这篇论坛帖子以及教程:forum.codecall.net/topic/56591-bit-fields-flags-tutorial-with-example/ 和 www.tutorialspoint.com/cprogramming/c_bit_fields.htm。
如何做到这一点...
-
打开之前为你生成的
Cargo.toml文件。 -
在
[dependencies]下,添加以下行:
bitflags = "1.0"
-
如果你想,你可以去 bitflags 的 crates.io 页面(
crates.io/crates/bitflags)查看最新版本,并使用那个版本。 -
在
bin文件夹中,创建一个名为bit_fields.rs的文件。 -
添加以下代码,并用
cargo run --bin bit_fields运行它:
1 #[macro_use]
2 extern crate bitflags;
3
4 bitflags! {
5 struct Spices: u32 {
6 const SALT = 0b0000_0001;
7 const PEPPER = 0b0000_0010;
8 const CHILI = 0b0000_0100;
9 const SAFFRON = 0b0000_1000;
10 const ALL = Self::SALT.bits
11 | Self::PEPPER.bits
12 | Self::CHILI.bits
13 | Self::SAFFRON.bits;
14 }
15 }
16
17 impl Spices {
18 // Implementing a "clear" method can be useful
19 pub fn clear(&mut self) -> &mut Self {
20 self.bits = 0;
21 self
22 }
23 }
24
25 fn main() {
26 let classic = Spices::SALT | Spices::PEPPER;
27 let spicy = Spices::PEPPER | Spices::CHILI;
28 // Bit fields can nicely be printed
29 println!("Classic: {:?}", classic);
30 println!("Bits: {:08b}", classic.bits());
31 println!("Spicy: {:?}", spicy);
32 println!("Bits: {:08b}", spicy.bits());
33
34 println!();
35
36 // Use set operations
37 println!("Union: {:?}", classic | spicy);
38 println!("Intersection: {:?}", classic & spicy);
39 println!("Difference: {:?}", classic - spicy);
40 println!("Complement: {:?}", !classic);
41
42 // Interact with flags in a bit field
43 let mut custom = classic | spicy;
44 println!("Custom spice mix: {:?}", custom);
45 custom.insert(Spices::SAFFRON);
46 // Note that ALL is now also contained in the bit field
47 println!("Custom spice after adding saffron: {:?}", custom);
48 custom.toggle(Spices::CHILI);
49 println!("Custom spice after toggling chili: {:?}", custom);
50 custom.remove(Spices::SALT);
51 println!("Custom spice after removing salt: {:?}", custom);
52
53 // This could be user input
54 let wants_salt = true;
55 custom.set(Spices::SALT, wants_salt);
56 if custom.contains(Spices::SALT) {
57 println!("I hope I didn't put too much salt in it");
58 }
59
60 // Read flags from raw bits
61 let bits = 0b0000_1101;
62 if let Some(from_bits) = Spices::from_bits(bits) {
63 println!("The bits {:08b} represent the flags {:?}", bits,
from_bits);
64 }
65
66 custom.clear();
67 println!("Custom spice mix after clearing: {:?}", custom);
68 }
它是如何工作的...
bitflags!宏允许您定义所有标志及其底层类型(在我们的情况下,这是u32)[4 到 15]。它们以ALL_CAPS的形式编写,因为它们是常量。我们也可以用这种方式定义标志集合,就像我们用ALL[10]做的那样。我们本可以添加更多的组合,例如:
const SPICY = Self::PEPPER.bits | Self::CHILI.bits;
该宏为您创建一个具有指定成员的结构,并为其实现了一组特质,以便启用熟悉的|、&、-和!表示法 [37 到 40]以及美化打印。您仍然可以直接通过同名成员访问在后台使用的原始bits。
注意,在打印时,标志组合将被单独列出。例如,查看第[47]行的输出。在将字段中所有可能的标志设置为活动状态后,它将按以下方式进行美化打印:
Custom spice after adding saffron: SALT | PEPPER | CHILI | SAFFRON | ALL
在位字段上定义的一个有用的方法是clear()[19]。这隐藏了底层的bits,并且易于阅读。
使用上述二进制运算符,您可以在您的位字段上执行集合操作 [37 到 40]。这些操作与您可以在HashSet上执行的操作相同,并在第二章中用一张漂亮的图表进行了说明,处理集合; 使用 HashSet。
在位字段中处理单个标志也非常简单。insert()将标志设置为活动状态 [45],remove()将其设置为非活动状态 [50],而toggle则将其从活动状态切换到非活动状态,反之亦然 [48]。如果您还不知道您将要insert或remove一个标志,就像不可预测的用户输入那样,您可以使用set()显式地将标志的激活状态设置为true或false [55]。
您可以通过调用contains() [56]来检查某个标志是否处于活动状态。这也适用于另一个位字段或标志组合。这意味着以下也是有效的:
if custom.contains(Spices::SALT | Spices::PEPPER) {
println!("The custom spice contains both salt and pepper");
}
此外,您还可以使用intersects()来检查两个位字段中的任何标志是否匹配。
最后但同样重要的是,您可以通过在它上面调用from_bits()来将原始字节反序列化到您生成的位字段结构中 [62]。这将检查每个位是否实际上对应于一个标志,如果不是,则返回None。如果您绝对 100%确信数据必须是有效的,您可以使用from_bits_truncate()跳过错误检查并简单地忽略无效位。
另请参阅
- 第二章中的使用 HashSet配方,处理集合
提供自定义的derive
您可能已经看过#[derive(Debug)]并假设它是某种奇怪的编译器魔法。并非如此。它是一个所谓的过程宏,即一个在编译时不仅扩展,而且运行的宏。这样,您可以在实际的编译过程中注入代码。最有用的应用是创建自定义的derive,通过它可以基于现有代码的分析生成新代码。
入门
这个配方将使用一个 抽象语法树,或 AST。它是语言元素之间相互关系的树形表示。在这个配方中,我们(即一个名为 syn 的酷 crate)将整个程序解析成一个单一的深层 struct。
如何做到这一点...
-
使用
cargo new chapter-five-derive创建一个用于自定义 derive 的新子 crate。 -
打开新创建的
chapter-five-derive/Cargo.toml文件。 -
将以下内容直接添加到文件的
[dependencies]部分上方,以标记该 crate 为过程宏 crate:
[lib]
proc-macro = true
- 在
[dependencies]下添加以下行:
syn = "0.11.11"
quote = "0.3.15"
如果你想,你可以访问 syn (crates.io/crates/syn) 和 quote (crates.io/crates/quote) 的 crates.io 网页,检查最新版本并使用那个版本。
- 在
chapter-five-derive/src/lib.rs文件中,删除生成的代码,并添加以下内容:
1 extern crate proc_macro;
2 #[macro_use]
3 extern crate quote;
4 extern crate syn;
5
6 use proc_macro::TokenStream;
7
8 // HelloWorld is the name for the derive
9 // hello_world_name is the name of our optional attribute
10 #[proc_macro_derive(HelloWorld, attributes(hello_world_name))]
11 pub fn hello_world(input: TokenStream) -> TokenStream {
12 // Construct a string representation of the type definition
13 let s = input.to_string();
14 // Parse the string representation into an abstract syntax
tree
15 let ast = syn::parse_derive_input(&s).expect("Failed to
parse the source into an AST");
16
17 // Build the implementation
18 let gen = impl_hello_world(&ast);
19
20 // Return the generated implementation
21 gen.parse()
22 .expect("Failed to parse the AST generated from deriving
from HelloWorld")
23 }
24
25 fn impl_hello_world(ast: &syn::DeriveInput) -> quote::Tokens {
26 let identifier = &ast.ident;
27 // Use the name provided by the attribute
28 // If there is no attribute, use the identifier
29 let hello_world_name =
get_name_attribute(ast).unwrap_or_else(||
identifier.as_ref());
30 quote! {
31 // Insert an implementation for our trait
32 impl HelloWorld for #identifier {
33 fn hello_world() {
34 println!(
35 "The struct or enum {} says: \"Hello world from
{}!\"",
36 stringify!(#identifier),
37 #hello_world_name
38 );
39 } //end of fn hello_world()
40 } //end of impl HelloWorld
41 } //end of quote
42 } //end of fn impl_hello_world
43
44 fn get_name_attribute(ast: &syn::DeriveInput) -> Option<&str> {
45 const ATTR_NAME: &str = "hello_world_name";
46
47 // Go through all attributes and find one with our name
48 if let Some(attr) = ast.attrs.iter().find(|a| a.name() ==
ATTR_NAME) {
49 // Check if it's in the form of a name-value pair
50 if let syn::MetaItem::NameValue(_, ref value) = attr.value
{
51 // Check if the value is a string
52 if let syn::Lit::Str(ref value_as_str, _) = *value {
53 Some(value_as_str)
54 } else {
55 panic!(
56 "Expected a string as the value of {}, found {:?}
instead",
57 ATTR_NAME, value
58 );
59 }
60 } else {
61 panic!(
62 "Expected an attribute in the form #[{} = \"Some
value\"]",
63 ATTR_NAME
64 );
65 }
66 } else {
67 None
68 }
69 }
- 在本章的原始
Cargo.toml文件中,在[dependencies]下添加以下内容:
custom-derive = { path = "custom-derive" }
-
在
bin文件夹中,创建一个名为custom_derive.rs的文件。 -
添加以下代码,并使用
cargo run --bin custom_derive运行:
1 #[macro_use]
2 extern crate chapter_five_derive;
3
4 // trait definitions have to be in "consumer" crate
5 trait HelloWorld {
6 // This method will send a friendly greeting
7 fn hello_world();
8 }
9
10 // thanks to the code in the custom_derive crate
11 // we can derive from HelloWorld in order to provide
12 // an automatic implementation for the HelloWorld trait
13 #[derive(HelloWorld)]
14 struct Switzerland;
15
16 #[derive(HelloWorld)]
17 struct Britain;
18
19 #[derive(HelloWorld)]
20 // We can use an optional attribute to change the message
21 #[hello_world_name = "the Land Down Under"]
22 struct Australia;
23
24 fn main() {
25 Switzerland::hello_world();
26 Britain::hello_world();
27 Australia::hello_world();
28 }
它是如何工作的...
这个配方的说明比其他配方复杂一些,因为我们需要管理两个独立的 crate。如果你的代码无法编译,请将你的版本与书中使用的版本进行比较,网址为 github.com/SirRade/rust-standard-library-cookbook/tree/master/chapter_five.。我们需要将代码分成两个 crate,因为提供自定义 derive 需要创建一个过程宏,正如代码中所有的 proc_macro 实例所示。过程宏是运行在编译器旁边并与它直接交互的 Rust 代码。由于这种代码的特殊性质和独特的限制,它需要在一个单独的 crate 中,该 crate 带有以下注释:
[lib]
proc-macro = true
这个 crate 通常以主 crate 的名称命名,并在后面添加 _derive 后缀。在我们的例子中,主 crate 名为 chapter_five,所以提供过程宏的 crate 称为 chapter_five_derive。
在我们的例子中,我们将创建一个派生版本的经典的 Hello World:一个从 HelloWorld 派生的 struct 或 enum 将实现 HelloWorld trait,提供一个包含其自身名称的友好问候的 hello_world() 函数。此外,你可以指定一个 HelloWorldName 属性来改变消息。
custom.rs 中的代码应该是自解释的。我们首先导入我们需要包含 #[macro_use] 属性以实际导入过程宏的派生 crate。然后我们定义我们的 HelloWorld 特征 [5],并在多个结构体 [13、16 和 19] 上派生它,就像我们使用内置的派生如 Debug 或 Clone 一样。Australia 通过 HelloWorldName 属性获得自定义消息。最后,在 main 函数中,我们调用生成的 hello_world() 函数。
现在我们来看看 chapter-five-derive/src/lib.rs。过程宏 crate 通常首先导入 proc_macro、quote 和 syn crate。细心的读者会注意到我们没有在我们的 crate 的 Cargo.toml 中的 [dependencies] 部分添加 proc_macro。我们不需要这样做,因为这个特殊支持 crate 是由标准 Rust 分发提供的。
quote crate 提供了 quote! 宏,它允许我们将 Rust 代码转换为编译器可以使用的令牌。这个宏真正有用的特性是它支持通过在变量前写一个 # 来进行代码插值。这意味着当我们写下以下内容时,struct_name 变量内的值被解释为 Rust 代码:
impl HelloWorld for #struct_name { ... }
如果 struct_name 有 Switzerland 的值,以下代码将被生成:
impl HelloWorld for Switzerland { ... }
syn crate 是一个基于 nom 解析器组合框架(github.com/Geal/nom)构建的 Rust 解析器,如果你在考虑编写一个解析器,也应该查看一下。实际上,第四章 中使用的某些 crate 也用 nom 编写。回到正题,syn 解析由你的自定义属性或 derives 注释的代码,并允许你使用生成的抽象语法树进行工作。
自定义派生的惯例是创建一个以派生名称命名的函数(在我们的例子中是 pub fn hello_world),该函数解析注释的代码,然后调用一个生成新代码的函数。第二个函数通常具有第一个函数的名称,前面加上 impl。在我们的代码中,这是 fn impl_hello_world。
在一个 proc_macro crate 中,只有标记为 proc_macro_derive 的函数才允许发布。顺便说一句,这个后果是我们无法将我们的 HelloWorld 特征移动到这个 crate 中;它不允许是 pub。
proc_macro_derive 注解要求你指定用于派生的名称(对我们来说是 HelloWorld)以及它允许的属性。如果我们不想接受 HelloWorldName 属性,我们可以简单地省略整个属性部分,并像这样注释我们的函数:
#[proc_macro_derive(HelloWorld)]
因为hello_world直接钩入编译器,它既接受也返回一个TokenStream,这是 Rust 代码的编译器内部表示。我们首先将TokenStream转换回String,以便再次由syn解析。这不是一个昂贵的操作,因为我们从编译器接收到的TokenStream不是整个程序,而只是我们自定义推导注解的部分。例如,第一个由HelloWorld注解的struct的TokenStream后面的String仅仅是以下内容:
struct Switzerland;
我们随后使用syn::parse_derive_input(&s)解析该字符串,这基本上告诉syn我们想要解析的代码是一个派生某些内容的struct或enum。
我们随后使用以下方式生成代码:
let gen = impl_hello_world(&ast);
然后,我们用以下方式将其转换回TokenStream:
gen.parse()
然后,TokenStream被编译器注入回代码中。正如你所见,过程宏不能更改现有代码,而只能分析它并生成额外的代码。
这里是hello_world中描述的过程:
-
将
TokenStream转换为String -
使用
syn解析String -
生成另一个方法的实现
-
将实现解析回
TokenStream
对于自定义推导来说,这是一个非常典型的例子。你可以在几乎所有基本过程宏中重用所展示的代码。
现在让我们继续到impl_hello_world。借助传递的ast,我们可以分析注解的结构。ident成员,代表标识符,告诉我们struct或enum的名称。例如,在从HelloWorld派生的第一个struct中,这就是"Switzerland"字符串。
然后,我们借助get_name_attribute这个小助手函数来决定在问候语中使用哪个名称,我们稍后会看到它。如果设置了HelloWorldName属性,它将返回该属性的值。如果没有设置,我们默认使用通过as_ref[29]转换的identifier。如何做到这一点将在下一个菜谱中解释。
最后,我们通过编写实现并用quote!包围它来创建一些quote::Tokens。再次注意,我们如何通过在变量前写#来将变量内插到代码中。此外,在打印时,我们用stringify!包围#identifier,将标识符转换为字符串。对于#hello_world_identifier,我们不需要这样做,因为它已经包含了一个字符串。为了理解为什么需要这样做,让我们看看如果我们没有包含stringify!,将为Switzerland结构生成的代码:
impl HelloWorld for Switzerland {
fn hello_world() {
println!(
"The struct or enum {} says: \"Hello world from {}!\"",
Switzerland,
"Switzerland"
);
}
}
尝试自己操作一下,你会看到一个错误信息,内容大致是“Switzerland无法使用默认格式化器格式化”。这是因为我们并没有打印"Switzerland"这个字符串,而是在尝试打印Switzerland结构体的概念本身,这显然是没有意义的。要解决这个问题,我们只需要确保插值变量被引号包围("),这正是stringify!所做的事情。
让我们来看看拼图的最后一部分:get_name_attribute。这个函数一开始可能会有些令人畏惧。让我们一步一步地来看:
if let Some(attr) = ast.attrs.iter().find(|a| a.name() == ATTR_NAME) { ... }
在这里,我们将遍历所有可用的属性并搜索一个名为"HelloWorldName"的属性。如果我们找不到,函数调用已经结束,通过返回None来结束。否则,我们继续执行下一行:
if let syn::MetaItem::NameValue(_, ref value) = attr.value { ... }
syn::MetaItem是syn如何调用属性的简单方式。这一行是必要的,因为 Rust 中编写属性的方式有很多。例如,syn::MetaItem::Word可以写成#[foo]。syn::MetaItem::List的一个例子是#[foo(Bar, Baz, Quux)]。#[derive(...)]本身也是一个syn::MetaItem::List。然而,我们只对syn::MetaItem::NameValue感兴趣,它是一种形式为#[foo = Bar]的属性。如果HelloWorldName属性不是这种形式,我们会panic!并显示一条解释问题的消息。过程宏中的panic会导致编译器错误。你可以通过将custom.rs中的#[HelloWorldName = "the Land Down Under"]替换为#[HelloWorldName]来验证这一点。
与常规程序不同,由于过程宏在编译时使用panic!,它们经常panic!是可以接受的。在这样做的时候,记住来自其他 crate 的错误非常难以调试,尤其是在任何类型的宏中,因此尽可能明确地编写错误信息非常重要。
我们最后需要检查的是HelloWorldName的值。由于我们打算打印它,我们只想接受字符串:
if let syn::Lit::Str(ref value_as_str, _) = *value { ... }
在成功的情况下,我们返回字符串。否则,我们再次panic!并显示一个详细说明问题的错误信息。
更多内容...
如果你运行这个配方时遇到了麻烦,可以使用 David Tolney 的cargo-expand(github.com/dtolnay/cargo-expand)来展示编译器是如何展开你的proc_macros的。这是一个非常有用的调试宏的工具,所以请务必查看。
两种 crate 限制背后的原因是历史性的,只是暂时的。最初,只有一种定义宏的方法,即macro_rules!。那些有特殊需求并愿意付出努力的人(现在仍然如此)能够通过直接挂钩 Rust 编译器本身来扩展他们的程序。以这种方式编写的 crate 被称为编译器插件。当然,这是极其不稳定的,因为每个 Rust 的小版本发布都可能破坏你的插件,但人们仍然继续这样做,因为它给了他们一个很大的优势,即自定义派生。核心团队为了应对对语言扩展性的增加需求,决定在未来某个时候推出macros2.0,对整个宏系统进行彻底的改革,并添加了许多额外的功能,例如命名空间宏。
由于他们看到大多数插件都只用于自定义派生,他们也决定通过macros1.1(也称为过程宏)来连接macros2.0发布前的这段时间。稳定了创建自定义派生所需的小部分编译器。唯一的问题是,crate 现在有正常运行的部分和在编译时运行的部分。混合它们在实现上很困难,而且有点混乱,因此创建了将所有过程宏代码移动到-derive crate 的两种 crate 系统。这是本食谱中使用的系统,因为在编写本食谱时,macros2.0尚未稳定。我鼓励你查看当前的进展:github.com/rust-lang/rust/issues/39412。
如果在你阅读这本书的时候,macros2.0已经发布,你应该更新你对如何编写现代自定义派生的知识。
将类型转换为彼此
拥有专用类型是很好的,但真正的灵活性只能在这些类型可以轻松相互转换时出现。幸运的是,Rust 通过From和Into特性以及一些相关特性,非常直接地提供了这种功能。
如何做...
-
在
bin文件夹中,创建一个名为conversion.rs的文件。 -
添加以下代码,并使用
cargo run --bin conversion运行它:
1 use std::ops::MulAssign;
2 use std::fmt::Display;
3
4 // This structure doubles all elements it stores
5 #[derive(Debug)]
6 struct DoubleVec<T>(Vec<T>);
7
8
9 // Allowing conversion from a Vec<T>,
10 // where T is multipliable with an integer
11 impl<T> From<Vec<T>> for DoubleVec<T>
12 where
13 T: MulAssign<i32>,
14 {
15 fn from(mut vec: Vec<T>) -> Self {
16 for elem in &mut vec {
17 *elem *= 2;
18 }
19 DoubleVec(vec)
20 }
21 }
22
23 // Allowing conversion from a slice of Ts
24 // where T is again multipliable with an integer
25 impl<'a, T> From<&'a [T]> for DoubleVec<T>
26 where
27 T: MulAssign<i32> + Clone,
28 {
29 fn from(slice: &[T]) -> Self {
30 // Vec<T: MulAssign<i32>> automatically
31 // implements Into<DoubleVec<T>>
32 slice.to_vec().into()
33 }
34 }
35
36 // Allowing conversion from a &DoubleVec<T> to a &Vec<T>
37 impl<T> AsRef<Vec<T>> for DoubleVec<T> {
38 fn as_ref(&self) -> &Vec<T> {
39 &self.0
40 }
41 }
42
43
44 fn main() {
45 // The following three are equivalent
46 let hello_world = "Hello World".to_string();
47 let hello_world: String = "Hello World!".into();
48 let hello_world = String::from("Hello World!");
49
50 // Vec<u8> implements From<&str>
51 // so hello_world_bytes has the value b"Hello World!"
52 let hello_world_bytes: Vec<u8> = "Hello World!".into();
53 let hello_world_bytes = Vec::<u8>::from("Hello World!");
54
55 // We can convert a Vec<T: MulAssign<i32>> into a DoubleVec
56 let vec = vec![1, 2, 3];
57 let double_vec = DoubleVec::from(vec);
58 println!("Creating a DoubleVec from a Vec: {:?}",
double_vec);
59
60 // Vec<T: MulAssign<i32>> automatically implements
Into<DoubleVec<T>>
61 let vec = vec![1, 2, 3];
62 let double_vec: DoubleVec<_> = vec.into();
63 println!("Converting a Vec into a DoubleVec: {:?}",
double_vec);
64
65 // A reference to DoubleVec can be converted to a reference
to Vec
66 // Which in turn dereferences to a slice
67 print_elements(double_vec.as_ref());
68
69 // The standard library provides From<T> for Option<T>
70 // You can design your API in an ergonomic way thanks to this
71 easy_public_func(Some(1337), Some(123), None);
72 ergonomic_public_func(1337, 123, None);
73 }
74
75
76 fn print_elements<T>(slice: &[T])
77 where
78 T: Display,
79 {
80 for elem in slice {
81 print!("{} ", elem);
82 }
83 println!();
84 }
85
86
87 // Easily written but cumbersome to use
88 fn easy_public_func(foo: Option<i32>, bar: Option<i32>, baz:
Option<i32>) {
89 println!(
90 "easy_public_func = foo: {:?}, bar: {:?}, baz: {:?}",
91 foo,
92 bar,
93 baz
94 );
95 }
96
97
98 // This is quite some extra typing, so it's only worth to do
for
99 // public functions with many optional parameters
100 fn ergonomic_public_func<Foo, Bar, Baz>(foo: Foo, bar: Bar,
baz: Baz)
101 where
102 Foo: Into<Option<i32>>,
103 Bar: Into<Option<i32>>,
104 Baz: Into<Option<i32>>,
105 {
106 let foo: Option<i32> = foo.into();
107 let bar: Option<i32> = bar.into();
108 let baz: Option<i32> = baz.into();
109
110 println!(
111 "ergonomic_public_func = foo: {:?}, bar: {:?}, baz: {:?}",
112 foo,
113 bar,
114 baz
115 );
116 }
它是如何工作的...
转换类型之间最重要的特性是From。实现它意味着定义如何从另一个类型中获取类型。
我们以DoubleVec[6]为例来介绍这个概念。它的概念很简单,当你从Vec构造它时,它会将其所有元素翻倍。为此,我们实现了From<Vec<T>> [11],并使用一个where子句指定T: MulAssign<i32>[13],这意味着该特性将为所有可以赋值给整数乘法结果的类型实现。或者,用代码来说,所有允许以下操作的类型:
t *= 2;
实际实现应该是自解释的,我们只是将向量中的每个元素乘以二,并将其包裹在我们的DoubleVec[19]中。之后,我们也为相同类型的切片实现了From[25]。
考虑到良好的实践,将所有与向量(Vec<T>)一起工作的特性定义扩展到也适用于切片(&[T])是很有用的。这样,你就能获得通用性和性能,因为你可以直接操作数组的引用(如&[1, 2, 3])和其他向量的范围(如vec[1..3]),而不需要先进行转换。这个最佳实践也适用于函数,你应该始终尽可能接受你类型的切片(如print_elements()所示),出于同样的原因。
然而,在这个实现中,我们看到了一些有趣的东西:
fn from(slice: &[T]) -> Self {
slice.to_vec().into()
}
slice.to_vec()简单地将切片转换成向量。但.into()是什么意思呢?嗯,它来自Into特性,它是From特性的对立面,它将一个类型转换成另一个类型。但Vec是如何知道如何转换成DoubleVec的呢?
让我们看看标准库中Into的实现,可以在github.com/rust-lang/rust/blob/master/src/libcore/convert.rs找到,其中我们发现以下行:
// From implies Into
impl<T, U> Into<U> for T where U: From<T>
{
fn into(self) -> U {
U::from(self)
}
}
哈哈!根据这个,每个实现From接口的T类型都会自动让U实现Into接口的T。确实如此,因为我们实现了From<Vec<T> for DoubleVec<T>>,所以我们自动也实现了Into<DoubleVec<T>> for Vec<T>。让我们再次看看之前的代码:
fn from(slice: &[T]) -> Self {
slice.to_vec().into()
}
这个切片被转换成了一个Vec,它实现了Into接口,用于DoubleVec以及其他许多类型。因为我们的函数签名表明我们返回Self,Rust 知道应该使用哪个Into实现,因为只有其中一个返回DoubleVec。
另一个对类型转换有用的特性是AsRef。它的唯一函数as_ref几乎与into相同,但它不是将自身移动到另一个类型,而是取自身的引用并返回另一个类型的引用。从某种意义上说,它转换引用。你可以预期在大多数情况下这个操作都很便宜,因为它通常只是返回一个内部对象的引用。实际上,你已经在上一个菜谱中使用了这个方法:
let hello_world_name = get_name_attribute(ast).unwrap_or_else(|| identifier.as_ref());
identifier内部持有其名称的String。编译器知道hello_world_name必须是&str,因为get_name_attribute(ast)的返回类型是Option<&str>,我们正在尝试使用默认值解包它。基于这些信息,as_ref()试图返回一个&str,它可以做到,因为identifier唯一返回&str的AsRef实现是返回上述包含其名称的String的引用。
我们只为 Vec 实现 AsRef,而不是切片,因为有一个指向向量的引用 (&Vec<T>) 可以自动解引用强制转换为切片 (&[T]),这意味着我们自动实现了它。你可以在 doc.rust-lang.org/book/second-edition/ch15-02-deref.html#implicit-deref-coercions-with-functions-and-methods 中了解更多关于解引用强制转换的概念。
AsRef 还有一个兄弟叫做 AsMut,它与前者相同,但操作的是可变引用。我们故意没有在这个示例中实现它,因为我们不希望用户去干扰 DoubleVec 的内部状态。一般来说,你也应该非常谨慎地使用这个特性,因为过度访问任何事物的内部可能会迅速变得非常混乱。
main 函数包含了一些类型转换的例子。一个流行的例子是第 46 到 48 行的从 &str 转换到 String。有趣的是,&str 也可以转换为其底层字节的向量 [52 和 53]。让我们看看我们的 DoubleVec 如何以同样的方式转换。
下一行展示了 double_vec.as_ref() 返回的 &Vec<i32> 如何无缝地表现得像 [i32],因为 print_elements() 只接受切片 [67]:
print_elements(double_vec.as_ref());
配方的最后一部分是关于 API 设计。标准库中 From 的小实现如下:
impl<T> From<T> for Option<T> {
fn from(val: T) -> Option<T> {
Some(val)
}
}
这意味着每个类型都可以转换为 Option。你可以使用这个技巧,如 ergonomic_public_func [100] 的实现所示,使具有多个 Option 类型参数的函数更容易使用和查看,正如你可以通过比较以下两个函数调用 [71 和 72] 所见:
easy_public_func(Some(1337), Some(123), None);
ergonomic_public_func(1337, 123, None);
然而,由于需要一些额外的类型来达到这个目的,如果你只在你 API 中的函数上这样做,也就是对 crate 的用户可用,那就没问题。如果你想了解更多关于 Rust 中干净 API 设计的技巧,请查看 Rust 核心开发者 Pascal Hertleif 的优秀博客文章:deterministic.space/elegant-apis-in-rust.html
包装数据
我们将要查看的第一个智能指针是 Box。这个非常特殊的数据类型是 C++ 中 unique_ptr 的类似物,它指向存储在堆上的数据,当它超出作用域时自动删除该数据。由于从栈到堆的转变,Box 可以通过故意丢失类型信息来提供一些灵活性。
如何做到...
-
在
bin文件夹中,创建一个名为boxing.rs的文件。 -
添加以下代码,并用
cargo run --bin boxing运行它:
1 use std::fs::File;
2 use std::io::BufReader;
3 use std::result::Result;
4 use std::error::Error;
5 use std::io::Read;
6 use std::fmt::Debug;
7
8 #[derive(Debug)]
9 struct Node<T> {
10 data: T,
11 child_nodes: Option<(BoxedNode<T>, BoxedNode<T>)>,
12 }
13 type BoxedNode<T> = Box<Node<T>>;
14
15 impl<T> Node<T> {
16 fn new(data: T) -> Self {
17 Node {
18 data,
19 child_nodes: None,
20 }
21 }
22
23 fn is_leaf(&self) -> bool {
24 self.child_nodes.is_none()
25 }
26
27 fn add_child_nodes(&mut self, a: Node<T>, b: Node<T>) {
28 assert!(
29 self.is_leaf(),
30 "Tried to add child_nodes to a node that is not a leaf"
31 );
32 self.child_nodes = Some((Box::new(a), Box::new(b)));
33 }
34 }
35
36 // Boxes enable you to use traditional OOP polymorph
37 trait Animal: Debug {
38 fn sound(&self) -> &'static str;
39 }
40
41 #[derive(Debug)]
42 struct Dog;
43 impl Animal for Dog {
44 fn sound(&self) -> &'static str {
45 "Woof!"
46 }
47 }
48
49 #[derive(Debug)]
50 struct Cat;
51 impl Animal for Cat {
52 fn sound(&self) -> &'static str {
53 "Meow!"
54 }
55 }
56
57 fn main() {
58 let mut root = Node::new(12);
59 root.add_child_nodes(Node::new(3), Node::new(-24));
60 root.child_nodes
61 .as_mut()
62 .unwrap()
63 0
64 .add_child_nodes(Node::new(0), Node::new(1803));
65 println!("Our binary tree looks like this: {:?}", root);
66
67 // Polymorphism
68 let mut zoo: Vec<Box<Animal>> = Vec::new();
69 zoo.push(Box::new(Dog {}));
70 zoo.push(Box::new(Cat {}));
71 for animal in zoo {
72 println!("{:?} says {}", animal, animal.sound());
73 }
74
75 for word in caps_words_iter("do you feel lucky, punk‽") {
76 println!("{}", word);
77 }
78
79 // Assuming a file called number.txt exists
80 let num = read_file_as_number("number.txt").expect("Failed
read the file as a number");
81 println!("number.txt contains the number {}", num);
82
83 // Dynamically composing functions
84 let multiplier = create_multiplier(23);
85 let result = multiplier(3);
86 println!("23 * 3 = {}", result);
87 }
88
89 // Via trait objects we can return any iterator
90 fn caps_words_iter<'a>(text: &'a str) -> Box<Iterator<Item =
String> + 'a> {
91 // Return an iterator over every word converted into
ALL_CAPS
92 Box::new(text.trim().split(' ').map(|word|
word.to_uppercase()))
93 }
94
95 // Same goes for errors
96 fn read_file_as_number(filename: &str) -> Result<i32,
Box<Error>> {
97 let file = File::open(filename)?;
98 let mut buf_reader = BufReader::new(file);
99 let mut content = String::new();
100 buf_reader.read_to_string(&mut content)?;
101 let number: i32 = content.parse()?;
102 Ok(number)
103 }
104
105 fn create_multiplier(a: i32) -> Box<Fn(i32) -> i32> {
106 Box::new(move |b| a * b)
107 }
它是如何工作的...
我们将要探索的第一件事是递归类型,即包含自身的类型。这不能直接完成,因为编译器需要提前知道一个类型需要多少空间。考虑以下 struct:
struct Foo {
bar: i32
}
编译器会自问:“创建一个 Foo 需要多少空间?”并发现它只需要足够的空间来存放一个i32。而一个i32需要多少空间?正好是 32 位。现在,考虑以下情况:
struct Foo {
bar: i32,
baz: Foo,
}
Foo 需要多少空间?足够存放一个i32和一个 Foo。一个i32需要多少空间?32 位。Foo 需要多少空间?足够存放一个i32和一个 Foo。而这个 Foo 需要多少空间?足够存放一个 Foo,以此类推,直到宇宙的热寂。显然,我们不想在编译上花费那么长时间。让我们看看我们问题的解决方案:
struct Foo {
bar: i32,
baz: Box<Foo>,
}
再说一次,Foo 有多大?足够存放一个i32和一个 Foo。i32有多大?32 位。Box<Foo>有多大?和其他类型的盒子一样大,即 64 位。每个Box都将始终具有相同的大小,因为它们都是同一件事,即指向堆中某个类型的指针。这样,我们就解决了问题,因为编译器现在在编译时知道类型的确切大小,并且很高兴。而且因为它很高兴,所以我们也很高兴。
在我们的代码示例中,我们展示了递归类型的一个可能的使用案例,一个简单的二叉树实现[9]。如果你不知道,二叉树由一个称为节点的数据簇组成,它可以连接到零个或两个其他子节点。连接到零个节点的节点是一个叶节点。在我们的例子中,我们构建了一个看起来像这样的树:

我们将其实现为一个包含任何数据以及可选的一对BoxedNode的struct Node,其中BoxedNode只是Box<Node>的别名。
一个针对速度优化的真实二叉树实现将比我们的示例稍微复杂一些。虽然递归的概念非常适合二叉树,但将每个节点单独存储在堆中的某个地方是非常低效的。真正的实现将只对用户表现出递归,但内部将节点存储在Vec<Node>中。这样,节点通过在连续的内存块中分配来获得速度上的提升,这将优化缓存。Rust 的BTreeMap和BTreeSet也遵循这个概念。
二叉树是一种非常适合数据遍历的数据结构。你可以在以下 StackOverflow 答案中了解一些它的最大用途,由 Danny Pflughoeft 提供:stackoverflow.com/questions/2130416/what-are-the-applications-of-binary-trees#2200588。
Box 允许我们做的下一件事是经典的泛型,就像你在其他语言中会认识的那样。为此,我们准备了一个名为 Animal [37] 的特征,它有一个用于产生 sound() 方法的特征。它的实现者 Dog [42] 将产生 "Woof!"[45],而 Cat [50] 的实现者将产生 "Meow!" [53]。我们的目标是存储一个 Dog 和一个 Cat 在一个 Vec 的 Animal 中。我们可以通过创建一个所谓的 特征对象 来做到这一点。它是由一个特征的 Box 创建的,就像在下面的例子中一样:
let mut zoo: Vec<Box<Animal>> = Vec::new();
这样,我们故意从 Box 中的实际类型中删除类型信息。编译器不再知道 Box 中是什么类型,只知道它实现了 Animal,这正是它需要知道的所有信息。正如你通过运行代码所看到的,Rust 运行时仍然会执行正确的函数,并为 Dog 和 Cat 产生不同的声音。
使用相同的机制,我们可以返回一个 Iterator[90] 的特征对象:
fn caps_words_iter<'a>(text: &'a str) -> Box<Iterator<Item = String> + 'a> { ... }
这样,我们可以在 caps_words_iter() 内混合和匹配迭代器,而不必关心确切的返回类型,只要它实现了 Iterator,它们都做到了。记住,我们不能直接返回 Iterator,没有任何 Box 包围它,因为我们不能返回一个特征。然而,特征对象是完全可行的。
接下来,我们来看 read_file_as_number()[96]。这个方法读取一个文件,并返回解析为 i32 的内容。这个文件不会为你生成,所以你必须从我们的 GitHub 仓库下载它,或者手动创建一个名为 number.txt 的文件,其中包含一个数字,比如 777。从签名中,你可以看出这次我们正在装箱 Error。这让我们可以混合返回的错误类型。确实,这个方法返回两种不同的错误:std::io::Error 和 std::num::ParseIntError。
我们将要探讨的最后一件事是如何使用 create_multiplier()[105] 返回闭包。由于所有闭包都实现了 Fn、FnOnce 和/或 FnMut,我们可以为它们返回一个特征对象。这样,我们就可以在运行时创建、组合和更改函数,就像在函数式语言中一样。
还有更多...
你可能已经注意到,返回 Box<Iterator> 或 Box<Error> 在效率上会有一些损失,因为它需要在没有任何理由的情况下将对象移动到堆上。目前有两种方法可以解决这个问题。
对于 Box<Error>,你应该创建一个自己的 Error 类型,结合你函数可以返回的所有可能的错误。这详细说明在 第六章,处理错误;提供用户定义的错误类型。
对于 Box<Iterator>,你可以分析编译器的输出,以找出你返回的确切真实类型。这对于小型迭代器有效,但任何复杂的迭代器都需要很长时间才能破解。由于这种情况并不理想,Rust 团队已经批准引入抽象类型,这将在第十章使用实验性夜间功能中介绍;返回抽象类型,因为它尚未进入稳定 Rust。
参见
-
在第六章处理错误中的提供用户定义的错误类型配方。
-
在第十章使用实验性夜间功能中的返回抽象类型配方。
与智能指针共享所有权
一些所有权关系并不像类型 A 拥有 B那样简单。有时,一组类型会拥有另一个类型。为了处理这种情况,我们需要另一个类似 Box 的智能指针,但只有在没有人需要它时才会删除底层资源,它是 Rc,代表引用计数。
如何做到...
-
在
bin文件夹中,创建一个名为shared.rs的文件。 -
添加以下代码,并使用
cargo run --bin shared运行它:
1 use std::rc::Rc;
2
3 // The ball will survive until all kids are done playing with
it
4 struct Kid {
5 ball: Rc<Ball>,
6 }
7 struct Ball;
8
9 fn main() {
10 {
11 // rc is created and count is at 1
12 let foo = Rc::new("foo");
13 // foo goes out of scope; count decreases
14 // count is zero; the object gets destroyed
15 }
16
17 {
18 // rc is created and count is at 1
19 let bar = Rc::new("bar");
20 // rc is cloned; count increases to 2
21 let second_bar = Rc::clone(&bar);
22 // bar goes out of scode; count decreases to 1
23 // bar goes out of scode; count decreases to 0
24 }
25
26 {
27 // rc is created and count is at 1
28 let baz = Rc::new("baz");
29 {
30 // rc is cloned; count increases to 2
31 let second_baz = Rc::clone(&baz);
32 // second_baz goes out of scode; count decreases to 1
33 }
34 // baz goes out of scode; count decreases to 0
35 }
36 let kid_one = spawn_kid_with_new_ball();
37 let kid_two = Kid {
38 ball: Rc::clone(&kid_one.ball),
39 };
40 let kid_three = Kid {
41 ball: Rc::clone(&kid_one.ball),
42 };
43 // ball lives until here
44 }
45
46 fn spawn_kid_with_new_ball() -> Kid {
47 let ball = Rc::new(Ball);
48 Kid {
49 ball: Rc::clone(&ball),
50 }
51 // Although the ball goes out of scope here, the object
behind it
52
53 // will survive as part of the kid
54 }
它是如何工作的...
Rc 的核心是其内部计数器,它记录当前有多少个对象拥有它。每次 Rc 被克隆时,计数器增加一,每次其克隆中的一个超出作用域时,计数器减少一。当这个计数器达到零时,Rc 后的对象将被销毁。main 方法包含一些注释示例,说明计数器在其生命周期中达到的值,以帮助你理解该机制的工作原理。
展示的简单规则的效果是,位于 Rc 后的资源只有在不再被使用时才会被删除,由于持续计数导致的运行时性能损失非常小,因此成本极低。这种延迟删除的效果非常适合在对象之间共享的资源。只需将它们包装在 Rc 中,它们就会确保一切都能存活足够长的时间。这相当于 C++ 中的 shared_ptr。
还有更多...
在一个边缘情况下,引用计数可能导致内存泄漏,即意外地阻止资源被删除。这种情况发生在存在两个对象,它们都包含指向对方的 Rc 时。由于这种循环依赖,它们都不会停止使用对方,因此这两个对象将在你的代码停止使用它们很久之后继续存在并指向对方。这里的解决方案是选择层次结构中的较薄弱环节,并用 Weak 替换其 Rc,Weak 包含一个非拥有引用。然而,由于这种情况相当罕见,我们不会详细讨论它。相反,只需记住内存泄漏的可能性,并在出现时再次阅读此内容。
Rc本质上是单线程的。如果你需要在多线程环境中(例如我们将在第七章[ca93ce61-1a86-4588-9da0-766bed49876f.xhtml]中探讨的,并行性和 Rayon;在多线程闭包中共享资源)使用其功能,你可以使用Arc代替。它代表原子引用计数,并且与Rc的行为相同。
参见
- 第七章[ca93ce61-1a86-4588-9da0-766bed49876f.xhtml]中的在多线程闭包中共享资源食谱
与内部可变性一起工作
尽管 Rust 的借用检查器是其最大的卖点之一,与它巧妙地错误处理的概念和令人印象深刻的工具一起,但它还不能读心。有时你可能必须亲自处理并手动借用对象。这是通过内部可变性的概念来完成的,它表明某些类型可以在非可变的情况下包装对象,并仍然可以对其进行可变操作。
如何实现...
-
在
bin文件夹中,创建一个名为interior_mutability.rs的文件。 -
添加以下代码,并使用
cargo test --bin interior_mutability运行它:
1 trait EmailSender {
2 fn send_mail(&self, msg: &Email) -> Option<String>;
3 }
4
5 #[derive(Debug, Clone)]
6 struct Email {
7 from: String,
8 to: String,
9 msg: String,
10 }
11
12 #[derive(Debug)]
13 struct Customer {
14 address: String,
15 wants_news: bool,
16 }
17
18 // Send news to every customer that wants to receive them
19 fn publish_news(msg: &str, sender: &EmailSender, customers: &
[Customer]) -> Option<i32> {
20 let mut count = 0;
21 let mut mail = Email {
22 from: "Rust Newsletter".to_string(),
23 to: "".to_string(),
24 msg: msg.to_string(),
25 };
26 for customer in customers {
27 if !customer.wants_news {
28 continue;
29 }
30 mail.to = customer.address.to_string();
31 if sender.send_mail(&mail).is_none() {
32 return None;
33 }
34 count += 1;
35 }
36 Some(count)
37 }
38
39 fn main() {
40 // No code running as we are concentrating on the tests instead
41 }
42
43
44 #[cfg(test)]
45 mod tests {
46 use super::*;
47 use std::cell::RefCell;
48
49 struct MockEmailSender {
50 // sent_mails can be modified even if MockEmailSender is
immutable
51 sent_mails: RefCell<Vec<Email>>,
52 }
53 impl MockEmailSender {
54 fn new() -> Self {
55 MockEmailSender {
56 sent_mails: RefCell::new(Vec::new()),
57 }
58 }
59 }
60
61 impl EmailSender for MockEmailSender {
62 fn send_mail(&self, msg: &Email) -> Option<String> {
63 // Borrow sent_mails mutably
64 self.sent_mails.borrow_mut().push(msg.clone());
65 Some("200 OK".to_string())
66 }
67 }
68
69 #[test]
70 fn sends_zero_to_zero_customers() {
71 let sent = publish_news("hello world!",
&MockEmailSender::new(), &[]);
72 assert_eq!(Some(0), sent);
73 }
74
75 #[test]
76 fn sends_one_to_one_willing() {
77 let customer = Customer {
78 address: "herbert@herbert.com".to_string(),
79 wants_news: true,
80 };
81 let sent = publish_news("hello world!",
&MockEmailSender::new(), &[customer]);
82 assert_eq!(Some(1), sent);
83 }
84
85 #[test]
86 fn sends_none_to_unwilling() {
87 let customer_one = Customer {
88 address: "herbert@herbert.com".to_string(),
89 wants_news: false,
90 };
91 let customer_two = Customer {
92 address: "michael@jackson.com".to_string(),
93 wants_news: false,
94 };
95 let sent = publish_news(
96 "hello world!",
97 &MockEmailSender::new(),
98 &[customer_one, customer_two],
99 );
100 assert_eq!(Some(0), sent);
101 }
102
103 #[test]
104 fn sends_correct_mail() {
105 let customer = Customer {
106 address: "herbert@herbert.com".to_string(),
107 wants_news: true,
108 };
109 let sender = MockEmailSender::new();
110 publish_news("hello world!", &sender, &
[customer]).expect("Failed to send mail");
111
112 // Borrow sent_mails immutable
113 let mails = sender.sent_mails.borrow();
114 assert_eq!(1, mails.len());
115 assert_eq!("Rust Newsletter", mails[0].from);
116 assert_eq!("herbert@herbert.com", mails[0].to);
117 assert_eq!("hello world!", mails[0].msg);
118 }
119 }
它是如何工作的...
本食谱的主角是RefCell,它是一个任何类型的包装器,将借用检查器的规则执行从编译时移动到运行时。基本操作很简单,通过调用.borrow()来不可变地借用底层值,通过调用.borrow_mut()来可变地借用。如果你不遵循只有多个读者或同时只有一个写者的黄金法则,程序将进入panic!状态。这种应用的一个例子是即使你的结构体本身是不可变的,也可以使你的结构体的成员可变。展示这种用法最有用的案例是模拟,这是为了测试目的而伪造基础设施的艺术。
我们的示例想法如下,我们希望向所有感兴趣的客户发送新闻通讯。为此,我们有一个EmailSender特性[1],它只指定了一个发送Email并返回响应的方法[2]。尝试通过特性来定义功能是一种良好的实践,以便对其进行模拟。
我们的publish_news函数[19]接受一个消息、一个EmailSender和一个Customer切片(请不要字面地想象),并将消息发送给所有希望接收新闻的客户。如果它遇到错误,它返回None[32],否则,它返回它发送的新闻通讯数量[36]。
你可能不想每次测试代码时都向客户发送电子邮件,这就是为什么我们在测试配置中创建MockEmailSender[49],它实际上不会发送任何东西,而是将所有邮件存储在一个Vec [64]中。为了做到这一点,它需要修改其成员,尽管它是不可变的。这正是RefCell的作用!多亏了它,我们可以有效地测试publish_news(),因为我们可以访问它将发送的所有消息,并将它们与我们期望的内容进行比较。
还有更多...
有许多类型使用内部可变性。其中之一是Cell,它不是分发引用,而是简单地复制它存储的值。当存储原始类型,如i32或bool时,这很好,因为它们都实现了复制。
其他类型包括RwLock和Mutex,它们对于并行性非常重要,正如我们将在第七章的配方中看到,即第七章,并行性和 Rayon;使用 RwLock 并行访问资源。
参见
- 第七章,并行性和 Rayon中的使用 RwLock 并行访问资源配方
第六章:处理错误
在本章中,我们将涵盖以下配方:
-
提供用户定义的错误类型
-
提供日志记录
-
创建自定义记录器
-
实现 Drop 特性
-
理解 RAII
简介
错误会发生,这是正常的。毕竟,我们都是凡人。生活和编程中的重要事情不是我们犯了什么错误,而是我们如何处理它们。Rust 通过为我们提供一个错误处理概念来帮助我们处理这一原则的编程方面,该概念保证我们在处理可能失败的函数时必须考虑失败的结果,因为它们不直接返回值,而是被包裹在一个必须以某种方式打开的Result中。唯一剩下的事情是以一种与这个概念很好地集成的代码方式设计我们的代码。
提供用户定义的错误类型
在前面的章节中,我们了解了几种处理必须处理不同类型错误的函数的方法。到目前为止,我们有:
-
遇到它们时直接崩溃
-
只返回一种错误,并将所有其他错误转换为它
-
在
Box中返回不同类型的错误
大多数这些方法都已被使用,因为我们还没有达到这个配方。现在,我们将学习做事的优选方式,创建一种包含多个子错误的自定义Error类型。
如何做到这一点...
-
使用
cargo new chapter-six创建一个 Rust 项目,在本章中工作。 -
导航到新创建的
chapter-six文件夹。在本章的其余部分,我们将假设您的命令行当前位于此目录。 -
在
src文件夹内创建一个名为bin的新文件夹。 -
删除生成的
lib.rs文件,因为我们不是创建一个库。 -
在
src/bin文件夹中创建一个名为custom_error.rs的文件。 -
添加以下代码,并使用
cargo run --bin custom_error运行它:
1 use std::{error, fmt, io, num, result};
2 use std::fs::File;
3 use std::io::{BufReader, Read};
4
5 #[derive(Debug)]
6 // This is going to be our custom Error type
7 enum AgeReaderError {
8 Io(io::Error),
9 Parse(num::ParseIntError),
10 NegativeAge(),
11 }
12
13 // It is common to alias Result in an Error module
14 type Result<T> = result::Result<T, AgeReaderError>;
15
16 impl error::Error for AgeReaderError {
17 fn description(&self) -> &str {
18 // Defer to the existing description if possible
19 match *self {
20 AgeReaderError::Io(ref err) => err.description(),
21 AgeReaderError::Parse(ref err) => err.description(),
22 // Descriptions should be as short as possible
23 AgeReaderError::NegativeAge() => "Age is negative",
24 }
25 }
26
27 fn cause(&self) -> Option<&error::Error> {
28 // Return the underlying error, if any
29 match *self {
30 AgeReaderError::Io(ref err) => Some(err),
31 AgeReaderError::Parse(ref err) => Some(err),
32 AgeReaderError::NegativeAge() => None,
33 }
34 }
35 }
36
37 impl fmt::Display for AgeReaderError {
38 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
39 // Write a detailed description of the problem
40 match *self {
41 AgeReaderError::Io(ref err) => write!(f, "IO error: {}",
err),
42 AgeReaderError::Parse(ref err) => write!(f, "Parse
error: {}", err),
43 AgeReaderError::NegativeAge() => write!(f, "Logic error:
Age cannot be negative"),
44 }
45 }
46 }
47
48 // Implement From<T> for every sub-error
49 impl From<io::Error> for AgeReaderError {
50 fn from(err: io::Error) -> AgeReaderError {
51 AgeReaderError::Io(err)
52 }
53 }
54
55 impl From<num::ParseIntError> for AgeReaderError {
56 fn from(err: num::ParseIntError) -> AgeReaderError {
57 AgeReaderError::Parse(err)
58 }
59 }
60
61 fn main() {
62 // Assuming a file called age.txt exists
63 const FILENAME: &str = "age.txt";
64 let result = read_age(FILENAME);
65 match result {
66 Ok(num) => println!("{} contains the age {}", FILENAME,
num),
67 Err(AgeReaderError::Io(err)) => eprintln!("Failed to open
the file {}: {}", FILENAME, err),
68 Err(AgeReaderError::Parse(err)) => eprintln!(
69 "Failed to read the contents of {} as a number: {}",
70 FILENAME, err
71 ),
72 Err(AgeReaderError::NegativeAge()) => eprintln!("The age in
the file is negative"),
73 }
74 }
75
76 // Read an age out of a file
77 fn read_age(filename: &str) -> Result<i32> {
78 let file = File::open(filename)?;
79 let mut buf_reader = BufReader::new(file);
80 let mut content = String::new();
81 buf_reader.read_to_string(&mut content)?;
82 let age: i32 = content.trim().parse()?;
83 if age.is_positive() {
84 Ok(age)
85 } else {
86 Err(AgeReaderError::NegativeAge())
87 }
88 }
它是如何工作的...
我们的示例目的是读取文件age.txt,并返回其中写下的数字,假设它代表某种年龄。在这个过程中,我们可能会遇到三种错误:
-
读取文件失败(可能它不存在)
-
读取其内容作为数字失败(它可能包含文本)
-
数字可能是负数
这些可能的错误状态是Error enum的可能变体:AgeReaderError[7]。通常,我们会根据它们所代表的子错误来命名变体。因为读取文件失败会引发io::Error,所以我们把对应的变体命名为AgeReaderError::Io[8]。将&str解析为i32失败会引发num::ParseIntError,所以我们把包含的变体命名为AgeReaderError::Parse[9]。
这两个std错误清楚地展示了错误命名的约定。如果你有一个模块可以返回许多不同的错误,通过它们的完整名称导出它们,例如num::ParseIntError。如果你的模块只返回一种Error,只需将其导出为Error,例如io::Error。我们故意不遵循这个约定,因为在配方中,独特的名称AgeReaderError使得讨论它更容易。如果这个配方逐个包含在一个 crate 中,我们可以通过将其导出为pub type Error = AgeReaderError;来实现传统效果。
我们接下来创建的是我们自己的Result[14]的别名:
type Result<T> = result::Result<T, AgeReaderError>;
这是你自己错误的一个极其常见的模式,这使得与它们一起工作变得非常愉快,正如我们在read_age的返回类型中看到的那样[77]:
fn read_age(filename: &str) -> Result<i32> { ... }
看起来不错,不是吗?然而,为了使用我们的enum作为Error,我们首先需要实现它[16]。Error特质需要两件事:一个description[17],它是对发生错误的一个简短解释,以及一个cause[27],它简单地是将错误重定向到底层错误(如果有的话)。你可以(并且应该)通过实现Display为你的Error[37]来提供关于当前问题的详细描述。在这些实现中,如果可能的话,你应该参考底层错误,就像以下行[20]所示:
AgeReaderError::Io(ref err) => err.description()
你需要为良好的自定义Error提供每个子错误的From实现。在我们的例子中,这将涉及From<io::Error>[49]和From<num::ParseIntError>[55]。这样,try操作符(?)将自动为我们转换涉及到的错误。
在实现所有必要的特性之后,你可以在任何函数中返回自定义的Error,并使用前面提到的操作符来解包其中的值。在这个例子中,当我们检查read_age的结果时,我们不需要match返回的值。在一个真正的main函数中,我们可能只是简单地调用.expect("…"),但我们仍然匹配了单独的错误变体,以便向您展示在使用已知的错误类型时,您如何优雅地应对不同的问题[65 到 73]:
match result {
Ok(num) => println!("{} contains the age {}", FILENAME, num),
Err(AgeReaderError::Io(err)) => eprintln!("Failed to open the file
{}: {}", FILENAME, err),
Err(AgeReaderError::Parse(err)) => eprintln!(
"Failed to read the contents of {} as a number: {}",
FILENAME, err
),
Err(AgeReaderError::NegativeAge()) => eprintln!("The age in the file is negative"),
}
还有更多...
为了组织原因,一个 crate 的Error通常被放在一个自定义的error模块中,然后直接导出以实现最佳可用性。相关的lib.rs条目可能看起来像这样:
mod error;
pub use error::Error;
提供日志记录
在一个大型应用程序中,事情迟早会不如预期。但没关系,只要你为用户提供了一个系统,让他们知道出了什么问题,如果可能的话,为什么,那就行了。一个经过时间考验的工具是详细的日志,它允许用户自己指定他们想要看到多少诊断信息。
如何做到这一点...
按照以下步骤操作:
-
打开之前为你生成的
Cargo.toml文件。 -
在
[dependencies]下添加以下行:
log = "0.4.1"
env_logger = "0.5.3"
-
如果你想,你可以访问
log(crates.io/crates/log) 或env_logger(crates.io/crates/env_log) 的 crate.io 页面,检查最新版本并使用那个版本。 -
在
bin文件夹中创建一个名为logging.rs的文件。 -
如果你使用的是基于 Unix 的系统,请添加以下代码并使用
RUST_LOG=logging cargo run --bin logging运行它。否则,在 Windows 上运行$env:RUST_LOG="logging"; cargo run --bin logging:
1 extern crate env_logger;
2 #[macro_use]
3 extern crate log;
4 use log::Level;
5
6 fn main() {
7 // env_logger's priority levels are:
8 // error > warn > info > debug > trace
9 env_logger::init();
10 // All logging calls log! in the background
11 log!(Level::Debug, "env_logger has been initialized");
12
13 // There are convenience macros for every logging level
however
14 info!("The program has started!");
15
16 // A log's target is its parent module per default
17 // ('logging' in our case, as we're in a binary)
18 // We can override this target however:
19 info!(target: "extra_info", "This is additional info that
will only show if you \
20 activate info level logging for the extra_info target");
21
22 warn!("Something that requires your attention happened");
23
24 // Only execute code if logging level is active
25 if log_enabled!(Level::Debug) {
26 let data = expensive_operation();
27 debug!("The expensive operation returned: \"{}\"", data);
28 }
29
30 error!("Something terrible happened!");
31 }
32
33 fn expensive_operation() -> String {
34 trace!("Starting an expensive operation");
35 let data = "Imagine this is a very very expensive
task".to_string();
36 trace!("Finished the expensive operation");
37 data
38 }
它是如何工作的...
Rust 的日志系统基于log crate,它为所有日志事物提供了一个共同的外观。这意味着它实际上并不提供任何功能,只是提供了接口。实现留给其他 crate,在我们的例子中是env_logger。这种外观和实现的分离非常实用,因为任何人都可以创建一种新的、酷的日志方式,这会自动与任何 crate 兼容。
应由你代码的使用者来决定使用的日志实现。如果你编写了一个 crate,不要使用任何实现,而应仅通过log crate 来记录所有事物。然后,使用该 crate 的(或他人的)可执行文件可以简单地初始化他们选择的日志记录器[9],以便实际处理日志调用。
log crate 提供了log!宏[11],它接受一个日志Level,一个可以像println!一样格式化的消息,以及一个可选的target。你可以这样记录事物,但使用每个日志级别的便利宏(error!、warn!、info!、debug!和trace!)更易于阅读,这些宏在后台简单地调用log!。日志的target[19]是一个额外的属性,有助于日志实现根据主题分组日志。如果你省略了target,它默认为当前的module。例如,如果你从foo crate 记录了某些内容,它的target将默认为foo。如果你在其子模块foo::bar中记录了某些内容,它的target将默认为bar。如果你然后在main.rs中使用了该 crate 并记录了某些内容,它的target将默认为main。
log提供的另一个好处是log_enabled!宏,它返回当前活动的日志记录器是否设置为处理特定的警告级别。这在与提供有用信息的昂贵操作的成本下特别有用。
env_logger 是 Rust 库提供的日志记录实现。它在其日志上打印到 stderr,并在你的终端支持的情况下使用漂亮的颜色来表示不同的日志级别。它依赖于一个 RUST_LOG 环境变量来过滤应该显示哪些日志。如果你没有定义这个变量,它将默认为 error,这意味着它将只打印来自所有目标的 error 级别的日志。正如你所猜到的,其他可能的值包括 warn、info、debug 和 trace。然而,这些不仅会过滤指定的级别,还会过滤其 以上 的所有级别,其中层次结构定义如下:
error > warn > info > debug > trace
这意味着将你的 RUST_LOG 设置为 warn 将显示所有 warn 和 error 级别的日志。将其设置为 debug 将显示 error、warn、info 和 debug 级别的日志。
你可以将 RUST_LOG 设置为目标,而不是错误级别,这将显示所选目标的全部日志,无论它们的日志级别如何。这就是我们在示例中所做的,我们将 RUST_LOG 设置为 logging 以显示所有带有 target 调用 logging 的日志,这是我们二进制中所有日志的标准目标。如果你想的话,你可以像这样将级别过滤器与目标过滤器结合使用:logging=warn,这将只显示 logging 目标中的 warn 和 error 级别的日志。
你可以使用逗号组合不同的过滤器。如果你想显示此示例的所有日志,你可以将你的变量设置为 logging,extra_info,这将过滤 logging 和 extra_info 目标。
最后,你可以使用斜杠 (/) 后跟一个正则表达式来通过内容过滤日志,该正则表达式必须匹配。例如,如果你将 RUST_LOG 设置为 logging=debug/expensive,则只有具有 logging 目标且包含单词 expensive 的 debug 级别以上的日志将被显示。
哇,这有很多配置!我建议你尝试不同的过滤模式,并运行示例以了解各个部分是如何结合在一起的。如果你需要更多信息,env_logger 当前版本中 RUST_LOG 值的所有可能性都在 docs.rs/env_logger/ 中有文档说明。
还有更多...
如果你以前从未使用过日志记录器,你可能想知道某些日志级别之间的区别。当然,你可以根据需要使用它们,但以下约定在许多语言的日志记录器中是常见的:
| 日志级别 | 用法 | 示例 |
|---|---|---|
Error |
发生了一些可能很快终止程序的重大问题。如果应用程序是一个应该始终运行的服务,系统管理员应立即被通知。 | 数据库连接已断开。 |
Warn |
发生了一些不是严重的问题或具有自动修复方法的错误。应该在某个时候有人检查并修复它。 | 一个用户的配置文件包含未识别的选项,这些选项已被忽略。 |
Info |
可能会在以后查看的一些有用信息。这记录了正常条件。 | 用户已启动或停止了一个进程。由于没有提供配置,已使用默认值。 |
Debug |
当尝试解决问题时对程序员或系统管理员有帮助的信息。与许多其他语言相反,调试日志在发布构建中不会被删除。 | 传递给主要函数的参数。应用程序在各个点的当前状态。 |
Trace |
只有在程序员试图追踪错误时才有用的非常低级别的控制流信号。允许重建堆栈跟踪。 | 辅助函数的参数。函数的开始和结束。 |
许多语言也包含一个Fatal日志级别。在 Rust 中,传统的panic!()用于此目的。如果您想以某种特殊方式记录您的恐慌,可以通过调用std::panic::set_hook()并传递您想要的任何功能来简单地将其打印到stderr,从而替换对恐慌的常规反应。以下是一个示例:
std::panic::set_hook(Box::new(|e| {
println!("Oh noes, something went wrong D:");
println!("{:?}", e);
}));
panic!("A thing broke");
env_logger的一个好替代品是slog包,它以陡峭的学习曲线为代价提供了出色的可扩展结构化日志。此外,它的输出看起来很漂亮。如果您对此感兴趣,请务必在github.com/slog-rs/slog.上查看。
创建一个自定义日志记录器
有时您或您的用户可能会有非常具体的日志需求。在这个配方中,我们将学习如何创建一个自定义日志记录器以与log包一起使用。
如何做到这一点...
-
打开之前为您生成的
Cargo.toml文件。 -
在
[dependencies]部分下,如果您在上一个配方中没有这样做,请添加以下行:
log = "0.4.1"
如果您愿意,您可以访问日志的 crates.io 页面(crates.io/crates/log)以检查最新版本,并使用该版本。
-
在
bin文件夹中创建一个名为custom_logger.rs的文件。 -
如果您使用的是基于 Unix 的系统,请添加以下代码并使用
RUST_LOG=custom_logger cargo run --bin custom_logger运行它。否则,在 Windows 上运行$env:RUST_LOG="custom_logger"; cargo run --bin custom_logger:
1 #[macro_use]
2 extern crate log;
3
4 use log::{Level, Metadata, Record};
5 use std::fs::{File, OpenOptions};
6 use std::io::{self, BufWriter, Write};
7 use std::{error, fmt, result};
8 use std::sync::RwLock;
9 use std::time::{SystemTime, UNIX_EPOCH};
10
11 // This logger will write logs into a file on disk
12 struct FileLogger {
13 level: Level,
14 writer: RwLock<BufWriter<File>>,
15 }
16
17 impl log::Log for FileLogger {
18 fn enabled(&self, metadata: &Metadata) -> bool {
19 // Check if the logger is enabled for a certain log level
20 // Here, you could also add own custom filtering based on
targets or regex
21 metadata.level() <= self.level
22 }
23
24 fn log(&self, record: &Record) {
25 if self.enabled(record.metadata()) {
26 let mut writer = self.writer
27 .write()
28 .expect("Failed to unlock log file writer in write
mode");
29 let now = SystemTime::now();
30 let timestamp = now.duration_since(UNIX_EPOCH).expect(
31 "Failed to generate timestamp: This system is
operating before the unix epoch",
32 );
33 // Write the log into the buffer
34 write!(
35 writer,
36 "{} {} at {}: {}\n",
37 record.level(),
38 timestamp.as_secs(),
39 record.target(),
40 record.args()
41 ).expect("Failed to log to file");
42 }
43 self.flush();
44 }
45
46 fn flush(&self) {
47 // Write the buffered logs to disk
48 self.writer
49 .write()
50 .expect("Failed to unlock log file writer in write
mode")
51 .flush()
52 .expect("Failed to flush log file writer");
53 }
54 }
55
56 impl FileLogger {
57 // A convenience method to set everything up nicely
58 fn init(level: Level, file_name: &str) -> Result<()> {
59 let file = OpenOptions::new()
60 .create(true)
61 .append(true)
62 .open(file_name)?;
63 let writer = RwLock::new(BufWriter::new(file));
64 let logger = FileLogger { level, writer };
65 // set the global level filter that log uses to optimize
ignored logs
66 log::set_max_level(level.to_level_filter());
67 // set this logger as the one used by the log macros
68 log::set_boxed_logger(Box::new(logger))?;
69 Ok(())
70 }
71 }
这是我们在日志记录器中使用的自定义错误:
73 // Our custom error for our FileLogger
74 #[derive(Debug)]
75 enum FileLoggerError {
76 Io(io::Error),
77 SetLogger(log::SetLoggerError),
78 }
79
80 type Result<T> = result::Result<T, FileLoggerError>;
81 impl error::Error for FileLoggerError {
82 fn description(&self) -> &str {
83 match *self {
84 FileLoggerError::Io(ref err) => err.description(),
85 FileLoggerError::SetLogger(ref err) =>
err.description(),
86 }
87 }
88
89 fn cause(&self) -> Option<&error::Error> {
90 match *self {
91 FileLoggerError::Io(ref err) => Some(err),
92 FileLoggerError::SetLogger(ref err) => Some(err),
93 }
94 }
95 }
96
97 impl fmt::Display for FileLoggerError {
98 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
99 match *self {
100 FileLoggerError::Io(ref err) => write!(f, "IO error: {}",
err),
101 FileLoggerError::SetLogger(ref err) => write!(f, "Parse
error: {}", err),
102 }
103 }
104 }
105
106 impl From<io::Error> for FileLoggerError {
107 fn from(err: io::Error) -> FileLoggerError {
108 FileLoggerError::Io(err)
109 }
110 }
111
112 impl From<log::SetLoggerError> for FileLoggerError {
113 fn from(err: log::SetLoggerError) -> FileLoggerError {
114 FileLoggerError::SetLogger(err)
115 }
116 }
初始化和使用日志记录器:
118 fn main() {
119 FileLogger::init(Level::Info, "log.txt").expect("Failed to
init
FileLogger");
120 trace!("Beginning the operation");
121 info!("A lightning strikes a body");
122 warn!("It's moving");
123 error!("It's alive!");
124 debug!("Dr. Frankenstein now knows how it feels to be god");
125 trace!("End of the operation");
126 }
它是如何工作的...
我们的自定义FileLogger,正如其名所示,会将日志记录到文件中。它还接受初始化时的最大日志级别。
你不应该习惯直接在磁盘上记录日志。正如 The Twelve-Factor App 指南 (12factor.net/logs) 所述,日志应该被视为事件流,以原始转储到 stdout 的形式。然后,生产环境可以将所有日志流通过 systemd 或专门的日志路由器(如 Logplex (github.com/heroku/logplex) 或 Fluentd github.com/fluent/fluentd)路由到最终目的地。这些路由器将决定日志是否应该发送到文件、分析系统(如 Splunk www.splunk.com/)或数据仓库(如 Hive hive.apache.org/)。
每个日志记录器都需要实现 log::Log 特性,该特性包括 enabled、log 和 flush 方法。enabled 应该返回是否接受某个日志事件。在这里,你可以随心所欲地使用你想要的任何过滤逻辑[18]。这个方法永远不会直接被 log 调用,所以它的唯一目的是作为 log 方法中的辅助方法,我们将在稍后讨论。flush [46] 被以相同的方式处理。它应该应用你在日志中缓存的任何更改,但它永远不会被 log 调用。
事实上,如果你的日志记录器不与文件系统或网络交互,它可能只是简单地通过不执行任何操作来实现 flush:
fn flush(&self) {}
然而,Log 实现的核心是 log 方法[24],因为每当调用日志宏时都会调用它。实现通常从以下行开始,然后是实际的日志记录,最后调用 self.flush()[25]:
if self.enabled(record.metadata()) {
我们实际的日志记录操作只是简单地组合当前日志级别、Unix 时间戳、目标地址和日志消息,然后将这些内容写入文件,并在之后刷新。
从技术上来说,我们的 self.flush() 调用也应该在 if 块内部,但这将需要围绕可变借用 writer 的额外作用域,以避免两次借用。由于这与这里的根本课程无关,即如何创建一个日志记录器,我们将它放在块外,以便使示例更易于阅读。顺便说一下,我们从 RwLock 借用 writer 的方式是 第八章 中 使用 RwLock 并行访问资源 的主题。现在,只需知道 RwLock 是一个在并行环境(如日志记录器)中安全使用的 RefCell 就足够了。
在为 FileLogger 实现 Log 之后,用户可以使用它作为 log 调用的日志记录器。为此,用户需要做两件事:
-
通过
log::set_max_level()[66]告诉log日志级别,这是我们记录器能接受的最大日志级别。这是必需的,因为如果使用超过我们最大级别的日志级别,log会在运行时优化我们的记录器上的.log()调用。该函数接受一个LevelFilter而不是Level,这就是为什么我们首先需要用to_level_filter()将我们的级别转换的原因 [66]。这种类型的原因在更多内容...部分中解释。 -
使用
log::set_boxed_logger()[68]指定记录器。log接受一个盒子,因为它将其记录器实现视为一个特质对象,我们在第五章,高级数据结构的装箱数据部分讨论过。如果你想要(非常)小的性能提升,你也可以使用log::set_logger(),它接受一个static,你首先需要通过lazy_staticcrate 创建它。关于这一点,请参阅第五章,高级数据结构和配方创建懒加载静态对象。
这通常在提供的.init()方法上完成,就像env_logger一样,我们在第[58]行实现它。
fn init(level: Level, file_name: &str) -> Result<()> {
let file = OpenOptions::new()
.create(true)
.append(true)
.open(file_name)?;
let writer = RwLock::new(BufWriter::new(file));
let logger = FileLogger { level, writer };
log::set_max_level(level.to_level_filter());
log::set_boxed_logger(Box::new(logger))?;
Ok(())
}
当我们谈论这个话题时,我们也可以用同样的方法打开文件。其他可能性包括让用户直接将File传递给init作为参数,或者为了最大的灵活性,使记录器成为一个泛型记录器,它接受任何实现Write的流。
然后,我们在随后的行[74 到 116]返回一个自定义错误。
我们记录器的初始化示例可能看起来像这样 [119]:
FileLogger::init(Level::Info, "log.txt").expect("Failed to init FileLogger");
更多内容...
为了简单起见,FileLogger不对任何目标进行区分。一个更复杂的记录器,如env_logger,可以在不同的目标上设置不同的日志级别。为此,log为我们提供了LevelFilter枚举,它有一个Off状态,对应于为此目标未启用日志记录。如果你需要创建这样的记录器,务必记住这个枚举。你可以通过查看env_logger的源代码来获取一些关于如何实现基于目标的过滤器的灵感,源代码位于github.com/sebasmagri/env_logger/blob/master/src/filter/mod.rs。
在一个真正用户友好的记录器中,你希望显示用户自己的本地时间戳。对于与时间测量、时区和日期相关的一切,请查看chrono crate,位于crates.io/crates/chrono。
参见
-
在第五章,高级数据结构中的装箱数据配方
-
在第五章**,高级数据结构中的创建懒加载静态对象*配方
-
在第七章,并行和 Rayon中的使用 RwLocks 并行访问资源配方
实现 Drop 特性
在传统面向对象语言中,有析构函数,而 Rust 有 Drop 特性,它由一个单一的 drop 函数组成,当变量的生命周期结束时会被调用。通过实现它,你可以执行任何必要的清理或高级日志记录。你还可以通过 RAII 自动释放资源,正如我们将在下一个菜谱中看到的。
如何做...
-
在
bin文件夹中创建一个名为drop.rs的文件。 -
添加以下代码,并使用
cargo run --bin drop运行它:
1 use std::fmt::Debug;
2
3 struct CustomSmartPointer<D>
4 where
5 D: Debug,
6 {
7 data: D,
8 }
9
10 impl<D> CustomSmartPointer<D>
11 where
12 D: Debug,
13 {
14 fn new(data: D) -> Self {
15 CustomSmartPointer { data }
16 }
17 }
18
19 impl<D> Drop for CustomSmartPointer<D>
20 where
21 D: Debug,
22 {
23 // This will automatically be called when a variable is
dropped
24 // It cannot be called manually
25 fn drop(&mut self) {
26 println!("Dropping CustomSmartPointer with data `{:?}`",
self.data);
27 }
28 }
29
30 fn main() {
31 let a = CustomSmartPointer::new("A");
32 let b = CustomSmartPointer::new("B");
33 let c = CustomSmartPointer::new("C");
34 let d = CustomSmartPointer::new("D");
35
36 // The next line would cause a compiler error,
37 // as destructors cannot be explicitely called
38 // c.drop();
39
40 // The correct way to drop variables early is the following:
41 std::mem::drop(c);
42 }
它是如何工作的...
这个例子是从 Rust 书的第二版中稍作修改后得到的(doc.rust-lang.org/book/second-edition/),展示了如何开始实现自定义智能指针。在我们的例子中,它所做的只是在其被销毁时打印存储数据的 Debug 信息[26]。我们通过实现 Drop 特性及其单一的 drop 函数[25]来完成这项工作,编译器会在变量被销毁时自动调用这个函数。所有智能指针都是这样实现的。
变量销毁的时刻几乎总是它离开作用域的时候。因此,我们不能直接调用 drop 函数[38]。当它退出作用域时,编译器仍然会调用它,所以清理将会发生两次,导致未定义的行为。如果你需要提前销毁一个变量,你可以通过在它上面调用 std::mem:drop 来告诉编译器这样做 [41]。
变量退出其作用域时,会以 LIFO 方式被销毁:后进先出。这意味着最后声明的变量将是第一个被销毁的。如果我们按照 a、b、c 和 d 的顺序分配变量,它们将被销毁的顺序将是 d、c、b、a。在我们的例子中,我们提前销毁 c[41],所以我们的顺序变成了 c、d、b、a。
还有更多...
你想知道一个像 std::mem::drop 这样的复杂低级函数是如何实现的吗:
pub fn drop<T>(_x: T) { }
没错,它什么也没做!这样做的原因是它通过值传递 T,将其移动到函数中。函数什么也不做,并且它所有的拥有变量都退出了作用域。为 Rust 的借用检查器欢呼!
另请参阅
-
在第五章的“装箱数据”菜谱中。
-
在第五章的“与智能指针共享所有权”菜谱中。
理解 RAII
我们可以比简单的析构函数更进一步。我们可以创建可以给用户提供临时访问某些资源或功能的结构体,并在用户完成时自动撤销访问。这个概念被称为 RAII,代表 Resource Acquisition Is Initialization。换句话说,资源的有效性与变量的生命周期绑定。
如何做...
按照以下步骤操作:
-
打开之前为你生成的
Cargo.toml文件。 -
在
bin文件夹中创建一个名为raii.rs的文件。 -
添加以下代码,并用
cargo run --bin raii运行它:
1 use std::ops::Deref;
2
3 // This represents a low level, close to the metal OS feature
that
4 // needs to be locked and unlocked in some way in order to be
accessed
5 // and is usually unsafe to use directly
6 struct SomeOsSpecificFunctionalityHandle;
7
8 // This is a safe wrapper around the low level struct
9 struct SomeOsFunctionality<T> {
10 // The data variable represents whatever useful information
11 // the user might provide to the OS functionality
12 data: T,
13 // The underlying struct is usually not savely movable,
14 // so it's given a constant address in a box
15 inner: Box<SomeOsSpecificFunctionalityHandle>,
16 }
17
18 // Access to a locked SomeOsFunctionality is wrapped in a guard
19 // that automatically unlocks it when dropped
20 struct SomeOsFunctionalityGuard<'a, T: 'a> {
21 lock: &'a SomeOsFunctionality<T>,
22 }
23
24 impl SomeOsSpecificFunctionalityHandle {
25 unsafe fn lock(&self) {
26 // Here goes the unsafe low level code
27 }
28 unsafe fn unlock(&self) {
29 // Here goes the unsafe low level code
30 }
31 }
现在是structs的实现:
33 impl<T> SomeOsFunctionality<T> {
34 fn new(data: T) -> Self {
35 let handle = SomeOsSpecificFunctionalityHandle;
36 SomeOsFunctionality {
37 data,
38 inner: Box::new(handle),
39 }
40 }
41
42 fn lock(&self) -> SomeOsFunctionalityGuard<T> {
43 // Lock the underlying resource.
44 unsafe {
45 self.inner.lock();
46 }
47
48 // Wrap a reference to our locked selves in a guard
49 SomeOsFunctionalityGuard { lock: self }
50 }
51 }
52
53 // Automatically unlock the underlying resource on drop
54 impl<'a, T> Drop for SomeOsFunctionalityGuard<'a, T> {
55 fn drop(&mut self) {
56 unsafe {
57 self.lock.inner.unlock();
58 }
59 }
60 }
61
62 // Implementing Deref means we can directly
63 // treat SomeOsFunctionalityGuard as if it was T
64 impl<'a, T> Deref for SomeOsFunctionalityGuard<'a, T> {
65 type Target = T;
66
67 fn deref(&self) -> &T {
68 &self.lock.data
69 }
70 }
最后,实际的用法:
72 fn main() {
73 let foo = SomeOsFunctionality::new("Hello World");
74 {
75 // Locking foo returns an unlocked guard
76 let bar = foo.lock();
77 // Because of the Deref implementation on the guard,
78 // we can use it as if it was the underlying data
79 println!("The string behind foo is {} characters long",
bar.len());
80
81 // foo is automatically unlocked when we exit this scope
82 }
83 // foo could now be unlocked again if needed
84 }
它是如何工作的...
嗯,这是一堆复杂的代码。
让我们先介绍参与这个示例的结构:
-
SomeOsSpecificFunctionalityHandle[6]代表操作系统的一个未指定功能,该功能操作某些数据,并且可能直接使用是不安全的。我们假设这个功能锁定了一些需要再次解锁的操作系统资源。 -
SomeOsFunctionality[9]代表围绕该功能的保护器,以及一些可能对它有用的数据T。 -
SomeOsFunctionalityGuard[20]是通过使用lock函数创建的 RAII 保护器。当它被丢弃时,它将自动解锁底层资源。此外,它可以直接用作数据T本身。
这些函数可能看起来有些抽象,因为它们并没有做任何具体的事情,而是作用于某些未指定的操作系统功能。这是因为大多数真正有用的候选者已经存在于标准库中——比如File、RwLock、Mutex等等。剩下的主要是特定领域的使用案例,当编写低级库或处理一些需要自动解锁的特殊、自制的资源时。当你发现自己正在编写这样的代码时,你会欣赏 RAII 的优雅。
结构体的实现引入了一些可能初次遇到时会显得有些令人困惑的新概念。在SomeOsSpecificFunctionalityHandle的实现中,我们可以看到一些unsafe关键字[25, 28 和 44]:
impl SomeOsSpecificFunctionalityHandle {
unsafe fn lock(&self) {
// Here goes the unsafe low level code
}
unsafe fn unlock(&self) {
// Here goes the unsafe low level code
}
}
...
fn lock(&self) -> SomeOsFunctionalityGuard<T> {
// Lock the underlying resource.
unsafe {
self.inner.lock();
}
// Wrap a reference to our locked selves in a guard
SomeOsFunctionalityGuard { lock: self }
}
让我们从不安全块[44 到 46]开始:
unsafe {
self.inner.lock();
}
unsafe关键字告诉编译器以特殊方式处理前面的代码块。它禁用了借用检查器,并允许你做各种疯狂的事情:像 C 语言一样解引用原始指针,修改可变静态变量,调用不安全函数。作为交换,编译器也不会给你任何保证。例如,它可能会访问无效的内存,导致SEGFAULT。如果你想了解更多关于unsafe关键字的信息,请查看官方 Rust 书籍第二版中关于它的章节doc.rust-lang.org/book/second-edition/ch19-01-unsafe-rust.html。
通常来说,应该避免编写不安全的代码。然而,在以下情况下这样做是可以接受的:
-
你正在编写一些直接与操作系统交互的代码,并且你想要创建一个围绕不安全部分的安全包装器,这正是我们在这里所做的事情。
-
你绝对 100%完全确信,在非常具体的上下文中,你所做的是没有问题的,这与编译器的观点相反。
如果你想知道为什么unsafe块是空的,那是因为我们在这个配方中没有使用任何实际的操作系统资源。如果你想使用,处理它们的代码将放在那两个空块中。
unsafe关键字的其他用途如下[25]:
unsafe fn lock(&self) { ... }
这将函数本身标记为不安全,意味着它只能在不安全的块中调用。记住,在函数中调用unsafe代码并不会使函数自动变为不安全,因为函数可能是一个围绕它的安全包装。
现在我们从假设的低级实现SomeOsSpecificFunctionalityHandle转移到我们对其安全包装SomeOsFunctionality[33]的现实实现。其构造函数没有惊喜(如果你需要复习,请参阅第一章,学习基础知识和使用构造函数模式配方):
fn new(data: T) -> Self {
let handle = SomeOsSpecificFunctionalityHandle;
SomeOsFunctionality {
data,
inner: Box::new(handle),
}
}
我们只是简单地准备底层的操作系统功能,并将其与用户提供的资料一起存储在我们的struct中。我们使用Box封装了句柄,因为,如代码中较早的注释(在第 13 行和第 14 行)所述,与操作系统交互的低级结构通常不安全移动。然而,我们不想限制用户移动我们的安全包装,因此,我们通过将句柄放入堆中并通过Box来使其可移动,这给它提供了一个永久地址。然后移动的仅仅是指向该地址的智能指针。关于这一点,请阅读第五章,高级数据结构和装箱数据配方。
实际的封装发生在lock方法中:
fn lock(&self) -> SomeOsFunctionalityGuard<T> {
// Lock the underlying resource.
unsafe {
self.inner.lock();
}
// Wrap a reference to our locked selves in a guard
SomeOsFunctionalityGuard { lock: self }
}
当与实际的操作系统功能或自定义资源一起工作时,你希望在这样做之前确保self.inner.lock()在这个上下文中是安全的,否则,包装器将不会安全。这也是你可以对self.data做有趣事情的地方,你可能会将其与提到的资源结合起来使用。
在锁定我们的东西之后,我们返回一个 RAII 保护器,它包含对我们结构的引用[49],当它被丢弃时将解锁我们的资源。查看SomeOsFunctionalityGuard的实现,你可以看到我们不需要为它实现任何新的函数。我们只需要实现两个特质。我们首先从Drop[54]开始,你在之前的配方中已经见过。实现它意味着我们可以在保护器通过SomeOsFunctionality的引用被丢弃时解锁资源。再次确保以某种方式安排环境,在调用self.lock.inner.unlock()之前确保它实际上是安全的。
由于我们基本上是在创建一个指向data的智能指针,我们可以使用Deref特质[64]。通过将Target设置为A实现Deref for B允许对B的引用被解引用到A。或者用稍微不准确的话来说,它让B表现得像A。在我们的例子中,通过将Target设置为T实现Deref for SomeOsFunctionalityGuard意味着我们可以像使用底层数据一样使用我们的守护者。因为如果实现不当,这可能会给用户带来很大的困惑,Rust 建议您只在对智能指针实现它,而不是其他任何东西。
实现Deref对于 RAII 模式来说当然不是强制的,但可能会非常有用,正如我们一会儿将要看到的。
让我们看看我们现在如何使用所有这些花哨的功能:
fn main() {
let foo = SomeOsFunctionality::new("Hello World");
{
let bar = foo.lock();
println!("The string behind foo is {} characters long",
bar.len());
}
}
用户永远不应该直接使用SomeOsSpecificFunctionalityHandle,因为它是不安全的。相反,他可以构建一个SomeOsFunctionality的实例,他可以随意传递和存储[73]。每次他需要使用它背后的酷功能时,他都可以在当前作用域中调用lock,并且他将收到一个在完成后会为他清理的守护者[81]。因为他实现了Deref,所以他可以直接使用守护者,就像它是底层数据一样。在我们的例子中,data是一个&str,所以我们可以在我们的守护者上直接使用str的方法,就像我们在第[79]行所做的那样,通过调用.len()。
在这个小范围结束之后,我们的守护者对资源调用unlock,因为foo独立地仍然存在,我们可以继续以我们想要的任何方式再次锁定它。
还有更多...
这个例子是为了与RwLock和Mutex的实现保持一致。唯一缺少的是为了不让这个配方更加复杂而省略的一个额外的间接层。SomeOsSpecificFunctionalityHandle不应该包含lock和unlock的实际实现,而应该将调用传递给存储的实现,该实现针对您正在使用的任何操作系统。例如,假设您有一个针对基于 Windows 的实现的结构体windows::SomeOsSpecificFunctionalityHandle和一个针对基于 Unix 的实现的结构体unix::SomeOsSpecificFunctionalityHandle。SomeOsSpecificFunctionalityHandle应该根据正在运行的操作系统,有条件地将它的lock和unlock调用传递到正确的实现。这些可能具有更多功能。Windows 的那个可能有一个awesome_windows_thing()函数,这可能对需要它的不幸的 Windows 开发者很有用。Unix 实现可能有一个confusing_posix_thing()函数,它做一些只有 Unix 黑客才能理解的非常奇怪的事情。重要的是,我们的SomeOsSpecificFunctionalityHandle应该代表实现的一个通用接口。在我们的例子中,这意味着每个支持的操作系统都有能力锁定和解除锁定相关的资源。
参见
-
使用构造器模式 的配方在 第一章,学习基础知识
-
数据装箱 的配方在 第五章,高级数据结构
-
使用 RwLocks 并行访问资源 的配方在 第七章,并行性与 Rayon
第七章:并行性与 Rayon
在本章中,我们将介绍以下食谱:
-
并行化迭代器
-
同时运行两个操作
-
在线程间发送数据
-
在多线程闭包中共享资源
-
使用 RwLocks 并行访问资源
-
原子访问原语
-
在连接处理器中将所有内容组合在一起
简介
曾经有一段时间,随着处理器的不断升级,你的代码每年都会自动变快。但如今,正如 Herb Sutter 著名地指出,免费午餐已经结束 (www.gotw.ca/publications/concurrency-ddj.htm)。处理器核心数量增加而不是性能提升的时代已经很久远了。并不是所有编程语言都适合这种向无处不在的并发性的根本转变。
Rust 的设计正是针对这个问题。它的借用检查器确保大多数并发算法都能正常工作。它甚至更进一步:如果你的代码不可并行化,即使你还没有使用超过一个线程,你的代码甚至无法编译。正因为这些独特的保证,Rust 的一个主要卖点被称作无畏并发。
我们即将找出原因。
并行化迭代器
要有一个魔法按钮,让你能够轻松地将任何算法并行化,而不需要你做任何事情,岂不是很好?嗯,只要你的算法使用迭代器,rayon就是那个东西!
如何做到这一点...
-
使用
cargo new chapter-seven创建一个 Rust 项目,以便在本章中工作。 -
导航到新创建的
chapter-seven文件夹。在本章的其余部分,我们将假设你的命令行当前位于此目录。 -
打开为你生成的
Cargo.toml文件。 -
在
[dependencies]下添加以下行:
rayon = "1.0.0"
如果你想,你可以访问rayon的 crates.io 页面(crates.io/crates/rayon),检查最新版本并使用它。
-
在
src文件夹内,创建一个名为bin的新文件夹。 -
删除生成的
lib.rs文件,因为我们不是在创建一个库。 -
在
src/bin文件夹中,创建一个名为par_iter.rs的文件。 -
添加以下代码,并使用
cargo run --bin par_iter运行它:
1 extern crate rayon;
2 use rayon::prelude::*;
3
4 fn main() {
5 let legend = "Did you ever hear the tragedy of Darth Plagueis
The Wise?";
6 let words: Vec<_> = legend.split_whitespace().collect();
7
8 // The following will execute in parallel,
9 // so the exact order of execution is not foreseeable
10 words.par_iter().for_each(|val| println!("{}", val));
11
12 // par_iter can do everything that a normal iterator does, but
13 // in parallel. This way you can easily parallelize any
algorithm
14 let words_with_a: Vec<_> = words
15 .par_iter()
16 .filter(|val| val.find('a').is_some())
17 .collect();
18
19 println!(
20 "The following words contain the letter 'a': {:?}",
21 words_with_a
22 );
23 }
它是如何工作的...
rayon为每个实现了其标准库等效Iterator的类型的ParallelIterator特例实现了特例,我们在第二章,使用集合;**将集合作为迭代器访问中了解到。实际上,你可以再次使用该食谱中的所有知识。ParallelIterator特例提供的方法几乎与Iterator提供的方法相同,所以几乎在所有你注意到迭代器操作耗时过长并成为瓶颈的情况下,你只需简单地将.iter()替换为.par_iter()[10]。同样,对于移动迭代器,你可以使用.into_par_iter()而不是.into_iter()。
rayon 会为你处理所有繁琐的工作,因为它会自动将工作均匀分配到所有可用的核心上。但请记住,尽管有这种魔法,你仍然在处理并行性,因此你无法保证迭代器中项的处理顺序,正如第 [10] 行所示,每次执行程序时都会以不同的顺序打印:
words.par_iter().for_each(|val| println!("{}", val));
参见
- 访问集合作为迭代器菜谱在第二章,与集合一起工作
同时运行两个操作
上一个菜谱中的并行迭代器在内部基于一个更基本的功能构建,即 rayon::join,它接受两个闭包并可能并行运行它们。这样,性能提升与启动线程开销之间的平衡也已经为你完成。
如果你有一个不使用迭代器但仍然由一些明显分离的部分组成且可以从并行运行中受益的算法,考虑使用 rayon::join。
如何做到这一点...
-
打开之前为你生成的
Cargo.toml文件。 -
如果你在上一个菜谱中没有这样做,请在
[dependencies]下添加以下行:
rayon = "1.0.0"
-
如果你想,你可以访问
rayon的 crates.io 页面(crates.io/crates/rayon),检查最新版本并使用它。 -
在
bin文件夹中创建一个名为join.rs的文件。 -
添加以下代码,并使用
cargo run --bin join运行:
1 extern crate rayon;
2
3 #[derive(Debug)]
4 struct Rectangle {
5 height: u32,
6 width: u32,
7 }
8
9 impl Rectangle {
10 fn area(&self) -> u32 {
11 self.height * self.width
12 }
13 fn perimeter(&self) -> u32 {
14 2 * (self.height + self.width)
15 }
16 }
17
18 fn main() {
19 let rect = Rectangle {
20 height: 30,
21 width: 20,
22 };
23 // rayon::join makes closures run potentially in parallel and
24 // returns their returned values in a tuple
25 let (area, perimeter) = rayon::join(|| rect.area(), ||
rect.perimeter());
26 println!("{:?}", rect);
27 println!("area: {}", area);
28 println!("perimeter: {}", perimeter);
29
30 let fib = fibonacci(6);
31 println!("The sixth number in the fibonacci sequence is {}",
fib);
32 }
33
34 fn fibonacci(n: u32) -> u32 {
35 if n == 0 || n == 1 {
36 n
37 } else {
38 // rayon::join can really shine in recursive functions
39 let (a, b) = rayon::join(|| fibonacci(n - 1), || fibonacci(n
- 2));
40 a + b
41 }
42 }
它是如何工作的...
rayon::join 非常简单。它接受两个闭包,可能并行运行它们,并以元组的形式返回它们的返回值 [25]。等等,我们刚刚说了可能?难道不是总是并行运行事物更好吗?
不,至少不是总是这样。当然,如果你真的在乎事物始终一起运行而不阻塞,比如说一个 GUI 及其底层的 I/O,你绝对不希望当打开文件时鼠标光标冻结,你总是需要所有进程在自己的线程中运行。但大多数并发应用程序没有这个要求。并发之所以如此重要的一个重要部分是它能够并行运行通常按顺序(即,一行接一行)运行的代码。注意这里的措辞—通常按顺序运行的代码。这类算法本身不需要并发,但它们可能会从中受益。现在来说说潜在的部分—启动一个线程可能并不值得。
为了理解原因,让我们看看硬件方面。我们不会深入这个领域,因为:
a) 你在读这本书的事实让我认为你更倾向于软件人员,b) CPU 的确切机制现在变化非常快,我们不希望这里提供的信息在一年后过时。
你的 CPU 将其工作分配给其 核心。核心是 CPU 的基本计算单元。如果你正在阅读此内容的设备不是用纸做的,并且不到二十年,它很可能包含多个核心。这类核心被称为 物理,可以同时处理不同的事情。物理核心本身也有执行多项任务的方法。一些可以将自己分成多个 逻辑 核心,进一步分割工作。例如,英特尔 CPU 可以使用 超线程,这意味着如果一个程序只使用物理核心的整数加法单元,一个虚拟核心可能会开始为另一个程序处理浮点数加法单元,直到第一个完成。
如果你不在乎可用的核心数量,并且无限制地启动新线程,操作系统将开始创建实际上不会并发运行的线程,因为它已经没有核心了。在这种情况下,它将执行 上下文切换,这意味着它存储线程的当前状态,暂停它,短暂地处理另一个线程,然后再次恢复线程。正如你可以想象的那样,这会消耗相当多的资源。
这就是为什么如果并行运行两件事不是至关重要,你应该首先检查是否真的有任何 空闲(即可用)的核心。因为 rayon::join 会为你做这个检查;除此之外,它只有在这样做真正值得时才会并行运行两个闭包。如果你需要自己执行这项工作,请查看 num_cpus crate (crates.io/crates/num_cpus)。
顺便说一下,上道菜谱中的并行迭代器更进一步:如果元素的数量和工作量如此之小,以至于为它们启动一个新线程的成本比顺序运行它们还要高,它们将自动放弃并发为你。
还有更多...
rayon 的底层机制是 工作窃取。这意味着当我们调用以下函数时,当前线程将立即开始处理 a 并将 b 放入队列:
rayon::join(a, b);
同时,每当一个核心空闲时,rayon 将允许它处理队列中的下一个任务。然后新线程会从其他线程中“窃取”任务。在我们的例子中,那将是 b。如果 a 比意外地先于 b 完成,主线程将检查队列并尝试窃取工作。如果递归函数中多次调用 rayon::join,队列可以包含超过两个项目。
rayon 的作者 Niko Matsakis 在他的介绍博客文章中写下了以下伪 Rust 代码,以说明这一原则:smallcultfollowing.com/babysteps/blog/2015/12/18/rayon-data-parallelism-in-rust/:
fn join<A,B>(oper_a: A, oper_b: B)
where A: FnOnce() + Send,
B: FnOnce() + Send,
{
// Advertise `oper_b` to other threads as something
// they might steal:
let job = push_onto_local_queue(oper_b);
// Execute `oper_a` ourselves:
oper_a();
// Check whether anybody stole `oper_b`:
if pop_from_local_queue(oper_b) {
// Not stolen, do it ourselves.
oper_b();
} else {
// Stolen, wait for them to finish. In the
// meantime, try to steal from others:
while not_yet_complete(job) {
steal_from_others();
}
result_b = job.result();
}
}
顺便说一下,这个例子中提供的递归斐波那契实现[34]很容易查看,并说明了使用rayon::join的目的,但也是非常不高效的。要了解为什么,以及如何改进它,请查看第十章 Chapter 10,使用实验性夜间功能;基准测试你的代码**.
参见
- 第十章 Chapter 10,使用实验性夜间功能中的基准测试你的代码配方
在多线程闭包中共享资源
是时候在更低的层面上查看并行性,而不需要任何帮助我们的 crate。我们现在将检查如何跨线程共享资源,以便它们都可以使用同一个对象。这个配方也将作为手动创建线程的复习,以防你很久以前就学过它。
如何做到...
-
在
bin文件夹中创建一个名为sharing_in_closures.rs的文件。 -
添加以下代码,并用
cargo run --bin sharing_in_closures运行它:
1 use std::thread;
2 use std::sync::Arc;
3
4 fn main() {
5 // An Arc ("Atomically Reference Counted") is used the exact
6 // same way as an Rc, but also works in a parallel context
7 let some_resource = Arc::new("Hello World".to_string());
8
9 // We use it to give a new thread ownership of a clone of the
Arc
10 let thread_a = {
11 // It is very common to give the clone the same name as the
original
12 let some_resource = some_resource.clone();
13 // The clone is then moved into the closure:
14 thread::spawn(move || {
15 println!("Thread A says: {}", some_resource);
16 })
17 };
18 let thread_b = {
19 let some_resource = some_resource.clone();
20 thread::spawn(move || {
21 println!("Thread B says: {}", some_resource);
22 })
23 };
24
25 // .join() blocks the main thread until the other thread is done
26 thread_a.join().expect("Thread A panicked");
27 thread_b.join().expect("Thread B panicked");
28 }
它是如何工作的...
Rust 中并行性的基本构建块是Arc,代表原子引用计数。功能上,它的工作方式与我们在 Chapter 5,高级数据结构;与智能指针共享所有权中看到的Rc相同。唯一的区别是引用计数是使用原子原语完成的,这些是具有良好定义的并行交互的原生数据类型的版本,如usize。这有两个后果:
-
Arc比Rc慢一点,因为引用计数涉及更多的工作 -
Arc可以在线程间安全地使用
Arc的构造函数看起来与Rc[7]相同:
let some_resource = Arc::new("Hello World".to_string());
这创建了一个覆盖String的Arc。String是一个struct,它不是天生就保存下来以便跨线程操作的。在 Rust 术语中,我们说String不是Sync(关于这一点,稍后在配方原子访问原语中会详细介绍)。
现在,让我们看看线程是如何初始化的。thread::spawn()接受一个闭包并在新线程中执行它。因为这是并行执行的,所以主线程不会等待线程完成;它在创建后立即继续工作。
以下创建了一个线程,该线程打印出some_resource的内容,并为我们提供了一个名为thread_a的线程句柄[10]:
let thread_a = {
let some_resource = some_resource.clone();
thread::spawn(move || {
println!("Thread A says: {}", some_resource);
})
};
之后(或同时),我们在第二个线程thread_b中做完全相同的事情。
为了理解为什么我们需要Arc而不能直接将资源传递给闭包,让我们更仔细地看看闭包是如何工作的。
Rust 中的闭包只能操作三种类型的变量:
-
传递给它们的参数
-
static变量(具有'static生命周期的变量;参见 Chapter 5,高级数据结构;创建懒加载静态对象) -
它拥有的变量,无论是通过创建还是通过将它们移动到闭包中
考虑到这一点,让我们看看一个没有经验的 Rust 程序员可能会采取的最简单的方法:
let thread_a = thread::spawn(|| {
println!("Thread A says: {}", some_resource);
});
如果我们尝试运行这个程序,编译器会告诉我们以下信息:

看起来它不喜欢我们使用 some_resource 的方式。再次看看闭包中变量使用的规则:
-
some_resource没有被作为参数传递 -
它不是
static -
它既没有被创建在闭包中,也没有被移动到闭包中
但“闭包可能比当前函数存活时间更长”是什么意思呢?嗯,因为闭包可以被存储在普通变量中,所以它们可以从函数中返回。想象一下,如果我们编写了一个函数,创建了一个名为 some_resource 的变量,在闭包中使用它,然后返回它。由于函数拥有 some_resource,在返回闭包时会丢弃它,使得对它的任何引用都变得无效。我们不希望有任何无效的变量,所以编译器阻止我们可能启用它们。相反,它建议使用 move 关键字将 some_resource 的所有权移动到闭包中。让我们试试:
let thread_a = thread::spawn(move || {
println!("Thread A says: {}", some_resource);
});
编译器给出了以下响应:

因为我们将 some_resource 移动到了 thread_a 中的闭包内部,thread_b 就不能再使用它了!解决方案是创建 some_resource 引用的一个副本,并且只将副本移动到闭包中:
let some_resource_clone = some_resource.clone();
let thread_a = thread::spawn(move || {
println!("Thread A says: {}", some_resource_clone);
});
现在运行得很好,但看起来有点奇怪,因为我们现在带着关于我们正在处理的资源实际上是一个 clone 的知识负担。这可以通过将副本放入一个新的作用域中并使用与原始变量相同的名称来解决,这样我们就得到了我们代码的最终版本:
let thread_a = {
let some_resource = some_resource.clone();
thread::spawn(move || {
println!("Thread A says: {}", some_resource);
})
};
看起来清晰多了,不是吗?这种将 Rc 和 Arc 变量传递给闭包的方式是 Rust 中众所周知的一种惯用法,从现在起我们将在这个章节的所有其他菜谱中使用它。
在这个菜谱的最后,我们将通过在它们上调用 .join() 来合并两个线程 [26 和 27]。合并一个线程意味着阻塞当前线程,直到合并的线程完成其工作。之所以这样称呼,是因为我们将程序的两个线程合并为一个单一的线程。在思考这个概念时,想象实际的缝纫线会有所帮助。
我们在程序结束之前合并它们,否则我们无法保证它们在我们程序退出之前真正运行到底。一般来说,当你需要线程的结果并且不能再等待它们时,或者它们即将被丢弃时,你应该 join 你的线程。
参见
- 第五章中的使用智能指针共享所有权和创建懒加载静态对象菜谱
在线程间发送数据
到目前为止,我们已查看独立工作的线程。现在,让我们看看需要共享数据的交织线程。在设置服务器时,这种情况很常见,因为接收客户端消息的线程通常与实际处理和响应客户端输入的线程不同。Rust 通过提供 通道 的概念作为解决方案。通道被分为 发送者 和 接收者,它们可以在线程之间共享数据。
如何做到这一点...
-
打开之前为您生成的
Cargo.toml文件。 -
在
[dependencies]下添加以下行:
rand = "0.4.2"
-
如果你想,你可以访问 rand 的 crates.io 页面 (
crates.io/crates/rand) 检查最新版本并使用它。 -
在
bin文件夹中,创建一个名为channels.rs的文件。 -
添加以下代码,并用
cargo run --bin channels运行它:
1 extern crate rand;
2
3 use rand::Rng;
4 use std::thread;
5 // mpsc stands for "Multi-producer, single-consumer"
6 use std::sync::mpsc::channel;
7
8 fn main() {
9 // channel() creates a connected pair of a sender and a
receiver.
10 // They are usually called tx and rx, which stand for
11 // "transmission" and "reception"
12 let (tx, rx) = channel();
13 for i in 0..10 {
14 // Because an mpsc channel is "Multi-producer",
15 // the sender can be cloned infinitely
16 let tx = tx.clone();
17 thread::spawn(move || {
18 println!("sending: {}", i);
19 // send() pushes arbitrary data to the connected
receiver
20 tx.send(i).expect("Disconnected from receiver");
21 });
22 }
23 for _ in 0..10 {
24 // recv() blocks the current thread
25 // until a message was received
26 let msg = rx.recv().expect("Disconnected from sender");
27 println!("received: {}", msg);
28 }
29
30 let (tx, rx) = channel();
31 const DISCONNECT: &str = "Goodbye!";
32 // The following thread will send random messages
33 // until a goodbye message was sent
34 thread::spawn(move || {
35 let mut rng = rand::thread_rng();
36 loop {
37 let msg = match rng.gen_range(0, 5) {
38 0 => "Hi",
39 1 => DISCONNECT,
40 2 => "Howdy there, cowboy",
41 3 => "How are you?",
42 4 => "I'm good, thanks",
43 _ => unreachable!(),
44 };
45 println!("sending: {}", msg);
46 tx.send(msg).expect("Disconnected from receiver");
47 if msg == DISCONNECT {
48 break;
49 }
50 }
51 });
52
53 // An iterator over messages in a receiver is infinite.
54 // It will block the current thread until a message is
available
55 for msg in rx {
56 println!("received: {}", msg);
57 }
58 }
它是如何工作的...
如代码注释中所述,调用 std::sync::mpsc::channel() 会生成一个包含 Sender 和 Receiver 的元组,它们通常被称为 tx 用于 传输 和 rx 用于 接收 [12]。
这种命名约定并非来自 Rust,但自至少 1960 年 RS-232(推荐标准 232)被引入以来,一直是电信行业的标准,详细说明了计算机和调制解调器应该如何相互通信。
同一通道的这两部分可以在它们所在的当前线程之外独立地相互通信。模块的名称 mspc 告诉我们这个通道是一个 多生产者,单消费者 通道,这意味着我们可以按需多次 克隆 我们的消息发送者。在处理闭包时,我们可以利用这一事实 [16 到 21]:
for i in 0..10 {
let tx = tx.clone();
thread::spawn(move || {
println!("sending: {}", i);
tx.send(i).expect("Disconnected from receiver");
});
}
我们不需要将我们的发送者包裹在 Arc 中,因为它原生支持任意克隆!在闭包内部,你可以看到发送者的主要功能。send() 方法将数据发送到接收者所在的线程。如果接收者不再可用,例如它被过早地丢弃,它将返回一个错误。在这个线程中,我们将简单地并发地向接收者发送数字 0 到 9。需要注意的是,由于通道的两部分是静态类型的,它们只能发送一种特定的数据类型。如果你首先发送一个 i32,你的通道将只能与 i32 一起工作。如果你发送一个 String,它将是一个 String 通道。
接下来是接收者 [23 到 28]:
for _ in 0..10 {
let msg = rx.recv().expect("Disconnected from sender");
println!("received: {}", msg);
}
recv() 方法代表 接收,它会阻塞当前线程,直到收到消息。与它的对应方法类似,如果发送者不可用,它会返回一个错误。因为我们知道我们只发送了 10 条消息,所以我们只调用它 10 次。我们没有必要显式地 join 我们为发送者创建的线程,因为 recv() 阻塞了主线程,直到没有更多消息为止,这意味着发送者已经发送了所有需要发送的消息,也就是说,所有线程已经完成了它们的工作。这样,我们实际上已经将它们连接在一起了。
但在现实生活中,你无法保证客户端会向你发送信息的次数。为了更现实的演示,我们现在将创建一个线程,该线程会向接收者发送随机消息 [37],直到它最终发送足够多的消息并退出,发送 "Goodbye!" [48]。注意我们如何创建了一个新的通道对,因为旧的通道被设置为 i32 类型,因为默认情况下整数字面量(如 1 或 2)被视为 i32。
虽然发送代码看起来几乎与之前相同,但接收端看起来略有不同 [55 到 57]:
for msg in rx {
println!("received: {}", msg);
}
如您所见,接收者可以被迭代。它表现得像一个无限迭代器,遍历所有将来的消息,等待新消息时阻塞,类似于在循环中调用 recv()。区别在于,当发送者不可用时,迭代会自动停止。因为我们当发送者发送 "Goodbye!" [48] 时终止发送线程,所以当接收它时,这个接收者的迭代也会停止,因为此时发送者已经被丢弃。因为这意味着我们有发送线程完成的确切保证,所以我们不需要将其连接。
还有更多...
通道不是 Sync,因此只能跨通道移动,但不能在它们之间共享。如果您需要通道是 Sync,可以使用 std::sync::mpsc::sync_channel,当未回答的消息缓冲区满时,它会阻塞。一个可能需要这种情况的例子是,当网络框架提供管理您的类型,但只与 Sync 结构一起工作时。您可以在 原子访问原语 的配方中了解更多关于 Sync 的信息。
如其名称所示,mpsc 通道允许多个发送者,但只有一个接收者。大多数时候,这已经足够好了,但如果您发现自己需要完全相反的情况,即一个发送者和多个接收者,请查看 Sean McArthur 的 spmc crate,网址为 crates.io/crates/spmc,它为您提供了 单生产者,多消费者 通道。
参见
- 在 第二章 的 访问集合作为迭代器 配方中,与集合一起工作。
使用 RwLocks 并行访问资源
当我们使用 Arc 共享资源时,我们只以不可变的方式这样做。当我们想要我们的线程修改资源时,我们需要使用某种锁定机制来确保并行主义的黄金法则:多个读者或一个写者。RwLock 在线程之间强制执行这一规则,并在它们违反规则时阻塞它们。
如何操作...
-
在
bin文件夹中创建一个名为rw_lock.rs的文件。 -
添加以下代码并使用
cargo run --bin rwlock运行它:
1 use std::sync::{Arc, RwLock};
2 use std::thread;
3
4 fn main() {
5 // An RwLock works like the RefCell, but blocks the current
6 // thread if the resource is unavailable
7 let resource = Arc::new(RwLock::new("Hello
World!".to_string()));
8
9 // The reader_a thread will print the current content of
10 // our resource fourty times
11 let reader_a = {
12 let resource = resource.clone();
13 thread::spawn(move || {
14 for _ in 0..40 {
15 // Lock resource for reading access
16 let resource = resource
17 .read()
18 .expect("Failed to lock resource for reading");
19 println!("Reader A says: {}", resource);
20 }
21 })
22 };
23
24 // The reader_b thread will print the current content of
25 // our resource fourty times as well. Because RwLock allows
26 // multiple readers, it will execute at the same time as
reader_a
27 let reader_b = {
28 let resource = resource.clone();
29 thread::spawn(move || {
30 for _ in 0..40 {
31 // Lock resource for reading access
32 let resource = resource
33 .read()
34 .expect("Failed to lock resource for reading");
35 println!("Reader B says: {}", resource);
36 }
37 })
38 };
39
40 // The writer thread will modify the resource ten times.
41 // Because RwLock enforces Rust's access rules
42 // (multiple readers xor one writer), this thread will wait
until
43 // thread_a and thread_b are not using the resource and then
block
44 // them both until its done.
45 let writer = {
46 let resource = resource.clone();
47 thread::spawn(move || {
48 for _ in 0..10 {
49 // Lock resource for writing access
50 let mut resource = resource
51 .write()
52 .expect("Failed to lock resource for writing");
53
54 resource.push('!');
55 }
56 })
57 };
58
59 reader_a.join().expect("Reader A panicked");
60 reader_b.join().expect("Reader B panicked");
61 writer.join().expect("Writer panicked");
62 }
它是如何工作的...
RwLock 是我们在 第五章,“高级数据结构”;“处理内部可变性”中使用的 RefCell 的并行等价物。两者之间的大不同是,虽然 RefCell 在违反 Rust 的所有权概念时会引发恐慌,但 RwLock 只是简单地阻塞当前线程,直到违反行为结束。
RefCell 的 borrow() 方法的对应物是 read() [17],它锁定资源以进行不可变访问。borrow_mut() 的对应物是 write() [51],它锁定资源以进行可变访问。这说得通,不是吗?
这些方法返回一个 Result,它告诉我们线程是否已被 毒化。对于每个锁,毒化的含义都不同。在 RwLock 中,这意味着锁定资源进行 write 访问的线程发生了恐慌。这样,你可以对其他线程中的恐慌做出反应并以某种方式处理它们。一个这样的例子是在崩溃发生之前向服务器发送一些日志以便诊断问题。然而,在大多数情况下,如果你简单地 panic,通常就足够了,因为 panic 通常代表无法修复的严重故障。
在我们的示例中,我们通过设置两个请求 read 访问权限的线程来展示这个概念:reader_a [11] 和 reader_b [27]。由于 RwLock 允许多个读者,它们将并发地打印出我们资源的值 [19 和 35]。与此同时,writer [45] 尝试锁定资源以进行 write 访问。它必须等待直到 reader_a 和 reader_b 都不再使用该资源。按照同样的规则,当 writer 轮到它们并修改资源 [54] 时,reader_a 和 reader_b 必须等待它完成。
因为所有这些操作几乎同时发生,所以每次运行这个示例都会给出略微不同的结果。我鼓励你多次运行程序并比较输出。
还有更多...
尽管RwLock具有良好的可用性,但它仍然不是所有并发问题的万能药。在并发编程中有一个概念叫做死锁。当两个进程等待解锁其他进程持有的资源时,就会发生死锁。这将导致它们永远等待,因为没有人为第一步做好准备。有点像热恋中的青少年。一个例子是writer_a请求访问writer_b持有的文件。与此同时,writer_b需要从writer_a那里获取一些用户信息,然后他才能放弃文件锁。避免这种问题的最好方法是把它放在心里,当你即将创建相互依赖的进程时,要记住它。
另一种在其他语言中相当流行的锁是Mutex,Rust 也在std::sync::Mutex下提供了它。当它锁定资源时,它将每个进程都视为一个写者,所以即使它们没有修改数据,也没有两个线程能够同时使用Mutex。我们将在下一个菜谱中创建一个非常简单的实现。
参见
- 在第五章的高级数据结构中的与内部可变性一起工作菜谱
原子访问原始数据
当你阅读关于所有这些并行结构时,你可能想知道它们是如何实现的。在这个菜谱中,我们将揭开盖子,了解最基本的并行数据类型,这些类型被称为原子。我们将通过实现我们自己的Mutex来完成这项工作。
如何做到这一点...
-
在
bin文件夹中创建一个名为atomic.rs的文件。 -
添加以下代码,并用
cargo run --bin atomic运行它:
1 use std::sync::Arc;
2 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering, ATOMIC_BOOL_INIT, ATOMIC_USIZE_INIT};
3 use std::thread;
4 use std::ops::{Deref, DerefMut};
5 use std::cell::UnsafeCell;
6
7 fn main() {
8 // Atomics are primitive types suited for
9 // well defined concurrent behaviour
10 let some_number = AtomicUsize::new(0);
11 // They are usually initialized by copying them from
12 // their global constants, so the following line does the same:
13 let some_number = ATOMIC_USIZE_INIT;
14
15 // load() gets the current value of the atomic
16 // Ordering tells the compiler how exactly to handle the
interactions
17 // with other threads. SeqCst ("Sequentially Consistent") can
always be used
18 // as it results in the same thing as if no parallelism was
involved
19 let curr_val = some_number.load(Ordering::SeqCst);
20 println!("The current value of some_number is {}", curr_val);
21
22 // store() sets the variable
23 some_number.store(123, Ordering::SeqCst);
24 let curr_val = some_number.load(Ordering::SeqCst);
25 println!("The current value of some_number is {}", curr_val);
26
27 // swap() sets the variable and returns the old value
28 let old_val = some_number.swap(12_345, Ordering::SeqCst);
29 let curr_val = some_number.load(Ordering::SeqCst);
30 println!("The old value of some_number was {}", old_val);
31 println!("The current value of some_number is {}", curr_val);
32
33 // compare_and_swap only swaps the variable if it
34 // is currently equal to the first argument.
35 // It will always return the old variable
36 let comparison = 12_345;
37 let new_val = 6_789;
38 let old_val = some_number.compare_and_swap(comparison, new_val,
Ordering::SeqCst);
39 if old_val == comparison {
40 println!("The value has been updated");
41 }
42
43 // The previous atomic code is equivalent to
44 // the following sequential code
45 let mut some_normal_number = 12_345;
46 let old_val = some_normal_number;
47 if old_val == comparison {
48 some_normal_number = new_val;
49 println!("The value has been updated sequentially");
50 }
51
52 // fetch_add() and fetch_sub() add/subtract a number from the
value,
53 // returning the old value
54 let old_val_one = some_number.fetch_add(12, Ordering::SeqCst);
55 let old_val_two = some_number.fetch_sub(24, Ordering::SeqCst);
56 let curr_val = some_number.load(Ordering::SeqCst);
57 println!(
58 "some_number was first {}, then {} and is now {}",
59 old_val_one, old_val_two, curr_val
60 );
61
62 // fetch_or() performs an "or" ("||") operation on the variable
and
63 // an argument and sets the variable to the result. It then
returns the old value.
64 // For the other logical operations, fetch_and(), fetch_nand()
and fetch_xor also exist
65 let some_bool = ATOMIC_BOOL_INIT;
66 let old_val = some_bool.fetch_or(true, Ordering::SeqCst);
67 let curr_val = some_bool.load(Ordering::SeqCst);
68 println!("({} || true) is {}", old_val, curr_val);
69
70 // The following is a demonstration of our own Mutex
implementation,
71 // based on an AtomicBool that checks if it's locked or not
72 let naive_mutex = Arc::new(NaiveMutex::new(1));
73
74 // The updater thread will set the value in the mutex to 2
75 let updater = {
76 let naive_mutex = naive_mutex.clone();
77 thread::spawn(move || {
78 let mut val = naive_mutex.lock();
79 *val = 2;
80 })
81 };
82
83 // The updater thread will print the value in the mutex
84 let printer = {
85 let naive_mutex = naive_mutex.clone();
86 thread::spawn(move || {
87 let val = naive_mutex.lock();
88 println!("The value in the naive mutex is: {}", *val);
89 })
90 };
91
92 // The exact order of execution is unpredictable,
93 // but our mutex guarantees that the two threads will
94 // never access the data at the same time
95 updater.join().expect("The updater thread panicked");
96 printer.join().expect("The printer thread panicked");
97 }
- 现在是我们自己制作的互斥锁的实现:
99 // NaiveMutex is an easy, albeit very suboptimal,
100 // implementation of a Mutex, similar to std::sync::Mutex
101 // A mutex is a lock that only allows one thread to access a
ressource at all times
102 pub struct NaiveMutex<T> {
103 locked: AtomicBool,
104 // UnsafeCell is the underlying struct of every
105 // internally mutable container such as ours
106 data: UnsafeCell<T>,
107 }
108
109 // This is a RAII guard, identical to the one from the last
chapter
110 pub struct NaiveMutexGuard<'a, T: 'a> {
111 naive_mutex: &'a NaiveMutex<T>,
112 }
113
114 impl<T> NaiveMutex<T> {
115 pub fn new(data: T) -> Self {
116 NaiveMutex {
117 locked: ATOMIC_BOOL_INIT,
118 data: UnsafeCell::new(data),
119 }
120 }
121
122 pub fn lock(&self) -> NaiveMutexGuard<T> {
123 // The following algorithm is called a "spinlock", because it
keeps
124 // the current thread blocked by doing nothing (it keeps it
"spinning")
125 while self.locked.compare_and_swap(false, true,
Ordering::SeqCst) {}
126 NaiveMutexGuard { naive_mutex: self }
127 }
128 }
129
130 // Every type that is safe to send between threads is automatically
131 // safe to share between threads if wrapped in our mutex, as it
132 // guarantees that no threads will access it ressource at the
same time
133 unsafe impl<T: Send> Sync for NaiveMutex<T> {}
134
135 // Automatically unlock the mutex on drop
136 impl<'a, T> Drop for NaiveMutexGuard<'a, T> {
137 fn drop(&mut self) {
138 self.naive_mutex.locked.store(false, Ordering::SeqCst);
139 }
140 }
141
142 // Automatically dereference to the underlying data
143 impl<'a, T> Deref for NaiveMutexGuard<'a, T> {
144 type Target = T;
145 fn deref(&self) -> &T {
146 unsafe { &*self.naive_mutex.data.get() }
147 }
148 }
149
150 impl<'a, T> DerefMut for NaiveMutexGuard<'a, T> {
151 fn deref_mut(&mut self) -> &mut T {
152 unsafe { &mut *self.naive_mutex.data.get() }
153 }
154 }
它是如何工作的...
到写作的时候,标准库中的std::sync::atomic模块下有四种atomic类型:AtomicBool、AtomicIsize、AtomicUsize和AtomicPtr。它们每一个都代表一个原始类型,即bool、isize、usize和*mut。我们不会查看最后一个,因为作为一个指针,你只有在与其他语言编写的程序进行接口时才可能需要处理它。
如果你之前没有遇到过isize和usize,它们是你机器内存任何部分所需的最小字节数的表示。在 32 位目标上这是 4 字节,而在 64 位系统上则需要 8 字节。isize使用这些字节来表示一个有符号数字,就像一个可以负的整数。而usize则表示一个无符号数字,它只能为正,但在那个方向上有更多的容量来处理巨大的数字。它们通常用于处理集合容量。例如,Vec在调用其.len()方法时返回一个usize。此外,在夜间工具链中,所有其他具体整数类型(如u8或i32)都有原子变体。
我们原语的atomic版本与它们的亲戚工作方式相同,但有一个重要的区别:它们在并行环境中使用时具有明确的行为。所有这些方法都接受一个atomic::Ordering类型的参数,代表要使用哪种低级并发策略。在这个例子中,我们只将使用Ordering::SeqCst,它代表`sequentially consistent(顺序一致)。这反过来意味着行为相当直观。如果某些数据使用这种排序存储或修改,另一个线程在写入后可以看到其内容,就像两个线程一个接一个地运行一样。或者换句话说,行为与一系列事件的顺序行为是一致的。这种策略总是与所有并行算法一起工作。所有其他排序只是放松涉及数据的相关约束,以获得某种性能上的好处。
拥有这些知识,你应该能够理解main函数中大多数操作,直到NaiveMutex[72]的使用。注意一些atomic方法只是以不同的方式做与我们的正常原语相同的事情,增加了指定排序的附加功能,并且大多数方法返回旧值。例如,some_number.fetch_add(12, Ordering::SeqCst)除了返回some_number的旧值外,本质上只是some_number += 12。
原子的实际用例出现在示例代码的第二部分,其中我们实现了自己的Mutex。互斥锁(mutex)在所有现代编程语言中都非常突出,是一种不允许任何两个线程同时访问资源的锁。在阅读了最后一个食谱之后,你知道你可以将Mutex想象成一种RwLock,它总是以write模式锁定一切。
让我们在代码中跳过几行,来到[102]:
pub struct NaiveMutex<T> {
locked: AtomicBool,
data: UnsafeCell<T>,
}
如你所见,我们将基于一个简单的原子标志locked来构建我们的NaiveMutex,它将跟踪互斥锁是否可用。另一个成员data持有我们想要锁定的底层资源。它的类型UnsafeCell是每个实现某种内部可变性的结构的底层类型(参见第五章,高级数据结构;处理内部可变性)。
下一个结构体如果你阅读了第六章,处理错误;理解 RAII,你会觉得它很熟悉;因为它是一个带有对其父级[110]引用的 RAII 保护器:
pub struct NaiveMutexGuard<'a, T: 'a> {
naive_mutex: &'a NaiveMutex<T>,
}
让我们看看我们如何锁定一个线程:
pub fn lock(&self) -> NaiveMutexGuard<T> {
while self.locked.compare_and_swap(false, true, Ordering::SeqCst) {}
NaiveMutexGuard { naive_mutex: self }
}
初看可能有点奇怪,不是吗?compare_and_swap是较为复杂的atomic操作之一。它的工作原理如下:
-
它比较原子的值与第一个参数
-
如果它们相同,它将第二个参数存储在原子中
-
最后,它返回函数调用之前的原子值
让我们将这个应用到我们的调用中:
-
compare_and_swap检查self.locked是否包含false -
如果是这样,它将
self.locked设置为true -
在任何情况下,它都会返回旧值
如果返回的值是true,这意味着我们的互斥锁当前是锁定状态。那么我们的线程应该做什么呢?绝对什么也不做:{ }。因为我们是在while循环中调用这个的,所以我们会继续什么也不做(这被称为自旋),直到情况发生变化。这个算法被称为自旋锁。
当我们的互斥锁最终可用时,我们将它的locked标志设置为true,并返回一个 RAII 守护者,其中包含对NaiveMutex的引用。
这不是真正的std::sync::Mutex的实现方式。因为独占锁定资源是一个非常基本的并发任务,操作系统原生支持它。Rust 标准库中实现的Mutex仍然是通过 RAII 模式构建的,但使用的是操作系统的互斥锁句柄而不是我们的自定义逻辑。有趣的事实——Windows 实现使用 SRWLocks(msdn.microsoft.com/en-us/library/windows/desktop/aa904937(v=vs.85).aspx),这是 Windows 的本地版本的RwLock,因为它们证明比本地的Mutex更快。所以,至少在 Windows 上,这两种类型实际上非常相似。
NaiveMutexGuard的实现提供了在丢弃时的lock的对应物[138]:
fn drop(&mut self) {
self.naive_mutex.locked.store(false, Ordering::SeqCst);
}
我们简单地将值false存储在self.locked中,每当我们的守护者超出作用域时(参见第六章,处理错误;*实现 Drop 特性**)。接下来的两个特性NaiveMutexGuard实现是Deref和DerefMut,这使得我们可以在NaiveMutexGuard<T>上直接调用类型T的方法。它们几乎有相同的实现[146]:
unsafe { &*self.naive_mutex.data.get() }
记得我们说过你会在罕见的情况下处理指针吗?嗯,这就是其中之一。
UnsafeCell不保证任何借用安全性,因此得名和unsafe块。它依赖于你确保所有对其的调用实际上都是安全的。正因为如此,它给你一个原始的可变指针,你可以以任何你想要的方式操作它。我们在这里使用*对其进行解引用,所以*mut T变成了只有T。然后我们使用&返回对该对象的正常引用[146]。在deref_mut的实现中唯一不同的是,我们返回一个可变引用&mut [152]。我们所有的unsafe调用都保证遵循 Rust 的所有权原则,因为我们无论如何只允许一个作用域借用我们的资源。
我们Mutex实现所需的最后一件事是以下这一行,我们之前跳过了:
unsafe impl<T: Send> Sync for NaiveMutex<T> {}
Sync特性有一个相当小的实现,对吧?这是因为它是一个标记。它属于一组特性,它们实际上并不做任何事情,但只存在于告诉编译器有关实现它们的类型的信息。std::marker模块中的另一个特性是Send,我们在这里也使用它。
如果类型T实现了Send,它告诉世界可以通过将其作为值传递而不是引用来安全地在线程之间移动(发送)。几乎所有的 Rust 类型都实现了Send。
如果T是Sync,它告诉编译器可以通过传递&T引用来安全地在线程之间共享(它以同步的方式行为)。这比Send更难实现,但我们的NaiveMutex保证了其中的类型可以被共享,因为我们只允许一次访问其内部类型。这就是为什么我们为我们的互斥锁中的每个Send实现Sync特性的原因。如果可以传递它,那么在NaiveMutex内部共享它也是自动安全的。
回到main函数,你现在可以找到一些我们Mutex[75 和 84]的使用示例,类似于前一个配方中的示例。
更多...
因为SeqCst对于大多数应用来说已经足够好了,并且所有其他排序所涉及到的复杂性,所以我们不会查看任何其他的。不过,不要失望——Rust 使用与 C++几乎相同的atomic布局和功能,所以有很多资源可以告诉你这个问题实际上有多复杂。知名书籍《C++: Concurrency In Action》的作者 Anthony Williams(www.cplusplusconcurrencyinaction.com/)用整整 45 页(!)来简单地描述所有原子排序及其使用方法。另外 44 页用于展示所有这些排序的示例。普通程序能从这个级别的投入中受益吗?让我们看看这位先生的亲身说法,背景知识是std::memory_order_seq_cst是 C++对SeqCst的调用:
基本前提是:除非(a)你真的真的清楚你在做什么,并且可以证明在所有情况下放宽使用是安全的,以及(b)你的性能分析器显示你打算使用放宽排序的数据结构和操作是瓶颈,否则不要使用除std::memory_order_seq_cst(默认)之外的其他任何东西。
来源:stackoverflow.com/a/9564877/5903309
简而言之,你应该等到有很好的理由使用它们时再学习不同类型的排序。顺便说一句,这也是 Java 的方法,它使所有标记为volatile的变量以顺序一致的方式行为。
参见
-
处理内部可变性配方在第五章,高级数据结构
-
实现 Drop 特性和理解 RAII配方在第六章,处理错误
在连接处理器中将所有内容组合在一起
现在我们已经单独审视了许多不同的实践。然而,这些构建块的真实力量来自于它们的组合。这个食谱将向您展示如何将其中的一些组合起来,作为服务器连接处理部分的现实起点。
如何做到...
-
在
bin文件夹中创建一个名为connection_handler.rs的文件。 -
添加以下代码并使用
cargo run --bin connection_handler运行它:
1 use std::sync::{Arc, RwLock};
2 use std::net::Ipv6Addr;
3 use std::collections::HashMap;
4 use std::{thread, time};
5 use std::sync::atomic::{AtomicUsize, Ordering, ATOMIC_USIZE_INIT};
6
7 // Client holds whatever state your client might have
8 struct Client {
9 ip: Ipv6Addr,
10 }
11
12 // ConnectionHandler manages a list of connections
13 // in a parallelly safe way
14 struct ConnectionHandler {
15 // The clients are identified by a unique key
16 clients: RwLock<HashMap<usize, Client>>,
17 next_id: AtomicUsize,
18 }
19
20 impl Client {
21 fn new(ip: Ipv6Addr) -> Self {
22 Client { ip }
23 }
24 }
25
26 impl ConnectionHandler {
27 fn new() -> Self {
28 ConnectionHandler {
29 clients: RwLock::new(HashMap::new()),
30 next_id: ATOMIC_USIZE_INIT,
31 }
32 }
33
34 fn client_count(&self) -> usize {
35 self.clients
36 .read()
37 .expect("Failed to lock clients for reading")
38 .len()
39 }
40
41 fn add_connection(&self, ip: Ipv6Addr) -> usize {
42 let last = self.next_id.fetch_add(1, Ordering::SeqCst);
43 self.clients
44 .write()
45 .expect("Failed to lock clients for writing")
46 .insert(last, Client::new(ip));
47 last
48 }
49
50 fn remove_connection(&self, id: usize) -> Option<()> {
51 self.clients
52 .write()
53 .expect("Failed to lock clients for writing")
54 .remove(&id)
55 .and(Some(()))
56 }
57 }
通过模拟连接和断开客户端来使用我们的连接处理器:
59 fn main() {
60 let connections = Arc::new(ConnectionHandler::new());
61
62 // the connector thread will add a new connection every now and
then
63 let connector = {
64 let connections = connections.clone();
65 let dummy_ip = Ipv6Addr::new(0, 0, 0, 0, 0, 0xffff, 0xc00a,
0x2ff);
66 let ten_millis = time::Duration::from_millis(10);
67 thread::spawn(move || {
68 for _ in 0..20 {
69 connections.add_connection(dummy_ip);
70 thread::sleep(ten_millis);
71 }
72 })
73 };
74
75 // the disconnector thread will remove the third connection at
some point
76 let disconnector = {
77 let connections = connections.clone();
78 let fifty_millis = time::Duration::from_millis(50);
79 thread::spawn(move || {
80 thread::sleep(fifty_millis);
81 connections.remove_connection(2);
82 })
83 };
84
85 // The main thread will print the active connections in a short
interval
86 let five_millis = time::Duration::from_millis(5);
87 for _ in 0..40 {
88 let count = connections.client_count();
89 println!("Active connections: {}", count);
90 thread::sleep(five_millis);
91 }
92
93 connector.join().expect("The connector thread panicked");
94 disconnector
95 .join()
96 .expect("The disconnector thread panicked");
97 }
它是如何工作的...
这个食谱不引入任何新的模块或概念。它在这里是为了提供一个大致的想法,说明如何在某种现实环境中结合你在本食谱中学到的所有东西。具体来说,我们的上下文由管理以某种方式连接到我们的客户端的代码组成。
Client [8] 包含与连接相关的所有信息。作为一个基本示例,它目前包含客户端的 IP 地址。其他可能性包括客户端的用户名、位置、设备、ping 等。
ConnectionHandler [14] 本身持有一个列表,更具体地说是一个HashMap,其中包含活动连接,按唯一 ID 索引。类似地,它还存储下一个连接的 ID。
我们使用唯一的 ID 而不是Vec<Client>,因为客户端可能能够多次连接到我们在同一设备上提供的任何服务。最容易的例子是在浏览器中打开多个标签页,所有标签页都访问同一个网站。一般来说,始终在唯一键后面保存你的数据是一个好的做法,以避免将来遇到麻烦。
结构体的实现应该是直接的。需要修改clients成员的方法使用.write()锁定,其他所有方法使用.read()。
在add_connection中用于获取新 ID 的代码会将next_id加一,并返回其最后一个值,这是对atomic[42]的常规操作:
let last = self.next_id.fetch_add(1, Ordering::SeqCst);
在将连接添加到clients之后,我们将新获得的 ID 返回给调用者,以便他们可以按自己的方式存储 ID,并在需要使用remove_connection [50] 将客户端踢出时重新使用它。remove_connection会返回一个Option,告诉调用者被移除的 ID 最初是否在客户端列表中。我们不直接返回被移除的Client,因为这会向ConnectionHandler的用户透露不必要的实现细节。
main中的代码模拟了对假设服务的并行访问。一些客户端连接到我们的ConnectionHandler,然后又离开了。thread::sleep [70, 80 和 90]会阻塞当前线程一段时间,在这里用于模拟各种事件以不规则间隔发生的效果,这些效果由每个任务的不同等待时间表示。
就像RwLock示例一样,每次运行此程序时,输出都会非常不同,所以请多次尝试。
还有更多...
如果你需要在不同线程中响应用户的消息,你可以使用channel,这是我们之前在章节中提到的。这个用例的一个例子就是编程一个在线视频游戏。你将希望聚合所有来自玩家的输入,通过模拟你的世界来响应它,然后向玩家广播局部变化,这些任务将在单个线程中并发执行。
第八章:与未来一起工作
在本章中,我们将介绍以下食谱:
-
提供带有 CPU 池的未来并等待它们
-
为未来实现错误处理
-
组合未来
-
使用流
-
使用汇(Sinks)
-
使用单次通道
-
返回未来
-
使用 BiLocks 锁定资源
简介
未来提供了异步计算的零成本抽象构建块。异步通信对于处理超时、跨线程池计算、网络响应以及任何不立即返回值的函数非常有用。
在同步块中,计算机会在等待每个命令返回一个值后,依次执行每个命令。如果您在发送电子邮件时应用同步模型,您将发送消息,盯着您的收件箱,直到收到收件人的回复。
幸运的是,生活并不总是同步的。在我们发送电子邮件后,我们可以切换到另一个应用程序或离开椅子。我们可以开始执行其他任务,如购买杂货、做晚饭或读书。我们的注意力可以同时集中在执行其他任务上。定期地,我们会检查收件箱以查看收件人的回复。定期检查新消息的过程说明了异步模型。与人类不同,计算机可以在我们的收件箱中检查新消息,并同时执行其他任务。
Rust 的未来通过实现轮询模型来工作,该模型利用一个中心组件(例如,软件、硬件设备和网络主机)来处理来自其他组件的状态报告。中心或主组件会重复地向其他组件发送信号,直到主组件收到更新、中断信号或轮询事件超时。
要更好地理解 Rust 模型中并发的工作原理,您可以查看 Alex Crichton 的并发演示文稿,链接为 github.com/alexcrichton/talks。在我们的食谱中,我们将在主线程中使用 futures::executor::block_on 函数来返回值。这是为了演示目的而故意这样做的。在实际应用中,您将在另一个单独的线程中使用 block_on,并且您的函数将返回某种 futures::Future 实现例如 futures::future::FutureResult。
在撰写本文时,未来在其代码库中进行了大量的开发性更改。您可以在他们的官方仓库 github.com/rust-lang-nursery/futures-rfcs 上查看未来的 RFC(请求评论)。
提供带有 CPU 池的未来并等待它们
未来通常被分配给一个Task,然后被分配给一个Executor。当一个任务唤醒时,执行器将任务放入队列,并将在任务上调用poll()直到进程完成。未来为我们提供了执行任务的一些方便的方法:
-
使用
futures::executor::block_on()手动生成未来任务。 -
使用
futures::executor::LocalPool,这对于在单个线程上执行许多小任务非常有用。在我们的未来返回中,我们不需要实现Send,因为我们只在一个线程上涉及任务。然而,如果你省略了Send特性,你需要在Executor上使用futures::executor::spawn_local()。 -
使用
futures::executor::ThreadPool,它允许我们将任务卸载到其他线程。
如何做到这一点...
-
使用
cargo new futures创建一个 Rust 项目,在本章中工作。 -
导航到新创建的
futures文件夹。在本章的其余部分,我们将假设你的命令行在这个目录内。 -
在
src文件夹内,创建一个名为bin的新文件夹。 -
删除生成的
lib.rs文件,因为我们没有创建库。 -
打开生成的
Cargo.toml文件。 -
在
[dependencies]下添加以下行:
futures = "0.2.0-beta"
futures-util = "0.2.0-beta"
-
在
src/bin文件夹中,创建一个名为pool.rs的文件。 -
添加以下代码,并使用
cargo run —bin pool运行它:
1 extern crate futures;
2
3 use futures::prelude::*;
4 use futures::task::Context;
5 use futures::channel::oneshot;
6 use futures::future::{FutureResult, lazy, ok};
7 use futures::executor::{block_on, Executor, LocalPool,
ThreadPoolBuilder};
8
9 use std::cell::Cell;
10 use std::rc::Rc;
11 use std::sync::mpsc;
12 use std::thread;
13 use std::time::Duration;
让我们添加我们的常量、枚举、结构和特性实现:
15 #[derive(Clone, Copy, Debug)]
16 enum Status {
17 Loading,
18 FetchingData,
19 Loaded,
20 }
21
22 #[derive(Clone, Copy, Debug)]
23 struct Container {
24 name: &'static str,
25 status: Status,
26 ticks: u64,
27 }
28
29 impl Container {
30 fn new(name: &'static str) -> Self {
31 Container {
32 name: name,
33 status: Status::Loading,
34 ticks: 3,
35 }
36 }
37
38 // simulate ourselves retreiving a score from a remote
database
39 fn pull_score(&mut self) -> FutureResult<u32, Never> {
40 self.status = Status::Loaded;
41 thread::sleep(Duration::from_secs(self.ticks));
42 ok(100)
43 }
44 }
45
46 impl Future for Container {
47 type Item = ();
48 type Error = Never;
49
50 fn poll(&mut self, _cx: &mut Context) -> Poll<Self::Item,
Self::Error> {
51 Ok(Async::Ready(()))
52 }
53 }
55 const FINISHED: Result<(), Never> = Ok(());
56
57 fn new_status(unit: &'static str, status: Status) {
58 println!("{}: new status: {:?}", unit, status);
59 }
让我们添加我们的第一个本地线程函数:
61 fn local_until() {
62 let mut container = Container::new("acme");
63
64 // setup our green thread pool
65 let mut pool = LocalPool::new();
66 let mut exec = pool.executor();
67
68 // lazy will only execute the closure once the future has
been polled
69 // we will simulate the poll by returning using the
future::ok method
70
71 // typically, we perform some heavy computational process
within this closure
72 // such as loading graphic assets, sound, other parts of our
framework/library/etc.
73 let f = lazy(move |_| -> FutureResult<Container, Never> {
74 container.status = Status::FetchingData;
75 ok(container)
76 });
77
78 println!("container's current status: {:?}",
container.status);
79
80 container = pool.run_until(f, &mut exec).unwrap();
81 new_status("local_until", container.status);
82
83 // just to demonstrate a simulation of "fetching data over a
network"
84 println!("Fetching our container's score...");
85 let score = block_on(container.pull_score()).unwrap();
86 println!("Our container's score is: {:?}", score);
87
88 // see if our status has changed since we fetched our score
89 new_status("local_until", container.status);
90 }
现在是我们的本地生成的线程示例:
92 fn local_spawns_completed() {
93 let (tx, rx) = oneshot::channel();
94 let mut container = Container::new("acme");
95
96 let mut pool = LocalPool::new();
97 let mut exec = pool.executor();
98
99 // change our container's status and then send it to our
oneshot channel
100 exec.spawn_local(lazy(move |_| {
101 container.status = Status::Loaded;
102 tx.send(container).unwrap();
103 FINISHED
104 }))
105 .unwrap();
106
107 container = pool.run_until(rx, &mut exec).unwrap();
108 new_status("local_spanws_completed", container.status);
109 }
110
111 fn local_nested() {
112 let mut container = Container::new("acme");
114 // we will need Rc (reference counts) since
we are referencing multiple owners
115 // and we are not using Arc (atomic reference counts)
since we are only using
116 // a local pool which is on the same thread technically
117 let cnt = Rc::new(Cell::new(container));
118 let cnt_2 = cnt.clone();
119
120 let mut pool = LocalPool::new();
121 let mut exec = pool.executor();
122 let mut exec_2 = pool.executor();
123
124 let _ = exec.spawn_local(lazy(move |_| {
125 exec_2.spawn_local(lazy(move |_| {
126 let mut container = cnt_2.get();
127 container.status = Status::Loaded;
128
129 cnt_2.set(container);
130 FINISHED
131 }))
132 .unwrap();
133 FINISHED
134 }));
135
136 let _ = pool.run(&mut exec);
137
138 container = cnt.get();
139 new_status("local_nested", container.status);
140 }
现在是我们的线程池示例:
142 fn thread_pool() {
143 let (tx, rx) = mpsc::sync_channel(2);
144 let tx_2 = tx.clone();
145
146 // there are various thread builder options which are
referenced at
147 // https://docs.rs/futures/0.2.0-
beta/futures/executor/struct.ThreadPoolBuilder.html
148 let mut cpu_pool = ThreadPoolBuilder::new()
149 .pool_size(2) // default is the number of cpus
150 .create();
151
152 // We need to box this part since we need the Send +'static trait
153 // in order to safely send information across threads
154 let _ = cpu_pool.spawn(Box::new(lazy(move |_| {
155 tx.send(1).unwrap();
156 FINISHED
157 })));
158
159 let f = lazy(move |_| {
160 tx_2.send(1).unwrap();
161 FINISHED
162 });
163
164 let _ = cpu_pool.run(f);
165
166 let cnt = rx.into_iter().count();
167 println!("Count should be 2: {:?}", cnt);
168 }
最后,我们的main函数:
170 fn main() {
171 println!("local_until():");
172 local_until();
173
174 println!("\nlocal_spawns_completed():");
175 local_spawns_completed();
176
177 println!("\nlocal_nested():");
178 local_nested();
179
180 println!("\nthread_pool():");
181 thread_pool();
182 }
它是如何工作的...
让我们先介绍一下Future特性:
- 实现
Future特性只需要三个约束:一个Item类型,一个Error类型,以及一个poll()函数。实际的特性看起来如下:
pub trait Future {
type Item;
type Error;
fn poll(
&mut self,
cx: &mut Context
) -> Result<Async<Self::Item>, Self::Error>;
}
-
Poll<Self::Item, Self::Error>是一个类型,它转换为Result<Async<T>, E>,其中T = Item和E = Error。这就是我们在第 50 行使用的示例。 -
当使用位于
futures::executor的执行器执行futures::task::Waker(也可以称为Task)时,或者通过构建futures::task::Context并使用未来包装器(如futures::future::poll_fn)手动唤醒时,会调用poll()。
现在,让我们转到我们的local_until()函数:
-
LocalPool为我们提供了使用单个线程并发运行任务的能力。这对于具有最小复杂性的函数非常有用,例如传统的 I/O 绑定函数。LocalPools可以有多个LocalExecutors(正如我们在第 65 行创建的那样),它们可以生成我们的任务。由于我们的任务是单线程的,我们不需要Box或添加Send特性到我们的未来。 -
futures::future::lazy函数将创建一个新的未来,从一个FnOnce闭包,这个闭包返回的将是与闭包返回的相同的未来(任何futures::future::IntoFuture特性),在我们的情况下,这个未来是FutureResult<Container, Never>。 -
从
LocalPool执行run_until(F: Future)函数将执行所有 future 任务,直到Future(表示为F)被标记为完成。该函数在完成时会返回Result<<F as Future>::Item, <F as Future>::Error>。在示例中,我们在第 75 行返回futures::future::ok(Container),所以我们的F::Item将是我们的Container。
对于我们的local_spawns_completed()函数:
-
首先,我们设置了我们的
futures::channel::oneshot通道(稍后将在使用 oneshot 通道部分进行解释)。 -
我们将使用
oneshot通道的futures::channel::oneshot::Receiver作为在run_until()函数中运行直到完成的 future。这允许我们演示在从另一个线程或任务接收到信号之前(在我们的示例中,这发生在第 102 行的tx.send(...)命令)轮询将如何工作。 -
LocalExecutor的spawn_local()是一个特殊的spawn函数,它赋予我们执行 future 函数而不需要实现Send特质的权限。
接下来,我们的local_nested()函数:
-
我们设置了我们的常规
Container,然后声明了一个引用计数器,这将允许我们在多个 executors 或线程之间保持一个值(这将是我们Container)。由于我们使用spawn_local(),它在一个绿色线程(由虚拟机或运行时库调度的线程)上执行 future,所以我们不需要使用原子引用计数器。 -
LocalPool的run(exec: &mut Executor)函数将运行池中生成的任何 future,直到所有 future 都完成。这也包括可能在其他任务中spawn额外任务的任何 executors,如我们的示例所示。
关于我们的thread_pool()函数:
-
创建了一个
std::sync::mspc::sync_channel,目的是为了演示阻塞线程。 -
接下来,我们使用默认设置创建了一个
ThreadPool,并调用了它的spawn(F: Box<Future<Item = (), Error = Never> + 'static + Send>)函数,无论何时我们决定执行池,它都会轮询任务直到完成。 -
在设置好我们的任务后,我们执行
ThreadPool的run(F: Future)函数,这将阻塞调用run()的线程,直到F: Future完成。即使池中还有其他任务被生成和运行,该函数在 future 完成时也会返回一个值。使用mspc::sync_channel之前有助于减轻这个问题,但会在被调用时阻塞线程。 -
使用
ThreadPoolBuilder,你可以:-
设置工作线程的数量
-
调整栈大小
-
为池设置一个前缀名称
-
在每个工作线程启动后,在运行任何任务之前运行一个函数(函数签名为
Fn(usize) + Send + Sync + 'static) -
在每个工作线程关闭之前执行一个函数(函数签名为
Fn(usize) + Send + Sync + 'static)
-
处理 future 中的错误
在实际应用中,我们不会从直接返回 Async::Ready<T> 或 FutureResult<T, E> 的异步函数中立即返回一个值。网络请求超时,缓冲区满,由于错误或中断服务变得不可用,以及许多其他问题每天都在出现。尽管我们喜欢从混乱中建立秩序,但由于自然发生的熵(程序员可能称之为 范围蔓延)和衰减(软件更新、新的计算机科学范式等),通常混乱获胜。幸运的是,futures 库为我们提供了一个简单的方法来实现错误处理。
如何做...
-
在
bin文件夹内,创建一个名为errors.rs的新文件。 -
添加以下代码并使用
cargo run --bin errors运行它:
1 extern crate futures;
2
3 use futures::prelude::*;
4 use futures::executor::block_on;
5 use futures::stream;
6 use futures::task::Context;
7 use futures::future::{FutureResult, err};
- 然后,让我们添加我们的结构和实现:
9 struct MyFuture {}
10 impl MyFuture {
11 fn new() -> Self {
12 MyFuture {}
13 }
14 }
15
16 fn map_error_example() -> FutureResult<(), &'static str> {
17 err::<(), &'static str>("map_error has occurred")
18 }
19
20 fn err_into_example() -> FutureResult<(), u8> {
21 err::<(), u8>(1)
22 }
23
24 fn or_else_example() -> FutureResult<(), &'static str> {
25 err::<(), &'static str>("or_else error has occurred")
26 }
27
28 impl Future for MyFuture {
29 type Item = ();
30 type Error = &'static str;
31
32 fn poll(&mut self, _cx: &mut Context) -> Poll<Self::Item, Self::Error> {
33 Err("A generic error goes here")
34 }
35 }
36
37 struct FuturePanic {}
38
39 impl Future for FuturePanic {
40 type Item = ();
41 type Error = ();
42
43 fn poll(&mut self, _cx: &mut Context) -> Poll<Self::Item,
Self::Error> {
44 panic!("It seems like there was a major issue with
catch_unwind_example")
45 }
46 }
- 然后,让我们添加我们的泛型错误处理函数/示例:
48 fn using_recover() {
49 let f = MyFuture::new();
50
51 let f_recover = f.recover::<Never, _>(|err| {
52 println!("An error has occurred: {}", err);
53 ()
54 });
55
56 block_on(f_recover).unwrap();
57 }
58
59 fn map_error() {
60 let map_fn = |err| format!("map_error_example: {}", err);
61
62 if let Err(e) = block_on(map_error_example().map_err(map_fn))
{
63 println!("block_on error: {}", e)
64 }
65 }
66
67 fn err_into() {
68 if let Err(e) = block_on(err_into_example().err_into::()) {
69 println!("block_on error code: {:?}", e)
70 }
71 }
72
73 fn or_else() {
74 if let Err(e) = block_on(or_else_example()
75 .or_else(|_| Err("changed or_else's error message"))) {
76 println!("block_on error: {}", e)
77 }
78 }
- 现在是我们的
panic函数:
80 fn catch_unwind() {
81 let f = FuturePanic {};
82
83 if let Err(e) = block_on(f.catch_unwind()) {
84 let err = e.downcast::<&'static str>().unwrap();
85 println!("block_on error: {:?}", err)
86 }
87 }
88
89 fn stream_panics() {
90 let stream_ok = stream::iter_ok::<_, bool>(vec![Some(1),
Some(7), None, Some(20)]);
91 // We panic on "None" values in order to simulate a stream
that panics
92 let stream_map = stream_ok.map(|o| o.unwrap());
93
94 // We can use catch_unwind() for catching panics
95 let stream = stream_map.catch_unwind().then(|r| Ok::<_, ()>
(r));
96 let stream_results: Vec<_> =
block_on(stream.collect()).unwrap();
97
98 // Here we can use the partition() function to separate the Ok
and Err values
99 let (oks, errs): (Vec<_>, Vec<_>) =
stream_results.into_iter().partition(Result::is_ok);
100 let ok_values: Vec<_> =
oks.into_iter().map(Result::unwrap).collect();
101 let err_values: Vec<_> =
errs.into_iter().map(Result::unwrap_err).collect();
102
103 println!("Panic's Ok values: {:?}", ok_values);
104 println!("Panic's Err values: {:?}", err_values);
105 }
- 最后,我们的
main函数:
107 fn main() {
108 println!("using_recover():");
109 using_recover();
110
111 println!("\nmap_error():");
112 map_error();
113
114 println!("\nerr_into():");
115 err_into();
116
117 println!("\nor_else():");
118 or_else();
119
120 println!("\ncatch_unwind():");
121 catch_unwind();
122
123 println!("\nstream_panics():");
124 stream_panics();
125 }
它是如何工作的...
让我们从 using_recover() 函数开始:
-
在 future 中发生的任何错误都将被转换成
<Self as Future>::Item。任何<Self as Future>::Error类型都可以传递,因为我们永远不会产生实际的错误。 -
futures::executor::block_on(F: Future)函数将在调用线程中运行一个 future,直到完成。任何在 futures 的default executor中的任务也将在这个调用线程上运行,但由于F可能会在任务完成之前完成,所以任务可能永远不会完成。如果这种情况发生,那么产生的任务将被丢弃。LocalPool常常被推荐用于缓解这个问题,但对我们来说block_on()将足够。
所有这些错误处理函数都可以在 futures::FutureExt 特性中找到。
现在,让我们看看我们的 map_error() 函数:
<Self as Future>::map_err<E, F>(F: FnOnce(Self::Error) -> E)函数将 future 的 (Self) 错误映射到另一个错误,同时返回一个新的 future。这个函数通常与组合器(如 select 或 join)一起使用,因为我们可以保证 futures 将具有相同的错误类型以完成组合。
接下来,是 err_into() 函数:
-
使用
std::convert::Into特性将Self::Error转换为另一种Error类型 -
与
futures::FutureExt::map_err类似,这个函数对于组合组合器很有用
or_else() 函数:
-
如果
<Self as Future>返回一个错误,futures::FutureExt::or_else将执行一个具有以下签名的闭包:FnOnce(Self::Error) -> futures::future::IntoFuture<Item = Self::Item> -
对于将失败的组合器链接在一起很有用
-
如果 future 成功完成、panic 或其 future 被丢弃,闭包将不会执行
然后是 catch_unwind() 函数:
-
这个函数通常不推荐作为处理错误的方式,并且仅通过 Rust 的
std选项启用(默认启用) -
Future 特性实现了
AssertUnwindSafe特性作为AssertUnwindSafe<F: Future>特性
最后,stream_panics() 函数:
-
在第 95 行,此
futures::StreamExt::catch_unwind函数类似于futures::FutureExt::catch_unwind -
如果发生 panic,它将是流中的最后一个元素
-
此功能仅在 Rust 的
std选项启用时才可用 -
AssertUnwindSafe特性也实现了流作为AssertUnwindSafe<S: Stream>
流的组合器位于 futures::StreamExt 特性中,它具有与 futures::FutureExt 相同的函数,还有一些额外的流特定组合器,如 split() 和 skip_while(),这些可能对您的项目很有用。
参见
- 第六章,处理错误
结合 futures
结合和链式调用我们的 futures 允许我们按顺序执行多个操作,并有助于更好地组织我们的代码。它们可以用来转换、拼接、过滤等 <Self as Future>::Items。
如何做到...
-
在
bin文件夹中,创建一个名为combinators.rs的新文件。 -
添加以下代码,并使用
cargo run --bin combinators运行它:
1 extern crate futures;
2 extern crate futures_util;
3
4 use futures::prelude::*;
5 use futures::channel::{mpsc, oneshot};
6 use futures::executor::block_on;
7 use futures::future::{ok, err, join_all, select_all, poll_fn};
8 use futures::stream::iter_result;
9 use futures_util::stream::select_all as select_all_stream;
10
11 use std::thread;
12
13 const FINISHED: Result<Async<()>, Never> = Ok(Async::Ready(()));
- 让我们添加我们的
join_all示例函数:
15 fn join_all_example() {
16 let future1 = Ok::<_, ()>(vec![1, 2, 3]);
17 let future2 = Ok(vec![10, 20, 30]);
18 let future3 = Ok(vec![100, 200, 300]);
19
20 let results = block_on(join_all(vec![future1, future2,
future3])).unwrap();
21 println!("Results of joining 3 futures: {:?}", results);
22
23 // For parameters with a lifetime
24 fn sum_vecs<'a>(vecs: Vec<&'a [i32]>) -> Box<Future, Error =
()> + 'static> {
25 Box::new(join_all(vecs.into_iter().map(|x| Ok::<i32, ()>
(x.iter().sum()))))
26 }
27
28 let sum_results = block_on(sum_vecs(vec![&[1, 3, 5], &[6, 7,
8], &[0]])).unwrap();
29 println!("sum_results: {:?}", sum_results);
30 }
31
接下来,我们将编写我们的 shared 函数:
32 fn shared() {
33 let thread_number = 2;
34 let (tx, rx) = oneshot::channel::();
35 let f = rx.shared();
36 let threads = (0..thread_number)
37 .map(|thread_index| {
38 let cloned_f = f.clone();
39 thread::spawn(move || {
40 let value = block_on(cloned_f).unwrap();
41 println!("Thread #{}: {:?}", thread_index, *value);
42 })
43 })
44 .collect::<Vec<_>>();
45 tx.send(42).unwrap();
46
47 let shared_return = block_on(f).unwrap();
48 println!("shared_return: {:?}", shared_return);
49
50 for f in threads {
51 f.join().unwrap();
52 }
53 }
现在我们来看一下我们的 select_all 示例:
55 fn select_all_example() {
56 let vec = vec![ok(3), err(24), ok(7), ok(9)];
57
58 let (value, _, vec) = block_on(select_all(vec)).unwrap();
59 println!("Value of vec: = {}", value);
60
61 let (value, _, vec) =
block_on(select_all(vec)).err().unwrap();
62 println!("Value of vec: = {}", value);
63
64 let (value, _, vec) = block_on(select_all(vec)).unwrap();
65 println!("Value of vec: = {}", value);
66
67 let (value, _, _) = block_on(select_all(vec)).unwrap();
68 println!("Value of vec: = {}", value);
69
70 let (tx_1, rx_1) = mpsc::unbounded::();
71 let (tx_2, rx_2) = mpsc::unbounded::();
72 let (tx_3, rx_3) = mpsc::unbounded::();
73
74 let streams = vec![rx_1, rx_2, rx_3];
75 let stream = select_all_stream(streams);
76
77 tx_1.unbounded_send(3).unwrap();
78 tx_2.unbounded_send(6).unwrap();
79 tx_3.unbounded_send(9).unwrap();
80
81 let (value, details) = block_on(stream.next()).unwrap();
82
83 println!("value for select_all on streams: {:?}", value);
84 println!("stream details: {:?}", details);
85 }
现在我们可以添加我们的 flatten、fuse 和 inspect 函数:
87 fn flatten() {
88 let f = ok::<_, _>(ok::<u32, Never>(100));
89 let f = f.flatten();
90 let results = block_on(f).unwrap();
91 println!("results: {}", results);
92 }
93
94 fn fuse() {
95 let mut f = ok::<u32, Never>(123).fuse();
96
97 block_on(poll_fn(move |mut cx| {
98 let first_result = f.poll(&mut cx);
99 let second_result = f.poll(&mut cx);
100 let third_result = f.poll(&mut cx);
101
102 println!("first result: {:?}", first_result);
103 println!("second result: {:?}", second_result);
104 println!("third result: {:?}", third_result);
105
106 FINISHED
107 }))
108 .unwrap();
109 }
110
111 fn inspect() {
112 let f = ok::<u32, Never>(111);
113 let f = f.inspect(|&val| println!("inspecting: {}", val));
114 let results = block_on(f).unwrap();
115 println!("results: {}", results);
116 }
然后我们可以添加我们的 chaining 示例:
118 fn chaining() {
119 let (tx, rx) = mpsc::channel(3);
120 let f = tx.send(1)
121 .and_then(|tx| tx.send(2))
122 .and_then(|tx| tx.send(3));
123
124 let t = thread::spawn(move || {
125 block_on(f.into_future()).unwrap();
126 });
127
128 t.join().unwrap();
129
130 let result: Vec<_> = block_on(rx.collect()).unwrap();
131 println!("Result from chaining and_then: {:?}", result);
132
133 // Chaining streams together
134 let stream1 = iter_result(vec![Ok(10), Err(false)]);
135 let stream2 = iter_result(vec![Err(true), Ok(20)]);
136
137 let stream = stream1.chain(stream2)
138 .then(|result| Ok::<_, ()>(result));
139
140 let result: Vec<_> = block_on(stream.collect()).unwrap();
141 println!("Result from chaining our streams together: {:?}",
result);
142 }
现在我们来看一下 main 函数:
144 fn main() {
145 println!("join_all_example():");
146 join_all_example();
147
148 println!("\nshared():");
149 shared();
150
151 println!("\nselect_all_example():");
152 select_all_example();
153
154 println!("\nflatten():");
155 flatten();
156
157 println!("\nfuse():");
158 fuse();
159
160 println!("\ninspect():");
161 inspect();
162
163 println!("\nchaining():");
164 chaining();
165 }
它是如何工作的...
join_all() 函数:
-
从几个 futures 收集结果并返回一个新的具有
futures::future::JoinAll<F: Future>特性的 future -
新的 future 将在
futures::future::join_all调用中执行所有聚合 future 的命令,以 FIFO 顺序返回一个Vec<T: Future::Item>向量 -
一个错误将立即返回并取消其他相关 future
以及 shared() 函数:
-
futures::FutureExt::shared将创建一个可以克隆的句柄,它解析为<T as futures::future::SharedItem>的返回值,它可以延迟到T。 -
适用于在多个线程上轮询 future
-
此方法仅在 Rust 的
std选项启用时才可用(默认情况下是启用的) -
基础结果是
futures::future::Shared<Future::Item>,它实现了Send和Sync特性 -
使用
futures::future::Shared::peek(&self)如果任何单个共享句柄已经完成,将返回一个值而不阻塞
接下来,select_all_example() 函数:
-
futures::FutureExt::select_all返回一个新的 future,它从一系列向量中选择 -
返回值是
futures::future::SelectAll,它允许我们遍历结果 -
一旦某个 future 完成其执行,此函数将返回 future 的项目、执行索引以及还需要处理的 future 列表
然后是 flatten() 函数:
-
futures::FutureExt::flatten将将未来组合在一起,其返回的结果是它们的项被展平 -
结果项必须实现
futures::future::IntoFuture特性
接下来是 fuse() 函数:
-
当轮询已经返回
futures::Async::Ready或Err值的未来时,存在undefined behavior的小概率,例如恐慌或永远阻塞。futures::FutureExt::fuse函数允许我们再次poll未来,而不用担心undefined behavior,并且总是返回futures::Async::Pending。 -
被融合的未来在完成时将被丢弃,以回收资源。
inspect() 函数:
futures::FutureExt::inspect允许我们窥视未来的一个项,这在我们在链式组合器时非常有用。
然后是 chaining() 函数:
-
我们首先创建一个包含三个值的通道,并使用
futures::FutureExt::and_then组合器spawn一个线程将这三个值发送到通道的接收者。我们在第 130 行从通道中收集结果。 -
然后,我们在第 134 行和第 135 行将两个流链在一起,在第 140 行发生收集。两个流的结果应该在第 137 行和第 138 行链在一起。
参见
- 使用向量 和 将集合作为迭代器访问 的配方在 第二章,处理集合
使用流
流是一个事件管道,它异步地向调用者返回一个值。Streams 对于需要 Iterator 特性的项更有用,而 Futures 对于 Result 值更合适。当流中发生错误时,错误不会停止流,并且对流的轮询仍然会返回其他结果,直到返回 None 值。
Streams 和 Channels 对于一些人来说可能有点令人困惑。Streams 用于连续、缓冲的数据,而 Channels 更适合端点之间的完成消息。
如何做到这一点...
-
在
bin文件夹中,创建一个名为streams.rs的新文件。 -
添加以下代码,并使用
cargo run --bin streams运行它:
1 extern crate futures;
2
3 use std::thread;
4
5 use futures::prelude::*;
6 use futures::executor::block_on;
7 use futures::future::poll_fn;
8 use futures::stream::{iter_ok, iter_result};
9 use futures::channel::mpsc;
- 现在,让我们添加我们的常量、实现等:
11 #[derive(Debug)]
12 struct QuickStream {
13 ticks: usize,
14 }
15
16 impl Stream for QuickStream {
17 type Item = usize;
18 type Error = Never;
19
20 fn poll_next(&mut self, _cx: &mut task::Context) ->
Poll<Option, Self::Error> {
21 match self.ticks {
22 ref mut ticks if *ticks > 0 => {
23 *ticks -= 1;
24 println!("Ticks left on QuickStream: {}", *ticks);
25 Ok(Async::Ready(Some(*ticks)))
26 }
27 _ => {
28 println!("QuickStream is closing!");
29 Ok(Async::Ready(None))
30 }
31 }
32 }
33 }
34
35 const FINISHED: Result<Async<()>, Never> = Ok(Async::Ready(()));
- 我们的
quick_streams示例将是:
37 fn quick_streams() {
38 let mut quick_stream = QuickStream { ticks: 10 };
39
40 // Collect the first poll() call
41 block_on(poll_fn(|cx| {
42 let res = quick_stream.poll_next(cx).unwrap();
43 println!("Quick stream's value: {:?}", res);
44 FINISHED
45 }))
46 .unwrap();
47
48 // Collect the second poll() call
49 block_on(poll_fn(|cx| {
50 let res = quick_stream.poll_next(cx).unwrap();
51 println!("Quick stream's next svalue: {:?}", res);
52 FINISHED
53 }))
54 .unwrap();
55
56 // And now we should be starting from 7 when collecting the
rest of the stream
57 let result: Vec<_> =
block_on(quick_stream.collect()).unwrap();
58 println!("quick_streams final result: {:?}", result);
59 }
- 有几种方法可以遍历流;让我们将它们添加到我们的代码库中:
61 fn iterate_streams() {
62 use std::borrow::BorrowMut;
63
64 let stream_response = vec![Ok(5), Ok(7), Err(false), Ok(3)];
65 let stream_response2 = vec![Ok(5), Ok(7), Err(false), Ok(3)];
66
67 // Useful for converting any of the `Iterator` traits into a
`Stream` trait.
68 let ok_stream = iter_ok::<_, ()>(vec![1, 5, 23, 12]);
69 let ok_stream2 = iter_ok::<_, ()>(vec![7, 2, 14, 19]);
70
71 let mut result_stream = iter_result(stream_response);
72 let result_stream2 = iter_result(stream_response2);
73
74 let ok_stream_response: Vec<_> =
block_on(ok_stream.collect()).unwrap();
75 println!("ok_stream_response: {:?}", ok_stream_response);
76
77 let mut count = 1;
78 loop {
79 match block_on(result_stream.borrow_mut().next()) {
80 Ok((res, _)) => {
81 match res {
82 Some(r) => println!("iter_result_stream result #{}:
{}", count, r),
83 None => { break }
84 }
85 },
86 Err((err, _)) => println!("iter_result_stream had an
error #{}: {:?}", count, err),
87 }
88 count += 1;
89 }
90
91 // Alternative way of iterating through an ok stream
92 let ok_res: Vec<_> = block_on(ok_stream2.collect()).unwrap();
93 for ok_val in ok_res.into_iter() {
94 println!("ok_stream2 value: {}", ok_val);
95 }
96
97 let (_, stream) = block_on(result_stream2.next()).unwrap();
98 let (_, stream) = block_on(stream.next()).unwrap();
99 let (err, _) = block_on(stream.next()).unwrap_err();
100
101 println!("The error for our result_stream2 was: {:?}", err);
102
103 println!("All done.");
104 }
- 现在我们来看我们的通道示例:
106 fn channel_threads() {
107 const MAX: usize = 10;
108 let (mut tx, rx) = mpsc::channel(0);
109
110 let t = thread::spawn(move || {
111 for i in 0..MAX {
112 loop {
113 if tx.try_send(i).is_ok() {
114 break;
115 } else {
116 println!("Thread transaction #{} is still pending!", i);
117 }
118 }
119 }
120 });
121
122 let result: Vec<_> = block_on(rx.collect()).unwrap();
123 for (index, res) in result.into_iter().enumerate() {
124 println!("Channel #{} result: {}", index, res);
125 }
126
127 t.join().unwrap();
128 }
- 处理错误和通道可以这样做:
130 fn channel_error() {
131 let (mut tx, rx) = mpsc::channel(0);
132
133 tx.try_send("hola").unwrap();
134
135 // This should fail
136 match tx.try_send("fail") {
137 Ok(_) => println!("This should not have been successful"),
138 Err(err) => println!("Send failed! {:?}", err),
139 }
140
141 let (result, rx) = block_on(rx.next()).ok().unwrap();
142 println!("The result of the channel transaction is: {}",
143 result.unwrap());
144
145 // Now we should be able send to the transaction since we
poll'ed a result already
146 tx.try_send("hasta la vista").unwrap();
147 drop(tx);
148
149 let (result, rx) = block_on(rx.next()).ok().unwrap();
150 println!("The next result of the channel transaction is: {}",
151 result.unwrap());
152
153 // Pulling more should result in None
154 let (result, _) = block_on(rx.next()).ok().unwrap();
155 println!("The last result of the channel transaction is:
{:?}",
156 result);
157 }
- 我们甚至可以一起处理缓冲区和通道。让我们添加我们的
channel_buffer函数:
159 fn channel_buffer() {
160 let (mut tx, mut rx) = mpsc::channel::(0);
161
162 let f = poll_fn(move |cx| {
163 if !tx.poll_ready(cx).unwrap().is_ready() {
164 panic!("transactions should be ready right away!");
165 }
166
167 tx.start_send(20).unwrap();
168 if tx.poll_ready(cx).unwrap().is_pending() {
169 println!("transaction is pending...");
170 }
171
172 // When we're still in "Pending mode" we should not be able
173 // to send more messages/values to the receiver
174 if tx.start_send(10).unwrap_err().is_full() {
175 println!("transaction could not have been sent to the
receiver due \
176 to being full...");
177 }
178
179 let result = rx.poll_next(cx).unwrap();
180 println!("the first result is: {:?}", result);
181 println!("is transaction ready? {:?}",
182 tx.poll_ready(cx).unwrap().is_ready());
183
184 // We should now be able to send another message
since we've pulled
185 // the first message into a result/value/variable.
186 if !tx.poll_ready(cx).unwrap().is_ready() {
187 panic!("transaction should be ready!");
188 }
189
190 tx.start_send(22).unwrap();
191 let result = rx.poll_next(cx).unwrap();
192 println!("new result for transaction is: {:?}", result);
193
194 FINISHED
195 });
196
197 block_on(f).unwrap();
198 }
- 虽然我们正在使用 futures crate,但这并不意味着一切都必须是并发的。添加以下示例以演示如何使用通道进行阻塞:
200 fn channel_threads_blocking() {
201 let (tx, rx) = mpsc::channel::(0);
202 let (tx_2, rx_2) = mpsc::channel::<()>(2);
203
204 let t = thread::spawn(move || {
205 let tx_2 = tx_2.sink_map_err(|_| panic!());
206 let (a, b) =
block_on(tx.send(10).join(tx_2.send(()))).unwrap();
207
208 block_on(a.send(30).join(b.send(()))).unwrap();
209 });
210
211 let (_, rx_2) = block_on(rx_2.next()).ok().unwrap();
212 let (result, rx) = block_on(rx.next()).ok().unwrap();
213 println!("The first number that we sent was: {}",
result.unwrap());
214
215 drop(block_on(rx_2.next()).ok().unwrap());
216 let (result, _) = block_on(rx.next()).ok().unwrap();
217 println!("The second number that we sent was: {}",
result.unwrap());
218
219 t.join().unwrap();
220 }
- 有时候我们需要像无界通道这样的概念;让我们添加我们的
channel_unbounded函数:
222 fn channel_unbounded() {
223 const MAX_SENDS: u32 = 5;
224 const MAX_THREADS: u32 = 4;
225 let (tx, rx) = mpsc::unbounded::();
226
227 let t = thread::spawn(move || {
228 let result: Vec<_> = block_on(rx.collect()).unwrap();
229 for item in result.iter() {
230 println!("channel_unbounded: results on rx: {:?}", item);
231 }
232 });
233
234 for _ in 0..MAX_THREADS {
235 let tx = tx.clone();
236
237 thread::spawn(move || {
238 for _ in 0..MAX_SENDS {
239 tx.unbounded_send(1).unwrap();
240 }
241 });
242 }
243
244 drop(tx);
245
246 t.join().ok().unwrap();
247 }
- 现在我们可以添加我们的
main函数:
249 fn main() {
250 println!("quick_streams():");
251 quick_streams();
252
253 println!("\niterate_streams():");
254 iterate_streams();
255
256 println!("\nchannel_threads():");
257 channel_threads();
258
259 println!("\nchannel_error():");
260 channel_error();
261
262 println!("\nchannel_buffer():");
263 channel_buffer();
264
265 println!("\nchannel_threads_blocking():");
266 channel_threads_blocking();
267
268 println!("\nchannel_unbounded():");
269 channel_unbounded();
270 }
它是如何工作的...
首先,让我们谈谈 QuickStream 结构:
-
poll_next()函数将不断被调用,并且随着每次迭代的进行,i的 ticks 属性将递减1。 -
当 ticks 属性达到
0时,轮询将停止,并返回futures::Async::Ready<None>。
在quick_streams()函数内部:
-
我们通过使用
futures::future::poll_on(f: FnMut(|cx: Context|))构建一个futures::task::Context,这样我们就可以在 42 和 50 行显式调用QuickStream的poll_next()函数。 -
由于我们在 38 行声明了
10个 ticks,我们的前两个block_on的poll_next()调用应该产生9和8。 -
下一个
block_on调用,在 57 行,将不断轮询QuickStream,直到 ticks 属性等于零时返回futures::Async::Ready<None>。
在iterate_streams()内部:
-
futures::stream::iter_ok将一个Iterator转换为一个Stream,它将始终准备好返回下一个值。 -
futures::stream::iter_result与iter_ok做同样的事情,只是我们使用Result值而不是Ok值。 -
在 78 到 89 行,我们遍历流的输出并打印出一些信息,这取决于值是
Ok还是Error类型。如果我们的流返回了None类型,那么我们将退出循环。 -
92 到 95 行显示了使用
into_iter()调用迭代流Ok结果的另一种方法。 -
97 到 99 行显示了迭代流
Result返回类型的另一种方法。
循环、迭代结果和collect()调用是同步的。我们只使用这些函数进行演示/教育目的。在实际应用中,将使用如map()、filter()、and_then()等组合子。
channel_threads()函数:
-
在 107 行,我们定义了我们想要尝试的最大发送次数。
-
在 108 行,我们声明了一个通道以发送消息。通道容量是
buffer size(futures::channel::mpsc::channel 的参数)+ 发送者数量(每个发送者都保证在通道中有一个槽位)。通道将返回一个futures::channel::mpsc::Receiver<T>,它实现了Stream特质,以及一个futures::channel::mpsc::Sender<T>,它实现了Sink特质。 -
110 到 120 行是我们
spawn一个线程并尝试发送 10 个信号的地方,循环直到每个发送都成功发送。 -
我们在 122 到 125 行收集并显示我们的结果,并在 127 行合并我们的线程。
channel_error()部分:
-
在 131 行,我们使用
0 usize缓冲区作为参数声明我们的通道,这给我们提供了一个初始发送者的槽位。 -
我们在 133 行成功发送了第一条消息。
-
136 到 139 行应该失败,因为我们正在尝试向一个被认为是满的通道发送消息(因为我们没有收到值,删除初始发送者,刷新流等)。
-
在第 146 行,我们使用发送者的
futures::channel::mpsc::Sender::try_send(&mut self, msg: T)函数,除非我们不使用第 147 行的drop(T)调用发送者的销毁方法,否则它不会阻塞我们的线程。 -
在接收到最后一个值之后,对流的任何额外轮询都将始终返回
None。
接下来是channel_buffer()函数:
-
我们在第 162 行使用
poll_fn()设置了一个 future 闭包。 -
我们在第 163 行到第 165 行使用其
futures::sink::poll_ready(&mut self, cx: &mut futures::task::Context)方法检查我们的发送者是否准备好被轮询。 -
接收器有一个名为
futures::sink::start_send(&mut self, item: <Self as Sink>::SinkItem) -> Result<(), <Self as Sink>::SinkError>的方法,它准备要发送的消息,但不会发送,直到我们刷新或关闭接收器。poll_flush()通常用于确保从接收器发送了每条消息。 -
使用
futures::stream::poll_next(&mut self, cx: &mut futures::task::Context)方法轮询流以获取下一个值,也将减轻接收器/发送者中的空间,就像我们在第 179 行所做的那样。 -
我们可以检查我们的发送者是否准备好,就像我们在第 182 行使用
futures::Async::is_ready(&self) -> bool方法所做的那样。 -
我们最终的值应该是
22,并从第 192 行显示到控制台。
然后是channel_threads_blocking()函数:
-
首先,我们在第 201 行和第 202 行设置了我们的通道。
-
然后我们
spawn了一个线程,该线程将tx_2的所有错误映射到panic!(第 205 行),然后我们向第一个通道发送10的值,同时将第二个发送者与()值连接起来(第 206 行)。在第 208 行,我们向第二个通道发送30的值和另一个空值()。 -
在第 211 行我们轮询第二个通道,它将包含一个值为
()的值。 -
在第 212 行我们轮询第一个通道,它将包含一个值为
10的值。 -
我们在第 215 行丢弃了第二个通道的接收者,因为我们需要在第 208 行的
tx_2.send()调用上关闭或刷新(tx_2在这一行被称为变量b)。 -
执行丢弃操作后,我们最终可以从第一个通道的发送者返回第二个值,这应该是
30。
以及channel_unbounded()函数:
-
在第 225 行我们声明了一个
unbounded channel,这意味着只要接收者没有关闭,向这个通道发送消息总是会成功。消息将根据需要缓冲,由于这个通道是无界的,因此我们的应用程序可能会耗尽我们的可用内存。 -
从第 227 行到第 232 行,
spawn了一个线程来收集接收者的所有消息(第 228 行),我们在第 229 行遍历它们。第 230 行的项目是一个元组,包含接收消息的索引和消息的值(在我们的例子中,这始终是 1)。 -
从第 237 行到第 241 行将
spawn出线程的数量(使用MAX_THREADS常量)以及我们想要每个线程发送的次数(使用MAX_THREADS常量)。 -
在第 244 行我们将丢弃(关闭)通道的发送者,以便我们可以收集第 228 行的所有消息。
-
在第 246 行,我们将生成的线程与当前线程连接,这将执行收集和迭代命令(第 228 行至第 231 行)。
使用 Sinks
输出端是通道、套接字、管道等 发送端,其中可以异步发送消息。输出端通过启动发送信号进行通信,然后进行轮询。在使用输出端时需要注意的一点是,它们可能会耗尽发送空间,这将阻止发送更多消息。
如何做到...
-
在
bin文件夹中,创建一个名为sinks.rs的新文件。 -
添加以下代码并使用
cargo run --bin sinks运行它:
1 extern crate futures;
2
3 use futures::prelude::*;
4 use futures::future::poll_fn;
5 use futures::executor::block_on;
6 use futures::sink::flush;
7 use futures::stream::iter_ok;
8 use futures::task::{Waker, Context};
9
10 use std::mem;
- 让我们添加使用向量作为
sinks的示例:
12 fn vector_sinks() {
13 let mut vector = Vec::new();
14 let result = vector.start_send(0);
15 let result2 = vector.start_send(7);
16
17 println!("vector_sink: results of sending should both be
Ok(()): {:?} and {:?}",
18 result,
19 result2);
20 println!("The entire vector is now {:?}", vector);
21
22 // Now we need to flush our vector sink.
23 let flush = flush(vector);
24 println!("Our flush value: {:?}", flush);
25 println!("Our vector value: {:?}",
flush.into_inner().unwrap());
26
27 let vector = Vec::new();
28 let mut result = vector.send(2);
29 // safe to unwrap since we know that we have not flushed the
sink yet
30 let result = result.get_mut().unwrap().send(4);
31
32 println!("Result of send(): {:?}", result);
33 println!("Our vector after send(): {:?}",
result.get_ref().unwrap());
34
35 let vector = block_on(result).unwrap();
36 println!("Our vector should already have one element: {:?}",
vector);
37
38 let result = block_on(vector.send(2)).unwrap();
39 println!("We can still send to our stick to ammend values:
{:?}",
40 result);
41
42 let vector = Vec::new();
43 let send_all = vector.send_all(iter_ok(vec![1, 2, 3]));
44 println!("The value of vector's send_all: {:?}", send_all);
45
46 // Add some more elements to our vector...
47 let (vector, _) = block_on(send_all).unwrap();
48 let (result, _) = block_on(vector.send_all(iter_ok(vec![0, 6,
7]))).unwrap();
49 println!("send_all's return value: {:?}", result);
50 }
我们可以映射/转换我们的 sinks 值。让我们添加我们的 mapping_sinks 示例:
52 fn mapping_sinks() {
53 let sink = Vec::new().with(|elem: i32| Ok::<i32, Never>(elem
* elem));
54
55 let sink = block_on(sink.send(0)).unwrap();
56 let sink = block_on(sink.send(3)).unwrap();
57 let sink = block_on(sink.send(5)).unwrap();
58 println!("sink with() value: {:?}", sink.into_inner());
59
60 let sink = Vec::new().with_flat_map(|elem| iter_ok(vec![elem;
elem].into_iter().map(|y| y * y)));
61
62 let sink = block_on(sink.send(0)).unwrap();
63 let sink = block_on(sink.send(3)).unwrap();
64 let sink = block_on(sink.send(5)).unwrap();
65 let sink = block_on(sink.send(7)).unwrap();
66 println!("sink with_flat_map() value: {:?}",
sink.into_inner());
67 }
我们甚至可以向多个 sinks 发送消息。让我们添加我们的 fanout 函数:
69 fn fanout() {
70 let sink1 = vec![];
71 let sink2 = vec![];
72 let sink = sink1.fanout(sink2);
73 let stream = iter_ok(vec![1, 2, 3]);
74 let (sink, _) = block_on(sink.send_all(stream)).unwrap();
75 let (sink1, sink2) = sink.into_inner();
76
77 println!("sink1 values: {:?}", sink1);
78 println!("sink2 values: {:?}", sink2);
79 }
接下来,我们将想要实现一个自定义输出端的结构。有时我们的应用程序将需要我们手动刷新 sinks 而不是自动执行。让我们添加我们的 ManualSink 结构:
81 #[derive(Debug)]
82 struct ManualSink {
83 data: Vec,
84 waiting_tasks: Vec,
85 }
86
87 impl Sink for ManualSink {
88 type SinkItem = Option; // Pass None to flush
89 type SinkError = ();
90
91 fn start_send(&mut self, op: Option) -> Result<(),
Self::SinkError> {
92 if let Some(item) = op {
93 self.data.push(item);
94 } else {
95 self.force_flush();
96 }
97
98 Ok(())
99 }
100
101 fn poll_ready(&mut self, _cx: &mut Context) -> Poll<(), ()> {
102 Ok(Async::Ready(()))
103 }
104
105 fn poll_flush(&mut self, cx: &mut Context) -> Poll<(), ()> {
106 if self.data.is_empty() {
107 Ok(Async::Ready(()))
108 } else {
109 self.waiting_tasks.push(cx.waker().clone());
110 Ok(Async::Pending)
111 }
112 }
113
114 fn poll_close(&mut self, _cx: &mut Context) -> Poll<(), ()> {
115 Ok(().into())
116 }
117 }
118
119 impl ManualSink {
120 fn new() -> ManualSink {
121 ManualSink {
122 data: Vec::new(),
123 waiting_tasks: Vec::new(),
124 }
125 }
126
127 fn force_flush(&mut self) -> Vec {
128 for task in self.waiting_tasks.clone() {
129 println!("Executing a task before replacing our values");
130 task.wake();
131 }
132
133 mem::replace(&mut self.data, vec![])
134 }
135 }
现在是我们的 manual flush 函数:
137 fn manual_flush() {
138 let mut sink = ManualSink::new().with(|x| Ok::<Option, ()>
(x));
139 let _ = sink.get_mut().start_send(Some(3));
140 let _ = sink.get_mut().start_send(Some(7));
141
142 let f = poll_fn(move |cx| -> Poll<Option<_>, Never> {
143 // Try to flush our ManualSink
144 let _ = sink.get_mut().poll_flush(cx);
145 let _ = flush(sink.get_mut());
146
147 println!("Our sink after trying to flush: {:?}",
sink.get_ref());
148
149 let results = sink.get_mut().force_flush();
150 println!("Sink data after manually flushing: {:?}",
151 sink.get_ref().data);
152 println!("Final results of sink: {:?}", results);
153
154 Ok(Async::Ready(Some(())))
155 });
156
157 block_on(f).unwrap();
158 }
最后,我们可以添加我们的 main 函数:
160 fn main() {
161 println!("vector_sinks():");
162 vector_sinks();
163
164 println!("\nmapping_sinks():");
165 mapping_sinks();
166
167 println!("\nfanout():");
168 fanout();
169
170 println!("\nmanual_flush():");
171 manual_flush();
172 }
它是如何工作的...
首先,让我们看看 futures::Sink 特性本身:
pub trait Sink {
type SinkItem;
type SinkError;
fn poll_ready(
&mut self,
cx: &mut Context
) -> Result<Async<()>, Self::SinkError>;
fn start_send(
&mut self,
item: Self::SinkItem
) -> Result<(), Self::SinkError>;
fn poll_flush(
&mut self,
cx: &mut Context
) -> Result<Async<()>, Self::SinkError>;
fn poll_close(
&mut self,
cx: &mut Context
) -> Result<Async<()>, Self::SinkError>;
}
我们已经熟悉了来自 futures 和 streams 的 Item 和 Error 概念,因此我们将继续到所需的函数:
-
在每次尝试使用
start_send之前,必须使用返回值Ok(futures::Async::Ready(()))调用poll_ready。如果输出端收到错误,输出端将无法再接收项。 -
如前所述,
start_send准备要发送的消息,但只有在刷新或关闭输出端之前,它才会发送。如果输出端使用缓冲区,则Sink::SinkItem不会在缓冲区完全完成后被处理。 -
poll_flush将刷新输出端,这将允许我们收集正在处理的项。如果输出端在缓冲区中没有更多项,则将返回futures::Async::Ready,否则输出端将返回futures::Async::Pending。 -
poll_close将刷新并关闭输出端,遵循与poll_flush相同的返回规则。
现在,让我们看看我们的 vector_sinks() 函数:
-
输出端是为
Vec<T>类型实现的,因此我们可以声明一个可变向量并使用start_send()函数,该函数将立即将我们的值轮询到第 13 行至第 15 行的向量中。 -
在第 28 行,我们使用
futures::SinkExt::send(self, item: Self::SinkItem),这将完成在项被处理并通过输出端刷新后。建议使用futures::SinkExt::send_all来批量发送多个项,而不是在每次发送调用之间手动刷新(如第 43 行所示)。
我们的 mapping_sinks() 函数:
-
第 51 行展示了如何使用
futures::SinkExt::with函数在接收器内映射/操作元素。这个函数产生一个新的接收器,它遍历每个项目,并将最终值 作为一个 future 发送到 父 接收器。 -
第 60 行说明了
futures::SinkExt::flat_with_map函数,它基本上与futures::SinkExt::with函数具有相同的功能,除了每个迭代的项被作为流值发送到 父 接收器,并且将返回一个Iterator::flat_map值而不是Iterator::map。
接下来是 fanout() 函数:
futures::SinkExt::fanout函数允许我们一次向多个接收器发送消息,就像我们在第 72 行所做的那样。
然后执行 manual_flush():
-
我们首先使用
ManualSink<T>构造函数实现我们自己的Sink特性(第 81 到 135 行)。我们的ManualSink的poll_flush方法只有在我们的数据向量为空时才会返回Async::Ready(),否则,我们将任务(futures::task::Waker)推入通过waiting_tasks属性的队列中。我们在force_flush()函数(第 128 行)中使用waiting_tasks属性来手动 唤醒 我们的任务(第 130 行)。 -
在第 138 到 140 行,我们构建了我们的
ManualSink<Option<i32>>并开始发送一些值。 -
我们在第 142 行使用
poll_fn来快速构建一个futures::task::Context,以便我们可以将此值传递给底层的轮询调用。 -
在第 144 行,我们手动调用我们的
poll_flush()函数,由于任务被放置在waiting_tasks属性中,所以它不会执行实际的任务。 -
直到我们调用
force_flush(),我们的接收器将不会返回任何值(如第 150-151 行所示)。一旦这个函数被调用,并且底层的Waker任务执行完毕,我们就可以看到我们之前发送的消息(第 152 行,第 139 和 140 行)。
使用单次发送通道
单次发送通道在你只需要向通道发送一条消息时很有用。单次发送通道适用于那些实际上只需要更新/通知一次的任务,例如,接收者是否阅读了你的消息,或者作为任务管道中的最终目的地,通知最终用户任务已完成。
如何实现...
-
在
bin文件夹内,创建一个名为oneshot.rs的新文件。 -
添加以下代码并使用
cargo run --bin oneshot运行它:
1 extern crate futures;
2
3 use futures::prelude::*;
4 use futures::channel::oneshot::*;
5 use futures::executor::block_on;
6 use futures::future::poll_fn;
7 use futures::stream::futures_ordered;
8
9 const FINISHED: Result<Async<()>, Never> =
Ok(Async::Ready(()));
10
11 fn send_example() {
12 // First, we'll need to initiate some oneshot channels like
so:
13 let (tx_1, rx_1) = channel::();
14 let (tx_2, rx_2) = channel::();
15 let (tx_3, rx_3) = channel::();
16
17 // We can decide if we want to sort our futures by FIFO
(futures_ordered)
18 // or if the order doesn't matter (futures_unordered)
19 // Note: All futured_ordered()'ed futures must be set as a
Box type
20 let mut ordered_stream = futures_ordered(vec![
21 Box::new(rx_1) as Box<Future>,
22 Box::new(rx_2) as Box<Future>,
23 ]);
24
25 ordered_stream.push(Box::new(rx_3) as Box<Future>);
26
27 // unordered example:
28 // let unordered_stream = futures_unordered(vec![rx_1, rx_2,
rx_3]);
29
30 // Call an API, database, etc. and return the values (in our
case we're typecasting to u32)
31 tx_1.send(7).unwrap();
32 tx_2.send(12).unwrap();
33 tx_3.send(3).unwrap();
34
35 let ordered_results: Vec<_> =
block_on(ordered_stream.collect()).unwrap();
36 println!("Ordered stream results: {:?}", ordered_results);
37 }
38
39 fn check_if_closed() {
40 let (tx, rx) = channel::();
41
42 println!("Is our channel canceled? {:?}", tx.is_canceled());
43 drop(rx);
44
45 println!("Is our channel canceled now? {:?}",
tx.is_canceled());
46 }
47
48 fn check_if_ready() {
49 let (mut tx, rx) = channel::();
50 let mut rx = Some(rx);
51
52 block_on(poll_fn(|cx| {
53 println!("Is the transaction pending? {:?}",
54 tx.poll_cancel(cx).unwrap().is_pending());
55 drop(rx.take());
56
57 let is_ready = tx.poll_cancel(cx).unwrap().is_ready();
58 let is_pending =
tx.poll_cancel(cx).unwrap().is_pending();
59
60 println!("Are we ready? {:?} This means that the pending
should be false: {:?}",
61 is_ready,
62 is_pending);
63 FINISHED
64 }))
65 .unwrap();
66 }
67
68 fn main() {
69 println!("send_example():");
70 send_example();
71
72 println!("\ncheck_if_closed():");
73 check_if_closed();
74
75 println!("\ncheck_if_ready():");
76 check_if_ready();
77 }
它是如何工作的...
在我们的 send_example() 函数中:
-
在第 13 到 15 行,我们设置了三个
oneshot通道。 -
在第 20 到 23 行,我们使用
futures::stream::futures_ordered,它将 future 的列表(任何IntoIterator值)转换为在先进先出(FIFO)基础上产生结果的Stream。如果任何底层 future 在下一个 future 被调用之前没有完成,这个函数将等待直到长时间运行的 future 完成,然后将其内部重新排序到正确的顺序。 -
第 25 行显示我们可以将额外的 futures 推入
futures_ordered迭代器中。 -
第 28 行演示了另一个不依赖于基于 FIFO 排序的排序函数,称为
futures::stream::futures_unordered。这个函数的性能将优于其对应物futures_ordered,但对我们这个示例来说,我们发送的值不足以产生差异。 -
在第 31 到 33 行,我们向我们的通道发送值,模拟从 API、数据库等返回值的过程。如果发送成功,则返回
Ok(()),否则返回Err类型。 -
在我们最后两行(35 和 36)中,我们收集
futures_ordered的值并将它们显示到控制台。
接下来,是 check_if_closed() 函数:
- 我们的通道应该保持打开状态,直到我们显式地丢弃/销毁接收器(或向通道发送一个值)。我们可以通过调用
futures::channel::oneshot::Sender::is_canceled(&self) -> bool函数来检查我们接收器的状态,我们在第 42 和 45 行已经这样做过了。
然后是 check_if_ready() 函数:
-
在第 50 行,我们显式地为一个 oneshot 的接收器分配一个值,这将使我们的接收器处于挂起状态(因为它已经有一个值了)。
-
我们在第 55 行丢弃了我们的接收器,我们可以通过使用我们的发送器的
futures::channel::oneshot::Sender::poll_cancel函数来检查我们的接收器是否就绪,我们在第 57 和 58 行使用它。poll_cancel将在接收器被丢弃时返回Ok(Async::Ready),如果接收器没有被丢弃,则返回Ok(Async::Pending)。
返回 futures
Future 特性依赖于三个主要成分:一个类型、一个错误和一个返回 Result<Async<T>, E> 结构的 poll() 函数。poll() 方法永远不会阻塞主线程,而 Async<T> 是一个具有两个变体的枚举器:Ready(T) 和 Pending。定期地,poll() 方法将由任务上下文的 waker() 特性调用,位于 futures::task::context::waker 中,直到有值可以返回。
如何做到这一点...
-
在
src/bin文件夹中,创建一个名为returning.rs的文件。 -
添加以下代码并使用
cargo run —bin returning运行它:
1 extern crate futures;
2
3 use futures::executor::block_on;
4 use futures::future::{join_all, Future, FutureResult, ok};
5 use futures::prelude::*;
6
7 #[derive(Clone, Copy, Debug, PartialEq)]
8 enum PlayerStatus {
9 Loading,
10 Default,
11 Jumping,
12 }
13
14 #[derive(Clone, Copy, Debug)]
15 struct Player {
16 name: &'static str,
17 status: PlayerStatus,
18 score: u32,
19 ticks: usize,
20 }
- 现在是结构体的实现:
22 impl Player {
23 fn new(name: &'static str) -> Self {
24 let mut ticks = 1;
25 // Give Bob more ticks explicitly
26 if name == "Bob" {
27 ticks = 5;
28 }
29
30 Player {
31 name: name,
32 status: PlayerStatus::Loading,
33 score: 0,
34 ticks: ticks,
35 }
36 }
37
38 fn set_status(&mut self, status: PlayerStatus) ->
FutureResult<&mut Self, Never> {
39 self.status = status;
40 ok(self)
41 }
42
43 fn can_add_points(&mut self) -> bool {
44 if self.status == PlayerStatus::Default {
45 return true;
46 }
47
48 println!("We couldn't add any points for {}!", self.name);
49 return false;
50 }
51
52 fn add_points(&mut self, points: u32) -> Async<&mut Self> {
53 if !self.can_add_points() {
54 Async::Ready(self)
55 } else {
56 let new_score = self.score + points;
57 // Here we would send the new score to a remote server
58 // but for now we will manaully increment the player's
score.
59
60 self.score = new_score;
61
62 Async::Ready(self)
63 }
64 }
65 }
66
67 impl Future for Player {
68 type Item = Player;
69 type Error = ();
70
71 fn poll(&mut self, cx: &mut task::Context) ->
Poll<Self::Item, Self::Error> {
72 // Presuming we fetch our player's score from a
73 // server upon initial load.
74 // After we perform the fetch send the Result value.
75
76 println!("Player {} has been poll'ed!", self.name);
77
78 if self.ticks == 0 {
79 self.status = PlayerStatus::Default;
80 Ok(Async::Ready(*self))
81 } else {
82 self.ticks -= 1;
83 cx.waker().wake();
84 Ok(Async::Pending)
85 }
86 }
87 }
- 接下来,我们将添加我们的
helper函数和用于给玩家添加分数的Async函数:
89 fn async_add_points(player: &mut Player,
90 points: u32)
91 -> Box<Future + Send> {
92 // Presuming that player.add_points() will send the points to a
93 // database/server over a network and returns an updated
94 // player score from the server/database.
95 let _ = player.add_points(points);
96
97 // Additionally, we may want to add logging mechanisms,
98 // friend notifications, etc. here.
99
100 return Box::new(ok(*player));
101 }
102
103 fn display_scoreboard(players: Vec<&Player>) {
104 for player in players {
105 println!("{}'s Score: {}", player.name, player.score);
106 }
107 }
- 最后,实际的用法:
109 fn main() {
110 let mut player1 = Player::new("Bob");
111 let mut player2 = Player::new("Alice");
112
113 let tasks = join_all(vec![player1, player2]);
114
115 let f = join_all(vec![
116 async_add_points(&mut player1, 5),
117 async_add_points(&mut player2, 2),
118 ])
119 .then(|x| {
120 println!("First batch of adding points is done.");
121 x
122 });
123
124 block_on(f).unwrap();
125
126 let players = block_on(tasks).unwrap();
127 player1 = players[0];
128 player2 = players[1];
129
130 println!("Scores should be zero since no players were
loaded");
131 display_scoreboard(vec![&player1, &player2]);
132
133 // In our minigame, a player cannot score if they are
currently
134 // in the air or "jumping."
135 // Let's make one of our players' status set to the jumping
status.
136
137 let f =
player2.set_status(PlayerStatus::Jumping).and_then(move |mut
new_player2| {
138 async_add_points(&mut player1, 10)
139 .and_then(move |_| {
140 println!("Finished trying to give Player 1 points.");
141 async_add_points(&mut new_player2, 2)
142 })
143 .then(move |new_player2| {
144 println!("Finished trying to give Player 2 points.");
145 println!("Player 1 (Bob) should have a score of 10 and
Player 2 (Alice) should \
146 have a score of 0");
147
148 // unwrap is used here to since
149 display_scoreboard(vec![&player1,
&new_player2.unwrap()]);
150 new_player2
151 })
152 });
153
154 block_on(f).unwrap();
155
156 println!("All done!");
157 }
它是如何工作的...
让我们先介绍参与这个示例的结构:
-
PlayerStatus是一个枚举器,用于在玩家的实例上维护一个 全局 状态。变体有:-
“加载”,这是初始状态
-
“默认”,在我们完成加载玩家的统计数据后应用
-
“跳跃”是一种特殊状态,由于游戏规则,它不会允许我们向玩家的计分板上添加分数
-
-
Player包含玩家主要属性,以及一个名为ticks的特殊属性,它存储了我们想要在将玩家的状态从Loading转换为Default之前通过poll()运行的周期数。
现在,让我们来看一下我们的实现:
-
跳转到
Player结构中的fn set_status(&mut self, status: PlayerStatus) -> FutureResult<&mut Self, Never>函数,我们会注意到返回值是FutureResult,这告诉 futures 这个函数将立即从futures::futures的result()、ok()或err()函数返回一个计算值。这对于快速原型设计我们的应用程序非常有用,同时能够利用我们的executors和未来组合器。 -
在
fn add_points(&mut self, points: u32) -> Async<&mut Self>函数中,我们立即返回我们的Async值,因为我们目前没有服务器可以使用,但我们会为需要异步计算的函数实现Async<T>值,基于FutureResult。 -
我们使用玩家的
ticks属性来模拟网络请求所需的时间。Poll<I, E>将会一直执行,只要我们返回Async::Pending(行 [x])。执行器需要知道是否需要再次轮询任务。任务的Waker特性负责处理这些通知,我们可以在第 83 行手动调用它,使用cx.waker().wake()。一旦玩家的ticks属性达到零,我们发送一个Async::Ready(self)信号,这告诉执行器不再轮询此函数。
对于我们的 async_add_points() 辅助方法:
-
我们返回
Box<Future<Item = Player, Error = Never> + Send>,这告诉 futures 这个函数最终将返回一个Player类型的值(因为我们Never返回错误)。 -
返回值中的
+ Send部分对于我们的当前代码库不是必需的,但将来,我们可能希望将这些任务卸载到其他线程上,因为执行器需要这样做。跨线程生成需要我们返回futures::prelude::Never类型作为错误,以及一个'static变量。 -
当使用组合器(如
then和and_then)调用未来函数时,我们需要返回Never错误类型或与同一组合器流中调用的其他每个未来函数相同的错误类型。
最后,让我们来看一下我们的主块:
-
我们使用
futures::future::join_all函数,它接受任何包含所有InfoFuture特性元素的IntoIterator(这应该是所有未来函数)。这要么收集并返回排序为 FIFO 的Vec<T>,要么在集合中的任何未来函数返回第一个错误时取消执行,这成为join_all()调用的返回值。 -
then()和and_then()是内部使用future::Chain并返回Future特性值的组合器,这允许我们添加更多组合器。有关组合器的更多信息,请参阅 使用组合器和工具 部分。 -
block_on()是一个执行器方法,它处理任何未来函数或值作为其输入,并返回Result<Future::Item, Future::Error>。当运行此方法时,包含该方法的函数将阻塞,直到未来(s)完成。派生的任务将在默认执行器上执行,但它们可能不会在block_on完成其任务(s)之前完成。如果block_on()在派生任务之前完成,则那些派生的任务将被丢弃。 -
我们还可以使用
block_on()作为快速运行我们的周期/滴答和执行任务(s)的方法,这会调用我们的poll()函数。我们在第 124 行使用此方法来最初加载玩家进入游戏。
还有更多...
返回未来的box()方法会导致堆上的额外分配。另一种返回未来的方法是使用 Rust 的nightly版本,或者等待此问题github.com/rust-lang/rust/issues/34511得到解决。新的async_add_points()方法将返回一个隐含的Future特质,其外观如下:
fn async_add_points<F>(f: F, player: &mut Player, points: u32) -> impl Future<Item = Player, Error = F::Error>
where F: Future<Item = Player>,
{
// Presuming that player.add_points() will send the points to a
// database/server over a network and returns
// an updated player score from the server/database.
let _ = player.add_points(points).flatten();
// Additionally, we may want to add logging mechanisms, friend
notifications, etc. here.
return f.map(player.clone());
}
如果我们对一个未来调用poll()超过一次,Rust 可能会引起undefined behavior。这个问题可以通过使用into_stream()方法将未来转换为流或使用添加了微小运行时开销的fuse()适配器来缓解。
任务通常是通过使用executor(例如block_on()辅助函数)来执行/轮询的。你可以通过创建task::Context并直接从任务中调用poll()来手动执行任务。作为一般规则,建议不要手动调用poll(),而应该由执行器自动管理轮询。
参见
-
第五章中的数据结构高级章节中的数据装箱配方
-
第七章,并行性与 Rayon
使用 BiLocks 锁定资源
当我们需要在多个线程之间存储一个值,并且该值最多有两个所有者时,会使用 BiLocks。BiLock 类型的适用用途包括分割 TCP/UDP 数据用于读写,或者在接收器和流之间添加一层(用于日志记录、监控等),或者它可以是同时作为接收器和流。
当使用带有额外 crate(例如 tokio 或 hyper)的未来时,了解 BiLocks 可以帮助我们将数据包装在其他 crate 的常用方法周围。这将使我们能够在不等待 crate 的维护者明确支持并发的情况下,在现有 crate 之上构建未来和并发。BiLocks 是一个非常底层的实用工具,但了解它们的工作原理可以帮助我们在未来的(Web)应用中走得更远。
在下一章中,我们将主要关注 Rust 的网络编程,但我们也会练习将 futures 与其他 crate 集成。如果需要将 TCP/UDP 流分割成互斥状态,可以使用 BiLocks,尽管使用我们将要使用的 crate 时这样做不是必需的。
如何实现...
-
在
src/bin文件夹中,创建一个名为bilocks.rs的文件。 -
添加以下代码并使用
cargo run —bin bilocks运行它:
1 extern crate futures;
2 extern crate futures_util;
3
4 use futures::prelude::*;
5 use futures::executor::LocalPool;
6 use futures::task::{Context, LocalMap, Wake, Waker};
7 use futures_util::lock::BiLock;
8
9 use std::sync::Arc;
10
11 struct FakeWaker;
12 impl Wake for FakeWaker {
13 fn wake(_: &Arc) {}
14 }
15
16 struct Reader {
17 lock: BiLock,
18 }
19
20 struct Writer {
21 lock: BiLock,
22 }
23
24 fn split() -> (Reader, Writer) {
25 let (a, b) = BiLock::new(0);
26 (Reader { lock: a }, Writer { lock: b })
27 }
29 fn main() {
30 let pool = LocalPool::new();
31 let mut exec = pool.executor();
32 let waker = Waker::from(Arc::new(FakeWaker));
33 let mut map = LocalMap::new();
34 let mut cx = Context::new(&mut map, &waker, &mut exec);
35
36 let (reader, writer) = split();
37 println!("Lock should be ready for writer: {}",
38 writer.lock.poll_lock(&mut cx).is_ready());
39 println!("Lock should be ready for reader: {}",
40 reader.lock.poll_lock(&mut cx).is_ready());
41
42 let mut writer_lock = match writer.lock.lock().poll(&mut
cx).unwrap() {
43 Async::Ready(t) => t,
44 _ => panic!("We should be able to lock with writer"),
45 };
46
47 println!("Lock should now be pending for reader: {}",
48 reader.lock.poll_lock(&mut cx).is_pending());
49 *writer_lock = 123;
50
51 let mut lock = reader.lock.lock();
52 match lock.poll(&mut cx).unwrap() {
53 Async::Ready(_) => {
54 panic!("The lock should not be lockable since writer has
already locked it!")
55 }
56 _ => println!("Couldn't lock with reader since writer has
already initiated the lock"),
57 };
58
59 let writer = writer_lock.unlock();
60
61 let reader_lock = match lock.poll(&mut cx).unwrap() {
62 Async::Ready(t) => t,
63 _ => panic!("We should be able to lock with reader"),
64 };
65
66 println!("The new value for the lock is: {}", *reader_lock);
67
68 let reader = reader_lock.unlock();
69 let reunited_value = reader.reunite(writer).unwrap();
70
71 println!("After reuniting our locks, the final value is
still: {}",
72 reunited_value);
73 }
它是如何工作的...
-
首先,我们需要实现一个假的
futures::task::Waker,以便在我们创建一个新的上下文时使用(这就是第 11 行至第 14 行的FakeWaker结构的作用)。 -
由于 BiLocks 需要两个所有者,我们将所有权分为两个不同的结构,称为
Reader<T>(第 16 行至第 18 行)和Writer<T>(第 20 行至第 22 行)。 -
我们的
split() -> (Reader<u32>, Writer<u32>)函数只是为了更好地结构化/组织我们的代码,并且在调用BiLock::new(t: T)时,返回类型是两个futures_util::lock::BiLock元素的元组。
现在初步代码已经解释完毕,让我们深入到我们的main()函数:
-
在第 30 行至第 34 行,我们设置了一个新的
LocalPool、LocalExecutor、Waker(FakeWaker)和一个LocalMap(任务内的本地数据映射存储),用于创建一个新的Context,因为我们将会手动轮询锁以进行演示。 -
第 38 行和第 40 行使用了
futures_util::lock::BiLock::poll_lock函数,如果锁可用,则返回一个Async<futures_util::lock::BiLockGuard<T>>值。如果锁不可用,则函数将返回Async::Pending。当引用被丢弃时,锁(BiLockGuard<T>)将解锁。 -
在第 42 行,我们执行
writer.lock.lock(),这将阻塞锁并返回一个BiLockAcquire<T>,这是一个可以被轮询的未来。当轮询BiLockAcquire时,返回一个Poll<BiLockAcquired<T>, ()>值,并且这个值可以被可变地解引用。 -
在第 48 行,我们现在可以看到锁目前处于
Async::Pending状态,这不会允许我们再次锁定 BiLock,正如第 51 行至第 57 行所示。 -
在修改我们的锁的值(第 49 行)之后,我们现在应该解锁它(第 59 行),以便其他所有者可以引用它(第 61 行至第 64 行)。
-
当我们调用
BiLockAcquired::unlock()(第 68 行)时,返回原始的BiLock<T>,并且锁正式解锁。 -
在第 69 行,我们执行
futures_util::lock::BiLock::reunite(other: T),这恢复了锁的值并销毁了 BiLock 引用的两个部分(假设T是BiLock::new()调用中 BiLock 的另一部分)。
第九章:网络编程
在本章中,我们将介绍以下食谱:
-
设置基本 HTTP 服务器
-
配置 HTTP 服务器以执行回声和路由
-
配置 HTTP 服务器以执行文件服务
-
向 API 发送请求
-
设置基本的 UDP 套接字
-
配置 UDP 套接字以执行回声
-
通过 TLS 设置安全连接
简介
通过互联网,世界每天都在变得更小。网络以惊人的方式连接着人们。无数服务免费供您使用。数百万的人可以免费使用您的应用程序,甚至不需要安装它。
作为想要利用这一点的开发者,如果您以干净的方式设置了您的架构,将您的应用程序移植到互联网上可以非常简单。您唯一需要更改的是与外界交互的层。
本章将向您展示如何通过允许您的应用程序接受请求、响应它们,并展示如何创建对其他 Web 服务的请求来创建这一层。
设置基本 HTTP 服务器
让我们从将著名的 Hello World 程序带入 21 世纪,通过在服务器上托管它来开始我们的章节。我们将使用hyper crate 来完成这个任务,它是一个围绕所有 HTTP 事物的强类型包装器。除了是世界上速度最快的 HTTP 实现之一(www.techempower.com/benchmarks/#section=data-r15&hw=ph&test=plaintext),它还被几乎所有主要的高级框架使用(github.com/flosse/rust-web-framework-comparison#high-level-frameworks),唯一的例外是那些在 Rust 提供的std::net::TcpStream极其基本的字符串类型库上重新实现了它的框架。
准备工作
所有hyper食谱都与futures兼容,因此在继续之前,您应该阅读所有第八章,与 Futures 一起工作。
在撰写本文时,hyper尚未升级到futures v0.2(跟踪问题:github.com/hyperium/hyper/issues/1448),因此我们将使用futures v0.1。这应该在未来没有问题(没有打趣的意思),因为所有相关代码都是按照应该与0.2兼容的方式编写的。
如果某些意外的 API 更改破坏了食谱,您可以在本书的 GitHub 仓库中找到它们的修复版本(github.com/jnferner/rust-standard-library-cookbook/tree/master/chapter-nine/src/bin),它将始终更新以与所有库的最新版本兼容。
如何操作...
-
使用
cargo new chapter-nine创建一个 Rust 项目,在本章中对其进行工作。 -
导航到新创建的
chapter-nine文件夹。在本章的其余部分,我们将假设您的命令行当前位于此目录 -
打开为您生成的
Cargo.toml文件 -
在
[dependencies]下,添加以下行:
futures = "0.1.18"
hyper = "0.11.21"
-
如果您愿意,可以访问 futures' (
crates.io/crates/futures) 和 hyper's (crates.io/crates/hyper) 的 crates.io 页面,检查最新版本并使用它 -
在文件夹
src内,创建一个名为bin的新文件夹 -
删除生成的
lib.rs文件,因为我们没有创建库 -
在文件夹
src/bin中,创建一个名为hello_world_server.rs的文件 -
添加以下代码,并用
cargo run --bin hello_world_server运行它:
1 extern crate futures;
2 extern crate hyper;
3
4 use futures::future::Future;
5 use hyper::header::{ContentLength, ContentType};
6 use hyper::server::{const_service, service_fn, Http, Request, Response, Service};
7 use std::net::SocketAddr;
8
9 const MESSAGE: &str = "Hello World!";
10
11 fn main() {
12 // [::1] is the loopback address for IPv6, 3000 is a port
13 let addr = "[::1]:3000".parse().expect("Failed to parse address");
14 run_with_service_function(&addr).expect("Failed to run web server");
15 }
通过使用 service_fn 创建服务来运行服务器:
17 fn run_with_service_function(addr: &SocketAddr) -> Result<(),
hyper::Error> {
18 // Hyper is based on Services, which are construct that
19 // handle how to respond to requests.
20 // const_service and service_fn are convenience functions
21 // that build a service out of a closure
22 let hello_world = const_service(service_fn(|_| {
23 println!("Got a connection!");
24 // Return a Response with a body of type hyper::Body
25 Ok(Response::::new()
26 // Add header specifying content type as plain text
27 .with_header(ContentType::plaintext())
28 // Add header specifying the length of the message in
bytes
29 .with_header(ContentLength(MESSAGE.len() as u64))
30 // Add body with our message
31 .with_body(MESSAGE))
32 }));
33
34 let server = Http::new().bind(addr, hello_world)?;
35 server.run()
36 }
通过手动创建一个实现 Service 的 struct 来运行服务器:
38 // The following function does the same, but uses an explicitely
created
39 // struct HelloWorld that implements the Service trait
40 fn run_with_service_struct(addr: &SocketAddr) -> Result<(),
hyper::Error> {
41 let server = Http::new().bind(addr, || Ok(HelloWorld))?;
42 server.run()
43 }
44
45 struct HelloWorld;
46 impl Service for HelloWorld {
47 // Implementing a server requires specifying all involved
types
48 type Request = Request;
49 type Response = Response;
50 type Error = hyper::Error;
51 // The future that wraps your eventual Response
52 type Future = Box<Future>;
53
54 fn call(&self, _: Request) -> Self::Future {
55 // In contrast to service_fn, we need to explicitely return
a future
56 Box::new(futures::future::ok(
57 Response::new()
58 .with_header(ContentType::plaintext())
59 .with_header(ContentLength(MESSAGE.len() as u64))
60 .with_body(MESSAGE),
61 ))
62 }
63 }
它是如何工作的...
在 main 中,我们首先将表示我们的 IPv6 回环地址(例如 localhost)的字符串解析为 std::net::SocketAddr,这是一个包含 IP 地址和端口的类型 [13]。当然,我们可以使用一个常量作为我们的地址,但我们展示的是如何从字符串中解析它,因为在实际应用中,您可能需要从环境变量中获取地址,正如在 第一章 学习基础知识;与环境变量交互 中所示。
然后,我们运行在 run_with_service_function [17] 中创建的 hyper 服务器。让我们通过了解一下 hyper 来看看这个函数。
hyper 中最基础的特质是 Service。它被定义为如下:
pub trait Service where
<Self::Future as Future>::Item == Self::Response,
<Self::Future as Future>::Error == Self::Error, {
type Request;
type Response;
type Error;
type Future: Future;
fn call(&self, req: Self::Request) -> Self::Future;
}
应该很容易阅读 call 的签名:它接受一个 Request 并返回一个 Response 的 Future。hyper 使用这个特质来响应传入的请求。我们通常有两种方式来定义一个 Service:
-
手动创建一个实现
Service的struct,显式设置其关联类型为call返回的类型 -
通过传递一个返回
Result的闭包给service_fn,并使用const_service包装它,让Service为您构建
这两种变体都会产生完全相同的结果,所以这个例子包含了两种版本,以便您尝尝它们的味道。
run_with_service_function使用第二种风格[22]。它返回一个Response的Result,service_fn将其转换为Response的Future,因为Result实现了Future。service_fn然后为我们进行一些类型推导并创建一种Service。但我们的任务还没有完成。您可以看到,当hyper接收到一个新的连接时,它不会直接用Request调用我们的Service,而是首先复制它,以便为每个连接使用其自己的Service。这意味着我们的Service必须具有创建自身新实例的能力,这由NewService特质表示。幸运的是,我们也不需要自己实现它。我们Service核心的闭包不管理任何状态,所以我们可以称它为一个常量函数。常量非常容易复制,因为所有副本都保证是相同的。我们可以通过调用const_service来标记我们的Service为常量,这基本上只是将Service包装在一个Arc中,然后通过简单地返回其副本来实现NewService。但我们的Service究竟返回了什么呢?
Response<hyper::Body>创建一个新的 HTTP 响应[25],并管理其体作为hyper::Body,这是一个未来的Stream<Chunk>。Chunk只是 HTTP 消息的一部分。这个Response是一个构建器,所以我们可以通过调用各种方法来更改其内容。在我们的代码中,我们将其Content-Type标题设置为plaintext,这是hyper对 MIME 类型text/plain的快捷方式[27]。
MIME 类型是用于通过 HTTP 服务的数据的标签。它告诉客户端如何处理它接收到的数据。例如,大多数浏览器不会将消息<p>Hello World!</p>作为 HTML 渲染,除非它带有标题Content-Type: text/html。
我们还将其Content-Length标题设置为消息的长度(以字节为单位),以便客户端知道他们应该期望多少数据[29]。最后,我们将消息体设置为消息,然后将其作为"Hello World!"发送到客户端[31]。
我们的服务现在可以绑定到一个新的hyper::server::Http实例上,然后我们运行它[34 和 35]。现在您可以使用您选择的浏览器,将其指向http://localhost:3000。如果一切顺利,您应该会看到一个Hello World!消息。
如果我们调用run_with_service_struct而不是这个,它使用手动创建的Service,也会发生同样的事情[40]。快速检查其实现显示给我们与最后一种方法[45 到 63]的关键区别:
struct HelloWorld;
impl Service for HelloWorld {
type Request = Request;
type Response = Response;
type Error = hyper::Error;
type Future = Box<Future<Item = Self::Response, Error =
Self::Error>>;
fn call(&self, _: Request) -> Self::Future {
Box::new(futures::future::ok(
Response::new()
.with_header(ContentType::plaintext())
.with_header(ContentLength(MESSAGE.len() as u64))
.with_body(MESSAGE),
))
}
}
如您所见,我们需要明确指定基本上所有东西的具体类型[48 到 52]。我们也不能简单地在我们的call方法中返回一个Result,而需要返回实际的Future,并用Box[56]包装它,这样我们就不需要考虑我们正在使用哪种具体的Future类型了。
另一方面,这种方法与另一种方法相比有一个很大的优点:它可以以成员的形式管理状态。因为本章中所有的 hyper 菜谱都使用常量服务,即对等请求将返回相同的 Response 的服务,我们将使用第一个变体来创建服务。这仅仅是一个基于简单性的风格选择,因为它们都足够小,不值得提取到自己的 struct 中。在你的项目中,使用最适合当前用例的形式。
参见
- 使用构建器模式 和 第一章 中的 与环境变量交互 菜单,学习基础知识
配置一个执行回声和路由的 HTTP 服务器
我们学习了如何永久地提供相同的响应,但过一段时间后这会变得相当无聊。在这个菜谱中,你将学习如何读取请求并单独对它们进行响应。为此,我们将使用路由来区分对不同端点的请求。
准备工作
要测试这个菜谱,你需要一种轻松发送 HTTP 请求的方法。一个出色的免费工具是 Postman (www.getpostman.com/),它具有一个友好且易于理解的界面。如果你不想下载任何东西,你可以使用终端来做这个。如果你在 Windows 上,你可以打开 PowerShell 并输入以下命令来进行 HTTP 请求:
Invoke-WebRequest -UseBasicParsing <Your URL> -Method <Your method in CAPSLOCK> -Body <Your message as a string>
因此,如果你想将消息 hello there, my echoing friend POST 到 http://localhost:3000/echo,正如菜谱中稍后所要求的那样,你需要输入以下命令:
Invoke-WebRequest -UseBasicParsing http://localhost:3000/echo -Method POST -Body "Hello there, my echoing friend"
在 Unix 系统上,你可以使用 cURL 来做这个 (curl.haxx.se/)。相应的命令如下:
curl -X <Your method> --data <Your message> -g <Your URL>
cURL 将将 localhost 解析为其 /etc/hosts 中的条目。在某些配置中,这将是 IPv4 环回地址 (127.0.0.1)。在其他一些配置中,你必须使用 ip6-localhost。检查你的 /etc/hosts 以确定使用什么。在任何情况下,显式的 [::1] 总是有效的。例如,以下命令将再次将消息 hello there, my echoing friend POST 到 http://localhost:3000/echo:
curl -X POST --data "Hello there my echoing friend" -g "http://[::1]:3000/echo"
如何操作...
-
打开为你生成的
Cargo.toml文件 -
在
[dependencies]下,如果你在上一个菜谱中没有这样做,请添加以下行:
futures = "0.1.18"
hyper = "0.11.21"
-
如果你愿意,你可以访问 futures' (
crates.io/crates/futures) 和 hyper's (crates.io/crates/hyper) 的 crates.io 页面,检查最新版本并使用它。 -
在
src/bin文件夹中创建一个名为echo_server_with_routing.rs的文件 -
添加以下代码,并使用
cargo run --bin echo_server_with_routing运行它:
1 extern crate hyper;
2
3 use hyper::{Method, StatusCode};
4 use hyper::server::{const_service, service_fn, Http, Request,
Response};
5 use hyper::header::{ContentLength, ContentType};
6 use std::net::SocketAddr;
7
8 fn main() {
9 let addr = "[::1]:3000".parse().expect("Failed to parse
address");
10 run_echo_server(&addr).expect("Failed to run web server");
11 }
12
13 fn run_echo_server(addr: &SocketAddr) -> Result<(),
hyper::Error> {
14 let echo_service = const_service(service_fn(|req: Request| {
15 // An easy way to implement routing is
16 // to simply match the request's path
17 match (req.method(), req.path()) {
18 (&Method::Get, "/") => handle_root(),
19 (&Method::Post, "/echo") => handle_echo(req),
20 _ => handle_not_found(),
21 }
22 }));
23
24 let server = Http::new().bind(addr, echo_service)?;
25 server.run()
26 }
函数处理路由:
28 type ResponseResult = Result<Response, hyper::Error>;
29 fn handle_root() -> ResponseResult {
30 const MSG: &str = "Try doing a POST at /echo";
31 Ok(Response::new()
32 .with_header(ContentType::plaintext())
33 .with_header(ContentLength(MSG.len() as u64))
34 .with_body(MSG))
35 }
36
37 fn handle_echo(req: Request) -> ResponseResult {
38 // The echoing is implemented by setting the response's
39 // body to the request's body
40 Ok(Response::new().with_body(req.body()))
41 }
42
43 fn handle_not_found() -> ResponseResult {
44 // Return a 404 for every unsupported route
45 Ok(Response::new().with_status(StatusCode::NotFound))
46 }
它是如何工作的...
这个菜谱与上一个菜谱类似,所以让我们直接跳到 Service 的定义[14 到 22]:
|req: Request| {
match (req.method(), req.path()) {
(&Method::Get, "/") => handle_root(),
(&Method::Post, "/echo") => handle_echo(req),
_ => handle_not_found(),
}
}
我们现在使用的是 Request 参数,而上一个菜谱只是简单地忽略了它。
因为 Rust 允许我们在元组上使用模式匹配,所以我们可以直接区分 HTTP 方法与路径组合。然后我们将程序的控制流传递给专门的路线处理程序,它们反过来负责返回响应。
在具有大量路由的大程序中,我们不会在一个函数中指定所有这些,而是将它们分散在命名空间中,并将它们分成子路由器。
handle_root [29] 的代码看起来几乎与上一章的 hello world Service 相同,但指示调用者向 /post 路由发送 POST 请求。
对于所说的 POST,我们的匹配会导致 handle_echo [37],它简单地返回请求体作为响应体 [40]。你可以通过在 准备就绪 部分描述的方式将消息 POST 到 http://localhost:3000/echo 来尝试它。如果一切顺利,你的消息会直接返回给你。
最后,但同样重要的是,当没有路由匹配时,会调用 handle_not_found [43]。这次,我们不发送消息,而是返回世界上可能最著名的状态码:404 Not Found [45]。
配置 HTTP 服务器以执行文件服务
上一系列的菜谱对于构建网络服务非常有用,但让我们看看如何做 HTTP 最初创建的事情:向网络提供 HTML 文件。
如何做到这一点...
-
打开为你生成的
Cargo.toml文件。 -
在
[dependencies]下,如果你在上一个菜谱中没有这样做,请添加以下行:
futures = "0.1.18"
hyper = "0.11.21"
-
如果你想的话,你可以访问 futures' (
crates.io/crates/futures) 和 hyper's (crates.io/crates/hyper) 的 crates.io 页面,检查最新版本并使用它。 -
在
chapter-nine文件夹中创建一个名为files的文件夹。 -
在
files文件夹中创建一个名为index.html的文件,并将以下代码添加到其中:
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="/style.css">
<title>Home</title>
</head>
<body>
<h1>Home</h1>
<p>Welcome. You can access other files on this web server
aswell! Available links:</p>
<ul>
<li>
<a href="/foo.html">Foo!</a>
</li>
<li>
<a href="/bar.html">Bar!</a>
</li>
</ul>
</body>
</html>
- 在
files文件夹中创建一个名为foo.html的文件,并将以下代码添加到其中:
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="/style.css">
<title>Foo</title>
</head>
<body>
<p>Foo!</p>
</body>
</html>
- 在
files文件夹中创建一个名为bar.html的文件,并将以下代码添加到其中:
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="/style.css">
<title>Bar</title>
</head>
<body>
<p>Bar!</p>
</body>
</html>
- 在
files文件夹中创建一个名为not_found.html的文件,并将以下代码添加到其中:
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="/style.css">
<title>Page Not Found</title>
</head>
<body>
<h1>Page Not Found</h1>
<p>We're sorry, we couldn't find the page you requested.</p>
<p>Maybe it was renamed or moved?</p>
<p>Try searching at the
<a href="/index.html">start page</a>
</p>
</body>
</html>
- 在
files文件夹中创建一个名为invalid_method.html的文件,并将以下代码添加到其中:
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="/style.css">
<title>Error 405 (Method Not Allowed)</title>
</head>
<body>
<h1>Error 405</h1>
<p>The method used is not allowed for this URL</p>
</body>
</html>
-
在
src/bin文件夹中创建一个名为file_server.rs的文件。 -
添加以下代码并使用
cargo run --bin echo_server_with_routing运行它:
1 extern crate futures;
2 extern crate hyper;
3
4 use hyper::{Method, StatusCode};
5 use hyper::server::{const_service, service_fn, Http, Request,
Response};
6 use hyper::header::{ContentLength, ContentType};
7 use hyper::mime;
8 use futures::Future;
9 use futures::sync::oneshot;
10 use std::net::SocketAddr;
11 use std::thread;
12 use std::fs::File;
13 use std::io::{self, copy};
14
15 fn main() {
16 let addr = "[::1]:3000".parse().expect("Failed to parse
address");
17 run_file_server(&addr).expect("Failed to run web server");
18 }
19
20 fn run_file_server(addr: &SocketAddr) -> Result<(),
hyper::Error> {
21 let file_service = const_service(service_fn(|req: Request| {
22 // Setting up our routes
23 match (req.method(), req.path()) {
24 (&Method::Get, "/") => handle_root(),
25 (&Method::Get, path) => handle_get_file(path),
26 _ => handle_invalid_method(),
27 }
28 }));
29
30 let server = Http::new().bind(addr, file_service)?;
31 server.run()
32 }
以下是一些路由处理程序:
34 // Because we don't want the entire server to block when serving
a file,
35 // we are going to return a response wrapped in a future
36 type ResponseFuture = Box<Future>;
37 fn handle_root() -> ResponseFuture {
38 // Send the landing page
39 send_file_or_404("index.html")
40 }
41
42 fn handle_get_file(file: &str) -> ResponseFuture {
43 // Send whatever page was requested or fall back to a 404 page
44 send_file_or_404(file)
45 }
46
47 fn handle_invalid_method() -> ResponseFuture {
48 // Send a page telling the user that the method he used is not
supported
49 let response_future = send_file_or_404("invalid_method.html")
50 // Set the correct status code
51 .and_then(|response|
Ok(response.with_status(StatusCode::MethodNotAllowed)));
52 Box::new(response_future)
53 }
以下是为返回我们的文件的函数编写的代码:
55 // Send a future containing a response with the requested file
or a 404 page
56 fn send_file_or_404(path: &str) -> ResponseFuture {
57 // Sanitize the input to prevent unwanted data access
58 let path = sanitize_path(path);
59
60 let response_future = try_to_send_file(&path)
61 // try_to_send_file returns a future of Result<Response,
io::Error>
62 // turn it into a future of a future of Response with an
error of hyper::Error
63 .and_then(|response_result| response_result.map_err(|error|
error.into()))
64 // If something went wrong, send the 404 page instead
65 .or_else(|_| send_404());
66 Box::new(response_future)
67 }
68
69 // Return a requested file in a future of Result<Response,
io::Error>
70 // to indicate whether it exists or not
71 type ResponseResultFuture = Box<Future, Error = hyper::Error>>;
72 fn try_to_send_file(file: &str) -> ResponseResultFuture {
73 // Prepend "files/" to the file
74 let path = path_on_disk(file);
75 // Load the file in a separate thread into memory.
76 // As soon as it's done, send it back through a channel
77 let (tx, rx) = oneshot::channel();
78 thread::spawn(move || {
79 let mut file = match File::open(&path) {
80 Ok(file) => file,
81 Err(err) => {
82 println!("Failed to find file: {}", path);
83 // Send error through channel
84 tx.send(Err(err)).expect("Send error on file not
found");
85 return;
86 }
87 };
88
89 // buf is our in-memory representation of the file
90 let mut buf: Vec = Vec::new();
91 match copy(&mut file, &mut buf) {
92 Ok(_) => {
93 println!("Sending file: {}", path);
94 // Detect the content type by checking the file
extension
95 // or fall back to plaintext
96 let content_type =
get_content_type(&path).unwrap_or_else
(ContentType::plaintext);
97 let res = Response::new()
98 .with_header(ContentLength(buf.len() as u64))
99 .with_header(content_type)
100 .with_body(buf);
101 // Send file through channel
102 tx.send(Ok(res))
103 .expect("Send error on successful file read");
104 }
105 Err(err) => {
106 // Send error through channel
107 tx.send(Err(err)).expect("Send error on error reading
file");
108 }
109 };
110 });
111 // Convert all encountered errors to hyper::Error
112 Box::new(rx.map_err(|error|
io::Error::new(io::ErrorKind::Other,
error).into()))
113 }
114
115 fn send_404() -> ResponseFuture {
116 // Try to send our 404 page
117 let response_future =
try_to_send_file("not_found.html").and_then(|response_result|
{
118 Ok(response_result.unwrap_or_else(|_| {
119 // If the 404 page doesn't exist, sent fallback text
instead
120 const ERROR_MSG: &str = "Failed to find \"File not found\"
page. How ironic\n";
121 Response::new()
122 .with_status(StatusCode::NotFound)
123 .with_header(ContentLength(ERROR_MSG.len() as u64))
124 .with_body(ERROR_MSG)
125 }))
126 });
127 Box::new(response_future)
128 }
以下是一些辅助函数:
130 fn sanitize_path(path: &str) -> String {
131 // Normalize the separators for the next steps
132 path.replace("\\", "/")
133 // Prevent the user from going up the filesystem
134 .replace("../", "")
135 // If the path comes straigh from the router,
136 // it will begin with a slash
137 .trim_left_matches(|c| c == '/')
138 // Remove slashes at the end as we only serve files
139 .trim_right_matches(|c| c == '/')
140 .to_string()
141 }
142
143 fn path_on_disk(path_to_file: &str) -> String {
144 "files/".to_string() + path_to_file
145 }
146
147 fn get_content_type(file: &str) -> Option {
148 // Check the file extension and return the respective MIME type
149 let pos = file.rfind('.')? + 1;
150 let mime_type = match &file[pos..] {
151 "txt" => mime::TEXT_PLAIN_UTF_8,
152 "html" => mime::TEXT_HTML_UTF_8,
153 "css" => mime::TEXT_CSS,
154 // This list can be extended for all types your server
should support
155 _ => return None,
156 };
157 Some(ContentType(mime_type))
158 }
它是如何工作的...
哇!文件太多了。当然,对于这个菜谱来说,HTML 和 CSS 的确切内容并不重要,因为我们将要专注于 Rust。我们已经把它们都放在了 files 文件夹中,因为我们打算通过名称使任何客户端都可以公开访问其内容。
服务器设置的基本原理与回声食谱相同:使用 const_service 和 service_fn [21] 创建一个 Service,匹配请求的方法和路径,然后在不同的函数中处理路由。然而,当我们查看返回类型时,我们可以注意到一个差异 [36]:
type ResponseFuture = Box<Future<Item = Response, Error = hyper::Error>>;
我们不再直接返回一个 Response,而是将其封装在一个 Future 中。这允许我们在将文件加载到内存时不会阻塞服务器;我们可以在主线程中继续处理请求,同时文件服务的 Future 在后台运行。
当查看我们的路由处理程序时,你可以看到它们都使用了 send_file_or_404 函数。让我们看看它 [56]:
fn send_file_or_404(path: &str) -> ResponseFuture {
let path = sanitize_path(path);
let response_future = try_to_send_file(&path)
.and_then(|response_result| response_result.map_err(|error|
error.into()))
.or_else(|_| send_404());
Box::new(response_future)
}
首先,该函数清理我们的输入。sanitize_path [130 到 141] 的实现应该是相当直接的。它过滤掉潜在的麻烦制造者,这样恶意客户端就不能进行任何恶作剧,例如请求文件 localhost:3000/../../../../home/admin/.ssh/id_rsa。
然后我们在清理后的路径上调用 try_to_send_file [72]。我们将在下一分钟查看该函数,但就现在而言,查看其签名就足够了。它告诉我们它返回一个 Result 的 Future,这个 Result 可以是一个 Response 或一个 io::Error,因为这是在无效文件系统访问中遇到的错误。我们不能直接返回这个 Future,因为我们已经告诉 hyper 我们将返回一个 Response 的 Future,所以我们需要转换类型。如果从 try_to_send_file 生成的文件检索 Future 成功,我们就对其项目进行操作,该项目是一个 Result<Response, io::Error>。
因为 hyper::Error 实现了 From<io::Error>,我们可以通过调用 .into() [63](参见 第五章,高级数据结构;类型转换,了解 From 特质的介绍)。这将返回一个 Result<Response, hyper::Error>。因为 Future 可以从 Result 构造,所以它将隐式地转换为 Future<Response, hyper::Error>,这正是我们想要的。额外的一点是,我们处理 try_to_send_file 返回错误的情况,在这种情况下,我们可以安全地假设文件不存在,因此我们通过调用 send_404() [65] 返回一个自定义的 404 Not Found 页面。在查看其实现之前,我们先看看 try_to_send_file [72]。
首先,我们使用 path_on_disk [74] 将请求的路径转换为本地文件系统路径,其实现如下 [144]:
"files/".to_string() + path_to_file
我们为此创建了一个自定义函数,这样你就可以轻松扩展文件系统逻辑。例如,对于 Unix 系统,通常会将所有静态 HTML 放在 /var/www/,而 Windows 网络服务器通常将它们的所有数据放在它们自己的安装文件夹中。或者你可能想读取用户提供的配置文件,并将其值存储在 lazy_static 中,如 第五章,高级数据结构;使用懒静态变量,并使用该路径。你可以在该函数中实现所有这些规则。
在 try_to_send_file 函数中,我们创建了一个 oneshot::channel 来以 Future 的形式发送数据 [77]。这个概念在 第八章,使用 Future;使用 oneshot 通道 中有详细的解释。
函数的其余部分现在创建了一个新线程,在后台将文件加载到内存中 [78]。我们首先打开文件 [79],如果文件不存在,则通过通道返回错误。然后我们将整个文件复制到一个本地的字节数组中 [91],并再次传播可能发生的任何错误 [107]。如果将数据复制到 RAM 的过程成功,我们返回一个包含文件内容的 Response [100]。在这个过程中,我们必须确定文件的适当 MIME 类型 [96],正如在 设置基本 HTTP 服务器 的配方中所承诺的。为此,我们只需匹配文件的扩展名 [147 到 158]。
fn get_content_type(file: &str) -> Option<ContentType> {
let pos = file.rfind('.')? + 1;
let mime_type = match &file[pos..] {
"txt" => mime::TEXT_PLAIN_UTF_8,
"html" => mime::TEXT_HTML_UTF_8,
"css" => mime::TEXT_CSS,
_ => return None,
};
Some(ContentType(mime_type))
}
你可能会认为这种实现相当懒惰,应该有更好的方法,但请相信我,这正是所有大型网络服务器所做的方式。以 nginx 为例,(nginx.org/en/),你可以在这里找到 mime 检测算法:github.com/nginx/nginx/blob/master/conf/mime.types。如果你计划提供新的文件类型,你可以扩展它们的扩展名的 match。nginx 源代码是这方面的良好资源。
get_content_type 如果没有匹配项 [155] 则返回 None 而不是默认内容类型,这样每个调用者都可以为自己决定一个默认值。在 try_to_send_file 中,我们使用 .unwrap_or_else(ContentType::plaintext); [96] 将回退 MIME 类型设置为 text/plain。
在我们的示例中,最后剩下的未解释的函数是 send_404,我们经常将其用作回退。你可以看到它实际上只是对 404 页面 [117] 调用 try_to_send_file,并在出错时发送一个静态消息 [124]。
在 send_404 中的回退实际上展示了 Rust 错误处理概念的美丽。因为强类型错误是函数签名的一部分,与像 C++ 这样的语言不同,在 C++ 中你永远不知道谁可能会抛出异常,你必须有意识地处理错误情况。尝试移除 and_then 及其关联的闭包,你就会看到编译器不会让你编译程序,因为你没有以任何方式处理 try_to_send_file 的 Result。
现在就打开您的浏览器,通过指向http://localhost:3000/来亲眼看看我们的文件服务器的结果。
还有更多...
尽管相对容易理解,但我们的try_to_send_file实现并不是无限可扩展的。想象一下同时为成百万的客户端提供和加载大量文件到内存中。这会很快让你的 RAM 达到极限。一个更可扩展的解决方案是分块发送文件,即部分一部分,这样你只需要在任何给定时间保持文件的一小部分在内存中。要实现这一点,你需要将文件内容复制到一个固定大小的有限[u8]缓冲区中,并通过一个额外的通道作为hyper::Chunk的实例发送,它实现了From<Vec<T>>。
参见
-
将类型相互转换和创建 懒静态变量的配方在第五章,高级数据结构中。
-
使用第八章中的 oneshot 通道配方第八章,与 Futures 一起工作。
向 API 发送请求
本章的最后一个目的地将我们带离服务器,转向参与互联网通信的另一方:客户端。我们将使用reqwest,它是围绕hyper构建的,来创建对 Web 服务的 HTTPS 请求并将它们的数据解析成可用的 Rust 结构。您也可以使用这个配方的内容来编写您自己的 Web 服务的集成测试。
如何做到...
-
打开为您生成的
Cargo.toml文件。 -
在
[dependencies]下,如果您在上一个配方中没有这样做,请添加以下行:
reqwest = "0.8.5"
serde = "1.0.30"
serde_derive = "1.0.30"
-
如果您愿意,您可以去
request的(crates.io/crates/reqwest),serde的(crates.io/crates/serde),和serde_derive的(crates.io/crates/serde_derive) crates.io页面检查最新版本,并使用这些版本代替。 -
在
src/bin文件夹中,创建一个名为making_requests.rs的文件。 -
添加以下代码,并使用
cargo run --bin making_requests运行它:
1 extern crate reqwest;
2 #[macro_use]
3 extern crate serde_derive;
4
5 use std::fmt;
6
7 #[derive(Serialize, Deserialize, Debug)]
8 // The JSON returned by the web service that hands posts out
9 // it written in camelCase, so we need to tell serde about that
10 #[serde(rename_all = "camelCase")]
11 struct Post {
12 user_id: u32,
13 id: u32,
14 title: String,
15 body: String,
16 }
17
18 #[derive(Serialize, Deserialize, Debug)]
19 #[serde(rename_all = "camelCase")]
20 struct NewPost {
21 user_id: u32,
22 title: String,
23 body: String,
24 }
25
26 #[derive(Serialize, Deserialize, Debug)]
27 #[serde(rename_all = "camelCase")]
28 // The following struct could be rewritten with a builder
29 struct UpdatedPost {
30 #[serde(skip_serializing_if = "Option::is_none")]
31 user_id: Option,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 title: Option,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 body: Option,
36 }
37
38 struct PostCrud {
39 client: reqwest::Client,
40 endpoint: String,
41 }
42
43 impl fmt::Display for Post {
44 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
45 write!(
46 f,
47 "User ID: {}\nID: {}\nTitle: {}\nBody: {}\n",
48 self.user_id, self.id, self.title, self.body
49 )
50 }
51 }
以下代码展示了正在实现的请求:
53 impl PostCrud {
54 fn new() -> Self {
55 PostCrud {
56 // Build an HTTP client. It's reusable!
57 client: reqwest::Client::new(),
58 // This is a link to a fake REST API service
59 endpoint:
"https://jsonplaceholder.typicode.com/posts".to_string(),
60 }
61 }
62
63 fn create(&self, post: &NewPost) -> Result<Post,
reqwest::Error> {
64 let response =
self.client.post(&self.endpoint).json(post).send()?.json()?;
65 Ok(response)
66 }
67
68 fn read(&self, id: u32) -> Result<Post, reqwest::Error> {
69 let url = format!("{}/{}", self.endpoint, id);
70 let response = self.client.get(&url).send()?.json()?;
71 Ok(response)
72 }
73
74 fn update(&self, id: u32, post: &UpdatedPost) -> Result<Post,
reqwest::Error> {
75 let url = format!("{}/{}", self.endpoint, id);
76 let response =
self.client.patch(&url).json(post).send()?.json()?;
77 Ok(response)
78 }
79
80 fn delete(&self, id: u32) -> Result<(), reqwest::Error> {
81 let url = format!("{}/{}", self.endpoint, id);
82 self.client.delete(&url).send()?;
83 Ok(())
84 }
85 }
以下代码展示了我们如何使用我们的 CRUD 客户端:
87 fn main() {
88 let post_crud = PostCrud::new();
89 let post = post_crud.read(1).expect("Failed to read post");
90 println!("Read a post:\n{}", post);
91
92 let new_post = NewPost {
93 user_id: 2,
94 title: "Hello World!".to_string(),
95 body: "This is a new post, sent to a fake JSON API
server.\n".to_string(),
96 };
97 let post = post_crud.create(&new_post).expect("Failed to
create post");
98 println!("Created a post:\n{}", post);
99
100 let updated_post = UpdatedPost {
101 user_id: None,
102 title: Some("New title".to_string()),
103 body: None,
104 };
105 let post = post_crud
106 .update(4, &updated_post)
107 .expect("Failed to update post");
108 println!("Updated a post:\n{}", post);
109
110 post_crud.delete(51).expect("Failed to delete post");
111 }
它是如何工作的...
在顶部,我们定义了我们的结构。Post[11],NewPost [20],和UpdatedPost [29]都只是处理 API 不同需求的便捷方式。我们正在与之交互的特定 JSON API 使用 camelCase 变量,因此我们需要在每一个struct上指定这一点,否则serde将无法正确解析它们[10,19 和 27]。
因为与我们通信的PATCH方法不接受未更改变量的 null 值,所以我们把所有在UpdatedPost中等于None的标记为未序列化[30,32 和 34]:
#[serde(skip_serializing_if = "Option::is_none")]
此外,我们在Post上实现了fmt::Display特性,这样我们就可以很好地打印它了[43 到 51]。
但关于我们的模型就先说到这里;让我们来看看PostCrud [53]。它的目的是抽象化一个 CRUD(创建、读取、更新、删除)服务。为此,它通过reqwest::Client [57]提供了一个可重用的 HTTP 客户端,以及来自jsonplaceholder.typicode.com/的模拟 JSON API 端点。
它的方法展示了reqwest的使用是多么简单:你只需直接将所需的 HTTP 方法作为客户端上的一个函数使用,向它传递可选的数据,它将自动使用.json()为你反序列化,然后使用.send()发送请求,并通过第二次调用.json()再次将响应解析为 JSON [64]。
还有更多...
当然,reqwest也能够与非基于 JSON 的网络服务一起工作。为此它有多种方法,例如query,它将键值查询数组添加到 URL 中,或者form,它将url-encoded表单体添加到请求中。在使用所有这些方法时,reqwest会为你管理头部信息,但你也可以使用headers方法以任何你想要的方式显式地管理它们。
参见
-
第一章中的“使用构建器模式”配方,学习基础知识
-
第四章中的“使用 Serde 的序列化基础知识”配方,序列化
第十章:使用实验性 Nightly 功能
在本章中,我们将介绍以下食谱:
-
遍历一个包含范围的迭代
-
返回抽象类型
-
函数组合
-
高效地过滤字符串
-
以常规间隔遍历迭代器
-
对你的代码进行基准测试
-
使用生成器
简介
这最后一章将带我们了解 Rust 中最重要的实验性功能,这些功能在最新的nightly工具链上提供。截至编写时,这是rustc 1.25.0-nightly。如果你使用rustup(rustup.rs/),你可以像这样将其设置为默认工具链:
rustup default nightly
这些食谱将确保你在 Rust 的知识上保持领先,一旦它们稳定下来,或者现在在你的不稳定应用中,你就可以有效地使用它们。
本章中的所有食谱都有不同的稳定性保证。许多在进入stable工具链之前将经历巨大的变化,而其他的一些几乎已经完成。这意味着提供的某些示例代码可能无法在你的最新nightly版本上运行。当这种情况发生时,你可以在两个地方找到帮助:
-
如果该特性仍然是实验性的,它将在《不稳定手册》(
doc.rust-lang.org/unstable-book/)中有一个条目,以及一个链接到相关的 GitHub 问题和其周围的讨论 -
如果该特性已经稳定,你很可能会在《Rust 编程语言,第二版》(
doc.rust-lang.org/stable/book/second-edition/appendix-06-newest-features.html)的附录中找到它)
遍历一个包含范围的迭代
我们以一个小特性开始本章,这个特性可以使你的代码更易于阅读。包含范围的语法(..=)将创建一个包含该值的范围。这有助于你通过将它们重写为n ..= m来消除像n .. m+1这样的丑陋实例。
如何做到这一点...
-
使用
cargo new chapter-ten创建一个 Rust 项目,在本章中工作。 -
导航到新创建的
chapter-ten文件夹。在本章的其余部分,我们将假设你的命令行当前位于这个目录中。 -
删除生成的
lib.rs文件,因为我们不是在创建一个库。 -
在
src文件夹中,创建一个新的名为bin的文件夹。 -
在
src/bin文件夹中,创建一个名为inclusive_range.rs的文件。 -
添加以下代码,并使用
cargo run --bin inclusive_range运行它:
1 #![feature(inclusive_range_syntax)]
2
3 fn main() {
4 // Retrieve the entire alphabet in lower and uppercase:
5 let alphabet: Vec<_> = (b'A' .. b'z' + 1) // Start as u8
6 .map(|c| c as char) // Convert all to chars
7 .filter(|c| c.is_alphabetic()) // Filter only alphabetic
chars
8 .collect(); // Collect as Vec
9 println!("alphabet: {:?}", alphabet);
10
11 // Do the same, but using the inclusive range syntax:
12 let alphabet: Vec<_> = (b'A' ..= b'z') // Start as u8
13 .map(|c| c as char) // Convert all to chars
14 .filter(|c| c.is_alphabetic()) // Filter only alphabetic
chars
15 .collect(); // Collect as Vec
16 println!("alphabet: {:?}", alphabet);
17 }
它是如何工作的...
在这个例子中,代码是第二章中提到的片段的预期重写;与集合一起工作;将集合作为迭代器访问。
传统的范围语法(n .. m)是排他的,意味着 0 .. 5 只包括数字 0、1、2、3 和 4。这对于需要计算某物长度的用例来说很好,但在这个案例中,我们想要遍历字母表,包括 Z [5]。包含范围的语法(n ..= m)通过包含最后一个元素来帮助我们,所以 0 ..= 5 将产生 0、1、2、3、4 和 5。
参见
- 第二章 与集合一起工作 中的 将集合作为迭代器访问 配方
返回抽象类型
记得我们使用 Box 创建特质对象以隐藏返回的确切实现,并只提供关于实现特质的保证吗?这需要我们接受一些开销,因为 Box 在堆上分配其资源;然而,在当前的 nightly 中,情况不同。你可以使用本配方中引入的 impl trait 语法,将对象直接作为它们的特质返回到栈上,而无需使用 Box。目前,这仅适用于返回类型,但该语法计划扩展到大多数可以写入具体类型的地方。
如何做到这一点...
-
打开之前为你生成的
Cargo.toml文件。 -
在
bin文件夹中,创建一个名为return_abstract.rs的文件。 -
添加以下代码,并使用
cargo run --bin return_abstract运行它:
1 #![feature(conservative_impl_trait)]
2
3 trait Animal {
4 fn do_sound(&self);
5 }
6
7 struct Dog;
8 impl Animal for Dog {
9 fn do_sound(&self) {
10 println!("Woof");
11 }
12 }
13
14 fn main() {
15 // The caller doesn't know which exact object he gets
16 // He knows only that it implements the Animal trait
17 let animal = create_animal();
18 animal.do_sound();
19
20 for word in caps_words_iter("do you feel lucky, punk‽") {
21 println!("{}", word);
22 }
23
24 let multiplier = create_multiplier(23);
25 let result = multiplier(3);
26 println!("23 * 3 = {}", result);
27 }
28
29 // The impl trait syntax allows us to use abstract return types
30 // This means that we don't specify which exact struct we return
31 // but which trait(s) it implements
32 fn create_animal() -> impl Animal {
33 Dog {}
34 }
35
36 // Any iterator can be returned as an abstract return type
37 fn caps_words_iter<'a>(text: &'a str) -> impl Iterator + 'a {
38 // Return an iterator over every word converted into ALL_CAPS
39 text.trim().split(' ').map(|word| word.to_uppercase())
40 }
41
42 // Same goes for closures
43 fn create_multiplier(a: i32) -> impl Fn(i32) -> i32 {
44 move |b| a * b
45 }
它是如何工作的...
如果你已经阅读了第五章,高级数据结构;数据装箱,这个配方不需要太多解释。通过返回一个 impl trait,我们告诉函数的调用者不必关心返回的具体结构体,它对其的唯一保证是它实现了某些特质。从这个意义上说,抽象返回类型就像在所述配方中讨论的特质对象一样工作,而且还有一个额外的优点,那就是它们要快得多,因为它们没有任何开销。这对于返回迭代器 [37] 和闭包 [43] 很有用,这些是从关于装箱的配方中改编的,但也可以用来隐藏实现细节。考虑我们的函数 create_animal[32]。调用者只关心它返回一个实现了 Animal 的结构体,但不关心确切的动物。如果由于需求变化,Dog [7] 证明不是正确的东西,你可以创建一个 Cat,并返回它,而无需触及代码的其他部分,因为所有这些都依赖于 Animal。这是一种 依赖倒置 (en.wikipedia.org/wiki/Dependency_inversion_principle) 的形式。
conservative 在 conservative_impl_trait [1] 中告诉我们,这只是一个更大功能的组成部分。目前,你只能将其用于函数的返回类型。将来,你将能够在特质、约束和绑定中使用它。
还有更多...
在本章的所有食谱中,这个可能是最稳定的,因为它正在被考虑立即稳定化。讨论可以在github.com/rust-lang/rust/issues/34511.找到。
虽然你可以为某些形式的依赖反转使用抽象返回类型,但你不能为根据参数返回不同对象的传统 Java 风格工厂使用它们。这是因为抽象返回类型仅隐藏函数外部返回的特定结构,但仍然在内部依赖于特定的返回值。正因为如此,以下代码将无法编译:
trait Animal {
fn do_sound(&self);
}
struct Dog;
impl Animal for Dog {
fn do_sound(&self) {
println!("Woof");
}
}
struct Cat;
impl Animal for Cat {
fn do_sound(&self) {
println!("Meow");
}
}
enum AnimalType {
Dog,
Cat,
}
fn create_animal(animal_type: AnimalType) -> impl Animal {
match animal_type {
AnimalType::Cat => Cat {},
AnimalType::Dog => Dog {},
}
}
虽然外界不知道create_animal将返回哪种动物,但函数本身在内部需要特定的返回类型。因为我们的匹配中第一个可能的返回值是Cat的一个实例,create_animal假设我们将不会返回其他类型。我们在下一行通过返回一个Dog来打破这种预期,因此编译器会失败:

如果我们想让这个工厂编译,我们需要再次求助于Box:
fn create_animal(animal_type: AnimalType) -> Box<Animal> {
match animal_type {
AnimalType::Cat => Box::new(Cat {}),
AnimalType::Dog => Box::new(Dog {}),
}
}
顺便说一下,这正是 Java 在底层所做的事情。
对于大多数用途,你不需要像这里展示的这样的工厂。使用泛型和特质界限被认为更符合惯例。
参见
- 第五章,高级数据结构中的装箱数据食谱
函数组合
因为我们现在已经学会了如何无开销地返回任意闭包,我们可以将此与接受任何数量参数的宏(第一章,学习基础知识;接受可变数量的参数)结合起来,创建一种易于链式操作的方法,就像你在函数式语言如Haskell中习惯的那样。
如何做到它...
-
打开之前为你生成的
Cargo.toml文件。 -
在
bin文件夹中,创建一个名为compose_functions.rs的文件。 -
添加以下代码,并使用
cargo run --bin compose_functions运行它:
1 #![feature(conservative_impl_trait)]
2
3 // The compose! macro takes a variadic amount of closures and
returns
4 // a closure that applies them all one after another
5 macro_rules! compose {
6 ( $last:expr ) => { $last };
7 ( $head:expr, $ ($tail:expr), +) => {
8 compose_two($head, compose!($ ($tail), +))
9 };
10 }
11
12 // compose_two is a helper function used to
13 // compose only two closures into one
14 fn compose_two<FunOne, FunTwo, Input, Intermediate, Output>(
15 fun_one: FunOne,
16 fun_two: FunTwo,
17 ) -> impl Fn(Input) -> Output
18 where
19 FunOne: Fn(Input) -> Intermediate,
20 FunTwo: Fn(Intermediate) -> Output,
21 {
22 move |x| fun_two(fun_one(x))
23 }
24
25 fn main() {
26 let add = |x| x + 2.0;
27 let multiply = |x| x * 3.0;
28 let divide = |x| x / 4.0;
29 // itermediate(x) returns ((x + 2) * 3) / 4
30 let intermediate = compose!(add, multiply, divide);
31
32 let subtract = |x| x - 5.0;
33 // finally(x) returns (((x + 2) * 3) / 4) - 5
34 let finally = compose!(intermediate, subtract);
35
36 println!("(((10 + 2) * 3) / 4) - 5 is: {}", finally(10.0));
37 }
它是如何工作的...
通过在主函数中使用compose!([30 和 34]),你应该清楚地看到它做了什么:它接受你想要的任意数量的闭包,并将它们组合成一个新闭包,依次运行它们。这对于运行时用户驱动的功能组合非常有用。
宏的实现方式与第一章,学习基础知识;接受可变数量参数的标准宏类似;其边缘情况是一个单独的闭包[6]。当遇到更多时,它将通过递归遍历它们,并通过调用辅助函数compose_two [14]将它们成对组合。通常,类型参数以单个字符书写,但在这个配方中,我们为了可读性原因使用完整的单词,因为涉及到的类型有很多。使用的类型约束应该很好地说明了如何使用这些类型 [18 到 20]:
where
FunOne: Fn(Input) -> Intermediate,
FunTwo: Fn(Intermediate) -> Output,
FunOne是一个闭包,它接受一个Input,将其转换为Intermediate,并将其传递给FunTwo,后者返回一个Output。正如你所看到的实现,我们唯一做的事情就是在值上调用fun_one,然后在其返回值上调用fun_two [22]:
move |x| fun_two(fun_one(x))
参见
- 接受可变数量参数的配方在第一章,学习基础知识
高效地过滤字符串
虽然你可以在稳定频道上从String中过滤字符,但这需要创建一个新的包含过滤字符的String。在nightly上,你可以直接在同一个String上执行此操作,如果你需要多次或对非常大的字符串执行此类操作,这将大大提高你的性能。
如何操作...
-
打开之前为你生成的
Cargo.toml文件。 -
在
bin文件夹中,创建一个名为retain_string.rs的文件。 -
添加以下代码,并使用
cargo run --bin retain_string运行它:
1 #![feature(string_retain)]
2
3 fn main() {
4 let mut some_text = "H_el_l__o_ ___Wo_r__l_d_".to_string();
5 println!("Original text: {}", some_text);
6 // retain() removes all chars that don't fulfill a
7 // predicate in place, making it very efficient
8 some_text.retain(|c| c != '_');
9 println!("Text without underscores: {}", some_text);
10 some_text.retain(char::is_lowercase);
11 println!("Text with only lowercase letters: {}", some_text);
12
13 // Before retain, you had to filter the string as an iterator
over chars
14 // This will however create a new String, generating overhead
15 let filtered: String = "H_el_l__o_ ___Wo_r__l_d_"
16 .chars()
17 .filter(|c| *c != '_')
18 .collect();
19 println!("Text filtered by an iterator: {}", filtered);
20 }
它是如何工作的...
在第二章,与集合一起工作;使用向量中,我们学习了Vec::retain,它可以在原地过滤向量。在nightly工具链上,这个功能已经出现在String中,并且以相同的方式工作,就像String是一个Vec<char>——如果你这么想的话,它确实如此。
过滤String的功能始终存在,但它需要遍历字符串作为一个Iterator,并创建一个新的包含过滤字符的String;或者更糟糕的是,将String转换为一个新的Vec<char>,使用retain操作它,然后将字符转换回另一个新创建的String。
参见
- 使用向量的配方在第二章,与集合一起工作
以常规间隔遍历迭代器
你是否曾想通过仅查看每第 n 个项来遍历数据?在稳定的 Rust 中,解决这个问题的最佳方案是使用第三方 crate itertools (crates.io/crates/itertools),它为你带来一大堆迭代器好东西,或者允许你自己编写这个功能;然而,你有一个内置的step_by方法,它正好能做这件事。
如何操作...
-
打开之前为您生成的
Cargo.toml文件。 -
在
bin文件夹中,创建一个名为iterator_step_by.rs的文件。 -
添加以下代码,并使用
cargo run --bin iterator_step_by运行它:
1 #![feature(iterator_step_by)]
2
3 fn main() {
4 // step_by() will start on the first element of an iterator,
5 // but then skips a certain number of elements on every
iteration
6 let even_numbers: Vec<_> = (0..100).step_by(2).collect();
7 println!("The first one hundred even numbers: {:?}",
even_numbers);
8
9 // step_by() will always start at the beginning.
10 // If you need to skip the first few elements as well, use
skip()
11 let some_data = ["Andrei", "Romania", "Giuseppe", "Italy",
"Susan", "Britain"];
12 let countries: Vec<_> =
some_data.iter().skip(1).step_by(2).collect();
13 println!("Countries in the data: {:?}", countries);
14
15 let grouped_stream = "Aaron 182cm 70kg Alice 160cm 90kg Bob
197cm 83kg";
16 let weights: Vec<_> = grouped_stream
17 .split_whitespace()
18 .skip(2)
19 .step_by(3)
20 .collect();
21 println!("The weights of the people are: {:?}", weights);
22 }
它是如何工作的...
当你被 handed 一个遵循特定模式的非结构化数据流时,这个配方特别出色。例如,有时一些不使用 JSON 的旧 API,或者你可能想要与之交互的其他程序,会给你一些按位置分组的数据流,就像我们存储在grouped_stream[15]中的数据,它遵循以下模式:
person0 height0 weight0 person1 height1 weight1 person2 height2 weight2
step_by让我们能够非常容易地解析这个结构。它通过给你当前元素,然后在每次迭代中跳过一定数量的元素来实现。在我们的例子中,我们通过使用split_whitespace[17]创建一个遍历每个非空白子字符串的迭代器来解析grouped_stream,然后,因为我们只对权重感兴趣,所以我们跳过前两个元素("Aaron"和"182cm"),这使得我们的迭代器位于"70kg"。然后我们告诉迭代器从现在开始只查看每隔第三个元素,使用step_by(3)[19],结果是我们遍历"70kg"、"90kg"和"83kg"。最后,我们将元素collect到一个向量[20]中。
参见
- 使用字符串和将集合作为迭代器访问配方在第二章,与集合一起工作
基准测试您的代码
Rust 项目为编译器本身开发了一个测试 crate。因为它包括一些相当有用的功能,最重要的是一个基准测试器,所以它可以在夜间构建中作为内置的test crate 访问。因为它随着每个夜间构建一起分发,所以您不需要将其添加到Cargo.toml中即可使用它。
由于test crate 与编译器的紧密耦合,它被标记为不稳定。
如何做...
-
打开之前为您生成的
Cargo.toml文件。 -
在
bin文件夹中,创建一个名为benchmarking.rs的文件。 -
添加以下代码,并使用
cargo bench运行它:
1 #![feature(test)]
2 // The test crate was primarily designed for
3 // the Rust compiler itself, so it has no stability guaranteed
4 extern crate test;
5
6 pub fn slow_fibonacci_recursive(n: u32) -> u32 {
7 match n {
8 0 => 0,
9 1 => 1,
10 _ => slow_fibonacci_recursive(n - 1) +
slow_fibonacci_recursive(n - 2),
11 }
12 }
13
14 pub fn fibonacci_imperative(n: u32) -> u32 {
15 match n {
16 0 => 0,
17 1 => 1,
18 _ => {
19 let mut penultimate;
20 let mut last = 1;
21 let mut fib = 0;
22 for _ in 0..n {
23 penultimate = last;
24 last = fib;
25 fib = penultimate + last;
26 }
27 fib
28 }
29 }
30 }
31
32 pub fn memoized_fibonacci_recursive(n: u32) -> u32 {
33 fn inner(n: u32, penultimate: u32, last: u32) -> u32 {
34 match n {
35 0 => penultimate,
36 1 => last,
37 _ => inner(n - 1, last, penultimate + last),
38 }
39 }
40 inner(n, 0, 1)
41 }
42
43 pub fn fast_fibonacci_recursive(n: u32) -> u32 {
44 fn inner(n: u32, penultimate: u32, last: u32) -> u32 {
45 match n {
46 0 => last,
47 _ => inner(n - 1, last, penultimate + last),
48 }
49 }
50 match n {
51 0 => 0,
52 _ => inner(n - 1, 0, 1),
53 }
54 }
- 运行基准测试:
56 #[cfg(test)]
57 mod tests {
58 use super::*;
59 use test::Bencher;
60
61 // Functions annotated with the bench attribute will
62 // undergo a performance evaluation when running "cargo bench"
63 #[bench]
64 fn bench_slow_fibonacci_recursive(b: &mut Bencher) {
65 b.iter(|| {
66 // test::block_box is "black box" for the compiler and
LLVM
67 // Telling them to not optimize a variable away
68 let n = test::black_box(20);
69 slow_fibonacci_recursive(n)
70 });
71 }
72
73 #[bench]
74 fn bench_fibonacci_imperative(b: &mut Bencher) {
75 b.iter(|| {
76 let n = test::black_box(20);
77 fibonacci_imperative(n)
78 });
79 }
80
81 #[bench]
82 fn bench_memoized_fibonacci_recursive(b: &mut Bencher) {
83 b.iter(|| {
84 let n = test::black_box(20);
85 memoized_fibonacci_recursive(n)
86 });
87 }
88
89 #[bench]
90 fn bench_fast_fibonacci_recursive(b: &mut Bencher) {
91 b.iter(|| {
92 let n = test::black_box(20);
93 fast_fibonacci_recursive(n)
94 });
95 }
96 }
它是如何工作的...
test crate 的精髓是Bencher结构体[59]。当运行cargo bench时,它的一个实例会自动传递给每个带有#[bench]属性的函数[63]。它的iter方法接受一个闭包[65],并多次运行它以确定其一次迭代所需的时间。在这个过程中,它还会丢弃与其他时间测量相差甚远的测量值,以消除一次性极端值。
test crate 的另一个有用部分是其black_box结构体[68],它封装任何值并告诉编译器和 LLVM 不要优化它,无论什么情况。如果我们不在我们的基准测试中使用它,它们可能会被优化掉,导致一个相当乐观且无用的测量结果,即 0 ns/iter,或者每次闭包执行的零纳秒。
我们可以使用我们手中的工具来测试一些理论。记住在第七章中讨论的递归斐波那契实现,并行和射线;同时运行两个操作?嗯,这里重复一下,作为slow_fibonacci_recursive[6]。
这种实现之所以慢,是因为对slow_fibonacci_recursive(n - 1)和slow_fibonacci_recursive(n - 2)的两次调用都需要单独重新计算所有值。更糟糕的是,每次调用还会再次拆分成对slow_fibonacci_recursive(n - 1)和slow_fibonacci_recursive(n - 2)的调用,一次又一次地重新计算!在大 O的术语中,这是效率为
(证明见stackoverflow.com/a/360773/5903309)。相比之下,命令式算法fibonacci_imperative [14]只是一个简单的循环,所以它处于
。根据这个理论,它应该比慢速的递归实现快得多。运行cargo bench可以让我们轻松验证这些断言:

差异多么大!在我的电脑上,慢速的递归实现比命令式实现慢了 7,000 多倍!当然,我们可以做得更好。
StackOverflow用户Boiethios友好地为我们提供了memoized_fibonacci_recursivestackoverflow.com/a/49052806/5903309。正如其名所示,这种实现使用了一种称为记忆化的概念。这意味着算法有某种方式传递已经计算过的值。一种简单的方法是传递一个包含所有计算值的HashMap,但这又会带来自己的开销,因为它在堆上操作。相反,我们选择了累加器的方法。这意味着我们只需直接将相关值作为参数传递,在我们的例子中,这些值是penultimate,它代表n-2的斐波那契数,以及last,它代表n-1的斐波那契数。如果你想了解更多关于这些函数式概念的信息,请查看www.idryman.org/blog/2012/04/14/recursion-best-practices/。
检查基准测试,我们可以看到我们通过memoized_fibonacci_recursive大大提高了算法的效率。但它仍然比fibonacci_imperative慢一点。提高算法的许多可能方法之一是将每次递归调用中都会检查的n == 1匹配提取出来,如fast_fibonacci_recursive [43]所示,它在每次迭代中只需要 3 纳秒!
还有更多...
我们的实现还采用了另一种优化:尾调用优化,或简称 TCO。用简化的术语来说,TCO 发生在编译器能够将递归算法重写为命令式算法时。更普遍地说,TCO 是当编译器能够将递归调用编译成不需要为每个调用添加新的栈帧的形式,因此不会导致栈溢出(不是网站,而是错误)。关于这个话题的深入讨论,请参阅stackoverflow.com/questions/310974/what-is-tail-call-optimization。
尽管 Rust 本身不支持尾调用优化(TCO)(参见github.com/rust-lang/rfcs/issues/271中的 RFC),但底层的 LLVM 支持。它要求函数的最后一个调用必须是它自己的调用。inner 函数的最后一行是对 inner 的调用,因此它符合 TCO 的条件。
然而,在更大的 Rust 算法中,这有点难以保证,因为实现 Drop 特质的对象将在函数末尾注入一个对 drop() 的调用,从而消除了 TCO 的可能性。
参考以下内容
- 在第七章的 并行性和 Rayon 中,同时运行两个操作 的配方
使用生成器
在 Rust 中,目前仍有一些概念尚未完全可用,其中最大的概念之一就是 简单异步。其中一个原因是编译器对某些事物的支持不足,目前正在解决这个问题。异步之路的一个重要部分是 生成器,它们的实现方式类似于在 C#或 Python 中的使用。
如何实现...
-
打开之前为您生成的
Cargo.toml文件。 -
在
bin文件夹中,创建一个名为generator.rs的文件。 -
添加以下代码,并使用
cargo run --bin generator运行它:
1 #![feature(generators, generator_trait,
conservative_impl_trait)]
2
3 fn main() {
4 // A closure that uses the keyword "yield" is called a
generator
5 // Yielding a value "remembers" where you left off
6 // when calling .resume() on the generator
7 let mut generator = || {
8 yield 1;
9 yield 2;
10 };
11 if let GeneratorState::Yielded(value) = generator.resume() {
12 println!("The generator yielded: {}", value);
13 }
14 if let GeneratorState::Yielded(value) = generator.resume() {
15 println!("The generator yielded: {}", value);
16 }
17 // When there is nothing left to yield,
18 // a generator will automatically return an empty tuple
19 if let GeneratorState::Complete(value) = generator.resume() {
20 println!("The generator completed with: {:?}", value);
21 }
22
23 // At the moment, you can return a different type
24 // than you yield, although this feature is considered for
removal
25 let mut generator = || {
26 yield 100;
27 yield 200;
28 yield 300;
29 "I'm a string"
30 };
31 loop {
32 match generator.resume() {
33 GeneratorState::Yielded(value) => println!("The generator
yielded: {}", value),
34 GeneratorState::Complete(value) => {
35 println!("The generator completed with: {}", value);
36 break;
37 }
38 }
39 }
40
41 // Generators are great for implementing iterators.
42 // Eventually, all Rust iterators are going to be rewritten
with generators
43 let fib: Vec<_> = fibonacci().take(10).collect();
44 println!("First 10 numbers of the fibonacci sequence: {:?}",
fib);
45 }
46
47 // As of the time of writing, a generator does not have a
48 // direct conversion to an iterator yet, so we need a wrapper:
49 use std::ops::{Generator, GeneratorState};
50 struct GeneratorIterator(T);
51 impl Iterator for GeneratorIterator
52 where
53 T: Generator,
54 {
55 type Item = T::Yield;
56 fn next(&mut self) -> Option {
57 match self.0.resume() {
58 GeneratorState::Yielded(value) => Some(value),
59 GeneratorState::Complete(_) => None,
60 }
61 }
62 }
63
64 fn fibonacci() -> impl Iterator {
65 // Using our wrapper
66 GeneratorIterator(move || {
67 let mut curr = 0;
68 let mut next = 1;
69 loop {
70 yield curr;
71 let old = curr;
72 curr = next;
73 next += old;
74 }
75 })
76 }
它是如何工作的...
Generator 当前定义为任何使用新 yield 关键字的闭包。当它通过 .resume() [11] 执行时,它将运行到遇到 yield 为止。如果再次运行,生成器将从上次停止的地方继续执行,直到遇到另一个 yield 或遇到 return。如果没有更多的 yield,生成器将简单地返回一个空元组,就像遇到了 return ();。
因为生成器执行时存在两种情况(yield 与 return),所以每次使用它时都必须检查 .resume() 的结果,它可能是 GeneratorState::Yielded 或 GeneratorState::Complete。
在撰写本文时,你可以 return 一个不同于 yield 的类型。关于这一点的情况有些不清楚,因为这种异常的原因是上述在 yield 用尽时 return (); 的约定。也许 Rust 中生成器的最终版本将不会依赖这种行为,而只允许返回与 yield 相同的类型。你可以在github.com/rust-lang/rust/issues/43122找到关于这个话题和更多内容的讨论。
除了异步之外,生成器另一个重要的用例是迭代器。如此之多,以至于 Rust 标准库的迭代器计划最终将被生成器重写。目前,如何实现这一过渡的具体方式尚未确定,因此没有Iterator for Generator的泛型实现。为了解决这个问题,你可以创建一个小的包装类型,就像我们用GeneratorIterator[50]做的那样,它为它的包装Generator实现了Iterator。
我们通过重写第二章中的斐波那契迭代器来说明如何使用它,该章节名为处理集合;创建自定义迭代器,在fibonacci函数中使用生成器[64]。实现看起来相当简洁,不是吗?作为提醒,以下是使用Iterator特质直接编写的原始实现,它不仅需要一个函数,还需要一个struct和一个特质实现:
fn fibonacci() -> Fibonacci {
Fibonacci { curr: 0, next: 1 }
}
struct Fibonacci {
curr: u32,
next: u32,
}
impl Iterator for Fibonacci {
type Item = u32;
fn next(&mut self) -> Option<u32> {
let old = self.curr;
self.curr = self.next;
self.next += old;
Some(old)
}
}
参见
- 在第二章中,处理集合的创建自定义迭代器配方[977b8621-cb73-43de-9a2b-4bc9f5583542.xhtml]


访问。如果你可以的话,使用你收藏的本地访问方法(大多数情况下,这将是通过
完成的,而
完成它。
浙公网安备 33010602011771号