
11 迭代与闭包
闭包
FnOnce,FnMut,Fn 这三个泛型 trait 是闭包必须实现的三种可能的trait
基础理解
我们知道普通的函数定义在编译前,可以称为静态函数。而闭包则是在运行时根据运行时数据定义的动态函数。
- 不可变借用
- 可变借用
- 获取所有权
捕获会在闭包开始定义的时候开始,在闭包内部代码决定是否返还。因为调用这些闭包函数,需要传参,如果获取所有权之后,那么数据就没了,所以这种闭包只能调用1次。
编译器会根据闭包中使用作用域变量的形式来最小的捕获变量。
即使闭包体不严格需要所有权,如果希望强制闭包获取它在环境中所使用的值的所有权,可以在参数列表前使用 move 关键字。move | 参数列表 | -> 语句
新线程可能在主线程剩余部分执行完前执行完,也可能在主线程执行完之后执行完。如果主线程维护了 list 的所有权但却在新线程之前结束并且丢弃了 list ,则在线程中的不可变引用将失效。因此,编译器要求 list 被移动到在新线程中运行的闭包中,这样引用就是有效的。
因为闭包本质是匿名函数,无须暴露给外部调用,所以只需要满足编译条件即可。
最基本的形式是|参数或者没有| -> 语句
也可以添加类型注解:
let f = |num:u32| -> u32 {
num
};
闭包的参数类型、返回值类型虽然并非编译器确定,但是在运行时确定之后就不能够再改变了,因为改变了实际上就不是同一个闭包了。
所以当我们试图对一个已经被构建的闭包传入其他类型的参数或者返回值,就会得到报错,这些报错是编译器推断出来的。
在闭包定义时,闭包自身的名字尚未绑定,所以不能在内部使用 闭包之后的名字 调用自己。
但是可以利用传入参数、利用指针延迟绑定等来实现。
闭包是一种结构体
闭包会生成一个带有 call 方法的匿名结构体
-
创建一个匿名结构体,这个结构体的字段就是你捕获的所有变量。
-
根据闭包体对捕获变量的使用方式,自动为该结构体实现
FnOnce、FnMut或Fn中的一个或多个 trait。 -
闭包的“调用”其实就是调用该结构体上的
call方法。
当闭包的定义代码,如果没有具体类型信息,在第一次调用时则会初始化成功。
如果在定义时指名的具体类型信息,那么即刻初始化。
初始化伴随着环境变量的捕获。
call 方法的第一个参数是什么形式的 self,决定了闭包的调用次数以及释放时机。根据闭包体如何处理这些值,闭包会自动、渐进地实现一个、两个或全部三个 Fn trait。
| 闭包实现的 trait | call 方法的 self 参数 | 调用时对闭包本身的影响 |
|---|---|---|
FnOnce | self (所有权) | 消耗闭包,闭包本身被销毁 |
FnMut | &mut self | 闭包保留,但可修改其内部字段 |
Fn | &self | 闭包保留,内部字段不可变 |
当闭包对象不再会被调用,或者不再满足调用条件,就会被立即释放。
FnOnce 是唯一能消耗自身的闭包 trait。一个捕获了所有权的闭包只要实现了 FnOnce,调用它时就会把闭包销毁,并且可以将其内部的值移出返回。
闭包的“偷值”机制
如果让闭包存在期间一值占用值,总是不方便的,有什么方法能从闭包中移出值呢?
除了等待闭包释放之外,我们还可以在闭包存在期间,将闭包内部状态“偷”出去。
Option 本身并不特殊,真正特殊的是它提供了一个方法:take(),用 None 把原来的值替换出来,然后把旧所有权返回给你。
pub fn take(&mut self) -> Option<T> {
mem::replace(self, None)
}
任何类型,只要能提供一种“取出内部所有权并留下一个合法值”的操作,都可以达到同样效果。
第一次调用的时候返回Some,后面的调用返回None而不会导致释放,完全变成了FnMut因为调用Option的take使用的最小权限是&mut self
闭包作为参数
一个闭包作为参数传给函数时,本质上是把“一个实现了某个 Trait 的类型值”传给函数。所以我们可以使用泛型来对这个类型进行约束,要求实现闭包的trait。
// 参数是一个闭包,接收一个 i32,返回一个 bool
// 它隐式地引入了一个泛型。
fn process<F>(data: i32, mut callback: impl FnMut(i32) -> bool) {
callback(data);
}
//或者标准的泛型约束
fn process<F>(data: i32, callback: F) -> i32
where
F: Fn(i32) -> i32, // 约束:必须实现 Fn trait
{
callback(data) * 2
}
迭代器
[!summary] 迭代器是一个实现了 Iterator trait 的结构体
迭代器中收集了需要迭代的对象的不可变引用。然后通过调用过程方法来收集中间所需的闭包对象。
只有调用收集方法的时候才会开始进行迭代操作。
无论你调用 .iter()、.into_iter() 还是自己写一个 for 循环,底层都会生成一个结构体,这个结构体:
-
保存迭代的当前状态(例如索引、指针、某个数据结构的内部游标等)。
-
实现了
Iteratortrait,提供一个fn next(&mut self) -> Option<Self::Item>方法。 -
惰性求值:在调用
next()或消费方法(如collect,sum,for_each)之前,什么也不做。这和“调用闭包时才执行”是同一个原理。
迭代器上调用 next 方法会改变迭代器内部的状态,该状态用于跟踪迭代器在序列中的位置。
迭代器内部实现就是访问索引,但是内部管理了索引的状态,这些状态是属于迭代器的。
在迭代期间,如果外部更新了序列,增加了减少了,迭代器索引状态却不对应改变,会导致迭代器数据前后出现时空差异(迭代器所要迭代的数据应该是同一时刻的数据快照而非实时数据)。所以在迭代器存在期间,禁止更新序列。
iter 方法生成一个不可变引用的迭代器, iter_mut则是组成可变vector。以及获取所有权并组成具有迭代数据的所有权的迭代器into_iter。
最大的区别就是next方法返回的数据是否可变,是否有所有权。
消费与迭代操作
因为这些操作会推进迭代进度,“消耗”迭代器。
例如sum()方法,获取迭代器所有权,反复调用next直到最后一个元素。
collect()方法,消费迭代器并生成一个集合。
这些方法就像转换器一样,有输入也有输出。转换原始迭代器的某些方面来生成新的迭代器。
fold可以累加或者累乘之类的。
经典的迭代操作有: map() 转换原始元素为修改过的新元素,返回一个新的迭代器。
迭代器一旦消费了一个元素,就再也无法回溯过去。
迭代器是为了解决流的处理问题。流的概念是一个不可知长度的数据序列。通常流的概念用于网络传输,大文件读取等等
流在计算机中,通常都不会一次性全部载入内存,遵循 “读一点、消费一点、丢弃旧数据” 的模式。
在这种模式下,像数组一样直接访问数据序列的任意位置是不现实的。
迭代器的语义,就是逐步获取流中的下段数据。当迭代器到达终点之后,如果想回头,只能使用一个新的迭代器,因为迭代器本身没有回溯的语义。要回溯已经从内存抛弃的数据比较困难,其消耗远超重新从头迭代到指定位置。
如果要使用回溯,请使用for,loop,while等循环,更符合自由定制的观点。
零成本抽象
零开销原则:不使用的功能无需为其付出代价;而已经使用的功能,也不可能通过手写代码做得更好。
