Pattern matching não é novidade no C# — o operador is existe desde o C# 1. O que mudou radicalmente a partir do C# 8 (e evoluiu até o C# 13) é a expressividade: você substitui cascatas de if/else com casting manual por expressões declarativas que descrevem a estrutura dos dados. O resultado é código que lê como especificação, não como implementação.
O ponto de partida: switch tradicional vs switch expression
Compare as duas abordagens para calcular frete baseado no tipo de entrega:
// C# antigo — imperativo e verboso
decimal CalcularFrete(EntregaBase entrega)
{
switch (entrega)
{
case EntregaExpresso e when e.Peso > 10:
return 45.00m;
case EntregaExpresso:
return 25.00m;
case EntregaEconomica eco when eco.Regiao == "Norte":
return 18.00m;
case EntregaEconomica:
return 8.50m;
default:
throw new InvalidOperationException($"Tipo desconhecido: {entrega.GetType()}");
}
}
// C# moderno — declarativo e expressivo
decimal CalcularFrete(EntregaBase entrega) => entrega switch
{
EntregaExpresso { Peso: > 10 } => 45.00m,
EntregaExpresso => 25.00m,
EntregaEconomica { Regiao: "Norte" } => 18.00m,
EntregaEconomica => 8.50m,
_ => throw new InvalidOperationException(
$"Tipo desconhecido: {entrega.GetType()}")
};
A versão moderna não é só mais curta — ela elimina o boilerplate de variáveis temporárias, a necessidade de casting explícito, e torna a lógica de decisão visível na estrutura do código.
Property patterns: desestruturando objetos
Property patterns permitem inspecionar propriedades de um objeto diretamente na expressão de match:
record Pedido(string Status, decimal Valor, bool ClienteVip, string Pais);
string ClassificarPedido(Pedido p) => p switch
{
// Pattern aninhado — propriedades compostas
{ Status: "Pendente", Valor: > 1000, ClienteVip: true } =>
"Alta prioridade VIP",
{ Status: "Pendente", Valor: > 1000 } =>
"Alta prioridade",
// Property pattern em string com constant pattern
{ Status: "Cancelado", Pais: "BR" } =>
"Cancelado nacional — emitir nota de cancelamento",
{ Status: "Cancelado" } =>
"Cancelado internacional — acionar parceiro logístico",
{ Status: "Enviado" or "Entregue" } =>
"Em trânsito ou concluído",
_ => "Status não mapeado"
};
Positional patterns: desestruturando via Deconstruct
Se o tipo tem um método Deconstruct (incluindo records e tuples), você pode usar positional patterns:
record Coordenada(double Latitude, double Longitude)
{
// Records geram Deconstruct automaticamente
}
string ClassificarRegiao(Coordenada c) => c switch
{
// Desestrutura (Latitude, Longitude) diretamente
(var lat, var lon) when lat > 0 && lon > 0 => "Nordeste",
(var lat, var lon) when lat > 0 && lon < 0 => "Noroeste",
(var lat, var lon) when lat < 0 && lon > 0 => "Sudeste",
(var lat, var lon) when lat < 0 && lon < 0 => "Sudoeste",
(0, 0) => "Origem",
_ => "No eixo"
};
// Também funciona com tuples inline
string ClassificarResultado(int codigo, string mensagem) => (codigo, mensagem) switch
{
(200, _) => "Sucesso",
(400, var msg) => $"Erro do cliente: {msg}",
(401 or 403, _) => "Não autorizado",
(>= 500, var msg) => $"Erro do servidor: {msg}",
_ => "Status desconhecido"
};
List patterns (C# 11): matching em coleções
C# 11 adicionou list patterns — você consegue fazer match na estrutura de uma lista ou array:
string AnalisarHistorico(decimal[] ultimosPrecos) => ultimosPrecos switch
{
// Array vazio
[] => "Sem histórico",
// Exatamente um elemento
[var unico] => $"Apenas um preço: {unico}",
// Começa com dois valores específicos
[0, 0, ..] => "Começou zerado",
// Dois últimos elementos capturados com slice pattern
[.., var penultimo, var ultimo] when ultimo > penultimo =>
$"Tendência de alta: {penultimo} → {ultimo}",
[.., var penultimo, var ultimo] when ultimo < penultimo =>
$"Tendência de queda: {penultimo} → {ultimo}",
_ => "Estável"
};
// Verificar protocolo HTTP a partir de bytes
bool IsHttps(byte[] cabecalho) => cabecalho switch
{
[(byte)'H', (byte)'T', (byte)'T', (byte)'P', (byte)'S', ..] => true,
_ => false
};
Combinando padrões: and, or, not
C# 9 introduziu os operadores lógicos de pattern — and, or, not — que funcionam como conectivos dentro de expressões de match:
// Relational patterns + logical patterns
string ClassificarIdade(int idade) => idade switch
{
< 0 => throw new ArgumentException("Idade negativa"),
0 => "Recém-nascido",
>= 1 and <= 12 => "Criança",
>= 13 and <= 17 => "Adolescente",
>= 18 and <= 64 => "Adulto",
>= 65 => "Idoso",
_ => "Inválido" // nunca alcançado — compilador avisa
};
// not pattern — semântica de negação clara
bool PodeReceberConteudoAdulto(Usuario u) => u is
not { Status: "Banido" } and
not { Idade: < 18 } and
not { TermosAceitos: false };
// or em type patterns
void ProcessarEvento(IEvento evento)
{
if (evento is EventoPagamento or EventoEstorno { Aprovado: true })
AtualizarSaldo(evento);
}
Pattern matching em is expressions
O operador is evoluiu para suportar todos os mesmos padrões do switch. Útil para condicionais simples sem precisar de um switch completo:
// Type pattern com captura
if (resultado is ErroValidacao { Campos: { Length: > 0 } erros })
{
foreach (var campo in erros)
AdicionarErroFormulario(campo);
return;
}
// Negação com is not
if (usuário is not { Perfil: PerfilAdmin or PerfilSuperAdmin })
throw new UnauthorizedAccessException();
// Captura + guard em uma linha
while (fila.TryDequeue(out var item) && item is { Ativo: true } tarefa)
{
await ExecutarAsync(tarefa);
}
Caso de uso real: processamento de comandos DDD
Pattern matching brilha em aplicações com Command/Event handling, onde você precisa despachar para handlers específicos baseado no tipo e conteúdo:
public async Task<IResult> HandleAsync(IComando comando, CancellationToken ct) =>
comando switch
{
CriarPedidoComando { Itens: { Length: 0 } } =>
Results.BadRequest("Pedido deve ter pelo menos um item"),
CriarPedidoComando { ClienteId: var cid, Itens: var itens } =>
Results.Ok(await _pedidoService.CriarAsync(cid, itens, ct)),
CancelarPedidoComando { PedidoId: var pid, Motivo: null or "" } =>
Results.BadRequest("Motivo de cancelamento é obrigatório"),
CancelarPedidoComando { PedidoId: var pid, Motivo: var motivo } =>
Results.Ok(await _pedidoService.CancelarAsync(pid, motivo, ct)),
AtualizarEnderecoComando { EnderecoId: <= 0 } =>
Results.BadRequest("ID de endereço inválido"),
AtualizarEnderecoComando atualizar =>
Results.Ok(await _enderecoService.AtualizarAsync(atualizar, ct)),
_ => Results.BadRequest($"Comando não reconhecido: {comando.GetType().Name}")
};
Exhaustiveness checking: o compilador como aliado
Um dos maiores benefícios do switch expression é a análise de exaustividade em tempo de compilação. Com hierarquias de tipos seladas (sealed), o compilador verifica se todos os casos foram cobertos:
// Hierarquia selada — o compilador conhece todos os subtipos
abstract record Forma;
sealed record Circulo(double Raio) : Forma;
sealed record Retangulo(double Largura, double Altura) : Forma;
sealed record Triangulo(double Base, double Altura) : Forma;
// Se você esquecer um caso, o compilador AVISA (CS8509 — não erro, mas warning)
double CalcularArea(Forma f) => f switch
{
Circulo { Raio: var r } => Math.PI * r * r,
Retangulo { Largura: var l, Altura: var a } => l * a,
Triangulo { Base: var b, Altura: var a } => b * a / 2
// Sem _ => ... — compilador verifica exaustividade graças a sealed
};
Com classes abertas, o compilador não tem como garantir exaustividade — mas um _ com throw deixa explícito o caso não mapeado e lança em tempo de execução.
Quando pattern matching é a escolha errada
Pattern matching não substitui polimorfismo. Se você tem um switch em tipo que cresce frequentemente com novos casos, provavelmente deveria usar o padrão Strategy ou Virtual Methods — adicionar um novo tipo não vai exigir alterar um switch em vários lugares. Use pattern matching quando: a lógica de dispatch é localizada (um lugar, um propósito), os tipos são estáveis (não mudam frequentemente), ou você está combinando múltiplas dimensões de decisão que não fazem sentido como métodos virtuais.
Pattern matching não é uma feature de nicho — é o caminho que C# escolheu para escrever lógica condicional estruturada. Aprender os padrões disponíveis (property, positional, list, logical, relational) e saber quando combiná-los é uma das habilidades que mais reduz ruído em código .NET moderno.