我在玩弄一个爱好的项目,当我遇到一个类型推断的错误,我不明白来了。 我把它简化为以下简单的例子。
我有以下类和函数:
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); }
现在考虑下面的例子:
// 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());
最后一个例子产生一个编译器错误,但在我看来,它应该能够推断出类型参数没有任何问题。
问:为什么不能编译器推断<Foo, Bar, Baz>
在这种情况下?
更新:我发现,只是在包装标识功能的第二拉姆达将导致编译器能够正确推断所有的类型:
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()));
它为什么能做到各个步骤完美,但不是整个推论一次? 是否有顺序的一些细微之处,编译器分析隐含拉姆达的隐含类型泛型类型?
因为如在C#说明书中描述的算法并不在这种情况下成功。 让我们看一下说明书,才能看到这是为什么。
该算法的描述是长期而复杂的,所以我会严重缩写本。
在算法中提到的相关类型有下列值给你:
-
Eᵢ
=匿名拉姆达(Foo x) => (Bar y) => new Baz()
-
Tᵢ
=参数类型( Func<T1, Func<T2, T3>>
) -
Xᵢ
=三种一般类型参数( T1
, T2
, T3
)
首先,有第一阶段,而你的情况做的只有一件事:
7.5.2.1第一阶段
对于每个方法参数的Eᵢ
(在你的情况下,只有一个,拉姆达):
- 如果
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
是匿名拉姆达(Bar y) => new Baz()
和Tb
是Func<T2,T3>
提示下限推断 。
我不认为我需要引用整个下限推理算法现在(这是长); 这是不够的说,它并没有提到匿名函数。 这需要照顾的继承关系,接口实现,阵列协方差,接口类型和委托合作/逆变,...但不lambda表达式。 因此,它的最后一颗子弹一点也适用:
然后,我们再回到第二阶段,因为没有推论已经作出了其放弃T2
和T3
。
这个故事的寓意:类型推理算法的不是递归与lambda表达式。 它只能从参数推断类型和返回类型外的λ,不lambda表达式嵌套在它的内部。 只有下限推论是递归的(因此,它可能需要嵌套通用结构像List<Tuple<List<T1>, T2>>
开),但既没有输出类型推论 (§7.5.2.6),也不显式参数类型推论 (第7.5节1.2.7)是递归的,并且从不施加到内lambda表达式。
附录
当您添加调用识别功能I
:
G((Foo x) => I((Bar y) => new Baz()));
然后键入推理首先被施加到该呼叫到I
,这导致I
的返回类型被推断为Func<Bar, Baz>
。 那么“推断返回类型” U
外的λ是委托类型Func<Bar, Baz>
和Tb
是Func<T2, T3>
因此下限推断会成功,因为它将会面临两个显式委托类型( Func<Bar, Baz>
和Func<T2, T3>
但没有匿名函数/ lambda表达式。 这就是为什么识别功能,使得它取得成功。
拉姆达无法推断它的返回类型是什么,因为它不是分配的,不能由编译器来决定。 看看这个链接就如何lambda表达式返回类型由编译器来决定。 如果你会haved:
Func<Bar, Baz> f = (Bar y) => new Baz();
G((Foo x) => f);
那么编译器就已经能够计算基于它分配给拉姆达的返回类型,但因为现在没有分配到任何编译器的斗争,以确定什么的返回类型(Bar y) => new Baz();
将会。
对于编译器,一个拉姆达功能是从一个函数功能不同,即,使用一个lambda函数为函数求意味着类型转换。 专业仿制药时,编译器不会做“嵌套”类型转换。 然而,这将是你的榜样要求:
的类型(Foo x) => (Bar y) => new Baz ()
是lambda (Foo, lambda (Bar, Baz))
但Func (T1, Func (T2, T3))
将需要,即,两个转换,这是嵌套的。
文章来源: Nested Generics: Why can't the compiler infer the type arguments in this case?