Search code examples
c#genericspublish-subscribe

Can I subscribe to a generic Action with a concrete method?


See Enigmativity's answer for a much clearer phrasing of this question.


I have a generic Action that I am registering for, and then casting to the type I am expecting:

public interface IMyInterface { }

public static Action<IMyInterface> MyAction;

public class MyClass : IMyInterface { }

public void Subscribe()
{
    MyAction<MyClass> += MyMethod;
}

public void MyMethod(IMyInterface myInterface)
{
    var myClass = (MyClass)myInterface;
}

But I want to be able to subscribe with a method that already dictates the type so I can avoid the extra step of casting. Is it possible to only subscribe to MyActions such that IMyInterface has a specific type? So that MyMethod can be like this:

public void MyMethod(MyClass myClass)
{

}

The reason I am trying to do this is because I am writing a messaging system which uses the specific type. I am using generics to determine which messages to subscribe to. I don't think this part affects my question, but here is what that looks like:

private Dictionary<Type, List<Action<IMessage>> subscribers = new Dictionary<Type, List<Action<IMessage>>();

public void SubscribeMessage<TMessage>(Action<IMessage> callback)
    where TMessage : IMessage
{
    var type = typeof(TMessage);
    if (subscribers.ContainsKey(type))
    {
        if (!subscribers[type].Contains(callback))
        {
            subscribers[type].Add(callback);
        }
        else
        {
            LogManager.LogError($"Failed to subscribe to {type} with {callback}, because it is already subscribed!");
        }
    }
    else
    {
        subscribers.Add(type, new List<Action<IMessage>>());
        subscribers[type].Add(callback);
    }
}

public void UnsubscribeMessage<TMessage>(Action<IMessage> callback)
    where TMessage : IMessage
{
    var type = typeof(TMessage);
    if (subscribers.ContainsKey(type))
    {
        if (subscribers[type].Contains(callback))
        {
            subscribers[type].Remove(callback);
        }
        else
        {
            LogManager.LogError($"Failed to unsubscribe from {type} with {callback}, because there is no subscription of that type ({type})!");
        }
    }
    else
    {
        LogManager.LogError($"Failed to unsubscribe from {type} with {callback}, because there is no subscription of that type ({type})!");
    }
}

//The use case given MyClass implements IMessage
public void Subscribe()
{
    SubscribeMessage<MyClass>(MyMethod);
}

public void MyMethod(IMessage myMessage)
{
    var myClass = (MyClass)myMessage;
}

So is it possible for me to subscribe to a generic Action with a method that has a concrete type?


Solution

  • The types in your question seemed to be a bit garbled - IMyInterface at the top of the question and IMessage in the bottom part. I've assumed these interfaces and basic methods:

    public interface IMessage { }
    
    public class MyClass1 : IMessage { }
    public class MyClass2 : IMessage { }
    
    public void MyMethod1(MyClass1 myClass1)
    {
        Console.WriteLine("MyMethod1");
    }
    
    public void MyMethod2(MyClass2 myClass1)
    {
        Console.WriteLine("MyMethod2");
    }
    

    Now, I've further simplified your code to not have a SubscribeMessage and an UnsubscribeMessage method as this will force you to maintain a reference to the original delegate to remove a delegate. It's easier to have a single Subscribe method that returns an IDisposable that lets you unsubscribe. It's much easier to hold a bunch of disposables than many different types of delegates - otherwise it's an "out of the frying pan and into the fire" kind of thing.

    Here's all you need for Subscribe:

    private Dictionary<Type, List<Delegate>> _subscribers = new Dictionary<Type, List<Delegate>>();
    
    public IDisposable Subscribe<TMessage>(Action<TMessage> callback) where TMessage : IMessage
    {
        var type = typeof(TMessage);
        if (!_subscribers.ContainsKey(type))
        {
            _subscribers.Add(type, new List<Delegate>());
        }
        _subscribers[type].Add(callback);
        return new ActionDisposable(() => _subscribers[type].Remove(callback));
    }
    

    I've removed the need to check for duplicates. That should be a responsibility of the calling code. There are situations where calling a delegate twice might be valid. Leave it to the calling code to make sure it's a sane thing to do or not.

    I've also used a List<Delegate> as that enables any delegate type to be stored.

    Here's the ActionDisposable class you need:

    public sealed class ActionDisposable : IDisposable
    {
        private readonly Action _action;
        private int _disposed;
    
        public ActionDisposable(Action action)
        {
            _action = action;
        }
    
        public void Dispose()
        {
            if (Interlocked.Exchange(ref _disposed, 1) == 0)
            {
                _action();
            }
        }
    }
    

    And now here's Send:

    public void Send<TMessage>(TMessage message) where TMessage : IMessage
    {
        var type = typeof(TMessage);
        if (_subscribers.ContainsKey(type))
        {
            var subscriptions = _subscribers[type].Cast<Action<TMessage>>().ToArray();
            foreach (var subscription in subscriptions)
            {
                subscription(message);
            }
        }
    }
    

    So to call all of this code you can do:

    IDisposable subscription1 = Subscribe<MyClass1>(MyMethod1);
    IDisposable subscription2 = Subscribe<MyClass2>(MyMethod2);
    
    Send(new MyClass1());
    Send(new MyClass2());
    
    subscription1.Dispose();
    
    Send(new MyClass1());
    Send(new MyClass2());
    

    The result I get is:

    MyMethod1
    MyMethod2
    MyMethod2
    

    It clearly is subscribing and unsubscribing, and it's only calling the delegates for the type of message passed. I think that covers off on what you're trying to do.