blog.dopana

Back

中级 Rust 系列的第 14 篇文章。在掌握了智能指针之后,我们现在转向 Rust 的函数式编程特性。Rust 从函数式语言中借鉴了许多概念:它提供了闭包(匿名函数)和迭代器(惰性的、可组合的序列)。其承诺是零成本抽象 — 这些高级结构编译成与手写循环相同的机器码。

graph TD
    A["函数式 Rust"] --> B["闭包<br/>Fn, FnMut, FnOnce"]
    A --> C["迭代器<br/>惰性求值"]
    A --> D["零成本抽象"]
    A --> E["map, filter, fold"]

闭包:匿名函数#

闭包是一个可以捕获其环境的匿名函数。与普通函数不同,闭包可以从其周围作用域中”闭合”变量。

基本语法#

fn main() {
    let add_one = |x: i32| x + 1;
    println!("结果: {}", add_one(5)); // 结果: 6

    // 闭包可以捕获环境
    let multiplier = 3;
    let multiply = |x: i32| x * multiplier;
    println!("结果: {}", multiply(5)); // 结果: 15
}
rust

|x: i32| 语法定义了参数列表,类似于其他语言中的闭包。

捕获环境#

闭包以三种方式从环境中捕获变量:

  1. Fn — 通过引用捕获(&T
  2. FnMut — 通过可变引用捕获(&mut T
  3. FnOnce — 通过值捕获(移动所有权)

[!NOTE] 实现了 FnOnce 的闭包也可以用作 FnMutFn,但反之不行。编译器会推断出所需的最不受限制的 trait。

作为函数参数的闭包#

当闭包作为高阶函数的参数时,它们会特别强大:

fn apply_to_list<T>(list: &[T], f: impl Fn(&T)) {
    for item in list {
        f(item);
    }
}

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    apply_to_list(&numbers, |x| println!("数字: {x}"));
}
rust

作为返回值的闭包#

你也可以使用 impl Fn 从函数中返回闭包:

fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
    |x| x * factor
}

fn main() {
    let double = make_multiplier(2);
    let triple = make_multiplier(3);

    println!("2倍: {}", double(5)); // 2倍: 10
    println!("3倍: {}", triple(5)); // 3倍: 15
}
rust

迭代器:惰性序列#

迭代器是一个生成值序列的构造。在 Rust 中,迭代器是惰性的 — 它们在你调用 collect()for 这样的消费方法之前什么都不做。

创建迭代器#

Rust 中的每个集合都有一个 .iter() 方法,返回一个迭代器:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];

    // .iter() 返回一个迭代器
    let iter = numbers.iter();

    // 迭代器是惰性的 — 还没发生任何事
    for num in iter {
        println!("{num}");
    }
}
rust

迭代器适配器#

迭代器适配器接收一个迭代器并返回一个新的迭代器。最常见的适配器是 mapfilterfold

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    let result: Vec<i32> = numbers
        .iter()
        .filter(|&&x| x % 2 == 0)   // 保留偶数
        .map(|&x| x * 2)           // 翻倍
        .collect();                // 收集到 Vec 中

    println!("{:?}", result); // [4, 8, 12, 16, 20]
}
rust
graph LR
    A["[1,2,3,4,5,6,7,8,9,10]"] --> B["filter: even"]
    B --> C["[2,4,6,8,10]"]
    C --> D["map: x2"]
    D --> E["[4,8,12,16,20]"]

fold 方法#

fold 是带有累加器的 for 循环的函数式等价物:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];

    // 计算总和
    let sum: i32 = numbers.iter().fold(0, |acc, &x| acc + x);
    println!("总和: {sum}"); // 总和: 15

    // 找到最大值
    let max = numbers.iter().fold(0, |acc, &x| if x > acc { x } else { acc });
    println!("最大值: {max}"); // 最大值: 5
}
rust

使用 impl Iterator 创建自定义迭代器#

通过实现 Iterator trait,你可以创建自己的迭代器:

零成本抽象#

Rust 函数式特性的标志是它们编译成与手动编写的循环相同的机器码。让我们来证明这一点:

// 函数式风格
fn functional_sum(numbers: &[i32]) -> i32 {
    numbers.iter().filter(|&&x| x > &0).map(|&x| x * 2).sum()
}

// 命令式风格
fn imperative_sum(numbers: &[i32]) -> i32 {
    let mut sum = 0;
    for &x in numbers {
        if x > 0 {
            sum += x * 2;
        }
    }
    sum
}
rust

这两个函数生成相同的汇编代码。编译器会完全优化掉迭代器链。

[!TIP] Rust 的迭代器是零成本抽象的典型例子。你既获得了函数式编程的表达能力,也获得了手工优化的 C 语言的性能。

通俗比喻(ELI5):工厂的传送带#

把迭代器想象成工厂的传送带:

  • 原材料(原始集合)到达传送带的起点。
  • 每个适配器(mapfilter 等)是一个加工站,它转换或删除物品。
  • 传送带(迭代器)是惰性的 — 物品只有在终点的工人拉动时才会移动。
  • 最终消费者(collectsumfor)是从传送带上取出成品的工人。

因为传送带是惰性的,如果工人只需要 3 个物品,工厂只会处理 3 个 — 它不会浪费能量处理整个批次!

总结#

  • 闭包是可以捕获环境的匿名函数,具有三种捕获模式:FnFnMutFnOnce
  • 迭代器是惰性序列,根据需要生成值,具有强大的适配器如 mapfilterfold
  • 零成本抽象意味着函数式风格的代码编译成与命令式循环相同的机器码。
  • 通过实现 Iterator trait,可以创建自定义迭代器。

在下一篇文章中,我们将深入探讨Fearless Concurrency — Rust 在线程、通道和共享状态方面如何实现安全的并行编程。

参考资料#