C# 为什么在迭代集合时不应修改它

C# 为什么在迭代集合时不应修改它,c#,.net,collections,ienumerable,C#,.net,Collections,Ienumerable,我知道,在.net集合类型(或至少某些集合类型)中,当您对集合进行迭代时,不允许修改该集合 例如,在List类中存在如下代码: if (this.version != this.list._version) ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion); 但很明显,这是设计迭代器类的开发人员的决定,因为我可以提供IEnumerable的一些实现,

我知道,在.net集合类型(或至少某些集合类型)中,当您对集合进行迭代时,不允许修改该集合

例如,在List类中存在如下代码:

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;
          }
      }