在 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