緣起
接續 前一篇 對 IEnumerator 及 IEnumerable 的探討, 及進一步 Study, 可以發現, .NET Framework 的集合物件都有實作 IEnumerable (非泛型) 或 IEnumerable<T> (泛型),
故若要產生一個物件集合, 供外部逐一取出元素, 直接利用現成的泛型類別 (ex: List<T>, Dictionary<T>) 產生之後, 回傳給呼叫端; 再由呼叫端用 foreach 逐一讀取, 這是很直覺的寫法.
但有沒有更方便的方式呢? 在某些情境下, 或許可以用 yield 這個 Syntactic Sugar.
完整程式範例, 筆者放在 GitHub, 請由此下載.
故若要產生一個物件集合, 供外部逐一取出元素, 直接利用現成的泛型類別 (ex: List<T>, Dictionary<T>) 產生之後, 回傳給呼叫端; 再由呼叫端用 foreach 逐一讀取, 這是很直覺的寫法.
但有沒有更方便的方式呢? 在某些情境下, 或許可以用 yield 這個 Syntactic Sugar.
完整程式範例, 筆者放在 GitHub, 請由此下載.
程式範例
茲就以下的範例作說明:
1. GetEmployees1(): 採用直覺的寫法, 需要一個 List<Employee> 型別的物件作暫存, 產生集合後, 整個回傳. 由外部逐一取出元素,
2. GetEmployees2(): 採用 yield 的寫法, 不需要一個 List<Employee> 型別的物件作暫存.
重要的是, 當您逐步偵錯時, 可以發現執行至 IEnumerable<Employee> emps2 = GetEmployees2(); 時, 程式並未進去 GetEmployees2() 方法, 而是在 Main() 裡 foreach (var item in emps2) 的迴圈, 每一次才取回一筆.
class Employee
{
public string Id { get; set; }
public string Name { get; set; }
}
class Program
{
/// <summary>
/// 回傳 User000 - User004 (by 一般的方式)
/// </summary>
/// <returns></returns>
private static IEnumerable<Employee> GetEmployees1()
{
List<Employee> emps = new List<Employee>();
for (int i = 0; i < 5; i++)
{
emps.Add(new Employee() { Id = String.Format("{0:D3}", i), Name = "User" + String.Format("{0:D3}", i) });
}
return emps;
}
/// <summary>
/// 回傳 User000 - User004 (by yield)
/// </summary>
/// <returns></returns>
private static IEnumerable<Employee> GetEmployees2()
{
for (int i = 0; i < 5; i++)
{
yield return (new Employee() { Id = String.Format("{0:D3}", i), Name = "User" + String.Format("{0:D3}", i) });
}
}
static void Main(string[] args)
{
Console.WriteLine("===== Without yield ====");
IEnumerable<Employee> emps1 = GetEmployees1();
foreach (var item in emps1)
{
Console.WriteLine("Id={0}, Name={1}", item.Id, item.Name);
}
Console.WriteLine("===== With yield ====");
IEnumerable<Employee> emps2 = GetEmployees2();
foreach (var item in emps2)
{
Console.WriteLine("Id={0}, Name={1}", item.Id, item.Name);
}
Console.ReadLine();
////Output:
//===== Without yield ====
//Id=000, Name=User000
//Id=001, Name=User001
//Id=002, Name=User002
//Id=003, Name=User003
//Id=004, Name=User004
//===== With yield ====
//Id=000, Name=User000
//Id=001, Name=User001
//Id=002, Name=User002
//Id=003, Name=User003
//Id=004, Name=User004
}
}
依 ASP.NET MVC 5 網站開發美學 Ch03 的說明, yield 也是一個編譯器魔法 (compiler magic, 雷同於 Syntactic Sugar), 編譯器會在 yield 指令, 產生一個迭代運算的有限狀態機 (state machine), 這個狀態機主要控制巡覽時的存取動作, 每次巡覽觸發時, 才真正進入集合撈資料; 亦即對集合物件的存取, 會推遲到真正查詢時才觸發, 這個機制稱為延遲查詢 (Deferred Query) 或 延遲執行 (Deferred Execution)
關於 yield 的編譯細節, 可以參考以下 2 篇文章.
總結
本篇介紹了 yield 的使用方式, 就應用程式開發而言, 用到的機會應該不多, 但若是發展 Framework, 則用到的機會應該比較大, 參考看看囉.
參考文件
- {Book} ASP.NET MVC 5 網站開發美學 Ch03
- 針對 LINQ 的一些必備知識作說明
- {MSDN} yield
- 官方的說明
- {C# in Depth} Iterator block implementation details: auto-generated state machines
- 利用反組譯工具, 呈現 yield 區塊, 經由編譯器第1段編譯後的程式段落
- {Shawn Hargreaves Blog} Iterator state machines
- 如何利用 yield 建立 state machine 的程式
- {限量ㄟ蓋步} C# - yeild return 使用方法
- 內含 yield 的使用範例
- {Youtube} C# yield's Syntactic Sugar:
- 這個影片有用到 .NET Reflector 工具, 展示如何取得 yield 的實際對應程式內容.
- [DotNetPerls] C# Yield
- 相對於 MSDN 提供的非泛型範例, 該文章提供了一個泛型的範例, 並作了效能的比較, 有泛型的, 效能比較好.
沒有留言:
張貼留言