Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust 函数与闭包

1. Rust 函数是第一公民

在深入闭包之前,我们需要先理解一个更根本的事实:在 Rust 中,函数本身就是第一公民(first-class citizen)。这意味着函数不只是一段顶层的声明——它是,可以被绑定到变量、作为参数传递、从其他函数返回。

1.1 函数是值

每一个 fn 定义的函数都有一个独特的、编译期确定的函数项类型(function item type)。此类型的零大小(ZST)值就是该函数本身:

#![allow(unused)]
fn main() {
fn add(x: i32, y: i32) -> i32 { x + y }

let f = add;             // f 的类型是 fn(i32, i32) -> i32 的函数指针
let result = f(1, 2);    // 调用 f,等价于 add(1, 2)
assert_eq!(result, 3);
}

add 本身是一个函数项,当它被绑定到变量 f 时,会自动强制转换(coerce)为函数指针类型 fn(i32, i32) -> i32。函数指针是一个普通的值——它可以被复制、比较(==)、作为参数传递。

1.2 函数作为参数

既然函数指针是值,我们就可以把它传给另一个函数:

#![allow(unused)]
fn main() {
fn add(x: i32, y: i32) -> i32 { x + y }
fn mul(x: i32, y: i32) -> i32 { x * y }

// 接受一个函数指针作为参数
fn apply(f: fn(i32, i32) -> i32, a: i32, b: i32) -> i32 {
    f(a, b)
}

assert_eq!(apply(add, 2, 3), 5);
assert_eq!(apply(mul, 2, 3), 6);
}

也可以用泛型来接受任何可调用类型(函数指针、闭包、甚至自定义的 callable 结构体):

#![allow(unused)]
fn main() {
fn apply_generic<F>(f: F, a: i32, b: i32) -> i32
where
    F: Fn(i32, i32) -> i32,
{
    f(a, b)
}
// 函数指针和闭包都能传进来
assert_eq!(apply_generic(add, 2, 3), 5);
assert_eq!(apply_generic(|x, y| x - y, 5, 2), 3);
}

1.3 函数作为返回值

函数可以返回另一个函数:

#![allow(unused)]
fn main() {
fn select_op(kind: &str) -> fn(i32, i32) -> i32 {
    match kind {
        "add" => add,
        "mul" => mul,
        _     => |_, _| 0,  // 无捕获的闭包可以转为函数指针
    }
}

let op = select_op("add");
assert_eq!(op(10, 20), 30);
}

注意:这里只能返回无捕获的闭包(它可转为 fn 指针)或者其他顶层函数。想返回携带状态的闭包,需要用 impl FnBox<dyn Fn>(后面会讲)。

1.4 函数的局限:无法携带状态

函数指针很轻量(8 字节),但有一个根本性的限制:它只能访问参数,无法携带任何上下文

#![allow(unused)]
fn main() {
fn make_adder(base: i32) -> ??? {
    // 我们想返回一个”把 base 加到参数上”的可调用物
    // 但 fn 指针无法捕获外部变量 base
    // 这里 fn(i32) -> i32 做不到!
}
}

这就引出了闭包——函数携带上环境。闭包不是什么全新的概念,它只是在函数的基础上增加了”捕获外部变量”的能力。理解了函数是第一公民之后,闭包就是”能带数据的函数”。


2. 闭包:带环境的函数

2.1 直观感受

闭包(Closure)是匿名函数 + 捕获的环境的组合体。它的语法轻量,最常见的定义方式是直接用 |参数| 表达式

#![allow(unused)]
fn main() {
let base = 10;
let add_base = |x: i32| x + base;   // 捕获了外部变量 base
assert_eq!(add_base(5), 15);
}

你看这个 add_base,它做的事跟函数一样(接受参数、返回结果),但它记住了定义时所在作用域里的 base。这就是闭包比函数多出来的能力。

在 Rust 中,闭包几乎无处不在:

  • .map(|x| x * 2)——迭代器适配
  • .filter(|x| *x > 0)——条件过滤
  • thread::spawn(move || { ... })——跨线程传递
  • Option::unwrap_or_else(|| ...)——惰性默认值

2.2 语法结构

闭包语法的完整形式是:

|param1, param2, ...| -> ReturnType {
    // 闭包体(多条语句)
    expression
}

