提到迭代器我們不能不想到迭代器模式,那我就以迭代器模式作為開場白。
在我們的應用程序中常常有這樣一些數據結構:
它們是一個數據 的集合,如果你知道它們內部的實現結構就可以去訪問它們,它們各自的內部存儲 結構互不相同,各種集合有各自的應用場合.說到這裡大家可能想出一大堆這樣的 集合了:List,Hashtable,ArrayList等等。這些集合各自都有各自的個性,這就 是它們存在的理由。但如果你想遍歷它你必須知道它內部的存儲細節,作為一個 集合元素,把內部細節暴露出來肯定就不好了,這樣客戶程序就不夠穩定了,在你 更換集合對象的時候,比如List不能滿足需求的時候,你換成Hashtable,因為以 前的客戶程序過多的關注了List內部實現的細節,所以不能很好的移植。而迭代 器模式就是為解決這個問題而生的:
提供一種一致的方式訪問集合對象 中的元素,而無需暴露集合對象的內部表示。
比如現在有這樣一個需求 ,遍歷集合內的元素,然後輸出,但是並不限定集合是什麼類型的集合,也就是 未來集合可能發生改變。
思考:
集合會發生改變,這是變化點, 集合改變了,遍歷方法也改變,我們要保證遍歷的方法穩定,那麼就要屏蔽掉細 節。找到了變化點那我們就將其隔離起來(一般使用interface作為隔離手段): 假設所有的集合都繼承自ICollection接口,這個接口用來隔離具體集合的,將 集合屏蔽在接口後面,作為遍歷我們肯定需要這樣一些方法:MoveNext, Current,既然ICollection負責數據存儲,職責又要單一,那麼就新建立一個接 口叫做Iterator吧,每種具體的集合都有自己相對應的Iterator實現:
下面是一個簡易的實現代碼:
/// <summary>
/// 集合的接口
/// </summary>
public interface ICollection
{
int Count { get; }
/// <summary>
/// 獲取迭代器
/// </summary>
/// <returns>迭代器</returns>
Iterator GetIterator();
}
/// <summary>
/// 迭代器接口
/// </summary>
public interface Iterator
{
bool MoveNext();
object Current { get; }
}
public class List : ICollection
{
private const int MAX = 10;
private object[] items;
public List()
{
items = new object[MAX];
}
public object this[int i]
{
get { return items[i]; }
set { this.items[i] = value; }
}
#region ICollection Members
public int Count
{
get { return items.Length; }
}
public Iterator GetIterator()
{
return new ListIterator(this);
}
#endregion
}
public class ListIterator : Iterator
{
private int index = 0;
private ICollection list;
public ListIterator(ICollection list)
{
this.list = list;
index = 0;
}
#region Iterator Members
public bool MoveNext()
{
if (index + 1 > list.Count)
return false;
else
{
index++;
return true;
}
}
public object Current
{
get { return list[index]; }
}
#endregion
}
/// <summary>
/// 測試
/// </summary>
public class Program
{
static void Main()
{
ICollection list = new List();
Iterator iterator = list.GetIterator();
while (iterator.MoveNext())
{
object current = iterator.Current;
}
}
}
看看最後的測試,是不是不管具體的集合如何改變,遍歷代 碼都非常穩定?而且擴展新的集合類也非常方便,只是添加代碼不會修改原來的 代碼,符合開閉原則。當然,這麼好的解決方案微軟當然不會放過,現在C# 2.0 裡已經內置了對迭代器的支持,看看System.Collections, System.Collections.Generic命名空間,所有的集合都實現了這個接口: IEnumerable,這個接口還有泛型的版本。注意到這個接口只有一個方法: IEnumerator GetEnumerator();,IEnumerator就是迭代器的接口,相當於我的 實例裡面的Iterator,它也有泛型的版本。
那麼現在在.net裡所有的集 合類都可以這樣訪問了:
IEnumerator ienumerator = list.GetEnumerator();
while(ienumerator.MoveNext())
{
object current = ienumerator.Current;
}
但是這樣訪問也 太麻煩了,所以C#裡出現了foreach關鍵字,我們來看看foreach背後發生了什麼?假如有如下的代碼:
public static void Main()
{
ArrayList list = new ArrayList();
list.Add(1);
list.Add(2);
list.Add(3);
foreach (object item in list)
{
Console.WriteLine(item.ToString());
}
}
下面是它對應的IL代碼:
Code
.method private hidebysig static void Main() cil managed
{
.entrypoint
.maxstack 2
.locals init (
[0] class [mscorlib]System.Collections.ArrayList list,
[1] object item,
[2] class [mscorlib] System.Collections.IEnumerator CS$5$0000,
[3] class [mscorlib]System.IDisposable CS$0$0001)
L_0000: newobj instance void [mscorlib]System.Collections.ArrayList::.ctor()
L_0005: stloc.0
L_0006: ldloc.0
L_0007: ldc.i4.1
L_0008: box int32
L_000d: callvirt instance int32 [mscorlib]System.Collections.ArrayList::Add(object)
L_0012: pop
L_0013: ldloc.0
L_0014: ldc.i4.2
L_0015: box int32
L_001a: callvirt instance int32 [mscorlib]System.Collections.ArrayList::Add(object)
L_001f: pop
L_0020: ldloc.0
L_0021: ldc.i4.3
L_0022: box int32
L_0027: callvirt instance int32 [mscorlib] System.Collections.ArrayList::Add(object)
L_002c: pop
L_002d: ldloc.0
L_002e: callvirt instance class [mscorlib] System.Collections.IEnumerator [mscorlib] System.Collections.ArrayList::GetEnumerator()
L_0033: stloc.2
L_0034: br.s L_0048
L_0036: ldloc.2
L_0037: callvirt instance object [mscorlib] System.Collections.IEnumerator::get_Current()
L_003c: stloc.1
L_003d: ldloc.1
L_003e: callvirt instance string [mscorlib]System.Object::ToString()
L_0043: call void [mscorlib]System.Console::WriteLine(string)
L_0048: ldloc.2
L_0049: callvirt instance bool [mscorlib] System.Collections.IEnumerator::MoveNext()
L_004e: brtrue.s L_0036
L_0050: leave.s L_0063
L_0052: ldloc.2
L_0053: isinst [mscorlib]System.IDisposable
L_0058: stloc.3
L_0059: ldloc.3
L_005a: brfalse.s L_0062
L_005c: ldloc.3
L_005d: callvirt instance void [mscorlib]System.IDisposable::Dispose()
L_0062: endfinally
L_0063: call string [mscorlib]System.Console::ReadLine()
L_0068: pop
L_0069: ret
.try L_0034 to L_0052 finally handler L_0052 to L_0063
}
從.locals init 那 裡可以看出編譯器為我們添加了兩個局部變量,一個就是迭代器。
L_002d: ldloc.0
L_002e: callvirt instance class [mscorlib]System.Collections.IEnumerator [mscorlib]System.Collections.ArrayList::GetEnumerator()
L_0033: stloc.2
這三行代碼告訴我們,調用list的 GetEnumerator()方法,獲取迭代器實例將其賦值給編譯器為我們添加的那個迭 代器局部變量,接著是L_0034: br.s L_0048,br.s這個指令是強制跳轉,我們 接著看
L_0048:ldloc.2
L_0049:callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
調用 迭代器的MoveNext()方法,L_004e: brtrue.s L_0036 如果是true的話跳轉,
L_0036:ldloc.2
L_0037:callvirt instance object [mscorlib]System.Collections.IEnumerator::get_Current()
L_003c:stloc.1
L_003d:ldloc.1
L_003e:callvirt instance string [mscorlib]System.Object::ToString()
L_0043:call void [mscorlib]System.Console::WriteLine(string)
獲取當前值,然後輸出看到沒有,實際foreach後面干的事就是 獲取迭代器,然後一個while循環,不過這樣一些確實簡潔多了。說到這裡是不 是
IEnumerator ienumerator=list.GetEnumerator();
while (ienumerator.MoveNext())
{
object item=ienumerator.Current;
Console.WriteLine(item.ToString());
}
和
foreach (object item in list)
{
Console.WriteLine (item.ToString());
}
這兩樣代碼是一樣的呢?如果不一 樣那推薦使用哪一個呢?當然是使用第二種,簡潔嘛,除了簡潔之外就沒有其它 的了?細心讀者會發現上面的IL代碼,在結束循環後還有一大塊,可我們的C#代 碼中並沒有啊,接著分析:
.try L_0034 to L_0052 finally handler L_0052 to L_0063
這裡說明從L_0034到L_0052是被放在try裡面的,恰好 這段代碼是循環體裡的東西,L_0052到L_0063裡是放在finally裡的,看來 foreach還給我們加了一個try{}finally{}結構啊。那看看L_0052到L_0063裡是 什麼東西吧:
L_0052:ldloc.2
L_0053:isinst [mscorlib]System.IDisposable
L_0058:stloc.3
L_0059:ldloc.3
L_005a:brfalse.sL_0062
L_005c:ldloc.3
L_005d:callvirt instance void [mscorlib] System.IDisposable::Dispose()
L_0062:endfinally
L_0063:call string [mscorlib]System.Console::ReadLine()
判 斷迭代器對象是否是一個IDisposable實例,如果是,那就要調用它的Dispose() 方法了(為啥它要實現IDisposable接口?那肯定這個迭代器裡使用了一些非托管 資源)。看到了吧,foreach也真夠智能的,看來使用foreach的方式是比自己用 while方式安全穩定多了。
(PS:好像是扯遠了點,不過大家一起了解一下 ,呵呵,其實我當初也沒想說這個,不過後來看IL代碼有點不對勁,就當作個副 產品吧)C# 2.0裡還出現個關鍵字yield,我看了半天MSDN也沒明白它的意思:
在迭代器塊中用於向枚舉數對象提供值或發出迭代結束信號。到現在還 是沒明白,不過yield這個東西後面卻包含了很多東西,有一些非常“奇怪 ”的特性,我稱之為奇怪的意思是與我們以前的思維有的不符,Linq的一 些特質也是建立在這個特性之上的。關於yield的更多討論我想放在另外一篇文 章中,因為我覺得有必要。