Rust 实践指南

Rust 实践指南 - The Hitchhiker’s Guide to Rust

本书侧重于实践,通过以下实践过程体现目标。

  • 聚焦重要的主题,展示可能的解决方案,提供一个跳板引导读者进入一些更高级的内容(这些内容或许可以在网上或者参考手册中找到);
  • 以开发中遇到的实际应用问题为导向,以优雅的解决方案为目标,进行完整的实例实践解决方案;
  • 在实例实践过程中,引入解决方案所涉及的相关 crate,并进行实例开发展示。同时,若有同类 crate,则通过实践过程和实现效果进行比较;
  • 完整实例,可独立运行,也可整合于实际开发环境;
  • 对于实例开发中引入的 crate,同步更新于 crate 官方仓库,保持最新版本的深入实践。

本书是受到 rust-lang-nursery 团队的开源书籍项目《Cookin’ with Rust》启发而产生的,并且学习借鉴了它的目录结构。

本书具体内容还在规划中,并未确定。欢迎交流:ask@rusthub.org,budshome(个人微信)。另有微信公众号,如下图——

晨曦中

学以聚之,问以辩之。终日乾乾,与时偕行。


离线阅读

如果你喜欢本地阅读方式,可以使用 mdBook(中文文档) 进行书籍构建:

$ git clone https://github.com/zzy/rust-guide
$ cd rust-guide
$ cargo install mdbook # 请使用你感兴趣的版本参数,如:--vers "0.3.5"
$ mdbook serve --open # 或者 mdbook build

也可以直接用你喜欢的浏览器从 book 子目录打开 index.html 文件。

$ xdg-open ./book/index.html # linux
$ start .\book\index.html    # windows
$ open ./book/index.html     # mac

构建和测试

  • 本书使用 mdBook(中文文档) 进行构建。
  • 实践实例放在 examples 目录中,请提交前进行测试。

贡献

《Rust 实践指南》的目的是让 Rust 程序员新手能够更容易地参与到 Rust 语言社区中,因此非常欢迎您的参与。

祝您学习愉快,欢迎提交问题,欢迎发送 PR。

1. 前言

Rust 语言诞生以来,受到了众多好评。自 2015 年 5 月中旬 Rust 语言发布 1.0 版本以来,感兴趣者和学习者群体日益庞大,并且增长趋势呈现急剧上升的态势。

  • 在 StackOverflow 社区,Rust 语言连续数年被程序员群体评为最想学习的程序设计语言;
  • 在 GitHub 社区,Rust 生态类开源项目已经超过 10 万,以及众多将 Rust 语言作为主要开发语言的第三方杀手级生态项目,如:facebook 主导的区块链项目 libra、JavaScript/TypeScript 运行时项目 deno 等;
  • 在官方 crates 注册站点,有效托管的 crate 已经达到 5 万左右,下载累计 45.6 亿次左右;
  • 官方开源文档站点 docs.rs,每天全球的访问量达到百万上下。

Rust 语言作为一个诞生仅 10 年,1.0 版发布不到 5 年的程序设计语言,其用户群体增长趋势可以使用“疯狂”来形容。

1.1. Rust 语言的优势

思想层面的提升

在现代的编程语言设计中,高层工程学和底层控制往往是鱼和熊掌不可兼得的关系。而 Rust 语言设计开发团队则试图挑战这一矛盾。从目前的成果来看,他们的工作卓有成效。

  • Rust 语言中最为与众不同的特性和核心功能之一的所有权(ownership)语义,它让 Rust 语言无需垃圾回收(garbage collector)即可保障内存安全;
  • 在内存管理和使用方面,Rust 程序员基于所有权系统,无须亲自分配和释放内存,无需垃圾回收,即可以控制底层细节;
  • 生命周期(lifetime)的语义设计,有效地保持了引用的作用域,避免了悬垂引用;
  • 模式匹配、lambda 表达式、并发支持、泛型支持等现代语言的特性,在 Rust 语言的设计思想中,都得到了一定程度的改进和提升。

简而言之,Rust 语言从思想层面对编程语言的发展进行了极大提升。

优秀的用户体验

但实践才是检验新技术新思想的唯一标准,Rust 语言能在当前海量的编程语言中脱颖而出,正是因为它严格经受了实践的检验。Rust 语言实践中,其稳定地体现着媲美 C 语言的性能;因其系统级特性,可以帮助用户深刻理解计算机知识基础,培养个人的计算机素养,重新审视编程过程;使用 Rust 语言编程时,用户可以运用多种编程风格和思想,而且不用担心有太多的冲突;Rust 语言的安全机制,可在编译时最大程度帮助用户提高代码的可靠性,助力用户实现自己的想法,而不必过多担心内存使用这类复杂的问题;而其模式匹配、trait、crate 管理,以及项目组织等方面,则舒适地提高了生产力。

对于 Rust 语言的实际开发体验,很多 Rust 语言介绍性资料或理论书籍都进行了全面和客观的总结。正如 Rust 官方言简意赅的总结——

  • 高性能。Rust 语言速度惊人且内存利用率极高。由于没有运行时和垃圾回收,它能够胜任对性能要求特别高的服务,可以在嵌入式设备上运行,还能轻松和其他语言集成。
  • 可靠性。Rust 语言丰富的类型系统和所有权模型保证了内存安全和线程安全,让你在编译期就能够消除各种各样的错误。
  • 生产力。Rust 语言拥有出色的文档、友好的编译器和清晰的错误提示信息, 还集成了一流的工具——包管理器和构建工具, 智能地自动补全和类型检验的多编辑器支持, 以及自动格式化代码等等。

活跃而有保障的社区

在移动应用社区中,iOS 和 AndroidOS 是基础,对于开发者和使用者来说,移动生态甚至比平台技术更加重要。Rust 语言同样如此,其语言设计思想,以及核心特性和功能可以快速吸引大批程序员体验 Rust 语言。但是,最终决定公司是否将 Rust 语言纳入技术栈,或者程序员是否将 Rust 作为自己的主力开发语言的因素,则更多取决于 Rust 语言的生态环境:广度、深度、成熟度。

在 Rust 语言社区中,有类库团队 Rust Libz Blitz,主要目标为保障 Rust 生态系统的坚实核心,以及提升 crate 完整性和质量。

基于上述概要介绍,我们可以看到:无论是思想层面,还是实践检验,Rust 语言都非常优秀。而其专有的类库团队,则保障了快速增长态势下的生态环境,为项目产品中将 Rust 作为核心开发语言,提供了坚实基础。

1.2. 本书编撰原因和主要内容

在当前 Rust 生态中,资料和书籍基本都集中在理论方面,尤其以语法基础为主。而对于实际开发过程中的应用问题,资料较少,也比较碎片化。

当 Rust 程序员遇到实际应用问题,需要找到解决方案时,大多只能首先通过网络查找可用于解决此问题的 crate(类库),然后再去查阅此 crate 的官方文档。在实例参考方面,也仅能查阅官方 API 文档中的简要示例,或者通过搜索引擎查找有无应用实例。对于经验丰富的 Rust 程序员,或许能够通过官方 API 文档,或者搜索引擎获得一些帮助。但我们可以理解,这种做法效率较低。而且,遇到多个同类 crate,还需要自己编码对比,分辨其适用度。最后,即使经过测试后采用,但其成熟度也是未知的。

我们也需要意识到 Rust 语言还处于新兴阶段,相对来说,还是因为感兴趣而自学入门的 Rust 新手程序员居多。若要对目前的 Rust 生态系统做深入了解,体验并不友好。

籍于 Rust 生态现状,笔者认为,籍于实际开发应用问题,对于 Rust 生态系统进行详细实践,对于同类 crate 进行深入应用、比较分析的 Rust 实践指南类书籍,是比较需要甚至迫切需要的。其可以帮助 Rust 程序员系统地了解 Rust 在各个技术方向上的应用,以及技术层面的应用成熟度。对于公司建立 Rust 团队和技术栈选型,也有很大的辅助和参考作用。

笔者编撰此书,既是出于此意。

本书侧重于实践,通过以下实践过程体现目标。

  • 聚焦重要的主题,展示可能的解决方案,提供一个跳板引导读者进入一些更高级的内容(这些内容或许可以在网上或者参考手册中找到);
  • 以开发中遇到的实际应用问题为导向,以优雅的解决方案为目标,进行完整的实例实践解决方案;
  • 在实例实践过程中,引入解决方案所涉及的相关 crate,并进行实例开发展示。同时,若有同类 crate,则通过实践过程和实现效果进行比较;
  • 完整实例,可独立运行,也可整合于实际开发环境;
  • 对于实例开发中引入的 crate,同步更新于 crate 官方仓库,保持最新版本的深入实践。

本书是受到 rust-lang-nursery 团队的开源书籍项目《Cookin’ with Rust》启发而产生的,并且学习借鉴了它的目录结构。

关于本书中的 crate

本书的 crate 均来自 Rust 语言社区,主要是官方 crate 注册站点、GitHub 开源社区。但本书进行实践和解读的 crate,并非 Rust 社区中存在的全部 crate。

目前,本书的技术方向涵盖方面,大类较广,但小类并不全面。本书作为开源书籍项目,会后续完善,争取全面涵盖技术方向大类及其细分的小类,同时实践和解读更多 Rust 社区中成熟度较高的 crate。本书第一版中,在技术方向小类之下,实践和解读的 crate 来源主要为:其一是已经 Rust Libz Blitz 团队识别的 crate;其二是经笔者实践而推荐的 crate;其三是成熟度较高,社区人气较高的 crate。

关于 crate 的解读分析,则参考了 Rust 语言的官方开源文档站点中各自 crate 的基础文档。除此之外,本书中的解读和分析,均由笔者基于对各个 crate 的实践应用而撰写。

关于本书中的源代码

本书引用了开源书籍项目《Cookin’ with Rust》中的部分示例代码,以及其它开源项目的示例代码,以作为本书的基础实例。

本书中,对于所引用的开源项目的示例代码,部分稍作修改,因此可能和原有示例代码不尽一致。同时,对于引用的示例代码进行了详细实践,包括版本升级、深入解读、比较分析等方面。

除此自外,本书中的实例代码,均有笔者开发,以及跨平台(Linux/WSL、macOS、Windows)调试、测试。

2.3. 结语

“Rust,一门赋予每个人构建可靠且高效软件能力的语言。”

这是 Rust 官网显要位置的宣传语,吸引力十足且激励人心,但并无夸张。经实际开发过程的检验,Rust 在性能、可靠性,以及生产力方面,都有极好的体现。对于项目和产品的推进,从技术层面来讲,大有裨益。

Rust 程序设计语言,诸多新、优,未来可期。

感谢 Rust 语言社区各个 crate 开发者、rust-lang-nursery 团队的无私奉献!

笔者水平不足,错漏难免,敬请指导。

本书将完全开源,如果你感兴趣,欢迎参与,共结团队。

所谓学以聚之,问以辩之。欢迎交流,共同学习。

2020 年 12 月

2. 阅读建议

本书是 Rust 程序设计语言(Rust 2018 版)的实践实例集合:展示了在 Rust 生态系统中,使用各类 crate 来完成常见编程任务的良好实践。最终目的是对 Rust crate 生态系统的广泛覆盖,成为 Rust 生态中诸多高质量 crate 的参考手册。

本章中,基于本书的特点,对读者提供了一些建议。这些建议并非阅读本书的指导原则,仅是为了进一步帮助读者提高阅读效率。

2.1. 本书目标读者

本书并非 Rust 语言的理论书籍,对 Rust 语法基础未做介绍,因此阅读本书需要掌握 Rust 语言基础。

本书适用于 Rust 程序员新手,以便于他们可以快速了解和学习 Rust 生态系统中的各类 crate。并且可以跟随本书实例,对感兴趣的 crate 进行实践应用,掌握其功能。

同时,本书也适用于经验丰富的 Rust 程序员,他们可以在本书中找到如何完成常见任务的提示。并且可以参考本书中的实践解读,对目标 crate 进行比较分析,对技术选型做以辅助。

另外,每个实例都只是一个入门指导,如果读者想深入研究,需要自己去查阅更多资料。我们假定读者可以很熟练的使用搜索引擎以及知道怎样查询在线的 Rust 生态文档。

如果耐心阅读,从本书中你将学到一些新的技巧和技术,并应用到你自己的代码中去。

本书不适合 Rust 的初学者。事实上,本书已经假定读者具有 Rust 教程或入门书籍中所教授的基础知识。

本书也不是快速参考手册(例如快速查询某个 crate 或者其模块下的某个函数)。

2.2. 如何阅读本书

本书的目录展示了实践应用范畴和具体实践实例,组织为数个章节,如算法、压缩、数据结构等。这些章节是按照英文名称顺序排列的;后面的章节更深入一些,并且有些实例是在前面章节的概念之上构建。

在每章的初始介绍中,都列出了本章包含的实例列表。实例名称是要完成任务的简单描述,比如“在一个范围内生成随机数”;每个实例都有标记指示所使用的 crates,比如 rand-badge;以及 crate 在官方 crates 注册站点的分类,比如 cat-science-badge

Rust 程序员新手应该按照由第一章节直至最后章节的顺序来阅读,这种方式易于理解书中的实例。同时,这样也可以对 crate 生态系统有一个全面的了解。点击索引中的章节标题,或者在侧边栏中导航到本书的章节页面。

如果你只是在简单地为一个任务的寻找解决方案,那么找到特定实例的最简单的方法是详细查看目录,然后导航至每章的首页,根据实例名称(即是要完成任务的简单描述)查阅是否适配你的解决方案。

本书中,目前暂规划为三部分:

  • 第一部分是配置和工具。先简要介绍了 Rust 语言的优势,本书编撰原因和主要内容;然后基于本书的特点之上,对读者提供了一些建议,以帮助读者提高阅读效率;另外,在环境配置章节,阐述了 Rust 语言在 Linux、macOS、Windows 平台中的安装配置,辅助介绍了开发 Rust 程序所使用的编辑器和 IDE 的基本配置;最后,对于 Cargo 工具进行概要介绍,包括 crate 管理工具的使用方法,以及如何使用 cargo 工具开发、构建本书的实例。
  • 第二部分是实践和讨论。聚焦实际开发场景中重要的主题,展示可能的解决方案,并进行讨论;以开发中遇到的实际应用问题为导向,以优雅的解决方案为目标,进行完整的实例实践解决方案;在实例实践过程中,引入解决方案所涉及的相关 crate,并进行实例开发展示;同时,若有同类 crate,则通过实践过程和实现效果进行比较。
  • 第三部分是进阶和延伸。主要目标为 Rust 开发应用场景中遇到的特殊问题,或者一些附加工具的使用。

具体内容还在规划中,并未确定。

3. 环境配置

Linux 和 Windows 的 Linux 子系统 WSL(即 Windows Subsystem Linux)中,安装 Rust 的过程大抵相同。

在 macOS 操作系统环境中,有两种安装方式:一是通过Rust 官方命令安装,二是通过 Homebrew 软件包管理器安装。

而在 Windows 操作系统环境中,可以使用 Rust 官方提供的 rustup-init 工具,也可以使用 Windows 下的软件包管理器 Chocolatey 或 Scoop。另外,Windows 操作系统环境中安装 Rust,需要下载 Microsoft C++ 生成工具。

在各个系统的安装过程中,笔者提供了一些推荐操作,这些都可以根据个人习惯替换,仅供读者参考。

3.1. 配置 Rust 工具链的国内源

默认情况下,Rust 官方已经提供了工具链等资源的下载服务。

但是,Rust 官方服务器部署在北美洲,中国大陆用户下载速度较慢,甚至反复中断下载。因此笔者建议中国大陆用户使用国内镜像源,但如果你愿意等待较长时间,可以采用默认的官方源。

目前国内 Rust 工具链镜像源有:中国科学技术大学源、上海交通大学源、清华大学源,以及 rustcc 社区源。

读者可以前往笔者收集的 Cargo 注册表源,以查看即时更新的 Rust 工具链及 Cargo 相关资源的国内镜像源最新汇总。

本节仅介绍如何配置 rust-static 的国内镜像源,暂不考虑 Cargo 资源。Cargo 资源的国内镜像源配置,我们会在章节 4.1. 配置 cargo 国内镜像源中详细介绍。

下面,我们以上海交通大学的镜像地址为例,来了解如何配置 rust-static 的国内镜像源。

首先,打开网址 https://mirrors.sjtug.sjtu.edu.cn/docs/rust-static,我们可以看到上海交通大学提供的 Rust 工具链反向代理地址(红框内)及其使用帮助。如图 3.1-1 所示。

上交大 rust 工具链国内源

图 3.1-1

然后,我们配置自己机器中的环境变量,以在 Rust 工具链的执行中,采用上海交通大学的镜像源,而非默认的 Rust 官方源。不同的操作系统环境,配置过程稍有不同。

如果你是 Linux/WSL/macOS 操作系统

  • 复制图 3.1-1 中红框内 2 行命令,然后直接在 shell 窗口执行:
export RUSTUP_DIST_SERVER=https://mirrors.sjtug.sjtu.edu.cn/rust-static
export RUSTUP_UPDATE_ROOT=https://mirrors.sjtug.sjtu.edu.cn/rust-static/rustup
  • 上述方式仅对当前 shell 窗口有效,在 shell 窗口重启后,需要重新配置才能生效。也可以将其存储到系统环境的个性化设置文件中,如 .bashrc.profile。直接编辑你的个性化设置文件,在文件末尾增加上述 2 行。或者执行以下命令:
echo "export RUSTUP_DIST_SERVER=https://mirrors.sjtug.sjtu.edu.cn/rust-static" >> ~/.bashrc
echo "export RUSTUP_UPDATE_ROOT=https://mirrors.sjtug.sjtu.edu.cn/rust-static/rustup" >> ~/.bashrc

source ~/.bashrc

如果你是 Windows 操作系统

  • 仅需复制图 3.1-1 中红框内 2 行命令的 export 单词后的部分,然后直接在命令提示符(CMD)或者 powershell 窗口执行:

如果是命令提示符(CMD)窗口,请执行:

set RUSTUP_DIST_SERVER=https://mirrors.sjtug.sjtu.edu.cn/rust-static
set RUSTUP_UPDATE_ROOT=https://mirrors.sjtug.sjtu.edu.cn/rust-static/rustup

如果是 powershell 窗口,请执行:

$env:RUSTUP_DIST_SERVER=https://mirrors.sjtug.sjtu.edu.cn/rust-static
$env:RUSTUP_UPDATE_ROOT=https://mirrors.sjtug.sjtu.edu.cn/rust-static/rustup
  • 上述方式仅对当前命令提示符(CMD)或者 powershell 窗口有效,在命令提示符(CMD)或者 powershell 窗口重启后,需要重新配置才能生效。也可以将其存储到 Windows 系统的用户变量或者系统变量中:

右键点击“此电脑”或者“我的电脑” -> 点击“高级系统设置” -> 点击“环境变量”,打开环境变量设置窗口。如下图 3.1-2 中的 1,2,3 标记处所示。

windows 环境变量窗口

图 3.1-2

在“用户变量”或者“系统变量”区域(选其一即可),点击“新建”按钮,设置用户或系统的环境变量。分别增加“变量名”为 RUSTUP_DIST_SERVERRUSTUP_UPDATE_ROOT 2 个变量,前者“变量值”为 https://mirrors.sjtug.sjtu.edu.cn/rust-static,后者“变量值”为 https://mirrors.sjtug.sjtu.edu.cn/rust-static/rustup。如下图 3.1-3 中的 4,5 标记处所示,示例图为 RUSTUP_DIST_SERVER 变量值的填写,不要忘记增加变量 RUSTUP_UPDATE_ROOT

windows 环境变量设置

图 3.1-3

用户变量或环境变量保存后,需要重新打开命令提示符(CMD)或者 powershell 窗口才能生效。

3.2. Linux/WSL/macOS 配置 Rust 环境

本节介绍在 Linux/WSL/macOS 操作系统配置 Rust 环境,官方推荐的命令方式安装 Rust 作为平台共用而统一介绍。

对于 macOS 操作系统中使用 Homebrew 工具安装 Rust,单独作为一个小节来介绍。

3.2.1. 命令方式安装 Rust

官方提供的命令方式安装 Rust,是最为推荐的。其可以安装 rustup、rustc,以及 Cargo 等工具,并主动配置环境变量。

注意:在 Linux/WSL、macOS、Windows 操作系统,配置 Rust 环境需要先安装 curl(文件传输工具)、gcc/g++(GNU的 c/c++ 编译器)软件包。

请先检查系统环境,如果未安装 curl、gcc/g++ 软件包,请在各自系统环境进行软件包的编译和安装。

apt install curl, gcc # 或者 g++ // debian、ubuntu

yum install curl, gcc # 或者 g++ // redhat、centos

brew install curl, gcc # 或者 g++ // mac

上述命令仅供参考。通常,Linux 各发行版和 macOS 已经包含 curl 软件包,具体情况请根据你的操作系统环境处理。

3.2.1.1. 下载安装包,并进入安装状态

打开 Rust 官网安装页面 https://www.rust-lang.org/zh-CN/tools/install ,复制页面提示你执行的命令,其将适用于你的 Linux/WSL、macOS 环境。比如 Linux 平台环境,在官网页面可以看到和你操作系统相匹配的可执行命令,其和 WSL 环境安装命令是相同的。如图 3.2.1.1-1 所示。

官网提示命令

图 3.2.1.1-1

根据机器、操作系统,以及安装软件及其版本等因素的情况,你看到的命令或许和书中有所差别。

  • Linux/WSL 环境
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  • macOS 环境
curl https://sh.rustup.rs -sSf | sh

然后我们只需要等待,如果你没有配置 Rust 工具链的国内源,默认为 Rust 官方源,安装包下载进度会比较慢。如果所报错误为不能成功下载安装包,请参考章节 3.1. 配置 Rust 工具链的国内源。安装包下载完成后,会自动进入安装状态。脚本会检测你的操作系统,然后提示你进行安装选项的选择。

3.2.1.2. 安装选项

下载安装包,并进入安装状态后,我们会看到大抵如此的界面。

此界面为 macOS 操作系统环境截图。Linux/WSL 操作系统中,default host triple 一项则有所不同。如图 3.2.1.2-1 所示。

安装选项

图 3.2.1.2-1

  • 选项 1 是默认选项。它是安装脚本对你的操作系统环境进行检测后,向你推荐的 Rust 安装选项。默认选项安装当前 Rust 最新的稳定版本(stable),并会主动更改你的环境变量。
  • 选项 2 是自定义安装配置。在此选项中,你可以自定义安装,比如安装稳定版本(stable)还是每晚发布的版本(nightly),是否更改环境变量等。
  • 选项 3 是取消安装。

rustup 工具非常强大,具备不同 Rust 版本管理的功能,因此你可以选择执行选项 1 或 2 任意一个,安装成功后,再使用 rustup 工具,通过版本管理的方式增加其它版本。

rustup 工具的使用方法,可以使用 rustup help 命令方便地查阅,并且没有需要特别注意的细节,因此我们不做详细涉及。

在此,我们选择默认安装。

若安装编译报错,请检查是否已经安装软件包 gcc 或 g++。

3.2.1.3. 执行 source 命令

安装完成后,可以看到安装成功的提示信息(注意你的日期部分会和笔者的显示不同)。如下图 3.2.1.3-1 所示:

安装成功

图 3.2.1.3-1

请注意底行的 To configure your current shell run source $HOME/.cargo/env,根据提示,直接复制到 shell 窗口执行。

source $HOME/.cargo/env

如果你希望详细查看安装包是否主动配置了你的环境变量,请检查你的 .profile.bashrc 文件(通常是 .profile 文件),查看其末尾是否增加了 Cargo 环境变量。

最后,请打开 shell 窗口,运行命令 rustc --versioncargo --versionrustup --version 检测你的环境。如果可以看到 rustc、cargo,以及 rustup 的版本信息,并且没有提示任何错误信息,则表示已经成功安装。

至此,官方命令方式安装 Rust 已经完成。

3.2.2. Homebrew 安装 Rust

Homebrew 是 macOS(或 Linux)缺失的软件包的管理器。虽然其已经可以使用在 Linux 环境,但主要使用群体还是 macOS 用户。因此,本小节内容也主要是面向 macOS 环境的开发者。

  • 通过 brew 直接安装 Rust。此安装方式不能使用 rustup 工具来管理版本,所以不推荐,仅作为了解。
brew install rust
  • 我们希望使用官方提供的强大的 Rust 版本管理工具 rustup,因此我们通过 brew 安装 rustup 工具。
brew install rustup-init
  • 然后,在 bash 窗口执行 rustup 初始化命令,即可安装好 rust 环境及相关工具链。
rustup-init

但是,brew 安装 rustup 工具后,也会接管 rustup 的更新及卸载。不能再使用 rustup self update 更新升级 rustup 工具。

最后,请打开 shell 窗口,运行命令 rustc --versioncargo --versionrustup --version 检测你的环境。如果可以看到 rustc、cargo,以及 rustup 的版本信息,并且没有提示任何错误信息,则表示已经成功安装。

至此,Homebrew 方式安装 Rust 已经完成。

3.3. Windows 配置 Rust 环境

Windows 操作系统中,官方推荐的安装方式是采用 rustup,同时需要 Microsoft C++ 生成工具。

虽然,Windows 操作系统中也可以采用软件管理工具安装 Rust,如 Chocolatey 或者 Scoop 等,但我们希望使用官方提供的强大的 Rust 版本管理工具 rustup。因此,本书中仅介绍并推荐读者采用 rustup 在 Windows 平台安装配置 Rust,而非其它方式。

3.3.1. 安装 Microsoft C++ 生成工具

打开 Rust 官网安装页面 https://www.rust-lang.org/zh-CN/tools/install ,根据官网指示的 Microsoft C++ 生成工具链接地址下载 Microsoft C++ 生成工具。如图 3.3.1-1 所示。

Microsoft C++ 生成工具

图 3.3.1-1

Microsoft C++ 生成工具下载完成后,请根据提示安装,其耗时较长,请耐心等待。

3.3.2. rustup 安装器

3.3.2.1. 获取 rustup-init.exe

在 Windows 操作系统环境中,可以使用 Rust 官方提供的 rustup-init 工具,也可以使用 Windows 下的软件包管理器 Chocolatey 或 Scoop。

我们推荐使用 Rust 官方提供的 rustup-init 工具安装 Rust, Chocolatey 或 Scoop 不做涉及。

打开 Rust 官网安装页面 https://www.rust-lang.org/zh-CN/tools/install,根据你的 Windows 操作系统类型,下载对应的 32 位或 64 位 rustup-init.exe。如图 3.3.2.1-1 所示。

windows 官网提示

图 3.3.2.1-1

3.3.2.2. 运行 rustup-init

Microsoft C++ 生成工具安装完成后,运行官网获取的 rustup-init.exe。下面我们在命令提示符(CMD)窗口运行 rustup-init.exe,如果你喜欢并使用的是 powershell 窗口,其显示信息和操作步骤大抵相同。如图 3.3.2.2-1 所示。

运行 rustup-init

图 3.3.2.2-1

  • 选项 1 是默认选项。它是安装脚本对你的操作系统环境进行检测后,向你推荐的 Rust 安装选项。默认选项安装当前 Rust 最新的稳定版本(stable),并会主动更改你的环境变量。
  • 选项 2 是自定义安装配置。在此选项中,你可以自定义安装,比如安装稳定版本(stable)还是每晚发布的版本(nightly),是否更改环境变量等。
  • 选项 3 是取消安装。

rustup 工具非常强大,具备不同 Rust 版本管理的功能,因此你可以选择执行选项 1 或 2 任意一个,安装成功后,再使用 rustup 工具,通过版本管理的方式增加其它版本。

rustup 工具的使用方法,可以使用 rustup help 命令方便地查阅,并且没有需要特别注意的细节,因此我们不做详细涉及。

在此,我们选择默认安装。

安装完成后,Rust 和 Cargo 环境变量会添加到操作系统的环境变量中。请检查你的系统环境变量Path 变量名的变量值中是否增加了 Rust 和 Cargo 环境变量。命令提示符(CMD)中,可以直接键入 path 来查看,当然你也可以跟随配置 Rust 工具链的国内源(可选)小节中的配置截图,来查看安装后环境变量的实际配置情况。

同时,由此也可以查看你机器中 Rust 及其 Cargo 等工具链的安装位置,一般是在 C 盘符的用户目录中。

一般情况下,Rust 及其 Cargo 的环境变量不会添加到你的用户变量中,但事情总有例外。如果你在系统环境变量Path 变量中未找到,请检查用户变量中的 Path 变量。

最后,请打开命令提示符(CMD)或者 powershell,运行命令 rustc --versioncargo --versionrustup --version 检测你的环境。如果可以看到 rustc、cargo,以及 rustup 的版本信息,并且没有提示任何错误信息,则表示已经成功安装。

至此,rustup 安装器已经在你的 Windows 平台成功安装 Rust。

3.4. 更新和卸载

Linux/WSL、macOS 操作系统,请打开 shell 窗口;Windows 操作系统,请打开命令提示符(CMD)或者 powershell 窗口。然后,即可运行 Rust 更新或卸载的相关命令。

  • 更新 Rust
rustup update
  • 卸载 Rust

任何时候,如果你想卸载 Rust,可以运行命令:

rustup self uninstall

3.5. 编辑器、IDE

本节仅是 Rust 开发工具方面的概要信息,包括编辑器及其扩展、IDE 及其插件等,以供读者参考。编辑器、IDE 等开发工具相关的发展速度迅猛,因此详细介绍作用不大,读者可自行网络搜索获取最新版本和使用方法。

目前,支持 Rust 开发的工具越来越丰富。总体来说,主要分为编辑器和 IDE 两类。如以扩展方式支持 Rust 开发的编辑器:Visual Studio Code、Atom、Sublime 等;以插件方式支持 Rust 开发的 IDE:Eclipse IDE for Rust Developers、IntelliJ Rust。

另外,还有一些 Rust 专用的 IDE,但功能还不完善,或者说,笔者的使用体验感觉不如上文提及的编辑器和 IDE。因此,本书中不做赘述。

虽然从功能上来看,编辑器相较于 IDE,是被包含关系。但目前就 Rust 开发方面来说,编辑器和 IDE 的开发体验,笔者感觉差别不是很大。笔者更推荐使用 Visual Studio Code 编辑器和其一众 Rust 支持插件的方式来进行 Rust 开发。所以本小节也会对 Visual Studio Code 及其 Rust 支持插件着墨较多。

但名声显要的 Eclipse 和 IntelliJ 也开发了支持 Rust 的 IDE,如 Eclipse IDE for Rust Developers 和 IntelliJ Rust。本节中也会做相应介绍,辅助读者比较选择。

注:本书中的实例开发、构建、调试、运行,以及发布,均与编辑器或者 IDE 无关。使用何种编辑器或者 IDE,依据读者的个人喜好。

3.5.1. 编辑器及其扩展

本小节的内容是纲要性的功能简介。重点是介绍 Visual Studio Code 编辑器及其扩展,其可谓目前 Rust 开发中体验最好的工具,笔者最为推荐。在 Rust 开发中,Visual Studio Code 编辑器体验极好:运行速度快、界面美观简洁、布局合理。辅以 Rust 相关扩展,开发效率很高,用起来很趁手。

Visual Studio Code 编辑器的下载、安装,以及使用,网络资料很多,笔者不做涉及。笔者重点介绍其几种常用扩展,供读者参考。

此外,同类编辑器还有Atom、Sublime:Atom 编辑器是 Visual Studio Code 的基础,插件使用方式非常类似,但性能不太好;Sublime 性能极佳,但对 Rust 开发支持方面,插件性能较弱。

这几种编辑器都是著名的工具产品,所以截图不做详细放置,请读者自行搜索比较。

3.5.1.1. rust-analyzer

目前,Visual Studio Code 编辑器下最好的 RLS 服务器扩展,在此作为重点推荐。安装界面如图 2.5-1 所示。

rust-analyzer

图 3.5.1.1-1

rust-analyzer 提供了 rls 插件的大部分功能外,还支持以下特性:

  • workspace 符号查找;
  • 当前文档符号搜索;
  • 输入辅助;
  • 代码辅助;
  • 结构体代码辅助;
  • 自动添加缺失 trait 成员方法;
  • 路径导入;
  • 改变函数可见性;
  • 填充模式匹配分支;
  • Magic 填充等。

