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

來源:互聯網
上載者:User

我前面幾篇部落格中提到過.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~。

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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.