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

C# 委派與事件深度解析:從函式指標到觀察者模式

     編輯:關於C語言

委派是 C# 型別安全的「函式指標」,事件則是委派的受限封裝,兩者構成 .NET 觀察者模式的基石。本文由 C/C++ 函式指標類比切入,層層深入至委派多播、事件封裝、標準事件模式與最佳實務,並附完整可執行範例。

1. 歷史脈絡:C 函式指標 → C# 委派

// C 風格函式指標
void square(int x) { printf("square: %d\n", x*x); }
void cube(int x)   { printf("cube: %d\n", x*x*x); }

int main() {
    void (*calc)(int) = square; // 指向 square
    calc(5);                    // 輸出 25
    calc = cube;                // 改指向 cube
    calc(5);                    // 輸出 125
    return 0;
}

C# 為了型別安全與記憶體管理,不允許直接操作指標,改以 委派型別 封裝方法簽章:

delegate void MathOp(int x);   // 定義委派型別

static void Square(int x) => Console.WriteLine($"Square: {x*x}");
static void Cube(int x)   => Console.WriteLine($"Cube: {x*x*x}");

static void Main()
{
    MathOp op = Square;        // 實例化委派
    op(5);                     // 呼叫 Square
    op += Cube;                // 多播:加入 Cube
    op(5);                     // 依序呼叫 Square → Cube
    op -= Square;              // 移除 Square
    op(5);                     // 僅呼叫 Cube
}

2. 委派本質:編譯器產生的密封類別

delegate void MathOp(int x); 編譯後等價於:

public sealed class MathOp : MulticastDelegate
{
    public MathOp(object target, IntPtr methodPtr);
    public virtual void Invoke(int x);
    public virtual IAsyncResult BeginInvoke(int x, AsyncCallback cb, object state);
    public virtual void EndInvoke(IAsyncResult result);
}
  • 繼承 MulticastDelegate → 天然支援多播(呼叫清單)。
  • Invoke 同步呼叫;BeginInvoke/EndInvoke 非同步模式(APM)。
  • 委派實例為不可變(Immutable),+=/-= 實際產生新實例。

3. 封裝演進:public delegate → private delegate + event

階段 1:公開委派欄位(破壞封裝)

public class Calculator
{
    public MathOp OnCalculate; // 外部可任意指派、清空、直接呼叫
}

階段 2:引入 event 關鍵字(標準封裝)

public class Calculator
{
    // 編譯器自動產生:
    // private MathOp _onCalculate;
    // public event MathOp OnCalculate { add { _onCalculate += value; } remove { _onCalculate -= value; } }
    public event MathOp OnCalculate;

    public void Calculate(int x) => OnCalculate?.Invoke(x); // 安全觸發
}
  • event 限制外部僅能 +=/-=,不可直接指派 = 或呼叫 Invoke。
  • 內部仍可透過 OnCalculate?.Invoke(...) 觸發。

4. 標準 .NET 事件模式

框架規範建議使用 EventHandler<TEventArgs> 與 EventArgs 衍生類別:

// 事件參數
public class CalcEventArgs : EventArgs
{
    public int Input { get; }
    public int Result { get; set; }
    public CalcEventArgs(int input) => Input = input;
}

// 發送者
public class Calculator
{
    public event EventHandler Calculated;

    public int Compute(int x)
    {
        var args = new CalcEventArgs(x);
        args.Result = x * x; // 範例:平方
        OnCalculated(args);
        return args.Result;
    }

    protected virtual void OnCalculated(CalcEventArgs e)
        => Calculated?.Invoke(this, e); // 傳遞 this 為 sender
}

// 訂閱者
static void Main()
{
    var calc = new Calculator();
    calc.Calculated += (s, e) => Console.WriteLine($"計算完成:{e.Input}² = {e.Result}");
    calc.Compute(10);
}

5. 進階主題

5.1 委派與介面的選擇

場景建議
單一方法回呼、輕量、需多播委派
多組相關方法、需強型別契約、DI 容器管理介面
事件通知、符合 .NET 標準模式event + EventHandler<T>

5.2 非同步事件觸發

public async Task RaiseAsync(EventArgs e)
{
    var tasks = Calculated?.GetInvocationList()
        .Cast>()
        .Select(h => Task.Run(() => h(this, e)));
    if (tasks != null) await Task.WhenAll(tasks);
}

⚠ 注意:事件處理器中若有 UI 親和性操作,需回主執行緒(Dispatcher.Invoke/Control.Invoke)。

5.3 記憶體洩漏防範

  • 長生命週期物件持有短生命週期物件的事件處理器 → 導致無法 GC。
  • 解法:WeakEventManager (WPF)、WeakReference 自訂、或於 Dispose/Unload 時明確 -=。

6. 完整可執行範例

using System;

delegate void MathOp(int x); // 1. 定義委派型別

class Calculator
{
    public event MathOp Calculated; // 2. 宣告事件

    public void Compute(int x)
    {
        int result = x * x;
        Console.WriteLine($"[Calculator] 計算 {x}² = {result}");
        OnCalculated(result); // 3. 觸發事件
    }

    protected virtual void OnCalculated(int result)
        => Calculated?.Invoke(result); // 安全觸發
}

class Program
{
    static void Main()
    {
        var calc = new Calculator();
        
        // 4. 訂閱多個處理器
        calc.Calculated += r => Console.WriteLine($"  → 記錄器:結果 {result}");
        calc.Calculated += r => Console.WriteLine($"  → 通知器:已完成");
        
        calc.Compute(5);
        calc.Compute(10);
        
        // 5. 取消訂閱
        calc.Calculated -= calc.Calculated.GetInvocationList()[0] as MathOp;
        calc.Compute(3);
    }
}

結語

委派提供「方法即資料」的頭等公民地位,事件在其之上加上發布/訂閱語意與封裝保護。掌握「委派多播機制、事件標準模式、非同步觸發與弱參考」四大面向,即能在 C# 中靈活實作觀察者模式、回呼管線與領域事件,寫出低耦合、高可測試的程式碼。

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