In questo articolo vedremo cosa siano i metodi di estensione, e quale sia il loro utilizzo e la loro implementazione mediante C#. Al termine dell'articolo, il lettore conoscerà le metodologie più comuni per la creazione dei metodi di estensione
I metodi di estensione sono - per l'appunto - dei metodi (intesi come funzioni e procedure implementate mediante codice) attraverso i quali aggiungere funzionalità ai tipi di variabile comunemente disponibili, senza la necessità di creare dei tipi derivati appositi. Tipicamente, vengono impiegati per estendere le funzionalità proprie di LINQ, e quindi maggiormente impiegati nell'estensione di metodi per oggetti di tipo IEnumerable e derivati, come ad esempio oggetti di tipo List<T>.
Per implementare metodi di estensione, è necessario dichiarare una classe di tipo static, all'interno della quale definire metodi (i quali saranno anch'essi static), che ricevano come parametro principale in ingresso una variabile del tipo cui si applica il metodo, premettendogli la parola chiave this.
Si veda come esempio la classe seguente:
public
static
class
Estensione
{
public
static void
MioMetodo(
this
string
ingresso)
{
// TO DO
}
}
In questo caso, abbiamo la classe Estensione, dichiarata come static, al cui interno definiamo il metodo statico MioMetodo. Quest'ultimo possiede il parametro stringa ingresso, preceduto dalla parola chiave this. Questo significa che, all'interno di una qualsiasi procedura del nostro programma, avendo dichiarato quanto sopra ci permetterà di accedere a MioMetodo come se si trattasse di una funzione qualsiasi collegata al tipo string. Da notare soprattutto l'inclusione del metodo in Intellisense, ad indicare la completa integrazione in Visual Studio.
Prima di valutare scenari più complessi, iniziamo con il valutare un esempio semplice. Supponiamo di avere la necessità di tradurre in italiano la descrizione True / False di un tipo booleano. Se è vero che potremmo predisporre un semplice if/then/else, i metodi di estensione ci consentono di avere codice facilmente riutilizzabile e pulito, che sarà semplice manutenere in futuro. Nel caso esplicitato poco sopra, potremmo disporre un semplice metodo di questo tipo:
public
static
string
Booleano(
this
bool
ingresso)
{
return
ingresso ?
"Vero"
:
"Falso"
;
}
Notiamo come il metodo restituisca un tipo string, ed in ingresso accetti un boolean. Il codice, molto semplicemente, verifica lo stato del booleano, e se è True restituira "Vero", mentre in caso contrario avremo la stringa "Falso". Diventerà quindi molto comodo sfruttare questo metodo in questo modo:
var a =
true
;
Console.WriteLine(a.Booleano());
Console.Read();
Potremmo essere nella necessità di passare al nostro metodo alcuni parametri aggiuntivi. Supponiamo di voler implementare un metodo che, data una stringa, ci restituisca un conteggio delle occorrenze di un datoound-color:whitesmoke;">}
Notiamo come il metodo restituisca un tipo string, ed in ingresso accetti un boolean. Il codice, molto semplicemente, verifica lo stato del booleano, e se è True restituira "Vero", mentre in caso contrario avremo la stringa "Falso". Diventerà quindi molto comodo sfruttare questo metodo in questo modo:
var a =
true
;
carattere al suo interno. Oltre al parametro
this, relativo al fatto che il metodo si applica al tipo
string, sarà necessario fornire anche il parametro relativo al carattere di cui si desidera effettuare un conteggio.
Basterà aggiungere quest'ultimo alla lista dei parametri, senza alcuna parola chiave ad accompagnarlo. Potremo cioè dichiarare un tale metodo in questo modo:
public
static
int
Carattere(
this
string
ingresso,
char
carattere)
{
return
ingresso.ToCharArray().Where(x => x == carattere).Count();
}
A questo punto, il metodo potrà essere richiamato su un tipo string, passando un unico parametro, ovvero il carattere da conteggiare.
var stringa =
"Stringa di test per il conteggio del carattere a"
;
Console.WriteLine(stringa.Carattere(
'a'
));
Eseguito il codice qui sopra riportato, vedremo restituito il valore di 4.
Concatenazione di metodi
Uno degli aspetti più utili dei metodi di estensione, è che - al pari di quelli definiti nello standard del .NET Framework - sono concatenabili. Non solo tra di loro, ma anche con quelli predefiniti. In riferimento allo snippet appena visto, potremmo ad
esempio pensare di rimuovere una parte della stringa in ingresso, e solo dopo applicare il metodo Carattere.
var stringa =
"Stringa di test per il conteggio del carattere a"
;
Console.WriteLine(stringa.Remove(2, 6).Carattere(
'a'
));
Notiamo ad esempio come venga qui applicato prima il metodo standard Remove, partendo dal carattere 2 per lunghezza 6, e solo dopo il metodo di estensione
Carattere. Eseguendo il codice, vedremo come questa volta venga restituito il valore di 3 (Remove, infatti, eseguito come sopra eliminerà - tra gli altri caratteri - un'occorrenza tra quelle ricercate).
Molto banalmente, i metodi di estensione possono essere concatenati in qualsiasi posizione. Sopra abbiamo visto il metodo
Carattere applicato come ultima funzione di una sequenza, ma naturalmente le estensioni possono proseguire con altri metodi. Prendiamo ad esempio il metodo visto sopra per ottenere i valori stringa italiani "Vero / Falso". Per un qualsiasi motivo,
desideriamo che in un punto specifico del nostro programma quelle due stringhe debbano essere maiuscole. Dovendo modificare solo un punto di programma (rispetto a tutti quelli che utilizzeranno il metodo), non modificheremo il metodo stesso. Possiamo però
applicare il metodo standard ToUpper() al risultato del nostro metodo esteso. In codice, sarà:
var a =
true
;
Console.WriteLine(a.Booleano().ToUpper());
Anche in questo caso, possiamo vedere la completa interazione con Intellisense. Dal momento infatti che il metodo
Booleano() restituisce una stringa, nel proseguire da esso ci verranno mostrati tutti i possibili metodi applicabili a
string, estensioni incluse.
Utilizzo nelle query LINQ
I metodi di estensione sono particolarmente utili all'interno di interrogazioni LINQ, per realizzare funzionalità aggiuntive e semplificare query che potenzialmente possono risultare di lettura più complessa, se implementate senza una separazione logica
dei processi che la compongono.
Vediamo un esempio per farci un'idea: si supponga di voler interrogare una stringa, e ricavare il conteggio delle lettere che lo compongono, per esporlo poi in ordine decrescente. Ovviamente esistono molti modi per eseguire un'elaborazione di questo tipo,
ed anche se ad una prima occhiata può sembrare eccessivo quanto andremo a vedere, è utile riflettere sulla flessibilità e potenzialità che i metodi di estensione ci offrono, anche nell'ottica di avere funzioni "parlanti" che eseguano compiti precisi facilmente
concatenabili.
Come prima cosa, dichiariamo una classe pubblica, Occorrenze. In essa predisporremo le proprietà
Carattere e Conteggio. Intuitivamente, esse corrispondono rispettivamente al carattere letto ed al numero di volte che compare in una data stringa.
public
class
Occorrenze
{
public
char
Carattere {
get
;
set
; }
public
int
Conteggio {
get
;
set
; }
}
Successivamente dichiariamo un metodo statico che, dato un IEnumerable
in ingresso, ne effettui il conteggio dei caratteri
public
static
IEnumerable<Occorrenze> ConteggioOccorrenza(
this
string
ingresso)
{
var lista =
new
List<Occorrenze>();
ingresso.ToCharArray().ToList().ForEach(x =>
{
var oggetto = lista.FirstOrDefault(y => y.Carattere == x);
if
(oggetto !=
null
)
++oggetto.Conteggio;
else
lista.Add(
new
Occorrenze() { Carattere = x, Conteggio = 1 });
});
if
(oggetto !=
null
)
++oggetto.Conteggio;
else
lista.Add(
new
Occorrenze() { Carattere = x, Conteggio = 1 });
});
return
lista;
}
Il funzionamento del metodo è semplice: si dichiara una List<Occorrenze>, e tramite metodo ForEach si va a verificare la presenza del carattere ciclato all'interno di tale lista. Se assente, lo si crea con conteggio 1. Se presente, semplicemente
si incrementa il valore di occorrenza. Al termine del ciclo, si restituisce la lista sotto forma di
IEnumerable.
Implementiamo ora un metodo per ottenere una lista di tali occorrenze in forma leggibile
public
static
string
TestoStampabile(
this
IEnumerable<Occorrenze> ingresso)
{
string
retval =
""
;
ingresso.ToList().ForEach(x =>
{
retval += x.Carattere +
"\t"
+ x.Conteggio.ToString() + Environment.NewLine;
});
return
retval;
}
Tale metodo accetta in ingresso un IEnumerable, e tramite metodo ForEach va a popolare una stringa con un numero di righe uguale alle occorrenze in esso presenti. Per ciascuna riga, riporterà il carattere e le sue occorrenze separate da tabulazione.
A questo punto diventa estremamente semplice applicare tali metodi ad una stringa.
var stringa =
"Stringa di test per il conteggio delle occorrenze dei caratteri in ambito LINQ"
;
var result = stringa.ToLower()
.ConteggioOccorrenza()
.OrderByDescending(x => x.Conteggio)
.TestoStampabile();
Console.WriteLine(result);
Dal momento che non ci interessa una distinzione tra maiuscole e minuscole, effettuiamo come prima cosa una chiamata al metodo ToLower. Dopo ciò, useremo il nostro metodo ConteggioOccorrenza per ottenere un IEnumerable di tipo Occorrenza. Tale insieme può
sfruttare i metodi propri degli IEnumerable, e dal momento che vogliamo esporre il conteggio in modo decrescente, applichiamo il metodo standard OrderByDescending, indicando come parametro di ordinamento la proprietà Conteggio della classe Occorrenze. Infine,
eseguiamo il metodo TestoStampabile per ottenere una testo riassuntivo da emettere a video, cosa che faremo passando la variabile result alla funzione Console.WriteLine.
Pensando di dover rimettere mano al codice dopo diverso tempo, balza all'occhio l'estrema leggibilità del codice, e di conseguenza la semplicità e la chiarezza che esso conserva nel tempo. Con metodi di estensione consolidati e ben strutturati a seconda
delle esigenze, lo sviluppatore può dunque essere facilitato nelle operazioni di creazione, revisione e futura implementazione dei propri progetti.
Bibliografia