具体规则:

  • 参数放在一对竖线 | | 之间。类型通常省略,由编译器从调用上下文推断;必要时可以显式标注,如 |x: i32, y: i32| -> i32 { x + y }
  • 花括号 {} 在闭包体只有一条表达式时可以省略:|a, b| a + b
  • 返回值的类型由闭包体最后一行表达式决定,自动推导。想返回单元值 (),可以写分号或者显式 { ...; }
  • 类型锁定:参数和返回值的类型在第一次调用时就被确定下来,后续调用必须沿用。同一个闭包不能上一次当 i32 -> i32 用,下一次又当 f64 -> f64 用。

2.3 简单示例

#![allow(unused)]
fn main() {
// 无参数,不返回值
let say_hello = || println!("Hello");
say_hello();

// 单表达式,类型全部由编译器推断
let add = |a, b| a + b;
let result = add(3, 5);   // result: i32 = 8

// 带显式类型注解
let mul = |x: f64, y: f64| -> f64 { x * y };
let product = mul(2.5, 4.0); // 10.0

// 多语句闭包
let compute = |x: i32| {
    let y = x + 10;
    y * 2
};
assert_eq!(compute(5), 30);
}

3. 函数与闭包的全面对比

3.1 定义方式对比

#![allow(unused)]
fn main() {
// 函数:顶层定义,必须有名字
fn add_fn(a: i32, b: i32) -> i32 {
    a + b
}

// 闭包:匿名定义,绑定到变量
let add_cl = |a: i32, b: i32| -> i32 { a + b };
}

3.2 本质差异分析

