Rust-快速启动指南-全-
Rust 快速启动指南(全)
原文:
annas-archive.org/md5/e9a843f2bd92e621911511b3b285f94a译者:飞龙
前言
Rust 是一种系统级编程语言,越来越受欢迎。这种受欢迎程度是由其语义驱动的,它鼓励创建快速、可靠的软件。在这本书中,我们将学习语言的基础,最终达到可以开始编写可用的程序的程度。
本书面向对象
本书面向对学习日益流行的 Rust 编程语言感兴趣的人。您不必已经是程序员,但如果您是,那将有所帮助。
为了充分利用本书
至少对另一种编程语言有一些了解将有助于比较和对比。您需要互联网连接来下载和安装编译器工具链。假设您能够使用命令行工具。
下载示例代码文件
您可以从 www.packt.com 的账户下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问 www.packt.com/support 并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
- 
在 www.packt.com 登录或注册。 
- 
选择“支持”标签。 
- 
点击“代码下载与勘误”。 
- 
在搜索框中输入书籍名称,并遵循屏幕上的说明。 
文件下载后,请确保使用最新版本的以下软件解压或提取文件夹:
- 
WinRAR/7-Zip for Windows 
- 
Zipeg/iZip/UnRarX for Mac 
- 
7-Zip/PeaZip for Linux 
本书代码包托管在 GitHub 上,地址为 github.com/PacktPublishing/Rust-Quick-Start-Guide。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有来自我们丰富的图书和视频目录的其他代码包可供下载,地址为 github.com/PacktPublishing/。查看它们!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/9781789616705_ColorImages.pdf。
使用的约定
本书使用了多种文本约定。
CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“将下载的 WebStorm-10*.dmg 磁盘镜像文件作为系统中的另一个磁盘挂载。”
代码块设置如下:
fn main() {
     println!("Hello, world!");
 }
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
if 3 > 4 {
    println!("Uh-oh. Three is greater than four.");
}
else if 3 == 4 {
    println!("There seems to be something wrong with math.");
}
任何命令行输入或输出都按以下方式编写:
$ mkdir css
$ cd css
粗体: 表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“从管理面板中选择系统信息。”
警告或重要提示看起来像这样。
小贴士和技巧看起来像这样。
联系我们
我们欢迎读者的反馈。
一般反馈: 如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并通过customercare@packtpub.com给我们发送邮件。
勘误: 尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告这一点。请访问www.packt.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版: 如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packt.com与我们联系,并提供材料的链接。
如果您有兴趣成为作者: 如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
评论
请留下评论。一旦您阅读并使用了这本书,为什么不在此处购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 公司可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
如需了解 Packt 的更多信息,请访问packt.com。
第一章:准备工作
在本指南中,我们将学习与 Rust 一起工作的基础知识,这是一种在过去的几年中逐渐成名的系统级编程语言。Rust 是一种严格的语言,旨在使最常见的错误成为不可能,使不太常见的错误变得明显。
作为一种系统级语言,Rust 受没有安全网的低级程序需求的指导,因为它们正是高级程序的安全网。操作系统内核、网络浏览器和其他关键基础设施都是系统级应用。
这并不是说 Rust 只能用于编写关键基础设施,当然不是。Rust 代码的效率和可靠性可以惠及任何程序。只是高级代码的优先级可能不同。
在本章中,我们将介绍以下主题:
- 
rustup工具
- 
cargo工具
- 
如何开始一个新的 Rust 项目 
- 
如何编译 Rust 项目 
- 
如何定位第三方库 
- 
如何管理依赖项 
- 
如何保持 Rust 安装更新 
- 
如何在稳定版和 beta 版 Rust 之间切换 
安装 Rust
在任何支持的平台上安装 Rust 都非常简单。我们只需导航到rustup.rs/。该页面将为我们提供一个步骤的安装命令行 Rust 编译器的程序。根据平台的不同,程序略有差异,但从不困难。这里我们看到的是 Linux 的rustup.rs页面:

安装程序不仅安装了 Rust 编译器,还安装了一个名为rustup的工具,我们可以在任何时候使用它来升级我们的编译器到最新版本。为此,我们只需打开一个命令行(或终端)窗口,并输入:rustup update。
升级编译器需要简单,因为 Rust 项目使用六周的快速发布计划,这意味着每六周就会有一个新的编译器版本,就像时钟一样。每个版本都包含自上次发布以来被认为稳定的所有新功能,以及之前版本的功能。
不要担心,新功能的快速发布并不意味着这些功能在发布前的六周内匆忙拼凑而成。它们在发布前通常已经经过多年的开发和测试。发布计划只是确保一旦一个功能被认为真正稳定,它就能很快进入我们的手中。
如果我们不愿意等待一个功能经过审查和稳定,无论出于什么原因,我们也可以使用rustup下载、安装和更新编译器的beta或nightly版本。
要下载和安装 beta 编译器,我们只需输入这个:rustup toolchain install beta。
从那时起,当我们使用rustup更新我们的编译器时,它会确保我们拥有稳定和测试版编译器的最新版本。然后我们可以使用rustup default beta来激活测试版编译器。
请注意,测试版编译器与下一个稳定编译器的下一个版本不是同一回事。测试版是功能在毕业到稳定之前存在的地方,功能可以在测试版中保持多年。
每晚版本最多落后于开发代码仓库 24 小时,这意味着它可能以任何方式出现错误。除非你实际上正在参与 Rust 本身的发展,否则它并不特别有用。然而,如果你想要尝试它,rustup也可以安装和更新它。你也可能发现自己依赖于一个依赖于仅在每晚构建中存在的特性的库,在这种情况下,你需要告诉rustup你也需要每晚构建。
rustup将安装的一个工具是名为cargo的工具,我们将在本章中看到很多,并在本书的其余部分幕后使用它。cargo工具是整个 Rust 编译系统的前端:它用于创建包含程序或库初始样板代码的新 Rust 项目目录,用于安装和管理依赖项,以及编译 Rust 程序等。
开始新项目
好吧,所以我们已经安装了编译器。太好了!但我们如何使用它呢?
第一步是打开一个命令行窗口,并导航到我们想要存储新项目的目录。然后我们可以使用cargo new foo来创建一个新程序的骨架。
当我们这样做时,cargo将在foo目录中创建一个新的目录,并在其中设置骨架程序。
默认情况下,cargo会创建一个可执行程序的骨架,但我们可以告诉它为我们设置一个新的库。这只需要一个额外的命令行参数(bar是新创建的目录的名称,就像foo一样):cargo new --lib bar。
当我们查看新创建的foo目录时,我们看到一个名为Cargo.toml的文件和一个名为src的子目录。可能还会有一个 Git 版本控制仓库,我们现在可以忽略它。
项目元数据
Cargo.toml文件是存储程序元数据的地方。这包括程序名称、版本号和作者,但更重要的是,它还有一个依赖部分。编辑[dependencies]部分的内容是告诉 Rust 在编译我们的代码时应该链接到外部库,以及使用哪些库和版本,以及它们在哪里。外部库是一系列源代码的集合,被打包起来以便作为其他程序组件使用。通过找到并链接好的库,我们可以节省编写整个程序的时间和精力。相反,我们只需编写别人还没有做过的部分。
顺便说一句,.toml文件是用Tom's Obvious, Minimal Language(TOML)编写的,它是旧.ini格式的更定义明确和功能更完整的版本,虽然微软推广了它但从未标准化。TOML 变得越来越受欢迎,并且在各种语言和应用中被支持和使用。你可以在github.com/toml-lang/toml找到语言规范。
来自 crates.io 的库的依赖
如果我们程序依赖的库在crates.io/上发布,我们只需将其链接代码添加到依赖部分即可。假设我们想在程序中使用serde(一个将 Rust 数据转换为 JSON 等格式并反向转换的工具)。首先,我们使用以下命令找到它的链接代码:cargo search serde。
我最初是通过浏览crates.io了解到serde的,这是一个我鼓励你也尝试的探索过程。
这将打印出一个类似以下内容的匹配列表:
     serde = "1.0.70"                          # A generic serialization/deserialization framework
     serde_json = "1.0.24"                 # A JSON serialization file format
     serde_derive_internals = "0.23.1"     # AST representation used by Serde derive macros. Unstable.
     serde_any = "0.5.0"                   # Dynamic serialization and deserialization with the format chosen at runtime
     serde_yaml = "0.7.5"                 # YAML support for Serde
     serde_bytes = "0.10.4"              # Optimized handling of `&[u8]` and `Vec<u8>` for Serde
     serde_traitobject = "0.1.0"       # Serializable trait objects.  This library enables the serialization of trait objects such…
     cargo-ssearch = "0.1.2"             # cargo-ssearch: cargo search on steroids
     serde_codegen_internals = "0.14.2"    # AST representation used by Serde codegen. Unstable.
     serde_millis = "0.1.1"                #     A serde wrapper that stores integer millisecond value for timestamps     and duration…
     ... and 458 crates more (use --limit N to see more)
第一个是核心serde库,链接代码是#符号之前的部分。我们只需将其复制粘贴到Cargo.toml文件的依赖部分,Rust 就会知道在编译我们的foo程序时应该编译和链接serde。因此,Cargo.toml的依赖部分将看起来像这样:
 [dependencies]
 serde = "1.0.70"
来自 Git 仓库的依赖
依赖于存储在 Git 版本控制系统中的库,无论是本地还是远程,也很容易。链接代码略有不同,但看起来像这样:
    [dependencies]
    thing = { git = "https://github.com/example/thing" }
我们告诉 Rust 如何找到仓库,它知道如何检出、编译并将它链接到我们的程序。仓库位置不一定是 URL;它可以是被git命令识别的任何仓库位置。
本地库的依赖
当然,我们也可以链接到存储在我们自己系统上的其他库。为此,我们只需在我们的Cargo.toml文件中添加一个类似这样的条目:
     [dependencies]
     example = { path = "/path/to/example" }
路径可以是绝对路径或相对路径。如果是相对路径,它将被解释为相对于包含我们的Cargo.toml文件的目录。
自动生成的源文件
当创建可执行程序时,cargo会在创建时将一个名为main.rs的文件添加到我们的项目中。对于一个新创建的库,它则添加lib.rs。在两种情况下,该文件都是整个项目的入口点。
让我们看看模板main.rs文件:
     fn main() {
         println!("Hello, world!");
     }
简单到令人难以置信,对吧?Cargo 的默认程序是经典hello world程序的 Rust 版本,无数新程序员在几乎每一种可想象的编程语言中都已重新实现了它。
如果我们查看一个新库的lib.rs文件,事情会变得更有趣:
     #[cfg(test)]
     mod tests {
         #[test]
         fn it_works() {
             assert_eq!(2 + 2, 4);
         }
     }
与所有可执行程序都需要一个主函数以有一个启动点不同,库的模板包括一个自动化测试框架以及一个确认2 + 2 = 4的单个测试。
编译我们的项目
编译 Rust 程序的基本命令很简单:cargo build。
我们需要位于包含Cargo.toml的目录中(或该目录的任何子目录),这样才能做到这一点,因为这是cargo程序知道要编译哪个项目的方式。然而,我们不需要提供任何其他信息,因为它所需的所有信息都在元数据文件中。
这里,我们看到构建chapter02源代码的结果:

这些警告是可以预见的,并不会阻止编译成功。如果我们仔细查看这些警告,我们可以看到 Rust 的警告比许多编程语言都要有帮助,它给我们提供了改进效率等提示,而不是仅仅谈论语言语法。
当我们构建程序时,会创建一个Cargo.lock文件和target目录。
Cargo.lock记录了构建项目时使用的依赖项的确切版本,这使得从不同编译的同一程序中产生可重复的结果变得容易得多。在很大程度上,可以忽略这个文件,因为cargo通常会处理与之相关的任何需要做的事情。
Rust 社区建议,如果你的项目是一个程序,应该将Cargo.lock文件添加到你的版本控制系统(例如 Git)中,但如果你的项目是一个库,则不应这样做。这是因为程序Cargo.lock文件存储了导致完整程序成功编译的所有版本,而库只包含部分画面,因此当分发给他人时,可能会比有帮助造成更多的困惑。
target目录包含所有由编译过程产生的构建工件和中间文件,以及最终的程序文件。存储中间文件允许未来的编译只处理需要处理的文件,从而加快编译过程。
我们的项目本身位于target/debug/foo文件中(或在 Windows 上的target\debug\foo.exe),如果我们想手动导航到它并运行它,可以这样做。然而,cargo提供了一个快捷方式:cargo run。
我们可以从项目的任何子目录中使用该命令,并且它会为我们找到并运行我们的程序。
此外,cargo run隐含着cargo build,这意味着如果我们自上次运行程序以来更改了源代码,cargo run将在运行程序之前重新编译程序。这意味着我们可以在修改代码和用cargo run执行它以查看其效果之间交替进行。
调试和发布构建
你可能已经注意到程序位于一个名为target/debug的目录中。这是怎么回事?默认情况下,cargo以调试模式构建我们的程序,这是程序员通常想要的。
这意味着生成的程序被配置为与rust-gdb调试程序一起工作,这样我们就可以检查其内部发生的情况,并在崩溃转储等情况下提供有用的信息,同时跳过编译器的优化阶段。优化被跳过,因为这些优化以某种方式重新排列事物,使得调试信息几乎无法理解。
然而,有时一个程序可能没有更多的错误(我们知道的情况)并且我们准备将其分发给其他人。为了构建程序的最终、优化版本,我们使用cargo build --release。
这将构建程序的发布版本,并将其留在target/release/foo中。我们可以从那里复制它,并打包它以进行分发。
动态库、软件分发和 Rust
在很大程度上,Rust 避免使用动态库。相反,Rust 程序的依赖项都直接链接到可执行文件中,并且只有选择性的操作系统库是动态链接的。这使得 Rust 程序比预期的要大一些,但在千兆字节的时代,几兆字节并不成问题。作为交换,Rust 程序非常便携,并且不受动态链接库版本问题的影响。
这意味着,如果一个 Rust 程序能正常工作,它几乎可以在编译时使用的任何运行大致相同操作系统和架构的计算机上运行,没有任何麻烦。你可以将 Rust 程序的发布版本压缩,然后有信心地将其通过电子邮件发送给其他人,他们不会有任何问题运行它。
这并没有完全消除外部依赖。例如,如果你的程序是一个客户端,它连接的服务器需要可用。然而,它确实大大简化了整个打包和分发过程。
使用 crates.io
我们之前看到了cargo search,它允许我们从命令行快速轻松地找到第三方库,以便我们可以将它们链接到我们的程序中。这非常有用,但有时我们需要的不仅仅是它提供的信息。当我们确切知道我们想要哪个库,并且只需要快速参考链接代码时,它最有用。
当我们不知道自己确切想要什么时,通常最好使用网页浏览器浏览crates.io/并寻找选项。
当我们在网页浏览器中找到一个有趣或有用的库时,我们会得到以下信息:
- 
链接代码 
- 
简介信息 
- 
文档 
- 
人气统计 
- 
版本历史 
- 
许可信息 
- 
库的网站链接 
- 
源代码链接 
这更丰富的信息有助于确定哪个库或哪些库最适合我们的项目。选择最适合工作的库最终可以节省大量时间,因此crates.io的网页界面非常出色。
crates.io的前页展示了新的和受欢迎的库,以多种方式划分,这些可以是有趣和有用的探索对象。然而,主要价值在于搜索框。使用搜索框,我们通常可以找到任何可能需要的库的几个候选者。
摘要
因此,现在我们知道了如何安装 Rust 编译器,设置 Rust 项目,查找和链接有用的第三方库,并将源代码编译成可用的程序。我们还简要了解了cargo在为我们设置新程序或库项目时生成的样板代码。我们学习了调试构建和发布构建之间的区别,并快速浏览了将 Rust 程序分发给用户所涉及的内容。
在第二章“Rust 语言基础”中,我们将开始研究 Rust 编程语言本身,而不是围绕它的支持设施。我们将了解语言的结构和一些最重要的命令。
第二章:Rust 语言基础
好的,我们现在准备开始编写一些 Rust 代码。在本章中,我们将探讨 Rust 程序的结构,以及如何在语言中表达各种常见的编程元素。我们将从函数和模块开始,然后转向基本语言特性,如分支、循环和数据结构。本章中涵盖的几乎所有内容在大多数其他编程语言中都有对应;这些都是编程的基础。
具体来说,本章描述了以下内容:
- 
函数,它们类似于大型程序中的一部分小程序 
- 
模块,用于组织程序 
- 
表达式,这是我们告诉程序实际执行特定事情的方式 
- 
分支,这是我们告诉程序做出决策的方式 
- 
循环,这是我们告诉程序执行扩展操作的方式 
- 
结构体,这是我们组织信息以便程序处理的方式 
- 
将函数附加到结构或其他数据类型上,使它们更有用 
函数
在上一章中,当我们查看由cargo new自动生成的样板代码时,我们顺便看到了几个函数。我们实际上看到了什么?
函数是一系列计算机需要遵循的指令。它有点像食谱。如果我们知道他们已经有了一个饼干食谱,我们就不必告诉一个人如何使用多少面粉、糖和牛奶来烤饼干。我们只需说:请烤一些饼干。函数也是类似的。我们不必告诉计算机如何确切地将一些信息保存到数据库中;如果有save_to_database函数,我们可以用它来完成这项工作。
在 Rust 中,能够告诉计算机采取行动的指令只能写在函数内部。一切始于一个名为main的函数,它可以启动其他函数的运行,而这些函数又可以启动更多函数,以此类推。再次使用我们的食谱类比,就像一个馅饼食谱说:使用第 57 页的食谱制作饼皮面团。
定义一个函数
在 Rust 中,函数以fn关键字开始。关键字是一系列字母或符号,在语言中有固定的含义。我们在程序中所做的一切都不能改变关键字的含义,我们使用的库也不能改变其含义。关键字在不同的上下文中偶尔有不同的含义,但它们在以相同方式使用时始终具有相同的含义。关键字是构建其他一切的基础。
因此,fn关键字用于告诉 Rust 编译器我们即将告诉它一个新函数。之后,通过一个空格隔开,接着是函数的名称。函数名称的规则如下:
- 
它必须由以下内容组成: - 
英语字母(从 A到Z的大写或小写形式)
- 
阿拉伯数字(数字 0到9)
- 
下划线( _)
 
- 
- 
它不能以数字开头(因此 7samurai不是一个有效的名称)
- 
如果它以下划线开头,它必须至少有一个后续字符( _单独具有特殊含义)
然后是一个开括号 ( 和一个闭括号 ),它们之间是参数列表。我们现在暂时跳过参数列表,稍后再回来讨论。如果函数不需要参数,括号之间不需要有任何内容,这就是我们现在要这样做的方式。
在参数列表的闭括号之后,我们可以选择性地包含一个 → 符号,后面跟着返回类型,这是我们稍后会更详细讨论的另一件事。
接下来是一个 { 符号,它告诉 Rust 我们即将开始一系列命令,然后是 Rust 需要的命令,以便 Rust 知道我们希望函数做什么,最后是一个 } 符号来标记结束。
回到样板代码,让我们再次看看自动生成的 main 函数:
fn main() {
     println!("Hello, world!");
 }
