Compiler Ambiguous invocation error - anonymous method and method group with Func<> or Action
Asked Answered
E

4

104

I have a scenario where I want to use method group syntax rather than anonymous methods (or lambda syntax) for calling a function.

The function has two overloads, one that takes an Action, the other takes a Func<string>.

I can happily call the two overloads using anonymous methods (or lambda syntax), but get a compiler error of Ambiguous invocation if I use method group syntax. I can workaround by explicit casting to Action or Func<string>, but don't think this should be necessary.

Can anyone explain why the explicit casts should be required.

Code sample below.

class Program
{
    static void Main(string[] args)
    {
        ClassWithSimpleMethods classWithSimpleMethods = new ClassWithSimpleMethods();
        ClassWithDelegateMethods classWithDelegateMethods = new ClassWithDelegateMethods();

        // These both compile (lambda syntax)
        classWithDelegateMethods.Method(() => classWithSimpleMethods.GetString());
        classWithDelegateMethods.Method(() => classWithSimpleMethods.DoNothing());

        // These also compile (method group with explicit cast)
        classWithDelegateMethods.Method((Func<string>)classWithSimpleMethods.GetString);
        classWithDelegateMethods.Method((Action)classWithSimpleMethods.DoNothing);

        // These both error with "Ambiguous invocation" (method group)
        classWithDelegateMethods.Method(classWithSimpleMethods.GetString);
        classWithDelegateMethods.Method(classWithSimpleMethods.DoNothing);
    }
}

class ClassWithDelegateMethods
{
    public void Method(Func<string> func) { /* do something */ }
    public void Method(Action action) { /* do something */ }
}

class ClassWithSimpleMethods
{
    public string GetString() { return ""; }
    public void DoNothing() { }
}

C# 7.3 Update

As per 0xcde's comment below on March 20 2019 (nine years after I posted this question!), this code compiles as of C# 7.3 thanks to improved overload candidates.

Euton answered 13/1, 2010 at 14:5 Comment(4)
I have tried your code and I'm getting an additional compile time error: 'void test.ClassWithSimpleMethods.DoNothing()' has the wrong return type (which is on line 25, which is where the ambiguity error is)Tully
@Matt: I see that error as well. The errors I cited in my post were the compilation issues that VS highlights before you even try a full compile.Euton
By the way, this was a great question. I love anything which forces me into the specs :)Roaster
Note that your sample code will compile if you use C# 7.3 (<LangVersion>7.3</LangVersion>) or later thanks to improved overload candidates.Carpal
F
100

First off, let me just say that Jon's answer is correct. This is one of the hairiest parts of the spec, so good on Jon for diving into it head first.

Second, let me say that this line:

An implicit conversion exists from a method group to a compatible delegate type

(emphasis added) is deeply misleading and unfortunate. I'll have a talk with Mads about getting the word "compatible" removed here.

The reason this is misleading and unfortunate is because it looks like this is calling out to section 15.2, "Delegate compatibility". Section 15.2 described the compatibility relationship between methods and delegate types, but this is a question of convertibility of method groups and delegate types, which is different.

Now that we've got that out of the way, we can walk through section 6.6 of the spec and see what we get.

To do overload resolution we need to first determine which overloads are applicable candidates. A candidate is applicable if all the arguments are implicitly convertible to the formal parameter types. Consider this simplified version of your program:

class Program
{
    delegate void D1();
    delegate string D2();
    static string X() { return null; }
    static void Y(D1 d1) {}
    static void Y(D2 d2) {}
    static void Main()
    {
        Y(X);
    }
}

So let's go through it line by line.

An implicit conversion exists from a method group to a compatible delegate type.

I've already discussed how the word "compatible" is unfortunate here. Moving on. We are wondering when doing overload resolution on Y(X), does method group X convert to D1? Does it convert to D2?

Given a delegate type D and an expression E that is classified as a method group, an implicit conversion exists from E to D if E contains at least one method that is applicable [...] to an argument list constructed by use of the parameter types and modifiers of D, as described in the following.

So far so good. X might contain a method that is applicable with the argument lists of D1 or D2.

The compile-time application of a conversion from a method group E to a delegate type D is described in the following.

This line really doesn't say anything interesting.

Note that the existence of an implicit conversion from E to D does not guarantee that the compile-time application of the conversion will succeed without error.

This line is fascinating. It means that there are implicit conversions which exist, but which are subject to being turned into errors! This is a bizarre rule of C#. To digress a moment, here's an example:

void Q(Expression<Func<string>> f){}
string M(int x) { ... }
...
int y = 123;
Q(()=>M(y++));

An increment operation is illegal in an expression tree. However, the lambda is still convertible to the expression tree type, even though if the conversion is ever used, it is an error! The principle here is that we might want to change the rules of what can go in an expression tree later; changing those rules should not change the type system rules. We want to force you to make your programs unambiguous now, so that when we change the rules for expression trees in the future to make them better, we don't introduce breaking changes in overload resolution.

Anyway, this is another example of this sort of bizarre rule. A conversion can exist for the purposes of overload resolution, but be an error to actually use. Though in fact, that is not exactly the situation we are in here.

Moving on:

A single method M is selected corresponding to a method invocation of the form E(A) [...] The argument list A is a list of expressions, each classified as a variable [...] of the corresponding parameter in the formal-parameter-list of D.

OK. So we do overload resolution on X with respect to D1. The formal parameter list of D1 is empty, so we do overload resolution on X() and joy, we find a method "string X()" that works. Similarly, the formal parameter list of D2 is empty. Again, we find that "string X()" is a method that works here too.

The principle here is that determining method group convertibility requires selecting a method from a method group using overload resolution, and overload resolution does not consider return types.

If the algorithm [...] produces an error, then a compile-time error occurs. Otherwise the algorithm produces a single best method M having the same number of parameters as D and the conversion is considered to exist.

There is only one method in the method group X, so it must be the best. We've successfully proven that a conversion exists from X to D1 and from X to D2.

Now, is this line relevant?

The selected method M must be compatible with the delegate type D, or otherwise, a compile-time error occurs.

Actually, no, not in this program. We never get as far as activating this line. Because, remember, what we're doing here is trying to do overload resolution on Y(X). We have two candidates Y(D1) and Y(D2). Both are applicable. Which is better? Nowhere in the specification do we describe betterness between these two possible conversions.

Now, one could certainly argue that a valid conversion is better than one that produces an error. That would then effectively be saying, in this case, that overload resolution DOES consider return types, which is something we want to avoid. The question then is which principle is better: (1) maintain the invariant that overload resolution does not consider return types, or (2) try to pick a conversion we know will work over one we know will not?

This is a judgment call. With lambdas, we do consider the return type in these sorts of conversions, in section 7.4.3.3:

E is an anonymous function, T1 and T2 are delegate types or expression tree types with identical parameter lists, an inferred return type X exists for E in the context of that parameter list, and one of the following holds:

  • T1 has a return type Y1, and T2 has a return type Y2, and the conversion from X to Y1 is better than the conversion from X to Y2

  • T1 has a return type Y, and T2 is void returning

It is unfortunate that method group conversions and lambda conversions are inconsistent in this respect. However, I can live with it.

Anyway, we have no "betterness" rule to determine which conversion is better, X to D1 or X to D2. Therefore we give an ambiguity error on the resolution of Y(X).

Flavory answered 13/1, 2010 at 17:30 Comment(1)
Cracking - many thanks for both the answer and (hopefully) the resulting improvement in the spec :) Personally I think it would be reasonable for overload resolution to take return type into account for method group conversions in order to make the behaviour more intuitive, but I do understand it would do so at the cost of consistency. (The same can be said of generic type inference as applied to method group conversions when there's only one method in the method group, as I think we've discussed before.)Roaster
R
36

EDIT: I think I've got it.

As zinglon says, it's because there's an implicit conversion from GetString to Action even though the compile-time application would fail. Here's the introduction to section 6.6, with some emphasis (mine):

