Overloaded Method-Group Argument Confuses Overload Resolution

Overloaded method-group argument confuses overload resolution?

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.

Disambiguating between overloaded methods passed as delegates in an overloaded call

There are several ways to disambiguate overload resolution of method groups.

Method 1: cast the method group

CallWithDelegate((SomeDelegateWithoutParameters)SomeOverloadedMethod);
CallWithDelegate((SomeDelegateWithParameter)SomeOverloadedMethod);

This disambiguates the overload. That's pretty uncommon syntax in the wild, but it works (C# 5 spec §6.6 Method group conversions):

As with all other implicit and explicit conversions, the cast operator can be used to explicitly perform a method group conversion.

[...]

Method groups may influence overload resolution, and participate in type inference.

Method 2: instantiate the delegate explicitly

CallWithDelegate(new SomeDelegateWithoutParameters(SomeOverloadedMethod));
CallWithDelegate(new SomeDelegateWithParameter(SomeOverloadedMethod));

This is the same as the previous method without the syntactic sugar. See the spec at §7.6.10.5 Delegate creation expressions for more details.

The binding-time processing of a delegate-creation-expression of the form new D(E), where D is a delegate-type and E is an expression, consists of the following steps:

  • If E is a method group, the delegate creation expression is processed in the same way as a method group conversion (§6.6) from E to D.

[...]

There's even an example closely related to your question:

As described above, when a delegate is created from a method group, the formal parameter list and return type of the delegate determine which of the overloaded methods to select. In the example

delegate double DoubleFunc(double x);

class A
{
DoubleFunc f = new DoubleFunc(Square);
static float Square(float x) {
return x * x;
}
static double Square(double x) {
return x * x;
}
}

the A.f field is initialized with a delegate that refers to the second Square method because that method exactly matches the formal parameter list and return type of DoubleFunc. Had the second Square method not been present, a compile-time error would have occurred.

Method 3: use a lambda

CallWithDelegate(() => SomeOverloadedMethod());
CallWithDelegate(i => SomeOverloadedMethod(i));
CallWithDelegate((int i) => SomeOverloadedMethod(i)); // Explicit types, if needed

This form is not ambiguous but it has an indirection (the lambda is called, and it then calls the target method). This may get optimized by the JIT though, and it most probably won't have a visible performance impact anyway.

Method 4: use anonymous delegates

CallWithDelegate(delegate() { SomeOverloadedMethod(); });
CallWithDelegate(delegate(int i) { SomeOverloadedMethod(i); });

This is equivalent to the lambda calls, but it uses the bulkier (and older) delegate syntax.


If you'd like to know the exact overload resolution rules, they're described in the spec in §7.5.3 Overload resolution.

Method overload resolution in java

The compiler will consider not a downcast, but an unboxing conversion for overload resolution. Here, the Integer i will be unboxed to an int successfully. The String method isn't considered because an Integer cannot be widened to a String. The only possible overload is the one that considers unboxing, so 8 is printed.

The reason that the first code's output is 10 is that the compiler will consider a widening reference conversion (Integer to Object) over an unboxing conversion.

Section 15.12.2 of the JLS, when considering which methods are applicable, states:

  1. The first phase (§15.12.2.2) performs overload resolution without permitting boxing or unboxing conversion, or the use of variable arity method invocation. If no applicable method is found during this phase then processing continues to the second phase.

 


  1. The second phase (§15.12.2.3) performs overload resolution while allowing boxing and unboxing [...]

Compiler error when replacing Lambda expression with method group

Essentially, this is an overload resolution issue.

Count has only one overload that takes two arguments (extended argument + predicate), but Where has two (one where the predicate considers the item-index, and one that doesn't). To complicate matters, Regex.IsMatch has multiple overloads of its own.
Now it turns out that the compiler is correct to complain about ambiguity since two of these overloads of IsMatch are genuinely applicable (each one is compatible with a different overload of Where):

// Where overload without item-index
Regex.IsMatch(string) is compatible with Where<string>(string, Func<string, bool>)

// Where overload with item-index
Regex.IsMatch(string, int) is compatible with Where<string>(string, Func<string, int, bool>)

...but there can be other related cases involving method-groups (when return-type analysis is required) where the compiler can complain about ambiguity even when there is no ambiguity for a human.

Weird extension method overload resolution

Oh, I got it after re-reading my own answer! Nice question =)
The overload does not work because it does not take constraint where T:IMaker into account while resolving overload (constraint is not a part of method signature). When you reference a parameter in lambda you (can) add a hint to the compiler:

  1. This works:

    new Container<A>().Foo(a => a.AProp == 0);

    because here we do hint that a:A;

  2. This does not work even with a reference to parameter:

    new Container<A>().Foo(a => a != null);

    because there is still not enough information to infer the type.

As far as I understand the specification, in "Foo scenario" the inference can fail on the second (Func) argument thus making the call ambiguous.

Here's what spec (25.6.4) says:

Type inference occurs as part of the compile-time processing of a method invocation (§14.5.5.1) and takes place before the overload resolution step of the invocation. When a particular method group is specified in a method invocation, and no type arguments are specified as part of the method invocation, type inference is applied to each generic method in the method group. If type inference succeeds, then the inferred type arguments are used to determine the types of arguments for subsequent overload resolution.

If overload resolution chooses a generic method as the one to invoke, then the inferred type arguments are used as the runtime type arguments for the invocation. If type inference for a particular method fails, that method does not participate in overload resolution. The failure of type inference, in and of itself, does not cause a compile-time error. However, it often leads to a compile-time error when overload resolution then fails to find any applicable methods.

Now lets get to pretty straightforward "Bar scenario". After type inference we will get only one method, because only one is applicable:

  1. Bar(Container<A>) for new Container<A>() (does not implement IMaker)
  2. Bar(A) for new A()(is not a Container)

And here's the ECMA-334 specification, just in case.
P.s. I'm not 100% sure that I got it right, but I prefer to think that I grasped the essential part.

Conflicting overloaded methods with optional parameters

From MSDN:

If two candidates are judged to be equally good, preference goes to a candidate that does not have optional parameters for which arguments were omitted in the call. This is a consequence of a general preference in overload resolution for candidates that have fewer parameters.

Compiler bug with overloaded function

This is, I suspect, to be expected.

The issue is that the compiler-intrinsic Round function returns a 64-bit integer. Both CodeInsight and the official documentation tells me that. And if the compiler has to chose between a routine taking a 32-bit integer or a double, when given a 64-bit integer, it chooses the routine accepting a double.

To verify this, try

procedure Test(x: Double); overload;
begin
ShowMessage('double');
end;

procedure Test(x: Integer); overload;
begin
ShowMessage('integer');
end;

procedure TForm5.FormCreate(Sender: TObject);
begin
Test(Int64.MaxValue)
end;


Related Topics



Leave a reply



Submit