O pattern matching testa um valor contra uma forma: "é um Circle?", "é null?", "o Status dele é "paid" e o Total passa de 100?". Quando o teste dá certo, o padrão também pode extrair partes do valor para novas variáveis. Ele começou pequeno no C# 7, com o operador is, e cresceu a cada versão desde então, então esta página indica a versão de cada forma.
O padrão de tipo com is (C# 7)
Antes do C# 7, verificar um tipo e usá-lo levava dois passos: as mais uma verificação de null, ou is mais um cast. O padrão de tipo faz as duas coisas de uma vez e dá a você uma variável tipada:
Saída:
card ending 4242: 49.90
transfer from PT50...: 1200
no payment
other payment: 5
p is CardPayment card só é verdadeiro quando p é um CardPayment não null (ou uma classe derivada dele), e então card o guarda com o tipo estático certo. Um valor null nunca corresponde a um padrão de tipo, e é por isso que o pagamento null cai na verificação de null.
Escopo e atribuição definida
A variável do padrão existe no bloco que a contém, mas o compilador só deixa você lê-la onde se sabe que a correspondência deu certo. Isso faz funcionar a forma de "guard clause", em que você sai cedo e usa a variável depois:
Saída:
19.99
42 is over 40
price must be text
boxed is int n && n > 40 mostra a outra forma comum: a variável pode ser usada à direita do &&, porque esse lado só executa quando a correspondência deu certo. À direita de || ela não está atribuída, e o compilador avisa (CS0165).
Os padrões também fazem unboxing: boxed is int n dá certo para um int com boxing e entrega o valor sem um cast que poderia lançar exceção. Para tipos de valor anuláveis, maybe is int n dá certo exatamente quando maybe tem um valor.
Padrões de constante e de null
Uma constante também é um padrão. x is null é o mais usado, e o C# 7 também permite x is 0, status is "paid" ou um membro de enum:
if (customer is null) return; // null check that ignores any == overload
if (count is 0) Console.WriteLine("empty");
if (level is LogLevel.Error) Alert();
Muitas equipes preferem is null a == null porque ele não pode ser redirecionado por uma sobrecarga de operador do tipo. O C# 9 adiciona a forma negada, is not null.
Padrões em instruções switch (C# 7)
Os mesmos padrões de tipo podem rotular os case, e uma cláusula when acrescenta uma condição. Os cases são testados de cima para baixo, então ordene-os do específico para o geral:
static string Describe(object o)
{
switch (o)
{
case null: return "nothing";
case int n when n < 0: return "negative number";
case int n: return $"number {n}";
case string s when s.Length == 0: return "empty text";
case string s: return $"text of {s.Length} chars";
case IEnumerable<int> list: return "a sequence of numbers";
default: return o.GetType().Name;
}
}
Describe(-4); // negative number
Describe(""); // empty text
Describe(new List<int>()); // a sequence of numbers
Describe(2.5); // Double
O compilador verifica a ordem: um case que nunca pode corresponder porque um anterior o cobre é o erro CS8120. O default é sempre avaliado por último, esteja onde estiver. A mesma lógica escrita com cadeias de if/is, como no exemplo de pagamento acima, funciona em qualquer versão de C# a partir da 7.
Expressões switch (C# 8)
Uma expressão switch é a forma compacta para "calcular um valor a partir de vários casos". O valor vem primeiro, depois switch, depois os braços pattern => result separados por vírgulas, com _ como alternativa final:
public abstract record Shape;
public record Circle(double Radius) : Shape;
public record Rectangle(double Width, double Height) : Shape;
static double Area(Shape shape) => shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle { Width: var w, Height: var h } => w * h,
_ => throw new ArgumentException("Unknown shape", nameof(shape)),
};
Area(new Rectangle(3, 4)); // 12
Se nenhum braço corresponder em tempo de execução, ela lança SwitchExpressionException, e o compilador avisa (CS8509) quando consegue ver que alguma entrada não é tratada. A página da expressão switch cobre a sintaxe por si só; o resto desta página é sobre os padrões que você pode colocar nos braços.
Padrões de propriedade (C# 8, estendidos no C# 10)
Um padrão de propriedade compara as propriedades de um objeto com padrões aninhados, entre chaves. O objeto também precisa ser não null, então { } sozinho significa "não null":
public record Address(string City, string Country);
public record Order(decimal Total, string Status, Address ShipTo, int Items);
static string Shipping(Order order) => order switch
{
{ Status: "cancelled" } => "no shipment",
{ ShipTo.Country: "PT", Total: >= 50m } => "free, domestic", // C# 10 dotted form
{ ShipTo.Country: "PT" } => "4.90, domestic",
{ Total: > 200m } => "free, international",
_ => "12.00, international",
};
Shipping(new Order(60m, "paid", new Address("Porto", "PT"), 2)); // free, domestic
Shipping(new Order(260m, "paid", new Address("Lyon", "FR"), 2)); // free, international
No C# 8 e no 9, a forma aninhada é obrigatória: { ShipTo: { Country: "PT" } }. O C# 10 permite a abreviação com ponto usada acima. Padrões de propriedade também funcionam com is, o que faz verificações com várias condições se lerem como uma descrição: if (order is { Status: "paid", Items: > 0 }).
Padrões relacionais e lógicos (C# 9)
O C# 9 adicionou as comparações (<, <=, >, >=) e os combinadores and, or e not, que transformam intervalos em padrões:
static string Grade(int score) => score switch
{
< 0 or > 100 => "invalid",
>= 90 => "A",
>= 75 and < 90 => "B",
>= 50 => "C",
_ => "F",
};
Grade(82); // B
Grade(101); // invalid
if (input is not null and not "") { ... }
if (c is >= 'a' and <= 'z' or >= 'A' and <= 'Z') { ... } // and binds tighter than or
O equivalente em C# 7 é uma cadeia de if/else if ou um switch com cláusulas when (case int s when s >= 90:).
Padrões de tupla e posicionais (C# 8)
Fazer switch sobre vários valores ao mesmo tempo é feito com switch sobre uma tupla:
static string Quadrant(int x, int y) => (x, y) switch
{
(0, 0) => "origin",
(> 0, > 0) => "I",
(< 0, > 0) => "II",
(< 0, < 0) => "III",
(> 0, < 0) => "IV",
_ => "on an axis",
};
Quadrant(-2, 5); // II
Um padrão posicional faz o mesmo para qualquer tipo com um método Deconstruct, incluindo os records: case Point(0, 0): ou p is Rectangle(var w, var h) && w == h.
Padrões de lista (C# 11)
Os padrões de lista comparam arrays e listas pelos seus elementos. .. corresponde a qualquer quantidade de elementos (uma "fatia"), e cada posição de elemento pode ter qualquer outro padrão:
static string Route(string[] args) => args switch
{
[] => "help",
["add", var item] => $"add {item}",
["remove", var item, ..] => $"remove {item}",
[var cmd, ..] => $"unknown command {cmd}",
};
Route(new[] { "add", "milk" }); // add milk
Route(new[] { "remove", "milk", "now" }); // remove milk
int[] nums = { 1, 2, 3, 4 };
if (nums is [1, .., var last]) Console.WriteLine(last); // 4
Que versão adicionou o quê
| Padrão | Exemplo | Versão |
|---|---|---|
| Padrão de tipo com variável | o is Circle c | C# 7.0 |
| Padrão de constante | x is null, n is 0 | C# 7.0 |
Padrão var | x is var v | C# 7.0 |
Padrões em rótulos case, guardas when | case int n when n < 0: | C# 7.0 |
Expressão switch, descarte _ | x switch { ... } | C# 8 |
| Padrão de propriedade | { Status: "paid" } | C# 8 |
| Padrão de tupla | (x, y) switch { (0, 0) => ... } | C# 8 |
| Padrão posicional | Point(0, 0) | C# 8 |
| Padrão relacional | > 100, <= 0 | C# 9 |
| Padrões lógicos | not null, >= 1 and <= 5, 'a' or 'b' | C# 9 |
| Padrão de tipo simples | case Circle:, Circle => ... | C# 9 |
| Padrão de propriedade estendido | { ShipTo.City: "Porto" } | C# 10 |
| Padrão de lista e fatia | [first, .., last] | C# 11 |
Erros comuns
- Ordenar os cases do geral para o específico.
case Payment p:antes decase CardPayment c:torna o segundo inalcançável (CS8120). O específico primeiro. - Esperar que um padrão de tipo corresponda a null.
null is string sé falso. Tratenullcom um case próprio ou comis null. - Ler a variável do padrão onde a correspondência pode ter falhado. Depois de
a is int n || ...,nnão está atribuída (CS0165). - Switches longos por tipo em vez de métodos virtuais. Se toda subclasse nova exige editar o mesmo switch em cinco lugares, o comportamento pertence à hierarquia de classes. Os padrões brilham com dados vindos de fora (formatos JSON, mensagens, tuplas de entradas) e com conjuntos fechados de tipos que você controla.
- Esquecer a alternativa final em uma expressão switch. Uma entrada sem correspondência lança exceção em tempo de execução; leia o aviso CS8509.
Perguntas frequentes
O que é pattern matching em C#?
Pattern matching testa um valor contra uma forma e, quando ele corresponde, pode extrair partes dele para variáveis no mesmo passo. if (shape is Circle c) testa o tipo e dá a você um c tipado; order is { Status: "paid", Total: > 100 } testa propriedades. Os padrões aparecem em expressões is, em instruções switch e em expressões switch.
Como usar is com uma variável em C#?
if (obj is Customer c) { ... } verifica se obj é um Customer (e não null) e o atribui a c, tipado como Customer, dentro do if. Ele substitui o antigo par var c = obj as Customer; if (c != null). A variável só está definitivamente atribuída onde se sabe que o teste é verdadeiro, então if (!(obj is Customer c)) return; deixa c utilizável depois do if.
Qual a diferença entre is null e == null em C#?
x is null sempre verifica a própria referência. x == null chama o operador == do tipo se ele tiver uma sobrecarga, o que normalmente dá a mesma resposta, mas não é garantido. Para o contrário, escreva x != null, !(x is null) ou, a partir do C# 9, x is not null.
Como funciona o pattern matching no switch com when em C#?
Um rótulo case pode ter um padrão mais uma guarda when: case Order o when o.Total > 100:. O case só corresponde se o padrão corresponder e a condição for verdadeira, e os cases são testados de cima para baixo, então coloque os mais específicos primeiro. O compilador reporta CS8120 quando um case nunca pode ser alcançado porque um anterior já o cobre.
Qual versão de C# adicionou cada padrão?
C# 7.0: padrões de tipo, de constante e var em is e case, mais when. C# 8: expressões switch, padrões de propriedade, de tupla e posicionais. C# 9: padrões relacionais (> 5), lógicos (and, or, not) e padrões de tipo simples. C# 10: padrões de propriedade estendidos ({ Address.City: "Porto" }). C# 11: padrões de lista ([1, .., var last]).