Skip to content

22.4 闭包与泛型

我们已经知道,闭包是依靠 trait 来实现的。跟普通 trait 一样,我们不能直接用 Fn FnMut FnOnce 作为变量类型、函数参数、函数返回值。

跟其他的 trait 相比,闭包相关的 trait 语法上有特殊之处。比如,如果我们想让闭包作为一个参数传递到函数中,可以这样写:


rust
fn call_with_closure<F>(some_closure: F) -> i32
        where F : Fn(i32) -> i32 {
        some_closure(1)
}

fn main() {
    let answer = call_with_closure(|x| x + 2);
    println!("{}", answer);
}

其中泛型参数 F 的约束条件是F:Fn(i32) -> i32。这里Fn(i32) -> i32是针对闭包设计的专门的语法,而不是像普通 trait 那样使用Fn<i32,i32>来写。这样设计为了让它们看起来跟普通函数类型fn(i32) -> i32更相似。除了语法之外,Fn FnMut FnOnce 其他方面都跟普通的泛型一致。

一定要注意的是:每个闭包,编译器都会为它生成一个匿名结构体类型;即使两个闭包的参数和返回值一致,它们也是完全不同的两个类型,只是都实现了同一个 trait 而已。下面我们用一个示例演示:


rust
fn main() {
// 同一个变量绑定了两次
    let mut closure = |x : i32| -> i32 { x + 2 };
    closure = |x: i32| -> i32 { x - 2 } ;
    println!("{}", closure());
}

编译,结果出错,错误信息为:


rust
error: mismatched types:
expected `[closure@temp.rs:3:21: 3:47]`,
    found `[closure@temp.rs:4:13: 4:38]`
(expected closure,
    found a different closure) [E0308]

可以看到,我们用同一个变量来绑定两个闭包的时候发生了类型错误。请大家牢牢记住,不同的闭包是不同的类型。

既然如此,跟普通的 trait 一样,如果我们需要向函数中传递闭包,有下面两种方式。

  • 通过泛型的方式。这种方式会为不同的闭包参数类型生成不同版本的函数,实现静态分派。

  • 通过 trait object 的方式。这种方式会将闭包装箱进入堆内存中,向函数传递一个胖指针,实现运行期动态分派。

关于动态分派和静态分派的内容,将在下一章中详细说明。此处只做一个简单示例:


rust
fn static_dispatch<F>(closure: &F)  // 这里是泛型参数。对于每个不同类型的参数,编译器将会生成不同版本的函数
    where F: Fn(i32) -> i32
{
    println!("static dispatch {}", closure(42));
}

fn dynamic_dispatch(closure: &Fn(i32)->i32) // 这里是 `trait object``Box<Fn(i32)->i32>`也算`trait object`。
{
    println!("dynamic dispatch {}", closure(42));
}

fn main() {
    let closure1 = | x | x * 2;
    let closure2 = | x | x * 3;
    fn function_ptr(x: i32)->i32 { x * 4 };

    static_dispatch(&closure1);
    static_dispatch(&closure2);
    static_dispatch(&function_ptr); // 普通`fn`函数也实现了`Fn trait`,它可以与此参数类型匹配。`fn`不可以捕获外部变量

    dynamic_dispatch(&closure1);
    dynamic_dispatch(&closure2);
    dynamic_dispatch(&function_ptr);
}

如果我们希望一个闭包作为函数的返回值,那么就不能使用泛型的方式了。因为如果泛型类型不在参数中出现,而仅在返回类型中出现的话,会要求在调用的时候显式指定类型,编译器才能完成类型推导。可是调用方根本无法指定具体类型,因为闭包类型是匿名类型,用户无法显式指定。所以下面这样的写法是编译不过的:


rust
fn test<F>() -> F
    where F: Fn(i32)->i32
{
    return | i | i * 2;
}

fn main() {
    let closure = test();
}

修复这段代码有两种方案,一种是静态分派,一种是动态分派。

  • 静态分派。我们可以用一种新的语法fn test() -> impl Fn(i32)->i32来实现。在后面的章节中有这个语法糖的详细介绍。

  • 动态分派。就是把闭包装箱进入堆内存中,使用Box<dyn Fn(i32)->i32>这种 trait object 类型返回。关于 trait object 的内容可参见下一章。


rust
fn test() -> Box<dyn Fn(i32)->i32 >
{
    let c = | i: i32 | i * 2;
    Box::new(c)
}

fn main() {
    let closure = test();
    let r = closure(2);
    println!("{}", r);
}

Released under the MIT License