Rust 中的函数式语言功能:迭代器与闭包

对原文做了删减,原文参考Rust 中的函数式语言功能:迭代器与闭包


闭包和迭代器是 Rust 受函数式编程语言观念所启发的功能,对 Rust 以高性能来明确的表达高级概念的能力有很大贡献。
闭包和迭代器的实现达到了不影响运行时性能的程度,这正是 Rust 竭力提供零成本抽象的目标的一部分。

闭包:可以捕获环境的匿名函数

Rust 的 闭包(closures) 是可以保存在一个变量中或作为参数传递给其他函数的匿名函数,可以在一个地方创建闭包然后在不同的上下文中执行闭包运算。不同于函数,闭包允许捕获被定义时所在作用域中的值。

闭包会捕获其环境

衬衫公司赠送场景示例代码:

//使用有 Red 和 Blue 两个成员的 ShirtColor 枚
#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
    Red,
    Blue,
}
//使用 Inventory 结构体来代表公司的库存
struct Inventory {
    //表示库存中的衬衫的颜色
    shirts: Vec<ShirtColor>,
}

//获取免费衬衫得主所喜爱的颜色(如有),并返回其获得的衬衫的颜色
impl Inventory {
    fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
        //如果被选中的成员设置了喜爱的颜色,将获得那个颜色的 T 恤
        //如果没有设置喜爱的颜色,会获赠公司现存最多的颜色的款式
        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
    );
}

Option<T> 上的方法 unwrap_or_else 由标准库定义,它获取一个没有参数、返回值类型为 T (与 Option<T> 的 Some 成员所存储的值的类型一样:

  • 如果 Option<T> 是 Some 成员,则 unwrap_or_else 返回 Some 中的值。
  • 如果 Option<T> 是 None 成员,则 unwrap_or_else 调用闭包并返回闭包的返回值。

将被闭包表达式 || self.most_stocked() 用作 unwrap_or_else 的参数,这是一个本身不获取参数的闭包(如果闭包有参数,它们会出现在两道竖杠之间)。运行代码会打印出:

The user with preference Some(Red) gets Red
The user with preference None gets Blue

闭包捕获了一个 Inventory 实例的不可变引用到 self,并连同其它代码传递给 unwrap_or_else 方法。相比之下,函数就不能以这种方式捕获其环境。

闭包类型推断和注解

闭包并不总是要求像 fn 函数那样在参数和返回值上注明类型,也有编译器需要闭包类型注解的罕见情况。
定义了一个闭包并将它保存在变量中,而不是在传参的地方定义它:

//为闭包的参数和返回值增加可选的类型注解
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  ;  //去掉了可选的大括号,因为闭包体只有一个表达式

注:调用闭包是 add_one_v3 和 add_one_v4 能够编译的必要条件,因为类型将从其用法中推断出来。这类似于 let v = Vec::new();,Rust 需要类型注解或是某种类型的值被插入到 Vec 才能推断其类型。

编译器会为闭包定义中的每个参数和返回值推断一个具体类型,尝试调用一个被推断为两个不同类型的闭包会得到一个错误

let example_closure = |x| x;
let s = example_closure(String::from("hello")); //使用 String 类型作为参数
let n = example_closure(5);                     //使用 u32 类型作为参数

捕获引用或者移动所有权

闭包可以通过三种方式捕获其环境(函数获取参数的三种方式):不可变借用可变借用获取所有权,闭包会根据函数体中如何使用被捕获的值决定用哪种方式捕获。

定义了一个捕获名为 list 的 vector 的不可变引用的闭包,因为只需不可变引用就能打印其值:

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 vector 增加一个元素,闭包现在捕获一个可变引用:

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

即使闭包体不严格需要所有权,也可以在参数列表前使用 move 关键字强制闭包获取它用到的环境中值的所有权。使用 move 来强制闭包为线程获取 list 的所有权:

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导致线程中的不可变引用将失效。

将被捕获的值移出闭包和 Fn trait

闭包体可以做以下任何事:将一个捕获的值移出闭包,修改捕获的值,既不移动也不修改值,或者一开始就不从环境中捕获值

闭包捕获和处理环境中的值的方式影响闭包实现的 trait,Trait 是函数和结构体指定它们能用的闭包的类型的方式。
取决于闭包体如何处理值,闭包自动、渐进地实现一个、两个或三个 Fn trait

  • FnOnce 适用于能被调用一次的闭包,所有闭包都至少实现了这个 trait,因为所有闭包都能被调用。一个会将捕获的值移出闭包体的闭包只实现 FnOnce trait,这是因为它只能被调用一次。
  • FnMut 适用于不会将捕获的值移出闭包体的闭包,但它可能会修改被捕获的值。这类闭包可以被调用多次。
  • Fn 适用于既不将被捕获的值移出闭包体也不修改被捕获的值的闭包,当然也包括不从环境中捕获值的闭包。这类闭包可以被调用多次而不改变它们的环境,这在会多次并发调用闭包的场景中十分重要。

看看 Option<T> 上的 unwrap_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(),
        }
    }
}

