Um pedido pode estar aberto, pago, enviado, entregue ou cancelado, e nunca "pagp" ou "7": dar nomes a um conjunto fechado de possibilidades elimina erros que nenhum teste pegaria a tempo.
Terminamos "Classe, Struct ou Record" com uma pendência: o Pedido não sabe em que etapa está. A tentação imediata é guardar isso numa string ("pago") ou num número (2). Mas nada impede que alguém escreva "Pago", "pagp" ou 7, e o compilador não tem como ajudar. Nesta aula, conhecemos os enums, que dão nomes verificados a um conjunto fechado de valores, e aprofundamos o pattern matching, que já usamos em pedaços em várias aulas e que se revela uma forma inteira de fazer perguntas sobre os dados.
Enums: um conjunto fechado de nomes
Um enum declara um tipo cujos valores possíveis têm nome:
enum StatusPedido
{
Aberto,
Pago,
Enviado,
Entregue,
Cancelado
}
Agora StatusPedido.Pago é um valor do tipo StatusPedido, verificado pelo compilador. Um erro de digitação como StatusPedido.Pagp não compila, e o editor oferece autocompletar com as opções válidas:
StatusPedido status = StatusPedido.Aberto;
if (status == StatusPedido.Aberto)
Console.WriteLine("Ainda dá para adicionar itens.");
Console.WriteLine(status); // Aberto: o ToString devolve o nome
Por baixo, um enum é um inteiro com nome. O primeiro valor vale 0, e cada seguinte vale um a mais. Você pode fixar os valores explicitamente, o que é recomendado quando eles serão gravados num banco de dados ou trocados com outro sistema:
enum StatusPedido
{
Aberto = 1,
Pago = 2,
Enviado = 3,
Entregue = 4,
Cancelado = 9
}
Fixar os números protege os dados já gravados: se alguém inserir um status novo no meio da lista, os existentes não mudam de valor.
A honestidade sobre os enums: nem tudo é verificado
Aqui está um detalhe que surpreende quem vem de linguagens com enums mais rígidos, como Rust. Como um enum do C# é só um inteiro, qualquer inteiro pode ser convertido para ele:
StatusPedido estranho = (StatusPedido)42;
Console.WriteLine(estranho); // 42
Console.WriteLine(Enum.IsDefined(estranho)); // False
O compilador aceita a conversão, e o programa passa a ter um status que não corresponde a nenhum nome. Isso acontece na prática quando o valor vem de fora: de um banco de dados, de um arquivo, de uma requisição HTTP. A regra é: valide enums na fronteira do sistema. Para converter texto, use Enum.TryParse, que segue o padrão Try que você já domina:
if (Enum.TryParse("enviado", ignoreCase: true, out StatusPedido lido) && Enum.IsDefined(lido))
Console.WriteLine($"Status lido: {lido}");
Por que o IsDefined depois do TryParse? Porque Enum.TryParse("42", out ...) também "consegue", já que 42 é um número válido para o tipo subjacente. Só a combinação dos dois garante um nome legítimo. Para listar todos os valores (útil num menu, por exemplo), use Enum.GetValues<StatusPedido>().
Enums de flags: combinações de opções
Às vezes os valores não são exclusivos, mas combináveis: um livro pode estar disponível em impresso e e-book ao mesmo tempo. Para isso existem os enums marcados com [Flags], cujos valores são potências de 2, e cada um ocupa um bit diferente:
[Flags]
enum FormatosDisponiveis
{
Nenhum = 0,
Impresso = 1,
Ebook = 2,
Audiolivro = 4
}
var formatos = FormatosDisponiveis.Impresso | FormatosDisponiveis.Ebook; // combina
Console.WriteLine(formatos); // Impresso, Ebook
Console.WriteLine(formatos.HasFlag(FormatosDisponiveis.Ebook)); // True
formatos &= ~FormatosDisponiveis.Impresso; // remove o impresso
Console.WriteLine(formatos); // Ebook
Se você vem do C, reconhece a técnica: são os velhos campos de bits com |, & e ~, agora com nomes, verificação de tipo e um ToString que lista as opções ativas. O atributo [Flags] (atributos entre colchetes ganham aula própria na Fase 3) é o que faz o ToString mostrar a combinação em vez de um número.
Pattern matching: o panorama completo
Desde "O Programa Que Escolhe e Repete" usamos padrões aqui e ali: relacionais no switch de descontos, de tipo em "Herança", de propriedade num exemplo rápido. Agora vamos organizar o repertório inteiro. Um padrão é uma pergunta sobre a forma de um valor, e pode aparecer depois de is, nos braços de uma expressão switch e nos case de um switch clássico.
Padrão constante: o valor é igual a uma constante?
if (status is StatusPedido.Cancelado) { /* ... */ }
Padrões relacionais e lógicos: comparações combinadas com and, or e not:
bool emTransito = status is StatusPedido.Pago or StatusPedido.Enviado;
bool faixaMedia = preco is >= 30m and < 80m;
if (cliente is not null) { /* ... */ }
Repare no is not null: é a forma idiomática de testar nulidade, preferível a != null porque não pode ser alterada por uma sobrecarga de operador.
Padrão de tipo, que você viu em "Herança":
if (produto is LivroImpresso impresso)
Console.WriteLine(impresso.PesoGramas);
Padrão de propriedade: perguntas sobre as propriedades de um objeto, que podem ser aninhadas:
bool freteGratis = produto is Ebook or LivroImpresso { PesoGramas: < 500, Preco: > 50m };
bool itemCaro = item is { Produto.Preco: > 100m, Quantidade: >= 2 };
A segunda linha usa o padrão de propriedade estendido (C# 10): Produto.Preco navega de uma propriedade para outra sem precisar aninhar chaves.
Padrão posicional: desconstrói um record ou uma tupla e testa as partes:
string Quadrante(Coordenada c) => c switch
{
(> 0, > 0) => "nordeste",
(> 0, < 0) => "noroeste",
(< 0, > 0) => "sudeste",
(< 0, < 0) => "sudoeste",
_ => "sobre um eixo"
};
Padrão de tupla: uma tupla é um agrupamento leve de valores, escrito entre parênteses, como (status, acao). Já usamos uma para trocar variáveis em "Guardando Muitos Valores". Combinada com o switch, ela permite decidir com base em vários valores de uma vez, e é a ferramenta perfeita para máquinas de estado, como veremos no fim da aula.
Padrões de lista (C# 11): perguntas sobre a forma de uma sequência:
string Descrever(int[] numeros) => numeros switch
{
[] => "vazio",
[var unico] => $"só o {unico}",
[var primeiro, var segundo] => $"{primeiro} e {segundo}",
[var primeiro, .., var ultimo] => $"de {primeiro} a {ultimo}"
};
O .. casa com "qualquer quantidade de elementos no meio". Padrões de lista funcionam com arrays, listas e qualquer tipo que tenha Count ou Length e um indexador.
Por fim, os braços de uma expressão switch aceitam guardas com when, para condições que nenhum padrão expressa:
string Avaliar(Pedido p) => p switch
{
{ Status: StatusPedido.Cancelado } => "cancelado",
{ Total: > 500m } when p.Itens.Count == 1 => "um único item caro",
{ Total: > 500m } => "pedido grande",
_ => "pedido comum"
};
Um aviso sobre exaustividade com enums: um switch que cobre todos os nomes de um enum, mas não tem o braço _, recebe o aviso CS8524. O compilador lembra que, como vimos, (StatusPedido)42 é possível, e esse valor não teria braço. Leve o aviso a sério: um braço _ => throw new ArgumentOutOfRangeException(...) documenta que valores fora da lista são um erro.
Juntando tudo: o ciclo de vida do pedido
O Pedido ganha um status e as regras de transição. A pergunta "posso ir do estado A para o B?" é naturalmente um par de valores, e um switch de tupla responde em uma tabela legível:
using static StatusPedido; // permite escrever Aberto em vez de StatusPedido.Aberto
sealed class Pedido
{
private static int _proximoNumero = 1000;
private readonly List<ItemPedido> _itens = [];
public Pedido() => Numero = ++_proximoNumero;
public int Numero { get; }
public StatusPedido Status { get; private set; } = Aberto;
public IReadOnlyList<ItemPedido> Itens => _itens.AsReadOnly();
public decimal Subtotal
{
get
{
decimal soma = 0;
foreach (var item in _itens) soma += item.Subtotal;
return soma;
}
}
public void Adicionar(Produto produto, int quantidade)
{
if (Status is not Aberto)
throw new InvalidOperationException($"O pedido {Numero} está {Status} e não aceita itens.");
_itens.Add(new ItemPedido(produto, quantidade));
}
public void Pagar()
{
if (_itens is [])
throw new InvalidOperationException("Não é possível pagar um pedido vazio.");
MudarPara(Pago);
}
public void Enviar() => MudarPara(Enviado);
public void ConfirmarEntrega() => MudarPara(Entregue);
public void Cancelar() => MudarPara(Cancelado);
private void MudarPara(StatusPedido novo)
{
if (!TransicaoPermitida(Status, novo))
throw new InvalidOperationException(
$"O pedido {Numero} não pode passar de {Status} para {novo}.");
Status = novo;
}
private static bool TransicaoPermitida(StatusPedido de, StatusPedido para) => (de, para) switch
{
(Aberto, Pago) => true,
(Aberto, Cancelado) => true,
(Pago, Enviado) => true,
(Pago, Cancelado) => true,
(Enviado, Entregue) => true,
_ => false
};
public string Resumo() => _itens switch
{
[] => $"Pedido {Numero} ({Status}): vazio",
[var unico] => $"Pedido {Numero} ({Status}): {unico.Quantidade} x {unico.Produto.Titulo}",
[var primeiro, ..] =>
$"Pedido {Numero} ({Status}): {primeiro.Produto.Titulo} e mais {_itens.Count - 1} item(ns)"
};
}
A diretiva using static StatusPedido; vai no topo do arquivo e permite usar os nomes do enum sem o prefixo, deixando a tabela de transições limpa. O método TransicaoPermitida é a especificação do negócio: quem ler aquelas cinco linhas entende o ciclo de vida inteiro do pedido, e qualquer transição fora da tabela (reabrir um pedido entregue, enviar um cancelado) é recusada pelo braço _.
Em Resumo, o padrão de lista distingue vazio, um item e vários itens. No último braço, usamos [var primeiro, ..] e calculamos o restante com _itens.Count - 1. Também daria para capturar o restante com [var primeiro, .. var resto]: o recorte por intervalo, que os arrays aceitam desde "Guardando Muitos Valores", vale para a List<T> desde o .NET 8, que ganhou o método Slice. Mas o recorte cria uma lista nova a cada chamada, e aqui só queremos contar os itens. Para isso, _itens.Count - 1 não copia nada.
Usando o pedido:
var dom = new LivroImpresso("Dom Casmurro", "Machado de Assis", 59.90m, pesoGramas: 350, estoque: 10);
var pedido = new Pedido();
pedido.Adicionar(dom, 2);
pedido.Pagar();
pedido.Enviar();
Console.WriteLine(pedido.Resumo()); // Pedido 1001 (Enviado): 2 x Dom Casmurro
try
{
pedido.Cancelar(); // Enviado → Cancelado não está na tabela
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.Message); // O pedido 1001 não pode passar de Enviado para Cancelado.
}
Nenhuma string mágica, nenhum número solto e nenhum estado impossível. A combinação de enum com tupla e pattern matching transformou uma regra de negócio, que costuma se espalhar em ifs por todo o sistema, numa tabela de cinco linhas num único método.
A Livraria tem agora um modelo de domínio de verdade: produtos numa hierarquia, políticas de desconto intercambiáveis, objetos de valor, itens como records e um pedido com ciclo de vida verificado. Tudo isso, porém, mora num único projeto de console, misturado com o código de interface. Quando chegarmos à API e ao site, eles vão precisar exatamente destas mesmas classes, e copiá-las seria o erro de sempre. Organizar o código em projetos separados, com nomes que evitam conflitos e dependências declaradas, e aproveitar bibliotecas escritas por outras pessoas, é o que fecha esta fase.
Fontes e leituras recomendadas
- Tipos de enumeração (Microsoft Learn): https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-types/enum. Cobre valores explícitos, conversões e enums de flags.
FlagsAttribute(referência da API): https://learn.microsoft.com/dotnet/api/system.flagsattribute. Traz as regras para desenhar enums de flags corretamente.- Padrões (referência da linguagem): https://learn.microsoft.com/dotnet/csharp/language-reference/operators/patterns. É a especificação de todos os padrões: constante, relacional, lógico, de tipo, de propriedade, posicional,
vare de lista. - Visão geral de pattern matching: https://learn.microsoft.com/dotnet/csharp/fundamentals/functional/pattern-matching. Mostra os cenários de uso e quando o pattern matching substitui cadeias de
if. - Tipos de tupla: https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-types/value-tuples. Explica tuplas nomeadas, desconstrução e uso em padrões.
Exercícios
Exercício 1
Declare um enum DiaDaSemana e escreva um método EhFimDeSemana(DiaDaSemana dia) usando um padrão lógico com or.
Ver resposta
✓ Resposta:
static bool EhFimDeSemana(DiaDaSemana dia) => dia is DiaDaSemana.Sabado or DiaDaSemana.Domingo;
enum DiaDaSemana { Domingo, Segunda, Terca, Quarta, Quinta, Sexta, Sabado }
O padrão is A or B é equivalente a dia == A || dia == B, mas se lê como a própria frase e não repete a variável. Os nomes do enum não levam acento (Terca, Sabado). Identificadores com acento são permitidos em C#, mas a convenção quase universal é evitá-los, para não criar atrito com teclados, ferramentas e sistemas que os tratam mal. Vale saber que o .NET já tem um enum pronto, System.DayOfWeek, com os dias em inglês. Em código de produção, prefira-o a reinventar.
Exercício 2
Preveja a saída e explique cada linha:
var s = (StatusPedido)42;
Console.WriteLine(s);
Console.WriteLine(Enum.IsDefined(s));
Console.WriteLine(Enum.TryParse("42", out StatusPedido t));
Ver resposta
✓ Resposta: Imprime 42, False e True. Como um enum é um inteiro por baixo, a conversão explícita (StatusPedido)42 é aceita mesmo sem nome correspondente, e o ToString mostra o número por não ter um nome a exibir. Enum.IsDefined confirma que 42 não é um valor declarado. E Enum.TryParse("42", ...) devolve True: ele aceita tanto nomes quanto números, e 42 é um número válido para um int. É por isso que, ao ler enums de fontes externas, é preciso combinar TryParse com IsDefined. A primeira só garante que o texto virou um valor do tipo, e a segunda garante que esse valor tem nome.
Exercício 3
Usando o enum FormatosDisponiveis da aula, comece com um livro disponível só em impresso, acrescente e-book e audiolivro, remova o impresso e, no fim, verifique se ele ainda está disponível em algum formato digital.
Ver resposta
✓ Resposta:
var formatos = FormatosDisponiveis.Impresso;
formatos |= FormatosDisponiveis.Ebook | FormatosDisponiveis.Audiolivro;
formatos &= ~FormatosDisponiveis.Impresso;
var digitais = FormatosDisponiveis.Ebook | FormatosDisponiveis.Audiolivro;
bool temDigital = (formatos & digitais) != FormatosDisponiveis.Nenhum;
Console.WriteLine(formatos); // Ebook, Audiolivro
Console.WriteLine(temDigital); // True
|= liga bits, &= ~X desliga o bit de X (o ~ inverte todos os bits de X, e o & mantém tudo menos ele). O teste final usa & com a máscara dos formatos digitais: se sobrar algum bit ligado, há pelo menos um formato digital. Repare que HasFlag(digitais) responderia outra pergunta, "tem todos os formatos digitais?", e por isso a interseção com & é a forma certa para "tem algum". Declarar Nenhum = 0 é uma recomendação oficial para enums de flags: dá nome ao "nada ligado" e torna comparações como esta legíveis.
Exercício 4
Escreva um método que diga se um Produto tem frete grátis usando só padrões: e-books e audiolivros sempre têm; livros impressos têm se pesarem menos de 500 g e custarem mais de R$ 50; os demais, não.
Ver resposta
✓ Resposta:
static bool TemFreteGratis(Produto produto) => produto is
Ebook or
Audiolivro or
LivroImpresso { PesoGramas: < 500, Preco: > 50m };
A regra inteira cabe numa expressão que se lê quase como o enunciado. LivroImpresso { PesoGramas: < 500, Preco: > 50m } combina o padrão de tipo com o de propriedade, e as vírgulas dentro das chaves significam "e". Os três padrões unidos por or formam a regra completa. Um alerta de desenho que já apareceu em "Herança": se esta regra for usada em muitos lugares, talvez ela devesse virar um método virtual TemFreteGratis() em Produto. Padrões de tipo são ótimos em regras de negócio pontuais, que atravessam a hierarquia, e em pontos de exibição, mas não devem substituir o polimorfismo.
Exercício 5
Escreva um método que use padrões de lista para descrever um array de inteiros como: "vazio", "um elemento: X", "começa e termina com X" (quando o primeiro e o último são iguais e há pelo menos dois elementos) ou "de X a Y".
Ver resposta
✓ Resposta:
static string Descrever(int[] v) => v switch
{
[] => "vazio",
[var x] => $"um elemento: {x}",
[var primeiro, .., var ultimo] when primeiro == ultimo => $"começa e termina com {primeiro}",
[var primeiro, .., var ultimo] => $"de {primeiro} a {ultimo}"
};
Console.WriteLine(Descrever([])); // vazio
Console.WriteLine(Descrever([7])); // um elemento: 7
Console.WriteLine(Descrever([3, 8, 1, 3])); // começa e termina com 3
Console.WriteLine(Descrever([2, 4, 6])); // de 2 a 6
O padrão [var primeiro, .., var ultimo] casa com qualquer array de dois ou mais elementos. O .. aceita zero elementos no meio, por isso [5, 5] também cai no terceiro braço. A condição "primeiro igual ao último" não é expressável por um padrão, porque compara duas partes entre si, e por isso usa uma guarda when. A ordem dos braços importa: o braço com guarda precisa vir antes do genérico, senão nunca seria alcançado. Note também que o switch é exaustivo sem _: vazio, um elemento e dois ou mais cobrem todos os tamanhos possíveis, e o compilador sabe disso.