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 - Gli Attributi

Guida al Visual Basic .NET

Capitolo 48° - Gli Attributi

<< Precedente Prossimo >>


Cosa sono e a cosa servono

Gli attributi sono una particolare categoria di oggetti che ha come unico scopo quello di fornire informazioni su altre entità. Tutte le informazioni contenute in un attributo vanno sotto il nome di metadati. Attraverso i metadati, il programmatore può definire ulteriori dettagli su un membro o su un tipo e sul suo comportamento in relazione con le altre parti del codice. Gli attributi, quindi, non sono strumenti di "programmazione attiva", poiché non fanno nulla, ma dicono semplicemente qualcosa; si avvicinano, per certi versi, alla programmazione dichiarativa. Inoltre, essi non possono essere usati né rintracciati dal codice in cui sono definiti: non si tratta di variabili, metodi, classi, strutture o altro; gli attributi, semplicemente parlando, non "esistono" se non come parte invisibile di informazione attaccata a qualche altro membro. Sono come dei parassiti: hanno senso solo se attribuiti, appunto, a qualcosa. Per questo motivo, l'unico modo per utilizzare le informazioni che essi si portano dietro consiste nella Reflection.
La sintassi con cui si assegna un attributo a un membro (non "si dichiara", né "si inizializza", ma "si assegna" a qualcosa) è questa:
<Attributo(Parametri)> [Entità]
Faccio subito un esempio. Il Visual Basic permette di usare array con indici a base maggiore di 0: questa è una sua peculiarità, che non si trova in tutti i linguaggi .NET. Le Common Language Specifications del Framework specificano che un qualsiasi linguaggio, per essere qualificato come .NET, deve dare la possibilità di gestire array a base 0. VB fa questo, ma espone anche quella particolarità di prima che gli deriva dal VB classico: questa potenzialità è detta non CLS-Compliant, ossia che non rispetta le specifiche del Framework. Noi siamo liberi di usarla, ad esempio in una libreria, ma dobbiamo avvertire gli altri che, qualora usassero un altro linguaggio .NET, non potrebbero probabilmente usufruire di quella parte di codice. L'unico modo di fare ciò consiste nell'assegnare un attributo CLSCompliant a quel membro che non rispetta le specifiche:
<CLSCompliant(False)> _
Function CreateArray(Of T)(ByVal IndexFrom As Int32, ByVal IndexTo As Int32) As Array
    'Per creare un array a estremi variabili è necessario
    'usare una funzione della classe Array, ed è altrettanto
    'necessario dichiarare l'array come di tipo Array, anche se
    'questo è solitamente sconsigliato. Per creare il
    'suddetto oggetto, bisogna passare alla funzione tre
    'parametri:
    ' - il tipo degli oggetti che l'array contiene;
    ' - un array contenente le lunghezze di ogni rango dell'array;
    ' - un array contenente gli indici iniziali di ogni rango.
    Return Array.CreateInstance(GetType(T), New Int32() {IndexTo - IndexFrom}, New Int32() {IndexTo})
End Function

Sub Main()
    Dim CustomArray As Array = CreateArray(Of Int32)(3, 9)

    CustomArray(3) = 1
    '...
End Sub
Ora, se un programmatore usasse la libreria in cui è posto questo codice, probabilmente il compilatore produrrebbe un Warning aggiuntivo indicano esplicitamente che il metodo CreateArray non è CLS-Compliant, e perciò non è sicuro usarlo.
Allo stesso modo, un altro esempio potrebbe essere l'attributo Obsolete. Specialmente quando si lavora su grandi progetti di cui esistono più versioni e lo sviluppo è in costante evoluzione, capita che vengano scritte nuove versioni di membri o tipi già esistenti: quelle vecchie saranno molto probabilmente mantenute per assicurare la compatibilità con software datati, ma saranno comunque marcate con l'attributo obsolete. Ad esempio, con questa riga di codice potrete ottenere l'indirizzo IP del mio sito:
Dim IP As Net.IPHostEntry = System.Net.Dns.Resolve("www.totem.altervista.org")
Tuttavia otterrete un Warning che riporta la seguente dicitura: 'Public Shared Function Resolve(hostName As String) As System.Net.IPHostEntry' is obsolete: Resolve is obsoleted for this type, please use GetHostEntry instead. http://go.microsoft.com/fwlink/?linkid=14202 . Questo è un esempio di un metodo, esistente dalla versione 1.1 del Framework, che è stato rimpiazzato e quindi dichiarato obsoleto.
Un altro attributo molto interessante è, ad esempio, Conditional, che permette di eseguire o tralasciare del codice a seconda che sia definita una certa costante di compilazione. Queste costanti sono impostabili in una finestra di compilazione avanzata che vedremo solo più avanti. Tuttavia, quando l'applicazione è in modalità debug, è di default definita la costante DEBUG.
<Conditional("DEBUG")> _
Sub WriteStatus()
    'Scriva a schermo la quantità di memoria usata,
    'senza aspettare la prossima garbage collection
    Console.WriteLine("Memory: {0}", GC.GetTotalMemory(False))
End Sub

'Crea un nuovo oggetto
Function CreateObject(Of T As New)() As T
    Dim Result As New T
    'Richiama WriteStatus: questa chiamata viene IGNORATA
    'in qualsiasi caso, tranne quando siamo in debug.
    WriteStatus()
    Return Result
End Function

Sub Main()
    Dim k As Text.StringBuilder = _ 
        CreateObject(Of Text.StringBuilder)()
    '...
End Sub
Usando CreateObject possiamo monitorare la quantità di memoria allocata dal programma, ma solo in modalità debug, ossia quando la costante di compilazione DEBUG è definita.
Come avrete notato, tutti gli esempi che ho fatto comunicavano informazioni direttamente al compilatore, ed infatti una buona parte degli attributi serve proprio per definire comportamente che non si potrebbero indicare in altro modo. Un attributo può essere usato, quindi, nelle maniere più varie e introdurrò nuovi attributi molto importanti nelle prossime sezioni. Questo capitolo non ha lo scopo di mostrare il funzionamento di ogni attributi esistente, ma di insegnare a cosa esso serva e come agisca: ecco perchè nel prossimo paragrafo ci cimenteremo nella scrittura di un nuovo attributo.


Dichiarare nuovi attributi

Formalmente, un attributo non è altro che una classe derivata da System.Attribute. Ci sono alcune convenzioni riguardo la scrittura di queste classi, però:
  • Il nome della classe deve sempre terminare con la parola "Attribute";
  • Gli unici membri consentiti sono: campi, proprietà e costruttori;
  • Tutte le proprietà che vengono impostate nei costruttori devono essere ReadOnly, e viceversa.
Il primo punto è solo una convenzione, ma gli altri sono di utilità pratica. Dato che lo scopo dell'attributo è contenere informazione, è ovvio che possa contenere solo proprietà, poiché non spetta a lui usarne il valore. Ecco un esempio semplice con un attributo senza proprietà:
'In questo codice, cronometreremo dei metodi, per
'vedere quale è il più veloce!
Module Module1

    'Questo è un nuovo attributo completamente vuoto.
    'L'informazione che trasporta consiste nel fatto stesso
    'che esso sia applicato ad un membro.
    'Nel metodo di cronometraggio, rintracceremo e useremo
    'solo i metodi a cui sia stato assegnato questo attributo.
    Public Class TimeAttribute
        Inherits Attribute

    End Class

    'I prossimi quattro metodi sono procedure di test. Ognuna
    'esegue una certa operazione 100mila o 10 milioni di volte.
    
    <Time()> _
    Sub AppendString()
        Dim S As String = ""
        For I As Int32 = 1 To 100000
            S &= "a"
        Next
        S = Nothing
    End Sub

    <Time()> _
    Sub AppendBuilder()
        Dim S As New Text.StringBuilder()
        For I As Int32 = 1 To 100000
            S.Append("a")
        Next
        S = Nothing
    End Sub

    <Time()> _
    Sub SumInt32()
        Dim S As Int32
        For I As Int32 = 1 To 10000000
            S += 1
        Next
    End Sub

    <Time()> _
    Sub SumDouble()
        Dim S As Double
        For I As Int32 = 1 To 10000000
            S += 1.0
        Next
    End Sub

    'Questa procedura analizza il tipo T e ne estrae tutti
    'i metodi statici e senza parametri marcati con l'attributo
    'Time, quindi li esegue e li cronometra, poi riporta
    'i risultati a schermo per ognuno.
    'Vogliamo che i metodi siano statici e senza parametri
    'per evitare di raccogliere tutte le informazioni per la
    'funzione Invoke.
    Sub ReportTiming(ByVal T As Type)
        Dim Methods() As MethodInfo = T.GetMethods()
        Dim TimeType As Type = GetType(TimeAttribute)
        Dim TimeMethods As New List(Of MethodInfo)

        'La funzione GetCustomAttributes accetta due parametri
        'nel secondo overload: il primo è il tipo di
        'attributo da cercare, mentre il secondo specifica se
        'cercare tale attributo in tutto l'albero di
        'ereditarietà del membro. Restituisce come
        'risultato un array di oggetti contenenti gli attributi
        'del tipo voluto. Vedremo fra poco come utilizzare
        'questo array: per ora limitiamoci a vedere se non è
        'vuoto, ossia se il metodo è stato marcato con Time
        For Each M As MethodInfo In Methods
            If M.GetCustomAttributes(TimeType, False).Length > 0 And _
               M.GetParameters().Count = 0 And _
               M.IsStatic Then
                TimeMethods.Add(M)
            End If
        Next

        Methods = Nothing

        'La classe Stopwatch rappresenta un cronometro. Start
        'per farlo partire, Stop per fermarlo e Reset per
        'resettarlo a 0 secondi.
        Dim Crono As New Stopwatch

        For Each M As MethodInfo In TimeMethods
            Crono.Reset()
            Crono.Start()
            M.Invoke(Nothing, New Object() {})
            Crono.Stop()
            Console.WriteLine("Method: {0}", M.Name)
            Console.WriteLine("  Time: {0}ms", Crono.ElapsedMilliseconds)
        Next

        TimeMethods.Clear()
        TimeMethods = Nothing
    End Sub

    Sub Main()
        Dim This As Type = GetType(Module1)

        'Non vi allarmate se il programma non stampa nulla
        'per qualche secondo. Il primo metodo è molto
        'lento XD
        ReportTiming(This)

        Console.ReadKey()
    End Sub
End Module
Ecco i risultati del benchmarking (termine tecnico) sul mio portatile:
Method: AppendString
  Time: 4765ms
Method: AppendBuilder
  Time: 2ms
Method: SumInt32
  Time: 27ms
Method: SumDouble
  Time: 34ms
Come potete osservare, concatenare le stringhe con & è enormemente meno efficiente rispetto all'Append della classe StringBuilder. Ecco perchè, quando si hanno molti dati testuali da elaborare, consiglio sempre di usare il secondo metodo. Per quando riguarda i numeri, le prestazioni sono comunque buone, se non che i Double occupano 32 bit in più e ci vuole più tempo anche per elaborarli. In questo esempio avete visto che gli attributi possono essere usati solo attraverso la Reflection. Prima di procedere, bisogna dire che esiste uno speciale attributo applicabile solo agli attributi che definisce quali entità possano essere marcate con dato attributo. Esso si chiama AttributeUsage. Ad esempio, nel codice precedente, Time è stato scritto con l'intento di marcare tutti i metodi che sarebbero stati sottoposti a benchmarking, ossia è "nato" per essere applicato solo a metodi, e non a classi, enumeratori, variabili o altro. Tuttavia, per come l'abbiamo dichiarato, un programmatore può applicarlo a qualsiasi cosa. Per restringere il suo campo d'azione si dovrebbe modificare il sorgente come segue:
<AttributeUsage(AttributeTargets.Method)> _
Public Class TimeAttribute
    Inherits Attribute

End Class
AttributeTargets è un enumeratore codificato a bit.
Ma veniamo ora agli attributi con parametri:
Module Module1

    'UserInputAttribute specifica se una certa proprietà
    'debba essere valorizzata dall'utente e se sia
    'obbligatoria o meno. Il costruttore impone un solo
    'argomento, IsUserScope, che deve essere per forza
    'specificato, altrimenti non sarebbe neanche valsa la
    'pena di usare l'attributo, dato che questa è la
    'sua unica funzione.
    'Come specificato dalle convenzioni, la proprietà
    'impostata nel costruttore è ReadOnly, mentre
    'le altre (l'altra) è normale.
    <AttributeUsage(AttributeTargets.Property)> _
    Class UserInputAttribute
        Inherits Attribute

        Private _IsUserScope As Boolean
        Private _IsCompulsory As Boolean = False

        Public ReadOnly Property IsUserScope() As Boolean
            Get
                Return _IsUserScope
            End Get
        End Property

        Public Property IsCompulsory() As Boolean
            Get
                Return _IsCompulsory
            End Get
            Set(ByVal value As Boolean)
                _IsCompulsory = value
            End Set
        End Property

        Sub New(ByVal IsUserScope As Boolean)
            _IsUserScope = IsUserScope
        End Sub

    End Class

    'Cubo
    Class Cube
        Private _SideLength As Single
        Private _Density As Single
        Private _Cost As Single

        'Se i parametri del costruttore vanno specificati
        'tra parentesi quando si assegna l'attributo, allora
        'come si fa a impostare le altre proprietà
        'facoltative? Si usa un particolare operatore di
        'assegnamento ":=" e si impostano esplicitamente
        'i valori delle proprietà ad uno ad uno,
        'separati da virgole, ma sempre nelle parentesi.
        <UserInput(True, IsCompulsory:=True)> _
        Public Property SideLength() As Single
            Get
                Return _SideLength
            End Get
            Set(ByVal value As Single)
                _SideLength = value
            End Set
        End Property

        <UserInput(True)> _
        Public Property Density() As Single
            Get
                Return _Density
            End Get
            Set(ByVal value As Single)
                _Density = value
            End Set
        End Property

        'Cost non verrà chiesto all'utente
        <UserInput(False)> _
        Public Property Cost() As Single
            Get
                Return _Cost
            End Get
            Set(ByVal value As Single)
                _Cost = value
            End Set
        End Property

    End Class

    'Crea un oggetto di tipo T richiendendo all'utente di
    'impostare le proprietà marcate con UserInput
    'in cui IsUserScope è True.
    Function GetInfo(ByVal T As Type) As Object
        Dim O As Object = T.Assembly.CreateInstance(T.FullName)

        For Each PI As PropertyInfo In T.GetProperties()
            If Not PI.CanWrite Then
                Continue For
            End If

            Dim Attributes As Object() = PI.GetCustomAttributes(GetType(UserInputAttribute), True)

            If Attributes.Count = 0 Then
                Continue For
            End If

            'Ottiene il primo (e l'unico) elemento dell'array,
            'un oggetto di tipo UserInputAttribute che rappresenta
            'l'attributo assegnato e contiene tutte le informazioni
            'passate, sottoforma di proprietà.
            Dim Attr As UserInputAttribute = Attributs(0)

            'Se la proprietà non è richiesta all'utente,
            'allora continua il ciclo
            If Not Attr.IsUserScope Then
                Continue For
            End If

            Dim Value As Object = Nothing
            'Se è obbligatoria, continua a richiederla
            'fino a che l'utente non immette un valore corretto.
            If Attr.IsCompulsory Then
                Do
                    Try
                        Console.Write("* {0} = ", PI.Name)
                        Value = Convert.ChangeType(Console.ReadLine, PI.PropertyType)
                    Catch Ex As Exception
                        Value = Nothing
                        Console.WriteLine(Ex.Message)
                    End Try
                Loop Until Value IsNot Nothing
            Else
                'Altrimenti la richiede una sola volta
                Try
                    Console.Write("{0} = ", PI.Name)
                    Value = Convert.ChangeType(Console.ReadLine, PI.PropertyType)
                Catch Ex As Exception
                    Value = Nothing
                End Try
            End If
            If Value IsNot Nothing Then
                PI.SetValue(O, Value, Nothing)
            End If
        Next

        Return O
    End Function

    Sub Main()
        Dim O As Object 
        
        Console.WriteLine("Riempire i campi (* = obbligatorio):")
        
        O = GetInfo(GetType(Cube))

        'Stampa i valori con il metodo PrintInfo scritto qualche
        'capitolo fa
        PrintInfo(O, "")

        Console.ReadKey()
    End Sub
End Module
Vi lascio immaginare cosa faccia il metodo Convert.ChangeType...

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