Correspondência de padrão recursivo
Observação
Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ela inclui alterações de especificação propostas, juntamente com as informações necessárias durante o design e o desenvolvimento do recurso. Esses artigos são publicados até que as alterações de especificação propostas sejam finalizadas e incorporadas na especificação ECMA atual.
Pode haver algumas divergências entre a especificação do recurso e a implementação concluída. Essas diferenças são capturadas nas notas pertinentes da reunião de design de idioma (LDM).
Você pode aprender mais sobre o processo de adoção de especificações de características no padrão de linguagem C# no artigo sobre as especificações .
Problema do especialista: https://github.com/dotnet/csharplang/issues/45
Resumo
As extensões de correspondência de padrões para C# permitem muitos dos benefícios dos tipos de dados algébricos e da correspondência de padrões de linguagens funcionais, mas de uma forma que se integra suavemente com a natureza da linguagem subjacente. Elementos desta abordagem são inspirados em recursos relacionados nas linguagens de programação F# e Scala.
Projeto detalhado
Is Expression
O operador is
é estendido para testar uma expressão em relação a um padrão.
relational_expression
: is_pattern_expression
;
is_pattern_expression
: relational_expression 'is' pattern
;
Essa forma de relational_expression é um complemento às formas existentes na especificação C#. Será um erro em tempo de compilação se relational_expression à esquerda do token is
não designar um valor ou não tiver um tipo.
Cada identificador do padrão introduz uma nova variável local definitivamente atribuída depois que o operador is
é true
(ou seja, definitivamente atribuído quando verdadeiro).
Observação: tecnicamente, há uma ambiguidade entre tipo em um
is-expression
e constant_pattern, em que qualquer um deles pode ser uma interpretação válida de um identificador qualificado. Tentamos associá-lo como um tipo para compatibilidade com versões anteriores do idioma e, somente se isso falhar, resolvemos da mesma forma que fazemos com uma expressão em outros contextos, para o primeiro elemento encontrado (que precisa ser uma constante ou um tipo). Essa ambiguidade só está presente no lado direito de uma expressãois
.
Padrões
Os padrões são usados no operador is_pattern, em switch_statement e em switch_expression, para expressar a forma de dados em relação aos quais os dados de entrada (que chamamos de valor de entrada) devem ser comparados. Os padrões podem ser recursivos para que partes dos dados possam ser correspondidas com subpadrões.
pattern
: declaration_pattern
| constant_pattern
| var_pattern
| positional_pattern
| property_pattern
| discard_pattern
;
declaration_pattern
: type simple_designation
;
constant_pattern
: constant_expression
;
var_pattern
: 'var' designation
;
positional_pattern
: type? '(' subpatterns? ')' property_subpattern? simple_designation?
;
subpatterns
: subpattern
| subpattern ',' subpatterns
;
subpattern
: pattern
| identifier ':' pattern
;
property_subpattern
: '{' '}'
| '{' subpatterns ','? '}'
;
property_pattern
: type? property_subpattern simple_designation?
;
simple_designation
: single_variable_designation
| discard_designation
;
discard_pattern
: '_'
;
Padrão de declaração
declaration_pattern
: type simple_designation
;
O padrão de declaração testa se uma expressão é de um determinado tipo e a converte para esse tipo caso o teste seja bem-sucedido. Isso pode introduzir uma variável local do tipo fornecido, nomeada pelo identificador fornecido, se a designação for single_variable_designation. Essa variável local é definitivamente atribuída quando o resultado da operação de correspondência de padrões é true
.
A semântica de runtime dessa expressão é que ela testa o tipo de runtime do operando relational_expression à esquerda em relação ao tipo no padrão. Se for desse tipo de runtime (ou algum subtipo) e não null
, o resultado de is operator
será true
.
Determinadas combinações de tipo estático do lado esquerdo e do tipo fornecido são consideradas incompatíveis e resultam em erro de tempo de compilação. Um valor de tipo estático E
é considerado compatível com o padrão com um tipo T
quando há conversão de identidade, uma conversão de referência implícita, uma conversão de compactação, uma conversão de referência explícita ou uma conversão de descompactação de E
para T
, ou se um desses tipos for um tipo aberto. É um erro de tempo de compilação se uma entrada do tipo E
não for compatível com o padrão com o tipo em um padrão de tipo com o qual ele corresponde.
O padrão de tipo é útil para executar testes de tipo de runtime de tipos de referência e substitui a expressão idiomática
var v = expr as Type;
if (v != null) { // code using v
Com um pouco mais de concisão
if (expr is Type v) { // code using v
Será um erro se type for um tipo de valor anulável.
O padrão de tipo pode ser usado para testar valores de tipos nullable: um valor do tipo Nullable<T>
(ou T
compactado) corresponde a um padrão de tipo T2 id
quando o valor é não nulo e o tipo de T2
é T
, ou um 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 instrução if
é true
durante o 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.
Padrão de constante
constant_pattern
: constant_expression
;
Um padrão de constante testa o valor de uma expressão em relação a um valor constante. A constante pode ser qualquer expressão constante, como um literal, o nome de uma variável const
declarada ou uma constante de enumeração. Quando o valor de entrada não é um tipo aberto, a expressão constante é convertida implicitamente no tipo da expressão correspondente; se o tipo do valor de entrada não for compatível com padrões com o tipo da expressão constante, a operação de correspondência de padrões será um erro.
O padrão c é considerado correspondente ao valor de entrada convertido e se object.Equals(c, e)
retornasse true
.
Esperamos ver e is null
como a maneira mais comum de testar null
em código recém-escrito, pois ele não pode invocar operator==
definido pelo usuário.
Padrão var
var_pattern
: 'var' designation
;
designation
: simple_designation
| tuple_designation
;
simple_designation
: single_variable_designation
| discard_designation
;
single_variable_designation
: identifier
;
discard_designation
: _
;
tuple_designation
: '(' designations? ')'
;
designations
: designation
| designations ',' designation
;
Se a designação for simple_designation, uma expressão e corresponderá ao padrão. Em outras palavras, uma correspondência com um padrão var sempre é bem-sucedida com simple_designation. Se simple_designation for single_variable_designation, o valor de e será vinculado a uma variável local recém-introduzida. O tipo da variável local é o tipo estático de e.
Se a designação for tuple_designation, então o padrão será equivalente a um positional_pattern da forma (var
designação, ... )
em que as designações serão aquelas encontradas dentro de tuple_designation. Por exemplo, o padrão var (x, (y, z))
é equivalente a (var x, (var y, var z))
.
Será um erro se o nome var
se associar a um tipo.
Padrão de descarte
discard_pattern
: '_'
;
Uma expressão e sempre corresponde ao padrão _
. Em outras palavras, cada expressão corresponde ao padrão de descarte.
Um padrão de descarte não pode ser usado como o padrão de is_pattern_expression.
Padrão posicional
Um padrão posicional verifica se o valor de entrada não é null
, invoca um método apropriado de Deconstruct
e executa uma correspondência adicional de padrões nos valores resultantes. Ele também dá suporte a uma sintaxe de padrão semelhante a tupla (sem fornecer o tipo) 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 ITuple
e o tipo de tempo de execução da expressão implementa ITuple
.
positional_pattern
: type? '(' subpatterns? ')' property_subpattern? simple_designation?
;
subpatterns
: subpattern
| subpattern ',' subpatterns
;
subpattern
: pattern
| identifier ':' pattern
;
Se o tipo for omitido, consideramos que é o tipo estático do valor de entrada.
Considerando a correspondência de um valor de entrada para o padrão tipo(
subpattern_list)
, um método é selecionado buscando em tipo declarações acessíveis de Deconstruct
e selecionando uma entre elas com as mesmas regras usadas para a declaração de desconstrução.
Será erro se positional_pattern omitir o tipo, tiver um único subpadrão sem um identificador, não tiver property_subpattern e não tiver simple_designation. Isso desambigua entre um padrão constante que está entre parênteses e um padrão posicional .
Para extrair os valores a serem correspondentes aos padrões na lista,
- Se o tipo seja omitido e o tipo do valor de entrada for um tipo de tupla, o número de subpadrões deve ser igual à cardinalidade da tupla. Cada elemento tuple é comparado 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, então ele deverá nomear um elemento tuple na posição correspondente no tipo de tupla.
- Caso contrário, se existir um
Deconstruct
adequado como membro do tipo , será um erro de tempo de compilação se o tipo do valor de entrada não for compatível com o padrão de tipo. Em runtime, o valor de entrada é testado em tipo. Se isso falhar, a correspondência de padrão posicional falhará. Se funcionar, o valor de entrada será convertido para esse tipo eDeconstruct
será invocado com variáveis geradas pelo compilador para receber os parâmetros deout
. Cada valor recebido é comparado ao subpadrão correspondente , e a correspondência é bem-sucedida se todas as correspondências individuais forem bem-sucedidas. Se qualquer subpadrão tiver um identificador, ele deverá nomear um parâmetro na posição correspondente deDeconstruct
. - Caso contrário, se tipo for omitido e o valor de entrada é do tipo
object
ouITuple
ou algum tipo que pode ser convertido emITuple
por uma conversão de referência implícita, e nenhum identificador de aparece entre os subpadrões, então fazemos a correspondência usandoITuple
. - Caso contrário, o padrão é um erro de tempo de compilação.
A ordem em que a correspondência de subpadrões acontece em runtime não é especificada, e uma correspondência malsucedida pode não tentar abranger todos os subpadrões.
Exemplo
Este exemplo usa muitos dos recursos descritos nesta especificação
var newState = (GetState(), action, hasKey) switch {
(DoorState.Closed, Action.Open, _) => DoorState.Opened,
(DoorState.Opened, Action.Close, _) => DoorState.Closed,
(DoorState.Closed, Action.Lock, true) => DoorState.Locked,
(DoorState.Locked, Action.Unlock, true) => DoorState.Closed,
(var state, _, _) => state };
Padrão de propriedade
Um padrão de propriedade 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 (deve ser do segundo tipo, que possui um identificador ). Uma vírgula à direita após o último subpadrão é opcional.
Observe que um padrão de verificação nula cai de um padrão de propriedade comum. Para verificar se a cadeia de caracteres s
não é nula, escreva qualquer um dos formatos a seguir
if (s is object o) ... // o is of type object
if (s is string x) ... // x is of type string
if (s is {} x) ... // x is of type string
if (s is {}) ...
Dada a correspondência de uma expressão e com o padrão tipo{
property_pattern_list}
, será um erro de tempo de compilação se a expressão e não for compatível com padrões com o tipo T designado por tipo. Se o tipo estiver ausente, consideramos que seja o tipo estático de e. Se o identificador está presente, ele declara uma variável de padrão do tipo type. Cada um dos identificadores que aparecem no lado esquerdo de property_pattern_list deve designar uma propriedade legível acessível ou um campo de T. Se simple_designation de property_pattern estiver presente, ele definirá uma variável de padrão do tipo T.
Em tempo de execução, a expressão é testada em T. Se isso falhar, a correspondência do padrão de propriedades falhará e o resultado será false
. Se for bem-sucedido, cada campo ou propriedade property_subpattern será lido e seu valor corresponderá ao padrão correspondente. O resultado da correspondência inteira será false
somente se o resultado de qualquer uma for false
. A ordem em que a correspondência de subpadrões acontece não é especificada, e uma correspondência malsucedida pode não tentar abranger todos os subpadrões em runtime. Se a correspondência for bem-sucedida e simple_designation de property_pattern for single_variable_designation, ela definirá uma variável do tipo T que recebe o valor correspondente.
Observação: o padrão de propriedade pode ser usado para correspondência de padrões com tipos anônimos.
Exemplo
if (o is string { Length: 5 } s)
Expressão switch
Um switch_expression é adicionado para auxiliar uma semântica semelhante a switch
para um contexto de expressão.
A sintaxe da linguagem C# é aumentada com as seguintes produções sintáticas:
multiplicative_expression
: switch_expression
| multiplicative_expression '*' switch_expression
| multiplicative_expression '/' switch_expression
| multiplicative_expression '%' switch_expression
;
switch_expression
: range_expression 'switch' '{' '}'
| range_expression 'switch' '{' switch_expression_arms ','? '}'
;
switch_expression_arms
: switch_expression_arm
| switch_expression_arms ',' switch_expression_arm
;
switch_expression_arm
: pattern case_guard? '=>' expression
;
case_guard
: 'when' null_coalescing_expression
;
switch_expression não é permitido como um expression_statement.
Estamos considerando flexibilizar isso em uma revisão futura.
O tipo de switch_expression é o melhor tipo comum (§12.6.3.15) das expressões que aparecem à direita dos =>
tokens de switch_expression_arms se esse tipo existir e a expressão em cada braço da expressão switch puder ser convertida implicitamente nesse tipo. Além disso, adicionamos uma nova conversão de expressão switch, que é uma conversão implícita predefinida de uma expressão switch para cada tipo T
para o qual existe uma conversão implícita da expressão de cada braço para T
.
Será erro se algum padrão de switch_expression_arm não puder afetar o resultado porque um padrão e guarda anteriores sempre correspondem.
Uma expressão switch é considerada exaustiva se algum braço da expressão switch manipula cada valor de sua entrada. O compilador produzirá um aviso se uma expressão switch não for exaustiva.
Em runtime, o resultado de switch_expression é o valor da expressão do primeiro switch_expression_arm para o qual a expressão no lado esquerdo de switch_expression corresponde ao padrão de switch_expression_arm e para o qual case_guard de switch_expression_arm, se estiver presente, será avaliado como true
. Se não houver switch_expression_arm, o switch_expression lançará a exceção System.Runtime.CompilerServices.SwitchExpressionException
.
Parênteses opcionais ao alternar com tupla literal
Para ativar uma tupla literal usando switch_statement, você precisa escrever parênteses redundantes
switch ((a, b))
{
Para permitir
switch (a, b)
{
os parênteses da instrução switch são opcionais quando a expressão que está sendo ativada é uma tupla literal.
Ordem de avaliação na correspondência de padrões
Dar flexibilidade ao compilador na reordenação das operações executadas durante a correspondência de padrões pode permitir flexibilidade que pode ser usada para melhorar a eficiência da correspondência de padrões. O requisito (não obrigatório) seria que as propriedades acessadas em um padrão, e os métodos Deconstruct, devem ser "puros" (sem efeito colateral, idempotentes, etc). Isso não significa que adicionaríamos a puridade como um conceito de linguagem, apenas que permitiríamos a flexibilidade do compilador nas operações de reordenação.
Resolução 2018-04-04 LDM: Confirmado: o compilador tem permissão para a reordenação de chamadas para Deconstruct
, acessos de propriedade e invocações de métodos em ITuple
, e pode assumir que os valores retornados são os mesmos de várias chamadas. O compilador não deve invocar funções que não podem afetar o resultado, e teremos muito cuidado antes de fazer qualquer alteração na ordem de avaliação gerada pelo compilador no futuro.
Algumas otimizações possíveis
A compilação da correspondência de padrões pode aproveitar partes comuns de padrões. Por exemplo, se o teste de tipo de nível superior de dois padrões sucessivos em switch_statement for do mesmo tipo, o código gerado poderá ignorar o teste de tipo para o segundo padrão.
Quando alguns dos padrões são inteiros ou cadeias de caracteres, o compilador pode gerar o mesmo tipo de código gerado para switch-statement em versões anteriores da linguagem.
Para obter mais informações sobre esses tipos de otimizações, consulte [Scott e Ramsey (2000)].
C# feature specifications