闭包与匿名函数
闭包与匿名函数
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是 引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
在 Rust 中,函数和闭包都是实现了 Fn
、FnMut
或 FnOnce
特质(trait)的类型。任何实现了这三种特质其中一种的类型的对象,都是 可调用对象,都能像函数和闭包一样通过这样name()
的形式调用,()
在 rust 中是一个操作符,操作符在 rust 中是可以重载的。rust 的操作符重载是通过实现相应的trait
来实现,而 ()
操作符的相应trait
就是 Fn
、FnMut
和 FnOnce
,所以,任何实现了这三个trait
中的一种的类型,其实就是重载了()
操作符。
Rust 使用闭包 (closure) 来创建匿名函数:
let num = 5;
let plus_num = |x: i32| x + num;
其中闭包 plus_num
借用了它作用域中的 let
绑定 num
。如果要让闭包获得所有权,可以使用 move
关键字:
let mut num = 5;
{
let mut add_num = move |x: i32| num += x; // 闭包通过move获取了num的所有权
add_num(5);
}
// 下面的num在被move之后还能继续使用是因为其实现了Copy特性
assert_eq!(5, num);
基本语法
闭包看起来像这样:
let plus_one = |x: i32| x + 1;
assert_eq!(2, plus_one(1));
我们创建了一个绑定,plus_one
,并把它赋予一个闭包。闭包的参数位于管道(|
)之中,而闭包体是一个表达式,在这个例子中,x + 1
。记住{}
是一个表达式,所以我们也可以拥有包含多行的闭包:
let plus_two = |x| {
let mut result: i32 = x;
result += 1;
result += 1;
result
};
assert_eq!(4, plus_two(2));
你会注意到闭包的一些方面与用fn
定义的常规函数有点不同。第一个是我们并不需要标明闭包接收和返回参数的类型。我们可以:
let plus_one = |x: i32| -> i32 { x + 1 };
assert_eq!(2, plus_one(1));
不过我们并不需要这么写。为什么呢?基本上,这是出于“人体工程学”的原因。因为为命名函数指定全部类型有助于像文档和类型推断,而闭包的类型则很少有文档因为它们是匿名的,并且并不会产生像推断一个命名函数的类型这样的“远距离错误”。
第二个的语法大同小异。我会增加空格来使它们看起来更像一点:
fn plus_one_v1 (x: i32) -> i32 { x + 1 }
let plus_one_v2 = |x: i32| -> i32 { x + 1 };
let plus_one_v3 = |x: i32| x + 1 ;
捕获变量
之所以把它称为“闭包”是因为它们“包含在环境中”(close over their environment)。这看起来像:
let num = 5;
let plus_num = |x: i32| x + num;
assert_eq!(10, plus_num(5));
这个闭包,plus_num
,引用了它作用域中的let
绑定:num
。更明确的说,它借用了绑定。如果我们做一些会与这个绑定冲突的事,我们会得到一个错误。比如这个:
let mut num = 5;
let plus_num = |x: i32| x + num;
let y = &mut num;
错误是:
error: cannot borrow `num` as mutable because it is also borrowed as immutable
let y = &mut num;
^~~
note: previous borrow of `num` occurs here due to use in closure; the immutable
borrow prevents subsequent moves or mutable borrows of `num` until the borrow
ends
let plus_num = |x| x + num;
^~~~~~~~~~~
note: previous borrow ends here
fn main() {
let mut num = 5;
let plus_num = |x| x + num;
let y = &mut num;
}
^
一个啰嗦但有用的错误信息!如它所说,我们不能取得一个num
的可变借用因为闭包已经借用了它。如果我们让闭包离开作用域,我们可以:
let mut num = 5;
{
let plus_num = |x: i32| x + num;
} // plus_num goes out of scope, borrow of num ends
let y = &mut num;
如果你的闭包需要它,Rust 会取得所有权并移动环境:
let nums = vec![1, 2, 3];
let takes_nums = || nums;
println!("{:?}", nums);
这会给我们:
note: `nums` moved into closure environment here because it has type
`[closure(()) -> collections::vec::Vec<i32>]`, which is non-copyable
let takes_nums = || nums;
^~~~~~~
Vec<T>
拥有它内容的所有权,而且由于这个原因,当我们在闭包中引用它时,我们必须取得nums
的所有权。这与我们传递nums
给一个取得它所有权的函数一样。
move 闭包
我们可以使用move
关键字强制使我们的闭包取得它环境的所有权:
let num = 5;
let owns_num = move |x: i32| x + num;
现在,即便关键字是move
,变量遵循正常的移动语义。在这个例子中,5
实现了Copy
,所以owns_num
取得一个5
的拷贝的所有权。那么区别是什么呢?
let mut num = 5;
{
let mut add_num = |x: i32| num += x;
add_num(5);
}
assert_eq!(10, num);
那么在这个例子中,我们的闭包取得了一个num
的可变引用,然后接着我们调用了add_num
,它改变了其中的值,正如我们期望的。我们也需要将add_num
声明为mut
,因为我们会改变它的环境。
如果我们加上move
修饰闭包,会发生些不同:
let mut num = 5;
{
let mut add_num = move |x: i32| num += x;
add_num(5);
}
assert_eq!(5, num);
我们只会得到5
。这次我们没有获取到外部的num
的可变借用,我们实际上是把 num
move 进了闭包。因为 num
具有 Copy 属性,因此发生 move 之后,以前的变量生命周期并未结束,还可以继续在 assert_eq!
中使用。我们打印的变量和闭包内的变量是独立的两个变量。如果我们捕获的环境变量不是 Copy 的,那么外部环境变量被 move 进闭包后,
它就不能继续在原先的函数中使用了,只能在闭包内使用。
不过在我们讨论获取或返回闭包之前,我们应该更多的了解一下闭包实现的方法。作为一个系统语言,Rust 给予你了大量的控制你代码的能力,而闭包也是一样。
闭包作为参数和返回值
闭包作为参数(Taking closures as arguments)
现在我们知道了闭包是 trait,我们已经知道了如何接受和返回闭包;就像任何其它的 trait!这也意味着我们也可以选择静态或动态分发。首先,让我们写一个获取可调用结构的函数,调用它,然后返回结果:
fn call_with_one<F>(some_closure: F) -> i32
where F : Fn(i32) -> i32 {
some_closure(1)
}
let answer = call_with_one(|x| x + 2);
assert_eq!(3, answer);
我们传递我们的闭包,|x| x + 2
,给call_with_one
。它正做了我们说的:它调用了闭包,1
作为参数。让我们更深层的解析call_with_one
的签名:
fn call_with_one<F>(some_closure: F) -> i32
# where F : Fn(i32) -> i32 {
# some_closure(1) }
我们获取一个参数,而它有类型F
。我们也返回一个i32
。这一部分并不有趣。下一部分是:
# fn call_with_one<F>(some_closure: F) -> i32
where F : Fn(i32) -> i32 {
# some_closure(1) }
因为Fn
是一个 trait,我们可以用它限制我们的泛型。在这个例子中,我们的闭包取得一个i32
作为参数并返回i32
,所以我们用泛型限制是Fn(i32) -> i32
。
还有一个关键点在于:因为我们用一个 trait 限制泛型,它会是单态的,并且因此,我们在闭包中使用静态分发。这是非常简单的。在很多语言中,闭包固定在堆上分配,所以总是进行动态分发。在 Rust 中,我们可以在栈上分配我们闭包的环境,并静态分发调用。这经常发生在迭代器和它们的适配器上,它们经常取得闭包作为参数。
当然,如果我们想要动态分发,我们也可以做到。trait 对象处理这种情况,通常:
fn call_with_one(some_closure: &Fn(i32) -> i32) -> i32 {
some_closure(1)
}
let answer = call_with_one(&|x| x + 2);
assert_eq!(3, answer);
现在我们取得一个 trait 对象,一个&Fn
。并且当我们将我们的闭包传递给call_with_one
时我们必须获取一个引用,所以我们使用&||
。
函数指针和闭包
一个函数指针有点像一个没有环境的闭包。因此,你可以传递一个函数指针给任何函数除了作为闭包参数,下面的代码可以工作:
fn call_with_one(some_closure: &Fn(i32) -> i32) -> i32 {
some_closure(1)
}
fn add_one(i: i32) -> i32 {
i + 1
}
let f = add_one;
let answer = call_with_one(&f);
assert_eq!(2, answer);
在这个例子中,我们并不是严格的需要这个中间变量f
,函数的名字就可以了:
let answer = call_with_one(&add_one);
返回闭包(Returning closures)
对于函数式风格代码来说在各种情况返回闭包是非常常见的。如果你尝试返回一个闭包,你可能会得到一个错误。在刚接触的时候,这看起来有点奇怪,不过我们会搞清楚。当你尝试从函数返回一个闭包的时候,你可能会写出类似这样的代码:
fn factory() -> (Fn(i32) -> i32) {
let num = 5;
|x| x + num
}
let f = factory();
let answer = f(1);
assert_eq!(6, answer);
编译的时候会给出这一长串相关错误:
error: the trait `core::marker::Sized` is not implemented for the type
`core::ops::Fn(i32) -> i32` [E0277]
fn factory() -> (Fn(i32) -> i32) {
^~~~~~~~~~~~~~~~
note: `core::ops::Fn(i32) -> i32` does not have a constant size known at compile-time
fn factory() -> (Fn(i32) -> i32) {
^~~~~~~~~~~~~~~~
error: the trait `core::marker::Sized` is not implemented for the type `core::ops::Fn(i32) -> i32` [E0277]
let f = factory();
^
note: `core::ops::Fn(i32) -> i32` does not have a constant size known at compile-time
let f = factory();
^
为了从函数返回一些东西,Rust 需要知道返回类型的大小。不过Fn
是一个 trait,它可以是各种大小(size)的任何东西。比如说,返回值可以是实现了Fn
的任意类型。一个简单的解决方法是:返回一个引用。因为引用的大小(size)是固定的,因此返回值的大小就固定了。因此我们可以这样写:
fn factory() -> &(Fn(i32) -> i32) {
let num = 5;
|x| x + num
}
let f = factory();
let answer = f(1);
assert_eq!(6, answer);
不过这样会出现另外一个错误:
error: missing lifetime specifier [E0106]
fn factory() -> &(Fn(i32) -> i32) {
^~~~~~~~~~~~~~~~~
对。因为我们有一个引用,我们需要给它一个生命周期。不过我们的factory()
函数不接收参数,所以省略不能用在这。我们可以使用什么生命周期呢?'static
:
fn factory() -> &'static (Fn(i32) -> i32) {
let num = 5;
|x| x + num
}
let f = factory();
let answer = f(1);
assert_eq!(6, answer);
不过这样又会出现另一个错误:
error: mismatched types:
expected `&'static core::ops::Fn(i32) -> i32`,
found `[closure@<anon>:7:9: 7:20]`
(expected &-ptr,
found closure) [E0308]
|x| x + num
^~~~~~~~~~~
这个错误让我们知道我们并没有返回一个&'static Fn(i32) -> i32
,而是返回了一个[closure <anon>:7:9: 7:20]
。等等,什么?
因为每个闭包生成了它自己的环境struct
并实现了Fn
和其它一些东西,这些类型是匿名的。它们只在这个闭包中存在。所以 Rust 把它们显示为closure <anon>
,而不是一些自动生成的名字。
这个错误也指出了返回值类型期望是一个引用,不过我们尝试返回的不是。更进一步,我们并不能直接给一个对象'static
声明周期。所以我们换一个方法并通过Box
装箱Fn
来返回一个 trait 对象。这个几乎可以成功运行:
fn factory() -> Box<Fn(i32) -> i32> {
let num = 5;
Box::new(|x| x + num)
}
# fn main() {
let f = factory();
let answer = f(1);
assert_eq!(6, answer);
# }
这还有最后一个问题:
error: closure may outlive the current function, but it borrows `num`,
which is owned by the current function [E0373]
Box::new(|x| x + num)
^~~~~~~~~~~
好吧,正如我们上面讨论的,闭包借用他们的环境。而且在这个例子中。我们的环境基于一个栈分配的 5
,num
变量绑定。所以这个借用有这个栈帧的生命周期。所以如果我们返回了这个闭包,这个函数调用将会结束,栈帧也将消失,那么我们的闭包指向了被释放的内存环境!再有最后一个修改,我们就可以让它运行了:
fn factory() -> Box<Fn(i32) -> i32> {
let num = 5;
Box::new(move |x| x + num)
}
# fn main() {
let f = factory();
let answer = f(1);
assert_eq!(6, answer);
# }
通过把内部闭包添加 move
关键字,我们强制闭包使用 move 的方式捕获环境变量。因为这里的 num 类型是 i32,实际上这里的 move 执行的是 copy, 这样一来,闭包就不再拥有指向环境的指针,而是完整拥有了被捕获的变量。并允许它离开我们的栈帧。
闭包的实现
Rust 的闭包实现与其它语言有些许不同。它们实际上是 trait 的语法糖。理解闭包底层是如何工作的关键有点奇怪:使用 ()
调用函数,像 foo()
,是一个可重载的运算符。到此,其它的一切都会明了。在 Rust 中,我们使用 trait 系统来重载运算符。调用函数也不例外。我们有三个 trait 来分别重载:
# mod foo {
pub trait Fn<Args> : FnMut<Args> {
extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}
pub trait FnMut<Args> : FnOnce<Args> {
extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}
pub trait FnOnce<Args> {
type Output;
extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}
# }
你会注意到这些 trait 之间的些许区别,不过一个大的区别是 self
:Fn
获取&self
,FnMut
获取 &mut self
,而FnOnce
获取self
。这包含了所有 3 种通过通常函数调用语法的self
。不过我们将它们分在 3 个 trait 里,而不是单独的 1 个。这给了我们大量的对于我们可以使用哪种闭包的控制。
闭包的|| {}
语法是上面 3 个 trait 的语法糖。Rust 将会为了环境创建一个结构体,impl
合适的 trait,并使用它。