Maneira inteligente de remover itens de um List <T> enquanto enumerar em C #

86

Tenho o caso clássico de tentar remover um item de uma coleção enquanto o enumera em um loop:

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

No início da iteração, após uma mudança ocorrer, um InvalidOperationExceptioné lançado, porque os enumeradores não gostam quando a coleção subjacente muda.

Preciso fazer alterações na coleção durante a iteração. Existem muitos padrões que podem ser usados ​​para evitar isso , mas nenhum deles parece ter uma boa solução:

  1. Não exclua dentro deste loop; em vez disso, mantenha uma “Lista de Exclusão” separada, que você processa após o loop principal.

    Normalmente, essa é uma boa solução, mas no meu caso, preciso que o item desapareça instantaneamente, pois "esperar" até que o loop principal realmente exclua o item altera o fluxo lógico do meu código.

  2. Em vez de excluir o item, basta definir um sinalizador no item e marcá-lo como inativo. Em seguida, adicione a funcionalidade do padrão 1 para limpar a lista.

    Isso iria trabalhar para todas as minhas necessidades, mas isso significa que um monte de código terá que mudar a fim de verificar a bandeira inativo sempre que um item é acessado. Isso é administração demais para o meu gosto.

  3. De alguma forma, incorpore as idéias do padrão 2 em uma classe que deriva de List<T>. Esta Superlista tratará do sinalizador inativo, da exclusão de objetos após o fato e também não exporá os itens marcados como inativos aos consumidores de enumeração. Basicamente, ele apenas encapsula todas as idéias do padrão 2 (e subsequentemente do padrão 1).

    Existe uma classe como esta? Alguém tem código para isso? Ou há um modo melhor?

  4. Disseram-me que acessar em myIntCollection.ToArray()vez de myIntCollectionresolverá o problema e me permitirá deletar dentro do loop.

    Isso parece um padrão de design ruim para mim, ou talvez seja bom?

Detalhes:

  • A lista conterá muitos itens e irei remover apenas alguns deles.

  • Dentro do loop, estarei realizando todos os tipos de processos, incluindo, removendo etc., portanto, a solução precisa ser bastante genérica.

  • O item que preciso excluir pode não ser o item atual no loop. Por exemplo, posso estar no item 10 de um loop de 30 itens e precisar remover o item 6 ou o item 26. Andar para trás pela matriz não funcionará mais por causa disso. ; o (

John Stock
fonte
Possível informação útil para outra pessoa: Erro de modificação de Avoid Collection (um encapsulamento do padrão 1)
George Duckett
Uma observação lateral: as listas poupam muito tempo (geralmente O (N), onde N é o comprimento da lista) movendo os valores. Se o acesso aleatório eficiente for realmente necessário, é possível obter exclusões em O (log N), usando uma árvore binária balanceada contendo o número de nós na subárvore de cuja raiz ele está. É um BST cuja chave (o índice na sequência) está implícita.
Palec 01 de
Por favor, veja a resposta: stackoverflow.com/questions/7193294/…
Dabbas

Respostas:

195

A melhor solução geralmente é usar o RemoveAll()método:

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

Ou, se você precisar remover certos elementos:

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

Isso pressupõe que seu loop se destina exclusivamente a fins de remoção, é claro. Se você fazer necessidade de processamento adicional, então o melhor método é geralmente a utilização de um forou whileloop, desde então você não está usando um enumerador:

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

Retroceder garante que você não pule nenhum elemento.

Resposta para editar :

Se você deseja remover elementos aparentemente arbitrários, o método mais fácil pode ser apenas manter o controle dos elementos que deseja remover e removê-los todos de uma vez depois. Algo assim:

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));
dlev
fonte
Em relação à sua resposta: Eu preciso remover os itens instantaneamente durante o processamento desse item, não depois que todo o ciclo foi processado. A solução que estou usando é anular todos os itens que desejo excluir instantaneamente e depois excluí-los. Esta não é uma solução ideal porque eu tenho que verificar se há NULL em todo o lugar, mas FUNCIONA.
John Stock
Querendo saber se 'elem' não é um int, então não podemos usar RemoveAll dessa forma que está presente no código de resposta editado.
Usuário M de
22

Se você deve enumerar a List<T>e remover dele, então sugiro simplesmente usar um whileloop em vez de umforeach

var index = 0;
while (index < myList.Count) {
  if (someCondition(myList[index])) {
    myList.RemoveAt(index);
  } else {
    index++;
  }
}
JaredPar
fonte
Esta deve ser a resposta aceita em minha opinião. Isso permite que você considere o restante dos itens em sua lista, sem repetir uma lista de itens a serem removidos.
Slvrfn
12

Sei que este post é antigo, mas pensei em compartilhar o que funcionou para mim.

Crie uma cópia da lista para enumerar e, em seguida, no for each loop, você pode processar os valores copiados e remover / adicionar / qualquer coisa com a lista de origem.

private void ProcessAndRemove(IList<Item> list)
{
    foreach (var item in list.ToList())
    {
        if (item.DeterminingFactor > 10)
        {
            list.Remove(item);
        }
    }
}
D-Jones
fonte
Excelente ideia no ".ToList ()"! Tapa na cabeça simples e também funciona nos casos em que você não está usando o estoque de metanfetamina "Remover ... ()" diretamente.
user1172173
Muito ineficiente.
nawfal
8

Quando você precisa iterar por uma lista e pode modificá-la durante o loop, é melhor usar um loop for:

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

Claro que você deve ter cuidado, por exemplo, eu decremento isempre que um item é removido, caso contrário, iremos pular as entradas (uma alternativa é voltar para trás na lista).

Se você tiver o Linq, você deve apenas usar RemoveAllcomo dlev sugeriu.

Justin
fonte
Só funciona quando você está removendo o elemento atual. Se estiver removendo um elemento arbitrário, você precisará verificar se seu índice estava em / antes ou depois do índice atual para decidir se o faz --i.
CompuChip
A pergunta original não deixava claro que a exclusão de outros elementos além do atual deve ser suportada, @CompuChip. Esta resposta não mudou desde que foi esclarecida.
Palec 01 de
@Palec, eu entendo, daí meu comentário.
CompuChip
5

Conforme você enumera a lista, adicione aquele que deseja MANTER a uma nova lista. Depois, atribua a nova lista aomyIntCollection

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;
James Curran
fonte
3

Vamos adicionar seu código:

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

Se você quiser mudar a lista enquanto estiver em um foreach, você deve digitar .ToList()

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

E se

int[] tmp = new int[myIntCollection.Count ()];
myIntCollection.CopyTo(tmp);
foreach(int i in tmp)
{
    myIntCollection.Remove(42); //The error is no longer here.
}
Olaf
fonte
No C # atual, ele pode ser reescrito como foreach (int i in myIntCollection.ToArray()) { myIntCollection.Remove(42); }para qualquer enumerável e, List<T>especificamente, oferece suporte a esse método mesmo no .NET 2.0.
Palec
0

Se você estiver interessado em alto desempenho, pode usar duas listas. O seguinte minimiza a coleta de lixo, maximiza a localidade da memória e nunca remove realmente um item de uma lista, o que é muito ineficiente se não for o último item.

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;
}
Will Calderwood
fonte
0

Para aqueles que podem ajudar, escrevi este método de extensão para remover itens correspondentes ao predicado e retornar a lista de itens removidos.

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