Skip to content

高级函数和闭包

函数指针

我们讨论过了如何向函数传递闭包;也可以将普通函数传递给函数!这个技术在我们希望传递已经定义的函数而不是重新定义闭包作为参数时很有用.

基本概念

  • 定义:函数指针是指向普通函数的指针.它的类型写作小写的 fn(注意与闭包 trait Fn 区分).
  • 特性:fn 是一个类型(Type),而非 trait.因此在作为参数时,可以直接声明类型,不需要像闭包那样使用泛型约束(Trait Bound).
  • 强制转换:普通函数会自动强制转换为 fn 类型.

语法示例

rust
fn add_one(x: i32) -> i32 {
  x + 1
}

// 参数 f 的类型是 fn(i32) -> i32
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
  f(arg) + f(arg)
}

fn main() {
  let answer = do_twice(add_one, 5);
  println!("{}", answer); // 结果为 12
}

与闭包的关系

  • 兼容性:函数指针实现了全部三个闭包 trait(FnFnMutFnOnce).
  • 适用场景:
    • 通用性建议:编写函数时,优先使用泛型和闭包 trait(如 T: Fn(...)),这样既能接收闭包,也能接收普通函数.
    • 必须使用 fn 的场景:与外部代码(如 C 语言)交互时.C 语言支持函数作为参数,但没有闭包概念.

常见应用技巧

替代简单闭包:

可以使用已有的函数名或完全限定语法来简化代码.

rust
let list = vec![1, 2, 3];

// 使用闭包
let s: Vec<String> = list.iter().map(|i| i.to_string()).collect();

// 使用函数指针 (ToString trait 提供的函数)
let s: Vec<String> = list.iter().map(ToString::to_string).collect();

枚举构造函数:

枚举的元组变体(如 Status::Value(u32))本质上是返回实例的函数,也可作为函数指针.

rust
enum Status {
  Value(u32),
  Stop,
}

let list: Vec<Status> = (0..20).map(Status::Value).collect();

当你定义 Value(u32) 时,Rust 编译器会自动创建一个同名的初始化函数.在底层,Status::Value 的行为就像下面这个普通的函数:

rust
fn Value(i: u32) -> Status {
  Status::Value(i)
}

这个函数的签名是 fn(u32) -> Status.因为它符合"接收一个 u32 并返回一个结果"的格式,所以它完全可以被当做函数指针传递给 map.

核心区别总结

特性函数指针 (fn)闭包 (Fn/FnMut/FnOnce)
本质类型 (Type)Trait
捕获环境不可以(无状态)可以(有状态)
性能编译成普通函数调用通常内联,性能极高
用法直接作为参数类型通常作为泛型约束

返回闭包

闭包的匿名性

  • 闭包无法直接返回:闭包没有一个可以写出来的具体类型名(编译器为每个闭包生成唯一的匿名类型).
  • fn 指针的局限:如果闭包捕获了环境中的变量,它就不能被转换为 fn 指针,必须使用闭包 trait(Fn / FnMut / FnOnce).

使用 impl Trait 返回闭包

如果你只需要从一个函数返回一种闭包,可以使用 impl Trait 语法.

  • 优点:不需要堆内存分配(不使用 Box),性能更高(静态分发).
rust
fn returns_closure() -> impl Fn(i32) -> i32 {
  |x| x + 1
}

impl Trait 的不透明性

即使两个函数都写着返回 impl Fn(i32) -> i32,且签名完全一样,它们的实际类型也是不同的.Rust 编译器将每个 impl Trait 视为独立的不透明类型.因此,你不能把这两个函数的返回值放在同一个 Vec 里(Vec 要求所有元素类型一致).

rust
fn returns_closure() -> impl Fn(i32) -> i32 {
  |x| x + 1
}

fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
  move |x| x + init
}

fn main() {
  // ❌ 编译错误:类型不匹配
  // let handlers = vec![returns_closure(), returns_initialized_closure(123)];
}

使用 Trait 对象返回闭包

如果你需要在一个地方(如数组或条件分支)处理多个不同的闭包,必须使用 Trait 对象 (dyn Fn(...) -> ...)Box.

  • 原理:通过 Box 将闭包放在堆上,抹除具体类型,统一为 Box<dyn Fn(...) -> ...>.
rust
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
  Box::new(|x| x + 1)
}

fn returns_initialized_closure(init: i32) -> Box<dyn Fn(i32) -> i32> {
  Box::new(move |x| x + init)
}

fn main() {
  let handlers: Vec<Box<dyn Fn(i32) -> i32>> = vec![
    returns_closure(),
    returns_initialized_closure(10),
  ];

  for handler in handlers {
    println!("{}", handler(5));
  }
}

总结对比

方案语法适用场景内存分配
impl Trait-> impl Fn(...)返回同一种闭包逻辑栈分配(静态分发)
Trait 对象-> Box<dyn Fn(...)>返回多种不同实现或存入集合堆分配(动态分发)

注意:在返回捕获了局部变量的闭包时,通常需要配合 move 关键字,以确保闭包拥有捕获变量的所有权,防止引用失效.

基于 MIT 协议发布