Rust系统编程之闭包与迭代器

1.闭包

1.1 函数式编程

函数式编程风格通常包含将函数作为参数值或其他函数的返回值、将函数赋值给变量以供之后执行等等。

1.2 闭包与函数

不同于函数,闭包允许捕获被定义时所在作用域中的值。

1
2
3
4
5
6
7
// 函数
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 ;

但是可以在需要一个闭包作为参数的函数中使用函数作为参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pub fn test_param<T, F>(val:T, f: F) -> T where F: FnOnce() {
f();
val
}

fn test_fun() {
println!("I'm function");
}

fn main() {
let my_closure = || println!("I'm closure");
test_param(123, my_closure);
test_param(456, test_fun);
}

1.3 捕获变量

1.3.1 不可变引用

定义一个捕获名为 list 的 vector 的不可变引用的闭包,因为只需不可变引用就能打印其值:

1
2
3
4
5
6
7
8
let list = vec![1, 2, 3];
println!("Before defining closure: {:?}", list);

let only_borrows = || println!("From closure: {:?}", list);

println!("Before calling closure: {:?}", list);
only_borrows();
println!("After calling closure: {:?}", list);

此时没有问题,因为可以存在多个不可变引用。

1.3.2 可变引用

定义一个捕获名为 list 的 vector 的可变引用的闭包,闭包体让它向 list vector 增加一个元素:

1
2
3
4
5
6
7
8
9
let mut list = vec![1, 2, 3];
println!("Before defining closure: {:?}", list);

let mut borrows_mutably = || list.push(7);

// 这行导致报错
println!("After calling closure: {:?}", list);
borrows_mutably();
println!("After calling closure: {:?}", list);

上面报错的原因是在定义闭包和调用闭包这段过程,push操作得到的可变引用会始终存在,此时再调用println!宏获得不可变引用就会出问题。

1.3.3 获取所有权

需要在闭包中指定move关键字:

1
2
3
4
5
6
7
8
9
10
use std::thread;

fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {:?}", list);

thread::spawn(move || println!("From thread: {:?}", list))
.join()
.unwrap();
}

这种写法在线程过程函数中会很常见,因为子线程和主线程的结束时间不一致,将所有权交给子线程会更安全。

1.4 移入移出被捕获的值

一旦闭包捕获了定义它的环境中一个值的引用或者所有权(也就影响了什么会被移 闭包,如有),闭包体中的代码定义了稍后在闭包计算时对引用或值如何操作(也就影响了什么会被移 闭包,如有)。闭包体可以做以下任何事:将一个捕获的值移出闭包,修改捕获的值,既不移动也不修改值,或者一开始就不从环境中捕获值。

闭包捕获和处理环境中的值的方式影响闭包实现的 trait。Trait 是函数和结构体指定它们能用的闭包的类型的方式。取决于闭包体如何处理值,闭包自动、渐进地实现一个、两个或三个 Fn trait。

  • FnOnce 适用于能被调用一次的闭包,所有闭包都至少实现了这个 trait,因为所有闭包都能被调用。一个会将捕获的值移出闭包体的闭包只实现 FnOnce trait,这是因为它只能被调用一次。
  • FnMut 适用于不会将捕获的值移出闭包体的闭包,但它可能会修改被捕获的值。这类闭包可以被调用多次。
  • Fn 适用于既不将被捕获的值移出闭包体也不修改被捕获的值的闭包,当然也包括不从环境中捕获值的闭包。这类闭包可以被调用多次而不改变它们的环境,这在会多次并发调用闭包的场景中十分重要。

示例:Option<T> 上的 unwrap_or_else 方法的定义

1
2
3
4
5
6
7
8
9
10
11
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(),
}
}
}

有些闭包具有特殊的属性,比如在闭包内调用了Vec.push(val),来将 val 的所有权转移,此时这个闭包就是 FnOnce 的,因为只能调用一次。

如果闭包内只会修改,但是不会夺取所有权,此时这个闭包就是FnMut 的,可以调用多次。

2.迭代器

2.1 新建迭代器

2.1.1 迭代器可变性

这里的可变性是指迭代器是否可以遍历。

1
2
3
4
5
6
7
// 不可变
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();

// 可变
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter();

2.1.2 元素可变性

这里的可变性是指这个迭代器对原来的元素的影响程度。

1
2
3
4
5
6
7
8
9
10
11
// 这个迭代器获取元素的不可变引用
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();

// 这个迭代器获取元素的可变引用
let mut v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter_mut();

// 这个迭代器夺走元素所有权
let mut v1 = vec![1, 2, 3];
let mut v1_iter = v1.into_iter();

2.2 消费迭代器

Rust迭代器默认都是惰性的,即需要手动来修改里面的游标进行消费。每个迭代器类型必须实现next trait。

1
2
3
4
5
6
7
8
9
10
11
#[test]
fn iterator_demonstration() {
let v1 = vec![1, 2, 3];

let mut v1_iter = v1.iter();

assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
}

next方法可以消费迭代器,就是逐个向下遍历,返回一个Option<T>,如果遍历到末尾则返回None。

2.3 迭代适配器

Iterator trait 中定义了另一类方法,被称为 迭代器适配器iterator adaptors),他们允许我们将当前迭代器变为不同类型的迭代器。如map方法:

1
2
3
let v1: Vec<i32> = vec![1, 2, 3];

v1.iter().map(|x| x + 1);

map方法在每个元素上调用闭包来处理元素并返回一个新的迭代器。上面代码会报警告,因为所有的迭代器都是惰性的,需要进行消费。

collect方法可以消费迭代器并将结果收集到一个数据结构中:

1
2
3
4
5
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]);

2.4 filter

1
2
3
fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}

可以收集某个容器中满足指定条件的元素序列。


Rust系统编程之闭包与迭代器
http://helloymf.github.io/2023/04/17/rust-xi-tong-bian-cheng-zhi-bi-bao-yu-die-dai-qi/
作者
JNZ
许可协议