Visão geral dos padrões correspondentes

A correspondência de padrões é uma técnica em que você testa uma expressão para determinar se ela tem determinadas características. A correspondência de padrões C# fornece uma sintaxe mais concisa para testar expressões e tomar medidas quando uma expressão corresponde. A "expressão is" dá suporte à correspondência de padrões para testar uma expressão e declarar condicionalmente uma nova variável para o resultado dessa expressão. A "expressão switch" permite que você execute ações com base no padrão de primeira correspondência de uma expressão. Essas duas expressões dão suporte a um vocabulário avançado de padrões.

Este artigo fornece uma visão geral dos cenários em que você pode usar a correspondência de padrões. Essas técnicas podem melhorar a legibilidade e a correção do código. Para obter uma discussão completa sobre todos os padrões que você pode aplicar, consulte o artigo sobre padrões na referência de linguagem.

Verificações nulas

Um dos cenários mais comuns para correspondência de padrões é garantir que os valores não sejam null. Você pode testar e converter um tipo de valor anulável em seu tipo subjacente durante o teste de null usando o seguinte exemplo:

int? maybe = 12;

if (maybe is int number)
{
    Console.WriteLine($"The nullable int 'maybe' has the value {number}");
}
else
{
    Console.WriteLine("The nullable int 'maybe' doesn't hold a value");
}

O código anterior é um padrão de declaração para testar o tipo da variável e atribuí-lo a uma nova variável. As regras de idioma tornam essa técnica mais segura do que muitas outras. A variável number só é acessível e atribuída na parte verdadeira da cláusula if. Se você tentar acessá-la em outro lugar, na cláusula else ou após o bloco if, o compilador emitirá um erro. Em segundo lugar, como você não está usando o operador ==, esse padrão funciona quando um tipo sobrecarrega o operador ==. Isso o torna uma maneira ideal de verificar os valores de referência nulos, adicionando o padrão not:

string? message = ReadMessageOrDefault();

if (message is not null)
{
    Console.WriteLine(message);
}

O exemplo anterior usou um padrão constante para comparar a variável com null. not é um padrão lógico que corresponde quando o padrão negado não corresponde.

Testes de tipo

Outro uso comum para correspondência de padrões é testar uma variável para ver se ela corresponde a um determinado tipo. Por exemplo, o código a seguir testa se uma variável não é nula e implementa a interface System.Collections.Generic.IList<T>. Se isso acontecer, ele usará a propriedade ICollection<T>.Count nessa lista para localizar o índice do meio. O padrão de declaração não corresponde a um valor null, independentemente do tipo de tempo de compilação da variável. O código abaixo protege contra null e um tipo que não implementa IList.

public static T MidPoint<T>(IEnumerable<T> sequence)
{
    if (sequence is IList<T> list)
    {
        return list[list.Count / 2];
    }
    else if (sequence is null)
    {
        throw new ArgumentNullException(nameof(sequence), "Sequence can't be null.");
    }
    else
    {
        int halfLength = sequence.Count() / 2 - 1;
        if (halfLength < 0) halfLength = 0;
        return sequence.Skip(halfLength).First();
    }
}

Os mesmos testes podem ser aplicados em uma expressão switch para testar uma variável em vários tipos diferentes. Você pode usar essas informações para criar algoritmos melhores com base no tipo em tempo de execução específico.

Comparar valores discretos

Você também pode testar uma variável para encontrar uma correspondência em valores específicos. O código a seguir mostra um exemplo em que você testa um valor em relação a todos os valores possíveis declarados em uma enumeração:

public State PerformOperation(Operation command) =>
   command switch
   {
       Operation.SystemTest => RunDiagnostics(),
       Operation.Start => StartSystem(),
       Operation.Stop => StopSystem(),
       Operation.Reset => ResetToReady(),
       _ => throw new ArgumentException("Invalid enum value for command", nameof(command)),
   };

