5

I have recently found an interesting behavior of C# compiler. Imagine an interface like this:

public interface ILogger
{
   void Info(string operation, string details = null);
   void Info(string operation, object details = null);
}

Now if we do

logger.Info("Create")

The compiler will complain that he does not know which overload to chose (Ambiguous invocation...). Seems logical, but when you try to do this:

logger.Info("Create", null)

It will suddenly have no troubles figuring out that null is a string. Moreover it seems that the behavior of finding the right overload has changed with time and I had found a bug in an old code that worked before and stopped working because compiler decided to use another overload.

So I am really wondering why does C# not generate the same error in the second case as it does in the first. Seems very logical to do this, but instead it tries and resolves it to random overload.

P.S. I don't think that it's good to provide such ambiguous interfaces and do not recommend that, but legacy is legacy and has to be maintained :)

Ilya Chernomordik
  • 27,817
  • 27
  • 121
  • 207
  • When I saw the title I (mentally) grabbed a bag of popcorn. But this really sounds interesting. – Romano Zumbé Jul 21 '17 at 11:30
  • You need to do some research. This question has been asked a lot. E.g. https://stackoverflow.com/questions/32892243/which-c-sharp-method-overload-is-chosen – rory.ap Jul 21 '17 at 11:30
  • @rory.ap But this doesn't explain why the behavior changed in his legacy code – Romano Zumbé Jul 21 '17 at 11:31
  • I'm not sure that's true. He or she is probably mistaken. – rory.ap Jul 21 '17 at 11:31
  • 2
    Does [this](https://stackoverflow.com/questions/42951282/breaking-change-in-method-overload-resolution-in-c-sharp-6-explanation) answer your question ? Compiler had breaking changes in Roslyn that made overload resolution more strict. – Zein Makki Jul 21 '17 at 11:33
  • he says nowhere that the behavior of the compiler changed over time – Serve Laurijssen Jul 21 '17 at 11:33
  • @rory.ap not too unlikely – Romano Zumbé Jul 21 '17 at 11:35
  • 2
    @ServéLaurijssen Did you read his question? **"Moreover it seems that the behavior of finding the right overload has changed with time and I had found a bug in an old code that worked before"** – Romano Zumbé Jul 21 '17 at 11:35
  • Code doesn't change. If it was working before and not working now, you surely changed at least the compiler version (or the normal it was never working and you thought it was :)) – Camilo Terevinto Jul 21 '17 at 11:37
  • 1
    @RomanoZumbé oh sry, need coffee I guess – Serve Laurijssen Jul 21 '17 at 11:38
  • @user3185569 It seems not exactly the same, since the build was broken then. Mine compiles and thus it makes this thing much more scary :) But I think it is very likely that previous time it was compiled in VS2015 or even VS2013 and something has changed. – Ilya Chernomordik Jul 21 '17 at 11:38
  • @IlyaChernomordik If i am understanding your correctly it currently compiles with logger.Info("Create", null). From what i would expect here i would want it to report an ambigious diagnostic error. Did you try github/roslyn? – Dbl Jul 21 '17 at 11:44
  • 1
    @Dbl It did compile before, it does compile now, but the overload chosen is different. Scary stuff, I would really expect compiler error here. – Ilya Chernomordik Jul 21 '17 at 11:49
  • @IlyaChernomordik Which version of C# were you using before when it used to pick the `object` overload ? This seems to be the same behavior since C# 4. It compiles and chooses `string` because `string` is more specific than `object` – Zein Makki Jul 21 '17 at 12:11
  • @user3185569 I think it was C#4, but in VS2015 or VS2013 compiler. Now I use 2017. String is more specific, but strange it's more specific for `null`. – Ilya Chernomordik Jul 21 '17 at 12:24
  • @IlyaChernomordik You code works the same for C# 4 compiler. Check here : http://rextester.com/ – Zein Makki Jul 21 '17 at 12:26
  • @user3185569, Yes works the same there, but it says //Compiler version 4.0.30319.17929 for Microsoft (R) .NET Framework 4.5. Perhaps it does use roslyn there? – Ilya Chernomordik Jul 21 '17 at 12:29
  • @user3185569 I have simplified the example, it was more complex, so perhaps there is more to it than this or maybe there was some other thing in there. But thank you very much for the explanations in any case. I would still prefer a compiler error, but at least makes some sense now :) – Ilya Chernomordik Jul 21 '17 at 12:31
  • @IlyaChernomordik That's C# 4 and not roslyn. You can make sure of that by trying to declare a string as: `string str = $"abc";` Roslyn supports $, but it was not in C# 4. – Zein Makki Jul 21 '17 at 12:31
  • @user3185569 Yes, you are right. Have tried here: https://dotnetfiddle.net/. You can chose compiler in there. Works all the same with string. – Ilya Chernomordik Jul 21 '17 at 12:32

1 Answers1

6

There was a breaking change introduced in C# 6 that made the overload resolution better. Here it is with the list of features:

Improved overload resolution

There are a number of small improvements to overload resolution, which will likely result in more things just working the way you’d expect them to. The improvements all relate to “betterness” – the way the compiler decides which of two overloads is better for a given argument.

One place where you might notice this (or rather stop noticing a problem!) is when choosing between overloads taking nullable value types. Another is when passing method groups (as opposed to lambdas) to overloads expecting delegates. The details aren’t worth expanding on here – just wanted to let you know!


but instead it tries and resolves it to random overload.

No, C# doesn't pick overloads randomly, that case is the ambiguous call error. C# picks the better method. Refer to section 7.5.3.2 Better function member in the C# specs:

7.5.3.2 Better function member

Otherwise, if MP has more specific parameter types than MQ, then MP is better than MQ. Let {R1, R2, …, RN} and {S1, S2, …, SN} represent the uninstantiated and unexpanded parameter types of MP and MQ. MP’s parameter types are more specific than MQ’s if, for each parameter, RX is not less specific than SX, and, for at least one parameter, RX is more specific than SX:

Given that string is more specific than object and there is an implicit cast between null and string, then the mystery is solved.

Zein Makki
  • 29,485
  • 6
  • 52
  • 63
  • I really liked the "The details aren’t worth expanding on here – just wanted to let you know!" part :) – Ilya Chernomordik Jul 21 '17 at 11:47
  • Seems very scary what they did there, because out of nowhere code stopped working, but compiles just fine. But it looks that this is really what happened, though I can't confirm it because the details aren't worth expanding :) Thanks for the answer – Ilya Chernomordik Jul 21 '17 at 11:48
  • The example in the question doesn't use nullable value types or pass a method group argument. – Lee Jul 21 '17 at 11:50
  • @user3185569 I am just wondering how is `string` better than `object` if you send null? I suppose that if you send `"abc"`, then obviously `string` overload should be used, but it seems it works like that with `null`, which is strange. – Ilya Chernomordik Jul 21 '17 at 12:23
  • @IlyaChernomordik `string` is more specific than `object` in general. Given `null` value fits in both, then the rule is applied. – Zein Makki Jul 21 '17 at 12:25
  • @user3185569 Tried to add an `int?` to the equation, now it's suddenly an error because `string` and `int?` are at the same "specificity" level :) But both are more specific that object. – Ilya Chernomordik Jul 21 '17 at 12:34
  • @IlyaChernomordik That was exactly one of the examples I came up with to force the error to come up. :) – Zein Makki Jul 21 '17 at 12:35
  • @user3185569 Thanks for help :) – Ilya Chernomordik Jul 21 '17 at 12:39