几乎每一种比C语言高级的语言都有闭包。Rust也同样支持闭包。闭包有一个很通俗的名称:匿名函数,它有着如下优点:
Rust创建闭包的语法很简单:|参数列表| -> 返回类型 {代码段}
。用两个竖线将函数参数包裹起来,后面接返回值的类型,最后是用花括号包裹的代码段就可以了。如:
fn main() {
let closure = |a: i32| -> i32 {
println!("a={}", a);
a
};
println!("closure return {}", closure(10));
}
如果没有返回值,返回类型可以被省略:
fn main() {
let closure = |a: i32| {
println!("a={}", a);
};
closure(10);
}
如果代码段只有一行,花括号也可以被省略:
fn main() {
let closure = |a: i32| println!("a = {}", a);
closure(10);
}
Rust还可以根据闭包的调用,自动推到出闭包参数的类型。如我们只写一个闭包,省略参数,而不去调用它:
fn main() {
let closure = |a| println!("a = {}", a);
}
此时,编译会报错:
error[E0282]: type annotations needed
--> src\main.rs:2:20
|
2 | let closure = |a| println!("a = {}", a);
| ^ consider giving this closure parameter a type
error: aborting due to previous error
添加上调用,Rust就可以自动推导了:
fn main() {
let closure = |a| println!("a = {}", a);
closure(10);
}
不过,这个推导只在第一次调用时进行,如果有两次调用,但参数的类型不一致:
fn main() {
let closure = |a| println!("a = {}", a);
closure(10);
closure(1.0);
}
当第一次调用时,a被推导为整数,而第二次调用时编译器已经知道a的类型,再传入了浮点数,编译器就会报错:
error[E0308]: mismatched types
--> src\main.rs:4:13
|
4 | closure(1.0);
| ^^^ expected integer, found floating-point number
error: aborting due to previous error
闭包的一个重要的特性就是它可以捕获当前环境中的变量,如
fn main() {
let num = 5;
let closure = |a| println!("a={}, num={}", a, num);
closure(10);
}
这里的闭包,不只用到了闭包的参数,还用到了一个外部的变量num。
注意,闭包捕获外部变量可不是一件简单的事情,它并不像函数传递参数那样。如下面的例子:
fn main() {
let mut num = 5;
let closure = |a| println!("a={}, num={}", a, num);
closure(10);
num = 10;
closure(10);
}
闭包中使用可变的变量num,然后闭包执行后,外部再修改num的值,然后再一次调用闭包。简单的理解这段代码,以为是第一次输出num=5,第二输出num=10。但是,事情却和想像中的不同,这段代码并不能通过编译:
error[E0506]: cannot assign to `num` because it is borrowed
--> src\main.rs:6:5
|
3 | let closure = |a| println!("a={}, num={}", a, num);
| --- borrow of `num` occurs here --- borrow occurs due to use in closure
...
6 | num = 10;
| ^^^^^^^^ assignment to borrowed `num` occurs here
7 | closure(10);
| ------- borrow later used here
error: aborting due to previous error; 1 warning emitted
编译器说闭包已经借用了num,取得了num的所有权,外部不能再改变它了。我不理解为什么借用的所有权没有归还,问群里的大佬,大佬说把第6行和第7行换一下位置就可以了:
fn main() {
let mut num = 5;
let closure = |a| println!("a={}, num={}", a, num);
closure(10);
closure(10);
num = 10;
}
虽然这段代码在逻辑上和这一段代码不同了,但它确实通过了编译。原来闭包并不像函数那样,调用完了就完了,因为闭包绑定到了变量closure,在离开closure的作用域之前,闭包一直有效,借用不会归还。第一段代码num=10后面还有对闭包的调用,所以闭包并没有被销毁,而第二段代码第二次调用闭包后,就不再使用了,因此执行完了第二次调用闭包就被销毁了,num=10才能正常执行。
为了实现第一段代码的逻辑,一个闭包是不能完成的,需要两个闭包才能实现:
fn main() {
let mut num = 5;
let closure = |a| println!("a={}, num={}", a, num);
closure(10);
num = 10;
let closure = |a| println!("a={}, num={}", a, num);
closure(10);
}
第一个闭包执行完后被销毁,num=10可以被执行,再使用第二个闭包(虽然两个一模一样),第二个闭包再捕获的变量就是改变以后的了,可以到达想像中的效果。
群里的大佬们开始讨论起来闭包捕获外部变量的高级知识,弄得我像听天书一看,于是上网查阅资料,发现一知乎大神讲得的非常全面。我在这里仅作一下总结,想了解更底层细节的可以直接去看大神的文章:https://www.jianshu.com/p/ea1b96cbf0a1。
闭包捕获变量有三种方式,是能过三个特性实现的:
&T
类型,闭包按照Fn
trait方式执行,闭包可以重复多次执行&mut T
类型,闭包按照FnMut
trait方式执行,闭包可以重复多次执行FnOnce
trait方式执行,闭包只能执行一次这三种方式是编译器根据闭包中的行为自动选择的,我们并不能指定它。此外,还可以在闭包前添加move
关键字,告诉编译器复制一份变量到闭包中,这样,闭包就不再借用外部变量了:
fn main() {
let mut num = 5;
let closure = move |a| println!("a={}, num={}", a, num);
closure(10);
num = 10;
closure(10);
}
此时,闭包中的num与外部的num不再是同一个变量,外部对变量的修改也不再对闭包产生影响。只是我不太明白,为什么是move
而不是Copy
或Clone
呢。
上面例子的闭包被保存在变量closure中,当然,它可以作为函数参数进行传递。不过,这似乎有些麻烦,比如:
fn main() {
let closure = |a| println!("a = {}", a);
call_closure(closure);
}
fn call_closure(closure) {
closure(10);
}
写到这自然会想到,call_closure函数的参数closure是什么类型呢?
事实上,闭包不属于任何类型,它的本质是:特性。要想使用闭包作为函数参数,应该这么写:
fn main() {
let closure = |a| println!("a = {}", a);
call_closure(closure);
}
fn call_closure(closure: impl Fn(i32)) {
closure(10);
}
closure参数指定的不是参数的类型,而是指定参数实现的特性。可以进一步使用泛型来作为函数参数,它同样支持传递闭包(还不懂泛型的小朋友可以参考我关于泛型的博文:《Rust的泛型》):
fn main() {
let num = 5;
let closuer = move |a| println!("a={}, num={}", a, num);
call_closuer(closuer);
}
fn call_closuer(closuer: F)
where F: Fn(i32) {
closuer(10);
}
与直接调用不同,使用了泛型以后,我们就有了选择特性的手段:特性绑定。通过特性绑定,我们可以需要自己决定使用哪种变量捕获方式。此时,就需要我们明确的知道自己写的闭包到底是通过Fn
、FnMut
还是FnOnce
的方式。这不是可以随意选择的,必须和创建闭包时编译器选择的一样,所以,还是需要牢记这三个特性的使用条件:
&T
类型,闭包按照Fn
trait方式执行,闭包可以重复多次执行&mut T
类型,闭包按照FnMut
trait方式执行,闭包可以重复多次执行FnOnce
trait方式执行,闭包只能执行一次