如何解释这个“调用不明确”的错误?

Goo*_*ide 7 c# generics overload-resolution

问题

考虑这两个扩展方法,它们只是从任何类型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);
}
Run Code Online (Sandbox Code Playgroud)

现在,当我使用第二个重载映射到引用类型时......

var a = Task
    .FromResult("foo")
    .Map(x => $"hello {x}"); // ERROR

var b = Task
    .FromResult(1)
    .Map(x => x.ToString()); // ERROR
Run Code Online (Sandbox Code Playgroud)

...我收到以下错误:

CS0121:以下方法或属性之间的调用不明确:“Ext.Map(T1, Func)”和“Ext.Map(Task, Func)”

映射到值类型工作正常:

var c = Task
    .FromResult(1)
    .Map(x => x + 1); // works

var d = Task
    .FromResult("foo")
    .Map(x => x.Length); // works
Run Code Online (Sandbox Code Playgroud)

但只要映射实际使用输入产生输出:

var e = Task
    .FromResult(1)
    .Map(_ => 0); // ERROR
Run Code Online (Sandbox Code Playgroud)

问题

任何人都可以向我解释这里发生了什么吗?我已经放弃为这个错误寻找可行的解决方案,但至少我想了解这个混乱的根本原因。

补充说明

到目前为止,我发现了三种解决方法,不幸的是在我的用例中是不可接受的。第一个是明确指定的类型参数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
Run Code Online (Sandbox Code Playgroud)

另一个解决方法是不使用 lambdas:

string foo(string x) => $"hello {x}";
var h = Task
    .FromResult("foo")
    .Map(foo); // works
Run Code Online (Sandbox Code Playgroud)

第三个选项是限制映射到内函数(即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);
}
Run Code Online (Sandbox Code Playgroud)

创建了一个 .NET Fiddle,您可以在其中自己尝试上述所有示例。

Ili*_*hev 3

根据 C# 规范方法调用,使用以下规则将泛型方法视为F方法调用的候选者:

  • 方法具有与类型参数列表中提供的相同数量的方法类型参数,

  • 一旦类型参数被替换为相应的方法类型参数,则参数列表中的所有构造类型都 F满足其约束(Satisfyingconstraints),并且参数列表F适用于A(Applicablefunctionmember)。A- 可选参数列表。

为了表达

Task.FromResult("foo").Map(x => $"hello {x}");
Run Code Online (Sandbox Code Playgroud)

两种方法

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);
Run Code Online (Sandbox Code Playgroud)

满足这些要求:

  • 它们都有两个类型参数;
  • 他们构建的变体

    // 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>);
    
    Run Code Online (Sandbox Code Playgroud)

满足类型约束(因为方法没有类型约束Map)并且根据可选参数适用(因为方法也没有可选参数Map)。注意:要定义第二个参数(lambda 表达式)的类型,使用类型推断。

因此,在这一步,算法将两种变体视为方法调用的候选者。对于这种情况,它使用重载解析来确定哪个候选者更适合调用。规范中的文字:

使用重载决策的重载决策规则来识别候选方法集中的最佳方法。如果无法识别单个最佳方法,则方法调用不明确,并且会发生绑定时间错误。执行重载决策时,在用类型实参(提供的或推断的)替换相应的方法类型参数后,才会考虑泛型方法的参数。

表达

// I intentionally wrote it as static method invocation.
Ext.Map(Task.FromResult("foo"), x => $"hello {x}");
Run Code Online (Sandbox Code Playgroud)

可以使用 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}");
Run Code Online (Sandbox Code Playgroud)

重载解析使用更好的函数成员算法来定义这两种方法中哪一种更适合方法调用。

我已经多次阅读这个算法,但没有找到该算法可以将该方法定义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() );
Run Code Online (Sandbox Code Playgroud)

现在,第一个类型参数T1已明确定义,并且不会出现歧义。