总结
-
闭包是一个函数指针(
fn
)和一个上下文(context)的组合 -
一个没有上下文的闭包仅是一个函数指针
-
一个带有不可变上下文的闭包,满足
Fn
-
一个带有可变上下文的闭包,满足
FnMut
-
一个拥有上下文所有权的闭包,满足
FnOnce
理解 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);
这个函数不依赖它的上下文。它的前面和后面发生什么都不会影响到它。我们几乎能互换地使用 func1
和 closure1
。
当一个闭包一点也不依赖它的上下文时,这个闭包的类型是 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
。我们只能调用它一次,因为在这之后,上下文就被销毁了。
结论
-
不需要上下文的函数是
fn
类型,我们能在任何地方调用它。 -
只需要不可变地访问它的上下文的函数属于
Fn
trait,能在任何上下文还存在与此的作用域里面调用。 -
需要可变地访问它的上下文的函数实现了
FnMut
trait,它能在任何上下文仍有效的地方调用,但每次调用都可能会做不同的事情。 -
拥有上下文所有权的函数只能被调用一次。