19.3 高级函数和闭包
19.3.1 函数指针
我们已经讲过把闭包传进函数。实际上,我们也可以把函数传进函数。
在传递时,函数会被强制转换成 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!("The answer is: {answer}");
}
do_twice 的第一个参数 f 的类型是 fn,也就是函数指针。它期望一个参数类型为 i32、返回类型也为 i32 的函数。函数体中调用了两次 f。
输出:
The answer is: 12
函数指针与闭包的区别
闭包至少实现了 Fn、FnOnce 和 FnMut 这三个 trait 之一。 函数指针 fn 是一个类型,不是 trait。我们可以直接把 fn 指定为参数类型,而不必声明一个以 Fn trait 为约束的泛型参数。
函数指针实现了全部三种闭包 trait,也就是 Fn、FnOnce 和 FnMut。 所以你总是可以把函数指针作为参数传给接受闭包的函数。正因为如此,我们编写函数时通常更倾向于使用带闭包 trait 的泛型参数,因为这样函数既能接受闭包,也能接受普通函数。
在某些情况下,我们可能想接受 fn 类型而不是闭包,例如与不支持闭包的代码交互时,比如 C 函数。该怎么写呢?
看一个例子:
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();
// 分行只是为了可读性,并不是必须的
}
list_of_numbers 中的元素是 i32,我们想把它们转换成 String 赋给 list_of_strings。步骤是:
- 先用
iter产生一个迭代器 - 再用
map中的闭包|i| i.to_string()转换每个元素 - 最后用
collect把所有转换后的元素收集成一个集合
这段代码也可以这样写:
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();
}
区别在于 .map(ToString::to_string),这里直接传入了 to_string 函数。效果与上一版相同。顺便一提,ToString::to_string 使用了 19.2. 高级 trait:关联类型、默认泛型参数和运算符重载、完全限定语法、supertrait 和 newtype 讨论过的完全限定语法。
来看一下 map 的定义:
#![allow(unused)]
fn main() {
fn map<B, F>(self, f: F) -> Map<Self, F>
where
Self: Sized,
F: FnMut(Self::Item) -> B
}
map 要求 f 实现 FnMut trait,而闭包和函数指针都满足这个要求,所以两者都可以传入。
再看另一个例子:
fn main() {
enum Status {
Value(u32),
Stop,
}
let list_of_statuses: Vec<Status> = (0u32..20)
.map(Status::Value)
.collect();
}
注意 map 的参数。我们使用构造函数 Status::Value,对范围内的每个 u32 调用 map,并创建 Status::Value 实例。
有人可能会问:Status::Value 不是枚举变体吗?怎么变成函数了?这是因为在 Rust 中,这样的构造函数被实现为接收一个参数并返回新实例的函数。换句话说:
#![allow(unused)]
fn main() {
let v = Status::Value(3);
}
这只是一个例子。这里初始化了 v,而 Status::Value(3) 可以看作一次构造函数调用:3 是构造函数的参数。由于构造函数被实现为函数,我们可以把它们当作函数来用,3 就是它们的参数。
所以我们也可以把这类构造函数用作实现了闭包 trait 的函数指针。
19.3.2 返回闭包
闭包通过 trait 来表达,因此不能直接从函数返回闭包。相反,你可以返回一个实现了该 trait 的具体类型。
例如:
#![allow(unused)]
fn main() {
fn returns_closure() -> dyn Fn(i32) -> i32 {
|x| x + 1
}
}
这个函数试图直接返回一个闭包。
输出:
error[E0746]: return type cannot be a trait object without pointer indirection
--> 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() -> dyn Fn(i32) -> i32 {
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
Rust 不知道需要多少空间来存储这个闭包,所以会报错。
还记得我们以前在哪里遇到过同样的“Rust 不知道该分配多少空间”的错误吗?没错——在学习链表时。当时的解决办法是用 Box<T> 包裹链表,这里也可以这样做:
#![allow(unused)]
fn main() {
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
}
因为返回值位于指针之后,返回类型现在在编译时就有了已知大小。