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() { }
}
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:
(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:
So let's go through it line by line.
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?
So far so good. X might contain a method that is applicable with the argument lists of D1 or D2.
This line really doesn't say anything interesting.
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:
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:
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.
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?
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:
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).
The overloading with
Func
andAction
is akin (because both of them are delegates) toIf you notice, the compiler does not know which one to call because they only differ by return types.
Using
Func<string>
andAction<string>
(obviously very different toAction
andFunc<string>
) in theClassWithDelegateMethods
removes the ambiguity.The ambiguity also occurs between
Action
andFunc<int>
.I also get the ambiguity error with this:
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.
EDIT: I think I've got it.
As zinglon says, it's because there's an implicit conversion from
GetString
toAction
even though the compile-time application would fail. Here's the introduction to section 6.6, with some emphasis (mine):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 theGetString
method group, but theGetString()
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 ofGetString()
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:
Neither of the method invocation expressions in
Main
compiles, but the error messages are different. Here's the one forIntMethod(GetString)
: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)
: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)?