Consider these two extension methods which are just a simple map from any type T1
to T2
, plus an overload to fluently map over Task<T>
:
public static class Ext {
public static T2 Map<T1, T2>(this T1 x, Func<T1, T2> f)
=> f(x);
public static async Task<T2> Map<T1, T2>(this Task<T1> x, Func<T1, T2> f)
=> (await x).Map(f);
}
Now, when I use the second overload with a mapping to a reference type...
var a = Task
.FromResult("foo")
.Map(x => $"hello {x}"); // ERROR
var b = Task
.FromResult(1)
.Map(x => x.ToString()); // ERROR
...I get the following error:
CS0121: The call is ambiguous between the following methods or properties: 'Ext.Map(T1, Func)' and 'Ext.Map(Task, Func)'
Mapping to a value type works fine:
var c = Task
.FromResult(1)
.Map(x => x + 1); // works
var d = Task
.FromResult("foo")
.Map(x => x.Length); // works
But only as long the mapping actually uses the input to produce an output:
var e = Task
.FromResult(1)
.Map(_ => 0); // ERROR
Can anyone please explain to me what is going on here? I've already given up on finding a feasible fix for this error, but at least I'd like to understand the root cause of this mess.
So far I found three workarounds which are unfortunately not acceptable in my use case. The first is to specify the type arguments of Task<T1>.Map<T1,T2>()
explicitly:
var f = Task
.FromResult("foo")
.Map<string, string>(x => $"hello {x}"); // works
var g = Task
.FromResult(1)
.Map<int, int>(_ => 0); // works
Another workaround is to not use lambdas:
string foo(string x) => $"hello {x}";
var h = Task
.FromResult("foo")
.Map(foo); // works
And the third option is to restrict the mappings to endofunctions (i.e. Func<T, T>
):
public static class Ext2 {
public static T Map2<T>(this T x, Func<T, T> f)
=> f(x);
public static async Task<T> Map2<T>(this Task<T> x, Func<T, T> f)
=> (await x).Map2(f);
}
I created a .NET Fiddle where you can try out all the above examples yourself.
According to C# Specification, Method invocations, the next rules are used to consider a generic method F
as a candidate for method invocation:
Method has the same number of method type parameters as were supplied in the type argument list,
and
Once the type arguments are substituted for the corresponding method type parameters, all constructed types in the parameter list of
F
satisfy their constraints (Satisfying constraints), and the parameter list ofF
is applicable with respect toA
(Applicable function member).A
- optional argument list.
For expression
Task.FromResult("foo").Map(x => $"hello {x}");
both methods
public static T2 Map<T1, T2>(this T1 x, Func<T1, T2> f);
public static async Task<T2> Map<T1, T2>(this Task<T1> x, Func<T1, T2> f);
satisfy these requirements:
their constructed variants
// T2 Map<T1, T2>(this T1 x, Func<T1, T2> f)
string Ext.Map<Task<string>, string>(Task<string>, Func<Task<string>, string>);
// Task<T2> Map<T1, T2>(this Task<T1> x, Func<T1, T2> f)
Task<string> Ext.Map<string, string>(Task<string>, Func<string, string>);
satisfy type constraints (because there is no type constraints for Map
methods) and applicable according to optional arguments (because also there is no optional arguments for Map
methods). Note: to define the type of the second argument (lambda expression) a type inference is used.
So at this step the algorithm considers both variants as candidates for method invocation. For this case it uses Overload resolution to determine which candidate better fits for invocation. Words from specification:
The best method of the set of candidate methods is identified using the overload resolution rules of Overload resolution. If a single best method cannot be identified, the method invocation is ambiguous, and a binding time error occurs. When performing overload resolution, the parameters of a generic method are considered after substituting the type arguments (supplied or inferred) for the corresponding method type parameters.
Expression
// I intentionally wrote it as static method invocation.
Ext.Map(Task.FromResult("foo"), x => $"hello {x}");
can be rewritten the next way using constructed variants of the method Map:
Ext.Map<Task<string>, string>(Task.FromResult("foo"), (Task<string> x) => $"hello {x}");
Ext.Map<string, string>(Task.FromResult("foo"), (string x) => $"hello {x}");
Overload resolution uses Better function member algorithm to define which of this two methods better fits method invocation.
I have read this algorithm several times and haven't found a place where the algorigthm can define the method Exp.Map<T1, T2>(Task<T1>, Func<T1, T2>)
as better method for considered method invocation. In this case (when better method cannot be defined) a compile time error occures.
To sum up:
Another approach of helping compiler to choose better method (as you did in your other workarounds):
// Call to: T2 Map<T1, T2>(this T1 x, Func<T1, T2> f);
var a = Task.FromResult("foo").Map( (string x) => $"hello {x}" );
// Call to: async Task<T2> Map<T1, T2>(this Task<T1> x, Func<T1, T2> f);
var b = Task.FromResult(1).Map( (Task<int> x) => x.ToString() );
Now the first type argument T1
is explicitly defined and an ambiguity does not occur.