Ogni versione passata di C# è stata caratterizzata da un particolare aspetto, prima di esaminare nei dettagli le novità introdotte dalla quarta edizione del linguaggio, ne ripercorriamo brevemente la storia.
Versione | Data di rilascio | Caratteristiche particolari |
---|---|---|
1.0 | Gennaio 2002 | Ha portato alla luce un nuovo linguaggio di tipo managed con una sintassi simile al C/C++ e Java |
2.0 | Novembre 2005 | Ha introdotto l'uso dei tipi generici |
3.0 | Novembre 2006 | Ha arricchito C# con caratteristiche tipiche dei linguaggi funzionali, rendendo, grazie a LINQ, la sua sintassi molto più dichiarativa piuttosto che imperativa |
La peculiarità della prossima versione sarà la dinamicità, ovvero l'aggiunta di un insieme di funzionalità che renderanno il comportamento di C# simile a quello dei linguaggi dinamici.
Ma perché introdurre funzionalità di tipizzazione dinamica in un linguaggio a tipizzazione statica?
La risposta va cercata in quelle che solo le attuali direzioni che guidano l'evoluzione dei moderni linguaggi di programmazione, i quali possono essere:
Tipo di linguaggio | Descrizione |
---|---|
Dichiarativo | Permette di scrivere il codice in una forma che descrive cosa fare piuttosto che come farla |
Concorrente | Consente di scrivere codice in modo da sfruttare le potenzialità di CPU multi-processore, ormai presenti su ogni computer, anche di fascia bassa |
Dinamico | In scenari che sono di per natura dinamici, come il DOM di una pagina Web, un linguaggio a tipizzazione dinamica rende l'esperienza di sviluppo migliore rispetto ad uno a tipizzazione statica |
C#, grazie anche al .NET Framework, mira a diventare un linguaggio misto, che racchiude in se le principali caratteristiche delle tre tendenze e quindi adatto alla maggior parte degli scenari. La nuova versione un grande passo verso questa meta.
Non solo. Dinamicità vuol dire anche facilità di interoperabilità fra ambienti eterogenei. Di fatto la versione 4.0 di C# semplificherà notevolmente l'interoperabilità con COM ed aggiungerà la possibilità di interagire con linguaggi completamente dinamici e di scripting come Python o Ruby, questo grazie al DLR un nuovo run-time costruito sopra al CLR.
Infine, sempre in termini di interoperabilità, abbiamo, da parte di Microsoft, la volontà di allineare C# e VB.NET in modo da avere dalla versione 4.0 in poi una co-evoluzione dei due linguaggi.
Risoluzione Dinamica (Dynamic Lookup)
Fino alla versione 3.0 di C# l'unico modo per invocare dinamicamente i membri di una classe era tramite la Reflection. Con la prossima versione grazie alle novità introdotte potremmo scrivere del codice che sfrutta la risoluzione dinamica. Cos'è la risoluzione dinamica?
La risoluzione dinamica consiste nella possibilità di scrivere codice che verrà valutato durante l'esecuzione piuttosto che in fase di compilazione. In sostanza quello che possiamo fare è "dire" al compilatore: "qualsiasi operazione effettuata su questo tipo, voglio che venga esclusa dalla fase di compilazione e valutata solo durante l'esecuzione".
Questo modo di lavorare ci risulterà comodo in determinati scenari che sono per loro natura dinamici, ovvero:
- Accesso dinamico, quindi tramite Reflection, ad oggetti .NET
- Interoperabilità con oggetti COM
- Strutture di oggetti soggette a cambiamento come il DOM di una pagina web
- Interazione con linguaggi puramente dinamici come Python e Ruby
Il tipo 'dynamic'
Per rendere un oggetto dinamico non dobbiamo far altro che dichiararlo utilizzando il nuovo tipo dynamic
, dopodiché possiamo effettuare su di esso tutte le operazione che vogliamo, come invocare metodi piuttosto che impostare proprietà.
Il funzionamento visto ad alto livello è molto semplice. Normalmente il codice viene compilato come staticamente tipizzato, quando utilizziamo la keyword dynamic
indichiamo al compilatore che non conosciamo la natura di quell'oggetto e che quindi vogliamo utilizzare la risoluzione dinamica, sia per l'operazione stessa, sia per i parametri richiesti da essa.
Per esempio ipotizziamo di aver definito una classe completamente vuota di nome Example
.
//il successivo frammento di codice produrrà un errore in fase di compilazione
var e = new Example();
e.Process(5,"prova");
//mentre il seguente passerà la fase di compilazione ma produrrà un errore in fase di esecuzione
dynamic d = new Example();
d.Process(5,"prova");
d.SomeProperty = 12.34m;
d["chiave"] = "valore";
Possiamo implicitamente convertire qualsiasi tipo in dynamic
e viceversa, in questo caso dobbiamo indicare il specifico tipo.
dynamic d = 10; // conversione implicita
int x = d; // conversione implicita (valutata a run-time)
var y = d; // conversione non valida
Anche la risoluzione del corretto overload di un metodo avviene in fase di esecuzione. Prendiamo come esempio il metodo statico Abs
della classe Math
, del quale esistono sei overload per sei differenti tipi di parametro.
// viene scelto in fase di compilazione l'overload per il tipo int
int x = 10;
int result = Math.Abs(x);
// viene scelto in fase di esecuzione l'overload per il tipo int prima e double dopo
dynamic y = 10;
dynamic result = Math.Abs(y);
dynamic k = 1.75;
dynamic result = Math.Abs(k);
Qualsiasi valore restituito dall'accesso ad un membro dinamico è sua volta di tipo dinamico.
//var è a sua volta di tipo dynamic
dynamic y = 10;
var result = Math.Abs(y);
Va sottolineato il fatto che, all'interno di Visual Studio, per il tipo dynamic
non abbiamo il supporto dell'IntelliSense, questo perché l'oggetto reale non è conosciuto in fase di compilazione.
Quando l'applicazione sarà in esecuzione il run-time cercherà di effettuare la risoluzione delle operazioni effettuate, se non vi riuscirà, scatenerà un'eccezione. Questo comportamento rappresenta sia l'aspetto positivo, sia quello negativo dei linguaggi dinamici: di fatto se utilizzato in maniera incontrollata in tutti i contesti di sviluppo può portare notevoli svantaggi. Per questo è opportuno valutare con attenzione i casi in cui utilizzare oggetti dinamici.
Il binding di un oggetto dinamico
Una volta impostato il tipo come dynamic
non ci dobbiamo più preoccupare di quale sia la sua origine, il codice sarà eseguito in modi diversi in base alla natura dell'oggetto stesso. In questo caso infatti non si parla di typing ma di binding, il tipo dipenderà proprio dall'oggetto che sarà collegato alla nostra variabile
L'esecuzione di una operazione dinamica comincia con la fase di runtime lookup: il run-time ispeziona l'oggetto interessato e successivamente delega l'invocazione ad uno specifico componente. Per esempio se stiamo lavorando con un oggetto .NET, come nel primo esempio, il componente invocato utilizzerà la Reflection per effettuare l'operazione. Diversamente, se stiamo lavorando con un oggetto COM, l'operazione sarà delegata ad un componente che effettuerà l'operazione dinamica tramite l'interfaccia IDispatch
.
Se stiamo interoperando con un oggetto di un altro ambiente, prettamente dinamico come Python, verrà utilizzato uno specifico componente che eseguirà le operazioni sfruttando il Dynamic Language Runtime (DLR). Questi componenti scelti ed invocati a run-time si chiamano "binder".
Figura 1. Binder
IDynamicMetaObjectProvider
Possiamo sviluppare un nostro binder creando una classe che implementi l'interfaccia IDynamicMetaObjectProvider
o più semplicemente eredita dalla classe base DynamicObject ed effettua l'override dei metodi interessati.
All'interno del .NET Framework 4.0 troveremo anche un altro oggetto che implementa l'interfaccia IDynamicMetaObjectProvider
, l'ExpandoObject. Questo tipo rappresenta un oggetto i cui membri possono essere dinamicamente aggiunti o rimossi in fase di esecuzione. Prima di descrivere dettagliatamente l'ExpandoObject
analizziamo il seguente frammento di codice.
var obj = new Dictionary();
obj["Name"] = "Matteo Baglini";
obj["Print"] = new Action(p => Console.WriteLine("Nome: {0}",p));
(obj["Print"] as Action)(obj["Name"]);
Sfruttando un Dictionary
, con chiave di tipo string
e valore di tipo object
, creiamo un oggetto al quale possiamo aggiungere o rimuovere un qualsiasi elemento, quindi da un certo punto di vista, dinamico.
Naturalmente il codice di esempio porta degli svantaggi: è difficile da leggere, non permette il supporto dell'IntelliSense e se viene richiesto un elemento non presente ottengo una eccezione. Grazie ad ExpandoObject
posso riscrivere il codice precedente in modo che sia più leggibile, vediamo come.
dynamic obj = new ExpandoObject();
obj.Name = "Matteo Baglini";
obj.Print = new Action<object>(p => Console.WriteLine("Nome: {0}", p));
obj.Print(obj.Name);
I passi fondamentali sono due: creare una istanza di ExpandObject
e dichiararlo come dynamic
. Il risultato sarà un oggetto dinamico estremamente flessibile e costruito a runtime. Possiamo aggiungere membri di ogni genere, anche eventi, come nel seguente esempio.
//inizializzo l'evento
obj.MyEvent = null;
//aggancio un handler
obj.MyEvent += new EventHandler((sender, e) => Console.WriteLine("Evento intercettato!")); ;
//scateno l'evento
if (obj.MyEvent != null)
obj.MyEvent(obj, EventArgs.Empty);
A questo punto come aggiungere membri dovrebbe essere chiaro, la domanda che sorge spontanea è: come faccio a rimuovere un membro? La risposta è più semplice di quello che possiamo immaginare. La classe ExpandoObject
implementa anche l'interfaccia IDictionary<string,object>
la quale ci permette di accedere ai membri come abbiamo visto nell'esempio introduttivo oppure iterare su di essi.
foreach (var property in (IDictionary) obj)
Console.WriteLine(property.Key + ": " + property.Value);
((IDictionary)obj).Remove("Print");
Nel precedente esempio vengono stampati a Console
tutti i membri dell'istanza obj
ed infine viene rimosso il metodo Print
.
Ma non finisce qui. ExpandObject
è adatta anche in scenari di databinding, perché implementa l'apposita interfaccia INotifyPropertyChanged
, che ci notifica, grazie all'evento PropertyChanged
, quando il valore di un membro è stato modificato.
((INotifyPropertyChanged) obj).PropertyChanged +=
new PropertyChangedEventHandler(
(sender, e) => Console.WriteLine("{0}", e.PropertyName)
);
obj.Name = "Mario Bianchi";
Anche in questo caso l'unica accorgimento è quello di effettuare la conversione esplicita all'interfaccia desiderata.
Parametri Opzionali e Nominali
Nel .NET Framework è abbastanza frequente trovare classi con metodi che accettano molti parametri, i quali di conseguenza, danno origine a molti overload per fornire dei parametri predefiniti. Con la versione 4.0 di C# non dobbiamo più scrivere tutti quei metodi dato che sarà possibile rendere opzionali i parametri di un metodo. Per sfruttare questa funzionalità basterà assegnare al parametro, direttamente nella firma del metodo, un valore predefinito. Vediamo un esempio creando un'applicazione di tipo console e dichiarando nella classe Program
il seguente metodo Process
.
private static void Process(int p_1 = 10, string p_2 = "prova", double p_3 = 12.34)
{
Console.WriteLine("Parametri: {0}, {1}, {2}", p_1, p_2, p_3);
}
L'unica responsabilità del metodo è quella di stampare a video i valori dei quattro parametri, dei quali solo il primo non è opzionale. Possiamo invocare il precedente metodo passando solo i parametri che ci interessano lasciando gli altri invariati. Di seguito vediamo tutte le possibili invocazioni ed il relativo output.
Process(1, "hello", 5.6); //Parametri: 1, hello, 5.6
Process(1, "hello"); //Parametri: 1, hello, 12.34
Process(1); //Parametri: 1, prova, 12.34
Process(); //Parametri: 10, prova, 12.34
L'unico vincolo imposto da questa funzionalità riguarda il valore predefinito, il quale deve essere costante in fase di compilazione.
I parametri nominali sono la diretta conseguenza dell'introduzione dei parametri opzionali: anche se possiamo indicare valori predefiniti per i parametri di un metodo non possiamo effettuare invocazioni omettendo i parametri iniziali o centrali.
Quindi prendendo come esempio sempre il metodo Process
, non sarà possibile effettuare le seguenti invocazioni.
Process(,"test",1.5); //Errore: Argument missing
Process(5,,1.5); //Errore: Argument missing
L'impossibilità è dovuta ad una scelta di design da parte del team di C#, il quale giustamente, reputava illeggibile codice di questo tipo.
Grazie ai parametri nominali possiamo indicare, al momento dell'invocazione di un metodo, quali valori assegnare a determinati parametri, selezionati in base al nome del parametro stesso. Possiamo superare il vincolo riscontrato nel precedente frammento di codice così:
Process(p_2: "test", p_3: 1.5);
Process(5, p_3: 1.5);
Inoltre grazie a questa funzionalità possiamo anche cambiare l'ordine di assegnazione dei parametri.
Process(p_3: 1.5, p_1: 5, p_2: "test");
Sia i parametri opzionali, già presenti in Visual Basic (sin dalla versione 6) e nei linguaggi nativi, sia i parametri nominali, sono semplici funzionalità che migliorano l'esperienza di sviluppo e la leggibilità del codice in tutti gli scenari di interoperabilità fra questi ambienti, ad esempio nell'uso di librerie COM come le Office Automation API.
Varianza
La possibilità di utilizzare tipi generici è stata introdotta sin dalla versione 2.0 di C# e del .NET Framework. Tuttavia esistono degli scenari, legati all'ereditarietà, non percorribili con l'attuale versione del linguaggio. Per capire meglio osserviamo il seguente frammento di codice.
IList<string> list1 = new List<string>();
IList<object> list2 = list1;
Dato che il tipo string
è implicitamente convertibile a object
, il precedente codice sembra perfettamente valido, però se proviamo a compilarlo otteniamo un errore di conversione, che in sostanza ci informa dell'impossibilità di convertire in maniera implicita IList<string>
in IList<object>
. Come mai? Questo limite è dovuto al fatto che l'interfaccia generica IList<T>
rappresenta una collezione di elementi completamente modificabile, quindi, se ipoteticamente la conversione in fase di compilazione fosse possibile, potremmo scrivere codice come il seguente, con un risultato a run-time sicuramente disastroso.
list2[0] = 10;
string s = list1[0];
È anche vero che all'interno del .NET Framework esistono delle interfacce che hanno la caratteristica di fornire un accesso in sola lettura, quindi immuni al problema evidenziato poc'anzi.
Nel nostro esempio abbiamo utilizzato una collezione, quindi in questo caso il primo pensiero va subito all'interfaccia generica IEnumerable<T>
, la quale definisce il contratto per iterare una collezione. Solo iterare. Nessuna possibilità di aggiungere o rimuovere elementi. Quindi una conversione da IEnumerable<string>
a IEnumerable<object>
può avvenire senza alcun effetto collaterale.
Da qui l'introduzione nel .NET Framework e in C# 4.0 del concetto di Varianza e nello specifico di covarianza e controvarianza, applicabile solo su delegati e interfacce generiche.
out
Tramite l'uso della parola chiave out
parola chiave out, applicata ad parametro generico T
, andiamo ad indicare al compilatore che quel tipo è covariante in T, quindi saranno ammesse conversioni da B
ad A
, qualora B
erediti>da A
. Supponiamo di avere una classe Rectangle che eredita da Shape, possiamo applicare la Covarianza ad un delegato come mostrato dal seguente codice:
delegate TResult Create<out TResult>();
Create<Rectangle> createRectangle = () => new Rectangle();
Create<Shape> createShape = createRectangle;
Console.WriteLine(createShape());
in
Diversamente la Controvarianza la otteniamo tramite la parola chiave in
:
delegate void Print<in TArg>(TArg arg);
Print<Shape> printShape = arg => Console.WriteLine(arg);
Print<Rectangle> printRectangle = printShape;
printRectangle(new Rectangle());
Invariante
Possiamo anche applicare entrambe o nessuna restrizione alla definizione di un tipo generico. Come mostrato dalla seguente interfaccia, nella quale abbiamo definito TResult
come covariante, TArg
come controvariante e TProperty
come invariante.
interface IMyInterface<out TResult, in TArg, TProperty>
{
TResult Process(TArg arg);
TProperty Property { get; set; }
}