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

C# 事件機制深度解析:從委派到事件的完整演進

     編輯:關於C語言

前言

事件是 C# 實現觀察者模式的核心語言特性,廣泛用於 UI 互動、非同步通知、領域事件等場景。本文深入剖析事件的底層原理、語法糖本質、進階用法與最佳實踐,幫助開發者從「會用」進階到「懂原理、能設計」。

核心關係:委派 → 事件

委派:類型安全的函式指標


// 委派宣告:定義方法簽章
public delegate void EventHandler(object sender, EventArgs e);

// 等同於編譯器產生的類別
public sealed class EventHandler : MulticastDelegate
{
    public EventHandler(object @object, IntPtr method);
    public virtual IAsyncResult BeginInvoke(object sender, EventArgs e, AsyncCallback callback, object object);
    public virtual void EndInvoke(IAsyncResult result);
    public virtual void Invoke(object sender, EventArgs e);
}

事件:受限的委派欄位/屬性

關鍵差異:

特性委派欄位事件
外部可直接呼叫 Invoke是否 (僅類別內部可觸發)
外部可直接指派 (=)是否 (僅 += / -=)
外部可清空委派鏈是否
封裝性破壞封裝保護發布者控制權

事件語法完整形式

1. 欄位型事件 - 編譯器自動產生 add/remove


public class Button
{
    // 編譯器產生:私有委派欄位 + 公開 add/remove 存取器
    public event EventHandler Click;
    
    protected virtual void OnClick(EventArgs e)
    {
        Click?.Invoke(this, e); // 安全觸發
    }
}

2. 屬性型事件 - 自訂儲存邏輯 (節省記憶體)


public class Control : Component
{
    // 靜態唯一鍵值,避免每個實例都配置委派欄位
    static readonly object MouseDownEventKey = new object();
    static readonly object MouseUpEventKey = new object();
    
    // 內部儲存:EventHandlerList (鍵值對字典)
    protected EventHandlerList Events { get; } = new EventHandlerList();
    
    protected Delegate GetEventHandler(object key) => Events[key];
    protected void SetEventHandler(object key, Delegate handler) => Events[key] = handler;
    
    public event MouseEventHandler MouseDown
    {
        add => Events.AddHandler(MouseDownEventKey, value);
        remove => Events.RemoveHandler(MouseDownEventKey, value);
    }
    
    public event MouseEventHandler MouseUp
    {
        add => Events.AddHandler(MouseUpEventKey, value);
        remove => Events.RemoveHandler(MouseUpEventKey, value);
    }
}

事件存取器詳細行為


public event EventHandler MyEvent
{
    // 外部 += 時呼叫
    add
    {
        lock (this) // 或自訂同步
        {
            _myEvent += value;
        }
    }
    
    // 外部 -= 時呼叫
    remove
    {
        lock (this)
        {
            _myEvent -= value;
        }
    }
}

// 等價於欄位型事件的編譯器產生代碼
private EventHandler _myEvent;
public event EventHandler MyEvent
{
    add => _myEvent += value;
    remove => _myEvent -= value;
}

標準事件模式

1. EventArgs 承載資料


// 含資料的事件參數
public class PriceChangedEventArgs : EventArgs
{
    public decimal OldPrice { get; }
    public decimal NewPrice { get; }
    public PriceChangedEventArgs(decimal oldPrice, decimal newPrice)
    {
        OldPrice = oldPrice; NewPrice = newPrice;
    }
}

// 發布者
public class Stock
{
    public event EventHandler PriceChanged;
    
    decimal _price;
    public decimal Price
    {
        get => _price;
        set
        {
            if (_price != value)
            {
                var args = new PriceChangedEventArgs(_price, value);
                _price = value;
                OnPriceChanged(args);
            }
        }
    }
    
    protected virtual void OnPriceChanged(PriceChangedEventArgs e)
    {
        PriceChanged?.Invoke(this, e);
    }
}

2. 訂閱者模式


var stock = new Stock();
stock.PriceChanged += (sender, e) =>
{
    Console.WriteLine($"價格變動: {e.OldPrice} → {e.NewPrice}");
};
stock.Price = 100; // 觸發事件

進階主題

1. 事件與介面


public interface INotifyPriceChanged
{
    event EventHandler PriceChanged;
}

// 明確實作介面事件
public class Stock : INotifyPriceChanged
{
    event EventHandler INotifyPriceChanged.PriceChanged
    {
        add => PriceChanged += value;
        remove => PriceChanged -= value;
    }
    
    public event EventHandler PriceChanged;
    // ...
}

2. 靜態事件


public static class ApplicationEvents
{
    public static event EventHandler ApplicationExit;
    
    internal static void OnApplicationExit()
    {
        ApplicationExit?.Invoke(null, EventArgs.Empty);
    }
}

// 訂閱需注意:靜態事件生命週期 = 應用程式域,易造成記憶體洩漏
ApplicationEvents.ApplicationExit += OnExit;
// 必須在適當時機 -= 移除

3. 事件與繼承


public class Base
{
    public virtual event EventHandler MyEvent;
    protected virtual void OnMyEvent(MyEventArgs e) => MyEvent?.Invoke(this, e);
}

public class Derived : Base
{
    // 子類別可 override OnMyEvent 擴充行為
    protected override void OnMyEvent(MyEventArgs e)
    {
        // 子類別邏輯...
        base.OnMyEvent(e); // 呼叫基類觸發
    }
}

4. 非同步事件觸發


// 避免單一訂閱者阻塞其他訂閱者
protected virtual async Task OnEventAsync(MyEventArgs e)
{
    var handlers = MyEvent?.GetInvocationList();
    if (handlers == null) return;
    
    var tasks = handlers
        .Cast>()
        .Select(h => Task.Run(() => h(this, e)));
    
    await Task.WhenAll(tasks);
}

記憶體洩漏陷阱與解決

問題:訂閱者未移除導致無法被 GC 回收


public class Publisher
{
    public event EventHandler MyEvent;
}

public class Subscriber
{
    public Subscriber(Publisher p)
    {
        p.MyEvent += OnEvent; // Publisher 持有 Subscriber 參考
    }
    void OnEvent(object s, EventArgs e) { }
}

// 使用後若不 p.MyEvent -= subscriber.OnEvent,Subscriber 永不被回收

解決方案

  1. 明確移除:實作 IDisposable,在 Dispose 中 -=
  2. 弱事件模式:使用 WeakReference 或 WeakEventManager (WPF)
  3. 靜態事件謹慎用:改用非靜態或手動管理生命週期

// 弱事件包裝器簡易實作
public class WeakEvent where TEventArgs : EventArgs
{
    private readonly List>> _handlers = new();
    
    public void Add(EventHandler handler)
    {
        _handlers.Add(new WeakReference>(handler));
    }
    
    public void Remove(EventHandler handler)
    {
        _handlers.RemoveAll(wr => !wr.TryGetTarget(out var h) || h == handler);
    }
    
    public void Raise(object sender, TEventArgs e)
    {
        _handlers.RemoveAll(wr => !wr.TryGetTarget(out _)); // 清理已死參考
        foreach (var wr in _handlers)
        {
            if (wr.TryGetTarget(out var handler))
                handler(sender, e);
        }
    }
}

效能優化建議

場景建議
大量事件、少數訂閱使用 EventHandlerList (屬性型事件)
高頻觸發 (如滑鼠移動)節流/去抖、合併事件引數、避免配置物件
跨執行緒觸發使用 SynchronizationContext 或 Task.Run 切換上下文
熱路徑考慮 ValueTask、避免委派鏈過長

現代替代方案比較

模式優點缺點適用場景
C# 事件語言原生、型別安全、IDE 支援好強參考易洩漏、靜態難管理UI、元件內部通知
System.Reactive (Rx.NET)可組合、LINQ 查詢、背壓控制學習曲線陡、依賴外部套件複雜事件流、串流處理
MediatR / 領域事件解耦、單一職責、測試友善間接層增加、除錯較難DDD、微服務、CQRS
Channel / Dataflow高效能、生產消費者、非同步原生非事件語意、需手動管理高併發管道、背景處理

總結

事件本質是 受限的多播委派,透過 add/remove 存取器保護發布者的觸發控制權。掌握 標準模式、記憶體管理、進階用法,能設計出健壯、可維護的事件驅動架構。現代開發中,視複雜度選擇:簡單場景用原生事件,複雜流程用 Rx.NET/MediatR,高效能管道用 Channel。核心原則:發布者決定何時觸發,訂閱者決定如何回應,雙方鬆耦合。

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