Zona HTML Zona Java Zona PHP Zona ASP Zona Bases de datos
Inicio > Tutoriales > Lenguajes orientados a objeto > C# > El lenguaje de programación C#
-Tutoriales

El lenguaje de programación C#


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

Copyright © 1999-2010 Programación en castellano. Todos los derechos reservados.
Formulario de Contacto - Datos legales - Publicidad

diseño y desarrollo web por Color Vivo Internet. Un proyecto de los Hermanos Carrero