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

C# 委派與事件深度解析:從基礎到觀察者模式實作

     編輯:ASP.NET 基礎

前言:跨越「委派/事件」這道門檻

委派與事件是 .NET 開發者必修的核心觀念,但許多初學者對其感到困惑:「委派到底是什麼?為什麼要把方法當參數傳?事件又是怎麼封裝委派的?」本文透過兩個循序漸進的範例——從簡單的多語系問候,到完整的熱水器觀察者模式——帶您徹底理解委派本質、事件封裝機制、IL 層面實作細節,以及如何套用至 Observer 設計模式與 .NET Framework 標準事件模型。

一、為什麼需要委派?

問題場景:硬編碼的問候邏輯

public void GreetPeople(string name) {
    EnglishGreeting(name); // 寫死呼叫英文版
}
public void EnglishGreeting(string name) => Console.WriteLine("Morning, " + name);

需求變更:支援中文問候。直覺做法加 enum + switch,但每新增一種語言就得修改枚舉與方法,違反開放封閉原則。

靈感:把方法當作參數傳遞

若 GreetPeople 能接受一個「代表問候方法」的參數,呼叫端決定用中文還是英文,擴充性瞬間解放。關鍵在於:這個參數的型別必須能描述「方法的簽名」——這就是委派的由來。

二、委派基礎語法與特性

宣告委派型別

// 定義:回傳 void、接受一個 string 參數的方法型別
public delegate void GreetingDelegate(string name);

編譯器會將其編譯為繼承 System.MulticastDelegate 的密封類別,擁有 Invoke、BeginInvoke/EndInvoke 等方法。

使用委派重構


public void GreetPeople(string name, GreetingDelegate makeGreeting) {
    makeGreeting(name); // 像呼叫一般方法一樣呼叫委派
}

// 呼叫端
GreetPeople("Jimmy", EnglishGreeting);
GreetPeople("張子陽", ChineseGreeting);

委派的多重繫結

委派可串聯多個方法(調用清單),以 += 累加、-= 移除:


GreetingDelegate d = EnglishGreeting;
d += ChineseGreeting;   // 現在 d 代表兩個方法
d("Jimmy");             // 依序輸出 Morning... / 早上好...
d -= EnglishGreeting;   // 移除英文版
d("張子陽");            // 僅輸出 早上好...

⚠️ 首次指派必須用 =(或 new Delegate(method)),直接用 += 會編譯錯誤「使用未指派的區域變數」。

三、事件的由來:封裝委派,防止外部濫用

直接暴露委派欄位的風險


public class GreetingManager {
    public GreetingDelegate OnGreet; // public 欄位
}

問題:

  1. 外部可用 = 直接覆寫,抹除先前訂閱者。
  2. 外部可直接呼叫 manager.OnGreet("X"),觸發不應被外部觸發的邏輯。
  3. 語意不清:= 是指派,+= 是訂閱,混用易誤。

event 關鍵字的封裝效果


public class GreetingManager {
    public event GreetingDelegate OnGreet; // 加上 event
    public void GreetPeople(string name) => OnGreet?.Invoke(name);
}

編譯器產生:

  • 私有委派欄位:private GreetingDelegate OnGreet;
  • add_OnGreet / remove_OnGreet 存取器:對應 +=/-=,內部呼叫 Delegate.Combine/Remove,並以 MethodImplOptions.Synchronized 保證執行緒安全。
  • 外部只能 += / -=,不能 = 或直接 Invoke。

這實現了「發布-訂閱」的語意邊界:類別內部決定何時觸發,外部只能訂閱/退訂。

四、觀察者模式實作:熱水器、警報器、顯示器

領域模型

  • Subject (被觀察者):Heater,擁有 temperature,燒水過程中溫度變化。
  • Observer (觀察者):Alarm、Display,關心溫度超過 95°C 時的通知。

鬆耦合實作


public class Heater {
    public delegate void BoiledHandler(int temp);
    public event BoiledHandler Boiled;

    public void BoilWater() {
        for (int i = 0; i <= 100; i++) {
            if (i > 95) Boiled?.Invoke(i); // 觸發事件
        }
    }
}

public class Alarm {
    public void MakeAlert(int temp) => Console.WriteLine($"Alarm: 水已 {temp} 度!");
}
public class Display {
    public static void ShowMsg(int temp) => Console.WriteLine($"Display: 當前 {temp} 度。");
}

// 組裝
var heater = new Heater();
var alarm = new Alarm();
heater.Boiled += alarm.MakeAlert;        // 實例方法
heater.Boiled += new Alarm().MakeAlert;  // 匿名實例
heater.Boiled += Display.ShowMsg;        // 靜態方法
heater.BoilWater();

五、.NET Framework 標準事件模式

為了統一性與擴充性(如傳遞 Sender 讓觀察者反查來源、傳遞 EventArgs 攜帶豐富資料),框架定義了慣例:

  • 委派命名以 EventHandler 結尾:BoiledEventHandler
  • 委派簽名:void Handler(object sender, EventArgs e)
  • 事件命名移除 EventHandler:Boiled
  • 自訂 EventArgs 類別攜帶資料,命名以 EventArgs 結尾。

標準化重構


public class Heater {
    public string Model = "RealFire 001";
    public string Area = "China Xian";

    public class BoiledEventArgs : EventArgs {
        public readonly int Temperature;
        public BoiledEventArgs(int t) => Temperature = t;
    }

    public delegate void BoiledEventHandler(object sender, BoiledEventArgs e);
    public event BoiledEventHandler Boiled;

    protected virtual void OnBoiled(BoiledEventArgs e) => Boiled?.Invoke(this, e);

    public void BoilWater() {
        for (int i = 0; i <= 100; i++) {
            if (i > 95) OnBoiled(new BoiledEventArgs(i));
        }
    }
}

public class Alarm {
    public void MakeAlert(object sender, Heater.BoiledEventArgs e) {
        var h = (Heater)sender;
        Console.WriteLine($"{h.Area} - {h.Model}: 水已 {e.Temperature} 度!");
    }
}

優勢:觀察者可透過 sender 存取完整 Heater 物件(型號、產地等),無需修改委派簽名即可擴充資料。

六、總結與最佳實務

觀念關鍵點
委派型別安全的方法參考,支援多重繫結,編譯為 MulticastDelegate 衍生類別
事件委派的封裝:外部只能 +=/-=,內部決定觸發時機,編譯產生 add/remove 存取器
觀察者模式Subject 定義事件,Observer 訂閱,達成鬆耦合;.NET 事件模型即是內建實作
標準模式sender + EventArgs,利於擴充、符合框架慣例、工具支援度高

實務建議

  • ✅ 公開事件時遵循 EventHandler<TEventArgs> 泛型委派(.NET 2.0+),減少自訂委派宣告。
  • ✅ 觸發事件前以 ?.Invoke 防禦性檢查,避免無訂閱者時拋出 NullReferenceException。
  • ✅ 事件引發方法標記 protected virtual(OnXxx),允許衍生類別覆寫或抑制事件。
  • ⚠️ 避免在事件處理器執行耗時工作,必要時啟動背景工作或使用 Task.Run。
  • ⚠️ 記憶體洩漏風險:長壽命物件訂閱短壽命物件事件時,務必在 Dispose 或 Unload 時 -= 移除。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved