Sempre dal libro “C# in depth” trovo interessante vedere come la sintassi per l’uso dei delegates sia variata dalla versione 1.0 sino alla version 3.0 di C#.
Prendiamo come esempio il classico ambito di utilizzo dei delegates: la gestione degli eventi.
Supponiamo di avere un applicativo Winform con un pulsante “Button” e di voler tracciare, scrivendo sulla console, i tre eventi: Key Press, Mouse Click e Click.
In C# 1.0 avremmo così fatto:
</p> <p>static void LogClickEvent (object sender, EventArgs e)</p> <p>{</p> <blockquote> <p>Console.WriteLine (“Log Click Event”);</p> </blockquote> <p>}</p> <p>static void LogMouseClickEvent (object sender, MouseEventArgs e)</p> <p>{</p> <blockquote> <p>Console.WriteLine (“Log Mouse Click Event”);</p> </blockquote> <p>}</p> <p>static void LogKeyboardPresEvent (object sender, KeyPressEventArgs e)</p> <p>{</p> <blockquote> <p>Console.WriteLine (“Log Keyboard Press Event”);</p> </blockquote> <p>}</p> <p> </p> <p>…</p> <p>Button button = new Button();</p> <p>button.Text = “Click me”;</p> <p>button.Click += new EventHandler(LogClickEvent);</p> <p>button.KeyPress += new KeyPressEventHandler(LogKeyboardPresEvent);</p> <p>button.MouseClick += new MouseEventHandler(LogMouseClickEvent);</p> <p>…</p> <p>
Cioè in pratica per associare l’handler dell’evento: si crea una nuova istanza specificando il tipo di delegate e passando la action (cioè il metodo da chiamare).
Vediamo come avremmo potuto migliorare la scrittura del codice sopra, usando tre features che sono state introdotte nei delegates C# 2.0 e cioè:
1) conversione implicita da un method group ad un tipo di delegate compatibile.
2) supporto per i delegates di “return type covariance” e “parameter type contravariance”.
3) anonymous methpds
Vediamo in dettaglio:
Per prima cosa sarebbe bello che il compilatore si occupasse di capire da solo il tipo del delegate, risparmiandoci il fatto di doverlo esplicitare nella clausola new.
Questo è possibile in C# 2.0 grazie ad una conversione implicita da un method group ad un tipo di delegate compatibile. Un method group è semplicemente il nome del metodo; viene chiamato method group in quanto ad un singolo nome di metodo possono essere associati diversi metodi con lo stesso nome ma che differiscono per i parametri (overloading). Grazie a questa conversione è possibile scrivere:
</p> <p>…</p> <p>button.Click += LogClickEvent;</p> <p>button.KeyPress += LogKeyboardPresEvent;</p> <p>button.MouseClick += LogMouseClickEvent;</p> <p>…</p> <p>
Più leggibile no?
Questa conversione implicita non funziona in tutti i casi. Proprio perchè si passa da un method group che può essere un insieme di metodi diversi, in alcuni casi il compilatore non è in grado di capire quale tipo di delegate si richiede. In questi casi si può effettuare un cast al tipo corretto.
Lo stesso libro invece che dare una regola suggerisce di usare la modalità “just try it”: cioè sperimentare con il codice, se il compilatore non si lamenta allora tutto ok. Io spesso e volentieri lo faccio, poi a posteriori cerco di capirne il motivo ma almeno ho passato l’ostacolo ed evito di scoraggiarmi.
Un’altra feature introdotta in C# 2.0 è il supporto per i delegates di “return type covariance” e “parameter type contravariance”. Vediamo una semplice definizione:
"Return type Covariance” significa che un tipo ritornato può essere trattato come fosse un super-tipo.
“Parameter type Contravariance” significa che un parametro di un certo tipo può essere trattato come un sotto-tipo
Se guardiamo la definizione dei tre delegati usati per intercettare gli eventi, abbiamo:
</p> <p>void EventHandler (object sender, EventArgs e) <br />void KeyPressEventHandler (object sender, KeyPressEventArgs e) <br />void MouseEventHandler (object sender, MouseEventArgs e)</p> <p>
MouseEventArgs e KeyPressEventArgs derivano da EventArgs. Dal fatto che i delegate supportano “parameter type contravariance” possiamo usare oggetti di tipo MouseEventArgs e KeyPressEventArgs a metodi che si aspettano il tipo EventArgs, essendo sicuri che tali oggetti verranno trattati come i corrispondenti sotto tipi.
Se quindi prendiamo l’esempio originario ed immaginiamo di non dover distinguere tra gli eventi accaduti, ma solo tracciare il firing, possiamo scrivere il seguente codice:
</p> <p>…</p> <p>button.Click += LogClickEvent;</p> <p>button.KeyPress += LogClickEvent;</p> <p>button.MouseClick += LogClickEvent;</p> <p>…</p> <p>
Far vedere il supporto di “return type covariance” è un po’ più difficile in quanto quasi tutti i delegate di .NET sono dichiarati con un return type void. Il libro ne definisce uno ad-hoc
</p> <p>delegate Stream StreamFactory();</p> <p>static MemoryStream GenerateRandomData()</p> <p>{</p> <blockquote> <p>…</p> </blockquote> <blockquote> <p>return new MemoryStream(buffer);</p> </blockquote> <p>}</p> <p>…</p> <p>StreamFactory factory = GenerateRandomData();</p> <p>Stream returnedStream = factory();</p> <p>
In pratica nonostante il delegate StreamFactory ritorni un tipo Stream, abbiamo associato una action (metodo GenerateRandomData) che ritorna un MemoryStream. L’invocazione all’ultima riga è possibile proprio grazie al supporto per “return type covariance”. Se nell’ultima riga cambiamo il tipo della variabile returnedStream a MemoryStream il compilatore darà errore poichè non sarà più possibile trovare un delegate corrispondente alla signature.
Il prossimo post invece sulla terza feature introdotta con C# 2.0, quella ritenuta la più importante: anonymous methods.