委托
什么是委托
可以認為委托是持有一個或多個方法的對象。當然,正常情況下你不想“執行”一個對象,但委托與典型對象不同。可以執行委托,這時委托會執行它所“持有”的方法。
我們從下面的示例代碼開始。具體細節將在本章剩余內容介紹。
- 代碼開始部分聲明了一個委托類型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參數–此時必須注明類型(顯式類型)
- 如果只有一個參數,並且是隱式類型的,周圍的圓括號可以省略
- 如果沒有參數,必須使用一組空圓括號