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

深入理解 .NET 事件機制 — 從 Java 監聽器到 delegate、event、多播與事件順序

     編輯:關於.NET

前言

.NET 事件 (event) 是觀察者模式的語言級實作,但許多開發者對 delegate、event、多播、sender/EventArgs 的關係感到困惑。本文從 Java 事件監聽器 對比切入,層層遞進重構出 .NET 事件完整模型,並釐清多播調用順序、執行緒安全、自訂 EventArgs 等實戰細節。

第 1 步:Java 風格 — 介面回呼 (Interface Callback)

Java 無 delegate,事件監聽器即介面:

// Java 風格 (C# 模擬)
interface ActionListener { void ActionPerformed(); }

class MyButton
{
    private ActionListener _listener;
    public void SetActionListener(ActionListener l) => _listener = l;
    public void ClickMe() => _listener?.ActionPerformed();
}

// 使用:建立實作類別、實例化、註冊
class MyListener : ActionListener 
{ 
    public void ActionPerformed() => Console.WriteLine("Button Clicked!"); 
}

var btn = new MyButton();
btn.SetActionListener(new MyListener());
btn.ClickMe();
缺點:每個事件需定義一介面、一實作類別,樣板代碼極多。

第 2 步:引入 delegate — 以方法簽章為型別

delegate 是「方法簽章的型別安全封裝」,可視為「函數指標」的物件導向版:

delegate void OnClick(); // 定義無參無回傳的方法簽章型別

class MyButton
{
    private OnClick _click; // 委派欄位
    public void SetOnClick(OnClick oc) => _click = oc;
    public void ClickMe() => _click?.Invoke();
}

// 使用:匿名方法 / Lambda 直接傳遞
var btn = new MyButton();
btn.SetOnClick(() => Console.WriteLine("Button Clicked!"));
btn.ClickMe();
優點:無需額外介面/類別,內嵌邏輯即可註冊。

第 3 步:多播 — 一個事件、多個處理器

Delegate 本身支援鏈結 (+/-):

class MyButton
{
    private List _clicks = new List();
    public void AddOnClick(OnClick oc) => _clicks.Add(oc);
    public void ClickMe() => _clicks.ForEach(c => c?.Invoke());
}

// 註冊多個
btn.AddOnClick(() => Console.WriteLine("First"));
btn.AddOnClick(() => Console.WriteLine("Second"));
btn.ClickMe();
// 輸出:First → Second (註冊順序 = 執行順序)

第 4 步:event 關鍵字 — 封裝與安全

event 限制外部只能 +=/-=,不能直接 = 覆寫或 Invoke():

class MyButton
{
    public event OnClick Click; // 編譯器自動產生 add_Click/remove_Click + 私有委派欄位
    
    public void ClickMe() => Click?.Invoke(this, EventArgs.Empty); // 標準模式傳 sender
}

var btn = new MyButton();
btn.Click += (s, e) => Console.WriteLine("First");
btn.Click += (s, e) => Console.WriteLine("Second");
btn.ClickMe();
event vs delegate 欄位:event 禁止外部 `Click = null` 或 `Click()`,保護封裝性。

第 5 步:標準事件模式 — sender + EventArgs

.NET Framework 約定:事件處理器簽章為 void Handler(object sender, TEventArgs e)

// 1. 自訂 EventArgs 攜帶資料
class MyClickEventArgs : EventArgs
{
    public string Message { get; set; }
    public DateTime ClickTime { get; } = DateTime.Now;
}

// 2. 定義委派型別 (慣用 EventHandler 泛型)
delegate void ClickEventHandler(object sender, MyClickEventArgs e);

// 3. 發布者
class MyButton
{
    public event ClickEventHandler Click;
    public string Text { get; set; }
    
    public void ClickMe()
    {
        var args = new MyClickEventArgs { Message = "User clicked!" };
        Click?.Invoke(this, args); // this = sender
    }
}

// 4. 訂閱者
var btn = new MyButton { Text = "OK" };
btn.Click += (sender, e) => 
{
    var b = (MyButton)sender;
    Console.WriteLine($"{b.Text}: {e.Message} at {e.ClickTime}");
};
btn.ClickMe();

第 6 步:事件順序 — Before / On / After 模式

WinForms 控制項常見 MouseDown → Click → MouseUp 順序,實作方式為 在同一方法中依序 Invoke 多個事件:

class MyButton
{
    public event ClickEventHandler BeforeClick;
    public event ClickEventHandler Click;
    public event ClickEventHandler AfterClick;
    
    public void ClickMe()
    {
        var args = new MyClickEventArgs();
        
        args.Message = "Before Click"; BeforeClick?.Invoke(this, args);
        args.Message = "On Click";     Click?.Invoke(this, args);
        args.Message = "After Click";  AfterClick?.Invoke(this, args);
    }
}

// 共用單一處理器
void Handle(object s, MyClickEventArgs e) 
    => Console.WriteLine($"{e.Message}");

btn.BeforeClick += Handle;
btn.Click += Handle;
btn.AfterClick += Handle;
btn.ClickMe();
// 輸出順序固定:Before → On → After
每次新建 EventArgs:避免多執行緒下共用物件導致資料競爭;語意上每次事件參數應獨立。

實戰進階主題

1. 標準 EventHandler<TEventArgs> 泛型委派

// 無需自訂 delegate,直接使用框架內建
public event EventHandler Click;
// 等同於:delegate void EventHandler(object sender, T e);

2. 事件存取器 — 自訂 add/remove 邏輯

private EventHandler _click;
public event EventHandler Click
{
    add 
    { 
        lock(_sync) _click += value; // 執行緒安全註冊
        // 可加入日誌、驗證、弱引用包裝等
    }
    remove => lock(_sync) _click -= value;
}

3. 弱引用事件 — 避免記憶體洩漏

訂閱者若未明確 `-=`,發布者持有強引用導致無法 GC。可用 WeakReference 包裝委派:

// 簡易 WeakEventManager 概念
class WeakEvent where T : class
{
    private List _refs = new List();
    public void Add(T handler) => _refs.Add(new WeakReference(handler));
    public void Remove(T handler) => _refs.RemoveAll(r => !r.IsAlive || r.Target == handler);
    public void Raise(object sender, EventArgs e)
    {
        foreach (var r in _refs.Where(r => r.IsAlive))
            (r.Target as Delegate)?.DynamicInvoke(sender, e);
    }
}

4. 非同步事件 — 避免阻塞發布者

public async void ClickMeAsync()
{
    var args = new MyClickEventArgs();
    var tasks = Click?.GetInvocationList()
        .Cast>()
        .Select(d => d(this, args)) ?? Enumerable.Empty();
    await Task.WhenAll(tasks); // 等待所有非同步處理器完成
}

常見誤區與最佳實務

誤區正確做法
直接用 public delegate 欄位改用 event 封裝,防止外部覆寫/Invoke
EventArgs 重複使用 (修改屬性後再次 Invoke)每次 Invoke 新建實例,保證執行緒安全與語意正確
同步事件中執行耗時 I/O改非同步事件或背景工作,避免阻塞 UI/發布者
未實作 IDisposable 移除訂閱Dispose 時 `-=` 所有事件,或用弱引用
靜態事件導致單例洩漏謹慎使用靜態事件,必須提供明確清理機制

總結:.NET 事件完整圖譜

delegate (方法簽章型別)
    ↓ 封裝為
event (add/remove 存取器 + 私有委派欄位)
    ↓ 多播鏈結
InvocationList (Delegate[])
    ↓ 依序調用
handler1(sender, args) → handler2(sender, args) → ...
    ↓ 標準模式
sender (object) + EventArgs (資料承載)
    ↓ 擴展
自訂 EventArgs / 弱引用 / 非同步 / 自訂存取器
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved