當要把方法作為實參傳送給其他方法的形參時,形參需要使用委托。委托是一個類型,是一個函數指針類型,這個類型將該委托的實例化對象所能指向的函數的細節封裝起來了,即規定了所能指向的函數的簽名,也就是限制了所能指向的函數的參數和返回值。當實例化委托的時候,委托對象會指向某一個匹配的函數,實質就是將函數的地址賦值給了該委托的對象,然後就可以通過該委托對象來調用所指向的函數了。利用委托,程序員可以在委托對象中封裝一個方法的引用,然後委托對象作為形參將被傳給調用了被引用方法的代碼,而不需要知道在編譯時刻具體是哪個方法被調用。
一般的調用函數,我們都不會去使用委托,因為如果只是單純的調用函數,使用委托更麻煩一些;但是如果想將函數作為實參,傳遞給某個函數的形參,那麼形參就一定要使用委托來接收實參,一般使用方法是:在函數外面定義委托對象,並指向某個函數,再將這個對象賦值給函數的形參,形參也是該委托類型的對象變量,函數裡面再通過形參來調用所指向的函數。
語法如下:
delegate result-type Identifier ([parameters]);
說明:
result-type:返回值的類型,和方法的返回值類型一致
Identifier:委托的名稱
parameters:參數,要引用的方法帶的參數
小結:
當定義了委托之後,該委托的對象一定可以而且也只能指向該委托所限制的函數。即參數的個數、類型、順序都要匹配,返回值的類型也要匹配。
因為定義委托相當於是定義一個新類,所以可以在定義類的任何地方定義委托,既可以在一個類的內部定義,那麼此時就要通過該類的類名來調用這個委托(委托必須是public、internal),也可以在任何類的外部定義,那麼此時在命名空間中與類的級別是一樣的。根據定義的可見性,可以在委托定義上添加一般的訪問修飾符:當委托定義在類的外面,那麼可以加上public、internal修飾符;如果委托定義到類的內部,那麼可以加上public、 private、 protected、internal。一般委托都是定義在類的外面的。
Identifier objectName = new Identifier( functionName);
實例化委托的實質就是將某個函數的地址賦值給委托對象。在這裡:
Identifier :這個是委托名字。
objectName :委托的實例化對象。
functionName:是該委托對象所指向的函數的名字。對於這個函數名要特別注意:定義這個委托對象肯定是在類中定義的,那麼如果所指向的函數也在該類中,不管該函數是靜態還是非靜態的,那麼就直接寫函數名字就可以了;如果函數是在別的類裡面定義的public、
internal,但是如果是靜態,那麼就直接用類名.函數名,如果是非靜態的,那麼就類的對象名.函數名,這個函數名與該對象是有關系的,比如如果函數中出現了this,表示的就是對當前對象的調用。
C# 2.0用委托推斷擴展了委托的語法。當我們需要定義委托對象並實例化委托的時候,就可以只傳送函數的名稱,即函數的地址:
Identifier objectName = functionName;
這裡面的functionName與14.1.2節中實例化委托的functionName是一樣的,沒什麼區別,滿足上面的規則。
C#編譯器創建的代碼是一樣的。編譯器會用objectName檢測需要的委托類型,因此會創建Identifier委托類型的一個實例,用functionName即方法的地址傳送給Identifier的構造函數。
注意:
不能在functionName後面加括號和實參,然後把它傳送給委托變量。調用方法一般會返回一個不能賦予委托變量的普通對象,除非這個方法返回的是一個匹配的委托對象。總之:只能把相匹配的方法的地址賦予委托變量。
委托推斷可以在需要委托實例化的任何地方使用,就跟定義普通的委托對象是一樣的。委托推斷也可以用於事件,因為事件基於委托(參見本章後面的內容)。
到目前為止,要想使委托工作,方法必須已經存在。但實例化委托還有另外一種方式:即通過匿名方法。
用匿名方法定義委托的語法與前面的定義並沒有區別。但在實例化委托時,就有區別了。下面是一個非常簡單的控制台應用程序,說明了如何使用匿名方法:
using System;
namespace Wrox.ProCSharp.Delegates
{
class Program
{
delegate string DelegateTest(string val);
static void Main()
{
string mid = ", middle part,";
//在方法中定義了方法
DelegateTest anonDel = delegate(string param)
{
param += mid;
param += " and this was added to the string.";
return param;
};
Console.WriteLine(anonDel("Start of string"));
}
}
}
委托DelegateTest在類Program中定義,它帶一個字符串參數。有區別的是Main方法。在定義anonDel時,不是傳送已知的方法名,而是使用一個簡單的代碼塊:它前面是關鍵字delegate,後面是一個參數:
delegate(string param)
{
param += mid;
param += " and this was added to the string.";
return param;
};
匿名方法的優點是減少了要編寫的代碼。方法僅在有委托使用時才定義。在為事件定義委托時,這是非常顯然的。(本章後面探討事件。)這有助於降低代碼的復雜性,尤其是定義了好幾個事件時,代碼會顯得比較簡單。使用匿名方法時,代碼執行得不太快。編譯器仍定義了一個方法,該方法只有一個自動指定的名稱,我們不需要知道這個名稱。
在使用匿名方法時,必須遵循兩個規則:
1、在匿名方法中不能使用跳轉語句跳到該匿名方法的外部,反之亦然:匿名方法外部的跳轉語句不能跳到該匿名方法的內部。
2、在匿名方法內部不能訪問不安全的代碼。另外,也不能訪問在匿名方法外部使用的ref和out參數。但可以使用在匿名方法外部定義的其他變量。方法內部的變量、方法的參數可以任意的使用。
如果需要用匿名方法多次編寫同一個功能,就不要使用匿名方法。而編寫一個指定的方法比較好,因為該方法只需編寫一次,以後可通過名稱引用它。
前面使用的每個委托都只包含一個方法調用,調用委托的次數與調用方法的次數相同,如果要調用多個方法,就需要多次給委托賦值,然後調用這個委托。
委托也可以包含多個方法,這時候要向委托對象中添加多個方法,這種委托稱為多播委托,多播委托有一個方法列表,如果調用多播委托,就可以連續調用多個方法,即先執行某一個方法,等該方法執行完成之後再執行另外一個方法,這些方法的參數都是一樣的,這些方法的執行是在一個線程中執行的,而不是每個方法都是一個線程,最終將執行完成所有的方法。
如果使用多播委托,就應注意對同一個委托調用方法鏈的順序並未正式定義,調用順序是不確定的,不一定是按照添加方法的順序來調用方法,因此應避免編寫依賴於以特定順序調用方法的代碼。如果要想確定順序,那麼只能是單播委托,調用委托的次數與調用方法的次數相同。
多播委托的各個方法簽名最好是返回void;否則,就只能得到委托最後調用的一個方法的結果,而最後調用哪個方法是無法確定的。
多播委托的每一個方法都要與委托所限定的方法的返回值、參數匹配,否則就會有錯誤。
我自己寫代碼測試,測試的結果目前都是調用順序和加入委托的順序相同的,但是不排除有不同的時候。
delegate result-type Identifier ([parameters]);
Identifier objectName = new Identifier( functionName);
或者
Identifier objectName = functionName;
這裡的“=”號表示清空 objectName 的方法列表,然後將 functionName 加入到 objectName 的方法列表中。
objectName += new Identifier( functionName1);
或者
objectName += functionName1;
這裡的“+=”號表示在原有的方法列表不變的情況下,將 functionName1 加入到 objectName 的方法列表中。可以在方法列表中加上多個相同的方法,執行的時候也會執行完所有的函數,哪怕有相同的,就會多次執行同一個方法。
注意:objectName 必須是已經賦值了的,否則在定義的時候直接使用該符號:
Identifier objectName += new Identifier( functionName1);或者
Identifier objectName += functionName1;就會報錯。
objectName -= new Identifier( functionName1);
或者
objectName -= functionName1;
這裡的“-=”號表示在 objectName 的方法列表中減去一個functionName1。可以在方法列表中多次減去相同的方法,減一次只會減一個方法,如果列表中無此方法,那麼減就沒有意義,對原有列表無影響,也不會報錯。
注意:objectName 必須是已經賦值了的,否則在定義的時候直接使用該符號:
Identifier objectName -= new Identifier( functionName1);或者
Identifier objectName -= functionName1;就會報錯。
Identifier objectName = objectName + functionName1 - functionName1;或者
Identifier objectName = new Identifier( functionName1) + functionName1 - functionName1;
對於這種+、-表達式,在第一個符號+或者-的前面必須是委托而不能是方法,後面的+、-左右都隨便。這個不是絕對規律,還有待進一步的研究。
通過一個委托調用多個方法還有一個大問題。多播委托包含一個逐個調用的委托集合。如果通過委托調用的一個方法拋出了異常,整個迭代就會停止。下面是MulticastIteration示例。其中定義了一個簡單的委托DemoDelegate,它沒有參數,返回void。這個委托調用方法One()和Two(),這兩個方法滿足委托的參數和返回類型要求。注意方法One()拋出了一個異常:
using System;
namespace Wrox.ProCSharp.Delegates
{
public delegate void DemoDelegate();
class Program
{
static void One()
{
Console.WriteLine("One");
throw new Exception("Error in one");
}
static void Two()
{
Console.WriteLine("Two");
}
在Main()方法中,創建了委托d1,它引用方法One(),接著把Two()方法的地址添加到同一個委托中。調用d1委托,就可以調用這兩個方法。異常在try/catch塊中捕獲:
static void Main()
{
DemoDelegate d1 = One;
d1 += Two;
try
{
d1();
}
catch (Exception)
{
Console.WriteLine("Exception caught");
}
}
}
}
委托只調用了第一個方法。第一個方法拋出了異常,所以委托的迭代會停止,不再調用Two()方法。當調用方法的順序沒有指定時,結果會有所不同。
One
Exception Caught
注意:
多播委托包含一個逐個調用的委托集合。如果通過委托調用的一個方法拋出了異常,整個迭代就會停止。即如果任一方法引發了異常,而在該方法內未捕獲該異常,則該異常將傳遞給委托的調用方,並且不再對調用列表中後面的方法進行調用。
在這種情況下,為了避免這個問題,應手動迭代方法列表。Delegate類定義了方法GetInvocationList(),它返回一個Delegate對象數組。現在可以使用這個委托調用與委托直接相關的方法,捕獲異常,並繼續下一次迭代。
static void Main()
{
DemoDelegate d1 = One;
d1 += Two;
Delegate[] delegates = d1.GetInvocationList();
foreach (DemoDelegate d in delegates)
{
try
{
d();
}
catch (Exception)
{
Console.WriteLine("Exception caught");
}
}
}
修改了代碼後運行應用程序,會看到在捕獲了異常後,將繼續迭代下一個方法。
One
Exception caught
Two
注意:其實如果在多播委托的每個具體的方法中捕獲異常,並在內部處理,而不拋出異常,一樣能實現多播委托的所有方法執行完畢。這種方式與上面方式的區別在於這種方式的宜昌市在函數內部處理的,上面那種方式的異常是在函數外面捕獲並處理的。
1、委托實例的名稱,後面的括號中應包含調用該委托中的方法時使用的參數。
2、調用委托對象的Invoke()方法,Invoke後面的括號中應包含調用該委托中的方法時使用的參數。
注意:實際上,給委托實例提供括號與調用委托類的Invoke()方法完全相同。因為Invoke()方法是委托的同步調用方法。
注意:不管是多播委托還是單播委托,在沒有特殊處理的情況下,在一個線程的執行過程中去調用委托(委托對象所指向的函數),調用委托的執行是不會新起線程的,這個執行還是在原線程中的,這個對於事件也是一樣的。當然,如果是在委托所指向的函數裡面去啟動一個新的線程那就是另外一回事了。
Delegate result-type delegateName ([parameters]);
這個委托可以在類A內定義也可以在類A外定義。
Event delegateName eventName;
eventName不是一個類型,而是一個具體的對象,這個具體的對象只能在類A內定義而不能在類A外定義。
ReturnType FunctionName([parameters])
{
……
If(eventName != null)
{
eventName([parameters]);
或者eventName.Invoke([parameters]);
}
……
}
觸發事件之後,事件所指向的函數將會被執行。這種執行是通過事件名稱來調用的,就像委托對象名一樣的。
觸發事件的方法只能在A類中定義,事件的實例化,以及實例化之後的實現體都只能在A類外定義。
在類B中定義一個類A的對象,並且讓類A對象的那個事件指向類B中定義的方法,這個方法要與事件關聯的委托所限定的方法吻合。
在B類中去調用A類中的觸發事件的方法:用A類的對象去調用A類的觸發事件的方法。