上篇文章我们着重介绍了闭包的语法和实际开发过程中的运用,但一直有一个疑问,为什么闭包能作为函数的参数以及返回类型,还有就是像函数一样能在方法体中调用,跟我们以往的编程思维有点区别。那为什么闭包会有如此强大的特性了,带着这个疑问我们一探究竟闭包的原理。

闭包和结构体的抽象

闭包表达式会生成具一个有唯一匿名类型的闭包值,该类型我们在代码中无法直接看到,是在编译器底层抽象生成的。闭包类型大致相当于包含捕获的变量的结构体。例如,下面这个闭包:

fn f<F : FnOnce() -> String> (g: F) {
    println!("{}", g());
}

let mut s = String::from("foo");
let t = String::from("bar");

f(|| {
    s += &t;
    s
});
// Prints "foobar".

上面的代码我们实现了一个字符串拼接的闭包功能,首先我们显式的定义了一个方法f,它的参数是一个FnOnce的闭包类型,接着我们创建一个可变的String类型变量s和一个不可变String类型的t,然后调用方法f传入一个闭包实现字符串的拼接。

之前我们讲解闭包的时候我们讲过三种类型的闭包分别是:Fn, FnOnce,FnMut。它们三个有一个共同的地方是就是都是trait,我们知道一般情况下我们可以为结构体(struct)实现trait,那我们不妨试着用结构体结合trait的方式推导下闭包的本质了?我们根据上面的代码把闭包所涉及的抽象成一个结构体:

struct Closure<'a'>{
  s:String,
  t:&'a String,
}

为我们抽象的这个结构体实现FnOnce trait 代码如下:

impl <'a> FnOnce<()> for Closure<'a'>{
  type Output = String;
  fn call_once(self) -> String {
    self.s += &*self.t;
    self.s
 }
}

因此上面我们创建的闭包函数运行起来如下:

f(Closure{s: s, t: &t});

这样我们通过在方法中使用结构体就可以捕获在方法中声明的任何变量,然后对变量进行我们想要的操作和运算。但是具体怎么捕获的呢,又是一个疑问,我们接着往下探索。

捕获模式

Rust 编译器在处理闭包时,会优先考虑以不可变借用(immutable borrow)的方式捕获环境中的变量,其次是唯一的不可变借用,然后是可变借用,最后才是通过移动(move)的方式捕获。编译器会根据闭包体内部对变量的使用情况,选择与之兼容的捕获方式。这个选择过程独立于闭包外部的代码,例如变量的生命周期或闭包的使用场景。

如果你在闭包前使用了 move 关键字,那么闭包将通过移动(move)的方式捕获所有变量,或者对于实现了 Copy trait 的类型,通过复制(copy)的方式捕获。这意味着,无论借用是否可行,闭包都将获取变量的所有权或复制值。move 关键字的使用通常是为了确保闭包的生命周期长于它所捕获的变量,这在闭包被返回或用于创建新线程时特别有用。简而言之,move 关键字允许闭包独立于其捕获变量的生命周期,自主管理这些变量的所有权。

复合类型(如结构体、元组和枚举)始终是全部捕获的,而不是各个字段分开捕获的。如果真要捕获单个字段,那可能需要先借用该字段到本地局部变量中:

struct SetVec {
    set: HashSet<u32>,
    vec: Vec<u32>
}

impl SetVec {
    fn populate(&mut self) {
        let vec = &mut self.vec;
        self.set.iter().for_each(|&n| {
            vec.push(n);
        })
    }
}

相反,如果闭包直接使用 self.vec,那么它将尝试通过可变引用捕获 self。但是由于 self.set 已经被借用来迭代,因此代码无法编译。

捕获中的唯一不可变借用

捕获还有另一种方式,就是可以通过一种称为唯一不可变借用的特殊借用来实现,它不能在语言中的其他任何地方使用,也不能显式写出(我们为了便于给大家讲清楚原理而抽象的代码)。它在修改可变引用的引用时发生,如以下示例所示:

let mut b = false;
let x = &mut b;
{
    let mut c = || { *x = true; };
    // 下面一行就会发生错误:
    // let y = &x;
    c();
}
let z = &x;

在上述代码中,不可能可变地借用 x,因为 x 不可变的。但与此同时,不可变地借用 x 会使赋值非法,因为 & mut 引用可能不是唯一的,因此它不能安全地用于修改值。所以使用了一个唯一的不可变借用:它不可变地借用 x,但就像可变借用一样,它必须是唯一的。在上面的例子中,取消 y 的声明将产生错误,因为它会违反闭包借用 x 的唯一性;z 的声明是有效的,因为闭包的生命周期在区块结束时到期,释放了借用。

调用trait 和自动强转

闭包类型都是实现了FnOnce,这表明它们可以通过消耗闭包的所有权调用执行它一次。此外,一些闭包实现了更具体的调用trait

  • 对采用移动语义来捕获任何变量(变量的原始所有者仍拥有该变量的所有权)的闭包都实现了FnMut,这表明该闭包可以通过可变引用来调用
  • 对捕获的变量没移除它的值,也没去修改其他值的闭包都实现了Fn,这就表明该闭包可以通过共享引用来调用。

    注意:move 闭包可能仍然实现了FnFnMut,即使它们通过移动(move)语义捕获变量。这是因为闭包类型实现了什么样的trait是由闭包对捕获的变量做了什么做决定,而不是由闭包如何捕获它们来决定。

非捕获闭包(non-capturing closure) 是指不捕获环境中的任何变量的闭包。它们可以通过匹配签名的方式或被自动强转成函数指针(例如fn()).


#![allow(unused)]
fn main() {
let add = |x, y| x + y;

let mut x = add(5,7);

type Binop = fn(i32, i32) -> i32;
let bo: Binop = add;
x = bo(5,7);
}

其它trait

所有闭包类型都实现了 Sized。此外,闭包捕获的变量的类型如果实现了如下 trait,此闭包类型也会自动实现这些 trait:

  • Clone
  • Copy
  • Sync
  • Send
    闭包类型实现 SendSync 的规则与普通结构体类型实现这俩 trait 的规则一样,而 CloneCopy 就像它们在 derived属性中表现的一样。对于 Clone,捕获变量的克隆顺序目前还没正式的规范出来。

由于捕获通常是通过引用进行的,因此会出现以下一般规则:

  • 如果所有的捕获变量都实现了 Sync,则此闭包就也实现了 Sync
  • 如果所有非唯一不可变引用捕获的变量都实现了 Sync,并且所有由唯一不可变、可变引用、复制或移动语义捕获的值都实现了 Send,则此闭包就也实现了 Send
  • 如果一个闭包没有通过唯一不可变引用或可变引用捕获任何值,并且它通过复制或移动语义捕获的所有值都分别实现了 CloneCopy,则此闭包就也实现了 CloneCopy

#![allow(unused)]
fn main() {
fn f1<F : Fn() -> i32> (g: F) { println!("{}", g());}
fn f2<F : FnMut() -> i32> (mut g: F) { println!("{}", g());}
let t = 8;    
f1(move || { t });
f2(move || { t });
}

最后希望大家关注我的公众号:花说编程,每天给你带来rust得知识


lizehucoder
1 声望0 粉丝