Correspondência de padrões recursiva
Observação
Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui mudanças de especificação propostas, juntamente com as informações necessárias durante o design e desenvolvimento do recurso. Estes artigos são publicados até que as alterações de especificações propostas sejam finalizadas e incorporadas na especificação ECMA atual.
Pode haver algumas discrepâncias entre a especificação do recurso e a implementação concluída. Essas diferenças são capturadas nas notas pertinentes da Language Design Meeting (LDM).
Você pode saber mais sobre o processo de adoção de especificações de recursos no padrão de linguagem C# no artigo sobre as especificações .
Questão campeã: 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 sensação da linguagem subjacente. Os elementos dessa abordagem são inspirados por recursos relacionados nas linguagens de programação F# e Scala.
Projeto detalhado
É expressão
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 é adicional aos formulários existentes na especificação C#. É um erro em tempo de compilação se o relational_expression não designar um valor ou não tiver um tipo à esquerda do token is
.
Cada identificador do padrão introduz uma nova variável local que é definitivamente atribuída depois que o operador is
é true
(ou seja, definitivamente atribuído quando verdadeiro).
Nota: Existe tecnicamente uma ambiguidade entre tipo num
is-expression
e constant_pattern, qualquer um dos quais pode ser uma interpretação válida de um identificador qualificado. Tentamos vinculá-lo como um tipo para compatibilidade com versões anteriores da língua; só se isso falhar é que a resolvemos como fazemos com uma expressão noutros contextos, até à primeira coisa encontrada (que deve ser uma constante ou um tipo). Esta ambiguidade só está presente no lado direito de uma expressãois
.
Padrões
Os padrões são usados no operador is_pattern, em um switch_statemente em um switch_expression para expressar a forma dos dados com os quais os dados recebidos (que chamamos de valor de entrada) devem ser comparados. Os padrões podem ser recursivos para que partes dos dados possam ser comparadas 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 declaration_pattern verifica se uma expressão é de um determinado tipo e, caso o teste seja bem-sucedido, converte-a para esse tipo. Isto pode introduzir uma variável local do tipo dado denominada pelo identificador fornecido, se a designação for uma 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 tempo de execução dessa expressão é que ela testa o tipo de tempo de execução do operando relational_expression esquerdo em relação ao tipo no padrão. Se for desse tipo de tempo de execução (ou algum subtipo) e não null
, o resultado do is operator
é true
.
Certas combinações do tipo estático do lado esquerdo e do tipo dado são consideradas incompatíveis e resultam em erro em tempo de compilação. Um valor de tipo estático E
é dito ser compatível com um tipo T
se existir uma conversão de identidade, uma conversão de referência implícita, uma conversão de boxe, uma conversão de referência explícita ou uma conversão de unboxing de E
para T
, ou se um desses tipos for um tipo aberto. É um erro em tempo de compilação se uma entrada do tipo E
não for compatível com o tipo em um padrão de tipo com o qual ele é correspondido.
O padrão de tipo é ú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
De forma ligeiramente mais concisa
if (expr is Type v) { // code using v
É um erro se tipo for um tipo de valor anulável.
O padrão de tipo pode ser usado para testar valores de tipos anuláveis: um valor de tipo Nullable<T>
(ou um T
encaixotado) corresponde a um padrão de tipo T2 id
se o valor for não-nulo e o tipo de 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 instrução if
é 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.
Padrão constante
constant_pattern
: constant_expression
;
Um padrão 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 é implicitamente convertida para o tipo da expressão correspondente; Se o tipo do valor de entrada não for compatível com o padrão com o tipo da expressão constante, a operação de correspondência de padrão 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 não pode invocar um 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 um simple_designation, uma expressão e corresponde ao padrão. Em outras palavras, um ajuste a um padrão var é sempre bem-sucedido com uma designação_simples . Se o simple_designation for um single_variable_designation, o valor de e está 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 uma designação de tuplo, então o padrão é equivalente a um padrão posicional da forma (var
designação, ... )
em que as designações são as que se encontram na designação de tuplo. Por exemplo, o padrão var (x, (y, z))
é equivalente a (var x, (var y, var z))
.
É um erro se o nome var
se liga a um tipo.
Padrão de descarte
discard_pattern
: '_'
;
Uma expressão e corresponde ao padrão _
sempre. Em outras palavras, cada expressão corresponde ao padrão de eliminação.
Um padrão de descarte não pode ser usado como o padrão de um is_pattern_expression.
Padrão posicional
Um padrão posicional verifica se o valor de entrada não está null
, invoca um método Deconstruct
apropriado 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 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, consideraremos que é o tipo estático do valor de entrada.
Dada uma correspondência de um valor de entrada com o padrão tipo(
subpattern_list)
, um método é selecionado ao pesquisar em tipo por declarações acessíveis de Deconstruct
e ao selecionar uma entre elas, usando as mesmas regras aplicadas à declaração de desconstrução.
É considerado um erro se um positional_pattern omite o tipo, se tiver um único subpadrão sem um identificador , se não tiver property_subpattern ou se não tiver 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 foi omitido e o tipo do valor de entrada é uma tupla, então o número de subpadrões deve corresponder à cardinalidade da tupla. Cada elemento de tupla é combinado 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 isso deve nomear um elemento de tupla na posição correspondente no tipo de tupla.
- Caso contrário, se existir um
Deconstruct
adequado como membro de tipo, é um erro em tempo de compilação se o tipo do valor de entrada não for compatível com padrão com tipo. No tempo de execução, o valor de entrada é testado contra o 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 eDeconstruct
será invocado com novas variáveis geradas pelo compilador para receber os parâmetrosout
. Cada valor recebido é 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 , isso deverá nomear um parâmetro na posição correspondente deDeconstruct
. - Caso contrário, se tipo foi 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 aparece entre os subpadrões, então nós correspondemos usandoITuple
. - Caso contrário, o padrão é um erro em tempo de compilação.
A ordem em que os subpadrões são correspondidos no tempo de execução não é especificada, e falhar numa correspondência pode não tentar corresponder a 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 da segunda forma, que tem um identificador ). Uma vírgula à direita após o último subpadrão é opcional.
Observe que 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, você pode escrever qualquer um dos seguintes formulários
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 uma correspondência de uma expressão e ao padrão tipo{
property_pattern_list}
, é um erro em tempo de compilação se a expressão e não for compatível com o padrão com o tipo T designado por tipo. Se o tipo estiver ausente, tomamos como sendo o tipo estático de e. Se o identificador estiver presente, ele declarará uma variável de padrão do tipo do tipo. Cada um dos identificadores que aparecem no lado esquerdo do seu property_pattern_list deve designar uma propriedade ou campo acessível e legível de T. Se o simple_designation do property_pattern estiver presente, ele define uma variável padrão do tipo T.
Durante a execução, a expressão é testada contra T. Se isso falhar, a correspondência de padrão de propriedade falha e o resultado é false
. Se for bem-sucedido, então cada campo ou propriedade property_subpattern é lido e seu valor é comparado com o padrão correspondente. O resultado de toda a partida é false
apenas se o resultado de qualquer um deles for false
. A ordem na qual os subpadrões são correspondidos não é especificada e uma correspondência falhada pode não abranger todos os subpadrões em tempo de execução. Se a correspondência for bem-sucedida e a simple_designation do property_pattern for uma single_variable_designation, ela define uma variável do tipo T à qual é atribuído o valor associado.
Nota: 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)
Alternar expressão
Um switch_expression é adicionado para dar suporte à 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
;
O switch_expression não é permitido como expression_statement.
Estamos a estudar a possibilidade de flexibilizar esta situação numa futura revisão.
O tipo do switch_expression é o melhor tipo comum (§12.6.3.15) das expressões que aparecem à direita dos tokens de =>
do switch_expression_arms se tal tipo existir e a expressão em cada braço da expressão switch puder ser implicitamente convertida para esse 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
.
É um erro se algum padrão de switch_expression_armnão puder afetar o resultado, porque algum padrão e guarda anteriores sempre corresponderão.
Diz-se que uma expressão switch é exaustiva se algum braço da expressão switch manipular todos os valores de sua entrada. O compilador deve emitir um aviso se uma expressão do interruptor não for exaustiva.
No tempo de execução, o resultado do switch_expression é o valor da expressão do primeiro switch_expression_arm para o qual a expressão no lado esquerdo do switch_expression corresponde ao padrão do switch_expression_arme para o qual o case_guard do switch_expression_arm, se presente, avalia a true
. Se não houver esse switch_expression_arm, o switch_expression lança uma instância da exceção System.Runtime.CompilerServices.SwitchExpressionException
.
Parênteses opcionais ao aplicar a uma tupla literal
Para ativar uma tupla literal usando o switch_statement, tem que escrever o que parecem ser parênteses redundantes.
switch ((a, b))
{
Permitir
switch (a, b)
{
Os parênteses da instrução switch são opcionais quando a expressão que está sendo ligada é um literal de tupla.
Ordem de avaliação na correspondência de padrões
Dar ao compilador flexibilidade 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 imposto) seria que propriedades acessadas num padrão, e os métodos Deconstruct, tenham de ser "puros" (sem efeitos secundários, idempotentes, etc). Isso não significa que adicionaríamos pureza como um conceito de linguagem, apenas que permitiríamos ao compilador flexibilidade na reordenação das operações.
Resolução 2018-04-04 LDM: confirmado: o compilador tem permissão para reordenar chamadas para Deconstruct
, acessos a propriedades e invocações de métodos em ITuple
, podendo assumir que os valores retornados são os mesmos em várias chamadas. O compilador não deve invocar funções que não possam afetar o resultado, e teremos muito cuidado antes de fazer quaisquer alterações na ordem de avaliação gerada pelo compilador no futuro.
Algumas otimizações possíveis
A compilação de correspondência de padrões pode tirar proveito de partes comuns de padrões. Por exemplo, se o teste de tipo de nível superior de dois padrões sucessivos em um 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 strings, o compilador pode gerar o mesmo tipo de código que gera para uma instrução switch em versões anteriores da linguagem.
Para saber mais sobre estes tipos de otimizações, consulte [Scott e Ramsey (2000)].
C# feature specifications