嵌套generics:为什么编译器在这种情况下不能推断出类型参数?

当我遇到一个我不理解的类型推断错误时,我正在玩一个爱好项目。 我把它简化为以下简单的例子。

我有以下类和函数:

class Foo { } class Bar { } class Baz { } static T2 F(Func f) { return default(T2); } static T3 G(Func<T1, Func> f) { return default(T3); } 

现在考虑以下示例:

 // 1. F with explicit type arguments - Fine F(x => new Bar()); // 2. F with implicit type arguments - Also fine, compiler infers  F((Foo x) => new Bar()); // 3. G with explicit type arguments - Still fine... G(x => y => new Baz()); // 4. G with implicit type arguments - Bang! // Compiler error: Type arguments cannot be inferred from usage G((Foo x) => (Bar y) => new Baz()); 

最后一个示例产生编译器错误,但在我看来它应该能够毫无问题地推断出类型参数。

问题:为什么编译器在这种情况下不能推断

更新:我发现简单地将第二个lambda包装在一个标识函数中将导致编译器正确地推断出所有类型:

 static Func I(Func f) { return f; } // Infers G and I G((Foo x) => I((Bar y) => new Baz())); 

为什么它可以完美地完成所有单个步骤,而不是一次完成整个推理? 编译器分析隐式lambda类型和隐式generics类型的顺序是否存在一些细微之处?

因为在这种情况下,C#规范中描述的算法不成功。 让我们看一下规范,看看为什么会这样。

算法描述冗长而复杂,所以我会大量缩写。

算法中提到的相关类型具有以下值:

  • Eᵢ =匿名lambda (Foo x) => (Bar y) => new Baz()
  • Tᵢ =参数类型( Func>
  • Xᵢ =三个通用类型参数( T1T2T3

首先,有第一阶段, 在你的情况下只做一件事:

7.5.2.1第一阶段

对于每个方法参数Eᵢ (在您的情况下,只有一个,lambda):

  • 如果Eᵢ是一个匿名函数[它],则从EᵢTᵢ进行显式参数类型推断 (第7.5.2.7节)
  • 否则,[不相关]
  • 否则,[不相关]
  • 否则,不会对此参数进行推断。

我将在这里跳过显式参数类型推断的细节; 对于调用G((Foo x) => (Bar y) => new Baz())就足够了,它推断出T1 = Foo

然后是第二阶段,它实际上是一个循环,试图缩小每个generics类型参数的类型,直到它找到所有这些或放弃。 一个重要的要点是最后一个:

7.5.2.2第二阶段

第二阶段进行如下:

  • […]
  • 否则,对于具有相应参数类型Tᵢ所有参数Eᵢ ,其中输出类型 (第7.5.2.4节)包含不固定类型变量Xj输入类型 (第7.5.2.3节)不包含, 输出类型推断 (第7.5.2.6节)是 Eᵢ Tᵢ 。 然后重复第二阶段。

[翻译并应用于您的案例,这意味着:

  • 否则,如果委托返回类型 (即Func )包含尚未确定的类型变量(确实如此)但其参数类型 (即T1 )不包含(它们没有,我们已经知道T1 = Foo ), 输出类型推断 (第7.5.2.6节)。

输出类型推断现在进行如下; 再次,只有一个子弹点是相关的,这次是第一个:

7.5.2.6输出类型推断

输出类型推断 表达式E 类型T以下列方式进行:

  • 如果E是具有推断返回类型U (第7.5.2.12节)的匿名函数[它],并且T是具有返回类型Tb的委托类型或表达式树类型,则进行下限推理 (第7.5.2.9节) U Tb
  • 否则,[rest剪断]

“推断返回类型” U是匿名lambda (Bar y) => new Baz()TbFunc 。 提示下限推断

我认为我现在不需要引用整个下界推理算法(它很长); 它足以说它没有提到匿名函数。 它负责inheritance关系,接口实现,数组协方差,接口和委托协调/反演,……但不是lambdas。 因此,它的最后一个要点适用:

  • 否则,不做任何推论。

然后我们回到第二阶段,因为没有对T2T3进行推论而放弃了。

故事的道德:类型推理算法不是lambda的递归。 它只能从参数和外部lambda的返回类型推断出类型,而不是嵌套在它内部的lambdas。 只有下界推断才是递归的(因此它可以采用嵌套的通用结构,如List, T2>> ),但既不是输出类型推断 (第7.5.2.6节),也不是显式参数类型推断 (第7.5节) .2.7)是递归的,永远不会应用于内部lambda。

附录

当您向该识别函数I添加调用时:

  • G((Foo x) => I((Bar y) => new Baz()));

然后类型推断首先应用于对I的调用,这导致I的返回类型被推断为Func 。 然后外部lambda的“推断返回类型” U是委托类型Func并且TbFunc 。 因此, 下限推理将成功,因为它将面临两个显式委托类型( FuncFunc )但没有匿名函数/ lambdas。 这就是识别function使其成功的原因。

lambda无法推断它的返回类型是什么,因为它没有分配,也不能由编译器确定。 查看此链接 ,了解lambdas返回类型的方式由编译器确定。 如果你愿意:

 Func f = (Bar y) => new Baz(); G((Foo x) => f); 

那么编译器就能够根据它的分配来计算lambda的返回类型,但是从现在开始它没有分配给任何东西,编译器很难确定(Bar y) => new Baz();的返回类型(Bar y) => new Baz(); 将会。

对于编译器,lambda函数与Func不同,即对Func使用lambda函数意味着类型转换。 在专门化generics时,编译器不会执行“嵌套”类型转换。 但是,在您的示例中需要:

类型(Foo x) => (Bar y) => new Baz ()lambda (Foo, lambda (Bar, Baz)) ,但是需要Func (T1, Func (T2, T3)) ,即两次转换,是嵌套的。