rust学习笔记之基础:闭包和迭代器
闭包和迭代器
闭包
闭包是可以捕获环境的匿名函数。Rust 的闭包(closures)是可以保存进变量或作为参数传递给其他函数的匿名函数。可以在一个地方创建闭包,然后在不同的上下文中执行闭包运算。不同于函数,闭包允许捕获调用者作用域中的值。
- 声明时使用 || 替代 () 将输入参数括起来。
- 函数体定界符({})对于单个表达式是可选的,其他情况必须加上。
- 有能力捕获外部环境的变量。
- 输入和返回类型两者都可以自动推导,而输入变量名必须指明
let expensive_closure = |num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
闭包的定义以一对竖线(|)开始,在竖线中指定闭包的参数;这个闭包有一个参数 num;如果有多于一个参数,可以使用逗号分隔,比如 |param1, param2|。参数之后是存放闭包体的大括号,如果闭包体只有一行则大括号是可以省略的。
闭包类型推断和标注
闭包不要求像 fn 函数那样在参数和返回值上注明类型。函数中需要类型标注是因为他们是暴露给用户的显式接口的一部分。严格的定义这些接口对于保证所有人都认同函数输入和返回值的类型来说是很重要的。但是闭包并不用于这样暴露在外的接口:他们储存在变量中并被使用,不用命名他们或暴露给库的用户调用。闭包通常很短,且只关联于小范围的上下文而非任意情境。在这些有限制的上下文中,编译器能可靠的推断参数和返回值的类型,类似于它是如何能够推断大部分变量的类型一样。
类似于变量,可以选择增加类型标注;
let expensive_closure = |num: u32| -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
有了类型标注,闭包的语法就更类似函数了。如下是函数与拥有相同行为闭包语法的纵向对比:
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);
闭包会捕获其环境
闭包拥有函数所没有的功能:他们可以捕获其环境并访问其被定义的作用域内的变量。闭包周围的作用域被称为其环境。
fn main() {
let x = 4;
let equal_to_x = |z| z == x;
let y = 4;
assert!(equal_to_x(y));
}
这里,即便 x 并不是 equal_to_x 的一个参数,equal_to_x 闭包却被允许使用变量 x,因为它与 equal_to_x 定义于相同的作用域。
闭包可以通过三种方式捕获其环境,他们直接对应函数的三种获取参数的方式:获取所有权,可变借用和不可变借用。这三种捕获值的方式被编码为如下三个 Fn trait:
- FnOnce 获取变量所有权并在定义闭包时将其移动进闭包,会消费从周围作用域捕获的变量。
- FnMut 获取可变的借用值,所以可以改变其环境
- Fn 从其环境获取不可变的借用值
当创建一个闭包时,Rust 根据其如何使用环境中变量来推断我们希望如何引用环境。由于所有闭包都可以被调用至少一次,所以所有闭包都实现了 FnOnce 。那些并没有移动被捕获变量的所有权到闭包内的闭包也实现了 FnMut ,而不需要对被捕获的变量进行可变访问的闭包则也实现了 Fn 。
use std::mem;
fn main() {
let color = String::from("green");
// 借用`color` 并将该借用和闭包本身存储到 `print` 变量中。`color` 会一直保持被借用状态直到 `print` 离开作用域。
let print = || println!("`color`: {}", color);
print();
// `color` 可再次被不可变借用,因为闭包只持有一个指向 `color` 的不可变引用。
let _reborrow = &color;
print();
// 在最后使用 `print` 之后,移动或重新借用都是允许的。
let _color_moved = color;
let mut count = 0;
// 该闭包可变借用 `count`。`inc` 前面需要加上 `mut`,因为闭包里存储着一个 `&mut` 变量。
// 调用闭包时,该变量的变化就意味着闭包内部发生了变化。因此闭包需要是可变的。
let mut inc = || {
count += 1;
println!("`count`: {}", count);
};
// 使用可变借用调用闭包
inc();
// 因为之后调用闭包,所以 inc 中可变借用 `count` 仍然是有效的。试图重新借用将导致错误
// let _reborrow = &count;
inc();
// 闭包不再借用 `&mut count`,因此可以正确地重新借用
let _count_reborrowed = &mut count;
// 不可复制类型(non-copy type)。
let movable = Box::new(3);
// `mem::drop` 要求 `T` 类型本身,所以闭包将会捕获变量的值。
// 可复制类型将会复制给闭包,从而原始值不受影响。不可复制类型必须移动到闭包中。
let consume = || {
println!("`movable`: {:?}", movable);
mem::drop(movable);
};
// `consume` 消耗了该变量,所以该闭包只能调用一次。
consume();
//consume(); // 重复调用将报错
}
如果你希望强制闭包获取其使用的环境值的所有权,可以在参数列表前使用 move 关键字。
fn main() {
let x = vec![1, 2, 3];
let equal_to_x = move |z| z == x;
println!("can't use x here: {:?}", x);
}
使用带有泛型和 Fn trait 的闭包
Fn 系列 trait 由标准库提供。所有的闭包都实现了 trait Fn、FnMut 或 FnOnce 中的一个。
struct Cacher<T>
where T: Fn(u32) -> u32
{
calculation: T,
value: Option<u32>,
}
impl<T> Cacher<T>
where T: Fn(u32) -> u32
{
fn new(calculation: T) -> Cacher<T> {
Cacher {
calculation,
value: None,
}
}
fn value(&mut self, arg: u32) -> u32 {
match self.value {
Some(v) => v,
None => {
let v = (self.calculation)(arg);
self.value = Some(v);
v
},
}
}
}
结构体 Cacher 有一个泛型 T 的字段 calculation。T 的 trait bound 指定了 T 是一个使用 Fn 的闭包。任何我们希望储存到 Cacher 实例的 calculation 字段的闭包必须有一个 u32 参数并必须返回一个 u32。
闭包作为函数参数
虽然 Rust 无需类型说明就能在大多数时候完成变量捕获,但当以闭包作为输入参数时,必须指出闭包的完整类型。它是通过使用Fn trait 中的一种来指定的。其受限制程度按以下顺序递减:Fn(捕获方式为通过引用(&T)的闭包)、FnMut(捕获方式为通过可变引用(&mut T)的闭包)、FnOnce(捕获方式为通过值(T)的闭包)。注:顺序之所以是这样,是因为 &T 只是获取了不可变的引用,&mut T 则可以改变变量,T 则是拿到了变量的所有权而非借用。
fn apply<F>(f: F) where
F: FnOnce() { // 闭包没有输入值和返回值。
f();
}
fn apply_to_3<F>(f: F) -> i32 where
F: Fn(i32) -> i32 { // 闭包处理一个 `i32` 整型并返回一个 `i32` 整型。
f(3)
}
fn main() {
use std::mem;
let greeting = "hello";
// `to_owned` 从借用的数据创建有所有权的数据。
let mut farewell = "goodbye".to_owned();
// 捕获 2 个变量:通过可变引用捕获 `greeting`,通过值捕获 `farewell`。
let diary = || {
// `greeting` 通过引用捕获,故需要闭包是 `Fn`。
println!("I said {}.", greeting);
// 下文改变了 `farewell` ,因而要求闭包通过可变引用来捕获它。现在需要 `FnMut`。
farewell.push_str("!!!");
println!("Then I screamed {}.", farewell);
// 手动调用 drop 又要求闭包通过值获取 `farewell`。现在需要 `FnOnce`。
mem::drop(farewell);
};
// 以闭包作为参数,调用函数 `apply`。
apply(diary);
// 闭包 `double` 满足 `apply_to_3` 的 trait 约束。
let double = |x| 2 * x;
println!("3 doubled: {}", apply_to_3(double));
}
闭包作为返回值
目前 Rust 只支持返回具体(非泛型)的类型。只有使用 impl Trait 才能返回一个闭包。返回闭包的有效特征是:Fn、FnMut、FnOnce。除此之外,还必须使用 move 关键字,它表明所有的捕获都是通过值进行的。这是必须的,因为在函数退出时,任何通过引用的捕获都被丢弃,在闭包中留下无效的引用。
fn create_fn() -> impl Fn() {
let text = "Fn".to_owned();
move || println!("This is a: {}", text)
}
fn create_fnmut() -> impl FnMut() {
let text = "FnMut".to_owned();
move || println!("This is a: {}", text)
}
fn create_fnonce() -> impl FnOnce() {
let text = "FnOnce".to_owned();
move || println!("This is a: {}", text)
}
fn main() {
let fn_plain = create_fn();
let mut fn_mut = create_fnmut();
let fn_once = create_fnonce();
fn_plain();
fn_mut();
fn_once();
}
标准库中的闭包例子
Iterator::any 是一个函数,在迭代器上调用 any 时传入闭包,当其中任一元素满足条件时它将返回 true,否则返回 false。签名如下:
pub trait Iterator {
// 被迭代的类型。
type Item;
// `any` 接受 `&mut self` 参数,表明函数的调用者可以被借用和修改,但不会被消耗。
fn any<F>(&mut self, f: F) -> bool where
// `FnMut` 表示被捕获的变量最多只能被修改,而不能被消耗。
// `Self::Item` 指明了被捕获变量的类型是迭代器的元素本身的类型
F: FnMut(Self::Item) -> bool {}
}
使用
fn main() {
let vec1 = vec![1, 2, 3];
let vec2 = vec![4, 5, 6];
// 对 vec 的 `iter()` 举出 `&i32`。因此闭包接收到的参数是 `&i32` 类型的。
println!("2 in vec1: {}", vec1.iter().any(|&x| x == 2)); // 2 in vec1: true
// 对 vec 的 `into_iter()` 举出 `i32` 类型,无需解构。向量的.into_iter()会消耗整个向量,之后向量将不再可用
println!("2 in vec2: {}", vec2.into_iter().any(|x| x == 2)); // 2 in vec2: false
println!("vec1: {:?}",vec1); // vec1: [1, 2, 3]
//println!("vec2: {:?}",vec2); // 注释打开会报错
let array1 = [1, 2, 3];
let array2 = [4, 5, 6];
// 对数组的 `iter()` 举出 `&i32`。
println!("2 in array1: {}", array1.iter().any(|&x| x == 2)); // 2 in array1: true
// 对数组的 `into_iter()` 通常举出 `i32`。数组的.into_iter()方法:如果数组元素实现了Copy,则迭代不会消耗数组(因为数组会复制);否则,将按元素逐个消耗数组
println!("2 in array2: {}", array2.into_iter().().any(|x| x == 2)); // 2 in array2: false
println!("array1: {:?}",array1); // array1: [1, 2, 3]
println!("array2: {:?}",array2); // array2: [4, 5, 6]
}
Iterator::find 是一个函数,在迭代器上调用 find 时传入闭包,,将用 Option 类型返回第一个满足条件的元素。签名如下:
pub trait Iterator {
// 被迭代的类型。
type Item;
// `find` 接受 `&mut self` 参数,表明函数的调用者可以被借用和修改,但不会被消耗。
fn find<P>(&mut self, predicate: P) -> Option<Self::Item> where
// `FnMut` 表示被捕获的变量最多只能被修改,而不能被消耗。
// `&Self::Item` 指明了被捕获变量的类型是对迭代器元素的引用类型
P: FnMut(&Self::Item) -> bool {}
}
使用
fn main() {
let vec1 = vec![1, 2, 3];
let vec2 = vec![4, 5, 6];
// 注:注意 `find` 方法会把迭代器元素的引用传给闭包。迭代器元素自身是 `&i32` 类型,所以传给闭包的是 `&&i32` 类型。
// 对迭代器举出的元素的引用是 `&&i32` 类型。解构成 `i32` 类型。
println!("Find 2 in vec1: {:?}", vec1.iter().find(|&&x| x == 2)); // Find 2 in vec1: Some(2)
// 对迭代器举出的元素的引用是 `&i32` 类型。解构成 `i32` 类型。
println!("Find 2 in vec2: {:?}", vec2.into_iter().find(| &x| x == 2)); // Find 2 in vec2: None
let array1 = [1, 2, 3];
let array2 = [4, 5, 6];
// 对数组的 `iter()` 举出 `&i32`。
println!("Find 2 in array1: {:?}", array1.iter().find(|&&x| x == 2)); // Find 2 in array1: Some(2)
// 对数组的 `into_iter()` 通常举出 `i32``。
println!("Find 2 in array2: {:?}", array2.into_iter().find(|&x| x == 2)); // Find 2 in array2: None
}
迭代器
使用迭代器处理元素序列
迭代器模式允许你对一个序列的项进行某些处理。迭代器(iterator)负责遍历序列中的每一项和决定序列何时结束的逻辑。在 Rust 中,迭代器是惰性的,这意味着在调用方法使用迭代器之前它都不会有效果。
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter(); // 创建一个迭代器
for val in v1_iter { // for 循环中使用迭代器
println!("Got: {}", val);
}
Iterator trait 和 next 方法
迭代器都实现了一个叫做 Iterator 的定义于标准库的 trait。这个 trait 的定义看起来像这样:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// 此处省略了方法的默认实现
}
可以直接调用迭代器的 next 方法;
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);
注意 v1_iter 需要是可变的:在迭代器上调用 next 方法改变了迭代器中用来记录序列位置的状态。换句话说,代码消费了或使用了迭代器。每一个 next 调用都会从迭代器中消费一个项。使用 for 循环时无需使 v1_iter 可变因为 for 循环会获取 v1_iter 的所有权并在后台使 v1_iter 可变。
另外需要注意到从 next 调用中得到的值是 vector 的不可变引用。iter 方法生成一个不可变引用的迭代器。如果我们需要一个获取 v1 所有权并返回拥有所有权的迭代器,则可以调用 into_iter 而不是 iter。类似的,如果我们希望迭代可变引用,则可以调用 iter_mut 而不是 iter。
消费适配器
Iterator trait 有一系列由标准库提供默认实现的方法;一些方法在其定义中调用了 next 方法,这些调用 next 方法的方法被称为消费适配器,因为调用他们会消耗迭代器。
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum();
assert_eq!(total, 6);
//调用 sum 之后不再允许使用 v1_iter 因为调用 sum 时它会获取迭代器的所有权。
迭代器适配器
Iterator trait 中定义了另一类方法,被称为迭代器适配器,他们允许我们将当前迭代器变为不同类型的迭代器。可以链式调用多个迭代器适配器。不过因为所有的迭代器都是惰性的,必须调用一个消费适配器方法以便获取迭代器适配器调用的结果。
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); // 调用迭代器适配器 map 来创建一个新迭代器,并将结果收集到一个数据结构中
assert_eq!(v2, vec![2, 3, 4]);
迭代器的 filter 方法接收一个使用迭代器的每一个项并返回布尔值的闭包。如果闭包返回 true,其值将会包含在 filter 提供的新迭代器中。如果闭包返回 false,其值不会包含在结果迭代器中。
#[derive(PartialEq, Debug)]
struct Shoe {
size: u32,
style: String,
}
fn shoes_in_my_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes.into_iter()
.filter(|s| s.size == shoe_size)
.collect()
}
实现 Iterator trait 来创建自定义迭代器
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
self.count += 1;
if self.count < 4 {
Some(self.count)
} else {
None
}
}
}
fn main(){
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(), None);
let sum: u32 = Counter::new()
.zip(Counter::new().skip(1))
.map(|(a, b)| a * b)
.filter(|x| x % 3 == 0)
.sum();
println!("{}", sum); // 6
}
高阶函数和迭代器
Rust 提供了高阶函数 HOF,指那些输入一个或多个函数,并且或者产生一个更有用的函数的函数。HOF 和惰性迭代器给 Rust 带来了函数式编程的风格。
// 函数式的写法
let upper = 1000;
let sum_of_squared_odd_numbers: u32 =
(0..).map(|n| n * n) // 所有自然数取平方
.take_while(|&n| n < upper) // 取小于上限的
.filter(|&n| is_odd(n)) // 取奇数
.fold(0, |sum, i| sum + i); // 最后加起来
println!("functional style: {}", sum_of_squared_odd_numbers);
浙公网安备 33010602011771号