【问题标题】:How to explain this "call is ambiguous" error?如何解释这个“调用不明确”的错误?
【发布时间】:2020-06-30 10:21:46
【问题描述】:

问题

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

问题

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

附加说明

到目前为止,我发现了三个在my use case 中不可接受的变通方法。首先是明确指定Task&lt;T1&gt;.Map&lt;T1,T2&gt;()的类型参数:

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&lt;T, T&gt;):

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,您可以自己尝试以上所有示例。

【问题讨论】:

  • 拜托,看看下面的帖子,Overloaded method-group argument confuses overload resolutionWhy is Func<T> ambiguous with Func<IEnumerable<T>> 我想主要的想法和你的问题很相似
  • 这些读物很有趣,但除了错误消息之外,我看不出与我的问题有密切的联系。有人可以给 Eric Lippert 打电话吗? :D
  • 致匿名投票者:请向我解释如何使这个问题更加集中。 “这是一个编译器错误和一些最小的示例代码。请帮助我让它消失,或者至少帮助我理解这里出了什么问题。”究竟是什么。不够专注?
  • 强制 API 的客户端几乎总是指定通常可以推断的类型参数对我来说更像是一个丑陋的解决方法。无论如何,我终于找到了一个更简单的例子,并将重写整个问题。
  • @GoodNightNerdPride 请参阅此question。我认为这与您的问题有关。

标签: c# generics overload-resolution


【解决方案1】:

添加大括号

var result = (await Task
            .FromResult<string?>("test"))
            .Map(x => $"result: {x}");

你的FilterExt异步方法只是给(await x)加上大括号,然后调用非异步方法,那你需要什么异步方法??

更新: 正如我在许多 .net 库中注意到的那样,开发人员只是将 Async 后缀添加到异步方法中。您可以将方法命名为 MapAsync、FilterAsync

【讨论】:

  • 大括号是我想要避免的。我的 API 应该可以流畅地使用 T?Task&lt;T?&gt;。大括号打破了流畅 API 的流程。这样,您只需在方法链中 await 一次,而不是每个 async 调用一次。
  • 很奇怪,第二个例子运行良好,但第一个不行。
  • 这只是强制使用同步重载;这不是问题的答案。
  • 不能使用Async 后缀,因为T2?.MapAsync&lt;T1,T2&gt;(Func&lt;T1, Task&lt;T2&gt;)Task&lt;T2?&gt;.MapAsync&lt;T1,T2&gt;(Func&lt;T1, Task&lt;T2&gt;) 需要它。
  • 更奇怪的是,当我使用正确的方法而不是 Map() 的 lambda 时,没有问题。查看我的问题的更新。
【解决方案2】:

在重载决议中,如果未指定,编译器将推断类型参数。

在所有错误情况下,Fun&lt;T1, T2&gt; 中的输入类型 T1 是不明确的。例如:

Task&lt;int&gt;int都有ToString方法,所以无法推断是task还是int。

但是如果在表达式中使用+,很明显输入类型是整数,因为任务不支持+运算符。 .Length 是同一个故事。

这也可以解释其他错误。

更新

传递Task&lt;T1&gt; 的原因不会使编译器在参数列表中选择带有Task&lt;T1&gt; 的方法是编译器需要努力从Task&lt;T1&gt; 中推断出T1,因为T1 不是直接在方法的参数列表中。

可能的修复: 使Func&lt;&gt; 使用方法的参数列表中存在的内容,因此编译器在推断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)

【讨论】:

  • 有趣,我没想到。但我还是不太明白。编译器知道我在Task&lt;T1&gt; 上调用Map()。为什么它甚至会尝试其他重载?为什么在我协助类型推断时能准确解决歧义?
  • 因为Task&lt;V&gt; 可以匹配Map&lt;Task&lt;V&gt;,X&gt;。例如T1 == Task&lt;V&gt;。最简单的解决方法,将您的异步方法重命名为 MapAsync...
  • 如 cmets 中对另一个答案所述,重命名为 MapAsync 不是一种选择,因为我需要异步 mapping 函数的后缀(例如 T1.MapAsync(Func&lt;T1, Task&lt;T2&gt;)) .
  • 我知道 T1 可以是 Task&lt;T1&gt;,但 AFAIK 重载解析将支持最具体的参数类型,在我的例子中是 Task&lt;T1&gt;
  • @GoodNightNerdPride 最具体的参数只有在只有一种情况下才为真,但在你的情况下,有两种。
【解决方案3】:

根据 C# 规范 Method invocations,接下来的规则用于将泛型方法 F 视为方法调用的候选对象:

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

  • 一旦将类型实参替换为对应的方法类型参数,参数列表中的所有构造类型 F 满足他们的约束(满足约束),并且 F的参数列表适用于A(适用 功能成员)。 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}");

重载解析使用Better function member 算法来定义这两种方法中的哪一种更适合方法调用。

我已多次阅读此算法,但没有找到算法可以将方法 Exp.Map&lt;T1, T2&gt;(Task&lt;T1&gt;, Func&lt;T1, T2&gt;) 定义为考虑方法调用的更好方法的地方。在这种情况下(当无法定义更好的方法时)会发生编译时错误。

总结一下:

  • 方法调用算法将这两种方法都视为候选方法;
  • 更好的函数成员算法无法定义更好的调用方法。

帮助编译器选择更好方法的另一种方法(就像您在其他解决方法中所做的那样):

// 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 已明确定义,不会出现歧义。

【讨论】:

  • 这是一个很好的答案,但有“更好”的解决方法:) 请参阅我的答案。在这种情况下,“更好”意味着更少的输入。
猜你喜欢
  • 2010-10-15
  • 2010-10-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多