【问题标题】:How is foreach implemented in C#? [duplicate]C#中的foreach是如何实现的? [复制]
【发布时间】:2012-06-26 02:35:03
【问题描述】:

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

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

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

但是我不确定到底发生了什么。每个周期使用什么方法返回enumerator.Current?它是返回 [for each cycle] 还是需要匿名函数或其他东西来执行 foreach 的主体?

【问题讨论】:

  • 基本上,// do some stuff here 被替换为 foreach 循环“之前”编译的内部。 (或者更确切地说,编译器生成等效的字节码。)
  • 不是真正的链接 question 的副本。只有标题远程匹配,但正文要求完全不同。

标签: c# foreach language-implementation language-specifications


【解决方案1】:

惊讶于没有触及确切的实现。虽然您在问题中发布的是最简单的形式,但完整的实现(包括枚举器处理、强制转换等)在 8.8.4 section of the spec.

现在有两种情况可以在一个类型上运行 foreach 循环:

  1. 如果该类型有一个名为 GetEnumerator 的公共/非静态/非泛型/无参数方法,则该方法返回具有公共 MoveNext 方法和公共 Current 属性的内容。 As noted by Mr Eric Lippert in this blog article,这是为了适应前通用时代的类型安全和在值类型的情况下与装箱相关的性能问题。请注意,这是 duck 打字 的情况。例如这有效:

    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
    {
    
    }
    

    然后编译器将其翻译为:

    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. 如果类型实现了IEnumerable,其中GetEnumerator 返回IEnumerator,它具有公共MoveNext 方法和公共Current 属性。 但一个有趣的子情况是,即使如果你明确实现IEnumerable(即Test 类上没有公共GetEnumerator 方法),你可以有一个foreach

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

    这是因为在这种情况下foreach 被实现为(假设类中没有其他公共GetEnumerator 方法):

    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();
    }
    

    如果该类型显式实现了IEnumerable<T>,则foreach 被转换为(假设类中没有其他公共GetEnumerator 方法):

    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. 在上述两种情况下,Enumerator 类都应该有一个公共的MoveNext 方法和一个公共的Current 属性。换句话说,如果你正在实现IEnumerator 接口,它必须被隐式实现。例如,foreach 不适用于这个枚举器:

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

    (感谢 Roy Namir 指出这一点。foreach 实现并不像表面上看起来那么容易)

  2. 枚举器优先级 - 如果你有一个public GetEnumerator 方法,那么这是foreach 的默认选择,无论谁在实现它。例如:

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

    如果您没有公共实现(即只有显式实现),则优先级类似于 IEnumerator&lt;T&gt; > IEnumerator

  3. foreach 的实现中涉及一个转换运算符,其中集合元素被转换回类型(在foreach 循环本身中指定)。这意味着即使您像这样写了SomethingEnumerator

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

    你可以写:

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

    因为Somethingobject 类型兼容,遵循C# 规则,或者换句话说,如果两种类型之间可能存在显式转换,编译器就会允许它。否则编译器会阻止它。实际的转换是在运行时执行的,可能会失败也可能不会失败。

【讨论】:

  • @EricLippert 谢谢,我会更新它..
  • 当然,如果 either IEnumerable IEnumerable&lt;X&gt; 为一种类型 X (IEnumerable&lt;Y&gt; 可以在病理中以不同的方式实现case) 是隐式实现的,那么肯定有一个“有效的”publicGetEnumerator,因此属于上述情况 1。 (我们忽略了基类中的GetEnumerator 被相关类型中的相同重载隐藏 的情况。)但是如果你显式地实现接口,你仍然可以有一个“坏”的公共@ 987654373@。例如,如果您有public void/* bad */ GetEnumerator() { },则不会考虑IEnumerable
  • 如果我没记错的话,如果 GetEnumerator 是公共的、非静态的、非泛型的并且采用零参数,则尝试案例 1。如果GetEnumerator 不是这样,或者不存在,则考虑IEnumerable&lt;&gt;IEnumerable
  • @JeppeStigNielsen 当然,这属于第一种情况。如果有一个公共GetEnumerator,那就很重要了。我确实在答案中提到了它。作为一个例子,我提出了一个更令人惊讶的案例,即一个简单的独立公共GetEnumerator,即使它们来自IEnumerable 接口,它也可以覆盖显式的公共GetEnumerator。当然,它也应该是非通用和非静态的,我将对其进行更新。谢谢!
【解决方案2】:

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

foreach 不是函数调用——它是语言本身的内置函数,就像for 循环和while 循环一样。它不需要返回任何东西或“获取”任何类型的功能。

请注意,foreach 有一些有趣的皱纹:

  • 在迭代数组时(在编译时已知),编译器可以使用循环计数器并与数组的长度进行比较,而不是使用IEnumerator
  • foreach 将在最后处理迭代器;这对于扩展IDisposableIEnumerator&lt;T&gt; 来说很简单,但是由于IEnumerator ,编译器会插入一个检查以在执行时测试迭代器是否实现IDisposable
  • 您可以迭代未实现IEnumerableIEnumerable&lt;T&gt; 的类型,只要您有一个适用的GetEnumerator() 方法,该方法返回一个具有合适CurrentMoveNext() 成员的类型。正如在 cmets 中所指出的,一个类型可以显式地实现 IEnumerableIEnumerable&lt;T&gt;,但有一个公共的 GetEnumerator() 方法,它返回的类型不是 IEnumerator/IEnumerator&lt;T&gt;。有关示例,请参见 List&lt;T&gt;.GetEnumerator() - 这避免了在许多情况下不必要地创建引用类型对象。

有关详细信息,请参阅 C# 4 规范的第 8.8.4 节。

【讨论】:

  • 您的第三个项目符号不仅适用于未实现 IEnumerable 的类型:当一个类型确实实现了它并且还提供了一个不同于 IEnumerable&lt;T&gt;.GetEnumeratorGetEnumerator 方法,该类型是自己的GetEnumerator 将被使用。标准类List&lt;T&gt; 就是一个很好的例子。
  • 谢谢@JonSkeet!出于兴趣,我们是否可以在 C# 中使用类似于 Type x in collection 的参数类型。我们可以自己使用这种方言吗?
  • @JamieDixon:恐怕我不明白你在问什么。
  • 当我们使用foreach 时,我们使用int x in collection 语法,如foreach(int x in collection)。是否可以将此语法用作标准方法的属性?
  • @JonSkeet 我认为有关演员阵容的一句话将使这个答案完整..
猜你喜欢
  • 2011-12-04
  • 1970-01-01
  • 1970-01-01
  • 2020-09-30
  • 2012-12-15
  • 2018-02-06
  • 2023-03-13
  • 2021-04-02
  • 2012-04-03
相关资源
最近更新 更多