闭包在现代化的编程语言中普遍存在。闭包是一种匿名函数,它可以赋值给变量也可以作为参数传递给其它函数,不同于函数的是,它允许捕获调用者作用域中的值。Rust 闭包在形式上借鉴了 Smalltalk 和 Ruby 语言,与函数最大的不同就是它的参数是通过 |parm1| 的形式进行声明,如果是多个参数就 |param1, param2,…|, 下面给出闭包的形式定义:
|param1, param2,...| {
语句1;
语句2;
返回表达式
}
如果只有一个返回表达式的话,定义可以简化为:
|param1| 返回表达式
Rust 是静态语言,因此所有的变量都具有类型,但是得益于编译器的强大类型推导能力,在很多时候我们并不需要显式地去声明类型,但是显然函数并不在此列,必须手动为函数的所有参数和返回值指定类型,原因在于函数往往会作为 API 提供给你的用户,因此你的用户必须在使用时知道传入参数的类型和返回值类型。
与函数相反,闭包并不会作为 API 对外提供,因此它可以享受编译器的类型推导能力,无需标注参数和返回值的类型。
为了增加代码可读性,有时候我们会显式地给类型进行标注,出于同样的目的,也可以给闭包标注类型:
let sum = |x: i32, y: i32| -> i32 {
x + y
}
下面展示了同一个功能的函数和闭包实现形式:
fn add_one_v1 (x: u32) -> u32 { x + 1 } // 函数
let add_one_v2 = |x: u32| -> u32 { x + 1 }; // 闭包形式1
let add_one_v3 = |x| { x + 1 }; // 闭包形式2
let add_one_v4 = |x| x + 1 ; // 闭包形式3
三种不同的闭包也展示了三种不同的使用方式:省略参数、返回值类型和花括号对。
假设我们要实现一个简易缓存,功能是获取一个值,然后将其缓存起来,那么可以这样设计:
一个闭包用于获取值
一个变量,用于存储该值
可以使用结构体来代表缓存对象,最终设计如下:
struct Cacher<T>
where
T: Fn(u32) -> u32,
{
query: T,
value: Option<u32>,
}
这里的T的特征约束Fn(u32) -> u32
有点像C++中声明函数指针类型,标准库提供的 Fn 系列特征,再结合特征约束,就能很好的解决了这个问题. T: Fn(u32) -> u32 意味着 query 的类型是 T,该类型必须实现了相应的闭包特征 Fn(u32) -> u32。约束表明该闭包拥有一个u32类型的参数,同时返回一个u32类型的值。Fn 特征不仅仅适用于闭包,还适用于函数,因此上面的 query 字段除了使用闭包作为值外,还能使用一个具名的函数来作为它的值。下面的例子将上面的u32类型换成了泛型,可以用来缓存各种数据类型。
use std::fmt::Debug;
#[allow(unused)]
fn main() {
let x = "Hello World!".to_string();
let mut cache = Cacher::new(|x| -> String {x});
cache.value(x);
println!("{:?}", cache.value);
let x = 123;
let mut cache = Cacher::new(|x| -> i32 {x});
cache.value(x);
println!("{:?}", cache.value);
}
struct Cacher<T, E>
where
T: Fn(E) -> E,
E: Clone + Debug
{
query: T,
value: Option<E>,
}
impl<T, E> Cacher<T, E>
where
T: Fn(E) -> E,
E: Clone + Debug
{
fn new(query: T) -> Self{
Cacher { query, value: None}
}
fn value(&mut self, arg: E) -> E {
match self.value.clone() {
Some(v) => v,
None => {
let v = (self.query)(arg);
self.value = Some(v.clone());
v
}
}
}
}
上面这段代码有以下几点需要注意。
Option
,同时Cacher还拥有一个value方法。(因此在rust里,set和get操作,就是给字段和方法起同一个名字,我的评价是不如C#)let v = (self.query)(arg)
这行,前面的self.query必须用括号包括起来,否则会编译器会报错。cache.value(x)
)需要知道这里发生了什么。闭包可以通过三种方式捕获作用域中的值,它们直接对应到函数获取参数的三种方式:不可变借用,可变借用和获取所有权。闭包会根据函数体中如何使用被捕获的值决定用哪种方式捕获。这点非常抽象,不如C++的lambda表达式简单易懂。
以不可变引用方式捕获
fn main() {
let mut x = "Hello".to_string();
let lambda = || println!("{}", x); // 在闭包对变量x的操作是只读,因此rust会使用不可变引用方式来捕获
lambda(); // 使用闭包
x.push_str(", World!");
println!("{}", x);
}
在来看一个例子:
fn main() {
let mut x = "Hello".to_string();
let lambda = || println!("{}", x); // 在闭包对变量x的操作是只读,因此rust会使用不可变引用方式来捕获
lambda(); // 使用闭包,(其中存在x的不可变引用)
x.push_str(", World!");
println!("{}", x);
lambda(); // 再次调用lambda
}
这个例子无法通过编译,这是因为编译器检查到在同一作用域内,既有可变引用,又有不可变引用。最后一次调用lambda的时候,其中存在x的不可变引用,而之前的x.push_str又是一个可变引用。具体的报错如下所示: 报错中很直接的指出既有mutable又有immutable。
2. 以可变引用方式捕获
fn main() {
let mut x = "Hello".to_string();
let mut lambda = || x.push_str(", World");
lambda();
x.push_str("-- from zhangsan");
println!("{}", x);
}
这个例子中,我们在闭包中对捕获的x做了修改,因此rust会以可变引用的方式捕获,需要注意的是lambda必须是可变的才行。另外我们在调用了lambda之后,又使用了push_str来修改x,编译成功通过。这是因为rust的编译器检测到lambda不再使用,直接被drop掉了。因此当前作用域内只有一个可变引用,而不是两个可变引用。我们可以通过下面的例子来证实这一点。
fn main() {
let mut x = "Hello".to_string();
let mut lambda = || x.push_str(", World!");
lambda();
x.push_str("-- from zhangsan");
lambda(); // 再次调用lambda
}
以转移所有权方式捕获
下面这个例子用来展示转移所有权方式捕获。
fn func(s: String) {
println!("{s}");
}
fn main() {
let x = "Hello".to_string();
let lambda = || func(x);
lambda();
}
我们在闭包中调用了func函数,将x的所有权转移到了func函数中。x随着func函数调用结束而释放。我们来使用一个例子来证实。
fn func(s: String) {
println!("{s}");
}
fn main() {
let x = "Hello".to_string();
let lambda = || func(x);
lambda();
println!("{}", x);
}
编译报错信息如下所示:
错误显示我们借用了一个moved之后的值。因此会失败。同时上面这种方式也会导致只能调用一次lambda闭包。例如:
fn func(s: String) {
println!("{s}");
}
fn main() {
let x = "Hello".to_string();
let lambda = || func(x);
lambda();
lambda(); // ERROR
}
同时注意到编译器提示我们this value implements FnOnce, which causes it to be moved when called
。说我们的lambda实现了FnOnce trait,在调用时会发生所有权移动。因为随着x的所有权被转移到func函数中,它已经随着第一次func函数调用而被释放。如果我们想要既能捕获环境中变量的所有权,又能多次调用,需要使用关键字move
,它将环境中的变量所有权转移到闭包中。在将闭包传递到一个新的线程时这个技巧很有用,它可以移动数据所有权给新线程。例如:
fn func(s: &String) { // 传递String类型的引用
println!("{s}");
}
fn main() {
let x = "Hello".to_string();
let lambda = move || func(&x); // move
lambda();
lambda(); // 第二次调用lambda
}
这样,我们的第二次调用lambda就是成功的。为了验证x确实被移动走了,我们在调用一次lambda之后增加一行打印。来看看程序执行是否出错,如果出错那就证明x被移走了,否则x没有被移走。
fn func(s: &String) {
println!("{s}");
}
fn main() {
let x = "Hello".to_string();
let lambda = move || {func(&x)};
lambda();
println!("{}", x); // ERROR value borrowed here after move
lambda();
}
程序执行报错,表明x确实被移走了。因此我们无法在println!中打印这个x。不过此时还有一个疑问,那就是x可以被转移到闭包内,它的生命周期和闭包本身是一样的,而闭包的生命周期就是它最后一次被调用的时候。
闭包捕获和处理环境中的值的方式影响闭包实现的 trait。Trait 是函数和结构体指定它们能用的闭包的类型的方式。取决于闭包体如何处理值,闭包自动、渐进地实现一个、两个或三个 Fn trait。
函数也可以实现所有的三种 Fn traits。如果我们要做的事情不需要从环境中捕获值,则可以在需要某种实现了 Fn trait 的东西时使用函数而不是闭包。下面的例子展示了Fn trait的用法,并且这个例子中充满了陷阱。
struct Human<T>
where T: FnOnce() -> String
{
name: T
}
impl<T> Human<T>
where T: FnOnce() -> String
{
fn new(name: T) -> Self {
Human { name }
}
fn get_name(self) -> String{
(self.name)()
}
}
fn main() {
let name = "zhangsan".to_string();
let lambda = || name;
let h = Human::new(lambda);
println!("{}", h.get_name());
}
首先,由于FnOnce的trait约束,lambda闭包将以转移所有权的方式捕获环境中的name,并且闭包第一次调用后,被释放;
其次,get_name中的参数是self,意味着将h的所有权转移到get_name中,随着get_name调用结束,h被释放。我们再来看一个例子:
fn main() {
let s = "Hello".to_string();
let update_string = || println!("{}",s);
exec(update_string);
}
fn exec<F: FnOnce()>(f: F) {
f();
}
这个例子中的,闭包以不可变引用的方式捕获了s,exec的参数f要求的约束是FnOnce。因此f只能被调用一次,如果在exec中多次调用f,那么编译器会提示你加上Copy trait。
实际上,一个闭包并不仅仅实现某一种 Fn 特征,规则如下:
下面这个例子很好的说明这一点。
fn main() {
let s = "Hello".to_string();
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()
}
虽然,闭包只是对 s 进行了不可变借用,实际上,它可以适用于任何一种 Fn 特征:三个 exec 函数说明了一切。一个闭包实现了哪种 Fn 特征取决于该闭包如何使用被捕获的变量。下面是三个Fn trait的简化版源码。
pub trait Fn<Args> : FnMut<Args> {
extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}
pub trait FnMut<Args> : FnOnce<Args> {
extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}
pub trait FnOnce<Args> {
type Output;
extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}
从特征约束能看出来 Fn 的前提是实现 FnMut,FnMut 的前提是实现 FnOnce,因此要实现 Fn 就要同时实现 FnMut 和 FnOnce。从源码中还能看出一点:Fn 获取 &self,FnMut 获取 &mut self,而 FnOnce 获取 self。 在实际项目中,建议先使用 Fn 特征,然后编译器会告诉你正误以及该如何选择。