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::newEventEvent #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