Rust Cookbook 中文版
中文译注(Chinese translation of the Rust Cookbook):
- 《Rust Cookbook 中文版》 翻译自 The Rust Cookbook(Cookin' with Rust),查看此书的 Github 翻译项目。
- 本书翻译内容源自 zzy的开源的翻译版本,rust-lang-cn 开源组织对原译者感激不尽!
- 许可协议:跟随原翻译版本的 MIT 和 Apache 2.0 双许可授权。另本站的英文版保持原有的 CC0 协议。
- 本站支持文档中英文切换,点击页面右上角语言图标可切换到相同章节的英文页面,英文版每天都会自动同步一次官方的最新版本。
- 若发现本页表达错误或帮助我们改进翻译,可点击右上角的编辑按钮打开本页对应源码文件进行编辑和修改,Rust 中文资源的开源组织发展离不开大家,感谢您的支持和帮助!
《Rust Cookbook 中文版》是 Rust 程序设计语言(Rust 官方教程简体中文版)的简要实例示例集合:展示了在 Rust 生态系统中,使用各类 crate 来完成常见编程任务的良好实践。
了解更多关于《Rust Cookbook 中文版》一书的信息,请阅读关于本书,包括:如何阅读本书的提示、如何使用实例示例,以及关于注释的约定。
参与贡献
Rust Cookbook 项目(英文和中文两者)的目的是让 Rust 程序员新手能够更容易地参与到 Rust 社区中,因此欢迎你做出贡献,详情可查看“参与贡献”文件。
算法
生成随机值
实例名称 | Crates | 类别 |
---|---|---|
生成随机数 | ||
生成范围内随机数 | ||
生成给定分布随机数 | ||
生成自定义类型随机值 | ||
从一组字母数字字符创建随机密码 | ||
从一组用户定义字符创建随机密码 |
Vector 排序
实例名称 | Crates | 类别 |
---|---|---|
整数 Vector 排序 | ||
浮点数 Vector 排序 | ||
结构体 Vector 排序 |
命令行
参数解析
实例名称 | Crates | 类别 |
---|---|---|
解析命令行参数 |
ANSI 终端
实例名称 | Crates | 类别 |
---|---|---|
ANSI 终端 |
压缩
使用 tar 包
实例名称 | Crates | 类别 |
---|---|---|
解压 tar 包 | ||
压缩目录为 tar 包 | ||
从路径移除前缀时,解压 tar 包 |
并发/并行
显式线程
实例名称 | Crates | 类别 |
---|---|---|
生成短期线程 | ||
创建并发的数据管道 | ||
在两个线程间传递数据 | ||
保持全局可变状态 | ||
对所有 iso 文件的 SHA256 值并发求和 | ||
将绘制分形的线程分派到线程池 |
数据并行
实例名称 | Crates | 类别 |
---|---|---|
并行改变数组中元素 | ||
并行测试集合中任意或所有的元素是否匹配给定断言 | ||
使用给定断言并行搜索项 | ||
对 vector 并行排序 | ||
Map-reduce 并行计算 | ||
并行生成 jpg 缩略图 |
密码学
散列(哈希)
实例名称 | Crates | 类别 |
---|---|---|
计算文件的 SHA-256 摘要 | ||
使用 HMAC 摘要对消息进行签名和验证 |
加密
实例名称 | Crates | 类别 |
---|---|---|
使用 PBKDF2 对密码进行加密(salt)和散列(hash)运算 |
数据结构
位域
实例名称 | Crates | 类别 |
---|---|---|
定义并操作位域风格的类型 |
数据库
SQLite
实例名称 | Crates | 类别 |
---|---|---|
创建 SQLite 数据库 | ||
数据插入和查询 | ||
事务处理 |
使用 Postgres
实例名称 | Crates | 类别 |
---|---|---|
Postgres 数据库中创建表 | ||
数据插入和查询 | ||
数据聚合 |
日期及时间
期间和计算
实例名称 | Crates | 类别 |
---|---|---|
测量运行时间 | ||
执行日期检查和时间计算 | ||
时间的时区转换 |
解析与显示
实例名称 | Crates | 类别 |
---|---|---|
检查日期和时间 | ||
日期和 UNIX 时间戳的互相转换 | ||
日期和时间的格式化显示 | ||
将字符串解析为 DateTime 结构体 |
开发工具
调试工具
日志信息
实例名称 | Crates | 类别 |
---|---|---|
记录调试信息到控制台 | ||
记录错误信息到控制台 | ||
记录信息时,用标准输出 stdout 替换标准错误 stderr | ||
使用自定义日志记录器记录信息 | ||
记录到 Unix 系统日志 |
日志配置
实例名称 | Crates | 类别 |
---|---|---|
启用每个模块的日志级别 | ||
用自定义环境变量设置日志记录 | ||
在日志信息中包含时间戳 | ||
将信息记录到自定义位置 |
版本控制
实例名称 | Crates | 类别 |
---|---|---|
解析并递增版本字符串 | ||
解析复杂的版本字符串 | ||
检查给定版本是否为预发布版本 | ||
查询适配给定范围的最新版本 | ||
检查外部命令的版本兼容性 |
构建时
实例名称 | Crates | 类别 |
---|---|---|
编译并静态链接到绑定的 C 语言库 | ||
编译并静态链接到绑定的 C++ 语言库 | ||
编译 C 语言库时自定义设置 |
编码
字符集
实例名称 | Crates | 类别 |
---|---|---|
百分比编码(URL 编码)字符串 | ||
将字符串编码为 application/x-www-form-urlencoded | ||
编码和解码十六进制 | ||
编码和解码 base64 |
CSV 处理
实例名称 | Crates | 类别 |
---|---|---|
读取 CSV 记录 | ||
读取有不同分隔符的 CSV 记录 | ||
筛选匹配断言的 CSV 记录 | ||
用 Serde 处理无效的 CSV 数据 | ||
将记录序列化为 CSV | ||
用 Serde 将记录序列化为 CSV | ||
转换 CSV 文件的列 |
结构化数据
实例名称 | Crates | 类别 |
---|---|---|
对非结构化 JSON 序列化和反序列化 | ||
反序列化 TOML 配置文件 | ||
以小端模式(低位模式)字节顺序读写整数 |
错误处理
处理错误变量
实例名称 | Crates | 类别 |
---|---|---|
在 main 方法中对错误适当处理 | ||
避免在错误转变过程中遗漏错误 | ||
获取复杂错误场景的回溯 |
文件系统
文件读写
实例名称 | Crates | 类别 |
---|---|---|
读取文件的字符串行 | ||
避免读取写入同一文件 | ||
使用内存映射随机访问文件 |
目录遍历
实例名称 | Crates | 类别 |
---|---|---|
过去 24 小时内修改过的文件名 | ||
查找给定路径的循环 | ||
递归查找重名文件 | ||
使用给定断言递归查找所有文件 | ||
跳过隐藏文件遍历目录 | ||
在给定深度的目录,递归计算文件大小 | ||
递归查找所有 png 文件 | ||
忽略文件名大小写,使用给定模式查找所有文件 |
硬件支持
处理器
实例名称 | Crates | 类别 |
---|---|---|
检查逻辑 cpu 内核的数量 |
内存管理
常量
实例名称 | Crates | 类别 |
---|---|---|
声明延迟计算常量 |
网络
服务器
实例名称 | Crates | 类别 |
---|---|---|
监听未使用的 TCP/IP 端口 |
操作系统
外部命令
实例名称 | Crates | 类别 |
---|---|---|
运行外部命令并处理 stdout | ||
运行传递 stdin 的外部命令,并检查错误代码 | ||
运行管道传输的外部命令 | ||
将子进程的 stdout 和 stderr 重定向到同一个文件 | ||
持续处理子进程的输出 | ||
读取环境变量 |
科学计算
数学
线性代数
实例名称 | Crates | 类别 |
---|---|---|
Vector 范数 | ||
Vector 比较 | ||
矩阵相加 | ||
矩阵相乘 | ||
标量、vector、矩阵相乘 | ||
矩阵求逆 | ||
(反)序列化矩阵 |
三角学
实例名称 | Crates | 类别 |
---|---|---|
计算三角形的边长 | ||
验证正切(tan)等于正弦(sin)除以余弦(cos) | ||
地球上两点之间的距离 |
复数
实例名称 | Crates | 类别 |
---|---|---|
创建复数 | ||
复数相加 | ||
复数的数学函数 |
统计学
实例名称 | Crates | 类别 |
---|---|---|
集中趋势度量 | ||
计算标准偏差 |
其它数学计算
实例名称 | Crates | 类别 |
---|---|---|
大数 |
文本处理
正则表达式
实例名称 | Crates | 类别 |
---|---|---|
验证并提取电子邮件登录信息 | ||
从文本提取标签元素唯一的列表 | ||
从文本提取电话号码 | ||
通过匹配多个正则表达式来筛选日志文件 | ||
文本模式替换 |
字符串解析
实例名称 | Crates | 类别 |
---|---|---|
收集 Unicode 字符 | ||
自定义结构体 并实现 FromStr trait |
Web 编程
抓取网页
实例名称 | Crates | 类别 |
---|---|---|
从 HTML 网页中提取所有链接 | ||
检查网页死链 | ||
从 MediaWiki 标记页面提取所有唯一性链接 |
URL
实例名称 | Crates | 类别 |
---|---|---|
解析 URL 字符串为 Url 类型 | ||
通过移除路径段创建基本 URL | ||
从基本 URL 创建新 URLs | ||
提取 URL 源(scheme/ host/ port) | ||
从 URL 移除片段标识符和查询对 |
媒体类型(MIME)
实例名称 | Crates | 类别 |
---|---|---|
从字符串获取 MIME 类型 | ||
从文件名获取 MIME 类型 | ||
解析 HTTP 响应的 MIME 类型 |
客户端
请求处理
实例名称 | Crates | 类别 |
---|---|---|
发出 HTTP GET 请求 | ||
为 REST 请求设置自定义消息标头和 URL 参数 |
Web API 调用
实例名称 | Crates | 类别 |
---|---|---|
查询 GitHub API | ||
检查 API 资源是否存在 | ||
使用 GitHub API 创建和删除 Gist | ||
使用 RESTful API 分页 | ||
处理速率受限 API |
下载
实例名称 | Crates | 类别 |
---|---|---|
下载文件到临时目录 | ||
使用 HTTP range 请求头进行部分下载 | ||
POST 文件到 paste-rs |
关于本书
目录
指南目标读者
本指南适用于 Rust 程序员新手,以便于他们可以快速了解 Rust crate 生态系统的功能。同时,本指南也适用于经验丰富的 Rust 程序员,他们可以在本指南中找到如何完成常见任务的简单提示。
如何阅读指南
指南的索引包含完整的实例列表,组织为数个章节:基础、编码、并发等。这些章节基本是按照顺序排列的;后面的章节更深入一些,并且有些实例是在前面章节的概念之上构建。
在索引中,每个章节都包含实例列表。实例名称是要完成任务的简单描述,比如“在一个范围内生成随机数”;每个实例都有标记指示所使用的 crates,比如 ;以及 crate 在 crates.io 上的分类,比如 。
Rust 程序员新手应该按照由第一章节直至最后章节的顺序来阅读,这种方式易于理解书中的实例。同时,这样也可以对 crate 生态系统有一个全面的了解。点击索引中的章节标题,或者在侧边栏中导航到指南的章节页面。
如果你只是在简单地为一个任务的寻找解决方案,那么指南就较难以导航。找到特定实例的最简单的方法是详细查看索引,寻找感兴趣的 crate 及其类别,然后点击实例的名称来阅读它。指南的导航和浏览还在改进,以后或许会有所改善。
如何使用实例
指南的设计是为了让你能够即时访问可工作的代码,以及对其正在做什么有一个完整阐述,并指导你了解如何更进一步的信息。
指南中的所有实例都是完整的、可独立运行的程序,因此你可以直接复制它们到自己的项目中进行试验。为此,请按照以下说明进行操作。
考虑这个实例:“在一个范围内,生成随机数”:
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 中(译者注:了解更多请阅读 Cargo 中文文档,关于 Cargo.toml 详细信息也可参阅 Cargo.toml 与 Cargo.lock 中文文档),如上面实例代码顶部的 crate 标志所示,在本实例中仅使用了 “rand” crate。为了增加 “rand” crate,我们将使用 cargo add
命令,该命令由 cargo-edit
crate 提供,我们需要先安装它:
cargo install cargo-edit
cargo add rand
接下来,可以使用实例代码替换 src/main.rs
文件的全部内容,并通过如下命令运行:
cargo run
亲,成功执行了吧?你已经是一个 Rustacean(Rust 开发者)了!——此处应该有掌声 :-)
上面实例代码顶部的 crate 标志,链接到了 crate 在站点 docs.rs 上的完整文档,通常你在决定使用那个 crate 组件后,应该阅读一下它的文档。
关于错误处理
正确处理时,Rust 中的错误处理是健壮的,但在现今的 Rust 中,它需要大量的模板文件。正因为如此,你会发现 Rust 实例中经常充满了 unwrap
调用,而不是正确的错误处理。
由于本指南旨在提供原样重用的实例,并鼓励最佳实践。因此当涉及到 Result
类型时,它们可以正确地设置错误处理。
我们使用的基本模式是有一个 fn main() -> Result
函数签名。
错误处理的结构,通常如下:
use error_chain::error_chain; use std::net::IpAddr; use std::str; error_chain! { foreign_links { Utf8(std::str::Utf8Error); AddrParse(std::net::AddrParseError); } } fn main() -> Result<()> { let bytes = b"2001:db8::1"; // Bytes 格式化为 string let s = str::from_utf8(bytes)?; // String 解析为 IP address let addr: IpAddr = s.parse()?; println!("{:?}", addr); Ok(()) }
使用 error_chain!
宏自定义 Error
和 Result
类型,以及来自两种标准库的错误类型的自动转换。自动转换使得 ?
操作符正常运作。
在默认情况下——基于可读性目的——错误处理模板是隐藏的,如下代码所示。要阅读完整代码,请单击位于代码段右上角的“展开”()按钮。
use error_chain::error_chain; use url::{Url, Position}; error_chain! { foreign_links { UrlParse(url::ParseError); } } fn main() -> Result<()> { let parsed = Url::parse("https://httpbin.org/cookies/set?k2=v2&k1=v1")?; let cleaned: &str = &parsed[..Position::AfterPath]; println!("cleaned: {}", cleaned); Ok(()) }
要了解更多关于 Rust 错误处理的背景知识,请阅读 Rust 程序设计语言(2018)中的错误处理章节,以及这篇博客文章。
关于 crate 的说明
本指南的最终目的是对 Rust crate 生态系统的广泛覆盖,但目前还处于引导和演示过程中,因此覆盖范围有限。我们希望从小范围开始,然后慢慢拓展,这将有助于指南更快地成为高质量的资源,并使其在增长过程中保持一致的质量水平。
目前,本指南聚焦在标准库、以及“核心”或“基础”方面的 crate ——这些 crate 构成了最常见的编程任务,而生态系统的其它部分则是以此为基础构建的。
本指南与 Rust Libz Blitz 密切相关,后者是一个用于识别并提高 crate 质量的项目,因此它在很大程度上推迟了 crate 的选择。任何已经走完评估过程的 crate,都在本指南的撰写范围内,而且等待评估的 crate 也是如此。
算法
生成随机值
实例名称 | Crates | 类别 |
---|---|---|
生成随机数 | ||
生成范围内随机数 | ||
生成给定分布随机数 | ||
生成自定义类型随机值 | ||
从一组字母数字字符创建随机密码 | ||
从一组用户定义字符创建随机密码 |
Vector 排序
实例名称 | Crates | 类别 |
---|---|---|
整数 Vector 排序 | ||
浮点数 Vector 排序 | ||
结构体 Vector 排序 |
生成随机值
生成随机数
在随机数生成器 rand::Rng
的帮助下,通过 rand::thread_rng
生成随机数。可以开启多个线程,每个线程都有一个初始化的生成器。整数在其类型范围内均匀分布,浮点数是从 0 均匀分布到 1,但不包括 1。
use rand::Rng; fn main() { let mut rng = rand::thread_rng(); let n1: u8 = rng.gen(); let n2: u16 = rng.gen(); println!("Random u8: {}", n1); println!("Random u16: {}", n2); println!("Random u32: {}", rng.gen::<u32>()); println!("Random i32: {}", rng.gen::<i32>()); println!("Random float: {}", rng.gen::<f64>()); }
生成范围内随机数
使用 Rng::gen_range
,在半开放的 [0, 10)
范围内(不包括 10
)生成一个随机值。
use rand::Rng; fn main() { let mut rng = rand::thread_rng(); println!("Integer: {}", rng.gen_range(0..10)); println!("Float: {}", rng.gen_range(0.0..10.0)); }
使用 Uniform
模块可以得到均匀分布的值。下述代码和上述代码具有相同的效果,但在相同范围内重复生成数字时,下述代码性能可能会更好。
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!("Roll the die: {}", throw); if throw == 6 { break; } } }
生成给定分布随机数
默认情况下,随机数在 rand
crate 中是均匀分布。rand_distr
crate 提供其它的分布类型。如要使用,首先实例化一个分布,然后在随机数生成器 rand::Rng
的帮助下,使用 Distribution::sample
从该分布中进行采样。
关于更多信息,阅读可用分布文档。如下是一个使用正态(Normal)
分布的实例。
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, 3.0)?;
let v = normal.sample(&mut rng);
println!("{} is from a N(2, 9) distribution", v);
Ok(())
}
生成自定义类型随机值
随机生成一个元组 (i32, bool, f64)
和用户定义类型为 Point
的变量。为 Standard
实现 Distribution
trait,以允许随机生成。
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!("Random tuple: {:?}", rand_tuple); println!("Random Point: {:?}", rand_point); }
从一组字母数字字符创建随机密码
随机生成一个给定长度的 ASCII 字符串,范围为 A-Z,a-z,0-9
,使用字母数字样本。
use rand::{thread_rng, Rng}; use rand::distributions::Alphanumeric; fn main() { let rand_string: String = thread_rng() .sample_iter(&Alphanumeric) .take(30) .map(char::from) .collect(); println!("{}", rand_string); }
从一组用户定义字符创建随机密码
使用用户自定义的字节字符串,使用 gen_range
函数,随机生成一个给定长度的 ASCII 字符串。
fn main() { use rand::Rng; const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\ 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); }
Vector 排序
整数 Vector 排序
这个实例通过 vec::sort
对一个整数 Vector 进行排序。另一种方法是使用 vec::sort_unstable
,后者运行速度更快一些,但不保持相等元素的顺序。
fn main() { let mut vec = vec![1, 5, 10, 2, 15]; vec.sort(); assert_eq!(vec, vec![1, 2, 5, 10, 15]); }
浮点数 Vector 排序
f32 或 f64 的 vector,可以使用 vec::sort_by
和 PartialOrd::partial_cmp
对其进行排序。
fn main() { let mut vec = vec![1.1, 1.15, 5.5, 1.123, 2.0]; vec.sort_by(|a, b| a.partial_cmp(b).unwrap()); assert_eq!(vec, vec![1.1, 1.123, 1.15, 2.0, 5.5]); }
结构体 Vector 排序
依据自然顺序(按名称和年龄),对具有 name
和 age
属性的 Person 结构体 Vector 排序。为了使 Person 可排序,你需要四个 traits:Eq
、PartialEq
、Ord
,以及 PartialOrd
。这些 traits 可以被简单地派生。你也可以使用 vec:sort_by
方法自定义比较函数,仅按照年龄排序。
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] struct Person { name: String, age: u32 } impl Person { pub fn new(name: String, age: u32) -> Self { Person { name, age } } } fn main() { let mut people = vec![ Person::new("Zoe".to_string(), 25), Person::new("Al".to_string(), 60), Person::new("John".to_string(), 1), ]; // 根据获得的自然顺序(name 和 age)对 people 进行排序 people.sort(); assert_eq!( people, vec![ Person::new("Al".to_string(), 60), Person::new("John".to_string(), 1), Person::new("Zoe".to_string(), 25), ]); // 根据 age 值对 people 进行排序 people.sort_by(|a, b| b.age.cmp(&a.age)); assert_eq!( people, vec![ Person::new("Al".to_string(), 60), Person::new("Zoe".to_string(), 25), Person::new("John".to_string(), 1), ]); }
命令行
参数解析
实例名称 | Crates | 类别 |
---|---|---|
解析命令行参数 |
ANSI 终端
实例名称 | Crates | 类别 |
---|---|---|
ANSI 终端 |
参数解析
解析命令行参数
此应用程序使用 clap
构建器样式描述其命令行界面的结构。文档还提供了另外两种可用的方法去实例化应用程序。
在构建器样式中,with_name
函数是 value_of
方法将用于检索传递值的唯一标识符。short
和 long
选项控制用户将要键入的标志;short 标志看起来像 -f
,long 标志看起来像 --file
。
use clap::{Arg, App}; fn main() { let matches = App::new("My Test Program") .version("0.1.0") .author("Hackerman Jones <[email protected]>") .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
生成。实例应用程序的用法如下所示。
My Test Program 0.1.0
Hackerman Jones <[email protected]>
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.
ANSI 终端
ANSI 终端
此程序描述了 ansi_term
crate 的使用方法,以及它如何用于控制 ANSI 终端上的颜色和格式,如蓝色粗体文本或黄色下划线文本。
ansi_term
中有两种主要的数据结构:ANSIString
和 Style
。Style
包含样式信息:颜色,是否粗体文本,或者是否闪烁,或者其它样式。还有 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")); }
压缩
使用 tar 包
实例名称 | Crates | 类别 |
---|---|---|
解压 tar 包 | ||
压缩目录为 tar 包 | ||
从路径移除前缀时,解压 tar 包 |
使用 tar 包
解压 tar 包
从当前工作目录中的压缩包 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(()) }
压缩目录为 tar 包
压缩 /var/log
目录内的内容到 archive.tar.gz
压缩包中。
创建一个用 GzEncoder
和 tar::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(()) }
从路径移除前缀时,解压 tar 包
循环遍历 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(()) }
并发/并行
显式线程
实例名称 | Crates | 类别 |
---|---|---|
生成短期线程 | ||
创建并发的数据管道 | ||
在两个线程间传递数据 | ||
保持全局可变状态 | ||
对所有 iso 文件的 SHA256 值并发求和 | ||
将绘制分形的线程分派到线程池 |
数据并行
实例名称 | Crates | 类别 |
---|---|---|
并行改变数组中元素 | ||
并行测试集合中任意或所有的元素是否匹配给定断言 | ||
使用给定断言并行搜索项 | ||
对 vector 并行排序 | ||
Map-reduce 并行计算 | ||
并行生成 jpg 缩略图 |
显式线程
生成短期线程
本实例使用 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() }
创建并发的数据管道
下面的实例使用 crossbeam 和 crossbeam-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(); }
在两个线程间传递数据
这个实例示范了在单生产者、单消费者(SPSC)环境中使用 crossbeam-channel。我们构建的生成短期线程实例中,使用 crossbeam::scope
和 Scope::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); } }
保持全局可变状态
使用 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(()) }
对所有 iso 文件的 SHA256 值并发求和
下面的实例计算了当前目录中每个扩展名为 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(()) }
将绘制分形的线程分派到线程池
此实例通过从朱莉娅集绘制分形来生成图像,该集合具有用于分布式计算的线程池。
使用 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(()) }
数据并行
并行改变数组中元素
下面的实例使用了 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); }
并行测试集合中任意或所有的元素是否匹配给定断言
这个实例示范如何使用 rayon::any
和 rayon::all
方法,这两个方法是分别与 std::any
和 std::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 )); }
使用给定断言并行搜索项
下面的实例使用 rayon::find_any
和 par_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)); }
对 vector 并行排序
本实例对字符串 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(); }
Map-reduce 并行计算
此实例使用 rayon::filter
、rayon::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); }
并行生成 jpg 缩略图
本实例为当前目录中的所有 .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)?) }
密码学
散列(哈希)
实例名称 | Crates | 类别 |
---|---|---|
计算文件的 SHA-256 摘要 | ||
使用 HMAC 摘要对消息进行签名和验证 |
加密
实例名称 | Crates | 类别 |
---|---|---|
使用 PBKDF2 对密码进行加密(salt)和散列(hash)运算 |
散列(哈希)
计算文件的 SHA-256 摘要
如下实例中,先创建文件,写入一些数据。然后使用 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(()) }
使用 HMAC 摘要对消息进行签名和验证
使用 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."; let signature = hmac::sign(&key, message.as_bytes()); hmac::verify(&key, message.as_bytes(), signature.as_ref())?; Ok(()) }
加密
使用 PBKDF2 对密码进行加密(salt)和散列(hash)运算
对于通过 PBKDF2 密钥派生函数 pbkdf2::derive
生成的加密(加盐算法)密码,使用 ring::pbkdf2
进行散列(哈希)运算,使用 pbkdf2::verify
验证散列(哈希)运算是否正确。salt 值是使用 SecureRandom::fill
生成的,salt 字节数组被其安全生成的随机数填充。
use data_encoding::HEXUPPER; use ring::error::Unspecified; use ring::rand::SecureRandom; use ring::{digest, pbkdf2, rand}; 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 = [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: {}", HEXUPPER.encode(&salt)); 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(()) }
数据结构
位域
实例名称 | Crates | 类别 |
---|---|---|
定义并操作位域风格的类型 |
位域
定义并操作位域风格的类型
如下实例在 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"); }
数据库
SQLite
实例名称 | Crates | 类别 |
---|---|---|
创建 SQLite 数据库 | ||
数据插入和查询 | ||
事务处理 |
使用 Postgres
实例名称 | Crates | 类别 |
---|---|---|
Postgres 数据库中创建表 | ||
数据插入和查询 | ||
数据聚合 |
SQLite
创建 SQLite 数据库
使用 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(()) }
数据插入和查询
Connection::open
将打开在前一章节实例中创建的数据库 cats
的连接。下面的实例使用 Connection
的 execute
方法将数据插入 cat_colors
和 cats
两张表中。首先,将数据插入到 cat_colors
表中。随后,使用 Connection
的 last_insert_rowid
方法来获取 cat_colors
表最后插入记录的 id
。当向 cats
表中插入数据时,使用此 id
。然后,使用 prepare
方法准备执行 select 查询操作,该方法提供 statement
结构体。最后,使用 statement
的 query_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(()) }
事务处理
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() }
使用 Postgres
Postgres 数据库中创建表
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(()) }
数据插入和查询
下述实例中使用 Client
的 execute
方法将数据插入到 author
表中。然后,使用 Client
的 query
方法查询 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(()) }
数据聚合
下述实例按照降序列出了美国纽约州现代艺术博物馆
数据库中首批 7999 位艺术家的国籍。
use postgres::{Client, Error, NoTls}; struct Nation { nationality: String, count: i64, } fn main() -> Result<(), Error> { let mut client = Client::connect( "postgresql://postgres:[email protected]/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(()) }
日期及时间
期间和计算
实例名称 | Crates | 类别 |
---|---|---|
测量运行时间 | ||
执行日期检查和时间计算 | ||
时间的时区转换 |
解析与显示
实例名称 | Crates | 类别 |
---|---|---|
检查日期和时间 | ||
日期和 UNIX 时间戳的互相转换 | ||
日期和时间的格式化显示 | ||
将字符串解析为 DateTime 结构体 |
期间和计算
测量运行时间
测量从 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); }
执行日期检查和时间计算
使用 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."), } }
时间的时区转换
使用 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)); }
解析与显示
检查日期和时间
通过 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() ); }
日期和 UNIX 时间戳的互相转换
使用 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); }
日期和时间的格式化显示
使用 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")); }
将字符串解析为 DateTime 结构体
熟悉的时间格式 RFC 2822、RFC 3339,以及自定义时间格式,通常用字符串表达。要将这些字符串解析为 DateTime
结构体,可以分别用 DateTime::parse_from_rfc2822
、DateTime::parse_from_rfc3339
,以及 DateTime::parse_from_str
。
可以在 chrono::format::strftime
中找到适用于 DateTime::parse_from_str
的转义序列。注意:DateTime::parse_from_str
要求这些 DateTime 结构体必须是可创建的,以便它唯一地标识日期和时间。要解析不带时区的日期和时间,请使用 NaiveDate
、NaiveTime
,以及 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(()) }
开发工具
调试工具
日志信息
实例名称 | Crates | 类别 |
---|---|---|
记录调试信息到控制台 | ||
记录错误信息到控制台 | ||
记录信息时,用标准输出 stdout 替换标准错误 stderr | ||
使用自定义日志记录器记录信息 | ||
记录到 Unix 系统日志 |
日志配置
实例名称 | Crates | 类别 |
---|---|---|
启用每个模块的日志级别 | ||
用自定义环境变量设置日志记录 | ||
在日志信息中包含时间戳 | ||
将信息记录到自定义位置 |
版本控制
实例名称 | Crates | 类别 |
---|---|---|
解析并递增版本字符串 | ||
解析复杂的版本字符串 | ||
检查给定版本是否为预发布版本 | ||
查询适配给定范围的最新版本 | ||
检查外部命令的版本兼容性 |
构建时
实例名称 | Crates | 类别 |
---|---|---|
编译并静态链接到绑定的 C 语言库 | ||
编译并静态链接到绑定的 C++ 语言库 | ||
编译 C 语言库时自定义设置 |
调试工具
日志信息
实例名称 | Crates | 类别 |
---|---|---|
记录调试信息到控制台 | ||
记录错误信息到控制台 | ||
记录信息时,用标准输出 stdout 替换标准错误 stderr | ||
使用自定义日志记录器记录信息 | ||
记录到 Unix 系统日志 |
日志配置
实例名称 | Crates | 类别 |
---|---|---|
启用每个模块的日志级别 | ||
用自定义环境变量设置日志记录 | ||
在日志信息中包含时间戳 | ||
将信息记录到自定义位置 |
日志信息
记录调试信息到控制台
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
记录错误信息到控制台
正确的错误处理会将异常视为错误。下述实例中,通过 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); } }
记录信息时,用标准输出 stdout 替换标准错误 stderr
使用 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"); }
使用自定义日志记录器记录信息
本实例实现一个打印到 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(()) }
记录到 Unix 系统日志
本实例实现将信息记录到 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."); }
日志配置
启用每个模块的日志级别
创建两个模块: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
的日志等级设置为 info
和 debug
。
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
用自定义环境变量设置日志记录
Builder
配置日志记录。
Builder::parse
以 RUST_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"); }
在日志信息中包含时间戳
使用 Builder
创建自定义记录器配置。每个日志项调用 Local::now
以获取本地时区中的当前 DateTime
,并使用 DateTime::format
和 strftime::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
将信息记录到自定义位置
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(()) }
版本控制
解析并递增版本字符串
使用 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(()) }
解析复杂的版本字符串
使用 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(()) }
检查给定版本是否为预发布版本
给定两个版本,使用 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(()) }
查询适配给定范围的最新版本
给定一个版本字符串 &str 的列表,查找最新的语义化版本 semver::Version
。semver::VersionReq
用 VersionReq::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(()) }
检查外部命令的版本兼容性
本实例使用 Command
模块运行命令 git --version
,然后使用 Version::parse
将版本号解析为语义化版本 semver::Version
。VersionReq::matches
将 semver::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(()) }
构建工具
本节介绍在编译 crate 源代码之前运行的“构建时”工具或代码。按照惯例,构建时代码存放在 build.rs 文件,通常称为“构建脚本”。常见的用例包括:Rust 代码生成、绑定的 C/C++/asm 代码的编译。要获取更多信息,请阅读(Cargo 手册 中文版) 的构建脚本文档。
编译并静态链接到绑定的 C 语言库
为了适应项目中需要混合 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]
...
build = "build.rs"
[build-dependencies]
cc = "1"
[dependencies]
error-chain = "0.11"
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(())
}
编译并静态链接到绑定的 C++ 语言库
链接绑定的 C++ 语言库非常类似于链接绑定的 C 语言库。编译并静态链接绑定的 C++ 库时,与链接绑定的 C 语言库相比有两个核心区别:一是通过构造器方法 cpp(true)
指定 C++ 编译器;二是通过在 C++ 源文件顶部添加 extern "C"
代码段,以防止 C++ 编译器的名称篡改。
Cargo.toml
[package]
...
build = "build.rs"
[build-dependencies]
cc = "1"
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));
}
}
编译 C 语言库时自定义设置
使用 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]
...
version = "1.0.2"
build = "build.rs"
[build-dependencies]
cc = "1"
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();
}
}
编码
字符集
实例名称 | Crates | 类别 |
---|---|---|
百分比编码(URL 编码)字符串 | ||
将字符串编码为 application/x-www-form-urlencoded | ||
编码和解码十六进制 | ||
编码和解码 base64 |
CSV 处理
实例名称 | Crates | 类别 |
---|---|---|
读取 CSV 记录 | ||
读取有不同分隔符的 CSV 记录 | ||
筛选匹配断言的 CSV 记录 | ||
用 Serde 处理无效的 CSV 数据 | ||
将记录序列化为 CSV | ||
用 Serde 将记录序列化为 CSV | ||
转换 CSV 文件的列 |
结构化数据
实例名称 | Crates | 类别 |
---|---|---|
对非结构化 JSON 序列化和反序列化 | ||
反序列化 TOML 配置文件 | ||
以小端模式(低位模式)字节顺序读写整数 |
字符集
百分比编码(URL 编码)字符串
使用 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
。
将字符串编码为 application/x-www-form-urlencoded
如下实例使用 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); }
编码和解码十六进制
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(()) }
编码和解码 base64
使用 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(()) }
CSV 处理
读取 CSV 记录
将标准的 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(()) }
读取有不同分隔符的 CSV 记录
使用制表(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(()) }
筛选匹配断言的 CSV 记录
仅仅 返回 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 教程。
用 Serde 处理无效的 CSV 数据
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(()) }
将记录序列化为 CSV
本实例展示了如何序列化 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(()) }
用 Serde 将记录序列化为 CSV
下面的实例展示如何使用 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(()) }
转换 CSV 文件的列
将包含颜色名称和十六进制颜色值的 CSV 文件转换为具有颜色名称和 rgb 颜色值的 CSV 文件。使用 csv crate 读写 csv 文件,使用 serde crate 对行输入字节进行反序列化,对行输出字节进行序列化。
详细请参阅 csv::Reader::deserialize
、serde::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(()) }
结构化数据
对非结构化 JSON 序列化和反序列化
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(()) }
反序列化 TOML 配置文件
将一些 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! <[email protected]>"] [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! <[email protected]>"] [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! <[email protected]>"]); assert_eq!(package_info.dependencies["serde"], "1.0"); Ok(()) }
以小端模式(低位模式)字节顺序读写整数
字节序 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) }
错误处理
处理错误变量
实例名称 | Crates | 类别 |
---|---|---|
在 main 方法中对错误适当处理 | ||
避免在错误转变过程中遗漏错误 | ||
获取复杂错误场景的回溯 |
处理错误变量
在 main 方法中对错误适当处理
处理尝试打开不存在的文件时发生的错误,是通过使用 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), }; }
避免在错误转变过程中遗漏错误
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), } } }
获取复杂错误场景的回溯
本实例展示了如何处理一个复杂的错误场景,并且打印出错误回溯。依赖于 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
运行实例,以显示与此错误相关的详细回溯。
文件系统
文件读写
实例名称 | Crates | 类别 |
---|---|---|
读取文件的字符串行 | ||
避免读取写入同一文件 | ||
使用内存映射随机访问文件 |
目录遍历
实例名称 | Crates | 类别 |
---|---|---|
过去 24 小时内修改过的文件名 | ||
查找给定路径的循环 | ||
递归查找重名文件 | ||
使用给定断言递归查找所有文件 | ||
跳过隐藏文件遍历目录 | ||
在给定深度的目录,递归计算文件大小 | ||
递归查找所有 png 文件 | ||
忽略文件名大小写,使用给定模式查找所有文件 |
文件读写
读取文件的字符串行
我们向文件写入三行信息,然后使用 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(()) }
避免读取写入同一文件
对文件使用 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
报错,因为是同一文件。
使用内存映射随机访问文件
使用 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(()) }
目录遍历
过去 24 小时内修改过的文件名
通过调用 env::current_dir
获取当前工作目录,然后通过 fs::read_dir
读取目录中的每个条目,通过 DirEntry::path
提取条目路径,以及通过通过 fs::Metadata
获取条目元数据。Metadata::modified
返回条目自上次更改以来的运行时间 SystemTime::elapsed
。Duration::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(()) }
查找给定路径的循环
使用 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") )) ); }
递归查找重名文件
在当前目录中递归查找重复的文件名,只打印一次。
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); } } }
使用给定断言递归查找所有文件
在当前目录中查找最近一天内修改的 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(()) }
跳过隐藏文件遍历目录
递归下行到子目录的过程中,使用 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())); }
在给定深度的目录,递归计算文件大小
通过 WalkDir::min_depth
和 WalkDir::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); }
递归查找所有 png 文件
递归地查找当前目录中的所有 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(()) }
忽略文件名大小写,使用给定模式查找所有文件
在 /media/
目录中查找与正则表达模式 img_[0-9]*.png
匹配的所有图像文件。
一个自定义 MatchOptions
结构体被传递给 glob_with
函数,使全局命令模式下不区分大小写,同时保持其他选项的默认值 Default
。
译者注:
glob
是glob 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(()) }
硬件支持
处理器
实例名称 | Crates | 类别 |
---|---|---|
检查逻辑 cpu 内核的数量 |
处理器
检查逻辑 cpu 内核的数量
使用 [num_cpus::get
] 显示当前机器中的逻辑 CPU 内核的数量。
fn main() { println!("Number of logical cores is {}", num_cpus::get()); }
内存管理
常量
实例名称 | Crates | 类别 |
---|---|---|
声明延迟计算常量 |
常量
声明延迟计算常量
声明延迟计算的常量 HashMap
。HashMap
将被计算一次,随后存储在全局静态(全局堆栈)引用。
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"); }
网络
服务器
实例名称 | Crates | 类别 |
---|---|---|
监听未使用的 TCP/IP 端口 |
服务器
监听未使用的 TCP/IP 端口
在本实例中,程序将监听显示在控制台上的端口,直到一个请求被发出。当将端口设置为 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(()) }
操作系统
外部命令
实例名称 | Crates | 类别 |
---|---|---|
运行外部命令并处理 stdout | ||
运行传递 stdin 的外部命令,并检查错误代码 | ||
运行管道传输的外部命令 | ||
将子进程的 stdout 和 stderr 重定向到同一个文件 | ||
持续处理子进程的输出 | ||
读取环境变量 |
外部命令
运行外部命令并处理 stdout
将 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(()) }
运行传递 stdin 的外部命令,并检查错误代码
使用外部命令 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) } }
运行管道传输的外部命令
显示当前工作目录中前 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(()) }
将子进程的 stdout 和 stderr 重定向到同一个文件
生成子进程并将 stdout
和 stderr
重定向到同一个文件。它遵循与运行管道传输的外部命令相同的思想,但是 process::Stdio
会将输出写入指定的文件。对 stdout
和 stderr
而言,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(()) }
持续处理子进程的输出
在运行外部命令并处理-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(()) }
读取环境变量
通过 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(()) }
科学计算
数学
线性代数
实例名称 | Crates | 类别 |
---|---|---|
Vector 范数 | ||
Vector 比较 | ||
矩阵相加 | ||
矩阵相乘 | ||
标量、vector、矩阵相乘 | ||
矩阵求逆 | ||
(反)序列化矩阵 |
三角学
实例名称 | Crates | 类别 |
---|---|---|
计算三角形的边长 | ||
验证正切(tan)等于正弦(sin)除以余弦(cos) | ||
地球上两点之间的距离 |
复数
实例名称 | Crates | 类别 |
---|---|---|
创建复数 | ||
复数相加 | ||
复数的数学函数 |
统计学
实例名称 | Crates | 类别 |
---|---|---|
集中趋势度量 | ||
计算标准偏差 |
其它数学计算
实例名称 | Crates | 类别 |
---|---|---|
大数 |
数学
线性代数
实例名称 | Crates | 类别 |
---|---|---|
Vector 范数 | ||
Vector 比较 | ||
矩阵相加 | ||
矩阵相乘 | ||
标量、vector、矩阵相乘 | ||
矩阵求逆 | ||
(反)序列化矩阵 |
三角学
实例名称 | Crates | 类别 |
---|---|---|
计算三角形的边长 | ||
验证正切(tan)等于正弦(sin)除以余弦(cos) | ||
地球上两点之间的距离 |
复数
实例名称 | Crates | 类别 |
---|---|---|
创建复数 | ||
复数相加 | ||
复数的数学函数 |
统计学
实例名称 | Crates | 类别 |
---|---|---|
集中趋势度量 | ||
计算标准偏差 |
其它数学计算
实例名称 | Crates | 类别 |
---|---|---|
大数 |
线性代数
矩阵相加
使用 ndarray::arr2
创建两个二维(2-D)矩阵,并按元素方式求和。
注意:sum 的计算方式为 let sum = &a + &b
,借用 &
运算符获得 a
和 b
的引用,可避免销毁他们,使它们可以稍后显示。这样,就创建了一个包含其和的新数组。
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); }
矩阵相乘
使用 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)); }
标量、vector、矩阵相乘
使用 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); }
Vector 比较
ndarray crate 支持多种创建数组的方法——此实例使用 from
从 std::Vec
创建数组 ndarray::Array
。然后,对数组以元素方式求和。
下面的实例按元素方式比较两个浮点型 vector。浮点数的存储通常不精确,因此很难进行精确的比较。但是,approx
crate 中的 assert_abs_diff_eq!
宏允许方便地比较浮点型元素。要将 approx
和 ndarray
两个 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.])); }
Vector 范数
这个实例展示了 Array1
类型、ArrayView1
类型、fold
方法,以及 dot
方法在计算给定 vector 的 l1 和 l2 范数时的用法。
+ l2_norm
函数是两者中较简单的,它计算一个 vector 与自身的点积(dot product,数量积)的平方根。
+ l1_norm
函数通过 fold
运算来计算元素的绝对值(也可以通过 x.mapv(f64::abs).scalar_sum()
执行,但是会为 mapv
的结果分配一个新的数组)。
请注意:l1_norm
和 l2_norm
都采用 ArrayView1
类型。这个实例考虑了 vector 范数,所以范数函数只需要接受一维视图(ArrayView1
)。虽然函数可以使用类型为 &Array1<f64>
的参数,但这将要求调用方引用拥有所有权的数组,这比访问视图更为严格(因为视图可以从任意数组或视图创建,而不仅仅是从拥有所有权的数组创建)。
Array
和 ArrayView
都是 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)); }
矩阵求逆
用 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!"); } } }
(反)序列化矩阵
本实例实现将矩阵序列化为 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(()) }
三角学
计算三角形的边长
计算直角三角形斜边的长度,其中斜边的角度为 2 弧度,对边长度为 80。
fn main() { let angle: f64 = 2.0; let side_length = 80.0; let hypotenuse = side_length / angle.sin(); println!("Hypotenuse: {}", hypotenuse); }
验证正切(tan)等于正弦(sin)除以余弦(cos)
验证 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); }
地球上两点之间的距离
默认情况下,Rust 提供了数学上的浮点数方法,例如:三角函数、平方根、弧度和度数之间的转换函数等。
下面的实例使用半正矢公式计算地球上两点之间的距离(以公里为单位)。两个点用一对经纬度表示,然后,to_radians
将它们转换为弧度。sin
、cos
、powi
以及 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 ); }
复数
创建复数
创建类型 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); }
复数相加
对复数执行数学运算与对内置类型执行数学运算是一样的:计算的数字必须是相同的类型(如浮点数或整数)。
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); }
复数的数学函数
在与其他数学函数交互时,复数有一系列有趣的特性,尤其是和自然常数 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 }
统计学
集中趋势度量
本节实例计算 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); }
计算标准偏差
本实例计算一组测量值的标准偏差和 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); }
其它数学计算
大数
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)); }
文本处理
正则表达式
实例名称 | Crates | 类别 |
---|---|---|
验证并提取电子邮件登录信息 | ||
从文本提取标签元素唯一的列表 | ||
从文本提取电话号码 | ||
通过匹配多个正则表达式来筛选日志文件 | ||
文本模式替换 |
字符串解析
实例名称 | Crates | 类别 |
---|---|---|
收集 Unicode 字符 | ||
自定义结构体 并实现 FromStr trait |
正则表达式
验证并提取电子邮件登录信息
验证电子邮件地址的格式是否正确,并提取 @ 符号之前的所有内容。
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 protected]"), Some(r"I❤email")); assert_eq!( extract_login(r"[email protected]"), Some(r"sdf+sdsfsd.as.sdsd") ); assert_eq!(extract_login(r"More@Than@[email protected]"), None); assert_eq!(extract_login(r"Not an email@email"), None); }
从文本提取标签元素唯一的列表
本实例展示从文本中提取、排序和去除标签列表的重复元素。
这里给出的标签正则表达式只捕获以字母开头的拉丁语标签,完整的 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); }
从文本提取电话号码
使用 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(()) }
通过匹配多个正则表达式来筛选日志文件
读取名为 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(()) }
文本模式替换
将所有出现的国际标准 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"); }
字符串解析
收集 Unicode 字符
使用 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], "é"); }
自定义结构体
并实现 FromStr
trait
本实例中,创建一个自定义结构体 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 } ); }
Web 编程
抓取网页
实例名称 | Crates | 类别 |
---|---|---|
从 HTML 网页中提取所有链接 | ||
检查网页死链 | ||
从 MediaWiki 标记页面提取所有唯一性链接 |
URL
实例名称 | Crates | 类别 |
---|---|---|
解析 URL 字符串为 Url 类型 | ||
通过移除路径段创建基本 URL | ||
从基本 URL 创建新 URLs | ||
提取 URL 源(scheme/ host/ port) | ||
从 URL 移除片段标识符和查询对 |
媒体类型(MIME)
实例名称 | Crates | 类别 |
---|---|---|
从字符串获取 MIME 类型 | ||
从文件名获取 MIME 类型 | ||
解析 HTTP 响应的 MIME 类型 |
客户端
请求处理
实例名称 | Crates | 类别 |
---|---|---|
发出 HTTP GET 请求 | ||
为 REST 请求设置自定义消息标头和 URL 参数 |
Web API 调用
实例名称 | Crates | 类别 |
---|---|---|
查询 GitHub API | ||
检查 API 资源是否存在 | ||
使用 GitHub API 创建和删除 Gist | ||
使用 RESTful API 分页 | ||
处理速率受限 API |
下载
实例名称 | Crates | 类别 |
---|---|---|
下载文件到临时目录 | ||
使用 HTTP range 请求头进行部分下载 | ||
POST 文件到 paste-rs |
提取链接
从 HTML 网页中提取所有链接
使用 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/en-US/") .await? .text() .await?; Document::from(res.as_str()) .find(Name("a")) .filter_map(|n| n.attr("href")) .for_each(|x| println!("{}", x)); Ok(()) }
检查网页死链
调用 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(()) }
从 MediaWiki 标记页面提取所有唯一性链接
使用 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(()) }
URL
解析 URL 字符串为 Url
类型
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(()) }
通过移除路径段创建基本 URL
基本 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) }
从基本 URL 创建新 URLs
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) }
提取 URL 源(scheme / host / port)
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(()) }
从 URL 移除片段标识符和查询对
解析 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(()) }
媒介类型
从字符串获取 MIME 类型
下面的实例展示如何使用 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 ); }
从文件名获取 MIME 类型
下面的实例展示如何使用 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); } }
解析 HTTP 响应的 MIME 类型
当从 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(()) }
客户端
请求处理
实例名称 | Crates | 类别 |
---|---|---|
发出 HTTP GET 请求 | ||
为 REST 请求设置自定义消息标头和 URL 参数 |
Web API 调用
实例名称 | Crates | 类别 |
---|---|---|
查询 GitHub API | ||
检查 API 资源是否存在 | ||
使用 GitHub API 创建和删除 Gist | ||
使用 RESTful API 分页 | ||
处理速率受限 API |
下载
实例名称 | Crates | 类别 |
---|---|---|
下载文件到临时目录 | ||
使用 HTTP range 请求头进行部分下载 | ||
POST 文件到 paste-rs |
请求处理
发出 HTTP GET 请求
解析提供的 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(()) }
为 REST 请求设置自定义消息标头和 URL 参数
本实例中为 HTTP GET 请求设置标准的和自定义的 HTTP 消息标头以及 URL 参数。使用 hyper::header!
宏创建 XPoweredBy
类型的自定义消息标头。
使用 Url::parse_with_params
构建复杂的 URL。使用 RequestBuilder::header
方法设置标准消息标头 header::UserAgent
、header::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(()) }
Web API 调用
查询 GitHub API
使用 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(()) }
检查 API 资源是否存在
使用消息标头 HEAD 请求((Client::head
)查询 GitHub 用户端接口,然后检查响应代码以确定是否成功。这是一种无需接收 HTTP 响应消息主体,即可快速查询 rest 资源的方法。使用 ClientBuilder::timeout
方法配置的 reqwest::Client
结构体将确保请求不会超时。
由于 ClientBuilder::build
和 RequestBuilder::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(()) }
使用 GitHub API 创建和删除 Gist
使用 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 授权流程。
使用 RESTful API 分页
可以将分页的 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(()) }
处理速率受限 API
此实例使用 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(()) }
下载
下载文件到临时目录
使用 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(()) }
使用 HTTP range 请求头进行部分下载
使用 reqwest::blocking::Client::head
获取响应的消息主体的大小(即消息主体内容长度)。
然后,使用 reqwest::blocking::Client::get
下载 10240 字节的内容,同时打印进度消息。本实例使用同步的 reqwest 模块,消息范围标头指定响应的消息块大小和位置。
译者注: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(()) }
POST 文件到 paste-rs
本实例使用 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(()) }