在 .NET 中,事件本質上是一對 add/remove 存取器,若能將其封裝為獨立物件 DelegateEvent<TDelegate>,便能像委派一樣自由傳遞、組合、管理事件訂閱。本文探討四種建構 DelegateEvent 的模式,各有適用場景與優缺點。
public class DelegateEvent<TDelegate> where TDelegate : Delegate
{
private Action<TDelegate> _addHandler;
private Action<TDelegate> _removeHandler;
public DelegateEvent<TDelegate> AddHandler(TDelegate handler) { ... }
public DelegateEvent<TDelegate> RemoveHandler(TDelegate handler) { ... }
}
關鍵在於如何初始化 _addHandler 與 _removeHandler 兩個委派欄位,以下介紹四種建構函式模式。
最直觀的方式,由呼叫端提供具體的加減邏輯:
public DelegateEvent(Action<TDelegate> add, Action<TDelegate> remove)
{
if (add == null || remove == null) throw new ArgumentNullException();
if (!typeof(Delegate).IsAssignableFrom(typeof(TDelegate)))
throw new ArgumentException("TDelegate must be a delegate type.");
_addHandler = add;
_removeHandler = remove;
}
用法:
var myClass = new MyClass();
var de = new DelegateEvent<EventHandler>(
h => myClass.MyEvent += h,
h => myClass.MyEvent -= h);
缺點: 無法驗證注入邏輯正確性(如誤用 += 取代 -=),且呼叫端程式碼冗長。
透過反射取得 EventInfo,自動繫結存取器:
public DelegateEvent(object obj, string eventName)
{
if (obj == null || string.IsNullOrEmpty(eventName))
throw new ArgumentNullException();
BindEvent(obj.GetType(), obj, eventName);
}
private void BindEvent(Type type, object obj, string eventName)
{
var flags = BindingFlags.Public | BindingFlags.NonPublic |
(obj == null ? BindingFlags.Static : BindingFlags.Instance);
var eventInfo = type.GetEvent(eventName, flags);
if (eventInfo == null)
throw new ArgumentException($"Event {eventName} not found in {type.FullName}");
if (eventInfo.EventHandlerType != typeof(TDelegate))
throw new ArgumentException($"Event handler type mismatch.");
_addHandler = h => eventInfo.AddEventHandler(obj, (Delegate)(object)h);
_removeHandler = h => eventInfo.RemoveEventHandler(obj, (Delegate)(object)h);
}
用法: new DelegateEvent<EventHandler>(myClass, "MyEvent")
缺點: 字串型別事件名稱無編譯期檢查,重構不支援;無法綁定靜態事件(需另行處理)。
針對靜態事件,傳入 Type 而非實例:
public DelegateEvent(Type type, string eventName)
{
if (type == null || string.IsNullOrEmpty(eventName))
throw new ArgumentNullException();
BindEvent(type, null, eventName); // obj 為 null 表示靜態事件
}
用法: new DelegateEvent<EventHandler>(typeof(MyClass), "MyStaticEvent")
同樣面臨字串硬編碼的維護性問題。
利用 Expression<Func<TDelegate>> 捕獲事件成員資訊:
public DelegateEvent(Expression<Func<TDelegate>> eventExpr)
{
if (eventExpr == null) throw new ArgumentNullException(nameof(eventExpr));
var memberExpr = eventExpr.Body as MemberExpression;
if (memberExpr == null) throw new ArgumentException("Not an event access expression.");
object instance = null;
if (memberExpr.Expression != null)
{
var instanceExpr = Expression.Lambda<Func<object>>(memberExpr.Expression);
instance = instanceExpr.Compile().Invoke(); // 取得實例(或 null 為靜態)
}
BindEvent(memberExpr.Member.DeclaringType, instance, memberExpr.Member.Name);
}
輔助工廠簡化呼叫:
public static class EventFactory
{
public static DelegateEvent<T> Create<T>(Expression<Func<T>> eventExpr)
=> new DelegateEvent<T>(eventExpr);
}
用法:
class Program
{
public event EventHandler MyEvent;
public static event EventHandler MyStaticEvent;
static void Main()
{
var sde = EventFactory.Create(() => MyStaticEvent); // 靜態事件
var p = new Program();
var de = EventFactory.Create(() => p.MyEvent); // 實例事件
}
}
優勢: 完整支援 IntelliSense、編譯期檢查、重構重命名。
限制: 僅能在「定義事件的類別內部」使用,因需存取事件成員(外部無法直接存取 event 的 add/remove)。
| 模式 | 適用場景 | 優點 | 缺點 |
|---|---|---|---|
| 直接注入 | 快速原型、特殊邏輯 | 彈性最大 | 無驗證、程式碼冗長 |
| 實例+字串 | 執行期動態綁定、外部組件 | 通用性強 | 無靜態檢查、無法重構 |
| 型別+字串 | 靜態事件綁定 | 支援靜態事件 | 同字串缺點 |
| 運算式樹 | 事件定義者內部暴露 | 強型別、完整工具鏈支援 | 僅限內部使用、語法稍複雜 |
優秀的 API 設計應預見多種使用情境,提供多重建構方式,並儘早暴露錯誤(編譯期優於執行期)。這正是「趣味程式設計」的核心:在有限範圍內深入挖掘,考慮周全,而非僅求「能跑」。