Visão geral da correspondência de padrões

A correspondência de padrões é uma técnica em que você testa uma expressão para determinar se ela tem certas características. A correspondência de padrões em C# fornece uma sintaxe mais concisa para testar expressões e agir quando uma expressão corresponde. A "is expressão" suporta correspondência de padrões para testar uma expressão e declarar condicionalmente uma nova variável para o resultado dessa expressão. A "switch expressão" permite que você execute ações com base no primeiro padrão correspondente para uma expressão. Estas duas expressões suportam um vocabulário rico 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 seu código. Para uma discussão completa de todos os padrões que você pode aplicar, consulte o artigo sobre padrões na referência de idioma.

Verificações nulas

Um dos cenários mais comuns para a correspondência de padrões é garantir que os valores não nullsejam . Você pode testar e converter um tipo de valor nulo para seu tipo subjacente ao testar null o uso do exemplo a seguir:

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í-la a uma nova variável. As regras linguísticas tornam esta técnica mais segura do que muitas outras. A variável number só é acessível e atribuída na parte verdadeira da if cláusula. Se você tentar acessá-lo em outro lugar, seja na else cláusula ou após o if bloco, o compilador emitirá um erro. Em segundo lugar, porque 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 valores de referência nulos, adicionando o not padrão:

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. O not é um padrão lógico que corresponde quando o padrão negado não corresponde.

Ensaios 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 System.Collections.Generic.IList<T> interface. Se isso acontecer, ele usará a ICollection<T>.Count propriedade nessa lista para encontrar o índice do meio. O padrão de declaração não corresponde a um null valor, independentemente do tipo de tempo de compilação da variável. O código abaixo protege contra null, além de proteger contra 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 switch expressão para testar uma variável contra vários tipos diferentes. Você pode usar essas informações para criar algoritmos melhores com base no tipo de 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 um despacho 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 lida com quaisquer condições de erro em que o valor não corresponde a um dos valores definidos enum . Se você omitir esse braço de comutação, o compilador avisa que sua expressão de padrão não manipula todos os valores de entrada possíveis. Em tempo de execução, a switch expressão lança uma exceção se o objeto que está sendo examinado não corresponder a nenhum dos braços de comutação. Você pode usar constantes numéricas em vez de um conjunto de valores de enum. 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 um enum. 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 a Span<char> ou a 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ê manipule todas as entradas. O compilador ajuda você certificando-se de que todos os valores de entrada possíveis sejam manipulados.

Padrões relacionais

Você pode usar padrões relacionais para testar como um valor se compara a 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 padrão lógico conjuntivoand para verificar se ambos os padrões relacionais correspondem. Você também pode usar um padrão disjuntivo or para verificar se qualquer um dos padrões corresponde. Os dois padrões relacionais são cercados por parênteses, que você pode usar em torno de qualquer padrão para clareza. Os dois braços interruptores finais lidam com os gabinetes 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 manipular todos os valores de entrada. O compilador também emite um aviso se o padrão para um braço de switch estiver coberto por um padrão anterior. Isso lhe dá liberdade para refatorar e reordenar expressões de switch. Outra maneira de escrever a mesma expressão poderia ser:

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

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

Entradas múltiplas

Todos os padrões cobertos até agora foram verificados uma entrada. Você pode escrever padrões que examinam várias propriedades de um objeto. Considere o seguinte Order registro:

public record Order(int Items, decimal Cost);

O tipo de registro posicional anterior declara dois membros em posições explícitas. Aparecendo primeiro é o Items, depois o da ordem Cost. Para obter mais informações, consulte Registros.

O código a seguir examina o número de itens e o valor de um pedido 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. A próxima verificação contra null, e a final corresponde a qualquer outro valor. Se o Order tipo definir um método adequado Deconstruct , 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 onde as propriedades são desconstruídas para a expressão.

Padrões de lista

Você pode verificar elementos em uma lista ou uma matriz usando um padrão de lista. Um padrão de lista fornece um meio de 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 contendo 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. Ainda pior para o processamento, uma coluna no WITHDRAWAL tipo contém texto gerado pelo usuário e pode conter uma vírgula no texto. Um padrão de lista que inclui o padrão de descarte, padrão constante e 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, onde cada elemento é um campo na linha. As switch teclas de expressão no segundo campo, que determina o tipo de transação e o número de colunas restantes. Cada linha garante que os dados estejam 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 elementos restantes saltam para o campo com o montante. A correspondência final usa o padrão var para capturar a representação de cadeia de caracteres da quantidade. 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 à localização dos elementos. Você usa outros padrões para corresponder características sobre elementos individuais.

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

Consulte também