Guida al Visual Basic .NET
Capitolo 48° - Gli Attributi
Cosa sono e a cosa servonoGli 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 SubOra, 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 SubUsando 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 attributiFormalmente, un attributo non è altro che una classe derivata da System.Attribute. Ci sono alcune convenzioni riguardo la scrittura di queste classi, però:
'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 ModuleEcco i risultati del benchmarking (termine tecnico) sul mio portatile: Method: AppendString Time: 4765ms Method: AppendBuilder Time: 2ms Method: SumInt32 Time: 27ms Method: SumDouble Time: 34msCome 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 ClassAttributeTargets è 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 ModuleVi lascio immaginare cosa faccia il metodo Convert.ChangeType...
C#, TypeScript, java, php, EcmaScript (JavaScript), Spring, Hibernate, React, SASS/LESS, jade, python, scikit, node.js, redux, postgres, keras, kubernetes, docker, hexo, etc...
|