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

7

问题

考虑以下两个扩展方法,它们只是从任何类型 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,您可以在其中尝试上述示例。


1
请查看以下线程:过载的方法组参数混淆了重载分辨率为什么Func<T>与Func<IEnumerable<T>>模糊不清? 我猜主要思想与您的问题类似。 - Pavel Anikhouski
1
这些文章很有趣,但除了错误信息外,我没有看到与我的问题有强烈的联系。可以请人联系Eric Lippert吗? :D - Good Night Nerd Pride
1
对于匿名的投票者:请解释一下如何让这个问题更加专注。在“这里有一个编译器错误和一些最小化的示例代码。请帮助我解决它,或者至少帮助我理解这里出了什么问题。”中,到底哪一部分不够专注? - Good Night Nerd Pride
1
强制API的客户端几乎总是指定通常可以推断出的类型参数,对我来说更像是一个丑陋的解决方法。无论如何,我终于找到了一个更简洁的示例,并将重写整个问题。 - Good Night Nerd Pride
1
@GoodNightNerdPride 请参考这个问题,我认为它与你的问题有关。 - Iliar Turdushev
显示剩余3条评论
3个回答

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);

满足这些要求:

  • they both have two type parameters;
  • their constructed variants

    // 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 已经明确定义,不会发生歧义。

这是一个很好的答案,但有一个“更好”的解决方法 :) 请看我的答案。“更好”在这种情况下意味着输入更少。 - weichch

2

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

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

Task<int>int都有ToString方法,因此无法推断它是任务还是整数。

但是,如果在表达式中使用+,则清楚输入类型为整数,因为任务不支持+运算符。.Length也是同样的道理。

这也可以解释其他错误。

更新

传递Task<T1>的原因无法使编译器选择具有Task<T1>的方法是因为编译器需要努力从Task<T1>中推断出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)

有趣,我没有想到那个。但我仍然不太明白。编译器知道我在Task<T1>上调用Map()。它为什么会尝试其他重载?为什么当我协助类型推断时歧义得到解决? - Good Night Nerd Pride
因为Task<V>可以匹配Map<Task<V>,X>,例如T1 == Task<V>。最简单的解决方法是将异步方法重命名为MapAsync... - Jeremy Lakeman
正如在其他答案的评论中所述,将其重命名为“MapAsync”不是一个选项,因为我需要该后缀用于异步映射函数(例如T1.MapAsync(Func<T1, Task<T2>))。 - Good Night Nerd Pride
我知道T1可以是Task<T1>,但据我所知,重载解析将优先选择最具体的参数类型,而在我的情况下是Task<T1> - Good Night Nerd Pride
@GoodNightNerdPride 最具体的参数仅在只有一个情况时为真,但在您的情况下,有两种情况。 - Akash Kava
@GoodNightNerdPride 你可以把重载决策想象成一个聪明的懒人。尽可能少的努力胜出。编译器需要推断T1并将其放入Func<T1>中,与从Task<T1>中推断T1并将其放入Func<T1>中所需的努力相同。因为对于第二种情况,T1不在方法的参数列表中,所以编译器需要计算并从Task<T1>中获取它。要解决这个问题,你可以让Fun<>使用方法参数列表中的现有参数,这样可以减少所需的努力。请参见更新后的答案,了解可能的解决方案。 - weichch

0

添加大括号

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

你的FilterExt异步方法只是在(await x)周围添加括号,然后调用非异步方法,那么你为什么需要异步方法?

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


我想要避免的是花括号。我的 API 应该能够流畅地处理 T?Task<T?>。花括号会破坏流畅 API 的连贯性。这样,您只需要在方法链中 await 一次,而不是每个 async 调用都要 await 一次。 - Good Night Nerd Pride
1
非常奇怪,第二个例子运行良好,但第一个却不行。 - Надиль Каримов
这只是强制使用同步重载,而不是回答问题的答案。 - Johnathan Barclay
使用“Async”后缀不是一种选择,因为我需要它来使用 T2?.MapAsync<T1,T2>(Func<T1, Task<T2>)Task<T2?>.MapAsync<T1,T2>(Func<T1, Task<T2>) - Good Night Nerd Pride
更奇怪的是,当我使用Map()的适当方法而不是lambda时,就没有问题。请参见我的问题更新。 - Good Night Nerd Pride

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接