1

I have two overloads of the method Foo, one for synchronous and one for asynchronous delegates:

public static void Foo<T>(Func<T> function, List<T> list = null) { }

public static void Foo<T>(Func<Task<T>> function, List<T> list = null) { }

This method accepts a function, and a second optional argument of type List<T>.

When I try to call this method without specifying the type <T>, the compiler is smart enough to resolve the ambiguity:

Foo(async () => { await Task.CompletedTask; return 0;}); // OK

...unless I attempt to pass explicitly null as the second argument:

Foo(async () => { await Task.CompletedTask; return 0;}, null); // Error CS0121

In that case I get the following compilation error:

The call is ambiguous between the following methods or properties: 'Test.Foo<T>(Func<T>, List<T>)' and 'Test.Foo<T>(Func<Task<T>>, List<T>)'

The strange thing is that null is the default value of this argument anyway!

Is this a limitation of the compiler that could be fixed, or an unsolvable case of ambiguity that can't be handled in any other way than by a compilation error?

Screenshot from Visual Studio

C# 8, Visual Studio 16.3.9, .NET Framework 4.8, .NET Core 3.0


Update: Here is a helpful answer to this question.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Overload resolution happens before default parameters are filled in. It _has_ to, because for positional parameters, the default value could be radically different depending on which overload is selected. Once an overload is selected, there's no ambiguity (by definition). But if _you_ provide the parameter value, now the compiler has no way to know which method overload is better, because the inference rules bail on the `null` value before they get far enough to backtrack to `T`. See marked duplicates for same issue. – Peter Duniho Nov 22 '19 at 16:51
  • 1
    @PeterDuniho I read carefully the two questions referred as duplicates, including the long and detailed answer by Eric Lippert, but I don't think that these questions are duplicates of my question. The first question refers to the `params` keyword, that I haven't mention in my question. None of these questions makes any mention to optional arguments, that are core to my question. So I still don't know the answer to my question. Is this a limitation of the compiler that could be fixed, or an unsolvable problem that can't be handled in any other way than by a compilation error? – Theodor Zoulias Nov 22 '19 at 17:52
  • @PeterDuniho your comment makes some sense however. Could you expand it to an answer? – Theodor Zoulias Nov 22 '19 at 17:52
  • I find the marked duplicates address the need adequately. The compiler _can't_ do overload resolution in your scenario, which the error message makes clear. The solution is, as always and as explained elsewhere in marked duplicates, provide the compiler enough type information so that it can. E.g. cast the `null` to the type you expect. Or even better, just don't specify the parameter. Why would you explicitly pass `null` when that's the default value anyway? – Peter Duniho Nov 22 '19 at 18:16
  • @PeterDuniho I understand the message of the compiler. The compiler isn't doing overload resolution, but not because of ambiguity between the *normal* and the *expanded* form of my `params`-including method. I don't have `params` in my example. The questions referred as duplicates may at best provide some hints, not a clear and convincing explanation of the error I observe. And I would like to have such an explanation as an answer if possible, not as a comment. – Theodor Zoulias Nov 22 '19 at 18:40

1 Answers1

0

You cannot conclude from "null" whether this is List<int> or List<string> and the compiler can't do it either. This is for the latter argument.

And it cannot find out if your Task<R> should match Func<T> or Func<Task<T>>, not unique again, so there it's ambiguous. Either T= Task, or R=T, both is possible.

Holger
  • 2,446
  • 1
  • 14
  • 13
  • So why I get no error if I omit the `null` argument? It's default value is `null` anyway, so I should get an error in this case too. – Theodor Zoulias Nov 22 '19 at 14:00
  • Ah, I see, and besides 'no error', it is bound to the Func> version ? What is inferred ? Task.CompletedTask is of Type 'Task', so it's definitly not of type 'Task', so is must be bound to Func ? Right ? I cannot explain the effect of passing the default parameter or not. – Holger Nov 22 '19 at 14:16
  • When I omit the `null`, the compiler correctly infers that the type is `int`. When I include the `null` the compiler thinks that beyond `T is int` there is also the possibility of `T is Task` (with the first overload that accepts a synchronous delegate). :-/ – Theodor Zoulias Nov 22 '19 at 14:52
  • yes, you have a Func. If you remove the async staff, everything works as expected. The async lambda does implictly create a Task, kind of an "async int" is the same as Task. You really think your first overload is fixed to synchronous ? T cannot be Task ? We are rather about to explain why the ambiguity is obvious, we just don't know why it disappears then omitting the second argument. Really think on compiler bug also. – Holger Nov 22 '19 at 15:17
  • If you look at the overloads of [`Task.Run`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.run), there are two similar to my `Foo` overloads. If the compiler was not smart to understand that it is preferable to use the overload that accepts an asynchronous delegate, then we would have to write the `` explicitly every time we called `Task.Run`! – Theodor Zoulias Nov 22 '19 at 15:25
  • But you also see how they deal with default parameters. They do not use them, they take one overload with 1 parameter, and another overload with 2 parameters. But you do not have a real problem to solve, you just want to understand the compiler ? right. It's just curiosity. And my statement was just Func is not necessarily synchronous, you just proofed it is not used, in case of a Func> is present. – Holger Nov 22 '19 at 15:33
  • Actually I stumbled upon [a real problem](https://stackoverflow.com/questions/58978894/tpl-dataflow-duplicate-message-to-all-consumers/58985506#58985506), where instead of a second argument of type `List` I had two arguments of type `Action`, which forced the consumer of my API to provide the type explicitly. – Theodor Zoulias Nov 22 '19 at 15:37