在.net中,委託,匿名方法和Lambda運算式是三個很容易讓人混淆的概念.以下代碼或許可見一斑:對First的調用中,哪些(個)會被編譯?哪些(個)將會返回我們所期待的答案?(ID號為5的Customer).事實上,答案就是:所有的6種方法不令都將編譯,而且它們都能夠返回正常的customer,它們在功能上是相同的.如果你還在問自己:為什麼是這樣呢?那麼,這篇文章將為你解答.
class Customer
{
public int ID { get; set; }
public static bool Test(Customer x)
{
return x.ID == 5;
}
}
...
List<Customer> custs = new List<Customer>();
custs.Add(new Customer() { ID = 1 });
custs.Add(new Customer() { ID = 5 });
custs.First(new Func<Customer, bool>(delegate(Customer x) { return x.ID == 5; }));
custs.First(new Func<Customer, bool>((Customer x) => x.ID == 5));
custs.First(delegate(Customer x) { return x.ID == 5; });
custs.First((Customer x) => x.ID == 5);
custs.First(x => x.ID == 5);
custs.First(Customer.Test);
一,什麼是委託?
舉例來說吧,假如有一個購物籃類(Shoppingcart)用於處理顧客(Customer)的訂單(Order).經理決定對超過一定金額或購買量等的顧客打折.於是,你必須使用他們制訂的策略來計算訂單.這並不是什麼難事:你簡單的聲明了一個變數來儲存折扣將在計算訂單金額時調用它.
class Program { static void Main(string[] args) { new ShoppingCart().Process(); } } class ShoppingCart { public void Process() { int magicDiscount = 5; // ... }}
然而,第二天,精明的經理又決定要根據一天當中的時間來打折.呵呵,這也不難,你只需要簡單的改變一下代碼就可以了:
class ShoppingCart { public void Process() { int magicDiscount = 5; if (DateTime.Now.Hour < 12) { magicDiscount = 10; } } }
接下來幾天裡,經理一再增加或修改折扣的計算方法.暈倒!我怎麼才能處理和維護這近似荒謬的邏輯呀?其實,你所需要做的只是"移交",或者稱之為"委託"責任給別人就可以了.在.net中,就有這麼一種機制,我不說你也一定猜到了,那就是"委託".
二,委託
如果大家有C/C++背景的話,那麼最佳描述委託的就是函數指標.而對於一般人來說,可以認為它是一種把方法像普通參數一樣傳遞的方式.例如,以下三行代碼體現了同樣的基本原則:向Process方法
傳遞一條資料,而不使用它.
// passing an integer value for the Process method to use Process( 5 ); // passing a reference to an ArrayList object for the Process method to use Process( new ArrayList() ); // passing a method reference for the Process method to call Process( discountDelegate );
那麼,上面的discountDelegate是什麼呢?我們該怎麼去建立它呢?Process方法又是怎樣使用它的呢?首先,我們需要做的是聲明一個delegate類型就像我們聲明一個類一樣.
delegate int DiscountDelegate();
這名代碼讓我們擁有了一個DiscountDelegate委託,我們可以像類和結構一樣使用它.這個委託沒有參數,只有一個int型的傳回值.和類一樣,在建立它的任何執行個體之前,我們不能使用它.在執行個體化委託的時候,需要特別注意:委託只是某方法的一個引用而已,即使DiscountDelegate沒有任何建構函式,當執行個體化的時候,會有一個隱藏的建構函式(無參數但返回int值).怎樣給這個建構函式一個方法呢?OK,在.net中,我們只需要簡單的輸入想要調用的方法的名稱就可以了,省略了方法名後面的括弧.
DiscountDelegate discount = new DiscountDelegate(class.method);
在進一步探討之前,讓我們回過頭來看看之前的例子.我們將增加一個Calculator
類來協助我們計算折扣,並設計一些方法來提供給委託引用.
delegate int DiscountDelegate(); class Program { static void Main(string[] args) { Calculator calc = new Calculator(); DiscountDelegate discount = null; if (DateTime.Now.Hour < 12) { discount = new DiscountDelegate(calc.Morning); } else if (DateTime.Now.Hour < 20) { discount = new DiscountDelegate(calc.Afternoon); } else { discount = new DiscountDelegate(calc.Night); } new ShoppingCart().Process(discount); } } class Calculator { public int Morning() { return 5; } public int Afternoon() { return 10; } public int Night() { return 15; } } class ShoppingCart { public void Process(DiscountDelegate discount) { int magicDiscount = discount(); // ... } }
正如我們所看到的,我們在Calculator
類中為每一個折扣的計算邏輯建立了一個方法.在Main方法中,我們為Calculator類和DiscountDelegate
委拖分別建立了一個執行個體,它們將共同為我們即將調用的目標方法服務.
到目前為止,我們不再為Process
方法裡的計算邏輯而擔憂啦,我們可以簡單的調用委託來完成.不過要記住:我們並不關心這個委託是如何,何時建立的.我們只是在需要它的時候像調用其它方法一樣調用它.可見,委託也可以理解為延緩方法執行.真正的計算方法是在我們調用discount()方法的時候才得以執行.再看看上面的代碼,還有一些重複的地方.在Calculator
類中,我們是不是可以用一個方法來提供所有傳回值?當然可以,那就讓我們一起來改進吧.
delegate int DiscountDelegate(); class Program { static void Main(string[] args) { new ShoppingCart().Process(new DiscountDelegate(Calculator.Calculate)); } } class Calculator { public static int Calculate() { int discount = 0; if (DateTime.Now.Hour < 12) { discount = 5; } else if (DateTime.Now.Hour < 20) { discount = 10; } else { discount = 15; } return discount; } } class ShoppingCart { public void Process(DiscountDelegate discount) { int magicDiscount = discount(); // ... } }
這下好啦,我們做到啦.靜態Calculate
方法讓類變得清爽了很多.Main方法也不再有那麼多的對DiscountDelegate
的引用啦.好,現在,我們已經對委託有所瞭解了.
三,我們需要這樣的功能!
在.NET 2.0中,引入了泛型的概念.MS小心翼翼的通過提供Action<T>類來邁向泛型委拖.然後,我認為,一段時間之後,它被我們大多數人所遺忘了.後來,到了3.5,MS很友好的為我們預定義了一些常見的委託來使用,所以我們不再需要不斷的定義是我們自己的委託啦.他們還擴充了Action並增加了Func.Action和Func唯一的不同就是前者沒傳回值而後者有.
也就是說,我們不再需要去聲明DiscountDelegate
,我們可以使用Func<int>
來代替它.為們示範參數的工作方法,讓我們假設經理再一次改變了折扣的計算邏輯:現在,我們需要記錄一個特別的折扣.這也是小菜一碟,我們只需要在Calculate
方法中調用一個boolean類型的值就可以啦.
這樣,我們的原本無參委託就變成了Func<bool, int>的形式了.注意,現在的Calculate
方法有一個boolean類型的參數,我們在調用discount
的時候要帶上它.
class Program { static void Main(string[] args) { new ShoppingCart().Process(new Func<bool, int>(Calculator.Calculate)); } } class Calculator { public static int Calculate(bool special) { int discount = 0; if (DateTime.Now.Hour < 12) { discount = 5; } else if (DateTime.Now.Hour < 20) { discount = 10; } else if (special) { discount = 20; } else { discount = 15; } return discount; } } class ShoppingCart { public void Process(Func<bool,int> discount) { int magicDiscount = discount(false); int magicDiscount2 = discount(true); } }
這主意不錯,我們又節省了幾行代碼,不是嗎?當然不是,如果利用類型介面,我們還可以節省更多的代碼和時間..net允許我們完全省略Func<bool, int>的建立過程,只要這個方法具有和我們期望的委託同樣的參數和傳回值.
// works because Process expects a method that takes a bool and returns int new ShoppingCart().Process(Calculator.Calculate);
基於這一點,我們通過省略自訂委託並在隨後略去Func委託的直接建立,又省下了不少代碼.還有什麼方法可以減少代碼量的嗎?當然有,因為我們才說了本文的一半.
四,匿名方法
匿名方法允許我們聲明一個沒有名字的方法.在後台,有一個名叫'normal'的方法.然而,在代碼中我們不能顯式的調用這個方法,匿名方法只能在使用委託,並且使用Delegate關鍵字建立時建立和使用.如:
class Program { static void Main(string[] args) { new ShoppingCart().Process( new Func<bool, int>(delegate(bool x) { return x ? 10 : 5; } )); } }
從上面我們可以看到:我們完全不用Calculator
類.你可以輸入更多的小邏輯到花括弧的分枝中,就像在其它方法中使用的一樣.如果你覺得難以理解這段代碼的工作過程,就請假設聲明delegate(bool x)
是一個方法的簽名而不是一個委託的關鍵字.把這段代碼放到一個類中,delegate(bool x) { return 5; }
是一個特別的邏輯計算的方法的聲明(本應有傳回值類型的).這方面來理解,就好辦啦.delegate
只是使原本方法的名稱隱藏起來了而已.
好,我相信到目前為止,我們可以省略更多的代碼啦.自然的,我們可以忽略對Func委託的顯示聲明,當我們使用delegate關鍵字的時候,.net會替我們處理好一切.
class Program { static void Main(string[] args) { new ShoppingCart().Process( delegate(bool x) { return x ? 10 : 5; } ); } }
在.net中,當方法期望一個委託作為參數並響應事件的時候,才能看見匿名方法的真正威力.以前,我們必須為每一個可能的動作建立方法.而現在,我們只需要一行語句就搞定了,而且不會"汙染"命名空間.
// creates an anonymous comparer custs.Sort(delegate(Customer c1, Customer c2) { return Comparer<int>.Default.Compare(c1.ID, c2.ID); }); // creates an anonymous event handler button1.Click += delegate(object o, EventArgs e) { MessageBox.Show("Click!"); };
五,Lambda運算式(引自MSDN)Lambda運算式是一個能夠包含運算式和語句,並且能夠用來建立委託或運算式樹狀架構類型的匿名函數.我們需要理解"用來建立委託"部分,Lambda運算式到底扮演著怎麼的著色呢?好,其實上,運算式和運算式樹狀架構已超出了本方的闡述範圍.我們只需要知道:運算式就是.net程式中所謂的資料或對象本身的代碼而已.運算式樹狀架構是一種其它代碼可以"詢問"的表達邏輯的方式.當lambda 運算式被轉換成運算式樹狀架構時,編輯器並沒產生新的IL代碼,運算式樹狀架構和lambda 運算式代表了相同的邏輯.
我們所需要關注的是:用lambda 運算式代替匿名方法並增加新的特性.回顧我們最後的那個例子,我們已經在最初建立的基礎上去掉了很多代碼,將把折扣計算邏輯寫到了一行上.
class Program { static void Main(string[] args) { new ShoppingCart().Process( delegate(bool x) { return x ? 10 : 5; } ); }}
你相信我們還能讓它變得更簡短一些嗎?Lambda 運算式使用"=>"符號來代表"參數被傳遞到運算式中",編譯器遇到這個符號的時候會允許我們忽略參數的類型,並且為我們推斷出類型來.如果有兩個或兩個以上的參數,可以使用括弧:(x,y) =>
. 如果只有一個,那就這樣:x =>
.
static void Main(string[] args) { Func<bool, int> del = x => x ? 10 : 5; new ShoppingCart().Process(del); } // even shorter... static void Main(string[] args) { new ShoppingCart().Process(x => x ? 10 : 5); }
對,就是這樣的.x被推斷成boolean,傳回值類型也一樣.因為Process
有一個Func<bool, int>類型的參數,如果你想像前面一樣執行這段代碼,我們只需要增加分枝就可以了.
static void Main(string[] args) { new ShoppingCart().Process( x => { int discount = 0; if (DateTime.Now.Hour < 12) { discount = 5; } else if (DateTime.Now.Hour < 20) { discount = 10; } else if(x) { discount = 20; } else { discount = 15; } return discount; }); }
六,最後的事
在是否使用分枝的問題上,一個很大的不同.當你使用的時候,相當於是建立了一個"語句式的lambda'",反之,它就是一個"運算式式的lambda",前者可以根據分枝需要執行多行語句,但不能建立運算式樹狀架構.當我們在使用IQueryable
介面的時候,有可能會陷入這個問題中, 下面的例子展示了這一問題"
List<string> list = new List<string>(); IQueryable<string> query = list.AsQueryable(); list.Add("one"); list.Add("two"); list.Add("three"); string foo = list.First(x => x.EndsWith("o")); string bar = query.First(x => x.EndsWith("o")); // foo and bar are now both 'two' as expected foo = list.First(x => { return x.EndsWith("e"); }); //no error bar = query.First(x => { return x.EndsWith("e"); }); //error bar = query.First((Func<string,bool>)(x => { return x.EndsWith("e"); })); //no error
第二次計算bar值的時候發生了編輯時錯誤.這是因為IQueryable.First
期望一個運算式做為參數,然而,運算式方法List<T>.First
則期望一個委託.你可以強制將lambda 運算式的值轉換為delegate,像上面第三次一樣.
寫到這裡,其實還有很多未言的東西,但是我想我必須要結束本文了.lambda 基本上被分為兩類:一類是建立匿名方法和委託,另一類是建立運算式.
七,總結
希望這編文章可以達到解答最前面的六個易混淆的有關委託,匿名方法和lambda運算式調用問題的目標.
<完>