在 Rust 中實現 Iterator 和 IntoIterator 特徵

迭代器是一種強大的工具,它允許對數據結構進行有效的迭代,並且在許多編程語言中都實現了迭代器。然而,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 可以生成一個迭代器,但它本身不是迭代器。

創建迭代器

讓我們回到 Todos 結構體看看我們如何創建一種方法來迭代它的元素。Todos 有一個字段列表,它是一個 Vec

在 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,
        }
    }
}

// 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
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ISm4aPoQMZYOA3jrOb2Daw