MVP Records:

Windows Azure (2011)
ASP.NET (2007-2010)
Solution Architecture (2006)
SQL Server (2004-2005)

Award Records:

SQL Server 五虎將 (2011)
Hyper-V 金翅級戰士 (2012)

Get Microsoft Silverlight

我的著作:

1. Windows Azure 教戰手札(繁體版)
(點此進入書籍服務區)



走进云计算:Windows Azure实战手记 (簡體版)

2. ASP.NET 問題解決實戰
(點此進入書籍服務區)









 

 

 

技術資訊

線上書店

最新回應

PS: 這篇不是要和 91 打對台,只是就算看了他的文章,我還是不了解,所以我透過我的觀點來描述看看,讀者可以自由選擇要看 91 的文還是我這篇文。

延遲執行 (Deferred Execution) 是 LINQ 的重點技術之一,對於像是會存取資料庫的 Framework 或指令,如果在指令下的當下就執行的話,有可能會在下個指令存取之前就跑了,這樣可能會有時間差,或是下一個指令無法反應到結果上的問題,這個和之前在 ORM 系列文講到的 Lazy Loading (延遲載入) 不太一樣,Lazy Loading 是指已經有鍵值資料,但為了要節省查詢時間,因此只有在需要時才會載入全部的資料,而這裡的延遲執行是指說要在真正要瀏覽元素時才正式執行指令

不過我一直很好奇的是,以下面的 LINQ 指令來說:

var query = from customer in db.Customers  
            where customer.City == "Paris" 
            select customer;               

誰會是查詢的發動者?是 in?from?where?還是 select?亦或是都不發動?

依照我們前面的說明,事實上就算到了 select 也不會發動,因為查詢也有可能會複雜到這樣:

var query = from i in (from i in list
                        where i > 0
                        select i)
            where i > 5
            select new { i = i, s = "Greater Than 5" };

那如果是這樣的指令,應該是哪個 select 才是發動者呢?

