C# 为什么在迭代集合时不应修改它
我知道,在.net集合类型(或至少某些集合类型)中,当您对集合进行迭代时,不允许修改该集合 例如,在List类中存在如下代码:C# 为什么在迭代集合时不应修改它,c#,.net,collections,ienumerable,C#,.net,Collections,Ienumerable,我知道,在.net集合类型(或至少某些集合类型)中,当您对集合进行迭代时,不允许修改该集合 例如,在List类中存在如下代码: if (this.version != this.list._version) ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion); 但很明显,这是设计迭代器类的开发人员的决定,因为我可以提供IEnumerable的一些实现,
if (this.version != this.list._version)
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
但很明显,这是设计迭代器类的开发人员的决定,因为我可以提供IEnumerable
的一些实现,这些实现在修改底层集合时至少不会引发任何异常
然后我有一些问题:
- 为什么在迭代集合时不应该修改它
- 可以创建一个支持在迭代时修改的集合,而不存在任何其他问题吗?(注意:第一个答案也可以回答这个问题)
- 当C#编译器生成
接口实现会考虑这类事情吗枚举器时
- 一个原因是线程安全。如果另一个线程添加到列表中,则无法保证迭代器以正确的方式从
列表的备份数组中读取,这可能会导致重新分配到新数组
值得注意的是,即使使用for
循环枚举列表
,也会显示出线程安全性的不足
在此基础上,他创建了一个ThreadSafeList
类:
集合不再实现IEnumerable。IEnumerable仅在集合未在引擎盖下更改时有效。对于以这种方式构建的集合,无法轻松地做出此保证,因此它已被删除
值得一提的是,IEnumerable
的所有实现都不允许在枚举期间进行修改。确实如此,因为它们保证了线程安全。不允许在对集合进行迭代时修改集合的一个重要原因是,如果删除集合中的元素或插入新元素,它将停止迭代。(在集合中迭代工作的地方插入或删除了一个元素;现在下一个元素是什么?新的停止条件是什么?使用yield语句加载要修改的元素,并在事实发生后执行此操作
如果必须在迭代时修改集合(如果可以对其编制索引),使用for循环并解除对象与循环声明的关联…但是您要确保在循环周围使用lock语句以确保您是唯一操纵对象的人…并且您要记住在循环的下一个过程中您自己的操作…也许您可以这样做,但这将是意外的行为,在IEnumerable和IEnumerator接口的意图之外
只要集合保持不变,枚举数就保持有效
不变。如果对集合进行了更改,例如添加,
修改或删除元素时,枚举数无法恢复
无效,其行为未定义
这可以避免像LinkedList这样的集合出现问题。假设您有一个包含4个节点的链表,然后迭代到第二个节点。然后更改链表,其中第二个节点移动到链表的头部,第三个节点移动到尾部。在那一点上,下一步使用您的枚举器意味着什么?可能的行为是模棱两可的,不容易猜测。当您通过它的接口处理对象时,您不必考虑底层类是什么,以及该类及其枚举数是否对修改容忍。接口说修改会使枚举数无效,所以事情应该是这样的
为什么在迭代集合时不应该修改它
有些集合可以在迭代时修改,因此它在全局上并不坏。在大多数情况下,编写一个有效的迭代器是非常困难的,即使在底层集合被修改时也能正常工作。在许多情况下,迭代器编写者会说他们只是不想处理它,这是个例外
在某些情况下,当底层集合发生更改时,迭代器应该做什么并不清楚。有些情况是明确的,但对于其他情况,不同的人会期望不同的行为。无论何时处于这种情况,都表明存在更深层次的问题(不应该对正在迭代的序列进行变异)
可以创建一个支持在迭代时修改的集合,而不存在任何其他问题吗?(注意:第一个答案也可以回答这个问题)
当然
将此迭代器视为列表:
public static IEnumerable<T> IterateWhileMutating<T>(this IList<T> list)
{
for (int i = 0; i < list.Count; i++)
{
yield return list[i];
}
}
- 如果从序列中的“早期”中删除某个项目,我们将正常继续,而不会跳过某个项目
- 如果在序列中添加了一个项目“更早”,我们将不会显示它,但也不会显示其他项目两次
- 如果项目从当前位置之前移动到之后,则将显示两次,但不会跳过或重复其他项目。如果项目从当前位置之后移动到当前位置之前,则不会显示该项目,仅此而已。如果一个项目稍后从集合中的一个较后位置移动到另一个位置,则没有问题,并且将在结果中看到移动;如果它从一个较前位置移动到另一个较前位置,则一切正常,迭代器不会“看到”移动
- 更换物品不是问题;但只有在当前位置之后才能看到它
- 重置集合将导致序列在当前位置优雅结束
请注意,此迭代器不会处理具有多个线程的情况。如果另一个线程在另一个线程进行迭代时对集合进行变异,则可能会发生不好的事情(ite)
public static IEnumerable<T> IterateWhileMutating<T>(
this ObservableCollection<T> list)
{
int i = 0;
NotifyCollectionChangedEventHandler handler = (_, args) =>
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
if (args.NewStartingIndex <= i)
i++;
break;
case NotifyCollectionChangedAction.Move:
if (args.NewStartingIndex <= i)
i++;
if (args.OldStartingIndex <= i) //note *not* else if
i--;
break;
case NotifyCollectionChangedAction.Remove:
if (args.OldStartingIndex <= i)
i--;
break;
case NotifyCollectionChangedAction.Reset:
i = int.MaxValue;//end the sequence
break;
default:
//do nothing
break;
}
};
try
{
list.CollectionChanged += handler;
for (i = 0; i < list.Count; i++)
{
yield return list[i];
}
}
finally
{
list.CollectionChanged -= handler;
}
}