首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

C# 事件存取器深度解析:為什麼需要自定義 add/remove?

     編輯:關於.NET

前言:編譯器生成的默認實現夠用嗎?

聲明 public event EventHandler MyEvent; 時,編譯器自動生成背後委托欄位 _myEvent 及線程安全的 add/remove 存取器 (加 MethodImplOptions.Synchronized)。大多數情況下這已足夠,但特定場景下必須手寫存取器。本文系統梳理四大核心理由並給出典型代碼模式。

理由一:自定義底層存儲 —— 節省記憶體 (EventHandlerList 模式)

問題

類別擁有大量事件 (如 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] (隱藏欄位),手寫存取器可控制欄位屬性。
  • 同理適用於 XML 序列化 (預設只序列化 public),但二進制序列化最常見。

理由三:自定義同步策略 —— 避免 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 創建廣播視窗、訊息泵)
權限要求addnew EventLogPermission(...).Demand(),無權限拋異常阻止訂閱
委托預備 (CER)addRuntimeHelpers.PrepareDelegate(value),確保受約束執行區域 (CER) 內委托可靠執行
註冊/註銷外部鉤子add/removeConsole.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; }
}

CER 委托預備範例


public event EventHandler MyEvent {
    add {
        RuntimeHelpers.PrepareDelegate(value); // 預編譯/預備委托
        _myEvent += value;
    }
    remove { _myEvent -= value; }
}

總結:何時該寫自定義存取器?

  1. 大量事件、稀疏訂閱 → EventHandlerList 集中存儲。
  2. 需控制序列化 → 手寫欄位加 [NonSerialized]。
  3. 避免 lock(this) / 需自定義同步 → 私有鎖物件。
  4. 顯式介面實現同名事件 → 必須手寫至少一個。
  5. 訂閱時需執行副作用 (初始化、權限、CER、外部註冊) → add 注入邏輯。

大多數情況編譯器默認實現足夠。只有遇到上述具體場景,才動用自定義存取器 —— 這是「按需付費」的進階功能。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved