【Rust 笔记】13-迭代器(中)

13.3 - 迭代器(中 - 迭代器适配器)

  • Iterator 特型可以为迭代器提供大量可供选择的适配器方法,或简称适配器(adapter)。
  • 适配器消费一个迭代器,并创建一个具备有用行为的新迭代器。

13.3.1-mapfilter

  • map 适配器:为迭代器的每个迭代项都应用一个闭包。

  • filter 适配器:通过迭代器来过滤某些迭代项,使用闭包来决定保留或消除哪个迭代项。

    • 标准库 str::trim 方法可以清除 &str 开头和末尾的空格,返回一个新的修整后的借用原始值 &str

    • 可以通过 map 适配器给迭代器的每一行应用 str::trim

      let text = " ponies \n giraffes\niguanas \nsquid".to_string();
      let v: Vec<&str> = text.lines()
          .map(str::trim)
          .collect();
      assert_eq!(v, ["ponies", "giraffes", "iguanas", "squid"]);
      
    • 调用 map 返回的迭代器本身仍然可以继续使用适配器。如下所示,继续排除 "iguanas"

      let text = " ponies \n giraffes\niguanas \nsquid".to_string();
      let v: Vec<&str> = text.lines()
          .map(str::trim)               // 先调用适配器map
          .filter(|s| *s != "iguanas")  // 再调用适配器filter
          .collect();
      assert_eq!(v, ["ponies", "giraffes", "iguanas", "squid"]);
      
  • mapfilter 适配器的签名:

    fn map<B, F>(self, f: F) -> some Iterator<Item=B>
        where Self: Sized, F: FnMut(Self:: Item) -> B;
    
    fn filter<P>(self, predicate: P) -> some Iterator<Item=Self::Item>
        where Self: Sized, P: FnMut(&Self:: Item) -> bool;
    
  • mapfilter 适配器的区别:

    • map 适配器将迭代器每一项按值传给其闭包,进而将闭包返回结果的所有权传递给消费者。
    • filter 适配器将迭代器每一项的共享引用传给其闭包,在将选中项传给其消费者时,保留该项的所有权。
  • 对于迭代器适配器有以下两个特点:

    • 简单地在一个迭代器上调用适配器不会消费任何项,只会返回一个新迭代器,并可以按需从第一个迭代器排取以产生自己的项。在适配器链中,唯一可以真正让操作落地的是在最终的迭代器上调用 next

      ["earth", "water", "air", "fire"]
          .iter().map(|elt| println!("{}", elt));
      

      对于上述代码,编译时 Rust 会给出警告提示:其中 lazy 指的是一种将计算推迟到真正需要时的机制。

      warning: unused result which must b used:
      iterator adaptors are lazy and do nothing unless consumed
          |
          |
      387 | / ["earth", "water", "air", "fire"]
      388 | |     .iter().map(|elt| println!("{}", elt));
          | |_________________________________________^
          |
      = note: #[warn(unused_must_use)] on by default
      
    • 迭代器适配器属于零开销抽象。因为 mapfilter 及其同类方法是泛型的,所以把它们应用给迭代器会针对涉及的特定迭代器类型特化它们的代码。

      // 上述代码等效于:
      for line in text.lines() {
          let line = line.trim();
          if line != "iguanas" {
              v.push(line);
          }
      }
      

13.3.2-filter_mapflat_map

  • filter_map 适配器:允许闭包在迭代过程中转换项(类似 map),也支持删除项。类似 filtermap 的组合。

    fn filter_map<B, F>(self, f: F) -> some Iterator<Item=B>
        where Self: Sized, F: FnMut(Self:: Item) -> Option<B>;
    
    • 适配器中的闭包返回的是 Option<B>

    • 如果适配器返回 None,表示从迭代中清除项;

    • 如果适配器返回 Some(b),则 b 就是 filter_map 迭代器的下一项。

      use std::str::FromStr;
      
      let text = "1\nfrond .25 289\n3.1415 estuary\n";
      for number in text.split_whitespace()
          .filter_map(|w| f64::from_str(w).ok()) {
              println!("{:4.2}", number.sqrt());
      }
      
    • 上述代码输出结果为:

      1.00
      0.50
      17.00
      1.77
      
    • 在迭代中决定是否包含某一项的最好方式,就是尝试实际去处理它。

  • flat_map 适配器:传给它的闭包必须返回一个可迭代类型,任何可迭代类型都可以。

    fn flat_map<U, F>(self, f: F) -> some Iterator<Item=U::Item>
        where F: FnMut(Self::Item) -> U, U: IntoIterator;
    
    • 只有 for 循环调用 flat_map 迭代器的 next 方法时,才会实际发生迭代。

    • 拼接完整序列的操作不会在内存中发生。

    • 对于下述例子:对每个国家,先取得其城市的向量,然后把所有向量拼接为一个序列,再把它打印出来。

      use std::collections::HashMap;
      
      let mut major_cities = HashMap::new();
      major_cities.insert("Japan", vec!["Tokyo", "Kyoto"]);
      major_cities.insert("The United States", vec!["Portland", "Nashville"]);
      major_cities.insert("Brazil", vec!["Sao Paulo", "Brasilia"]);
      major_cities.insert("Kenya", vec!["Nairobi", "Mombasa"]);
      major_cities.insert("The Netherlands", vec!["Amsterdam", "Utrecht"]);
      
      let countries = ["Japan", "Brazil", "Kenya"];
      
      for &city in countries.iter().flat_map(|country| &major_cities[country]) {
          println!("{}", city);
      }
      
    • 结果输出为:

      Tokyo
      Kyoto
      Sao Paulo
      Brasilia
      Nairobi
      Mombasa
      

