Como remover elementos de uma lista genérica enquanto itera sobre ela?

451

Estou procurando um padrão melhor para trabalhar com uma lista de elementos que cada um precisa ser processado e, dependendo do resultado, serão removidos da lista.

Você não pode usar .Remove(element)dentro de um foreach (var element in X)(porque resulta em Collection was modified; enumeration operation may not execute.exceção) ... você também não pode usar for (int i = 0; i < elements.Count(); i++)e .RemoveAt(i)porque isso interrompe sua posição atual na coleção em relação a i.

Existe uma maneira elegante de fazer isso?

InvertedAcceleration
fonte

Respostas:

734

Itere sua lista no sentido inverso com um loop for:

for (int i = safePendingList.Count - 1; i >= 0; i--)
{
    // some code
    // safePendingList.RemoveAt(i);
}

Exemplo:

var list = new List<int>(Enumerable.Range(1, 10));
for (int i = list.Count - 1; i >= 0; i--)
{
    if (list[i] > 5)
        list.RemoveAt(i);
}
list.ForEach(i => Console.WriteLine(i));

Como alternativa, você pode usar o método RemoveAll com um predicado para testar:

safePendingList.RemoveAll(item => item.Value == someValue);

Aqui está um exemplo simplificado para demonstrar:

var list = new List<int>(Enumerable.Range(1, 10));
Console.WriteLine("Before:");
list.ForEach(i => Console.WriteLine(i));
list.RemoveAll(i => i > 5);
Console.WriteLine("After:");
list.ForEach(i => Console.WriteLine(i));
Ahmad Mageed
fonte
19
Para quem vem de Java, a lista de C # é como ArrayList, em que a inserção / remoção é O (n) e a recuperação através de um índice é O (1). Esta não é uma lista vinculada tradicional. Parece um pouco lamentável que o C # use a palavra "Lista" para descrever essa estrutura de dados, pois traz à mente a lista vinculada clássica.
perfil completo de Jarrod Smith
79
Nada no nome 'Lista' indica 'LinkedList'. Pessoas provenientes de outras linguagens que não Java podem ficar confusas quando se trata de uma lista vinculada.
GolezTrol
5
Eu acabei aqui através de uma pesquisa vb.net, portanto, caso alguém queira a sintaxe equivalente vb.net para RemoveAll:list.RemoveAll(Function(item) item.Value = somevalue)
pseudocoder
1
Isso também funciona em Java com modificações mínimas: em .size()vez de .Counte em .remove(i)vez de .removeAt(i). Inteligente - obrigado!
Autumn Leonard
2
Fiz um pequeno teste de desempenho e, ao que parece, RemoveAll()leva três vezes mais tempo que o forloop reverso. Então, eu definitivamente estou aderindo ao loop, pelo menos nas seções onde isso importa.
Crusha K.Rool
84

Uma solução simples e direta:

Use um loop for padrão correndo para trás em sua coleção e RemoveAt(i)para remover elementos.

Jan
fonte
2
Esteja ciente de que a remoção de itens um de cada vez não será eficiente se sua lista contiver muitos itens. Tem potencial para ser O (n ^ 2). Imagine uma lista com dois bilhões de itens em que apenas o primeiro bilhão de itens acaba sendo excluído. Cada remoção força todos os itens posteriores a serem copiados, então você acaba copiando um bilhão de itens um bilhão de vezes cada. Isso não é devido à iteração reversa, é devido à remoção de uma vez. RemoveAll garante que cada item seja copiado no máximo uma vez, para que seja linear. A remoção única de cada vez pode ser um bilhão de vezes mais lenta. O (n) versus O (n ^ 2).
Bruce Dawson
71

A iteração reversa deve ser a primeira coisa a se lembrar quando você deseja remover elementos de uma coleção enquanto itera sobre ela.

Felizmente, existe uma solução mais elegante do que escrever um loop for, que envolve digitação desnecessária e pode estar sujeito a erros.

ICollection<int> test = new List<int>(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10});

