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 II

Guida al Visual Basic .NET

Capitolo 45° - La Reflection Parte II

<< Precedente Prossimo >>


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 


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