13.3.3-scan

  • scan 适配器:

    • 类似于 map,区别是它会传给闭包一个可修改的值,而且可以选择提前终止迭代。
    • 接收一个初始状态值和一个闭包,闭包又接收一个对这个状态的可修改引用和底层迭代器的下一项。
    • 这个闭包必须返回 Optionscan 迭代器将其作为自己的下一项。
  • 如下所示例子:一个迭代器链会对另一个迭代器的项求平方,并在和超过 10 时终止迭代:

    let iter = (0..10).scan(0, |sum, item| {
        *sum += item;
        if *sum > 10 {
            None
        } else {
            Some(item * item)
        }
    });
    
    assert_eq!(iter.collection::<Vec<i32>>(), vec![0, 1, 4, 9, 16]);
    
    • 闭包的 sum 参数是一个迭代器私有变量的可修改引用,在 scan 的第一个参数中初始化,即 0。
    • 闭包会更新 *sum,检查它是否超过了限制,然后返回迭代器的下一个结果。

13.3.4-taketake_while

  • Iterator 特型的 taketake_while 适配器用于在取得一定项数之后或闭包决定中断时终止迭代。

    fn take(self, n: usize) -> some Iterator<Item=Self:: Item>
        where Self: Sized;
    
    fn take_while<P>(self, predicate: P) -> some Iterator<Item=Self:: Item>
        where Self: Sized, P: FnMut(&Self:: Item) -> bool;
    
    • 这两个适配器都会取得一个迭代器的所有权,返回一个新迭代器。
    • 这个新迭代器会从第一项开始产生值,但可能提前终止序列。
    • take 适配器在产生最多 n 项后,返回 None
    • take_while 适配器对每一项应用 predicate,在遇到第一个 predicate 返回 false 项时,适配器返回 None,后续每次调用 next 也都返回 None
  • 如下所示:使用 take_while 迭代邮件的标题行。邮件的内容以空格分隔标题行和正文。

    let message = "To: jimb\r\n\
        From: superego <editor@oreilly.com>\r\n\
        \r\n
        Did you get any writing done today?\r\n\
        When will you stop wasting time plotting fractals?\r\n";
    
    for header in message.lines().take_while(|l| !l.is_empty) {
        println!("{}", header);
    }
    
    • 当一行字符串以 \ 斜杠结尾时,Rust 不会在字符串中包含下一行的缩进,因此字符串中每一行前面都不会有空格。

    • 此时,第三行是空的。take_while 适配器碰到这个空行就会终止迭代,因此上述代码只会打印以下信息:

      To: jimb
      From: superego <editor@oreilly.com>
      

13.3.5-skipskip_while

  • Iterator 特型的 skipskip_while 方法是对 taketake_while 的补充。它们从迭代开始清除一定数量的项,或者一直清除到闭包发现一个可以接受的项,然后将剩余项原封不动返回。

    fn skip(self, n: usize) -> some Iterator<Item=Self:: Item>
        where Self: Sized;
    
    fn skip_while<P>(self, predicate: P) -> some Iterator<Item=Self:: Item>
        where Self: Sized, P: FnMut(&Self:: Item) -> bool;
    
  • skip 适配器的应用场景:在迭代程序的命令行参数时跳过命令名。

    for arg in std::env::args().skip(1) {
        ...
    }
    
    • std::env::args 函数返回一个迭代器,该迭代器会产生 String 类型的程序参数,其中第一项是程序本身的名字。
    • 第一次被调用时,skip(1) 会清除程序名,然后产生后面所有的参数。
  • skip_while 适配器使用闭包来决定清除序列开头的多少项。

    let message = "To: jimb\r\n\
        From: superego <editor@oreilly.com>\r\n\
        \r\n
        Did you get any writing done today?\r\n\
        When will you stop wasting time plotting fractals?\r\n";
    
    for body in message.lines()
        .skip_while(|l| !l.is_empty())
        .skip(1) {
            println!("{}", body);
    }
    
    • skip_while 会跳过非空的行,但此时迭代器还会产生空行,因为这个闭包碰到空行会返回 false

    • 所以,又使用 skip 方法跳过了这个空行,这样得到的迭代器第一项就是邮件正文的第一行。

    • 输出结果为:

      Did you get any writing done today?
      When will you stop wasting time plotting fractals?
      

13.3.6-peekable

  • Iterator 特型的 peekable 方法可以让代码在不消费下一项的情况下探测下一项。调用这个方法可以将几乎任何迭代器转换为可探测的迭代器:

    fn peekable(self) -> std::iter::Peekable<Self>
        where Self: Sized;
    
    • Peekable<Self> 是一个实现了 Iterator<Item=Self::Item> 的结构体。
    • Self 是底层迭代器的类型。
    • Peekable 迭代器有一个 peek 方法,该方法返回 Option<&Item>:如果底层迭代器终止就返回 None,否则返回 Some(r),其中 r 是下一项的共享引用。
    • 调用 peek 会尝试从底层迭代器取出下一项,如果取到了,就将其缓存到下一次调用 next
  • 举例:从一个字符流中解析数值,在发现其后面第一个非数值字符之前无法确定数值是否结束:

    use std::iter::Peekable;
    
    fn parse_number<I>(tokens: &mut Peekable<I>) -> u32
        where I: Iterator<Item=char>
    {
        let mut n = 0;
        loop {
            match tookens.peek() {
                Some(r) if r.is_digit(10) => {
                    n = n * 10 + r.to_digit(10).unwrap();
                }
                _ => return n
            }
            tokens.next();
        }
    }
    
    let mut chars = "226153980,1766319049".chars().peekable();
    assert_eq!(parse_number(&mut chars), 226153980);
    assert_eq!(chars.next(), Some(','));
    assert_eq!(parse_number(&mut chars), 1766319049);
    assert_eq!(chars.next(), None);
    
    • parse_number 函数使用 peek 检查下一个字符,只有该字符是数值时才消费它。
    • 如果不是数字或这迭代器被耗尽(即 peek 返回 None),则返回已解析的数值,并把下一个字符留在迭代器中供后面消费。

13.3.7-fuse

Iterator 特型的 fuse 适配器,可以将任何适配器转换为第一次返回 None 之后始终继续返回 None 迭代器。

  • 用于解决迭代器返回 None 的情况下,再调用 next 的场景。
  • 适合处理不确定来源迭代器的泛型代码。这时候不用架设所有迭代器的行为一致。
struct Flaky(bool);

impl Iterator for Flaky {
    type Item = &'static str;
    fn next(&mut self) -> Option<Self::Item> {
        if self.0 {
            self.0 = false;
            Some("totally the last item")
        } else {
            self.0 = true;
            None
        }
    }
}

let mut flaky = Flaky(true);
assert_eq!(flaky.next(), Some("totally the last item"));
assert_eq!(flaky.next(), None);
assert_eq!(flaky.next(), Some("totally the last item"));

let mut not_flaky = Flaky(true).fuse();
assert_eq!(not_flaky.next(), Some("totally the last item"));
assert_eq!(not_flaky.next(), None);
assert_eq!(not_flaky.next(), None);

