本做法介紹了 ? 運算子的使用方式,與錯誤的使用 event 可能會造成 NullReferenceException。 首先同樣也複習一下 event 的使用方式,典型的事件用法是在類別或物件發生變動時,告知其他類別或物件或者執行方法, 通常會有一個發佈者(publisher)與接收者(subscriber)。

從原始碼可以知道事件其實也是一種特殊的委派因此我們可以自己手動寫一個事件委派。

public delegate void SampleEventHandler(object sender, SampleEventArgs e);

public class SampleEventArgs
{
	public SampleEventArgs(string text) { Text = text; }
	public string Text { get; } // readonly
}

同樣.NET 也有提供內建的事件委派叫做 EventHandler 給我們使用,通常使用內建的就夠了,基本上不需要自己宣告事件委派,

public delegate void EventHandler(object? sender, EventArgs e);

與一般委派不同,我們需要使用 event 關鍵字宣告事件 ThresholdReached,並且根據官方文檔建議建立一個 On 開頭的 protectedvirtual 的方法用來喚起(raise)事件,也就是下方的 OnThresholdReached 方法。

這樣我們就能在未來建立衍生型別,透過覆寫的方式來修改 OnThresholdReached 執行流程。

public class Counter
{
	public event EventHandler ThresholdReached;

	public virtual void OnThresholdReached(EventArgs e)
	{
		ThresholdReached?.Invoke(this, e);
	}
}

接下來建立一個事件處理方法 c_ThresholdReached 並把它加入到 ThresholdReached 事件裡面,可以這樣加是因為 EventHandler 本質上就是 Delegate 並且 .NET 中的 Delegate 都是 MulticastDelegate,所以使用加號就是新增委派到 _invocationList 裡面而已。

所以能讓 c_ThresholdReached 方法變成一個等待被呼喚的方法,也可以說它是訂閱者。 最後呼叫 OnThresholdReached 方法,模擬事件觸發可以看到視窗輸出提示訊息。

void Main()
{
	var c = new MyCounter();
	c.ThresholdReached += c_ThresholdReached;
	c.OnThresholdReached(new ThresholdReachedEventArgs());
}

static void c_ThresholdReached(object sender, EventArgs e)
{
	Console.WriteLine("The threshold was reached.");
}

public class MyCounter : Counter
{
	public override void OnThresholdReached(EventArgs e)
	{
		base.OnThresholdReached(e);
	}
}

public class ThresholdReachedEventArgs : EventArgs
{
    public int Threshold { get; set; }
    public DateTime TimeReached { get; set; }
}

接下來要了解 ? 運算子的用法,從這個結構可以得知我們有可能沒有加入任何訂閱者,那就有可能造成 NullReferenceException,因此需要 事先檢查 ThresholdReached 是否為空,可以直接將 ? 拿掉即可看到錯誤產生。

public class Counter
{
	public event EventHandler ThresholdReached;

	public virtual void OnThresholdReached(EventArgs e)
	{
		ThresholdReached?.Invoke(this, e);
	}
}

? 運算子還沒有出現前是使用傳統的 null 檢查來避免報錯。

public class Counter
{
	public event EventHandler ThresholdReached;

	public virtual void OnThresholdReached(EventArgs e)
	{
		if(ThresholdReached != null)
		{
			ThresholdReached(this, e);
		}
	}
}

但是上面的寫法並不是線程安全的,因此產生出下面這個特殊寫法,也就是先將目前的 EventHandler 複製到區域變數之中, 這個用意是複製所當初有訂閱的 EventHandler 的參考,這樣即使 ThresholdReached 在某一個線程突然被修改為空也沒關係。

public class Counter
{
	public event EventHandler ThresholdReached;

	public virtual void OnThresholdReached(EventArgs e)
	{
		var handler = ThresholdReached;
		if(handler != null)
		{
			handler(this, e);
		}
	}
}

但是上面這個已經是過時的寫法了,現在是建議使用一開始的寫法,不僅可以檢查空值也能確保線程安全。

public class Counter
{
	public event EventHandler ThresholdReached;

	public virtual void OnThresholdReached(EventArgs e)
	{
		ThresholdReached?.Invoke(this, e);
	}
}

Summary

這個做法複習了 EventHandler 這個委派的用法還有 event 關鍵字,還有主要的內容 ? 運算子,現在建議最佳的做法 就是使用事件一定要使用 ?.Invoke 這種寫法才安全也比較好懂,使用複製區域變數的方式雖然也可以但是需要有經驗的程式設計師 才會了解其用意,一般的的程式設計師看到可能會誤解他的用法或者是移除。