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 Classquando 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`2In 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.
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 ModuleEcco 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 ClassL'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 ModuleL'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 = 0Per 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).
A cura di: Il Totem