[.NET]延遲執行(Deferred Execution) 簡單概念實作
前言
最近迷上了研究LINQ底層實作的東西,希望自己可以不透過反組譯,從LINQ可以達到的功能以及一些特性,去反思如果是自己設計一套這樣的framework,該怎麼著手。
這兩天則與小朱和Bill叔討論到LINQ to Object裡面的延遲執行查詢(請參考:LINQ 查詢簡介 (C#)裡面的查詢執行)如果要自己設計,該怎麼做。這實在是個很有趣的議題,從目的、委派、編譯器到覆寫operator,一堆天馬行空的想法,就是想自己組出一樣的效果。(說到這,要更佩服Bill叔的熱忱,我們下班的時候還用手機在講可以用哪些東西,可能是用哪些東西來組合成『延遲執行』的想法,晚上睡覺前Bill叔就寫出一版雛形來印證想法了,害我躺在床上睡不著,爬起來把我腦袋中的想法,寫成sample code)
概念
我自己想了幾個簡單的概念,或許可以當作一個最陽春的延遲執行的基底。
- 使用委派:要延遲執行,基本需求描述就是『等到需要執行的時候,把剛剛要跑的一大串東西,做一次跑完』,這代表著『呼叫某個要延遲執行的function時,要把這個方法內容暫存起來』,第一個想法,當然就是使用委派。
- 使用一個集合,來存委派跟參數集合
- 使用Action<T>,來當作function的暫存轉接。
- 每個function會回傳自己這個instance回去,以達到LINQ裡面的function chain的味道。
- 當被實際執行後,要把剛剛暫存的delegate集合清空。(如果稱做expression tree,會不會更專業一點 ^___^)
簡單實作
- 先針對單一型別來設計,這邊舉例的是int,未來進階版就可以改成泛型。(例如IEnumerable<T>裡面的T)
- 針對單數類別設計,先用單純的方式設計,未來進階版就可以改成集合。(例如IEnumerable<T>的IEnumerable)
- 設計兩個function,分別為Add(int), Minus(int),當呼叫這兩個function時並不直接執行與回傳結果。
- 當呼叫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;
}
}
程式碼其實頗單純,都只是上面提到的簡單概念應用。內容說明如下:
- struct MyPair:用來存放每一次的Delegate,以及該Delegate所需參數的結構。
- private List<MyPair> myExpression:上面提到的暫存Delegate的集合。
- MyValue property與建構式:給外界取用的結果值,在建構式時,給一個初始值。透過這個property我們可以判別Expression到底實際執行了沒。
- Add()與Minus():使用Action<int>來當作該function的『影子』,也就是把這次實際要執行的function內容,用Action<T>暫存起來,並加入整個instance之後要執行的Expression集合中。
- 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。
- 呼叫Add(2), Add(3), Add(4), Minus(5)後,可以發現MyValue的值,仍然是1,沒有改變。
- 呼叫InvokeExpression(),可以看到MyValue的值變成5了,且畫面有呈現出每一個Expression執行的過程。
- 當已經被呼叫過一次InvokeExpression(),接著我們在用剛剛的instance,呼叫Add(20), Add(30), Add(40), Minus(50),呼叫完後,檢查值仍為第一次結果的值:5。
- 再呼叫一次InvokeExpression(),檢查結果,如同預期的45。
執行結果畫面
結論
這只是最簡單的概念,也不會是最佳化的設計。但有了這樣的基底和概念,之後還有很多東西可以玩,例如:
- 怎麼把int改成泛型
- 怎麼把Joey改成集合
- 怎麼把Joey改成介面
- 怎麼把方法改成擴充方法
- invoke的方法怎麼透過event去trigger發動
- 怎麼把expression集合,改成expression tree或其他更好、更彈性的資料結構
- 怎麼把expression改成由資料發動(用資料寫程式,用程式寫資料)
- 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/