Menu

Pattern matching em C#: is, padrões no switch, padrões de propriedade e de lista

Como o pattern matching funciona em C#: o operador is com padrões de tipo e de constante, instruções switch com padrões nos case e when, expressões switch, padrões de propriedade, de tupla e posicionais, padrões relacionais e lógicos (and, or, not), padrões de lista e em que versão de C# cada um chegou.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

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ãoExemploVersão
Padrão de tipo com variávelo is Circle cC# 7.0
Padrão de constantex is null, n is 0C# 7.0
Padrão varx is var vC# 7.0
Padrões em rótulos case, guardas whencase 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 posicionalPoint(0, 0)C# 8
Padrão relacional> 100, <= 0C# 9
Padrões lógicosnot null, >= 1 and <= 5, 'a' or 'b'C# 9
Padrão de tipo simplescase 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 de case 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. Trate null com um case próprio ou com is null.
  • Ler a variável do padrão onde a correspondência pode ter falhado. Depois de a is int n || ..., n nã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]).

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR