迭代器

2018-08-12 22:03 更新

迭代器

下面我們來探討一下循環(huán)問題。

還記得 Rust 的 for 循環(huán)嗎?下面有一個例子:

for x in 0..10 {
    println!("{}", x);
}

現(xiàn)在你已經(jīng)知道了更多的 Rust,我們可以詳細談?wù)勊侨绾喂ぷ鞯摹7秶?(0 . . 10) 是一個迭代器。我們可以使用 .next() 方法反復(fù)調(diào)用迭代器,它給出了事情的一個序列。

如下所示:

let mut range = 0..10;

loop {
    match range.next() {
        Some(x) => {
            println!("{}", x);
         },
        None => { break }
    }
}

我們針對范圍給出一個可變的綁定,這就是迭代器。然后用一個內(nèi)在的 match 進行 loop 。用這個 match 操作 range.next() 的結(jié)果,這就給出了到迭代器的下一個值的一個引用。next 返回一個 Option,在這種情況下,一旦循環(huán)運行完畢,我們會得到一個值: Some(i32) 或者 None。如果我們得到 Some(i32),就打印出來,如果我們得到 None,就跳出循環(huán)。

這個代碼示例和我們的 for 循環(huán)版本基本上是一樣的。for 循環(huán)僅僅是編寫loop/match/break 構(gòu)造的一個方便的方式。

然而,for 循環(huán)不是唯一使用迭代器的情況。編寫自己的迭代器包括實現(xiàn)迭代器的特征。雖然這種操作不是在本指南的范圍之內(nèi),Rust 提供了許多有用的迭代器來完成各種任務(wù)。在我們談?wù)撨@些之前,我們應(yīng)該談?wù)撘幌?Rust 反模式。這就是范圍的使用方式。

是的,我們剛剛談到范圍很有用。但范圍也很原始。例如,如果你需要遍歷一個 vector 的內(nèi)容,你可能會這樣寫:

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

for num in &nums {
    println!("{}", num);
}

這比使用一個真正的迭代器嚴(yán)格的多。你可以直接對 vector 進行迭代,像下面寫的這樣:

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

for num in &nums {
    println!("{}", num);
}

這樣做有兩個原因。首先,這可以更直接的表達我們的意思。我們遍歷整個 vector,而不是遍歷索引,然后通過索引訪問 vector。第二,這個版本是更高效的:第一個版本將有額外的邊界檢查,因為它使用了索引、nums[i]。但是因為我們使用迭代器依次針對 vector 的每個元素產(chǎn)生一個引用,在第二個示例中不涉及邊界檢查。這對于迭代器是很常見的:我們可以忽略不必要的檢查范圍,但仍知道我們是安全的。

關(guān)于 println! 怎樣工作,這里還有一個細節(jié)不是 100% 的清楚。num 實際上是 &i32 類型的數(shù)字。也就是說,它是對 i32 的一個引用,而不是本身就是 i32,println! 為我們處理非關(guān)聯(lián)化的事物,所以我們不能看到它的細節(jié)。下面這段代碼同樣工作正常:

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

for num in &nums {
    println!("{}", *num);
}

現(xiàn)在我們來明確非關(guān)聯(lián)化 num。為什么 &num 可以給我們引用?首先,因為我們明確地使用 & 調(diào)用。其次,如果給我們數(shù)據(jù)本身,我們必須是它的擁有者,它將生成一個數(shù)據(jù)的副本,并將副本給我們。與引用相比,我們只是借用了引用數(shù)據(jù),所以它只是傳遞一個引用,而不需要做移動。

所以,既然我們已經(jīng)認(rèn)定范圍往往不是你想要的,讓我們來談?wù)勀阆胍臇|西。

這里主要有3個類,它們是彼此相關(guān)的事物:迭代器(iterators),迭代器適配器(iterator adapters),和消費者(consumers)。下面給出一些定義:

  • 迭代器給出序列值。
  • 迭代器適配器使用一個迭代器,產(chǎn)生一個新的迭代器,它擁有不同的輸出序列。
  • 消費者使用一個迭代器,產(chǎn)生一些值最后的設(shè)置。

首先讓我們來談?wù)勏M者,因為你已經(jīng)看到一個迭代器。

消費者

consumer 操作一個迭代器,返回某些類型的值。最常見的 consumer 是 collect()。下面的代碼并沒有完全編譯,但它已經(jīng)顯示了意圖:

let one_to_one_hundred = (1..101).collect();

正如你所看到的,我們可以在我們的迭代器上調(diào)用 collect()。collect() 盡可能多地接收迭代器給它的值,并返回結(jié)果的集合。為什么這不能編譯呢?Rust 不能確定你想收集什么類型的值,所以你需要讓它知道。下邊是編譯的版本:

let one_to_one_hundred = (1..101).collect::<Vec<i32>>();

如果你還記得,:: 語法允許我們給出一個類型提示,所以我們可以告訴它,我們需要一個整數(shù)向量。盡管你并不總是需要使用整個類型。使用一個 _ 可以允許你提供部分提示:

let one_to_one_hundred = (1..101).collect::<Vec<_>>();

即:“請收集 Vec,但為我推斷出T是什么?!币驗檫@個原因 _ 有時被稱為一種“占位符”。

collect() 是最常見的 consumer,但也存在其他的消費者。find()就是其中之一:

let greater_than_forty_two = (0..100)
                            .find(|x| *x > 42);

match greater_than_forty_two {
    Some(_) => println!("We got some numbers!"),
    None => println!("No numbers found :("),
}

find 消耗一個閉包,針對迭代器的每個元素的引用操作。如果元素是我們要找的元素,這個閉包返回 true,否則返回 false。因為我們可能找不到一個匹配的元素,find 會返回一個 Option 而不是元素本身。

