成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

你不知道的陷阱:C#委托和事件的困惑

開發 后端
C語言因為函數指針獲得了極強的動態性,因為你可以通過給函數指針賦值并動態改變其行為,我曾在單片機上寫的一個小系統中,任務調度機制玩的就是函數指針。

一. 問題引入

通常,一個C語言學習者登堂入室的標志就是學會使用了指針,而成為高手的標志又是“玩轉指針”。指針是如此奇妙,通過一個地址,可以指向一個數,結構體,對象,甚至函數。最后的一種函數,我們稱之為“函數指針”(和“指針函數”可不一樣!)就像如下的代碼:

  1. int func(int x); /* 聲明一個函數 */ 
  2.     int (*f) (int x); /* 聲明一個函數指針 */ 
  3.    f=func; /* 將func函數的首地址賦給指針f */ 

C語言因為函數指針獲得了極強的動態性,因為你可以通過給函數指針賦值并動態改變其行為,我曾在單片機上寫的一個小系統中,任務調度機制玩的就是函數指針。

在.NET時代,函數指針有了更安全更優雅的包裝,就是委托。而事件,則是為了限制委托靈活性引入的新“委托”(之所以為什么限制,后面會談到)。同樣,熟練掌握委托和事件,也是C#登堂入室的標志。有了事件,大大簡化了編程,類庫變得前所未有的開放,消息傳遞變得更加簡單,任何熟悉事件的人一定都深有體會。

但你也知道,指針強大,高性能,帶來的就是危險,你不知道這個指針是否安全,出了問題,非常難于調試。事件和委托這么好,可是當你寫了很多代碼,完成大型系統時,心里是不是總覺得怪怪的?有當年使用指針時類似的感覺?

如果是的話,請看如下的問題:

1.若多次添加同一個事件處理函數時,觸發時處理函數是否也會多次觸發?

2.若添加了一個事件處理函數,卻執行了兩次或多次”取消事件“,是否會報錯?

3.如何認定兩個事件處理函數是一樣的? 如果是匿名函數呢?

4.如果不手動刪除事件函數,系統會幫我們回收嗎?

5.在多線程環境下,掛接事件時和對象創建所在的線程不同,那事件處理函數中的代碼將在哪個線程中執行?

6.當代碼的層次復雜時,開放委托和事件是不是會帶來更大的麻煩?

列下這些問題,下面就讓我們討論這些”尖酸刻薄“的問題。

#p#

二. 事件訂閱和取消問題

我們考慮一個典型的例子:加熱器,加熱器內部加熱,在達到溫度后通知外界”加熱已經完成“。 嘗試寫下如下測試類:

  1. ///   
  2.    /// 熱水器  
  3.    ///   
  4.    public class Heater  
  5.    {  
  6.        public event EventHandler OnBoiled;  
  7.        private  void RasieBoiledEvent()  
  8.        {  
  9.            if(OnBoiled==null)  
  10.            {  
  11.                Console.WriteLine("加熱完成處理訂閱事件為空");  
  12.            }  
  13.            else 
  14.            {  
  15.                OnBoiled(thisnew EventArgs());  
  16.            }  
  17.        }  
  18.        private Thread heatThread;  
  19.        public void Begin()  
  20.        {  
  21.            heatTime = 5;  
  22.            heatThread = new Thread(new ThreadStart(Heat));  
  23.            heatThread.Start();  
  24.            Console.WriteLine("加熱器已經開啟", heatTime);  
  25.    
  26.        }  
  27.        private int heatTime;  
  28.        private void Heat()  
  29.        {  
  30.            while (true)  
  31.            {  
  32.                Console.WriteLine("加熱還需{0}秒", heatTime);  
  33.    
  34.                if (heatTime == 0)  
  35.                {  
  36.                    RasieBoiledEvent();  
  37.                     return;  
  38.                }  
  39.                heatTime--;  
  40.                Thread.Sleep(1000);  
  41.    
  42.            }  
  43.        }  
  44.    } 

OK,簡單了,下面是main函數:

  1. class Program  
  2.     {  
  3.         static void Main(string[] args)  
  4.         {  
  5.             var test = new Heater();  
  6.             test.OnBoiled += TestOnBoiled;  
  7.             test.OnBoiled += TestOnBoiled;  
  8.             test.Begin();  
  9.             Console.ReadKey();  
  10.         }  
  11.         static void TestOnBoiled(object sender, EventArgs e)  
  12.         {  
  13.             Console.WriteLine("Hello事件被調用");  
  14.         }  
  15.     } 

我們有意將事件掛載了兩次,看看執行效果:

你可能不知道的陷阱:C#委托和事件的困惑

很明顯,如果多次掛載同一事件處理函數,函數將會執行多次。

