C# Enums
Um pedido da lojinha da Ana pode estar "novo", "pago", "enviado" ou "entregue". Nada além disso. Guardar isso num string abre a porta para erros de digitação ("Pagoo", "pago", "PAGO"), e guardar num int (0, 1, 2, 3) deixa o código cheio de números mágicos que ninguém lembra o que querem dizer. O enum (de enumeration, "enumeração", uma lista) resolve os dois problemas: é um tipo com uma lista fechada de opções com nome, conferida pelo compilador.
- Como declarar e usar um
enum. - O número por trás de cada opção, como escolher esses números e como percorrer todas as opções.
- Os erros que o compilador cobra (
CS0266,CS0117,CS0029). - Enum no
switche os avisosCS8509eCS8524. - Transformar texto em enum com
Enum.TryParse. [Flags]: combinar várias opções ao mesmo tempo, com|,&,~eHasFlag.
Todos os programas desta aula rodaram com dotnet run arquivo.cs no .NET 10 da máquina da Ana, na pasta ~/cs.
A ideia: um cardápio, não um papel em branco
Numa lanchonete, o pedido pode ser feito de dois jeitos. Num papel em branco, o cliente escreve o que quiser, e o cozinheiro recebe "x-bacom", "xis bacon", "X-BACON"... Num cardápio, o cliente marca uma das opções impressas, e não existe como pedir algo que não está lá. Um enum é o cardápio: as opções são fixas, têm nome, e o compilador recusa qualquer coisa fora da lista.
Um enum simples
StatusPedido status = StatusPedido.Novo;
Console.WriteLine(status);
status = StatusPedido.Pago;
Console.WriteLine($"Agora o pedido está: {status}");
if (status == StatusPedido.Pago)
{
Console.WriteLine("Pode separar os produtos.");
}
enum StatusPedido
{
Novo,
Pago,
Enviado,
Entregue
}
- enum StatusPedido { Novo, Pago, Enviado, Entregue }
- Declara um tipo novo,
StatusPedido, com quatro opções, separadas por vírgulas. Como as classes, o enum fica depois das instruções de nível superior (ou num arquivo próprio). Cada opção também se chama membro do enum. Os nomes seguem o PascalCase, e o nome do enum fica no singular (cada variável guarda um status). - StatusPedido status = StatusPedido.Novo;
- Uma variável do tipo
StatusPedido. Ela só aceita uma das quatro opções, sempre escrita com o nome do enum na frente:StatusPedido.Novo. - Console.WriteLine(status)
- Mostrar um enum mostra o nome da opção.
- status == StatusPedido.Pago
- Enums se comparam com
==e!=.
$ dotnet run status.cs Novo Agora o pedido está: Pago Pode separar os produtos.
O número por trás de cada nome
Por dentro, cada opção de um enum é um número inteiro (um int). A primeira vale 0, a segunda 1, e assim por diante. Um cast (veja Conversão de Tipos) mostra e converte:
StatusPedido s = StatusPedido.Enviado;
int numero = (int)s;
Console.WriteLine($"{s} = {numero}");
StatusPedido outro = (StatusPedido)1;
Console.WriteLine(outro);
StatusPedido estranho = (StatusPedido)42;
Console.WriteLine(estranho);
Console.WriteLine(Enum.IsDefined(estranho));
foreach (StatusPedido st in Enum.GetValues<StatusPedido>())
{
Console.WriteLine($"{(int)st}: {st}");
}
enum StatusPedido
{
Novo,
Pago,
Enviado,
Entregue
}
$ dotnet run numeros.cs Enviado = 2 Pago 42 « 42 não tem nome, então aparece o número False 0: Novo 1: Pago 2: Enviado 3: Entregue
- (int)s
- Do enum para o número:
Enviadoé a terceira opção, então vale 2. - (StatusPedido)1
- Do número para o enum: o 1 é o
Pago. - (StatusPedido)42
- Cuidado: o cast aceita qualquer número, mesmo sem opção correspondente. O C# não confere. Na saída aparece só
42. - Enum.IsDefined(estranho)
- Pergunta se o valor é uma das opções com nome. Use antes de confiar num número que veio de fora (de um arquivo, do usuário).
- Enum.GetValues<StatusPedido>()
- Devolve todas as opções, na ordem, para percorrer com
foreach. Há tambémEnum.GetNames<StatusPedido>(), que devolve os nomes comostring.
Missão: a lojinha passou a aceitar cancelamentos. Acrescente a opção Cancelado no fim do enum e troque StatusPedido.Enviado por StatusPedido.Cancelado na primeira linha. A saída deve ser Cancelado = 4. Teste também colocar o Cancelado no começo da lista: os números de todos mudam!
StatusPedido s = StatusPedido.Enviado;
Console.WriteLine($"{s} = {(int)s}");
enum StatusPedido
{
Novo,
Pago,
Enviado,
Entregue
}
StatusPedido s = StatusPedido.Cancelado;
Console.WriteLine($"{s} = {(int)s}");
enum StatusPedido
{
Novo,
Pago,
Enviado,
Entregue,
Cancelado
}
Escolher os números
Você pode dizer o número de cada opção com =. As opções sem número continuam contando a partir da anterior:
Console.WriteLine((int)Prioridade.Baixa);
Console.WriteLine((int)Prioridade.Alta);
Console.WriteLine((int)HttpStatus.NaoEncontrado);
enum Prioridade
{
Baixa = 1,
Media,
Alta
}
enum HttpStatus
{
Ok = 200,
NaoEncontrado = 404,
ErroNoServidor = 500
}
$ dotnet run valores.cs 1 3 « Baixa = 1, Media = 2, Alta = 3 404
Escolha os números quando eles significam algo fora do programa (os códigos do HTTP, um código gravado num banco de dados). Se o enum é gravado em arquivo como número, fixe os valores: assim, acrescentar uma opção no meio da lista não muda o significado do que já estava gravado.
O número por trás é um int, mas dá para escolher outro tipo inteiro, para economizar memória: enum Nivel : byte { Facil, Medio, Dificil }. Só vale a pena em casos especiais (milhões de valores, arquivos binários).
O compilador protege a lista
A Ana escreveu três linhas erradas de propósito:
StatusPedido s = 1;
StatusPedido t = StatusPedido.Cancelado;
string texto = "Pago";
StatusPedido u = texto;
enum StatusPedido { Novo, Pago, Enviado, Entregue }
$ dotnet run erro1.cs /home/ana/cs/erro1.cs(1,18): error CS0266: Não é possível converter implicitamente tipo "int" em "StatusPedido". Existe uma conversão explícita (há uma conversão ausente?) /home/ana/cs/erro1.cs(2,31): error CS0117: "StatusPedido" não contém uma definição para "Cancelado" /home/ana/cs/erro1.cs(4,18): error CS0029: Não é possível converter implicitamente tipo "string" em "StatusPedido" /home/ana/cs/erro1.cs(1,14): warning CS0219: A variável "s" é atribuída, mas seu valor nunca é usado Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
- Linha 1,
CS0266: um número não vira enum sozinho. Se for mesmo o que você quer, escreva o cast:(StatusPedido)1. (A única exceção é o0, que o C# aceita sem cast.) - Linha 2,
CS0117: a opçãoCanceladonão existe. É exatamente a proteção do "cardápio": um erro de digitação, comoStatusPedido.Pagoo, também cai aqui, na hora de compilar, e não depois, com o programa rodando. - Linha 4,
CS0029: um texto não vira enum. Para isso existe oEnum.TryParse, mais abaixo.
Enum no switch
Enums e expressões switch foram feitos um para o outro: cada opção, uma resposta.
StatusPedido[] todos = [StatusPedido.Novo, StatusPedido.Pago, StatusPedido.Enviado, StatusPedido.Entregue];
foreach (StatusPedido s in todos)
{
Console.WriteLine($"{s}: {Descrever(s)}");
}
string Descrever(StatusPedido s) => s switch
{
StatusPedido.Novo => "aguardando pagamento",
StatusPedido.Pago => "separando os produtos",
StatusPedido.Enviado => "a caminho",
StatusPedido.Entregue => "concluído",
};
enum StatusPedido { Novo, Pago, Enviado, Entregue }
$ dotnet run switch.cs /home/ana/cs/switch.cs(7,39): warning CS8524: A expressão switch não lida com alguns valores do seu tipo de entrada (sem limitação) que envolvem um valor de enumeração não nomeado. Por exemplo, o padrão '(StatusPedido)4' não está coberto. Novo: aguardando pagamento Pago: separando os produtos Enviado: a caminho Entregue: concluído
Funciona, mas com um aviso curioso. As quatro opções estão cobertas, então do que o compilador reclama? Do (StatusPedido)42 que você viu acima: como um cast pode criar um valor sem nome, o compilador lembra que o switch não sabe o que fazer com ele (o exemplo dele é o (StatusPedido)4). O conserto é um braço _ ("qualquer outra coisa") no fim:
string Descrever(StatusPedido s) => s switch
{
StatusPedido.Novo => "aguardando pagamento",
StatusPedido.Pago => "separando os produtos",
StatusPedido.Enviado => "a caminho",
StatusPedido.Entregue => "concluído",
_ => "desconhecido",
};
Com o _, o aviso some e a saída é a mesma. E se faltar uma opção com nome? A Ana apagou a linha do Entregue (e o _):
$ dotnet run switch3.cs /home/ana/cs/switch3.cs(7,39): warning CS8509: A expressão switch não manipula todos os valores possíveis do tipo de entrada (ela não é exaustiva). Por exemplo, o padrão 'StatusPedido.Entregue' não é coberto. Novo: aguardando pagamento Pago: separando os produtos Enviado: a caminho Unhandled exception. System.Runtime.CompilerServices.SwitchExpressionException: Non-exhaustive switch expression failed to match its input. Unmatched value was Entregue. at <PrivateImplementationDetails>.ThrowSwitchExpressionException(Object unmatchedValue) at Program.<<Main>$>g__Descrever|0_0(StatusPedido s) in /home/ana/cs/switch3.cs:line 7 at Program.<Main>$(String[] args) in /home/ana/cs/switch3.cs:line 4
O aviso CS8509 já dizia qual opção faltava. A Ana ignorou, e o programa quebrou ao chegar no Entregue ("valor não encontrado: Entregue"), com o código de saída 134. Esse é um bom motivo para não colocar o _ logo de cara: sem ele, quando alguém acrescentar uma opção nova ao enum, o compilador avisa todos os switch que precisam ser atualizados.
De texto para enum: Enum.TryParse
Quando o status vem de fora (digitado, lido de um arquivo), ele chega como texto. O Enum.TryParse faz a conversão com segurança, como o int.TryParse (veja Parse e TryParse):
string[] digitados = ["Pago", "enviado", "Cancelado", "2"];
foreach (string texto in digitados)
{
if (Enum.TryParse(texto, ignoreCase: true, out StatusPedido status))
{
Console.WriteLine($"\"{texto}\" → {status}");
}
else
{
Console.WriteLine($"\"{texto}\" não é um status");
}
}
enum StatusPedido { Novo, Pago, Enviado, Entregue }
- Enum.TryParse(texto, ignoreCase: true, out StatusPedido status)
- Tenta converter. Devolve
truee preenchestatusse der certo, oufalsese não der. O tipo do enum vem da variável doout. - ignoreCase: true
- "Ignorar maiúsculas e minúsculas": aceita
"enviado","ENVIADO". Sem ele, só o nome exato. (É um argumento nomeado.)
$ dotnet run parse.cs "Pago" → Pago "enviado" → Enviado "Cancelado" não é um status "2" → Enviado « surpresa: números também são aceitos
A última linha é uma pegadinha: o TryParse também aceita números, e aceitaria até "42", um valor sem nome. Se a entrada não pode ser um número, confira depois com Enum.IsDefined(status) ou rejeite textos que são números.
O irmão sem "Try", Enum.Parse<StatusPedido>("Cancelado"), lança uma exceção quando o texto não é uma opção:
$ dotnet run parse-erro.cs Unhandled exception. System.ArgumentException: Requested value 'Cancelado' was not found. at System.Enum.TryParseByName[TStorage](RuntimeType enumType, ReadOnlySpan`1 value, Boolean ignoreCase, Boolean throwOnFailure, TStorage& result) at System.Enum.Parse[TEnum](String value, Boolean ignoreCase) at System.Enum.Parse[TEnum](String value) at Program.<Main>$(String[] args) in /home/ana/cs/parse-erro.cs:line 1
"O valor pedido, Cancelado, não foi encontrado." Com entrada do usuário, prefira sempre o TryParse.
[Flags]: várias opções ao mesmo tempo
Um pedido está em um status por vez. Mas há listas em que várias opções valem juntas: as permissões de um arquivo (ler, escrever, apagar), os dias de funcionamento da loja, os ingredientes extras de um lanche. Para isso, o enum ganha o atributo [Flags] ("bandeiras", como bandeirinhas levantadas ou abaixadas) e opções com valores 1, 2, 4, 8… (as potências de 2):
Permissoes ana = Permissoes.Ler | Permissoes.Escrever;
Console.WriteLine(ana);
Console.WriteLine((int)ana);
Console.WriteLine(ana.HasFlag(Permissoes.Escrever));
Console.WriteLine(ana.HasFlag(Permissoes.Apagar));
ana |= Permissoes.Apagar;
Console.WriteLine(ana);
ana &= ~Permissoes.Escrever;
Console.WriteLine(ana);
Console.WriteLine(Permissoes.Nenhuma);
Console.WriteLine(Permissoes.Tudo);
[Flags]
enum Permissoes
{
Nenhuma = 0,
Ler = 1,
Escrever = 2,
Apagar = 4,
Tudo = Ler | Escrever | Apagar
}
$ dotnet run flags.cs Ler, Escrever « as duas bandeiras juntas 3 « 1 + 2 True False Tudo « Ler, Escrever e Apagar = 7, que tem nome Ler, Apagar « o Escrever foi tirado Nenhuma Tudo
- [Flags]
- Um atributo: uma etiqueta entre colchetes, antes do enum. Ele avisa que as opções podem ser combinadas e faz o
ToString()mostrar a combinação (Ler, Escrever). - Ler = 1, Escrever = 2, Apagar = 4
- Cada opção é uma potência de 2. Em binário, cada uma é um único "interruptor":
001,010,100. Assim, qualquer combinação dá um número único. - Nenhuma = 0
- A opção "nenhuma bandeira". É o valor padrão de qualquer enum, e é bom ela ter nome.
- Tudo = Ler | Escrever | Apagar
- Uma opção que é a combinação de outras.
- Permissoes.Ler | Permissoes.Escrever
- O
|("ou" bit a bit) junta bandeiras:001 | 010 = 011. - ana.HasFlag(Permissoes.Escrever)
- "Tem esta bandeira?" Devolve
trueoufalse. O mesmo que(ana & Permissoes.Escrever) != 0, com o&("e" bit a bit), que fica só com as bandeiras em comum. - ana |= Permissoes.Apagar;
- Levanta uma bandeira (veja Operadores de Atribuição).
- ana &= ~Permissoes.Escrever;
- Abaixa uma bandeira. O
~inverte todos os interruptores (010vira…101), e o&mantém tudo, menos oEscrever.
Para ver os interruptores, o programa abaixo mostra cada opção em binário (Convert.ToString(n, 2) escreve o número na base 2):
foreach (Permissoes p in Enum.GetValues<Permissoes>())
{
Console.WriteLine($"{p,-9} {(int)p,2} {Convert.ToString((int)p, 2).PadLeft(3, '0')}");
}
// … o enum Permissoes de antes
$ dotnet run binario.cs Nenhuma 0 000 Ler 1 001 Escrever 2 010 Apagar 4 100 Tudo 7 111
Sem [Flags]
Os operadores |, & e o HasFlag funcionam mesmo sem o atributo. O que muda é o ToString(): tirando o [Flags] do exemplo, a primeira linha da saída vira 3, em vez de Ler, Escrever, e o Ler, Apagar vira 5. O atributo é o que diz "este número é uma combinação".
A armadilha: esquecer os valores
O [Flags] não muda os números das opções. Sem os = 1, = 2, = 4, elas continuam valendo 0, 1, 2, 3…, e as combinações se misturam:
Console.WriteLine(Dias.Segunda | Dias.Terca);
Console.WriteLine(Dias.Quarta);
[Flags]
enum Dias
{
Segunda,
Terca,
Quarta,
Quinta
}
$ dotnet run flagserr.cs Terca « Segunda vale 0: juntar com 0 não muda nada Quarta
Nenhum erro, nenhum aviso: só um resultado errado. Num enum [Flags], escreva sempre os valores. E, por convenção, dê a ele um nome no plural (Permissoes, Dias), porque uma variável pode guardar várias opções.
Quando dá errado
$ dotnet run erro1.cs /home/ana/cs/erro1.cs(2,31): error CS0117: "StatusPedido" não contém uma definição para "Cancelado"
O que aconteceu: a opção não existe no enum, ou o nome está escrito diferente (maiúsculas contam).
Como resolver: corrija o nome ou acrescente a opção ao enum.
Como resolver: para número, (StatusPedido)n (e confira com Enum.IsDefined); para texto, Enum.TryParse(texto, out StatusPedido s).
SwitchExpressionException
/home/ana/cs/switch3.cs(7,39): warning CS8509: A expressão switch não manipula todos os valores possíveis do tipo de entrada (ela não é exaustiva). Por exemplo, o padrão 'StatusPedido.Entregue' não é coberto.
O que aconteceu: faltou uma opção no switch. Se esse valor aparecer, o programa quebra.
Como resolver: acrescente o braço que falta. O aviso CS8524 (valores sem nome) se resolve com um _ => … no fim.
[Flags] que mostra a combinação errada
O que aconteceu: as opções não são potências de 2 (faltou = 1, = 2, = 4…).
Como resolver: dê os valores explicitamente, com Nenhuma = 0 no começo.
Referência
| Escrita | Significado |
|---|---|
enum StatusPedido { Novo, Pago } | Declara um enum (Novo = 0, Pago = 1) |
enum Prioridade { Baixa = 1, Media, Alta } | Escolhe os números |
(int)s / (StatusPedido)1 | Enum → número / número → enum |
s.ToString() | O nome da opção (ou o número, se não tiver nome) |
Enum.GetValues<T>(), Enum.GetNames<T>() | Todas as opções / todos os nomes |
Enum.IsDefined(valor) | O valor é uma opção com nome? |
Enum.TryParse(texto, true, out T v) | Texto → enum, sem exceção, ignorando maiúsculas |
[Flags] enum P { Nenhuma = 0, A = 1, B = 2, C = 4 } | Opções combináveis |
a | b, v.HasFlag(a), v &= ~a | Juntar, testar e tirar bandeiras |
Exercícios
1. Declarar
Complete para declarar o enum Tamanho com P, M e G:
2. O número
No enum Tamanho { P, M, G }, quanto vale (int)Tamanho.G?
3. Juntar bandeiras
Complete para dar à Bia as permissões de ler e apagar:
4. O que mostra Console.WriteLine((StatusPedido)42);, se o enum só tem 4 opções?
5. Qual é o jeito seguro de transformar o texto digitado pelo usuário num enum?
6. Qual é a lista de valores certa para um enum [Flags] com quatro opções?
Missão final: rode o programa: ele diz que a Bia pode escrever, mas ela só recebeu Ler e Apagar! As opções valem 0, 1, 2, 3, e Ler | Apagar (1 | 3) dá 3, que é o próprio Apagar, que "contém" o 2 do Escrever. Conserte o enum: coloque [Flags] e os valores Nenhuma = 0, Ler = 1, Escrever = 2, Apagar = 4. A saída deve ter Bia: Ler, Apagar e Bia pode escrever? False.
Permissoes bia = Permissoes.Ler | Permissoes.Apagar;
Console.WriteLine($"Bia: {bia}");
Console.WriteLine($"Bia pode escrever? {bia.HasFlag(Permissoes.Escrever)}");
Console.WriteLine($"Bia pode apagar? {bia.HasFlag(Permissoes.Apagar)}");
enum Permissoes
{
Nenhuma,
Ler,
Escrever,
Apagar
}
Permissoes bia = Permissoes.Ler | Permissoes.Apagar;
Console.WriteLine($"Bia: {bia}");
Console.WriteLine($"Bia pode escrever? {bia.HasFlag(Permissoes.Escrever)}");
Console.WriteLine($"Bia pode apagar? {bia.HasFlag(Permissoes.Apagar)}");
[Flags]
enum Permissoes
{
Nenhuma = 0,
Ler = 1,
Escrever = 2,
Apagar = 4
}
$ dotnet run permissoes.cs « antes do conserto Bia: Apagar Bia pode escrever? True Bia pode apagar? True $ dotnet run permissoes.cs « depois Bia: Ler, Apagar Bia pode escrever? False Bia pode apagar? True
Resumo
enum= um tipo com uma lista fechada de opções com nome. O compilador recusa opções que não existem (CS0117), números (CS0266) e textos (CS0029).- Cada opção é um número (0, 1, 2… ou os que você escolher com
=).(int)se(Enum)nconvertem; o cast aceita qualquer número, então confira comEnum.IsDefined. Enum.GetValues<T>()percorre as opções.Enum.TryParseconverte texto com segurança (e aceita números!).- No
switch, cubra todas as opções:CS8509avisa a que falta;CS8524pede um_para valores sem nome. [Flags]com valores 0, 1, 2, 4, 8… permite combinar:|junta,HasFlagtesta,&= ~tira, e oToString()mostraLer, Escrever. Próxima aula: Records.