CodeGym /Corsi /C# SELF /Rimozione di un elemento da una collezione in un ciclo <...

Rimozione di un elemento da una collezione in un ciclo foreach

C# SELF
Livello 29 , Lezione 4
Disponibile

1. Introduzione

Quasi tutti quelli che iniziano a programmare in C# prima o poi si trovano davanti allo stesso problema: hai una collezione (tipo una lista di oggetti) e devi togliere da lì gli elementi inutili secondo una certa condizione. Sembra facile, e la mano va subito al ciclo foreach, che è il modo più “sicuro” e “amichevole” per scorrere tutto. Ma poi, all’improvviso, salta fuori uno strano errore a runtime che non c’era negli esempi semplici — e il programma si blocca di botto.

Vediamo insieme perché succede, cosa succede “sotto il cofano” delle collezioni e degli iteratori, e come eliminare gli elementi in modo furbo per evitare sorprese e bug.

Perché foreach non va d’accordo con la rimozione degli elementi

Per capire cosa succede, immagina una fila di persone (questa è la nostra collezione). Tu vai lungo la fila e chiedi a ciascuno: "Ti tengo o ti cancello?" Se inizi a cancellare qualcuno mentre stai ancora passando, tutta la fila si sposta, la gente cambia posto, e il tuo piano “la prossima persona è la prossima nella lista” va subito a farsi benedire. Magari qualcuno non lo interroghi mai, o qualcun altro lo becchi due volte.

Esempio in C#:


List<string> names = new List<string> { "Anton", "Boris", "Vika", "Grisha" };

foreach (string name in names)
{
    if (name.StartsWith("V"))
        names.Remove(name); // Boom! InvalidOperationException
}

Quando il programma arriva a "Vika" e decide di eliminarla, l’iteratore interno perde il “contatto con la realtà” — e ti becchi il messaggio:
InvalidOperationException: Collection was modified; enumeration operation may not execute.

Non è un capriccio — è C# che ti protegge da bug difficili da beccare e da danni alla struttura dei dati.

2. Perché un codice così semplice non funziona?

Come funziona tutto dentro?

Quando scrivi un ciclo foreach, il compilatore genera un oggetto speciale — un iteratore (IEnumerator), che tiene traccia della posizione attuale nella collezione. Questo oggetto si ricorda quanti elementi c’erano all’inizio, quale elemento è “attivo” ora, e controlla in modo rigido che la collezione non cambi durante il giro.

Qualsiasi tentativo di togliere o aggiungere un elemento dentro il foreach rompe questo patto. Perché? Se dopo la tua rimozione gli indici si spostano, l’iteratore non riesce più a passare correttamente all’elemento successivo. Qualcuno può essere saltato, qualcun altro contato due volte — insomma, viene fuori un casino. Quindi, al primo cambiamento della collezione, .NET ti lancia subito un errore chiaro.

Cosa succede se rimuovi “di brutto”

Immagina di aver scritto un programma così:


List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
foreach (int x in numbers)
{
    if (x % 2 == 0)
        numbers.Remove(x);
}

Sembra tutto logico: scorri tutti i numeri, togli quelli pari. Ma al secondo giro il programma lancia l’errore — “collezione modificata durante l’enumerazione”.

A volte viene voglia di aggirare l’avviso e provare “a proprio rischio”. Ma anche se non ci fosse l’errore, a seconda della struttura della collezione il risultato sarebbe imprevedibile. Magari salti qualche elemento o non togli tutto quello che volevi.

3. Come si fa allora?

Tecnica n°1: Ciclo for al contrario

Il punto è che quando togli un elemento, quelli dopo si “spostano” a sinistra, e se parti dall’inizio della lista rischi di confondere gli indici e saltare degli elementi. Per evitarlo, conviene partire dalla fine.


List<string> names = new List<string> { "Anton", "Boris", "Vika", "Grisha" };

