Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does the variance of a class type parameter have to match the variance of its methods' return/argument type parameters?

The following raises complaints:

interface IInvariant<TInv> {}
interface ICovariant<out TCov> {
    IInvariant<TCov> M(); // The covariant type parameter `TCov'
                          // must be invariantly valid on
                          // `ICovariant<TCov>.M()'
}
interface IContravariant<in TCon> {
    void M(IInvariant<TCon> v); // The contravariant type parameter
                                // `TCon' must be invariantly valid
                                // on `IContravariant<TCon>.M()'
}

but I can't imagine where this wouldn't be type-safe. (snip*) Is this the reason why this is disallowed, or is there some other case which violates type safety which I'm not aware of?


* My initial thoughts were admittedly convoluted, but despite this, the responses are very thorough, and @Theodoros Chatzigiannakis even dissected my initial assumptions with impressive accuracy.

Alongside a good slap from retrospect, I realize that I had falsely assumed that the type signature of ICovariant::M remains a Func<IInvariant<Derived>> when its ICovariant<Derived> is assigned to a ICovariant<Base>. Then, assigning that M to Func<IInvariant<Base>> would look fine coming from an ICovariant<Base>, but would of course be illegal. Why not just ban this last, obviously-illegal cast? (so I thought)

I feel this false and tangential guess detracts from the question, as Eric Lippert also points out, but for historical purposes, the snipped part:

The most intuitive explanation to me is that, taking ICovariant as an example, the covariant TCov implies that the method IInvariant<TCov> M() could be cast to some IInvariant<TSuper> M() where TSuper super TCov, which violates the invariance of TInv in IInvariant. However, this implication doesn't seem necessary: the invariance of IInvariant on TInv could easily be enforced by disallowing the cast of M.

like image 649
concat Avatar asked May 17 '16 14:05

concat


People also ask

What variance is imposed on generic type parameters?

A generic interface or generic delegate type can have both covariant and contravariant type parameters. 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.

What is type variance?

In short, type variance describes the types that may be substituted in place of another type. Covariance: We say a substitution is covariant with a type, Foo, if Foo or any other class with a subclass relationship to Foo is valid.

What is co variance and contra variance in generics?

Covariance: Covariance lets you to pass a derived type object where a base type object is expected Covariance can be applied on delegate, generic, array, interface, etc. Contravariance: Contravariance is applied to parameters.

What is variance in Scala?

Variance is the interconnection of Sub-Typing relationships which are either of complicated types or of their constituent types. Variance explains inheritance correlation of Types that have parameters or arguments within them. These types belongs to the generic classes, which takes a type like a parameter.


1 Answers

Let's look at a more concrete example. We'll make a couple implementations of these interfaces:

class InvariantImpl<T> : IInvariant<T>
{
}

class CovariantImpl<T> : ICovariant<T>
{
    public IInvariant<T> M()
    {
        return new InvariantImpl<T>();
    }
}

Now, let's assume that the compiler didn't complain about this and try to use it in a simple way:

static IInvariant<object> Foo( ICovariant<object> o )
{
    return o.M();
}

So far so good. o is ICovariant<object> and that interface guarantees that we have a method that can return an IInvariant<object>. We don't have to perform any casts or conversions here, everything is fine. Now let's call the method:

var x = Foo( new CovariantImpl<string>() );

Because ICovariant is covariant, this is a valid method call, we can substitute an ICovariant<string> wherever something wants an ICovariant<object> because of that covariance.

But we have a problem. Inside Foo, we call ICovariant<object>.M() and expect it to return an IInvariant<object> because that's what the ICovariant interface says it will do. But it can't do that, because the actual implementation we've passed actually implements ICovariant<string> and its M method returns IInvariant<string>, which has nothing to do with IInvariant<object> due to the invariance of that interface. They are completely different types.

like image 89
Kyle Avatar answered Nov 16 '22 00:11

Kyle