C#中的委託,匿名方法和Lambda表示式

iDotNetSpace發表於2010-10-08
在.NET中,委託,匿名方法和Lambda表示式很容易發生混淆。我想下面的程式碼能證實這點。下面哪一個First會被編譯?哪一個會返回我們需要的結果?即Customer.ID=5.答案是6個First不僅被編譯,並都獲得正確答案,且他們的結果一樣。如果你對此感到困惑,那麼請繼續看這篇文章。
01 class Customer 
02
03     public int ID { get; set; } 
04     public static bool Test(Customer x) 
05     { 
06         return x.ID == 5; 
07     } 
08
09 ... 
10 List custs = new List(); 
11 custs.Add(new Customer() { ID = 1 }); 
12 custs.Add(new Customer() { ID = 5 }); 
13    
14 custs.First(new Funcbool>(delegate(Customer x) { return x.ID == 5; })); 
15 custs.First(new Funcbool>((Customer x) => x.ID == 5)); 
16 custs.First(delegate(Customer x) { return x.ID == 5; }); 
17 custs.First((Customer x) => x.ID == 5); 
18 custs.First(x => x.ID == 5); 
19 custs.First(Customer.Test);

什麼是委託?

  現在你定義一個處理使用者訂單的購物車ShoppingCart類。管理層決定根據數量,價格等給客人折扣。做為其中的一部分,他們已經實現了處理訂單時你要考慮一方面。不用考慮過多,你簡單宣告一個變數來儲存有“吸引力的折扣”(magicDisCount),然後實現邏輯。

01 class Program { 
02     static void Main(string[] args)  { 
03         new ShoppingCart().Process(); 
04     } 
05 }  
06 class ShoppingCart { 
07     public void Process() { 
08         int magicDiscount = 5; 
09         // ... 
10     }
11 }

  第二天,異想天開的管理層決定根據購買時間調整折扣。這個很簡單,但需要你改動一點程式碼。

1 class ShoppingCart { 
2     public void Process() { 
3         int magicDiscount = 5; 
4         if (DateTime.Now.Hour < 12) { 
5             magicDiscount = 10; 
6         } 
7     } 
8 }

  接下來一段時間裡,管理層又反覆新增更多的折扣邏輯。這時你就會在心理抱怨“受夠了”。那麼我該怎麼做才能把這些無聊的邏輯從我的程式碼中剝離出去,讓該處理的人去處理呢?這時你要做的是移交或者委派給相應職能的別人。幸運的是,.NET為此提供了一種叫做“委託”的機制。

委託

  如果你有C/C++程式設計背景,描述委託最好的方法是“函式指標”。對所有人來說,可以認為把委託傳遞給方法與把值或物件傳遞給方法一樣。比如下面三行程式碼就表現出一樣的基本原則:你在傳遞資料給Process處理而不是你自己使用。

1 // 給方法Process傳遞一個整形值
2 Process( 5 ); 
3 // 給方法Process傳遞一個ArrayList的引用
4 Process( new ArrayList() ); 
5 // 給方法Process傳遞一個方法的引用
6 Process( discountDelegate );

  DiscountDelegate是什麼?我如何建立?Process方法如何使用?首先如同宣告一個類一樣,宣告一個委託型別。

1 delegate int DiscountDelegate();
  這句話的意思是我們有一個叫DiscountDelegate的委託型別,我們可以像使用類,結構體等一樣使用它。它不需要資料引數,但返回一個整數值。像類一樣,我們必須建立一個它的例項它才有意義。記住,建立一個委託例項實質上是建立一個方法的引用。建立例項時關鍵是要明白DiscountDelegate沒有任何構造器,它有一個隱式的建構函式來構造一個與它相同簽名的方法(沒有傳入引數,返回一個整數)。那你怎麼給這個建構函式一個方法呢?.NET向你提供了一個向它名字一樣簡單的方法,你所做的只是忽略圓括號。
1DiscountDelegate discount = new DiscountDelegate(class.method);
  在深入之前,先回到開始的例子,整理一個程式碼。我們會新增一個Calculator類來幫助我們處理折扣邏輯,並給我們的委託提供一些方法。
