先来看一下闭包的语法:
1 | fn main() { |
闭包和函数一样,可以有参数和返回值,对于参数和返回值的类型,可以由编译器根据上下文自动推导,我们也可以显示指定。
闭包捕获外部变量时,默认是获取外部变量的引用,如果在闭包内对变量进行更新,那么就会获取mutable reference,这也要求外部变量要是mutable
;而如果只是进行读取,则会获取immutable reference。
除了borrow
外部变量的reference
,有时候我们需要closure
获得外部变量的ownership
,比如我们的closure
需要传到子线程执行,这时候它的lifetime
需要是'static
的,我们可以使用move
关键字,来表明我们需要让closure
获取所有它捕获的所有外部变量的ownership
:
1 | let a = String::from("hello, closure"); |
当我们在声明closure
的时候,指定了move
关键字,那么它捕获的所有外部变量都会move
到匿名结构体中。
目前,还不支持声明泛型closure
,如果需要泛型还是使用函数,或者使用泛型函数返回一个闭包。
rust
的闭包,实际上是语法糖,它本质上是一个实现了特定trait
的匿名的struct
,与闭包相关的trait
有这三个:
Fn
FnMut
FnOnce
Fn trait
1 |
|
Instances of Fn
can be called repeatedly without mutating state.
Fn
is implemented automatically by closures which only take immutable references to captured variables or don’t capture anything at all, as well as (safe) function pointers (with some caveats, see their documentation for more details). Additionally, for any type F
that implements Fn
, &F
implements Fn
, too.
1 | fn main() { |
实际上,没有捕获外部变量或者只捕获了immutable reference
的closure
,会被编译成实现了Fn
这个trait
的匿名的struct
,我们可以通过实现这个,当前还不支持重载trait
来重载call
这个运算符call
运算符。
并且这个struct
还会实现Copy
这个trait
,因为这个匿名struct
里面只有ref
,是可以copy
的,因此这个closure
可以多次传输。
闭包本质上就是一个重载了call
运算符的匿名结构体,不同的闭包,其结构体是不一样的,因此是DST,因此当需要使用闭包作为参数时,需要使用泛型或者trait object,当需要返回闭包时,可以使用impl Fn(T) -> U
或者使用 trait object,比如 Box
当我们使用move
将闭包捕获的外部变量move
到闭包内,并且不进行修改时,会实现Fn
但是不会实现Copy
:
1 | fn main() { |
这时候,匿名结构体拥有被捕获变量的ownership
,因此无法实现Copy
这个trait
。
FnMut trait
1 |
|
当一个闭包捕获了外部的mutable reference
时,生成的匿名结构体会实现FnMut
这个trait
,并且不会实现Copy
这个trait
,而且,从方法call_mut
的签名可以看到参数是&mut self
:
1 | fn main() { |
可以看到,作为参数传输的时候,会move
,我们可以通过传递引用来解决:
1 |
|
FnMut
is implemented automatically by closures which take mutable references to captured variables, as well as all types that implement Fn
, e.g., (safe) function pointers (since FnMut
is a supertrait of Fn
). Additionally, for any type F
that implements FnMut
, &mut F
implements FnMut
, too.
FnMut
是Fn
的supertrait
,也就是实现Fn
的前提是实现FnMut
。
同样可以使用move
将变量的ownership
转移到闭包内:
1 | fn main() { |
对于获取了外部变量的ownership
,但是没有consume
外部变量的closure
,也同样实现FnMut
这个trait
,就像上面的例子中的一样。
FnOnce trait
当一个closure
获取了外部变量的ownership
,并且可能consume
这个变量,那么这个closure
只会实现FnOnce
这个trait
,并且只能被调用一次:
1 | fn main() { |
我们来看一下FnOnce
的声明:
1 |
|
可以看到,call_once
的参数是self
。
FnOnce
is implemented automatically by closure that might consume captured variables, as well as all types that implement FnMut
, e.g., (safe) function pointers (since FnOnce
is a supertrait of FnMut
).
这里的consume
指的是,在执行过程中,把获得了其ownership,已经move到匿名结构体中的变量又move
到别的地方去了,即便后面又move
回去了,编译器也会认为我们consume
这个变量。
这个时候,原来的变量已经被move
掉了,如果允许再次被执行,就和所有权系统相违背了。
这三个trait
,FnOnce
是FnMut
的supertrait
,而FnMut
是Fn
的supertrait
部分获取ownership
前面说到,使用move
,会把闭包捕获的所有变量的ownership
都move
到闭包内
但是,有时候,可能我们只需要move
部分变量的ownership
到闭包内,这时候,我们可以:
1 | fn main() { |
在上面的例子中,编译器会自动分析,需要捕获外部变量的ownership
,因此hello
被move
到闭包内了,但是s
只是获取了它的引用。
但是,这个闭包只实现了FnOnce
,因为消费了hello
:
1 | 57 | l(); |
即使我们又把hello
给move
回去了,编译器还是认为我们消费了该变量
closure创建的时候就捕获了变量
我们来看下面的代码:
1 | fn main() { |
运行的时候,编译器会报错:
1 | let mut l = || hello.push_str("!"); |
可能我们会想,我们不是还没有调用闭包吗,怎么就拿了hello
的mutable ref
了呢???
那是因为,前面说到,闭包的本质是一个实现了特定trait的匿名结构体,因此我们在声明一个闭包的时候,就是创建了一个匿名结构体,而这个匿名结构体,拿了它捕获的外部变量hello
的mutable ref
一旦我们知道了闭包实现的原理,一切就变得明晰了。
FnXxx和fn的区别
FnXxx
是trait
,因此它们是DST
,而fn
是类型,函数指针,它的size的编译时已知的。
fn
也实现了这些trait
,因此声明参数的时候,我们可以统一使用泛型加FnXxx
,这样调用的时候,无论传closure
还是传函数指针都是可以。
不过,比如与c
语言进行交互,就需要用函数指针了。