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