Search code examples
c#genericsinterfacecastingfactory

Type parameter must be contravariantly valid


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?


Solution

  • 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.