Esplora E-book
Categorie
Esplora Audiolibri
Categorie
Esplora Riviste
Categorie
Esplora Documenti
Categorie
INTRODUZIONE AL C#
Introduzione
Per parlare di C# cominciamo a dire che gestire la memoria è un problema, perché biso-
gna rilasciarla e accedere a essa solo quando è “valida”. Per questo motivo hanno avuto
grande successo i linguaggi gestiti, fatti in modo tale che la memoria non sia sotto il di-
retto controllo del programmatore, ma di un qualche sistema – come il garbage collector
– che si occupa di rilasciarla nel momento opportuno. Quindi Microsoft ha provato a fare
una sua implementazione di Java, ma Sun si è un po’ incazzata. Perciò ha accantonato la
cosa creando C#, che ricalca solo l’idea di Java.
C# supporta astrazioni a livello più elevato rispetto a quelle di C/C++. L’oggetto non è
caratterizzato solo in termini di metodi e campi, ma ha anche altre cose:
• proprietà, sembrano variabili istanza ma sono dei metodi
• eventi, un misto tra una variabile che contiene un dato e un puntatore a funzione
• attributi, che in realtà sono metadati e assomigliano alle annotazioni in Java
A differenza di quello che succede negli altri linguaggi dove ciò che si compila diventa
un blocco .obj o un .class, qui nasce un eseguibile che prende il nome di assembly.
Quest’ultimo è un pacchetto entro-contenuto ed autodescrittivo che non richiede l’uso di
linguaggi ad hoc per la descrizione delle interfacce e contiene dentro di sé sia il pro-
gramma che un insieme di metadati (versione, copyright, certificati associati, documen-
tazione...). Perché ciò sia possibile, l’esecuzione non avviene direttamente sul processo-
re specifico ma su una virtual machine (“compile once, run everywhere”).
Dato che C# è arrivato dopo ha risolto tutta una rogne che c’erano in Java, dove i tipi
elementari non sono oggetti: questo implica l’aggiunta di una serie di complicazioni nel-
la gestione di liste, tra cui l’introduzione di classi wrapper. Questi ultimi però non sono
mutabili.
Contemporaneamente a C# sono stati ripensati una serie di altri linguaggi: da Visual Ba-
sic ad esempio è nato .NET. Il tutto è stato ridisegnato al fine di garantire un alto livello
di interoperabilità (es.: programmi con interfaccia in C#, business logic in C++ e acces-
so al database in VB) e con altri standard come XML.
Architettura .NET
Com’è che funziona tutta la baracca? Alla base di tutto c’è
Windows, con i suoi meccanismi. Sopra Windows c’è una
macchina virtuale che prende il nome di CLR: Common
Language Runtime, la quale conosce i modelli elementari
del linguaggio (tipi, eccezioni, debug, compilazione JIT).
Al di sopra di questo strato c’è il framework che è formato da un insieme di classi strut-
turate gerarchicamente e facenti capo alla classe System.Object. Queste classi servono
a gestire I/O, stringhe, strutture dati complesse, ecc.
Su questo strato elementare che consente di creare applicazioni general purpose, c’è
uno strato che consente la gestione dei dati in basi dati. C’è dunque tutto un insieme di
package che ci permettono di esportare da e verso diversi tipi di basi dati.
In cima a tutto c’è lo strato applicativo, molto differenziato a seconda di quello che vo-
gliamo usare (interfacce vecchie, nuove o per applicazioni web)
Managed code
Il codice è detto managed perché l’esecuzione avviene nella macchina virtuale e gli og-
getti hanno un ciclo di vita gestito automaticamente (siamo noi a decidere quando na-
scono, ma è il sistema a decidere quando muoiono). Nel sistema è inglobata la gestione
delle eccezioni.
Il linguaggio C#
C# è un linguaggio ad oggetti: la cosa più piccola creabile è una classe, quindi per scri-
vere un main() dobbiamo fare come in Java. E’ fortemente tipato, quindi non si può dire
“sì, questa è una stringa ma usala come intero”. Ha dei tipi più deboli (come i puntatori)
da utilizzare a nostro rischio e pericolo.
Come Java è ad ereditarietà semplice, quindi non è lecito derivare da più classi. Tutte le
classi derivano da System.Object. I metodi per default sono polimorfici: se nella classe
Base è definito un metodo m che viene ridefinito nella classe Derivata, è sempre usata la
definizione contenuta in quest’ultima anche se l’utilizzatore pensa che l’oggetto appar-
tenga alla classe Base. Ogni oggetto contiene un puntatore alla classe a cui appartiene,
quindi è possibile sapere la classe da cui deriva secondo il meccanismo della reflection:
è inoltre possibile sintetizzare via codice nuove classi, secondo il meccanismo dell’emit.
Così come Java è costruito sui pattern di programmazione, anche C# fa la stessa cosa:
troviamo ad esempio la gestione di file con gli stream, i meccanismi di iterazione, la ge-
stione delle risorse, ecc.
using System;
/* Non è necessario dichiarare degli args. Main va con la maiuscola */
class Hello {
public static void Main() {
Console.WriteLine("Hello World!");
}
}
In ciascuna di queste due DLL è presente una funzione _CorExeMain, che inizializza
l’ambiente d’esecuzione, compila JIT il metodo Main (scritto in CIL dentro Hello.exe) in
un buffer e salta a questo buffer.
Vale la pena spendere due parole sulle differenze tra Win32 e .NET per quanto riguarda
l’unità di esecuzione. In Win32 l’unità di esecuzione è il processo che contiene uno spa-
zio di indirizzamento, in .NET è un AppDomain: un singolo processo che contiene una
macchina virtuale può contenere molte applicazioni separate. I thread sono divisi in
gruppi e ogni gruppo non può inficiare gli altri.
Sintassi del linguaggio
Commenti XML
Facendo precedere ogni riga da un triplo slash (///) siamo in grado di aggiungere docu-
mentazione al linguaggio in dialetto XML.
class Element {
/// <summary>
/// Returns the attribute with the given name and
/// namespace</summary>
/// <param name="name">
/// The name of the attribute</param>
/// <param name="ns">
/// The namespace of the attribute, or null if
/// the attribute has no namespace</param>
/// <return>
/// The attribute value, or null if the attribute
/// does not exist</return>
/// <seealso cref="GetAttr(string)"/>
///
public string GetAttr(string name, string ns) {
...
}
}
Istruzioni ed espressioni
Circa le stesse che si hanno in C/C++/Java. Ci sono alcune lievi differenze:
• lo switch in quasi tutti gli altri linguaggi ha un comportamento fall-through (se
non scrivo break si continua a fare tutto il resto), in C# no
• non è possibile fare goto da un blocco a un altro blocco
• esiste l’istruzione foreach (Variabile in Contenitore) che automatizza l’analisi
di tutti gli elementi in un contenitore che implementa l’interfaccia System.Ienume-
ration
Tipi di dato
Ogni dato ha ovviamente un tipo. In C# anche ogni variabile ha un tipo (in Javascript ad
esempio non è così) predeterminato: il compilatore si fa carico di garantire che a quella
variabile siano assegnati dati coerenti col tipo statico noto alla variabile.
I tipi sono organizzati in una gerarchia di ereditarietà semplice con radice in System.Ob-
ject. Vengono distinti due rami:
• tipi valore, che contengono direttamente il dato, pensati per essere ospitati (an-
che) dentro lo stack; non possono valere NULL e quando vengono copiati si effet-
tua una copia del valore
• tipi reference, che contengono un puntatore al valore situato nell’heap gestito
(quello soggetto a garbage collection); possono valere NULL e quando vengono
copiati si effettua una copia del puntatore
Nell’esempio, int i causa la predisposizione di un
blocco nello stack in cui è contenuto direttamente il
valore. L’istruzione string s causa invece l’allocazio-
ne di un blocco nell’heap gestito: la variabile s cono-
sce solo il puntatore all’oggetto.
Tipi predefiniti
Numerici interi Con segno: sbyte, short, int, long
Senza segno: byte, ushort, uint, ulong
Numerici reali float, double, decimal
Non numerici char (formato Unicode), bool
Riferimento object base di tutti i tipi (System.Object)
string sequenza immutabile di caratteri Unicode (System.String)
Strutture
E’ possibile la costruzione di tipi valore come le struct che assomigliano sintatticamente
alle classi (possono avere campi, metodi e costruttori), ma non supportano l’ereditarie-
tà. Intasano un po’ di più lo stack, ma non richiedendo l’uso dello heap non generano
problemi di garbage collection.
Classi
Le classi sono organizzate in una gerarchia di
ereditarietà semplice e possono implementare
molte interfacce. Ogni classe ha un nome e un
package (che qui è chiamato namespace).
All’interno della classe si possono dichiarare
elementi public, protected, private, internal.
Le proprietà sono elementi presenti in una classe che sintatticamente vengono usati
come campi, ma che in realtà consistono in una coppia di metodi getter e setter. Da un
punto di vista pratico una proprietà si appoggia su una variabile privata dello stesso
tipo. In definitiva sono metodi che permettono di accedere a una variabile privata come
se fosse pubblica.
Dal C++ viene ripreso il concetto di operator overloading, che permette di utilizzare gli
operatori nei modi più disparati. L’indicizzatore effettua l’overloading di operator[]: in
questo modo si può far apparire un oggetto come un array, senza che questo lo sia.
Interfacce
Un’interfaccia è una classe completamente astratta. Una singola classe può implemen-
tare molte interfacce.
interface IDataBound {
void Bind(IDataBinder binder);
}
class EditBox: Control, IDataBound {
void IDataBound.Bind(IDataBinder binder) {...}
}
Nelle interfacce è lecito inserire, oltre ai metodi, anche proprietà, indicizzatori ed eventi.
L’implementazione del pattern “callback” richiede una certa quantità di codice sia in
C++ (usando liste di puntatori a funzione) che in Java (usando liste e interfacce liste-
ner). Quando si creano le interfacce grafiche servono un sacco di callback, quindi biso-
gna che il giochino funzioni bene e sia facile.
Per semplificare la vita, in C# è stato introdotto il tipo delegato che serve a implementa-
re il meccanismo della callback. Internamente mantiene delle liste di coppie (this, meto-
do da chiamare) e il tipo del delegato mi specifica la signature del metodo da inserire
dentro la lista. Su un oggetto di tipo delegato mi posso registrare, indicando la mia iden-
tità e il metodo da chiamare desiderato.
Il tipo delegato è eseguibile! Quando eseguo un tipo delegato vado da tutti quelli che
sono dentro chiamandoli, facendo sì che tutti facciano qualcosa.
namespace PDSLezM37 {
class Program {
delegate void TipoDelegato(int i); /* 1) Dichiarazione simil-prototipo */
static void ProvaLui(int n) {
Console.WriteLine("Prova (" + n + ")");
}
La dichiarazione del tipo delegato è più o meno come un prototipo di funzione, con un
tipo di ritorno, un nome e una lista di parametri formali. Normalmente il tipo di ritorno è
void.
I metodi sono chiamati in ordine. Se uno degli n chiamati scatena un’eccezione si blocca
tutto e l’eccezione viene mandata al pezzo di codice che invoca il delegato (surrounda-
bile con try/catch).
Esattamente come in C++ è lecito costruire delle funzioni lambda assegnabili a istanze
di delegati o di eventi. La sintassi ( () => {}) è simile a quella del C++. Gli elementi tra
parentesi tonde vengono catturati per riferimento e il ciclo di vita è prolungato perché le
variabili sono promosse sullo heap.
Attributi
Gli attributi permettono l’aggiunta di metadati al codice sorgente, che vengono scritti
tra parentesi quadre vicino al nome dei metodi.
Input/Output
Molto più ganzo rispetto a quello del C. La classe astratta Stream costituisce la principale
astrazione e rappresenta una sequenza di byte che si possono leggere o scrivere. Si la-
vora sempre con una qualche classe concreta
Per l’uso di flussi di testo si possono usare le classi TextReader e TextWriter che permet-
tono rispettivamente di leggere e scrivere un flusso di caratteri, uno alla volta. Da que-
ste derivano classi concrete per leggere da stream o stringhe.
Interfacce grafiche
Cambiano radicalmente il modo in cui si utilizza il programma, perché l’utente non se-
gue più il flusso dettato dal calcolatore. Di conseguenza, la comunicazione da e verso
l’applicazione avviene servendosi dei controlli disegnati sullo schermo: ciascun elemen-
to presente nell’interfaccia offre un numero limitato di funzionalità (nel bottone non pos-
so scriverci, la casella di testo non è cliccabile), in modo che i singoli oggetti siano alta-
mente specializzati. In compenso il tutto si complica, proprio perché non posso prevede-
re l’ordine dei comandi dettato dall’utente.
E’ dunque il programma a reagire agli eventi esterni cooperando col sistema operativo.
L’applicazione deve prima di tutto comporre le videate e in seguito preparare delle routi-
ne da eseguire, invocate automaticamente dal sistema
quando si verifica qualcosa di particolare.
Siccome non è prevedibile quale azione compia l’utente in quale momento bisogna se-
guire un pattern di programmazione reattiva. Il nostro programma deve dunque dire in
anticipo cosa deve succedere (e in ciò gli eventi diventano essenziali). In relazione a un
evento si esegue un po’ di codice e recarsi eventualmente dal GUI Manager per ridise-
gnare il tutto.
Le modalità con cui queste operazioni vengono effettuate, dipendono dal sistema opera-
tivo e/o dagli eventuali strati software intermedi adottati.
***