Con l'avvento del .NET Framework 3.5 e di Visual Studio 2008, sono state anche rilasciate le versioni 3.0 di C# e 9.0 di Visual Basic. Molte le novità per entrambi i linguaggi, tutte a supporto della nuova versione del framework ed in particolare di LINQ: il nuovo linguaggio per gestire l'accesso ai dati.
Tutte le novità sono supportate da Visual Studio 2008 con i meccanismi di intellisense, di refactoring, di formattazione del codice e di organizzazione delle dichiarazioni "using", tutti totalmente rivisti ed aggiornati per fornire allo sviluppatore un ambiente integrato e performante per la stesura del codice.
Vediamo in dettaglio le novità introdotte in C# 3.0.
Variabili locali implicite
La possibilità di dichiarare variabili locali implicite è già presente in alcuni linguaggi di programmazione (come JavaScript) e può risultare comoda in quanto permette allo sviluppatore di dichiarare delle variabili senza preoccuparsi di dichiararne il tipo. Sarà poi il compilatore ad assegnare i tipi, desumendolo dal valore che facciamo assumere alle rispettive variabili.
Per dichiarare una variabile locale implicita con C# 3.0, possiamo utilizzare la parola chiave var
da inserire nella dichiarazione al posto del tipo.
Variabile locale implicita (di tipo int)
var count = 0;
Utilizzare questo tipo di dichiarazione significa poter lavorare con una variabile senza dover effettuare operazioni di casting o di unboxing, come invece saremmo costretti a fare utilizzando variabili di tipo object
.
var count = 0;
object conto = 0; // boxing
int c1 = count;
int c2 = (int)conto; // cast e unboxing
Come mostrato nell'esempio, per ottenere il valore di una variabile di tipo object
dobbiamo effettuare un casting, mentre con la variabile implicita non siamo legati da questo tipo di vincolo.
Visual Studio 2008 offre il supporto completo per questa nuova funzionalità, aggiornando l'intellisense in base al tipo di valore assegnato alla nostra variabile locale implicita. Inoltre, utilizzando la keyword default
(già presente nelle versioni precedenti del linguaggio), è possibile dichiarare una variabile implicita e valorizzarla con il valore di default del tipo scelto (per gli interi il valore di default è 0, per le stringhe è una stringa vuota, e così via).
var intero = default(int); // vale 0
var stringa = default(string); // vale ""
Oltre ad essere dichiarate implicitamente, questo tipo di variabili possono essere usate solo localmente, all'interno dei metodi e non come parametri.
Tipi anonimi
Con "tipo anonimo" si indica la dichiarazione di una struttura personalizzata con una o più proprietà valorizzate, ma in sola lettura, che non necessita la creazione di un tipo custom.
Le proprietà possono essere di tipi differenti tra loro. Questi tipi vengono riconosciuti implicitamente in base ai valori assegnati, come per le variabili implicite. Inoltre il compilatore genera il codice che rappresenta la nostra nuova struttura e lo tiene nascosto, permettendo il corretto funzionamento dell'applicazione.
La sintassi per creare un tipo anonimo è la seguente:
Esempio di tipo anonimo
var person = new { Name = "Peppe", Surname = "Marchi" };
Possiamo richiamare tutte le proprietà della variabile come se fosse un comune tipo custom, in modalità read-only.
Utilizzare un tipo anonimo
var person = new { Name = "Peppe", Surname = "Marchi" };
Console.WriteLine("{0} {1}", person.Name, person.Surname);
I tipi anonimi sono utilizzati per lo più all'interno di query LINQ (subito dopo la clausola "select"), in quanto permettono la creazione di nuovi tipi di strutture per rappresentare le fonti di dati in cui viene effettuata la query.
Esempio di query LINQ
var query = from p in persons select new { p.Name, p.Surname };
foreach (var r in query)
{
Console.WriteLine("{0} {1}", r.Name, r.Surname);
}
Nella query d'esempio, la variabile p
viene creata attraverso la tecnica dei tipi anonimi.
Proprietà auto-implementate
Le proprietà auto-implementate permettono allo sviluppatore di creare le proprietà di una particolare classe con i vari accessori get
e set
, con uno sforzo minimo.
Dichiarazione di proprietà auto-implementate
namespace CSharp3Features
{
public class Author
{
public Author() { }
public Author(int id)
{
ID = id;
}
public int ID { get; private set; }
public string Name { get; set; }
public string Surname { get; set; }
}
}
Questa dichiarazione è ottima per tutte quelle proprietà in cui non sono previste particolari logiche aggiuntive per quanto riguarda gli accessori get e set.
La classe Author descritta sopra prevede così tre proprietà, di cui una, la proprietà ID, segnata come read-only (attraverso la keyword "private"). Tale proprietà può essere comunque valorizzata solamente all'interno di metodi appartenenti alla medesima classe (nel nostro caso all'interno del costruttore). Tentare di valorizzarla altrove porterà ad un errore di compilazione.
Author peppe = new Author(1);
peppe.ID = 2; // porta ad un errore di compilazione
peppe.Name = "Peppe";
peppe.Surname = "Marchi";
Inizializzatori di oggetti e collezioni
Gli inizializzatori di oggetti e di collezioni permettono allo sviluppatore di assegnare un particolare valore per ogni proprietà accessibile durante la creazione di una nuova istanza di un oggetto (sia questo un normale oggetto che una collezione) senza dover richiamare il costruttore di default.
Assegnare valori alle proprietà senza passare per il costruttore
Author peppe = new Author { Name = "Peppe", Surname = "Marchi" };
La sintassi che ci permette di valorizzare le proprietà è molto simile a quella utilizzata per i tipi anonimi, con la differenza che qui non usiamo la keyword "var" (che è comunque utilizzabile al posto della dichiarazione del tipo "Author") e possiamo vedere e modificare il codice della classe istanziata.
Allo stesso modo, è possibile creare delle nuove collezioni di oggetti, valorizzandone il contenuto in fase di creazione della nuova istanza.
Assegnare valori agli enumerati
List<int> numeri = new List<int> { 0, 1, 2, 3 };
List<Author> autori = new List<Author> {
new Author {Name = "Peppe", Surname="Marchi"},
new Author {Name = "Giuseppe", Surname="Marchi"},
};
Per utilizzare questa tecnica sulle collezioni di oggetti è d'obbligo che queste implementino l'interfaccia IEnumerable
. Tramite questa nuova tecnica di inizializzazione, non siamo obbligati a dover richiamare il metodo Add()
per ogni nuovo elemento che vogliamo aggiungere alla collezione.
Extension Methods
Gli Extension Methods sono un'importante novità per quanto riguarda la programmazione orientata agli oggetti, in quanto permettono di aggiungere dei metodi personalizzati a tutte le classi dichiarate come "selead", quindi non estendibili.
Nei linguaggi di programmazione ad oggetti, l'ereditarietà risulta uno strumento molto valido, ma spesso anche un'arma a doppio taglio, in quanto può essere difficile sviluppare delle classi da cui ereditare, senza intaccare la logica vera e propria della classe stessa. È per questo motivo che la maggior parte delle classi del .NET Framework sono dichiarate come "selead", così come tutti i tipi primitivi e derivati.
Attraverso l'utilizzo degli Extension Methods però, abbiamo la possibilità di aggiungere delle funzionalità personalizzate a quei tipi di oggetti, senza dover creare delle classi figlie (il che risulta comunque impossibile).
Il meccanismo è molto simile a quello dei metodi statici, ma che sono applicati non dalla classe in cui sono dichiarati, bensì su un tipo esterno. In altre parole, gli Extension Methods sono particolari tipi di metodi statici da dichiarare all'interno di una o più classi statiche che possono essere richiamati da oggetti del tipo che è stato esteso.
Definizione di un Extension Method
public static <tipo da estendere> <nome del metodo>(this <tipo da estendere> <nome parametro> [, <altri parametri>])
Il tipo scelto, deve essere sia il tipo di ritorno del metodo, sia il tipo del primo parametro. inoltre, sempre il primo parametro del nostro metodo, deve essere marcato con la parola chiave this
.
Aggiungere due metodi personalizzati al tipo "int"
namespace CSharp3Features
{
public static class Extensions
{
public static int Increment(this int i) { return ++i; }
public static int Decrement(this int i) { return --i; }
}
}
L'esempio definisce una classe statica (la classe Extensions) che contiene due metodi, sempre statici, che svolgono le operazioni di incremento e di decremento su valore intero. Una volta che inserita questa classe nel nostro progetto, possiamo utilizzare entrambi i metodi su qualunque variabile intera.
var count = 10;
while (count != 0)
{
Console.WriteLine(count);
count = count.Decrement();
}
Il .NET Framework 3.5 stesso contiene una grossa quantità di estensioni, soprattutto per quanto riguarda l'utilizzo di query LINQ. Inserendo il namespace System.Linq
nelle direttive using
di una classe, tutti gli oggetti di tipo IEnumerable<T>
espongono un set di nuovi metodi, da utilizzare all'interno delle query.
L'intellisense di Visual Studio segnala comunque i metodi estesi marcandoli con la keyword extension
prima delle definizione del metodo.
Gli Extension Methods sono veramente una bella novità, di cui però è meglio non abusare. Se, per aggiungere nuove funzionalità a strutture già presenti, possiamo creare delle nuove classi sfruttando il consolidato concetto dell'ereditarietà, è meglio preferire questa via.
Le novità introdotte in C# 3.0 sono veramente tante, ne esamineremo delle altre nella seconda parte dell'articolo.
Continuiamo ad esaminare alcune tra le più importanti novità del .NET Framework 3.5 e di C# 3.0
Metodi parziali
Nella seconda versione del .NET Framework sono state introdotte le classi parziali, cioè classi la cui definizione può essere divisa in due file differenti o in due differenti locazioni all'interno dello stesso file.
Alla versione 3.5 sono stati aggiunti anche i metodi parziali: è possibile inserire la dichiarazione di un metodo, senza la sua implementazione, all'interno di una classe e successivamente svilupparne l'implementazione all'interno della classe stessa o in un'altra locazione.
Dichiarare un metodo parziale
namespace CSharp3Features
{
public partial class Author
{
public int ID { get; private set; }
partial void setID(int id); // dichiarazione
partial void setID(int id) // implementazione
{
ID = id;
}
}
}
I metodi parziali risultano molto utili nel caso di codice generato automaticamente. Così facendo è possibile inserire nella generazione automatica solamente la definizione del metodo e lasciare allo sviluppatore la decisione se implementarlo o meno.
Questi invece, i principali limiti riscontrati nell'utilizzo dei metodi parziali:
- devono essere per forza dichiarati con tipo di ritorno
void
; - possono avere solo parametri di tipo ref e non out;
- sono implicitamente privati;
- non è possibile creare un delegato riferendosi ad un metodo parziale;
- non possono essere dichiarati come esterni (attraverso la keyword
extern
); - infine, è importante ricordarsi che la tecnica dei metodi parziali può essere utilizzata solamente all'interno di classi anch'esse segnate come
partial
.
Lambda Expressions
Nella versione 2.0 del .NET Framework furono inseriti gli anonymous methods, essenzialmente dei metodi, definiti anonimi in quanto non hanno una dichiarazione esplicita, che possono essere chiamati da un delegato.
Il codice di questi metodi risiede inline e non è necessario scrivere dei metodi appositi per essere utilizzati all'interno di un unico delegato. Questi, vengono utilizzati tramite la keyword delegate.
Dichiarare un gestore di eventi anonimo
Thread t = new Thread(delegate() { Console.WriteLine("Hello world"); }); t.Start();
Al posto della creazione (molto più laboriosa) di un metodo apposito per l'esecuzione del delegato:
Definizione classica di un delegato
static void Main(string[] args) { Thread t = new Thread(new ThreadStart(DoWork)); t.Start(); } private static void DoWork() { Console.WriteLine("Hello world"); }
Con l'arrivo delle Lambda Expressions, viene facilitata ancor di più la stesura di codice da eseguire a fronte della presenza di un delegato, in quanto queste rappresentano semplicemente delle funzioni vere e proprie che esprimono l'implementazione di un metodo e la creazione di una nuova istanza del relativo delegato in un unico costrutto sintattico.
La stessa funzionalità descritta sopra prima con un metodo anonimo e poi con il metodo tradizionale, tramite l'utilizzo di una Lambda Expression risulta cambiata.
Thread t = new Thread( result => {
Console.WriteLine("Hello lambda expression world");
});
t.Start();
La struttura delle Lambda Expressions
In questo caso specifico non sembrano molte le differenze, ma le Lambda Expression sono molto di più di quanto visto fin'ora. Analizziamo la struttura base di queste espressioni, che risultano composte in tre diversi blocchi.
(<elenco parametri>) => <espressione> | <istruzione> | <blocco di codice>
- Il primo blocco rappresenta l'elenco dei parametri attesi dal delegato che stiamo gestendo. Mettiamo questi parametri tra parentesi tonde se ne specifichiamo più di uno. Ad esempio scriviamo
(s, e)
per i classici parametri "Source e EventHandler" - il secondo blocco è composto dall'operatore =>, introdotto a supporto delle Lambda Expressions
- il terzo blocco rappresenta un'espressione, un'istruzione o un blocco di codice che utilizza i parametri descritti nel primo blocco e fornisce un tipo di ritorno (che deve essere lo stesso del delegato che si sta gestendo).
Attraverso le Lambda Expressions, possiamo dichiarare delegati per gli eventi in diversi modi.
Delegati che ritornano valori interi
private delegate int myDelegate(int a); private delegate int myDoubleDelegate(int a, int b); static void Main(string[] args) { myDelegate delegato = (x) => x + 1; // parametro con tipo implicito Console.WriteLine("{0}", delegato(20)); // 21 delegato = (int x) => x * x; // parametro con tipo esplicito Console.WriteLine("{0}", delegato(20)); // 400 myDoubleDelegate delegato2 = (x, y) => x * y; // più parametri Console.WriteLine("{0}", delegato2(10, 5)); // 50 delegato2 = (int x, int y) => // blocco di codice { if ((x % y) == 0) { Console.WriteLine("Sono multipli"); return x / y; } return 0; }; Console.WriteLine("{0}", delegato2(10, 5)); // 2 }
È utile osservare che il numero di parametri inseriti nella Lambda Expressions è sempre quello atteso dal relativo gestore di eventi. Inotre, espressioni e blocchi di codice ritornano sempre valori dello stesso tipo definito dal delegato di partenza.
Le Lambda Expressions sono particolari metodi anonimi, funzioni vere e proprie, aggiunte al .NET Framework per facilitare la scrittura e la lettura di espressioni LINQ. Nel nuovo linguaggio per le query su collezioni di oggetti, infatti, le Lambda Expressions vengono utilizzate continuamente. Facciamo l'esempio di una query LINQ per contare i numeri pari in un array di interi.
Conta i numeri pari in un array (forma compatta)
int[] numeri = { 1, 2, 3, 4, 5 }; int numeriPari = numeri.Count(n => n%2 == 0);
Il metodo Count di un array generico è stato esteso tramite la tecnica degli Extension Methods e che è stato reso disponibile dalla dichiarazione dell'utilizzo del namespace System.Linq. L'estensione prevede anche il passaggio di un delegato come parametro. Con le Lambda Expression possiamo utilizzarlo direttamente attraverso un costrutto sintattico unico, evitando dichiarazioni più estese.
Conta i numeri pari in un array (forma estesa)
int[] numeri = { 1, 2, 3, 4, 5 }; int numeriPari = numeri.Count(delegate(int arg) { if ((arg % 2) == 0) return true; return false; });
Keyword per query LINQ
Per supportare le funzionalità di LINQ, sono state introdotte anche alcune keyword. Approfondiremo queste novità in un articolo dedicato espressamente a LINQ, ma intanto elenchiamo le parole chiave descrivendole brevemente:
- from - è la keyword di inizio di ogni query LINQ e specifica la fonte di dati nella quale dovrà essere eseguita la query;
- where - è la clausola che specifica quali elementi della fonte di dati saranno ritornati dalla query; applica una sorta di filtro di selezione;
- select - è la clausola che definisce i tipi di valori che saranno prodotti dalla query;
- group - è la clausola che raggruppa i risultati secondo una certa chiave di raggruppamento;
- orderby - effettua un sort ascendente o discendente della fonte di dati in base ad un filtro;
- join - è la clausola che permette di associare più fonti di dati all'interno di un'unica query;
- into - è la keyword contestuale che indica in quale variabile temporanea vengono salvati i risultati di una select, di un group o di un join;
- let - è la keyword che permette di salvare temporaneamente il risultato di una subquery per poi utilizzarlo all'interno della query principale;