網(wǎng)上講C#委托和事件
的博文已經(jīng)非常多了,其中也不乏一些深入淺出、條理清晰的文章越败。我之所以還是繼續(xù)寫触幼,主要是借機(jī)整理學(xué)習(xí)筆記硼瓣、歸納總結(jié)從而理解更透徹,當(dāng)然能夠以自己的理解和思路給其他人講明白更好置谦。
另外堂鲤,太長(zhǎng)的文章會(huì)讓很多讀者失去興趣,所以我決定把這篇分成四個(gè)部分來(lái)介紹媒峡。分別是委托的基礎(chǔ)瘟栖、委托的進(jìn)階、事件的基礎(chǔ)和事件的進(jìn)階谅阿。對(duì)使用委托與事件要求不高的同學(xué)可以跳過(guò)進(jìn)階部分半哟。
本文開始講最后一部分,事件的進(jìn)階签餐,前三節(jié)請(qǐng)內(nèi)容請(qǐng)參見(jiàn)C#委托與事件(1)寓涨、C#委托與事件(2)和C#委托與事件(3)。
7. 事件的高級(jí)知識(shí)
(1) 揭開事件的神秘面紗
回到上一節(jié)中我們聲明的事件public event BoilHandler BoilEvent;
氯檐,這里的事件雖然被聲明為公開的戒良,但上一節(jié)最后我們?cè)噲D對(duì)h.BoilEvent
進(jìn)行賦值操作會(huì)出現(xiàn)編譯錯(cuò)誤。為什么會(huì)這樣呢冠摄?看看編譯后的BoilEvent
你就明白了糯崎。
private BoilHandler BoilEvent; // 事件實(shí)際上被聲明為私有的委托變量
[MethodImpl(MethodImplOptions.Synchronized)]
public void add_BoilEvent(BoilHandler value)
{ // 在事件上綁定方法實(shí)際上是間接調(diào)用Delegate.Combine()
this.BoilEvent= (BoilHandler) Delegate.Combine(this.BoilEvent, value);
}
[MethodImpl(MethodImplOptions.Synchronized)]
public void remove_BoilEvent(BoilHandler value)
{ // 在事件上解除綁定方法實(shí)際上是間接調(diào)用Delegate.Remove()
this.BoilEvent = (BoilHandler) Delegate.Remove(this.BoilEvent, value);
}
上面的代碼進(jìn)一步說(shuō)明,實(shí)際上事件就是封裝了的委托河泳,而且我們只需要鍵入event
關(guān)鍵字沃呢,編譯器就可以幫我們把相關(guān)的方法自動(dòng)生成。另外拆挥,代表事件本身的CIL
代碼將使用.addon
和.removeon
指令對(duì)應(yīng)要調(diào)用的add_XXX()
和remove_XXX()
方法的名稱薄霜。
.event MyFirstEvent.Heater/BoilHandler BoilEvent
{
.addon instance void MyFirstEvent.Heater::add_BoilEvent(class MyFirstEvent.Heater/BoilHandler)
.removeon instance void MyFirstEvent.Heater::remove_BoilEvent(class MyFirstEvent.Heater/BoilHandler)
}
(2) 重申事件的必要性
我們?cè)?strong>第6節(jié)從封裝性的角度分析了“為什么要用事件”,上面部分又說(shuō)明了實(shí)際上event
就是封裝的delegate
,所以事件在易用性上又占了上風(fēng)黄锤,因?yàn)?code>event關(guān)鍵字能夠幫助我們節(jié)省很多代碼的鍵入搪缨。另外,張子陽(yáng)又從設(shè)計(jì)模式的角度解釋了“為什么要使用事件而不是委托”鸵熟。我覺(jué)得很有道理副编,所以將借用他的例子來(lái)重申一下他的觀點(diǎn)。
首先我們定義一個(gè)帶有一個(gè)int
參數(shù)的委托NumberChangedEventHandler
流强,然后再定義一個(gè)發(fā)布者和訂閱者痹届。其中發(fā)布者類中可以聲明上述委托的變量或者對(duì)應(yīng)的事件,發(fā)布者通過(guò)DoSomething()
方法來(lái)執(zhí)行委托變量或事件上綁定的方法打月。而訂閱者類中則聲明了訂閱的方法即實(shí)際上應(yīng)該綁定到委托的方法队腐。所以,用通俗的語(yǔ)言來(lái)解釋就是奏篙,訂閱者訂閱了某種消息柴淘,發(fā)布者在它的某種行為中滿足了某種條件后觸發(fā)某件事情的發(fā)生,而這件事情的發(fā)生正是訂閱者訂閱的東西秘通,所以發(fā)布者觸發(fā)某事件后即會(huì)通知到所有的訂閱者为严。
(a) 如果聲明委托變量,那么在Main
函數(shù)也就是客戶端中可以通過(guò)pub.NumberChanged(100);
來(lái)觸發(fā)事件肺稀,這不滿足事件由發(fā)布者在其內(nèi)部的某種行為下觸發(fā)的設(shè)計(jì)模式第股。
(b) 如果聲明為事件,則它的封裝性限制了客戶端的行為(直接調(diào)用pub.NuberChanged
會(huì)出現(xiàn)編譯錯(cuò)誤)话原,從而保證了事件只能供其他類型訂閱夕吻。
namespace PubSub
{
public delegate void NumberChangedEventHandler(int count); //定義委托
public class Publishser
{
private int count;
//public NumberChangedEventHandler NumberChanged; // 聲明委托變量
public event NumberChangedEventHandler NumberChanged; // 聲明一個(gè)事件
public void DoSomething()
{
// todo: do something
if (++count > 0)
{
if (NumberChanged != null)
{
NumberChanged(count);
}
}
}
}
public class Subscriber
{
public void OnNumberChanged(int count)
{
Console.WriteLine("Subscriber notified: count = {0}", count);
}
}
class Program
{
static void Main(string[] args)
{
Publishser pub = new Publishser();
Subscriber sub = new Subscriber();
pub.NumberChanged += new NumberChangedEventHandler(sub.OnNumberChanged);
pub.DoSomething(); // 通過(guò)Pub DoSomething()來(lái)觸發(fā)事件
//pub.NumberChanged(100); // 委托變量可以被這樣直接(不恰當(dāng)?shù)?調(diào)用,但事件不行
}
}
}
(3) 自定義事件的參數(shù)與返回值
(a) 事件的參數(shù)
微乳推薦的事件模式帶有兩個(gè)參數(shù)object
和EventArgs
繁仁,其中object
參數(shù)表示一個(gè)隊(duì)發(fā)送事件的對(duì)象的引用涉馅,EventArgs
則表示與該事件相關(guān)的信息,它可以是表示不發(fā)送任何信息的基類EventArgs
改备,也可以是派生自EventArgs
的類的實(shí)例控漠。
To declare an event inside a class, first a delegate type for the event must be declared, if none is already declared.
public delegate void ChangedEventHandler(object sender, EventArgs e);
The delegate type defines the set of arguments that are passed to the method that handles the event.
所以,接下來(lái)我們按照規(guī)范的方式改寫上面的程序悬钳,為了增加其他類型的參數(shù)盐捷,我們還會(huì)在發(fā)布者中傳遞一些字符串類型的message
給訂閱者。
namespace PubSub
{
public delegate void NumberChangedEventHandler(object sender, ChangedEventArgs e); //定義委托
public class ChangedEventArgs : EventArgs
{
public readonly int count;
public readonly string msg;
public ChangedEventArgs(int cnt, string str)
{
count = cnt;
msg = str;
}
}
public class Publishser
{
private int count;
public event NumberChangedEventHandler NumberChanged; // 聲明一個(gè)事件
public void DoSomething()
{
// todo: do something
if (++count > 0)
{
if (NumberChanged != null)
{
NumberChanged(this, new ChangedEventArgs(count, "Something happened"));
}
}
}
}
public class Subscriber
{
public void OnNumberChanged(object sender, ChangedEventArgs e)
{
Console.WriteLine("Subscriber notified from [{0}]: \ncount = [{1}], msg = [{2}]", sender, e.count, e.msg);
}
}
class Program
{
static void Main(string[] args)
{
Publishser pub = new Publishser();
Subscriber sub = new Subscriber();
pub.NumberChanged += new NumberChangedEventHandler(sub.OnNumberChanged);
pub.DoSomething();
}
}
}
輸出結(jié)果:
Subscriber notified from [PubSub.Publishser]:
count = [1], msg = [Something happened]
(b) 事件的返回值
一般情況下默勾,最好讓事件的返回值為空碉渡。因?yàn)?br>
首先,事件上可以綁定多個(gè)方法母剥,這些方法是依次執(zhí)行的滞诺,如果每個(gè)方法都有返回值形导,則需要發(fā)布者顯示地依次執(zhí)行每個(gè)方法,并處理相應(yīng)的返回值习霹。否則的話只有最后一個(gè)方法的返回值能被發(fā)布者獲得朵耕。
其次,發(fā)布者與訂閱者之間是松耦合的淋叶,發(fā)布者并不關(guān)心有誰(shuí)訂閱它的事件阎曹,更不會(huì)關(guān)心訂閱者返回的東西。
(4) 事件的異常處理
還是上面的例子煞檩,我們?cè)俳o發(fā)布者多加兩個(gè)訂閱者处嫌,并讓其中一個(gè)訂閱者的方法拋出異常。我們可以回顧一下斟湃,訂閱者的方法被綁定到事件后熏迹,發(fā)布者一旦滿足某種條件后觸發(fā)該事件。而觸發(fā)該事件時(shí)發(fā)布者要做的事情就是依次執(zhí)行該事件上綁定的方法凝赛,即執(zhí)行NumberChanged(this, EventArgs.Empty);
這句話注暗。所以我們嘗試對(duì)這句話進(jìn)行結(jié)構(gòu)化異常處理。
namespace PubSub
{
public delegate void NumberChangedEventHandler(object sender, EventArgs e); //定義委托
public class Publishser
{
private int count;
public event NumberChangedEventHandler NumberChanged; // 聲明一個(gè)事件
public void DoSomething()
{
// todo: do something
if (++count > 0)
{
if (NumberChanged != null)
{
try
{
NumberChanged(this, EventArgs.Empty);
}
catch (Exception e)
{
Console.WriteLine("Exception: {0}", e.Message);
}
}
}
}
}
public class Subscriber1
{
public void OnNumberChanged(object sender, EventArgs e)
{
Console.WriteLine("Subscriber1 invoked");
}
}
public class Subscriber2
{
public void OnNumberChanged(object sender, EventArgs e)
{
throw new Exception("Subscriber2 threw exception");
}
}
public class Subscriber3
{
public void OnNumberChanged(object sender, EventArgs e)
{
Console.WriteLine("Subscriber3 invoked");
}
}
class Program
{
static void Main(string[] args)
{
var pub = new Publishser();
var sub1 = new Subscriber1();
var sub2 = new Subscriber2();
var sub3 = new Subscriber3();
pub.NumberChanged += new NumberChangedEventHandler(sub1.OnNumberChanged);
pub.NumberChanged += new NumberChangedEventHandler(sub2.OnNumberChanged);
pub.NumberChanged += new NumberChangedEventHandler(sub3.OnNumberChanged);
pub.DoSomething();
}
}
}
輸出結(jié)果:
Subscriber1 invoked
Exception: Subscriber2 threw exception
結(jié)果顯示哄酝,第2個(gè)訂閱者拋出異常后發(fā)布者確實(shí)捕捉到了該異常友存,但是遺憾的是第3個(gè)訂閱者的方法并沒(méi)有被執(zhí)行。也就是說(shuō)事件上綁定的方法一旦出現(xiàn)了異常就終止了整個(gè)事件的處理陶衅,這樣就影響到了后面的訂閱者。所以對(duì)于這種情況直晨,我們需要先獲得事件上的委托鏈表搀军,然后再在遍歷鏈表的循環(huán)中處理異常。修改上面的DoSomething()
方法如下:
public void DoSomething()
{
// todo: do something
if (++count > 0)
{
if (NumberChanged != null)
{
Delegate[] delArray = NumberChanged.GetInvocationList();
foreach (Delegate del in delArray)
{
NumberChangedEventHandler method = (NumberChangedEventHandler)del; // 強(qiáng)制轉(zhuǎn)換為具體的委托類型
try
{
method(this, EventArgs.Empty);
}
catch (Exception e)
{
Console.WriteLine("Exception: {0}", e.Message);
}
}
}
}
}
輸出結(jié)果:
Subscriber1 invoked
Exception: Subscriber2 threw exception
Subscriber3 invoked
(5) 泛型EventHandler<T>委托
既然有了上面提到的第一個(gè)參數(shù)為object
勇皇,第二個(gè)參數(shù)為EventArgs
派生類型的規(guī)范來(lái)書寫自定義委托罩句,我們肯定大多時(shí)候都是定義類似這樣的委托。所以C#
中還提供了EventHandler<T>
泛型類型來(lái)簡(jiǎn)化我們得自定義過(guò)程敛摘。我們直接將它應(yīng)用到上面的例子(3)(a)中即可得到:
namespace PubSub
{
//public delegate void NumberChangedEventHandler(object sender, ChangedEventArgs e); //不再需要定義委托
public class ChangedEventArgs : EventArgs
{
public readonly int count;
public readonly string msg;
public ChangedEventArgs(int cnt, string str)
{
count = cnt;
msg = str;
}
}
public class Publishser
{
private int count;
public event EventHandler<ChangedEventArgs> NumberChanged; // 利用EventHanler泛型聲明一個(gè)事件
public void DoSomething()
{
// todo: do something
if (++count > 0)
{
if (NumberChanged != null)
{
NumberChanged(this, new ChangedEventArgs(count, "Something happened"));
}
}
}
}
public class Subscriber
{
public void OnNumberChanged(object sender, ChangedEventArgs e)
{
Console.WriteLine("Subscriber notified from [{0}]: \ncount = [{1}], msg = [{2}]", sender, e.count, e.msg);
}
}
class Program
{
static void Main(string[] args)
{
Publishser pub = new Publishser();
Subscriber sub = new Subscriber();
pub.NumberChanged += new EventHandler<ChangedEventArgs>(sub.OnNumberChanged);
pub.DoSomething();
}
}
}
8. 委托與事件的編碼規(guī)范
終于把委托與事件學(xué)習(xí)完了门烂,但是整篇文章中的代碼并不規(guī)范,尤其是前面部分兄淫。因此屯远,最后我們?cè)賮?lái)簡(jiǎn)單談?wù)?code>.Net Framework的編碼規(guī)范,以免我的code
誤導(dǎo)了大家捕虽。
(1) 委托類型的名稱應(yīng)以EventHandler
結(jié)束慨丐,如果沒(méi)有什么特征要求,則可以直接使用泛型委托定義EventHandler<T>
泄私。
(2) 委托的原型定義:有一個(gè)void
返回值房揭,并接受兩個(gè)輸入?yún)?shù):一個(gè)是object
類型备闲,一個(gè)是EventArgs
類或其派生類。
(3) 繼承自EventArgs
的類型應(yīng)該以EventArgs
結(jié)尾捅暴。
(4) 事件的命名為: 委托定義名去掉EventHandler
之后剩余的部分恬砂。
(5) 訂閱事件的方法的命名通常為: On事件名
。
寫在最后蓬痒,如果您從頭到尾看到了這里觉既,那真的是非常感謝您的信任與支持。有任何疑問(wèn)或者不妥之處乳幸,歡迎指正瞪讼!再次感謝!
參考文獻(xiàn):
《精通C#》
C# 中的委托和事件
C# 中的委托和事件(續(xù))
Events Tutorial