程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#基礎知識 >> .Net WInform開發筆記(五)關於事件Event

.Net WInform開發筆記(五)關於事件Event

編輯:C#基礎知識
我前面幾篇博客中提到過.net中的事件與Windows事件的區別,本文討論的是前者,也就是我們代碼中經常用到的Event。Event很常見,Button控件的Click、KeyPress等等,PictureBox控件的Paint等等都屬於本文討論范疇,本文會例舉出有關“事件編程”的幾種方法,還會提及由“事件編程”引起的MemoryLeak(跟“內存洩露”差不多),以及由“事件編程”引起的一些異常。

引子
.net中事件最常用在“觀察者”設計模式中,事件的發布者(subject)定義一個事件,事件的觀察者(observer)注冊這個事件,當發布者激發該事件時,所有的觀察者就會響應該事件(表現為調用各自的事件處理程序)。知道這個邏輯過程後,我們可以寫出以下代碼:
代碼如下:

ViewCode
ClassSubject
{
publiceventXXEventHandlerXX;
protectedvirtualvoidOnXX(XXEventArgse)
{
If(XX!=null)
{
XX(this,e);
}
}
publicvoidDoSomething()
{
//符合某一條件
OnXX(newXXEventArgs());
}
}
delegatevoidXXEventHandler(objectsender,XXEventArgse);
ClassXXEventArgs:EventArgs
{
}

以上就是一個最最原始的含有事件類的定義。外部對象可以注冊Subject對象的XX事件,當某一條件滿足時,Subject對象就會激發XX事件,所以觀察者作出響應。

:編碼中請按照標准的命名方式,事件名、事件參數名、虛方法名、參數名等等,標准請參考微軟。
事件觀察者注冊事件代碼為:
代碼如下:

ViewCode
Subjectsub=newSubject();
Sub.XX+=newXXEventHandler(sub_XX);
voidsub_XX(objectsender,XXEventArgse)
{
//dosomething
}

以上是一個最簡單的“事件編程”結構代碼,其余所有的寫法都是從以上擴展出來的,基本原理不變。
升級
在定義事件變量時,有時候我們可以這樣寫:
代碼如下:

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());
}
}

其余代碼跟之前一樣,升級後的代碼顯示的實現了“add/remove”,顯示實現“add/remove”的好處網上很多人都說可以在注冊事件之前添加額外的邏輯,這個就像“屬性”和“字段”的關系,
代碼如下:

ViewCode
publiceventXXEventHandlerXX
{
add
{
//添加邏輯
_xx=(XXEventHandler)Delegate.Combine(_xx,value);
}
remove
{
//添加邏輯
_xx=(XXEventHandler)Delegate.Remove(_xx,value);
}
}

沒錯,確實與“屬性(Property)”的作用差不多,但它不止這一個好處,我們知道(不知道的上網看看),在多線程編程中,很重要的一點就是要保證對象“線程安全”,因為多線程同時訪問同一資源時,會出現預想不到的結果。當然,在“事件編程”中也要考慮多線程的情況。“引子”部分代碼經過編譯器編譯後,確實可以解決多線程問題,但是存在問題,它經過編譯後:
代碼如下:

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);
}

以上轉換為編譯器自動完成,事件(取消)注冊(+=、-=)間接轉換由add_XX和remove_XX代勞,通過在add_XX方法和remove_XX方法前面添加類似[MethodImpl(MethodImplOptions.Synchronized)]聲明,表明該方法為同步方法,也就是說多線程訪問同一Subject對象時,同時只能有一個線程訪問add_XX或者是remove_XX,這就確保了不可能同時存在兩個線程操作_xx這個委托鏈表,也就不可能發生不可預測結果。那麼,[MethodImpl(MethodImplOptions.Synchronized)]是怎麼做到線程同步的呢?其實查看IL語言,我們不難發現,[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);
}
}
}

如我們所見,它就相當於給自己加了一個同步鎖,lock(this),我不知道諸位在使用同步鎖的時候有沒有刻意去避免lock(this)這種,我要說的是,使用這種同步鎖要謹慎。原因至少兩個
1)將自己(Subject對象)作為鎖定目標的話,客戶端代碼中很可能仍以自己為目標使用同步鎖,造成死鎖現象。因為this是暴露給所有人的,包括代碼使用者。
代碼如下:

ViewCode
privatevoidDoWork(Subjectsub)//客戶端代碼
{
lock(sub)//客戶端代碼鎖定sub對象
{
sub.XX+=newXXEventHandler(…);//嵌套鎖定同一目標
//sub.add_XX(newXXEventHandler(…));相當於調用add_XX,出現死鎖
//
//
//
//dootherthing
}
}

2)當Subject類包含多個事件,XX1、XX2、XX3、XX4…時,每注冊(或取消)一個事件時,都需要鎖定同一目標(Subject對象),這完全沒必要。因為不同的事件有不同的委托鏈表,多個線程完全可以同時訪問不同的委托鏈表。然而,編譯器還是這樣做了。
代碼如下:

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);
}
}
}

在一個線程中執行sub.XX1+=newXXEventHandler(…)(間接調用sub.add_XX1(newXXEventHandler(…)))的時候,完全可以在另一線程中同時執行sub.XX2+=newEventHandler(…)(間接調用sub.add_XX2(newEventHandler(…)))。_xx1和_xx2兩個沒有任何聯系,訪問他們更不需要線程同步。如果這樣做了,影響性能效率(編譯器自動轉換成的代碼就是這樣子)。

結合以上兩點,可以將“升級”部分代碼修改為以下,從而可以很好的解決“線程安全”問題而且不會像編譯器自動轉換的代碼那樣影響效率:
代碼如下:

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類中增加一個同步鎖目標“_xxSync”,不再以對象本身為同步鎖目標,這樣_xxSync只在類內部可見(客戶端代碼不可使用該對象作為同步鎖目標),不會出現死鎖現象。另外,如果Subject有多個事件,那麼我們可以完全增加多個類似“_xxSync”這樣的東西,比如“_xx1Sync、_xx2Sync…”等等,每個同步鎖目標之間沒有任何關聯。

當一個類(比如前面提到的Subject)中包含的事件增多時,幾十個甚至幾百個,而且派生類還會增加事件,在這種情況下,我們需要統一管理這些事件,由一個集合來統一管理這些事件是個不錯的選擇,比如:
代碼如下:

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>類型,該容器存放各個委托鏈表的表頭,每當有一個“事件注冊”的動作發生時,先查找字典中是否有表頭,如果有,直接加到表頭後面;如果沒有,向字典中新加一個表頭。“事件注銷”操作類似。

 
字典的作用是將每個委托鏈表的表頭組織起來,便於查詢訪問。可能有人已經看出來修改後的代碼並沒有考慮“線程安全”問題,的確,引進了集合去管理委托鏈表之後,再也沒辦法解決“線程安全”而又不影響效率了,因為現在各個事件不再是獨立存在的,它們都放在了同一集合。另外,集合Dictionary<object,Delegate>聲明為protected,子類完全可以使用該集合對子類的事件委托鏈表進行管理。
:上圖中委托鏈中各節點引用的都是實例方法,沒有列舉靜態方法。
其實,.net中所有從System.Windows.Forms.Control類繼承下來的類,都是用這種方式去維護事件委托鏈表的,只不過它不是用的字典(我只是用字典模擬),它使用一個EventHandlerList類對象來存儲所有的委托鏈表表頭,作用跟Dictionary<object,Delegate>差不多,並且,.net中也沒去處理“線程安全”問題。總之,CLR在處理“線程安全”問題做得不是足夠好,當然,一般事件編程也基本用在單線程中(比如Winform中的UI線程中),打個比方,在UI線程中創建的Control(或其派生類),基本上都在同一線程中訪問它,基本不涉及跨線程去訪問Control(或其派生類),所以大可不必擔心事件編程中遇到“線程安全”問題。

