定義:委托是一種引用類型,表示對(duì)具有特定參數(shù)列表和返回類型的方法的引用。 在實(shí)例化委托時(shí),你可以將其實(shí)例與任何具有兼容簽名和返回類型的方法相關(guān)聯(lián)
目的:方法聲明和方法實(shí)現(xiàn)的分離,使得程序更容易擴(kuò)展
一、對(duì)委托的理解
1. 為什么將方法作為另一個(gè)方法的參數(shù)
先不解釋定義,看一段代碼
public void Method1(object obj)
{
//內(nèi)部可以訪問(wèn)obj的成員
}
這是隨便寫(xiě)的一個(gè)方法,沒(méi)有實(shí)際意義,但是,根據(jù)我們已掌握的關(guān)于類型的基礎(chǔ)知識(shí),應(yīng)該明白,這里的obj(引用類型)作為形參,存放的是對(duì)象的引用,既然獲取到了對(duì)象的引用,那么我們可以在run方法內(nèi)部對(duì)obj的成員進(jìn)行訪問(wèn)(一段廢話)。好了,現(xiàn)在我要問(wèn)一個(gè)問(wèn)題:為什么要將obj作為參數(shù)?
問(wèn)題先慢慢想著,我們?cè)俅慰匆幌挛械亩x,"是引用類型,對(duì)方法的引用",看下面的代碼
public void Method2(delegate del)
{
//內(nèi)部可以訪問(wèn)del的什么?
//只能執(zhí)行方法
del();
}
delegate 作為一種引用類型,引用的是個(gè)方法,我們能對(duì)方法做什么,只能執(zhí)行方法。
下面我們回答剛才的問(wèn)題,obj作為參數(shù)(類型聲明),將類型的聲明和類型的實(shí)例分離。當(dāng)然這樣做的目的是為了封裝變化,所有類型都可以作為實(shí)參來(lái)使用Method1,因?yàn)镺bject是基類,當(dāng)然也可以將Object換成其他接口類型,只要實(shí)現(xiàn)了該接口的類型都可以作為Method1的實(shí)參。
雖然Method1和Method2參數(shù)類型不一樣,但是目的是一致的,委托是將方法的聲明和實(shí)現(xiàn)分離。
下面看一個(gè)網(wǎng)上使用廣泛的例子
示例1
//定義委托,與任何具有兼容簽名和返回類型的方法相關(guān)聯(lián)
public delegate void GreetingDelegate(string name);
class Program
{
private static void EnglishGreeting(string name)
{
Console.WriteLine("Morning, " + name);
}
private static void ChineseGreeting(string name)
{
Console.WriteLine("早上好, " + name);
}
//將委托類型GreetingDelegate作為形參聲明
private static void GreetPeople(string name, GreetingDelegate MakeGreeting)
{
//這里可以完成其他的業(yè)務(wù)邏輯
//然后調(diào)用委托
MakeGreeting(name);
}
static void Main(string[] args)
{
GreetPeople("hanmeimei", EnglishGreeting);//使用靜態(tài)方法初始化委托
GreetPeople("韓梅梅", new Program().ChineseGreeting);//使用實(shí)例方法初始化委托
Console.ReadKey();
}
}
委托類型GreetingDelegate作為形參聲明,只要是具有兼容簽名和返回類型的方法都可以作為GreetPeople方法的實(shí)參。這樣一來(lái),GreetPeople方法不僅可以通過(guò)中文和英文問(wèn)好了,所有與委托類型GreetingDelegate相關(guān)聯(lián)的語(yǔ)言(方法)都可以問(wèn)好了,程序更容易擴(kuò)展了。
2. 委托是一種引用類型
既然委托是一種類型,應(yīng)該包含類型的成員,我們將代碼編譯完成后,借助反編譯工具看下,編譯后的樣子:

下面這兩種方式是等同的:
MakeGreeting(name);
MakeGreeting.Invoke(name);
而B(niǎo)eginInvoke和EndInvoke是屬于異步調(diào)用的范疇,我們稍后再說(shuō)。
3.Lambda表達(dá)式比匿名方法簡(jiǎn)約
從示例1中可以看到分別顯示調(diào)用了靜態(tài)方法和實(shí)例方法初始化了委托,但是對(duì)于那些只使用一次的方法,就沒(méi)有必要?jiǎng)?chuàng)建具名方法了。C#2.0提出了使用匿名方法代替具名方法的解決方案
GreetingDelegate MakeGreeting = delegate (string name)
{
Console.WriteLine("早上好, " + name);
};
GreetPeople("韓梅梅", MakeGreeting);
匿名方法仍然比較繁瑣,C#3.0引入了Lambda表達(dá)式
//去掉delegate關(guān)鍵字并添加 =>運(yùn)算符
GreetingDelegate MakeGreeting = (string name) =>
{
Console.WriteLine("早上好, " + name);
};
GreetPeople("韓梅梅", MakeGreeting);
//根據(jù)委托的參數(shù)類型推斷,string類型也去掉了
GreetingDelegate MakeGreeting = name =>
{
Console.WriteLine("早上好, " + name);
};
GreetPeople("韓梅梅", MakeGreeting);
4. 委托也是類型安全的
GreetingDelegate MakeGreeting1 = (int name) =>
{
Console.WriteLine("早上好, " + name);
};

5. 泛型委托的協(xié)變和逆變
常用泛型委托
- Action<in T,....>:有參數(shù)無(wú)返回值
- Func<in T1,...,out TResult>:有參數(shù)有返回值,最后一個(gè)類型參數(shù)為返回值類型
關(guān)于泛型委托的協(xié)變和逆變可以閱讀這篇文章《C#基本功之泛型》
有了常用泛型委托,我們就不需要自己定義GreetingDelegate委托類型了,對(duì)委托的使用進(jìn)一步的簡(jiǎn)化了。
//用泛型委托聲明形參,F(xiàn)unc于此類似,只不過(guò)有返回值而已
private static void GreetPeople(string name,Action<string> action)
{
//這里可以完成其他的業(yè)務(wù)邏輯
//然后調(diào)用委托
action.Invoke(name);
}
//調(diào)用
GreetPeople("韓梅梅", name => Console.WriteLine("早上好, " + name))
到目前為止,我們將方法的變化抽象,并用委托封裝,實(shí)現(xiàn)了委托的簡(jiǎn)單使用。但委托還有很大的用處。
委托是類的成員,我們看一個(gè)作為類的成員使用的例子:現(xiàn)在生活中智能設(shè)備越來(lái)越普及,以前需要自己動(dòng)手拉開(kāi)窗簾、打開(kāi)熱水器等等,現(xiàn)在只需要設(shè)定場(chǎng)景,利用智能設(shè)備就可以完成這些操作。
當(dāng)時(shí)間定格為早上7點(diǎn)的時(shí)候,鬧鐘想起,窗簾自動(dòng)打開(kāi),熱水器開(kāi)始燒水,加濕器關(guān)閉.....
//先不用管EventArgs參數(shù),object 類型的sender,可以理解為任何類型都可以傳遞
public delegate void EventHandler(object sender, EventArgs e);
/// <summary>
/// 控制中心
/// </summary>
public class ControlCore
{
public DateTime Time { get; set; } = DateTime.Now;
/// <summary>
/// 執(zhí)行任務(wù)
/// </summary>
public event EventHandler Task;
}
static void Main(string[] args)
{
var controlCore= new ControlCore();
controlCore.Task= new EventHandler(AlarmClock);
//怎么操作委托?
//添加、移除
controlCore.Task+=....;
controlCore.Task-=....;
//或者直接覆蓋掉
controlCore.Task=....;
controlCore.Task(null, null);
}
/// <summary>
/// 鬧鐘
/// </summary>
public static void AlarmClock(object sender, EventArgs e)
{
Console.WriteLine("起床了,親");
}
作為類的成員時(shí),要怎么操作委托?我們都知道委托還可以添加或移除方法,所以我們不僅可以直接調(diào)用委托,還可以添加、移除或者直接覆蓋委托,對(duì)委托的操作沒(méi)有任何限制。如此一來(lái),破壞了類的封裝性。我們希望對(duì)委托有一些限制,就像用屬性去限制字段的輸入輸出一樣;
//將委托的訪問(wèn)級(jí)別改為private
private EventHandler Task;
//為委托添加方法
public void AddTask(EventHandler handler)
{
if (this.Task== null)
this.Task= new EventHandler(handler);
else
this.Task+= new EventHandler(handler);
}
//移除方法
public void RemoveTask(EventHandler handler)
{
System.Delegate.Remove(this.Task, handler);
}
//因?yàn)槲卸x為private,所以需要提供調(diào)用委托的接口
public void OnTask(EventArgs e)
{
//7點(diǎn)了
if (Time.Hour == 7)
{
if (this.Task!= null)
{
this.Task(this, e);
}
}
}
如果對(duì)委托的使用僅僅是添加或移除方法,然后執(zhí)行委托的調(diào)用列表,我相信用事件會(huì)更簡(jiǎn)單容易;
事件是以委托為基礎(chǔ),可以理解為對(duì)委托的進(jìn)一步封裝。
二、對(duì)事件的理解
1. 事件是將委托封裝,并對(duì)外公布了訂閱和取消訂閱的接口
將委托private EventHandler Task;修改為事件public event EventHandler Task;而我們創(chuàng)建的方法AddTask和RemoveTask也需要去掉了,重新生成代碼,通過(guò)反編譯工具可以看到:

事件編譯后生成的兩個(gè)方法,與我們的示例中AddClick和RemoveClick方法類似;同時(shí)可以看到EventHandler委托,已經(jīng)字段變?yōu)閜rivate的訪問(wèn)級(jí)別了(小鎖表示私有);這樣一來(lái),事件幫我們完成了對(duì)委托的“限制“;
在客戶端訪問(wèn)事件也只能+=(訂閱)或者 -=(取消訂閱)了,如果直接用“=“運(yùn)算符賦值就會(huì)報(bào)錯(cuò)(在Control類內(nèi)容還是可以的);

2. 事件使用 發(fā)布-訂閱(publisher-subscriber) 模型
發(fā)布者:包含事件的類用于觸發(fā)事件,而這個(gè)類稱為事件的“發(fā)布者”。
通過(guò)聲明委托類型的事件,將委托與事件關(guān)聯(lián)。發(fā)布者對(duì)象調(diào)用這個(gè)事件,并通知訂閱者對(duì)象
訂閱者:其他注冊(cè)該事件的類稱為“訂閱者”。
訂閱者注冊(cè)事件并提供事件處理程序(鬧鐘、打開(kāi)窗簾、熱水器燒水等),在發(fā)布者類中通過(guò)委托調(diào)用訂閱者的事件處理程序。
public delegate void EventHandler(object sender, EventArgs e);
/// <summary>
/// 控制中心
/// </summary>
public class ControlCore
{
public DateTime Time { get; set; } = DateTime.Now;
/// <summary>
/// 執(zhí)行任務(wù)
/// </summary>
public event EventHandler Task;
/// <summary>
/// 觸發(fā)事件
/// </summary>
/// <param name="e"></param>
public void OnTask(EventArgs e)
{
//7點(diǎn)了
if (Time.Hour == 7)
{
if (this.Task != null)
{
this.Task(this, e);
}
}
}
}
訂閱者類中的事件處理程序
//下面的方法分別屬于訂閱者類中的方法,篇幅有限,沒(méi)有單獨(dú)聲明每一個(gè)訂閱者類
/// <summary>
/// 鬧鐘響起
/// </summary>
public static void AlarmClock(object sender, EventArgs e)
{
var core = (ControlCore)sender;
Console.WriteLine(core.Time.Hour+"點(diǎn)了,起床了,親");
}
/// <summary>
/// 打開(kāi)窗簾
/// </summary>
public static void OpenWindow(object sender, EventArgs e)
{
var core = (ControlCore)sender;
Console.WriteLine(core.Time.Hour + "點(diǎn)了,打開(kāi)窗簾");
}
/// <summary>
/// 熱水器燒水
/// </summary>
public static void BoilWater(object sender, EventArgs e)
{
var core = (ControlCore)sender;
Console.WriteLine(core.Time.Hour + "點(diǎn)了,熱水器開(kāi)始燒水");
}
客戶端代碼
static void Main(string[] args)
{
var controlCore = new ControlCore();
controlCore.Task += new EventHandler(AlarmClock);
controlCore.Task += OpenWindow;
controlCore.Task += BoilWater;
while (true)
{
controlCore.OnTask(null);
Console.ReadKey();
}
}
執(zhí)行結(jié)果
7點(diǎn)了,起床了,親
7點(diǎn)了,打開(kāi)窗簾
7點(diǎn)了,熱水器開(kāi)始燒水
3. 關(guān)于sender和EventArgs
在示例代碼中,可以看到將ControlCore本身作為參數(shù)傳遞給訂閱者,既然訂閱了發(fā)布者的動(dòng)態(tài),那么關(guān)于發(fā)布者的某些信息或許感興趣(比如ControlCore中的Time)。
而EventArgs是作為發(fā)布者信息之外的信息傳遞
//自定義參數(shù)類型
public class CustomEventArgs: EventArgs
{
public string Arg1 { get; set; }
public string Arg2 { get; set; }
}
用CustomEventArgs替換委托中的參數(shù)EventArgs。public delegate void EventHandler(object sender, CustomEventArgs e)
那么在客戶端調(diào)用時(shí)傳入更多的信息
controlCore.OnTask(new ButtonEventArgs()
{
Arg1="",
Arg2=""
});
總結(jié):一直想寫(xiě)一篇關(guān)于委托和事件的文章,但是網(wǎng)上已經(jīng)有很多這類優(yōu)秀的文章了,不乏一些佼佼者,由淺入深,從無(wú)到有的風(fēng)格將知識(shí)點(diǎn)講的很透徹。如果我再按照這個(gè)類型去寫(xiě),實(shí)在沒(méi)有意思。
所以我想,我們可不可以從已知到未知這條路徑來(lái)將知識(shí)點(diǎn)講明白,比如,我們知道將具有相同屬性和行為的對(duì)象抽象為類型。那么我是不是可以將具有相同簽名和返回類型的方法抽象為委托?再比如,我們知道屬性封裝了字段,并對(duì)字段的輸入輸出進(jìn)行了限制。那么我是不是可以將委托封裝,控制委托的注冊(cè)或取消,這樣就引出了事件。
希望可以幫助到朋友們
注:.NET關(guān)于委托和事件的編碼規(guī)范
- 委托類型的名稱都應(yīng)該以EventHandler結(jié)束。
- 事件的命名為 委托去掉 EventHandler之后剩余的部分。
- 委托的原型定義:有一個(gè)void返回值,并接受兩個(gè)輸入?yún)?shù):一個(gè)Object 類型,一個(gè) EventArgs類型(或繼承自EventArgs)。
- 繼承自EventArgs的類型應(yīng)該以EventArgs結(jié)尾。