从for循环遍历数组说起
在c语言时代(这个说法不妥,c语言依然没有被时代抛弃),如果遍历一个数组,我们通常会用for循环,代码类似于这样:
int arr[] = {1, 2, 3, 4, 5};
int i;
for (i = 0; i < 5; i++ ) {
printf("%d\n", arr[i]);
}
c语言的遍历核心是基于索引:起始位置,结束条件,索引变更方式。通过控制索引,找到集合中的数据元素。
在golang中,遍历同样的数组,一般有两种方式:
其一,按索引遍历,这种方式和c语言是一致的:
arr := []int{1, 2, 3, 4, 5}
for i := 0; i < len(arr); i++ {
fmt.Println(arr[i])
}
其二,通range关键字进行“迭代”:
arr := []int{1, 2, 3, 4, 5}
for i, v := range arr {
fmt.Println("Index:", i, "Value:", v)
}
golang中,并没有语言层面的“迭代”这个概念,可以理解为range 是一个语法糖,将一些可以索引化的数据类型(并且由golang 语言层面进行了索引化支持),自动进行索引化转换(设置起始位置,结束条件,索引变更方式),然后可以应用于for循环。
但golang的 for range支持了更丰富的数据类型:数组,切片,映射,通道;从整体上看,golang 的for range呈现了一种迭代化获取数据的方式:
在一个数据集合中,依次获取集合中的数据元素,而不用关注底层数据存储形式、以及数据扫描、获取方式!
rust 则是通过引入迭代器,将上述“迭代化获取数据”的方式作为一种通用的数据遍历方式!
迭代器允许我们迭代化访问一个连续的集合,例如数组、动态数组 Vec
、HashMap
等,以及任何实现了迭代器特性的自定义数据集合;在此过程中,只需关心集合中的元素如何处理,而无需关心如何开始、如何结束、按照什么样的索引去访问等问题。
在rust中,遍历一个数组类似这样:
let vec = vec![1, 2, 3, 4, 5];
for i in vec {
println!("{}", i);
}
看上去,这端代码和golang 的for range 几乎一致(只不过是关键字 换成了in);但实际上它们之间存在本质差别:golang 的for range 本质还是通过索引获取数据(所以可以拿到index),而rust 的for in是通过迭代器的迭代方法(next方法)获取数据!
为了理解这点,我们还得从rust 的 Iterator
特性说起......
Iterator (迭代器)
Iterator
是rust标准库中定义的一个trait
,通常称之为“迭代器”。任何实现了该trait 的结构体,都可以用 for in
的方式进行迭代化数据访问。Iterator trait 中有两个关键的定义,其一是 type Item
,表示该迭代器返回的数据类型;其二是 next
方法,即迭代方法(通过循序调用next方法,遍历集合中的数据):
pub trait Iterator {
/// The type of the elements being iterated over.
type Item;
/// Advances the iterator and returns the next value.
/// Returns None when iteration is finished. Individual iterator implementations may choose to resume iteration, and so calling next() again may
/// or may not eventually start returning Some(Item) again at some point.
fn next(&mut self) -> Option<Self::Item>;
... // 其他对集合操作的函数接口
}
作为示例,我们可以写一个自己的简易迭代器:
struct Revolver {
bullets: u16,
}
impl Revolver {
fn new() -> Self {
Revolver { bullets: 6 }
}
fn reload(&mut self) {
self.bullets = 6;
}
}
impl Iterator for Revolver {
type Item = String;
fn next(&mut self) -> Option<Self::Item> {
if self.bullets > 0 {
self.bullets -= 1;
Some("Bang!".to_string())
} else {
None
}
}
}
#[test]
fn fire_all_bullets() {
let revolver = Revolver::new();
for fired in revolver {
println!("{}", fired);
}
}
见代码片段,我们实现了一把左轮手枪,默认填6发子弹; 用for in 测试连续射击(fire_all_bullets),执行效果:
事实上,fire_all_bullets还有一个更直白的写法:
#[test]
fn fire_all_bullets() {
let mut revolver = Revolver::new();
loop {
// 扣动扳机
let fired = revolver.next();
match fired {
Some(bullet) => {
// 子弹发射
println!("{}", bullet)
}
// 没有子弹了,射击结束
None => break,
}
}
}
上述代码和用for in 实现效果一样:Bang!Bang!Bang!Bang!Bang!Bang!
注意:当使用next()
手动遍历时,迭代器变量要设置为可变,这是因为迭代过程中会修改迭代器内部的数据!
事实上,loop展开的迭代器调用模式,才是迭代器真实执行的模型;for in 可以看做是编译器的语法糖,在编译期间将for in 迭代器展开为上述loop 的模型;只不过处理过程中,还会涉及一些数据克隆,引用转换,可变引用转换等,以方便使用;前一个测试中,for in 的revolver 没有加mut 也可以执行,这是因为for in 展开时,自己做了一次转换!
IntoIterator
第一章有个举例,for in 遍历数组:
let arr = vec![1, 2, 3, 4, 5];
for i in arr {
println!("{}", i);
}
按第二章的理解,for in 可以 直接作用于Vec,那么Vec应该实现了 Iterator?即Vec本身也是迭代器?
查看Vec实现并未发行Vec 实现了Iterator,相反它实现了IntoIterator
这个 trait。
功能如其名,IntoIterator
这个特性就是将实现了这个trait的数据转化为一个迭代器。
pub trait IntoIterator {
/// The type of the elements being iterated over.
type Item;
/// Which kind of iterator are we turning this into?
type IntoIter: Iterator<Item = Self::Item>;
/// Creates an iterator from a value.
fn into_iter(self) -> Self::IntoIter;
}
该trait主要包括三个成员: Item指定迭代器返回的元素;IntoIter 指定迭代器类型; into_iter实现由当前数据类型转化为指定迭代器的方法!
下面,我们用一个实例来演示一下IntoIterator 及Iterator 的关系和使用。
实战
考虑这样一个需求:对一个字符串,以空格为分割的方式,遍历获取每个单词。
直观的,要实现对一个字符串按空格进行逐个单词的迭代分割,我们需要引入一个位置标记(position),记录当前已经完成了单词分割的位置;当下一次请求单词时,从剩余的内容中找到下一个空格(或者直到内容末尾),将这部分内容作为分割的单词返回;同时,更新位置标记到当前返回的位置;最后,当位置标记到达原始字符串末尾时,处理完成,后续请求返回为None。
按照上面的思路,给出编码如下:
首先,自定义字符串数据结构:
// 定义MyString结构体
struct MyString {
content: String,
}
通常,我们需要对现有数据进行额外处理时,为了不破坏原有的数据结构和实现,通过wapper包装是一个不错的选择。
其次,定义MyString的迭代器数据结构(根据上面的分析,需要一个位置标记来记录处理过程):
// 定义MyStringIterator结构体
struct MyStringIterator {
content: String,
position: usize,
}
第三,为MyStringIterator 实现Iterator特性,使之成为迭代器:
impl Iterator for MyStringIterator {
type Item = String;
fn next(&mut self) -> Option<Self::Item> {
let refx = self.content.as_str();
let bytes = refx.as_bytes();
while self.position < refx.len() && bytes[self.position] == b' ' {
self.position += 1;
}
if self.position < refx.len() {
let rest = &refx[self.position..];
let idx = rest.find(' ').unwrap_or(rest.len());
let result = rest[0..idx].to_string();
self.position += idx;
Some(result)
} else {
None
}
}
}
这里,我们指定返回迭代的元素为String,即每个单词以String类型返回;next实现中,会自动跳过连续的空格,通过 &str.find接口找到下一个空格的位置(如果找不到,则直到末尾),并进行字符切片截取,再转化为Some(String)返回。
第四,为MyString 实现IntoIterator 特性,使其可以转换为迭代器,方便对内容进行迭代处理:
impl IntoIterator for MyString {
type Item = String;
type IntoIter = MyStringIterator;
fn into_iter(self) -> Self::IntoIter {
MyStringIterator {
content: self.content,
position: 0,
}
}
}
需要注意的是,实现IntoIterator时,需要指定元素类型和迭代器类型,并且所指定的元素类型和迭代器要求的元素类型相匹配。
into_inter中,接受的self 是MyString的实例,并且获得其所有权;同时,将MyString中content变量绑定到 迭代器的content上;初始化位置为0.
第五, 测试应用
#[test]
fn test_my_string_iterator() {
let my_string = MyString {
content: "My name is LiLei".to_string(),
};
let mut iter = my_string.into_iter();
assert_eq!("My", iter.next().unwrap());
assert_eq!("name", iter.next().unwrap());
assert_eq!("is", iter.next().unwrap());
assert_eq!("LiLei", iter.next().unwrap());
let nstr = MyString {
content: "What's your name?".to_string(),
};
for word in nstr {
println!("{}", word);
}
}
执行效果:
bingo, 测试通过。手动迭代assert,for 循环打印符合预期按空格分割的单词进行迭代输出。
再说for in Vec
前面说到了 Vec并没有实现Iterator,那么Vec在for in 结构里是怎么执行的呢?
前面也说到,for in 结构在rust 中是一种语法糖,它自己会进行类型推导:for in 结构后变量是迭代器时,对迭代器进行循环展开;当for in 结构后的变量非迭代器时,编译器会推断其必须可以进行迭代器转换,即它必须有一个 IntoIterator 实现,并尝试调用其into_iter方法获取迭代器(如果没有实现则会报错)。
所以,开始示例的for in 直接作用于数组:
let arr = vec![1, 2, 3, 4, 5];
for i in arr {
println!("{}", i);
}
等效于:
let arr = vec![1, 2, 3, 4, 5];
for i in arr.into_iter {
println!("{}", i);
}
值得注意的是:
into_iter 进行迭代器转换时,会移走原始数据的所有权,导致原始数据不再可用!
如下测试代码:
#[test]
fn test_iter_vec() {
let values = vec![1, 2, 3];
for v in values {
println!("{}", v)
}
println!("{:?}", values);
}
运行代码会报错:
错误提示说得明白,values
在再调用into_iter()
时被移交了所有权!
iter, iter_mut
在实际开发过程中,迭代器在数据处理上提供了很大的便利性;但是into_iter会拿走所有权会限制该模式在很多场景的使用。因此,很多数据类型在实现原始数据到迭代器的转化时,还提供了其他的转化方式。比如Vec还提供了 iter()
返回一个以不可变借用的方式遍历原数据的迭代器;iter_mut()
返回一个以可变借用的方式遍历原数据的迭代器。
以下是into_iter()
,iter()
, iter_mut()
的一个对比测试, 大家可以实际跑一下,理解输出:
#[test]
fn test_iter_vec() {
let values = vec![1, 2, 3];
for v in values.into_iter() {
println!("{}", v)
}
// 下面的代码将报错,因为 values 的所有权在上面 `for` 循环中已经被转移走
// println!("{:?}",values);
let values = vec![1, 2, 3];
for v in values.iter() {
println!("{}", v)
}
// 不会报错,因为 values_iter 只是借用了 values 中的元素
println!("{:?}", values);
let mut values = vec![1, 2, 3];
for v in values.iter_mut() {
*v = *v * 2;
}
// 输出[0, 4, 6]
println!("{:?}", values);
}
注意:iter,iter_mut 不是 IntoIterator 的标准接口,用在for循环中时,必须显示调用!
当然,你也可以定义自己的转化函数,定义对原始数据的处理方式;但为了增加代码可读性,强烈建议遵循rust社区的一些通用编程实践!
最后
迭代器 Iterator中还有很多实用接口,比如map(),filter(), zip(), count()等等。熟练掌握这些接口的功能和使用方法,能够让你在数据处理时更加游刃有余。这部分内容,就留给大家自行学习补充吧。