[.NET]快快樂樂學LINQ系列前哨戰-IEnumerable, IEnumerator, yield, Iterator
前言
LINQ的基礎,牽扯到幾個長的很像的東西,例如IEnumerable, Enumerable, IEnumerator, 還加上對應的泛型介面IEnumerable<T>, IEnumerator<T>,還有IEnumerable裡面的方法GetEnumerator()會回傳IEnumerator,IEnumerable<T>裡面的方法GetEnumerator()則會回傳IEnumerator<T>。
這幾個東西到底是在搞什麼鬼,在這篇文章以及下篇文章,希望透過文章的介紹,可以讓讀者更瞭解它們之間的關係。
關係圖
先用一張簡單的圖來說明foreach, IEnumerable, IEnumerable<T>, IEnumerator與IEnumerator<T>的關係。
接下來將先逐一說明foreach的相關原理。
說明
有寫過程式的朋友,幾乎一定用過foreach迴圈來針對某一個物件集合,進行逐一巡覽的動作,而且覺得這樣的程式再自然不過了。但是,其實foreach幫忙簡化了很多的動作,我們來看透过IL看C# (3)-foreach语句這篇文章所提到的兩段程式碼。
第一段是foreach的sample:
static void Test(ICollection<int> values)
{
foreach(int i in values)
Console.WriteLine(i);
}
接下來是這個Test方法的IL程式碼:
.method private hidebysig static void Test(class [mscorlib]System.Collections.Generic.ICollection`1<int32> values) cil managed
{
// 代码大小 55 (0x37)
.maxstack 2
.locals init (int32 V_0, // i
class [mscorlib]System.Collections.Generic.IEnumerator`1<int32> V_1,
bool V_2)
IL_0000: nop
IL_0001: nop
// V_1 = (IEnumerator<int>)values.GetEnumerator()
IL_0002: ldarg.0
IL_0003: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator()
IL_0008: stloc.1
.try
{
// goto IL_0019
IL_0009: br.s IL_0019
// V_0 = V_1.Current
IL_000b: ldloc.1
IL_000c: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current()
IL_0011: stloc.0
// Console.WriteLine(V_0)
IL_0012: ldloc.0
IL_0013: call void [mscorlib]System.Console::WriteLine(int32)
IL_0018: nop
// V_2 = V_1.MoveNext()
IL_0019: ldloc.1
IL_001a: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
IL_001f: stloc.2
// if(V_2) goto IL_000b else goto IL_0035 //(IL_0035===ret)
IL_0020: ldloc.2
IL_0021: brtrue.s IL_000b
IL_0023: leave.s IL_0035
} // end .try
finally
{
// if(V_1 != null) V_1.Dispose()
IL_0025: ldloc.1
IL_0026: ldnull
IL_0027: ceq
IL_0029: stloc.2
IL_002a: ldloc.2
IL_002b: brtrue.s IL_0034
IL_002d: ldloc.1
IL_002e: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0033: nop
IL_0034: endfinally
} // end handler
IL_0035: nop
IL_0036: ret
} // end of method Program::Test
看不懂IL程式碼,沒關係,上面的註解寫得相當清楚,請讀者把前面關係圖裡面的關鍵字找出來即可。
簡單的說,foreach的in所使用的物件,必須實作IEnumerable或IEnumerable<T>。
為什麼in所使用的物件,必須實作IEnumerable呢?在這例子中,foreach的底層運作,
- 從IL上第11行的註解可以看到,會先去呼叫values.GetEnumerator()這個IEnumerable介面上所宣告的方法,取得IEnumerator<int>放到V_1中。
- 接著21行中,會取得IEnumerator<T>的Current屬性,得到目前的項目。
- 接著26行,指的是進入foreach迴圈的本體,也就是Test方法那段sample 的第4行:Console.WriteLine(i);
- 迴圈本體執行完後,第31行可以看到,呼叫IEnumerator<int>的MoveNext()方法,這時values集合中的index會指到下一個項目的index,並得到一個bool值。bool若為true,則代表還有下一個項目。
- 若MoveNext()方法得到的結果為true,則goto IL_000b,也就是第22行,也就是所謂的下一圈迴圈。若為false,則跳到IL_0035,也就是第55行。跳出/結束迴圈。
透過上面簡單的說明,可以瞭解到foreach的逐一巡覽,其實幫我們隱藏了IEnumerator的相關動作。
而我們常用到的集合結構,例如ICollection, IDictionary, IList, 以及其泛型介面,實作的類別,以及實作的泛型類別,都有實作IEnumerable或IEnumerable<T>,所以這些集合都可以透過foreach來展開逐一巡覽的動作。
然而,那又如何?foreach本來就是這樣用了,多瞭解了背後運作原理,相信我,十年後我連IEnumerator怎麼拼都不會拼,我還是可以活的好好的。跟yield有什麼關係呢?
只有用.net framework內建的類別,當然看不太出來,因為最麻煩的是在:
- 每一個想要被foreach逐一巡覽的類別,都要實作IEnumerable介面,實作GetEnumerator方法。
- 接下來還要自己新增一個類別,來實作IEnumerator介面,並且實作MoveNext(), Reset()與Current屬性,以供GetEnumerator方法回傳。
到這邊,是不是覺得很麻煩,而且覺得怎麼可能需要這麼麻煩?可以參考一下Clark的[.NET] Lazy Row Mapping這篇文章,這個時候讀者應該就可以看懂,那一堆IEnumerator<T>跟IEnumerable<T>是在幹嘛了。文章的中間有提到,可以透過yield來實作這堆麻煩事,接下來,就來介紹yield可以幫我們省掉什麼麻煩。而在講yield之前,要先說明一下什麼是Iterator。
Iterator與yield
先來看MSDN很重要的兩句話:
1. 您只要提供 Iterator,它會只往返於類別中的資料結構。當編譯器偵測到您的 Iterator 時,它會自動產生 IEnumerable 或 IEnumerable<T> 介面的 Current、MoveNext 和 Dispose 方法。
2. Iterator 程式碼會使用 yield return 陳述式輪流傳回各元素。yield break 則會結束反覆運算。
針對第一點最後的描述,個人覺得MSDN上說的不夠清楚,應該是:『它會自動產生IEnumerable或IEnumerable<T>介面中,GetEnumerator方法裡面取得的IEnumerator或IEnumerator<T>介面中的Current、MoveNext和Dispose方法。』不曉得是我誤解字面上的意思,還是筆誤,還是太繞口了。
接著來看MSDN上,Iterator使用yield return來完成IEnumerable介面的GetEnumrator方法內容。
使用yield:
public class DaysOfTheWeek : System.Collections.IEnumerable
{
string[] m_Days = { "Sun", "Mon", "Tue", "Wed", "Thr", "Fri", "Sat" };
public System.Collections.IEnumerator GetEnumerator()
{
for (int i = 0; i < m_Days.Length; i++)
{
yield return m_Days[i];
}
}
}
class TestDaysOfTheWeek
{
static void Main()
{
// Create an instance of the collection class
DaysOfTheWeek week = new DaysOfTheWeek();
// Iterate with foreach
foreach (string day in week)
{
System.Console.Write(day + " ");
}
}
}
這樣似乎看不太出來,yield return做了什麼事,這邊我用一模一樣的例子,簡單地模擬一下如果沒有yield,程式碼會變得多麻煩:
public class DaysOfTheWeek : IEnumerable
{
private string[] m_Days = { "Sun", "Mon", "Tue", "Wed", "Thr", "Fri", "Sat" };
public IEnumerator GetEnumerator()
{
var result = new DaysOfTheWeek_Enumerator(m_Days);
return result;
}
}
//要自己新增一個class,並且實作Current, MoveNext, Reset
//若是透過yield來處理,則省了一個class,以及逐一巡覽的實作過程
public class DaysOfTheWeek_Enumerator : IEnumerator
{
private int index = -1;
private string[] m_Days;
public DaysOfTheWeek_Enumerator(string[] days)
{
m_Days = days;
}
public object Current
{
get { return m_Days[index]; }
}
public bool MoveNext()
{
index++;
return (index < m_Days.Length);
}
public void Reset()
{
index = -1;
}
}
internal class TestDaysOfTheWeek
{
private static void Main()
{
// Create an instance of the collection class
DaysOfTheWeek week = new DaysOfTheWeek();
// Iterate with foreach
foreach (string day in week)
{
System.Console.Write(day + " ");
}
}
}
順便補充一下,Iterator其實是一種design pattern,有興趣的朋友可以參考一下:Iterator Pattern。
還有,MSDN上有提到yield return的回傳型別種類:
Iterator 的傳回型別必須是 IEnumerable、IEnumerator、IEnumerable<T> 或 IEnumerator<T>。
yield 範例
這邊透過範例,來讓讀者朋友瞭解到,iterator在使用yield return 和yield break,與foreach展開IEnumerable物件的關係與順序性。
internal class Program
{
private static IEnumerable<string> GetEnumeratorFromDays(string[] days)
{
foreach (var day in days)
{
Console.WriteLine("yield return前, 準備回傳day為:{0}", day);
if (day == "3")
{
Console.WriteLine("day為3,呼叫yield break");
yield break;
}
yield return day;
Console.WriteLine("yield下一行, 準備回傳day為:{0}", day);
}
}
private static void Main(string[] args)
{
string[] days = { "0", "1", "2", "3", "4", "5", "6" };
var result = GetEnumeratorFromDays(days);
foreach (var item in result)
{
Console.WriteLine("實際展開的result item:{0}", item);
Console.WriteLine("item:{0}處理完畢,準備跳下一個item", item);
Console.WriteLine();
}
Console.WriteLine("結束result");
}
}
先來看一下結果:
重點:
- 要建立一個IEnumerable<T>的資料集合,只要使用yield return就可以輕鬆達成。
- 而yield return這次的項目後,下一次被呼叫時,會接著從剛剛yield return後開始執行,而不是GetEnumeratorFromDays()重頭執行唷。
- 呼叫yield break後,會直接結束foreach迴圈,就類似於Iterator的break。
結論
本來只是想快快樂樂的說一下yield return與yield break的用法,沒想到一路帶回到foreach的基本知識。不曉得會不會違背了原本快快樂樂的主題。
但越簡單的東西,真的越難說明白。
讀者朋友們也可以算一下,自己用過了多少次的foreach,MSDN上也都有相關的資料,但您真的有去瞭解背後的原理嗎?
最後,用.NET開發是幸福的,因為這種繁瑣、重複的動作,都被幫忙處理掉了。至於變成一位.NET工程師,會不會可悲,會不會根本沒去瞭解背後的運作原理,就看各位自己的學習心態囉。
Reference
- foreach、in (C# 參考)
- IEnumerable 介面
- IEnumerable 泛型介面
- IEnumerator 介面
- IEnumerator<T> 介面
- Iterator (C# 程式設計手冊)
- yield (C# 參考)
- Iterator Pattern
- 透过IL看C# (3)——foreach语句
- 談C# 編譯器編譯前的程式碼擴展行為
blog 與課程更新內容,請前往新站位置:http://tdd.best/