下面我們來探討一下循環(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)。下面給出一些定義:
首先讓我們來談?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è)置為 0,x 是 nums 的第一個元素 1。然后將 sum 和 x 相加,即 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)于迭代器和消費者的不同點的完整列表,你可以查閱迭代器模塊文檔。
更多建議: