1. 程式人生 > >深入理解IEnumerable和IQueryable兩介面的區別

深入理解IEnumerable和IQueryable兩介面的區別

無論是在ado.net EF或者是在其他的Linq使用中,我們經常會碰到兩個重要的靜態類Enumerable、Queryable,他們在System.Linq名稱空間下。那麼這兩個類是如何定義的,又是來做什麼用的呢?特別是Queryable類,它和EF的延遲載入技術有什麼聯絡呢?

好,帶著上面的問題開始我們今天的學習。

首先介紹兩個類的定義

(1)Enumerable類,對繼承了IEnumerable<T>介面的集合進行擴充套件;

(2)Queryable類,針對繼承了IQueryable<T>介面的集合進行擴充套件。

在繼續學習之前,我們先來看一下EF中定義的實體集DbSet<T>

通過上面的截圖我們可以看到 DbSet<T>實現了IQueryable<T>、IEnumerable<T>介面。

與上面的兩句話結合起來意思就是可以通過兩個靜態類對DbSet<T>進行擴充套件操作。其實檢視兩個類的原始碼可以知道,這兩個類對實現了IQueryable<T>、IEnumerable<T>介面的集合進行了很多方法的擴充套件。

可能你還不知道如何進行擴充套件方法的定義以及操作,沒事兒,請參考另外一篇文章:C#擴充套件方法的理解

但是那麼的擴充套件方法不都是我們需要的,我們在ado.net EF中最常用的就是擴充套件的Where方法。

兩個類中Where擴充套件方法的定義分別如下

(1)Enumerable類

        public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);
        public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, int, bool> predicate);

觀察Where方法,可以看到第一個引數是實現了IEnumable介面的類,第二個引數是一個Func<T>委託型別
 

(2)Queryable類

        public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate);
        public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, int, bool>> predicate);

觀察Where方法,可以看到第一個引數是實現了IEnumable介面的類,第二個引數是一個Expresssion型別
很顯然,兩個類擴充套件的Where方法是不同的,那具體有什麼不同呢?那麼這種不同又導致什麼結果呢?

OK,帶著疑問繼續往下學習。

為了便於大家好學習,在這裡,我們編寫一段程式碼,通過監視工具檢視兩者的區別。

先上程式碼

        private void Form1_Load(object sender, EventArgs e)
        {
            using (DemoContext context = new DemoContext())
            {
                var customer = context.cunstomer.Where(c => c.Name == "牡丹");
                foreach (var item in customer)
                {
                    MessageBox.Show(item.Id.ToString());
                }
            }
        }

至於程式碼中的上下文定義以及實體集大家不必糾結,我們在這裡要透過表象看本質。在上面的程式中新增斷點,同時啟動sql server profiler監視工具,執行程式。

程式會在斷點處停下來,如下所示

上面只是到點斷點處,當然斷點處的語句還有執行,繼續單步執行。

執行過斷點處所在的語句,觀察監視工具還是什麼都沒有。

咦,是不是出什麼問題了呢?為什麼沒有查詢語句執行呢?真的是監視工具出問題了嗎?

繼續單步除錯

咦,這個時候怎麼出現sql查詢語句了。很奇怪吧,這就是ado.net EF的延遲載入技術,這裡面很重要的一部分就是通過IQueryable介面實現的(具體我們放到最後再說)。

講過了Queryable類的Where方法,接下來我們再來看一下Enumable類的Where方法。

修改上面的程式碼如下所示

        private void Form1_Load(object sender, EventArgs e)
        {
            using (DemoContext context = new DemoContext())
            {
                var customer = context.cunstomer.Where(c => c.Name == "牡丹").AsEnumerable();
                foreach (var item in customer)
                {
                    MessageBox.Show(item.Id.ToString());
                }
            }
        }

同樣是開啟監視工具,新增斷點,執行程式

單步除錯,繼續執行

執行過斷點所在的語句及執行了查詢語句。

關於上面的兩個測試總結如下。

(1)所有對於IEnumerable的過濾,排序等操作,都是在記憶體中發生的。也就是說資料已經從資料庫中獲取到了記憶體中,只是在記憶體中進行過濾和排序操作。

(2)所有對於IQueryable的過濾,排序等操作,只有在資料真正用到的時候才會到資料庫中查詢。這也是Linq的延遲載入核心所在。

那最後一個問題,IQueryable介面為何那麼特殊呢?

觀察它的定義

    // 摘要:
    //     提供對未指定資料型別的特定資料來源的查詢進行計算的功能。
    public interface IQueryable : IEnumerable
    {
        // 摘要:
        //     獲取在執行與 System.Linq.IQueryable 的此例項關聯的表示式樹時返回的元素的型別。
        //
        // 返回結果:
        //     一個 System.Type,表示在執行與之關聯的表示式樹時返回的元素的型別。
        Type ElementType { get; }
        //
        // 摘要:
        //     獲取與 System.Linq.IQueryable 的例項關聯的表示式樹。
        //
        // 返回結果:
        //     與 System.Linq.IQueryable 的此例項關聯的 System.Linq.Expressions.Expression。
        Expression Expression { get; }
        //
        // 摘要:
        //     獲取與此資料來源關聯的查詢提供程式。
        //
        // 返回結果:
        //     與此資料來源關聯的 System.Linq.IQueryProvider。
        IQueryProvider Provider { get; }
    }

該介面有三個特殊的屬性,具體內容程式碼已經介紹了,那查詢時具體又是如何執行呢?

答案是該介面會把查詢表示式先快取到表示式樹中,只有當真正遍歷發生的時候,才會由IQueryProvider解析表示式樹,生成sql語句執行資料庫查詢操作。

哎呀,寫到現在終於差不多快寫完了。

上面介紹了兩個介面的區別與聯絡,具體使用哪種就看自己的專案需求了。

最後補充一下List.Where()方法,還是以程式碼說明。

List<string> fruits =
    new List<string> { "apple", "passionfruit", "banana", "mango", 
                    "orange", "blueberry", "grape", "strawberry" };

IEnumerable<string> query = fruits.Where(fruit => fruit.Length < 6);

foreach (string fruit in query)
{
    Console.WriteLine(fruit);

檢視List<T>的定義,如下圖所示

它也是繼承了IEnumerable介面,因此,他也不存在延遲載入。

OK,到此,所有工作完成。