定義基底類別包含兩種事件寫法:
class Base
{
// 自動實作事件 (編譯器產生私有委派欄位 + add/remove)
public event Action TestEventA;
// 手動實作事件 (自行管理委派欄位)
private Action _testEventB;
public event Action TestEventB
{
add { _testEventB += value; }
remove { _testEventB -= value; }
}
protected void OnTestEventA() { TestEventA?.Invoke(); }
protected void OnTestEventB() { _testEventB?.Invoke(); }
}
class Child : Base
{
public void Do()
{
// TestEventA(); // 編譯錯誤:事件只能在宣告類別內部引發
}
}
兩種事件編譯後結構幾乎相同:
TestEventA,手動為 _testEventB)。Delegate.Combine/Remove 操作欄位,並以 lock(this) 確保執行緒安全。raise 存取器,也未將委派欄位設為 protected。add/remove 已處理同步,若開放欄位直接操作,開發者易忽略同步導致競態條件。protected virtual void OnXxx() 供子類別覆寫「引發前邏輯」,而非直接引發事件。
class Child : Base
{
public void DoSomething()
{
// 業務邏輯...
OnTestEventA(); // 呼叫基底類別受保護的引發方法
}
// 覆寫引發邏輯 (如加入日誌、條件判斷)
protected override void OnTestEventA()
{
Console.WriteLine("Child 準備引發 TestEventA");
base.OnTestEventA();
}
}
錯誤寫法:
protected void OnTestEventA() { TestEventA(); } // 多執行緒下,Invoke 期間若有移除訂閱,可能 NullReferenceException
正確寫法 (複製委派快照):
protected void OnTestEventA()
{
var handler = TestEventA; // 讀取欄位到區域變數 (原子操作)
handler?.Invoke(); // 對快照呼叫,避免競態
}
一位老先生問我如何複製貼上,我說 Ctrl+C 再 Ctrl+V。隔天他說不行,原來他是在公司 Ctrl+C,回家才 Ctrl+V。
真人版:某開發在基底類別定義 Event,子類別 A 引發,子類別 B 訂閱 —— 卻發現 B 收不到通知,因為是不同實例各自擁有獨立事件欄位。
C# 事件設計強制封裝引發權,透過 protected virtual OnXxx 讓子類別參與擴充,確保封裝性、執行緒安全與正確的發布/訂閱語意。若需跨實例通訊,請使用共用的事件來源物件 (如靜態事件、事件聚合器、MediatR 等)。