【问题标题】:Why is the error handling for IEnumerator.Current different from IEnumerator<T>.Current?为什么 IEnumerator.Current 的错误处理与 IEnumerator<T>.Current 不同?
【发布时间】:2015-08-30 16:33:16
【问题描述】:

我原以为为实现IEnumerable&lt;T&gt; 的空集合执行以下代码会引发异常:

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

因为集合是空的,所以访问IEnumerator.Current 是无效的,我本来预计会出现异常。但是,List&lt;T&gt; 不会抛出异常。

the documentation for IEnumerator&lt;T&gt;.Current 允许这样做,它指出 Current 在以下任何条件下都是未定义的:

  • 枚举器位于集合中第一个元素之前,紧接在创建枚举器之后。在读取 Current 的值之前,必须调用 MoveNext 以将枚举数前进到集合的第一个元素。
  • 最后一次调用 MoveNext 返回 false,表示结束 集合。
  • 枚举器因集合中的更改而失效,例如添加、修改或删除元素。

(我假设“未能抛出异常”可以归类为“未定义的行为”......)

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

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

我的问题是:为什么会有这种差异?有什么我不知道的技术原因吗?

这意味着看起来相同的代码的行为会因使用IEnumerable&lt;T&gt;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);
        }
    }
}

【问题讨论】:

  • 但在第一种情况下 - T - int 不能为 null,所以也不例外,但在第二种情况下 - Curretn 是对象,所以默认为 null
  • 我一直在写这个作为答案,但感觉还不够好,所以... 性能 - 通用集合的设计旨在解决装箱值类型的巨大性能问题一份清单(当然还有其他原因)。在每个 Current 访问上检查 hasValue 听起来并不多,但考虑到 LINQ 也在那个时候出现 - 使用枚举器大量,即使对于曾经是简单数组迭代的事情也是如此.无论如何,您都应该检查MoveNext,因此当MoveNext 返回false 时,允许Current 任意设置并没有太大伤害。
  • List&lt;T&gt; 的源代码:current = default(T);
  • @Grundy 不管它是否是引用类型都会发生,所以这与它无关。您可以将我的示例代码更改为List&lt;string&gt;,同样的事情也会发生。
  • 值类型的长期泄漏抽象的另一个副作用。并不是说框架无法检查这一点,这样做的代码太昂贵了,无法添加到 Current 的每次使用中。

标签: c# ienumerable


【解决方案1】:

IEnumerable&lt;T&gt; 的问题在于Current 的类型为T。而不是抛出异常,default(T) is returned(它是从MoveNextRare 设置的)。

当使用IEnumerable时,你没有类型,你不能返回一个默认值。

实际的问题是你没有检查MoveNext的返回值。如果它返回false,则不应调用Current。例外没关系。我认为他们发现在IEnumerable&lt;T&gt; 的情况下返回default(T) 更方便。

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

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

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

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

他们实际上只是将责任推给foreach,因为那是枚举器的主要用户:

与枚举器的大量大多数交互都采用 foreach 循环的形式,它已经防止在这两种状态下访问当前,因此每次迭代都消耗额外的 CPU 周期来检查是浪费的对于这些几乎没有人会遇到的状态。

【讨论】:

  • 我不认为返回default(T) 更方便是答案。当然,抛出异常同样容易(并且对调用者来说更多信息)?我的问题是,他们为什么要选择掩盖错误?
  • this bug report:For performance reasons the Current property of generated Enumerators is kept extremely simple - it simply returns the value of the generated 'current' backing field.
  • 但为什么泛型Current 不抛出异常?
  • 但不抛出异常意味着无效代码会错误执行!
  • @MatthewWatson:但是如果记录在案你应该检查返回值,谁错了?
【解决方案2】:

为了更好地匹配人们在实践中倾向于实施它的方式。与以前版本的文档中的“Current也抛出异常......”的措辞变化一样,当前版本的“Current应该抛出......”。

根据实现的工作方式,抛出异常可能需要相当多的工作,但是由于CurrentMoveNext() 结合使用的方式,因此几乎不会出现异常状态。当我们考虑到绝大多数使用是编译器生成的并且实际上并没有在Current 之前调用MoveNext() 或在它返回false 之后调用的错误时更是如此.在正常使用的情况下,我们可以预期这种情况永远不会出现。

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

而且由于除了以错误的方式使用接口的人之外不会引起任何问题,将其记录为未定义的行为会将负担从实现者转移到调用者而不做某事调用者一开始就不应该这样做。

但话虽如此,因为IEnumerable.Current 仍然记录为“应该抛出InvalidOperationException 以实现向后兼容性,并且因为这样做会匹配IEnumerable&lt;T&gt;.Current 的“未定义”行为,这可能是完美实现接口的记录行为是让IEnumerable&lt;T&gt;.Current 在这种情况下抛出一个InvalidOperationException,并让IEnumerable.Current 调用它。

在某种程度上,这与IEnumerable&lt;T&gt; 也继承自IDisposable 的事实相反。 IEnumerable 的编译器生成的使用将检查实现是否也实现了 IDisposable 并调用 Dispose(),但除了该测试的轻微性能开销之外,这意味着实现者和手动编码的调用者有时会忘记这一点,不要在应该的时候实施或调用Dispose()。强制所有实现至少有一个空的 Dispose() 使人们的生活更轻松,这与让 Current 在无效时具有未定义的行为相反。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-11-17
    • 1970-01-01
    • 2010-09-18
    • 1970-01-01
    相关资源
    最近更新 更多