︿
Top

2015年1月17日 星期六

C# IEnumerable, IEnumerator, IQueryable

緣起

最近在 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() 的方式


如果有空, 可以看一下 String 類別的源碼;
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)

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 的物件), 利用該物作, 進行資料存取.

註: 
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<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]

架構圖:
說明: IEnumerable<T> 係將資料全部取回 Client 端, 再作過濾, 上述會將所有 EmployeeID==2 的全部清單取回 Client 端之後, 進行 Where 條件, 再進行 Count().


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> 來得好.

總結

本篇主要在釐清 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 簡要過程


參考文件


版本修訂

  • 2015.01.22 加入 延伸閱讀(二) IEnumerable<T> and IQueryable<T> 的比較
  • 2015.01.17 首次發行
.

6 則留言: