Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why doesn't the C# compiler consider this generic type inference ambiguous?

Given the following class:

public static class EnumHelper
{
    //Overload 1
    public static List<string> GetStrings<TEnum>(TEnum value)
    {
        return EnumHelper<TEnum>.GetStrings(value);
    }

    //Overload 2
    public static List<string> GetStrings<TEnum>(IEnumerable<TEnum> value)
    {
        return EnumHelper<TEnum>.GetStrings(value);
    }
}

What rules are applied to select one of its two generic methods? For example, in the following code:

List<MyEnum> list;
EnumHelper.GetStrings(list);

it ends up calling EnumHelper.GetStrings<List<MyEnum>>(List<MyEnum>) (i.e. Overload 1), even though it seems just as valid to call EnumHelper.GetStrings<MyEnum>(IEnumerable<MyEnum>) (i.e. Overload 2).

For example, if I remove overload 1 entirely, then the call still compiles fine, instead choosing the method marked as overload 2. This seems to make generic type inference kind of dangerous, as it was calling a method which intuitively seems like a worse match. I'm passing a List/Enumerable as the type, which seems very specific and seems like it should match a method with a similar parameter (IEnumerable<TEnum>), but it's choosing the method with the more generic, generic parameter (TEnum value).

like image 209
Triynko Avatar asked May 18 '18 06:05

Triynko


1 Answers

What rules are applied to select one of its two generic methods?

The rules in the specification - which are extremely complex, unfortunately. In the ECMA C# 5 standard, the relevant bit starts at section 12.6.4.3 ("better function member").

However, in this case it's relatively simple. Both methods are applicable, with type inference occurring separately for each method:

  • For method 1, TEnum is inferred to be List<MyEnum>
  • For method 2, TEnum is inferred to be MyEnum

Next the compiler starts checking the conversions from arguments to parameters, to see whether one conversion is "better" than the other. That goes into section 12.6.4.4 ("better conversion from expression").

At this point we're considering these conversions:

  • Overload 1: List<MyEnum> to List<MyEnum> (as TEnum is inferred to be List<MyEnum>)
  • Overload 2: List<MyEnum> to IEnumerable<MyEnum> (as TEnum is inferred to be MyEnum)

Fortunately, the very first rule helps us here:

Given an implicit conversion C1 that converts from an expression E to a type T1, and an implicit conversion C2 that converts from an expression E to a type T2, C1 is a better conversion than C2 if at least one of the following holds:

  • E has a type S and an identity conversion exists from S to T1 but not from S to T2

There is an identity conversion from List<MyEnum> to List<MyEnum>, but there isn't an identity conversion from List<MyEnum> to IEnumerable<MyEnum>, therefore the first conversion is better.

There aren't any other conversions to consider, therefore overload 1 is seen as the better function member.

Your argument about "more general" vs "more specific" parameters would be valid if this earlier phase had ended in a tie-break, but it doesn't: "better conversion" for arguments to parameters is considered before "more specific parameters".

In general, both overload resolution is incredibly complicated. It has to take into account inheritance, generics, type-less arguments (e.g. the null literal, the default literal, anonymous functions), parameter arrays, all the possible conversions. Almost any time a new feature is added to C#, it affects overload resolution :(

like image 170
Jon Skeet Avatar answered Nov 14 '22 23:11

Jon Skeet