1. Classificazione delle interfacce chiave
Se pensi che le interfacce siano solo un giocattolo per architetti e amanti del "clean code", e che nella vita reale si possa farne a meno, lascia che ti sorprenda: qualsiasi progetto serio in C# si basa pesantemente sulle interfacce. Perché? Perché praticamente ogni parte della libreria standard .NET è costruita sulle interfacce! Senza di loro non puoi lavorare con le collezioni, leggere file, o anche solo filtrare collezioni con LINQ.
Le interfacce sono il fondamento del polimorfismo e dell'estendibilità in .NET, e sono proprio loro a determinare come tutti i "mattoncini" del framework si incastrano tra loro.
La libreria standard .NET è letteralmente piena zeppa di interfacce. Per non affogare in questo mare, ti propongo questa classificazione (ovviamente non è completa — non basterebbe una vita per coprire tutto!):
| Categoria | Interfacce | A cosa servono? |
|---|---|---|
| Collezioni | , , , , |
Enumerazione, modifica, accesso tramite indice, lavoro con coppie chiave-valore |
| Gestione risorse | |
Rilascio delle risorse (file, connessioni, stream) |
| Confronto | , , |
Ordinamento, confronto, unicità degli oggetti |
| Serializzazione | |
Trasformare oggetti in stream di byte (e viceversa) |
| LINQ e query | , |
Supporto per query complesse (ad esempio verso DB) |
| Asincronia | , |
Enumerazione asincrona e pulizia delle risorse |
| Eventi e notifiche | , |
Reazione ai cambiamenti di proprietà/collezioni |
| Data e ora | |
Formattazione custom delle stringhe |
| Strutture dati | , |
Confronto profondo di collezioni e tuple |
| Stream/input-output | , , , |
Lavoro con stream, notifiche push/pull |
Attenzione! Molte delle interfacce sopra hanno parametri di tipo: List<string>. Di solito si usano nelle collezioni e per le interfacce delle collezioni Int[] → List<int>. Le vedremo meglio tra qualche livello, quando parleremo di collezioni.
2. Interfacce delle collezioni
IEnumerable e IEnumerator — il tuo pass per foreach
Praticamente ogni collezione in .NET implementa l'interfaccia IEnumerable o la sua versione generica IEnumerable<T>. È proprio questa interfaccia che ti permette di usare la magia di foreach:
List<int> numeri = new List<int> { 1, 2, 3 };
foreach (int n in numeri) // funziona perché List<int> implementa IEnumerable<int>
{
Console.WriteLine(n);
}
Ecco come appare questa interfaccia nella versione minimal:
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
IEnumerator è l'interfaccia del vero "enumeratore", a cui è affidato il compito di scorrere la tua collezione:
public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}
Nella pratica molto spesso passerai parametri e restituirai valori di tipo IEnumerable<T>. Ad esempio, un metodo che restituisce tutti i numeri pari da un array:
public IEnumerable<int> OttieniNumeriPari(int[] array)
{
foreach (var x in array)
if (x % 2 == 0) yield return x;
}
Sì, la parola chiave yield fa la magia — implementa l'interfaccia per te, ma ne parleremo meglio più avanti.
ICollection e IList — collezioni con accesso tramite indice e modifica
Se ti serve non solo enumerare, ma anche aggiungere/rimuovere elementi o accedervi tramite indice, servono interfacce più specializzate:
- ICollection<T> — aggiunge i metodi Add, Remove e la proprietà Count.
- IList<T> — estende le operazioni possibili, incluso l'accesso tramite indice (this[int index]).
public void StampaPrimoElemento(IList<string> lista)
{
if (lista.Count > 0)
Console.WriteLine(lista[0]);
}
List<T> implementa sia IEnumerable<T>, sia ICollection<T>, sia IList<T>. È super comodo — puoi usarla come semplice lista enumerabile o sfruttare tutti i suoi metodi.
IDictionary<TKey, TValue> — coppie "chiave-valore"
Se ti piace lavorare con i dizionari (e succede spesso!), usa l'interfaccia IDictionary<TKey, TValue>. Ti garantisce che puoi ottenere un valore tramite chiave e viceversa.
public void StampaTutteLeCoppie(IDictionary<string, int> eta)
{
foreach (var coppia in eta)
Console.WriteLine($"{coppia.Key}: {coppia.Value}");
}
Qui eta può essere qualsiasi cosa, sia Dictionary<string, int> che SortedDictionary<string, int> — l'importante è che implementino questa interfaccia!
3. Interfaccia IDisposable: gestione corretta delle risorse
Tutto l'input-output, il lavoro con file, connessioni di rete, database in .NET ruota attorno all'interfaccia IDisposable. Questa interfaccia definisce un contratto fondamentale: se un oggetto ha risorse non gestite, va "ripulito" dopo l'uso. Sì, è proprio lei che ha reso possibile la sintassi using:
using (StreamReader lettore = new StreamReader("file.txt"))
{
// Lavori col file, e dopo il using il file viene chiuso automaticamente!
string riga = lettore.ReadLine();
}
Com'è fatta l'interfaccia? Semplicissima:
public interface IDisposable
{
void Dispose();
}
Ma ogni libreria "seria" la implementa! Per saperne di più sulle buone pratiche con questa interfaccia leggi la documentazione ufficiale.
4. Interfacce per confronto e unicità
IComparable<T> e IComparer<T>
Se vuoi che la tua collezione di oggetti possa essere ordinata, gli oggetti devono sapersi confrontare tra loro. Per questo esiste l'interfaccia IComparable<T>:
public class Studente : IComparable<Studente>
{
public string Nome { get; set; }
public int Punteggio { get; set; }
public int CompareTo(Studente? altro)
{
// Ordinamento decrescente per punteggio
if (altro == null) return 1;
return altro.Punteggio.CompareTo(this.Punteggio);
}
}
// Ora puoi ordinare gli studenti:
var studenti = new List<Studente> { ... };
studenti.Sort();
Maggiori info nella documentazione Microsoft.
IComparer<T> ti permette di definire il confronto fuori dalla classe — ad esempio, vuoi ordinare gli studenti a volte per nome, a volte per punteggio:
public class ComparatoreNome : IComparer<Studente>
{
public int Compare(Studente? x, Studente? y)
{
return string.Compare(x?.Nome, y?.Nome);
}
}
IEquatable<T> — confronto di uguaglianza
Vuoi che il tuo oggetto sia usato in un HashSet<T> (cioè sia "unico" per le collezioni)? Implementa IEquatable<T>:
public class Persona : IEquatable<Persona>
{
public string Nome { get; set; }
public bool Equals(Persona? altro)
{
if (altro == null) return false;
return this.Nome == altro.Nome;
}
}
5. Interfacce per eventi e notifiche
Ti è mai capitato di voler che il tuo programma "sapesse" quando una proprietà di un oggetto cambia? Tipo vuoi che l'interfaccia si aggiorni da sola quando l'utente cambia il cognome? Proprio per questo esiste l'interfaccia INotifyPropertyChanged. È super usata nelle app con interfaccia grafica (tipo WPF o Xamarin).
La firma dell'interfaccia è semplice:
public interface INotifyPropertyChanged
{
event PropertyChangedEventHandler? PropertyChanged;
}
Esempio di implementazione:
public class Utente : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private string nome;
public string Nome
{
get => nome;
set
{
if (nome != value)
{
nome = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Nome)));
}
}
}
}
Ora qualsiasi binding dell'interfaccia utente a questo oggetto si aggiornerà automaticamente quando cambia Nome. Se ti interessa — leggi la documentazione ufficiale.
6. Altre interfacce utili
Interfaccia IFormattable: formattazione flessibile
Quando vuoi che il tuo oggetto possa essere stampato in modo bello e flessibile (tipo con diversi numeri di decimali), implementa l'interfaccia IFormattable:
public class Temperatura : IFormattable
{
public double Celsius { get; }
public Temperatura(double celsius) => Celsius = celsius;
public string ToString(string? formato, IFormatProvider? formatProvider)
{
// Non complichiamo, mostriamo solo 'C' o 'F' a seconda del formato
if (formato == "F")
return $"{Celsius * 9 / 5 + 32} F";
return $"{Celsius} C";
}
}
Ora puoi chiamare temp.ToString("F", null) o temp.ToString("C", null). Ecco perché DateTime, double, decimal e altri tipi built-in supportano la formattazione flessibile.
Interfacce per l'asincronia
Con l'arrivo dell'asincronia in C# sono nate nuove interfacce per lavorare nel mondo async. Ad esempio, se il tuo oggetto libera risorse in modo asincrono — implementa IAsyncDisposable:
public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}
E ora invece di using scrivi await using!
Con le enumerazioni asincrone (await foreach) si usa l'interfaccia IAsyncEnumerable<T>, che ti permette di scorrere elementi senza bloccare il thread principale. Si usa, ad esempio, per leggere grandi file a pezzi o per lavorare con stream di dati da internet. Più info al livello 58 :P
Interfacce di serializzazione: ISerializable
Se il tuo compito è trasformare oggetti in stream di byte per salvarli/trasmetterli (tipo via rete), .NET offre l'interfaccia ISerializable. Oggi non si usa spesso, perché ci sono meccanismi più comodi (tipo tramite attributi e serializer già pronti), ma merita una menzione. Ecco la firma:
public interface ISerializable
{
void GetObjectData(SerializationInfo info, StreamingContext context);
}
7. Uso pratico delle interfacce: un'app reale
Supponiamo che tu stia creando un'app console per gestire i libri in biblioteca (dai, qualcuno dovrà pur farlo!). Vuoi poter stampare liste di libri, ordinarli, filtrarli, caricare e salvare dati. Grazie alla conoscenza delle interfacce .NET potrai:
- Restituire diversi tipi di collezioni tramite IEnumerable<Book>, così l'utente non dovrà preoccuparsi se è un array o una lista.
- Aggiungere ordinamento per criteri diversi: implementi IComparable<Book> per ordinare per titolo e un IComparer<Book> separato per autore.
- Gestire correttamente il rilascio delle risorse lavorando con file tramite IDisposable.
- Filtrare i libri per genere o autore usando LINQ, che funziona con tutto ciò che implementa IEnumerable<T>.
- Scalare l'app — ad esempio, sostituire l'archiviazione su file con un database, se le tue classi lavorano tramite interfacce (IBookStorage) e non implementazioni concrete.
8. Errori tipici e particolarità
Uno degli errori più comuni tra i principianti è usare implementazioni concrete ("List") invece delle interfacce ("IEnumerable", "IList") quando dichiari variabili e parametri dei metodi. RICORDA: se "programmi sulle interfacce", il tuo codice sarà flessibile e facilmente estendibile.
A volte devi fare attenzione a quale livello di interfaccia usare — ad esempio, se scrivi un metodo che ha solo bisogno di enumerare, usa IEnumerable<T> e non IList<T>, così non imponi vincoli inutili.
Un'altra sottigliezza: se una classe implementa molte interfacce e tra loro ci sono metodi o proprietà con lo stesso nome, dovrai usare l'implementazione esplicita dell'interfaccia (explicit implementation), altrimenti il compilatore non ci capisce nulla.
GO TO FULL VERSION