在这里,我们可以看到 fn 关键字、函数名和空参数列表。可选的返回类型已被省略。然后,在 { 和 } 之间,我们看到一条单独的指令,它告诉计算机,每当它被要求运行 main 函数时,我们希望它打印出 Hello, world!。
在我们理解了在 { 和 } 符号之间我们可以给计算机下达哪些指令之前,关于函数就没有太多可以说的了。主要思想是我们可以将许多指令捆绑成一个函数,然后在程序的其他地方使用一条单独的指令来告诉计算机 执行所有这些操作。
模块
模块为我们提供了一种将函数(以及具有名称的其他项目,如数据结构)组织成类别的途径。这有助于我们保持事物有序,并允许我们在模块中多次使用相同的名称,只要我们每次只使用一次。它还让我们大多数时候可以使用该事物的简短版本,但在那些简短名称可能令人困惑或含糊不清的情况下,我们可以使用较长的版本。
定义模块
定义模块很简单。在任何编译器将要查看的 .rs 文件中,我们可以使用 mod 关键字来开始一个新的模块。尽管如此,使用该关键字有两种不同的方式,这取决于我们是否希望将模块定义为当前文件的一部分或作为单独的文件。
模块作为文件的一部分
要将模块定义为文件的一部分,我们使用 mod 关键字后跟一个名称,然后是一个 { 符号,然后是模块的内容,最后是一个 } 符号来完成。
因此,如果我们定义一个新的模块,包含几个函数,它看起来可能就像这样:
pub mod module_a {
    pub fn a_thing() {
         println!("This is a thing");
    }
    pub fn a_second_thing() {
         a_thing();
         println!("This is another thing");
    }
}
我们创建了一个名为 module_a 的模块,并将 a_thing 和 a_second_thing 函数放入其中。我们之前没有见过它,但 a_second_thing 中的 a_thing(); 这一行是给计算机运行的 a_thing 函数的指令。所以,当 a_second_thing 运行时,它首先运行 a_thing,然后打印出它自己的消息。
pub 关键字表示 module_a 是当前模块的公共接口的一部分,而不仅仅是内部数据。我们很快就会更多地讨论这一点。
模块作为一个独立的文件
更多的时候,我们希望给我们的模块它们自己的文件。尽可能地将事物分开并包含起来会更好,因为这有助于保持代码的可管理性。幸运的是,这同样简单。在我们的 .rs 文件中,我们可以简单地写下以下内容:
pub mod module_b;
这看起来与之前的例子非常相似,只不过它没有在 { 和 } 之间直接包含模块内容。相反,Rust 编译器会寻找一个名为 module_b.rs 或 module_b/mod.rs 的文件,并使用整个文件作为 module_b 模块的内容。所以,如果文件包含一些与我们之前看到的类似的功能:
pub fn a_thing() {
    println!("This is a module_b thing");
}
pub fn a_second_thing() {
    a_thing();
    println!("This is another module_b thing");
}
那么 module_b 将包含两个名为 a_thing 和 a_second_thing 的函数。这些函数与之前 module_a 模块中的函数同名并不是问题,因为它们在不同的模块中。
为什么编译器在两个地方寻找 module_b 的源代码?这使我们能够更灵活地安排我们程序的源代码目录结构。
从外部访问模块内容
在 模块作为文件的一部分 部分中,a_second_thing 函数是 a_thing 函数所在模块的一部分,因此它自动允许使用其他函数的短名称来引用它。然而,模块外的代码需要使用全名来引用模块内的项。这可以通过两种方式完成。可以直接这样做,如果我们不经常引用该项,这是一个不错的选择,或者我们可以告诉 Rust 我们想要在不同的模块中使用项的短名称,如果我们打算在代码中经常使用该项,这也是一个好的选择。
直接使用项目的全名
项目的全名由模块名、一个 :: 符号和项目的短名组成。如果我们需要通过多层模块才能找到我们想要的项,我们按顺序列出这些模块的名称,每个名称之间用 :: 隔开。例如,我们可能会引用 std::path::Path 来从 std 模块的 path 模块中获取 Path 项。
我们可以在任何地方使用全名,并且对所讨论的项目完全明确无误。
使用项目的短名
我们还可以使用use关键字来告诉 Rust 我们想要通过短名来引用不同模块中的项。这只需写下use后跟我们要使用的项的完整名称即可。例如,use std::path::Path;允许我们在后续指令中使用该项的短名(在这个例子中是Path),直到我们遇到关闭我们use关键字所在代码段的大括号}(或者我们到达模块文件末尾,这效果相同)。
我们可以使用相同的语法来告诉 Rust 我们想要使用模块的名称,而不是模块中的项。例如,std::path是一个有效的命令。这将允许我们使用path::Path作为后续代码中Path项的名称。这通常很方便,因为它仍然将外部项封装并分离,同时提供了合理简短且信息丰富的名称来工作。
公共和私有模块项
在许多前面的例子中,我们看到了pub关键字。该关键字使其附加的项变为public,这意味着它对不属于同一模块的代码是可用的。如果我们省略项上的pub关键字,则该项是private,这意味着它只能在定义它的模块内访问。private是默认的,因此我们需要明确标记那些我们希望作为模块外部可访问接口部分的项为public,通过使用pub关键字。
将项设置为私有并不是一种安全机制。如果你担心你的代码将与恶意代码链接,可能会滥用你的代码或数据,将代码或数据设置为私有将不会防止此类攻击。相反,公共和私有之间的区别存在是为了帮助我们明确哪些代码部分是打算在当前模块之外使用的,哪些是打算仅内部使用的。这有助于我们维护软件,因为我们可以在私有项上自由做出任何我们想要的更改,而对于公共项,我们必须小心我们的更改不会破坏我们可能甚至不知道存在的外部事物。
表达式
告诉计算机在 Rust 程序中执行某事的指令几乎都是表达式。表达式告诉计算机如何计算特定值,并产生该值作为其结果。在数学中,2 + 2是一个结果值为 4 的表达式。同样,(2 + 2) - 1是一个结果值为 3 的表达式,它本身由一个加法表达式和一个减法表达式组成。在 Rust 中,同样的基本思想适用:表达式告诉计算机如何找到一个值,并且它们可以组合在一起,因为使用产生值的表达式与直接使用该值具有相同的结果,就像写作(2 + 2) - 1与写作4 - 1具有相同的结果一样。
虽然 Rust 中的所有表达式看起来并不像数学。Rust 是一种编程语言,而不仅仅是一个计算器。重要的是表达式的概念,它们将值组合起来产生新的值。
字面量表达式
Rust 最简单的表达式就是我们直接写出我们想要的值的表示。例如,当 Rust 看到2时,它知道我们要求它给出数字2。同样,当 Rust 看到"Hello, world!"时,它知道我们要求它产生拼写为Hello, world!的字母序列。
Rust 识别以下字面量表达式:
- 
数字 
- 
引号文本 
- 
字节序列 
- 
单个 Unicode 点 
- 
单个字节 
- 
布尔值 
数字可以写成整数、十进制数或工程记数法,对于引号文本和字节序列也有几种变体。布尔值写作true或false。在这本书中,我们不需要任何引号文本的变体,也不需要字节序列,所以不会详细介绍这些。如果你好奇,请参阅doc.rust-lang.org/。
运算符表达式
同样,像数学一样,Rust 有许多可以应用于值以将它们转换为新值的符号运算符。例如,+是 Rust 运算符,用于将两个值相加。所以,2 + 2是一个 Rust 表达式,将数字 2 加到自身,产生数字 4。Rust 还使用-作为减法运算符,*作为乘法运算符,/作为除法运算符,%作为余数运算符。
Rust 不仅限于数学运算符。在 Rust 中,&表示and,|表示or,^表示exclusive or,!表示not(例如,!true是false),<<表示leftward bit shift,>>表示rightward bit shift。有时这些运算符的含义取决于它们所作用值的类型。例如,当应用于整数时,|表示bitwise or,但当应用于布尔值时,表示logical or。
然后是比较运算符。==运算符表示check whether two values are equal。围绕==运算符构建的表达式,如果被比较的两个值相等,则产生布尔值true,如果不相等,则产生false。所以,例如,5 == 4是一个产生false结果的表达式。同样,!=表示not equal,>表示greater than,<表示less than,>=表示greater than or equal,<=表示less than or equal。所有这些在关系正确时都产生true,在关系不正确时产生false。
最后,Rust 识别 && 和 || 操作符。这些只能应用于布尔值(true 或 false),并且当应用于相同的值时,它们会产生与 & 和 | 相同的结果。区别在于 && 和 || 是所谓的 懒 或 短路 操作符,这意味着如果左侧操作数提供了足够的信息来确定操作符产生的值,它们将不会麻烦去评估其右侧操作数。例如,对于表达式 false && some_expensive_calculation(),Rust 将永远不会麻烦去运行 some_expensive_calculation 函数,因为无论该函数产生什么结果,&& 操作的结果都将为 false。
在大多数情况下,当我们会在布尔值上使用 & 或 | 时,我们应该使用 && 或 ||,因为这样可以使得 Rust 更有效率,特别是如果我们足够细心,将更昂贵的操作放在操作符的右侧。
这不是 Rust 操作符的完整列表,随着我们继续学习这门语言,我们将看到一些更专业的操作符。这些是我们表达程序中大多数计算、计算和决策所需的操作符。
数组和元组表达式
数组是一系列数据值的顺序集合。有许多使用和操作它们的方法,但在这里我们感兴趣的是创建它们并访问其内部数据值的专用表达式。要告诉 Rust 我们想要创建一个新的数组,我们只需要写一个 [ 符号,然后是一个逗号分隔的表达式列表,这些表达式产生我们想要存储在数组中的值,最后是一个 ] 符号。如果我们想要一个空数组,那么在开始和结束符号之间不需要有任何内容。因此,我们可以将 [] 写作产生空数组的表达式,或者将 [1, 3, 5] 写作产生包含三个数字的数组的表达式。数组中存储的所有值都需要具有相同的数据类型——在这个例子中是整数——所以如果我们尝试将第二个元素设置为文本字符串,例如 "nope",那么在尝试编译程序时会产生编译器错误。
这对于需要创建短数组的场景来说非常好,但想象一下编写包含一千个值的数组的表达式!我们不想逐个写出它们。幸运的是,我们可以做类似 [0; 1000] 的事情,这将产生一个包含一千个零值的数组。然后,代码的另一个部分可以填充这些槽位中的不同值。
一旦我们有一个数组值,我们通常需要访问存储在其中的值。这也通过使用 [ 和 ] 符号来实现。如果我们有一个名为 an_array 的数组(我们将在本章的 变量、类型和可变性 部分中看到如何给值命名),我们可以通过 an_array[0] 访问数组中的第一个值,通过 an_array[1] 访问第二个值,依此类推。请注意,第一个值用 0 编号,而第二个值用 1 编号。许多编程语言都是这样计数的,因为这简化了它们经常需要与数组和其他值序列一起进行的某些数学运算。
除了数组之外,Rust 允许我们创建 元组。创建元组的表达式与数组类似:(1,wow,true)是一个包含数字值 1、文本值 wow 和布尔值 true 的元组。如果我们有一个名为 a_tuple 的元组,那么 a_tuple.1 将产生元组中的第二个值,在这种情况下是单词 wow。但是,没有简化的方式来创建包含一千个重复值的元组,因为它们不是为此而设计的。与数组不同,单个元组可以包含多个不同类型的值,它们旨在作为轻量级的数据结构,而不是作为许多相似数据值的集合。
在某些语言中,元组的元素内容不能被更改。然而,在 Rust 中并非如此,Rust 中的元组遵循与其他数据结构相同的规则。
如果我们需要创建一个只包含一个元素的元组(这并不常见,因为元组的主要目的是将多个值关联在一起),我们需要在值后面包含一个逗号。因此,包含数字 5 的单元素元组看起来是这样的:(5,)
块表达式
有时候,确定表达式结果值的必要步骤根本不适合我们之前看到的单表达式。也许它们需要暂时存储一个数据值以执行效率更高,或者在其他方面过于复杂,无法以 1 + ((2 * 57) / 13) 的风格合理编写。
这就是块表达式的作用。块表达式看起来很像函数的主体,因为函数的主体就是一个块表达式。它们以 { 开始,以 } 结束。在这两个标记之间,我们可以编写所需的任何指令,包括定义变量或其他命名项。
在块的末尾应该出现产生块最终结果值的表达式。例如,块表达式 { 2 + 2; 19 % 3; println!("In a block"); true} 是一个(有点愚蠢的)块表达式,其结果为布尔值 true,但不是在它计算出 2 加 2 等于 4、计算出 19 除以 3 的余数是 1、并将 In a block 打印到控制台之后。
顺便说一下,Rust 编译器会警告我们关于那个块表达式,因为它计算了两个值,然后又把它们丢弃了。这是浪费的,所以 Rust 会指出这一点。如果启用了优化,编译器实际上会跳过生成计算这些值的代码,但这只是一个优化,程序和编译器仍然应该像它们确实执行了计算一样行事。
注意块表达式中的分号(;)。块中的每个顶级指令后面都有一个分号,除了最后一个之外。这是因为分号告诉 Rust,它前面的表达式应该被视为一个语句,这意味着它不会产生值,或者即使产生了值,我们也不关心那个值是什么。在某些情况下,可以在块中最后一个表达式之前省略分号,但我不建议这样做,因为明确地丢弃我们不会使用的表达式的结果,可以让编译器有更多的自由来做出推断和优化,并且可以帮助避免一些相当隐晦的编译器错误。
如果我们在块中的最后一个表达式后面放一个 ;,这意味着该块根本没有任何有意义的返回值。在这种情况下,它最终会有一个 () 作为其返回值。这是一个空元组,这是一个很好的方式来说明:这里没有什么可看的。() 在 Rust 语言和库中就是这样使用的。
分支表达式
程序真正有用的一个特点就是它们能够做出决策。在 Rust 中,我们可以通过使用 if 表达式来实现这一点。一个 if 表达式看起来可能像这样:
if 3 > 4 {
    println!("Uh-oh. Three is greater than four.");
}
else if 3 == 4 {
    println!("There seems to be something wrong with math.");
}
else {
    println!("Three is not greater than or equal to four.");
};
如果你熟悉其他编程语言,你可能会想知道条件表达式周围为什么没有括号。Rust 的语法并不要求在那里使用括号。实际上,如果我们把条件放在括号里,编译器会警告我们这些括号是不必要的。
我们这里有一个展示 if 所有功能的表达式。它从关键字 if 开始,后面跟着一个产生 true 或 false 的条件表达式,然后是一个块表达式。如果条件表达式产生 true,则执行块表达式,但如果条件表达式产生 false,则不执行块表达式。
使用 3 > 4 作为我们的条件表达式并不是很有用。我们完全可以只写 false,或者完全省略那个块表达式,因为它永远不会被执行。然而,在实际代码中,我们会使用一个条件表达式,其结果在我们编写代码时我们并不知道。例如,是否在上午 8 点到下午 5 点之间、用户是否从菜单中选择了这个值,以及值是否与数据库中存储的值匹配,这些都是更现实的条件,尽管当然它们必须用 Rust 表达出来。
之后,我们有一个 else if 和另一个条件表达式和代码块。这意味着,如果第一个条件表达式返回 false,计算机应该检查第二个是否返回 true,如果是,就运行相关的代码块。
我们可以在初始 if 之后链式连接尽可能多的 else if 表达式,因此我们可以提供给计算机的不同选项数量没有限制。然而,在任何给定的决策之后,只有一个会运行。计算机将从初始 if 开始,逐个检查条件表达式的值,直到找到一个返回 true 的,然后它会运行相关的代码块,然后它会完成 if 表达式并继续后续指令。
在 if 和我们可能希望包含的任何 else if 之后,我们可以放置一个 else 后跟一个代码块。这是一个没有条件的分支,它的意思是 如果没有任何条件表达式产生 true,就执行这个操作。换句话说,它允许我们告诉计算机在没有提供特殊情况的默认情况下应该做什么。
循环表达式
程序变得有用的基本能力之一是循环。Rust 有几种不同的循环类型,但我们将在这里查看两种:while 循环和 for 循环。
while 循环
while 循环与 if 表达式非常相似。区别在于,while 循环不是只检查一次条件表达式,然后根据结果运行或跳过代码块,之后结束,而是会重复这个过程,直到条件表达式产生 false。所以,如果条件表达式一开始就返回 false,代码块就不会运行。另一方面,如果条件表达式第一次检查就返回 true,代码块就会运行,然后再次评估条件表达式。如果它再次返回 true,代码块会再次运行,以此类推,直到条件表达式最终返回 false。
这意味着代码块必须改变影响条件表达式结果的内容非常重要。如果条件表达式返回 true 而代码块没有机会改变这一点,程序将会陷入不断循环执行该代码块,直到程序被强制终止。这是导致程序冻结的最简单方法。
所以,这是一个简单的 while 循环:
while i < 3 {
    i = i + 1;
    println!("While loop {}", i);
}
我们在这里使用了一个名为 i 的变量,我们将在本章的 变量和可变性 部分详细讨论。现在,只需将 i 视为一个可以在不同时间赋予不同值的名称,有点像我们可以在不同时间让不同的人坐在同一把椅子上。
因此,我们有while关键字后跟一个条件表达式。这个条件表达式使用一个变量,我们在块表达式中改变其值,所以我们不会陷入无限循环的危险。如果i的初始值为0,我们应该看到块表达式运行三次:一次当i为0时,一次当i为1时,一次当i为2时。当i达到3时,条件表达式产生false作为其结果(3 不小于 3),循环停止。
for 循环
有时,while循环正是我们所需要的,但循环的两个最常见需求是循环特定次数或使用循环来处理数组或类似数据结构中的每个元素。在这两种情况下,for循环表现更好。
一个for循环会对其块表达式进行一次迭代,每次迭代都会产生一个由迭代器生成的值。迭代器是一种特殊的数据值,其任务是逐个返回一系列值。例如,数组的迭代器每次被请求值时,都会产生数组的一个不同成员。
要循环特定次数,我们可以使用for循环和范围表达式,这是一个产生一系列数字的迭代器的表达式。让我们看看一个具体的例子:
for num in 3..7 {
    println!("for loop {}", num);
}
我们从for关键字开始,然后是num,这是将要赋予迭代器逐个产生的每个值的名称,当这些值被for循环处理时,一次一个。然后是另一个关键字in。最后,产生我们的迭代器的表达式。在这种情况下,我们有一个范围表达式,它表示值3、4、5和6。注意,7不包括在范围内。与从零开始计数一样,这使计算机的一些数学运算变得更容易,在这种情况下,这也使我们的操作更容易。如果我们想循环七次,我们只需写0..7。
我们可以使用一个变体来包含输出中的最后一个数字,如果我们需要的话:3..=7。只需记住,如果你通过0..=7循环,你将运行块表达式八次。
for循环闪耀的另一个时刻是我们有一个想要处理的实际值的集合,如下面的例子所示:
for word in ["Hello", "world", "of", "loops"].iter() {
    println!("{}", word);
}
这个循环会打印出数组中的每个单词,每个单词占一行。word名称被设置为迭代器产生的第一个值"Hello",然后运行块表达式。然后word被设置为迭代器产生的第二个值"world",再次运行块表达式。这会一直持续到迭代器没有更多的值可以产生,然后停止。
在这里,我们的迭代器正在生成数组中存储的值。那个表达式的.iter()部分基本上是在说:数组知道如何为自己创建迭代器,所以请求数组给我们一个迭代器。我们将在后面的章节中看到更多关于如何实现特定于数据类型的函数的内容,但就现在而言,我们只需要知道.符号的含义:点右侧的东西是点左侧东西的特定部分。我们要求计算机运行的不是任何iter函数,而是与我们的数组关联的iter函数。
变量、类型和可变性
变量是一个可以存储数据值的命名盒子。变量本身不是数据值,就像一盒牛奶不等于牛奶(它是由蜡纸和类似物质包含牛奶)。
另一方面,如果有人需要牛奶,你递给他们一盒满牛奶,他们不会抱怨,对于 Rust 也是如此。如果一个 Rust 表达式需要一个整数,而我们提供了一个包含整数的变量,Rust 会对此感到非常满意。
变量通常使用let关键字创建:
let x = 10;
这个语句创建了一个名为x的变量,其中包含10值。一旦完成,我们就可以将x作为表达式的一部分来引用。例如,x + 5现在是一个有效的表达式,其结果值为15。
for循环使用的名称也是变量,函数参数也是如此,尽管它们不是用let关键字创建的。
除了有名称之外,变量还以其可以存储的值的类型为特征。每个变量可以存储一种类型的值,并且永远不能存储任何其他类型的信息。Rust 通常可以确定给定变量可以存储的信息类型,但我们始终有明确指定它的选项。如果我们告诉 Rustlet x: i32 = 99;,Rust 将确保x变量可以存储 32 位有符号整数,如果我们尝试存储其他东西,它会报告一个错误。另一方面,let x: f64 = 999.0;告诉 Rust 我们希望x存储 64 位浮点数,并且尝试存储其他任何东西都是错误的。
我们不必为变量提供一个初始值。例如,我们可以说let x: u16;来告诉 Rust,x变量需要能够存储 16 位无符号整数。这是可以的。然而,如果有可能我们的某些代码会尝试使用变量的内容,而在此之前没有存储任何内容,Rust 编译器会认为这是一个错误。通常在创建变量时直接提供一个起始值会更容易一些。
变量被称为 变量,因为它们包含的值可以改变。但在 Rust 中,默认情况下,它们不能。Rust 允许我们使用多个 let 语句创建与旧变量具有相同名称的新变量,但我们不能只是给现有变量赋新值,除非该变量是 可变的。
使用与现有变量相同名称创建新变量被称为遮蔽旧变量。被遮蔽的变量仍然包含其之前的值,但不能再通过名称访问,因为那个名称现在属于另一个变量。如果还有任何对旧变量的引用仍在使用中,它们仍然会访问旧变量,而不是新变量。
可变只是意味着变量将接受更改,包括完全新的值。我们使用 mut 关键字告诉 Rust 一个变量应该是可变的:
let mut x = 17;
新的 x 变量是可变的。这意味着我们可以修改其内容:
x = 0;
现在 x 不再包含 17,而是包含 0。
我们用于变量的等号(=)不是数学等式的陈述。它并不意味着:“这两个东西被定义为相同”。相反,它意味着:“在这里,现在,右侧表达式的值要存储在左侧的变量中”。这在数学中是荒谬的,但在 Rust 中却完全合理:
for i in 0..5 {
    x = x + i;
}
Rust 有相当多的内置数据类型。我们已经看到了 i32、f64 和 u16,分别是 32 位有符号整数、64 位浮点数和 16 位无符号整数。还有更多遵循相同模式的数据类型,例如 u64 用于无符号 64 位整数,以及如 bool 用于布尔值;isize 和 usize 用于与目标架构上的内存地址占用相同数量的位的有符号和无符号整数;以及 char 和 str 用于单个 Unicode 代码点和它们的序列。
这些被称为原始类型,因为它们是语言固有的。然而,Rust 也允许我们创建新的类型,因此 Rust 标准库包含许多适合各种特定用途的数据类型,第三方库中还有更多。
类型推断
正如我们之前注意到的,我们可以指定变量的类型,但通常不必这样做。这是因为 Rust 有一个名为类型推断的功能,它通常可以通过观察我们对变量的操作来推断变量的类型。例如,如果我们使用 Tokio 网络库,我们可能会使用如下代码:
let addr = "127.0.0.1:12345".parse()?;
let tcp = TcpListener::bind(&addr)?;
我们没有指定 addr 变量应该有什么类型。更有趣的是,我们没有告诉文本地址我们需要它解析成什么类型的信息。
解析意味着将表示形式转换为可用的数据,大约如此。许多东西都可以表示为文本字符串,如果你知道如何将文本解析成你真正想要的信息。
在这个例子中的问号是 Rust 的错误处理机制的一部分,而&符号是一个影响addr变量如何与函数共享的运算符。我们很快就会看到这两个方面的更多内容。
然而,Rust 可以看到我们正在将addr变量(或者更确切地说,它的引用,但下一章会详细介绍)作为TcpListener::bind函数的参数传递,并且它知道该函数需要一个SocketAddr的引用,所以addr必须是SocketAddr。然后,由于它已经确定addr是SocketAddr,它进一步确定应该使用产生SocketAddr的字符串解析函数作为其结果值。
类型推断可以在像 Rust 这样严格的编程语言中节省大量的时间。另一方面,如果你看到一条关于你从未听说过的数据类型的错误消息,可能会感到惊讶,因为 Rust 决定这正是你需要的数据类型。如果发生这种情况,尝试将你实际期望的类型分配给你的变量,并看看 Rust 编译器之后会说什么。
数据结构
创建数据结构是将新数据类型添加到 Rust 中的方法之一。数据结构是一组相互关联的变量,它们组合成一个新的数据类型,意味着所有这些一起。
使用struct关键字定义一个新的结构体:
pub struct Constrained {
    pub min: i32,
    pub max: i32,
    current: i32,
}
注意,在定义每个包含变量之后都有逗号。在那里使用分号可能会很有诱惑力,但那会导致编译器错误。最后的逗号是可选的,但建议使用,因为它意味着可以重新排列行,而无需注意逗号可能缺失的位置,以及其他原因。
在这里,我们定义了一个名为Constrained的结构体,它由三个不同的 32 位无符号整数变量组成。该结构体本身是公共的,这意味着它可以在定义它的模块外部使用。
min和max包含的变量也是公共的,但这意味着略有不同。这意味着在任何我们有Constrained值的地方,我们都可以直接访问包含的min和max值。另一方面,current值是私有的,这意味着它只能直接在定义结构的模块内访问。我们可以在该模块中定义具有明确访问私有结构成员数据的目的的函数,但成员本身并不是结构公共接口的一部分,即使结构体本身是公共的。
要访问min和max,我们可以使用之前在几个地方看到的相同的.符号。所以,如果cons是一个可变的Constrained值,那么我们可以做类似这样的事情:
cons.min = 5;
数据结构的可变性
我们不能使用 mut 关键字来使结构体内部包含的值可变,并且省略关键字也不会使它们不可变。相反,整个结构体在特定情况下是可变或不可变的。例如,参见以下内容:
let change_no: Constrained;
let mut change_yes: Constrained;
上述代码意味着有两个变量,它们的数据类型都是 Constrained,但存储在 change_no 中的值是不可变的,而存储在 change_yes 中的值是可变的。
更多关于函数
现在,我们将通过讨论参数和返回类型来填补之前关于函数讨论中留下的空白。
参数
参数允许我们在请求函数运行时提供信息。
请求一个函数运行被称为 调用 它。
当我们定义一个函数时,我们可以告诉它我们希望它使用的变量名和类型,以便接收参数,如下面的例子所示:
pub fn set(&mut self, value: i32) {
    self.current = value;
}
我们将在本章的 实现类型行为 部分讨论 self。现在,忽略它,看看 value。在这里,我们提供了一个名称和数据类型,就像我们使用 let 创建新变量时一样。我们没有为 value 变量提供值,因为那是在函数调用时发生的。
我们一直看到函数调用,但为了清晰起见,它们看起来是这样的:
some_function(2 + 2, false)
在那个例子中,some_function 是一个函数的名称,分配给其参数的值是表达式 2 + 2 和 false 的结果。参数表达式在函数调用之前被评估,所以参数的实际值是数字 4 和布尔值,false。
返回类型
调用一个函数是一个表达式,这意味着它会产生一个结果值。我们一直忽略了这一点。如果一个函数将要产生一个结果值,我们必须告诉编译器这个结果值的数据类型。我们这样做:
pub fn get(&self) -> i32 {
    if self.current < self.min {
        return self.min;
    }
    else if self.current > self.max {
        return self.max;
    }
    else {
        return self.current;
    };
}
这是一个较长的例子,但目前为止我们专注于第一行。在函数参数之后,我们看到 ->  i32。这告诉 Rust 语言,get 函数的结果数据类型是 i32。一旦它知道了这一点,编译器将确保它是正确的。在这个例子中,没有一条路径在函数中不会产生 i32 值,所以编译器对此很满意。
在那个例子中,我们也使用了 return 关键字。return 语句停止当前正在运行的函数(这意味着在 return 语句之后本应运行的任何指令实际上并没有运行)并为函数调用表达式提供结果值。在这个例子中,如果当前值小于最小值,则返回最小值。如果当前值大于最大值,则返回最大值。否则,返回当前值。
你可能还记得,在 Rust 中,函数体是块表达式,if及其相关内容也是一个表达式,这意味着它们都会自然地产生一个结果值,即使我们没有使用return关键字。这意味着我们可以这样编写示例函数并得到相同的结果:
pub fn alternate_get(&self) -> i32 {
    if self.current < self.min {
        self.min
    }
    else if self.current > self.max {
        self.max
    }
    else {
        self.current
    }
}
你看到区别了吗?之前,我们使用return来明确终止函数并提供一个结果值。在这里,函数的块表达式的结果值是if表达式的结果值,也就是它所跟随的块表达式的结果值,无论是self.min、self.max还是self.current。最终结果是相同的,但表达方式不同。
错误处理
有时候,我们可以预测到可能会出错的可能性,或者我们正在使用一个知道它可能不会成功的库函数。当这种情况发生时,我们会发现自己正在使用特殊的Result数据类型。结果是泛型类型,我们将在后面的章节中讨论,但它对于使用函数是如此关键,我们将在这里以机械的方式展示如何使用它。
一个可能会失败的功能将有一个类似于这样的返回类型:Result<i32, &'static str>。我承认,乍一看这有点疯狂。让我们分解一下。类型从Result开始,然后是一个<,然后是i32,然后是一个,,然后是&'static str,最后是一个>。这意味着如果功能成功,它将产生一个i32,如果失败,它将产生一个&'static str。&'static str恰好是字面文本表达式的类型,比如oops, it broke,所以我们在这里真正说的是,该函数将返回一个整数或错误消息。
通常有一个专门用于表示错误的类型,比如一个Error结构体,而不是仅仅使用文本错误消息。
使用 Result 来表示成功或失败
在我们的例子基础上扩展,我们如何编写一个既能成功也能失败的功能?请看以下示例:
fn can_fail(x: bool) -> Result<i32, &'static str> {
    if x {
        return Ok(5);
    }
    else {
        return Err("x is false");
    };
}
首先,我们将返回类型设置为使用Result,然后在函数体中,我们使用Ok()或Err()来表示我们正在返回一个有效值或错误。
如果一个功能可能会失败,但在成功时没有任何有意义的返回值,我们可以使用()作为成功的返回类型。所以,在这种情况下,返回类型可能看起来像这样:Result<(), &'static str>。成功的返回值将是Ok(())。
调用返回 Result 类型的功能
当我们调用返回Result类型的功能时,返回值正如请求的那样是一个Result,而不是我们真正需要的的数据类型。根据我们的具体需求,有几种处理方式。
处理 Result 的最简单方法是用 ? 操作符,它从成功的 Result 中提取存储的值,如果查看的 Result 指示错误,则返回包含错误值的 Result。因为 ? 可能以与 return 语句相同的方式从当前函数返回,所以 ? 只能在自身返回 Result 并使用相同的数据类型来表示错误的函数中使用。使用 ? 的样子如下:
let mut cons: Constrained = new_constrained(0, 10, 5)?; 
在这里,我们调用 new_constrained 函数,它返回一个成功的结果或错误信息。然而,我们赋值的变量类型是 Constrained,而不是 Result。这之所以可行,是因为末尾的 ?,它会在函数调用成功时提取 Constrained 值,如果函数调用失败则返回。
处理返回的 Result 的下一个简单方法是使用 expect 函数。这个函数与 ? 类似,如果 Result 指示成功,则提取成功值,但处理失败的方式不同。expect 不是从当前函数返回错误,而是终止整个程序并打印出错误信息。使用 expect 的函数不需要返回 Result,因此它可以在 ? 不可用的情况下使用。使用 expect 的样子如下:
let mut cons: Constrained = new_constrained(0, 10, 5).expect("Something went very wrong");
传递给 expect 的参数是它在失败时应该显示的错误信息。还有一些其他与 expect 类似的函数,以不同的方式处理错误,例如调用错误处理函数。
最后,我们可以通过检查返回的 Result 是 Ok 还是 Err 来实际处理错误。这是通过使用 match 或 if let 表达式来完成的,我们将在第四章(700c26ca-e1de-4069-afaf-d9acb22dd6ab.xhtml)中学习,即通过模式匹配做出决策。
实现类型的行为
在前面的例子中,我们看到了看似调用包含在数据值中的函数的情况,例如 "127.0.0.1:12345".parse() 或 ["Hello", "world", "of", "loops"].iter()。这些是在这些值的类型上实现的函数。在类型上实现函数的样子如下:
impl Constrained {
    pub fn set(&mut self, value: i32) {
        self.current = value;
    }
    pub fn get(&self) -> i32 {
        if self.current < self.min {
            return self.min;
        }
        else if self.current > self.max {
            return self.max;
        }
        else {
            return self.current;
        };
    }
}
这是一个数据类型(在这个例子中是我们在前面创建的 Constrained 类型)的实现块(它不是表达式块)。实现块通过 impl 关键字引入,然后是我们要在其中放置函数的类型名称,然后是我们想要添加到数据类型的函数,在 { 和 } 符号之间。
虽然我们可以像访问包含在数据值中的变量一样访问在类型上实现的函数,但它们实际上并不存储在包含数据值的内存中。没有必要让每个数据值都有函数的副本,因为所有的副本都将完全相同。
在类型上实现的函数可以是公开的或私有的,这取决于我们是否希望外部用户使用数据类型,或者只允许当前模块内的其他函数使用。
当一个函数在某个类型上实现时,第一个参数是特殊的。即使在我们调用函数时没有将其作为函数参数传递,它也会自动提供给函数。这个自动参数传统上被称为self。self的职责是让函数能够访问通过它被调用的数据值,这意味着如果我们做类似"127.0.0.1".parse()的操作,解析函数将"127.0.0.1"作为其self参数。self参数可以写成self、&self或&mut self,这个选择将在下一章讨论。
实现块的语法允许我们指定我们要在哪种数据类型上实现函数。我们能否在未创建的类型上实现函数,比如i32或SocketAddr?答案是肯定的,但前提是我们必须创建一个特质。我们将在第五章中了解更多关于特质的内容,一种数据类型表示多种数据类型。不使用特质,我们只能在我们同一项目中创建的数据类型上实现函数,尽管它们不必在同一个模块中。
摘要
当我们将本章学到的知识付诸实践时,我们对这些知识的掌握将会更加牢固。我们学习了 Rust 程序的基本结构,以及如何编写函数、循环和分支。此外,我们还学习了 Rust 的类型系统以及如何将行为附加到数据类型上。这些为我们提供了一个基础,让我们可以在此基础上学习使 Rust 真正区别于其他编程语言的特点。
在下一章,我们将探讨 Rust 的基本概念——所有权和借用是如何工作的。
第三章:大概念——所有权和借用
所有权和借用是使 Rust 与其他编程语言区分开来的特性。你可能会找到最接近的等效物是 C++中常见的资源获取即初始化(RAII)设计模式,但那是一个设计模式,不是一个语言特性,并且并不完全类似。
在本章中,我们将讨论以下内容:
- 
值的所有权和变量的作用域 
- 
作用域之间所有权转移的方式 
- 
借用和借出数据值,以及这与所有权如何交互 
- 
借用值的生命周期 
- 
函数的 self参数,以及借用或不借用它的含义
作用域和所有权
在 Rust 中,每个数据值都有一个单一的所有作用域——不多也不少。那么,什么是作用域呢?简单的答案是,作用域是块表达式存储其变量的地方。作用域在源代码中不是直接表示的,但作用域从块表达式开始时开始,以{符号为标志,并在块表达式结束时结束,以}(或当在块达到其结束之前运行return语句时)。作用域是存储块变量的内存块。
每个数据值都有一个所有作用域,包括像2 + 2这样的隐式临时值,当我们要求 Rust 计算(2 + 2) * 3时。
当 Rust 完成一个作用域后,该作用域拥有的所有数据值都会被丢弃,用于存储它们的内存也会被释放出来供其他用途使用。这包括在堆上分配的内存,我们将在第六章中学习如何使用它,堆内存和智能指针。
从值创建到其拥有作用域完成之间的时间称为值的生命周期。
栈
与大多数编程语言一样,Rust 使用栈来处理作用域的内存管理。栈是一种简单的数据结构,也被称为后进先出队列或LIFO。栈支持两种操作:push,用于存储新值,和pop,用于移除并返回最近存储的值。
我们可以将栈想象成一堆盒子。如果我们想取出顶部盒子中存储的东西,我们只需将其取下并查看里面的内容。然而,如果我们想取出下面某个盒子中存储的东西,我们首先必须移除上面的盒子。以下是我所描述内容的示意图,其中下面的盒子被上面的盒子阻挡,无法访问:

当 Rust 块表达式开始时,它会记录栈的高度,当块结束时,它会从栈中移除东西,直到栈的高度与开始时相同。在中间,当块需要存储新值时,它会将那个值推入栈中。
当一个值从栈中移除时,Rust 编译器还会确保在丢弃值之前进行任何必要的清理,包括如果定义了自定义清理函数,则调用该值的自定义清理函数。
大多数编程语言都会这样做,但并非全部。在 Rust 中,即使数据值使用堆内存,它也会在栈上表示并由所有权规则控制。通过遵循这个简单的程序,Rust 可以高效地处理程序的记录保存和内存管理,而且不需要垃圾回收。
垃圾回收是许多编程语言中用来减轻程序员内存管理负担的一种机制。它甚至比 Rust 的方法更容易使用,但它确实需要时间来运行垃圾回收机制,这可能会影响程序性能。Rust 的方法在编译时几乎完全是确定性的:Rust 编译器知道何时分配和释放内存,而无需在程序运行时进行推断。
转移所有权
将值的所有权转移到不同作用域是可能的(并且很常见)。例如,我们可以这样做:
{
    let main_1 = Point2D {x: 10.0, y: 10.0};
    receive_ownership(main_1);
    receive_ownership(main_1); // This will cause a compiler error!
}
正在发生的是,main_1 变量在当前作用域(值被推入栈)的所有权下创建和初始化,但当值作为函数参数使用时,所有权就转移到了构成 receive_ownership 函数体代码块的作用域。编译器知道当前作用域不再负责清理存储在 main_1 中的值,因为这项工作现在属于不同的作用域。
表示栈上值的字节被复制到栈上的新位置,在接收所有权的范围内。然而,大多数数据值将它们的一些信息存储在栈外,因此,在旧作用域中留下的字节被认为不再有意义或安全使用。
如果我们尝试在将 main_1 移动到不同作用域之后使用其存储的值,就像我们在对 receive_ownership 的第二次调用中所做的那样,编译器将报告错误。不仅仅是将值用作函数参数会导致错误,任何对已移动值的任何使用都是错误。它不再存在以供使用。
所有权也可以向相反的方向转移。这个函数接收其参数的所有权,但随后将参数(以及所有权)返回到调用它的代码块:
pub fn receive_ownership(point: Point2D) -> Point2D {
    println!("Point2D{{x: {}, y: {}}} is now owned by a new scope", point.x, point.y);
    return point;
}
这并不意味着原始变量(main_1)再次变得可使用,但如果我们将函数的返回值赋给一个变量,我们就可以通过这个新变量继续使用这个值。
所有权也可以通过将值赋给不同的变量“横向”转移。我们做类似这样的事情:
let mut main_4 = main_2;
在这里,存储在main_2中的值被移动到main_4。在这个基本示例中,这并不特别有趣;我们只是得到了一个新变量,它包含旧变量曾经包含的值,而且它们都在同一个作用域内。当我们像将值赋给结构体成员那样做事情时,这会更有趣,尤其是当结构体具有不同的生命周期时。
Rust 的编译器对所有权非常小心,当它检测到所有权没有得到适当尊重的情况,或者甚至可能没有得到适当尊重的情况时,它会报告错误。以下函数将无法编译,因为它仅在switch参数为false时有效:
pub fn uncertain_ownership(switch: bool) {
    let point = Point2D {x: 3.0, y: 3.0};
    if switch {
        receive_ownership(point);
    }
    println!("point is Point2D{{x: {}, y: {}}}", point.x, point.y);
}
当我们尝试编译uncertain_ownership函数时,编译器会输出如下信息:

对于编译器而言,如果我们能够在使用它之前移动该值,我们就无法使用它。
复制
在本章“转移所有权”部分的末尾讨论的编译器错误中,我们看到编译器指出数据值被移动,因为它没有实现Copy特质,这很有趣。这意味着什么呢?
对于某些数据类型,尤其是像整数和浮点数这样的原始类型,复制表示它们的栈上的字节就足以实际上制作一个完整的工作副本。换句话说,它们的表示不引用内存中存储的其他任何内容,也不依赖于所有权来保持一切正确。
标准库中有许多数据类型在内存使用方面可以具有Copy特质,但利用所有权来保持其他事物的安全性和正确性。例如,表示对文件或网络套接字等外部资源访问的数据类型,以及与并发有关的数据类型。所有权已经证明比最初预期的更强大的工具。
完全不依赖所有权的那些数据类型被称为具有Copy特质。我们将在第八章,“重要标准特质”中看到如何声明我们的数据类型具有Copy特质。
当一个值的数据类型具有Copy特质时,Rust 在转移值时不会移动该值。接收者仍然接收该值,但旧值仍然有效。而不是移动,值已经被复制。这个函数的结构几乎与uncertain_ownership函数完全相同,后者拒绝编译:
pub fn copied_ownership(switch: bool) {
    let local = 4.0;
    if switch {
        receive_ownership(Point2D {x: local, y: 4.0});
    }
    println!("x is {}", x);
}
这里重要的区别是local包含一个浮点值,而浮点数据类型具有 Copy trait,这意味着尽管local的值被放置在Point2D结构体内部,并且该结构体随后被移动到receive_ownership函数的作用域中,local在当前作用域中仍然有效。这是因为local的值并没有被移动到Point2D中。它是被复制的。
我们使用结构初始化器来分配local的值,而不是使用=符号,这并不会造成任何区别。无论是哪种方式,它都是一个赋值操作,并且无论是哪种方式,数据类型的 Copy trait 都会决定赋值是复制还是移动。
借贷
我们还可以通过借贷的方式将信息发送到不同的作用域。当我们移动一个数据值时,接收的作用域成为该值的新所有者。当我们复制一个数据值时,接收的作用域拥有它接收的副本,而发送的作用域保留对原始值的所有权。当我们借贷一个数据值时,事情可能会变得更加复杂,因为原始作用域保留了所有权,但接收的作用域仍然可以访问数据。
原始作用域仍然拥有数据,这意味着当该作用域结束时,数据将会消失。如果在那时作用域中包含的一些数据仍然借给了不同的作用域,程序可能会崩溃,由于 Rust 编译器讨厌潜在的崩溃,它不允许我们陷入那种情况。相反,它要求在拥有作用域的时间结束之前,必须返回任何借用的信息。
当一个数据值被借用时,该值既没有被复制也没有被移动。表示该值在栈上的字节仍然保持在原来的位置。相反,借用人接收这些字节在栈上的内存地址,这使得它可以违反栈的概念性想法,通过访问存储在顶部以下的信息,可能在完全不同的作用域中。你可以看到为什么编译器想要对此小心谨慎!
当前借用的数据值不能被所有者更改,即使数据值存储在一个可变变量中。这是防止借贷引起问题的部分:数据值最多只能在一个地方更改一次,并且当它可以更改时,它永远不会在其他地方被使用。
不可变借贷
在借贷时,默认情况下是进行不可变借贷,这意味着借用的数据可以读取,但不能更改。我们可以同时向多个借用人进行不可变借贷,这是安全的,因为它们中没有人可以更改借用的数据,因此它们不能通过意外更改数据值来相互干扰。
要创建一个不可变借用,我们需要在产生数据值的表达式前加上&,如下所示:
borrow_ownership(&main_3);
在这里,我们调用一个名为borrow_ownership的函数,并向它传递从main_3变量借用的数据值。
可变借贷
有时,我们希望借出一个数据值并允许接收者修改它,以便在借用结束后,拥有作用域中的数据值已发生变化。当我们需要这样做时,我们以可变的方式借出数据。
我们只能在借出的数据值存储在可变变量中时才能以可变方式借出,这意味着我们在声明变量时使用了mut关键字,如下所示:
let mut main_4 = main_2;
由于我们有一个可变变量可以借出,我们可以通过使用mut关键字再次,在不同的上下文中创建该变量值的可变借用:
borrow_ownership_mutably(&mut main_4);
如果有一个数据值的可变借用,则无法为其创建另一个借用(无论是哪种类型),如果存在任何不可变借用,则创建可变借用是不可能的。这条规则意味着如果一个数据值是可变借用的,那么它不会在其他任何地方被借用。结合我们之前讨论的规则,该规则防止借用数据被其实际所有者更改,这意味着只要存在一个活动的可变借用,那么这个借用就是修改借用数据值的唯一方式。
然而,一旦借用结束,所有者就会重新获得对(可能已修改的)数据值的控制权。
访问借用数据
要接收借用数据,我们需要正确指定数据类型作为借用。这是通过在接收端使用&或&mut与数据类型一起完成的,就像我们在发送端使用数据值一样。
虽然Rust中常用术语借用,但技术术语是引用。因此,我们通常会说我正在借用数据,使用借用数据,或者一个数据值作为借用被访问,但我们也可以说我们在引用数据,使用引用数据,或者一个数据值通过引用被访问。
在这里,我们有两个函数的定义,与我们在之前的示例中使用的相同两个函数。看看每个函数中为point参数指定的数据类型:
pub fn borrow_ownership(point: &Point2D) {
    println!("Point2D{{x: {}, y: {}}} is now borrowed by a new scope", point.x, point.y);
}
pub fn borrow_ownership_mutably(point: &mut Point2D) {
    println!("Point2D{{x: {}, y: {}}} is now borrowed by a new scope", point.x, point.y);
    point.x = 13.5;
    println!("Borrowed value changed to Point2D{{x: {}, y: {}}}", point.x, point.y);
}
作为借用或可变借用是参数数据类型的一部分。这意味着编译器知道传递给参数的值必须是一个借用,并且会拒绝编译尝试将非借用值传递给函数的代码。
大多数时候,使用借用值和使用非借用值相同,正如我们可以在这些函数中看到的那样。它们与point交互,就像它是一个局部拥有的变量一样。
然而,那是因为编译器很聪明。事实是,借用是一个数据值的内存地址,而不是数据值本身(这是存储在内存中的字节)。大多数时候,编译器可以弄清楚它需要采取额外的步骤,查找局部变量中的地址,然后在那个地址的内存中查找数据,而不是只在局部变量中查找数据。
这个过程被称为解引用。*由于某种原因,没有人说还借。
有时候编译器无法确定我们想要解引用并自动处理它。在这些情况下,我们可以使用*符号手动解引用一个借用值。
这种情况最常见的地方是在对借用值进行赋值时。如果借用值是一个结构体或者具有内部数据的某种东西,我们可以毫无问题地对内部数据进行赋值,但当我们想要对一个借用变量赋予一个全新的值时,我们需要使用解引用。
这段代码试图像它不是一个借用一样对值进行赋值:
pub fn set_to_six(value: &mut u32) {
    value = 6;
}
我们看到value是一个 32 位无符号整数的可变借用。当我们尝试直接对该变量进行赋值时,编译器会告诉我们这一点:

这里发生的情况是,编译器无法区分我想将这个值赋给内存中引用位置存储的内容和我想让这个引用变量指向不同的内存位置。它需要假设其中之一,并让我们告诉它如果我们想要另一个,它选择的假设是第二个。
将这个变量赋予一个新值作为默认值是有意义的,因为在任何其他情况下,=都代表这个意思。处理借用数据是一个特殊情况,而不是默认情况。
编译器在这里给出的建议也是基于这样的假设:我们想要的是value指向一个新的内存地址,这意味着如果我们盲目地遵循它,编译器错误会消失,但程序不会做我们想要的事情。它不会在借用变量中存储数字 6,而是会将value变量设置为包含一个新的借用。
我们真正想要做的是这样:
pub fn set_to_six(value: &mut u32) {
    *value = 6;
}
这告诉编译器,我们不是要对value进行赋值,而是要通过value对最初借用的变量进行赋值。
*符号可以用来读取和写入借用值,即使不是严格必需的,如果我们想明确表示也可以使用。
尽管解引用和乘法使用的是相同的符号,但编译器永远不会将它们混淆。乘法在借用上不是一个有效的操作,而解引用在数字上也不是一个有效的操作。此外,乘法总是需要在*的两侧都有数据值,而解引用只需要在一边有数据值。在这两块信息之间,编译器有足够的信息知道我们要求的是哪种操作。
借用数据的生命周期
借用不能比它们借用的数据值存在的时间更长。Rust 编译器必须确保程序中没有任何部分允许这种情况发生,这意味着它必须跟踪每个借用的生命周期。在我们迄今为止看到的例子中,这很简单,因为每个借用都是在调用函数时创建的,并在函数返回时结束,而借用的值则一直存活到包含函数调用的代码块表达式结束。
借用的生命周期显然比变量的生命周期短,开始得晚,结束得早。
然而,创建需要我们向编译器提供有关借用存在时间或借用值有效时间提示的情况并不困难。我们之前已经见过一次,当时我们在Result中将&'static str用作错误类型。正如我们所知,这是一个对str的不可变引用,但还有那个'static部分需要理解。
当我们在&符号后面写上'static或'a时,我们是在告诉 Rust 那个引用的生命周期有一个名字,它通过所有生命周期名称都以'符号开头来识别。如果我们说一个借用的生命周期被命名为'a,那么我们可以在其他地方使用那个名字来描述该生命周期与其他借用生命周期的关系。
静态生命周期是特殊的,因为它用于始终可用的数据值,只要程序在运行,例如我们在之前的示例中用作错误信息的字符串常量。
当我们定义函数时,给生命周期命名最有用,因为我们不知道将要填充到函数参数变量中的数据值是什么。如果其中一些参数是借用,我们需要能够告诉 Rust 我们对这些借用生命周期的期望,以便它可以确保调用我们的函数的代码是正确进行的。
这里有一个 Rust 无法安全编译的函数,因为它需要比我们告诉它的更多关于生命周期的信息(尚未告知):
pub fn smaller_x(value1: &Point2D, value2: &Point2D) -> &f64 {
    if value1.x < value2.x {
        &value1.x
    }
    else {
        &value2.x
    }
}
这里的问题是,在这个函数中我们接收了两个借用参数,每个参数可能具有不同的生命周期,并且返回另一个借用值。不幸的是,Rust 编译器不知道返回值将借用哪个参数或其生命周期是什么,因此它无法正确检查调用我们的smaller_x函数时该值的用法。由于它无法确定一切是否正确,编译器简单地拒绝尝试。
我们可以通过添加生命周期注解来修复这个问题:
pub fn smaller_x<'a>(value1: &'a Point2D, value2: &'a Point2D) -> &'a f64 {
    if value1.x < value2.x {
        &value1.x
    }
    else {
        &value2.x
    }
}
我们在这里使用名称'a来表示所有三个借用值的生命周期,并且还在<和>之间、函数名称和参数列表之间放置了'a。<和>标记了函数的泛型参数列表的开始和结束,我们将在第七章中更详细地讨论,泛型类型。现在,重要的是我们要告诉 Rust 存在一个生命周期,它等于或短于value1和value2的实际生命周期,称为'a,并且返回值可以在那个'a生命周期内安全使用。
指定一个生命周期名称永远不会改变借用实际的周期。如果value1和value2有不同的生命周期,在这里为它们指定'a不会使其中一个持续更长,也不会缩短另一个的范围。当应用于参数时,生命周期名称告诉 Rust 该命名生命周期必须与该参数兼容,这意味着该命名生命周期必须完全包含在参数的实际生命周期内。然后,当我们为返回值的生命周期使用相同的名称时,我们告诉 Rust 返回值将只保证在相同的限制内有效——在这种情况下,当两个参数仍然有效时。
Rust 使用这个保证来检查调用代码。如果我们尝试这样做,Rust 编译器会拒绝,因为我们试图以可能不正确的方式使用返回值,而 Rust 不处理可能的情况:
let main_4 = Point2D {x: 25.0, y: 25.0};
let smaller;
{
    let main_5 = Point2D {x: 50.0, y: 50.0};
    smaller = smaller_x(&main_4, &main_5);
}
println!("The smaller x is {}", smaller);
这是一个在{和}之间的块表达式,就像我们在第二章中看到的,Rust 语言基础,这意味着它有自己的作用域,它拥有main_5变量。这意味着当我们创建main_5的借用时,它的生命周期比main_4变量的借用短。Rust 查看smaller_x函数的定义,看到返回值只在main_4和main_5的生命周期内保证有效,因此尝试在块表达式结束后使用它会产生编译器错误。
这即使实际上main_4包含了smaller_x,也是一个编译器错误,因此返回值是当我们到达打印命令时仍然有效的值的借用。Rust 在检查生命周期时不会分析函数的逻辑,它只是查看我们告诉它的关于参数和返回的信息。
这是一件好事。在这种情况下,检查用于参数的值,认识到它们是始终产生相同行为的常量值,并逻辑推理出返回的借用的生命周期等于第一个参数的生命周期是可能的。然而,通常情况下,这种推理是不可能的(如果第一个参数是用户输入的怎么办?),尝试这样做只会造成问题。想象一下改变一个变量值的来源,突然在程序的其他某个不应该关心的部分出现编译错误!将这些事情作为函数接口的实体部分会更好。
所有权和 self 参数
如我们之前所见,当我们为类型实现行为时,我们定义的函数以 self、&self 或 &mut self 作为第一个参数。我们现在已经足够了解,这意味着 self 要么被移动(或复制)到函数的作用域中,要么被借用,或者可变借用。我们选择使用哪一种可能会有一些相当重要的后果。
self 的数据类型是隐式的:它必须是我们在其上实现函数的数据类型,因此,我们无法在参数列表中将数据类型指定为 self。由于没有数据类型可以前缀 & 或 &mut,我们可以在 self 前写它们。
在所有三种情况下,self 指的是 通过该函数被调用的数据值。如果我们有一个名为 x 的 u32 变量,并告诉 Rust 执行 x.pow(3),为 u32 实现的 pow 函数将接收 两个 参数:x 的值作为 self,以及 3 作为第二个参数。
移动、借用和可变借用 self 值的规则与应用于任何其他值的规则相同。如果我们当前有任何值的借用,我们无法将其可变借用到 self 中,也不能移动它(因为这会使得现有的借用无效)。如果我们当前有值的可变借用,我们无法借用它或将其移动到 self 中,因为可变借用不允许其他人借用或更改值。同样,将借用放入函数的 self 影响我们在其他地方访问数据的方式,因为这是一个借用,并且有关借用共存的规则。
移动 self
如果 self 值被移动到函数中,它就像移动任何其他值一样;我们不能再在它原来的地方使用它了。这里有一个函数:
impl Point2D {
    pub fn transpose(self) -> Point2D {
        return Point2D {x: self.y, y: self.x};
    }
}
这可以被认为是 消耗 self 值。当它被调用时,值被移动到函数的作用域中,而原来包含该值的旧变量就不再可用了。这个特定的函数返回一个新的不同的 Point2D 值,所以一旦这个函数运行完毕,self 的值就完全消失了。
有一些原因可能正是我们想要的这种行为。在前面的例子中,函数将self值转换成新的东西,这体现在它消耗了旧值。
函数消耗self的一个非常常见的用途是构建器模式。这是 Rust 中的一个设计模式,我们通过填充一个构建器结构中的值来逐步构建复杂的数据结构,然后调用在构建器结构上实现的构建函数来构建最终的数据值。大多数时候,构建函数会消耗其self,因为每个构建器值应该只用来构建一个最终值。
构建器模式本质上是一种使用 Rust 语法来实现某些其他语言中通过关键字参数和默认值实现相同功能的方法。
任何时候self的值会因为函数的操作而被无效化,无论是字面意义上的还是概念上的,将self移动到函数的作用域内都是有意义的。
借用self
如果将self值不可变借用到一个函数中,那么该函数只能对该值进行只读访问。这在许多情况下很有用,因为它允许我们调用该函数而不需要为它复制self值,也不需要编译器确保写入访问规则得到维护。
这里是一个示例函数,它不可变地借用了self:
impl Point2D {
    // ...
    pub fn magnitude(&self) -> f64 {
        return (self.x.powi(2) + self.y.powi(2)).sqrt();
    }
}
此函数返回 ,换句话说,是存储在
,换句话说,是存储在self中的点与坐标系原点之间的距离。
magnitude函数不需要改变self,因此没有必要使用可变借用并处理由此产生的限制。它可以用移动的self工作,但两次在同一个值上调用magnitude函数并没有什么问题,所以这也不是我们想要的。
使用不可变借用self通常是正确的选择。我们需要一个使用self或&mut self的理由,如果没有这样的理由,我们就使用&self。
可变借用self
有时候,一个函数需要改变它的self。对于这些情况,我们可以通过&mut self接收self作为可变借用。就像我们创建任何其他可变借用一样,我们只能调用这样的函数,如果我们有一个存储在可变变量中的self值,并且该值目前没有被其他地方借用。换句话说,我们只能在 ourselves 有写入访问权时调用具有写入访问权的函数。
这里,我们有一个示例函数,它可变地借用了self:
impl Point2D {
    // ...
    pub fn unit(&mut self) {
        let mag = self.magnitude();
        self.x = self.x / mag;
        self.y = self.y / mag;
    }
}
我们在这里看到了几个方面。首先,我们能够在self上调用只读的magnitude函数,尽管该函数接受一个不可变的self借用,而我们得到了一个可变的self借用。反之则不成立:如果我们试图在magnitude函数内部调用unit函数,编译器会拒绝允许这样做。
第二,由于我们有对self的写入访问权限,我们可以更改其中存储的数据。这就是写入访问的含义。
第三,我们为这个函数没有指定返回类型。技术上,它默认返回(),但这只是另一种说法,表示它没有返回任何有意义的内容。对于需要改变self的函数,不返回值或返回一个带有()作为成功值的Result是常见的做法,如果函数需要报告错误的能力。这是因为函数的真正结果是更新后的self值。
摘要
所有权是 Rust 与其他编程语言最显著的区别。一开始这个想法似乎很显然,然后出人意料地复杂,最后变得强大且有用。所有权为 Rust 提供了几乎免费的自动内存管理,以及诸如安全且易于多线程和并发等特性,以及通常能够在编译器中比其他语言发现更多错误的能力。
借用利用所有权来创建其他语言中最大的问题点之一的安全版本:通过内存地址访问数据。与内存地址相关的错误是程序遇到的最常见问题之一,在 Rust 中,这些错误会被编译器捕获,并附带有关如何解决它们的 helpful hints。
在本章中,我们还探讨了如何根据数据类型是否移动、借用或可变借用其self值来实现消耗性、只读或读写函数,并讨论了编译器在各种情况下可能报告的各种错误。
在下一章,我们将学习如何使用数据类型上的模式匹配来做出决策。
第四章:通过模式匹配进行决策
我们已经看到了 Rust 的if表达式,但那些是基于数据值进行决策的。Rust 是一种非常注重类型的语言,因此能够基于数据类型进行决策也非常重要。Rust 的match和if let表达式允许我们这样做,比较复杂的数据类型,并允许我们从匹配的模式中提取数据值以进行进一步处理。
在本章中,我们将做以下事情:
- 
学习如何在 let语句的上下文中使用模式匹配进行变量赋值
- 
将我们关于模式匹配的知识应用到使用 if let表达式进行决策中
- 
使用 match表达式来选择众多可能模式中的确切一个
- 
在模式匹配中使用无关紧要的值 
- 
看看借用如何与模式匹配交互 
- 
学习如何匹配复杂、嵌套的数据结构 
使用模式匹配进行变量赋值
我们已经多次看到如何在 Rust 中分配变量:我们做类似let x = y;的事情,这告诉 Rust 创建一个名为x的新变量,并将存储在y中的值移动或复制到它里面,具体取决于数据类型。
然而,这实际上只是 Rust 真正所做事情的一个简化案例,即匹配一个模式到一个值,并从匹配的模式中提取数据值以存储在目标变量中,如下例所示:
pub struct DemoStruct {
 pub id: u64,
 pub name: String,
 pub probability: f64,
}
// ...
let source1 = DemoStruct { id: 31, name: String::from("Example Thing"), probability: 0.42 };
let DemoStruct{ id: x, name: y, probability: z } = source1;
好吧,刚才发生了什么?首先,我们有一个结构定义。我们之前见过这些,这里唯一的新东西就是我们正在使用String数据类型。
String与str有一个有趣的关系。当我们使用str时,我们几乎总是实际上使用一个借用,比如&str或&'static str,而不是普通的str。这是因为普通的str在栈中没有固定的大小,这使得我们想要做的许多事情都无法编译。因此,我们使用&str,它确实有一个固定的大小。但是,使用引用作为数据结构中的包含值也打开了基于生命周期的所有 sorts 的限制,所以我们并不真的想在这里使用pub name: &str。幸运的是,我们可以使用String代替。当需要时,String可以伪装成&str,但它实际上不是一个借用,所以所有权是直接的。然而,使用String稍微低效一些,所以一般规则是当String解决问题时使用它,其余时间使用&str。
然后,我们使用DemoStruct类型创建一个新的数据值,包含其三个包含值。我们也见过这种情况。
在示例的最后一行我们究竟在做什么?DemoStruct{ id: x, name: y, probability: z }是一个模式。我们正在告诉 Rust 我们期望分配的值是一个DemoStruct,并且它的包含值应该依次与x、y和z子模式匹配。
当我们使用变量名作为模式时,它会匹配任何值,并将该值分配给该名称,这正是这里发生的事情。这也是简单的 let x = 5 发生的事情。因此,x、y 和 z 最终成为包含之前存储在 source.id、source.name 和 source.probability 中的值的新的变量。
虽然我们不需要为子模式使用变量名。例如,我们可以尝试这样做:
DemoStruct{ id: 31, name: y, probability: z } = source1;
然而,如果我们这样做,编译器将报告一个错误。错误不是因为 31 是一个无效的模式。这是一个非常好的模式,并且恰好匹配我们实际上会找到的值。不过,编译器会拒绝编译它,因为它没有匹配源值的所有可能性,而 Rust 不允许可能因为变量值改变而失败的 let 语句。想象一下这可能会造成多大的麻烦!
Rust 编译器将模式匹配时能够处理所有可能性称为 覆盖。
对于可能匹配或不匹配输入值的模式,我们可以使用 if let 表达式。
使用 if let 表达式来测试模式是否匹配
使用模式匹配将值解包到多个变量中可能很有用,但使用模式匹配来做决策才是这个功能真正发光的地方,如下面的例子所示:
let source2 = DemoStruct { id: 35, name: String::from("Another Thing"), probability: 0.42 };
let source3 = DemoStruct { id: 63, name: String::from("Super Thing"), probability: 0.99 };
if let DemoStruct { id: 63, name: y, probability: z } = source2 {
    println!("When id is 63, name is {} and probability is {}", y, z);
}
if let DemoStruct { id: 63, name: y, probability: z } = source3 {
    println!("When id is 63, name is {} and probability is {}", y, z);
}
在这里,我们定义了包含 DemoStruct 值的两个更多变量,然后使用模式匹配将它们拆分开来,并将它们的值分配给单独的变量。不过,这次我们在 if let 表达式中而不是在 let 表达式中这样做。这有很大的不同,因为现在模式不需要覆盖所有可能的输入值域。如果模式匹配成功,if let 表达式就会运行其块中的代码。如果模式不匹配,那么 if let 表达式就不会运行代码。它是条件性的。
由于模式不需要覆盖整个域,这意味着我们可以使用 63 作为模式来匹配 id 值,这没有什么问题。同样的原则适用于更复杂的模式或任何只匹配可能由其匹配的数据类型表示的值的子集的模式。
我们可以将 if let 与正常的 if 和 else 表达式结合,以创建更复杂的决策结构,有几种方法可以实现。
首先,我们可以在 if、else if 或 else 表达式的块中放置一个 if let 表达式,反之亦然。这是自然而然的,因为那些块表达式没有什么不寻常的地方——并没有因为它们在条件表达式中而被放置特殊的限制。
其次,我们可以将 if let 或 else if let 与 if、else if 和 else 结合到同一个选项链中。这看起来是这样的:
    if false {
 println!("This never happens");
 }
 else if let DemoStruct{ id: 35, name: y, probability: z } = source4 {
 println!("When id is 35, name is {} and probability is {}", y, z);
 }
 else if let DemoStruct{ id: 36, name: y, probability: z } = source4 {
 println!("When id is 36, name is {} and probability is {}", y, z);
 }
 else {
 println!("None of the conditions matched");
 }
