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.

Você vai aprender
  • 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 switch e os avisos CS8509 e CS8524.
  • Transformar texto em enum com Enum.TryParse.
  • [Flags]: combinar várias opções ao mesmo tempo, com |, &, ~ e HasFlag.

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ém Enum.GetNames<StatusPedido>(), que devolve os nomes como string.

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.

Outro tipo por baixo

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:

Este código tem erros 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 é o 0, que o C# aceita sem cast.)
  • Linha 2, CS0117: a opção Cancelado não existe. É exatamente a proteção do "cardápio": um erro de digitação, como StatusPedido.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 o Enum.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 true e preenche status se der certo, ou false se não der. O tipo do enum vem da variável do out.
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 true ou false. 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 (010 vira …101), e o & mantém tudo, menos o Escrever.

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

CS0117: "… não contém uma definição para …"
$ 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.

CS0266 / CS0029: número ou texto no lugar de um enum

Como resolver: para número, (StatusPedido)n (e confira com Enum.IsDefined); para texto, Enum.TryParse(texto, out StatusPedido s).

CS8509 e 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.

Um enum [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

EscritaSignificado
enum StatusPedido { Novo, Pago }Declara um enum (Novo = 0, Pago = 1)
enum Prioridade { Baixa = 1, Media, Alta }Escolhe os números
(int)s / (StatusPedido)1Enum → 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 &= ~aJuntar, testar e tirar bandeiras

Exercícios

1. Declarar

Complete para declarar o enum Tamanho com P, M e G:

Tamanho { P, M, 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:

Permissoes bia = Permissoes.Ler Permissoes.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)s e (Enum)n convertem; o cast aceita qualquer número, então confira com Enum.IsDefined.
  • Enum.GetValues<T>() percorre as opções. Enum.TryParse converte texto com segurança (e aceita números!).
  • No switch, cubra todas as opções: CS8509 avisa a que falta; CS8524 pede um _ para valores sem nome.
  • [Flags] com valores 0, 1, 2, 4, 8… permite combinar: | junta, HasFlag testa, &= ~ tira, e o ToString() mostra Ler, Escrever. Próxima aula: Records.