13.3.8 - 可逆迭代器与 rev

  • 可逆迭代器:可以从序列两端取得项。

  • 一个迭代向量的迭代器可以转换为从向量末尾开始取值。即可以实现 std::iter::DoubleEndedIterator 特型,该特型扩展了 Iterator

    trait DoubleEndedIterator: Iterator {
        fn next_back(&mut self) -> Option<Self::Item>;
    }
    
    • 调用这个特型:

      use std::iter::DoubleEndedIterator;
      
      let bee_parts = ["head", "thorax", "abdomen"];
      let mut iter = bee_parts.iter();
      assert_eq!(iter.next(), Some(&"head"));
      assert_eq!(iter.next_back(), Some(&"abdomen"));
      assert_eq!(iter.next(), Some(&"thorax"));
      
      assert_eq!(iter.next(), None);
      assert_eq!(iter.next_back(), None);
      
    • 这样的迭代器相当于一个指针,分别指向尚未产生元素的头和尾。

    • 并不是所有迭代器都可以实现两端迭代。如基于另一个线程返回给 Receiver 值的迭代器无法预算接收到的最后一个值是什么。此时,需要查询标准库文档来确定一个迭代器是否实现了 DoubleEndedIterator 特型。

  • rev 适配器:对于实现了 DoubleEndedIterator 特型的迭代器,可以使用 rev 适配器将其反转。

    fn rev(self) -> some Iterator<Item=Self>
        where Self: Sized + DoubleEndedIterator;
    
    • 返回的迭代器同样支持两端取值,其 nextnext_back 方法知识简单互换。

      let meals = ["breakfast", "lunch", "dinner"];
      
      let mut iter = meals.iter().rev();
      assert_eq!(iter.next(), Some(&"dinner"));
      assert_eq!(iter.next(), Some(&"lunch"));
      assert_eq!(iter.next(), Some(&"breakfast"));
      assert_eq!(iter.next(), None);
      
    • 在应用给可逆迭代器后,大多数迭代器适配器会返回另一个可逆迭代器。如 mapfilter 都具有可逆性。

13.3.9-inspect

  • inspect 适配器:

    • 用于迭代器适配器管道的调试。
    • 只是简单地对每一项的共享引用应用一个闭包,然后再产生相应的项。
    • 这个闭包不影响产生的项,但可以打印项或对项执行断言。
  • 举例:把字符串转换为大写会改变其长度:

    let upper_case: String = "grosse".chars()
        .inspect(|c| println!("before: {:?}", c))
        .flat_map(|c| c.to_uppercase())
        .inspect(|c| println!(" after: {:?}", c))
        .collect();
    
    assert_eq!(upper_case, "GROSSE");
    

13.3.10-chain

  • chain 适配器:将一个迭代器添加到另一个适配器后面。

    • i1.chain(i2) 会返回一个迭代器,该迭代器会先从 i1 中提取项,取完后再继续从 i2 中提取项。

    • chain 适配器的签名如下:

      fn chain<U>(self, other: U) -> some Iterator<Item=Self:: Item>
          where Self: Sized, U: IntoIterator<Item=Self:: Item>;
      
  • 可以将迭代器与任何产生相同项类型的可迭代类型连缀在一起:

    let v: Vec<i32> = (1..4).chain(vec![20, 30, 40]).collect();
    assert_eq!(v, [1, 2, 3, 20, 30, 40]);
    
    • 如果连缀的两个迭代器都是可逆的,则 chain 返回的迭代器也是可逆的:

      lev v: Vec<i32> = (1..4).chain(vec![20, 30, 40]).rev().collect();
      assert_eq!(v, [40, 30, 20, 3, 2, 1]);
      
    • chain 迭代器会跟踪底层的迭代器是否返回 None,根据情况调用某个底层迭代器的 nextnext_back

13.3.11-enumerate

  • Iterator 特型的 enumerate 适配器,可以向序列中添加连续的索引。

    • 对于产生项为 A, B, C... 的迭代器,其返回的迭代器则产生 (0, A), (1, B), (2, C)...
    • 消费者可以通过索引却别不通的项,从而建立处理每一项的上下文。
  • 用法举例:

    // 创建一个矩形的像素缓冲区
    let mut pixels = vec![0; columns * rows];
    
    // 使用chunks_mut 将图像切分成水平长条,每个线程分派一个。
    let threads = 8;
    let band_rows = rows / threads + 1;
    let bands: Vec<&mut [u8]> = pixels.chunks_mut(band_rows * columns).collect();
    
    // 迭代这些长条,为每个长条启动一个线程。
    for (i, band) in bands.into_iter().enumerate() {
        let top = band_rows * i;  // 启动一个线程渲染top..top + band_rows
    }
    
    • 每次迭代都得到一对值 (i, band),其中 band 是线程应该渲染的像素缓冲的 &mut [u8] 切片;
    • i 是相应长条在整个图像中的索引。这个索引是 enumerate 适配器提供的。

