Search code examples
c#genericscovariancecontravariance

Can't seem to get Generic Covariance/Contravariance to work


Suppose I have the following code:

public interface IBaseMessage { }

public interface IMessageProcessor<in T> where T : IBaseMessage {
            void Process(T msg);
        }

public class RRMessage : IBaseMessage {
          //something here
        }

public class BaseMessageProcessor {
         //something here
        }

public class RRMessageProcessor : BaseMessageProcessor, IMessageProcessor<RRMessage> {
            public void Process(RRMessage msg) {
                Console.WriteLine("Processed RRMessage");
            }
        }

public Dictionary<Type, IMessageProcessor<IBaseMessage>> MessageProcessors = new Dictionary<Type, IMessageProcessor<IBaseMessage>>();

[Test]
public void Test1() {
     var msgProcessor = new RRMessageProcessor();
     MessageProcessors.Add(typeof(RRMessage), msgProcessor);
     }

I enabled contravariance for the interface IMessageProcessor. Why does MessageProcessors.Add(typeof(RRMessage), msgProcessor); cause a compile time error:

Argument 2: cannot convert from 'RRMessageProcessor' to 'IMessageProcessor<IBaseMessage>'

It seems like it should be able to convert because RRMessageProcessor:IMessageProcessor<RRMessage:IBaseMessage>> How can I get this to work?


Solution

  • IMessageProcessor<in T> is contravariant in T, not covariant. That means the following is allowed:

    class RRSubtype : RRMessage {}
    IMessageProcessor<RRSubtype> p = new RRMessageProcessor();
    

    what you are trying to do is statically unsafe since it would allow you do to:

    class NotRRMessage : IBaseMessage { }
    IMessageProcessor<IBaseMessage> p = new RRMessageProcessor();
    p.Process(new NotRRMessage());
    

    so you need to maintain safety dynamically instead, which you seem to be attempting with the dictionary Type -> Processor. You can therefore create an unsafe wrapper type:

    public class ProcessorWrapper<T> : IMessageProcessor<IBaseMessage> {
        private readonly IMessageProcessor<T> inner;
        public ProcessorWrapper(IMessageProcessor<T> inner) { this.inner = inner; }
    
        public void Process(IBaseMessage msg)
        {
            if(msg is T) { inner.Process((T)msg); }
            else throw new ArgumentException("Invalid message type");
        }
    }
    

    You can then construct your list with these wrappers while maintaining the inner processors typed as you want e.g.

    public Dictionary<Type, IMessageProcessor<IBaseMessage>> MessageProcessors = new Dictionary<Type, IMessageProcessor<IBaseMessage>>();
    MessageProcessors.Add(new ProcessorWrapper<RRMessage>(new RRMessageProcessor());