什么是闭包?维基百科上对闭包是这样描述的:
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。捕捉时对于值的处理可以是值拷贝,也可以是名称引用,这通常由语言设计者决定,也可能由用户自行指定(如 C++)。
闭包的概念出现于 60 年代,最早实现闭包的程序语言是 Scheme。之后,闭包被广泛使用于函数式编程语言如 ML 语言和 LISP。很多命令式程序语言也开始支持闭包。
Rust 中的闭包(Closure)
Rust 中的闭包(closure)是一种匿名函数,闭包可以赋给变量也可以作为参数进行传递。闭包能够捕获外部作用域中的变量。
在 Rust 中,函数和闭包都是实现了 Fn
、FnMut
或 FnOnce
Trait 的类型。
闭包的定义与函数定义类似,使用 ||
代替 ()
将参数括起来,函数体同样使用 {}
作为边界。
示例:
// 函数的定义
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 ; // 函数体中只有一个表达式时,可以省略大括号
闭包的参数类型可以省略,编译器会自动推断参数类型,并且以第一次被调用时的推断结果为准。
示例:
let example_closure = |x| x;
let s = example_closure(String::from("hello")); // 此时参数 x 的类型确定为 `String`
let n = example_closure(5); //expected struct `String`, found integer rustc(E0308)
在上面的示例中,闭包在第一次调用时编译器推断出它的参数类型是 String
,接下来再调用时就不能传入其他类型值了。
闭包可以捕获周围环境中的变量,捕获的方式有三种:
示例:
fn main() {
// 获取所有权
let msg1 = String::from("Hello String!");
let say_hello = || {
let msg = msg1;
println!("{}", msg);
};
say_hello(); // Hello String!
//println!("{}", msg1); // 此时无法访问`msg1`,因为它的所有权已经转移到闭包了
// 不可变借用
let msg2 = String::from("Hello &String!");
let say_hello = || {
let msg = &msg2;
println!("{}", msg);
};
say_hello(); // Hello &String!
println!("{}", msg2); // Hello &String!
// 可变借用
let mut msg3 = String::from("Hello &mut String");
let mut say_hello = || {
let msg = &mut msg3;
msg.push_str("!");
println!("{}", msg);
};
say_hello(); // Hello &mut String!
say_hello(); // Hello &mut String!!
println!("{}", msg3); // Hello &mut String!!
}
另外,如果要强制闭包获取环境中的值的所有权,我们可以使用 move
关键字。
示例:
正常情况下,下面两次打印输出都是 10
fn main() {
let mut num = 5;
{
let mut add_num = |x: i32| {
num += x;
println!("num = {}", num);
};
add_num(5); // 10
}
println!("num = {}", num); // 10
}
使用 move
之后,闭包中的 num
获得了 5 的所有权(通过拷贝)。
fn main() {
let mut num = 5;
{
let mut add_num = move |x: i32| {
num += x;
println!("num = {}", num);
};
add_num(5); // 10
}
println!("num = {}", num); // 5
}
Rust 中的闭包实际上是 Trait 的语法糖。
每个闭包实例都有一个唯一的 Trait,即:Fn
,FnMut
和 FnOnce
。
这三个 Trait 对应三种捕获方式:
Fn
– 不可变借用FnMut
– 可变借用FnOnce
– 移动(获得变量值的所有权)这三个 Fn Trait 的定义:
pub trait FnOnce {
type Output;
pub extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}
pub trait FnMut: FnOnce {
pub extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}
pub trait Fn: FnMut {
pub extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}
闭包的 || {}
语法是上面 3 个 Trait 的语法糖。Rust 将会为了环境创建一个结构体,impl 合适的 Trait,并使用它。
从三个 Trait 的定义中看,这三种 Trait 是继承关系,FnOnce
是顶级父 Trait。假如我们指定 FnOnce
作为参数类型,这说明闭包可能采取这三种捕获方式中的任意一种。
示例一:在结构体中使用 Fn Trait
struct Cacher
where
T: Fn(u32) -> u32,
{
calculation: T,
value: Option,
}
示例二:在函数中使用 Fn Trait
fn call_with_one(some_closure: F) -> i32
where F: Fn(i32) -> i32 {
some_closure(1)
}
let answer = call_with_one(|x| x + 2);
assert_eq!(3, answer);
一个普通函数有点像一个没有环境的闭包,因此,我们可以将这个函数传给闭包参数。
前提是,这个普通函数的函数指针类型 得与闭包的类型保持一致,即:两者的参数类型和返回值需一致。
示例:
fn call_with_one(some_closure: F) -> i32
where
F: Fn(i32) -> i32,
{
some_closure(1)
}
// 函数指针类型是:`fn(i32) -> i32`
fn add_one(i: i32) -> i32 {
i + 1
}
fn main() {
let answer = call_with_one(add_one);
println!("answer: {}", answer);
let closure = |i| i + 1;
let answer = call_with_one(closure);
println!("answer: {}", answer);
}
Functional Language Features: Iterators and Closures
Closures - Rust By Example
闭包 · RustPrimer
Closures in Rust | Yury Zhauniarovich
rust-book-chinese/Closures