這就是第一個問題的答案。

  1. 接下來,我們將上文中main函數中紅色代碼替換成如下蛋疼的代碼:  
  2. test.OnBoiled += TestOnBoiled;  
  3. test.OnBoiled -= TestOnBoiled;  
  4. test.OnBoiled -= TestOnBoiled; 

在實際開發中,這種情況是很普遍的,誰都有可能取消訂閱多次,結果如何呢?

你可能不知道的陷阱:C#委托和事件的困惑

在執行過程中,刪除兩次事件沒有報錯,但當觸發事件時,由于事件訂閱列表為空,所以,第二個問題的答案:多次刪除同一事件是不會報錯的,即使事件只被訂閱了一次。若出現訂閱三次,取消訂閱兩次時,依舊執行一次。

這個事情是好理解的,事件列表,實際上就是List,最簡單的增刪問題。

#p#

三. 有了匿名函數后?

自從學習匿名函數后,筆者就特別喜歡用它,除非代碼量特別長,否則十行之內的事件訂閱,我都會用匿名函數。可是事情變得有意思了,寫了匿名函數后,幾乎沒人記得取消訂閱,那么,發生了什么事情呢?

和上次一樣,我們將前面紅色代碼改成下面的樣子:

  1. test.OnBoiled += (s, e) => Console.WriteLine("加熱完成事件被調用");test.OnBoiled -= (s, e) => Console.WriteLine("加熱完成事件被調用");test.Bein(); 

Resharper直接給我畫了灰線,如下圖:

你可能不知道的陷阱:C#委托和事件的困惑

我估計情況不太樂觀,執行之后:

你可能不知道的陷阱:C#委托和事件的困惑

果然!加熱完成事件還是被調用了,也就是說,看著形式完全一致的兩個匿名函數,編譯器生成的方法簽名是不一致的,根本就是兩個不同的函數。因此,匿名函數完全沒法取消訂閱! 這是第三個問題的答案。

事件不能被取消訂閱!這下可慘了,我真的要取消怎么辦?沒辦法,只能乖乖的寫完整的事件函數。匿名方法雖好,千萬別用過頭。

但是,真正麻煩的問題來了,一個復雜的動態系統中,一定隨時會有大量的對象生成和銷毀,你也一定會給它訂閱一些事件,當你用匿名函數后,這些函數是不是就像死神一樣,一直掐著你的脖子? 如果事件處理函數涉及重要操作,比如給對方付款,執行多次你是不是就要哭死了?

#p#

四. 垃圾回收和事件

垃圾回收機制攙和進來后,故事變的更有意思了。

我“殷切”的希望,垃圾回收器會幫我解決第三節最后一段談到的問題,幫我收拾掉那些函數,那真實的情況呢?我們做個試驗:

同樣的,替換掉紅色部分:

  1. test.OnBoiled += (s, e) => Console.WriteLine("加熱完成事件被調用");  
  2. test=new Heater();  
  3. GC.Collect();  //強制垃圾回收實際上可有可無  
  4. test.Bein(); 

下面是執行結果:

你可能不知道的陷阱:C#委托和事件的困惑

哈,起碼在我更新了對象引用,new了新對象之后,原來的匿名事件確實沒有了。看來編譯器還是夠意思的。

可是,多數實際開發情況中,我們很少直接new一個對象覆蓋掉原來的引用。而是重新new了一個對象出來。這種情況的代碼如下

  1. test.OnBoiled += (s, e) => Console.WriteLine("加熱完成事件被調用");  
  2.             var heaters = new List() { test, test };  
  3.             heaters.Clear();  
  4.             test.Begin();  
  5.             test = null;  
  6.             GC.Collect(); 

執行結果如下圖:

你可能不知道的陷阱:C#委托和事件的困惑

這種情況下,test即使被賦值為null,事件還是會乖乖執行,因為是匿名函數,你也沒法取消訂閱,而GC強制收集也沒用! 這就是我們真實場景中最可怕的事情,你認為它已經消失了,可是它還掛在事件上!

其實這里有個破綻:Heater類里開了線程,我即使賦值為null,線程肯定還沒有被銷毀,事件確實可能會執行,時間所限,我沒有嘗試在寫一個類測試不開線程的情況,有興趣的讀者可以幫忙試一試。

而且,經過我查閱資料,當你的對象訂閱了外部的事件,而又沒有取消訂閱,那么該對象是不會被GC回收的!這會造成很恐怖的問題,產生了幾千萬個對象沒法被回收。可是,匿名函數讓我怎么么取消訂閱?!

所以我們得到了結論,除非確實是一般場景,比如界面開發的window,生成了一直存在,或者在應用程序關閉時回收,否則少用匿名函數吧!記得取消事件訂閱!否則會是非常麻煩的事情!

#p#

五.高潮: 多線程和事件