并且,rust-analyzer 插件相较于 rls 插件,性能具有很大优势。

rust-analyzer 扩展和 IntelliJ Rust 关系密切,rust-analyzer 扩展现在的主要负责人之一,是 IntelliJ Rust 团队的前技术负责人。

3.5.1.2. rust

Rust RLS 服务器扩展还有官方最早开发支持的编辑器扩展 Rust for vsCode。历时较长,支持特性丰富,但性能和体验不如 rust-analyzer,未来应该会和 rust-analyzer 统一:

  • 代码补全;
  • 代码跳转:跳转到定义,查看定义,查找所有引用,符号搜索;
  • 悬停时的查看类型和文档;
  • 代码格式化;
  • 重构;
  • 错误提示及建议;
  • snippets;
  • 构建任务等。

特性众多,功能完善且成熟,但性能有所欠缺。后来 Rust 官方新开了 rust-analyzer 插件项目,并且计划用来替代 rust 插件。

3.5.1.3. Even Better TOML

功能丰富的 toml 语言支持插件,帮助 Rust 开发者编辑 Rust 代码中的 toml 文件。支持语法高亮、代码扩展、语义分析、代码收缩,以及代码格式化等。

  • 语法高亮,如图 3.5.1.3-1 所示;

语法高亮

图 3.5.1.3-1

  • 代码格式化,如图 3.5.1.3-2 所示。

代码格式化

图 3.5.1.3-2

3.5.1.4. crates

crates.io 中各 crate 依赖项的扩展插件,在 Rust 开发者在使用 Cargo.toml 文件时,crates 插件可以辅助开发者管理依赖项。

  • 更新单个依赖,如图 3.5.1.4-1 所示;

更新单个依赖

图 3.5.1.4-1

  • 更新所有依赖,如图 3.5.1.4-2 所示。

更新所有依赖

图 3.5.1.4-2

3.5.1.5. LLDB VSCode

支持在 vsCode 中调试 Rust,请读者自行搜索安装。

3.5.1.6. 其它插件

比如 code9 插件等,也对在 vsCode 进行 Rust 开发有帮助。

读者可以在 vsCode 插件面板搜索 rust、crate 等,将会看到众多 Rust 开发辅助支持插件,筛选使用。

3.5.2. IDE 及其插件

IDE 巨头 Eclipse 和 IntelliJ 对 Rust 的开发支持非常热心,目前都有专门针对 Rust IDE 的独立项目,分别为 Eclipse IDE for Rust Developers 和 IntelliJ Rust。这两个 Rust IDE 功能强大,特性丰富,实现程度大抵相同,并且流行程度很高。因此本书不做详细介绍,仅提供信息,请读者自行搜索。下文通过 IntelliJ 官方支持的插件 IntelliJ Rust 做功能特性的简单展示,如图 3.5.2-1 所示。

IntelliJ 截图

图 3.5.2-1

除了基本的代码高亮、格式化、自动完成、错误提示外,最重要的特性有这几个:

  • 代码跳转支持标准库和依赖的第三方库(神器,学 Rust 必需)。比如我们正在学 Vec,写了个 Vec::new(),既能跳到 Vec 这个 struct 的定义,也能跳到 new 函数的定义,看到对应的实现。IntelliJ Rust 同样支持通用的跳转和查找的快捷键,如双击 Shift 查找所有符号、Ctrl + N 查找类型(struct 和 trait)、Alt + F7 查找用处、Ctrl + Alt + B 查找实现(包括实现 trait 的 struct 和所有 impl 块)等。如图 3.5.2-2 所示。

IntelliJ 截图

图 3.5.2-2

  • 支持调试 Rust 程序。Windows平台需要MinGW+Rust GNU工具链的配合。如图 3.5.2-3 所示。

IntelliJ 截图

图 3.5.2-3

  • 其它小功能。如变量类型提示、文档高亮、Alt + F7 查看代码结构、动态模板、重构等。

4. Cargo 工具

Rust 官方提供了非常强大的构建系统和包管理器 Cargo。Cargo 可以为你处理很多任务,比如下载 crate 依赖项、编译 crate、构建代码、生成可分发的 crate,并将它们上传到 crates.io —— Rust 社区的 crate 注册表。

Rust 中的 crate,类似于其它编程语言中的“包”或者“库”。目前,Rust 中约定不做翻译。

Rust 和 Cargo 捆绑,或者说 Rust 安装器中内置了 Cargo。因此,成功安装 Rust 之后,Cargo 也会随之安装,并主动配置环境变量。

注:本章中对 Cargo 工具的介绍比较简略,仅限于创建、编译、调试,以及运行本书中的实例。在附录章节 24.2. 附录二:Cargo 进阶中对 Cargo 做了进一步介绍,你也可以查阅 Cargo 中文文档以对 Cargo 进行全面了解。

4.1. 配置 Cargo 国内镜像源

Rust 官方默认的 Cargo 源服务器为 crates.io,其同时也是 Rust 官方的 crate 管理仓库,放置在 github。

Cargo 的“注册表源”与 crates.io 本身相同。也就是说,Cargo 也有一个在 github 存储库中提供的索引。该存储库匹配 crates.io index 的格式,即 github 仓库 https://github.com/rust-lang/crates.io-index,由该存储库的索引指示下载包的配置。

但是,正如我们在章节 3.1. 配置 Rust 工具链的国内源中提到过,Rust 官方服务器部署在北美洲,中国大陆用户下载速度较慢,甚至反复中断下载。因此笔者建议中国大陆用户使用国内镜像源,但如果你愿意等待较长时间,可以采用默认的官方源。

提供了 Rust 工具链镜像源的站点,一般也会提供 Cargo 国内镜像源服务。目前,国内 cargo 镜像源有:中国科学技术大学源、上海交通大学源、清华大学源,以及 rustcc 社区源。

Cargo 支持更换 crates.io 源索引,通过 $HOME/.cargo/config 文件配置。自定义 Cargo 源有两种方法,笔者推荐使用第一种:

  1. 创建 $HOME/.cargo/config 文件(各操作系统及版本均大致相同),然后在 config 文件内写入下述配置内容。其中协议推荐使用 git,但对于 https 和 git 协议,一般各镜像源都支持,并且是可以互换的。如果你所处的环境中不允许使用 git 协议,或者配置 git 协议后不能正常获取和编译 crate,可以换 https 协议再试试。
