Questo sito utilizza cookies solo per scopi di autenticazione sul sito e nient'altro. Nessuna informazione personale viene tracciata. Leggi l'informativa sui cookies.
Username: Password: oppure
Guida al Visual Basic .NET - La Reflection Parte III

Guida al Visual Basic .NET

Capitolo 46° - La Reflection Parte III

<< Precedente Prossimo >>


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

<< Precedente Prossimo >>
A proposito dell'autore

C#, TypeScript, java, php, EcmaScript (JavaScript), Spring, Hibernate, React, SASS/LESS, jade, python, scikit, node.js, redux, postgres, keras, kubernetes, docker, hexo, etc...