泛型 F 的 trait bound 是 FnOnce() -> T,这意味着 F 必须能够被调用一次,没有参数并返回一个 T。

注意:函数也可以实现所有的三种 Fn traits,如果不需要从环境中捕获值则可以使用函数而不是闭包。举个例子,可以在 Option<Vec<T>> 的值上调用 unwrap_or_else(Vec::new) 以便在值为 None 时获取一个新的空的 vector。

看看 slice 上的标准库方法 sort_by_key,使用 sort_by_key 对长方形按宽度排序:

#[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 },
    ];

    //按 Rectangle 的 width 属性对它们从低到高排序
    list.sort_by_key(|r| r.width);
    println!("{:#?}", list);
}

sort_by_key 被定义为接收一个 FnMut 闭包的原因是它会多次调用这个闭包:每个 slice 中的元素调用一次。闭包 |r| r.width 不捕获、修改或将任何东西移出它的环境,所以它满足 trait bound 的要求。

尝试在 sort_by_key 上使用一个 FnOnce 闭包会编译错误:

// --snip--
let mut sort_operations = vec![];
let value = String::from("by key called");

//这个闭包只实现了 FnOnce,value 使用一次就会被移出
list.sort_by_key(|r| {
   //尝试统计排序 list 时 sort_by_key 被调用的次数
    sort_operations.push(value);
    r.width
});
// --snip--

在环境中保持一个计数器并在闭包体中增加它的值是计算 sort_by_key 被调用次数的一个更简单直接的方法:

// --snip--
let mut num_sort_operations = 0;
list.sort_by_key(|r| {
    num_sort_operations += 1;
    r.width
});
// --snip--

使用迭代器处理元素序列

迭代器模式允许你对一个序列的项进行某些处理,迭代器(iterator)负责遍历序列中的每一项和决定序列何时结束的逻辑

Rust 中迭代器是 惰性的(lazy),在调用方法使用迭代器之前它都不会有效果。以下代码没有任何用处:

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

//for循环没使用前,此行代码没有任何用处
let v1_iter = v1.iter();  

for val in v1_iter {
    println!("Got: {}", val);
}

Iterator trait 和 next 方法

迭代器都实现了一个叫做 Iterator 的定义于标准库的 trait,定义看起来像这样:

pub trait Iterator {
    type Item; //关联类型

    fn next(&mut self) -> Option<Self::Item>;

    // 此处省略了方法的默认实现
}

next 是 Iterator 实现者被要求定义的唯一方法。next 一次返回迭代器中的一个项,封装在 Some 中,当迭代器结束时,它返回 None。

可以在迭代器上(直接)调用 next 方法:

#[test]
fn iterator_demonstration() {
    let v1 = vec![1, 2, 3];

    let mut v1_iter = v1.iter();

    assert_eq!(v1_iter.next(), Some(&1));
    assert_eq!(v1_iter.next(), Some(&2));
    assert_eq!(v1_iter.next(), Some(&3));
    assert_eq!(v1_iter.next(), None);
}

注意 next 返回值的所有权:

  • iter 方法生成一个不可变引用的迭代器。
  • into_iter 方法生成一个拥有所有权的迭代器。
  • iter_mut 方法生成一个可变引用的迭代器。、

消费迭代器的方法

Iterator trait 有一系列不同的由标准库提供默认实现的方法,一些方法在其定义中调用了 next 方法,这些调用 next 方法的方法被称为 消费适配器(consuming adaptors)
一个消费适配器的例子是 sum 方法,一个展示 sum 方法使用的测试:

#[test]
fn iterator_sum() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

    let total: i32 = v1_iter.sum();

    assert_eq!(total, 6);
}

调用 sum 之后不再允许使用 v1_iter 因为调用 sum 时它会获取迭代器的所有权。

产生其他迭代器的方法

Iterator trait 中定义了另一类方法,被称为 迭代器适配器(iterator adaptors),它们允许我们将当前迭代器变为不同类型的迭代器。
可以链式调用多个迭代器适配器,因为所有的迭代器都是惰性的,必须调用一个消费适配器方法以便获取迭代器适配器调用的结果

调用 map 方法创建一个新迭代器,接着调用 collect 方法消费新迭代器,对其中 vector 中的每个元素都被加 1:

let v1: Vec<i32> = vec![1, 2, 3];

//调用迭代器适配器 map 来创建一个新迭代
//collect 方法消费迭代器并将结果收集到一个数据结构中
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

assert_eq!(v2, vec![2, 3, 4]);

使用捕获其环境的闭包

很多迭代器适配器接受闭包作为参数,而通常指定为迭代器适配器参数的闭包会是捕获其环境的闭包。

使用 filter 和一个捕获环境中变量 shoe_size 的闭包来遍历一个 Shoe 结构体集合,返回指定大小的鞋子:

#[derive(PartialEq, Debug)]
struct Shoe { size: u32, style: String, }

