Nat*_*han 3 .net c# ienumerable multithreading iterator
请考虑以下代码:
List<int> list = new List<int>();
IEnumerable<int> enumerable = list;
IEnumerator<int> enumerator = enumerable.GetEnumerator();
list.Add(1);
bool any = enumerator.MoveNext();
Run Code Online (Sandbox Code Playgroud)
在运行时,最后一行抛出:
InvalidOperationException:Collection已被修改; 枚举操作可能无法执行.
我理解在更改IEnumerators
时需要抛出'Collection was modified'异常IEnumerable
,但我不明白这一点:
为什么IEnumerator
在第一次调用时抛出此异常MoveNext()
?由于IEnumerator
不表示IEnumerable
直到MoveNext()
第一次调用的状态,为什么它不能从第一次开始跟踪变化MoveNext()
而不是从GetEnumerator()
?
可能是因为规则"如果底层集合被修改,则枚举器无效"比规则"如果在第一次调用MoveNext之后修改底层集合时枚举器无效"则更简单.或者只是它的实施方式.另外,假设Enumerator在创建Enumerator时表示底层集合的状态是合理的,并且依赖于不同的行为可能是bug的来源.
我觉得需要快速回顾一下迭代器.
迭代器(IEnumerator和IE#的IEnumerable)用于以有序的方式访问结构的元素,而不暴露底层表示.结果是它允许您具有可扩展的通用功能,如下所示.
void Iterator<T, V>(T collection, Action<V> actor) where T : IEnumerable<V>
{
foreach (V value in collection)
actor(value);
}
//Or the more verbose way
void Iterator<T, V>(T collection, Action<V> actor) where T : IEnumerable<V>
{
using (var iterator = collection.GetEnumerator())
{
while (iterator.MoveNext())
actor(iterator.Current);
}
}
//Or if you need to support non-generic collections (ArrayList, Queue, BitArray, etc)
void Iterator<T, V> (T collection, Action<V> actor) where T : IEnumerable
{
foreach (object value in collection)
actor((V)value);
}
Run Code Online (Sandbox Code Playgroud)
如C#规范中所示,存在权衡取舍.
5.3.3.16 Foreach陈述
foreach(expr中的类型标识符)embedded-statement
expr开头的v的明确赋值状态与stmt开头的v的状态相同.
控制流转移到嵌入语句或stmt结束点的v的明确赋值状态与expr末尾的v的状态相同.
这只是意味着值是只读的.他们为什么只读?这很简单.由于foreach
是如此高级别的声明,它不能也不会假设您正在迭代的容器.如果您在迭代二叉树并决定在foreach语句中随机分配值,该怎么办?如果foreach
没有强制只读访问,那么您的二叉树将退化为树.整个数据结构将处于混乱状态.
但这不是你原来的问题.您甚至在访问第一个元素之前修改了集合,并且抛出了错误.为什么?为此,我使用ILSpy挖掘了List类.这是List类的片段
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable
{
private int _version;
public struct Enumerator : IEnumerator<T>, IDisposable, IEnumerator
{
private List<T> list;
private int version;
private int index;
internal Enumerator(List<T> list)
{
this.list = list;
this.version = list._version;
this.index = 0;
}
/* All the implemented functions of IEnumerator<T> and IEnumerator will throw
a ThrowInvalidOperationException if (this.version != this.list._version) */
}
}
Run Code Online (Sandbox Code Playgroud)
枚举器使用父列表的"版本"和对父列表的引用进行初始化. 所有迭代操作都会检查以确保初始版本等同于引用列表的当前版本.如果它们不同步,则迭代器不再有效.为什么BCL这样做?为什么实施者不检查枚举数的索引是否为0(表示新的枚举数),如果是,只是重新同步版本?我不确定.我只能假设团队希望在所有实现IEnumerable的类中保持一致,他们也希望保持简单.因此,List的枚举器(我相信大多数其他人)只要它们在范围内就不会区分元素.
这是您的问题的根本原因.如果您绝对必须具有此功能,那么您将必须实现自己的迭代器,并且最终可能必须实现自己的List.在我看来,太多的工作来对抗BCL的流量.
在设计BCL团队可能遵循的迭代器时,这是GoF的引用:
在遍历聚合时修改聚合可能很危险.如果从聚合中添加或删除元素,则最终可能会访问元素两次或完全丢失它.一个简单的解决方案是复制聚合并遍历副本,但这一般来说太昂贵了
BCL团队很可能认为它在时空复杂性和人力方面太昂贵了.整个C#都可以看到这种理念.允许修改foreach中的变量可能太昂贵了,让List的Enumerator区分列表中的位置太昂贵,而且太昂贵了.希望我已经很好地解释了它可以看到迭代器的力量和约束.
参考:
是什么改变了列表的"版本",从而使所有当前的枚举者无效?
Add
AddRange
Clear
Insert
InsertRange
RemoveAll
RemoveAt
RemoveRange
Reverse
Sort
归档时间: |
|
查看次数: |
4533 次 |
最近记录: |