(十一)Rust_迭代器和闭包


迭代器和闭包

闭包-使用闭包创建抽象行为

  1. 闭包:可以捕获其所在环境的匿名函数。

    • 是匿名函数。
    • 可以保存为变量、作为参数。
    • 可在一个地方创建闭包,然后在另一个上下文中调用闭包来完成运算。
    • 可从其定义的作用域捕获值。
  2. 例子-生成自定义运动计划的程序:

    • 目标:不让用户发生不必要的等待。
      • 仅在必要时调用该算法。
      • 只调用一次。
    use std::{thread, time::Duration};
    
    fn main() {
        let simulated_user_specified_value = 10;
        let simulated_random_number = 7;
        generate_workout(simulated_user_specified_value, simulated_random_number);
    }
    
    fn simulated_expensive_calculation(intensity: u32) -> u32 {
        println!("calculating slowly ....");
        thread::sleep(Duration::from_secs(2));
        intensity
    }
    
    fn generate_workout(intensity: u32, random_number: u32) {
        if intensity < 25 {
            println!(
                "Today, do {} pushups!",
                simulated_expensive_calculation(intensity)
            );
            println!(
                "Next, do {} situps!",
                simulated_expensive_calculation(intensity)
            );
        } else if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                simulated_expensive_calculation(intensity)
            );
        }
    }
    
  3. 针对以上代码进行优化:

    • 方案一:提取耗时计算,但是存在一个问题当 random 等于 3 时同样运行了耗时函数。
    fn generate_workout(intensity: u32, random_number: u32) {
        let expensive_result = simulated_expensive_calculation(intensity);
        if intensity < 25 {
            println!("Today, do {} pushups!", expensive_result);
            println!("Next, do {} situps!", expensive_result);
        } else if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!("Today, run for {} minutes!", expensive_result);
        }
    }
    
    • 方案二:闭包方式。
    fn generate_workout(intensity: u32, random_number: u32) {
        let expensive_closure = |num| {
            println!("calculating slowly ....");
            thread::sleep(Duration::from_secs(2));
            num
        };
        if intensity < 25 {
            println!("Today, do {} pushups!", expensive_closure(intensity));
            println!("Next, do {} situps!", expensive_closure(intensity));
        } else if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!("Today, run for {} minutes!", expensive_closure(intensity));
        }
    }
    
    • 方法三:使用 struct,持有闭包函数与结果:
      • 只会在需要结果时才执行该闭包。
      • 可缓存结果。
      • struct 的定义需要知道所有字段的类型,因此需要指定闭包的类型。
        • 每个闭包实例都有自己唯一的匿名类型,即使两个闭包签名完全一致,因此需要使用范型和 Trait Bound(08 章讲解)。
        • Fn Trait 由标准库提供,所有闭包都至少实现了一下 trait 之一:
          • Fn。
          • FnMut。
          • FnOnce。
    use std::{thread, time::Duration};
    
    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
                }
            }
        }
    }
    
    fn main() {
        let simulated_user_specified_value = 10;
        let simulated_random_number = 7;
        generate_workout(simulated_user_specified_value, simulated_random_number);
    }
    
    fn generate_workout(intensity: u32, random_number: u32) {
        let mut expensive_closure = Cacher::new(|num| {
            println!("calculating slowly ....");
            thread::sleep(Duration::from_secs(2));
            num
        });
        if intensity < 25 {
            println!("Today, do {} pushups!", expensive_closure.value(intensity));
            println!("Next, do {} situps!", expensive_closure.value(intensity));
        } else if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_closure.value(intensity)
            );
        }
    }
    
    • 缓存器的限制,嘉定针对不同的 arg 参数,value 方法总会得到同样的值。
      • 可以使用 HashMap 代替单个值:
        • key:arg 参数。
        • value:执行闭包结果。
      • 另一个限制:只能接收一个 u32 类型的参数和 u32 类型的返回值。
    #[cfg(test)]
    mod tests {
        #[test]
        fn call_with_different_values() {
            let mut c = super::Cacher::new(|a| a);
            let _v1 = c.value(1);
            let v2 = c.value(2);
            assert_eq!(v2, 2);
        }
    }
    

