Definir e ler atributos personalizados

Os atributos fornecem uma maneira de associar informações ao código de forma declarativa. Eles também podem fornecer um elemento reutilizável que pode ser aplicado a vários destinos. Considere o ObsoleteAttribute. Ele pode ser aplicado a classes, structs, métodos, construtores e muito mais. Ele declara que o elemento é obsoleto. Em seguida, cabe ao compilador C# procurar esse atributo e realizar alguma ação em resposta.

Neste tutorial, você aprenderá a adicionar atributos a seu código, como criar e usar seus próprios atributos e como usar alguns atributos que são criados no .NET.

Pré-requisitos

Você precisa configurar seu computador para executar o .NET. Você vai encontrar as instruções de instalação na página de downloads do .NET. Você pode executar esse aplicativo no Windows, Ubuntu Linux, macOS ou em um contêiner do Docker. Você precisa instalar o editor de código da sua preferência. As descrições a seguir usam o Visual Studio Code, que é um editor de software livre entre plataformas. No entanto, você pode usar ferramentas com que esteja familiarizado.

Criar o aplicativo

Agora que você instalou todas as ferramentas, crie um aplicativo de console .NET. Para usar o gerador de linha de comando, execute o seguinte comando no shell de sua preferência:

dotnet new console

Esse comando cria arquivos de projeto .NET bare-bones. Execute dotnet restore para restaurar as dependências necessárias para compilar esse projeto.

Não é necessário executar dotnet restore, pois ele é executado implicitamente por todos os comandos que exigem uma restauração, como dotnet new, dotnet build, dotnet run, dotnet test, dotnet publish e dotnet pack. Para desabilitar a restauração implícita, use a opção --no-restore.

O comando dotnet restore ainda é útil em determinados cenários em que realizar uma restauração explícita faz sentido, como compilações de integração contínua no Azure DevOps Services ou em sistemas de compilação que precisam controlar explicitamente quando a restauração ocorrerá.

Para obter informações sobre como gerenciar feeds do NuGet, confira a documentação do dotnet restore.

Para executar o programa, use dotnet run. Você deve ver a saída do "Olá, Mundo" no console.

Adicionar atributos ao código

No C#, os atributos são classes que herdam da classe base Attribute. Qualquer classe que herda de Attribute pode ser usada como uma espécie de "marcação" em outras partes do código. Por exemplo, há um atributo chamado ObsoleteAttribute. Esse atributo sinaliza que o código está obsoleto e não deve mais ser usado. Coloque este atributo em uma classe, por exemplo, usando colchetes.

[Obsolete]
public class MyClass
{
}

Embora a classe seja chamada de ObsoleteAttribute, só é necessário usar [Obsolete] no código. A maioria dos códigos C# segue esta convenção. Você poderá usar o nome completo [ObsoleteAttribute] se escolher.

Ao marcar uma classe obsoleta, é uma boa ideia fornecer algumas informações como o motivo de estar obsoleto e/ou o que usar no lugar. Você inclui um parâmetro de cadeia de caracteres para o atributo Obsolete para fornecer essa explicação.

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

A cadeia de caracteres está sendo passada como um argumento para um construtor ObsoleteAttribute, como se você estivesse escrevendo var attr = new ObsoleteAttribute("some string").

Os parâmetros para um construtor de atributo são limitados a literais/tipos simples: bool, int, double, string, Type, enums, etc e matrizes desses tipos. Você não pode usar uma expressão ou uma variável. Você pode usar parâmetros posicionais ou nomeados.

Criar seu próprio atributo

Você cria um atributo definindo uma nova classe que herda da classe base Attribute.

public class MySpecialAttribute : Attribute
{
}

Com o código anterior, agora você pode usar [MySpecial] (ou [MySpecialAttribute]) como um atributo em qualquer lugar na base do código.

[MySpecial]
public class SomeOtherClass
{
}

Os atributos biblioteca de classes base do .NET, como ObsoleteAttribute, disparam determinados comportamentos no compilador. No entanto, qualquer atributo que você cria atua como metadados e não resulta em qualquer código dentro da classe de atributo que está sendo executada. Cabe a você agir nesses metadados no seu código.

Há uma pegadinha aqui. Conforme mencionado anteriormente, somente determinados tipos podem ser passados como argumentos ao usar atributos. No entanto, ao criar um tipo de atributo, o compilador C# não impede você de criar esses parâmetros. No exemplo a seguir, você criou um atributo com um construtor que compila corretamente.

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

No entanto, não é possível usar esse construtor com a sintaxe de atributo.

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

O código anterior causa um erro do compilador como Attribute constructor parameter 'myClass' has type 'Foo', which is not a valid attribute parameter type

Como restringir o uso do atributo

Os atributos podem ser usados nos "destinos" a seguir. Os exemplos acima mostram os atributos em classes, mas eles também podem ser usados em:

  • Assembly
  • Classe
  • Construtor
  • Delegar
  • Enumeração
  • Evento
  • Campo
  • GenericParameter
  • Interface
  • Método
  • Módulo
  • Parâmetro
  • Propriedade
  • ReturnValue
  • Estrutura

Quando você cria uma classe de atributo, por padrão, o C# permite que você use esse atributo em qualquer um dos destinos possíveis do atributo. Se quiser restringir seu atributo a determinados destinos, você poderá fazer isso usando o AttributeUsageAttribute em sua classe de atributo. É isso mesmo, um atributo em um atributo!

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

Se tentar colocar o atributo acima em algo que não é uma classe ou um struct, você obtém um erro do 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()
    { }
}

Como usar atributos anexados a um elemento de código

Atributos agem como metadados. Sem nenhuma força, eles não fazem nada.

Para localizar e agir sobre os atributos, uma reflexão é necessária. A reflexão permite que você escreva código em C# que examine outro código. Por exemplo, você pode usar a Reflexão para obter informações sobre uma classe (adicione using System.Reflection; no início do seu código):

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

Isso imprime algo como: The assembly qualified name of MyClass is ConsoleApplication.MyClass, attributes, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

Depois de ter um objeto TypeInfo (ou um MemberInfo, FieldInfo ou outro objeto), você poderá usar o método GetCustomAttributes. Esse método retorna uma coleção de objetos Attribute. Você também poderá usar GetCustomAttribute e especificar um tipo de atributo.

Aqui está um exemplo do uso de GetCustomAttributes em uma instância MemberInfo para MyClass (que vimos, anteriormente, que tem um atributo [Obsolete] nele).

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

Isso imprime no console: Attribute on MyClass: ObsoleteAttribute. Tente adicionar outros atributos a MyClass.

É importante observar que esses objetos Attribute são instanciados lentamente. Ou seja, eles não serão instanciados até que você use GetCustomAttribute ou GetCustomAttributes. Eles também são instanciados a cada vez. Chamar GetCustomAttributes duas vezes em uma linha retornará duas instâncias diferentes do ObsoleteAttribute.

Atributos comuns no runtime

Os atributos são usados por muitas ferramentas e estruturas. O NUnit usa atributos como [Test] e [TestFixture] que são usados pelo executor de teste NUnit. O ASP.NET MVC usa atributos como [Authorize] e fornece uma estrutura de filtro de ação para executar questões abrangentes sobre as ações do MVC. O PostSharp usa a sintaxe de atributo para permitir a programação em C# orientada ao aspecto.

Aqui estão alguns atributos importantes incorporados às bibliotecas de classes base do .NET Core:

  • [Obsolete]. Este foi usado nos exemplos acima, e reside no namespace System. Isso é útil para fornecer a documentação declarativa sobre uma base de código de alteração. Uma mensagem pode ser fornecida na forma de uma cadeia de caracteres e outro parâmetro booliano pode ser usado para encaminhamento de um aviso do compilador para um erro do compilador.
  • [Conditional]. Esse atributo está no namespace System.Diagnostics. Esse atributo pode ser aplicado aos métodos (ou classes de atributo). Você deve passar uma cadeia de caracteres para o construtor. Se essa cadeia de caracteres não corresponder a uma diretiva #define, o compilador C# removerá todas as chamadas a esse método (mas não o próprio método). Normalmente, você usa essa técnica para fins de depuração (diagnóstico).
  • [CallerMemberName]. Esse atributo pode ser usado em parâmetros e reside no namespace System.Runtime.CompilerServices. CallerMemberName é um atributo usado para injetar o nome do método que está chamando outro método. É uma forma de eliminar 'cadeias de caracteres mágicas' ao implementar INotifyPropertyChanged em diversas estruturas de interface do usuário. Por exemplo:
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
            }
        }
    }
}

No código acima, você não precisa ter uma cadeia de caracteres "Name" literal. Usar CallerMemberName evita erros relacionados à digitação e também possibilita a refatoração/renomeação mais suave. Atributos trazem poder declarativo para C#, mas eles são uma forma de metadados de código e não agem sozinhos.