Effective C# 08.對事件叫用使用空條件運算子 Effective C# 08.對事件叫用使用空條件運算子 (Use the Null Conditional Operator for Event Invocations)

Published on Sunday, September 29, 2024

本做法介紹了 ? 運算子的使用方式,與錯誤的使用 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 開頭的事件名稱, 這裡叫做 OnThresholdReached,最後使用 Invoke 方法呼叫他的訂閱者。

public class Counter
{
	public event EventHandler ThresholdReached;

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

接下來建立一個事件處理方法 c_ThresholdReached 並把它加入到 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);
	}
}

接下來要了解 ? 運算子的用法,從這個結構可以得知我們有可能沒有加入任何訂閱者,那就有可能造成 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 這種寫法才安全也比較好懂,使用複製區域變數的方式雖然也可以但是需要有經驗的程式設計師 才會了解其用意,一般的的程式設計師看到可能會誤解他的用法或者是移除。