Rust编程学习 - 如何学习有关函数和闭包的高级特性,这包括函数指针以及返回闭包

高级函数与闭包

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

函数指针

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

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

文件名:src/main.rs

rust 复制代码
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-28/src/main.rs}}

示例 20-28: 使用 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 语言没有闭包。

作为一个既可以使用内联定义的闭包又可以使用命名函数的例子,让我们看看一个标准库中 Iterator trait 提供的 map 方法的应用。使用 map 函数将一个数字 vector 转换为一个字符串 vector,就可以使用闭包,如示例 20-29 所示:

rust 复制代码
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-29/src/main.rs:here}}

示例 20-29:使用闭包和 `map` 方法将数字转换为字符串

或者可以将函数作为 map 的参数来代替闭包,如示例 20-30 所示:

rust 复制代码
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-30/src/main.rs:here}}

示例 20-30:使用 `String::to_string` 方法将数字转换为字符串

注意这里必须使用 ["高级 trait"][advanced-traits] 部分讲到的完全限定语法,因为存在多个叫做 to_string 的函数。

这里使用了定义于 ToString trait 的 to_string 函数,标准库为所有实现了 Display 的类型实现了这个 trait。

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

rust 复制代码
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-31/src/main.rs:here}}

示例 20-31:使用枚举构造函数和 `map` 方法从数字创建 `Status` 实例

这里,我们通过 Status::Value 的初始化函数,对 map 所作用的范围内每个 u32 值创建 Status::Value 实例。一些人倾向于函数式风格,一些人喜欢闭包。它们会编译成相同的代码,因此请选择对你来说更清晰的那一种。

返回闭包

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

相反,可以正常地使用第十章所学的 impl Trait 语法。可以使用 FnFnOnceFnMut 返回任何函数类型。例如,示例 20-32 中的代码就可以正常工作。

rust 复制代码
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-32/src/lib.rs}}

示例 20-32:使用 `impl Trait` 语法从函数返回闭包

然而,如我们在 ["闭包类型推断和注解"][closure-types] 中所注意到的,每一个闭包也有其独立的类型。如果你需要处理多个拥有相同签名但是不同实现的函数,就需要使用 trait 对象。考虑一下如果编写类似示例 20-33 中所示代码会发生什么。

rust,ignore,does_not_compile 复制代码
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-33/src/main.rs}}

示例 20-33:创建一个由返回 `impl Fn` 的函数定义的闭包的 `Vec`

这里有两个函数,returns_closurereturns_initialized_closure,它们都返回 impl Fn(i32) -> i32。注意它们返回的闭包是不同的,即使它们实现了相同的类型。如果尝试编译这段代码,Rust 会告诉我们这不可行:

text 复制代码
{{#include ../listings/ch20-advanced-features/listing-20-33/output.txt}}

错误信息告诉我们每当返回一个 impl Trait Rust 会创建一个独特的不透明类型opaque type ),这是一个无法看清 Rust 为我们构建了什么细节的类型。所以即使这些函数都返回了实现了相同 trait( Fn(i32) -> i32)的闭包,Rust 为我们生成的不透明类型也是不同的。这类似于 Rust 如何为不同的异步代码块生成不同的具体类型,即使它们有着相同的输出类型,如第十七章 ["使用任意数量的 futures"][any-number-of-futures] 所示。我们已经多次看到这个问题的解决方案:我们可以使用 trait 对象,如示例 20-34 所示。

rust 复制代码
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-34/src/main.rs:here}}

示例 20-34:创建一个由返回 `Box` 的函数定义的闭包的 `Vec` 以便它们有相同的类型

这段代码正好可以编译。关于 trait 对象的更多内容,请回顾第十八章的 [顾及不同类型值的 trait 对象"][using-trait-objects-that-allow-for-values-of-different-types] 部分。


作为最后的项目,我们将要实现一个返回 "hello" 的 web server,它在浏览器中看起来就如图 21-1 所示:

如下是构建 web server 的计划:

  1. 学习一些 TCP 与 HTTP 知识
  2. 在套接字(socket)上监听 TCP 请求
  3. 解析少量的 HTTP 请求
  4. 创建一个合适的 HTTP 响应
  5. 通过线程池改善 server 的吞吐量

在开始之前,我们先提两点说明。首先,这里使用的方法并不是使用 Rust 构建 web server 的最佳方式。crates.io 上有很多可用于生产环境的 crate,它们提供了比我们所要编写的更为完整的 web server 和线程池实现。然而,本章的目的在于学习,而不是走捷径。因为 Rust 是一个系统编程语言,我们能够选择处理什么层次的抽象,并能够选择比其他语言可能或可用的层次更低的层次。

其次,我们不会在这里使用 async 和 await。构建线程池本身已经是一个相当大的挑战,无需再加入构建异步运行时的复杂度!不过,我们会指出 async 和 await 在本章中会遇到的一些问题上的可能应用。

因此我们将手动编写一个基础的 HTTP server 和线程池,以便学习将来可能用到的 crate 背后的通用理念和技术。

相关推荐
LBuffer2 小时前
破解入门学习笔记题三十四
java·笔记·学习
_pass_2 小时前
flask 框架的ORM 学习及应用
学习·flask·orm
哈乐2 小时前
网工应用题:配置命令补全类题目
服务器·前端·网络
张人玉2 小时前
C# TCP 服务器和客户端
服务器·tcp/ip·c#
雯0609~2 小时前
宝塔配置:IP文件配置,根据端口配置多个项目文件(不配置域名的情况)
服务器·网络协议·tcp/ip
河南博为智能科技有限公司2 小时前
RS485转以太网串口服务器-串口设备联网的理想选择
大数据·服务器·人工智能·单片机·嵌入式硬件·物联网
JanelSirry2 小时前
Redis服务器的的内存是多大
服务器·redis·github
再睡一夏就好2 小时前
【C++闯关笔记】unordered_map与unordered_set的底层:哈希表(哈希桶)
开发语言·c++·笔记·学习·哈希算法·散列表
potato_15542 小时前
现代C++核心特性——内存篇
开发语言·c++·学习