当前位置:网站首页>【Rust 笔记】13-迭代器(中)
【Rust 笔记】13-迭代器(中)
2022-07-05 05:50:00 【phial03】
13.3 - 迭代器(中 - 迭代器适配器)
Iterator
特型可以为迭代器提供大量可供选择的适配器方法,或简称适配器(adapter)。- 适配器消费一个迭代器,并创建一个具备有用行为的新迭代器。
13.3.1-map
和 filter
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"]);
map
和filter
适配器的签名: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
适配器将迭代器每一项的共享引用传给其闭包,在将选中项传给其消费者时,保留该项的所有权。
对于迭代器适配器有以下两个特点:
简单地在一个迭代器上调用适配器不会消费任何项,只会返回一个新迭代器,并可以按需从第一个迭代器排取以产生自己的项。在适配器链中,唯一可以真正让操作落地的是在最终的迭代器上调用
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
迭代器适配器属于零开销抽象。因为
map
、filter
及其同类方法是泛型的,所以把它们应用给迭代器会针对涉及的特定迭代器类型特化它们的代码。// 上述代码等效于: for line in text.lines() { let line = line.trim(); if line != "iguanas" { v.push(line); } }
13.3.2-filter_map
和 flat_map
filter_map
适配器:允许闭包在迭代过程中转换项(类似map
),也支持删除项。类似filter
与map
的组合。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
,区别是它会传给闭包一个可修改的值,而且可以选择提前终止迭代。 - 接收一个初始状态值和一个闭包,闭包又接收一个对这个状态的可修改引用和底层迭代器的下一项。
- 这个闭包必须返回
Option
,scan
迭代器将其作为自己的下一项。
- 类似于
如下所示例子:一个迭代器链会对另一个迭代器的项求平方,并在和超过 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-take
和 take_while
Iterator
特型的take
和take_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 <[email protected]>\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-skip
和 skip_while
Iterator
特型的skip
和skip_while
方法是对take
和take_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 <[email protected]>\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;
返回的迭代器同样支持两端取值,其
next
和next_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);
在应用给可逆迭代器后,大多数迭代器适配器会返回另一个可逆迭代器。如
map
和filter
都具有可逆性。
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
,根据情况调用某个底层迭代器的next
和next_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
也是迭代器,其next
和size_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 程序设计》(吉姆 - 布兰迪、贾森 - 奥伦多夫著,李松峰译)第十五章
原文地址
边栏推荐
- Reader writer model
- How many checks does kubedm series-01-preflight have
- Detailed explanation of expression (csp-j 2021 expr) topic
- ALU逻辑运算单元
- Solution to game 10 of the personal field
- One question per day 1765 The highest point in the map
- 剑指 Offer 53 - I. 在排序数组中查找数字 I
- kubeadm系列-02-kubelet的配置和启动
- The connection and solution between the shortest Hamilton path and the traveling salesman problem
- js快速将json数据转换为url参数
猜你喜欢
Talking about JVM (frequent interview)
【实战技能】非技术背景经理的技术管理
Using HashMap to realize simple cache
剑指 Offer 35.复杂链表的复制
CF1637E Best Pair
Sword finger offer 05 Replace spaces
lxml. etree. XMLSyntaxError: Opening and ending tag mismatch: meta line 6 and head, line 8, column 8
网络工程师考核的一些常见的问题:WLAN、BGP、交换机
Some common problems in the assessment of network engineers: WLAN, BGP, switch
1.14 - 流水线
随机推荐
One question per day 1765 The highest point in the map
Solution to game 10 of the personal field
Dynamic planning solution ideas and summary (30000 words)
leetcode-556:下一个更大元素 III
过拟合与正则化
Daily question 2006 Number of pairs whose absolute value of difference is k
Daily question - Search two-dimensional matrix PS two-dimensional array search
Control unit
LeetCode 1200.最小绝对差
1.14 - 流水线
Introduction et expérience de wazuh open source host Security Solution
Binary search template
How many checks does kubedm series-01-preflight have
Developing desktop applications with electron
API related to TCP connection
CF1634E Fair Share
Time of process
After setting up the database and website When you open the app for testing, it shows that the server is being maintained
Detailed explanation of expression (csp-j 2021 expr) topic
ALU逻辑运算单元