for (int i = names.Count - 1; i >= 0; i--)
{
    if (names[i].StartsWith("V"))
        names.RemoveAt(i);
}

In questo esempio, dopo ogni rimozione tutti gli elementi dopo quello tolto si spostano, ma gli indici che non hai ancora toccato restano a posto. Così non salti niente.

Tecnica n°2: Filtrare e creare una nuova lista

A volte è più semplice (e spesso più veloce) scorrere la collezione, raccogliere solo gli elementi che vuoi tenere, e sostituire la lista originale con quella nuova.


var names = new List<string> { "Anton", "Boris", "Vika", "Grisha" };
names = names.Where(name => !name.StartsWith("V")).ToList();
// Alla fine restano "Anton" e "Grisha"

Questo metodo va bene quando la collezione non è troppo grande o non è fondamentale mantenere il riferimento originale all’oggetto.

Tecnica n°3: Usare i metodi speciali delle collezioni

Se lavori con un classico List<T>, per rimuovere in base a una condizione c’è un metodo comodissimo:


names.RemoveAll(name => name.StartsWith("V"));

Tutto il processo viene gestito bene dentro, e tu hai un codice corto e chiaro.

Tecnica n°4: Segnare cosa eliminare

Ci sono collezioni che non puoi modificare “al volo” (tipo Dictionary, HashSet, o magari una tua classe custom). In questi casi si usa il metodo “segna per l’eliminazione”:

  1. Prima scorri la collezione e raccogli tutti gli elementi da togliere in una lista a parte.
  2. Poi scorri questa nuova lista e togli gli elementi dalla collezione originale.

Dictionary<int, string> dict = new Dictionary<int, string> { [1] = "uno", [2] = "due", [3] = "tre" };
var toDelete = new List<int>();
foreach (var kvp in dict)
{
    if (kvp.Key % 2 == 0)
        toDelete.Add(kvp.Key);
}
foreach (var key in toDelete)
    dict.Remove(key);

4. Qualche dettaglio utile

Errori e miti dei principianti

Uno degli errori più comuni è aspettarsi che togliere un elemento dalla collezione durante il giro funzioni “in qualche modo”, perché in altri linguaggi (tipo Python) spesso si può fare. Ma in C# è assolutamente vietato proprio per la tua sicurezza: meglio un errore chiaro che un bug silenzioso e subdolo che nessuno riesce a riprodurre.

Un altro errore tipico — usare un ciclo for con l’indice che cresce invece che decresce. Così, dopo aver tolto un elemento, tutti quelli dopo si “spostano” e rischi di saltare qualcosa. Parti sempre dalla fine se togli per indice.

La morale della storia

Il problema “togliere elementi da una collezione in base a una condizione” lo trovi in metà dei programmi C#, ma farlo direttamente dentro un ciclo foreach non si può — è proprio una scelta dell’architettura del linguaggio, fatta per proteggere i tuoi dati e prevenire errori strani.
Ricordati questa regola e ti risparmi nottate insonni col debugger.

Come fare sempre giusto

  • Non togliere mai elementi da una collezione direttamente in un ciclo foreach. Ti becchi un errore a runtime.
  • Per liste (List<T>) e array usa o il ciclo for dalla fine, o i metodi RemoveAll e la filtrazione con LINQ.
  • Per dizionari, set e altre collezioni complesse — prima raccogli gli elementi da togliere, poi scorri questa lista e li elimini dalla collezione originale.
  • Se non sei sicuro — pensa: come cambia la collezione quando togli qualcosa? Cosa fa l’iteratore? Se hai anche solo un dubbio, il metodo non va bene.
2
Compito
C# SELF, livello 29, lezione 4
Bloccato
Rimozione delle stringhe che iniziano con un carattere specificato
Rimozione delle stringhe che iniziano con un carattere specificato
1
Sondaggio/quiz
Filtraggio degli elementi, livello 29, lezione 4
Non disponibile
Filtraggio degli elementi
Lavorare con le collezioni
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION