C#中对IEnumerable<IList<object>>的foreach循环可以编译但不应该这样做

7

我有以下代码:

IEnumerable<IList<MyClass>> myData = //...getMyData

foreach (MyClass o in myData)
{
    // do something
}

它编译通过,并运行,但显然我得到了一个“System.InvalidCastException”错误。
为什么编译器不会报错?“MyClass”只是一个简单的bean,没有扩展。
编辑1:
如David所建议的,将类型从“IList”更改为“List”,编译器会报错。
编辑2:
我已经理解这种行为是符合C#语言定义的C# Language definition。然而,我不明白为什么允许这样的强制转换/转换,因为在运行时我总是会得到InvalidCastException。我打开this以深入了解。

使用Resharper时出现“可疑转换”错误。 - Tim Schmelter
1
@Emaborsa:同意,下面的回复对我来说是很有启发性的。只是想补充一下信息,因为我觉得这个还是很有趣的。 - David
2
@Emaborsa:编译器不会进行此代码分析(而Resharper会这样做)。实际上,可能存在一种类型,它继承自MyClass并实现了IList<MyClass>。编译器不会检查您分配了什么,它只检查左侧声明的类型。如果您分配了new List<MyClassChild>()并且MyClassChild是一个子类并实现了IList<MyClass>,那么该代码将完全有效。 - Tim Schmelter
@Rango 在 foreach 之外,仍然需要进行向下转换。也就是说,写成 MyClass foo = myData.GetEnumerator().Current; 是不合法的 - 它需要显式转换为 MyClass。向下转换通常不是隐式的。因此,问题仍然是为什么 foreach 会自动插入这样的转换。我猜想答案是为了让用户在泛型出现之前能够编写 foreach (MyType x in myUntypedList) - sepp2k
2
@sepp2k:foreach总是有一个内置的显式转换,它们可以枚举List<Object>并将其转换为您在循环变量类型中指定的任何内容。这将在运行时失败。当然,原因是foreach比泛型早得多。 - Tim Schmelter
显示剩余3条评论
4个回答

6

因为IList<MyClass>是一个接口,理论上你可以有一个实现该接口的类,并且派生自MyClass

如果你将它更改为IEnumerable<List<MyClass>>,它将无法编译。

无论如何,至少我会收到可疑转换的警告,因为解决方案中没有从IList<MyClass>MyClass两者继承的类。


1
我收到了一个可疑类型转换的警告,但这是来自 Resharper 的吧? - Tim Schmelter
@Rango 可能是的,说实话它看起来像一个本地警告,但我已经和 Resharper 工作了这么长时间,以至于我注意不到区别了。 - Pinx0
非本地警告。我使用的是原版VS 2017,没有显示任何错误或警告。 - Liam
@Liam 那一定是 Resharper。 - Pinx0

5

foreach 在编译时遵循的是一种模式,而不是特定类型(就像 LINQ 和 await 一样)。

foreach 不是在寻找一个 IEnumerableIEnumerable<T> 类型,而是在寻找一个具有 GetEnumerator() 方法的类型(IList<T> 就有这个方法)。而外部列表中的对象可以是派生自 MyClass 并实现 IList<T> 的类型。

也就是说,编译器只进行了轻量级的“匹配模式”检查,而不是完整的检查。

请参阅 C#5 语言规范的 §8.8.3 章节,其中详细讨论了这个问题(你会看到我上面简化了一些东西:甚至连 IEnumerator 都没有被检查,只要有一个 MoveNext() 方法和一个 Current 属性即可)。


4

IList<MyClass>可以转换为MyClass

但如果您使用非空的可枚举对象运行它,

IEnumerable<IList<MyClass>> myData = new IList<MyClass>[1] { new List<MyClass>() {new MyClass()}};

您会收到以下错误:

无法将类型为'System.Collections.Generic.List`1[MyClass]'的对象强制转换为类型'MyClass'。

这符合规范:

第8.8.4节 foreach语句

... 如果T(元素类型)与V(foreach语句中的本地变量类型)之间没有显式转换(§6.2),则会产生错误,并且不会采取进一步步骤。

...

IList<MyClass>MyClass之间存在显式转换(虽然在运行时会失败),因此不会产生错误。

第6.2.4节 显式引用转换

显式引用转换包括:

  • 从object和dynamic到任何其他引用类型。
  • 从任何类类型S到任何类类型T,条件是S是T的基类。
  • 从任何类类型S到任何接口类型T,条件是S未密封并且S未实现T。
  • 从任何接口类型S到任何类类型T,条件是T未密封或T实现了S。

...


我认为第三个语句是我想要的。但是,我不太理解它的含义。 - Emaborsa
@Emaborsa 第三个语句是什么?你能引用一下让我解释一下吗? - Sweeper
我以为你的回答是“从任何未密封的类类型S到任何接口类型T,前提是S没有实现T”,这是对我的问题的回答。 - Emaborsa
@Emaborsa 啊!我复制错了。这是更正后的内容:从任何接口类型 S 到任何类类型 T,前提是 T 不是密封的或者 T 实现了 S。 MyClass 没有被密封。因此,您可以从任何接口转换为 MyClass - Sweeper
我理解它的意思...但我不明白为什么允许这样的转换。 - Emaborsa
显示剩余6条评论

2

假设 MyClass 没有实现 IList<MyClass>,那么可能会有一个派生类型的 MyClass 实现了 IList<MyClass>,那么您的循环将是有效的。

也就是说,

class MyClass
{
}

class Derived : MyClass, IList<MyClass>
{
    // ...
}

// ...

// Here IList<MyClass> is Derived, which is valid because Derived implements IList<MyClass>
IEnumerable<IList<MyClass>> myData = new []{new Derived()};

// Here MyClass is Derived, which is valid because Derived inherits from MyClass
foreach (MyClass o in myData)
{
    // do something
}

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