C#中的foreach如何实现?

23

foreach 在 C# 中是如何实现的?

我想它的一部分看起来像这样:

var enumerator = TInput.GetEnumerator();
while(enumerator.MoveNext())
{
  // do some stuff here
}

不过我不确定到底发生了什么。用于返回enumerator.Current每个循环的方法是什么?它是返回[每个循环],还是需要一个匿名函数或其他东西来执行foreach的主体?


基本上,“// do some stuff here”在编译之前会被foreach循环内部的内容所替换。(或者说,编译器会生成等效的字节码。) - millimoose
这是它的实现方式 https://referencesource.microsoft.com/#System.Activities/System/Activities/Statements/ForEach.cs,fd6f1b9d9e676ae4 - Alexander Ryan Baggett
并不是链接问题的重复,问题只有标题略微相似,但正文提出了完全不同的问题。 - Frédéric
2个回答

31

它不使用匿名函数。基本上,编译器将代码转换成与您在此处展示的 while 循环大致等效的内容。

foreach并不是函数调用 - 它内置于语言中,就像for循环和while循环一样。没有必要返回任何内容或“接受”任何类型的函数。

请注意,foreach有一些有趣的细节:

  • 当遍历一个在编译时已知的数组时,编译器可以使用循环计数器并与数组长度进行比较,而无需使用 IEnumerator
  • foreach将在结束时处理迭代器;对于扩展了 IDisposableIEnumerator<T>,这很简单,但是对于不扩展 IDisposableIEnumerator,编译器会插入一个检查以在执行时测试迭代器是否实现了 IDisposable
  • 您可以遍历不实现 IEnumerableIEnumerable<T> 的类型,只要您有一个适用的 GetEnumerator() 方法,该方法返回具有合适的 CurrentMoveNext() 成员的类型。正如注释中所述,一种类型还可以显式地实现 IEnumerableIEnumerable<T>,但是具有公共GetEnumerator()方法,该方法返回一个类型,而非 IEnumerator / IEnumerator<T>。请参见List<T>.GetEnumerator() 以获取示例 - 这避免了在许多情况下不必要地创建引用类型对象。

有关更多信息,请参见 C# 4 规范的第8.8.4节。


4
第三个要点不仅适用于未实现“IEnumerable”接口的类型:当某个类型实现了该接口并且提供了一个与“IEnumerable<T>.GetEnumerator”不同的“GetEnumerator”方法时,该类型自己的“GetEnumerator”方法将被使用。标准类“List<T>”是一个很好的例子。 - user743382
@JamieDixon:恐怕我不明白你在问什么。 - Jon Skeet
当我们使用 foreach 时,我们使用语法 int x in collection,例如 foreach(int x in collection)。是否可能将此语法用作标准方法的属性? - Jamie Dixon
为了好玩,我尝试着实现了自己的 foreach。我在这里写了一些关于它的内容:http://www.jamie-dixon.co.uk/csharp/implementing-a-custom-foreach-iterator/ - Jamie Dixon
1
@JonSkeet 我认为对于所涉及的类型转换进行一些说明会使这个答案更完整。 - nawfal
显示剩余3条评论

11
意外的是,确切的实现并没有被触及。虽然你在问题中发布的是最简单的形式,但完整的实现(包括枚举器处理、类型转换等)在规范的8.8.4章节中
现在有两种情况可以对类型运行foreach循环:
  1. If the type has a public/non-static/non-generic/parameterless method named GetEnumerator which returns something that has a public MoveNext method and a public Current property. As noted by Mr Eric Lippert in this blog article, this was designed so as to accommodate pre generic era for both type safety and boxing related performance issues in case of value types. Note that this a case of duck typing. For instance this works:

    class Test
    {
        public SomethingEnumerator GetEnumerator()
        {
    
        }
    }
    
    class SomethingEnumerator
    {
        public Something Current //could return anything
        {
            get { return ... }
        }
    
        public bool MoveNext()
        {
    
        }
    }
    
    //now you can call
    foreach (Something thing in new Test()) //type safe
    {
    
    }
    

    This is then translated by the compiler to:

    E enumerator = (collection).GetEnumerator();
    try {
       ElementType element; //pre C# 5
       while (enumerator.MoveNext()) {
          ElementType element; //post C# 5
          element = (ElementType)enumerator.Current;
          statement;
       }
    }
    finally {
       IDisposable disposable = enumerator as System.IDisposable;
       if (disposable != null) disposable.Dispose();
    }
    
  2. If the type implements IEnumerable where theGetEnumerator returns IEnumerator that has a public MoveNext method and a public Current property. But an interesting sub case is that even if you implement IEnumerable explicitly (ie no public GetEnumerator method on Test class), you can have a foreach.

    class Test : IEnumerable
    {
        IEnumerator IEnumerable.GetEnumerator()
        {
    
        }
    }
    

    This is because in this case foreach is implemented as (provided there is no other public GetEnumerator method in the class):

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

    If the type implements IEnumerable<T> explicitly then the foreach is converted to (provided there is no other public GetEnumerator method in the class):

    IEnumerator<T> enumerator = ((IEnumerable<T>)(collection)).GetEnumerator();
    try {
        ElementType element; //pre C# 5
        while (enumerator.MoveNext()) {
            ElementType element; //post C# 5
            element = (ElementType)enumerator.Current; //Current is `T` which is cast
            statement;
       }
    }
    finally {
        enumerator.Dispose(); //Enumerator<T> implements IDisposable
    }
    

值得注意的几件有趣的事情是:

  1. In both the above cases the Enumerator class should have a public MoveNext method and a public Current property. In other words, if you're implementing IEnumerator interface it has to be implemented implicitly. For eg, foreach wont work for this enumerator:

    public class MyEnumerator : IEnumerator
    {
        void IEnumerator.Reset()
        {
            throw new NotImplementedException();
        }
    
        object IEnumerator.Current
        {
            get { throw new NotImplementedException(); }
        }
    
        bool IEnumerator.MoveNext()
        {
            throw new NotImplementedException();
        }
    }
    

    (Thanks Roy Namir for pointing this out. foreach implementation isnt as easy it seems on the surface)

  2. Enumerator precedence - It goes like if you have a public GetEnumerator method, then that is the default choice of foreach irrespective of who is implementing it. For example:

    class Test : IEnumerable<int>
    {
        public SomethingEnumerator GetEnumerator()
        {
            //this one is called
        }
    
        IEnumerator<int> IEnumerable<int>.GetEnumerator()
        {
    
        }
    }
    

    If you don't have a public implementation (ie only explicit implementation), then precedence goes like IEnumerator<T> > IEnumerator.

  3. There is a cast operator involved in the implementation of foreach where the collection element is cast back to the type (specified in the foreach loop itself). Which means even if you had written the SomethingEnumerator like this:

    class SomethingEnumerator
    {
        public object Current //returns object this time
        {
            get { return ... }
        }
    
        public bool MoveNext()
        {
    
        }
    }
    

    You could write:

    foreach (Something thing in new Test())
    {
    
    }
    

    Because Something is type compatible with object, going by C# rules ,or in other words, the compiler lets it if there is an explicit cast possible between the two types. Otherwise the compiler prevents it. The actual cast is performed at run time which may or may not fail.


1
你所思考的文章在这里:http://blogs.msdn.com/b/ericlippert/archive/2011/06/30/following-the-pattern.aspx - Eric Lippert
@EricLippert 谢谢,我会更新它的。 - nawfal
如果我没记错的话,Case 1 将尝试调用 GetEnumerator 方法,该方法必须是公共的、非静态的、非泛型的且不带参数。如果 GetEnumerator 不符合这些条件或不存在,则会考虑 IEnumerable<>IEnumerable - Jeppe Stig Nielsen
@JeppeStigNielsen 当然,这属于第一种情况。如果有一个公共的 GetEnumerator,那就是最重要的。我在答案中提到了它。例如,我举了一个更令人惊讶的例子,即一个简单的独立的公共 GetEnumerator,即使它们来自IEnumerable接口,也可以覆盖显式的GetEnumerator。当然,它也应该是非泛型和非静态的,我会更新的。谢谢! - nawfal
https://dev59.com/VV0Z5IYBdhLWcg3whQwj - Royi Namir
显示剩余2条评论

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