Overloaded method-group argument confuses overload resolution?
Asked Answered
C

2

8

The following call to the overloaded Enumerable.Select method:

var itemOnlyOneTuples = "test".Select<char, Tuple<char>>(Tuple.Create);

fails with an ambiguity error (namespaces removed for clarity):

The call is ambiguous between the following methods or properties: 
'Enumerable.Select<char,Tuple<char>>
           (IEnumerable<char>,Func<char,Tuple<char>>)'
and 
'Enumerable.Select<char,Tuple<char>>
          (IEnumerable<char>, Func<char,int,Tuple<char>>)'

I can certainly understand why not specifying the type-arguments explicitly would result in an ambiguity (both the overloads would apply), but I don't see one after doing so.

It appears clear enough to me that the intention is to call the first overload, with the method-group argument resolving to Tuple.Create<char>(char). The second overload should not apply because none of the Tuple.Create overloads can be converted to the expected Func<char,int,Tuple<char>> type. I'm guessing the compiler is confused by Tuple.Create<char, int>(char, int), but its return-type is wrong: it returns a two-tuple, and is hence not convertible to the relevant Func type.

By the way, any of the following makes the compiler happy:

  1. Specifying a type-argument for the method-group argument: Tuple.Create<char> (Perhaps this is actually a type-inference issue?).
  2. Making the argument a lambda-expression instead of a method-group: x => Tuple.Create(x). (Plays well with type-inference on the Select call).

Unsurprisingly, trying to call the other overload of Select in this manner also fails:

var itemIndexTwoTuples = "test".Select<char, Tuple<char, int>>(Tuple.Create);

What's the exact problem here?

Clown answered 5/3, 2011 at 12:25 Comment(0)
E
20

First off, I note that this is a duplicate of:

Why is Func<T> ambiguous with Func<IEnumerable<T>>?

What's the exact problem here?

Thomas's guess is essentially correct. Here are the exact details.

Let's go through it a step at a time. We have an invocation:

"test".Select<char, Tuple<char>>(Tuple.Create); 

Overload resolution must determine the meaning of the call to Select. There is no method "Select" on string or any base class of string, so this must be an extension method.

There are a number of possible extension methods for the candidate set because string is convertible to IEnumerable<char> and presumably there is a using System.Linq; in there somewhere. There are many extension methods that match the pattern "Select, generic arity two, takes an IEnumerable<char> as the first argument when constructed with the given method type arguments".

In particular, two of the candidates are:

Enumerable.Select<char,Tuple<char>>(IEnumerable<char>,Func<char,Tuple<char>>)
Enumerable.Select<char,Tuple<char>>(IEnumerable<char>,Func<char,int,Tuple<char>>) 

Now, the first question we face is are the candidates applicable? That is, is there an implicit conversion from each supplied argument to the corresponding formal parameter type?

An excellent question. Clearly the first argument will be the "receiver", a string, and it will be implicitly convertible to IEnumerable<char>. The question now is whether the second argument, the method group "Tuple.Create", is implicitly convertible to formal parameter types Func<char,Tuple<char>>, and Func<char,int, Tuple<char>>.

When is a method group convertible to a given delegate type? A method group is convertible to a delegate type when overload resolution would have succeeded given arguments of the same types as the delegate's formal parameter types.

That is, M is convertible to Func<A, R> if overload resolution on a call of the form M(someA) would have succeeded, given an expression 'someA' of type 'A'.

Would overload resolution have succeeded on a call to Tuple.Create(someChar)? Yes; overload resolution would have chosen Tuple.Create<char>(char).

Would overload resolution have succeeded on a call to Tuple.Create(someChar, someInt)? Yes, overload resolution would have chosen Tuple.Create<char,int>(char, int).

Since in both cases overload resolution would have succeeded, the method group is convertible to both delegate types. The fact that the return type of one of the methods would not have matched the return type of the delegate is irrelevant; overload resolution does not succeed or fail based on return type analysis.

One might reasonably say that convertibility from method groups to delegate types ought to succeed or fail based on return type analysis, but that's not how the language is specified; the language is specified to use overload resolution as the test for method group conversion, and I think that's a reasonable choice.

Therefore we have two applicable candidates. Is there any way that we can decide which is better than the other? The spec states that the conversion to the more specific type is better; if you have

void M(string s) {}
void M(object o) {}
...
M(null);

then overload resolution chooses the string version because string is more specific than object. Is one of those delegate types more specific than the other? No. Neither is more specific than the other. (This is a simplification of the better-conversion rules; there are actually lots of tiebreakers, but none of them apply here.)

Therefore there is no basis to prefer one over the other.

Again, one could reasonably say that sure, there is a basis, namely, that one of those conversions would produce a delegate return type mismatch error and one of them would not. Again, though, the language is specified to reason about betterness by considering the relationships between the formal parameter types, and not about whether the conversion you've chosen will eventually result in an error.

Since there is no basis upon which to prefer one over the other, this is an ambiguity error.

It is easy to construct similar ambiguity errors. For example:

void M(Func<int, int> f){}
void M(Expression<Func<int, int>> ex) {}
...
M(x=>Q(++x));

That's ambiguous. Even though it is illegal to have a ++ inside an expression tree, the convertibility logic does not consider whether the body of a lambda has something inside it that would be illegal in an expression tree. The conversion logic just makes sure that the types check out, and they do. Given that, there's no reason to prefer one of the M's over the other, so this is an ambiguity.

You note that

"test".Select<char, Tuple<char>>(Tuple.Create<char>); 

succeeds. You now know why. Overload resolution must determine if

Tuple.Create<char>(someChar)

or

Tuple.Create<char>(someChar, someInt)

would succeed. Since the first one does and the second one does not, the second candidate is inapplicable and eliminated, and is therefore not around to become ambiguous.

You also note that

"test".Select<char, Tuple<char>>(x=>Tuple.Create(x)); 

is unambiguous. Lambda conversions do take into account the compatibility of the returned expression's type with the target delegate's return type. It is unfortunate that method groups and lambda expressions use two subtly different algorithms for determining convertibility, but we're stuck with it now. Remember, method group conversions have been in the language a lot longer than lambda conversions; had they been added at the same time, I imagine that their rules would have been made consistent.

Emotion answered 9/3, 2011 at 22:40 Comment(4)
Eric, the statement that "we're stuck with [two subtly different algorithms]" for convertibility suggests that there is a backwards compatibility consideration at play here, yes? Presumably allowing method group conversions to use return type analysis for resolution would create situations where existing code would result in a different overload choice. However, I can't construct a scenario where this would actually happen - is there actually such a case? Or is this more a matter of balancing risk/reward for a low-value use case?Levesque
I really appreciate the detail in this answer; I think I have a slightly better grasp of this now. I actually half-expected my code to work because I was under the impression that the return-type of method-groups were given more 'attention' in C# 4, but presumably that's only for type-inference? Anyway, for me, overload resolution and type-inference have a bit of black magic about them; my intuition lets me down quite often.Clown
On another note, I find it slightly puzzling that C# tends to consistently separate the following two aspects so strongly: a) what might have the user meant b) is the thing the user might have meant legal. Is it wrong to want b) to play a bigger role in a)?Clown
@Ani: return types of method groups are now used in method type inference in some cases. For example, if you have methods M<T, U>(T t, Func<T, U>) and int N(string), and you say M("hello", N) then first we infer that T is string, then we do overload resolution on N(someString) and it succeeds, and then we infer that U is int.Emotion
P
5

I'm guessing the compiler is confused by Tuple.Create<char, int>(char, int), but its return-type is wrong: it returns a two-tuple.

The return type isn't part of the method signature, so it isn't considered during overload resolution; it's only verified after an overload has been picked. So as far as the compiler knows, Tuple.Create<char, int>(char, int) is a valid candidate, and it is neither better nor worse than Tuple.Create<char>(char), so the compiler can't decide.

Polygnotus answered 5/3, 2011 at 13:49 Comment(9)
Thanks, that sounds very plausible. Do you have any references to confirm this?Clown
It is not just plausible, it is accurate. References are difficult at SO, votes are anonymous.Detrital
@Hans: Sorry, didn't understand your comment. I was talking about documentation, if that wasn't clear. What's your point?Clown
@Ani, look at the C# 4 specs, §7.5.3.1 (Applicable function member). None of the rules mention anything about the return type, the compiler only checks that the provided arguments match the declared parameters. In your case, both overloads are applicable. The next section (§7.5.3.2) explains the rules used to determine the better function member, and again, the return type is not consideredPolygnotus
@Thomas: That's quite vague. Can you explain more clearly? Note that none of the overloads of Tuple.Create can be converted to Func<char,int,Tuple<char>>, so just from the sentence "the compiler only checks that the provided arguments match the declared parameters", it would appear that the second overload of Select should no longer be an applicable function member for the call. Obviously, what I just said is wrong, but your answer doesn't appear to address it (in a way that I understand).Clown
@Ani: Actually, in your case there are two overload resolutions involved: one to resolve the correct Tuple.Create overload, and another to resolve the correct Enumerable.Select overload. Of course the outcome of the second depends on the outcome of the first. Sorry, I'm not very good at explaining the spec (I don't understand it very well myself)... with some luck, Jon Skeet or Eric Lippert will see your question and answer it ;)Polygnotus
@Thomas: You're right about the two resolutions (I don't know whether finding the best applicable overload to use when converting a method-group to a delegate is also called "overload resolution"). Anyway, I appreciate your answer. I'm not looking for anything precise to the T, but there's probably a more accurate way of describing the problem.Clown
@Ani: Have a look at msmvps.com/blogs/jon_skeet/archive/2010/10/28/… - and then hold your nose before reading msmvps.com/blogs/jon_skeet/archive/2010/11/02/…Dichroscope
@Jon: Thanks, but how do those posts relate? There are no constraints involved here..Clown

© 2022 - 2024 — McMap. All rights reserved.