Pattern Matching: Patterns de Tipo, Constante e Relacionais, e switch Expressions
Objective
Pattern matching permite que uma única expressão pergunte "o que é este valor?"
e desvie conforme a resposta: o tipo em runtime, se ele é igual a uma constante,
em que faixa cai, ou uma combinação lógica disso. Os mesmos patterns funcionam
no is, na instrução switch e na switch expression. O que os torna mais do
que sintaxe é a análise do compilador: ele sabe quando uma switch expression
deixa valores sem tratamento, quando um braço nunca será alcançado e quando um
teste com is é sempre ou nunca verdadeiro, e compila os braços numa árvore de
decisão em vez de uma lista de ifs. Este conceito cobre os patterns de tipo,
constante, relacional, lógico, var e descarte, as guardas when,
exaustividade e subsunção com as mensagens reais do compilador, e como fica o
código gerado. Property, positional e list patterns ficam para o próximo
conceito.
Use Cases
- Mapear um enum ou um código de status para um valor numa única expressão, com o compilador apontando o caso que você esqueceu.
- Classificar números em faixas (faixas de imposto, famílias de status HTTP,
notas) sem cadeias de
if/else if. - Tratar um
objectou um tipo base que pode conter vários tipos concretos, como uma mensagem vinda de uma fila ou um nó JSON. - Trocar
x != null && x is Fooe casts por um únicox is Foo foo.
Deep Dive
O catálogo
csharpstatic string Describe(object? o) => o switch
{
null => "null constant", // constant pattern
0 => "zero", // constant pattern (int)
int n and > 0 => $"positive int {n}", // declaração + relacional, combinados com and
int => "other int", // type pattern (C# 9)
string { Length: 0 } => "empty string", // tipo + property pattern
string s => $"string of {s.Length}", // declaration pattern
char c and (>= 'a' and <= 'z') => $"lowercase {c}",
long or short => "long or short", // or entre dois type patterns
_ => $"something else ({o.GetType().Name})", // descarte
};
Describe(42); // positive int 42
Describe(-3); // other int
Describe(""); // empty string
Describe('x'); // lowercase x
Describe(7L); // long or short
Describe(new[] {1}); // something else (Int32[])Algumas regras valem ser conhecidas com precisão:
- Patterns de tipo e de declaração nunca casam com
null.maybe is stringéfalsepara umstring?nulo. Eles casam com o tipo em runtime por identidade, herança, implementação de interface, boxing ou unboxing, e com umNullable<T>que tem valor (int? ni = 5; ni is int xdáx = 5). Ignoram conversões definidas pelo usuário. - O pattern
varcasa com tudo, inclusivenull.maybe is var vétruemesmo commaybenulo. Use para capturar um valor intermediário, não para testar algo. - Constant patterns aceitam valores numéricos,
char,string,bool, enum econst, além denull.x is nullnunca chama um==definido pelo usuário, e é por isso que é preferido ax == null. - Relational patterns (
<,<=,>,>=) recebem uma constante numérica,charou enum. Não casam comnullnem comNaN, que precisa ser tratado com o constant patterndouble.NaN.
Patterns lógicos e precedência
not liga mais forte que and, que liga mais forte que or. Isso torna um
erro muito fácil de escrever:
csharpstatic bool WrongNotLower(char c) => c is not >= 'a' and <= 'z'; // (not >= 'a') and <= 'z'
static bool RightNotLower(char c) => c is not (>= 'a' and <= 'z');
WrongNotLower('~'); // False: '~' vem depois de 'z', então "<= 'z'" falha
RightNotLower('~'); // TrueO compilador não pega todas as variações. n is not 1 or 2 significa
(not 1) or 2, então o 2 é redundante e a expressão é só n is not 1, mas o
SDK 10.0.100 compila isso sem nenhum aviso. Na dúvida, coloque parênteses.
Guardas when
Uma guarda acrescenta uma condição booleana qualquer depois de um pattern. Ela só roda quando o pattern já casou, e é o único lugar para checagens que patterns não expressam, como comparar dois valores capturados ou chamar um método:
csharpdouble d when double.IsFinite(d) => $"finite double {d}",O preço é que o compilador não consegue raciocinar sobre guardas. Elas não contam para a exaustividade, como mostra a próxima seção.
Exaustividade
Uma switch expression precisa produzir um valor para toda entrada. Quando o
compilador encontra uma entrada que nenhum braço trata, ele avisa e mostra um
exemplo. Estas são as mensagens do SDK 10.0.100:
csharpn switch { < 0 => "negative", 0 => "zero" }
// warning CS8509: The switch expression does not handle all possible values of its
// input type (it is not exhaustive). For example, the pattern '1' is not covered.
c switch { Color.Red => "red", Color.Green => "green", Color.Blue => "blue" }
// warning CS8524: ... involving an unnamed enum value. For example, the pattern
// '(Color)3' is not covered.
s switch { string text => text } // s é string?
// warning CS8655: The switch expression does not handle some null inputs
// (it is not exhaustive). For example, the pattern 'null' is not covered.
n switch { var x when x >= 0 => "non-negative", var x when x < 0 => "negative" }
// warning CS8846: ... For example, the pattern '_' is not covered. However, a
// pattern with a 'when' clause might successfully match this value.O caso do enum surpreende: um enum é só um inteiro, então (Color)42 é um valor
legal. Se o aviso for ignorado e esse valor chegar, o runtime lança:
csharpName((Color)42);
// SwitchExpressionException: Non-exhaustive switch expression failed to match its input.
// Unmatched value was 42.Hierarquias de classes não ganham ajuda no C# 14. Mesmo com uma base abstract
e duas subclasses sealed, ambas tratadas, o compilador ainda reporta CS8509
com o pattern '_', porque outro assembly poderia acrescentar uma subclasse. A
documentação do C# 15, ainda em preview, descreve classes closed que mudam
isso; no C# 14 você precisa de um braço de descarte. Já um switch sobre bool
com braços true e false é exaustivo e compila sem aviso.
Subsunção: braços que nunca rodam
Os braços são testados na ordem do texto, então um braço cujos valores já foram todos tratados antes é código morto. O compilador transforma isso em erro, não em aviso:
csharpn switch { > 0 => "positive", > 10 => "big", _ => "other" }
// error CS8510: The pattern is unreachable. It has already been handled by a
// previous arm of the switch expression or it is impossible to match.
o switch { string s => s, "hello" => "greeting", _ => "other" } // mesmo CS8510
switch (n) { case > 0: break; case 5: break; }
// error CS8120: The switch case is unreachable. It has already been handled by a
// previous case or it is impossible to match.A mesma análise roda no is: n is > 5 and < 3 é o erro CS8518 ("An
expression of type 'int' can never match the provided pattern"), e n is int
sobre um int é o aviso CS0183 ("The given expression is always of the provided
('int') type").
O que o compilador gera
Os braços não viram uma sequência de ifs. O Roslyn monta um grafo de decisão
que compartilha testes entre braços e os ordena por velocidade. Desmontando um
build Release, dá para ver:
- Constantes inteiras densas (
0 => ..., 1 => ..., 4 => ...) viram uma única instrução ILswitch, uma tabela de saltos. - Faixas relacionais (
< 0,< 10,< 100,_) viram uma busca binária: a primeira comparação no IL é contra10, a fronteira do meio, e não contra0, o primeiro braço. - Constantes string são despachadas pelo
Lengthcom umswitchde IL, depois por um caractere numa posição escolhida, e só então confirmadas comstring ==. Para sete verbos HTTP, nenhum hash é calculado. - Type patterns viram
isinst, e dois braços do mesmo tipo (string s when s.Length > 3estring) compartilham um únicoisinst string.
A consequência prática: a ordem dos braços importa para o resultado, mas você não precisa reordenar braços por desempenho. Escreva na ordem que lê melhor e deixe a checagem de subsunção te proteger.
Trade-offs
- Um braço de descarte silencia a exaustividade para sempre.
_ => throwdeixa o código de hoje seguro, mas quando alguém adicionaColor.Yellowo compilador fica quieto. Para enums seus, prefira listar todos os membros e aceitar o CS8524, ou lance a exceção com o valor para a falha ficar óbvia.csharp_ => throw new ArgumentOutOfRangeException(nameof(c), c, "Unknown color"), - Guardas derrotam a análise. Todo
whené opaco para exaustividade e subsunção. Expresse a condição como pattern (> 0em vez dewhen x > 0) sempre que der. switchsobre tipos não é polimorfismo. Um switch de tipos coloca o comportamento perto de quem chama, o que é bom para conjuntos fechados que você controla (mensagens, resultados) e ruim quando tipos novos aparecem com frequência: todo switch precisa mudar. Um método virtual mantém o comportamento junto do tipo.- A precedência de
not,and,orengana fácil, e nem todo erro gera diagnóstico. Coloque parênteses em tudo que misturenote um combinador. NaNenullescapam de braços relacionais.< 0e>= 0juntos não cobrem umdouble. Adicione braçosdouble.NaNenullexplicitamente quando o tipo de entrada permitir.
Documentation Links
- Patterns, C# reference, Microsoft Learn
- switch expression, C# reference, Microsoft Learn
- Pattern matching overview, Microsoft Learn
- Selection statements: if and switch, C# reference, Microsoft Learn
- Errors and warnings for pattern matching, Microsoft Learn
- SwitchExpressionException class, Microsoft Learn