O exemplo anterior demonstra uma expedição de método com base no valor de uma enumeração. O caso _ final é um padrão de descarte que corresponde a todos os valores. Ele identifica quaisquer condições de erro em que o valor não corresponda a um dos valores enum definidos. Se você omitir esse braço de comutador, o compilador avisará que sua expressão padrão não manipula todos os valores de entrada possíveis. Em tempo de execução, a expressão switch lançará uma exceção se o objeto que está sendo examinado não corresponder a nenhum braço switch. Você pode usar constantes numéricas em vez de um conjunto de valores de enumeração. Você também pode usar essa técnica semelhante para valores de cadeia de caracteres constantes que representam os comandos:

public State PerformOperation(string command) =>
   command switch
   {
       "SystemTest" => RunDiagnostics(),
       "Start" => StartSystem(),
       "Stop" => StopSystem(),
       "Reset" => ResetToReady(),
       _ => throw new ArgumentException("Invalid string value for command", nameof(command)),
   };

O exemplo anterior mostra o mesmo algoritmo, mas usa valores de cadeia de caracteres em vez de uma enumeração. Você usaria esse cenário se seu aplicativo respondesse a comandos de texto em vez de um formato de dados regular. A partir do C# 11, você também pode usar um Span<char> ou ReadOnlySpan<char> para testar valores de cadeia de caracteres constantes, conforme mostrado no exemplo a seguir:

public State PerformOperation(ReadOnlySpan<char> command) =>
   command switch
   {
       "SystemTest" => RunDiagnostics(),
       "Start" => StartSystem(),
       "Stop" => StopSystem(),
       "Reset" => ResetToReady(),
       _ => throw new ArgumentException("Invalid string value for command", nameof(command)),
   };

Em todos esses exemplos, o padrão de descarte garante que você identifique todas as entradas. O compilador ajuda você a garantir que todos os valores de entrada possíveis sejam identificados.

Padrões relacionais

Os padrões relacionais podem ser usados para testar como um valor se compara às constantes. Por exemplo, o código a seguir retorna o estado da água com base na temperatura em Fahrenheit:

string WaterState(int tempInFahrenheit) =>
    tempInFahrenheit switch
    {
        (> 32) and (< 212) => "liquid",
        < 32 => "solid",
        > 212 => "gas",
        32 => "solid/liquid transition",
        212 => "liquid / gas transition",
    };

O código anterior também demonstra o andpadrão lógico conjuntivo para verificar se ambos os padrões relacionais correspondem. Você também pode usar um padrão disjuntivo or para verificar se um dos padrões corresponde. Os dois padrões relacionais ficam entre parênteses, que podem ser usados em qualquer padrão para maior clareza. Os dois últimos braços switch identificam os casos para o ponto de fusão e o ponto de ebulição. Sem esses dois braços, o compilador avisa que sua lógica não cobre todas as entradas possíveis.

O código anterior também demonstra outro recurso importante que o compilador fornece para expressões de correspondência de padrões: o compilador avisa se você não identifica todos os valores de entrada. O compilador também emitirá um aviso se o padrão de um braço de comutador for coberto por um padrão anterior. Isso lhe dá liberdade para refatorar e reordenar as expressões de alternância. Outra maneira de gravar a mesma expressão pode ser:

string WaterState2(int tempInFahrenheit) =>
    tempInFahrenheit switch
    {
        < 32 => "solid",
        32 => "solid/liquid transition",
        < 212 => "liquid",
        212 => "liquid / gas transition",
        _ => "gas",
};

A lição principal no exemplo anterior e qualquer outra refatoração ou reordenação é que o compilador valida que seu código manipula todas as entradas possíveis.

Várias entradas

Todos os padrões vistos até agora foram para verificar uma entrada. Você pode gravar padrões que examinam várias propriedades de um objeto. Considere o registro Order a seguir:

public record Order(int Items, decimal Cost);

O tipo de registro posicional anterior declara dois membros em posições explícitas. Primeiro, Items aparece e, em seguida, o Cost da ordem. Para saber mais, confira Registros.

O código a seguir examina o número de itens e o valor de uma ordem para calcular um preço com desconto:

public decimal CalculateDiscount(Order order) =>
    order switch
    {
        { Items: > 10, Cost: > 1000.00m } => 0.10m,
        { Items: > 5, Cost: > 500.00m } => 0.05m,
        { Cost: > 250.00m } => 0.02m,
        null => throw new ArgumentNullException(nameof(order), "Can't calculate discount on null order"),
        var someObject => 0m,
    };

