离线下载
PDF版 ePub版

极客学院团队出品 · 更新于 2018-06-19 14:01:25

闭包

Rust 不仅命名函数,还命名匿名函数。匿名函数有一个关联的环境被称为“闭包”,因为他们关闭了一个环境。如我们将会看到的,Rust 对它们有完美的实现。

语法

闭包看起来是这样的:

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));

但我们并不需要这样做。这是为什么呢?根本上来说,这是出于人体工程学的考虑。对于文档和类型推断而言,指定已命名函数的完整类型命名是有用的,然而闭包的类型很少被记录因为他们是匿名的,而且他们不会引 error-at-a-distance 错误,这种错误能够推断命名函数的类型。

第二,语法是相似的,但有点不同。我在这里添加空格让他们看起来更接近:

ffn  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  ;

差异很小,但它们类似。

闭包和他们的环境

闭包之所以被称为闭包,是因为他们封闭了他们的环境。它看起来像这样:

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 进行一个可变的 borrow,因为闭包已经 borrow 过它了。如果我们让闭包超出范围,我们可以:

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 > 对其内容拥有所有权,因此,当我们在闭包的操作涉及到它时,我们将不得不声称对 num 的所有权。同样的,如果我们将 num 传递给一个函数,则这个函数对其拥有所有权。

移动闭包

我们可以强制我们的闭包用 move 关键字获取环境移所有权:

let num = 5;

let owns_num = move |x: i32| x + num;

现在,即使关键字是 move,变量仍然遵循正常 move语义。在这种情况下,5 实现复制,所以 owns_num 持有 num 的副本。然而区别在哪里呢?

let mut num = 5;

{ 
let mut add_num = |x: i32| num += x;

add_num(5);
}

assert_eq!(10, num);

所以在这种情况下,我们的闭包获得了一个可变 num,我们称为 add_num, 如我们所期望的,它改变了 num 的潜在值。我们还需要将 add_nu m声明为 mut,因为我们正在改变其环境。

如果我们换成一个 move 闭包,就会出现不同:

let mut num = 5;

{ 
let mut add_num = move |x: i32| num += x;

add_num(5);
}

assert_eq!(5, num);

我们只得到 5。而不是从 num 得到可变的 borrow 我们对副本拥有所有权。

另一种方式思考 mov e闭包:他们分配给闭包一个自己的堆栈帧。没有 move 一个闭包可能与创建它的堆栈帧联系到一起,而且闭包是自包含的。这意味着你不能从函数返回一个 non-move 闭包。

但在我们讨论使用和返回闭包并之前,我们应该更多的讨论闭包的实现方式。作为一种系统语言、Rust 让你能够控制代码所做的事情,而闭包是没有什么不同的。

闭包实现

Rust 的闭包实现有点不同于其他语言。对特征来说他们是非常高效的语言。你要确保阅读这一章之前已经阅读了特征这一章,以及特征对象这一章。

都明白了吗?很棒。

闭包工作的关键点有些奇怪:使用()调用一个函数,就像 foo() 是一种可重载操作符。由此,在其他的任何地方单击鼠标都能进入空间。在 Rust 语言里面,我们使用特征系统重载操作符。调用函数也不例外。我们有三个独立的过载与特征:

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;
}

你会注意到这些特征之间的差异,但很大的一个点是 self:Fn 和 &self,FnMut 和 &mut self,FnOnce 和 self。这通过常用的方法调用语法涵盖了所有三种 self。但是我们把他们分成三个特征,而不是一个。这给了我们足够的控制权去决定我们可以采用什么样的闭包。

对闭包的| | { }语法对这三个特征来说是糖衣语法。Rus t将为环境生成一个 struct, impl 适当的特征,然后使用它。

使用闭包作为参数

现在我们知道,闭包特征,我们已经知道如何接受和返回闭包:就像任何其他特征那样!

这也意味着我们可以选择静态与动态调度。首先,让我们写一个可以调用其他函数的函数,调用它,并返回结果:

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 + 2去调用 call_with_one。它只是做它所表明的事情:它调用闭包,1 作为参数。

让我们更深入地检查 call_with_one 的签名:

fn call_with_one < F >(some_closure:F)- >i32

我们需要一个参数,它的类型为 F .我们也返回一个 i32。这部分不是很有趣。下一个部分是:

