在通常的情況下,我們非常習慣於用foreach來迭代容器中的元素。雖然比起for循環來說,foreach可以讓我們少打一些字母。
如果我們有下面的需求:把一個整數的容器中所有元素乘2,然後保存在新的容器中,那麼我們通常會寫下下面的代碼:
1int[] datas = { 1, 3, 2, 5, 7, 94, 1, 8, 42, 74, 8 }; 2ArrayList src = new ArrayList(datas); 3ArrayList dest = new ArrayList(); 4foreach (object obj in src) 5{ 6 dest.Add(((int)obj) * 2); 7} 8
對於表達來說,我們真正需要注意的是“dest.Add(((int)obj) * 2);”這行代碼。而在這行代碼中,最重要的是“((int)obj) * 2”代碼。而剩下的代碼僅僅是為了輔助完成上面的任務。
真正在開發中,我們可能會經常遇到上面的命題。如果每次都來寫foreach,然後是幾行的輔助代碼,確實感覺有點繁瑣。那麼我們有什麼好的方法嗎?能把foreach封裝起來就好了。
在.Net的System.Collections.Generic命名空間裡,我們會發現List類有下面幾個方法:
ConvertAll 將當前 List 中的元素轉換為另一種類型,並返回包含轉換後的元素的列表。
Find 搜索與指定謂詞所定義的條件相匹配的元素,並返回整個 List 中的第一個匹配元素。
FindAll 檢索與指定謂詞所定義的條件相匹配的所有元素。
FindLast 搜索與指定謂詞所定義的條件相匹配的元素,並返回整個 List 中的最後一個匹配元素。
FindLastIndex 已重載。 搜索與指定謂詞所定義的條件相匹配的元素,返回 List 或它的一部分中最後一個匹配項的從零開始的索引。
ForEach 對 List 的每個元素執行指定操作。
這些方法都有一個特殊的參數:代理。這些代理都是在System命名空間中聲明的。
下面我們還是沿用剛才的命題,用ConvertAll來寫一段代碼完成任務。
1int[] datas = { 1, 3, 2, 5, 7, 94, 1, 8, 42, 74, 8 }; 2List<int> src = new List<int>(datas); 3List<int> dest = src.ConvertAll(delegate(int val) 4{ 5 return val * 2; 6});
現在,你看不到foreach循環,而且也不用負責向dest中加入元素,你唯一需要關注的就是“val * 2”。現在都是.Net 3.5了,我們應該換上時髦的Lambda表達式!
1int[] datas = { 1, 3, 2, 5, 7, 94, 1, 8, 42, 74, 8 }; 2List<int> src = new List<int>(datas); 3List<int> dest = src.ConvertAll(val => val * 2);
現在讓我們來數數,原本聲明dest,foreach外加val * 2的3行代碼,現在一行就完成了。而且從代碼上你一眼就可以看明白“List<int> dest = src.ConvertAll(val => val * 2);”這句話是什麼意思:聲明一個dest容器,它應該是指向一個src以“val => val * 2”為規則轉化而成的容器。
那麼現在你肯定想知道這是怎麼做到的。其實並不難,讓我們來以ArrayList為基類創建一個自己的List類:MyList。
1class MyList<T> : ArrayList 2{ 3 public MyList() { } 4 public MyList(ICollection list) : base(list) { } 5 public MyList(int capacity) : base(capacity) { } 6}
好了,在此先創建一個泛型的MyList。繼承自ArrayList。接下來,我們可以聲明3個代理,負責3種不同的操作:ForEach、FindAll、CollectAll
1public delegate void ForEachAction<T>(T val); 2public delegate R CollectAllFunc<T, R>(T val); 3public delegate bool FindAllFunc<T>(T val);
下面我們就來創建ForEach方法、FindAll方法和CollectAll方法。首先,我們來看看ForEach。這個最簡單。
1public MyList<T> ForEach(ForEachAction<T> act) 2{ 3 foreach (object obj in this) 4 { 5 act((T)obj); 6 } 7 return this; 8}
這類方法的設計思路大體是這樣的。把變動的代碼視為一個代碼塊,有輸入和輸出。這樣就可以把變動的代碼抽象成一個函數。於是我們就可以把變動的代碼分析一下,得出需要接受多少參數,然後會返回什麼結果。最後設計一個代理來限定這個代碼塊的大模樣。而ForEach方法中,僅僅是固定不變的代碼,如foreach循環。在最後之所以返回this,目的是為了鏈式調用:
list.ForEach(…).Add(…)
至於CollectAll和FindAll方法也使用同樣的設計思路:
1public MyList<RType> CollectAll<RType>(CollectAllFunc<T, RType> act) 2{ 3 MyList<RType> ret = new MyList<RType>(); 4 foreach (object obj in this) 5 { 6 ret.Add(act((T)obj)); 7 } 8 return ret; 9} 10 11public MyList<T> FindAll(FindAllFunc<T> act) 12{ 13 MyList<T> ret = new MyList<T>(); 14 foreach (object obj in this) 15 { 16 if(act((T)obj)) ret.Add(obj); 17 } 18 return ret; 19}
下面我們來看看這個MyList類的使用,當然,跟List類的使用基本一樣。這次的需求增加了一下,需要先打印所有的元素,然後給每個元素×2,然後打印大於10的元素。
1int[] datas = { 1, 3, 2, 5, 7, 94, 1, 8, 42, 74, 8 }; 2MyList<int> list = new MyList<int>(datas); 3list.ForEach(val => Console.Write("{0}\t", val)) 4 .CollectAll(val => val * 2).FindAll(val => val > 10) 5 .ForEach(val => Console.Write("{0}\t", val));
這段代碼看上去都是一些方法調用,其實它替代了許多foreach循環。不過至於後面兩個需求,使用一個foreach循環就可以辦到,上面的鏈式調用對性能有所損失。不過依據個人的愛好吧,使用哪種方式都可以。
至於Lambda表達式,其實是脫胎於代理的一個概念(至少在.Net中可以這麼說吧)。給代理賦值的時候,我們可以使用匿名函數的方式:
1ForEachAction<int> act = delegate(int val) 2{ 3 //do something 4};
使用匿名函數在輸入上會有些許麻煩,如果僅僅是一小段簡單的代碼,使用匿名函數可能會顯得有點不值得。所以我們可以用Lambda表達式來達到同樣的目的,但是可以少輸入點字母:
1ForEachAction<int> act = val => {/**//*do something*/};
唠叨了這麼多,僅僅是對.Net迭代器的一些看法。也算是對在.Net中看到了Ruby式迭代器的一種興奮。