如何解释这个 "call is ambiguous" 错误?

How to explain this "call is ambiguous" error?

问题

考虑这两个扩展方法,它们只是从任何类型 T1T2 的简单映射,加上一个重载以流畅地映射到 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 已明确定义,不会出现歧义。