Rust 学习笔记(23)-闭包

参考章节《Rust 程序设计语言》第13.1章 闭包:可以捕获环境的匿名函数

闭包是可以保存在一个变量中或作为参数传递给其他函数的匿名函数。

我们先来看一个简单的闭包的例子

1
2
3
4
5
6
7
fn main() {
    let hello = |world: &str| {
        println!("Hello {}", world);
    };

    hello("Rust")
}

闭包的定义以一对竖线 (|) 开始,在竖线中指定闭包的参数,如果有多于一个参数,可以使用逗号分隔,比如 |param1, param2|
参数之后是存放闭包体的大括号,如果闭包体只有一行则大括号是可以省略的。

例如,如下写法与上面的写法等价

1
2
3
4
5
fn main() {
    let hello = |world: &str| println!("Hello {}", world);

    hello("Rust")
}

看到这里你肯定会问,这和函数有什么区别?

请看如下例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fn main() {
    let hello = create_str("Hello World!"); // 调用函数获取返回值

    if true {
        println!("{}", hello)
    } else {
        // 无论如何,就算这个代码快没有引用 hello,但它依然要等待create_hello函数执行
        println!("false")
    }
}

fn create_str(s: &str) -> String {
    String::from(s)
}

使用闭包

1
2
3
4
5
6
7
8
9
fn main() {
    let create_str = |world: &str| String::from(world); // 定义闭包

    if true {
        println!("{}", create_str("Hello World!")) // 在使用时才调用
    } else {
        println!("false")
    }
}

闭包不要求像 fn 函数那样在参数和返回值上注明类型。
闭包通常很短,并只关联于小范围的上下文而非任意情境。在这些有限制的上下文中,编译器能可靠的推断参数和返回值的类型,类似于它是如何能够推断大部分变量的类型一样。

闭包会捕获其环境

我们来看一个例子,这里书上这一节讲的非常清楚,因此我就照搬过来了

1
2
3
4
5
6
7
8
9
fn main() {
    let x = 4;

    let equal_to_x = |z| z == x; // 闭包使用了 x

    let y = 4;

    assert!(equal_to_x(y));
}

这里,即便 x 并不是 equal_to_x 的一个参数,equal_to_x 闭包也被允许使用变量 x,因为它与 equal_to_x 定义于相同的作用域。

函数则不能做到同样的事,如果尝试如下例子,它并不能编译

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 这段代码不能编译
fn main() {
    let x = 4;

    fn equal_to_x(z: i32) -> bool {
        z == x
    }

    let y = 4;

    assert!(equal_to_x(y));
}

闭包捕获环境是有代价的

当闭包从环境中捕获一个值,闭包会在闭包体中储存这个值以供使用。
这会使用内存并产生额外的开销,一般情况下,当我们不需要闭包来捕获环境时,我们不希望产生这些开销。
就像函数一样,因为函数从未允许捕获环境,定义和使用函数也就从不会有这些额外开销。

闭包可以通过三种方式捕获其环境,他们直接对应函数的三种获取参数的方式:获取所有权可变借用不可变借用

这三种捕获值的方式被编码为如下三个 Fn trait

FnOnce 消费从周围作用域捕获的变量,闭包周围的作用域被称为其 环境,environment。

为了消费捕获到的变量,闭包必须获取其所有权并在定义闭包时将其移动进闭包。
其名称的 Once 部分代表了闭包不能多次获取相同变量的所有权的事实,所以它只能被调用一次。

FnMut 获取可变的借用值所以可以改变其环境
Fn 从其环境获取不可变的借用值

当创建一个闭包时,Rust 根据其如何使用环境中变量来推断我们希望如何引用环境。这一切都是自动的。也就是说Rust自动判断这个闭包需要实现哪些 Fn trait

强制闭包获取所有权

如果你希望强制闭包获取其使用的环境值的所有权,可以在参数列表前使用 move 关键字。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 这段代码不能编译
fn main() {
    let x = vec![1, 2, 3];

    let equal_to_x = move |z| z == x; // 获取了x的所有权

    println!("can't use x here: {:?}", x); // 这里就不能再用了

    let y = vec![1, 2, 3];

    assert!(equal_to_x(y));
}

看懂上面这些后,再回过头来看书上的例子,你就能看懂了,不然你应该和我一样搞不懂书上举那个例子到底什么意思

它这个例子的目的是什么? 答,主要是为了避免多次调用闭包函数产生多于的开销,所以实现这个用来缓存结果

  1. 首先它定义了一个结构体,这个结构体有两个成员,一个成员存储闭包函数,一个成员存储闭包函数的结果
1
2
3
4
5
6
7
struct Cacher<T>
where
    T: Fn(u32) -> u32, // 这里就定义了 T 类型,必须是实现了 `Fn trait` 也就是不需要捕获环境
{
    calculation: T, // 闭包函数
    value: Option<u32>, // 闭包函数结果
}
  1. 定义方法,这段代码应该都能看懂吧,如果 value 有值就直接返回,否则就调用闭包函数获取值
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
impl<T> Cacher<T>
where
    T: Fn(u32) -> u32,
{
    fn new(calculation: T) -> Cacher<T> {
        Cacher {
            calculation,
            value: None,
        }
    }

    fn value(&mut self, arg: u32) -> u32 {
        match self.value {
            Some(v) => v,
            None => {
                let v = (self.calculation)(arg);
                self.value = Some(v);
                v
            }
        }
    }
}

书中还有一个更深入的例子,我就不写了,感觉它这个例子和闭包没什么关系,主要是让你知道可以通过 Fn trait 之类的 trait 来限制闭包的类型

不过我还是把代码贴上来,你应该能看得懂,很简单

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 上面的Cacher实现2个小问题
// 1.当我们调用过一次expensive_result.value(intensity)
//   后其值就已经存储在Some中,之后无论我们再传什么参数进去也是之前的值,也就是说我们无法修改。
// 2.第二个问题是它被限制为只接受获取一个 u32 值并返回一个 u32 值的闭包。
//   我们可以尝试引入更多泛型参数来增加 Cacher 功能的灵活性。

struct Cacher<T, U>
where
    T: Fn(U) -> U,
    U: Copy,
{
    calculation: T,
    value: HashMap<String, U>,
}

impl<T, U> Cacher<T, U>
where
    T: Fn(U) -> U,
    U: Copy,
{
    fn new(calculation: T) -> Cacher<T, U> {
        let value = HashMap::new();
        Cacher { calculation, value }
    }
    fn value(&mut self, key: &String, arg: U) -> U {
        let v = (self.calculation)(arg);
        self.value.insert(key.clone(), v);
        v
    }
}

总结一下

  1. 闭包类似匿名函数
  2. 闭包不需要显示注明类型和返回值,Rust 可以自动推断
  3. 闭包会捕获环境变量,Rust会根据情境实现 Fn FnMut FnOnce
updatedupdated2025-03-012025-03-01