改來改去,LINQ 這一章的骨架與呈現方式終於大致底定。如果我正要開始學習 LINQ,我會希望有這樣的書可以參考。我打算把其中部分內容摘錄一些上來,一方面替這個快要荒蕪的部落格加一些東西,另一方面也是為這本書打個廣告。

我想,這系列的第一篇就從〈LINQ 提供者〉這一節開始節錄吧,其中包括 IEnumerable<T> 與 IQueryable<T> 的差別,這是面試時經常問到的問題。
註:由於是從電子書上面轉貼至網頁,有些文字的格式(例如程式碼)可能跟書裡面呈現的不一樣。此外,電子書的內容仍在持續更新,所以一定會比這裡摘錄的文章還要新(當然也更完整)。
以下開始摘錄內容...

LINQ 提供者

經過前面的介紹,你已經知道,.NET Framework 為 LINQ 查詢語法定義了一套基本的查詢操作(例如篩選、排序等等),這套基本的查詢操作稱為「標準查詢運算子」。那麼,對於每一種不同類型的資料來源,都得要有人去實作這些運算子(你可以理解為實作一組底層的 API),我們才能用 LINQ 語法去查詢特定類型的資料。那些針對特定資料類型所實作的一組 LINQ API,便叫做「LINQ 提供者」(LINQ providers)。.NET Framework 內建了數個提供者,其中最常用到的兩個 LINQ 提供者是:
  • LINQ to Objects
    用來對序列/集合物件進行查詢。它是由 System.Linq.Enumerable 類別所提供的擴充方法所構成的一組 API,擴充的對象是 IEnumerable<T>。因此,只要是有實作 IEnumerable<T> 的物件,都可以用這組擴充方法。由於這組 API 所查詢的對象是本地(local)記憶體中的集合物件,故這類查詢又稱之為「本地查詢」。
  • LINQ to Entities
    用於查詢資料庫。此提供者是使用 IQuerable<T> 介面,而我們可以透過 System.Linq.Queryable 類別所實作的運算子(擴充方法)來進行查詢。

另外還有 LINQ to XML、LINQ to OData 等等,你也可以在網路上找到一些第三方、開放原始碼的 LINQ 提供者。有了這些 LINQ 提供者,我們就能使用同一種語法來查詢不同來源的資料。
註:本章內容幾乎都是屬於 LINQ to Objects 的範疇。 

IEnumerable<T> vs. IQueryable<T>

介面 IEnumerable<T> 可說是 LINQ to Objects 的靈魂,因為其查詢運算子所操作的對象都是此類型的物件。另一方面,許多資料庫提供者則是使用 IQueryable<T>,亦即查詢的資料來源是實作了 IQueryable<T> 的物件。這兩種介面有什麼差異呢?

底下是 IQueryable<T> 與其相關型別的定義:

public interface IQueryable : IEnumerable
{
    Type ElementType { get; }
    Expression Expression { get; }
    IQueryProvider Provider { get; }
}

public interface IQueryable<out T> : IEnumerable<T>, IQueryable
{ 
}

public interface IQueryProvider
{
    IQueryable CreateQuery(Expression expression);
    IQueryable<TElement> CreateQuery<TElement>(Expression expression);
    object Execute(Expression expression);
    TResult Execute<TResult>(Expression expression);
}

你可以看到, IQueryable<T> 繼承自 IQueryable 和 IEnumerable<T>,所以 IEnumerable<T> 能做的, IQueryable<T> 也都能做。就像 System.Linq.Enumerable 靜態類別為 IEnumerable<T> 提供了一組擴充方法(所謂的標準查詢運算子),在 System.Linq.Queryable 靜態類別當中也同樣為 IQueryable<T> 提供了一組標準查詢運算子。

雖然 IQueryable<T> 本身並未增加任何新的方法,但它的基礎型別 IQueryable 有兩個新成員:
  • IQueryProvider 物件。此物件的角色是查詢提供者,它會負責把 LINQ 查詢表示式轉 換成底層資料來源的查詢語法。
  • Expression 物件。此物件保存了查詢表示式樹( expression tree)。

觀察 IQueryProvider 介面所定義的方法,可以發現它操作的對象是 Expression 物件。而我們又可以從 IQueryable<T> 的擴充方法所接受的參數看到 Expression,例如 Where() 方法:

public static class Queryable
{
    public static IQueryable<TSource> Where<TSource>(
        this IQueryable<TSource> source,
        Expression<Func<TSource, bool>> predicate) // 傳入 Expression 物件
    ...
}

這是因為,IQueryable<T> 的擴充方法其實都是把真正的查詢工作交給背後的 IQueryProvider 物件來執行。

這裡有一個和 IEnumerable<T> 明顯不同的地方:使用 IQueryable<T> 的擴充方法時,傳入的 lambda 表示式會被編譯器轉譯成 Expression 類型的物件,而使用 IEnumerable<T> 的擴充方法時,傳入的 lambda 則被單純轉譯成委派。試將底下的 IEnumerable<T> 的 Where() 方法跟剛才的 IQueryable<T> 的 Where() 比較,便可發現兩者的差異。
Note: Lambda 表示式可以被轉換成委派(delegate)或表示式樹(expression tree)。
public static class Enumerable
{
    public static IEnumerable<TSource> Where<TSource>(
        this IEnumerable<TSource> source,
        Func<TSource, bool> predicate)  // 傳入委派
    ...
}

有些 LINQ 提供者需要使用 IQueryable<T> (而不是 IEumerable<T>),通常就是因為它們需要保存完整的表示式樹( expression tree),以便將 LINQ 查詢轉換成底層資料來源所使用的查詢語法(例如 SQL)。

使用 IQueryable<T> 的好處是,它是針對查詢結果來列舉元素,亦即在遠端(資料庫)執行完查詢之後,才去巡覽其中的元素。另一方面,使用 IEumerable<T> 的查詢方法時,它需要巡覽集合中的全部元素,以便對各元素進行篩選或排序等操作。也因為這個緣故,我們說 IEumerable<T> 適用於查詢本地記憶體中的( in-memory)集合,而 IQueryable<T> 適合用於查詢遠端資料的場合。


容我借用網路上一個影片當中的例子來補充說明。範例程式碼如下:

EmpEntities ent = new EmpEntities();
IEnumerable<Employee> emp = ent.Employees;  // 透過 Entity Framework 載入員工資料
IEnumerable<Employee> temp = emp.Where(x => x.Empid == 2).ToList<Employee>();

這裡使用了 Entity Framework 來查詢資料庫。當程式執行第 3 行敘述時,背後產生的 SQL 指令是選取整個資料表,而沒有事先篩選 Empid == 2 的員工資料。篩選的工作是後來在用戶端這邊才處理的。

如果把 IEumerable<T> 換成 IQueryable<T>:

EmpEntities ent = new EmpEntities();
IQueryable<Employee> emp = ent.Employees;
IQueryable<Employee> temp = emp.Where(x => x.Empid == 2).ToList<Employee>()

背後產生的 SQL 指令則有包含 where 子句,亦即在資料庫端就先篩選好資料,然後才傳回用戶端。這種做法當然更又效率。

影片中使用了兩張圖來表現兩者的差異,我依樣畫葫蘆,附在下方。


下回預告:LINQ 的延遲執行與重複取值

工商時間:目前 LINQ 這一章初步完成的內容如下圖


這本書目前在下列平台上架:




請注意:不同銷售平台有同的價格策略,例如「Google 圖書」,即使作者設定書價為 200,實際上銷售時,該平台可能只賣 150(作者似乎無法掌控這個部分)。因此,請就您認為最方便、好用的平台,同時參酌書籍當時的售價,來決定要在哪個平台下單。我的個人建議仍是 leanpub,因為原始的書籍製作就是使用此平台,故任何細微的修正更新,都會先出現在 leanpub。其他電子書平台的更新速度則比較慢些,通常是累積到比較大幅度的更新時才上傳新版本。

Happy reading!