|
La classe System.Type
La classe Type è una classe davvero particolare, poiché rappresenta un tipo. Con tipo indichiamo tutte le possibili
tipologie di dato esistenti: tipi base, enumeratori, strutture, classi e delegate. Per ogni tipo contemplato, esiste un corrispettivo oggetto
Type che lo rappresenta: avevo detto all'inizio della guida, infatti, che ogni cosa in .NET è un oggetto, ed i tipi non fanno
eccezione. Vi sorprenderebbe sapere tutto ciò che può essere rappresentato da una classe e fra poco vi svelerò un
segreto... Ma per ora concentriamoci su Type. Questi oggetti rappresentanti un tipo - che possiamo chiamare per brevità OT (non è
un termine tecnico) - vengono creati durante la fase di inizializzazione del programma e ne esiste una e una sola copia per ogni singolo tipo
all'interno di un singolo AppDomain. Ciò significa che due contesti applicativi differenti avranno due OT diversi per rappresentare
lo stesso tipo, ma non analizzeremo questa peculiare casistica. Ci limiteremo, invece, a studiare gli OT all'interno di un solo
dominio applicativo, coincidente con il nostro programma.
Come per gli assembly, esistono molteplici modi per ottenere un OT:
- Tramite l'operatore GetType(Tipo);
- Tramite la funzione d'istanza GetType();
- Tramite la funzione condivisa Type.GetType("nometipo").
Ecco un semplice esempio di come funzionano questi metodi:
Module Module1
Sub Main()
'Ottiene un OT per il tipo double tramite
'l'operatore GetType
Dim DoubleType As Type = GetType(Double)
Console.WriteLine(DoubleType.FullName)
'Ottiene un OT per il tipo string tramite
'la funzione statica Type.GetType. Essa richiede
'come parametro il nome (possibilmente completo)
'del tipo. Nel caso il nome non corrisponda a
'nessun tipo, verrà restituito Nothing
Dim StringType As Type = Type.GetType("System.String")
Console.WriteLine(StringType.FullName)
'Ottiene un OT per il tipo ArrayList tramite
'la funzione d'istanza GetType. Da notare che,
'mentre le precedenti usavano come punto
'di partenza direttamente un tipo (o il suo nome),
'questa richiede un oggetto di quel tipo.
Dim A As New ArrayList
Dim ArrayListType As Type = A.GetType()
Console.WriteLine(ArrayListType.FullName)
Console.ReadKey()
End Sub
End Module
Ora che ho esemplificato come ottenere un OT, vorrei mostrare l'unicità di OT ottenuti in modi differenti: anche se usassimo tutti
i tre metodi sopra menzionati per ottenere un OT per il tipo String, otterremo un riferimento allo stesso oggetto, poiché il tipo
String è unico:
Module Module1
Sub Main()
Dim Type1 As Type = GetType(String)
Dim Type2 As Type = Type.GetType("System.String")
Dim Type3 As Type = "Ciao".GetType()
Console.WriteLine(Type1 Is Type2)
'> True
Console.WriteLine(Type2 Is Type3)
'> True
'Gli OT contenuti in Type1, Type2 e Type3
'SONO lo stesso oggetto
Console.ReadKey()
End Sub
End Module
Questo non vale per il tipo System.Type stesso, poiché il metodo d'istanza GetType restituisce un oggetto RuntimeType. Questi dettagli,
tuttavia, non vi interesseranno se non tra un bel po' di tempo, quindi possiamo anche evitare di soffermarci e procedere con la spiegazione.
Ogni oggetto Type espone una quantità inimmaginabile di membri e penso che potrebbe essere la classe più ampia di tutto il
Framework. Di questa massa enorme di informazioni, ve ne è un sottoinsieme che permette di sapere in che modo il tipo è stato
dichiarato e quali sono le sue caratteristiche principali. Possiamo ricavare, ad esempio, gli specificatori di accesso, gli eventuali
modificatori, possiamo sapere se si tratta di una classe, un enumeratore, una struttura o altro, e, nel primo caso, se è astratta o
sigillata; possiamo sapere le sua classe base, le interfacce che implementa, se si tratta di un array o no, eccetera... Di seguito elenco
i membri di questo sottoinsieme:
- Assembly : restituisce l'assembly a cui il tipo appartiene (ossia in cui è stato dichiarato);
- AssemblyQualifiedName : restituisce il nome dell'assembly a cui il tipo appartiene;
- BaseType : se il tipo corrente eredita da una classe base, questa proprietà restituisce un oggetto Type in riferimento a tale
classe;
- DeclaringMethod : se il tipo corrente è parametro di un metodo, questa proprietà restituisce un oggetto MethodBase che rappresenta
tale metodo;
- DeclaringType : se il tipo corrente è membro di una classe, questa proprietà restituisce un oggetto Type che rappresenta
tale classe; questa proprietà viene valorizzata, quindi, solo se il tipo è stato dichiarato all'interno di un altro tipo
(ad esempio classi nidificate);
- FullName : il nome completo del tipo corrente;
- IsAbstract : determina se il tipo è una classe astratta;
- IsArray : determina se è un array;
- IsClass : determina se è una classe;
- IsEnum : determina se è un enumeratore;
- IsInterface : determina se è un'interfaccia;
- IsNested : determina se il tipo è nidificato: questo significa che rappresenta un membro di classe o di struttura; di conseguenza
tutte le proprietà il cui nome inizia per "IsNested" servono a determinare l'ambito di visibilità del membro, e quindi il suo
specificatore di accesso;
- IsNestedAssembly : determina se il membro è Friend;
- IsNestedFamily : determina se il membro è Protected;
- IsNestedFamORAssem : determina se il membro è Protected Friend;
- IsNestedPrivate : determina se il membro è Private;
- IsNestedPublic : determina se il membro è Public;
- IsNotPublic : determina se il tipo non è Public (solo per tipi non nidificati). Vi ricordo, infatti, che all'interno di un
namespace, gli unici specificatori possibili sono Public e Friend (gli altri si adottano solo all'interno di una classe);
- IsPointer : determina se è un puntatore;
- IsPrimitive : determina se è uno dei tipi primitivi;
- IsPublic : determina se il tipo è Public (solo per tipi non nidificati);
- IsSealed : determina se è una classe sigillata;
- IsValueType : determina se è un tipo value;
- Name : il nome del tipo corrente;
- Namespace : il namespace in cui è contenuto il tipo corrente.
Con questa abbondante manciata di proprietà possiamo iniziare a scrivere un metodo di analisi un po' più approfondito. Nella
fattispecie, la prossima procedura EnumerateTypes accetta come parametro il riferimento ad un assembly e scrive a schermo tutti i tipi
ivi definiti:
Module Module1
Sub EnumerateTypes(ByVal Asm As Assembly)
Dim Category As String
'GetTypes restituisce un array di Type che
'indicano tutti i tipi definiti all'interno
'dell'assembly Asm
For Each T As Type In Asm.GetTypes
If T.IsClass Then
Category = "Class"
ElseIf T.IsInterface Then
Category = "Interface"
ElseIf T.IsEnum Then
Category = "Enumerator"
ElseIf T.IsValueType Then
Category = "Structure"
ElseIf T.IsPrimitive Then
Category = "Base Type"
End If
Console.WriteLine("{0} ({1})", T.Name, Category)
Next
End Sub
Sub Main()
'Ottiene un riferimento all'assembly in esecuzione,
'quindi al programma. Non otterrete molti tipi
'usando questo codice, a meno che il resto del
'modulo non sia pieno di codice vario come nel
'mio caso XD
Dim Asm As Assembly = Assembly.GetExecutingAssembly()
EnumerateTypes(Asm)
Console.ReadKey()
End Sub
End Module
Il nostro piccolo segreto
Prima di procedere con l'enumerazione dei membri, vorrei mostrare che in realtà tutti i tipi sono classi, soltanto con regole "speciali"
di ereditarietà e di sintassi. Questo codice rintraccia tutte le classi basi di un tipo, costruendone l'albero di ereditarietà
fino alla radice (che sarà ovviamente System.Object):
Module Module1
'Analizza l'albero di ereditarietà di un tipo
Sub AnalyzeInheritance(ByVal T As Type)
'La proprietà BaseType restituisce la classe
'base da cui T è derivata
If T.BaseType IsNot Nothing Then
Console.WriteLine("> " & T.BaseType.FullName)
AnalyzeInheritance(T.BaseType)
End If
End Sub
Enum Status
Enabled
Disabled
Standby
End Enum
Structure Example
Dim A As Int32
End Structure
Delegate Sub Sample()
Sub Main()
Console.WriteLine("Integer:")
AnalyzeInheritance(GetType(Integer))
Console.WriteLine()
Console.WriteLine("Enum Status:")
AnalyzeInheritance(GetType(Status))
Console.WriteLine()
Console.WriteLine("Structure Example:")
AnalyzeInheritance(GetType(Example))
Console.WriteLine()
Console.WriteLine("Delegate Sample:")
AnalyzeInheritance(GetType(Sample))
Console.WriteLine()
Console.ReadKey()
End Sub
End Module
L'output mostra che il tipo Integer e la struttura Example derivano entrambi da System.ValueType, che a sua volta deriva da Object. La
definizione rigorosa di "tipo value", quindi, sarebbe "qualsiasi tipo derivato da System.ValueType". Infatti, al pari dei primi due, anche
l'enumeratore deriva indirettamente da tale classe, anche se mostra un passaggio in più, attraverso il tipo System.Enum. Allo stesso
modo, il delegate Sample deriva dalla classe DelegateMulticast, la quale derivata da Delegate, la quale deriva da Object. La differenza
sostanziale tra tipi value e reference, quindi, risiede nel fatto che i primi hanno almeno un passaggio di ereditarietà attraverso
la classe System.ValueType, mentre i secondi derivano direttamente da Object.
System.Enum e System.Delegate sono classi astratte che espongono utili metodi statici che potete ispezionare da soli (sono pochi e di
facile comprensione). Ma ora che sapete che tutti i tipi sono classi, potete anche esplorare i membri esposti dai tipi base.
Enumerare i membri
Fino ad ora abbiamo visto solo come analizzare i tipi, ma ogni tipo possiede anche dei membri (variabili, metodi, proprietà, eventi,
eccetera...). La Reflection permette anche di ottenere informazioni sui membri di un tipo, e la classe in cui queste informazioni vengono
poste è MemberInfo, del namespace System.Reflection. Dato che ci sono diverse categorie di membri, esistono altrettante classi
derivate da MemberInfo che ci raccontano una storia tutta diversa a seconda di cosa stiamo guardando: MethodInfo contiene informazioni su
un metodo, PropertyInfo su una proprietà, ParamterInfo su un parametro, FieldInfo su un campo e via dicendo. Fra le molteplici
funzioni esposte da Type, ce ne sono alcune che servono proprio a reperire questi dati; eccone un elenco:
- GetConstructors() : restituisce un array di ConstructorInfo, ognuno dei quali rappresenta uno dei costruttori definiti per quel tipo;
- GetEvents() : restituisce un array di EventInfo, ognuno dei quali rappresenta uno degli eventi dichiarati in quel tipo;
- GetFields() : restituisce un array di FieldInfo, ognuno dei quali rappresenta uno dei campi dichiarati in quel tipo;
- GetInterfaces() : restituisce un array di Type, ognuno dei quali rappresenta una delle interfacce implementate da quel tipo;
- GetMembers() : restituisce un array di MemberInfo, ognuno dei quali rappresenta uno dei membri dichiarati in quel tipo;
- GetMethods() : restituisce un array di MethodInfo, ognuno dei quali rappresenta uno dei metodi dichiarati in quel tipo;
- GetNestedTypes() : restituisce un array di Type, ognuno dei quali rappresenta uno dei tipi dichiarati in quel tipo;
- GetProperties() : restituisce un array di PropertyInfo, ognuno dei quali rappresenta una delle proprietà dichiarate in quel tipo;
La funzione GetMembers, da sola, ci fornisce una lista generale di tutti i membri di quel tipo:
Module Module1
Sub Main()
Dim T As Type = GetType(String)
'Elenca tutti i membri di String
For Each M As MemberInfo In T.GetMembers
'La proprietà MemberType restituisce un enumeratore che
'specifica di che tipo sia il membro, se una proprietà,
'un metodo, un costruttore, eccetera...
Console.WriteLine(M.MemberType.ToString & " " & M.Name)
Next
Console.ReadKey()
End Sub
End Module
Eseguendo il codice appena proposto, potrete notare che a schermo appaiono tutti i membri di String, ma molti sono ripetuti: questo si verifica
perchè i metodi che possiedono delle varianti in overload vengono riportati tante volte quante sono le varianti; naturalemnte,
ogni oggetto MethodInfo sarà diverso dagli altri per le informazioni sulla quantità e sul tipo di parametri passati
a tale metodo. Accanto a questa stranezza, noterete, poi, che per ogni proprietà ci sono due metodi definiti come get_NomeProprietà
e set_NomeProprietà: questi metodi vengono creati automaticamente quando il codice di una proprietà viene compilato, e vengono
eseguiti al momento di impostare od ottenere il valore di tale proprietà. Altra stranezza è che tutti i costruttori si
chiamano ".ctor" e non New. Stiamo cominciando ad entrare nel mondo dell'Intermediate Language, il linguaggio intermedio simil-macchina
in cui vengono convertiti i sorgenti una volta compilati. Di fatto, noi stiamo eseguendo il processo inverso della compilazione, ossia la
decompilazione. Alcune informazioni vengono manipolate nel passaggio da codice a IL, e quando si torna indietro, le si vede in
altro modo, ma tutta l'informazione necessaria è ancora contenuta lì dentro. Non esiste, tuttavia, una classe già
scritta che ritraduca in codice tutto il linguaggio intermedio: ciò che il Framework ci fornisce ci consente solo di conoscere "a pezzi"
tutta l'informazione ivi contenuta, ma sottolineiamo "tutta". Sarebbe, quindi, possibile - ed infatti è già stato fatto -
ritradurre tutti questi dati in codice sorgente. Per ora, ci limiteremo a "ricostruire" la signature di un metodo.
Prima di procedere, vi fornisco un breve elenco dei membri significativi di ogni derivato di MemberInfo:
MemberInfo
- DeclaringType : la classe che dichiara questo membro;
- MemberType : categoria del membro;
- Name : il nome del membro;
- ReflectedType : il tipo usato per ottenere un riferimento a questo membro tramite reflection;
MethodInfo
- GetBaseDefinition() : se il metodo è modificato tramite polimorfismo, restituisce la versione della classe base (se non è
stato sottoposto a polimorfismo, restituisce Nothing);
- GetCurrentMethod() : restituisce un MethodInfo in riferimento al metodo in cui questa funzione viene chiamata;
- GetMethodBody() : restituisce un oggetto MethodBody (che vedremo in seguito) contenente informazioni sulle variabili locali, le eccezioni e il
codice IL;
- GetParameters() : restituisce un elenco di ParameterInfo rappresentanti i parametri del metodo;
- IsAbstract : determina se il metodo è MustOverride;
- IsConstructor : determina se è un costruttore;
- IsFinal : determina se è NotOverridable;
- IsStatic : determina se è Shared;
- IsVirtual : determina se è Overridable;
- ReturnParameter : qualora il metodo fosse una funzione, restituisce informazioni sul valore restituito;
- ReturnType : in una funzione, restituisce l'oggetto Type associato al tipo restituito. Se il metodo non
è una funzione, restituisce Nothing o uno speciale OT in riferimento al tipo System.Void.
FieldInfo
- GetRawCostantValue() : se il campo è una costante, ne restituisce il valore;
- IsLiteral : determina se è una costante;
- IsInitOnly : determina se è una variabile readonly;
PropertyInfo
- CanRead : determina se si può leggere la proprietà;
- CanWrite : determina se si può impostare la proprietà;
- GetGetMethod() : restituisce un MethodInfo corrispondente al blocco Get;
- GetSetMethod() : restituisce un MethodInfo corrispondente al blocco Set;
- GetPropertyType() : restituisce un oggetto Type in riferimento al tipo della proprietà.
EventInfo (per ulteriori informazioni, vedere i capitoli della sezione B sugli eventi)
- GetAddMethod() : restituisce un riferimento al metodo usato per aggiungere gli handler d'evento;
- GetRaiseMethod() : restituisce un riferimento al metodo che viene richiamato quando si scatena l'evento;
- GetRemoveMethod() : restituisce un riferimento al metodo usato per rimuovere gli handler d'evento;
- IsMulticast : indica se l'evento è gestito tramite un delegate multicast.
Module Module1
'Analizza il metodo rappresentato dall'oggetto MI
Sub AnalyzeMethod(ByVal MI As MethodInfo)
'Il nome
Dim Name As String
'Il nome completo, con scpecificatori di accesso,
'modificatori, signature e tipo restituito. Per
'ulteriori informazioni sul tipo StringBuilder,
'vedere il capitolo "Magie con le stringhe"
Dim CompleteName As New System.Text.StringBuilder
'Lo specificatore di accesso
Dim Scope As String
'Gli eventuali modificatori
Dim Modifier As String
'La categoria: Sub o Function
Dim Category As String
'La signature del metodo, che andremo a costruire
Dim Signature As New System.Text.StringBuilder
'Di solito, tutti i metodi hanno un tipo restituito,
'poiché, in analogia con la sintassi del C#, una
'procedura è una funzione che restituisce Void,
'ossia niente. Per questo bisogna controllare anche il
'nome del tipo di ReturnParameter
If MI.ReturnParameter IsNot Nothing AndAlso _
MI.ReturnType.FullName <> "System.Void" Then
Category = "Function"
Else
Category = "Sub"
End If
If MI.IsConstructor Then
Name = "New"
Else
Name = MI.Name
End If
If MI.IsAssembly Then
Scope = "Friend"
ElseIf MI.IsFamily Then
Scope = "Protected"
ElseIf MI.IsFamilyOrAssembly Then
Scope = "Protected Friend"
ElseIf MI.IsPrivate Then
Scope = "Private"
Else
Scope = "Public"
End If
If MI.IsFinal Then
'Vorrei far notare una sottigliezza. Se il metodo è
'Final, ossia NotOverridable, significa che non può
'essere modificato nelle classi derivate. Ma tutti i
'membri non dichiarati esplicitamente Overridable
'non sono modificabili nelle classi derivate. Quindi,
'definire un metodo senza modificatori polimorfici
'(come quelli che seguono qua in basso), equivale a
'definirlo NotOverridable. Perciò non si
'aggiunge nessun modificatore in questo caso
ElseIf MI.IsAbstract Then
Modifier = "MustOverride"
ElseIf MI.IsVirtual Then
Modifier = "Overridable"
ElseIf MI.GetBaseDefinition IsNot Nothing AndAlso _
MI IsNot MI.GetBaseDefinition Then
Modifier = "Overrides"
End If
If MI.IsStatic Then
If Modifier <> "" Then
Modifier = "Shared " & Modifier
Else
Modifier = "Shared"
End If
End If
'Inizia la signature con una parentesi tonda aperta.
'Append aggiunge una stringa a Signature
Signature.Append("(")
For Each P As ParameterInfo In MI.GetParameters
'Se P è un parametro successivo al primo, lo separa dal
'precedente con una virgola
If P.Position > 0 Then
Signature.Append(", ")
End If
'Se P è passato per valore, ci vuole ByVal, altrimenti
'ByRef. IsByRef è un membro di Type, ma viene
'usato solo quando il tipo in questione indica il tipo
'di un parametro
If P.ParameterType.IsByRef Then
Signature.Append("ByRef ")
Else
Signature.Append("ByVal ")
End If
'Se P è opzionale, ci vuole la keyword Optional
If P.IsOptional Then
Signature.Append("Optional ")
End If
Signature.Append(P.Name)
If P.ParameterType.IsArray Then
Signature.Append("()")
End If
'Dato che la sintassi del nome è in stile C#, al
'posto delle parentesi tonde in un array ci sono delle
'quadre: rimediamo
Signature.Append(" As " & P.ParameterType.Name.Replace("[]",""))
'Si ricordi che i parametri optional hanno un valore
'di default
If P.IsOptional Then
Signature.Append(" = " & P.DefaultValue.ToString)
End If
Next
Signature.Append(")")
If MI.ReturnParameter IsNot Nothing AndAlso _
MI.ReturnType.FullName <> "System.Void" Then
Signature.Append(" As " & MI.ReturnType.Name)
End If
'Ed ecco il nome completo
CompleteName.AppendFormat("{0} {1} {2} {3}{4}", Scope, Modifier, _
Category, Name, Signature.ToString)
Console.WriteLine(CompleteName.ToString)
Console.WriteLine()
'Ora ci occupiamo del corpo
Dim MB As MethodBody = MI.GetMethodBody
If MB Is Nothing Then
Exit Sub
End If
'Massima memoria occupata sullo stack
Console.WriteLine("Massima memoria stack : {0} bytes", _
MB.MaxStackSize)
Console.WriteLine()
'Variabili locali (LocalVariableInfo è una variante di
'FieldInfo)
Console.WriteLine("Variabili locali:")
For Each L As LocalVariableInfo In MB.LocalVariables
'Dato che non si può ottenere il nome, ci si deve
'accontentare di un indice
Console.WriteLine(" Var({0}) As {1}", L.LocalIndex, _
L.LocalType.Name)
Next
Console.WriteLine()
'Gestione delle eccezioni
Console.WriteLine("Gestori di eccezioni:")
For Each Ex As ExceptionHandlingClause In MB.ExceptionHandlingClauses
'Tipo di clausola: distingue tra filtro (When),
'clausola (Catch) o un blocco Finally
Console.WriteLine(" Tipo : {0}", Ex.Flags.ToString)
'Se si tratta di un blocco Catch, ne specifica la
'natura
If Ex.Flags = ExceptionHandlingClauseOptions.Clause Then
Console.WriteLine(" Catch Ex As " & Ex.CatchType.Name)
End If
'Offset, ossia posizione in bytes nel Try, del gestore
Console.WriteLine(" Offset : {0}", Ex.HandlerOffset)
'Lunghezza, in bytes, del codice eseguibile del gestore
Console.WriteLine(" Lunghezza : {0}", Ex.HandlerLength)
Console.WriteLine()
Next
End Sub
Sub Test(ByVal Num As Int32, ByVal S As String)
Dim T As Date
Dim V As String
Try
Console.WriteLine("Prova 1, 2, 3")
Catch Ex As ArithmeticException
Console.WriteLine("Errore 1")
Catch Ex As ArgumentException
Console.WriteLine("Errore 2")
Finally
Console.WriteLine("Ciao")
End Try
End Sub
Sub Main()
Dim T As Type = GetType(Module1)
Dim Methods() As MethodInfo = T.GetMethods
Dim Index As Int16
Console.WriteLine("Inserire un numero tra i seguenti per analizzare il metodo corrispondente:")
Console.WriteLine()
For I As Int16 = 0 To Methods.Length - 1
Console.WriteLine("{0} - {1}", I, Methods(I).Name)
Next
Console.WriteLine()
Index = Console.ReadLine
If Index >= 0 And Index &rt; Methods.Length Then
AnalyzeMethod(Methods(Index))
End If
Console.ReadKey()
End Sub
End Module
Analizzando il metodo Test, si otterrà questo output:
Public Shared Sub Test(ByVal Num As Int32, ByVal S As String)
Massima memoria stack : 2 bytes
Variabili locali:
Var(0) As DateTime
Var(1) As String
Var(2) As ArithmeticException
Var(3) As ArgumentException
Gestori di eccezioni:
Tipo : Clause
Catch Ex As ArithmeticException
Offset : 15
Lunghezza : 26
Tipo : Clause
Catch Ex As ArgumentException
Offset : 41
Lunghezza : 26
Tipo : Finally
Offset : 67
Lunghezza : 13
|
|