Questo sito utilizza cookies, anche di terze parti, per mostrare pubblicità e servizi in linea con il tuo account. Leggi l'informativa sui cookies.
Username: Password: oppure
Guida al Visual Basic .NET - La Reflection Parte I

Guida al Visual Basic .NET

Capitolo 44° - La Reflection Parte I

<< Precedente Prossimo >>


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 Contesti

Prima 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.

FirefoxAppDomains1.jpg
FirefoxAppDomains2.jpg


Caricare un assembly

Un 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:
  • Assembly.GetExcecutingAssembly()
    Restituisce un riferimento all'assembly che è in esecuzione e dal quale questa chiamata a funzione viene lanciata. In poche parole, l'oggetto che ottenete invocando questo metodo si riferisce al programma o alla libreria che state scrivendo;
  • Assembly.GetAssembly(ByVal T As System.Type) oppure T.Assembly()
    Restituiscono un riferimento all'assembly in cui è definito il tipo T specificato;
  • Assembly.Load("Nome")
    Carica un assembly a partire dal nome completo o parziale. Ad esempio, si può caricare System.Xml.dll dinamicamente con Assembly.Load("System.Xml"). Restituisce un riferimento all'assembly caricato. "Nome" può anche essere il nome completo dell'assembly, che comprende nome, versione, cultura e token della chiave pubblica. La chiave pubblica è un lunghissimo codice formato da cifre esadecimali che identificano univocamente il file; il suo token ne è una versione "abbreviata", utile per non scrivere la chiave intera. Vedremo tra poco una descrizione dettagliata del nome di un assembly.
    Se un assembly viene caricato con Load, esso diviene parte del contesto di esecuzione corrente, e inoltre il Framework è capace di trovare e caricare le sue dipendenze da altri file, ossia tutti gli assembly che servono a questo per funzionare (in genere tutti quelli specificati nelle direttive Imports). In gergo, quest'ultima azione si dice "risolvere le dipendenze";
  • Assembly.LoadFrom("File")
    Carica un assembly a partire dal suo percorso su disco, che può essere relativo o assoluto, e ne restituisce un riferimento. Il file caricato in questo modo diventa parte del contesto di esecuzione di LoadFrom. Inoltre, il Framework è in grado di risolverne le dipendenze solo nel caso in cui queste siano presenti nella cartella principale dell'applicazione;
  • Assembly.LoadFile("File")
    Agisce in modo analogo a LoadFrom, ma l'assembly viene caricato in un contesto di esecuzione differente, e il Framework non è in grado di risolverne le dipendenze, a meno che queste non siano state già caricate con i metodi sopra riportati;
  • Assembly.ReflectionOnlyLoad("Nome")
    Restituisce un riferimento all'assembly con dato Nome. Questo non viene caricato in memoria, poichè il metodo serve solamente a ispezionarne gli elementi;
  • Assembly.ReflectionOnlyLoadFrom("File")
    Restituisce un riferimento all'assembly specificato nel percorso File. Questo non viene caricato in memoria, poichè il metodo serve solamente a ispezionarne gli elementi.
Gli ultimi due metodi hanno anche un particolare effetto collaterale. Anche se gli assembly non vengono caricati in memoria, ossia non diventano parte attiva dal dominio applicativo, purtuttavia vengono posti in un altro contesto speciale, detto contesto di ispezione. Quest'ultimo è unico per ogni processo e condiviso da tutti gli AppDomain presenti nel processo.


Nome dell'assembly e analisi superficiale

Una 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:
  • Fullname : restituisce il nome completo dell'assembly, specificando nome, cultura, versione e token della chiave pubblica;
  • CodeBase : nel caso l'assembly sia scaricato da internet, ne restituisce la locazione in formato opportuno;
  • Location : restituisce il percorso su disco dell'assembly;
  • GlobalAssemblyChace : proprietà che value True nel caso l'assembly sia stato caricato dalla GAC;

    Global Assembly Cache (GAC)
    La cartella fisica in cui vengono depositati tutti gli assembly pubblici. Per assembly pubblico, infatti, s'intende ogni assembly accessibile da ogni applicazione su una determinata macchina. Gli assembly pubblici sono, solitamente, tutti quelli di base del Framework .NET, ma è possibile aggiungerne altri con determinati comandi. La GAC di Windows è di solito posizionata in C:WINDOWSassembly e contiene tutte le librerie base del Framework. Ecco perchè basta specificare il nome dell'assembly pubblico per caricarlo (la cartella è nota a priori).
  • ReflectionOnly : restituisce True se l'assembly è stato caricato per soli scopi di analisi (reflection);
  • GetName() : restituisce un oggetto AssemblyName associato all'assembly corrente;
  • GetTypes() : restituisce un array di Type che definiscono ogni tipo dichiarato all'interno dell'assembly.
Prima di terminare il capitolo, esaminiamo le particolarità del nome dell'assembly. In genere il nome completo di un assembly ha questo formato:
[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 Module
Con 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 


<< Precedente Prossimo >>
A proposito dell'autore

Programmatore e analista .NET 2005/2008/2010 (in particolare C# e VB.NET), anche nell'implementazione Mono per Linux. Conoscenze approfondite di Pascal, PHP, XML, HTML 4.01/5, CSS 2.1/3, Javascript (e jQuery). Conoscenze buone di C, LUA, GML, Ruby, XNA, AJAX e Assembly 68000. Competenze basilari di C++, SQL, Hlsl, Java.