Definición y lectura de atributos personalizados

Los atributos proporcionan una manera de asociar la información con el código de manera declarativa. También pueden proporcionar un elemento reutilizable que se puede aplicar a diversos destinos. Tenga en cuenta ObsoleteAttribute. Se puede aplicar a clases, structs, métodos, constructores y más. Declara que el elemento está obsoleto. Es decisión del compilador de C# buscar este atributo y realizar alguna acción como respuesta.

En este tutorial, obtendrá información sobre cómo agregar atributos al código, cómo crear y usar sus propios atributos y cómo usar algunos atributos que se integran en .NET.

Requisitos previos

Debe configurar la máquina para ejecutar .NET. Puede encontrar las instrucciones de instalación en la página Descargas de .NET. Puede ejecutar esta aplicación en Windows, Ubuntu Linux, macOS o en un contenedor de Docker. Debe instalar el editor de código que prefiera. En las siguientes descripciones se usa Visual Studio Code, que es un editor multiplataforma de código abierto. Sin embargo, puede usar las herramientas que le resulten más cómodas.

Creación de la aplicación

Ahora que ha instalado todas las herramientas, cree una nueva aplicación de consola de .NET. Para usar el generador de línea de comandos, ejecute el siguiente comando en su shell favorito:

dotnet new console

Este comando crea archivos de proyecto de .NET básicos. Ejecute dotnet restore para restaurar las dependencias necesarias para compilar este proyecto.

No es necesario ejecutar dotnet restore porque lo ejecutan implícitamente todos los comandos que necesitan que se produzca una restauración, como dotnet new, dotnet build, dotnet run, dotnet test, dotnet publish y dotnet pack. Para deshabilitar la restauración implícita, use la opción --no-restore.

El comando dotnet restore sigue siendo válido en algunos escenarios donde tiene sentido realizar una restauración explícita, como las compilaciones de integración continua en Azure DevOps Services o en los sistemas de compilación que necesitan controlar explícitamente cuándo se produce la restauración.

Para obtener información sobre cómo administrar fuentes de NuGet, vea la documentación de dotnet restore.

Para ejecutar el programa, use dotnet run. Deberá ver la salida "Hola a todos" a la consola.

Agregar atributos al código

En C#, los atributos son clases que se heredan de la clase base Attribute. Cualquier clase que se hereda de Attribute puede usarse como una especie de "etiqueta" en otros fragmentos de código. Por ejemplo, hay un atributo llamado ObsoleteAttribute. Este atributo indica que el código está obsoleto y que ya no se debe usar. Puede colocar este atributo en una clase, por ejemplo, mediante corchetes.

[Obsolete]
public class MyClass
{
}

Mientras que la clase se denomina ObsoleteAttribute, solo es necesario usar [Obsolete] en el código. La mayoría del código de C# sigue esta convención. Puede usar el nombre completo [ObsoleteAttribute] si así lo prefiere.

Cuando se marca una clase como obsoleta, es una buena idea proporcionar alguna información de por qué es obsoleta, o qué usar en su lugar. Se incluye un parámetro de cadena en el atributo Obsoleto para proporcionar esta explicación.

[Obsolete("ThisClass is obsolete. Use ThisClass2 instead.")]
public class ThisClass
{
}

La cadena se pasa como argumento a un constructor ObsoleteAttribute, como si estuviera escribiendo var attr = new ObsoleteAttribute("some string").

Los parámetros a un constructor de atributos se limitan a literales o tipos simples: bool, int, double, string, Type, enums, etc y matrices de esos tipos. No se puede usar una expresión o una variable. Es libre de usar parámetros posicionales o con nombre.

Crear su propio atributo

Para crear un atributo, defina una nueva clase que herede de la clase base Attribute.

public class MySpecialAttribute : Attribute
{
}

Con el código anterior, puede usar [MySpecial] (o [MySpecialAttribute]) como atributo en otra parte del código base.

[MySpecial]
public class SomeOtherClass
{
}

Los atributos de la biblioteca de clases base de .NET como ObsoleteAttribute desencadenan ciertos comportamientos en el compilador. Sin embargo, cualquier atributo que cree funcionará como metadatos y no tendrá como resultado ningún código dentro de la clase de atributo que se ejecuta. Es decisión suya actuar sobre esos metadatos en otra parte del código.

Aquí hay un problema que se debe vigilar. Como se mencionó anteriormente, solo se pueden pasar determinados tipos como argumentos al usar atributos. Sin embargo, al crear un tipo de atributo, el compilador de C# no le impide crear esos parámetros. En el ejemplo siguiente, ha creado un atributo con un constructor que se compila correctamente.

public class GotchaAttribute : Attribute
{
    public GotchaAttribute(Foo myClass, string str)
    {
    }
}

Sin embargo, no puede usar este constructor con una sintaxis de atributo.

[Gotcha(new Foo(), "test")] // does not compile
public class AttributeFail
{
}

El código anterior provoca un error del compilador, como Attribute constructor parameter 'myClass' has type 'Foo', which is not a valid attribute parameter type

Cómo restringir el uso de atributos

Los atributos se pueden usar en los siguientes "destinos". Los ejemplos anteriores los muestran en las clases, pero ahora se pueden usar en:

  • Ensamblado
  • Clase
  • Constructor
  • Delegar
  • Enumeración
  • evento
  • Campo
  • GenericParameter
  • Interfaz
  • Método
  • Módulo
  • Parámetro
  • Propiedad.
  • ReturnValue
  • Estructura

Cuando se crea una clase de atributo, de forma predeterminada, C# permite usar ese atributo en cualquiera de los destinos de atributo posibles. Si desea restringir el atributo a determinados destinos, puede hacerlo mediante la clase de atributo AttributeUsageAttribute. ¡Sí, un atributo en un atributo!

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class MyAttributeForClassAndStructOnly : Attribute
{
}

Si intenta colocar el atributo anterior en algo que no sea una clase o una estructura, obtendrá un error del compilador como Attribute 'MyAttributeForClassAndStructOnly' is not valid on this declaration type. It is only valid on 'class, struct' declarations

public class Foo
{
    // if the below attribute was uncommented, it would cause a compiler error
    // [MyAttributeForClassAndStructOnly]
    public Foo()
    { }
}

Cómo usar los atributos asociados a un elemento de código

Los atributos actúan como metadatos. Sin algo de fuerza exterior, no harían realmente nada.

Para buscar y actuar sobre los atributos, se requiere en general reflexión. La reflexión permite escribir código en C# que examina otro código. Por ejemplo, puede usar reflexión para obtener información sobre una clase (agregue using System.Reflection; al principio del código):

TypeInfo typeInfo = typeof(MyClass).GetTypeInfo();
Console.WriteLine("The assembly qualified name of MyClass is " + typeInfo.AssemblyQualifiedName);

Se muestra algo parecido a: The assembly qualified name of MyClass is ConsoleApplication.MyClass, attributes, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

Una vez que tenga un objeto TypeInfo (o un objeto MemberInfo, FieldInfo o de otro tipo) puede usar el método GetCustomAttributes. Este método devuelve una colección de objetos Attribute. También puede usar GetCustomAttribute y especificar un tipo de atributo.

Este es un ejemplo del uso de GetCustomAttributes en una instancia de MemberInfo para MyClass (que anteriormente vimos que contiene un atributo [Obsolete]).

var attrs = typeInfo.GetCustomAttributes();
foreach(var attr in attrs)
    Console.WriteLine("Attribute on MyClass: " + attr.GetType().Name);

Se muestra lo siguiente en la consola: Attribute on MyClass: ObsoleteAttribute. Intente agregar otros atributos a MyClass.

Es importante tener en cuenta que se crean instancias de estos objetos Attribute de forma diferida. Es decir, no podrá crea una instancia de ellos hasta que use GetCustomAttribute o GetCustomAttributes. También se crea una instancia de ellos cada vez. Al llamar a GetCustomAttributes dos veces en una fila se devuelven dos instancias diferentes de ObsoleteAttribute.

Atributos comunes en el entorno de ejecución

Muchas herramientas y marcos de trabajo emplean atributos. NUnit usa atributos como [Test] y [TestFixture] que son utilizados por la serie de pruebas NUnit. ASP.NET MVC usa atributos como [Authorize] y proporciona un marco de filtro de acción para ejecutar problemas transversales en acciones de MVC. PostSharp usa la sintaxis de atributo para permitir la programación en C# orientada a aspectos.

Estos son algunos atributos importantes integrados en las bibliotecas de clases base de .NET Core:

  • [Obsolete]. Este se usó en los ejemplos anteriores, y se encuentra en el espacio de nombres System. Es útil proporcionar documentación declarativa sobre una base de código cambiante. Se puede proporcionar un mensaje en forma de cadena, y se puede usar otro parámetro booleano para escalarlo de una advertencia del compilador a un error del compilador.
  • [Conditional]. Este atributo está en el espacio de nombres System.Diagnostics. Este atributo se puede aplicar a métodos (o clases de atributos). Debe pasar una cadena al constructor. Si esa cadena no coincide con una directiva #define, el compilador de C# quita las llamadas a ese método (pero no el método propiamente dicho). Normalmente, se usa esta técnica con fines de depuración (diagnóstico).
  • [CallerMemberName]. Este atributo se puede usar en parámetros, y reside en el espacio de nombres System.Runtime.CompilerServices. CallerMemberName es un atributo que se usa para inyectar el nombre del método que llama a otro método. Es una manera de eliminar "cadenas mágicas" al implementar INotifyPropertyChanged en distintos marcos de interfaz de usuario. Por ejemplo:
public class MyUIClass : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    public void RaisePropertyChanged([CallerMemberName] string propertyName = default!)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private string? _name;
    public string? Name
    {
        get { return _name;}
        set
        {
            if (value != _name)
            {
                _name = value;
                RaisePropertyChanged();   // notice that "Name" is not needed here explicitly
            }
        }
    }
}

En el código anterior, no necesita tener una cadena "Name" de literal. El uso de CallerMemberName evita los errores relacionados con los tipos y también agiliza los procesos de refactorización o cambio de nombre. Los atributos traen la eficacia declarativa a C#, pero son una forma de metadatos de código y no actúan por sí mismos.