Rust - Basic - 12 - Iterators and Closures
Iterators and Closures
迭代器与闭包,函数式编程的思想之一
Comprehension
closures
表现:函数可以被赋值为变量,此时函数未被调用
特点:不对调用者暴露、简短且意义明确
编译器会进行推断以决定使用哪种 Closures:
FnOnce
当使用的外部环境被 taking ownership (move) 则使用FnMut
当使用的外部环境被 borrow mutably 则使用Fn
当使用的外部环境被 borrow immutably 则使用环境即 envrionment,一般指闭包内使用的外部变量
// 常见声明 let expensive_closure = |num| { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num }; // 只有 a single expression 的时候 let add_one_v3 = |x| { x + 1 }; // 大括号可以去掉 let add_one_v4 = |x| x + 1 ; // closures 自带 type inferred let example_closure = |x| x; let s = example_closure(String::from("hello")); let n = example_closure(5); // 编译器处理到这里的时候会报错 // 使用 struct + closures + trait 实现 lazy load 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 } } } } // 通过 move 关键字强制指定 // 一般不会这么做(交给编译器推导) // 以下代码无法通过编译(因为 x 被 move 了) fn main() { let x = vec![1, 2, 3]; let equal_to_x = move |z| z == x; println!("can't use x here: {:?}", x); let y = vec![1, 2, 3]; assert!(equal_to_x(y)); }
iterators
迭代 和 循环 性能没有差别。
// 基础写法 #[test] fn iterator_demonstration() { let v1 = vec![1, 2, 3]; // 由于后续需要 next,会修改 v1 迭代器状态,故需要 mut 关键字 // 如果需要 take ownership,则使用 into_iter // 如果需要 mutable referance,则使用 iter_mut 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); } let v1: Vec<i32> = vec![1, 2, 3]; // 这里没有 consuming adaptors(调用 next 的 methods)会触发 compile warning v1.iter().map(|x| x + 1); let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); // 使用 collect 消费迭代器 assert_eq!(v2, vec![2, 3, 4]); /// into_inter 使用示例 - filter #[derive(PartialEq, Debug)] struct Shoe { size: u32, style: String, } fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> { shoes.into_iter().filter(|s| s.size == shoe_size).collect() } // 实现一个能 iter 的 struct 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 } } } #[test] fn calling_next_directly() { 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); } // 使用其它 Iterator Trait Methods #[test] fn using_other_iterator_trait_methods() { let sum: u32 = Counter::new() .zip(Counter::new().skip(1)) // 任一 iter 返回 None,zip 停止 pair items,所以这里只有 4 pairs .map(|(a, b)| a * b) .filter(|x| x % 3 == 0) .sum(); assert_eq!(18, sum); }
Origin
https://doc.rust-lang.org/book/ch13-00-functional-features.html
…
Closure Type Inference and Annotation
Closures don’t require you to annotate the types of the parameters or the return value like fn
functions do. Type annotations are required on functions because they’re part of an explicit interface exposed to your users. Defining this interface rigidly is important for ensuring that everyone agrees on what types of values a function uses and returns. But closures aren’t used in an exposed interface like this: they’re stored in variables and used without naming them and exposing them to users of our library.
Closures are usually short and relevant only within a narrow context rather than in any arbitrary scenario. Within these limited contexts, the compiler is reliably able to infer the types of the parameters and the return type, similar to how it’s able to infer the types of most variables.
Making programmers annotate the types in these small, anonymous functions would be annoying and largely redundant with the information the compiler already has available.
…
Capturing the Environment with Closures
In the workout generator example, we only used closures as inline anonymous functions. However, closures have an additional capability that functions don’t have: they can capture their environment and access variables from the scope in which they’re defined.
Listing 13-12 has an example of a closure stored in the equal_to_x
variable that uses the x
variable from the closure’s surrounding environment.
Filename: src/main.rs
fn main() {
let x = 4;
let equal_to_x = |z| z == x;
let y = 4;
assert!(equal_to_x(y));
}
Listing 13-12: Example of a closure that refers to a variable in its enclosing scope
Here, even though x
is not one of the parameters of equal_to_x
, the equal_to_x
closure is allowed to use the x
variable that’s defined in the same scope that equal_to_x
is defined in.
We can’t do the same with functions; if we try with the following example, our code won’t compile:
Filename: src/main.rs
fn main() {
let x = 4;
fn equal_to_x(z: i32) -> bool {
z == x
}
let y = 4;
assert!(equal_to_x(y));
}
We get an error:
$ cargo run
Compiling equal-to-x v0.1.0 (file:///projects/equal-to-x)
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
For more information about this error, try `rustc --explain E0434`.
error: could not compile `equal-to-x` due to previous error
The compiler even reminds us that this only works with closures!
When a closure captures a value from its environment, it uses memory to store the values for use in the closure body. This use of memory is overhead that we don’t want to pay in more common cases where we want to execute code that doesn’t capture its environment. Because functions are never allowed to capture their environment, defining and using functions will never incur this overhead.
Closures can capture values from their environment in three ways, which directly map to the three ways a function can take a parameter: taking ownership, borrowing mutably, and borrowing immutably. These are encoded in the three Fn
traits as follows:
FnOnce
consumes the variables it captures from its enclosing scope, known as the closure’s environment. To consume the captured variables, the closure must take ownership of these variables and move them into the closure when it is defined. TheOnce
part of the name represents the fact that the closure can’t take ownership of the same variables more than once, so it can be called only once.FnMut
can change the environment because it mutably borrows values.Fn
borrows values from the environment immutably.
When you create a closure, Rust infers which trait to use based on how the closure uses the values from the environment. All closures implement FnOnce
because they can all be called at least once. Closures that don’t move the captured variables also implement FnMut
, and closures that don’t need mutable access to the captured variables also implement Fn
. In Listing 13-12, the equal_to_x
closure borrows x
immutably (so equal_to_x
has the Fn
trait) because the body of the closure only needs to read the value in x
.
When you create a closure, Rust infers which trait to use based on how the closure uses the values from the environment. All closures implement FnOnce
because they can all be called at least once. Closures that don’t move the captured variables also implement FnMut
, and closures that don’t need mutable access to the captured variables also implement Fn
. In Listing 13-12, the equal_to_x
closure borrows x
immutably (so equal_to_x
has the Fn
trait) because the body of the closure only needs to read the value in x
.
If you want to force the closure to take ownership of the values it uses in the environment, you can use the move
keyword before the parameter list. This technique is mostly useful when passing a closure to a new thread to move the data so it’s owned by the new thread.
Note: move closures may still implement Fn or FnMut, even though they capture variables by move. This is because the traits implemented by a closure type are determined by what the closure does with captured values, not how it captures them. The move keyword only specifies the latter.
We’ll have more examples of move
closures in Chapter 16 when we talk about concurrency. For now, here’s the code from Listing 13-12 with the move
keyword added to the closure definition and using vectors instead of integers, because integers can be copied rather than moved; note that this code will not yet compile.
Filename: src/main.rs
fn main() {
let x = vec![1, 2, 3];
let equal_to_x = move |z| z == x;
println!("can't use x here: {:?}", x);
let y = vec![1, 2, 3];
assert!(equal_to_x(y));
}
We receive the following error:
$ cargo run
Compiling equal-to-x v0.1.0 (file:///projects/equal-to-x)
error[E0382]: borrow of moved value: `x`
--> src/main.rs:6:40
|
2 | let x = vec![1, 2, 3];
| - move occurs because `x` has type `Vec<i32>`, which does not implement the `Copy` trait
3 |
4 | let equal_to_x = move |z| z == x;
| -------- - variable moved due to use in closure
| |
| value moved into closure here
5 |
6 | println!("can't use x here: {:?}", x);
| ^ value borrowed here after move
For more information about this error, try `rustc --explain E0382`.
error: could not compile `equal-to-x` due to previous error
The x
value is moved into the closure when the closure is defined, because we added the move
keyword. The closure then has ownership of x
, and main
isn’t allowed to use x
anymore in the println!
statement. Removing println!
will fix this example.
Most of the time when specifying one of the Fn
trait bounds, you can start with Fn
and the compiler will tell you if you need FnMut
or FnOnce
based on what happens in the closure body.