首页 > 解决方案 > 如何解释这个“调用不明确”的错误?

问题描述

问题

考虑这两个扩展方法,它们只是从任何类型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:以下方法或属性之间的调用不明确:“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

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

var e = Task
    .FromResult(1)
    .Map(_ => 0); // ERROR

问题

谁能向我解释这里发生了什么?我已经放弃为这个错误寻找可行的修复方法,但至少我想了解这个混乱的根本原因。

补充说明

到目前为止,我发现了三种在我的用例中不可接受的变通方法。首先是明确指定类型参数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);
}

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

标签: c#genericsoverload-resolution

解决方案


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

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

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

用于表达

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 表达式)的类型,需要使用类型推断。

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

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

表达

// 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}");

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

我已经读过这个算法好几次了,还没有找到一个算法可以将方法定义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被明确定义并且不会出现歧义。


推荐阅读