6.9 KiB
闭包
ch13-01-closures.md
commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
Rust 提供了定义闭包的能力,它类似于函数。让我们先不从技术上的定义开始,而是看看闭包语句结构,然后再返回他们的定义。列表 13-1 展示了一个被赋值给变量add_one
的小的闭包定义,之后可以用这个变量来调用闭包:
Filename: src/main.rs
fn main() {
let add_one = |x| x + 1;
let five = add_one(4);
assert_eq!(5, five);
}
Listing 13-1: A closure that takes one parameter and adds
one to it, assigned to the variable add_one
闭包的定义位于第一行,展示了闭包获取了一个叫做x
的参数。闭包的参数位于竖线之间(|
)。
这是一个很小的闭包,它只包含一个表达式。列表 13-2 展示了一个稍微复杂一点的闭包:
Filename: src/main.rs
fn main() {
let calculate = |a, b| {
let mut result = a * 2;
result += b;
result
};
assert_eq!(7, calculate(2, 3)); // 2 * 2 + 3 == 7
assert_eq!(13, calculate(4, 5)); // 4 * 2 + 5 == 13
}
Listing 13-2: A closure with two parameters and multiple expressions in its body
可以通过大括号来定义多于一个表达式的闭包体。
你会注意到一些闭包不同于fn
关键字定义的函数的地方。第一个不同是并不需要声明闭包的参数和返回值的类型。也可以选择加上类型注解;列表 13-3 展示了列表 13-1 中闭包带有参数和返回值类型注解的版本:
Filename: src/main.rs
fn main() {
let add_one = |x: i32| -> i32 { x + 1 };
assert_eq!(2, add_one(1));
}
Listing 13-3: A closure definition with optional parameter and return value type annotations
在带有类型注解的情况下闭包的语法于函数就更接近了。让我们来更直接的比较一下不同闭包的语法与函数的语法。这里增加了一些空格来对齐相关的部分:
fn add_one_v1 (x: i32) -> i32 { x + 1 } // a function
let add_one_v2 = |x: i32| -> i32 { x + 1 }; // the full syntax for a closure
let add_one_v3 = |x| { x + 1 }; // a closure eliding types
let add_one_v4 = |x| x + 1 ; // without braces
定义闭包时不要求类型注解而在定义函数时要求的原因在于函数是显式暴露给用户的接口的一部分,所以为了严格的定义接口确保所有人都同意函数使用和返回的值类型是很重要的。但是闭包并不像函数那样用于暴露接口:他们存在于绑定中并直接被调用。强制标注类型就等于为了很小的优点而显著的降低了工程性(本末倒置)。
不过闭包的定义确实会推断每一个参数和返回值的类型。例如,如果用i8
调用列表 13-1 中没有类型注解的闭包,如果接着用i32
调用同一闭包则会得到一个错误:
Filename: src/main.rs
let add_one = |x| x + 1;
let five = add_one(4i8);
assert_eq!(5i8, five);
let three = add_one(2i32);
编译器给出如下错误:
error[E0308]: mismatched types
-->
|
7 | let three = add_one(2i32);
| ^^^^ expected i8, found i32
因为闭包是直接被调用的所以能可靠的推断出其类型,再强制要求标注类型就显得有些冗余了。
闭包与函数语法不同还有另一个原因是,它与函数有着不同的行为:闭包拥有其环境(上下文)。
闭包可以引用其环境
我们知道函数只能使用其作用域内的变量,或者要么是const
的要么是被声明为参数的。闭包则可以做的更多:闭包允许使用包含他们的作用域的变量。列表 13-4 是一个在equal_to_x
变量中并使用其周围环境中变量x
的闭包的例子:
Filename: src/main.rs
fn main() {
let x = 4;
let equal_to_x = |z| z == x;
let y = 4;
assert!(equal_to_x(y));
}
Listing 13-4: Example of a closure that refers to a variable in its enclosing scope
这里。即便x
并不是equal_to_x
的一个参数,equal_to_x
闭包也被允许使用它,因为变量x
定义于同样定义equal_to_x
的作用域中。并不允许在函数中进行与列表 13-4 相同的操作;尝试这么做看看会发生什么:
Filename: src/main.rs
fn main() {
let x = 4;
fn equal_to_x(z: i32) -> bool { z == x }
let y = 4;
assert!(equal_to_x(y));
}
我们会得到一个错误:
error[E0434]: can't capture dynamic environment in a fn item; use the || { ... }
closure form instead
-->
|
4 | fn equal_to_x(z: i32) -> bool { z == x }
| ^
编译器甚至提醒我们这只能用于闭包!
获取他们环境中值的闭包主要用于开始新线程的场景。我们也可以定义以闭包作为参数的函数,通过使用Fn
trait。这里是一个函数call_with_one
的例子,它的签名有一个闭包参数:
fn call_with_one<F>(some_closure: F) -> i32
where F: Fn(i32) -> i32 {
some_closure(1)
}
let answer = call_with_one(|x| x + 2);
assert_eq!(3, answer);
我们将|x| x + 2
传递给了call_with_one
,而call_with_one
用1
作为参数调用了这个闭包。some_closure
调用的返回值接着被call_with_one
返回。
call_with_one
的签名使用了第十章 trait 部分讨论到的where
语法。some_closure
参数有一个泛型类型F
,它在where
从句中被定义为拥有Fn(i32) -> i32
trait bound。Fn
trait 代表了一个闭包,而且可以给Fn
trait 增加类型来代表一个特定类型的闭包。在这种情况下,闭包拥有一个i32
的参数并返回一个i32
,所以泛型的 trait bound 被指定为Fn(i32) -> i32
。
在函数签名中指定闭包要求使用泛型和 trait bound。每一个闭包都有一个独特的类型,所以不能写出闭包的类型而必须使用泛型。
Fn
并不是唯一可以指定闭包的 trait bound,事实上有三个:Fn
、FnMut
和FnOnce
。这是在 Rust 中经常见到的三种模式的延续:借用、可变借用和获取所有权。用Fn
来指定可能只会借用其环境中值的闭包。用FnMut
来指定会修改环境中值的闭包,而如果闭包会获取环境值的所有权则使用FnOnce
。大部分情况可以从Fn
开始,而编译器会根据调用闭包时会发生什么来告诉你是否需要FnMut
或FnOnce
。
为了展示拥有闭包作为参数的函数的应用场景,让我们继续下一主题:迭代器。