C#泛型类型推理与协方差 - 错误或限制

时间:2017-06-27 09:50:36

标签: c# generics type-inference covariance

当具有相关参数的通用方法推断出类型时,它会在某些情况下产生意外结果。如果我明确指定了类型,那么一切都可以正常运行而无需进一步的更改。

IEnumerable<List<string>> someStringGroups = null; // just for demonstration
IEqualityComparer<IEnumerable<string>> someSequenceComparer = null;

var grouped = someStringGroups
  .GroupBy(x => x, someSequenceComparer);

当然,上面的代码并不打算执行,但它表明grouped的结果类型是IEnumerable<IEnumerable<string>,List<string>>而不是IEnumerable<List<string>,List<string>>,因为{{1} }。

如果我明确指定类型,一切都很好。

x => x

如果我不使用明确的比较器,一切都可以正常运行。

我认为问题在于,使用所提供的参数类型(var grouped = someStringGroups .GroupBy<List<string>,List<string>>(x => x, someSequenceComparer); )的最小公分母将{em>优先级超过IEnumerable<string>接口的协方差。我本来期望相反的,即泛型方法应该推断出参数满足的最具体的类型。

问题是:这是错误还是记录在案的行为?

2 个答案:

答案 0 :(得分:1)

  

我本来期望相反的,即泛型方法应该推断出参数满足的最具体的类型。

基于什么,究竟是什么?

您看到的行为已记录在案并符合C#规范。正如您可能想象的那样,类型推断规范相当复杂。我不会在这里引用整个内容,但如果您有兴趣,可以自己查看。相关部分为 7.5.2类型推断

根据你写的评论,我认为至少部分混淆源于你忘记了这个方法有三个参数,而不是两个(这会影响推理的进行)。此外,似乎你期望第二个参数keySelector委托影响类型推断,当它不在这种情况下时(至少,不是直接...它在类型参数之间创建依赖关系,但不是在物质方面)。

但我认为主要的是你期望类型推断对类型差异更具侵略性,而不是事实上要求的规范。

在类型推断期间,首先发生的事情在规范中描述,在 7.5.2.1第一阶段下。第二个论点是,在这个阶段的所有意图和目的,都被忽略了。它没有为其参数显式声明的类型(但是,如果它的话,它并不重要)。在此阶段,类型推断开始为类型参数开发 bounds ,但不会自行修复参数。

您正在调用此GroupBy()的重载:

public static IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>(
    this IEnumerable<TSource> source,
    Func<TSource, TKey> keySelector,
    IEqualityComparer<TKey> comparer)

需要推断出两种类型参数,TSourceTKey。在推理期间,编译器确定类型参数的上限和下限。但这些都是基于传递给方法调用的类型。编译器不会搜索满足类型要求的备用基类或派生类型。

因此,对于TSource,标识了List<string>的下限,而对于TKey,标识了IEnumerable<string>的上限( 7.5 .2.9下限推论)。这些类型是您为调用提供的类型,因此这是编译器使用的类型。

在第二阶段,尝试修复类型。 TSource不依赖于任何其他参数,因此它首先被修复为List<string>。第二阶段的第二次复飞修复了TKey。虽然类型方差允许TKey设置的边界以容纳List<string>,但是没有必要,因为根据其边界,您传入的类型可以直接使用。

因此,您最终会使用IEnumerable<string>

当然,编译器使用List<string>代替TKey是合法的(如果不符合规范)。如果相应地显式地转换参数,我们可以看到这个工作:

var grouped2 = someStringGroups
  .GroupBy(x => x, (IEqualityComparer<List<string>>)someSequenceComparer);

这改变了用于调用的表达式的类型,因此改变了使用的边界,最后改变了推理期间选择的实际类型。但是在原始调用中,编译器在推理期间不需要使用与您指定的类型不同的类型,即使它已经被允许,因此它没有。

C#规范有一些相当毛茸茸的部分。类型推断肯定是其中之一,坦率地说,我不是解释规范的这一部分的专家。这让我头疼,并且肯定有一些我可能不理解的更具挑战性的角落情况(即我怀疑我可以实现规范的这一部分,而不需要更多的研究)。但我相信以上是对与你的问题相关的部分的正确解释,我希望我已经做了一个合理的工作来解释它。

答案 1 :(得分:0)

我很确定这是预期的行为。

我们感兴趣的方法签名是:

public static IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>(
    this IEnumerable<TSource> source,
    Func<TSource, TKey> keySelector,
    IEqualityComparer<TKey> comparer
)

source属于IEnumerable<List<string>>类型,因此TSource的自然选择是List<string>comparer的类型为IEqualityComparer<IEnumerable<string>>,因此TKey的自然选择是IEnumerable<string>

如果我们再查看最后一个参数keySelectorx=>x。这是否满足我们迄今为止的类型约束?是的,因为x是List<string>,可以隐式转换为IEnumerable<string>

此时为什么编译器要寻找更多东西?无需铸造的自然而明显的选择是有效的,所以它使用它。如果你不喜欢它,你总是可以像你一样做,并明确说明通用参数。

当然你可以制作类型IEqualityComparer<List<string>>的比较器,在这种情况下你的输出对象将是你期望的类型(我希望你能看出为什么会这样)。