C#圖解教程 第十三章 委託
委託
什麼是委託
可以認為委託是持有一個或多個方法的物件。當然,正常情況下你不想“執行”一個物件,但委託與典型物件不同。可以執行委託,這時委託會執行它所“持有”的方法。
我們從下面的示例程式碼開始。具體細節將在本章剩餘內容介紹。
- 程式碼開始部分聲明瞭一個委託型別MyDel(沒錯,是委託型別不是委託物件)
- Program類聲明瞭3個方法:PrintLow、PrintHigh和Main。接下來要建立的委託物件將持有PrintLow或PrintHigh方法,但具體使用哪個執行時確定
- Main聲明瞭區域性變數del,持有一個MyDel型別的委託物件的引用。這不會建立物件。只是建立持有委託物件引用的變數,在幾行後便會建立委託物件,並將值賦給這個變數
- Main建立了Random類的物件,這是個隨機數生成器類。接著呼叫該物件Next方法,將99作為引數。這會返回介於0到99間的隨機整數,並將這個值儲存在區域性變數randomValue中
- 下面一行檢查這個隨機值是否小於50
- 小於50,就建立一個MyDel委託物件並初始化,讓它持有PrintLow方法的引用
- 否則,就建立一個持有PrintHigh方法引用的MyDel委託物件
- 最後,Main執行委託物件del,這將執行它持有的方法(PrintLow或PrintHigh)
如果你有C++背景,理解委託最快的方法是把它看成一個型別安全的、面向物件的C++函式指標
delegate void MyDel(int value);//宣告委託型別 class Program { void PrintLow(int value) { Console.WriteLine("{0} - Low Value",value); } void PrintHigh(int value) { Console.WriteLine("{0} - High Value",value); } static void Main() { Program program=new Program(); MyDel del; //宣告委託變數 var rand=new Random(); var randomValue=rand.Next(99); del=randomValue<50 ?new MyDel(program.PrintLow) :new MyDel(program.PrintHigh); del(randomValue); //執行委託 } }
委託概述
委託和類一樣,是使用者自定義型別。但類表示的是資料和方法的集合,而委託持有一個或多個方法,以及一系列預定義操作。
可以通過以下操作步驟來使用委託。
- 宣告一個委託型別。委託宣告看上去和方法宣告相似,只是沒有實現塊
- 使用該委託型別宣告一個委託變數
- 建立委託型別的物件,把它賦值給委託變數。新的委託物件包括指向某個方法的引用,這個方法和第一步定義的簽名和返回型別一致
- 你可以選擇為委託物件增加其他方法。這些方法必須與第一步中定義的委託型別有相同的簽名和返回型別
- 在程式碼中你可以像呼叫方法一樣呼叫委託。在呼叫委託時,其包含的每個方法都會被執行
你可以把delegate看做一個包含有序方法列表的物件,這些方法的簽名和返回型別相同。
- 方法的列表稱為呼叫列表
- 委託儲存的方法可以來自任何類或結構,只要它們在下面兩點匹配
- 委託的返回型別
- 委託的簽名(包括ref和out修飾符)
- 呼叫列表中的方法可以是例項方法也可以是靜態方法
- 在呼叫委託時,會執行其呼叫列表中的所有方法
宣告委託型別
與類一樣,委託型別必須在被用來建立變數以及型別的物件前宣告。宣告格式如下。
關鍵字 委託型別名 ↓ ↓ delegate void MyDel(int x); ↑ ↑ 返回型別 簽名
雖然委託型別宣告看上去和方法宣告一樣,但它不需要在類內部宣告,因為它是型別宣告。
建立委託物件
委託是引用型別,因此有引用和物件。委託型別聲明後,我們可以宣告變數並建立型別的物件。
有兩種建立委託物件的方法,一種是使用帶new運算子的物件建立表示式,如下面程式碼所示。
delVar=new MyDel(myInstObj.MyM1); dVar=new MyDel(SClass.OtherM2);
我們還可以使用快捷語法,它僅由方法說明符構成。這種快捷語法能夠工作是因為在方法名稱和其相應的委託型別間存在隱式轉換。
delVar=myInstObj.MyM1; dVar=SClass.OtherM2;
建立委託物件會為委託分配記憶體,還會把第一個方法放入委託呼叫列表。
給委託賦值
由於委託是引用型別,我們可以通過給它賦值來改變包含在委託變數中的引用。舊的委託物件會被GC回收。
MyDel delvar; delVar=myInstObj.MyM1; ... delVar=SClass.OtherM2;
組合委託
迄今為止,我們見過的所有委託在呼叫列表中都只有一個方法。委託可以使用額外的運算子來“組合”。這個運算子會建立一個新的委託,其呼叫列表連線了作為運算元的兩個委託的呼叫列表副本。
例:建立3個委託,第3個委託由前兩個組合而成。
MyDel delA=myInstObj.MyM1; MyDel delB=SClass.OtherM2;
MyDel delC=delA+delB;
儘管術語組合委託(combining delegate)讓我們覺得好想運算元委託被修改了,其實它們並沒有被修改。事實上,委託是恆定的。委託物件被建立後不能再被改變。
為委託新增方法
儘管通過上一節我們知道委託是恆定的,不過C#提供了看上去可以為委託新增方法的語法,即使用+=運算子。
例:為委託的呼叫列表增加兩個方法。
MyDel delVar=inst.MyM1; delVar+=SCL.m3; delvar+=X.Act;
當然,使用+=運算子時,實際發生的是建立了一個新的委託,其呼叫列表是左邊的委託加上右邊的組合。然後將這個新的委託賦值給delVar。
從委託移除方法
我們可以使用-=運算子從委託移除方法。
delVar-=SCL.m3;
與為委託增加方法一樣,其實是建立了一個新的委託。新的委託是舊委託的副本–只是沒有了已經被移除方法的引用。
移除委託時需要記住以下事項:
- 如果在呼叫列表中有多個例項,-=運算子將從列表最後開始搜尋,並且移除第一個與方法匹配的例項
- 試圖刪除委託中不存在的方法沒有效果
- 試圖呼叫空委託會丟擲異常。我們可以通過把委託和null進行比較來判斷委託列表是否為空。如果呼叫列表為空,則委託是null
呼叫委託
可以像呼叫方法一樣簡單地呼叫委託。呼叫委託的引數將會用於呼叫列表中的每個方法(除非有輸出引數,我們稍後介紹)。
例:delVar委託接受一個整數值。使用引數呼叫委託會使用相同的引數值呼叫它呼叫列表中的每個成員
MyDel delVar=inst.MyM1; delVar+=SCL.m3; delVar+=X.Act; ... delVar(55);
如果一個方法在呼叫列表中出現多次,當委託被呼叫時,每次在列表中遇到該方法時它都會被呼叫一次。
委託示例
如下程式碼定義並使用了沒有引數和返回值的委託。有關程式碼的注意事項如下:
- Test類定義了兩個列印函式
- Main方法建立了委託的例項並增加了3個方法
- 程式隨後呼叫委託,呼叫前檢測了委託是否為null
delegate void PrintFunction(); class Test { public void Print1() { Console.WriteLine("Print1 -- instance"); } public static void Print2() { Console.WriteLine("Print2 -- static"); } } class Program { static void Main() { var t=new Test(); PrintFunction pf; pf=t.Print1; pf+=Test.Print2; pf+=t.Print1; pf+=Test.Print2; if(null!=pf) { pf(); } else { Console.WriteLine("Delegate is empty"); } } }
呼叫帶返回值的委託
如果委託有返回值並且呼叫列表中有一個以上方法,會發生下面的情況:
- 呼叫列表中最後一個方法返回的值就是委託呼叫的返回值
- 呼叫列表中其他返回值被忽略
delegate int MyDel(); class MyClass { int IntValue=5; public int Add2() { IntValue+=2; return IntValue; } public int Add3() { IntValue+=3; return IntValue; } } class Program { static void Main() { var mc=new MyClass(); MyDel mDel=mc.Add2; mDel+=mc.Add3; mDel+=mc.Add2; Console.WriteLine("Value: {0}",mDel()); } }
呼叫帶引用引數的委託
如果委託有引用引數,引數值會根據呼叫列表中的一個或多個方法的返回值而改變。
在呼叫委託列表中的下一個方法時,引數的新值會傳給下一個方法。
delegate void MyDel(ref int X); class MyClass { public int Add2(ref int x) { x+=2; } public int Add3(ref int x) { x+=3; } static void Main() { var mc=new MyClass(); MyDel mDel=mc.Add2; mDel+=mc.Add3; mDel+=mc.Add2; int x=5; mDel(ref x); Console.WriteLine("Value: {0}",x); } }
匿名方法
匿名方法(anonymous method)是在初始化委託時內聯(inline)宣告的方法。
例:第一個聲明瞭Add20方法,第二個使用匿名方法。
class Program { public static int Add20(int x) { return x+=20; } delegate int OtherDel(int InParam); static void Main() { OtherDel del=Add20; Console.WriteLine("{0}",del(5)); Console.WriteLine("{0}",del(6)); } } class Program { delegate int OtherDel(int InParam); static void Main() { OtherDel del=delegate(int x) { return x+20; }; Console.WriteLine("{0}",del(5)); Console.WriteLine("{0}",del(6)); } }
使用匿名方法
我們可以在如下地方使用匿名方法。
- 宣告委託變數時作為初始化表示式
- 組合委託時在賦值語句的右邊
- 為委託增加事件(第14章)時在賦值語句的右邊
匿名方法的語法
匿名方法表示式語法包含如下:
關鍵字 引數列表 語句塊 ↓ ↓ ↓ delegate(Parameters){ImplementationCode}
Lambda 表示式
在匿名方法的語法中,delegate關鍵字有點多餘,因為編譯器已經知道我們在將方法賦值給委託。我們可以很容易地通過如下步驟把匿名方法轉換為Lambda表示式:
- 刪除delegate關鍵字
- 在引數列表和匿名方法主體之間放Lambda運算子=>(讀作goes to)。
MyDel del=delegate(int x) {return x+1;};//匿名方法 MyDel le1= (int x) => {return x+1;};//Lambda表示式
術語Lambda表示式來源於數學家Alonzo Church等人在1920到1930年期間發明的Lambda積分。Lambda積分是用於表示函式的一套系統,它使用希臘字母Lambda(λ)來表示無名函式。近來,函數語言程式設計語言(如Lisp及其方言)使用這個術語來表示可以直接用於描述函式定義的表示式,表示式不再需要有名字了。
除了這種簡單的轉換,通過編譯器的自動推斷,我們可以更進一步簡化Lambda表示式。
- 編譯器還可以從委託的宣告中知道委託引數的型別,因此Lambda表示式允許我們省略型別引數,如le2
- 帶有型別的引數列表稱為顯示型別
- 省略型別的引數列表稱為隱式型別
- 如果只有一個隱式型別引數,我們可以省略周圍的圓括號,如le3
- 最後,Lambda表示式允許表示式的主體是語句塊或表示式。如果語句塊包含了一個返回語句,我們可以將語句塊替換為return關鍵字後的表示式,如le4
MyDel del=delegate(int x) {return x+1;}; MyDel le1= (int x) => {return x+1;}; MyDel le2= (x) => {return x+1;}; MyDel le3= x => {return x+1;}; MyDel le4= x => x+1 ;
例:Lambda表示式完整示例
delegate double MyDel(int par); class Program { static void Main() { MyDel del=delegate(int x) {return x+1;}; MyDel le1= (int x) => {return x+1;}; MyDel le2= (x) => {return x+1;}; MyDel le3= x => {return x+1;}; MyDel le4= x => x+1 ; Console.WriteLine("{0}",del(12)); Console.WriteLine("{0}",le1(12)); Console.WriteLine("{0}",le2(12)); Console.WriteLine("{0}",le3(12)); Console.WriteLine("{0}",le4(12)); } }
有關Lambda表示式的引數列表的要點如下:
- Lambda表示式引數列表中的引數必須在引數數量、型別和位置上與委託相匹配
- 表示式的引數列表中的引數不一定需要包含型別(隱式型別),除非委託由ref或out引數–此時必須註明型別(顯式型別)
- 如果只有一個引數,並且是隱式型別的,周圍的圓括號可以省略
- 如果沒有引數,必須使用一組空圓括號
from: http://www.cnblogs.com/moonache/p/6269113.html