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

C# 事件模式深度解析:Delegate、Event 與訂閱發布實戰

     編輯:關於C語言

核心概念

C# 事件模式建立在 委派 (Delegate) 之上,實作「發布-訂閱」模式。事件提供封裝層,防止外部直接呼叫委派或清除訂閱清單,確保封裝性。

標準事件模式三要素

  1. 委派型別:定義事件處理方法簽章
  2. 事件成員:使用 event 關鍵字宣告,限制存取權
  3. 引發方法:protected virtual void OnXxx(EventArgs e) 供衍生類別覆寫

完整範例:商品名稱變更通知

1. 定義事件引數 (可選,攜帶舊/新值)

public class NameChangedEventArgs : EventArgs
{
    public string OldName { get; }
    public string NewName { get; }
    public NameChangedEventArgs(string oldName, string newName)
    {
        OldName = oldName; NewName = newName;
    }
}

2. 發布者 (Publisher) - Product 類別

public class Product
{
    private string _name;

    // 1. 委派型別 (慣用 EventHandler<TEventArgs>)
    public event EventHandler<NameChangedEventArgs> NameChanged;

    public string Name
    {
        get => _name;
        set
        {
            if (_name == value) return; // 值未變更不引發
            string old = _name;
            _name = value;
            OnNameChanged(new NameChangedEventArgs(old, value));
        }
    }

    // 3. 引發方法 - 慣例命名 On + 事件名
    protected virtual void OnNameChanged(NameChangedEventArgs e)
    {
        // 安全引發模式:複製委派參考避免執行緒競態
        var handler = NameChanged;
        handler?.Invoke(this, e);
    }
}

3. 訂閱者 (Subscriber) - 表單或服務

public class OrderService
{
    public void Subscribe(Product product)
    {
        // 使用 += 訂閱,方法簽章需符合 EventHandler<NameChangedEventArgs>
        product.NameChanged += OnProductNameChanged;
    }

    public void Unsubscribe(Product product)
    {
        product.NameChanged -= OnProductNameChanged; // 必須使用相同方法參考
    }

    private void OnProductNameChanged(object sender, NameChangedEventArgs e)
    {
        var product = (Product)sender;
        Console.WriteLine($"[訂單服務] 產品名稱變更: '{e.OldName}' → '{e.NewName}' (ID: {product.GetHashCode()})");
        // 實際業務邏輯:更新訂單明細、發送通知、寫入稽核記錄...
    }
}

4. 使用示範

var product = new Product { Name = "滑鼠" };
var orderSvc = new OrderService();
orderSvc.Subscribe(product);

product.Name = "無線滑鼠"; // 觸發事件
// 輸出: [訂單服務] 產品名稱變更: '滑鼠' → '無線滑鼠' (ID: 123456)

orderSvc.Unsubscribe(product);
product.Name = "藍牙滑鼠"; // 不再觸發

進階主題

自訂委派 vs EventHandler<T>

方式適用場景範例
EventHandler<TEventArgs>標準 .NET 事件模式,支援 sender、e,工具箱自動識別UI 控制項、領域事件
Action<T1, T2...>簡單回呼、內部事件、不需 sender內部狀態機、簡易通知
自訂 delegate void XxxHandler(...)需特定參數名稱、回傳值、ref/out 參數需回傳取消旗標的驗證事件

執行緒安全引發模式

// 標準模式 (C# 6+ null-conditional operator)
protected virtual void OnNameChanged(NameChangedEventArgs e)
{
    NameChanged?.Invoke(this, e);
}

// 高併發場景:複製委派參考 (避免 Invoke 期間訂閱清單被修改)
protected virtual void OnNameChanged(NameChangedEventArgs e)
{
    var handler = Volatile.Read(ref NameChanged); // 或直接 var handler = NameChanged;
    handler?.Invoke(this, e);
}

事件存取器 (add/remove) 自訂儲存

// 預設編譯器產生私有委派欄位 + 鎖定同步
// 可自訂以節省記憶體 (大量事件物件) 或導向事件總線
private EventHandler<NameChangedEventArgs> _nameChanged;
public event EventHandler<NameChangedEventArgs> NameChanged
{
    add
    {
        lock (_lock) _nameChanged += value; // 或 EventHandlerList
    }
    remove
    {
        lock (_lock) _nameChanged -= value;
    }
}

弱式事件模式 (WeakEventManager)

解決訂閱者忘記取消訂閱導致記憶體洩漏 (發布者持有強參考)。WPF/UWP 內建 WeakEventManager,一般專案可用 WeakReference 封裝或引用 Microsoft.Toolkit.HighPerformance。

常見錯誤與最佳實務

  • ❌ 直接在外部呼叫委派:product.NameChanged(product, e) → 破壞封裝,改用 OnNameChanged protected virtual 供衍生類別擴充。
  • ❌ 匿名方法/Lambda 訂閱後無法取消:product.NameChanged += (s,e) => ... → 無法用 -= 移除。需保存委派實例:var handler = new EventHandler<...>(OnChanged); product.NameChanged += handler;
  • ❌ 事件引數重複配置:高頻事件 (如 MouseMove) 應重用 EventArgs 實例或使用 struct 避免 GC 壓力。
  • ✅ 命名慣例:事件名用動詞過去式 (NameChanged、Completed),引發方法 OnNameChanged,引數類別 NameChangedEventArgs。
  • ✅ 空值檢查:永遠使用 ?.Invoke 或複製參考模式,避免無訂閱者時拋 NullReferenceException。

標準 .NET 事件模式速查表

// 1. 引數類別
public class MyEventArgs : EventArgs { public string Data { get; set; } }

// 2. 發布者
public class Publisher
{
    public event EventHandler<MyEventArgs> MyEvent;
    protected virtual void OnMyEvent(MyEventArgs e) => MyEvent?.Invoke(this, e);
    public void DoWork() { /* ... */ OnMyEvent(new MyEventArgs { Data = "done" }); }
}

// 3. 訂閱者
public class Subscriber
{
    public void Hook(Publisher p) => p.MyEvent += Handle;
    public void Unhook(Publisher p) => p.MyEvent -= Handle;
    private void Handle(object sender, MyEventArgs e) { Console.WriteLine(e.Data); }
}
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved