为什么C#隐式地将使用实现接口的引用类型参数化的泛型类型转换为使用已实现接口参数化的相同泛型类型,但不对引用类型执行相同的隐式转换?
基本上,为什么第一行编译但第二行失败?
IEnumerable<IComparable<Version>> x = Enumerable.Empty<Version>();
IEnumerable<IComparable<int>> y = Enumerable.Empty<int>();
特别棒的是对描述此行为的规范部分的引用。
答案 0 :(得分:3)
尽管名称为“隐式”,但隐式转换不适用,除非规则明确说明了这些,并且规则在从IEnumerable<int>
转到IEnumerable<IComparable<int>>
时不允许装箱转换。作为一个更简单的情况,出于同样的原因,您无法从IEnumerable<int>
转到IEnumerable<object>
,并且该案例已有详细记录。
好的,首先,为什么将 IEnumerable<T>
转换为IEnumerable<IComparable<T>>
?这将在§6.1.6(C# Language Specification 5.0)中进行介绍:
隐式参考转换是:
[...]
- 从任何引用类型到接口或委托类型
T
,如果它具有到接口的隐式标识或引用转换或 委托类型T0
和T0
是方差可兑换(第13.1.3.2节)到T
。
并且§13.1.3.2说:
类型
T<A1, …, An>
是 方差可转换为类型T<B1, …, Bn>
如果T
是接口或委托类型 使用变体类型参数T<X1, …, Xn>
声明, 并为每个变体类型参数Xi
以下之一:
Xi
是协变的,从Ai
到Bi
存在隐式引用或标识转换IEnumerable<T>
由于T
中的T
是协变的,这意味着如果存在从IComparable<T>
到IEnumerable<T>
的隐式引用或身份转换,然后存在从IEnumerable<IComparable<T>>
到Version
的隐式引用转换,因为它们是方差可转换的。
IComparable<Version>
实现S
,因此存在隐式引用转换:
- 从任何类型
T
到任何接口类型S
,提供T
实现IEnumerable<int>
。
是的,现在,为什么IEnumerable<IComparable<int>>
没有隐式转换为int
?毕竟,IComparable<int>
会隐式转换为IComparable<int> x = 0; // sure
:
IEnumerable<int>
但它通过引用转换或身份转换实现 ,但是通过装箱转换(第6.1.7节):
从任何非可空值类型 [...]到存在装箱转换 由非可空值类型实现的任何接口类型。
§13.1.3.2的规则在考虑是否可以进行方差转换时不允许装箱转换,并且没有其他规则可以实现从IEnumerable<IComparable<int>>
到object x = 0; // sure, an int is an object
IEnumerable<object> x = new int[] { 0 }; // except when it's not
的隐式转换。尽管名称如此,隐式转换仍由明确的规则涵盖。
这个问题实际上有一个更为简单的例子:
int
出于同样的原因,不允许这样做:没有从object
到IEnumerable<int>
的引用转换,只有装箱转换,而且不考虑转换。在这种形式中,Stack Overflow上有几个问题可以解释为什么不允许这样做(如this one)。总结一下:这不是不可能,但为了支持它,编译器必须生成支持代码以便在某处粘贴用于装箱转换的代码。 C#团队重视这种情况下易用性的透明度,并决定仅允许保留身份的转换。
最后,作为一个实际考虑,假设你有一个IEnumerable<IComparable<int>>
而你需要一个Func<int, IComparable<int>> asComparable = i => i; // compiles to ldarg ; box ; ret
IEnumerable<IComparable<int>> x = Enumerable.Empty<int>().Select(asComparable);
,你会怎么做到的?好吧,自己做拳击:
Enumerable.Cast
当然使用if (jsonObject.entrySet().isEmpty()) {
}
会更实用;我这样写是为了强调隐含的转换。这项行动需要付出代价,而这正是重点所在。 C#设计师希望这个成本是明确的。