【问题标题】:What is the best way to modify a list in a 'foreach' loop?在“foreach”循环中修改列表的最佳方法是什么?
【发布时间】:2010-10-20 02:03:54
【问题描述】:

C#/.NET 4.0 中的一个新特性是,您可以在 foreach 中更改您的枚举,而不会出现异常。有关此更改的信息,请参阅 Paul Jackson 的博客条目 An Interesting Side-Effect of Concurrency: Removing Items from a Collection While Enumerating

执行以下操作的最佳方法是什么?

foreach(var item in Enumerable)
{
    foreach(var item2 in item.Enumerable)
    {
        item.Add(new item2)
    }
}

通常我使用IList 作为缓存/缓冲区,直到foreach 结束,但有更好的方法吗?

【问题讨论】:

  • Hmm.. 你能指出我们这个变化的文档吗?枚举集合时,可枚举对象始终是不可变的。
  • 这是一个主题的变体,促使史蒂夫·麦康奈尔建议never to monkey with the loop index
  • 我知道我在这里挖掘了一个非常古老的对话,但我会非常小心。只有新的并发集合在 foreach 中是可修改的——所有以前的集合类型,我想未来的大多数集合类型也将在枚举它们时仍然是不可变的。大量使用这个怪癖会有效地锁定你使用并发集合,因为如果你想在未来使用不同的集合,所有你怪异的 foreach 循环都会突然中断。
  • 这个问题是专门询问并发集合的吗?还是它提出了一个更笼统的问题并仅出于对比而提及?

标签: c# .net list enumeration enumerable


【解决方案1】:

foreach 中使用的集合是不可变的。这在很大程度上是设计使然。

正如MSDN 所说:

foreach 语句用于 遍历集合以获得 您想要的信息,但可以 不用于添加或删除项目 从源集合中避免 不可预知的副作用。 如果你 需要添加或删除项目 源集合,使用 for 循环。

Poko 提供的link 中的帖子表明这在新的并发集合中是允许的。

【讨论】:

  • 您的回答并没有真正回答问题。他知道基本的 foreach 循环是不可变的……他想知道将更改应用于可枚举集合的最佳方式。
  • 那么答案是:按照引用中的建议使用常规的 for 循环。我知道 OP 在 C# 4.0 中提到了这种行为变化,但我找不到任何相关信息。目前,我认为这仍然是一个相关的答案。
【解决方案2】:

制作枚举的副本,在这种情况下使用 IEnumerable 扩展方法,并对其进行枚举。这会将每个内部可枚举中的每个元素的副本添加到该枚举。

foreach(var item in Enumerable)
{
    foreach(var item2 in item.Enumerable.ToList())
    {
        item.Add(item2)
    }
}

【讨论】:

  • 但是为什么要将枚举复制到列表而不是数组?列表提供了我们不需要的搜索、排序和操作集合的方法。
  • 这应该是公认的答案!而说不可能回答的人被标记为回答!
【解决方案3】:

为了说明 Nippysaurus 的回答:如果您要将新项目 添加 到列表中,并希望在同一枚举期间也处理新添加的项目,那么您可以使用 for 循环而不是 foreach 循环,问题解决了 :)

var list = new List<YourData>();
... populate the list ...

//foreach (var entryToProcess in list)
for (int i = 0; i < list.Count; i++)
{
    var entryToProcess = list[i];

    var resultOfProcessing = DoStuffToEntry(entryToProcess);

    if (... condition ...)
        list.Add(new YourData(...));
}

对于可运行的示例:

void Main()
{
    var list = new List<int>();
    for (int i = 0; i < 10; i++)
        list.Add(i);

    //foreach (var entry in list)
    for (int i = 0; i < list.Count; i++)
    {
        var entry = list[i];
        if (entry % 2 == 0)
            list.Add(entry + 1);

        Console.Write(entry + ", ");
    }

    Console.Write(list);
}

最后一个例子的输出:

0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 3, 5, 7, 9,

列表(15 项)
0
1
2
3
4
5
6
7
8
9
1
3
5
7
9

