IEnumerator
包含MoveNext()
,Reset()
和Current
作为其成员。现在假设我已将这些方法和属性移至IEnumerable
接口并移除了GetEnumerator()
方法和IEnumerator
接口。
现在,实现IEnumerable
的类的对象将能够访问方法和属性,因此可以迭代。
IEnumerator
界面的存在如何解决这些问题?答案 0 :(得分:49)
迭代器包含与集合分离的状态:它包含一个光标,用于在集合中的位置。因此,必须有一个单独的对象来表示额外的状态,一种获取该对象的方式,以及 on 该对象的操作 - 因此IEnumerator
(和IEnumerator<T>
),GetEnumerator()
和迭代器成员。
想象一下,如果我们没有拥有单独的状态,那么我们写道:
var list = new List<int> { 1, 2, 3 };
foreach (var x in list)
{
foreach (var y in list)
{
Console.WriteLine("{0} {1}", x, y);
}
}
应该打印“1 1”,“1 2”,“1 3”,“2 1”等...但没有任何额外的状态,它怎么能“知道”这两个两个循环的不同位置?
答案 1 :(得分:25)
现在假设我已将这些方法和属性移动到IEnumerable接口并删除了GetEnumerator()方法和IEnumerator接口。
这样的设计会阻止集合中的并发枚举。如果集合本身跟踪当前位置,则不能有多个线程枚举相同的集合,甚至嵌套枚举,例如:
foreach (var x in collection)
{
foreach (var y in collection)
{
Console.WriteLine("{0} {1}", x, y);
}
}
通过将跟踪当前位置的责任委托给另一个对象(枚举器),它使集合的每个枚举独立于其他对象
答案 2 :(得分:8)
有点长的回答,之前的两个答案涵盖了大部分内容,但我在C#
语言规范中查找foreach时发现了一些我觉得有趣的方面。除非您对此感兴趣,否则请停止阅读。
现在转到intering部分,根据以下陈述的C#
规范扩展:
foreach (V v in x) embedded-statement
祝你:
{`
E e = ((C)(x)).GetEnumerator();
try {
while (e.MoveNext()) {
V v = (V)(T)e.Current;
embedded-statement
}
}
finally {
… // Dispose e
}
}
在x == ((C)(x)).GetEnumerator()
之后使用某种身份函数(它是它自己的枚举器)并使用@ JonSkeet的循环产生类似这样的东西(为简洁起见,删除了try / catch):
var list = new List<int> { 1, 2, 3 };
while (list.MoveNext()) {
int x = list.Current; // x is always 1
while (list.MoveNext()) {
int y = list.Current; // y becomes 2, then 3
Console.WriteLine("{0} {1}", x, y);
}
}
将打印出以下内容:
1 2
1 3
然后list.MoveNext()
将永远返回false
。如果你看一下这一点非常重要:
var list = new List<int> { 1, 2, 3 };
// loop
foreach (var x in list) Console.WriteLine(x); // Will print 1,2,3
// loop again
// Will never enter loop, since reset wasn't called and MoveNext() returns false
foreach (var y in list) Console.WriteLine(x); // Print nothing
所以考虑到上述情况,并注意它完全可行,因为foreach
语句在检查类型是否实现GetEnumerator()
之前查找IEnumerable<T>
- 方法:
为什么没有遵循上述方法以及我将面临的问题 如果我遵循它?
您不能嵌套循环,也不能使用foreach
语句多次访问同一个集合而不在它们之间手动调用Reset()
。当我们在每个foreach
之后处理枚举器时会发生什么?
IEnumerator接口的存在如何解决这些问题?
所有迭代都是相互独立的,无论我们是在讨论嵌套,多线程等,枚举都与集合本身是分开的。您可以将其视为有点像separations of concerns, or SoC,因为想法是将遍历与实际列表本身分开,并且在任何情况下遍历都不应该改变集合的状态。 IE通过您的示例调用MoveNext()
将修改集合。
答案 3 :(得分:3)
其他人已经回答了你的问题,但我想补充一点细节。
让我们反编译System.Collection.Generics.List(Of T)。它的定义如下:
public class List<T> : IList<T>, ICollection<T>, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>, IEnumerable<T>, IEnumerable
及其枚举器定义如下所示:
public struct Enumerator : IEnumerator<T>, IDisposable, IEnumerator
如您所见,列表本身是一个类,但其枚举器是一个结构,而此设计helps boost performance。
让我们假设您没有IEnumerable
和IEnumerator
之间的分隔。在这种情况下,你被迫使列表成为一个结构,但this is not a very good idea,所以你不能这样做。因此,你正在失去一个获得性能提升的好机会。
将IEnumerable
和IEnumerator
分开后,您可以根据需要实现每个界面,并使用struct
作为枚举器。
答案 4 :(得分:2)
迭代逻辑(foreach)未绑定到IEnumerables或IEnumerator。你需要foreach工作的是类中的一个名为GetEnumerator的方法,它返回一个具有MoveNext(),Reset()方法和Current属性的类对象。例如,以下代码有效,它将创建一个无限循环。
在设计透视图中,分离是为了确保容器(IEnumerable)在迭代(foreach)操作完成期间和之后不会保持任何状态。
public class Iterator
{
public bool MoveNext()
{
return true;
}
public void Reset()
{
}
public object Current { get; private set; }
}
public class Tester
{
public Iterator GetEnumerator()
{
return new Iterator();
}
public static void Loop()
{
Tester tester = new Tester();
foreach (var v in tester)
{
Console.WriteLine(v);
}
}
}