[APPTXT] Function Ptr, Delegati, Eventi - Numero 0
Chi ha paura degli eventi ? Perchè, in fondo, cos'hanno fatto di male ?
D'altra parte si sa che spesso si ha paura di qualcosa se non la si conosce !
E dopo essere entrato negli annali per questa frase "leggermente" scontata, passiamo alla parte tecnica del tutto: cosa sono in realtà gli "eventi" in .NET ?
A dire il vero le domande sono state, molto più spesso: "A cosa servono ?", oppure, visto che in fondo qualche evento le classi di sistema lo espongono, "Ma li devo usare anch'io ?".
Iniziamo subito col dire che nulla si "deve usare". Una buona idea però sarebbe quella di comprendere gli strumenti che si hanno a disposizione, in modo tale che essi si "possano usare", valutandono preventivamente i benefici e le opportunità di utillizzo.
Ovviamente gli eventi non sfuggono a questa regola.
Una cosa che, per la mia esperienza almeno, li ha sempre resi ostici da capire (e da spiegare) è il meccanismo che i rende possibili: cosa che è resa ancora più difficile dalle differenze di background tecnico delle persone.
Mi spiego: quante volte un articolo/capitolo/contenuto sui delegati (poi vedremo come i delegati sono correlati agli eventi) li introduce come "puntatori a funzione type-safe" ? Non critico la correttezza o meno di questa definizione, solamente il fatto che per dedurne qualcosa di utile da aprendere bisognerebbe avere una buona conoscenza di:
- Puntatori
- Funzioni
- Type-Safe
Non è così banale come sembra.
E comunque, per farla breve, diverse volte mi sono trovato a spiegare i meccanismo di eventi del .NET Framework, ma non sempre i concetti di base erano così ben chiari.
Scopo del post (di questo e dei prossimi correlati): provare a descrivere il meccanismo degli eventi partendo dal giurassico ... niente paura comunque, l'assembler ce lo teniamo per un'altra volta :-)
Ready, set, go.
Ci siamo noi, programmatori, che scriviamo il nostro codice nel nostro linguaggio di programmazione preferito (rimaniamo nell'ambito dei linguaggi "classici", per dirla in modo corretto "imperativi", per dirla in modo semplice: C, C++, Pascal, Java, VB, VB.NET, C# eccetera).
Tipicamente avremo suddiviso il codice di cui sopra in "funzioni". A questo livello di discussione i metodi delle classi rientrano nella categoria "funzioni".
Compiliamo e produciamo un file di output (dll, exe, ...).
All'interno del file (binario) ci sono zone che contengono il "codice" della funzione: una sequenza ordinata di istruzioni che ne rappresentano il flusso di esecuzione.
Queste istruzioni possono essere la rappresentazione binaria dell'instruction set del linguaggio macchina (x86, ad esempio). Questo avviene se compiliamo un'applicazione nativa (in C, ad esempio) con un compilatore che produce codice macchina.
In alternativa queste istruzioni possono essere la rappresentazione binaria delle istruzioni IL (Intermediate Language, il linguaggio della macchina virtuale .NET). Usando un compilatore C# (VB.NET, eccetera) questo è quello che si ottiene.
In termini più generali, il compilatore traduce il nostro codice sorgente in codice eseguibile da un qualche sistema di esecuzione: il CLR, la JVM, la CPU, ...
Il fatto che, poi, il codice eseguibile da una macchina virtuale come il CLR debba essere tradotto in codice eseguibile da un processore ... beh ... non c'è forse un altro compilatore di mezzo (il Just In Time compiler del CLR) ? In questi termini il codice IL diventa il "sorgente" per il Jitter, che infine produce x86 (o qualunque altro tipo di codice eseguibile sulla macchina target).
Benissimo.
In ogni caso questo "codice eseguibile" è ovviamente una cosa solo in potenza, finchè qualcuno non lo esegue !
Lanciamo l'applicazione. Se ci limitiamo al mondo Windows, l'OS crea un processo (un'area di memoria e un bel po' di informazioni sullo stesso) e copia all'interno dell'area di memoria relativa il contenuto dell'eseguibile che abbiamo prodotto.
Di fatto, la nostra funzione "Somma" ora è costituita, sì, da una serie ordinata di istruzioni, che in fase di esecuzione risiedono però in memoria ad un certo indirizzo, essendovi state "mappate" a partire dal file fisico (il .exe, .dll, ...).
Un puntatore a funzione contiene questo indirizzo.
Ora, immaginate il caso in cui una funzione (A) richiami un'altra funzione (B).
"Richiamare" significa, in fin dei conti, che A manda in esecuzione il codice di B. Di fatto, chi mantiene un riferimento al codice in esecuzione al momento (nelle architetture x86 è un registro) vede che il proprio valore cambia: dentro ci andiamo a mettere l'indirizzo della funzione B.
La cosa non è del tutto corretta e ci sono eccezioni, ma teniamocela per buona.
Una domanda, ora.
Chi sa qual è l'indirizzo della funzione B ?
In altri termini, se nel codice sorgente che abbiamo scritto è stata effettuata una classica chiamata di funzione, ci possiamo immaginare che il compilatore sia stato in grado di indicare l'indirizzo giusto nel momento in cui ha "emesso" l'istruzione di chiamata (la CALL x86, la call-callvirt in IL).
I problema nasce quando questo indirizzo non è noto a priori.
Ma quando mai ??
Beh, per esempio se A si aspetta che qualcuno, dall'esterno e durante l'esecuzione, gli indichi la funzione da chiamare.
Pensatela in questi termini.
A è un algoritmo molto complicato che lavora su dati di diverso tipo (la versione .NET della cosa è che lavora con delle interfacce, se volete, o con delle classi abstract). Ad un certo punto, A ha la necessità di fare un confronto fra due dati.
Poichè A è generico (lavora su dati diversi, potenzialmente anche non noti nel momento in cui A stato scritto), A non ha la minima idea di come poter fare il confronto !!
B è invece una funzione che fa, appunto, il confronto fra due dati di tipo X.
Quando A viene utilizzato per elaborare dati di tipo X è buona cosa che chiami la funzione B, che è deputata proprio a questo.
E come fa A a sapere che deve invocare la funzione B ? Ma non avevamo detto che ...
Infatti, A non lo sa, e qualcuno glielo deve dire.
Così come passiamo ad A dei dati di input di tipo X, allo stesso modo passiamo ad A un "puntatore alla funzione B".
La chiamata (A-->B) avverrà lo stesso, ma in via assolutamente dinamica !
Mi rendo conto di come questa descrizione sia piuttosto semplicistica, però spero riesca a porre un minimo di basi per comprendere meglio come, nel .NET Framework, questi concetti sono implementati.
Enter the world of delegates :-)
(... to be continued ...)