Anders Hejlsberg ha annunciato il 7 novembre a Seattle (http://oopsla.acm.org) alcune novità di C# che potremo vedere nelle future versioni di .NET Framework. Attenzione, però: non stiamo parlando della versione 1.1 che è già in beta e uscirà nel 2003, ma di qualcosa di più distante, che a oggi si chiama Visual Studio for Yukon e non vedrà la luce prima del 2004 (ci siamo capiti, parliamo almeno del 2005).
Vediamo brevemente le principali innovazioni:
- Generic
- Iteratori
- Metodi anonimi
- Tipi parziali
Generic
Un generic è un costrutto simile ai template di C++.
L’idea è di poter definire una classe generica, che viene poi istanziata in base a un tipo specifico.
Oggi ciò è possibile, in parte, usando il tipo object come tipo generico. Gli svantaggi sono dati dalla mancanza di un controllo forte sui tipi in fase di compilazione e dalle performance minori nel caso in cui si voglia usare la classe con un tipo value (che dovrà sempre essere sottoposto a boxing per essere trattato come object).
La sintassi sarà simile a quella di C++.
public class Stack< ItemType > { private ItemType[] items; public void Push( ItemType data ) { // ... } public void Pop( ItemType data ) { // ... } }
La classe Stack precedente potrà essere istanziata con questa sintassi.
Stack<int> stack = new Stack<int>; stack.Push(3); int x = stack.Pop();
Fin qui nulla di nuovo e di diverso da C++. La cosa interessante è che la classe Stack<int> sarà validata in fase di compilazione, ma verrà istanziata in fase di esecuzione. Non avendo una versione alfa a disposizione per fare le necessarie verifiche, non posso spiegare meglio come questo avvenga tecnicamente. Di sicuro sarà interessante scoprire se sarà possibile creare al volo una classe Stack<T> dove T è un tipo deciso al momento dell’esecuzione. Non è indispensabile, ma sarebbe utile in particolari situazioni.
In un generic possiamo avere più parametri. La novità è il fatto di poter definire dei vincoli (constraint) per garantire che alcune condizioni siano rispettate da chi usa il generic; a quel punto, la condizione si può assumere come vera e consente di ottimizzare il codice risultante.
Vediamo un esempio.
public class Dictionary<KeyType, ValType> { public void Add(KeyType key, ValType val) { // ... switch ( ((IComparable)key).CompareTo(x) ) { // ... } // ... } }
Il metodo Add deve ottenere l’interfaccia IComparable da key per poter chiamare il metodo CompareTo. Siccome KeyType è un parametro del generic, è un tipo di cui non si conoscono le interfacce implementate (si può solo assumere che abbia le stesse caratteristiche di object). Sarebbe però interessante “forzare” chi usa Dictionary a usare per KeyType solo dei tipi che implementino l’interfaccia IComparable.
Ecco come diventerà il codice che impone tale vincolo.
public class Dictionary<KeyType, ValType> where KeyType : IComparable { public void Add(KeyType key, ValType val) { // ... switch ( key.CompareTo(x) ) { // ... } // ... } }
A questo punto non è più necessario ottenere l’interfaccia IComparable con un cast all’interno della condizione di switch. Qualcuno potrebbe storcere il naso, ma è bene ricordare che in C++ il tipo associato a un template è conosciuto in fase di compilazione. In .NET il compilatore che userà un assembly in cui è definito un template non genererà il codice IL del generic specializzato per un certo tipo, ma lascerà questo compito al JIT. Ciò significa che, nei fatti, i parametri di un generic saranno considerati semanticamente sempre di tipo object durante la compilazione. I costraint sono quindi indispensabili per creare dei generic che “usano” i tipi e non si limitano semplicemente a fare da contenitori.
I costraint potranno anche essere multipli e fare riferimento a classi oltre che a interfacce, seguendo la sintassi che segue.
public class Dictionary<KeyType, ValType> where KeyType : IComparable, KeyType : IEnumerable, ValType : Customer { // ... }
Dal punto di vista dell’interoperabilità con gli altri linguaggi .NET, l’idea di Microsoft è che tutti i linguaggi (per lo meno Visual J#, Managed C++ e VB.NET) potranno consumare i generic, anche se non tutti potranno definirne di nuovi (ma su questo sono ancora molto vaghi).
Iteratori
Questa è un’innovazione del linguaggio che, come le keyword lock e using, non necessita di un supporto specifico da parte del CLR, ma aumenta la produttività del programmatore generando automaticamente del codice IL secondo pattern noti.
L’idea è di eliminare, nella maggior parte dei casi, la necessità di scrivere una classe che implementi l’interfaccia IEnumerator.
Una classe List normalmente deve implementare un metodo GetEnumerator, che restituisce un tipo (in questo caso ListEnumerator) che deve implementare IEnumerator e, nota dolente, va completamente scritta dal programmatore.
public class List { internal object[] elements; internal int count; public ListEnumerator GetEnumerator() { return new ListEnumerator(this); } }
Se nella classe List, anziché implementare GetEnumerator, si implementa un metodo chiamato foreach, questo può eseguire un ciclo di iterazione che, ogni volta che incontra la keyword yield, ferma il ciclo, restituisce il valore al foreach chiamante (quello che itera realmente l’oggetto List) e, alla richiesta del valore successivo, restituisce il valore seguente.
public class List { internal object[] elements; internal int count; public object foreach() { foreach(object o in elements) { yield o; } } }
Qualche problema? Ok, facciamo un passo indietro. L’uso di yield potrebbe essere il seguente.
public class ListDemo { // ... public string foreach() { yield "A new"; yield "C#"; yield "feature"; } } public class Demo { public void Main() { ListDemo l = new ListDemo(); foreach (string s in l) { System.Console.WriteLine( s ); } } }
Eseguendo il codice precedente otterremmo il seguente risultato:
A new C# feature
In realtà tutto quello che abbiamo visto è un modo per dire al compilatore di generare una classe equivalente alla vecchia ListEnumerator che non avevamo voglia di scrivere.
Metodi anonimi
Quante volte avete definito una funzione da passare a un delegate composta solamente da una o due istruzioni? In questi casi, è il codice scritto per dichiarare il metodo è più lungo del codice contenuto nel metodo stesso.
Per semplificare un po’ le cose potremo usare questa sintassi.
public class MyForm { ListBox listBox; TextBox textBox; Button button; public MyForm() { // ... button.Click += new EventHandler(sender, e) { listBox.Items.Add(textBox.Text); }; } }
Anche in questo caso non ci sono particolari favoritismi da parte del CLR. È il compilatore C# che genera un metodo contenente il codice scritto tra parentesi graffe dopo l’istanza del delegate; tale metodo avrà la stessa signature del delegate, quindi il codice del “metodo anonimo” potrà usare tali parametri.
Tipi parziali
C# attualmente impone di definire tutto il codice di una classe nello stesso file sorgente. La keyword partial consentirà di separare il codice di una classe su più file, come si può vedere di seguito.
public partial class Foo { public void f1() { // ... } }
public partial class Foo { public void f2() { // ... } }
I due file andranno comunque compilati nel medesimo assembly. Il risultato dell’esempio sarà un assembly contenente una sola classe Foo, contenente sia il metodo f1 che il metodo f2.
