C #에서 열거하는 동안 List <T>에서 항목을 제거하는 지능적인 방법


87

루프에서 열거하는 동안 컬렉션에서 항목을 제거하려는 고전적인 경우가 있습니다.

List<int> myIntCollection = new List<int>();
myIntCollection.Add(42);
myIntCollection.Add(12);
myIntCollection.Add(96);
myIntCollection.Add(25);

foreach (int i in myIntCollection)
{
    if (i == 42)
        myIntCollection.Remove(96);    // The error is here.
    if (i == 25)
        myIntCollection.Remove(42);    // The error is here.
}

변경이 발생한 후 반복이 시작될 InvalidOperationException때 열거자가 기본 컬렉션이 변경되는시기를 좋아하지 않기 때문에가 발생합니다.

반복하는 동안 컬렉션을 변경해야합니다. 이를 방지하는 데 사용할 수있는 패턴 이 많이 있지만 그중 어느 것도 좋은 해결책이없는 것 같습니다.

  1. 이 루프 내부를 삭제하지 말고 대신 메인 루프 이후에 처리하는 별도의 "삭제 목록"을 유지하십시오.

    이것은 일반적으로 좋은 해결책이지만 제 경우에는 항목을 실제로 삭제하기 위해 항목을 실제로 삭제하기 위해 코드의 논리 흐름이 변경 될 때까지 항목이 "대기"상태로 즉시 사라져야합니다.

  2. 항목을 삭제하는 대신 항목에 플래그를 설정하고 비활성으로 표시하면됩니다. 그런 다음 패턴 1의 기능을 추가하여 목록을 정리하십시오.

    것이 내 요구 모두를 위해 작동하지만 그것은 것을 의미합니다 많은 코드가 비활성 플래그를 항목에 액세스 할 때마다 시간을 확인하기 위해 변경해야합니다. 이것은 내 취향에 비해 너무 많은 관리입니다.

  3. 어떻게 든 패턴 2의 아이디어를 List<T>. 이 수퍼리스트는 비활성 플래그, 팩트 이후의 객체 삭제를 처리하고 비활성으로 표시된 항목을 열거 소비자에게 노출하지 않습니다. 기본적으로 패턴 2 (및 이후 패턴 1)의 모든 아이디어를 캡슐화합니다.

    이와 같은 클래스가 있습니까? 누구든지 이것에 대한 코드가 있습니까? 아니면 더 좋은 방법이 있습니까?

  4. myIntCollection.ToArray()대신 액세스 myIntCollection하면 문제가 해결되고 루프 내부에서 삭제할 수 있다고 들었습니다 .

    이것은 나에게 나쁜 디자인 패턴처럼 보이거나 괜찮을까요?

세부:

  • 목록에는 많은 항목이 포함되며 그중 일부만 제거합니다.

  • 루프 내에서 모든 종류의 프로세스, 추가, 제거 등을 수행 할 것이므로 솔루션은 상당히 일반적이어야합니다.

  • 삭제해야하는 항목이 루프의 현재 항목 이 아닐 수 있습니다. 예를 들어, 30 개 항목 루프의 항목 10에 있고 항목 6 또는 항목 26을 제거해야 할 수 있습니다.이 때문에 배열을 뒤로 걸어가는 작업이 더 이상 작동하지 않습니다. ;영형(


다른 사람을위한 유용한 정보 : 수집 방지 오류가 수정되었습니다 (패턴 1의 캡슐화)
George Duckett

참고 : 값을 이동하는 여가 시간 (일반적으로 O (N), 여기서 N은 목록의 길이)을 나열합니다. 효율적인 임의 액세스가 실제로 필요한 경우 루트가있는 하위 트리의 노드 수를 유지하는 균형 이진 트리를 사용하여 O (log N)에서 삭제를 수행 할 수 있습니다. 키 (시퀀스의 인덱스)가 내포 된 BST입니다.
Palec

답변을 참조하십시오 : stackoverflow.com/questions/7193294/…
Dabbas

답변:


196

가장 좋은 해결책은 일반적으로 다음 RemoveAll()방법 을 사용하는 것입니다.

myList.RemoveAll(x => x.SomeProp == "SomeValue");

또는 특정 요소를 제거 해야하는 경우 :

MyListType[] elems = new[] { elem1, elem2 };
myList.RemoveAll(x => elems.Contains(x));

물론 루프가 제거 목적으로 만 사용된다고 가정합니다. 추가 처리 필요한 경우 열거자를 사용하지 않기 때문에 일반적으로 for또는 while루프 를 사용하는 것이 가장 좋은 방법입니다 .

for (int i = myList.Count - 1; i >= 0; i--)
{
    // Do processing here, then...
    if (shouldRemoveCondition)
    {
        myList.RemoveAt(i);
    }
}

뒤로 이동하면 요소를 건너 뛰지 않습니다.

편집에 대한 응답 :

겉보기에 임의의 요소를 제거하려는 경우 가장 쉬운 방법은 제거하려는 요소를 추적 한 다음 한 번에 모두 제거하는 것입니다. 이 같은:

List<int> toRemove = new List<int>();
foreach (var elem in myList)
{
    // Do some stuff

    // Check for removal
    if (needToRemoveAnElement)
    {
        toRemove.Add(elem);
    }
}

// Remove everything here
myList.RemoveAll(x => toRemove.Contains(x));

귀하의 응답에 관하여 : 전체 루프가 처리 된 후가 아니라 해당 항목을 처리하는 동안 즉시 항목을 제거해야합니다. 내가 사용하는 해결책은 즉시 삭제하려는 항목을 NULL로 삭제하고 나중에 삭제하는 것입니다. NULL을 모든 곳에서 확인해야하므로 이상적인 솔루션은 아니지만 작동합니다.
존 스톡

'elem'이 int가 아닌지 궁금하면 편집 된 응답 코드에있는 방식으로 RemoveAll을 사용할 수 없습니다.
User M

22

a를 열거 List<T>하고 제거 해야한다면 a while대신 루프를 사용하는 것이 좋습니다 .foreach

var index = 0;
while (index < myList.Count) {
  if (someCondition(myList[index])) {
    myList.RemoveAt(index);
  } else {
    index++;
  }
}

이것은 제 생각에 받아 들여진 대답이어야합니다. 이를 통해 제거 할 항목의 목록을 반복하지 않고도 목록의 나머지 항목을 고려할 수 있습니다.
Slvrfn

13

이 게시물이 오래되었다는 것을 알고 있지만 저에게 효과가있는 것을 공유 할 것이라고 생각했습니다.

열거 할 목록의 복사본을 만든 다음 for each 루프에서 복사 된 값을 처리하고 소스 목록을 사용하여 제거 / 추가 / 무엇이든 할 수 있습니다.

private void ProcessAndRemove(IList<Item> list)
{
    foreach (var item in list.ToList())
    {
        if (item.DeterminingFactor > 10)
        {
            list.Remove(item);
        }
    }
}

".ToList ()"에 대한 좋은 아이디어! 간단한 헤드 슬 래퍼이며 스톡 "Remove ... ()"메서드를 직접 사용하지 않는 경우에도 작동합니다.
galaxis

1
그래도 매우 비효율적입니다.
nawfal

8

목록을 반복해야하고 루프 중에 수정할 수있는 경우 for 루프를 사용하는 것이 좋습니다.

for (int i = 0; i < myIntCollection.Count; i++)
{
    if (myIntCollection[i] == 42)
    {
        myIntCollection.Remove(i);
        i--;
    }
}

물론주의해야합니다. 예를 들어 i항목이 제거 될 때마다 감소합니다. 그렇지 않으면 항목을 건너 뜁니다 (대안은 목록을 뒤로 이동하는 것입니다).

Linq가 있다면 RemoveAlldlev가 제안한대로 사용해야합니다.


하지만 현재 요소를 제거 할 때만 작동합니다. 임의의 요소를 제거하는 경우 해당 인덱스가 현재 인덱스 이전 / 이후인지 여부를 확인하여 --i.
CompuChip

원래 질문은 현재 요소가 아닌 다른 요소의 삭제가 지원되어야한다는 것을 명확하게 나타내지 않았습니다. @CompuChip. 이 답변은 명확해진 이후 변경되지 않았습니다.
Palec

@Palec, 이해하므로 내 의견.
CompuChip

5

목록을 열거 할 때 보관할 목록을 새 목록에 추가합니다. 그런 다음 새 목록을myIntCollection

List<int> myIntCollection=new List<int>();
myIntCollection.Add(42);
List<int> newCollection=new List<int>(myIntCollection.Count);

foreach(int i in myIntCollection)
{
    if (i want to delete this)
        ///
    else
        newCollection.Add(i);
}
myIntCollection = newCollection;

3

코드를 추가해 보겠습니다.

List<int> myIntCollection=new List<int>();
myIntCollection.Add(42);
myIntCollection.Add(12);
myIntCollection.Add(96);
myIntCollection.Add(25);

foreach에있는 동안 목록을 변경하려면 다음을 입력해야합니다. .ToList()

foreach(int i in myIntCollection.ToList())
{
    if (i == 42)
       myIntCollection.Remove(96);
    if (i == 25)
       myIntCollection.Remove(42);
}

1

도움이 될 수있는 사람들을 위해이 Extension 메서드를 작성하여 조건 자와 일치하는 항목을 제거하고 제거 된 항목 목록을 반환합니다.

    public static IList<T> RemoveAllKeepRemoved<T>(this IList<T> source, Predicate<T> predicate)
    {
        IList<T> removed = new List<T>();
        for (int i = source.Count - 1; i >= 0; i--)
        {
            T item = source[i];
            if (predicate(item))
            {
                removed.Add(item);
                source.RemoveAt(i);
            }
        }
        return removed;
    }

0

어때

int[] tmp = new int[myIntCollection.Count ()];
myIntCollection.CopyTo(tmp);
foreach(int i in tmp)
{
    myIntCollection.Remove(42); //The error is no longer here.
}

현재 C #에서는 foreach (int i in myIntCollection.ToArray()) { myIntCollection.Remove(42); }모든 열거 형 으로 다시 작성할 수 있으며 List<T>특히 .NET 2.0 에서도이 메서드를 지원합니다.
Palec

0

고성능에 관심이 있다면 두 개의 목록을 사용할 수 있습니다. 다음은 가비지 수집을 최소화하고 메모리 지역성을 최대화하며 목록에서 항목을 실제로 제거하지 않습니다. 이는 마지막 항목이 아닌 경우 매우 비효율적입니다.

private void RemoveItems()
{
    _newList.Clear();

    foreach (var item in _list)
    {
        item.Process();
        if (!item.NeedsRemoving())
            _newList.Add(item);
    }

    var swap = _list;
    _list = _newList;
    _newList = swap;
}

0

항목을 처리하는 동안 목록에서 항목을 제거해야하는 유사한 문제에 대한 솔루션을 공유 할 것이라고 생각했습니다.

따라서 기본적으로 "foreach"는 항목이 반복 된 후 목록에서 항목을 제거합니다.

내 테스트 :

var list = new List<TempLoopDto>();
list.Add(new TempLoopDto("Test1"));
list.Add(new TempLoopDto("Test2"));
list.Add(new TempLoopDto("Test3"));
list.Add(new TempLoopDto("Test4"));

list.PopForEach((item) =>
{
    Console.WriteLine($"Process {item.Name}");
});

Assert.That(list.Count, Is.EqualTo(0));

작업을 수행 한 다음 목록에서 항목을 제거하는 확장 메서드 "PopForEach"로이 문제를 해결했습니다.

public static class ListExtensions
{
    public static void PopForEach<T>(this List<T> list, Action<T> action)
    {
        var index = 0;
        while (index < list.Count) {
            action(list[index]);
            list.RemoveAt(index);
        }
    }
}

이것이 누구에게나 도움이되기를 바랍니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.