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.
- O que é um atributo, onde ele fica e como se escreve (argumentos, nomes, o sufixo
Attribute). [Obsolete]: os avisosCS0618eCS0612, o erroCS0619e como silenciar um aviso de propósito.[Flags]: enums que se combinam, comoLer, 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,CS0592eCS0579.
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.Jsonlê[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 finalAttribute. - ("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
CS0618apontou 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.
| Forma | Resultado 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.
À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:
| Forma | Exemplo | O 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 linha | Várias etiquetas na mesma coisa. |
| Num parâmetro | void F([CallerMemberName] string m = "") | O atributo fica antes do parâmetro. |
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
| Linha | O que acontece |
|---|---|
Permissao.Ler | Permissao.Escrever | 1 | 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.Tudo | Uma combinação com nome próprio aparece pelo nome. |
(Dia)3 | O 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 )
| Trecho | O 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) : Attribute | A 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
| Atributo | Onde | Quem lê | Para quê |
|---|---|---|---|
[Obsolete] | Qualquer membro ou tipo | Compilador | Avisar (ou proibir) o uso de algo velho |
[Flags] | Enum | .NET | Enum de combinações (Ler, Escrever) |
[CallerMemberName], [CallerLineNumber], [CallerFilePath], [CallerArgumentExpression] | Parâmetro opcional | Compilador | Preencher informações de quem chamou |
[AttributeUsage] | Classe de atributo | Compilador | Onde o atributo pode ser usado |
[JsonPropertyName("x")], [JsonIgnore] | Propriedade | System.Text.Json | Nome no JSON / deixar de fora |
[Fact], [Theory] | Método | xUnit | Marcar um teste |
[Required], [Range(1, 10)] | Propriedade | Validação (ASP.NET e outros) | Regras de preenchimento |
[DebuggerDisplay("…")] | Tipo | Depurador | O que mostrar ao inspecionar o objeto |
[SetsRequiredMembers] | Construtor | Compilador | Dizer que o construtor preenche os required |
Quando dá errado
Atributo com nome errado (ou sem o using)
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
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
[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
| Forma | Exemplo |
|---|---|
| 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âmetro | void F([CallerMemberName] string m = "") |
| Criar | class 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:
2. De qual classe todo atributo herda?
3. Qual código de aviso aparece quando se usa um método com [Obsolete("mensagem")]?
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 avisoCS0612; com mensagem,CS0618; comerror: true, o erroCS0619. Sempre diga na mensagem o que usar no lugar.[Flags]marca enums de combinações (valores 1, 2, 4, 8…, juntados com|), e oToStringpassa a mostrarLer, 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 emAttribute); o[AttributeUsage]limita onde ela vale (CS0592) e se pode repetir (CS0579). Lê-se comGetCustomAttribute. - Os argumentos de atributos precisam ser constantes. Nome errado ou
usingfaltando:CS0246.
Próxima aula: Reflexão, o programa examinando os próprios tipos, métodos e atributos enquanto roda.