函数式语言特性:闭包与迭代器
Auto 的设计借鉴了许多现有语言和技术,其中一个重要的影响来源是_函数式编程_。函数式编程风格通常包括将函数作为值传递、从其他函数返回、赋值给变量以供后续执行等。
本章将介绍:
- 闭包,可以存储在变量中的匿名函数
- 迭代器,处理一系列元素的方式
- 如何用闭包和迭代器改进第 12 章的 I/O 项目
- 闭包和迭代器的性能
闭包
闭包是可以保存在变量中或作为参数传递给其他函数的匿名函数。与普通函数不同,闭包可以捕获定义它的作用域中的值。
闭包语法
Auto 闭包使用 (params) => expr 语法:
let add = (a int, b int) => a + b
let result = add(5, 3)
print(result)let add = |a: i32, b: i32| a + b;
let result = add(5, 3);
println!("{}", result);这等价于 Rust 的 |params| expr 语法。闭包也可以有显式返回类型:
let expensive_closure = (num int) -> int {
print("calculating slowly...")
num
}let expensive_closure = |num: i32| -> i32 {
println!("calculating slowly...");
num
};捕获环境
闭包可以从其环境中捕获值。以下是一个使用 unwrap_or_else 的示例,其中闭包捕获了 self:
闭包 (self) => self.most_stocked() 捕获了 self 的不可变引用并调用 most_stocked()。?T(Option)上的 unwrap_or_else 方法仅在值为 None 时 才调用闭包。
类型推断
闭包通常不需要类型标注——编译器会从使用中推断:
编译器从第一次调用推断 x 的类型为 String。一旦推断完成,闭包的类型就被 锁定——用不同的类型调用它会导致编译错误。
捕获引用或转移所有权
闭包可以通过三种方式捕获值,对应函数接收参数的三种方式:
- 不可变引用 — 闭包只读取值
- 可变引用 — 闭包修改值
- 所有权 — 闭包获取所有权(Rust 中使用
move)
下面是一个捕获不可变引用的闭包:
闭包 only_borrows 通过不可变引用捕获 list。原始 list 在闭包调用前、 调用中和调用后都可以访问。
以及一个捕获可变引用的闭包:
当 borrows_mutably 捕获可变引用时,在闭包定义和调用之间不允许对 list 的其他访问。
Fn 特征
与 Rust 一样,闭包实现一个或多个 Fn 特征:
FnOnce— 只能调用一次(移出捕获的值)FnMut— 可以多次调用,可能修改捕获的值Fn— 可以多次调用且不修改捕获的值
这些特征在将闭包传递给函数或存储在类型中时很重要。
迭代器
迭代器提供了一种处理元素序列的方式。在 Auto 中,for 循环是迭代的主要方式:
for item in collection {
print(item)
}for item in &collection {
println!("{}", item);
}for 循环
for 循环适用于数组、列表、范围以及任何实现迭代器模式的类型:
// 遍历数组
let arr = [1, 2, 3]
for n in arr {
print(n)
}
// 遍历范围
for i in 0..10 {
print(i)
}let arr = [1, 2, 3];
for n in arr {
println!("{}", n);
}
for i in 0..10 {
println!("{}", i);
}在底层,for 循环使用迭代器。循环获取迭代器的所有权,重复调用 next() 直到返回 None。
迭代器方法
注意:以下迭代器方法是 Rust
Iterator特征的标准方法。当使用 Auto 的use.rust时,可以直接在实现Iterator的类型上访问这些方法。
创建迭代器
| 方法 | 描述 | 所有权 |
|---|---|---|
.iter() | 遍历引用 | 借用每个元素 |
.iter_mut() | 遍历可变引用 | 可变借用每个元素 |
.into_iter() | 遍历拥有的值 | 获取所有权 |
消费适配器
消费适配器调用 next() 并消耗迭代器:
// sum — 消耗迭代器并返回总和
let v1 = vec![1, 2, 3];
let total: i32 = v1.iter().sum(); // 6迭代器适配器
迭代器适配器转换迭代器但不消耗它。它们是_惰性的_——在消费适配器被调用之前 不会执行任何操作:
// map — 转换每个元素(惰性!)
let v1 = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
// v2 为 [2, 3, 4]
// filter — 保留匹配谓词的元素(惰性!)
let shoes: Vec<Shoe> = shoes.into_iter()
.filter(|s| s.size == shoe_size)
.collect();在迭代器方法中使用闭包
迭代器方法通常接受闭包作为参数。以下示例使用 filter 和一个从环境中捕获 shoe_size 的闭包:
fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}闭包 |s| s.size == shoe_size 从周围作用域捕获 shoe_size,并与每双鞋的 尺寸进行比较。
改进 I/O 项目
借助闭包和迭代器,我们可以改进第 12 章的 minigrep 项目。
用迭代器重构 search
原始的 search 函数使用了一个可变的 results 向量:
fn search(query String, contents String) []String {
var results = List.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()
.filter(|line| line.contains(query))
.collect()
}这里使用 filter 只保留包含查询字符串的行,然后用 collect 将它们收集到 向量中。函数式风格最小化了可变状态,使代码更易理解。
闭包与迭代器速查
| 特性 | Auto | Rust |
|---|---|---|
| 闭包语法 | (x) => x + 1 | |x| x + 1 |
| 带类型的闭包 | (x int) -> int { x + 1 } | |x: i32| -> i32 { x + 1 } |
| for 循环 | for x in list { ... } | for x in &list { ... } |
| 范围 | 0..10 | 0..10 |
| 过滤 | .filter((s) => s.size == 10) | .filter(|s| s.size == 10) |
| 映射 | .map((x) => x + 1) | .map(|x| x + 1) |
| 收集 | .collect() | .collect() |
| 求和 | .sum() | .sum() |
总结
本章介绍了两个关键的函数式编程特性:
闭包 — 捕获其环境的匿名函数
(params) => expr语法- 从使用中推断类型
- 通过不可变引用、可变引用或所有权捕获
Fn、FnMut、FnOnce特征
迭代器 — 逐个处理元素的惰性序列
for循环作为主要迭代机制- 消费适配器(
.sum())和迭代器适配器(.map()、.filter()) - 链式适配器实现复杂转换
I/O 项目改进 — 用迭代器适配器简化
search
闭包和迭代器在 Rust 中是零成本抽象——它们编译为与手写循环相同的代码。 Auto 继承了这一性能模型。
在下一章中,我们将探索 Auto 的智能指针类型以及它们如何管理内存。