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

C# 委派與事件深度解析:從單播到多播、事件模式與實戰應用

     編輯:關於C語言

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

委派是 C# 的核心機制,封裝方法參考,支援型別安全、多播、非同步呼叫。類似 C++ 函式指標,但更安全、更強大。

委派宣告與基本用法

// 宣告:無參數、回傳 string
public delegate string GreetingDelegate();

// 目標方法
public class Greeter
{
    public string SayHello() => "Hello, World!";
    public string SayGoodbye() => "Goodbye!";
}

// 使用
var greeter = new Greeter();
GreetingDelegate d1 = greeter.SayHello;
GreetingDelegate d2 = greeter.SayGoodbye;

Console.WriteLine(d1()); // Hello, World!
Console.WriteLine(d2()); // Goodbye!

多播委派:鏈結多個方法

委派可透過 `+` / `-` 運算子組合/移除,形成調用清單:

public delegate void NotifyDelegate(string message);

public class Notifier
{
    public void Email(string msg) => Console.WriteLine($"Email: {msg}");
    public void Sms(string msg) => Console.WriteLine($"SMS: {msg}");
    public void Push(string msg) => Console.WriteLine($"Push: {msg}");
}

var notifier = new Notifier();
NotifyDelegate multicast = null;

multicast += notifier.Email;
multicast += notifier.Sms;
multicast += notifier.Push;

// 呼叫一次,依序執行三個方法
multicast("系統維護通知");
// 輸出:
// Email: 系統維護通知
// SMS: 系統維護通知
// Push: 系統維護通知

// 移除某個方法
multicast -= notifier.Sms;
multicast("第二次通知");
// 僅執行 Email 與 Push

多播關鍵行為

  • 回傳值:只有最後一個方法的回傳值被保留(前面的被忽略)
  • 例外:若調用清單中某方法拋出例外,後續方法不會執行,例外向上冒泡
  • 執行緒安全:委派本身不可變,`+=`/`-=` 會產生新實例,但多執行緒同時修改需自行同步

事件:基於委派的發布-訂閱模式

事件提供封裝層,限制外部只能 `+=`/`-=` 訂閱/取消訂閱,不能直接呼叫或清空調用清單。

// 1. 定義事件委派型別(慣用 EventHandler 模式)
public delegate void PriceChangedEventHandler(decimal oldPrice, decimal newPrice);

public class Stock
{
    private decimal _price;
    
    // 2. 宣告事件
    public event PriceChangedEventHandler PriceChanged;

    public decimal Price
    {
        get => _price;
        set
        {
            if (_price != value)
            {
                decimal old = _price;
                _price = value;
                // 3. 觸發事件(檢查 null,避免無訂閱者時擲出 NullReferenceException)
                PriceChanged?.Invoke(old, value);
            }
        }
    }
}

// 4. 訂閱與處理
var stock = new Stock();
stock.PriceChanged += (old, @new) => 
    Console.WriteLine($"價格變動: {old:C} → {@new:C}");

stock.Price = 100.50m; // 觸發事件
stock.Price = 101.20m; // 再次觸發

標準事件模式:EventHandler

.NET 標準化事件簽名:void EventHandler(object sender, TEventArgs e)

// 自訂事件參數
public class PriceChangedEventArgs : EventArgs
{
    public decimal OldPrice { get; }
    public decimal NewPrice { get; }
    public DateTime ChangeTime { get; }

    public PriceChangedEventArgs(decimal oldPrice, decimal newPrice)
    {
        OldPrice = oldPrice;
        NewPrice = newPrice;
        ChangeTime = DateTime.UtcNow;
    }
}

// 發布者
public class StockV2
{
    private decimal _price;
    public event EventHandler PriceChanged;

    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);
    }
}

// 訂閱者
var stock = new StockV2();
stock.PriceChanged += (sender, e) =>
{
    Console.WriteLine($"[{e.ChangeTime:HH:mm:ss}] {((StockV2)sender).GetType().Name}: {e.OldPrice:C} → {e.NewPrice:C}");
};

進階主題

1. 事件存取器:自訂 Add/Remove 邏輯

private EventHandler _priceChanged;

public event EventHandler PriceChanged
{
    add
    {
        lock (_lock) // 執行緒安全
        {
            _priceChanged += value;
        }
    }
    remove
    {
        lock (_lock)
        {
            _priceChanged -= value;
        }
    }
}

2. 弱事件模式:防止記憶體洩漏

訂閱者若未取消訂閱,發布者持有強參考導致無法 GC。解決方案:

  • WeakReference + 自訂事件存取器
  • WPF WeakEventManager
  • 第三方庫(如 Microsoft.Toolkit.HighPerformance.WeakEventListener)

3. 非同步事件呼叫

// 同步呼叫(預設):逐一執行,阻塞發布者
PriceChanged?.Invoke(this, e);

// 非同步並行呼叫(需自行實作)
foreach (var handler in PriceChanged?.GetInvocationList() ?? Array.Empty())
{
    Task.Run(() => handler.DynamicInvoke(this, e));
}
// 注意:例外處理、順序保證、完成等待需自行設計

實戰設計模式

觀察者模式

public interface IObserver
{
    void OnNext(T value);
    void OnError(Exception error);
    void OnCompleted();
}

public class Subject
{
    private readonly List> _observers = new();
    private readonly object _lock = new();

    public IDisposable Subscribe(IObserver observer)
    {
        lock (_lock) _observers.Add(observer);
        return new Unsubscriber(_observers, observer);
    }

    public void Notify(T value)
    {
        IObserver[] snapshot;
        lock (_lock) snapshot = _observers.ToArray();
        foreach (var obs in snapshot) obs.OnNext(value);
    }

    private class Unsubscriber : IDisposable
    {
        private readonly List> _list;
        private readonly IObserver _observer;
        public Unsubscriber(List> list, IObserver observer) { _list = list; _observer = observer; }
        public void Dispose() { lock (_list) _list.Remove(_observer); }
    }
}

領域事件

public static class DomainEvents
{
    private static readonly List _handlers = new();
    private static readonly object _lock = new();

    public static void Register(Action handler) where T : IDomainEvent
    {
        lock (_lock) _handlers.Add(handler);
    }

    public static void Raise(T @event) where T : IDomainEvent
    {
        Delegate[] snapshot;
        lock (_lock) snapshot = _handlers.ToArray();
        foreach (var h in snapshot.OfType>()) h(@event);
    }
}

public interface IDomainEvent { }
public class OrderPlacedEvent : IDomainEvent { public int OrderId { get; set; } }

// 使用
DomainEvents.Register(e => SendConfirmationEmail(e.OrderId));
DomainEvents.Raise(new OrderPlacedEvent { OrderId = 123 });

常見陷阱與最佳實務

陷阱後果解決方案
事件未取消訂閱記憶體洩漏(發布者持有訂閱者參考)實作 IDisposable、使用弱事件、或在 Unload 時清理
多執行緒並發修改事件調用清單競態條件事件存取器加 lock、或用 ImmutableList
事件處理器拋出例外後續處理器不執行、發布者崩潰每個處理器內部 try/catch、或自訂事件存取器捕獲
在建構函式中訂閱靜態事件物件永遠無法被 GC(靜態事件存活到進程結束)避免靜態事件、或提供明確 Unregister 方法
委派回傳值被忽略邏輯錯誤(以為所有回傳值都會用到)多播委派勿依賴回傳值、改用 ref/out 參數或事件參數回傳

總結

委派是 C# 函式式程式設計的基石,事件則是建立鬆耦合、可擴充架構的關鍵。掌握多播行為、標準事件模式、執行緒安全、弱事件、非同步呼叫等進階技巧,並避開常見陷阱,即能設計出健壯、優雅的事件驅動系統。

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