Type parameter must be contravariantly valid

Viewed 178

Consider a bird:

public interface IBird
{

}

public class Duck : IBird
{
    public string DuckyProperty { get; set; } //this property is Duck specific!
}

And a bird processor:

public interface IBirdProcessor<out T> where T : IBird
{
    double GetBirdWeight(T obj); //this does not compile
}

public class DuckProcessor : IBirdProcessor<Duck>
{
    public double GetBirdWeight(Duck duck)
    {
        return double.Parse(duck.DuckyProperty);
    }
}

And a factory to get a bird processor:

public class BirdProcessorFactory
{
    public IBirdProcessor<T> GetBirdProcessor<T>(T obj) where T : IBird
    {
        return (IBirdProcessor<T>)new DuckProcessor();
    }
}

Startup:

static void Main()
{
    var bird = new Duck() { DuckyProperty = "23" } as IBird; //the cast is necessary to simulate a real world scenario
    var factory = new BirdProcessorFactory();
    var provider = factory.GetBirdProcessor(bird);

    var weight = provider.GetBirdWeight(bird);
}

I want to have an abstract generic bird factory but I get the following error instead:

The type parameter 'T' must be contravariantly valid on 'Program.IBirdProcessor<T>.GetBirdWeight(T)'. 'T' is covariant

If I remove 'out' keyword then I get:

System.InvalidCastException: 'Unable to cast object of type 'DuckProcessor' to type 'IBirdProcessor`1[NotVegetables.Program+IBird]'.'

The cast won't work!

As a workaround I can do this:

public class DuckProcessor : IBirdProcessor<IBird>
{
    public double GetBirdWeight(IBird bird)
    {
        var duck = bird as Duck;
        return double.Parse(duck.DuckyProperty);
    }
}

But this defies usage of generics at all, I feel like DuckProcessor should work with ducks, not with abstract birds.

Am I doing something completely wrong?

2 Answers

All the types in C# generics have to be known at compile time. Only IBird is known at compile time, and the factory has to return an IBirdProcessor<IBird>. Since covariancy isn't an option (because DuckProcessor.GetBirdWeight only accepts Ducks), a DuckProcessor cannot be returned.

You can kind of get the behavior you are wanting with:

public interface IBirdProcessor
{
  double GetBirdWeight(IBird obj);
}

public abstract class BirdProcessorBase<T> : IBirdProcessor where T : IBird
{
  public double GetBirdWeight(IBird bird) => GetBirdWeightInternal((T)bird);

  protected abstract double GetBirdWeightInternal(T bird);
}

public class DuckProcessor : BirdProcessorBase<Duck>
{
  protected override double GetBirdWeightInternal(Duck duck)
  {
    return double.Parse(duck.DuckyProperty);
  }
}

public class BirdProcessorFactory
{
  public IBirdProcessor GetBirdProcessor(IBird bird)
  {
    if (bird.GetType().IsAssignableTo(typeof(Duck)))
      return new DuckProcessor();

    throw new Exception($"No processor for {bird.GetType().Name}");
  }
}

Though if you ever call the processor returned from the factory with a type that doesn't match, it will throw exceptions at you.

Please take a look at my solution for the problem:

public interface IBird
{

}

public class Duck : IBird
{
    public string DuckyProperty { get; set; } //this property is Duck specific!
}

public class Chicken : IBird
{
    public string ChickenProperty { get; set; } //this property is Chicken specific!
}

public interface IBirdProcessor<Y>
    where Y : IBird
{
    double GetBirdWeight(Y obj);
}

public class DuckProcessor : IBirdProcessor<Duck>
{

    public double GetBirdWeight(Duck duck)
    {
        return double.Parse(duck.DuckyProperty);
    }
}

public class ChickenProcessor : IBirdProcessor<Chicken>
{
    public double GetBirdWeight(Chicken chicken)
    {
        return double.Parse(chicken.ChickenProperty);
    }
}

public class BirdProcessorFactory
{
    public static IBirdProcessor<T> GetProcessor<T>(T bird)
        where T : IBird
    {
        switch (bird){
            case Chicken t1:
                return (IBirdProcessor<T>)new ChickenProcessor();
            case Duck t1:
                return (IBirdProcessor<T>)new DuckProcessor();
            default:
                throw new ArgumentException();
        }
        
    }
}

To show that it works as expected, I have added additional ChickenProcessor.

And the execution looks like next:

Duck duck = new Duck() { DuckyProperty = "23" };
Chicken chicken = new Chicken() { ChickenProperty = "25" };

var duckProvider = BirdProcessorFactory.GetProcessor(duck);
var chickenProvider = BirdProcessorFactory.GetProcessor(chicken);

duckProvider.GetBirdWeight(duck).Dump();
chickenProvider.GetBirdWeight(chicken).Dump();

And result is as expected:

enter image description here

Related