迭代器与闭包
- 闭包(Closures),一个可以储存在变量里的类似函数的结构
- 迭代器(Iterators),一种处理元素序列的方式
闭包:可以捕获环境的匿名函数
Rust 的 闭包(closures)是可以保存在变量中或作为参数传递给其他函数的匿名函数。你可以在一个地方创建闭包,然后在不同的上下文中执行闭包运算。不同于函数,闭包允许捕获其被定义时所在作用域中的值
闭包会捕获其环境
#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
Red,
Blue,
}
struct Inventory {
shirts: Vec<ShirtColor>,
}
impl Inventory {
fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
/*
接受一个无参闭包作为参数,该闭包返回一个 T 类型的值(与 Option<T> 的 Some 变体中存储的值类型相同,这里是 ShirtColor)。如果 Option<T> 是 Some 成员,则 unwrap_or_else 返回 Some 中的值。如果 Option<T> 是 None 成员,则 unwrap_or_else 调用闭包并返回闭包的返回值。
*/
/*
我们传递了一个闭包,该闭包会在当前的 Inventory 实例上调用 self.most_stocked() 方法。标准库不需要了解我们定义的 Inventory 或 ShirtColor 类型,也不需要了解我们在这个场景中要使用的逻辑。闭包捕获了对 self(即 Inventory 实例)的不可变引用,并将其与我们指定的代码一起传递给 unwrap_or_else 方法。相比之下,函数无法以这种方式捕获其环境。
*/
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!(
"The user with preference {:?} gets {:?}",
user_pref1, giveaway1
);
let user_pref2 = None;
let giveaway2 = store.giveaway(user_pref2);
println!(
"The user with preference {:?} gets {:?}",
user_pref2, giveaway2
);
}
闭包类型推断和注解
函数与闭包还有更多区别。闭包通常不要求像
fn
函数那样对参数和返回值进行类型注解。函数需要类型注解是因为这些类型是暴露给用户的显式接口的一部分。严格定义这些接口对于确保所有人对函数使用和返回值的类型达成一致理解非常重要。与此相比,闭包并不用于这样暴露在外的接口:它们储存在变量中并被使用,不用命名它们或暴露给库的用户调用。闭包通常较短,并且只与特定的上下文相关,而不是适用于任意情境。在这些有限的上下文中,编译器可以推断参数和返回值的类型,类似于它推断大多数变量类型的方式(尽管在某些罕见的情况下,编译器也需要闭包的类型注解)。
类似于变量,如果我们希望增加代码的明确性和清晰度,可以添加类型注解,但代价是是会使代码变得比严格必要的更冗长。
let expensive_closure = |num: u32| -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
/*
第一行展示了一个函数定义,第二行展示了一个完整标注的闭包定义。第三行闭包定义中省略了类型注解,而第四行去掉了可选的大括号,因为闭包体只有一个表达式,所以大括号是可选的。这些都是有效的闭包定义,并在调用时产生相同的行为。调用闭包是 add_one_v3 和 add_one_v4 能够编译的必要条件,因为类型将从其用法中推断出来。这类似于 let v = Vec::new();,Rust 需要类型注解或是某种类型的值被插入到 Vec 中,才能推断其类型。
*/
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 example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);//报错,第一次使用 String 值调用 example_closure 时,编译器推断出 x 的类型以及闭包的返回类型为 String。接着这些类型被锁定进闭包 example_closure 中,如果尝试对同一闭包使用不同类型则就会得到类型错误。
捕获引用或者移动所有权
闭包可以通过三种方式捕获其环境中的值,它们直接对应到函数获取参数的三种方式:不可变借用、可变借用和获取所有权。闭包将根据函数体中对捕获值的操作来决定使用哪种方式。
//不可变引用
fn main() {
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:?}");
}
//可变引用
fn main() {
let mut list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
let mut borrows_mutably = || list.push(7);
borrows_mutably();
println!("After calling closure: {list:?}");
}
//使用 move 来强制闭包为线程获取 list 的所有权
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();
}
将被捕获的值移出闭包和 Fn
trait
一旦闭包捕获了定义它的环境中的某个值的引用或所有权(也就影响了什么会被移 进 闭包,如有),闭包体中的代码则决定了在稍后执行闭包时,这些引用或值将如何处理(也就影响了什么会被移 出 闭包,如有)。闭包体可以执行以下任一操作:将一个捕获的值移出闭包,修改捕获的值,既不移动也不修改值,或者一开始就不从环境中捕获任何值。
闭包捕获和处理环境中的值的方式会影响闭包实现哪些 trait,而 trait 是函数和结构体指定它们可以使用哪些类型闭包的方式。根据闭包体如何处理这些值,闭包会自动、渐进地实现一个、两个或全部三个
Fn
trait。
FnOnce
适用于只能被调用一次的闭包。所有闭包至少都实现了这个 trait,因为所有闭包都能被调用。一个会将捕获的值从闭包体中移出的闭包只会实现FnOnce
trait,而不会实现其他Fn
相关的 trait,因为它只能被调用一次。FnMut
适用于不会将捕获的值移出闭包体,但可能会修改捕获值的闭包。这类闭包可以被调用多次。Fn
适用于既不将捕获的值移出闭包体,也不修改捕获值的闭包,同时也包括不从环境中捕获任何值的闭包。这类闭包可以被多次调用而不会改变其环境,这在会多次并发调用闭包的场景中十分重要。
impl<T> Option<T> {
pub fn unwrap_or_else<F>(self, f: F) -> T
where
F: FnOnce() -> T//unwrap_or_else 函数有额外的泛型参数 F。F 是参数 f 的类型,f 是调用 unwrap_or_else 时提供的闭包。泛型 F 的 trait bound 是 FnOnce() -> T,这意味着 F 必须能够被调用一次,没有参数并返回一个 T。在 trait bound 中使用 FnOnce 表示 unwrap_or_else 最多只会调用 f 一次。在 unwrap_or_else 的函数体中可以看到,如果 Option 是 Some,f 不会被调用。如果 Option 是 None,f 将会被调用一次。由于所有的闭包都实现了 FnOnce,unwrap_or_else 接受所有三种类型的闭包,十分灵活。
{
match self {
Some(x) => x,
None => f(),
}
}
}
使用迭代器处理元素序列
迭代器模式允许你依次对一个序列中的项执行某些操作。迭代器(iterator)负责遍历序列中的每一项并确定序列何时结束的逻辑。使用迭代器时,你无需自己重新实现这些逻辑。
在 Rust 中,迭代器是 惰性的(lazy),这意味着在调用消费迭代器的方法之前不会执行任何操作。
fn main() {
let v1 = vec![1, 2, 3];
//代器被储存在 v1_iter 变量中。一旦创建迭代器之后,可以选择用多种方式利用它。
let v1_iter = v1.iter();
//迭代器的创建和 for 循环中的使用分开。当 for 循环使用 v1_iter 中的迭代器时,迭代器中的每一个元素都会用于循环的一次迭代,并打印出每个值。
for val in v1_iter {
println!("Got: {val}");
}
}
Iterator
trait 和 next
方法
//迭代器都实现了一个叫做 Iterator 的定义于标准库的 trait。
//type Item 和 Self::Item,它们定义了 trait 的 关联类型(associated type)。这段代码表明实现 Iterator trait 要求同时定义一个 Item 类型,这个 Item 类型被用作 next 方法的返回值类型。换句话说,Item 类型将是迭代器返回元素的类型。
pub trait Iterator {
type Item;
//next 是 Iterator 实现者被要求定义的唯一方法:next 方法,该方法每次返回迭代器中的一个项,封装在 Some 中,并且当迭代完成时,返回 None。
fn next(&mut self) -> Option<Self::Item>;
// 此处省略了方法的默认实现
}
//在迭代器上(直接)调用 next 方法
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter();
//还需要注意的是,从 next 调用中获取的值是对 vector 中值的不可变引用。iter 方法生成一个不可变引用的迭代器。如果我们需要一个获取 v1 所有权并返回拥有所有权的迭代器,则可以调用 into_iter 而不是 iter。类似地,如果我们希望迭代可变引用,可以调用 iter_mut 而不是 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);
消费迭代器的方法
Iterator
trait 有一系列不同的由标准库提供默认实现的方法;你可以在Iterator
trait 的标准库 API 文档中找到所有这些方法。一些方法在其定义中调用了next
方法,这也就是为什么在实现Iterator
trait 时要求实现next
方法的原因。这些调用
next
方法的方法被称为 消费适配器(consuming adaptors),因为调用它们会消耗迭代器。
//一个消费适配器的例子是 sum 方法。这个方法获取迭代器的所有权并反复调用 next 来遍历迭代器,因而会消费迭代器。在遍历过程中,它将每个项累加到一个总和中,并在迭代完成时返回这个总和。
fn iterator_sum() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
//调用 sum 之后不再允许使用 v1_iter 因为调用 sum 时它会获取迭代器的所有权。
let total: i32 = v1_iter.sum();
assert_eq!(total, 6);
}
产生其他迭代器的方法
Iterator
trait 中定义了另一类方法,被称为 迭代器适配器(iterator adaptors),它们不会消耗当前的迭代器,而是通过改变原始迭代器的某些方面来生成不同的迭代器。
//该方法使用一个闭包对每个元素进行操作。map 方法返回一个新的迭代器,该迭代器生成经过修改的元素。这里的闭包创建了一个新的迭代器,其中 vector 中的每个元素都被加 1。
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]);
使用捕获其环境的闭包
很多迭代器适配器接受闭包作为参数,而我们通常会指定捕获其环境的闭包作为迭代器适配器的参数。
#[derive(PartialEq, Debug)]
struct Shoe {
size: u32,
style: String,
}
//shoes_in_size 函数获取一个鞋子 vector 的所有权和一个鞋码作为参数。它返回一个只包含指定鞋码的鞋子的 vector。
fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
//shoes_in_size 函数体中调用了 into_iter 来创建一个获取 vector 所有权的迭代器。接着调用 filter 将这个迭代器适配成一个只含有那些闭包返回 true 的元素的新迭代器。
//使用 filter 方法和一个捕获 shoe_size 的闭包
shoes.into_iter().filter(|s| s.size == shoe_size).collect()
//闭包从环境中捕获了 shoe_size 变量并使用其值与每一只鞋的大小作比较,只保留指定鞋码的鞋子。最终,调用 collect 将迭代器适配器返回的值收集进一个 vector 并返回。
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filters_by_size() {
let shoes = vec![
Shoe {
size: 10,
style: String::from("sneaker"),
},
Shoe {
size: 13,
style: String::from("sandal"),
},
Shoe {
size: 10,
style: String::from("boot"),
},
];
let in_my_size = shoes_in_size(shoes, 10);
assert_eq!(
in_my_size,
vec![
Shoe {
size: 10,
style: String::from("sneaker")
},
Shoe {
size: 10,
style: String::from("boot")
},
]
);
}
}