C#`foreach`行为 – 澄清?

我在这里阅读了Eric关于foreach枚举的文章以及foreach可以工作的不同场景

为了防止旧的C#版本进行装箱,C#团队为foreach启用了鸭子类型,以便在非Ienumerable集合上运行。(返回具有公共MoveNextCurrent属性的公共GetEnumerator就足够了(。

所以,埃里克写了一个样本 :

 class MyIntegers : IEnumerable { public class MyEnumerator : IEnumerator { private int index = 0; object IEnumerator.Current { return this.Current; } int Current { return index * index; } public bool MoveNext() { if (index > 10) return false; ++index; return true; } } public MyEnumerator GetEnumerator() { return new MyEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); } } 

但我相信它有一些拼写错误(在Current属性实现中缺少get访问器)阻止它编译(我已经通过电子邮件发送给他)。

无论如何这里是一个工作版本:

 class MyIntegers : IEnumerable { public class MyEnumerator : IEnumerator { private int index = 0; public void Reset() { throw new NotImplementedException(); } object IEnumerator.Current { get { return this.Current; } } int Current { get { return index*index; } } public bool MoveNext() { if (index > 10) return false; ++index; return true; } } public MyEnumerator GetEnumerator() { return new MyEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); } } 

好。

根据MSDN :

如果类型C实现System.Collections.IEnumerable接口通过满足以下所有条件来实现collection pattern ,则称其为collection type

  • C包含一个带有签名GetEnumerator()的公共实例方法,该签名返回struct-type,class-type或interface-type,在下文中称为E.

  • E包含一个带有签名MoveNext()和返回类型bool的公共实例方法。

  • E包含一个名为Current的公共实例属性,允许读取当前值。 该属性的类型被称为集合类型的元素类型。

好。 让我们将文档与Eric的样本相匹配

Eric的样本称为collection type因为它确实实现了System.Collections.IEnumerable接口(显然)。 但由于子弹3,它不是(!)一个collection patternMyEnumerator 不是名为Current的公共实例属性。

MSDN说:

如果集合表达式是实现集合模式的类型(如上所述),则foreach语句的扩展为:

 E enumerator = (collection).GetEnumerator(); try { while (enumerator.MoveNext()) { ElementType element = (ElementType)enumerator.Current; statement; } } finally { IDisposable disposable = enumerator as System.IDisposable; if (disposable != null) disposable.Dispose(); } 

否则 ,集合表达式是一个实现System.IEnumerable(!)的类型,而foreach语句的扩展是:

 IEnumerator enumerator = ((System.Collections.IEnumerable)(collection)).GetEnumerator(); try { while (enumerator.MoveNext()) { ElementType element = (ElementType)enumerator.Current; statement; } } finally { IDisposable disposable = enumerator as System.IDisposable; if (disposable != null) disposable.Dispose(); } 

问题#1

似乎Eric的样本既没有实现collection pattern也没有实现System.IEnumerable – 所以它不应该匹配上面指定的任何条件。 那么为什么我仍然可以通过以下方式迭代它:

  foreach (var element in (new MyIntegers() as IEnumerable )) { Console.WriteLine(element); } 

问题2

为什么我必须提到new MyIntegers() as IEnumerable ? 它已经是Ienumerable(!!),甚至在那之后,编译器是否已经通过转换自己完成了这项工作:

 ((System.Collections.IEnumerable)(collection)).GetEnumerator() ? 

就在这里:

  IEnumerator enumerator = ((System.Collections.IEnumerable)(collection)).GetEnumerator(); try { while (enumerator.MoveNext()) { ... 

那么为什么它仍然要我提到不可数?

MyEnumerator没有必需的公共方法

是的,或者更确切地说,如果Current是公开的话。 所需要的只是它有:

  • 公共的,可读的Current属性
  • 一个没有返回bool类型参数的公共MoveNext()方法

这里缺乏public只是另一个错字,基本上。 事实上,这个例子没有做到它的意思(防止拳击)。 它使用IEnumerable实现,因为你使用new MyIntegers() as IEnumerable – 所以表达式类型是IEnumerable ,它只是使用整个接口。

你声称它没有实现IEnumerable (它是System.Collections.IEnumerable ,顺便说一句)但它确实使用显式接口实现。

没有实现IEnumerable情况下测试这类事情是最容易的:

 using System; class BizarreCollection { public Enumerator GetEnumerator() { return new Enumerator(); } public class Enumerator { private int index = 0; public bool MoveNext() { if (index == 10) { return false; } index++; return true; } public int Current { get { return index; } } } } class Test { static void Main(string[] args) { foreach (var item in new BizarreCollection()) { Console.WriteLine(item); } } } 

现在如果你将Current私有,它将无法编译。

在MSDN上对System.IEnumerable的引用只不过是旧语言规范中的拼写错误,不存在这样的接口,我相信它应该引用System.Collections.IEnumerable

您应该真正阅读您正在使用的C#版本的语言规范, 此处提供了C#5.0语言规范。

有关无法转换此示例的原因的一些进一步信息会导致错误,而不是回退到使用System.Collections.IEnumerable

foreach规范(第8.8.4节)中,您将看到规则略有变化(为简洁起见,已经删除了一些步骤):

  • 使用标识符GetEnumerator对类型X执行成员查找,而不使用类型参数。 如果成员查找不产生匹配,或者产生歧义,或产生不是方法组的匹配,请检查可枚举接口,如下所述。 如果成员查找产生除方法组或不匹配之外的任何内容,建议发出警告。
  • 使用生成的方法组和空参数列表执行重载解析。 如果重载决策导致没有适用的方法,导致歧义,或导致单个最佳方法但该方法是静态的或不公开的,请检查可枚举的接口,如下所述。 如果重载决策产生除明确的公共实例方法或没有适用的方法之外的任何内容,建议发出警告。
  • 如果GetEnumerator方法的返回类型E不是类,结构或接口类型,则会产生错误,并且不会采取进一步的步骤。
  • 成员查找在E上执行,标识符为Current,没有类型参数。 如果成员查找不产生匹配,则结果是错误,或者结果是除允许读取的公共实例属性之外的任何内容,产生错误并且不执行进一步的步骤。
  • 集合类型为X,枚举器类型为E,元素类型为Current属性的类型。

因此,从第一个项目符号点我们可以找到public MyEnumerator GetEnumerator() ,第二个和第三个项目符号无错误地通过。 当我们到达第四个项目符号点时,没有名为Current公共成员可用,这会导致您在没有强制转换的情况下看到错误,这种确切的情况永远不会让编译器有机会查找可枚举的接口。

当您将实例显式转换为IEnumerable所有要求都满足IEnumerable类型,并且关联的IEnumerator满足所有要求。

还来自文档:

上述步骤,如果成功,则明确地生成集合类型C,枚举器类型E和元素类型T.表单的foreach语句

 foreach (V v in x) embedded-statement 

然后扩展到:

 { E e = ((C)(x)).GetEnumerator(); try { while (e.MoveNext()) { V v = (V)(T)e.Current; embedded-statement } } finally { … // Dispose e } } 

因此,鉴于您对IEnumerable的显式IEnumerable ,您将得到以下结果:

  • C = System.Collections.IEnumerable
  • x = new MyIntegers() as System.Collections.IEnumerable
  • E = System.Collections.IEnumerator
  • T = System.Object
  • V = System.Object
 { System.Collections.IEnumerator e = ((System.Collections.IEnumerable)(new MyIntegers() as System.Collections.IEnumerable)).GetEnumerator(); try { while (e.MoveNext()) { System.Object element = (System.Object)(System.Object)e.Current; Console.WriteLine(element); } } finally { System.IDisposable d = e as System.IDisposable; if (d != null) d.Dispose(); } } 

这解释了为什么使用演员表。