Alterações de correspondência de padrões para C# 9.0
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 .
Estamos considerando um pequeno punhado de melhorias na correspondência de padrões para C# 9.0 que têm sinergia natural e funcionam bem para resolver vários problemas comuns de programação:
- https://github.com/dotnet/csharplang/issues/2925 Padrões de tipo
- https://github.com/dotnet/csharplang/issues/1350 Padrões entre parênteses para impor ou enfatizar a precedência dos novos combinadores
-
https://github.com/dotnet/csharplang/issues/1350 Padrões conjuntivos de
and
que requerem os dois padrões diferentes para corresponder; -
https://github.com/dotnet/csharplang/issues/1350 Padrões
or
disjuntivos que requerem qualquer um dos dois padrões diferentes para corresponder; -
https://github.com/dotnet/csharplang/issues/1350 Padrões de
not
negados que exigem um determinado padrão não corresponder; e ainda - https://github.com/dotnet/csharplang/issues/812 Padrões relacionais que exigem que o valor de entrada seja menor, menor ou igual a, etc uma determinada constante.
Padrões entre parênteses
Os padrões entre parênteses permitem que o programador coloque parênteses em torno de qualquer padrão. Isso não é tão útil com os padrões existentes em C# 8.0, no entanto, os novos combinadores de padrões introduzem uma precedência que o programador pode querer substituir.
primary_pattern
: parenthesized_pattern
| // all of the existing forms
;
parenthesized_pattern
: '(' pattern ')'
;
Padrões de tipo
Permitimos um tipo como padrão:
primary_pattern
: type-pattern
| // all of the existing forms
;
type_pattern
: type
;
Isto altera retroativamente a expressão de tipo existente is-type-expression para ser uma expressão de padrão is-pattern-expression na qual o padrão é um padrão de tipo, embora não alteremos a árvore de sintaxe produzida pelo compilador.
Uma questão de implementação sutil é que essa gramática é ambígua. Uma cadeia de caracteres como a.b
pode ser analisada como um nome qualificado (em um contexto de tipo) ou uma expressão pontilhada (em um contexto de expressão). O compilador já é capaz de tratar um nome qualificado da mesma forma que uma expressão pontilhada para lidar com algo como e is Color.Red
. A análise semântica do compilador seria ainda mais estendida para ser capaz de ligar um padrão constante (sintático) (por exemplo, uma expressão pontilhada) como um tipo, a fim de tratá-lo como um padrão de tipo ligado, a fim de suportar esta construção.
Após essa alteração, você seria capaz de escrever
void M(object o1, object o2)
{
var t = (o1, o2);
if (t is (int, string)) {} // test if o1 is an int and o2 is a string
switch (o1) {
case int: break; // test if o1 is an int
case System.String: break; // test if o1 is a string
}
}
Padrões relacionais
Os padrões relacionais permitem ao programador expressar que um valor de entrada deve satisfazer uma restrição relacional quando comparado a um valor constante:
public static LifeStage LifeStageAtAge(int age) => age switch
{
< 0 => LifeStage.Prenatal,
< 2 => LifeStage.Infant,
< 4 => LifeStage.Toddler,
< 6 => LifeStage.EarlyChild,
< 12 => LifeStage.MiddleChild,
< 20 => LifeStage.Adolescent,
< 40 => LifeStage.EarlyAdult,
< 65 => LifeStage.MiddleAdult,
_ => LifeStage.LateAdult,
};
Os padrões relacionais suportam os operadores relacionais <
, <=
, >
e >=
em todos os tipos internos que suportam esses operadores relacionais binários com dois operandos do mesmo tipo em uma expressão. Especificamente, suportamos todos esses padrões relacionais para sbyte
, byte
, short
, ushort
, int
, uint
, long
, ulong
, char
, float
, double
, decimal
, nint
e nuint
.
primary_pattern
: relational_pattern
;
relational_pattern
: '<' relational_expression
| '<=' relational_expression
| '>' relational_expression
| '>=' relational_expression
;
A expressão é necessária para avaliar até um valor constante. É um erro se o valor constante for double.NaN
ou float.NaN
. É um erro se a expressão for uma constante nula.
Quando a entrada é um tipo para o qual um operador relacional binário embutido adequado é definido que é aplicável com a entrada como seu operando esquerdo e a constante dada como seu operando direito, a avaliação desse operador é tomada como o significado do padrão relacional. Caso contrário, convertemos a entrada para o tipo da expressão usando uma conversão explícita anulável ou unboxing. É um erro em tempo de compilação se não existir tal conversão. Considera-se que o padrão não coincide se a conversão falhar. Se a conversão for bem-sucedida, o resultado da operação de correspondência de padrões é o resultado da avaliação da expressão e OP v
onde e
é a entrada convertida, OP
é o operador relacional e v
é a expressão constante.
Combinadores de Padrões
Os combinadores de padrão permitem corresponder a ambos de dois padrões diferentes usando and
(isto pode ser estendido para qualquer número de padrões através do uso repetido de and
), a qualquer um de dois padrões diferentes usando or
(idem), ou a negação de um padrão usando com not
.
Um uso comum de um combinador será a expressão idiomática
if (e is not null) ...
Mais legível do que o idioma atual e is object
, esse padrão expressa claramente que se está verificando um valor não nulo.
Os combinadores and
e or
serão úteis para testar intervalos de valores
bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';
Este exemplo ilustra que and
terá uma prioridade de análise mais alta (ou seja, ligará mais estreitamente) do que or
. O programador pode usar o padrão parênteses para tornar a precedência explícita:
bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');
Como todos os padrões, esses combinadores podem ser usados em qualquer contexto em que um padrão é esperado, incluindo padrões aninhados, a expressão é-padrão , a expressão de switch e o padrão do rótulo de um caso numa instrução switch.
pattern
: disjunctive_pattern
;
disjunctive_pattern
: disjunctive_pattern 'or' conjunctive_pattern
| conjunctive_pattern
;
conjunctive_pattern
: conjunctive_pattern 'and' negated_pattern
| negated_pattern
;
negated_pattern
: 'not' negated_pattern
| primary_pattern
;
primary_pattern
: // all of the patterns forms previously defined
;
Alterar para 6.2.5 Ambiguidades gramaticais
Devido à introdução do padrão de tipo , é possível que um tipo genérico apareça antes do token =>
. Portanto, adicionamos =>
ao conjunto de tokens listados em §6.2.5 Ambiguidades gramaticais para permitir a desambiguação do <
que inicia a lista de argumentos de tipo. Ver também https://github.com/dotnet/roslyn/issues/47614.
Problemas em aberto com alterações propostas
Sintaxe para operadores relacionais
Are and
, or
e not
algum tipo de palavra-chave contextual? Em caso afirmativo, existe uma mudança disruptiva (por exemplo, quando comparado à sua utilização como designador num padrão de declaração ).
Semântica (por exemplo, tipo) para operadores relacionais
Esperamos suportar todos os tipos primitivos que podem ser comparados em uma expressão usando um operador relacional. O significado em casos simples é claro
bool IsValidPercentage(int x) => x is >= 0 and <= 100;
Mas quando a entrada não é um tipo tão primitivo, para que tipo tentamos convertê-la?
bool IsValidPercentage(object x) => x is >= 0 and <= 100;
Propusemos que, quando o tipo de entrada já é um primitivo comparável, esse é o tipo de comparação. No entanto, quando a entrada não é uma primitiva comparável, tratamos o relacional como incluindo um teste de tipo implícito para o tipo da constante no lado direito do relacional. Se o programador pretende suportar mais de um tipo de entrada, isso deve ser feito explicitamente:
bool IsValidPercentage(object x) => x is
>= 0 and <= 100 or // integer tests
>= 0F and <= 100F or // float tests
>= 0D and <= 100D; // double tests
Resultado: O relacional inclui um teste de tipo implícito para o tipo da constante no lado direito do relacional.
O fluxo das informações de tipo da esquerda para a direita do and
Foi sugerido que, ao escrever um combinador and
, as informações de tipo aprendidas à esquerda sobre o tipo de nível superior possam fluir para a direita. Por exemplo
bool isSmallByte(object o) => o is byte and < 100;
Aqui, o tipo de entrada para o segundo padrão é restringido pelos requisitos de estreitamento do tipo à esquerda do and
. Definiríamos a semântica de estreitamento de tipo para todos os padrões da seguinte forma. O tipo estreito de um padrão P
é definido da seguinte forma:
- Se
for um padrão de tipo, o tipo reduzido de é o tipo do padrão de tipo. - Se
P
for um padrão de declaração, o tipo restrito é o tipo associado ao padrão de declaração. - Se
P
é um padrão recursivo que dá um tipo explícito, o tipo estreito é esse tipo. - Se
P
for associado através das regras paraITuple
, o tipo mais especializado é o tipoSystem.Runtime.CompilerServices.ITuple
. - Se
P
é um padrão constante onde a constante não é a constante nula e onde a expressão não tem conversão de expressão constante para o tipo de entrada , o tipo estreito é o tipo da constante. - Se
for um padrão relacional em que a expressão constante não possui a conversão da expressão constante para o tipo de entrada , o tipo limitado é o tipo da constante. - Se
P
é um padrãoor
, o tipo estreito é o tipo comum do tipo estreito dos subpadrões, se tal tipo comum existir. Para este propósito, o algoritmo de tipo comum considera apenas identidade, encaixotamento e conversões de referência implícitas, e considera todos os subpadrões numa sequência de padrõesor
(ignorando padrões entre parênteses). - Se
P
é um padrãoand
, o tipo reduzido é o tipo reduzido do padrão à direita. Além disso, a de tiporestrita do padrão esquerdo é o tipo de entrada do padrão direito. - Caso contrário, o tipo restringido do
de é o tipo de entrada de .
Resultado: A semântica de estreitamento acima foi implementada.
Definições de variáveis e atribuição definida
A adição de padrões or
e not
cria alguns novos problemas interessantes em torno de variáveis de padrão e atribuição definida. Como as variáveis normalmente podem ser declaradas no máximo uma vez, parece que qualquer variável de padrão declarada em um lado de um padrão or
não seria definitivamente atribuída quando o padrão corresponder. Da mesma forma, não se espera que uma variável declarada dentro de um padrão not
seja definitivamente atribuída quando o padrão corresponder. A maneira mais simples de resolver isso é proibir a declaração de variáveis de padrão nesses contextos. No entanto, isto pode ser demasiado restritivo. Há outras abordagens a considerar.
Um cenário que vale a pena considerar é este
if (e is not int i) return;
M(i); // is i definitely assigned here?
Isto não funciona atualmente porque, para uma expressão de padrão, as variáveis de padrão são consideradas definitivamente atribuídas apenas quando a expressão de padrão é verdadeira ("definitivamente atribuídas quando for verdadeira").
Apoiar isso seria mais simples (do ponto de vista do programador) do que adicionar suporte para uma instrução if
com condição negada. Mesmo se adicionarmos esse suporte, os programadores se perguntariam por que o trecho acima não funciona. Por outro lado, o mesmo cenário em um switch
faz menos sentido, pois não há nenhum ponto correspondente no programa onde definitivamente atribuído quando falso seria significativo. Permitiríamos isto num is-pattern-expression mas não noutros contextos onde padrões são permitidos? Isso parece irregular.
Relacionado com isto está o problema da atribuição definida num padrão disjuntivo .
if (e is 0 or int i)
{
M(i); // is i definitely assigned here?
}
Só expectamos que o i
seja definitivamente atribuído quando a entrada não for zero. Mas como não sabemos se a entrada é zero ou não dentro do bloco, i
não é definitivamente atribuído. No entanto, e se permitirmos i
ser declarada em diferentes padrões mutuamente exclusivos?
if ((e1, e2) is (0, int i) or (int i, 0))
{
M(i);
}
Aqui, a variável i
é definitivamente atribuída dentro do bloco e toma seu valor do outro elemento da tupla quando um elemento zero é encontrado.
Também foi sugerido permitir que as variáveis sejam definidas múltiplas vezes em cada caso de um bloco de casos.
case (0, int x):
case (int x, 0):
Console.WriteLine(x);
Para fazer qualquer um desses trabalhos, teríamos que definir cuidadosamente onde essas definições múltiplas são permitidas e em que condições tal variável é considerada definitivamente atribuída.
Se optarmos por adiar esse trabalho para mais tarde (o que aconselho), poderíamos dizer em C# 9
- abaixo de um
not
ouor
, as variáveis de padrão não podem ser declaradas.
Então, teríamos tempo para desenvolver alguma experiência que ofereça uma compreensão sobre o possível benefício de relaxar essa regra mais tarde.
Resultado: as variáveis de padrão não podem ser declaradas abaixo de um padrão not
ou or
.
Diagnóstico, subsunção e exaustividade
Estas novas formas de padrão introduzem muitas novas oportunidades para o erro diagnosticável do programador. Teremos de decidir que tipos de erros iremos diagnosticar e como fazê-lo. Eis alguns exemplos:
case >= 0 and <= 100D:
Este caso nunca pode coincidir (porque a entrada não pode ser simultaneamente um int
e um double
). Já temos um erro quando detetamos um caso que nunca pode corresponder, mas sua redação ("O caso do switch já foi tratado por um caso anterior" e "O padrão já foi manipulado por um braço anterior da expressão do switch") pode ser enganosa em novos cenários. Podemos ter que modificar a redação para apenas dizer que o padrão nunca corresponderá à entrada.
case 1 and 2:
Da mesma forma, isso seria um erro porque um valor não pode ser 1
e 2
.
case 1 or 2 or 3 or 1:
Este caso é possível de combinar, mas o or 1
no final não acrescenta significado ao padrão. Sugiro que devemos procurar produzir um erro sempre que alguma conjunção ou disjunção de um padrão composto não definir uma variável de padrão ou não afetar o conjunto de valores correspondentes.
case < 2: break;
case 0 or 1 or 2 or 3 or 4 or 5: break;
Aqui, 0 or 1 or
nada acrescenta ao segundo caso, uma vez que esses valores teriam sido tratados pelo primeiro caso. Também isto merece um erro.
byte b = ...;
int x = b switch { <100 => 0, 100 => 1, 101 => 2, >101 => 3 };
Uma expressão de comutador como esta deve ser considerada exaustiva (lida com todos os valores de entrada possíveis).
Em C# 8.0, uma expressão de switch com uma entrada do tipo byte
só é considerada exaustiva se contiver um braço final cujo padrão corresponda a tudo (um padrão de descarte ou um padrão variável ). Mesmo uma expressão de switch que tem um braço para cada valor de byte
distinto não é considerada exaustiva em C# 8. A fim de lidar adequadamente com a exaustividade dos padrões relacionais, teremos que lidar com este caso também. Esta será tecnicamente uma mudança de rutura, mas, provavelmente, nenhum utilizador notará.
C# feature specifications