Android程序员初学Rust-闭包

0 阅读5分钟

1.jpg

何为闭包?

闭包 是由捆绑起来(封闭的)的函数和函数周围状态(词法环境)的引用组合而成。换言之,闭包让函数能访问它的外部作用域。

闭包是在支持头等函数的编程语言中实现词法绑定的一种技术,在实现上是一个存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)结构体。

那么在 Rust 中,闭包是如何使用的呢?

语法

2.png

闭包是用竖线创建的:|..|

fn main() {
    // 对于轻量级语法,参数和返回类型可以推断
    let double_it = |n| n * 2;
    dbg!(double_it(50));

    // 也可以指定类型并将代码体加上括号以使其完全清晰明确:
    let add_1f32 = |x: f32| -> f32 { x + 1.0 };
    dbg!(add_1f32(50.));
}

参数放在 |..| 之间。代码体可以用 {..} 括起来,但如果它是单个表达式,这些括号可以省略。

参数类型是可选的,如果未指定则会进行推断。返回类型也是可选的,但只有在代码体周围使用 {..} 时才能写出返回类型。

这些示例也都可以仅写成嵌套函数的形式——它们不会从词法环境中捕获任何变量。接下来我们看看捕获变量的情况。

捕获

3.jpg

闭包可以从其定义的环境中捕获变量:

fn main() {
    let max_value = 2;
    let clamp = |v| {
        if v > max_value {
            max_value
        } else {
            v
        }
    };

    dbg!(clamp(1));
    dbg!(clamp(3));
}

// Output
// [src/main.rs:11:5] clamp(1) = 1
// [src/main.rs:12:5] clamp(3) = 2

默认情况下,闭包通过引用捕获变量。在这里,max_valueclamp 捕获,而 main 函数仍可使用它来进行打印。

如果闭包对值进行了可变操作,它会通过可变引用捕获这些值。

我们尝试一下修改 max_value 的值:

fn main() {
    let mut max_value = 2; // mut
    let mut clamp = |v| { // clamp 也需要声明为 mut
        if v > max_value {
            max_value *= 10; // 修改值
            max_value
        } else {
            v
        }
    };
    
    // compile error: cannot borrow `max_value` as immutable because it is also borrowed as mutable
    // println!("{}", max_value); 
    
    dbg!(clamp(1));
    dbg!(clamp(3));
    
    println!("{}", max_value); // Ok here
}

可以看到,要想代码编译过,首先,我们要声明 max_value 是可变的,同时,clamp 也需要是可变的。

如果我们想打印 max_value 的值,我们必须在可变引用的作用范围之外才能使用。上一文我们了解到借用检查规则,可变应用是独占的,这儿得到了体现。

可以使用 move 关键字强制闭包转移值而不是引用值。这对生命周期管理很有帮助,例如,如果闭包的生命周期必须长于被捕获的值(稍后会详细介绍生命周期)。

默认情况下,闭包会以对外部作用域中每个变量要求最低的访问形式捕获它们(如果可能,优先通过共享引用捕获,其次是独占引用,然后是值转移)。move 关键字强制通过值转移进行捕获 。

fn main() {
    let str = String::from("Hello");
    
    let greet = move |v| {
        println!("{} {}",str, v);
    };
    
    greet("World");
    greet("Rust");
    
    // println!("{}", str); // compile error: borrow of moved value: `str`
}

当我们使用 move 强制闭包转移值时,main 将不再能使用 str

闭包 trait

4.png

闭包或 lambda 表达式具有无法命名的类型。不过,它们实现了特殊的 FnFnMutFnOnce 特征。

特殊类型 fn(..) -> T 指的是函数指针——要么是函数的地址,要么是不捕获任何变量的闭包。

fn apply_and_log(
    func: impl FnOnce(&'static str) -> String,
    func_name: &'static str,
    input: &'static str,
) {
    println!("Calling {func_name}({input}): {}", func(input))
}

fn main() {
    let suffix = "-itis";
    let add_suffix = |x| format!("{x}{suffix}");
    apply_and_log(&add_suffix, "add_suffix", "senior");
    apply_and_log(&add_suffix, "add_suffix", "appenix");

    let mut v = Vec::new();
    let mut accumulate = |x| {
        v.push(x);
        v.join("/")
    };
    apply_and_log(&mut accumulate, "accumulate", "red");
    apply_and_log(&mut accumulate, "accumulate", "green");
    apply_and_log(&mut accumulate, "accumulate", "blue");

    let take_and_reverse = |prefix| {
        let mut acc = String::from(prefix);
        acc.push_str(&v.into_iter().rev().collect::<Vec<_>>().join("/"));
        acc
    };
    apply_and_log(take_and_reverse, "take_and_reverse", "reversed: ");
}

上述代码输出如下:

// Output
Calling add_suffix(senior): senior-itis
Calling add_suffix(appenix): appenix-itis
Calling accumulate(red): red
Calling accumulate(green): red/green
Calling accumulate(blue): red/green/blue
Calling take_and_reverse(reversed: ): reversed: blue/green/red

Fn 类型的闭包(例如 add_suffix)既不会消耗也不会修改捕获的值。调用它时只需要对闭包的共享引用,这意味着该闭包可以重复甚至并发执行。

FnMut 类型的闭包(可以理解为 mut Fn,例如 accumulate)可能会修改捕获的值。通过独占引用访问闭包对象,因此它可以重复调用,但不能并发调用 。

如果有一个 FnOnce 类型的闭包(例如 take_and_reverse),则只能调用一次。调用它会消耗该闭包以及通过值转移捕获的所有值。

FnMutFnOnce 的子类型。FnFnMutFnOnce 的子类型。也就是说,在需要 FnOnce 的地方可以使用 FnMut,在需要 FnMutFnOnce 的地方可以使用 Fn

千万不要记忆谁是谁的子类型,虽然上述文段确实是这么表达的,但是一旦你记忆这些,后续很容易混淆。我们想象一下设计模式中的里氏替换原则:程序中任何使用父类对象的地方,都应该能够透明地替换为子类对象,且行为逻辑保持一致。所以此处,如果一个需要 FnOnce 的地方可以使用 FnMut,那么证明 FnMutFnOnce 的子类型。

当定义一个接受闭包的函数时,如果可能的话,应该接受 FnOnce 类型的闭包(即只调用一次),否则接受 FnMut 类型的,最后才考虑 Fn 类型的。这样可以为调用者提供最大的灵活性。

相反,当拥有一个闭包时,灵活性最高的是 Fn 类型(它可以传递给接受这三种闭包特征中任何一种的函数),其次是 FnMut 类型,最后是 FnOnce 类型。

编译器还会根据闭包捕获的内容推断出 Copy(例如对于 add_suffix)和 Clone(例如对于 take_and_reverse)。函数指针(对 fn 项的引用)实现了 CopyFn