使用事件的常用步驟:
- 定義事件參數;
- 事件源類型中聲明事件;
- 注冊處理事件的方法,即監聽;
- 再需要是,觸發事件。
public class CustomEventArgs: System.EventArgs
{
public string EventData { get; private set; }
public CustomEventArgs(string eventData)
{
EventData = eventData;
}
}
public class CustomEventSource
{
public System.EventHandler<CustomEventArgs> CustomEvent;
public void DoWork()
{
// do other work
// 最初的寫法
if(null != CustomEvent)
{
CustomEvent(this, new CustomEventArgs("event argument"));
}
// 有經驗的同事會告訴我們,需要這樣寫
//var handler = CustomEvent;
//if(null != handler)
//{
// handler(this, new CustomEventArgs("event argument"));
//}
}
}
public class EventUsage
{
private CustomEventSource handlerDemo;
public void Init()
{
handlerDemo = new CustomEventSource();
handlerDemo.CustomEvent += HandleCustomEvent;
}
private void HandleCustomEvent(object sender, CustomEventArgs e)
{
Console.WriteLine($"event: {e.EventData}");
}
public void UnInit()
{
if(null != handlerDemo)
{
handlerDemo.CustomEvent -= HandleCustomEvent;
}
}
}
其實那會不是很理解為什么這么寫,然后他們就告訴我,這樣可以避免多線程使用時帶來的bug,而且不好查。就是當前程序執行完if語句之后,會被另一個線程打斷,并且另一個線程解除事件監聽,也就是解除了事件訂閱,這樣事件處理程序成了null,這樣就引發了空引用的異常。所以會先賦值一個,用賦值后的內容去處理事件。原理是:該賦值會對賦值符號右邊的內容做淺拷貝(創建新引用,并令其指向原來的事件處理程序),當另一個線程注銷事件處理程序的時候,只會修改類實例中的handlerDemo字段,而不會把該處理程序同時從局部變量handler里面移走,這樣,handler中還保存著早前執行淺拷貝時所記錄的事件訂閱者,這樣就不會出錯了。
這樣的處理是線程安全的,但是會復雜一些,C#引入的null條件運算符,可以解決這個問題,即:
CustomEvent?.Invoke(this, new CustomEventArgs("event argument"));
現在編譯器也會智能提示我們這樣修改,這樣很方便,而且在語義上,與早期的if結構類似,但區別在于?.運算符左側內容只會計算一次。現在我已經慢慢習慣這種使用方式了。