看看這個常常被初級程式設計師弄不懂的 “事件”

renke發表於2021-09-09

       眾所周知在面試中,經常有些崽子面試官會問些“事件和委託”的關係,也許一路走來的程式設計師大多都會被問到這個,那麼對於這個

高頻的”事件和委託“問題,如何回擊呢?首先我從最經典的一套面試題說起,用事件來實現 “貓爪老鼠“,這是一個從網上copy過來的一

個例子。

        static void Main(string[] args)
        {
            Mouse mouse = new Mouse();

            Cat cat = new Cat();

            cat.OnCry();

            Console.ReadLine();
        }
    }

    public delegate void CryEventHandler();

    public class Cat
    {
        public static event CryEventHandler Cry;

        public Cat()
        {
            Console.WriteLine("Cat:I'm coming.");
        }

        public virtual void OnCry()
        {
            Console.WriteLine("Cat:MiaoMiao");
            if (Cry != null)
            {
                Cry.Invoke();
            }
        }
    }

    public class Mouse
    {
        public Mouse()
        {
            Cat.Cry += new CryEventHandler(Run);
            Console.WriteLine("Mouse:I go to find something,and I must always listen cat's crying.");
        }

        public void Run()
        {
            Console.WriteLine("Mouse:A cat is coming,I must go back!");
        }
    }

 事件定義啥的什麼玩意這個我就不說了,沒什麼意思,為了瞭解這個跟委託有什麼關係,下面我們來看看這段程式碼最後生成的IL是什麼樣的。

 

1:CryEventHandler委託

1 public delegate void CryEventHandler();

    這個我想大家都清楚,委託本質上是一個繼承於MulticastDelegate的類,同時會生成僅有的4個方法,看下IL即知。

 

2:Cat類

 1 public class Cat
 2     {
 3         public static event CryEventHandler Cry;
 4 
 5         public Cat()
 6         {
 7             Console.WriteLine("Cat:I'm coming.");
 8         }
 9 
10         public virtual void OnCry()
11         {
12             Console.WriteLine("Cat:MiaoMiao");
13             if (Cry != null)
14             {
15                 Cry.Invoke();
16             }
17         }
18     }

從這個類中,我們看到了一個Cry事件,然後就是一個Cry.Invoke(),不過當你看到Invoke的時候,你是不是很懷疑Cry是不是一個委託欄位呢?

其實你懷疑的是一點問題都沒有,32個贊,看下IL。

 

從上圖中我們看到了兩個好玩的東西:

① field Cry 欄位,完整定義如下,然來所謂的“事件欄位” 其實在IL下面蛻變成了委託欄位,如果你覺得很奇怪,請看第二點。

.field private static class Sample.CryEventHandler Cry

② add_Cry,remove_Cry,如果僅僅將事件欄位變成委託欄位,那確實是編譯器在發什麼神經,然來編譯器還給事件配備了兩個方法,這個

    其實也就是事件裡面+=,-=的奧祕所在,下面我們挑add_Cry方法說下,看看方法定義的IL程式碼是怎麼樣的。

 

很新奇,我們找到了Combine方法,這個我們都知道,原來事件中的+=,其實就是利用Combine來將當前的委託例項放到Delegate的

委託連結串列中(其實裡面是array實現的),為了方便理解,我把上面的IL程式碼翻譯成C#程式碼。

 

 1  public class Cat
 2     {
 3         /// <summary>
 4         /// 私有的委託變數
 5         /// </summary>
 6         private static CryEventHandler Cry;
 7 
 8         /// <summary>
 9         /// 事件生成的方法
10         /// </summary>
11         /// <param name="cryEventHandler"></param>
12         public void Add_Cry(CryEventHandler cryEventHandler)
13         {
14             var result = (CryEventHandler)Delegate.Combine(Cry, cryEventHandler);
15 
16             Interlocked.CompareExchange<CryEventHandler>(ref Cry, result, Cry);
17         }
18 
19         public void Remove_Cry(CryEventHandler cryEventHandler)
20         {
21             var result = (CryEventHandler)Delegate.Remove(Cry, cryEventHandler);
22 
23             Interlocked.CompareExchange<CryEventHandler>(ref Cry, result, Cry);
24         }
25 
26         public Cat()
27         {
28             Console.WriteLine("Cat:I'm coming.");
29         }
30 
31         public virtual void OnCry()
32         {
33             Console.WriteLine("Cat:MiaoMiao");
34 
35             if (Cry != null)
36             {
37                 //委託專用的四個方法,invoke,begininvoke,endinvoke,ctor
38                 Cry.Invoke();
39             }
40         }
41     }

可能有些同學對IL指令不是很熟悉,沒關係,我也一樣,我們部落格園上面有位大神飛鳥的一篇IL指令集的博文或許能幫得到你。

 

3:Mouse類

    如果你對Cat類的IL程式碼琢磨的差不多的話,下面這個Mouse類就非常簡單了,僅僅呼叫而已嘛。

 1     public class Mouse
 2     {
 3         public Mouse()
 4         {
 5             Cat.Cry += new CryEventHandler(Run);
 6             Console.WriteLine("Mouse:I go to find something,and I must always listen cat's crying.");
 7         }
 8 
 9         public void Run()
10         {
11             Console.WriteLine("Mouse:A cat is coming,I must go back!");
12         }
13     }

這個地方最讓人關心的就是:Cat.Cry += new CryEventHandler(Run) 這個語句,從它的IL中可以看到其實做了三件事。

① ldftn:       將Run方法的非託管指標推送到計算堆疊上。

② newobj:   將CryEventHandler委託new一下,同時將計算堆疊上的Run方法的非託管指標作為建構函式的引數。

③ call:         呼叫Cat類的Add_Cry方法,將CryEventHandler的例項作為引數傳遞下去。

 

下面繼續將該IL程式碼反編譯回來,不過針對IL指令:call       void Sample.Cat::add_Cry(class Sample.CryEventHandler)

並沒有很好的翻譯過來,只能new Cat()了一下才能呼叫Add_Cry,從而觸發了Cat的建構函式。

 1   public class Mouse
 2     {
 3         public Mouse()
 4         {
 5             var cryHandler = new CryEventHandler(Run);
 6 
 7             /*
 8              *  針對IL:call       void Sample.Cat::add_Cry(class Sample.CryEventHandler)
 9              *  這個沒有反編譯好,因為我new Cat()將會再次呼叫建構函式。
10              */
11             new Cat().Add_Cry(cryHandler);
12 
13             Console.WriteLine("Mouse:I go to find something,and I must always listen cat's crying.");
14         }
15 
16         public void Run()
17         {
18             Console.WriteLine("Mouse:A cat is coming,I must go back!");
19         }
20     }

 

好了,說了這麼多,應該也有總結性的東西出來了,原來事件是完完全全的建立在委託的基礎上,你可以認為事件就是用委託來玩一個

觀察者模式的,你甚至可以認為事件就是委託。沒有本質區別。

相關文章