如何解释这个 "call is ambiguous" 错误?
How to explain this "call is ambiguous" error?
问题
考虑这两个扩展方法,它们只是从任何类型 T1
到 T2
的简单映射,加上一个重载以流畅地映射到 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);
}
现在,当我将第二个重载与到引用类型的映射一起使用时...
var a = Task
.FromResult("foo")
.Map(x => $"hello {x}"); // ERROR
var b = Task
.FromResult(1)
.Map(x => x.ToString()); // ERROR
...我收到以下错误:
CS0121: The call is ambiguous between the following methods or properties: 'Ext.Map(T1, Func)' and 'Ext.Map(Task, Func)'
映射到值类型工作正常:
var c = Task
.FromResult(1)
.Map(x => x + 1); // works
var d = Task
.FromResult("foo")
.Map(x => x.Length); // works
但只要映射实际使用输入来产生输出:
var e = Task
.FromResult(1)
.Map(_ => 0); // ERROR
问题
谁能给我解释一下这是怎么回事?我已经放弃为这个错误寻找可行的修复方法,但至少我想了解这个混乱的根本原因。
补充说明
到目前为止,我发现了三种变通方法,不幸的是,它们在 my use case 中是不可接受的。第一种是显式指定 Task<T1>.Map<T1,T2>()
的类型参数:
var f = Task
.FromResult("foo")
.Map<string, string>(x => $"hello {x}"); // works
var g = Task
.FromResult(1)
.Map<int, int>(_ => 0); // works
另一种解决方法是不使用 lambda:
string foo(string x) => $"hello {x}";
var h = Task
.FromResult("foo")
.Map(foo); // works
第三个选项是将映射限制为内函数(即 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);
}
我 created a .NET Fiddle 你可以自己尝试上面所有的例子。
添加大括号
var result = (await Task
.FromResult<string?>("test"))
.Map(x => $"result: {x}");
你的FilterExt异步方法只是在(await x)中加上大括号,然后调用非异步方法,你要异步方法干什么?
更新:
正如我在许多 .net 库中注意到的那样,开发人员只是将 Async 后缀添加到异步方法。您可以将方法命名为 MapAsync、FilterAsync
在重载决议中,如果未指定,编译器将推断类型参数。
在所有错误情况下,Fun<T1, T2>
中的输入类型 T1
是不明确的。例如:
Task<int>
和int
都有ToString
方法,无法判断是task还是int
但是如果在表达式中使用+
,显然输入类型是整数,因为任务不支持+
运算符。 .Length
是同一个故事。
这也可以解释其他错误。
更新
传递 Task<T1>
不会让编译器选择参数列表中带有 Task<T1>
的方法的原因是编译器需要努力从 Task<T1>
因为 T1
不直接在方法的参数列表中。
可能的修复:
使 Func<>
使用方法参数列表中现有的内容,因此编译器在推断 T1
.
时花费更少的精力
static class Extensions
{
public static T2 Map<T1, T2>(this T1 obj, Func<T1, T2> func)
{
return func(obj);
}
public static T2 Map<T1, T2>(this Task<T1> obj, Func<Task<T1>, T2> func)
{
return func(obj);
}
}
用法:
// This calls Func<T1, T2>
1.Map(x => x + 1);
// This calls Func<Task<T1>, T2>
Task.FromResult(1).Map(async _=> (await _).ToString())
// This calls Func<Task<T1>, T2>
Task.FromResult(1).Map(_=> 1)
// This calls Func<Task<T1>, T2>.
// Cannot compile because Task<int> does not have operator '+'. Good indication.
Task.FromResult(1).Map(x => x + 1)
根据 C# 规范,Method invocations,以下规则用于将泛型方法 F
视为方法调用的候选者:
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 of F
is applicable with respect to A
(Applicable
function member). A
- optional argument list.
表达方式
Task.FromResult("foo").Map(x => $"hello {x}");
两种方法
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);
满足这些要求:
- 它们都有两个类型参数;
他们构建的变体
// 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>);
满足类型约束(因为 Map
方法没有类型约束)并且根据可选参数适用(因为 Map
方法也没有可选参数)。 注意:使用类型推断来定义第二个参数(lambda 表达式)的类型。
因此,在此步骤中,算法将两种变体都视为方法调用的候选者。对于这种情况,它使用过载解析来确定哪个候选更适合调用。规范中的单词:
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.
表达式
// I intentionally wrote it as static method invocation.
Ext.Map(Task.FromResult("foo"), x => $"hello {x}");
可以使用方法 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}");
重载解析使用 Better function member 算法来定义这两种方法中哪一种更适合方法调用。
我已经多次阅读这个算法,但还没有找到算法可以将方法 Exp.Map<T1, T2>(Task<T1>, Func<T1, T2>)
定义为考虑方法调用的更好方法的地方。在这种情况下(当无法定义更好的方法时)会发生编译时错误。
总结一下:
- 方法调用算法将两种方法都视为候选方法;
- 更好的函数成员算法无法定义更好的调用方法。
另一种帮助编译器选择更好方法的方法(就像您在其他解决方法中所做的那样):
// 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() );
现在第一个类型参数 T1
已明确定义,不会出现歧义。
问题
考虑这两个扩展方法,它们只是从任何类型 T1
到 T2
的简单映射,加上一个重载以流畅地映射到 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);
}
现在,当我将第二个重载与到引用类型的映射一起使用时...
var a = Task
.FromResult("foo")
.Map(x => $"hello {x}"); // ERROR
var b = Task
.FromResult(1)
.Map(x => x.ToString()); // ERROR
...我收到以下错误:
CS0121: The call is ambiguous between the following methods or properties: 'Ext.Map(T1, Func)' and 'Ext.Map(Task, Func)'
映射到值类型工作正常:
var c = Task
.FromResult(1)
.Map(x => x + 1); // works
var d = Task
.FromResult("foo")
.Map(x => x.Length); // works
但只要映射实际使用输入来产生输出:
var e = Task
.FromResult(1)
.Map(_ => 0); // ERROR
问题
谁能给我解释一下这是怎么回事?我已经放弃为这个错误寻找可行的修复方法,但至少我想了解这个混乱的根本原因。
补充说明
到目前为止,我发现了三种变通方法,不幸的是,它们在 my use case 中是不可接受的。第一种是显式指定 Task<T1>.Map<T1,T2>()
的类型参数:
var f = Task
.FromResult("foo")
.Map<string, string>(x => $"hello {x}"); // works
var g = Task
.FromResult(1)
.Map<int, int>(_ => 0); // works
另一种解决方法是不使用 lambda:
string foo(string x) => $"hello {x}";
var h = Task
.FromResult("foo")
.Map(foo); // works
第三个选项是将映射限制为内函数(即 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);
}
我 created a .NET Fiddle 你可以自己尝试上面所有的例子。
添加大括号
var result = (await Task
.FromResult<string?>("test"))
.Map(x => $"result: {x}");
你的FilterExt异步方法只是在(await x)中加上大括号,然后调用非异步方法,你要异步方法干什么?
更新: 正如我在许多 .net 库中注意到的那样,开发人员只是将 Async 后缀添加到异步方法。您可以将方法命名为 MapAsync、FilterAsync
在重载决议中,如果未指定,编译器将推断类型参数。
在所有错误情况下,Fun<T1, T2>
中的输入类型 T1
是不明确的。例如:
Task<int>
和int
都有ToString
方法,无法判断是task还是int
但是如果在表达式中使用+
,显然输入类型是整数,因为任务不支持+
运算符。 .Length
是同一个故事。
这也可以解释其他错误。
更新
传递 Task<T1>
不会让编译器选择参数列表中带有 Task<T1>
的方法的原因是编译器需要努力从 Task<T1>
因为 T1
不直接在方法的参数列表中。
可能的修复:
使 Func<>
使用方法参数列表中现有的内容,因此编译器在推断 T1
.
static class Extensions
{
public static T2 Map<T1, T2>(this T1 obj, Func<T1, T2> func)
{
return func(obj);
}
public static T2 Map<T1, T2>(this Task<T1> obj, Func<Task<T1>, T2> func)
{
return func(obj);
}
}
用法:
// This calls Func<T1, T2>
1.Map(x => x + 1);
// This calls Func<Task<T1>, T2>
Task.FromResult(1).Map(async _=> (await _).ToString())
// This calls Func<Task<T1>, T2>
Task.FromResult(1).Map(_=> 1)
// This calls Func<Task<T1>, T2>.
// Cannot compile because Task<int> does not have operator '+'. Good indication.
Task.FromResult(1).Map(x => x + 1)
根据 C# 规范,Method invocations,以下规则用于将泛型方法 F
视为方法调用的候选者:
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.
表达方式
Task.FromResult("foo").Map(x => $"hello {x}");
两种方法
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);
满足这些要求:
- 它们都有两个类型参数;
他们构建的变体
// 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>);
满足类型约束(因为 Map
方法没有类型约束)并且根据可选参数适用(因为 Map
方法也没有可选参数)。 注意:使用类型推断来定义第二个参数(lambda 表达式)的类型。
因此,在此步骤中,算法将两种变体都视为方法调用的候选者。对于这种情况,它使用过载解析来确定哪个候选更适合调用。规范中的单词:
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.
表达式
// I intentionally wrote it as static method invocation.
Ext.Map(Task.FromResult("foo"), x => $"hello {x}");
可以使用方法 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}");
重载解析使用 Better function member 算法来定义这两种方法中哪一种更适合方法调用。
我已经多次阅读这个算法,但还没有找到算法可以将方法 Exp.Map<T1, T2>(Task<T1>, Func<T1, T2>)
定义为考虑方法调用的更好方法的地方。在这种情况下(当无法定义更好的方法时)会发生编译时错误。
总结一下:
- 方法调用算法将两种方法都视为候选方法;
- 更好的函数成员算法无法定义更好的调用方法。
另一种帮助编译器选择更好方法的方法(就像您在其他解决方法中所做的那样):
// 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() );
现在第一个类型参数 T1
已明确定义,不会出现歧义。