Os dois primeiros braços examinam duas propriedades do Order. O terceiro examina apenas o custo. O próximo verifica null e o final corresponde a qualquer outro valor. Se o tipo Order definir um método Deconstruct adequado, você poderá omitir os nomes de propriedade do padrão e usar a desconstrução para examinar as propriedades:

public decimal CalculateDiscount(Order order) =>
    order switch
    {
        ( > 10,  > 1000.00m) => 0.10m,
        ( > 5, > 50.00m) => 0.05m,
        { Cost: > 250.00m } => 0.02m,
        null => throw new ArgumentNullException(nameof(order), "Can't calculate discount on null order"),
        var someObject => 0m,
    };

O código anterior demonstra o padrão posicional em que as propriedades são desconstruídas para a expressão.

Padrões de lista

Você pode verificar os elementos em uma lista ou uma matriz usando um padrão de lista. Um padrão de lista fornece um meio para aplicar um padrão a qualquer elemento de uma sequência. Além disso, você pode aplicar o padrão de descarte (_) para corresponder a qualquer elemento ou aplicar um padrão de fatia para corresponder a zero ou mais elementos.

Os padrões de lista são uma ferramenta valiosa quando os dados não seguem uma estrutura regular. Você pode usar a correspondência de padrões para testar a forma e os valores dos dados em vez de transformá-los em um conjunto de objetos.

Considere o seguinte trecho de um arquivo de texto que contém transações bancárias:

04-01-2020, DEPOSIT,    Initial deposit,            2250.00
04-15-2020, DEPOSIT,    Refund,                      125.65
04-18-2020, DEPOSIT,    Paycheck,                    825.65
04-22-2020, WITHDRAWAL, Debit,           Groceries,  255.73
05-01-2020, WITHDRAWAL, #1102,           Rent, apt, 2100.00
05-02-2020, INTEREST,                                  0.65
05-07-2020, WITHDRAWAL, Debit,           Movies,      12.57
04-15-2020, FEE,                                       5.55

É um formato CSV, mas algumas das linhas têm mais colunas do que outras. Pior ainda para o processamento, uma coluna do tipo WITHDRAWAL tem texto gerado pelo usuário e pode conter uma vírgula no texto. Um padrão de lista que inclui o padrão descarte, o padrão constante e o padrão var para capturar os dados de processos de valor neste formato:

decimal balance = 0m;
foreach (string[] transaction in ReadRecords())
{
    balance += transaction switch
    {
        [_, "DEPOSIT", _, var amount]     => decimal.Parse(amount),
        [_, "WITHDRAWAL", .., var amount] => -decimal.Parse(amount),
        [_, "INTEREST", var amount]       => decimal.Parse(amount),
        [_, "FEE", var fee]               => -decimal.Parse(fee),
        _                                 => throw new InvalidOperationException($"Record {string.Join(", ", transaction)} is not in the expected format!"),
    };
    Console.WriteLine($"Record: {string.Join(", ", transaction)}, New balance: {balance:C}");
}

O exemplo anterior usa uma matriz de cadeia de caracteres, em que cada elemento é um campo na linha. As teclas de expressão switch no segundo campo, que determina o tipo de transação, e o número de colunas restantes. Cada linha garante que os dados estão no formato correto. O padrão de descarte (_) ignora o primeiro campo, com a data da transação. O segundo campo corresponde ao tipo de transação. As correspondências de elemento restantes pulam para o campo com a quantidade. A partida final usa o padrão var para capturar a representação de cadeia de caracteres do valor. A expressão calcula o valor a ser adicionado ou subtraído do saldo.

Os padrões de lista permitem que você corresponda na forma de uma sequência de elementos de dados. Você usa os padrões de descarte e fatia para corresponder ao local dos elementos. Você usa outros padrões para corresponder às características sobre elementos individuais.

Este artigo fez um tour pelos tipos de código que você pode gravar com correspondência de padrões em C#. Os artigos a seguir mostram mais exemplos de uso dos padrões em cenários e o vocabulário completo dos padrões disponíveis para uso.

Confira também