[.NET]延遲執行(Deferred Execution) 簡單概念實作

  • 10328
  • 0

[.NET]延遲執行(Deferred Execution) 簡單概念實作

前言
最近迷上了研究LINQ底層實作的東西,希望自己可以不透過反組譯,從LINQ可以達到的功能以及一些特性,去反思如果是自己設計一套這樣的framework,該怎麼著手。

這兩天則與小朱和Bill叔討論到LINQ to Object裡面的延遲執行查詢(請參考:LINQ 查詢簡介 (C#)裡面的查詢執行)如果要自己設計,該怎麼做。這實在是個很有趣的議題,從目的、委派、編譯器到覆寫operator,一堆天馬行空的想法,就是想自己組出一樣的效果。(說到這,要更佩服Bill叔的熱忱,我們下班的時候還用手機在講可以用哪些東西,可能是用哪些東西來組合成『延遲執行』的想法,晚上睡覺前Bill叔就寫出一版雛形來印證想法了,害我躺在床上睡不著,爬起來把我腦袋中的想法,寫成sample code)

概念
我自己想了幾個簡單的概念,或許可以當作一個最陽春的延遲執行的基底。

  1. 使用委派:要延遲執行,基本需求描述就是『等到需要執行的時候,把剛剛要跑的一大串東西,做一次跑完』,這代表著『呼叫某個要延遲執行的function時,要把這個方法內容暫存起來』,第一個想法,當然就是使用委派。
  2. 使用一個集合,來存委派跟參數集合
  3. 使用Action<T>,來當作function的暫存轉接。
  4. 每個function會回傳自己這個instance回去,以達到LINQ裡面的function chain的味道。
  5. 當被實際執行後,要把剛剛暫存的delegate集合清空。(如果稱做expression tree,會不會更專業一點 ^___^)


簡單實作

  1. 先針對單一型別來設計,這邊舉例的是int,未來進階版就可以改成泛型。(例如IEnumerable<T>裡面的T)
  2. 針對單數類別設計,先用單純的方式設計,未來進階版就可以改成集合。(例如IEnumerable<T>的IEnumerable)
  3. 設計兩個function,分別為Add(int), Minus(int),當呼叫這兩個function時並不直接執行與回傳結果。
  4. 當呼叫InvokeExpression()時,才將需要執行的Add()與Minus()做一次執行。


接下來我們來看程式碼,這邊的例子是設計一個類別叫做Joey:

 


    public class Joey
    {
        struct MyPair
        {
            public Delegate expression;
            public object[] parameters;
        }

        private List<MyPair> myExpression = new List<MyPair>();

        public int MyValue { get; private set; }
        public Joey(int initialValue)
        {
            this.MyValue = initialValue;
        }

        public Joey Add(int value)
        {
            Action<int> mydelegate = (x) =>
            {
                this.MyValue += x;
                Console.Write(" + {0}", x.ToString());
            };

            var o = new MyPair { expression = mydelegate, parameters = new object[] { value } };

            this.myExpression.Add(o);

            return this;
        }

        public Joey Minus(int value)
        {
            Action<int> mydelegate = (x) =>
            {
                this.MyValue -= x;
                Console.Write(" - {0}", x.ToString());
            };

            var o = new MyPair { expression = mydelegate, parameters = new object[] { value } };

            this.myExpression.Add(o);

            return this;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        /// <remarks>可改為event trigger</remarks>
        public int InvokeExpression()
        {
            Console.Write(this.MyValue.ToString());
            foreach (var item in this.myExpression)
            {
                item.expression.DynamicInvoke(item.parameters);
            }

            //清空
            this.myExpression.Clear();

            Console.WriteLine();
            return this.MyValue;
        }
    }

程式碼其實頗單純,都只是上面提到的簡單概念應用。內容說明如下:

  1. struct MyPair:用來存放每一次的Delegate,以及該Delegate所需參數的結構。
  2. private List<MyPair> myExpression:上面提到的暫存Delegate的集合。
  3. MyValue property與建構式:給外界取用的結果值,在建構式時,給一個初始值。透過這個property我們可以判別Expression到底實際執行了沒。
  4. Add()與Minus():使用Action<int>來當作該function的『影子』,也就是把這次實際要執行的function內容,用Action<T>暫存起來,並加入整個instance之後要執行的Expression集合中。
  5. InvokeExpression():把剛剛暫存的Expression集合,一個一個叫出來配合參數調用委派。調用完後清除Expression集合,避免影響到這個instance之後其他的Expression。


使用場景範例


    class Program
    {
        static void Main(string[] args)
        {
            var joey = new Joey(1);

            var o = joey.Add(2).Add(3).Add(4).Minus(5);

            var resultWithoutInvoke = o.MyValue;
            Console.WriteLine("尚未執行的結果:{0}", resultWithoutInvoke);

            o.InvokeExpression();

            var result = o.MyValue;
            Console.WriteLine("執行後的結果:{0}", result);

            Console.ReadKey();
            Console.WriteLine();

            var o2 = o.Add(20).Add(30).Add(40).Minus(50);

            var resultWithoutInvoke2 = o2.MyValue;
            Console.WriteLine("尚未執行第二次前的結果:{0}", resultWithoutInvoke2);

            o2.InvokeExpression();

            var result2 = o2.MyValue;
            Console.WriteLine("第二次執行後的結果:{0}", result2);
            Console.WriteLine();
        }
    }
  1. 一開始給初始值1。
  2. 呼叫Add(2), Add(3), Add(4), Minus(5)後,可以發現MyValue的值,仍然是1,沒有改變。
  3. 呼叫InvokeExpression(),可以看到MyValue的值變成5了,且畫面有呈現出每一個Expression執行的過程。
  4. 當已經被呼叫過一次InvokeExpression(),接著我們在用剛剛的instance,呼叫Add(20), Add(30), Add(40), Minus(50),呼叫完後,檢查值仍為第一次結果的值:5。
  5. 再呼叫一次InvokeExpression(),檢查結果,如同預期的45。


執行結果畫面
image

結論
這只是最簡單的概念,也不會是最佳化的設計。但有了這樣的基底和概念,之後還有很多東西可以玩,例如:

  1. 怎麼把int改成泛型
  2. 怎麼把Joey改成集合
  3. 怎麼把Joey改成介面
  4. 怎麼把方法改成擴充方法
  5. invoke的方法怎麼透過event去trigger發動
  6. 怎麼把expression集合,改成expression tree或其他更好、更彈性的資料結構
  7. 怎麼把expression改成由資料發動(用資料寫程式,用程式寫資料)
  8. invoke的方式是否有調整空間,例如遞迴,MapFunction之類的方式。


原本想寫的系列文,是用自己的方式建構出LINQ上面那堆美妙的API,透過那樣的過程來讓自己訓練怎麼從最基本的元素組成這麼漂亮的framework。

不過老狗叔一個link就把我打回原形了,C# in Depth的作者已經寫了這個系列了,請參考:Reimplementing LINQ to Objects: Part 45 - Conclusion and List of Posts,看了幾篇更覺得自己是水井國民啊…大概對每一個function的基本概念都對,但是其他要考量的點真的是多到不行,例如這篇文章介紹的『延遲執行』。

我不認為我幾年內,能寫出像Jon Skeet這麼好、這麼細、這麼深的書或文章,所以還是快樂的當個寄生蟲學習者,仔細的咀嚼大師們嘔心瀝血之作吧。不過,看這種文章可以一直給腦袋的想法帶來衝擊,那種衝擊感真的挺過癮的…

Sample Project:DeferredExecution.zip


blog 與課程更新內容,請前往新站位置:http://tdd.best/