先来看一下闭包的语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
// 参数在`||`里面
let l1 = |x| x + 1; // 如果只有一个表达式,可以省略`{}`
let l2 = |x, y| x + y;
let l3 = || 1;
let l4 = |x: i32, y: i32| x + y;
let l5 = |x, y| -> i32 { x + y };
let l6 = |x: i32, y: i32| -> i32 { x + y }; // 如果显示指定了返回值,需要`{}`
let l7 = |x: i32, y: i32| {
println!("{}+{}", x, y);
x + y
};

println!("{}", l1(1));
println!("{}", l2(1, 2));
println!("{}", l3());
println!("{}", l4(1, 2));
println!("{}", l5(1, 2));
println!("{}", l6(1, 2));
println!("{}", l7(1, 2));
}

闭包和函数一样,可以有参数和返回值,对于参数和返回值的类型,可以由编译器根据上下文自动推导,我们也可以显示指定。

闭包捕获外部变量时,默认是获取外部变量的引用,如果在闭包内对变量进行更新,那么就会获取mutable reference,这也要求外部变量要是mutable;而如果只是进行读取,则会获取immutable reference。

除了borrow外部变量的reference,有时候我们需要closure获得外部变量的ownership,比如我们的closure需要传到子线程执行,这时候它的lifetime需要是'static的,我们可以使用move关键字,来表明我们需要让closure获取所有它捕获的所有外部变量的ownership

1
2
let a = String::from("hello, closure");
let l = move || println!("{}", a); // 变量a会move到闭包内

当我们在声明closure的时候,指定了move关键字,那么它捕获的所有外部变量都会move到匿名结构体中。

目前,还不支持声明泛型closure,如果需要泛型还是使用函数,或者使用泛型函数返回一个闭包。

rust的闭包,实际上是语法糖,它本质上是一个实现了特定trait的匿名的struct,与闭包相关的trait有这三个:

  • Fn
  • FnMut
  • FnOnce

Fn trait

1
2
3
4
5
#[lang = "fn"]
#[must_use = "closures are lazy and do nothing unless called"]
pub trait Fn<Args>: FnMut<Args> {
extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn main() {
let hello = String::from("hello, rust!");
let l = || println!("{}", hello);
call_fn(l);
call_fnmut(l);
l();
(&l)();
}

fn call_fn<T>(t: T)
where
T: Fn() + Copy,
{
t();
}

fn call_fnmut<T>(mut t: T)
where
T: FnMut() + Copy,
{
t();
}

实际上,没有捕获外部变量或者只捕获了immutable referenceclosure,会被编译成实现了Fn这个trait的匿名的struct我们可以通过实现这个trait来重载call这个运算符,当前还不支持重载call运算符。

并且这个struct还会实现Copy这个trait,因为这个匿名struct里面只有ref,是可以copy的,因此这个closure可以多次传输。

闭包本质上就是一个重载了call运算符的匿名结构体,不同的闭包,其结构体是不一样的,因此是DST,因此当需要使用闭包作为参数时,需要使用泛型或者trait object,当需要返回闭包时,可以使用impl Fn(T) -> U或者使用 trait object,比如 BoxU>

当我们使用move将闭包捕获的外部变量move到闭包内,并且不进行修改时,会实现Fn但是不会实现Copy

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let mut s=String::from("x");
let l=move ||println!("{}",s ); // 如果capture外部变量的ownership,并且没有mutate,那么实现Fn这个trait,但是没有实现Copy这个trait
l();
call_lambda(l);
// l(); // l 已经move了,无法使用了
}

fn call_lambda<T>(t:T)
where T:Fn(){
t();
}

这时候,匿名结构体拥有被捕获变量的ownership,因此无法实现Copy这个trait

FnMut trait

1
2
3
4
5
#[lang = "fn_mut"]
#[must_use = "closures are lazy and do nothing unless called"]
pub trait FnMut<Args>: FnOnce<Args> {
extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}

当一个闭包捕获了外部的mutable reference时,生成的匿名结构体会实现FnMut这个trait,并且不会实现Copy这个trait,而且,从方法call_mut的签名可以看到参数是&mut self

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let mut hello = String::from("hello, closure!");
let mut l = || {
hello.push_str("!");
println!("{}", hello);
};
call_fnmut(l);
- value moved here
l();
^ value borrowed here after move
}

fn call_fnmut<T>(mut t: T) // 这里`t`需要指定为`mut`
where
T: FnMut(),
{
t();
}

可以看到,作为参数传输的时候,会move,我们可以通过传递引用来解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

fn main() {
let mut hello = String::from("hello, rust!");
let mut l = || {
hello.push_str("!");
println!("{}", hello);
};
call_fnmut(&mut l); // 传递 mutable ref
l(); // l需要是mut的,因为参数是`&mut self`
(&mut l)();

let l = || {
hello.push_str("!");
println!("{}", hello);
};
call_fnmut(l);
}

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.

FnMutFnsupertrait,也就是实现Fn的前提是实现FnMut

同样可以使用move将变量的ownership转移到闭包内:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let mut hello = String::from("hello, closure!");
let mut l = move || {
// 如果外面hello声明不是mut的,那么在closure内我们无法对其进行修改
hello.push_str("!");
println!("{}", hello);
};
call_fnmut(&mut l);
l();
l();
// println!("{}", hello); // hello已经被move到closure内部了,这里无法使用了
}

fn call_fnmut<T>(t: &mut T)
where
T: FnMut(),
{
t();
}

对于获取了外部变量的ownership,但是没有consume外部变量的closure,也同样实现FnMut这个trait,就像上面的例子中的一样。

FnOnce trait

当一个closure获取了外部变量的ownership,并且可能consume这个变量,那么这个closure只会实现FnOnce这个trait,并且只能被调用一次:

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let mut hello = String::from("hello, closure!");
let l = move || { // hello被move到closure内
// 如果外面hello声明不是mut的,那么在closure内我们无法对其进行修改
hello.push_str("!");
println!("{}", hello);
drop(hello); // 这里消费了hello
};
l();
// l(); // 只能被调用一次
}

我们来看一下FnOnce的声明:

1
2
3
4
5
6
#[lang = "fn_once"]
#[must_use = "closures are lazy and do nothing unless called"]
pub trait FnOnce<Args> {
type Output;
extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

可以看到,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掉了,如果允许再次被执行,就和所有权系统相违背了。

这三个traitFnOnceFnMutsupertrait,而FnMutFnsupertrait

部分获取ownership

前面说到,使用move,会把闭包捕获的所有变量的ownershipmove到闭包内

但是,有时候,可能我们只需要move部分变量的ownership到闭包内,这时候,我们可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let mut hello = String::from("hello, closure!");
let s = String::from("this is a string");
let l = || {
hello = capture(hello);
hello.push_str("!");
println!("{}", hello);
println!("{}", s);
};

l();
// println!("{}", hello); // hello被move到闭包内了
println!("{}", s); // s并没有被move到闭包内
}

fn capture<T>(t: T) -> T { // 该方法会让编译器分析需要获取ownership
t
}

在上面的例子中,编译器会自动分析,需要捕获外部变量的ownership,因此hellomove到闭包内了,但是s只是获取了它的引用。

但是,这个闭包只实现了FnOnce,因为消费了hello

1
2
3
4
5
6
7
8
9
10
57 |     l();
| - value moved here
58 | l();
| ^ value used here after move
|
note: closure cannot be invoked more than once because it moves the variable `hello` out of its environment
--> src/main.rs:52:25
|
52 | hello = capture(hello);
| ^^^^^

即使我们又把hellomove回去了,编译器还是认为我们消费了该变量

closure创建的时候就捕获了变量

我们来看下面的代码:

1
2
3
4
5
6
fn main() {
let mut hello = String::from("hello, closure!");
let mut l = || hello.push_str("!");
println!("{}", hello);
l();
}

运行的时候,编译器会报错:

1
2
3
4
5
6
7
8
let mut l = || hello.push_str("!");
-- ----- first borrow occurs due to use of `hello` in closure
|
mutable borrow occurs here
println!("{}", hello);
^^^^^ immutable borrow occurs here
l();
- mutable borrow later used here

可能我们会想,我们不是还没有调用闭包吗,怎么就拿了hellomutable ref了呢???

那是因为,前面说到,闭包的本质是一个实现了特定trait的匿名结构体,因此我们在声明一个闭包的时候,就是创建了一个匿名结构体,而这个匿名结构体,拿了它捕获的外部变量hellomutable ref

一旦我们知道了闭包实现的原理,一切就变得明晰了。

FnXxx和fn的区别

FnXxxtrait,因此它们是DSTfn是类型,函数指针,它的size的编译时已知的。

fn也实现了这些trait,因此声明参数的时候,我们可以统一使用泛型加FnXxx,这样调用的时候,无论传closure还是传函数指针都是可以。

不过,比如与c语言进行交互,就需要用函数指针了。