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

C# 委派與事件深度解析:從泡沫排序回呼到觀察者模式的演進

     編輯:關於C語言

前言

委派與事件是 C# 實作「觀察者模式」與「策略模式」的核心語言機制。本文以經典《C# 高級編程》範例——泡沫排序委派回呼——為切入點,層層深入至事件標準模式、EventHandler<T>、弱事件、非同步事件、來源產生器與現代化最佳實務。

一、核心範例重構:策略模式與委派

原始碼結構


// 委派定義:比較兩個物件,回傳 true 表示右邊大於左邊
public delegate bool CompareOp(object lhs, object rhs);

// 策略實作:依薪資比較 Employee
public class Employee
{
    public string Name { get; }
    public decimal Salary { get; }
    public Employee(string name, decimal salary) => (Name, Salary) = (name, salary);
    public override string ToString() => $"{Name}, {Salary:C}";

    // 靜態方法符合 CompareOp 簽名
    public static bool RhsIsGreater(object lhs, object rhs)
        => ((Employee)rhs).Salary > ((Employee)lhs).Salary;
}

// 演算法類別:接受委派作為比較策略
public static class BubbleSorter
{
    public static void Sort(object[] array, CompareOp comparer)
    {
        for (int i = 0; i < array.Length; i++)
            for (int j = i + 1; j < array.Length; j++)
                if (comparer(array[j], array[i])) // 委派呼叫
                    (array[i], array[j]) = (array[j], array[i]); // 交換
    }
}

// 使用
var employees = new object[] 
{ 
    new Employee("Karli", 20000), 
    new Employee("Bill", 10000), 
    new Employee("Simon", 25000) 
};

CompareOp cmp = new CompareOp(Employee.RhsIsGreater); // 建立委派實例
BubbleSorter.Sort(employees, cmp); // 注入策略

關鍵觀察

  • 委派是「方法的類型安全指標」:封裝 方法指標 + 目標物件 (this)。
  • 多播委派:cmp += AnotherMethod 可串聯多個方法,依序呼叫。
  • 協變/逆變 (C# 4+):Func<in T, out TResult> 允許衍生類別替換基底類別。

二、從委派到事件:標準事件模式


public class TemperatureMonitor
{
    // 1. 定義事件資料類別 (繼承 EventArgs)
    public class TemperatureChangedEventArgs : EventArgs
    {
        public decimal Temperature { get; }
        public DateTime Timestamp { get; }
        public TemperatureChangedEventArgs(decimal temp) 
            => (Temperature, Timestamp) = (temp, DateTime.UtcNow);
    }

    // 2. 定義委派類型 (慣用 EventHandler)
    public event EventHandler TemperatureChanged;

    // 3. 受保護虛擬方法觸發事件 (供衍生類別覆寫)
    protected virtual void OnTemperatureChanged(TemperatureChangedEventArgs e)
    {
        // 執行緒安全:複製委派參考再檢查 null
        TemperatureChanged?.Invoke(this, e);
    }

    // 4. 業務邏輯
    private decimal _currentTemp;
    public decimal CurrentTemp
    {
        get => _currentTemp;
        set
        {
            if (_currentTemp != value)
            {
                _currentTemp = value;
                OnTemperatureChanged(new TemperatureChangedEventArgs(value));
            }
        }
    }
}

// 訂閱者
var monitor = new TemperatureMonitor();
monitor.TemperatureChanged += (sender, e) => 
    Console.WriteLine($"[{e.Timestamp:O}] Temp: {e.Temperature}°C");

monitor.CurrentTemp = 25.5m; // 觸發事件

三、事件進階主題

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


// 大量事件時用 EventHandlerList 儲存 (WinForms 元件模式)
public class ComponentBase
{
    private readonly EventHandlerList _events = new();
    private static readonly object _clickKey = new();

    public event EventHandler Click
    {
        add => _events.AddHandler(_clickKey, value);
        remove => _events.RemoveHandler(_clickKey, value);
    }

    protected void OnClick(EventArgs e) 
        => (_events[_clickKey] as EventHandler)?.Invoke(this, e);
}

2. 弱事件模式:避免訂閱者無法被 GC


// .NET 4.5+ WeakEventManager (WPF 風格)
public class WeakTemperatureMonitor
{
    public event EventHandler TemperatureChanged
    {
        add => WeakEventManager
            .AddHandler(this, "TemperatureChanged", value);
        remove => WeakEventManager
            .RemoveHandler(this, "TemperatureChanged", value);
    }
    // ... 觸發邏輯同前
}

// 或自訂 WeakDelegate (通用)
public static class WeakEvent
{
    public static void Subscribe(object source, string eventName, 
        EventHandler handler) where TEventArgs : EventArgs
    {
        var weakRef = new WeakReference(handler);
        EventHandler proxy = (s, e) =>
        {
            if (weakRef.Target is EventHandler h) h(s, e);
            else WeakEventManager.Detach(source, eventName, proxy);
        };
        // 使用反射或來源產生器掛鉤 proxy
    }
}

3. 非同步事件處理


// 問題:同步事件阻塞發布者,異步事件例外處理困難
// 方案 A:發布者火後不顧
public async Task RaiseAsync(TemperatureChangedEventArgs e)
{
    var tasks = TemperatureChanged?.GetInvocationList()
        .Cast>()
        .Select(h => Task.Run(() => h(this, e))) ?? Enumerable.Empty();
    await Task.WhenAll(tasks);
}

// 方案 B:Channel / MediatR / Event Bus (架構層級解耦)

四、現代化語法糖與來源產生器

1. Func/Action 取代自訂委派


// 泡沫排序現代寫法
public static void Sort(T[] array, Func comparer)
{
    for (int i = 0; i < array.Length; i++)
        for (int j = i + 1; j < array.Length; j++)
            if (comparer(array[j], array[i]))
                (array[i], array[j]) = (array[j], array[i]);
}

// 使用
BubbleSorter.Sort(employees, (a, b) => a.Salary > b.Salary);

2. 來源產生器自動實作事件樣板


// 套件: Microsoft.CodeAnalysis.CSharp.SourceGenerators
// 定義 partial 類別與屬性
[GenerateEventArgs]
public partial class OrderService
{
    public event EventHandler OrderCreated;
    
    // 產生器自動建立 OnOrderCreated、EventArgs 類別等
}

五、常見錯誤與最佳實務

錯誤後果修正
直接呼叫 Event(args) 不檢查 null無訂閱時拋 NullReferenceExceptionEvent?.Invoke(this, args)
在建構函式訂閱靜態事件,未取消訂閱記憶體洩漏 (靜態事件持有實例參考)實作 IDisposable、WeakEvent
事件處理器拋例外中斷後續處理器部分訂閱者未執行每個處理器包 try/catch、或用 GetInvocationList 逐一 try
在迴圈中用 lambda 訂閱事件捕捉迴圈變數所有處理器共用同一變數 (閉包陷阱)區域變數複製: var i = index; ev += (s,e)=>Use(i);
事件資料類別可變訂閱者竄改資料影響後續處理器EventArgs 設計為不可變 (init-only/setter 私有)

六、總結:委派/事件演進路線圖

  1. C# 1.0:delegate 關鍵字、多播委派、事件關鍵字。
  2. C# 2.0:匿名方法、EventHandler<T>、協變/逆變雛形。
  3. C# 3.0:Lambda 運算式、Func/Action 通用委派。
  4. C# 4.0:泛型協變/逆變 (in/out)、動態繫結。
  5. C# 6.0:?.Invoke null 條件運算子。
  6. C# 7.0+:模式比對、ref 回傳、區域函式。
  7. C# 9.0+:record EventArgs、init only 屬性、來源產生器。
  8. 現代架構:MediatR、Event Bus、Channels、Reactive Extensions (Rx.NET) 取代手工事件。

結語

從 CompareOp 委派注入排序策略,到 EventHandler<T> 標準化觀察者模式,再到 Func<T,T,bool> 函數式風格與架構級事件總線,委派與事件見證了 C# 從物件導向走向函數式、從單體走向微服務的演進。掌握其類型安全、多播語意、生命週期管理、執行緒同步四大核心,才能在任何架構層級駕馭「反轉控制」的強大威力。

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