闭包的类型推断

  1. 闭包不要求标注参数和返回值的类型。

  2. 闭包通常很短小,只在狭小的上下文中工作,编译器通常能推断出类型。

    • 注意:闭包的定义最终只会为参数/返回值推断出唯一具体的类型(不会像模版一样)。
    /*
     --> src/main.rs:32:29
     |
     32 |     let n = example_closure(5);
     |                             ^ expected `&str`, found integer
    
     For more information about this error, try `rustc --explain E0308`.
     error: could not compile `simulated_expensive_calculation` due to previous error
     */
    
    let example_closure = |x| x;
    let s = example_closure("aa");
    let n = example_closure(5);
    

闭包捕获所在环境

  1. 闭包可以访问定义它的作用域内的变量,而普通函数不行。

    • 会产生内存开销。
  2. 闭包捕获环境值的方式:

    • 取得所有权:FnOnce:闭包可以从环境中消耗所捕获的变量。需要移动这些变量的所有权,因此只能调用一次。
    • 可变借用:FnMut:闭包可以从环境中可变的借用所捕获的变量。可以调用多次。
    • 不可变借用:Fn:闭包不可以修改环境中捕获的变量。
  3. 创建闭包时,通过闭包对环境值的使用,Rust 推断出具体使用哪个 trait:

    • 所有闭包都实现了 FnOnce.
    • 没有移动捕获变量的实现了 FnMut。
    • 无需可变访问捕获变量的闭包实现了 Fn。

move 关键字

  1. 在参数列表前使用 move 关键字,可以强制闭包取得它所使用的环境值的所有权:

    • 当将闭包传递给新线程以移动数据使其归新线程所有时,此技术最有用。
    /* 已经报错提示 x 被移动到闭包里 无法再使用。
    --> src/main.rs:72:44
    |
    70 |         let x = vec![1, 2, 3];
    |             - move occurs because `x` has type `Vec<i32>`, which does not implement the `Copy` trait
    71 |         let equal_to_x = move |z| z == x;
    |                          --------      - variable moved due to use in closure
    |                          |
    |                          value moved into closure here
    72 |         println!("can't see x here: {:?}", x);
    |                                            ^ value borrowed here after move
    |
    = note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-bac
    ktrace for more info)
    */
    fn call_test() {
        let x = vec![1, 2, 3];
        let equal_to_x = move |z| z == x;
        println!("can't see x here: {:?}", x);
        let y = vec![1, 2, 3];
        assert!(equal_to_x(y));
    }
    

最佳实践

  1. 当使用 Fn trait bound 之一时,首先使用 Fn,基于闭包体里的情况,如果需要 FnOnce 或 FnMut,编译器会告诉你。

迭代器

  1. 迭代器模式:对一系列项执行某些任务。

  2. 迭代器负责:

    • 遍历每个项。
    • 确定序列(遍历)何时完成。
  3. Rust 的迭代器:

    • lazy:除非调用消费迭代器的方法,否则迭代器本身没有任何效果。
    fn call_itertor() {
        let v1 = vec![1, 2, 3];
        let v1_iter = v1.iter();
    
        for val in v1_iter {
            println!("Got : {}", val);
        }
    }
    

iterator trait 和 next 方法

  1. 所有的迭代器都实现里 Iterator trait。

  2. Iterator trait 定义于标准库,定义大致如下:

    pub trait Iterator{
        type Item;
        fn next(&mut self) -> Option<Self::Item>;
    }
    
  3. type ItemSelf::Item 定义里与此个 trait 关联的类型:

    • 实现 Iterator trait 需要你定义一个 Item 类型,它用于 next 方法的返回类型(迭代器的返回类型)。
  4. 因此 Iterator trait 仅要求实现一个方法,即 next

    • 调用 next 方法,每次返回迭代器中的一项。
    • 返回的结果包裹在 Some 里。
    • 迭代结束时,返回 None。
    • 可直接在迭代器上调用 next 方法。
     fn iterator_demonstration() {
         let v1 = vec![1, 2, 3];
         let mut v1_iter = v1.iter();
    
         // 调用 next 方法时,相当于改变迭代器中的记录 pos 的数值了
         // 因此 迭代器 为 mut
         assert_eq!(v1_iter.next(), Some(&1));
         assert_eq!(v1_iter.next(), Some(&2));
         assert_eq!(v1_iter.next(), Some(&3));
     }
    
  5. 其他的迭代方法:

    • iter 方法:在不可变引用上创建迭代器。
    • into_iter 方法:创建的迭代器会获取所有权,for...in 这种循环就是使用这种因此不需要加 mut。
    • iter_mut 方法:迭代可变的引用。

消耗迭代器的方法

  1. 在标准库中, Iterator trait 有一些默认实现的方法。其中有一些方法会调用 next 方法:

    • 实现 Iterator trait 时就必须实现 next 方法的原因之一。
  2. 通常将调用 next 的方法叫做消耗型适配器

    • 因为调用它们会把迭代器消耗尽。
    • 例如:sum 方法,会取得迭代器的所有权,通过反复调用 next,遍历所有元素,每次迭代把当前元素添加到一个总和里,迭代结束返回总和。
    fn iterator_sum() {
        let v1 = vec![1, 2, 3];
        let v1_iter = v1.iter();
        let total: i32 = v1_iter.sum();
        assert_eq!(total, 6);
    }
    

产生其他迭代器的方法

  1. 定义在 Iterator trait 上的另外一些方法叫做迭代器适配器

    • 把迭代器转换为不同种类的迭代器。
  2. 可以通过链式调用使用多个迭代器适配器来执行复杂的操作,这种调用可读性较高。

    • 例如:map 可以接收一个闭包,将闭包作用于每一个元素,进而类似于产生里一个新的迭代器。
      • collect 方法:消耗型适配器,把结果收集到一个集合类型中。
     fn iterator_map() {
         let v1 = vec![1, 2, 3];
         // 迭代器是惰性的,如果不调用消耗型适配器方法那么 map 捕获做任何事情。
         // v1.iter().map(|x| x + 1);
         let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
         assert_eq!(v2, vec![2, 3, 4]);
     }
    

使用闭包捕获环境

  1. filter 方法:一个迭代器适配器。

    • 接收一个闭包。
    • 这个闭包在遍历迭代器的每个元素时,返回 bool 类型。
    • 如果闭包返回 true:当前元素将会包含在 filter 产生的迭代器中。
    • 如果闭包返回 false:当前元素将不会包含在 filter 产生的迭代器中。
    #[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(|x| x.size == shoe_size).collect()
    }
    
    #[test]
    fn filter_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_my_size(shoes, 10);
        assert_eq!(
            in_my_size,
            vec![
                Shoe {
                    size: 10,
                    style: String::from("sneaker"),
                },
                Shoe {
                    size: 10,
                    style: String::from("boot"),
                },
            ]
        )
    }
    

创建自定义的迭代器

  1. 使用 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
            }
        }
    }
    
    #[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);
    }
    
  2. 自定义另一个迭代器进行相乘:

     #[test]
     fn using_other_iterator_trait_method() {
         // zip 拉链,可以将两个迭代器合并到一起,结果是一个元组,其中到元素就是原先到两个迭代器
         // map 的参数是 zip 产生的元组,其分别对应的就是两个迭代器的值
         // filter 参数过滤掉其中不能被三整除的值
         // 最红 sum 进行遍历求和
         let sum: u32 = Counter::new()
             .zip(Counter::new().skip(1))
             .map(|(a, b)| a * b)
             .filter(|x| x % 3 == 0)
             .sum();
         assert_eq!(18, sum);
     }
    

改进 I/O 项目

  1. 详见 minigrep 与 minigrep-Simulated 改进差别。

文章作者: Layton
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Layton !
  目录