01delegate int DiscountDelegate(); 
02   
03class Program { 
04    static void Main(string[] args) { 
05        Calculator calc = new Calculator(); 
06        DiscountDelegate discount = null
07        if (DateTime.Now.Hour < 12) { 
08            discount = new DiscountDelegate(calc.Morning); 
09        } 
10        else if (DateTime.Now.Hour < 20) { 
11            discount = new DiscountDelegate(calc.Afternoon); 
12        } 
13        else
14            discount = new DiscountDelegate(calc.Night); 
15        } 
16        new ShoppingCart().Process(discount); 
17    } 
18}  
19class Calculator { 
20    public int Morning() { 
21        return 5; 
22    } 
23    public int Afternoon() { 
24        return 10; 
25    } 
26    public int Night() { 
27        return 15; 
28    } 
29}  
30class ShoppingCart { 
31    public void Process(DiscountDelegate discount) { 
32        int magicDiscount = discount(); 
33        // ... 
34    } 
35}

  正如你所見,在Calculator類中,我們為每個邏輯分支建立了一個方法。在Main方法中,我們建立一個Calculator例項和一個DiscountDelegate例項,並按照我們所期望的把它們整合在一起。

  太棒了,我們不用擔心Process方法中的邏輯了,我們只需要簡單得回撥我們定義的委託。記住!我們不關心委託是如何建立的(或什麼時間),我們就像呼叫其他方法一樣呼叫它。如你所見,另一種理解委託的方法是,它延遲執行一個方法。Calculator方法在過去某個時間本選擇,但不會執行,直到我們呼叫discount()的時候。現在看看我們的解決方案,這裡仍然存在一些醜陋的程式碼。在Calculator類中,我們可以用一個不同的方法來返回替代每個有返回值得方法嗎?答案是肯定的,讓我們把這些亂糟糟的程式碼合併起來。

01delegate int DiscountDelegate(); 
02   
03class Program { 
04    static void Main(string[] args) { 
05        new ShoppingCart().Process(new DiscountDelegate(Calculator.Calculate)); 
06    } 
07}  
08class Calculator { 
09    public static int Calculate() { 
10        int discount = 0; 
11        if (DateTime.Now.Hour < 12) { 
12            discount = 5; 
13        } 
14        else if (DateTime.Now.Hour < 20) { 
15            discount = 10; 
16        } 
17        else
18            discount = 15; 
19        } 
20        return discount; 
21    } 
22}  
23class ShoppingCart { 
24    public void Process(DiscountDelegate discount) { 
25        int magicDiscount = discount(); 
26         // ... 
27    } 
28}

  這樣子看起來更好點。你會注意到我們用一個靜態的Calculate方法替換了所有原來的方法,在Main方法中也不用費心維護一個指向DiscountDelegate的引用。現在你明白了所有關於委託的東西了嗎?在2004年.NET1.1中可以這麼說,但是很不幸的是,這種框架自那以後更加成熟了。

燈光,鏡頭,開始 或者我們需要Func!

  微軟在.NET 2.0中引入了泛型,並提供了一個泛型委託:Action。老實說,我認為它遠不夠用。後來在.NET 3.5中,它為我們提供了一些我們不想定義的通用委託。他們擴充套件了Action,並新增了Func,二者唯一區別在於Func型方法有一個返回值而Action型方法沒有。

  這意味著我們不需要宣告自己的DiscountDelegate,可以用Func替代。為說明這些觀點是如何工作的,我們來假設管理層又一次改變了我們的邏輯,我們需要提供一些特殊的折扣。很簡單,我們將給Calculate方法傳入一個bool型值。

  現在我們的委託簽名變成Func。注意Calculate方法現在包含一個bool型引數,我們用一個bool值呼叫discount()。

01class Program { 
02    static void Main(string[] args) { 
03        new ShoppingCart().Process(new Func<bool, int>(Calculator.Calculate)); 
04    } 
05
06   
07class Calculator { 
08    public static int Calculate(bool special) { 
09        int discount = 0; 
10        if (DateTime.Now.Hour < 12) { 
11            discount = 5; 
12        } 
13        else if (DateTime.Now.Hour < 20) { 
14            discount = 10; 
15        } 
16        else if (special) { 
17            discount = 20; 
18        } 
19        else
20            discount = 15; 
21        } 
22        return discount; 
23    } 
24
25   
26class ShoppingCart { 
27    public void Process(Func<bool,int> discount) { 
28        int magicDiscount = discount(false); 
29        int magicDiscount2 = discount(true); 
30    } 
31}

  好像還算不錯,我們又省了一行程式碼,這樣算結束了嗎?當然沒有,我們甚至能省掉型別判斷。只要我們傳遞的方法有嚴格簽名的委託,.NET允許我們完全忽略掉顯式建立Func

1//因為Process期望的方法有一個bool型輸入引數和返回一個int值,所以下面這句話是正確的
2new ShoppingCart().Process(Calculator.Calculate);

  至此,首先通過忽略自定義委託,我們省略了程式碼;然後排出了明確的建立Func委託。我們能繼續壓縮程式碼行嗎?到此我們才完成此文的一半,答案顯然是“能”。

 匿名方法

  匿名方法能夠讓你宣告一個方法體而不需要給它指定一個名字。在接下來的場景裡,它們以一個“普通的”方法存在;但是在你的程式碼中沒有任何方法顯式呼叫它。匿名方法只能在使用委託的時候建立,事實上,它們通過delegate關鍵字建立。

1class Program { 
2    static void Main(string[] args)     { 
3        new ShoppingCart().Process( 
4            new Func<bool, int>(delegate(bool x) { return x ? 10 : 5; } 
5        )); 
6    } 
7}

  正如你所見,我們完全刪除了Calculator類的需求。你可以在打括號中新增任何其他方法中的邏輯。如果你在看它如何執行時有困難,那就把delegate(bool x)做為一個方法簽名,而不是一個關鍵字。設想這段程式碼在一個類裡,delegate(bool x){return 5;}是一個完整的合法方法宣告(我們確實有一個返回值),恰好delegate是一個保留字,在這裡,它讓這個方法匿名。

  至此,我確信現在你知道這裡我們甚至能壓縮更多的程式碼。順利成章的,我們能忽略顯式宣告Func委託的需要;.NET讓我們使用delegate關鍵字更方便。

1class Program { 
2    static void Main(string[] args) { 
3        new ShoppingCart().Process( 
4          delegate(bool x) { return x ? 10 : 5; } 
5        ); 
6    } 
7}

  當把.NET方法做為委託引數時或處理時間時,就能看到匿名方法的真正用處。之前,你會為你所關注的所有可能行為建立了一個方法,現在你僅需以內聯的方式建立它們,並可以避免汙染你的名稱空間。

1// 建立一個匿名比對方法
2custs.Sort(delegate(Customer c1, Customer c2) { 
3    return Comparer<int>.Default.Compare(c1.ID, c2.ID); 
4}); 
5   
6//  建立一個匿名事件
7button1.Click += delegate(object o, EventArgs e) { MessageBox.Show("Click!"); };

Lambda 表示式

  MSDN中寫道:“Lambda 表示式”是一個匿名函式,它可以包含表示式和語句,並且可用於建立委託或表示式樹型別。你應當明白“使用者建立委託”部分,但什麼是“表示式”呢?老實說,表示式和表示式樹不在此為討論範圍內。現在我們唯一需要明白的是,表示式是.NET程式執行時表示資料或物件的程式碼(C#程式碼)。引用Jon Skeet的話:“表示式樹是一種表達邏輯,這樣其他的程式碼可以查詢的方法。當一個lambda表示式轉換成一個表示式樹,編譯器不會發出了lambda表示式的白細胞介素,它會發出白細胞介素這將會建立一個表示式樹表示相同的邏輯。

  我們需要關注的是Lambda表示式替換匿名方法,和其他的特性。回顧我們最後例子,我們已經在一行程式碼裡壓縮了處理整個折扣演算法的邏輯。

1class Program { 
2    static void Main(string[] args) { 
3        new ShoppingCart().Process( 
4            delegate(bool x) { return x ? 10 : 5; } 
5        ); 
6    }
7}

  你相信我們能讓這個更短嗎?Lambda表示式用'=>'運算子表明什麼引數傳遞給表示式。編譯器進一步處理,允許我們忽略型別並自動替我們推斷這些型別。如果你有2個或更多個引數,你需要用圓括號:(x,y)=>。如果只有一個,你設定不需要這樣:x=>。

1static void Main(string[] args) { 
2    Func<bool, int> del = x => x ? 10 : 5; 
3    new ShoppingCart().Process(del); 
4
5// 更短啦...
6static void Main(string[] args) { 
7    new ShoppingCart().Process(x => x ? 10 : 5); 
8}

  就是這樣子。x被推斷為bool型,並且有返回值,因為Process接收一個Func。如果我們想實現像之前那樣的完整程式碼塊,我們只需要加上大括號。

01static void Main(string[] args) { 
02    new ShoppingCart().Process(  x => { 
03        int discount = 0; 
04        if (DateTime.Now.Hour < 12) { 
05            discount = 5; 
06        } 
07        else if (DateTime.Now.Hour < 20) { 
08            discount = 10; 
09        } 
10        else if(x)  { 
11            discount = 20; 
12        } 
13        else
14             discount = 15; 
15        } 
16        return discount; 
17    }); 
18}

寫在最後

  使用與不使用大括號有一個重要的不同。當你用時,你建立一個“語句Lambda”,反之,它是"表達Lambda"。語句Lambda能執行多條語句(因此需要大括號),但不能建立表達樹。你可能只在使用IQueryable介面是遇到這個問題。下面的例子說明這個問題。

01List<string> list = new List<string>(); 
02IQueryable<string> query = list.AsQueryable(); 
03list.Add("one"); 
04list.Add("two"); 
05list.Add("three"); 
06   
07string foo = list.First(x => x.EndsWith("o")); 
08string bar = query.First(x => x.EndsWith("o")); 
09// foo and bar are now both 'two' as expected 
10foo = list.First(x => { return x.EndsWith("e"); }); //no error 
11bar = query.First(x => { return x.EndsWith("e"); }); //error 
12bar = query.First((Func<string,bool>)(x => { return x.EndsWith("e"); })); //no error

  倒數第二行在編譯時失敗。這是因為IQueryable.First期望得到一個表示式作為引數,然而List.First期望得到一個委託。你可以按照最後一行強制轉換Lambda到一個委託(使用First的方法過載)。

  這裡很難結束討論,但是我覺得必須停止。Lambda大體上分為兩類:一類建立匿名方法和委託;另一類建立表示式。表示式自成一體,並不是.NET開發者的必備知識(無疑在LINQ中已有實現)。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-675543/,如需轉載,請註明出處,否則將追究法律責任。

相關文章