Вы находитесь на странице: 1из 31

LEZIONE 1

Premesse (di Antonio Cangiano)

Introduzione
Benvenuti al corso di C#!
La seguente lezione è un'introduzione al corso. Le lezioni verranno pubblicate con una frequenza
non inferiore di una alla settimana, per permettere l'apprendimento del linguaggio in tempi
ragionevolmente brevi. Gli scopi del corso sono quelli di fornire gli strumenti essenziali per la
programmazione C#, e le basi necessarie per approfondire gli argomenti mostrati.

Conoscenze richieste
Il fruitore ideale del corso ha delle conoscenze di programmazione (anche minime); tuttavia
cercherò di essere abbastanza esplicativo per venire in contro alle esigenze di chi non ha mai
programmato, ma è interessato. Resta inteso che la conoscenza dell'uso basilare del computer è
assolutamente necessaria.

Requisiti Software
Il miglior modo per apprendere un nuovo linguaggio di programmazione è quello di studiare i
concetti fondamentali e contemporaneamente esercitarsi nello scrivere codice. C# è uno dei
linguaggi di programmazione Microsoft .NET, per cui è necessario installare nel proprio computer
il .NET Framework SDK o eventualmente un prodotto commerciale come Visual C# Standard e
Visual Studio .NET. Allo stato attuale l'installazione del .NET Framework SDK è possibile sui
seguenti sistemi operativi:

• Windows NT 4.0 (con Service Pack 6a)


• Windows 2000 (qualsiasi versione) (consigliata la Service Pack 2)
• Windows XP Professional

Non è possibile installare il .NET Framework SDK nei sistemi operativi di tipo Home (Windows
95/98/ME/XP HOME), tuttavia un collaboratore del sito ha escogitato una soluzione alternativa;
su sistemi come Windows 98, si installi il .NET Framework Redistributable. All'interno di esso
è presente sorprendentemente il compilatore senza documentazione. Potremo dunque
programmare da linea di comando anche su computer non di ultima generazione, ma non sarà
possibile installare il Visual Studio .NET o Visual C# Standard. (a breve proporremo un articolo a
riguardo). Mediante l'ausilio di MONO (un progetto open source) è inoltre possibile programmare
con C# in ambienti Linux. Dopo aver installato il .NET Framework SDK, si consiglia il download e
l'installazione della Service Pack 2. Siamo pronti a sviluppare in .NET!

Prima di iniziare
Prima di iniziare con la programmazione C# è bene sapere che il Framework .NET SDK, contiene
tutti gli strumenti necessari alla creazione di applicazioni desktop e web. Non bisogna sostenere
alcuna spesa per programmare in C#: basterà scrivere il codice dei nostri programmi in file di
testo con estensione .cs, servendosi di un qualsiasi editor di testo (anche il notepad) per poi
sfruttare il compilatore csc presente nell'SDK. Un approccio di questo tipo ha l'indubbio
vantaggio di rendere gratuito lo sviluppo, ma ha come svantaggio la mancanza di un editor
visuale (simile a Visual Basic 6) per creare in maniera intuitiva e visuale l'interfaccia grafica del
programma. Chi intendesse programmare applicazioni Desktop con C#, troverà alquanto utile
l'acquisto di Visual C# Standard o di Visual Studio .NET; per la programmazione Web
(essenzialmente ASP.NET) si può ricorrere all'ottimo tool visuale gratuito, rilasciato da Microsoft:
Web Matrix Project. Coloro che desiderano sviluppare in C# senza avvalersi dell'uso di uno
strumento commerciale, possono utilizzare uno strumento gratuito più potente del notepad:
Sharp Develop. Sono previste versioni future di Sharp Develop aventi caratteristiche visuali, ma
per il momento il prodotto non dispone di funzionalità per la creazione di interfacce tramite il
trascinamento di oggetti già pronti all'interno delle finestre delle applicazioni (Form).

Verifica dell'installazione
Come già detto, per compilare programmi scritti in C# si sfrutta il compilatore csc (csharp
compiler) presente nel Framework SDK. La verifica della corretta installazione del Framework
.NET e quindi della possibilità di compilare programmi C# è piuttosto semplice: è sufficiente
entrare nel prompt di Dos ed inserire csc /?. Se l'installazione è andata a buon fine, il
compilatore visualizza a video informazioni sulla versione corrente del compilatore e del
framework, ed un elenco dei possibili parametri da utilizzare. Nel caso di installazione del Visual
Studio o Visual C# Standard questa verifica non è necessaria.Questa lezione è solo una
premessa che mostra i prerequisiti per la programmazione C#. Nella prossima lezione vedremo
come funziona .NET e il processo di compilazione ed esecuzione di programmi scritti in C#.

LEZIONE 2

L'architettura .NET di Antonio Cangiano

Introduzione al .NET Framework


