C# 事件模式建立在 委派 (Delegate) 之上,實作「發布-訂閱」模式。事件提供封裝層,防止外部直接呼叫委派或清除訂閱清單,確保封裝性。
event 關鍵字宣告,限制存取權protected virtual void OnXxx(EventArgs e) 供衍生類別覆寫public class NameChangedEventArgs : EventArgs
{
public string OldName { get; }
public string NewName { get; }
public NameChangedEventArgs(string oldName, string newName)
{
OldName = oldName; NewName = newName;
}
}
public class Product
{
private string _name;
// 1. 委派型別 (慣用 EventHandler<TEventArgs>)
public event EventHandler<NameChangedEventArgs> NameChanged;
public string Name
{
get => _name;
set
{
if (_name == value) return; // 值未變更不引發
string old = _name;
_name = value;
OnNameChanged(new NameChangedEventArgs(old, value));
}
}
// 3. 引發方法 - 慣例命名 On + 事件名
protected virtual void OnNameChanged(NameChangedEventArgs e)
{
// 安全引發模式:複製委派參考避免執行緒競態
var handler = NameChanged;
handler?.Invoke(this, e);
}
}
public class OrderService
{
public void Subscribe(Product product)
{
// 使用 += 訂閱,方法簽章需符合 EventHandler<NameChangedEventArgs>
product.NameChanged += OnProductNameChanged;
}
public void Unsubscribe(Product product)
{
product.NameChanged -= OnProductNameChanged; // 必須使用相同方法參考
}
private void OnProductNameChanged(object sender, NameChangedEventArgs e)
{
var product = (Product)sender;
Console.WriteLine($"[訂單服務] 產品名稱變更: '{e.OldName}' → '{e.NewName}' (ID: {product.GetHashCode()})");
// 實際業務邏輯:更新訂單明細、發送通知、寫入稽核記錄...
}
}
var product = new Product { Name = "滑鼠" };
var orderSvc = new OrderService();
orderSvc.Subscribe(product);
product.Name = "無線滑鼠"; // 觸發事件
// 輸出: [訂單服務] 產品名稱變更: '滑鼠' → '無線滑鼠' (ID: 123456)
orderSvc.Unsubscribe(product);
product.Name = "藍牙滑鼠"; // 不再觸發
| 方式 | 適用場景 | 範例 |
|---|---|---|
EventHandler<TEventArgs> | 標準 .NET 事件模式,支援 sender、e,工具箱自動識別 | UI 控制項、領域事件 |
Action<T1, T2...> | 簡單回呼、內部事件、不需 sender | 內部狀態機、簡易通知 |
自訂 delegate void XxxHandler(...) | 需特定參數名稱、回傳值、ref/out 參數 | 需回傳取消旗標的驗證事件 |
// 標準模式 (C# 6+ null-conditional operator)
protected virtual void OnNameChanged(NameChangedEventArgs e)
{
NameChanged?.Invoke(this, e);
}
// 高併發場景:複製委派參考 (避免 Invoke 期間訂閱清單被修改)
protected virtual void OnNameChanged(NameChangedEventArgs e)
{
var handler = Volatile.Read(ref NameChanged); // 或直接 var handler = NameChanged;
handler?.Invoke(this, e);
}
// 預設編譯器產生私有委派欄位 + 鎖定同步
// 可自訂以節省記憶體 (大量事件物件) 或導向事件總線
private EventHandler<NameChangedEventArgs> _nameChanged;
public event EventHandler<NameChangedEventArgs> NameChanged
{
add
{
lock (_lock) _nameChanged += value; // 或 EventHandlerList
}
remove
{
lock (_lock) _nameChanged -= value;
}
}
解決訂閱者忘記取消訂閱導致記憶體洩漏 (發布者持有強參考)。WPF/UWP 內建 WeakEventManager,一般專案可用 WeakReference 封裝或引用 Microsoft.Toolkit.HighPerformance。
product.NameChanged(product, e) → 破壞封裝,改用 OnNameChanged protected virtual 供衍生類別擴充。product.NameChanged += (s,e) => ... → 無法用 -= 移除。需保存委派實例:var handler = new EventHandler<...>(OnChanged); product.NameChanged += handler;MouseMove) 應重用 EventArgs 實例或使用 struct 避免 GC 壓力。NameChanged、Completed),引發方法 OnNameChanged,引數類別 NameChangedEventArgs。?.Invoke 或複製參考模式,避免無訂閱者時拋 NullReferenceException。// 1. 引數類別
public class MyEventArgs : EventArgs { public string Data { get; set; } }
// 2. 發布者
public class Publisher
{
public event EventHandler<MyEventArgs> MyEvent;
protected virtual void OnMyEvent(MyEventArgs e) => MyEvent?.Invoke(this, e);
public void DoWork() { /* ... */ OnMyEvent(new MyEventArgs { Data = "done" }); }
}
// 3. 訂閱者
public class Subscriber
{
public void Hook(Publisher p) => p.MyEvent += Handle;
public void Unhook(Publisher p) => p.MyEvent -= Handle;
private void Handle(object sender, MyEventArgs e) { Console.WriteLine(e.Data); }
}