另一個重要的消費者是 fold。下面就是 fold 的示例:

let sum = (1..4).fold(0, |sum, x| sum + x);

fold() 是一個消費者,語法:fold(base, |accumulator, element| ...)。它需要兩個參數(shù):第一個是一個稱為 的元素 。第二個是一個閉包,本身有兩個參數(shù):第一個被稱為累加器,第二個是一個元素。在每次迭代中,調(diào)用閉包,結(jié)果是在下一次迭代中累加器的值。在第一次迭代中,base 是累加器的值。

好吧,這有點令人困惑。讓我們看看在這個迭代器中所有事物的值:

累加器 元素 閉包結(jié)果
0 0 1 1
0 1 2 3
0 3 3 6

我們使用這些參數(shù)來調(diào)用 fold() 函數(shù):

.fold(0, |sum, x| sum + x);

所以,0 是基,sum 是累加器,x 是我們的元素。在第一次迭代中,我們將 sum 設(shè)置為 0xnums 的第一個元素 1。然后將 sumx 相加,即 0 + 1 = 1。第二次迭代中,和值成為我們的累加器 sum,元素是數(shù)組的第二個元素 2,相加,即 1 + 2 = 3 ,這樣就得到了最后一次迭代的累加器的值。在這次迭代中,x 是最后一個元素 3 ,相加,即 3 + 3 = 6,這就是求和最后的結(jié)果。1 + 2 + 3 = 6,這就是我們最后得到的結(jié)果。

對于 fold 如果你剛開始接觸它,可能會覺得這種語法有點奇怪,但一旦開始使用它,您會發(fā)現(xiàn)它的使用范圍很廣,幾乎到處都可以使用。任何時候,如果你有一系列的事物,而你想要一個單一的結(jié)果,fold 都是最適當(dāng)?shù)摹?/p>

由于迭代器存在另一個我們還沒有談到屬性:懶惰,消費者就變得尤為重要。讓我們多談?wù)撘恍╆P(guān)于迭代器的問題,你就會明白消費者為什么如此重要。

迭代器

正如之前說過的,我們可以使用 .next() 方法反復(fù)調(diào)用一個迭代器,它給出了一個事情的序列。因為你需要調(diào)用這個方法,這意味著迭代器可以偷懶,而不是預(yù)先生成的所有值。例如,在這段代碼中,實際上并沒有生成 1-100的數(shù)字,它僅僅代表了一個序列,而不是產(chǎn)生一個值:

let nums = 1..100;

因為我們沒有針對范圍做任何事情,它不會生成序列。下面讓我們加入消費者:

let nums = (1..100).collect::<Vec<i32>>();

消費者 collect() 要求范圍給它一些數(shù)字,這樣它才會做生成序列的工作。

您將看到范圍是兩個基本的迭代器之一。另一種是 iter()。iter() 可以把一個 vector 變成一個簡單的迭代器,反過來這個迭代器給出每個元素:

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

for num in nums.iter() {
    println!("{}", num);
}

這兩種基本迭代器應(yīng)該能很好地為你服務(wù)。有一些更高級的迭代器,包括那些不限范圍的。

這里談?wù)摰降牡饕呀?jīng)足夠你使用。迭代器適配器是我們需要談?wù)摰年P(guān)于迭代器最后的概念。

迭代器適配器

迭代器適配器獲得一個迭代器,以某種方式對其進行修改,產(chǎn)生一個新的迭代器。最簡單的一個叫做 map

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

map 被另一個迭代器調(diào)用,產(chǎn)生一個新的迭代器,在新的迭代器中,每個元素引用迭代器給出的關(guān)閉作為調(diào)用它的參數(shù)。這將打印出 2-100 的數(shù)字。如果你編譯這個示例,您會得到一個警告:

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

迭代器的懶惰又起作用了!這個閉包永遠不會執(zhí)行。這個例子也不會打印任何數(shù)字:

(1..100).map(|x| println!("{}", x));

如果你只是為了測試其副作用,而在一個迭代器上執(zhí)行一個閉包,那只需要使用 for 就好了。

有很多有趣的迭代器適配器。take(n) 將在原迭代器的 n 個元素的基礎(chǔ)上返回一個新的迭代器。注意,這個對于原迭代器沒有副作用。讓我們使用一下之前提到的無限迭代器:

for i in (1..).step_by(5).take(5) {
    println!("{}", i);
}

這將打印出

1
6
11
16
21

filter() 是一個以一個閉包作為參數(shù)的適配器。這個閉包返回 true 或者 false。新的迭代器 filter() 產(chǎn)生唯一的元素,閉包返回true:

for i in (1..100).filter(|&x| x % 2 == 0) {
    println!("{}", i);
}

這將打印 1 到 100 之間的所有的偶數(shù)。(注意:因為 filter 不消耗將遍歷的元素,它只是傳遞每一個元素的引用,從而可以使用 &x 模式過濾謂詞來提取整數(shù)本身。)

現(xiàn)在你可以將三件事放在一起考慮:首先是一個迭代器,經(jīng)過幾次調(diào)整,然后消耗這個結(jié)果。檢查一下:

(1..1000)
    .filter(|&x| x % 2 == 0)
    .filter(|&x| x % 3 == 0)
    .take(5)
    .collect::<Vec<i32>>();

這將給你一個包含 6、12、18、24 和 30 的向量。

這只是一個關(guān)于迭代器,迭代器適配器,和消費者的小的嘗試。有很多非常有用的迭代器,您也可以編寫您自己的迭代器。迭代器提供一個安全、有效的方式來操作各種列表。起初你會覺得它們有點不同尋常,但一旦你開始使用它們,你就會迷上它們。關(guān)于迭代器和消費者的不同點的完整列表,你可以查閱迭代器模塊文檔。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號