巢状泛型:为什么编译器不能推断出这种情况下的类型参数?

22

我在玩一个业余项目时,遇到了一个我不理解的类型推断错误。我将其简化为以下的小例子。

我有以下类和函数:

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>

更新: 我发现将第二个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()));
为什么它可以完美地执行所有单独的步骤,但不能一次完成整个推理过程?编译器分析隐式lambda类型和隐式泛型类型的顺序中是否存在一些微妙之处?

请注意,您可以将 (Bar y) => new Baz() 强制转换为 Func<Bar, Baz>(甚至可以省略 Bar 中的 Bar y),而不是使用身份函数。这样就可以编译了。 - Rawling
3个回答

19
因为按照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`。
然后是第二阶段,它实际上是一个循环,尝试缩小每个泛型类型参数的类型,直到找到所有泛型类型参数或放弃为止。最重要的一个要点是最后一个:

7.5.2.2 第二阶段

第二阶段的处理如下:

  • [...]
  • 否则,对于所有参数 Eᵢ 及其相应的参数类型 Tᵢ ,其中 输出类型(§7.5.2.4)包含尚未确定的类型变量 Xj输入类型(§7.5.2.3)不包含时,将从 EᵢTᵢ 进行 输出类型推断(§7.5.2.6),然后重复第二阶段。

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

  • 否则,如果 委托的返回类型(即 Func<T2,T3>)包含一个 尚未确定 的类型变量(是的),但其参数类型(即 T1)没有(它们没有,我们已经知道 T1 = Foo ),则将从 EᵢTᵢ 进行输出类型推断(§7.5.2.6) 。]

现在进行输出类型推断的处理如下,这次只有一个符合条件的项目,即第一个:

7.5.2.6 输出类型推断

将表达式 E 推断为类型 T 的方式如下:

  • 如果 E是一个匿名函数 [是],具有推断出的返回类型 U(§7.5.2.12),并且 T 是一个带有返回类型 Tb 的委托类型或表达式树类型,则从UTb 进行下限推断(§7.5.2.9)。
  • 否则,[其余部分省略]

“推断出的返回类型”U是匿名lambda表达式 (Bar y) => new Baz(),而TbFunc<T2,T3>。执行下限推断

我认为现在不需要引用整个下限推理算法(它很长);只需说它没有提及匿名函数。它处理继承关系、接口实现、数组协变性、接口和委托的协变/逆变性等,但不包括 lambda。因此,它的最后一个要点适用于:
否则,不做任何推理。
然后我们回到第二阶段,因为对 T2 和 T3 没有进行任何推理而放弃。
故事的寓意是:类型推断算法不递归使用 lambda。它只能从外部 lambda 的参数和返回类型推断类型,而不能从嵌套在其中的 lambda 推断类型。只有下限推理是递归的(这样它就可以将嵌套的泛型结构如 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,Tb 是 Func。因此,下限推理将成功,因为它将面临两个显式委托类型(Func 和 Func),但没有匿名函数/lambda。这就是标识函数使其成功的原因。

3
一份清晰而全面的说明一些非常复杂的内容。好的答案,谢谢! - verdesmarald

1

由于 Lambda 表达式没有被分配,编译器无法推断其返回类型。 请查看此 link 了解编译器如何确定 Lambda 表达式的返回类型。 如果您有:

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

如果此时 lambda 表达式被分配了值,编译器就能够根据分配给它的内容计算其返回类型;但由于现在它没有被分配任何值,编译器无法确定 (Bar y) => new Baz(); 的返回类型是什么。


在示例2中(F((Foo x) => new Bar());),编译器毫无问题地推断出Func<Foo, Bar>作为参数类型。在您的答案中的示例(G((Foo x) => f);)中,编译器也可以毫无问题地推断出Func <Foo, Func<Bar, Baz>>作为参数类型。如果它可以独立地推断出(Foo x) => new Bar()(Foo x) => f的类型,那么为什么将它们链接在一起就会失败呢? - verdesmarald
在示例2中,您不必推断嵌套函数的返回类型,而是对参数进行推断,参数的类型充当分配类型。将其嵌套在另一个lambda中不会为其提供要分配的返回类型,就像示例2中提供的参数一样。 - Cornelius

0
对于编译器而言,Lambda函数与Func是不同的,即使用Lambda函数作为Func意味着进行类型转换。当专门化泛型时,编译器不会执行“嵌套”类型转换。然而,在您的示例中需要执行此操作:
(Foo x) => (Bar y) => new Baz () 的类型是 lambda (Foo, lambda (Bar, Baz)),但需要 Func (T1, Func (T2, T3)),即两个嵌套的转换。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接