Chapter 4 - Item 42 : Distinguish between IEnumerable and IQueryable Data Sources

Effective C# (Covers C# 6.0), (includes Content Update Program): 50 Specific Ways to Improve Your C#, 3rd Edition By Bill Wagner 讀後心得

使用列舉集合時,.NET framework 提供了兩種介面:IEnumerable<T> 與 IQueryable<T>。兩種介面都可以用 LINQ 查詢;也因此在使用上容易混淆,本節將介紹兩種的使用時機。

首先,IQueryable<T> 擴充了 IEnumerable<T>。

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

1. 盡可能使用 IQueryable<T>,透過 LINQ to SQL 取得結果。

我們可以很輕鬆的利用 AsEnumerable() 與 AsQueryable() 相互轉換列舉集合類型。

考慮以下程式碼:

var q1 = from c in dbContext.Customers
	 where c.City == "Taipei"
	 select c;
var finalAnswer = from c in q1
                  orderby c.Name
                  select c;

var q2 = (from c in dbContext.Customers
	  where c.City == "Taipei"
	  select c).AsEnumerable();
var finalAnswer2 = from c in q2
                   orderby c.Name
                   select c;

繞行 q1 與 q2 在編譯與執行階段都不會跳出錯誤,但內部運作卻很不一樣。在 LINQ to SQL 情況下,q1 會被轉譯成 SQL Command,會將 finalAnswer 串接成整串 SQL Command;也就是會在資料庫端取得依照 city.Name 排序後的結果並回傳至本機。而 q2 在排序前呼叫了 AsEnumerable(),意味著 finalAnswer2 的排序會在本機操作;換句話說,本機從資料庫拉下了整包資料後,繼續在本機做排序(LINQ to Objects)。

一般來說,盡可能的把自資料庫取得的結果最佳化是一個目標;q1 是一個比較好的選擇。

2. IQueryable<T> 與 IEnumerable<T> 轉譯委派的方式不同,不要混用。

private bool IsValidProduct( Product p ) =>
	p.ProductName.LastIndexOf('C') == 0;

// 可以正常運行。
var q1 = from p in dbContext.Products.AsEnumerable()
         where IsValidProduct(p)
         select p;
// 呼叫 foreach 時將擲出例外。
var q2 = from p in dbContext.Products
         where IsValidProduct(p)
         select p;

如同 Item 38 提及,由於 LINQ to SQL 委派表達式是透過 IQueryProvider 轉譯成 SQL Command,程式自定義的方法將無法轉譯;執行時會跳出錯誤。呼叫 AsEumerable() 會有效能問題,比較好的作法是將 query 內容維持一般的判斷式(或將整個 query 寫成擴充方法),不要將 query 部分加入自定義方法。

3. 利用 AsQueryable() 提高列舉集合的可利用性。

public static IEnumerable<Product> ValidProducts(this IEnumerable<Product> products)
{
	var q1 = from p in dbContext.Products
                 where p.ProductName.LastIndexOf('C') == 0
                 select p;
}

// 這樣寫是合法的,因為 LINQ to SQL provider 可以轉譯 string.LastIndexOf()。
public static IQueryable<Product> ValidProducts(this IQueryable<Product> products)
{
	var q1 = from p in dbContext.Products.AsEnumerable()
                 where p.ProductName.LastIndexOf('C') == 0
                 select p;
}

為了提供兩種回傳與輸入集合類型,我們或許會同時需要兩種方法;但壞處就是重複的程式碼變多了。

public static IEnumerable<Product> ValidProducts(this IEnumerable<Product> products)
{
	var q1 = from p in dbContext.Products.AsQueryable()
		 where p.ProductName.LastIndexOf('C') == 0
		 select p;
}

利用 AsQueryable() 將輸入類型嘗試轉成 IQueryable<T>,若轉型成功,則之後的 query 會利用 LINQ to SQL provider 轉譯:反之,程式也不會跳出錯誤,繼續利用 LINQ to Objects 取得結果。

這樣寫的好處是,我們不需要去擔心輸入型別為何;只需提供一個 api 就可以讓程式自動判斷輸入類型並作相應的處置。效能議題與重複的程式碼也就不復存在。

結論:
1. IQueryable<T> 通常提供比較好的系統效能。
2. 不論在寫 query 或 method call 時,需注意 LINQ to SQL provider 是否有提供轉譯方法。(LINQ to SQL provider 並不支援 Last() 方法)