我必须立刻押注 Rust 之九🎲:什么是闭包?

230 阅读4分钟

"闭包是什么",是JavaScript 中最经典的问题之一。

你总是会写一段这样的代码

function init() {
  var name = "Mozilla"; // name 是 init 创建的局部变量
  function displayName() {
    // displayName() 是内部函数,它创建了一个闭包
    console.log(name); // 使用在父函数中声明的变量
  }
  displayName();
}
init();

来向人们解释闭包由 在一个作用域中可以访问另一个函数内部的局部变量的函数 形成。

这是一个相当抽象的概念,因为在 JavaScript 中 , function 在语法上是混淆的。

你可以在 Rust 中写一段类似的代码

fn main() {
    let mut x = 5;

    fn add_one(n:i32) -> i32 {
        x += 1
    }
    add_one();
    println!("The value of x is: {}", x);
}

很遗憾,这段代码编译不会通过,因为你不能在 Rust 的 fn 中捕获外部的变量。

Rust 中的闭包

Rust中,有专门的闭包语法,你可以像这样完成上述代码的功能

fn main() {
    let mut x = 5;

    let mut add_one = |n:i32| {
        x += n;
    };
    add_one(1);
    println!("The value of x is: {}", x);
}

闭包语法

/**
 * 闭包语法
 * |参数| -> 返回值 {
 *   函数体
 * }
 */
let mut add_one_and_return = |n:i32| -> i32 {
    x = x + n;
    x
};

如果闭包体的最后一行是一个表达式(没有显式的 return 语句),那么闭包会默认返回该表达式的结果。

若 Rust 能够推断出闭包的类型, 我们可以最终将闭包简写为 |参数| 函数体 的形式。

let x = 5;
let add_one_and_return_new = |n| x + n;
let y = add_one_and_return_new(1);
println!("{}", y);

闭包提供了简洁、灵活且强大的函数定义方式,这点特别像 JavaScript 中的箭头函数Java 中的 Lambda 表达式, 尽管它们在语法上有一些差异。

闭包类型

Rust 会根据闭包使用的上下文推导出闭包的类型。闭包的类型分为三种:

Fn

Fn:表示一个不可变的闭包(它不会改变捕获的环境)。

let closure: fn(i32, i32) -> i32 = |a, b| a + b;
let result = closure(1, 2);
println!("The result is: {}", result);

FnMut

FnMut:表示一个可以修改环境的闭包。

let mut x = 5;
let mut add_one = |n:i32| {
    x += n;
};
add_one(1);
println!("The value of x is: {}", x);

FnOnce

FnOnce:表示一个消耗环境的闭包。

let s = String::from("Hello");

// `move` 关键字将 `s` 的所有权移入闭包
let consume_string = move || {
  println!("Consumed string: {}", s);
};

// 调用闭包,`s` 的所有权已被移入闭包
consume_string();  // 输出: Consumed string: Hello

// 此时,`s` 的所有权已被消耗,无法再访问 `s`,以下代码会导致编译错误
// println!("{}", s);  // 编译错误: use of moved value: `s`

在闭包定义中使用了 move 关键字,它使得闭包捕获外部变量 s 的所有权而不是借用它。

闭包应用

在很多方法中,支持闭包作为参数,比如 mapfiltersort 等。

let muns = [1, 2, 3, 4, 5];

// 使用 `filter` 方法过滤数组中的偶数
let even_muns = muns.iter().filter(|&x| x % 2 == 0);

// 遍历过滤后的数组
for mun in even_muns {
    println!("{}", mun);
}

定义解读

在编辑器中点击 filter 方法,你会发现它的定义如下

pub trait Iterator {
    type Item;
    // ...
    fn filter<P>(self, predicate: P) -> Filter<Self, P>
    where
        Self: Sized,
        P: FnMut(&Self::Item) -> bool,
    {
        Filter::new(self, predicate)
    }
    // ...
}
  • fn filter<P>(self, predicate: P) -> Filter<Self, P> 表示 filter 是一个实例方法,它接受参数 P,返回一个 Filter(迭代器类型, 便于链式调用)。

  • where 关键字用于为泛型类型参数指定更多的约束

    • Self: Sized 说明 Self 实例类型的大小是已知的
    • P: FnMut(&Self::Item) -> bool 说明 predicate: P 参数是一个可变的闭包,它的参数是 Self::Item (迭代中Item要素) 的 引用 ,返回值是一个布尔值。
  • Filter::new(self, predicate) 表示函数体返回 Filter 迭代器的新实例由 selfpredicate 构建而成

使用解读

这是使用的闭包是 |&x| x % 2 == 0 , 你可能会疑惑,为什么是 |&x| 而不是 |x| ?

这是因为 muns.iter() 返回值是 Iterator<Item = &i32> , 即 Iterator::Item = &i32Self::Item = &i32 .

由于在闭包的参数 &Self::ItemItem 的不可变引用。 |x| 实际为 &&i32 (&i32 的引用), 这里使用 |&x| 结构了一层引用。

你在编辑器的类型提示中,你可以更明确这一行为

image-20241227165331219.png

  • 闭包提供了捕获外部的变量能力
  • 闭包提供了简洁、灵活且强大的函数定义方式
  • 类型约束(FnFnMutFnOnce)提高了代码的安全性和性能