链必须以if或if let表达式( whichever one we need)开始,然后可以有任意数量的else if或else if let,最后是一个我们需要的else if表达式。
尽管如此,我们仍然只是使用模式匹配从我们的结构中提取数据值。当模式与其他数据类型匹配时,我们可以做更多的事情。一个重要的是我们之前讨论过的Result数据类型。记住,Result可以是Ok或Err,无论哪种方式,它都包含一个值,要么是结果值,要么是某种错误值。我们之前看到如何使用?或各种函数来处理Result,但我们也可以使用模式匹配来处理它,这通常是我们选择的方式。
因此,这里有一个函数为我们构建DemoStruct值,但它只在我们请求的id值是偶数(除以二的余数为零)时才这样做。这个函数给我们一个包含创建的DemoStruct值或错误信息的Result:
pub fn might_fail(id: u64) -> Result<DemoStruct, &'static str> {
    if id % 2 == 0 {
     Ok(DemoStruct { id: id, name: String::from("An Even Thing"), probability: 0.2})
 }
 else {
 Err("Only even numbers are allowed")
 }
}
如果我们调用该函数,我们可以使用模式匹配来确定它是否成功或失败:
    if let Ok(x) = might_fail(37) {
        println!("Odd succeeded, name is {}", x.name);
    }
    if let Ok(x) = might_fail(38) {
        println!("Even succeeded, name is {}", x.name);
    }
在这里,我们两次调用might_fail,一次使用奇数作为参数值,一次使用偶数。两次都使用模式匹配来检查结果是否为Ok,如果是,就将包含的值赋给一个名为x的变量。
Ok不是一个数据结构,Err也不是。我们将在下一章中了解更多关于它们的信息。现在重要的是,模式匹配给我们提供了一个简单的方法来检查Result是否表示成功或失败,并且可以轻松地处理一个或两个情况。
使用match选择多个模式之一
你可能已经注意到,在我们的前一个例子中,我们没有处理函数返回错误值的情况。部分原因是使用if let处理这种情况有点尴尬。我们可以这样做:
if let Ok(x) = might_fail(39) {
    println!("Odd succeeded, name is {}", x.name);
}
else if let Err(x) = might_fail(39) {
    println!("Odd failed, message is '{}'", x);
}
但这样会不必要地运行函数两次,所以这是低效的。我们可以通过这样做来修复它:
let result = might_fail(39);
if let Ok(x) = result {
    println!("Odd succeeded, name is {}", x.name);
}
else if let Err(x) = result {
    println!("Odd failed, message is '{}'", x);
}
这样更好,但变量是用来存储信息的,一旦我们检查了函数的成功或失败,就不再需要result值了,所以没有必要继续存储它。
我们可以使用match表达式来处理这种情况,以获得最佳结果:
match might_fail(39) {
    Ok(x) => { println!("Odd succeeded, name is {}", x.name) }
    Err(x) => { println!("Odd failed, message is '{}'", x) }
}
match表达式会将单个值(在这种情况下,调用might_fail(39)的结果)与多个模式进行匹配,直到找到一个成功匹配该值的模式,然后运行与该特定模式关联的代码块。模式是从上到下进行匹配的,因此通常我们将最具体的模式放在前面,最通用的模式放在后面。
match表达式中的单个模式不需要涵盖所有可能的价值,但所有模式加在一起需要涵盖。
Ok、Err以及假设的Dunno(例如),那么我们之前的匹配表达式将无法编译,因为我们没有告诉它在Dunno的情况下应该做什么。
这与一系列的if let和el不同
se if let,可以自由忽略他们想要的任何可能性。如果我们使用match,编译器会告诉我们是否遗漏了可能性,所以当我们打算处理所有选项时,我们应该始终使用match。另一方面,if let用于挑选一个或几个特殊情况。
在模式中使用“无关紧要”
有时候,变量的名称在模式中匹配任何值的技巧可能很有用,但我们实际上并不需要存储在变量中的信息。例如,在匹配Result时,我们可能不在乎错误值,只在乎确实发生了错误。在这种情况下,我们可以使用_符号来表示“我不在乎这个值是什么”:
match might_fail(39) {
    Ok(x) => { println!("Odd succeeded, name is {}", x.name) }
    Err(_) => { println!("Odd failed! Woe is me.") }
}
这就是为什么_本身不能用作变量名的原因:它有自己的特殊含义。我们可以将_与任何数据类型的任何数据值匹配,甚至在同一个表达式中多次匹配,匹配的值将被简单地忽略。
将值匹配到_上甚至不会移动或复制该值。当我们告诉编译器我们不在乎一个值时,它会相信我们。然而,在完整变量和“无关紧要”之间有一个中间级别。如果我们从一个以_开头的变量名开始,但在之后继续使用有效的变量名,那么这个命名变量不是一个“无关紧要”的变量,但它确实可以免于一些编译器的警告。
例如,通常情况下,如果我们将一个值放入变量中但随后没有对其进行任何操作,编译器会警告我们。将_放在变量名开头意味着编译器不会对此提出异议。当_单独使用时意味着“我不在乎这个值”,而以_开头的变量名意味着“即使我不使用这个值也行”。
这种用法的一个常见场景是我们为未来设计。我们可能预计一个函数参数或结构成员将在未来变得有用,所以我们现在就把它放进去,但还没有使用它。如果我们用_作为名称的前缀,编译器就不会为此而对我们大喊大叫。然后,当真正需要使用它的时候,我们移除_,这样我们就可以从编译器的所有检查中受益。
在前面的例子中,我们使用_来匹配错误值,这意味着我们不在乎错误值实际上是什么,只要它是错误即可。然而,_可以匹配任何东西,这意味着我们也可以这样做:
    match might_fail(39) {
        Ok(x) => { println!("Odd succeeded, name is {}", x.name) }
        _ => { println!("If none of the above patterns match, _ certainly will") }
    }
在这里,我们match中的最后一个模式是一个_,它匹配任何东西,但根本不捕获任何数据。这非常类似于在if链的末尾放置else。任何包含_模式的匹配表达式也会自动覆盖所有可能值的整个空间,这意味着只要存在一个合理的回退操作来处理没有更精确的模式匹配的情况,Rust 就不会向我们抱怨我们没有覆盖所有可能性。
顺便说一下,如果我们不在match表达式的底部而是其他任何地方放置一个普通_模式,Rust 会警告我们。这是好事,因为其下的任何模式永远不会有机会匹配。
模式匹配中的移动和借用
当我们匹配包含变量的模式时,匹配的数据值会被移动到变量中(除非它们的类型具有Copy特性)。例如,这会导致编译器报告错误,尽管乍一看这似乎是合理的,尤其是对于习惯于其他编程语言的人来说:
let source5 = DemoStruct { id: 40, name: String::from("A Surprising Thing"), probability: 0.93 };
if let DemoStruct {id: 41, name: x, probability: _} = source5 {
    println!("Extracted name: {}", x);
}
println!("source5.name is {}", source5.name);
问题在于,在if let之后,source5.name不再(或者至少可能不再)包含一个值,因为那个值已经被移动到x变量中。编译器不能确定最终的println!命令始终有效,这是一个问题,因为无论if let块是否运行,都会发生。
我们能否在if let中借用值,而不是移动它?这样,未来对该值的任何使用仍然有效。答案是肯定的,但我们需要解决一个问题。我们可以尝试这样做:
let source5 = DemoStruct { id: 40, name: String::from("A Surprising Thing"), probability: 0.93 };
if let DemoStruct {id: 41, name: &x, probability: _} = source5 {
    println!("Extracted name: {}", x);
}
println!("source5.name is {}", source5.name);
但我们发现编译器抱怨它期望在模式中是String,而找到的是一个引用。这是因为以这种方式在模式中使用&并不意味着我们想要借用值;这意味着我们期望该值在源数据中已经是一个借用。
为了告诉 Rust 我们想要借用一个由模式中的变量匹配的值,我们需要使用一个新的关键字:ref。它看起来是这样的:
let source5 = DemoStruct { id: 40, name: String::from("A Surprising Thing"), probability: 0.93 };
if let DemoStruct {id: 41, name: ref x, probability: _} = source5 {
    println!("Extracted name: {}", x);
}
println!("source5.name is {}", source5.name);
最后,编译器很高兴,我们也很高兴。source5.name中的值不是一个借用,但当我们将source5与我们的模式匹配时,我们将其借入x变量,这意味着它不是从source5中移出,并且最终的println!将始终有效。
ref关键字和&符号密切相关。这两行代码完全做同样的事情:
let ref borrowed1 = source5;
let borrowed2 = &source5;
它们的区别在于语法:我们将ref关键字应用于借用将要存储的变量,而我们将&符号应用于值已经存储的变量。我们根据我们正在编写的借用的哪一端来选择使用哪一个,我们不需要两者都使用。
事实上,同时使用这两种模式会创建原始值的借用借用,这通常不是我们想要的。Rust 编译器可以自动解引用任意层次的借用,以找到函数参数所需的价值,因此像这样的事情可以正常工作而不会引起任何错误:
pub fn borrow_demostruct(x: &DemoStruct) {
    println!("Borrowed {}", x.name);
}
let ref borrowed_borrow = &source5;
borrow_demostruct(borrowed_borrow);
编译器看到borrow_demostruct函数需要DemoStruct的借用,而我们试图传递给它的值是DemoStruct的借用借用,因此它对该值进行了一次解引用,并将其传递给函数参数。一切正常。
“借用借用”是什么意思?好吧,首先,我们有一个DemoStruct值。然后,我们借用了它,得到了一个&DemoStruct值。然后,我们又借用了那个*值,得到了一个&&DemoStruct值。
然而,计算机必须付出比必要的更多努力才能达到相同的结果。多层借用只有在解决问题时才应该使用,因为在不必要的时候使用它们只是浪费。
此外,&&DemoStruct实际上并不与&DemoStruct是相同的数据类型,尽管 Rust 编译器可以在将其用作函数参数时将其视为后者。有时这很重要。
匹配元组和更复杂的模式
匹配简单模式非常有用,但我们还能做更多。模式可以更复杂,由多层嵌套数据结构和其他数据类型的表示组成。模式可以在深入结构的同时将变量名分配给变量,以确保包含的信息符合我们的要求。或者模式可以被简化,只检查数据结构的一小部分,忽略其余部分。
嵌套模式
我们可以使用模式匹配从复杂的数据结构中提取值。只要模式匹配数据值,模式和价值有多复杂就无关紧要。如果我们想匹配元组元组并从内部元组中提取一个特定的值,我们可以这样做:
let (_, (_, x, _, _), _) = ((5, 6, 7), (8, 9, 10, 11), (12, 13, 14, 15));
println!("x is {}", x);
这个图案匹配任何有三个元素的元组,其中第二个元素是一个包含四个元素的嵌套元组,并将嵌套元组的第二个元素存储在x变量中,然后打印出x的值。
我们可以更加具体,将一些_替换为更详细的子模式以匹配。这将给我们一个更加关注外部元组的第一个和/或最后一个元素,或者内部元组的其他元素的图案。
我们可以使用同样的技术来深入其他数据类型,这不仅仅限于元组。例如,之前我们使用模式匹配来检查一个函数是否成功或失败地正确运行:
match might_fail(39) {
    Ok(x) => { println!("Odd succeeded, name is {}", x.name) }
    Err(_) => { println!("Odd failed! Woe is me.") }
}
在那段代码中,我们只是将成功值匹配到x变量,但如果我们想根据成功值的细节以不同的方式处理事情怎么办?我们可以通过使包含的值匹配一个更详细的子模式来实现这一点:
match might_fail(38) {
    Ok(DemoStruct {id: 38, name: ref name, probability: _}) => {
        println!("Even succeeded with the proper id: name is {}", name)
    }
    Ok(DemoStruct {id: ref id, name: ref name, probability: _}) => {
        println!("Even succeeded with the wrong id: id is {}, name is {}", id, name)
    }
    Err(_) => { println!("Even failed! Woe is me.") }
}
在这里,我们有一个模式,当函数返回成功并且成功值是一个具有正确 ID 的DemoStruct时匹配,第二个模式匹配当函数返回成功并且成功值是一个无论 ID 是什么的DemoStruct时,第三个模式匹配函数可能返回的任何错误。
第一个模式匹配预期的情况。如果它不匹配,第二个模式匹配,允许我们处理技术上报告为成功但意外的结果。如果这两个模式都不匹配,第三个模式处理显式错误。
如果我们编译这个示例,它将正常工作,但编译器会警告我们第一个模式中的name:和第二个模式中的id:和name:是冗余的。这是因为当我们初始化 Rust 中的数据结构或数据结构模式时,如果目标名称与源名称相同,我们可以省略目标名称。换句话说,第二个模式可以写成Ok(DemoStruct {ref id, ref name, probability: _}),Rust 仍然会理解它,因为id和name是结构包含的变量的名称之一。冗余警告只是告诉我们我们写多了。
存储匹配的值并将其与模式进行比较
通常情况下,我们要么使用变量名来匹配数据值的一部分,要么使用结构模式来检查它是否是正确的“形状”,并在该结构模式内部使用变量名来匹配和提取我们感兴趣的该结构的一部分。
然而,我们可以通过使用@符号同时进行这两项操作:
if let (1, x @ (_, _), _) = (1, (2, 3), (4, 5, 6)) {
    println!("matched x to {:?}", x);
}
因此,这里我们有一个匹配 3 元组且第一个元素为1,第二个元素为 2 元组,第三个元素为任何内容的模式,并将第二个元素(它已确认是一个 2 元组)存储在名为x的变量中。要存储的变量名在@之前,要检查匹配的模式在@之后。
忽略数据结构的大部分内容
一些数据结构包含大量数据值,如果需要在每个我们想要匹配的模式中逐一列出它们,将会非常不方便。幸运的是,Rust 有一个简写语法,意味着“其他所有内容都是无关紧要的”。
为了做到这一点,我们可以在模式的末尾包含..,如下所示:
if let DemoStruct {id: 40, ..} = source5 {
    println!("id is 40, don't care about the rest");
}
这与列出结构中包含的所有变量具有相同的效果,除了我们在模式中明确描述的那些,并将每个与_匹配。
注意事项
在模式匹配中,我们可能会尝试一些看似合理的事情,但它们可能不会像我们预期的那样工作。我们将查看这些情况,弄清楚它们实际上在做什么以及为什么 Rust 会那样工作。
并非所有值都可以与字面量模式匹配
在我们之前的所有例子中,当我们在一个模式中匹配 DemoStruct 时,我们将 probability 匹配到一个变量或 _。这是因为 probability 是一个浮点数,这意味着两个在功能上相同的值可能不会完全相等。
如果我们尝试在模式中使用浮点数字面量(在 Rust 1.29 中),我们会看到一个像这样的警告:

这只是一个警告,但正如警告所说,随着 Rust 的发展,它将成为一个错误。无论如何,我们都应该将其视为错误,因为即使它(目前)可以编译,模式可能也无法正常工作。
在这个例子中,原因是因为浮点数是近似的。它们必须适应有限数量的位,所以有时必须进行舍入。这可能导致在纯粹数学意义上应该相同的数字因为它们在最低有效位上的表示不同而不同。最低有效位通常造成的差异非常小,以至于舍入误差并不重要,但它们可能会破坏相等比较。
结果是,如果我们试图在一个不安全的模式中使用字面量,Rust 将会警告我们或给出错误。像往常一样,Rust 不愿意让潜在的问题不被注意。
如果我们需要做类似的事情,我们可以使用匹配守卫来绕过限制。我们即将学习它们,所以请继续阅读!
模式将值分配给变量名
当我们在模式中使用变量名时,它会匹配任何值,并且匹配的值会被存储在变量中。这意味着如果我们尝试这样做:
let x = 5;
let source6 = DemoStruct {id: 7, name: String::from("oops"), probability: 0.26};
if let DemoStruct { id: x, name: _, probability: _ } = source6 {
    println!("The pattern matched, x is {}", x);
}
我们没有得到一个将 source6.id 的值与 x 的值(在这个例子中是五个)进行比较的模式,我们没有得到我们期望的结果。
相反,我们会得到一个错误,说模式是不可反驳的:

不可反驳意味着模式永远不会失败匹配,这在 if let 表达式中是一个问题。
如果我们尝试一个类似的模式,它是可反驳的,但仍然使用 x 变量,程序可以编译,但模式匹配时我们并不希望它匹配:
let x = 5;
let source6 = DemoStruct {id: 7, name: String::from("oops"), probability: 0.26};
if let DemoStruct { id: 7, name: x, probability: _ } = source6 {
    println!("The pattern matched, x is {}", x);
}
这两种情况都源于我们之前提到的一条规则:在模式中使用的变量名匹配任何值,并将匹配的值存储为具有给定名称的新变量。如果我们仔细想想,这意味着如果已经存在具有该名称的变量,其当前值并不重要。
虽然这并不意味着我们完全无望,但我们可以使用 match 语法的扩展来涉及现有变量在决策中:
let x = 5;
let source7 = DemoStruct {id: 7, name: String::from("oops"), probability: 0.26};
match source7 {
    DemoStruct { id: y, name: _, probability: _ } if y == x => {
        println!("The pattern with match guard matched, y is {}", y);
    }
    _ => {
        println!("The pattern with match guard did not match")
    }
}
我们在这里所做的是将匹配守卫应用于模式。我们通过在模式之后但在=>之前放置if关键字,并随后跟上一个布尔表达式来实现这一点。这让我们能够向匹配分支添加非模式匹配标准。在这种情况下,我们说如果模式匹配并且 ID(存储在y中)与存储在x中的值匹配,我们应该运行那块代码。
有关于为if let创建类似功能的讨论,但大多数人只是使用嵌套的if表达式或match。
当我们使用匹配守卫时,我们需要特别注意我们的模式不要遮蔽我们想要在守卫中使用的任何变量名。这就是为什么在这个例子中,我们将DemoStruct的id与一个名为y的变量匹配,而不是x。我们需要保留x,以便我们的匹配守卫可以使用它。
概述
在本章中,我们看到了如何使用模式修补来增强我们做决策和分配变量的能力。特别是,我们学习了以下内容:
- 
如何通过将整个值与一个模式匹配,该模式匹配我们感兴趣的特定部分到一个变量名,将数据值的一部分分配给一个变量 
- 
如何使用 if let和else if let来决定一个if链的特定分支是否应该运行
- 
如何使用 match来检查单个值与多个模式
- 
如何在模式中使用 _作为无关紧要的元素
- 
匹配借用值和借用匹配值的模式之间的区别 
- 
如何匹配复杂、嵌套的数据结构中的模式 
- 
当我们使用模式匹配时可能出现的惊喜,以及如何处理它们 
在下一章中,当我们查看枚举、特性和特对象时,我们将看到更多的模式匹配。
第五章:一种数据类型表示多种数据类型
有时,一个数据值可能需要是多种不同数据类型之一。Rust 有三种方法来处理这种情况,同时不破坏严格的类型安全:枚举、特质对象和 Any。每种方法都有其优点和缺点,所以我们将逐一检查它们,并讨论何时适用。
在本章中,我们将学习以下内容:
- 
枚举是什么 
- 
如何创建枚举类型 
- 
如何创建枚举值 
- 
如何使用枚举值中存储的信息 
- 
特质和特质对象是什么 
- 
如何创建一个特质 
- 
如何创建一个特质对象 
- 
如何使用特质对象 
- 
Any是什么
- 
如何使用 Any
- 
枚举有什么好处,特质对象有什么好处, Any有什么好处
枚举
与结构体类似,枚举允许我们建立一个新的数据类型。尽管内容与结构体不同。当我们说一个变量的数据类型是枚举时,我们是在告诉 Rust,其包含的值必须是我们为该枚举描述的特定选择之一,不能是其他任何东西。
基本枚举
在 Rust 中,枚举是一种表示一组固定值之一的 数据类型。例如,我们可以定义一个表示彩虹中常见的七种颜色的枚举,如下所示:
pub enum Color {
    Red,
    Orange,
    Yellow,
    Green,
    Blue,
    Indigo,
    Violet,
}
一旦我们定义了这个枚举,我们就可以使用 Color 作为数据类型,以及 Color::Red 等类似的数据值:
let color: Color = Color::Green;
枚举是通过 enum 关键字创建的。注意,就像结构体一样,如果我们想让枚举在当前模块外部直接可访问,它需要 pub 关键字。在 { 和 } 之间,我们有数据类型的可能值列表,按名称列出,并用逗号分隔。
Rust 对大小写有些意见,所以如果枚举值以小写字母开头,它会警告我们,尽管这实际上不是一个错误。
参数化枚举
好吧,那么枚举与在一个变量中表示不同类型的数据有什么关系呢?嗯,枚举值还有一个可以改变一切的特性:参数。
假设我们想要创建一系列驾驶方向的表示,例如 向右转,向前行驶三个街区,或 停车。这个例子足够简单,我们可能可以用简单的枚举就能应付,但即使这样,使用参数也会更容易:
pub enum Drive {
    Forward(u8),
    Turn{slight: bool, right: bool},
    Stop,
}
我们这里有一个具有参数化值的枚举。Forward 值有一个 u8 参数(无符号 8 位整数),而 Turn 值有两个带有名称的 bool 参数。Stop 不需要任何参数。
实际上,Forward值携带一个 1 元组,而Turn值携带一个具有两个成员的结构。这就是为什么在Forward参数周围使用括号,而在Turn参数周围使用花括号的原因。我们是否希望参数是类似元组或类似结构的是我们的决定。无论如何,我们可以根据需要添加或删除参数。
现在,我们可以创建一个Drive类型的变量,它可能包含一个Forward、Turn或Stop参数。如果是一个Forward参数,这意味着该变量还包含一个u8数字,告诉我们需要驾驶多远。如果是一个Turn参数,这意味着该变量还包含一对布尔值,告诉我们是否向右或向左转弯,以及转弯是否轻微。换句话说,该变量可能包含几种不同类型的信息之一。
如果你熟悉 C++或类似的语言,请注意,Rust 枚举与 C++枚举不太一样。带有参数值的 Rust 枚举更像是一个 C++联合体,但它进行了类型检查并且是安全的。
更好的是,我们可以创建一个包含驾驶指令的数组,以表示一次完整的旅程:
let directions = [
    Drive::Forward(3),
    Drive::Turn{slight: false, right: true},
    Drive::Forward(1),
    Drive::Stop,
];
这个Drive值数组表示向前驾驶三个街区,向右转,再向前驾驶一个街区,然后停止。
检查值类型和提取参数值
当程序运行时,它可以根据它正在查看的具体枚举类型采取不同的行动,并且它可以访问存储为参数的数据值。我们已经看到了完成这项工作的最佳工具,即match:
    for step in directions.iter() {
        match step {
            Drive::Forward(blocks) => {
                println!("Drive forward {} blocks", blocks);
            },
            Drive::Turn{slight, right} => {
                println!("Turn {}{}",
                         if *slight {
                             "slightly "
                         }
                         else {
                             ""
                         },
                         if *right {
                             "right"
                         }
                         else {
                             "left"
                         }
                );
            },
            Drive::Stop => {
                println!("You have reached your destination");
            }
        };
    };
这里,我们有一个for循环,它逐个处理方向数组中的每个项目,从第一个开始。一步一步地,以下是正在发生的事情:
- 
我们要求 directions提供一个迭代器,它很乐意这样做。迭代器将逐个提供数组中每个值的借用。
- 
for循环从迭代器请求一个值,并将返回的借用分配给名为step的变量。
- 
匹配表达式使用 step变量来提供它将与之匹配的值。因为这个变量每次循环都会改变,所以匹配表达式每次都会与不同的值进行比较:- 
如果 step变量包含Drive::Forward的借用,其参数将被分配给名为blocks的变量。因为step是一个借用,所以blocks中的值也是一个借用,但在这个例子中这并没有造成重大差异。我们将其传递给println!,它调用一个函数将其转换为文本字符串,然后自动解引用。
- 
如果 step变量包含(对Drive::Turn的借用),则其参数的借用被分配给slight和right名称,并且我们可以使用之前看到的简写符号,因为slight和right是源和目标中变量的名称。然后是一个打印命令,但我们使用if表达式来决定打印什么。注意,我们明确地解引用了slight和right值;它们是借用,并且与调用函数不同,if表达式不会自动为我们解引用,所以我们需要自己来做。
- 
如果 step变量包含(对Drive::Stop的借用),则没有要处理的参数,我们只需打印一条消息。
 
- 
- 
如果迭代器中还有任何值,则回到步骤 2。 
非常酷。我们这里有一些做些实际事情的代码。那时真正的乐趣才真正开始!
结果是一个枚举,通过前缀访问
你可能已经注意到了,但带有Ok和Err值的Result类型看起来非常像枚举。这是因为它确实是一个枚举,这就是它能够根据它表示错误还是成功来包含不同数据类型的原因。它是一个具有泛型类型参数的枚举,所以在我们了解那些之前,我们无法实现完全相同的功能,但在其基础上,它仍然只是一个枚举。
当我们编写可能失败或处理此类函数的结果的函数时,我们可以使用Ok和Err而不是Result::Ok和Result::Err,因为这些值直接添加到 Rust 前缀中,方便我们使用。实际上,我们可以使用Result类型而不必说明它来自哪里,原因相同。
前缀是一个包含非常基本和有用的数据类型、值和特性的集合,它自动在每个 Rust 模块中可用。它包含诸如String、Vec(一个向量,它与数组的关系类似于String与str的关系),以及Box(我们将在下一章中讨论)等东西。
特性和特性对象
特性对象是 Rust 存储可能为几种可能类型之一的数据值到单个变量中的另一种机制,但在我们讨论特性对象之前,我们需要讨论特性。
特性
特性是为数据类型可能提供的一块特定功能命名和正式定义。之前,我们讨论了数据类型可能具有Copy特性,以及当它们具有该特性时,编译器会复制它们而不是移动它们。这就是一般想法:当特性为数据类型实现时,该数据类型就获得了以某种特定方式与程序的其他部分交互的能力。
一些内置的特性,如Copy,实际上会影响编译器与类型的交互方式,但在这里我们更感兴趣的是创建我们自己的特性。我们将在后面的章节中讨论这些内置特性。
这一切都很抽象,所以让我们通过查看我们之前用枚举解决的同一个“驾驶方向”问题来更加具体化。我们仍然希望能够有一个驾驶指令数组,并按顺序打印它们,所以让我们创建一个代表打印驾驶指令能力的特性:
pub trait PrintableDirection {
    fn forward(&self);
    fn reverse(&self);
}
trait关键字引入了一个特性,不出所料。在其中,我们有组成这个特性接口的函数签名。换句话说,如果一个数据类型将要拥有PrintableDirection特性,它需要提供针对该类型和特性的特定函数的实现。
只要每个函数是不同特性的一个部分,一个数据类型就可以有多个具有相同名称的实现函数。特性不需要担心与其他特性或基本数据类型发生名称冲突。
我们通过指定具有PrintableDirection特性的数据值知道如何以前往和返回的方向打印自身,或者至少这是我们希望forward和reverse函数的含义。
特性可以通过只提供签名来指定函数,就像之前一样,或者它们也可以通过简单地填写一个完整的函数定义来提供函数的默认实现。当特性为特定数据类型实现时,具有默认实现的函数不必实现,尽管如果需要,它们可以。
注意,特性内部函数的签名没有pub关键字。特性本身是公开的或不是,作为一个整体。在这种情况下(以及大多数情况下),特性是公开的,因此它所要求的函数自动是公开的。
实现我们的 PrintableDirection 特性
特性不是独立存在的;它们是数据类型拥有的东西。因此,我们首先需要做的是创建数据类型来表示各种驾驶方向:
pub struct Forward {
    pub blocks: u8,
}
pub struct Turn {
    pub slight: bool,
    pub right: bool,
}
pub struct Stop {}
我们在这里只是使用基本结构,但特性可以为任何数据类型实现。
这些结构包含的信息与我们之前在枚举参数中包含的信息相同。Stop结构很有趣,因为它为空。它不存储任何信息。它的唯一目的是作为一个数据类型。
现在,我们有一个特性和数据类型,但到目前为止,它们之间没有任何关系。让我们为这些类型中的每一个实现我们的特性:
impl PrintableDirection for Forward {
    fn forward(&self) {
        println!("Go forward {} blocks", self.blocks);
    }
    fn reverse(&self) {
        println!("Go forward {} blocks", self.blocks);
    }
}
impl PrintableDirection for Turn {
    fn forward(&self) {
        println!("Turn {}{}",
                 if self.slight {"slightly "} else {""},
                 if self.right {"right"} else {"left"});
    }
    fn reverse(&self) {
        println!("Turn {}{}",
                 if self.slight {"slightly "} else {""},
                 if self.right {"left"} else {"right"});
    }
}
impl PrintableDirection for Stop {
    fn forward(&self) {
        println!("You have reached your destination");
    }
    fn reverse(&self) {
        println!("Turn 180 degrees");
    }
}
这些实现块看起来与我们之前看到的类似,但这次它们不仅仅是说impl和类型名,而是要为类型名实现特性。然后,在内部,我们放置适用于该数据类型的特性函数的具体版本。
现在,Forward、Turn 和 Stop 数据类型各自都具有 PrintableDirection 特质,这意味着它们都知道如何将其包含的信息作为驾驶指令显示出来。
特质对象
Forward、Turn 和 Stop 仍然是三种不同的数据类型。没有表示 Forward、Turn、Stop 的数据类型,尽管我们可以使用枚举来创建一个,但还有另一种方法。有一个数据类型可以表示任何具有 PrintableDirection 特质的 any 数据类型的借用。它被写成 &dyn PrintableDirection,被称为特质对象引用。
我们不能只写像 let x: dyn PrintableDirection 这样的东西,并创建一个可以存储具有 PrintableDirection 特质的任何内容的变量。它需要是一个借用,或者某种存储包含在栈外数据的数据,例如我们将在下一章中看到的 Box 类型。
dyn 关键字代表动态分派,它意味着在大多数情况下看起来和表现像借用,实际上要复杂一些。借用本身存储的内存地址实际上是隐藏数据结构的地址。
那个隐藏的数据结构包含了实际借用地址以及实现借用数据值特质的函数的内存地址。当我们将一个数据值赋给特质对象时,Rust 会初始化那个隐藏的数据结构,当我们调用特质函数中的一个时,Rust 会查找在同一个隐藏数据结构中实际要调用的函数。
这意味着通过特质对象调用函数将始终需要计算机执行一些额外的步骤,与在具有具体类型的数据值上调用函数相比。另一方面,如果我们仔细思考,任何可能允许我们处理任意数据类型的机制都必须分配时间和内存来跟踪它正在处理的数据类型以及数据值存储的位置。如果我们从头开始尝试创建相同的功能,我们最终会做同样的事情。
使用我们的 PrintableDirection 特质
因此,这次我们不是创建枚举值的数组,而是创建一个特质对象引用的数组:
    let mut directions = [
        &Forward{ blocks: 5 },
        &Turn{ slight: true, right: false },
        &Forward{ blocks: 1 },
        &Turn{ slight: false, right: true },
        &Forward{ blocks: 2 },
        &Stop{},
    ];
不幸的是,如果我们尝试编译它,我们会得到一个错误,如下所示:

这次,Rust 不能仅通过查看我们分配给它的值来确定数组的数据类型,因为那个值看起来像是一个包含多个数据类型的数组,而这在我们甚至不能在 Rust 中做到。编译器不会去寻找它们都实现的特质,任意选择一个,并决定数组实际上是一个特质对象数组。这会比有帮助的情况更常见,所以我们必须告诉 Rust directions 变量应该有什么数据类型。
我们实际上告诉它directions是[&dyn PrintableDirection; 6]。这意味着它是一个PrintableDirection特质对象引用的数组,并且有空间容纳六个。现在,编译器知道如何正确解释我们的数组表达式:
    let mut directions: [&dyn PrintableDirection; 6] = [
        &Forward{ blocks: 5 },
        &Turn{ slight: true, right: false },
        &Forward{ blocks: 1 },
        &Turn{ slight: false, right: true },
        &Forward{ blocks: 2 },
        &Stop{},
    ];
现在,我们准备好实际打印出驾驶方向:
for step in directions.iter() {
    step.forward();
};
为了好玩,我们还会打印出回家的方向:
directions.reverse();
for step in directions.iter() {
    step.reverse();
};
这里有两个函数调用来反转,但它们并不是调用同一个函数。directions.reverse()调用调用的是在数组上实现的反转函数,它反转数组中存储的项目顺序。同时,step.reverse()调用调用的是反转函数,这是所有具有PrintableDirection特质的类型必须实现的,适用于步骤值的特定具体类型。这些函数碰巧有相同的名字,但它们根本不是同一回事。
当我们编译并运行所有特质对象代码时,我们得到如下输出:

哈哈,它工作了!
特质对象只提供对特质接口的访问
当我们使用特质对象时,我们唯一可以访问的原始对象的部分是特质定义的部分。这意味着我们可以调用forward和reverse函数,但我们无法直接访问Forward类型的blocks成员或Turn类型的slight成员等。特质对象只给我们提供它所代表的任何数据类型中保证存在的那些东西:特质的自身接口。
当我们思考时,这很有道理。如果我们要访问slight,但查看的值实际上是Forward类型,计算机应该做什么呢?一些语言会让我们尝试,如果我们在错误的时间这样做,程序就会崩溃,而其他语言会在程序运行时花费时间检查这些事情,并在发生错误时捕获它们,但都不是 Rust 的方式。在 Rust 中,如果编译器不能确定某件事是正确的,通常是一个错误。
这意味着特质的接口需要是完整的,从意义上说,你可以用具有特质的任何数据值合理做的事情都应该包含在接口中。这通常不是负担。毕竟,我们为什么要以任何其他方式呢?
我们一直在使用接口这个词。一些语言,如 Java,有一个实际上称为接口的功能,是的,Rust 特质与 Java 接口相似,尽管并不相同。
Any
Any是一个 Rust 中大多数数据类型自动实现的特质,这意味着我们可以在Any类型的特质对象中存储几乎所有东西。然而,正如我们之前提到的,我们只能根据特质的接口来访问特质对象中存储的值,所以Any接口让我们做什么呢?
Any 可以存储几乎所有东西
Rust 编译器会自动为任何数据类型实现Any,除非该数据类型包含非静态引用。所以,我们在特质对象部分使用的Forward、Turn和Stop结构已经自动实现了Any,但像这样的事情就不会:
pub struct DoesNotHaveAnyTrait<'a> {
    pub name: &'a str,
    pub count: i32,
}
更准确地说,当'a等于'static时,DoesNotHaveAnyTrait才只有Any特质,如果我们使用一个简单的字符串表达式,如this is a static string来初始化它,它就是'static,但如果我们使用其他机制来检索或构造一个&str值,则不是。
如果我们尝试做不可能的事情,编译器可能会给出关于生命周期而不是关于Any特质的错误,如下面的例子所示:

你看到注释了吗?导致错误的代码尝试创建一个&dyn Any,这告诉编译器'a生命周期需要与'static兼容,这告诉它wrong.as_str()的生命周期太短,因此它报告了一个错误。
通常,这并不是什么大问题,因为我们有其他几个原因避免在我们的数据类型中使用非静态引用,我们可以使用String、Vec、Box等来实现相同的结果。这只是我们需要记住的事情。
但要访问它,我们首先必须了解真实的数据类型
回到我们的例子,我们将创建我们的驾驶方向数组,并且我们会添加一些不是驾驶方向的东西,只是为了证明我们可以做到。
在这里,我们有一个包含驾驶方向和另一件事的Any特质引用数组:
use std::any::Any;
//...
pub struct DoesHaveAnyTrait {
 pub name: String,
 pub count: i32,
}
//...
let okay = String::from("okay");
let directions: [&dyn Any; 7] = [
    &Forward{ blocks: 5 },
    &Turn{ slight: true, right: false },
    &Forward{ blocks: 1 },
    &Turn{ slight: false, right: true },
    &Forward{ blocks: 2 },
    &Stop{},
    &DoesHaveAnyTrait{ name: okay, count: 16},
];
到目前为止,它看起来与我们的特质对象示例非常相似,这是有道理的,因为Any也是一个特质,而且这仍然是一个特质对象引用的数组。这里巨大的不同之处在于,我们向数组中添加了一个额外的项目,而且它与驾驶方向完全没有关系。
特质对象引用只给我们提供了访问特质函数的权限。现在我们有了Any特质对象引用的数组,Any提供了哪些函数让我们能够做些有用的事情?Any提供了两个重要的函数:有一个函数可以检查包含的值是否具有特定的数据类型,还有一个函数族允许我们提取包含的值。
作为第一个例子,我们将查看允许我们检查数据类型的函数:
    for step in directions.iter() {
        if step.is::<Forward>() {
            println!("Go forward");
        }
        else if step.is::<Turn>() {
            println!("Turn here");
        }
        else if step.is::<Stop>() {
            println!("Stop now");
        }
    }
这里有一些新的语法。当我们说step.is::<Forward>()时,我们是在说我们想要调用在(自动创建的)impl Any for Forward实现中定义的is函数。编译器知道我们在谈论Any,因为step是一个&dyn Any,但它不知道我们想要的是Forward版本而不是无数其他特定类型的Any实现之一,所以我们需要告诉它。
这种语法有点令人困惑,因为它与我们通常在use语句中写的顺序相反,但除此之外看起来很相似。不过读起来倒是挺顺的:如果步骤是向前几乎可以作为一个句子。
然而,这个版本有点令人不满意,因为打印输出完全基于数据类型,而没有考虑到数据值中存储的信息。我们可以用一个未参数化的枚举做得一样好。幸运的是,我们还可以使用Any的 downcast 函数来获取对引用值的访问:
    for step in directions.iter() {
        if let Some(x) = step.downcast_ref::<Forward>() {
            x.forward();
        }
        else if let Some(x) = step.downcast_ref::<Turn>() {
            x.forward();
        }
        else if let Some(x) = step.downcast_ref::<Stop>() {
            x.forward();
        }
    }
再次强调,我们正在告诉 Rust 我们想要调用来自特定类型实现的Any函数版本。在第一个if let分支中,我们要求Any给我们一个Forward数据值的引用。如果该值实际上是一个Forward数据值,该函数将返回一个名为Some的枚举值,其中引用作为其参数。如果该值不是一个Forward数据值,该函数将返回一个名为None的枚举值,它自然不匹配if let模式。
Some和None是Option枚举的可能值,这是预定义中包含的另一件事。它被广泛用于表示可能存在或不存在的数据值,尤其是在它们不是必须存在的情况下。在其他语言中,通常有一个特殊值,如NULL、null、None或nil,它可以分配给任何东西。Rust 的None只能分配给Option,这有助于编译器确保一切正确。
这个例子中的x变量实际上是Forward、Turn或Stop数据值的实际引用,因此如果我们进入其中一个if分支的代码块,我们就能够访问该数据类型可以执行的所有操作,而不仅仅是特定特质定义的功能。实际上,我们正在调用为这些类型在PrintableDirection特质中实现的forward函数,这是一个很好的证明我们拥有完全访问权限的例子。
注意,在使用is和downcast_ref时,如果不指定我们感兴趣的特定数据类型,就无法使用它们。如果我们尝试在不指定确切使用哪种数据类型的情况下使用这些函数,我们会得到一个类似这样的错误:

这意味着虽然Any可以存储几乎任何东西,但除非我们明确处理存储值的正确数据类型,否则我们无法访问存储的信息。在我们的例子中,我们没有 if 分支来处理DoesHaveAnyTrait值,所以数组中的最后一个值最终被忽略。
除了downcast_ref之外,Any特质还提供了downcast_mut,它给我们一个可变引用。在某些情况下,还可用downcast函数,它将值移动到我们的当前作用域而不是借用它。
这些技术的比较
Rust 社区倾向于使用枚举来解决单变量多类型问题。在运行时成本方面,简单的枚举是最有效率的,效率对 Rust 程序员来说很重要。
然而,使用枚举有一个缺点,那就是决定如何处理特定枚举值及其相关数据的match表达式(或类似)可能会分散在程序的源代码中。如果我们发现需要添加或删除枚举值,或者更改枚举值的参数,我们必须找到并更改所有这些匹配表达式。
如果我们决定在Drive枚举中添加一个Reverse值,匹配表达式就必须进行更改:

编译器会指出需要更新的每个match表达式,但它不会捕获需要类似更改的if let表达式的地方(因为if let只能处理一些可能性),因此这可能会成为一个重大问题。
相反,特质对象通过使不同数据类型的行为实际上成为数据类型的一部分,使我们能够将所有相关代码放在一起。它们还允许我们编写可以与尚未创建的数据类型一起工作的代码,但它们效率较低,因为计算机需要在程序运行时维护和使用隐藏的特质对象结构。
我们可能会认为通过创建一个枚举并在其上实现包含匹配表达式和针对枚举的每个值进行不同处理的函数,我们可以得到两者的最佳结合,并且在某种程度上,我们可以做到。然而,如果这些函数通过选择另一个枚举值特定的函数并调用那个来工作,我们只是再次重新创建了特质对象,但效率更低。
如果我们尝试以下方法来避免特质对象,我们最好还是使用特质对象:
pub enum LikeATraitObject {
    Integer(i32),
    Float(f32),
    Bool(bool),
}
fn handle_integer(x: i32) {
    println!("Integer {}", x);
}
fn handle_float(x: f32) {
    println!("Float {}", x);
}
fn handle_bool(x: bool) {
    println!("Bool {}", x);
}
impl LikeATraitObject {
    pub fn handle(&self) {
        match self {
            LikeATraitObject::Integer(x) => { handle_integer(*x); }
            LikeATraitObject::Float(x) => { handle_float(*x); }
            LikeATraitObject::Bool(x) => { handle_bool(*x); }
        }
    }
}
这并不是说这样的结构没有用,因为它们是有用的。然而,如果这样做唯一的原因是避免使用特质对象引用,那是一个错误。
可能看起来Any实际上是最佳选择,因为它可以存储如此广泛的值并且给我们提供了对存储值数据的完全访问,但通常其他选项中的一个是更好的。使用Any意味着我们需要在代码的各个地方检查所有可能性,就像使用枚举一样,而且与枚举不同,编译器在寻找需要更改的地方时不能提供任何帮助,因为,就像特质对象引用一样,没有定义的可能性的列表。在许多方面,Any是两者的最差结合。
虽然有些问题Any是正确的选择,尽管如此。如果我们真的需要处理一组无关的数据类型,我们需要Any。
摘要
好的,所以我们有三种不同的方法来处理相同的问题,每种方法都有其不同的优势和劣势:
- 
我们了解到枚举是最有效率的,尤其是在简单情况下 
- 
我们了解到特性对象引用可以产生最简单的代码,但代价是额外的开销 
- 
我们了解到 Any特性为我们提供了一种几乎可以引用任何事物的途径,但我们必须明确提取所需的信息类型
在下一章中,我们将学习如何将数据存储在栈外,以及为什么我们想要这样做
第六章:堆内存和智能指针
我们已经讨论了栈,以及它是 Rust 存储数据和跟踪需要保留和需要清理的内容的地方。这是一个强大而实用的机制,但并不适合所有情况。
假设我们有一个包含图像的变量。它占用几个兆字节的内存,我们需要在不同时间将它的所有权在程序的不同部分之间转移。如果我们只是将它放在栈上,并允许 Rust 根据需要将其移动到新的作用域,一切都会正常工作,但每次将值移动到新的所有者时,都需要复制这些兆字节的数据,这会减慢速度。
这不是存储信息在栈上不理想的情况的唯一场景,但它是一个很好的说明。
另一方面,我们最不想做的事情就是破坏栈和基于作用域的所有权模型,这是 Rust 获得其强大功能的原因。
幸运的是,有一种方法可以在栈外存储数据,同时让它表现得好像它是作用域的一部分:智能指针。
Rust 标准库包括几种不同类型的智能指针,旨在满足不同的需求。智能指针的值本身存储在栈上,就像其他数据值一样,但它们包括在创建时分配一块堆内存的必要工具,并在其生命周期结束时将其释放回系统。存储在该堆内存中的数据值可以通过智能指针访问,就像它存储在智能指针内部一样。
堆是栈的对立面。栈有一个特定的结构,有助于 Rust 跟踪在任何给定时间哪些操作是安全的,哪些是不安全的,而堆可以被视为无组织内存。一般来说,程序可以在任何时间请求为使用保留一段堆内存,并且可以在任何时间将其释放回系统。现在想象一下,当分配的堆内存部分分配得太晚,或者释放得太早,或者应该在释放时没有释放会发生什么。内存分配和释放的错误是程序崩溃的主要原因之一。
多亏了智能指针,存储在堆内存中的值的生命周期与遵循 Rust 正常规则的值的生命周期相匹配,但有一个大优点,即当智能指针移动到新的作用域时,不需要复制堆内存的部分。我们的多兆字节图像可以在作用域之间移动,只需移动几个字节,因为图像本身不需要移动,只需要控制它的智能指针。
Box
标准智能指针中最直接的是Box。Box做了我们之前讨论的事情:它在堆上存储一个数据值,同时确保它仍然遵循生命周期规则,就像它是Box值本身的一部分一样。
这里有一个例子。首先,我们将为要在堆上存储的数据创建一个数据类型:
pub struct Person {
    pub name: String,
    pub validated: bool,
}
现在,创建和使用Box本身是很容易的:
let jack = Box::new(Person { name: "Jack".to_string(), validated: true });
let x = &jack.name;
println!("The person in the box is {}", x);
第一行创建了一个Box。我们必须给它一个要存储的数据值,因为 Rust 不允许空Box,所以我们要初始化一个Person对象并将其传递给函数,该函数创建一个新的Box作为其内部值。
为什么 Rust 不允许空Box,或者任何其他类型的智能指针?因为如果允许,那么每次访问该智能指针的内容时,它都必须担心该智能指针是否指向已初始化的内存。要求只要智能指针存在,它管理的内存必须包含有效的数据值,这简化了许多事情,并使得一种常见的错误变得不可能。
一旦初始化了Box,我们就可以将其主要视为对包含数据的正常借用。我们可以通过解引用来将数据移回栈上:
let x = *jack;
这将Person值从名为jack的Box内部移动到x变量中,使得jack变得不可用。
我们也可以通过Box访问包含的数据值的数据和函数,就像它是包含数据的借用一样:
let x = &jack.name;
println!("The person in the box is {}", x);
在这里,我们要求借用jack.name到x,然后打印出那个名字。我们也可以通过以下方式得到相同的结果:
println!("The person in the box is {}", jack.name);
但这实际上是以非常不同的方式工作的。第一个例子是借用名称,然后打印出借用的String值。第二个实际上调用了一个名为jack.name.fmt的函数,它的self参数是一个不可变借用。这是因为 Rust 在解引用和函数调用方面非常智能。
fmt在哪里被调用的?答案是println!是一个宏,这意味着它实际上不是一个函数,而是像将一些代码直接粘贴到程序中一样。粘贴的代码调用fmt,所以这就像我们亲自调用了fmt一样。在 Rust 中,我们可以通过宏的名称总是以!结尾来识别它们,而函数名称则不会。
Box和可变大小
我们之前遇到过 Rust 需要确切知道特定数据值可以占用多少字节的必要性。大多数时候,Rust 可以解决这个问题,大多数时候这并不是问题,但有一些情况下,为数据值定义固定大小是不可能的。
一个基本的例子是数据结构,例如以下结构,其中实例包含它自己的其他实例:
pub struct TreeNode {
    pub value: i32,
    pub left: TreeNode,
    pub right: TreeNode,
}
乍一看这似乎是合理的,但 Rust 正确地指出,计算出的尺寸是无限的(因为TreeNode的大小是两个TreeNode的大小加上32位):

正如编译器建议的那样,我们可以用一个Box来修复这个问题:
pub struct TreeNode {
    pub value: i32,
    pub left: Box<TreeNode>,
    pub right: Box<TreeNode>,
}
现在,TreeNode的大小是两个Box的大小加上 32 位,这是完全合理的。
Box和Any
当一个变量的类型是 Box<dyn Any> 时,它表现得就像一个 &dyn Any,但增加了一个新特性。一个普通的 &dyn Any 有一个 downcast_ref 函数,我们可以使用它来获取包含值的引用,如果我们知道使用什么类型来提取它。现在,&dyn mut Any 增加了一个 downcast_mut,我们可以使用它来获取可变引用。当我们有一个 Box<dyn Any> 时,我们可以访问这两个函数,但也可以调用一个普通的 downcast 函数,将包含的值从 Any 移动到正确类型的变量中。这会消耗 Box 和 Any,并给我们返回一个新的包含数据值及其正确数据类型的 Box。
不要忘记,如果我们打算使用 Any 特性,我们代码中需要包含 use std::any::Any;。
我们可以几乎以创建 boxed Person 相同的方式创建 boxed Any:
let jill: Box<dyn Any> = Box::new(Person { name: "Jill".to_string(), validated: false });
这里的唯一区别是,我们告诉 Rust 我们想要 jill 变量包含一个 Box<dyn Any> 而不是让它自己决定变量包含 Box<Person>。
现在,要访问包含的 Person,我们可以这样做:
let real_jill = jill.downcast::<Person>().unwrap();
println!("{}", real_jill.name);
就像其他 downcast 函数一样,我们需要指定我们正在向下转换的具体数据类型。downcast 函数返回一个 Result,如果成功,则包含一个 Box<Person>。一旦我们有了 Box<Person>,我们就可以对它包含的 Person 值做任何我们想做的事情。
我们在这里调用的 unwrap 函数消耗一个 Result,如果成功,则返回其包含的值,如果失败,则终止程序并显示错误信息。我们使用 unwrap 来处理一个我们非常确信将会成功的 Result。
Vec 和 String
当数据值可能 改变 大小时,它几乎必须在堆上存储。因此,Rust 预定义中包含了 String 和 Vec 类型,它们分别是用于存储文本和可变长度数组的智能指针。
String
我们已经多次看到了 String,当我们使用它来简化文本字符串的所有权时。尽管如此,我们还可以用它做其他事情,因为存储在 String 中的文本可以更改。
在这里,我们像以下代码所示那样多次更改 String:
let mut text = String::new();
text.push('W');
text.push_str("elcome to mutable strings");
text.insert_str(11, "exciting ");
text.replace_range(28.., "text");
println!("{}", text);
让我们一步一步地看看:
- 
在第一行,我们创建了一个空的 String,并将其存储在一个可变变量中。它必须是可变的,因为我们将要改变存储的值。
- 
在第二行,我们正在向 String添加一个字符。
- 
在第三行,我们正在将一个 &str的全部内容追加到String中。
- 
在第四行,我们正在将一个 &str的全部内容插入到字符串的11字节偏移处。记住,Rust 从零开始计数,所以字符串中W的偏移量是0。
- 
在第五行,我们正在替换一系列偏移量中的字符,用一组新的字符序列来替换。我们使用的具体范围是 28..,这意味着从28开始的范围,一直延伸到无限(或者字符串的末尾,哪个先到算哪个)。
- 
最后,我们打印出所有操作的最后结果。 
我们在使用 String 的字节偏移量时必须小心,因为 String 类型总是使用 UTF-8 编码来存储文本。这意味着任何单个字符可能使用的字节数可以从一个字节少到四个字节。如果我们尝试使用位于字符中间的偏移量,程序将因错误信息而终止。String 和 &str 有许多函数可以让我们在 String 中找到有效的字节偏移量,或者在不使用偏移量的情况下操作它,例如 find、lines、split_whitespace、contains、starts_with、ends_with、split、trim 和 char_indices。
使用我们的 text 变量,&text 的数据类型可以是 &String 或 &str。Rust 的类型推断系统会根据将要存储值的变量的数据类型,或者将要分配给函数参数的数据类型等来做出决定。这也意味着,任何为 &str 实现的函数或者接受 &str 参数的函数也可以用在 String 上。例如,str 有一个 lines(&self) 函数,所以我们可以调用 text.lines()。此外,我们还可以将 String 作为文本参数传递给我们在本例中看到的 push_str、insert_str 和 replace_range 函数,就像它是一个真正的 &str 一样。
Vec
Vec 数据类型存储一个 向量,这是编程中常用的一个词,用来表示一维、可变长度的数组。像实际的数组一样,它们可以存储多个数据值,只要这些数据值都具有相同的数据类型。像 Strings 一样,Vecs 可以改变大小,因此它们是专门化的智能指针,它们在堆上存储包含的值。
要创建一个空的 Vec,我们可以使用 Vec::new(),如下所示:
let mut vector = Vec::new();
然后,我们可以使用 push 向它添加一个数据值:
vector.push(1.5);
现在,到目前为止,我们还没有提到向量可以包含哪种类型的数据,而 Rust 对此非常满意,因为我们不需要这样做。我们写的所有内容都与向量包含浮点原始数据类型之一是一致的,所以 Rust 就认为它包含的是这种类型。
如果我们做了不一致的事情,比如尝试在向量中存储一个 &str,会发生什么?
vector.push("nope");
现在 Rust 无法确定向量应该包含哪种数据类型,因此它拒绝编译:

然而,我们可以这样做:
let x: f64 = 99.9;
vector.push(x);
在这里,我们有一个名为 x 的变量,其数据类型为 f64。这与 Rust 之前能够识别的“某种浮点数”兼容,因此将其添加到向量中不会引起任何问题。实际上,它告诉 Rust,我们之前的 1.5 应该被视为 f64,并且该向量包含特定的 f64 值。
我们使用了数字作为那个例子,但只要我们遵循每个向量只包含一个数据类型的规则,Rust 就可以在 Vec 中存储任何数据类型。
将一个 &str 添加到我们的数字向量中是个问题,但我们可以无任何困难地创建一个 &str 向量:
let mut second_vector = Vec::new();
second_vector.push("This");
second_vector.push("works");
second_vector.push("fine");
我们可以使用与数组相同的语法来访问向量中的元素:
    println!("{} {} {}.", second_vector[0], second_vector[1], second_vector[2]);
Vec 实现了多个用于访问存储数据值的函数,如下所示:
- 
pop,它移除并返回向量中的最后一个项目
- 
remove,它从特定索引处移除项目
- 
insert,它在特定索引处添加一个项目,将位于该索引处的项目以及之后的所有项目向后推一个位置
- 
append,它将值从另一个向量中移出并添加到末尾
- 
len,它只是告诉我们向量中有多少个项目
- 
iter,它返回包含数据的迭代器
创建一个空向量然后向其中推送大量值可能会有些繁琐,所以有一个宏可以使事情变得更容易:
let third_vector = vec!["This", "works", "too"];
我们通过其 ! 符号来识别宏,但这次它并不是真正地假装是一个函数。相反,它几乎看起来像是一个前缀数组表达式。宏在它们的形态上有很多灵活性,对于这个宏来说,看起来像数组表达式是有意义的。这个结果就像我们使用 new 创建了一个向量,然后使用 push 向其中添加信息一样。这仅仅是一种更方便的写法。
Rc
有时候,Rust 对每个数据值只有一个所有者的坚持并不适合我们的数据模型。如果我们正在编写一个文字处理引擎,并希望人们能够在多个地方包含相同的图像而不浪费内存在重复上;或者如果我们正在模拟一个组织,其中一个人可能被多个角色引用呢?
我们可以有一个共享信息的确定所有者,并在其他地方使用借用,如果这行得通,那可能就是可行的方案。尽管如此,有两种情况它是不适用的:
- 
我们不知道共享数据值每个用户的生命周期有多长 
- 
我们至少需要有一个用户对共享数据有写访问权限 
文字处理引擎是问题一的很好例子:一个图像可能在同一文档中被使用多次,但我们永远不知道用户何时会决定删除其中一个,也不知道哪个会被删除。也许所有都会被删除,而且谁知道这会发生什么顺序或时间呢。
要完全解决第二个问题,我们需要 Rc 和 RefCell 数据类型,所以我们将在本章后面讨论这一点。
当我们发现自己处于需要共享信息但不知道该信息各种借用相对生命周期的情境时,我们可以使用 Rc 智能指针来让一切正常工作。Rc 代表 "引用计数",它所做的就是跟踪自身存在的副本数量。当这个数字达到零时,包含的数据值的生命周期结束。
让我们看看如何创建一些引用计数智能指针:
pub fn make_vector_of_rcs() -> Vec<Rc<String>> {
    let ada = Rc::new("Ada".to_string());
    let mel = Rc::new("Mel".to_string());
    return vec![
        Rc::clone(&ada),
        Rc::clone(&mel),
        Rc::clone(&ada),
        Rc::clone(&ada),
        Rc::clone(&mel),
        Rc::clone(&ada),
        Rc::clone(&mel),
    ];
}
我们使用 Rc::new 在函数体的前两行创建了第一个 Rc 值。它们都包含一个 String 值。
之后,我们使用 Rc::clone 来创建每个 Rc 的几个副本。请注意,String 值没有被复制,只是 Rc 智能指针。返回的向量包含四个共享相同 ada 字符串的 Rc,以及三个共享相同 mel 字符串的 Rc。
然后,函数的作用域结束,原始的 ada 和 mel 引用计数智能指针的生命周期也随之结束。然而,各种副本是返回值的一部分,所以它们的生命周期不会结束,因此这两个字符串值的引用计数仍然大于零,它们的生命周期也不会结束。
我们在这里使用了 Rc::clone,但如果我们写了 ada.clone() 或 mel.clone(),会产生相同的结果。人们通常更喜欢写成 Rc::clone,以清楚地表明我们正在克隆 Rc,而不是 Rc 包含的数据值。
现在我们将编写一个简短的程序,该程序依赖于用户输入来确定每个 Rc 副本的生命周期何时结束。没有固定的 Rc 删除顺序,所以编译器无法提前知道何时可以清理它们共享的数据值,但多亏了引用计数机制,只要需要,String 值就会被保留,然后它们的生命周期结束。
在这里,我们根据用户输入从向量中删除元素:
let mut ada_and_mel = make_vector_of_rcs();
while ada_and_mel.len() > 0 {
 println!("{:?}", ada_and_mel);
 print!("Remove which: ");
 io::stdout().flush().unwrap();
 let mut line = String::new();
 io::stdin().read_line(&mut line).unwrap();
 let idx: usize = line.trim().parse().unwrap();
 ada_and_mel.remove(idx);
}
首先,我们调用我们的 make_vector_of_rcs 函数来创建初始的引用计数智能指针向量,这些智能指针指向共享数据。
然后,我们循环,直到向量中还有任何值仍然被存储。在循环中,我们首先打印出当前的向量({:?} 代码告诉 Rust 打印出向量的 'debug' 表示形式,它看起来像 Rust 数组表达式)。然后我们打印出一个提示,并刷新输出流以确保提示确实被显示。然后我们从输入流中读取一行,将其解析为整数,并使用该整数作为索引从向量中删除一个元素。
当我们运行那个程序时,它看起来像这样:

当最后一个引用"Mel"值的Rc被移除时,那个String的生命周期最终结束,对包含"Ada"的String也是如此。
我们在那段代码中使用了大量的unwrap,实际上,我们过度使用了它。对flush和read_line的结果进行解包是有意义的;如果它们返回一个失败的Result,程序可能应该终止,因为操作系统级别出了问题。然而,对parse的结果进行解包并不是一个好主意,因为那里的失败结果只是意味着用户输入了意料之外的内容。我们真的应该使用match来响应,当输入无法正确解析时打印出一条消息。我们还应该检查数字是否是向量中实际存在的值的索引,而不是超出两端。Rust 不会让我们访问无效的索引,但尝试这样做将导致程序因错误消息而终止,这并不理想。
解析意味着将编码为文本字符串的信息转换为我们可以实际处理的数据值;例如,将"5"转换为数字5。parse函数相当强大,因为它根据我们分配给其返回值的变量的数据类型来确定我们想要的信息类型,然后确定使用哪个函数将字符串转换为那种数据类型。当然,它不能为我们编写那个函数,所以它只适用于最初就有那种函数的数据类型。此外,实际上所有的推理工作都是由 Rust 编译器完成的。parse函数只是利用了编译器的规则和推理系统。
弱引用
引用计数有一个致命的缺陷,这也是为什么它不是默认用于每种编程语言中所有变量的原因:循环。如果有两个或更多引用计数的值以某种方式相互引用,它们的生命周期将永远不会结束。它们形成了所谓的循环。
并非总是一目了然何时发生循环。如果 A 指向 B,B 指向 C,C 指向 D,D 又指向 A,我们仍然有一个循环。
我们可以通过使用弱引用来打破循环,这是Rc的辅助数据类型。当我们有一个Rc时,我们可以调用它的downgrade函数(例如,let weak_mel = Rc::downgrade(&mel))来检索一个Weak数据值。
我们实际上对Weak无法做任何事情,除了通过调用其upgrade函数(例如weak_mel.upgrade())来检索一个Rc,但使用Weak让我们能够在不实际引用的情况下跟踪引用计数值,这意味着我们可以在组织信息的方式上保持自然的同时避免创建循环。
如果引用数据值的Rcs的数量为零,即使还有弱引用引用该值,该数据值的生命周期也会结束。
由于引用的值可能已经不存在,upgrade函数返回一个Option。当我们调用upgrade时,我们可能会得到包含我们的Rc的Some,或者我们可能会得到None。
因此,这里的模式是,当我们想要确保数据值在我们需要它的时候一直存在时,我们使用Rc,当我们知道它将一直存在(例如,当它引用树结构中的父节点时)或者当我们不关心它是否存在(例如,当它是一个可以重新生成的缓存值时)时,我们使用Weak。
Cell 和 RefCell
Rust 的规则是任何时刻只有一个代码块可以写入数据值,这是一个好规则,但有时确保在编译器运行时始终遵循此规则的限制过于严格。有时,我们需要在程序运行时检查规则的额外自由度。
编译器检查将确保程序不能破坏规则,而运行时检查将确保程序不会破坏规则,这为我们提供了更多的灵活性,但代价是增加了开销。
为了支持这个选项,Rust 为我们提供了Cell和RefCell数据类型,它们是智能指针,允许我们在它们的内容不是存储在可变变量中的情况下更改它们。
Cell
Cell类型存储单个数据值,即使Cell没有标记为可变,我们也可以将其移动到Cell中。要将值移动到Cell中,我们可以使用以下方法:
- 
Cell::new,因为当创建单元格时,初始值已经移动到单元格中
- 
set,用于将新值移动到单元格中,并结束已存储值的生命周期
- 
replace,用于将新值移动到单元格中,并将旧值移动到当前作用域
要将值从Cell中移出,我们可以使用以下方法:
- 
replace,用于将新值移动到单元格中,并将旧值移动到当前作用域
- 
into_inner,用于消耗单元格,并返回它所包含的值
Cell不支持任何允许我们拥有空Cell的操作:它们总是必须包含某些内容,就像其他智能指针类型一样。
让我们看看一个单元格的实际应用:
let cell = Cell::new("Let me out!".to_string());
println!("{}", cell.replace("Don't put me in there.".to_string()));
println!("{}", cell.replace("I didn't do anything.".to_string()));
cell.set("You'll never hold me, copper!".to_string());
println!("{}", cell.into_inner());
注意,cell变量不是可变的。在这里,我们设置了一个单元格,使用replace几次来从cell中检索旧值,同时设置一个新值,然后使用set来设置新值并丢弃旧值,最后使用into_inner来移除cell并提取其包含的值。
into_inner函数将包含的值从cell中移出,但这不会创建一个空的cell,因为cell已经不存在了。如果我们尝试在调用into_inner之后访问它,编译器会显示错误,如下面的截图所示:

我们还可以使用一个函数来访问Cell中包含的数据值,但前提是包含的数据类型具有Copy特性:get。我们可以做类似println!("{}", cell.get())的事情,在检索其副本的同时保留cell的内容,但前提是复制数据值实际上是可能的。
这有什么用?
好吧,这实际上有什么好处?我们本来可以用一个可变变量,以更少的开销产生相同的结果。Cell(和RefCell)主要用于与Rc和类似的数据类型一起使用。Rc类型遵循 Rust 关于可变性的正常规则,并且由于它旨在作为在许多地方访问共享数据值的机制,这意味着共享值必须是不可变的。
除非该值是一个包含真实共享值的Cell或RefCell。
Cell或RefCell确保一次只有一个代码块实际上修改共享值,但任何通过Rc的克隆访问它的代码块都有能力这样做。
RefCell
Cell在将存储的数据值移入和移出cell时的语义并不总是方便使用,对于大型数据值,移动它们可能是一个昂贵的操作,我们不想在没有必要的情况下反复重复。RefCell来拯救!
RefCell类型支持RefCell::new、replace和into_inner,就像Cell一样,但它还有允许我们以可变或不可变方式借用包含值的函数。
让我们来试一试RefCell:
 let refcell = RefCell::new("It's a string".to_string());
 match refcell.try_borrow() {
 Ok(x) => { println!("Borrowed: {}", x); }
 Err(_) => { println!("Couldn't borrow first try"); }
 };
 let borrowed_mutably = refcell.try_borrow_mut()?;
 match refcell.try_borrow() {
 Ok(x) => { println!("Borrowed: {}", x); }
 Err(_) => { println!("Couldn't borrow second try"); }
 };
 println!("Mutable borrow is still alive: {}", borrowed_mutably);
