何为闭包?
闭包 是由捆绑起来(封闭的)的函数和函数周围状态(词法环境)的引用组合而成。换言之,闭包让函数能访问它的外部作用域。
闭包是在支持头等函数的编程语言中实现词法绑定的一种技术,在实现上是一个存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)结构体。
那么在 Rust 中,闭包是如何使用的呢?
语法
闭包是用竖线创建的:|..|
:
fn main() {
// 对于轻量级语法,参数和返回类型可以推断
let double_it = |n| n * 2;
dbg!(double_it(50));
// 也可以指定类型并将代码体加上括号以使其完全清晰明确:
let add_1f32 = |x: f32| -> f32 { x + 1.0 };
dbg!(add_1f32(50.));
}
参数放在 |..|
之间。代码体可以用 {..}
括起来,但如果它是单个表达式,这些括号可以省略。
参数类型是可选的,如果未指定则会进行推断。返回类型也是可选的,但只有在代码体周围使用 {..}
时才能写出返回类型。
这些示例也都可以仅写成嵌套函数的形式——它们不会从词法环境中捕获任何变量。接下来我们看看捕获变量的情况。
捕获
闭包可以从其定义的环境中捕获变量:
fn main() {
let max_value = 2;
let clamp = |v| {
if v > max_value {
max_value
} else {
v
}
};
dbg!(clamp(1));
dbg!(clamp(3));
}
// Output
// [src/main.rs:11:5] clamp(1) = 1
// [src/main.rs:12:5] clamp(3) = 2
默认情况下,闭包通过引用捕获变量。在这里,max_value
被 clamp
捕获,而 main
函数仍可使用它来进行打印。
如果闭包对值进行了可变操作,它会通过可变引用捕获这些值。
我们尝试一下修改 max_value
的值:
fn main() {
let mut max_value = 2; // mut
let mut clamp = |v| { // clamp 也需要声明为 mut
if v > max_value {
max_value *= 10; // 修改值
max_value
} else {
v
}
};
// compile error: cannot borrow `max_value` as immutable because it is also borrowed as mutable
// println!("{}", max_value);
dbg!(clamp(1));
dbg!(clamp(3));
println!("{}", max_value); // Ok here
}
可以看到,要想代码编译过,首先,我们要声明 max_value
是可变的,同时,clamp
也需要是可变的。
如果我们想打印 max_value
的值,我们必须在可变引用的作用范围之外才能使用。上一文我们了解到借用检查规则,可变应用是独占的,这儿得到了体现。
可以使用 move
关键字强制闭包转移值而不是引用值。这对生命周期管理很有帮助,例如,如果闭包的生命周期必须长于被捕获的值(稍后会详细介绍生命周期)。
默认情况下,闭包会以对外部作用域中每个变量要求最低的访问形式捕获它们(如果可能,优先通过共享引用捕获,其次是独占引用,然后是值转移)。move
关键字强制通过值转移进行捕获 。
fn main() {
let str = String::from("Hello");
let greet = move |v| {
println!("{} {}",str, v);
};
greet("World");
greet("Rust");
// println!("{}", str); // compile error: borrow of moved value: `str`
}
当我们使用 move
强制闭包转移值时,main
将不再能使用 str
。
闭包 trait
闭包或 lambda 表达式具有无法命名的类型。不过,它们实现了特殊的 Fn
、FnMut
和 FnOnce
特征。
特殊类型 fn(..) -> T
指的是函数指针——要么是函数的地址,要么是不捕获任何变量的闭包。
fn apply_and_log(
func: impl FnOnce(&'static str) -> String,
func_name: &'static str,
input: &'static str,
) {
println!("Calling {func_name}({input}): {}", func(input))
}
fn main() {
let suffix = "-itis";
let add_suffix = |x| format!("{x}{suffix}");
apply_and_log(&add_suffix, "add_suffix", "senior");
apply_and_log(&add_suffix, "add_suffix", "appenix");
let mut v = Vec::new();
let mut accumulate = |x| {
v.push(x);
v.join("/")
};
apply_and_log(&mut accumulate, "accumulate", "red");
apply_and_log(&mut accumulate, "accumulate", "green");
apply_and_log(&mut accumulate, "accumulate", "blue");
let take_and_reverse = |prefix| {
let mut acc = String::from(prefix);
acc.push_str(&v.into_iter().rev().collect::<Vec<_>>().join("/"));
acc
};
apply_and_log(take_and_reverse, "take_and_reverse", "reversed: ");
}
上述代码输出如下:
// Output
Calling add_suffix(senior): senior-itis
Calling add_suffix(appenix): appenix-itis
Calling accumulate(red): red
Calling accumulate(green): red/green
Calling accumulate(blue): red/green/blue
Calling take_and_reverse(reversed: ): reversed: blue/green/red
Fn
类型的闭包(例如 add_suffix
)既不会消耗也不会修改捕获的值。调用它时只需要对闭包的共享引用,这意味着该闭包可以重复甚至并发执行。
FnMut
类型的闭包(可以理解为 mut Fn
,例如 accumulate
)可能会修改捕获的值。通过独占引用访问闭包对象,因此它可以重复调用,但不能并发调用 。
如果有一个 FnOnce
类型的闭包(例如 take_and_reverse
),则只能调用一次。调用它会消耗该闭包以及通过值转移捕获的所有值。
FnMut
是 FnOnce
的子类型。Fn
是 FnMut
和 FnOnce
的子类型。也就是说,在需要 FnOnce
的地方可以使用 FnMut
,在需要 FnMut
或 FnOnce
的地方可以使用 Fn
。
千万不要记忆谁是谁的子类型,虽然上述文段确实是这么表达的,但是一旦你记忆这些,后续很容易混淆。我们想象一下设计模式中的里氏替换原则:程序中任何使用父类对象的地方,都应该能够透明地替换为子类对象,且行为逻辑保持一致。所以此处,如果一个需要 FnOnce
的地方可以使用 FnMut
,那么证明 FnMut
是 FnOnce
的子类型。
当定义一个接受闭包的函数时,如果可能的话,应该接受 FnOnce
类型的闭包(即只调用一次),否则接受 FnMut
类型的,最后才考虑 Fn
类型的。这样可以为调用者提供最大的灵活性。
相反,当拥有一个闭包时,灵活性最高的是 Fn
类型(它可以传递给接受这三种闭包特征中任何一种的函数),其次是 FnMut
类型,最后是 FnOnce
类型。
编译器还会根据闭包捕获的内容推断出 Copy
(例如对于 add_suffix
)和 Clone
(例如对于 take_and_reverse
)。函数指针(对 fn
项的引用)实现了 Copy
和 Fn
。