I metodi di estensione in C#

I metodi di estensione in C#

Quando abbiamo esplorato LINQ, abbiamo concatenato metodi come Where e Select direttamente sulle collezioni, come se appartenessero al tipo List. In realtà quei metodi non sono definiti nelle collezioni: sono metodi di estensione, un meccanismo elegante che permette di aggiungere nuovi metodi a tipi esistenti senza modificarli né derivarli. In questo articolo capiamo come funzionano, come scriverne di propri e quali sono le buone pratiche per usarli con giudizio.

L'idea di fondo

A volte si vorrebbe aggiungere un metodo a un tipo che non possiamo modificare, perché appartiene alla libreria standard o a una libreria di terze parti. I metodi di estensione risolvono esattamente questo bisogno: permettono di invocare un metodo statico come se fosse un metodo di istanza del tipo, migliorando la leggibilità e la scopribilità del codice.

Definire un metodo di estensione

Un metodo di estensione è un metodo statico, definito in una classe statica, il cui primo parametro è preceduto dalla parola chiave this. Quel primo parametro indica il tipo che viene esteso. Da quel momento, il metodo può essere richiamato con la sintassi a punto sugli oggetti di quel tipo.

// La classe che ospita i metodi di estensione deve essere statica
static class StringExtensions
{
    // this string indica che estendiamo il tipo string
    public static bool IsNullOrBlank(this string? value)
    {
        return string.IsNullOrWhiteSpace(value);
    }

    public static string Truncate(this string value, int maxLength)
    {
        if (value.Length <= maxLength)
        {
            return value;
        }
        return value.Substring(0, maxLength) + "...";
    }
}

Con queste definizioni in scope, i nuovi metodi diventano disponibili su qualsiasi stringa, come se facessero parte del tipo originale:

string title = "Un titolo molto lungo da abbreviare";

// Invocazione con la sintassi a punto, come un metodo normale
string shortTitle = title.Truncate(10); // "Un titolo ..."

bool empty = "   ".IsNullOrBlank(); // true

Come funziona dietro le quinte

I metodi di estensione sono in realtà zucchero sintattico. Il compilatore traduce la chiamata con la sintassi a punto in una normale chiamata al metodo statico, passando l'oggetto come primo argomento. Le due forme seguenti sono quindi perfettamente equivalenti.

string text = "esempio";

// Sintassi come metodo di estensione
string a = text.Truncate(3);

// Chiamata statica equivalente, quella effettivamente generata
string b = StringExtensions.Truncate(text, 3);

LINQ come insieme di metodi di estensione

Ora si comprende meglio la natura di LINQ: tutti i suoi operatori sono metodi di estensione definiti sull'interfaccia IEnumerable<T>. Ecco perché sono disponibili su qualsiasi collezione che implementi quell'interfaccia e perché è necessario importare il namespace System.Linq per usarli. Possiamo scrivere i nostri operatori nello stesso stile.

using System.Collections.Generic;

static class EnumerableExtensions
{
    // Un operatore personalizzato su qualsiasi sequenza di interi
    public static double Median(this IEnumerable<int> source)
    {
        var sorted = source.OrderBy(n => n).ToList();
        int count = sorted.Count;

        if (count == 0)
        {
            throw new InvalidOperationException("La sequenza e vuota");
        }

        int middle = count / 2;
        return count % 2 == 0
            ? (sorted[middle - 1] + sorted[middle]) / 2.0
            : sorted[middle];
    }
}

var numbers = new List<int> { 5, 2, 8, 1, 9 };
Console.WriteLine(numbers.Median()); // Stampa 5

Estendere le interfacce

La possibilità di estendere un'interfaccia è particolarmente potente, perché il metodo diventa disponibile per tutti i tipi che la implementano. Questo consente di aggiungere comportamenti condivisi senza toccare le singole classi, promuovendo il riuso.

static class CollectionExtensions
{
    // Disponibile su qualsiasi ICollection: List, array e altri
    public static bool IsEmpty<T>(this ICollection<T> collection)
    {
        return collection.Count == 0;
    }
}

Precedenza e risoluzione

È importante sapere che i metodi di istanza reali hanno sempre la precedenza sui metodi di estensione. Se un tipo definisce già un metodo con la stessa firma, verrà chiamato quello dell'istanza e il metodo di estensione sarà ignorato. I metodi di estensione, inoltre, sono visibili solo se il loro namespace è importato, il che consente di organizzarli e attivarli selettivamente.

Buone pratiche

I metodi di estensione sono comodi ma non vanno abusati. Alcuni criteri per usarli bene:

  • Usarli quando non si può o non si dovrebbe modificare il tipo originale, ad esempio per tipi di librerie esterne.
  • Mantenerli semplici e privi di effetti collaterali sorprendenti: chi legge si aspetta un comportamento coerente con il tipo esteso.
  • Raggrupparli in classi statiche dal nome chiaro e in namespace ben organizzati, così da controllarne la visibilità.
  • Preferire un metodo di istanza quando si ha il controllo sul tipo e il comportamento gli appartiene naturalmente.

Conclusione

Abbiamo scoperto i metodi di estensione come tecnica per arricchire tipi esistenti con nuovi comportamenti, definendoli come metodi statici con il primo parametro marcato this. Abbiamo compreso come il compilatore li traduca, perché LINQ ne sia il più celebre esempio e come estendere anche le interfacce per massimizzare il riuso. Infine abbiamo esaminato le regole di precedenza e le buone pratiche. Nel prossimo articolo saliremo di livello per parlare di principi di progettazione, esaminando i principi SOLID e alcuni design pattern ricorrenti.