21

迭代器适配器(Iterator Adapter)

 4 years ago
source link: http://bean-li.github.io/Iterator-Adapter/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

前言

本文介绍Iterator Adapters,即迭代器适配器。Iterator Adapter消耗一个迭代元素转换成另一个迭代元素。

迭代器就好像是现实世界的水管,现实世界中,水管是标准化的接口,打开水管就可以出水。厨房用水可能需要加热,洗澡间用水可能需要冷热混合,淋浴花洒可能需要将水分割成若干个小水流。干这些事情的,我们称之为适配器。

map and filter

map很简单,将方法映射到迭代器产生的每一个元素,从而输出新的迭代元素。

fn main() {
    let text = " Panda \n    giraffes\niguanas \nsquid".to_string();
    let v:Vec<&str> = text.lines()
        .map(str::trim)
        .collect() ;

    assert_eq!(v, ["Panda", "giraffes", "iguanas", "squid"]);
}

String和&str提供了lines方法,该方法也会产生一个迭代器,这是上一篇产生迭代器方法中没有提到的。类似的方法有很多:

  • s.bytes()
  • s.chars()
  • s.split_whitespace()
  • s.lines()
  • s.split(‘/’)
  • s.matches(char::is_numeric)

所以text.lines 方法返回的是一个迭代器,即以回车字符为界,每次迭代产生出一行的内容。

map将每一行的内容,执行str::trim方法,即将每一行头部和尾部的空白字符删除。

collect方法将迭代器转换成Vec,从而完成将一段文本,按行生成一个Vec,同时去除头部和尾部的无用空白字符。

map很容易理解,即对每一个元素都执行对应的方法,得到新的迭代元素,从而得到新的迭代器。filter,顾名思义,即过滤,即对每个迭代元素执行对应方法,得到bool型的结果,只有bool值为true的迭代元素才会保留下来,bool值为负的元素被滤出掉,从而形成新的迭代器。

fn main() {
    let text = " Panda \n    giraffes\niguanas \nsquid".to_string();
    let v:Vec<&str> = text.lines()
        .map(str::trim)
        .filter(|s| *s != "iguanas")
        .collect() ;

    assert_eq!(v, ["Panda", "giraffes", "squid"]);
}

如上示范代码,滤除等于”iguanas”的迭代元素。

这两个适配器的方法签名如下:

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

我们细细品map和filter,可以得到如下结论:

  • map 是值传递,而filter是共享引用传递。
  • map之后新的迭代器的元素和原始迭代器产生的元素,未必是同一种类型
  • filter之后,新的迭代器的元素和原始迭代器的元素是同一种类型

filter是共享引用,这也是为什么我们示例代码中,为 *s != “iguanas”的原因。

最后需要指出的是,迭代器适配器是惰性的,要有真正的消费行为,才会产生迭代,否则就只是个迭代器而已,无人调用。回到我们的范例,最后的collect方法调用,才真正产生了消费行为,才会不断调用迭代器的next方法。

我们给个反面的示范:

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

上面方法看起来很好,但是打印每一个元素的值,但是实际上,map适配器是产生出新的迭代器,但是没有任何方法要求迭代器产生出新的元素,因此,Rust会发出警告。

enumerate

enumerate 适配器是一个看起来相当无聊,但在实际编程中,非常常用的适配器。我相信写过python代码的人,读到这一句,都会会心一笑,赞同我所言非虚。

比如一个迭代器产生出 A , B , C这种元素,那么经过enumerate适配器之后,新的迭代器产生出(0,A),(1,B), (2,C) 这种迭代元素。

fn main() {
    let a = ['a', 'b', 'c'] ;
    let iter = a.iter().enumerate();

    for (idx, elem) in iter {
        println!("({}, {})", idx, elem)
    }
}

输出结果如下:

(0, a)
(1, b)
(2, c)

zip

zip适配器是将两个迭代器的元素,捏合成一个迭代器。比如第一个迭代器的迭代元素是”a”, “b”, “c”,第二个迭代器的迭代元素’A’, ‘B’, ‘C’,那么iter1.zip(iter2),产生的新的迭代器的元素为(‘a’, ‘A’), (‘b’, ‘B’),(‘c’, ‘C’)。

从上面的讨论可以看出,enumerate适配器,本质是zip适配器的一种特例。

(0..).zip(iter) 
iter.enumerate()

chain

zip是将两个迭代器配对成一个新的迭代器,新迭代器中每个元素是两个迭代器同一位置的元素组成的元组。

chain 适配器是将两个迭代器连接在一起,即append的意思。

以 iter1.chain(iter2)为例,返回一个新的迭代器,新的迭代器的next方法,会先返回iter1迭代器中的元素,当iter1中元素耗尽,那么开始从iter2 迭代器中获取元素,知道iter2中的元素也耗尽。简单地说,就是把iter2 放到iter1的后面,连成一个新的迭代器。

fn main() {
    let a1 = [1, 2, 3] ;
    let a2 = [4, 5, 6] ;
    let iter = a1.iter().chain(a2.iter());
    for elem in iter {
        println!("{}", elem)
    }
}

上面的做法是严格的做法,实际上,

fn chain<U>(self, other: U) -> Chain<Self, <U as IntoIterator>::IntoIter>
where  U: IntoIterator<Item = Self::Item>,

chain 适配器使用IntoIterator,我们可以传给它任何可以转换成Iterator的变量,比如Vec本身,即上面的示范,也可以写成:

let iter = a1.iter().chain(a2)

因为slice(&[T]) 实现了IntoIterator,所以可以直接传给chain作为参数。

filter_map

filter_map 适配器是个非常有用的适配器,它有点是filter 和map的结合体的意味。我们有时候遍历Result类型的迭代器的时候,有时候,可能会有失败的情况,我们就用filter_map来忽略失败的项。

fn main() {
    let strings = vec!["tofu", "93", "18"];
    let numbers: Vec<_> = strings
        .into_iter()
        .map(|s| s.parse::<i32>())
        .filter_map(Result::ok)
        .collect();
    println!("Results: {:?}", numbers);
}

我们故意用了一个比较啰嗦的版本,我们来细细地分析:

  1. map( s s.parse:: ())返回的是类型是Result类型
  2. Result::ok()方法,如果result是成功的,那么返回Some(success_value),如果执行失败,返回None
  3. filter_map适配器会对迭代器的每一个元素调用闭包,如果闭包返回Some(element),那么element元素返回,如果闭包返回None,那么它会忽略该元素,尝试对下一个迭代器元素调用闭包。

所以我们总结下filter_map的实质:

  1. filter_map适配器,会对迭代器中的每一元素执行闭包,体现了map的方面
  2. 执行闭包之后,正常的结果元素,保留,作为结果迭代器的元素,如果闭包的返回值是None,忽略,体现filter的方面。

上面的实例代码,我们可以进一步简化:

fn main() {
    let strings = vec!["tofu", "93", "18"];
    let numbers: Vec<_> = strings
        .into_iter()
        .filter_map(|s| s.parse::<i32>().ok())
        .collect();
    println!("Results: {:?}", numbers);
}

flat_map

flat是平坦的意思,flatten是拍平。有些时候,可能需要将多个序列的结果级联起来,变成一个序列。

比如说我们有个数据结构称为主要城市:

  • 日本Japan的主要城市有Tokyo, Kyoto
  • 中国China的主要城市有BeiJing,ShangHai,NanJing
  • 美国USA的主要城市有Portland,Washington

如果我们需要得到一个中国 日本 美国的城市列表,我们就需要打破各自序列的界限,变成一个列表。

use std::collections::HashMap ;

fn main() {
    let mut major_cities = HashMap::new() ;

    major_cities.insert("China", vec!["Beijing", "Shanghai", "Nanjing"]);
    major_cities.insert("Japan", vec!["Tokyo", "Kyoto"]);
    major_cities.insert("USA", vec!["Portland", "New York"]);

    let counties = ["China", "Japan", "USA"];

    for &city in counties.iter().flat_map(|country| &major_cities[country]) {
        println!("{}", city);
    }
}

输出如下:

Beijing
Shanghai
Nanjing
Tokyo
Kyoto
Portland
New York

我们细细的解析flat_map

  • 首先是map,map之后,每个元素都是一个迭代器
  • 然后将map之后的多个迭代器的界限打破,平坦化,把多个迭代器变成一个迭代器。

另一个类似的例子如下:

fn main() {
    let words = ["alpha", "beta", "gamma"];

    // chars() returns an iterator
    let merged: String = words.iter()
        .flat_map(|s| s.chars())
        .collect();
    assert_eq!(merged, "alphabetagamma");
}

take和take_while

take适配器用来截取迭代器的前n个元素。

fn main() {
    let a = [1, 2, 3];
    let mut iter = a.iter().take(2);

    assert_eq!(iter.next(), Some(&1));
    assert_eq!(iter.next(), Some(&2));
    assert_eq!(iter.next(), None);
}

这个take没啥特别的,理解了take,可以接下来学习take_while适配器了。

take_while适配器和take一样,截取原始迭代器的部分元素,但是结束条件不一样:

  • take(n):很明确,即选取前n个元素,余者不取
  • take_while : 会有闭包函数,闭包函数返回bool型结果,如果bool型结果为false,即立刻停止。
fn main() {
    let message = "To: jimb\r\n\
                   From: superego <[email protected]>\r\n\
                   \r\n\
                   Did you get the 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)
    }
}

message是一封邮件的内容,header和body之间,有一空行。解析程序通过判断空行位置,来打印header信息:

To: jimb
From: superego <[email protected]>

#skip 和 skip_while

skip和take正好相反。skip是丢掉迭代器前n个元素。

我们做命令行解析的时候,第一个参数一般是可执行文件本身,需要被忽略掉。

for arg in std::env::args.skip(1) {
	...
}

skip_while的用法和take_while正好相反:

  • drop 或skip掉 迭代器的前面的元素
  • 直到闭包函数返回false
fn main() {
    let message = "To: jimb\r\n\
                   From: superego <[email protected]>\r\n\
                   \r\n\
                   Did you get the 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()){
        println!("{}", body)
    }
}

跳过前面的非空行,从第一个空行开始,打印后面所有的行:

Did you get the any writing done today
when will you stop wasting time plotting fractals

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK