Guida al Visual Basic .NET
Capitolo 44° - La Reflection Parte I
Con il termine generale di reflection si intendono tutte le classi del Framework che permettono di accedere o manipolare assembly e moduli. Assembly
L'assembly è l'unità logica più piccola su cui si basa il Framework .NET. Un assembly altro non è che un
programma o una libreria di classi (compilati in .NET). Il Framework stesso è composto da una trentina di assembly principali
che costituiscono le librerie di classi più importanti per la programmazione .NET (ad esempio System.dll, System.Drawing.dll,
System.Core.dll, eccetera...).
Il termine Reflection ha un significato molto pregnante: la sua traduzione in italiano è alquanto lampante e significa "riflessione". Dato che viene usata per ispezionare, analizzare e controllare il contenuto di assembly, risulta evidente che mediante reflection noi scriviamo del codice che analizza altro codice, anche se compilato: è una specie di ouroboros, il serpente che si morde la coda; una riflessione della programmazione su se stessa, appunto. Lasciando da parte questo intercorso filosofico, c'è da dire che la reflection è di gran lunga una delle tecniche più utilizzate dall'IDE e dal Framework stesso, anche se spesso questi meccanismi si svolgono "dietro le quinte" e vengono mascherati per non farli apparire evidenti. Alcuni esempi sono la serializzazione, di cui mi occuperò in seguito, ed il late binding. Late Binding
L'azione del legare (in inglese, appunto, "bind") un identificatore a un valore viene detta binding: si esegue un binding, ad esempio,
quando si assegna un nome a una variabile. Questo consente un'astrazione fondamentale affinché il programmatore possa comprendere
ciò che sta scritto nel codice: nessuno riuscirebbe a capire alcunché se al posto dei nomi di variabile ci fossero degli
indirizzi di memoria a otto cifre. Ebbene, esistono due tipi di binding: quello statico o "early", e quello dinamico o "late".
Il primo viene effetuato prima che il programma sia eseguito, ed è quello che permette al compilatore di tradurre in linguaggio
intermedio le istruzioni scritte in forma testuale dal programmatore. Quando assegnamo un nome ad una variabile, o richiamiamo un metodo
da un oggetto stiamo attuando un early binding: sappiamo che quell'identificatore è logicamente legato a quel preciso valore di
quel preciso tipo e che, allo stesso modo, quel nome richiamerà proprio quel metodo da quell'oggetto e, non, magari, un metodo
a caso disperso nella memoria.
Il secondo, al contrario, viene portato a termine mentre il programma è in esecuzione: ad esempio, richiamare dei metodi d'istanza di
una classe Person da un oggetto Object ? un esempio di late binding, poiché solo a run-time, il nome del membro verrà letto,
verificato, e, in caso di successo, richiamato. Tuttavia, non esiste alcun legame tra una variabile Object e una di tipo Person, se non
che, a runtime, la prima potrà contenere un valore di tipo Person, ma questo il compilatore non può saperlo in anticipo
(mentre noi sì).
Esiste un unico namespace dedicato interamente alla reflection e si chiama, appunto, System.Reflection. Una delle classi più importanti in questo ambito, invece, è System.Type. Quest'ultima è una classe molto speciale, poiché ne esistono molte istanze, ognuna unica, ma non è possibile crearne di nuove. Ogni istanza di Type rappresenta un tipo: ad esempio, c'è un oggetto Type per String, uno per Person, uno per Integer, e via dicendo. Risulta logico che non possiamo creare un oggetto Type, perchè non sarebbe associato ad alcun tipo e non avrebbe motivo di esistere: possiamo, al contrario, ottenere un oggetto Type già esistente. I ContestiPrima di iniziare a vedere come analizzare un assembly, dobbiamo fermarci un attimo a capire come funziona il sistema operativo a livello un po' più basso del normale. Questo ci sarà utile per scegliere una modalità di accesso all'assembly coerente con le nostre necessità.Quasi ogni sistema operativo è composto di più strati sovrapposti, ognuno dei quali ha il compito di gestire una determinata risorsa dell'elaboratore e di fornire per essa un'astrazione, ossia una visione semplificata ed estesa. Il primo strato è il gestore di processi (o kernel), che ha lo scopo di coordinare ed isolare i programmi in esecuzione racchiudendoli in aree di memoria separate, i processi appunto. Un processo rappresenta un "programma in esecuzione" e non contiene solo il semplice codice eseguibile, ma, oltre a questo, mantiene tutti i dati inerenti al funzionamento del programma, ivi compresi variabili, collegamenti a risorse esterne, stato della CPU, eccetera... Oltre ad assegnare un dato periodo di tempo macchina ad ogni processo, il kernel separa le aree di memoria riservate a ciascuno, rendendo impossibile per un processo modificare i dati di un altro processo, causando, in questo modo, un possibile crash di entrambi i programmi o del sistema stesso. Questa politica di coordinamento, quindi, rende sicura e isolata l'esecuzione di un programma. Il CLR del .NET, tuttavia, aggiunge un'ulteriore suddivisione, basata sui domini applicativi o AppDomain o contesti di esecuzione. All'interno di un singolo processo possono esistere più domini applicativi, i quali sono tra loro isolati come se fossero due processi differenti: in questo modo, un assembly appartenente ad un certo AppDomain non può modificare un altro assembly in un altro AppDomain. Tuttavia, come è lecito scambiare dati fra processi, è anche lecito scambiare dati tra contesti di esecuzione: l'unica differenza sta nel fatto che questi ultimi sono allocati nello stesso processo e, quindi, possono comunicare molto più velocemente. Così facendo, un singolo programa può creare due domini applicativi che corrono in parallelo come se fossero processi differenti, ma attraverso i quali è molto più semplice la comunicazione e lo scambio di dati. Un semplice esempio lo potrete trovare osservando il Task Manager di Windows quando ci sono due finestre di FireFox aperte allo stesso tempo: notere che vi è un solo processo firefox.exe associato. Caricare un assemblyUn assembly è rappresentato dalla classe System.Reflection.Assembly. Tutte le operazioni effettuabili su di esso sono esposte mediante metodi della classe assembly. Primi fra tutti, spiccano i metodi per il caricamento, che si distinguono dagli altri per la loro copiosa quantità. Esistono, infatti, ben sette metodi statici per caricare od ottenere un riferimento ad un assembly, e tutti offrono una modalità di caricamento diversa dagli altri. Eccone una lista:
Nome dell'assembly e analisi superficialeUna volta ottenuto un riferimento ad un oggetto di tipo Assembly, possiamo usarne i membri per ottenere le più varie informazioni. Ecco una breve lista delle proprietà e dei metodi più significativi:
[Nome Principale], Version=a.b.c.d, Culture=[Cultura], PublicKeyToken=[Token]Il nome principale è determinato dal programmatore e di solito indica il namespace principale contenuto nell'assembly. La versione è un numero di versione a quattro parti, divise solitamente, in ordine, come segue: Major (numero di versione principale) , Minor (numero di versione minore, secondario), Revision (numero della revisione a cui si è giunti per questa versione), Build (numero di compilazioni eseguite per questa revisione). Il numero di versione indica di solito la versione del Framework per cui l'assembly è stato scritto: se state usando VB2005, tutte le versioni saranno uguali o inferiori a 2.0.0.0; con VB2008 saranno uguali o inferiori a 3.5.0.0. Culture rappresenta la cultura in cui è stato scritto l'assembly: di solito è semplicmente "neutral", neutrale, ma nel caso in cui sia differente, influenza alcuni aspetti secondari come la rappresentazione dei numeri (sepratori decimali e delle migliaia), dell'orario, i simboli di valuta, eccetera... Il token della chiave pubblica è un insieme di otto bytes che identifica univocamente la chiave pubblica (è una sua versione "abbreviata"), la quale identifica univocamente l'assembly. Viene usato il token e non tutta la chiave per questioni di lunghezza. Ecco un esempio che ottiene questi dati: Module Module1 Sub Main() 'Carica un assembly per soli scopi di analisi. 'mscorlib è l'assembly più importante di 'tutto il Framework, da cui deriva pressochè ogni 'cosa. Data la sua importanza, non ha dipendenze, 'perciò non ci saranno problemi nel risolverle. 'Se volete caricare un altro assembly, dovrete usare 'uno dei metodi in grado di risolvere le dipendenze. Dim Asm As Assembly = Assembly.ReflectionOnlyLoad("mscorlib") Dim Name As AssemblyName = Asm.GetName Console.WriteLine(Asm.FullName) Console.WriteLine("Nome: " & Name.Name) Console.WriteLine("Versione: " & Name.Version.ToString) Console.WriteLine("Cultura: " & Name.CultureInfo.Name) 'Il formato X indica di scrivere un numero usando la 'codifica esadecimale. X2 impone di occupare sempre almeno 'due posti: se c'è una sola cifra, viene inserito 'uno zero. Console.Write("Public Key: ") For Each B As Byte In Name.GetPublicKey() Console.Write("{0:X2}", B) Next Console.WriteLine() Console.Write("Public Key token: ") For Each B As Byte In Name.GetPublicKeyToken Console.Write("{0:X2}", B) Next Console.WriteLine() Console.WriteLine("Processore: " & _ Name.ProcessorArchitecture.ToString) Console.ReadKey() End Sub End ModuleCon quello che abbiamo visto fin'ora si potrebbe scrivere una procedura che enumeri tutti gli assembly presenti nel contesto corrente: Sub EnumerateAssemblies() Dim Asm As Assembly Dim Name As AssemblyName 'AppDomain è una variabile globale, oggetto singleton, da cui 'si possono trarre informazioni sull'AppDomain corrente o 'crearne degli altri. For Each Asm In AppDomain.CurrentDomain.GetAssemblies Name = Asm.GetName Console.WriteLine("Nome: " & Name.Name) Console.WriteLine("Versione: " & Name.Version.ToString) Console.Write("Public Key Token: ") For Each B As Byte In Name.GetPublicKeyToken Console.Write(Hex(B)) Next Console.WriteLine() Console.WriteLine() Next End Sub
C#, TypeScript, java, php, EcmaScript (JavaScript), Spring, Hibernate, React, SASS/LESS, jade, python, scikit, node.js, redux, postgres, keras, kubernetes, docker, hexo, etc...
|