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