I'm using my own Action-based Event Manager for a while and looking for ways to improve it.
Mainly it's used in game development, where excessive garbage generation can lead to severe consequences. That's why I'm trying to avoid any boxing which normally exists in such managers.
There are different event types (dozens, but not hundreds), each event type holds several subscribers. Subscriptions happen not often, but some events can fire hundreds of times per second (it won't actually, but i'm considering the possibility while creating my manager).
There is my previous version which is works just fine, but asks for improvement:
internal class EventManager: IEventManager
{
private readonly Dictionary<int, Action<object>?> events = new Dictionary<int, Action<object>?>();
public virtual void Subscribe(int incidentId, Action<object> action)
{
if (events.ContainsKey(incidentId)) events[incidentId] += action;
else events.Add(incidentId, action);
}
public virtual void Unsubscribe(int incidentId, Action<object> action)
{
if (!events.ContainsKey(incidentId)) return;
events[incidentId] -= action;
}
public virtual void Trigger(int incidentId, object args)
{
if (!events.ContainsKey(incidentId)) return;
events[incidentId]?.Invoke(args);
}
}
// Sender:
eventManager.Trigger(1, "1234");
// Receiver:
eventManager.Subscribe(1, a => Console.WriteLine($"Received string length: {(a as string)?.Length}"));
I'm actually wrapping object
inside convenient struct, but it doesn't matter. Sender is boxing some data to object
, receiver is unboxing it. That's behavior I would like to avoid. Creation of an object
each time event is fired can be painful.
There is another version of event manager:
internal class EventManager: IEventManager
{
private readonly Dictionary<int, Dictionary<Type, Delegate>> events = new Dictionary<int, Dictionary<Type, Delegate>>();
public virtual void Subscribe<T>(int incidentId, Action<T> action)
{
if (!events.ContainsKey(incidentId))
events[incidentId] = new Dictionary<Type, Delegate> {[typeof(T)] = action};
else if (!events[incidentId].ContainsKey(typeof(T)))
events[incidentId][typeof(T)] = action;
else
events[incidentId][typeof(T)] = Delegate.Combine(events[incidentId][typeof(T)], action);
}
public virtual void Unsubscribe<T>(int incidentId, Action<T> action)
{
if (!events.ContainsKey(incidentId) ||
!events[incidentId].ContainsKey(typeof(T))) return;
events[incidentId][typeof(T)] = Delegate.Remove(events[incidentId][typeof(T)], action);
}
public virtual void Trigger<T>(int incidentId, T args)
{
if (!events.ContainsKey(incidentId) ||
!events[incidentId].ContainsKey(typeof(T))) return;
var genericEvent = events[incidentId][typeof(T)] as Action<T>;
genericEvent?.Invoke(args);
}
}
// ...
// Sender:
eventManager.Trigger(1, "1234");
// Receiver:
eventManager.Subscribe<string>(1, a => Console.WriteLine($"Received string length: {a.Length}"));
That's much better! Events are still mapped with Ids, senders can send any data and subscribers would get it without need of unboxing. Even conflicting data types between sender and receiver are handled: receiver wouldn't get incorrect message type (in previous version I'd have to add null and default values handling).
But usage of generics leads to another collection of Type
which stores generic Actions. I cannot just declare something like Dictionary<int, Action<T>>
.
Dictionary inside a dictionary?.. Is it normal? Are there hidden traps somewhere?
Or I'm missing some simple pattern which would allow to pass data via events and get rid of object
boxing and support different Ids for different event types?
P.S. Subscriptions are rare, so I'm not necessarily looking into micro-optimizations here. Event firing, on the other hand, could be extremely often.
UPDATE: Clarification: I'd like to receive feedback on second implementation, potential improvements and possible problems in future. First example is just "standard" version: I'm wondering if I'd better to stick with it or use generics magic with second example.