聲明 public event EventHandler MyEvent; 時,編譯器自動生成背後委托欄位 _myEvent 及線程安全的 add/remove 存取器 (加 MethodImplOptions.Synchronized)。大多數情況下這已足夠,但特定場景下必須手寫存取器。本文系統梳理四大核心理由並給出典型代碼模式。
類別擁有大量事件 (如 Control 69 個),每個事件一個委托欄位,32 位元系統每實例浪費 276 位元組。實際運行時極少數事件被訂閱。
class MyClass {
private static readonly object _myEventKey = new object(); // 靜態鍵,全域唯一
private EventHandlerList _handlers = new EventHandlerList(); // 單一列表存所有事件委托
public event EventHandler MyEvent {
add { _handlers.AddHandler(_myEventKey, value); }
remove { _handlers.RemoveHandler(_myEventKey, value); }
}
protected void OnMyEvent() {
var handler = _handlers[_myEventKey] as EventHandler;
handler?.Invoke(this, EventArgs.Empty);
}
}
.NET 二進制序列化預設序列化所有欄位 (含私有)。委托引用目標物件,導致訂閱者被意外序列化,甚至拋出異常。
NonSerialized + 手寫存取器
[Serializable]
class MyClass {
[NonSerialized] private EventHandler _myEvent; // 明確標記不序列化
public event EventHandler MyEvent {
[MethodImpl(MethodImplOptions.Synchronized)]
add { _myEvent = (EventHandler)Delegate.Combine(_myEvent, value); }
[MethodImpl(MethodImplOptions.Synchronized)]
remove { _myEvent = (EventHandler)Delegate.Remove(_myEvent, value); }
}
}
[NonSerialized] (隱藏欄位),手寫存取器可控制欄位屬性。lock(this) 反模式編譯器生成的 Synchronized 等效於 lock(this),暴露鎖物件,外部代碼可鎖定你的實例導致死鎖風險。
class MyClass {
private readonly object _eventLock = new object();
private EventHandler _myEvent;
public event EventHandler MyEvent {
add { lock (_eventLock) _myEvent += value; }
remove { lock (_eventLock) _myEvent -= value; }
}
}
ReaderWriterLockSlim),事件存取器可復用,保持一致性。
interface I1 { event EventHandler MyEvent; }
interface I2 { event EventHandler MyEvent; }
class MyClass : I1, I2 {
// 隱式實現:單一事件同時滿足 I1 和 I2
public event EventHandler MyEvent;
// 顯式實現 I2.MyEvent:分離存儲
private EventHandler _i2MyEvent;
event EventHandler I2.MyEvent {
add { _i2MyEvent += value; }
remove { _i2MyEvent -= value; }
}
}
| 場景 | 典型代碼位置 | 說明 |
|---|---|---|
| 延遲初始化 | add | 首次訂閱時創建昂貴資源 (如 SystemEvents 創建廣播視窗、訊息泵) |
| 權限要求 | add | new EventLogPermission(...).Demand(),無權限拋異常阻止訂閱 |
| 委托預備 (CER) | add | RuntimeHelpers.PrepareDelegate(value),確保受約束執行區域 (CER) 內委托可靠執行 |
| 註冊/註銷外部鉤子 | add/remove | Console.CancelKeyPress 註冊 SetConsoleCtrlHandler 回調 |
class MyClass {
private static bool _initialized;
private static readonly object _lock = new object();
private static EventHandler _myEvent;
public static event EventHandler MyEvent {
add {
lock (_lock) {
EnsureInitialized(); // 首次訂閱時初始化
_myEvent += value;
}
}
remove { lock (_lock) _myEvent -= value; }
}
private static void EnsureInitialized() {
if (!_initialized) { /* 創建視窗、啟動訊息泵 */ _initialized = true; }
}
}
public event EventHandler MyEvent {
add {
new EventLogPermission(EventLogPermissionAccess.Administer, ".").Demand();
_myEvent += value;
}
remove { _myEvent -= value; }
}
public event EventHandler MyEvent {
add {
RuntimeHelpers.PrepareDelegate(value); // 預編譯/預備委托
_myEvent += value;
}
remove { _myEvent -= value; }
}
EventHandlerList 集中存儲。[NonSerialized]。lock(this) / 需自定義同步 → 私有鎖物件。add 注入邏輯。大多數情況編譯器默認實現足夠。只有遇到上述具體場景,才動用自定義存取器 —— 這是「按需付費」的進階功能。