C#/ .NET 4.0中的一项新功能是,您可以在foreach中更改您的枚举,而不会出现异常。有关此更改的信息,请参见Paul Jackson的博客条目。有趣的并发副作用:枚举时从集合中删除项目。
执行以下操作的最佳方法是什么?
foreach(var item in Enumerable)
{
    foreach(var item2 in item.Enumerable)
    {
        item.Add(new item2)
    }
}

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

评论

嗯..您能指出我们有关此更改的文档吗?枚举集合时,可枚举始终是不可变的。

这是主题的一种变体,促使史蒂夫·麦康奈尔(Steve McConnell)建议不要让循环索引胡闹。

我知道我在这里进行很老的对话,但是对此我会非常小心。在foreach中只有新的并发集合可以修改-所有以前的集合类型,我想将来的大多数集合类型在枚举它们时仍将是不可变的。大量使用此怪癖将有效地锁定您使用并发集合,因为如果将来您想使用其他集合,则所有怪异的foreach循环都会突然中断。

C#的可能重复-不要胡闹循环索引

这个问题是否专门询问并发集合?还是在问一个更笼统的问题,并只是为了对比而提到它?

#1 楼

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

如MSDN上所述:


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


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

评论


您所回答的并不能真正回答问题。他知道基本的foreach循环是不可变的……他想知道将更改应用于可枚举集合的最佳方法。

–乔希(Josh G)
09年4月17日在12:46

答案是:使用常规的for循环,如引文中所建议。我知道OP在C#4.0中提到了此行为更改,但是我找不到任何有关此信息。就目前而言,我认为这仍然是一个相关的答案。

–力克
09年4月17日在12:51

#2 楼

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

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


评论


但是,为什么将可枚举复制到列表而不是数组中?列表提供了搜索,排序和操作集合的方法,而我们不需要这些方法。

–Rudey
17-10-3在8:37

#3 楼

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

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


评论


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

–antiduh
18-2-26在23:48

#4 楼

为了说明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个项目)
1
2
3
4
5
6
8
9
5
7
9

#5 楼

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

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

要么:

1)首先复制集合。枚举复制的集合,并在枚举期间更改原始集合。 (@tvanfosson)



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

#6 楼

这是您可以执行的操作(快速又肮脏的解决方案。如果您确实需要这种行为,则应重新考虑设计或覆盖所有IList<T>成员并汇总源列表):

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


#7 楼

LINQ在处理收藏时非常有效。

我不清楚您的类型和结构,但我会尽我所能使您的示例适合您。

来自您的代码似乎表明,对于每个项目,您正在将其“ Enumerable”属性中的所有内容添加到该项目中。这非常简单:

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


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

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


基于每个现有项添加项?没问题:

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


#8 楼

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

例如,假设您有一个List<string>,并且希望列表中以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<T>提供了一种从数组的一部分构造列表的方法,那将会很有帮助,但是我不知道这样做的任何有效方法。尽管如此,对阵列的操作还是非常快的。值得注意的是,从列表中添加和删除项目并不需要“推”其他项目。每个项目都直接写入数组中的相应位置。

#9 楼

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

评论


这可能会使代码杂乱无章,并带有另一个变量,该变量将保留集合中元素的原始数量。

– Anton Gogolev
09年4月17日在10:53

如果要更改集合的长度,则希望针对新长度而不是原始长度评估for循环,否则可能会超出范围

– cjk
09年4月17日在10:56

@Anton:您将需要该额外的变量,因为修改集合后,您必须自己管理迭代

–力克
09年4月17日在11:19

@Nippysaurus:听起来不错,理论上我也同意您的观点,但是有些收藏无法编入索引。您必须遍历它们。

–乔希(Josh G)
09年4月17日在12:11

#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 楼

我已经写了一个简单的步骤,但是由于此性能会降低

这是我的代码段:-

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