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”:
- Prima scorri la collezione e raccogli tutti gli elementi da togliere in una lista a parte.
- 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.
GO TO FULL VERSION