Nella precedente lezione abbiamo introdotto i prerequisiti per la programmazione in C#. Si è
sottolineato in più punti la necessità del .NET Framework al fine di programmare in C# o in un
qualsiasi altro linguaggio .NET (VB.NET, VC++.NET, VJ#.NET,ecc...). Il compilatore di ognuno di
questi linguaggi si trova, difatti, all'interno del framework, come pure le classi che permettono
l'accesso alle più svariate funzionalità. Possiamo definire VB.NET e VC++.NET come degli
adattamenti di linguaggi pre-esistenti alla nuova piattaforma. Visual C#, invece, è stato
sviluppato contemporaneamente all'architettura .NET e per questo motivo, è il linguaggio che più
di tutti ha delle forti dipendenze e correlazioni con l'architettura .NET. La filosofia con la quale è
stato pensato e creato l'ambiente .NET è la stessa che si riscontra programmando in C#; il
linguaggio si limita a fornire un ottimo strumento per l'accesso e la gestione delle risorse
disponibili in .NET, attraverso una sintassi più comprensibile per l'uomo. All'interno di un corso
che si occupa di mostrare la programmazione in Visual C#, è necessario l'inserimento di una
lezione introduttiva alla piattaforma .NET; una volta compresi i concetti base che permettono il
funzionamento delle applicazioni sviluppate in C# (in .NET), si potranno esporre e sfruttare al
meglio tutte le potenzialità di questo linguaggio di programmazione innovativo. .NET ha
essenzialmente due compiti:

• Da un punto di vista astratto, è una sorta di middleware, ovvero un ambiente di


esecuzione che si pone al di sopra del sistema operativo (Windows). L'ambiente .NET, che
costituisce un'ulteriore interfaccia tra il codice e la macchina, si occupa della gestione
dell'esecuzione dei programmi generati dal compilatore C#.
• Inoltre, fornisce una serie di funzionalità e "servizi" necessari alla programmazione in
ambiente Windows e Web. Queste "funzionalità" sono raccolte in classi, (le Classi di base
.NET) e il C# è uno dei linguaggi che permette di accedere ed usufruire in maniera
ottimale di queste risorse fornite da Microsoft. C# permette inoltre la creazione di classi
personalizzate come ogni altro linguaggio Object Oriented.

Nella realtà dei fatti, l'architettura .NET è molto più ricca e complessa di quanto possa sembrare,
tuttavia, ai fini della comprensione di questo corso, si mostreranno esclusivamente le nozioni
necessarie alla programmazione. In questa lezione dunque mostreremo, in maniera concisa,
come .NET gestisce il processo di compilazione ed esecuzione di un programma scritto in C#;
durante il corso verranno esposti i concetti della programmazione Object Oriented .NET e verrà
analizzato l'uso delle classi di base più importanti e necessarie allo sviluppo di applicazioni
Desktop e per il Web.
Compilazione tradizionale
Nei linguaggi tradizionali, fatta eccezione per Java, il codice sorgente viene compilato da uno
speciale programma, detto compilatore, che realizza una "traduzione" in codice comprensibile
ed eseguibile dalla macchina fisica. In altre parole, scrivendo un programma in Pascal, il
compilatore traduce il codice scritto dal programmatore, e genera, mediante una serie di
operazioni, un file eseguibile (.exe) contente istruzioni assembly per la CPU su cui si compila
(codice nativo). Ciò significa che un programma scritto in Pascal o in C, che viene compilato su
di un Pentium, non sarà eseguibile su di un Server Alpha, perché la natura dei processori X86
della Pentium è certamente diversa da quella dell'Alpha: dunque le istruzioni assembler generate
dal compilatore saranno comprese esclusivamente da processori della categoria X86. La
compilazione, inoltre, tiene anche conto del tipo di sistema operativo sul quale verrà eseguito il
programma finale. La gestione dei file eseguibili è diversa tra Windows e Linux, per cui un
programma compilato per Windows, non sarà eseguibile su di un sistema Linux/Unix o Mac OS.
Si ricorda tuttavia, che il programma Pascal suddetto, se non fa uso di particolari funzionalità
presenti esclusivamente in Windows o in Linux, sarà comunque compilabile da un compilatore su
di un sistema Linux, il quale genererà del codice nativo eseguibile su Linux. In C# il processo di
compilazione è un po' diverso.

Compilazione ed esecuzione in .NET


Il compilatore per C# è invocabile da riga di comando o in ambienti visuali; il suo nome è:
csc.exe.
Le opzioni e gli argomenti del compilatore sono molteplici e rispondono alle più svariate esigenze,
dalla semplice inclusione di riferimenti ad altri file, sino alla compilazione in modalità debug.
L'obiettivo del compilatore è piuttosto simile a quello dei compilatori tradizionali: ho un file di
input scritto con C# e desidero in output un file "eseguibile" dal computer. L'esempio più
semplice di compilazione tramite riga di comando del prompt di dos è : csc nomefile.cs.
L'invocazione del compilatore mediante tale comando, privo di ulteriori argomenti, genera il file
nomefile.exe. Bisogna fare molta attenzione nel distinguere l'.exe prodotto dal compilatore C#
e l'.exe prodotto da un compilatore tradizionale. Abbiamo verificato che un exe tradizionale è una
traduzione in codice nativo, in .NET questo non accade. Il programma .exe prodotto dal
compilatore C# è eseguibile esclusivamente dall'ambiente di esecuzione .NET. Su di un computer
privo del .NET Framework Redistributable l'exe appena generato non funzionerà. Come si spiega
tutto ciò? Il compilatore C#, come pure gli altri compilatori .NET, creano un eseguibile che
contiene un linguaggio intermedio detto IL ( Intermediate Language) o MSIL (Microsoft
Intermediate Language). In .NET dunque, un file .exe contiene codice IL, (linguaggio intermedio
di basso livello) e non codice nativo. IL, per sua natura, è ottimizzato per la conversione in codice
nativo con grande rapidità e senza un calo significativo delle performance. Questo linguaggio
intermedio permette l'utilizzo di programmi compilati su più piattaforme; l'unico requisito è che
vi sia installato il .NET Framework. Ovviamente sarà Microsoft ad incentivare la distribuzione di
framework per le più svariate piattaforme hardware. Il segreto di questa implementazione, sta
nel fatto che il codice intermedio IL viene convertito nel codice nativo delle varie piattaforme,
mediante il JIT Compiler (compilatore Just in time). La caratteristica di questo ulteriore
compilatore, presente nel Common Language Runtime o CLR (il vero e proprio ambiente di
esecuzione .NET) è quella di tradurre il codice IL in codice macchina. Il "Just in time" del nome è
dovuto al fatto che questa conversione viene effettuata nel momento in cui si richiede l'utilizzo di
una particolare funzione del programma e solamente questa porzione di codice IL viene
convertita: non il programma interamente. In questo modo, se si utilizza una sola funzionalità di
pochi Kb in un programma molto esteso, si elimina la fase di overhead dovuta alla compilazione
in codice nativo di un intero programma di svariati mega byte. Siamo abituati a pensare il
software come un file eseguibile e un insieme di librerie ed eventualmente componenti COM.
Nella logica .NET, il software è costituito da diversi assembly con funzionalità di librerie e da un
file assembly principale (avente estensione .exe) necessario per l'avvio del programma stesso.
Ogni assembly è autodescrittivo, nel senso che all'interno di esso è presente il codice IL con dei
metadati che forniscono informazioni sulle strutture dati utilizzate (i tipi) e sulle funzionalità
delle classi inserite (metodi), informazioni utili al CLR per effettuare la compilazione JIT e gestire
il programma in fase di esecuzione. Si osservi il seguente schema:
Il file sorgente viene compilato da csc.exe in un assembly IL con estensione .exe.
Avviando il programma .exe, il primo assembly, l'entry point del programma, è immediatamente
caricato. Successivamente vengono caricate le classi di base .NET necessarie, eventuali librerie
(assemblies con estensione .dll) ed eventuali componenti legacy COM sfruttando i servizi di
interoperabilità COM di .NET. Il Common Language Runtime si occupa del controllo dei tipi e dei
permessi di esecuzione in base alle informazioni contenuti nei metadati e in base alle
impostazioni di sicurezza del sistema su cui è in esecuzione il CLR. Il programma, inoltre, non
funzionerà se l'assembly principale è stato modificato successivamente alla compilazione. Questa
verifica è effettuata tramite un confronto tra il valore di hash calcolato durante la fase di
esecuzione e il valore presente nei metadati. Controllate le questioni inerenti alla sicurezza, viene
definito un dominio di accesso a memoria per l'applicazione, nel quale viene inserito il thread
principale del programma. Appena c'è un riferimento ad un metodo di qualche classe, dunque
non appena viene richiesta qualche particolare funzionalità del programma, il codice IL
corrispondente viene convertito in codice nativo dal Compilatore JIT. Infine il CLR si occupa
anche della gestione degli spazi di memoria da liberare perché non più necessari all'esecuzione
dell'applicazione stessa. Il Garbage Collector si occupa proprio della eliminazione delle risorse
superflue al fine di sollevare il programmatore dalla gestione diretta della memoria. (concetto
introdotto da Java). Il CLR verifica anche l'aderenza dei programmi agli standard definiti dal CTS
(Common Type System) e CLS (Common Language Specification) per permettere
l'interoperabilità tra codice C# e librerie scritte in altri linguaggi come VB.NET o VC++.NET. Il
CTS definisce inoltre una vera e propria gerarchia dei tipi disponibili e con i quali è possibile
definire classi personalizzate. Un programma C# può non aderire a questi standard e sfruttare
anche le specifiche non compatibili con gli altri linguaggi, ma in questo caso non sarà possibile
l'interoperabilità. Il codice sorgente che una volta compilato è gestito dal CLR viene detto codice
gestito o managed code. Il Common Language Runtime si occupa inoltre della gestione delle
eccezioni, permettendo un eccellente gestione delle stesse. Il CLR rispecchia esattamente le
caratteristiche del codice IL. Le principali funzionalità che un linguaggio per .NET deve avere
sono:
• Object Oriented: organizzazione del codice in classi, contenenti campi e metodi. Utilizzo di
classi già definite e possibilità di personalizzazione delle classi e operazioni tra di esse.
• Eredità singola delle classi: definire classi attraverso le "differenze" da una classe base.
• Tipi di valore e di riferimento: forte tipizzazione e distinzione tra strutture dati allocate
nella parte di memoria denominata stack e quelle allocate nello heap.
• Gestione errori tramite classi di eccezioni.

Sono stati definiti molti compilatori per linguaggi pre-esistenti che sono stati adattati a queste
caratteristiche di IL. Tali linguaggi sono detti: linguaggi enabled. Pertanto, linguaggi come
COBOL o JSCRIPT sono stati riveduti nella visione IL e .NET. C#, come detto in precedenza, è
nato in totale sintonia con tale ambiente di esecuzione e con il linguaggio intermedio IL.

Namespace e Classi di base .NET


I namespace (spazi dei nomi) sono una catalogazione delle classi di base .NET e delle classi
personalizzate definite dai programmatori. Le classi simili vengono raggruppate in namespace dal
nome esplicativo, e a loro volta possono contenere altri namespace. La maggior parte delle classi
di base sono inserite nel namespace System. Quando definiamo una classe personalizzata
quest'ultima va inserita in un namespace personalizzato, in caso contrario verrà
automaticamente inserita in un namespace privo di nominativo. La gerarchia dei namespace è
nata al fine di organizzare le classi esistenti e per consentire l'uso di due classi aventi nomi uguali
tra loro. Tale organizzazione favorisce la ricerca tra le migliaia di classi .NET. e se per esempio,
volessimo definire due namespace Tim e Omnitel che accedono alla classe clienti, con strutture
diverse tra loro ma con lo stesso nome, tramite il meccanismo dei namespace si accederà
rispettivamente a Namespace1.clienti() e a Namespace2.clienti(). Questi argomenti verranno
affrontati più in dettaglio durante questo corso. Le funzionalità fornite dalle librerie di classi di
base .NET sono accessibili tramite la dichiarazione dell'opportuno namespace di appartenenza
tramite la direttiva using all'inizio del file .cs (ES: using System.Console; ci permette di
accedere a tutte le classi del namespace Console, e dunque di effettuare operazioni con le
finestre di console...vedremo un primo esempio nel corso della prossima lezione). Tra le varie
possibilità offerte abbiamo: supporto per la programmazione dei WinForm per applicazioni che
fanno uso di interfacce grafiche nello stile di Windows, Web Forms e ASP.NET, accesso ai dati con
ADO.NET, XML, networking, multithreading, riflessione, conversioni tra tipi, grafica con GDI+,
interoperabilità COM, COM+, servizi windows, servizi web (Web Services), ecc... Scopriremo
questi "strumenti" nelle lezioni che trattano dell'applicazione del C# a problematiche di sviluppo
reali.

Molti dei concetti esposti possono sembrare ostici, non preoccupatevi... per il momento l'obiettivo
è quello di avere una sommaria visione di insieme e teorica di cosa accade programmando in C#,
con esempi e lezioni successive verranno chiariti e affrontati a poco a poco. Nella prossima
lezione mostreremo un primo esempio in C#.

LEZIONE 3

Un primo esempio di Antonio Cangiano

Introduzione agli oggetti


La storia dei linguaggi di programmazione è segnata da costanti tentativi di miglioramento.
La programmazione non è certamente un'attività semplice; sono richieste capacità logiche,
matematiche, analitiche, organizzative e la massima precisione. Lo sforzo maggiore sta tuttavia
nel riuscire a riproporre nella logica del linguaggio scelto, un problema reale da risolvere. Un
programmatore in fondo non è altro che uno dei tanti "commercianti di soluzioni". I linguaggi
strutturati (o imperativi) come il C hanno reso l'attività di programmazione più versatile e
semplice sotto molti punti di vista. In luogo delle poche e semplici istruzioni dell'assembly, nei
linguaggi ad alto livello come C o Pascal sono state introdotte istruzioni più complesse che
racchiudono in una sola riga/statement, delle funzionalità che in assembly andavano sviluppate
mediante diverse righe di codice. La grande novità dei linguaggi ad alto livello sta proprio nella
loro astrazione. Sostanzialmente si tratta di utilizzare un linguaggio più simile al modo di pensare
dell'uomo. Quanto sarebbe facile programmare un computer dandogli delle istruzioni in puro
italiano? Sarebbe semplicissimo (per noi italiani naturalmente :-)) perché potremmo sfruttare
l'abilità di astrazione della nostra mente per risolvere ogni sorta di problemi. Voglio un
programma che mi faccia il grafico di una funzione? Basterebbe in tale ipotesi un semplice
listato:

"Disegnami una finestra grande 500x500, traccia il grafico di y= x^2, nel range
che va da -4 a 4 con i puntini gialli e lo sfondo nero"

Voilà, ecco apparire la finestra con il grafico tracciato. Sapreste farlo in assembly? Credo proprio
di no. Sapreste farlo in C? Alcuni di voi forse sì. Ora cerchiamo di analizzare il perché di tutto ciò.
Quando io scrivo la mia riga in italiano, sto facendo una grande astrazione. Io mi sto
disinteressando di come il computer crea la form, come fa a disegnare il grafico, come si creano
gli assi e li si divida in intervalli, come si disegnino i punti in una form, come si setti lo sfondo,
ecc... Non sto guardano a cosa c'è dietro, sto astranendo per ottenere la soluzione al mio
problema. Il linguaggio di programmazione "italiano" è chiaramente irrealizzabile a causa della
sua non unicità di interpretazione e a causa della sua eccessiva complessità e al non costante
rigore logico-matematico; dunque nessun compilatore riuscirà mai a tradurre le nostre istruzioni
in italiano in codice nativo comprensibile al computer. La ricerca però ci spinge alla creazione di
linguaggi che siano sempre più simili al modo di pensare dell'uomo. Il linguaggio C che ha avuto
un grande successo, e che è tuttora usato e piuttosto valido, è dotato di una strutturazione logica
che permette di creare dei programmi anche non banali, e di organizzarli in modo tale da non
compiere uno sforzo eccessivo per "tradurre" in C soluzioni a problemi reali. Un altro merito del
linguaggio C è quello di aver fornito degli strumenti per astrarre secondo una logica più umana
evitando la cosiddetta "programmazione spaghetti"; difatti l'uso di riferimenti a parti di codice
contrassegnate da etichette al fine di ottenere delle iterazioni e dei cicli nella computazione,
rendevano alquanto difficile il processo di manutenzione e la leggibilità/scalabilità dei programmi,
mentre con il C istruzioni di controllo come For o Do While, rendevano molto più evidente al
programmatore quale blocco di codice era ripetuto, il come e il perché. Le motivazioni del suo
successo sono molteplici ma l'astrazione permessa dal C è ancora abbastanza distante
dall'astrazione dell'uomo. Difatti il C puntualizza l'attenzione sulle procedure di esecuzione, sui
metodi, sugli algoritmi, piuttosto che sulle strutture dati da utilizzare. La famosa equazione
ALGORITMI+STRUTTURE DATI = PROGRAMMI in C e nei linguaggi imperativi in genere, ha
una fortissima componenete algoritmica. Le strutture dati offerte dal C si limitano a delle variabili
o dei puntatori che esprimono l'astrazione di contenitori di informazione o riferimenti a
informazioni, ma che sono certamente ancora distanti dalla nostra astrazione. Questo limite si è
evidenziato particolarmente durante la realizzazione di progetti realmente complessi in C. Il
riuscire a proporre nella logica e nel linguaggio C, le soluzioni a problematiche reali delle quali la
mente ha già fornito un'astrazione non è impresa semplice in molti casi ed è l'abilità del
programmatore a fare la differenza. La soluzione a questo problema è stata offerta dalla OOP,
ovvero la programmazione orientata agli oggetti. La mente umana ragiona per oggetti, e
allora linguaggi come C++ e Java... l'assecondano. Per chiarire come la mente interpreti tutto
come un oggetto ed evitando definizioni rigorose e complesse, si ricorra all'esempio di un'auto.
Istruire un guidatore alla guida di un'auto è un'impresa non troppo complessa. Perché? Perché
l'autista deve solamente accedere ad alcune funzionalità dell'oggetto automobile senza essere
obbligato a sapere come sia stata realizzata internamente. Per guidare la macchina non è
necessario conoscerne il funzionamento interno e l'armonia di componenti che interagiscono tra
di loro per permettere di accelerare, frenare, svoltare a destra, a sinistra, ecc... L'automobile è
per l'autista una scatola nera di cui non si conosce quasi nulla, ciò che conta però è l'astrazione
della nostra mente, che vede l'auto come un oggetto dotato di alcune funzionalità (metodi). I
vantaggi di questa maggiore astrazione sono notevoli. Io potrei creare una macchina all'idrogeno
e dotarla degli stessi metodi di una macchina comune. Qualsiasi "guidatore" sarebbe in grado di
fruirne, anche se solo io che sono il costruttore so come è realmente realizzata. Programmare in
C è come istruire un autista alla guida della propria macchina non ragionando in termini di
oggetti ma in termini di azioni sulle singole e minuscole parti (strutture dati di base). Non si dirà
quindi "premi l'acceleratore", perché non si può presupporre che l'acceleratore sia un oggetto.
Bisognerà invece dire "appoggia il piede al disopra di una griglia di 20 quadratini da 1 cm l'uno
collocati all'indirizzo X, fai pressione, lascia che dal serbatoio Y gli indirizzi di memoria che
rappresentano la benzina finiscano nel carburatore che è a sua volta costituito da ... ": insomma,
qualcosa di improponibile. Durante il corso verranno illustrate meglio e in maniera più rigorosa
le varie tecniche implementative dell'astrazione offerta dal C#. Per ora è sufficiente aver capito,
anche solo a grandi linee, che l'astrazione offerta permette di agire su degli oggetti già
precostituiti e su oggetti creabili e personalizzabili all'occorrenza, riducendo lo sforzo e la
possibilità di errori. Il ragionare in termini di object orientation(OO) permette la gestione di
progetti complessi, sviluppati da team di programmatori e l'organizzazione e risoluzione di
problemi apparentemente non risolvibili mediante l'uso del calcolatore. Per questi ed altri motivi
un buon linguaggio di programmazione moderno non può non essere OO, tanto che in C# non è
neppure possibile dichiarare delle funzionalità (metodi) al di fuori di una classe (ovvero il
"modello" di un oggetto). Ci si limiti a considerare un programma come un insieme di oggetti,
con determinate proprietà e funzionalità (metodi) che gli oggetti possono svolgere. Si ricordi in
fine che la OOP dà la precedenza alla struttura dati, lasciando in "secondo ordine" gli algoritmi
che agiscono sui dati, al contrario di C: per questo è fondamentale capire a fondo l'utilizzo delle
classi per programmare bene in C#. Probabilmente vi sembrirà di leggere "aria fritta" per cui è il
caso di iniziare subito con un primo esempio di programma C#, riservandoci di comprendere
pienamente la OOP durante il corso.

Il primo esempio
Il primo esempio, per motivazioni "storiche" è l' "HELLO WORLD!", ovvero un programma che
permette la scrittura in output di una stringa. Diamo subito un'occhiata al codice:

using System; //namespace contenente la classe Console

/* Il mio primo programma


* scritto in C# */
class ClasseHello
{
static void Main()
{
Console.WriteLine("Hello World!");
}
}

Scrivere il codice in un qualsiasi editor (anche il notepad) e denominare il file come: Hello.cs,
compilare con l'istruzione del prompt:

csc Hello.cs

Se il compilatore è correttamente funzionante, verrà generato il file contente il codice IL che


verrà gestito ed eseguito dal CLR:

Hello.exe

A questo punto basterà digitare nel prompt dei comandi Hello, o cliccare in Windows Explorer
sul file Hello.exe per avviare il programma e mobilitare il Common Language Runtime. L'output
di tale programma è mostrato in figura:
Analisi del codice sorgente
Passiamo all'analisi del programma che abbiamo appena scritto.
Alla prima riga del codice sorgente troviamo:

using System; //namespace contenente la classe Console

La direttiva using permette di specificare un namespace contenente le classi necessarie al nostro


programma. Spesso capita di dover indicare più di un namespace, per cui all'inizio del
programma si vedranno una serie di direttive using seguite dal namespace e dal ";". Avremmo
potuto evitare tale dichiarazione se all'interno del codice avessimo inserito l'istruzione:

System. Console.WriteLine("Hello World!");

Tuttavia questa pratica, oltre a rendere molto più lunga l'operazione di digitazione delle
istruzioni, è sconsigliata perché poco intuitiva. Il namespace System è una "raccolta" di classi
molto vasta, ed esso stesso contiene altri namespace ai quali si accede mediante l'operatore ".".
Se si volesse includere ad esempio il namespace Math, appartenente al namespace System,
bisognerebbe inserire all'inizio del programma la direttiva:

using System.Math;

Nel nostro caso abbiamo incluso il namespace System perché contiene la classe Console.
Console è una classe del Framework .NET che fornisce vari metodi per la gestione dell'input e
dell'output. Si noti come le istruzioni in C#, parimenti a C++ e Java, e a differenza di Visual
Basic, siano separate tra loro dal punto e virgola ";". Immediatamente dopo tale ";" nella prima
riga del programma troviamo il commento:

//namespace contenente la classe Console

In C#, come in C++ e Java, i commenti su di una singola riga, possono trovarsi sulla stessa riga
di una istruzione. I commenti di questo tipo iniziano con due barre oblique (slash-slash) "//" e
includono tutti i caratteri sino alla fine della riga, senza necessitare di una chiusura di commento.
Un buon programmatore deve saper commentare i propri programmi per renderli leggibili sia ad
eventuali colleghi, sia a se stesso, nel caso in cui riprenda il codice a distanza di tempo. La regola
è di non eccedere, ma di essere abbastanza chiari nei punti cardine del programma. In C#, come
pure in Java, esiste inoltre il commento su più righe:

/* Il mio primo programma


* scritto in C# */

C'è la possibilità di scrivere dei commenti su più righe mediante i caratteri "/*" di apertura e i
caratteri "*/" di chiusura; si ricordi che non bisogna mai annidare commenti dentro altri
commenti per evitare errori possibili. Esiste un terzo tipo di commento, su riga singola che inizia
con "///", dedicato alla generazione di documentazione Xml. Per approfondimenti si rimanda ad
un mio ariticolo a riguardo, che trovate qui.
Nella riga successiva al commento su più righe troviamo:

class ClasseHello

In C# ogni programma deve avere almeno una classe. Nel nostro caso abbiamo una sola classe
che abbiamo denominato ClasseHello, che viene dichiarata mediante la parola chiave class.
Salvo un uso maldestro di parole chiave riservate, possiamo scegliere un qualsiasi nome per la
classe, seguendo la convenzione secondo la quale l'identificativo della classe inizia con una
lettera maiuscola. Se la classe si fosse chiamata classeHello, anziché ClasseHello, il compilatore
non ci avrebbe segnalato alcun tipo di errore: è semplicemente una questione di stile e di
convenzioni per i nomi degli identificatori. Si usino sempre delle lettere nelle dichiarazioni ed
eventualmente il carattere di underscore "_" perché alcune regole rigide impediscono di
compilare codice avente come primo carattere di un identifier di classe, un numero o altri
caratteri speciali (ES: 1ClasseHello non compila / Classe_Hello compila). Tutto il codice della
classe è racchiuso in un blocco racchiuso dalla parentesi graffa aperta "{" (digitare ALT + 123) e
la parentesi graffa chiusa "}" (digitare ALT + 125). Successivamente troviamo la dichiarazione
del metodo principale, ovvero del punto di inizio del programma. Quando si manderà in
esecuzione l'assembly principale (.exe), la prima riga di codice ad essere eseguita è la prima del
metodo Main():

static void Main()

E' necessario che ogni programma abbia un metodo statico Main(). Esso può essere presente in
una classe o in una struttura (che vedremo in seguito). I programmatori Java e C++ stiano
attenti nello scrivere "Main" con la lettera iniziale maiuscola e non "main" che genererebbe un
errore. Si ignori per il momento il significato della parola "static"; preannuncio solamente ai
programmatori Visual Basic che non ha lo stesso significato che aveva in Visual Basic 6. Il Main()
come detto è la funzione (metodo) principale e il void che lo precede, indica che non deve
restituire nessun valore di ritorno. Avremmo potuto dichiarare anche:

static int Main()

Questa dichiarazione però implica che il metodo Main restituisca un valore di ritorno di tipo int.
Vedremo in seguito i vari tipi di strutture dati del C#, per ora si consideri che il valore di ritorno
deve essere un numero intero con segno, compreso in un range che va da -2.147.483.648 a
2.147.483.647. Se si fa la scelta di dichiarare il Main come un metodo che restituisce un valore
int, prima della chiusura del blocco Main() mediante la parentesi graffa chiusa "}" bisogna
inserire l'istruzione:

return 0;

Il valore di ritorno è arbitrario nel range degli int, ma usualmente si inserisce lo 0, subito dopo
l'istruzione return che specifica il valore di ritorno di un metodo. In fine esiste una terza
possibilità per la dichiarazione del Main:

static void Main(string[] args) o static int Main(string[] args)

"string[] args" indica che args è una variabile di tipo array di stringhe. Esso contiene i
parametri che sono stati passati da riga di comando. Se si fosse scritto ad esempio "hello ciao da
me" nel prompt avremmo nella variabile vettore args i tre valori "ciao", "da", "me". Attenzione
programmatori C/C++, il nome del programma eseguibile (hello.exe nel nostro caso) non viene
incluso nei parametri del Main come accade invece in C/C++.
Passiamo ora all'analisi dell'istruzione:

Console.WriteLine("Hello World!");
Dovendo visualizzare una stringa nella shell a caratteri (vedi illustrazione precedente), dobbiamo
avvalerci della classe Console. Console fornisce vari metodi per disporre in output dei caratteri e
per leggerli da input; nel nostro esempio abbiamo fatto uso di WriteLine. Si noti che
l'invocazione del metodo avviene anteponendo il nome della classe di appartenenza, seguita da
un punto. La funzionalità di WriteLine è quella di mostrare nello standard output uno stream di
caratteri, ovvero la/le stringhe passate come parametro al metodo e di spostare il cursore sulla
riga successiva, in pratica avviene l'inserimento di un carattere di newline. Il passaggio di un
parametro ad un metodo avviene mediante la scrittura dello stesso tra le parentesi tonde che
seguono il metodo. Il parametro del metodo WriteLine in questo caso è la stringa "Hello World!".
Si noti che esiste un metodo piuttosto simile per la scrittura dello standard output, Write,
invocabile scrivendo Console.Write al posto di Console.WriteLine. La differenza sta nel fatto che
Write non inserisce un carattere di newline che manda a capo il cursore lampeggiante dopo aver
scritto la stringa a video. La lettura dell'input avviene attraverso il metodo Read o l'analogo
ReadLine. Di questo e molto altro ancora, parleremo nella prossima lezione.

LEZIONE 4

I tipi valore di Antonio Cangiano

Introduzione ai tipi
Abbiamo già accennato alla celebre ugualianza ALGORITIMO + STRUTTURA DATI =
PROGRAMMA; vi sono diversi tipi di strutture dati adatte alle più svariate esigenze. In .NET e nei
linguaggi di programmazione Object Oriented il concetto di classe o oggetto ha introdotto delle
strutture dati del tutto innovative e astrattive. I tipi di dati, possono essere di due tipi in .NET:
tipi di valore e tipi di riferimento. Entrambi, in ultima analisi, permettono al programmatore di
accedere a determinate aree di memoria mediante un identificatore di variabile o classe, ma ci
sono alcune differenze fondamentali che vedremo a breve. Un qualsiasi programma sensato
dovrà ricorrere all'uso di variabili per la memorizzazione delle informazioni. Vedremo in seguito
come sfruttare questa caratteristica delle variabili e qual'è la sintassi per "utilizzare" tali
strumenti. Si tenga presente che in C# la tipizzazione è molto forte, ovvero è necessario
dichiarare ogni variabile e specificarne il tipo, prima di poterla utilizzare all'interno del
programma. Nulla di nuovo per i programmatori Pascal, C, Java, e simili. Alcuni programmatori
Visual Basic avevano la cattiva abitudine di utilizzare le variabili anche senza averle
preventivamente dichiarate: i risultati erano spesso disastrosi perché si aveva l'impressione di
lavorare con un grande minestrone di variabili e codice. In Visual Basic 6 e precedenti l'Option
Explicit andava dichiarato esplicitamente per imporre la dichiarazione di tutte le variabili prima
dell'uso, in Visual Basic.NET tale opzione è attivata di default, ma è disattivabile (non fatelo!). In
C# tutto ciò non c'è, dovete dichiarare le variabili (fortunatamente) agevolando il compito del
CLR per quanto riguarda la gestione del Garbage Collector, l'interoperabilità tra i linguaggi, e
l'ottimizzazione del codice IL prodotto dal compilatore!

Tipi di Riferimento e Tipi di Valore


Come detto in C# e in .NET esistono molti tipi di variabile (interi, virgola mobile, caratteri,
stringhe, booleani, oggetti...) ma la distinzione più netta è tra tipi di dati valore e di riferimento.
I tipi di valore sono quei tipi che permettono alla variabile di contenere nel proprio spazio di
memoria i dati. Le variabili dichiarate come tipi di riferimento contengono nel proprio spazio di
memoria solamente un indirizzo (riferimento) all'area di memoria contenente i dati veri e propri.
I programmatori Java noteranno la totale somiglianza, i programmatori C++ invece pensino i tipi
di riferimento come dei puntatori ad una variabile. I programmatori Visual Basic li considerino
una sorta di Objects. Le variabili di tipo valore vengono allocate all'interno dell'area di memoria
definita STACK, mentre le variabili o le classi di tipo riferimento vengono allocate all'interno
dell'area di memoria "complementare" allo Stack, ovvero nello HEAP o Managed Heap. Si
tenga presente che un oggetto appartiene sempre a un tipo di riferimento, e che anche le sue
variabili di tipo valore vengono allocate nello Heap anziché nello Stack. IL è caratterizzato da una
gerarchia di tipi ben definiti (il CTS - Common Type System) e da alcune regole (il CLS -
Common Language Specification); il CTS e il CLS permettono l'interoperabilità tra i vari linguaggi
di programmazione .NET. Ogni linguaggio .NET ha dei propri alias che si riferiscono al tipo
definito dal CTS. Ad esempio se dichiaro una variabile di tipo int, mi riferisco in realtà al tipo
System.Int32. Tali alias sono stati introdotti perché più intuitivi rispetto ai tipi del CTS. La parola
"int" è preferibile ad un System.Int32 che sta per tipo di dato intero a 32 bit. Prima di proporvi
un elenco dei vari tipi disponibili per la dichiarazione delle variabili, vediamo brevemente un
esempio di assegnazione.

Tipi di Valore semplici


Vogliamo assegnare il valore 135 alla variabile che identifichiamo come x. La sintassi C# è la
seguente:

int x;

x = 135;

oppure direttamente: int x = 135;

In C# sono consentiti anche delle dichiarazioni multiple:

int x, y, z; //dichiara tre variabili intere x, y e z

e anche degli assegnamenti in fase di dichiarazione per più variabili:

int x = 100, y = 200, z = 5; //dichiara 3 variabili e le inizializza

Come è d'obbligo, veniamo alla carrellata di tipi di valore semplici predefiniti:

Tipo C# Tipo CTS Info Valori assunti


sbyte System.SByte intero 8 -128 a 127
short System.Int16 intero 16 -32768 a 32767
-2^31 a 2^31-1
int System.Int32 intero 32
(2.147.483.647)
-2^63 a 2^63-1
long System.Int64 intero 64
(9.223.372.036.854.775.807)
intero 8
byte System.Byte 0 a 255
unsigned
intero 16
ushort System.UInt16 0 a 65535
unsigned
intero 32
uint System.UInt32 0 a 4.294.967.295
unsigned
char System.Char 16 Caratteri a 16 bit nel set Unicode
Intero 64 0a
ulong System.UInt64
unsigned 18.446.744.073.709.551.615
Virgola
float System.Single ±1,5x10^-45 a ±3,4x10^38
Mobile 32
Virgola
double System.Double ±5,0x10^-324 a ±1,7x10^308
Mobile 64
Virgola
decimal System.Decimal ±1,0x10^-28 a ±7,9x10^28
Mobile 128
bool System.Boolean True o False
Nulla di nuovo o sconvolgente, delle semplici tipologie di variabili atte a contenere informazioni di
ogni tipo "di base". Quando si parla di Unsigned ci si riferisce a tipi che accettano esclusivamente
valori positivi interi. Il tipo decimal ha un intervallo di valori ammessi inferiore al quello del float
o del double, ma in compenso ha un numero di cifre significative maggiori (28 contro le 15 del
double e le 7 del single) ed è particolarmente indicato per applicazioni di tipo finanziario o che
richiedano grande precisione in virgola mobile. Si ricorda in fine che i numeri esadeciamali interi
possono essere rappresentati apponendo il prefisso "0x" (zero x):

int a = 0x1ab21;

ATTENZIONE: I programmatori C/C++ devono fare particolare attenzione al tipo bool


perché in C# non è consentita la conversione implicita dei valori 0 in False e 1 o un
altro intero in True.

L'Assegnamento
L'assegnamento di un valore o di una espressione ad una variabile è possibile mediante
l'operatore binario "=" e può avvenire come già visto o in fase di dichiarazione, o in fase
successiva. Ciò che è importante ricordare è di dichiarare le variabili sempre prima del loro
utilizzo per evitare errori che non ci permetterebbero di superare la fase di compilazione. Spesso
è buona prassi inizializzare le variabili in fase di dichiarazione per effettuare delle operazioni su di
esse successivamente.

int a, b=5, somma;

Quest'istruzione permette dichiara tre variabili intere a, b e somma, di cui esclusivamente b ha


un valore assegnato. Se il programmatore inserisse il seguente codice, riceverebbe un errore:

somma = a + b; //Erorre! a non è stata inizializzata

Se avessimo avuto:

int a, b=5, somma;

a=4;

somma = a + b;

non ci sarebbe stato nessun problema. Ricordatevi dunque di inizializzare le variabili prima di
usarle in espressioni e assegnamenti. E' buona prassi negli assegnamenti diretti con valori
specificare il tipo di dato utilizzando i "letterali" come suffisso, i più comuni sono U per unsigned,
L per long, F per Float, M per Decimal:

uint a = 543U;
long b= 543L;
ulong c = 543UL;
float d = 543.34F; //. e non virgola!!!
decimal e = 123.13M;

Questo è necessario perché altrimenti negli assegnamenti diretti con dei valori dei tipi interi uint,
long e ulong, avremmo conversioni in automatico nel tipo int, il float e il decimal in automatico in
double. Si noti che il separatore decimale è il punto e non la virgola e si noti che le lettere U, L, F
e M possono essere scritte anche in minuscolo (anche se C# è case-sensitive!!!).

I tipi di Valore Complessi


Esistono due tipi di Valore (allocati nello stack) che non sono semplici e vengono detti Complessi
perché "costruiti" liberamente a partire dai tipi semplici: gli enumeratori e le strutture. I vantaggi
dei tipi complessi sono molteplici. Le strutture ad esempio, sono piuttosto simili alle classi, ma
hanno il vantaggio di essere allocate nello stack in quanto tipi di valore; questo significa che
l'allocazione è molto più rapida, come pure la deallocazione automatica, oltre al grande vantaggio
di poter effettuare degli assegnamenti e delle copie allo stesso modo delle variabili di tipo
semplice. L'operatore = permette di assegnare e copiare in maniera diretta le strutture.

Gli enumeratori
Gli enumeratori, o enumerazioni, permettono di definire un insieme di valori interi a cui ci si
riferisce tramite un identificativo. Il vantaggio che ne ricava la leggibilità è chiaro a tutti, come
pure la possibilità di controllare eventuali assegnamenti di valori non leciti; infatti se si cerca di
accedere ad un valore non compreso nell'enumeratore il compilatore solleva un eccezione. Un
altro vantaggio non marginale sta nel fatto che gli utilizzatori di Visual Studio.NET o di
SharpDevelop potranno accedere ai valori dell'istanza dell'enumeratore tramite Intellisense,
ovvero il meccanismo che permette la scelta visuale dei valori possibili. Vediamo un esempio di
dichiarazione di enumeratore per capirne il funzionamento e la sintassi:

public enum Giorno


{
Domenica = 0,
Lunedì = 1,
Martedì = 2,
Mercoledì = 3,
Giovedì = 4,
Venerdì = 5,
Sabato = 6
}

La parola chiave è enum e i valori accettati dall'enumeratore sono separati da virgola e sono
sempre riferiti a valori interi. In un programma che compia determinati calcoli sulla paga dei
dipendenti in base al giorno, è più semplice accedere a Giorno.Sabato piuttosto che impostare e
portarsi dietro per tutto il codice uno sterile 6.

Le strutture
Le strutture, come già detto, sono abbastanza simili alle classi ma sono allocate nello stack e
godono di tutti i vantaggi che questa implementazione fornisce. La copia di un oggetto (allocato
nello heap) in genere è una operazione non semplicissima e richiede la creazione di un
particolare metodo di copia o clonazione, mentre con le strutture l'assegnamento avviene come
per tutti i tipi valore. Una struttura viene definita nel seguente modo:

public struct Utente


{
public string Nome;
public string Cognome;
public int ID;
public string Email;
}

La parola chiave struct è l'equivalente della parola class per le strutture, e in maniera del tutto
analoga possiede dei campi (in questo caso 3 di tipo string e uno di tipo long). Si sorvoli per il
momento sul significato della parola chiave public che permette di definire l'accessibilità della
variabile nel programma. Facciamo un esempio che sfrutta la definizione di struttura appena
creata:

Utente Utente1;
Utente Utente2;

Utente1 = new Utente;

Utente1.Nome = "Antonio";

Utente1.Cognome = "Cangiano";

Utente1.ID = 1;

Utente1.Email = "antonio@visualcsharp.it";

Utente2 = Utente1; // effettua la copia dell'Utente1 in Utente2

Come vedete quella semplice istruzione istanzia una struttura Utente2 del tutto identica a
Utente1.

LEZIONE 5

I tipi Riferimento di Antonio Pelleriti

Introduzione
Nella precedente lezione abbiamo visto che in C# esistono due categorie fondamentali di tipi di
dati: i tipi valore, che abbiamo visto nella scorsa lezione, ed i tipi riferimento.

I tipi valore memorizzano direttamente un valore, mentre una variabile, o meglio un oggetto di
tipi riferimento conserva solo un riferimento a questo valore.

Ad esempio, supponiamo di avere un tipo Cliente (in realtà una classe Cliente come vedremo più
avanti), e istanziamo un oggetto di tale tipo:

Cliente cliente=new Cliente();

la variabile cliente è un riferimento ad un oggetto, oggetto che è conservato nella parte di


memoria detta managed heap. Se adesso dichiariamo una nuova variabile, riferimento ancora ad
un oggetto Cliente, e la inizializziamo così:

Cliente cliente2=cliente;

avremo due variabili che sono un riferimento alla stessa area di memoria, cioè allo stesso
oggetto. Infatti sarà indifferente agire sulla variabile cliente o sulla variabile cliente2.

Quindi il codice:

cliente.nome="Pippo";

Console.WriteLine(cliente.nome);

cliente2.nome="Mario";

Console.WriteLine(cliente.nome);

produrrà in output le due righe:

Pippo
Mario

Questo perchè, sebbene facciamo stampare sempre il valore del campo nome dell'oggetto
cliente, il cambiamento fatto sull'oggetto cliente2 si riflette anche su cliente. Cioè cliente e
cliente2 sono in realtà un riferimento al medesimo oggetto.

Class type
In un linguaggio orientato agli oggetti, il tipo Class è naturalmente un tipo fondamentale.
Vedremo nelle prossime lezioni come creare i nostri tipi personalizzati, cioè le nostre classi. Per
ora basta sapere che una classe è una struttura dati, che può contenere membri (campi,
costanti), funzioni membro (metodi, proprietà, eventi, indexers, operatori, construttori e
distruttori) ed altri tipi.

C# supporta e fornisce due tipi di riferimento predefiniti, che sono anche due tipi Class, il tipo
object ed il tipo string.

Un tipo riferimento non è solamente e necessariamente un tipo class, esso può anche essere uno
di questi altri tre tipi:

• interface
• array
• delegate

ma essendo argomenti un pò più avanzati, li riprenderemo nelle lezioni successive.

Il tipo object
Il tipo object è il tipo padre di tutti i tipi in C#, cioè qualsiasi oggetto deriva, direttamente o
tramite una catena di derivazione più lunga, dal tipo object (anche i tipi valore!).

Il tipo object, che in realtà è solo un alias per il tipo System.Object del Common Type System del
.NET framework, implementa dei metodi generici, che qualsiasi altra classe può utilizzare o
sovrascrivere, tali metodi sono:

Metodo Descrizione
bool Equals Determina se due oggetti sono uguali.
Viene utilizzato come funzione hash per un tipo particolare,
int GetHashCode in modo da ottenere un valore int da un oggetto di un
qualsiasi tipo.
Type GetType Restituisce il tipo Type dell'oggetto
static bool Determina se le istanze obj1 e obj2 rappresentano la stessa
ReferenceEquals istanza.

string ToString
Restituisce un oggetto String che rappresenta l'oggetto
Object corrente.

E' utile fare qualche esperimento con i metodi precedenti, in modo da poterli comprendere
meglio ed utilizzarli proficuamente nelle nostre classi.

Se proviamo a compilare ed eseguire il seguente codice

public class Oggetti {


static void Main()
{
object o1=new object();
object o2=new object();
Console.WriteLine(o1.Equals(o2));

o2=o1;
Console.WriteLine(o1.Equals(o2));

o1=new object();
Console.WriteLine(o1.Equals(o2));
}
}

otterremo le tre righe di output seguenti, una per ogni confronto:

• false --> o1 ed o2 sono due istanze diverse della classe object;


• true --> o2 è ora un riferimento alla stessa istanza o1;
• false --> o1 è ora un riferimento ad una nuova istanza;

ora con l'istruzione:


Console.WriteLine(o1.GetType());

otterremo l'output System.Object, infatti il tipo di o1 è object, che come detto è un alias per la
classe System.Object.

Notate che se invece della precedente, eseguiamo:

Console.WriteLine(o1);

oppure Console.WriteLine(o1.ToString());

otteniamo ancora lo stesso output. Questo significa che il metodo ToString() restituisce una
rappresentazione in formato stringa di o1, che nel caso della classe System.Object restituisce
proprio il nome del tipo.

Il metodo ToString viene in genere sovrascritto dalle classi personalizzate in modo da ottenere
una stringa che contenga il maggior numero possibile di informazioni utili su un oggetto della
classe stessa.

Ad esempio nel caso dei tipi valore, come il tipo int, il metodo ToString permette di ottenere il
valore numerico stesso, in formato stringa.

Il tipo string

Il tipo string rappresenta una sequenza di caratteri Unicode. Anche string è un alias per un
tipo del CTS, per la precisione System.String, classeche deriva direttamente da System.Object.

Una stringa può essere dichiarata, inizializzata e stampata in modo molto semplice ed
immediato:

string str="una stringa";

System.Console.WriteLine(str);

La classe System.String possiede un sacco di metodi molto utili, che impareremo ad utilizzare
nella prossima lezione.
Lezione 6 - Conversioni di tipo
di Antonio Pelleriti

Spesso è necessario convertire un tipo di dato in un altro. Il linguaggio C#


consente di effettuare questa operazione, in due modi diversi, in maniera
implicita ed in maniera esplicita.

Conversioni implicite

Con il termine conversione implicita intendiamo che la conversione del tipo in un altro tipo
avviene in maniera automatica e trasparente e indolore. Infatti per effettuare tale tipo di
conversione è sufficiente specificare l'assegnazione ad una variabile di un certo tipo di un valore
di un tipo diverso. Naturalmente non sempre ciò è possibile.

Consideriamo il codice:

int i=10;
byte b=i;

nonostante il tipo byte possa contenere il valore 10, il compilatore segnala un'errore, dicendo che
non è possibile convertire implicitamente il tipo int in byte, questo perchè in generale, il tipo int
consente di memorizzare valori più grandi, e l'esempio precedente è un caso particolare. Invece:

const int i=10;


byte b=i;

compila correttamente, perchè a tempo di compilazione si sa già che i assume il valore 10, ed
esso quindi può essere implicitamente convertito in byte, mentre le istruzioni:

const int i=10000;


byte b=i;

non verranno compilate, perchè 10000 è un valore troppo grande per entrare in un byte.

Vediamo ora un esempio un pò più particolare:

byte b1=1;
byte b2=2;
byte somma=b1+b2; //è possibile?

1+2 dovrebbe restituire 3, quindi un valore facilmente convertibile in byte, ma il compilatore


segnala che è impossibile convertire un int in un byte. Evidentemente la somma ( o meglio
l'operatore +) nel sommare le due variabili di tipo byte, le converte in int e restituisce quindi
ancora una variabile int.

In generale, comunque, la regola per verificare che una conversione implicita è possibile è quella
di verificare che il tipo da convertire sia rappresentato da un numero di bit minore o uguale a
quelli del tipo destinazione. La seguente tabella contiene le conversioni implicite permesse in C#:

Dal tipo Al tipo


sbyte short, int, long, float,double, decimal
short, ushort, int, uint, long, ulong, float, double,
byte
decimal
char ushort, int, uint, long, ulong, float, double, decimal
short int, long, float, double, decimal
ushort int, uint, long, ulong, float, double, decimal
int long, float, double, decimal
uint long, ulong, float, double, decimal
long float, double, decimal
ulong float, double, decimal
float double

Conversioni esplicite

Oltre ai casi delle conversioni implicite, possibili fra i tipi sorgente e destinazione specificati nella
tabella precedente, esistono altri casi di conversioni, che il compilatore non accetta se non
specificando la nostra intenzione di "rischiare" la conversione.

Ad esempio convertire una variabile short in un int non è implicitamente possibile:

int i=10;
short s=i; //errore, conversione implicita impossibile

ma se siamo sicuri che la variabile i assumerà valori sempre convertibili in short, allora possiamo
forzare la conversione, cioè richiedere la conversione in modo esplicito verso il tipo short:

int i=10;
short s=(short) i;
Console.Writeline("s="+s);//output-> s=10

naturalmente potrebbe accadere che i abbia un valore troppo grande:

int i=100000000;
short s=(short) i;
Console.Writeline("s="+s);//output-> s=-7936

il valore di i supera il valore massimo ammissibile per un int ed anche se in compilazione non
abbiamo problemi, il valore assunto da s in esecuzione non è certamente quello che ci saremmo
aspettati.

C# fornisce l'operatore checked per gestire tali situazioni:

short s=checked( (short) i );

in questo modo, se la conversione esplicita non fosse possibile perchè i assume un valore troppo
grande, verrà lanciata a run-time un 'eccezione a segnalare l'overflow.

Naturalmente l'eccezione dovrebbe essere catturata e gestita, ma questo lo vedremo più avanti
nel corso.

boxing ed unboxing

Abbiamo visto nelle lezioni precedenti, che ogni tipo di .NET, è un tipo derivato dal tipo padre
object, o meglio System.Object.

Ad esempio int è in realtà un alias della struttura System.Int32, che deriva dalla classe
ValueType, la quale a sua volta è una figlia della classe System.Object.
Questo suggerisce il fatto che possiamo trattare ogni tipo, anche quelli valore, come se fossero
tipi riferimento.

int i=10;
Console.Writeline(i.Equals(10));

Ma come è possibile questo comportamento di un tipo valore? Per mezzo delle operazioni di
boxing e unboxing, che consentono di convertire un tipo valore in un tipo riferimento, e
viceversa. In pratica un tipo valore viene "inscatolato" in un tipo riferimento, e quindi è possibile
trattarlo come un object.

Viceversa, se vogliamo "scartare" il tipo valore nascosto nell'object basta fare una conversione
esplicita:

object o=i;
int i=(int) o;

Naturalmente l'ultima conversione è possibile, solo se l'oggetto o contiene un int, oppure un tipo
convertibile in int, altrimenti verrebbe scatenata un'eccezione a run-time.

Lezione 7- Gli Array (I Parte)


di Antonio Pelleriti

Cos'è un array
Un array è una sequenza di elementi, tutti dello stesso tipo. In italiano la parola che meglio
identifica il concetto di array è vettore, ad esempio:

vettore=[ 1 , 2 , 3 , 4];

Un vettore può anche avere più dimensioni, una matrice è un vettore a due dimensioni, cioè con
elementi disposti per righe e per colonne.

C#, così come la maggior parte dei linguaggi (facciano attenzione i programmatori Visual Basic),
utilizza per il tipo array la notazione a parentesi quadre []. Cioè un array viene dichiarato facendo
seguire al tipo le parentesi quadre e quindi il nome di una variabile che ci servirà ad identificare
l'array e quindi utilizzarlo.

Ad esempio il seguente codice dichiara un array di numeri interi:

int[] numeri;

ma quanti elementi possono essere inseriti nel vettore interi? Per dare la dimensione utilizziamo
l'operatore new, anticipando il fatto che in C# ogni array è in realtà un oggetto della classe
System.Array, e quindi un tipo riferimento.

numeri=new int[5];

l'istruzione precedente indica al compilatore di allocare lo spazio sufficiente a contenere 5


variabili di tipo int. Naturalmente l'utilizzo degli array non è limitato ad elementi di tipo valore,
cioè possiamo anche lavorare con array di oggetti. Ad esempio,

MiaClasse[] oggetti=new MiaClasse[10];


dichiara ed inizializza un array di 10 oggetti, istanze della classe MiaClasse, ma ancora non
abbiamo parlato di oggetti, nè di classi, quindi ci torneremo più avanti.

L'operatore []

Torniamo al nostro array monodimensionale di interi. Per accedere agli elementi è in genere
utilizzato l'operatore [], specificando all'interno delle parentesi l'indice, cioè la posizione
dell'elemento, cui vogliamo accedere, sia in lettura che in scrittura. E' importante ricordare che
gli elementi sono numerati a partire da 0 (e non è possibile variare tale valore, come ad esempio
in VB), quindi è possibile accedere agli elementi dell'array nella seguente maniera:

using System;
class Test
{
static void Main()

{
int[] numeri = new int[5];

Console.WriteLine("arr[0] = {0}", arr[0]);

Console.WriteLine("arr[1] = {0}", arr[1]);

Console.WriteLine("arr[2] = {0}", arr[2]);

Console.WriteLine("arr[3] = {0}", arr[3]);

Console.WriteLine("arr[4] = {0}", arr[4]);


}
}

Il codice precedente stampa gli elementi dell'array numeri, accedendovi uno alla volta. Un pò
scomodo, ed è legato al fatto di conoscere a priori la dimensione dell'array, ma per le nostre
prime prove va bene.

Il codice precedente stamperà cinque 0, perchè gli elementi, essendo tipi valore, vengono
inizializzati al valore di default del tipo, in questo caso 0. Ma sempre con l'operatore [] possiamo
modificare i valori degli elementi:

arr[0] = 0
arr[1] = 1
arr[2] = 4
arr[3] = 9
arr[4] = 16

Se proviamo ancora a stampare gli elementi dell'array, magari con un ciclo for, otteniamo i valori
da noi impostati.

for(int i=0;i<5;i++)

Console.WriteLine("arr[{0}]={1}",i,arr[i]);

E' possibile inizializzare gli elementi di un array mediante una lista di inizializzatori, separati dalla
virgola e racchiusi fra parentesi graffe, naturalmente del tipo definito per l'array, ad esempio, per
inizializzare l'array precedente con i valori visti sopra possiamo anche scrivere una sola
istruzione:

int[] arr=new int[]{ 0, 1, 4, 9, 16};


Il contesto in cui viene usato l'inizializzazione precedente determina il tipo degli elementi stessi,
ad esempio i numeri precedenti possono essere considerati anche di tipo short o long, dunque è
possibile scrivere:

short[] a = {0,1,4,9,16};
int[] b = {0,1,4,9,16};
long[] c = {0,1,4,9,16};

cioè gli stessi inizializzatori sono usati per differenti tipi di array.

L'istruzione foreach

L'istruzione foreach permette di scorrere sequenzialmente tutti gli elementi di un array (ma
anche di altri tipi di collezioni, come vedremo nelle future lezioni), dal primo all'ultimo.

La sintassi dell'istruzione è la seguente:

foreach(tipo variabile in array) blocco-istruzioni

L'esempio seguente scorre tutti gli elementi di un array di interi e ne calcola e stampa i quadrati:

int [] arr=new int[]{ 1 , 2, 3, 4, 5};

foreach(int i in arr)

int quadrato=i*i;

Console.WriteLine("{0}^2={1}",i,quadrato);

E' comunque importante specificare che all'interno del blocco di istruzioni non è possibile tentare
di assegnare un valore alla variabile di iterazione, inq uesto caso i, pena un errore di
compilazione. Se abbiamo bisogno di far ciò, dobbiamo ricorrere ad un classico ciclo come il for o
un while.

Nella prossima lezione vedremo gli array multidimensionali, e la classe System.Array, che
fornisce una serie di metodi e proprietà utili nel trattamento degli array.

Lezione 8- Gli Array (II Parte)


di Antonio Pelleriti

Introduzione

Abbiamo visto nella prima parte della lezione cos'è un array, come crearne uno, e come accedere
ai suoi elementi. Abbiamo anche accennato alla classe che il framework .NET fronisce per trattare
gli array, vale a dire la classe System.Array, ed agli array multidimensionali, continuiamo quindi
proprio da qui.
Array multidimensionali

Un array monodimensionale, come visto nella precdente lezione, è una sequenza di elementi di
un certo tipo. Se immaginiamo di disporre vari array monodimensionali su più righe otteniamo
degli array a più dimensioni. Un esempio classico, per chi comprende un pò di matematica, sono
le matrici.

Array rettangolari

C# supporta gli array multidimensionali in due modi. Il primo modo di intendere un array
multidimensionale è un array rettangolare, appunto una matrice, in cui se ad esempio
consideriamo un array rettangolare a due dimensioni, abbiamo diverse righe, ognuna delle quali
contiene sempre lo stesso numero di elementi, è come avere cioè una tabella con mrighe ed n
colonne:

Come si dichiara ed inizializza una simile struttura in c#? Più facile a farsi che a dirsi.
Supponiamo di volere dichiarare una matrice di double ( ma naturalmente vale per ogni tipo),
basta dichiarare una variabile matrice così:

double[,] matrice;

se vogliamo inizializzare la variabile, ad esempio per realizzare una matrice


quadrata con 5 righe e 5 colonne basta scrivere adesso:

matrice=new double[5,5];

è lo stesso procedimento vale anche se il numero di righe è diverso dal numero di colonne.

Come per gli array monodimensionali è possibile dichiarare e contemporaneamente inizializzare


l'array con i valori che vogliamo, utilizzando ancora le parentesi graffe, ma stavolta in modo
annidato, ad esempio:

double[,] matrice2={{1, 2, 3},


{4, 5, 6} };

Per accedere agli elementi della matrice possiamo ancora utilizzare l'operatore [], ad esempio
per stampare il primo elemento della prima riga, tenendo bene a mente che gli indici iniziano da
zero, possiamo semplicemente scrivere l'istruzione

System.Console.WriteLine(matrice[0,0]);

ed allo stesso modo si possono settare i valori dell'array. E' semplice ad esempio inizializzare la
matrice in modo da avere tutti i valori a 1, utilizzando un ciclo for annidato:

double[,] matrice=new double[3,4];


for(int i=0;i<3;i++)
for(int j=0;j<4;j++)
matrice[i,j]=1;
Jagged array

Il secondo tipo di array multidimensionale supportato da C# è il cosiddetto jagged array. Con


questo termine si intende un array ancora a più dimensioni, ma ogni dimensione può anche
avere un nunero diverso di elementi, nel caso di due dimensioni è come avere una matrice con
ogni riga di diversa lunghezza.

Uno jagged array è in pratica un array di array, ed è proprio in tale maniera che è più facile
pensarlo e quindi utilizzarlo. Ad esempio se vogliamo dichiarare un array multidimensionale di
stringhe in cui la prima riga ha tre stringhe, la seconda quattro e la terza cinque dobbiamo prima
dichiarare una variabile così:

string[][] jaggedString=new string[3][];

abbiamo così specificato che la variabile jaggedString è un array a due dimensioni , con tre righe.
Adesso specifichiamo anche di quanti elementi, cioè di quante stringhe, è composta ogni riga,
utilizzando la sintassi che abbiamo visto nella prima lezione ( infatti ogni riga è un array
monodimensionale):

jaggedString[0]=new string[3];
jaggedString[1]=new string[4];
jaggedString[2]=new string[5];

L'accesso ad uno jagged array avviene naturalmente utilizzando la stessa sintassi, sia in lettura
che in scrittura, cioè sia per ricavare il valore di un elemento che per settarlo:

jaggedStrings[0][0]="primo elemento";
Console.WriteLine("jaggedStrings[0][0]="+jaggedStrings[0][0]);
jaggedStrings[2][4]="ultimo elemento";
Console.WriteLine("jaggedStrings[2][4]="+jaggedStrings[2][4]);

La classe System.Array

La classe System.Array costituisce la classe base per ogni array in .NET, sia esso
monodimensionale, ma anche multidimensionale. La classe System.Array fornisce una serie di
metodi di una utilità notevole, anzi necessari se vogliamo lavorare seriamente con gli array.

Negli esempi visti fino adesso, abbiamo sempre lavorato con lunghezze e dimensioni fisse dei
nostri array, stabiliti da noi in fase di inizializzazione. ma se a run-time, ad esempio, volessimo
conoscere tali dimensioni in modo da accedere correttamente agli elementi in un ciclo for? O se
ad esempio volessimo ordinare gli elementi in ordine crescente? La classe System.Array viene in
nostro soccorso con dei metodi e delle proprietà in grado di risolvere queste e altre
problematiche, molti di questi sono metodi statici, ed anticipiamo che ciò significa che sono
metodi di classe, e non di una signola istanza (cioè di ogni singolo array). E' come avere cioè dei
metodi generici utilizzabili per ogni array.

Rank

La proprietà Rank ci consente di conoscere il numero di dimensioni dell'array:

int[] mono=new int[3];


int[,] bidim=new int[5,3];
int[,,] tridim=new int[3,4,5];
Console.WriteLine("mono ha dimensione {0}",mono.Rank);
Console.WriteLine("bidim ha dimensione {0}",bidim.Rank);
Console.WriteLine("tridim ha dimensione {0}",tridim.Rank);

Le tre istruzioni Console.WriteLine stamperanno le dimensioni 1, 2 e 3 rispettivamente.


Length e GetLength()

La proprietà Length ottiene il numero totale di elementi in tutte le dimensioni di un array:

int[,] numeri=new int[5,3];

è una matrice 5x3, cioè ha in totale 15 elementi, infatti se eseguiamo l'istruzione:

int elementi=numeri.Length;

otteniamo il valore 15. Se invece vogliamo conoscere una per una le dimensioni di ogni riga,
possiamo utilizzare il metodo GetLength(int dim) che prende come argomento l'indice della
dimensione di cui vogliamo la lunghezza:

Console.WriteLine("La prima riga della matrice numeri ha {0}


elementi",numeri.GetLength(0));

Possiamo quindi utilizzare il metodo GetLength per inizializzare gli elementi di un array con un
ciclo for:

for(int i=0;i<numeri.GetLength(0);i++)
for(int j=0;j<numeri.GetLength(1);j++)
numeri[i,j]=i*j;

GetLowerBound e GetUpperBound

I metodi GetLowerBound e GetUpperBound restituiscono i limiti inferiore e superiore di una


dimensione dell'array, ad esempio per un array bidimensionale di 3 per 5 elementi, le istruzioni
seguenti

string[,] s=new string[3,5];

for(int i=0;i<numeri.Rank;i++)
{
Console.WriteLine("rank{0}:indice inf={1} e sup={2}", i, numeri.GetLowerBound(i),
numeri.GetUpperBound(i));
}

stamperanno per la prima dimensione i limiti [0,2] e per la seconda [0,4].

BinarySearch

Il metodo statico BinarySearch implementa la ricerca binaria in un array. Essendo un metodo


statico il suo utilizzo è un pò diverso, infatti una delle possibili sintassi è la seguente:

int Array.BinarySearch(arr,elem)

in tale maniera si ricerca all'interno dell'array arr l'elemento elem, e viene restituito l'indice
dell'elemento se è stato trovato, altrimenti un numero negativo, ad es:

int[] altriNumeri={ 1,2,3,4,5,6,7,8 };


int nIndex=Array.BinarySearch(altriNumeri,4);
Console.WriteLine("4 si trova all'indice {0}",nIndex);
nIndex=Array.BinarySearch(altriNumeri,10);
if(nIndex<0)
Console.WriteLine("Il numero 10 non c'è");

Clear
Il metodo statico Clear serve a cancellare gli elementi, tutti o un intervallo, di un array.
Cancellare significa impostare a 0 dei numeri, a false dei boolean, o a null, altri tipi di oggetti.

La sintassi è la seguente

public static void Clear(


Array array,
int index,
int length
);

in cui il parametro array indica l'array da cancellare, index è l'indice a partire dal quale
cancellare, e length il numero di elementi da cancellare, ad esempio se abbiamo un array di 5
numeri interi:

int[] numeri={ 1,2,3,4,5};

Array.Clear(numeri,2,2);

l'istruzione Clear in questo caso cancellerà gli elementi 3,4, cioè 2 elementi a partire dall'indice 2.

Copy

Il metodo statico Copy, ricopia gli elementi di un array in un secondo array:

public static void Copy(


Array src,
Array dest,
int length
);

in cui il parametro src è l'array sorgente, dest l'array destinazione, e length il numero di elementi
da copiare.

int[] altriNumeri={ 1,2,3,4,5};


int[] arrCopia=new int[altriNumeri.Length];

Array.Copy(altriNumeri,arrCopia,altriNumeri.Length);
for(int i=0;i<altriNumeri.Length;i++)
Console.Write("{0,3}",altriNumeri[i]);

In output otterremo naturalmente gli stessi elementi dell'array sorgente.

Sort

Per ordinare gli elementi di un array in modo semplice e veloce, possiamo utilizzare il metodo
statico Sort, che prende come argomento l'array da ordinare.

string[] str={"a","c","f","e","d","b"};
for(int i=0;i<str.Length;i++)
Console.Write("{0,3}",str[i]);

Array.Sort(str);

Console.WriteLine("Dopo l'ordinamento");
for(int i=0;i<str.Length;i++)
Console.Write("{0,3}",str[i]);

Dopo la chiamata a Sort gli elementi verranno stampati in ordine alfabetico.


Reverse

Infine vediamo in questa lezione il metodo statico Reverse, che serve ad invertire l'ordine degli
elementi di un array, si usa come il precedente Sort, passando cioè come argomento l'array da
invertire. Se ad esempio dopo aver ordinato l'array precedente, effettuiamo ora la chiamata:

Array.Reverse(str);

e stampiamo gli elementi, vedremo che l'ordine è adesso decrescente, cioè dalla f alla a.

Esistono ancora altri metodi facenti parte della classe System.Array, e quelli visti in questa
lezione, possono anche essere chiamati con altri tipi di argomenti, ma lascio a voi come esercizio
di provarli, armati di MSDN e di compilatore C#.

Lezione 9 - Dichiarazioni e variabili


di Antonio Pelleriti

Un programma C#, così come un programma scritto in ogni altro linguaggio di programmazione,
ha dei ben precisi elementi che lo costituiscono. Gli elementi di un programma, prima di essere
usati, devono essere dichiarati, cioè è necessario assegnare loro un nome univoco.

Molti dei concetti che vedremo in questa lezione sembreranno forse una ripetizione, ma è bene
fare un approfondimento prima di passare ad argomenti nuovi.

Variabili

Uno dei mattoni fondamentali di un programma, è la variabile. E' infatti praticamente impossibile
scrivere un programma che abbia una qualche utilità pratica, senza fare uso delle variabili.Ed in
effetti ne abbiamo già fatto uso nelle lezioni precedenti. Adesso però è ora di presentarle in modo
ufficiale, in modo da togliere ogni dubbio, o almeno buona parte dei dubbi, ed in modo da farne
l'uso migliore possibile.

Una variabile rappresenta uno spazio di memoria, in cui conservare qualcosa.

Ogni variabile quindi ha un tipo, che indica che cosa possiamo conservare nella zona di memoria
che essa rappresenta, ed ha un nome, che viene utilizzato per chiamare proprio quella zona di
memoria. Tale nome è in genere chiamato identificatore.

Dichiarazione

La dichiarazione di una variabile avviene scrivendo per primo il nome del tipo seguito
dall'identificatore, ad esempio per dichiarare una variabile di tipo int che abbia nome numero,
basta scrivere:

int unNumero;

Sicuramente tutti i lettori avranno visto una istruzione come la precedente ma cerchiamo di
entrare più nel dettaglio, in modo da capire cosa succede effettivamente quando scriviamo
qualcosa del genere, in modo da iniziare anche a pensare da veri programmatori.

Abbiamo detto che la variabile rappresenta una zona di memoria, in questo caso abbimo
"prenotato" una locazione sufficientemente grande a contenere un numero di tipo int.
In .NET, int è un alias del tipo System.Int32, che rappresenta un numero intero a 32 bit. Quindi
la dichiarazione precedente indica al compilatore di riservare esattamente 32 bit di memoria, e di
chiamare questa zona unNumero:

unNumero [32 bit]

Inizializzazione ed assegnazione

Naturalmente, la locazione di memoria è utile se la riempiamo con qualcosa, in questo caso con
un numero intero. Questa operazione si chiama inizializzazione della variabile, ed avviene per
mezzo dell'operatore =. Quando scriviamo

unNumero=10;

o contemporaneamente alla dichiarazione

int unNumero=10;

non facciamo altro che inizializzare, termine più appropriato nel caso del primo utilizzo della
variabile, o in genere assegnare il valore 10 alla variabile unNumero, cioè riempiamo la locazione
di memoria chiamata unNumero con il valore 10:

unNumero 10

Identificatori

L'assegnazione dei nomi, cioè degli identificatori, segue anch'essa delle regole ben precise,
regole innanzitutto di buon senso, ad esempio se abbiamo bisogno di una variabile che
memorizza la temperatura, sarebbe opportuno assegnarle un nome significativo, ad esempio

float temperatura;

float temp;

evitando di usare nomi magari troppo lunghi, e nomi troppo corti e dal significato ingannevole ( a
meno che non vogliamo intenzionalmente rendere il codice illeggibile).

Esistono poi regole dettate dalla grammatica del linguaggio stesso. Riassumendo brevemente tali
regole,senza essere esaustivi un identificatore è formato da una sequenza di caratteri Unicode,
sia lettere che numeri, ma deve necessariamente iniziare con una lettera oppure con
l'underscore.

Ad esempio, sono identificatori validi:

identificatore1;

_identificatore2;

ma è possibile anche utilizzare le sequenze di escape che identificano un carattere mediante il


codice Unicode, in generale \uXXXX, ad esempio il codice corrispondente all'unicode è /u005f.

Non è possibile naturalmente usare le keyword del linguaggio come identificatore, ma se proprio
ne avete voglia, o ad esempio sperimentando l'interoperabilità dei linguaggi .NET vi imbattete in
un identificatore che per un linguaggio costituisce una parola chiave ma per altri no, potete usare
i cosiddetti identificatori verbatim, cioè la keyword stessa preceduta dal carattere @. E' per
esempio lecito, ma non mi sento di consigliarlo, dichiarare un identificatore come @if, oppure
@else o ancora @class.

Scope e durata

Affrontiamo ora un'altra questione fondamentale, cioè dopo che abbiamo dichiarato ed
inizializzato una variabile, dove e per quanto tempo possiamo usarla?

Una variabile è valida nel suo spazio di dichiarazione o blocco, cioè in una parte di codice
delimitata da { e }, ed è vietato dichiarare due identificatori uguali all'interno dello stesso blocco.
Con il termine Scope si intende lo spazio di codice nel quale è lecito utilizzare un identificatore,
cioè lo spazio in cui esso è valido. Date un'occhiata all'esempio seguente:

int i=10;
... qui possiamo utilizzare i

}//qui i non è più valida

Quando, come nell'esempio precedente, la variabile è dichiarata all'interno di un blocco essa si


dice locale al blocco.

E' possibile avere più blocchi annidati :

int i=0;

int j=0;

//qui è valida anche i;

//qui è valida solo i

Non è possibile invece dichiarare in un blocco interno una variabile con lo stesso nome di una
variabile definita nel blocco esterno, questo costituisce una novità per i programmatori C/C++, in
cui invece una simile operazione è possibile:

int i=0;

int i=10; //errore di compilazione, i darebbe un diverso significato alla i


esterna;

}
Il concetto di durata temporale di una variabile è ancora strettamente legato a quello di scope
della variabile, anzi spesso coincide, come negli esempi precedenti, in cui una variabile locale
smette di vivere quando viene raggiunta la fine del blocco. Sarà necessario fare delle precisazioni
in altri casi che incontreremo più avanti nel corso, ad esempio anticipiamo che la durata di una
variabile di classe è legata alla vita dell'oggetto di cui fa parte, e dunque non muore ad esempio
quando muore il blocco di codice che costituisce un metodo della classe.

Lezione 10- Espressioni ed operatori


di Antonio Pelleriti

Per scrivere un programma un pò più complesso dobbiamo essere in grado di valutare delle
espressioni, ad esempio in base al valore assunto da una variabile possiamo decidere se
intraprendere una via oppure un'altra. Un'espressione è una sequenza formata da operandi
inframezzati dagli operatori che il linguaggio mette a disposizione.

Le espressioni possono essere costruite in vari modi, ma nella quasi totalità dei casi, esse devono
poter essere valutate, cioè devono restituire un valore ben preciso.

Operatori

Come detto sopra un'espressione è costituita da operandi, ad esempio delle variabili o altre
espressioni nel caso di espressioni complesse, e da operatori, che indicano come valutare
un'espressione in base a regole dettate da ogni operatore.

Ad esempio l'operatore + applicato a due operandi, ad esempio due variabili a e b, indica che il
valore dell'espressione è dato dalla somma degli operandi a e b.

Se poi scriviamo l'espressione:

c=a+b;

oltre ad indicare che vogliamo effettuare la somma degli operandi a e b, usando l'operatore di
assegnazione = stiamo dicendo che il valore dell'espressione a+b deve essere assegnato
all'operando c.

C# supporta tre tipi di operatori:

• operatori unari: si applicano ad un solo operandi;


• binari: si applicano a due operandi;
• ternario: ne esiste uno solo, applicabile a tre operandi.

Precedenza ed associatività

Quando nelle espressioni più complesse appaioni più operatori è necessario ricorrere a delle
regole per stabilire quale operazione effettuare per prima, in maniera analoga a come abbiamo
imparato alle elementari per valutare un'espressione come la seguente:

a+b*c

Sappiamo che la moltiplicazione viene prima della somma, e quindi potremmo raggruppare le
due operazioni usando delle parentesi:
a+(b*c)

Che rende più chiaro l'intento di chi ha scritto l'espressione. Anche in C# è naturalmente
possibile usare le parentesi per lo stesso scopo, ma è bene conoscere le prima citate regole di
precedenza. La tabella seguente riassume tutti gli operatori del linguaggio, raggruppati per
categoria ed ordinati per precedenza, cioè andando dalla prima riga verso l'ultima abbiamo prima
gli operatori con precedenza maggiore, e quindi via via decrescente.

NB: Molti degli operatori non li abbiamo nemmeno visti da lontano, ma nelle prossime puntate li
impareremo ad utilizzare tutti quanti.

Categoria Operatori
x.y a[x] f(x) x++ x-- typeof checked
primari unchecked new
unari + - ! ~ ++x --x (T)x
moltiplicativi * / %
additivi + -
shift << >>

confronto e type testing < > <= >= is as


uguaglianza == !=
AND logico &
XOR logico ^
OR logico |
AND condizionale &&
OR condizionale ||
ternario ?:
assegnazione = *= /= %= += -= <<= >>= &= ^= |=

Nel caso in cui però all'interno della stessa espressione compaiano più operandi con uguale
precedenza è necessario ricorrere alle regole di associatività.

Ad eccezione dedi quelli di assegnazione, tutti gli operatori binari sono associativi a sinistra, cioè
le operazioni vengono effettuate da sinistra verso destra, ad esempio nell'espressione:

a/b*c

abbiamo due operatori binari con medesimo livello di precedenza e quindi verrà valutata a partire
da sinistra, cioè prima l'operazione / e poi la moltiplicazione *.

Gli operatori di assegnazione e l'operatore ternario ?= sono associativi a destra, ad esempio:

a=b=c

Nell'espressione precedente viene prima assegnato il valore di c a b, e quindi quello di b ad a:

a=(b=c)

Come suggerimento in caso di indecisione possiamo dire che conviene usare le parentesi per
risolvere eventuali ambiguità e per evitare di non farci capire da chi deve leggere il nostro codice.
Cominciamo adesso a vedere l'utilizzo pratico ed il funzionamento dei vari operatori

Operatori unari

Gli operatori unari vengono utilizzati in notazione prefissa, cioè l'operatore precede l'operando,
che deve essere di un tipo numerico (a meno di non sovraccaricare l'operatore, ma questo lo
vedremo più avanti nel corso)

• l'operatore +

l'operatore + restituisce semplicemente il valore dell'operando a cui viene applicato:

int x=1;
int y=-1;
int a= +x;//assegna ad a il valore 1
Console.WriteLine("a="+a);
int b= +y;//assegna a b il valore -1
Console.WriteLine("b="+b);

• l'operatore -

l'operatore - restituisce invece il valore dell'operando invertito di segno, o se preferite il


valore dell'operando sottratto al valore 0:

int x=1;
int y=-1;
int a= -x;//assegna ad a il valore -1
Console.WriteLine("a="+a);
int b= -y;//assegna a b il valore +1
Console.WriteLine("b="+b)

• l'operatore !

l'operatore ! è l'operatore di negazione, e si applica ad un operando di tipo booleano. Esso


restituisce il valore negato dell'operando, cioè se l'operando o lespressione che segue
l'operatore ! ha valore true, verrà restituito il valore false e viceversa:

bool b=true;
bool c= !b;//assegna a c il valore false
Console.WriteLine("c="+c);
bool d= !c;//assegna a d il valore opposto di c
Console.WriteLine("d="+d);

• L'operatore ~

L'operatore tilde è l'operatore di complemento bit a bit dell'operando a cui viene applicato,
in parole povere esso inverte i bit 1 in bit 0 e viceversa. L'esempio seguente applicato ad
un operando di tipo byte chiarisce tutto:

byte bb=10;//in binario 0000 1010


byte cc=(byte)~bb;// 1111 0101 = 245
Console.WriteLine("cc="+cc);