.Net WInform開發(fā)筆記(五)關(guān)于事件Event
引子:
.net中事件最常用在“觀察者”設(shè)計(jì)模式中,事件的發(fā)布者(subject)定義一個(gè)事件,事件的觀察者(observer)注冊(cè)這個(gè)事件,當(dāng)發(fā)布者激發(fā)該事件時(shí),所有的觀察者就會(huì)響應(yīng)該事件(表現(xiàn)為調(diào)用各自的事件處理程序)。知道這個(gè)邏輯過程后,我們可以寫出以下代碼:
ViewCode
ClassSubject
{
publiceventXXEventHandlerXX;
protectedvirtualvoidOnXX(XXEventArgse)
{
If(XX!=null)
{
XX(this,e);
}
}
publicvoidDoSomething()
{
//符合某一條件
OnXX(newXXEventArgs());
}
}
delegatevoidXXEventHandler(objectsender,XXEventArgse);
ClassXXEventArgs:EventArgs
{
}
以上就是一個(gè)最最原始的含有事件類的定義。外部對(duì)象可以注冊(cè)Subject對(duì)象的XX事件,當(dāng)某一條件滿足時(shí),Subject對(duì)象就會(huì)激發(fā)XX事件,所以觀察者作出響應(yīng)。
注:編碼中請(qǐng)按照標(biāo)準(zhǔn)的命名方式,事件名、事件參數(shù)名、虛方法名、參數(shù)名等等,標(biāo)準(zhǔn)請(qǐng)參考微軟。
事件觀察者注冊(cè)事件代碼為:
ViewCode
Subjectsub=newSubject();
Sub.XX+=newXXEventHandler(sub_XX);
voidsub_XX(objectsender,XXEventArgse)
{
//dosomething
}
以上是一個(gè)最簡(jiǎn)單的“事件編程”結(jié)構(gòu)代碼,其余所有的寫法都是從以上擴(kuò)展出來的,基本原理不變。
升級(jí):
在定義事件變量時(shí),有時(shí)候我們可以這樣寫:
ViewCode
ClassSubject
{
privateXXEventHandler_xx;
publiceventXXEventHandlerXX
{
add
{
_xx=(XXEventHandler)Delegate.Combine(_xx,value);
}
remove
{
_xx=(XXEventHandler)Delegate.Remove(_xx,value);
}
}
protectedvirtualvoidOnXX(XXEventArgse)
{
if(_xx!=null)
{
_xx(this,e);
}
}
publicvoidDoSomething()
{
//符合某一條件
OnXX(newXXEventArgs());
}
}
其余代碼跟之前一樣,升級(jí)后的代碼顯示的實(shí)現(xiàn)了“add/remove”,顯示實(shí)現(xiàn)“add/remove”的好處網(wǎng)上很多人都說可以在注冊(cè)事件之前添加額外的邏輯,這個(gè)就像“屬性”和“字段”的關(guān)系,
ViewCode
publiceventXXEventHandlerXX
{
add
{
//添加邏輯
_xx=(XXEventHandler)Delegate.Combine(_xx,value);
}
remove
{
//添加邏輯
_xx=(XXEventHandler)Delegate.Remove(_xx,value);
}
}
沒錯(cuò),確實(shí)與“屬性(Property)”的作用差不多,但它不止這一個(gè)好處,我們知道(不知道的上網(wǎng)看看),在多線程編程中,很重要的一點(diǎn)就是要保證對(duì)象“線程安全”,因?yàn)槎嗑€程同時(shí)訪問同一資源時(shí),會(huì)出現(xiàn)預(yù)想不到的結(jié)果。當(dāng)然,在“事件編程”中也要考慮多線程的情況?!耙印辈糠执a經(jīng)過編譯器編譯后,確實(shí)可以解決多線程問題,但是存在問題,它經(jīng)過編譯后:
ViewCode
publiceventXXEventHandlerXX;
//該行代碼編譯后類似如下:
privateXXEventHandler_xx;
[MethodImpl(MethodImplOptions.Synchronized)]
publicvoidadd_XX(XXEventHandlerhandler)
{
_xx=(XXEventHandler)Delegate.Combine(_xx,handler);
}
[MethodImpl(MethodImplOptions.Synchronized)]
publicvoidremove_XX(XXEventHandlerhandler)
{
_xx=(XXEventHandler)Delegate.Remove(_xx,handler);
}
以上轉(zhuǎn)換為編譯器自動(dòng)完成,事件(取消)注冊(cè)(+=、-=)間接轉(zhuǎn)換由add_XX和remove_XX代勞,通過在add_XX方法和remove_XX方法前面添加類似[MethodImpl(MethodImplOptions.Synchronized)]聲明,表明該方法為同步方法,也就是說多線程訪問同一Subject對(duì)象時(shí),同時(shí)只能有一個(gè)線程訪問add_XX或者是remove_XX,這就確保了不可能同時(shí)存在兩個(gè)線程操作_xx這個(gè)委托鏈表,也就不可能發(fā)生不可預(yù)測(cè)結(jié)果。那么,[MethodImpl(MethodImplOptions.Synchronized)]是怎么做到線程同步的呢?其實(shí)查看IL語言,我們不難發(fā)現(xiàn),[MethodImpl(MethodImplOptions.Synchronized)]的作用類似于下:
ViewCode
ClassSubject
{
privateXXEventHandler_xx;
publicvoidadd_XX(XXEventHandlerhandler)
{
lock(this)
{
_xx=(XXEventHandler)Delegate.Combine(_xx,handler);
}
}
publicvoidremove_XX(XXEventHandlerhandler)
{
lock(this)
{
_xx=(XXEventHandler)Delegate.Remove(_xx,handler);
}
}
}
如我們所見,它就相當(dāng)于給自己加了一個(gè)同步鎖,lock(this),我不知道諸位在使用同步鎖的時(shí)候有沒有刻意去避免lock(this)這種,我要說的是,使用這種同步鎖要謹(jǐn)慎。原因至少兩個(gè):
1)將自己(Subject對(duì)象)作為鎖定目標(biāo)的話,客戶端代碼中很可能仍以自己為目標(biāo)使用同步鎖,造成死鎖現(xiàn)象。因?yàn)閠his是暴露給所有人的,包括代碼使用者。
ViewCode
privatevoidDoWork(Subjectsub)//客戶端代碼
{
lock(sub)//客戶端代碼鎖定sub對(duì)象
{
sub.XX+=newXXEventHandler(…);//嵌套鎖定同一目標(biāo)
//sub.add_XX(newXXEventHandler(…));相當(dāng)于調(diào)用add_XX,出現(xiàn)死鎖
//
//
//
//dootherthing
}
}
2)當(dāng)Subject類包含多個(gè)事件,XX1、XX2、XX3、XX4…時(shí),每注冊(cè)(或取消)一個(gè)事件時(shí),都需要鎖定同一目標(biāo)(Subject對(duì)象),這完全沒必要。因?yàn)椴煌氖录胁煌奈墟湵恚鄠€(gè)線程完全可以同時(shí)訪問不同的委托鏈表。然而,編譯器還是這樣做了。
ViewCode
ClassSubject
{
privateXXEventHandler_xx1
privateEventHandler_xx2;
publicvoidadd_XX1(XXEventHandlerhandler)
{
lock(this)
{
_xx1=(XXEventHandler)Delegate.Combine(_xx1,handler);
}
}
publicvoidremove_XX1(XXEventHandlerhandler)
{
lock(this)
{
_xx1=(XXEventHandler)Delegate.Remove(_xx1,handler);
}
}
publicvoidadd_XX2(EventHandlerhandler)
{
lock(this)
{
_xx2=(EventHandler)Delegate.Combine(_xx2,handler);
}
}
publicvoidremove_XX2(EventHandlerhandler)
{
lock(this)
{
_xx2=(EventHandler)Delegate.Remove(_xx2,handler);
}
}
}
在一個(gè)線程中執(zhí)行sub.XX1+=newXXEventHandler(…)(間接調(diào)用sub.add_XX1(newXXEventHandler(…)))的時(shí)候,完全可以在另一線程中同時(shí)執(zhí)行sub.XX2+=newEventHandler(…)(間接調(diào)用sub.add_XX2(newEventHandler(…)))。_xx1和_xx2兩個(gè)沒有任何聯(lián)系,訪問他們更不需要線程同步。如果這樣做了,影響性能效率(編譯器自動(dòng)轉(zhuǎn)換成的代碼就是這樣子)。
結(jié)合以上兩點(diǎn),可以將“升級(jí)”部分代碼修改為以下,從而可以很好的解決“線程安全”問題而且不會(huì)像編譯器自動(dòng)轉(zhuǎn)換的代碼那樣影響效率:
ViewCode
ClassSubject
{
privateXXEventHandler_xx;
privateobject_xxSync=newobject();
publiceventXXEventHandlerXX
{
add
{
lock(_xxSync)
{
_xx=(XXEventHandler)Delegate.Combine(_xx,value);
}
}
remove
{
lock(_xxSync)
{
_xx=(XXEventHandler)Delegate.Remove(_xx,value);
}
}
}
protectedvirtualvoidOnXX(XXEventArgse)
{
if(_xx!=null)
{
_xx(this,e);
}
}
publicvoidDoSomething()
{
//符合某一條件
OnXX(newXXEventArgs());
}
}
在Subject類中增加一個(gè)同步鎖目標(biāo)“_xxSync”,不再以對(duì)象本身為同步鎖目標(biāo),這樣_xxSync只在類內(nèi)部可見(客戶端代碼不可使用該對(duì)象作為同步鎖目標(biāo)),不會(huì)出現(xiàn)死鎖現(xiàn)象。另外,如果Subject有多個(gè)事件,那么我們可以完全增加多個(gè)類似“_xxSync”這樣的東西,比如“_xx1Sync、_xx2Sync…”等等,每個(gè)同步鎖目標(biāo)之間沒有任何關(guān)聯(lián)。
當(dāng)一個(gè)類(比如前面提到的Subject)中包含的事件增多時(shí),幾十個(gè)甚至幾百個(gè),而且派生類還會(huì)增加事件,在這種情況下,我們需要統(tǒng)一管理這些事件,由一個(gè)集合來統(tǒng)一管理這些事件是個(gè)不錯(cuò)的選擇,比如:
ViewCode
ClassSubject
{
protectedDictionary<object,Delegate>_handlerList=newDictionary<object,Delegate>();
Staticobject_XX1_KEY=newobject();
Staticobject_XX2_KEY=newobject();
Staticobject_XXn_KEY=newobject();
//事件
publiceventEventHandlerXX1
{
add
{
if(_handlerList.ContainsKey(_XX1_KEY))
{
_handlerList[_XX1_KEY]=Delegate.Combine(_handlerList[_XX1_KEY],value);
}
else
{
_handlerList.Add(_XX1_KEY,value);
}
}
remove
{
if(_handlerList.ContainsKey(_XX1_KEY))
{
_handlerList[_XX1_KEY]=Delegate.Remove(_handlerList[_XX1_KEY],value);
}
}
}
publiceventEventHandlerXX2
{
add
{
if(_handlerList.ContainsKey(_XX2_KEY))
{
_handlerList[_XX2_KEY]=Delegate.Combine(_handlerList[_XX2_KEY],value);
}
else
{
_handlerList.Add(_XX2_KEY,value);
}
}
remove
{
if(_handlerList.ContainsKey(_XX2_KEY))
{
_handlerList[_XX2_KEY]=Delegate.Remove(_handlerList[_XX2_KEY],value);
}
}
}
publiceventEventHandlerXXn
{
add
{
if(_handlerList.ContainsKey(_XXn_KEY))
{
_handlerList[_XXn_KEY]=Delegate.Combine(_handlerList[_XXn_KEY],value);
}
else
{
_handlerList.Add(_XXn_KEY,value);
}
}
remove
{
if(_handlerList.ContainsKey(_XXn_KEY))
{
_handlerList[_XXn_KEY]=Delegate.Remove(_handlerList[_XXn_KEY],value);
}
}
}
protectedvirtualvoidOnXX1(EventArgse)
{
if(_handlerList.ContainsKey(_XX1_KEY))
{
EventHandlerhandler=_handlerList[_XX1_KEY]asEventHandler;
If(handler!=null)
{
Handler(this,e);
}
}
}
protectedvirtualvoidOnXX2(EventArgse)
{
if(_handlerList.ContainsKey(_XX2_KEY))
{
EventHandlerhandler=_handlerList[_XX2_KEY]asEventHandler;
if(handler!=null)
{
Handler(this,e);
}
}
}
protectedvirtualvoidOnXXn(EventArgse)
{
if(_handlerList.ContainsKey(_XXn_KEY))
{
EventHandlerhandler=_handlerList[_XXn_KEY]asEventHandler;
If(handler!=null)
{
Handler(this,e);
}
}
}
publicvoidDoSomething()
{
//符合某一條件
OnXX1(newEventArgs());
OnXX2(newEventArgs());
OnXXn(newEventArgs());
}
}
存放事件委托鏈表的容器為Dictionary<object,Delegate>類型,該容器存放各個(gè)委托鏈表的表頭,每當(dāng)有一個(gè)“事件注冊(cè)”的動(dòng)作發(fā)生時(shí),先查找字典中是否有表頭,如果有,直接加到表頭后面;如果沒有,向字典中新加一個(gè)表頭?!笆录N”操作類似。
字典的作用是將每個(gè)委托鏈表的表頭組織起來,便于查詢?cè)L問??赡苡腥艘呀?jīng)看出來修改后的代碼并沒有考慮“線程安全”問題,的確,引進(jìn)了集合去管理委托鏈表之后,再也沒辦法解決“線程安全”而又不影響效率了,因?yàn)楝F(xiàn)在各個(gè)事件不再是獨(dú)立存在的,它們都放在了同一集合。另外,集合Dictionary<object,Delegate>聲明為protected,子類完全可以使用該集合對(duì)子類的事件委托鏈表進(jìn)行管理。
注:上圖中委托鏈中各節(jié)點(diǎn)引用的都是實(shí)例方法,沒有列舉靜態(tài)方法。
其實(shí),.net中所有從System.Windows.Forms.Control類繼承下來的類,都是用這種方式去維護(hù)事件委托鏈表的,只不過它不是用的字典(我只是用字典模擬),它使用一個(gè)EventHandlerList類對(duì)象來存儲(chǔ)所有的委托鏈表表頭,作用跟Dictionary<object,Delegate>差不多,并且,.net中也沒去處理“線程安全”問題??傊?,CLR在處理“線程安全”問題做得不是足夠好,當(dāng)然,一般事件編程也基本用在單線程中(比如Winform中的UI線程中),打個(gè)比方,在UI線程中創(chuàng)建的Control(或其派生類),基本上都在同一線程中訪問它,基本不涉及跨線程去訪問Control(或其派生類),所以大可不必?fù)?dān)心事件編程中遇到“線程安全”問題。
事件編程中的內(nèi)存泄露
說到“內(nèi)存泄露”,可能很多人認(rèn)為這不應(yīng)該是.net討論的問題,因?yàn)镚C自動(dòng)回收內(nèi)存,不需要編程的人去管理內(nèi)存,其實(shí)不然。凡是發(fā)生了不能及時(shí)釋放內(nèi)存的情況,都可以叫“內(nèi)存泄露”,.net中包括“托管內(nèi)存”也包括“非托管內(nèi)存”,前者由GC管理,后者必然由編程者考慮了(類似C++中的內(nèi)存),這里我們討論的是前者,也就是托管內(nèi)存的泄露。
我們知道(假設(shè)諸位都知道),當(dāng)一個(gè)托管堆中的對(duì)象不可達(dá)時(shí),也就是程序中沒有對(duì)該對(duì)象有引用時(shí),該對(duì)象所占堆內(nèi)存就屬于GC回收的范圍了。可是,如果編程者認(rèn)為一個(gè)對(duì)象生命期應(yīng)該結(jié)束(該對(duì)象不再使用)的時(shí)候,同時(shí)也理所當(dāng)然地認(rèn)為GC會(huì)回收該對(duì)象在堆中占用的內(nèi)存時(shí),情況往往不是TA所認(rèn)為的那樣,應(yīng)為很有可能(概率很大),該對(duì)象在其他的地方仍然被引用,而且該引用相對(duì)來說不會(huì)很明顯,我們叫這個(gè)為“隱式強(qiáng)引用”(Implicitstrongreference),而對(duì)于ClassA=newClass();這樣的代碼,A就是“顯示強(qiáng)引用”(Explicitstrongreference)了。(至于什么是強(qiáng)引用什么是弱引用,這個(gè)在這里我就不說了)那么,不管是“顯示強(qiáng)引用”還是“隱式強(qiáng)引用”都屬于“強(qiáng)引用”,一個(gè)對(duì)象有一個(gè)強(qiáng)引用存在的話,GC就不會(huì)對(duì)它進(jìn)行內(nèi)存回收。
事件編程中,經(jīng)常會(huì)產(chǎn)生“隱式強(qiáng)引用”,參考前面的“圖1”中委托鏈表中的每個(gè)節(jié)點(diǎn)都包含一個(gè)target,當(dāng)一個(gè)事件觀察者向發(fā)布者注冊(cè)一個(gè)事件時(shí),那么,發(fā)布者就會(huì)保持一個(gè)觀察者的強(qiáng)引用,這個(gè)強(qiáng)引用不是很明顯,因此我們稱之為隱式強(qiáng)引用。因此,當(dāng)觀察者被編程者理所當(dāng)然地認(rèn)為生命期結(jié)束了,再?zèng)]有任何對(duì)它的引用存在時(shí),事件發(fā)布者卻依然保持了一個(gè)強(qiáng)引用。如下圖:
盡管有時(shí)候,Observer生命期結(jié)束(我們理所當(dāng)然地那樣認(rèn)為),Subject(發(fā)布者)卻依舊對(duì)Observer有一個(gè)強(qiáng)引用(strongreference)(圖2中紅色箭頭),該引用稱作為“隱式強(qiáng)引用”。GC不會(huì)對(duì)Observer進(jìn)行內(nèi)存回收,因?yàn)檫€有強(qiáng)引用存在。如果Observer為大對(duì)象,且系統(tǒng)存在很多這樣的Observer,當(dāng)系統(tǒng)運(yùn)行時(shí)間足夠長(zhǎng),托管堆中的“僵尸對(duì)象”(有些對(duì)象雖然已經(jīng)沒有使用價(jià)值了,但是程序中依舊存在對(duì)它的強(qiáng)引用)越來越多,總有一個(gè)時(shí)刻,內(nèi)存不足,程序崩潰。
事件編程中引起的異常
其實(shí)還是因?yàn)槲覀兊腛bserver注冊(cè)了事件,但在Observer生命期結(jié)束(編程者認(rèn)為的)時(shí),釋放了一些必備資源,但是Subject還是對(duì)Observer有一個(gè)強(qiáng)引用,當(dāng)事件發(fā)生后,Subject還是會(huì)通知Observer,如果Observer在處理事件的時(shí)候,也就是事件處理程序中用到了之前已經(jīng)釋放了的“必備資源”,程序就會(huì)出錯(cuò)。導(dǎo)致這個(gè)異常的原因就是,編程者以為對(duì)象已經(jīng)死了,將其資源釋放,但對(duì)象本質(zhì)上還未死去,仍然會(huì)處理它注冊(cè)過的事件。
ViewCode
//Form1.cs中:
privatevoidform1_Load(objectsender,EventArgse)
{
Form2form2=newForm2();
form2.Click+=newEventHandler(form2_Click);
form2.Show();
}
privatevoidform2_Click(objectsender,EventArgse)
{
this.Show();
}
form1為Observer,form2為Subject,form1監(jiān)聽form2的Click事件,在事件處理程序中將自己Show出來,一切運(yùn)行良好,但是,當(dāng)form1關(guān)閉后,再次點(diǎn)擊form2激發(fā)Click事件時(shí),程序報(bào)錯(cuò),提示form1已經(jīng)disposed。原因就是我們關(guān)閉form1時(shí),認(rèn)為form1生命期已經(jīng)結(jié)束了,事實(shí)上并非如此,form2中還有對(duì)form1的引用,當(dāng)事件發(fā)生后,還是會(huì)通知form1,調(diào)用form1的事件處理程序(form2_Click),而碰巧的是,事件處理程序中調(diào)用了this.Show()方法,意思要將form1顯示出來,可此時(shí)form1已經(jīng)關(guān)閉了。
小結(jié)
不管是內(nèi)存泄露還是引起的異常,都是因?yàn)槲覀冏?cè)了某些事件,在對(duì)象生命期結(jié)束時(shí),沒有及時(shí)將已注冊(cè)的事件注銷,告訴事件發(fā)布者“我已死,請(qǐng)將我的引用刪除”。因此一個(gè)簡(jiǎn)單的方法就是在對(duì)象生命期結(jié)束時(shí)將所有的事件注銷,但這個(gè)只對(duì)簡(jiǎn)單的代碼結(jié)構(gòu)有效,復(fù)雜的系統(tǒng)幾乎無效,事件太多,根本無法記錄已注冊(cè)的事件,再者,你有時(shí)候根本不知道對(duì)象什么時(shí)候生命期結(jié)束。下次介紹利用弱引用概念(Weakreference)引申出來的弱委托(Weakdelegate),它能有效地解決事件編程中內(nèi)存泄露問題。原理就是將圖2中每個(gè)節(jié)點(diǎn)中的Target由原來的強(qiáng)引用(StrongReference)改為弱引用(WeakReference)。
希望有幫助O(∩_∩)O~。
跟之前一樣,代碼未調(diào)試運(yùn)行,可能有錯(cuò)誤。
- C# Winform實(shí)現(xiàn)捕獲窗體最小化、最大化、關(guān)閉按鈕事件的方法
- C#實(shí)現(xiàn)WinForm捕獲最小化事件的方法
- C#中winform實(shí)現(xiàn)自動(dòng)觸發(fā)鼠標(biāo)、鍵盤事件的方法
- WinForm實(shí)現(xiàn)移除控件某個(gè)事件的方法
- winform攔截關(guān)閉按鈕觸發(fā)的事件示例
- winform使用委托和事件來完成兩個(gè)窗體之間通信的實(shí)例
- 解讀在C#中winform程序響應(yīng)鍵盤事件的詳解
- WinForm判斷關(guān)閉事件來源于用戶點(diǎn)擊右上角“關(guān)閉”按鈕的方法
相關(guān)文章
C# Dynamic之:ExpandoObject,DynamicObject,DynamicMetaOb的應(yīng)用(上)
本篇文章對(duì)C#中ExpandoObject,DynamicObject,DynamicMetaOb的應(yīng)用進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05C#抓取網(wǎng)絡(luò)圖片保存到本地的實(shí)現(xiàn)方法
下面小編就為大家分享一篇C#抓取網(wǎng)絡(luò)圖片保存到本地的實(shí)現(xiàn)方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-01-01C#使用itextsharp生成PDF文件的實(shí)現(xiàn)代碼
以下是對(duì)在C#中使用itextsharp生成PDF文件的實(shí)現(xiàn)代碼進(jìn)行了詳細(xì)分析介紹,需要的朋友可以過來參考下2013-07-07winform異型不規(guī)則界面設(shè)計(jì)的實(shí)現(xiàn)方法
這篇文章主要介紹了winform異型不規(guī)則界面設(shè)計(jì)的實(shí)現(xiàn)方法,具有不錯(cuò)的實(shí)用價(jià)值,需要的朋友可以參考下2014-08-08WinForm 自動(dòng)完成控件實(shí)例代碼簡(jiǎn)析
在Web的應(yīng)用方面有js的插件實(shí)現(xiàn)自動(dòng)完成(或叫智能提示)功能,但在WinForm窗體應(yīng)用方面就沒那么好了,接下來參考一下這個(gè)實(shí)例,看看有沒有以外收獲,感興趣的朋友可以了解下啊,希望本文對(duì)你有幫助啊2013-01-01