高级函数与闭包

ch20-05-advanced-functions-and-closures.md
commit 21cf840842bdf768a798869f06373c96c1cc5122

本部分将探索一些有关函数和闭包的高级功能,这包括函数指针以及返回值闭包。

函数指针

我们讨论过了如何向函数传递闭包;也可以向函数传递常规函数!这个技术在我们希望传递已经定义的函数而不是重新定义闭包作为参数时很有用。函数满足类型 fn(小写的 f),不要与闭包 trait 的 Fn 相混淆。fn 被称为 函数指针function pointer)。通过函数指针允许我们使用函数作为另一个函数的参数。

指定参数为函数指针的语法类似于闭包,如示例 19-27 所示,这里定义了一个 add_one 函数将其参数加一。do_twice 函数获取两个参数:一个指向任何获取一个 i32 参数并返回一个 i32 的函数指针,和一个 i32 值。do_twice 函数传递 arg 参数调用 f 函数两次,接着将两次函数调用的结果相加。main 函数使用 add_one5 作为参数调用 do_twice

文件名:src/main.rs

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}");
}

示例 19-27: 使用 fn 类型接受函数指针作为参数

这会打印出 The answer is: 12do_twice 中的 f 被指定为一个接受一个 i32 参数并返回 i32fn。接着就可以在 do_twice 函数体中调用 f。在 main 中,可以将函数名 add_one 作为第一个参数传递给 do_twice

不同于闭包,fn 是一个类型而不是一个 trait,所以直接指定 fn 作为参数而不是声明一个带有 Fn 作为 trait bound 的泛型参数。

函数指针实现了所有三个闭包 trait(FnFnMutFnOnce),所以总是可以在调用期望闭包的函数时传递函数指针作为参数。倾向于编写使用泛型和闭包 trait 的函数,这样它就能接受函数或闭包作为参数。

一个只期望接受 fn 而不接受闭包的情况的例子是与不存在闭包的外部代码交互时:C 语言的函数可以接受函数作为参数,但 C 语言没有闭包。

作为一个既可以使用内联定义的闭包又可以使用命名函数的例子,让我们看看一个 map 的应用。使用 map 函数将一个数字 vector 转换为一个字符串 vector,就可以使用闭包,比如这样:

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();
}

或者可以将函数作为 map 的参数来代替闭包,像是这样:

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();
}

注意这里必须使用 “高级 trait” 部分讲到的完全限定语法,因为存在多个叫做 to_string 的函数;这里使用了定义于 ToString trait 的 to_string 函数,标准库为所有实现了 Display 的类型实现了这个 trait。

回忆一下第六章 “枚举值” 部分中定义的每一个枚举成员也变成了一个构造函数。我们可以使用这些构造函数作为实现了闭包 trait 的函数指针,这意味着可以指定构造函数作为接受闭包的方法的参数,如下:

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

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

这里创建了 Status::Value 实例,它通过 map 用范围的每一个 u32 值调用 Status::Value 的初始化函数。一些人倾向于函数风格,一些人喜欢闭包。这两种形式最终都会产生同样的代码,所以请使用对你来说更明白的形式吧。

返回闭包

闭包表现为 trait,这意味着不能直接返回闭包。对于大部分需要返回 trait 的情况,可以使用实现了期望返回的 trait 的具体类型来替代函数的返回值。但是这不能用于闭包,因为它们没有一个可返回的具体类型;例如不允许使用函数指针 fn 作为返回值类型。

这段代码尝试直接返回闭包,它并不能编译:

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

编译器给出的错误是:

$ cargo build
   Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0746]: return type cannot have an unboxed trait object
 --> 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() -> 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

错误又一次指向了 Sized trait!Rust 并不知道需要多少空间来储存闭包。不过我们在上一部分见过这种情况的解决办法:可以使用 trait 对象:

fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}

这段代码正好可以编译。关于 trait 对象的更多内容,请回顾第十八章的 顾及不同类型值的 trait 对象” 部分。

接下来让我们学习宏!