多線程本來就是程序員頭疼的問題,筆者在多線程知識上只是入門,沒開發過高并發系統,倒是經常用并行庫加速算法執行。 讓我們看看多線程和事件兩個最難搞的東西糾纏在一起時是個什么樣子。

一種常見的場景,是事件處理很耗時,比如執行長時間的IO操作,或者進行了復雜的數學計算,我們不想影響主線程,那么你想當然的會通過多線程的方法解決。

創建對象的線程,一般是主線程(或者UI線程),那么,怎么讓事件處理函數在另外一個線程執行呢? 你真的保證處理函數在另外一個線程中執行了?異步調用?好辦法,不過我們此處不說這個。

//////////////////**************///////////////////////////

修正:經過了重新的測試,發現我的測試用例寫的有問題,為了讓Heater類自己觸發事件,我在內部寫了一個新線程,導致測試不準確。

結論應該是: 不論是不是在多線程環境下,事件處理函數一定在觸發事件位置所在的線程中,和事件訂閱者的創建線程,訂閱事件時所在的線程無關。。。。。。我第五節的內容,有多半都是錯的。。。。

因此,若是觸發事件所在線程是主線程的話,基本上只能用我提出的第二種做法,通過事件內部使用線程池來執行了。感謝 West Continent 的討論。

/////////////////*************/////////////////////

1. 新建線程方法:

初學者會這么做:

  1. test.OnBoiled += (s, e) =>  
  2.                 {  
  3.                     var newThread = new Thread(  
  4.                         new ThreadStart(  
  5.                             () =>  
  6.                                 {  
  7.                              Thread.Sleep(2000); //模擬長時間操作  
  8.                                     Console.WriteLine("總算把熱好的水加到了暖瓶里");  
  9.                                 }));  
  10.                     newThread.Start();  
  11.                 };             
  12.             test.Begin(); 

我的手指還是選擇了匿名函數,用起來真爽,這種情況下,顯然事件處理函數所在線程和主線程不一樣。

可是,稍微有點基礎的人就知道,當事件被頻繁觸發時,線程就會被頻繁生成,線程同樣是非常昂貴的系統資源,更何況,線程的啟動時間是不確定的,可能會耽誤大事。這不是個好方案。

2. 線程池

采用.NET 4.0的線程池試試看,代碼如下:

  1. var mainThread = Thread.CurrentThread;  
  2.             test.OnBoiled += (s, e) =>  
  3.                 {  
  4.                     ThreadPool.QueueUserWorkItem((d) =>  
  5.                         {  
  6.                             Thread.Sleep(2000); //模擬長時間操作  
  7.                             Console.WriteLine("總算把熱好的水加到了暖瓶里");  
  8.                             if (Thread.CurrentThread != mainThread)  
  9.                             {  
  10.                                 Console.WriteLine("兩者執行的是不同的線程");  
  11.                             }  
  12.                             else 
  13.                             {  
  14.                                 Console.WriteLine("兩者執行的是相同的線程");  
  15.                             }  
  16.                         });  
  17.                 };  
  18.             test.Begin(); 

我們通過緩存主線程,并比較處理函數中的線程,得到結果如下:

你可能不知道的陷阱:C#委托和事件的困惑

確實,采用線程池時,會是兩個是不一樣的線程,線程池由于內部做了管理,因此可以有效的利用線程,避免瘋狂新開線程造成的嚴重的性能問題。

可是,我覺得還是麻煩,尤其是有多種事件時,挨個寫線程池還是太麻煩了。那么,我們是不是有兩種方案?

一種是將構造函數寫在一個新線程中,另外一種是將事件訂閱函數寫在新線程中,兩者會發生怎樣的情況呢?

3. 對象的構造函數處在新線程時:

如下測試代碼:

  1. var mainThread = Thread.CurrentThread;  
  2.             var autoResetEvent = new AutoResetEvent(false);  //通過信號機制保證對象首先被創建  
  3.             ThreadPool.QueueUserWorkItem((d) =>  
  4.                 {  
  5.                     test=new Heater();  
  6.                     autoResetEvent.Set();  
  7.                 });  
  8.             autoResetEvent.WaitOne();  
  9.             test.OnBoiled += (s, e) => Console.WriteLine(Thread.CurrentThread != mainThread ? "兩者執行的是不同的線程" : "兩者執行的是相同的線程");  
  10.             test.Begin(); 

代碼值得一提的是,為了保證對象被首先創建,采用了信號機制實現線程同步,當創建后,主線程才會往下執行,否則會拋出空引用的異常.

結果如下:

你可能不知道的陷阱:C#委托和事件的困惑

可見: 主線程稱為Main, 若對象構造函數在B線程執行,事件不在主線程中執行。那是不是在B線程中執行呢?暫時還不知道。

4. 對象的事件訂閱函數處在新線程時:

在另外一個線程里創建對象是更麻煩的,你要解決線程同步問題,惡心不,哈哈。

那么,若訂閱事件的代碼在線程B時,情況是怎樣的呢?

  1. var mainThread = Thread.CurrentThread;   
  2.             ThreadPool.QueueUserWorkItem((d) =>  
  3.                 {  
  4.                     var bThread = Thread.CurrentThread;  
  5.                     test.OnBoiled += (s, e) =>  
  6.                         {  
  7.                             if(Thread.CurrentThread == mainThread )  
  8.                                 Console.WriteLine("事件在主線程中執行");  
  9.                             else if (bThread==Thread.CurrentThread)  
  10.                             {  
  11.                                 Console.WriteLine("事件在訂閱事件的線程B中執行");  
  12.                             }  
  13.                             else 
  14.                             {  
  15.                                 Console.WriteLine("事件在第三個線程中執行");  
  16.                             }  
  17.                         };  
  18.                 });  
  19.    
  20.             test.Begin(); 

結論:

你可能不知道的陷阱:C#委托和事件的困惑

說實話,我看到這個場景的時候大吃一驚,居然執行事件的代碼不在主線程,不在訂閱事件的線程,而在另外一個第三者線程!這可能就是線程池的無敵之處吧,它連事件訂閱函數都給托管了!真是碉堡了!!

不過,管它是什么線程里執行,反正我主線程是不會被堵塞了,哈哈.

六.結語

本來想今天把最后一個問題都解決的,可是時間實在太晚,而且文章已經夠長了。不妨最后一個問題,“在復雜軟件環境下,如何理性正確的使用委托和事件”放在第二部分吧。有些問題我也沒搞清,在做實驗的情況下,才逐漸接近結論。 寫完這篇文章,我深有收獲。

其實,按照慣例,應該把IL代碼好好搞出來給大家看才算是“專業”的選擇,不過我確實不懂IL,就不拿出來丟人了,高手們請自行腦補。

本文介紹了C#的委托和事件的訂閱和取消訂閱,并在匿名函數和多線程兩個環境下討論了一些問題。如果你覺得這篇文章對你有幫助,請點一下推薦,若有任何問題,歡迎留言討論,共同學習。

測試代碼見附件,請將不同Region的代碼解開注釋進行測試。

原文鏈接:http://www.cnblogs.com/buptzym/archive/2013/03/15/2962300.html

責任編輯:張偉 來源: 博客園
相關推薦

2014-12-08 10:39:15

2020-06-12 09:20:33

前端Blob字符串

2020-07-28 08:26:34

WebSocket瀏覽器

2010-08-23 09:56:09

Java性能監控

2011-09-15 17:10:41

2021-02-01 23:23:39

FiddlerCharlesWeb

2022-10-13 11:48:37

Web共享機制操作系統

2009-12-10 09:37:43

2020-08-05 12:17:00

C語言代碼分配

2009-08-18 10:54:17

C#事件和委托

2021-10-17 13:10:56

函數TypeScript泛型

2012-11-23 10:57:44

Shell

2020-08-11 11:20:49

Linux命令使用技巧

2021-12-29 11:38:59

JS前端沙箱

2021-12-22 09:08:39

JSON.stringJavaScript字符串

2015-06-19 13:54:49

2020-09-15 08:35:57

TypeScript JavaScript類型

2022-11-04 08:19:18

gRPC框架項目

2021-02-01 08:39:26

JTAG接口Jlink

2024-06-28 10:19:02

委托事件C#
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产精品一区二区精品 | 欧美精品一区在线 | 91久久精品一区二区二区 | 免费在线观看一区二区 | 91av视频在线免费观看 | 91视频免费视频 | 永久www成人看片 | 羞羞网站免费观看 | 一级黄大片 | 久久99蜜桃综合影院免费观看 | 欧美日韩1区2区3区 欧美久久一区 | 久久91精品| 国产高清精品一区二区三区 | 综合视频在线 | 狠狠干av| 成人免费视频网站在线看 | 日韩一级免费大片 | 91在线精品播放 | 91精品国产综合久久久久久蜜臀 | 国产精品一区二区三 | 久久草在线视频 | 亚洲另类春色偷拍在线观看 | 国产高清毛片 | 国产精品久久久 | 天天干b | 国产日韩欧美在线 | 国产精品揄拍一区二区 | 亚洲在线一区二区 | 欧美一级电影免费观看 | 精品二三区 | 天天操天天天干 | 伊人网在线看 | 激情福利视频 | 国产激情偷乱视频一区二区三区 | 丁香久久 | 国产成人精品一区二区三区网站观看 | 欧美激情一区二区 | 一级大片 | 亚洲欧美激情精品一区二区 | 成人精品视频在线观看 | a级黄色毛片免费播放视频 国产精品视频在线观看 |