理解 Rust 中的闭包

11 minute read Published: 2019-07-11

本文翻译自博文 Understanding Closures in Rust

所有权利归原作者所有。

总结

理解 Rust 中不同类型的闭包

不像一些其它的语言,Rust 希望我们显式地使用 self 参数。当我们为一个结构体实现方法时,我们必须把函数签名中的第一个参数指定为 self

struct MyStruct {
    text: &'static str,
    number: u32,
}
impl MyStruct {
    fn new (text: &'static str, number: u32) -> MyStruct {
        MyStruct {
            text: text,
            number: number,
        }
    }
    // We have to specify that 'self' is an argument.
    fn get_number (&self) -> u32 {
        self.number
    }
    // We can specify different kinds of ownership and mutability of self.
    fn inc_number (&mut self) {
        self.number += 1;
    }
    // There are three different types of 'self'
    fn destructor (self) {
        println!("Destructing {}", self.text);
    }
}

因此,下面两种方式是等价的:

obj.get_number();
MyStruct::get_number(&obj);

作为对比,其它语言一般把 self(或者 this)隐式包含。这些语言中一个对象或者结构体关联的函数,隐含的第一个参数就是 self。以上说明我们有 4 种可选的 self:不可变引用、可变引用、拥有所有权、不在参数中使用 self

因此,self 暗示着函数执行时的一系列上下文。它在 Rust 里面是显式的,但是在其它语言中一般是隐式的。

在这个帖子里面我们将会使用下面几个函数:

fn is_fn <A, R>(_x: fn(A) -> R) {}
fn is_Fn <A, R, F: Fn(A) -> R> (_x: &F) {}
fn is_FnMut <A, R, F: FnMut(A) -> R> (_x: &F) {}
fn is_FnOnce <A, R, F: FnOnce(A) -> R> (_x: &F) {}

这几个函数仅仅用来做类型检查。比如,如果 is_FnMut(&fnuc) 可以编译,那么我们就知道 func 属于 FnMut trait。

不带上下文和 fn 类型

在这种思路下,考虑一些使用上面的 MyStruct 结构体的闭包的例子:

let obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
let closure1 = |x: &MyStruct| x.get_number() + 3;
assert_eq!(closure1(&obj1), 18);
assert_eq!(closure1(&obj2), 13);

这是一个尽可能简单的例子。这个闭包接受任何 MyStruct 类型的对象,返回 3 加上这个对象里面的数的和。这个闭包在任何地方都能正确执行,编译也会很正常。我们能很轻松地将 closure1 改写成这样:

// It doesn't matter what code appears here, the function will behave
// exactly the same.
fn func1 (x: &MyStruct) -> u32 {
    x.get_number() + 3
}
assert_eq!(func1(&obj1), 18);
assert_eq!(func1(&obj2), 13);

这个函数不依赖它的上下文。它的前面和后面发生什么都不会影响到它。我们几乎能互换地使用 func1closure1

当一个闭包一点也不依赖它的上下文时,这个闭包的类型是 fn

// compiles successfully.
is_fn(closure1);
is_Fn(&closure1);
is_FnMut(&closure1);
is_FnOnce(&closure1);

不可变的上下文和 Fn trait

和上面相比,我们接下来在闭包中添加一个上下文。

let obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
// obj1 is borrowed by the closure immutably.
let closure2 = |x: &MyStruct| x.get_number() + obj1.get_number();
assert_eq!(closure2(&obj2), 25);
// We can borrow obj1 again immutably...
assert_eq!(obj1.get_number(), 15);
// But we can't borrow it mutably.
// obj1.inc_number();               // ERROR

closure2 依赖变量 obj1 并且包含了它周围作用域里面的信息。在这个例子里,closure2 将会借用 obj1 使之能在函数体里面使用。我们仍然能不可变地借用 obj1,但是如果我们企图可变地借用 obj1,那就会遇到一个借用错误。

如果我们尝试使用 fn 语法重写这个闭包,函数体里面需要的所用东西都必须通过形参传进去,所以我们添加一个额外的形参来表示函数的上下文:

struct Context<'a>(&'a MyStruct);
let obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
let ctx = Context(&obj1);
fn func2 (context: &Context, x: &MyStruct) -> u32 {
    x.get_number() + context.0.get_number()
}

这样它的行为和我们的闭包几乎一致:

assert_eq!(func2(&ctx, &obj2), 25);
// We can borrow obj1 again immutably...
assert_eq!(obj1.get_number(), 15);
// But we can't borrow it mutably.
// obj1.inc_number(); // ERROR

注意这个 Context 结构体包含一个 MyStruct 结构体的不可变引用,暗示着我们不能在函数里面修改它。

当我们调用 closure2(原文为 clousre1 ,疑为笔误)时,意味着我们把周围的上下文通过参数传给了闭包,就像我们必须在 fn 中做的那样。像其它一些我们不需要显式把 self 作为参数传递的语言那样,Rust 不需要我们显式把上下文作为一个参数传递给闭包。

当一个闭包接受一个不可变引用作为上下文,我们说它实现了 Fn trait。这告诉我们可以调用它多次而不会修改上下文:

// Does not compile:
// is_fn(closure2);
// Compiles successfully:
is_Fn(&closure2);
is_FnMut(&closure2);
is_FnOnce(&closure2);

可变上下文和 FnMut trait

如果我们在闭包里面修改 obj1,那就是另一结果了:

let mut obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
// obj1 is borrowed by the closure mutably.
let mut closure3 = |x: &MyStruct| {
    obj1.inc_number();
    x.get_number() + obj1.get_number()
};
assert_eq!(closure3(&obj2), 26);
assert_eq!(closure3(&obj2), 27);
assert_eq!(closure3(&obj2), 28);
// We can't borrow obj1 mutably or immutably
// assert_eq!(obj1.get_number(), 18);   // ERROR
// obj1.inc_number();                   // ERROR

这次我们不能再可变或不可变地借用 obj1。我们也必须把闭包标记为 mut。如果我们希望用 fn 语法重写它,我们得到下面这个:

struct Context<'a>(&'a mut MyStruct);
let mut obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
let mut ctx = Context(&mut obj1);
// obj1 is borrowed by the closure mutably.
fn func3 (context: &mut Context, x: &MyStruct) -> u32 {
    context.0.inc_number();
    x.get_number() + context.0.get_number()
};

它的行为和 closure3 一样:

assert_eq!(func3(&mut ctx, &obj2), 26);
assert_eq!(func3(&mut ctx, &obj2), 27);
assert_eq!(func3(&mut ctx, &obj2), 28);
// We can't borrow obj1 mutably or immutably
// assert_eq!(obj1.get_number(), 18);       // ERROR
// obj1.inc_number();                       // ERROR

注意我们必须把它的上下文作为可变引用传递进去。这暗示着我们每次调用这个函数时会得到不同的结果。

当一个闭包接受可变引用作为它的上下文时,我们说它属于 FnMut trait:

// Does not compile:
// is_fn(closure3);
// is_Fn(&closure3);
// Compiles successfully:
is_FnMut(&closure3);
is_FnOnce(&closure3);

拥有所有权的上下文

在最后一个例子中,我们将会拥有 obj1 的所有权:

let obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
// obj1 is owned by the closure
let closure4 = |x: &MyStruct| {
    obj1.destructor();
    x.get_number()
};

我们在使用 closure4 之前先检查它的类型:

// Does not compile:
// is_fn(closure4);
// is_Fn(&closure4);
// is_FnMut(&closure4);
// Compiles successfully:
is_FnOnce(&closure4);

现在我们可以检查它的行为:

assert_eq!(closure4(&obj2), 10);
// We can't call closure4 twice...
// assert_eq!(closure4(&obj2), 10);             //ERROR
// We can't borrow obj1 mutably or immutably
// assert_eq!(obj1.get_number(), 15);           // ERROR
// obj1.inc_number();                           // ERROR

在这个例子里面,我们只能调用它一次。一旦我们第一次调用之后,我们就销毁了 obj1,所以第二次调用时它就不存在了。使用一个已经被 move 的值会让 Rust 报错。这就是为什么在之前我们必须检查它的类型。

fn 写出来会是下面这样:

struct Context(MyStruct);
let obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
let ctx = Context(obj1);
// obj1 is owned by the closure
fn func4 (context: Context, x: &MyStruct) -> u32 {
    context.0.destructor();
    x.get_number()
};

它正如我们期望的那样,和闭包的行为表现一致:

assert_eq!(func4(ctx, &obj2), 10);
// We can't call func4 twice...
// assert_eq!(func4(ctx, &obj2), 10);             //ERROR
// We can't borrow obj1 mutably or immutably
// assert_eq!(obj1.get_number(), 15);           // ERROR
// obj1.inc_number();                           // ERROR

当我们用 fn 重写这个闭包时,我们必须使用一个拥有它自己所有权的 Context 结构体。当一个闭包拥有上下文的所有权,我们说它实现了 FnOnce。我们只能调用它一次,因为在这之后,上下文就被销毁了。

结论