Rust中与闭包相关的三个trait
- 2 minutes read - 311 words在 Rust 中,闭包就是一种能捕获 上下文环境变量 的函数。
let range = 0..10;
let get_range_count = || range.count();  
代码里的这个 get_range_count 就是闭包,range 是被这个闭包捕获的环境变量。
虽然说它是一种函数,但是不通过 fn 进行定义。在 Rust 中,并不把这个闭包的类型处理成 fn 这种函数指针类型,而是有单独的类型定义。
切记这里是将闭包处理成是 单独的类型定义,这一点区别与其它开发语言。
至于按哪一种类型来处理,这个没有办法得知,因为只有在Rust编译器在编译的时候才可以确定其类型,并且在确定类型时,还需要根据这个闭包捕获上下文环境变量时的行为来确定。
闭包trait分类
根据闭包行为划分为三类trait( 主因是受到所有权影响):
- FnOnce适用于能被调用一次的闭包,- 所有闭包都至少实现了这个 trait,因为所有闭包都必须能够被调用。一个会将捕获的值移出闭包体的闭包只实现- FnOncetrait,这是因为它只能被调用一次。其获取了上下文环境变量的所有权。
- FnMut适用于不会将捕获的值移出闭包体的闭包,但它可能会修改被捕获的值,这类闭包可以被调用多次。其只获取了上下文环境变量的 &mut 引用。
- Fn适用于既不将被捕获的值移出闭包体也不修改被捕获的值的闭包,当然也包括不从环境中捕获值的闭包。这类闭包可以被调用多次而不改变它们的环境,这在会- 多次并发调用闭包的场景中十分重要。其只获取了上下文环境变量的 & 不可变引用。
在编译时会将闭包确定为以上三种类型中的一种或多种组合。 Rust 给我们暴露了 FnOnce、FnMut、Fn 这 3 个 trait,就刚对应上面这三类数据类型。
它们在标准库中的定义为
trait FnOnce<Args> {
    type Output;
    fn call_once(self, args: Args) -> Self::Output;
}
trait FnMut<Args>: FnOnce<Args> {
    fn call_mut(&mut self, args: Args) -> Self::Output;
}
trait Fn<Args>: FnMut<Args> {
    fn call(&self, args: Args) -> Self::Output;
}
注意这三者的关系,其中 Fn 继承了 FnMut ,而 FnMut 又继承了 FnOnce,它们三者有层次关系的。
环境变量所有权 FnOnce
FnOnce 代表的闭包类型只能被调用一次。
fn main() {
    let x = String::from("test");
    let fnonce_closure = move || {
        let y = x; // 转移所有权
        println!("FnOnce closure, y = {}", y);
    };
    fnonce_closure();  // 可以调用
    fnonce_closure();  // 错误!在第一次调用后,x 被移动了,再次调用将导致编译错误
}
使用 move 强制闭包取得被捕获变量 x 的所有权,接着通过赋值另一个变量转移所有权,因此闭包第一次调用结正常,再次调用将出错。
另外,有时候在闭包函数前面不指定 move ,也有可能自动发生所有权转移,如
fn main() {
    let range = 0..10;
    let get_range_count = || range.count();
    assert_eq!(get_range_count(), 10); // ✅
    get_range_count(); // ❌
}
虽然没有 move 关键字,但由于range.count()会自动移动range的所有权,因此也不能多次调用。
这里range.count()调用会使range变量被消费(它是 std::ops::Range 类型的一个实例)。在方法调用时, self关键字被用于Iterator::count(self),其方法定义为“消耗”的特性,这里消耗了range,因此再次调用的时候将出错。
环境变量可变引用 FnMut
FnMut 代表的闭包类型能被调用多次,并且能修改上下文环境变量的值,不过有一些副作用,在某些情况下可能会导致错误或者不可预测的行为。
fn main() {
    let mut x = 4;
    let mut increment = || {
        x += 1;
        println!("x: {}", x);
    };
    
    increment(); // 输出 "x: 5"
    increment(); // 输出 "x: 6"
}
在这个例子中,闭包 increment 捕获了一个可变引用到变量 x 并对其进行操作。这是一个典型的 FnMut 闭包示例,因为它需要对它捕获的变量进行修改。注意,这个闭包也满足 FnOnce 特质,但不满足 Fn 特质,因为它需要对捕获的变量进行修改。
环境变量不可变引用 Fn
Fn 代表的这类闭包能被调用多次,但是对上下文环境变量没有副作用。
fn main() {
    let nums = vec![0, 4, 2, 8, 10, 7, 15, 18, 13];
    let min = 9;
    let greater_than_9 = nums.into_iter().filter(|&n| n > min).collect::<Vec<_>>();
    assert_eq!(vec![10, 15, 18, 13], greater_than_9); // ✅
}
闭包过滤出所有大于min的元素值,在filter 函数里并没有对环境变量 min 做任何的修改。
总结
根据 Rust 的设计(上方三个trait 的定义),trait 之间存在层次结构。

如果一个闭包满足 Fn 这个trait 的条件,那么它也一定满足 FnMut 和 FnOnce 这两个trait 的条件。如果满足 FnMut 这个trait的话,则一定满足 FnOnce trait 的条件,因此一个实现了 Fn 的闭包实际上是实现了 Fn 、FnMut 和 FnOnce 这三个trait。
由此可看出上面的不可用引用 Fn 例子是实现了三个trait的组合的,同样下方的示例也实现了三个trait。
fn main() {
    let x = 4;
    let f = ||{
        println!("{}", x); 
    };
    f();
    f();
}
参考资料
- https://kaisery.github.io/trpl-zh-cn/ch04-01-what-is-ownership.html 
- https://rustwiki.org/zh-CN/rust-by-example/fn/closures/capture.html