Rust 中的闭包

  |   0 评论   |   0 浏览

什么是闭包?维基百科上对闭包是这样描述的:

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。捕捉时对于值的处理可以是值拷贝,也可以是名称引用,这通常由语言设计者决定,也可能由用户自行指定(如 C++)。

闭包的概念出现于 60 年代,最早实现闭包的程序语言是 Scheme。之后,闭包被广泛使用于函数式编程语言如 ML 语言和 LISP。很多命令式程序语言也开始支持闭包。

Rust 中的闭包(Closure)

Rust 中的闭包(closure)是一种匿名函数,闭包可以赋给变量也可以作为参数进行传递。闭包能够捕获外部作用域中的变量。

在 Rust 中,函数和闭包都是实现了 FnFnMutFnOnce Trait 的类型。

闭包的定义与函数定义类似,使用 || 代替 () 将参数括起来,函数体同样使用 {} 作为边界。

示例

1// 函数的定义
2fn  add_one_v1   (x: u32) -> u32 { x + 1 }
3
4// 闭包的定义
5let add_one_v2 = |x: u32| -> u32 { x + 1 };
6let add_one_v3 = |x|             { x + 1 }; // 参数类型可以省略
7let add_one_v4 = |x|               x + 1  ; // 函数体中只有一个表达式时,可以省略大括号

类型推断

闭包的参数类型可以省略,编译器会自动推断参数类型,并且以第一次被调用时的推断结果为准。

示例

1let example_closure = |x| x;
2let s = example_closure(String::from("hello")); // 此时参数 x 的类型确定为 `String`
3let n = example_closure(5); //expected struct `String`, found integer rustc(E0308)

在上面的示例中,闭包在第一次调用时编译器推断出它的参数类型是 String,接下来再调用时就不能传入其他类型值了。

捕获变量

闭包可以捕获周围环境中的变量,捕获的方式有三种:

  • 不可变借用
  • 可变借用
  • 获取所有权

示例

 1fn main() {
 2    // 获取所有权
 3    let msg1 = String::from("Hello String!");
 4    let say_hello = || {
 5        let msg = msg1;
 6        println!("{}", msg);
 7    };
 8    say_hello(); // Hello String!
 9    //println!("{}", msg1); // 此时无法访问`msg1`,因为它的所有权已经转移到闭包了
10
11    // 不可变借用
12    let msg2 = String::from("Hello &String!");
13    let say_hello = || {
14        let msg = &msg2;
15        println!("{}", msg);
16    };
17    say_hello(); // Hello &String!
18    println!("{}", msg2); // Hello &String!
19
20    // 可变借用
21    let mut msg3 = String::from("Hello &mut String");
22    let mut say_hello = || {
23        let msg = &mut msg3;
24        msg.push_str("!");
25        println!("{}", msg);
26    };
27    say_hello(); // Hello &mut String!
28    say_hello(); // Hello &mut String!!
29    println!("{}", msg3); // Hello &mut String!!
30}

另外,如果要强制闭包获取环境中的值的所有权,我们可以使用 move 关键字。

示例
正常情况下,下面两次打印输出都是 10

 1fn main() {
 2    let mut num = 5;
 3
 4    {
 5        let mut add_num = |x: i32| {
 6            num += x;
 7            println!("num = {}", num);
 8        };
 9
10        add_num(5); // 10
11    }
12
13    println!("num = {}", num); // 10
14}

使用 move 之后,闭包中的 num 获得了 5 的所有权(通过拷贝)。

 1fn main() {
 2    let mut num = 5;
 3
 4    {
 5        let mut add_num = move |x: i32| {
 6            num += x;
 7            println!("num = {}", num);
 8        };
 9
10        add_num(5); // 10
11    }
12
13    println!("num = {}", num); // 5
14}

闭包的实现

Rust 中的闭包实际上是 Trait 的语法糖。

每个闭包实例都有一个唯一的 Trait,即:FnFnMutFnOnce

这三个 Trait 对应三种捕获方式:

  • Fn -- 不可变借用
  • FnMut -- 可变借用
  • FnOnce -- 移动(获得变量值的所有权)

这三个 Fn Trait 的定义:

 1pub trait FnOnce<Args> {
 2    type Output;
 3    pub extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
 4}
 5
 6pub trait FnMut<Args>: FnOnce<Args> {
 7    pub extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
 8}
 9
10pub trait Fn<Args>: FnMut<Args> {
11    pub extern "rust-call" fn call(&self, args: Args) -> Self::Output;
12}

闭包的 || {} 语法是上面 3 个 Trait 的语法糖。Rust 将会为了环境创建一个结构体,impl 合适的 Trait,并使用它。

闭包作为参数

从三个 Trait 的定义中看,这三种 Trait 是继承关系,FnOnce 是顶级父 Trait。假如我们指定 FnOnce 作为参数类型,这说明闭包可能采取这三种捕获方式中的任意一种。

示例一:在结构体中使用 Fn Trait

1struct Cacher<T>
2where
3    T: Fn(u32) -> u32,
4{
5    calculation: T,
6    value: Option<u32>,
7}

示例二:在函数中使用 Fn Trait

1fn call_with_one<F>(some_closure: F) -> i32
2    where F: Fn(i32) -> i32 {
3
4    some_closure(1)
5}
6
7let answer = call_with_one(|x| x + 2);
8
9assert_eq!(3, answer);

闭包和函数指针

一个普通函数有点像一个没有环境的闭包,因此,我们可以将这个函数传给闭包参数。

前提是,这个普通函数的函数指针类型 得与闭包的类型保持一致,即:两者的参数类型和返回值需一致。

示例:

 1fn call_with_one<F>(some_closure: F) -> i32
 2where
 3    F: Fn(i32) -> i32,
 4{
 5    some_closure(1)
 6}
 7
 8// 函数指针类型是:`fn(i32) -> i32`
 9fn add_one(i: i32) -> i32 {
10    i + 1
11}
12
13fn main() {
14    let answer = call_with_one(add_one);
15    println!("answer: {}", answer);
16
17    let closure = |i| i + 1;
18    let answer = call_with_one(closure);
19    println!("answer: {}", answer);
20}

相关资料

Functional Language Features: Iterators and Closures

Closures - Rust By Example

闭包 · RustPrimer

Closures in Rust | Yury Zhauniarovich

rust-book-chinese/Closures