Atributos diversos interpretados pelo compilador C#

Os atributos Conditional, Obsolete, AttributeUsage, AsyncMethodBuilder, InterpolatedStringHandler, ModuleInitializer e Experimental podem ser aplicados a elementos em seu código. Eles adicionam significado semântico a esses elementos. O compilador usa esses significados semânticos para alterar a saída e relatar possíveis erros cometidos pelos desenvolvedores que usam seu código.

Atributo Conditional

O atributo Conditional torna a execução de um método dependente de um identificador de pré-processamento. O atributo Conditional é um alias para ConditionalAttribute e pode ser aplicado a um método ou uma classe de atributo.

No exemplo seguinte, Conditional é aplicado a um método para habilitar ou desabilitar a exibição de informações de diagnóstico específicas do 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.");
    }
}

Se o identificador TRACE_ON não estiver definido, a saída de rastreamento não será exibida. Explore por conta própria na janela interativa.

O atributo Conditional é frequentemente usado com o identificador DEBUG para habilitar os recursos de rastreamento e log em builds de depuração, mas não em builds de versão, conforme mostrado no seguinte exemplo:

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

Quando um método marcado como condicional é chamado, a presença ou a ausência do símbolo de pré-processamento especificado determina se o compilador inclui ou omite chamadas para o método. Se o símbolo estiver definido, a chamada será incluída, caso contrário, a chamada será omitida. Um método condicional deve ser um método em uma declaração de classe ou struct e deve ter um tipo de retorno void. Usar Conditional é mais limpo, mais elegante e menos propenso a erros do que os métodos delimitadores dentro de blocos #if…#endif.

Se um método tiver vários atributos Conditional, o compilador incluirá chamadas ao método se um ou mais símbolos condicionais forem definidos (os símbolos serão vinculados logicamente entre si usando o operador OR). No seguinte exemplo, a presença de um A ou B resulta em uma chamada de método:

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

Usar Conditional com classes de atributo

O atributo Conditional também pode ser aplicado a uma definição de classe de atributos. No exemplo a seguir, o atributo Documentation personalizado adiciona informações aos metadados, se DEBUG é definido.

[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

O atributo Obsolete marca um elemento de código como não mais recomendado para uso. O uso de uma entidade marcada como obsoleta gera um aviso ou um erro. O atributo Obsolete é um atributo de uso único e pode ser aplicado a qualquer entidade que permite atributos. Obsolete é um alias para ObsoleteAttribute.

No exemplo a seguir, o atributo Obsolete é aplicado à classe A e ao método B.OldMethod. Como o segundo argumento do construtor de atributo aplicado B.OldMethod é definido como true, esse método causa um erro do compilador, enquanto o uso da classe A produz um aviso. Chamar B.NewMethod, no entanto, não produz aviso nem erro. Por exemplo, ao usá-lo com as definições anteriores, o código a seguir gera um erro e dois avisos:


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();
        }
    }
}

A cadeia de caracteres fornecida como o primeiro argumento para o construtor de atributo é exibida como parte do aviso ou erro. São gerados dois avisos para a classe A: um para a declaração da referência de classe e outro para o construtor de classe. O atributo Obsolete pode ser usado sem argumentos, mas é recomendável incluir uma explicação do que deve ser usado no lugar.

No C# 10, você pode usar a interpolação de cadeia de caracteres constante e o operador nameof para garantir que os nomes correspondam:

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

    public void NewMethod() { }
}

Atributo Experimental

A partir do C# 12, tipos, métodos e assemblies podem ser marcados com System.Diagnostics.CodeAnalysis.ExperimentalAttribute para indicar um recurso experimental. O compilador emitirá um aviso se você acessar um método ou digitar anotado com o ExperimentalAttribute. Todos os tipos declarados em um assembly ou módulo marcados com o atributo Experimental são experimentais. O compilador emite um aviso se você acessar qualquer um deles. Você pode desabilitar esses avisos para piloto de um recurso experimental.

Aviso

Os recursos experimentais estão sujeitos a alterações. As APIs podem ser alteradas ou podem ser removidas em atualizações futuras. Incluir recursos experimentais é uma maneira de os autores da biblioteca receberem comentários sobre ideias e conceitos para desenvolvimento futuro. Tenha extrema cautela ao usar qualquer recurso marcado como experimental.

Você pode ler mais detalhes sobre o atributo Experimental na especificação do recurso.

Atributo SetsRequiredMembers

O atributo SetsRequiredMembers informa ao compilador que um construtor define todos os membros required nessa classe ou estrutura. O compilador assume que qualquer construtor com o atributo System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute inicializa todos os membros required. Qualquer código que invoque tal construtor não precisa de inicializadores de objeto para definir os membros necessários. Adicionar o atributo SetsRequiredMembers é útil principalmente para registros posicionais e construtores primários.

Atributo AttributeUsage

O atributo AttributeUsage determina como uma classe de atributos personalizados pode ser usada. AttributeUsageAttribute é um atributo aplicado a definições de atributo personalizado. O atributo AttributeUsage permite que você controle:

  • A quais elementos de programa o atributo pode ser aplicado. A menos que você restrinja seu uso, um atributo pode ser aplicado a qualquer um dos seguintes elementos de programa:
    • Assembly
    • Módulo
    • Campo
    • Evento
    • Método
    • Param
    • Propriedade
    • Retorno
    • Tipo
  • Indica se um atributo pode ser aplicado a um único elemento do programa várias vezes.
  • Se classes derivadas herdam atributos.

As configurações padrão se parecem com o seguinte exemplo quando aplicadas explicitamente:

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

Neste exemplo, a classe NewAttribute pode ser aplicada a qualquer elemento de programa compatível. Porém, ele pode ser aplicado apenas uma vez para cada entidade. Classes derivadas herdam o atributo aplicado a uma classe base.

Os argumentos AllowMultiple e Inherited são opcionais e, portanto, o seguinte código tem o mesmo efeito:

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

O primeiro argumento AttributeUsageAttribute deve ser um ou mais elementos da enumeração AttributeTargets. Vários tipos de destino podem ser vinculados junto com o operador OR, como mostra o seguinte exemplo:

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

Os atributos podem ser aplicados à propriedade ou ao campo de backup de uma propriedade simplificada automaticamente. O atributo se aplica à propriedade, a menos que você especifique o especificador field no atributo. Ambos são mostrados no seguinte exemplo:

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;
}

Se o argumento AllowMultiple for true, o atributo resultante poderá ser aplicado mais de uma vez a uma única entidade, conforme mostrado no seguinte exemplo:

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

[MultiUse]
[MultiUse]
class Class1 { }

[MultiUse, MultiUse]
class Class2 { }

Nesse caso, MultiUseAttribute pode ser aplicado repetidas vezes porque AllowMultiple está definido como true. Os dois formatos mostrados para a aplicação de vários atributos são válidos.

Se Inherited for false, as classes derivadas não herdarão o atributo de uma classe base atribuída. Por exemplo:

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

[NonInherited]
class BClass { }

class DClass : BClass { }

Nesse caso, NonInheritedAttribute não é aplicado para DClass por meio de herança.

Você também pode usar essas palavras-chave para especificar onde um atributo deve ser aplicado. Por exemplo, você pode usar o especificador field: para adicionar um atributo ao campo de backup de uma propriedade autoimplementada. Ou você pode usar o especificador field:, property: ou param: aplicar um atributo a qualquer um dos elementos gerados por meio de um registro posicional. Para obter um exemplo, confira a Sintaxe posicional para definição de propriedade.

Atributo AsyncMethodBuilder

Você adiciona o atributo System.Runtime.CompilerServices.AsyncMethodBuilderAttribute a um tipo que pode ser um tipo de retorno assíncrono. O atributo especifica o tipo que cria a implementação do método assíncrono quando o tipo especificado é retornado de um método assíncrono. O atributo AsyncMethodBuilder pode ser aplicado a um tipo que:

O construtor do atributo AsyncMethodBuilder especifica o tipo do construtor associado. O construtor deve implementar os seguintes membros acessíveis:

  • Um método estático Create() que retorna o tipo do construtor.

  • Uma propriedade Task legível que retorna o tipo de retorno assíncrono.

  • Um método void SetException(Exception) que define a exceção quando uma tarefa falha.

  • Um método void SetResult() ou void SetResult(T result) que marca a tarefa como concluída e, opcionalmente, define o resultado da tarefa

  • Um método Start com a seguinte assinatura de API:

    void Start<TStateMachine>(ref TStateMachine stateMachine)
              where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine
    
  • Um método AwaitOnCompleted com a seguinte assinatura:

    public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : System.Runtime.CompilerServices.INotifyCompletion
        where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine
    
  • Um método AwaitUnsafeOnCompleted com a seguinte assinatura:

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

Você pode aprender sobre construtores de métodos assíncronos lendo sobre os seguintes construtores fornecidos pelo .NET:

No C# 10 e versões posteriores, o atributo AsyncMethodBuilder pode ser aplicado a um método assíncrono para substituir o construtor por aquele tipo.

Atributos InterpolatedStringHandler e InterpolatedStringHandlerArguments

Do C# 10 em diante, você usa esses atributos para especificar que um tipo é um manipulador de cadeia de caracteres interpolada. A biblioteca do .NET 6 já inclui System.Runtime.CompilerServices.DefaultInterpolatedStringHandler para cenários em que você usa uma cadeia de caracteres interpolada como argumento para um parâmetro string. Você pode ter outras instâncias em que deseja controlar como as cadeias de caracteres interpoladas são processadas. Você aplica System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute ao tipo que implementa seu manipulador. Você aplica System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute aos parâmetros do construtor desse tipo.

Você pode saber mais sobre como criar um manipulador de cadeia de caracteres interpolada na especificação de recursos do C# 10 para melhorias de cadeia de caracteres interpolada.

Atributo ModuleInitializer

O atributo ModuleInitializer marca um método chamado pelo runtime quando o assembly é carregado. ModuleInitializer é um alias para ModuleInitializerAttribute.

O atributo ModuleInitializer só pode ser aplicado a um método que:

  • É estático.
  • Não tem parâmetros.
  • Retorna void.
  • É acessível por meio do módulo que o contém, ou seja, internal ou public.
  • Não é um método genérico.
  • Não está contido em uma classe genérica.
  • Não é uma função local.

O atributo ModuleInitializer pode ser aplicado a vários métodos. Nesse caso, a ordem em que o runtime os chama é determinística, mas não especificada.

O exemplo a seguir ilustra o uso de vários métodos inicializadores de módulo. Os métodos Init1 e Init2 métodos são executados antes de Main e cada um deles adiciona uma cadeia de caracteres à propriedade Text. Portanto, quando Main é executado, a propriedade Text já tem cadeias de caracteres de ambos os métodos inicializadores.

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! ";
    }
}

Os geradores de código-fonte às vezes precisam gerar código de inicialização. Inicializadores de módulo fornecem um local padrão para esse código. Na maioria dos outros casos, você deve escrever um construtor estático em vez de um inicializador de módulo.

Atributo SkipLocalsInit

O atributo SkipLocalsInit impede que o compilador defina o sinalizador .locals init ao emitir metadados. O atributo SkipLocalsInit é um atributo de uso único e pode ser aplicado a um método, uma propriedade, uma classe, um struct, uma interface ou um módulo, mas não a um assembly. SkipLocalsInit é um alias para SkipLocalsInitAttribute.

O sinalizador .locals init faz com que o CLR inicialize todas as variáveis locais declaradas em um método com os respectivos valores padrão delas. Como o compilador também garante que você nunca use uma variável antes de atribuir algum valor a ela, .locals init normalmente não é necessário. No entanto, a inicialização zero extra pode ter impacto mensurável no desempenho em alguns cenários, como quando você usa stackalloc para alocar uma matriz na pilha. Nesses casos, você pode adicionar o atributo SkipLocalsInit. Se aplicado diretamente a um método, o atributo afeta esse método e todas as funções aninhadas, incluindo lambdas e funções locais. Se aplicado a um tipo ou módulo, ele afeta todos os métodos aninhados dentro. Esse atributo não afeta métodos abstratos, mas afeta o código gerado para a implementação.

Esse atributo requer a opção do compilador AllowUnsafeBlocks. Esse requisito sinaliza que, em alguns casos, o código pode exibir memória não atribuída (por exemplo, leitura da memória alocada em pilha não inicializada).

O exemplo a seguir ilustra o efeito do atributo SkipLocalsInit em um método que usa stackalloc. O método exibe o que estava na memória quando a matriz de inteiros foi alocada.

[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 experimentar esse código por conta própria, defina a opção do compilador AllowUnsafeBlocks no arquivo .csproj:

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

Confira também