Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic interface type inference weirdness in c#

C# cannot infer a type argument in this pretty obvious case:

public void Test<T>(IEnumerable<KeyValuePair<string, T>> kvp)
{
    Console.WriteLine(kvp.GetType().Name + ": KeyValues");
}

Test(new Newtonsoft.Json.Linq.JObject());

The JObject type clearly implements IEnumerable<KeyValuePair<string, JToken>>, but I get the following error:

CS0411: The type arguments for method cannot be inferred from the usage.

Why does this happen?

UPD: To the editor who marked this question as duplicate: please note how my method's signature accepts not IEnumerable<T>, but IEnumerable<KeyValuePair<string, T>>. The JObject type implements IEnumerable twice, but only one of the implementations matches this constraint - so there should be no ambiguity.

UPD: Here's a complete self-contained repro without JObject: https://gist.github.com/impworks/2eee2cd0364815ab8245b81963934642

like image 702
Impworks Avatar asked May 17 '18 11:05

Impworks


1 Answers

Here's a simpler repro:

interface I<T> {}
class X<T> {}
class Y {}
class Z {}  
class C : I<X<Y>>, I<Z> {}
public class P
{   
    static void M<T>(I<X<T>> i) { }
    public static void Main()
    {
        M(new C());
    }
}

Type inference fails. You ask why, and why questions are always difficult to answer, so let me rephrase the question:

What line of the specification disallows this inference?

I have my copy of the C# 3 specification at hand; the line there is as follows

  • V is the type we are inferring to, so I<X<T>> in this case
  • U is the type we are inferring from, so C in this case

This will be slightly different in C# 4 because I added covariance, but we can ignore that for the purposes of this discussion.

... if V is a constructed type C<V1, … Vk> and there is a unique set of types U1, … Uk such that an implicit conversion exists from U to C<U1, … Uk> then an exact inference is made from each Ui to the corresponding Vi. Otherwise no inferences are made.

Notice the word unique in there. There is NOT a unique set of types such that C is convertible to I<Something> because both X<Y> and Z are valid.

Here's another non-why question:

What factors were considered when the design team made this decision?

You are right that in theory we could detect in your case that X<Y> is intended and Z is not. If you would care to propose a type inference algorithm that can handle non-unique situations like this that never makes a mistake -- remember, Z could be a subtype or a supertype of X<Y> or X<Something Else> and I could be covariant -- then I am sure that the C# team would be happy to consider your proposal.

We had that argument in 2005 when designing the C# 3 type inference algorithm and decided that scenarios where one class implements two of the same interface were rare, and that dealing with those rare situations created considerable complications in the language. Those complications would be expensive to design, specify, implement and test, and we had other things to spend money and effort on that would have bigger impact.

Also, we did not know when we made C# 3 whether or not we would be adding covariance in C# 4. We never want to introduce a new language feature that makes a possible future language feature impossible or difficult. It is better to put restrictions in the language now and consider removing them later than to do a lot of work for a rare scenario that makes a common scenario difficult in the next version.

The fact that I helped design this algorithm and implemented it multiple times, and completely did not remember this rule at first should tell you how often this has come up in the last 13 years. Hardly at all. It is very rare for someone to be in your particular boat, and there is an easy workaround: specify the type.

A question which you did not ask but comes to mind:

Could the error message be better?

Yep. I apologize for that. I did a lot of work making overload resolution error messages more descriptive for common LINQ scenarios, and I always wanted to go back and make the other type inference error messages more clear. I wrote the type inference and overload resolution code to maintain internal information explaining why a type had been inferred or an overload had been chosen or rejected, both for my own debugging purposes and to make better error messages, but I never got around to exposing that information to the user. There was always something higher priority.

You might consider entering an issue at the Roslyn GitHub site suggesting that this error message be improved to help users diagnose the situation more easily. Again, the fact that I didn't immediately diagnose the problem and had to go back to the spec is indicative that the message is unclear.

like image 144
Eric Lippert Avatar answered Oct 13 '22 21:10

Eric Lippert