24

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.

svick
  • 236,525
  • 50
  • 385
  • 514
Federico Berasategui
  • 43,562
  • 11
  • 100
  • 154
  • 3
    This post from Eric Lippert recently discussed how overloads are resolved: http://ericlippert.com/2013/12/23/closer-is-better/ – Katie Kilian Jan 07 '14 at 16:38
  • @CharlieKilian: From memory I don't think that discussed this situation did it? I don't recall params being mentioned at all and I'm not sure how you can call one closer than the other if both are basically empty arrays... Hopefully Eric will see this and enlighten us. Certainly a good question... – Chris Jan 07 '14 at 16:41
  • Does flipping the order the functions are defined change which one is called? If so then the closer one is the nearest one or first one. – Felan Jan 07 '14 at 16:44
  • 1
    @Chris fair enough, it might not apply directly. But given that `params` can be omitted entirely from the calling parameters (see here: http://msdn.microsoft.com/en-us/library/w5zay9db.aspx) then I was taking closer to mean 'the first one defined'. – Katie Kilian Jan 07 '14 at 16:45
  • 1
    @CharlieKilian There is nothing about the order of declarations in this blog post. – BartoszKP Jan 07 '14 at 16:48
  • @DStanley He has shown specifically how he's calling it, and it is ambiguous. – Servy Jan 07 '14 at 16:52
  • For fun, what does the compiler say when you remove the `params` and give both default values of `null`? – leppie Jan 07 '14 at 16:53
  • 1
    Fun fact: Even though this works, Resharper 7.1 treats it as an error. – Yandros Jan 07 '14 at 16:54
  • 1
    It seems that it considers the generic object to be "closer" in the params. If both are generic or both are non-generic then it complains about ambiguity. I can't tell you why this is though but I suspect some line in the spec somewhere says that this is the way of it. – Chris Jan 07 '14 at 16:56
  • @CharlieKilian: Well if you'd tried it you'll see order of declaration is irrelevant and I would be shocked if the compiler every just said "Oh well, you declared this one first so I'll ignore the ambiguity". As you saw from the article on resolution you'll see that it gets a candidate set and eliminates the possibilities til one remains and if there is more than one we expect an ambiguous call exception to occur. There is thus clearly a rule somewhere that defines one as closer than the other, just that nobody here (currently) can say what it is. – Chris Jan 07 '14 at 16:58
  • 1
    Related question: http://stackoverflow.com/q/17390255/15541 – leppie Jan 07 '14 at 16:58
  • @Chris You're right, the article doesn't specify the exact reason this method is considered 'closer' -- if indeed that is what is going on here. I'd be interested to hear the exact rule. – Katie Kilian Jan 07 '14 at 17:01
  • Look at IDEONE link in my answer - this seems like a bug to me :) – BartoszKP Jan 07 '14 at 17:44

3 Answers3

29

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!)

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • 2
    Thanks for your answer. This didn't cause any trouble to us, I was asking out of curiosity. I'm glad that I'm contributing to improve the language / compiler =) – Federico Berasategui Jan 07 '14 at 19:05
  • @HighCore: You're welcome; thank Chris for bringing it to my attention. Will it cause you trouble when Roslyn suddenly starts reporting this as an error when you upgrade in the future? – Eric Lippert Jan 07 '14 at 19:07
  • no, I (and the rest of my team agrees on this too) prefer that the compiler catches and removes as much ambiguity as possible. We can easily fix that by adding an overload `T Find(this ObjectSet set, int id){...}` – Federico Berasategui Jan 07 '14 at 19:11
  • @EricLippert: Thanks for the feedback. I'll try not to make a habit of asking you for help .:) – Chris Jan 07 '14 at 20:34
7

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.

BartoszKP
  • 34,786
  • 15
  • 102
  • 130
  • The confusing thing is that their expanded forms should be absolutely identical (it adds 0 parameters thus their expanded forms are the same as you've already noted) so I don't understand how it could differentiate after that... – Chris Jan 07 '14 at 17:36
  • @Chris Look at the IDEONE link I've provided. This seems to work fine on MONO, so it seems this is a bug. – BartoszKP Jan 07 '14 at 17:42
  • That definitely looks like a bug. The only question is in which one. I personally don't trust myself to read the spec and all its complicated language perfectly. ;-) Or it might be something has changed and we are comparing different spec versions... And a belated +1 since I've jsut realised I haven't done so yet. – Chris Jan 07 '14 at 17:52
6

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".

Chris
  • 27,210
  • 6
  • 71
  • 92
  • That would be contrary to the [C# specification](http://www.microsoft.com/downloads/details.aspx?familyid=DFBF523C-F98C-4804-AFBD-459E846B268E&displaylang=en): `If MP is a non-generic method and MQ is a generic method, then MP is better than MQ.` – Yandros Jan 07 '14 at 17:13
  • I don't have office on my computer so I'm having trouble viewing a copy of the spec but that snippet is tested and not guesswork output. However your quote refers to generic methods and not generic parameters on methods. Also the fact it has to be a params type parameter is going to be relevant in there somewhere... – Chris Jan 07 '14 at 17:17
  • Changing the type of parameter of the other overload to `Action` doesn't change anything unfortunately. – BartoszKP Jan 07 '14 at 17:20
  • Unfortunately this doesn't actually answer the question. – ken2k Jan 07 '14 at 17:27
  • @ken2k: As I said in the first paragraph I am aware that it is only a half answer but it at least answers in as far as it says "It chooses the one with generic type parameters" even if it doesn't explain why the compiler would choose to do that. – Chris Jan 07 '14 at 17:34
  • 5
    @Yandros: That is referring to the genericity of the *method*, not the *formal parameter type*. That is, if we have a conflict between two methods `M(int)` and `M(T)` where `T` is `int`, then the non-generic one wins. – Eric Lippert Jan 07 '14 at 19:02