[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"
# 指定镜像
replace-with = '镜像源名' # 如:tuna、sjtu、ustc,或者 rustcc

# 注:以下源配置一个即可,无需全部

# 中国科学技术大学
[source.ustc]
registry = "https://mirrors.ustc.edu.cn/crates.io-index"
# >>> 或者 <<<
registry = "git://mirrors.ustc.edu.cn/crates.io-index"

# 上海交通大学
[source.sjtu]
registry = "https://mirrors.sjtug.sjtu.edu.cn/git/crates.io-index/"

# 清华大学
[source.tuna]
registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"

# rustcc社区
[source.rustcc]
registry = "https://code.aliyun.com/rustcc/crates.io-index.git"

cargo 1.68 版本开始支持稀疏索引:不再需要完整克隆 crates.io-index 仓库,可以加快获取包的速度。如果您的 cargo 版本大于等于 1.68,可以在 $HOME/.cargo/config 中添加如下内容:

以下配置仅以中国科学技术大学源为示例,其它源配置类同。

[source.crates-io]
replace-with = 'ustc'

[source.ustc]
registry = "sparse+https://mirrors.ustc.edu.cn/crates.io-index"
  1. 或者在项目工程结构中,与 Cargo.toml 同级目录的 .cargo 文件夹下创建 config 文件,config 文件配置方法和内容与第一种相同。

关于 Cargo 的国内镜像源,笔者会及时收集、验证,以及综合测试。请定期查看、比较,以及更新,以便于提高你的开发效率。

4.2. 使用 cargo 管理 crate

可以通过 cargo installcargo uninstall 管理本地环境可执行 crate。

cargo install 用于在本地环境安装可执行 crate。Linux/WSL、macOS 环境默认路径为 $HOME/.cargo/bin,Windows 环境默认路径为 %USERPROFILE%\.cargo\bin

本书附录中,我们将会实践 Rust 的模糊测试库 cargo-fuzz,我们既可以通过 cargo install cargo-fuzz 来将其可执行程序安装到本地环境。如果需要一次安装多个,通过空格分隔即可,假设我们也需要安装 Rust 语言开发的优秀书籍构建工具 mdbook。执行如下图 4.2-1 所示命令:

cargo install

图 4.2-1

在图 4.2-1 中,各处标记反映了如下信息——

  • 标记 1 处为我们将要执行的安装命令 cargo install mdbook cargo-fuzz,我们要将 mdbook 和 cargo-fuzz 的可执行程序安装到本地环境。
  • 标记 2 处信息表示当前使用的 Cargo 源服务器地址,笔者使用的是中国科学技术大学提供的 Cargo 镜像源。
  • 标记 3 处表示 mdboo、cargo-fuzz 这 2 个可执行 crate 安装时,需要依赖编译的 crate 数量,我们可以看到达到了 253 个依赖项。
  • 另外,安装过程中,我们还可以看到分为下载和编译 2 个阶段。

当安装完成后,会在命令窗口底部提示安装路径,版本等信息。如图 4.2-2 底部 2 行所示。

cargo installed

图 4.2-2

我们根据图 4.2-2 中提示的 crate 安装后的路径,查看是否安装成功。执行查看命令 ll(Windows CMD 窗口请使用 dir)查看安装位置所在目录,如图 4.2-3 所示,2 个红框内的可执行程序,表示已经安装成功。

cargo bin

图 4.2-3

如果需要卸载本地环境的可执行程序 cargo-fuzz,执行 cargo uninstall cargo-fuzz 命令即可。同样,可以一次卸载多个本地环境的可执行程序。如图 4.2-4 所示。

cargo uninstall

图 4.2-4

4.3. 使用 cargo 构建项目

本小节重点为结合本书实例,介绍如何使用 Cargo 创建、构建编译,以及运行项目。

需要特别说明的是:本小节对于 Cargo 命令的介绍仍然是以适用本书为主,如果你要全面了解 Cargo,请前往附录章节 24.2. 附录二:Cargo 进阶,或查阅 Cargo 中文文档

4.3.1. 创建项目

使用 Cargo 创建项目,执行 cargo new 命令:

$ cargo new hello_world --bin

传递参数 --bin 是为了创建一个二进制程序,--bin 也是未指定情况下的默认传递参数。如果希望创建一个库(lib),就需要传递参数 --lib

默认情况下,新创建项目目录会初始化为一个 git 仓库,如果你不希望初始化为 git 仓库,需要传递参数 --vcs none

上述命令执行后,Cargo 会创建以下文件:

$ cd hello_world
$ tree .
.
├── Cargo.toml
└── src
    └── main.rs

1 directory, 2 files

然后,让我们看看 Cargo.toml 文件:

[package]
name = "hello_world"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
edition = "2018"

[dependencies]

此文件被称作为 manifest 元清单,它包含了 Cargo 编译项目所需的所有元数据。

接下来,我们阅读 src/main.rs 源文件:

fn main() {
    println!("Hello, world!");
}

如此,我们即创建了一个新项目。

4.3.2. 编译和运行项目

4.3.2.1. 编译项目

cargo new 命令创建了一个 “hello_world”,我们通过 cargo build 命令来编译它:

$ cargo build
   Compiling hello_world v0.1.0 (file:///path/to/project/hello_world)

cargo build 命令将代码编译为可执行文件。目前,我们使用的是默认的调试模式进行编译,因此要运行此示例代码,执行 ./target/debug 目录下的二进制文件:

$ ./target/debug/hello_world
Hello, world!

4.3.2.2. 编译并运行项目

我们也可以直接使用 cargo run 命令来运行源代码。cargo run 命令会自行编译,然后运行它:

$ cargo run
     Fresh hello_world v0.1.0 (file:///path/to/project/hello_world)
   Running `target/hello_world`
Hello, world!

执行 cargo build 命令后,你会注意到,项目目录中创建了几个新文件和目录:

$ tree .
.
|-- Cargo.lock
|-- Cargo.toml
|-- src
|   `-- main.rs
`-- target
    `-- debug
        |-- build
        |-- deps
        |   |-- hello_world-6ad0b2df81336e7f
        |   |-- hello_world-6ad0b2df81336e7f.d
        |   `-- hello_world-6ad0b2df81336e7f.dSYM
        |       `-- Contents
        |           |-- Info.plist
        |           `-- Resources
        |               `-- DWARF
        |                   `-- hello_world-6ad0b2df81336e7f
        |-- examples
        |-- hello_world
        |-- hello_world.d
        |-- hello_world.dSYM -> deps/hello_world-6ad0b2df81336e7f.dSYM
        |-- incremental
        |   // ...
        `-- native

15 directories, 19 files

其中的 Cargo.lock 文件,包含项目依赖项的有关信息(即使还未有依赖,此文件也会在编译后产生),其内容可读性较差。另外,target 目录包含所有构建产品(二进制文件、依赖项编译文件等)。并且,如上文 cargo build 命令执行时所提及:Cargo 默认生成调试(debug)版本。

4.3.3. 发布项目

前面介绍的编译和运行,都是在调试(debug)模式之下。Rust 代码在这种模式下未有优化,生成的可执行程序性能很差。

如果希望发布自己开发的程序给他方使用,则须在发布模式下进行编译。发布模式在开启优化的情况下,编译文件,使用命令 cargo build --release

$ cargo build --release
   Compiling hello_world v0.1.0 (file:///path/to/project/hello_world)

cargo build --release 执行后,产生的二进制文件将放入目录 target/release,而不再是目录 target/debug

使用调试模式(debug)进行编译,是 Rust 开发的默认设置。因为调试模式下的编译过程中,编译器不进行优化,因此其编译时间较短。但代码编译后产生的二进制可执行文件,其运行速度会较慢。

使用发布模式(release)进行编译,会需要更多的时间,但代码编译后产生的二进制可执行文件,其运行速度会更快。

4.4. 使用 cargo 构建本书实例

本书的设计是为了让你能够即时访问可工作的代码,以及对其正在做什么有一个完整阐述,并指导你了解如何更进一步的信息。

本书中的所有实例都是完整的、可独立运行的程序,因此你可以直接复制它们到自己的项目中进行试验。为此,请按照以下说明进行操作。

考虑这个实例:“在一个范围内,生成随机数”:

rand-badge cat-science-badge

use rand::Rng;

fn main() {
    let mut rng = rand::thread_rng();
    println!("Random f64: {}", rng.gen::<f64>());
}

欲在本地使用,我们可以运行以下命令来创建一个新的 cargo 项目,并切换到该目录:

cargo new my-example --bin
cd my-example

然后,我们还需要添加必要的 crate 到 Cargo.toml 中,如上面实例代码顶部的 crate 标志所示,在本实例中仅使用了 “rand” crate。为了增加 “rand” crate,我们将使用 cargo add 命令,该命令由 [cargo-edit] crate 提供,我们需要先安装它:

cargo install cargo-edit
cargo add rand

接下来,可以使用实例代码替换 src/main.rs 文件的全部内容,并通过如下命令运行:

cargo run

亲,成功执行了吧?你已经是一个 Rustacean(Rust 开发者)了!

关于 Cargo 的使用,本章节到此为止。我们会在附录章节 24.2. 附录二:Cargo 进阶中对 Cargo 做了进一步介绍,你也可以查阅 Cargo 中文文档以对 Cargo 进行全面了解。

5. 算法

实际开发过程中,我们经常会碰到到诸如取得随机值、查询、排序、过滤、平均值、平方/立方根,以及公约数计算等等这些普遍存在的问题。这一章的目的就是讨论这些比较常见的问题和算法。

5.1 生成随机值

生成随机数最显著的应用场景莫过于一些随机业务,如公司年会的抽奖、各类彩票业务、邮箱新用户生成密钥等。

但对于非随机业务来说,随机数的生成也有罕见。多见于瞬时负载压力,数据大批量处理等方面的业务。比如:

  • 多客户端多线程并发类似的业务。在高峰期间,会导致应用服务器、数据库服务器的瞬时负载率超高。将会造成任务处理的延迟,必须进行优化以降低负载。在这种情况下,有一种解决方案即为分析梳理业务,进行随机休眠处理,这样可将执行散列到不同的时间段。
  • 数据预热业务。当有大量数据不能离线预热,必须要线上预热的时候,势必造成资源的严重紧张,甚至打垮服务器。所以采用随机预热方式,使数据逐渐预热。预热成功后,在取消随机预热。
  • 批量的缓存处理。当大批量缓存同时建立,又批量失效,导致缓存建立不分散,对服务端瞬时产生压力。可以通过将部分缓存失效时间随机延长几分钟即可,分散批量建立和失效的压力。
  • 也可用于重复提交方面的解决方案、浏览器缓存处理等等。

生成随机数的应用场景非常广阔,在此不一一赘述。而当前技术潮流中,和生成随机数功能结合最紧密的,莫过于大红大紫的区块链技术。

在区块链及其数字证券领域中,随机数是关键的技术要点,其直接与区块链及其数字证券领域中最重要和最基本的安全保障方面息息相关。

5.1.1. 生成随机数

rand-badge cat-science-badge

问题:

你想取得某个标量类型的随机数。

解决方案:

Rust 生态中,有一个比较成熟的随机值计算库 rand crate,提供了各类扩展 trait 和随机值计算的方法。在随机数生成器 rand::Rng 的帮助下,通过 rand::thread_rng 生成随机数。

并且,可以开启多个线程,每个线程都可以存在一个初始化的生成器。整数在其类型范围内均匀分布,浮点数是从 0 均匀分布到 1,但不包括 1。

以下实例代码引用自开源书籍项目《Cookin’ with Rust》,笔者在其基础上稍作修改。

use rand::Rng; // `RngCore` 上自动实现的扩展 trait,实现了高层次的泛型方法

fn main() {
    let mut rng = rand::thread_rng(); // 由系统创建的本地线程,是延迟初始化的随机数生成器

    let n1: u8 = rng.gen(); // 返回一个支持标准分布的随机值
    let n2: u16 = rng.gen();
    
    println!("  u8    随机数: {}", n1);
    println!("  u16   随机数: {}", n2);
    
    println!("  u32   随机数: {}", rng.gen::<u32>());
    println!("  i32   随机数: {}", rng.gen::<i32>());
    println!("  float 随机数: {}", rng.gen::<f64>());
}

代码第 1 行,我们使用 use 关键字将 rand::Rng 引入作用域。rand::Rng 是在 RngCore trait 上自动实现的扩展 trait,它实现了高层次的泛型方法。

代码第 4 行,由系统创建本地线程,作用为延迟初始化的随机数生成器。

代码第 6,7 行,均为返回支持标准分布的随机值,分别是 u8,u16 的无符号整型。

构建并运行后,结果大抵如下所示。

  u8    随机数: 73
  u16   随机数: 36982
  u32   随机数: 2372946688
  i32   随机数: 17124411
  float 随机数: 0.8223380141339609

注:你的运行结果值和笔者运行结果不一定相同。

讨论:

rand::RngRngCore 上自动实现的扩展 trait,为抽样取值和其它便捷方法实现了高层次的泛型方法。

rand::thread_rng 是创建随机数生成器线程的函数。调用后,由系统创建延迟初始化的本地线程。随机生成器线程将用于方法链(method chaining)样式,如 thread_rng().gen::<i32>()。或本地缓存,如 let mut rng = thread_rng();。由 Default trait 调用,等效于 ThreadRng::default()

5.1.2. 生成范围内随机数

rand-badge cat-science-badge

问题:

你想从一个自定义标量区间内生成随机数。

解决方案:

我们继续使用 rand crate 来解决此问题。rand crate 中有一个产生范围区间的方法 Rng::gen_range,可以在半开放的 [低位,高位] 范围内生成一个随机值,随机值包含低位,不包含高位。比如我们在 [0, 10) 范围内生成一个随机值。

以下实例代码引用自开源书籍项目《Cookin’ with Rust》,笔者在其基础上稍作修改。

use rand::Rng;

fn main() {
    let mut rng = rand::thread_rng();
    
    println!("  整数:   {}", rng.gen_range(0..10)); // 半开放范围取随机值,即包括`低位`而不包括`高位`
    println!("  浮点数: {}", rng.gen_range(0.0..10.0));
}

代码第 1 行,我们使用 use 关键字将 rand::Rng 引入作用域。rand::Rng 是在 RngCore trait 上自动实现的扩展 trait,它实现了高层次的泛型方法。

代码第 4 行,由系统创建本地线程,作用为延迟初始化的随机数生成器。

代码第 6,7 行,分别从半开放范围取整型(0,10)和浮点型(0.0,10.0)随机值,即包括低位而不包括高位

构建并运行后,结果大抵如下所示。

  整数:   8
  浮点数: 5.352798350588619

注:你的运行结果值和笔者运行结果不一定相同。

讨论:

Rng::gen_range 方法在 [低位,高位] 范围内生成一个随机值。此范围为半开放范围,即包括低位而不包括高位。此函数针对给定范围内仅生成一个样本值的情况进行了优化。另外,此函数为均匀分布类型,如果从相同的范围重复抽样取值,执行速度将会更快。

Uniform 是一个结构体,表示样本值均匀地分布在两个界限之间。Uniform::newUniform::new_inclusive 构造给定范围内的均匀分布采样;这些函数可能会预先做一些额外的工作,以便更快地对多个值进行采样。必须特别注意:确保四舍五入不会导致采样值小于(<)低位或者大于等于(>=)高位

均匀分布:在概率论和统计学中,均匀分布也叫矩形分布,它是对称概率分布,在相同长度间隔的分布概率是可能相等的。

使用 Uniform 模块可以得到均匀分布的值。下述代码和上述代码具有相同的效果,但在相同范围内重复生成数字时,下述代码性能可能会更好。

以下实例代码引用自开源书籍项目《Cookin’ with Rust》,笔者在其基础上稍作修改。

use rand::distributions::{Distribution, Uniform};

fn main() {
    let mut rng = rand::thread_rng();
    let die = Uniform::from(1..7);

    loop {
        let throw = die.sample(&mut rng);

        println!("  丢一次骰子: {}", throw);
        
        if throw == 6 {
            break;
        }
    }
}

本实例模拟丢骰子的程序。

代码第 1 行,我们使用 use 关键字将均匀分布模块 rand::distributions::{Distribution, Uniform}; 引入作用域。其中 Distribution trait 必须引入,其将自动实现随机值的取值方法。若不引入,代码第 8 行的 sample 方法将不能使用,控制会输出如下错误:

error[E0599]: no method named `sample` found for struct `Uniform<{integer}>` in the current scope
 --> examples/rand-range-uniform.rs:8:25
  |
8 |         let throw = die.sample(&mut rng);
  |                         ^^^^^^ method not found in `Uniform<{integer}>`
  |
  = help: items from traits can only be used if the trait is in scope
help: the following trait is implemented but not in scope; perhaps add a `use` for it:
  |
1 | use rand::distributions::Distribution;
  |

error: aborting due to previous error

For more information about this error, try `rustc --explain E0599`.
error: could not compile `randomness`

To learn more, run the command again with --verbose.

代码第 4 行,由系统创建本地线程,作用为延迟初始化的随机数生成器。

代码第 5 行,制定了均匀分布取随机值的半开放范围(1,7),包括 1 而不包括 7

代码第 8 行,使用 Distribution trait 实现的 sample 方法进行随机值的半开放范围取值。

构建并运行后,结果大抵如下所示。

  丢一次骰子: 4
  丢一次骰子: 1
  丢一次骰子: 5
  丢一次骰子: 4
  丢一次骰子: 2
  丢一次骰子: 6

注:你的运行结果值,包括丢骰子的次数和骰子面值,都和笔者运行结果不一定相同。

5.1.3. 生成给定分布随机数

rand_distr-badge cat-science-badge

问题:

你想从一个标量区间内取得随机数,并且需要使用指定的分布类型采样。

解决方案:

默认情况下,随机数在 rand crate 中是均匀分布。对其做补充的 rand_distr crate 提供了其它的分布类型。

如要使用,首先实例化一个分布,然后在随机数生成器 rand::Rng 的帮助下,使用 Distribution::sample 从该分布中进行采样。Distribution::sample 方法创建一个迭代器,用来生成泛型 T 的随机值,其使用 rng 作为随机来源。

如下是一个使用正态(Normal)分布的实例。

以下实例代码引用自开源书籍项目《Cookin’ with Rust》,笔者在其基础上稍作修改。

use rand_distr::{Distribution, Normal, NormalError};
use rand::thread_rng;

fn main() -> Result<(), NormalError> {
    let mut rng = thread_rng();
    let normal = Normal::new(2.0, 9.0)?; // 正态分布
    let v = normal.sample(&mut rng);

    println!("  正态分布: {}", v);

    Ok(())
}

代码第 1-5 行,分别为使用 use 将正态分布相关模块引入作用域,以及由系统创建本地线程,作用为延迟初始化的随机数生成器。

代码第 6,7 行,建立正态分布模型,以及使用通过正态分布 rand_distr::{Distribution, Normal, NormalError} trait 实现的 sample 方法从半开放范围(2.0,9.0)取随机值。

构建并运行后,结果大抵如下所示。

  正态分布: 9.911104023278147

注:你的运行结果值和笔者运行结果不一定相同。

讨论:

rand_distr crate 是 rand::distributions 模块的一个超级集合,实现了诸多概率分布类型,如均匀分布、正态分布(Normal distribution)、柯西分布(Cauchy distribution)等。

关于更多信息,可阅读相关可用分布文档

5.1.4. 生成自定义类型随机值

rand-badge cat-science-badge

问题:

你想取得一个随机值,但随机值类型并非标量,而是自定义类型。

解决方案:

假设我们需要随机生成一个元组 (i32, bool, f64),和一个用户定义类型为 Point 的变量。对于随即元组 (i32, bool, f64),如同前几个小结生成标量随机值一样,可以通过 rng.gen::<(i32, bool, f64)>() 直接生成。

而对于随机生成用户定义类型 Point 的变量,则需要使用通用的随机值分布结构体。rand crate 中,定义了一个结构体 Standard,它是通用的随机值分布结构体,通常生成均匀分布的数值,并且具有与类型相适应的范围。我们已经了解,Distribution 是用于创建概率分布类型的 trait。因此,我们需要为 Standard 实现 Distribution trait,以允许随机生成。

以下实例代码引用自开源书籍项目《Cookin’ with Rust》,笔者在其基础上稍作修改。

use rand::Rng;
use rand::distributions::{Distribution, Standard};

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

impl Distribution<Point> for Standard {
    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Point {
        let (rand_x, rand_y) = rng.gen();
        Point {
            x: rand_x,
            y: rand_y,
        }
    }
}

fn main() {
    let mut rng = rand::thread_rng();
    let rand_tuple = rng.gen::<(i32, bool, f64)>();
    let rand_point: Point = rng.gen();

    println!("  随机值元组:   {:?}", rand_tuple);
    println!("  随机值结构体: {:?}", rand_point);
}

这段代码行数虽多,但逻辑不复杂。

代码第 1,2 行,使用 use 将相关模块引入作用域。

代码第 5-8 行,自定义一个结构体类型 Point

最主要的是代码第 10-18 行,为 Standard 实现 Distribution trait,其中 Distribution trait 的类型为结构体 Point。这样可以在 sample 方法中,直接将产生的随机值封装到结构体 Point

构建并运行后,结果大抵如下所示。

  随机值元组:   (413049996, true, 0.19354408882040275)
  随机值结构体: Point { x: 1087377718, y: -375119726 }

注:你的运行结果值和笔者运行结果不一定相同。

讨论:

对于用户自定义类型,我们需要使用通用的随机值分布结构体 Standard,然后为结构体 Standard 实现 Distribution trait。结构体 Standard 为 rand crate 中的众多基本类型实现,也可适用于用户自定义类型。而 Distribution trait 则用于创建概率分布类型,可以用来创建泛型 T 的随机实例的类型。提供 sample_iter 方法,该方法生成一个迭代器,从分布中采样。

5.1.5. 从字母数字字符创建随机密码

rand-badge cat-os-badge

问题:

你想从一组字母数字生成一个随机值,比如创建随机密码。

解决方案:

在 rand crate 的分布类型中,包含一个字母数字样本,范围为 A-Z,a-z,0-9。因此我们可以使用字母数字样本,随机生成一个给定长度的 ASCII 字符串。

以下实例代码引用自开源书籍项目《Cookin’ with Rust》,笔者在其基础上稍作修改。

use rand::{thread_rng, Rng};
use rand::distributions::Alphanumeric;

fn main() {
    let rand_string: String = thread_rng()
        .sample_iter(&Alphanumeric)
        .take(30)
        .collect();

    println!("  随机密码: {}", rand_string);
}

代码第 1,2 行使用 use 关键字将相关模块引入作用域,其中 rand::distributions::Alphanumeric字母数字样本,范围为 A-Z,a-z,0-9

代码第 6,7 行使用 sample_iter 方法迭代从字母数字样本产生总长度为 30 的随机密码。

构建并运行后,结果大抵如下所示。

  随机密码: vVvYkJpQhiBbrB7rVNvzHpzXOrmO7K

注:你的运行结果值和笔者运行结果不一定相同。

讨论:

rand::distributions::Alphanumeric字母数字样本,范围限定为 A-Z,a-z,0-9,如果需要生成的随机值字符包括特殊字符,请参阅下一小节 5.1.6. 从用户定义字符创建随机密码

5.1.6. 从用户定义字符创建随机密码

rand-badge cat-os-badge

问题:

你想设定一组自定义字符,然后从此自定义字符范围内生成一个随机值,比如创建只包含小写字母、数字,以及特殊字符的随机密码。

解决方案:

使用用户自定义的字节字符串,使用 gen_range 函数,随机生成一个给定长度的 ASCII 字符串。

以下实例代码引用自开源书籍项目《Cookin’ with Rust》,笔者在其基础上稍作修改。

use rand::Rng;

fn main() {
    const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz\
                            0123456789)(*&^%$#@!~";
    const PASSWORD_LEN: usize = 30;

    let mut rng = rand::thread_rng(); // 由系统创建延迟初始化的随机数生成器本地线程

    let password: String = (0..PASSWORD_LEN)
        .map(|_| {
            let idx = rng.gen_range(0..CHARSET.len()); // 半开放范围取随机值,即包括`低位`而不包括`高位`
            CHARSET[idx] as char
        })
        .collect();

    println!("  随机密码: {:?}", password);
}

代码第 1 行,我们使用 use 关键字将 rand::Rng 引入作用域。rand::Rng 是在 RngCore trait 上自动实现的扩展 trait,它实现了高层次的泛型方法。

代码第 3,4 行,自定义字节字符串样本,并设定密码长度。

代码第 11 行,通过 0..PASSWORD_LEN 匹配一个闭区间范围,其是密码字符串的长度。

代码第 12-15 行,首先在 map 方法内,通过闭包从自定义字节字符串样本中取随机值,其将根据密码字符串的长度迭代产生。然后通过 collect 方法绑定到变量 password

构建并运行后,结果大抵如下所示。

  随机密码: "5%6y$upw4j1!w!mt)fw%)p^&b7xp($"

注:你的运行结果值和笔者运行结果不一定相同。

讨论:

gen_range 函数在 [低位,高位] 范围内生成一个随机值。此范围为半开放范围,即包括低位而不包括高位。此函数针对给定范围内仅生成一个样本值的情况进行了优化。另外,此函数为均匀分布类型,如果从相同的范围重复抽样取值,执行速度将会更快。

5.1.7. 随机值 crates 小结

我们在随机值计算中,主要引入了 rand crate 和 rand_distr crate,其中后者是对前者的补充。在此,再做一些扩展介绍。但这些介绍都是概要性的,若要详细了解,请打开实例标题之下的 url,参阅 crate 官方文档。

5.1.7.1. rand

rand crate 是 Rust 生态中的一个随机数生成工具。其提供了如下功能:

  • 生成随机数;
  • 将生成的随机数进行类型转换、分发;
  • 一些与随机性相关的算法。

rand crate 是由一系列 crate 组成的,rand crate 提供了主用户界面。如果需要额外的分发类型,可以使用 rand_distr crate 或者 statrs crate 来补充。具体结构关系如图 5.1.7.1-1 所示。

getrandom ┐
          └ rand_core ┐
                      ├ rand_chacha ┐
                      ├ rand_hc     ┤
                      ├ rand_pcg    ┤
                      └─────────────┴ rand ┐
                                           ├ rand_distr
                                           └ statrs

图 5.1.7.1-1

5.1.7.2. rand_distr

rand_distr crate 实现了诸多概率分布类型,如均匀分布、正态分布(Normal distribution)、柯西分布(Cauchy distribution)等。rand_distr crate 是 rand::distributions 模块的一个超级集合,提供了以下概率分布:

  • 线性增长相关(例如误差、偏移量等):
    • 正态(Normal)分布,以标准正态(StandardNormal)为原语
    • 柯西(Cauchy)分布
  • 伯努利试验相关(给定概率的是/否事件):
    • 二项(Binomial)分布
  • 指数增长相关(例如价格、收入、人口等):
    • 对数正态(LogNormal)分布
  • 给定条件下独立事件的发生率相关:
    • 帕雷托(Pareto)分布
    • 泊松(Poisson)分布
    • 指数(Exponential)分布,以结构体 Exp1 为原语
    • 韦布尔(Weibull)分布
  • 伽马分布、导出分布:
    • 伽马(Gamma)分布
    • 卡方(ChiSquared)分布
    • 学生-T(StudentT)分布
    • 费歇尔-F(FisherF)分布
  • 三角学分布:
    • Beta 分布
    • 三角(Triangular)分布
  • 多元概率分布:
    • 狄利克雷(Dirichlet)分布
    • UnitSphere 分布
    • UnitBall 分布
    • UnitCircle 分布
    • UnitDisc 分布
  • 其它分布:
    • 逆高斯(InverseGaussian)分布
    • 正态逆高斯(NormalInverseGaussian)分布

在上图 5.1.7.1-1 结构中已经展示,rand_distr crate 是对 rand crate 的补充。

5.2. Vector 排序

本节主要内容是对 Rust 语言中的 Vec 类型进行排序。Vec 类型也被称为 vector,它是动态数组。

vector 允许我们在一个单独的数据结构中储存多于一个的值,它在内存中彼此相邻地排列所有的值。vector 只能储存相同类型的值,但其存储类型可以自定义。

vector 排序在拥有一系列项的场景下非常实用,例如文件中的文本行,或是购物车中商品的价格等。

5.2.1. 整数 Vector 排序

std-badge cat-science-badge

问题:

你想对整数类型的动态数组 vector 进行排序。

解决方案:

通过 vec::sort 对一个整数 Vector 进行排序。另一种方法是使用 vec::sort_unstable,后者运行速度更快一些,但不保持相等元素的顺序。

以下实例代码引用自开源书籍项目《Cookin’ with Rust》,笔者在其基础上稍作修改。

fn main() {
    let mut vec = vec![1, 5, 10, 2, 15];
    println!("  排序前: {:?}", vec);

    vec.sort();
    println!("  排序后: {:?}", vec);

    assert_eq!(vec, vec![1, 2, 5, 10, 15]);
}

代码第 5 行,通过 sort 方法对 vector 进行排序。

构建并运行后,结果大抵如下所示。

  排序前: [1, 5, 10, 2, 15]
  排序后: [1, 2, 5, 10, 15]

注:第 8 行是两个 vector 比较的断言,如果不相等,会输出 “thread ‘main’ panicked at ’assertion failed: (left == right)”,请你自行尝试。

讨论:

使用 vec::sort 对切片进行排序,这种排序是稳定的(即不重新排序相等的元素)。在合适的场景,不稳定排序是首选的,因为它通常比稳定排序快,并且不分配辅助内存。

使用 vec::sort_unstable 对切片进行排序,但可能不会保留相等元素的顺序。这种排序类型不甚稳定(即,可能重新排序相等的元素)。

5.2.2. 浮点数 Vector 排序

std-badge cat-science-badge

问题:

你想对浮点类型的动态数组 vector 进行排序。

解决方案:

f32 或 f64 的 vector,可以使用 vec::sort_byPartialOrd::partial_cmp 对其进行排序。

以下实例代码引用自开源书籍项目《Cookin’ with Rust》,笔者在其基础上稍作修改。

fn main() {
    let mut vec = vec![1.1, 1.15, 5.5, 1.123, 2.0];
    println!("  排序前: {:?}", vec);

    vec.sort_by(|a, b| a.partial_cmp(b).unwrap());
    println!("  排序后: {:?}", vec);

    assert_eq!(vec, vec![1.1, 1.123, 1.15, 2.0, 5.5]);
}

代码第 5 行,使用 PartialOrd::partial_cmp 对浮点型 vector 进行排序。

构建并运行后,结果大抵如下所示。

  排序前: [1.1, 1.15, 5.5, 1.123, 2.0]
  排序后: [1.1, 1.123, 1.15, 2.0, 5.5]

断言的使用和整型 vector 排序类似,不再赘述。

讨论:

vec::sort_by 使用比较器(comparator )函数对切片进行排序,这种排序是稳定的(即不重新排序相等的元素)。比较器(comparator )函数必须为切片中的元素定义一个总顺序。如果不是全序关系排序,则不指定元素的顺序。如果排序是全序关系(对于所有 abc),则:

  • 完全反对称:a < ba == ba > b 中的一个为真,并且
  • 等量代换:a < bb < c 意味着 a < c。对于 ==>,同样具有等量代换关系。

使用 PartialOrd::partial_cmp时,如果存在其它值(other ),此方法将返回自己(self)其它值(other )之间的排序。

5.2.3. 结构体 Vector 排序

std-badge cat-science-badge

问题:

你想对结构体类型的动态数组 vector 进行排序。

解决方案:

依据自然顺序(按名称和年龄),对具有 nameage 属性的 Person 结构体 Vector 排序。为了使 Person 可排序,你需要四个 traits:EqPartialEqOrd,以及 PartialOrd。这些 traits 可以被简单地派生。你也可以使用 vec:sort_by 方法自定义比较函数,仅按照年龄排序。

以下实例代码引用自开源书籍项目《Cookin’ with Rust》,笔者在其基础上稍作修改。

#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
struct Person {
    name: String,
    age: u32
}

impl Person {
    pub fn new(name: &str, age: u32) -> Self {
        Person {
            name: name.to_string(),
            age
        }
    }
}

fn main() {
    let mut people = vec![
        Person::new("Zhang", 25),
        Person::new("Liu", 60),
        Person::new("Wang", 1),
    ];
    println!("  排序前: {:?}", people);

    // 根据获得的自然顺序(name 和 age)对 people 进行排序
    people.sort();
    println!("  排序后(name 和 age): {:?}", people);

    assert_eq!(
        people,
        vec![
            Person::new("Liu", 60),
            Person::new("Wang", 1),
            Person::new("Zhang", 25),
        ]);

    // 根据 age 值对 people 进行排序
    people.sort_by(|a, b| b.age.cmp(&a.age));
    println!("  排序后(age): {:?}", people);

    assert_eq!(
        people,
        vec![
            Person::new("Liu", 60),
            Person::new("Zhang", 25),
            Person::new("Wang", 1),
        ]);

}

代码第 1-5 行,创建结构体类型 Person,并为其派生一系列宏 Debug, Eq, Ord, PartialEq, PartialOrd

代码第 14 行,为结构体类型 Person 实现 new 方法。并且在代码第 17-21 行,使用 new 方法绑定值到结构体 people

代码第 25 行,根据自然顺序,即为全部值 name 和 age,对结构体 people 进行排序。

代码第 37 行,仅根据 age 值对结构体 people 进行排序,在 sort_by 方法中通过闭包,指定排序依据 age 值。

构建并运行后,结果大抵如下所示。

  排序前: [Person { name: "Zhang", age: 25 }, Person { name: "Liu", age: 60 }, Person { name: "Wang", age: 1 }]
  排序后(name 和 age): [Person { name: "Liu", age: 60 }, Person { name: "Wang", age: 1 }, Person { name: "Zhang", age: 25 }]
  排序后(age): [Person { name: "Liu", age: 60 }, Person { name: "Zhang", age: 25 }, Person { name: "Wang", age: 1 }]

断言的使用和整型 vector 排序类似,不再赘述。

讨论:

Eq 是对等式进行等价关系比较的 trait。这意味着,除了 a == ba != b 严格可逆比较外,等式必须为(对于所有 abc 而言):

  • 自反:a == a
  • 对称:a == b 意味着 b == a;以及
  • 等量代换:a == bb == c 意味着 a == c

PartialEq 是对等式部分进行等价关系比较的 trait。对于没有完全等价关系的类型,实现此 trait 允许部分相等。例如,在浮点数中,NaN != NaN,所以浮点类型实现 PartialEq 而不是 Eq。从形式上讲,等式必须为(对于所有 abc 而言):

  • 对称:a == b 意味着 b == a;以及
  • 等量代换:a == bb == c 意味着 a == c

Ord 是用于构成全序关系类型的 trait。如果是全序关系的排序(对于所有 abc 而言):

  • 完全非对称:a < ba == ba > b 中的一个为真;以及
  • 等量代换:a < bb < c 意味着 a < c。对于 ==>,同样具有等量代换关系。

PartialOrd 是可比较排序规则的 trait。对于所有 abc 而言,比较关系必须满足:

  • 非对称:如果 a < b,那么 !(a > b),以及 a > b 意味着 !(a < b);以及
  • 等量代换:a < bb < c 意味着 a < c。对于 ==>,同样具有等量代换关系。

还有 vec::sort,其对切片进行排序,这种排序是稳定的(即不重新排序相等的元素)。在合适的场景,不稳定排序是首选的,因为它通常比稳定排序快,并且不分配辅助内存。

5.2.4. Vector 排序 crates 小结

我们在对 Rust 语言中的 Vec 类型进行排序中,仅使用了标准库。在此,再做一些扩展介绍。但这些介绍都是概要性的,若要详细了解,请打开实例标题之下的 url,参阅 crate 官方文档。

5.2.4.1. std(The Rust Standard Library)

Rust 标准库是可移植的 Rust 软件的基础,它针对广泛的 Rust 生态系统,是其最核心的一组共享抽象,经过严格的测试和检验。

Rust 标准库提供了核心类型,如 Vec<T>Option<T>、语言原语上的库定义操作、标准宏、I/O,以及多线程等。

std 默认适用于所有 Rust crate。因此,可以通过 std 路径,在 use 语句中访问标准库,就像 use std::env 一样。

6. 命令行

参数解析实践:

  • 解析命令行参数
  • 参数解析 crates 小结

ANSI 终端实践:

  • 控制 ANSI 终端的颜色和格式
  • ANSI 终端 crates 小结

6.1. 参数解析

  • 解析命令行参数
  • 参数解析 crates 小结

6.1.1. 解析命令行参数

clap-badge cat-command-line-badge

此应用程序使用 clap 构建器样式描述其命令行界面的结构。文档还提供了另外两种可用的方法去实例化应用程序。

在构建器样式中,with_name 函数是 value_of 方法将用于检索传递值的唯一标识符。shortlong 选项控制用户将要键入的标志;short 标志看起来像 -f,long 标志看起来像 --file

use clap::{Arg, App};

fn main() {
    let matches = App::new("测试程序")
        .version("0.1.0")
        .author("李希 <llxx@example.com>")
        .about("Teaches argument parsing")
        .arg(Arg::with_name("file")
                 .short("f")
                 .long("file")
                 .takes_value(true)
                 .help("A cool file"))
        .arg(Arg::with_name("num")
                 .short("n")
                 .long("number")
                 .takes_value(true)
                 .help("Five less than your favorite number"))
        .get_matches();

    let myfile = matches.value_of("file").unwrap_or("input.txt");
    println!("The file passed is: {}", myfile);

    let num_str = matches.value_of("num");
    match num_str {
        None => println!("No idea what your favorite number is."),
        Some(s) => {
            match s.parse::<i32>() {
                Ok(n) => println!("Your favorite number must be {}.", n + 5),
                Err(_) => println!("That's not a number! {}", s),
            }
        }
    }
}

使用信息由 clap 生成。实例应用程序的用法如下所示。

测试程序 0.1.0
李希 <llxx@example.com>
Teaches argument parsing

USAGE:
    testing [OPTIONS]

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

OPTIONS:
    -f, --file <file>     A cool file
    -n, --number <num>    Five less than your favorite number

我们可以通过运行如下命令来测试应用程序。

$ cargo run -- -f myfile.txt -n 251

输出为:

The file passed is: myfile.txt
Your favorite number must be 256.

6.1.2. 参数解析 crates 小结

涉及 crates:

  • clap

6.2. ANSI 终端

  • 控制 ANSI 终端的颜色和格式
  • ANSI 终端 crates 小结

6.2.1. 控制 ANSI 终端的颜色和格式

ansi_term-badge cat-command-line-badge

此程序描述了 ansi_term crate 的使用方法,以及它如何用于控制 ANSI 终端上的颜色和格式,如蓝色粗体文本或黄色下划线文本。

ansi_term 中有两种主要的数据结构:ANSIStringStyleStyle 包含样式信息:颜色,是否粗体文本,或者是否闪烁,或者其它样式。还有 Colour 变量,代表简单的前景色样式。ANSIString 是与 Style 配对的字符串。

注意:英式英语中使用 Colour 而不是 Color,不要混淆。

打印彩色文本到终端

use ansi_term::Colour;

fn main() {
    println!("This is {} in color, {} in color and {} in color",
             Colour::Red.paint("red"),
             Colour::Blue.paint("blue"),
             Colour::Green.paint("green"));
}

终端中的粗体文本

对于比简单的前景色变化更复杂的事情,代码需要构造 Style 结构体。Style::new() 创建结构体,并链接属性。

use ansi_term::Style;

fn main() {
    println!("{} and this is not",
             Style::new().bold().paint("This is Bold"));
}

终端中的粗体和彩色文本

Colour 模块实现了许多类似 Style 的函数,并且可以链接方法。

use ansi_term::Colour;
use ansi_term::Style;

fn main(){
    println!("{}, {} and {}",
             Colour::Yellow.paint("This is colored"),
             Style::new().bold().paint("this is bold"),
             Colour::Yellow.bold().paint("this is bold and colored"));
}

6.2.2. ANSI 终端 crates 小结

涉及 crates:

  • ansi_term

7. 压缩

主要是使用 tar 包方面的实践:

  • 解压 tar 包
  • 压缩目录为 tar 包
  • 从路径移除前缀时,解压 tar 包
  • 使用 tar 包 crates 小结

7.1. 使用 tar 包

  • 解压 tar 包
  • 压缩目录为 tar 包
  • 从路径移除前缀时,解压 tar 包
  • 使用 tar 包 crates 小结

7.1.1. 解压 tar 包

flate2-badge tar-badge cat-compression-badge

从当前工作目录中的压缩包 archive.tar.gz,解压(GzDecoder)和提取(Archive::unpack)所有文件,并放在同一位置。

use std::fs::File;
use flate2::read::GzDecoder;
use tar::Archive;

fn main() -> Result<(), std::io::Error> {
    let path = "archive.tar.gz";

    let tar_gz = File::open(path)?;
    let tar = GzDecoder::new(tar_gz);
    let mut archive = Archive::new(tar);
    archive.unpack(".")?;

    Ok(())
}

7.1.2. 压缩目录为 tar 包

flate2-badge tar-badge cat-compression-badge

压缩 /var/log 目录内的内容到 archive.tar.gz 压缩包中。

创建一个用 GzEncodertar::Builder 包裹的 File

使用 Builder::append_dir_all,将 /var/log 目录内的内容递归添加到 backup/logs 路径下的归档文件中。在将数据写入压缩包 archive.tar.gz 之前,GzEncoder 负责清晰地将数据压缩。

use std::fs::File;
use flate2::Compression;
use flate2::write::GzEncoder;

fn main() -> Result<(), std::io::Error> {
    let tar_gz = File::create("archive.tar.gz")?;
    let enc = GzEncoder::new(tar_gz, Compression::default());
    let mut tar = tar::Builder::new(enc);
    tar.append_dir_all("backup/logs", "/var/log")?;
    Ok(())
}

7.1.3. 从路径移除前缀时,解压 tar 包

flate2-badge tar-badge cat-compression-badge

循环遍历 Archive::entries。使用 Path::strip_prefix 移除指定的路径前缀(bundle/logs)。最终,通过 Entry::unpack 提取 tar::Entry(tar 包中的内容)。

use error_chain::error_chain;
use std::fs::File;
use std::path::PathBuf;
use flate2::read::GzDecoder;
use tar::Archive;

error_chain! {
  foreign_links {
    Io(std::io::Error);
    StripPrefixError(::std::path::StripPrefixError);
  }
}

fn main() -> Result<()> {
    let file = File::open("archive.tar.gz")?;
    let mut archive = Archive::new(GzDecoder::new(file));
    let prefix = "bundle/logs";

    println!("Extracted the following files:");
    archive
        .entries()?
        .filter_map(|e| e.ok())
        .map(|mut entry| -> Result<PathBuf> {
            let path = entry.path()?.strip_prefix(prefix)?.to_owned();
            entry.unpack(&path)?;
            Ok(path)
        })
        .filter_map(|e| e.ok())
        .for_each(|x| println!("> {}", x.display()));

    Ok(())
}

7.1.4. 使用 tar 包 crates 小结

涉及 crates:

  • error-chain
  • flate2
  • tar

8. 并发/并行

并发/并行方面包括显式线程、数据并行。

显式线程实践:

  • 生成短期线程
  • 创建并发的数据管道
  • 在两个线程间传递数据
  • 保持全局可变状态
  • 对所有 iso 文件的 SHA256 值并发求和
  • 将绘制分形的线程分派到线程池
  • 显式线程 crates 小结

数据并行实践:

  • 并行改变数组中元素
  • 并行测试集合中任意或所有的元素是否匹配给定断言
  • 使用给定断言并行搜索项
  • 对 vector 并行排序
  • Map-reduce 并行计算
  • 并行生成 jpg 缩略图
  • 数据并行 crates 小结

8.1. 显式线程

  • 生成短期线程
  • 创建并发的数据管道
  • 在两个线程间传递数据
  • 保持全局可变状态
  • 对所有 iso 文件的 SHA256 值并发求和
  • 将绘制分形的线程分派到线程池
  • 显式线程 crates 小结

8.1.1. 生成短期线程

crossbeam-badge cat-concurrency-badge

本实例使用 crossbeam crate 为并发和并行编程提供了数据结构和函数。Scope::spawn 生成一个新的作用域线程,该线程确保传入 crossbeam::scope 函数的闭包在返回之前终止,这意味着您可以从调用的函数中引用数据。

本实例将数组一分为二,并在不同的线程中并行计算。

fn main() {
    let arr = &[1, 25, -4, 10];
    let max = find_max(arr);
    assert_eq!(max, Some(25));
}

fn find_max(arr: &[i32]) -> Option<i32> {
    const THRESHOLD: usize = 2;
  
    if arr.len() <= THRESHOLD {
        return arr.iter().cloned().max();
    }

    let mid = arr.len() / 2;
    let (left, right) = arr.split_at(mid);
  
    crossbeam::scope(|s| {
        let thread_l = s.spawn(|_| find_max(left));
        let thread_r = s.spawn(|_| find_max(right));
  
        let max_l = thread_l.join().unwrap()?;
        let max_r = thread_r.join().unwrap()?;
  
        Some(max_l.max(max_r))
    }).unwrap()
}

8.1.2. 创建并发的数据管道

crossbeam-badge cat-concurrency-badge

下面的实例使用 crossbeamcrossbeam-channel 两个 crate 创建了一个并行的管道,与 ZeroMQ 指南 中所描述的类似:管道有一个数据源和一个数据接收器,数据在从源到接收器的过程中由两个工作线程并行处理。

我们使用容量由 crossbeam_channel::bounded 分配的有界信道。生产者必须在它自己的线程上,因为它产生的消息比工作线程处理它们的速度快(因为工作线程休眠了半秒)——这意味着生产者将在对 [crossbeam_channel::Sender::send] 调用时阻塞半秒,直到其中一个工作线程对信道中的数据处理完毕。也请注意,信道中的数据由最先接收它的任何工作线程调用,因此每个消息都传递给单个工作线程,而不是传递给两个工作线程。

通过迭代器 crossbeam_channel::Receiver::iter 方法从信道读取数据,这将会造成阻塞,要么等待新消息,要么直到信道关闭。因为信道是在 crossbeam::scope 范围内创建的,我们必须通过 drop 手动关闭它们,以防止整个程序阻塞工作线程的 for 循环。你可以将对 drop 的调用视作不再发送消息的信号。

extern crate crossbeam;
extern crate crossbeam_channel;

use std::thread;
use std::time::Duration;
use crossbeam_channel::bounded;

fn main() {
    let (snd1, rcv1) = bounded(1);
    let (snd2, rcv2) = bounded(1);
    let n_msgs = 4;
    let n_workers = 2;

    crossbeam::scope(|s| {
        // 生产者线程
        s.spawn(|_| {
            for i in 0..n_msgs {
                snd1.send(i).unwrap();
                println!("Source sent {}", i);
            }
            // 关闭信道 —— 这是退出的必要条件
            // for 巡海在工作线程中
            drop(snd1);
        });

        // 由 2 个县城并行处理
        for _ in 0..n_workers {
            // 从数据源发送数据到接收器,接收器接收数据
            let (sendr, recvr) = (snd2.clone(), rcv1.clone());
            // 在不同的线程中衍生工人
            s.spawn(move |_| {
            thread::sleep(Duration::from_millis(500));
                // 接收数据,直到信道关闭前
                for msg in recvr.iter() {
                    println!("Worker {:?} received {}.",
                             thread::current().id(), msg);
                    sendr.send(msg * 2).unwrap();
                }
            });
        }
        // 关闭信道,否则接收器不会关闭
        // 退出 for 循坏
        drop(snd2);

        // 接收器
        for msg in rcv2.iter() {
            println!("Sink received {}", msg);
        }
    }).unwrap();
}

8.1.3. 在两个线程间传递数据

crossbeam-badge cat-concurrency-badge

这个实例示范了在单生产者、单消费者(SPSC)环境中使用 crossbeam-channel。我们构建的生成短期线程实例中,使用 crossbeam::scopeScope::spawn 来管理生产者线程。在两个线程之间,使用 crossbeam_channel::unbounded 信道交换数据,这意味着可存储消息的数量没有限制。生产者线程在消息之间休眠半秒。

use std::{thread, time};
use crossbeam_channel::unbounded;

fn main() {
    let (snd, rcv) = unbounded();
    let n_msgs = 5;
    crossbeam::scope(|s| {
        s.spawn(|_| {
            for i in 0..n_msgs {
                snd.send(i).unwrap();
                thread::sleep(time::Duration::from_millis(100));
            }
        });
    }).unwrap();
    for _ in 0..n_msgs {
        let msg = rcv.recv().unwrap();
        println!("Received {}", msg);
    }
}

8.1.4. 保持全局可变状态

lazy_static-badge cat-rust-patterns-badge

使用 lazy_static 声明全局状态。lazy_static 创建了一个全局可用的 static ref,它需要 Mutex 来允许变化(请参阅 RwLock)。在 Mutex 的包裹下,保证了状态不能被多个线程同时访问,从而防止出现争用情况。必须获取 MutexGuard,方可读取或更改存储在 Mutex 中的值。

use error_chain::error_chain;
use lazy_static::lazy_static;
use std::sync::Mutex;

error_chain!{ }

lazy_static! {
    static ref FRUIT: Mutex<Vec<String>> = Mutex::new(Vec::new());
}

fn insert(fruit: &str) -> Result<()> {
    let mut db = FRUIT.lock().map_err(|_| "Failed to acquire MutexGuard")?;
    db.push(fruit.to_string());
    Ok(())
}

fn main() -> Result<()> {
    insert("apple")?;
    insert("orange")?;
    insert("peach")?;
    {
        let db = FRUIT.lock().map_err(|_| "Failed to acquire MutexGuard")?;

        db.iter().enumerate().for_each(|(i, item)| println!("{}: {}", i, item));
    }
    insert("grape")?;
    Ok(())
}

8.1.5. 对所有 iso 文件的 SHA256 值并发求和

threadpool-badge num_cpus-badge walkdir-badge ring-badge cat-concurrency-badgecat-filesystem-badge

下面的实例计算了当前目录中每个扩展名为 iso 的文件的 SHA256 哈希值。线程池生成的线程数与使用 num_cpus::get 获取的系统内核数相等。Walkdir::new 遍历当前目录,并调用 execute 来执行读取和计算 SHA256 哈希值的操作。

use walkdir::WalkDir;
use std::fs::File;
use std::io::{BufReader, Read, Error};
use std::path::Path;
use threadpool::ThreadPool;
use std::sync::mpsc::channel;
use ring::digest::{Context, Digest, SHA256};

// Verify the iso extension
fn is_iso(entry: &Path) -> bool {
    match entry.extension() {
        Some(e) if e.to_string_lossy().to_lowercase() == "iso" => true,
        _ => false,
    }
}

fn compute_digest<P: AsRef<Path>>(filepath: P) -> Result<(Digest, P), Error> {
    let mut buf_reader = BufReader::new(File::open(&filepath)?);
    let mut context = Context::new(&SHA256);
    let mut buffer = [0; 1024];

    loop {
        let count = buf_reader.read(&mut buffer)?;
        if count == 0 {
            break;
        }
        context.update(&buffer[..count]);
    }

    Ok((context.finish(), filepath))
}

fn main() -> Result<(), Error> {
    let pool = ThreadPool::new(num_cpus::get());

    let (tx, rx) = channel();

    for entry in WalkDir::new("/home/user/Downloads")
        .follow_links(true)
        .into_iter()
        .filter_map(|e| e.ok())
        .filter(|e| !e.path().is_dir() && is_iso(e.path())) {
            let path = entry.path().to_owned();
            let tx = tx.clone();
            pool.execute(move || {
                let digest = compute_digest(path);
                tx.send(digest).expect("Could not send data!");
            });
        }

    drop(tx);
    for t in rx.iter() {
        let (sha, path) = t?;
        println!("{:?} {:?}", sha, path);
    }
    Ok(())
}

8.1.6. 将绘制分形的线程分派到线程池

threadpool-badge num-badge num_cpus-badge image-badge cat-concurrency-badgecat-science-badgecat-rendering-badge

此实例通过从朱莉娅集绘制分形来生成图像,该集合具有用于分布式计算的线程池。

使用 ImageBuffer::new 为指定宽度和高度的输出图像分配内存,Rgb::from_channels 信道则计算输出图像的 RGB 像素值。使用 ThreadPool 创建线程池,线程池中的线程数量和使用 num_cpus::get 获取的系统内核数相等。ThreadPool::execute 将每个像素作为单独的作业接收。

mpsc::channel 信道接收作业,Receiver::recv 接收器则检索作业。ImageBuffer::put_pixel 处理数据,设置像素颜色。最后,ImageBuffer::save 将图像存储为 output.png

use error_chain::error_chain;
use std::sync::mpsc::{channel, RecvError};
use threadpool::ThreadPool;
use num::complex::Complex;
use image::{ImageBuffer, Pixel, Rgb};

error_chain! {
    foreign_links {
        MpscRecv(RecvError);
        Io(std::io::Error);
    }
}

// 将强度值转换为 RGB 值的函数
// 基于 http://www.efg2.com/Lab/ScienceAndEngineering/Spectra.htm
fn wavelength_to_rgb(wavelength: u32) -> Rgb<u8> {
    let wave = wavelength as f32;

    let (r, g, b) = match wavelength {
        380..=439 => ((440. - wave) / (440. - 380.), 0.0, 1.0),
        440..=489 => (0.0, (wave - 440.) / (490. - 440.), 1.0),
        490..=509 => (0.0, 1.0, (510. - wave) / (510. - 490.)),
        510..=579 => ((wave - 510.) / (580. - 510.), 1.0, 0.0),
        580..=644 => (1.0, (645. - wave) / (645. - 580.), 0.0),
        645..=780 => (1.0, 0.0, 0.0),
        _ => (0.0, 0.0, 0.0),
    };

    let factor = match wavelength {
        380..=419 => 0.3 + 0.7 * (wave - 380.) / (420. - 380.),
        701..=780 => 0.3 + 0.7 * (780. - wave) / (780. - 700.),
        _ => 1.0,
    };

    let (r, g, b) = (normalize(r, factor), normalize(g, factor), normalize(b, factor));
    Rgb::from_channels(r, g, b, 0)
}

// 将茱莉亚集距离映射为强度值
fn julia(c: Complex<f32>, x: u32, y: u32, width: u32, height: u32, max_iter: u32) -> u32 {
    let width = width as f32;
    let height = height as f32;

    let mut z = Complex {
        // scale and translate the point to image coordinates
        re: 3.0 * (x as f32 - 0.5 * width) / width,
        im: 2.0 * (y as f32 - 0.5 * height) / height,
    };

    let mut i = 0;
    for t in 0..max_iter {
        if z.norm() >= 2.0 {
            break;
        }
        z = z * z + c;
        i = t;
    }
    i
}

// 规格 RGB 颜色值范围内的强度值
fn normalize(color: f32, factor: f32) -> u8 {
    ((color * factor).powf(0.8) * 255.) as u8
}

fn main() -> Result<()> {
    let (width, height) = (1920, 1080);
    let mut img = ImageBuffer::new(width, height);
    let iterations = 300;

    let c = Complex::new(-0.8, 0.156);

    let pool = ThreadPool::new(num_cpus::get());
    let (tx, rx) = channel();

    for y in 0..height {
        let tx = tx.clone();
        pool.execute(move || for x in 0..width {
                         let i = julia(c, x, y, width, height, iterations);
                         let pixel = wavelength_to_rgb(380 + i * 400 / iterations);
                         tx.send((x, y, pixel)).expect("Could not send data!");
                     });
    }

    for _ in 0..(width * height) {
        let (x, y, pixel) = rx.recv()?;
        img.put_pixel(x, y, pixel);
    }
    let _ = img.save("output.png")?;
    Ok(())
}

8.1.7. 显式线程 crates 小结

涉及 crates:

  • crossbeam
  • crossbeam-channel
  • error-chain
  • image
  • lazy_static
  • num
  • num_cpus
  • ring
  • threadpool
  • walkdir

8.2. 数据并行

  • 并行改变数组中元素
  • 并行测试集合中任意或所有的元素是否匹配给定断言
  • 使用给定断言并行搜索项
  • 对 vector 并行排序
  • Map-reduce 并行计算
  • 并行生成 jpg 缩略图
  • 数据并行 crates 小结

8.2.1. 并行改变数组中元素

rayon-badge cat-concurrency-badge

下面的实例使用了 rayon crate,这是一个 Rust 程序设计语言的数据并行库。rayon 为任何并行可迭代的数据类型提供 par_iter_mut 方法。这是一个类迭代器的链,可以对链内的数据并行计算。

use rayon::prelude::*;

fn main() {
    let mut arr = [0, 7, 9, 11];
    arr.par_iter_mut().for_each(|p| *p -= 1);
    println!("{:?}", arr);
}

8.2.2. 并行测试集合中任意或所有的元素是否匹配给定断言

rayon-badge cat-concurrency-badge

这个实例示范如何使用 rayon::anyrayon::all 方法,这两个方法是分别与 std::anystd::all 相对应的并行方法。rayon::any 并行检查迭代器的任意元素是否与断言匹配,并在找到一个匹配的元素时就返回。rayon::all 并行检查迭代器的所有元素是否与断言匹配,并在找到不匹配的元素时立即返回。

use rayon::prelude::*;

fn main() {
    let mut vec = vec![2, 4, 6, 8];

    assert!(!vec.par_iter().any(|n| (*n % 2) != 0));
    assert!(vec.par_iter().all(|n| (*n % 2) == 0));
    assert!(!vec.par_iter().any(|n| *n > 8 ));
    assert!(vec.par_iter().all(|n| *n <= 8 ));

    vec.push(9);

    assert!(vec.par_iter().any(|n| (*n % 2) != 0));
    assert!(!vec.par_iter().all(|n| (*n % 2) == 0));
    assert!(vec.par_iter().any(|n| *n > 8 ));
    assert!(!vec.par_iter().all(|n| *n <= 8 )); 
}

8.2.3. 使用给定断言并行搜索项

rayon-badge cat-concurrency-badge

下面的实例使用 rayon::find_anypar_iter 并行搜索 vector 集合,以查找满足指定闭包中的断言的元素。

如果有多个元素满足 rayon::find_any 闭包参数中定义的断言,rayon 将返回搜索发现的第一个元素,但不一定是 vector 集合的第一个元素。

请注意,实例中闭包的参数是对引用的引用(&&x)。有关更多详细信息,请参阅关于 std::find 的讨论。

use rayon::prelude::*;

fn main() {
    let v = vec![6, 2, 1, 9, 3, 8, 11];

    let f1 = v.par_iter().find_any(|&&x| x == 9);
    let f2 = v.par_iter().find_any(|&&x| x % 2 == 0 && x > 6);
    let f3 = v.par_iter().find_any(|&&x| x > 8);

    assert_eq!(f1, Some(&9));
    assert_eq!(f2, Some(&8));
    assert!(f3 > Some(&8));
}

8.2.5. 对 vector 并行排序

rayon-badge rand-badge cat-concurrency-badge

本实例对字符串 vector 并行排序。

首先,分配空字符串 vector;然后,通过 par_iter_mut().for_each 并行对 vector 填充随机值。尽管存在多种选择,可以对可枚举数据类型进行排序,但 par_sort_unstable 通常比稳定排序(相同的值排序后相对顺序不变)算法快。

use rand::{Rng, thread_rng};
use rand::distributions::Alphanumeric;
use rayon::prelude::*;

fn main() {
  let mut vec = vec![String::new(); 100_000];
  vec.par_iter_mut().for_each(|p| {
    let mut rng = thread_rng();
    *p = (0..5).map(|_| rng.sample(&Alphanumeric)).collect()
  });
  vec.par_sort_unstable();
}

8.2.5. Map-reduce 并行计算

rayon-badge cat-concurrency-badge

此实例使用 rayon::filterrayon::map,以及 rayon::reduce 计算 Person 对象中年龄超过 30 岁的那些人的平均年龄。

rayon::filter 过滤集合中满足给定断言的元素。rayon::map 对每个元素执行一次计算,创建一个新的迭代;然后,基于前一次的 reduce 计算结果和当前元素一起,rayon::reduce 执行新的计算。也可以查看 rayon::sum,它与本实例中的 reduce 计算具有相同的结果。

use rayon::prelude::*;

struct Person {
    age: u32,
}

fn main() {
    let v: Vec<Person> = vec![
        Person { age: 23 },
        Person { age: 19 },
        Person { age: 42 },
        Person { age: 17 },
        Person { age: 17 },
        Person { age: 31 },
        Person { age: 30 },
    ];

    let num_over_30 = v.par_iter().filter(|&x| x.age > 30).count() as f32;
    let sum_over_30 = v.par_iter()
        .map(|x| x.age)
        .filter(|&x| x > 30)
        .reduce(|| 0, |x, y| x + y);

    let alt_sum_30: u32 = v.par_iter()
        .map(|x| x.age)
        .filter(|&x| x > 30)
        .sum();

    let avg_over_30 = sum_over_30 as f32 / num_over_30;
    let alt_avg_over_30 = alt_sum_30 as f32/ num_over_30;

    assert!((avg_over_30 - alt_avg_over_30).abs() < std::f32::EPSILON);
    println!("The average age of people older than 30 is {}", avg_over_30);
}

8.2.6. 并行生成 jpg 缩略图

rayon-badge glob-badge image-badge cat-concurrency-badge cat-filesystem-badge

本实例为当前目录中的所有 .jpg 图像文件生成缩略图,然后将生成的缩略图保存在一个名为 thumbnails 的新文件夹中。

glob::glob_with 在当前目录中查找 jpeg 图像文件,rayon 通过 par_iter 方法调用 DynamicImage::resize,并行地调整图像大小。

use error_chain::error_chain;

use std::path::Path;
use std::fs::create_dir_all;

use error_chain::ChainedError;
use glob::{glob_with, MatchOptions};
use image::{FilterType, ImageError};
use rayon::prelude::*;

error_chain! {
    foreign_links {
        Image(ImageError);
        Io(std::io::Error);
        Glob(glob::PatternError);
    }
}

fn main() -> Result<()> {
    let options: MatchOptions = Default::default();
    let files: Vec<_> = glob_with("*.jpg", options)?
        .filter_map(|x| x.ok())
        .collect();

    if files.len() == 0 {
        error_chain::bail!("No .jpg files found in current directory");
    }

    let thumb_dir = "thumbnails";
    create_dir_all(thumb_dir)?;

    println!("Saving {} thumbnails into '{}'...", files.len(), thumb_dir);

    let image_failures: Vec<_> = files
        .par_iter()
        .map(|path| {
            make_thumbnail(path, thumb_dir, 300)
                .map_err(|e| e.chain_err(|| path.display().to_string()))
        })
        .filter_map(|x| x.err())
        .collect();

    image_failures.iter().for_each(|x| println!("{}", x.display_chain()));

    println!("{} thumbnails saved successfully", files.len() - image_failures.len());
    Ok(())
}

fn make_thumbnail<PA, PB>(original: PA, thumb_dir: PB, longest_edge: u32) -> Result<()>
where
    PA: AsRef<Path>,
    PB: AsRef<Path>,
{
    let img = image::open(original.as_ref())?;
    let file_path = thumb_dir.as_ref().join(original);

    Ok(img.resize(longest_edge, longest_edge, FilterType::Nearest)
        .save(file_path)?)
}

8.2.7. 数据并行 crates 小结

涉及 crates:

  • error-chain
  • glob
  • image
  • rand
  • rayon

9. 密码学

密码学实践涵盖散列(哈希)、加密。

散列(哈希)实践:

  • 计算文件的 SHA-256 摘要
  • 使用 HMAC 摘要对消息进行签名和验证
  • 散列(哈希)crates 小结

加密实践:

  • 使用 PBKDF2 对密码进行加密(salt)和散列(hash)运算
  • 加密 crates 小结

9.1. 散列(哈希)

  • 计算文件的 SHA-256 摘要
  • 使用 HMAC 摘要对消息进行签名和验证
  • 散列(哈希)crates 小结

9.1.1. 计算文件的 SHA-256 摘要

ring-badge data-encoding-badge cat-cryptography-badge

如下实例中,先创建文件,写入一些数据。然后使用 digest::Context 计算文件内容的 SHA-256 摘要 digest::Digest

use error_chain::error_chain;
use data_encoding::HEXUPPER;
use ring::digest::{Context, Digest, SHA256};
use std::fs::File;
use std::io::{BufReader, Read, Write};

error_chain! {
    foreign_links {
        Io(std::io::Error);
        Decode(data_encoding::DecodeError);
    }
}

fn sha256_digest<R: Read>(mut reader: R) -> Result<Digest> {
    let mut context = Context::new(&SHA256);
    let mut buffer = [0; 1024];

    loop {
        let count = reader.read(&mut buffer)?;
        if count == 0 {
            break;
        }
        context.update(&buffer[..count]);
    }

    Ok(context.finish())
}

fn main() -> Result<()> {
    let path = "file.txt";

    let mut output = File::create(path)?;
    write!(output, "We will generate a digest of this text")?;

    let input = File::open(path)?;
    let reader = BufReader::new(input);
    let digest = sha256_digest(reader)?;

    println!("SHA-256 digest is {}", HEXUPPER.encode(digest.as_ref()));

    Ok(())
}

9.1.2. 使用 HMAC 摘要对消息进行签名和验证

ring-badge cat-cryptography-badge

使用 ring::hmac 创建字符串的签名 hmac::Signature,然后验证签名是否正确。

use ring::{hmac, rand};
use ring::rand::SecureRandom;
use ring::error::Unspecified;

fn main() -> Result<(), Unspecified> {
    let mut key_value = [0u8; 48];
    let rng = rand::SystemRandom::new();
    rng.fill(&mut key_value)?;
    let key = hmac::Key::new(hmac::HMAC_SHA256, &key_value);

    let message = "Legitimate and important message.";
    println!("{}", &message);

    let signature = hmac::sign(&key, message.as_bytes());
    println!("{:?}", &signature);

    hmac::verify(&key, message.as_bytes(), signature.as_ref())?;
    
    Ok(())
}

9.1.3. 散列(哈希)crates 小结

涉及 crates:

  • data-encoding
  • error-chain
  • ring

9.2. 加密

  • 使用 PBKDF2 对密码进行加密(salt)和散列(hash)运算
  • 加密 crates 小结

9.2.1. 使用 PBKDF2 对密码进行加密(salt)和散列(hash)运算

ring-badge data-encoding-badge cat-cryptography-badge

对于通过 PBKDF2 密钥派生函数 pbkdf2::derive 生成的加密(加盐算法)密码,使用 ring::pbkdf2 进行散列(哈希)运算,使用 pbkdf2::verify 验证散列(哈希)运算是否正确。salt 值是使用 SecureRandom::fill 生成的,salt 字节数组被其安全生成的随机数填充。

use data_encoding::HEXUPPER;
use ring::rand::SecureRandom;
use ring::{digest, pbkdf2, rand};
use ring::error::Unspecified;
use std::num::NonZeroU32;

fn main() -> Result<(), Unspecified> {
    const CREDENTIAL_LEN: usize = digest::SHA512_OUTPUT_LEN;
    let n_iter = NonZeroU32::new(100_000).unwrap();
    let rng = rand::SystemRandom::new();

    // let mut salt = salt("budshome");
    let mut salt = [0u8; CREDENTIAL_LEN];
    rng.fill(&mut salt)?;

    let password = "Guess Me If You Can,猜猜我是谁";
    let mut pbkdf2_hash = [0u8; CREDENTIAL_LEN];
    pbkdf2::derive(
        pbkdf2::PBKDF2_HMAC_SHA512,
        n_iter,
        &salt,
        password.as_bytes(),
        &mut pbkdf2_hash,
    );
    println!("Salt: {:?}", &salt);
    println!("Salt: {}", HEXUPPER.encode(&salt));

    println!("PBKDF2 hash: {:?}", &pbkdf2_hash);
    println!("PBKDF2 hash: {}", HEXUPPER.encode(&pbkdf2_hash));

    let should_succeed = pbkdf2::verify(
        pbkdf2::PBKDF2_HMAC_SHA512,
        n_iter,
        &salt,
        password.as_bytes(),
        &pbkdf2_hash,
    );
    let wrong_password = "Definitely not the correct password";
    let should_fail = pbkdf2::verify(
        pbkdf2::PBKDF2_HMAC_SHA512,
        n_iter,
        &salt,
        wrong_password.as_bytes(),
        &pbkdf2_hash,
    );

    assert!(should_succeed.is_ok());
    assert!(!should_fail.is_ok());

    Ok(())
}

// fn salt(username: &str) -> Vec<u8> {
//     let mut salt = Vec::with_capacity(username.as_bytes().len());
//     salt.extend(username.as_bytes());
//     salt
// }

9.2.2. 加密 crates 小结

涉及 crates:

  • data-encoding
  • ring

10. 数据结构

数据结构目前仅涵盖位域。

位域实践:

  • 定义并操作位域风格的类型
  • 位域 crates 小结

10.1. 位域

  • 定义并操作位域风格的类型
  • 位域 crates 小结

10.1.1. 定义并操作位域风格的类型

bitflags-badge cat-no-std-badge

如下实例在 bitflags! 宏的帮助下创建类型安全的位域类型 MyFlags,并为其实现基本的清理操作(clear 方法)以及 Display trait。随后,展示了基本的按位操作和格式化。

use bitflags::bitflags;
use std::fmt;

bitflags! {
    struct MyFlags: u32 {
        const FLAG_A       = 0b00000001;
        const FLAG_B       = 0b00000010;
        const FLAG_C       = 0b00000100;
        const FLAG_ABC     = Self::FLAG_A.bits
                           | Self::FLAG_B.bits
                           | Self::FLAG_C.bits;
    }
}

impl MyFlags {
    pub fn clear(&mut self) -> &mut MyFlags {
        self.bits = 0;  
        self
    }
}

impl fmt::Display for MyFlags {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:032b}", self.bits)
    }
}

fn main() {
    let e1 = MyFlags::FLAG_A | MyFlags::FLAG_C;
    let e2 = MyFlags::FLAG_B | MyFlags::FLAG_C;
    assert_eq!((e1 | e2), MyFlags::FLAG_ABC);   
    assert_eq!((e1 & e2), MyFlags::FLAG_C);    
    assert_eq!((e1 - e2), MyFlags::FLAG_A);    
    assert_eq!(!e2, MyFlags::FLAG_A);           

    let mut flags = MyFlags::FLAG_ABC;
    assert_eq!(format!("{}", flags), "00000000000000000000000000000111");
    assert_eq!(format!("{}", flags.clear()), "00000000000000000000000000000000");
    assert_eq!(format!("{:?}", MyFlags::FLAG_B), "FLAG_B");
    assert_eq!(format!("{:?}", MyFlags::FLAG_A | MyFlags::FLAG_B), "FLAG_A | FLAG_B");
}

10.1.2. 位域 crates 小结

涉及 crates:

  • bitflags

11. 数据库

数据库实践涵盖驱动程序、ORM工具。

驱动程序实践:

  • SQLite
    • 创建 SQLite 数据库
    • 数据插入和查询
    • 事务处理
  • Postgres
    • Postgres 数据库中创建表
    • 数据插入和查询
    • 数据聚合
  • MySql
  • MongoDB
  • 驱动程序 crates 小结

ORM 工具实践:

  • diesel
  • rbatis
  • rustorm
  • sqlx
  • ORM 工具 crates 小结

11.1. 驱动程序

  • SQLite
    • 创建 SQLite 数据库
    • 数据插入和查询
    • 事务处理
  • Postgres
    • Postgres 数据库中创建表
    • 数据插入和查询
    • 数据聚合
  • MySql
  • MongoDB
  • 驱动程序 crates 小结

11.1.1. SQLite

  • 创建 SQLite 数据库
  • 数据插入和查询
  • 事务处理

11.1.1.1. 创建 SQLite 数据库

rusqlite-badge cat-database-badge

使用 rusqlite crate 打开 SQLite 数据库连接。Windows 上编译 rusqlite crate 请参考文档

如果数据库不存在,Connection::open 方法将创建它。

use rusqlite::{Connection, Result};
use rusqlite::NO_PARAMS;

fn main() -> Result<()> {
    let conn = Connection::open("cats.db")?;

    conn.execute(
        "create table if not exists cat_colors (
             id integer primary key,
             name text not null unique
         )",
        NO_PARAMS,
    )?;
    conn.execute(
        "create table if not exists cats (
             id integer primary key,
             name text not null,
             color_id integer not null references cat_colors(id)
         )",
        NO_PARAMS,
    )?;

    Ok(())
}

11.1.1.2. 数据插入和查询

rusqlite-badge cat-database-badge

Connection::open 将打开在前一章节实例中创建的数据库 cats 的连接。下面的实例使用 Connectionexecute 方法将数据插入 cat_colorscats 两张表中。首先,将数据插入到 cat_colors 表中。随后,使用 Connectionlast_insert_rowid 方法来获取 cat_colors 表最后插入记录的 id。当向 cats 表中插入数据时,使用此 id。然后,使用 prepare 方法准备执行 select 查询操作,该方法提供 statement 结构体。最后,使用 statementquery_map 方法执行查询。

use rusqlite::NO_PARAMS;
use rusqlite::{Connection, Result};
use std::collections::HashMap;

#[derive(Debug)]
struct Cat {
    name: String,
    color: String,
}

fn main() -> Result<()> {
    let conn = Connection::open("cats.db")?;

    let mut cat_colors = HashMap::new();
    cat_colors.insert(String::from("Blue"), vec!["Tigger", "Sammy"]);
    cat_colors.insert(String::from("Black"), vec!["Oreo", "Biscuit"]);

    for (color, catnames) in &cat_colors {
        conn.execute(
            "INSERT INTO cat_colors (name) values (?1)",
            &[&color.to_string()],
        )?;
        let last_id: String = conn.last_insert_rowid().to_string();

        for cat in catnames {
            conn.execute(
                "INSERT INTO cats (name, color_id) values (?1, ?2)",
                &[&cat.to_string(), &last_id],
            )?;
        }
    }
    let mut stmt = conn.prepare(
        "SELECT c.name, cc.name from cats c
         INNER JOIN cat_colors cc
         ON cc.id = c.color_id;",
    )?;

    let cats = stmt.query_map(NO_PARAMS, |row| {
        Ok(Cat {
            name: row.get(0)?,
            color: row.get(1)?,
        })
    })?;

    for cat in cats {
        println!("Found cat {:?}", cat);
    }

    Ok(())
}

11.1.1.3. 事务处理

rusqlite-badge cat-database-badge

[Connection::open] 将打开来自前述实例的数据库 cats.db

使用 Connection::transaction 开始事务,除非使用 Transaction::commit 显式提交,否则事务将回滚。

在下面的实例中,颜色表对颜色名称具有唯一性约束。当尝试插入重复的颜色时,事务会回滚。

use rusqlite::{Connection, Result, NO_PARAMS};

fn main() -> Result<()> {
    let mut conn = Connection::open("cats.db")?;

    successful_tx(&mut conn)?;

    let res = rolled_back_tx(&mut conn);
    assert!(res.is_err());

    Ok(())
}

fn successful_tx(conn: &mut Connection) -> Result<()> {
    let tx = conn.transaction()?;

    tx.execute("delete from cat_colors", NO_PARAMS)?;
    tx.execute("insert into cat_colors (name) values (?1)", &[&"lavender"])?;
    tx.execute("insert into cat_colors (name) values (?1)", &[&"blue"])?;

    tx.commit()
}

fn rolled_back_tx(conn: &mut Connection) -> Result<()> {
    let tx = conn.transaction()?;

    tx.execute("delete from cat_colors", NO_PARAMS)?;
    tx.execute("insert into cat_colors (name) values (?1)", &[&"lavender"])?;
    tx.execute("insert into cat_colors (name) values (?1)", &[&"blue"])?;
    tx.execute("insert into cat_colors (name) values (?1)", &[&"lavender"])?;

    tx.commit()
}

11.1.2. Postgres

  • Postgres 数据库中创建表
  • 数据插入和查询
  • 数据聚合

11.1.2.1. Postgres 数据库中创建表

postgres-badge cat-database-badge

Postgres 数据库中,使用 postgres crate 创建表。

Client::connect 用于连接到现有数据库。本实例中使用 Client::connect 格式化连接数据库的 URL 字符串。假设存在一个数据库:名为 library,用户名为 postgres,密码为 postgres

use postgres::{Client, NoTls, Error};

fn main() -> Result<(), Error> {
    let mut client = Client::connect("postgresql://postgres:postgres@localhost/library", NoTls)?;
    
    client.batch_execute("
        CREATE TABLE IF NOT EXISTS author (
            id              SERIAL PRIMARY KEY,
            name            VARCHAR NOT NULL,
            country         VARCHAR NOT NULL
            )
    ")?;

    client.batch_execute("
        CREATE TABLE IF NOT EXISTS book  (
            id              SERIAL PRIMARY KEY,
            title           VARCHAR NOT NULL,
            author_id       INTEGER NOT NULL REFERENCES author
            )
    ")?;

    Ok(())

}

11.1.2.2. 数据插入和查询

postgres-badge cat-database-badge

下述实例中使用 Clientexecute 方法将数据插入到 author 表中。然后,使用 Clientquery 方法查询 author 表中的数据。

use postgres::{Client, NoTls, Error};
use std::collections::HashMap;

struct Author {
    _id: i32,
    name: String,
    country: String
}

fn main() -> Result<(), Error> {
    let mut client = Client::connect("postgresql://postgres:postgres@localhost/library", 
                                    NoTls)?;
    
    let mut authors = HashMap::new();
    authors.insert(String::from("Chinua Achebe"), "Nigeria");
    authors.insert(String::from("Rabindranath Tagore"), "India");
    authors.insert(String::from("Anita Nair"), "India");

    for (key, value) in &authors {
        let author = Author {
            _id: 0,
            name: key.to_string(),
            country: value.to_string()
        };

        client.execute(
                "INSERT INTO author (name, country) VALUES ($1, $2)",
                &[&author.name, &author.country],
        )?;
    }

    for row in client.query("SELECT id, name, country FROM author", &[])? {
        let author = Author {
            _id: row.get(0),
            name: row.get(1),
            country: row.get(2),
        };
        println!("Author {} is from {}", author.name, author.country);
    }

    Ok(())

}

11.1.2.3. 数据聚合

postgres-badge cat-database-badge

下述实例按照降序列出了美国纽约州现代艺术博物馆数据库中首批 7999 位艺术家的国籍。

use postgres::{Client, Error, NoTls};

struct Nation {
    nationality: String,
    count: i64,
}

fn main() -> Result<(), Error> {
    let mut client = Client::connect(
        "postgresql://postgres:postgres@127.0.0.1/moma",
        NoTls,
    )?;

    for row in client.query 
	("SELECT nationality, COUNT(nationality) AS count 
	FROM artists GROUP BY nationality ORDER BY count DESC", &[])? {
        
        let (nationality, count) : (Option<String>, Option<i64>) 
		= (row.get (0), row.get (1));
        
        if nationality.is_some () && count.is_some () {

            let nation = Nation{
                nationality: nationality.unwrap(),
                count: count.unwrap(),
        };
            println!("{} {}", nation.nationality, nation.count);
            
        }
    }

    Ok(())
}

11.1.3. MySql

分同步和异步 2 个crates。

  • mysql
  • mysql-async

暂先请参阅官网。

11.1.4. MongoDB

MongoDB 天生对异步支持较为全面。

  • mongodb(官方 crates)
  • wither

暂先请参阅官网。

11.1.5. 驱动程序 crates 小结

涉及 crates:

  • postgres
  • rusqlite

11.2. ORM 工具

  • diesel
  • rbatis
  • rustorm
  • sqlx
  • ORM 工具 crates 小结

11.2.1. diesel

暂先请参阅官网。

11.2.2. rbatis

暂先请参阅官网。

11.2.3. rustorm

暂先请参阅官网。

11.2.4. sqlx

暂先请参阅官网。

11.2.5. ORM 工具 crates 小结

涉及 crates:

  • diesel
  • rbatis
  • rustorm
  • sqlx

12. 日期及时间

日期及时间实践涵盖期间和计算、解析与显示。

期间和计算实践:

  • 测量运行时间
  • 执行日期检查和时间计算
  • 时间的时区转换

解析与显示实践:

  • 检查日期和时间
  • 日期和 UNIX 时间戳的互相转换
  • 日期和时间的格式化显示
  • 将字符串解析为 DateTime 结构体

以及,日期及时间 crates 小结

12.1. 期间和计算

  • 测量运行时间
  • 执行日期检查和时间计算
  • 时间的时区转换

12.1.1. 测量运行时间

std-badge cat-time-badge

测量从 time::Instant::now 开始运行的时间 time::Instant::elapsed

调用 time::Instant::elapsed 将返回 time::Duration,我们将在实例末尾打印该时间。此方法不会更改或者重置 time::Instant 对象。

use std::time::{Duration, Instant};
use std::thread;

fn expensive_function() {
    thread::sleep(Duration::from_secs(1));
}

fn main() {
    let start = Instant::now();
    expensive_function();
    let duration = start.elapsed();

    println!("Time elapsed in expensive_function() is: {:?}", duration);
}

12.1.2. 执行日期检查和时间计算

chrono-badge cat-date-and-time-badge

使用 DateTime::checked_add_signed 计算并显示两周之后的日期和时间,使用 DateTime::checked_sub_signed 计算并显示前一天的日期。如果无法计算出日期和时间,这些方法将返回 None。

可以在 chrono::format::strftime 中找到适用于 DateTime::format 的转义序列。

use chrono::{DateTime, Duration, Utc};

fn day_earlier(date_time: DateTime<Utc>) -> Option<DateTime<Utc>> {
    date_time.checked_sub_signed(Duration::days(1))
}

fn main() {
    let now = Utc::now();
    println!("{}", now);

    let almost_three_weeks_from_now = now.checked_add_signed(Duration::weeks(2))
            .and_then(|in_2weeks| in_2weeks.checked_add_signed(Duration::weeks(1)))
            .and_then(day_earlier);

    match almost_three_weeks_from_now {
        Some(x) => println!("{}", x),
        None => eprintln!("Almost three weeks from now overflows!"),
    }

    match now.checked_add_signed(Duration::max_value()) {
        Some(x) => println!("{}", x),
        None => eprintln!("We can't use chrono to tell the time for the Solar System to complete more than one full orbit around the galactic center."),
    }
}

12.1.3. 时间的时区转换

chrono-badge cat-date-and-time-badge

使用 offset::Local::now 获取本地时间并显示,然后使用 DateTime::from_utc 结构体方法将其转换为 UTC 标准格式。最后,使用 offset::FixedOffset 结构体,可以将 UTC 时间转换为 UTC+8 和 UTC-2。

use chrono::{DateTime, FixedOffset, Local, Utc};

fn main() {
    let local_time = Local::now();
    let utc_time = DateTime::<Utc>::from_utc(local_time.naive_utc(), Utc);
    let china_timezone = FixedOffset::east(8 * 3600);
    let rio_timezone = FixedOffset::west(2 * 3600);
    println!("Local time now is {}", local_time);
    println!("UTC time now is {}", utc_time);
    println!(
        "Time in Hong Kong now is {}",
        utc_time.with_timezone(&china_timezone)
    );
    println!("Time in Rio de Janeiro now is {}", utc_time.with_timezone(&rio_timezone));
}

12.2. 解析与显示

  • 检查日期和时间
  • 日期和 UNIX 时间戳的互相转换
  • 日期和时间的格式化显示
  • 将字符串解析为 DateTime 结构体

12.2.1. 检查日期和时间

chrono-badge cat-date-and-time-badge

通过 Timelike 获取当前 UTC DateTime 及其时/分/秒,通过 Datelike 获取其年/月/日/工作日。

use chrono::{Datelike, Timelike, Utc};

fn main() {
    let now = Utc::now();

    let (is_pm, hour) = now.hour12();
    println!(
        "The current UTC time is {:02}:{:02}:{:02} {}",
        hour,
        now.minute(),
        now.second(),
        if is_pm { "PM" } else { "AM" }
    );
    println!(
        "And there have been {} seconds since midnight",
        now.num_seconds_from_midnight()
    );

    let (is_common_era, year) = now.year_ce();
    println!(
        "The current UTC date is {}-{:02}-{:02} {:?} ({})",
        year,
        now.month(),
        now.day(),
        now.weekday(),
        if is_common_era { "CE" } else { "BCE" }
    );
    println!(
        "And the Common Era began {} days ago",
        now.num_days_from_ce()
    );
}

12.2.2. 日期和 UNIX 时间戳的互相转换

chrono-badge cat-date-and-time-badge

使用 NaiveDateTime::timestamp 将由 NaiveDate::from_ymd 生成的日期和由 NaiveTime::from_hms 生成的时间转换为 UNIX 时间戳。然后,它使用 NaiveDateTime::from_timestamp 计算自 UTC 时间 1970 年 01 月 01 日 00:00:00 开始的 10 亿秒后的日期。

use chrono::{NaiveDate, NaiveDateTime};

fn main() {
    let date_time: NaiveDateTime = NaiveDate::from_ymd(2017, 11, 12).and_hms(17, 33, 44);
    println!(
        "Number of seconds between 1970-01-01 00:00:00 and {} is {}.",
        date_time, date_time.timestamp());

    let date_time_after_a_billion_seconds = NaiveDateTime::from_timestamp(1_000_000_000, 0);
    println!(
        "Date after a billion seconds since 1970-01-01 00:00:00 was {}.",
        date_time_after_a_billion_seconds);
}

12.2.3. 日期和时间的格式化显示

chrono-badge cat-date-and-time-badge

使用 Utc::now 获取并显示当前 UTC 时间。使用 DateTime::to_rfc2822 将当前时间格式化为熟悉的 RFC 2822 格式,使用 DateTime::to_rfc3339 将当前时间格式化为熟悉的 RFC 3339 格式,也可以使用 DateTime::format 自定义时间格式。

use chrono::{DateTime, Utc};

fn main() {
    let now: DateTime<Utc> = Utc::now();

    println!("UTC now is: {}", now);
    println!("UTC now in RFC 2822 is: {}", now.to_rfc2822());
    println!("UTC now in RFC 3339 is: {}", now.to_rfc3339());
    println!("UTC now in a custom format is: {}", now.format("%a %b %e %T %Y"));
}

12.2.4. 将字符串解析为 DateTime 结构体

chrono-badge cat-date-and-time-badge

熟悉的时间格式 RFC 2822RFC 3339,以及自定义时间格式,通常用字符串表达。要将这些字符串解析为 DateTime 结构体,可以分别用 DateTime::parse_from_rfc2822DateTime::parse_from_rfc3339,以及 DateTime::parse_from_str

可以在 chrono::format::strftime 中找到适用于 DateTime::parse_from_str 的转义序列。注意:DateTime::parse_from_str 要求这些 DateTime 结构体必须是可创建的,以便它唯一地标识日期和时间。要解析不带时区的日期和时间,请使用 NaiveDateNaiveTime,以及 NaiveDateTime

use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime};
use chrono::format::ParseError;


fn main() -> Result<(), ParseError> {
    let rfc2822 = DateTime::parse_from_rfc2822("Tue, 1 Jul 2003 10:52:37 +0200")?;
    println!("{}", rfc2822);

    let rfc3339 = DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00")?;
    println!("{}", rfc3339);

    let custom = DateTime::parse_from_str("5.8.1994 8:00 am +0000", "%d.%m.%Y %H:%M %P %z")?;
    println!("{}", custom);

    let time_only = NaiveTime::parse_from_str("23:56:04", "%H:%M:%S")?;
    println!("{}", time_only);

    let date_only = NaiveDate::parse_from_str("2015-09-05", "%Y-%m-%d")?;
    println!("{}", date_only);

    let no_timezone = NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")?;
    println!("{}", no_timezone);

    Ok(())
}

12.3. 日期及时间 crates 小结

涉及 crates:

  • chrono

13. 开发工具

开发工具涵盖调试工具、日志工具、版本控制、构建工具。

调试工具实践:

  • 日志工具
    • 记录调试信息到控制台
    • 记录错误信息到控制台
    • 记录信息时,用标准输出 stdout 替换标准错误 stderr
    • 使用自定义日志记录器记录信息
    • 记录到 Unix 系统日志
    • 启用每个模块的日志级别
    • 用自定义环境变量设置日志记录
    • 在日志信息中包含时间戳
    • 将信息记录到自定义位置
    • 日志工具 crates 小结

版本控制实践:

  • 解析并递增版本字符串
  • 解析复杂的版本字符串
  • 检查给定版本是否为预发布版本
  • 查询适配给定范围的最新版本
  • 检查外部命令的版本兼容性
  • 版本控制 crates 小结

构建工具实践:

  • 编译并静态链接到绑定的 C 语言库
  • 编译并静态链接到绑定的 C++ 语言库
  • 编译 C 语言库时自定义设置
  • 构建工具 crates 小结

13.1. 调试工具

  • 日志工具
    • 记录调试信息到控制台
    • 记录错误信息到控制台
    • 记录信息时,用标准输出 stdout 替换标准错误 stderr
    • 使用自定义日志记录器记录信息
    • 记录到 Unix 系统日志
    • 启用每个模块的日志级别
    • 用自定义环境变量设置日志记录
    • 在日志信息中包含时间戳
    • 将信息记录到自定义位置
    • 日志工具 crates 小结

13.1.1. 日志工具

日志信息实践:

  • 记录调试信息到控制台
  • 记录错误信息到控制台
  • 记录信息时,用标准输出 stdout 替换标准错误 stderr
  • 使用自定义日志记录器记录信息
  • 记录到 Unix 系统日志

日志配置实践:

  • 启用每个模块的日志级别
  • 用自定义环境变量设置日志记录
  • 在日志信息中包含时间戳
  • 将信息记录到自定义位置
  • 日志工具 crates 小结

13.1.1.1. 记录调试信息到控制台

log-badge env_logger-badge cat-debugging-badge

log crate 提供了日志工具,env_logger crate 通过环境变量配置日志记录。log::debug! 宏的工作方式类似于其它 std::fmt 格式化的字符串。

fn execute_query(query: &str) {
    log::debug!("Executing query: {}", query);
}

fn main() {
    env_logger::init();

    execute_query("DROP TABLE students");
}

运行上述代码时,并没有输出信息被打印。因为默认情况下,日志级别为 error,任何较低级别的日志信息都将被忽略。

设置 RUST_LOG 环境变量以打印消息:

$ RUST_LOG=debug cargo run

Cargo 运行后,会在输出的最后一行打印出调试信息:

DEBUG:main: Executing query: DROP TABLE students

13.1.1.2. 记录错误信息到控制台

log-badge env_logger-badge cat-debugging-badge

正确的错误处理会将异常视为错误。下述实例中,通过 log 便捷宏 log::error!,将错误记录到 stderr。

fn execute_query(_query: &str) -> Result<(), &'static str> {
    Err("I'm afraid I can't do that")
}

fn main() {
    env_logger::init();

    let response = execute_query("DROP TABLE students");
    if let Err(err) = response {
        log::error!("Failed to execute query: {}", err);
    }
}

13.1.1.3. 记录信息时,用标准输出 stdout 替换标准错误 stderr

log-badge env_logger-badge cat-debugging-badge

使用 Builder::target 创建自定义的日志记录器配置,将日志输出的目标设置为 Target::Stdout

use env_logger::{Builder, Target};

fn main() {
    Builder::new()
        .target(Target::Stdout)
        .init();

    log::error!("This error has been printed to Stdout");
}

13.1.1.4. 使用自定义日志记录器记录信息

log-badge cat-debugging-badge

本实例实现一个打印到 stdout 的自定义记录器 ConsoleLogger。为了使用日志宏,ConsoleLogger 实现了 log::Log trait,通过 log::set_logger 安置。

use log::{Record, Level, Metadata, LevelFilter, SetLoggerError};

static CONSOLE_LOGGER: ConsoleLogger = ConsoleLogger;

struct ConsoleLogger;

impl log::Log for ConsoleLogger {
  fn enabled(&self, metadata: &Metadata) -> bool {
     metadata.level() <= Level::Info
    }

    fn log(&self, record: &Record) {
        if self.enabled(record.metadata()) {
            println!("Rust says: {} - {}", record.level(), record.args());
        }
    }

    fn flush(&self) {}
}

fn main() -> Result<(), SetLoggerError> {
    log::set_logger(&CONSOLE_LOGGER)?;
    log::set_max_level(LevelFilter::Info);

    log::info!("hello log");
    log::warn!("warning");
    log::error!("oops");
    Ok(())
}

13.1.1.5. 记录到 Unix 系统日志

log-badge syslog-badge cat-debugging-badge

本实例实现将信息记录到 UNIX syslog。使用 syslog::init 初始化记录器后端。syslog::Facility 记录提交日志项分类的程序,log::LevelFilter 表示欲记录日志的等级,Option<&str> 定义应用程序名称(可选)。

#[cfg(target_os = "linux")]
#[cfg(target_os = "linux")]
use syslog::{Facility, Error};

#[cfg(target_os = "linux")]
fn main() -> Result<(), Error> {
    syslog::init(Facility::LOG_USER,
                 log::LevelFilter::Debug,
                 Some("My app name"))?;
    log::debug!("this is a debug {}", "message");
    log::error!("this is an error!");
    Ok(())
}

#[cfg(not(target_os = "linux"))]
fn main() {
    println!("So far, only Linux systems are supported.");
}

13.1.1.6. 启用每个模块的日志级别

log-badge env_logger-badge cat-debugging-badge

创建两个模块:foo 和其嵌套的 foo::bar,日志记录指令分别由 RUST_LOG 环境变量控制。

mod foo {
    mod bar {
        pub fn run() {
            log::warn!("[bar] warn");
            log::info!("[bar] info");
            log::debug!("[bar] debug");
        }
    }

    pub fn run() {
        log::warn!("[foo] warn");
        log::info!("[foo] info");
        log::debug!("[foo] debug");
        bar::run();
    }
}

fn main() {
    env_logger::init();
    log::warn!("[root] warn");
    log::info!("[root] info");
    log::debug!("[root] debug");
    foo::run();
}

RUST_LOG 环境变量控制 env_logger 的输出。模块声明采用逗号分隔各项,格式类似于 path::to::module=log_level。按如下方式运行 test 应用程序:

RUST_LOG="warn,test::foo=info,test::foo::bar=debug" ./test

将日志等级 log::Level 的默认值设置为 warn,将模块 foo 和其嵌套的模块 foo::bar 的日志等级设置为 infodebug

WARN:test: [root] warn
WARN:test::foo: [foo] warn
INFO:test::foo: [foo] info
WARN:test::foo::bar: [bar] warn
INFO:test::foo::bar: [bar] info
DEBUG:test::foo::bar: [bar] debug

13.1.1.7. 用自定义环境变量设置日志记录

log-badge env_logger-badge cat-debugging-badge

Builder 配置日志记录。

Builder::parseRUST_LOG 语法的形式解析 MY_APP_LOG 环境变量的内容。然后,Builder::init 初始化记录器。所有这些步骤通常由 env_logger::init 在内部完成。

use std::env;
use env_logger::Builder;

fn main() {
    Builder::new()
        .parse(&env::var("MY_APP_LOG").unwrap_or_default())
        .init();

    log::info!("informational message");
    log::warn!("warning message");
    log::error!("this is an error {}", "message");
}

13.1.1.8. 在日志信息中包含时间戳

log-badge env_logger-badge chrono-badge cat-debugging-badge

使用 Builder 创建自定义记录器配置。每个日志项调用 Local::now 以获取本地时区中的当前 DateTime,并使用 DateTime::formatstrftime::specifiers 来格式化最终日志中使用的时间戳。

如下实例调用 Builder::format 设置一个闭包,该闭包用时间戳、Record::level 和正文(Record::args)对每个信息文本进行格式化。

use std::io::Write;
use chrono::Local;
use env_logger::Builder;
use log::LevelFilter;

fn main() {
    Builder::new()
        .format(|buf, record| {
            writeln!(buf,
                "{} [{}] - {}",
                Local::now().format("%Y-%m-%dT%H:%M:%S"),
                record.level(),
                record.args()
            )
        })
        .filter(None, LevelFilter::Info)
        .init();

    log::warn!("warn");
    log::info!("info");
    log::debug!("debug");
}

stderr 输入将含有:

2017-05-22T21:57:06 [WARN] - warn
2017-05-22T21:57:06 [INFO] - info

13.1.1.9. 将信息记录到自定义位置

log-badge log4rs-badge cat-debugging-badge

log4rs 将日志输出配置到自定义位置。log4rs 可以使用外部 YAML 文件或生成器配置。

使用文件附加器 log4rs::append::file::FileAppender 创建日志配置,文件附加器定义日志记录的目标位置。日志配置使用 log4rs::encode::pattern 中的自定义模式进行编码,将配置项分配给 log4rs::config::Config,并设置默认的日志等级 log::LevelFilter

use error_chain::error_chain;

use log::LevelFilter;
use log4rs::append::file::FileAppender;
use log4rs::encode::pattern::PatternEncoder;
use log4rs::config::{Appender, Config, Root};

error_chain! {
    foreign_links {
        Io(std::io::Error);
        LogConfig(log4rs::config::Errors);
        SetLogger(log::SetLoggerError);
    }
}

fn main() -> Result<()> {
    let logfile = FileAppender::builder()
        .encoder(Box::new(PatternEncoder::new("{l} - {m}\n")))
        .build("log/output.log")?;

    let config = Config::builder()
        .appender(Appender::builder().build("logfile", Box::new(logfile)))
        .build(Root::builder()
                   .appender("logfile")
                   .build(LevelFilter::Info))?;

    log4rs::init_config(config)?;

    log::info!("Hello, world!");

    Ok(())
}

13.1.1.10. 日志工具 crates 小结

涉及 crates:

  • chrono
  • env_logger
  • log
  • log4rs
  • syslog

13.2. 版本控制

  • 解析并递增版本字符串
  • 解析复杂的版本字符串
  • 检查给定版本是否为预发布版本
  • 查询适配给定范围的最新版本
  • 检查外部命令的版本兼容性
  • 版本控制 crates 小结

13.2.1. 解析并递增版本字符串

semver-badge cat-config-badge

使用 Version::parse 从字符串字面量构造语义化版本 semver::Version,然后逐个递增补丁(修订)版本号、副(次要)版本号和主版本号。

注意:根据语义化版本控制规范,增加副(次要)版本号时会将补丁(修订)版本号重置为 0,增加主版本号时会将副(次要)版本号和补丁(修订)版本号都重置为 0。

use semver::{Version, SemVerError};

fn main() -> Result<(), SemVerError> {
    let mut parsed_version = Version::parse("0.2.6")?;

    assert_eq!(
        parsed_version,
        Version {
            major: 0,
            minor: 2,
            patch: 6,
            pre: vec![],
            build: vec![],
        }
    );

    parsed_version.increment_patch();
    assert_eq!(parsed_version.to_string(), "0.2.7");
    println!("New patch release: v{}", parsed_version);

    parsed_version.increment_minor();
    assert_eq!(parsed_version.to_string(), "0.3.0");
    println!("New minor release: v{}", parsed_version);

    parsed_version.increment_major();
    assert_eq!(parsed_version.to_string(), "1.0.0");
    println!("New major release: v{}", parsed_version);

    Ok(())
}

13.2.2. 解析复杂的版本字符串

semver-badge cat-config-badge

使用 Version::parse 从复杂的版本字符串构造语义化版本 semver::Version。该字符串包含语义化版本控制规范中定义的预发布和构建元数据。

需要注意的是:根据语义化版本控制规范,构建元数据是虽然被解析,但在比较版本时不考虑。换句话说,即使两个版本的构建字符串不同,但它们的版本可能是相等的。

use semver::{Identifier, Version, SemVerError};

fn main() -> Result<(), SemVerError> {
    let version_str = "1.0.49-125+g72ee7853";
    let parsed_version = Version::parse(version_str)?;

    assert_eq!(
        parsed_version,
        Version {
            major: 1,
            minor: 0,
            patch: 49,
            pre: vec![Identifier::Numeric(125)],
            build: vec![],
        }
    );
    assert_eq!(
        parsed_version.build,
        vec![Identifier::AlphaNumeric(String::from("g72ee7853"))]
    );

    let serialized_version = parsed_version.to_string();
    assert_eq!(&serialized_version, version_str);

    Ok(())
}

13.2.3. 检查给定版本是否为预发布版本

semver-badge cat-config-badge

给定两个版本,使用 is_prerelease 断言一个是预发布,另一个不是。

use semver::{Version, SemVerError};

fn main() -> Result<(), SemVerError> {
    let version_1 = Version::parse("1.0.0-alpha")?;
    let version_2 = Version::parse("1.0.0")?;

    assert!(version_1.is_prerelease());
    assert!(!version_2.is_prerelease());

    Ok(())
}

13.2.4. 查询适配给定范围的最新版本

semver-badge cat-config-badge

给定一个版本字符串 &str 的列表,查找最新的语义化版本 semver::Versionsemver::VersionReqVersionReq::matches 过滤列表,也可以展示语义化版本 semver 的预发布参数设置。

use error_chain::error_chain;

use semver::{Version, VersionReq};

error_chain! {
    foreign_links {
        SemVer(semver::SemVerError);
        SemVerReq(semver::ReqParseError);
    }
}

fn find_max_matching_version<'a, I>(version_req_str: &str, iterable: I) -> Result<Option<Version>>
where
    I: IntoIterator<Item = &'a str>,
{
    let vreq = VersionReq::parse(version_req_str)?;

    Ok(
        iterable
            .into_iter()
            .filter_map(|s| Version::parse(s).ok())
            .filter(|s| vreq.matches(s))
            .max(),
    )
}

fn main() -> Result<()> {
    assert_eq!(
        find_max_matching_version("<= 1.0.0", vec!["0.9.0", "1.0.0", "1.0.1"])?,
        Some(Version::parse("1.0.0")?)
    );

    assert_eq!(
        find_max_matching_version(
            ">1.2.3-alpha.3",
            vec![
                "1.2.3-alpha.3",
                "1.2.3-alpha.4",
                "1.2.3-alpha.10",
                "1.2.3-beta.4",
                "3.4.5-alpha.9",
            ]
        )?,
        Some(Version::parse("1.2.3-beta.4")?)
    );

    Ok(())
}

13.2.5. 检查外部命令的版本兼容性

semver-badge cat-text-processing-badge cat-os-badge

本实例使用 Command 模块运行命令 git --version,然后使用 Version::parse 将版本号解析为语义化版本 semver::VersionVersionReq::matchessemver::VersionReq 与解析的语义化版本进行比较。最终,命令输出类似于“git version x.y.z”。

use error_chain::error_chain;

use std::process::Command;
use semver::{Version, VersionReq};

error_chain! {
    foreign_links {
        Io(std::io::Error);
        Utf8(std::string::FromUtf8Error);
        SemVer(semver::SemVerError);
        SemVerReq(semver::ReqParseError);
    }
}

fn main() -> Result<()> {
    let version_constraint = "> 1.12.0";
    let version_test = VersionReq::parse(version_constraint)?;
    let output = Command::new("git").arg("--version").output()?;

    if !output.status.success() {
        error_chain::bail!("Command executed with failing error code");
    }

    let stdout = String::from_utf8(output.stdout)?;
    let version = stdout.split(" ").last().ok_or_else(|| {
        "Invalid command output"
    })?;
    let parsed_version = Version::parse(version)?;

    if !version_test.matches(&parsed_version) {
        error_chain::bail!("Command version lower than minimum supported version (found {}, need {})",
            parsed_version, version_constraint);
    }

    Ok(())
}

13.2.6. 版本控制 crates 小结

涉及 crates:

  • error-chain
  • semver

13.3. 构建工具

本章节介绍在编译 crate 源代码之前运行的“构建时”工具或代码。按照惯例,构建时代码存放在 build.rs 文件,通常称为“构建脚本”。常见的用例包括:Rust 代码生成、绑定的 C/C++/asm 代码的编译。

要获取更多信息,请阅读 Cargo(中文文档) 的[构建脚本文档][https://cargo-book.rusthub.org/reference/build-scripts.html]。

  • 编译并静态链接到绑定的 C 语言库
  • 编译并静态链接到绑定的 C++ 语言库
  • 编译 C 语言库时自定义设置
  • 构建工具 crates 小结

13.3.1. 编译并静态链接到绑定的 C 语言库

cc-badge cat-development-tools-badge

为了适应项目中需要混合 C、C++,或 asm 等语言的场景,cc crate 提供了一个简单的 API,用于将绑定的 C/C++/asm 代码编译成静态库(.a),静态库可以通过 rustc 静态链接。

下面的实例有一些绑定的 C 语言代码(src/hello.c),将从 rust 中调用它们。在编译 rust 源代码之前,Cargo.toml 中指定的“构建”文件(build.rs)预先运行。使用 cc crate,将生成一个静态库文件(本实例中为 libhello.a,请参阅 compile 文档),通过在 extern 代码块中声明外部函数签名,然后就可以从 rust 中调用该静态库。

本实例中绑定的 C 语言文件非常简单,只需要将一个源文件传递给 cc::Build。对于更复杂的构建需求,cc::Build 提供了一整套构建器方法,用于指定(包含)include路径和扩展编译器标志(flag)

Cargo.toml

[package]
name = "cc-bundled-static"
version = "0.1.0"
authors = ["zzy <ask@rusthub.org>"]
edition = "2018"

build = "build.rs"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[build-dependencies]
cc = "1.0.66"

[dependencies]
error-chain = "0.12.4"


build.rs

fn main() {
    cc::Build::new()
        .file("src/hello.c")
        .compile("hello");   // 输出 `libhello.a`
}

src/hello.c

#include <stdio.h>


void hello() {
    printf("Hello from C!\n");
}

void greet(const char* name) {
    printf("Hello, %s!\n", name);
}

src/main.rs

use error_chain::error_chain;
use std::ffi::CString;
use std::os::raw::c_char;

error_chain! {
    foreign_links {
        NulError(::std::ffi::NulError);
        Io(::std::io::Error);
    }
}
fn prompt(s: &str) -> Result<String> {
    use std::io::Write;
    print!("{}", s);
    std::io::stdout().flush()?;
    let mut input = String::new();
    std::io::stdin().read_line(&mut input)?;
    Ok(input.trim().to_string())
}

extern {
    fn hello();
    fn greet(name: *const c_char);
}

fn main() -> Result<()> {
    unsafe { hello() }
    let name = prompt("What's your name? ")?;
    let c_name = CString::new(name)?;
    unsafe { greet(c_name.as_ptr()) }
    Ok(())
}

13.3.2. 编译并静态链接到绑定的 C++ 语言库

cc-badge cat-development-tools-badge

链接绑定的 C++ 语言库非常类似于链接绑定的 C 语言库。编译并静态链接绑定的 C++ 库时,与链接绑定的 C 语言库相比有两个核心区别:一是通过构造器方法 cpp(true) 指定 C++ 编译器;二是通过在 C++ 源文件顶部添加 extern "C" 代码段,以防止 C++ 编译器的名称篡改。

Cargo.toml

[package]
name = "cc-bundled-cpp"
version = "0.1.0"
authors = ["zzy <ask@rusthub.org>"]
edition = "2018"

build = "build.rs"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[build-dependencies]
cc = "1.0.66"

[dependencies]


build.rs

fn main() {
    cc::Build::new()
        .cpp(true)
        .file("src/foo.cpp")
        .compile("foo");   
}

src/foo.cpp

extern "C" {
    int multiply(int x, int y);
}

int multiply(int x, int y) {
    return x*y;
}

src/main.rs

extern {
    fn multiply(x : i32, y : i32) -> i32;
}

fn main(){
    unsafe {
        println!("{}", multiply(5,7));
    }   
}

13.3.3. 编译 C 语言库时自定义设置

cc-badge cat-development-tools-badge

使用 [cc::Build::define] 自定义构建绑定的 C 语言代码非常简单。该方法接受 [Option] 值,因此可以创建这样的定义:#define APP_NAME "foo"#define WELCOME(将 None 作为不确定值传递)。如下实例构建了一个绑定的 C 语言文件,其在 build.rs 中设置了动态定义,并在运行时打印 “Welcome to foo - version 1.0.2”。Cargo 设定了一些环境变量,这些变量可能对某些自定义设置有用。

Cargo.toml

[package]
name = "cc-defines"
authors = ["zzy <ask@rusthub.org>"]
edition = "2018"

version = "1.0.2"
build = "build.rs"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[build-dependencies]
cc = "1.0.66"

[dependencies]


build.rs

fn main() {
    cc::Build::new()
        .define("APP_NAME", "\"foo\"")
        .define("VERSION", format!("\"{}\"", env!("CARGO_PKG_VERSION")).as_str())
        .define("WELCOME", None)
        .file("src/foo.c")
        .compile("foo");
}

src/foo.c

#include <stdio.h>

void print_app_info() {
#ifdef WELCOME
    printf("Welcome to ");
#endif
    printf("%s - version %s\n", APP_NAME, VERSION);
}

src/main.rs

extern {
    fn print_app_info();
}

fn main(){
    unsafe {
        print_app_info();
    }   
}

13.3.4. 构建工具 crates 小结

涉及 crates:

  • cc
  • error-chain

14. 编码

编码实践涵盖字符集、CSV 处理、结构化数据。

字符集实践:

  • 百分比编码(URL 编码)字符串
  • 将字符串编码为 application/x-www-form-urlencoded
  • 编码和解码十六进制
  • 编码和解码 base64
  • 字符集 crates 小结

CSV 处理实践:

  • 读取 CSV 记录
  • 读取有不同分隔符的 CSV 记录
  • 筛选匹配断言的 CSV 记录
  • 用 Serde 处理无效的 CSV 数据
  • 将记录序列化为 CSV
  • 用 Serde 将记录序列化为 CSV
  • 转换 CSV 文件的列
  • CSV 处理 crates 小结

结构化数据实践:

  • 对非结构化 JSON 序列化和反序列化
  • 反序列化 TOML 配置文件
  • 以小端模式(低位模式)字节顺序读写整数
  • 结构化数据 crates 小结

14.1. 字符集

  • 百分比编码(URL 编码)字符串
  • 将字符串编码为 application/x-www-form-urlencoded
  • 编码和解码十六进制
  • 编码和解码 base64
  • 字符集 crates 小结

14.1.1. 百分比编码(URL 编码)字符串

percent-encoding-badge cat-encoding-badge

使用 percent-encoding crate 中的 utf8_percent_encode 函数对输入字符串进行百分比编码(URL 编码)。解码使用 percent_decode 函数。

use percent_encoding::{utf8_percent_encode, percent_decode, AsciiSet, CONTROLS};
use std::str::Utf8Error;

/// https://url.spec.whatwg.org/#fragment-percent-encode-set
const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');

fn main() -> Result<(), Utf8Error> {
    let input = "confident, productive systems programming";

    let iter = utf8_percent_encode(input, FRAGMENT);
    let encoded: String = iter.collect();
    assert_eq!(encoded, "confident,%20productive%20systems%20programming");

    let iter = percent_decode(encoded.as_bytes());
    let decoded = iter.decode_utf8()?;
    assert_eq!(decoded, "confident, productive systems programming");

    Ok(())
}

编码集定义哪些字节(除了非 ASCII 字节和控制键之外)需要进行百分比编码(URL 编码),这个集合的选择取决于上下文。例如,url 对 URL 路径中的 ? 编码,而不对查询字符串中的 ? 编码。

编码的返回值是 &str 切片的迭代器,然后聚集为一个字符串 String

14.1.2. 将字符串编码为 application/x-www-form-urlencoded

url-badge cat-encoding-badge

如下实例使用 form_urlencoded::byte_serialize 将字符串编码为 application/x-www-form-urlencoded 表单语法,随后使用 form_urlencoded::parse 对其进行解码。这两个函数都返回迭代器,然后这些迭代器聚集为 String

use url::form_urlencoded::{byte_serialize, parse};

fn main() {
    let urlencoded: String = byte_serialize("What is ❤?".as_bytes()).collect();
    assert_eq!(urlencoded, "What+is+%E2%9D%A4%3F");
    println!("urlencoded:'{}'", urlencoded);

    let decoded: String = parse(urlencoded.as_bytes())
        .map(|(key, val)| [key, val].concat())
        .collect();
    assert_eq!(decoded, "What is ❤?");
    println!("decoded:'{}'", decoded);
}

14.1.3. 编码和解码十六进制

data-encoding-badge cat-encoding-badge

data_encoding crate 提供了 HEXUPPER::encode 方法,该方法接受 &[u8] 参数并返回十六进制数据的字符串 String

类似地,data_encoding crate 提供了 HEXUPPER::decode 方法,该方法接受 &[u8] 参数。如果输入数据被成功解码,则返回 Vec<u8>

下面的实例将 &[u8] 数据转换为等效的十六进制数据,然后将此值与预期值进行比较。

use data_encoding::{HEXUPPER, DecodeError};

fn main() -> Result<(), DecodeError> {
    let original = b"The quick brown fox jumps over the lazy dog.";
    let expected = "54686520717569636B2062726F776E20666F78206A756D7073206F76\
        657220746865206C617A7920646F672E";

    let encoded = HEXUPPER.encode(original);
    assert_eq!(encoded, expected);

    let decoded = HEXUPPER.decode(&encoded.into_bytes())?;
    assert_eq!(&decoded[..], &original[..]);

    Ok(())
}

14.1.4. 编码和解码 base64

base64-badge cat-encoding-badge

使用 encode 将字节切片编码为 base64 字符串,对 base64 字符串解码使用 decode

use error_chain::error_chain;

use std::str;
use base64::{encode, decode};

error_chain! {
    foreign_links {
        Base64(base64::DecodeError);
        Utf8Error(str::Utf8Error);
    }
}

fn main() -> Result<()> {
    let hello = b"hello rustaceans";
    let encoded = encode(hello);
    let decoded = decode(&encoded)?;

    println!("origin: {}", str::from_utf8(hello)?);
    println!("base64 encoded: {}", encoded);
    println!("back to origin: {}", str::from_utf8(&decoded)?);

    Ok(())
}

14.1.5. 字符集 crates 小结

涉及 crates:

  • base64
  • data-encoding
  • error-chain
  • percent-encoding
  • url

14.2. CSV 处理

  • 读取 CSV 记录
  • 读取有不同分隔符的 CSV 记录
  • 筛选匹配断言的 CSV 记录
  • 用 Serde 处理无效的 CSV 数据
  • 将记录序列化为 CSV
  • 用 Serde 将记录序列化为 CSV
  • 转换 CSV 文件的列
  • CSV 处理 crates 小结

14.2.1. 读取 CSV 记录

csv-badge cat-encoding-badge

将标准的 CSV 记录读入 csv::StringRecord——一种弱类型的数据表示方式,它需要 CSV 中的行数据是有效的 UTF-8 字符编码。另外,csv::ByteRecord 对 UTF-8 不做任何预设。

use csv::Error;

fn main() -> Result<(), Error> {
    let csv = "year,make,model,description
        1948,Porsche,356,Luxury sports car
        1967,Ford,Mustang fastback 1967,American car";

    let mut reader = csv::Reader::from_reader(csv.as_bytes());
    for record in reader.records() {
        let record = record?;
        println!(
            "In {}, {} built the {} model. It is a {}.",
            &record[0],
            &record[1],
            &record[2],
            &record[3]
        );
    }

    Ok(())
}

Serde 将数据反序列化为强类型结构体。具体查阅 csv::Reader::deserialize 方法。

use serde::Deserialize;

#[derive(Deserialize)]
struct Record {
    year: u16,
    make: String,
    model: String,
    description: String,
}

fn main() -> Result<(), csv::Error> {
    let csv = "year,make,model,description
1948,Porsche,356,Luxury sports car
1967,Ford,Mustang fastback 1967,American car";

    let mut reader = csv::Reader::from_reader(csv.as_bytes());

    for record in reader.deserialize() {
        let record: Record = record?;
        println!(
            "In {}, {} built the {} model. It is a {}.",
            record.year,
            record.make,
            record.model,
            record.description
        );
    }

    Ok(())
}

14.2.2. 读取有不同分隔符的 CSV 记录

csv-badge cat-encoding-badge

使用制表(tab)分隔符 delimiter 读取 CSV 记录。

use csv::Error;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct Record {
    name: String,
    place: String,
    #[serde(deserialize_with = "csv::invalid_option")]
    id: Option<u64>,
}

use csv::ReaderBuilder;

fn main() -> Result<(), Error> {
    let data = "name\tplace\tid
        Mark\tMelbourne\t46
        Ashley\tZurich\t92";

    let mut reader = ReaderBuilder::new().delimiter(b'\t').from_reader(data.as_bytes());
    for result in reader.deserialize::<Record>() {
        println!("{:?}", result?);
    }

    Ok(())
}

14.2.3. 筛选匹配断言的 CSV 记录

csv-badge cat-encoding-badge

仅仅 返回 data 中字段(field)与 query 匹配的的行。

use error_chain::error_chain;

use std::io;

error_chain!{
    foreign_links {
        Io(std::io::Error);
        CsvError(csv::Error);
    }
}

fn main() -> Result<()> {
    let query = "CA";
    let data = "\
City,State,Population,Latitude,Longitude
Kenai,AK,7610,60.5544444,-151.2583333
Oakman,AL,,33.7133333,-87.3886111
Sandfort,AL,,32.3380556,-85.2233333
West Hollywood,CA,37031,34.0900000,-118.3608333";

    let mut rdr = csv::ReaderBuilder::new().from_reader(data.as_bytes());
    let mut wtr = csv::Writer::from_writer(io::stdout());

    wtr.write_record(rdr.headers()?)?;

    for result in rdr.records() {
        let record = result?;
        if record.iter().any(|field| field == query) {
            wtr.write_record(&record)?;
        }
    }

    wtr.flush()?;
    Ok(())
}

免责声明:此实例改编自csv crate 教程

14.2.4. 用 Serde 处理无效的 CSV 数据

csv-badge serde-badge cat-encoding-badge

CSV 文件通常包含无效数据。对于这些情形,csv crate 提供了一个自定义的反序列化程序 csv::invalid_option,它自动将无效数据转换为 None 值。

use csv::Error;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct Record {
    name: String,
    place: String,
    #[serde(deserialize_with = "csv::invalid_option")]
    id: Option<u64>,
}

fn main() -> Result<(), Error> {
    let data = "name,place,id
mark,sydney,46.5
ashley,zurich,92
akshat,delhi,37
alisha,colombo,xyz";

    let mut rdr = csv::Reader::from_reader(data.as_bytes());
    for result in rdr.deserialize() {
        let record: Record = result?;
        println!("{:?}", record);
    }

    Ok(())
}

14.2.5. 将记录序列化为 CSV

csv-badge cat-encoding-badge

本实例展示了如何序列化 Rust 元组。csv::writer 支持从 Rust 类型到 CSV 记录的自动序列化。write_record 只写入包含字符串数据的简单记录。具有更复杂值(如数字、浮点和选项)的数据使用 serialize 进行序列化。因为 csv::writer 使用内部缓冲区,所以在完成时总是显式刷新 flush

use error_chain::error_chain;

use std::io;

error_chain! {
    foreign_links {
        CSVError(csv::Error);
        IOError(std::io::Error);
   }
}

fn main() -> Result<()> {
    let mut wtr = csv::Writer::from_writer(io::stdout());

    wtr.write_record(&["Name", "Place", "ID"])?;

    wtr.serialize(("Mark", "Sydney", 87))?;
    wtr.serialize(("Ashley", "Dublin", 32))?;
    wtr.serialize(("Akshat", "Delhi", 11))?;

    wtr.flush()?;
    Ok(())
}

14.2.6. 用 Serde 将记录序列化为 CSV

csv-badge serde-badge cat-encoding-badge

下面的实例展示如何使用 serde crate 将自定义结构体序列化为 CSV 记录。

use error_chain::error_chain;
use serde::Serialize;
use std::io;

error_chain! {
   foreign_links {
       IOError(std::io::Error);
       CSVError(csv::Error);
   }
}

#[derive(Serialize)]
struct Record<'a> {
    name: &'a str,
    place: &'a str,
    id: u64,
}

fn main() -> Result<()> {
    let mut wtr = csv::Writer::from_writer(io::stdout());

    let rec1 = Record { name: "Mark", place: "Melbourne", id: 56};
    let rec2 = Record { name: "Ashley", place: "Sydney", id: 64};
    let rec3 = Record { name: "Akshat", place: "Delhi", id: 98};

    wtr.serialize(rec1)?;
    wtr.serialize(rec2)?;
    wtr.serialize(rec3)?;

    wtr.flush()?;

    Ok(())
}

14.2.7. 转换 CSV 文件的列

csv-badge serde-badge cat-encoding-badge

将包含颜色名称和十六进制颜色值的 CSV 文件转换为具有颜色名称和 rgb 颜色值的 CSV 文件。使用 csv crate 读写 csv 文件,使用 serde crate 对行输入字节进行反序列化,对行输出字节进行序列化。

详细请参阅 csv::Reader::deserializeserde::Deserialize,以及 std::str::FromStr

use error_chain::error_chain;
use csv::{Reader, Writer};
use serde::{de, Deserialize, Deserializer};
use std::str::FromStr;

error_chain! {
   foreign_links {
       CsvError(csv::Error);
       ParseInt(std::num::ParseIntError);
       CsvInnerError(csv::IntoInnerError<Writer<Vec<u8>>>);
       IO(std::fmt::Error);
       UTF8(std::string::FromUtf8Error);
   }
}

#[derive(Debug)]
struct HexColor {
    red: u8,
    green: u8,
    blue: u8,
}

#[derive(Debug, Deserialize)]
struct Row {
    color_name: String,
    color: HexColor,
}

impl FromStr for HexColor {
    type Err = Error;

    fn from_str(hex_color: &str) -> std::result::Result<Self, Self::Err> {
        let trimmed = hex_color.trim_matches('#');
        if trimmed.len() != 6 {
            Err("Invalid length of hex string".into())
        } else {
            Ok(HexColor {
                red: u8::from_str_radix(&trimmed[..2], 16)?,
                green: u8::from_str_radix(&trimmed[2..4], 16)?,
                blue: u8::from_str_radix(&trimmed[4..6], 16)?,
            })
        }
    }
}

impl<'de> Deserialize<'de> for HexColor {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        FromStr::from_str(&s).map_err(de::Error::custom)
    }
}

fn main() -> Result<()> {
    let data = "color_name,color
red,#ff0000
green,#00ff00
blue,#0000FF
periwinkle,#ccccff
magenta,#ff00ff"
        .to_owned();
    let mut out = Writer::from_writer(vec![]);
    let mut reader = Reader::from_reader(data.as_bytes());
    for result in reader.deserialize::<Row>() {
        let res = result?;
        out.serialize((
            res.color_name,
            res.color.red,
            res.color.green,
            res.color.blue,
        ))?;
    }
    let written = String::from_utf8(out.into_inner()?)?;
    assert_eq!(Some("magenta,255,0,255"), written.lines().last());
    println!("{}", written);
    Ok(())
}

14.2.8. CSV 处理 crates 小结

涉及 crates:

  • csv
  • error-chain
  • serde
  • serde_json

14.3. 结构化数据

  • 对非结构化 JSON 序列化和反序列化
  • 反序列化 TOML 配置文件
  • 以小端模式(低位模式)字节顺序读写整数
  • 结构化数据 crates 小结

14.3.1. 对非结构化 JSON 序列化和反序列化

serde-json-badge cat-encoding-badge

serde_json crate 提供了 from_str 函数来解析 JSON 切片 &str

非结构化 JSON 可以被解析为一个通用的 serde_json::Value 类型,该类型能够表示任何有效的 JSON 数据。

下面的实例展示如何解析 JSON 切片 &str,期望值被 json! 宏声明。

use serde_json::json;
use serde_json::{Value, Error};

fn main() -> Result<(), Error> {
    let j = r#"{
                 "userid": 103609,
                 "verified": true,
                 "access_privileges": [
                   "user",
                   "admin"
                 ]
               }"#;

    let parsed: Value = serde_json::from_str(j)?;

    let expected = json!({
        "userid": 103609,
        "verified": true,
        "access_privileges": [
            "user",
            "admin"
        ]
    });

    assert_eq!(parsed, expected);

    Ok(())
}

14.3.2. 反序列化 TOML 配置文件

toml-badge cat-encoding-badge

将一些 TOML 配置项解析为一个通用的值 toml::Value,该值能够表示任何有效的 TOML 数据。

use toml::{Value, de::Error};

fn main() -> Result<(), Error> {
    let toml_content = r#"
          [package]
          name = "your_package"
          version = "0.1.0"
          authors = ["You! <you@example.org>"]

          [dependencies]
          serde = "1.0"
          "#;

    let package_info: Value = toml::from_str(toml_content)?;

    assert_eq!(package_info["dependencies"]["serde"].as_str(), Some("1.0"));
    assert_eq!(package_info["package"]["name"].as_str(),
               Some("your_package"));

    Ok(())
}

使用 Serde crate 将 TOML 解析为自定义的结构体。

use serde::Deserialize;

use toml::de::Error;
use std::collections::HashMap;

#[derive(Deserialize)]
struct Config {
    package: Package,
    dependencies: HashMap<String, String>,
}

#[derive(Deserialize)]
struct Package {
    name: String,
    version: String,
    authors: Vec<String>,
}

fn main() -> Result<(), Error> {
    let toml_content = r#"
          [package]
          name = "your_package"
          version = "0.1.0"
          authors = ["You! <you@example.org>"]

          [dependencies]
          serde = "1.0"
          "#;

    let package_info: Config = toml::from_str(toml_content)?;

    assert_eq!(package_info.package.name, "your_package");
    assert_eq!(package_info.package.version, "0.1.0");
    assert_eq!(package_info.package.authors, vec!["You! <you@example.org>"]);
    assert_eq!(package_info.dependencies["serde"], "1.0");

    Ok(())
}

14.3.3. 以小端模式(低位模式)字节顺序读写整数

byteorder-badge cat-encoding-badge

字节序 byteorder 可以反转结构化数据的有效字节。当通过网络接收信息时,这可能是必要的,例如接收到的字节来自另一个系统。


use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use std::io::Error;

#[derive(Default, PartialEq, Debug)]
struct Payload {
    kind: u8,
    value: u16,
}

fn main() -> Result<(), Error> {
    let original_payload = Payload::default();
    let encoded_bytes = encode(&original_payload)?;
    let decoded_payload = decode(&encoded_bytes)?;
    assert_eq!(original_payload, decoded_payload);
    Ok(())
}

fn encode(payload: &Payload) -> Result<Vec<u8>, Error> {
    let mut bytes = vec![];
    bytes.write_u8(payload.kind)?;
    bytes.write_u16::<LittleEndian>(payload.value)?;
    Ok(bytes)
}

fn decode(mut bytes: &[u8]) -> Result<Payload, Error> {
    let payload = Payload {
        kind: bytes.read_u8()?,
        value: bytes.read_u16::<LittleEndian>()?,
    };
    Ok(payload)
}

14.3.4. 结构化数据 crates 小结

涉及 crates:

  • byteorder
  • serde
  • serde_json
  • toml

15. 错误处理

主要实践为处理错误变量

  • 在 main 方法中对错误适当处理
  • 避免在错误转变过程中遗漏错误
  • 获取复杂错误场景的回溯
  • 错误处理 crates 小结

15.1. 处理错误变量

  • 在 main 方法中对错误适当处理
  • 避免在错误转变过程中遗漏错误
  • 获取复杂错误场景的回溯
  • 错误处理 crates 小结

15.1.1. 在 main 方法中对错误适当处理

error-chain-badge cat-rust-patterns-badge

处理尝试打开不存在的文件时发生的错误,是通过使用 error-chain crate 来实现的。error-chain crate 包含大量的模板代码,用于 Rust 中的错误处理

foreign_links 代码块内的 Io(std::io::Error) 函数允许由 std::io::Error 所报错误信息到 error_chain! 所定义错误类型的自动转换,error_chain! 所定义错误类型将实现 Error trait。

下文的实例将通过打开 Unix 文件 /proc/uptime 并解析内容以获得其中第一个数字,从而告诉系统运行了多长时间。除非出现错误,否则返回正常运行时间。

本书中的其它实例将隐藏 error-chain 模板,如果需要查看,可以通过 ⤢ 按钮展开代码。

use error_chain::error_chain;

use std::fs::File;
use std::io::Read;

error_chain!{
    foreign_links {
        Io(std::io::Error);
        ParseInt(::std::num::ParseIntError);
    }
}

fn read_uptime() -> Result<u64> {
    let mut uptime = String::new();
    File::open("/proc/uptime")?.read_to_string(&mut uptime)?;

    Ok(uptime
        .split('.')
        .next()
        .ok_or("Cannot parse uptime data")?
        .parse()?)
}

fn main() {
    match read_uptime() {
        Ok(uptime) => println!("uptime: {} seconds", uptime),
        Err(err) => eprintln!("error: {}", err),
    };
}

15.1.2. 避免在错误转变过程中遗漏错误

error-chain-badge cat-rust-patterns-badge

error-chain crate 使得匹配函数返回的不同错误类型成为可能,并且相对简洁。ErrorKind 是枚举类型,可以确定错误类型。

下文实例使用 reqwest::blocking 来查询一个随机整数生成器的 web 服务,并将服务器响应的字符串转换为整数。Rust 标准库 reqwest 和 web 服务都可能会产生错误,所以使用 foreign_links 定义易于辨认理解的 Rust 错误。另外,用于 web 服务错误信息的 ErrorKind 变量,使用 error_chain! 宏的 errors 代码块定义。

use error_chain::error_chain;

error_chain! {
    foreign_links {
        Io(std::io::Error);
        Reqwest(reqwest::Error);
        ParseIntError(std::num::ParseIntError);
    }
    errors { RandomResponseError(t: String) }
}

fn parse_response(response: reqwest::blocking::Response) -> Result<u32> {
  let mut body = response.text()?;
  body.pop();
  body
    .parse::<u32>()
    .chain_err(|| ErrorKind::RandomResponseError(body))
}

fn run() -> Result<()> {
  let url =
    format!("https://www.random.org/integers/?num=1&min=0&max=10&col=1&base=10&format=plain");
  let response = reqwest::blocking::get(&url)?;
  let random_value: u32 = parse_response(response)?;
  println!("a random number between 0 and 10: {}", random_value);
  Ok(())
}

fn main() {
  if let Err(error) = run() {
    match *error.kind() {
      ErrorKind::Io(_) => println!("Standard IO error: {:?}", error),
      ErrorKind::Reqwest(_) => println!("Reqwest error: {:?}", error),
      ErrorKind::ParseIntError(_) => println!("Standard parse int error: {:?}", error),
      ErrorKind::RandomResponseError(_) => println!("User defined error: {:?}", error),
      _ => println!("Other error: {:?}", error),
    }
  }
}

15.1.3. 获取复杂错误场景的回溯

error-chain-badge cat-rust-patterns-badge

本实例展示了如何处理一个复杂的错误场景,并且打印出错误回溯。依赖于 chain_err,通过附加新的错误来扩展错误信息。从而可以展开错误堆栈,这样提供了更好的上下文来理解错误的产生原因。

下述代码尝试将值 256 反序列化为 u8。首先 Serde 产生错误,然后是 csv,最后是用户代码。

use error_chain::error_chain;
use serde::Deserialize;

use std::fmt;

error_chain! {
    foreign_links {
        Reader(csv::Error);
    }
}

#[derive(Debug, Deserialize)]
struct Rgb {
    red: u8,
    blue: u8,
    green: u8,
}

impl Rgb {
    fn from_reader(csv_data: &[u8]) -> Result<Rgb> {
        let color: Rgb = csv::Reader::from_reader(csv_data)
            .deserialize()
            .nth(0)
            .ok_or("Cannot deserialize the first CSV record")?
            .chain_err(|| "Cannot deserialize RGB color")?;

        Ok(color)
    }
}

impl fmt::UpperHex for Rgb {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let hexa = u32::from(self.red) << 16 | u32::from(self.blue) << 8 | u32::from(self.green);
        write!(f, "{:X}", hexa)
    }
}

fn run() -> Result<()> {
    let csv = "red,blue,green
102,256,204";

    let rgb = Rgb::from_reader(csv.as_bytes()).chain_err(|| "Cannot read CSV data")?;
    println!("{:?} to hexadecimal #{:X}", rgb, rgb);

    Ok(())
}

fn main() {
    if let Err(ref errors) = run() {
        eprintln!("Error level - description");
        errors
            .iter()
            .enumerate()
            .for_each(|(index, error)| eprintln!("└> {} - {}", index, error));

        if let Some(backtrace) = errors.backtrace() {
            eprintln!("{:?}", backtrace);
        }

        // In a real use case, errors should handled. For example:
        // ::std::process::exit(1);
    }
}

错误回溯信息如下:

Error level - description
└> 0 - Cannot read CSV data
└> 1 - Cannot deserialize RGB color
└> 2 - CSV deserialize error: record 1 (line: 2, byte: 15): field 1: number too large to fit in target type
└> 3 - field 1: number too large to fit in target type

可以通过附加命令参数 RUST_BACKTRACE=1 运行实例,以显示与此错误相关的详细回溯

15.2. 错误处理 crates 小结

涉及 crates:

  • csv
  • error-chain
  • reqwest
  • serde

16. 文件系统

文件系统涵盖文件读写、目录遍历。

文件读写实践:

  • 读取文件的字符串行
  • 避免读取写入同一文件
  • 使用内存映射随机访问文件

目录遍历实践:

  • 过去 24 小时内修改过的文件名
  • 查找给定路径的循环
  • 递归查找重名文件
  • 使用给定断言递归查找所有文件
  • 跳过隐藏文件遍历目录
  • 在给定深度的目录,递归计算文件大小
  • 递归查找所有 png 文件
  • 忽略文件名大小写,使用给定模式查找所有文件

以及,文件系统 crates 小结

16.1. 文件读写

  • 读取文件的字符串行
  • 避免读取写入同一文件
  • 使用内存映射随机访问文件

16.1.1. 读取文件的字符串行

std-badge cat-filesystem-badge

我们向文件写入三行信息,然后使用 BufRead::lines 创建的迭代器 Lines 读取文件,一次读回一行。File 模块实现了提供 BufReader 结构体的 Read trait。File::create 打开文件 File 进行写入,File::open 则进行读取。

use std::fs::File;
use std::io::{Write, BufReader, BufRead, Error};

fn main() -> Result<(), Error> {
    let path = "lines.txt";

    let mut output = File::create(path)?;
    write!(output, "Rust\n💖\nFun")?;

    let input = File::open(path)?;
    let buffered = BufReader::new(input);

    for line in buffered.lines() {
        println!("{}", line?);
    }

    Ok(())
}

16.1.2. 避免读取写入同一文件

same_file-badge cat-filesystem-badge

对文件使用 same_file::Handle 结构体,可以测试文件句柄是否等同。在本实例中,将对要读取和写入的文件句柄进行相等性测试。

use same_file::Handle;
use std::fs::File;
use std::io::{BufRead, BufReader, Error, ErrorKind};
use std::path::Path;

fn main() -> Result<(), Error> {
    let path_to_read = Path::new("new.txt");

    let stdout_handle = Handle::stdout()?;
    let handle = Handle::from_path(path_to_read)?;

    if stdout_handle == handle {
        return Err(Error::new(
            ErrorKind::Other,
            "You are reading and writing to the same file",
        ));
    } else {
        let file = File::open(&path_to_read)?;
        let file = BufReader::new(file);
        for (num, line) in file.lines().enumerate() {
            println!("{} : {}", num, line?.to_uppercase());
        }
    }

    Ok(())
}
cargo run

显示文件 new.txt 的内容。

cargo run >> ./new.txt

报错,因为是同一文件。

16.1.3. 使用内存映射随机访问文件

memmap-badge cat-filesystem-badge

使用 memmap 创建文件的内存映射,并模拟文件的一些非序列读取。使用内存映射意味着您仅需索引一个切片,而不是使用 seek 方法来导航整个文件。

Mmap::map 函数假定内存映射后的文件没有被另一个进程同时更改,否则会出现竞态条件

use memmap::Mmap;
use std::fs::File;
use std::io::{Write, Error};

fn main() -> Result<(), Error> {
    write!(File::create("content.txt")?, "My hovercraft is full of eels!")?;

    let file = File::open("content.txt")?;
    let map = unsafe { Mmap::map(&file)? };

    let random_indexes = [0, 1, 2, 19, 22, 10, 11, 29];
    assert_eq!(&map[3..13], b"hovercraft");
    let random_bytes: Vec<u8> = random_indexes.iter()
        .map(|&idx| map[idx])
        .collect();
    assert_eq!(&random_bytes[..], b"My loaf!");
    
    Ok(())
}

16.2. 目录遍历

  • 过去 24 小时内修改过的文件名
  • 查找给定路径的循环
  • 递归查找重名文件
  • 使用给定断言递归查找所有文件
  • 跳过隐藏文件遍历目录
  • 在给定深度的目录,递归计算文件大小
  • 递归查找所有 png 文件
  • 忽略文件名大小写,使用给定模式查找所有文件

16.2.1. 过去 24 小时内修改过的文件名

std-badge cat-filesystem-badge

通过调用 env::current_dir 获取当前工作目录,然后通过 fs::read_dir 读取目录中的每个条目,通过 DirEntry::path 提取条目路径,以及通过通过 fs::Metadata 获取条目元数据。Metadata::modified 返回条目自上次更改以来的运行时间 SystemTime::elapsedDuration::as_secs 将时间转换为秒,并与 24 小时(24 * 60 * 60 秒)进行比较。Metadata::is_file 用于筛选出目录。

use error_chain::error_chain;

use std::{env, fs};

error_chain! {
    foreign_links {
        Io(std::io::Error);
        SystemTimeError(std::time::SystemTimeError);
    }
}

fn main() -> Result<()> {
    let current_dir = env::current_dir()?;
    println!(
        "Entries modified in the last 24 hours in {:?}:",
        current_dir
    );

    for entry in fs::read_dir(current_dir)? {
        let entry = entry?;
        let path = entry.path();

        let metadata = fs::metadata(&path)?;
        let last_modified = metadata.modified()?.elapsed()?.as_secs();

        if last_modified < 24 * 3600 && metadata.is_file() {
            println!(
                "Last modified: {:?} seconds, is read only: {:?}, size: {:?} bytes, filename: {:?}",
                last_modified,
                metadata.permissions().readonly(),
                metadata.len(),
                path.file_name().ok_or("No filename")?
            );
        }
    }

    Ok(())
}

16.2.2. 查找给定路径的循环

same_file-badge cat-filesystem-badge

使用 same_file::is_same_file 检测给定路径的循环。例如,可以通过软连接(符号链接)在 Unix 系统上创建循环:

mkdir -p /tmp/foo/bar/baz
ln -s /tmp/foo/  /tmp/foo/bar/baz/qux

下面的实例将断言存在一个循环。

use std::io;
use std::path::{Path, PathBuf};
use same_file::is_same_file;

fn contains_loop<P: AsRef<Path>>(path: P) -> io::Result<Option<(PathBuf, PathBuf)>> {
    let path = path.as_ref();
    let mut path_buf = path.to_path_buf();
    while path_buf.pop() {
        if is_same_file(&path_buf, path)? {
            return Ok(Some((path_buf, path.to_path_buf())));
        } else if let Some(looped_paths) = contains_loop(&path_buf)? {
            return Ok(Some(looped_paths));
        }
    }
    return Ok(None);
}

fn main() {
    assert_eq!(
        contains_loop("/tmp/foo/bar/baz/qux/bar/baz").unwrap(),
        Some((
            PathBuf::from("/tmp/foo"),
            PathBuf::from("/tmp/foo/bar/baz/qux")
        ))
    );
}

16.2.3. 递归查找重名文件

walkdir-badge cat-filesystem-badge

在当前目录中递归查找重复的文件名,只打印一次。

use std::collections::HashMap;
use walkdir::WalkDir;

fn main() {
    let mut filenames = HashMap::new();

    for entry in WalkDir::new(".")
            .into_iter()
            .filter_map(Result::ok)
            .filter(|e| !e.file_type().is_dir()) {
        let f_name = String::from(entry.file_name().to_string_lossy());
        let counter = filenames.entry(f_name.clone()).or_insert(0);
        *counter += 1;

        if *counter == 2 {
            println!("{}", f_name);
        }
    }
}

16.2.4. 使用给定断言递归查找所有文件

walkdir-badge cat-filesystem-badge

在当前目录中查找最近一天内修改的 JSON 文件。使用 follow_links 确保软链接(符号链接)像普通目录和文件一样被按照当前查找规则执行。

use error_chain::error_chain;

use walkdir::WalkDir;

error_chain! {
    foreign_links {
        WalkDir(walkdir::Error);
        Io(std::io::Error);
        SystemTime(std::time::SystemTimeError);
    }
}

fn main() -> Result<()> {
    for entry in WalkDir::new(".")
            .follow_links(true)
            .into_iter()
            .filter_map(|e| e.ok()) {
        let f_name = entry.file_name().to_string_lossy();
        let sec = entry.metadata()?.modified()?;

        if f_name.ends_with(".json") && sec.elapsed()?.as_secs() < 86400 {
            println!("{}", f_name);
        }
    }

    Ok(())
}

16.2.5. 跳过隐藏文件遍历目录

walkdir-badge cat-filesystem-badge

递归下行到子目录的过程中,使用 filter_entry 对目录中的条目传递 is_not_hidden 断言,从而跳过隐藏的文件和目录。Iterator::filter 可应用到要检索的任何目录 WalkDir::DirEntry,即使父目录是隐藏目录。

根目录 "." 的检索结果,通过在断言 is_not_hidden 中使用 WalkDir::depth 参数生成。

use walkdir::{DirEntry, WalkDir};

fn is_not_hidden(entry: &DirEntry) -> bool {
    entry
         .file_name()
         .to_str()
         .map(|s| entry.depth() == 0 || !s.starts_with("."))
         .unwrap_or(false)
}

fn main() {
    WalkDir::new(".")
        .into_iter()
        .filter_entry(|e| is_not_hidden(e))
        .filter_map(|v| v.ok())
        .for_each(|x| println!("{}", x.path().display()));
}

16.2.6. 在给定深度的目录,递归计算文件大小

walkdir-badge cat-filesystem-badge

通过 WalkDir::min_depthWalkDir::max_depth 方法,可以灵活设置目录的递归深度。下面的实例计算了 3 层子文件夹深度的所有文件的大小总和,计算中忽略根文件夹中的文件。

use walkdir::WalkDir;

fn main() {
    let total_size = WalkDir::new(".")
        .min_depth(1)
        .max_depth(3)
        .into_iter()
        .filter_map(|entry| entry.ok())
        .filter_map(|entry| entry.metadata().ok())
        .filter(|metadata| metadata.is_file())
        .fold(0, |acc, m| acc + m.len());

    println!("Total size: {} bytes.", total_size);
}

16.2.7. 递归查找所有 png 文件

glob-badge cat-filesystem-badge

递归地查找当前目录中的所有 PNG 文件。在本实例中,** 模式用于匹配当前目录及其所有子目录。

在路径任意部分使用 ** 模式,例如,/media/**/*.png 匹配 media 及其子目录中的所有 PNG 文件。

use error_chain::error_chain;

use glob::glob;

error_chain! {
    foreign_links {
        Glob(glob::GlobError);
        Pattern(glob::PatternError);
    }
}

fn main() -> Result<()> {
    for entry in glob("**/*.png")? {
        println!("{}", entry?.display());
    }

    Ok(())
}

16.2.8. 忽略文件名大小写,使用给定模式查找所有文件

glob-badge cat-filesystem-badge

/media/ 目录中查找与正则表达模式 img_[0-9]*.png 匹配的所有图像文件。

一个自定义 MatchOptions 结构体被传递给 glob_with 函数,使全局命令模式下不区分大小写,同时保持其它选项的默认值 Default

注:globglob command 的简写。在 shell 里面,用 * 等匹配模式来匹配文件,如:ls src/*.rs。

use error_chain::error_chain;
use glob::{glob_with, MatchOptions};

error_chain! {
    foreign_links {
        Glob(glob::GlobError);
        Pattern(glob::PatternError);
    }
}

fn main() -> Result<()> {
    let options = MatchOptions {
        case_sensitive: false,
        ..Default::default()
    };

    for entry in glob_with("/media/img_[0-9]*.png", options)? {
        println!("{}", entry?.display());
    }

    Ok(())
}

16.3. 文件系统 crates 小结

涉及 crates:

  • error-chain
  • glob
  • same-file
  • walkdir
  • memmap
  • same-file

17. 硬件支持

硬件支持涵盖处理器。

  • 检查逻辑 cpu 内核的数量
  • 硬件支持 crates 小结

17.1. 处理器

  • 检查逻辑 cpu 内核的数量
  • 硬件支持 crates 小结

17.1.1. 检查逻辑 cpu 内核的数量

num_cpus-badge cat-hardware-support-badge

使用 [num_cpus::get] 显示当前机器中的逻辑 CPU 内核的数量。

fn main() {
    println!("Number of logical cores is {}", num_cpus::get());
}

17.2. 硬件支持 crates 小结

涉及 crates:

  • num_cpus

18. 内存管理

内存管理涵盖全局静态/全局堆栈。

  • 声明延迟计算常量
  • 内存管理 crates 小结

18.1. 全局静态/全局堆栈

  • 声明延迟计算常量
  • 内存管理 crates 小结

18.1.1. 声明延迟计算常量

lazy_static-badge cat-caching-badge cat-rust-patterns-badge

声明延迟计算的常量 HashMapHashMap 将被计算一次,随后存储在全局静态(全局堆栈)引用。

use lazy_static::lazy_static;
use std::collections::HashMap;

lazy_static! {
    static ref PRIVILEGES: HashMap<&'static str, Vec<&'static str>> = {
        let mut map = HashMap::new();
        map.insert("James", vec!["user", "admin"]);
        map.insert("Jim", vec!["user"]);
        map
    };
}

fn show_access(name: &str) {
    let access = PRIVILEGES.get(name);
    println!("{}: {:?}", name, access);
}

fn main() {
    let access = PRIVILEGES.get("James");
    println!("James: {:?}", access);

    show_access("Jim");
}

18.2. 内存管理 crates 小结

涉及 crates:

  • lazy_static

19. 网络

网络实践涵盖服务器。

  • 监听未使用的 TCP/IP 端口
  • 服务器 crates 小结

19.1. 服务器

  • 监听未使用的 TCP/IP 端口
  • 服务器 crates 小结

19.1.1. 监听未使用的 TCP/IP 端口

std-badge cat-net-badge

在本实例中,程序将监听显示在控制台上的端口,直到一个请求被发出。当将端口设置为 0 时,SocketAddrV4 会分配一个随机端口。

use std::net::{SocketAddrV4, Ipv4Addr, TcpListener};
use std::io::{Read, Error};

fn main() -> Result<(), Error> {
    let loopback = Ipv4Addr::new(127, 0, 0, 1);
    let socket = SocketAddrV4::new(loopback, 0);
    let listener = TcpListener::bind(socket)?;
    let port = listener.local_addr()?;
    println!("Listening on {}, access this port to end the program", port);
    let (mut tcp_stream, addr) = listener.accept()?; // 阻塞,直到被请求
    println!("Connection received! {:?} is sending data.", addr);
    let mut input = String::new();
    let _ = tcp_stream.read_to_string(&mut input)?;
    println!("{:?} says {}", addr, input);
    Ok(())
}

19.1.2. 服务器 crates 小结

涉及 crates:

  • std

20. 操作系统

操作系统实践涵盖外部命令。

  • 运行外部命令并处理 stdout
  • 运行传递 stdin 的外部命令,并检查错误代码
  • 运行管道传输的外部命令
  • 将子进程的 stdout 和 stderr 重定向到同一个文件
  • 持续处理子进程的输出
  • 读取环境变量
  • 操作系统 crates 小结

20.1. 外部命令

  • 运行外部命令并处理 stdout
  • 运行传递 stdin 的外部命令,并检查错误代码
  • 运行管道传输的外部命令
  • 将子进程的 stdout 和 stderr 重定向到同一个文件
  • 持续处理子进程的输出
  • 读取环境变量
  • 操作系统 crates 小结

20.1.1. 运行外部命令并处理 stdout

regex-badge cat-os-badge cat-text-processing-badge

git log --oneline 作为外部命令 Command 运行,并使用 Regex 检查其 Output,以获取最后 5 次提交的哈希值和消息。

use error_chain::error_chain;

use std::process::Command;
use regex::Regex;

error_chain!{
    foreign_links {
        Io(std::io::Error);
        Regex(regex::Error);
        Utf8(std::string::FromUtf8Error);
    }
}

#[derive(PartialEq, Default, Clone, Debug)]
struct Commit {
    hash: String,
    message: String,
}

fn main() -> Result<()> {
    let output = Command::new("git").arg("log").arg("--oneline").output()?;

    if !output.status.success() {
        error_chain::bail!("Command executed with failing error code");
    }

    let pattern = Regex::new(r"(?x)
                               ([0-9a-fA-F]+) # 提交的哈希值
                               (.*)           # 提交信息")?;

    String::from_utf8(output.stdout)?
        .lines()
        .filter_map(|line| pattern.captures(line))
        .map(|cap| {
                 Commit {
                     hash: cap[1].to_string(),
                     message: cap[2].trim().to_string(),
                 }
             })
        .take(5)
        .for_each(|x| println!("{:?}", x));

    Ok(())
}

20.1.2. 运行传递 stdin 的外部命令,并检查错误代码

std-badge cat-os-badge

使用外部命令 Command 打开 python 解释器,并传递一条 python 语句供其执行,然后解析语句的输出结构体 Output

use error_chain::error_chain;

use std::collections::HashSet;
use std::io::Write;
use std::process::{Command, Stdio};

error_chain!{
    errors { CmdError }
    foreign_links {
        Io(std::io::Error);
        Utf8(std::string::FromUtf8Error);
    }
}

fn main() -> Result<()> {
    let mut child = Command::new("python").stdin(Stdio::piped())
        .stderr(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()?;

    child.stdin
        .as_mut()
        .ok_or("Child process stdin has not been captured!")?
        .write_all(b"import this; copyright(); credits(); exit()")?;

    let output = child.wait_with_output()?;

    if output.status.success() {
        let raw_output = String::from_utf8(output.stdout)?;
        let words = raw_output.split_whitespace()
            .map(|s| s.to_lowercase())
            .collect::<HashSet<_>>();
        println!("Found {} unique words:", words.len());
        println!("{:#?}", words);
        Ok(())
    } else {
        let err = String::from_utf8(output.stderr)?;
        error_chain::bail!("External command failed:\n {}", err)
    }
}

20.1.3. 运行管道传输的外部命令

std-badge cat-os-badge

显示当前工作目录中前 10 大的文件和子目录,它等同于运行: du -ah . | sort -hr | head -n 10

每个命令 Command 代表一个进程,子进程的输出是通过父进程和子进程之间的管道 Stdio::piped 捕获的。

use error_chain::error_chain;

use std::process::{Command, Stdio};

error_chain! {
    foreign_links {
        Io(std::io::Error);
        Utf8(std::string::FromUtf8Error);
    }
}

fn main() -> Result<()> {
    let directory = std::env::current_dir()?;
    let mut du_output_child = Command::new("du")
        .arg("-ah")
        .arg(&directory)
        .stdout(Stdio::piped())
        .spawn()?;

    if let Some(du_output) = du_output_child.stdout.take() {
        let mut sort_output_child = Command::new("sort")
            .arg("-hr")
            .stdin(du_output)
            .stdout(Stdio::piped())
            .spawn()?;

        du_output_child.wait()?;

        if let Some(sort_output) = sort_output_child.stdout.take() {
            let head_output_child = Command::new("head")
                .args(&["-n", "10"])
                .stdin(sort_output)
                .stdout(Stdio::piped())
                .spawn()?;

            let head_stdout = head_output_child.wait_with_output()?;

            sort_output_child.wait()?;

            println!(
                "Top 10 biggest files and directories in '{}':\n{}",
                directory.display(),
                String::from_utf8(head_stdout.stdout).unwrap()
            );
        }
    }

    Ok(())
}

20.1.4. 将子进程的 stdout 和 stderr 重定向到同一个文件

std-badge cat-os-badge

生成子进程并将 stdoutstderr 重定向到同一个文件。它遵循与运行管道传输的外部命令相同的思想,但是 process::Stdio 会将输出写入指定的文件。对 stdoutstderr 而言,File::try_clone 引用相同的文件句柄。它将确保两个句柄使用相同的光标位置进行写入。

下面的实例等同于运行 Unix shell 命令 ls . oops >out.txt 2>&1

use std::fs::File;
use std::io::Error;
use std::process::{Command, Stdio};

fn main() -> Result<(), Error> {
    let outputs = File::create("out.txt")?;
    let errors = outputs.try_clone()?;

    Command::new("ls")
        .args(&[".", "oops"])
        .stdout(Stdio::from(outputs))
        .stderr(Stdio::from(errors))
        .spawn()?
        .wait_with_output()?;

    Ok(())
}

20.1.5. 持续处理子进程的输出

std-badge cat-os-badge

运行外部命令并处理-stdout 实例中,直到外部命令 Command 完成,stdout 的处理才开始。下面的实例调用 Stdio::piped 创建管道,并在 BufReader 被更新后立即读取 stdout,持续不断地处理。

下面的实例等同于 Unix shell 命令 journalctl | grep usb

use std::process::{Command, Stdio};
use std::io::{BufRead, BufReader, Error, ErrorKind};

fn main() -> Result<(), Error> {
    let stdout = Command::new("journalctl")
        .stdout(Stdio::piped())
        .spawn()?
        .stdout
        .ok_or_else(|| Error::new(ErrorKind::Other,"Could not capture standard output."))?;

    let reader = BufReader::new(stdout);

    reader
        .lines()
        .filter_map(|line| line.ok())
        .filter(|line| line.find("usb").is_some())
        .for_each(|line| println!("{}", line));

     Ok(())
}

20.1.6. 读取环境变量

std-badge cat-os-badge

通过 std::env::var 读取环境变量。

use std::env;
use std::fs;
use std::io::Error;

fn main() -> Result<(), Error> {
    // 从环境变量 `CONFIG` 读取配置路径 `config_path`。
    // 如果 `CONFIG` 未设置,采用默认配置路径。
    let config_path = env::var("CONFIG")
        .unwrap_or("/etc/myapp/config".to_string());

    let config: String = fs::read_to_string(config_path)?;
    println!("Config: {}", config);

    Ok(())
}

20.2. 操作系统 crates 小结

涉及 crates:

  • error-chain
  • regex

21. 科学计算

科学计算实践涵盖数学、线性代数、三角学、复数、统计学、其他数学计算。

线性代数实践:

  • 矩阵相加
  • 矩阵相乘
  • 标量、vector、矩阵相乘
  • Vector 比较
  • Vector 范数
  • 矩阵求逆
  • (反)序列化矩阵

三角学实践:

  • 计算三角形的边长
  • 验证正切(tan)等于正弦(sin)除以余弦(cos)
  • 地球上两点之间的距离

复数实践:

  • 创建复数
  • 复数相加
  • 复数的数学函数

统计学实践:

  • 集中趋势度量
  • 计算标准偏差

其它数学计算实践:

  • 大数
  • 数学 crates 小结

21.1. 数学

线性代数实践:

  • 矩阵相加
  • 矩阵相乘
  • 标量、vector、矩阵相乘
  • Vector 比较
  • Vector 范数
  • 矩阵求逆
  • (反)序列化矩阵

三角学实践:

  • 计算三角形的边长
  • 验证正切(tan)等于正弦(sin)除以余弦(cos)
  • 地球上两点之间的距离

复数实践:

  • 创建复数
  • 复数相加
  • 复数的数学函数

统计学实践:

  • 集中趋势度量
  • 计算标准偏差

其它数学计算实践:

  • 大数
  • 数学 crates 小结

21.1.1. 线性代数

  • 矩阵相加
  • 矩阵相乘
  • 标量、vector、矩阵相乘
  • Vector 比较
  • Vector 范数
  • 矩阵求逆
  • (反)序列化矩阵

21.1.1.1. 矩阵相加

ndarray-badge cat-science-badge

使用 ndarray::arr2 创建两个二维(2-D)矩阵,并按元素方式求和。

注意:sum 的计算方式为 let sum = &a + &b,借用 & 运算符获得 ab 的引用,可避免销毁他们,使它们可以稍后显示。这样,就创建了一个包含其和的新数组。

use ndarray::arr2;

fn main() {
    let a = arr2(&[[1, 2, 3],
                   [4, 5, 6]]);

    let b = arr2(&[[6, 5, 4],
                   [3, 2, 1]]);

    let sum = &a + &b;

    println!("{}", a);
    println!("+");
    println!("{}", b);
    println!("=");
    println!("{}", sum);
}

21.1.1.2. 矩阵相乘

ndarray-badge cat-science-badge

使用 ndarray::arr2 创建两个矩阵,并使用 ndarray::ArrayBase::dot 对它们执行矩阵乘法。

use ndarray::arr2;

fn main() {
    let a = arr2(&[[1, 2, 3],
                   [4, 5, 6]]);

    let b = arr2(&[[6, 3],
                   [5, 2],
                   [4, 1]]);

    println!("{}", a.dot(&b));
}

21.1.1.3. 标量、vector、矩阵相乘

ndarray-badge cat-science-badge

使用 ndarray::arr1 创建一维(1-D)数组(vector),使用 ndarray::arr2 创建二维(2-D)数组(矩阵)。

首先,一个标量乘以一个 vector 得到另一个 vector。然后,使用 ndarray::Array2::dot 将矩阵乘以新的 vector(矩阵相乘使用 dot 函数,而 * 运算符执行元素方式的乘法)。

ndarray crate 中,根据上下文,一维数组可以解释为行 vector 或列 vector。如果 vector 表示的方向很重要,则必须使用只有一行或一列的二维(2-D)数组。在本实例中,vector 是右侧的一维(1-D)数组,因此 dot 函数将其处理为列 vector。

use ndarray::{arr1, arr2, Array1};

fn main() {
    let scalar = 4;

    let vector = arr1(&[1, 2, 3]);

    let matrix = arr2(&[[4, 5, 6],
                        [7, 8, 9]]);

    let new_vector: Array1<_> = scalar * vector;
    println!("{}", new_vector);

    let new_matrix = matrix.dot(&new_vector);
    println!("{}", new_matrix);
}

21.1.1.4. Vector 比较

ndarray-badge

ndarray crate 支持多种创建数组的方法——此实例使用 fromstd::Vec 创建数组 ndarray::Array。然后,对数组以元素方式求和。

下面的实例按元素方式比较两个浮点型 vector。浮点数的存储通常不精确,因此很难进行精确的比较。但是,approx crate 中的 assert_abs_diff_eq! 宏允许方便地比较浮点型元素。要将 approxndarray 两个 crate一起使用,必须在 Cargo.toml 文件中的 ndarray 依赖项添加 approx 特性。例如:ndarray = { version = "0.13", features = ["approx"] }

此实例还包含其它所有权示例。在这里,let z = a + b 执行后,会销毁 a and b,然后所有权会转移到 z。或者,let w = &c + &d 创建一个新的 vector,而不销毁 c 或者 d,允许以后对它们进行修改。有关其它详细信息,请参见带有两个数组的二进制运算符

use approx::assert_abs_diff_eq;
use ndarray::Array;

fn main() {
  let a = Array::from(vec![1., 2., 3., 4., 5.]);
  let b = Array::from(vec![5., 4., 3., 2., 1.]);
  let mut c = Array::from(vec![1., 2., 3., 4., 5.]);
  let mut d = Array::from(vec![5., 4., 3., 2., 1.]);

  let z = a + b;
  let w =  &c + &d;

  assert_abs_diff_eq!(z, Array::from(vec![6., 6., 6., 6., 6.]));

  println!("c = {}", c);
  c[0] = 10.;
  d[1] = 10.;

  assert_abs_diff_eq!(w, Array::from(vec![6., 6., 6., 6., 6.]));

}

21.1.1.5. Vector 范数

ndarray-badge

这个实例展示了 Array1 类型、ArrayView1 类型、fold 方法,以及 dot 方法在计算给定 vector 的 l1l2 范数时的用法。

  • l2_norm 函数是两者中较简单的,它计算一个 vector 与自身的点积(dot product,数量积)的平方根。
  • l1_norm 函数通过 fold 运算来计算元素的绝对值(也可以通过 x.mapv(f64::abs).scalar_sum() 执行,但是会为 mapv 的结果分配一个新的数组)。

请注意:l1_norml2_norm 都采用 ArrayView1 类型。这个实例考虑了 vector 范数,所以范数函数只需要接受一维视图(ArrayView1)。虽然函数可以使用类型为 &Array1<f64> 的参数,但这将要求调用方引用拥有所有权的数组,这比访问视图更为严格(因为视图可以从任意数组或视图创建,而不仅仅是从拥有所有权的数组创建)。

ArrayArrayView 都是 ArrayBase 的类型别名。于是,大多数的调用方参数类型可以是 &ArrayBase<S, Ix1> where S: Data,这样调用方就可以使用 &array 或者 &view 而不是 x.view()。如果该函数是公共 API 的一部分,那么对于用户来说,这可能是一个更好的选择。对于内部函数,更简明的 ArrayView1<f64> 或许更合适。

use ndarray::{array, Array1, ArrayView1};

fn l1_norm(x: ArrayView1<f64>) -> f64 {
    x.fold(0., |acc, elem| acc + elem.abs())
}

fn l2_norm(x: ArrayView1<f64>) -> f64 {
    x.dot(&x).sqrt()
}

fn normalize(mut x: Array1<f64>) -> Array1<f64> {
    let norm = l2_norm(x.view());
    x.mapv_inplace(|e| e/norm);
    x
}

fn main() {
    let x = array![1., 2., 3., 4., 5.];
    println!("||x||_2 = {}", l2_norm(x.view()));
    println!("||x||_1 = {}", l1_norm(x.view()));
    println!("Normalizing x yields {:?}", normalize(x));
}

21.1.1.6. 矩阵求逆

nalgebra-badge cat-science-badge

nalgebra::Matrix3 创建一个 3x3 的矩阵,如果可能的话,将其求逆。

use nalgebra::Matrix3;

fn main() {
    let m1 = Matrix3::new(2.0, 1.0, 1.0, 3.0, 2.0, 1.0, 2.0, 1.0, 2.0);
    println!("m1 = {}", m1);
    match m1.try_inverse() {
        Some(inv) => {
            println!("The inverse of m1 is: {}", inv);
        }
        None => {
            println!("m1 is not invertible!");
        }
    }
}

21.1.1.7. (反)序列化矩阵

ndarray-badge cat-science-badge

本实例实现将矩阵序列化为 JSON,以及从 JSON 反序列化出矩阵。序列化由 serde_json::to_string 处理,serde_json::from_str 则执行反序列化。

请注意:序列化后再反序列化将返回原始矩阵。

extern crate nalgebra;
extern crate serde_json;

use nalgebra::DMatrix;

fn main() -> Result<(), std::io::Error> {
    let row_slice: Vec<i32> = (1..5001).collect();
    let matrix = DMatrix::from_row_slice(50, 100, &row_slice);

    // 序列化矩阵
    let serialized_matrix = serde_json::to_string(&matrix)?;

    // 反序列化出矩阵
    let deserialized_matrix: DMatrix<i32> = serde_json::from_str(&serialized_matrix)?;

    // 验证反序列化出的矩阵 `deserialized_matrix` 等同于原始矩阵 `matrix`
    assert!(deserialized_matrix == matrix);

    Ok(())
}

21.1.2. 三角学

  • 计算三角形的边长
  • 验证正切(tan)等于正弦(sin)除以余弦(cos)
  • 地球上两点之间的距离

21.1.2.1. 计算三角形的边长

std-badge cat-science-badge

计算直角三角形斜边的长度,其中斜边的角度为 2 弧度,对边长度为 80。

fn main() {
    let angle: f64 = 2.0;
    let side_length = 80.0;

    let hypotenuse = side_length / angle.sin();

    println!("Hypotenuse: {}", hypotenuse);
}

21.1.2.2 验证正切(tan)等于正弦(sin)除以余弦(cos)

std-badge cat-science-badge

验证 tan(x) 是否等于 sin(x)/cos(x),其中 x=6。

fn main() {
    let x: f64 = 6.0;

    let a = x.tan();
    let b = x.sin() / x.cos();

    assert_eq!(a, b);
}

21.1.2.3. 地球上两点之间的距离

std-badge

默认情况下,Rust 提供了数学上的浮点数方法,例如:三角函数、平方根、弧度和度数之间的转换函数等。

下面的实例使用半正矢公式计算地球上两点之间的距离(以公里为单位)。两个点用一对经纬度表示,然后,to_radians 将它们转换为弧度。sincospowi 以及 sqrt 计算中心角。最终,可以计算出距离。

fn main() {
    let earth_radius_kilometer = 6371.0_f64;
    let (paris_latitude_degrees, paris_longitude_degrees) = (48.85341_f64, -2.34880_f64);
    let (london_latitude_degrees, london_longitude_degrees) = (51.50853_f64, -0.12574_f64);

    let paris_latitude = paris_latitude_degrees.to_radians();
    let london_latitude = london_latitude_degrees.to_radians();

    let delta_latitude = (paris_latitude_degrees - london_latitude_degrees).to_radians();
    let delta_longitude = (paris_longitude_degrees - london_longitude_degrees).to_radians();

    let central_angle_inner = (delta_latitude / 2.0).sin().powi(2)
        + paris_latitude.cos() * london_latitude.cos() * (delta_longitude / 2.0).sin().powi(2);
    let central_angle = 2.0 * central_angle_inner.sqrt().asin();

    let distance = earth_radius_kilometer * central_angle;

    println!(
        "Distance between Paris and London on the surface of Earth is {:.1} kilometers",
        distance
    );
}

21.1.3. 复数

  • 创建复数
  • 复数相加
  • 复数的数学函数

21.1.3.1. 创建复数

num-badge cat-science-badge

创建类型 num::complex::Complex 的复数,复数的实部和虚部必须是同一类型。

fn main() {
    let complex_integer = num::complex::Complex::new(10, 20);
    let complex_float = num::complex::Complex::new(10.1, 20.1);

    println!("Complex integer: {}", complex_integer);
    println!("Complex float: {}", complex_float);
}

21.1.3.2. 复数相加

num-badge cat-science-badge

对复数执行数学运算与对内置类型执行数学运算是一样的:计算的数字必须是相同的类型(如浮点数或整数)。

fn main() {
    let complex_num1 = num::complex::Complex::new(10.0, 20.0); // 必须为浮点数
    let complex_num2 = num::complex::Complex::new(3.1, -4.2);

    let sum = complex_num1 + complex_num2;

    println!("Sum: {}", sum);
}

21.1.3.3. 复数的数学函数

num-badge cat-science-badge

在与其它数学函数交互时,复数有一系列有趣的特性,尤其是和自然常数 e(欧拉数)类似的正弦相关函数。要将这些函数与复数一起使用,复数类型有几个内置函数,详细请参阅 num::complex::Complex

use std::f64::consts::PI;
use num::complex::Complex;

fn main() {
    let x = Complex::new(0.0, 2.0*PI);

    println!("e^(2i * pi) = {}", x.exp()); // =~1
}

21.1.4. 统计学

  • 集中趋势度量
  • 计算标准偏差

21.1.4.1. 集中趋势度量

std-badge cat-science-badge

本节实例计算 Rust 数组中包含的数据集的集中趋势度量。对于一个空的数据集,可能没有平均数、中位数或众数去计算,因此每个函数都返回 [Option] ,由调用者处理。

第一个实例是通过对数据引用生成一个迭代器,然后计算平均数(所有测量值的总和除以测量值的计数),并使用 [sum] 和 [len] 函数分别确定值的总和及值的计数。

fn main() {
    let data = [3, 1, 6, 1, 5, 8, 1, 8, 10, 11];

    let sum = data.iter().sum::<i32>() as f32;
    let count = data.len();

    let mean = match count {
       positive if positive > 0 => Some(sum  / count as f32),
       _ => None
    };

    println!("Mean of the data is {:?}", mean);
}

第二个实例使用快速选择算法(quick select algorithm)计算中位数,该算法只对已知可能包含中位数的数据集的分区进行排序,从而避免了完整[排序][sort]。该算法使用 [cmp] 和 [Ordering] 简便地地决定要检查的下一个分区,并使用 [split_at] 为每个步骤的下一个分区选择一个任意的枢轴量。

use std::cmp::Ordering;

fn partition(data: &[i32]) -> Option<(Vec<i32>, i32, Vec<i32>)> {
    match data.len() {
        0 => None,
        _ => {
            let (pivot_slice, tail) = data.split_at(1);
            let pivot = pivot_slice[0];
            let (left, right) = tail.iter()
                .fold((vec![], vec![]), |mut splits, next| {
                    {
                        let (ref mut left, ref mut right) = &mut splits;
                        if next < &pivot {
                            left.push(*next);
                        } else {
                            right.push(*next);
                        }
                    }
                    splits
                });

            Some((left, pivot, right))
        }
    }
}

fn select(data: &[i32], k: usize) -> Option<i32> {
    let part = partition(data);

    match part {
        None => None,
        Some((left, pivot, right)) => {
            let pivot_idx = left.len();

            match pivot_idx.cmp(&k) {
                Ordering::Equal => Some(pivot),
                Ordering::Greater => select(&left, k),
                Ordering::Less => select(&right, k - (pivot_idx + 1)),
            }
        },
    }
}

fn median(data: &[i32]) -> Option<f32> {
    let size = data.len();

    match size {
        even if even % 2 == 0 => {
            let fst_med = select(data, (even / 2) - 1);
            let snd_med = select(data, even / 2);

            match (fst_med, snd_med) {
                (Some(fst), Some(snd)) => Some((fst + snd) as f32 / 2.0),
                _ => None
            }
        },
        odd => select(data, odd / 2).map(|x| x as f32)
    }
}

fn main() {
    let data = [3, 1, 6, 1, 5, 8, 1, 8, 10, 11];

    let part = partition(&data);
    println!("Partition is {:?}", part);

    let sel = select(&data, 5);
    println!("Selection at ordered index {} is {:?}", 5, sel);

    let med = median(&data);
    println!("Median is {:?}", med);
}

最后一个实例使用可变的 [HashMap] 来计算众数,[fold] 和 [entry] API 用来从集合中收集每个不同整数的计数。[HashMap] 中最常见的值可以用 [max_by_key] 取得。

use std::collections::HashMap;

fn main() {
    let data = [3, 1, 6, 1, 5, 8, 1, 8, 10, 11];

    let frequencies = data.iter().fold(HashMap::new(), |mut freqs, value| {
        *freqs.entry(value).or_insert(0) += 1;
        freqs
    });

    let mode = frequencies
        .into_iter()
        .max_by_key(|&(_, count)| count)
        .map(|(value, _)| *value);

    println!("Mode of the data is {:?}", mode);
}

21.1.4.2. 计算标准偏差

std-badge cat-science-badge

本实例计算一组测量值的标准偏差和 z 分数(z-score)。

标准偏差定义为方差的平方根(用 f32 浮点型的 [sqrt] 计算),其中方差是每个测量值与平均数之间的平方差的除以测量次数。

z 分数(z-score)是指单个测量值偏离数据集平均数的标准差数。

fn mean(data: &[i32]) -> Option<f32> {
    let sum = data.iter().sum::<i32>() as f32;
    let count = data.len();

    match count {
        positive if positive > 0 => Some(sum / count as f32),
        _ => None,
    }
}

fn std_deviation(data: &[i32]) -> Option<f32> {
    match (mean(data), data.len()) {
        (Some(data_mean), count) if count > 0 => {
            let variance = data.iter().map(|value| {
                let diff = data_mean - (*value as f32);

                diff * diff
            }).sum::<f32>() / count as f32;

            Some(variance.sqrt())
        },
        _ => None
    }
}

fn main() {
    let data = [3, 1, 6, 1, 5, 8, 1, 8, 10, 11];

    let data_mean = mean(&data);
    println!("Mean is {:?}", data_mean);

    let data_std_deviation = std_deviation(&data);
    println!("Standard deviation is {:?}", data_std_deviation);

    let zscore = match (data_mean, data_std_deviation) {
        (Some(mean), Some(std_deviation)) => {
            let diff = data[4] as f32 - mean;

            Some(diff / std_deviation)
        },
        _ => None
    };
    println!("Z-score of data at index 4 (with value {}) is {:?}", data[4], zscore);
}

21.1.5. 其它数学计算

  • 大数
  • 数学 crates 小结

21.1.5.1. 大数

num-badge cat-science-badge

BigInt 使得超过 128 位的大整数计算成为可能。

use num::bigint::{BigInt, ToBigInt};

fn factorial(x: i32) -> BigInt {
    if let Some(mut factorial) = 1.to_bigint() {
        for i in 1..(x+1) {
            factorial = factorial * i;
        }
        factorial
    }
    else {
        panic!("Failed to calculate factorial!");
    }
}

fn main() {
    println!("{}! equals {}", 100, factorial(100));
}

21.1.6. 数学 crates 小结

涉及 crates:

  • num
  • approx
  • nalgebra
  • ndarray
  • serde
  • serde_json

22. 文本处理

文本处理实践涵盖正则表达式、字符串解析。

正则表达式实践:

  • 验证并提取电子邮件登录信息
  • 从文本提取标签元素唯一的列表
  • 从文本提取电话号码
  • 通过匹配多个正则表达式来筛选日志文件
  • 文本模式替换

字符串解析实践:

  • 收集 Unicode 字符
  • 自定义结构体并实现 FromStr trait
  • 文本处理 crates 小结

22.1. 正则表达式

  • 验证并提取电子邮件登录信息
  • 从文本提取标签元素唯一的列表
  • 从文本提取电话号码
  • 通过匹配多个正则表达式来筛选日志文件
  • 文本模式替换

22.1.1. 验证并提取电子邮件登录信息

regex-badge lazy_static-badge cat-text-processing-badge

验证电子邮件地址的格式是否正确,并提取 @ 符号之前的所有内容。

use lazy_static::lazy_static;

use regex::Regex;

fn extract_login(input: &str) -> Option<&str> {
    lazy_static! {
        static ref RE: Regex = Regex::new(r"(?x)
            ^(?P<login>[^@\s]+)@
            ([[:word:]]+\.)*
            [[:word:]]+$
            ").unwrap();
    }
    RE.captures(input).and_then(|cap| {
        cap.name("login").map(|login| login.as_str())
    })
}

fn main() {
    assert_eq!(extract_login(r"I❤email@example.com"), Some(r"I❤email"));
    assert_eq!(
        extract_login(r"sdf+sdsfsd.as.sdsd@jhkk.d.rl"),
        Some(r"sdf+sdsfsd.as.sdsd")
    );
    assert_eq!(extract_login(r"More@Than@One@at.com"), None);
    assert_eq!(extract_login(r"Not an email@email"), None);
}

22.1.2. 从文本提取标签元素唯一的列表

regex-badge lazy_static-badge cat-text-processing-badge

本实例展示从文本中提取、排序和去除标签列表的重复元素。

这里给出的标签正则表达式只捕获以字母开头的拉丁语标签,完整的 twitter 标签正则表达式要复杂得多。

use lazy_static::lazy_static;

use regex::Regex;
use std::collections::HashSet;

fn extract_hashtags(text: &str) -> HashSet<&str> {
    lazy_static! {
        static ref HASHTAG_REGEX : Regex = Regex::new(
                r"\#[a-zA-Z][0-9a-zA-Z_]*"
            ).unwrap();
    }
    HASHTAG_REGEX.find_iter(text).map(|mat| mat.as_str()).collect()
}

fn main() {
    let tweet = "Hey #world, I just got my new #dog, say hello to Till. #dog #forever #2 #_ ";
    let tags = extract_hashtags(tweet);
    assert!(tags.contains("#dog") && tags.contains("#forever") && tags.contains("#world"));
    assert_eq!(tags.len(), 3);
}

22.1.3. 从文本提取电话号码

regex-badge cat-text-processing-badge

使用 Regex::captures_iter 处理一个文本字符串,以捕获多个电话号码。这里的例子中是美国电话号码格式。

use error_chain::error_chain;

use regex::Regex;
use std::fmt;

error_chain!{
    foreign_links {
        Regex(regex::Error);
        Io(std::io::Error);
    }
}

struct PhoneNumber<'a> {
    area: &'a str,
    exchange: &'a str,
    subscriber: &'a str,
}

impl<'a> fmt::Display for PhoneNumber<'a> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "1 ({}) {}-{}", self.area, self.exchange, self.subscriber)
    }
}

fn main() -> Result<()> {
    let phone_text = "
    +1 505 881 9292 (v) +1 505 778 2212 (c) +1 505 881 9297 (f)
    (202) 991 9534
    Alex 5553920011
    1 (800) 233-2010
    1.299.339.1020";

    let re = Regex::new(
        r#"(?x)
          (?:\+?1)?                       # 国家代码,可选项
          [\s\.]?
          (([2-9]\d{2})|\(([2-9]\d{2})\)) # 地区代码
          [\s\.\-]?
          ([2-9]\d{2})                    # 交换代码
          [\s\.\-]?
          (\d{4})                         # 用户号码"#,
    )?;

    let phone_numbers = re.captures_iter(phone_text).filter_map(|cap| {
        let groups = (cap.get(2).or(cap.get(3)), cap.get(4), cap.get(5));
        match groups {
            (Some(area), Some(ext), Some(sub)) => Some(PhoneNumber {
                area: area.as_str(),
                exchange: ext.as_str(),
                subscriber: sub.as_str(),
            }),
            _ => None,
        }
    });

    assert_eq!(
        phone_numbers.map(|m| m.to_string()).collect::<Vec<_>>(),
        vec![
            "1 (505) 881-9292",
            "1 (505) 778-2212",
            "1 (505) 881-9297",
            "1 (202) 991-9534",
            "1 (555) 392-0011",
            "1 (800) 233-2010",
            "1 (299) 339-1020",
        ]
    );

    Ok(())
}

22.1.4. 通过匹配多个正则表达式来筛选日志文件

regex-badge cat-text-processing-badge

读取名为 application.log 的文件,并且只输出包含下列内容的行:“version X.X.X”、端口为 443 的 IP 地址(如 “192.168.0.1:443”)、特定警告。

正则表达集构造器 regex::RegexSetBuilder 构建了正则表达式集 regex::RegexSet。由于反斜杠在正则表达式中非常常见,因此使用原始字符串字面量可以使它们更具可读性。

use error_chain::error_chain;

use std::fs::File;
use std::io::{BufReader, BufRead};
use regex::RegexSetBuilder;

error_chain! {
    foreign_links {
        Io(std::io::Error);
        Regex(regex::Error);
    }
}

fn main() -> Result<()> {
    let log_path = "application.log";
    let buffered = BufReader::new(File::open(log_path)?);

    let set = RegexSetBuilder::new(&[
        r#"version "\d\.\d\.\d""#,
        r#"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:443"#,
        r#"warning.*timeout expired"#,
    ]).case_insensitive(true)
        .build()?;

    buffered
        .lines()
        .filter_map(|line| line.ok())
        .filter(|line| set.is_match(line.as_str()))
        .for_each(|x| println!("{}", x));

    Ok(())
}

22.1.5. 文本模式替换

regex-badge lazy_static-badge cat-text-processing-badge

将所有出现的国际标准 ISO 8601 日期模式 YYYY-MM-DD 替换为具有斜杠的等效美式英语日期模式。例如: 2013-01-15 替换为 01/15/2013

Regex::replace_all 方法将替换整个正则表示匹配的所有内容。&str 实现了 Replacer trait,它允许类似 $abcde 的变量引用相应的搜索匹配模式(search regex)中的命名捕获组 (?P<abcde>REGEX)。有关示例和转义的详细信息,请参阅替换字符串语法

注:正则表达式的使用,需要了解匹配规则:全文匹配(match regex)、搜索匹配(search regex)、替换匹配(replace regex)。

use lazy_static::lazy_static;

use std::borrow::Cow;
use regex::Regex;

fn reformat_dates(before: &str) -> Cow<str> {
    lazy_static! {
        static ref ISO8601_DATE_REGEX : Regex = Regex::new(
            r"(?P<y>\d{4})-(?P<m>\d{2})-(?P<d>\d{2})"
            ).unwrap();
    }
    ISO8601_DATE_REGEX.replace_all(before, "$m/$d/$y")
}

fn main() {
    let before = "2012-03-14, 2013-01-15 and 2014-07-05";
    let after = reformat_dates(before);
    assert_eq!(after, "03/14/2012, 01/15/2013 and 07/05/2014");
}

22.2. 字符串解析

  • 收集 Unicode 字符
  • 自定义结构体并实现 FromStr trait
  • 文本处理 crates 小结

22.2.1. 收集 Unicode 字符

unicode-segmentation-badge cat-text-processing-badge

使用 unicode-segmentation crate 中的 UnicodeSegmentation::graphemes 函数,从 UTF-8 字符串中收集个别的 Unicode 字符。

use unicode_segmentation::UnicodeSegmentation;

fn main() {
    let name = "José Guimarães\r\n";
    let graphemes = UnicodeSegmentation::graphemes(name, true)
    	.collect::<Vec<&str>>();
    assert_eq!(graphemes[3], "é");
}

22.2.2. 自定义结构体并实现 FromStr trait

std-badge cat-text-processing-badge

本实例中,创建一个自定义结构体 RGB 并实现 FromStr trait,以将提供的颜色十六进制代码转换为其 RGB 颜色代码。

use std::str::FromStr;

#[derive(Debug, PartialEq)]
struct RGB {
    r: u8,
    g: u8,
    b: u8,
}

impl FromStr for RGB {
    type Err = std::num::ParseIntError;

    // 解析格式为 '#rRgGbB..' 的颜色十六进制代码
    // 将其转换为 'RGB' 实例
    fn from_str(hex_code: &str) -> Result<Self, Self::Err> {
    
        // u8::from_str_radix(src: &str, radix: u32) 
        // 将给定的字符串切片转换为 u8
        let r: u8 = u8::from_str_radix(&hex_code[1..3], 16)?;
        let g: u8 = u8::from_str_radix(&hex_code[3..5], 16)?;
        let b: u8 = u8::from_str_radix(&hex_code[5..7], 16)?;

        Ok(RGB { r, g, b })
    }
}

fn main() {
    let code: &str = &r"#fa7268";
    match RGB::from_str(code) {
        Ok(rgb) => {
            println!(
                r"The RGB color code is: R: {} G: {} B: {}",
                rgb.r, rgb.g, rgb.b
            );
        }
        Err(_) => {
            println!("{} is not a valid color hex code!", code);
        }
    }

    // 测试 from_str 函数执行是否符合预期
    assert_eq!(
        RGB::from_str(&r"#fa7268").unwrap(),
        RGB {
            r: 250,
            g: 114,
            b: 104
        }
    );
}

22.3. 文本处理 crates 小结

涉及 crates:

  • error-chain
  • lazy_static
  • regex
  • unicode-segmentation

23. Web 编程

Web 编程实践涵盖提取链接、URL、媒体类型(MIME)、电子邮件、客户端、请求处理、Web API 调用、下载。

提取链接实践:

  • 从 HTML 网页中提取所有链接
  • 检查网页死链
  • 从 MediaWiki 标记页面提取所有唯一性链接
  • 提取链接 crates 小结

URL 实践:

  • 解析 URL 字符串为 Url 类型
  • 通过移除路径段创建基本 URL
  • 从基本 URL 创建新 URLs
  • 提取 URL 源(scheme/ host/ port)
  • 从 URL 移除片段标识符和查询对
  • URL crates 小结

媒体类型(MIME)实践:

  • 从字符串获取 MIME 类型
  • 从文件名获取 MIME 类型
  • 解析 HTTP 响应的 MIME 类型
  • 媒体类型(MIME)crates 小结

电子邮件实践。

客户端实践:

  • 请求处理
    • 发出 HTTP GET 请求
    • 为 REST 请求设置自定义消息标头和 URL 参数
  • Web API 调用
    • 查询 GitHub API
    • 检查 API 资源是否存在
    • 使用 GitHub API 创建和删除 Gist
    • 使用 RESTful API 分页
    • 处理速率受限 API
  • 下载
    • 下载文件到临时目录
    • 使用 HTTP range 请求头进行部分下载
    • POST 文件到 paste-rs
    • 客户端 crates 小结

23.1. 提取链接

  • 从 HTML 网页中提取所有链接
  • 检查网页死链
  • 从 MediaWiki 标记页面提取所有唯一性链接
  • 提取链接 crates 小结

23.1.1. 从 HTML 网页中提取所有链接

reqwest-badge select-badge cat-net-badge

使用 reqwest::get 执行 HTTP GET 请求,然后使用 Document::from_read 将响应信息解析为 HTML 文档。以“a”(锚元素)作为结构体 Name 的参数,将结构体 Name 作为条件,使用 find 方法检索所有链接。在结构体 Selection 上调用 filter_map 方法,从具有 “href” attr(属性)的链接检索所有 URL。

use error_chain::error_chain;
use select::document::Document;
use select::predicate::Name;

error_chain! {
      foreign_links {
          ReqError(reqwest::Error);
          IoError(std::io::Error);
      }
}

#[tokio::main]
async fn main() -> Result<()> {
  let res = reqwest::get("https://www.rust-lang.org/zh-CN/")
    .await?
    .text()
    .await?;

  Document::from(res.as_str())
    .find(Name("a"))
    .filter_map(|n| n.attr("href"))
    .for_each(|x| println!("{}", x));

  Ok(())
}

23.1.2. 检查网页死链

reqwest-badge select-badge url-badge cat-net-badge

调用 get_base_url 方法检索 base URL,如果 HTML 文档有 base 标签,从 base 标记获取 href attr,初始 URL 的默认值是 Position::BeforePath

遍历 HTML 文档中的链接,并创建一个 tokio::spawn 任务,该任务将使用 url::ParseOptions 结构体和 Url::parse 方法解析单个链接。任务执行中,使用 reqwest 向链接发起请求,并验证状态码结构体 StatusCode。实例中使用 await 异步等待任务完成,然后结束程序。

use error_chain::error_chain;
use reqwest::StatusCode;
use select::document::Document;
use select::predicate::Name;
use std::collections::HashSet;
use url::{Position, Url};

error_chain! {
  foreign_links {
      ReqError(reqwest::Error);
      IoError(std::io::Error);
      UrlParseError(url::ParseError);
      JoinError(tokio::task::JoinError);
  }
}

async fn get_base_url(url: &Url, doc: &Document) -> Result<Url> {
  let base_tag_href = doc.find(Name("base")).filter_map(|n| n.attr("href")).nth(0);
  let base_url =
    base_tag_href.map_or_else(|| Url::parse(&url[..Position::BeforePath]), Url::parse)?;
  Ok(base_url)
}

async fn check_link(url: &Url) -> Result<bool> {
  let res = reqwest::get(url.as_ref()).await?;
  Ok(res.status() != StatusCode::NOT_FOUND)
}

#[tokio::main]
async fn main() -> Result<()> {
  let url = Url::parse("https://www.rust-lang.org/en-US/")?;
  let res = reqwest::get(url.as_ref()).await?.text().await?;
  let document = Document::from(res.as_str());
  let base_url = get_base_url(&url, &document).await?;
  let base_parser = Url::options().base_url(Some(&base_url));
  let links: HashSet<Url> = document
    .find(Name("a"))
    .filter_map(|n| n.attr("href"))
    .filter_map(|link| base_parser.parse(link).ok())
    .collect();
    let mut tasks = vec![];

    for link in links {
        tasks.push(tokio::spawn(async move {
            if check_link(&link).await.unwrap() {
                println!("{} is OK", link);
            } else {
                println!("{} is Broken", link);
            }
        }));
    }

    for task in tasks {
        task.await?
    }

  Ok(())
}

23.1.3. 从 MediaWiki 标记页面提取所有唯一性链接

reqwest-badge regex-badge cat-net-badge

使用 reqwest::get 获取 MediaWiki 页面的源代码,然后使用 Regex::captures_iter 查找内部和外部链接的所有条目。使用智能指针 Cow 可以提供对借用数据的不可变引用,避免分配过多的字符串

阅读 MediaWiki 链接语法详细了解。

use lazy_static::lazy_static;
use regex::Regex;
use std::borrow::Cow;
use std::collections::HashSet;
use std::error::Error;

fn extract_links(content: &str) -> HashSet<Cow<str>> {
  lazy_static! {
    static ref WIKI_REGEX: Regex = Regex::new(
      r"(?x)
                \[\[(?P<internal>[^\[\]|]*)[^\[\]]*\]\]    # internal links
                |
                (url=|URL\||\[)(?P<external>http.*?)[ \|}] # external links
            "
    )
    .unwrap();
  }

  let links: HashSet<_> = WIKI_REGEX
    .captures_iter(content)
    .map(|c| match (c.name("internal"), c.name("external")) {
      (Some(val), None) => Cow::from(val.as_str().to_lowercase()),
      (None, Some(val)) => Cow::from(val.as_str()),
      _ => unreachable!(),
    })
    .collect();

  links
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
  let content = reqwest::get(
    "https://en.wikipedia.org/w/index.php?title=Rust_(programming_language)&action=raw",
  )
  .await?
  .text()
  .await?;

  println!("{:#?}", extract_links(content.as_str()));

  Ok(())
}

23.1.4. 提取链接 crates 小结

涉及 crates:

  • error-chain
  • lazy_static
  • regex
  • reqwest
  • select
  • tokio
  • url

23.2. URL

  • 解析 URL 字符串为 Url 类型
  • 通过移除路径段创建基本 URL
  • 从基本 URL 创建新 URLs
  • 提取 URL 源(scheme/ host/ port)
  • 从 URL 移除片段标识符和查询对
  • URL crates 小结

23.2.1. 解析 URL 字符串为 Url 类型

url-badge cat-net-badge

url crate 中的 parse 方法验证并解析 &str 切片为 Url 结构体。如果输入字符串的格式不正确,解析方法 parse 会返回 Result<Url, ParseError>

一旦 URL 被解析,它就可以使用 Url 结构体类型中的所有方法。

use url::{Url, ParseError};

fn main() -> Result<(), ParseError> {
    let s = "https://github.com/rust-lang/rust/issues?labels=E-easy&state=open";

    let parsed = Url::parse(s)?;
    println!("The path part of the URL is: {}", parsed.path());

    Ok(())
}

23.2.2. 通过移除路径段创建基本 URL

url-badge cat-net-badge

基本 URL(base URL)包括协议和域名。但基本 URL(base URL)不包括目录、文件或查询字符串,这些项都可以从给定的 URL 中剥离出来。创建基本 URL(base URL)时,通过 PathSegmentsMut::clear 方法移除目录和文件路径,通过方法 Url::set_query 移除查询字符串。

use error_chain::error_chain;

use url::Url;

error_chain! {
    foreign_links {
        UrlParse(url::ParseError);
    }
    errors {
        CannotBeABase
    }
}

fn main() -> Result<()> {
    let full = "https://github.com/rust-lang/cargo?asdf";

    let url = Url::parse(full)?;
    let base = base_url(url)?;

    assert_eq!(base.as_str(), "https://github.com/");
    println!("The base of the URL is: {}", base);

    Ok(())
}

fn base_url(mut url: Url) -> Result<Url> {
    match url.path_segments_mut() {
        Ok(mut path) => {
            path.clear();
        }
        Err(_) => {
            return Err(Error::from_kind(ErrorKind::CannotBeABase));
        }
    }

    url.set_query(None);

    Ok(url)
}

23.2.3. 从基本 URL 创建新 URLs

url-badge cat-net-badge

join 方法从基路径和相对路径创建新的 URL。

use url::{Url, ParseError};

fn main() -> Result<(), ParseError> {
    let path = "/rust-lang/cargo";

    let gh = build_github_url(path)?;

    assert_eq!(gh.as_str(), "https://github.com/rust-lang/cargo");
    println!("The joined URL is: {}", gh);

    Ok(())
}

fn build_github_url(path: &str) -> Result<Url, ParseError> {
    const GITHUB: &'static str = "https://github.com";

    let base = Url::parse(GITHUB).expect("hardcoded URL is known to be valid");
    let joined = base.join(path)?;

    Ok(joined)
}

23.2.4. 提取 URL 源(scheme / host / port)

url-badge cat-net-badge

Url 结构体定义了多种方法,以便于提取有关它所表示的 URL 的信息。

use url::{Url, Host, ParseError};

fn main() -> Result<(), ParseError> {
    let s = "ftp://rust-lang.org/examples";

    let url = Url::parse(s)?;

    assert_eq!(url.scheme(), "ftp");
    assert_eq!(url.host(), Some(Host::Domain("rust-lang.org")));
    assert_eq!(url.port_or_known_default(), Some(21));
    println!("The origin is as expected!");

    Ok(())
}

origin 方法产生相同的结果。

use error_chain::error_chain;

use url::{Url, Origin, Host};

error_chain! {
    foreign_links {
        UrlParse(url::ParseError);
    }
}

fn main() -> Result<()> {
    let s = "ftp://rust-lang.org/examples";

    let url = Url::parse(s)?;

    let expected_scheme = "ftp".to_owned();
    let expected_host = Host::Domain("rust-lang.org".to_owned());
    let expected_port = 21;
    let expected = Origin::Tuple(expected_scheme, expected_host, expected_port);

    let origin = url.origin();
    assert_eq!(origin, expected);
    println!("The origin is as expected!");

    Ok(())
}

23.2.5. 从 URL 移除片段标识符和查询对

url-badge cat-net-badge

解析 Url 结构体,并使用 url::Position 枚举对其进行切片,以去除不需要的 URL 片段。

use url::{Url, Position, ParseError};

fn main() -> Result<(), ParseError> {
    let parsed = Url::parse("https://github.com/rust-lang/rust/issues?labels=E-easy&state=open")?;
    let cleaned: &str = &parsed[..Position::AfterPath];
    println!("cleaned: {}", cleaned);
    Ok(())
}

23.2.6. URL crates 小结

涉及 crates:

  • error-chain
  • url

23.3. 媒体类型(MIME)

  • 从字符串获取 MIME 类型
  • 从文件名获取 MIME 类型
  • 解析 HTTP 响应的 MIME 类型
  • 媒体类型(MIME)crates 小结

23.3.1. 从字符串获取 MIME 类型

mime-badge cat-encoding-badge

下面的实例展示如何使用 mime crate 从字符串解析出 MIME 类型。FromStrError 结构体在 unwrap_or 子句中生成默认的 MIME 类型。

use mime::{Mime, APPLICATION_OCTET_STREAM};

fn main() {
    let invalid_mime_type = "i n v a l i d";
    let default_mime = invalid_mime_type
        .parse::<Mime>()
        .unwrap_or(APPLICATION_OCTET_STREAM);

    println!(
        "MIME for {:?} used default value {:?}",
        invalid_mime_type, default_mime
    );

    let valid_mime_type = "TEXT/PLAIN";
    let parsed_mime = valid_mime_type
        .parse::<Mime>()
        .unwrap_or(APPLICATION_OCTET_STREAM);

    println!(
        "MIME for {:?} was parsed as {:?}",
        valid_mime_type, parsed_mime
    );
}

23.3.2. 从文件名获取 MIME 类型

mime-badge cat-encoding-badge

下面的实例展示如何使用 mime crate 从给定的文件名返回正确的 MIME 类型。程序将检查文件扩展名并与已知的 MIME 类型列表匹配,返回值为 mime:Mime

use mime::Mime;

fn find_mimetype (filename : &String) -> Mime{

    let parts : Vec<&str> = filename.split('.').collect();

    let res = match parts.last() {
            Some(v) =>
                match *v {
                    "png" => mime::IMAGE_PNG,
                    "jpg" => mime::IMAGE_JPEG,
                    "json" => mime::APPLICATION_JSON,
                    &_ => mime::TEXT_PLAIN,
                },
            None => mime::TEXT_PLAIN,
        };
    return res;
}

fn main() {
    let filenames = vec!("foobar.jpg", "foo.bar", "foobar.png");
    for file in filenames {
        let mime = find_mimetype(&file.to_owned());
     	println!("MIME for {}: {}", file, mime);
     }

}

23.3.3. 解析 HTTP 响应的 MIME 类型

reqwest-badge mime-badge cat-net-badge cat-encoding-badge

当从 reqwest 接收到 HTTP 响应时,MIME 类型或媒体类型可以在实体头部的 Content-Type 标头中找到。reqwest::header::HeaderMap::get 方法将标头检索为结构体 reqwest::header::HeaderValue,结构体可以转换为字符串。然后 mime crate 可以解析它,生成 mime::Mime 值。

[mime] crate 也定义了一些常用的 MIME 类型。

请注意:reqwest::header 模块是从 http crate 导出的。

use error_chain::error_chain;
use mime::Mime;
use std::str::FromStr;
use reqwest::header::CONTENT_TYPE;

 error_chain! {
    foreign_links {
        Reqwest(reqwest::Error);
        Header(reqwest::header::ToStrError);
        Mime(mime::FromStrError);
    }
 }

#[tokio::main]
async fn main() -> Result<()> {
    let response = reqwest::get("https://www.rust-lang.org/logos/rust-logo-32x32.png").await?;
    let headers = response.headers();

    match headers.get(CONTENT_TYPE) {
        None => {
            println!("The response does not contain a Content-Type header.");
        }
        Some(content_type) => {
            let content_type = Mime::from_str(content_type.to_str()?)?;
            let media_type = match (content_type.type_(), content_type.subtype()) {
                (mime::TEXT, mime::HTML) => "a HTML document",
                (mime::TEXT, _) => "a text document",
                (mime::IMAGE, mime::PNG) => "a PNG image",
                (mime::IMAGE, _) => "an image",
                _ => "neither text nor image",
            };

            println!("The reponse contains {}.", media_type);
        }
    };

    Ok(())
}

23.3.4. 媒体类型(MIME)crates 小结

涉及 crates:

  • error-chain
  • mime
  • reqwest
  • tokio

23.4. 电子邮件

相关 crates:

  • reqwest
  • select
  • lettre
  • lettre_email

23.5. 客户端

  • 请求处理
    • 发出 HTTP GET 请求
    • 为 REST 请求设置自定义消息标头和 URL 参数
  • Web API 调用
    • 查询 GitHub API
    • 检查 API 资源是否存在
    • 使用 GitHub API 创建和删除 Gist
    • 使用 RESTful API 分页
    • 处理速率受限 API
  • 下载
    • 下载文件到临时目录
    • 使用 HTTP range 请求头进行部分下载
    • POST 文件到 paste-rs
    • 客户端 crates 小结

23.5.1. 请求处理

  • 发出 HTTP GET 请求
  • 为 REST 请求设置自定义消息标头和 URL 参数

23.5.1.1. 发出 HTTP GET 请求

reqwest-badge cat-net-badge

解析提供的 URL,并使用 reqwest::blocking::get 发起同步 HTTP GET 请求。打印获取的响应消息状态和标头 reqwest::blocking::Response。使用 read_to_string 将 HTTP 响应消息主体正文读入到指派的字符串 String

use error_chain::error_chain;
use std::io::Read;

error_chain! {
    foreign_links {
        Io(std::io::Error);
        HttpRequest(reqwest::Error);
    }
}

fn main() -> Result<()> {
    let mut res = reqwest::blocking::get("http://httpbin.org/get")?;
    let mut body = String::new();
    res.read_to_string(&mut body)?;

    println!("Status: {}", res.status());
    println!("Headers:\n{:#?}", res.headers());
    println!("Body:\n{}", body);

    Ok(())
}

异步

常见的方法是通过包含 tokio 在内的类似异步执行器,使主函数执行异步,但检索处理相同的信息。

本实例中,tokio::main 处理所有繁重的执行器设置,并允许在 .await 之前不阻塞的按顺序执行代码。

也可以使用 reqwest 的异步版本,其请求函数 reqwest::get 和响应结构体 reqwest::Response 都是异步的。

use error_chain::error_chain;

error_chain! {
    foreign_links {
        Io(std::io::Error);
        HttpRequest(reqwest::Error);
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    let res = reqwest::get("http://httpbin.org/get").await?;
    println!("Status: {}", res.status());
    println!("Headers:\n{:#?}", res.headers());

    let body = res.text().await?;
    println!("Body:\n{}", body);
    Ok(())
}

23.5.1.2. 为 REST 请求设置自定义消息标头和 URL 参数

reqwest-badge hyper-badge url-badge cat-net-badge

本实例中为 HTTP GET 请求设置标准的和自定义的 HTTP 消息标头以及 URL 参数。使用 hyper::header! 宏创建 XPoweredBy 类型的自定义消息标头。

使用 Url::parse_with_params 构建复杂的 URL。使用 RequestBuilder::header 方法设置标准消息标头 header::UserAgentheader::Authorization,以及自定义类型 XPoweredBy,然后使用 RequestBuilder::send 发起请求。

请求的服务目标为 http://httpbin.org/headers,其响应结果是包含所有请求的消息标头的 JSON 字典,易于验证。

use error_chain::error_chain;
use serde::Deserialize;

use std::collections::HashMap;
use url::Url;
use reqwest::Client;
use reqwest::header::{UserAgent, Authorization, Bearer};

header! { (XPoweredBy, "X-Powered-By") => [String] }

#[derive(Deserialize, Debug)]
pub struct HeadersEcho {
    pub headers: HashMap<String, String>,
}

error_chain! {
    foreign_links {
        Reqwest(reqwest::Error);
        UrlParse(url::ParseError);
    }
}

fn main() -> Result<()> {
    let url = Url::parse_with_params("http://httpbin.org/headers",
                                     &[("lang", "rust"), ("browser", "servo")])?;

    let mut response = Client::new()
        .get(url)
        .header(UserAgent::new("Rust-test"))
        .header(Authorization(Bearer { token: "DEadBEEfc001cAFeEDEcafBAd".to_owned() }))
        .header(XPoweredBy("Guybrush Threepwood".to_owned()))
        .send()?;

    let out: HeadersEcho = response.json()?;
    assert_eq!(out.headers["Authorization"],
               "Bearer DEadBEEfc001cAFeEDEcafBAd");
    assert_eq!(out.headers["User-Agent"], "Rust-test");
    assert_eq!(out.headers["X-Powered-By"], "Guybrush Threepwood");
    assert_eq!(response.url().as_str(),
               "http://httpbin.org/headers?lang=rust&browser=servo");

    println!("{:?}", out);
    Ok(())
}

23.5.2. Web API 调用

  • 查询 GitHub API
  • 检查 API 资源是否存在
  • 使用 GitHub API 创建和删除 Gist
  • 使用 RESTful API 分页
  • 处理速率受限 API

23.5.2.1. 查询 GitHub API

reqwest-badge serde-badge cat-net-badge cat-encoding-badge

使用 reqwest::get 查询 点赞的用户 API v3,以获取某个 GitHub 项目的所有点赞用户的列表。使用 Response::json 将响应信息 reqwest::Response 反序列化为实现了 serde::Deserialize trait 的 User 对象。

tokio::main 用于设置异步执行器,该进程异步等待 reqwest::get 完成,然后将响应信息反序列化到用户实例中。

use serde::Deserialize;
use reqwest::Error;

#[derive(Deserialize, Debug)]
struct User {
    login: String,
    id: u32,
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    let request_url = format!("https://api.github.com/repos/{owner}/{repo}/stargazers",
                              owner = "rust-lang-nursery",
                              repo = "rust-cookbook");
    println!("{}", request_url);
    let response = reqwest::get(&request_url).await?;

    let users: Vec<User> = response.json().await?;
    println!("{:?}", users);
    Ok(())
}

23.5.2.2. 检查 API 资源是否存在

reqwest-badge cat-net-badge

使用消息标头 HEAD 请求((Client::head)查询 GitHub 用户端接口,然后检查响应代码以确定是否成功。这是一种无需接收 HTTP 响应消息主体,即可快速查询 rest 资源的方法。使用 ClientBuilder::timeout 方法配置的 reqwest::Client 结构体将确保请求不会超时。

由于 ClientBuilder::buildRequestBuilder::send 都返回错误类型 reqwest::Error,所以便捷的 reqwest::Result 类型被用于主函数的返回类型。

use reqwest::Result;
use std::time::Duration;
use reqwest::ClientBuilder;

#[tokio::main]
async fn main() -> Result<()> {
    let user = "ferris-the-crab";
    let request_url = format!("https://api.github.com/users/{}", user);
    println!("{}", request_url);

    let timeout = Duration::new(5, 0);
    let client = ClientBuilder::new().timeout(timeout).build()?;
    let response = client.head(&request_url).send().await?;

    if response.status().is_success() {
        println!("{} is a user!", user);
    } else {
        println!("{} is not a user!", user);
    }

    Ok(())
}

23.5.2.3. 使用 GitHub API 创建和删除 Gist

reqwest-badge serde-badge cat-net-badge cat-encoding-badge

使用 Client::post 创建一个 POST 请求提交到 GitHub gists API v3 接口的 gist,并使用 Client::delete 使用 DELETE 请求删除它。

reqwest::Client 负责这两个请求的详细信息,包括:URL、消息体(body)和身份验证。serde_json::json! 宏的 POST 主体可以提供任意形式的 JSON 主体,通过调用 RequestBuilder::json 设置请求主体,RequestBuilder::basic_auth 处理身份验证。本实例中调用 RequestBuilder::send 方法同步执行请求。

use error_chain::error_chain;
use serde::Deserialize;
use serde_json::json;
use std::env;
use reqwest::Client;

error_chain! {
    foreign_links {
        EnvVar(env::VarError);
        HttpRequest(reqwest::Error);
    }
}

#[derive(Deserialize, Debug)]
struct Gist {
    id: String,
    html_url: String,
}

#[tokio::main]
async fn main() ->  Result<()> {
    let gh_user = env::var("GH_USER")?;
    let gh_pass = env::var("GH_PASS")?;

    let gist_body = json!({
        "description": "the description for this gist",
        "public": true,
        "files": {
             "main.rs": {
             "content": r#"fn main() { println!("hello world!");}"#
            }
        }});

    let request_url = "https://api.github.com/gists";
    let response = Client::new()
        .post(request_url)
        .basic_auth(gh_user.clone(), Some(gh_pass.clone()))
        .json(&gist_body)
        .send().await?;

    let gist: Gist = response.json().await?;
    println!("Created {:?}", gist);

    let request_url = format!("{}/{}",request_url, gist.id);
    let response = Client::new()
        .delete(&request_url)
        .basic_auth(gh_user, Some(gh_pass))
        .send().await?;

    println!("Gist {} deleted! Status code: {}",gist.id, response.status());
    Ok(())
}

实例中使用 HTTP 基本认证 为了授权访问 GitHub API。实际应用中或许将使用一个更为复杂的 OAuth 授权流程。

23.5.2.4. 使用 RESTful API 分页

reqwest-badge serde-badge cat-net-badge cat-encoding-badge

可以将分页的 web API 方便地包裹在 Rust 迭代器中,当到达每一页的末尾时,迭代器会从远程服务器加载下一页结果。

use reqwest::Result;
use serde::Deserialize;

#[derive(Deserialize)]
struct ApiResponse {
    dependencies: Vec<Dependency>,
    meta: Meta,
}

#[derive(Deserialize)]
struct Dependency {
    crate_id: String,
}

#[derive(Deserialize)]
struct Meta {
    total: u32,
}

struct ReverseDependencies {
    crate_id: String,
    dependencies: <Vec<Dependency> as IntoIterator>::IntoIter,
    client: reqwest::blocking::Client,
    page: u32,
    per_page: u32,
    total: u32,
}

impl ReverseDependencies {
    fn of(crate_id: &str) -> Result<Self> {
        Ok(ReverseDependencies {
               crate_id: crate_id.to_owned(),
               dependencies: vec![].into_iter(),
               client: reqwest::blocking::Client::new(),
               page: 0,
               per_page: 100,
               total: 0,
           })
    }

    fn try_next(&mut self) -> Result<Option<Dependency>> {
        if let Some(dep) = self.dependencies.next() {
            return Ok(Some(dep));
        }

        if self.page > 0 && self.page * self.per_page >= self.total {
            return Ok(None);
        }

        self.page += 1;
        let url = format!("https://crates.io/api/v1/crates/{}/reverse_dependencies?page={}&per_page={}",
                          self.crate_id,
                          self.page,
                          self.per_page);

        let response = self.client.get(&url).send()?.json::<ApiResponse>()?;
        self.dependencies = response.dependencies.into_iter();
        self.total = response.meta.total;
        Ok(self.dependencies.next())
    }
}

impl Iterator for ReverseDependencies {
    type Item = Result<Dependency>;

    fn next(&mut self) -> Option<Self::Item> {
        match self.try_next() {
            Ok(Some(dep)) => Some(Ok(dep)),
            Ok(None) => None,
            Err(err) => Some(Err(err)),
        }
    }
}

fn main() -> Result<()> {
    for dep in ReverseDependencies::of("serde")? {
        println!("reverse dependency: {}", dep?.crate_id);
    }
    Ok(())
}

23.5.2.5. 处理速率受限 API

reqwest-badge hyper-badge cat-net-badge

此实例使用 GitHub API - 速率限制展示如何处理远程服务器错误。本实例使用 hyper::header! 宏来解析响应头并检查 reqwest::StatusCode::Forbidden。如果响应超过速率限制,则将等待并重试。

use error_chain::error_chain;

use std::time::{Duration, UNIX_EPOCH};
use std::thread;
use reqwest::StatusCode;

error_chain! {
   foreign_links {
       Io(std::io::Error);
       Time(std::time::SystemTimeError);
       Reqwest(reqwest::Error);
   }
}

header! { (XRateLimitLimit, "X-RateLimit-Limit") => [usize] }
header! { (XRateLimitRemaining, "X-RateLimit-Remaining") => [usize] }
header! { (XRateLimitReset, "X-RateLimit-Reset") => [u64] }

fn main() -> Result<()> {
    loop {
        let url = "https://api.github.com/users/rust-lang-nursery ";
        let client = reqwest::Client::new();
        let response = client.get(url).send()?;

        let rate_limit = response.headers().get::<XRateLimitLimit>().ok_or(
            "response doesn't include the expected X-RateLimit-Limit header",
        )?;

        let rate_remaining = response.headers().get::<XRateLimitRemaining>().ok_or(
            "response doesn't include the expected X-RateLimit-Remaining header",
        )?;

        let rate_reset_at = response.headers().get::<XRateLimitReset>().ok_or(
            "response doesn't include the expected X-RateLimit-Reset header",
        )?;

        let rate_reset_within = Duration::from_secs(**rate_reset_at) - UNIX_EPOCH.elapsed()?;

        if response.status() == StatusCode::Forbidden && **rate_remaining == 0 {
            println!("Sleeping for {} seconds.", rate_reset_within.as_secs());
            thread::sleep(rate_reset_within);
            return main();
        } else {
            println!(
                "Rate limit is currently {}/{}, the reset of this limit will be within {} seconds.",
                **rate_remaining,
                **rate_limit,
                rate_reset_within.as_secs(),
            );
            break;
        }
    }
    Ok(())
}

23.5.3. 下载

  • 下载文件到临时目录
  • 使用 HTTP range 请求头进行部分下载
  • POST 文件到 paste-rs
  • 客户端 crates 小结

23.5.3.1. 下载文件到临时目录

reqwest-badge tempdir-badge cat-net-badge cat-filesystem-badge

使用 tempfile::Builder 创建一个临时目录,并使用 reqwest::get 通过 HTTP 协议异步下载文件。

使用 Response::url 方法内部的 tempdir() 方法获取文件名字,使用 File 结构体创建目标文件,并使用 io::copy 将下载的数据复制到文件中。程序退出时,会自动删除临时目录。

use error_chain::error_chain;
use std::io::copy;
use std::fs::File;
use tempfile::Builder;

error_chain! {
     foreign_links {
         Io(std::io::Error);
         HttpRequest(reqwest::Error);
     }
}

#[tokio::main]
async fn main() -> Result<()> {
    let tmp_dir = Builder::new().prefix("example").tempdir()?;
    let target = "https://www.rust-lang.org/logos/rust-logo-512x512.png";
    let response = reqwest::get(target).await?;

    let mut dest = {
        let fname = response
            .url()
            .path_segments()
            .and_then(|segments| segments.last())
            .and_then(|name| if name.is_empty() { None } else { Some(name) })
            .unwrap_or("tmp.bin");

        println!("file to download: '{}'", fname);
        let fname = tmp_dir.path().join(fname);
        println!("will be located under: '{:?}'", fname);
        File::create(fname)?
    };
    let content =  response.text().await?;
    copy(&mut content.as_bytes(), &mut dest)?;
    Ok(())
}

23.5.3.2. 使用 HTTP range 请求头进行部分下载

reqwest-badge cat-net-badge

使用 reqwest::blocking::Client::head 获取响应的消息主体的大小(即消息主体内容长度)。

然后,使用 reqwest::blocking::Client::get 下载 10240 字节的内容,同时打印进度消息。本实例使用同步的 reqwest 模块,消息范围标头指定响应的消息块大小和位置。

RFC7233 中定义了消息范围标头。

注:RFC(Request For Comments)是一系列以编号排定的文件。文件收集了有关互联网相关信息,以及 UNIX 和互联网社区的软件文件。

use error_chain::error_chain;
use reqwest::header::{HeaderValue, CONTENT_LENGTH, RANGE};
use reqwest::StatusCode;
use std::fs::File;
use std::str::FromStr;

error_chain! {
    foreign_links {
        Io(std::io::Error);
        Reqwest(reqwest::Error);
        Header(reqwest::header::ToStrError);
    }
}

struct PartialRangeIter {
  start: u64,
  end: u64,
  buffer_size: u32,
}

impl PartialRangeIter {
  pub fn new(start: u64, end: u64, buffer_size: u32) -> Result<Self> {
    if buffer_size == 0 {
      Err("invalid buffer_size, give a value greater than zero.")?;
    }
    Ok(PartialRangeIter {
      start,
      end,
      buffer_size,
    })
  }
}

impl Iterator for PartialRangeIter {
  type Item = HeaderValue;
  fn next(&mut self) -> Option<Self::Item> {
    if self.start > self.end {
      None
    } else {
      let prev_start = self.start;
      self.start += std::cmp::min(self.buffer_size as u64, self.end - self.start + 1);
      Some(HeaderValue::from_str(&format!("bytes={}-{}", prev_start, self.start - 1)).expect("string provided by format!"))
    }
  }
}

fn main() -> Result<()> {
  let url = "https://httpbin.org/range/102400?duration=2";
  const CHUNK_SIZE: u32 = 10240;
    
  let client = reqwest::blocking::Client::new();
  let response = client.head(url).send()?;
  let length = response
    .headers()
    .get(CONTENT_LENGTH)
    .ok_or("response doesn't include the content length")?;
  let length = u64::from_str(length.to_str()?).map_err(|_| "invalid Content-Length header")?;
    
  let mut output_file = File::create("download.bin")?;
    
  println!("starting download...");
  for range in PartialRangeIter::new(0, length - 1, CHUNK_SIZE)? {
    println!("range {:?}", range);
    let mut response = client.get(url).header(RANGE, range).send()?;
    
    let status = response.status();
    if !(status == StatusCode::OK || status == StatusCode::PARTIAL_CONTENT) {
      error_chain::bail!("Unexpected server response: {}", status)
    }
    std::io::copy(&mut response, &mut output_file)?;
  }
    
  let content = response.text()?;
  std::io::copy(&mut content.as_bytes(), &mut output_file)?;

  println!("Finished with success!");
  Ok(())
}

23.5.3.3. POST 文件到 paste-rs

reqwest-badge cat-net-badge

本实例使用 reqwest::Client 建立与 https://paste.rs 的连接,遵循 reqwest::RequestBuilder 结构体模式。调用 Client::post 方法,以 URL 为参数连接目标,RequestBuilder::body 通过读取文件设置要发送的内容,RequestBuilder::send 方法在文件上传过程中将一直阻塞,直到返回响应消息。最后,read_to_string 返回响应消息并显示在控制台中。

use error_chain::error_chain;
use std::fs::File;
use std::io::Read;

 error_chain! {
     foreign_links {
         HttpRequest(reqwest::Error);
         IoError(::std::io::Error);
     }
 }
 #[tokio::main]

async fn main() -> Result<()> {
    let paste_api = "https://paste.rs";
    let mut file = File::open("message")?;

    let mut contents = String::new();
    file.read_to_string(&mut contents)?;

    let client = reqwest::Client::new();
    let res = client.post(paste_api)
        .body(contents)
        .send()
        .await?;
    let response_text = res.text().await?;
    println!("Your paste is located at: {}",response_text );
    Ok(())
}

23.5.4. 客户端 crates 小结

涉及 crates:

  • error-chain
  • reqwest
  • serde
  • serde_json
  • tokio
  • tempdir
  • tempfile
  • hyper
  • url

24. 源码编译安装 Rust

25. Rust 构建进阶

25.1. Cargo 进阶

25.1.1. Cargo 清单格式详解

25.1.2. Cargo 文件和依赖项管理

25.1.3. 项目的构建过程

25.1.4. 使用 Cargo 发布 crate

25.1.5. Cargo 构建脚本

25.1.6. Cargo 外部工具

25.1.7. Cargo 常见问题

26. Rust 模糊测试

26.1. 使用 fuzz 进行模糊测试

26.2. 使用 afl.rs 进行模糊测试

27. 书籍构建工具 mdBook