DAY 21 迭代器

迭代器

迭代器我找不到一個好的解釋,所以請看完本文後自行領悟吧。

迭代器的關鍵字 yield,只有在傳回值為 IEnumerator 或者 IEnumerable 類型的方法才可以使用,傳回時是以 yield return 而不能只是 return 單一關鍵字,且方法不能夠使用 ref 或 out 參數。如果要停止迭代的話可以使用 yield break。

然而各位還記得 DAY 05 的 foreach 吧,foreach 的逐一查看陣列、集合中的元素就是透過今天要介紹的迭代器。

foreach 有些集合類別能用,有些不能使用,差別在於有無實作 IEnumerator 或者 IEnumerable 這兩個介面,雖然系列文講到現在的內容,是不會發生這種問題。

我們來簡單的示範什麼是迭代器,所先建立一個傳回 IEnumerable 的方法,並且使用這個方法

1
2
3
4
5
6
7
8
9
10
11
12
static void Main (string[] args) {
var strs = GetStrings ();
foreach (var str in strs)
Console.WriteLine (str);
}
static IEnumerable<string> GetStrings () {
yield return "Mio";
yield return "Miffy";
yield return "Lulu";
yield break;
yield return "NekoSan";
}

可以下中斷點來看一下程式的執行過程,這樣比較好了解迭代器做了什麼,你可以看到程式執行過程,不斷的跳來跳去。

迭代器主要是實現 IEnumerator 介面來執行,成員為 Current 與 MoveNext (),你可以想像成一個集合內,Current 代表目前元素,MoveNext () 為檢查有無下個元素。當程式呼叫迭代器時,他會先檢查 MoveNext (),如果有則移動 Current,直到沒有下一個元素。

但上述方法只是簡單的示範 yield 運作模式,無實作 IEnumerator 介面,你可以先當作 yield return 為傳回一個 Current,如果有下一句 yield return 則 MoveNext () 為 true,然而碰到 yield break 則結束迭代。

除此之外也可以看到,strs 在傳值給 str 時,才會開始迭代,因為迭代器有延遲執行的特性,所以在資料處理方面,與一般的情況不太一樣。以往處理資料時,是整理好一大筆符合資料後才傳回,但迭代器則是出現一筆符合資料的就傳回,直到該集合逐一檢查完成。

再來我們以 DAY 18 的 Node 來進行改造,讓他可以使用 foreach,但在此之前要先說一聲抱歉,我那天寫的時候可能神智不清楚,所以在操作 Node 時的範例不是很洽當,但還是可以當作泛型的介紹,所以我們重新來建立新的 Node

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Program {
static void Main (string[] args) {
Node<int> nodes = new Node<int> (2);
nodes.Next = new Node<int> (6);
}
}
class Node<T> {
public Node () {
Value = default (T);
Next = null;
}
public Node (T value) {
Value = value;
Next = null;
}
public T Value;
public Node<T> Next;
}

有發現差異點吧,沒有的同學就當沒事發生。

當今天要去逐一查看 Node 的內容時,我們想要重頭開始列出每一個 Node 的 value 直到 Next == null,所以我們來實作 IEnumerable 介面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Program {
static void Main (string[] args) {
Node<int> nodes = new Node<int> (2);
nodes.Next = new Node<int> (6);
foreach (var n in nodes) {
Console.WriteLine (n);
}
}
}
class Node<T> : IEnumerable<T> {
public Node () {
Value = default (T);
Next = null;
}
public Node (T value) {
Value = value;
Next = null;
}
public T Value;
public Node<T> Next;
public IEnumerator<T> GetEnumerator () {
var temp = this;
while (temp != null) {
yield return temp.Value;
temp = temp.Next;
}
}
IEnumerator IEnumerable.GetEnumerator () => GetEnumerator ();
}

簡單的設計一下,就可以看到 foreach 能夠正確的印出 Node 裡每一個 value 值。

既然做了那我們來優化一下,提供一個可以知道該 Node 包含的 Node 數的 Length,並加上索引存取運算子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public int Length {
get {
var result = 1;
var current = this;
while ((current = current.Next) != null)
result++;
return result;
}
}
public T this [int index] {
get {
if (index > 0 && this.Next != null)
return this.Next[index - 1];
return Value;
}
}

可以自行使用看看,這邊就不用在教導如何使用了吧。

一些小結論

迭代器可以實現延遲執行,可以參考文末連結,或者自己再去找找資源補充一下,我敘述的可能有點混亂,好處也就是資料的傳回方式不一樣,可能還有其他的,但我不知道而已,可能這樣寫看起來很潮也是好處之一。

在最後補上的兩個方法,想說介紹一下存取子的用法,已經寫到不知道有沒有寫過了,大致上再講一遍,如果講過就當複習吧。

存取子可以控制屬性成員的讀寫運算機制。

get 為讀取,如果只有 get 如上述範例,則可以表示該屬性唯獨,並可以控制它讀取時傳回的內容。

set 為寫入,如果只有 set 則表示該屬性唯寫,如寫入一個數字時永遠加二這個邏輯,但個人是沒有碰過唯寫屬性的例子,如果使用一般會跟著 get 一起。

get & set 為可讀可寫,可以控制讀取的傳回值,與設定時的邏輯。正常什麼都不做的情況預設就是 get & set,但讀寫時無任何規則,單純的操作屬性而已。

感謝閱讀。

參考連結
[C#: yield return] #1. How It Work ? — 安德魯的部落格