緣起
最近在 Study 一些 C# 的議題, 為避免忘記, 所以留下一些摘要記錄; 以下文章大部份係摘自 C# 5.0 in a Nutshell, 5th Edition 的第4章 "Advanced C#" 裡的 Enumeration and Iterators 說明.完整程式範例, 筆者放在 GitHub, 請由此下載.
名詞定義
- enumerator : a read-only, forward-only cursor over a sequence of values
- 實作介面
- System.Collections.IEnumerator
- System.Collections.Generic.IEnumerator<T>
- 基本上, 只要有 MoveNext() 方法及 Current 屬性 的物件, 就可以被視為 enumerator
- enumerable object : the logical representation of a sequence. It is not itself a cursor, but an object that produces cursors over itself
- 實作介面
- IEnumerable
- IEnumerable<T>
- 必須要有一個 GetEnumerator() 的方法, 以回傳 enumerator 物件
- 請注意: 上述一個是 IEnumator, IEnumator<T>, 一個是 IEnumerable, IEnumerable<T>; 不要搞混了
- 骨架如下:
class Enumerator // Typically implements IEnumerator or IEnumerator<T> { public IteratorVariableType Current { get {...} } public bool MoveNext() {...} } class Enumerable // Typically implements IEnumerable or IEnumerable<T> { public Enumerator GetEnumerator() {...} }
範例
// High-level way of iterating through the characters in the word “beer”: foreach (char c in "beer") Console.WriteLine (c); // Low-level way of iterating through the same characters: using (var enumerator = "beer".GetEnumerator()) while (enumerator.MoveNext()) { var element = enumerator.Current; Console.WriteLine (element); }
上述程式碼, 共有 2 種列出所有元素的方式; 其實, foreach 敍述, 會被改以 GetEnumerator() 的方式作處理, 這是 C# 提供的 Syntactic Sugar
- 採用 foreach 敍述
- 採用 GetEnumerator() 的方式
1. String 類別實作了以下介面
public sealed class String : IComparable, ICloneable, IConvertible, IEnumerable, IComparable<string>, IEnumerable<char>, IEquatable<string>
2. String 類別實作了 GetEnumerator() 的方法, 會回傳一個 CharEnumerator 的物件
3. CharEnumerator 類別實作了以下介面
public sealed class CharEnumerator : IEnumerator, ICloneable, IEnumerator<char>, IDisposable
4. CharEnumerator 類別實作了 MoveNext() 的方法, 並有一個 Current 的屬性
如果有空, 也可以看一下 List<T> 的源碼, 與上述範例的骨架差不多.
延伸閱讀(一) Enumerable<T>
.NET Framework 另外提供了一個靜態類別 Enumerable (MSDN / 源碼), 它屬於 System.Linq 這個命名空間, 針對實作 IEnumerable<T> 的類別, 提供了一些靜態方法, 例如: Where, Average ..., 讓實作 IEnumerable<T> 的類別 得以使用 LINQ 的語法.
以下是 Where 擴充方法的原始程式碼
說明一下 ( this IEnumerable<TSource> source, Func<TSource, bool> predicate ) 的參數意義:
第1個參數: 代表呼叫該方法的物件本身, 此為擴充方法的設計規格
第2個參數: 代表一個傳入為 TSource, 回傳為 bool 的委派物件 (Func<T1, TResult> 本身就是一個 delegate)
說明一下 ( this IEnumerable<TSource> source, Func<TSource, bool> predicate ) 的參數意義:
第1個參數: 代表呼叫該方法的物件本身, 此為擴充方法的設計規格
第2個參數: 代表一個傳入為 TSource, 回傳為 bool 的委派物件 (Func<T1, TResult> 本身就是一個 delegate)
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) { if (source == null) throw Error.ArgumentNull("source"); if (predicate == null) throw Error.ArgumentNull("predicate"); if (source is Iterator<TSource>) return ((Iterator<TSource>)source).Where(predicate); if (source is TSource[]) return new WhereArrayIterator<TSource>((TSource[])source, predicate); if (source is List<TSource>) return new WhereListIterator<TSource>((List<TSource>)source, predicate); return new WhereEnumerableIterator<TSource>(source, predicate); }
以下是一個範例, 細節說明都寫在程式裡:
附帶一提, var emps2 = emps.Where(o => o.Name.StartsWith("J")); 代表 emps 呼叫 Where 擴充方法, 而傳入一個 匿名方法 o => o.Name.StartsWith("J"), 作為委派物件回呼 (callback) 之用, 傳入的 o 是 Employee 的資料型態, 而 StartsWith(...) 的回傳值是一個 bool.
class Employee { public string Id { get; set; } public string Name { get; set; } } class Program { static void Main(string[] args) { //建立測試資料 List<Employee> emps = new List<Employee>() { new Employee { Id = "001", Name="Jasper" } , new Employee { Id = "002", Name="Judy" } , new Employee { Id = "003", Name="John" } , new Employee { Id = "004", Name="Anita"} , new Employee { Id = "005", Name="Belle"} }; //說明: //1. List<Employee> 是採用 List<T> 這個泛型的 constructed type or closed type; 代表程式裡用到的真正資料型態 //2. List<T> 實作 IEnumerable<T> //3. Enumerable 靜態類別提供 IEnumerable<T> 的擴充方法, 例如: Where, Average ... 等 //4. 因為 Where 是擴充方法, 所以可以直接用 .Where(...) 的方式作處理 //參考一下: //可以順便查一下 Where 對應的源碼, 可以發現, 它是由 Emumeratable 這個靜態類別所提供的擴充方法 //http://referencesource.microsoft.com/#System.Core/System/Linq/Enumerable.cs,e73922753675387a //Lambda Expression Console.WriteLine("===== Lambda ====="); var emps2 = emps.Where(o => o.Name.StartsWith("J")); foreach (var item in emps2) { Console.WriteLine("Id:{0}, Name:{1}", item.Id, item.Name); } //LINQ Statement Console.WriteLine("===== LINQ ====="); var query = from o in emps where o.Name.StartsWith("J") select o; foreach (var item in query) { Console.WriteLine("Id:{0}, Name:{1}", item.Id, item.Name ); } Console.ReadLine(); ////Output: //===== Lambda ===== //Id:001, Name:Jasper //Id:002, Name:Judy //Id:003, Name:John //===== LINQ ===== //Id:001, Name:Jasper //Id:002, Name:Judy //Id:003, Name:John } }
延伸閱讀(二) IEnumerable<T> and IQueryable<T>
如上討論, IEnumerable<T> 必須要有一個實際的已存在集合, 如果在本地沒有問題; 但如果在遠端, 勢必要把遠端所有的資料都拉到本地, 才能作處理, 如果資料量很大, 效能上就會受到影響.
為了處理這個問題, 微軟提供了一個 IQueryable<T> 的界面作處理.
當使用者對 IQueryable<T> 進行操作時, Queryable 類別會透過呼叫 IQueryable<T> 的 Provider 屬性 (這個屬性存放的是實作 IQueryProvider 的物件), 利用該物作, 進行資料存取.
為了處理這個問題, 微軟提供了一個 IQueryable<T> 的界面作處理.
當使用者對 IQueryable<T> 進行操作時, Queryable 類別會透過呼叫 IQueryable<T> 的 Provider 屬性 (這個屬性存放的是實作 IQueryProvider 的物件), 利用該物作, 進行資料存取.
註:
1. IQureyable<T> 繼承自 IEnumerable<T>
2. IEnumerable<T> 有 Enumerable<T> 這個類別提供 extension method
3. IQueryable<T> 有 Queryable<T> 這個類別提供 extension method
在 CodeProject 有2篇文章探討 IEnumerable<T> 與 IQueryable<T>
- IEnumerable vs IQueryable
- 主要繪製了 IEnumerable<T> 與 IQueryable<T> 在進行資料過慮時的簡圖; 同時也有一段影片作 demo
- IEnumerable vs IQueryable
- 製作了一張比較表
黑暗執行緒 也有一篇文章, 將 IEnumerable<T> 與 IQueryable() 作了深入的追踨.
- 關於IQueryable<T>特性的小實驗
- 這篇將 IEnumerable<T> 與 IQueryable<T> 以北風資料庫的 Product table 作為範例; 利用 Visual Studio 偵錯, 並在 SQL Profile 觀察實際傳送至資料庫執行的 SQL 指令; 發現 IQueryable<T> 是在 Server 端作過濾, 再將結果傳回 Client 端, 故若為資料庫存取, 應採用 IQueryable<T>
以下綜合上述文章, 並依 黑暗執行緒 的那篇文章的程序, 重作一次 (只是對象換成 Employees 這個 table)
IEnumerable<T>
程式碼:private static void TestIEnumerable() { using (NorthwindEntities ctx = new NorthwindEntities()) { IEnumerable<Employee> emps = ctx.Employees; int count = emps.Where(x => x.EmployeeID == 2).Count(); Console.WriteLine("Count={0}", count); } }
相對應的 SQL 語句:
SELECT [Extent1].[EmployeeID] AS [EmployeeID], [Extent1].[LastName] AS [LastName], [Extent1].[FirstName] AS [FirstName], [Extent1].[Title] AS [Title], [Extent1].[TitleOfCourtesy] AS [TitleOfCourtesy], [Extent1].[BirthDate] AS [BirthDate], [Extent1].[HireDate] AS [HireDate], [Extent1].[Address] AS [Address], [Extent1].[City] AS [City], [Extent1].[Region] AS [Region], [Extent1].[PostalCode] AS [PostalCode], [Extent1].[Country] AS [Country], [Extent1].[HomePhone] AS [HomePhone], [Extent1].[Extension] AS [Extension], [Extent1].[Photo] AS [Photo], [Extent1].[Notes] AS [Notes], [Extent1].[PhotoPath] AS [PhotoPath], [Extent1].[ReportsTo] AS [ReportsTo] FROM [dbo].[Employees] AS [Extent1]
架構圖:
IQueryable<T>
程式碼:
private static void TestIQueryable() { using (NorthwindEntities ctx = new NorthwindEntities()) { IQueryable<Employee> emps = ctx.Employees; int count = emps.Where(x => x.EmployeeID == 2).Count(); Console.WriteLine("Count={0}", count); } }
相對應的 SQL 語句:
SELECT [GroupBy1].[A1] AS [C1] FROM ( SELECT COUNT(1) AS [A1] FROM [dbo].[Employees] AS [Extent1] WHERE 2 = [Extent1].[EmployeeID] ) AS [GroupBy1]
架構圖:
說明: IQueryable<T> 係在 Server 端作完過濾, 才將資料傳回. 上述會將所有 EmployeeID==2 的資料在資料庫端就進行 Where(), 並取得其 Count().
綜上所述, 在資料庫相關的環境下, 用 IQueryable<T> 的效能會比 IEnumerable<T> 來得好.
綜上所述, 在資料庫相關的環境下, 用 IQueryable<T> 的效能會比 IEnumerable<T> 來得好.
總結
本篇主要在釐清 IEnumerable 與 IEnumerator 的不同. 也介紹 Enumerable 這個靜態類別, 並記錄了一些網路上 IEnumerable 與 IQuery 進行比較的資料.下圖取自 C# 5.0 in a Nutshell, 5th Edition 的第7章, 用以表達整個 IEnumerator 及 IEnumerator<T> 的繼承架構.
Collection Interfaces |
前述有關 Syntactic Sugar 的部份, 有另外一篇文章可以參考, 它將 C# compile 的過程, 大致繪製如下圖; 亦即, 編譯的過程中, 會有一個階段, 將 Fancy C# Code 轉為 Regular C# Code.
C# Compile 簡要過程 |
參考文件
- {Book} C# 5.0 in a Nutshell, 5th Edition Ch04, Ch07
- 針對集合的操作有深入的說明
- {LittleLin's Scrapbook} [C#][筆記] IEnumerable、IEnumerator 與 IEnumerable<T>、IEnumerator<T>
- 針對 IEnumerable、IEnumerator 與 IEnumerable<T>、IEnumerator<T> 有範例說明
- {MSDN Blogs} C# Delegates, Actions, Funcs, Lambdas–Keeping it super simple
- 針對 Delegates, Actions, Funcs, Lambdas 的程式撰寫方式有作說明
- {Huan-Lin 學習筆記} C# 筆記:從 Lambda 表示式到 LINQ
- 針對 Lambda Expression 有詳細的說明
- IEnumerable vs IQueryable
- 主要繪製了 IEnumerable<T> 與 IQueryable<T> 在進行資料過慮時的簡圖; 同時也有一段影片作 demo
- IEnumerable vs IQueryable
- 製作了一張比較表
- {黑暗執行緒} 關於IQueryable<T>特性的小實驗
- 這篇將 IEnumerable<T> 與 IQueryable<T> 以北風資料庫的 Product table 作為範例; 利用 Visual Studio 偵錯, 並在 SQL Profile 觀察實際傳送至資料庫執行的 SQL 指令; 發現 IQueryable<T> 是在 Server 端作過濾, 再將結果傳回 Client 端, 故若為資料庫存取, 應採用 IQueryable<T>
版本修訂
- 2015.01.22 加入 延伸閱讀(二) IEnumerable<T> and IQueryable<T> 的比較
- 2015.01.17 首次發行
.
受益良多,謝謝您的分享
回覆刪除不客氣, 很高興能夠幫助到你.
刪除學習了,謝謝您 : )
回覆刪除不客氣, 很高興能夠幫助到你.
刪除清楚明瞭!感謝分享~
回覆刪除不客氣, 很高興能夠幫助到你.
刪除