Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 876
Alex Q Avatar asked Jun 19 '18 18:06

Alex Q


1 Answers

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.

like image 132
Eric Lippert Avatar answered Nov 19 '22 12:11

Eric Lippert