工作十餘年,還是一直被問 委託和事件 有什麼區別? 真是夠了

一線碼農發表於2020-08-10

一:背景

1. 講故事

前幾天公司一個妹子問我,事件和委託有什麼區別? 先由衷感嘆一下,編碼十餘年,年輕的時候常被面試官問起,現在年長了,卻被後輩們時常問候,看樣子逃離編碼生涯之前是跑不掉了,不過奇怪的是,這個問題被問起的時候,我發現有很多人用: 事件是一種特殊的委託 來進行總結,是不是挺有意思,我想這句話可能來自於網路上的面試題答案吧,這篇我就試著徹底總結一下。

二:事件真的是特殊的委託嗎?

1. 貓和老鼠 經典案例

要想知道兩者到底什麼關係? 先得有一些基礎程式碼,這裡就用大家初學事件時用到的 貓和老鼠 經典案例,程式碼簡化如下:


    class Program
    {
        static void Main(string[] args)
        {
            Cat cat = new Cat("湯姆");
            Mouse mouse1 = new Mouse("傑瑞", cat);
            Mouse mouse2 = new Mouse("傑克", cat);
            cat.CatComing();
            Console.ReadKey();
        }
    }

    class Cat
    {
        public event Action CatCome;   //宣告一個事件

        private string name;

        public Cat(string name)
        {
            this.name = name;
        }
        public void CatComing()
        {
            Console.WriteLine("貓" + name + "來了");
            CatCome?.Invoke();
        }
    }

    class Mouse
    {
        private string name;

        public Mouse(string name, Cat cat)
        {
            this.name = name;
            cat.CatCome += this.RunAway;        //Mouse 註冊 CatCome 主題
        }
        public void RunAway()
        {
            Console.WriteLine(name + "正在逃跑");
        }
    }

程式碼非常簡潔,貓的 CatCome 動作一旦觸發,註冊到 CatCome 上的 兩隻 mouse 就會執行各自的逃跑動作 RunAway,如果大家沒有看懂可以多看幾遍哈。

2. 觀察者模式/釋出訂閱模式

如果你瞭解過設計模式,我想你應該第一眼就能看出這是 觀察者模式,對的,現在無數的框架都在使用這個模式,比如前端的: Vue,Knockout,React,還有redis的釋出訂閱等等,如果用圖畫一下大概就是這樣。

從圖中可以看到,幾個 subscribe 都訂閱了一個叫做 subject 的主題,一旦有外來的 publish 推送到了 subject,那麼訂閱 subject 的 subscribe 都會收到通知,接下來根據這張圖對剛才的程式碼再縷一篇:

  • 貓的 public event Action CatCome 就是一個主題 (subject)。
  • 老鼠的 cat.CatCome += this.RunAway 就是 subscribe 對 subject 的訂閱。
  • 最後的 public void CatComing() 就是對 subject 的推送, pubish了一條 貓來了

3. 使用觀察者模式 對 貓鼠進行解剖

有了觀察者模式的基礎,對上面的程式碼進行改造就方便多了, 我可以把 public event Action CatCome; 改成 一個 List<Action> 陣列,模擬 Subject 哈,簡化後的程式碼如下:


    class Cat
    {
        public List<Action> Subject = new List<Action>();   //定義一個主題

        private string name;

        public Cat(string name)
        {
            this.name = name;
        }
        public void CatComing()
        {
            Console.WriteLine("貓" + name + "來了");

            Subject.ForEach(item => { item.Invoke(); });
        }
    }

    class Mouse
    {
        private string name;

        public Mouse(string name, Cat cat)
        {
            this.name = name;

            cat.Subject.Add(RunAway);    //將 逃跑 方法注入到 subject 中
        }
        public void RunAway()
        {
            Console.WriteLine(name + "正在逃跑");
        }
    }

看到這裡,我想你對 事件和委託 應該有一個大概的認識了吧,但這裡還有一個問題,C#中的事件 真的如我寫的觀察者模式這樣的嗎??? 要回答這個問題,需要從 IL 角度看一下事件到底生成了什麼。

三:從IL角度看事件

1. 使用 ilspy /ildasm 小工具

首先來看一下所謂的事件到底在 IL 層面是個什麼東西,如下圖:

從圖中看其實就是兩個接收 Action 引數的 add_CatComeremove_CatCome方法,這兩個方法簡化後的 il 程式碼如下:


.event [mscorlib]System.Action CatCome
{
	.addon instance void ConsoleApp2.Cat::add_CatCome(class [mscorlib]System.Action)
	.removeon instance void ConsoleApp2.Cat::remove_CatCome(class [mscorlib]System.Action)
}

.method public hidebysig specialname 
	instance void add_CatCome (
		class [mscorlib]System.Action 'value'
	) cil managed 
{
	// Method begins at RVA 0x2090
	// Code size 41 (0x29)
	.maxstack 3
	.locals init (
		[0] class [mscorlib]System.Action,
		[1] class [mscorlib]System.Action,
		[2] class [mscorlib]System.Action
	)

	IL_0000: ldarg.0
	IL_0001: ldfld class [mscorlib]System.Action ConsoleApp2.Cat::CatCome
	IL_0006: stloc.0
	// loop start (head: IL_0007)
		IL_000b: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate)
		IL_0010: castclass [mscorlib]System.Action

		IL_0017: ldflda class [mscorlib]System.Action ConsoleApp2.Cat::CatCome

		IL_001e: call !!0 [mscorlib]System.Threading.Interlocked::CompareExchange<class [mscorlib]System.Action>(!!0&, !!0, !!0)

	// end loop
	IL_0028: ret
} // end of method Cat::add_CatCome

.method public hidebysig specialname 
	instance void remove_CatCome (
		class [mscorlib]System.Action 'value'
	) cil managed 
{
	IL_0000: ldarg.0
	IL_0001: ldfld class [mscorlib]System.Action ConsoleApp2.Cat::CatCome
	IL_0006: stloc.0
	// loop start (head: IL_0007)
		IL_000b: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Remove(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate)
		IL_0010: castclass [mscorlib]System.Action

		IL_0017: ldflda class [mscorlib]System.Action ConsoleApp2.Cat::CatCome

		IL_001e: call !!0 [mscorlib]System.Threading.Interlocked::CompareExchange<class [mscorlib]System.Action>(!!0&, !!0, !!0)

		IL_0026: bne.un.s IL_0007
	// end loop
	IL_0028: ret
} // end of method Cat::remove_CatCome


接下來看看 mouse 類的註冊是怎麼實現的。

從圖中可以看到,所謂的註冊就是將 RunAway 作為 add_CatCome 方法的引數傳進去而已,回過頭來看,最核心的就是那兩個所謂的 addxxxremovexxx 方法。

2. 將IL程式碼進行C#還原

可能有些同學對 IL 程式碼不是很熟悉,如果能還原成 C# 程式碼就??了,接下來我就試著還原一下。


    class Cat
    {
        Action CatCome;

        public void add_CatCome(Action value)
        {
            Action action = this.CatCome;
            Action action2 = null;

            do
            {
                action2 = action;
                Action value2 = (Action)Delegate.Combine(action2, value);
                action = Interlocked.CompareExchange(ref this.CatCome, value2, action2);
            }
            while ((object)action != action2);
        }

        public void remove_CatCome(Action value)
        {
            Action action = this.CatCome;
            Action action2 = null;

            do
            {
                action2 = action;
                Action value2 = (Action)Delegate.Remove(action2, value);
                action = Interlocked.CompareExchange(ref this.CatCome, value2, action2);
            }
            while ((object)action != action2);
        }

        private string name;

        public Cat(string name)
        {
            this.name = name;
        }
        public void CatComing()
        {
            Console.WriteLine("貓" + name + "來了");
            CatCome?.Invoke();
        }
    }

    class Mouse
    {
        private string name;

        public Mouse(string name, Cat cat)
        {
            this.name = name;
            cat.add_CatCome(this.RunAway);
        }
        public void RunAway()
        {
            Console.WriteLine(name + "正在逃跑");
        }
    }

可以看出還原後的C#程式碼跑起來是沒有問題的,和觀察者模式相比,這裡貌似沒有看到 subject 這樣的 List<Action> 集合,但是你仔細分析的話,其實是有的,你一定要著重分析這句程式碼: Action value2 = (Action)Delegate.Combine(action2, value); 它用的就是多播委託,用 Combine 方法將後續的 Action 送到前者Action的 _invocationList 中,不信的話,我除錯給你看哈。

沒毛病吧, Action CatCome 中已經有了兩個 callback 方法啦,一旦 CatCome.Invoke(), _invocationList 中的方法就會被執行,也就看到兩隻老鼠在逃跑啦。

四: 總結

您現在是不是明白啦,委託和事件的關係 好比 磚頭和房子的關係,房子只是磚頭的一個應用場景,您如果說房子是一種特殊的磚,這句話品起來是不是有一種怪怪的感覺,不是嗎?

如您有更多問題與我互動,掃描下方進來吧~

圖片名稱

相關文章