Partilhar via


11 Padrões e correspondência de padrões

11.1 Generalidades

Um padrão é uma forma sintática que pode ser usada com o is operador (§12.14.12), em um switch_statement (§13.8.3) e em um switch_expression (§12.11) para expressar a forma dos dados com os quais os dados recebidos devem ser comparados. Os padrões podem ser recursivos, de modo que partes dos dados podem ser comparadas com subpadrões.

Um padrão é testado em relação a um valor em vários contextos:

  • Em uma instrução switch, o padrão de um rótulo de maiúsculas e minúsculas é testado em relação à expressão da instrução switch.
  • Em um operador is-pattern , o padrão no lado direito é testado em relação à expressão à esquerda.
  • Em uma expressão switch, o padrão de um switch_expression_arm é testado em relação à expressão no lado esquerdo da expressão switch.
  • Em contextos aninhados, o subpadrão é testado em relação a valores recuperados de propriedades, campos ou indexados de outros valores de entrada, dependendo do formulário de padrão.

O valor em relação ao qual um padrão é testado é chamado de valor de entrada do padrão.

11.2 Formas padrão

11.2.1 Generalidades

Um padrão pode ter uma das seguintes formas:

pattern
    : declaration_pattern
    | constant_pattern
    | var_pattern
    | positional_pattern
    | property_pattern
    | discard_pattern
    ;

Alguns padrõespodem resultar na declaração de uma variável local.

Cada formulário de padrão define o conjunto de tipos de valores de entrada aos quais o padrão pode ser aplicado. Um padrão P é aplicável a um tipo T se T estiver entre os tipos cujos valores o padrão pode corresponder. É um erro em tempo de compilação se um padrão P aparece em um programa para corresponder a um valor de entrada de padrão (§11.1) do tipo T se P não for aplicável a T.

Exemplo: O exemplo a seguir gera um erro em tempo de compilação porque o tipo de tempo de compilação de v é TextReader. Uma variável do tipo TextReader nunca pode ter um valor que seja compatível com a referência com string:

TextReader v = Console.In; // compile-time type of 'v' is 'TextReader'
if (v is string) // compile-time error
{
    // code assuming v is a string
}

No entanto, o seguinte não gera um erro em tempo de compilação porque o tipo de tempo de compilação é vobject. Uma variável do tipo object pode ter um valor compatível com a referência com string:

object v = Console.In;
if (v is string s)
{
    // code assuming v is a string
}

Exemplo final

Cada formulário de padrão define o conjunto de valores para os quais o padrão corresponde ao valor em tempo de execução.

A ordem de avaliação das operações e efeitos colaterais durante a correspondência de padrões (chamadas para , acessos à Deconstructpropriedade e invocações de métodos em System.ITuple) não é especificada.

11.2.2 Modelo da declaração

Um declaration_pattern é usado para testar se um valor tem um determinado tipo e, se o teste for bem-sucedido, para opcionalmente fornecer o valor em uma variável desse tipo.

declaration_pattern
    : type simple_designation
    ;
simple_designation
    : discard_designation
    | single_variable_designation
    ;
discard_designation
    : '_'
    ;
single_variable_designation
    : identifier
    ;

Um simple_designation com o token _ será considerado um discard_designation e não um single_variable_designation.

O tipo de tempo de execução do valor é testado em relação ao tipo no padrão usando as mesmas regras especificadas no operador is-type (§12.14.12.1). Se o teste for bem-sucedido, o padrão corresponderá a esse valor. É um erro em tempo de compilação se o tipo for um tipo de valor anulável (§8.3.12) ou um tipo de referência anulável (§8.9.3). Este formulário de padrão nunca corresponde a um null valor.

Nota: A expressão e is T is-type e o padrão e is T _ de declaração são equivalentes quando T não é um tipo anulável. Nota final

Dado um valor de entrada padrão (§11.1) e, se o simple_designation for discard_designation, ele denota um descarte (§9.2.9.2), e o valor de e não está vinculado a nada. (Embora uma variável declarada com o nome _ possa estar no escopo nesse ponto, essa variável nomeada não é vista neste contexto.) Caso contrário, se o simple_designation for single_variable_designation, é introduzida uma variável local (§9.2.9) do tipo dado nomeado pelo identificador dado. A essa variável local é atribuído o valor do valor de entrada do padrão quando o padrão corresponde ao valor.

Certas combinações do tipo estático do valor de entrada do padrão e do tipo dado são consideradas incompatíveis e resultam em um erro em tempo de compilação. Diz-se que um valor de tipo E estático é compatível com o tipo T se existir uma conversão de identidade, uma conversão de referência implícita ou explícita, uma conversão de boxe ou uma conversão de unboxing de E para T, ou se um ou ET for um tipo aberto (§8.4.3). Um padrão de declaração nomeando um tipo T é aplicável a.

Nota: O suporte para tipos abertos pode ser mais útil ao verificar tipos que podem ser struct ou tipos de classe, e o boxe deve ser evitado. Nota final

Exemplo: O padrão de declaração é útil para executar testes de tipo em tempo de execução de tipos de referência e substitui o idioma

var v = expr as Type;
if (v != null) { /* code using v */ }

com o ligeiramente mais conciso

if (expr is Type v) { /* code using v */ }

Exemplo final

Exemplo: O padrão de declaração pode ser usado para testar valores de tipos anuláveis: um valor de tipo Nullable<T> (ou um in a box) Tcorresponde a um padrão T2 id de tipo se o valor for não-nulo e T2 for T, ou algum tipo base ou interface de T. Por exemplo, no fragmento de código

int? x = 3;
if (x is int v) { /* code using v */ }

A condição da if instrução está true em tempo de execução e a variável v mantém o valor 3 do tipo int dentro do bloco. Após o bloco, a variável v está no escopo, mas não definitivamente atribuída. Exemplo final

11.2.3 Padrão constante

Um constant_pattern é usado para testar o valor de um valor de entrada padrão (§11.1) em relação ao valor constante dado.

constant_pattern
    : constant_expression
    ;

Um padrão P constante é aplicável a um tipo T se houver uma conversão implícita da expressão constante de P para o tipo T.

Para um padrão Pconstante , seu valor convertido é

  • se o tipo do valor de entrada do padrão for um tipo integral ou um tipo de enum, o valor constante do padrão convertido para esse tipo; caso contrário,
  • se o tipo do valor de entrada do padrão for a versão anulável de um tipo integral ou um tipo de enum, o valor constante do padrão convertido para seu tipo subjacente; caso contrário,
  • O valor do valor constante do padrão.

Dado um valor de entrada padrão e e um padrão P constante com valor convertido v,

  • se e tem tipo integral ou tipo enum, ou uma forma anulável de um destes, e v tem tipo integral, o padrão Pcorresponde ao valor e se o resultado da expressão e == v for true;
  • O padrão Pcorresponde ao valor e se object.Equals(e, v) retorna true.

Exemplo: A switch instrução no método a seguir usa cinco padrões constantes em seus rótulos de maiúsculas e minúsculas.

static decimal GetGroupTicketPrice(int visitorCount)
{
    switch (visitorCount) 
    {
        case 1: return 12.0m;
        case 2: return 20.0m;
        case 3: return 27.0m;
        case 4: return 32.0m;
        case 0: return 0.0m;
        default: throw new ArgumentException(...);
    }
}

Exemplo final

11.2.4 Padrão Var

Um var_patterncorresponde a todos os valores. Ou seja, uma operação de correspondência de padrões com um var_pattern sempre é bem-sucedida.

Um var_pattern é aplicável a todos os tipos.

var_pattern
    : 'var' designation
    ;
designation
    : simple_designation
    | tuple_designation
    ;
tuple_designation
    : '(' designations? ')'
    ;
designations
    : designation (',' designation)*
    ;

Dado um valor de entrada padrão (§11.1) e, se a designação for discard_designation, denota uma eliminação (§9.2.9.2), e o valor de e não está vinculado a nada. (Embora uma variável declarada com esse nome possa estar no escopo nesse ponto, essa variável nomeada não é vista neste contexto.) Caso contrário, se a designação for single_variable_designation, no tempo de execução o valor de e é vinculado a uma variável local recém-introduzida (§9.2.9) desse nome cujo tipo é o tipo estático de e, e o valor de entrada do padrão é atribuído a essa variável local.

É um erro se o nome var se ligar a um tipo onde um var_pattern é usado.

Se a designação for um tuple_designation, o padrão é equivalente a um positional_pattern (§11.2.5) da designação do formulário(var, ... ) quando as denominaçõess são as que se encontram no tuple_designation. Por exemplo, o padrão var (x, (y, z)) é equivalente a (var x, (var y, var z)).

11.2.5 Padrão de posição

Um positional_pattern verifica se o valor de entrada não nullé , invoca um método apropriado Deconstruct (§12.7) e executa uma correspondência de padrão adicional nos valores resultantes. Ele também suporta uma sintaxe de padrão semelhante a uma tupla (sem que o tipo seja fornecido) quando o tipo do valor de entrada é o mesmo que o tipo que contém Deconstruct, ou se o tipo do valor de entrada é um tipo de tupla, ou se o tipo do valor de entrada é object ou System.ITuple e o tipo de tempo de execução da expressão implementa System.ITuple.

positional_pattern
    : type? '(' subpatterns? ')' property_subpattern? simple_designation?
    ;
subpatterns
    : subpattern (',' subpattern)*
    ;
subpattern
    : pattern
    | identifier ':' pattern
    ;

Dada uma correspondência de um valor de entrada com ossubpadrões) do tipo( padrão, um método é selecionado pesquisando em tipo por declarações acessíveis de e selecionando uma entre elas usando as mesmas regras que para a declaração de Deconstruct desconstrução. É um erro se um positional_pattern omite o tipo, tem um único subpadrão sem um identificador, não tem property_subpattern e não tem simple_designation. Isso desambigua entre um constant_pattern que está entre parênteses e um positional_pattern. A fim de extrair os valores para corresponder aos padrões na lista,

  • Se o tipo for omitido e o tipo da expressão de entrada for um tipo de tupla, então o número de subpadrões deve ser o mesmo que a cardinalidade da tupla. Cada elemento de tupla é combinado com o subpadrão correspondente, e a correspondência é bem-sucedida se todos eles forem bem-sucedidos. Se qualquer subpadrão tiver um identificador, este deve nomear um elemento de tupla na posição correspondente no tipo de tupla.
  • Caso contrário, se existir um adequado Deconstruct como um membro do tipo, é um erro em tempo de compilação se o tipo do valor de entrada não for compatível com o padrão. No tempo de execução, o valor de entrada é testado em relação ao tipo. Se isso falhar, a correspondência do padrão posicional falhará. Se for bem-sucedido, o valor de entrada será convertido para esse tipo e Deconstruct invocado com novas variáveis geradas pelo compilador para receber os parâmetros de saída. Cada valor recebido é correspondido com o subpadrão correspondente, e a correspondência será bem-sucedida se todos eles forem bem-sucedidos. Se qualquer subpadrão tiver um identificador, esse deve nomear um parâmetro na posição correspondente de Deconstruct.
  • Caso contrário, se o tipo for omitido, e o valor de entrada for do tipo object ou de algum tipo que possa ser convertido por System.ITuple uma conversão de referência implícita, e nenhum identificador aparecer entre os subpadrões, a correspondência usará System.ITuple.
  • Caso contrário, o padrão é um erro em tempo de compilação.

A ordem na qual os subpadrões são correspondidos no tempo de execução não é especificada, e uma correspondência com falha pode não tentar corresponder a todos os subpadrões.

Exemplo: Aqui, desconstruímos um resultado de expressão e fazemos a correspondência entre os valores resultantes e os padrões aninhados correspondentes:

static string Classify(Point point) => point switch
{
    (0, 0) => "Origin",
    (1, 0) => "positive X basis end",
    (0, 1) => "positive Y basis end",
    _ => "Just a point",
};

public readonly struct Point
{
    public int X { get; }
    public int Y { get; }
    public Point(int x, int y) => (X, Y) = (x, y);
    public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}

Exemplo final

Exemplo: Os nomes dos elementos da tupla e dos parâmetros Deconstruct podem ser usados em um padrão posicional, da seguinte forma:

var numbers = new List<int> { 10, 20, 30 };
if (SumAndCount(numbers) is (Sum: var sum, Count: var count))
{
    Console.WriteLine($"Sum of [{string.Join(" ", numbers)}] is {sum}");
}

static (double Sum, int Count) SumAndCount(IEnumerable<int> numbers)
{
    int sum = 0;
    int count = 0;
    foreach (int number in numbers)
    {
        sum += number;
        count++;
    }
    return (sum, count);
}

A produção produzida é

Sum of [10 20 30] is 60

Exemplo final

11.2.6 Padrão de propriedade

Um property_pattern verifica se o valor de entrada não nullé e corresponde recursivamente aos valores extraídos pelo uso de propriedades ou campos acessíveis.

property_pattern
    : type? property_subpattern simple_designation?
    ;
property_subpattern
    : '{' '}'
    | '{' subpatterns ','? '}'
    ;

É um erro se qualquer subpadrão de um property_pattern não contiver um identificador.

É um erro em tempo de compilação se o tipo for um tipo de valor anulável (§8.3.12) ou um tipo de referência anulável (§8.9.3).

Nota: Um padrão de verificação nula cai fora de um padrão de propriedade trivial. Para verificar se a cadeia de caracteres s não é nula, pode-se escrever qualquer um dos seguintes formulários:

#nullable enable
string s = "abc";
if (s is object o) ...  // o is of type object
if (s is string x1) ... // x1 is of type string
if (s is {} x2) ...     // x2 is of type string
if (s is {}) ...

Nota final Dada uma correspondência de uma expressão e com o tipo{de padrão property_pattern_list}, é um erro em tempo de compilação se a expressão e não for compatível com o tipo T designado por tipo. Se o tipo estiver ausente, o tipo é assumido como sendo o tipo estático de e. Cada um dos identificadores que figuram no lado esquerdo do seu property_pattern_list designa uma propriedade ou campo de T acessível e legível. Se o simple_designation do property_pattern estiver presente, ele declara uma variável padrão do tipo T.

No tempo de execução, a expressão é testada em relação a T. Se isso falhar, a correspondência do padrão de propriedade falhará e o resultado será false. Se for bem-sucedido, cada campo ou propriedade property_subpattern será lido e seu valor correspondido ao padrão correspondente. O resultado de toda a partida é false apenas se o resultado de qualquer um destes for false. A ordem na qual os subpadrões são correspondidos não é especificada e uma correspondência com falha pode não testar todos os subpadrões em tempo de execução. Se a correspondência for bem-sucedida e a simple_designation do property_pattern for um single_variable_designation, a variável declarada receberá o valor correspondente.

O property_pattern pode ser usado para correspondência de padrões com tipos anônimos.

Exemplo:

var o = ...;
if (o is string { Length: 5 } s) ...

Exemplo final

Exemplo: Uma verificação de tipo em tempo de execução e uma declaração de variável podem ser adicionadas a um padrão de propriedade, da seguinte forma:

Console.WriteLine(TakeFive("Hello, world!"));  // output: Hello
Console.WriteLine(TakeFive("Hi!"));            // output: Hi!
Console.WriteLine(TakeFive(new[] { '1', '2', '3', '4', '5', '6', '7' }));  // output: 12345
Console.WriteLine(TakeFive(new[] { 'a', 'b', 'c' }));  // output: abc

static string TakeFive(object input) => input switch
{
    string { Length: >= 5 } s => s.Substring(0, 5),
    string s => s,
    ICollection<char> { Count: >= 5 } symbols => new string(symbols.Take(5).ToArray()),
    ICollection<char> symbols => new string(symbols.ToArray()),
    null => throw new ArgumentNullException(nameof(input)),
    _ => throw new ArgumentException("Not supported input type."),
};

A produção produzida é

Hello
Hi!
12345
abc

Exemplo final

11.2.7 Padrão de eliminação

Cada expressão corresponde ao padrão de descarte, o que resulta no valor da expressão que está sendo descartada.

discard_pattern
    : '_'
    ;

É um erro em tempo de compilação usar um padrão de descarte em uma relational_expression dopadrão de relational_expressionis de forma ou como o padrão de um switch_label.

Nota: Nesses casos, para corresponder a qualquer expressão, use um var_pattern com um descarte var _. Nota final

Exemplo:

Console.WriteLine(GetDiscountInPercent(DayOfWeek.Friday));
Console.WriteLine(GetDiscountInPercent(null));
Console.WriteLine(GetDiscountInPercent((DayOfWeek)10));

static decimal GetDiscountInPercent(DayOfWeek? dayOfWeek) => dayOfWeek switch
{
    DayOfWeek.Monday => 0.5m,
    DayOfWeek.Tuesday => 12.5m,
    DayOfWeek.Wednesday => 7.5m,
    DayOfWeek.Thursday => 12.5m,
    DayOfWeek.Friday => 5.0m,
    DayOfWeek.Saturday => 2.5m,
    DayOfWeek.Sunday => 2.0m,
    _ => 0.0m,
};

A produção produzida é

5.0
0.0
0.0

Aqui, um padrão de descarte é usado para manipular null e qualquer valor inteiro que não tenha o membro correspondente da DayOfWeek enumeração. Isso garante que a switch expressão manipule todos os valores de entrada possíveis. Exemplo final

11.3 Subsunção do padrão

Em uma instrução switch, é um erro se o padrão de um caso é subsumido pelo conjunto anterior de casos não protegidos (§13.8.3). Informalmente, isso significa que qualquer valor de entrada teria sido correspondido por um dos casos anteriores. As regras a seguir definem quando um conjunto de padrões subsume um determinado padrão:

Um padrão P corresponderia

Um conjunto de padrões Qsubsume um padrão P se qualquer uma das seguintes condições se mantiver:

  • Pé um padrão constante e qualquer um dos padrões no conjunto Q corresponderia Pao valor convertido do
  • Pé um padrão var e o conjunto de padrões Q é exaustivo
  • P é um padrão de declaração com tipo T e o conjunto de padrões Q é exaustivo para o tipo T (§11.4).

11.4 Exaustividade do padrão

Informalmente, um conjunto de padrões é exaustivo para um tipo se, para cada valor possível desse tipo diferente de nulo, algum padrão no conjunto for aplicável. As regras a seguir definem quando um conjunto de padrões é exaustivo para um tipo:

Um conjunto de padrões Q é exaustivo para um tipo T se qualquer uma das seguintes condições se mantiver:

  1. T é um tipo integral ou enum, ou uma versão anulável de um desses, e para cada valor possível de T's tipo subjacente não anulável, algum padrão em Q corresponderia a esse valor;
  2. Algum padrão em Q é um padrão var;
  3. Alguns padrões em Q é um padrão de declaração para o tipo D, e há uma conversão de identidade, uma conversão de referência implícita ou uma conversão de boxe de T para D.

Exemplo:

static void M(byte b)
{
    switch (b) {
        case 0: case 1: case 2: ... // handle every specific value of byte
            break;
        // error: the pattern 'byte other' is subsumed by the (exhaustive)
        // previous cases
        case byte other: 
            break;
    }
}

Exemplo final