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

ver*_*ald 22 c# type-inference nested-generics

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

我有以下类和函数:

class Foo { }
class Bar { }
class Baz { }

static T2 F<T1, T2>(Func<T1, T2> f) { return default(T2); }
static T3 G<T1, T2, T3>(Func<T1, Func<T2, T3>> f) { return default(T3); }
Run Code Online (Sandbox Code Playgroud)

现在考虑以下示例:

// 1. F with explicit type arguments - Fine
F<Foo, Bar>(x => new Bar());

// 2. F with implicit type arguments - Also fine, compiler infers <Foo, Bar>
F((Foo x) => new Bar());

// 3. G with explicit type arguments - Still fine...
G<Foo, Bar, Baz>(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());
Run Code Online (Sandbox Code Playgroud)

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

问题:为什么编译器<Foo, Bar, Baz>在这种情况下无法推断?

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

static Func<T1, T2> I<T1, T2>(Func<T1, T2> f) { return f; }

// Infers G<Foo, Bar, Baz> and I<Bar, Baz>
G((Foo x) => I((Bar y) => new Baz()));
Run Code Online (Sandbox Code Playgroud)

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

Tim*_*mwi 18

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

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

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

  • E? =匿名的lambda (Foo x) => (Bar y) => new Baz()
  • T?=参数类型(Func<T1, Func<T2, T3>>)
  • X?=三种一般类型参数(T1,T2,T3)

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

7.5.2.1第一阶段

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

  • 如果E?是一个匿名函数[它是]时,显式参数类型推断(§7.5.2.7)从制成E?于T?
  • 否则,[不相关]
  • 否则,[不相关]
  • 否则,不会对此参数进行推断.

我将在这里跳过显式参数类型推断的细节; 它足以说明,对于呼叫G((Foo x) => (Bar y) => new Baz()),它推断出T1= Foo.

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

7.5.2.2第二阶段

第二阶段进行如下:

  • [...]
  • 否则,对于所有参数E?与相应的参数类型T?,其中的输出类型(§7.5.2.4)包含未定影型变量Xj,但输入类型(§7.5.2.3)不这样做,一个输出类型推断(§7.5.2.6)由从 E? 到 T?.然后重复第二阶段.

[翻译并适用于您的情况,这意味着:

  • 否则,如果委托的返回类型(即Func<T2,T3>)包含一个尚未确定的类型变量(它确实),但它的参数类型(即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.
  • 否则,[休息剪断]

"推断的返回类型" U是匿名的lambda (Bar y) => new Baz()而且Tb是Func<T2,T3>.提示下限推断.

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

  • 否则,不做任何推论.

然后,我们再回到第二阶段,因为没有推论已经作出了其放弃T2和T3.

故事的道德:类型推理算法不是lambda的递归.它只能从参数中推断出类型,并返回外部lambda的返回类型,而不是嵌套在它内部的lambdas.只有下限推理是递归的(因此它可以采用嵌套的通用结构,例如,List<Tuple<List<T1>, T2>>除了)但是输出类型推断(第7.5.2.6节)和显式参数类型推断(第7.5.2.7节)都不是递归的,并且永远不会应用于内部lambdas .

附录

当您向该识别功能添加调用时I:

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

然后类型推断首先应用于调用I,这导致I返回类型被推断为Func<Bar, Baz>.然后U外部lambda 的"推断返回类型" 是委托类型Func<Bar, Baz>而且Tb是Func<T2, T3>.因此,下限推理将成功,因为它将面临两个显式委托类型(Func<Bar, Baz>和Func<T2, T3>)但没有匿名函数/ lambdas.这就是识别功能使其成功的原因.

  • 对一些非常复杂的东西进行清晰而彻底的解释.很好的回答,谢谢! (3认同)