C#筆記 – 事件

  • CLR事件模型以委托為基礎

    • 委托以類型安全的方式調用回調

    • 對象憑藉回調方法接收它們訂閱的通知

    • 通常是為了響應提供事件的類型/對象的狀態的改變(回調),通知其他對象發生了特定的事情
  • 事件包含:

    • 允許靜態/實例方法登記對事件關注的方法

    • 允許靜態/實例方法注銷對事件關注的方法

    • 一個維護已登記的方法集的委托字段

      • 已登記方法的列表,事件發生後,將通知列表中所有已登記的方法

  • public event EventHandler someEvent;
  • 靜態事件

    • 讓類型向一個/多個靜態/實例方法發送通知

  • 實例事件

    • 讓對象向一個/多個靜態/實例方法發送通知

要公開事件的類型設計
  • 一、定義容納所有需要發送給事件通知接收者的附加信息(事件參數)類

    • 該類繼承自System.EventArgs

    • class TestArgs : EventArgs
      {
         private readonly int x;
         public int X { get { return x; } }

      public
      TestArgs(int x)

        {
              this.x = x;
        }
      }  
  • 二、定義事件成員

    • 使用event關鍵字定義一個委托類型的成員

    • public event EventHandler<TestArgs> newEvent;
    • newEvent為事件名;成員類型為EventHandler<TestArgs>

      • 意味著所有「事件通知」的接收者都必須提供一個和EventHandler<TestArgs>委托的簽名匹配的回調方法

      • public delegate void EventHandler<TEventArgs>(object sender, TEventArgs args);
      • 方法必須具有以下形式:

        • public void MethodName(Object sender, TestArgs e){ }
  • 三、定義引發事件的方法來通知事件的登記對象

    • 一個接收一個參數(EventArgs)的保護虛方法

    • protected virtual void Raise(TestArgs arg)
      {
         newEvent?.Invoke(this, arg);
      }
  • 四、定義方法使輸入轉化為事件的引發

    • public void InputSimulation(int x)
      {
         TestArgs arg = new TestArgs(x);
         Raise(arg);
      }

編譯器中的事件實現
  • 比如聲明了一個事件:

    • public event EventHandler<TestArgs> newEvent;
  • C#編譯器編譯時會將它轉換以下3個構造

    • 私有委托字段

      • 該字段是對一個委托列表的頭部的引用,事件發生時會通知這個列表中的委托

        • 一個方法通過「添加關注」方法登記對事件的關注時,該字段會引用EventHandler<TestArgs>委托的實例,這個委托又可以引用更多的EventHandler<TestArgs>委托實例

      • 即使聲明事件字段時將其聲明為public,其編譯後的委托字段也始終是private,防止類外部的代碼不正確地操縱它

    • 公共「添加關注」方法

      • .method public hidebysig specialname instance void 
        add_newEvent(class [System.Runtime]System.EventHandler`1<class CLR_Ch11.TestArgs> 'value') cil managed
        {
        .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 )
        // 程式碼大小       41 (0x29)
        .maxstack 3
        .locals init (class [System.Runtime]System.EventHandler`1<class CLR_Ch11.TestArgs> V_0,
        class [System.Runtime]System.EventHandler`1<class CLR_Ch11.TestArgs> V_1,
        class [System.Runtime]System.EventHandler`1<class CLR_Ch11.TestArgs> V_2)
        IL_0000: ldarg.0
        IL_0001: ldfld     class [System.Runtime]System.EventHandler`1<class CLR_Ch11.TestArgs> CLR_Ch11.Program::newEvent
        IL_0006: stloc.0
        IL_0007: ldloc.0
        IL_0008: stloc.1
        IL_0009: ldloc.1
        IL_000a: ldarg.1
        IL_000b: call       class [System.Runtime]System.Delegate [System.Runtime]System.Delegate::Combine(class [System.Runtime]System.Delegate,
        class [System.Runtime]System.Delegate)
        IL_0010: castclass class [System.Runtime]System.EventHandler`1<class CLR_Ch11.TestArgs>
        IL_0015: stloc.2
        IL_0016: ldarg.0
        IL_0017: ldflda     class [System.Runtime]System.EventHandler`1<class CLR_Ch11.TestArgs> CLR_Ch11.Program::newEvent
        IL_001c: ldloc.2
        IL_001d: ldloc.1
        IL_001e: call       !!0 [System.Threading]System.Threading.Interlocked::CompareExchange<class [System.Runtime]System.EventHandler`1<class CLR_Ch11.TestArgs>>(!!0&,
        !!0,
        !!0)
        IL_0023: stloc.0
        IL_0024: ldloc.0
        IL_0025: ldloc.1
        IL_0026: bne.un.s   IL_0007
        IL_0028: ret
        } // end of method Program::add_newEvent
      • 該方法允許了其他對象登記對事件的關注,調用了System.Delegate的靜態Combine方法將委托實例添加到委托列表中,返回新的列表頭,存回到上面的私有委托字段中

    • 公共「移除關注」方法

      • .method public hidebysig specialname instance void 
        remove_newEvent(class [System.Runtime]System.EventHandler`1<class CLR_Ch11.TestArgs> 'value') cil managed
        {
        .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 )
        // 程式碼大小       41 (0x29)
        .maxstack 3
        .locals init (class [System.Runtime]System.EventHandler`1<class CLR_Ch11.TestArgs> V_0,
        class [System.Runtime]System.EventHandler`1<class CLR_Ch11.TestArgs> V_1,
        class [System.Runtime]System.EventHandler`1<class CLR_Ch11.TestArgs> V_2)
        IL_0000: ldarg.0
        IL_0001: ldfld     class [System.Runtime]System.EventHandler`1<class CLR_Ch11.TestArgs> CLR_Ch11.Program::newEvent
        IL_0006: stloc.0
        IL_0007: ldloc.0
        IL_0008: stloc.1
        IL_0009: ldloc.1
        IL_000a: ldarg.1
        IL_000b: call       class [System.Runtime]System.Delegate [System.Runtime]System.Delegate::Remove(class [System.Runtime]System.Delegate,
        class [System.Runtime]System.Delegate)
        IL_0010: castclass class [System.Runtime]System.EventHandler`1<class CLR_Ch11.TestArgs>
        IL_0015: stloc.2
        IL_0016: ldarg.0
        IL_0017: ldflda     class [System.Runtime]System.EventHandler`1<class CLR_Ch11.TestArgs> CLR_Ch11.Program::newEvent
        IL_001c: ldloc.2
        IL_001d: ldloc.1
        IL_001e: call       !!0 [System.Threading]System.Threading.Interlocked::CompareExchange<class [System.Runtime]System.EventHandler`1<class CLR_Ch11.TestArgs>>(!!0&,
        !!0,
        !!0)
        IL_0023: stloc.0
        IL_0024: ldloc.0
        IL_0025: ldloc.1
        IL_0026: bne.un.s   IL_0007
        IL_0028: ret
        } // end of method Program::remove_newEvent
      • 該方法允許了其他對象注銷對事件的關注,調用了System.Delegate的靜態Remove方法,將委托實例從委托列表中刪除,返回新的列表頭引用至上面的私有委托字段中

    • 上述的「添加」和「移除」方法都是公共的,是因為事件字段被定義為公共的。

      • 雖然事件字段編譯後轉換成的委托構造無論如何都是private的,但是「添加」和「移除」方法的可訪問性與事件字段的定義保持一致

      • 意味著事件的可訪問性決定了甚麼代碼能登記和注銷對事件的關注

      • 無論如何都只有類型本身才能直接訪問委托字段

      • 事件也可以被定義為virtual或者static的,如此一來,其生成的add和remove方法也會被標記為static/virtual

  • 另外,編譯器還會在元數據中生成一個「事件定義記錄項」

    • .event class [System.Runtime]System.EventHandler`1<class CLR_Ch11.TestArgs> newEvent
      {
      .addon instance void CLR_Ch11.Program::add_newEvent(class [System.Runtime]System.EventHandler`1<class CLR_Ch11.TestArgs>)
      .removeon instance void CLR_Ch11.Program::remove_newEvent(class [System.Runtime]System.EventHandler`1<class CLR_Ch11.TestArgs>)
      } // end of event Program::newEvent
    • Event #1 (14000001)
      -------------------------------------------------------
      Name     : newEvent (14000001)
      Flags     : [none] (00000000)
      EventType : 1B000001 [TypeSpec]
      AddOnMethd: (06000001) add_newEvent
      RmvOnMethd: (06000002) remove_newEvent
      FireMethod: (06000000)
      0 OtherMethods
    • 該記錄項包含了一些flag和基礎委托類型,主要是類似屬性(Property)一樣,引用了自身的「訪問器方法」—— 「add」和「remove」

    • 可以通過System.Reflection.EventInfo獲取這些信息

  • 事件關注的添加與移除
    • 使用+= / -= 進行

    • C#編譯器內置了對事件的支持,會將+=/-=操作符翻譯成對應的「事件關注添加/移除方法」的調用

    • 添加(+=)

      • public event EventHandler<TestArgs> newEvent;

        public void MethodName(Object sender, TestArgs e)
        {
           Console.WriteLine("Debug Somthing");
        }
        public void AddEvent()
        {
           newEvent += MethodName;
        }
      • //Add Event
        .method public hidebysig instance void AddEvent() cil managed
        {
        //...
        IL_000e: call       instance void CLR_Ch11.Program::add_newEvent(class [System.Runtime]System.EventHandler`1<class CLR_Ch11.TestArgs>)
        //...
        } // end of method Program::AddEvent
    • 移除(-=)

      • public event EventHandler<TestArgs> newEvent;

        public void MethodName(Object sender, TestArgs e)
        {
           Console.WriteLine("Debug Somthing");
        }
        public void RemoveEvent()
        {
           newEvent -= MethodName;
        }
      • //Remove Event
        .method public hidebysig instance void RemoveEvent() cil managed
        {
        //...
        IL_000e: call       instance void CLR_Ch11.Program::remove_newEvent(class [System.Runtime]System.EventHandler`1<class CLR_Ch11.TestArgs>)
        //...
        } // end of method Program::RemoveEvent
    • 對象不再希望接收事件通知時,需要注銷對事件的關注。因為只要對象向事件登記了它的一個方法,該對象就不能被回收

顯式實現事件
  • 如果一個類有n個事件,全部都直接用event關鍵字來定義,那麼它就會自動生成n個上面的編譯後結構

  • 為了提升效率,比較好的方案是「顯式實現」我們重點關注的事件

  • 公開了事件的每個對象都應維護一個集合(一般是字典),以某種事件標識符(如枚舉)為鍵,對應的委托列表為值。

  • 對象構造時,該集合是空的

    • 登記對一個事件的關注時,會在集合中查找事件的標識符:

      • 找到:合并委托列表

      • 找不到:添加標識符與新委托至集合

    • 需要引發事件時,會在集合查找事件標識符:

      • 沒有找到對應的標識符:說明沒有任何對象登記對這個事件的關注,沒有任何委托需要回調

      • 找到了對應的標識符:遍歷調用關聯的委托列表

    • 注銷對一個事件的關注時,在集合中查找事件的標識符:

      • 找到:掃瞄並移除指定的委托

      • 找不到:事件從沒有被該委托關注過

  • Steps:

    1. 定義一個事件管理器

      • /// <summary>
        /// 事件集合控制器
        /// </summary>
        public sealed class EventSet
        {
           readonly Dictionary<EventKey, Delegate> events = new Dictionary<EventKey, Delegate>();

           public void Add(EventKey key, Delegate handler)
          {
               Delegate d;
               events.TryGetValue(key, out d);
               events[key] = Delegate.Combine(d, handler);
          }

           public void Remove(EventKey key, Delegate handler)
          {
               Delegate d;
               if(events.TryGetValue(key, out d))
              {
                   d = Delegate.Remove(d, handler);
                   if (d != null) { events[key] = d; }
                   else { events.Remove(key); }
              }
          }

           public void Raise(EventKey key, object sender, EventArgs e)
          {
               Delegate d;
               events.TryGetValue(key, out d);
               d?.DynamicInvoke(new object[] { sender, e });
          }
        }
    2. 定義事件參數類和具體需要公開事件的類

      • //事件參數類
        public class FooEventArgs : EventArgs { }

        //一個包含了很多事件的類
        public class TypeWithLotsOfEvents
        {
           //實例化一個事件管理器
           readonly EventSet m_EventSet = new EventSet();
           protected EventSet EventSet { get { return m_EventSet; } }

           //一個事件的標識符
           protected static readonly EventKey m_Key = new EventKey();

           //事件訪問器,用於在集合中增刪委托
           public event EventHandler<FooEventArgs> Foo
          {
               //add/remove的顯式調用
               //外部模塊通過這裡向該類的事件管理器添加/移除事件  
               add { m_EventSet.Add(m_Key, value); }
               remove { m_EventSet.Remove(m_Key, value); }
          }

           //發起事件入口
           protected virtual void OnFoo(FooEventArgs e)
          {
               //執行經過上面的訪問器添加至事件管理器的回調函數
               m_EventSet.Raise(m_Key, this, e);
          }
           public void SimulateFoo()
          {
               OnFoo(new FooEventArgs());
          }        
        }
    3. 外部模塊不關注事件是顯式還是隱式實現,只需要用標準語法進行事件的登記

      • static void Main(string[] args)
        {
           TypeWithLotsOfEvents twie = new TypeWithLotsOfEvents();

           twie.Foo += HandleFooEvent;

           twie.SimulateFoo(); //Worked!
        }

        private static void HandleFooEvent(object sender, FooEventArgs e)
        {
           Console.WriteLine("Handling Foo Event Here...");
        }


參考書目

  • 《CLR via C#》(第4版) Jeffrey Richter