1

I stumbled upon a case in where automatic type deduction of the .NET 4.0 MS-C# compiler failed and I had to specify the type "by hand".

This is not a big problem for me, but enough to get me curious why the compiler can not automatically find the correct types for the call..

I reduced the call down to the following programm:

class Program
{
    interface GenericInterface<T> { }
    interface Covariant<out T> { }

    static void Fn<T, U>(GenericInterface<T> t, U u)
        where T : Covariant<U>
    {
    }

    class Base { }
    class Derived : Base { }

    static void Main(string[] args)
    {
        Base b = null;
        Derived d = null;
        GenericInterface<Covariant<Base>> c = null;

        Fn(c, b);                           // 1
        Fn<Covariant<Base>, Base>(c, d);    // 2
        Fn(c, d);                           // 3
    }
}

The code does not compile because of the last call to Complex, marked "// 3".

The first call is easy and straight-forward - there is no base/subclass involved. The second call just specifies all parameter "by hand".

I would have expected that the compiler automatically choose the parameter used in the second for the third call as well. Sure, the second parameter is actually given as "Derived" but this can be converted into "Base" and the first parameter needs U to be of type "Base". The where-clause should still be possible with "Base" as U because of the covariant type in the interface.

I don't know exactly the rules for generic type parameter deduction in C#, but I always assumed, it works a bit like "If there is exactly one possible assignment for the parameters, use this one. If not, refuse to compile."

Why doesn't the compiler detect the types automatically? Is this one of these "if a compiler could do this, then it also would have to be able to solve Fermat's Last Theorem" - cases? :D

Imi
  • 1,579
  • 1
  • 12
  • 22
  • It might be working the other way around. `d` being of type `Derived` makes `U` be `Derived`. Then `T` becomes `Covariant`, which is not a subtype of `Covariant`. That is, the inference algorithm isn't necessarily, as you assume, "try out everything and see what fits", but rather some more straightforward approach. – millimoose Dec 04 '13 at 15:04
  • @millimoose : I do not assume its "try out everything" but rather "be extremely clever and find the only possible assignment in an very cool and almost magical way" ;). You are probably right for the reason why the deduction fails (the compiler nails `U` to be `Derived` and then refuses to continue). But I am interested in insights about the "why isn't the compiler be more clever?" Maybe its just "We try not being too clever and confusing in corner cases". Or "compiler writer are humans, after all..." Or maybe "Solve this and you shown that NP==P"? Any insights? – Imi Dec 04 '13 at 15:32

1 Answers1

2

The where-clause should still be possible with "Base" as U because of the covariant type in the interface.

I think this is where you've gone wrong. The compiler does not consider generic constraints to be part of the method signature.

So I think what might be happening in your third case is that the compiler is deducing T = Covariant<Base> and U = Derived. It then proceeds to check the generic constraint on T. Covariant<Base> doesn't implement Covariant<Derived> so the constraint is not satisfied and compilation fails.

Kyle
  • 6,500
  • 2
  • 31
  • 41
  • Yes, your analysis seems correct to me. For example, if I invert the covariant to an contravariant (`interface ...`), then it works and T=Derived and U=Base is used. Any ideas why C# does not consider the constraint when trying to find the generic types? (I can't get the idea behind the reasoning of the linked article. Sure, if the constraints are not part of the signature, and if type deduction only looks at the signature, then it must fail. But why shouldn't the constraints be part of the signature in the first place and everyone is happy?) – Imi Dec 04 '13 at 16:04
  • Well, I think they didn't design the generic constraints to be part of the signature (in the overloading sense) for the same reason that the return type isn't part of the signature. In certain cases it might be obvious which method overload to pick based on constraint parameters (or return type), but in general it probably isn't obvious and could potentially lead to very subtle bugs. It also leads to some weirdness where you could overload on constraints: `Foo( T x ) where T : A` and `Foo( T x )`. What is that supposed to mean? – Kyle Dec 04 '13 at 16:33
  • I never thought it is necessary to use the same definition of "what part of a function is considered" for method overloading and type deduction. But yes - its probably the case. After all, `T Foo() {...}` can not be automatically deduced at the moment either. – Imi Dec 05 '13 at 08:25