Rust的设计从已有的编程语言和技术中吸取了灵感,其中一项是函数式编程。这意味着可以将函数作为参数值进行传递,也可以返回函数作为返回值。可以将其分配给变量以便后续执行它们。
更具体的是:
- 闭包:一个类似函数的结构,你可以将其保存在变量中。
- 迭代器:处理一系列元素的方式。
- 怎样使用闭包和迭代器来改进之前的输入/输出项目
- 闭包和迭代器的性能
我们前面学习过的一些Rust特性,例如模式匹配、枚举类型,这些也会用到函数式编程。因为闭包和迭代器非常重要的,它可以让我们写成运行速度更快,更加地道的Rust代码。
15.1闭包
Rust的闭包是一个匿名函数,它可以保存在变量中,也可以作为参数传递给函数。你可以在某个地方创建一个闭包,在另一个地方调用这个闭包并在不同的场景下执行它。和函数不同,闭包可以获取同一作用域中的变量。接下来我们将示范这些闭包如何允许为代码所复用,如何自定义其行为。
15.1.1 捕获环境
我们先查看一下如何使用闭包来捕获来环境中的值,这些值是为了后续使用而定义的。有一个场景是:服装公司出版了一款专门的限量版T恤给那些在我们邮件列表中的人,专门作为促销使用。那些在邮件列表的人可以选择自己喜欢的T恤的颜色作为自己的个人资料。如果某个人设置了自己喜欢的颜色,他就会得到这种颜色的T恤。如果她没有设置喜欢的颜色,它就会得到库存量最大的那种颜色的T恤。
有很多方法可以实现此功能,例如:我们使用一个称之为ShirtColor的枚举类型,它有两个成员Red和Blue(限制可用颜色的数量便于简化程序)。我们使用一个Inventory的结构体来表示公司的库存,它有一个成员叫shirts,它是一个Vec<ShirtColor>类型,表示当前库存中所拥有的T恤的颜色。Inventory的方法giveaway会尽量返回客户喜爱的T恤颜色。代码如下:
rust
#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
Red,
Blue,
}
struct Inventory {
shirts: Vec<ShirtColor>,
}
impl Inventory {
fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
user_preference.unwrap_or_else(|| self.most_stocked())
}
fn most_stocked(&self) -> ShirtColor {
let mut num_red = 0;
let mut num_blue = 0;
for color in &self.shirts {
match color {
ShirtColor::Red => num_red += 1,
ShirtColor::Blue => num_blue += 1,
}
}
if num_red > num_blue {
ShirtColor::Red
} else {
ShirtColor::Blue
}
}
}
fn main() {
let store = Inventory {
shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
};
let user_pref1 = Some(ShirtColor::Red);
let giveaway1 = store.giveaway(user_pref1);
println!(
"用户喜欢的颜色是{:?},他获取的是{:?}",
user_pref1, giveaway1
);
let user_pref2 = None;
let giveaway2 = store.giveaway(user_pref2);
println!(
"用户喜欢的颜色是{:?},他获取的是{:?}",
user_pref2, giveaway2
);
}
主函数中定义的store中保存着两件蓝色的T恤,一件红色的T恤,用于本次限量版的T恤促销活动。我们调用了giveaway方法两次,一次客户设置了自己的偏爱的颜色,另一次客户没有做任何设置。
代码可以以不同的方式实现,但是这次关注点放在闭包的使用。在giveaway方法中,可以看到闭包的使用。它使用Option<ShirtColor>类型作为参数,参数名叫user_perference,然后它就可以调用unwrap_or_else方法,这个方法有标准库提供。该方法接收了闭包作为一个参数,该闭包不带任何参数,它会返回泛型T的值,该泛型是ShirtColor,即返回ShirtColor。如果user_perference包含了Some<T>,则返回T,如果是None则返回闭包的运行结果。
我们使用了闭包表达式"|| self.most_stocked()"作为参数传递给unwrap_to_else方法。这个闭包本身不带参数(如果闭包带参数,需将参数放置在两个竖杠内),闭包内部调用self.most_stocks()。我们虽然在这定义了闭包,但是unwarp_or_else的实现将在需要结果时再执行闭包。
执行上面的代码,结果如下所示:
rust
cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
Running `target\debug\lession15_001.exe`
用户喜欢的颜色是Some(Red),他获取的是Red
用户喜欢的颜色是None,他获取的是Blue
有趣的是我们传递一个闭包作为参数,这个闭包在当前Inventory实例上调用self.most_stocke()方法。标准库无需知道我们自己定义的Inventory和shirtColor类型。或者用于此场景下的业务逻辑。这个闭包会获取这个实例本身的self的不可变引用,并通过unwrap_or_else()方法传递到闭包内部。如果使用函数则无法获取该实例本身的引用。明确的定义接口是确保所有调用者按照此协议使用参数值和返回值的类型前提。但是,闭包不是用于暴露给所有用户的,它们存于变量中,它们不需要命名,也无需暴露给使用我们库的用户。
15.1.2 推断和声明闭包类型
函数和闭包有很多区别。闭包通常不需要声明参数类型或返回值,而函数通常是需要的。在函数签名中声明类型是因为类型是需要暴漏给用户显式接口的一部分。
闭包通常短小,只与很小的上下文范围相关,而不是用于任意场景中。在这些有限的上下文中,编译器可以图短处参数和返回值的类型,类似与它是怎么推断出大部分变量的类型(偶尔,编译器也需要闭包尽心类型声明)。
和变量一样,我们可以显式的增加变量声明,纯粹是为了代码清晰易懂,而不是真的必须。如下所示,我们声明了带有类型声明的闭包,并保存到一个变量中,而不是将它作为参数传递。
rust
use std::thread;
use std::time::Duration;
let expensive_closure = |num: u32| -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
闭包增加了类型声明就更像函数了。下面我们用一组实现相同功能函数和闭包作为对比,我们增加了一些空格,使其能够列对齐,这样就可以看出它们的相似性,并可以看出那些不同,那些可以省略掉。
rust
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
第一行是一个函数的定义;第二行是一个闭包的定义,它带有完整的类型声明;第三行,是我们移除了类型声明的闭包;第四行,我们去除了花括号,因为闭包内只有一行代码,所以外面的花括号可以省略掉。这些都是对的,运行它们都会有相同的输出。第三和四行,能够编译通过,是因为在它们被调用时可以推断出参数和返回值的类型。这就有点类似于let v=Vec::new()代码;它们要么在一开始声明类型,要么在插入数据时推断出数据类型。
对于闭包的定义,编译器会为它们所有的参数和返回值推断出一个具体的类型。下面的短小的闭包代码(纯属展示作用)会展示出:它有一个参数和返回值,参数即返回值。这个闭包定义中没有包含任何类型声明,由于没有任何类型声明,它可以使用任何类型作为参数,我们先使用字符串作为参数,然后使用整形数据作为参数,这时你会得到另一个错误信息:
rust
fn main() {
let example_x = |x| x;
let s = example_x(String::from("hello"));
let n = example_x(5);
}
运行代码后:
rust
cargo run
Blocking waiting for file lock on package cache
Compiling lession15_002 v0.1.0 (D:\projects\rust\rust_learn\lession15_002)
error[E0308]: mismatched types
--> src\main.rs:9:23
|
9 | let n = example_x(5);
| --------- ^ expected `String`, found integer
| |
| arguments to this function are incorrect
|
note: expected because the closure was earlier called with an argument of type `String`
--> src\main.rs:8:23
|
8 | let s = example_x(String::from("hello"));
| --------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
| |
| in this closure call
note: closure parameter defined here
--> src\main.rs:7:22
|
7 | let example_x = |x| x;
| ^
help: try using a conversion method
|
9 | let n = example_x(5.to_string());
| ++++++++++++
For more information about this error, try `rustc --explain E0308`.
error: could not compile `lession15_002` (bin "lession15_002") due to 1 previous error
第一次调用我们使用了字符串作为参数,编译器将x的类型推断为字符串,返回值也是字符串。这些类型和闭包绑死了,当第二次使用不同的类型,编译器就会报类型错误。
15.1.3 捕获引用或转移所有权
闭包可以以三种方式捕获它们环境中的值,类似于函数的三种方式:可变借用,不可变借用,占用所有权。基于函数所采用的相同策略,闭包就会决定使用哪一种。
下面的代码中,我们定义了一个闭包,它会捕获环境中的一个向量数组,并作为不可以变引用,代码如下所示:
rust
fn main() {
let list = vec![1, 2, 3];
println!("定义闭包前:{list:?}");
let only_borrows = || println!("闭包内使用同一作用域中的变量{list:?}");
println!("定义闭包后:{list:?}");
only_borrows();
println!("调用闭包后:{list:?}");
}
这段代码也展示了一个闭包可以赋值给一个变量,后面可以变量名后面跟着小括号来调用闭包,就像它是一个函数。
因为不可以变借用可以多次使用,所以list在闭包的定义前后,调用前后都可以使用。运行代码,输出信息如下所示:
rust
cargo run
Compiling lession15_003 v0.1.0 (D:\projects\rust\rust_learn\lession15_003)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.39s
Running `target\debug\lession15_003.exe`
定义闭包前:[1, 2, 3]
定义闭包后:[1, 2, 3]
闭包内使用同一作用域中的变量[1, 2, 3]
调用闭包后:[1, 2, 3]
接下来修改闭包内的代码,为list变量增加一个元素,这样闭包捕获的变量作为可变借用:
rust
fn main() {
let mut list = vec![1, 2, 3];
println!("定义闭包前:{list:?}");
let mut mut_borrows = || list.push(7);
// println!("定义闭包后:{list:?}");
mut_borrows();
println!("调用闭包后:{list:?}");
}
运行上面的代码,输出信息如下所示:
rust
cargo run
Compiling lession15_004 v0.1.0 (D:\projects\rust\rust_learn\lession15_004)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.90s
Running `target\debug\lession15_004.exe`
定义闭包前:[1, 2, 3]
调用闭包后:[1, 2, 3, 7]
注意在闭包mut_borrows的定义和调用之间不可以有println!宏使用list变量,因为闭包定义时,它捕获了变量list的可变借用等于独占list的所有权,当闭包调用之后,list的可变调用结束,list的所有权也被返回了,这是可以还可以进行不可变借用的调用,但是可变借用不能用了。也就是说闭包只能调用一次,因为可变借用在第一次调用完后就结束了。在可变借用之间,也不允许不可变即用。
如果要强占所有权(即使不需要),需要在参数列表前使用move关键字。
这在将闭包传递到一个新的进程中,它所引用的数据也需要移入新进程时是非常有用的。之后会详细讨论线程和同步的概念,现在我们只是简单的研究一下使用move关键字转移所有权到闭包之内的用法。修改之前的代码,将在新的进程内输出list向量数组的信息。
rust
use std::thread;
fn main() {
let mut list = vec![1, 2, 3];
println!("闭包定义前的list:{list:?}");
thread::spawn(move || {
list.push(8);
list.extend(9..100);
println!("线程内的list:{list:?}");
})
.join()
.unwrap();
}
我们创建了一个新线程,并把一个闭包作为参数传递给新线程。闭包的内容是输出显示list向量数组中的成员。闭包使用不可变引用捕获list变量,因为这是显示输出所需的最小权限。在本示例中,即使闭包只需要不可变引用,我们还是需要使用move关键字(在闭包定义之前)将list的所有权转移到闭包内。如果主线程在使用新线程调用join之前执行更多的操作,新线程可能在主线程完成其余操作之前就结束了,也可能新线程结束之前主线程先结束。如果主线程保留了list 的所有权,但是在新线程结束前结束了,它会销毁掉list,在新线程中的list虽然是不可变引用,也会由于无效而引起异常。因此编译器会强迫要求将list的所有权转移至新线程内,以便在新线程该引用一致有效。你可以尝试删除move关键字,或闭包定义之后调用list,看看编译器会给你输出什么错误信息。
15.1.4 将捕获的值转移出闭包
当闭包从作用域中捕获了某个值的引用或所有权(如果要施加一些处理的话,无论什么,都必须转移所有权到闭包内),闭包内代码对这些引用或值施加了一些处理,但是在闭包结束后还需要后续的操作,则需要将所有权移出闭包(无论什么处理,只要有,就需将所有权要移出闭包)。
闭包体可以做下列事情:将捕获的值所有权移出闭包,变更捕获的值,既不转移所有权,也不修改值,或者从一开始就不从作用域中捕获任何东西。
闭包从作用域中捕获和处理值的方式会决定闭包实现那一个接口,这些接口规定了函数或结构体能够使用哪一种闭包。闭包将会自动实现这些Fn接口中的一种、两种或全部三种。根据闭包内部如何处理这些值,可以按照累加的方式来实现不同的接口。
- FnOnce:该闭包只能调用一次。所有闭包的实现都会包含该接口,因为所有的闭包都会调用。适用于哪些需要将捕获的值移出闭包的操作(这种操作只能调用一次)。
- FnMut:适用于不会将捕获的值移出闭包,但是会修改这些值。这种闭包可以调用多次。
- Fn:使用于那些不会将捕获的值移出闭包也不修改值的闭包。或者那些从作用域中不捕获任何东西的闭包。这些闭包可以调用多次而不会修改作用域中内容,只对于同时调用很多次的闭包是非常有用的。
我们看一下Option<T>的方法unwrap_or_else的定义:
rust
impl<T> Option<T> {
pub fn unwrap_or_else<F>(self, f: F) -> T
where
F: FnOnce() -> T
{
match self {
Some(x) => x,
None => f(),
}
}
}
T是泛型,它通常都是保存在Option中的Some变量中,T也是unwrap_or_else方法的返回类型,例如:Option<String>调用unwrap_or_else方法后会返回一个Stirng。
unwrap_or_else还有一个泛型参数F。F类型定义了一个叫f的参数,它是一个闭包,当我们调用unwrap_or_else方法是提供给它的。
接口限定范式指定了泛型F的接口类型是FnOnce() -> T,它意味着F只能被调用一次,它没有参数,返回类型为T。在接口限制范式中使用FnOnce代表了某种限制,即unwrap_or_else只能调用f一次。在unwrap_or_else内部,可以看到如果Option是Some,f不会被调用,如果是None,f会被调用一次。因为所有的闭包都实现了FnOnce,unwrap_or_else可以接受三种类型的闭包,因此它具有最大的灵活性。
注意:如果我们想要做的事不需要捕获作用域中的值,我们可以使用一个函数名,而不需要使用闭包,只要这个函数实现了Fn接口。例如在Option<Vec<T>>中,当这个值是None时,我们可以调用unwrap_or_else(Vec::new)来获取一个新的、空的向量数组。编译器会自动实现适用于函数定义的某个Fn接口。
现在我们可以看看标准库方法sort_by_key方法,它定义在切片中,看看它是如何区别于unwrap_or_else方法的,为什么sort_by_key使用FnMut,而不是FnOnce作为接口限制规范。
这个闭包会获取切片中某个元素的引用作为参数,并返回一个K类型的值,它是可以排序的,这个函数用于对每一个成员项的特殊属性进行某个切片排序。例如:我们有一个Rectangle数组实例,我们使用sort_by_key来对应width进行排序(按照从低到高的顺序)。
rust
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle {
width: 10,
height: 1,
},
Rectangle {
width: 3,
height: 5,
},
Rectangle {
width: 7,
height: 12,
},
];
list.sort_by_key(|r| r.width);
println!("排序后的list:{list:#?}");
}
运行上面的代码,输出的结果为:
rust
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.96s
Running `target\debug\lession15_006.exe`
排序后的list:[
Rectangle {
width: 3,
height: 5,
},
Rectangle {
width: 7,
height: 12,
},
Rectangle {
width: 10,
height: 1,
},
]
sort_by_key被定义为FnMut闭包的原因是它需要多次调用这个闭包:每次使用数组中的一个成员。闭包|r| r.width不会捕获、修改或转移所有权离开它的作用域,因此它可以满足接口限制规范的要求。
相反,下面的示例展示了闭包实现了FnOnce接口,因为它将某个值的所有权移走了。这种情况下,编译器不会让我们使用sort_by_key方法。
rust
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle {
width: 10,
height: 1,
},
Rectangle {
width: 3,
height: 5,
},
Rectangle {
width: 7,
height: 12,
},
];
let mut sort_operations=vec![];
let value = String::from("closure called");
list.sort_by_key( |r| {
sort_operations.push(value);
r.width});
println!("排序后的list:{list:#?}");
println!("修改后的数组:{sort_operations:#?}");
}
我们人为增加了复杂性来统计闭包调用的次数,但是程序不能运行。这段代码尝试向sort_operations数组添加字符串来统计次数------这个字符串来自同一作用域。闭包捕获了value,接着在闭包内将value的所有权转移到sort_operations数组。这个闭包只能调用一次;如果再次调用就会报错,因为value的所有权不在作用域内,因此不能再次将其插入到sort_operations中。基于上面的原因,这个闭包只能实现FnOnce接口。当我们尝试编译这段代码,编译器会显示:value不能将所有权移出闭包,因为闭包必须实现FnMut接口。
rust
cargo run
Compiling lession15_007 v0.1.0 (D:\projects\rust\rust_learn\lession15_007)
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
--> src\main.rs:30:30
|
28 | let value = String::from("closure called");
| ----- ------------------------------ move occurs because `value` has type `String`, which does not implement the `Copy` trait
| |
| captured outer variable
29 | list.sort_by_key( |r| {
| --- captured by this `FnMut` closure
30 | sort_operations.push(value);
| ^^^^^ `value` is moved here
|
help: `Fn` and `FnMut` closures require captured values to be able to be consumed multiple times, but `FnOnce` closures may consume them only once
--> C:\Users\刘海涛\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib/rustlib/src/rust\library\alloc\src\slice.rs:249:12
|
249 | F: FnMut(&T) -> K,
| ^^^^^^^^^^^^^^
help: consider cloning the value if the performance cost is acceptable
|
30 | sort_operations.push(value.clone());
| ++++++++
错误信息明确指明闭包内部将value所有权迁移出作用域的那一行。为了修复这个问题,你需要闭包内的代码,使它不会将所有权迁移出它所在的作用域。在环境中保留一个计数器,当闭包调用时增加其值,这是一个简单有效的解决方案。下面的代码在闭包中使用了num_sort_operations计数器,它是一个可以捕获到的可变变量的引用,因此它可以被多值调用。
rust
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle {
width: 10,
height: 1,
},
Rectangle {
width: 3,
height: 5,
},
Rectangle {
width: 7,
height: 12,
},
];
let mut num_sort_operations = 0;
list.sort_by_key( |r| {
num_sort_operations+=1;
r.width});
println!("排序后的list:{list:#?}");
println!("闭包运行的次数:{num_sort_operations:?}");
}
当函数或类型有用到闭包,或定义时,Fn接口是很重要的。下一节,我们会讨论迭代器。许多迭代器方法都使用闭包作为参数,后面我们会继续探索闭包的细节。