闭包
闭包是一种匿名函数,它可以赋值给变量也可以作为参数传递给其它函数,不同于函数的是,它允许捕获调用者作用域中的值:
fn main() {
let x = 1;
let sum = |y| x + y;
assert_eq!(3, sum(2));
}rust
上面的代码展示了非常简单的闭包 sum,它拥有一个入参 y,同时捕获了作用域中的 x 的值,因此调用 sum(2) 意味着将 2(参数 y)跟 1(x)进行相加,最终返回它们的和:3。
Rust 闭包的形式定义:
|param1, param2,...| { 语句1; 语句2; 返回表达式 }rust
如果只有一个返回表达式的话,定义可以简化为:
|param1| 返回表达式rust
闭包的类型推导
Rust 是静态语言,因此所有的变量都具有类型,但是得益于编译器的强大类型推导能力,在很多时候我们并不需要显式地去声明类型,但是显然函数并不在此列,必须手动为函数的所有参数和返回值指定类型,原因在于函数往往会作为 API 提供给你的用户,因此你的用户必须在使用时知道传入参数的类型和返回值类型。
与函数相反,闭包并不会作为 API 对外提供,因此它可以享受编译器的类型推导能力,无需标注参数和返回值的类型。
为了增加代码可读性,有时候我们会显式地给类型进行标注,出于同样的目的,也可以给闭包标注类型:
let sum = |x: i32, y: i32| -> i32 {
x + y
}rust
与之相比,不标注类型的闭包声明会更简洁些:let sum = |x, y| x + y,需要注意的是,针对 sum 闭包,如果你只进行了声明,但是没有使用,编译器会提示你为 x, y 添加类型标注,因为它缺乏必要的上下文:
let sum = |x, y| x + y;
let v = sum(1, 2);rust
这里我们使用了 sum,同时把 1 传给了 x,2 传给了 y,因此编译器才可以推导出 x,y 的类型为 i32。
当编译器推导出一种类型后,它就会一直使用该类型:
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);rust
首先,在 s 中,编译器为 x 推导出类型 String,但是紧接着 n 试图用 5 这个整型去调用闭包,跟编译器之前推导的 String 类型不符,因此报错:
error[E0308]: mismatched types
--> src/main.rs:5:29
|
5 | let n = example_closure(5);
| ^
| |
| expected struct `String`, found integer // 期待String类型,却发现一个整数
| help: try using a conversion method: `5.to_string()`bash
结构体中的闭包
假设我们要实现一个简易缓存,功能是获取一个值,然后将其缓存起来,那么可以这样设计:
- 一个闭包用于获取值
- 一个变量,用于存储该值
可以使用结构体来代表缓存对象,最终设计如下:
struct Cacher<T>
where
T: Fn(u32) -> u32,
{
query: T,
value: Option<u32>,
}rust
这里使用泛型而不是直接在 query
上声明闭包是因为:每一个闭包实例都有独属于自己的类型,即使于两个签名一模一样的闭包,它们的类型也是不同的。
而标准库提供的 Fn
系列特征,再结合特征约束,就能很好的解决了这个问题. T: Fn(u32) -> u32
意味着 query
的类型是 T
,该类型必须实现了相应的闭包特征 Fn(u32) -> u32
。
需要注意的是,其实 Fn 特征不仅仅适用于闭包,还适用于函数,因此上面的 query 字段除了使用闭包作为值外,还能使用一个具名的函数来作为它的值
接着,为缓存实现方法:
impl<T> Cacher<T>
where
T: Fn(u32) -> u32,
{
fn new(query: T) -> Cacher<T> {
Cacher {
query,
value: None,
}
}
// 先查询缓存值 `self.value`,若不存在,则调用 `query` 加载
fn value(&mut self, arg: u32) -> u32 {
match self.value {
Some(v) => v,
None => {
let v = (self.query)(arg);
self.value = Some(v);
v
}
}
}
}rust
捕获作用域的值
fn main() {
let x = 4;
let equal_to_x = |z| z == x;
let y = 4;
assert!(equal_to_x(y));
}rust
上面代码中,x
并不是闭包 equal_to_x
的参数,但是它依然可以去使用 x
,因为 equal_to_x
在 x
的作用域范围内。
对于函数来说,就算定义在 main 函数体中,它也不能访问 x:
fn main() {
let x = 4;
fn equal_to_x(z: i32) -> bool {
z == x
}
let y = 4;
assert!(equal_to_x(y));
}rust
报错如下:
error[E0434]: can't capture dynamic environment in a fn item // 在函数中无法捕获动态的环境 --> src/main.rs:5:14 | 5 | z == x | ^ | = help: use the `|| { ... }` closure form instead // 使用闭包替代bash
当闭包从环境中捕获一个值时,会分配内存去存储这些值。对于有些场景来说,这种额外的内存分配会成为一种负担。与之相比,函数就不会去捕获这些环境值,因此定义和使用函数不会拥有这种内存负担。
三种闭包函数特征
闭包捕获变量有三种途径,恰好对应函数参数的三种传入方式:转移所有权、可变借用、不可变借用,因此相应的 Fn 特征也有三种。
FnOnce
FnOnce
,该类型的闭包会拿走被捕获变量的所有权。Once 顾名思义,说明该闭包只能运行一次:
fn fn_once<F>(func: F)
where
F: FnOnce(usize) -> bool,
{
println!("{}", func(3));
println!("{}", func(4));
}
fn main() {
let x = vec![1, 2, 3];
fn_once(|z|{z == x.len()})
}rust
仅实现 FnOnce 特征的闭包在调用时会转移所有权,所以显然不能对已失去所有权的闭包变量进行二次调用:
error[E0382]: use of moved value: `func`
--> src\main.rs:6:20
|
1 | fn fn_once<F>(func: F)
| ---- move occurs because `func` has type `F`, which does not implement the `Copy` trait
// 因为`func`的类型是没有实现`Copy`特性的 `F`,所以发生了所有权的转移
...
5 | println!("{}", func(3));
| ------- `func` moved due to this call // 转移在这
6 | println!("{}", func(4));
| ^^^^ value used here after move // 转移后再次用
|bash
实际上为闭包加上 Copy
特征就可以二次调用:
fn fn_once<F>(func: F)
where
F: FnOnce(usize) -> bool + Copy,// 改动在这里
{
println!("{}", func(3));
println!("{}", func(4));
}
fn main() {
let x = vec![1, 2, 3];
fn_once(|z|{z == x.len()})
}rust
上面代码中,func 的类型 F 实现了 Copy 特征,调用时使用的将是它的拷贝,所以并没有发生所有权的转移,可以正常运行。
如果你想强制闭包取得捕获变量的所有权,可以在参数列表前添加 move 关键字,这种用法通常用于闭包的生命周期大于捕获变量的生命周期时,例如将闭包返回或移入其他线程:
use std::thread;
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();rust
如果闭包捕获的参数实现了 Copy
特征,那么则会出现一点不一样的情况:
fn main() {
let mut count = 0;
let mut inc = move || {
count += 1;
println!("`count`: {}", count);
};
inc();
let _reborrow = &count;
inc();
// The closure no longer needs to borrow `&mut count`. Therefore, it is
// possible to reborrow without an error
let _count_reborrowed = &mut count;
assert_eq!(count, 0);
}rust
输出:
`count`: 1 `count`: 2bash
代码成功退出,说明了在闭包中对 count
进行了复制,并且和外面的 count
一点关联都没有了,最后可以发现我们的 count
仍然是 0,而闭包中的 count
可以正常递增。
FnMut
FnMut
,它以可变借用的方式捕获了环境中的值,因此可以修改该值:
fn main() {
let mut s = String::new();
// 注意这里需要加上 mut
let mut update_string = |str| s.push_str(str);
update_string("hello");
println!("{:?}",s);
}rust
Fn
Fn
特征,它以不可变借用的方式捕获环境中的值:
fn main() {
let mut s = String::new();
let update_string = |str| s.push_str(str);
exec(update_string);
println!("{:?}",s);
}
fn exec<'a, F: Fn(&'a str)>(mut f: F) {
f("hello")
}rust
运行后:
error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnMut`
--> src/main.rs:4:26 // 期望闭包实现的是`Fn`特征,但是它只实现了`FnMut`特征
|
4 | let update_string = |str| s.push_str(str);
| ^^^^^^-^^^^^^^^^^^^^^
| | |
| | closure is `FnMut` because it mutates the variable `s` here
| this closure implements `FnMut`, not `Fn` //闭包实现的是FnMut,而不是Fn
5 |
6 | exec(update_string);
| ---- the requirement to implement `Fn` derives from hererust
因为在闭包函数中我们会修改 s
,因此 update_string
是 FnMut
类型的闭包函数。
如果不对 s
进行修改:
fn main() {
let s = "hello, ".to_string();
let update_string = |str| println!("{},{}",s,str);
exec(update_string);
println!("{:?}",s);
}
fn exec<'a, F: Fn(String) -> ()>(f: F) {
f("world".to_string())
}rust
在这里,因为无需改变 s,因此闭包中只对 s 进行了不可变借用,那么在 exec 中,将其标记为 Fn 特征就完全正确。
三种 Fn 的关系
实际上,一个闭包并不仅仅实现某一种 Fn 特征,规则如下:
- 所有的闭包都自动实现了 FnOnce 特征,因此任何一个闭包都至少可以被调用一次
- 没有移出所捕获变量的所有权的闭包自动实现了 FnMut 特征
- 不需要对捕获变量进行改变的闭包自动实现了 Fn 特征
fn main() {
let s = String::new();
let update_string = || println!("{}",s);
exec(update_string);
exec1(update_string);
exec2(update_string);
}
fn exec<F: FnOnce()>(f: F) {
f()
}
fn exec1<F: FnMut()>(mut f: F) {
f()
}
fn exec2<F: Fn()>(f: F) {
f()
}rust
虽然,闭包只是对 s
进行了不可变借用,实际上,它可以适用于任何一种 Fn
特征:三个 exec
函数说明了一切。
闭包作为函数返回值
fn factory() -> Fn(i32) -> i32 {
let num = 5;
|x| x + num
}
let f = factory();
let answer = f(1);
assert_eq!(6, answer);rust
上述代码,看着很正常,实际编译时会报错:
fn factory<T>() -> Fn(i32) -> i32 { | ^^^^^^^^^^^^^^ doesn't have a size known at compile-time // 该类型在编译器没有固定的大小 help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/main.rs:11:5: 11:21]`, which implements `Fn(i32) -> i32` | 8 | fn factory<T>() -> impl Fn(i32) -> i32 {bash
Rust 要求函数的参数和返回类型,必须有固定的内存大小,例如 i32
就是 4 个字节,引用类型是 8 个字节,总之,绝大部分类型都有固定的大小,但是不包括特征,因为特征类似接口,对于编译器来说,无法知道它后面藏的真实类型是什么,因为也无法得知具体的大小。
因此在这里编译器也提示我们使用 impl Fn(i32) -> i32
,但是这种方式只能返回一种类型:
fn factory(x:i32) -> impl Fn(i32) -> i32 {
let num = 5;
if x > 1{
move |x| x + num
} else {
move |x| x - num
}
}rust
运行后编译器报错:
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:15:9
|
12 | / if x > 1{
13 | | move |x| x + num
| | ---------------- expected because of this
14 | | } else {
15 | | move |x| x - num
| | ^^^^^^^^^^^^^^^^ expected closure, found a different closure
16 | | }
| |_____- `if` and `else` have incompatible types
|
= help: consider boxing your closure and/or using it as a trait objectbash
这时编译器又提示使用 Box
:
fn factory(x:i32) -> Box<dyn Fn(i32) -> i32> {
let num = 5;
if x > 1{
Box::new(move |x| x + num)
} else {
Box::new(move |x| x - num)
}
}rust
至此,闭包作为函数返回值就已完美解决。
迭代器
For 循环与迭代器
在 rust 中的一个 for
循环如下:
let arr = [1, 2, 3];
for v in arr {
println!("{}",v);
}rust
Rust 没有使用索引,它把 arr
数组当成一个迭代器,直接去遍历其中的元素,从哪里开始,从哪里结束,都无需操心。
虽然数组可以遍历,但是它并不是一个迭代器!它能迭代的原因是因为它实现了 IntoIterator
特征,只要实现了这个特征的类型都可以被 for
遍历。
类似的还有:
for i in 1..10 {
println!("{}", i);
}rust
直接对数值序列进行迭代,也是很常见的使用方式。
IntoIterator 特征拥有一个 into_iter 方法,因此我们还可以显式的把数组转换成迭代器:
let arr = [1, 2, 3];
for v in arr.into_iter() {
println!("{}", v);
}rust
手动遍历
Iterator
特征部分结构如下:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// 省略其余有默认实现的方法
}rust
其中的 next
方法,就是控制如何从集合中取值,最终返回值的类型是关联类型 Item
。
而 for
循环也正是不断调用迭代器上的 next
方法,来获取迭代器中的元素。
我们也可以手动调用:
fn main() {
let arr = [1, 2, 3];
let mut arr_iter = arr.into_iter();
assert_eq!(arr_iter.next(), Some(1));
assert_eq!(arr_iter.next(), Some(2));
assert_eq!(arr_iter.next(), Some(3));
assert_eq!(arr_iter.next(), None);
}rust
通过调用其上的 next
方法,我们获取了 arr
中的元素,有两点需要注意:
next
方法返回的是Option
类型,当有值时返回Some(i32)
,无值时返回None
- 遍历是按照迭代器中元素的排列顺序依次进行的,因此我们严格按照数组中元素的顺序取出了
Some(1)
,Some(2)
,Some(3)
- 手动迭代必须将迭代器声明为
mut
可变,因为调用next
会改变迭代器其中的状态数据(当前遍历的位置等),而for
循环去迭代则无需标注mut
,因为它会帮我们自动完成
模拟 for
循环:
let values = vec![1, 2, 3];
{
let result = match IntoIterator::into_iter(values) {
mut iter => loop {
match iter.next() {
Some(x) => { println!("{}", x); },
None => break,
}
},
};
result
}rust
IntoIterator::into_iter
是使用完全限定的方式去调用 into_iter
方法,这种调用方式跟 values.into_iter()
是等价的。
同时我们使用了 loop
循环配合 next
方法来遍历迭代器中的元素,当迭代器返回 None
时,跳出循环。
IntoIterator 特征
into_iter, iter, iter_mut
除了 into_iter
外,还有 iter
和 iter_mut
另外两个方法:
- into_iter 会夺走所有权
- iter 是不可变借用
- iter_mut 是可变借用
具体用例如下:
fn main() {
let values = vec![1, 2, 3];
for v in values.into_iter() {
println!("{}", v)
}
// 下面的代码将报错,因为 values 的所有权在上面 `for` 循环中已经被转移走
// println!("{:?}",values);
let values = vec![1, 2, 3];
let _values_iter = values.iter();
// 不会报错,因为 values_iter 只是借用了 values 中的元素
println!("{:?}", values);
let mut values = vec![1, 2, 3];
// 对 values 中的元素进行可变借用
let mut values_iter_mut = values.iter_mut();
// 取出第一个元素,并修改为0
if let Some(v) = values_iter_mut.next() {
*v = 0;
}
// 输出[0, 2, 3]
println!("{:?}", values);
}rust
消费者与适配器
消费者是迭代器上的方法,它会消费掉迭代器中的元素,然后返回其类型的值,这些消费者都有一个共同的特点:在它们的定义中,都依赖 next
方法来消费元素,因此这也是为什么迭代器要实现 Iterator
特征,而该特征必须要实现 next
方法的原因。
消费者适配器
只要迭代器上的某个方法 A 在其内部调用了 next
方法,那么 A 就被称为消费性适配器:因为 next
方法会消耗掉迭代器上的元素,所以方法 A 的调用也会消耗掉迭代器上的元素。
其中一个例子是 sum
方法,它会拿走迭代器的所有权,然后通过不断调用 next
方法对里面的元素进行求和:
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum();
assert_eq!(total, 6);
// v1_iter 是借用了 v1,因此 v1 可以照常使用
println!("{:?}",v1);
// 以下代码会报错,因为 `sum` 拿到了迭代器 `v1_iter` 的所有权
println!("{:?}",v1_iter);
}rust
运行后,只有最后一段代码报错:
error[E0382]: borrow of moved value: `v1_iter`
--> src/main.rs:14:21
|
4 | let v1_iter = v1.iter();
| ------- move occurs because `v1_iter` has type `std::slice::Iter<'_, i32>`, which does not implement the `Copy` trait
5 |
6 | let total: i32 = v1_iter.sum();
| ----- `v1_iter` moved due to this method call
...
14 | println!("{:?}",v1_iter);
| ^^^^^^^ value borrowed here after move
|
note: `std::iter::Iterator::sum` takes ownership of the receiver `self`, which moves `v1_iter`bash
迭代器适配器
迭代器适配器会返回一个新的迭代器,与消费者适配器不同,迭代器适配器是惰性的,意味着你需要一个消费者适配器来收尾,最终将迭代器转换成一个具体的值:
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);rust
这里的 map
方法是一个迭代者适配器,它是惰性的,不产生任何行为,因此我们还需要一个消费者适配器(collect
)进行收尾。
上面代码中,使用了 collect
方法,该方法就是一个消费者适配器,使用它可以将一个迭代器中的元素收集到指定类型中,这里我们为 v2
标注了 Vec<_>
类型,就是为了告诉 collect
:请把迭代器中的元素消费掉,然后把值收集成 Vec<_>
类型,至于为何使用 _
,因为编译器会帮我们自动推导。
再来看看如何使用 collect 收集成 HashMap 集合:
use std::collections::HashMap;
fn main() {
let names = ["sunface", "sunfei"];
let ages = [18, 18];
let folks: HashMap<_, _> = names.into_iter().zip(ages.into_iter()).collect();
println!("{:?}",folks);
}rust
zip
是一个迭代器适配器,它的作用就是将两个迭代器的内容压缩到一起,形成 Iterator<Item=(ValueFromA, ValueFromB)>
这样的新的迭代器,在此处就是形如 [(name1, age1), (name2, age2)]
的迭代器。
然后再通过 collect
将新迭代器中(K, V)
形式的值收集成 HashMap<K, V>
,同样的,这里必须显式声明类型,然后 HashMap
内部的 KV
类型可以交给编译器去推导,最终编译器会推导出 HashMap<&str, i32>
。
实现 Iterator 特征
首先创建一个计数器:
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}rust
之后为其实现 Iterator
特征:
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}rust
首先,将该特征的关联类型设置为 u32
,由于我们的计数器保存的 count
字段就是 u32
类型, 因此在 next
方法中,最后返回的是实际上是 Option<u32>
类型。
每次调用 next
方法,都会让计数器的值加一,然后返回最新的计数值,一旦计数大于 5
,就返回 None
。
最后,使用我们新建的 Counter
进行迭代:
let mut counter = Counter::new();
assert_eq!(counter.next(), Some(1));
assert_eq!(counter.next(), Some(2));
assert_eq!(counter.next(), Some(3));
assert_eq!(counter.next(), Some(4));
assert_eq!(counter.next(), Some(5));
assert_eq!(counter.next(), None);
let sum: u32 = Counter::new()
.zip(Counter::new().skip(1))
.map(|(a, b)| a * b)
.filter(|x| x % 3 == 0)
.sum();
assert_eq!(18, sum);rust