首先,我们创建了一个新的RefCell,包含一个文本字符串。之后,我们使用try_borrow函数来获取包含的数据值的不可变借用。关于借用的规则仍然得到执行,这意味着如果我们已经对值进行了可变借用,我们就不能借用该值,如果我们已经以任何方式借用了该值,我们也不能对该值进行可变借用,这意味着try_borrow可能实际上不会成功。因此,我们必须处理它可能失败的可能性,我们在这里通过使用match表达式来做到这一点。
接下来,我们获取一个可变借用并将其存储在一个局部变量中。之前的借用在match表达式中选择的块的末尾结束,因此没有活跃的借用,我们预计try_borrow_mut会成功,但我们仍然需要处理失败的可能性。在这种情况下,我们使用?来处理返回的Result,它将提取成功的值,或者将失败返回给调用我们当前函数的函数。如果try_borrow_mut如预期那样成功,那么borrowed_mutably变量将包含对refcell包含的数据值的可变引用。
然后,我们再次尝试不可变地借用包含的数据值。由于不可变借用与可变借用不兼容,而且我们的可变借用仍然存在,我们预计这次尝试会失败。
Arc
当涉及到在多个代码块之间共享数据时,还有另一层复杂性:线程和多线程。Rc、Cell 和 RefCell 都无法在线程之间共享,但它们所代表的思想对于实现线程间的通信是有用的。
对于线程的使用,有一个 Rc 的直接等效物:Arc。Arc 是一个原子引用计数的智能指针,它因为那个 atomic 而可以在线程之间共享,这基本上意味着即使两个线程同时尝试使用它,它也不会被搞乱或混淆。
Arc 在内部有不同的名称和不同的工作方式,但表面上它就像 Rc 一样。我们关于如何使用 Rc 的知识也适用于 Arc。
没有使用 Mutex 或 RwLock 就难以展示 Arc 的特殊功能,所以请参阅下一节中的示例代码。
Mutex 和 RwLock
Mutex 和 RwLock 在某些方面与 RefCell 类似,但与 Arc 和 Rc 的关系并不那么紧密。
Mutex 的职责是确保一次只有一个线程可以访问包含的数据。由于它保证了在任何给定时间只有一个代码块可以访问,因此 Mutex 可以安全地提供读写访问,而不会违反 Rust 的规则。
在下面的示例中,我们有 Mutex 和 Arc 的实际应用,以及一些非常基本的线程操作:
let counter = Arc::new(Mutex::new(0));
for _ in 0..10 {
 let local_counter = Arc::clone(&counter);
 thread::spawn(move || {
 let wait = time::Duration::new(random::<u64>() % 8, 0);
 thread::sleep(wait);
 let mut shared = local_counter.lock().unwrap();
 *shared += 1;
 });
}
loop {
    {
        let shared = counter.lock().unwrap();
        println!("{} threads have completed", *shared);
        if *shared >= 10 {
            break;
        };
    };
    thread::sleep(time::Duration::new(1, 0));
}
我们首先做的是创建一个新的 Arc,其中包含一个 Mutex,而 Mutex 又包含一个整数。因此,我们的整数一次只能被一个线程访问,但它可以在多个线程之间共享,并且它的生命周期将持续到所有线程都完成使用它。
接下来,我们有一个 for 循环,它经过 10 个周期,并在每个周期启动一个线程。注意我们如何在调用 thread::spawn 之前创建 Arc 的克隆。这是因为我们正在使用闭包来定义线程应该做什么。闭包在很多方面都像函数,但它可以在定义时将其局部变量借用或移动到自己的作用域中。我们需要在要求它执行移动之前创建一个将要移动到其作用域中的 Arc 值。
这个闭包将局部变量移动到其自己的作用域中,因为我们定义它时使用了 move 关键字,并且它特别移动了 local_counter 变量,仅仅因为我们在这个闭包中引用了它。
在每个线程的闭包中,我们要求它等待少于 8 秒的随机持续时间,然后向计数器加 1。为了向计数器加 1,我们首先必须锁定 Mutex,以确保没有其他线程可以访问。我们通过调用 Mutex 的 lock() 函数并通过 Arc(因为 Arc 可以假装是它内部事物的正常借用)来实现这一点。lock 函数返回的值在解引用时为我们提供对包含数据的访问,并跟踪 Mutex 应保持锁定的时间。当该返回值的生命周期结束时,Mutex 被解锁,以便其他线程可以访问包含的数据值。如果另一个线程在 Mutex 仍然锁定时尝试锁定该值,Mutex 将使该线程等待直到它被解锁才能继续。
实际上,lock 函数返回一个 Result,但我们在这里只是解包它。如果 lock 调用失败,那是因为在另一个线程拥有 Mutex 锁定时发生了错误,结束程序可能是明智的选择。
最后,我们只需执行 *shared +=1 来实际上将 1 添加到共享计数器中。
之后,我们有一个循环,它锁定 Mutex,然后打印出计数器的当前值,并在它大于或等于 10 时结束循环(使用 break 关键字)。如果循环没有结束,它将等待一秒钟,然后再次执行。
注意,在那个循环中,我们还有一个代码块表达式,并且 thread::sleep 调用位于它之外。这是因为 Mutex 的工作方式:只要返回值的生命周期没有结束,Mutex 就保持锁定。我们不希望 Mutex 在代码睡眠时被锁定,所以我们把返回值放入一个更短的范围内,这样它的生命周期就会在我们调用 thread::sleep 之前结束,Mutex 也会被解锁。
RwLock 与 Mutex 类似,但它对管理包含数据值的访问有不同的规则。RwLock 有两个而不是一个锁函数:read 和 write。任何数量的线程都可以同时调用 read 来访问包含的信息,但任何给定时刻只有一个线程可以使用 write 来访问它,并且当线程有写访问权限时,不允许其他线程读取。如果线程在不受允许的时间尝试读取或写入,RwLock 将使线程等待直到它想要做的事情再次被允许。
我们不需要同时使用 read 和 write 来获得两种类型的访问。使用 write 意味着我们也有读访问权限。
总结
在本章中,我们学习了以下内容:
- 
堆内存和栈内存之间的区别 
- 
当我们希望这样做时,如何使用 Box简单地在堆上存储某些东西
- 
如何使用 Rc来管理在多个具有不同生命周期的作用域中所需的数据值的生命周期
- 
如何使用 Cell和RefCell允许对存储在Rc中的数据进行写访问
- 
如何使用 Arc、Mutex和RwLock来管理线程间的信息共享
在下一章中,我们将探讨泛型类型,以及如何为我们的数据类型使用泛型类型参数。
第七章:泛型类型
有时候,数据类型的细节并不重要。无论数据类型是什么,只要它是 某种,我们的代码都会正常工作。
我们已经多次看到这类情况的例子,比如 Result、Option、Rc 等等。所有这些,以及更多,都可以与广泛的不同的数据类型一起工作,因为它们有一个或多个 泛型类型参数。
在本章中,我们将做以下事情:
- 
学习泛型类型参数是什么 
- 
学习如何将泛型类型参数应用于数据类型 
- 
学习如何将泛型类型参数应用于函数 
- 
学习泛型类型和特例对象如何不同 
- 
创建一个完整且有用的二叉树数据结构 
具有泛型类型参数的类型
当一个数据类型具有泛型类型参数时,它严格来说根本不是一个数据类型。它是一系列数据类型。让我们暂时看看 Option。Option 定义如下:
pub enum Option<T> {
    None,
    Some(T),
}
这意味着它有一个名为 T 的泛型类型参数。如果我们尝试在不指定泛型类型参数类型的情况下使用 Option,Rust 将报告错误:
let x: Option = None;
它产生了这个错误:

这实际上在告诉我们,Option 不是一个可用的数据类型。然而,Option<u32> 是,Option<String> 也是,Option<Result<f64, String>>> 等等。此外,Option<u32> 和 Option<String> 不是同一类型,Rust 也不会假装它们是。它们是两种具有相同形状的不同数据类型。
当我们编写 Option<String> 时,我们是在告诉 Rust 使用 String 替换定义中的 T 来创建一个数据类型。
限制可用于类型参数的类型
有时候,我们需要我们的类型具有泛型类型参数,但我们不希望它们是 完全 泛型的。例如,我们可能需要替换参数的类型能够在不同线程之间移动,或者支持转换为 String,或者任何其他事情。幸运的是,Rust 提供了一种方法让我们能够做到这一点。
我们通过要求泛型类型参数具有一个或多个特质来限制泛型类型参数的范围。这被称为特例约束。让我们以一个基本的二叉树数据结构为例。
二叉树由节点组成。每个节点都有一个键,一个相关的值,以及两个子树:一个用于键小于当前节点键的节点,另一个用于键大于当前节点键的节点。在树中查找具有特定键的节点只是将其与根节点键进行比较,然后如果它不是相同的,就选择较小的或较大的树,并在那里做同样的事情,依此类推。
这里有一对表示二叉树的结构,具有用于键和值类型的泛型类型参数,以及对键类型的特例约束,以确保它实际上支持我们为二叉树键所需的比较:
struct TreeNode<K, V> where K: PartialOrd + PartialEq {
    key: K,
    value: V,
    lesser: Option<Box<TreeNode<K, V>>>,
    greater: Option<Box<TreeNode<K, V>>>,
}
这里是第二个结构,它为我们提供了存储空树的方法:
pub struct Tree<K, V> where K: PartialOrd + PartialEq {
    root: Option<Box<TreeNode<K, V>>>,
}
我们需要第二个结构,以便可以表示不包含数据的树。在这两个结构中,我们在结构名称之后放置了通配类型参数的名称,并在其之间放置了 < 和 >,然后包含了一个 where 子句,说明 K: PartialOrd + PartialEq。这意味着任何替换 K 的数据类型都必须实现 PartialOrd 特性和 PartialEq 特性。如果我们尝试使用不实现这两个特性的数据类型,编译器将拒绝它。
我们将在第八章 重要标准特性中检查 PartialOrd 和 PartialEq 的具体含义,重要标准特性。大致来说,它们意味着 大于 和 小于 的概念适用于键。
我们还指定了 TreeNode 中的 lesser 和 greater 以及 Tree 中的 root 是具有 Option<Box<TreeNode<K, V>>> 数据类型的变量。这意味着它们是可选的(它们可以包含一个有意义的值,或者 None),如果它们包含一个有意义的值,则该值存储在堆上,并且存储在堆上的值是一个 TreeNode,其键的数据类型为 K,值的数据类型为 V。
实现具有泛型类型参数的类型的功能
如果我们想要有属于具有泛型类型参数的类型的功能的函数,我们需要有一个实现块,就像类型没有这些参数一样,但我们还需要参数化实现块。
这里是 TreeNode 类型的实现块的开始:
impl<K, V> TreeNode<K, V> where K: PartialOrd + PartialEq {
现在,TreeNode<K, V> 是我们正在实现功能的类型。impl<K, V> 部分告诉编译器 K 和 V 是泛型类型参数,而 K: PartialOrd + PartialEq 告诉它这些参数的特性行界。它并不只是使用为数据类型指定的相同泛型类型参数和特性行界,因为实现块可以与数据类型不同;例如,有一个为 Box<Any> 提供了 downcast 函数的实现块,这在其他情况下不是 Box 的一部分。如果实现块的特性行界与实际使用的类型参数匹配,则块中的函数是可用的。如果不匹配,则函数不可用。
在实现块内部,我们可以使用 K 和 V 作为数据类型的名称:
    fn set(&mut self, key: K, value: V) {
        if key == self.key {
            self.value = value;
        }
        else if key < self.key {
            match self.lesser {
                None => {
                    self.lesser = Some(
                        Box::new(TreeNode {key, value, lesser: None, 
                        greater: None })
                    );
                },
                Some(ref mut lesser) => {
                    lesser.set(key, value);
                }
            }
        }
        else {
            match self.greater {
                None => {
                    self.greater = Some(
                        Box::new(TreeNode {key, value, lesser: None, 
                        greater: None })
                    );
                }
                Some(ref mut greater) => {
                    greater.set(key, value);
                }
            }
        }
    }
在这里,我们有将一个键与二叉树中的值关联起来的代码。它从一个相当标准的函数定义开始,除了我们使用 K 和 V 来指定 key 和 value 参数的数据类型。我们以可变借用 self 的方式,因为设置包含的值是一种变异。
在函数内部,我们首先比较当前节点的键与我们正在寻找的键,如果它们相同,我们就将值赋给当前节点。
接下来,我们检查我们正在寻找的键是否小于或大于当前节点的键,并使用这一点来选择要下行的树的哪个分支。无论哪种方式,我们都使用match表达式来确定该侧是否确实有一个分支,如果没有,我们就创建一个包含指定键和值的分支。如果那一侧确实有一个分支,我们就调用该节点的set函数,它再次执行相同的事情,只是这次使用不同的self。
将泛型类型用作函数返回值
在具有泛型类型参数的实现块中,我们也可以将这些参数名称用作函数返回值的一部分。
这里是一个使用泛型类型参数名称在其return数据类型中的示例函数:
    fn get_ref(&self, key: K) -> Result<&V, String> {
        if key == self.key {
            return Ok(&self.value);
        }
        else if key < self.key {
            match self.lesser {
                None => {
                    return Err("No such key".to_string());
                }
                Some(ref lesser) => {
                    return lesser.get_ref(key);
                }
            }
        }
        else {
            match self.greater {
                None => {
                    return Err("No such key".to_string());
                }
                Some(ref greater) => {
                    return greater.get_ref(key);
                }
            }
        }
    }
此函数在二叉树中查找键,并返回关联值的不可变借用,如果键不在树中,则返回错误信息。
它的结构与之前看到的set函数非常相似,但由于我们没有任何更改或请求可变借用,self也可以是一个普通的不可变借用。
涉及泛型类型参数的编译器错误
我们树结构的要求是用于键的数据类型必须具有PartialOrd和PartialEq特质。&str类型恰好具有这些特质,因此我们可以使用&str作为键:
let mut tree: Tree<&'static str, f32> = Tree::new();
tree.set("first key", 12.65);
tree.set("second key", 99.999);
tree.set("third key", -128.5);
tree.set("fourth key", 67.21);
println!("tree.get_ref(\"third key\") is {}", match tree.get_ref("third key") {
    Err(_) => {println!("Invalid!"); &0.0},
    Ok(x) => x,
});
这里,我们创建了一个Tree<&'static str, f32>,或者是一个将静态字符串映射到 32 位浮点数的树。如果我们编译并运行包含此片段的完整程序,一切都会完美运行。
另一方面,此数据类型不具有PartialOrd特质:
pub enum NotOrdered {
    A,
    B,
    C,
}
如果我们将NotOrdered替换为&'static str作为树的键类型,我们突然得到七个不同的编译器错误,这可能会填满整个屏幕。其中大多数看起来像这样:

这告诉我们该函数是在一个需要PartialOrd和PartialEq的实现块中定义的。由于我们的NotOrdered数据类型没有这些特质,我们试图调用的函数不存在,编译器正在告诉我们这一点。
在错误列表的顶部,可能已经滚动到屏幕之外,是不同的错误信息:

此错误信息比其他错误信息更有帮助,但它源于同一原因。我们的Tree数据类型需要一个键类型,其值可以与其他相同类型的值进行比较,而NotOrdered根本不提供这一点。
实现块之外的函数上的泛型类型
即使它们不是实现块的一部分,也可以为函数使用泛型类型参数。这看起来像这样:
fn print_generic<T>(value: T) where T: Display {
    println!("{}", value);
}
此函数有一个泛型类型参数T,可以是任何具有Display特质的任何数据类型。这意味着,如果此函数被定义,我们可以做这样的事情:
print_generic(12.7);
print_generic("Hello");
print_generic(75);
每一行都调用一个不同的print_generic函数,针对参数的数据类型进行了特殊化。编译器为每个我们使用的print_generic版本生成代码,每个版本接受不同数据类型的参数。
当然,print_generic并没有做println!宏没有做的事情,但它用来展示独立函数的泛型类型参数的用法。
编写特性界限的替代方法
到目前为止,我们一直将特性界限写作where子句,但有两种不同的方式来编写它们。where子句的好处是它位于一边,允许我们编写甚至复杂的特性界限而不会干扰到阅读函数或数据类型声明。
第一种替代方法是将特性界限放在泛型类型参数名称旁边,如下所示:
impl<K: PartialOrd + PartialEq, V> TreeNode<K, V> {
对于一个独立函数,这种技术看起来是这样的:
fn print_generic<T: Display>(value: T) {
这对于只有简单特性界限的数据类型或函数来说可能很好,但我们可以看到,即使只有两个必需的特性,TreeNode实现块也变得有点难以阅读。特性界限有点打断了流程,当我们想要找到数据类型名称时,它会让我们去寻找。
还有另一种指定特性界限的方法,但这只适用于函数:
fn requires_trait(value: impl Display)  {
我们在这里说的是value参数可以是任何具有Display特性的数据类型。与任何其他具有泛型类型参数的函数一样,编译器将为用于value的每个实际数据类型生成函数的不同版本。然而,使用这种语法,我们没有为泛型类型参数命名,因此我们无法在函数的其他地方引用它。
在函数体内部,这通常不是什么大问题,因为我们通常可以跳过在函数体内指定数据类型,而只依赖编译器来推断它。
我们也可以使用类似的语法来指定函数的返回类型,这很方便,因为如果我们没有为一个或多个参数类型命名,编写返回类型可能会很困难:
fn requires_trait(value: impl Display) -> impl Display {
这并不意味着只要实现了Display(正确的方式是返回一个特性对象,例如Box<dyn Display>),函数就可以返回任何数据类型,但我们所关心的是返回类型确实实现了Display,并且我们希望编译器推断出返回类型的细节。
为了使这一点更清晰,这里有一个尝试返回两种不同数据类型的函数,这两种数据类型都实现了Display特性:
fn faulty_return(sel: bool) -> impl Display {
    if sel {
        return 52;
    } else {
        return "Oh no";
    }
}
这里是 Rust 在尝试编译它时给出的错误信息:

当它找到 return 52 时,Rust 会检查 52 是否实现了 Display 特质(它实现了)并决定函数的实际返回类型是某种整数形式。然后,它找到第二个 return 并决定有问题,因为尽管 "Oh no" 也实现了 Display 特质,但它肯定不是整数。返回 impl Display 或类似的类型并不意味着返回任何实现了 Display 的类型;这意味着确定我们返回的具体类型,只要它实现了 Display。
泛型类型与特质对象
我们可以使用特质对象以非常类似的方式使用泛型类型参数。从一个角度来看,这两个函数做的是同一件事:
fn print_generic<T>(value: T) where T: Display {
    println!("{}", value);
}
这可能看起来与之前的代码做的是同一件事:
fn print_trait(value: &dyn Display) {
    println!("{}", value);
}
第一个有一个带有特质边界的泛型类型参数,第二个接受一个特质对象,这意味着它们都可以与许多不同的数据类型一起工作,只要相关类型具有 Display 特质。
然而,在底层,它们非常不同。泛型函数用于生成一个针对传递给它的每个数据类型在编译器运行时专门化的函数版本。这意味着当我们调用函数时,程序正在运行,计算机不需要花费任何时间考虑各种数据类型之间的差异。它只是调用编译器告诉它使用的函数,即针对实际使用的数据类型专门化的版本。这更快,但所有各种泛型函数版本会使程序稍微大一些。
将基于泛型类型的函数模式转换为针对特定类型的多达多个实际函数的过程称为 单态化。
另一方面,接受特质对象作为其参数的函数只有一个版本,但计算机在运行时必须处理具有 Display 特质的各个数据类型之间的差异。这较慢,但需要的内存略少。
作为一条经验法则,当你能使用泛型类型参数时,选择使用它们。当它们不能在编译时完成时,我们在运行时做事情。
高阶函数和代表函数的特质边界
高阶函数是一种接受另一个函数或闭包作为参数的函数。在 Rust 中,有三个相对不寻常的特质允许我们指定一个函数或闭包作为参数的特质边界:Fn、FnOnce 和 FnMut。
这些特质之间的区别由它们允许的变量访问类型定义:
- 
FnOnce是这些特质中最广泛应用的,因为它对可以实现它的类型的要求最少。FnOnce只保证可以安全地调用它一次。消耗self的函数是一个自然的FnOnce的例子,因为消耗了self,它就不再有self可以在未来被调用。可以安全多次调用的函数和闭包仍然实现FnOnce,因为精确地调用它们一次不是错误。这意味着一个被限制为FnOnce的变量可以接受任何类型的函数或闭包。
- 
FnMut是应用最广泛的特质。FnMut保证可以多次调用它是安全的,但它不保证不会通过可变借用在代码的其他地方改变变量值。使用&mut self的函数是一个自然的FnMut的例子,因为它可能会改变其self中包含的一个或多个变量。不能或实际上没有改变任何外部变量的函数和闭包仍然实现FnMut,因为在允许变动的位置使用它们不是错误。
- 
Fn是应用最少的,因为它保证可以多次调用,并且不会改变任何外部变量。任何是Fn的东西都可以安全地用于期望FnMut或FnOnce的地方,但反过来则不成立。
这意味着当我们是接收者时,如果可能的话,我们应该优先接受 FnOnce,其次是 FnMut,最后是 Fn,当我们真正需要所有这些保证时,这样就可以给发送数据值给我们的人提供最大的灵活性,让他们可以选择发送什么。
这里是一个非常简单的更高阶函数,它使用特质界限来指定可以分配给 f 参数的函数类型:
fn higher_order(f: impl FnOnce(u32) -> u32) {
    f(5);
}
因此,这看起来有点奇怪。FnOnce(u32) -> u32 是我们要求 f 实现的数据类型的特质的完整名称。允许我们为 Fn、FnMut 和 FnOnce 指定参数和返回类型的特殊语法是这些特质的独特之处;我们无法在其他地方做类似的事情。
为了明确起见,该函数定义也可以写成如下形式:
fn higher_order2<F>(f: F) where F: FnOnce(u32) -> u32 {
    f(5);
}
我们也可以这样写同样的内容:
fn higher_order3<F: FnOnce(u32) -> u32>(f: F) {
    f(5);
}
所有的前面代码都意味着相同的意思:函数的 f 参数需要实现 FnOnce 特质,并接受一个 u32 参数,并返回一个 u32。
下面是一段调用我们的 higher_order 函数并传递一个闭包作为 f 的值的代码:
let mut y = "y".to_string();
higher_order(|x: u32| {
    y.push('X');
    println!("In the closure, y is now {}", y);
    x
});
println!("After higher_order, y is {}", y);
这个闭包有一个名为 x 的参数,定义在 | 和 | 符号之间,但它也访问了第一行定义的 y 变量。此外,它改变了该变量的值,这意味着它需要可变访问。因此,这个闭包实现了 FnOnce 和 FnMut,但没有实现 Fn。
如果我们将 higher_order 改为需要 Fn 特性并尝试编译此代码,我们将得到编译器错误,如下面的截图所示:

这个错误并不特别具有启发性。它的意思是,我们告诉 higher_order 需要一个 Fn,然后我们传递给它一个闭包,因此它必须是 Fn,但我们试图在闭包内部执行一个修改操作,在那里我们没有可变的借用,因为 Rust 确定闭包必须具有 Fn 特性,所以它报告了一个关于尝试在不可变变量上执行修改操作的错误。
为了修复这个问题,我们只需要将 higher_order 函数的 f 参数的特征约束改回 FnOnce(或 FnMut),这样闭包就可以在 y 上执行 push 操作。
一旦我们将 f 恢复到具有适当的特征约束,这段代码实际上做了什么?:
- 
创建一个包含 String的可变变量y
- 
构建一个捕获 y变量的可变借用的闭包,并接受一个x参数
- 
将这个闭包传递给 higher_order作为f参数的值
- 
higher_order然后调用f(即我们的闭包),并将5作为其x参数的值传递
- 
在闭包内部,发生以下情况: - 
字符 'X'被追加到存储在y中的字符串
- 
打印 y的新值
- 
返回 x的值,并成为f(5)表达式的结果
 
- 
- 
higher_order返回
- 
打印 y变量的当前值
注意,闭包内部的代码只有在闭包被调用时才会运行,但它可以访问在创建它的作用域中定义的变量。
两个 y 的打印输出都打印了字符串 yX,因为它们都引用了同一个实际变量,无论是直接引用还是通过可变借用。
完整实现了具有泛型类型参数的二叉树
我们终于在我们对 Rust 的探索中取得了足够的进步,可以产生一些真正有用的东西。我们的二叉树仍然可以通过多种方式改进,但它确实做到了它被设计要做的事情:它允许我们轻松地存储和检索任意数量的键/值对。
我们没有努力确保二叉树保持平衡,这意味着每个节点的左右分支的高度大致相同,因为那不会增加我们对泛型类型讨论的内容。如果我们这样做,这个数据结构也将保证是高效的。平衡二叉树在任意键/值数据结构方面几乎是最优的。
因此,这里我们有一个完整、有用的数据结构。首先,我们有实际的结构,它存储树节点数据:
struct TreeNode<K, V> where K: PartialOrd + PartialEq {
    key: K,
    value: V,
    lesser: Option<Box<TreeNode<K, V>>>,
    greater: Option<Box<TreeNode<K, V>>>,
}
接下来,我们有一个实现块来定义 TreeNode 类型的功能,从 set 函数开始,它将一个键与一个值关联:
impl<K, V> TreeNode<K, V> where K: PartialOrd + PartialEq {
    fn set(&mut self, key: K, value: V) {
        if key == self.key {
            self.value = value;
        }
        else if key < self.key {
            match self.lesser {
                None => {
                    self.lesser = Some(Box::new(TreeNode {key, value, 
                     lesser: None, greater: None }));
                },
                Some(ref mut lesser) => {
                    lesser.set(key, value);
                }
            }
        }
        else {
            match self.greater {
                None => {
                    self.greater = Some(Box::new(TreeNode {key, value, 
                    lesser: None, greater: None }));
                }
                Some(ref mut greater) => {
                    greater.set(key, value);
                }
            }
        }
    }
get_ref和get_mut函数的结构与set函数非常相似,因为这三个函数都使用相同的机制在树中搜索具有正确键的节点:
    fn get_ref(&self, key: K) -> Result<&V, String> {
        if key == self.key {
            return Ok(&self.value);
        }
        else if key < self.key {
            match self.lesser {
                None => {
                    return Err("No such key".to_string());
                }
                Some(ref lesser) => {
                    return lesser.get_ref(key);
                }
            }
        }
        else {
            match self.greater {
                None => {
                    return Err("No such key".to_string());
                }
                Some(ref greater) => {
                    return greater.get_ref(key);
                }
            }
        }
    }
    fn get_mut(&mut self, key: K) -> Result<&mut V, String> {
        if key == self.key {
            return Ok(&mut self.value);
        }
        else if key < self.key {
            match self.lesser {
                None => {
                    return Err("No such key".to_string());
                }
                Some(ref mut lesser) => {
                    return lesser.get_mut(key);
                }
            }
        }
        else {
            match self.greater {
                None => {
                    return Err("No such key".to_string());
                }
                Some(ref mut greater) => {
                    return greater.get_mut(key);
                }
            }
        }
    }
}
接下来是定义我们的Tree数据类型,它为我们提供了数据结构的公共接口,并允许我们有一个空树:
pub struct Tree<K, V> where K: PartialOrd + PartialEq {
    root: Option<Box<TreeNode<K, V>>>,
}
现在是Tree的实现块,其中包含提供我们与TreeNodes交互方式的公共函数:
impl<K, V> Tree<K, V> where K: PartialOrd + PartialEq {
    pub fn new() -> Tree<K, V> {
        Tree { root: None }
    }
    pub fn set(&mut self, key: K, value: V) {
        match self.root {
            None => {
                self.root = Some(Box::new(TreeNode { key, value, 
                lesser: None, greater: None }));
            }
            Some(ref mut root) => {
                root.set(key, value);
            }
        }
    }
    pub fn get_ref(&self, key: K) -> Result<&V, String> {
        match self.root {
            None => {
                return Err("No such key".to_string());
            }
            Some(ref root) => {
                return root.get_ref(key);
            }
        }
    }
    pub fn get_mut(&mut self, key: K) -> Result<&mut V, String> {
        match self.root {
            None => {
                return Err("No such key".to_string());
            }
            Some(ref mut root) => {
                return root.get_mut(key);
            }
        }
    }
}
最后,我们有一个主函数来实际使用我们的树,这样我们就可以看到它的实际应用:
fn main() {
    let mut tree: Tree<&'static str, f32> = Tree::new();
    tree.set("first key", 12.65);
    tree.set("second key", 99.999);
    tree.set("third key", -128.5);
    tree.set("fourth key", 67.21);
    println!("tree.get_ref(\"third key\") is {}", match 
     tree.get_ref("third key") {
        Err(_) => {println!("Invalid!"); &0.0},
        Ok(x) => x,
    });
}
摘要
在本章中,我们做了以下工作:
- 
研究了数据类型和函数的泛型类型参数 
- 
学习了如何限制泛型类型参数,以确保所选的具体类型实现了适当的特性 
- 
看到了与泛型类型相关的各种编译器错误以及它们的意义 
- 
学习了如何使用特性界限和 Fn、FnMut以及FnOnce特性来创建高阶函数
- 
了解使用泛型类型和使用特性对象之间的差异和相似之处 
- 
从本章和前几章的知识中汲取知识,构建了一个二叉树数据结构 
在下一章中,我们将通过查看更多特性来结束我们的 Rust 之旅,了解它们的意义以及如何实现它们。
第八章:重要标准特质
正如我们之前所看到的,特质是 Rust 生态系统的重要组成部分。Rust 标准库中内置的特质影响了许多事物,包括甚至可以在特定数据值上使用的运算符。在本章中,我们将回顾许多这些特质,并了解如何在我们的数据类型上实现它们。
在本章中,我们将做以下事情:
- 
查看由 Rust 标准库定义的特质集合 
- 
了解这些特质的含义和影响 
- 
了解哪些特质是自动应用的 
- 
学习如何使用 derive命令为选定的特质生成特质实现
- 
学习如何手动实现剩余的特质 
可以推导的特质
对于某些特质,编译器本身知道如何为类型实现它们。如果我们想要它们,我们只需要告诉它,然后它会为我们处理其余的事情。
我们仍然可以选择手动实现可推导的特质,但这通常只是浪费时间。
告诉编译器我们希望数据类型具有可推导的特质是很容易的。
在这里,我们告诉它我们希望我们的 CopyExample 枚举实现 Copy 和 Clone:
#[derive(Copy, Clone)]
pub enum CopyExample {
    Good,
    Bad,
}
一个特质只有在创建特质的那些人能够编写一个生成特质实现的程序时才能被推导。当我们编写 #[derive(Copy, Clone)] 时,我们告诉编译器在定义特质的包的源代码中查找这些程序,以推导 Copy 和 Clone,并在继续编译之前运行这些程序来生成特质实现的源代码。如果实现特质所需做出的决策对于没有用户输入的程序来说过于复杂,则特质不能被推导。
Clone
Clone 特质意味着可以显式地复制数据值。编译器永远不会自动这样做,但当我们想要复制一个值时,我们可以通过调用它的 Clone 函数来实现。
推导 Clone 特质的样式如下:
#[derive(Clone)]
pub enum CloneExample {
    Good,
    Bad,
}
Copy
Copy 特质意味着创建数据值的副本只是复制构成其表示的位。如果数据值包含任何借用或使用堆内存,它就不能有 Copy 特质。
当编译器在其他情况下会移动数据值时,它会自动复制具有 Copy 特质的数据值。
由于任何具有 Copy 特质的对象都可以在请求时肯定被复制,因此 Copy 需要 Clone 被实现。
推导 Copy 的样子如下:
#[derive(Copy, Clone)]
pub enum CopyExample {
    Good,
    Bad,
}
Debug
Debug 特质告诉 Rust 如何格式化数据值以供调试输出。这种用法的一个例子是,如果我们使用 {:?} 而不是 {} 作为 println! 或 print! 中数据值的替换标记。
由于数据值的调试表示应该非常接近它在源代码中的表示方式,Rust 能够自动为我们推导它。
推导 Debug 看起来像这样:
#[derive(Debug)]
pub enum DebugExample {
    Good,
    Bad,
}
PartialEq
PartialEq 特性表示比较数据值以确定它们是否相等的能力。然而,它并不表示一个值被认为是等于它自己。
PartialEq 特性被编译器用来实现==比较操作。
浮点数是具有 PartialEq 的经典数据类型示例,因为 NaN(非数字)值的浮点表示被认为不等于它自己。
推导 PartialEq 看起来像这样:
#[derive(PartialEq)]
pub enum PartialEqSelf {
    Good,
    Bad,
}
然而,那个推导只比较了两个 PartialEqSelf 数据值。如果我们想启用与其他类型的数据值的相等比较,我们需要手动实现该特性。
这里,我们有一个手动实现的特性,允许与 u32 数据类型进行比较:
pub enum PartialEqU32 {
    Good,
    Bad,
}
impl PartialEq<u32> for PartialEqU32 {
    fn eq(&self, other: &u32) -> bool {
        match self {
            PartialEqU32::Good => other % 2 == 0,
            PartialEqU32::Bad => other % 2 == 1,
        }
    }
}
在这里,我们已经安排了 PartialEqU32::Good 值与偶数 u32 进行比较视为相等,而 PartialEqU32::Bad 与奇数 u32 进行比较视为相等。
Eq
Eq 特性意味着与 PartialEq 相同的意思,除了数据值总是等于它自己。
实现 Eq 特性需要同时实现 PartialEq 特性,并且它所做的唯一超出 PartialEq 的事情就是向编译器提供提示,即当比较的两边都是相同的数据值时,不需要麻烦地运行 Eq 函数。
推导 Eq 看起来像这样:
#[derive(Eq, PartialEq)]
pub enum EqExample {
    Good,
    Bad,
}
PartialOrd
PartialOrd 特性表示在两个数据值之间定义某种排序能力,因此我们可以说出一个小于另一个,或大于另一个,或者它们是相同的,或者排序关系不适用于这些值。这就是为什么这是一个部分排序的原因。
由于“它们相同”是比较的有效结果,实现 PartialOrd 需要实现 PartialEq。
与 PartialEq 一样,我们可以推导出一个实现,用于比较相同类型的数据值,但我们也可以手动实现特性以允许与不同类型的数据比较。
这里,我们有特性的自动推导:
#[derive(PartialOrd, PartialEq)]
pub enum PartialOrdSelf {
    Good,
    Bad,
}
这里,我们手动实现了它以支持与不同数据类型的比较:
pub enum PartialOrdU32 {
 Good,
 Bad,
}
impl PartialEq<u32> for PartialOrdU32 {
    fn eq(&self, _other: &u32) -> bool {
        false
    }
}
impl PartialOrd<u32> for PartialOrdU32 {
    fn partial_cmp(&self, _other: &u32) -> Option<Ordering> {
        match self {
            PartialOrdU32::Good => Some(Ordering::Greater),
            PartialOrdU32::Bad => None,
        }
    }
}
这里,我们告诉 Rust,PartialOrdU32::Good 值总是大于任何 u32 值,但 PartialOrdU32::Bad 值与任何 u32 值都没有任何关系。
Ord
Ord类似于PartialOrd,除了它不允许返回“无关系”的选项;对于任何一对值,要么它们相等,要么一个小于另一个。
Ord被编译器用来实现<(小于)、>(大于)、<=(小于等于)、>=(大于等于)比较运算符。
如果一个数据类型具有Ord特质,它也必须具有PartialOrd、Eq和PartialEq特质。像这些特质一样,它们可以手动实现以启用不同数据类型之间的比较,但我们必须非常小心,确保用于实现这些特质的各个函数返回的结果是一致的。当我们推导特质时,我们不必担心这一点。
这里是推导Ord的一个示例:
#[derive(Ord, Eq, PartialOrd, PartialEq)]
pub enum OrdExample {
    Good,
    Bad,
}
哈希
Hash特质使数据值能够用作 Rust 标准库中几个数据结构的键,例如HashMap和HashSet。
推导Hash特质看起来像这样:
#[derive(Hash)]
pub enum HashExample {
    Good,
    Bad,
}
虽然Eq和PartialEq实际上不是实现Hash所必需的,但如果它们被实现,它们需要与之一致,也就是说,如果两个值相等,它们的哈希值也应该相等。自动生成的实现具有这个属性,所以我们只有在进行手动实现时才需要担心它。
默认
当为类型实现Default特质时,它使我们能够请求该类型数据的默认值。
当我们为一个数据类型推导Default时,它将该类型的默认值设置为包含所有包含数据类型的默认值,因此当我们这样做时:
#[derive(Default)]
pub struct DefaultExample {
    name: String,
    value: i32,
}
我们所做的是将DefaultExample类型的默认值设置为包含String和i32的默认值的DefaultExample。
我们可以这样请求一个默认值:
let x: DefaultExample = Default::default();
println!("Default String is {:?}, default i32 is {:?}", x.name, x.value);
使操作员具备特性的特质
大多数运算符和 Rust 的特殊语法都有特质的支撑,这些特质告诉编译器如何对特定数据类型执行操作。我们已经看到了一些,但其中许多不能被推导,所以如果我们想为我们自己的数据类型启用这种语法,我们需要手动实现它们。
添加、乘法、减法和除法
Add、Mul、Sub和Div特质代表了对两个值进行加、乘、减或除的能力。这些特质被编译器用来实现+、*、-和/运算符。
注意,如果self和other的值不具有Copy特质,它们将被移动到实现函数中并被消耗。
所有这些特质遵循相同的模式,所以这里是一个Add的实现示例:
pub enum AddExample {
    One,
    Two,
    Three,
    Many,
}
impl Add for AddExample {
    type Output = AddExample;
    fn add(self, other: AddExample) -> AddExample {
        match (self, other) {
            (AddExample::One, AddExample::One) => AddExample::Two,
            (AddExample::One, AddExample::Two) => AddExample::Three,
            (AddExample::Two, AddExample::One) => AddExample::Three,
            _ => AddExample::Many,
        }
    }
}
Mul、Sub和Div遵循相同的模式。
我们在这里定义的是一种非常原始的计数形式,它将任何大于 3 的数字视为“很多”。
在 impl 块内部,我们有 type Output = AddExample;。这是一种我们之前没有见过的语法。我们所做的是为这个实现设置 Output 关联类型,它被反馈到特性定义中,用于声明 add 函数的签名。毕竟,我们在这里返回一个 AddExample,而在特性最初定义时并没有这样的类型。但这不是问题,因为特性说明 add 函数返回一个类型为 Output 的数据值,而我们刚刚告诉它 Output 是 AddExample 的别名。
我们还可以通过实现 Add<OtherType> for OneType 来实现将两种不同类型的数据相加,这表示在 + 的左侧有一个 OneType 值,在右侧有一个 OtherType 值,这与我们在本章前面能够创建两种不同类型之间比较的方式类似。同样的技巧也适用于 Mul、Sub 和 Div。
AddAssign, MulAssign, SubAssign, and DivAssign
这些特性为实现了它们的类型启用了 +=、*=、-= 和 /= 操作符。
它们类似于 Add、Sub、Mul 和 Div 特性,不同之处在于它们的实现函数接受 &mut self 而不是普通的 self。它们不是消耗它们的左侧输入,而是有改变其包含值的能力。
所有这些特性都遵循相同的模式,所以这里是一个 AddAssign 的示例实现:
pub enum AddExample {
 One,
 Two,
 Three,
 Many,
}
impl AddAssign for AddExample {
    fn add_assign(&mut self, other: AddExample) {
        *self = match (&self, other) {
            (AddExample::One, AddExample::One) => AddExample::Two,
            (AddExample::One, AddExample::Two) => AddExample::Three,
            (AddExample::Two, AddExample::One) => AddExample::Three,
            _ => AddExample::Many,
        };
    }
}
除了基于将新值赋给 &mut self 的差异之外,这和 Add 特性的 add 函数实现非常相似,这并不令人惊讶。
特别是,虽然它不消耗它的 self,但它仍然消耗操作符右侧的值,假设这个值没有 Copy 特性。
BitAnd
BitAnd 特性为实现了它的类型启用了 & 操作符。这个操作符用于计算两个整数的 按位与 值(因此得名),但对于各种其他数据类型有不同的含义。
实现 BitAnd 的样子如下:
pub enum BitExample {
    Yes,
    No,
}
impl BitAnd for BitExample {
    type Output = BitExample;
 fn bitand(self, other: BitExample) -> BitExample {
 match (self, other) {
 (BitExample::Yes, BitExample::Yes) => BitExample::Yes,
            (BitExample::No, BitExample::Yes) => BitExample::No,
            (BitExample::Yes, BitExample::No) => BitExample::No,
            (BitExample::No, BitExample::No) => BitExample::No,
        }
    }
}
BitAndAssign
BitAndAssign 特性为实现了它的数据类型启用了 &= 操作符。
实现 BitAndAssign 的样子如下:
pub enum BitExample {
 Yes,
 No,
}
impl BitAndAssign for BitExample {
    fn bitand_assign(&mut self, other: BitExample) {
        *self = match (&self, other) {
            (BitExample::Yes, BitExample::Yes) => BitExample::Yes,
            (BitExample::No, BitExample::Yes) => BitExample::No,
            (BitExample::Yes, BitExample::No) => BitExample::No,
            (BitExample::No, BitExample::No) => BitExample::No,
        };
    }
}
BitOr
BitOr 特性为实现了它的类型启用了 | 操作符。这个操作符用于计算两个整数的 按位或 值,但对于各种其他数据类型有不同的含义。
实现 BitOr 的样子如下:
pub enum BitExample {
    Yes,
    No,
}
impl BitOr for BitExample {
 type Output = BitExample;
 fn bitor(self, other: BitExample) -> BitExample {
        match (self, other) {
            (BitExample::Yes, BitExample::Yes) => BitExample::Yes,
            (BitExample::No, BitExample::Yes) => BitExample::Yes,
            (BitExample::Yes, BitExample::No) => BitExample::Yes,
            (BitExample::No, BitExample::No) => BitExample::No,
        }
    }
}
BitOrAssign
BitOrAssign 特性为实现了它的数据类型启用了 |= 操作符。
实现 BitOrAssign 的样子如下:
pub enum BitExample {
 Yes,
 No,
}
impl BitOrAssign for BitExample {
    fn bitor_assign(&mut self, other: BitExample) {
        *self = match (&self, other) {
            (BitExample::Yes, BitExample::Yes) => BitExample::Yes,
            (BitExample::No, BitExample::Yes) => BitExample::Yes,
            (BitExample::Yes, BitExample::No) => BitExample::Yes,
            (BitExample::No, BitExample::No) => BitExample::No,
        };
    }
}
BitXor
BitXor 特性为实现了它的类型启用了 ^ 操作符。这个操作符用于计算两个整数的 按位异或 值,但对于各种其他数据类型有不同的含义。
实现 BitXor 的样子如下:
pub enum BitExample {
    Yes,
    No,
}
impl BitXor for BitExample {
 type Output = BitExample;
    fn bitxor(self, other: BitExample) -> BitExample {
        match (self, other) {
            (BitExample::Yes, BitExample::Yes) => BitExample::No,
            (BitExample::No, BitExample::Yes) => BitExample::Yes,
            (BitExample::Yes, BitExample::No) => BitExample::Yes,
            (BitExample::No, BitExample::No) => BitExample::No,
        }
    }
}
BitXorAssign
BitXorAssign特质为实现了它的数据类型启用了^=运算符。
实现BitXorAssign看起来是这样的:
pub enum BitExample {
 Yes,
 No,
}
impl BitXorAssign for BitExample {
    fn bitxor_assign(&mut self, other: BitExample) {
        *self = match (&self, other) {
            (BitExample::Yes, BitExample::Yes) => BitExample::No,
            (BitExample::No, BitExample::Yes) => BitExample::Yes,
            (BitExample::Yes, BitExample::No) => BitExample::Yes,
            (BitExample::No, BitExample::No) => BitExample::No,
        };
    }
}
Deref
Deref特质赋予了将值解引用为借用一样的功能。智能指针实现了这个特质,这就是为什么它们可以用作包含数据值的借用。String也做了同样的事情,这就是为什么我们可以在期望&str的地方使用String值。
这里是Deref特质的实现:
pub struct DerefExample {
 val: u32,
}
impl Deref for DerefExample {
    type Target = u32;
    fn deref(&self) -> &u32 {
        return &self.val;
    }
}
注意到实现函数实际上并没有解引用任何东西。相反,它将一个&self借用转换成了对其他东西的借用。
这正是编译器需要的东西,以便正确且高效地处理解引用智能指针等,但编译器也使用这种能力让我们能够像处理普通的&str一样与Rc<Box<String>>这样的东西交互。Rc有一个返回Box借用的deref函数,Box有一个返回String借用的deref函数,而String有一个返回str借用的deref函数,因此编译器允许我们将整个结构视为&str,以便调用其函数或将其用作参数。
DerefMut
DerefMut特质与Deref做的是同样的事情,但它用于解引用可变值。编译器决定使用Deref还是DerefMut,所以通常当我们需要实现一个时,我们通常需要实现两个。
这里是DerefMut的实现:
pub struct DerefExample {
    val: u32,
}
impl DerefMut for DerefExample {
    fn deref_mut(&mut self) -> &mut u32 {
        return &mut self.val;
    }
}
DerefMut特质要求也实现了Deref特质,并且deref和deref_mut函数有相同的返回类型。
Drop
当一个数据类型具有Drop特质时,程序将在该类型值的生命周期结束之前立即调用drop函数。这就是Rc、Mutex、RefCell等能够跟踪它们包含的值有多少借用的方式。
drop函数在数据值的生命周期结束之前被调用,所以我们不必担心它是一个无效的引用。此外,我们也不必担心手动清理我们数据类型中包含的值,因为它们将在我们的drop函数完成后自动丢弃。我们唯一需要做的就是处理导致我们最初实现Drop的特殊情况。
我们不能直接调用drop函数,因为这会是一个制造混乱的极好方式。有一个std::mem::drop函数我们可以使用,它会消耗一个数据值并将其丢弃,如果我们需要在特定时间触发这个操作。
实现Drop看起来是这样的:
pub enum DropExample {
 Good,
 Bad,
}
impl Drop for DropExample {
    fn drop(&mut self) {
        match self {
            DropExample::Good => println!("Good DropExample dropped"),
            DropExample::Bad => println!("Bad DropExample dropped"),
        };
    }
}
Index
Index特质意味着数据类型可以用x[y]语法使用,其中根据索引值y在x内部查找一个值。
当我们实现 Index 时,我们需要确定可以用于索引值的什么数据类型,以及操作返回什么数据类型,因此实现看起来是这样的:
pub struct IndexExample {
 first: u32,
 second: u32,
 third: u32,
}
impl<'a> Index<&'a str> for IndexExample {
    type Output = u32;
    fn index(&self, index: &'a str) -> &u32 {
        match index {
            "first" => &self.first,
            "second" => &self.second,
            "third" => &self.third,
            _ => &0,
        }
    }
}
我们使用了 &str 作为索引的数据类型,并使用 u32 作为值的类型。使用 &str 意味着我们需要稍微注意生命周期,但这并不太糟糕。
IndexMut
IndexMut 特性表示使用 x[y] = z 语法对包含的值进行赋值的 abilities。像 Index 特性一样,它允许我们通过提供一个索引值来查找包含的数据值,但它产生一个可变借用,可以用来更改它。
实现 IndexMut 的样子如下:
pub struct IndexExample {
 first: u32,
 second: u32,
 third: u32,
 junk: u32,
}
impl<'a> IndexMut<&'a str> for IndexExample {
    fn index_mut(&mut self, index: &'a str) -> &mut u32 {
        match index {
            "first" => &mut self.first,
            "second" => &mut self.second,
            "third" => &mut self.third,
            _ => &mut self.junk,
        }
    }
}
注意,我们在 IndexExample 结构体中添加了一个 junk 值。我们这样做是因为没有方法可以表示索引值不映射到一个有效的包含值;如果调用 index_mut 函数,它必须返回正确类型的可变借用,并且这个借用必须有足够长的生命周期。将垃圾值添加到数据结构是一种简单的方法,尽管还有其他可以节省内存的方法。
任何实现了 IndexMut 的类型都必须实现 Index,并且 index 和 index_mut 函数必须分别返回相同数据类型的借用和可变借用。
Neg
Neg 特性使数据类型能够与 一元否定 操作符一起使用,也称为负号。当我们写 -5 时,我们正在将一元否定操作符应用于值 5,产生一个结果为负 5。
实现 Neg 的样子如下:
pub enum NegExample {
 Yes,
 No,
}
impl Neg for NegExample {
 type Output = NegExample;
 fn neg(self) -> NegExample {
 match self {
 NegExample::Yes => NegExample::No,
 NegExample::No => NegExample::Yes,
 }
 }
}
Not
Not 特性启用了 逻辑非 操作符,它被写作一个 !。Not 在概念上和实际应用上都与 Neg 类似,但它的主要用途是布尔逻辑而不是算术。
实现 Not 的样子如下:
pub enum NotExample {
 True,
 False,
}
impl Not for NotExample {
    type Output = NotExample;
    fn not(self) -> NotExample {
        match self {
            NotExample::True => NotExample::False,
            NotExample::False => NotExample::True,
        }
    }
}
Rem 和 RemAssign
Rem 特性为实现了它的类型启用了 % 操作符。这个操作符用于计算两个整数的 模数(也称为除法的余数),但对于各种其他数据类型有不同的含义。
Rem 特性既有一个关联的 Output 类型,也有通过实现 Rem<OtherType> 而不是仅仅 Rem 来在多种类型上操作的选择。
RemAssign 与 Rem 的关系类似于 AddAssign 与 Add 的关系。
Shl 和 ShlAssign
Shl 特性为实现了它的类型启用了 << 操作符。这个操作符用于将整数左移一定数量的位,但对于各种其他数据类型有不同的含义。
Shl 特性既有一个关联的输出类型,也有通过实现 Shl<OtherType> 而不是仅仅 Shl 来在多种类型上操作的选择。
ShlAssign 与 Shl 的关系类似于 AddAssign 与 Add 的关系。
Shr 和 ShrAssign
Shr 特性为实现了它的类型启用了 >> 操作符。这个操作符用于将整数右移若干位,但对于其他各种数据类型有不同的含义。
Shr 特性既有 Output 关联类型,也有通过实现 Shr<OtherType> 而不是仅仅 Shr 来操作不同类型的选项。
ShrAssign 与 Shr 的关系类似于 AddAssign 与 Add 的关系。
自动实现的特性
有一些特性在适当的情况下会自动实现,甚至不需要 #[derive()] 标签。这些通常代表数据类型极其低级方面的特性。
Sync
Sync 特性会自动应用于任何可以在线程之间安全借用数据的数据类型。
虽然我们的数据类型如果符合条件会自动具有 Sync 特性,但有时我们想要确保数据类型不具有 Sync,即使它看起来对编译器来说应该是这样的。
我们可以通过为我们的数据类型实现 !Sync 来实现这一点:
pub enum NotSyncExample {
 Good,
 Bad,
}
impl !Sync for NotSyncExample {}
我们实际上不需要在 !Sync 中实现任何函数。我们只是在告诉编译器 Sync 特性对于这个类型来说是不合适的。
截至 Rust 1.29 版本,实现 !Sync 仍然被视为一个不稳定特性,并且不在编译器的稳定构建中可用。它可以通过在文件顶部放置 #![feature(optin_builtin_traits)] 来在夜间构建中启用。
许多数据类型都有 Sync 特性,但 Rc、Cell 和 RefCell 是不具该特性的类型中的显著例子。Arc、Mutex 和 RwLock 则具有。
Send
Send 特性会自动应用于任何可以在线程之间安全移动的数据类型。它是 Sync 的一个近亲,并且像 Sync 一样,我们可以实现 !Send 来告诉编译器一个数据类型不应该具有该特性。
如果我们没有明确禁止,编译器会根据它包含的类型是否具有该特性来决定一个类型是否具有 Send 特性。
Sized
对于编译器已知大小的任何数据类型,Sized 特性都会自动应用。所有特性界限都会自动包含 Sized 作为额外的、隐含的要求,除非我们明确告诉它 ?Sized 是要求。如果我们明确声明特性界限是 ?Sized,这意味着符合界限的数据类型允许是 Sized,但不是必须的。
Fn
Fn 特性会自动应用于任何只使用不可变借用访问其自身作用域之外数据的函数或闭包。
这是一个严格的要求,许多函数和闭包都未能通过这个测试,所以 Fn 是函数特性中最不常见的。
FnMut
FnMut 特性会自动应用于任何使用可变或不可变借用访问其自身作用域之外数据的函数或闭包。
这是一个中等要求,但一些函数和闭包未能通过这个测试,所以 FnMut 比较常见于 Fn。
FnOnce
FnOnce特质会自动应用于任何使用可变借用、不可变借用或移动变量来访问其作用域外数据的函数。
这是一个宽松的要求,任何函数或闭包都会满足,因此FnOnce是函数特质的常见类型。
摘要
在本章中,我们做了以下工作:
- 
研究了许多不同的特质 
- 
检查了特质的特定含义以及它们如何与 Rust 的语法交互 
- 
学习了实现特质的细节 
- 
学习了如何轻松推导支持该特性的特质 
我们已经到达了快速入门指南的结尾,但旅程永远不会结束。祝你在下一步好运。

 
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号