匿名函数
Rust
的 闭包(closures)
是可以保存在一个变量中或作为参数传递给其他函数的匿名函数。可以在一个地方创建闭包,然后在不同的上下文中执行闭包运算。不同于函数,闭包允许捕获被定义时所在作用域中的值。
fn main() {
let add = |x: u32, y: u32| -> u32 { x + y };
}
可以从示例上看出,闭包的语法跟函数很像。三个部分如下:
|参数1, 参数2, ...|
->返回值类型
{执行体}
函数与闭包还有更多区别。闭包并不总是要求像 fn
函数那样在参数和返回值上注明类型。函数中需要类型注解是因为他们是暴露给用户的显式接口的一部分。严格定义这些接口对保证所有人都对函数使用和返回值的类型理解一致是很重要的。与此相比,闭包并不用于这样暴露在外的接口:他们储存在变量中并被使用,不用命名他们或暴露给库的用户调用。
闭包通常很短,并只关联于小范围的上下文而非任意情境。在这些有限制的上下文中,编译器能可靠地推断参数和返回值的类型,类似于它是如何能够推断大部分变量的类型一样(同时也有编译器需要闭包类型注解的罕见情况)。
有了类型注解闭包的语法就更类似函数了。如下是一个对其参数加一的函数的定义与拥有相同行为闭包语法的纵向对比。这里增加了一些空格来对齐相应部分。这展示了除了使用竖线以及一些可选语法外,闭包语法与函数语法有多么地相似:
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
这些都是有效的闭包定义,并在调用时产生相同的行为。调用闭包是 add_one_v3
和 add_one_v4
能够编译的必要条件,因为类型将从其用法中推断出来。这类似于 let v = Vec::new();
,Rust
需要类型注解或是某种类型的值被插入到 Vec
才能推断其类型。
编译器会为闭包定义中的每个参数和返回值推断一个具体类型:
fn main() {
let a = |x| x;
a("aa".to_string());
a(1);
}
注意这个定义没有增加任何类型注解,所以我们可以用任意类型来调用这个闭包。但是如果尝试调用闭包两次,第一次使用 String
类型作为参数而第二次使用 u32
,则会得到一个错误:
$ cargo run
Compiling closure v0.1.0 (/rsut/closure)
error[E0308]: mismatched types
--> src/main.rs:4:7
|
4 | a(1);
| - ^- help: try using a conversion method: `.to_string()`
| | |
| | expected struct `String`, found integer
| arguments to this function are incorrect
|
note: closure parameter defined here
--> src/main.rs:2:14
|
2 | let a = |x| x;
| ^
For more information about this error, try `rustc --explain E0308`.
error: could not compile `closure` due to previous error
第一次使用 String
值调用 a
时,编译器推断这个闭包中 x
的类型以及返回值的类型是 String
。接着这些类型被锁定进闭包a
中,如果尝试对同一闭包使用不同类型则就会得到类型错误。
闭包可以通过三种方式捕获其环境,它们直接对应到函数获取参数的三种方式:不可变借用,可变借用和获取所有权。闭包会根据函数体中如何使用被捕获的值决定用哪种方式捕获。定义一个捕获名为 list
的 vector
的不可变引用的闭包,因为只需不可变引用就能打印其值:
fn main() {
let num = 5;
let closure = |a| println!("a={}, num={}", a, num);
println!("调用闭包之前 {}", num);
closure(10);
println!("调用闭包之前 {}", num);
}
这个示例也展示了变量可以绑定一个闭包定义,并且之后可以使用变量名和括号来调用闭包,就像变量名是函数名一样。
因为同时可以有多个num
的不可变引用,所以在闭包定义之前,闭包定义之后调用之前,闭包调用之后代码仍然可以访问num
。代码可以编译、运行并打印:
调用闭包之前 5
a=10, num=5
调用闭包之前 5
我们修改闭包体让它向调用闭包后修改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
。但是,事情却和想像中的不同,这段代码并不能通过编译:
$ cargo run
Compiling closure v0.1.0 (/rsut/closure)
warning: value assigned to `num` is never read
--> src/main.rs:6:5
|
6 | num = 10;
| ^^^
|
= help: maybe it is overwritten before being read?
= note: `#[warn(unused_assignments)]` on by default
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
For more information about this error, try `rustc --explain E0506`.
warning: `closure` (bin "closure") generated 1 warning
error: could not compile `closure` due to previous error; 1 warning emitted
编译器说闭包已经借用了num
,取得了num
的所有权,外部不能再改变它了。原因三借用的所有权没有归还:
fn main() {
let mut list = vec![1, 2, 3];
let mut borrows_mutably = |l| list.push(l);
borrows_mutably(4);
borrows_mutably(6);
list.push(5);
}
虽然这段代码在逻辑上和这一段代码不同了,但它确实通过了编译。原来闭包并不像函数那样,调用完了就完了,因为闭包绑定到了变量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
可以被执行,再使用第二个闭包(虽然两个一模一样),第二个闭包再捕获的变量就是改变以后的了,可以到达想像中的效果。
上面说过闭包捕获变量有三种方式,是能过三个特性实现的:
&T
类型,闭包按照Fn trait
方式执行,闭包可以重复多次执行&mut T
类型,闭包按照FnMut trait
方式执行,闭包可以重复多次执行move
进闭包,闭包按照FnOnce trait
方式执行,闭包只能执行一次这三种方式是编译器根据闭包中的行为自动选择的,我们并不能指定它。此外,还可以在闭包前添加move
关键字,告诉编译器复制一份变量到闭包中,这样,闭包就不再借用外部变量了:
fn main() {
let mut num = 5;
let closure = move |a| println!("a={}, num={}", a, num);
closure(10);
num = 10;
closure(10);
println!("num={}", num);
}
此时,闭包中的num
与外部的num
不再是同一个变量,外部对变量的修改也不再对闭包产生影响。这次我们没有获取到外部的num
的可变借用,我们实际上是把 num move
进了闭包。因为 num
具有Copy
属性,因此发生 move
之后,以前的变量生命周期并未结束,还可以继续在 println!
中使用。我们打印的变量和闭包内的变量是独立的两个变量。如果我们捕获的环境变量不是 Copy
的,那么外部环境变量被 move
进闭包后, 它就不能继续在原先的函数中使用了,只能在闭包内使用。
上面例子的闭包被保存在变量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
参数指定的不是参数的类型,而是指定参数实现的特性。可以进一步使用泛型来作为函数参数,它同样支持传递闭包:
fn main() {
let closure = |a| println!("a = {}", a);
call_closure(closure);
}
fn call_closure<F>(closure: F)
where
F: Fn(i32),
{
closure(10);
}
与直接调用不同,使用了泛型以后,我们就有了选择特性的手段:特性绑定。通过特性绑定,我们可以需要自己决定使用哪种变量捕获方式。此时,就需要我们明确的知道自己写的闭包到底是通过Fn、FnMut还是FnOnce的方式。这不是可以随意选择的,必须和创建闭包时编译器选择的一样。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!