韓志強(qiáng)
(赤峰學(xué)院 計(jì)算機(jī)科學(xué)與技術(shù)系,內(nèi)蒙古 赤峰 024000)
關(guān)于C#實(shí)現(xiàn)事件驅(qū)動(dòng)機(jī)制的研究
韓志強(qiáng)
(赤峰學(xué)院 計(jì)算機(jī)科學(xué)與技術(shù)系,內(nèi)蒙古 赤峰 024000)
事件驅(qū)動(dòng)機(jī)制是利用回調(diào)函數(shù)(低層的函數(shù)庫(kù)調(diào)用高層的應(yīng)用程序)通過(guò)控制反轉(zhuǎn)而實(shí)現(xiàn)的.事件驅(qū)動(dòng)機(jī)制最重要的兩個(gè)特征是被動(dòng)性和異步性.在C#中可以通過(guò)委托實(shí)現(xiàn)回調(diào)函數(shù),從而實(shí)現(xiàn)控制反轉(zhuǎn),進(jìn)而實(shí)現(xiàn)事件驅(qū)動(dòng)機(jī)制.
事件驅(qū)動(dòng);回調(diào)函數(shù);事件;觀察者模式
隨著.NET平臺(tái)的不斷推廣,現(xiàn)支持.NET開(kāi)發(fā)的編程語(yǔ)言越來(lái)越多.但在眾多編程語(yǔ)言中,只有C#是微軟公司從一開(kāi)始就專門(mén)針對(duì).NET平臺(tái)的CLR進(jìn)行設(shè)計(jì)的語(yǔ)言.由于.NET平臺(tái)需要Windows操作系統(tǒng)的支持,這就要求其支持的語(yǔ)言都必須接受Windows事件驅(qū)動(dòng)的運(yùn)行機(jī)制,C#也不例外.本文下面將對(duì)C#是如何實(shí)現(xiàn)事件驅(qū)動(dòng)模型這方面的內(nèi)容進(jìn)行探究.
廣義上講,事件也稱為消息,是程序中令人關(guān)注的信息狀態(tài)上的變化,其含義比較廣泛;在基于事件驅(qū)動(dòng)的系統(tǒng)中,事件分為內(nèi)建事件(包括底層事件和語(yǔ)義事件)和自定義事件.但是嚴(yán)格說(shuō)來(lái)消息與事件不是同一個(gè)概念.消息是Windows內(nèi)部最基本的通訊方式,事件需要通過(guò)消息來(lái)傳遞,是消息的主要來(lái)源.每當(dāng)用戶觸發(fā)一個(gè)事件,如移動(dòng)鼠標(biāo)或敲擊鍵盤(pán),系統(tǒng)都會(huì)將其轉(zhuǎn)化為消息并放入其相應(yīng)程序的消息隊(duì)列中.
為了了解事件驅(qū)動(dòng)模型,我們先看看利用win 32的API在windows下創(chuàng)建一個(gè)簡(jiǎn)單窗口的步驟:
①創(chuàng)建一個(gè)用于注冊(cè)事件處理器的事件源(發(fā)生事件時(shí),能發(fā)出消息到應(yīng)用程序的消息隊(duì)列)
②編寫(xiě)實(shí)現(xiàn)消息循環(huán)的事件管理器(從消息隊(duì)列獲取消息,并對(duì)消息進(jìn)行解釋分析,然后調(diào)用相應(yīng)的事件處理器)
③編寫(xiě)事件處理器(根據(jù)消息,被系統(tǒng)函數(shù)調(diào)用并執(zhí)行相應(yīng)的處理操作)
通過(guò)對(duì)上述步驟進(jìn)行分析,我們可以得到這樣的結(jié)論,事件驅(qū)動(dòng)機(jī)制是利用回調(diào)函數(shù)(低層的函數(shù)庫(kù)調(diào)用高層的應(yīng)用程序)通過(guò)控制反轉(zhuǎn)而實(shí)現(xiàn)的.事件驅(qū)動(dòng)式最重要的兩個(gè)特征是被動(dòng)性和異步性.被動(dòng)性來(lái)自控制反轉(zhuǎn),異步性來(lái)自會(huì)話切換.
在支持事件驅(qū)動(dòng)的開(kāi)發(fā)環(huán)境中,消息循環(huán)是現(xiàn)成的.許多IDE的圖形編輯器在程序開(kāi)發(fā)人員點(diǎn)擊控件后,還能自動(dòng)生成事件處理器的骨架代碼,連注冊(cè)的步驟也省略了.但我們要知道事件驅(qū)動(dòng)機(jī)制并不局限于GUI應(yīng)用.程序開(kāi)發(fā)人員有時(shí)須自行設(shè)計(jì)整個(gè)事件系統(tǒng),他需要決定:采用事件驅(qū)動(dòng)機(jī)制是否合適?如果合適,如何設(shè)計(jì)事件機(jī)制?其中包括事件定義、事件觸發(fā)、事件偵查、事件轉(zhuǎn)化、事件合并、事件調(diào)度、事件傳播、事件處理等一系列問(wèn)題.
觀察者模式又名發(fā)布/訂閱模式,既是事件驅(qū)動(dòng)模型的簡(jiǎn)化,也是事件驅(qū)動(dòng)模型的核心思想.該模式省略了事件管理器部分,由事件源直接調(diào)用事件處理器的接口.這樣更加簡(jiǎn)明易用,但威力有所削弱,缺少事件管理、事件連帶等機(jī)制.下面將以觀察者模式為例,說(shuō)明C#是如何實(shí)現(xiàn)事件驅(qū)動(dòng)機(jī)制的.
在C#中可以通過(guò)委托(delegate)實(shí)現(xiàn)回調(diào)函數(shù),從而實(shí)現(xiàn)控制反轉(zhuǎn),進(jìn)而實(shí)現(xiàn)事件驅(qū)動(dòng)機(jī)制.下面我們將分步介紹其實(shí)現(xiàn)步驟:
(1)在每個(gè)訂閱者類的內(nèi)部都定義能夠接收發(fā)布者通知的訂閱者方法.
(2)作為訂閱者方法,他們的參數(shù)和返回類型必須與來(lái)自于發(fā)布者類的委托匹配.
(1)在發(fā)布者類內(nèi)定義一個(gè)委托類型,委托類型的定義簽名和需注冊(cè)的訂閱者方法簽名相匹配.
(2)利用已定義的委托類型,定義一個(gè)該委托類型的“委托屬性”,該屬性用來(lái)存儲(chǔ)注冊(cè)的訂閱者方法列表.在發(fā)布者類中,只需一個(gè)委托字段即可存儲(chǔ)所有訂閱者.換言之,來(lái)自同一個(gè)發(fā)布者發(fā)布的通知會(huì)同時(shí)被多個(gè)訂閱者接收到.
(3)再定義一個(gè)成員(屬性或方法均可),負(fù)責(zé)通過(guò)“委托屬性”調(diào)用已注冊(cè)的多個(gè)訂閱者方法.在該環(huán)節(jié)中,必須要判斷“委托屬性”是否為空,如果“委托屬性”為空,在執(zhí)行時(shí)會(huì)引發(fā)一個(gè)Null Reference Exception異常.為了避免這個(gè)問(wèn)題,有必要在觸發(fā)事件之前檢查空值.在這里,我們并不是一開(kāi)始就檢查空值,而是首先將“委托屬性”賦給另一個(gè)委托變量.這個(gè)簡(jiǎn)單的修改可以確保在檢查空值和發(fā)送通知之間,假如所有的訂閱者都被一個(gè)不同的線程移除,也不會(huì)觸發(fā)Null Reference Exception異常.
(1)對(duì)發(fā)布者和多個(gè)訂閱者進(jìn)行實(shí)例化.
(2)向發(fā)布者的“委托屬性”注冊(cè)多個(gè)訂閱者.
(3)然后發(fā)布者通過(guò)“委托屬性”實(shí)現(xiàn)調(diào)用多個(gè)已注冊(cè)的訂閱者方法.
經(jīng)過(guò)上面的步驟我們利用委托實(shí)現(xiàn)了觀察者模式.但是如果我們仔細(xì)分析會(huì)發(fā)現(xiàn),委托結(jié)構(gòu)中存在的缺陷可能造成程序人員不經(jīng)意地引入了一個(gè)問(wèn)題.這個(gè)問(wèn)題和封裝有關(guān),即無(wú)論事件的訂閱方面還是發(fā)布方面,都不能得到充分的控制,外部對(duì)象可以很容易地影響事件的訂閱和發(fā)布.為此C#使用關(guān)鍵字event(事件)來(lái)解決這些問(wèn)題.
利用委托實(shí)現(xiàn)的觀察者模式中,我們?cè)谔砑佑嗛喺叻椒〞r(shí),可以使用運(yùn)算符“=”、“+”、“+=”;在取消訂閱時(shí),可以使用運(yùn)算符“-”、“-=”.由于個(gè)運(yùn)算符的作用不同,很容易出現(xiàn)錯(cuò)用運(yùn)算符的問(wèn)題.所以有必要對(duì)運(yùn)算符的使用作出限制.為此C#提供的event(事件)對(duì)運(yùn)算符作出限制,要求外部類只能使用“+=”添加訂閱,也只能使用“-=”取消訂閱.
在上述實(shí)現(xiàn)的觀察者模式中,已注冊(cè)的多個(gè)訂閱者方法“委托屬性”,除了可以被發(fā)布者調(diào)用外,還可以被其它的對(duì)象所調(diào)用.這會(huì)產(chǎn)生這樣一個(gè)問(wèn)題:當(dāng)所有的訂閱者都接到一個(gè)通知是,但這個(gè)通知卻可能不是發(fā)布者發(fā)布的,而是其它對(duì)象發(fā)布的.為了解決此問(wèn)題,C#提供的event(事件)要求只有包容類(發(fā)布者)才能觸發(fā)(發(fā)布)一個(gè)事件通知.
C#利用關(guān)鍵字event完美地解決了上述的問(wèn)題.因此我們可以對(duì)“利用委托實(shí)現(xiàn)的觀察者模式”進(jìn)行一下改進(jìn),把“委托屬性”移除,改為聲明一個(gè)添加了關(guān)鍵字event的public事件.
上面提到了C#利用event(事件)達(dá)到了解決問(wèn)題的目的.但事件在C#內(nèi)部是如何實(shí)現(xiàn)的呢?
事件實(shí)際上是一個(gè)特殊的委托;當(dāng)C#編譯器獲取到帶有event修飾符的public委托變量時(shí),首先將委托聲明為private;然后,還添加了兩個(gè)方法和兩個(gè)特殊的事件塊.因此從本質(zhì)上說(shuō),event關(guān)鍵字是C#編譯器用于生成恰當(dāng)封裝邏輯的一個(gè)C#快捷方式.
當(dāng)C#編譯器遇到event關(guān)鍵字,就會(huì)自動(dòng)對(duì)其代碼進(jìn)行擴(kuò)展,生成與之等價(jià)的CIL代碼.具體實(shí)現(xiàn)的內(nèi)容為:
(1)C#編譯器在獲取原始事件定義后,首先在原位置上定義一個(gè)private委托變量.這樣一來(lái),該委托在任何外部類都無(wú)法使用——即使是在從它派生的類中.
(2)接著編譯定義兩個(gè)方法,即add_XXX()和remove_XXX().其中,XXX后綴是從原始事件名中截取的.這兩個(gè)方法分別負(fù)責(zé)實(shí)現(xiàn)“+=”、“-=”賦值運(yùn)算符.
(3)最后定義了用event 關(guān)鍵字聲明的兩個(gè)事件塊,其語(yǔ)法與屬性的getter 和setter 非常類似,只是方法名變成了add 和remove.其中add 塊負(fù)責(zé)處理“+=”運(yùn)算符,它將調(diào)用add_XXX()方法實(shí)現(xiàn)添加訂閱;remove 塊負(fù)責(zé)處理“-=”運(yùn)算符,它將調(diào)用 remove_XXX()方法實(shí)現(xiàn)取消訂閱.
這里我們要注意,在最終的CIL代碼中,仍然保留了event關(guān)鍵字.換言之,事件是CIL代碼能夠識(shí)別的一樣?xùn)|西,他并不只是一個(gè)C#構(gòu)造.在CIL代碼中保留一個(gè)等價(jià)的event關(guān)鍵字之后,所有語(yǔ)言和編譯器都能將事件識(shí)別為一個(gè)特殊的類成員,并正確處理它.
通過(guò)上面的敘述,我們看到C#通過(guò)event關(guān)鍵字提供了必要的封裝來(lái)防止外部類發(fā)布一個(gè)通知或者取消之前的訂閱者.解決了普通委托存在的兩個(gè)問(wèn)題,這是C#提供event關(guān)鍵字的關(guān)鍵原因之一.所以利用C#實(shí)現(xiàn)事件驅(qū)動(dòng)機(jī)制的最佳做法就是使用event(事件).
TP 311
A
1673-260X(2010)12-0050-02