2
votes

I started off with a simple generic interface:

interface IFooContext<TObject>
{
    TObject Value { get; }

    String DoSomething<TValue>( Expression<Func<TObject,TValue>> lambdaExpression );
}

// Usage:
IFooContext<Panda> ctx = ...
String str = ctx.DoSomething( panda => panda.EatsShootsAndLeaves );

However I needed to make this interface's generic type covariant (for reasons I won't go into), however this causes a compiler error because Func<T0,TReturn> requires T0 to be contravariant (in T0) or invariant parameter:

interface IFooContext<out TObject>
{
    TObject Value { get; }

    String DoSomething<TValue>( Expression<Func<TObject,TValue>> lambdaExpression );
}

// Intended usage:
IFooContext<Panda> ctx1 = ...
IFooContext<Ursidae> ctx2 = ctx1; // yay for covariance!
String str = ctx2.DoSomething( bear => bear.PoopsInTheWoods );

So I get this compiler error for the DoSomething declaration:

Error CS1961 Invalid variance: The type parameter 'TObject' must be invariantly valid on 'IFooContext<TObject>.DoSomething<TValue>(Expression<Func<TObject, TValue>>)'. 'TObject' is covariant.

After throwing various ideas at the wall I found out that I can work-around this by moving DoSomething to a non-generic interface and have its TObject parameter specified on the method, then "expose" the originally intended method as an extension method like so:

interface IFooContext
{
    String DoSomething<TObject,TValue>( Expression<Func<TObject,TValue>> lambdaExpression );
}

interface IFooContext<TObject>
{
    TObject Value { get; }
}

public static class FooContextExtensions
{
    public static String DoSomething<TObject,TValue>( this IFooContext<TObject> context, Expression<Func<TObject,TValue>> lambdaExpression )
    {
        return context.DoSomething<TObject,Value>( lambdaExpression );
    }
}

// Actual usage:
IFooContext<Panda> ctx1 = ...
IFooContext<Ursidae> ctx2 = ctx1; // yay for covariance!
String str = ctx2.DoSomething( bear => bear.PoopsInTheWoods );

And this compiles and runs without any problems - and the syntax of the actual usage is identical to that of the intended usage of my earlier sample.

Why does this work and why can't the C# compiler do this trick for me internally with my original single covariant generic interface?

1
@MichaelRandall Given that variance constraints apply only to generic interfaces, not generic classes (i.e. the implementations) the "contract" is the same to the implementer (when the implementer itself is an open generic type) - only the contract to the consumer is different. - Dai
Yeah, after about 10 seconds of thought i retracted the comment, id be interested in the technical explanation myself - TheGeneral

1 Answers

0
votes

Because the TObject used by method is inherited from the interface. So when the generic interface type is constructed, TObject becomes a fixed type to the method.

For example, if the interface is constructed as IFoo<Chrome>, then DoSomething will expect Expression<Func<Chrome, TValue>> to be passed in.

Converting the interface to IFoo<Browser> will not change the constructed method above. And passing an instance of Browser to a method expecting Chrome is not allowed, so that’s why the compiler throws an error.

However, if you move definition of TObject to method, it will not be constructed until the method is called. i.e. if you pass Browser in the method is constructed using Browser. So you don’t have an obvious problem which the compiler would figure out. If your method implementation is expecting Chrome over other browsers, you will get a runtime exception.

Though the syntax looks the same, the compiled code is not under the hood.