what is closure?

Rust 里的 closure 是一种匿名函数,可以保存在变量里用于日后的调用,也可以作为参数传递给函数。而且相比于函数,closure 可以在其定义域内捕获变量

closure 的类型推导

  • closure 不是泛型,因此当编译器推导出一种类型后,它就会一直使用该类型
1
2
3
4
5
6
let example_closure = |x| x;

let s = example_closure(String::from("hello"));
// example_closure 的类型为 Fn(String) -> String
let n = example_closure(5); // 但这里希望以 Fn(i32) -> i32 调用
// 报错!
Rust 闭包可以用泛型吗?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
struct Cacher<T, E>
where
T: Fn(E) -> E,
E: Clone,
{
query: T,
value: Option<E>,
}

impl<T, E> Cacher<T, E>
where
T: Fn(E) -> E,
E: Clone,
{
fn new(query: T) -> Cacher<T, E> {
Cacher { query, value: None }
}

fn value(&mut self, arg: E) -> E {
match self.value {
Some(ref v) => v.clone(),
None => {
let v = (self.query)(arg.clone());
self.value = Some(v.clone());
v.clone()
}
}
}
}

fn main() {
let mut test = Cacher::new(|d: String| d + " world");
println!("first cache: {}", test.value("wtf".to_string()));
println!("second cache: {}", test.value("hello".to_string()));
}

closure 与内存

当闭包从环境中捕获一个值时,会分配内存去存储这些值。对于有些场景来说,这种额外的内存分配会成为一种负担。与之相比,函数就不会去捕获这些环境值,因此定义和使用函数不会拥有这种内存负担。

三种闭包特征 (Trait)

闭包捕获变量有三种途径,恰好对应函数参数的三种传入方式:转移所有权、可变借用、不可变借用,因此相应的 Fn 特征也有三种:

这三种的关系并不是说,我定义了闭包类型满足 FnOnce 那么这个闭包就只能调用一次,而是会根据捕获和使用方式,自动推导闭包属于哪一种 Trait

  • 可以移动变量所有权
  • 只能调用一次(除非也实现 Copy trait)

来看这么一个例子

1
2
3
4
5
6
7
8
9
10
11
fn fn_once<F>(func: F)
where F: FnOnce(usize) -> bool,
{
println!("{}", func(3));
println!("{}", func(4)); // 报错
}

fn main() {
let x = vec![1, 2, 3];
fn_once(|z| {z == x.len()})
}

这里一个问题是,FnOnce 特征的 func 变量为什么只能被调用一次?怎么从所有权的角度进行解释?

从捕获的变量的角度来说,闭包捕获了变量的所有权,根据 Rust 的语言设计,所有权只能在一个人手里,于是第二次再调用闭包就会无法拿到所有权。

那么闭包自身的所有权转移给谁了呢?闭包 func 的所有权会转移到调用闭包的代码上下文。此时,闭包可能释放其捕获的资源(如 x),或者将这些资源的所有权转移给其他逻辑(即使没有显式转移,闭包本身的调用也意味着其自身被“销毁”

这里 func(4) 的报错其实与 fn_once(|z| z == x.len()) 这个匿名函数本身没关系,只是泛型函数自己做的类型检查。

  • 使用变量的可变借用 (&mut)
  • 闭包变量本身也需要定义为 let mut,或者作为函数参数时以 mut 的方式借用
  • 使用变量的不可变借用

moveFn Trait

  • 实际上使用了 move 的闭包依然可以使用 FnFnMut 特征
  • 一个闭包实现了哪种 Fn 特征取决于该闭包如何使用被捕获的变量,而不是取决于闭包如何捕获它们

这个点怎么理解呢?考虑下面的代码

1
let f =  move || println!("{}", s.len());

这里 f 闭包同时实现了 FnOnce, FnMut, Fn,尽管 move 把所有权都转移走了(“闭包如何捕获他们”)但是 s.len() 仅仅只使用了不可变借用(“该闭包如何使用被捕获的变量”),因此还是 Fn,也因此可以作为 FnOnce, FnMut 泛型的参数。

更具体的,一个闭包并不仅仅实现某一种 Fn 特征,规则如下:

  1. 所有的闭包都自动实现了 FnOnce 特征,因此任何一个闭包都至少可以被调用一次
  2. 没有移出所捕获变量的所有权的闭包自动实现了 FnMut 特征
  3. 不需要对捕获变量进行改变的闭包自动实现了 Fn 特征

我们可以看成是一个继承的关系:

FnOnceFnMutFn \boxed{\texttt{FnOnce}}\longrightarrow\boxed{\texttt{FnMut}}\longrightarrow\boxed{\texttt{Fn}}

通过源码可以看得更清晰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pub trait FnOnce<Args: Tuple> {
/// The returned type after the call operator is used.
#[lang = "fn_once_output"]
#[stable(feature = "fn_once_output", since = "1.12.0")]
type Output;

/// Performs the call operation.
#[unstable(feature = "fn_traits", issue = "29625")]
extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

pub trait FnMut<Args: Tuple>: FnOnce<Args> {
/// Performs the call operation.
#[unstable(feature = "fn_traits", issue = "29625")]
extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}

pub trait Fn<Args: Tuple>: FnMut<Args> {
/// Performs the call operation.
#[unstable(feature = "fn_traits", issue = "29625")]
extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}