More Effective C# 16.為通知實作事件模式 More Effective C# 16.為通知實作事件模式(Implement the Event Pattern for Notifications)

這個做法是以前 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 關鍵字的時候會產生類似下面這段程式碼。

首先會產生addremove 存取子用來新增與移除新事件,並且可以看出 EventHandler 是 Immutable Type,還有從 Delegate.Combine 可以看出 EventHandler 是 MulticastDelegate,最後使用 Interlocked 確保更新與刪除時的線程安全,這也代表你在處理事件的時候可以自己 addremove 存取子,不過要注意線程安全的細節,通常是請編譯器自動產生就好。

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 的使用方式,以及它編譯後會產生addremove 存取子,並且本身就是一個 MulticastDelegate, 由於發起事件的時候不管有沒有訂閱者都能正常運行,使用這個模式可以很方便的解除發佈者跟訂閱者之間的耦合。