Rust 学习笔记:高级函数和闭包

Rust 学习笔记:高级函数和闭包

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

函数指针

我们可以将常规函数传递给函数!当你想传递一个已经定义的函数而不是定义一个新的闭包时,这种技术非常有用。函数强制转换为 fn 类型(带有小写的 f),不要与 Fn 闭包 trait 混淆。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!("{answer}");
}

我们定义了一个函数 add_one,它将参数加 1。函数 do_twice 接受两个形参:一个函数指针指向接受 i32 形参并返回 i32 的任何函数,另一个是一个 i32 值。do_twice 函数调用函数 f 两次,将 arg 值传递给它,然后将两次函数调用结果相加。

这段代码打印 12。

与闭包不同,fn 是一个类型而不是一个 trait,所以我们直接指定 fn 作为参数类型,而不是声明一个泛型类型参数,其中带一个 fn trait 作为 trait 约束。

函数指针实现了所有三个闭包 trait(Fn、FnMut 和 FnOnce),这意味着对于期望闭包的函数,你始终可以将函数指针作为参数传递。

最好使用泛型类型和闭包特征之一来编写函数,这样你的函数就可以接受函数或闭包。

当你与没有闭包的外部代码进行接口时,你希望只接受 fn 而不接受闭包的一个示例是:C 函数可以接受函数作为参数,但 C 没有闭包。

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

    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 的参数,而不是闭包。

    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 trait 中定义的 to_string 函数,标准库已经为任何实现 Display 的类型实现了该函数。

每个枚举变量的名称就是一个初始化函数。我们可以将这些初始化函数用作实现闭包 trait 的函数指针,这意味着我们可以将初始化函数指定为接受闭包的方法的参数。

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 作为返回类型。

相反,我们通常使用 impl Trait 语法。你可以使用 Fn、FnOnce 和 FnMut 返回任何函数类型。

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

然而,每个闭包也是它自己独特的类型。如果你需要处理具有相同签名但实现不同的多个函数,则需要为它们使用 trait 对象。

考虑一下,如果编写下列代码会发生什么。

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)];
    for handler in handlers {
        let output = handler(5);
        println!("{output}");
    }
}

这里我们有 returns_closure 和 returns_initialized_closure 两个函数,它们都返回 impl Fn(i32) -> i32。注意,它们返回的闭包是不同的,尽管它们实现了相同的类型。Rust 告诉我们编译错误:

在这里插入图片描述

错误消息告诉我们,每当我们返回一个 impl Trait 时,Rust 就会创建一个唯一的不透明类型,在这个类型中,我们无法看到 Rust 为我们构造的细节。因此,即使这两个函数都返回实现相同 trait(Fn(i32) -> i32)的闭包,Rust 为每个函数生成的不透明类型是不同的。

这类似于 Rust 如何为不同的异步块生成不同的具体类型,即使它们具有相同的输出类型。

这个问题的一种解决方案是使用 trait 对象。

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

这样就能通过编译了。

有关 trait 对象的更多信息,请参阅:Rust 学习笔记:trait 对象

练习题

考虑以两种方式实现一个注册函数,该函数接受一个回调函数。

fn register1(cb: fn(event) -> ());
fn register2(cb: F) where F: Fn(event) -> ();

哪种类型签名允许注册函数接受最广泛的参数类型?

A. register1
B. register2
C. 这两个签名是等价的

答:B。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

UestcXiye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值