An implicit conversion (§6.1) exists from a method group (§7.1) to a compatible delegate type. Given a delegate type D and an expression E that is classified as a method group, an implicit conversion exists from E to D if E contains at least one method that is applicable in its normal form (§7.4.3.1) to an argument list constructed by use of the parameter types and modifiers of D, as described in the following.

Now, I was getting confused by the first sentence - which talks about a conversion to a compatible delegate type. Action is not a compatible delegate for any method in the GetString method group, but the GetString() method is applicable in its normal form to an argument list constructed by use of the parameter types and modifiers of D. Note that this doesn't talk about the return type of D. That's why it's getting confused... because it would only check for the delegate compatibility of GetString() when applying the conversion, not checking for its existence.

I think it's instructive to leave overloading out of the equation briefly, and see how this difference between a conversion's existence and its applicability can manifest. Here's a short but complete example:

using System;

class Program
{
    static void ActionMethod(Action action) {}
    static void IntMethod(int x) {}

    static string GetString() { return ""; }

    static void Main(string[] args)
    {
        IntMethod(GetString);
        ActionMethod(GetString);
    }
}

Neither of the method invocation expressions in Main compiles, but the error messages are different. Here's the one for IntMethod(GetString):

Test.cs(12,9): error CS1502: The best overloaded method match for 'Program.IntMethod(int)' has some invalid arguments

In other words, section 7.4.3.1 of the spec can't find any applicable function members.

Now here's the error for ActionMethod(GetString):

Test.cs(13,22): error CS0407: 'string Program.GetString()' has the wrong return type

This time it's worked out the method it wants to call - but it's failed to then perform the required conversion. Unfortunately I can't find out the bit of the spec where that final check is performed - it looks like it might be in 7.5.5.1, but I can't see exactly where.


Old answer removed, except for this bit - because I expect Eric could shed light onto the "why" of this question...

Still looking... in the mean time, if we say "Eric Lippert" three times, do you think we'll get a visit (and thus an answer)?

Roaster answered 13/1, 2010 at 14:16 Comment(15)
@Jon - could it be that classWithSimpleMethods.GetString and classWithSimpleMethods.DoNothing are not delegates?Koblenz
@Daniel: No - those expressions are method group expressions, and the overloaded methods should only be deemed applicable when there's an implicit conversion from the method group to the relevant parameter type. See section 7.4.3.1 of the spec.Roaster
Reading section 6.6, it looks like the conversion from classWithSimpleMethods.GetString to Action is considered to exist since the parameter lists are compatible, but that the conversion (if attempted) fails at compile time. Therefore, an implicit conversion does exist to both delegate types and the call is ambiguous.Orography
@zinglon: How are you reading §6.6 to determine that a conversion from ClassWithSimpleMethods.GetString to Action is valid? For a method M to be compatible with a delegate type D (§15.2) "an identity or implicit reference conversion exists from the return type of M to the return type of D."Condillac
@Jason: The spec doesn't say the conversion is valid, it says it exists. In fact, it's invalid since it fails at compile time. The first two points of §6.6 determine whether the conversion exists. The following points determine whether the conversion will succeed. From point 2: "Otherwise the algorithm produces a single best method M having the same number of parameters as D and the conversion is considered to exist." §15.2 is invoked in point 3.Orography
Note that also states before the algorithm that "the existence of an implicit conversion from E to D does not guarantee that the compile-time application of the conversion will succeed without error" so there's a distinct difference between the existence of a conversion and the validity of that conversion.Orography
@zinglon: I read the first two points as being about determining which method from the method group to select. There is only one method in the method group ClassWithSimpleMethods.GetString and per §7.5.5.1 that method can be unambiguously selected. As I said in a comment elsewhere, what is special about this case (trying to resolve between Method(Func<string>) and Method(Action) that the compile can't handle but it can handle the case of resolving between Method(Func<string>), Method(Func<string, string>) and Method(Action<string>)?Condillac
@zinglon: It doesn't say the conversion exists. It says a conversion from a method group to a compatible delegate type. Delegate compatibility is described in 15.2, and GetString() is not compatible with Action - therefore no conversion exists, IMO.Roaster
@zinglon: Actually, looking at it more carefully, I think you've got it - though not quite in that way. Editing.Roaster
@Jon: It does say that in the preamble, but §15.2 isn't mentioned in the algorithm until after it's stated that the conversion is considered to exist. It seems kind of ambiguous to me.Orography
@zinglon: The preamble is the important bit, I believe. See my edit.Roaster
@Jon: I think you're right. One paragraph later in §6.6 is the sentence "[n]ote that the existence of an implicit conversion from E to D does not guarantee that the compile-time application of the conversion will succeed without error" basically admitting that the definition of "implicit conversion" from a method group to a delegate type can lead to ambiguity. I made the same mistake that you made and was hung up on the first sentence of §6.6. I wonder why the definition of implicit conversion will only consider parameter lists in cases such as this?Condillac
@Jason: No idea - let's see if Eric turns up to explain :) Admittedly it means it does work a bit like overload resolution itself... but in an unhelpful way, really :(Roaster
What am I, kibo? I'll have a look, but given the quantity of verbiage here so far, it might be a bit of a slog.Flavory
In the future if you want to bring something to my attention, you can email me directly (Jon) or drop me a note via the Contact link on my blog.Flavory
T
1

Using Func<string> and Action<string> (obviously very different to Action and Func<string>) in the ClassWithDelegateMethods removes the ambiguity.

The ambiguity also occurs between Action and Func<int>.

I also get the ambiguity error with this:

class Program
{ 
    static void Main(string[] args) 
    { 
        ClassWithSimpleMethods classWithSimpleMethods = new ClassWithSimpleMethods(); 
        ClassWithDelegateMethods classWithDelegateMethods = new ClassWithDelegateMethods(); 

        classWithDelegateMethods.Method(classWithSimpleMethods.GetOne);
    } 
} 

class ClassWithDelegateMethods 
{ 
    public void Method(Func<int> func) { /* do something */ }
    public void Method(Func<string> func) { /* do something */ } 
}

class ClassWithSimpleMethods 
{ 
    public string GetString() { return ""; } 
    public int GetOne() { return 1; }
} 

Further experimentation shows that when passing in a method group by its self, the return type is completely ignored when determining which overload to use.

class Program
{
    static void Main(string[] args)
    {
        ClassWithSimpleMethods classWithSimpleMethods = new ClassWithSimpleMethods();
        ClassWithDelegateMethods classWithDelegateMethods = new ClassWithDelegateMethods();

        //The call is ambiguous between the following methods or properties: 
        //'test.ClassWithDelegateMethods.Method(System.Func<int,int>)' 
        //and 'test.ClassWithDelegateMethods.Method(test.ClassWithDelegateMethods.aDelegate)'
        classWithDelegateMethods.Method(classWithSimpleMethods.GetX);
    }
}

class ClassWithDelegateMethods
{
    public delegate string aDelegate(int x);
    public void Method(Func<int> func) { /* do something */ }
    public void Method(Func<string> func) { /* do something */ }
    public void Method(Func<int, int> func) { /* do something */ }
    public void Method(Func<string, string> func) { /* do something */ }
    public void Method(aDelegate ad) { }
}

class ClassWithSimpleMethods
{
    public string GetString() { return ""; }
    public int GetOne() { return 1; }
    public string GetX(int x) { return x.ToString(); }
} 
Tully answered 13/1, 2010 at 15:34 Comment(0)
K
0

The overloading with Func and Action is akin (because both of them are delegates) to

string Function() // Func<string>
{
}

void Function() // Action
{
}

If you notice, the compiler does not know which one to call because they only differ by return types.

Koblenz answered 13/1, 2010 at 14:12 Comment(3)
I don't think it's really quite like that - because you can't convert a Func<string> into an Action... and you can't convert a method group consisting only of a method which returns a string into an Action either.Roaster
You can't cast a delegate that has no parameters and returns string to an Action. I don't see why there is ambiguity.Condillac
@dtb: Yes, removing the overloading removes the problem - but that doesn't really explain why there's a problem.Roaster

© 2022 - 2024 — McMap. All rights reserved.