Skip to content

什么是闭包 Closure

Rust 是一个比较新的编程语言(00后编程语言), 它在设计的时候 参考了很多已经存在的不同风格的编程语言, 其中就包括函数式编程风格

函数式编程风格通常包含将函数作为参数值或其他函数的返回值, 将函数赋值给变量以供之后执行等等

但是 Rust 不同于一些脚本语言(比如Lua/JS/PHP), 可以直接将函数当作参数/返回值来当作闭包使用 因为在 Rust 中函数并不是闭包, 虽然写法和用法上类似, 但这是两个不同的东西

闭包(Closure): 一个可以存储在变量里的可执行结构(类似函数)

  1. 匿名函数
  2. 保存为变量/作为参数/作为返回值
  3. 可以在一个地方创建闭包, 然后再另外一个上下文中调用闭包

使用闭包定义行为抽象

Rust设计语言这个章节, 说白了就是在说: 为什么要使用闭包?

简单来说, 我需要固定执行一些代码, 然后执行(x), 最后的结果会根据 x 的不同而不同, 举个例子吧, 我需要从一组数字中, 分别获取偶数和奇数:

rust
fn main() {
    let nums: Vec<u32> = vec![1, 2, 3, 4, 5, 6];

    let mut even_nums = vec![];
    for n in nums.iter() {
        // 先不用管 iter() 函数是什么,
        // 只需要知道这样操作可以遍历就行
        if n % 2 == 0 {
            even_nums.push(n);
        }
    }

    let mut odd_nums = vec![];
    for n in nums.iter() {
        if n % 2 == 1 {
            odd_nums.push(n);
        }
    }

    println!("{:?}", even_nums);
    println!("{:?}", odd_nums);
}

这个代码就是将 Vec 所有元素都遍历一遍, 然后根据一个条件(x)来筛选元素, 将所有符合 条件(x)的元素收集到一个新的 Vec 中, 可以看出, 仅仅是筛选的条件不同, 其他代码没有任何变化, 那么能否将筛选条件逻辑当作参数动态的传入? 实现行为的抽象

rust
fn filter<F>(nums: &[u32], filter_cn: F) -> Vec<u32>
where F: Fn(u32) -> bool // 声明这个匿名函数(也就是闭包)的参数和返回值
{
    let mut filtered: Vec<u32> = Vec::new();
    for n in nums.iter() {
        if filter_cn(*n) {
            filtered.push(*n);
        }
    }
    filtered
}


fn main() {
    let nums: Vec<u32> = vec![1, 2, 3, 4, 5, 6];

    // 将判断的行为当作参数传入就大大提高了代码复用性和可读性
    let even_nums = filter(&nums, |n| n % 2 == 0);
    let odd_nums = filter(&nums, |n| n % 2 == 1);

    println!("{:?}", even_nums);
    println!("{:?}", odd_nums);
}

将一个行为(可能是判断/也可能是其他操作)动态的传入另外一个函数 就叫做: 定义行为抽象

闭包的类型推断和标注

和函数非常类似, 但是有些情况下, 闭包不需要声明返回值和参数的数据类型

rust
// 函数
fn f1(x:i32) -> i32 {
  x + 1
}

// 闭包(指定参数和返回值类型)
let c1 = |x:i32| -> i32 {
  x + 1
}

// 闭包(未声明参数和返回值类型)
let c2 = |x| { x + 1 }

// 闭包(只有一句代码的时候可以省略{}
// 类似 python 的 lambda 表达式, JS 的箭头函数
let c3 = |x| x + 1

使用带有泛型和 Fn Trait 的闭包

  • 定义 struct 需要知道所有的字段类型
  • 需要指定闭包的类型
  • 每个闭包的实例都有自己唯一的匿名类型, 所有需要使用泛型
  • Fn Trait: 所有闭包都至少实现了一个 Fn Trait, 只需要约束泛型的类型为其中一个 Trait 即可
rust
struct Array<T> {
    items: Vec<T>,
}

impl<T> Array<T> {
    // 从 Vec 中所有所有元素, 创建一个 Array 实例
    fn from(items: Vec<T>) -> Self {
        Self {
            items,
        }
    }

    // 对数组进行过滤
    fn filter<F>(&self, filter_fn: F) -> Vec<T>
    where F: Fn(&T) -> bool,
          T: Clone,
    // Fn(&T) -> bool: 限制闭包的参数和返回值类型
    // &T 表示传入的数据类型的引用, 比如(i32) 那么参数类型就是 &i32
    // 限制泛型必须实现 Clone 这个 trait, 否则可能没有 clone 方法
    {
        let mut filtered_items = Vec::new();
        for item in self.items.iter() {
            if filter_fn(item) {
                filtered_items.push(item.clone());
            }
        }
        filtered_items
    }
}

fn main() {
    let array = Array::from(vec![1, 2, 3, 4]);
    let filtered = array.filter(|&x| x % 2 == 0);

    // 其实也可以简写为:
    // let odd_nums = Array::from(vec![1, 2, 3, 4]).filter(|&x| x % 2 == 1);

    println!("{:?}", filtered);
}

闭包会捕获所在环境

rust
fn main() {
    let num = 10;

    // 这个闭包捕获了 num 的值
    let increment = |x: i32| num + x;

    // 所以才能运算出想要的结果
    let num2 = increment(10);

    // 注意: 函数是不能捕获外部环境的, 只有闭包才能捕获外部环境
    // 但是: 捕获环境会产生额外的内存开销, 而函数不会
    // error[E0434]: can't capture dynamic environment in a fn item
    // fn decrement(x: i32) {
    //     num - x
    // }

    println!("num2 = {}", num2); // 20
}

函数从所在环境捕获值的方式

  • Fn: 只读引用
  • FnMut: 可变引用
  • FnOnce: 取得所有权

创建闭包时, 通过闭包对值得使用, Rust编译器可以推断出具体使用哪个 Trait

  • 所有闭包都实现了 FnOnce
  • 没有移动捕获变量的实现了 FnMut
  • 无需可变范文捕获变量的闭包实现了 Fn
rust
struct CaptureExample {
    value: i32,
}

impl CaptureExample {
    fn new(value: i32) -> Self {
        Self { value }
    }

    fn fn_closure<F>(&self, f: F)
    where
        F: Fn(i32) -> i32,
    {
        let result = f(self.value);
        println!("Result: {}", result);
    }

    fn fn_once_closure<F>(&mut self, f: F)
    where
        F: FnOnce(&mut i32) -> i32,
    {
        let result = f(&mut self.value);
        println!("Result with once closure: {}", result);
    }
}

fn main() {
    let mut ce = CaptureExample::new(10);

    // 使用闭包捕获环境, 传入只读引用
    ce.fn_closure(|val| val + 10);

    // 使用闭包捕获环境, 传入可变引用
    ce.fn_once_closure(|val| {
        *val += 10; // 需要手动解引用
        *val
    });
}

move关键字

在参数列表前使用 move 关键字, 可以强制闭包取得他所捕获的环境值的所有权

  • 当闭包传递给新线程以移动数据使其所有权归新线程时非常有用
rust
fn main() {
    let nums: Vec<i32> = vec![1, 2, 3];

    let eq_to_nums  = move |new_nums: Vec<i32>| new_nums == nums;
    // move 关键字会让被捕获的值(nums)的所有权移交给这个闭包
    // 所以后续就无法再输出 nums
    // println!("不能再使用 nums {:?}", nums);

    let anthor_nums = vec![1,2,3];
    println!("nums eq anthor_nums, {}", eq_to_nums(anthor_nums))
}

Released under the MIT License.