C# Atributos

Um atributo é uma etiqueta que você cola em cima de um pedaço de código, entre colchetes: [Obsolete], [Flags], [JsonIgnore]. Ele não muda o que o código faz por si só. Ele informa alguma coisa a quem lê o código depois: o compilador, uma biblioteca, uma ferramenta de testes ou o seu próprio programa. Nesta aula você vai usar os atributos mais comuns, ver o aviso que o [Obsolete] provoca e criar uma etiqueta sua.

Você vai aprender
  • O que é um atributo, onde ele fica e como se escreve (argumentos, nomes, o sufixo Attribute).
  • [Obsolete]: os avisos CS0618 e CS0612, o erro CS0619 e como silenciar um aviso de propósito.
  • [Flags]: enums que se combinam, como Ler, Escrever.
  • [CallerMemberName], [CallerLineNumber] e [CallerArgumentExpression] C# 10: o compilador preenche argumentos por você.
  • Como criar um atributo próprio com [AttributeUsage] e lê-lo com reflexão.
  • Os erros CS0246, CS0592 e CS0579.

Esta aula usa classes, herança, enums e parâmetros opcionais. Os exemplos rodaram com dotnet run no .NET 10 da máquina da Ana, na pasta ~/cs.


A ideia antes do código

Na lojinha da Ana, algumas caixas chegam com etiquetas: "FRÁGIL", "ESTE LADO PARA CIMA", "NÃO VENDER APÓS 30/11". A etiqueta não muda o que está dentro da caixa. Mas quem carrega a caixa lê a etiqueta e muda o jeito de tratar o que está ali.

Os atributos são essas etiquetas, coladas em classes, métodos, propriedades, parâmetros e enums. Quem lê as etiquetas pode ser:

  • o compilador, enquanto compila: o [Obsolete] faz ele avisar quem usa um método velho;
  • o .NET, enquanto o programa roda: o [Flags] muda o jeito de mostrar um enum;
  • uma biblioteca: o System.Text.Json lê [JsonPropertyName] para escolher o nome no arquivo JSON;
  • o seu programa, com reflexão (seção 5).

1. O primeiro atributo: [Obsolete]

A Ana escreveu, há tempos, um método Somar que só soma dois valores. Depois criou o SomarTudo, que soma quantos você quiser. Ela não quer apagar o Somar de uma vez, porque outras partes do programa ainda usam. Mas quer que todo mundo fique sabendo que ele vai sumir. Para isso existe o [Obsolete] ("obsoleto", fora de uso):

decimal total = Calculadora.Somar(10m, 5m);
Console.WriteLine(total);
Console.WriteLine(Calculadora.SomarTudo(10m, 5m, 2.5m));

static class Calculadora
{
    [Obsolete("Use SomarTudo, que aceita quantos valores você quiser.")]
    public static decimal Somar(decimal a, decimal b) => a + b;

    public static decimal SomarTudo(params decimal[] valores) => valores.Sum();
}
$ dotnet run obsoleto.cs
/home/ana/cs/obsoleto.cs(1,17): warning CS0618: "Calculadora.Somar(decimal, decimal)" é obsoleto: "Use SomarTudo, que aceita quantos valores você quiser."
15
17,5
[
Abre a etiqueta. O atributo fica logo acima (ou logo antes) do que ele marca: aqui, o método Somar.
Obsolete
O nome do atributo. Por dentro, é uma classe chamada ObsoleteAttribute; o C# deixa você omitir o final Attribute.
("Use SomarTudo, …")
Um argumento, como numa chamada de construtor: a mensagem que o compilador vai mostrar.
]
Fecha a etiqueta.

Repare no que aconteceu:

  • O programa compilou e rodou (mostrou 15 e 17,5). Um aviso não impede nada.
  • O aviso CS0618 apontou o lugar que usa o método velho (linha 1, coluna 17), não o lugar onde ele foi declarado. É ali que alguém precisa mexer.
  • A mensagem traz o texto que a Ana escreveu. Por isso vale a pena sempre dizer o que usar no lugar.

Sem mensagem, e como erro

Sem argumento nenhum, [Obsolete] dá um aviso de outro número, o CS0612, só dizendo que o método está obsoleto:

Console.WriteLine(Calculadora.Somar(10m, 5m));

static class Calculadora
{
    [Obsolete]
    public static decimal Somar(decimal a, decimal b) => a + b;
}
$ dotnet run sem-mensagem.cs
/home/ana/cs/sem-mensagem.cs(1,19): warning CS0612: "Calculadora.Somar(decimal, decimal)" está obsoleto
15

Quando chega a hora de proibir de vez, um segundo argumento, error: true, transforma o aviso em erro. O código que usa o método deixa de compilar:

Console.WriteLine(Calculadora.Somar(10m, 5m));

static class Calculadora
{
    [Obsolete("Somar foi removido; use SomarTudo.", error: true)]
    public static decimal Somar(decimal a, decimal b) => a + b;
}
$ dotnet run removido.cs
/home/ana/cs/removido.cs(1,19): error CS0619: "Calculadora.Somar(decimal, decimal)" é obsoleto: "Somar foi removido; use SomarTudo."

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
FormaResultado onde o método é usado
[Obsolete]Aviso CS0612: "está obsoleto"
[Obsolete("mensagem")]Aviso CS0618, com a mensagem
[Obsolete("mensagem", error: true)]Erro CS0619, com a mensagem: não compila

Esse é o caminho que as bibliotecas do .NET seguem para aposentar um método: primeiro um aviso por algumas versões, para dar tempo de todo mundo trocar; depois, um erro; por fim, o método some.

Silenciar um aviso de propósito

Às vezes você precisa usar o método velho por mais um tempo e não quer o aviso a cada compilação. Cerque só aquela linha com #pragma warning disable CS0618 e #pragma warning restore CS0618. O aviso some ali, e continua valendo no resto do programa. Faça isso com moderação: o aviso existe para ser resolvido. A aula Avisos do Compilador fala mais do #pragma.

Missão: o método PrecoComDesconto foi substituído por CalcularPreco. Marque-o com [Obsolete] e a mensagem "Use CalcularPreco.", e rode: o compilador deve dar o aviso CS0618 na linha que ainda usa o método velho.

Console.WriteLine(Loja.PrecoComDesconto(100m));
Console.WriteLine(Loja.CalcularPreco(100m, 0.10m));

static class Loja
{
    // Marque este método como obsoleto, com a mensagem "Use CalcularPreco."
    public static decimal PrecoComDesconto(decimal preco) => preco * 0.9m;

    public static decimal CalcularPreco(decimal preco, decimal desconto) => preco * (1 - desconto);
}
Console.WriteLine(Loja.PrecoComDesconto(100m));
Console.WriteLine(Loja.CalcularPreco(100m, 0.10m));

static class Loja
{
    [Obsolete("Use CalcularPreco.")]
    public static decimal PrecoComDesconto(decimal preco) => preco * 0.9m;

    public static decimal CalcularPreco(decimal preco, decimal desconto) => preco * (1 - desconto);
}

2. Como se escreve um atributo

Todo atributo segue o mesmo formato, que é o de criar um objeto de uma classe especial:

FormaExemploO que é
Sem argumentos[Flags]Usa o construtor sem parâmetros. [Flags()] também vale.
Argumentos na ordem[Obsolete("Use X.")]Vão para o construtor do atributo.
Argumento com nome[Obsolete("Use X.", error: true)]Um parâmetro do construtor, escrito pelo nome (como em parâmetros nomeados).
Propriedade[Secao("Papelaria", Corredor = 3)]Depois dos argumentos, Nome = valor preenche uma propriedade do atributo.
Nome completo[ObsoleteAttribute("…")]O mesmo que [Obsolete("…")].
Vários[Obsolete, Flags] ou um por linhaVárias etiquetas na mesma coisa.
Num parâmetrovoid F([CallerMemberName] string m = "")O atributo fica antes do parâmetro.
Só valores fixos

Os argumentos de um atributo são gravados no programa compilado, então precisam ser constantes: números, textos, true/false, valores de enum, typeof(Tipo) e arrays desses. Uma variável, um new Produto() ou um DateTime.Now não são aceitos.


3. [Flags]: enums que se combinam

Um enum comum guarda uma opção: Dia.Segunda. Às vezes você quer guardar várias ao mesmo tempo: a Ana pode ler e escrever na planilha da loja, mas não apagar. O truque é dar a cada opção um valor que é uma potência de 2 (1, 2, 4, 8…) e juntá-las com | ("ou" de bits). O [Flags] ("bandeiras") avisa o .NET de que o enum funciona assim:

Permissao ana = Permissao.Ler | Permissao.Escrever;
Console.WriteLine(ana);
Console.WriteLine(ana.HasFlag(Permissao.Escrever));
Console.WriteLine(ana.HasFlag(Permissao.Apagar));
Console.WriteLine(Permissao.Tudo);
Console.WriteLine((Dia)3);

[Flags]
enum Permissao
{
    Nenhuma = 0,
    Ler = 1,
    Escrever = 2,
    Apagar = 4,
    Tudo = Ler | Escrever | Apagar,
}

enum Dia { Seg = 1, Ter = 2 }
$ dotnet run flags.cs
Ler, Escrever
True
False
Tudo
3
LinhaO que acontece
Permissao.Ler | Permissao.Escrever1 | 2 = 3: as duas "bandeiras" ficam levantadas no mesmo valor.
Console.WriteLine(ana)Por causa do [Flags], o ToString mostra as partes: Ler, Escrever.
ana.HasFlag(Permissao.Escrever)"Tem a bandeira Escrever?" True.
Permissao.TudoUma combinação com nome próprio aparece pelo nome.
(Dia)3O Dia não tem [Flags]: o 3 não é nenhum dia e aparece só como número. Com [Flags], sairia Seg, Ter.

Aqui quem lê a etiqueta é o próprio .NET, enquanto o programa roda: o Enum.ToString() confere se o tipo tem [Flags] e muda o jeito de escrever.


4. O compilador preenche para você: Caller…

Três atributos da pasta System.Runtime.CompilerServices marcam parâmetros opcionais e pedem ao compilador para preenchê-los com informações de quem chamou o método:

using System.Runtime.CompilerServices;

Loja.Abrir();
Loja.Vender("caderno");

static class Loja
{
    public static void Abrir() => Registrar("Loja aberta");

    public static void Vender(string produto)
    {
        Registrar($"Vendi um {produto}");
    }

    static void Registrar(string mensagem,
        [CallerMemberName] string metodo = "",
        [CallerLineNumber] int linha = 0)
    {
        Console.WriteLine($"[{metodo}, linha {linha}] {mensagem}");
    }
}
$ dotnet run registro.cs
[Abrir, linha 8] Loja aberta
[Vender, linha 12] Vendi um caderno
using System.Runtime.CompilerServices;
A pasta (namespace) onde esses atributos moram.
[CallerMemberName] string metodo = ""
"Nome do membro que chamou." Quem chama não passa nada; o compilador escreve "Abrir" ou "Vender" no lugar do "".
[CallerLineNumber] int linha = 0
"Número da linha da chamada": 8 e 12, as linhas onde está o Registrar(…).

Nada disso acontece enquanto o programa roda: na hora de compilar, o C# reescreve Registrar("Loja aberta") como Registrar("Loja aberta", "Abrir", 8). É um registro de atividades (log) que se mantém certo sozinho, mesmo que você mude o nome do método ou mova a linha. Existe também o [CallerFilePath], com o caminho do arquivo.

[CallerArgumentExpression] C# 10

O mais esperto dos três guarda o texto que a pessoa escreveu como argumento. Ótimo para mensagens de erro que dizem exatamente qual condição falhou:

using System.Runtime.CompilerServices;

int idade = 15;
Conferir(idade >= 18);
Conferir(idade > 0);

void Conferir(bool condicao, [CallerArgumentExpression(nameof(condicao))] string expressao = "")
{
    Console.WriteLine(condicao ? $"ok: {expressao}" : $"FALHOU: {expressao}");
}
$ dotnet run conferir.cs
FALHOU: idade >= 18
ok: idade > 0

O argumento do atributo, nameof(condicao), diz de qual parâmetro copiar o texto. O próprio .NET usa isso: ArgumentNullException.ThrowIfNull(cliente) sabe escrever "cliente" na mensagem de erro porque tem um [CallerArgumentExpression].


5. Criar o seu próprio atributo

Um atributo é só uma classe que herda de Attribute. Por convenção, o nome termina em Attribute. A Ana quer etiquetar cada tipo de produto com a seção e o corredor da loja:

using System.Reflection;

foreach (var tipo in new[] { typeof(Caderno), typeof(Caneta), typeof(Borracha) })
{
    var etiqueta = tipo.GetCustomAttribute<SecaoAttribute>();
    Console.WriteLine($"{tipo.Name}: {etiqueta?.Nome ?? "sem seção"} (corredor {etiqueta?.Corredor})");
}

[AttributeUsage(AttributeTargets.Class)]
class SecaoAttribute(string nome) : Attribute
{
    public string Nome { get; } = nome;
    public int Corredor { get; set; }
}

[Secao("Papelaria", Corredor = 3)]
class Caderno { }

[Secao("Escrita")]
class Caneta { }

