Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Interface inheritance with generic lists

I want to build a generic system of producers and consumers for a simple project.

What I have now is

public interface IMessage     {    }
public interface Message1 : IMessage    {    }
public interface Message2 : IMessage    {    }
public interface IConsumer<T>    {    }
public interface IProducer<T>    {    }

public class Mop1 : IConsumer<Message1>, IProducer<Message2>
{
}



class Program
{
    static void Main(string[] args)
    {
        var list = new List<IConsumer<IMessage>>();
        var mop = new Mop1();
        list.Add(mop);   // Error occurs here
    }
}

The last line gives an error like cannot convert from 'Mop1' to 'IConsumer<GenericPubSub.IMessage>'

But Mop1 implements an IConsumer of an IMessage derived type. Is this some variance problem here? What is wrong about that?

like image 393
Mare Infinitus Avatar asked Dec 16 '14 17:12

Mare Infinitus


2 Answers

This is tricky case. You can use co/contravariance, but...

I will simplify code a little. Just to shut up the compiler you can do that:

public interface IMessage { }

public interface Message1 : IMessage { }


public class Mop1 : IConsumer<Message1>
{
}

public interface IConsumer<out IMessage>
{
}


class Program
{
    static void Main(string[] args)
    {
        var list = new List<IConsumer<IMessage>>();
        var mop = new Mop1();

        list.Add(mop);   // Error occurs here
    }
}

out IMessage will do the trick as suggested in another answer, but it fundamentally doesn't fix anything. Let me show, now you want to make your interface:

public interface IConsumer<out IMessage>
{
    object Process (IMessage m);
}

aaaand it will not compile. Because simply put if you say out IMessage it means that return types should be derived from IMessage, not parameter types.

So you will have it this way:

public interface IConsumer<in IMessage>
{
    object Process (IMessage m);
}

public class Mop1 : IConsumer<Message1>
{
    public object Process (Message1 msg)
    {
        return null;
    }
}

To make it compile and be valid. But now your list.Add(mop); will not work. And rightfully so, because your Mop1 is indeed not castable to IConsumer<IMessage> because if it was following code would be possible:

list[0].Process (new Message2 ());

and it is not possible because Mop1 accepts only Message1.

So to answer the question. You can't really do anything meaningful with message dispatching and pure C# compiler features. I see where it was going, you wanted static typing with messages and stuff. Unfortunately you can't have a list of consumers that consume specific messages in generic list, you have to have abstract signatures like bool CanProcess(IMessage); IMessage Process(IMessage); and cast inside. You can also do it slightly better with custom attributes and reflection, but again, not purely with C# compiler.

like image 80
Andrey Avatar answered Sep 28 '22 02:09

Andrey


In case you're planning to implement a typical producer, consumer pattern with these interfaces, please see this answer first.


If you want an IConsumer<TMessage> to also be an IConsumer<TMessageBase>, where TMessageBase is any type that TMessage inherits or implements, then you need to make your generic parameter covariant. Use the out modifier.

public interface IConsumer<out T>    {    }

Now IConsumer<Message1> is assignable to IConsumer<IMessage>, since Message1 implements IMessage.

like image 40
Asad Saeeduddin Avatar answered Sep 28 '22 00:09

Asad Saeeduddin