嵌套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ᵢ
=三个通用类型参数(T1
,T2
,T3
)
首先,有第一阶段, 在你的情况下只做一件事:
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()
, Tb
是Func
。 提示下限推断 。
我认为我现在不需要引用整个下界推理算法(它很长); 它足以说它没有提到匿名函数。 它负责inheritance关系,接口实现,数组协方差,接口和委托协调/反演,……但不是lambdas。 因此,它的最后一个要点适用:
- 否则,不做任何推论。
然后我们回到第二阶段,因为没有对T2
和T3
进行推论而放弃了。
故事的道德:类型推理算法不是lambda的递归。 它只能从参数和外部lambda的返回类型推断出类型,而不是嵌套在它内部的lambdas。 只有下界推断才是递归的(因此它可以采用嵌套的通用结构,如List
),但既不是输出类型推断 (第7.5.2.6节),也不是显式参数类型推断 (第7.5节) .2.7)是递归的,永远不会应用于内部lambda。
附录
当您向该识别函数I
添加调用时:
-
G((Foo x) => I((Bar y) => new Baz()));
然后类型推断首先应用于对I
的调用,这导致I
的返回类型被推断为Func
。 然后外部lambda的“推断返回类型” U
是委托类型Func
并且Tb
是Func
。 因此, 下限推理将成功,因为它将面临两个显式委托类型( Func
和Func
)但没有匿名函数/ 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))
,即两次转换,是嵌套的。