Rust-学习指南-全-
Rust 学习指南(全)
原文:
annas-archive.org/md5/67cdb4ed3945477d8150f8bf518d5547译者:飞龙
前言
Rust 是一种新的编程语言。它提供了与现代 C++ 相当或甚至更优的性能和安全性,同时它是一种具有相对较低入门门槛的现代语言。Rust 的势头,加上其活跃和友好的社区,为该语言的未来带来了巨大的希望。
虽然 Rust 现代且流畅,但它并不是一种特别容易的语言。内存管理系统跟踪程序中使用的每个实体的生命周期,并且设计得如此之好,以至于这种跟踪通常可以在编译时完全发生。Rust 程序员的负担是在编译器无法自行决定应该发生什么时帮助编译器。由于现代编程可以在不面对此类责任的情况下进行,现代程序员可能不会立即感到 Rust 舒适。
然而,就像所有专业和技能一样,越难获得,就越有价值,这本书就是为了帮助你。本书涵盖了 Rust 的基础知识,使你能够获得足够的技能来开始使用它进行编程。
本书涵盖的内容
第一章,介绍和安装 Rust,介绍了安装 Rust 工具集和使用基本工具。
第二章,变量,专注于使用不同类型的变量。
第三章,输入和输出,涵盖了基本的 I/O。
第四章,条件、递归和循环,探讨了 Rust 的不同循环和迭代方法。
第五章,记住,记住,涵盖了 Rust 的内存管理系统。
第六章,创建您的 Rust 应用程序,给你一个构建完整 Rust 应用程序的任务。
第七章,匹配和结构,教你复合数据类型以及如何解构它们。
第八章,Rust 应用程序的生命周期,涵盖了 Rust 独特的拥有权、借用和生命周期系统,这使资源安全无需垃圾回收。
第九章,介绍泛型、Impl 和 Traits,探讨了 Rust 的泛型类型。
第十章,创建您的自己的 Crate,指导你如何构建自己的 Rust 代码的容器包。
第十一章,Rust 中的并发,探讨了并发和并行技术。
第十二章,现在轮到你了!,给你另一组任务来完成。
第十三章,标准库,涵盖了 Rust 的标准库。
第十四章,外函数接口,介绍了将 Rust 代码与 C 程序接口的技术。
您需要这本书什么
要真正深入这本书的内容,您应该写出示例代码并完成练习。为此,您需要一个相当新的计算机;对于这本书的目的,1GB 的 RAM 应该足够了,但您拥有的越多,构建速度越快。
Linux 是这里支持最好的操作系统,但 Rust 本身在 macOS 和 Windows 的最新版本中也是一等公民,因此所有示例都应很好地适应那里。
这本书面向谁
这本书将吸引那些希望用 Rust 构建应用程序的应用程序开发者。不需要编程知识。
惯例
在这本书中,您将找到许多用于区分不同类型信息的文本样式。以下是一些这些样式的示例及其含义的解释。文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称的显示方式如下:“在unwrap方法中有一个简短的形式。这与expect方法相同,但在失败的情况下不会打印任何内容。”
代码块按照以下方式设置:
let mut file = File::create("myxml_file.xml).unwrap();
let mut output = io::stdout();
let mut input = io::stdin();
任何命令行输入或输出都按照以下方式编写:
cd app_name
cargo build app_name
新术语和重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:“打开 Visual Studio Code 并转到命令面板,可以通过视图菜单或通过键盘快捷键Ctrl + Shift + P(可能在平台上有所不同)。”
警告或重要注意事项如下所示。
技巧和窍门如下所示。
读者反馈
我们欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或不喜欢什么。读者反馈对我们来说很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。要发送一般反馈,请简单地发送电子邮件至feedback@packtpub.com,并在邮件主题中提及本书的标题。如果您在某个主题上具有专业知识,并且您有兴趣撰写或为本书做出贡献,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您是 Packt 图书的骄傲拥有者,我们有一些东西可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从www.packtpub.com的账户下载此书的示例代码文件。如果您在其他地方购买了此书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。您可以通过以下步骤下载代码文件:
-
使用您的电子邮件地址和密码登录或注册我们的网站。
-
将鼠标指针悬停在顶部的支持选项卡上。
-
点击代码下载和勘误。
-
在搜索框中输入书籍名称。
-
选择您想要下载代码文件的书籍。
-
从下拉菜单中选择您购买此书的来源。
-
点击代码下载。
文件下载完成后,请确保您使用最新版本解压缩或提取文件夹:
-
适用于 Windows 的 WinRAR / 7-Zip
-
适用于 Mac 的 Zipeg / iZip / UnRarX
-
适用于 Linux 的 7-Zip / PeaZip
该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Learning-Rust。我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。请查看它们!
勘误
尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的某本书中找到错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。要查看之前提交的勘误,请转到www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。
盗版
在互联网上侵犯版权材料是一个跨所有媒体持续存在的问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,请立即提供位置地址或网站名称,以便我们可以追究补救措施。请通过copyright@packtpub.com与我们联系,并提供涉嫌盗版材料的链接。我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。
问题
如果您在这本书的任何方面遇到问题,您可以联系我们的questions@packtpub.com,我们将尽力解决问题。
第一章:介绍和安装 Rust
Rust 是编程语言中不断增长的新成员之一。如果您从未使用过 Rust,但来自几乎任何过程式语言(如 C 或 Pascal)或习惯于使用 shell 脚本,那么您在使用 Rust 时应该会很快感到宾至如归。
熟悉 Rust 很简单,在本章中,我们将涵盖以下主题:
-
使用 rustup 安装 Rust
-
测试安装
-
设置项目
-
查看可用的 IDE
-
使用 Cargo 进行自动化
安装 Rust
与大多数语言一样,Rust 适用于大量平台。不可能为每个操作系统的每个变体安装编译器。幸运的是,有一个官方的安装 Rust 的方法,尽管细节可能略有不同,但在所有平台上过程几乎相同。因此,本书将介绍如何在 Fedora 27 上使用 rustup 安装 Rust。
rustup.rs始终包含有关如何在所有平台上开始使用的最新说明。在 Linux 和 macOS 上,它看起来可能如下:

在 Windows 上,此文本将被替换为指向rustup-init.exe的链接,这是一个在 Windows 上安装和设置 rustup 的可执行文件。
在 Linux 上安装 rustup
运行显示在rustup.rs上的建议命令。在终端中运行此命令。脚本建议一些默认设置,并要求您确认。完成整个脚本后,它的大致样子如下:

注意,此脚本尝试通过编辑您的.profile和.bash_profile文件来为您设置 rustup。如果您使用的是自定义设置,例如另一个 shell,您可能需要手动添加source $HOME/.cargo/env命令。
完成此脚本后,您可以通过从终端注销并重新登录来验证它是否成功,并检查工具是否已添加到您的路径中:

gcc 的先决条件
要构建链接到外部库的任何软件,您需要一个 C 编译器和您可能链接的任何库的开发版本。为了确保一切正常工作,请使用您操作系统的标准方法安装编译器。
在 Fedora 中,这将使用dnf工具完成:
sudo dnf install -y gcc
如果您不确定是否已安装 gcc,请在终端窗口中输入以下命令:
gcc -version
如果 gcc 已安装,您将看到类似以下内容:

测试您的安装
打开一个命令提示符窗口并输入以下内容:
rustc --version
如果一切安装正确,您将看到类似以下内容:

集成开发环境
为了有效地编写 Rust,你至少需要某种文本编辑器。所有流行的编辑器都得到了适当的支持,所以如果你的首选是 Vim、Emacs 或其他任何一种,你都会在那里找到一个高质量的 Rust 扩展。网站areweideyet.com/应该会提供当前的情况。
我们将介绍来自微软的轻量级 IDE,Visual Studio Code及其最新的 Rust 扩展,简单地称为Rust。这个 IDE 应该在不同桌面环境中都能很好地工作。安装说明和多个平台的包可在 Visual Studio Code 的主站点code.visualstudio.com找到。
- 打开 Visual Studio Code 并转到命令面板,可以通过视图菜单或通过键盘快捷键Ctrl + Shift + P(可能在不同的平台上有所不同)。输入
install extension以查找正确的命令,然后选择安装扩展:

- 在选择此选项后,在下一个字段中输入
rust以查找 Rust 扩展。在撰写本文时,最新版本是由kalitaalexey制作的:

- 你可以通过按安装按钮立即安装 Rust;或者,点击列表项本身以首先显示有关扩展的信息。安装后,重新加载编辑器。Rust 扩展现在已安装并准备好使用!
你的第一个 Rust 项目
你的第一个 Rust 项目可能不会特别出色。如果有的话,它将服务于以下四个目的:
-
展示 Rust 项目的结构
-
展示如何手动创建项目
-
展示如何使用 Rust Cargo 脚本创建项目
-
编译和执行程序
Rust 项目的结构
不论你在哪个平台上开发,Rust 项目都将具有以下结构:

前面的截图显示了最简单的 Rust 项目的结构,因此可以使用以下命令进行复制:
| OS X/Linux | Windows(从命令提示符) |
|---|
|
mkdir firstproject cd firstproject touch Cargo.toml mkdir src cd src touch main.rs
|
md firstproject cd firstproject md src echo $null >> Cargo.toml cd src echo $null >> main.rs
|
echo $null >> filename命令可以在不启动记事本的情况下创建一个空文件;保存文件并退出。
Cargo.toml文件是 Rust 的Makefile等效物。当手动创建.toml文件时,应将其编辑为包含类似以下内容:

Rust 项目的结构可以扩展,包括文档以及构建结构,如下所示:

自动化
虽然手动创建 Rust 项目没有错,但 Rust 确实附带了一个非常方便的实用工具,称为Cargo。Cargo 不仅可以自动化项目的设置,还可以编译和执行 Rust 代码。Cargo 可以用来创建库所需的组件,而不是可执行文件,还可以生成应用程序文档。
使用 Cargo 创建二进制包
就像任何其他脚本一样,Cargo 默认在当前工作目录上工作。(例如,在编写本章时,我的示例代码的工作目录在 Mac 和 Linux 系统上为 ~/Developer/Rust/chapter0,在 Windows 10 系统上为 J:\Developer\Rust\Chapter0。)
在其最简单的形式中,Cargo 可以生成正确的文件结构,如下所示:
cargo new demo_app_name -bin
前面的命令告诉 Cargo 创建一个名为 demo_app_name 的新结构,并且它应该是一个二进制文件。如果您移除 -bin,它将创建一个名为的结构,这将是一个库(或者更准确地说,是除了二进制文件之外的东西)。
如果您不想使用根目录(比如说您想在您的二进制框架内创建一个库),那么在 demo_app_name 之前,您应该附加与您的工作目录相关的结构。
在我之前给出的简单示例中,如果我想在我的二进制结构中创建一个库,我会使用以下命令:
cargo new app_name/mylib
这将创建如下结构:

Cargo.toml 文件在此阶段无需编辑,因为它包含了我们在手动创建项目时必须手动输入的信息。
Cargo 有多个目录分隔符“翻译器”。这意味着前面的示例可以在 OS X、Linux 和 Windows 上使用而不会出现问题;Cargo 已经将 / 转换为 \ 以适应 Windows。
使用 Cargo 构建和运行应用程序
由于我们都能创建目录结构,因此 Cargo 能够构建和执行我们的源代码。
如果您查看本章附带源代码,您将找到一个名为 app_name 的目录。要使用 Cargo 构建此包,请在终端(或 Windows 上的命令)窗口中输入以下内容:
cd app_name
cargo build app_name
这将构建源代码;最后,您将被告知编译已成功完成:

接下来,我们可以使用 Cargo 如下执行二进制文件:
cargo run
如果一切顺利,您将看到以下内容:

就像任何一种工具一样,可以将构建和执行“链式连接”到一行中,如下所示:
cargo build; cargo run
您可能想知道为什么第一个操作是移动到应用程序结构而不是直接输入 cargo build。这是因为 Cargo 正在寻找 Cargo.toml 文件(记住,这充当构建脚本)。
使用 Cargo 清理源代码树
当 Rust 编译器编译源文件时,它会生成一个称为对象文件的东西。对象文件将源文件(我们可以阅读和理解)编译成可以与其他库连接以创建二进制的形式。
这是一个好主意,因为它可以减少编译时间;如果源文件没有更改,就没有必要重新编译文件,因为对象文件将是相同的。
有时,目标文件会过时,或者另一个目标文件中的代码由于冲突导致恐慌。在这种情况下,清理构建是很常见的。这会删除目标文件,然后编译器必须重新编译所有源文件。
此外,它应该在创建发布构建之前始终执行。
标准的 Unix make程序使用clean命令(make clean)执行此操作。Cargo 以类似于 Unix 中的make实用程序的方式执行清理操作:
cargo clean
比较目录显示了使用前面的 Cargo 命令会发生什么:

整个目标目录结构已经被简单地删除(前面的截图来自 Mac,因此有dSYM和plist文件。这些在 Linux 和 Windows 上不存在)。
使用 Cargo 创建文档
与其他语言一样,Rust 能够根据源文件的元标签创建文档。以下是一个示例:
fn main()
{
print_multiply(4, 5);
}
/// A simple function example
///
/// # Examples
///
/// ```
/// print_multiply(3, 5);
///
/// ```rs
fn print_multiply(x: i32, y: i32)
{
println!("x * y = {}", x * y);
}
以///开头注释将被转换为文档。
文档可以通过两种方式之一创建:通过 Cargo 或使用rustdoc程序。
rustdoc 与 Cargo
与 Cargo 提供的其他操作一样,当创建文档时,它充当 rustdoc 的包装器。唯一的区别是,使用 rustdoc 时,你必须指定源文件所在的目录。在这种情况下,Cargo 表现得比较笨拙,并为所有源文件创建文档。
在最简单的形式中,rustdoc 命令的使用方法如下:
cargo doc
rustdoc src/main.rs
Cargo 确实有优势,可以在root文件夹内创建文档结构,而 rustdoc 则在目标文件夹内创建结构(使用cargo clean删除)。
使用 Cargo 帮助进行单元测试
希望单元测试不是你陌生的东西。单元测试是一种针对特定函数或方法而不是整个类或命名空间的测试。它确保函数在其呈现的数据上正确运行。
Rust 中的单元测试非常简单创建(在assert_unittest和unittest目录中给出了两个示例)。以下是从unittest示例中摘录的:
fn main() {
println!("Tests have not been compiled, use rustc --test instead (or cargo test)");
}
#[test]
fn multiply_test()
{
if 2 * 3 == 5
{
println!("The multiply worked");
}
}
当构建并执行时,你可能会对以下结果感到惊讶:

尽管乘法2 x 3不等于5,但这个单元测试仍然通过的原因是单元测试不是测试操作的结果,而是测试操作本身是否工作。从早期阶段理解这种区别非常重要,以防止以后产生混淆。
我们遇到了单元测试的限制:如果我们不是测试数据而是测试操作,我们如何知道操作的结果本身是正确的?
坚定自我!
单元测试为开发者提供了一系列称为断言方法的方法:
#[test]
fn multiply()
{
assert_eq!(5, 2 * 3);
}
assert_eq! (assert equal) macro. The first argument is the answer expected, and the second argument is what is being tested. If *2 * 3 = 5*, then the assertion is true and passes the unit test.
Cargo 能做什么?
对于 Rust 开发者来说,Cargo 是一个惊人的实用工具。除了这些常用功能外,它还有其他命令,如下表所示。所有命令都遵循以下形式:
cargo <command> <opts>
| 命令 | 执行的操作 |
|---|
|
fetch
此命令从网络中获取软件包的依赖项。如果可用锁文件,此命令将确保所有 Git 依赖项和/或注册表依赖项都已下载并本地可用。在锁文件更改之前,cargo fetch 之后永远不会调用网络。如果锁文件不可用,则此操作相当于 cargo generate-lockfile。将生成锁文件,并更新所有依赖项。 |
|---|
|
generate-lockfile
此命令为项目生成锁文件。锁文件通常在发出 cargo build 命令时生成(你将在目录结构中看到它作为 Cargo.lockfile)。 |
|---|
|
git-checkout
| 此命令检出 Git 仓库。你需要使用以下形式:
cargo git-checkout -url=URL
|
|
locate-project
| 此命令定位软件包。 |
|---|
|
login
| 此命令将注册表中的 API 令牌保存到本地。调用形式如下:
cargo login -host=HOST token
|
|
owner
| 此命令管理注册表上软件包的所有者。这允许更改软件包的所有权(软件包是一个 Rust 库),以及向软件包添加令牌。此命令将修改指定注册表(或默认注册表)上软件包的所有者。请注意,软件包的所有者可以上传新版本、撤回旧版本,还可以修改所有者集合,因此请谨慎操作! |
|---|
|
package
| 此命令将本地软件包组装成可分发的 tarball。 |
|---|
|
pkgid
| 此命令打印一个完全限定的软件包规范。 |
|---|
|
publish
| 此命令将软件包上传到注册表。 |
|---|
|
read-manifest
此命令读取清单文件(.toml)。 |
|---|
|
rustc
此命令编译完整软件包。当前软件包指定的目标将与所有依赖项一起编译。指定的选项将全部传递给最终的编译器调用,而不是任何依赖项。请注意,编译器仍将无条件接收如 -L、--extern 和 --crate-type 等参数,而指定的选项将简单地添加到编译器调用中。此命令要求只编译一个目标。如果当前软件包有多个目标可用,则必须使用 --lib、--bin 等过滤器来选择要编译的目标。 |
|---|
|
search
此命令在 crates.io/ 搜索软件包。 |
|---|
|
update
| 此命令根据本地锁文件中记录的依赖项更新依赖项。典型选项包括:
-
--package SPEC(要更新的软件包) -
--aggressive(强制更新<name>的所有依赖项) -
--precise PRECISE(更新单个依赖项到精确的PRECISE)
此命令要求已存在一个 Cargo.lock 文件,该文件由 cargo build 或相关命令生成。如果给出了包规范名称(SPEC),则将执行保守的锁文件更新。这意味着只有由 SPEC 指定的依赖项将被更新。如果必须更新依赖项才能更新 SPEC,则其传递依赖项也将被更新。所有其他依赖项将保持锁定在其当前记录的版本。如果指定了 PRECISE,则不得同时指定 --aggressive。PRECISE 是一个字符串,表示正在更新的包应该更新到的精确修订版本。例如,如果包来自 Git 仓库,则 PRECISE 将是仓库应该更新到的确切修订版本。如果没有给出 SPEC,则将重新解析所有依赖项并更新。 |
|
verify-project
| 此命令确保项目被正确创建。 |
|---|
|
version
| 此命令显示 Cargo 的版本。 |
|---|
|
yank
此命令从索引中移除已推送的 crate。yank 命令从服务器的索引中移除之前推送的 crate 版本。此命令不会删除任何数据,crate 仍然可以通过注册表的下载链接进行下载。请注意,已锁定到已移除版本的现有 crate 仍然可以下载已移除的版本以供使用。然而,Cargo 不会允许任何新的 crate 锁定到任何已移除的版本。 |
|---|
如你现在所欣赏的,Cargo 工具脚本非常强大且灵活。
摘要
我们现在已经安装了一个完整的 Rust 环境,并准备开始使用。我们已经解释了如何手动和通过 Cargo 工具设置项目,你应该已经对 Cargo 的有用性有了认识。
在下一章中,我们将探讨任何语言的基础:变量。
第二章:变量
与所有编程语言一样,我们需要一种方式在我们的应用程序中存储信息。这些信息可以是任何东西,并且与每种其他语言一样,它存储在变量中。然而,与每种其他语言不同,Rust 不按(比如说)C 的方式存储数据。
因此,在本章中,我们将做以下事情:
-
理解变量可变性
-
看看 Rust 如何在变量中存储信息,以及可用的变量类型
-
看看 Rust 如何处理向量变量类型
-
理解 Rust 如何以及如何不能操作变量
-
看看 Rust 如何传递变量
-
看看 Rust 如何内部存储变量
变量可变性
与许多其他语言不同,Rust 默认变量不可变。这意味着如果未明确定义为可变的,变量绑定实际上是常量。编译器会检查所有变量突变,并拒绝接受可变不可变变量绑定。
如果你来自 C 语言家族之一,不可变可以被认为是与 const 类型大致相同。
创建变量
在 Rust 中创建新的变量绑定,我们使用以下形式:
let x = 1;
这意味着我们创建了一个名为 x 的新变量绑定,其内容将是 1。数字的默认类型取决于具体情况,但通常是一个 32 位有符号整数。如果我们需要一个可以改变的变量,我们使用以下形式:
let mut x = 1;
默认情况下,Rust 中的所有变量都是不可变的;因此,我们必须明确地将变量定义为可变的。
我们如何让编译器知道我们想让 x 是一个整型?
Rust 有一种方式可以通知编译器和开发者变量类型。例如,对于 32 位的 int,我们会使用以下形式:
let x = 1i32;
换句话说,x = 1,一个 32 位有符号的 int。
如果没有定义 i32(或任何其他值),编译器将根据值的用法决定类型,默认为 i32。
定义其他变量类型
其他变量类型可以像 int 变量一样声明。
浮点数
与其他语言一样,在 Rust 中可以执行浮点运算。与整型变量一样,浮点型变量定义为 32 位 float 如下:
let pi = 3.14f32;
对于 64 位的 float,它将被定义为如下:
let pi = 3.14f64;
变量是字面值。另一种声明大小的方式是通过类型:
let pi: f32 = 3.14;
如果省略了类型(例如,let x = 3.14),变量将被声明为 64 位浮点变量。
有符号和无符号整数
一个有符号的 int(可以具有正负值)的定义如下:
let sint = 10i32;
无符号的 int 在定义中用 u 代替 i:
let usint = 10u32;
再次,这些是数字字面量,可以通过类型进行相同的声明:
let sint: i32 = 10;
有符号和无符号的 int 值可以是 8、16、32 或 64 位长。
常量和静态
Rust 有两种类型的常量:const和static。Const 有点像别名:它们的内含在它们被使用的地方被替换。语法如下:
const PI: f32 = 3.1415927;
静态变量更像是变量。它们具有程序的全球作用域,并且如下定义:
static MY_VARIABLE: i32 = 255;
它们不能被更改。
Rust 能够猜测局部函数变量的类型。这被称为局部类型推断。然而,它只是局部的,所以静态和常量的类型必须始终显式指定。
在使用前定义变量值
虽然在某些语言中这不是强制性的,但在 Rust 中,变量必须有一个初始值,即使它是零。这是一个好习惯,也有助于调试,因为所有变量都有已知的内容。如果没有,就存在未定义行为的风险。
未定义行为意味着程序执行的动作是任何人都可以猜测的。例如,如果变量没有初始值,它们的值将是分配值时内存中任意的内容。
字符串
通常,字符串可以通过两种方式之一来定义:
let myName = "my name";
这被称为字符串切片。这些将在稍后处理。
第二种方式是使用String::new();。这是一个字符串,首字母大写 S。它在堆上分配,并且可以动态增长。
在这一点上,打破当前的叙述并讨论 Rust 如何使用内存是个好主意,因为它将极大地帮助解释即将到来的许多主题。
Rust 如何使用内存
任何 Rust 程序占用的内存分为两个不同的区域:堆和栈。简单来说,栈包含原始变量,而堆存储复杂类型。就像我女儿卧室地板上的混乱一样,堆可以不断增长,直到可用内存耗尽。栈更快、更简单,但可能不会无限增长。Rust 中的每个绑定都在栈上,但这些绑定可能指向堆或其他地方的东西。
所有这些都直接与字符串示例相关。绑定myName在栈上,并引用一个字面量静态字符串*my name*。这个字符串是静态的,意味着它在程序开始时就在内存中。它也是静态的,这意味着它不能被改变。
String::new另一方面,在堆上创建一个字符串。它最初是空的,但可能增长以填充整个虚拟内存空间。
这里是一个正在增长的字符串的例子:
let mut myStringOne = "This is my first string ".to_owned();
let myStringTwo = "This is my second string. ";
let myStringThree = "This is my final string";
myStringOne = myStringOne + myStringTwo + myStringTwo + myStringThree + myStringTwo;
创建字符串的一种方法是在字符串切片上调用to_owned方法,就像我们刚才做的那样。还有其他方法,但这是最推荐的一种,因为它强调了所有权问题。我们稍后会回到这个问题。
在这里,绑定myStringOne最初是 24 个字符长,并且至少会在堆上分配那么大的空间。绑定myStringOne实际上是myStringOne在堆上位置的引用。
当我们向myStringOne添加内容时,它在堆上占用的空间会增加;然而,对基本位置的引用保持不变。
变量的生命周期和作用域必须考虑。例如,如果我们在一个函数的部分定义了一个字符串,然后尝试在函数外部访问这个字符串,我们会得到编译器错误。
回到字符串
正如我们之前在堆和栈上看到的,我们也可以这样定义一个字符串:
let mut myString = String::new();
String::告诉编译器我们将使用标准库中的String,我们告诉程序我们将创建一个可变的字符串,并在名为myString的地方将其引用存储在栈上。
动态字符串可以创建为空,或者预先为其分配内存。例如,如果我们想存储单词You'll never walk alone(总共 23 个字节),预先为它们分配空间。这是如何做的:
let mut ynwa = String::with_capacity(23);
ynwa.push_str("You'll never walk alone");
这只是一个性能优化,通常不是必需的,因为当字符串需要增长时,它们会自动增长。以下做了大致相同的工作:
let mut ynwa = "You'll never walk alone".to_owned();
Rust 字符串不是以空字符终止的,完全由有效的 Unicode 组成。因此,它们可以包含空字节和任何语言的字符,但它们可能需要的字节数比它们包含的字符数多。
字符串切片
字符串切片一开始可能会让人感到困惑。我们这样定义一个字符串切片:
let homeTeam = "Liverpool";
来自更动态的语言,你可能会认为我们在将字符串Liverpool赋值给变量绑定homeTeam。然而,实际情况并非如此。homeTeam绑定实际上是一个字符串切片:一个指向字符串某一部分的引用,而这个字符串实际上位于别处。
字符串切片也不是可变的。
以下在 Rust 中是不行的:
let homeTeam = "Liverpool";
let result = " beat ";
let awayTeam = "Manchester United";
let theString = homeTeam + result + awayTeam;
编译器不会允许这样做,并且会给出如下错误:

我们不能直接连接字符串切片,因为字符串切片是不可变的。为了做到这一点,我们首先需要将字符串切片转换成可变的东西,或者使用类似format!宏的方式来构建字符串。让我们都试一试。
和之前一样,to_owned()方法接受方法附加到的切片,并将其转换为String类型:
fn main() {
let homeTeam = "Liverpool";
let result = " beat ";
let awayTeam = "Manchester United";
let fullLine = homeTeam.to_owned() + result + awayTeam;
println!("{}", fullLine);
}
to_owned()方法只应用于第一个切片。这把字符串切片homeTeam转换成 String,在 String 上使用+操作符是可行的。
当构建并执行时,你会看到以下内容:

这些警告是什么意思?
Rust 推荐使用的格式是蛇形命名法(而不是驼峰命名法)。如果我们把变量名从homeTeam改为home_team,就可以移除警告。这并不是致命的,或者不太可能导致程序进行杀戮性的狂暴;这更多的是一个风格问题。
使用format!宏
format!宏的工作方式与其他语言的字符串格式化器类似:
fn main() {
let home_team = "Liverpool";
let result = " beat ";
let away_team = "Manchester United";
let full_line = format!("{}{}{}", home_team, result, away_team);
println!("{}", full_line);
}
格式字符串中的{}标记了后续参数的位置。这些位置按顺序填充,因此full_line将是home_team、result和away_team的连接。
当上述代码片段编译并执行时,你会看到以下内容:

构建字符串
我们已经看到,我们可以从字符串切片(使用to_owned()或format!宏)创建一个 String,或者我们可以使用String::new()创建它。
有两种进一步的方法可以帮助构建字符串:push向字符串添加单个字符,而push_str向字符串添加一个str。
以下展示了这一过程:
fn main() {
let home_team = "Liverpool";
let result = " beat ";
let away_team = "Manchester United";
let home_score = '3'; // single character
let away_score = "-0";
let mut full_line = format!("{}{}{} ", home_team, result, away_team);
// add the character to the end of the String
full_line.push(home_score);
// add the away score to the end of the String
full_line.push_str(away_score);
println!("{}", full_line);
}
当这段最后代码编译并执行时,你会看到以下内容:

代码审查
上述代码与之前示例中的代码略有不同,我们之前只是简单地使用to_owned()将切片转换为字符串。我们现在必须创建一个可变字符串并将其分配给该字符串,而不是像之前那样只添加到full_line的末尾。
原因是正在转换为字符串的切片不是可变的;因此,创建的类型也将是非可变的。由于你不能向非可变变量添加内容,所以我们不能使用push和push_str方法。
投掷
Rust 允许变量以不同的方式进行转换。这是通过使用as关键字实现的。这与 C#中的用法相同:
let my_score = 10i32;
let mut final_score : u32 = 100;
let final_score = my_score as u32;
我们还可以将类型转换为其他类型(例如,float到int):
let pi = 3.14;
let new_pi = pi as i32; // new_pi = 3
然而,像这种精度丢失的投掷效果可能并不理想。例如,如果你将一个超过i8位大小的浮点数投掷到i8,数字会被截断为0:
let n = 240.51;
let n_as_int = n as i8; // n_as_int = 0
如果你尝试转换的类型不兼容,将会发生错误;例如:
let my_home = "Newton-le-Willows";
let my_number = my_home as u32; // cannot convert &str to u32
Rust 不会在原始类型之间进行隐式转换,即使这样做是安全的。也就是说,如果一个函数期望一个i8作为参数,你必须将i16值转换为i8然后再传递。这样做的原因是为了实现最大程度的类型检查,从而减少潜在(且更复杂)的隐藏错误数量。
字符串方法
在任何语言中,字符串都很重要。没有它们,与用户的沟通变得困难,如果数据来自网络服务(以 XML、纯文本或 JSON 的形式),则需要对这些数据进行操作。Rust 为开发者提供了标准库中的许多方法来处理字符串。以下是一些有用方法的表格(现在不用担心类型):
| 方法 | 作用 | 用法(或示例项目) |
|---|---|---|
from(&str) -> String |
此方法从一个字符串切片创建一个新的 String。 |
let s = String::from("Richmond");
|
from_utf8(Vec<u8>) -> Result<String, FromUtf8Error> |
此方法从一个有效的 UTF-8 字符向量创建一个新的字符串缓冲区。如果向量包含非 UTF-8 数据,它将失败。 |
|---|
let s = String::from_utf8(vec!(33, 34)).expect("UTF8 decoding failed);
|
with_capacity(usize) -> String |
此方法预先分配一个具有指定字节数的 String。 |
|---|
let s = String::with_capacity(10);
|
as_bytes -> &[u8] |
此方法将字符串输出为字节切片。 |
|---|
let s = "A String".to_owned();
let slice = s.as_bytes();
|
insert(usize, char) |
此方法在位置 index 插入 char。 |
|---|
let mut s = "A String".to_owned();
s.insert(2, 'S');
// s = "A SString"
|
len -> usize |
此方法返回字符串的字节长度。因此,它可能大于字符串中的字符数。 |
|---|
let s = "A String äö";
// s.len() => 13
|
is_empty -> bool |
此方法返回 true,如果字符串为空。 |
|---|
let s1 = "".to_owned();
let s2 = "A String".to_owned();
// s1.is_empty() => true
// s2.is_empty() => false
|
is_char_boundary(usize) -> bool |
此方法返回 true,如果索引处的字符位于 Unicode 边界上。 |
|---|
let s1 = "Hellö World";
// s1.is_char_boundary(5) => false
// s1.is_char_boundary(6) => true
|
泛型和数组
对于来自 C# 或 C++ 背景的人来说,无疑已经习惯了泛型类型(通常称为具有类型 T);你会习惯看到如下内容:
T a = new T();
泛型允许为几种类型定义方法。在其最一般的形式中,T 表示“任何类型”。例如,以下函数接受两个可以是任何类型 T 的参数:
fn generic_function<T>(a: T, b: T)
T,如前所述,可以是任何类型。这意味着我们无法对它们做太多,因为只有少数方法实现了“任何类型”。例如,如果我们想将这些变量相加,我们需要对泛型类型进行一些限制。我们本质上需要告诉 Rust,“T 可以是任何类型,只要它实现了加法。”关于这一点稍后会有更多介绍。
数组
数组易于构建。例如:
let my_array = ["Merseybus", "Amberline", "Crosville", "Liverbus", "Liverline", "Fareway"];
数组必须遵守一些规则,如下所示:
-
数组具有固定的大小。它永远不会增长,因为它作为连续的内存块存储。
-
数组的内容只能是单一类型。
与任何类型的变量一样,默认情况下数组是不可变的。即使数组是可变的,整体大小也不能改变。例如,如果一个数组有五个元素,它不能变成六个。
我们还可以创建具有类型的数组,如下所示:
let mut my_array_two: [i32; 4] = [1, 11, 111, 1111];
let mut empty_array: [&str; 0] = [];
还可以通过以下方式创建具有相同值的数组多次:
let number = [111; 5];
这将创建一个名为 number 的数组,包含 5 个元素,所有元素都初始化为值 111。
数组性能
虽然数组很有用,但它们确实有性能损失;与数组上的大多数操作一样,Rust 运行时会执行边界检查以确保程序不会访问数组越界。这防止了经典的数组溢出攻击和错误。
向量
虽然数组易于使用,但它们有一个很大的缺点:它们不能调整大小。向量(Vec)的行为类似于 C# 中的 List。它也是一个泛型类型,因为 Vec 本身实际上是 Vec<T>。
Vec 类型位于标准库中(std::vec)。
创建向量时,我们使用类似于以下任一的方法:
let mut my_vector: Vec<f32> = Vec::new(); // explicit definition
或者这样:
let mut my_alt_vector = vec![4f32, 3.14, 6.28, 13.54, 27.08];
Vec 宏中的 f32 告诉编译器向量的类型是 f32。f32 可以省略,因为编译器可以确定向量的类型。
创建具有初始大小的向量
与字符串一样,可以创建一个具有初始内存分配的向量,如下所示:
let mut my_ids: Vec<i64> = Vec::with_capacity(30);
通过迭代器创建向量
创建向量的另一种方法是使用迭代器。这是通过collect()方法实现的:
let my_vec: Vec<u64> = (0..10).collect();
迭代器的格式非常方便。与let foo = {0,1,2,3};这样的类似,这被缩短为使用..,这意味着所有在a和b之间的数字(b被排除 - 因此0 .. 10创建一个包含 0,1,2,3,4,5,6,7,8,9 的向量)。这可以在本书提供的源示例中看到。
向量中添加和删除元素
类似于字符串,可以使用push和pull方法向向量列表中添加和删除元素。这些方法从向量栈的顶部添加或删除。考虑以下示例:
fn main() {
let mut my_vec : Vec<i32> = (0..10).collect();
println!("{:?}", my_vec);
my_vec.push(13);
my_vec.push(21);
println!("{:?}", my_vec);
let mut twenty_one = my_vec.pop(); // removes the last value
println!("twenty_one= {:?}", twenty_one);
println!("{:?}", my_vec);
}
我们使用从 0 开始到 10(因此最后一个值是 9)的值创建向量列表。
行println!("{:?}", my_vec);输出my_vec的全部内容。{:?}在这里是必需的,因为Vec<i32>没有实现某些格式化功能。
然后我们在向量列表的顶部添加 13 和 21,显示输出,然后从向量列表中移除最顶部的值,再次输出。
通过切片操作数组或向量
数组和向量都可以使用一个值(如my_vec[4])来访问。然而,如果你想操作数组的一部分,那么你将从一个数组中取一个切片。切片就像是对原始事物一部分的窗口。
要创建一个切片,使用以下方法:
let my_slice = &my_vec[1..5];
切片也没有预定义的大小:它可以是 2 字节,也可以是 202 字节。因此,切片的大小在编译时是未知的。这一点很重要,因为它阻止了某些方法的工作。
传递值
到目前为止,我们一直在单个方法内保持一切。对于小型演示(或方法测试),这是可以的。然而,对于更大的应用程序,方法之间传递值是必不可少的。
Rust 有两种主要方式将信息传递给其他方法:通过引用或通过值。通过引用通常意味着借用,这意味着所有权只是暂时性地给出,函数调用后可以再次使用。通过值意味着所有权的永久性改变,这意味着函数的调用者将无法访问该值,或者它可能意味着复制数据。
通过值传递
以下代码显示了如何在两个函数之间传递一个数字,并接收一个结果:
fn main()
{
let add = add_values(3, 5);
println!("{:?}", add);
}
fn add_values(a: i32, b: i32) -> i32
{
a + b
}
让我们看看接收函数的定义行:
fn add_values(a: i32, b: i32) -> i32
与任何编程语言一样,我们必须给函数一个名字,然后是一个参数列表。参数名称后面跟着一个冒号和参数的类型。
我们的功能返回一个特定类型的值(在这种情况下,i32)。只要你不意外地在那里放置分号,函数中最后评估的内容将从函数返回。隐式返回语句也存在,但它不是必需的,如果可能的话,通常更好的风格是省略它。
构建并运行后,你会看到以下内容:

通过引用传递
通过引用传递的变量看起来是这样的:
fn my_function(a: &i32, b: &i32) -> i32
我们将两个变量作为引用,并返回一个值。
要从引用中获取值,首先要做的就是取消引用。这是通过星号(*)操作符完成的:
let ref_num = &2;
let deref_num = *ref_num;
// deref_num = 2
引用类型
引用可以以三种方式之一书写:&、ref 或 ref mut:
let mut var = 4;
let ref_to_var = &var;
let ref second_ref = var;
let ref mut third_ref = var;
这里的引用都是等效的。然而,请注意,前面的代码由于可变引用规则而无法正常工作。Rust 允许多个不可变引用指向一个东西,但如果取了可变引用,则在此期间不能存在其他引用。因此,最后一行将不会工作,因为已经有两个活跃的引用指向 var。
一个实际例子
在示例代码 matrix 中,我们可以看到如何使用二维数组以及如何通过引用传递,接收函数计算矩阵乘法的结果。让我们来检查一下代码:
fn main()
{
// first create a couple of arrays - these will be used
// for the vectors
let line1: [i32; 4] = [4, 2, 3, 3];
let line2: [i32; 4] = [3, 4, 5, 7];
let line3: [i32; 4] = [2, 9, 6, 2];
let line4: [i32; 4] = [5, 7, 2, 4];
// create two holding arrays and assign
// we are creating an array of references
let array_one = [&line1, &line3, &line4, &line2];
let array_two = [&line2, &line1, &line3, &line4];
// let's do the multiply
// we are passing in a ref array containing ref arrays
let result = matrix_multiply(&array_one, &array_two);
println!("{:?}", result);
}
fn matrix_multiply(vec1: &[&[i32;4];4], vec2: &[&[i32;4];4]) -> [[i32; 4];4]
{
// we need to create the arrays to put the results into
let mut result = [[0i32; 4]; 4];
// loop through the two vectors
for vone in 0..4
{
for vtwo in 0..4
{
let mut sum = 0;
for k in 0..4
{
sum += vec1[vone][k] * vec2[k][vtwo];
}
result[vone][vtwo] = sum;
}
}
result
}
编译后,你会得到以下输出:

我们在这里真正需要考虑的是 matrix_multiply 函数的定义行:
fn matrix_multiply(vec1: &[&[i32;4];4], vec2: &[&[i32;4];4]) -> [[i32; 4];4]
如果你还记得我们之前是如何告诉函数变量名和类型的,我们说它是 variable_name: variable_type。前面的行可能看起来非常不同,但实际上并不是:

我们正在传递一个持有数组的引用,该数组持有其他数组的引用。数组使用 [i32;4] 定义;因此,引用是 &[i32;4]。这是内部数组。外部数组 [i32;4] 也是一个引用(&[i32;4]),大小为 4。因此,当我们把它们放在一起时,我们就有以下内容:

上述例子很好地展示了如何通过引用传递,尽管在现实中,编译器很可能会优化这个小数据样本,使其运行更快。然而,它确实展示了如何操作。
金规则是,你发送给函数的内容必须与函数期望的内容相匹配。
摘要
我们在本章中涵盖了大量的内容,我真心鼓励你尝试创建函数并传递值。
如果你不想每次创建新应用程序时都不断创建新项目,你可以在 Rust Playground 网站上创建和测试你的代码(play.rust-lang.org)。在这里,你可以输入你的代码,点击运行,立即查看你所写的内容是否有效。
在下一章中,我们将介绍如何获取信息,以及验证你的输入。
第三章:输入和输出
到目前为止,我们只看到了来自我们示例的数据,并且只使用了 println! 宏函数。虽然 println! 宏非常有用,但我们真的需要查看输出。我们还需要知道如何获取数据,一旦数据进入,我们必须检查输入的类型是否是所需的类型。
在本章中,我们将涵盖以下主题:
-
检查输出数据的方式
-
检查如何将数据输入到应用程序中
-
使用命令行参数启动程序
-
讨论 Rust 中的方法与其他语言中的方法的不同之处
-
标准库的简要介绍
Rust 中的函数和方法
当我们看 C++或 C#时,方法是一个类内的编程单元,它执行特定的任务。Rust 中的方法是与复合数据结构或结构体相关联的函数。这些方法通过使用 self 参数来访问对象的数据。它们在 impl 块中定义,如下面的示例所示(更完整的示例可以在源示例中找到):
struct Point {
x: f64,
y: f64
}
impl Point {
fn origin() -> Point {
Point {x: 0.0, y: 0.0 }
}
fn new(my_x: f64, my_y: f64) -> Point {
Point { x: my_x, y: my_y }
}
}
在这里,我们定义了一个名为 Point 的结构体,用于表示二维空间中的点。然后,我们为该结构体定义了两个构造方法:origin 用于创建位置在 0,0 的新点,另一个用于创建任意新点。
println! 和 println 之间的区别
到目前为止,我们使用 println! 来输出文本。这是可以的,但考虑一下 println! 做了什么。每次你看到 ! 标记时,它都象征着宏。宏用于在编译时而不是在运行时执行函数的一部分。
考虑以下内容:
println!("{}", a);
Console.WriteLine("{0}", a);
a on the line. In this case, a can be of any type that supports conversion to a formatted output. The same applies to Rust. A line is output with the value of a.
println! 宏实际上是在 Rust 标准库中实现的。
标准库简介
为了理解 println! 的来源,我们需要简要地看一下 Rust 标准库。如果你熟悉 C、C++或 C#(或任何其他常用语言),你可能会用到类似的东西:
#include <stdio.h>
#include <stdlib>
using System.Collections.Generic;
这些是编译器附带的标准库,开发者可以选择性地包含。它们包含许多有用的过程、函数和方法,所有这些都是为了使开发更简单,这样你就不需要在需要执行常见任务时不断重新发明轮子。
在 Rust 中,类似的系统以 crate 的形式存在。std crate 包含 Rust 标准库,并且默认包含在所有其他 crate 中。这意味着你可以使用那里的功能,而无需额外步骤。
箱子被进一步分为模块层次结构,其中双冒号 :: 是路径的分隔符。例如,std::fmt 是 std 模块内的 fmt 模块。它包含字符串格式化和打印功能。例如,我们之前已经使用过的 println! 宏就在那里。
那么为什么我们每次使用 println! 宏时都不需要写 std::fmt::println! 呢?因为 println! 是许多自动导入到每个命名空间的标准宏之一。
您也可以自己将事物导入当前命名空间,以节省一些按键。这是通过使用关键字完成的。以下是一个使用标准库中的 HashMap 集合类型的示例,而不使用使用关键字:
let mut my_hashmap: std::collections::HashMap<String, u8> =
std::collections::HashMap::new();
my_hashmap.insert("one".to_owned(), 1);
每次都明确地拼写完整的命名空间是可能的,但如您所见,噪声与信号比略低。将 HashMap 导入当前命名空间可以有所帮助。这段代码与之前的代码等价:
use std::collections::HashMap;
let mut my_hashmap: HashMap<String, u8> = HashMap::new();
my_hashmap.insert("one".to_owned(), 1);
Rust 的库系统与其他语言略有不同,因此可能对新手来说是一个绊脚石。我发现意识到使用子句不是使代码可见和可调用的必要条件是有用的:它们只是将命名空间导入到当前命名空间中。
库
std 库定义了我们已经遇到的原始类型(array、不同大小的浮点数和整数、String 等),但也包含了许多其他模块。它们还定义了常用的宏(如 write! 和 println!)。
为了本章的目的,我们将仅涵盖 std::io、std::fs 和 std::fmt。这些模块涉及输入/输出、文件系统和格式化。io 和 fs 模块将在本章后面讨论。
控制输出格式
std::fmt 模块为开发者提供了一系列用于格式化和打印字符串的实用工具。让我们从 format! 宏开始。这个宏返回一个字符串。
我们已经看到,如果我们使用 println!(Hello {}, myString),代码将在 Hello 后打印 myString 的内容。format! 宏的工作方式几乎相同,只是它返回格式化的字符串而不是输出它。实际上,println! 在底层本质上使用 format!。
定位输出
在 C# 中,更有用的扩展之一是 string.Format(...);。这允许根据特定位置的参数构建字符串。例如,以下语句构建了一个字符串,其中字符串字面量之后的参数位置被插入到字符串中(在这里,字母 B 在字符串中间和末尾各插入了一次):
var myString = string.Format("Hello {0}, I am a {1}{1}{2} computer model {1}", name, "B", "C");
Rust 也支持这种形式,但不同之处在于可以省略位置。
考虑以下示例:
format!("{} {}", 2, 10); // output 2 10
format!("{1} {} {0} {}", "B", "A");
第一个例子是我们之前看到的。格式字符串按顺序填充右侧的参数。
在第二个例子中,看起来我们要求四个参数,但实际上只提供了两个。这种方式是,当填充非位置参数时,会忽略位置参数。在编程中,索引通常从零开始。这就是处理参数的方式:
-
{1}插入第二个参数A -
{}插入第一个参数B -
{0}插入第一个参数B -
{}插入第二个参数A
因此,输出将是 A B B A。
以下是对位置参数的两个重要规则:
-
引号内的所有参数都必须使用。未能这样做将导致编译器错误。
-
你可以在格式化字符串中多次引用相同的参数。
命名参数
如格式化表所示,可以使用命名参数。这些参数的操作类似于位置参数;不过,区别在于使用的是命名参数。这在确保字符串中输出的值是正确的参数方面非常有用。
在使用命名参数时,在格式化字符串中使用空参数是完全可接受的,例如:
format!("{b} {a} {} {t}", b = "B", a = 'a', t = 33);
处理非位置参数与命名参数的规则与位置参数的规则相似:在确定位置时忽略命名参数。因此,输出将是 B a B 33。
指定参数类型
与 C 家族语言中的许多字符串处理一样,可以根据格式化字符串创建字符串(例如,{0:##.###}将给出形式为 xy.abc 的格式化输出)。
在 Rust 中也可以做类似的事情,如下所示:
let my_number = format!("{:.3}", 3.1415927);
在格式化字符串中,冒号表示我们正在请求对值的格式化。点号和数字3表示我们希望将数字格式化为三位小数。格式化器会为我们四舍五入值,因此输出将是 3.142。
格式化特性
格式化特性决定了格式化输出的生成方式。它们都以相同的方式进行使用:{:trait_name}.
以下是目前可用的特性:
| 格式化字符串 | 特性 | 含义 | 示例 |
|---|---|---|---|
{} |
显示 | 人类可读表示。并非所有事物都实现了 Display。 | 123 => "123" |
{:?} |
Debug | 内部表示。几乎一切事物都实现了 Debug。 | b"123" => [49, 50, 51] |
{:b} |
Binary | 将数字转换为二进制 | 123 => "1111011" |
{:x} |
LowerHex | 小写十六进制 | 123 => 7b |
{:X} |
UpperHex | 大写十六进制 | 123 => 7B |
{:e} |
LowerExp | 带有指数的小写数字 | 123.0 => 1.23e2 |
{:E} |
UpperExp | 带有指数的上标数字 | 123.0 => 1.23E2 |
{:p} |
指针 | 指针位置 | &123 => 0x55b3fbe72980(每次运行可能指向不同的地址) |
类似地,输出也可以使用格式化参数进行格式化。
格式化参数
实际上有四个可用的格式化参数。它们列在下面的表中:
| 参数 | 用途 |
|---|---|
Fill/Alignment |
与Width参数一起使用。基本上,如果输出小于宽度,这将添加额外的字符。 |
| Sign/#/0 | 格式化器使用的标志:
-
Sign表示符号应该始终输出(仅限数值)。如果值是正数,则+符号永远不会显示;同样,-符号仅对Signed值显示。 -
*#*表示将使用另一种打印形式。通常,如果使用{:x},则使用小写十六进制格式。通过使用#x,参数前面将加上0x。 -
0用于用0字符填充结果。它是符号感知的。
|
Width |
指定输出应该如何表示。例如,如果你有一个浮点计算,需要输出到小数点后四位,而结果只有两位小数,则宽度格式化器(与填充格式化参数结合使用)将创建所需的填充输出。 |
|---|
| Precision | 对于任何非数值,精度是最大宽度。例如,如果你有最大宽度为五,而一个包含八个字符的字符串,它将在五个字符后截断。对于整数,它被忽略。对于浮点类型,它表示小数点后的位数:
-
整数
.N::在这种情况下,N是精度。 -
整数后跟一个
$ (.N$:):这使用格式参数N作为精度。该参数必须是usize类型。 -
.*::这意味着{}中的内容与两个格式输入相关联。第一个持有usize精度,第二个持有要打印的值。
|
所有这些格式化器的示例都在本章的源代码示例中。
获取信息
到目前为止,我们一直专注于从 Rust 程序中获取信息,而不是输入信息。
输入是通过 std::io 模块完成的,使用 io::stdin() 函数获取一个读取器,然后在该读取器上调用 read_line。我们将输入的数据放入一个动态增长的 String,它需要是可变的。
输入的一个简单示例如下所示:
// 03/readline/src/main.rs
use std::io;
fn main() {
let reader: io::Stdin = io::stdin();
let mut input_text: String = String::new();
reader.read_line(&mut input_text).expect("Reading failed");
println!("Read {}", input_text);
}
我们可以在前面的代码中看到 Rust 的错误处理。read_line 方法返回一个结果类型,这意味着操作可能失败。结果类型在其内部封装了两个泛型类型,对于 read_line 来说,它们是 usize(用于报告读取了多少字节)和 io::Error(用于报告输入过程中的任何错误)。实际读取的字符串放在函数的第一个参数中,在这种情况下是 input_text。
在该结果类型上,我们的示例调用 expect 方法。它期望一切顺利,并返回第一个值(在这种情况下是 usize)。如果有错误,expect 方法会将“读取失败”打印到标准输出并退出程序。
这不是处理结果类型的唯一方法,但在我们预期事情通常能顺利进行的情况下,这是一种常见的方法。
处理错误的另一种方法是显式调用结果上的 is_err 方法。它返回一个布尔值,如下所示:
let result: Result<usize, io::Error> = reader.read_line(&mut input_text);
if result.is_err() {
println!("failed to read from stdin");
return;
}
如果我们希望进一步将输入解析为另一种类型,我们可以使用 parse 方法。
例如,如果我们想从输入中获取一个i32。read_line方法在输入数据中包含一个回车符,因此我们需要在解析之前使用trim方法将其去除:
let trimmed = input_text.trim();
let option: Option<i32> = trimmed.parse::<i32>().ok();
为了这个示例,最后一行使用ok方法将结果类型转换为Option。Option是结果的简化版本。这是一个有用的库,它可以有两种结果:Some或None。
这里,如果条目结果是None,则值不是整数,而Some将是一个整数:
match option {
Some(i) => println!("your integer input: {}", i),
None => println!("this was not an integer: {}", trimmed)
};
命令行参数
当程序启动时,它可以带参数或不带参数启动。这些参数通常在调用程序时作为参数传入。一个简单的例子是启动手册应用(在许多 BSD 和 Linux 机器上都可以找到):
man ffmpeg
在前面的语句中,man是要调用带有ffmpeg参数的程序或脚本的名称。类似地,看看以下针对 Windows 用户的示例:

记事本是程序名称,第一个参数是要读取的文件(在这个例子中,文件不存在,因此 UI 询问是否要创建它)。
一个程序加载另一个程序以执行任务的情况并不少见。
在 C 中,main的参数列表如下所示:
int main(int argc, char *argv[])
argc是argv中参数的最大数量,其中argv持有参数。在这里,程序名称是argv[0],所以所有附加参数从 1 开始。
Rust 的main不接收这样的参数。命令行参数可以通过标准库std::env::args(环境参数)获得。为了简单起见,将参数存储在Vec<String>中是很方便的,因为env::args返回一个迭代器,它产生一个String。
直接将参数传递给main的情况并不少见:
// 03/args/src/main.rs
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
println!("There was {:?} arguments passed in. They were {:?}.", args.len() - 1, &args[1..]);
}
collect方法将迭代器转换为向量,使得可以通过索引访问它。没有它,我们就必须逐个处理参数。
文件处理
我们对程序内外获取信息的巡视的最后一部分是使用文件。就 Rust 而言,文件只是另一个流,只是这个流去往别处。
当使用与文件相关的任何东西时,使用try!宏来捕获所有错误是很重要的。
从文件中读取
在这里,我们将使用std::io、std::io::prelude::*和std::fs::File。std::io是标准输入/输出库,prelude后的*表示使用预定义库中的任何内容,而std::fs是文件系统库。
文件系统调用非常特定于平台;Windows 用户使用类似C://Users/Paul/Documents/My Documents的路径作为用户的主目录,而 Linux 和 macOS 机器会使用~/作为用户的主目录。如果没有为文件指定路径,程序将假定文件位于二进制所在的同一目录中。
加载文件
要打开文件,我们使用File::open(filename)。我们可以使用try!宏或match来捕获异常,如下所示:
let file = try!(File::open("my_file.txt"))
或者可以使用以下方法:
let file = match File::open("my_file.txt") {
Ok(file) => file,
Err(..) => panic!("boom"),
}
如果文件可以打开,File::open将授予文件读取权限。为了加载文件,我们基于文件创建一个BufReader:
let mut reader = BufReader::new(&file);
let buffer_string = &mut String::new();
reader.read_line(buffer_string);
println!("Line read in: {}", buffer_string);
一旦文件被读取,可以通过reader.close()显式关闭流。然而,Rust 的资源管理系统保证了当其绑定超出作用域时文件将被关闭,所以这不是强制的。
写入文件
写入文件是一个两步过程:打开文件(如果之前不存在,则可能创建它)然后写入文件。这与 C 系列语言中写入文件的方式非常相似。
你可以通过对std::fs::File::create的单次调用来创建一个用于写入的文件。同一命名空间中的open方法用于打开文件进行读取。如果你需要更精细的权限,std::fs::OpenOptions::new创建一个对象,通过它可以调整参数然后打开文件。
与任何文件操作一样,任何操作都可能失败,因此应该始终检查结果:
let file: Result<File,Error> = options.open(path);
如前所述,Rust 经常使用泛型类型Result<T,U>作为错误捕获机制。它封装了两个值:当操作成功时使用左侧的值,当操作不成功时使用右侧的值。
一旦完成文件创建,我们就可以继续写入文件。
首先,我们检查Result比较的结果。如果没有抛出错误,则表示没有错误,然后我们可以创建一个BufWriter:
let mut writer = BufWriter::new(&file);
writer.write_all(b"hello text file\n");
我们不需要刷新缓冲区,因为write_all会为我们完成这个操作(它会在完成后调用flush())。如果你不使用write_all,那么你需要调用flush()以确保缓冲区被清除。
expect的使用
Rust 包含一个非常有用的函数expect。此方法与任何具有Option或Result类型(例如,文件写入示例中的Result)的调用一起使用。它通过将值从选项中移出并返回它来工作。如果option/result类型包含错误,expect调用将停止你的程序并打印出错误信息。
例如,以下语句将返回File或Error到file中:
let file: Result<File, Error> = options.open("my_file.txt").expect("Opening the file failed");
在unwrap方法中有一个简短的形式。这与expect方法相同,但在失败的情况下不会打印任何内容。一般来说,Some(a).unwrap()将返回a。
通常更喜欢使用expect而不是unwrap,因为完整的错误信息使得更容易找到错误来源。
XML 和 Rust
由于 Rust 非常适合在服务器上运行,因此考虑 XML 以及它在 Rust 中的处理似乎是合适的。
幸运的是,Rust 自带一个名为Xml的 crate,其工作方式与标准流读写类似。
读取文件
就像标准文件一样,我们首先需要打开文件并创建一个读取器:
let file = File::open("my_xmlfile.xml").unwrap();
let reader =BufferedReader::new(file);
接下来,我们开始读取。与普通读取器不同,我们使用EventReader。这提供了一系列事件(如StartElement、EndElement和Error),这些事件对于从不同节点读取是必需的:
let mut xml_parser = EventReader::new(reader);
接下来,我们按照以下方式遍历文件:
for e in xml_parser.events() {
match e {
StartElement { name, .. } => {
println!("{}", name);
}
EndElement {name} => {
println!("{}", name);
}
Error(e) => {
println!("Error in file: {}", e);
}
_ => {}
}
}
_ => {} essentially means that you don't care what is left, do something with it (in this case, the something is nothing). You will see the symbol _ quite a bit in Rust. Commonly, it is used in loops where the variable being acted on is never used, for example:
for _ in something() {...}
我们不会使用迭代器;我们只需要一些东西来使迭代能够跳到下一个值。
写入文件
编写 XML 文件比读取复杂得多。在这里,我们必须显式使用XmlEvent和EventWriter。我们还使用EmitterConfig,正如其名称所暗示的,即创建一个配置然后使用它。EventWriter、EmitterConfig和XmlEvent都是xml::writer的一部分。
让我们先考虑主函数。首先,创建文件和两个引用,一个指向stdin,一个指向stdout,如下所示:
let mut file = File::create("myxml_file.xml).unwrap();
let mut output = io::stdout();
let mut input = io::stdin();
接下来,我们通过EmitterConfig创建写入器:
let mut writer = EmitterConfig::new().preform_indent(true).create_writer(&mut file);
现在我们已经设置了写入器。perform_indent告诉写入器当为真时对每个节点进行缩进。
最后,我们创建一个循环并写入 XML。你会注意到一个对handle_event的调用;我们很快就会处理这个问题:
loop {
print!("> ");
output.flush().unwrap();
let mut line = String::new();
match input.readline(&mut line) {
Ok(0) => break,
Ok(_) => match handle_event(&mut writer, line) {
Ok(_) => {}
Err(e) => panic!("XML write error: {}", e)
}
Err(e) => panic!("Input error: {}", e);
}
}
函数handle_event的定义比我们之前看到的要复杂一些:
fn handle_event<W: Write>(w: &mut EventWriter<W>, line: String) -> Result<()> {
在 C#中,前面的定义将类似,并且会写成如下所示:
Result handle_result<T>(EventWriter<T> w, string line) where T:Write
我们将一个类型(无论是class、string、i32还是其他任何类型)传递给函数作为参数。在这种情况下,我们使用std::io::Write作为EventWriter使用的参数。
函数本身没有什么特别之处。我们首先通过修剪字符串来移除任何空白或回车:
let line = line.trim();
我们现在使用XmlEvent来生成代码:
let event: XmlEvent = if line.starts_with("+") && line.len() > 1 {
XmlEvent::start_element(&line[1..]).into()
} else if line.starts_with("-") {
XmlEvent::end_element().into()
} else {
XmlEvent::characters(&line).into()
};
w.write(&line).into();
}
into()将指针转换为结构(称为self)。在这种情况下,它接受(比如说)XmlEvent::characters(&line),并将其发送回行中。
概述
在本章中,我们涵盖了相当多的内容,你应该对处理字符串、XML 和文件感到更加熟悉,这些都可以用来为你的代码添加更多功能。请随时检查本章提供的示例。
在下一章中,我们将探讨循环、递归和分支。
第四章:条件、递归和循环
任何编程语言中的循环和条件都是操作的基本方面。你可能正在遍历一个列表,试图找到匹配项,当匹配发生时,分支执行其他任务;或者,你可能只想检查一个值是否满足条件。在任何情况下,Rust 都允许你这样做。
在本章中,我们将涵盖以下主题:
-
可用的循环类型
-
循环中的不同类型分支
-
递归方法
-
当分号(
;)可以省略及其含义
循环
Rust 本质上具有三种类型的循环:
-
loop是最简单的一种——它只是重复执行代码块,直到使用到循环中断关键字。 -
while类似于循环,但有一个条件——只要条件为真,代码块就会重复执行 -
for与上述两种不同——它是用于遍历序列的
for循环
for循环与 C-like 语言中的相同结构略有不同。在 C 中,for循环由三部分组成:初始化、停止条件和步进指令。Rust 的for循环则更高级一些:它们用于遍历序列。
让我们从简单的例子开始——一个从0到10的循环,并输出值:
for x in 0..10
{
println!("{},", x);
}
我们创建一个变量x,它从范围(0..10)中逐个获取元素,并对它进行一些操作。在 Rust 术语中,0..10不仅是一个变量,也是一个迭代器,因为它从一系列元素中返回一个值。
这显然是一个非常简单的例子。我们也可以定义迭代器以相反的方向工作。在 C 中,你可能会期望类似for (i = 10; i > 0; --i)的东西。在 Rust 中,我们使用rev()方法来反转迭代器,如下所示:
for x in (0..10).rev()
{
println!("{},", x);
}
值得注意的是,范围不包括最后一个数字。所以,对于前面的例子,输出的值是9到0;本质上,程序生成从0到10的输出值,然后以相反的顺序输出。
for循环的一般语法如下:
for var in sequence
{
// do something
}
以下代码的 C#等价代码如下:
foreach(var t in conditionsequence)
// do something
使用enumerate
loop条件也可以更复杂,使用多个条件和变量。例如,可以使用enumerate来跟踪for循环。这将跟踪循环执行了多少次,如下所示:
for(i, j) in (10..20).enumerate()
{
println!("loop has executed {} times. j = {}", i, j);
}
以下是输出:

假设我们有一个数组需要遍历以获取值。在这里,可以使用enumerate方法来获取数组成员的值。条件返回的值将是一个引用,所以如下所示的代码将无法执行(line是一个&引用,而期望的是i32):
// 04/enumerate/src/main.rs
fn main()
{
let my_array: [i32; 7] = [1i32,3,5,7,9,11,13];
let mut value = 0i32;
for(_, line) in my_array.iter().enumerate()
{
value += line;
}
println!("{}", value);
}
这可以简单地从引用值转换回来,如下所示:
for(_, line) in my_array.iter().enumerate()
{
value += *line;
}
iter().enumerate() 方法同样可以与 Vec 类型(或任何实现了迭代器特质的类型)一起使用,如下面的代码所示:
// 04/arrayloop/src/main.rs
fn main()
{
let my_array = vec![1i32,3,5,7,9,11,13];
let mut value = 0i32;
for(_,line) in my_array.iter().enumerate()
{
value += *line;
}
println!("{}", value);
}
在这两种情况下,最后的值都将是 49,如下面的截图所示:

_ 参数
你可能想知道 _ 参数是什么。在 Rust 中,即使我们不使用变量绑定,通常也不允许省略变量绑定。我们可以使用 _ 来表示我们知道这个位置需要一个变量绑定,但我们永远不会使用它。
简单循环
循环的一种简单形式被称为 循环:
loop
{
println!("Hello");
}
上述代码没有循环结束关键字,如 break;它将一直输出 Hello,直到手动终止应用程序。
while 条件
while 条件通过条件扩展循环,正如你将在下面的代码片段中看到的那样:
while (condition)
{
// do something
}
让我们看看以下示例:
fn main() {
let mut done = 0u32;
while done != 32
{
println!("done = {}", done);
done += 1;
}
}
上述代码将输出 done = 0 到 done = 31。循环将在 done 等于 32 时终止。
提前终止循环
根据循环内迭代的 数据大小,循环可能会在处理器时间上花费较多。例如,假设服务器正在从数据记录应用程序接收数据,例如从气相色谱仪测量值;在整个扫描过程中,它可能记录大约五十万个数据点及其相关的时间位置。
对于我们的目的,我们想要将所有记录的值相加,直到值超过 1.5,一旦达到这个值,我们就可以停止循环。
看起来很简单?但有一件事没有提到:没有保证记录的值会超过 1.5,那么如果值达到了,我们如何终止循环?
我们可以通过两种方式之一来完成这个操作。第一种是使用 while 循环并引入一个布尔值作为测试条件。在以下示例中,my_array 代表发送到服务器的数据的一个非常小的子集:
// 04/terminate-loop-1/src/main.rs
fn main()
{
let my_array = vec![0.6f32, 0.4, 0.2, 0.8, 1.3, 1.1, 1.7, 1.9];
let mut counter: usize = 0;
let mut result = 0f32;
let mut quit = false;
while quit != true
{
if my_array[counter] > 1.5
{
quit = true;
}
else
{
result += my_array[counter];
counter += 1;
}
}
println!("{}", result);
}
这里结果是 4.4。这段代码完全可接受,尽管稍微有些冗长。Rust 还允许使用 break 和 continue 关键字(如果你熟悉 C,它们的工作方式相同)。
我们使用 break 的代码如下:
// 04/terminate-loop-2/src/main.rs
fn main()
{
let my_array = vec![0.6f32, 0.4, 0.2, 0.8, 1.3, 1.1, 1.7, 1.9];
let mut result = 0f32;
for(_, value) in my_array.iter().enumerate()
{
if *value > 1.5
{
break;
}
else
{
result += *value;
}
}
println!("{}", result);
}
再次强调,这将给出 4.4 的答案,表明所使用的两种方法是等效的。
如果我们在前面的代码示例中将 break 替换为 continue,我们将得到相同的结果(4.4)。break 和 continue 的区别在于 continue 跳转到迭代中的下一个值而不是跳出,所以如果我们 my_array 的最终值为 1.3,最后的输出应该是 5.7。
当使用 break 和 continue 时,始终要记住这个区别。虽然这可能会导致代码崩溃,但错误地使用 break 和 continue 可能会导致你意想不到或不想得到的结果。
使用循环标签
Rust 允许我们给循环添加标签。这可以非常有用,例如在嵌套循环中。这些标签作为循环的符号名称,并且因为我们有一个循环的名称,我们可以指示应用程序在该名称上执行任务。
考虑以下简单的例子:
// 04/looplabels/src/main.rs
fn main()
{
'outer_loop: for x in 0..10
{
'inner_loop: for y in 0..10
{
if x % 2 == 0 { continue 'outer_loop; }
if y % 2 == 0 { continue 'inner_loop; }
println!("x: {}, y: {}", x, y);
}
}
}
这段代码会做什么?
在这里,x % 2 == 0(或y % 2 == 0)意味着如果一个变量除以二没有余数,那么条件就满足,并且执行花括号中的代码。当x % 2 == 0,或者当循环的值是偶数时,我们将告诉应用程序跳到outer_loop的下一个迭代,而outer_loop是一个奇数。然而,我们还有一个内层循环。同样,当y % 2是偶数时,我们将告诉应用程序跳到inner_loop的下一个迭代。
在这种情况下,应用程序将输出以下结果:

虽然这个例子可能看起来非常简单,但它确实在检查数据时提供了很大的速度。让我们回到我们之前将数据发送到网络服务的例子。回想一下,我们有两个值——记录的数据和一些其他值;为了方便,它将是一个数据点。每个数据点相隔 0.2 秒记录;因此,每第五个数据点是一秒。
这次,我们想要所有数据大于 1.5 的数据点及其相关的时间,但只在该数据点正好在一秒时。因为我们希望代码易于理解和阅读,我们可以在每个循环上使用循环标签。
以下代码并不完全正确。你能找出原因吗?代码编译如下:
// 04/looplabels-2/src/main.rs
fn main()
{
let my_array = vec![0.6f32, 0.4, 0.2, 0.8, 1.3, 1.1, 1.7, 1.9, 1.3, 0.1, 1.6, 0.6, 0.9, 1.1, 1.31, 1.49, 1.5, 0.7];
let my_time = vec![0.2f32, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8];
'time_loop: for(_, time_value) in my_time.iter().enumerate()
{
'data_loop: for(_, value) in my_array.iter().enumerate()
{
if *value < 1.5
{
continue 'data_loop;
}
if *time_value % 5f32 == 0f32
{
continue 'time_loop;
}
println!("Data point = {} at time {}s", *value, *time_value);
}
}
}
这个例子是一个非常好的例子,可以展示正确的运算符的使用。问题是if *time_value % 5f32 == 0f32这一行。我们正在取一个浮点值,并使用另一个浮点数的模来查看我们最终是否得到一个浮点数的 0。
将任何非string、int、long或bool类型的值与另一个值进行比较,从来不是一个好主意,尤其是如果该值是由某种形式的计算返回的。我们也不能简单地使用continue在时间循环中,那么我们该如何解决这个问题呢?
如果你还记得,我们正在使用_而不是命名参数来进行循环的枚举。这些值始终是整数;因此,如果我们用一个变量名替换_,那么我们可以使用% 5来进行计算,代码就变成了以下这样:
'time_loop: for(time_enum, time_value) in my_time.iter().enumerate()
{
'data_loop: for(_, value) in my_array.iter().enumerate()
{
if *value < 1.5
{
continue 'data_loop;
}
if time_enum % 5 == 0
{
continue 'time_loop;
}
println!("Data point = {} at time {}s", *value, *time_value);
}
}
下一个问题在于输出并不正确。代码给出了以下:
Data point = 1.7 at time 0.4s
Data point = 1.9 at time 0.4s
Data point = 1.6 at time 0.4s
Data point = 1.5 at time 0.4s
Data point = 1.7 at time 0.6s
Data point = 1.9 at time 0.6s
Data point = 1.6 at time 0.6s
Data point = 1.5 at time 0.6s
数据点是正确的,但时间完全错误,并且不断重复。我们仍然需要在数据点步骤中使用continue语句,但时间步骤是错误的。有几个解决方案,但可能最简单的方法是将数据和时间存储在一个新的向量中,然后在最后显示这些数据。
以下代码更接近所需的结果:
// 04/looplabels-3/src/main.rs
fn main()
{
let my_array = vec![0.6f32, 0.4, 0.2, 0.8, 1.3, 1.1, 1.7, 1.9, 1.3, 0.1, 1.6, 0.6, 0.9, 1.1, 1.31, 1.49, 1.5, 0.7];
let my_time = vec![0.2f32, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8];
let mut my_new_array = vec![];
let mut my_new_time = vec![];
'time_loop: for(t, _) in my_time.iter().enumerate()
{
'data_loop: for(v, value) in my_array.iter().enumerate()
{
if *value < 1.5
{
continue 'data_loop;
}
else
{
if t % 5 != 0
{
my_new_array.push(*value);
my_new_time.push(my_time[v]);
}
}
if v == my_array.len()
{
break;
}
}
}
for(m, my_data) in my_new_array.iter().enumerate()
{
println!("Data = {} at time {}", *my_data, my_new_time[m]);
}
}
现在,我们将得到以下输出:
Data = 1.7 at time 1.4
Data = 1.9 at time 1.6
Data = 1.6 at time 2.2
Data = 1.5 at time 3.4
Data = 1.7 at time 1.4
是的,我们现在有了正确的数据,但时间又重新开始。我们很接近,但还不是正确的。我们没有继续time_loop循环,我们还需要引入一个break语句。为了触发break,我们将创建一个新的变量done。当v,my_array的枚举器达到向量的长度(这是向量中的元素数量)时,我们将这个值从false改为true。然后,在data_loop外部进行测试。如果done == true,则跳出循环。
代码的最终版本如下:
// 04/dataloop/src/main.rs
fn main()
{
let my_array = vec![0.6f32, 0.4, 0.2, 0.8, 1.3, 1.1, 1.7, 1.9, 1.3, 0.1, 1.6, 0.6, 0.9, 1.1, 1.31, 1.49, 1.5, 0.7];
let my_time = vec![0.2f32, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6];
let mut my_new_array = vec![];
let mut my_new_time = vec![];
let mut done = false;
'time_loop: for(t, _) in my_time.iter().enumerate()
{
'data_loop: for(v, value) in my_array.iter().enumerate()
{
if v == my_array.len() - 1
{
done = true;
}
if *value < 1.5
{
continue 'data_loop;
}
else
{
if t % 5 != 0
{
my_new_array.push(*value);
my_new_time.push(my_time[v]);
}
else
{
continue 'time_loop;
}
}
}
if done {break;}
}
for(m, my_data) in my_new_array.iter().enumerate()
{
println!("Data = {} at time {}", *my_data, my_new_time[m]);
}
}
我们从代码中得到的最终输出如下:

递归函数
最后要考虑的循环形式被称为递归函数。这是一个在满足条件之前会调用自身的函数。在伪代码中,函数看起来是这样的:
float my_function(i32:a: i32)
{
// do something with a
if (a != 32)
{
my_function(a);
}
else
{
return a;
}
}
递归函数的实际实现看起来是这样的:
// 04/recurse-1/src/main.rs
fn recurse(n: i32)
{
let v = match n % 2
{
0 => n / 2,
_ => 3 * n + 1
};
println!("{}", v);
if v != 1
{
recurse(v)
}
}
fn main()
{
recurse(25)
}
递归函数的想法非常简单,但我们需要考虑这个代码的两个部分。第一部分是recurse函数中的let行及其含义:
let v = match n % 2
{
0 => n / 2,
_ => 3 * n + 1
};
另一种写法如下:
let mut v = 0i32;
if n % 2 == 0
{
v = n / 2;
}
else
{
v = 3 * n + 1;
}
第二部分是分号并不在所有地方使用。考虑以下示例:
fn main()
{
recurse(25)
}
有分号和没有分号之间有什么区别?
在 Rust 中,几乎一切都是表达式。这意味着几乎每件事都返回一个值。一个例外是变量绑定语句let。在let语句以及许多其他语句中,结尾的分号是语法的一个强制部分。
然而,在表达式的情况下,分号有两个作用:它丢弃表达式的返回值,同时允许进一步的语句。所以如果表达式是块中的最后一个,那么在它那里有分号意味着最后一个值被丢弃,而没有分号则意味着返回最后一个值。
以下是一个例子,应该会使它变得清楚:
// 04/semicolon_block/src/main.rs
fn main()
{
let x = 5u32;
let y =
{
let x_squared = x * x;
let x_cube = x_squared * x;
x_cube + x_squared + x
};
let z =
{
2 * x;
};
println!("x is {:?}", x);
println!("y is {:?}", y);
println!("z is {:?}", z);
}
分号有两种不同的用法。让我们首先看看let y这一行:
let y =
{
let x_squared = x * x;
let x_cube = x_squared * x;
x_cube + x_squared + x // no semi-colon
};
这段代码执行以下操作:
-
大括号内的代码将被处理
-
最后的行,没有分号,被分配给
y
实质上,这被视为一个内联函数,它将没有分号的行返回到变量中。
要考虑的第二行是关于z的:
let z =
{
2 * x;
};
再次,大括号内的代码将被评估。在这种情况下,行以分号结尾,因此结果被丢弃,空值()被绑定到z。
当它执行时,我们将得到以下结果:

在代码示例中,fn main中调用recurse的行,有分号和无分号都会给出相同的结果,因为 Rust 运行时不使用main的返回值做任何事情。
摘要
在本章中,我们介绍了 Rust 中可用的不同类型的循环,以及理解何时使用分号以及省略分号意味着什么。我们还考虑了枚举和遍历向量以及数组,以及如何处理它们所包含的数据。
在下一章中,我们将了解为什么 Rust 是服务器应用程序的良好选择:内存管理。
第五章:记住,记住
使用 Rust 而不是 C 等语言的主要优势之一是其内存管理。例如,如果你尝试在数组的末尾或使用malloc预留的区域之后写入,C 程序将遇到缓冲区溢出和相关的不确定行为。Rust 在保持效率的同时,保护了大多数这些问题。
在本章中,我们将深入探讨 Rust 如何处理内存,并将涵盖以下主题:
-
理解 Rust 中使用的内存系统
-
如果你不小心,可能会出错
-
查看指针、引用、栈溢出和防止崩溃
-
分配和释放内存
让我们从一开始说起
在第二章,变量中,我简要介绍了数据在内存中的存储方式,我说非复合类型,如i32,存储在栈上,而String、Vector<T>等类型则存储在堆上。
默认情况下,Rust 将数据存储在栈上,因为它非常快。但是,也存在一些缺点。栈的大小有限,分配仅持续到函数的生存期。
问题是,一个函数需要多少内存?
栈帧
int values and the single float32 type:
fn main()
{
let a = 10;
let b = 20;
let pi = 3.14f32;
}
一旦main退出,在进入时分配的栈帧将被释放。分配和释放的美丽之处在于,它们在没有用户需要做任何事情的情况下完成。内存量也可以提前计算,因为编译器知道哪些局部变量正在使用。这再次提高了速度。
对于每一个优点,都有一个缺点:存储的值仅存在于方法的生存期内。
什么是栈?
考虑栈的最简单方式是将内存视为一系列盒子。对于这些示例,请将盒子分为四组:函数名、地址、变量名和值。以下是一个包含单个局部变量的main函数示例:
fn main()
{
let i = 32;
}
栈盒子将看起来像这样:
| 函数名 | 地址 | 变量名 | 值 |
|---|---|---|---|
main |
0 |
i |
32 |
以下是一个略有不同的示例:
fn second()
{
let a = 32;
let b = 12;
}
fn main()
{
let d = 100;
}
在这里,我们将有两个未连接的栈盒子。由于second函数从未被调用,我们实际上从未为它分配栈内存。因此,内存分配与第一个示例完全相同。
我们的第三个示例是main函数调用second函数的情况;在这种情况下,我们实际上为second函数预留了内存:
fn second()
{
let a = 32;
let b = 12;
}
fn main()
{
let d = 100;
second();
}
就我们的栈盒子而言,我们有以下内容:
| 函数名 | 地址 | 变量名 | 值 |
|---|---|---|---|
second |
2 |
a |
32 |
1 |
b |
12 |
|
main |
0 |
d |
100 |
main函数中的变量地址为 0,因为它来自最顶层帧——调用其他函数的帧。地址的值纯粹是为了这个例子;它可以在任何地方,并且通常,不同类型需要不同数量的栈空间来存储它们。例如,如果number类型长度为 4 字节,则地址将是存储d的栈的基地址,然后是地址+4 的b,最后是地址+8 的a。
一旦foo函数返回,栈将恢复到以下状态:
| 函数名 | 地址 | 变量名 | 值 |
|---|---|---|---|
main |
0 |
d |
100 |
一旦main函数执行完毕,栈就为空。
这种堆叠会持续到应用程序中有多少不同的函数,并且它们总是以相同的方式工作。
让我们考虑堆
如前所述,堆通常用于复杂类型。栈帧模型仍然可以使用,但需要修改,因为栈需要指向堆上复杂类型的基地址。
让我们为以下代码片段构建一个栈帧:
fn main()
{
let f = 42;
let my_ids: Vec<i64> = Vec::with_capacity(5);
}
| 函数名 | 地址 | 变量名 | 值 |
|---|---|---|---|
main |
1 |
f |
42 |
0 |
my_ids |
(一个 Vector 实例) |
空间已正确分配给f,但my_ids不同;它是一个预分配了五个i64值空间的Vector<i64>。虽然向量本身存储在栈上,但其内容是在堆上分配的。
堆中的值被认为比栈中的值更持久。这意味着,与栈中的值不同,它们的生命周期不必像它们定义的块那样短。
释放
与在栈上释放内存不同,当你从堆上释放内存时,你会在堆中留下空洞。这些是空的,可以被重新分配给其他变量。与内存相关的任何事物一样,重新分配由操作系统处理。
释放由 Rust 以通常称为资源获取即初始化的风格自动处理。这个名称令人困惑的概念意味着资源(如堆内存,但也包括文件指针等其他事物)在对象创建时分配,在对象销毁时释放。Rust 中的对象销毁发生在绑定超出作用域时。如果您需要为您的对象定义自定义析构函数,您可以实现std::ops::Drop()特质。它包含一个方法,drop,当您的对象失去最后一个绑定时会被调用。
函数参数怎么办?
考虑以下代码片段:
fn main()
{
let a = 32;
let b = &a;
}
我们创建了两个变量绑定,第二个绑定(b)指向a的地址。b变量不包含a变量的值,而是指向a值所在的位置,从中它可以获取值(换句话说,b的值是从a借用的)。
在我们的栈图中,我们有以下内容:
| 函数名 | 地址 | 变量名 | 值 |
|---|---|---|---|
main |
1 |
b |
→ 地址 0 |
0 |
a |
32 |
如果我们有一个函数调用另一个函数,但带有参数,我们的栈将略有不同:
fn second(i: &i32)
{
let c = 42;
println!("{}", *i);
}
fn main()
{
let a = 32;
let b = &a;
second(b);
}
| 函数名 | 地址 | 变量名 | 值 |
|---|---|---|---|
3 |
c |
42 |
|
| second | 2 |
i |
→ 地址 0 |
1 |
b |
→ 地址 0 |
|
| main | 0 |
a |
32 |
i 绑定指向 地址 0,而 b 变量也指向 地址 0,这就是传递给 second 的参数。
如果你喜欢,可以使用这种方法来考虑复杂情况中的内存。
静态内存分配
当我们有栈和堆时,Rust 还有另一种类型的内存分配,那就是静态分配的内存。这不是在运行时分配的,而是在程序运行之前,随着程序的代码移动到内存中。
static 和 const 变量是静态分配的好例子。
静态内存分配的生存期与应用程序相同。
垃圾回收时间与所有权
如果你习惯了任何.NET 语言,你将非常习惯于垃圾回收器(GC)。本质上,当一个对象的引用全部超出作用域时,垃圾回收器会释放该对象的堆分配。垃圾回收器时不时地出现,基本上会检查整个已分配内存空间,看看是否有不再使用的内容,并将此类内容从内存中移除;换句话说,回收并移除未分配指针留下的垃圾。
Rust 有一个原始的垃圾回收器,以引用计数容器Rc<T>的形式存在。尽管如此,在大多数情况下并不需要,因为 Rust 使用一种称为分配所有权的系统。
到目前为止,当我们创建变量时,我们创建的变量大多在栈上。它们的生存期非常短。当我们创建一个在堆上生存的对象时,我们创建一个指向它的单个变量,但然后我们可以有任意数量的对象指向它,或者甚至通过指针的副本,使副本成为基础并释放原始指针。这会变得混乱,堆内存的释放可能导致各种内存问题。
我们可以将任何类型包裹在泛型容器Box<T>中。这会在 Rust 中创建一个所有者指针,它只能有一个所有者,当这个指针超出作用域时,内存会自动释放。这样,Rust 可以防止我们在其他语言中看到的大量问题。这个所有者盒子的目的是我们可以将盒子分发给其他函数,从而能够返回堆分配的变量。
所有权指针示例
考虑以下代码片段:
struct MyRectangle
{
x: i32,
y: i32,
length: i32,
breadth: i32,
}
fn allocate_rect()
{
let x: Box<MyRectangle> = Box::new (MyRectangle {x: 5, y: 5, length: 25, breadth:15});
}
x 变量是堆上 my_rectangle 对象的唯一所有者。一旦 allocate_rect() 完成,分配给 x 的堆内存就会被释放,因为最后一个所有者已经不存在了。
编译器强制执行单一所有者。以下示例演示了所有权转移。一旦转移完成,原始对象就不能再使用了:
fn swap_around()
{
let my_rect: Box<MyRectangle> = Box::new(MyRectangle{x:5, y:5, length:25, breadth:15});
let dup_rect = my_rect; // dup_rect is now the owner
println!("{}", dup_rect.x);
println!("{}", my_rect.x); // won't work - use of moved value
}
与 C 的比较
考虑以下 C 代码:
void myFunction()
{
int *memblock = malloc(sizeof(int));
*memblock = 256;
printf("%d\n", *memblock);
free(memblock);
}
以下代码做了什么:
-
int行分配了一个足够存储整数值的内存块。memblock变量将在栈上,它指向的内存块将在堆上。 -
在
x指向的位置放置了一个值256。 -
打印出由
x指向的内存位置的值。 -
分配给
memblock的内存被释放。
这工作得很好,但有以下三个主要缺点:
-
一旦内存被释放,仍然完全有可能使用
memblock。如果你尝试这样做,应用程序将表现出未定义的行为;最可能的情况是应用程序会直接退出,但也有可能它会导致内存损坏,从而引起系统崩溃。编译器不会尝试警告你做了这件事,因为它假设你知道你在做什么。 -
如果你分配了一个比
sizeof中放置的类型更大的类型,这也会引发未定义的行为。你本质上是在试图把一夸脱的液体放进一品的罐子里。 -
如果没有调用
free,即使没有任何指针指向它,内存仍然被保留,这会导致内存泄漏。
你可以在 Rust 中执行类似操作,但正如我们将看到的,Rust 会自动防止这种未定义的行为:
fn myMemory()
{
let memblock: Box<i64> = Box::new(256);
println!("{}", memblock);
}
C 和 Rust 代码版本之间存在许多差异。具体如下:
-
在 C 中,你使用
malloc函数分配堆内存。在 Rust 中,我们使用Box<T>泛型来通过所有者指针。 -
C 中的
malloc调用返回一个int指针(int *)。在 Rust 中,返回一个智能指针(Box<T>),在这种情况下指向i64。智能指针之所以被称为智能,是因为它控制着对象何时被释放。这可能是当指针超出作用域而没有将指针传递出去时。Rust 会跟踪对象以及如何清理内存。
另一个有用的智能指针类型是引用计数的指针,Rc<T>。这个泛型类型允许在多个位置共享其内部的数据。它的工作原理是,每当 Rc 绑定被克隆时,引用计数就会增加。每当这样的绑定被释放时,引用计数就会减少。只有当引用计数达到零时,底层值才会被释放。请注意,Rc<T> 只在单线程场景中工作。
它的使用方式如下:
// 05/rc-1/src/main.rs
use std::rc::Rc;
fn main()
{
let memblock: Rc<i64> = Rc::new(256);
// allocate space on the heap and assign
secondMethod(memblock.clone());
// clone a new reference counted pointer and pass it on to the method
println!("{}", memblock);
// output the value
} // free memory here
fn secondMethod(memblock: Rc<i64>)
{
println!("In secondMethod and memblock is {}", memblock);
let secMemblock: Rc<i64> = memblock.clone();
// yet another reference counted pointer to memblock
}
// secMemblock goes out of scope, but the memory is not deallocated
在此代码中,我们创建了几个引用计数的指针副本。在峰值(secondMethod 函数的第二行),我们总共有三个指向底层堆的指针。当我们离开 secondMethod 时,通过 secMemBlock 变量分配的指针被销毁。然后 memBlock 副本被释放。最后,当我们退出主函数时,最后一个指针消失,堆内存被释放。
让我们回顾一下旧代码
回到第四章,条件、递归和循环,我们有一些看起来像这样的代码:
let x = 2;
let y =
{
let x_squared = x * x;
let x_cube = x_squared * x;
x_cube + x_squared + x
};
解释说,它所做的操作是将 x_cube + x_squared + x 的结果赋值给 y。如果在这个范围之外,我们尝试访问 x_squared 或 x_cubed,那么我们就无法访问,因为它们只存在于计算 y 的那个计算范围内。
考虑,如果我们将 y 设置为引用并尝试将其指向一个临时值会发生什么:
// 05/refs-1/src/main.rs
fn main() {
let x = 2;
let y: &i32;
{
let x_squared = x * x;
let x_cube = x_squared * x;
y = &(x_cube + x_squared + x);
// this value goes away after this line
};
println!("Y = {}", *y);
}
我们将 y 赋值给一个仅在小型作用域中存在的变量的值(计算中的临时未命名值),然后我们尝试访问这个值,从而产生未定义的行为。正如我们所看到的,Rust 编译器会尽一切努力防止这种错误。在这种情况下,编译器会跟踪每一个引用,如果引用的持续时间超过正在使用的指针,则无法构建。
让我们不要急于前进!
与内存相关的任何事物一样,我们确实有内存在指针之间共享的时候。通常,当我们编写应用程序时,我们不会考虑在任何给定时间可能有多个线程同时运行,虽然我们可以通过遵循流程相当准确地预测会发生什么,但我们有时会遇到一个称为竞争条件的问题。简单地说,我们不知道哪个条件会首先击中。
让我们看看以下示例:
// 05/threads-1/src/main.rs
use std::thread;
use std::rc::Rc;
struct MyCounter
{
count: i32
}
fn wont_work()
{
let mut counter = Rc::new(MyCounter {count: 0});
thread::spawn(move || // new thread
{
counter.count += 1;
});
println!("{}", counter.count);
}
这将无法编译,因为编译器不知道哪个线程将是 0 或 1,因为它们都试图同时访问 counter。在 Rust 术语中,counter 被移动到内部线程,这意味着它不能在其他任何地方被访问。通过 Rc 类型的引用计数在这里没有帮助,因为它不是线程安全的。
停止竞争...
如何避免这种错误?
还有一个具有非常酷名字的引用计数类型:Atomic RC。在这种情况下,原子性指的是不可分割的操作和/或容器,这意味着它们是线程安全的。此外,我们还需要将 Arc 类型与 Mutex 配对,以便我们可以锁定数据以进行访问。以下是线程实现的全代码:
// 05/threads-2/src/main.rs
use std::thread;
use std::sync::{Arc, Mutex};
struct MyCounter
{
count: i32
}
fn wont_work()
{
let counter = Arc::new(Mutex::new(MyCounter {count: 0}));
let another_counter = counter.clone();
thread::spawn(move || // new thread
{
let mut counter = another_counter.lock().expect("Locking of cloned counter failed");
counter.count += 1;
});
println!("{}", counter.lock().unwrap().count);
}
通常,这段代码将打印 0,因为打印方法往往在线程中的突变发生之前被调用。
摘要
在内存处理方面,Rust 为开发者做了很多工作,几乎确保了不可能遇到像在 C 中发现的那种问题。从堆中释放内存是自动的,而且在使用指针时还有独特的保护机制,即拥有唯一和多个受保护的指针。
在下一章中,我们将从学习中暂时休息一下,看看你如何将我们所学的内容应用到自己的应用程序中。
第六章:创建您的 Rust 应用程序
我们现在大约完成了这本书的一半,而不是仅仅继续,这一章有几个任务供您尝试。尝试这些任务将有助于巩固我们到目前为止所涵盖的内容。
为了完成这些,您需要使用 Cargo 创建一个完整的代码项目,如第一章中所示,介绍和安装 Rust。如果您遇到困难,源代码目录中包含可能的解决方案。
项目 1 - 让我们从一些数学开始
数据分析非常重要,了解如何产生直线关系通常非常重要。您需要构建几个函数,以完成所谓的线性回归分析。
要求
以下是我们项目的一些简要要求:
-
数据将来自两个向量,并将是浮点数
-
答案将只存储在主中,并从那里显示
-
两个向量必须具有相同数量的元素
提供的数据
本项目的所需数据位于第六章文件夹中的Projects/MathsData.txt文件中。应在应用程序内使用文件内容(复制并粘贴)。如果数据集元素数量不同,请从元素数量较多的集合的末尾删除元素。
让我们看看所需的数学:
- 直线的方程:
方程非常简单:
y = mx + c,其中m是斜率,c是 y 轴上的截距
- 回归线的斜率:
方程如下:
>
这可能看起来很复杂,但只要记住以下规则,它就相当简单:
不等于
那么,区别是什么?
表示它是 x²的总和,而
是 x 平方的总和。以下代码作为示例:

为了节省时间,此时进行以下计算是值得的:
-
每个集合的xy,然后是每个的∑xy
-
∑x, ∑y, ∑x², (∑x)²,
(x的平均值),
(y值的平均值)
需要数据用于回归斜率和y值上的截距。之后,只需将数字插入即可。
例如,∑y = 5.8907,∑x = 5,∑x² = 7.5,(∑x)² = 25,n = 4,∑xy = 8.8528


此外,从现在开始的所有数学都不比之前的难。
- 获取截距:
我们已经通过方程得到了y轴上的截距公式(c),如下所示:

再次,使用之前相同的数据,数字填入并给出答案为-0.0167。
现在,这是 y 轴上的截距;然而,我们还想得到 x 轴上的截距。为此,我们可以这样说,我们想知道当 y = 0 时 x 的值。直线的方程是 y = mx + c;因此,为了得到 x 本身,方程将是以下这样:

简单!
- 偏差必须已知:
在回归分析中还有两个因素需要考虑——标准差(更广为人知的是线的误差)和 r² 值(相关系数;换句话说,直线实际上有多好)。
这两个方程比之前稍微难一些,但不是很多。
首先,一些额外的计算将涵盖标准差和 r² 的计算。
对于标准差,我们需要知道 (y[expt] - y[calc])²。这可以在行内完成,如下所示:
y[expt] 是实验中的值,y[calc] 可以读作 mx + c(三者都是已知的)。因此,如果我们只输入数字然后平方结果,我们最终得到 (y[expt] - y[calc])²。然后把这些数字加起来得到 ∑(y[expt] - y[calc])²。
在 r² 的计算中,我们需要计算
并然后汇总结果。这很容易,分母的部分也是如此。
然而,等等;偏差和 r² 计算中有一个共同的项目,即开平方根。这实际上只是另一种说平方根的方式。
现在只需要输入数字。
应用程序输出
应用程序必须输出以下信息:
-
每个向量中的元素数量
-
如果向量的元素数量不相同,输出被移除的值以及从哪个向量移除
-
直线的方程
-
X 和 Y 轴上的截距
-
数据的标准差
-
r² 值
项目 2 - 一些文本操作
回文是一个单词,它的拼写从后往前读和从前往后读是一样的。例如,单词 madam 就是一个回文。
要求
以下是一个简要的要求列表:
-
应用程序从键盘接收一行文本。
-
如果直线是空的或包含非字母字符,应该失败。
-
输入的任何文本都应该转换为小写或大写。
-
应该在单独的函数中测试单词是否是回文。如果是,函数应该返回 true;否则应该返回 false。
-
调用函数应该输出输入的文本是否(或不是)一个回文。
代码注释
回文函数应该是递归的。
项目 3 - 面积和体积
这个项目应该有助于巩固你应用程序的测试和文档。
你有一个简单的网络服务在某台服务器上运行。它作为一个测试平台,让用户发送数据并接收回数据。该服务期望输入三个字符串,如下所示:
-
用户名(字符串,非空,必须超过 6 个字符,不允许有空格)
-
密码(字符串,非空,必须超过 8 个字符,不能有空格,必须包含 1 个大写字母,1 个数字)
-
命令字符串
命令字符串是一个以逗号分隔的列表,包含是否为体积或面积计算,形状类型和参数列表的详细信息。
形状类型
| 类型 | 形状 | 类型 | 形状 |
|---|---|---|---|
| 0 | 圆/球体 | 3 | 五边形 |
| 1 | 三角形/棱锥 | 4 | 八边形 |
| 2 | 矩形/盒子 | 5 | 用户定义 |
体积或面积
对于命令字符串,区域由 true 给出,体积由 false 给出。
用户定义的形状
这个形状由你决定。它应该是一个目前不在列表上的形状。
计算公式
你应该使用以下公式进行计算:
| 形状 | 面积 | 体积 |
|---|---|---|
| 圆/球体(r = 半径) | A = Πr² | V = 4/3Πr³ |
| 三角形/棱锥(b = 底边,h = 高,l = 长度,w = 宽) | A = 1/2bh | ![]() |
| 矩形/盒子(l = 长度,h = 高,b = 宽) | A = lh | V = lbh |
| 五边形(a = 边长,h = 高) | ![]() |
![]() |
| 八边形(a 和 s = 边长,h = 高) | ![]() |
![]() |
测试标准
考虑以下测试标准:
- 命令行:在测试参数数据时,任何值为0的值应导致第一次测试失败。对于第二次测试,0应替换为你选择的浮点值。
命令行必须以下列格式提供:
volume/area, type, params
例如,true, 1, 3.1, 33.12, 4.3 是有效的;而 false, 1, 12 将失败。(它需要传递两个值。)
命令行如 false, 1, 12.1, 13.5, 1.4, 0 不会失败,因为参数列表中超过计算所需参数数量的任何内容都可以忽略。
如果命令失败,输出应该始终是-1。
- 用户名和密码:用户名和密码必须符合谜题开始时设定的标准。如果失败,输出应该是“用户名失败”或“密码失败”。不需要其他原因。
自动文档
应为每个函数生成文档,并清楚地说明入口和出口参数。
使用正则表达式(regex)
虽然 Rust 自带正则表达式作为其自己的包,但你被鼓励创建自己的方法来测试字符串输入。
输入和输出
与所有其他示例一样,这都应该通过标准终端的输入和输出点(读取键盘和显示器)进行。我建议使用三个提示:用户名,密码和命令行,因为每次输入后,将更容易测试提交的输入并相应地做出反应。
项目 4 – 内存
在这个项目中,你需要执行以下操作:
-
预留一个 1024 字节的内存块。
-
用随机字符填充那块内存。
-
创建一个大小也是 1,024 字节的数组。
-
将内存块的内容复制到数组中。
-
创建一个由
capacity函数设置的 1,024 字节限制的字符串。 -
将内存块的内容复制到字符串中。
到目前为止,你可能想知道为什么我们有三个相同的内存块。简单的原因是,你现在将创建一段代码,将依次将每个成员旋转 3 次,先进行简单的左移位旋转,然后向右旋转 3 次。
位运算旋转
在 Rust 中,使用 << 和 >> 运算符执行位运算旋转。
例如,如果我们有一个名为 x 的变量,它向左旋转了 3 位,我们将写 x << 3,而向右旋转则是 x >> 3。
假设我们有一个 x = 01101001,x << 3 将会是 01001000,而 x >> 3 将会是 00001101。
旋转注意事项
虽然我们可以简单地使用 x << 3,但这个谜题需要我们执行单次旋转 3 次(所以实际上是 x << 1,x << 1,x << 1)。
输出
并不期望你开始时的值就是你最终得到的值(如果你移位太远,字节中的空位将被 0 填充)。你应该能够找出有多少字节具有值为 0。你应该在最后显示这个结果。
摘要
尝试这四种不同的编程挑战应该有助于巩固你从本书前半部分获得的知识。在下半部分,我们将探讨更高级的主题,并继续探索 Rust 可以提供的强大和灵活性。
第七章:匹配和结构
在原始变量和泛型的基础上,我们将在第九章介绍泛型、Impl 和 Traits 中介绍,Rust 能够将不同类型的变量组合在struct结构中,这对于在 C 系列语言中开发的人来说可能很熟悉。还有一个相关的概念叫做枚举,用于创建具有交替选项的类型。如果还不够,Rust 还可以通过其强大的模式匹配代码将这些结合在一起。
在本章中,我们将涵盖以下主题:
-
学习如何使用和操作
struct数据类型 -
理解元组和元组结构体混合体
-
创建和使用枚举
-
理解和应用模式匹配的基础
结构体 101
对于本章,我将要求你想象以下场景。我有一所房子。我的房子有一定数量的房间,每个房间都有一个名字。每个房间有一扇或多扇门和窗户,以及一块地毯(有颜色),房间有宽度和长度。我们将使用结构体和枚举来模拟所有这些。
Rust 中的结构体非常常见;它们在语言的许多方面都有应用,理解和使用它们是有用的。在房子示例中,我们将看到它们有多么有用。
变量,无处不在的变量
让我们看看房子,并创建一些变量来描述它以及类型。从房子开始,它可以被认为是最基本的对象。我们只需要模拟它有多少个房间:
number_of_rooms: i32
让我们接下来考虑房间。
每个房间将有一些属性。假设它是一个两层楼的房子,它是楼上还是楼下?门的数量。窗户的数量。窗户的类型。窗户有窗帘吗?木地板或地毯覆盖?地毯的颜色。房间名称。是否有衣柜/壁橱?房间宽度。房间长度。你可以做得更深入,但这对现在来说已经足够了。
作为变量,它们如下所示:
is_upstairs: bool
number_of_doors: i32
number_of_windows: i32
window_type: String
has_curtains: bool // true = yes
wood_or_carpet: bool // true = carpet
carpet_color: String
room_name: String
has_wardrobe: bool // true = yes
room_width: f32
room_height: f32
没有理由你不能将这些定义为离散变量;然而,由于它们是描述房子或房间内特征的属性,为什么不按我们已有的方式将它们分组呢?这就是struct类型发挥作用的地方。
结构体的结构
struct 类型由三个部分组成:关键字 struct、struct 名称以及它所包含的变量。以下命令作为例子:
struct MyStruct
{
foo: i32,
bar: f32,
}
需要注意的是,与常规变量定义不同,变量类型后面直接跟逗号,而不是分号。
对于我们的例子,我们可以定义两个struct类型,一个用于房间,一个用于房子,如下所示:
struct Room
{
is_upstairs: bool,
number_of_doors: i32,
number_of_windows: i32,
window_type: String,
has_curtains: bool,
wood_or_carpet: bool,
carpet_color: String,
room_name: String,
has_wardrobe: bool,
room_width: f32,
room_height: f32,
}
因此,我们的房子如下所示:
struct House
{
room:... um...
}
虽然struct是一种特殊的变量类型,但它仍然是一种变量,并且作为类型;一个struct。因此,我们可以像对任何其他变量类型一样分配它:
struct House
{
room: Room,
}
如果我们有一个只有一个房间的房子,这没问题!我们可以定义一个房间数组,但这意味着我们将有固定数量的房间。相反,我们将将其定义为向量中使用的类型:
struct House
{
rooms: Vec<Room>
}
我们创建了两种特殊的变量类型,我们可以像声明和访问任何其他变量一样声明和访问它们。如果我们查看房间定义,我们可以进一步分解结构;但为什么我们要这样做呢?
小的更好
有一个论点是,你使父结构越小,管理起来就越容易。这是真的,但让我们从不同的角度来看。就目前而言,我们在这个结构中有几个可以被视为独立对象的元素。在这里,一个对象是具有其自身属性的东西。让我们看看窗户。
窗户有一个大小——宽度和高度;它有一个类型——例如,窗扇;它有百叶窗或窗帘,百叶窗/窗帘有颜色。窗户还可能有锁。它可能是一扇单窗或双窗,开启位置可能在顶部或侧面。
没有理由说只能有一个窗户。如果有多个窗户,那么我们需要多次定义我们的窗户。因此,定义我们的窗户并在主结构中将其作为向量引用更有意义。
在那之前,我们说过窗户将有大小(宽度,长度)。每个房间都将有大小,而且房子里的许多其他东西可能也是如此;因此,我们将移除大小,将其作为一个单独的struct。
因此,我们为窗户定义了以下struct:
struct Area
{
width: f32,
length: f32,
}
struct Window
{
window_area: Area,
window_type: String,
has_blinds: bool,
curtain_color: String,
has_lock: bool,
top_open: bool,
single_window: bool,
}
这在父struct中将转换为以下内容:
struct Room
{
is_upstairs: bool,
number_of_doors: i32,
window: Vec<Window> ,
wood_or_carpet: bool,
carpet_color: String,
room_name: String,
has_wardrobe: bool,
room_area: Area,
}
我们可以为房间中的任何其他东西继续这样做,包括家具的struct变量,以及可能减少地毯的大小——你如何处理它取决于你。现在,我们将保持在这个级别。
访问struct
为了访问struct变量,我们需要创建一个可以访问它的变量:
let mut room = Room { is_upstairs: true,
number_of_doors: 1, wood_or_carpet: true, carpet_color: "Red",
room_name: "Bedroom 1", has_wardrobe: true };
本节代码位于本书支持代码包的07/simplestruct文件夹中。
我们还没有定义该结构中的所有变量,目前这没关系,因为它们仍然需要在代码编译前定义。这个变量是可变的,因为我们想稍后更改其内容。
要访问struct的一个成员,我们将使用点符号。在这种情况下,我们可以有如下内容:
println!("Bedroom {} has {} door", room.room_name,
room.number_of_door);
定义子struct
我们有两种类型的struct——父struct和子struct。在这里,Room的struct是父struct,它有两个子struct:窗户定义和房间大小。它们非常不同,因为窗户定义是一个Vec类型,而另一个只是一个struct类型。
对于房间面积,在创建房间类型的实例时,我们可以使用以下内容:
room_area: Area {width: 2.3f32, length: 4.3f32}
我们正在定义room_area,然后我们将定义一个内联变量,它将作为指向面积结构体的指针,最后创建房间的尺寸。这是通过以下代码片段来访问的:
println!("The room width is {}m by {}m", room.room_area.width, room.room_area.length);
最后,我们必须定义窗口的向量。
这是以与我们定义任何其他向量非常相似的方式完成的,如下所示:
window: vec![
Window {
window_area: Area {width: 1.3f32, length: 1.4f32},
window_type: "Main".to_owned(),
has_blinds: true,
curtain_color: "Blue".to_owned(),
has_lock: false,
top_open: true,
single_window: true,
},
Window {
window_area: Area {width: 0.9f32, length: 1.1f32},
window_type: "Small".to_owned(),
has_blinds: true,
curtain_color: "Blue".to_owned(),
has_lock: false,
top_open: true,
single_window: true,
}
然后,我们将添加几行println!来展示我们有一些数据:
println!("The room width is {}m by {}m", room.room_area.width, room.room_area.length);
let ref window_two = room.window[1];
println!("Window 2 is {}m by {}m and has {} curtains", window_two.window_area.width, window_two.window_area.length, window_two.curtain_color);
编译时,代码会产生以下结果:

我们非常快速且简单地创建了一个多级结构。
多文件版本
如果你查看simplestruct文件中的源代码,你会发现结构体在开始处,下面跟着相应的代码。这并没有什么问题,但过了一段时间,它就会变得繁琐,尤其是如果我们有很多结构和枚举时。
为了解决这个问题,我们可以将结构和代码分散在两个文件中。
然而,在我们构建代码之前,我们必须向main.rs文件提供某种指向结构的指针。我们可以通过以下三种方式之一来实现。最简单的是使用include!宏:
include!("structs.rs");
本节的内容来源位于Chapter 7/multifile文件夹中,该文件夹包含在此书提供的配套代码包中。
这只是将文件内容插入到宏调用的位置,因此这不是最优雅的方式,并且完全绕过了 Rust 的模块系统。所以让我们看看更好的方法。
更好的方法是使用以下代码片段来引用模块:
mod structs;
use structs::*;
这可能导致大量问题,最大的问题是保护级别,public或private。按照这种方式编译时,会出现许多错误,例如以下示例所示:

错误将指示,尽管结构体是public的,但其中的字段不是,因此无法访问。解决方案是将所有字段设置为public。
私有与公共字段
默认情况下,结构体中的所有字段对其创建的模块都是private的。这有其用途;例如,如果你想保护结构体中的某个值,你可以通过read/write函数使其仅可通过,如下例所示:
// readwrite.rs
pub struct RWData
{
pub X: i32,
Y: i32
}
static mut rwdata: RWData = RWData {X: 0, Y: 0};
pub fn store_y(val: i32)
{
unsafe { rwdata.Y = val; }
}
pub fn new_y() -> i32
{
unsafe { rwdata.Y * 6 }
}
// main.rs
mod readwrite;
use readwrite::*;
fn main() {
store_y(6);
println!("Y is now {}", new_y());
}
本节的相关代码位于07/readwrite文件夹中,该文件夹包含在此书提供的配套代码包中。
当我们构建并运行这个程序时,我们会得到以下输出:

结构体 102
虽然我们已经定义了自己的结构体,但我们也可以访问一种称为单元结构体struct的结构。与我们的结构体不同,我们可以看到以下内容:
struct someStruct;
let x = someStruct;
它们后面没有任何内容——没有定义字段。这些与我们所定义的不同,那么它们是如何工作的呢?
要理解它们是如何工作的,我们需要了解元组结构体struct,为了理解这些,我们需要知道什么是元组。
元组
初始化元组有两种方式:
let tup = (3, "foo");
let tup: (i32, &str) = (3, "foo");
在第一行,我们让局部类型推断工作,只声明元组内的内容。Rust 会找出类型。在第二行,我们显式声明类型。
列表中的元素可以有(或没有)多少,实际上,它们是一个固定大小的有序列表。
与其他变量类型一样,只要它们包含相同类型和参数数量(arity),我们就可以将一个元组赋值给另一个。例如,以下具有相同的类型和 arity,因此可以相互赋值:
let mut change = (1.1f32, 1);
let into = (3.14f32, 6);
change = into;
以下是不允许的,因为类型不匹配,尽管 arity 相同:
let mut change = (1.1f32, 1);
let into = (1, 3.14f32);
使用元组定义变量
让我们考虑以下内容:
let test = 1i32;
我们创建了一个名为 test 的变量,该变量绑定了一个类型为 i32 的值为 1(绑定在第五章,记住,记住)的变量。我们如何用元组做类似的事情?到目前为止,我们已经做了以下事情:
let test = (1, 4f32);
我们将 test 绑定到一个包含两个值的元组上:(i32, f32)。
元组索引
要获取 f32 值,我们必须使用元组索引。这与索引数组非常相似,但我们替换以下片段:
let t = someArray[3];
我们使用以下片段代替:
let t = test.1
与数组索引一样,元组索引的范围从 0 到 n-1。
使用 let 进行解构
为了避免使用元组索引,Rust 有一种解构元组的方法。这与正常的 let 语句非常相似,但我们将一次定义多个变量名称:
let (one, two, three) = (1, 2, 3);
如果左侧的名称数量与右侧的参数数量相同,Rust 将内部拆分这些名称以一次创建三个绑定。
现在我们有三个绑定,可以像访问任何其他变量一样访问它们:
let (one, two, three) = (1, 2, 3);
println!("One = {}", one); // outputs One = 1
元组结构体 – 两种的结合
考虑一个有三个字段的 struct。它将有一个 struct 类型的名称和三个带有其类型的字段:
struct Test
{
drink: bool,
number: i32,
price: f32
}
让我们考虑这实际上是什么,以及我们是否可以将其重写如下:
let Test: (bool, i32, f32) = (false, 4, 1.55);
嗯,我们可以,但现在我们会遇到如何访问元组成员的问题。我们还会遇到将一个元组赋值给另一个元组的问题。实际上,你不能定义两个结构体,除了 struct 类型名称外,其他一切相同,然后将第二个 struct 类型赋值给第一个。
为了解决这个问题,Rust 有元组 struct 混合体。它包含 struct 类型,然后将字段作为元组分配:
struct TestOne (f32, i8, &str);
struct TestTwo (f32, i8, &str);
现在我们有了元组的灵活性,但又有 struct 的保护。尽管 arity 相同,结构体内部的类型也相同,但它们是不同的类型。
与常规元组一样,我们可以以相同的方式访问元组 struct 的成员:
let i = TestOne.1;
单元素元组结构体
到目前为止,你可能想知道元组 struct 与标准 struct 相比有什么用途。其中一个用途是当元组 struct 只有一个元素时。在这里,我们能够根据元组创建一个变量。它的外观类似于解构的元组:
struct MyPi(f32);
fn main()
{
let my_pi = MyPi(22f32 / 7f32);
let MyPi(pi) = my_pi;
println!("pi = {}", pi);
}
本节源代码位于 07/newtype 文件夹中,该文件夹包含本书提供的配套代码包。
编译并运行后,它会产生以下输出:

这种形式的赋值称为新类型模式;它允许创建一个与包含值不同的新类型。
回到类似单元的 struct
现在我们已经了解了元组和元组 struct,我们现在可以看看类似单元的 struct。这可以被认为是一个具有空元组的 struct,并且与元组 struct 一样,它定义了一个新类型。
通常,我们将它与特质一起使用,或者如果你没有数据要存储在其中。
枚举
如果你习惯了 C 语言,你将很熟悉枚举,例如:
enum myEnum {start = 4, next, nextone, lastone=999};
这创建了一个自动填充 next 和 nextone 为起始值 + 1 和 + 2 的 enum 类型。如果第一个命名参数没有提供初始值,它将被赋予 0 的值,并且所有后续的值都比上一个值大 1。它们可以通过 myEnum.nextone 访问。
Rust 中的 enum 类型结构与 struct 类型结构非常相似,如下面的代码所示:
enum MyEnum
{
TupleType(f32, i8, &str),
StructType { varone: i32, vartwo: f64 },
NewTypeTuple(i32),
SomeVarName
}
虽然与 C 语言一样,enum 是一个单一类型,但 enum 的值可以匹配其任何成员。
访问枚举成员
考虑到 Rust enum 的内容可能性,你可能认为在枚举中访问一个成员可能不是最简单的任务。幸运的是,它确实如此,因为 enum 变量有时被称为 可作用域 变量。例如,如果我们想访问成员,我们可以使用以下方法:
enum MyFirstEnum
{
TupleType(f32, i8, String),
StuctType {varone: i32, vartwo: f64},
NewTypeTuple(i32),
SomeVarName
}
enum MySecondEnum
{
TupleType(f32, i8, String),
StuctType {varone: i32, vartwo: f64},
NewTypeTuple(i32),
}
fn main()
{
let mut text1 = "".to_owned(); // text1: String
let mut text2 = "".to_owned(); // text2: String
let mut num1 = 0f32;
let value = MyFirstEnum::TupleType(3.14, 1, "Hello".to_owned());
let value2 = MySecondEnum::TupleType(6.28, 0, "World".to_owned());
if let MyFirstEnum::TupleType(f,i,s) = value
{
text1 = s;
num1 = f;
}
if let MySecondEnum::TupleType(f,i,s) = value2
{
text2 = s;
}
println!("{} {} from the {} man", text1, text2, num1)
}
本节代码位于 07/enumscope 文件夹中,该文件夹包含本书提供的配套代码包。
变量 value1 和 value2 分别作用域于 MyFirstEnum 和 MySecondEnum。当编译时,我们将看到以下输出:

你应该问的两个问题
这段代码有点让人挠头。我们当然应该能够使用类似以下代码的东西?
let value = MyFirstEnum::TupleType(3.14, 1, "Hello".to_owned());
然后使用 value.2 在 println! 语句中直接获取字符串部分,而不是使用 if let 构造?
我们不能这样做的原因是 enum 变体不是它们自己的类型,所以一旦我们创建了前面的值,上面的值就会立即丢失。
第二个问题是:if let 构造是什么?
在 Rust 中,if let 被用作执行某些类型模式匹配的一种方式。
模式和匹配
如我们所见,Rust 包含许多非常强大的功能。我们现在将考虑两个经常看到的,然后回过头来检查我们如何使用 if let 构造。
匹配
让我们看看一个非常不愉快的代码块,然后分析它的含义:
fn my_test(x: i32) -> String
{
if x == 1
{
return "one".to_owned();
}
else if x == 2
{
return "two".to_owned();
}
else if x == 3
{
return "three".to_owned();
}
return "not found".to_owned();
}
该代码接受一个 i32 参数并测试它等于什么。如果条件满足,将返回该数字的一些文本;否则,返回 "not found"。
这是一个简单的例子,但想象一下如果你正在测试 10 个不同的条件;if-else 构造将变得很丑陋。
如果我们在 C 中,我们可以使用 switch/case,Rust 也可以做类似的事情,但关键字是 match。如果我们使用 match 表达式,我们的函数将如下所示:
fn my_test(x: i32) -> String
{
let mut t = "".to_owned();
match x
{
1 => t = "one".to_owned(),
2 => t = "two".to_owned(),
3 => t = "three".to_owned(),
_ => t = "not found".to_owned()
}
return t;
}
在这种情况下,当 x 与 match 表达式内的值匹配时,t 被赋值。如果没有匹配(_ => ...),则 t 被设置为 未找到。match 中必须有一个 _ 通配符模式情况。这是由于 Rust 强制执行详尽性检查。换句话说,直到遇到 _ 通配符之前,Rust 假设必须还有其他值尝试匹配。
让我们真正使函数简单
虽然前面的例子相当紧凑,但我们可以通过将 match 用作表达式来进一步减少代码的足迹。
如果你习惯了 C# 中的 ?,你将熟悉以下构造:
var t = SomeCondition == 3 ? "three" : (SomeCondition == 4 ?
"four" : "not three or four");
这意味着我们可以将 t 赋值为 three,如果 SomeCondition == 3,否则 if SomeCondition == 4,t = four。如果这会传递,我们可以将 t 设置为 not three or four。
这可能会变得混乱。Rust 可以做到同样的事情,但更加干净利落。
在原始代码中,我们有以下内容:
let mut t = "".to_string();
match x
{
我们可以将 match 用作表达式来设置要返回的值:
let t = match x
{
...
};
return t;
或者,更简单地说,只需返回 match 的结果:
return match x
{
...
};
或者更简单地说,当我们记住在 Rust 中,如果一个块没有使用 ;,它会返回其最后一个表达式的结果:
fn my_test(x: i32) -> String {
match x {
1 => "one".to_owned(),
2 => "two".to_owned(),
3 => "three".to_owned(),
_ => "not found".to_owned()
}
}
使用枚举与 match
我们在本章中已经看到,枚举有时很难处理。幸运的是,我们可以在枚举上使用 match:
enum MyFirstEnum
{
TupleType(f32, i8, String),
StructType {varone: i32, vartwo: f64},
NewTypeTuple(i32),
SomeVarName
}
fn tuple_type(v: f32, c: i8, st: String) {//code}
fn struct_type(v1: i32, v2: f64) {//code}
fn new_type_tuple(n: i32) {//code}
fn process_varname() {//code}
fn match_enum_code(e: MyFirstEnum)
{
match e {
MyFirstEnum::SomeVarName => process_varname(),
MyFirstEnum::TupleType(f,i,s) => tuple_type(f,i,s),
MyFirstEnum::StructType(v1,v2) => struct_type(v1,v2),
MyFirstEnum::NewTypeTuple(i) => new_type_tuple(i)
};
}
你会注意到在这个例子中,没有包含 _。这是因为我们明确地匹配了枚举的所有可能选择,所以不需要一个通配符情况。例如,如果我们遗漏了 NewTypeTuple,代码将需要包含一个通配符:
fn match_enum_code(e:MyFirstEnum)
{
match e {
MyFirstEnum::SomeVarName => process_varname(),
MyFirstEnum::TupleType(f,i,s) => tuple_type(f,i,s),
MyFirstEnum::StructType(v1,v2) => struct_type(v1,v2),
_ => return // breaks out of the match
};
}
使用 match 忽略参数
在 match 构造中忽略参数是完全可能的。以下是一个 struct 的例子:
struct Test
{
answer: i32,
real_answer: i32,
score: i32,
}
我们可以在 match 构造中使用这个 struct,就像使用任何其他类型一样。然而,我们想要忽略 real_answer 之后的任何内容。为此,我们将使用 .. 操作符。我们的 match 将看起来像这样:
fn match_test(t: Test)
{
match t
{
Test {answer: Question::MyAnswer, real_answer:
Question::RealAnswer, ..} => {...}
}
}
我们还可以将 _ 用作参数(我们期望一个值,但我们不在乎它是什么):
fn match_test(t:Test)
{
match t
{
Test {answer: Question::MyAmswer, real_answer:
Question::RealAnswer, score:_} => {...}
}
}
你可以欣赏到 match 构造的强大功能,但让我们通过模式来看看它的实际应用。
传递模式
假设我们想要在 C 中以相同的方式有一个传递构造:
switch(foo)
{
case 1:
case 2: printf("1 and 2\n");
break;
case 3: printf("3\n");
break;
}
我们可以在 Rust 中使用 | 模式来做这件事,如下所示:
match foo
{
1 | 2 => println!("1 and 2"),
3 => println!("3"),
_ => println!("anything else")
}
范围
类似于使用 |,我们可以在一系列值上匹配,如下所示:
match foo
{
1 ... 10 => println!("Value between 1 and 10"),
_ => println!("Value not between 1 and 10")
}
我们可以用类似的方式与 char 一起使用,如下面的例子所示:
match char_foo
{
'A' ... 'M' => println!("A - M"),
'N' ... 'Y' => println!("N - Y"),
'Z' => println!("Z"),
_ => println!("something else")
}
在 match 模式中创建绑定
有时,在match构造中创建一个临时变量非常有用,并将模式的结果绑定到它。这可以通过使用@来完成,如下所示:
match test
{
e @ 1 ... 10 => println!("the value is {}", e),
_ => println!("nothing doing")
}
这尝试将模式1到10与test的值进行匹配。如果匹配成功,值将被绑定到t,然后我们可以像处理任何其他变量一样对其进行操作。
我们还可以使用类似于以下示例中的 fall through 构造来绑定到变量:
match test
{
t @ 1 ... 5 | t @ 10 ... 15 => println!("our value for t = {}", t),
_ => println!("dunno!")
}
让我们在其中加入一个 if 语句
我们可以在match模式中包含一个if语句,如下所示:
fn testcode(t: u8)
{
match t
{
1 | 2 if t != 1 => println!("t was not one"),
1 | 2 if t != 2 => println!("t was not two"),
_ => println!("")
}
}
使用复合类型与 match 结合
复合类型是一种包含许多不同类型的类型——在这里,struct可能是最简单的例子。以下也适用于枚举和元组。
我们可以用struct模式匹配,就像我们可以匹配任何其他类型的模式一样,以下是一个示例:
struct MyStruct
{
a: i32,
b: i32
}
fn derp(){
let mystruct=MyStruct{a:1, b:2};
match mystruct {
MyStruct{a, b} => println!("matched the structure"),
_ => println!("didn't match the structure")
}
}
如匹配部分所述,我们可以使用..在struct模式匹配中忽略参数,或者简单地使用_丢弃它们。
然后回到 if let
你现在可能已经意识到,实际上if let是一个以稍微不同的方式编写的match构造。
match构造如下:
match testmatch
{
1 => println!("1"),
_ => println!("not 1")
}
if let版本如下:
if let 1 = testmatch {
println!("1");
}
else
{
println!("not 1");
}
摘要
在本章中,我们看到了 Rust 如何处理一些相当复杂的数据类型,这些数据类型允许我们创建包含许多不同类型的类型,以及处理这些复合类型通常可以是一个相对无痛的过程。我们还注意到了使用枚举时的陷阱。
我们已经探讨了 Rust 对模式和匹配的结构化和灵活方法,以及简单性为开发者提供的强大功能。
在下一章中,我们将探讨一些需要相当多的实践才能理解,甚至更难正确处理的内容——Rust 的生命周期系统。
第八章:Rust 应用程序的寿命
如我们所见,Rust 是一种非常稳定的语言。它也可以被描述为一种内存安全的语言,因为在代码编译时,编译器会测试代码以确保不会出错,例如访问数组外部或释放内存两次。
这完全归功于 Rust 遵守三个关键规则——所有权、引用(或借用,更常见的是借用)和应用寿命。
在本章中,我们将讨论并看到这三个关键方面是如何协同工作以确保你的 Rust 应用程序始终表现良好的。它们如下:
-
所有权
-
借用
-
寿命
它们是什么?
简而言之,我们可以用这些术语来考虑这三个方面。
所有权
当我们想到所有权时,不可避免地会想到占有。我现在正在用我的 MacBook Pro 写这篇文本。它不属于任何财务协议,也没有被盗、借用或租赁,因此它的所有权是我的。
借用
如果我出售或丢弃我的电脑,我将把所有权释放给下一方,或者给回收设施。如果我的儿子有一张我想用的 DVD,我会从他那里借来——他没有把所有权释放给我,只是把它借给我一段有限的时间。他会保留记录或引用,表明我有它。
寿命
这是指某物持续的时间,不幸的是,几乎没有什么东西能永远持续。一旦应用或所有权结束,从获得所有权到移除所有权的时间,包括借用的东西,都被认为是该物体或过程的寿命。
让我们更详细地考虑这些方面。
Rust 中的所有权
为了欣赏所有权,我们需要稍微偏离一下编译抽象和一个非常常见的陷阱。
抽象
任何 Rust 应用程序优于其他语言应用程序的一个方面是它们真的很快且内存安全。这归功于一个称为零成本抽象的理想。抽象是将低级构造提升到更高层次的一种方式,使其更容易、更安全、更可靠。这些在跨平台库中很常见,其中用户界面有一个共同的抽象层,因此开发者只需说var n = new Label {Text = "Hello"};来创建一个用于 UI 的标签,而不需要了解底层发生了什么。
通常,抽象会导致某种形式的惩罚,这意味着使用抽象的代码会比相应的低级代码运行得更慢或使用更多的内存。在 Rust 的情况下,这些零成本抽象意味着在计算机资源方面,它们不会造成任何惩罚。这通常在编译期间完成;编译器生成抽象并执行它们。一旦完成,编译器将生成最佳可能的代码。
这确实有一个问题——编译器会反对开发者认为完全没问题的一段代码。这是因为,作为人类,我们的思维方式与语言不同,所以我们认为正确的拥有权并不是 Rust 所认为的。幸运的是,随着时间的推移,你使用 Rust 越多,这个问题就会变得越小。
拥有权的起源
让我们先考虑一个非常简单的代码片段,以帮助你理解它是如何工作的。我们之前已经多次看到过类似的情况:
fn my_function()
{
let mypi = 3.14f32;
}
当my_function被调用(进入作用域)时,Rust 会在栈上分配内存来存储这个值。当函数结束时(超出作用域),Rust 会自动清理以释放mypi使用的任何内存。
向量,或者任何使用堆的其他东西,以类似的方式工作:
fn my_second_function()
{
let myvec = vec![1.1f32, 2.2f32, 3.14f32];
}
如果你还记得第五章中的“记住,记住”,向量需要在堆和栈上都有内存,可以这么理解:
| 函数名 | 地址 | 变量名 | 值 |
|---|---|---|---|
heap |
heap_posn - 1 |
base_of_vecs |
|
heap_posn - 2 |
Vec[1] |
||
my_second_function |
0 |
myvec |
heap_posn - 1 |
这次,当my_second_function超出作用域时,不仅栈上的位置被清除,而且由myvec指向的堆上的连续位置也会被清除。
我在这里给出了两个变量的例子,这是有原因的——处理方式不同;向量接受一个泛型参数,这些处理方式与标准变量类型不同。
虽然事情并不像这样简单,但要真正理解拥有权下事物是如何运作的,我们确实需要从基本层面来考虑问题。
变量绑定
让我们考虑变量的创建:
let myvar = 10i32;
我们创建了一个名为myvar的非可变变量。然后我们说这个变量有一个值为10的 32 位整数。换句话说,如果这是在 C 语言中,它将是以下这样:
const int myvar = 32;
我们在这里实际上创建了一个变量名和值的绑定。我们说10i32绑定到myvar。绑定在拥有权方面非常重要。Rust 有一条规则,即你只能将某个东西绑定到另一个东西一次。
让我们考虑以下代码片段,因为它展示了在零成本抽象级别时为什么会出现问题:
let myvec = vec![1i32, 2i32, 3i32];
let myothervec = myvec;
通常,作为一个开发者,你会看到myvec绑定到一个包含三个元素的i32类型的向量,然后你会假设myothervec只是第一个向量的一个副本,就像在 C、C++和 C#中一样;这就是它的意思。实现方式可能不同,但含义是相同的。
问题在于,在 Rust 中,这意味着我首先创建了 myvec。当我然后说 myothervec = myvec 时,我实际上是在告诉编译器,myvec 绑定的所有权现在已经转移给了 myothervec,因此 myvec 已经超出作用域,如果(作为一个开发者)我尝试对 myvec 做任何事情,那么编译器将失败构建。
以下截图展示了这一点(可以在 Chapter 8/outofscope 中找到)。当你尝试构建它时,你会得到以下结果:

当一个函数接受所有权时,我们将会遇到类似的问题。
以下内容可以在 08/function_outofscope 中找到:
fn transfer_vec(v: Vec<i32>)
{
println!("v[0] in transfer_vec = {}", v[0]);
}
fn main()
{
let myvec = vec![1i32, 2i32, 3i32];
transfer_vec(myvec);
println!("myvec[0] is: {}", myvec[0]);
}
在第一眼看来,我们没有看到所有权的明显转移,通常,当你将一个变量传递给一个函数时,你并不真正将其视为一个转移。在 Rust 中,直接将变量传递给另一个函数与我们的第一个例子相同:所有权从 myvec 释放并传递给函数。
为了证明这一点,尝试编译代码,你将得到以下输出:

换句话说,这与之前相同的错误。
栈和堆变量
要理解我们为什么会遇到这个问题,我们需要深入理解 Rust 的工作方式,也就是说,在内存级别。
让我们从我们的变量开始:
let myvar = 32i32;
正如我所说的,在我们的心中,我们将创建一个 myvar 变量,类型为 i32,并将其绑定到值 32。另一方面,Rust 的做法不同。
首先,它确定我们需要在栈上为大小为 i32 的值腾出空间。
接下来,它将 32 的值复制到栈上分配的空间。
最后,它将绑定绑定到栈分配块的地址与变量名绑定。
换句话说,这与我们在心中所做的是完全相反的。
让我们看看当我们创建另一个绑定时会发生什么,就像这样:
let myvartwo = myvar;
编译器将绑定移动到 myvar 在栈上的数据位置,然后说那个位置(和数据)属于 myvartwo。绑定将被转移。那么 myvar 会发生什么?Rust 不允许事物 悬空 或允许信息绑定到两个不同的对象。一旦绑定被转移,myvar 就会被移除。
如果绑定指向堆中的某个东西,也会发生相同的事情。因此,当我们考虑 let myvec = vec![1i32, 2i32, 3i32]; 时,我们知道这将如何工作。编译器知道它需要在堆上分配足够的空间,以容纳三个 i32 类型的元素。这些值被复制到位置,连续内存块的基础地址被绑定到 myvec。
现在,让我们转移所有权:
let vectwo = myvec;
现在,vectwo 是堆上向量的唯一可用的绑定,而 myvec 被无效化。
这为什么很重要?
在 C# 等语言中,一个非常常见的错误是当你有以下代码时:
var myList = new List<int>{1,2,3,4,5,6};
var dupVar = myList;
dupVar.Remove(4); // 4
foreach(var n in myList)
Console.WriteLine(n);
从这个输出中,我们可能得不到预期的结果,如下所示:

可能会预期,当我们从 dupVar 中移除了重复项后,myList 变量仍然应该包含它最初设置的所有数字。在这段代码中,发生的情况是 dupVar 被称为复制指针——我们有两个变量绑定到栈上的同一个指针。虽然这看起来可能不是什么大问题,但我们有两个变量名能够改变数据。这会让很多人感到困惑,并导致比它值得的更多内存和内容错误。
由于 Rust 只允许每个块有一个指针,我们不能有类似这种情况。一旦所有权被转移,原始的绑定名称就不再可以访问。
Copy 特性
本节代码可以在 08/copyint 和 08/copyf32 文件夹中找到。
Rust 确实有一种创建原始数据副本的方法:Copy 特性(特性将在第十章创建自己的包中介绍),所有原始类型都实现了 Copy。如果我们有类似 let varone = 1i32; 或 let vartwo = varone; 这样的代码,那么 i32 是一个原始类型,vartwo 变量将包含 varone 的副本。两者都将有自己的栈分配,而不是 vartwo 指向 varone。所有权不会改变;值被复制并绑定到新变量。
本节代码可以在本书提供的配套代码包中的 08/copyint 和 08/copyf32 文件夹中找到。
因此,我们可以这样编写代码:
fn do_something(number: i32) -> i32
{
number + 32
}
fn main()
{
let num = 10i32;
let numtwo = do_something(num);
println!("num is: {}", num);
println!("numtwo is : {}", numtwo);
}
当编译上述代码时(numone 是一个 i32 值,它是一个原始类型,所以在传递给 do_something 并返回 i32 到 numtwo 时会复制自身)将产生以下输出:

copyf32 示例展示了相同的 Copy 特性在 f32 原始类型上的应用。
必须有一种方法可以绕过这个问题。
从某种意义上说,我们已经在本书中的许多例子中看到了答案——我们归还了所有权;然而,正如以下代码块所示,这可能会变得有些混乱:
fn sumprod(v1: Vec<i32>, v2: Vec<i32>) -> (Vec<i32>, Vec<i32>, i32)
{
let sum = v1.iter().fold(0i32, |a, &b| a + b);
let product = v2.iter().fold(1i32, |a, &b| a * b);
return (v1, v2, sum + product); // return ownership
}
fn main()
{
let vecone = vec![2,3,5];
let vectwo = vec![3,5];
let (vecone, vectwo, ans) = sumprod(vecone, vectwo); // pass ownership
println!("ans = {}", ans);
}
上一段代码将产生以下输出:

本节代码可以在本书提供的配套代码包中的 Chapter8/handback 文件夹中找到。
幸运的是,Rust 提供了一种更整洁的方式来传递所有权。我们不是给予所有权,而是借用所有权。
Rust 中的借用
在第二章 变量 中,我们提到了一个称为引用的东西,它被描述为对某个内存位置的指针的副本。这是 Rust 中借用的一部分含义。
在我们前面的例子中,我们可以利用借用。我们为其编写的代码如下:
fn sumprod(v1: &Vec<i32>, v2: &Vec<i32>) -> i32
{
let sum = v1.iter().fold(0i32, |a, &b| a + b);
let product = v2.iter().fold(1i32, |a, &b| a * b);
return sum + product;
}
fn main()
{
let vecone = vec![2,3,5];
let vectwo = vec![3,5];
let ans = sumprod(&vecone, &vectwo);
println!("ans = {}", ans);
}
本节代码可以在本书配套代码包中的 08/handback 文件夹中找到。
我们将不再传递所有权,而是将向量的引用传递过去。当编译时,我们将得到以下结果:

借用不可变性
如果我们回顾本章的开头,我描述了借用就像是从我儿子那里借 DVD。当我拿到 DVD 时,我不能改变它,因为我的儿子期望得到同样的 DVD。
对于 Rust 来说,引用不能改变,因为它们是不可变值。如果你这么想,这很有意义。让我解释一下。
我创建了一个 Vec<T> 数组类型,比如说,有八个值长(值是什么或者它们的类型无关紧要)。当堆和栈之间的绑定建立时,它将具有特定的类型。如果我们允许引用改变向量,我们就会遇到 C#示例中的同样问题,因此不能保证保证,Rust 编译器将失败构建。为了确保保证得到保持,Rust 简单地说你不能改变所借用的值。
可变借用
如果我们用借 DVD 的比喻来说,这更像是一个可写的 DVD 而不是预先录制的 DVD。
这里,我们使用了一个可变引用,我们必须小心如何使用它们。
本节代码位于本书提供的配套代码包中的 08/mutableref1 和 08/mutableref2 文件夹中。
在我们的第一个例子 (mutableref1) 中,我们将创建一个变量,引用,做些事情,然后得到一个新的值:
fn main()
{
let mut mutvar = 5;
{
println!("{}", mutvar); // outputs 5
let y = &mut mutvar; // creates the mutable ref to mutvar
*y += 1; // adds one to the reference and passes it back in to mutvar
}
println!("{}", mutvar); // outputs 6
}
这里重要的行是 *y += 1; 并且特别地,这里的 *,因为它意味着我们正在直接改变引用指向的内存位置上的值。在处理与内存相关的事情时,必须非常小心。
第二个需要注意的重要点是,我们在可变引用中使用了括号。去掉它们,一切都会失败(mutableref2):

重要的行是错误的输出;它表示你不能同时以可变和不可变的方式借用相同的项。这就像说你可以同时借用可以改变和不能改变的东西!简直是胡说八道。这是借用有规则的结果。
Rust 的借用规则
在借用时必须遵守两条规则,如下所示:
-
你所借用的东西不能比原始的存活时间更长
-
你可以有下面以下几种借用类型,但绝不能同时进行:
-
一个(或多个)类型为
&T的引用到资源 -
只有一个可变引用
-
第一条规则是有意义的:你不能让引用比它来的地方存活时间更长,因为一旦它来的地方出了作用域,它就会被销毁,一旦销毁,你到底借了什么?
第二条需要更多思考为什么它是这样的,以及 Rust 试图实现什么。
在这种情况下,Rust 确保发生了一个称为竞争条件的情况(如果你习惯于编写多线程应用程序,你将已经理解这些)。
在这里,Rust 试图防止两个引用同时尝试访问同一内存点。换句话说,Rust 试图防止同步错误。
对于不可变引用,你可以有任意多个,因为引用永远不能被写入。对于可变引用,Rust 通过只允许一个引用有效来防止问题。
考虑到这一点,我们可以使用这些规则来修复mutableref2中的代码,以去掉可变引用周围的{}花括号吗?
解决问题
让我们再次检查这段代码(我已经删除了这里不需要的原始内容):
let mut mutvar = 5;
let y = &mut mutvar;
*y += 1;
println!("{}", mutvar);
当我们尝试编译时,编译器会返回以下输出:

我们打破了第二个规则——你只能有一个可变引用或多个不可变引用,永远不能两者都有。
我们该如何解决这个问题?让我们回顾一下原始的mutableref1:
fn main()
{
let mut mutvar = 5;
{
println!("{}", mutvar); // outputs 5
let y = &mut mutvar; // creates mutable reference to mutvar
*y += 1; // adds 1 and passes result back in to mutvar
}
println!("{}", mutvar); // outputs 6
}
这行得通,但为什么?
考虑作用域
这段代码实际上发生了什么,是我们为代码的借用部分创建了一个新的作用域,在到达最终的println!之前,将其传递回mutvar。换句话说,作用域发生了变化;因此,当在println!中遇到mutvar时,没有发生借用,我们只是显示绑定到mutvar变量的内容。
如果我们要移除花括号,我们必须确保在通过println!输出之前,借用已经完成
这一切都是为了你自己的好处
这些编译器规则是为了帮助开发者。它们防止了在其他语言中常见的问题,最大的问题是变量被销毁后写入,或者做一些愚蠢的事情,比如尝试在遍历向量的循环中修改向量:
fn main()
{
let mut myvec = vec![5i32, 10i32, 15i32, 20i32, 25i32, 30i32];
for i in &myvec
{
println!("i = {}", i);
myvec.push(35i32);
}
}
本节的内容在本书的支持代码包的08/invaliditerator文件夹中,第五章,内存管理中有进一步的讨论。
这显然永远不会工作。如果你想想,我们有一个循环,它以myvec作为参数,然后在循环中向向量中添加内容,所以循环永远不知道一个保证,因为那个保证不存在:迭代器计数。它不会很好地构建,因为我们打破了第二个借用规则。
生命周期
让我们考虑另一段不会工作的代码:
let varname: &f32;
{
let x = 3.14f32;
varname = &x;
}
println!("varname = {}", varname);
当尝试构建这段代码时,编译器会如下抱怨:

你可能还记得,我们在第四章,条件、递归和循环中看到了类似的代码片段:
let y: &f32;
{
let x_squared = x * x;
let x_cube = x_squared * x;
y = &(x_cube + x_squared + x);
};
println!("Y = {}", *y);
在第五章,内存管理中,我们解释了为什么前面的代码不会工作。
我们正在将 y 赋值给一个仅在小型作用域中存在的变量的值,然后尝试访问该值,这导致了未定义的行为。正如我们所见,Rust 编译器会尽一切可能防止这种错误。在这种情况下,编译器会跟踪每一个引用,如果引用的持续时间超过了正在使用的指针,则无法构建。
我们在这里遇到了相同的情况:varname 在 x 之前声明;因此,它的生命周期比 x 更长,这就是导致错误的原因。
前面的代码是一个生命周期的简单演示,但它并不像那样简单。
神话般的银行账户
为了演示一个更复杂的问题,让我们考虑一个神话般的银行账户:
-
我被合法地赋予了访问银行账户的权限
-
我决定让我的朋友能够访问它
-
在一段时间之后,我决定我不再想访问这个账户,并移除了我的访问权限
-
然后,我的朋友尝试使用这个账户
当我的朋友来使用这个账户时,他无法这样做,因为我所拥有的,并且传递给他的引用不再存在。他正在尝试 use after free(在编程术语中),在这里他被称为 悬垂引用。
这听起来有些牵强,但在开发术语中,它发生的频率比你可能给予的信用要高得多。
生命周期变量 - '
Rust 中有两种生命周期类型——隐式和显式。我们已经多次看到了隐式函数:
fn myfunction(pi: &f32)
{
// do something
}
函数的生命周期是代码在括号内存在的时间长度,一旦被调用。
我们还有一个显式生命周期,用 ' 符号在名称之前表示:
fn expfunction<'a>(pi: &'a f32)
{
// do something
}
然而,'a 究竟意味着什么?
这意味着,对于 a 的生命周期。expfunction 后的 <> 表示该函数正在接受一个泛型参数(这些将在第九章 泛型和特质 中变得清晰),但它意味着一个类型。如果你考虑 Vec,它实际上是 Vec<T>。当我们创建一个 f32 类型的向量时,T 变成了 f32,因此在编译时它是 Vec<f32>。在 expfunction 的情况下,T 是 'a,因此括号内的类型也必须是 'a。
如果我们在 <> 中有另一个参数,我们会有 <'a, 'b>(f: &'a f32, g: &'b i32),依此类推。
其他类型的生命周期
我们通常会看到使用 struct 和 impl 等方式表达生命周期(impl 和 impl 生命周期在第十章 匹配和结构体 中处理)。你还可以使用多个生命周期。
结构体中的生命周期
如第七章 结构体 所见,Rust 中的结构体有特殊用途,它们也可以包含多个类型,并且可以根据需要扩展,使用所需的参数数量。以下代码片段作为例子:
struct MyStruct
{
a: i32,
b: f32,
c: bool,
}
以下代码创建了一个名为MyStruct的struct,具有三个属性,分别称为a、b和c。当mystruct的实例进入作用域时,struct内的元素可以轻松访问。如果我们想让struct能够接受一个生命周期变量,我们必须明确要求struct接受该生命周期变量,并将其分配给一个元素,如下面的代码所示:
struct<'a> MyStruct
{
lifetimevar: &'a f32,
nvar: i32,
}
在其中包含生命周期变量,我们可以确保结构体不会超出它传递给f32引用的生命周期。
多个生命周期
这两种方法都可以在函数内定义多个生命周期:
fn mylifetime<'a>(life: &'a i32, universe: &'a i32) -> &'a i32
{
// do something, return an i32 value
}
我们有两个生命周期参数的参数被i32值转换,并返回一个i32值。
我们也可以传入多个生命周期,如下所示:
fn mymultilife<'a, 'b>(foo: &'a f32, bar: &'b i32)
{
// do something
}
总是考虑作用域
与借用一样,我们必须考虑作用域以确保事情能够正确工作。
例如,以下代码片段将无法工作:
struct MyStruct<'a> {
lifea: &'a i32,
}
fn main()
{
let x;
{
let y = &5; // means let y = 5; let y = &y;
let f = MyStruct { lifea: y };
x = &f.lifea
}
println!("{}", x);
}
本节代码位于随本书提供的支持代码包中的08/lifetimescope文件夹中。
起初可能不明显为什么这不应该工作。从作用域的角度来看,f是在y之后创建的,因此位于y的作用域内,而y是在x的作用域内创建的。或者不是吗?
当代码构建时,我们将得到以下输出:

错误将是x = &f.lifea,因为我们试图将即将超出作用域的某个值赋给一个值。
struct
许多语言的一个有用方面是拥有一个在整个应用程序生命周期中存在的变量。虽然一些纯粹主义者认为拥有一个持续整个应用程序生命周期的变量不是好的实践,但他们无法否认它有其用途。
在 Rust 中,我们也可以使用一个特殊的struct类型,一个生命周期struct来完成这个操作:
let version: &'static str = "v1.3, 22nd May 2016";
本地类型推断允许我们在不是全局的情况下省略类型,因此这在函数内部等同于上面的代码:
let version = "v1.3, 22nd May 2016";
输入和输出生命周期
虽然不常被考虑,但有两种生命周期:输入(进入函数)和输出(离开函数)。
仅输入
以下代码片段是一个具有输入生命周期的函数示例:
fn inponlyfn<'a>(inp: &'a as i32) {...}
仅输出
以下代码片段是一个具有输出生命周期的函数示例:
fn outonlyfn<'a>() -> &'a as i32 {...}
输入和输出
以下代码片段是一个具有输入和输出生命周期的函数示例:
fn inandout<'a>(inp: &'a str) -> &'a str {...}
我们可以从前面的代码片段得出以下结论:
-
函数参数中的每个生命周期都成为一个独立的生命周期参数
-
如果只有一个输入生命周期,则生命周期被分配给返回值中的所有生命周期
我们还需要包含另一个概念:如果有多个输入生命周期,其中一个指向&self(可变或不可变),则self的生命周期适用于所有输出生命周期。
摘要
理解 Rust 如何处理变量的生命周期对于确保在创建 Rust 应用程序时尽可能少犯错误至关重要。我们已经考虑了信息如何在函数间传递,并看到了 Rust 模型如何确保我们不会留下悬垂引用或指向不再属于变量的内存位置的代码。我们还看到了 Rust 如何消除数据竞争条件的能力。
在下一章中,我们将考虑泛型类型,它们对您的 Rust 应用程序的重要性,以及编译器如何处理它们。
第九章:介绍泛型、实现和特性
任何现代语言的关键优势之一是能够使用任何类型的类型。这不仅减少了所需的代码量,而且允许在代码创建中具有更大的灵活性。Rust 不仅允许使用泛型类型和函数,还引入了特性;这些可以被视为泛型的逻辑扩展,因为它们告诉编译器类型必须提供的功能。
在本章中,我们将探讨以下主题:
-
Rust 中的泛型
-
实现和特性
-
泛型类型
-
特性对象
泛型 101
对于来自 C++ 和 C# 等语言的开发者来说,泛型将不会是陌生的。它通常表示为 T。它以与标准类型相同的方式使用。由于 T 实际上没有类型,它被称为多态参数。
关于泛型类型有一个简单的规则。
类型必须匹配——如果我们定义 T 为 f64 并尝试将其分配给一个 String,编译器将无法构建该代码。
虽然 T 也可能是最常用的通用类型字母,但实际上你可以使用任何字母,甚至单词。
例如,以下代码是完全可以接受的:
enum Result<Y, N>
{
Ok(Y),
Err(N),
}
Y 和 N 不需要是相同的类型;因此,Y 可以是一个 String,而 N 是一个 bool。
实际上,以下展示了泛型类型的工作方式。Option 是标准库的一部分:
enum Option<T>
{
Some_Type(T),
None
}
let varname: Option<f32> = Some_Type(3.1416f32);
泛型还提供了另一个有用的功能:它们允许生成泛型函数。
泛型函数——你可以向其投掷任何东西的函数!一个标准函数可能看起来像这样:
fn defined_type_fn(x: i32)
{
// do something with x
}
本节的示例代码可以在 09/multiply_generic_return_t 中找到。
正在被传递的参数是一个 i32 类型,并被称为 x。如果我们尝试传递一个浮点数、布尔值、字符串或任何其他不是 i32 类型的类型,编译器将因为类型不匹配而失败构建。
通用函数看起来非常相似:
fn generic_type_fn<T>(x: T)
{
// do something with x
}
在风格上,这与在 C# 中编写泛型方法非常相似:
void generic_type_method<T>(T x)
{
// do something
}
这可以扩展为接受具有相同类型的多个参数:
fn generic_type_fn<T>(x: T, y: T)
{
// do something
}
或者使用多种类型和参数:
fn generic_types_fn<T, U, V>(x: T, y: U, z: V)
{
// do something
}
最后,我们可以将泛型用作返回类型。回想一下,标准函数返回一个值如下:
fn multiply(a: i32, b: i32) -> i32
{
return a * b;
}
通用返回值将是以下这样:
fn multiply_generic<T>(a: T, b: T) -> T
{
return a * b;
}
这只适用于简单的类型;你不能乘以字符串类型,尽管你可以连接它们——这意味着你将一个字符串添加到另一个字符串上。但问题是,我们目前还不能这样做...
当我们尝试构建这个时,会生成一个错误:
Binary operation '*' cannot be applied to type 'T'; an implementation of 'std::ops::Mul' might be missing for 'T'
让我们看看我们能否将其分解一下,看看为什么我们会得到错误。
理解错误
我们知道 a 和 b 都是 T 类型,但 a 的“真实”类型是什么?
在这里,a 需要是任何实现了 std::ops::Mul 的类型——也就是说,* 操作符。此外,这个函数的输出也需要明确指定。
当你看到类似 std::ops::Mul 的东西时,它只是说我们将使用 namespace std.ops 的等效物(如果我们使用 C#)。这只是正在使用的库。
让我们改变类型,告诉编译器 T 需要实现 Mul,并且我们将产生一个类型为 T 的结果:
fn multiply_generic<T: Mul<Output = T>>(a: T, b: T) -> T
{
return a * b;
}
<T: Mul<Output = T>> 实际上意味着我们将使用 Mul,并且输出将是类型 T。
这次,我们可以构建代码,并且代码运行正常,如下面的截图所示:

非常方便!顺便说一下,还有另一种声明它的方法:
fn multiply_generic<T>(a: T, b: T) -> T
where T: Mul<Output = T>
{
return a * b;
}
哪一种更整洁取决于程序员,因此你可能会看到并使用这两种风格。
问题是:如果我们发送一个字符串会发生什么?幸运的是,在这个形式中,编译器会抛出一个错误,并且不允许代码构建:

一个泛型问题
泛型的一个方面是确定 T 是什么,因此我们可以如何处理它。在 C# 中,我们可以使用 System.Reflection 并使用 GetType 方法来查找类型,或者在使用类型比较时使用 typeof。
这部分的源代码可以在 09/generic_typeof 中找到。
在 Rust 中,我们使用 std::any:Any。这是一个用于模拟动态类型的数据类型:

只看这个输出,你可能在想:这些数字究竟是什么?我期望看到类似 f32 的东西*。
这部分的关联代码可以在 09/generic_typeof_print 中找到。
我们在这里看到的是类型的 ID 而不是类型本身。要实际显示变量类型,我们需要做一些稍微不同的事情:
#![feature(core_intrinsics)]
fn display_type<T>(_: &T)
{
let typename = unsafe {std::intrinsics::type_name::<T>()};
println!("{}", typename);
}
fn main()
{
display_type(&3.14f32);
display_type(&1i32);
display_type(&1.555);
display_type(&(vec!(1,3,5)));
}
在撰写本文时,此代码仅能在夜间分支上构建。很可能在你阅读这本书的时候,它已经进入了稳定分支。
当前面的代码在 Rust Playground 网站上运行时,会得到以下结果:

尽管我们已经多次看到大部分代码,但我们还没有在代码中遇到 unsafe 和 shebang (#!)。
不安全指令
我们已经多次看到,Rust 编译器会尽力确保你编写的代码不仅能够编译,而且不会做愚蠢的事情(例如超出数组的界限,使用错误的数据类型,或者使用未先赋予值的变量)。
这被称为 安全 代码。这并不是说所有安全代码都是好代码——你仍然可能遇到内存泄漏、整数溢出或线程死锁,这些都是你不想要的,但实际上并没有定义为不安全的。
在 Rust 中,unsafe 包围的代码确实意味着——你正在告诉编译器你写的代码将被内置保护忽略。
使用 unsafe 应该非常小心。我们稍后会遇到 unsafe。
整个 #!
对于那些习惯于 Linux Shell 脚本编写的人来说,你肯定见过 #!——在 Rust 中,# 是一个带有名称在 [] 中的声明属性。它们可以写成 #[attr] 或 #![attr]。
然而,#[attr] 和 #![attr] 的意义是不同的。#[attr] 仅直接应用于其后的内容。#! 改变了属性应用的对象。
我们在第二章,变量和变量类型 中看到过这个,当时我们讨论了编写测试。我们会有类似这样的内容:
#[test]
fn do_check()
{
// perform check
}
这个 do_check 函数只有在运行测试时才会执行。
特性和实现
Rust 中一个非常强大的特性,当处理泛型时通常可以看到,是能够告诉编译器特定类型将提供某些功能。这是通过一个称为 trait 的特殊功能提供的。
然而,要欣赏特性,我们首先必须看看 impl 关键字(简称实现)。
实现
impl 关键字的工作方式与函数非常相似。实现的结构需要被视为更接近静态类(在 C# 中)或函数中的函数:
impl MyImpl
{
fn reference_name (&self) ...
}
这将更多适用于非泛型类型。对于泛型,前面的代码变为以下内容:
impl <T> MyGenericImpl<T>
{
fn reference_name(&self) ...
}
注意 <T> 是必需的,以便告诉编译器 impl 是针对泛型的。reference_name 是用于访问 impl 函数的名称。它可以是你想要的任何名称。
impl 的一个例子可以在 09/impl_example 中找到。
如果你构建并运行 impl_example 代码,你将得到如下结果:

代码创建了两个函数的实现,这两个函数提供了定义的功能。
impl_example 是一个非常简单的例子。impl 可以根据需要变得非常复杂。
实现 lifetime
如第八章,Rust 应用程序生命周期 中所述,我们可以使用 lifetime 与 impl 一起使用:
impl<'a> MyFunction<'a>(varname: &'a as i32) {...}
'a 直接跟在 impl 和 MyFunction 之后。对于 impl,这是表示我们正在使用它,而在 MyFunction 之后,这是表示我们在 MyFunction 中使用它。
然后我们回到特性上...
将特性视为创建实现签名的最简单方式。如果你习惯于 C(或 C++),那么你将在类似这样的代码中看到它:
// mylib.h
int myFunction(int a, int b, float c);
// mylib.c
#include "mylib.h"
int myFunction(int a, int b, float c)
{
// implement the code
return some_value;
}
// myotherfile.c
#include "mylib.h"
int some_function()
{
int value = myFunction(1, 2, 3.14f);
return value;
}
编译器接受这段代码是正确的,因为 .h 文件中的签名在某处声明了一个编译函数,它提供了这个调用的实现。当编译器到达链接一切的时候,根据签名承诺的代码被找到,myFunction 执行它应该执行的操作并返回 int。
在 C# 中,这将通过 interface 提供。
使用 Rust,我们有一些非常相似的东西。
trait 提供了签名,impl 提供了实现,代码调用 impl。
现在这可能看起来有些过度。为什么要在实现通常在同一个源文件中的时候创建一个存根?答案是我们可以使用 Rust 库中的特性。特性告诉编译器某处有代码实现,它将在构建的最后阶段被链接。
我们将在下一章中查看 crates。
一个简单的 crate 示例
在这个例子中,我们将创建一个特质,它将包含两个函数的签名:calc_perimeter 和 calc_area。首先,我们构建一个 struct。在这种情况下,我们将有两个 struct:
struct Perimeter
{
side_one: i32,
side_two: i32,
}
struct Oval
{
radius: f32,
height: f32,
}
我们需要为每个创建一个特质。特质的通用格式如下所示:
trait TraitName
{
fn function_name(&self) -> return_type;
}
在我们的情况下,我们将有以下内容:
trait CalcPerimeter
{
fn calc_perimeter(&self) -> i32;
}
trait CalcArea
{
fn calc_area(&self) -> f32;
}
现在我们需要为这两个特质创建实现。然而,impl 的外观将不会完全相同。
在之前,我们有以下内容:
impl SomeImplement
{
...
}
这次,我们必须给出与它相关的结构体的名称:
impl SomeImplement for MyStruct
{
...
}
如果 impl 定义了特质,而特质只是一个占位符,为什么我们还需要说明它对应的结构体?
这是一个合理的问题。
没有特质,impl 的操作方式类似于函数。我们通过 &self 向 impl 提供参数。当我们有一个特质时,impl 必须说明 &self 指的是什么。
这个代码可以在 09/non_generic_trait 中找到。
我们第一个特质的 impl 将如下所示:
impl CalcPerimeter for Perimeter
{
fn calc_perimeter(&self) -> i32
{
self.side_one * 2 + self.side_two * 2
}
}
注意,该函数可以访问 Perimeter struct 中的 side_one 和 side_two。
第二个 impl 将看起来像这样:
impl CalcArea for Oval
{
fn calc_area(&self) -> f32
{
3.1415927f32 * self.radius * self.height
}
}
最后,对实现的调用。与之前的示例不同,这两个结构体都必须初始化,然后才能给出实现调用:
fn main()
{
let peri = Perimeter
{
side_one: 5, side_two: 12 };
println!("Side 1 = 5, Side 2 = 12, Perimeter = {}",
peri.calc_perimeter());
let area = Oval
{
radius: 5.1f32,
height: 12.3f32
};
println!("Radius = 5.1, Height = 12.3, Area = {}",
area.calc_area()); }
一旦代码编译完成,预期的答案如下所示:

特质和泛型
如果我们查看代码,我们有两个结构体实际上执行相同的功能,唯一的区别是参数的类型不相同。我们可以更改结构体的成员名称而不存在问题,以使生活更简单:
struct Perimeter { side_one: i32, side_two: i32, }
struct Oval { radius: f32, height: f32, }
这将变成以下内容:
struct Shape<T> { line_one: T, line_two: T, }
计算不能改变,因为它们完全不同,但需要更改参数名称。需要更改的另一个方面是函数的名称。让我们创建一个只使用部分代码的代码版本。
由于我们有 struct 的泛型版本,我们接下来需要创建一个特质:
trait Calculate<T> { fn calc(&self) -> T; }
我们必须使用 <T>,因为特质必须接受一个泛型。
实现的构建可以通过两种方式之一实现。
这个部分的代码可以在 09/generic_traits_simple 中找到。
为特定类型定义 impl
这是最简单的方法之一来创建代码。我们定义 Shape 可以接受的数据类型:
impl Calculate<i32> for Shape<i32>
{
fn calc(&self) -> i32
{
self.line_one * 2 + self.line_two * 2
}
}
按照这种方式编写代码可以确保我们不会将任何没有意义的东西传递给实现(例如,不能对它们应用 + 或 * 的类型)。
使用 where
如果你习惯于使用 C# 中的泛型编程,这应该对你来说很熟悉。
Rust 包含 where 的实现,因此我们能够定义 where 是什么。这意味着,正如我们在本章早期示例中看到的那样,构造 <T: Mul<Output = T>> 可以以修改后的方式使用:
impl<T> Calculate<T> for Shape<T> where T: Mul<Output = T>
虽然如此,这确实引发了许多其他问题。其中两个简单的问题是,我们乘以2——然而,如果它是2u8还是2i32,这个值并不明确。我们还尝试将值相加,但与将T相乘一样,没有保证你可以通过T来相加。
使其工作
最后一步是添加一个main函数。我们可以使用之前非泛型特质示例中的相同函数,但要去掉椭圆:
fn main()
{
let peri = Shape
{
line_one: 5,
line_two: 12
};
println!("line_one = 5, line_two = 12, Perimeter = {}",
peri.calc ());
}
编译后,它给出了以下输出:

由于我们已经创建了第二个实现,将main函数扩展以包括第二个计算应该是微不足道的。
有关这部分代码文件的详细信息,请参阅09/generic_trait_full。
我们还需要实现f32的计算:
impl Calculate<f32> for Shape<f32>
{
fn calc(&self) -> f32
{
3.1415927f32 * self.line_one * self.line_two
}
}
当这个文件被编译时,我们会看到以下内容:

你可能已经注意到了
如果我们比较两种不同的代码实现(泛型和非泛型),主要区别在于我们减少了所需的代码量,因为这两个结构体在名称之外都是相同的。我们还简化了代码,以便我们只有一个对calc的调用,并允许编译器根据传入的类型来决定我们需要哪个。
泛型 - 稍作补充
代码的缩减和简化始终是一件好事(至少大部分情况下是这样)。然而,在使用泛型时,总会有权衡,而且并不总是显而易见的。
让我们考虑以下代码片段:
fn my_multiply<T: Mul<Output = T>>(a: T, b: T) -> T { return a * b; }
这个函数通过乘以两个变量(类型为T)返回一个T类型的值。
问题是:你可以向该函数发送多种类型——如果编译器不知道T的类型,它将如何知道该做什么?唯一安全的方法是为每种可能的类型创建一个my_multiply版本。幸运的是,编译器会自动为你完成这个过程,称为单形化。
那么到底发生了什么?
为了给所有这些生成的函数赋予独特的名称,与泛型一起工作的编译器使用了一个称为名称混淆(或名称混淆)的过程。这为每个内部创建的、带有泛型参数的函数创建了一个独特的名称。
在链接过程中,对于应该使用哪个版本,链接器会分析所需的代码签名。如果链接器看到一个需要f32作为T的签名,那么混淆后的名称对象代码将被包含在最终对象列表中。一旦链接器完成分析,未使用的对象(那些不在最终列表中的对象)将被剥离。因此,最终的二进制文件只包含所需的代码,而不是可能的所有类型变体。
尽管不同的编译器对泛型有不同的处理方式,但在编译、名称混淆以及最终剥离的过程中,它们之间是通用的!
回到where版本
代码的where版本比非where版本更复杂。
这个版本的源代码可以在09/generic_trait_where中找到。
让我们检查一下代码:
extern crate num;
use std::ops::{Add, Mul};
use num::FromPrimitive;
我们之前在泛型乘法示例中见过 std::ops::Mul。如果我们需要从 std::ops(或者确实任何库)中包含多个项目,它们被放在花括号 {} 中。这里,我们包含了 Add 和 Mul。
到目前为止,我们还没有看到 extern crate 指令。现在,只需要知道这将包含一个外部库。Crates 在第九章,泛型和特性 中介绍。
最后,我们使用 num 库的 FromPrimitive。
我们的 struct 和 trait 和之前一样。不过,实现是不同的:
impl<T> Calculate<T> for Shape<T>
where T: Copy + FromPrimitive + Add<Output = T> +
Mul<Output = T>
{
fn calc(&self) -> T {
let two = T::from_u8(2).expect("Unable to create a value of 2");
self.line_one * two + self.line_two * two
}
}
这段代码中有两行很重要:where T:Copy + FromPrimitive + Add<Output = T> + Mul<Output = T> 和 let two = T::from_u8(2).expect("Unable to create a value of 2");。
这里,我们说的是我们想要复制类型,我们将使用 FromPrimitive 来将原始类型转换为 T,并且 Add 和 Mul 的输出都将为类型 T。Rust 使用 + 连接 where 使用的参数。
let two 行创建了一个变量,它接受一个无符号 8 位值并将其转换为 T。如果失败,则会抛出错误。
我们必须使用 Add<Output = T> 来确保我们可以将类型相加。
尝试编译
如果你使用标准的 cargo run,你会遇到一个错误,编译器 无法找到 extern crate num。这是由于 cargo 不知道依赖项在哪里。在第一次获取外部引用时,Rust 将更新可用的 crate 列表(注册表),然后下载所需的 crate。为此,需要编辑 Cargo.toml 文件并插入以下代码:
[dependencies]
num = "*"
保存后,执行 cargo run,你会看到如下输出:

特性限制
一个特性也可以被施加一个限制。实际上,限制是一个特性必须遵守的规则,并且被添加到声明类型参数中。
这一部分的源代码在 09/trait_bound_gen_struct。
在代码示例中,impl 对泛型类型放置了一个 PartialEq 限制。我们的 struct 内部包含四个参数,所以我们只想在该 struct 内进行部分相等性测试。如果没有在声明的类型参数上有 PartialEq,编译将失败,因为我们没有测试该 struct 内部的所有内容。
当代码编译时,我们得到以下输出:

我们能否进一步减少代码量?
是的。如果特性包含默认方法,则可以完全省略创建特性实现的必要性:
trait MyTrait
{
fn test_code(&self) -> bool;
fn self_test_code(&self) -> bool { self.test_code() } }
test_code 只是一个需要实现的存根。self_test_code 函数不需要实现,因为它已经有了默认方法。
默认方法可以被覆盖吗?
它可以。
这一部分的代码在 09/override_default_method。
让我们从定义一个trait开始编写代码。这有一个is_not_done的默认方法。尽管如此,我们仍然需要实现is_done,我们为UseFirstTime结构体这样做:
struct UseFirstTime;
impl MyTrait for UseFirstTime
{
fn is_done(&self) -> bool
{
println!("UseFirstTime.is_done");
true
}
}
接下来,我们想要重写is_not_done的默认方法。同样,我们创建了一个空的struct并编写了is_done和is_not_done的实现。当我们从第二个struct中调用is_not_done时,显示的是第二个struct中的println!,而不是第一个:
struct OverrideFirstTime;
impl MyTrait for OverrideFirstTime
{
fn is_done(&self) -> bool
{
println!("OverrideFirstTime.is_done");
true
}
fn is_not_done(&self) -> bool
{
println!("OverrideFirstTime.is_not_done");
true
}
}
编译后,我们得到以下输出:

特质总结
这是一个很大的主题,但我们还有两个关于特质的方面需要考虑:继承和派生。如果你熟悉任何形式的面向对象编程,这应该很熟悉。
继承
这与 C++和 C#中的继承非常相似:
trait One
{
fn one(&self);
}
trait OneTwo : One
{
fn onetwo(&self);
}
这一部分的代码在09/inheritance中。
实现OneTwo的代码也必须实现One(这与当我们重写默认方法时仍然需要定义is_done的情况相同),因此:
struct Three;
impl One for Three
{
fn one(&self)
{
println!("one");
}
}
impl OneTwo for Three
{
fn onetwo(&self)
{
println!("onetwo");
}
}
结果如下:

如果我们省略了impl One块,我们会得到一个编译错误,抱怨impl OneTwo需要impl One存在。
继承
Rust 提供了一个方便的属性,允许你访问许多常用特质,而无需自己重复实现。它们通过使用#[derive(Trait_Name)]来调用。
可用的特性如下:
-
Clone: 这创建了一个对象的副本 -
Copy: 这创建了一个对象的副本 -
Debug: 这提供了调试代码 -
Default: 这为类型提供了一个有用的默认值 -
Eq:相等性,这与PartialEq类似,但除了结构体内的所有参数 -
Hash: 这是一个可哈希的类型 -
Ord:排序, 这些是在所有类型上形成全序的类型 -
PartialEq:部分相等性,这仅在结构体的子集上进行测试 -
PartialOrd:部分排序, 可以比较以创建排序顺序的值
特质对象
通常,当我们调用 Rust 中的函数时,代码中会有一行类似于以下的内容:
call_some_method(some_value);
当我们在代码中有一个附加了impl的struct时,我们将有如下所示:
let m = MyStruct {a: 3, b: 4, c: 1, d: 4}; m.call_some_method();
这两者都是可以的。
如果你记得,在generic_trait_full示例中,我们定义了Calc,T可以是f32或i32。我们还讨论了应用程序如何知道在最终二进制文件中包含什么。这被称为静态调度(Rust 所偏好的)。
Rust 使用一个称为调度的系统,其中有两种类型:静态(Rust 所偏好的)和动态。动态调度依赖于称为特质对象的东西。
让我们创建一个示例测试设置
测试代码非常简单。我们有一个具有返回String的函数的特质。然后我们有几个实现和一个参数限制函数,该函数将显示实现的结果:
trait StaticObject
{
fn static_method(&self) -> String;
}
impl StaticObject for u8
{
fn static_method(&self) -> String {format!("u8 : {}, ", *self)}
}
impl StaticObject for String
{
fn static_method(&self) -> String {format!("string : {}", *self)}
}
fn display_code<T: StaticObject>(data : T)
{
println!("{}", data.static_method());
}
fn main()
{
let test_one = 8u8;
let test_two = "Some text".to_string();
display_code(test_one);
display_code(test_two);
}
这一部分的代码可以在09/trait_object_static中找到。
编译并执行后,我们得到以下结果:

从前面的解释中,我们知道编译器将生成 T 可以是的各种类型。
让我们看看动态分派
动态分派使用特征对象。特征对象可以存储任何实现了 trait 的类型的值。值的实际类型仅在运行时才知道。
该部分的代码可以在 09/dynamic_dispatch 中找到。
让我们看看一些代码来解释这是如何工作的。
之前,我们为 display_code 有以下内容:
fn display_code<T: StaticObject>(data: T)
{
println!("{}", data.static_method());
}
我们现在有这个:
fn display_code(data : &DynamicObject)
{
println!("{}", data.dynamic_method());
}
我们不再有 T 参数。
在静态版本中,display_code 被调用如下:
display_code(test_one);
对于动态版本,我们使用以下内容:
display_code(&test_one as &DynamicObject);
特征对象是从指针(&DynamicObject)中获得的,该指针通过使用强制转换(&test_one as &DynamicObject)实现了特征。也可以使用 display_code(&test_one)。这被称为强制转换:&test_one 被用作一个接受 &DynamicObject 参数的函数的参数。
动态分派的唯一问题是它可能较慢,因为每次代码运行时,运行时都会忘记指针的类型,并必须为不同类型创建一个新的实现。
保持对象安全
我们不能使用所有特征来创建特征对象。以下是一个例子:
let my_vec = vec![1,3,5,7,9];
let dupe = &my_vec as &Clone;
这将无法编译,因为 Clone 不是对象安全的,因为 Clone 包含 Self: Sized,而特征不能有。
如果特征不需要 Self: Sized 并且所有方法都是对象安全的,则它是一个对象安全特征。为了使方法对象安全,它必须要求 Self: Sized。如果方法不需要 Self: Sized,如果方法不需要任何参数并且不使用 Self,则即使方法不需要 Self: Sized,它仍然可以是对象安全的。
摘要
特征和泛型是开发的关键特性,Rust 在这些方面功能丰富。我们看到了如何创建实现,如何使用泛型,如何确保类型可以被绑定,以及特征的力量。希望你现在应该能够欣赏到泛型为开发者提供的纯粹的力量,在灵活性方面。泛型还允许通过本质上消除对泛型代表的过度担忧来减少我们(作为开发者)需要编写的代码量。
在下一章中,我们将探讨通过使用外部库(称为 crate)来扩展我们的 Rust 应用程序。
第十章:创建您的自己的箱子
大多数语言都允许创建外部库。这些库通常包含通用代码片段,用于通用用途。例如,用于反序列化 JSON 的库相当常见,数学库也是如此。Rust 也不例外。它允许创建库(称为箱子)。这些箱子可以保留给自己或以您认为合适的方式分发。箱子的元数据存储在公共服务上,在 crates.io/。
在本章中,我们将涵盖以下主题:
-
箱子的创建方式
-
目录结构的使用方式
-
箱子由模块组成的方式
-
如何在您的代码中包含您的箱子
-
如何使用模块的范围
箱子究竟是什么?
与所有语言一样,Rust 可以使用外部库,我们已知这些库被称为箱子。但它们是什么?
如果我们考虑一个箱子,我们会想到用来存放很多东西的东西。软件开发者喜欢保持他们的代码整洁,如果他们知道自己在做什么,他们往往会保持他们的库相当专业化。箱子内的这些专业化被称为模块。
箱子是一个容器,其中包含一个或多个模块。
查看模块
为了展示箱子是如何组合起来的,我们将创建一个。在这种情况下,它将是一个简单的数学箱子。
在我们考虑这一点之前,让我们考虑一下我们所有人都知道的东西:汽车。我们将汽车视为箱子,因为与汽车有关的一切都包含在其中。
首先,让我们考虑汽车的主要部分:发动机、燃料、内饰、车轮和运动,以及电气部分。
还有更多,但现在我们将忽略它们。让我们用一个方块图来表示,以便更清晰地展示它们之间的关系:

当然,我们可以将这些块分开(例如,我们可以将电气部分分为点火、音频、窗户、加热挡风玻璃、灯光和内饰风扇)。
汽车是箱子。每个块是一个模块。每个分割是一个子模块。现在很容易看出箱子是如何可视化的。
我可以看到这个类比存在一个问题
我选择汽车是有原因的。如果我们想想,所有的部件并不是真的那么离散;发动机需要燃料,电气部分需要发动机,但发动机也产生电力,等等。从编程的角度来看,这会导致一团糟。
我们如何将它们分开?
答案是,我们为每个模块使用一个范围。例如,这个箱子的顶层是Car。然后我们添加::,后面跟着模块名称(Car::Engine、Car::Fuel等等)。如果一个模块需要访问另一个模块,可以使用通常的use指令来包含它。
箱子的名称是在使用 cargo 创建库时使用的名称。在这个例子中,创建此箱子的命令行如下:
cargo new Car
注意,我们不使用--bin标志。
考虑以下示例:
// in Car::Engine
use Fuel;
use Electrics;
如果我们进一步分解模块,我们将以与之前相同的方式扩展作用域以便访问它们:
// in the main code
use Car::Interior::Audio;
use Car::Interior::Windows::HeatedRear;
回到我们的数学库
现在我们知道了 crates 和模块是如何结合在一起的,以及它们的作用域如何允许模块在具有相同名称的情况下(例如,Car::Engine和Car::Electics都可以有一个名为voltage_to_earth的函数,每个都执行不同的操作)不会混淆,让我们考虑我们的数学库。
该库将包含四个模块和多个子模块:
-
三角学:
-
正弦/余弦/正切
-
反正弦、反余弦和反正切
-
-
回归分析:
-
直线上的截距
-
标准差和 r²值
-
-
转换:
- 温度、压力和体积
-
基本函数:
-
n进制到十进制转换
-
十进制到n进制转换
-
m进制到n进制加、减、乘、除
-
使用有用的名称
模块内的命名非常重要;它将可见于任何使用它的人,因此应该描述它所做的工作。函数名称也是如此。例如,f_to_c是可以的,但库的整个目的就是你可以得到你想要的东西,而无需猜测作者的意图。一个名为fahrenheit_to_celcius的函数名称更有意义。
这同样适用于模块。如果我要用ra进行回归分析,这个名字可能看起来合理,但它是否清晰?这里的名字可以代表任何东西。创建名为regression_analysis的模块可能看起来很费力,但它将帮助其他用户了解可以期待什么。
让我们开始创建!
首先,我们需要创建 crate 本身。
要做到这一点,我们不需要写以下内容,而是需要告诉 cargo 我们正在创建一个库:
cargo new myapp -bin
要做到这一点,我们只需省略-bin标志:
cargo new MathLib
以下截图显示了这一点,随后是模块的树结构。你会注意到main.rs已被替换为lib.rs:

图 1
创建顶级模块
要创建一个模块,我们首先需要告诉编译器代码存储在模块中。在这个例子中,我将使用Trigonometry模块:
mod Trigonometry // top level module
{
mod Natural // sub module
{
}
mod Arc // sub module
{
}
}
当我们使用cargo build(而不是cargo run;库中没有main函数)编译并检查树结构时,我们会看到库(高亮显示):

图 2
这个部分的架构可以在第十章/数学库结构中找到。
目前我们无法用它做太多,因为它只包含一些几乎不起作用的占位符。在继续之前,先看看lib.rs源文件。除了模块名称外,它已经有 62 行。让我们为Conversion模块想一个非常简单的例子,华氏度转摄氏度。
做这件事的公式是(F - 32) * 5/9。因此,我们的函数将是以下内容:
pub fn fahrenheit_to_celcius(a: f32) -> f32
{
(a - 32f32) * 5f32 / 9f32
}
这只是四行代码。我们还需要从 C 到 F、从 K 到 C、从 C 到 K、从 F 到 K 以及从 K 到 F(K 代表开尔文,表示绝对温度,即 0K = -273.15^oC,也称为绝对零度)的转换。包括这些将使代码行数达到大约 24 行。这是一个简单的模块。回归分析的模块代码行数大约有 100 行。
我们的目标源文件将会非常大。由于我们希望保持模块的可管理性,我们需要将lib.rs文件分解一下。
多文件模块
为了分解我们当前的lib.rs文件,我们需要改变声明模块的方式。
本节的内容源文件位于Chapter10/MathsLibMultiFile和Chapter10/MathsLibMultiFileDirs。
目前,我们有以下内容:
mod Trigonometry // top level module
{
mod Natural // sub module
{
}
mod Arc // sub module
{
}
}
为了将其分解为单独的文件,我们只需要在lib.rs中声明顶级模块:
mod Trigonometry;
mod RegressionAnalysis;
mod Conversions;
mod Bases;
子模块怎么办?
当我们以这种方式声明顶级模块时,Rust 会期望每个模块都有一个目录,或者有四个源文件(Trigonometry.rs、RegressionAnalysis.rs、Conversions.rs和Bases.rs)。如果使用目录结构,Rust 会期望每个目录中都有一个名为mod.rs的文件。
让我们比较这两个系统的外观,然后我们可以检查每个系统的相对优势。MathsLibMultiFile的结构将如下所示:

图 3a
MathsLibMultiFileDirs的结构将如下所示:

图 3b
初看之下,它们似乎非常相似;唯一的区别是多文件目录(图 3b)将模块分解为单独的文件,而多文件(图 3a)每个模块只有一个文件。这是非目录结构的限制;子模块被保存在一个文件中,这对于非常小的模块来说是可以的,但对于较大的模块来说就不太好了。
在目录结构版本中,有一个mod.rs文件。这个文件完全是空的,但它的存在是为了让编译器知道我们有了子模块。如果(比如说)RegressionAnalysis::Statistics需要进一步分解,那么它将是在RegressionAnalysis目录内创建一个新的名为Statistics的目录(目录必须与模块同名),并添加一个新的mod.rs文件以及新的子模块。
mod.rs文件
此文件应包含对模块本身的接口。模块的名称将指向具有相同名称的文件。
考虑以下示例:
mod mycode;
前一行将指向mycode.rs。您需要在该模块目录中的每个文件中包含接口(mod.rs除外)。
让我们添加一些代码
我们现在已经建立了结构并拥有了基本框架;我们可以开始向库中添加一些代码。在这种情况下,将是Conversions::Temperature部分。我们已经看到了华氏到摄氏的温度转换函数,所以让我们添加其他函数:
// Temperature.rs
mod Temperature
{
fn fahrenheit_to_celcius(f: f32) -> f32
{
(f - 32f32) * 5f32/9f32
}
fn celcius_to_fahrenheit(f: f32) -> f32
{
(c * (9f32/5f32)) + 32f32
}
fn celcius_to_kelvin(c: f32) -> f32
{
c + 273.15
}
fn kelvin_to_celcius(k: f32) -> f32
{
k - 273.15;
}
fn fahrenheit_to_kelvin(f: f32) -> f32
{
(f + 459.67) * 5f32 / 9f32
}
fn kelvin_to_fahrenheit(k: f32) -> f32
{
(k * (9f32 / 5f32)) - 459.67
}
}
这段代码并没有什么惊人的地方,但我们确实需要停下来思考一下。开尔文温度从 0 到n;它永远不会低于零。用户完全可能想要使用celcius_to_kelvin并传入-274。这意味着函数的答案在数学上是正确的,但在物理上是不正确的。
这个部分的代码在Chapter10/MathsLib。
我们可以返回-1,但对于某些函数,这个答案是可以接受的。
我们需要返回的是一个元组,第一个参数是一个布尔值,表示计算是否有效(true = 有效)。如果是true,答案在第二个参数中;否则,返回原始传入的值。
作为快速测试,以下代码可以运行:
有关源代码,请参阅Chapter10/QuickTest。
fn kelvin_to_celcius(k: f32) -> (bool, f32)
{
if k < 0f32
{
return (false, k);
}
else
{
return (true, k - 273.15);
}
}
fn main()
{
let mut calc = kelvin_to_celcius(14.5);
match calc.0
{
true => println!("14.5K = {}C", calc.1),
_ => println!("equation was invalid"),
}
calc = kelvin_to_celcius(-4f32);
match calc.0
{
true => println!("-4K = {}C", calc.1),
_ => println!("invalid K"),
}
}
在这里使用元组的索引形式而不是将其解构为两个变量是很方便的。
编译时,我们得到以下输出:

图 4
这正是预期的结果。这也表明需要在库中添加一组单元测试,以确定输入数据的有效性(或无效性)。
小心双重名称作用域
创建一个可能遇到双重名称作用域问题的 crate 是一个相当常见的问题。考虑以下示例:
mathslib::conversions::temperature::temperature
将前面的行替换为后面的行会导致一个主要问题:
mathslib::conversions::temperature;
问题出在mod.rs和temperature文件上。
如果你查看lib.rs,它包含必须与目录名称匹配的模块名称,该目录反过来包含mod.rs文件。mod.rs文件(如我们所见)需要包含模块的公共接口。现在,按照这个逻辑,temperature.rs文件中的代码也应该有pub mod temperature { ... }。正是这个最后的pub mod导致了双重名称作用域。
为了避免这个问题,只需省略pub mod temperature行。只要文件名与mod.rs中的pub mod名称匹配,编译器就会认为该代码属于mod.rs中命名的模块。
查看以下代码片段:
// in mod.rs
pub mod temperature;
// all code in temperature.rs "belongs" to mod temperature
fn celcius_to_kelvin(c: f32) -> (bool, f32) { ... }
向库中添加单元测试
我们可以通过两种方式之一创建测试:要么添加一个包含lib.rs文件的tests目录,要么简单地添加一个包含该模块测试的文件。由于我们已经在使用目录结构,让我们继续使用它来编写单元测试。
如前所述,在第一章中,介绍和安装 Rust,为了添加单元测试,我们在代码前添加以下内容:
#[test]
然后为了构建,我们需要做以下操作:
cargo test
然而,当我们这样做的时候,我们遇到了一个问题。我们的单元测试文件看起来是这样的:
extern crate mathslib;
use mathsLib::conversions::temperature;
#[cfg(test)]
mod temperature_tests
{
#[test]
fn test_kelvin_to_celcius_pass()
{
let calc = kelvin_to_celcius(14.5);
assert_eq!(calc.0, true);
}
#[test]
#[should_panic(expected = "assertion failed")]
fn test_kelvin_to_celcius_fail()
{
let calc = kelvin_to_celcius(-4f32);
assert_eq!(calc.0,true);
}
}
表面上看,这似乎应该可行,但它返回的结果有些令人困惑:

图 5
这没有意义;我们知道有一个名为Temperature的模块,那么为什么我们会收到这条消息?答案是,这完全取决于模块和函数的隐私性。
公开某些内容
在第七章中,我们了解到,Rust 默认将所有函数、structs等设置为私有。这是可以的,因为它防止了代码的一些细节暴露给公共接口。
然而,这也意味着我们必须明确地将模块以及我们希望用户可以访问的所有函数设置为pub(公共)。因此,我们的温度转换函数将如下所示:
pub mod Temperature
{
pub fn fahrenheit_to_celcius(f: f32) -> f32
{
(f - 32f32) * 5f32/9f32
}
下次我们运行单元测试时,应该不会出现这个问题,除了以下问题:

图 6
我们确实在Temperature模块中有一个名为kelvin_to_celcius的pub函数。问题是以下这一行:
use mathslib::conversions::temperature;
这所做的只是导入模块,而不是任何符号(函数)。我们可以通过以下四种方式中的任何一种来修复这个问题:
- 我们可以使用以下方法:
use mathslib::conversions::temperature::*;
- 我们使用以下方法:
use mathslib::conversions::temperature::kelvin_to_celcius;
- 我们使用以下方法:
use mathslib::conversions::temperature;然后在kelvin_to_celcius前加上temperature::
- 我们删除了
use mathslib行,并在mod temperature_tests内部添加以下行:
use super::*;
使用这些中的任何一个都应该允许测试编译和运行。您将看到的输出应该类似于以下内容:

图 7: chap10_unittest
让我们快速运行我们的 crate
目前,我们的 crate 还远未完成。然而,其中包含足够的代码来查看它是否真的能运行。
本节代码位于Chapter10/first_run_out。
我们最初的代码如下所示:
extern crate mathslib;
use mathslib::conversions::temperature::*;
fn main()
{
let mut testval = celcius_to_fahrenheit(100f32);
println!("100C = {}F", testval.1); // should be 212
}
当我们构建这个项目时,我们得到以下结果:

图 7
这是有道理的;我们要求代码包含一个它一无所知的库。
外部依赖项
通常情况下,如果一个依赖项在应用程序外部,我们会在Cargo.toml文件中添加如下内容:
[dependencies]
mathslib = "0.1.0"
在这种情况下,我们无法使用 cargo 来构建;相反,我们需要使用rustc来编译。cargo 的工作方式是它会为每个项目重新编译依赖项(没有保证每个项目都会为给定的 crate 使用相同的特征集)。
我们可以使用以下命令模拟 cargo 运行:
rustc -L . src/main.rs && ./main
-L选项将.(根目录,其中包含Cargo.toml文件)中的任何库链接到.之后的源代码。/main部分实际上告诉命令行解释器在根目录(编译文件的名称)中执行名为./main的二进制文件。
执行此操作后,我们可以看到我们的应用程序的全貌:

图 8
我们现在知道,我们的 crate(现状)正在按预期运行。
改变作用域
我们可以使用我们的作用域名称执行的一些更有趣的功能之一是更改它们。我们还可以自定义在用法行上包含哪些模块。
修改 crate 名称
通常,当我们导入一个 crate 时,我们使用以下内容:
extern crate crate_name;
然而,为了避免与代码中的某些内容混淆,你可能希望用不同的名称来引用 crate:
extern crate crate_name as my_crate;
它看起来与铸造非常相似,这是因为它将名称 my_crate 铸造为 crate_name。
当我们现在提到 crate 时,我们不使用以下内容:
use crate_name::module;
我们更愿意使用以下内容:
use my_crate::module;
优化你的用法语句
Java 做得正确的一件非常少的事情是它在导入库的方式上的粒度程度;它促使开发者只包含应用程序实际需要的库的部分。这归因于 Java 的历史,但这是应该被鼓励的。Rust 也做了类似的事情。
use 语句可以采用多种不同的样式。
使用所有内容的做法
这具有以下形式:
use my_crate::module_name::*;
这通常被称为大锤做法,因为它使得 module_name 范围内的所有符号(公共函数、特性和等等)都可用。这种做法没有问题,但最终会导致更大的二进制文件(这可能会减慢最终应用程序的速度,并且运行代码时肯定会需要更多的内存)。
你决定的做法
这是使用 module_name 范围所需的最小内容:
use my_crate::module_name;
在这里,你正在告诉编译器 module_name 存在,并且只要函数名称存在于符号中,它就可以被使用。然而,为了使用 module_name,函数需要以 module_name 开头。例如,要使用存在于 module_name 中的 print_me(f32) 函数,你将会有以下内容:
let some_text = module_name::print_me(10.1f32);
module_name:: 必须添加到编译器,以便使用 module_name 范围而不是应用程序的当前作用域。
使用我的做法
在这里,我们告诉编译器我们只允许当前作用域使用 module_name 范围内的特定函数:
use my_crate::module_name::print_me;
使用我但叫其他名字的做法
这与通过另一个名称引用 crate 非常相似:
use my_crate::module_name as mod_name;
这并不意味着你可能认为的意思。在 crate 示例中,我们说我们将使用 my_crate,这是 crate_name 的铸造。在这种情况下,我们说的是 mod_name 是 my_crate::module_name 的铸造。
在上一行之后,让我们使用以下内容:
use my_crate::module_name;
let foo = module_name::print_me(10f32);
如果我们这样做,我们现在使用以下内容:
let foo = mod_name::print_me(10f32);
它看起来一样,但实际上意味着以下内容:
let foo = my_crate::module_name::print_me(10f32);
使用全局匹配的做法
这种方法与使用我的方法类似,但有一个例外,即我们在想要代码可以访问的内容周围使用 {}(称为 glob):
use my_crate::module_name::{print_me, calculate_time};
这一行意味着代码可以访问 module_name::print_me 和 module_name::calculate_time,但不能访问 module_name 范围内的其他内容。
使用全局匹配自我做法
在这里,glob 的第一个参数是self。在这个上下文中,self指的是根上下文:
use my_crate::module_name::{self, print as my_print, calculate as my_calc};
以扩展形式,这相当于以下内容:
use my_crate::module_name;
use my_crate::module_name::print as my_print;
use my_crate::module_name::calculate as my_calc;
摘要
在本章中,我们涵盖了大量的内容,并看到,在大多数情况下,货物使得构建 Rust 应用程序变得简单。当在最初创建项目之外测试自己的 crate 时,我们需要使用rustc来编译。我们看到了如何创建自己的库,如何添加单元测试,如何有效地利用 use 语句,以及如何用不同的名称调用 crate 和作用域。
在下一章中,我们将探讨如何真正利用 Rust 内置的内存保护系统,以充分利用并发性和并行性。
第十一章:Rust 的并发
在过去 35 年左右的时间里,计算机已经取得了长足的进步。最初,我们有 6502、6809 和 Z80 处理器。这些被称为单处理单元;它们一次只能运行一个程序,软件以线性方式运行(这意味着同时执行两个任务是不可能的)。
处理器不断发展,我们从单个处理单元(单核)过渡到了包含多个处理单元的处理器(多核)。编程语言也进化以支持这种处理器,同时运行多个操作(线程)成为现实。
Rust 作为一种非常现代的语言,也具有多处理的能力。所有你期望从 Rust 中获得的好处(例如内存安全和避免竞态条件)都可用,但还有一些其他的事情你需要注意。
在本章中,我们将:
-
理解 Rust 执行并发过程的方式
-
学习如何使用线程
-
看看不同线程模型之间的区别
一点故事
神秘移动设备正走在一条非常黑暗的道路上。无法知道前方是什么。在某个时刻,他们来到了一个道路交叉口,有三条道路从这里分叉。每条道路上都有一个写着出口的标志。作为勇敢的类型,弗雷迪让维拉走了一条路,夏吉和史酷比走第二条,达芬妮走第三条。作为勇敢的人,弗雷迪会开车走这条路。他们知道,尽管如此,这些道路最终会回到主路上。
他们同意,第一个到达出口的人会给其他人发消息。他们同步了手表,然后出发,不知道谁会第一个到达出口,甚至不知道是否能够到达出口。
那一切都是关于什么的?
大概在两段文字中,我阐述了 Rust 中并发性的三个非常重要的方面:Send(如神秘移动设备向其他人发送的消息所示)、Sync 和线程(每条道路都贡献一个线程,实际上,我们根本无法知道线程何时会重新连接到发送者,这可能会引起无数问题!)。
让我们逐一处理每个方面。
Send
Send 将类型安全地传输到另一个线程——换句话说,如果类型 T 实现了 Send,那么这意味着 T 已经被安全地传递到另一个线程。
使用 Send 有一些注意事项:
-
你不会用它来处理非线程安全的进程(例如 FFI)
-
Send 必须为该类型实现
Sync
Sync 被认为是超级安全的选项。当 T 实现了 Sync,就有内存安全的保证。然而,在我们继续之前,我们需要考虑以下问题。
何时一个不可变变量不再是不可变的?
到目前为止,我们一直认为变量要么是可变的,要么是不可变的,就是这样。然而,事实并非如此。
考虑以下内容:
let mut a = 10;
let b = &mut a;
这实际上意味着什么?首先,我们创建一个对 a 的可变绑定,最初包含值 10。
接下来,我们创建一个不可变的绑定到 b,它包含对 a 的可变值的引用。
它显然是可变的,定义中有 mut
让我们考虑一个不同的例子:
let vc: Vec<i32> = Vec::new();
let dup = vc.clone();
这个例子并不是表面上看起来那样。当调用克隆特质时,vc 必须更新其引用计数。问题是,vc 不是可变的,但这段代码可以编译并运行。
要了解这一点,我们必须知道借用系统是如何工作的(有关借用的更多信息,请参阅本实例中的第八章,Rust 应用程序生命周期)。借用有两种非常明确的操作模式:
-
对资源的单一(或多个)引用
-
精确一个可变引用
真正地,当我们谈论不可变性时,我们并不是真的在谈论一个变量是否固定,而是在谈论是否安全地拥有该变量的多个引用。在前面的例子中,可变发生在向量结构中,我们从中得到 &T。
由于向量结构不是面向用户的,它被称为外部可变。
内部可变性
相反(内部可变性)可以在本例中找到:
use std::cell::RefCell;
fn main()
{
let x = RefCell::new(42);
let y = x.borrow_mut();
}
在这里,RefCell 在调用 borrow_mut() 时提供 &mut。它工作得很好,但如果在 x 上再次调用 borrow_mut(),将会引发恐慌;你只能有一个可变引用。
回到 sync
为了同步,我们不能有任何使用内部可变性的类型(这还包括一些原始类型)。
当涉及到线程间的共享时,Rust 使用 Arc<T>。这是一个包装类型,如果满足以下条件,则实现 send 和 sync:T 必须同时实现 send 和 sync。RefCell 使用内部可变性,所以 Arc<RefCell<T>> 不会实现 sync,这也意味着不能使用 send——因此 RefCell 不能在线程间传递。
使用 send 和 sync 提供了 Rust 依赖以确保代码在使用线程系统时坚如磐石的安全保证。
Rust 线程入门指南
线程允许多个进程同时执行。以下是一个非常简单的线程程序示例:
use std::thread;
fn main()
{
thread::spawn(||
{
println!("Hello from a thread in your Rust program");
});
}
代码文件可以在 Chapter11/SimpleThreadExample 中找到。
当编译时,你可能期望看到 println! 输出。然而,你得到的是这个:

图 1
为什么 println! 没有显示?
思考线程工作方式的一个简单方法。
线程更容易用图形方式思考(至少我认为是这样)。我们从主线程开始:

图 2
主线程从应用程序的开始到结束。
在主线程的任何点上,我们都可以创建一个新的线程(或如果需要,创建多个线程)。

图 3
这两个新线程可以执行应用程序需要的任何操作。但是有一个简单的规则:线程只能持续到应用程序结束。正如图 3所示,线程开始并继续它们愉快的旅程;没有任何规定说线程必须重新连接到主线程,也没有任何规则说明线程何时返回(这可能导致一些非常大的线程安全问题,导致恐慌)。
不言而喻,每个线程也可以生成它们自己的线程来执行子进程:

图 4
如果你习惯了 C、C++和 C#中的线程,你将已经知道线程可以在任何时候返回到主线程,并且这个任何时候可能会对应用程序的安全运行造成灾难。在 Rust 中则不同。
当 Rust 中的线程从主线程(或任何子线程)生成时,会创建一个句柄。然后 Rust 使用这个令牌在给定点检索线程;因此,竞态条件的问题(其中一个线程在另一个线程之前返回,导致崩溃)基本上被消除了。
线程连接
要检索生成的线程,Rust 使用join()特性和然后解包结果。

图 5
因此,为了使我们的小型示例应用程序输出,我们需要将生成的线程连接回主线程:
use std::thread;
fn main()
{
let threadhandle = thread::spawn(||
{
"Hello from a thread in your Rust program"
});
println!("{}", threadhandle.join().unwrap());
}
修改的代码可以在Chapter11/joined_thread中找到。
当我们运行代码时,这次我们看到以下内容:

图 6
嘿,那代码并不相同!这是真的,这是由于 spawn 接受了一个闭包(||)。
闭包
闭包是许多语言中存在的一种强大的代码片段。本质上,闭包将当前代码范围内使用的代码或变量封装在一个整洁的小包中。
在其最简单的形式中,我们可以有类似以下的内容:
let add = |x : i32 | x + t;
| |内部定义了一个名为x的变量,它只在计算的范围内使用,并且其类型为i32。
好吧,这可能看起来并不那么有用——毕竟,我们在这里所做的只是将两个数字相加。但是等等——如果x只在计算的范围内定义,那么x实际上等于多少呢?
这就是闭包发挥作用的地方。通常,当我们创建一个绑定时,我们创建一个绑定到某个确定的东西。在这里,我们正在创建一个绑定,但是将其绑定到闭包的内容。管道| |之间的是参数,表达式是管道结束之后的内容。
如果你这么想,你实际上创建的更接近以下内容:
fn add(x : i32) -> i32
{
x + x
}
对于我们的问题“x实际上等于多少?”答案是它等于唯一的已知参数t。因此,x + t与说t + t相同。添加变量并没有直接绑定(即,与我们正常情况下绑定的方式相同),而是借用绑定。这意味着我们必须应用之前的相同借用规则。比如说我们有以下内容:
let m = &mut t;
这将给出以下错误:

图 7
你将在第十一章/close_mut_error中找到一个这个错误的例子。
抛回的重要部分是我们试图借用在不可变行中被借用的东西。我们可以通过改变闭包的作用域来修复这个问题,如下所示:
let mut t = 10i32;
{
let add = |x : i32 | x + t;
}
let m = &mut t;
这将导致错误发生。
考虑到这一点,我们可以开始扩展这个概念。如果管道之间的值是参数,那么我们可以清楚地用闭包做一些有趣的事情
本部分代码可在第十一章/closures中找到。
以此代码为例:
let calc = |x|
{
let mut result: i32 = x;
result *= 4;
result += 2;
result -= 1;
result
};
而不是创建一个全新的函数,我们使用闭包并在{}的作用域内内联创建函数,只存在result和x。
没有任何参数的闭包是以下内容的内联等价物:
fn do_something() -> T { ... }
闭包并不像它们最初看起来那样简单
闭包被称为语法糖(它们有效地为它们覆盖的基础特性增添了甜味)。这使得 Rust 中的闭包与其他语言中的闭包不同。
在这个前提下,我们也可以将闭包作为参数使用,以及从函数中返回它们。
闭包作为函数参数
考虑以下代码:
fn call_with_three<F>(some_closure: F) -> i32 where F : Fn(i32) -> i32
{
some_closure(3)
}
fn main()
{
let answer = call_with_three(|x| x + 10 );
println!("{}", answer);
}
本节代码可在第十一章/close_fn_args中找到。
我们调用call_with_three并传递闭包作为参数。函数call_with_three接受一个类型为F的参数。到目前为止,它与任何其他接受泛型值作为参数的函数没有区别。然而,我们将F绑定为一个返回类型为i32的函数类型,这使我们创建了一个内联函数作为被调用函数的参数!当代码编译时,我们在屏幕上得到预期的值——13:

图 8
带有显式生命周期的闭包 - 一个特殊情况
如我们在第八章“Rust 应用程序生命周期”中看到的,存在两种主要的作用域类型:全局和局部。具有局部作用域的变量一旦完成其任务就会超出范围,而全局作用域变量则在应用程序终止时清理。全局作用域变量还赋予生命周期标记,'。
闭包也有不同的作用域。通常,如果它们在调用时,它们将只有生命周期,但它们也可以是全局的。
“正常”函数(如之前所示)如下所示:
fn call_with_three<F>(some_closure: F) -> i32 where F : Fn(i32) -> i32
{
some_closure(3)
}
相反,对于生命周期作用域,我们将有以下内容:
fn call_with_three<'a, F>(some_closure: F) -> i32 where F : Fn(&'a 32) -> i32
然而,这不会编译。问题在于作用域。
在我们的第一个例子中,作用域仅限于调用期间。在第二个例子中,它是函数的生命周期(并且这是整个函数的生命周期),这意味着编译器将看到与不可变引用相同生命周期的可变引用。
尽管 Rust 仍然允许我们这样做,但我们需要使用一个叫做高阶特质界限的东西(简单来说,这意味着按照重要性的顺序,这会覆盖下面的东西)。这是通过告诉编译器使用闭包运行的最小生命周期来实现的,这反过来应该会满足借用检查器。在这种情况下,我们使用 for<...>:
fn call_with_three<'a, F>(some_closure: F) -> i32 where F :<for 'a> Fn(&'a 32) -> i32
返回一个闭包
由于 Rust 中的线程使用闭包返回,因此我们考虑返回一个闭包是完全可能的。然而,返回一个闭包并不像你想象的那样简单。
让我们先考虑一个普通函数:
fn add_five(x : i32) -> i32
{
return x + 5;
}
fn main()
{
let test = add_five(5);
println!("{}", test);
}
这将输出值 10。这并不是什么火箭科学。让我们将其改为闭包:
fn add_five_closure() ->(Fn(i32)->i32)
{
let num = 5;
|x| x + num
}
fn main()
{
let test = add_five_closure();
let f = test(5);
println!("{}", f);
}
示例代码可以在 第十一章/return_closure_one 中找到。
然而,当我们运行这个程序时,我们并没有得到预期的 10 答案——相反,我们得到这个:

图 9
那么,出了什么问题?当我们从函数返回时,我们必须告诉编译器我们返回的类型。然而,Fn 是一个特质,所以我们必须以某种方式满足这个要求。我们总是可以使其返回一个引用:
fn add_five_closure() -> &(Fn(i32)->i32)
这将生成另一个编译器错误,因为它需要应用生命周期期望。
我们总是可以让函数返回一个生命周期静态引用:
fn add_five_closure() -> &'static (Fn(i32) → i32)
然而,这将产生一个不同的错误,可能看起来有些令人困惑:

图 10
为什么类型不匹配?它期望一个 i32 类型,但发现了一个闭包。这确实很有道理,但为什么会发生这种情况?
这是因为 Rust 的工作方式。对于一个闭包,它会生成自己的结构体和 Fn(以及任何其他所需的内容)的实现,因此,我们处理的不只是一个字面量,而是其他东西。
尝试返回一个特质对象(如 Box)也不会工作,因为函数依赖于 num 绑定(它是栈分配的)。但是,如果我们从栈移动到堆,我们现在可以返回闭包:
fn add_five_closure() -> Box<(Fn(i32) ->→ i32)>
{
let num = 5;
Box::new(move |x| x + num)
}
fn main()
{
let test = add_five_closure();
let f = test(5);
println!("{}", f);
}
这个示例的源代码可以在 第十一章/return_closure_three 中找到。
现在代码可以编译,并给出以下结果:

图 11
移动参数是什么?
move 参数强制闭包获取其内部包含的所有内容的所有权。让我们更仔细地看看这一点:
let myNum = 10;
let myMove = move |x: i32| x + myNum;
在这里,myMove 获取 myNum 的所有权。myNum 的值实现了 Copy,它被分配给绑定。这和任何变量的操作是一样的,所以必须有某种东西来区分 move 和其他任何东西。
让我们看看一个稍微不同的例子,看看我们是否能看到实际发生的事情:
let mut myMutNum = 10;
{
let mut subNum = |x: i32| num -= x;
subNum(3);
}
我们之前已经见过这个,所以应该不难理解。这将给出答案 7。然而,如果我们使用 move,答案可能并不像预期的那样:
fn main()
{
let mut my_mut_num = 10;
{
let mut sub_num = move |x: i32| my_mut_num -= x;
sub_num(3);
}
println!("{}", my_mut_num);
}
这个示例的代码在 第十一章/move_closure_one 中。
当编译时,你可能期望得到 7 的答案,但结果却是:

图 12
我们如何得到一个值为 10 的值?
在非移动版本中,我们借用可变值的值。使用move,我们接管一个副本的所有权。从实际的角度来看,我们为闭包创建了一个全新的栈帧。sub_num()调用仍在执行,但当它被调用时,返回的值不是预期的值,而是原始值副本的所有权(10)。
回到线程
现在我们已经看到了闭包是如何工作的以及它们的重要性,我们可以继续讨论线程。
如果我们考虑图 5,我们可以使用闭包从子线程中返回一个值:
use std::thread;
fn main()
{
let x = 10;
thread::spawn(|| (println!("x is {}", x); ));
}
这会按原样工作吗?不幸的是,不会。我们正在借用x,由于所有权问题,我们无法这样做。然而,我们可以在调用中添加move:
use std::thread;
fn main()
{
let x = 10;
thread::spawn(move || (println!("x is {}", x); ));
}
线程将接管x的副本的所有权,而不是借用其值。通过接管所有权,Rust 防止了任何形式线程的常见问题:竞态条件。如果你还记得,从本章开始我就说过,传统线程没有保证线程何时返回的保证,这可能会引起各种问题。通常,其他语言使用互斥锁(mutex代表互斥排他,这应该能给你一些它们是如何工作的想法)来尝试防止竞态条件。通过接管所有权,Rust 做了很多防止竞态的事情。
所有权有其优势
在 Rust 的所有权系统中,我们可以很大程度上消除许多其他语言存在的问题,即共享可变状态。其他语言的开发者通常宁愿咬掉自己的腿也不愿处理共享可变状态;它们固有问题——你怎么能共享一个可变值而不产生线程返回的问题呢?
Rust 没有这个问题,因为共享部分是通过所有权系统排序的。
引用计数器
考虑以下代码片段。它不会工作,因为vec有多个所有者:
use std::thread;
use std::time::Duration;
fn main()
{
let mut my_data = vec![5, 8, 13];
for i in 0..10
{
thread::spawn(move || { my_data[0] += i; }); // fails here
}
thread::sleep(Duration::from_millis(50));
}
必须有一种方法可以使这个编译通过,而且确实有。Rust 为我们提供了一个名为Rc的引用计数器。引用计数必须与一个类型相关联,因此它通常被引用为Rc<T>。这可以用来跟踪每个引用。每次我们在执行中克隆时,引用计数都会增加(并创建一个新的所有者引用),因此编译器总是知道何时有东西被返回。
使用Rc<T>的唯一问题是它没有实现send。因此,我们使用Arc<T>(A代表原子——这是 Rust 自己的引用计数,可以在线程间访问)。
然而,Arc<T>也有它自己的问题:默认情况下,内容是不可变的。你可以用Arc<T>共享数据,但共享可变值则是另一回事。可变共享值会导致竞态条件,而这正是我们最不想看到的。
Arc<T>有替代方案(即RefCell<T>和Cell<T>,但这两个都没有实现sync,因此不能用于线程)。
问题解决——使用 Mutex
Rust 为我们提供了Mutex<T>。它与其他语言中的工作方式非常相似,通过锁定线程。我们在代码中实现mutex的方式如下:
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
fn main()
{
let primes = Arc::new(Mutex::new(vec![1,2,3,5,7,9,13,17,19,23]));
for i in 0..10
{
let primes = primes.clone();
thread::spawn(move ||
{
let mut data = primes.lock().unwrap();
data[0] += i;
});
}
thread::sleep(Duration::from_millis(50));
}
本例的代码位于Chapter 11/mutex。
通过使用lock,我们只允许在任何时候只有一个线程访问该数据(它具有互斥性)。其他任何线程都无法访问该值,如果任何其他线程尝试访问该值,它必须等待锁被释放。当数据超出作用域(当i增加时),锁将被释放。
我们为什么要让线程休眠?
通常,使用thread::sleep可以让执行暂停一段时间,作为防止竞态条件的额外保护措施。但这并不总是一个好的计划,因为没有真正的方法可以知道一个线程将花费多长时间,所以这最多只是一个猜测。就像所有的猜测一样,它们可能会非常不准确(在这里使用它是因为我们并没有对数据值做任何事情)。
在实际系统中,为了确保给予正确的时间以确保一切正常工作,通常的方法是使用通道同步线程。
线程同步
最好的方式是将通道视为一个对讲机。一端是发射器(发送),另一端是接收器:
use std::thread;
use std::sync::mpsc;
fn main() {
// tx = transmission = sender
// rx = receiver
let (tx, rx) = mpsc::channel();
for i in 0..10
{
let tx = tx.clone();
thread::spawn(move ||
{
let answer = (i * 2) * i;
tx.send(answer).unwrap();
});
}
for _ in 0..10
{
println!("{}", rx.recv().unwrap());
}
}
本例的代码位于Chapter 11/channels。
当我们运行这个程序时,我们会得到以下结果:

图 13
线程 panic
就像 Rust(以及几乎所有其他语言)中的任何事物一样,事情可能会出错,应用程序会抛出 panic。就像任何其他 panic 发生时一样,我们可以使用 panic!来捕获 panic,然后测试结果以查看线程是否确实 panic 了。我们使用如下构造:
let handle = thread::spawn(move || { panic! ("panic occurred"); });
let res = handle.join();
join()将返回Result<T,E>,然后可以检查是否有异常。
摘要
在本章中,我们已经看到了 Rust 如何处理应用程序内的线程。不要对线程的力量及其固有的问题抱有任何幻想。然而,当正确使用时,线程可以大大提高应用程序的性能。如果你需要进一步的证据,想想网络浏览器;想象一下如果所有操作都在单个线程上执行,一个简单的页面可能需要超过一分钟才能渲染!
我们还探讨了闭包及其背后的力量。将两者结合起来,你可以欣赏到线程和内联函数是多么强大。
我们将在下一章中暂停一下,通过另一个项目任务来检查你的 Rust 技能的进步,该任务将建立在第六章中执行的任务之上,即创建你自己的 Rust 应用程序。在那之后,我们将通过查看标准库和使用外部库来结束本书,通过它们与 Rust 应用程序的接口进一步改进你的 Rust 应用程序。
第十二章:现在轮到你了!
我们正迅速接近本书的结尾,是时候将我们所学的内容付诸实践了。就像 第六章,创建您的 Rust 应用程序 一样,这一章将采取一系列挑战的形式。这一章没有示例代码,所以一切都要靠你自己。大多数挑战将基于 第十章,创建您的自己的 crate 中介绍的 mathlib 库,以及使用 第六章,创建您的 Rust 应用程序 中创建的代码。
任务 1 – 清理代码(第一部分)
如果你考虑 temperature.rs 中的代码示例,你会看到有些使用元组,有些使用单个类型 return。虽然对于开发来说这是一个相当可接受的方法,但对于发布版本,我们可能希望有更结构化的东西。
考虑两个函数 kelvin_to_celcius 和 celcius_to_farenheit;为了使用它们,我们需要有两个变量:
let ktoc = kelvin_to_celcius(14.5f32);
let ctof = celcius_to_fahrenheit(24.3f32);
有多种可能的解决方案。
-
什么也不做!许多库在函数返回不同类型时使用多个变量。
-
在模块内实现一个特例,该特例测试返回值为假时的情况,并返回一个包含答案的
String或 计算失败。 -
定义一个单一的
struct用于形式化的答案,然后将其传递回调用者,如下所示:
pub struct maths_answersMathsAnswers {
calc_complete : bool,
fanswer : f32,
ianswer : i32,
}
如果我们移除第一个选项(毕竟,在一个专门用于测试我们所学内容的章节中这样做有什么意义?),我们就只剩下选项 2 - 4 或 3。
每个选项的问题
每个选项都有其独特的相关问题。
字符串选项
第二个选项的问题在于,如果我们返回一个 String,然后想对答案做其他处理(可能是一个来自其他模块的进一步计算),我们需要一种方法将字符串(在检查它不包含错误代码之后)转换回 f32 以传递给第二个函数。
结构体选项
第三个选项的问题在于,当我们库内调用时,我们要么返回类型为 tuple(bool, f32) 或 f32。因此,在只返回单个类型的函数中,我们需要将 calc_completed 设置为 true。
可以通过派生或实现 std::Default 在 struct 上设置默认值(我们将在 第十三章,标准库,和 第十四章,外部函数接口)中介绍标准库)。这是一个派生版本:
#[derive(Default)]
pub struct MathsAnswers {
calc_complete : bool,
fanswer : f32,
ianswer : i32,
}
Rust 中所有原始类型都有合理的默认值:数字为零,布尔值为假,字符串为空字符串,等等。前面的代码等同于以下手动实现的 Default:
impl Default for MathsAnswers {
fn default () -> MathsAnswers {
MathsAnswers {calc_complete: false, fanswer: 0f32, ianswer: 0i32 }
}
}
然而,我们希望calc_complete的默认值为 true,因此我们将使用此实现:
impl Default for MathsAnswers {
fn default () -> MathsAnswers {
MathsAnswers {calc_complete: false, fanswer: 0f32, ianswer: 0i32 }
}
}
在Default实现之后,我们可以在创建实例时只填充一些值,并为其余部分提供Default::default():
// do calculation then
let answers = MathsAnswers { fanswer: calc_ans, ..Default::default() };
return MathsAnswers;
可能的问题在于将 struct 放在什么范围内。在哪里放置它最好?
任务
您需要决定哪种代码重构选项最适合,然后实现它。您应该创建多个单元测试以确保检查正常工作,然后在您的测试应用中测试以确保 crate 和作用域没有出现问题。
任务 2 - 清理代码(第二部分)
虽然每个函数在 crate 中都是分开的,但我们总是可以清理代码以使其更安全(我们有一个公共函数,并将计算远离好奇的目光)。
任务
每个函数都接受一个参数,该参数为f32或i32类型,幸运的是,我们可以将模块分开,使其返回f32或i32(所有基础返回值都是i32:其他所有答案都在f32中)。
如果我们查看温度模块,所有返回值都将作为f32(在任务 1 之后,如何实现这一点取决于您)。因此,我们可以创建一个单函数,该函数将转换作为第一个参数,值作为第二个参数。
当单个函数识别出第一个参数时,它将调用现在私有的函数并返回值。
与第一个任务一样,您需要实现这一点并为新库创建文档。您应该为 crate 创建一个新的单元测试,并在您的测试应用中测试它。
任务 3 - 扩展 crate(第一部分)
您会注意到,在示例库中,regression_analysis模块根本没有任何代码。这是故意的。
回到第六章,创建您的 Rust 应用程序,其中一个任务是创建代码,使您能够根据提供的公式执行回归分析。现在创建的代码可以明确地分为两部分:
-
直线的方程,y = mx + c,这将给出x和y轴上的截距
-
标准差和回归分析
任务
在这个任务中,您需要将您的代码放入mathslibcrate 中。这可能不像看起来那么简单。库需要接受:
-
存储数据的文件名
-
包含一个
struct或tuple的向量,该向量用于存储数据
然而,问题不在于数据,而在于每次进行计算时,都必须执行整个回归分析。例如,要计算标准差,您不能只传递直线方程的结果——这样不会工作,但会导致整个计算再次执行。
就库的速度而言,这非常低效;你应该计算一次,然后能够从那里提取所有答案。至于你的代码,这需要一些重新组织
完成这些后,你应该为每个函数创建单元测试,并在你的测试应用中用向量以及文件名来测试它们
你需要将此任务的文档添加到你的当前文档中
任务 4 – 扩展 crate(第二部分)
到现在为止,你将牢固掌握 crate 的工作方式、所需的测试规范以及创建测试应用。本节你最后的任务是创建自己的 crate 扩展。不过,你的扩展也有一些标准:
-
其中一个函数必须返回一个非原始类型
-
计算应该是私有的;应该有一种形式来访问函数调用
-
应该有一个函数,它接受一个 XML 文件作为参数来执行计算
-
新模块必须完全文档化,并包含其自身的测试
摘要
我们已经完成了书籍的主要部分。我们已经涵盖了 Rust 语言的大部分内容,这些章节的结尾应该有助于你巩固你的知识
在书的最后一部分,我们将介绍标准库以及如何将你的 Rust 应用程序与外部库接口
第十三章:标准库
与所有编程语言一样,Rust 提供了一个丰富的库,通过提供常用功能来简化开发者的生活,而无需反复重写相同的内容。我们在这本书中已经遇到了标准库的一部分,我毫不怀疑你已经在其他代码示例中看到了它的许多实例。
在接下来的两章中,我们将探讨库提供的内容以及如何使用它。
在本章中,我们将处理标准包(std::)。
章节格式
与本书中的其他章节不同,由于库的规模庞大,本章将略有不同。它看起来像这样:
特型名称
它做什么/提供什么
笔记
提供的特性和结构体/枚举
下载示例
由于 Rust 也有两个主要变体(稳定版和不稳定版),我不会涵盖库中目前被分类为不稳定的内容;没有保证它将保留在库中或保持不变。
标准库是什么?
标准库包含 Rust 的核心功能。它分为四个部分:
-
标准模块
-
原始类型
-
宏
-
预设
标准模块(概述)
标准模块实现了字符串处理、IO、网络和操作系统调用等功能。总共有大约 60 个这样的模块。有些是自包含的,而有些为特性和结构体提供了实现。
模块名称可能与原始类型(如 i32)同名,这可能会引起一些混淆。
原始类型(概述)
原始类型是我们提供的类型。在其他语言中,它们可能是 int、float 和 char。在 Rust 中,我们有 i32、d32 和 i8(分别)。Rust 为开发者提供了 19 个原始类型,其中一些将提供额外的实现。
宏(概述)
宏在 Rust 应用程序开发中起着重要作用;它们被设计用来提供许多非常方便的快捷方式,以避免实现常见功能(如 println!(...) 和 format!(...))的痛苦。Rust 提供了 30 个宏。
预设
预设(Prelude)非常有用。你可能想知道为什么这本书中的许多示例都使用了标准模块,但你很少在源文件的顶部看到 use std::。原因是 Rust 会自动将预设模块注入到每个源文件中,为源文件提供许多核心模块。它按无特定顺序插入以下内容:
-
std::marker::{Copy, Send, Sized, Sync} -
std::ops::{Drop, Fn, FnMut, FnOnce} -
std::mem::drop -
std::boxed::Box -
std::borrow::ToOwned -
std::clone::Clone -
std::cmp::{PartialEq, PartialOrd, Eq, Ord } -
std::convert::{AsRef, AsMut, Into, From} -
std::default::Default -
std::iter::{Iterator, Extend, IntoIterator, DoubleEndedIterator, ExactSizeIterator} -
std::option::Option::{self, Some, None} -
std::result::Result::{self, Ok, Err} -
std::slice::SliceConcatExt
-
std::string::
-
std::vec::Vec
它将 extern crate std; 插入到每个包中,并将 use std::prelude::v1::*; 插入到每个模块中。这就是预览所需的所有内容——就这么简单!然而,每个模块都将依次处理。
标准模块
完成概述后,让我们看看标准模块。
std::Any
此模块通过运行时反射启用 'static 的动态转换。
它可以用来获取一个 TypeId。当用作借用特质引用 (&Any) 时,它可以用来确定值是否是给定类型(使用 Is),也可以用来获取内部值的引用作为类型(使用 downcast_ref)。&mut Any 将允许访问 downcast_mut,它获取内部值的可变引用。&Any 只能用于测试特定类型,不能用来测试类型是否实现了特质。
结构体
TypeId:TypeId是一个无法检查的不可见对象,但允许进行克隆、比较、打印和显示。仅适用于使用'static的类型。
实现
of<T>() -> TypeId where T:’static + Reflect + ?Sized: 返回函数实例化的类型T的TypeId
特质
pub trait Any: 'static + Reflect {fn get_type_id(&self) -> TypeId;}: 模拟动态类型。
特质方法
-
impl Any + 'static-
is<T>(&self) -> bool where T:Any: 如果装箱类型与T相同,则返回true -
downcast_ref<T>(&self) -> Option<&T> where T:Any: 返回ref到装箱值,无论它是否为类型T或None -
downcast_mut<T>(&mut self) -> Option<&mut T> where T:Any: 对于downcast_ref但返回一个可变引用或None
-
-
impl Any + 'static + Send-
is<T>(&self) -> bool where T:Any: 将发送到在类型Any上定义的方法 -
downcast_ref<T>(&self) -> Option<&T> where T:Any: 将发送到在类型Any上定义的方法 -
downcast_mut<T>(&mut self) -> Option<&mut T> where T:Any: 将发送到在类型Any上定义的方法
-
特质实现
-
impl Debug for Any + ‘staticfmt(&self, f: &mut Formatter) -> Result<(), Error>: 使用格式化器格式化值
-
impl Debug for Any + ‘static + Sendfmt(&self, f: &mut Formatter) -> Result<(), Error>: 发送到Debug方法上定义的方法
std::ascii
此模块对 ASCII 字符串执行操作。
AsciiExt 特质包含许多有用的字符串切片实用工具,用于测试,以及转换为大写和小写。
结构体
-
pub struct EscapeDefault: 迭代字节的逃逸版本 -
impl迭代器为EscapeDefaulttype Item = u8: 迭代器遍历的元素类型
-
impl迭代器为EscapeDefault函数-
next(&mut self) -> Option<u8>: 前进迭代器并返回下一个值 -
size_hint(&self) -> (usize, Option<usize>): 返回迭代器剩余长度的界限 -
count(self) -> usize: 返回迭代次数 -
last(self) -> Option<Self::Item>: 返回最后一个元素 -
nth(&mut self, n:usize) -> Option<Self::Item>: 返回第 n 个位置之后的下一个元素 -
chain<U>(self, other:U) -> Chain<Self, U::IntoIterator> where U: IntoIterator<Item=Self::Item>: 取两个迭代器并按顺序创建一个新的迭代器 -
zip<U>(self, other: U) -> Zip<Self, U:IntoIterator> where U:IntoIterator: 取两个迭代器并将它们合并成一个单一的对迭代器 -
map<T,U>(self, u: U) -> Map<Self, U> where U:FnMut(Self::Item) -> T: 从闭包创建迭代器,该闭包对每个元素调用该闭包 -
filter<F>(self, predicate: F) -> Filter<Self, F> where F: FnMut(&Self::Item) -> bool: 创建一个使用闭包确定是否返回元素的迭代器 -
enumerate(self) -> Enumerate<Self>: 提供当前迭代计数和下一个值 -
peekable(self) -> Peekable<Self>: 查看下一个值而不消耗迭代器 -
skip_while<P>(self, predicate:P) -> SkipWhile<Self, P> where P:FnMut(&Self::Item) -> bool: 创建一个基于谓词跳过 n 个元素的迭代器 -
take_while<P>(self, predicate:P) -> TakeWhile<Self, P> where P:FnMut(&Self::Item) -> bool: 创建一个基于谓词的迭代器,它产生元素。 -
skip(self, n: usize) -> Skip<Self>: 跳过前 n 个元素 -
take(self, n: usize) -> Take<Self>: 产生前 n 个元素的迭代器 -
scan<S, T, U>(self, interal_state: S, u: U) -> Scan<Self, S, U> where U:FnMut(&mut S, Self::Item)-> Option<T>: 持有内部状态并产生新迭代器的迭代器适配器 -
flat_map<T, U>(self, u:U) -> Flat_Map<Self, T, U> where U:FnMut(Self::Item) -> T, T:IntoIterator: 创建一个像 map 一样工作的迭代器,但产生一个扁平、嵌套的结构 -
fuse(self)->Fuse(Self): 在遇到第一个None实例后终止的迭代器 -
inspect<T>(self, t: T)->Insepect<Self, T> where T: FnMut(&self::Item)->(): 对每个迭代的元素执行一些操作并将值传递下去 -
by_ref(&mut self) -> &mut Self: 从迭代器中借用而不是消耗它 -
collect<T>(self) -> T where T:FromIterator(Self::Item): 从迭代器创建集合 -
partition<T, U>(self, u:U) -> (T,T) where T:Default + Extend<Self::Item>, U:FnMut(&Self::Item> -> bool: 从迭代器中创建两个集合 -
fold<T, U>(self, init:T, u:U)->T where U:FnMut(T, Self::Item) -> T: 应用函数以产生单个最终结果的迭代器适配器 -
all<T>(&mut self, t:T) -> bool where T:FnMut(Self::Item) -> bool: 测试迭代器中的所有元素是否匹配谓词T -
any<T>(&mut self, t:T) -> bool where T:FnMut(Self::Item) -> bool: 测试迭代器中的任何元素是否匹配谓词T -
find<T>(&mut self, predicate:T) -> Option<Self::Item> where T: FnMut(&Self::Item) -> bool: 在迭代器中搜索与谓词匹配的元素 -
position<T>(&mut self, predicate:T) -> Option<usize> where T:FnMut(Self::Item) -> bool: 在迭代器中搜索与谓词匹配的元素并返回索引 -
rposition<T>(&mut self, predicate:T) -> Option<usize> where T:FnMut(Self::Item) -> bool, Self:ExtractSizeIterator + doubleEndedIterator: 与position类似,但从右侧搜索 -
max(self_ => Option<Self::Item>: 返回迭代器的最大元素 -
min(self_ => Option<Self::Item>: 返回迭代器的最小元素 -
rev(self) -> Rev<Self> where Self:DoubleEndedIterator: 反转迭代器的方向 -
unzip<T, U, FromT, FromU>(self) -> (FromT, FromU) -> Where FromT: Default + Extend<T>, FromU: Default + Extend<U>, Self::Iterator<Item=(T,U)>: 执行 ZIP 的逆操作(从单个迭代器获取两个集合) -
cloned<'a, Y>(self) -> Cloned<Self> where Self:Iterator<Item = &'a T>, T: 'a + Clone: 创建一个迭代器,克隆其所有元素 -
cycle(self) -> Cycle<Self> where Self:Clone: 无限重复迭代器 -
sum<T>(self) -> T where Y:Add<Self::Item, Output=T> + Zero: 返回迭代器元素的累加和 -
Product<T>(self) -> T where T: Mul<Self::Item, Output = T> + One: 将迭代器的元素相乘并返回结果
-
-
impl DoubleEndedIterator for EscapeDefaultnext_back(&mut self) -> Option<u8>: 能够从两端产生结果的迭代器
-
impl ExactSizeIterator for EscapeDefaultLen(&self) -> usize: 返回迭代器迭代的次数
特质
pub trait AsciiExt {
type Owned;
fn is_ascii(&self) -> bool ;
fn to_ascii_uppercase(&self) -> Self:: Owned ;
fn to_ascii_lowercase(&self) -> Self:: Owned ;
fn eq_ignore_ascii_case(&self, other: &Self) -> bool ;
fn make_ascii_uppercase(&mut self);
fn make_ascii_lowercase(&mut self);
}
以下是对字符串切片进行 ASCII 子集操作的扩展方法:
-
关联类型
Owned:复制 ASCII 字符的容器。
-
必要方法
-
is_ascii(&self) -> bool: 值是否为 ASCII 值 -
to_ascii_uppercase(&self) -> Self::Owned: 将字符串复制为 ASCII 大写形式 -
to_ascii_lowercase(&self) -> Self::Owned: 与大写类似,但为小写 -
eq_ignore_ascii_case(&self, other: &Self) -> bool: 两个字符串在不考虑大小写的情况下是否相同
-
std::borrow
此模块用于处理借用数据。
enum Cow`` (写时复制智能指针)
Cow允许对借用数据进行不可变访问(并且可以包含此数据),并在需要修改或拥有时允许懒惰地克隆。它设计为使用Borrow特质。它还实现了Deref,这将允许访问Cow包含的数据上的非修改方法。to_mut将提供对拥有值的可变引用。
-
Trait std::borrow::Borrow: 数据可以通过多种方式借用:共享借用(T和&T)、可变借用(&mut T)以及从Vec<T>(&[T]和&mut[T])等类似类型借用的切片。Borrow特质提供了一个方便的方法来抽象给定类型。例如:T: Borrow<U>意味着从&T借用了&Ufn borrow(&self) -> &Borrowed: 从拥有值不可变借用
-
Trait std::borrow::BorrowMut: 用于可变借用数据fn borrow_mut(&mut self) -> &mut Borrowed: 从拥有值可变借用
-
Trait std::borrow:ToOwned:Clone的借用数据泛化。Clone仅在从&T到T转换时工作。ToOwned将Clone泛化以从任何给定类型的任何借用中构建拥有数据。fn to_owned(&self) -> Self::Owned: 从借用数据创建拥有数据
std::boxed
此模块用于堆分配。
一种非常简单的方法来在堆上分配内存、提供所有权并在作用域之外释放。
-
impl<T> Box<T>fn new(x:T) -> Box<T>: 在堆上分配内存并将x放入其中
-
impl <T> Box<T> where T: ?Sized-
unsafe fn from_raw(raw: *mut T) -> Box<T>: 从原始指针构建 box。创建后,指针由新的Box拥有。这非常不安全;Box析构函数将调用T的析构函数并释放分配的内存。这可能导致双重释放,从而引发崩溃。 -
fn into_raw(b: Box<T> -> *mut T: 消耗 box 并返回包装的原始指针。
-
-
impl Box<Any + 'static>fn downcast<T>(self) -> Result<Box<T>, Box<Any + 'static>> where T:Any: 尝试将 box 降级为具体类型。
-
impl Box<Any + 'static + Send>fn downcast<T>(self) -> Result<Box<T>, Box<Any + ‘static + Send>> where T:Any: 尝试将 box 降级为具体类型
方法
特征实现
-
Impl <T> Default for Box<T> where T:Defaultfn default() -> Box<T>: 返回类型的默认值
-
impl<T> Default for Box<[T]>fn default() -> Box<T>: 返回类型的默认值
-
impl<T> Clone for Box<T> where T:Clone-
fn clone(&self) -> Box<T>: 返回一个包含 box 内容副本的新 box -
fn clone_from(&mut self, source: &Box<T>): 将 sources 的内容复制到 self 中而不创建新的分配
-
-
impl Clone for Box<str>-
fn clone(&self) -> Box<str>: 返回值的副本 -
fn clone_from(&mut self, source: &Self): 从 source 执行复制赋值
-
-
impl<T> PartialEq<Box<T>> for Box<T> where T:PartialEq<T> + ?Sized-
fn eq(&self, other: &Box<T>) -> bool: 测试 self 和 other 是否相等。由==使用 -
fn ne(&self, other: &Box<T>) ->: 测试不等式。由!=使用
-
-
impl<T> PartialOrd<Box<T>> for Box<T> where T:PartialOrd<T> + ?Sized-
fn partial_cmp(&self, other: &Box<T>) -> Option<Ordering>: 如果存在,返回 self 和 other 之间的排序 -
fn lt(&self, other: &Box<T>) -> bool: 测试 self 是否小于 other。由<使用 -
fn le(&self, other: &Box<T>) -> bool: 测试 self 是否小于或等于 other。由<=使用 -
fn ge(&self, other: &Box<T>) -> bool: 测试 self 是否大于或等于 other。由>=使用 -
Fn gt(&self, other: &Box<T>) -> bool: 测试 self 是否大于 other。由>使用
-
-
impl <T> Ord for Box<T> where T:Ord + ?Sizedfn cmp(&self, other: &Box<T>) -> Ordering: 返回 self 和 other 之间的排序
-
impl <T> Hash for Box<T> where T: Hash + ?Sized-
fn hash<H>(&self, state: &mut H) where H: Hasher: 将值输入到状态中,并在需要时更新哈希器 -
fn hash_slice<H>(data: &[Self], state &mut H) where H: Hasher: 将此类型的切片输入到状态中
-
-
impl<T> From<T> for Box<T>fn from(t: T) -> Box<T>: 执行转换
-
impl<T> Display for Box<T> where T: Display + ?Sizedfn fmt(&self, f: &mut Formatter) -> Result<(), Error>: 使用给定的格式化器格式化值
-
impl<T> Debug for Box<T> where T:Debug + ?Sizedfn fmt(&self, f: &mut Formatter) -> Result<(), Error>: 使用给定的格式化器格式化值
-
impl<T> Pointer for Box<T> where T: ?Sizedfn fmt(&self, f: &mut Formatter) -> Result<(), Error>: 使用给定的格式化器格式化值
-
impl<T> Deref for Box<T> where T: ?Sizedfn deref(&self) -> &T: 解引用一个值
-
impl<T> DerefMut for Box<T> where T: ?Sizedfn deref_mut(&mut self) -> &mut T: 可变解引用一个值
-
impl<I> Iterator for Box<I> where I: Iterator + ?Sized-
fn next(&mut self) -> Option<I::Item>: 前进迭代器并返回下一个值 -
fn size_hint(&self) -> (usize, Option<usize>): 返回迭代器剩余长度的界限 -
fn count(self) -> usize: 返回迭代次数 -
fn last(self) -> Option<Self::Item>: 返回最后一个元素 -
fn nth(&mut self, n: usize) -> Option<Self::Item>: 消耗迭代器的n个元素,并返回之后的下一个元素 -
fn chain<U>(self, other: U) -> Chain<Self, U::Iterator> where U: IntoIterator <Item=Self::Item>: 取两个迭代器,并按顺序创建一个新的迭代器 -
fn zip<U>(self, other: U) -> Zip<Self, U::IntoIter> where U: IntoIter: 将两个迭代器合并成一个单一的对 -
fn map<B, F>(self, f: F) -> Map<Self, F> where F: FnMut(Self::Item) -> B: 使用闭包创建一个迭代器,对每个元素调用该闭包 -
fn filter<P>(self, predicate: P) -> Filter<Self, P> where P: FnMut(&Self::Item) -> bool: 使用闭包创建一个迭代器,以判断元素是否应该被产生 -
Fn filter_map<B, F>(self, f: F) -> FilterMap<Self, F> where F: FnMut(Self::Item) -> Option<B<: 创建一个过滤并映射的迭代器 -
fn enumerate(self) -> Enumerate<Self>: 创建一个迭代器,提供当前迭代计数和下一个值 -
fn peekable(self) -> Peekable<Self>: 创建一个迭代器,可以查看迭代器的下一个元素而不消耗它 -
fn skip_while<P>(self, predicate: P)-> SkipWhile<Self, P> where P: FnMut(&Self::Item) -> bool: 创建一个迭代器,根据谓词跳过元素 -
fn skip(self, n: usize) -> Skip<Self>: 创建一个迭代器,跳过前n个元素 -
fn take(self, n:usize) -> Take<Self>: 创建一个迭代器,产生前n个元素 -
fn take_while<P>(self, predicate: P) -> TakeWhile<Self, P> where P: FnMut(&Self::Item) -> bool: 创建一个基于谓词产生元素的迭代器 -
fn scan<St, B, F>(self, init_state: St, f: F) -> Scan<Self, St, F> where F: FnMut(&mut St, Self::Item) -> Option<B>: 类似于fold()的迭代器适配器,持有内部状态并产生一个新的迭代器 -
fn flat_map<U, F>(self f: F) -> FlatMap<Self, U, F> where F: FnMut(&Self::Item) -> U, U: IntoIterator: 创建一个扁平化的嵌套结构。类似于 map。 -
fn fuse(self) -> Fuse<Self>: 创建一个在第一个None实例后结束的迭代器 -
fn inspect<F>(self, f: F) -> Inspect<Self, F> where F: FnMut(&Self::Item) -> (): 对每个元素执行一些操作并将值传递下去 -
fn by_ref(&mut self) -> &mut Self: 借用迭代器。不会消耗它。 -
fn collect<B>(self) -> B where B: FromIterator<Self::Item>: 将迭代器转换为集合 -
fn partition<B, F>(self, f: F) -> (B, B) where B: Default + Extend<Self::Item>, F: FnMut(&Self::Item) -> bool: 消耗迭代器,从中创建两个集合 -
fn fold<B, F>(self, init: B, f: F) -> B where F: FnMut(B, Self::Item) -> B: 应用函数的迭代器适配器,产生一个单一、最终值 -
fn all<F>(&mut self, f: F) -> bool where F: FnMut(&Self::Item) -> bool: 测试迭代器中的每个元素是否匹配谓词 -
fn any<F>(&mut self, f: F) -> bool where F: FnMut(&Self::Item) -> bool: 测试迭代器中的任何元素是否匹配谓词 -
fn find<P>(&mut self, predicate: P) -> Option<Self::Item> where P: FnMut(&Self::Item) -> bool: 搜索满足谓词的迭代器元素 -
fn position<P>(&mut self, predicate: P) -> Option<usize> where P: FnMut(&Self::Item) -> bool: 在迭代器中搜索元素,返回其索引 -
fn rposition<P>(&mut self, predicate: P) -> Option<usize> where P: FnMut(&Self::Item) -> bool, Self: ExactSizeIterator + DoubleEndedIterator: 从右向迭代器中搜索元素,返回其索引 -
fn max(self) -> Option<Self::Item> where Self::Item: Ord: 返回迭代器的最大元素 -
fn min(self) -> Option<Self::Item> where Self::Item: Ord: 返回迭代器的最小元素 -
fn max_by_key<B, F>(self, f: F) -> Option<Self::Item> where B: Ord, F: FnMut(&Self::Item) -> B: 返回指定函数的最大值元素 -
fn min_by_key<B, F>(self, f: F) -> Option<Self::Item> where B: Ord, F: FnMut(&Self::Item) -> B: 返回指定函数的最小值元素 -
fn rev(self) -> Rev<Self> where Self: DoubleEndedIterator: 反转迭代器的方向 -
fn unzip <A, B, FromA, FromB> (self) -> (FromA, FromB) where FromA: Default + Extend<A>, FromB: Default + Extend<B>, Self: Iterator<Item=(A, B)>: 将对偶迭代器转换为两个容器 -
fn cloned<'a, T>(self) -> Cloned<Self> where Self: Iterator<Item=&'a T>, T: 'a + Clone: 创建一个迭代器,它会克隆其所有元素 -
fn cycle(self) -> Cycle<Self> where Self: Clone: 无限重复迭代器 -
fn sum<S>(self) -> S where S: Sum<Self::Item>: 迭代迭代器的元素并求和 -
fn product<P>(self) -> P where P: Product<Self::Item>: 迭代整个迭代器,将所有元素相乘 -
fn cmp<I>(self, other: I) -> Ordering where I: IntoIterator <Item=Self::Item>, Self::Item: Ord: 比较这个迭代器的元素与另一个迭代器的元素 -
fn partial_cmp<I>(self, other: I) -> Option<Ordering> where I: IntoIterator, Self::Item: PartialOrd<I::Item>: 比较这个迭代器的元素与另一个迭代器的元素 -
fn eq<I>(self, other: I) -> bool where I: IntoIterator, Self::Item: PartialEq<I::Item>: 判断这个迭代器的元素是否等于另一个迭代器的元素 -
fn ne<I>(self, other: I) -> bool where I: IntoIterator, Self::Item: PartialEq<I::Item>: 判断这个Iterator的元素是否不等于另一个迭代器的元素 -
fn lt<I>(self, other: I) -> bool where I: IntoIterator, Self::Item: PartialOrd<I::Item>: 判断这个迭代器的元素是否小于另一个迭代器的元素 -
fn le<I>(self, other: I) -> bool where I: IntoIterator, Self::Item: PartialOrd<I::Item>: 判断这个迭代器的元素是否小于或等于另一个迭代器的元素 -
fn gt<I>(self, other: I) -> bool where I: IntoIterator, Self::Item: PartialOrd<I::Item>: 判断这个迭代器的元素是否大于另一个迭代器的元素 -
fn ge<I>(self, other: I) -> bool where I: IntoIterator, Self::Item: PartialOrd<I::Item>: 判断这个迭代器的元素是否大于或等于另一个迭代器的元素
-
-
impl<I> DoubleEndedIterator for Box<I> where I: DoubleEndedIterator + ?Sized: 实现DoubleEndedIteratortrait,使得Box<I>可以双向迭代fn next_back(&mut self) -> Option<I::Item>: 从迭代器的末尾移除并返回一个元素
-
impl <T> ExactSizeIterator for Box<I> where I: ExactSizeIterator + ?Sized: 实现ExactSizeIteratortrait,使得Box<I>可以获取其确切大小fn len(&self) -> usize: 返回迭代器迭代的精确次数
-
impl<T> Clone for Box<[T]> where T:Clone: 实现Clonetrait,使得Box<[T]>可以被克隆-
fn clone(&self) -> Box<[T]>: 返回值的副本 -
fn clone_from(&mut self, source: &Self): 从源执行复制赋值
-
-
impl<T> Borrow<T> for Box<T> where T:?Sized: 实现Borrowtrait,使得Box<T>可以被借用fn borrow(&self) -> &T: 从拥有值中不可变借用
-
impl<T> BorrowMut<T> for Box<T> where T:?Sized: 实现BorrowMuttrait,使得Box<T>可以被可变借用fn borrow_mut(&mut self) -> &mut T: 从拥有值中可变借用
-
impl<T> AsRef<T> for Box<T> where T:?Sized: 实现AsReftrait,使得Box<T>可以被转换为引用fn as_ref(&self) -> &T: 执行转换
-
impl<T> AsMut for Box<T> where T:?Sized: 实现AsMuttrait,使得Box<T>可以被转换为可变引用fn as_mut(&mut self) -> &mut T: 执行转换
-
impl<’a, E: Error + ‘a> From<E> from Box<Error + ‘a>: 实现From<E>trait,将Box<Error + ‘a>从E类型转换而来fn from(err: E) -> Box<Error + 'a>: 执行转换
-
impl From<String> for Box<Error + Send + Sync>: 将String类型转换为Box<Error + Send + Sync>fn from(err: String) -> Box<Error + Send + Sync>: 执行转换
-
impl From<’a, ‘b> From<&’b str> for Box<Error + Send + Sync + ‘a>: 将Box<Error + Send + Sync + ‘a>实现为从&’b str转换的类型fn from(err: &'b str) -> Box<Error + Send + Sync + 'a>: 执行转换
-
impl<T: Error> Error for Box<T>: 将Box<T>实现为Error类型-
fn description(&self) -> &str: 错误的简短描述 -
fn cause(&self) -> Option<&Error>: 此错误的低级原因,如果有
-
-
impl<R: Read + ?Sized> Read for Box<R>: 将Box<R>实现为Read类型-
fn read(&mut self, buf: &mut [u8]) -> Result<usize>: 从指定缓冲区中拉取一些字节到这个源,并返回读取的字节数 -
fn read_to_end(&mut self, buf: &mut Vec<u8>) -> Result<usize>: 读取此源中的所有字节直到 EOF,并将它们放入buf -
fn read_to_string(&mut self, buf: &mut String) -> Result<usize>: 读取此源中的所有字节直到 EOF,并将它们放入buf -
fn read_exact(&mut self, buf: &mut [u8]) -> Result<()>: 读取恰好足够的字节以填充buf -
fn by_ref(&mut self) -> &mut Self where Self: Sized: 为Read实例创建一个引用适配器 -
fn bytes(self) -> Bytes<Self> where Self: Sized: 将此Read实例转换为对其字节的迭代器 -
fn chain<R: Read>(self, next: R) -> Chain<Self, R> where Self: Sized: 创建一个适配器,将此流与另一个流连接起来 -
fn take(self, limit: u64) -> Take<Self> where Self: Sized: 创建一个适配器,最多从它读取 limit 字节
-
-
impl <W: Write + ?Sized> Write for Box<W>: 将Box<W>实现为Write类型-
fn write(&mut self, buf: &[u8]) -> Result<usize>: 将缓冲区写入此对象,并返回写入的字节数 -
fn flush(&mut self) -> Result<()>: 清空此输出流,确保所有中间缓冲的内容都达到目的地 -
fn write_all(&mut self, buf: &[u8]) -> Result<()>: 尝试将整个缓冲区写入此写入器 -
fn write_fmt(&mut self, fmt: Arguments) -> Result<()>: 将格式化字符串写入此写入器,并返回遇到的任何错误 -
fn by_ref(&mut self) -> &mut Self where Self: Sized: 为Write实例创建一个引用适配器
-
-
impl<S: Seek + ?Sized> Seek for Box<S>: 将Box<S>实现为Seek类型fn seek(&mut self, pos: SeekFrom) -> Result<u64>: 在流中定位到指定的偏移量(以字节为单位)
-
impl<B: BufRead + ?Sized> BufRead for Box<B>: 将Box<B>实现为BufRead类型-
fn fill_buf(&mut self) -> Result<&[u8]>: 填充此对象的内部缓冲区,并返回缓冲区内容 -
fn consume(&mut self, amt: usize): 告诉此缓冲区已从缓冲区中消耗了 amt 字节,因此它们不应在调用be_read时返回 -
fn read_until(&mut self, byte: u8, buf: &mut Vec<u8>) -> Result<usize>: 将所有字节读取到buf中,直到遇到分隔字节 -
fn read_line(&mut self, buf: &mut String) -> Result<usize>: 读取所有字节直到遇到换行符(0 x A 字节),并将它们追加到提供的缓冲区 -
fn split(self, byte: u8) -> Split<Self> where Self: Sized: 返回一个迭代器,该迭代器遍历此读取器的内容,按字节分割 -
fn lines(self) -> Lines<Self> where Self: Sized: 返回此读取器的行迭代器
-
std::cell
与共享可变容器一起使用:
关于使用Cells、RefCell以及内部和外部引用的详细信息,请参阅第十一章,Rust 中的并发。第十一章
-
Std::cell::BorrowError: 由RefCell::try_borrow返回 -
impl Display for BorrowErrorfn fmt(&self, f: &mut Formatter) -> Result<(), Error>: 使用给定的格式化器格式化值。
-
impl Debug for BorrowErrorfn fmt(&self, f: &mut Formatter) -> Result<(), Error>: 使用给定的格式化器格式化值。
-
impl Error for BorrowError-
fn description(&self) -> &str: 错误的简短描述 -
fn cause(&self) -> Option<&Error>: 如果有的话,这是此错误的低级原因
-
-
std::cell::BorrowMutError: 由RefCell::try_borrow_mut返回 -
impl Display for BorrowMutErrorfn fmt(&self, f: &mut Formatter) -> Result<(), Error>: 使用给定的格式化器格式化值
-
impl Debug for BorrowMutErrorfn fmt(&self, f: &mut Formatter) -> Result<(), Error>: 使用给定的格式化器格式化值
-
impl Error for BorrowMutError-
fn description(&self) -> &str: 错误的简短描述 -
fn cause(&self) -> Option<&Error>: 如果有的话,这是此错误的低级原因
-
-
std::cell::Cell: 只允许Copy数据的可变内存位置
方法
-
impl<T> Cell<T> where T: Copy-
fn new(value: T) -> Cell<T>: 创建一个包含给定值的新的 Cell -
fn get(&self) -> T: 返回包含的值的副本 -
fn set(&self, value: T): 设置包含的值 -
fn as_ptr(&self) -> *mut T: 返回此单元格中底层数据的原始指针 -
fn get_mut(&mut self) -> &mut T: 返回对底层数据的可变引用
-
特质
-
impl<T> PartialEq<Cell<T>> for Cell<T> where T: Copy + PartialEq<T>-
fn eq(&self, other: &Cell<T>) -> bool: 测试 self 和 other 值是否相等,并由==使用 -
fn ne(&self, other: &Rhs) -> bool: 测试!=
-
-
impl<T> Default for Cell<T> where T: Copy + Defaultfn default() -> Cell<T>: 创建一个Cell<T>,其中T具有Default值
-
impl<T> Clone for Cell<T> where T: Copy-
fn clone(&self) -> Cell<T>: 返回值的副本 -
fn clone_from(&mut self, source: &Self): 从源执行复制赋值
-
-
impl<T> From<T> for Cell<T> where T: Copyfn from(t: T) -> Cell<T>: 执行转换
-
impl<T> Ord for Cell<T> where T: Copy + Ordfn cmp(&self, other: &Cell<T>) -> Ordering: 此方法返回 self 和 other 之间的Ordering
-
impl<T> Debug for Cell<T> where T: Copy + Debugfn fmt(&self, f: &mut Formatter) -> Result<(), Error>: 使用给定的格式化器格式化值
-
impl<T> PartialOrd<Cell<T>> for Cell<T> where T: Copy + PartialOrd<T>-
fn partial_cmp(&self, other: &Cell<T>) -> Option<Ordering>: 如果存在,此方法返回 self 和 other 值之间的排序 -
fn lt(&self, other: &Cell<T>) -> bool: 此方法测试(对于自身和其他)小于,并由<操作符使用 -
fn le(&self, other: &Cell<T>) -> bool: 此方法测试小于或等于(对于自身和其他),并由<=操作符使用 -
fn gt(&self, other: &Cell<T>) -> bool: 此方法测试大于(对于自身和其他),并由>操作符使用 -
fn ge(&self, other: &Cell<T>) -> bool: 此方法测试大于或等于(对于自身和其他),并由>=操作符使用
-
-
Std::cell::Ref: 在RefCell框中封装对值的借用引用
方法
-
impl<'b, T> Ref<'b, T> where T: ?Sized-
fn clone(orig: &Ref<'b, T>) -> Ref<'b, T>: 复制一个Ref。RefCell已经不可变借用,所以这不会失败。 -
fn map<U, F>(orig: Ref<'b, T>, f: F) -> Ref<'b, U>where F: FnOnce(&T) -> &U, U: ?Sized: 为借用数据的组件创建一个新的Ref。RefCell已经不可变借用,所以这不会失败。
-
特质实现
-
impl<'b, T> Debug for Ref<'b, T> where T: Debug + ?Sizedfn fmt(&self, f: &mut Formatter) -> Result<(), Error>: 使用给定的格式化器格式化值
-
impl<'b, T> Deref for Ref<'b, T> where T: ?Sizedfn deref(&self) -> &T: 调用此方法来取消引用一个值
-
Std::cell::RefCell: 具有动态检查借用规则的可变内存位置
方法
-
impl<T> RefCell<T>-
fn new(value: T) -> RefCell<T>: 创建一个包含值的RefCell -
fn into_inner(self) -> T: 消耗RefCell,返回封装的值。
-
-
impl<T> RefCell<T> where T: ?Sized-
fn borrow(&self) -> Ref<T>: 不可变借用封装的值。借用持续到返回的
Ref离开作用域。可以同时取出多个不可变借用。如果值当前正在可变借用,则引发恐慌。 -
fn try_borrow(&self) -> Result<Ref<T>, BorrowError>: 不可变借用封装的值,如果值当前正在可变借用,则返回错误。借用持续到返回的Ref离开作用域。可以同时取出多个不可变借用。 -
fn borrow_mut(&self) -> RefMut<T>: 可变借用封装的值。这个借用在返回的RefMut离开作用域时结束。在这次借用活动期间,不能再次借用该值(会引发恐慌)。 -
fn try_borrow_mut(&self) -> Result<RefMut<T>, BorrowMutError>: 可变借用封装的值,如果值当前正在借用,则返回错误。借用持续到返回的RefMut离开作用域。在这次借用活动期间,值不能被借用。 -
fn as_ptr(&self) -> *mut T: 返回此单元格中底层数据的原始指针。 -
fn get_mut(&mut self) -> &mut T: 返回对底层数据的可变引用。
-
特质实现
-
impl<T> PartialEq<RefCell<T>> for RefCell<T> where T: PartialEq<T> + ?Sized-
fn eq(&self, other: &RefCell<T>) -> bool: 测试自身和其他值是否相等,并由==操作符使用 -
fn ne(&self, other: &Rhs) -> bool: 测试!=
-
-
impl<T> Default for RefCell<T> where T: Defaultfn default() -> RefCell<T>: 创建一个RefCell<T>,具有T的Default值
-
impl<T> Clone for RefCell<T> where T: Clone-
fn clone(&self) -> RefCell<T>: 返回值的副本 -
fn clone_from(&mut self, source: &Self): 从源执行复制赋值
-
-
impl<T> From<T> for RefCell<T>fn from(t: T) -> RefCell<T>: 执行转换
-
impl<T> Ord for RefCell<T> where T: Ord + ?Sizedfn cmp(&self, other: &RefCell<T>) -> Ordering: 返回 self 和 other 之间的Ordering
-
impl<T> Debug for RefCell<T> where T: Debug + ?Sizedfn fmt(&self, f: &mut Formatter) -> Result<(), Error>: 使用给定的格式化程序格式化值
-
impl<T> PartialOrd<RefCell<T>> for RefCell<T> where T: PartialOrd<T> + ?Sized-
fn partial_cmp(&self, other: &RefCell<T>) -> Option<Ordering>: 如果存在,返回 self 和其他值之间的排序 -
fn lt(&self, other: &RefCell<T>) -> bool: 测试小于(对于 self 和 other)并用于<操作符 -
fn le(&self, other: &RefCell<T>) -> bool: 测试小于等于(对于 self 和 other)并用于<=操作符 -
fn gt(&self, other: &RefCell<T>) -> bool: 测试大于(对于 self 和 other)并用于>操作符 -
fn ge(&self, other: &RefCell<T>) -> bool: 测试大于等于(对于 self 和 other)并用于>=操作符
-
代码示例在 第十一章,Rust 的并发。
std::char
此模块用于结构体、特性和枚举字符类型。
-
结构体:
DecodeUtf16、DecodeUtf16Error、EscapeDefault、EscapeUnicode、ToLowercase和ToUpperCase。 -
常量:
Max和Replacement_Character。 -
函数:
decode_utf16、from_digit、from_u32和from_u32_unchecked。
std::clone
这是为了与不能隐式复制的类型一起使用。
更复杂的类型(如字符串)不能隐式复制。这些类型必须使用 Clone 特性和 clone 方法显式地使其可复制。
结构体、特性和枚举:特性 Clone。
std::cmp
此模块提供了对数据进行排序和比较的能力。
此模块定义了 PartialOrd(重载 <、<=、> 和 >=)和 PartialEq 特性(重载 == 和 !=)。
结构体、特性和枚举:枚举 Ordering、特性 Eq(相等比较)、Ord(全序)、PartialEq(部分相等关系)、PartialOrd(可以比较排序顺序的值)。
std::collections
这涵盖了向量、映射、集合和二叉堆。
有四种主要的集合类别,但在大多数情况下应使用 Vec 和 HashMap。
集合类型包括
-
序列(
Vec、VecDeque、LinkedList- 如果你习惯于 C#,这些提供了List<T>的功能) -
映射(
HashMap、BTreeMap。对于 C#用户,这些大致等同于Dictionary<T, U>和Map) -
集合(
HashSet、BTreeSet) -
二叉堆
应该使用哪个集合取决于你想做什么。每个都会根据你所做的事情产生不同的性能影响,尽管通常只有HashMap会产生负面影响。
使用示例:
-
Vec:创建一个可以调整大小的类型为T的集合;可以在末尾添加元素 -
VecDeque:创建一个类型为T的集合,但可以在两端插入元素;需要一个队列或双端队列(deque) -
LinkedList:当需要Vec或VecDeque,以及分割和连接列表时使用 -
HashMap:创建键与值的缓存关联 -
BTreeMap:用于需要最大和最小键值对的键值对
-
二叉堆:存储元素,但在需要时只处理最大或最重要的元素
这些集合各自处理自己的内存管理。这对于集合能够根据需要分配更多空间(并且在其运行的机器的硬件容量限制内)非常重要。
考虑以下情况:我创建了一个没有设置容量的Vec<T>。让T为一个结构体。这种情况并不少见。我向Vec中添加了一些对象,然后每个对象都在堆上分配。堆扩展了,这是可以的。然后我删除了这些对象中的几个。Rust 随后将属于Vec的堆的其他成员重新定位。
如果我使用with_capacity分配空间,那么我们就有了一个最大分配可用,这有助于内存管理。我们可以通过使用shrink_to_fit进一步帮助内存分配,这会减小我们的Vec的大小以适应所需的大小。
迭代器
迭代器非常实用,并且在库中广泛使用。主要来说,迭代器用于 for 循环中。几乎所有的集合都提供了三个迭代器:iter、iter_mut和into_iter。每种迭代器类型执行不同的功能:
-
iter:这提供了一个不可变引用的迭代器,按最适合集合类型的顺序遍历集合的所有内容。 -
iter_mut:这提供了一个与iter相同的顺序的可变引用迭代器。 -
into_iter:将集合转换为迭代器。当集合本身不需要,但其内容需要时非常有用。into_iter迭代器还包括扩展向量的能力。
结构体:BTreeMap、BTreeSet、BinaryHeap、HashMap、HashSet、LinkedList和VecDeque。
std::convert
此模块用于类型之间的转换。
当编写库时,实现From<T>和TryFrom<T>而不是Into<T>和TryInto<T>,因为From提供了更大的灵活性。
-
实现:
As*(引用到引用的转换),Into(在转换中消耗值),From(用于值和引用转换),TryFrom,和TryInto(类似于From和Into,允许失败) -
结构体、特性和枚举: 特质
AsMut,AsRef,From,和Into
std::default
此特质为类型提供有意义的值。
默认为各种原始类型提供默认值。如果使用复杂类型,则需要实现Default。
结构体、特性和枚举: 特质Default。
std:env
此模块用于处理进程环境。
提供了从当前操作系统获取值的一组函数。
结构体、特性和枚举
-
枚举:
VarError(env::var方法的可能错误) -
结构体:
Args(为每个参数生成一个String),ArgOs(为每个参数生成一个OsString),JoinPathsError(当路径无法连接时返回错误),SplitPaths(遍历PathBuf以解析环境变量到平台特定的约定),Vars,以及VarsOS(遍历进程的环境变量快照)
std:error
此模块用于处理错误。
结构体、特性和枚举: 特质Error(所有错误的基功能)
std::f32
此模块用于处理 32 位浮点类型。
此模块提供基本的数学常数:Digits,Epsilon,Infinity,Mantissa_Digits,Max(最大的有限f32值),Max_10_Exp,Max_Exp,Min(最小的有限f32值),Min_10_Exp,Min_Exp,Min_Positive(最小的可能归一化f32值),NAN,Neg_Infinity,和Radix。
std::f64
此模块用于处理 64 位浮点类型。
此模块提供基本的数学常数:Digits,Epsilon,Infinity,Mantissa_Digits,Max(最大的有限f64值),Max_10_Exp,Max_Exp,Min(最小的有限f64值),Min_10_Exp,Min_Exp,Min_Positive(最小的可能归一化f64值),NAN,Neg_Infinity,和Radix。
std:ffi
FFI 是 Rust 与非 Rust 库交互的方法。此特质为此目的提供了一些实用工具。
结构体、特性和枚举: 结构体CStr,CString(分别表示借用的 C 字符串和所有权的 C 兼容字符串),FromBytesWithNullError(从CStr::from_bytes_with_nul返回的错误),IntoStringError(从CString::into_string返回的错误,表示转换期间的 UTF8 错误),NulError(从CString::new返回的错误,指示在提供的向量中找到了空字节),OsStr,和OsString(切片到操作系统字符串)。
std::fmt
此模块用于格式化和输出字符串。
此模块提供了format!宏来处理输出。该宏非常强大且非常灵活,提供了大量的功能。
结构体、特性和枚举
-
结构体:
Arguments(表示格式字符串和参数的安全预编译版本),DebugList,DebugMap,DebugSet,DebugStruct,DebugTuple(帮助实现fmt::Debug),Error(将消息格式化为流时返回的错误类型),以及Formatter(表示输出格式化字符串的位置以及如何格式化它们)。 -
特性:
Binary,Debug,Display,LowerExp,LowerHex,Octal,Pointer,UpperExp,UpperHex和Write(提供将消息格式化为流所需的方法集合)。 -
函数:
format(接受一个预编译的格式字符串和参数,并返回一个格式化后的字符串)和write(接受一个输出流、一个预编译的格式字符串和参数列表。参数将根据指定的格式字符串进行格式化)。
std::fs
此模块在使用文件系统和操作文件时使用。
此模块提供了一组跨平台方法来操作应用程序所在的文件系统。如果可能的话,请避免使用 remove_dir_all 函数。
结构体、特性和枚举
-
结构体:
DirBuilder(用于创建目录),DirEntry(由ReadDir迭代器返回),File(在文件系统上打开文件),FileType(表示文件类型,具有访问每个文件类型的访问器),Metadata(关于文件的信息),OpenOptions(用于配置如何打开文件的选项和标志),Permissions(文件上的文件权限),以及ReadDir(目录内条目的迭代器)。 -
函数:
canonicalize(返回路径的规范形式),copy(复制文件),create_dir,create_dir_all(递归创建目录及其所有父组件,如果缺失),hard_link(在文件系统上创建硬链接),metadata(获取给定路径和文件的元数据),read_dir(返回目录内条目的迭代器),read_link(读取符号链接,返回它指向的文件),remove_dir(删除空目录),remove_dir_all(递归删除路径上的目录——在某些操作系统中这可能会完全删除您的硬盘,所以请小心!),remove_file(删除文件),rename(重命名给定的文件或目录),set_permissions(设置给定文件或目录的权限),以及symlink_metadata(查询不跟随任何符号链接的文件的元数据)。
std::hash
此模块用于提供哈希支持。
此模块确保为给定类型创建哈希的最简单方法是使用 #[derive(Hash)]。
结构体、特性和枚举
-
结构体:
BuildHasherDefault(为所有实现Default的 Hasher 类型实现BuildHasher)和SipHasher(SipHash的实现) -
特性:
BuildHasher,Hash和Hasher
std::i8
此模块定义了 8 位整数类型。
此模块定义了 MAX 和 MIN 常量。
std::i16
此模块定义了 16 位整数类型。
本模块定义了MAX和MIN常量。
std::i32
本模块定义了 32 位整数类型。
本模块定义了MAX和MIN常量。
std::i64
本模块用于处理 64 位整数类型。
本模块定义了MAX和MIN常量。
std::io
本模块提供了核心输入/输出的一系列功能。
本模块不仅为正常控制提供代码Read和Write功能,还为各种流类型(如 TCP 和文件)提供功能。访问可以是顺序的或随机的。IO 行为还取决于应用程序所在的平台,因此强烈建议进行测试。
结构体、特性和枚举
-
结构体:
BufReader(为任何读取器添加缓冲),BufWriter(缓冲写入器的输出),Bytes(读取器值的迭代器),Chain(连接两个读取器),Cursor(包装另一个类型并提供 Seek 实现),Empty(总是位于 EOF 的读取器),Error(IO 操作的错误类型),IntoInnerError(into_inner返回的错误,它结合了错误和缓冲写入器对象,可能可以恢复),LineWriter(包装写入器并将缓冲写入其中),Lines(遍历BufRead的行),Repeat(不断返回字节的读取器),Sink(将数据移动到 null 的写入器),Split(在BufRead内容中分割点的迭代器),Stderr(进程标准错误流的句柄),StdErrLock(Stderr的锁定引用),Stdin(标准输入流),StdinLock(Stdin的锁定引用),Stdout(全局输出流),StdoutLock(Stdout的锁定引用),以及Take(限制从读取器读取的字节数)。 -
枚举:
ErrorKind和SeekFrom。 -
特性:
BufRead(缓冲输入读取),Read(从源读取字节),Seek(提供可以在流中移动的光标),和Write。 -
函数:
copy(将读取器的内容复制到写入器),empty(创建一个空读取器的新句柄),repeat(创建一个不断重复 1 字节的读取器实例),sink(消耗所有数据的写入器实例),stderr(stderr的新句柄),stdin(stdin的新句柄),和stdout(stdout的新句柄)。
std::isize
本模块用于与指针大小的整数类型一起使用。
本模块定义了MAX和MIN常量。
std::iter
本模块用于迭代。
结构体、特性和枚举
-
结构体:
Chain(将两个迭代器连接起来)、Cloned(克隆底层迭代器)、Cycle(永不结束的迭代器)、Empty(不产生任何内容)、Enumerate(在迭代时产生当前计数和元素)、Filter(使用谓词过滤iter的元素)、FilterMap(使用iter的类型的迭代器,用于过滤和映射)、FlatMap(将每个元素映射到迭代器,产生元素)、Fuse(一旦底层迭代器首次迭代None,则持续产生None)、Inspect(在产生元素之前调用一个带有元素引用的函数)、Map(使用类型映射iter的值)、Once(只产生一个元素)、Peekable(允许使用peek())、Repeat(无限重复一个元素)、Rev(具有反向读取方向的双向迭代器)、Scan(在迭代另一个迭代器时保持状态)、Skip(跳过iter的n个元素)、SkipWhile(在谓词为真时拒绝元素)、Take(只迭代iter的前n个元素)、TakeWhile(在谓词为真时只接受迭代元素),以及Zip(同时迭代两个迭代器)。 -
特性:
DoubleEndedIterator(从两端产生元素)、ExactSizeIterator(已知确切长度)、Extend(使用迭代器的内容扩展集合)、FromIterator(从Iterator转换)、ToIterator(转换为Iterator)、Iterator(处理迭代器的接口)。 -
函数:
empty(产生不产生任何内容的新的迭代器)、once(只产生一个元素的新的迭代器)和repeat(持续重复单个元素的新的迭代器)。
std::marker
此模块提供了原始特性和标记来表示基本类型种类。
结构体、特性和枚举
-
结构体:
PhantomData(允许描述类型T)。 -
特性:
Copy(可复制的类型)、Send(可在线程间传输的类型)、Sized(具有固定大小的类型)和Sync(可在线程间安全共享的类型)。
std::mem
此模块执行内存处理函数。
此模块用于查询大小和对齐类型、初始化以及内存操作。
结构体、特性和枚举
- 函数:
align_of(返回类型的内存对齐方式),align_of_val(指向值val的类型的最小对齐方式),drop(处置),forget(将值留为 void,获取所有权但不运行析构函数),replace(用新值替换mut位置的值,返回旧值但不初始化或复制任何一个),size_of(返回类型的大小,以字节为单位),size_of_val(返回值的大小,以字节为单位),swap(交换两个 mut 位置的值;必须为同一类型),transmute(不安全地将一个类型的值转换为另一个类型),transmute_copy(将src解释为&T,然后读取src而不移动包含的值),uninitialized(绕过 Rust 的内存初始化要求),以及zeroed(创建一个初始化为零的值)。
std:net
此模块提供基本的 TCP/UDP 通信原语。
结构体、特性和枚举
-
结构体:
AddrParseError(解析 IP 或套接字地址时返回的错误),Incoming(TcpListener连接的无穷迭代器),Ipv4Addr(表示 IPv4 地址),Ipv6Addr(表示 IPv6 地址),SocketAddrV4(IPv4 套接字地址),SocketAddrV6(IPv6 套接字地址),TcpListener(表示套接字服务器),TcpStream(表示本地和远程套接字之间的 TCP 流),以及UdpSocket(UDP 套接字)。 -
枚举:
IpAddr(IPv4 或 IPv6 地址),Shutdown(传递给TcpStream的 shutdown 方法的值),以及SocketAddr(网络应用程序的套接字地址)。 -
特性:
ToSocketAddrs(可以转换为或解析一个或多个SocketAddr值的对象)。
std::num
此模块用于处理数字。
此模块提供处理数字的有用类型。
结构体、特性和枚举
-
结构体:
ParseFloatError(解析float时返回的错误),ParseIntError(解析int时返回的错误),以及Wrapping(对T有意包裹的算术运算)。 -
枚举:
FpCategory(浮点数的分类)。
std::os
此模块包含提供对应用程序正在运行的 OS 抽象访问的函数。
此模块包含三个模块:linux(特定于 Linux),raw(当前平台特定的原始 OS 类型),以及unix(实验性扩展)。
std::panic
此模块为标准库中的 panic 提供支持。
结构体、特性和枚举
-
结构体:
AssertUnwindSafe(检查类型是否为 panic 安全),Location(关于 panic 位置的信息),以及PanicInfo(关于 panic 的信息)。 -
特性:
RefUnwindSafe(表示共享 ref 被认为是recovery安全的特性)和UnwindSafe(表示 Rust 中 panic 安全的类型的特性)。 -
函数:
catch_unwind(调用闭包,捕获恢复的原因)、resume_unwind(触发恐慌而不调用恐慌)、set_hook(注册自定义恐慌钩子并替换以前的钩子)和take_hook(注销当前恐慌钩子)。
std::path
此模块以跨平台方式提供对路径的抽象访问,以便进行操作。
提供了两种类型,PathBuf和Path。这些是OsString和OsStr的包装器,允许根据本地平台路径直接在字符串上执行操作。
结构体、特性和枚举
-
结构体:
Components(核心迭代器,提供路径的部分)、Display(用于安全地使用format!()和{}打印路径)、Iter(路径部分的迭代器)、Path(路径切片)、PathBuf(所有者可变路径)、PrefixComponent(Windows 特定的路径前缀)和StripPrefixError(从Path::strip_prefix方法返回的错误,指示前缀在自身中未找到)。 -
枚举:
Component(路径的单个组件)和Prefix(路径前缀 [仅限 Windows])。 -
函数:
is_separator(确定字符是否是允许的路径分隔符之一)。
std::process
此模块用于处理进程。
结构体、特性和枚举
-
结构体:
Child(表示正在运行或已退出的子进程)、ChildStderr(子进程标准错误句柄)、ChildStdin(子进程标准输入句柄)、ChildStdout(子进程标准输出句柄)、Command(作为进程构建器)、ExitStatus(描述进程终止后的结果)、Output(已完成进程的输出)和Stdio(描述对子进程标准 IO 流的处理)。 -
函数:
exit(使用退出代码终止当前进程)。
std::ptr
此模块提供处理原始、不安全指针的访问。
请参阅第五章,记住,记住,以获取更多详细信息。
结构体、特性和枚举
函数: copy(从src复制count * size_of<T>到dest;可以重叠)、copy_nonoverlapping(与copy相同,但不能重叠)、drop_in_place(执行指向的值的析构函数)、null(创建新的空原始指针)、null_mut(创建新的空可变原始指针)、read(从src读取值而不移动它)、read_volatile(从src以非移动方式执行可变读取)、replace(将dest中的值替换为src,返回旧值)、swap(交换两个相同类型的可变位置的值)、write(覆盖内存位置,不读取或丢弃旧值)、write_bytes(在指定的指针上调用memset)和write_volatile(执行具有给定值的内存位置的可变写入)。
std::slice
此模块提供动态大小的放置到连续的 [T] 中。
切片是内存的表示为指针的可变切片(&mut [T])或共享切片(&[T])。它们实现了IntoIter,该类型正在执行IntoIter。
结构体、特性和枚举
-
结构体:
Chunks(每次迭代非重叠切片的size_of<T>元素大小的块),ChunksMut(与Chunks类似,但可变),Iter(不可变迭代器),IterMut(可变迭代器),RSplitN和RSplitNMut(迭代匹配谓词的子切片,限制在给定的分割次数内,并从切片的末尾开始)。Split和SplitMut(分别通过匹配谓词函数或谓词分隔的子切片迭代器),以及SplitN和SplitNMut(迭代匹配谓词函数的子切片),还有Windows(迭代长度为size_of<T>的重叠子切片)。 -
函数:
from_raw_parts(从指针和长度形成切片)和from_raw_parts_mut(与from_raw_parts类似,但返回的切片是可变的)。
std::str
此模块用于 Unicode 字符串切片。
结构体、特性和枚举
-
结构体:
Bytes(字符串字节的迭代器),CharIndices(字符串字符和字节偏移量的迭代器),Chars(字符串字符的迭代器),EncodeUtf16(字符串 UTF16 代码的外部迭代器),Lines(使用lines()创建),MatchIndices(使用match_indices()创建),Matches(使用matches()创建),ParseBoolError(当从字符串传递bool失败时返回的错误),RMatchIndicies(使用rmatch_indicies()创建),RMatches(使用rmatches()创建),RSplit(使用rsplit()创建),RSplitN(使用rsplitn()创建),RSplitTerminator(使用rsplit_terminator()创建),Split(使用split()创建),SplitN(使用splitn()创建),SplitTerminator(使用split_terminator()创建),SplitWhitespace(迭代字符串的非空白子字符串),以及Utf8Error(尝试将u8序列解释为字符串时可能发生的错误)。 -
特性:
FromStr(抽象了从字符串创建类型新实例的想法)。 -
函数:
from_utf8(将字节数组切片转换为字符串切片)和from_utf8_unchecked(与from_utf8类似,但不检查字符串是否包含有效的 UTF8)。
std::string
此模块提供使用 UTF-8 编码的可增长字符串进行字符串处理。
包含 String 类型以及将其转换为 String 的特性和错误类型。
结构体、特性和枚举
-
结构体:
Drain(排空迭代器),FromUtf16Error(从 UTF16 切片转换时可能出现的错误值),以及FromUtf8Error(与FromUtf16Error类似,但针对 UTF8),还有String(UTF8 编码的可增长字符串)。 -
枚举:
ParseError。 -
特性:
ToString(将值转换为字符串)。
std::sync
此模块提供线程同步函数。
这在第十一章第十一章,Rust 中的并发中有介绍。
结构体、特性和枚举
-
结构体:
Arc(原子引用计数包装器),Barrier(使多个线程能够同步某些计算的开始),BarrierWaitResult(线程等待的结果),Condvar(条件变量),Mutex(互斥原语),MutexGuard(作用域锁互斥量;当结构退出作用域时解锁),Once(用于运行一次性全局初始化的同步原语),PoisonError(当需要锁时可能返回的错误),RwLock(读写锁),RWLockReadGuard(用于在丢弃时释放对锁的共享读访问),RWWriteGuard(用于在丢弃时释放对锁的共享写访问),WaitTimeoutResult(用于确定条件变量是否超时的类型),以及Weak(指向Arc的弱指针)。 -
枚举:
TryLockError(在调用try_lock时可能发生的错误)。
请参阅第十一章中的代码示例第十一章,Rust 中的并发。
std::thread
这是主要的线程模块,为 Rust 应用程序提供原生线程。
线程在第十一章第十一章,Rust 中的并发中有介绍。
结构体、特性和枚举
-
结构体:
Builder(提供对新线程的详细控制),JoinHandle(拥有在某个线程上加入的权限),LocalKey(拥有内容的本地存储键),以及Thread(线程句柄)。 -
函数:
current(获取线程调用的句柄),panicking(如果线程由于 panic 而正在回溯),park(除非或直到令牌可用否则阻塞),park_timeout(阻塞一段时间),以及sleep(使当前线程休眠一段时间),spawn(创建新线程,返回JoinHandle),和yield_now(向操作系统调度器放弃时间片)。
请参阅第十一章中的代码示例第十一章,Rust 中的并发。
std::time
这是一个处理时间的模块。
结构体、特性和枚举
结构体:Duration(表示时间跨度),Instant(单调递增时钟的测量),SystemTime(测量系统时钟),以及SystemTimeError(从SystemTime.duration_since()返回的错误)。
std::u8
此模块定义了无符号 8 位整数类型。
此模块定义了MAX和MIN常量。
std::u16
此模块定义了无符号 16 位整数类型。
此模块定义了MAX和MIN常量。
std::u32
此模块定义了无符号 32 位整数类型。
此模块定义了MAX和MIN常量。
std::u64
此模块定义了无符号 64 位整数类型。
此模块定义了MAX和MIN常量。
std::usize
这是指针大小的无符号整数类型。
此模块定义了MAX和MIN常量。
std::vec
此模块定义了具有堆分配内容的可增长数组类型。
这被写成 Vec<T>,值可以通过 push 和 pull 分别添加到(或从)vec 的末尾。
结构体、特性和枚举
结构体:Drain(Vec<T> 的排空迭代器)、IntoIter(从向量中移出的迭代器)和 Vec(连续可增长数组类型)。
摘要
我们已经覆盖了 Rust 标准库的大部分内容。请始终检查在线官方文档 doc.rust-lang.org/std/——它质量极高且始终是最新的!
在下一章和最后一章中,我们将探讨如何通过 Rust 的 外部函数接口(FFI)使用外部库。
第十四章:外部函数接口
由于 Rust 是一种主要设计用于在服务器上工作的语言,而大多数服务器上的库(目前)都不是用 Rust 编写的,因此 Rust 应用程序能够利用用其他语言编写的库是有意义的。在本章中,我们将探讨如何做到这一点。
具体来说,我们将涵盖以下内容:
-
学习我们如何利用其他库
-
理解使用用其他语言编写的代码的陷阱。
-
在尽可能的范围内确保我们的代码保持安全。
就像之前的章节一样,源代码将可供你检查。你还将找到一个用 C 编写的、可在 Windows、macOS 和 Linux 上编译的小型库。这个库并不做什么,但它让你了解系统是如何工作的。其他库(如ImageMagick)也是以完全相同的方式工作的。
让我们开始吧!
介绍我们的简单库
库有三种类型:Windows 上的.dll(动态链接库)、.so(共享对象)和.a——.a和.so通常在 Unix 类型系统(包括 macOS)上找到。
我们的库非常简单;它作为一个计算库——你将值传递给正确的函数,然后返回结果。这不是火箭科学,但足以证明我们要做什么。
当使用外部库时,我们需要使用unsafe指令。Rust 无法控制外部库提供的内容,因此如果我们使用标准代码,编译器将不允许编译。
作为开发者,使用外部库必须谨慎处理。
三步程序
在你的 Rust 应用程序中使用库基本上有三个步骤:
-
包含依赖项。
-
编写使用库的代码。
-
将你的应用程序构建为链接到库。
最困难的是第二个阶段,因为它需要编写代码、回调代码和其他这样的包装器来使用库。
包含依赖项
就像使用Prelude未提供的任何库一样,编译器必须知道库的存在。正如我们在第八章《Rust 应用程序生命周期》中所做的那样,我们通过在Cargo.toml文件中包含,让编译器知道预期一个外部库,如下所示:
[dependency]
libc = "0.2.0"
引号中的是库版本。这很有用,因为它使得编译的 Rust 应用程序只能运行在特定版本的库上,这保证了所需的代码将在库中。缺点是,为了始终确保库可用,编译的二进制文件需要附带该库。在这种情况下(以及大多数外部库的情况),需要添加libc。
我们还需要将以下行添加到将要调用函数的源文件中:
extern crate libc;
创建代码。
这一部分的代码在第十四章/firstexample。
当我们处理来自我们应用程序外部的代码时,我们需要能够告诉编译器类似“嘿,看看,构建这段代码,并留下一个钩子,这个钩子可能存在也可能不存在,可能需要也可能不需要这些参数,但希望它能返回一些东西。”这就像给了拿着你签名支票的骗子一张空白支票,希望他们不会在上面填写并兑现!
在 Rust 中,我们通过使用链接指令并将函数放在extern块中来做到这一点。extern块内的代码调用库中持有的函数。它必须与库中函数的名称相同:
[link(name="mathlib")]
extern
{
fn add_two_int_numbers(a: i32, b: i32) -> i32;
}
然后,可以使用以下方式访问此代码:
fn main()
{
let ans = unsafe { add_two_int_numbers(10,20) };
println!("10 + 20 = {}", ans);
}
链接]的作用是什么?
这是一个指令,告诉编译器代码将要链接到一个名为引号内内容的库。你不需要在引号内使用mathlib.dll、mathlib.so或mathlib.a这样的名称,只需要没有扩展名的名称。
可用的三种不同类型的链接(称为模型,并在名称后的kind参数中定义)包括:动态、静态和框架(尽管后者仅适用于 macOS)。下表总结了它们各自的作用。在大多数情况下,使用的是动态类型。
| 类型 | 示例 | 注意事项 |
|---|---|---|
| 动态 | [link(name="foo")] |
这是默认选项。编译的二进制文件会创建钩子,这些钩子将链接到平台安装的库版本。 |
| 静态 | [link(name="foo", kind="static")] |
这些是.a文件。当应用程序构建时,会创建二进制文件,但平台库文件不需要分发。 |
| 框架 | [link(name="foo", kind="framework")] |
仅限 macOS。这将是一个.dylib文件,其处理方式与动态库相同。 |
那有什么大不了的?这很简单!
虽然表面上使用外部库通过 FFI 并不是什么难事,但它确实带来了一系列问题。为什么即使我们引用库中的已知名称,我们还需要在块上使用unsafe注释?
就像我们一次又一次地看到的那样,与 Rust 相比,编译器为开发者做了很多工作,这在许多其他编译器中是看不到的。它确保线程安全,特定操作可以完成,缓冲区不会溢出,我们不会留下未分配的内存或尝试两次释放内存,以及许多其他确保我们的代码尽可能运行并保持稳定(从可靠性角度)的事情。
不幸的是,对于外部库,编译器所能做的只是期望从链接库中得到一些东西。线程可能会挂起或是不安全;没有保证,如果我传递了 6 和 0 给一个类似的除法函数,返回的将是一个数字,而且几乎任何其他事情都可能出错。
通过使用unsafe,我们向编译器承诺,当它链接代码时,它链接到的将正确绑定。
让我们扩展一下内容
extern 块可以包含 Rust 应用程序使用的库所需的方法(或方法数量)。
每当向 extern 块添加新函数时,测试被包含的函数总是一个好主意。这可以通过单元测试或通过将函数添加到 extern 块并在 main 中调用该函数来实现。
我们也可以有多个包含库函数的 Rust 源文件。
例如,在 Source1.rs 文件中进行修改:
//Source1.rs
[link(name="mylib")]
extern
{
fn some_method(a: f32) → f32;
fn some_other_method(a: i32, b: f32, c: f64) → f64;
}
现在,在 Source2.rs 文件中进行修改:
[link(name="mylib")]
extern
{
fn some_other_method(a: i32, b: f32, c: f64) → f64;
fn some_text_method() → String;
}
只要包含链接行,这就不会引起问题。
如果类型不匹配会发生什么?
当你在 32 位平台上构建库时,不能保证 int 的大小与 64 位平台上的 int 的大小相同。它们通常会是相同的,但并不能保证。一个简单的例子如下:
sizeof(char) == 1
sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
因此,短整型可以与长整型大小相同!然而,更常见的情况是,int 将是平台字大小(在 32 位处理器上为 32 位,在 64 位处理器上为 64 位)。
浮点数的值更为严格,并符合 IEEE 754 标准。
如果 Rust 应用程序是在 64 位平台上构建的,而库是 32 位的,通常不会有问题。然而,如果情况相反,则可能发生溢出的情况。这种情况不太可能发生,但值得注意。
我们能否使事情更安全?
我们可以采取一种策略来尝试使事情稍微安全一些。
考虑我们的原始 extern 代码:
[link(name="mathlib")]
extern
{
fn add_two_int_numbers(a: i32, b: i32) -> i32;
}
这段代码正在调用原始 C API,正如讨论的那样,任何对此的调用都必须标记为 unsafe。这是不安全的,因为调用被认为是 低级 的。
在编程语言方面,语言越低级,它就越接近处理器理解的语言(汇编器被认为是实际有用的最低级语言,除非直接将原始二进制代码插入内存位置)。在这里,我们正在以最低级别暴露库。
为了使调用更安全,我们使用了一种称为 包装 的方法。
包装器
当使用为其他语言设计的库时,包装器非常常见。它们通过暴露一个高级函数名来隐藏下面的真正方法。这个暴露的函数名通常被称为库接口 API。通过仅暴露高级函数名,Rust 能够将不安全的部分与其他部分隔离开来。
一个实际例子
库中的一个方法接受一个 int 值的向量来执行平均值、中位数和众数计算,然后返回一个包含这些值的 float 值数组。然而,我们需要首先验证这些值(本质上,测试数组不为空)并且有五个或更多的值。这将返回一个布尔值。
代码的不安全版本将是:
[link(name="mathlib")]
extern
{
fn check_mean_mode_median(a: Vec<i32>) -> bool;
}
我们可以非常简单地为此创建一个包装器:
pub fn calc_mean_mode_median_check(a:Vec<int32>) -> bool
{
unsafe
{
check_mean_mode_median(a) == 0;
}
}
我们将安全函数暴露给代码,并隐藏(包装)不安全的部分。一旦我们返回一个 true 值,我们就知道数据可以进行计算。
现在,这是一段相当无意义的代码(它只是一个测试,以确保我们在向量中有正确的参数数量)。让我们修改这个包装器,使其返回一个Vec<f32>,它将包含-999f、-999f或-999f,如果检查失败,或者包含向量的平均值、中位数和众数。
然而,问题是原始库是用 C 编写的,所以我们需要将结果作为数组获取,然后将其放入向量中。
第一部分是进行第一次检查:
pub fn mean_mode_median(a: Vec<int32>) -> Vec<f32>
{
// we can have this result outside of the unsafe as it is a guaranteed parameter
// it must be mutable as it is used to store the result if the result is returned
let mut result = vec![-999f32, -999f32, -999f32];
unsafe
{
if check_mean_mode_median(a) != 0
{
return result;
}
else
{
let res = calc_mean_median_mode(a);
result = res.to_vec();
return result;
}
}
}
不仅我们现在只有一个对外部库的调用,我们还保证了编译器所需的保证。
访问全局变量
在 C 库中,经常会有用于版本细节和特定构建代码等用途的全局变量。Rust 可以以与其他变量类似的方式访问这些变量:
extern crate libc;
#[link(name = "mathlib")]
extern {
static code_version: libc::c_int;
}
fn main() {
println!("You have mathlib version {} installed.", code_version as i32);
}
自己清理
虽然数学库是一个非常简单的例子,但有时你可能需要使用返回大量数据块的库(例如,如果你创建了一个用于与ImageMagick(一个常用且功能强大的图形库)一起工作的包装器)。当库返回时,结果会被传递给 Rust 应用程序,你需要手动释放。
为了帮助你,Rust 提供了Drop特质。
丢弃它!
Drop特质是一个非常简单的特质:
pub trait Drop
{
fn drop(&mut self);
}
就像所有特质一样,在使用特质之前,它需要一个impl:
struct FreeMemory;
impl Drop for FreeMemory
{
fn drop(&mut self);
}
在这一点上,我们调用我们的pub fn,它从ImageMagick返回一个数据块。一旦我们用完那个内存块,我们必须释放它。我们将在一个名为graphics_block的变量中存储数据。要释放graphics_block中的块,我们使用:
graphics_block = FreeMemory;
当graphics_block超出作用域时,内存将被释放。
值得指出的是,panic!在回滚内存时会调用drop。因此,如果你在drop中有panic!,那么它很可能会中止。
在 FFI 中监控外部进程
在你使用计算机的时间里,你无疑会看到以下这样的图像:

这些进度条以类似的方式工作。比如说,你有一个有五个相等部分的进程,或者你正在从互联网下载文件。随着部分完成或下载的代码量增加,条形图和百分比会使用一种称为回调的编程技术更新。
回调的实现方式取决于所使用的语言。例如,在事件驱动的语言中,进程将发出信号或生成接收者监听的事件。当接收到信号/事件时,用户界面会更新。
Rust 没有不同;它能够在使用 FFI 时使用回调。Rust 能够处理同步和异步回调。还可以将回调定位到 Rust 对象。
定位同步回调
同步回调是最容易定位的,因为它们通常总是在同一个线程上。因此,我们不必处理代码比平时更不安全的情况,这在异步回调中通常是常见的情况。
这一部分的代码在 Chapter 14/synccallback 中。Linux、macOS 和 Windows 的构建说明包含在源示例中。
首先,让我们处理 Rust 代码的这一部分。在这里,我们有三个部分:
- 回调函数本身:
extern fn my_callback(percent: i32)
{
println!("Process is now {}% complete", percent);
}
- 调用外部代码:
[link(name="external_lib")]
extern
{
fn register_callback(call: extern fn(i32)) -> i32;
fn do_callback_trigger();
}
- 启动代码:
fn main()
{
unsafe
{
register_callback(my_callback);
do_callback_trigger();
}
}
register_callback(my_callback) 和 fn register_callback(call: extern fn(i32)) ->→ i32; 在第一眼看起来可能很奇怪。在正常的函数调用中,花括号内的参数被传递到接收函数中,然后接收函数对它们进行处理。
在这里,我们正在将一个函数作为参数传递,这实际上是不可以做的(或者至少不应该)。然而,回调是不同的,因为函数由于 extern 修饰符的存在,被视为一个指针,它将外部库返回的值作为自己的参数。
定位 Rust 对象
在最后一个例子中,我们有一个监听单个 int 的回调。但是,如果我们想监听外部库中的复杂对象(例如,一个结构体)会发生什么呢?我们不能返回一个结构体,但我们可以有一个可以映射到回调的 Rust 对象。
这比同步回调稍微复杂一些:
- 创建将映射到我们感兴趣的外部结构体的结构体:
#[repr(C)] // this is a name used within the extern in (2)
struct MyObject
{
a: i32,
// and anything else you want to get back from the library
// just make sure you add them into the call back
}
- 创建回调;
result是指向可变myobject的指针:
extern "C" fn callback(result: *mut MyObject, a: i32)
{
unsafe
{
(*result).a = a;
}
}
- 创建库的
extern函数:
#[link(name="external_lib")]
extern
{
fn register_callback(result: mut MyObject, cback: extern fn(mut MyObject, i32));
fn start_callback();
}
- 创建调用代码:
fn main()
{
// we need to create an object for the callback
let mystruct = Box::new (MyObject{a: 5i32});
unsafe {
register_callback(&mut *mystruct, callback);
start_callback();
}
}
从其他语言调用 Rust
Rust 也可以从其他语言中被调用,这是一个简单的过程。唯一的限制是使用的名称必须是未解析的。如果你还记得 第八章,“Rust 应用程序生命周期”,当你使用泛型时,编译器会生成必要的代码以确保链接器正常工作。它是通过混淆名称来做到这一点的,以确保在需要时编译和调用正确的代码。
解析(Unmangling)是相反的过程;它保留正在使用的函数的名称:
#[no_mangle]
pub extern fn hello_world() -> *const u8 {
"Hello, world!\0".as_ptr()
}
这可以从你自己的(非 Rust)应用程序中调用。
处理未知情况
C 开发者并不总是在具有 强类型 的函数之间传递参数;相反,他们传递 void* 类型。然后在接收函数中将其转换为某种具体类型。从某种意义上说,这与在函数之间传递泛型类型非常相似。
如果你想访问一个以 void* 作为参数类型的库中的函数,这些必须以不同的方式处理。
例如,C 函数可能是:
void output_data(void *data);
void transformed_data(void *data);
由于 Rust 中没有与 void* 相同的东西,我们需要使用一个可变指针:
extern crate libc;
extern "C"
{
pub fn output_data(arg: *mut libc::c_void);
pub fn transformed_data(arg: *mut libc::c_void);
}
这将完成这项工作。
C 结构体
在本章的早期,我们使用 struct 作为参数。在 C 中,没有阻止开发者将结构体作为参数传递:
struct MyStruct;
struct MyOtherStruct;
void pass_struct(struct MyStruct *arg);
void pass_struct2(struct MyOtherStruct *arg2);
MyStruct 和 MyOtherStruct 被称为不透明结构体。名称是公开的,但私有部分不是。
在 Rust 中处理 struct 并不像你最初想象的那么简单,但也不是那么困难。唯一的区别是我们在与 C 库接口时使用一个空的 enum 而不是 struct。这创建了一个不透明的类型,用于存储来自 C 不透明类型的信息。由于 enum 是空的,我们无法实例化它,更重要的是,由于 MyStruct 和 MyOtherStruct 并不相同,我们有了类型安全,因此不能将它们混淆:
enum MyStruct {};
enum MyOtherStruct {};
extern "C"
{
pub fn pass_struct(arg: *mut MyStruct);
pub fn pass_struct2(arg: *mut MyOtherStruct);
}
摘要
在本章中,我们介绍的内容不仅使 Rust 成为开发应用的优秀选择,而且通过使用非 Rust 库,也使其成为一种灵活且强大的语言。存在一些陷阱(例如需要使用 unsafe 和必须非常小心处理 panic! 代码),但优点远多于缺点。
对于本文的用途,.dll 仅用于 Windows。.NET 框架也使用 .dll 文件,如果它们不包含任何特定于 Windows 的内容,也可以在 macOS 和 Linux 上使用。


(x的平均值),
(y值的平均值)




浙公网安备 33010602011771号