Skip to main content

C13 函数式编程功能: 迭代器和闭包

函数式风格的编程中, 通常意味着将函数作为参数/返回值或是赋值给变量使用. 本章主要讲 Rust 提供的一些函数式编程功能:

  • Closure: 类似函数的结构, 可以存储在变量中.
  • Iterator: 用于处理一系列值.

闭包(Closure)

Rust 中闭包是一种匿名函数, 可以存储在变量中或作为参数传给另外一个函数. 和普通函数不同的是: 闭包可以捕捉它可见作用域中的值.

闭包语法如下:

// 无参数无返回值, 单一调用
let c1 = || dosomething();
// 参数和返回值
let c2 = |num: u32| -> u32 {
num = num + 1;
num
};

// 使用
c1();
c2(3);

passing_as_argument(c2);

闭包的上下文捕捉

闭包能够通过三种方式捕捉上下文, 和函数的三种传参方式类似:

  • 捕捉不可变引用
  • 捕捉可变引用
  • 获取所有权(move)

如下是三种情况的例子:

// 捕捉不可变引用: 因其中只需要该变量的不可变引用
let list = vec![1, 2, 3];
let only_borrows = || println!("From closure: {list:?}");

// 捕捉可变引用:
let list1 = vec![2, 3, 4];
let mut borrows_mutably = || list1.push(5);

// 获取所有权
let list2 = vec![3, 4, 5];
thread::spawn(move || println!("From thread: {list2:?}")).join().unwrap();

闭包三类型

结合上面提到的上下文捕捉行为, 以及闭包内生成的值是否被 move 出闭包(类似函数返回时的 move), Rust 中定义了闭包的三种 trait:

  • FnOnce: 自动应用到只可能被调用一次的闭包. 所有的闭包都至少实现此 trait. 且如果闭包会将闭包体内的值 move 出闭包, 则该闭包只会是此 trait 的实现.
  • FnMut: 自动应用到不会把值 move 出闭包体, 且可能会修改捕捉到的值的闭包. 此种闭包可以被反复调用.
  • Fn: 自动应用到不会把值 move 出闭包体, 且不会修改捕捉到的值, 或不会捕捉(使用)上下文中值的闭包. 此种类型闭包常用在多个线程调用的情况下, 因它不会改变其所处上下文环境. 此种闭包可以被反复调用.

"只能被调用一次"是指: 若想再次调用相同的闭包代码, 则该闭包必须被重新创建(而非创建后被反复调用).

比如:

pub fn unwrap_or_else<F>(self, f: F) -> T
where
F: FnOnce() -> T
// 这里 FnOnce 意味着 f 只能在此位置调用一次, 不能反复调用.
// 且 f 是无参数, 返回 T 的(意味着会把值从闭包体中 move 出去).
{
match self {
Some(x) => x,
None => f(),
}
}

此外, 函数也可以实现上述三种 trait. 意味着可以传入符合条件的函数作为闭包:

// 如上述的 unwrap_or_else, 就可以传入 Vec::new 函数, new 函数也是个无参, 有返回值的函数. 返回的值被 move 出函数.
// 即 Vec::new 是符合 FnOnce() -> T 类型的.
unwrap_or_else(Vec::new)

The version of the call operator that takes a by-value receiver.

Instances of FnOnce can be called, but might not be callable multiple times. Because of this, if the only thing known about a type is that it implements FnOnce, it can only be called once.

FnOnce is implemented automatically by closures that might consume captured variables, as well as all types that implement FnMut, e.g., (safe) [function pointers] (since FnOnce is a supertrait of FnMut).

Since both Fn and FnMut are subtraits of FnOnce, any instance of Fn or FnMut can be used where a FnOnce is expected. Use FnOnce as a bound when you want to accept a parameter of function-like type and only need to call it once. If you need to call the parameter repeatedly, use FnMut as a bound; if you also need it to not mutate state, use Fn.

rust book 中的解释实际不好理解, 更容易的是看代码文档中的解释(上面的英文).

  1. 闭包会 consume 它捕捉的值, 或任意实现 FnMut 的闭包, 都是 FnOnce, FnOnce 是子集.
  2. FnFnMut 都是 FnOnce 的 subtrait, 即任何需要 FnOnce 的地方, 都可以传入 FnFnMut.
  3. 若不改变上下文状态, 使用 Fn, 若改变状态但不 consume 捕捉值, 使用 FnMut.

典型例子:

let mut sort_operations = vec![];
let value = String::from("closure called");
list.sort_by_key(|r| {// sort_by_key 需要的是一个 FnMut, 即不会 consume 上下文值, 不会 move 值出闭包, 但会修改上下文状态.
sort_operations.push(value); // consume 了上下文的 value(将 value move 到了其他地方), 因此这个闭包是 FnOnce
r.width
});

关于闭包类型的总结总结:

  1. 以一个形象的例子看: FnOnce 是 动物, FnMut 是 狗, Fn 是 哈士奇, 限制越来越严格(越严格才可以重复调用).
  2. Fn: 不改变上下文, 不 consume 上下文值, 不 move in/out, 因此可重复调用, 且多线程环境安全.
  3. FnMut: 改变上下文, 不 consume 上下文值, 不 move in/out, 因此可重复调用.
  4. FnOnce: 改变上下文, 可 consume 上下文值, 可 move in/out, 因此只能调用一次.

迭代器(Iterator)

迭代器用于在一个元素序列上执行逐个操作. 和其他语言不同的是, Rust 中的迭代器是 lazy 的, 意味着它只有在被 consume 的时候才会真正被执行.

下面的例子演示一个没有被执行的迭代器:

let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();

我们将迭代器存放在 v1_iter 变量中.

此外, for-in 循环底层实际是: 创建迭代器并 consume, 从而对每个元素进行操作.

迭代器的实现

Rust 提供了一个 Iterator trait, 如下所示:

pub trait Iterator {
type Item;

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

// ... 忽略其他有默认实现的方法...
}

其中 ItemIterator 的关联类型, 这里表示的是被迭代序列中的元素类型. Iterator trait 要求实现 next 方法, 在迭代器运行过程中, 对元素的列举即通过 next 方法进行, 当结束时, next 会返回 None.

外界可以直接调用 next, 在某些特殊情况下比较有用(比如单独列举两个符合条件的元素出来并存入不同变量中, 且我们是已知该位置元素的含义时)

下面即为一个直接调用 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);
}

需要注意的是, 我们必须让迭代器是 mut 的, 因为在调用 next 的过程中, 迭代器内部状态会改变(比如更新"当前"下标). 但把迭代器用在 for-in loop 时则无需显式指定 mut 迭代器, 因 for-in loop 底层会自动做这个事情.

而调用 next 时, 我们就是在 consume 迭代器, 可以形象理解为每调用一次 next, 我们的迭代器就被"吃掉"一段.

另外需要注意, 上述 iter 得到的迭代器, 获取到的元素是该元素的不可变引用. 若想将序列中每个元素都 move 出来, 可以使用 into_iter. 而如果想每个元素都是可变引用, 则使用 iter_mut.

用于 consume 迭代器的一些方法: Consuming Adapters

除了上述 next, 还有许多方法可以"消费"迭代器, 这些消费迭代器的方法, 又被称为 consuming adapters. 因为它们内部都是去调用 next, 且调用它们时, 会将迭代器的 ownership move 到方法内.

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

用于在一个迭代器上创建新迭代器的方法: Iterator Adapters

这些方法并不 consume 迭代器(不调用 next), 而是通过之前的迭代器生成新的迭代器.

比如 map 方法, 用于元素变换:

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

// 不 consume 迭代器, 而是生成一个新的迭代器
let map_v1 = v1.iter().map(|x| x + 1);

// consume 迭代器, 得到新的序列:
let new_vec = map_v1.collect(); // new_vec 为 [2, 3, 4]

// 当然上述可以合成为一个连续调用:
let new_vec = v1.iter().map(|x| x + 1).collect();

实际开发时, 可以通过 iter, iter_mut, into_iter 创建初始迭代器, 然后再链接多个 iterator adapter (map, filter, reduce...), 最后再 consume 结果迭代器(collect, next...), 从而实现复杂的计算操作.

许多 iterator adapter 都使用闭包作为参数(比如上面的 map), 闭包可以捕捉上下文环境, 例如下面代码, 可以直接在闭包中使用函数参数:

fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
// 过滤所有鞋子尺码匹配 shoe_size 的鞋
shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}

迭代器和循环的选择

一般来说, 迭代器更易懂易读, 代码行数少, 优先选择. 而需要精细控制时才使用循环.

此外, 迭代器版本一般来说都会稍微快一些(运行速度), 原理上讲, 编译器能够理解迭代器, 从而生成比循环更高效的代码. 比如 Rust 的 Unrolling 技术, 它可以不生成循环控制代码从而减少开销(而是直接生成对应每个循环的重复代码块, 这样就把循环变为了顺序执行的多个相同代码).