foreach (int myInt in test.Reverse<int>())
{
    if (myInt % 2 == 0)
    {
        test.Remove(myInt);
    }
}
jedesah
fonte
7
Isso funcionou perfeitamente para mim. Simples, elegante e exigia alterações mínimas no meu código.
Stephen MacDougall
1
Isso é apenas um talento genial? E eu concordo com o @StephenMacDougall, não preciso usar esses C ++ 'y para loops e apenas continuar com o LINQ como pretendido.
Piotr Kula
5
Não vejo nenhuma vantagem sobre o foreach simples (int myInt em test.ToList ()) {if (myInt% 2 == 0) {test.Remove (myInt); }} Você ainda precisa alocar uma cópia para Reverse e ela apresenta Huh? momento - por que há reverso.
jahav
11
@jedesah Sim, Reverse<T>()cria um iterador que percorre a lista de trás para frente, mas aloca buffer extra com o mesmo tamanho da lista em si ( referenceource.microsoft.com/#System.Core/System/Linq/… ). Reverse<T>não passa apenas pela lista original na ordem inversa (sem alocar memória extra). Portanto, ambos ToList()e Reverse()têm o mesmo consumo de memória (ambos criam cópia), mas ToList()não fazem nada com os dados. Com Reverse<int>(), gostaria de saber por que a lista é revertida, por que motivo.
jahav
1
@jahav Entendo o seu ponto. Isso é bastante decepcionante porque a implementação de Reverse<T>()cria um novo buffer, não tenho muita certeza de entender por que isso é necessário. Parece-me que, dependendo da estrutura subjacente da Enumerable, pelo menos em alguns casos deve ser possível obter a iteração reversa sem alocar memória linear.
jedesah
68
 foreach (var item in list.ToList()) {
     list.Remove(item);
 }

Se você adicionar ".ToList ()" à sua lista (ou aos resultados de uma consulta LINQ), poderá remover o "item" diretamente da "lista" sem a temida " Coleção modificada; a operação de enumeração pode não ser executada ". erro. O compilador faz uma cópia da "lista", para que você possa remover com segurança a matriz.

Embora esse padrão não seja super eficiente, ele tem uma sensação natural e é flexível o suficiente para quase qualquer situação . Por exemplo, quando você deseja salvar cada "item" em um banco de dados e removê-lo da lista somente quando o salvamento do banco de dados for bem-sucedido.

Greg Little
fonte
6
Esta é a melhor solução se a eficiência não for crucial.
IvanP 02/06
2
isso também é mais rápido e legível: list.RemoveAll (i => true);
santiago Arizti
1
@ Greg Pouco, eu entendi você corretamente - quando você adiciona o compilador ToList () passa pela coleção copiada, mas remove da original?
Pyrejkee
Se sua lista contiver itens duplicados e você desejar remover o item que ocorrer mais tarde na lista, isso não removerá o item errado?
Tim MB
Isso também funciona para uma lista de objetos?
Tom el Safadi
23

Selecione os elementos que você não quer, em vez de tentar remover os elementos que você não quer. Isso é muito mais fácil (e geralmente mais eficiente também) do que remover elementos.

var newSequence = (from el in list
                   where el.Something || el.AnotherThing < 0
                   select el);

Eu queria postar isso como um comentário em resposta ao comentário deixado por Michael Dillon abaixo, mas é muito longo e provavelmente útil para ter em minha resposta de qualquer maneira:

Pessoalmente, eu nunca removeria itens um por um, se você precisar removê-los, em seguida, chamar RemoveAllque leva um predicado e reorganiza a matriz interna apenas uma vez, enquanto Removefaz uma Array.Copyoperação para cada elemento que você remove. RemoveAllé muito mais eficiente.

E quando você está repetindo a iteração sobre uma lista, você já possui o índice do elemento que deseja remover, portanto seria muito mais eficiente chamar RemoveAt, porque Removeprimeiro é percorrida a lista para encontrar o índice do elemento que você deseja remover. está tentando remover, mas você já conhece esse índice.

Então, apesar de tudo, não vejo motivo para chamar Removeum loop for. E, idealmente, se for possível, use o código acima para transmitir elementos da lista conforme necessário, para que nenhuma segunda estrutura de dados tenha que ser criada.

JulianR
fonte
1
Ou adicione um ponteiro aos elementos indesejados em uma segunda lista; depois que o loop terminar, itere a lista de remoção e use-a para remover os elementos.
18711 Michael Dillon
22

O uso do ToArray () em uma lista genérica permite que você remova (item) na sua lista genérica:

        List<String> strings = new List<string>() { "a", "b", "c", "d" };
        foreach (string s in strings.ToArray())
        {
            if (s == "b")
                strings.Remove(s);
        }
Etienne Brouillard
fonte
2
Isso não está errado, mas devo salientar que isso ignora a necessidade de criar uma segunda lista de "armazenamento" dos itens que você deseja remover às custas de copiar a lista inteira para uma matriz. Uma segunda lista de elementos escolhidos a dedo provavelmente terá menos itens.
Ahmad Mageed 17/10/09
20

O uso de .ToList () fará uma cópia da sua lista, conforme explicado nesta pergunta: ToList () - Cria uma nova lista?

Usando ToList (), você pode remover da sua lista original, porque na verdade está iterando sobre uma cópia.

foreach (var item in listTracked.ToList()) {    

        if (DetermineIfRequiresRemoval(item)) {
            listTracked.Remove(item)
        }

     }
StuartQ
fonte
1
Mas, do ponto de vista do desempenho, você está copiando sua lista, o que pode levar algum tempo. Boa e maneira fácil de fazê-lo, mas com um não tão bom desempenho
Florian K
12

Se a função que determina quais itens excluir não tem efeitos colaterais e não altera o item (é uma função pura), uma solução simples e eficiente (tempo linear) é:

list.RemoveAll(condition);

Se houver efeitos colaterais, eu usaria algo como:

var toRemove = new HashSet<T>();
foreach(var item in items)
{
     ...
     if(condition)
          toRemove.Add(item);
}
items.RemoveAll(toRemove.Contains);

Ainda é um tempo linear, assumindo que o hash seja bom. Mas tem um aumento no uso de memória devido ao hashset.

Finalmente, se sua lista é apenas um em IList<T>vez de um List<T>, sugiro minha resposta para Como posso fazer esse iterador especial para cada um? . Isso terá um tempo de execução linear, considerando as implementações típicas de IList<T>, em comparação com o tempo de execução quadrático de muitas outras respostas.

CodesInChaos
fonte
11

Como qualquer remoção é feita sob uma condição, você pode usar

list.RemoveAll(item => item.Value == someValue);
Ahmad
fonte
Essa é a melhor solução se o processamento não alterar o item e não tiver efeitos colaterais.
CodesInChaos
9
List<T> TheList = new List<T>();

TheList.FindAll(element => element.Satisfies(Condition)).ForEach(element => TheList.Remove(element));
Mauricio Ramalho
fonte
8

Você não pode usar o foreach, mas pode iterar para frente e gerenciar sua variável de índice de loop ao remover um item, assim:

for (int i = 0; i < elements.Count; i++)
{
    if (<condition>)
    {
        // Decrement the loop counter to iterate this index again, since later elements will get moved down during the remove operation.
        elements.RemoveAt(i--);
    }
}

Observe que, em geral, todas essas técnicas dependem do comportamento da coleção que está sendo iterada. A técnica mostrada aqui funcionará com a lista padrão (T). (É bem possível escrever sua própria classe de coleção e iterator que faz permitir a remoção item durante um loop foreach.)

yoyo
fonte
6

O uso Removeou RemoveAtem uma lista durante a iteração sobre essa lista foi intencionalmente difícil, porque quase sempre é a coisa errada a se fazer . Você pode fazê-lo funcionar com algum truque inteligente, mas seria extremamente lento. Toda vez que você ligar, Removeele deve percorrer toda a lista para encontrar o elemento que você deseja remover. Toda vez que você ligar, RemoveAtele deve mover os elementos subsequentes 1 posição para a esquerda. Como tal, qualquer solução que use Removeou RemoveAt, exigiria tempo quadrático, O (n²) .

Use RemoveAllse puder. Caso contrário, o padrão a seguir filtrará a lista no local em tempo linear, O (n) .

// Create a list to be filtered
IList<int> elements = new List<int>(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
// Filter the list
int kept = 0;
for (int i = 0; i < elements.Count; i++) {
    // Test whether this is an element that we want to keep.
    if (elements[i] % 3 > 0) {
        // Add it to the list of kept elements.
        elements[kept] = elements[i];
        kept++;
    }
}
// Unfortunately IList has no Resize method. So instead we
// remove the last element of the list until: elements.Count == kept.
while (kept < elements.Count) elements.RemoveAt(elements.Count-1);
bcmpinc
fonte
3

Eu gostaria que o "padrão" fosse algo assim:

foreach( thing in thingpile )
{
    if( /* condition#1 */ )
    {
        foreach.markfordeleting( thing );
    }
    elseif( /* condition#2 */ )
    {
        foreach.markforkeeping( thing );
    }
} 
foreachcompleted
{
    // then the programmer's choices would be:

    // delete everything that was marked for deleting
    foreach.deletenow(thingpile); 

    // ...or... keep only things that were marked for keeping
    foreach.keepnow(thingpile);

    // ...or even... make a new list of the unmarked items
    others = foreach.unmarked(thingpile);   
}

Isso alinharia o código com o processo que ocorre no cérebro do programador.

garante
fonte
1
Bastante fácil. Basta criar uma matriz de sinalizador booleano (use um tipo de 3 estados, por exemplo Nullable<bool>, se você deseja permitir não marcado), depois use-o após o foreach para remover / manter itens.
precisa
3

Assumindo que predicado é uma propriedade booleana de um elemento, que, se for verdadeiro, o elemento deve ser removido:

        int i = 0;
        while (i < list.Count())
        {
            if (list[i].predicate == true)
            {
                list.RemoveAt(i);
                continue;
            }
            i++;
        }
roeesha
fonte
Dei um voto positivo, pois às vezes poderia ser mais eficiente percorrer a lista em ordem (não na ordem inversa). Talvez você possa parar ao encontrar o primeiro item a não remover porque a lista está ordenada. (Imagine um 'quebrar' onde a i ++ é neste exemplo.
FrankKrumnow
3

Eu reatribuiria a lista a partir de uma consulta LINQ que filtrasse os elementos que você não queria manter.

list = list.Where(item => ...).ToList();

A menos que a lista seja muito grande, não deve haver problemas significativos de desempenho ao fazer isso.

Martin Liversage
fonte
3

A melhor maneira de remover itens de uma lista durante a iteração é usando RemoveAll(). Mas a principal preocupação escrita pelas pessoas é que elas precisam fazer algumas coisas complexas dentro do loop e / ou ter casos complexos de comparação.

A solução ainda é usar, RemoveAll()mas use esta notação:

var list = new List<int>(Enumerable.Range(1, 10));
list.RemoveAll(item => 
{
    // Do some complex operations here
    // Or even some operations on the items
    SomeFunction(item);
    // In the end return true if the item is to be removed. False otherwise
    return item > 5;
});
Hüseyin Yağlı
fonte
2
foreach(var item in list.ToList())

{

if(item.Delete) list.Remove(item);

}

Basta criar uma lista totalmente nova a partir da primeira. Digo "Fácil" ao invés de "Certo", pois a criação de uma lista totalmente nova provavelmente tem um prêmio de desempenho em relação ao método anterior (não me importei com nenhum benchmarking). Geralmente prefiro esse padrão, mas também pode ser útil para superar Limitações do Linq-To-Entities.

for(i = list.Count()-1;i>=0;i--)

{

item=list[i];

if (item.Delete) list.Remove(item);

}

Dessa maneira, percorre a lista para trás com um loop For simples e antigo. Fazer isso adiante pode ser problemático se o tamanho da coleção for alterado, mas sempre deve ser seguro.

Lijo
fonte
1

Copie a lista que você está iterando. Em seguida, retire da cópia e interaja com o original. Retroceder é confuso e não funciona bem ao fazer um loop paralelo.

var ids = new List<int> { 1, 2, 3, 4 };
var iterableIds = ids.ToList();

Parallel.ForEach(iterableIds, id =>
{
    ids.Remove(id);
});
Timothy Gonzalez
fonte
1

Eu faria assim

using System.IO;
using System;
using System.Collections.Generic;

class Author
    {
        public string Firstname;
        public string Lastname;
        public int no;
    }

class Program
{
    private static bool isEven(int i) 
    { 
        return ((i % 2) == 0); 
    } 

    static void Main()
    {    
        var authorsList = new List<Author>()
        {
            new Author{ Firstname = "Bob", Lastname = "Smith", no = 2 },
            new Author{ Firstname = "Fred", Lastname = "Jones", no = 3 },
            new Author{ Firstname = "Brian", Lastname = "Brains", no = 4 },
            new Author{ Firstname = "Billy", Lastname = "TheKid", no = 1 }
        };

        authorsList.RemoveAll(item => isEven(item.no));

        foreach(var auth in authorsList)
        {
            Console.WriteLine(auth.Firstname + " " + auth.Lastname);
        }
    }
}

RESULTADO

Fred Jones
Billy TheKid
Gambitier
fonte
0

Eu me encontrei em uma situação semelhante, onde eu tinha que remover todos os n º elemento em uma determinada List<T>.

for (int i = 0, j = 0, n = 3; i < list.Count; i++)
{
    if ((j + 1) % n == 0) //Check current iteration is at the nth interval
    {
        list.RemoveAt(i);
        j++; //This extra addition is necessary. Without it j will wrap
             //down to zero, which will throw off our index.
    }
    j++; //This will always advance the j counter
}
Paul Nelson Baker
fonte
0

O custo de remover um item da lista é proporcional ao número de itens após o item a ser removido. No caso em que a primeira metade dos itens se qualifica para remoção, qualquer abordagem baseada na remoção de itens individualmente terá que executar cerca de N * N / 4 operações de cópia de itens, que podem ficar muito caras se a lista for grande .

Uma abordagem mais rápida é percorrer a lista para encontrar o primeiro item a ser removido (se houver) e, a partir desse momento, copiar cada item que deve ser mantido no local a que pertence. Uma vez feito isso, se os itens R devem ser retidos, os primeiros itens R na lista serão esses itens R e todos os itens que requerem exclusão estarão no final. Se esses itens forem excluídos na ordem inversa, o sistema não precisará copiar nenhum deles; portanto, se a lista tiver N itens dos quais itens R, incluindo todos os primeiros F, foram retidos, será necessário copie itens de RF e reduza a lista por um item NR vezes. Todo o tempo linear.

supercat
fonte
0

Minha abordagem é que primeiro crie uma lista de índices, que devem ser excluídos. Depois, percorro os índices e removo os itens da lista inicial. É assim:

var messageList = ...;
// Restrict your list to certain criteria
var customMessageList = messageList.FindAll(m => m.UserId == someId);

if (customMessageList != null && customMessageList.Count > 0)
{
    // Create list with positions in origin list
    List<int> positionList = new List<int>();
    foreach (var message in customMessageList)
    {
        var position = messageList.FindIndex(m => m.MessageId == message.MessageId);
        if (position != -1)
            positionList.Add(position);
    }
    // To be able to remove the items in the origin list, we do it backwards
    // so that the order of indices stays the same
    positionList = positionList.OrderByDescending(p => p).ToList();
    foreach (var position in positionList)
    {
        messageList.RemoveAt(position);
    }
}
teste
fonte
0

Em C #, uma maneira fácil é marcar as que você deseja excluir e criar uma nova lista para repetir ...

foreach(var item in list.ToList()){if(item.Delete) list.Remove(item);}  

ou ainda mais simples usar linq ....

list.RemoveAll(p=>p.Delete);

mas vale a pena considerar se outras tarefas ou threads terão acesso à mesma lista ao mesmo tempo em que você estiver removendo e, talvez, use uma ConcurrentList.

andrew pate
fonte
0

Rastreie os elementos a serem removidos com uma propriedade e remova todos eles após o processo.

using System.Linq;

List<MyProperty> _Group = new List<MyProperty>();
// ... add elements

bool cond = true;
foreach (MyProperty currObj in _Group)
{
    if (cond) 
    {
        // SET - element can be deleted
        currObj.REMOVE_ME = true;
    }
}
// RESET
_Group.RemoveAll(r => r.REMOVE_ME);
Roberto Mutti
fonte
0

Só queria adicionar meus 2 centavos a isso, caso isso ajude alguém, eu tive um problema semelhante, mas precisava remover vários elementos de uma lista de matriz enquanto ela estava sendo repetida. a resposta mais votada mais alta fez por mim a maior parte do tempo, até que encontrei erros e percebi que o índice era maior do que o tamanho da lista de matrizes em alguns casos porque vários elementos estavam sendo removidos, mas o índice do loop não foi mantido. faixa disso. Corrigi isso com uma verificação simples:

ArrayList place_holder = new ArrayList();
place_holder.Add("1");
place_holder.Add("2");
place_holder.Add("3");
place_holder.Add("4");

for(int i = place_holder.Count-1; i>= 0; i--){
    if(i>= place_holder.Count){
        i = place_holder.Count-1; 
    }

// some method that removes multiple elements here
}
Sasha1296
fonte
-4
myList.RemoveAt(i--);

simples;
SW
fonte
1
O que simples;faz aqui?
Denny
6
é um delegado .. ele diminui a minha resposta toda vez que executa #
SW
SW ROFL! Tem que amar o seu comentário.
Erick Brown