//获取一个鞋子 vector 的所有权和一个鞋子大小作为参数,返回一个只包含指定大小鞋子的 vector
fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {

    //调用了 into_iter 来创建一个获取 vector 所有权的迭代器    
    shoes.into_iter()
    //调用 filter 将这个迭代器适配成一个只含有那些闭包返回 true 的元素的新迭代器
    //(闭包从环境中捕获了 shoe_size 变量并使用其值与每一只鞋的大小作比较)    
    .filter(|s| s.size == shoe_size)
    //调用 collect 将迭代器适配器返回的值收集进一个 vector 并返回
    .collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn filters_by_size() {
        let shoes = vec![
            Shoe { size: 10, style: String::from("sneaker"), },
            Shoe { size: 13, style: String::from("sandal"), },
            Shoe { size: 10, style: String::from("boot"), },
        ];
        let in_my_size = shoes_in_size(shoes, 10);
        assert_eq!(in_my_size, vec![
            Shoe { size: 10, style: String::from("sneaker") },
            Shoe { size: 10, style: String::from("boot") },
        ]);
    }
}

改进 I/O 项目

使用迭代器改进IO项目中 Config::build 函数和 search 函数的实现。

使用迭代器并去掉 clone

原来 Config::build 函数的实现如下:

impl Config {
    pub fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 { return Err("not enough arguments"); }
        let query = args[1].clone();
        let file_path = args[2].clone();
        let ignore_case = env::var("IGNORE_CASE").is_ok();
        Ok(Config { query, file_path, ignore_case, })
    }
}

可以将 build 函数改为获取一个有所有权的迭代器作为参数而不是借用 slice,一旦获取了迭代器的所有权并不再使用借用的索引操作,就可以将迭代器中的 String 值移动到 Config 中,而不是调用 clone 分配新的空间。

直接使用返回的迭代器

不同于之前将迭代器的值收集到一个 vector 中接着传递一个 slice 给 Config::build,现在直接将 env::args 返回的迭代器的所有权传递给 Config::build

fn main() {
    let config = Config::build(env::args()).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    // --snip--
}

以迭代器作为参数更新 Config::build 的签名:

impl Config {
    //将 mut 关键字添加到 args 参数的规范中以使其可变
    pub fn build(
        mut args: impl Iterator<Item = String>,
    ) -> Result<Config, &'static str> {
        // --snip--

使用 Iterator trait 代替索引

修改 Config::build 的函数体来使用迭代器方法:

impl Config {
    pub fn build(mut args: impl Iterator<Item = String>) -> Result<Config, &'static str> {
        args.next();  //env::args 返回值的第一个值是程序的名称,忽略它并获取下一个值
        let query = args.next().unwrap_or_else(|| return Err("Didn't get a query string"));
        let file_path = args.next().unwrap_or_else(|| return Err("Didn't get a file path"));
        let ignore_case = env::var("IGNORE_CASE").is_ok();
        Ok(Config { query, file_path, ignore_case, })
    }
}

使用迭代器适配器来使代码更简明

I/O 项目中其他可以利用迭代器的地方是 search 函数,原函数定义如下:

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }
    results
}

可以通过使用迭代器适配器方法来编写更简明的代码:

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    contents
        .lines()
        //只保留 line.contains(query) 返回 true 的那些行
        .filter(|line| line.contains(query))
        .collect()
}

性能对比:循环 VS 迭代器

如下是 for 循环版本和迭代器版本的 search 函数在《福尔摩斯全集》上的性能测试结果:

test bench_search_for  ... bench:  19,620,300 ns/iter (+/- 915,700)
test bench_search_iter ... bench:  19,234,900 ns/iter (+/- 657,200)

迭代器是 Rust 的 零成本抽象(zero-cost abstractions)之一,它意味着抽象并不会引入运行时开销。

解码算法使用线性预测数学运算(linear prediction mathematical operation)来根据之前样本的线性函数预测将来的值来看一下示例:

let buffer: &mut [i32];      //一个叫 buffer 的数据 slice
let coefficients: [i64; 12]; //一个有 12 个元素的数组 coefficients
let qlp_shift: i16;          //个代表位移位数的 qlp_shift

for i in 12..buffer.len() {
    let prediction = coefficients.iter()
                                 .zip(&buffer[i - 12..i])
                                 .map(|(&c, &s)| c * s as i64)
                                 .sum::<i64>() >> qlp_shift;
    let delta = buffer[i];
    buffer[i] = prediction as i32 + delta;
}

为了计算 prediction 的值,这些代码遍历了 coefficients 中的 12 个值,使用 zip 方法将系数与 buffer 的前 12 个值组合在一起。接着将每一对值相乘,再将所有结果相加,然后将总和右移 qlp_shift 位。

热门相关:史上第一宠婚:慕少的娇妻   盛世娇宠之名门闺香   都市狐仙养成记   霍先生结婚吧   锦绣医妃之庶女凰途