Varios atributos interpretados por el compilador de C#

Hay varios atributos que se pueden aplicar a los elementos en su código que agregan significado semántico a esos elementos:

  • Conditional: hace que la ejecución de un método dependa de un identificador de preprocesador.
  • Obsolete: marca un tipo o miembro para una (potencial) eliminación futura.
  • AttributeUsage: declara los elementos del lenguaje en los que se puede aplicar un atributo.
  • AsyncMethodBuilder: declara un tipo de generador de métodos asincrónico.
  • InterpolatedStringHandler: define un generador de cadenas interpoladas para un escenario conocido.
  • ModuleInitializer: declara un método que inicializa un módulo.
  • SkipLocalsInit: omite el código que inicializa el almacenamiento de variables locales a 0.
  • UnscopedRef: declara que una variable ref que normalmente se interpreta como scoped debe tratarse como sin ámbito.
  • OverloadResolutionPriority: agregue un atributo de desempate para influir en la resolución de sobrecargas para sobrecargas posiblemente ambiguas.
  • Experimental: marca un tipo o miembro como experimental.

El compilador usa esos significados semánticos para modificar su salida e informar de los posibles errores por parte de los desarrolladores que usan el código.

Atributo Conditional

El atributo Conditional hace que la ejecución de un método dependa de un identificador de preprocesamiento. El atributo Conditional es un alias de ConditionalAttribute y se puede aplicar a un método o a una clase de atributo.

En el ejemplo siguiente, Conditional se aplica a un método para habilitar o deshabilitar la representación de información de diagnóstico específica del programa:

#define TRACE_ON
using System.Diagnostics;

namespace AttributeExamples;

public class Trace
{
    [Conditional("TRACE_ON")]
    public static void Msg(string msg)
    {
        Console.WriteLine(msg);
    }
}

public class TraceExample
{
    public static void Main()
    {
        Trace.Msg("Now in Main...");
        Console.WriteLine("Done.");
    }
}

Si no se define el identificador TRACE_ON, no se muestra el resultado del seguimiento. Explore por sí mismo en la ventana interactiva.

El atributo Conditional se suele usar con el identificador DEBUG para habilitar las funciones de seguimiento y de registro para las compilaciones de depuración, pero no en las compilaciones de versión, como se muestra en el ejemplo siguiente:

[Conditional("DEBUG")]
static void DebugMethod()
{
}

Al llamar a un método marcado como condicional, la presencia o ausencia del símbolo de preprocesamiento especificado determina si el compilador incluye u omite la llamada al método. Si el símbolo está definido, se incluye la llamada; de lo contrario, se omite la llamada. Un método condicional debe ser un método de una declaración de clase o estructura, y no debe tener un tipo de valor devuelto void. El uso de Conditional resulta más limpio y elegante, y menos propenso a generar errores que incluir los métodos dentro de bloques #if…#endif.

Si un método tiene varios atributos Conditional, el compilador incluye llamadas al método si se define uno o más símbolos condicionales (los símbolos se vinculan de manera lógica entre sí mediante el operador OR). En el ejemplo siguiente, la presencia de A o B da como resultado una llamada al método:

[Conditional("A"), Conditional("B")]
static void DoIfAorB()
{
    // ...
}

Uso de Conditional con clases de atributos

El atributo Conditional también se puede aplicar a una definición de clase de atributo. En el ejemplo siguiente, el atributo personalizado Documentation agrega información a los metadatos si se define DEBUG.

[Conditional("DEBUG")]
public class DocumentationAttribute : System.Attribute
{
    string text;

    public DocumentationAttribute(string text)
    {
        this.text = text;
    }
}

class SampleClass
{
    // This attribute will only be included if DEBUG is defined.
    [Documentation("This method displays an integer.")]
    static void DoWork(int i)
    {
        System.Console.WriteLine(i.ToString());
    }
}

Atributo Obsolete

El atributo Obsolete marca un elemento de código como ya no recomendado para su uso. El uso de una entidad marcada como obsoleta genera una advertencia o un error. El atributo Obsolete es un atributo de uso único y se puede aplicar a cualquier entidad que admita atributos. Obsolete es un alias de ObsoleteAttribute.

En el ejemplo siguiente, el atributo Obsolete se aplica a la clase A y al método B.OldMethod. Dado que el segundo argumento del constructor de atributos aplicado a B.OldMethod se establece en true, este método produce un error del compilador, mientras que el uso de la clase A genera una advertencia. En cambio, si se llama a B.NewMethod, no se generará ninguna advertencia o error. Por ejemplo, al usarla con las definiciones anteriores, el código siguiente genera dos advertencias y un error:


namespace AttributeExamples
{
    [Obsolete("use class B")]
    public class A
    {
        public void Method() { }
    }

    public class B
    {
        [Obsolete("use NewMethod", true)]
        public void OldMethod() { }

        public void NewMethod() { }
    }

    public static class ObsoleteProgram
    {
        public static void Main()
        {
            // Generates 2 warnings:
            A a = new A();

            // Generate no errors or warnings:
            B b = new B();
            b.NewMethod();

            // Generates an error, compilation fails.
            // b.OldMethod();
        }
    }
}

La cadena proporcionada como primer argumento al constructor del atributo se muestra como parte de la advertencia o el error. Se generan dos advertencias para la clase A: una para la declaración de la referencia de clase y otra para el constructor de clases. El atributo Obsolete se puede usar sin argumentos, pero se recomienda incluir una explicación de qué se debe usar en su lugar.

En C# 10, puede usar la interpolación de cadenas constantes y el operador nameof para asegurarse de que los nombres coinciden:

public class B
{
    [Obsolete($"use {nameof(NewMethod)} instead", true)]
    public void OldMethod() { }

    public void NewMethod() { }
}

Atributo Experimental

Desde C# 12, los tipos, métodos y ensamblados se pueden marcar con el System.Diagnostics.CodeAnalysis.ExperimentalAttribute para indicar una característica experimental. El compilador emite una advertencia si tiene acceso a un método o tipo anotado con el ExperimentalAttribute. Todos los tipos declarados en un ensamblado o módulo marcados con el atributo Experimental son experimentales. El compilador emite una advertencia si accede a cualquiera de ellos. Puede deshabilitar estas advertencias para probar una característica experimental.

Advertencia

Las características experimentales están sujetas a cambios. Las API pueden cambiar o se pueden quitar en futuras actualizaciones. Incluir características experimentales es una manera de que los autores de bibliotecas obtengan comentarios sobre ideas y conceptos para el desarrollo futuro. Tenga precaución extrema al usar cualquier característica marcada como experimental.

Puede leer más detalles sobre el atributo Experimental en la especificación de características.

Atributo SetsRequiredMembers

El atributo SetsRequiredMembers informa al compilador de que un constructor establece todos los miembros required de esa clase o estructura. El compilador supone que cualquier constructor con el atributo System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute inicializa todos los miembros required. Cualquier código que invoque este constructor no necesita inicializadores de objeto para establecer los miembros necesarios. Agregar el atributo SetsRequiredMembers es principalmente útil para registros posicionales y constructores principales.

Atributo AttributeUsage

El atributo AttributeUsage determina cómo se puede usar una clase de atributo personalizado. AttributeUsageAttribute es un atributo que se aplica a las definiciones de atributo personalizado. El atributo AttributeUsage permite controlar lo siguiente:

  • A qué elementos de programa se puede aplicar el atributo. A menos que restrinja su uso, se puede aplicar un atributo a cualquiera de los siguientes elementos del programa:
    • Ensamblado
    • Módulo
    • Campo
    • Evento
    • Método
    • Parámetro
    • Propiedad
    • Valor devuelto
    • Tipo
  • Si un atributo se puede aplicar a un mismo elemento de programa varias veces.
  • Si las clases derivadas heredan atributos.

La configuración predeterminada se parece al siguiente ejemplo cuando se aplica explícitamente:

[AttributeUsage(AttributeTargets.All,
                   AllowMultiple = false,
                   Inherited = true)]
class NewAttribute : Attribute { }

En este ejemplo, la clase NewAttribute se puede aplicar a cualquier elemento de programación compatible, pero solamente se puede aplicar una vez a cada entidad. Las clases derivadas heredan el atributo aplicado a una clase base.

Los argumentos AllowMultiple y Inherited son opcionales, por lo que el siguiente código tiene el mismo efecto:

[AttributeUsage(AttributeTargets.All)]
class NewAttribute : Attribute { }

El primer argumento AttributeUsageAttribute debe ser uno o varios elementos de la enumeración AttributeTargets. Se pueden vincular diversos tipos de destino con el operador OR, como se refleja en el siguiente ejemplo:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
class NewPropertyOrFieldAttribute : Attribute { }

Los atributos se pueden aplicar a la propiedad o al campo de respaldo de una propiedad autoimplementada. El atributo se aplica a la propiedad, a menos que se indique el especificador field en el atributo. Ambos se muestran en el siguiente ejemplo:

class MyClass
{
    // Attribute attached to property:
    [NewPropertyOrField]
    public string Name { get; set; } = string.Empty;

    // Attribute attached to backing field:
    [field: NewPropertyOrField]
    public string Description { get; set; } = string.Empty;
}

Si el argumento AllowMultiple es true, el atributo resultante se puede aplicar más de una vez a cada una de las entidades, como se muestra en el siguiente ejemplo:

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
class MultiUse : Attribute { }

[MultiUse]
[MultiUse]
class Class1 { }

[MultiUse, MultiUse]
class Class2 { }

En este caso, MultiUseAttribute se puede aplicar varias veces porque AllowMultiple está establecido en true. Los dos formatos mostrados para aplicar varios atributos son válidos.

Si Inherited es false, las clases derivadas no heredan el atributo de una clase base con atributos. Por ejemplo:

[AttributeUsage(AttributeTargets.Class, Inherited = false)]
class NonInheritedAttribute : Attribute { }

[NonInherited]
class BClass { }

class DClass : BClass { }

En este caso, NonInheritedAttribute no se aplica a DClass mediante la herencia.

También puede usar estas palabras clave para especificar dónde se debe aplicar un atributo. Por ejemplo, puede usar el field: especificador para agregar un atributo al campo de respaldo de una propiedad implementada automáticamente. También puede usar el especificador field:, property: o param: para aplicar un atributo a cualquiera de los elementos generados a partir de un registro posicional. Para obtener un ejemplo, vea Sintaxis posicional para la definición de propiedades.

Atributo AsyncMethodBuilder

El atributo System.Runtime.CompilerServices.AsyncMethodBuilderAttribute se agrega a un tipo que puede ser un tipo de valor devuelto asincrónico. El atributo especifica el tipo que compila la implementación del método asincrónico cuando se devuelve el tipo especificado desde un método asincrónico. El atributo AsyncMethodBuilder se puede aplicar a un tipo que:

El constructor del atributo AsyncMethodBuilder especifica el tipo del generador asociado. El generador debe implementar los siguientes miembros accesibles:

  • Un método Create() estático que devuelve el tipo del compilador.

  • Una propiedad Task legible que devuelve el tipo de valor devuelto asincrónico.

  • Un método void SetException(Exception) que establece la excepción cuando se produce un error en una tarea.

  • Un método void SetResult() o void SetResult(T result) que marca la tarea como completada y, opcionalmente, establece el resultado de la tarea.

  • Un método Start con la signatura de API siguiente:

    void Start<TStateMachine>(ref TStateMachine stateMachine)
              where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine
    
  • un método AwaitOnCompleted con la signatura siguiente:

    public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : System.Runtime.CompilerServices.INotifyCompletion
        where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine
    
  • un método AwaitUnsafeOnCompleted con la signatura siguiente:

          public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
              where TAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion
              where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine
    

Para obtener información sobre los generadores de métodos asincrónicos, lea sobre los generadores siguientes proporcionados por .NET:

En C# 10 y versiones posteriores, el atributo AsyncMethodBuilder se puede aplicar a un método asincrónico para invalidar el generador de ese tipo.

Atributos InterpolatedStringHandler y InterpolatedStringHandlerArguments

A partir de C# 10, se usan estos atributos para especificar que un tipo es un controlador de cadenas interpoladas. La biblioteca de .NET 6 ya incluye System.Runtime.CompilerServices.DefaultInterpolatedStringHandler para escenarios en los que se usa una cadena interpolada como argumento para un parámetro string. Es posible que tenga otras instancias en las que quiera controlar cómo se procesan las cadenas interpoladas. Aplique System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute al tipo que implementa el controlador. Aplique System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute a los parámetros del constructor de ese tipo.

Puede obtener más información sobre la creación de un controlador de cadenas interpoladas en la especificación de características de C# 10 sobre las mejoras de cadenas interpoladas.

Atributo ModuleInitializer

El atributo ModuleInitializer marca un método al que llama el tiempo de ejecución cuando se carga el ensamblado. ModuleInitializer es un alias de ModuleInitializerAttribute.

El atributo ModuleInitializer solo se puede aplicar a un método que:

  • Sea estático.
  • No tenga parámetros.
  • Devuelve void.
  • Sea accesible desde el módulo contenedor, es decir, internal o public.
  • No sea un método genérico.
  • No esté incluido en una clase genérica.
  • No sea una función local.

El atributo ModuleInitializer se puede aplicar a varios métodos. En ese caso, el orden de llamada desde el entorno de ejecución es determinista pero no se especifica.

En el ejemplo siguiente se muestra el uso de varios métodos de inicializador de módulo. Los métodos Init1 y Init2 se ejecutan antes que Main y cada uno agrega una cadena a la propiedad Text. Por tanto, cuando se ejecuta Main, la propiedad Text ya tiene cadenas de los dos métodos de inicializador.

using System;

internal class ModuleInitializerExampleMain
{
    public static void Main()
    {
        Console.WriteLine(ModuleInitializerExampleModule.Text);
        //output: Hello from Init1! Hello from Init2!
    }
}
using System.Runtime.CompilerServices;

internal class ModuleInitializerExampleModule
{
    public static string? Text { get; set; }

    [ModuleInitializer]
    public static void Init1()
    {
        Text += "Hello from Init1! ";
    }

    [ModuleInitializer]
    public static void Init2()
    {
        Text += "Hello from Init2! ";
    }
}

En ocasiones los generadores de código deben generar código de inicialización. Los inicializadores de módulos proporcionan una ubicación estándar para ese código. En la mayoría de los demás casos, debe escribir un constructor estático en lugar de un inicializador de módulo.

Atributo SkipLocalsInit

El atributo SkipLocalsInit impide que el compilador establezca la marca .locals init al emitir los metadatos. SkipLocalsInit es un atributo de uso único y se puede aplicar a un método, una propiedad, una clase, una estructura, una interfaz o un módulo, pero no a un ensamblado. SkipLocalsInit es un alias de SkipLocalsInitAttribute.

La marca .locals init hace que el CLR inicialice todas las variables locales declaradas en un método en sus valores predeterminados. Como el compilador también se asegura de que nunca se use una variable antes de asignarle un valor, .locals init no suele ser necesario. Sin embargo, la inicialización cero adicional podría tener un impacto medible en el rendimiento en algunos escenarios, como cuando se usa stackalloc para asignar una matriz en la pila. En esos casos, puede agregar el atributo SkipLocalsInit. Si se aplica directamente a un método, el atributo afecta a ese método y a todas sus funciones anidadas, incluidas las expresiones lambda y las funciones locales. Si se aplica a un tipo o un módulo, afecta a todos los métodos anidados. Este atributo no afecta a los métodos abstractos, pero sí al código generado para la implementación.

Este atributo necesita la opción del compilador AllowUnsafeBlocks. Este requisito indica que, en algunos casos, el código podría ver la memoria no asignada (por ejemplo, la lectura de la memoria asignada a la pila no inicializada).

En el ejemplo siguiente se muestra el efecto del atributo SkipLocalsInit en un método que usa stackalloc. El método muestra lo que hubiera en la memoria al asignar la matriz de enteros.

[SkipLocalsInit]
static void ReadUninitializedMemory()
{
    Span<int> numbers = stackalloc int[120];
    for (int i = 0; i < 120; i++)
    {
        Console.WriteLine(numbers[i]);
    }
}
// output depends on initial contents of memory, for example:
//0
//0
//0
//168
//0
//-1271631451
//32767
//38
//0
//0
//0
//38
// Remaining rows omitted for brevity.

Para probar este código, establezca la opción del compilador AllowUnsafeBlocks en el archivo .csproj:

<PropertyGroup>
  ...
  <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

Atributo UnscopedRef

El atributo UnscopedRef marca una declaración de variable como sin ámbito, lo que significa que se permite que la referencia se escape.

Agregue este atributo donde el compilador trata un ref como implícitamente scoped:

  • El parámetro this para los métodos de instancia de struct.
  • ref parámetros que hacen referencia a tipos de ref struct.
  • Parámetros out.

Aplicar el System.Diagnostics.CodeAnalysis.UnscopedRefAttribute marca el elemento como sin ámbito.

Atributo OverloadResolutionPriority

OverloadResolutionPriorityAttribute permite a los autores de bibliotecas preferir una sobrecarga a otra cuando dos sobrecargas pueden ser ambiguas. Su uso principal es que los autores de bibliotecas puedan escribir sobrecargas de mejor rendimiento sin que se interrumpa el código existente.

Por ejemplo, puede agregar una nueva sobrecarga que use ReadOnlySpan<T> para reducir las asignaciones de memoria:

[OverloadResolutionPriority(1)]
public void M(params ReadOnlySpan<int> s) => Console.WriteLine("Span");
// Default overload resolution priority of 0
public void M(params int[] a) => Console.WriteLine("Array");

La resolución de sobrecargas considera que los dos métodos son igual de buenos para algunos tipos de argumentos. Para un argumento de int[], prefiere la primera sobrecarga. Para que el compilador prefiera la versión ReadOnlySpan, puede aumentar la prioridad de esa sobrecarga. En el ejemplo siguiente se muestra el efecto de agregar el atributo:

var d = new OverloadExample();
int[] arr = [1, 2, 3];
d.M(1, 2, 3, 4); // Prints "Span"
d.M(arr); // Prints "Span" when PriorityAttribute is applied
d.M([1, 2, 3, 4]); // Prints "Span"
d.M(1, 2, 3, 4); // Prints "Span"

Todas las sobrecargas con una prioridad menor que la prioridad de sobrecarga más alta se quitan del conjunto de métodos aplicables. Los métodos sin este atributo tienen la prioridad de sobrecarga establecida en el valor predeterminado de cero. Los autores de bibliotecas deben usar este atributo como último recurso al agregar una sobrecarga de método nueva y mejor. Los autores de bibliotecas deben tener una comprensión profunda de cómo afecta la resolución de sobrecargas al elegir el mejor método. De lo contrario, pueden producirse errores inesperados.

Consulte también