了解 C# foreach 内部语句和使用 yield 实现的自定义迭代器
在本期專欄中,我將介紹我們?cè)诰幊虝r(shí)經(jīng)常用到的 C# 核心構(gòu)造(即 foreach 語句)的內(nèi)部工作原理。了解 foreach 內(nèi)部行為后,便可以探索如何使用 yield 語句實(shí)現(xiàn) foreach 集合接口,我將對(duì)此進(jìn)行介紹。
雖然 foreach 語句編碼起來很容易,但很少有開發(fā)者了解它的內(nèi)部工作原理,這讓我感到非常驚訝。例如,你是否注意到 foreach 對(duì)數(shù)組的運(yùn)行方式不同于 IEnumberable<T> 集合嗎? 你對(duì) IEnumerable<T> 和 IEnumerator<T> 之間關(guān)系的熟悉程度如何? 而且,就算你了解可枚舉接口,是否熟練掌握使用 yield 語句實(shí)現(xiàn)此類接口呢??
集合類的關(guān)鍵要素
根據(jù)定義,Microsoft .NET Framework 集合是至少可實(shí)現(xiàn) IEnumerable<T>(或非泛型 IEnumerable 類型)的類。此接口至關(guān)重要,因?yàn)橹辽俦仨殞?shí)現(xiàn) IEnumerable<T> 的方法,才支持迭代集合。
foreach 語句語法十分簡(jiǎn)單,開發(fā)者無需知道元素?cái)?shù)量,避免編碼過于復(fù)雜。不過,運(yùn)行時(shí)并不直接支持 foreach 語句。C# 編譯器會(huì)轉(zhuǎn)換代碼,接下來的部分會(huì)對(duì)此進(jìn)行介紹。
foreach 和數(shù)組: 下面展示了簡(jiǎn)單的 foreach 循環(huán),用于迭代整數(shù)數(shù)組,然后將每個(gè)整數(shù)打印輸出到控制臺(tái)中:
int[] array = new int[]{1, 2, 3, 4, 5, 6};foreach (int item in array) {Console.WriteLine(item); }在此代碼中,C# 編譯器為 for 循環(huán)創(chuàng)建了等同的 CIL:
int[] tempArray;int[] array = new int[]{1, 2, 3, 4, 5, 6}; tempArray = array;for (int counter = 0; (counter < tempArray.Length); counter++) {int item = tempArray[counter];Console.WriteLine(item); }在此示例中,請(qǐng)注意,foreach 依賴對(duì) Length 屬性和索引運(yùn)算符 ([]) 的支持。借助 Length 屬性,C# 編譯器可以使用 for 語句迭代數(shù)組中的每個(gè)元素。
foreach 和 IEnumerable<T> 集合: 雖然前面的代碼適用于長(zhǎng)度固定且始終支持索引運(yùn)算符的數(shù)組,但并不是所有類型集合的元素?cái)?shù)量都是已知的。此外,許多集合類(包括 Stack<T>、Queue<T> 和 Dictionary<TKey and TValue>)都不支持按索引檢索元素。因此,需要使用一種更為通用的方法來迭代元素集合。迭代器模式就派上用場(chǎng)了。假設(shè)可以確定第一個(gè)、第二個(gè)和最后一個(gè)元素,那么就沒有必要知道元素?cái)?shù)量,也沒有必要支持按索引檢索元素。
System.Collections.Generic.IEnumerator<T> 和非泛型 System.Collections.IEnumerator 接口旨在啟用迭代器模式(而不是前面介紹的長(zhǎng)度索引模式)來迭代元素集合。它們的關(guān)系類圖如圖 1?所示。
圖 1:IEnumerator<T> 和 IEnumerator 接口的類圖
IEnumerator<T> 派生自的 IEnumerator 包含三個(gè)成員。第一個(gè)成員是布爾型 MoveNext。使用這種方法,可以在集合中從一個(gè)元素移到下一個(gè)元素,同時(shí)檢測(cè)是否已枚舉完所有項(xiàng)。第二個(gè)成員是只讀屬性 Current,用于返回當(dāng)前處理的元素。Current 在 IEnumerator<T> 中進(jìn)行重載,提供按類型分類的實(shí)現(xiàn)代碼。借助集合類中的這兩個(gè)成員,只需使用 while 循環(huán),即可迭代集合:
System.Collections.Generic.Stack<int> stack =new System.Collections.Generic.Stack<int>();int number;// ...// This code is conceptual, not the actual code.while (stack.MoveNext()) {number = stack.Current;Console.WriteLine(number); }在此代碼中,當(dāng)移到集合末尾時(shí),MoveNext 方法返回 false。這樣一來,便無需在循環(huán)的同時(shí)計(jì)算元素?cái)?shù)量。
(Reset 方法通常會(huì)拋出 NotImplementedException,因此不得進(jìn)行調(diào)用。如果需要重新開始枚舉,只要新建一個(gè)枚舉器即可。)
前面的示例展示的是 C# 編譯器輸出要點(diǎn),但實(shí)際上并非按此方式進(jìn)行編譯,因?yàn)槠渲新匀チ藘蓚€(gè)重要的實(shí)現(xiàn)細(xì)節(jié):交錯(cuò)和錯(cuò)誤處理。
狀態(tài)為共享: 前面示例中展示的實(shí)現(xiàn)代碼存在一個(gè)問題,即如果兩個(gè)此類循環(huán)彼此交錯(cuò)(一個(gè) foreach 在另一個(gè)循環(huán)內(nèi),兩個(gè)循環(huán)使用相同的集合),集合必須始終有當(dāng)前元素的狀態(tài)指示符,以便在調(diào)用 MoveNext 時(shí),可以確定下一個(gè)元素。在這種情況下,交錯(cuò)的一個(gè)循環(huán)可能會(huì)影響另一個(gè)循環(huán)。(對(duì)于多個(gè)線程執(zhí)行的循環(huán),也是如此。)
為了解決此問題,集合類不直接支持 IEnumerator<T> 和 IEnumerator 接口。而是直接支持另一種接口 IEnumerable<T>,其唯一方法是 GetEnumerator。此方法用于返回支持 IEnumerator<T> 的對(duì)象。不必使用始終指示狀態(tài)的集合類,而是可以使用另一種類,通常為嵌套類,這樣便有權(quán)訪問集合內(nèi)部,從而支持 IEnumerator<T> 接口,并始終指示迭代循環(huán)的狀態(tài)。枚舉器就像是序列中的“游標(biāo)”或“書簽”。可以有多個(gè)“書簽”,移動(dòng)其中任何一個(gè)都可以枚舉集合,與其他枚舉器互不影響。使用此模式,foreach 循環(huán)的 C# 等同代碼如圖 2?所示。
圖 2:迭代期間始終指示狀態(tài)的獨(dú)立枚舉器
System.Collections.Generic.Stack<int> stack =new System.Collections.Generic.Stack<int>();int number; System.Collections.Generic.Stack<int>.Enumeratorenumerator;// ...// If IEnumerable<T> is implemented explicitly,// then a cast is required.// ((IEnumerable<int>)stack).GetEnumerator();enumerator = stack.GetEnumerator();while (enumerator.MoveNext()) {number = enumerator.Current;Console.WriteLine(number); }迭代后清除狀態(tài): 由于實(shí)現(xiàn) IEnumerator<T> 接口的類始終指示狀態(tài),因此有時(shí)需要在退出循環(huán)后清除狀態(tài)(因?yàn)橐此械淹瓿?#xff0c;要么拋出異常)。為此,從 IDisposable 派生 IEnumerator<T> 接口。實(shí)現(xiàn) IEnumerator 的枚舉器不一定實(shí)現(xiàn) IDisposable,-但如果實(shí)現(xiàn)了,同樣也會(huì)調(diào)用 Dispose。這樣可以在退出 foreach 循環(huán)后調(diào)用 Dispose。因此,最終 CIL 的 C# 等同代碼如圖 3?所示。
圖 3:對(duì)集合執(zhí)行 foreach 的編譯結(jié)果
System.Collections.Generic.Stack<int> stack =new System.Collections.Generic.Stack<int>(); System.Collections.Generic.Stack<int>.Enumeratorenumerator; IDisposable disposable; enumerator = stack.GetEnumerator();try{int number;while (enumerator.MoveNext()){number = enumerator.Current;Console.WriteLine(number);} }finally{// Explicit cast used for IEnumerator<T>.? disposable = (IDisposable) enumerator;disposable.Dispose();// IEnumerator will use the as operator unless IDisposable? // support is known at compile time.? // disposable = (enumerator as IDisposable);? // if (disposable != null)? // {? //?? disposable.Dispose();? // }}請(qǐng)注意,由于 IEnumerator<T> 支持 IDisposable 接口,因此 using 語句可以將圖 3?中的代碼簡(jiǎn)化為圖 4?中的代碼。
圖 4:使用 using 執(zhí)行錯(cuò)誤處理和資源清除
System.Collections.Generic.Stack<int> stack =new System.Collections.Generic.Stack<int>();int number;using(System.Collections.Generic.Stack<int>.Enumeratorenumerator = stack.GetEnumerator()) {while (enumerator.MoveNext()){number = enumerator.Current;Console.WriteLine(number);} }然而,重新調(diào)用 CIL 并不直接支持 using 關(guān)鍵字。因此,圖 3 中的代碼實(shí)際上是用 C# 更精準(zhǔn)表示的 foreach CIL 代碼。
在不實(shí)現(xiàn)?IEnumerable 的情況下使用 foreach: C# 不要求必須實(shí)現(xiàn) IEnumerable/IEnumerable<T> 才能使用 foreach 迭代數(shù)據(jù)類型。編譯器改用鴨子類型這一概念;它使用 Current 屬性和 MoveNext 方法查找可返回類型的 GetEnumerator 方法。鴨子類型涉及按名稱搜索,而不依賴接口或顯式方法調(diào)用。(“鴨子類型”一詞源自將像鴨子一樣的鳥視為鴨子的怪誕想法,對(duì)象必須僅實(shí)現(xiàn) Quack 方法,無需實(shí)現(xiàn) IDuck 接口。) 如果鴨子類型找不到實(shí)現(xiàn)的合適可枚舉模式,編譯器便會(huì)檢查集合是否實(shí)現(xiàn)接口。
迭代器簡(jiǎn)介
至此,你已了解 foreach 的內(nèi)部實(shí)現(xiàn)代碼,是時(shí)候了解如何使用迭代器創(chuàng)建 IEnumerator<T>、IEnumerable<T> 和自定義集合對(duì)應(yīng)的非泛型接口的自定義實(shí)現(xiàn)代碼了。迭代器提供明確的語法,用于指定如何迭代集合類中的數(shù)據(jù),尤其是使用 foreach 循環(huán)。這樣一來,集合的最終用戶就可以瀏覽其內(nèi)部結(jié)構(gòu),而無需知道相應(yīng)結(jié)構(gòu)。
枚舉模式存在的問題是,手動(dòng)實(shí)現(xiàn)起來不方便,因?yàn)楸仨毷冀K指示描述集合中的當(dāng)前位置所需的全部狀態(tài)。對(duì)于列表集合類型類,指示這種內(nèi)部狀態(tài)可能比較簡(jiǎn)單;當(dāng)前位置的索引就足夠了。相比之下,對(duì)于需要遞歸遍歷的數(shù)據(jù)結(jié)構(gòu)(如二叉樹),指示狀態(tài)可能就會(huì)變得相當(dāng)復(fù)雜。為了減少實(shí)現(xiàn)此模式所帶來的挑戰(zhàn),C# 2.0 新增了 yield 上下文關(guān)鍵字,這樣類就可以更輕松地決定 foreach 循環(huán)如何迭代其內(nèi)容。
定義迭代器:迭代器是更為復(fù)雜的枚舉器模式的快捷語法,用于實(shí)現(xiàn)類的方法。如果 C# 編譯器遇到迭代器,它會(huì)將其內(nèi)容擴(kuò)展到實(shí)現(xiàn)枚舉器模式的 CIL代碼中。因此,實(shí)現(xiàn)迭代器時(shí)沒有運(yùn)行時(shí)依賴項(xiàng)。由于 C# 編譯器通過生成 CIL 代碼處理實(shí)現(xiàn)代碼,因此使用迭代器無法獲得真正的運(yùn)行時(shí)性能優(yōu)勢(shì)。不過,使用迭代器取代手動(dòng)實(shí)現(xiàn)枚舉器模式可以大大提高程序員的工作效率。為了理解這一優(yōu)勢(shì),我將先思考一下,如何在代碼中定義迭代器。
迭代器語法: 迭代器提供迭代器接口(IEnumerable<T> 和 IEnumerator<T> 接口的組合)的簡(jiǎn)單實(shí)現(xiàn)代碼。圖 5?通過創(chuàng)建 GetEnumerator 方法,聲明了泛型 BinaryTree<T> 類型的迭代器(盡管還沒有實(shí)現(xiàn)代碼)。
圖 5:迭代器接口模式
using System;using System.Collections.Generic;public class BinaryTree<T>:IEnumerable<T> {public BinaryTree ( T value){Value = value;}#region IEnumerable<T>public IEnumerator<T> GetEnumerator(){// ...? }#endregion IEnumerable<T>public T Value { get; }? // C# 6.0 Getter-only Autoproperty? public Pair<BinaryTree<T>> SubItems { get; set; } }public struct Pair<T>: IEnumerable<T> {public Pair(T first, T second) : this(){First = first;Second = second;}public T First { get; }public T Second { get; }#region IEnumerable<T>public IEnumerator<T> GetEnumerator(){yield return First;yield return Second;}#endregion IEnumerable<T>#region IEnumerable MembersSystem.Collections.IEnumeratorSystem.Collections.IEnumerable.GetEnumerator(){return GetEnumerator();}#endregion? // ...}通過迭代器生成值: 迭代器接口類似于函數(shù),不同之處在于一次生成一系列值,而不是返回一個(gè)值。如果為 BinaryTree<T>,迭代器會(huì)生成一系列為 T 提供的類型參數(shù)值。如果使用非泛型版本 IEnumerator,生成的值將改為類型對(duì)象。
為了正確實(shí)現(xiàn)迭代器模式,必須始終指示某內(nèi)部狀態(tài),以便在枚舉集合的同時(shí)跟蹤當(dāng)前位置。如果為 BinaryTree<T>,跟蹤樹中哪些元素已枚舉,以及哪些元素尚未枚舉。編譯器將迭代器轉(zhuǎn)換成“狀態(tài)機(jī)”,用于跟蹤當(dāng)前位置,并確定如何“將自身移”到下一個(gè)位置。
每當(dāng)?shù)饔龅?yield return 語句,都會(huì)生成值;控制權(quán)會(huì)立即重歸請(qǐng)求獲取此項(xiàng)的調(diào)用方。當(dāng)調(diào)用方請(qǐng)求獲取下一項(xiàng)時(shí),之前執(zhí)行的 yield return 語句后面緊接著的代碼便會(huì)開始執(zhí)行。在圖 6?中,C# 內(nèi)置數(shù)據(jù)類型關(guān)鍵字依序返回。
圖 6:依序生成一些 C# 關(guān)鍵字
using System;using System.Collections.Generic;public class CSharpBuiltInTypes: IEnumerable<string> {public IEnumerator<string> GetEnumerator(){yield return "object";yield return "byte";yield return "uint";yield return "ulong";yield return "float";yield return "char";yield return "bool";yield return "ushort";yield return "decimal";yield return "int";yield return "sbyte";yield return "short";yield return "long";yield return "void";yield return "double";yield return "string";}// The IEnumerable.GetEnumerator method is also required??? // because IEnumerable<T> derives from IEnumerable.? System.Collections.IEnumeratorSystem.Collections.IEnumerable.GetEnumerator(){// Invoke IEnumerator<string> GetEnumerator() above.??? return GetEnumerator();} }public class Program {static void Main(){var keywords = new CSharpBuiltInTypes();foreach (string keyword in keywords){Console.WriteLine(keyword);}} }圖 6?的結(jié)果如圖 7?所示,即 C# 內(nèi)置類型的列表。
圖 7:圖 6 中代碼輸出的一些 C# 關(guān)鍵字的列表
object byte uint ulong float char bool ushort decimal int sbyte short long void double string很顯然,這需要有更多說明,但由于本期專欄的空間有限,我將在下一期專欄中對(duì)此進(jìn)行說明,給大家留個(gè)懸念。我只想說,借助迭代器,可以神奇般地將集合創(chuàng)建為屬性,如圖圖 8?所示。在此示例中,依賴 C# 7.0 元組只是因?yàn)檫@樣做比較有趣。若要進(jìn)一步了解,可以查看源代碼,也可以參閱我的“C# 本質(zhì)論”一書的第 16 章。
圖 8:使用 yield return 實(shí)現(xiàn) IEnumerable<T> 屬性
IEnumerable<(string City, string Country)> CountryCapitals {get? {yield return ("Abu Dhabi","United Arab Emirates");yield return ("Abuja", "Nigeria");yield return ("Accra", "Ghana");yield return ("Adamstown", "Pitcairn");yield return ("Addis Ababa", "Ethiopia");yield return ("Algiers", "Algeria");yield return ("Amman", "Jordan");yield return ("Amsterdam", "Netherlands");// ...? } }總結(jié)
在本期專欄中,我回顧了 C# 版本 1.0 及更高版本中的一項(xiàng)功能,此功能在 C# 2.0 中引入泛型后沒有改變太多。雖然此功能使用頻繁,但許多人都不了解它的內(nèi)部工作原理。然后,我通過舉例泛泛地介紹了利用 yield return 構(gòu)造的迭代器模式。
本期專欄的大部分內(nèi)容截取自我的“C# 本質(zhì)論”一書 (IntelliTect.com/EssentialCSharp),目前我正在修改“C# 7.0 本質(zhì)論”。 有關(guān)詳細(xì)信息,請(qǐng)參閱此書的第 14 和 16 章。
Mark Michaelis?是 IntelliTect 的創(chuàng)始人,擔(dān)任首席技術(shù)架構(gòu)師和培訓(xùn)師。在近二十年的時(shí)間里,他一直是 Microsoft MVP,并且自 2007 年以來一直擔(dān)任 Microsoft 區(qū)域總監(jiān)。Michaelis 還是多個(gè) Microsoft 軟件設(shè)計(jì)評(píng)審團(tuán)隊(duì)(包括 C#、Microsoft Azure、SharePoint 和 Visual Studio ALM)的成員。他在開發(fā)者會(huì)議上發(fā)表了演講,并撰寫了大量書籍,包括最新的“必備 C# 6.0(第 5 版)”(itl.tc/EssentialCSharp)。可通過他的 Facebook?facebook.com/Mark.Michaelis、博客?IntelliTect.com/Mark、Twitter?@markmichaelis?或電子郵件?mark@IntelliTect.com?與他取得聯(lián)系。
感謝以下 IntelliTect 技術(shù)專家對(duì)本文的審閱: Kevin Bost、Grant Erickson、Chris Finlayson、Phil Spokas 和 Michael Stokesbary
原文地址:https://msdn.microsoft.com/en-us/magazine/mt797654
.NET社區(qū)新聞,深度好文,微信中搜索dotNET跨平臺(tái)或掃描二維碼關(guān)注
總結(jié)
以上是生活随笔為你收集整理的了解 C# foreach 内部语句和使用 yield 实现的自定义迭代器的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET的一点历史故事:误入歧途,越陷越
- 下一篇: C# 7 中的模范和实践