Rust闭包

Rust的闭包(closures)是一种可以保存在变量或作为参数传递的匿名函数,与函数不同的是,它可以捕获调用者作用域中的值:

fn main() {
    let x = 1;
    let sum = |y| x + y;

    assert_eq!(sum(2), 3);
}

在上面的例子中,sum变量被赋值为一个闭包,这个闭包捕获了x变量的值。

闭包捕获的环境🔗

我们通过下面例子来了解这一点。例如,有Red和Blue两种成员的ShirtColor枚举,使用Inventory结构体来表示库存,Inventory结构体有一个类型为Vec<ShirtColor>的字段shirts,表示库存中的衬衫颜色,并定义giveaway方法获得免费衬衫得主喜爱的颜色,并返回颜色,代码如下:

#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
    Red,
    Blue,
}

struct Inventory {
    shirts: Vec<ShirtColor>,
}

impl Inventory {
    fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
        user_preference.unwrap_or_else(|| self.most_stocked())
    }

    fn most_stocked(&self) -> ShirtColor {
        let mut num_red = 0;
        let mut num_blue = 0;

        for color in &self.shirts {
            match color {
                ShirtColor::Red => num_red += 1,
                ShirtColor::Blue => num_blue += 1,
            }
        }

        if num_red > num_blue {
            ShirtColor::Red
        } else {
            ShirtColor::Blue
        }
    }
}

fn main() {
    let store = Inventory {
        shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
    };

    let user_pref1 = Some(ShirtColor::Red);
    let giveaway1 = store.giveaway(user_pref1);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref1, giveaway1
    );

    let user_pref2 = None;
    let giveaway2 = store.giveaway(user_pref2);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref2, giveaway2
    );
}

main函数里定义store变量还剩两件蓝衬衫和一件红衬衫,调用giveaway方法分别传入Some(ShirtColor::Red)和None参数,分别表示用户有红衬衫偏好和无衬衫偏好。

在giveaway方法中,使用unwrap_or_else方法来处理用户没有指定衬衫偏好的情况,unwrap_or_else方法接受一个无参闭包作为参数,该闭包返回ShirtColor类型的值。如果用户有衬衫偏好,unwrap_or_else方法会返回用户指定的衬衫颜色,否则会调用most_stocked方法来返回库存中最受欢迎的颜色。

将闭包表达式|| self.most_stocked()作为参数传递给unwrap_or_else方法,这是一个本身不获取参数的闭包(如果闭包有参数,它们会出现在||中间)。闭包体调用了self.most_stocked()方法。这里定义了闭包,而unwrap_or_else方法会在用户没有指定衬衫偏好时调用闭包。

我们传递的闭包会在当前Inventory实例上调用self.most_stocked()方法,闭包捕获了self(Inventory实例)的不可变引用,并将代码传递给unwrap_or_else方法。

闭包类型推断和注解🔗

闭包一般不要求对参数和返回值进行类型注解,因为函数是暴露给用户的显示接口的一部分,严格定义接口对确保所有人对函数使用和返回值的类型达成一致理解非常重要。而闭包不用于此,它们储存在变量中并被使用。

闭包通常较短,且与特定的上下文相关,编译器根据上下文推断参数和返回值类型。

如果添加了类型注解,代码会变得冗长,考虑下面代码:

let expensive_closure = |num: u32| -> u32 {
    println!("calculating slowly...");
    thread::sleep(Duration::from_secs(2));
    num
}

这个语法就更像是函数语法了,下面是闭包与函数语法的纵向对比:

fn  add_one_v1   (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

v3和v4需要调用才能编译,因为如果不调用,其类型无法被推断。

对于闭包,编译器会为每个参数和返回值推断一个具体类型,考虑下面代码:

let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);

这里在第一次调用闭包是使用了String类型,编译器会推断x的类型为String,这个类型就锁定进闭包example_closure中,在第二次调用闭包时使用了i32类型,编译器就会报错。

捕获引用或移动所有权🔗

闭包通过三种方式捕获环境的值,对应三种获取参数的方式:不可变借用、可变借用、获取所有权。考虑下面代码:

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);
}

这里展示一个变量可以绑定一个闭包,我们可以像函数名一样,使用变量名和括号来调用该闭包。

可以看到在闭包定义之后到调用之后,代码仍然可以访问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);
}
在这里闭包定义和调用之间就不能再有`println!`语句了,因为闭包定义时捕获的`list`的可变引用,而在闭包调用后就不再使用,可变借用结束,所以可以使用`println!`语句。

如果希望闭包将至获得所有权,可以在参数列表前使用`move`关键字,在将闭包传递到一个新线程时非常有用,考虑下面代码:
```rust
use std::thread;

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

    thread::spawn(move || println!("From thread: {:?}", list))
        .join()
        .unwrap();
}

我们我们生成了一个新线程,并给它传递一个闭包作为参数。这个例子中我们使用了move关键字来获取list的所有权,如果不这样做,可能在新线程结束之前list的不可变引用就会失效。

将被捕获的值移出闭包🔗

闭包捕获和处理环境中的值的方式会影响闭包实现trait,根据闭包体处理这些值的方式,闭包会自动、渐进地实现Fntrait。

  • FnOnce:这个trait适用于只能被调用一次的闭包。所有闭包都至少实现了这个trait,因为所有闭包都能被调用。一个会讲捕获的值移出闭包体的闭包只会实现这个trait而不实现其他Fn相关的trait。
  • FnMut:这个trait适用于不会将捕获的值移出闭包体,但可能会修改捕获值的闭包。这类闭包可以被调用多次。
  • Fn:这个trait适用于不会将捕获的值移出闭包体,也不会修改捕获值的闭包。这类闭包在会多次并发调用闭包的场景中十分重要。

下面是之前使用的unwarp_or_else方法的定义:

impl<T> Option<T> {
    pub fn unwrap_or_else<F>(self, f: F) -> T
    where
        F: FnOnce() -> T,
    {
        match self {
            Some(x) => x,
            None => f(),
        }
    }
}

unwrap_or_else方法接受一个闭包作为参数,这个闭包必须能被调用一次,没有参数,返回T类型的值。trait bound中使用了FnOnce表示unwrap_or_else方法最多只会调用闭包一次。

ℹ️ 注意

函数可以实现三种Fntrait。

现在我们看定义在slice上的标准库方法sort_by_key,这个这个方法使用FnMuttrait:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];
    list.sort_by_key(|r| r.width);
    println!("{:#?}", list);
}

sort_by_key方法接受一个FnMut类型的闭包作为参数的原因是它会多次调用这个闭包,而|r| r.width满足trait要求。

下面是只实现了FnOncetrait的闭包的例子:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];
    let mut sort_oprations = vec![];
    let value = String::from("closure called");

    list.sort_by_key(|r| {
        sort_oprations.push(value);
        r.width
    });
    println!("{:#?}", list);
}

这里闭包调用push方法时,value变量的所有权已经被移动到sort_oprations中,闭包不能再使用value变量,这个闭包只实现了FnOncetrait,所以会报错。

ℹ️ 注意

感觉有点像泛函。