13.3.12-zip

  • zip 适配器:将两个适配器组合为一个适配器,产生之前两个迭代器项的项对。(如同拉链把分开的两边拼在一起)

  • 举例:将半开范围 0.. 与其他迭代器组合在一起,得到与使用 enumerate 适配器同样的效果。

    let v: Vec<_> = (0..).zip("ABCD".chars()).collect();
    assert_eq!(v, vec![0, 'A'], [1, 'B'], (2, 'C'), (3, 'D'));
    
  • zip 相当于通用化的 enumerate

    • enumerate 只能给其他序列添加索引;
    • zip 可以添加任何迭代项。
  • 传给 zip 的参数不一定是迭代器本身,也可以是任何可迭代类型:

    use std::iter::repeat;
    
    let endings = vec!["once", "twice", "chicken soup with rice"];
    let rhyme: Vec<_> = repeat("going")
        .zip(endings)
        .collect();
    assert_eq!(rhyme, vec![
        ("going", "once"),
        ("going", "twice"),
        ("going", "chicken soup with rice")
    ]);
    

13.3.13-by_ref

  • 迭代器的 by_ref 方法可以借用迭代器的一个可修改引用,以便把适配器应用给这个引用。

    • 适配器会取得底层迭代器的所有权,没有办法再还原迭代器。
    • by_ref 方法在通过适配器消费完迭代器的项后,借用结束,可以恢复对原始迭代器的访问。
  • by_ref 的定义:

    impl<'a, I: Iterator + ?Sized> Iterator for &'a mut I {
        type Item = I:: Item;
        fn next(&mut self) -> Option<I: Item> {
            (**self).next()
        }
        fn size_hint(&self) -> (usize, Option<usize>) {
            (**self).size_hint()
        }
    }
    
    • 如果 I 是某种迭代器类型,那么 &mut I 也是迭代器,其 nextsize_hint 方法会解引用到它的引用值。
    • 对迭代器的可修改引用调用适配器时,适配器取得引用而非迭代器本身的所有权。在社佩奇超出作用域时会终止这个借用关系。
  • 举例:

    let message = "
        To: jimb\r\n\
        From: id\r\n\
        \r\n
        Oooooh, donuts!!\r\n
    ";
    let mut lines = message.lines()
    
    println!("Headers:");
    for header in lines.by_ref().take_while(|l| !l.is_empty()) {
        println!("{}", header);
    }
    
    println!("\nBody:");
    for body in lines {
        println!("{}", body)
    }
    
    • lines.by_ref() 调用从迭代器借用了一个可修改引用,而 take_while 迭代器只是取得了这个引用的所有权。
    • 第一个 for 循环结束后,take_while 迭代器离开作用域,借用关系随之终止。
    • 第二个 for 循环中,可以继续使用 lines

13.3.14-cloned

cloned 适配器可以将一个产生引用的迭代器转换为产生基于引用克隆的值的迭代器。引用值的类型必须实现 Clone

let a = ['1', '2', '3', '4'];

assert_eq!(a.iter().next(),          Some(&'1'));
assert_eq!(a.iter().cloned().next(),  Some('1'));

13.3.15-cycle

  • cycle 适配器返回一个无休止重复底层迭代器的迭代器。

    • 底层迭代器必须实现 std::clone::Clone

    • 以便 cycle 可以保存其初始状态并在每次循环开始时重用。

      let dirs = ["North", "East", "South", "West"];
      let mut spin = dirs.iter().cycle();
      
      assert_eq(spin.next(), Some(&"North"));
      assert_eq(spin.next(), Some(&"East"));
      assert_eq(spin.next(), Some(&"South"));
      assert_eq(spin.next(), Some(&"West"));
      
      assert_eq(spin.next(), Some(&"North"));
      assert_eq(spin.next(), Some(&"East"));
      
  • 典型计算题:用 fizz 替换可以被 3 整除的数,用 buzz 替换可以被 5 整除的数。可以同时被 3 和 5 整除的数就会得到 fizzbuzz

    use std::iter::{once, repeat};
    
    let fizzes = repeat("").take(2).chain(once("fizz")).cycle();
    let buzzes = repeat("").take(4).chain(once("buzz")).cycle();
    let fizzes_buzzes = fizzes.zip(buzzes);
    
    let fizz_buzz = (1..100).zip(fizzes_buzzes)
        .map(
            |tuple| match tuple {
                (i, ("", "")) => i.to_string(),
                (_, (fizz, buzz)) => format!("{}--{}{}", i.to_string(), fizz, buzz)
            }
    );
    
    for line in fizz_buzz {
        println!("{}", line);
    }
    

详见《Rust 程序设计》(吉姆 - 布兰迪、贾森 - 奥伦多夫著,李松峰译)第十五章
原文地址

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

phial03

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值