事件編程中的內存洩露
說到“內存洩露”,可能很多人認為這不應該是.net討論的問題,因為GC自動回收內存,不需要編程的人去管理內存,其實不然。凡是發生了不能及時釋放內存的情況,都可以叫“內存洩露”,.net中包括“托管內存”也包括“非托管內存”,前者由GC管理,後者必然由編程者考慮了(類似C++中的內存),這裡我們討論的是前者,也就是托管內存的洩露。

我們知道(假設諸位都知道),當一個托管堆中的對象不可達時,也就是程序中沒有對該對象有引用時,該對象所占堆內存就屬於GC回收的范圍了。可是,如果編程者認為一個對象生命期應該結束(該對象不再使用)的時候,同時也理所當然地認為GC會回收該對象在堆中占用的內存時,情況往往不是TA所認為的那樣,應為很有可能(概率很大),該對象在其他的地方仍然被引用,而且該引用相對來說不會很明顯,我們叫這個為“隱式強引用”(Implicitstrongreference),而對於ClassA=newClass();這樣的代碼,A就是“顯示強引用”(Explicitstrongreference)了。(至於什麼是強引用什麼是弱引用,這個在這裡我就不說了)那麼,不管是“顯示強引用”還是“隱式強引用”都屬於“強引用”,一個對象有一個強引用存在的話,GC就不會對它進行內存回收。

事件編程中,經常會產生“隱式強引用”,參考前面的“圖1”中委托鏈表中的每個節點都包含一個target,當一個事件觀察者向發布者注冊一個事件時,那麼,發布者就會保持一個觀察者的強引用,這個強引用不是很明顯,因此我們稱之為隱式強引用。因此,當觀察者被編程者理所當然地認為生命期結束了,再沒有任何對它的引用存在時,事件發布者卻依然保持了一個強引用。如下圖:


盡管有時候,Observer生命期結束(我們理所當然地那樣認為),Subject(發布者)卻依舊對Observer有一個強引用(strongreference)(圖2中紅色箭頭),該引用稱作為“隱式強引用”。GC不會對Observer進行內存回收,因為還有強引用存在。如果Observer為大對象,且系統存在很多這樣的Observer,當系統運行時間足夠長,托管堆中的“僵屍對象”(有些對象雖然已經沒有使用價值了,但是程序中依舊存在對它的強引用)越來越多,總有一個時刻,內存不足,程序崩潰。

事件編程中引起的異常
其實還是因為我們的Observer注冊了事件,但在Observer生命期結束(編程者認為的)時,釋放了一些必備資源,但是Subject還是對Observer有一個強引用,當事件發生後,Subject還是會通知Observer,如果Observer在處理事件的時候,也就是事件處理程序中用到了之前已經釋放了的“必備資源”,程序就會出錯。導致這個異常的原因就是,編程者以為對象已經死了,將其資源釋放,但對象本質上還未死去,仍然會處理它注冊過的事件。
代碼如下:

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監聽form2的Click事件,在事件處理程序中將自己Show出來,一切運行良好,但是,當form1關閉後,再次點擊form2激發Click事件時,程序報錯,提示form1已經disposed。原因就是我們關閉form1時,認為form1生命期已經結束了,事實上並非如此,form2中還有對form1的引用,當事件發生後,還是會通知form1,調用form1的事件處理程序(form2_Click),而碰巧的是,事件處理程序中調用了this.Show()方法,意思要將form1顯示出來,可此時form1已經關閉了。

小結

不管是內存洩露還是引起的異常,都是因為我們注冊了某些事件,在對象生命期結束時,沒有及時將已注冊的事件注銷,告訴事件發布者“我已死,請將我的引用刪除”。因此一個簡單的方法就是在對象生命期結束時將所有的事件注銷,但這個只對簡單的代碼結構有效,復雜的系統幾乎無效,事件太多,根本無法記錄已注冊的事件,再者,你有時候根本不知道對象什麼時候生命期結束。下次介紹利用弱引用概念(Weakreference)引申出來的弱委托(Weakdelegate),它能有效地解決事件編程中內存洩露問題。原理就是將圖2中每個節點中的Target由原來的強引用(StrongReference)改為弱引用(WeakReference)。
希望有幫助O(∩_∩)O~。

跟之前一樣,代碼未調試運行,可能有錯誤。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved