“foreach”循环背后发生了什么?

13

可能是重复问题:
如何在C#中使用foreach循环?

我在网上搜索了一下,但仍然无法找到关于C#中foreach循环的背后发生了什么的答案。

我知道这个问题实际上与编程无关,但这让我困扰。我对面向对象编程尤其是接口比较新。我明白它们是合同,也明白IEnumerableIEnumerator的工作原理 - 或者我认为是这样。

我一直在阅读MSDN上的这篇文章: IEnumerable Interface

我明白所有东西是如何设置的。但在Main循环中,我有点不清楚foreach如何知道遍历_people中的所有值。它如何知道这一点?通过调用return new PeopleEnum(_people);它如何跟踪Current

编辑:我不明白这怎么是严格的重复问题。是的,它们有相似的背景,并且同样的问题正在被问,但我们正在寻找不同的答案我想要的答案没有在以前的问题中讨论。

像foreach(int i in obj){...}这样的foreach循环实际上等同于

......“kinda”不是我要找的答案。


使用 Ildasm.exe 查看 dll 代码,可以帮助简单的 Foreach。 - Nikshep
我一遍又一遍地阅读了那个。我知道它告诉你如何使用foreach循环,但它并没有真正解释编译器如何使用它。我更感兴趣的是背后的实现。 - Adam Beck
@Adam:在可能的重复问题的被接受答案中,它大致展示了代码的精髓。当然,下面Eric的回答更加详细。 - James Michael Hare
现在,当像我这样的人决定谷歌“foreach循环的幕后”,它将成为顶部链接,并且他们将得到非常具体、非常详细的答案。 - Adam Beck
2
我不认为这是一个完全重复的问题。另一个问题问的是“哪些类型的类可以使用foreach循环”,而不是对其工作原理的详细解释。 - Meta-Knight
3个回答

24

我建议您阅读C#规范的8.8.4章节,其中详细回答了您的问题。以下是方便起见从中引用的内容:


一个形如foreach语句的结构:

foreach (V v in x) embedded-statement

然后扩展为:

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

类型 E、C、V 和 T 是语义分析器推导出的枚举器、集合、循环变量和集合元素类型;有关详细信息,请参见规范。
所以,这就是 "foreach" 的含义,它只是一种更方便的编写 "while" 循环的方式,该循环调用 MoveNext 直到 MoveNext 返回 false。
还有一些微妙的问题:
  • 不需要生成相同的代码,只需生成能够产生相同结果的代码即可。例如,如果你"foreach"遍历一个数组或字符串,我们只需生成一个"for"循环(或多重循环,在多维数组的情况下)来索引数组或字符串的字符,而不必花费时间分配枚举器。

  • 如果枚举器是值类型,则处理代码可能会选择在处理之前将枚举器装箱或不装箱。不要依赖于其中的任何一种方式。(有关相关问题,请参见http://blogs.msdn.com/b/ericlippert/archive/2011/03/14/to-box-or-not-to-box-that-is-the-question.aspx。)

  • 同样,如果上面插入的强制转换被确定为身份转换,则即使这样做会导致值类型被复制,强制转换也可能会被省略。

  • 未来版本的C#很可能会将循环变量v的声明放在while循环体内;这将防止常见的“修改闭包”错误,该错误每天都会在Stack Overflow上报告一次。[更新:This change has indeed been implemented in C# 5.]


@reggie:请理解我不想知道如何实现“foreach”循环或我需要什么才能使用它。我想知道编译器是如何思考的,我想知道它看到了什么。我甚至不知道这些规范文档存在。我一定会用它们来回答几个问题。谢谢。 - Adam Beck
1
@Adam:我建议你先下载C# 4规范,然后考虑购买Addison-Wesley出版的印刷版本。我和我的同事们已经对它进行了广泛的注释,包括你感兴趣的那些详细解释。请参见http://blogs.msdn.com/b/ericlippert/archive/2010/11/15/the-annotated-fourth-edition-is-available.aspx。 - Eric Lippert
1
@Tigran:当然,如果我们确定不需要处理,则整个try-finally将被删除;我们不会生成一个带有空finally的try-protected区域! - Eric Lippert
1
@Gabe:通常情况下并没有什么不同。这主要是出于历史原因。考虑一下:如果集合是T类型的数组,C# 1.0中的数组实现了IEnumerable而不是IEnumerable<T>,所以“Current”返回的是对象而不是T。 - Eric Lippert
@EricLippert,迭代变量v是如何只读的?我已经在这里发布了一个问题:https://dev59.com/2Lbna4cB1Zd3GeqPZFI1 - David Klempfner
显示剩余5条评论

3
考虑一下这个问题:
 var enumerator = <YourEnumerableHere>.GetEnumerator();

 while(enumerator.MoveNext())
    <YourForEachMethodBodyHere>

因此,当它调用GetEnumerator时,您将结果返回给PeopleEnum。每次调用MoveNext方法时,PeopleEnum的位置增加一,遍历列表。
当位置到达末尾时,MoveNext调用返回false,并且循环结束。

0

它知道如何迭代遍历 IEnumerable,因为这是语言设计者决定 foreach 应该做的事情。 foreach 为您提供了一种快捷的方式来循环遍历 IEnumerable

IEnumerator 类定义了“获取下一个元素”的含义。如果您愿意,可以使 IEnumerator 返回每个其他元素,但大多数人只想按顺序遍历每个元素。

我不确定是否已经回答了您的问题。如果没有,请告诉我,我会回复您。


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