维度函数(fn闭包
名称必须有函数名(项级函数)匿名,通常绑定到变量或直接传参
捕获环境无法捕获外部变量(只能通过参数传入)可捕获定义域中的变量,支持不可变借用、可变借用、移动所有权三种方式
类型拥有唯一的函数指针类型(如 fn(i32) -> i32每个闭包表达式生成一个独特的匿名类型,即便签名相同,两个闭包的类型也不同
trait 实现所有同签名的函数共享同一个 fn 指针类型,且自动实现 FnFnMutFnOnce根据捕获方式自动实现 FnOnce,可能同时实现 FnMutFn(见第五节)
大小零大小(函数项)或指针(fn 指针 = 8 字节)大小由捕获的环境决定,可能为 0(无捕获)、8(引用)、或更大(移动的值)
内联优化编译器可内联编译器同样可内联(类型已知),通常与泛型配合实现零成本抽象

3.3 关键理解:闭包 ≠ 函数指针

闭包不是函数指针。它是一个编译器生成的匿名结构体,同时包含了函数指针和捕获的变量。当一个闭包完全不捕获任何环境变量时,它可以被强制转换为函数指针:

#![allow(unused)]
fn main() {
let no_cap = |x: i32| x + 1;
let f: fn(i32) -> i32 = no_cap;   // 无捕获的闭包 → 函数指针
}

一旦闭包捕获了环境,这种强制转换就不允许——此时闭包携带了数据,不再是一个纯指针。这种场景只能用泛型(F: Fn(...))或 trait 对象(Box<dyn Fn(...)>)来接收。

3.4 什么时候用函数,什么时候用闭包?

场景推荐原因
独立逻辑、不需要外部数据函数有名字、可复用、类型清晰
迭代器适配(.map.filter闭包轻量、就地定义、代码紧凑
需要携带上下文闭包函数做不到
作为 HashMap 的 key函数指针 fn实现了 Eq + Hash
与 C FFI 交互函数指针 fnABI 稳定,extern "C" fn
作为 trait 约束泛型 F: Fn(...)同时接受函数和闭包,最灵活

4. 闭包的捕获方式

Rust 编译器会根据闭包体对捕获变量的使用方式,自动选择开销最小、权限最低的捕获方式:

4.1 不可变借用(&T

当闭包体只需要读取外部变量时,编译器会为其生成一个捕获 &T 的闭包结构体。此时闭包实现了 Fn trait(以及 FnMutFnOnce)。

#![allow(unused)]
fn main() {
let msg = String::from("hello");
let print_msg = || println!("{}", msg);   // 只读,捕获 &msg
print_msg();
println!("{}", msg);                       // 外部仍可使用 msg
}

4.2 可变借用(&mut T

当闭包体需要修改外部变量时,捕获方式变为 &mut T,闭包因此实现 FnMut(以及 FnOnce),但不实现 Fn。这也意味着该闭包在借用期间会排他性地持有变量,外部不能同时访问。

let mut counter = 0; let mut inc = || { counter += 1; // 需要修改 counter,捕获 &mut counter println!(“{}”, counter); }; inc(); // println!(“{}”, counter); // 错误:counter 仍被可变借用 inc(); // 可再次调用 drop(inc); // 释放闭包,归还借用 println!(“{}”, counter); //

4.3 获取所有权(移动 T

当闭包需要接管变量所有权(例如将变量移动到另一线程、或者变量在闭包内被消费、drop)时,编译器会直接将变量移动进闭包结构体。该闭包仅实现 FnOnce,因为调用它即消耗闭包内的值。

#![allow(unused)]
fn main() {
let owned_str = String::from("world");
let consume = || {
    let s = owned_str;      // 强制移动,捕获所有权
    println!("{}", s);
    // s 在此被 drop
};
consume();
// consume(); // 错误:consume 已被消耗(FnOnce)
}

4.4 move 关键字强制移动

可以在闭包声明前加上 move 关键字,强制闭包获取捕获变量的所有权,无论闭包体是否真的需要所有权。这在跨线程传递、异步任务中非常常见。

#![allow(unused)]
fn main() {
let name = String::from("Alice");
let printer = move || println!("{}", name);
// name 已被移动,此处不能再使用
printer();
}

捕获方式总结

  • &T → 实现 Fn(以及 FnMutFnOnce
  • &mut T → 实现 FnMut(以及 FnOnce),不实现 Fn
  • T(所有权移动)→ 仅实现 FnOnce

编译器优先选择借用,只有在必须时才升级为可变借用或所有权移动。


5. 闭包的类型

每个闭包表达式都拥有一个唯一的、不可显式书写的匿名类型。该类型根据捕获方式自动实现以下三个 trait 中的某几个。这三个 trait 定义在标准库的 std::ops 模块中:

5.1 FnOnce —— 调用即消耗

#![allow(unused)]
fn main() {
pub trait FnOnce<Args> {
    type Output;
    // 签名:消耗 self,返回 Output
    fn call_once(self, args: Args) -> Self::Output;
}
}
  • 形参self(按值接收闭包自身),以及调用参数 args
  • 返回值:关联类型 Output,即闭包返回值类型。
  • 所有闭包至少实现该 trait。如果一个闭包会从环境中移动值,则它仅实现 FnOnce,只能被调用一次。

5.2 FnMut —— 可重复调用,且可以修改环境

#![allow(unused)]
fn main() {
pub trait FnMut<Args>: FnOnce<Args> {
    // 签名:&mut self,返回 Output
    fn call_mut(&mut self, args: Args) -> Self::Output;
}
}
  • 继承自 FnOnce
  • 调用时需要 &mut self,所以要求闭包本身是可变的。
  • 如果一个闭包会可变借用环境变量(但未移动所有权),它实现 FnMut 而不实现 Fn

5.3 Fn —— 纯函数式,仅不可变借用

#![allow(unused)]
fn main() {
pub trait Fn<Args>: FnMut<Args> {
    // 签名:&self,返回 Output
    fn call(&self, args: Args) -> Self::Output;
}
}
  • 继承自 FnMut(进而继承 FnOnce)。
  • 调用时仅需 &self,允许多个不可变引用同时存在。
  • 如果闭包只对捕获变量进行不可变访问,则它实现 Fn

trait 约束应用示例

#![allow(unused)]
fn main() {
// 要求闭包实现 Fn,可以多次调用且不会修改环境
fn apply_three_times<F>(f: F, x: i32) -> i32
where
    F: Fn(i32) -> i32,
{
    f(f(f(x)))
}

let add_one = |n| n + 1;
let result = apply_three_times(add_one, 1); // 4

// 要求闭包实现 FnMut,允许闭包在内部修改变量
fn for_each<F>(slice: &[i32], mut action: F)
where
    F: FnMut(i32),
{
    for &item in slice {
        action(item);
    }
}

let mut total = 0;
for_each(&[1, 2, 3], |val| total += val);
println!("{}", total); // 6

// 要求闭包仅 FnOnce(例如需要移动所有权)
fn consume_once<F>(f: F)
where
    F: FnOnce() -> String,
{
    let s = f();
    println!("Consumed: {}", s);
    // 不能再调用 f
}

let msg = String::from("done");
consume_once(move || msg);  // 所有权转移
}

说明:编写接受闭包参数的函数时,尽量从 Fn 开始,若编译器报需要 FnMut 则放宽,若仍需所有权则使用 FnOnce。这给调用者最大的灵活性。


6. 闭包作为回调函数

回调是闭包最典型的应用场景。我们通常将“未来才执行的逻辑”封装为闭包传递给某个函数或结构体。根据是否需要返回值、是否可能多次调用,选择适当的 trait 约束。

6.1 明确返回值类型的回调

若回调的返回类型在编译时即确定,使用泛型加 Fn/FnMut/FnOnce 约束:

#![allow(unused)]
fn main() {
// 对一个值应用某种转换
fn transform<T, F>(val: T, f: F) -> T
where
    F: FnOnce(T) -> T,
{
    f(val)
}

let double = |x| x * 2;
assert_eq!(transform(21, double), 42);
}

6.2 不明确返回值类型(异质回调)

有时我们希望回调的返回值类型不固定,甚至是不同的具体类型,但仍需类型安全。常见方案是:

a) 使用 trait 对象,将返回值擦除为统一 trait(如 Box<dyn Display>

#![allow(unused)]
fn main() {
// 接受返回任意可 Display 类型的闭包
fn log_and_return<F>(f: F) -> Box<dyn std::fmt::Display>
where
    F: FnOnce() -> Box<dyn std::fmt::Display>,
{
    let result = f();
    println!("log: callback executed");
    result
}

let num = log_and_return(|| Box::new(42));
let text = log_and_return(|| Box::new("hello".to_string()));
println!("{}, {}", num, text);
}

b) 使用枚举实现“内聚”的类型归并

如果返回值类型的集合已知且有限,可定义枚举:

#![allow(unused)]
fn main() {
enum CallbackResult {
    Int(i32),
    Str(String),
}

fn run_cb<F>(f: F) -> CallbackResult
where
    F: FnOnce() -> CallbackResult,
{
    f()
}
}

c) 返回 impl Trait + 泛型闭包(同一闭包返回具体类型)

若是单个闭包但希望调用者不需要写出具体类型,可在函数内部使用 impl Trait,但场景有限:

#![allow(unused)]
fn main() {
fn apply<F, R>(f: F) -> impl std::fmt::Display
where
    F: FnOnce() -> R,
    R: std::fmt::Display,
{
    f()
}
// 调用时闭包只需返回一个实现了 Display 的具体类型,外层并不失 type info
}

d) 不确定返回值且不想使用 trait 对象时,可用 any 或动态分发库,但 Rust 惯用方式仍是 trait 对象或泛型。

6.3 异步回调

异步任务常将闭包与 Future 结合。tokio::spawn 接受的是一个 Future,而 async move { ... } 会生成一个捕获环境所有权的匿名 Future。

#![allow(unused)]
fn main() {
tokio::spawn(async move {
    // 捕获环境所有权
});
}

这类被 spawn 的 Future 通常需要满足 Send + 'static,因为运行时可能把它调度到其他线程,并且任务可能活得比当前函数作用域更久。


7. 传闭包和函数

7.1 传递闭包

当 API 需要“可调用物”时,推荐使用泛型与 Fn 系列 trait,而非函数指针。这样做可以接受闭包、函数指针、甚至实现了相应 trait 的自定义类型,且编译器可进行内联优化。

#![allow(unused)]
fn main() {
fn call_twice<F>(mut f: F)
where
    F: FnMut(),
{
    f();
    f();
}

let mut count = 0;
call_twice(|| count += 1);
assert_eq!(count, 2);
}

7.2 传递函数指针

函数指针类型 fn(Args) -> Ret 既可以接受普通函数,也可以接受不捕获任何变量的闭包。

#![allow(unused)]
fn main() {
fn plain_fn(x: i32) -> i32 { x * x }

fn apply_fn_ptr(f: fn(i32) -> i32, val: i32) -> i32 {
    f(val)
}

// 传入普通函数
apply_fn_ptr(plain_fn, 10);

// 传入无捕获的闭包
apply_fn_ptr(|x| x + 1, 10);
}

一旦闭包开始捕获环境,就无法再转换为 fn 指针,此时必须改为泛型 F: Fn(...) 参数。

最佳实践:编写库代码时,优先使用泛型 + Fn trait 约束,这给调用者最大灵活性。仅在确实需要函数指针的场景(如与 C 交互、存储一组同签名的函数指针表)时使用 fn 类型。


8. 闭包作为返回值

因为每个闭包的匿名类型对调用者不可知,函数无法直接返回一个闭包的具体类型。常用的解决方式有两种:

8.1 使用 impl Trait(静态分发)

#![allow(unused)]
fn main() {
fn make_adder(base: i32) -> impl Fn(i32) -> i32 {
    move |x| base + x
}

let add_five = make_adder(5);
assert_eq!(add_five(10), 15);
}
  • impl Fn(i32) -> i32 告诉编译器:“这个函数返回某个实现了 Fn(i32) -> i32 的具体类型,但不必显式写出”。
  • 闭包环境通常需要移动所有权,因此闭包体前常加 move
  • 局限:同一个函数只能返回一种具体类型,不能根据运行时条件返回不同闭包。

8.2 使用 trait 对象(动态分发)

#![allow(unused)]
fn main() {
fn make_counter(init: i32) -> Box<dyn FnMut() -> i32> {
    let mut count = init;
    Box::new(move || {
        count += 1;
        count
    })
}

let mut counter = make_counter(0);
assert_eq!(counter(), 1);
assert_eq!(counter(), 2);
}
  • 返回 Box<dyn FnMut() -> i32>,将闭包类型擦除,仅保留 vtable。
  • 可以在运行时选择返回不同的闭包(只要它们都满足 FnMut() -> i32)。
  • 代价是一次堆分配和动态分发带来的间接调用开销。

9. 示例代码

以下示例综合展示了闭包捕获、作为参数、作为返回值,以及 Fn/FnMut/FnOnce 的实际使用。

use std::collections::HashMap;

/// 缓存型计算器:接受一个闭包作为计算公式,并返回缓存结果的闭包。
fn memoize<F, K, V>(calc: F) -> impl FnMut(K) -> V
where
    F: Fn(K) -> V,
    K: std::cmp::Eq + std::hash::Hash + Clone,
    V: Clone,
{
    // 移动 calc 和新建的 HashMap 到返回闭包环境中
    let mut cache = HashMap::new();
    move |key: K| {
        if let Some(val) = cache.get(&key) {
            println!("Cache hit!");
            val.clone()
        } else {
            let val = calc(key.clone());
            cache.insert(key, val.clone());
            val
        }
    }
}

// 一个接受 FnOnce 并以回调方式处理的函数
fn with_cleanup<F, T>(produce: F) -> T
where
    F: FnOnce() -> T,
{
    let result = produce();
    println!("Cleanup performed");
    result
}

fn main() {
    // 1. 闭包捕获与类型
    let factor = 10;
    let multiply = |x| x * factor;   // 捕获 &factor,实现 Fn

    // 2. 作为参数传递
    let results: Vec<i32> = (1..=3).map(multiply).collect();
    println!("{:?}", results); // [10, 20, 30]

    // 3. 缓存闭包(FnMut)
    let mut cached_fib = memoize(|n: u64| -> u64 {
        if n <= 1 { n }
        else { cached_fib(n-1) + cached_fib(n-2) }   // 递归调用(栈溢出仅演示概念)
    });
    println!("fib(5) = {}", cached_fib(5)); // 计算并缓存
    println!("fib(5) = {}", cached_fib(5)); // 命中缓存

    // 4. 所有权移动的回调(FnOnce)
    let data = vec![1, 2, 3];
    let result = with_cleanup(move || {
        data.iter().sum::<i32>()  // data 所有权被移动
    });
    println!("Sum: {}", result);

    // 5. 闭包作为返回值
    let adder = make_adder(100);
    println!("100 + 1 = {}", adder(1));
}

// 辅助函数:返回一个闭包
fn make_adder(base: i32) -> impl Fn(i32) -> i32 {
    move |x| base + x
}