• Rust中迭代器的用法
  • 发布于 2个月前
  • 332 热度
    0 评论
迭代器是一种强大的工具,它允许对数据结构进行有效的迭代,并且在许多编程语言中都实现了迭代器。然而,Rust独特的所有权系统在如何实现和使用迭代器方面产生了有趣的差异。在本文中,我们将通过创建和实现最常用的迭代器特征——iterator和IntoIterator,来探索这些差异。

假设你有一个这样的结构体:
pub struct Todos {
    pub list: Vec<Todo>,
}

pub struct Todo {
    pub message: String,
    pub done: bool,
}
如果我们希望遍历这个Vec中的每个Todo,我们可以简单地使用它的list属性并遍历它的元素,但是,如果我们想迭代Todos本身,而不暴露其内部属性,该怎么办呢?

Iterator
在Rust中,与Python等语言类似,迭代器是惰性的。这意味着除非对它们进行迭代(也就是消耗它们),否则它们是无效的。
let numbers = vec![1, 2, 3];
let numbers_iter = numbers.iter();
上面的代码创建了一个迭代器——但没有对它做任何操作。要使用迭代器,我们应该创建一个for循环,如下所示:
let numbers = vec![1, 2, 3];
let numbers_iter = numbers.iter();

for number in numbers {
    println!("{}", number)
}
还有其他方法可以用来创建迭代器。例如,每次在Rust中使用map()时,我们都在创建一个迭代器。

迭代器 vs 可迭代对象
如前所述,vector是一个可迭代对象。这意味着我们可以对它们进行迭代;但更准确地说,这意味着我们可以使用Vec来创建Iterator。例如,Vec<u32>可以生成一个迭代器,但它本身不是迭代器。

创建迭代器
让我们回到Todos结构体看看我们如何创建一种方法来迭代它的元素。Todos有一个字段列表,它是一个Vec<Todo>。在Rust中,Iterator是一个trait,其中包含一些方法,例如next(),它负责获取集合的下一个元素并返回它,或者如果我们已经到达集合的末尾则返回None。它的实现大致如下:
trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}
然后,我们可以创建一种方法来遍历Todos列表字段的元素,编写一个自定义的next()函数。这样做的逻辑很简单——我们可以在伪代码中这样做:
function next() {
    if index < list.len() {
        let todo = Some(list[index])
        self.index += 1
        return todo 
    } else {
        return None
    }
}
在上面的逻辑中,我们检查元素的索引并返回它,前提是它的索引小于列表长度。但我们有个问题。在哪里存储索引?

存储状态
这就是迭代器和可迭代对象之间的区别。翻译上面的伪代码,可以像这样在Todos中实现next()函数,从Iterator trait实现一个方法:
impl<'a> Iterator for Todos {
    type Item = &'a Todo;
    fn next(&mut self) -> Option<Self::Item> {
        if self.index < self.list.len() {
            let result = Some(&self.list[self.index]);
            self.index += 1;
            result
        } else {
            None
        }
    }
}
但这需要我们改变Todos的结构:我们需要在其中添加一个索引(index)字段,以便每次调用next()时使用——也就是说,每次通过迭代器迭代它的元素时使用。
pub struct Todos {
    pub list: Vec<Todo>,
    index: usize,
}
在这个逻辑中,我们将修改构造函数,以便始终创建索引为0的结构体。
pub fn new(list: Vec<Todo>) -> Self {
    Todos { list, index: 0 }
}
然而,这种方法感觉不太对……

在结构体中存储索引字段并不理想,索引用于在迭代器中存储当前状态,因此它应该是迭代器的一部分,而不是结构体的一部分。这就是为什么我们要创建一个迭代器类型——它将存储索引属性——并为该类型实现iterator特性的原因。

TodoIterator
首先,我们需要创建一个迭代器类型:
struct TodosIterator<'a> {
    todos: &'a Todos,
    index: usize,
}
注意这里的生命周期注释,TodosIterator有一个todos字段,它引用了一个Todos。当我们处理引用时,我们需要确保这个字段指向有效的东西——这里就需要生命周期参数。

