Piszę sobie statyczną klasę, która będzie mogła dostarczać wiele różnych typów zdarzeń. Powiedzmy, że wszystkie implementują jakiś interfejs IEvent
. Mógłbym stworzyć jedno zdarzenie, które będzie wywoływane dla wszystkich zdarzeń (oraz powiedzmy, że dostarczę obok samego zdarzenia (typu IEvent
) również Type
albo jakiś enum
i użytkownik sobie zrobi switcha czy drabinkę ifów. Ale nie podoba mi się to rozwiązanie, tak samo jak udostępnienie tylu zdarzeń, co moich typów. Ważne jest również to, że nie wszystkie będą one wykorzystywane oraz w mojej bazowej klasie chciałbym się w łatwy sposób dowiedzieć jakie typy zdarzeń zażyczył sobie użytkownik (w przypadku poprzedniego pomysłu i tak musiałbym dostarczyć jakąś dodatkową metodą te typy/enumy).
Najlepsze byłoby takie, które jest statycznie typowane (safe-type), a wywołanie zdarzenia nie jest późno wiązane (late-bound) i API było proste dla użytkownika. Pierwsze to kwestia wygody oraz bezpieczeństwa, a drugie wydajności, więc przekazywanie MulticastDelegate odpada (no chyba, że będziemy się bawić w magiczne sztuczki typu dynamiczne generowanie IL).
Niestety w .NET nie ma czegoś takiego jak generyczne zdarzenia (cholera no, przydały by się szablony ;)), ale można stworzyć delegate
, który ma generyczny argument. Oczywiście taki delegate
musiałby się znajdować w klasie, która ten parametr dostarczy. No i powstało mi takie coś:
using System;
using System.Collections.Generic;
using System.Linq;
namespace Rev.GenericEventsTest
{
public interface IEvent { }
public struct SomeEvent : IEvent { }
public struct AnotherEvent : IEvent { }
public static class Something
{
private static Dictionary<int, Tuple<Type, Action<IEvent>>> _events = new Dictionary<int, Tuple<Type, Action<IEvent>>>();
private static void AddEvent(int identifier, Type type, Action<IEvent> invoker)
{
if(!_events.ContainsKey(identifier))
_events.Add(identifier, new Tuple<Type, Action<IEvent>>(type, invoker));
}
private static void RemoveEvent(int identifier)
{
if(_events.ContainsKey(identifier))
_events.Remove(identifier);
}
public static void InvokeAll(Type type, IEvent argument)
{
foreach (var @event in _events.Where(e => e.Value.Item1 == type))
@event.Value.Item2(argument);
}
public static class Subscriber<T> where T : IEvent
{
private static int GenerateIdentifier(SomeEventHandler eventHandler)
{
return eventHandler.GetHashCode() | eventHandler.Method.GetHashCode();
}
public delegate void SomeEventHandler(T @event);
public static event SomeEventHandler OnEvent
{
add
{
AddEvent(GenerateIdentifier(value), typeof(T), GetInvoker(value));
}
remove
{
RemoveEvent(GenerateIdentifier(value));
}
}
private static Action<IEvent> GetInvoker(SomeEventHandler eventHandler)
{
return (IEvent @event) => eventHandler.Invoke((T)@event);
}
}
}
class Program
{
static void Main(string[] args)
{
Something.Subscriber<SomeEvent>.OnEvent += Program_OnEvent1;
Something.Subscriber<SomeEvent>.OnEvent += Program_OnEvent1;
Something.Subscriber<AnotherEvent>.OnEvent += Program_OnEvent1;
Something.Subscriber<SomeEvent>.OnEvent += Program_OnEvent2;
Something.InvokeAll(typeof(SomeEvent), new SomeEvent());
Something.Subscriber<SomeEvent>.OnEvent -= Program_OnEvent1;
Console.WriteLine();
Something.InvokeAll(typeof(SomeEvent), new SomeEvent());
Console.WriteLine();
Something.InvokeAll(typeof(AnotherEvent), new AnotherEvent());
}
static void Program_OnEvent1(AnotherEvent ev)
{
Console.WriteLine("Program_OnEvent1 - AnotherType");
}
static void Program_OnEvent1(SomeEvent ev)
{
Console.WriteLine("Program_OnEvent1 - SomeEvent");
}
static void Program_OnEvent2(SomeEvent ev)
{
Console.WriteLine("Program_OnEvent2 - SomeEvent");
}
}
}
Z perspektywy użytkownika dołączenie swojego handlera jest bajecznie proste, usunięcie też. "Pod maską" będę sobie wywoływać coś w stylu InvokeAll
(tutaj publiczny, żebym mógł rozwiązanie przetestować).
Czy moje rozwiązanie jest optymalne? Co można w nim ulepszyć, a może jest w ogóle jakiś inny sposób na rozwiązanie tego problemu? Bez upubliczniania metody typu AddEvent
;).