• 闭包作为参数和返回值
    • 闭包作为参数(Taking closures as arguments)
    • 函数指针和闭包
    • 返回闭包(Returning closures)
      • 这部分引用自 The Rust Programming Language中文版

    闭包作为参数和返回值

    闭包作为参数(Taking closures as arguments)

    现在我们知道了闭包是 trait,我们已经知道了如何接受和返回闭包;就像任何其它的 trait!

    这也意味着我们也可以选择静态或动态分发。首先,让我们写一个获取可调用结构的函数,调用它,然后返回结果:

    1. fn call_with_one<F>(some_closure: F) -> i32
    2. where F : Fn(i32) -> i32 {
    3. some_closure(1)
    4. }
    5. let answer = call_with_one(|x| x + 2);
    6. assert_eq!(3, answer);

    我们传递我们的闭包,|x| x + 2,给call_with_one。它正做了我们说的:它调用了闭包,1作为参数。

    让我们更深层的解析call_with_one的签名:

    1. fn call_with_one<F>(some_closure: F) -> i32
    2. # where F : Fn(i32) -> i32 {
    3. # some_closure(1) }

    我们获取一个参数,而它有类型F。我们也返回一个i32。这一部分并不有趣。下一部分是:

    1. # fn call_with_one<F>(some_closure: F) -> i32
    2. where F : Fn(i32) -> i32 {
    3. # some_closure(1) }

    因为Fn是一个trait,我们可以用它限制我们的泛型。在这个例子中,我们的闭包取得一个i32作为参数并返回i32,所以我们用泛型限制是Fn(i32) -> i32

    还有一个关键点在于:因为我们用一个trait限制泛型,它会是单态的,并且因此,我们在闭包中使用静态分发。这是非常简单的。在很多语言中,闭包固定在堆上分配,所以总是进行动态分发。在Rust中,我们可以在栈上分配我们闭包的环境,并静态分发调用。这经常发生在迭代器和它们的适配器上,它们经常取得闭包作为参数。

    当然,如果我们想要动态分发,我们也可以做到。trait对象处理这种情况,通常:

    1. fn call_with_one(some_closure: &Fn(i32) -> i32) -> i32 {
    2. some_closure(1)
    3. }
    4. let answer = call_with_one(&|x| x + 2);
    5. assert_eq!(3, answer);

    现在我们取得一个trait对象,一个&Fn。并且当我们将我们的闭包传递给call_with_one时我们必须获取一个引用,所以我们使用&||

    函数指针和闭包

    一个函数指针有点像一个没有环境的闭包。因此,你可以传递一个函数指针给任何函数除了作为闭包参数,下面的代码可以工作:

    1. fn call_with_one(some_closure: &Fn(i32) -> i32) -> i32 {
    2. some_closure(1)
    3. }
    4. fn add_one(i: i32) -> i32 {
    5. i + 1
    6. }
    7. let f = add_one;
    8. let answer = call_with_one(&f);
    9. assert_eq!(2, answer);

    在这个例子中,我们并不是严格的需要这个中间变量f,函数的名字就可以了:

    1. let answer = call_with_one(&add_one);

    返回闭包(Returning closures)

    对于函数式风格代码来说在各种情况返回闭包是非常常见的。如果你尝试返回一个闭包,你可能会得到一个错误。在刚接触的时候,这看起来有点奇怪,不过我们会搞清楚。当你尝试从函数返回一个闭包的时候,你可能会写出类似这样的代码:

    1. fn factory() -> (Fn(i32) -> i32) {
    2. let num = 5;
    3. |x| x + num
    4. }
    5. let f = factory();
    6. let answer = f(1);
    7. assert_eq!(6, answer);

    编译的时候会给出这一长串相关错误:

    1. error: the trait `core::marker::Sized` is not implemented for the type
    2. `core::ops::Fn(i32) -> i32` [E0277]
    3. fn factory() -> (Fn(i32) -> i32) {
    4. ^~~~~~~~~~~~~~~~
    5. note: `core::ops::Fn(i32) -> i32` does not have a constant size known at compile-time
    6. fn factory() -> (Fn(i32) -> i32) {
    7. ^~~~~~~~~~~~~~~~
    8. error: the trait `core::marker::Sized` is not implemented for the type `core::ops::Fn(i32) -> i32` [E0277]
    9. let f = factory();
    10. ^
    11. note: `core::ops::Fn(i32) -> i32` does not have a constant size known at compile-time
    12. let f = factory();
    13. ^

    为了从函数返回一些东西,Rust 需要知道返回类型的大小。不过Fn是一个 trait,它可以是各种大小(size)的任何东西。比如说,返回值可以是实现了Fn的任意类型。一个简单的解决方法是:返回一个引用。因为引用的大小(size)是固定的,因此返回值的大小就固定了。因此我们可以这样写:

    1. fn factory() -> &(Fn(i32) -> i32) {
    2. let num = 5;
    3. |x| x + num
    4. }
    5. let f = factory();
    6. let answer = f(1);
    7. assert_eq!(6, answer);

    不过这样会出现另外一个错误:

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

    对。因为我们有一个引用,我们需要给它一个生命周期。不过我们的factory()函数不接收参数,所以省略不能用在这。我们可以使用什么生命周期呢?'static

    1. fn factory() -> &'static (Fn(i32) -> i32) {
    2. let num = 5;
    3. |x| x + num
    4. }
    5. let f = factory();
    6. let answer = f(1);
    7. assert_eq!(6, answer);

    不过这样又会出现另一个错误:

    1. error: mismatched types:
    2. expected `&'static core::ops::Fn(i32) -> i32`,
    3. found `[closure@<anon>:7:9: 7:20]`
    4. (expected &-ptr,
    5. found closure) [E0308]
    6. |x| x + num
    7. ^~~~~~~~~~~

    这个错误让我们知道我们并没有返回一个&'static Fn(i32) -> i32,而是返回了一个[closure <anon>:7:9: 7:20]。等等,什么?

    因为每个闭包生成了它自己的环境struct并实现了Fn和其它一些东西,这些类型是匿名的。它们只在这个闭包中存在。所以Rust把它们显示为closure <anon>,而不是一些自动生成的名字。

    这个错误也指出了返回值类型期望是一个引用,不过我们尝试返回的不是。更进一步,我们并不能直接给一个对象'static声明周期。所以我们换一个方法并通过Box装箱Fn来返回一个 trait 对象。这个几乎可以成功运行:

    1. fn factory() -> Box<Fn(i32) -> i32> {
    2. let num = 5;
    3. Box::new(|x| x + num)
    4. }
    5. # fn main() {
    6. let f = factory();
    7. let answer = f(1);
    8. assert_eq!(6, answer);
    9. # }

    这还有最后一个问题:

    1. error: closure may outlive the current function, but it borrows `num`,
    2. which is owned by the current function [E0373]
    3. Box::new(|x| x + num)
    4. ^~~~~~~~~~~

    好吧,正如我们上面讨论的,闭包借用他们的环境。而且在这个例子中。我们的环境基于一个栈分配的5num变量绑定。所以这个借用有这个栈帧的生命周期。所以如果我们返回了这个闭包,这个函数调用将会结束,栈帧也将消失,那么我们的闭包指向了被释放的内存环境!再有最后一个修改,我们就可以让它运行了:

    1. fn factory() -> Box<Fn(i32) -> i32> {
    2. let num = 5;
    3. Box::new(move |x| x + num)
    4. }
    5. # fn main() {
    6. let f = factory();
    7. let answer = f(1);
    8. assert_eq!(6, answer);
    9. # }

    通过把内部闭包添加move关键字,我们强制闭包使用 move 的方式捕获环境变量。因为这里的 num 类型是 i32,实际上这里的 move 执行的是 copy, 这样一来,闭包就不再拥有指向环境的指针,而是完整拥有了被捕获的变量。并允许它离开我们的栈帧。

    这部分引用自The Rust Programming Language中文版