← 筆記

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

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

      •   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委托的實例,這個委托又可以引用更多的EventHandler委托實例
    • 即使聲明事件字段時將其聲明為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:
    • 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