一、理解事件
事件采用發(fā)布/訂閱模型欲逃,其中發(fā)行者決定在什么情況下引發(fā)事件,而訂戶決定為響應(yīng)事件而執(zhí)行的操作饼暑。事件可以有多個(gè)訂戶稳析,在這種情況下,將在事件被引發(fā)時(shí)同步調(diào)用事件處理程序弓叛。如果沒有訂戶彰居,事件將不會(huì)被引發(fā)。訂戶可處理來自多個(gè)發(fā)行者的多個(gè)事件撰筷。
ps:委托(delegate)
在傳統(tǒng)編程術(shù)語中陈惰,事件是一種回調(diào),能夠?qū)⒎椒ㄗ鳛閰?shù)傳遞給另一個(gè)方法毕籽。在 C#中抬闯,這些回調(diào)被稱為委托。事件處理程序不過是通過委托調(diào)用的方法关筒。
委托類似于C和C++中的函數(shù)指針以及Delphi封裝(closure)溶握,是一種類型安全的回調(diào)編寫方法。委托在調(diào)用方(而不是聲明方)的安全權(quán)限下運(yùn)行蒸播。
委托類型定義了一個(gè)方法簽名睡榆,可將任何具有兼容簽名的方法與委托相關(guān)聯(lián)。委托簽名和常規(guī)方法簽名之間的一個(gè)不同之處是袍榆,前者包含返回類型胀屿,可在參數(shù)列表中使用修飾符params。
二包雀、訂閱和取消訂閱
要響應(yīng)另一個(gè)類發(fā)布的事件宿崭,可訂閱它:定義一個(gè)事件處理程序,其簽名與事件的委托簽名匹配才写;然后使用加法賦值運(yùn)算符(+ =)將事件處理程序與事件相關(guān)聯(lián)葡兑。如下代碼演示了如何訂閱一個(gè)基于委托ElapseEventHandler的事件奴愉。
var timer = new Timer(1000);
timer.Elapsed +=new ElapsedEventHandler(TimerElapsedHandler);
void TimerElapsedHandler(object sender,ElapsedEventArgs e)
{
MessageBox.Show("The timer has expired.");
}
第一個(gè)參數(shù)總是名為 sender,其類型為 object铁孵,它表示引發(fā)事件的對(duì)象锭硼;第二個(gè)參數(shù)是傳遞給事件處理程序的數(shù)據(jù),名為e蜕劝,其類型為EventArgs或從EventArgs派生而來的類型檀头。事件處理程序的返回類型總是為void。
ps:方法組推斷
上述程序演示了關(guān)聯(lián)事件處理程序的傳統(tǒng)方法岖沛,但 C#允許使用一種更簡單的語法暑始,這被稱為方法組推斷(method group inference)。下面使用方法組推斷語法重寫了上述程序:
var timer = new Timer(1000);
timer.Elapsed += TimerElaspsedHandler;
void TimerElapsedHandler(object sender,ElapsedEventArgs e)
{
MessageBox.Show("The timer has expired.");
}
雖然Visual Studio自動(dòng)使用傳統(tǒng)語法來關(guān)聯(lián)事件處理程序婴削,但是當(dāng)關(guān)聯(lián)自己的事件處理程序時(shí)廊镜,通常使用方法組推斷語法。
可以任何方式給事件處理程序命名唉俗,但是為了確保一致性嗤朴,最好通過組合如下部分來生成名稱:提供事件的對(duì)象名、要處理的事件的名稱以及字樣Handler虫溜。
雖然很多類都使用這種方式來訂閱事件雹姊,但是Visual Studio使得訂閱事件很容易,尤其是訂閱用戶界面控件發(fā)布的事件時(shí)衡楞。
當(dāng)你不希望事件被引發(fā)時(shí)調(diào)用相應(yīng)的事件處理程序吱雏,必須取消訂閱該事件。另外瘾境,訂戶對(duì)象刪除前歧杏,必須取消訂閱事件;否則迷守,發(fā)行者將繼續(xù)保存一個(gè)引用犬绒,它指向表示訂戶事件處理程序的委托,這將禁止垃圾收集器釋放訂戶對(duì)象盒犹。
要取消訂閱事件懂更,可刪除XAML標(biāo)記中相應(yīng)的屬性或使用減法賦值運(yùn)算符(? =)眨业,如下所示急膀。
timer.Elapsed -= TimerElapsedHandler;
ps:匿名方法
匿名方法讓你能夠編寫一個(gè)未命名的內(nèi)聯(lián)語句塊,并在調(diào)用委托時(shí)執(zhí)行它龄捡。
如下代碼使用的是匿名方法卓嫂,而不是命名委托:
var timer = new Timer(1000);
timer.Elapsed += delegate(object sender,ElapsedEventArgs e)
{
MessageBox.Show("The timer has expired.");
}
雖然將匿名方法用作事件處理程序帶來了很多方便之處,但是取消訂閱事件時(shí)將不那么容易
三聘殖、發(fā)布事件
類和結(jié)構(gòu)都可發(fā)布事件(雖然事件通常用于類中)晨雳,為此只需使用簡單的事件聲明行瑞。事件可基于任何有效的委托類型,但是標(biāo)準(zhǔn)做法是讓事件基于委托 EventHandler 和EventHandler<T>餐禁。這些委托是在.NET Framework中預(yù)定義的血久,專門用于定義事件。
定義自己的事件時(shí)帮非,要做出的第一個(gè)決策是氧吐,是否要將自定義數(shù)據(jù)發(fā)送給事件。.NET Framework 提供了 EventArgs 類末盔,預(yù)定義的事件委托類型都支持它筑舅。如果要向事件發(fā)送自定義數(shù)據(jù),需要從EventArgs派生出一個(gè)新類陨舱;否則翠拣,可直接使用EventArgs類型,但以后就不能修改它了游盲,否則將破壞兼容性误墓。因此,總是應(yīng)該創(chuàng)建一個(gè)從EventArgs派生而來的新類(哪怕這個(gè)類最初為空)益缎,以便以后能夠靈活地添加數(shù)據(jù)优烧。
如下代碼是一個(gè)從EventArgs派生而來的類
public class CustomEventArgs:System.EventArgs
{
private object data;
public CustomEventArgs(object data)
{
this.data = data;
}
public Object Data
{
get
{
return this.data;
}
}
}
聲明事件時(shí),通常使用類似于字段的語法链峭。如果沒有從 EvnetArgs 派生出新類畦娄,就使用委托類型EventHandler,如下所示
public class Contact
{
public event EventHandler AddresChanged;
}
如果從 EventArgs 派生出了新類弊仪,就需要使用泛型委托 EventHandler<T>熙卡,并用從EventArgs派生而來的類替換T。
雖然通常使用類似于字段的事件定義励饵,但是它并非總是效率最高的驳癌,尤其是當(dāng)類包含大量事件時(shí)。類包含大量事件時(shí)役听,通常只有其中的幾個(gè)事件有訂戶颓鲜。使用字段聲明語法時(shí),每個(gè)事件都需單獨(dú)聲明典予,這帶來了大量不必要的開銷甜滨。
為解決這種問題,C#還允許使用屬性語法定義事件瘤袖,如下所示
public class Contact
{
private EventHandlerList events = new EventHandlerList();
private static readonly object addressChangedEventKey = new object();
public event EventHandler AddressChanged
{
add
{
this.events.AddHandler(addressChangedEventKey, value);
}
remove
{
this.events.RemoveHandler(addressChangedEventKey, value);
}
}
}
第3行聲明了一個(gè)EventHandlerList變量衣摩,這種類型專門設(shè)計(jì)用于存儲(chǔ)事件委托列表,這樣對(duì)于所有有訂戶的事件捂敌,都可在一個(gè)變量中存儲(chǔ)其對(duì)應(yīng)的列表項(xiàng)艾扮。接下來既琴,第4行聲明了一個(gè)只讀的靜態(tài)object變量,該變量名為addressChangedEventKey泡嘴,它是在EventHandlerList中用于表示事件的鍵甫恩。最后,第6~16行聲明了實(shí)際的事件酌予。
訪問器add將委托實(shí)例加入列表填物,而remove將其刪除。這兩個(gè)訪問器都使用預(yù)定義的鍵來添加和刪除實(shí)例霎终。
對(duì)于事件滞磺,一種方便而一致的描述方法是,將其分為事前事件和事后事件莱褒。
事后事件最常見击困,它在對(duì)象的狀態(tài)發(fā)生變化后發(fā)生。事前事件也稱為可撤銷的事件广凸,它在對(duì)象狀態(tài)發(fā)生變化前發(fā)生阅茶,讓你能夠撤銷事件;這些事件使用 CancelEventArgs 類來存儲(chǔ)事件數(shù)據(jù)谅海,這個(gè)類添加了一個(gè)Cancel屬性脸哀,可在代碼中讀寫它。創(chuàng)建自己的可撤銷事件時(shí)扭吁,應(yīng)從CancelEventArgs類派生自定義的事件數(shù)據(jù)類撞蜂。
四、引發(fā)事件
如果沒有發(fā)起事件的機(jī)制侥袜,定義事件就沒有多大意義蝌诡。發(fā)起事件也稱為引發(fā)或觸發(fā)事件,它遵循一種標(biāo)準(zhǔn)模式枫吧。通過遵循模式浦旱,使用事件將更容易,因?yàn)橄嚓P(guān)的結(jié)構(gòu)定義明確且一致九杂。
如下演示了完整的事件處理程序
public class Contact
{
public event EventHandler<AddressChangedEventArgs> AddressChanged;
private string address;
protected virtual void OnAddressChaged(AddressChangeEventArgs e)
{
EventHandler<AddressChangedEventArgs> handler = AddressChanged;
if (handler != null)
{
handler (this, e);
}
}
public string Address
{
get
{
return this.address;
}
set
{
this.address = value;
AddressChangedEventArgs args = new AddressChangedEventArgs (this.address);
OnAddressChaged (args);
}
}
}
第3行使用委托EventHandler<T>聲明了事件颁湖。第7~14行聲明了一個(gè)受保護(hù)的虛方法,用于引發(fā)該事件例隆。通過將這個(gè)方法聲明為protected和virtual甥捺,讓派生類能夠通過重寫這個(gè)方法(而不是訂閱事件)來處理這個(gè)事件;對(duì)派生類來說裳擎,這是一種更方便涎永、更自然的機(jī)制思币。最后鹿响,第22~23行創(chuàng)建了一個(gè)新的AddressChangedEventArgs實(shí)例并引發(fā)了事件羡微。如果事件沒有自定義數(shù)據(jù),就可以使用EventArgs.Empty字段表示空EventArgs惶我。
ps:引發(fā)使用屬性語法定義的事件
如果事件是使用屬性語法定義的妈倔,那么在用于引發(fā)事件的方法中,需要以稍微不同的方式從句柄列表中獲取事件句柄绸贡,如下所示盯蝴。
protected virtual void OnAddressChaged(AddressChangeEventArgs e)
{
var handler = events [addressChangedEventKey] as EventHandler<AddressChangedEventArgs>;
if (handler != null)
{
handler (this, e);
}
}
根據(jù)約定,給引發(fā)事件的方法命名時(shí)听怕,以 On 打頭捧挺,然后是事件的名稱。對(duì)于非密封類的非靜態(tài)事件尿瞭,事件引發(fā)方法應(yīng)聲明為protected和virtual闽烙。對(duì)于靜態(tài)事件、密封類的非靜態(tài)事件以及結(jié)構(gòu)的事件声搁,事件引發(fā)方法應(yīng)聲明為公有的黑竞。事件引發(fā)方法的返回類型總是void,且只接收一個(gè)參數(shù)疏旨,該參數(shù)名為e很魂,其類型為EventArg或其合適的派生類。
這個(gè)方法遵循一種標(biāo)準(zhǔn)模式檐涝,即創(chuàng)建事件的一個(gè)臨時(shí)備份(第 9 行)遏匆,以免出現(xiàn)競(jìng)態(tài)條件(race condition):在 null檢查(第 10行)和引發(fā)事件(第 12行)之間,最后一個(gè)訂戶取消訂閱谁榜。
多線程和事件
這種模式只能避免一種可能的競(jìng)態(tài)條件—事件在檢查后變成了null拉岁;僅當(dāng)代碼是多線程時(shí),遵循這種模式才顯得重要惰爬。編寫多線程事件時(shí)喊暖,必須應(yīng)對(duì)眾多復(fù)雜的情形。例如撕瞧,在執(zhí)行依賴于某種狀態(tài)的代碼前陵叽,必須確保以線程安全的方式提供了這種狀態(tài)。