Rust笔记(上)
关于为什么最终还是选择了Rust作为主力语言
原因之前也提到过,我在工作中的项目使用Python,出现了明显的性能问题。在计算密集型任务中,GIL锁造成了阻碍,而使用多进程又有明显的开销,所以不得不考虑更换主力语言。但我仍然认为Python代码是最漂亮的。
之前考虑过C++,但几个月后就被劝退了。并非单纯的语言本身使我知难而退,而是许多C++开发者都认为在数年乃至十数年的开发经历后,他们并不能完全掌握这种工具。“C++是用来学习的,而不是用来精通的”,真正使我放弃的是这句广为流传的话。我很确定编程语言只是我的实用工具,不希望在编程语言本身花费比使用它来完成需求更多的时间。我愿意让渡一些灵活和完全控制的自由来换取程序的稳定。在我的理解中,这种完全没有限制的自由就像无边大海,弄潮儿们自然可以随心所欲,但我这种游泳技术较差之人只会淹没其中。这就是我选择Rust的原因。
学习书籍:《Rust程序设计(第二版)》。
大致浏览一遍后,我发现,Rust是个缝合怪,我在Python、Golang、C++中见到过的很多优秀特性和近年来的某些趋势都可以在Rust中见到。坏消息是这是个缝合怪,好消息是这个缝合怪的针是自己磨的,而且看起来效果很好。
基本数据类型
编写Rust代码相较于编写动态语言代码稍显繁杂,但类型推断和泛型函数对于简化代码还是有所助力。
- 数值类型(默认i32)
- 给定位宽整数,i表示有符号,u表示无符号,数字表示位宽,例如i8、u64等
- 与机器字保持一致宽度整数,isize、usize
- 浮点数,单精度f32、双精度f64
- 布尔类型,true、false
- char,注意是4字节
- 元组
- 单元,即空元组
- 结构体
- 枚举
- Box<Attend>
- &、&mut,共享引用或可变引用
- String,UTF-8字符串,动态分配大小
- 数组,固定长度,元素类型相同
- 向量,Vec<ele>,元素类型相同但长度可变
- Option,可选值,None或者Some<v>
- Result,成功Ok<v>或者Err<e>
- &dyn any、&mut dyn Any,特型对象,是对任何实现了一组给定方法的值的引用
- fn(arg) -> res,函数指针
- 闭包
无符号整型会使用完整范围来表示正值和0,有符号整型会使用二进制补码表示。
Rust会使用u8类型作为字节值,并且为u8值提供了字节字面量。
#[test]
fn test_any() {
assert_eq!(b'A', 65u8);
}
可以使用as运算符将一种整型转换为另一种整型。
整型运算溢出时,debug中会panic,release中会回绕(数学结果对值类型范围取模的值)。
as运算符还可以完成bool到数值类型的转换但不能完成逆运算。
#[test]
fn test_any() {
assert_eq!(false as i32, 0); // 即便只需要一个位,但是还是会占用一个字节
assert_eq!(true as i32, 1);
}
数组的声明
#[test]
fn test_any() {
let a: [i32; 6] = [0, 0, 0, 0, 0, 0];
let b = [0; 6];
assert_eq!(a, b);
}
向量的声明
#[test]
fn test_any() {
let a = vec![0, 0, 0, 0, 0, 0];
let b = vec![0i32; 6];
let c: Vec<i32> = (0..7).collect();
let d: Vec<i32> = [0, 1, 2, 3, 4, 5, 6].to_vec();
assert_eq!(a, b);
assert_eq!(c, d);
}
Vec<T>由3个值组成,即指向堆中缓冲区的指针、容量和长度。
对切片[T]的引用是一个胖指针,包括指向切片第一个元素的指针和切片中元素的数量。
带有b前缀的字符串字面量都是字节串,这样的字节串是u8值(字节)的切片而不是Unicode文本。字节串不能包含任意Unicode字符。
#[test]
fn test_any() {
assert_eq!(b"GET", &[b'G', b'E', b'T', ]);
}
&str也是一个胖指针,很像切片的引用&[T],而String则类似于Vec<T>。&Vec<T>可以自动转换到&[T],而&String可以自动转换到&str。当作为参数类型时,为了方便调用,将切片的引用&[T]或者字符串切片的引用&str作为类型可能更好。
类型别名:type Bytes = Vec<u8>;
所有权与移动
所有权
Rust内置了所有权概念,每个值都有决定其生命周期的唯一拥有者,当拥有者被释放时,它所拥有的值也会同时被丢弃(drop)。
像变量拥有自己的值一样,结构体拥有自己的字段,元素、数组和向量则拥有自己的元素。拥有者及其拥有的值形成了一棵树:值的拥有者是值的父节点,值拥有的值是值的子节点。每棵树的总根都是一个变量,当该变量超出作用域时整棵树都将随之消失。
Rust程序通常不需要像C程序或者C++程序那样显式地使用free和delete来丢弃值。在Rust中丢弃一个值的方式就是从所有权树中移除它:或者离开变量的作用域,或者从向量中删除一个元素,或者执行其他类似的操作。这样一来,Rust就会确保正确地丢弃该值及其所拥有的一切。
看起来这种所有权概念非常严格,但Rust提供了一些所有权模型的灵活性:
- 值的所有权可以转移
- copy类型(即非常简单的、拷贝起来开销极小的类型)不受所有权规则的约束
- Rc和Arc这类引用计数指针类型可以帮助值有限制地有多个拥有者
- 在有限的生命周期约束下可以对值进行borrow借用以获得值的引用
移动
在Rust中,对大多数类型来说下,像为变量赋值、将其传给函数或从函数返回这样的操作,都不会复制值,而是会移动值。源会把值的所有权转移给目标并变回未初始化状态,改由目标变量来控制值的生命周期。Rust程序会以每次只移动一个值的方式来建立和拆除复杂的结构。
这种移动语义与Python直接拷贝指针并增加引用计数、C++完整生成副本相比,开销和Python一样低但所有权和C++一样明确。这种两全的代价是如果需要同时访问源和目标,这时候需要显式复制,此时就和C++一样,执行深拷贝。
#[test]
fn test_any() {
{
let mut _s = "Hello".to_string();
_s = "World".to_string(); // 在此处"Hello"值已被丢弃
}
{
let mut _s = "Hello".to_string();
let _t = _s; // _t接手"Hello"值的所有权,_s回归未初始化状态
_s = "World".to_string();
}
{
struct Person {_name: String, _birth: i32}
let mut s = Vec::new(); // new构造一个新向量并将其本身返回给s,此新向量的所有权从new转移给了s
// to_string返回值本身并将所有权交给Person的构造器,Person实例值被传给push函数,所有权被向量s接管
s.push(Person{ _name: "hello".to_string(), _birth: 0 });
}
}
这样移动值乍一看可能效率地下,但是:
- 移动的永远是值本身,对于在堆中有缓冲区的这些实例,值本身就是三个机器字(指向缓冲区的指针、容量、长度),指向的堆中的缓冲区的位置不用发生变化。
- 编译器会做优化,机器码通常会将值直接存储在它应该在的位置。
注意
- 禁止在循环中进行变量移动而不赋予其新值。
- 移动向量中的内容时需要注意,不能使用索引直接将元素移动出向量,因为向量并不会记住哪些元素是未初始化的。
- 在循环消耗向量时,将向量直接传给循环,原本拥有该向量的变量变回未初始化状态,而循环机制会接管向量所有权,并且向量本身对代码不再可见。
- 移动例外:对Copy类型的值进行复制会复制这个值而不会移动它。
- 只有可以通过简单地复制位来复制其值的类型才能作为Copy类型,而根据经验,任何在丢弃值时需要做一些特殊操作的类型都不能是Copy类型。
- 自定义类型中所有字段本身都是Copy类型,可以通过#[derive(Copy, Clone)]来创建Copy类型。
- 所有字段本身都是Copy类型的自定义类型不会自动实现Copy,因为从Copy到非Copy的变更会形成较大的维护成本。
- Rust中不能重载赋值运算符或定义专门的复制构造函数和移动构造函数。每次移动或复制都是字节级一对一浅拷贝,只不过复制会保留源的初始化状态。
#[test]
fn test_any() {
let mut v = Vec::new();
for i in 101..106 {
v.push(i.to_string());
}
// let third = v[2]; error: cannot move out of index of `Vec<String>`
// 正确的取值方法
let fifth = v.pop().expect("空向量"); // 直接取末尾元素
assert_eq!(fifth, "105");
let second = v.swap_remove(1); // swap 末尾元素与指定索引位置元素并pop
assert_eq!(second, "102");
let third = std::mem::replace(&mut v[2], "106".to_string()); // 使用指定值替换指定索引处元素并返回原元素
assert_eq!(third, "103");
assert_eq!(v, vec!["101", "104", "106"])
}
Rc与Arc:共享所有权
Rc与Arc是引用计数指针类型,区别是Arc基于其原子操作特性可以在线程之间安全共享。
对于任意类型T,Rc<T>值是指向附带引用计数的在堆上分配的T型指针,克隆Rc<T>并不会复制T,而只会创建另一个指向它的指针并增加引用计数。当丢弃最后一个现有Rc时,T也会被丢弃。Rc指针拥有的值是不可变的。
Rust的内存和线程安全保证的基石是:确保不会有任何值是既可变又共享的。
在现在看,这种方式的确可以避免类似循环引用导致的内存泄漏,但结合之后介绍的内部可变性,仍然可以是实现这种错误。
引用
迄今为止,我们看到的所有指针类型(无论是简单的Box<T>堆指针,还是String值和Vec内部的指针)都是拥有型指针,这意味着当拥有者被丢弃时,它的引用目标也会随之消失。Rust还有一种名为引用的非拥有型指针,这种指针对引用目标的生命周期毫无影响。
事实上,影响是反过来的,引用的生命周期绝不能超出其引用目标。你的代码必须遵循这样的规则,即任何引用的生命周期都不可能超出它指向的值。为了强调这一点,Rust把创建对某个值的引用的操作称为借用(borrow)某个值:凡事借用,终须归还。
共享引用
Copy类型。可同时持有多个。假设存在某值x,则&x即可产生x的共享引用。当存在此引用时,值会被锁定,拒绝各种形式的修改请求,包括来自值的拥有者的修改请求。
可变引用
非Copy类型。只能同时持有一个,并且排斥任何引用。假设存在某值x,则&mut x即可产生x的可变引用。当存在此引用时,除当前引用外,拒绝各种形式的访问或修改请求,当前引用成为唯一访问/修改通道。
.运算符会按需对其左操作数隐式解引用,也可以根据需要隐式借用对其左操作数的引用。
.运算符和比较运算符均能看穿任意数量的引用,但比较运算符左值和右值应该保持相同的类型(相同的引用层数和值类型)。
注意:
- 与C++不同,Rust中把引用赋值给某个引用变量会让该变量指向新的地方而不是将新值存储在其引用目标中。
- 表示对某个可能不存在的事物的引用,可以使用Option<&T>,None即表示空指针,Some(&T)表示非零地址
- 在let语句中,如果立即将引用赋值给某个变量(或者使其成为立即被赋值的某个结构体或数组的一部分),那么RUst就会让匿名变量的生命周期与let创建的变量保持一致,否则,匿名变量会存续到所属封闭语句块的末尾。
- 两种胖指针,即携带某个值地址的双字值:对切片的引用,携带了起始地址和长度;特型对象,即对实现了指定特型的值的引用。
生命周期
生命周期是编译期内用来检查引用安全的规则,有时需要手动提供这些信息来帮助编译器帮助我们检查代码。生命周期'a读作"tick A"。
static mut STASH: &i32 = &128; // 静态变量必须初始化
fn f(p: &'static i32) { // 给可变静态变量赋值,源的生命周期必须足够长
unsafe { // 可变静态变量并不是线程安全的,所以只能在unsafe块中访问可变静态变量
STASH = p;
}
}
#[test]
fn test_any() {
static WORTH_POINTING_AT: i32 = 1000;
f(&WORTH_POINTING_AT); // f(p: 'static i32),在Rust中,函数的签名总是会揭示出函数体的行为。
}
省略生命周期
- 如果函数的参数只有一个生命周期,那么Rust就会假设返回值具有相同的生命周期。
- 如果函数是某个类型的方法并且具有引用类型的self参数,那么Rust就会假定返回值的生命周期与self参数的生命周期相同。
- 除以上外,如果函数的参数有多个生命周期,那么就需要明确指定生命周期。
表达式
Rust是所谓表达式语言。C中的大多数控制流工具是语句。而在Rust中,它们都是表达式。所有可以链式书写的运算符都是左结合的。
块与分号
let x = {
let a = b.c(); // let语句需要;结尾
a.d(e); // ;结尾丢弃返回值
a.f() // 没有分号结尾,该表达式值存入x
};
声明
fn test() {
// let name: type = expr; // 块可以包含let声明,类型和初始化代码是可选的。
// 遮蔽
let x = vec![0, 1, 2, 3, 4, 5];
for i in x {
let i = i;
println!("{i}");
}
// 块还可以包含语法项声明
fn a() {
let mut v = vec![0, 1, 2, 3, 4, 5];
v.reverse();
fn b() {
// println!("{:?}", v) // 无法捕获动态环境,勿与python中的闭包混淆
println!("hello")
}
b();
println!("{:?}", v)
}
a();
}
#[test]
fn test_any() {
test();
}
if与match
if语句每个condition都必须是bool类型的表达式,不会发生隐式转换,condition周围不需要圆括号。
if condition1 {
block1
} else if condition2 {
block2
} else {
block_n
}
match是另一种强大的分支语句。
match value {
pattern => expr,
...
}
fn main() {
let x: Option<i32> = Some(6);
match x { // match的多功能性源于其模式匹配的能力,Rust禁止执行未覆盖所有可能值的match表达式。
Some(i) => println!("{i}"),
None => {}
}
}
if表达式中的所有块或者match表达式的所有分支必须具有相同的类型。
if let
if let pattern = expr {
block1
} else {
block2
}
// 是以下形式的简化语法
match expr {
pattern => {block1},
_ => {block2}
}
循环
while condition {
block
}
while let pattern=expr {
block
}
loop {
block
}
for pattern in iterable {
block
}
各种循环都是Rust中的表达式,但是while循环或for循环的值总是()。
为了与Rust的移动语义保持一致,把值用于for循环会消耗该值。想要仅访问而不消耗值,可以在循环中访问其引用,便利可迭代对象的引用会为每个元素提供一个可变引用。
break
在loop循环中,break后可以跟随值作为loop表达式的值,多个break后跟随的值必须是同一类型的。break和continue可以和声明周期标签一起使用来在多层循环中切换。
#[test]
fn test_any() {
let a = [0, 1, 2, 3, 4, 5, 6];
let b = [100, 101, 102];
let mut n = 0;
let x = 'outer: loop {
let j = &a[n];
n += 1;
for i in &b {
if *i + *j == 108 {
break 'outer (*j, *i)
}
}
};
assert_eq!(x, (6, 102))
}
还以一个和break表达式一样可以放弃正在进行中的工作的表达式:return。
无限循环或者程序panic之类的表达式属于一个特殊类型,即!。形似fn exit(code: i32) -> !
let a = (0..n).collect<Vec<i32>>(); // 在表达式中<是小于运算符
let a = (0..n).collect::<Vec<i32>>(); // 正确
let a: Vec<i32> = (0..n).collect(); // 正确
错误处理
panic
panic针对的是那种永远不应该发生的错误。
panic是安全的,没有违反Rust的任何安全规则,即使你故意在标准库方法的中间引发panic,它也永远不会在内存中留下悬空指针或半初始化的值。
panic是基于线程的。一个线程panic时,其他线程可以继续运行。
Result
Rust中没有异常,常见的错误处理方式之一是使用Result。Result要么是一个携带成功结果的Ok(v),要么是一个携带错误信息的Err(e)值。
方法 | 返回值 | 释义 |
---|---|---|
result.is_ok() / result.is_err() | bool | 成功还是出错 |
result.ok() | Option<T> | 成功返回Some(v),错误返回None |
result.err() | Option<E> | 有错误时返回 |
result.unwrap_or(fallback) | T | 成功时解包返回T,失败时返回指定T |
result.unwrap_or_else(fallback_fn) | T | 成功时解包返回T,失败时调用 |
result.unwrap() | T/panic | 直接解包,包含错误时panic |
result.expect(message) | T/panic | 直接解包,包含错误时panic并打印信息 |
result.as_ref() | Result<&T, &E>(值的类型) | 转引用 |
result.as_mut() | Result<&mut T, &mut E>(值的类型) | 转可变引用 |
.as_ref()和.as_mut()可以使用在不想通过以上方法(除result.is_ok() / result.is_err() )消耗值时,这样,可以result.as_ref().ok()这样不会消耗result并会返回Option<&T>。
Result类型别名
// 这只是示例,std::io模块中包含的代码
pub type Result<T> = result::Result<T, Error>;
// 这样定义Result<T>就可以将Error类型固定隐藏在Result类型中,在可见Result<T>处可以:
fn f() -> Result<()>
自定义错误类型
当自定义一种错误时,如果要使其达到预期,应该实现fmt::Formatter(方便打印)和std::error::Error这两个特型
处理多种错误类型
type GenericError = Box<dyn std::error::Error + Send + Sync + 'static>;
type GenericResult<T> = Result<T, GenericError>;
最常用的error crate:thiserror。
结构体
三种结构体类型:具名字段型结构体、元组型结构体、单元型结构体。具名字段型结构体,很明显组件有字段名做标识;元组型结构体实际上就是不具名字段型结构体,或者说它的组件被默认为以顺序命名;单元型结构体没有组件。
命名风格约定:所有类型的名称都使用CamelCase(大驼峰),字段和方法为snake_case。
结构体默认是私有的,仅在声明结构体的模块或其子模块中可见,结构体的每个字段默认也是私有的。如果要约束使用者创建实例的过程,则可以不给字段添加pub权限,这样使用者将不得不使用类型关联函数来创建实例。
mod time_struct {
#[derive(Debug, Eq, PartialEq)]
pub struct Timer {
pub name: String,
pub hour: u8,
pub minute: u8,
pub second: u8,
}
impl Timer {
pub fn test_self(self) -> (String, u8, u8, u8) { // 注意,在self本身不是Copy类型时,会消耗自身
(self.name, self.hour, self.minute, self.second)
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct Screen(pub usize, usize);
impl Screen { // 在impl块中定义的函数称为关联函数(关联函数默认也是私有的),因为它们是与特定类型相关联的。与关联函数相对的是自由函数。
const DEFAULT_SCREEN_SIZE: (i32, i32) = (1920, 1080); // 关联常量
pub fn new(length: usize, width: usize) -> Self { // 类型关联函数
Screen(length, width)
}
pub fn modify(&mut self, length: usize, width: usize) { // 根据函数体的行为来在第一个参数传入self,&self, &mut self
self.0 = length;
self.1 = width;
}
pub fn test_self(self) -> (usize, usize) { // 注意,在self本身是Copy类型时,并不会消耗自身
(self.0, self.1)
}
}
// 多数时候,从Box<T>、Rc<T>、Arc<T>调用方法和字段访问,Rust会自动借入对应的引用,但是如果确实需要self参数也可以是:
// Box<Self>、Rc<Self>、Arc<Self>类型,类似于以下
// impl Node {
// fn append_to(self: Rc<Self>, parent: &mut Node) {
// parent.children.push(self);
// }
// }
}
#[test]
fn test_any() {
use time_struct::{Timer, Screen};
let t = Timer{ // 初始化时需要写明字段名
name: "timer".to_string(),
hour: 12,
minute: 1,
second: 1,
};
let hour = 12u8;
let minute = 1u8;
let second = 1u8;
let name = "timer".to_string();
let t1 = Timer{ // 同名局域变量可以简化初始化
name,
hour,
minute,
second,
};
let t2 = Timer {
name: "timer".to_string(),
..t // 可以从同类型其他值中取值完成初始化,注意,如果字段类型不是Copy,源值会被消耗。
};
assert_eq!(t, t1);
assert_eq!(t1, t2);
let s = Screen::new(1920, 1080);
assert_eq!(1920, s.0);
// assert_eq!(1080, s.1); // s.1是private
let s = Screen::new(1920, 1080);
let (_a, _b) = s.test_self(); // Screen本身是Copy类型,传入test_self并没有消耗自己,而是消耗了自己的拷贝
assert_eq!(s, Screen::new(1920, 1080));
let t = Timer {
name: "timer".to_string(),
hour: 12,
minute: 1,
second: 1,
};
let (_1, _2, _3, _4) = t.test_self();
// println!("{:?}", t) // borrow of moved value: `t`
}
泛型结构体
Rust中的结构体可以是泛型的,可以携带类型参数、生命周期函数、常量参数。
pub struct Queue<T> {
older: Vec<T>,
younger: Vec<T>
}
impl Queue {
pub fn new() -> Self {
Queue { older: Vec::new(), younger: Vec::new() } // 此处没有提供类型参数,但Rust会自动推断
}
}
struct Extrma<'elt> { // 声明周期参数
greatest: &'elt i32,
least: &'elt i32
}
struct Polynomial<const N: usize> {
coefficients: [f64; N] // 此处数组声明必须提供常量
}
结构体自动派生公共特型
如果结构体的每一个字段都实现了某个特型,可以使用派生自动实现这种特型。
#[derive(Copy, Clone, Debug, PartialEq)]
struct Point {
x: f64,
y: f64
}
内部可变性
我们需要一个不可变值中的一丁点儿可变数据。这称为内部可变性。Rust提供了多种可选方案,本节将讨论两种最直观的类型,即Cell<T>和RefCell<T>,它们都在std::cell模块中。
Cell<T>
Cell::new(v) // 将创建一个新的Cell,并将给定的值移动进去。
cell.get() // 将返回cell中值的副本。
cell.set(v) // 将储存指定值并丢弃先前值。注意方法签名:fn set(&self, value: T),不是&mut self。但Cell不允许在共享值上调用mut方法,所以对于非Copy类型,没有办法修改。
RefCell<T>
RefCell::new(v) // 将创建一个新的RefCell,并将给定的值移动进去。
ref_cell.borrow() // 返回一个Ref<T>, 即存储在RefCell的值的共享引用,注意,如果该值已被可变借出会panic
ref_cell.borrow_mut() // 返回一个RefMut<T>, 即存储在RefCell的值的可变引用,注意,此前的任何借出会panic
ref_cell.try_borrow()和ref_cell.try_borrow_mut() // 非panic,会返回Result
通常情况下,当你借用一个变量的引用时,Rust会在编译期进行检查,而RefCell会在运行期检查。
pub struct SpiderRobot {
...
log_file: RefCell<File>,
...
}
impl SpiderRobot {
pub fn log(&self, message: &str) {
let mut file = self.log_file.borrow_mut();
writeln!(file, "{}", message).unwrap();
}
}
注意Cell不是线程安全的。
枚举与模式
枚举
- Rust的枚举允许包含数据,甚至不同类型的数据。
- 与结构体一样,枚举也可以派生实现一些特型、拥有方法。
- Rust有3中枚举变体,对应三种结构体,没有数据的变体对应单元型结构体,元组型变体对应元组型结构体,结构体型变体对应具名字段结构体。单个枚举中可以同时存在3种类型的变体。
- 枚举可以是泛型的。
- 注意:当枚举的泛型参数T是引用、Box或其他智能指针时,Rust可以省掉标签字段,将变体表示为单个机器字。
pub enum Ordering {
Less = -1, // 如果不指定,Rust会从0开始分配数值。
Equal = 0, // 枚举的可能值被称为变体或者构造器
Greater = 1,
}
use std::collections::HashMap;
enum Json {
Null,
Boolean(bool),
Number(f64),
String(String),
Array(Vec<Json>),
Object(Box<HashMap<String, Json>>),
}
enum Option<T> {
Some(T),
None
}
enum Result<T, E> {
Ok(T),
Err(E)
}
// BinaryTree
enum BinaryTree<T> {
Empty,
NonEmpty(Box<TreeNode<T>>),
}
struct TreeNode<T> {
element: T,
left: BinaryTree<T>,
right: BinaryTree<T>,
}
模式
枚举的变体中尽管包含了字段数据,但是无法直接访问,要取出这些数据,可以使用模式,模式更像是一种针对数据的正则。表达式会生成值,而模式会消耗值。
模式速查表
模式类型 | 例子 | 释义 |
---|---|---|
字面量 | 100 / "x" | 匹配确定值 |
范围 | 0..=100 / 256.. | 匹配范围内的值 |
通配符 | _ | 匹配任何值并忽略它 |
变量 | num / s | 匹配任何值并将其移动或复制进num / s |
引用变量 | ref field / ref mut field | 借用对匹配值的引用,而不移动或者复制 |
子模式 | val @ 0..=99 / ref field @ Field | 使用@左边的变量名,匹配右边的模式 |
枚举模式 | Some(v) / Err(e) | / |
元组模式 | (x, y) | / |
数组模式 | [a, b, c] | / |
切片模式 | [x, _, z] / [a, .., z] | / |
结构体模式 | Color{r, g, b} / Color | / |
引用模式 | &(a, b) / &v | 仅匹配引用值 |
多个 | 6 | 8 | / |
守卫表达式 | x if x < 0 | 只能在match表达式中 |
提示:
- 当你不想在匹配时消耗源值时,可以使用ref模式
- 与ref模式相对的是&模式,ref模式从值中取引用,而&模式从引用中取值。
- 匹配模式如果使用标识符,需要注意,仅当标识符是常量值时才是精准匹配,否则会被视为创建变量,即使可见范围内已有同名变量,也将被视为遮蔽,而不是匹配已有变量的值。此时可以使用匹配守卫。
特型与泛型
编程领域的伟大发现之一,就是可以编写能对许多不同类型(甚至是尚未发明的类型)的值进行操作的代码。
特型是Rust体系中的接口或抽象基类。泛型和特型紧密相关:泛型函数会在限界(对泛型T可能的类型范围作出限制的要求)中使用特型来阐明它能针对哪些类型的参数进行调用。
大多数情况下,一种类型支持某个特型的意思是其可以做一些事,即拥有一种能力(我觉得这和python或者golang中鸭子类型更关注行为的思想相通)。注意:特型本身必须在作用域内,否则其所有方法不可见,标准库预导入的一些特型始终在作用域内,所以无需显式导入即可工作。
特型方法相较于虚方法,没有运行期多态,只有通过&mut dyn write调用时才会产生动态派发的开销。
特型与泛型概念
特型对象
对特型类型的引用叫做特型对象。与任何其他引用一样,特型对象指向某个值,它具有生命周期,并且可以是可变的或共享的。
let mut buf: Vec<u8> = vec![];
let writer: &mut dyn Write = &mut buf;
特型对象也是一个胖指针(双指针,占两个机器字),由指向值的指针和指向表示该值类型的虚表的指针组成。
泛型函数中的特型限界
use std::hash::Hash;
use std::fmt::Debug;
fn top_ten<T: Debug + Hash + Eq>(values: &Vec<T>) {...}
// 当限界变得很长时,可以使用where关键字整理
fn run_query<M, R>(data: &DataSet, map: M, reduce: R) -> Results
where M: Mapper + Serialize,
R: Reducer + Serialize
{...}
// 泛型函数可以同时具有生命周期参数和类型参数,生命周期参数在前
fn nearest<'t, 'c, P>(target: &'t P, candidates: &'c [P]) -> &'c P
where P: MeasureDistance
{...}
// 泛型函数也接受常量参数
除函数外,非泛型类型上的方法、类型别名也可以是泛型的。
特型对象和泛型代码
一种使用特型对象的场景
trait Vegetable {...}
struct Salad {
// veggies: Vec<dyn Vegetable> // dyn Vegetable大小不确定
veggies: Vec<Box<dyn Vegetable>> // 特型对象,即对特性类型的引用
}
特型对象相对于泛型代码还可以减小编译后代码的总大小,因为单态化要求针对用到了他的每种类型编译一次,所以可能会使二进制文件变大。
但在Rust中,泛型是更常见的选择,因为:
- 速度。泛型代码不涉及动态派发,编译器也有足够的信息对函数做出优化。而特型对象调用虚方法和检查错误的开销无法避免;
- 不是每个特型都能支持特型对象;
- 更容易指定多个特型的泛型参数限界。
定义与实现特型
trait TraitName {
// 特型方法定义
// 可以提供特型方法的默认实现
...
}
// 实现特型
impl TraitName for Type {
// 特型方法的具体实现
}
孤儿规则:实现特型的前提是,特型或者类型二者必须至少有一个在当前crate中新建。这可以帮助Rust确保特型的实现是唯一的。
使用泛型impl可以为某个特型实现扩展特型。
特型可以使用关键字Self作为类型,但是使用了Self类型的特型与特型对象不兼容。
子特型
trait A: B {...} // A是B的子特型,B是A的超特型,等效于:
trait A where Self: B {...}
// 在为一个类型实现A特型 时,也必须为其实现B特型
// 子特型不会继承其超特型的关联项,如果你想调用超特型的方法,那么仍要保证每个特型都在作用域内
类型关联函数
很多语言中,接口不能包含静态方法或构造函数,但是特型可以包含类型关联函数。类型关联函数可以不接受self参数,就像静态方法或者构造函数一样。可以使用::调用。
特型对象不支持类型关联函数。可以向构造函数的返回类型Self添加Sized限界来声明特型对象不需要支持已经被Sized限制的关联函数。
完全限定的方法调用
当方法重名、无法推断self唯一类型、将函数本身用作函数类型的值、在宏中调用特型方法时,完全限定语法会很好用。
str::to_string("hello") // 限定方法调用,指明方法关联的类型
ToString::to_string("hello") // 限定方法调用,指明方法关联的特型
<str as ToString>::to_string("hello") // 完全限定方法调用,同时指明两者
定义类型之间关系的特型
关联类型
例如迭代器特型:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>; // 注意Option中的类型要指明是实现本特型的当前类型的关联类型,所以要指定Self::Item
...
}
impl Iterator for Args {
type Item = String;
fn next(&mut self) -> Option<String> {
...
}
...
}
// 在泛型代码中,如果需要用到当前的Item,可以使用特型限界::Item
fn print<I>(iter: I)
where I: Iterator, I::Item: Debug
{...}
// 或者直接指定Item类型
fn print<I: Iterator<Item=String>>(iter: I)
{...}
泛型特型
泛型特型在涉及孤儿规则时会得到特殊豁免:可以为外部类型实现外部特型,前提是特型的类型参数之一是当前crate中定义的类型(举例:自定义类型A,则可以为f64实现Mul<A>)。
pub trait Mul<RHS=Self> {...}
当泛型函数返回值类型复杂或难以描述,但使用特型作为限界会更方便描述时,可以使用impl trait:
fn fn_test(arg: impl TraitName) -> impl TraitName2 {...}
但是如果将impl trait语法用在参数描述上,需要注意,使用泛型允许调用者指定泛型参数的类型,比如fn_test::<u8>(6),而如果使用impl trait则不能这样做。
与结构体和枚举一样,特型也可以有关联常量。可以在特型中声明即初始化,也可以仅声明,约束特型的实现者定义这些值。
实用工具特型
Drop
析构器,RUst丢弃值时自动运行的清理代码。
如果一个类型实现了Drop,就不能再实现Copy类型了。
Sized
标记特型。固定大小类型,即每个值在内存中都有相同大小的类型。常见无固定大小类型:str,[T],这两个都是表示不定大小的值集,还有dyn,这个是特型对象的引用目标。
Rust不能将无固定大小的值存储在变量中或将它们作为参数传递,所以,指向无固定大小值的指针始终是一个胖指针,宽度为两个机器字,指向切片的指针带有切片的长度,特型对象带有指向方法实现的虚表的指针。除指针外携带的长度或者虚表指针信息是必要的,因为无法在不知道长度的情况下对[T]进行索引,也无法在不知道具体实现的情况下调用&dyn Write的方法:缺少静态信息就用动态信息来弥补。
Rust是将Sized作为隐式默认值:Struct A<T> {}即Struct A<T: Sized> {},只有确定不要这种约束时才需要显式声明。注意,这样可能会导致指向T的普通指针变成胖指针。
除了切片对象和特型对象,还有另一种无固定大小类型。结构体类型的最后一个字段(只能是最后一个)可以是无固定大小的,这样的结构体本身也是无固定大小的。可以将其与固定大小类型或者无固定大小类型一起使用,当无固定大小类型是特型对象时,可以尝试先构建一个普通大小类型,然后将其引用转换为胖引用。
Clone
trait Clone: Sized {
fn clone(&self) -> Self;
fn clone_from(&mut self, source: &Self) {
*self = source.clone()
}
}
std::clone::Clone特型适用于可复制自身的类型。在泛型代码中应该优先使用clone_from,这可能得到相应的优化。如果认为clone_from不需要再做优化并且类型中每个字段都可以clone,那么可以使用#[derive(clone)]派生该特型。注意:clone必须是不会失败的。
Copy
标记特型。
// std::maker::Copy标记特型
trait Copy: Clone {}
// 实现
impl Copy for A {}
注意只有类型可以简单地逐字节复制时,才可以实现Copy。任何实现了Drop特型的类型都不能是Copy类型。此处书中前后共给出了两种解释,一种具体点,一种较笼统。具体点是实现Copy的类型可以简单地逐字节赋值生成独立副本,对同一份数据多次调用同一个drop方法显然是错误的;笼统点是如果一个类型需要特殊的清理代码,那么必然需要特殊的赋值代码,因此不能是Copy类型。
Deref与DerefMut
trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}
trait DerefMut: Deref {
fn deref_mut(&mut self) -> &mut Self::Target;
}
检查类型不匹配问题时,如果deref调用可以解决该问题,那么Rust就会这样做。实现DerefMut也可以为可变引用启用相应的转换,甚至在必要的情况下连续应用多次,这就是隐式解引用:一种类型被转换成了另一种类型。
当在泛型函数中,转换检查通过但是特型限界并不会直接检查转换结果类型,这可能会导致限界检查失败,此时可以使用as关键字显式转换或者使用&*强制转换。
Default
trait Default {
fn default() -> Self;
}
即类型的默认值。当需要为大量参数集合的结构体生成默认值时,..Default::default()作为自动补全字段值非常方便。Rust不会为结构体类型隐式实现Default,需要使用#[derive(Default)]自动实现。
AsRef和AsMut
如果一个类型实现了AsRef<T>,那么就意味着你可以高效地从中接入&T。AsMut是AsRef针对可变引用的对应类型。
trait AsRef<T: ?Sized> {
fn as_ref(&self) -> &T;
}
trait AsMut<T: ?Sized> {
fn as_mut(&mut self) -> &mut T;
}
Borrow与BorrowMut
如果一个类型实现了Borrow<T>,那么它的borrow方法就能高效地从自身接入一个&T。但是Borrow施加了更多限制:只有当&T能通过与它借来的值相同地方式进行哈希和比较时,此类型彩英实现Borrow<T>(没有做强制检查)。这使得Borrow在处理哈希表和树中地键或者处理因为某些原因要进行哈希或者比较的值时非常有用。
From和Into
std::convert::From特型和std::convert::Into特型表示类型转换,即接受一种类型的值并返回另一种类型的值。对比:AsRef和AsMut特型用于从一种类型借入另一种类型的引用,而From和Into会获取其参数的所有权,对其进行转换,然后将转换结果的所有权返回给调用者。
trait Into<T>: Sized {
fn into(self) -> T; // 注意消耗了self
}
trait From<T>: Sized {
fn from(other: T) -> Self; // 注意消耗了other
}
使用场景:
- Into,用来做参数约束,比如泛型函数的类型参数限界
- from用作泛型构造函数
给定适当的From实现,标准库会自动实现相应的Into特型。当你定义自己的类型时,如果它具有某些单参数构造函数,那么就应该将他们写成适当类型的From<T>的实现,这样就会自动获得相应的Into实现。因为from和into会接手参数的所有权,所以此转换可以服用原始值的资源来构造出转换后的值。
let text = "Beautiful Soup".to_string();
let bytes: Vec<u8> = text.into();
From和Into是不会失败的特型。如果需要允许出现错误后以Result来处理错误,可以使用TryFrom与TryInto特型。
ToOwned
trait ToOwned {
type Owned: Borrow<Self>;
fn to_owned(&self) -> Self::Owned;
}
Cow
即clone on write,写入时克隆。当无法预先在编码时确定该借用还是该拥有时,可以使用Cow。
// std::borrow::Cow
enum Cow<'a, B: ?Sized>
where B: ToOwned
{
Borrowed(&'a B),
Owned(<B as ToOwned>::Owned),
}
运算符重载
算术符运算符与按位运算符
特型的泛型实现
use std::ops::Add;
impl<L, R> Add<Complex<R>> for Complex<L>
where:
L: Add<R>
{
type Output = Complex<L::Output>;
fn add(self, rhs: Complex<R>) -> Self::Output {
Complex {
re: self.re + rhs.re,
im: self.im + rhs.im,
}
}
}
一元运算符
一元取负运算符:std::ops::Neg,一元取反运算符:std::ops::Not。
impl<T> Neg for Complex<T>
where
T: Neg<Output=T>
{
type Output = Complex<T>;
fn neg(self) -> Complex<T> {
Complex {
re: -self.re,
im: -self.im,
}
}
}
二元运算符
二元运算符的内置特型
算术运算符:
- std::ops::Add,x + y
- std::ops::Sub,x - y
- std::ops::Mul,x * y
- std::ops::Div,x / y
- std::ops::Rem,x % y
按位运算符:
- std::ops::BitAnd,x & y
- std::ops::BitOr,x | y
- std::ops::BitXor,x ^ y
- std::ops::shl,x << y
- std::ops::shr,x >> y
Rust的所有数值类型都实现了算术运算符,整数和bool类型都实现了按位运算符。
提示:字符串连接的+操作符,左操作数不可以是&str类型,出于性能考虑。
复合赋值运算符
Rust中的x += y并非是x = x + y的简写形式,而是x.add_assign(y)的简写形式。
复合运算符的内置特型类似二元运算符的内置特型。
Rust的所有数值类型都实现了算术复合赋值运算符,整数和bool类型都实现了按位复合赋值运算符。
use std::ops::AddAssign;
impl<T> AddAssign for Complex<T>
where
T: AddAssign<T>
{
fn add_assign(&mut self, rhs: Complex<T>) {
Complex {
self.re += rhs.re,
self.im += rhs.im,
}
}
}
相等性比较
Rust的相等性运算符==和!=是对调用std::cmp::PartialEq特型的eq和ne这两个方法的简写。
trait PartialEq<Rhs=Self>
where
Rhs: ?Sized, // 允许无固定大小类型,例如str
{
fn eq(&self, rhs: &Rhs) -> bool;
fn ne(&self, rhs: &Rhs) -> bool{
!self.eq(rhs)
}
}
impl<T: PartialEq> PartialEq for Complex<T> {
fn eq(&self, rhs: &Complex<T>) -> bool {
self.re == rhs.re && self.im == rhs.im
}
}
对于枚举和结构体类型,如果每个字段/变体都实现了PartialEq,那么可以为其派生出PartialEq特型。
PartialEq特型的命名来源,在数学定义中,对于任意值x和y的等价关系有三个要求:
- 交换性,若x == y为真,则y==x也为真,
- 传递性,若xy且yz,则x==z一定成立,
- x==x必须始终为真。
但IEEE标准浮点值要求任何值与NaN值进行比较都必须返回false,这没办法满足等价关系的第三个要求。所以Rust使用部分相等关系的PartialEq作为==运算符的内置特型。
如果想要完全相等关系,可以使用std::cmp::Eq特型作为限界。在标准库中,事实上只有f32和f64只实现了PartialEq而没有实现Eq。
自己实现PartialEq的ne方法时要注意ne和eq必须精确互补。
有序比较
Rust会根据单个特型std::cmp::PartialOrd来定义全部的有序比较运算符<,>,<=和>=的行为:
trait PartialOrd<Rhs=Self>: PartialEq<Rhs>
where
Rhs: ?Sized,
{
fn partial_cmp(&self, other: &Rhs) -> Option<Ordering>;
...
}
PartialOrd扩展了PartialEq<Rhs>,只有可以比较相等性的类型才能比较顺序性。PartialOrd中必须自行实现的唯一方法是partial_cmp。partial_cmp返回None时,代表两者都不大于对方,但也不想等,较大可能是出现了NaN值。返回Some(o)时则应指出两个值之间的关系。
更严格的顺序特型:
trait Ord: Eq + PartialOrd<Self> {
fn cmp(&self, other: &Self) -> Ordering;
}
类似于PartialEq和Eq的关系,在标准库中,事实上只有f32和f64只实现了PartialOrd而没有实现Ord。
Index与IndexMut
表达式a[i]通常是*a.index(i)的简写形式,a[i, j]通常是*a.index(std::ops::Range{start: i, end: j})的简写形式。
trait Index<Idx> {
type Output: ?Sized;
fn index(&self, index: Idx) -> &Self::Output;
}
trait IndexMut<Idx>: Index<Idx> {
fn index_mut(&mut self, index: Idx) -> &mut Self::Output;
}
闭包
捕获变量
包括引用与移动变量。
Python中捕获变量是捕获函数周围环境变量,然后使被捕获的变量不因单次调用结束而被回收。与Python不同,当Rust创建闭包时,会自动借入对被捕获变量的引用。
此处的引用与移动仍然遵守全局约定,Copy类型会直接捕获其副本。
use std::thread;
fn main() {}
#[derive(Debug, PartialEq)]
struct Cat {
age: u8,
}
impl Cat {
fn rank(&self, _offset: u8) -> i32 {
self.age as i32
}
}
fn test(mut cats: Vec<Cat>, offset: u8) -> thread::JoinHandle<Vec<Cat>> {
let k = move |cat: &Cat| -> i32 { -cat.rank(offset) }; // 注意这个move,获取了offset的所有权。
thread::spawn(
move || { // 注意这个move,获取了cats和k的所有权。
cats.sort_by_key(k);
cats
}
)
}
#[test]
fn test_any() {
let cats = vec![Cat{ age: 6 }, Cat{ age: 8 }, Cat{ age: 9 }];
let x = test(cats, 0);
assert_eq!(x.join().unwrap(), vec![Cat{ age: 9 }, Cat{ age: 8 }, Cat{ age: 6 }])
}
函数与闭包的类型
fn rank(offset: u8) -> i32 {
...
}
// 该函数类型是 fn(u8) -> i32
可以像操作其他值一样操作函数值,结构体可以有函数类型的字段,Vec也可以存储同一个fn类型的多个函数,而且函数值占用的空间很小,fn值即函数机器码的内存地址,类似C/C++中的函数指针。
// 注意区别
fn(u8) -> i32 // fn类型,只接受函数
Fn(u8) -> i32 // Fn特型,既接受函数也接受闭包
因为每个闭包都有自己的类型(任何两个闭包的类型都不一样),所以使用闭包的代码通常都应该是泛型的。
相对于其他语言,Rust的闭包不会在堆中分配内存(除非故意放在某些容器中或使用其他手段),没有动态派发及垃圾回收。只要知道正在调用的闭包的类型就可以内联该闭包的代码并做出优化。
let x = "x".to_string();
let f = || drop(x);
f();
f(); // 报错,使用已移动的值,如果执行将会双重释放
不同的闭包特型
FnOnce、FnMut、Fn
// 伪代码
// Fn是可以不受限制地调用任意多次的闭包和函数系列也包括所有fn函数
trait Fn() -> R {
fn call(&self) -> R;
}
// 本身被声明为mut并且可以多次调用地闭包系列
trait FnMut() -> R {
fn call_mut(&mut self) -> R;
}
// 注意,只有在闭包内将捕获变量移动或者当场显式消耗、丢弃才会实现FnOnce,闭包的花括号不会触发对捕获变量的drop
// 如果调用者拥有此闭包,就只能调用一次地闭包系列
trait FnOnce() -> R {
fn call_once(self) -> R; // 注意FnOnce的特型方法消耗了自身,所以只能调用一次
}
FnOnce包含FnMut包含Fn。
注意,修改闭包捕获变量需要将变量本身声明为mut、闭包类型本身声明为mut、闭包泛型限界声明为FnMut(),修改闭包捕获变量需要从闭包本身借入可变引用。
闭包的Clone和Copy规则与全局规则一致,不修改变量的非move闭包只持有共享引用,共享引用本身既可Clone也可Copy,所以闭包也既能Clone也能Copy。而会修改值的非move闭包中也可以有可变引用,可变引用既不能Clone,也不能Copy,闭包亦同。而对于move闭包,如果闭包捕获的所有内容都能Clone,则闭包可以Clone,如果闭包捕获的所有内容都能Copy则闭包亦可。
可以通过限制调用者仅使用非捕获型闭包来牺牲调用者灵活性换取性能。
迭代器
迭代器是一个值,它可以生成一系列值,通常用来执行循环操作。
迭代器是实现了std::iter::Iterator特型的任意值:
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
...
}
只要可以用某种自然的方式来迭代某种类型,该类型就可以实现std::iter::IntoIterator,这个特型的into_iter方法会接受一个值并返回一个迭代器。
trait IntoIterator where Self::IntoIter: Iterator<Item=Self::Item> {
type Item;
type IntoIter: Iterator;
fn into_iter(self) -> Self::IntoIter;
}
任何实现了IntoIterator的类型都称为可迭代者。
所有迭代器都自动实现了IntoIterator,并带有一个直接返回迭代器的into_iter方法。
iter、iter_mut、into_iter
大多数集合类型提供了iter方法和iter_mut方法,它们会返回该类型的自然迭代器,为每个条目生成共享引用或可变引用。注意,这只是类似一种默契,并未归纳为一种特型。
至于into_iter方法,要看调用者的类型(&T、&mut T、T),迭代器Item借用与所有权形式与调用者保持一致。
from_fn和successors
from_fn和successors接受实现了FnMut闭包,返回迭代器。
use rand::random;
use std::iter::from_fn;
let lengths: Vec<f64> = from_fn(|| Some((random::<f64>() - random::<f64>())abs())).take(1000).collect();
如果每个条目都依赖于前一个条目,可以尝试successors。
use std::iter::successors;
fn main() {
// 初始值 前值
let x: Vec<i32> = successors(Some(0), |&x| { Some(x + x + 1) })
.take(6)
.collect();
println!("{:?}", x);
}
迭代器适配器
适配器会消耗某个迭代器并构建一个实现了特定行为的新迭代器。
map和filter
map会返回一个迭代器,这个迭代器的条目来自对原迭代器每个条目调用map参数的结果。
filter会返回一个迭代器,这个迭代器的条目来自原迭代器中可以使filter参数闭包返回真的条目,在传递给闭包条目时实际传递的是条目的共享引用,但是filter会保留条目所有权以便筛选条目。
注意:
- 大多数适配器要求使用值调用,所以要求原迭代器必须是Sized。
- 迭代器的惰性,单纯在迭代器上调用适配器并不会消耗任何条目,消耗条目需要在最终迭代器上调用next。
- 迭代器的适配器是一种零成本抽象,Rust可以搜集到足够的信息支持代码特化、内联并翻译成机器代码。
#[test]
fn test_any() {
let text = " hello, \n world \n ! \n nothing".to_string();
let new_text: Vec<_> = text.lines()
.map(str::trim)
.filter(|s| *s != "nothing")
.collect();
assert_eq!(new_text, vec!["hello,", "world", "!"]);
}
filter_map和flat_map
当需要先试着实际处理一下条目才能决定是否需要这个条目时,可以尝试filter_map:
#[test]
fn test_any() {
use std::str::FromStr;
let text = "1\nsdfsfs 25 68\n3 ";
let mut test = vec![1, 25, 68, 3].into_iter();
for num in text
.split_whitespace()
.filter_map(|n| i32::from_str(n).ok()) // .ok()变成Option<f64>,丢弃None并为Some(v)生成v
{
assert_eq!(num, test.next().unwrap())
}
}
flat_map迭代器会生成此闭包返回的序列串联后的结果。
#[test]
fn test_any() {
use std::collections::HashMap;
let mut x = HashMap::new();
x.insert("A", vec![1, 2, 3]);
x.insert("B", vec![4, 5, 6]);
x.insert("C", vec![7, 8, 9]);
let y: Vec<_> = ["A", "B", "C"].iter().flat_map(|&s| &x[s]).cloned().collect();
assert_eq!(y, vec![1, 2, 3, 4, 5, 6, 7, 8, 9])
}
flatten
flatten适配器会串联起迭代器的各个条目。
#[test]
fn test_any() {
use std::collections::BTreeMap; // 注意是BTreeMap
let mut x = BTreeMap::new();
x.insert("A", vec![1, 2, 3]);
x.insert("B", vec![4, 5, 6]);
x.insert("C", vec![7, 8, 9]);
let y: Vec<_> = x.values().flatten().cloned().collect();
assert_eq!(y, vec![1, 2, 3, 4, 5, 6, 7, 8, 9])
}
提取集合中被包裹的数据的小技巧
如果要从Vec<Option<T>>中提取值,可以借用Option的性质:
#[test]
fn test_any() {
let x = vec![None, Some(1), None, None, Some(2)]
.into_iter() // Option本身也实现了IntoIterator,表示0个或1个元素组成的序列
.flatten() // Some值可视作含有一个值的向量,None可视为空向量,所以None会被忽视
.collect::<Vec<_>>();
assert_eq!(x, vec![1, 2]);
}
// 同理于Result,但是要注意,这会丢弃Err,而通常不应该忽视Err。
// 如果需要遇到Err就停止构建(不忽视Err),可以用Result对FromIterator的实现
#[test]
fn test_any() {
{
let x: Vec<Result<i32, ()>> = vec![Ok(1), Ok(2), Err(())];
let y = x
.into_iter()
.collect::<Result<Vec<i32>, ()>>().unwrap_or(vec![3, ]);
assert_eq!(y, vec![3]);
}
{
let x: Vec<Result<i32, ()>> = vec![Ok(1), Ok(2)];
let y = x
.into_iter()
.collect::<Result<Vec<i32>, ()>>().unwrap_or(vec![3, ]);
assert_eq!(y, vec![1, 2]);
}
}
如果同时调用了map适配器和flatten适配器,则flat_map可能是更好的选择。
take、skip、take_while、skip_while
take即当条目数达到一定数量时结束迭代。
skip即从迭代开始丢弃一定数量的条目。
take_while即当闭包首次返回false时结束迭代。
skip_while即从迭代开始丢弃条目直至闭包返回false。
peekable
允许窥视即将生成的下一个条目,而无须实际消耗它。peekable迭代器有一个额外的方法peek,该方法会返回一个Option<&Item>:底层迭代器耗尽时为None,否则为Some(v)。
fn parse_number<I>(tokens: &mut Peekable<I>) -> u32
where I: Iterator<Item=char>
{
let mut n = 0;
loop {
match tokens.peek() {
Some(r) if r.is_digit(10) => {
n = n * 10 + r.to_digit(10).unwrap();
}
_ => return n
}
tokens.next();
}
}
#[test]
fn test_any() {
let mut chars = "226153980,163164163".chars().peekable();
assert_eq!(parse_number(&mut chars), 226153980);
assert_eq!(chars.next(), Some(','));
assert_eq!(parse_number(&mut chars), 163164163);
assert_eq!(chars.next(), None);
}
fuse
fuse适配器能接受任何迭代器并生成一个确保在第一次返回None后继续返回None的迭代器。
可逆迭代器和rev
双端迭代器实现了std::iter::DoubleEndedIterator。
trait DoubleEndedIterator: Iterator {
fn next_back(&mut self) -> Option<Self::Item>;
}
如果迭代器是双端的,就可以用rev适配器将其逆转,返回的迭代器也是双端的,只是互换了next方法和next_back方法。
fn rev(self) -> impl Iterator<Item=Self::Item>
where Self: Sized + DoubleEndedIterator;
inspect、chain、enumerate
inspect适配器(探查适配器)只对每个条目的共享引用调用闭包,然后传递该条目,不会影响条目。
x.chain(y)会返回一个迭代器,该迭代器会从x中提取条目,x耗尽时从y中提取条目。
fn chain<U>(self, other: U) -> impl Iterator<Item=self::Item>
where Self: Sized, U: IntoIterator<Item=Self::Item>;
enumerate基本上其他语言都有。
zip
泛化版enumerate,可以指定index(类似概念)。
#[test]
fn test_any() {
let x: Vec<_> = repeat(6u8)
.zip(["A", "B", "C", "D", ]) // zip的参数可以是任意可迭代者。
.collect();
assert_eq!(x, vec![(6u8, "A"), (6, "B"), (6, "C"), (6, "D")])
}
by_ref
迭代器的by_ref方法会借入迭代器的可变引用,便于将各种适配器应用于该引用一旦消耗完适配器中的条目,就会丢弃这些适配器,借用也就结束了,就能重新获得对原始迭代器的访问权。
cloned与copied
cloned适配器会接受一个生成引用的迭代器,并返回一个会生成从这些引用克隆而来的值的迭代器,就像iter.map(|item| item.clone())。当然,引用目标的类型也必须实现了Clone。copied类似。
cycle
底层迭代器必须实现std::clone::Clone,cycle会无限重复底层迭代器生成的序列。
消耗迭代器的方式
count、sum、product、min、max、min_by、max_by、min_by_key、max_by_key
count即计数,sum求和,product求乘积,min最小,max最大,min_by根据给定函数确认最小,max_by根据给定函数确定最大,min_by_key根据给定闭包确定最小,max_by_key根据指定闭包确定最大。
迭代器比较
包括相等比较eq、ne和有序比较lt、le、gt、ge。
any与all
any和all会将闭包应用于迭代器的各个条目,任意一条目作用于返回true,则any立即短路返回true,all则需要所有条目作用于闭包都返回true。
position、rposition和ExactSizeIterator
position对每个条目调用闭包,返回调用结果为true的第一个条目的Option<索引>,rposition从右侧开始搜索,其要求迭代器是确切大小迭代器,即实现了std::iter::ExactSizeIterator的迭代器。
fold、rfold、try_fold、try_rfold
#[test]
fn test_any() {
let x = [1, 2, 3, 4, 5, 6];
// 累加器初始值 当前累加器 下一条目
assert_eq!(x.iter().fold(0, |n, _| n+1), 6); // count
assert_eq!(x.iter().fold(0, |n, i| n+i), 21); // sum
assert_eq!(x.iter().fold(1, |n, i| n*i), 720); // product
}
rfold与fold基本相同,但需要一个双端迭代器。
try_fold与fold类似,只是可以提前退出迭代。Result<T, E>的Err(e)、Option<T>的None、std::ops::ControlFlow的Break(b)会指示立即停止折叠。
nth、nth_back、last
返回第n个或者倒数第n个条目,返回结果为Option。last会从前面开始消耗所有条目并返回最后一个条目。双端迭代器应首先考虑iter.next_back()。
find、rfind和find_map
find提取条目,返回第一个作用于给定闭包的结果为true的条目(Option),rfind要求双端迭代器,从后往前搜索。find_map会返回第一个类型为Some的Option。
构建集合:collect与FromIterator
如果某些集合类型知道如何从迭代器构造自身,就会自行实现std::iter::FromIterator特型,而collect只是一个便捷的浅层包装而已。
Extend、partition、for_each、try_for_each
trait Extend<A> {
fn extend<T>(&mut self, iter: T)
where T: IntoIterator<Item=A>;
}
#[test]
fn test_any() {
let mut x = vec![1, 2, 3, 4, 5, 6];
x.extend(vec![7, 8, 9]);
assert_eq!(x, vec![1, 2, 3, 4, 5, 6, 7, 8, 9]);
}
#[test]
fn test_any() {
let x = vec!["aab", "bab", "aac", "bba"];
let (y, z): (Vec<&str>, Vec<&str>) = x.iter().partition(|&&x| x.starts_with("a"));
assert_eq!(y, vec!["aab", "aac"]);
assert_eq!(z, vec!["bab", "bba"]);
}
for_each类似于for循环,同样可以使用break和continue,相对于for循环某些情况下更清晰。如果需要容错或提前退出,可以使用try_for_each。
集合
集合 | 描述 |
---|---|
Vec<T> | 可增长数组(普通向量) |
VecDeque<T> | 双端队列向量、更适合做先入先出队列 |
BinaryHeap<T> | 二叉堆,最大堆,可以高效查找和移除最大值 |
HashMap<K, V> | 键值对表 |
BtreeMap<K, V> | 会根据键来对条目进行排序的表 |
HashSet<T> | 可以很快查询值是否在Set中 |
BtreeSet<T> | Set会将元素排序 |
全是方法介绍,快速浏览......
字符串
快速浏览......
输入与输出
Rust标准库中的输入和输出的特型是围绕3个特性组织的,即Read、BufRead和Write。
实现了Read的值具有面向字节的输入方法。即读取器。
实现了BufRead的值是缓冲读取器。支持Read的所有方法,外加读取文本行等方法。
实现了Write的值能支持面向字节和UTF-8文本的输出。即写入器。
将所有字节从任意读取器复制到任意写入器的函数示例:
use std::io::{self, Read, Write, ErrorKind};
const DEFAULT_BUF_SIZE = 8 * 1024;
pub fn copy<R: ?Sized, W: ?Sized>(reader: &mut R, writer: &mut W) -> io::Result<u64>
where R: Read, W: Write
{
let mut buf = [0; DEFAULT_BUF_SIZE];
let mut written = 0;
loop {
let len = match reader.read(&mut buf) {
Ok(0) => return Ok(written),
Ok(len) => len,
Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
writer.write_all(&buf[..len])?;
written += len as u64;
}
}
读取器
Read
reader.read(&mut buffer)
// 从数据源读取不大于buffer.len()的字节并存储在buffer中,返回类型是Result<u64, io::Error>。如果返回Ok(len),len是实际成功读取的字节数,Ok(0)则表明没有更多输入可以读取。如果返回Err(e),需要单独处理一种特殊错误:io::ErorKind::Interrupted,这种错误表示读取恰好被某种信号中断了,多数情况下应该再次尝试读取。
reader.read_to_end(&mut byte_vec)
// 读取所有剩余的输入,将其追加到Vec<u8>型的byte_vec中。返回io::Result<usize>,即已读取的字节数。
reader.read_to_string(&mut string)
// 将数据附加到给定的String,如果流不是有效的UTF-8,则返回ErrorKind::InvalidData错误。
reader.read_exact(&mut buf)
// 读取足够的数据来填充给定的缓冲区,参数类型是&[u8]。如果数据不足,则返回ErrorKind::UnexpectedEof。
reader.bytes()
// 返回字节迭代器,返回输入流中各字节的迭代器。条目类型是io::Result<u8>,无缓冲情况下非常低效。
reader.chain(reader2)
// 返回读取器,先生成来自reader的所有输入,在生成来组reader2的所有输入。
reader.take(n)
// 返回读取器,从与reader相同的数据源读取,仅限前n字节的输入。
// 通常读取器和写入器会实现Drop以便自行关闭。
BufRead
reader.read_line(&mut line)
// 读行,返回io::Result<usize>表示已读取的字节数,包含换行符
reader.lines()
// 返回行的迭代器,条目类型是io::Result<String>,不含换行符
reader.read_until(stop_byte, &mut byte_vec)
// 读到stop_byte为止
reader.split(stop_byte)
// 根据stop_byte拆分,返回迭代器
Write
writer.write(&buf)
// 底层方法,返回已写入的字节数(io::Result<uszie>),如果流提前关闭,可能会小于buf.len()。
writer.write_all(&buf)
// 写入全部字节,返回Result<()>
writer.flush()
// 书写缓冲在内存中的数据,返回Result<()>
和BufReader::new(reader)会为任意读取器添加缓冲区一样,BufWriter::new(writer)也会为任意写入器添加缓冲区。
当丢弃BufWriter时,所有剩余的缓冲数据都将写入底层写入器,此过程(drop)中发生的错误会被忽略,所以需要在丢弃带缓冲的写入器之前手动.flush()。
文件
File::open(filename)
// 打开现有文件进行读取,返回io::Reslt<File>,文件不存在时报错。
File::create(filename)
// 创建一个用于写入的新文件。如果存在同名文件,则会截断。
其他读取器与写入器类型
io::stdin()
// 标准输入,io::stdin().lock()会返回io::StdinLock,这是一个带缓冲的读取器。
io::stdout() // io::stderr()
// 同上
Vec<u8>
// 写入器
Cursor::new()
// 游标,一个从buf读取数据的缓冲读取器
std::net::TcpStream
// 既是读取器又是写入器
std::process::Command
// 支持启动子进程并通过管道将数据传输到其标准输入
io::sink()
// 无操作写入器,只是丢弃数据
io::empty()
// 无操作读取器,读取总会成功,但只会返回EOF
io::repeat(byte)
// 返回一个会无限重复给定字节的读取器
文件与目录
OsStr和Path
OsStr是一种字符串类型,它是UTF-8的超集。OsStr的任务是表示当前系统上的所有文件名、命令行参数和环境变量,无论它们是不是有效的Unicode。OsStr可以保存任意字节序列。std::path::Path只是一个便捷名称,其与OsStr完全一样,只是添加了许多关于文件名的便捷方法。绝对路径和相对路径都使用Path表示,对于路径中的单个组件,使用OsStr。
属性 | str | OsStr | Path |
---|---|---|---|
无固定大小类型,总是通过引用传递 | 是 | 是 | 是 |
可以包含任意Unicode文本 | 是 | 是 | 是 |
通常看起来和UTF-8一样 | 是 | 是 | 是 |
可以包含非Unicode数据 | 否 | 是 | 是 |
文本处理类方法 | 是 | 否 | 否 |
文件名相关方法 | 否 | 否 | 是 |
拥有型、可增长且分配在堆上的等价类型 | String | OsString | PathBuf |
转换为拥有型类型 | .to_string() | .to_os_string() | .to_path_buf() |
上述三种类型的泛型函数的类型参数限界可以使用AsRef<Path>。
Path和PathBuf
Path::new(str)
// 新建,返回&Path,将&str或&OsStr转换为&Path,OsStr::new(str)可以将&str转换为&OsStr,注意,不会复制字符串,新的&Path会指向与原始&str或&OsStr相同的字节。
path.parent()
// 返回父目录,Option<&Path>,同样不会复制路径,父目录一定是path的字串
path.file_name()
// 返回path的最后一个组件,Option<&OsStr>
path.is_absolute()
path.is_relative()
// 判断是否为绝对路径或相对路径
path1.join(path2)
// 联结两个路径,返回一个新的PathBuf,如果path2本身是绝对路径,则只会返回path2的副本
path.components()
// 组件迭代器,条目类型为std::path::Component
path.ancestors()
// 祖先迭代器,从path开始一直遍历到根路径,每个条目都是从根路径开始的全路径
path.to_str()
// 转字符串,返回Option<&str>,如果path不是有效的UTF-8,返回None。
create_dir(path)
// 类似mkdir()
create_dir_all(path)
// 递归创建指定目录
remove_dir(path)
// 类似rmdir() Removes an empty directory.
remove_dir_all(path)
// 递归移除目录 Removes a directory at this path, after removing all its contents. Use carefully!
copy(src_path, dest_apth) -> Result<u64>
// 复制
rename(src_path, dest_apth)
// 移动
hard_link(src_path, dest_apth)
// 链接