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:
-
定義一個事件管理器
-
/// <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 }); } }
-
-
定義事件參數類和具體需要公開事件的類
-
//事件參數類 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()); } }
-
-
外部模塊不關注事件是顯式還是隱式實現,只需要用標準語法進行事件的登記
-
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