天天看點

【轉】【UNITY3D 遊戲開發之七】C# 中的委托、事件、匿名函數、LAMBDA 表達式

      unity3d 開發中,常用的莫過于委托和事件了,是以轉載一篇相關文章,寫的比較詳細的,這裡分享一下。

     對于匿名函數以及lambda表達式也是非常常用的,這裡就直接分享連結,童鞋們自行學習。

【以下内容是介紹委托和事件的,均為轉載内容】

委托 和 事件在 .net framework中的應用非常廣泛,然而,較好地了解委托和事件對很多接觸c#時間不長的人來說并不容易。它們就像是一道檻兒,過了這個檻的人,覺得真是太容易了,而沒有過去的人每次見到委托和事件就覺得心裡别(biè)得慌,混身不自在。本文中,我将通過兩個範例由淺入深地講述什麼是委托、為什麼要使用委托、事件的由來、.net framework中的委托和事件、委托和事件對observer設計模式的意義,對它們的中間代碼也做了讨論。

我們先不管這個标題如何的繞口,也不管委托究竟是個什麼東西,來看下面這兩個最簡單的方法,它們不過是在螢幕上輸出一句問候的話語:

public void greetpeople(string name) {

// 做某些額外的事情,比如初始化之類,此處略

englishgreeting(name);

}

public void englishgreeting(string name) {

console.writeline(“morning, ” + name);

暫且不管這兩個方法有沒有什麼實際意義。greetpeople用于向某人問好,當我們傳遞代表某人姓名的name參數,比如說“jimmy”,進去的時候,在這個方法中,将調用englishgreeting方法,再次傳遞name參數,englishgreeting則用于向螢幕輸出 “morning, jimmy”。

現在假設這個程式需要進行全球化,哎呀,不好了,我是中國人,我不明白“morning”是什麼意思,怎麼辦呢?好吧,我們再加個中文版的問候方法:

public void chinesegreeting(string name){

console.writeline(“早上好, ” + name);

這時候,greetpeople也需要改一改了,不然如何判斷到底用哪個版本的greeting問候方法合适呢?在進行這個之前,我們最好再定義一個枚舉作為判斷的依據:

public enum language{

english, chinese

public void greetpeople(string name, language lang){

//做某些額外的事情,比如初始化之類,此處略

swith(lang){

case language.english:

break;

case language.chinese:

chinesegreeting(name);

ok,盡管這樣解決了問題,但我不說大家也很容易想到,這個解決方案的可擴充性很差,如果日後我們需要再添加韓文版、日文版,就不得不反複修改枚舉和greetpeople()方法,以适應新的需求。

在考慮新的解決方案之前,我們先看看 greetpeople的方法簽名:

public void greetpeople(string name, language lang)

我們僅看 string name,在這裡,string 是參數類型,name 是參數變量,當我們賦給name字元串“jimmy”時,它就代表“jimmy”這個值;當我們賦給它“張子陽”時,它又代表着“張子陽”這個值。然後,我們可以在方法體内對這個name進行其他操作。哎,這簡直是廢話麼,剛學程式就知道了。

如果你再仔細想想,假如greetpeople()方法可以接受一個參數變量,這個變量可以代表另一個方法,當我們給這個變量指派 englishgreeting的時候,它代表着 englsihgreeting() 這個方法;當我們給它指派chinesegreeting 的時候,它又代表着chinesegreeting()方法。我們将這個參數變量命名為 makegreeting,那麼不是可以如同給name指派時一樣,在調用 greetpeople()方法的時候,給這個makegreeting 參數也賦上值麼(chinesegreeting或者englsihgreeting等)?然後,我們在方法體内,也可以像使用别的參數一樣使用makegreeting。但是,由于makegreeting代表着一個方法,它的使用方式應該和它被賦的方法(比如chinesegreeting)是一樣的,比如:

makegreeting(name);

好了,有了思路了,我們現在就來改改greetpeople()方法,那麼它應該是這個樣子了:

public void greetpeople(string name, *** makegreeting){

注意到 *** ,這個位置通常放置的應該是參數的類型,但到目前為止,我們僅僅是想到應該有個可以代表方法的參數,并按這個思路去改寫greetpeople方法,現在就出現了一個大問題:這個代表着方法的makegreeting參數應該是什麼類型的?

note:這裡已不再需要枚舉了,因為在給makegreeting指派的時候動态地決定使用哪個方法,是chinesegreeting還是 englishgreeting,而在這個兩個方法内部,已經對使用“morning”還是“早上好”作了區分。

聰明的你應該已經想到了,現在是委托該出場的時候了,但講述委托之前,我們再看看makegreeting參數所能代表的 chinesegreeting()和englishgreeting()方法的簽名:

public void englishgreeting(string name)

public void chinesegreeting(string name)

如同name可以接受string類型的“true”和“1”,但不能接受bool類型的true和int類型的1一樣。makegreeting的 參數類型定義 應該能夠确定 makegreeting可以代表的方法種類,再進一步講,就是makegreeting可以代表的方法 的 參數類型和傳回類型。

于是,委托出現了:它定義了makegreeting參數所能代表的方法的種類,也就是makegreeting參數的類型。

note:如果上面這句話比較繞口,我把它翻譯成這樣:string 定義了name參數所能代表的值的種類,也就是name參數的類型。

本例中委托的定義:

public delegate void greetingdelegate(string name);

可以與上面englishgreeting()方法的簽名對比一下,除了加入了delegate關鍵字以外,其餘的是不是完全一樣?

現在,讓我們再次改動greetpeople()方法,如下所示:

public void greetpeople(string name, greetingdelegate makegreeting){

如你所見,委托greetingdelegate出現的位置與 string相同,string是一個類型,那麼greetingdelegate應該也是一個類型,或者叫類(class)。但是委托的聲明方式和類卻完全不同,這是怎麼一回事?實際上,委托在編譯的時候确實會編譯成類。因為delegate是一個類,是以在任何可以聲明類的地方都可以聲明委托。更多的内容将在下面講述,現在,請看看這個範例的完整代碼:

using system;

using system.collections.generic;

using system.text;

namespace delegate {

//定義委托,它定義了可以代表的方法的類型

class program {

private static void englishgreeting(string name) {

private static void chinesegreeting(string name) {

//注意此方法,它接受一個greetingdelegate類型的方法作為參數

private static void greetpeople(string name, greetingdelegate makegreeting) {

static void main(string[] args) {

greetpeople(“jimmy zhang”, englishgreeting);

greetpeople(“張子陽”, chinesegreeting);

console.readkey();

輸出如下:

morning, jimmy zhang

早上好, 張子陽

我們現在對委托做一個總結:

委托是一個類,它定義了方法的類型,使得可以将方法當作另一個方法的參數來進行傳遞,這種将方法動态地賦給參數的做法,可以避免在程式中大量使用if-else(switch)語句,同時使得程式具有更好的可擴充性。

看到這裡,是不是有那麼點如夢初醒的感覺?于是,你是不是在想:在上面的例子中,我不一定要直接在greetpeople()方法中給 name參數指派,我可以像這樣使用變量:

string name1, name2;

name1 = “jimmy zhang”;

name2 = “張子陽”;

greetpeople(name1, englishgreeting);

greetpeople(name2, chinesegreeting);

而既然委托greetingdelegate 和 類型 string 的地位一樣,都是定義了一種參數類型,那麼,我是不是也可以這麼使用委托?

greetingdelegate delegate1, delegate2;

delegate1 = englishgreeting;

delegate2 = chinesegreeting;

greetpeople(“jimmy zhang”, delegate1);

greetpeople(“張子陽”, delegate2);

如你所料,這樣是沒有問題的,程式一如預料的那樣輸出。這裡,我想說的是委托不同于string的一個特性:可以将多個方法賦給同一個委托,或者叫将多個方法綁定到同一個委托,當調用這個委托的時候,将依次調用其所綁定的方法。在這個例子中,文法如下:

greetingdelegate delegate1;

delegate1 = englishgreeting; // 先給委托類型的變量指派

delegate1 += chinesegreeting;   // 給此委托變量再綁定一個方法

// 将先後調用 englishgreeting 與 chinesegreeting 方法

輸出為:

早上好, jimmy zhang

實際上,我們可以也可以繞過greetpeople方法,通過委托來直接調用englishgreeting和chinesegreeting:

delegate1 (“jimmy zhang”);

note:這在本例中是沒有問題的,但回頭看下上面greetpeople()的定義,在它之中可以做一些對于englshihgreeting和chinesegreeting來說都需要進行的工作,為了簡便我做了省略。

注意這裡,第一次用的“=”,是指派的文法;第二次,用的是“+=”,是綁定的文法。如果第一次就使用“+=”,将出現“使用了未指派的局部變量”的編譯錯誤。

我們也可以使用下面的代碼來這樣簡化這一過程:

greetingdelegate delegate1 = new greetingdelegate(englishgreeting);

看到這裡,應該注意到,這段代碼第一條語句與執行個體化一個類是何其的相似,你不禁想到:上面第一次綁定委托時不可以使用“+=”的編譯錯誤,或許可以用這樣的方法來避免:

greetingdelegate delegate1 = new greetingdelegate();

delegate1 += englishgreeting;   // 這次用的是 “+=”,綁定文法。

但實際上,這樣會出現編譯錯誤: “greetingdelegate”方法沒有采用“0”個參數的重載。盡管這樣的結果讓我們覺得有點沮喪,但是編譯的提示:“沒有0個參數的重載”再次讓我們聯想到了類的構造函數。我知道你一定按捺不住想探個究竟,但再此之前,我們需要先把基礎知識和應用介紹完。

既然給委托可以綁定一個方法,那麼也應該有辦法取消對方法的綁定,很容易想到,這個文法是“-=”:

console.writeline();

delegate1 -= englishgreeting; //取消對englishgreeting方法的綁定

// 将僅調用 chinesegreeting

greetpeople(“張子陽”, delegate1);

讓我們再次對委托作個總結:

使用委托可以将多個方法綁定到同一個委托變量,當調用此變量時(這裡用“調用”這個詞,是因為此變量代表一個方法),可以依次調用所有綁定的方法。

我們繼續思考上面的程式:上面的三個方法都定義在programe類中,這樣做是為了了解的友善,實際應用中,通常都是 greetpeople 在一個類中,chinesegreeting和 englishgreeting 在另外的類中。現在你已經對委托有了初步了解,是時候對上面的例子做個改進了。假設我們将greetingpeople()放在一個叫greetingmanager的類中,那麼新程式應該是這個樣子的:

//建立的greetingmanager類

public class greetingmanager{

public void greetpeople(string name, greetingdelegate makegreeting) {

// … …

這個時候,如果要實作前面示範的輸出效果,main方法我想應該是這樣的:

greetingmanager gm = new  greetingmanager();

gm.greetpeople(“jimmy zhang”, englishgreeting);

gm.greetpeople(“張子陽”, chinesegreeting);

我們運作這段代碼,嗯,沒有任何問題。程式一如預料地那樣輸出了:

現在,假設我們需要使用上一節學到的知識,将多個方法綁定到同一個委托變量,該如何做呢?讓我們再次改寫代碼:

greetingmanager gm = new  greetingmanager();

delegate1 += chinesegreeting;

gm.greetpeople(“jimmy zhang”, delegate1);

輸出:

到了這裡,我們不禁想到:面向對象設計,講究的是對象的封裝,既然可以聲明委托類型的變量(在上例中是delegate1),我們何不将這個變量封裝到 greetmanager類中?在這個類的用戶端中使用不是更友善麼?于是,我們改寫greetmanager類,像這樣:

//在greetingmanager類的内部聲明delegate1變量

public greetingdelegate delegate1;

現在,我們可以這樣使用這個委托變量:

gm.delegate1 = englishgreeting;

gm.delegate1 += chinesegreeting;

gm.greetpeople(“jimmy zhang”, gm.delegate1);

盡管這樣做沒有任何問題,但我們發現這條語句很奇怪。在調用gm.greetpeople方法的時候,再次傳遞了gm的delegate1字段:

既然如此,我們何不修改 greetingmanager 類成這樣:

if(delegate1!=null){     //如果有方法注冊委托變量

delegate1(name);      //通過委托調用方法

在用戶端,調用看上去更簡潔一些:

gm.greetpeople(“jimmy zhang”);      //注意,這次不需要再傳遞 delegate1變量

盡管這樣達到了我們要的效果,但是還是存在着問題:

在這裡,delegate1和我們平時用的string類型的變量沒有什麼分别,而我們知道,并不是所有的字段都應該聲明成public,合适的做法是應該public的時候public,應該private的時候private。

我們先看看如果把 delegate1 聲明為 private會怎樣?結果就是:這簡直就是在搞笑。因為聲明委托的目的就是為了把它暴露在類的用戶端進行方法的注冊,你把它聲明為private了,用戶端對它根本就不可見,那它還有什麼用?

再看看把delegate1 聲明為 public 會怎樣?結果就是:在用戶端可以對它進行随意的指派等操作,嚴重破壞對象的封裝性。

最後,第一個方法注冊用“=”,是指派文法,因為要進行執行個體化,第二個方法注冊則用的是“+=”。但是,不管是指派還是注冊,都是将方法綁定到委托上,除了調用時先後順序不同,再沒有任何的分别,這樣不是讓人覺得很别扭麼?

現在我們想想,如果delegate1不是一個委托類型,而是一個string類型,你會怎麼做?答案是使用屬性對字段進行封裝。

于是,event出場了,它封裝了委托類型的變量,使得:在類的内部,不管你聲明它是public還是protected,它總是private的。在類的外部,注冊“+=”和登出“-=”的通路限定符與你在聲明事件時使用的通路符相同。

我們改寫greetingmanager類,它變成了這個樣子:

//這一次我們在這裡聲明一個事件

public event greetingdelegate makegreet;

makegreet(name);

很容易注意到:makegreet 事件的聲明與之前委托變量delegate1的聲明唯一的差別是多了一個event關鍵字。看到這裡,在結合上面的講解,你應該明白到:事件其實沒什麼不好了解的,聲明一個事件不過類似于聲明一個進行了封裝的委托類型的變量而已。

為了證明上面的推論,如果我們像下面這樣改寫main方法:

gm.makegreet = englishgreeting;         // 編譯錯誤1

gm.makegreet += chinesegreeting;

gm.greetpeople(“jimmy zhang”);

會得到編譯錯誤:事件“delegate.greetingmanager.makegreet”隻能出現在 += 或 -= 的左邊(從類型“delegate.greetingmanager”中使用時除外)。

這時候,我們注釋掉編譯錯誤的行,然後重新進行編譯,再借助reflactor來對 event的聲明語句做一探究,看看為什麼會發生這樣的錯誤:

【轉】【UNITY3D 遊戲開發之七】C# 中的委托、事件、匿名函數、LAMBDA 表達式

可以看到,實際上盡管我們在greetingmanager裡将 makegreet 聲明為public,但是,實際上makegreet會被編譯成 私有字段,難怪會發生上面的編譯錯誤了,因為它根本就不允許在greetingmanager類的外面以指派的方式通路,進而驗證了我們上面所做的推論。

我們再進一步看下makegreet所産生的代碼:

private greetingdelegate makegreet; //對事件的聲明 實際是 聲明一個私有的委托變量

[methodimpl(methodimploptions.synchronized)]

public void add_makegreet(greetingdelegate value){

this.makegreet = (greetingdelegate) delegate.combine(this.makegreet, value);

public void remove_makegreet(greetingdelegate value){

this.makegreet = (greetingdelegate) delegate.remove(this.makegreet, value);

現在已經很明确了:makegreet事件确實是一個greetingdelegate類型的委托,隻不過不管是不是聲明為public,它總是被聲明為private。另外,它還有兩個方法,分别是add_makegreet和remove_makegreet,這兩個方法分别用于注冊委托類型的方法和取消注冊。實際上也就是: “+= ”對應 add_makegreet,“-=”對應remove_makegreet。而這兩個方法的通路限制取決于聲明事件時的通路限制符。

在add_makegreet()方法内部,實際上調用了system.delegate的combine()靜态方法,這個方法用于将目前的變量添加到委托連結清單中。我們前面提到過兩次,說委托實際上是一個類,在我們定義委托的時候:

當編譯器遇到這段代碼的時候,會生成下面這樣一個完整的類:

public sealed class greetingdelegate:system.multicastdelegate{

public greetingdelegate(object @object, intptr method);

public virtual iasyncresult begininvoke(string name, asynccallback callback, object @object);

public virtual void endinvoke(iasyncresult result);

public virtual void invoke(string name);

【轉】【UNITY3D 遊戲開發之七】C# 中的委托、事件、匿名函數、LAMBDA 表達式

關于這個類的更深入内容,可以參閱《clr via c#》等相關書籍,這裡就不再讨論了。

上面的例子已不足以再進行下面的講解了,我們來看一個新的範例,因為之前已經介紹了很多的内容,是以本節的進度會稍微快一些:

假設我們有個高檔的熱水器,我們給它通上電,當水溫超過95度的時候:1、揚聲器會開始發出語音,告訴你水的溫度;2、液晶屏也會改變水溫的顯示,來提示水已經快燒開了。

現在我們需要寫個程式來模拟這個燒水的過程,我們将定義一個類來代表熱水器,我們管它叫:heater,它有代表水溫的字段,叫做temperature;當然,還有必不可少的給水加熱方法boilwater(),一個發出語音警報的方法makealert(),一個顯示水溫的方法,showmsg()。

class heater {

private int temperature; // 水溫

// 燒水

public void boilwater() {

for (int i = 0; i <= 100; i++) {

temperature = i;

if (temperature > 95) {

makealert(temperature);

showmsg(temperature);

// 發出語音警報

private void makealert(int param) {

console.writeline(“alarm:嘀嘀嘀,水已經 {0} 度了:” , param);

// 顯示水溫

private void showmsg(int param) {

console.writeline(“display:水快開了,目前溫度:{0}度。” , param);

static void main() {

heater ht = new heater();

ht.boilwater();

上面的例子顯然能完成我們之前描述的工作,但是卻并不夠好。現在假設熱水器由三部分組成:熱水器、警報器、顯示器,它們來自于不同廠商并進行了組裝。那麼,應該是熱水器僅僅負責燒水,它不能發出警報也不能顯示水溫;在水燒開時由警報器發出警報、顯示器顯示提示和水溫。

這時候,上面的例子就應該變成這個樣子:

// 熱水器

public class heater {

private int temperature;

private void boilwater() {

// 警報器

public class alarm{

// 顯示器

public class display{

console.writeline(“display:水已燒開,目前溫度:{0}度。” , param);

這裡就出現了一個問題:如何在水燒開的時候通知報警器和顯示器?在繼續進行之前,我們先了解一下observer設計模式,observer設計模式中主要包括如下兩類對象:

subject:監視對象,它往往包含着其他對象所感興趣的内容。在本範例中,熱水器就是一個監視對象,它包含的其他對象所感興趣的内容,就是temprature字段,當這個字段的值快到100時,會不斷把資料發給監視它的對象。

observer:監視者,它監視subject,當subject中的某件事發生的時候,會告知observer,而observer則會采取相應的行動。在本範例中,observer有警報器和顯示器,它們采取的行動分别是發出警報和顯示水溫。

在本例中,事情發生的順序應該是這樣的:

警報器和顯示器告訴熱水器,它對它的溫度比較感興趣(注冊)。

熱水器知道後保留對警報器和顯示器的引用。

熱水器進行燒水這一動作,當水溫超過95度時,通過對警報器和顯示器的引用,自動調用警報器的makealert()方法、顯示器的showmsg()方法。

類似這樣的例子是很多的,gof對它進行了抽象,稱為observer設計模式:observer設計模式是為了定義對象間的一種一對多的依賴關系,以便于當一個對象的狀态改變時,其他依賴于它的對象會被自動告知并更新。observer模式是一種松耦合的設計模式。

我們之前已經對委托和事件介紹很多了,現在寫代碼應該很容易了,現在在這裡直接給出代碼,并在注釋中加以說明。

public delegate void boilhandler(int param);   //聲明委托

public event boilhandler boilevent;        //聲明事件

if (boilevent != null) { //如果有對象注冊

boilevent(temperature);  //調用所有注冊對象的方法

public class alarm {

public void makealert(int param) {

console.writeline(“alarm:嘀嘀嘀,水已經 {0} 度了:”, param);

public class display {

public static void showmsg(int param) { //靜态方法

console.writeline(“display:水快燒開了,目前溫度:{0}度。”, param);

heater heater = new heater();

alarm alarm = new alarm();

heater.boilevent += alarm.makealert;    //注冊方法

heater.boilevent += (new alarm()).makealert;   //給匿名對象注冊方法

heater.boilevent += display.showmsg;       //注冊靜态方法

heater.boilwater();   //燒水,會自動調用注冊過對象的方法

alarm:嘀嘀嘀,水已經 96 度了:

display:水快燒開了,目前溫度:96度。

// 省略…

盡管上面的範例很好地完成了我們想要完成的工作,但是我們不僅疑惑:為什麼.net framework 中的事件模型和上面的不同?為什麼有很多的eventargs參數?

在回答上面的問題之前,我們先搞懂 .net framework的編碼規範:

委托類型的名稱都應該以eventhandler結束。

委托的原型定義:有一個void傳回值,并接受兩個輸入參數:一個object 類型,一個 eventargs類型(或繼承自eventargs)。

事件的命名為 委托去掉 eventhandler之後剩餘的部分。

繼承自eventargs的類型應該以eventargs結尾。

再做一下說明:

委托聲明原型中的object類型的參數代表了subject,也就是監視對象,在本例中是 heater(熱水器)。回調函數(比如alarm的makealert)可以通過它通路觸發事件的對象(heater)。

eventargs 對象包含了observer所感興趣的資料,在本例中是temperature。

上面這些其實不僅僅是為了編碼規範而已,這樣也使得程式有更大的靈活性。比如說,如果我們不光想獲得熱水器的溫度,還想在observer端(警報器或者顯示器)方法中獲得它的生産日期、型号、價格,那麼委托和方法的聲明都會變得很麻煩,而如果我們将熱水器的引用傳給警報器的方法,就可以在方法中直接通路熱水器了。

現在我們改寫之前的範例,讓它符合 .net framework 的規範:

public string type = “realfire 001”;       // 添加型号作為示範

public string area = “china xian”;         // 添加産地作為示範

//聲明委托

public delegate void boiledeventhandler(object sender, boiledeventargs e);

public event boiledeventhandler boiled; //聲明事件

// 定義boiledeventargs類,傳遞給observer所感興趣的資訊

public class boiledeventargs : eventargs {

public readonly int temperature;

public boiledeventargs(int temperature) {

this.temperature = temperature;

// 可以供繼承自 heater 的類重寫,以便繼承類拒絕其他對象對它的監視

protected virtual void onboiled(boiledeventargs e) {

if (boiled != null) { // 如果有對象注冊

boiled(this, e);  // 調用所有注冊對象的方法

// 燒水。

//建立boiledeventargs 對象。

boiledeventargs e = new boiledeventargs(temperature);

onboiled(e);  // 調用 onbolied方法

public void makealert(object sender, heater.boiledeventargs e) {

heater heater = (heater)sender;     //這裡是不是很熟悉呢?

//通路 sender 中的公共字段

console.writeline(“alarm:{0} – {1}: “, heater.area, heater.type);

console.writeline(“alarm: 嘀嘀嘀,水已經 {0} 度了:”, e.temperature);

public static void showmsg(object sender, heater.boiledeventargs e) {   //靜态方法

heater heater = (heater)sender;

console.writeline(“display:{0} – {1}: “, heater.area, heater.type);

console.writeline(“display:水快燒開了,目前溫度:{0}度。”, e.temperature);

heater.boiled += alarm.makealert;   //注冊方法

heater.boiled += (new alarm()).makealert;      //給匿名對象注冊方法

heater.boiled += new heater.boiledeventhandler(alarm.makealert);    //也可以這麼注冊

heater.boiled += display.showmsg;       //注冊靜态方法

alarm:china xian – realfire 001:

alarm: 嘀嘀嘀,水已經 96 度了:

display:china xian – realfire 001:

// 省略 …

在本文中我首先通過一個greetingpeople的小程式向大家介紹了委托的概念、委托用來做什麼,随後又引出了事件,接着對委托與事件所産生的中間代碼做了粗略的講述。

在第二個稍微複雜點的熱水器的範例中,我向大家簡要介紹了 observer設計模式,并通過實作這個範例完成了該模式,随後講述了.net framework中委托、事件的實作方式。

希望這篇文章能給你帶來幫助。