5

I have an interface with a covariant type parameter:

interface I<out T>
{
    T Value { get; }
}

Additionally, I have a non-generic base class and another deriving from it:

class Base
{
}

class Derived : Base
{
}

Covariance says that an I<Derived> can be assigned to an I<Base>, and indeed I<Base> ib = default(I<Derived>); compiles just fine.

However, this behavior apparently changes with generic parameters with an inheritance constraint:

class Foo<TDerived, TBase>
    where TDerived : TBase
{
    void Bar()
    {
        I<Base> ib = default(I<Derived>); // Compiles fine
        I<TBase> itb = default(I<TDerived>); // Compiler error: Cannot implicitly convert type 'I<TDerived>' to 'I<TBase>'. An explicit conversion exists (are you missing a cast?)
    }
}

Why are these two cases not treated the same?

Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
Alex Q
  • 125
  • 6
  • 1
    If you say that `TDerived` is also a `class`, then it compiles. – Lasse V. Karlsen Jun 19 '18 at 18:55
  • That constraint is quite weak... using generics you could specify a reference type for `TDerived` and a value type for `TBase` (definitely won't compile). You cannot guarantee the compiler that reference types will always be used. Use a class constraint for that – ethane Jun 19 '18 at 18:57
  • @ethane Care to show an example of which reference type and value type it would compile for? – Lasse V. Karlsen Jun 19 '18 at 18:59
  • @LasseVågsætherKarlsen, yeah let me just hope on to my magical machine and do it. Read carefully. – ethane Jun 19 '18 at 19:03
  • But if it won't compile, isn't the constraint in fact *strong*? And it's me that should hop on my magical (time) machine because you edited your comment to add "(definitely won't compile)." and everything that follows after I started writing my comment. – Lasse V. Karlsen Jun 19 '18 at 19:06
  • @LasseVågsætherKarlsen. It is weak because you are, unknowingly, relying on the compiler to prevent unexpected behaviour. The compiler enforces this 'strength' you speak of and has to stop this type of misuse. – ethane Jun 19 '18 at 19:09

2 Answers2

13

Covariance says that an I<Derived> can be assigned to an I<Base>

Correct.

Why are these two cases not treated the same?

You are overgeneralizing from your statement. Your logic seems to go like this:

  • Covariance says that an I<Derived> can be assigned to an I<Base>
  • Derived and Base are arbitrary types that have a supertype-subtype relationship.
  • Therefore covariance works with any types that have a supertype-subtype relationship.
  • Therefore covariance works with generic type parameters constrained to have such a relationship.

Though plausible, that chain of logic is wrong. The correct chain of logic is:

  • Covariance says that an I<Derived> can be assigned to an I<Base>
  • Derived and Base are arbitrary reference types that have a superclass-subclass relationship.
  • Therefore covariance works with any reference types that have a superclass-subclass relationship.
  • Therefore covariance works with generic type parameters that are constrained to be reference types that have such a relationship.

In your example one would be perfectly within rights to make a Foo<int, object>. Since I<int> cannot be converted to I<object>, the compiler rejects a conversion from I<TDerived> to I<TBase>. Remember, generic methods must be proved by the compiler to work for all possible constructions, not just the constructions you make. Generics are not templates.

The error message could be more clear, I agree. I apologize for the poor experience. Improving that error was on my list, and I never got to it.

You should put class constraints on your generic type parameters, and then it will work as you expect.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • 2
    +1 I was not aware that variance only applies reference types or the reason why. I was also not aware that the `class` constraint is really restraining to reference types (including interfaces, thus structs are indirectly supported via boxing). – Alex Q Jun 19 '18 at 19:22
  • @AlexQ: Well then you learned a few important things today, so that's awesome. Variance only applies to reference types because of boxing; if you have `interface I { T M(); }` and `C : I { public int M() { return 1; } }` and `I i = new C();` were legal, then **who does the boxing** when you call `object x = i.M();` ? `C.M` returns an int, not a boxed int, but it has to be boxed by the time that the result is assigned to x, and there is no way that the compiler could know that a boxing instruction needs to go in there *sometimes*. – Eric Lippert Jun 19 '18 at 19:33
  • @AlexQ: I've always found it vexing that `class` as a constraint means "class, interface, delegate or array" and `struct` means "struct or enum" but not "nullable value type". It would have been clearer to make the constraints `valuetype` and `referencetype` for instance. – Eric Lippert Jun 19 '18 at 19:35
  • 1
    Yes I found another answer that helped by Jon Skeet (https://stackoverflow.com/questions/12454794/why-covariance-and-contravariance-do-not-support-value-type) in which he linked a blog post of yours (https://blogs.msdn.microsoft.com/ericlippert/2009/03/19/representation-and-identity/). Definitely a good read for those wondering about the deeper reasoning. I was also going to say that class seems like the wrong name for the constraint and also suggest the same keywords ^_^ – Alex Q Jun 19 '18 at 19:38
  • that's true nice solution – ArunPratap Jun 20 '18 at 08:46
-2

Generic params TDerived, TBase has no referenes to the classes you created Derived and Base. It's just aliases for any classes which has the same relation you described in where clause

Olha Shumeliuk
  • 720
  • 7
  • 14
  • 2
    So if they have the same relationship, shouldn't that mean that the code **should** compile? Bear in mind that since he hasn't added a constraint to `class`, the relationship might not be classes at all, it could be an interface and a struct. – Lasse V. Karlsen Jun 19 '18 at 18:54