Frage

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?

War es hilfreich?

Lösung

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.

Andere Tipps

One thing you're missing is the variance declaration in your interface. The interface is not variant unless you declare it to be:

public interface IInvoker<in TParameter, out TResult>
//                        ^^             ^^^
//                        Look!          Here too!
{
    TResult Invoke(TParameter parameter);
}

The in and out keywords help underscore the nature of the variance. The type is contravariant with respect to the in parameter and covariant with respect to the out parameter. In other words, you can do this, assuming the usual Animal : Mammal : Cat example:

IInvoker<Mammal, Mammal> a = Whatever();
IInvoker<Cat, Mammal> b = a;
IInvoker<Mammal, Animal> c = a;

That's not particularly useful by itself, but the point is that you can use the IInvoker<Mammal, Mammal> anywhere you need an IInvoker<Cat, Mammal> or an IInvoker<Mammal, Animal>.

There's also something important missing from your question: What exactly do you want to do with your set of IInvoker<,> implementations? ("I want to process a set of different implementations of IInvoker<,>....") The answer to this question will lead you to your solution. Do you want to invoke them all with some objects inheriting from AbstractParameter? If so, as Lee explains, you'd have some trouble if you could do what you want, since nothing would prevent this:

IInvoker<AbstractParameter, AbstractResult>[] invokers = { new InvokerOne(), new InvokerTwo() };
AbstractParameter[] parameters = { new ParameterOne(), new ParameterTwo() };
AbstractResult[] results = { invokers[0].Invoke(parameters[1] /* oops */), invokers[1].Invoke(parameters[0] /* oops */) };

One way to solve that problem would be to remove the parameter from the interface. Make it a private field of the invoker, or else make a class that pairs invokers with their parameters, something like this:

interface IInvokerParameterPair<out TResult>()
    where TResult : AbstractResult
{
    TResult InvokeTheInvoker();
}

class InvokerParameterPair<TParameter, TResult> : IInvokerParameterPair<TResult>
    where TParameter : AbstractParameter 
    where TResult : AbstractResult
{
    private IInvoker<TParameter, TResult> _invoker;
    private TParameter _parameter;
    public InvokerParameterPair(IInvoker<TParameter, TResult> invoker, TParameter parameter)
    {
        _invoker = invoker;
        _parameter = parameter;
    }
    public TResult InvokeTheInvoker()
    {
        return _invoker.Invoke(_parameter);
    }
}

If, on the other hand, you would like to do some processing that has nothing to do with the Invoke method, then your invokers should implement some other common interface or inherit from some other common base class, like this:

public interface IProcessable { }
public interface IInvoker<in TParameter, out TResult> : IProcessable
{
    TResult Invoke(TParameter parameter);
}

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

IProcessable[] invokers = { new InvokerOne(), new InvokerTwo() };

or this:

public interface IInvoker<in TParameter, out TResult> : IProcessable
{
    TResult Invoke(TParameter parameter);
}

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

Processable[] invokers = { new InvokerOne(), new InvokerTwo() };
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top