Tema 17: Atributos
Concepto de atributo
Un atributo es información que se puede añadir
a los metadatos de un módulo de código. Esta
información puede ser referente tanto al propio
módulo o el ensamblado al que peretenezca como a los tipos
de datos en definidos en él, sus miembros, los
parámetros de sus métodos, los bloques set y
get de sus propiedades e indizadores o los bloques
add y remove de sus eventos.
En C# se incluyen numerosos modificadores que nos permiten asociar
información a los metadatos de un módulo. Por
ejemplo, con los modificadores
public, protected, private, internal o
protected internal podemos añadir información
sobre la visibilidad de los tipos del módulo y de sus
miembros. Pues bien, los atributos pueden verse como un mecanismo
mediante el cual el programador puede crear sus propios
modificadores.
Un ejemplo de atributo podría ser uno llamado Ayuda que
pudiese prefijar las definiciones de miembros de tipos e indicase
cuál es la URL donde se pudiese encontrar información
detallada con ayuda sobre el significado del miembro
prefijado.
Utilización de
atributos
Para colocar un atributo a un elemento basta prefijar la
definición de dicho elemento con una estructura de esta
forma:
[<nombreAtributo>(<parámetros>)]
Esta estructura ha de colocarse incluso antes que cualquier
modificador que pudiese acompañar la definición del
elemento a atribuir.
Los parámetros de una atributo pueden ser opcionales, y si
se usa sin especificar valores para sus parámetros no hay
porqué que usar paréntesis vacíos como en las
llamadas a métodos, sino que basta usar el atributo
indicando sólo la sintaxis
[<nombreAtributo>]
Los parámetros de un atributo pueden ser de dos tipos:
-
Parámetros sin nombre: Se usan de forma similar a
los parámetros de los métodos, sólo que no
pueden contar con modificadores ref u out.
-
Parámetros con nombre: Son opcionales y pueden
colocarse en cualquier posición en la lista de
<parámetros> del atributo. Lo último se debe
a que a la hora de darles valor se usa la sintaxis
<nombreParámetro>=<valor>, por lo que el
compilador no dependerá de su posición a la hora
de determinar a qué parámetro se le está
dando cada valor.
Para evitar conflictos entre parámetros con nombre y
parámetros sin nombre, los primeros siempre se han de
incluir después de los segundos, no siendo posible
mezclarlos indiscriminadamente.
Si se desean especificar varios atributos para un mismo elemento se
pueden indicar todos ellos entre unos mismos corchetes serapados
por comas. Es decir, de la forma:
[<atributo1>(<parametros1>), <atributo2>(<parámetros>), ...]
Aunque también sería posible especificarlos por
separado. O sea, de esta otra forma:
[<atributo1>(<parametros1>)] [<atributo2>(parámetros>)] ...
Hay casos en los que por la ubicación del atributo no se
puede determinar de manera unívoca a cuál elemento se
le desea aplicar, ya que podría ser aplicable a varios. En
esos casos, para evitar ambigüedades lo que se hace es usar el
atributo prefijando su nombre de un indicador de tipo de
elemento, quedando así la sintaxis a usar:
[<indicadorElemento>:<nombreAtributo> (<parámetros>)]
Aunque cada implementación de C# puede incluir sus propios
indicadores de tipo de elemento, todas ellas incluirán al
menos los siguientes:
-
assembly: Indica que el atributo se aplica al ensamblado
en que se compile el código fuente que lo contenga. Al
definir atributos de ensamblado es obligatorio incluir este
indicador, ya que estos atributos se colocan precediendo
cualquier definición de clase o espacio de nombres y si
no se incluyesen se confundiría con atributos de tipo,
que se colocan en el mismo sitio.
-
module: Indica que el atributo se aplica al módulo
en que se compile el código fuente que lo contenga. Al
igual que el indicador assembly, hay que incluirlo
siempre para definir este tipo de atributos porque si no se
confundirían con atributos de tipo, ya que también
se han de ubicar precediendo las definiciones de clases y
espacios de nombres.
-
type: Indica que el atributo se aplica al tipo cuya
definición precede. En realidad no hace falta utilizarlo,
pues es lo que por defecto se considera para todo atributo que
preceda a una definición de tipo. Sin embargo, se ha
incluido por consistencia con el resto de indicadores de tipo de
atributo y porque puede resultar conveniente incluirlo ya que
explicitarlo facilita la lectura del código.
-
return: Indica que el atributo se aplica a un valor de
retorno de un método, operador, bloque get, o
definición de delegado. Si no se incluyese se
consideraría que se aplica a la definición del
método, operador, bloque get o delegado, ya que
estos atributos se colocan antes de la misma al igual que los
atributos de valores de retorno.
-
param: Indica que el atributo se aplica a un
parámetro de un método. Si no se incluyese al
definir bloques set, add o remove se
consideraría que el atributo se refiere a los bloques en
sí y no al parámetro value en ellos
implícito.
-
method: Indica que el atributo se aplica al método
al que precede. En realidad no es necesario usarlo porque, como
se dice en la explicación de los indicadores param
y return, es lo que se considera por defecto. Sin
embrago, y como pasaba con type, se incluye por
consistencia y porque puede ser buena idea incluirlo para
facilitar la legibilidad del código con su
explicitación.
-
event: Indica que el atributo se aplica al evento a cuya
definición precede. En realidad no es necesario incluirlo
porque es lo que se considera por defecto, pero nuevamente se ha
incluido por consistencia y para facilitar la lectura del
código.
-
property: Indica que el atributo se aplica a la propiedad
a cuya definición precede. Éste también es
es un indicador innecesario e incluido tan sólo por
consistencia y para facilitar la legibilidad del código.
-
field: Indica que el atributo se aplica al cuya
definición precede. Como otros indicadores, sólo
se incluye por consistencia y para hacer más legible el
código.
Definición de
nuevos atributos
Especificación
del nombre del atributo
Se considera que un atributo es toda aquella clase que derive de
System.Attribute. Por tanto, para definir un nuevo tipo de
atributo hay que crear una clase que derive de ella. Por convenio,
a este tipo de clases suele dárseles nombres acabados en
Attribute, aunque a la hora de usarlas desde C# es posible
obviar dicho sufijo. Un ejemplo de cómo definir una atributo
llamado Ayuda es:
using System;
class AyudaAttribute:Attribute
{}
Y ejemplos de cómo usarlo prefijando la definición de
clases son:
[Ayuda] class A
{}
[AyudaAttribute] class B
{}
Puede darse la circunstancia de que se haya definido un atributo
con un cierto nombre sin sufijo Attribute y otro que si lo tenga.
Como es lógico, en ese caso cuando se use el atributo sin
especificar el sufijo se hará referencia a la versión
sin sufijo y cuando se use con sufijo se hará referencia a
la versión con sufijo.
Especificación
del uso de un atributo
Por defecto cualquier atributo que se defina puede preceder
la definición de cualquier elemento del lenguaje. Si
se desea limitar a qué definiciones puede preceder es
necesario prefijar la clase que lo define con un atributo especial
llamado System.AttributeUsage. Este atributo consta de los
siguientes parámetros con nombre:
-
AllowMultiple: Por defecto cada atributo sólo
puede aparecer una vez prefijando a cada elemento.
Dándole el valor true a este parámetro se
considerará que puede aparecer múltiples veces.
-
Inherited: Por defecto los atributos aplicados a una
clase no son heredados en sus clases hijas. Dándole el
valor true a este parámetros se consigue que
sí lo sean.
Aparte de estos dos parámetros, AttributeUsage
también puede contar con un parámetro opcional sin
nombre que indique a qué tipos de definiciones puede
preceder. Por defecto se considera que un atributo puede preceder a
cualquier elemento, lo que es equivalente a darle el valor
AttributeTargets.All a este parámetro. Sin embrago es
posible especificar otras posibilidades dándole valores de
la enumeración System.AttributeTargets, que son
los que se recogen en la Tabla 9:
| Valor de AttributeTargets |
Significa que el atributo puede preceder a... |
|
All
|
Cualquier definición
|
|
Assembly
|
Definiciones de espacio de nombres,
considerándose que el atributo se refiere al
ensamblado en general.
|
|
Module
|
Definiciones de espacio de nombres,
considerándose que el atributo se refiere al
módulo en su conjunto.
|
|
Class
|
Definiciones de clases
|
|
Delegate
|
Definiciones de delegados
|
|
Interface
|
Definiciones de interfaces
|
|
Struct
|
Definiciones de estructuras
|
|
Enum
|
Definiciones de enumeraciones
|
|
Field
|
Definiciones de campos
|
|
Method
|
Definiciones de métodos
|
|
Constructor
|
Definiciones de constructores
|
|
Property
|
Definiciones de propiedades o indizadores
|
|
Event
|
Definiciones de eventos
|
|
Parameter
|
Definiciones de parámetros de
métodos
|
|
ReturnValue
|
Definiciones de valores de retorno de
métodos
|
Tabla
9: Valores de AttributeTargets
Es posible combinar varios de estos valores mediante operaciones
lógicas "or" (carácter |) Por ejemplo,
si queremos definir el atributo Ayuda anterior de modo que
sólo pueda ser usado para prefijar definiciones de
enumeraciones o de clases se haría:
[AttributeUsage(AttributeTargets.Class | AttributeTargetes.Enum)]
class Ayuda:Attribute
{}
Es importante resaltar que AttributeUsage sólo puede
incluirse precediendo definiciones de otros atributos (o sea, de
clases derivadas de System.Attribute)
Especificación de
parámetros válidos
Se considera que los parámetros sin nombre que puede tomar
un atributo son aquellos que se especifiquen como parámetros
en el constructor del tipo que lo define, y que sus
parámetros con nombre serán las propiedades y campos
públicos, no estáticos y de lectura/escritura
definidos en dicho tipo.
Un ejemplo de cómo definir el atributo Ayuda anterior de
modo que tome un parámetro sin nombre con la URL que indique
dónde encontrar la ayuda sobre el miembro o clase al que
precede y un parámetro con nombre llamado Autor que indique
quién es el autor de esa documentación es:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Enum)]
class Ayuda:Attribute
{
private string autor;
private string url;
public Ayuda(string URL)
{ url=URL; }
public string Autor
{
set {autor = value;}
get {return autor;}
}
}
Ejemplos de usos válidos de este atributo son:
[Ayuda("http://www.josan.com/Clases/A.html")]
class A {}
[Ayuda("http://www.josan.com/Clases/B.html", Autor="José Antonio González Seco")]
class B {}
Los tipos válidos de parámetros, tanto con nombre
como sin él, que puede tomar un atributo son: cualquier tipo
básico excepto decimal y los tipos enteros sin signo,
cualquier enumeración pública, System.Type o
tablas unidimensionales de elementos de cualquiera de los
anteriores tipos válidos.
Lectura de atributos en
tiempo de ejecución
Para acceder a los metadatos de cualquier ensamblado se utilizan
las clases del espacio de nombres System.Reflection. Este
espacio de nombres es inmenso y explicar cómo
utilizarlo queda fuera del alcance de este libro, aunque de todos
modos a continuación se darán unas ideas
básicas sobre cómo acceder a través de sus
tipos a los atributos incluidos en los ensamblados.
La clave para acceder a los atributos se encuentra en el
método estático de la clase System.Attribute
llamado Attribute[] GetCustomAttributes(<x>
objetoReflexivo), donde <x> es el tipo de
System.Reflection que representa a los elementos cuyos
atributos se desea obtener. Los posibles tipos son:
Assembly, que representa ensamblados, Module que
representa módulos, MemberInfo que representa
miembros (incluidos tipos, que al fin y al cabo son miembros de
espacios de nombres), y ParameterInfo que representa
parámetros. El parámetros tomado por este
método será el objeto que represente al elemento en
concreto cuyos metadatos se quieren obtener.
Como se ve, GetCustomAttributes() devuelve una tabla con los
atributos en forma de objetos Attribute, que es la clase
base de todos los atributos, por lo que si a partir de ellos se
desease acceder a características específica de cada
tipo de atributo habría que aplicar downcasting como se
comentó en el Tema 5: Clases (para
asegurase de que las conversiones se realicen con éxito
recuérdese que se puede usar el operador is para
determinar cuál es el verdadero tipo de cada atributo de
esta tabla)
Para obtener el objeto Assembly que representa al ensamblado
al que pertenezca el código que se esté ejecutando se
usa el método Assembly GetExecutingAssembly() de la
clase Assembly, que se usa tal y como se muestra:
Assembly ensamblado = Assembly.GetExecutingAssembly();
Otra posibilidad sería obtener ese objeto Assembly a
partir del nombre del fichero donde se encuentre almacenado el
ensamblado. Para ello se usa el método Assembly
LoadFrom(string rutaEnsamblado) de la clase Assembly
como se muestra:
Assembly ensamblado = Assembly.LoadFrom("josan.dll");
Una vez obtenido el objeto que representa a un
ensamblado pueden obtenerse los objetos Module que
representan a los módulos que lo forman a través de
su método Module[] GetModules().
A partir del objeto Module que representa a un módulo
puede obtenerse los objetos Type que representan a sus tipos
a través de su método Type[] GetTypes()
Otra posibilidad sería usar el operador typeof ya
visto para obtener el Type que representa a un tipo en
concreto sin necesidad de crear objetos Module o
Assembly.
En cualquier caso, una vez obtenido un objeto Type, a
través de sus métodos FieldInfo[] GetFields(),
MethodInfo[] GetMethods(), ConstructorInfo[]
GetConstructors(), EventInfo[] GetEvents[] y
PropertyInfo[] GetProperties() pueden obtenerse los objetos
reflexivos que representan, de manera respectiva, a sus campos,
métodos, constructores, eventos y propiedades o
indizadores. Tanto todos estos objetos como los objetos Type
derivan de MemberInfo, por lo que pueden ser pasados como
parámetros de GetCustomAttributes() para obtener los
atributos de los elementos que representan.
Por otro lado, a través de los objetos MethodInfo
y ConstructorInfo, es posible obtener los tipos reflexivos
que representan a los parámetros de métodos y
constructores llamando a su método ParameterInfo[]
GetParameters() Además, en el caso de los objetos
MethodInfo también es posible obtener el objeto que
representa al tipo de retorno del método que representan
mediante su propiedad Type ReturnType {get;}.
En lo referente a las propiedades, es posible obtener los objetos
MethodInfo que representan a sus bloques get y
set a través de los métodos MethodInfo
GetSetMethod() y MethodInfo GetSetMethod() de los
objetos PropertyInfo que las representan. Además,
para obtener los objetos reflexivos que representen a los
índices de los indizadores también se dispone de un
método ParamterInfo[] GetIndexParameters()
Y en cuanto a los eventos, los objetos EventInfo disponen de
métodos MethodInfo GetAddMethod() y MethodInfo
GetRemoveMethod() mediante los que es posible obtener los
objetos reflexivos que representan a sus bloques add y
remove.
A continuación se muestra un programa de ejemplo que lo que
hace es mostrar por pantalla el nombre de todos los atributos que
en él se hayan definido:
using System.Reflection;
using System;
[assembly: EjemploEnsamblado]
[module: EjemploModulo]
[AttributeUsage(AttributeTargets.Method)]
class EjemploMétodo:Attribute
{}
[AttributeUsage(AttributeTargets.Assembly)]
class EjemploEnsamblado:Attribute
{}
[AttributeUsage(AttributeTargets.Module)]
class EjemploModulo:Attribute
{}
[AttributeUsage(AttributeTargets.Class)]
class EjemploTipo:Attribute
{}
[AttributeUsage(AttributeTargets.Field)]
class EjemploCampo:Attribute
{}
[EjemploTipo]
class A
{
public static void Main()
{
Assembly ensamblado = Assembly.GetExecutingAssembly();
foreach (Attribute atributo in Attribute.GetCustomAttributes(ensamblado))
Console.WriteLine("ENSAMBLADO: {0}",atributo);
foreach (Module modulo in ensamblado.GetModules())
{
foreach(Attribute atributo in Attribute.GetCustomAttributes(modulo))
Console.WriteLine("MODULO: {0}", atributo);
foreach (Type tipo in modulo.GetTypes())
{
foreach(Attribute atributo in Attribute.GetCustomAttributes(tipo))
Console.WriteLine("TIPO: {0}", atributo);
foreach (FieldInfo campo in tipo.GetFields())
muestra("CAMPO", campo);
foreach (MethodInfo metodo in tipo.GetMethods())
muestra("METODO", metodo);
foreach (EventInfo evento in tipo.GetEvents())
muestra("EVENTO", evento);
foreach (PropertyInfo propiedad in tipo.GetProperties())
muestra("PROPIEDAD", propiedad);
foreach (ConstructorInfo constructor in tipo.GetConstructors())
muestra("CONSTRUCTOR",constructor);
}
}
}
static private void muestra(string nombre, MemberInfo miembro)
{
foreach (Attribute atributo in Attribute.GetCustomAttributes(miembro))
Console.WriteLine("{0}: {1}", nombre, atributo);
}
}
Lo único que hace el Main() de este programa es
obtener el Assembly que representa el ensamblado actual y
mostrar todos sus atributos de ensamblado. Luego obtiene todos los
Modules que representa a los módulos de dicho
ensamblado y muestra todos los atributos de módulo de
cada uno. Además, de cada módulo se obtienen todos
los Types que representan a los tipos en él definidos
y se muestran todos sus atributos; y de cada tipo se obtienen los
objetos reflexivos que representan a sus diferentes tipos de
miembros y se muestran los atributos atributos de cada miembro.
Aparte del método Main() en el ejemplo se han
incluido definiciones de numerosos atributos de ejemplo aplicables
a diferentes tipos de elemento y se han diseminado a lo largo del
fuente varios usos de estos atributos. Por ello, la salida del
programa es:
ENSAMBLADO: EjemploEnsamblado
ENSAMBLADO: System.Diagnostics.DebuggableAttribute
MODULO EjemploModulo
TIPO: System.AttributeUsageAttribute
TIPO: System.AttributeUsageAttribute
TIPO: System.AttributeUsageAttribute
TIPO: System.AttributeUsageAttribute
TIPO: System.AttributeUsageAttribute
TIPO: EjemploTipo
METODO: EjemploMétodo
Nótese que aparte de los atributos utilizados en el
código fuente, la salida del programa muestra que el
compilador ha asociado a nivel de ensamblado un atributo extra
llamado Debuggable. Este atributo incluye información
sobre si pueden aplicarse optimizaciones al compilar JIT el
ensamblado o si se ha de realizar una traza de su ejecución.
Sin embargo, no conviene fiarse de su implementación ya que
no está documentado por Microsoft y puede cambiar en futuras
versiones de la plataforma .NET.
Atributos de
compilación
Aunque la mayoría de los atributos son interpretados en
tiempo de ejecución por el CLR u otras aplicaciones, hay una
serie de atributos que tienen un significado especial en C# y
condicionan el proceso de compilación. Estos son los que se
explican a continuación.
Atributo
System.AttributeUsage
Ya hemos visto en este mismo tema que se utiliza para indicar
dónde se pueden colocar los nuevos atributos que el
programador defina, por lo que no se hará más
hincapié en él.
Atributo
System.Obsolete
Se puede usar prefijando cualquier elemento de un fichero de
código fuente para indicar que el elemento precedido
está obsoleto. Puede tomar dos parámetros sin nombre:
-
Un primer parámetro de tipo string que contenga una
cadena con un mensaje a mostrar cuando al compilar se detecte que
se ha usado el elemento obsoleto.
-
Un segundo parámetro de tipo bool que indique si se
ha de producir un aviso o un error cuando se detecte el uso del
elemento obsoleto. Por defecto se muestra un aviso, pero si se le
da el valor true a este parámetro se producirá
un error.
El siguiente ejemplo muestra como usar este atributo:
using System;
class Obsoleta
{
[Obsolete("No usar f(), que está obsoleto.", true)]
public static void f()
{}
public static void Main()
{
f();
}
}
Cuando se compile este programa el compilador producirá el
siguiente mensaje de error:
obsolete.cs(11,17): error CS0619: 'Obsoleta.f()' is obsolete: no usr f(), que está obsoleto.
Si se hubiese usado Obsolete sin segundo parámetros,
entonces se mostraría el aviso:
obsolete.cs(11,17): warning CS0618: 'Obsoleta.f()' is obsolete: no usr f(), que está obsoleto.
Atributo
System.Diagnostics.Conditional
Este atributo sólo puede prefijar definiciones de
métodos, y permite definir si las llamadas al método
prefijado se han de compilar o no. Puede usarse múltiples
veces prefijando a un mismo método y toma un
parámetro sin nombre de tipo string. Sólo se
compilarán aquellas llamadas al método tales que en
el momento de hacerlas esté definida alguna directiva de
preprocesado con el mismo nombre que el parámetro de alguno
de los atributos Conditional que prefijen la
definición de ese método.
Como se ve, este atributo ces una buena forma de simplificar
la escritura de código que se deba compilar
condicionalmente, ya que evita tener varias directivas #if
que encierren cada llamada al método cuya ejecución
se desea controlar. Sin embargo, Conditional no controla la
compilación de ese método, sino sólo las
llamadas al mismo.
El siguiente ejemplo muestra cómo usar Conditional:
using System;
using System.Diagnostics;
class Condicional
{
[Conditional("DEBUG")]
public static void F()
{ Console.WriteLine("Ff()"); }
public static void Main()
{
F();
}
}
Sólo si compilamos el este código definiendo la
constante de preprocesado DEBUG se mostrará por pantalla el
mensaje F() En caso contrario, nunca se hará la llamada a
F()
Hay que precisar que en realidad Conditional no puede
preceder a cualquier definición de método, sino que
en su colocación hay impuestas ciertas restricciones
especiales:
-
El método ha de tener un tipo de retorno void.
Esto se debe a que si tuviese otro se podría usar su
valor de retorno como operando en expresiones, y cuando no
fuesen compiladas sus llamadas esas expresiones podrían
no tener sentido y producir errores de compilación.
-
Si se aplica a un método virtual todas sus redefiniciones
lo heredan, siendo erróneo aplicárselo
explícitamente a una de ellas. Esto debe a que en tiempo
de compilación puede no saberse cuál es el
verdadero tipo de un objeto, y si unas redefiniciones pudiesen
ser condicionales y otras no, no podría determinarse al
compilar si es condicional la versión del método a
la que en cada caso se llame.
-
No puede atribuirse a métodos definidos en interfaces ni
a implementaciones de métodos de interfaces, pues son
también virtuales y podrían reimplementarse.