C# 泛型类型推断与协变 - 错误或限制
C# generic type inference versus covariance - bug or restriction
当具有依赖参数的泛型方法推断类型时,它在某些情况下会产生意想不到的结果。如果我明确指定类型,则一切正常,无需任何进一步更改。
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>>
因为 x => x
.
如果我明确指定类型,一切都很好。
var grouped = someStringGroups
.GroupBy<List<string>,List<string>>(x => x, someSequenceComparer);
如果我不使用显式比较器,一切都会按预期工作。
我认为问题是采用提供的参数类型的最小公分母 (IEnumerable<string>
) 优先于 IEqualityComparer<>
的协方差] 界面。我本以为相反,即泛型方法应该推断出参数满足的最具体的类型。
问题是:这是 错误还是记录在案的行为?
我很确定这是预期的行为。
我们感兴趣的方法签名是:
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>
.
如果我们再看最后一个参数keySelector
是x=>x
。这是否满足我们目前的类型限制?是的,因为 x 是 List<string>
并且可以隐式转换为 IEnumerable<string>
.
在这一点上,编译器为什么还要去寻找更多的东西?不需要铸造的自然而明显的选择有效,因此它使用了它。如果您不喜欢它,您始终可以选择照原样做并明确说明通用参数。
或者当然你可以将你的比较器设为 IEqualityComparer<List<string>>
类型,在这种情况下你的输出对象将是你期望的类型(我希望你能明白这是为什么)。
I would have expected the opposite, i.e. a generic method should infer the most specific type that is satisfied by the arguments.
具体基于什么?
您看到的行为已记录并符合 C# 规范。正如您想象的那样,类型推断规范相当复杂。这里就不全文引用了,有兴趣的可以自行查阅。相关部分是 7.5.2 类型推断。
根据您所写的评论,我认为至少部分混淆源于您忘记了此方法有 三个 个参数,而不是两个(这会影响如何推理进行)。此外,您似乎期望第二个参数 keySelector
委托会影响类型推断,而在这种情况下它不会(至少,不会直接……它会在类型参数之间创建依赖关系,但不会material 方式)。
但我认为最主要的是您期望类型推断比规范实际要求的类型变化更积极。
在类型推断期间,首先发生的事情在规范中有描述,在7.5.2.1 第一阶段 下。出于本阶段的所有意图和目的,第二个参数被忽略。它没有为其参数显式声明类型(尽管如此,如果有也没有关系)。在此阶段,类型推断开始为类型参数开发边界,但不会固定参数本身。
您正在调用 GroupBy()
的重载:
public static IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
IEqualityComparer<TKey> comparer)
有两个类型参数需要推断,TSource
和TKey
。在推理期间,编译器确实确定了类型参数的上限和下限。但这些都是基于传递给方法调用的类型。编译器不会搜索满足类型要求的替代基类型或派生类型。
因此,对于 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# 规范有一些相当复杂的部分。类型推断绝对是其中之一,坦率地说,我不是解释规范这一部分的专家。这让我头疼,而且肯定有一些我可能不理解的更具挑战性的极端情况(即我怀疑我是否可以 实现 规范的这一部分,而无需更多研究).但我相信以上是对与您的问题相关的部分的正确解释,我希望我已经做了合理的解释工作。
当具有依赖参数的泛型方法推断类型时,它在某些情况下会产生意想不到的结果。如果我明确指定类型,则一切正常,无需任何进一步更改。
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>>
因为 x => x
.
如果我明确指定类型,一切都很好。
var grouped = someStringGroups
.GroupBy<List<string>,List<string>>(x => x, someSequenceComparer);
如果我不使用显式比较器,一切都会按预期工作。
我认为问题是采用提供的参数类型的最小公分母 (IEnumerable<string>
) 优先于 IEqualityComparer<>
的协方差] 界面。我本以为相反,即泛型方法应该推断出参数满足的最具体的类型。
问题是:这是 错误还是记录在案的行为?
我很确定这是预期的行为。
我们感兴趣的方法签名是:
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>
.
如果我们再看最后一个参数keySelector
是x=>x
。这是否满足我们目前的类型限制?是的,因为 x 是 List<string>
并且可以隐式转换为 IEnumerable<string>
.
在这一点上,编译器为什么还要去寻找更多的东西?不需要铸造的自然而明显的选择有效,因此它使用了它。如果您不喜欢它,您始终可以选择照原样做并明确说明通用参数。
或者当然你可以将你的比较器设为 IEqualityComparer<List<string>>
类型,在这种情况下你的输出对象将是你期望的类型(我希望你能明白这是为什么)。
I would have expected the opposite, i.e. a generic method should infer the most specific type that is satisfied by the arguments.
具体基于什么?
您看到的行为已记录并符合 C# 规范。正如您想象的那样,类型推断规范相当复杂。这里就不全文引用了,有兴趣的可以自行查阅。相关部分是 7.5.2 类型推断。
根据您所写的评论,我认为至少部分混淆源于您忘记了此方法有 三个 个参数,而不是两个(这会影响如何推理进行)。此外,您似乎期望第二个参数 keySelector
委托会影响类型推断,而在这种情况下它不会(至少,不会直接……它会在类型参数之间创建依赖关系,但不会material 方式)。
但我认为最主要的是您期望类型推断比规范实际要求的类型变化更积极。
在类型推断期间,首先发生的事情在规范中有描述,在7.5.2.1 第一阶段 下。出于本阶段的所有意图和目的,第二个参数被忽略。它没有为其参数显式声明类型(尽管如此,如果有也没有关系)。在此阶段,类型推断开始为类型参数开发边界,但不会固定参数本身。
您正在调用 GroupBy()
的重载:
public static IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
IEqualityComparer<TKey> comparer)
有两个类型参数需要推断,TSource
和TKey
。在推理期间,编译器确实确定了类型参数的上限和下限。但这些都是基于传递给方法调用的类型。编译器不会搜索满足类型要求的替代基类型或派生类型。
因此,对于 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# 规范有一些相当复杂的部分。类型推断绝对是其中之一,坦率地说,我不是解释规范这一部分的专家。这让我头疼,而且肯定有一些我可能不理解的更具挑战性的极端情况(即我怀疑我是否可以 实现 规范的这一部分,而无需更多研究).但我相信以上是对与您的问题相关的部分的正确解释,我希望我已经做了合理的解释工作。