CodeGym /Corsi /C# SELF /Interfacce nella libreria standard .NET

Interfacce nella libreria standard .NET

C# SELF
Livello 24 , Lezione 3
Disponibile

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
IEnumerable
,
IEnumerator
,
IList
,
ICollection
,
IDictionary
Enumerazione, modifica, accesso tramite indice, lavoro con coppie chiave-valore
Gestione risorse
IDisposable
Rilascio delle risorse (file, connessioni, stream)
Confronto
IComparable
,
IComparer
,
IEquatable
Ordinamento, confronto, unicità degli oggetti
Serializzazione
ISerializable
Trasformare oggetti in stream di byte (e viceversa)
LINQ e query
IQueryable
,
IQueryProvider
Supporto per query complesse (ad esempio verso DB)
Asincronia
IAsyncEnumerable
,
IAsyncDisposable
Enumerazione asincrona e pulizia delle risorse
Eventi e notifiche
INotifyPropertyChanged
,
INotifyCollectionChanged
Reazione ai cambiamenti di proprietà/collezioni
Data e ora
IFormattable
Formattazione custom delle stringhe
Strutture dati
IStructuralComparable
,
IStructuralEquatable
Confronto profondo di collezioni e tuple
Stream/input-output
IStream
,
IAsyncDisposable
,
IObserver
,
IObservable
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.

2
Compito
C# SELF, livello 24, lezione 3
Bloccato
Verifica dell'interfaccia IEnumerable
Verifica dell'interfaccia IEnumerable
2
Compito
C# SELF, livello 24, lezione 3
Bloccato
Ordinamento con l'interfaccia IComparable
Ordinamento con l'interfaccia IComparable
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION