Alex Q
Alex Q

Reputation: 125

Covariance error in generically constrained class

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?

Upvotes: 5

Views: 270

Answers (2)

Eric Lippert
Eric Lippert

Reputation: 660038

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.

Upvotes: 13

Olha Shumeliuk
Olha Shumeliuk

Reputation: 740

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

Upvotes: -2

Related Questions