Rust中的闭包

本文深入探讨 Rust 中的闭包,解析闭包与函数的区别,并详细阐述 Rust 的 Fn、FnMut 和 FnOnce 三个闭包标准,以及它们在内存管理和并发中的作用。通过实例解释闭包对变量的捕获和使用规则。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一直对 Rust 中的闭包似懂非懂, 用的时候基本都是以编译器不报错为准, 报错了就按编译器提示的改。今天有点时间,把 the book 中的关于闭包的部分从头到尾看了一遍,发现自己原来的理解不说是入木三分吧,至少也是南辕北辙。

1.闭包跟函数(方法)的区别

貌似在这点上,大部分的语言的标准都还是比较一致的, 两者的最大的区别就是闭包可以捕获上下文中的变量, 简单来说就是在闭包里可操作的变量不单单是你显式传给闭包的变量,理论上在闭包所存在的作用域中, 所有在闭包之前定义的变量都可以为闭包所用。而这也应该是 closure 这个名字的由来。

2.Rust 中的闭包

Constraint or Trait?

因为 Rust 独有的 ownership 内存管理方式,导致闭包这个重点针对上下文中变量所创造出来的概念变的复杂了。你把上下文中的变量都拿到你的 body 里去了,你操作完之后拍拍屁股走了,让外层的函数们怎么办?要是只让借不让拿,这倒是个办法,但是有些情况下,我就是要将变量挪到闭包里去(比如说某些用到多线程的时候),这时候如果不让转移所有权,代码写起来也会很麻烦。Rust 面对这种情况还是选择了它惯用的办法,它定标准,你按它的标准声明你的代码,它再检查你的代码是不是符合你的声明。Rust 一共提供了三种标准, FnOnce, FnMut, Fn。它们实际上是三个标准库中的三个 trait, 但之所以说它们是标准而不是 trait, 是因为我们定义的闭包的函数签名不会因为我们要实现其中的任意一种而变得不一样, 而且我们也不需要去手动实现这三个 trait 的方法, 编译器会根据你写的闭包的实际代码来判断你的这个闭包符合以上三个当中的哪个标准。而且这三个标准对于闭包对变量的使用限制是逐级增强的,在要求较低标准闭包的地方传入符合较高标准的闭包是允许的, 比如有个函数要求传入 FnOnce 级别的闭包, 你实际传入的是个可以符合 FnMut 标准的闭包,这是完全可以的, 但是反过来不行, 也就是人家要求高标准的闭包, 你传给人家个低标准的,编译器就会报错

标准是什么?

既然这哥仨是标准, 那它们的定义或者说要求是什么呢?编译器又是如何检测你提供的闭包是符合哪个标准的呢?

以下是摘自 the book 的原文:

Closures will automatically implement one, two, or all three of these Fn traits, in an additive fashion:

  1. FnOnce applies to closures that can be called at least once. All closures implement this trait, because all closures can be called. If a closure moves captured values out of its body, then that closure only implements FnOnce and not any of the other Fn traits, because it can only be called once.

  2. FnMut applies to closures that don’t move captured values out of their body, but that might mutate the captured values. These closures can be called more than once.

  3. Fn applies to closures that don’t move captured values out of their body and that don’t mutate captured values. These closures can be called more than once without mutating their environment, which is important in cases such as calling a closure multiple times concurrently. Closures that don’t capture anything from their environment implement Fn.

颠覆我三观的情况来了, 原来 FnOnce 中的 Once 是指的至少被调用一次, 我一直是认为这是只能调用一次的意思…

这里明确提到闭包会自动的实现其中的一种、两种或者三种全部都实现,注意人家说的是in an additive fashion, 所以是叠加的,FnOnce 是基础, 我一个闭包可以实现 FnOnce 或者(FnOnce 和 FnMut)甚至(FnOnce、FnMut 和 Fn), 但绝对不可能只实现(FnOnce 和 Fn)或者(FnMut 和 Fn)。

我们分别看一下这三个具体都是什么要求

FnOnce: 闭包的最低标准, 所有闭包至少要符合这一条, 至少被调用一次。如果有所有变量权从闭包外转移到闭包内的情况,那这个闭包一定是符合 FnOnce 的

FnMut: 如果没有所有权从外转移到内部的情况,但是闭包内有可能用到 mut reference 来修改闭包外的变量的情况。该种闭包可以被调用多次

Fn: 不转移所有权也没有 mut reference 的闭包。可以被调用多次,而且可以被并发调用, 因为它不会改变任何上下文环境中的变量

很简单是吧?基本就是对外界变量的取用方式的区别, 编译器也正是用这些标准来检查你所提供的闭包的

但是有一点要注意, 就是闭包对变量的拿取或者借用是在闭包定义的地方就发生的,而不是在执行的时候才发生, 以下是 the book 中的例子:

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {:?}", list);

    let only_borrows = || println!("From closure: {:?}", list);

    println!("Before calling closure: {:?}", list);
    only_borrows();
    println!("After calling closure: {:?}", list);
}

以上代码中的 only_borrows 是一个只 borrow 了变量 list 的闭包, 代码可以正常编译运行

fn main() {
    let mut list = vec![1, 2, 3];
    println!("Before defining closure: {:?}", list);


    let mut borrows_mutably = || list.push(7);

    borrows_mutably();
    println!("After calling closure: {:?}", list);
}

以上代码中的 borrows_mutably 是一个 mut borrow 了变量 list 的闭包, 也就是相当于到闭包中用到了&mut list, 大家注意对比的话可以看到那行println!("Before calling closure: {:?}", list);不见了, 那是因为如果添加上这行,编译器会报错,因为 println!会用到&list, 而从let mut borrows_mutably = || list.push(7);这行开始,&mut list 就已经产生了, 如果添上原有的那行 before calling, 就违反了 mut reference 只能有一个的原则

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值