這個做法是以前 Effective C# 做法 7、做法 8 的延伸討論,主要是解說 .NET 事件模式的設計與使用它的好處。
假設我有一個紀錄日誌的類別叫做 Logger
,他的工作是負責把訊息發送給事件的訂閱者,我們可以使用 AddMsg
方法添加日誌並使用
Invoke
方法把自訂的事件參數 LoggerEventArgs
發送給事件的訂閱者,注意 ?.Invoke
這個寫法是新的線程安全的寫法。
public class Logger
{
static Logger()
{
Singleton = new Logger();
}
private Logger()
{
}
public static Logger Singleton { get; }
public event EventHandler<LoggerEventArgs> Log;
public void AddMsg(int priority, string msg) => Log?.Invoke(this, new LoggerEventArgs(priority, msg));
}
public class LoggerEventArgs : EventArgs
{
public int Priority { get; set; }
public string Message { get; set; }
public LoggerEventArgs(int priority, string msg)
{
Priority = priority;
Message = msg;
}
}
使用時只要添加訂閱者並呼叫 AddMsg
方法,之後就會 Invoke
並執行訂閱者的方法。
void Main()
{
Logger.Singleton.Log += (sender, msg) =>
Console.Error.WriteLine("{0}:\t{1}",
msg.Priority.ToString(),
msg.Message);
Logger.Singleton.AddMsg(1, "Hi");
}
在之前的做法也有提到過 EventHandler
就是一種自訂的委派,跟 Func
差不多意思,所以這裡的訂閱者其實就是一個符合 EventHandler 簽章的方法。
void Main()
{
Logger.Singleton.Log += SendConsoleLogger;
Logger.Singleton.AddMsg(1, "Hi");
}
public void SendConsoleLogger(object sender, LoggerEventArgs args)
{
Console.Error.WriteLine("{0}:\t{1}",
args.Priority.ToString(),
args.Message);
}
還有新增與刪除訂閱者使用的是符號 +=
和 -=
,可以發現刪除之後第二條的事件就不會輸出在 Console 上了。
void Main()
{
Logger.Singleton.Log += SendConsoleLogger;
Logger.Singleton.AddMsg(1, "Hi");
Logger.Singleton.Log -= SendConsoleLogger;
Logger.Singleton.AddMsg(2, "Hi");
}
比較特別的是這段程式碼 public event EventHandler<LoggerEventArgs> Log;
第一個 event 是關鍵字用來宣告這個成員可以用來觸發通知,
EventHandler<T>
才是它的型別,編譯器看到 event 關鍵字的時候會產生類似下面這段程式碼。
首先會產生add
與 remove
存取子用來新增與移除新事件,並且可以看出 EventHandler 是 Immutable Type,還有從 Delegate.Combine
可以看出 EventHandler 是 MulticastDelegate
,最後使用 Interlocked 確保更新與刪除時的線程安全,這也代表你在處理事件的時候可以自己
add
與 remove
存取子,不過要注意線程安全的細節,通常是請編譯器自動產生就好。
public event EventHandler<LoggerEventArgs> Log
{
[CompilerGenerated]
add
{
EventHandler<LoggerEventArgs> eventHandler = this.Log;
while (true)
{
EventHandler<LoggerEventArgs> eventHandler2 = eventHandler;
EventHandler<LoggerEventArgs> value2 = (EventHandler<LoggerEventArgs>)Delegate.Combine(eventHandler2, value);
eventHandler = Interlocked.CompareExchange(ref this.Log, value2, eventHandler2);
if ((object)eventHandler == eventHandler2)
{
break;
}
}
}
[CompilerGenerated]
remove
{
EventHandler<LoggerEventArgs> eventHandler = this.Log;
while (true)
{
EventHandler<LoggerEventArgs> eventHandler2 = eventHandler;
EventHandler<LoggerEventArgs> value2 = (EventHandler<LoggerEventArgs>)Delegate.Remove(eventHandler2, value);
eventHandler = Interlocked.CompareExchange(ref this.Log, value2, eventHandler2);
if ((object)eventHandler == eventHandler2)
{
break;
}
}
}
}
我們的 Logger
類別目前只有一個事件,在某些情況可能一個類別能包含多個事件,如果每個事件都要宣告一次那會非常麻煩,
所以可以改用 EventHandlerList 這個類別來保存多個 EventHandler。
public sealed class Logger
{
private static EventHandlerList Handlers = new EventHandlerList();
static public void AddLogger(string system, EventHandler<LoggerEventArgs> ev) =>
Handlers.AddHandler(system, ev);
static public void RemoveLogger(string system, EventHandler<LoggerEventArgs> ev) =>
Handlers.RemoveHandler(system, ev);
static public void AddMsg(string system, int priority, string msg)
{
if (!string.IsNullOrEmpty(system))
{
EventHandler<LoggerEventArgs> handler = Handlers[system] as EventHandler<LoggerEventArgs>;
LoggerEventArgs args = new LoggerEventArgs(priority, msg);
handler?.Invoke(null, args);
// The empty string means receive all messages:
handler = Handlers[""] as EventHandler<LoggerEventArgs>;
handler?.Invoke(null, args);
}
}
}
public class LoggerEventArgs
{
public int Priority { get; set; }
public string Message { get; set; }
public LoggerEventArgs(int priority, string msg)
{
Priority = priority;
Message = msg;
}
}
它的運作原理也很簡單,就是建立額外的 ListEntry
物件透過 key
來將多個委派進行分組,跟字典的原理差不多,
不過 EventHandlerList 沒有泛型的版本,並且在搜尋事件的時候使用效率很差的線性搜索。
// EventHandlerList.cs
public void AddHandler(object key, Delegate? value)
{
ListEntry? e = Find(key);
if (e != null)
{
e._handler = Delegate.Combine(e._handler, value);
}
else
{
_head = new ListEntry(key, value, _head);
}
}
public void RemoveHandler(object key, Delegate? value)
{
ListEntry? e = Find(key);
if (e != null)
{
e._handler = Delegate.Remove(e._handler, value);
}
}
private ListEntry? Find(object key)
{
ListEntry? found = _head;
while (found != null)
{
if (found._key == key)
{
break;
}
found = found._next;
}
return found;
}
private sealed class ListEntry
{
internal readonly ListEntry? _next;
internal readonly object _key;
internal Delegate? _handler;
public ListEntry(object key, Delegate? handler, ListEntry? next)
{
_next = next;
_key = key;
_handler = handler;
}
}
所以了解原理之後自己使用字典的方式改寫也不會太困難。
public sealed class Logger
{
private static Dictionary<string, EventHandler<LoggerEventArgs>>
Handlers = new Dictionary<string, EventHandler<LoggerEventArgs>>();
static public void AddLogger(string system, EventHandler<LoggerEventArgs> ev)
{
if (Handlers.ContainsKey(system))
Handlers[system] += ev;
else
Handlers.Add(system, ev);
}
static public void RemoveLogger(string system, EventHandler<LoggerEventArgs> ev) =>
Handlers[system] -= ev;
static public void AddMsg(string system, int priority, string msg)
{
if (string.IsNullOrEmpty(system))
{
EventHandler<LoggerEventArgs> handler = null;
Handlers.TryGetValue(system, out handler);
LoggerEventArgs args = new LoggerEventArgs(priority, msg);
handler?.Invoke(null, args);
// The empty string means receive all messages:
handler = Handlers[""] as EventHandler<LoggerEventArgs>;
handler?.Invoke(null, args);
}
}
}
Summary
這個做法又再次複習了 EventHandler 的使用方式,以及它編譯後會產生add
與 remove
存取子,並且本身就是一個 MulticastDelegate
,
由於發起事件的時候不管有沒有訂閱者都能正常運行,使用這個模式可以很方便的解除發佈者跟訂閱者之間的耦合。