迭代器

從for循環講起

我們在控制語句裡學習了Rust的for循環表達式,我們知道,Rust的for循環實際上和C語言的循環語句是不同的。這是為什麼呢?因為,for循環不過是Rust編譯器提供的語法糖!

首先,我們知道Rust有一個for循環能夠依次對迭代器的任意元素進行訪問,即:

for i in 1..10 {
    println!("{}", i);
}

這裡我們知道, (1..10) 其本身是一個迭代器,我們能對這個迭代器調用 .next() 方法,因此,for循環就能完整的遍歷一個循環。 而對於Vec來說:

let values = vec![1,2,3];
for x in values {
    println!("{}", x);
}

在上面的代碼中,我們並沒有顯式地將一個Vec轉換成一個迭代器,那麼它是如何工作的呢?現在就打開標準庫翻api的同學可能發現了,Vec本身並沒有實現 Iterator ,也就是說,你無法對Vec本身調用 .next() 方法。但是,我們在搜索的時候,發現了Vec實現了 IntoIterator 的 trait。

其實,for循環真正循環的,並不是一個迭代器(Iterator),真正在這個語法糖裡起作用的,是 IntoIterator 這個 trait。

因此,上面的代碼可以被展開成如下的等效代碼(只是示意,不保證編譯成功):

let values = vec![1, 2, 3];

{
    let result = match IntoIterator::into_iter(values) {
        mut iter => loop {
            match iter.next() {
                Some(x) => { println!("{}", x); },
                None => break,
            }
        },
    };
    result
}

在這個代碼裡,我們首先對Vec調用 into_iter 來判斷其是否能被轉換成一個迭代器,如果能,則進行迭代。

那麼,迭代器自己怎麼辦?

為此,Rust在標準庫裡提供了一個實現:

impl<I: Iterator> IntoIterator for I {
    // ...
}

也就是說,Rust為所有的迭代器默認的實現了 IntoIterator,這個實現很簡單,就是每次返回自己就好了。

也就是說:

任意一個 Iterator 都可以被用在 for 循環上!

無限迭代器

Rust支持通過省略高位的形式生成一個無限長度的自增序列,即:

let inf_seq = (1..).into_iter();

不過不用擔心這個無限增長的序列撐爆你的內存,佔用你的CPU,因為適配器的惰性的特性,它本身是安全的,除非你對這個序列進行collect或者fold! 不過,我想聰明如你,不會犯這種錯誤吧! 因此,想要應用這個,你需要用take或者take_while來截斷他,必須? 除非你將它當作一個生成器。當然了,那就是另外一個故事了。

消費者與適配器

說完了for循環,我們大致弄清楚了 IteratorIntoIterator 之間的關係。下面我們來說一說消費者和適配器。

消費者是迭代器上一種特殊的操作,其主要作用就是將迭代器轉換成其他類型的值,而非另一個迭代器。

而適配器,則是對迭代器進行遍歷,並且其生成的結果是另一個迭代器,可以被鏈式調用直接調用下去。

由上面的推論我們可以得出: 迭代器其實也是一種適配器!

消費者

就像所有人都熟知的生產者消費者模型,迭代器負責生產,而消費者則負責將生產出來的東西最終做一個轉化。一個典型的消費者就是collect。前面我們寫過collect的相關操作,它負責將迭代器裡面的所有數據取出,例如下面的操作:

let v = (1..20).collect(); //編譯通不過的!

嘗試運行上面的代碼,卻發現編譯器並不讓你通過。因為你沒指定類型!指定什麼類型呢?原來collect只知道將迭代器收集到一個實現了 FromIterator 的類型中去,但是,事實上實現這個 trait 的類型有很多(Vec, HashMap等),因此,collect沒有一個上下文來判斷應該將v按照什麼樣的方式收集!!

要解決這個問題,我們有兩種解決辦法:

  1. 顯式地標明v的類型:

     let v: Vec<_> = (1..20).collect();
    
  2. 顯式地指定collect調用時的類型:

     let v = (1..20).collect::<Vec<_>>();
    

當然,一個迭代器中還存在其他的消費者,比如取第幾個值所用的 .nth()函數,還有用來查找值的 .find() 函數,調用下一個值的next()函數等等,這裡限於篇幅我們不能一一介紹。所以,下面我們只介紹另一個比較常用的消費者—— fold

當然了,提起Rust裡的名字你可能沒啥感覺,其實,fold函數,正是大名鼎鼎的 MapReduce 中的 Reduce 函數(稍微有點區別就是這個Reduce是帶初始值的)。

fold函數的形式如下:

fold(base, |accumulator, element| .. )

我們可以寫成如下例子:

let m = (1..20).fold(1u64, |mul, x| mul*x);

需要注意的是,fold的輸出結果的類型,最終是和base的類型是一致的(如果base的類型沒指定,那麼可以根據前面m的類型進行反推,除非m的類型也未指定),也就是說,一旦我們將上面代碼中的base1u64 改成 1,那麼這行代碼最終將會因為數據溢出而崩潰!

適配器

我們所熟知的生產消費的模型裡,生產者所生產的東西不一定都會被消費者買賬,因此,需要對原有的產品進行再組裝。這個再組裝的過程,就是適配器。因為適配器返回的是一個新的迭代器,所以可以直接用鏈式請求一直寫下去。

前面提到了 Reduce 函數,那麼自然不得不提一下另一個配套函數 —— map :

熟悉Python語言的同學肯定知道,Python裡內置了一個map函數,可以將一個迭代器的值進行變換,成為另一種。Rust中的map函數實際上也是起的同樣的作用,甚至連調用方法也驚人的相似!

(1..20).map(|x| x+1);

上面的代碼展示了一個“迭代器所有元素的自加一”操作,但是,如果你嘗試編譯這段代碼,編譯器會給你提示:

warning: unused result which must be used: iterator adaptors are lazy and
         do nothing unless consumed, #[warn(unused_must_use)] on by default
(1..20).map(|x| x + 1);
 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

呀,這是啥?

因為,所有的適配器,都是惰性求值的!

也就是說,除非你調用一個消費者,不然,你的操作,永遠也不會被調用到!

現在,我們知道了map,那麼熟悉Python的人又說了,是不是還有filter!?答,有……用法類似,filter接受一個閉包函數,返回一個布爾值,返回true的時候表示保留,false丟棄。

let v: Vec<_> = (1..20).filter(|x| x%2 == 0).collect();

以上代碼表示篩選出所有的偶數。

其他

上文中我們瞭解了迭代器、適配器、消費者的基本概念。下面將以例子來介紹Rust中的其他的適配器和消費者。

skip和take

take(n)的作用是取前n個元素,而skip(n)正好相反,跳過前n個元素。

let v = vec![1, 2, 3, 4, 5, 6];
let v_take = v.iter()
    .cloned()
    .take(2)
    .collect::<Vec<_>>();
assert_eq!(v_take, vec![1, 2]);

let v_skip: Vec<_> = v.iter()
    .cloned()
    .skip(2)
    .collect();
assert_eq!(v_skip, vec![3, 4, 5, 6]);

zip 和 enumerate的恩怨情仇

zip是一個適配器,他的作用就是將兩個迭代器的內容壓縮到一起,形成 Iterator<Item=(ValueFromA, ValueFromB)> 這樣的新的迭代器;

let names = vec!["WaySLOG", "Mike", "Elton"];
let scores = vec![60, 80, 100];
let score_map: HashMap<_, _> = names.iter()
    .zip(scores.iter())
    .collect();
println!("{:?}", score_map);

enumerate, 熟悉的Python的同學又叫了:Python裡也有!對的,作用也是一樣的,就是把迭代器的下標顯示出來,即:

let v = vec![1u64, 2, 3, 4, 5, 6];
let val = v.iter()
    .enumerate()
    // 迭代生成標,並且每兩個元素剔除一個
    .filter(|&(idx, _)| idx % 2 == 0)
    // 將下標去除,如果調用unzip獲得最後結果的話,可以調用下面這句,終止鏈式調用
    // .unzip::<_,_, vec<_>, vec<_>>().1
    .map(|(idx, val)| val)
    // 累加 1+3+5 = 9
    .fold(0u64, |sum, acm| sum + acm);

println!("{}", val);

一系列查找函數

Rust的迭代器有一系列的查找函數,比如:

  • find(): 傳入一個閉包函數,從開頭到結尾依次查找能令這個閉包返回true的第一個元素,返回Option<Item>
  • position(): 類似find函數,不過這次輸出的是Option<usize>,第幾個元素。
  • all(): 傳入一個函數,如果對於任意一個元素,調用這個函數返回false,則整個表達式返回false,否則返回true
  • any(): 類似all(),不過這次是任何一個返回true,則整個表達式返回true,否則false
  • max()min(): 查找整個迭代器裡所有元素,返回最大或最小值的元素。注意:因為第七章講過的PartialOrder的原因,maxmin作用在浮點數上會有不符合預期的結果。

以上,為常用的一些迭代器和適配器及其用法,僅作科普,對於這一章。我希望大家能夠多練習去理解,而不是死記硬背。

好吧,留個習題:

習題

利用迭代器生成一個升序的長度為10的水仙花數序列,然後對這個序列進行逆序,並輸出

results matching ""

    No results matching ""