【讨论】:

    【解决方案4】:

    如前所述,但带有代码示例:

    foreach(var item in collection.ToArray())
        collection.Add(new Item...);
    

    【讨论】:

    • 这不是一个很好的答案,因为它执行不必要的分配和复制 (ToArray()) 只是为了遍历列表。
    【解决方案5】:

    您无法在枚举时更改可枚举集合,因此您必须在枚举之前或之后进行更改。

    for 循环是一个不错的选择,但如果您的 IEnumerable 集合没有实现 ICollection,则不可能。

    要么:

    1) 先复制收藏。枚举复制的集合并在枚举过程中更改原始集合。 (@tvanfosson)

    2) 保留更改列表并在枚举后提交。

    【讨论】:

      【解决方案6】:

      在这种情况下,您真的应该使用for() 而不是foreach()

      【讨论】:

      • 这可能会使代码变得混乱,其中包含一个变量,该变量将保存集合中的原始元素数量。
      • 如果您要更改集合的长度,那么您希望根据新长度而不是原始长度来评估您的 for 循环,否则您可能会遇到超出范围的异常
      • @Anton:你将需要那个额外的变量,因为当集合被修改时你必须自己管理迭代
      • @Nippysaurus:听起来不错,理论上我同意你的观点,但有些收藏无法编入索引。你必须遍历它们。
      【解决方案7】:

      您可以这样做(快速而肮脏的解决方案。如果您真的需要这种行为,您应该重新考虑您的设计或覆盖所有 IList&lt;T&gt; 成员并汇总源列表) :

      using System;
      using System.Collections.Generic;
      
      namespace ConsoleApplication3
      {
          public class ModifiableList<T> : List<T>
          {
              private readonly IList<T> pendingAdditions = new List<T>();
              private int activeEnumerators = 0;
      
              public ModifiableList(IEnumerable<T> collection) : base(collection)
              {
              }
      
              public ModifiableList()
              {
              }
      
              public new void Add(T t)
              {
                  if(activeEnumerators == 0)
                      base.Add(t);
                  else
                      pendingAdditions.Add(t);
              }
      
              public new IEnumerator<T> GetEnumerator()
              {
                  ++activeEnumerators;
      
                  foreach(T t in ((IList<T>)this))
                      yield return t;
      
                  --activeEnumerators;
      
                  AddRange(pendingAdditions);
                  pendingAdditions.Clear();
              }
          }
      
          class Program
          {
              static void Main(string[] args)
              {
                  ModifiableList<int> ints = new ModifiableList<int>(new int[] { 2, 4, 6, 8 });
      
                  foreach(int i in ints)
                      ints.Add(i * 2);
      
                  foreach(int i in ints)
                      Console.WriteLine(i * 2);
              }
          }
      }
      

      【讨论】:

        【解决方案8】:

        LINQ 对于处理集合非常有效。

        我不清楚你的类型和结构,但我会尽力让你的榜样适应我的能力。

        从您的代码看来,对于每个项目,您都从它自己的“可枚举”属性向该项目添加所有内容。这很简单:

        foreach (var item in Enumerable)
        {
            item = item.AddRange(item.Enumerable));
        }
        

        作为一个更一般的示例,假设我们要迭代一个集合并删除某个条件为真的项目。避免foreach,使用LINQ:

        myCollection = myCollection.Where(item => item.ShouldBeKept);
        

        根据每个现有项目添加一个项目?没问题:

        myCollection = myCollection.Concat(myCollection.Select(item => new Item(item.SomeProp)));
        

        【讨论】:

          【解决方案9】:

          从性能角度来看,最好的方法可能是使用一个或两个数组。将列表复制到数组,对数组进行操作,然后从数组中构建一个新列表。访问数组元素比访问列表项要快,List&lt;T&gt;T[] 之间的转换可以使用快速的“批量复制”操作,从而避免访问单个项目相关的开销。

          例如,假设您有一个List&lt;string&gt;,并希望列表中以T 开头的每个字符串后跟一个项目“Boo”,而每个以“U”开头的字符串都被完全删除。最佳方法可能是这样的:

          int srcPtr,destPtr;
          string[] arr;
          
          srcPtr = theList.Count;
          arr = new string[srcPtr*2];
          theList.CopyTo(arr, theList.Count); // Copy into second half of the array
          destPtr = 0;
          for (; srcPtr < arr.Length; srcPtr++)
          {
            string st = arr[srcPtr];
            char ch = (st ?? "!")[0]; // Get first character of string, or "!" if empty
            if (ch != 'U')
              arr[destPtr++] = st;
            if (ch == 'T')
              arr[destPtr++] = "Boo";
          }
          if (destPtr > arr.Length/2) // More than half of dest. array is used
          {
            theList = new List<String>(arr); // Adds extra elements
            if (destPtr != arr.Length)
              theList.RemoveRange(destPtr, arr.Length-destPtr); // Chop to proper length
          }
          else
          {
            Array.Resize(ref arr, destPtr);
            theList = new List<String>(arr); // Adds extra elements
          }
          

          如果List&lt;T&gt; 提供了一种从数组的一部分构造列表的方法会很有帮助,但我不知道有任何有效的方法可以做到这一点。尽管如此,对数组的操作还是相当快的。值得注意的是,从列表中添加和删除项目不需要“推动”其他项目。每个项目都直接写入数组中的相应位置。

          【讨论】:

            【解决方案10】:

            添加到 Timo 的答案中,LINQ 也可以这样使用:

            items = items.Select(i => {
            
                 ...
                 //perform some logic adding / updating.
            
                 return i / return new Item();
                 ...
            
                 //To remove an item simply have logic to return null.
            
                 //Then attach the Where to filter out nulls
            
                 return null;
                 ...
            
            
            }).Where(i => i != null);
            

            【讨论】:

              【解决方案11】:

              我已经写了一个简单的步骤,但是因为这个性能会下降

              这是我的代码 sn-p:-

              for (int tempReg = 0; tempReg < reg.Matches(lines).Count; tempReg++)
                                          {
                                              foreach (Match match in reg.Matches(lines))
                                              {
                                                  var aStringBuilder = new StringBuilder(lines);
                                                  aStringBuilder.Insert(startIndex, match.ToString().Replace(",", " ");
                                                  lines[k] = aStringBuilder.ToString();
                                                  tempReg = 0;
                                                  break;
                                              }
                                          }
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2011-09-11
                • 1970-01-01
                • 2018-01-08
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多