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

时间:2012-09-04 02:30:25

标签: 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); }

现在考虑以下示例:

// 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>

UPDATE:我发现简单地将第二个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类型和隐式泛型类型的顺序是否存在一些细微之处?

3 个答案:

答案 0 :(得分:18)

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

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

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

  • Eᵢ =匿名lambda (Foo x) => (Bar y) => new Baz()
  • Tᵢ =参数类型(Func<T1, Func<T2, T3>>
  • Xᵢ =三种通用类型参数(T1T2T3

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

  

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的委托类型或表达式树类型{1}},然后下限推理(§7.5.2.9)从 U {{1> }}。
  •   
  • 否则,[rest snipped]
  •   

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

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

  
      
  • 否则,不做推论。
  •   

然后我们回到第二阶段,因为没有对Func<T2,T3>T2进行任何推断而放弃了。

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

附录

当您添加对该识别函数List<Tuple<List<T1>, T2>>的调用时:

  • I

然后类型推断首先应用于对G((Foo x) => I((Bar y) => new Baz()));的调用,这会导致I的返回类型被推断为I。然后,外部lambda的“推断返回类型”Func<Bar, Baz>是委托类型UFunc<Bar, Baz>Tb。因此,下限推理将成功,因为它将面临两个显式委托类型(Func<T2, T3>Func<Bar, Baz>)但没有匿名函数/ lambdas。这就是识别功能使其成功的原因。

答案 1 :(得分:1)

lambda无法推断它的返回类型是什么,因为它没有被分配,并且无法由编译器确定。 查看此link有关lambdas返回类型的方式由编译器确定。 如果你愿意:

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

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

答案 2 :(得分:0)

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

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