为什么IEnumerator.Current的错误处理与IEnumerator <t> .Current不同?

时间:2015-06-15 08:41:17

标签: c# ienumerable

我原本以为为实现IEnumerable<T>的空集合执行以下代码会抛出异常:

var enumerator = collection.GetEnumerator();
enumerator.MoveNext();
var type = enumerator.Current.GetType(); // Surely should throw?

因为集合是空的,所以访问IEnumerator.Current无效,我本来应该有一个例外。但是,List<T>没有例外。

the documentation for IEnumerator<T>.Current允许这样做,其中指出Current在以下任何条件下未定义:

  • 在创建枚举数之后,枚举数位于集合中的第一个元素之前。在读取Current的值之前,必须调用MoveNext将枚举数推进到集合的第一个元素。
  • 对MoveNext的最后一次调用返回false,表示结束 集合。
  • 由于集合中所做的更改(例如添加,修改或删除元素),枚举数无效。

(我假设&#34;未能抛出异常&#34;可归类为&#34;未定义的行为&#34; ...)

但是,如果您执行相同的操作但使用IEnumerable,则会出现异常。此行为由the documentation for IEnumerator.Current指定,其中指出:

  • 如果最后一次调用MoveNext返回false,则当前应抛出InvalidOperationException,这表示集合结束。

我的问题是:为什么会出现这种差异?是否有一个我不知道的好技术原因?

这意味着看似相同的代码的行为可能会有很大不同,具体取决于它是否使用IEnumerable<T>IEnumerable,如下面的程序所示(请注意showElementType1()中的代码如何和showElementType1()相同):

using System;
using System.Collections;
using System.Collections.Generic;

namespace ConsoleApplication2
{
    class Program
    {
        public static void Main()
        {
            var list = new List<int>();

            showElementType1(list); // Does not throw an exception.
            showElementType2(list); // Throws an exception.
        }

        private static void showElementType1(IEnumerable<int> collection)
        {
            var enumerator = collection.GetEnumerator();
            enumerator.MoveNext();
            var type = enumerator.Current.GetType(); // No exception thrown here.
            Console.WriteLine(type);
        }

        private static void showElementType2(IEnumerable collection)
        {
            var enumerator = collection.GetEnumerator();
            enumerator.MoveNext();
            var type = enumerator.Current.GetType(); // InvalidOperationException thrown here.
            Console.WriteLine(type);
        }
    }
}

2 个答案:

答案 0 :(得分:22)

IEnumerable<T>的问题是Current的类型为T。而不是抛出异常default(T) is returned(它是从MoveNextRare设置的。)

使用IEnumerable时,您没有该类型,并且您无法返回默认值。

实际问题是您没有检查MoveNext的返回值。如果它返回false,则不应致电Current。例外是可以的。我认为他们发现在default(T)案例中返回IEnumerable<T>会更方便。

异常处理带来了开销,返回default(T)没有(那么多)。也许他们只是认为在Current(他们不知道类型)的情况下从IEnumerable属性返回没有任何用处。使用IEnumerable<T>时,default(T)中的问题已“解决”。

根据此bug report(感谢Jesse评论):

  

出于性能原因,生成的枚举器的Current属性保持非常简单 - 它只返回生成的“当前”后备字段的值。

这可能指向异常处理的开销。或者验证current的价值所需的额外步骤。

他们实际上只是将责任推到了foreach,因为那是调查员的主要用户:

  

浩瀚大多数与枚举器的交互都采用foreach循环的形式,这些循环已经防止在这些状态中的任何一个中访问当前的流量,因此为每次迭代检查额外的CPU循环来检查是浪费的对于这些几乎没有人会遇到的状态。

答案 1 :(得分:3)

更好地匹配人们在实践中如何实施它。正如#34;措辞的变化一样,也会引发例外......&#34;在以前版本的文档中,&#34; Current应该抛出......&#34;在当前版本中。

根据实现的工作方式,抛出异常可能会有相当多的工作,但由于CurrentMoveNext()一起使用的方式,那么异常状态几乎不会发生来吧当我们认为绝大多数用法是编译器生成的并且实际上没有在Current之前或之后调用MoveNext()的错误的范围时,情况更是如此。 false永远发生。在正常使用情况下,我们可以预期案件永远不会出现。

因此,如果您正在编写IEnumerableIEnumerable<T>的实现,其中捕获错误条件很棘手,您可能决定不这样做。如果你做出这个决定,它可能不会给你带来任何问题。是的,你违反了规则,但这可能并不重要。

并且由于除了以错误的方式使用接口的人之外不会导致任何问题,将其记录为未定义的行为会将负担从执行者转移到调用者而不会执行呼叫者不应该首先做的事情。

但是所有这一切都说明了,因为IEnumerable.Current仍然被记录为&#34;应该抛出InvalidOperationException以便向后兼容,因为这样做会匹配&#34; undefined&#34; IEnumerable<T>.Current的行为,可能是完美地完成界面记录行为的最好方法是让IEnumerable<T>.Current在这种情况下抛出InvalidOperationException,并IEnumerable.Current调用IEnumerable<T>进入那个。

在某种程度上,这与IDisposable也从IEnumerable继承的事实相反。编译器生成的IDisposable用法将检查实现是否也实现Dispose()并调用Dispose(),但是除了该测试的轻微性能开销之外,它意味着实现者和手动编码的呼叫者有时会忘记这一点,并且在他们应该的时候不实现或呼叫Dispose()。强制所有实现至少使Current空的生活更容易让人们以相反的方式让Current在其无效时具有未定义的行为。

如果没有向后兼容性问题,那么在这种情况下,对于两个接口,我们可能会将IDisposable记录为未定义,并且两个接口都继承自Reset()。我们可能也不会post.py这只是一种麻烦。