Metodi di estensione
Scritto da
Marco Amendola
il
giovedì 17 giugno 2010
Linguaggio:
•
Framework:
•
Livello: 200
Download sorgenti
I metodi di estensione (extension methods) sono
costrutti introdotti intorno al novembre 2006 nel linguaggio C#
(versione 3.0) e in Visual Basic (versione 9.0). Si tratta di una
sintassi in grado di "aggiungere" metodi addizionali a classi o
interfacce già esistenti.
Diversamente da alcuni linguaggi dinamici come Ruby o
Javascript, questi metodi di estensione non possono essere aggiunti
a run-time ma solamente in fase di codifica; pur conservando le
caratteristiche di type safety (controllo rigido dei tipi
in fase di compilazione), su cui gli utilizzatori fanno
affidamento, questo costrutto aumenta notevolemente le potenzialità
espressive del linguaggio.
Il meccanismo su cui si basano i metodi di estensione
non è quello dell'ereditarietà (come vedremo in
seguito più in dettaglio); in altre parole non stiamo definendo una
sottoclasse derivata ma semplicemente aggiungendo dei metodi
"esterni" ai membri di una classe esistente.
A riprova di questo è interessante notare che i metodi di
estensione sono supportati, in particolare, anche per i tipi
sealed, ovvero le classi esplicitamente definite come non
derivabili. Il risultato finale, dal punto di vista
dell'utilizzatore del metodo aggiuntivo, è comunque sostanzialmente
identico a quello di un membro "proprio" della classe estesa.
Sintassi
La forma con cui vengono definiti i metodi di estensione è la
seguente:
C#
namespace Utils
{
public static class StringExtensions
{
public static string Quote(this string target)
{
return string.Concat("\"", target, "\"");
}
}
}
VB
Imports System.Runtime.CompilerServices
Namespace Utils
Public Module StringExtensions
<Extension()>
Public Function Quote(ByVal target As String) As String
Return String.Concat("""", target, """")
End Function
End Module
End Namespace
Come si vede, la struttura somiglia alla semplice definizione di
una semplice funzione di libreria; l'unica differenza è la keyword
this nel codice C# e l'attributo Extension con
cui è decorato il metodo in VB (evidenziati in grassetto).
Il risultato di questa dichiarazione è la possibilità di
utilizzare il nuovo metodo come se appartenesse alla classe
string:
C#
using Utils;
public class Program
{
static void Main(string[] args)
{
string text = "Una stringa non quotata";
Console.WriteLine(text.Quote());
Console.ReadLine();
}
}
VB
Imports Utils
Module Program
Sub Main()
Dim text As String = "Una stringa non quotata"
Console.WriteLine(text.Quote())
Console.ReadLine()
End Sub
End Module
Durante la scrittura del metodo abbiamo inoltre il consueto
supporto dell'Intellisense, che rileva la presenza del metodo
aggiuntivo segnalandolo con un simbolo leggermente differente:

Analizziamo le varie parti di codice più in dettaglio:
- namespace Utils: è il namespace in cui viene
inclusa l'estensione; la scelta del namespace è estremamente
importante in quanto costituisce il meccanismo attraverso il quale
è possibile "attivare" o disattivare la presenza del metodo
aggiuntivo nell'oggetto che desideriamo estendere.
I metodi aggiuntivi, infatti, sono visibili solo
se il relativo namespace viene importato nella classe in cui
si intende utilizzarli;
- public static class StringExtensions/Public Module
StringExtensions: è la dichiarazione della classe
contenente uno o più metodi di estensione; in C# è essenziale
che questo "contenitore" sia una classe static, mentre in
VB è necessario utilizzare un modulo.
Il nome del modulo, invece, è del tutto ininfluente, ma è pratica
comune denominare il modulo contenitore con il suffisso "Extension"
o "Extensions"; vale la pena puntualizzare che il contenitore
dei metodi di estensione non ha alcuna attinenza con il tipo che
viene esteso;
- public static string Quote(…)/Public Function
Quote(…): è il nome del metodo di estensione. In questo
particolare esempio si tratta di un metodo che restituisce un
valore, ma è altrettanto possibile definire il metodo come
void (sub in VB) se non si tratta di una
funzione. Nel codice C# si noti che il metodo viene dichiarato
static (l'assenza della clausola static darebbe luogo
comunque ad un errore di compilazione);
- …(this string target)/ByVal target As String:
è la parte che specifica quale classe stiamo
estendendo. La forma è identica a quella di un normale parametro ma
per convenzione il primo parametro del metodo di estensione
specifica il tipo della classe che viene estesa; in C# questa
convenzione viene rafforzata dalla presenza della clausola
this appena prima del tipo;
- <Extension()>: in VB occorre precisare
esplicitamente che si tratta di un metodo di estensione decorando
la dichiarazione con l'attributo Extension (contenuto nel namespace
System.Runtime.CompilerServices).
Il parametro (negli esempi denominato "target") verrà
valorizzato in fase di invocazione con l'istanza su cui invochiamo
il metodo di estensione; è opportuno precisare che il nome scelto
per il parametro è del tutto ininfluente ai fini della corretta
dichiarazione del metodo.
Oltre al primo (obbligatorio), i metodi di estensione
supportano la presenza di ulteriori parametri, i quali si
comportano in maniera del tutto usuale.
Come funzionano?
Il codice presente nel corpo del metodo di estensione si
comporta esattamente come in qualunque metodo tradizionale. Come
già evidenziato, nel parametro target sarà passata l'istanza su cui
il metodo viene invocato; facendo riferimento all'esempio
precedente, alla chiamata di
si otterrà l'invocazione del metodo Quote con il passaggio della
variabile text come valore del parametro
target.
Questo comportamento risulta ancora più evidente se si considera
l'effettivo meccanismo con cui sono realizzati i metodi di
estensione "dietro le quinte"; essi non sono, infatti, che un mero
"abbellimento" della sintassi (syntactic sugar) per
l'invocazione di un metodo statico.
Durante una fase preliminare della compilazione, infatti, i
metodi di estensione sono risolti in invocazioni tradizionali e
successivamente trattati dal compilatore come tali.
L'esempio riportato verrebbe riconvertito in qualcosa del
genere:
C#
namespace Utils
{
public static class StringExtensions
{
public static string Quote(string target)
{
return string.Concat("\"", target, "\"");
}
}
}
...
using Utils;
public class Program
{
static void Main(string[] args)
{
string text = "Una stringa non quotata";
Console.WriteLine(StringExtensions.Quote(text));
Console.ReadLine();
}
}
VB
Namespace Utils
Public Module StringExtensions
Public Function Quote(ByVal target As String) As String
Return String.Concat("""", target, """")
End Function
End Module
End Namespace
...
Imports DomusDotNet.Articoli.MetodiDiEstensione.Utils
Module Program
Sub Main()
Dim text As String = "Una stringa non quotata"
Console.WriteLine(Quote(text))
Console.ReadLine()
End Sub
End Module
Per verificare che il codice effettivamente prodotto dal
compilatore equivalga a quanto riportato, occorre sbirciare
nell'assembly compilato ed analizzare direttamente l'IL
prodotto:

Utilizzo con istanze nulle
Un aspetto che può sorprendere è il fatto che i metodi di
estensione possano essere invocati su istanze null
(nothing in VB), mentre con un metodo "normale"
questo non avrebbe senso e scatenerebbe un'eccezione a
runtime.
La possibilità di essere chiamati in corrispondenza di istanze
nulle è, in effetti, un ampliamento della semantica di invocazione
caratteristica degli extension methods.
Considerando il modo in cui i metodi di estensione vengono
convertiti dal compilatore, si intuisce che questa casistica non
crea problemi nella fase di invocazione, quanto piuttosto nel corso
dell'esecuzione del metodo, se non si tiene conto del fatto che il
parametro target può contenere valori
nulli. Vediamo il seguente esempio (che utilizza anche un parametro
aggiuntivo oltre a quello di istanza):
C#
namespace Utils
{
public static class StringExtensions
{
public static string ClipTo(this string target, int maxLenght)
{
if (target.Length <= maxLenght)
return target;
else
return target.Substring(0, maxLenght);
}
}
}
...
string longText = "Una stringa abbastanza lunga che eccede i 20 caratteri";
string nullText = null;
Console.WriteLine(longText.ClipTo(20));
Console.WriteLine(nullText.ClipTo(20));
VB
Namespace Utils
Public Module StringExtensions
<Extension()>
Public Function ClipTo(ByVal target As String, ByVal maxLenght As Integer) As String
If (target.Length <= maxLenght) Then
Return target
Else
Return target.Substring(0, maxLenght)
End If
End Function
End Module
End Namespace
...
Dim longText As String = "Una stringa abbastanza lunga che eccede i 20 caratteri"
Dim nullText As String = Nothing
Console.WriteLine(longText.ClipTo(20))
Console.WriteLine(nullText.ClipTo(20))
Esaminiamo la seconda chiamata, quella relativa alla variabile
nullText: quando il codice viene eseguito, il metodo
ClipTo viene correttamente invocato; tuttavia esso causa
immediatamente un errore poiché viene acceduta la proprietà Lenght
sul parametro target, che è nullo.

Una versione migliore dovrebbe verificare il parametro:
C#
public static string ClipTo(this string target, int maxLenght)
{
if (target == null || target.Length <= maxLenght)
return target;
else
return target.Substring(0, maxLenght);
}
VB
<Extension()>
Public Function ClipTo(ByVal target As String, ByVal maxLenght As Integer) As String
If target Is Nothing OrElse target.Length <= maxLenght Then
Return target
Else
Return target.Substring(0, maxLenght)
End If
End Function
Questa caratteristica degli extension methods può essere
utilmente sfruttata in diverse circostanze, ad esempio per snellire
il codice dai test di nullità.
Vediamo in pratica un metodo con questa finalità:
C#
public static class StringExtensions
{
public static string ToSafeString(this object target)
{
string result = null;
if (target != null)
{
result = target.ToString();
}
if (result == null)
{
result = String.Empty;
}
return result;
}
}
...
//con test di nullità
string result = SomeFunction();
if (result != null){
result = result.ToString().ToUpper();
}
Console.WriteLine("Il risultato, in maiuscolo, è: {0}", result);
//versione concisa
Console.WriteLine("Il risultato, in maiuscolo, è: {0}", SomeFunction().ToSafeString().ToUpper());
VB
Public Module StringExtensions
<Extension()>
Public Function ToSafeString(ByVal target As Object) As String
Dim result As String = Nothing
If target IsNot Nothing Then
result = target.ToString()
End If
If result Is Nothing Then
result = String.Empty
End If
Return result
End Function
End Module
...
'con test di nullità
Dim result As String = SomeFunction()
If (result IsNot Nothing) Then
result = result.ToString().ToUpper()
End If
Console.WriteLine("Il risultato, in maiuscolo, è: {0}", result)
'versione concisa
Console.WriteLine("Il risultato, in maiuscolo, è: {0}", SomeFunction().ToSafeString().ToUpper())
Come si vede, utilizzando il metodo di estensione
ToSafeString viene evitato un test esplicito rendendo il
codice decisamente più conciso e leggibile.
Risoluzione degli overload sul tipo esteso
Un altro aspetto interessante riguarda la risoluzione dei metodi
di estensione in caso di overload, in particolare quando
si varia proprio il parametro che rappresenta il tipo esteso.
Prendiamo ad esempio il seguente codice:
C#
namespace Utils
{
public static class LoggingExtensions
{
public static void LogTo(this string target, TextWriter writer)
{
writer.WriteLine("Il contenuto di questa stringa è: {0}", target);
}
public static void LogTo(this int target, TextWriter writer)
{
writer.WriteLine("Questo intero è uguale a: {0}", target);
}
public static void LogTo(this object target, TextWriter writer)
{
writer.WriteLine("Il valore di questo oggetto è: {0}", target);
}
}
}
...
int someNumber = 3;
string someString ="Questa è una stringa";
DateTime someDate = DateTime.Now;
object someObject = "Questa è una stringa contenuta in una variabile di tipo object";
someNumber.LogTo(Console.Out); //Questo intero è uguale a: 3
someString.LogTo(Console.Out); //Il contenuto di questa stringa è: Questa è una stringa
someDate.LogTo(Console.Out); //Il valore di questo oggetto è: 17/06/2010 12.00.00
someObject.LogTo(Console.Out); //Il valore di questo oggetto è: Questa è una stringa contenuta in una variabile di tipo object
VB
Namespace Utils
Public Module LoggingExtensions
<Extension()>
Public Sub LogTo(ByVal target As String, ByVal writer As TextWriter)
writer.WriteLine("Il contenuto di questa stringa è: {0}", target)
End Sub
<Extension()>
Public Sub LogTo(ByVal target As Integer, ByVal writer As TextWriter)
writer.WriteLine("Questo intero è uguale a: {0}", target)
End Sub
<Extension()>
Public Sub LogTo(ByVal target As Object, ByVal writer As TextWriter)
writer.WriteLine("Il valore di questo oggetto è: {0}", target)
End Sub
End Module
End Namespace
...
Dim someNumber As Integer = 3
Dim someString As String = "Questa è una stringa"
Dim someDate As Date = DateTime.Now
Dim someObject As Object = "Questa è una stringa contenuta in una variabile di tipo object"
someNumber.LogTo(Console.Out) 'Questo intero è uguale a: 3
someString.LogTo(Console.Out) 'Il contenuto di questa stringa è: Questa è una stringa
someDate.LogTo(Console.Out) 'Il valore di questo oggetto è: 12/06/2010 21.39.47
someObject.LogTo(Console.Out) 'System.MissingMemberException: Public member 'LogTo' on type 'String' not found.
Come si vede, viene applicato il normale algoritmo di
risoluzione dell'overload di metodi, che prevede che venga presa la
"versione" meglio corrispondente al parametro passato; più
esattamente il "binding" con l'overload corretto viene fatto in
base al tipo della variabile che viene passata e non in base
all'istanza effettiva a run-time.
Nell'esempio, le prime due chiamate vengono risolte nei metodi con
parametro tipizzato rispettivamente string e int,
mentre la terza chiamata viene risolta nel metodo con parametro
object, non essendo presente un overload specifico con
datetime. Anche la quarta chiamata, in C#, segue la
stessa regola.
Nel quarto caso in VB si verifica una particolarità: viene
scatenata un'eccezione MissingMethodException (metodo mancante). In
VB, infatti, i metodi di estensione non hanno effetto sul late
binding e in caso di variabili dichiarate come Object
vengono ignorati.
Questo comportamento è spiegato dal fatto che in VB le variabili
dichiarate come Object supportano il late-binding, ovvero
la caratteristica di verificare solo a runtime l'effettiva presenza
del metodo; in questo contesto, la semplice presenza di un
extension method attribuibile a tutte le variabili Object avrebbe
potuto interrompere il funzionamento di codice già esistente nel
caso di sovrapposizione dei nomi dei metodi.
Sovrapposizione con i metodi "propri"
Possiamo verificare meglio l'interazione degli extension methods
con i metodi già esistenti nei tipi estesi
aggiungendo una classe al precedente esempio:
C#
public class Point {
public int X { get; set; }
public int Y { get; set; }
public void LogTo(TextWriter writer) {
writer.WriteLine("Le coordinate di questo punto sono: {0},{1}", X, Y);
}
}
...
public static class LoggingExtensions
{
public static void LogTo(this Point target, TextWriter writer)
{
writer.WriteLine("Le coordinate di questo punto, riportate dal metodo di estensione, sono: {0},{1}", target.X, target.Y);
}
}
...
Point somePoint = new Point { X = 1, Y = 2 };
object someObject = new Point { X = 1, Y = 2 };
somePoint.LogTo(Console.Out); //Le coordinate di questo punto sono: 1,2
someObject.LogTo(Console.Out); //Il valore di questo oggetto è: DomusDotNet.Articoli.MetodiDiEstensione.Point
VB
Public Class Point
Public Property X As Integer
Public Property Y As Integer
Public Sub LogTo(ByVal writer As TextWriter)
writer.WriteLine("Le coordinate di questo punto sono: {0},{1}", X, Y)
End Sub
End Class
...
Public Module LoggingExtensions
<Extension()>
Public Sub LogTo(ByVal target As Point, ByVal writer As TextWriter)
writer.WriteLine("Le coordinate di questo punto, riportate dal metodo di estensione, sono: {0},{1}", target.X, target.Y)
End Sub
End Module
...
Dim somePoint As Point = New Point With {.X = 1, .Y = 2}
someObject = New Point With {.X = 1, .Y = 2}
somePoint.LogTo(Console.Out) 'Le coordinate di questo punto sono: 1,2
someObject.LogTo(Console.Out) 'Le coordinate di questo punto sono: 1,2
Come si vede, il comportamento nei due linguaggi è differente ma
segue la stessa logica: garantire che la semplice aggiunta del
namespace contenente i metodi di estensione non interrompa
in maniera silenziosa il funzionamento di classi già
esistenti.
Osservando la prima delle due chiamate
(somePoint.LogTo) in entrambi i linguaggi il metodo di
estensione di Point viene ignorato, in quanto esiste già un metodo
proprio identico che non deve essere "mascherato"; nella fase di
"binding" i metodi propri dell'oggetto hanno la precedenza
su quelli di estensione.
Per quanto riguarda la seconda invocazione (effettuata sulla
variabile someObject):
- VB utilizza il late-binding, quindi invoca il metodo LogTo di
Point sulla variabile è di tipo Object perché ispeziona a run-time
l'istanza effetivamente contenuta;
- C#, invece, utilizza sempre un early-bnding (ovvero decide
l'overload in fase di compilazione) e quindi associa la chiamata
con il metodo di estensione del generico Object.
Risoluzione degli overload sugli altri parametri
Per completare i casi possibili, occorre verificare che la
regola degli overload sia valida anche quando la differenza fra le
versioni riguarda gli altri parametri del metodo, e non quello
rappresentante l'istanza estesa. Per fortuna in questo scenario non
ci sono sorprese o comportamenti irregolari.
Arricchiamo l'esempio precedente:
C#
public class Point
{
...
public bool CanInteractWith(object other)
{
if (other is Point)
return true;
else
return false;
}
}
...
public static class PointExtensions
{
public static bool CanInteractWith(this Point target, int value)
{
return true;
}
}
...
Console.WriteLine("somePoint può interagire con someString? {0}", somePoint.CanInteractWith(someString)); //somePoint può interagire con someString? False
Console.WriteLine("somePoint può interagire con someNumber? {0}", somePoint.CanInteractWith(someNumber)); //somePoint può interagire con someString? False
VB
Public Class Point
...
Public Function CanInteractWith(ByVal other As Object)
If TypeOf other Is Point Then
Return True
Else
Return False
End If
End Function
End Class
...
Public Module PointExtensions
<Extension()>
Public Function CanInteractWith(ByVal target As Point, ByVal value As Integer)
Return True
End Function
End Module
...
Console.WriteLine("somePoint può interagire con someString? {0}", somePoint.CanInteractWith(someString)) 'somePoint può interagire con someNumber? False
Console.WriteLine("somePoint può interagire con someNumber? {0}", somePoint.CanInteractWith(someNumber)) 'somePoint può interagire con someNumber? False
In questo caso a cambiare è il parametro value; come
anticipato, la risoluzione segue le regole già evidenziate.
Nonostante il metodo di estensione con parametro int sia
in effetti disponibile (e ben visibile attraverso Intellisense)

la chiamata
somePoint.CanInteractWith(someNumber) non attiva
questa versione, ma il metodo originale della classe con parametro
object, anche se più "lasco".
Anche in questo caso il compilatore sta privilegiando i metodi
propri della classe per non cambiare il comportamento esistente;
solo in mancanza di un binding adeguato passerà a considerare
eventuali metodi di estensione.
Tags: extension methods