延遲執行事實上想解決的就是這種問題,只是 LINQ 本身的算符太多太大了,所以我簡化一下,寫一個自己的 IQueryable<T> 以及 Queryable<T>:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DefrredExecutionPrototype
{
    public interface IQueryable<T>
    {
        T First();
        T Last();
        IEnumerable<T> Where(Func<T, bool> WhereClause);
        IEnumerable<T> ToList();
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DefrredExecutionPrototype
{
    public class Queryable<T> : IQueryable<T> 
    {
        private IEnumerable<T> _list = null;

        public Queryable(IEnumerable<T> InitialList)
        {
            this._list = InitialList;
        }
        
        public T First()
        {
            IEnumerator<T> cursor = this._list.GetEnumerator();
            int offset = 0;

            if (this._list.Count() == 0)
                return default(T);

            while (cursor.MoveNext())
            {
                if (offset == 0)
                    return cursor.Current;
            }

            return default(T);
        }

        public T Last()
        {
            IEnumerator<T> cursor = this._list.GetEnumerator();
            int offset = 0;

            while (cursor.MoveNext())
            {
                if (offset == this._list.Count() - 1)
                    return cursor.Current;

                offset++;
            }

            return default(T);
        }

        public IEnumerable<T> Where(Func<T, bool> WhereClause)
        {
            IEnumerator<T> cursor = this._list.GetEnumerator();
            List<T> itemStore = new List<T>();

            if (this._list.Count() == 0)
                return new List<T>();

            Console.WriteLine("Invoke query");

            while (cursor.MoveNext())
            {
                if (WhereClause.Invoke(cursor.Current))
                    itemStore.Add(cursor.Current);
            }

            return itemStore;
        }

        public IEnumerable<T> ToList()
        {
            IEnumerator<T> cursor = this._list.GetEnumerator();
            List<T> itemStore = new List<T>();

            if (this._list.Count() == 0)
                return new List<T>();

            while (cursor.MoveNext())
            {
                itemStore.Add(cursor.Current);
            }

            return itemStore;
        }
    }
}

IQueryable<T> 模擬了 First(), Last(), ToList() 和 Where() 四個指令,其中 First() 和 Last() 的實作最簡單,只是把傳入的 IEnumerable<T> 的第一個或最後一個元素回傳,而 ToList() 是將所有的資料複製到 List<T> 也不難,但有點小難的是 Where()。

public IEnumerable<T> Where(Func<T, bool> WhereClause)
{
    IEnumerator<T> cursor = this._list.GetEnumerator();
    List<T> itemStore = new List<T>();

    if (this._list.Count() == 0)
        return new List<T>();

    Console.WriteLine("Invoke query");

    while (cursor.MoveNext())
    {
        if (WhereClause.Invoke(cursor.Current))
            itemStore.Add(cursor.Current);
    }

    return itemStore;
}

Where 接受一個查詢條件的引數,只是這個查詢條件是一個委派,Func<T, bool> 的意思是指會傳入一個參數 T,而回傳 bool 為結果的意思,相對於它的是 Action<T>,Action 是無傳回值的委派。在 Where 裡面會將每個 IEnumerable<T> 中的元素都丟到 WhereClause 中比對,並且只要符合的就會丟給 List<T>,這點和 ToList() 就很像。

我們的用戶端程式是這樣寫的:

List<int> list = new List<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
IQueryable<int> query = new Queryable<int>(list);

Console.WriteLine("first: {0}, last: {1}", query.First(), query.Last());

foreach (var item in query.Where((i) => i % 2 == 0))
{
    Console.WriteLine(item);
}

Console.ReadLine();

實際執行,你會發現:

image

"Invoke query" 是我放在 Where() 中的一個輸出字串,這樣看起來,只有 query.Where() 時才會真正發動查詢,不過我很好奇的是,它似乎不會另外找地方存,為了證實這件事,我在迴圈中加了輸出指令,然後執行,它的結果會是:

image

這個結果就挺令人玩味了,為什麼它會完全依照順序,而不是再次產生新的空間來巡覽呢?我就用 Reflector 展開這個程式碼,發現一件有趣的事:

image

這 ... 看起來不像是程式碼本身的東西,而且,在程式中還出現了一個叫做 <Where>d_0<T> 的類別:

image

看到 1_state ... 看來似乎結論出來了,這個由編譯器產生的類別是一個有限狀態機 (Finite State Machine) 類別,我們再展開它的 MoveNext 看看:

image

裡面出現了我們下的指令。

簡單的說,在每次 Where 執行時,它並不會從頭來執行,透過由編譯器產生的這個狀態機類別,在每次巡覽元素時,會檢查狀態機的狀態,同時狀態機內會保存當下的 Queryable<T> 物件,以及最後一次巡覽到的值 (this.<>2_current),下次當巡覽要求再次呼叫時,就會由最後一次巡覽到的位置來查詢。這樣的作法可以節省巡覽時的所需要的時間和記憶體,對資料庫來說,LINQ provider 只需要在這個時候下達 SQL 指令,就能將資料取回。

透過上面的解析,能得到一個結論:LINQ 在呼叫 select 時,還是沒有引發查詢動作,真正引發查詢動作會是在 foreach,也就是 IEnumerator<T>.MoveNext(),而這個方法會被由編譯器產生的狀態機類別處理,因此開發人員通常不會感覺到,但這卻是延遲執行的精華所在。

為了證實這一點,我們在 Where() 後和 foreach 前加入一個字串 "Execute Query":

namespace DefrredExecutionPrototype
{
    class Program
    {
        static void Main(string[] args)
        {
            List<int> list = new List<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
            IQueryable<int> query = new Queryable<int>(list);

            Console.WriteLine("first: {0}, last: {1}", query.First(), query.Last());
            var q = query.Where((i) => i % 2 == 0);

            Console.WriteLine("Execute query");

            foreach (var item in q)
            {
                Console.WriteLine(item);
            }

            Console.ReadLine();
        }
    }
}

它的執行結果會是:

image

"Execute query" 在 foreach 之前觸發,證明了實際的元素查詢是在 foreach 執行。

最終的 Queryable<T> 程式碼如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DefrredExecutionPrototype
{
    public class Queryable<T> : IQueryable<T> 
    {
        private IEnumerable<T> _list = null;

        public Queryable(IEnumerable<T> InitialList)
        {
            this._list = InitialList;
        }
        
        public T First()
        {
            IEnumerator<T> cursor = this._list.GetEnumerator();
            int offset = 0;

            if (this._list.Count() == 0)
                return default(T);

            while (cursor.MoveNext())
            {
                if (offset == 0)
                    return cursor.Current;
            }

            return default(T);
        }

        public T Last()
        {
            IEnumerator<T> cursor = this._list.GetEnumerator();
            int offset = 0;

            while (cursor.MoveNext())
            {
                if (offset == this._list.Count() - 1)
                    return cursor.Current;

                offset++;
            }

            return default(T);
        }

        public IEnumerable<T> Where(Func<T, bool> WhereClause)
        {
            if (WhereClause == null)
                throw new ArgumentNullException("WhereClause", "WHERE clause cannot be null.");

            IEnumerator<T> cursor = this._list.GetEnumerator();
            List<T> itemStore = new List<T>();

            if (this._list.Count() == 0)
                throw new InvalidOperationException("List cannot empty.");
            
            return this.WhereImpl(WhereClause);
        }

        private IEnumerable<T> WhereImpl(Func<T, bool> WhereClause)
        {
            foreach (var item in this._list)
            {
                Console.WriteLine("Invoke query item: {0}", item);

                if (WhereClause(item))
                    yield return item;
            }
        }

        public IEnumerable<T> ToList()
        {
            IEnumerator<T> cursor = this._list.GetEnumerator();
            List<T> itemStore = new List<T>();

            if (this._list.Count() == 0)
                return new List<T>();

            while (cursor.MoveNext())
            {
                itemStore.Add(cursor.Current);
            }

            return itemStore;
        }
    }
}

Reference:

http://msmvps.com/blogs/jon_skeet/archive/2010/09/03/reimplementing-linq-to-objects-part-2-quot-where-quot.aspx

http://csharpindepth.com/Articles/Chapter6/IteratorBlockImplementation.aspx

 


DotBlogs Tags: LINQ Deferred Execution

關連文章

[Programming] 編譯 vs 直譯

[快訊] Windows 8, Windows Server 8, Visual Studio 11 Beta 發表~

[.NET][Office] 使用 Word 2010 在 Server 端將 DOC/DOCX 轉換成 PDF

[.NET] Fluent Interface: 實作 Method Chaining 又不會有耦合性的作法

回應

  • # re: [.NET] LINQ 的延遲執行 (Deferred Execution) by 91

    比我專業太多了 XD

    我看Reference那一篇,一路看到編譯器就卡關了...

    2012/1/12 下午 03:12 | 回覆

  • # re: [.NET] LINQ 的延遲執行 (Deferred Execution) by 91

    補充一下,要了解LINQ的延遲執行,小朱大這篇完整、清楚太多了!

    我那篇只是自己想要做出『類似延遲執行』的效果...所以才會想說用委派來暫存所有要做的事 :P  (好孩子不要隨便學唷~)

    2012/1/12 下午 03:54 | 回覆

  • # re: [.NET] LINQ 的延遲執行 (Deferred Execution) by gelis

    不可多得的好文

    小朱真的造福太多人了

    真的是由衷的感謝~^________^

    2012/1/12 下午 11:40 | 回覆

  • # re: [.NET] LINQ 的延遲執行 (Deferred Execution) by QQ

    有個小陷阱喔, 小朱提供的上半部程式碼似乎與Reflector所呈現的反編譯代碼似乎不一致喔。因為C#自動產生的狀態機是由yield IEnumerable機制所產生的(完整版的程式碼裡面才有用到),要不要修正一下

    2012/1/16 下午 03:27 | 回覆

  • # re: [.NET] LINQ 的延遲執行 (Deferred Execution) by 小朱

    to QQ :

    沒有必要修改,因為它既然是由 yield 產生的,那不管程式是長怎樣,行為都不會變,只要簡單寫一個 yield return IEnumeable<T> 的程式再用 Reflector 看都有相似的結果,所以和程式長怎樣沒有什麼直接的關係。

    2012/1/16 下午 05:52 | 回覆

登入後使用進階評論

Please add 5 and 6 and type the answer here: