Rust 支持函数式编程,因此高阶函数、闭包等特性都不可或缺。
本文主要参考了 Rust Course 和 Rust 标准库文档。
什么是闭包
闭包是在支持头等函数的编程语言中实现词法绑定的一种技术,与函数不同的是,它可以捕捉自由变量,这意味着即使脱离了闭包创建时的上下文也能正常运行。
闭包的用途:
-
闭包实现了类似环境变量的功能,多个函数使用相同的环境。
-
闭包可以定义自己的控制流程。
-
闭包可以实现对象系统。
在 Rust 中,可以像如下方式创建一个最简单的闭包:
let add1 = |x: i32| x + 1;
assert_eq!(add1(1), 2);
上面这段代码创建了一个匿名函数add1
并在之后调用。
Rust 中闭包的特点:
-
参数列表中使用
||
代替()
。 -
如果只有一个语句可以省略掉
{}
。 -
可以捕捉自由变量。
可以利用闭包的类型自动推导,以下几种方法是等效的:
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 ;
需要注意的是,Rust 虽然有类型的自动推导,但是它只是一种语法糖,而不是泛型,当编译器推导出一种类型的闭包之后,它会一直使用该类型。
闭包本质上是一种词法绑定,故当闭包从环境中捕获一个值时,会分配内存去存储这些值。对于有些场景来说,这种额外的内存分配会成为一种负担。与之相比,函数就不会去捕获这些环境值,因此定义和使用函数不会拥有这种内存负担。
三种Fn
特征
闭包有三种方式捕获自由变量,分别是:转移所有权、可变借用和不可变借用:
FnOnce
,顾名思义,这种闭包只能使用一次,因为它会夺取捕获到的变量的所有权。
pub trait Fn<Args> : FnMut<Args> {
extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}
FnMut
,使用可变引用捕获了环境中的值,因此可以对其进行修改。
pub trait FnMut<Args> : FnOnce<Args> {
extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}
Fn
,使用不可变引用捕获了环境中的值。
pub trait FnOnce<Args> {
type Output;
extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}
当闭包的生命周期大于需要捕获的自由变量的生命周期时,可以使用move
关键字将自由变量的所有权移入闭包中:
let v = vec![1, 2, 3];
let print_vec = move || println!("{:?}", v);
除了
FnMut
需要在创建变量时使用mut
,创建闭包时并不需要显式地指定它实现的特征,因此闭包的类型实际上是编译器推导出来的,只有将闭包当作参数传递需要类型标注时才需要显式标记类型。
一个闭包实现了哪种 Fn 特征取决于该闭包如何使用被捕获的变量,而不是取决于闭包如何捕获它们。
- 所有的闭包都自动实现了
FnOnce
特征,因此任何一个闭包都至少可以被调用一次 - 没有移出所捕获变量的所有权的闭包自动实现了
FnMut
特征 - 不需要对捕获变量进行改变的闭包自动实现了
Fn
特征
下面是 Rust Course 中的一个例子,说明了闭包实现的特征取决于使用自由变量的方法,而且因为只使用了不可变引用,三种特征都可以实现。
fn main() {
let s = String::new();
let update_string = || println!("{}",s);
exec(update_string);
exec1(update_string);
exec2(update_string);
}
fn exec<F: FnOnce()>(f: F) {
f()
}
fn exec1<F: FnMut()>(mut f: F) {
f()
}
fn exec2<F: Fn()>(f: F) {
f()
}
在编程中需要我们善于利用编译器的报错,可以先使用 Fn 特征,然后编译器会告诉你正误以及该如何选择。
闭包作为返回值
闭包作为函数返回值时不要忘了使用impl
关键字,可以理解为实现了Fn(xxx) -> yyy
特征。
fn add_num(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
fn main() {
let add1 = add_num(1);
assert_eq!(add1(0), 1);
}
使用这种方式有一个局限性就是只能返回一种闭包,因为即使是相同函数签名的闭包也是不同类型的,这时就需要使用特征对象:
fn factory(x:i32) -> Box<dyn Fn(i32) -> i32> {
let num = 5;
// 要注意闭包中只捕获了 num 而没有捕获 x,x是闭包的参数,而不是上文中的 x
if x > 1{
Box::new(move |x| x + num)
} else {
Box::new(move |x| x - num)
}
}
fn main() {
let add5 = factory(2);
let sub5 = factory(1);
assert_eq!(add5(0), 5);
assert_eq!(sub5(0), -5);
}
闭包的生命周期
Rust 中函数和闭包有不同的生命周期省略原则(Lifetime Elision),故两段几乎相同的代码在函数中能编译通过,在闭包中不能通过:
fn fn_elision(x: &i32) -> &i32 { x }
let closure_slision = |x: &i32| -> &i32 { x };
会报错:
error: lifetime may not live long enough
--> d.rs:2:47
|
2 | let closure_slision = |x: &i32| -> &i32 { x };
| - - ^^ returning this value requires that `'1` must outlive `'2`
| | |
| | let's call the lifetime of this reference `'2`
| let's call the lifetime of this reference `'1`
如果函数参数中只有一个引用类型,那该引用的生命周期会被自动分配给所有的返回引用。但是闭包不会这样,这点值得我们注意。
结构体中使用闭包
如下代码是官方示例的升级版,Cacher
结构体只有在需要获取值且尚未重复计算时执行闭包,在执行后将计算结构缓存到一个HashMap
之中,这种模式被叫做惰性求值。
use std::collections::HashMap;
struct Cacher<F>
where F: Fn(i32) -> i32
{
val_map: HashMap<i32, i32>,
calculation: F
}
impl <F> Cacher<F>
where F: Fn(i32) -> i32
{
fn new(calculation: F) -> Cacher<F> {
Cacher {
val_map: HashMap::new(),
calculation
}
}
fn value(&mut self, arg: i32) -> i32 {
if let Some(v) = self.val_map.get(&arg) {
*v
} else {
let v = (self.calculation)(arg);
self.val_map.insert(arg, v);
v
}
}
}
fn main() {
let mut c = Cacher::new(|x| x);
let v1 = c.value(1);
let v2 = c.value(2);
assert_eq!(v1, 1);
assert_eq!(v2, 2);
}