CodeGym /課程 /C# SELF /發布者-訂閱者 範式 和 C# 事件 ( event

發布者-訂閱者 範式 和 C# 事件 ( event)

C# SELF
等級 52 , 課堂 0
開放

1. 介紹

在寫程式時,你會不可避免碰到某個部分需要「通知」其他部分某件重要事情發生了的情況。經典例子 — 使用者點擊滑鼠,這個事件需要被處理。在日常生活中我們早就生活在事件的世界:水壺開始吹口哨 — 你聽到信號就趕快去關火。咖啡灑到鍵盤 — 心臟一驟 — 你立刻衝去救筆電。程式設計也遵循相同的規則。

事件 — 是一個機制,允許來源物件(發布者)去通知其他物件(訂閱者)某些變更或動作已發生。可以把它理解為「我發出通知 — 有聽到的人就回應」。

在 C# 裡,事件是基於 delegate 的特殊語法結構。delegate 定義了回調的簽名 — 訂閱者會以什麼樣的方式被呼叫。事件的宣告使用關鍵字 event,delegate 使用關鍵字 delegate

為什麼需要事件?

  • 鬆耦合:發布者不需要知道訂閱者的存在 — 只發出信號。
  • 架構彈性:可以在不改變發布者代碼的情況下動態增加或移除處理器。
  • 可擴展性:加入新的訂閱者後,馬上就能收到通知。

經典的「發布者-訂閱者」範式

想像有一個類別「火警警報器」(發布者)和一個類別「建築裡的人」(訂閱者)。當警報器觸發時,它會同時向所有人發出信號 — 不管有多少人或他們在哪裡。這就是發布者-訂閱者(或 Observer)模式。

發布者不知道有多少或是哪一些訂閱者 — 它只是發出通知,其他人自行決定要訂閱還是忽略。

在 C# 中它如何工作?

  • 發布者:定義事件(event),提供訂閱/退訂機制。
  • 訂閱者:訂閱事件並實作處理器(當事件發生時,處理器會被呼叫)。

2. 事件實作示範:第一個例子

從概念轉到程式碼,我們來描述一個最簡模型。假設我們有一個 console 應用,裡面有個 timer 每秒「滴答」,不同的處理器會對此做出反應(例如在 console 輸出「滴」或統計滴答數)。

步驟 1. 定義 delegate 和 事件


public class SimpleTimer
{
    // 宣告事件用的 delegate
    public delegate void TickEventHandler(object sender, EventArgs e);

    // 基於 delegate 的事件
    public event TickEventHandler Tick;

    public void Start(int count)
    {
        for (int i = 0; i < count; i++)
        {
            System.Threading.Thread.Sleep(1000); // 模擬一次滴答!
            OnTick(); // 觸發事件!
        }
    }

    protected virtual void OnTick()
    {
        // 如果有訂閱者就呼叫事件 (Tick != null)
        Tick?.Invoke(this, EventArgs.Empty);
    }
}

這段在做什麼?

  • 定義了 delegate TickEventHandler,有典型簽名 object sender, EventArgs e
  • 事件 Tick — 作為處理器的訂閱點。
  • Start 方法模擬「滴答」,定時呼叫 OnTick
  • OnTick 中以安全方式呼叫事件:Tick?.Invoke(..., EventArgs.Empty)。

步驟 2. 訂閱事件


class Program
{
    static void Main()
    {
        var timer = new SimpleTimer();

        // 訂閱 Tick 事件
        timer.Tick += Timer_Tick;

        timer.Start(3);

        // 需要的話可以退訂
        timer.Tick -= Timer_Tick;
    }

    static void Timer_Tick(object sender, EventArgs e)
    {
        Console.WriteLine("滴!");
    }
}

我們建立一個 timer,透過 += 運算子訂閱,之後每次滴答都會呼叫處理器。退訂使用 -=

3. 有用的細節

為什麼事件比「硬編碼」呼叫更好?

如果 SimpleTimerOnTick 直接把東西寫到 console,這個類別就會跟具體動作緊密耦合。事件讓程式更自由:timer 不知道訂閱者會做什麼——發射火箭、寫日誌到檔案或發送 e-mail 都有可能。

事件和 delegate 的重要區別

  • delegate 是指向方法的「指標」,而 事件 則是在 delegate 基礎上的存取限制封裝。
  • 訂閱者只能訂閱/退訂;外部不能直接觸發事件 — 只有發布者可以。
  • 要宣告事件,在 delegate 類型前加上修飾詞 event — 編譯器會處理正確的存取模型。

C# 事件的簡要工作流程


+------------------+          +------------------------------+
|                  |          |                              |
|     發布者       | <------> |         訂閱者               |
| (Publisher/Event)|          |      (Subscriber/Handler)    |
|                  |          |                              |
+------------------+          +------------------------------+
         | 1) 宣告事件               | 2) 訂閱該事件
         | 3) 觸發事件               | 4) 實作處理器

什麼時候使用事件?

  • 需要通知不定數量的聽眾時。
  • 不想在來源類別內綁定具體行為時。
  • UI、非同步互動、系統通知 — 到處都是事件的身影。

在 C# 中實作事件的主要特性

  • 外部不能直接觸發事件:只有發布者的程式碼能呼叫 Invoke
  • 訂閱/退訂:使用 +=/-=;可有多個處理器。
  • 事件本質上是一個 delegate 列表:事件發生時,會依照訂閱順序呼叫所有處理器。
  • 推薦的 delegate:使用 EventHandlerEventHandler<TEventArgs> 以便與 .NET 生態系統相容。

4. 新手常見錯誤

忘記檢查是否有訂閱者:Tick != null。更好的做法是使用安全呼叫:Tick?.Invoke(...)。

訂閱了事件但在處理器不需要時沒有退訂。這可能會導致物件被持有在記憶體中而造成 memory leak。

嘗試從外部類別去「呼叫」事件 — 編譯器不允許。不能像呼叫方法那樣寫 game.GameOver(),如果那是事件而不是方法。

沒有遵守 delegate 的簽名。對於事件,建議使用標準的 EventHandlerEventHandler<TEventArgs> — 這樣程式碼能和其它 .NET 函式庫互通。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION