高级函数和闭包

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

函数指针

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

指定参数为函数指针的语法与闭包的语法相似,如清单 20-28 所示,我们定义了一个将参数加一的函数 add_one。函数 do_twice 接受两个参数:一个指向接受 i32 参数并返回 i32 的任何函数的函数指针,和一个 i32 值。do_twice 函数调用函数 f 两次,将 arg 值传递给它,然后将两次函数调用的结果相加。main 函数使用参数 add_one5 调用 do_twice

Filename: 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}");
}
Listing 20-28: Using the fn type to accept a function pointer as an argument

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

与闭包不同,fn 是一个类型而不是特质,因此我们直接将 fn 指定为参数类型,而不是声明一个带有 Fn 特质之一作为特质约束的泛型类型参数。

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

也就是说,一个你只想接受fn而不接受闭包的例子是在与没有闭包的外部代码交互时:C 函数可以接受函数作为参数,但 C 没有闭包。

作为可以使用内联定义的闭包或命名函数的示例,让我们看看标准库中 Iterator 特性提供的 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();
}

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

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

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

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

在这里我们使用 map 被调用的范围中的每个 u32 值来创建 Status::Value 实例, 通过使用 Status::Value 的初始化函数。 有些人喜欢这种风格,有些人则喜欢使用闭包。它们 编译成相同的代码,所以使用对你来说更清晰的风格。

返回闭包

闭包由特征(traits)表示,这意味着你不能直接返回闭包。在大多数你可能想要返回特征的情况下,你可以使用实现该特征的具体类型作为函数的返回值。然而,你不能对闭包这样做,因为它们没有可返回的具体类型;例如,你不被允许使用函数指针 fn 作为返回类型。

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

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

然而,正如我们在第 13 章的“闭包类型推断和注解”部分中提到的,每个闭包也是其自身独特类型。如果你需要处理具有相同签名但实现不同的多个函数,你需要为它们使用一个特质对象:

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 对象的更多信息,请参阅第 18 章的 “使用允许不同类型的值的 trait 对象” 部分。

接下来,让我们看看宏!