where F : Fn(i32) -> i32 {

因为 Fn 是一个特征,我们可以将我们的泛型和它绑定到一起。在这种情况下,我们的闭包需要把 i32 作为参数,并返回一个 i32 所以我们使用的泛型边界是 Fn(i32) -> i32

这里还有一个关键问题:因为我们吧泛型和特征绑定到了一起,这将导致单形态,因此,我们将在闭包里面做静态调度。那是就清晰多了。在许多语言中,闭包本身就是堆分配,总是涉及到动态调度。在 Rust 语言中,我们可以用堆栈分配我们的闭包环境,和静态调度 call 语句。这种情况通常发生于迭代器和适配器,通常采用闭包作为参数。

当然,如果我们想要动态调度,我们也可以那样。特征对象处理这种情况时,像往常一样:

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);

现在我们把一个特征对象,&Fn。当我们传递这个特征到 call_with_one 时我们必须参考我们的闭包,所以我们使用 & | |

返回闭包

在各种情况下,返回闭包是很常见的函数形式。如果你想返回一个闭包,您可能会遇到一个错误。起初,这看起来可能有些奇怪,但是我们会找到答案。你可以试着从下面的函数返回一个闭包:

fn factory() -> (Fn(i32) -> Vec<i32>) {
let vec = vec![1, 2, 3];

|n| vec.push(n)
}

let f = factory();

let answer = f(4);
assert_eq!(vec![1, 2, 3, 4], answer);
error: the trait `core::marker::Sized` is not implemented for the type
`core::ops::Fn(i32) -> collections::vec::Vec<i32>` [E0277]
f = factory();
^
note: `core::ops::Fn(i32) -> collections::vec::Vec<i32>` does not have a
constant size known at compile-time
f = factory();
^
error: the trait `core::marker::Sized` is not implemented for the type
`core::ops::Fn(i32) -> collections::vec::Vec<i32>` [E0277]
factory() -> (Fn(i32) -> Vec<i32>) {
 ^~~~~~~~~~~~~~~~~~~~~
note: `core::ops::Fn(i32) -> collections::vec::Vec<i32>` does not have a constant size known at compile-time
fa ctory() -> (Fn(i32) -> Vec<i32>) {
  ^~~~~~~~~~~~~~~~~~~~~

为了从一个函数返回一些东西,Rust 需要知道返回类型的大小。但由于 Fn 是一个特征,它可能具有是各种不同大小的 size : 许多不同的类型都可以实现 Fn。来给一些事物赋予 siz e的一种简单的方法是参考已知的 size。所以如下面我们写的这样:

fn factory() -> &(Fn(i32) -> Vec<i32>) {
let vec = vec![1, 2, 3];

|n| vec.push(n)
}

let f = factory();

let answer = f(4);
assert_eq!(vec![1, 2, 3, 4], answer);

但我们得到了另一个错误:

error: missing lifetime specifier [E0106]
fn factory() -> &(Fn(i32) -> i32) {
^~~~~~~~~~~~~~~~~

正确的。因为我们有一个参考,我们需要给它指定生命周期。但是我们的 factory() 函数不带参数,所以此处省略不写。我们可以选择什么样的生命周期呢?静态:

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]。等等,这是什么?

因为每个闭包生成自己的环境结构和实现 Fn 特征以及 friends 这些都是匿名的。它们的存在只是因为这个闭包。Rust 把他们显示为 closure <anon>而不是一些自动生成的名字。

但是为什么我们的闭包不实现 &'static Fn?正如我们之前讨论的,闭包 borrow 了他们的环境。在这种情况下,我们的环境是基于 5 栈分配以及num 变量绑定。因此,borrow 的生命周期为的堆栈帧长度。所以如果我们返回这个闭包,函数调用将结束,堆栈框架将消失,并且我们的闭包将对垃圾内存的环境进行捕获!

那么该怎么办?这就是工作原理:

fn factory() -> Box<Fn(i32) -> i32> {
let num = 5;

Box::new(|x| x + num)
}
let f = factory();

let answer = f(1);
assert_eq!(6, answer);

我们使用一个特征对象,通过建立 Fn。只有最后一个问题:

error: `num` does not live long enough
Box::new(|x| x + num)
 ^~~~~~~~~~~

我们仍然有一个对父堆栈帧的引用。通过最后一次修复,我们这样做可以做可以行得通:

fn factory() -> Box<Fn(i32) -> i32> {
let num = 5;

Box::new(move |x| x + num)
}
let f = factory();

let answer = f(1);
assert_eq!(6, answer);

通过内部 move Fn,我们可以为我们的闭包创建一个新的堆栈帧。我们给它一个已知大小,并对它进行填充,而且允许他脱离我们的堆栈帧。