[C#] 委托與事件(4)

網(wǎng)上講C#委托和事件的博文已經(jīng)非常多了,其中也不乏一些深入淺出、條理清晰的文章越败。我之所以還是繼續(xù)寫触幼,主要是借機(jī)整理學(xué)習(xí)筆記硼瓣、歸納總結(jié)從而理解更透徹,當(dāng)然能夠以自己的理解和思路給其他人講明白更好置谦。
另外堂鲤,太長(zhǎng)的文章會(huì)讓很多讀者失去興趣,所以我決定把這篇分成四個(gè)部分來(lái)介紹媒峡。分別是委托的基礎(chǔ)瘟栖、委托的進(jìn)階、事件的基礎(chǔ)和事件的進(jìn)階谅阿。對(duì)使用委托與事件要求不高的同學(xué)可以跳過(guò)進(jìn)階部分半哟。

本文開始講最后一部分,事件的進(jìn)階签餐,前三節(jié)請(qǐng)內(nèi)容請(qǐng)參見(jiàn)C#委托與事件(1)寓涨、C#委托與事件(2)C#委托與事件(3)


7. 事件的高級(jí)知識(shí)

(1) 揭開事件的神秘面紗

回到上一節(jié)中我們聲明的事件public event BoilHandler BoilEvent;氯檐,這里的事件雖然被聲明為公開的戒良,但上一節(jié)最后我們?cè)噲D對(duì)h.BoilEvent進(jìn)行賦值操作會(huì)出現(xiàn)編譯錯(cuò)誤。為什么會(huì)這樣呢冠摄?看看編譯后的BoilEvent你就明白了糯崎。

private BoilHandler BoilEvent; // 事件實(shí)際上被聲明為私有的委托變量

[MethodImpl(MethodImplOptions.Synchronized)]
public void add_BoilEvent(BoilHandler value)
{   // 在事件上綁定方法實(shí)際上是間接調(diào)用Delegate.Combine()
    this.BoilEvent= (BoilHandler) Delegate.Combine(this.BoilEvent, value);
}

[MethodImpl(MethodImplOptions.Synchronized)]
public void remove_BoilEvent(BoilHandler value)
{   // 在事件上解除綁定方法實(shí)際上是間接調(diào)用Delegate.Remove()
    this.BoilEvent = (BoilHandler) Delegate.Remove(this.BoilEvent, value);
}

上面的代碼進(jìn)一步說(shuō)明,實(shí)際上事件就是封裝了的委托河泳,而且我們只需要鍵入event關(guān)鍵字沃呢,編譯器就可以幫我們把相關(guān)的方法自動(dòng)生成。另外拆挥,代表事件本身的CIL代碼將使用.addon.removeon指令對(duì)應(yīng)要調(diào)用的add_XXX()remove_XXX()方法的名稱薄霜。

.event MyFirstEvent.Heater/BoilHandler BoilEvent
{
    .addon instance void MyFirstEvent.Heater::add_BoilEvent(class MyFirstEvent.Heater/BoilHandler)
    .removeon instance void MyFirstEvent.Heater::remove_BoilEvent(class MyFirstEvent.Heater/BoilHandler)
}
(2) 重申事件的必要性

我們?cè)?strong>第6節(jié)從封裝性的角度分析了“為什么要用事件”,上面部分又說(shuō)明了實(shí)際上event就是封裝的delegate,所以事件在易用性上又占了上風(fēng)黄锤,因?yàn)?code>event關(guān)鍵字能夠幫助我們節(jié)省很多代碼的鍵入搪缨。另外,張子陽(yáng)又從設(shè)計(jì)模式的角度解釋了“為什么要使用事件而不是委托”鸵熟。我覺(jué)得很有道理副编,所以將借用他的例子來(lái)重申一下他的觀點(diǎn)。

首先我們定義一個(gè)帶有一個(gè)int參數(shù)的委托NumberChangedEventHandler流强,然后再定義一個(gè)發(fā)布者和訂閱者痹届。其中發(fā)布者類中可以聲明上述委托的變量或者對(duì)應(yīng)的事件,發(fā)布者通過(guò)DoSomething()方法來(lái)執(zhí)行委托變量或事件上綁定的方法打月。而訂閱者類中則聲明了訂閱的方法即實(shí)際上應(yīng)該綁定到委托的方法队腐。所以,用通俗的語(yǔ)言來(lái)解釋就是奏篙,訂閱者訂閱了某種消息柴淘,發(fā)布者在它的某種行為中滿足了某種條件后觸發(fā)某件事情的發(fā)生,而這件事情的發(fā)生正是訂閱者訂閱的東西秘通,所以發(fā)布者觸發(fā)某事件后即會(huì)通知到所有的訂閱者为严。

(a) 如果聲明委托變量,那么在Main函數(shù)也就是客戶端中可以通過(guò)pub.NumberChanged(100);來(lái)觸發(fā)事件肺稀,這不滿足事件由發(fā)布者在其內(nèi)部的某種行為下觸發(fā)的設(shè)計(jì)模式第股。
(b) 如果聲明為事件,則它的封裝性限制了客戶端的行為(直接調(diào)用pub.NuberChanged會(huì)出現(xiàn)編譯錯(cuò)誤)话原,從而保證了事件只能供其他類型訂閱夕吻。

namespace PubSub
{
    public delegate void NumberChangedEventHandler(int count); //定義委托

    public class Publishser
    {
        private int count;
        //public NumberChangedEventHandler NumberChanged;     // 聲明委托變量
        public event NumberChangedEventHandler NumberChanged; // 聲明一個(gè)事件

        public void DoSomething()
        {
            // todo: do something

            if (++count > 0)
            {
                if (NumberChanged != null)
                {
                    NumberChanged(count);
                }
            }
        }
    }

    public class Subscriber
    {
        public void OnNumberChanged(int count)
        {
            Console.WriteLine("Subscriber notified: count = {0}", count);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Publishser pub = new Publishser();
            Subscriber sub = new Subscriber();

            pub.NumberChanged += new NumberChangedEventHandler(sub.OnNumberChanged);
            pub.DoSomething();          // 通過(guò)Pub DoSomething()來(lái)觸發(fā)事件
            //pub.NumberChanged(100);   // 委托變量可以被這樣直接(不恰當(dāng)?shù)?調(diào)用,但事件不行
        }
    }
}
(3) 自定義事件的參數(shù)與返回值
(a) 事件的參數(shù)

微乳推薦的事件模式帶有兩個(gè)參數(shù)objectEventArgs繁仁,其中object參數(shù)表示一個(gè)隊(duì)發(fā)送事件的對(duì)象的引用涉馅,EventArgs則表示與該事件相關(guān)的信息,它可以是表示不發(fā)送任何信息的基類EventArgs改备,也可以是派生自EventArgs的類的實(shí)例控漠。

To declare an event inside a class, first a delegate type for the event must be declared, if none is already declared.
public delegate void ChangedEventHandler(object sender, EventArgs e);
The delegate type defines the set of arguments that are passed to the method that handles the event.

所以,接下來(lái)我們按照規(guī)范的方式改寫上面的程序悬钳,為了增加其他類型的參數(shù)盐捷,我們還會(huì)在發(fā)布者中傳遞一些字符串類型的message給訂閱者。

namespace PubSub
{
    public delegate void NumberChangedEventHandler(object sender, ChangedEventArgs e); //定義委托

    public class ChangedEventArgs : EventArgs
    {
        public readonly int count;
        public readonly string msg;
        public ChangedEventArgs(int cnt, string str)
        {
            count = cnt;
            msg = str;
        }
    }

    public class Publishser
    {
        private int count;
        public event NumberChangedEventHandler NumberChanged; // 聲明一個(gè)事件

        public void DoSomething()
        {
            // todo: do something
            if (++count > 0)
            {
                if (NumberChanged != null)
                {
                    NumberChanged(this, new ChangedEventArgs(count, "Something happened"));
                }
            }
        }
    }

    public class Subscriber
    {
        public void OnNumberChanged(object sender, ChangedEventArgs e)
        {
            Console.WriteLine("Subscriber notified from [{0}]: \ncount = [{1}], msg = [{2}]", sender, e.count, e.msg);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Publishser pub = new Publishser();
            Subscriber sub = new Subscriber();

            pub.NumberChanged += new NumberChangedEventHandler(sub.OnNumberChanged);
            pub.DoSomething();
        }
    }
}

輸出結(jié)果:

Subscriber notified from [PubSub.Publishser]:
count = [1], msg = [Something happened]
(b) 事件的返回值

一般情況下默勾,最好讓事件的返回值為空碉渡。因?yàn)?br> 首先,事件上可以綁定多個(gè)方法母剥,這些方法是依次執(zhí)行的滞诺,如果每個(gè)方法都有返回值形导,則需要發(fā)布者顯示地依次執(zhí)行每個(gè)方法,并處理相應(yīng)的返回值习霹。否則的話只有最后一個(gè)方法的返回值能被發(fā)布者獲得朵耕。
其次,發(fā)布者與訂閱者之間是松耦合的淋叶,發(fā)布者并不關(guān)心有誰(shuí)訂閱它的事件阎曹,更不會(huì)關(guān)心訂閱者返回的東西。

(4) 事件的異常處理

還是上面的例子煞檩,我們?cè)俳o發(fā)布者多加兩個(gè)訂閱者处嫌,并讓其中一個(gè)訂閱者的方法拋出異常。我們可以回顧一下斟湃,訂閱者的方法被綁定到事件后熏迹,發(fā)布者一旦滿足某種條件后觸發(fā)該事件。而觸發(fā)該事件時(shí)發(fā)布者要做的事情就是依次執(zhí)行該事件上綁定的方法凝赛,即執(zhí)行NumberChanged(this, EventArgs.Empty);這句話注暗。所以我們嘗試對(duì)這句話進(jìn)行結(jié)構(gòu)化異常處理。

namespace PubSub
{
    public delegate void NumberChangedEventHandler(object sender, EventArgs e); //定義委托

    public class Publishser
    {
        private int count;
        public event NumberChangedEventHandler NumberChanged; // 聲明一個(gè)事件

        public void DoSomething()
        {
            // todo: do something
            if (++count > 0)
            {
                if (NumberChanged != null)
                {
                    try
                    {
                        NumberChanged(this, EventArgs.Empty);
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine("Exception: {0}", e.Message);
                    }
                }
            }
        }
    }

    public class Subscriber1
    {
        public void OnNumberChanged(object sender, EventArgs e)
        {
            Console.WriteLine("Subscriber1 invoked");
        }
    }

    public class Subscriber2
    {
        public void OnNumberChanged(object sender, EventArgs e)
        {
            throw new Exception("Subscriber2 threw exception");
        }
    }

    public class Subscriber3
    {
        public void OnNumberChanged(object sender, EventArgs e)
        {
            Console.WriteLine("Subscriber3 invoked");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var pub = new Publishser();
            var sub1 = new Subscriber1();
            var sub2 = new Subscriber2();
            var sub3 = new Subscriber3();

            pub.NumberChanged += new NumberChangedEventHandler(sub1.OnNumberChanged);
            pub.NumberChanged += new NumberChangedEventHandler(sub2.OnNumberChanged);
            pub.NumberChanged += new NumberChangedEventHandler(sub3.OnNumberChanged);
            pub.DoSomething();
        }
    }
}

輸出結(jié)果:

Subscriber1 invoked
Exception: Subscriber2 threw exception

結(jié)果顯示哄酝,第2個(gè)訂閱者拋出異常后發(fā)布者確實(shí)捕捉到了該異常友存,但是遺憾的是第3個(gè)訂閱者的方法并沒(méi)有被執(zhí)行。也就是說(shuō)事件上綁定的方法一旦出現(xiàn)了異常就終止了整個(gè)事件的處理陶衅,這樣就影響到了后面的訂閱者。所以對(duì)于這種情況直晨,我們需要先獲得事件上的委托鏈表搀军,然后再在遍歷鏈表的循環(huán)中處理異常。修改上面的DoSomething()方法如下:

public void DoSomething()
{
    // todo: do something
   if (++count > 0)
   {
       if (NumberChanged != null)
        {
            Delegate[] delArray = NumberChanged.GetInvocationList();
            foreach (Delegate del in delArray)
            {
                NumberChangedEventHandler method = (NumberChangedEventHandler)del; // 強(qiáng)制轉(zhuǎn)換為具體的委托類型
                try
                {
                    method(this, EventArgs.Empty);
                }
                catch (Exception e)
                {
                    Console.WriteLine("Exception: {0}", e.Message);
                }
            }
        }
    }
}

輸出結(jié)果:

Subscriber1 invoked
Exception: Subscriber2 threw exception
Subscriber3 invoked
(5) 泛型EventHandler<T>委托

既然有了上面提到的第一個(gè)參數(shù)為object勇皇,第二個(gè)參數(shù)為EventArgs派生類型的規(guī)范來(lái)書寫自定義委托罩句,我們肯定大多時(shí)候都是定義類似這樣的委托。所以C#中還提供了EventHandler<T>泛型類型來(lái)簡(jiǎn)化我們得自定義過(guò)程敛摘。我們直接將它應(yīng)用到上面的例子(3)(a)中即可得到:

namespace PubSub
{
    //public delegate void NumberChangedEventHandler(object sender, ChangedEventArgs e); //不再需要定義委托

    public class ChangedEventArgs : EventArgs
    {
        public readonly int count;
        public readonly string msg;
        public ChangedEventArgs(int cnt, string str)
        {
            count = cnt;
            msg = str;
        }
    }

    public class Publishser
    {
        private int count;
        public event EventHandler<ChangedEventArgs> NumberChanged; // 利用EventHanler泛型聲明一個(gè)事件

        public void DoSomething()
        {
            // todo: do something
            if (++count > 0)
            {
                if (NumberChanged != null)
                {
                    NumberChanged(this, new ChangedEventArgs(count, "Something happened"));
                }
            }
        }
    }

    public class Subscriber
    {
        public void OnNumberChanged(object sender, ChangedEventArgs e)
        {
            Console.WriteLine("Subscriber notified from [{0}]: \ncount = [{1}], msg = [{2}]", sender, e.count, e.msg);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Publishser pub = new Publishser();
            Subscriber sub = new Subscriber();

            pub.NumberChanged += new EventHandler<ChangedEventArgs>(sub.OnNumberChanged);
            pub.DoSomething();
        }
    }
}

8. 委托與事件的編碼規(guī)范

終于把委托與事件學(xué)習(xí)完了门烂,但是整篇文章中的代碼并不規(guī)范,尤其是前面部分兄淫。因此屯远,最后我們?cè)賮?lái)簡(jiǎn)單談?wù)?code>.Net Framework的編碼規(guī)范,以免我的code誤導(dǎo)了大家捕虽。
(1) 委托類型的名稱應(yīng)以EventHandler結(jié)束慨丐,如果沒(méi)有什么特征要求,則可以直接使用泛型委托定義EventHandler<T>泄私。
(2) 委托的原型定義:有一個(gè)void返回值房揭,并接受兩個(gè)輸入?yún)?shù):一個(gè)是object 類型备闲,一個(gè)是EventArgs類或其派生類。
(3) 繼承自EventArgs的類型應(yīng)該以EventArgs結(jié)尾捅暴。
(4) 事件的命名為: 委托定義名去掉EventHandler之后剩余的部分恬砂。
(5) 訂閱事件的方法的命名通常為: On事件名

寫在最后蓬痒,如果您從頭到尾看到了這里觉既,那真的是非常感謝您的信任與支持。有任何疑問(wèn)或者不妥之處乳幸,歡迎指正瞪讼!再次感謝!

參考文獻(xiàn):
《精通C#》
C# 中的委托和事件
C# 中的委托和事件(續(xù))
Events Tutorial

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末粹断,一起剝皮案震驚了整個(gè)濱河市符欠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌瓶埋,老刑警劉巖希柿,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異养筒,居然都是意外死亡曾撤,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門晕粪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)挤悉,“玉大人,你說(shuō)我怎么就攤上這事巫湘∽氨” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵尚氛,是天一觀的道長(zhǎng)成艘。 經(jīng)常有香客問(wèn)我戏阅,道長(zhǎng)碍讯,這世上最難降的妖魔是什么组民? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮讯柔,結(jié)果婚禮上抡蛙,老公的妹妹穿的比我還像新娘。我一直安慰自己磷杏,他們只是感情好溜畅,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著极祸,像睡著了一般慈格。 火紅的嫁衣襯著肌膚如雪怠晴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天浴捆,我揣著相機(jī)與錄音蒜田,去河邊找鬼。 笑死选泻,一個(gè)胖子當(dāng)著我的面吹牛冲粤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播页眯,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼梯捕,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了窝撵?” 一聲冷哼從身側(cè)響起傀顾,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎碌奉,沒(méi)想到半個(gè)月后短曾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡赐劣,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年嫉拐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片魁兼。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡婉徘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出璃赡,到底是詐尸還是另有隱情判哥,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布碉考,位于F島的核電站,受9級(jí)特大地震影響挺身,放射性物質(zhì)發(fā)生泄漏侯谁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一章钾、第九天 我趴在偏房一處隱蔽的房頂上張望墙贱。 院中可真熱鬧,春花似錦贱傀、人聲如沸惨撇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)魁衙。三九已至报腔,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間剖淀,已是汗流浹背纯蛾。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留纵隔,地道東北人翻诉。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像捌刮,于是被迫代替她去往敵國(guó)和親碰煌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容