3

Whilst trying to get the C# compiler to do as much work as possible, I usually end up using (some might say abusing) genericity.

There is one particular situation I find very often and I'm not able to explain why. It would be great to have an explanation similar to Eric Lippert's brilliant answer to this similar - but not the same, as far as I can see - question: https://stackoverflow.com/a/17440148/257372

I have adapted the names of the real classes to use Animal so that it matches the answer above. I have also removed all methods and any other unnecessary details in order to keep things as simple as possible.

public interface IAnimal { }

public interface IAnimalOperationResult<out TAnimal> where TAnimal : IAnimal { }

public record DefaultSuccessfulResult<TAnimal>() : IAnimalOperationResult<TAnimal> where TAnimal : IAnimal;

public abstract class AnimalHandler<TAnimal, TSuccessfulAnimalOperationResult> where TAnimal : IAnimal
    where TSuccessfulAnimalOperationResult : IAnimalOperationResult<IAnimal> { }

// The compiler complains here with the following message:
// Error CS0311: The type 'DefaultSuccessfulResult<TAnimal>' cannot be used as type parameter 'TSuccessfulAnimalOperationResult' in the generic type or method 'AnimalHandler<TAnimal, TSuccessfulAnimalOperationResult>'.
// There is no implicit reference conversion from 'DefaultSuccessfulResult<TAnimal>' to 'IAnimalOperationResult<IAnimal>'
public class AnimalHandlerWithDefaultSuccessfulResult<TAnimal> : AnimalHandler<TAnimal, DefaultSuccessfulResult<TAnimal>>
    where TAnimal : IAnimal { }

The error message says There is no implicit reference conversion from 'DefaultSuccessfulResult<TAnimal>' to 'IAnimalOperationResult<IAnimal>'

Which, according to the compiler, is not true, since it accepts the following code:

public record Dog() : IAnimal;

[Fact]
public void CanAssignValues()
{
    DefaultSuccessfulResult<Dog> source = new();

    // This assignment requires the same implicit reference conversion the compiler claims doesn't exist.
    // However, in this instance, the compiler accepts it.
    IAnimalOperationResult<IAnimal> target = source;
}

I'm obviously missing something, but what?

Rodolfo Grave
  • 1,148
  • 12
  • 24
  • In case it's not clear why the constraint described in the answer is necessary: if as noted in the answer you made a struct type that implemented IAnimal, then in order to use TAnimal in a context where IAnimal is needed, the compiler would have to emit a boxing instruction somewhere... but where? If the conversion you want succeeds then the compiler has no ability to know where in the code the box is required. – Eric Lippert Dec 07 '22 at 23:33
  • 1
    Note also here that the error message correctly describes the problem using the jargon of the specification -- indeed, there is no *implicit reference conversion*. But it fails utterly to phrase the error message in a manner that leads the reader to know what code needs to change. This is a particularly tricky problem in error message design and I regret that we did not have time during my tenure to improve it. And apparently it has not been improved in the ten years since! – Eric Lippert Dec 07 '22 at 23:35

1 Answers1

6

In short - add the class generic constraint:

public class AnimalHandlerWithDefaultSuccessfulResult<TAnimal> : AnimalHandler<TAnimal, DefaultSuccessfulResult<TAnimal>>
    where TAnimal : class, IAnimal { }

The reason being that variance in C# is supported only for reference types. From the docs on generics variance:

Variance applies only to reference types; if you specify a value type for a variant type parameter, that type parameter is invariant for the resulting constructed type.

You will be able to "confirm" the case by changing Dog record to be a value type (using C# 10's record structs):

public record struct Dog() : IAnimal; 

DefaultSuccessfulResult<Dog> source = new();
// Following produces compiler error:
// Cannot implicitly convert type 'DefaultSuccessfulResult<Dog>' to 'IAnimalOperationResult<IAnimal>'. 
// An explicit conversion exists (are you missing a cast?)
IAnimalOperationResult<IAnimal> target = source;
Guru Stron
  • 102,774
  • 10
  • 95
  • 132