Rust入門之迭代器(Iterators)
本文已同步本人博客網站
本文相關源碼已上傳Github
前言
迭代器(Iterators)是 Rust 中最核心的工具之一,它不僅是遍歷集合的抽象,更是 Rust 零成本抽象(Zero-Cost Abstractions)和所有權系統完美結合的典范。與其他語言不同,Rust 的迭代器在提供高效遍歷能力的同時,通過編譯器的嚴格檢查,確保內存安全和性能優化,從而避免了其他語言中常見的迭代器失效或越界訪問等問題。本文將跟隨《Rust 程序設計語言》通過剖析迭代器的設計原理、使用方法及底層機制,幫助你掌握這一工具,并寫出更符合 Rust 哲學的代碼。
定義
迭代器模式允許你依次對一個序列中的項執行某些操作。迭代器(iterator)負責遍歷序列中的每一項并確定序列何時結束的邏輯。
在 Rust 中,迭代器是 惰性的(lazy),這意味著在調用消費迭代器的方法之前不會執行任何操作。
fn main() {let v1 = vec![1, 2, 3];// 創建了一個迭代器,這段代碼本身并沒有執行任何有用的操作。let v1_iter = v1.iter();
}
- 惰性求值的優勢:避免不必要的計算開銷,例如處理無限序列或僅在需要時生成元素。
- 實際應用場景:處理大型數據集時,惰性迭代器可以節省內存,只在需要時加載數據。
- 顯式消費的必要性:必須調用消費方法(如
collect()
)才能觸發實際迭代,否則不會執行任何操作。
使用for
關鍵字打印出數組中的元素。
fn main() {let v1 = vec![1, 2, 3];// 創建了一個迭代器,這段代碼本身并沒有執行任何有用的操作。let v1_iter = v1.iter();for val in v1_iter {println!("Got: {}", val);}
}
Iterator Trait 和 next方法
迭代器都實現了一個叫做 Iterator
的定義于標準庫的 trait,源碼如下:
pub trait Iterator {type Item;fn next(&mut self) -> Option<Self::Item>;// 此處省略了方法的默認實現
}
type Item
和 Self::Item
,它們定義了 trait 的 關聯類型。意味著實現 Iterator
trait的時候必須定義一個Item,用于next
函數的返回元素的類型。
next函數
next
是 Iterator
實現者被要求定義的唯一方法:next
方法,該方法每次返回迭代器中的一個項,封裝在 Some
中,并且當迭代完成時,返回 None
。
如果調用next
函數,迭代器變量要聲明為可變的。在迭代器上調用 next
方法會改變迭代器內部的狀態,每次調用next
函數都會消費迭代器。
#[cfg(test)]
mod tests {#[test]fn iterator_demonstration() {let v1 = vec![1, 2, 3];// 迭代器變量要聲明為可變let mut v1_iter = v1.iter();assert_eq!(v1_iter.next(), Some(&1));assert_eq!(v1_iter.next(), Some(&2));assert_eq!(v1_iter.next(), Some(&3));assert_eq!(v1_iter.next(), None);}
}
代碼中調用了四次next
函數,如果有值返回的是由Some包著的引用,如果沒有值了返回None。
根據需要可以獲取不同引用所有權的迭代器:
iter()
創建的是不可變引用的迭代器。into_iter()
能獲得數組的的所有權,并返回具有所有權的值。iter_mut()
創建的是一個可以遍歷到可變引用的迭代器。
方法 | 元素類型 | 所有權 | 原集合后續可用性 |
---|---|---|---|
iter() | &T | 借用 | 是 |
iter_mut() | &mut T | 可變借用 | 是 |
into_iter() | T | 轉移所有權 | 否 |
示例:所有權發生轉移,println!
不能再使用v1
let v1 = vec![1, 2, 3];
let v1_iter = v1.into_iter();
// println!("{:?}", v1); // 編譯錯誤:value borrowed after move
消費迭代器的方法
Iterator
trait 有一系列不同的由標準庫提供默認實現的方法。我們可以在 Iterator
trait 的標準庫 API 文檔中找到所有這些方法。一些方法在其定義中調用了 next
方法,這也就是為什么在實現 Iterator
trait 時要求實現 next
方法的原因。這些調用 next
方法的方法被稱為 消費適配器(consuming adaptors),因為調用它們會消耗迭代器。一個消費適配器的例子是 sum
方法。這個方法獲取迭代器的所有權并反復調用 next
來遍歷迭代器,因而會消費迭代器。在遍歷過程中,它將每個項累加到一個總和中,并在迭代完成時返回這個總和。
點開sum函數源碼,并沒看到調用next方法,這里其實是使用了 Sum trait
,Iterator::sum()
的實現通過將遍歷和累加委托給 Sum trait
,只要能確認迭代器中存放的什么類型就能由對應的trait實現求和。后續這里我專門寫一篇文章探究學習一下。
#[stable(feature = "iter_arith", since = "1.11.0")]fn sum<S>(self) -> SwhereSelf: Sized,S: Sum<Self::Item>,{Sum::sum(self)}
- 標準庫sum方法文檔:https://doc.rust-lang.org/stable/std/iter/trait.Iterator.html#method.sum
使用sum()
對集合當中的元素求和
#[test]fn test_sum() {let v1 = vec![1,2,3];let total: i32 = v1.iter().sum();assert_eq!(total, 6);}
創建迭代器的方法
Iterator
trait 中定義了另一類方法,被稱為 迭代適配器(iterator adaptors),它們不會消耗當前的迭代器,而是通過改變原始迭代器的某些方面來生成不同的迭代器,如 map
方法。
#[test]fn test_map() {let v1 = vec![1,2,3];let v3: Vec<_> = v1.iter().map(|i| i + 1).collect();assert_eq!(v3, vec![2, 3, 4]);}
由于 map
接受一個閉包,因此我們可以指定希望在每個元素上執行的任何操作。這是一個很好的例子,展示了如何通過閉包來自定義某些行為,同時復用 Iterator
trait 提供的迭代行為。
可以鏈式調用多個迭代器適配器來以一種可讀的方式進行復雜的操作。不過因為所有的迭代器都是惰性的,你必須調用一個消費適配器方法,才能從這些迭代器適配器的調用中獲取結果。
消費迭代器的方法叫做消費適配器
創建迭代器的方法 叫做迭代適配器
使用捕獲其環境的閉包
很多迭代器適配器接受閉包作為參數,而我們通常會指定捕獲其環境的閉包作為迭代器適配器的參數。
在這里再介紹一個迭代適配器filter()
,也利用了閉包中的一個特性:通過閉包捕獲定義它的環境中的值(跳轉閉包相關文章)。
我們使用 filter
方法來獲取一個閉包。該閉包從迭代器中獲取一項并返回一個 bool
。如果閉包返回 true
,其值將會包含在 filter
提供的新迭代器中。如果閉包返回 false
,其值不會被包含。
使用 filter
方法和一個捕獲 shoe_size
的閉包,示例代碼如下:
#[derive(PartialEq, Debug)]
struct Shoe {size:i32,style: String,
}fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: i32) -> Vec<Shoe> {// 必須使用into_iter() 獲得數組所有權,因為collect 要求生成一個Vec<Shoe>shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}
#[test]fn filters_by_size() {let shoes = vec![Shoe {size: 10,style: String::from("sneaker"),},Shoe {size: 13,style: String::from("sandal"),},Shoe {size: 10,style: String::from("boot"),},];let in_my_size = shoes_in_size(shoes, 10);assert_eq!(in_my_size,vec![Shoe {size: 10,style: String::from("sneaker")},Shoe {size: 10,style: String::from("boot")},]);}
shoes_in_size
函數獲取一個鞋子 vector 的所有權和一個鞋碼作為參數。它返回一個只包含指定鞋碼的鞋子的 vector。
shoes_in_size
函數體中調用了 into_iter
來創建一個獲取 vector 所有權的迭代器。接著調用 filter
將這個迭代器適配成一個只含有那些閉包返回 true
的元素的新迭代器。
閉包從環境中捕獲了 shoe_size
變量并使用其值與每一只鞋的大小作比較,只保留指定鞋碼的鞋子。最終,調用 collect
將迭代器適配器返回的值收集進一個 vector 并返回。
這個測試展示當調用 shoes_in_size
時,返回的只會是與我們指定的鞋碼相同的鞋子。
在代碼中使用的是into_iter()
,沒有使用iter()
是為什么呢? 這里再分析一下他們的區別:
iter()
與 into_iter()
的核心區別
iter()
:生成一個 不可變引用 的迭代器,元素類型為&T
。原集合保留所有權,后續仍可使用。into_iter()
:生成一個 擁有所有權 的迭代器,元素類型為T
。原集合被消耗(所有權轉移),后續無法再使用。
調用collect()
方法 試圖將引用類型 &Shoe
收集到 Vec<Shoe>
中,但 Vec<Shoe>
只能存儲 Shoe
類型的元素,不能存儲引用,所以需要獲得所有權。使用 into_iter()
是為了將元素的所有權從原集合轉移到新集合中,確保 collect()
可以直接生成 Vec<Shoe>
,避免類型不匹配和克隆開銷。這是 Rust 所有權系統和類型安全性的直接體現。