What is the best way to modify a list in a 'foreach' loop?

前端 未结 11 744
不思量自难忘°
不思量自难忘° 2020-11-22 05:23

A new feature in C# / .NET 4.0 is that you can change your enumerable in a foreach without getting the exception. See Paul Jackson\'s blog entry An Interest

相关标签:
11条回答
  • 2020-11-22 05:57

    Here's how you can do that (quick and dirty solution. If you really need this kind of behavior, you should either reconsider your design or override all IList<T> members and aggregate the source list):

    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);
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-22 05:59

    The collection used in foreach is immutable. This is very much by design.

    As it says on MSDN:

    The foreach statement is used to iterate through the collection to get the information that you want, but can not be used to add or remove items from the source collection to avoid unpredictable side effects. If you need to add or remove items from the source collection, use a for loop.

    The post in the link provided by Poko indicates that this is allowed in the new concurrent collections.

    0 讨论(0)
  • 2020-11-22 05:59

    LINQ is very effective for juggling with collections.

    Your types and structure are unclear to me, but I will try to fit your example to the best of my ability.

    From your code it appears that, for each item, you are adding to that item everything from its own 'Enumerable' property. This is very simple:

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

    As a more general example, let's say we want to iterate a collection and remove items where a certain condition is true. Avoiding foreach, using LINQ:

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

    Add an item based on each existing item? No problem:

    myCollection = myCollection.Concat(myCollection.Select(item => new Item(item.SomeProp)));
    
    0 讨论(0)
  • 2020-11-22 06:05

    The best approach from a performance perspective is probably to use a one or two arrays. Copy the list to an array, do operations on the array, and then build a new list from the array. Accessing an array element is faster than accessing a list item, and conversions between a List<T> and a T[] can use a fast "bulk copy" operation which avoids the overhead associated accessing individual items.

    For example, suppose you have a List<string> and wish to have every string in the list which starts with T be followed by an item "Boo", while every string that starts with "U" is dropped entirely. An optimal approach would probably be something like:

    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
    }
    

    It would have been helpful if List<T> provided a method to construct a list from a portion of an array, but I'm unaware of any efficient method for doing so. Still, operations on arrays are pretty fast. Of note is the fact that adding and removing items from the list does not require "pushing" around other items; each item gets written directly to its appropriate spot in the array.

    0 讨论(0)
  • 2020-11-22 06:06

    Make a copy of the enumeration, using an IEnumerable extension method in this case, and enumerate over it. This would add a copy of every element in every inner enumerable to that enumeration.

    foreach(var item in Enumerable)
    {
        foreach(var item2 in item.Enumerable.ToList())
        {
            item.Add(item2)
        }
    }
    
    0 讨论(0)
  • 2020-11-22 06:06

    To add to Timo's answer LINQ can be used like this as well:

    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);
    
    0 讨论(0)
提交回复
热议问题