1. Perché le collezioni hanno bisogno delle interfacce?
Diciamolo chiaro: finché scrivevamo codice con un tipo concreto (List<int>, Dictionary<string, string>), tutto era semplice e comprensibile. Ma immagina che improvvisamente decidi di sostituire List<T> con HashSet<T>, perché ti servono solo valori unici. Oppure vuoi creare un metodo che funzioni sia con uno che con l'altro tipo di collezione. Che si fa?
Serve un unico punto di ingresso — ti serve una "presa universale". In C# questa presa sono le interfacce. Definiscono un set di regole (metodi, proprietà) che una collezione deve avere. Tutto ciò che implementa l'interfaccia — supporta le operazioni indicate.
È come il caricabatterie del telefono: se hai una presa USB-C standard, puoi caricare sia il telefono, che un aspirapolvere moderno, e pure uno spazzolino elettrico (l'importante è non confonderli al buio!).
Esempio pratico
Supponiamo che vuoi scrivere un metodo che calcola la somma di tutti i numeri in qualsiasi collezione di numeri. Ecco come sarebbe senza interfacce:
// Questo metodo funziona solo con array:
static int SommaArray(int[] array)
{
int somma = 0;
for (int i = 0; i < array.Length; i++)
{
somma += array[i];
}
return somma;
}
// E questo - solo con List<int>:
static int SommaLista(List<int> lista)
{
int somma = 0;
for (int i = 0; i < lista.Count; i++)
somma += lista[i];
return somma;
}
Scomodo! Il codice è praticamente uguale. Con l'interfaccia IEnumerable<int> puoi farlo universale:
static int SommaTutto(IEnumerable<int> collezione)
{
int somma = 0;
foreach (var numero in collezione)
somma += numero;
return somma;
}
E ora questo metodo accetta qualsiasi tipo di collezione che implementa IEnumerable<int>: array, lista, set e così via.
2. Le principali interfacce delle collezioni
In .NET per le collezioni ci sono alcune interfacce chiave. Vediamole brevemente — nei prossimi incontri vedrai i dettagli di ciascuna.
| Interfaccia | A cosa serve | Esempi di collezioni che la implementano |
|---|---|---|
|
Enumerazione degli elementi (foreach) | Tutte: , , array |
|
Set modificabile (Add, Remove, Count) | , , |
|
Accesso per indice, modifica | , array |
|
Lavoro con coppie "chiave-valore" | , |
|
Set (elementi unici) | , |
P.S. Per semplicità per ora ignoriamo i "diversi livelli" di ereditarietà tra queste interfacce. Chi eredita da chi — ne parleremo più avanti.
Visualizzazione: albero delle interfacce delle collezioni
IEnumerable<T>
^
|
ICollection<T>
^ ^
| |
IList<T> ISet<T>
| |
List<T> HashSet<T>
Per i dizionari:
IEnumerable<KeyValuePair<K,V>>
^
|
ICollection<KeyValuePair<K,V>>
^
|
IDictionary<K,V>
|
Dictionary<K,V>
3. Perché le interfacce servono nella vita reale?
Flessibilità
Quando il tuo metodo o classe lavora non con un tipo concreto (List<int>), ma con un'interfaccia (IEnumerable<int>), puoi passargli qualsiasi collezione che implementa quell'interfaccia. Alla fine il programma diventa flessibile ed estendibile. (Invece di "Solo iPhone" — ora va bene qualsiasi telefono con Bluetooth.)
static void StampaTutto(IEnumerable<string> collezione)
{
foreach (var item in collezione)
Console.WriteLine(item);
}
Ora puoi stampare elementi di qualsiasi lista, array, set, anche il risultato di una query LINQ!
Unificazione
Se implementi il tuo tipo di collezione (magari una collezione super specifica per qualche compito), implementa le interfacce giuste — e la tua collezione funzionerà con codice di altri che non la conoscono. Ecco perché in .NET si dice spesso: "programma a livello di interfacce, non di implementazioni concrete".
Compatibilità con gli strumenti standard
Grazie al supporto delle interfacce standard, le tue collezioni possono lavorare con LINQ, ordinamento, serializzazione e altre feature di .NET.
4. List<T> — è sia lista, che collezione, che enumerazione
Due parole sull'enumerazione
IEnumerable<T> — è l'interfaccia più importante e di base delle collezioni in .NET. Definisce un solo metodo — GetEnumerator(), che restituisce un oggetto enumeratore per iterare la collezione. Proprio grazie a lui puoi scrivere:
foreach (var nome in nomi)
{
Console.WriteLine(nome);
}
Dove nomi — può essere lista, set, anche array. foreach — è una sintassi furba che sotto il cofano usa il metodo GetEnumerator().
L'interfaccia IEnumerable (senza <T>) — è più vecchia, non generica. Ormai usa quasi sempre la versione generica.
Sintassi della dichiarazione della classe List
Vediamo la dichiarazione della classe List<T>, se sbirciamo un po' dietro le quinte:
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, ...
Questo significa che l'oggetto List<T> puoi usarlo come:
- Lista con accesso casuale tramite indice (IList<T>)
- Collezione dove puoi aggiungere/rimuovere elementi (ICollection<T>)
- Semplice insieme di elementi su cui puoi iterare (IEnumerable<T>)
Esempio:
List<string> nomi = new List<string> { "Anna", "Boris", "Vasya" };
// Come IEnumerable - puoi iterare con il ciclo
IEnumerable<string> comeEnumerable = nomi;
foreach (var n in comeEnumerable) Console.WriteLine(n);
// Come ICollection - puoi sapere il Count:
ICollection<string> comeCollezione = nomi;
int quanti = comeCollezione.Count;
// Come IList - puoi accedere per indice:
IList<string> comeLista = nomi;
string primoElemento = comeLista[0];
5. Esempio di applicazione
Supponiamo di avere una semplice app per gestire utenti e i loro ruoli (abbiamo iniziato a farla nelle lezioni precedenti):
// Classe utente
public class Utente
{
public string Nome { get; set; }
public int Eta { get; set; }
}
Possiamo salvare gli utenti in qualsiasi collezione che implementa IEnumerable<Utente>:
List<Utente> listaUtenti = new List<Utente> { new Utente { Nome = "Anna", Eta = 20 } };
HashSet<Utente> setUtenti = new HashSet<Utente> { new Utente { Nome = "Boris", Eta = 25 } };
// Metodo che stampa i nomi di tutti gli utenti
static void StampaNomiUtenti(IEnumerable<Utente> utenti)
{
foreach (var utente in utenti)
Console.WriteLine(utente.Nome);
}
// Usiamo con diversi tipi di collezioni:
StampaNomiUtenti(listaUtenti);
StampaNomiUtenti(setUtenti);
Ora l'app è molto più flessibile — senza magie o overload personalizzati!
6. Errori tipici e trappole
Uno degli errori più comuni — usare tipi troppo concreti quando basta un'interfaccia:
// Male (troppo legato all'implementazione)
void FaiQualcosaConLista(List<int> numeri) { ... }
// Meglio (il metodo funziona con qualsiasi collezione)
void FaiQualcosaConNumeri(IEnumerable<int> numeri) { ... }
Un'altra trappola — dimenticare che l'interfaccia definisce solo le operazioni che sono esplicitamente descritte. Per esempio, in IEnumerable<T> non puoi sapere quanti elementi ci sono (Count), per questo devi usare ICollection<T> — oppure contarli a mano.
GO TO FULL VERSION