TodosIterator结构体在此的生命周期是'a,基本上,我们使用这个符号来指定迭代器的todos字段需要具有相同的生命周期。这样,我们就可以确保它不能引用已被删除的Todos结构体。

接下来我们来实现TodosIterator的迭代器:
impl<'a> Iterator for TodosIterator<'a> {
    type Item = &'a Todo;

    fn next(&mut self) -> Option<Self::Item> {
        if self.index < self.todos.list.len() {
            let result = Some(&self.todos.list[self.index]);
            self.index += 1;
            result
        } else {
            None
        }
    }
}
现在我们已经创建了TodoIterator结构体,并为该结构体实现了Iterator特性,我们如何使用它来迭代Todos呢?答案在于Todos结构体的iter()方法,该方法接受Todos的一个引用,并使用它来创建一个迭代器,我们可以使用它来进行迭代!
impl Todos {
    pub fn iter(&self) -> TodosIterator {
        TodosIterator {
            todos: self,
            index: 0,
        }
    }
}
// 堆代码 duidaima.com
// Now we can iterate:
for todo in todos.iter() {
    println!("{}", todo); // each todo is a &Todo, and is immutable
}
IntoIterator
IntoIterator与Iterator特性有点不同,它有一个单一方法into_iter()返回覆盖数据的迭代器。这使得所有实现IntoIterator的类型都可以转换为Iterator。

让我们来理解它的实现:
pub trait IntoIterator {
    type Item;
    type IntoIter: Iterator<Item = Self::Item>;

    fn into_iter(self) -> Self::IntoIter;
}
这里有一些关键的点:
1,Item类型参数是迭代器将生成的元素的类型。
2,IntoIter类型参数是into_iter方法返回的迭代器的类型。这个类型必须实现Iterator trait,并且它的Item的类型必须与IntoIterator的Item的类型相同。
3,into_iter方法接受self作为参数,这意味着它使用原始对象并返回一个遍历其元素的迭代器。

怎么实现呢?你可能认为我们可以重用TodosIterator——然而,我们不能这样做,因为它需要&Todos,而且这里需要获得迭代对象的所有权。那么让我们创建另一个迭代器来完成它:
pub struct TodosIntoIterator {
    todos: Todos
}
TodosIntoIterator和TodosIterator的区别在于,这里我们没有使用引用,而是获取所有权并返回每个元素本身——这就是为什么我们不再需要生命周期注释了。而且也没有索引来保存状态,我们很快就会看到原因。

遵循IntoIterator trait的定义,我们可以为Todos实现它:
impl IntoIterator for Todos {
    type Item = Todo;
    type IntoIter = TodosIntoIterator;

    fn into_iter(self) -> TodosIntoIterator {
        TodosIntoIterator { todos: self }
    }
}
然而,在此之前,我们需要实现TodosIntoIterator的Iterator(还记得类型参数吗?)来描述我们将如何迭代它。
impl Iterator for TodosIntoIterator {
    type Item = Todo;

    fn next(&mut self) -> Option<Self::Item> {
        if self.todos.list.len() == 0 {
            return None;
        }
        let result = self.todos.list.remove(0);
        Some(result)
    }
}
这个实现与我们为TodosIterator所做的略有不同,我们利用了Rust中存在的用于Vecs的remove()方法。该方法移除位置n处的元素并将其返回给我们,并给出其所有权(这对于返回Todo而不是&Todo是必要的)。由于这个方法的工作方式,我们总是可以使用“0”来返回第一个元素,而不是存储一个状态并按顺序增加它。

现在,我们完成了!这两种实现使我们能够以两种不同的方式迭代Todos:
1,引用的方式(&Todos)
for todo in todo_list.iter() {
    println!("{}", todo);// todo is a &Todo
}
2,获取所有权的方式
for todo in todo_list {
    println!("{}", todo); // todo is a Todo
}

用户评论