We just found these in our code:
public static class ObjectContextExtensions
{
public static T Find<T>(this ObjectSet<T> set, int id, params Expression<Func<T, object>>[] includes) where T : class
{
...
}
public static T Find<T>(this ObjectSet<T> set, int id, params string[] includes) where T : class
{
...
}
}
As you can see, these have the same signature except for the params
.
And they're being used in several ways, one of them:
DBContext.Users.Find(userid.Value); //userid being an int? (Nullable<int>)
which, strangely enough to me, resolves to the first overload.
Q1: Why doesn't this produce a compile error?
Q2: Why does the C# compiler resolve the above call to the first method?
Edit: Just to clarify, this is C# 4.0, .Net 4.0, Visual Studio 2010.
This is clearly a bug in overload resolution.
It reproduces in C# 5 and C# 3 but not in Roslyn; I do not recall if we decided to deliberately take the breaking change or if this is an accident. (I don't have C# 4 on my machine right now but if it repros in 3 and 5 then it will in 4 also almost certainly.)
I have brought it to the attention of my former colleagues on the Roslyn team. If they get back to me with anything interesting I'll update this answer.
As I no longer have access to the C# 3 / 4 / 5 source code I am unable to say what the cause of the bug is. Consider reporting it on connect.microsoft.com.
Here's a much-simplified repro:
class P
{
static void M(params System.Collections.Generic.List<string>[] p) {}
static void M(params int[] p) {}
static void Main()
{
M();
}
}
It appears to have something to do with the genericity of the element type. Bizarrely, as Chris points out in his answer, the compiler chooses the more generic one! I would have expected the bug to be the other way, and choose the less generic one.
The bug is, incidentally, likely my fault, as I did a fair amount of the work on the overload resolution algorithm in C# 3. Apologies for the error.
UPDATE
My spies in the Roslyn team tell me that this is a known bug of long standing in overload resolution. There was a tiebreaker rule implemented that was never documented or justified that said that the type with the bigger generic arity was the better type. This is a bizarre rule with no justification, but it's never been removed from the product. The Roslyn team decided some time ago to take the breaking change and fix overload resolution so that it produces an error in this case. (I don't recall that decision, but we made a lot of decisions about this sort of thing!)
On IDEONE the compiler successfully produces an error. And it should be an error, if you analyse the resolution algorithm step by step:
1) The set of candidate methods for the method invocation is constructed. Starting with the set of methods associated with M, which were found by a previous member lookup [...] The set reduction consists of applying the following rules to each method T.N in the set, where T is the type in which the method N is declared:
For simplicity, we can deduce here that that the set of methods here contains both of your methods.
Then the reduction proceeds:
2) If N is not applicable with respect to A (Section 7.4.2.1), then N is removed from the set.
Both methods are applicable with respect to Applicable function member rules in their expanded form:
The expanded form is constructed by replacing the parameter array in the function member declaration with zero or more value parameters of the element type of the parameter array such that the number of arguments in the argument list A matches the total number of parameters. If A has fewer arguments than the number of fixed parameters in the function member declaration, the expanded form of the function member cannot be constructed and is thus not applicable.
This rule leaves both methods in the reduction set.
Experiments (changing the id
parameter type to float
in one or both methods) confirm that both functions remain in the candidate set, and are further discriminated by the implicit conversion comparison rules .
This suggests that the above algorithm works fine in terms of creating the candidate set, and is not dependent on some internal method ordering. Since the only thing that discriminates methods further are Overload resolution rules this seems to be a bug, because:
the best function member is the one function member that is better than all other function members with respect to the given argument list, provided that each function member is compared to all other function members using the rules in Section 7.4.2.2.
and clearly none of these methods is better than other, because no implicit conversions exist here.
This isn't a full answer since it explains the differences but not why. It really needs s specification reference for completeness. However I didn't want the research I've done to get lost in comments so am posting as an answer.
The difference between the two overloads lies in the fact that the params for one is generic and the other is not. The compiler seems to decide that the generic type is closer than the non-generic.
That is if the Expression<...>
type was changed to an int
the compiler would complain about ambiguity. Similar if the types are both generic then it complains about ambiguity.
The following snippet will exhibit this behaviour more simply:
void Main()
{
TestMethod();
}
public void TestMethod(params string[] args)
{
Console.WriteLine("NonGeneric");
}
public void TestMethod(params List<string>[] args)
{
Console.WriteLine("Generic");
}
This will print "Generic".