说明:本文主要是 Rust语言圣经 相关章节的学习笔记,大部分与其内容相同。
迭代器允许我们迭代一个集合,在此过程中只需关心集合中的元素如何处理,而无需关心如何开始、如何结束、按照什么样的索引去访问等问题。
for 循环与迭代器
迭代器与 for 循环最主要的差别就在于:是否通过索引来访问集合。严格来说,Rust 中的 for 循环是编译器提供的语法糖,最终还是对迭代器中的元素进行遍历。在 Rust 中,实现了 IntoIterator
trait 的类型都可以自动把类型集合转换为迭代器,然后通过 for 语法糖进行访问,例如数组:
let arr = [1, 2, 3];
for v in arr {
peintln!("{}", v);
}
也可以使用 IntoIterator
trait 的 into_iter
方法显式地将数组转换成迭代器。
此外,还可以使用 for 循环对数值序列进行迭代,如 for i in 1..100 { … }
。
惰性初始化
在 Rust 中,迭代器是惰性的,也就是在定义迭代器后不使用将不会发生任何事,只有在使用时迭代器才会开始迭代其中的元素。
next 方法
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// 省略其余有默认实现的方法
}
迭代器之所以成为迭代器,就是因为实现了 Iterator
trait,要实现该 trait,最主要的就是要实现其中的 next
方法。for 循环通过不断调用迭代器上的 next
方法来获取迭代器中的元素。
迭代器本身也可以直接调用 next
方法,返回 Option 类型,同时手动迭代必须将迭代器声明为 mut
可变,因为调用 next
会改变迭代器其中的状态数据,使用 for 循环时则无需标注。
let arr = [1, 2, 3, 4, 5];
let mut arr_iter = arr.into_iter();
assert_eq!(arr_iter.next(), Some(1));
IntoIterator trait
不只是 Vec 等常见类型,标准库也为迭代器实现了 into_iter
方法,也就是 arr.into_iter()
与 arr.into_iter().into_iter().into_iter()
是相同的。
into_iter, iter, iter_mut
前面示例中使用 into_iter
方法将数组转化为迭代器,此外还有另外两个:
into_iter
方法会夺走所有权,如let arr_iter = arr.into_iter()
会将 arr 的堆上数据的内存所有权转移至迭代器中,arr 将不能再使用;iter
方法是引用,如let arr_iter = arr.iter()
中的迭代器会使用 arr 元素的引用;iter_mut
方法是可变引用,可以在可变引用的迭代器中改变 arr 元素的值,见下面的代码示例;
命名规律:into_
之类的,都是拿走所有权,_mut
之类的都是可变引用,剩下的就是不可变引用。
let mut arr: Vec<i32> = vec![1,2,3];
let arr_iter = arr.iter_mut(); //使用可变引用的迭代器
for i in arr_iter { //此处 i 的类型为 &mut i32
println!("{}", i);
*i += 1; //更改引用的值
}
println!("{:?}", arr);
需注意的是,.iter()
方法实现的迭代器,调用 next
方法返回的类型是 Some(&T)
,.iter_mut()
方法实现的迭代器,调用 next
方法返回的类型是 Some(&mut T)
。
Iterator 和 IntoIterator 的区别
Iterator
是迭代器 trait,只有实现了它才能称为迭代器,才能调用 next
方法。
IntoIterator
强调的是某一个类型如果实现了该 trait,那么该类型数据可以通过 into_iter
、iter()
或 iter_mut()
方法将其变成一个迭代器。
消费者与适配器
消费者是迭代器上的方法,它会消费掉迭代器中的元素,然后返回该类型的值,这些消费者都有一个共同的特点:在定义中都依赖 next
方法来消费元素。
消费者适配器
只要迭代器上的某个方法 A 在其内部调用了 next
方法,那么 A 就被称为消费性迭代器:因为 next
方法会消耗掉迭代器上的元素,所以 A 的调用也会消耗掉迭代器上的元素。其中一个例子就是 sum
方法,该方法会拿走迭代器的所有权(注意不是拿走原类型数据的所有权,而是生成的迭代器的所有权),然后通过不断调用 next
方法对元素求和。
Iterator中文标准库给出了 Iterator
trait 实现了的各种方法,可以翻阅查看各种方法的使用方法和原理实现。
迭代器适配器
迭代器适配器会返回一个新的迭代器,这是实现链式方法调用的关键:v.iter().map().filter()…
,其中 map
方法就是一个迭代器适配器,其返回的就是一个新的迭代器。
let v1: Vec<i32> = vec![1, 2, 3];
v1.iter().map(|x| x + 1); //警告,因为map方法返回一个迭代器,map 迭代器是惰性的,不产生任何效果
let v2: Vec<> = v1.iter().map(|x| x + 1).collect(); //将v1中的数依次加1然后生成一个新的Vec,其中collect()方法就是一个消费者适配器
collect
使用 collect
方法可以将一个迭代器中的元素收集到指定类型中,上面代码示例中的 Vec<_>
表明将迭代器中的元素收集成 Vec 类型,具体元素类型通过类型推导获得。
map
方法会将迭代器中的每一个值进行一系列操作,然后把该值转换成另外一个新值,该操作通过闭包完成。
let names = ["sunface", "sunfei"];
let ages = [18, 18];
let folks: HashMap<_, _> = names.into_iter().zip(ages.into_iter()).collect();
zip
是一个迭代器适配器,其作用就是将两个迭代器的内容压缩到一起,形成 Iterator<Item=(ValueFromA, ValueFromB)>
这样的新的迭代器,然后通过 collect
方法将迭代器中的(K, V)形式的值收集成 HashMap<K, V>
。
闭包作为适配器参数
之前的 map
方法中使用闭包作为迭代器适配器的参数,其最大的好处不仅在于可以就地实现迭代器中元素的处理,还在于可以捕获环境值。下面的代码示例同时体现了这两个优点:
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() //捕获外部环境中的变量 shoe_size 来与输入参数进行比较
}
实现 Iterator trait
前面使用了数组来创建迭代器,但其实基于其它集合类型一样可以创建迭代器,也可以创建自己的迭代器——只要为自定义类型实现 Iterator trait 即可。 中文标准库中的 Module std::iter 中给出了如何实现迭代器的方法。
实现 Iterator trait 的其他方法
在 Iterator
trait 中,不仅仅是只有 next
一个方法,但我们只需要实现它。因为其他方法都具有https://course.rs/basic/trait/trait.html#%E9%BB%98%E8%AE%A4%E5%AE%9E%E7%8E%B0[默认实现],这些默认实现的方法其实都是基于 next
方法实现的。
enumerate
enumerate
是 Iterator
trait 上的方法,该方法产生一个新的迭代器,其每个元素都是元素(索引,值)。enumerate
是迭代器适配器,可以使用消费者迭代器或 for 循环对新产生的迭代器进行处理。代码示例如下:
let v = vec![1u64, 2, 3, 4, 5, 6];
for (i,v) in v.iter().enumerate() {
println!("第{}个值是{}",i,v)
}
迭代器的性能
根据测试,使用迭代器和 for 循环完成同样的任务,迭代器的运行时间还要少一点。
迭代器是 Rust 的零成本抽象之一,这就意味着抽象并不会引入运行时开销。
总之,迭代器是 Rust 受函数式语言启发而提供的高级语言特性,可以写出更加简洁、逻辑清晰的代码。编译器还可以通过循环展开(Unrolling)、向量化、消除边界检查等优化手段,使得迭代器和 for
循环都有极为高效的执行效率。