|
Reflection dei generics
I generics si comportano in modo differente in molti ambiti, e la Reflection ricade proprio fra questi. Infatti, un Type che rappresenta un
tipo generic non ha lo stesso nome di quando è stato dichiarato nel codice, ma possiede una forma contratta e diversa. Ad esempio, ammettendo
che l'assembly che stiamo analizzando contenga questa classe:
Class Example(Of T, K)
'...
End Class
quando troveremo l'oggetto Type che la rappresenta durante l'enumerazione dei tipi, scopriremo che il suo nome è molto strano. Sarà
molto simile a questa stringa:
Example`2
In questa particolare formattazione, il due indica che la classe example lavora su due tipi generics: i loro nome "virtuali" non vengono
riportati nel nome, cosicché anche confrontando i nomi di due OT indicanti tipi generics, magari provenienti da AppDomain
diversi, si capisce che in realtà sono proprio lo stesso tipo, poiché la vera differenza sta solo nel nome e nella quantità
di parametri generics (l'identificatore di questi ultimi, infatti, essendo solo un segnaposto, è ininfluente). Nonostante l'assenza di dettagli, ci sono delle proprietà
che ci permettono di recuperare il nome dei tipi generics aperti, ossia "T" e "K" in questo caso. In generale, per lavorare su classi o
tipi genrics, è importante fare affidamento su questi membri di Type:
- IsGenericTypeDefinition : determina se questo Type rappresenta una definizione di un tipo generics. Fate attenzione ai dettagli,
poiché esiste un'altra proprietà molto simile con la quale ci si può confondere. Affinché questa
properietà restituisca True è necessario (e sufficiente) che il tipo che
stiamo esaminando contenga una definizione di uno o più tipi generics APERTI (e non collegati). Ad esempio:
Module Module1
'Dichiaro questa classe e la prossima variabile come
'pubblici perchè se fossero Friend bisognerebbe
'usare un overload troppo lungo di GetField e
'GetNestedTypes specificando ci cercare i membri non
'pubblici. Di default, le funzioni di ricerca operano
'solo su membri pubblici
Public Class Example(Of T)
End Class
Public E As Example(Of Int32)
Sub Main()
'Ottiene il tipo di questo modulo
Dim ModuleType As Type = GetType(Module1)
'Enumera tutti i tipi presenti nel modulo fino a
'trovare la classe Example. Ho usato un for perchè
'non si può usare GetType (in qualsiasi
'sua versione) su una classe generics senza specificare
'un tipo generics collegato, cosa che noi non
'vogliamo affatto. Per ottenere il riferimento a
'Example(Of T) bisogna per forza usare una funzione
'che restituisca tutti i tipi esistenti e poi
'cercarlo tra questi.
For Each T As Type In ModuleType.GetNestedTypes()
If T.Name.StartsWith("Example") Then
Console.WriteLine("{0} - IsGenericTypeDefinition: {1}", _
T.Name, T.IsGenericTypeDefinition)
End If
Next
'Ottiene un riferimento al campo E dichiarayo sopra
Dim EField As FieldInfo = ModuleType.GetField("E")
'E ne ottiene il tipo
Dim EType As Type = EField.FieldType
Console.WriteLine("{0} - IsGenericTypeDefinition: {1}", EType.Name, EType.IsGenericTypeDefinition)
Console.ReadKey()
End Sub
End Module
A schermo apparirà lo stesso nome due volte, ma in un caso IsGenericTypeDefinition sarà True e nell'altro False. Questo perchè
il tipo della variabile E è sì dichiarato come generic, ma all'atto pratico lavora su un solo tipo: Int32; perciò
non si tratta di una definizione di tipo generic, ma di un uso di un tipo generic;
- IsGenericType : molto simile alla precedente, ma funziona al contrario, ossia restituisce True se il tipo NON è una definizione
di tipo generic, ma una sua applicazione mediante tipi collegati. Nell'esempio di prima, EType.IsGenericType sarebbe stato True;
- GetGenericArguments() : se almeno uno tra IsGenericTypeDefinition e IsGenericType è vero, allora abbiamo a che fare con tipi
generics. Questa funzione restituisce gli OT dei tipi generics aperti (nel primo caso) o collegati (nel secondo caso). Tra breve ne vedremo
un esempio.
Ecco un esempio di come enumerare tutti i tipi generics di un assembly:
Module Module1
Sub EnumerateGenerics(ByVal Asm As Assembly)
For Each T As Type In Asm.GetTypes
'Controlla se si tratta di un tipo contenente
'tipi generics aperti
If T.IsGenericTypeDefinition Then
'Ottiene il nome semplice di quel tipo (la
'versione completa è troppo lunga XD)
Dim Name As String = T.Name
'Controlla che il nome contenga l'accento tonico.
'Infatti, possono esistere casi in cui la
'propietà IsGeneircTypeDefinition è vera,
'ma non ci troviamo di fronte a un tipo la cui
'signature contenga effettivamente tipi generics.
'Ne darò un esempio dopo...
If Not Name.Contains("`") Then
Continue For
End If
'Ottiene una stringa in cui elimina tutti i
'caratteri a partire dall'indice del'accento
Name = T.Name.Remove(T.Name.IndexOf("`"))
'E poi gli aggiunge un "(Of ", per far vedere che
'si sta iniziando una dichiarazione generic
Name &= "(Of "
'Quindi aggiunge tutti gli argomenti generic
For Each GenT As Type In T.GetGenericArguments
'Se il parametro non è il primo, lo separa dal
'precedente con una virgola.
If GenT.GenericParameterPosition > 0 Then
Name &= ", "
End If
'Quindi vi aggiunge il nome
Name &= GenT.Name
Next
'E chiude la parentesi
Name &= ")"
Console.WriteLine(Name)
End If
Next
End Sub
'Notate che la classe Type espone molte proprietà che
'si possono usare solo in determinati casi. Ad esempio, in
'questo codice è lecito richiamare GenericParametrPosition
'poiché sappiamo a priori che quel Type indica un tipo
'generic in una signature generic. Ma un in un qualsiasi OT
'non ha alcun senso usare tale proprietà!
Sub Main()
'Ottiene un riferimento all'assembly corrente
Dim Asm As Assembly = Assembly.GetExecutingAssembly()
EnumerateGenerics(Asm)
Console.ReadKey()
End Sub
End Module
Ecco alcuni dei miei risultati:
ThreadSafeObjectProvider(Of T)
Collection(Of T)
ComparableCollection(Of T)
Relation(Of T1, T2)
IsARelation(Of T, U)
DataFilter(Of T)
Riguardo all'if posto nel ciclo enumerativo, vorrei far notare che IsGenericTypeDefinition restituisce true se rintraccia nel tipo un
riferimento ad un tipo generic aperto, indipendentemente che questo sia dichiarato nel tipo o da un'altra parte. Ad esempio:
Class Example(Of T)
Delegate Sub DoSomething(ByVal Data As T)
'...
End Class
L'enumerazione raggiunge anche DoSomething, poiché è anch'esso un tipo, anche se nidificato, accessibile a tutti i membri
dell'assembly (o, se pubblico, a tutti); ed anche in quel caso, la proprietà IsGenericTypeDefinition è True, poiché
la sua signature contiene un tipo generic aperto (T). Tuttavia, il suo nome non contiene accenti tonici, poiché il generics è
stato dichiarato a livello di classe.
Ecco un altro esempio, ma sui tipi generic collegati:
Module Module1
'Enumera solo i campi generic di un tipo
Sub EnumerateGenericFieldMembers(ByVal T As Type)
For Each F As FieldInfo In T.GetFields()
If F.FieldType.IsGenericType Then
Dim Name As String = F.FieldType.Name
Dim I As Int16 = 0
If Not Name.Contains("`") Then
Continue For
End If
Name = Name.Remove(Name.IndexOf("`"))
Name &= "(Of "
For Each GenP As Type In F.FieldType.GetGenericArguments
'Dato che non si stanno analizzando dei
'parametri generic, non si può utilizzare
'la proprietà GenericParameterPosition
If I > 0 Then
Name &= ", "
End If
Name &= GenP.Name
I += 1
Next
Name &= ")"
Console.WriteLine("Dim {0} As {1}", F.Name, Name)
End If
Next
End Sub
Public L As New List(Of Integer)
Public I As Int32?
Sub Main()
EnumerateGenericFieldMembers(GetType(Module1))
Console.ReadKey()
End Sub
End Module
L'uso della Reflection
Fino ad ora non abbiamo fatto altro che enumerare membri e tipi. Devo dirlo, una cosa un po' noiosa... Tuttavia ci è servita per
comprendere come fare per accedere a certe informazioni che si celano negli assembly. Anche se non useremo quasi mai la reflection per
enumerare le parti di un assembly (a meno che non decidiate di scrivere un object browser), ora sappiamo quali informazioni possiamo
raggiungere e come prenderle. Questo è importante soprattutto quando si lavora con assembly che vengono caricati dinamicamente,
ad esempio in un sistema di plug-ins, come mostrerò fra poco. Per darvi un assaggio della potenza della reflection, ho scritto un
semplice codice che permette di accedere a tutte le informazioni di un oggetto, qualsiasi esso sia, di qualunque tipo e in qualunque
assembly. Per farlo, mi è bastato ottenerne le proprietà:
Module Module1
'Stampa tutte le informazioni ricavabili dalle
'proprietà di un dato oggetto O. Indent è solo
'una variabile d'appoggio per la formattazione, in modo
'da indentare bene le righe nel caso i valori delle
'proprietà siano altri oggetti.
Public Sub PrintInfo(ByVal O As Object, ByVal Indent As String)
'Ottiene il tipo di O
Dim T As Type = O.GetType()
Console.WriteLine("{0}Object of type {1}", Indent, T.Name)
'Enumera tutte le proprietà
For Each Prop As PropertyInfo In T.GetProperties()
'Ottiene il tipo restituito dalla proprietà
Dim PropType As Type = Prop.PropertyType()
'Se si tratta di una proprietà parametrica,
'la salta: in questo esempio non volevo dilungarmi,
'ma potete completare il codice se desiderate.
If Prop.GetIndexParameters().Count > 0 Then
Continue For
End If
'Se è un di tipo base o una stringa (giacché le
'stringhe non sono tipo base ma reference), ne stampa
'direttamente il valore a schermo
If (PropType.IsPrimitive) Or (PropType Is GetType(String)) Then
Console.WriteLine("{0} {1} = {2}", _
Indent, Prop.Name, Prop.GetValue(O, Nothing))
'Altrimenti, se si tratta di un oggetto, lo analizza a
'sua volta
ElseIf PropType.IsClass Then
Console.WriteLine("{0} {1} = ", Indent, Prop.Name)
PrintInfo(Prop.GetValue(O, Nothing), Indent & " ")
End If
Next
Console.WriteLine()
End Sub
Sub Main()
'Crea alcuni oggetti vari
Dim P As New Person("Mario", "Rossi", New Date(1982, 3, 17))
Dim T As New Teacher("Luigi", "Bianchi", New Date(1879, 8, 21), "Storia")
Dim R As New Relation(Of Person, Teacher)(P, T)
Dim Q As New List(Of Int32)
Dim K As New Text.StringBuilder()
'Ne stampa le proprietà, senza sapere nulla a priori
'sulla natura degli oggetti.
'Notate che i nomi generics rimangono con l'accento...
PrintInfo(P, "")
PrintInfo(T, "")
PrintInfo(R, "")
PrintInfo(Q, "")
PrintInfo(K, "")
Console.ReadKey()
End Sub
End Module
L'output sarà questo:
Object of type Person
FirstName = Mario
LastName = Rossi
CompleteName = Mario Rossi
Object of type Teacher
Subject = Storia
LastName = Prof. Bianchi
CompleteName = Prof. Luigi Bianchi, dottore in Storia
FirstName = Luigi
Object of type Relation`2
FirstObject =
Object of type Person
FirstName = Mario
LastName = Rossi
CompleteName = Mario Rossi
SecondObject =
Object of type Teacher
Subject = Storia
LastName = Prof. Bianchi
CompleteName = Prof. Luigi Bianchi, dottore in Storia
FirstName = Luigi
Object of type List`1
Capacity = 0
Count = 0
Object of type StringBuilder
Capacity = 16
MaxCapacity = 2147483647
Length = 0
Per scrivere questo codice mi sono basato sul metodo GetValue esposto dalla classe PropertyInfo. Esso permette di ottenere il valore che
la proprietà rappresentata dall'oggetto PropertyInfo da cui viene invocato possiede nell'oggetto specificato come parametro. In
generale, GetValue accetta due parametri: il primo è l'oggetto da cui estrarre il valore della proprietà, mentre il secondo
è un array di oggetti che rappresenta i parametri da passare alla proprietà. Come avete visto, ho enumerato solo
proprietà non parametriche e perciò non c'era bisogno di fornire alcun parametro: ecco perchè ho messo Nothing.
Al pari di GetValue c'è SetValue che permette di impostare, invece, la proprietà (ma solo se non è in sola lettura,
ossia se CanWrite è True). Ovviamente SetValue ha un parametro in più, ossia il valore da impostare (secondo parametro).
Ecco un esempio:
Module Module1
'Non riscrivo PrintInfo, ma considero che stia
'ancora in questo modulo
Sub Main()
Dim P As New Person("Mario", "Rossi", New Date(1982, 3, 17))
Dim T As New Teacher("Luigi", "Bianchi", New Date(1879, 8, 21), "Storia")
Dim R As New Relation(Of Person, Teacher)(P, T)
Dim Q As New List(Of Int32)
Dim K As New Text.StringBuilder()
Dim Objects() As Object = {P, T, R, Q, K}
Dim Cmd As Int32
Console.WriteLine("Oggetti nella collezione: ")
For I As Int32 = 0 To Objects.Length - 1
Console.WriteLine("{0} - Istanza di {1}", _
I, Objects(I).GetType().Name)
Next
Console.WriteLine("Inserire il numero corrispondente all'oggetto da modificare: ")
Cmd = Console.ReadLine
If Cmd < 0 Or Cmd > Objects.Length - 1 Then
Console.WriteLine("Nessun oggetto corrispondente!")
Exit Sub
End If
Dim Selected As Object = Objects(Cmd)
Dim SelectedType As Type = Selected.GetType()
Dim Properties As New List(Of PropertyInfo)
For Each Prop As PropertyInfo In SelectedType.GetProperties()
If (Prop.PropertyType.IsPrimitive Or Prop.PropertyType Is GetType(String)) And _
Prop.CanWrite Then
Properties.Add(Prop)
End If
Next
Console.Clear()
Console.WriteLine("Proprietà dell'oggetto:")
For I As Int32 = 0 To Properties.Count - 1
Console.WriteLine("{0} - {1}", _
I, Properties(I).Name)
Next
Console.WriteLine("Inserire il numero corrispondente alla proprietà da modificare:")
Cmd = Console.ReadLine
If Cmd < 0 Or Cmd > Objects.Length - 1 Then
Console.WriteLine("Nessuna proprietà corrispondente!")
Exit Sub
End If
Dim SelectedProp As PropertyInfo = Properties(Cmd)
Dim NewValue As Object
Console.Clear()
Console.WriteLine("Nuovo valore: ")
NewValue = Console.ReadLine
'Imposta il nuovo valore della proprietà. Noterete che
'si ottiene un errore di cast con tutti i tipi che
'non siano String. Questo accade poiché viene
'eseguito un matching sul tipo degli argomenti: se essi
'sono diversi, indipendentemente dal fatto che possano
'essere convertiti l'uno nell'altro (al contrario di
'quanto dice il testo dell'errore), viene sollevata
'quell'eccezione. Per aggirare il problema, si
'dovrebbe eseguire un cast esplicito controllando prima
'il tipo della proprietà:
' If SelectedProp.PropertyType Is GetType(Int32) Then
' NewValue = CType(NewValue, Int32)
' ElseIf SelectedProp. ...
'È il prezzo da pagare quando si lavora con
'uno strumento così generale come la Reflection.
'[Generalmente si conosce in anticipo il tipo]
SelectedProp.SetValue(Selected, NewValue, Nothing)
Console.WriteLine("Proprietà modificata!")
PrintInfo(Selected, "")
Console.ReadKey()
End Sub
End Module
Chi ha letto anche la versione precedente della guida, avrà notato che manca il codice per l'assembly browser, ossia quel programma
che elenca tutti i tipi (e tutti i membri di ogni tipo) presenti in un assembly. Mi sembrava troppo noioso e laborioso e troppo poco
interessante per riproporlo anche qui, ma siete liberi di darci un'occhiata (al relativo capitolo della versione 2).
|
|