Woot woot! Solved a problem that was absolutely BUGGING me in my code for the last several weeks (I just had bigger fish to fry until tonight when I finally decided to bite the bullet).
I have a simple MessageManager class in my code (C#) that you can register listeners with and send messages through. It is powered by generics. For example, adding a listener (before this change):
public class MessageManager
{
private Dictionary<Type, Action<Message>> _messageListeners;
...
public void AddListener<T>(Action<Message>> listener) where T : Message
{
if (!_messageListeners.Keys.Contains(typeof(T)))
{
_messageListeners[typeof(T)] = new List<object>();
}
_messageListeners[typeof(T)].Add(listener);
}
The thing I hate about this code is that when you call AddListener, the Action you are registering is a callback that accepts the base Message type (not the derived type specified with T).
Because of variance (covariance and contravariance) in .NET Generics, Action<DerivedMessage> can't automatically cast down to Action<Message> as it is not type-safe. So, I'm left with writing ugly code that casts into the right type:
public override void Loaded()
{
// Yuck, yuck, yuck! Look at those casts!
this.GameObject.MessageManager.AddListener<CardsShuffledMessage>(msg => OnCardsShuffled((CardsShuffledMessage)msg));
...
}
private void OnCardsShuffled(CardsShuffledMessage msg)
{
_spriteComponent.Texture = ContentService.Instance.GetAsset<Texture2D>(AcornAssets.CardBack);
}
Or in the above, I could have OnCardsShuffled accept just Message and cast to CardsShuffledMessage in the method body. Either way, yuck!!! This code has been bothering me big time!!!
I finally spent some time to solve it tonight. I don't think there is any way I could have solved this several years ago before I started playing around with higher-order functions in functional-programming languages (functions that return functions).
The key for me was to create a method that returned a "wrapper Action<Message>" delegate that internally did the casting to the type it knew about via generic type parameters. Then I stored listeners as List<object> instead of List<Action<Message>> to enable the more loosely-typed casting.
public class MessageManager
{
private Dictionary<Type, List<object>> _messageListeners;
// New lookup of senders that accept Action<Message> and invoke strongly-typed Action<T> in listeners
private Dictionary<Type, Action<Message>> _messageSenders;
...
// Now I can accept Action<T> (derived message listeners) rather than Action<Message>
public void AddListener<T>(Action<T> listener) where T : Message
{
if (!_messageListeners.Keys.Contains(typeof(T)))
{
_messageListeners[typeof(T)] = new List<object>();
_messageSenders[typeof(T)] = CreateMessageSender<T>();
}
_messageListeners[typeof(T)].Add(listener);
}
// The function that does the magic
private Action<Message> CreateMessageSender<T>() where T : Message
{
return new Action<Message>(param => {
foreach (var listener in _messageListeners[typeof(T)])
{
// Do the cast to a strongly-typed action from our generic param T
((Action<T>)listener)((T)param);
}
});
}
// When a message is sent to listeners, it's as simple as using the "wrapper Action" we created
private void ProcessMessage(Message msg)
{
if (_messageListeners.Keys.Contains(msg.GetType()))
{
_messageSenders[msg.GetType()](msg);
}
}
Long story short, now I can write clean code that I _like_ without nasty type conversions:
// YUCK!!!! This is the code I had to write before...
//this.GameObject.MessageManager.AddListener<CardsShuffledMessage>(msg => OnCardsShuffled((CardsShuffledMessage)msg));
// YUM!!!! Much better :).
this.GameObject.MessageManager.AddListener<CardsShuffledMessage>(OnCardsShuffled);
I'm also very happy to have done this without having to resort to any reflection whatsoever. Quite ecstatic. I know I know, it's a small change, but I'm excited
.