闭包
闭包(closures)就是可以捕获其所在环境的匿名函数。
闭包的特点
- 是匿名函数
- 保存为变量、作为参数
- 可在一个地方创建闭包,然后在另一个上下文中调用闭包完成运算
- 可从其定义的作用域捕获值
闭包的类型推断
- 不要求标注参数和返回值的类型(也可以标注,不过一般省略)
- 通常很短小,只在狭小的上下文中工作,编译器通常能推断出类型
- 闭包的定义最终只会为参数/返回值推断出唯一具体的类型
// 在未使用的时候可能会报错,因为没有指定类型,但是使用之后就不会报错了
let expensive_closure = |num| -> {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
// 返回值比较简单也可以写成下面这种形式
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
// 此处会报错,因为定义 s 的时候已经确定了闭包的参数和返回类型,不能修了
let n = example_closure(5);
使用泛型参数和 Fn Trait 存储闭包
使用 struct,它持有闭包及其调用结果。struct 的定义需要知道所有字段的类型,而且每一个闭包实例都有自己唯一的匿名函数,即使两个闭包签名完全一样。所以需要使用 泛型和 Trait Bound。
Fn Traits 由标准库提供,所有的闭包都至少实现了以下 trait 之一:
- Fn
- FnMut
- FnOnce
struct Cacher<T>
where T: Fn(u32) -> u32, // Fn Trait
{
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
}
}
}
}
使用闭包来捕获环境
闭包能访问定义它的作用域内的变量,而普通函数则不能。
fn main() {
let x = 4;
let equal_to_x = |z| z == x;
let y = 4;
assert!(equal_to_x(y));
}
如果使用下面的函数写法则会报错
fn main() {
let x = 4;
fn equal_to_x(z: i32) -> bool {
z == x // can't capture dynamic environment in a fn item
}
let y = 4;
assert!(equal_to_x(y));
}
不过闭包捕获他们所在环境的变量会产生内存开销。
闭包从所在环境捕获值的方式(与函数获得参数的三种方式一样):
- 取得所有权:
FnOnce - 可变借用:
FnMut - 不可变借用:
Fn
创建闭包时,通过闭包对环境值的使用,Rust 可以推断出具体使用哪个 trait:
- 所有的闭包都实现了
FnOnce - 没有移动捕获变量的实现了
FnMut - 无需可变访问捕获变量的闭包实现了
Fn
move 关键字
在参数列表前使用 move 关键字,可以强制闭包取得它所使用的环境值的所有权。
当将闭包传递给新线程以移动数据使其归新线程所有时,此技术最有用。
fn main() {
let x = vec![1, 2, 3];
let equal_to_x = move |z| z == x;
println!("can't use x here: {:?}", x); // borrow of moved value: `x`
let y = vec![1, 2, 3];
assert!(equal_to_x(y));
}
最佳实践
当指定 Fn trait bound 之一时,首先用 Fn,基于闭包体里的情况,如果需要 FnOnce 或 FnMut,编译器会再告诉你。
迭代器
迭代器模式:对一系列项执行某些任务。迭代器负责遍历每个项,确定序列(遍历)何时完成。
Rust 的迭代器是惰性的,除非调用消费迭代器的方法,否则迭代器本身没有任何效果。
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter(); // 产生一个迭代器,用于遍历 v1,到这一步它未使用所以没有任何效果
for val in v1_iter {
println!("Got: {}", val);
}
}
所有的迭代器都实现了 Iterator trait,Iterator trait 定义域标准库,定义大致如下:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// methods with default implementations elided
}
type Item 和 Self::Item 定义了与该 trait 关联的类型。实现 Iterator trait 需要你定义一个 Item 类型,它用于 next 方法的返回类型(迭代器的返回类型)。
Iterator trait 仅要求实现 next 方法。next 方法返回迭代器中的一项,返回结果包裹在 Some 里。迭代结束,返回 None。
fn main() {
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter(); // for in 不需要要加 mut 因为在其内部已经取得了所有权
println!("Got: {}", v1_iter.next() == Some(&1));
println!("Got: {}", v1_iter.next() == Some(&2));
println!("Got: {}", v1_iter.next() == Some(&3));
}
迭代方法
- iter:在不可变引用上创建迭代器
- into_iter:创建的迭代器会获得所有权
- iter_mut:迭代可变的引用
fn main() {
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.into_iter();
println!("Got: {}", v1_iter.next() == Some(1));
println!("Got: {}", v1_iter.next() == Some(2));
println!("Got: {}", v1_iter.next() == Some(3));
}
fn main() {
let mut v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter_mut();
println!("Got: {}", v1_iter.next() == Some(&mut 1));
println!("Got: {}", v1_iter.next() == Some(&mut 2));
println!("Got: {}", v1_iter.next() == Some(&mut 3));
}
消耗迭代器的方法
在标准库中,Iterator trait 有一些带默认实现的方法,其中有一些方法会调用 next 方法,所以实现 Iterator trait 时必须实现 netx 方法的原因之一。
调用 next 的方法叫做消耗型适配器。因为调用 next 方法会把元素一个一个都吃掉,最终耗尽迭代器。
比如:sum 方法
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum();
println!("{}", total)
}
产生其它迭代器的方法
定义在 Iterator trait 上的另外一些方法叫做迭代器适配器。它会把当前迭代器转换为不同种类的迭代器。可以通过链式调用使用多个迭代器适配器来执行复杂的操作,这种调用可读性较高。
例如:map,接收一个闭包作为参数,闭包作用于每个元素,产生一个新的迭代器。
fn main() {
let v1 = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); // collect 也是消耗型适配器,把结果收集到一个集合类型中。
println!("{:?}", v2); // [2, 3, 4]
}
使用闭包捕获环境
filter 方法,接收一个闭包,这个闭包在遍历迭代器的某个元素时,返回 bool 类型,如果闭包返回 true, 当前元素将会包含在 filter 产生的迭代器中,如果闭包返回 false,当前元素将不会包含在 filter 产生的迭代器中。
struct Shoe {
size: u32,
style: String,
}
fn shoe_in_my_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes.into_iter().filter(|x| x.size == shoe_size).collect()
}
fn main() {
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 result = shoe_in_my_size(shoes, 10);
println!("{}", result.len())
}
创建自定义迭代器
使用 Iterator trait 创建自定义迭代器,关键的一步就是要提供一个 next 方法。
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> {
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
fn main() {
let mut counter = Counter::new();
println!("{}", counter.next() == Some(1));
println!("{}", counter.next() == Some(2));
println!("{}", counter.next() == Some(3));
println!("{}", counter.next() == Some(4));
println!("{}", counter.next() == Some(5));
println!("{}", counter.next() == None);
}
性能对比:循环 VS 迭代器
在 Rust 中,循环和迭代器性能基本是一样的,因为在编译之后,生成了和手写的底层代码基本一样的产物。这一套东西在 Rust 里面就叫零开销抽象(Zero-Cost Abstraction),即使用抽象时不会引入额外的运动时开销。