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

19.3 高级函数和闭包

19.3.1 函数指针

我们已经讲过把闭包传进函数。实际上,我们也可以把函数传进函数。

在传递时,函数会被强制转换成 fn 类型,这就是函数指针

例如:

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

fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}

fn main() {
    let answer = do_twice(add_one, 5);

    println!("The answer is: {answer}");
}

do_twice 的第一个参数 f 的类型是 fn,也就是函数指针。它期望一个参数类型为 i32、返回类型也为 i32 的函数。函数体中调用了两次 f

输出:

The answer is: 12

函数指针与闭包的区别

闭包至少实现了 FnFnOnceFnMut 这三个 trait 之一。 函数指针 fn 是一个类型,不是 trait。我们可以直接把 fn 指定为参数类型,而不必声明一个以 Fn trait 为约束的泛型参数。

函数指针实现了全部三种闭包 trait,也就是 FnFnOnceFnMut 所以你总是可以把函数指针作为参数传给接受闭包的函数。正因为如此,我们编写函数时通常更倾向于使用带闭包 trait 的泛型参数,因为这样函数既能接受闭包,也能接受普通函数。


在某些情况下,我们可能想接受 fn 类型而不是闭包,例如与不支持闭包的代码交互时,比如 C 函数。该怎么写呢?

看一个例子:

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> = list_of_numbers
        .iter()
        .map(|i| i.to_string())
        .collect();
    // 分行只是为了可读性,并不是必须的
}

list_of_numbers 中的元素是 i32,我们想把它们转换成 String 赋给 list_of_strings。步骤是:

  • 先用 iter 产生一个迭代器
  • 再用 map 中的闭包 |i| i.to_string() 转换每个元素
  • 最后用 collect 把所有转换后的元素收集成一个集合

这段代码也可以这样写:

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> = list_of_numbers
        .iter()
        .map(ToString::to_string)
        .collect();
}

区别在于 .map(ToString::to_string),这里直接传入了 to_string 函数。效果与上一版相同。顺便一提,ToString::to_string 使用了 19.2. 高级 trait:关联类型、默认泛型参数和运算符重载、完全限定语法、supertrait 和 newtype 讨论过的完全限定语法。

来看一下 map 的定义:

#![allow(unused)]
fn main() {
fn map<B, F>(self, f: F) -> Map<Self, F>
where
    Self: Sized,
    F: FnMut(Self::Item) -> B
}

map 要求 f 实现 FnMut trait,而闭包和函数指针都满足这个要求,所以两者都可以传入。

再看另一个例子:

fn main() {
    enum Status {
        Value(u32),
        Stop,
    }

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

注意 map 的参数。我们使用构造函数 Status::Value,对范围内的每个 u32 调用 map,并创建 Status::Value 实例。

有人可能会问:Status::Value 不是枚举变体吗?怎么变成函数了?这是因为在 Rust 中,这样的构造函数被实现为接收一个参数并返回新实例的函数。换句话说:

#![allow(unused)]
fn main() {
let v = Status::Value(3);
}

这只是一个例子。这里初始化了 v,而 Status::Value(3) 可以看作一次构造函数调用:3 是构造函数的参数。由于构造函数被实现为函数,我们可以把它们当作函数来用,3 就是它们的参数。

所以我们也可以把这类构造函数用作实现了闭包 trait 的函数指针。

19.3.2 返回闭包

闭包通过 trait 来表达,因此不能直接从函数返回闭包。相反,你可以返回一个实现了该 trait 的具体类型。

例如:

#![allow(unused)]
fn main() {
fn returns_closure() -> dyn Fn(i32) -> i32 {
    |x| x + 1
}
}

这个函数试图直接返回一个闭包。

输出:

error[E0746]: return type cannot be a trait object without pointer indirection
 --> src/lib.rs:1:25
  |
1 | fn returns_closure() -> dyn Fn(i32) -> i32 {
  |                         ^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
  |
help: consider returning an `impl Trait` instead of a `dyn Trait`
  |
1 - fn returns_closure() -> dyn Fn(i32) -> i32 {
1 + fn returns_closure() -> impl Fn(i32) -> i32 {
  |
help: alternatively, box the return type, and wrap all of the returned values in `Box::new`
  |
1 ~ fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
2 ~     Box::new(|x| x + 1)
  |

For more information about this error, try `rustc --explain E0746`.
error: could not compile `functions-example` (lib) due to 1 previous error

Rust 不知道需要多少空间来存储这个闭包,所以会报错。

还记得我们以前在哪里遇到过同样的“Rust 不知道该分配多少空间”的错误吗?没错——在学习链表时。当时的解决办法是用 Box<T> 包裹链表,这里也可以这样做:

#![allow(unused)]
fn main() {
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}
}

因为返回值位于指针之后,返回类型现在在编译时就有了已知大小。