在.NET平台下,委托類型用來定義和響應應用程序中的回調。事實上,.NET委托類型是一個類型安全的對象,指向可以以後調用的其他方法。和傳統的C++函數指針不同,.NET委托是內置支持多路廣播和異步方法調用的對象。
委托類型包含3個重要信息:
1.定義一個委托類型
// 這個委托可以指向任何傳入兩個整數,返回整數的方法
public delegate int BinaryOp(int x,int y);
創建一個委托類型時,需要使用delegate關鍵字。當C#編譯器處理委托類型時,它先自動產生一個派生自System.MulticastDelegate的密封類。
通過ildasm.exe來查看BinaryOp委托,如下圖

編譯器是如何確切的定義 Invoke(),BeginInvoke(),EndInvoke() 的呢?讓我們看看下面這段代碼:
sealed class BinaryOp : System.MulticastDelegate
{
public int Invoke(int x,int y);
public IAsyncResult BeginInvoke(int x,int y,AsyncCallback cb,object state);
public int EndInvoke(IasyncResult result);
}
Invoke() 方法定義的參數和返回值完全匹配BinaryOp委托的定義。
BeginInvoke() 成員簽名的參數也基於BinaryOp委托;但BenginInvoke()方法將總是提供最後兩個參數(AsyncCallback,object),用於異步方法的調用。
EndInvoke() 方法的返回值與初始的委托聲明相同,總是以一個實現了IasyncResult接口的對象作為其唯一的參數。
我們再定義一個 帶有 out/ref 參數的委托類型如下:
public delegate string MyOtherDelegate(out bool a,ref bool b,int c);
試想一下,產生的代碼還和上面一樣嗎?好。
sealed class MyOtherDelegate: System.MulticastDelegate
{
public string Invoke(out bool a,ref bool b,int c);
public IAsyncResult BeginInvoke(out bool a,ref bool b,int c,AsyncCallback cb,object state);
public stringEndInvoke(out bool a,ref bool b,IasyncResult result);
}
我們發現,Invoke() 和 BeginInvoke() 方法的簽名不出所料,但 EndInvoke() 略有差異,其中包括了委托類型定義的所有 out/ref 參數。
2.System.Delegate和System.MulticastDelegate基類
使用關鍵字創建委托的時候,也就間接的聲明了一個派生自System.MulticastDelegate的類。
下面是System.MulticastDelegate部分成員源代碼:

下面是System.Delegate部分成員源代碼:

Remove()
RemoveAll()
這些靜態從調用列表中移除一個(所有)方法;在C#中,Remove方法可以通過使用重載-=操作符來調用
一. 創建簡單的委托
好了,了解了以上這些委托的基礎信息,我們開始創建屬於我們的第一個委托:
public class MyDelegate
{
// 這個委托可以指向任何傳入兩個整數並返回一個整數的方法
private delegate int BinaryOp(int x, int y);
// BinaryOp委托將指向的方法
private static int Add(int x, int y) => x + y;
public static void Show()
{
// 創建一個指向 Add() 方法的BinaryOp委托對象
BinaryOp b = new BinaryOp(Add);
// 使用委托對象間接調用Add()方法的兩種方法:
Console.WriteLine($"b(2, 3)-->{b(2, 3)}");
Console.WriteLine($"b.Invoke(2,3)-->{ b.Invoke(2, 3)}");
DisplayDelegateInfo(b);
}
// 將輸出委托調用列表那個每個成員的名稱
public static void DisplayDelegateInfo(Delegate delObj)
{
foreach (Delegate item in delObj.GetInvocationList())
{
// 若delObj委托指向的是靜態方法,則 item.Target 為null
Console.WriteLine($"{item.Method},{item.Target}");
}
}
}
二.使用委托發送對象狀態通知
以上的示例純粹用來說明委托的作用,因為僅為兩個數相加創建一個委托沒有多大必要,為了更現實的委托應用,我們用委托來定義Car類,它可以通知外部實體當前引擎的狀態。步驟如下:
定義Car類:
public class Car
{
// 內部狀態數據
public int CurrentSpeed { get; set; }
public int MaxSpeed { get; set; }
public string PetName { get; set; }
// 汽車是否可用
private bool CarIsDead;
public Car()
{
MaxSpeed = 100;
}
public Car(string name, int maxSp, int currSp)
{
CurrentSpeed = currSp;
MaxSpeed = maxSp;
PetName = name;
}
// 1. 定義委托類型
public delegate void CarEngineHandler(string msgForCaller);
// 2. 定義每個委托類型的成員變量
private CarEngineHandler listOfHandlers;
// 3. 向調用者添加注冊函數
public void RegisterWithCarEngine(CarEngineHandler methodCall)
{
listOfHandlers = methodCall;
}
// 4. 實現Accelerate()方法以在某些情況下調用委托的調用列表
public void Accelerate(int delta)
{
if (CarIsDead)
{
if (listOfHandlers != null)
{
listOfHandlers("sorry,this car is dead///");
}
}
else
{
CurrentSpeed += delta;
if ((MaxSpeed - CurrentSpeed) == 10 && listOfHandlers != null)
{
listOfHandlers("careful buddy ! gonna blow !");
}
if (CurrentSpeed >= MaxSpeed)
{
CarIsDead = true;
}
else
{
Console.WriteLine($"CurrentSpeed={CurrentSpeed}");
}
}
}
}
注意:我們在調用 listOfHandlers 成員變量保存方法之前,需要堅持該變量是否為空,因為在調用 RegisterWithCarEngine() 方法分配這些對象是 調用者的任務。如果調用者沒有調用這個方法而試圖調用 listOfHandlers ,將引發空異常。
我們來看看具體該如何調用:
public class CarDelegate
{
public static void Show()
{
Console.WriteLine("******Delegate Car******");
// 創建一個Car對象
Car c1 = new Car("bmw", 100, 10);
// 告訴汽車,它想要向我們發送信息時條用哪個方法
c1.RegisterWithCarEngine(new Car.CarEngineHandler(OncarEngineEvent));
// 加速,觸發事件
Console.WriteLine("******Speeding up******");
for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}
}
// 要傳入事件的方法
private static void OncarEngineEvent(string msg)
{
Console.WriteLine($"=>{msg}");
}
}
注意:RegisterWithCarEngine() 方法要求傳入一個內嵌的 CarEngineHandler 的委托的實例,與其他委托一樣,我們指定一個“所指向的方法”作為構造函數的參數。OncarEngineEvent() 方法與相關的委托完全匹配,也包含 string 類型的輸入參數和 void 返回類型。
運行結果如下圖:

a.支持多路廣播
一個委托對象可以維護一個可調用方法的列表而不只是單獨的一個方法。給委托對象添加多個方法時,重載+=操作符即可。為上面的Car類支持多路廣播,修改RegisterWithCarEngine()方法,具體如下:
// 現在支持多路廣播
// 注意藍色"+="操作符,而非賦值操作符"="
public void RegisterWithCarEngine(CarEngineHandler methodCall) { listOfHandlers += methodCall; }
如此一來,調用者就可以為同樣的回調注冊多個目標對象了,這個,第二個采用打印大寫的傳入信息。
public class CarDelegate
{
public static void Show()
{
Console.WriteLine("******Delegate Car******");
// 創建一個Car對象
Car c1 = new Car("bmw", 100, 10);
// 為通知注冊多個目標
c1.RegisterWithCarEngine(new Car.CarEngineHandler(OncarEngineEvent));
c1.RegisterWithCarEngine(new Car.CarEngineHandler(OncarEngineEcentToUpper));
// 加速,觸發事件
Console.WriteLine("******Speeding up******");
for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}
}
//現在在發送通知信息時,Car類將調用以下這兩個方法
private static void OncarEngineEvent(string msg)
{
Console.WriteLine($"=>{msg}");
}
private static void OncarEngineEcentToUpper(string msg)
{
Console.WriteLine($"message form car object=>{msg.ToUpper()}");
}
}
b.從委托調用列表中移除成員
Dlegate類還定義了靜態Remove()方法,允許調用者動態從委托對象的調用列表中移除方法,這樣一來,調用者就在運行是簡單的“退訂”某個已知的通知。你可以直接使用Delegate.Remove(),也可以 “-=” 操作符。
為Car類添加 UnRegisterWithCarEngine() 方法,允許調用者從調用列表中移除某個方法。
public void UnRegisterWithCarEngine(CarEngineHandler methodCall)
{
listOfHandlers -= methodCall;
}
調用如下:
public static void Show()
{
Console.WriteLine("******Delegate Car******");
// 創建一個Car對象
Car c1 = new Car("bmw", 100, 10);
// 告訴汽車,它想要向我們發送信息時條用哪個方法
c1.RegisterWithCarEngine(new Car.CarEngineHandler(OncarEngineEcent));
// += 先綁定委托對象,稍後注銷
Car.CarEngineHandler handler2 = new Car.CarEngineHandler(OncarEngineEcentToUpper);
c1.RegisterWithCarEngine(handler2);
// 加速,觸發事件
Console.WriteLine("******Speeding up******");
for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}
// -= 注銷第二個處理程序
c1.UnRegisterWithCarEngine(handler2);
// 看不到大寫的消息了
Console.WriteLine("******Speeding up******");
for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}
}
c.方法組轉換語法
我們顯示的創建了 Car.CarEngineHandler 委托對象的實例,以注冊和注銷引擎通知:
Car c1 = new Car("bmw", 100, 10);
c1.RegisterWithCarEngine(new Car.CarEngineHandler(OncarEngineEcent));
Car.CarEngineHandler handler2 = new Car.CarEngineHandler(OncarEngineEcentToUpper);
c1.RegisterWithCarEngine(handler2);
c1.UnRegisterWithCarEngine(handler2);
如果要調用MulticastDelegate或Delegate總繼承的任何成員,手工創建一個委托變量是最直接的方式。但是大多數情況下,我們並不依靠委托對象。我們可以使用C#提供的方法組轉換的方法,它允許我們在調用以委托作為參數的方法時直接提供了與委托期望的簽名想匹配的方法的名稱(返回 void,參數 string),而不是創建委托對象。
public class CarDelegateMethodGroupConversion
{
public static void Show()
{
Console.WriteLine("******Delegate Car******");
Car c1 = new Car("slogbug", 100, 10);
// 注冊簡單的類型名稱
c1.RegisterWithCarEngine(CallMethere);
Console.WriteLine("******Speeding up******");
for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}
// 注冊簡單的類型名稱
c1.UnRegisterWithCarEngine(CallMethere);
Console.WriteLine("******Speeding up******");
for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}
}
private static void CallMethere(string msg) => Console.WriteLine($"=>{msg}");
}
四:泛型委托
如果我們希望定義一個委托類型來調用任何返回void並且接受單個參數的方法。如果這個參數可能會不同,我們就可以通過類型參數來構建。
下面我們看一個小示例:
public class GenericDelegate
{
public delegate void MyGenericDelegate<T>(T arg);
public static void Show()
{
// 注冊目標
MyGenericDelegate<string> stringTarget = new MyGenericDelegate<string>(StringTarget);
stringTarget("i am ok");
MyGenericDelegate<int> intTarget = new MyGenericDelegate<int>(IntTarget);
intTarget.Invoke(100);
}
static void StringTarget(string arg) => Console.WriteLine($"StringTarget--> {arg.ToUpper()}");
static void IntTarget(int arg) => Console.WriteLine($"IntTarget--> {++arg}");
}
a. 泛型Action<> 和 Func<> 委托
從以上的學習中我們已經了解到,使用委托在應用程序中進行回調需要遵循以下步驟:
其實,這種方式通常會構建大量只用於當前任務的自定義委托。當委托名無關緊要的時候,我們可以使用框架內置的Action<> 和 Func<> 泛型委托,可指向至多傳遞16個參數的方法。
Action<>:無返回值: 定義 public delegate void Action<...>
public class MyActionDelegate
{
public static void Show()
{
// 使用Action<>委托來指向 DisplayMessage()
Action<string, ConsoleColor, int> actionTarget = new Action<string, ConsoleColor, int>(DisplayMessage);
actionTarget("actionTarget", ConsoleColor.Red, 5);
}
// Action<> 委托的一個目標
private static void DisplayMessage(string msg, ConsoleColor txtColor, int printCount)
{
ConsoleColor previous = Console.ForegroundColor;
Console.ForegroundColor = txtColor;
for (int i = 0; i < printCount; i++)
{
Console.WriteLine(msg);
}
Console.ForegroundColor = previous;
}
}
運行效果如下圖:

Func<>:有返回值 public delegate TResult Func<..., out TResult>
public class FuncDelagate
{
public static void Show()
{
Func<int, int, int> funcTarget = new Func<int, int, int>(Add);
int result = funcTarget(1, 2);
Console.WriteLine(result);
Func<int, int, string> funcTarget2 = new Func<int, int, string>(SumToString);
string sumStr = funcTarget2(3, 4);
Console.WriteLine(sumStr);
}
static int Add(int x, int y) => x + y;
static string SumToString(int x, int y) => (x + y).ToString();
}
運行結果:3,7
鑒於 Action<> 和 Func<> 節省了手工創建自定義委托的步驟,but 總是應該使用他們嗎?
答案:“視情況而定”。
很多情況下 Action<> 和 Func<> 都是首選,但如果你覺得一個具有自定義名稱的委托更有助於捕獲問題范疇,那麼構建自定義委托不過就是一行代碼的事兒。
注:Linq中就大量的用到了 Action<> 和 Func<>。
本文參考《精通C#》
學無止境,望各位看官多多指教。