为什么C#无法从这种看似简单,明显的情况推断出类型


71

给出以下代码:

class C
{
    C()
    {
        Test<string>(A); // fine
        Test((string a) => {}); // fine
        Test((Action<string>)A); // fine

        Test(A); // type arguments cannot be inferred from usage!
    }

    static void Test<T>(Action<T> a) { }

    void A(string _) { }
}

编译器抱怨Test(A)无法弄清楚Tstring

对我来说,这似乎是一个非常简单的案例,并且我发誓我已经对我编写的其他通用实用程序和扩展函数进行了更为复杂的推断。我在这里想念什么?

更新1:这是在C#4.0编译器中。我在VS2010中发现了这个问题,上面的示例来自我在LINQPad 4中所做的最简单的复制。

更新2:在有效列表中添加了更多示例。


4
您正在使用哪个版本的C#?与C#3编译器相比,C#4编译器在方法组方面具有更好的类型推断。
乔恩·斯基特

4
我验证了在C#4下这仍然是编译器错误。
Yuck

4
@Jon-但是C#4也无法解决问题
Marc Gravell

4
对,太可惜了。抱歉,我本人无法验证-我目前正在度假,只有酒店的笔记本电脑。我想我可以在其上安装.NET 4 :)
Jon Skeet

4
@Jon Skeet-我暂时不相信您拥有一台未安装.NET的笔记本电脑。=)
Yuck

Answers:


38
Test(A);

之所以失败,是因为唯一适用的方法(Test<T>(Action<T>))需要类型推断,并且类型推断算法要求每个每个参数都属于某种类型或为匿名函数。(此事实是从类型推断算法的规范(第7.5.2节)中推断出来的)。方法组A不是任何类型(即使它可以转换为适当的委托类型),并且它不是匿名函数。

Test<string>(A);

这成功完成,区别在于绑定Test不需要类型推断,并且方法组A可转换为所需的委托参数type void Action<string>(string)

Test((string a) => {});

这样成功了,区别在于类型推断算法在第一阶段(第7.5.2.1节)提供了匿名函数。匿名函数的参数和返回类型是已知的,因此可以进行显式的参数类型推断,从而在匿名函数(void ?(string))中的类型与Test方法的参数的委托类型((void Action<T>(T))。没有为与匿名函数的算法相对应的方法组指定算法。

Test((Action<string>)A);

这成功完成了,区别在于未类型化方法组参数A被强制转换为类型,从而允许类型推断Test正常进行特定类型的表达式作为方法的唯一参数。

从理论上讲,我无法认为为什么无法在方法组上尝试重载解析A。然后,如果找到单个最佳绑定,则可以为方法组提供与匿名函数相同的处理。在这样的情况下尤其如此,其中方法组仅包含一个候选项,并且没有类型参数。但是它在C#4中不起作用的原因似乎是没有设计和实现此功能的事实。鉴于此功能的复杂性,其应用的局限性以及三个简单的变通办法的存在,我不会为此而屏息!


2
感谢您对所有案例进行了如此详尽的解释。这为我解决了问题!
scobi 2011年

4
过载分辨率不能被尝试过载,因为分辨率决定一方法给予方法组参数。但这是我们正在尝试计算的参数类型!那是一个鸡与蛋的问题,我们没有试图解决。
埃里克·利珀特

8
说“方法组仅包含一个方法,所以让我们暂时说,即使我们不知道参数,重载解析也会成功”,实际上是添加了一个新的怪异的重载解析算法,该算法仅在包含一种方法。我们真的不想去那里。您是否真的想增加第二个重载会严重改变类型推断的工作方式?
埃里克·利珀特

5
最后,请注意,在C#4中,如果您要尝试推断的东西是返回类型,并且所有参数类型都已经成功推断出来,则将继续进行重载解析。
埃里克·利珀特

感谢您的评论埃里克!
scobi 2011年

8

我认为这是因为有两步推断:

  • 它必须推断出您要将A转换为通用委托

  • 它必须推断委托参数的类型是什么

我不确定这是否是原因,但我的直觉是两步推断对于编译器而言不一定很容易。


编辑:

只是预感,但是有什么告诉我第一步是问题。编译器必须弄清楚要转换为具有不同数量的通用参数的委托,因此无法推断参数的类型。


这对我来说听起来不错。我可以做到Test((string a) => {}),它可以很好地解决,也可以使用Pierre-Alain的例子,Test((Action<string>)A)并且也可以。但为什么?这是编译器的限制吗?规则有怪癖吗?现在,我开始思考,因为从未有过99%的时间将lambda传递给我编写的类似于Test的函数,所以我从未遇到过,所以永远不会发生此问题。
scobi 2011年

@斯科特:我认为您的第一个例子是一个很好的证据,证明这是事实;我没想到那个测试。关于“为什么”:我认为这是因为两步推理通常非常困难且耗时,而且没有人想到对此进行特殊处理(尽管我认为这对编译器作者来说并不那么令人信服哈哈,因为他们必须担心更糟糕的特殊情况)。
user541686 2011年

@Scott:实际上,我似乎错了,否则第一种情况应该已经编译了,对吧?
user541686 2011年

@Scott:或许不是-这仍然是一个两步的转换,一个推断StringActionAction<T>,一个推断T推断Tstring...
user541686

5

在我看来,这似乎是一个恶性循环。

Test方法需要从泛型类型构造的委托类型参数Action<T>。您改为传入一个方法组Test(A)。这意味着编译器必须将您的参数转换为委托类型(方法组转换)。

但是,哪种代表类型?要知道委托类型,我们需要知道T。我们没有明确指定它,因此编译器必须推断出它以找出委托类型。

为了推断方法的类型参数,我们需要知道方法参数的类型,在这种情况下为委托类型。编译器不知道参数类型,因此失败。

在所有其他情况下,两种类型的参数都是显而易见的:

// delegate is created out of anonymous method,
// no method group conversion needed - compiler knows it's Action<string>
Test((string a) => {});

// type of argument is set explicitly
Test((Action<string>)A); 

或明确指定类型参数:

Test<string>(A); // compiler knows what type of delegate to convert A to

PS更多关于类型推断


2
具体来说,匿名方法和方法组都不具有自己的类型,但是匿名方法会使类型推断做一些额外的工作来利用方法的参数类型(第7.5.2.1节,7.5.2.7节),而方法组(甚至只有一个成员的方法组)也不会。
zinglon 2011年

2

您正在传递方法A的名称。.Net框架可以将其转换为Action,但它是隐式的,对此不承担任何责任。

但尽管如此,方法名是明确的Action<>对象。因此,它不会将类型推断为Action类型。


“不承担任何责任”是什么意思?您是说编译器在完成这样的转换后停止,并且该选项此时未打开以进行推理?
scobi 2011年

不,我的意思是编译器不会从自己的转换中隐式推断类型。那是因为它不能安全地推断您的意图(通常,尽管在这种情况下,它似乎很明显)。这就是为什么在特定情况下(像这样),您需要进行显式强制转换以告诉您确切的类型。
Yochai Timmer

2

我可能是错的,但是我想C#无法推断类型的真正原因是由于方法重载和产生的歧义。例如,假设我有以下方法:void foo (int)void foo (float)。现在,如果我写var f = foofoo编译器应该选择哪个?同样,使用的示例也会发生相同的问题Test(foo)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.