Search code examples
c#covariancecontravariance

Co/contravariant interfaces and assignability


I'm having some issues with, I think, variance, which I don't fully understand. I have a generic interface with two type parameters, like this:

public interface IInvoker<TParameter, TResult> {
    TResult Invoke(TParameter parameter);
}

Now, in my case, I want to let TA and TB be abstract classes, something like this:

public abstract class AbstractParameter {
    public int A { get; set; }
}
public abstract class AbstractResult {
    public string X { get; set; }
}

public class Parameter1 : AbstractParameter {
    public int B { get; set; }
}
public class Result1 : AbstractResult {
    public string Y { get; set; }
}
// ... Many more types

I then want to process a set of different implementations of IInvoker<,>, so I figured I could do something like this

public class InvokerOne : IInvoker<Parameter1, Result1> { /* ... */ }
public class InvokerTwo : IInvoker<Parameter2, Result2> { /* ... */ }

// ..
IInvoker<AbstractParameter, AbstractResult>[] invokers = { new InvokerOne(), new InvokerTwo() };

This does not work, because IInvoker<AbstractParameter, AbstractResult> cannot be assigned from IInvoker<Parameter1, Result1> (and friends), as far as I understand. First I figured this was the time to slap some in and out on my interface (interface IInvoker<in TParameter, out TResult>), but that did not help.

But I don't understand why? As far as I can tell, anyone using an IInvoker<AbstractParameter, AbstractResult> should be able to call Invoke, right? What am I missing?


Solution

  • The problem is the TResult type parameter is contra-variant, but you are trying to use them co-variantly in your assignment e.g.

    IInvoker<AbstractParameter, AbstractResult> i1 = new InvokerOne();
    

    TResult is co-variant, so it's ok for AbstractResult to be a larger type than Result1. However, since TParameter is contra-variant, TParameter must be a smaller type than Parameter1, which is not the case for AbstractParameter.

    If the above was valid you could do:

    class OtherParameter : AbstractParameter { ... };
    IInvoker<AbstractParameter, AbstractResult> i1 = new InvokerOne();
    i1.Invoke(new OtherParameter());
    

    which is not safe.

    You could have the following however:

    public class OtherParameter1 : Parameter1 { }
    IInvoker<OtherParameter1, AbstractResult> i1 = new InvokerOne();
    

    here OtherParameter1 can be passed as a parameter to Invoke since it will always be a valid for Parameter1.