为什么在迭代集合时不应该修改集合?
迭代时可以修改某些集合,因此它不是全局坏的。在大多数情况下,很难编写一个有效的迭代器,即使底层集合被修改也能正常工作。在许多情况下,迭代器编写者会说他们只是不想处理它。
在某些情况下,当底层集合发生变化时,迭代器应该做什么并不清楚。有些情况是明确的,但对于其他情况,不同的人会期望不同的行为。每当您处于这种情况时,这表明存在更深层次的问题(您不应该改变正在迭代的序列)
是否可以创建一个支持在对其进行迭代时进行修改的集合,而不会出现任何其他问题?(注意:第一个答案也可以回答这个)
当然。
考虑这个迭代器的列表:
public static IEnumerable<T> IterateWhileMutating<T>(this IList<T> list)
{
for (int i = 0; i < list.Count; i++)
{
yield return list[i];
}
}
如果您从基础列表中删除当前索引处或之前的项目,则迭代时将跳过项目。如果您在当前索引处或之前添加一个项目,则该项目将被复制。但是,如果您在迭代期间添加/删除超过当前索引的项目,那么就不会有问题。我们可以试着花点时间尝试查看是否从列表中删除/添加了一个项目并相应地调整索引,但它并不总是有效,因此我们无法处理所有情况。如果我们有类似的东西,ObservableCollection
那么我们可以收到添加/删除及其索引的通知并相应地调整索引,从而允许迭代器处理底层集合的变异(只要它不在另一个线程中)。
由于 an 的迭代器ObservableCollection
可以知道添加/删除任何项目的时间以及它们的位置,因此它可以相应地调整其位置。我不确定内置迭代器是否正确处理突变,但这里有一个可以处理底层集合的任何突变:
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;
}
}
如果一个项目从序列中的“早期”中删除,我们会正常继续而不跳过一个项目。
如果在序列中“更早”添加了一个项目,我们将不会显示它,但我们也不会显示其他项目两次。
如果一个项目从当前位置之前移动到之后,它将显示两次,但不会跳过或重复其他项目。如果一个项目从当前位置之后移动到当前位置之前,它将不会显示,但仅此而已。如果一个项目从集合中的后一个位置移动到另一个位置,则没有问题,移动将在结果中看到,如果它从较早的位置移动到另一个较早的位置,一切都很好,移动迭代器不会“看到”。
更换物品不是问题;但是,只有在当前位置“之后”时才能看到它。
重置集合会导致序列在当前位置优雅地结束。
请注意,此迭代器不会处理具有多个线程的情况。如果另一个线程在另一个线程迭代时改变了集合,则可能会发生不好的事情(项目被跳过或重复,甚至出现异常,例如索引超出范围异常)。这确实允许在迭代期间发生突变,其中要么只有一个线程,要么只有一个线程正在执行移动迭代器或改变集合的代码。
当 C# 编译器生成 Enumerator 接口实现时会考虑到这种情况吗?
编译器不生成接口实现;一个人会。