class Borracha { }
$ dotnet run secao.cs
Caderno: Papelaria (corredor 3)
Caneta: Escrita (corredor 0)
Borracha: sem seção (corredor )
TrechoO que faz
[AttributeUsage(AttributeTargets.Class)]Um atributo no atributo: diz onde a etiqueta pode ser colada. Aqui, só em classes. Outros alvos: Method, Property, Field, Parameter, Enum, All…
class SecaoAttribute(string nome) : AttributeA classe do atributo, com um construtor primário que recebe o nome da seção.
public int Corredor { get; set; }Uma propriedade com set: é ela que o Corredor = 3 preenche.
[Secao("Papelaria", Corredor = 3)]O uso, sem o sufixo Attribute.
tipo.GetCustomAttribute<SecaoAttribute>()Lê a etiqueta de um tipo, enquanto o programa roda. Devolve null se não houver (a Borracha). Isso é reflexão, o assunto da próxima aula.

Repare: colar [Secao] numa classe, por si só, não faz nada. Ele só ganha efeito porque o laço lá em cima procura a etiqueta e age conforme o que encontra. É assim com todo atributo: alguém precisa lê-lo. Bibliotecas inteiras funcionam desse jeito, como o xUnit (que procura métodos com [Fact] para rodar como testes) e o System.Text.Json (que procura [JsonPropertyName] e [JsonIgnore]; veja JSON).


Atributos que você vai encontrar

AtributoOndeQuem lêPara quê
[Obsolete]Qualquer membro ou tipoCompiladorAvisar (ou proibir) o uso de algo velho
[Flags]Enum.NETEnum de combinações (Ler, Escrever)
[CallerMemberName], [CallerLineNumber], [CallerFilePath], [CallerArgumentExpression]Parâmetro opcionalCompiladorPreencher informações de quem chamou
[AttributeUsage]Classe de atributoCompiladorOnde o atributo pode ser usado
[JsonPropertyName("x")], [JsonIgnore]PropriedadeSystem.Text.JsonNome no JSON / deixar de fora
[Fact], [Theory]MétodoxUnitMarcar um teste
[Required], [Range(1, 10)]PropriedadeValidação (ASP.NET e outros)Regras de preenchimento
[DebuggerDisplay("…")]TipoDepuradorO que mostrar ao inspecionar o objeto
[SetsRequiredMembers]ConstrutorCompiladorDizer que o construtor preenche os required

Quando dá errado

Atributo com nome errado (ou sem o using)

Este código tem um erro de propósito.
Console.WriteLine("oi");
[Obsoleto]
class Produto { }
$ dotnet run desconhecido.cs
/home/ana/cs/desconhecido.cs(2,2): error CS0246: O nome do tipo ou do namespace "ObsoletoAttribute" não pode ser encontrado (está faltando uma diretiva using ou uma referência de assembly?)
/home/ana/cs/desconhecido.cs(2,2): error CS0246: O nome do tipo ou do namespace "Obsoleto" não pode ser encontrado (está faltando uma diretiva using ou uma referência de assembly?)

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que quer dizer: o compilador procurou uma classe ObsoletoAttribute e, depois, Obsoleto (por isso são dois erros iguais) e não achou nenhuma. Os nomes dos atributos do .NET são em inglês. Como consertar: confira a grafia ([Obsolete]). Se a grafia estiver certa, falta o using da pasta do atributo, como using System.Runtime.CompilerServices; para o [CallerMemberName] ou using System.Text.Json.Serialization; para o [JsonIgnore].

Atributo no lugar errado

Este código tem um erro de propósito.
class Produto
{
    [Secao("x")]
    public string Nome = "";
}
[AttributeUsage(AttributeTargets.Class)]
class SecaoAttribute(string nome) : Attribute
{
    public string Nome { get; } = nome;
}
$ dotnet run alvo.cs
/home/ana/cs/alvo.cs(3,6): error CS0592: Atributo "Secao" não é válido neste tipo de declaração. Ele é válido somente em "classe" declarações.

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que quer dizer: o [AttributeUsage(AttributeTargets.Class)] diz que Secao só vale em classes, e ele foi colado num campo. (A ordem estranha das palavras, "em classe declarações", é da tradução.) Como consertar: mova o atributo para a classe ou, se ele deve mesmo valer em campos, acrescente o alvo: [AttributeUsage(AttributeTargets.Class | AttributeTargets.Field)].

O mesmo atributo duas vezes

Este código tem um erro de propósito.
[Secao("a")]
[Secao("b")]
class Caderno { }
[AttributeUsage(AttributeTargets.Class)]
class SecaoAttribute(string nome) : Attribute { public string Nome { get; } = nome; }
$ dotnet run duplicado.cs
/home/ana/cs/duplicado.cs(2,2): error CS0579: Duplicar atributo "Secao"

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que quer dizer: "atributo Secao duplicado" (Duplicate 'Secao' attribute). Por padrão, cada atributo só pode aparecer uma vez no mesmo lugar. Como consertar: deixe uma etiqueta só ou, se repetir faz sentido (um produto em duas seções), permita: [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)], e leia todas com GetCustomAttributes<SecaoAttribute>() (no plural).

error CS0619: o método foi aposentado

O [Obsolete("…", error: true)] transforma o uso em erro (seção 1). Como consertar: faça o que a mensagem manda e troque pelo método novo. Se o código é seu e você ainda não pode trocar, volte o atributo para aviso (sem o error: true) por enquanto.


Referência

FormaExemplo
Atributo simples[Flags]
Com argumentos[Obsolete("Use X.", error: true)]
Com propriedade[Secao("Papelaria", Corredor = 3)]
Vários[A, B] ou [A] e [B] em linhas separadas
Em parâmetrovoid F([CallerMemberName] string m = "")
Criarclass SecaoAttribute : Attribute { … }
Restringir o alvo[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
Permitir repetir[AttributeUsage(…, AllowMultiple = true)]
Ler (um)typeof(T).GetCustomAttribute<SecaoAttribute>() (com using System.Reflection;)
Ler (vários)typeof(T).GetCustomAttributes<SecaoAttribute>()
Silenciar um aviso#pragma warning disable CS0618 … #pragma warning restore CS0618

Exercícios

1. Complete para marcar o método como obsoleto, com mensagem:

[("Use Novo.")]

2. De qual classe todo atributo herda?

class MeuAttribute : { }

3. Qual código de aviso aparece quando se usa um método com [Obsolete("mensagem")]?

Resposta:

4. O que [Obsolete("Use SomarTudo.", error: true)] faz com o código que usa o método?

5. Você colou [Secao("Papelaria")] numa classe e nada mudou no programa. Por quê?

6. Com [Flags] enum Permissao { Ler = 1, Escrever = 2 }, o que mostra Console.WriteLine(Permissao.Ler | Permissao.Escrever)?

Missão final: o Conferir mostra só FALHOU, sem dizer o quê. Acrescente um segundo parâmetro [CallerArgumentExpression(nameof(condicao))] string expressao = "" e mostre-o na mensagem, para a saída ficar ok: estoque > 0, FALHOU: preco >= 0 e FALHOU: nome.Length <= 5. Não esqueça o using lá em cima.

int estoque = 3;
decimal preco = -2.50m;
string nome = "Caderno";

Conferir(estoque > 0);
Conferir(preco >= 0);
Conferir(nome.Length <= 5);

void Conferir(bool condicao)
{
    Console.WriteLine(condicao ? "ok" : "FALHOU");
}
using System.Runtime.CompilerServices;

int estoque = 3;
decimal preco = -2.50m;
string nome = "Caderno";

Conferir(estoque > 0);
Conferir(preco >= 0);
Conferir(nome.Length <= 5);

void Conferir(bool condicao, [CallerArgumentExpression(nameof(condicao))] string expressao = "")
{
    Console.WriteLine(condicao ? $"ok: {expressao}" : $"FALHOU: {expressao}");
}

A solução, na máquina da Ana:

$ dotnet run conferir2.cs
ok: estoque > 0
FALHOU: preco >= 0
FALHOU: nome.Length <= 5

Resumo

  • Um atributo é uma etiqueta entre colchetes colada em tipos, membros e parâmetros. Ele só tem efeito quando alguém o lê: o compilador, o .NET, uma biblioteca ou o seu código.
  • [Obsolete] dá o aviso CS0612; com mensagem, CS0618; com error: true, o erro CS0619. Sempre diga na mensagem o que usar no lugar.
  • [Flags] marca enums de combinações (valores 1, 2, 4, 8…, juntados com |), e o ToString passa a mostrar Ler, Escrever.
  • [CallerMemberName], [CallerLineNumber] e [CallerArgumentExpression] C# 10 fazem o compilador preencher parâmetros opcionais com dados de quem chamou.
  • Um atributo próprio é uma classe que herda de Attribute (nome terminando em Attribute); o [AttributeUsage] limita onde ela vale (CS0592) e se pode repetir (CS0579). Lê-se com GetCustomAttribute.
  • Os argumentos de atributos precisam ser constantes. Nome errado ou using faltando: CS0246.

Próxima aula: Reflexão, o programa examinando os próprios tipos, métodos e atributos enquanto roda.