From: 011netservice@gmail.com Date: 2022-04-24 Date: 2021-01-24 ---------- 20210124 yield break 停止 yield 運作! 如下例使用於無窮迴圈中, 控制迴圈結束點. while(true) { var value = this.ReadString(reader, delimiter, out var newLine); if (value == null) yield break; // 這裡會跳出迴圈並結束函數. if (index < header.Count) { var key = header[index++]; doc[key] = value; } if (newLine) { yield return doc; doc = new BsonDocument(); index = 0; } } 為什麼叫做yield 依照C# 語言規格-類別定義,Yield Type代表Iterator的回傳資料型態,所以yield就是Iterator的回傳資料。 ---------- 20200219 public class CollectionGeneric { static CollectionGeneric _Me = new CollectionGeneric(); Dictionary _Dict1 = new Dictionary(); List _List1 = new List(); // 開放內部清單物件給外部使用 foreach 語法. public IEnumerable GetValues() { foreach (string item1 in _Dict1.Values) yield return item1; } // 開放內部清單物件給外部使用 foreach 語法. public IEnumerable GetItems() { foreach (object item1 in _List1) yield return item1; } } // foreach 語法使用 yield 回傳的資料. foreach (string sValue in CollectionGeneric._Me.GetValues()) Console.WriteLine(sValue); // foreach 語法使用 yield 回傳的資料. foreach (object oValue in CollectionGeneric._Me.GetItems()) Console.WriteLine(oValue); ---------- 2019119 以下範例示範 yield 搭配 Dictionary 及 ConcurrentDictionary 產生不同的結果: Dictionary: 多工新增結果與單工不相符. ConcurrentDictionary: 多工新增結果與單工一致. private static IEnumerable> GetStrings() { for (int i = 0; i < 1000; i++) { yield return new KeyValuePair("Product" + i, "Value-" + i); } } private static void RunYieldTest() { var products = GetStrings(); Dictionary newProducts = new Dictionary(); Console.WriteLine("來源資料筆數:{0}", products.Count()); Console.WriteLine("執行前結果資料筆數:{0}", newProducts.Count()); Parallel.ForEach(products, x => { newProducts.Add(x.Key, x.Value); }); Console.WriteLine("執行後結果資料筆數:{0}", newProducts.Count()); } private static void RunYieldTestConcurrent() { var products = GetStrings(); ConcurrentDictionary newProducts = new ConcurrentDictionary(); Console.WriteLine("來源資料筆數:{0}", products.Count()); Console.WriteLine("執行前結果資料筆數:{0}", newProducts.Count()); Parallel.ForEach(products, x => { newProducts.TryAdd(x.Key, x.Value); }); Console.WriteLine("執行後結果資料筆數:{0}", newProducts.Count()); } ---------- 20191106 yield 搭配 foreach foreach 迴圈的每個反覆項目都會呼叫 Iterator 方法。 當 Iterator 方法中到達 yield return 陳述式時,就會傳回 expression 並保留程式碼中目前的位置。 下一次呼叫 Iterator 函式時,便會從這個位置重新開始執行。 public static class GalaxyClass { public static void ShowGalaxies() { var theGalaxies = new Galaxies(); foreach (Galaxy theGalaxy in theGalaxies.NextGalaxy) { Debug.WriteLine(theGalaxy.Name + " " + theGalaxy.MegaLightYears.ToString()); } } public class Galaxies { public System.Collections.Generic.IEnumerable NextGalaxy { get { yield return new Galaxy { Name = "Tadpole", MegaLightYears = 400 }; yield return new Galaxy { Name = "Pinwheel", MegaLightYears = 25 }; yield return new Galaxy { Name = "Milky Way", MegaLightYears = 0 }; yield return new Galaxy { Name = "Andromeda", MegaLightYears = 3 }; } } } public class Galaxy { public String Name { get; set; } public int MegaLightYears { get; set; } } } ---------- 20190709 ref: https://docs.microsoft.com/zh-tw/dotnet/csharp/language-reference/keywords/yield yield (C# 參考) 2015/07/20 +2 在陳述式中使用 yield 內容關鍵字時,您會指出關鍵字所在的方法、運算子或 get 存取子是迭代器。 如果使用 yield 定義迭代器,當您為自訂集合類型實作 IEnumerator 和 IEnumerable 模式時,就不需要明確的額外類別 (保存列舉之狀態的類別,請參閱 IEnumerator 中的範例)。 下列範例將示範兩種形式的 yield 陳述式。 C# 複製 yield return ; yield break; 備註 您可以使用 yield return 陳述式一次傳回一個元素。 從迭代器方法傳回的序列,可以透過使用 foreach 陳述式或 LINQ 查詢來取用。 foreach 迴圈的每個反覆項目都會呼叫 Iterator 方法。 當 Iterator 方法中到達 yield return 陳述式時,就會傳回 expression 並保留程式碼中目前的位置。 下一次呼叫 Iterator 函式時,便會從這個位置重新開始執行。 您可以使用 yield break 陳述式結束反覆項目。 如需迭代器的詳細資訊,請參閱 Iterators (迭代器)。 Iterator 方法和 get 存取子 迭代器的宣告必須符合下列需求: 傳回類型必須是 IEnumerable、IEnumerable、IEnumerator 或 IEnumerator。 宣告不可包含任何 in、ref 或 out 參數。 傳回 yield 或 IEnumerable 的 IEnumerator 類型迭代器為 object。 如果迭代器傳回 IEnumerable 或 IEnumerator,則 yield return 陳述式中必須進行從運算式類型轉換成泛型類型參數的隱含轉換。 具有下列特性的方法中不可包含 yield return 或 yield break 陳述式: 匿名方法。 如需詳細資訊,請參閱匿名方法。 包含不安全區塊的方法。 如需詳細資訊,請參閱 unsafe。 例外狀況處理 yield return 陳述式不能位於 try-catch 區塊內。 yield return 陳述式可以位於 try-finally 陳述式的 try 區塊內。 yield break 陳述式可以位於 try 區塊或 catch 區塊中,但是不可位於 finally 區塊中。 如果 foreach 主體 (在 Iterator 方法之外) 擲回例外狀況,則會執行 Iterator 方法中的 finally 區塊。 技術實作 下列程式碼會從 Iterator 方法傳回 IEnumerable,然後逐一查看其元素。 C# 複製 IEnumerable elements = MyIteratorMethod(); foreach (string element in elements) { ... } 對 MyIteratorMethod 的呼叫不會執行方法的主體。 呼叫會改為將 IEnumerable 傳回至 elements 變數中。 在 foreach 迴圈的反覆項目上,會針對 MoveNext 呼叫 elements 方法。 這個呼叫會執行 MyIteratorMethod 的主體,直到下一個 yield return 陳述式為止。 yield return 陳述式所傳回的運算式,不僅會判斷迴圈主體所使用之 element 變數的值,也會判斷 elements 的 Current 屬性,其為 IEnumerable。 在 foreach 迴圈的每個後續反覆項目上,迭代器主體會從上次停止的位置繼續執行,並且在到達 yield return 陳述式時再次停止。 當 Iterator 方法結束或到達 foreach 陳述式時,yield break 迴圈便完成。 範例 下列範例中的陳述式 yield return 位於 for 迴圈內。 Main 方法中 foreach 陳述式主體的每個反覆項目都會建立對 Power Iterator 函式的呼叫。 每次呼叫 Iterator 函式都會執行下一個 yield return 陳述式,這會在 for 迴圈的下一個反覆項目期間發生。 Iterator 方法的傳回類型是 IEnumerable,其為 Iterator 介面類型。 呼叫 Iterator 方法時,它會傳回包含數字乘冪的可列舉物件。 C# 複製 public class PowersOf2 { static void Main() { // Display powers of 2 up to the exponent of 8: foreach (int i in Power(2, 8)) { Console.Write("{0} ", i); } } public static System.Collections.Generic.IEnumerable Power(int number, int exponent) { int result = 1; for (int i = 0; i < exponent; i++) { result = result * number; yield return result; } } // Output: 2 4 8 16 32 64 128 256 } 範例 下列範例將示範本身為迭代器的 get 存取子。 在這個範例中,每個 yield return 陳述式都會傳回使用者定義類別的執行個體。 C# 複製 public static class GalaxyClass { public static void ShowGalaxies() { var theGalaxies = new Galaxies(); foreach (Galaxy theGalaxy in theGalaxies.NextGalaxy) { Debug.WriteLine(theGalaxy.Name + " " + theGalaxy.MegaLightYears.ToString()); } } public class Galaxies { public System.Collections.Generic.IEnumerable NextGalaxy { get { yield return new Galaxy { Name = "Tadpole", MegaLightYears = 400 }; yield return new Galaxy { Name = "Pinwheel", MegaLightYears = 25 }; yield return new Galaxy { Name = "Milky Way", MegaLightYears = 0 }; yield return new Galaxy { Name = "Andromeda", MegaLightYears = 3 }; } } } public class Galaxy { public String Name { get; set; } public int MegaLightYears { get; set; } } } ---------- 20190709 ref: https://dotblogs.com.tw/hatelove/2012/05/10/introducing-foreach-ienumerable-ienumerator-yield-iterator 前言 LINQ的基礎,牽扯到幾個長的很像的東西,例如IEnumerable, Enumerable, IEnumerator, 還加上對應的泛型介面IEnumerable, IEnumerator,還有IEnumerable裡面的方法GetEnumerator()會回傳IEnumerator,IEnumerable裡面的方法GetEnumerator()則會回傳IEnumerator。 這幾個東西到底是在搞什麼鬼,在這篇文章以及下篇文章,希望透過文章的介紹,可以讓讀者更瞭解它們之間的關係。 關係圖 先用一張簡單的圖來說明foreach, IEnumerable, IEnumerable, IEnumerator與IEnumerator的關係。 接下來將先逐一說明foreach的相關原理。 說明 有寫過程式的朋友,幾乎一定用過foreach迴圈來針對某一個物件集合,進行逐一巡覽的動作,而且覺得這樣的程式再自然不過了。但是,其實foreach幫忙簡化了很多的動作,我們來看透过IL看C# (3)-foreach语句這篇文章所提到的兩段程式碼。 第一段是foreach的sample: static void Test(ICollection values) { foreach(int i in values) Console.WriteLine(i); } 接下來是這個Test方法的IL程式碼: .method private hidebysig static void Test(class [mscorlib]System.Collections.Generic.ICollection`1 values) cil managed { // 代码大小 55 (0x37) .maxstack 2 .locals init (int32 V_0, // i class [mscorlib]System.Collections.Generic.IEnumerator`1 V_1, bool V_2) IL_0000: nop IL_0001: nop // V_1 = (IEnumerator)values.GetEnumerator() IL_0002: ldarg.0 IL_0003: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1 class [mscorlib]System.Collections.Generic.IEnumerable`1::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::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。 為什麼in所使用的物件,必須實作IEnumerable呢?在這例子中,foreach的底層運作, 從IL上第11行的註解可以看到,會先去呼叫values.GetEnumerator()這個IEnumerable介面上所宣告的方法,取得IEnumerator放到V_1中。 接著21行中,會取得IEnumerator的Current屬性,得到目前的項目。 接著26行,指的是進入foreach迴圈的本體,也就是Test方法那段sample 的第4行:Console.WriteLine(i); 迴圈本體執行完後,第31行可以看到,呼叫IEnumerator的MoveNext()方法,這時values集合中的index會指到下一個項目的index,並得到一個bool值。bool若為true,則代表還有下一個項目。 若MoveNext()方法得到的結果為true,則goto IL_000b,也就是第22行,也就是所謂的下一圈迴圈。若為false,則跳到IL_0035,也就是第55行。跳出/結束迴圈。 透過上面簡單的說明,可以瞭解到foreach的逐一巡覽,其實幫我們隱藏了IEnumerator的相關動作。 而我們常用到的集合結構,例如ICollection, IDictionary, IList, 以及其泛型介面,實作的類別,以及實作的泛型類別,都有實作IEnumerable或IEnumerable,所以這些集合都可以透過foreach來展開逐一巡覽的動作。 然而,那又如何?foreach本來就是這樣用了,多瞭解了背後運作原理,相信我,十年後我連IEnumerator怎麼拼都不會拼,我還是可以活的好好的。跟yield有什麼關係呢? 只有用.net framework內建的類別,當然看不太出來,因為最麻煩的是在: 每一個想要被foreach逐一巡覽的類別,都要實作IEnumerable介面,實作GetEnumerator方法。 接下來還要自己新增一個類別,來實作IEnumerator介面,並且實作MoveNext(), Reset()與Current屬性,以供GetEnumerator方法回傳。 到這邊,是不是覺得很麻煩,而且覺得怎麼可能需要這麼麻煩?可以參考一下Clark的[.NET] Lazy Row Mapping這篇文章,這個時候讀者應該就可以看懂,那一堆IEnumerator跟IEnumerable是在幹嘛了。文章的中間有提到,可以透過yield來實作這堆麻煩事,接下來,就來介紹yield可以幫我們省掉什麼麻煩。而在講yield之前,要先說明一下什麼是Iterator。 Iterator與yield 先來看MSDN很重要的兩句話: 1. 您只要提供 Iterator,它會只往返於類別中的資料結構。當編譯器偵測到您的 Iterator 時,它會自動產生 IEnumerable 或 IEnumerable 介面的 Current、MoveNext 和 Dispose 方法。 2. Iterator 程式碼會使用 yield return 陳述式輪流傳回各元素。yield break 則會結束反覆運算。 針對第一點最後的描述,個人覺得MSDN上說的不夠清楚,應該是:『它會自動產生IEnumerable或IEnumerable介面中,GetEnumerator方法裡面取得的IEnumerator或IEnumerator介面中的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 或 IEnumerator。 yield 範例 這邊透過範例,來讓讀者朋友瞭解到,iterator在使用yield return 和yield break,與foreach展開IEnumerable物件的關係與順序性。 internal class Program { private static IEnumerable 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的資料集合,只要使用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 介面 Iterator (C# 程式設計手冊) yield (C# 參考) Iterator Pattern 透过IL看C# (3)——foreach语句 談C# 編譯器編譯前的程式碼擴展行為