闭包类型

closure.md
commit: 5642af891714145cb2a765f244fff7d6b618a4c7
本章译文最后维护日期:2020-11-14

闭包表达式生成的闭包值具有唯一性和无法写出的匿名性。闭包类型近似相当于包含捕获变量的结构体。比如以下闭包示例:


#![allow(unused)]
fn main() {
fn f<F : FnOnce() -> String> (g: F) {
    println!("{}", g());
}

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

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

生成大致如下所示的闭包类型:

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

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

所以调用 f 相当于:

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

捕获方式

编译器倾向于优先通过不可变借用(immutable borrow)来捕获闭合变量(closed-over variable),其次是通过唯一不可变借用(unique immutable borrow)(见下文),再其次可变借用(mutable borrow),最后使用移动语义(move)来捕获。编译器将选择这些中的第一个能让此闭包编译通过的选项。这个选择只与闭包表达式的内容有关;编译器不考虑闭包表达式之外的代码,比如所涉及的变量的生存期。

如果使用了关键字 move ,那么所有捕获都是通过移动(move)语义进行的(当然对于 Copy类型,则是通过拷贝语义进行的),而不管借用是否可用。关键字 move 通常用于允许闭包比其捕获的值活得更久,例如返回闭包或用于生成新线程。

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


#![allow(unused)]
fn main() {
use std::collections::HashSet;

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 已经被借出用来迭代了,所以代码将无法编译。

捕获中的唯一不可变借用

捕获方式中有一种被称为唯一不可变借用的特殊类型的借用捕获,这种借用不能在语言的其他任何地方使用,也不能显式地写出。唯一不可变借用发生在修改可变引用的引用对象(referent)时,如下面的示例所示:


#![allow(unused)]
fn main() {
let mut b = false;
let x = &mut b;
{
    let mut c = || { *x = true; };
    // 下行代码不正确
    // let y = &x;
    c();
}
let z = &x;
}

在这种情况下,不能去可变借用 x,因为 x 没有标注 mut。但与此同时,如果不可变借用 x,那对其赋值又会非法,因为 & &mut 引用可能不是唯一的,因此此引用不能安全地用于修改值。所以这里闭包使用了唯一不可变借用:它采用了不可变的方式借用了 x,但是又像可变借用一样,当然前提是此借用必须是唯一的。在上面的例子中,解开 y 那行上的注释将产生错误,因为这将违反闭包对 x 的借用的唯一性;z 的声明是有效的,因为闭包的生存期在块结束时已过期,从而已经释放了对 x 的借用。

调用trait 和自动强转

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

  • 对没采用移动语义来捕获任何变量(译者注:变量的原始所有者仍拥有该变量的所有权)的闭包都实现了 FnMut,这表明该闭包可以通过可变引用来调用。

  • 对捕获的变量没移出其值,也没去修改其值的闭包都实现了 Fn,这表明该闭包可以通过共享引用来调用。

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

*非捕获闭包(Non-capturing closures)*是指不捕获环境中的任何变量的闭包。它们可以通过匹配签名的方式被自动强转成函数指针(例如 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:

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

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

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

译者的实验代码:


#![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 });
}