Search code examples
c#covariance

How to "carry" covariance through multiple interfaces


I've got an interface structure that looks like this:

At the most basic level is an IDataProducer with this definition:

public interface IDataProducer<out T>
{
    IEnumerable<T> GetRecords();
}

and an IDataConsumer that looks like this:

public interface IDataConsumer<out T>
{
    IDataProducer<T> Producer { set; }
}

Finally, I've got an IWriter that derives off of IDataConsumer like so:

public interface IWriter<out T> : IDataConsumer<T>
{
    String FileToWriteTo { set; }

    void Start();
}

I wanted to make IWriter's generic type T covariant so that I could implement a Factory method to create Writers that could handle different objects without having to know what type would be returned ahead of time. This was implemented by marking the generic type "out". The problem is, I'm having a compile error on IDataConsumer because of this:

Invalid variance: The type parameter 'T' must be contravariantly valid on 'IDataConsumer<T>.Producer'. 'T' is covariant.

I'm not really sure how this can be. It looks to me like the generic type is marked as covariant through the whole chain of interfaces, but it is very possible I don't totally understand how covariance works. Can someone explain to me what I am doing wrong?


Solution

  • The problem is that your Producer property is write-only. That is, you are actually using T in a contravariant way, by passing the value that is generic on type T into the implementer of the interface, rather than the implementer passing it out.

    One of the things I like best about the way the C# language design team handled the variance feature in generic interfaces is that the keywords used to denote covariant and contravariant type parameters are mnemonic with the way the parameters are used. I always have a hard time remembering what the words "covariant" and "contravariant" mean, but I never have any trouble remembering what out T vs. in T means. The former means that you promise to only return T values from the interface (e.g. method return values or property getters), while the latter means that you promise to only accept T values into the interface (e.g. method parameters or property setters).

    You broke that promise by providing a setter for the Producer property.

    Depending on how these interfaces are implemented, it's possible what you want is interface IDataConsumer<in T> instead. That would at least compile. :) And as long as the IDataConsumer<T> implementation really is only consuming the T values, that would probably work. Hard to say without a more complete example.