CodeGym /Corsi /C# SELF /Contratto di enumerabilità:

Contratto di enumerabilità: IEnumerable<T>

C# SELF
Livello 28 , Lezione 1
Disponibile

1. Introduzione

Come forse hai già notato, le varie collezioni in C# hanno delle cose in comune. Per esempio, quasi tutte possono essere attraversate con il ciclo foreach:


var names = new List<string> { "Anja", "Boris", "Vika" };

// Attenzione — il codice funziona sia con List, che con array, e pure con HashSet!
foreach (var name in names)
{
    Console.WriteLine(name);
}

Dov'è la magia? Tutto sta nel fatto che tutte le collezioni moderne implementano l'interfaccia IEnumerable<T> — una specie di "contratto" che garantisce che la collezione sa restituire gli elementi uno alla volta, in sequenza.

Immagina un'azienda dove per qualsiasi collezione di dipendenti (che sia un reparto, un team di progetto, una lista di partecipanti alla festa aziendale) c'è una regola: se l'oggetto implementa l'interfaccia "IEnumerable", allora puoi sempre scorrere tutti i dipendenti in un certo ordine, non importa "come sono memorizzati", l'importante è che puoi iterarli.

2. Interfaccia IEnumerable<T> — cosa c'è dentro?

Vediamo com'è fatta questa interfaccia in .NET:


public interface IEnumerable<out T>
{
    IEnumerator<T> GetEnumerator();
}

Ha solo un metodoGetEnumerator, che restituisce un oggetto di tipo IEnumerator<T>. Questo oggetto si occupa proprio del processo di "enumerazione": sa qual è l'elemento attuale, come passare al prossimo, quando è finito tutto.

In breve: se una classe (o collezione) implementa IEnumerable<T>, allora puoi scorrerla con il ciclo foreach, prendere gli elementi uno dopo l'altro — e non importa come sono implementati dentro: può essere una lista, una hash table, o anche su disco fisso.

Mini-schema


Collezione (tipo List<T>)
    |
    v
IEnumerable<T>
    |
    v
IEnumerator<T> (il vero "iteratore" degli elementi)

3. Pratica e universalità del codice

Il grande vantaggio di questo contratto è l'universalità. Se una funzione o un metodo accetta in input un IEnumerable<T>, vuol dire che è compatibile con qualsiasi tipo di collezione — da array e liste a hash set, code e anche collezioni custom!

Per esempio, vediamo una funzione che calcola la somma degli elementi:


// Puoi sommare qualsiasi insieme di int che supporta IEnumerable<int>
int Somma(IEnumerable<int> numeri)
{
    int risultato = 0;
    foreach (var n in numeri)
        risultato += n;
    return risultato;
}

// Funziona con List<int>
var lista = new List<int> { 1, 2, 3 };
Console.WriteLine(Somma(lista));

// Funziona con array!
int[] array = { 4, 5, 6 };
Console.WriteLine(Somma(array));

// Funziona anche con il risultato di un metodo che filtra elementi
Console.WriteLine(Somma(lista.Where(x => x % 2 == 0))); // Usiamo LINQ

Esempio reale: metodi universali

Immagina che devi scrivere un'utility per trovare la parola più lunga in qualsiasi collezione di stringhe. Grazie a IEnumerable<string>, puoi farlo per qualsiasi sorgente — che sia un array, una lista, il risultato di metodi di filtro ecc.:


string TrovaPiuLunga(IEnumerable<string> parole)
{
    string piuLunga = "";
    foreach (var parola in parole)
        if (parola.Length > piuLunga.Length)
            piuLunga = parola;
    return piuLunga;
}

Puoi usarlo con qualsiasi insieme adatto.

4. Perché il ciclo foreach funziona con IEnumerable<T>?

Domanda classica: perché il ciclo foreach "capisce" tutte le collezioni? Semplice: il compilatore C# cerca nel classe il metodo GetEnumerator e si aspetta di ricevere un oggetto con i metodi MoveNext() e la proprietà Current. Questo è lo standard — IEnumerator<T>.

Grazie a questo approccio, puoi anche scrivere una tua classe che "restituisce" elementi uno alla volta (tipo un generatore della sequenza di Fibonacci), e se implementa IEnumerable<int>, puoi usarla con foreach come una lista normale.

5. Cos'è un Enumerator e come funziona?

Dentro ogni collezione che implementa IEnumerable<T> c'è uno speciale “iteratore” — Enumerator (o più precisamente: un oggetto che implementa l'interfaccia IEnumerator<T>). È lui che "porta" gli elementi della collezione uno alla volta quando scrivi il ciclo foreach.

Interfaccia IEnumerator<T>

Ecco cosa sa fare un Enumerator standard:


public interface IEnumerator<T> : IDisposable
{
    T Current { get; }         // Elemento attuale
    bool MoveNext();           // Passa all'elemento successivo
    void Reset();              // Torna all'inizio (usato raramente)
}

Come funziona dentro?

  • MoveNext() — sposta il "puntatore" al prossimo elemento e restituisce true se c'è un elemento. Se non ci sono più elementi — restituisce false.
  • Current — restituisce l'elemento attuale (quello su cui è puntato l'enumerator).
  • Reset() — riporta l'Enumerator all'inizio (ma praticamente non si usa mai).
  • Dispose() — libera le risorse (serve per collezioni che lavorano con file o rete).

Esempio: come funziona foreach "sotto il cofano"

Quando scrivi:


var numeri = new List<int> { 1, 2, 3 };
foreach (var n in numeri)
    Console.WriteLine(n);

In realtà il compilatore lo trasforma più o meno così:


var numeri = new List<int> { 1, 2, 3 };

// Prendiamo l'"iteratore"
var enumerator = numeri.GetEnumerator();
while (enumerator.MoveNext())
{
    var n = enumerator.Current;
    Console.WriteLine(n);
}
// Il compilatore chiama automaticamente Dispose() nel blocco using (se enumerator implementa IDisposable)

Importante: se la collezione lavora con risorse esterne (tipo file, database), l'Enumerator può liberarle automaticamente quando finisce l'iterazione.

Schema visivo


Inizio iterazione -> GetEnumerator() -> Enumerator
         |
         v
  MoveNext() -> Current
         |
         v
  MoveNext() -> Current
         |
        ...
         |
         v
  MoveNext() == false -> fine iterazione

6. IEnumerable e array, liste, set: chi è parente di chi

Vediamo quali container standard di .NET implementano questa interfaccia:

Tipo di collezione Implementa IEnumerable<T>? Si può scorrere con foreach?
Array (
int[]
)
List<T>
Dictionary<TKey, V>
✅ (per coppie, chiavi, valori)
HashSet<T>
Queue<T>
Stack<T>

Anche la stringa (string) implementa il normale IEnumerable, quindi puoi scorrere ogni carattere della stringa allo stesso modo.

7. Implementare un proprio Enumerable

Sfida interessante: proviamo a implementare una collezione minimale che contiene i numeri pari da 0 a N e implementa IEnumerable<int>. Così possiamo scorrerla con il ciclo e usarla con LINQ.


// Classe-collezione che si può "iterare"
class NumeriPari : IEnumerable<int>
{
    private int max;

    public NumeriPari(int max)
    {
        this.max = max;
    }

    public IEnumerator<int> GetEnumerator()
    {
        for (int i = 0; i <= max; i += 2)
            yield return i; // Magia speciale per implementare l'enumerator
    }

    // Implementazione esplicita dell'interfaccia IEnumerable non generica per compatibilità
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

// Uso:
var pari = new NumeriPari(10);
foreach(var e in pari)
    Console.Write($"{e} "); // 0 2 4 6 8 10

Nota la cosa chiave: se la tua classe implementa IEnumerable<T> — la rendi automaticamente compatibile con la maggior parte degli strumenti .NET: LINQ, foreach, metodi che accettano enumerable.

8. Errori tipici e dettagli

A volte chi è alle prime armi pensa che IEnumerable<T> sia una collezione separata che ha degli elementi. In realtà — è solo una "promessa" che se inizi a iterare, ti verranno "dati" gli elementi uno alla volta.

Se ti serve accesso casuale tramite indice (myList[5]), usare metodi tipo Add o Remove — l'interfaccia IEnumerable<T> non ti aiuta. Serve solo per iterazione sequenziale!

Errore: provare a modificare la collezione durante l'iterazione. Per esempio:


foreach (var item in myList)
{
    if (item < 0)
        myList.Remove(item); // PERICOLOSO! InvalidOperationException
}

Meglio prima creare una lista separata per la rimozione, oppure usare metodi che creano una nuova collezione:


// Modo sicuro — creiamo una nuova collezione a mano
var nuovaLista = new List<int>();
foreach (var item in myList)
{
    if (item >= 0)
        nuovaLista.Add(item);
}
myList = nuovaLista;

// Oppure rimuoviamo per indici in ordine inverso
for (int i = myList.Count - 1; i >= 0; i--)
{
    if (myList[i] < 0)
        myList.RemoveAt(i);
}
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION