为什么 IEnumerator.Current 的错误处理与 IEnumerator<T>.Current 不同?
Why is the error handling for IEnumerator.Current different from IEnumerator<T>.Current?
我原以为对一个实现了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,表示结束
集合。
- 枚举器因集合中的更改(例如添加、修改或删除元素)而失效。
(我假设 "fails to throw an exception" 可以归类为 "undefined behaviour"...)
然而,如果你做同样的事情但使用 IEnumerable
代替,你会得到一个例外。此行为由 the documentation for IEnumerator.Current
指定,其中规定:
- 如果对 MoveNext 的最后一次调用返回 false,则 Current 应该抛出 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);
}
}
}
IEnumerable<T>
的问题是 Current
的类型是 T
。 default(T)
is returned(它是从 MoveNextRare
设置的)而不是抛出异常。
当使用 IEnumerable
时你没有类型,你不能 return 默认值。
实际问题是您没有检查 MoveNext
的 return 值。如果它 returns false
,你不应该调用 Current
。异常没关系。我认为他们发现 return default(T)
在 IEnumerable<T>
的情况下更方便。
异常处理会带来开销,returning default(T)
不会(那么多)。也许他们只是认为在 IEnumerable
的情况下 Current
属性 对 return 没有任何用处(他们不知道类型)。使用 default(T)
.
时,问题是 IEnumerable<T>
中的 'solved'
据此bug report (thanks Jesse进行评论):
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
.
的值
他们实际上只是将责任推给了 foreach
,因为那是枚举器的主要用户:
The vast majority of interactions with enumerators are in the form of foreach loops which already guard against accessing current in either of these states so it would be wasteful to burn extra CPU cycles for every iteration to check for these states that almost no one will ever encounter.
为了更好地匹配人们在实践中倾向于如何实施它。将先前版本文档中的措辞从 "Current also throws an exception …" 更改为当前版本中的 "Current should throw …"。
根据实现的工作方式,抛出异常可能需要大量工作,但由于 Current
与 MoveNext()
结合使用的方式,异常状态很难实现永远会出现。当我们考虑到绝大多数使用都是编译器生成的,并且实际上没有在 MoveNext()
之前或返回 [=] 之后调用 Current
的错误时,情况就更是如此了14=]永远不会发生。在正常使用的情况下,我们可以预期永远不会出现这种情况。
因此,如果您正在编写 IEnumerable
或 IEnumerable<T>
的实现,其中捕获错误条件很棘手,您可能会决定不这样做。如果你确实做出了那个决定,它可能不会给你带来任何问题。是的,你违反了规则,但这可能并不重要。
并且由于它不会引起任何问题,除非有人 使用 接口时出现错误,因此将其记录为未定义的行为会将负担从实现者转移到调用者不做调用者一开始就不应该做的事情。
但话虽如此,由于 IEnumerable.Current
仍被记录为 IEnumerable<T>.Current
的 "should throw InvalidOperationException
for backwards compatibility and since doing so would match the " 未定义“行为,完美实现接口记录行为的最佳方法可能是让 IEnumerable<T>.Current
在这种情况下抛出 InvalidOperationException
,让 IEnumerable.Current
调用它。
在某种程度上,这与 IEnumerable<T>
也继承自 IDisposable
的事实相反。编译器生成的 IEnumerable
的使用将检查实现是否也实现了 IDisposable
并在实现时调用 Dispose()
,但除了该测试的轻微性能开销外,这意味着实现者和手工编码的调用者有时会忘记这一点,而不是在他们应该实现或调用 Dispose()
的时候。强制所有实现至少有一个空的 Dispose()
以相反的方式让人们的生活更轻松,而不是让 Current
在无效时具有未定义的行为。
如果没有向后兼容性问题,那么在这种情况下,我们可能会将 Current
记录为未定义的两个接口,并且两个接口都继承自 IDisposable
。我们可能也不会有 Reset()
,这不过是一件麻烦事。
我原以为对一个实现了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,表示结束 集合。
- 枚举器因集合中的更改(例如添加、修改或删除元素)而失效。
(我假设 "fails to throw an exception" 可以归类为 "undefined behaviour"...)
然而,如果你做同样的事情但使用 IEnumerable
代替,你会得到一个例外。此行为由 the documentation for IEnumerator.Current
指定,其中规定:
- 如果对 MoveNext 的最后一次调用返回 false,则 Current 应该抛出 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);
}
}
}
IEnumerable<T>
的问题是 Current
的类型是 T
。 default(T)
is returned(它是从 MoveNextRare
设置的)而不是抛出异常。
当使用 IEnumerable
时你没有类型,你不能 return 默认值。
实际问题是您没有检查 MoveNext
的 return 值。如果它 returns false
,你不应该调用 Current
。异常没关系。我认为他们发现 return default(T)
在 IEnumerable<T>
的情况下更方便。
异常处理会带来开销,returning default(T)
不会(那么多)。也许他们只是认为在 IEnumerable
的情况下 Current
属性 对 return 没有任何用处(他们不知道类型)。使用 default(T)
.
IEnumerable<T>
中的 'solved'
据此bug report (thanks Jesse进行评论):
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
.
他们实际上只是将责任推给了 foreach
,因为那是枚举器的主要用户:
The vast majority of interactions with enumerators are in the form of foreach loops which already guard against accessing current in either of these states so it would be wasteful to burn extra CPU cycles for every iteration to check for these states that almost no one will ever encounter.
为了更好地匹配人们在实践中倾向于如何实施它。将先前版本文档中的措辞从 "Current also throws an exception …" 更改为当前版本中的 "Current should throw …"。
根据实现的工作方式,抛出异常可能需要大量工作,但由于 Current
与 MoveNext()
结合使用的方式,异常状态很难实现永远会出现。当我们考虑到绝大多数使用都是编译器生成的,并且实际上没有在 MoveNext()
之前或返回 [=] 之后调用 Current
的错误时,情况就更是如此了14=]永远不会发生。在正常使用的情况下,我们可以预期永远不会出现这种情况。
因此,如果您正在编写 IEnumerable
或 IEnumerable<T>
的实现,其中捕获错误条件很棘手,您可能会决定不这样做。如果你确实做出了那个决定,它可能不会给你带来任何问题。是的,你违反了规则,但这可能并不重要。
并且由于它不会引起任何问题,除非有人 使用 接口时出现错误,因此将其记录为未定义的行为会将负担从实现者转移到调用者不做调用者一开始就不应该做的事情。
但话虽如此,由于 IEnumerable.Current
仍被记录为 IEnumerable<T>.Current
的 "should throw InvalidOperationException
for backwards compatibility and since doing so would match the " 未定义“行为,完美实现接口记录行为的最佳方法可能是让 IEnumerable<T>.Current
在这种情况下抛出 InvalidOperationException
,让 IEnumerable.Current
调用它。
在某种程度上,这与 IEnumerable<T>
也继承自 IDisposable
的事实相反。编译器生成的 IEnumerable
的使用将检查实现是否也实现了 IDisposable
并在实现时调用 Dispose()
,但除了该测试的轻微性能开销外,这意味着实现者和手工编码的调用者有时会忘记这一点,而不是在他们应该实现或调用 Dispose()
的时候。强制所有实现至少有一个空的 Dispose()
以相反的方式让人们的生活更轻松,而不是让 Current
在无效时具有未定义的行为。
如果没有向后兼容性问题,那么在这种情况下,我们可能会将 Current
记录为未定义的两个接口,并且两个接口都继承自 IDisposable
。我们可能也不会有 Reset()
,这不过是一件麻烦事。