高级函数和闭包

本节探讨一些与函数和闭包相关的高级特性,包括函数指针和返回闭包。

函数指针

我们已经讨论过如何将闭包传递给函数;你也可以将常规函数传递给函数!当你想要传递一个已经定义的函数而不是定义一个新的闭包时,此技术非常有用。函数会被强制转换为 fn 类型(小写 f),不要与 Fn 闭包 trait 混淆。fn 类型被称为函数指针。使用函数指针传递函数将允许你将函数用作其他函数的参数。

指定参数为函数指针的语法与闭包的语法类似,如清单 20-28 所示,我们在其中定义了一个函数 add_one,它将其参数加一。函数 do_twice 接受两个参数:一个指向任何接受 i32 参数并返回 i32 的函数的函数指针,以及一个 i32 值。do_twice 函数调用函数 f 两次,传递给它 arg 值,然后将两个函数调用结果相加。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}"); }
清单 20-28:使用 fn 类型接受函数指针作为参数

此代码打印 The answer is: 12。我们指定 do_twice 中的参数 f 是一个 fn,它接受一个 i32 类型的参数并返回一个 i32。然后我们可以在 do_twice 的主体中调用 f。在 main 中,我们可以将函数名 add_one 作为 do_twice 的第一个参数传递。

与闭包不同,fn 是一种类型而不是 trait,因此我们直接将 fn 指定为参数类型,而不是声明一个具有 Fn traits 之一作为 trait bound 的泛型类型参数。

函数指针实现了所有三个闭包 traits (FnFnMutFnOnce),这意味着你始终可以将函数指针作为期望闭包的函数的参数传递。最好使用泛型类型和其中一个闭包 traits 来编写函数,以便你的函数可以接受函数或闭包。

话虽如此,你可能只想接受 fn 而不接受闭包的一个例子是与没有闭包的外部代码进行交互时:C 函数可以接受函数作为参数,但 C 没有闭包。

作为你可以使用内联定义的闭包或命名函数的示例,让我们看一下标准库中 Iterator trait 提供的 map 方法的用法。要使用 map 函数将数字向量转换为字符串向量,我们可以使用闭包,像这样

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

请注意,我们必须使用我们在前面 “高级 Traits” 中讨论的完全限定语法部分,因为有多个名为 to_string 的函数可用。在这里,我们使用 ToString trait 中定义的 to_string 函数,标准库已为任何实现 Display 的类型实现了该 trait。

回想一下第 6 章的 “枚举值”第 6 章的部分,我们定义的每个枚举变体的名称也成为初始化器函数。我们可以使用这些初始化器函数作为实现闭包 traits 的函数指针,这意味着我们可以将初始化器函数指定为接受闭包的方法的参数,就像这样

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 实例。有些人喜欢这种风格,有些人喜欢使用闭包。它们编译成相同的代码,因此请使用对你来说更清晰的风格。

返回闭包

闭包由 traits 表示,这意味着你不能直接返回闭包。在大多数情况下,你可能想要返回一个 trait,你可以改为使用实现该 trait 的具体类型作为函数的返回值。但是,你不能对闭包执行此操作,因为它们没有可返回的具体类型;例如,你不允许使用函数指针 fn 作为返回类型。

相反,你通常会使用我们在第 10 章中学习的 impl Trait 语法。你可以返回任何函数类型,使用 FnFnOnceFnMut。例如,此代码可以正常工作

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

但是,正如我们在第 13 章的 “闭包类型推断和注解” 中指出的那样第 13 章的部分,每个闭包也是其自身独特的类型。如果你需要使用具有相同签名但不同实现的多个函数,则需要为它们使用 trait object

fn main() { let handlers = vec![returns_closure(), returns_initialized_closure(123)]; for handler in handlers { let output = handler(5); println!("{output}"); } } 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) }

此代码可以正常编译——但如果我们尝试坚持使用 impl Fn(i32) -> i32,则不会。有关 trait objects 的更多信息,请参阅 “使用允许不同类型值的 Trait Objects” 部分在第 18 章中。

接下来,让我们看看宏!