Cambios de coincidencia de patrones para C# 9.0
Nota
Este artículo es una especificación de características. La especificación actúa como documento de diseño de la característica. Incluye cambios de especificación propuestos, junto con la información necesaria durante el diseño y el desarrollo de la característica. Estos artículos se publican hasta que se finalizan los cambios de especificación propuestos e se incorporan en la especificación ECMA actual.
Puede haber algunas discrepancias entre la especificación de características y la implementación completada. Esas diferencias se recogen en las notas de la reunión de diseño de lenguaje (LDM) correspondientes.
Puede obtener más información sobre el proceso de adopción de especificaciones de características en el estándar del lenguaje C# en el artículo sobre las especificaciones de .
Estamos considerando una pequeña serie de mejoras en la coincidencia de patrones para C# 9.0 que tienen sinergia natural y funcionan bien para abordar una serie de problemas comunes de programación:
- https://github.com/dotnet/csharplang/issues/2925 Patrones de tipo
- https://github.com/dotnet/csharplang/issues/1350 patrones entre paréntesis para aplicar o resaltar la prioridad de los nuevos combinadores
- https://github.com/dotnet/csharplang/issues/1350 Patrones conjuntivos
and
que requieren que coincidan dos patrones diferentes; - https://github.com/dotnet/csharplang/issues/1350 patrones disjuntivos de
or
que requieren que cualquiera de los dos patrones diferentes coincida; - https://github.com/dotnet/csharplang/issues/1350 Patrones
not
negados que requieren que un patrón determinado para no coincidir; y - https://github.com/dotnet/csharplang/issues/812 patrones relacionales que requieren que el valor de entrada sea menor que, menor o igual que, etc. una constante determinada.
Patrones entre paréntesis
Los patrones entre paréntesis permiten al programador colocar paréntesis alrededor de cualquier patrón. Esto no es tan útil con los patrones existentes en C# 8.0, pero los nuevos combinadores de patrones presentan una prioridad que el programador puede querer invalidar.
primary_pattern
: parenthesized_pattern
| // all of the existing forms
;
parenthesized_pattern
: '(' pattern ')'
;
Patrones de tipo
Permitimos un tipo como patrón:
primary_pattern
: type-pattern
| // all of the existing forms
;
type_pattern
: type
;
Esto modifica la is-type-expression existente para convertirse en una is-pattern-expression en la que el patrón es un patrón de tipo, aunque no cambiaríamos el árbol de sintaxis generado por el compilador.
Un problema sutil de implementación es que esta gramática es ambigua. Una cadena como a.b
se puede interpretar como un nombre calificado (en un contexto de tipo) o una expresión con puntos (en un contexto de expresión). El compilador ya es capaz de tratar un nombre calificado igual que una expresión con puntos para manejar algo como e is Color.Red
. El análisis semántico del compilador se extendería aún más para poder enlazar un patrón de constante (sintáctico) (por ejemplo, una expresión de puntos) como un tipo para tratarlo como un patrón de tipo enlazado para admitir esta construcción.
Después de este cambio, vas a poder escribir
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
}
}
Patrones relacionales
Los patrones relacionales permiten al programador expresar que un valor de entrada debe satisfacer una restricción relacional en comparación con un 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,
};
Los patrones relacionales admiten los operadores relacionales <
, <=
, >
y >=
en todos los tipos integrados que admiten estos operadores relacionales binarios con dos operandos del mismo tipo en una expresión. En concreto, se admiten todos estos patrones relacionales para sbyte
, byte
, short
, ushort
, int
, uint
, long
, ulong
, char
, float
, double
, decimal
, nint
y nuint
.
primary_pattern
: relational_pattern
;
relational_pattern
: '<' relational_expression
| '<=' relational_expression
| '>' relational_expression
| '>=' relational_expression
;
La expresión es necesaria para evaluar un valor constante. Es un error si ese valor constante es double.NaN
o float.NaN
. Se trata de un error si la expresión es una constante null.
Cuando la entrada es un tipo para el que se define un operador relacional binario integrado adecuado que se aplica con la entrada como su operando izquierdo y la constante dada como su operando derecho, la evaluación de ese operador se toma como el significado del patrón relacional. De lo contrario, convertimos la entrada al tipo de la expresión mediante una conversión explícita que acepta valores NULL o de unboxing. Es un error en tiempo de compilación si no existe dicha conversión. El patrón se considera que no coincide si se produce un error en la conversión. Si la conversión se realiza correctamente, el resultado de la operación de coincidencia de patrones es el resultado de evaluar la expresión e OP v
donde e
es la entrada convertida, OP
es el operador relacional y v
es la expresión constante.
Combinadores de patrones
Los combinadores de patrón permiten hacer coincidir ambos patrones diferentes mediante and
(esto se puede extender a cualquier número de patrones mediante el uso repetido de )and
), uno de dos patrones diferentes mediante or
(ditto) o la negación de un patrón mediante not
.
Un uso común de un combinador será la expresión
if (e is not null) ...
Más legible que la expresión actual e is object
, este patrón expresa claramente que se está comprobando un valor distinto de NULL.
Los combinadores and
y or
serán útiles para probar intervalos de valores.
bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';
En este ejemplo se muestra que and
tendrá una prioridad de análisis más alta (es decir, enlazará más estrechamente) que or
. El programador puede usar el patrón entre paréntesis para que la prioridad sea explícita:
bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');
Al igual que todos los patrones, estos combinadores se pueden usar en cualquier contexto en el que se espera un patrón, incluidos los patrones anidados, is-pattern-expression, switch-expression y el patrón de la etiqueta de mayúsculas y minúsculas de una instrucción 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
;
Cambiar a 6.2.5 Ambigüedades gramaticales
Debido a la introducción del patrón de tipo , es posible que un tipo genérico aparezca antes del token =>
. Por lo tanto, agregamos =>
al conjunto de tokens enumerados en §6.2.5 Ambigüedades gramaticales para permitir la desambiguación del <
que inicia la lista de argumentos de tipo. Consulte también https://github.com/dotnet/roslyn/issues/47614.
Problemas abiertos con cambios propuestos
Sintaxis para operadores relacionales
¿Se and
, or
y not
algún tipo de palabra clave contextual? Si es así, ¿hay un cambio radical, por ejemplo, en comparación con su uso como designador en un patrón de declaración ?
Semántica (por ejemplo, tipo) para operadores relacionales
Esperamos admitir todos los tipos primitivos que se pueden comparar en una expresión mediante un operador relacional. El significado en casos simples es claro
bool IsValidPercentage(int x) => x is >= 0 and <= 100;
Pero cuando la entrada no es de este tipo primitivo, ¿a qué tipo intentamos convertirla?
bool IsValidPercentage(object x) => x is >= 0 and <= 100;
Hemos propuesto que cuando el tipo de entrada ya sea un primitivo comparable, es decir, el tipo de la comparación. Sin embargo, cuando la entrada no es un primitivo comparable, tratamos el relacional como una prueba de tipo implícita al tipo de la constante en el lado derecho del relacional. Si el programador pretende admitir más de un tipo de entrada, debe realizarse explícitamente:
bool IsValidPercentage(object x) => x is
>= 0 and <= 100 or // integer tests
>= 0F and <= 100F or // float tests
>= 0D and <= 100D; // double tests
Resultado: el relacional incluye una prueba de tipo implícita al tipo de la constante en el lado derecho del relacional.
Flujo de información de tipo de izquierda a derecha de and
Se ha sugerido que, al escribir un combinador de and
, la información de tipo aprendida a la izquierda sobre el tipo de nivel superior podría fluir a la derecha. Por ejemplo
bool isSmallByte(object o) => o is byte and < 100;
En este caso, el tipo de entrada al segundo patrón se limita mediante los requisitos de restricción de tipo a la izquierda de and
. Definiríamos la semántica de restricción de tipos para todos los patrones como se indica a continuación. El tipo restringido de un patrón P
se define de la siguiente manera:
- Si
P
es un patrón de tipo, el tipo restringido es el tipo correspondiente al patrón de tipo. - Si
P
es un patrón de declaración, el tipo restringido es el tipo correspondiente al patrón de declaración. - Si
P
es un patrón recursivo que proporciona un tipo explícito, el tipo restringido es ese tipo. - Si
P
se coteja mediante las reglas paraITuple
, el tipo restringido es el tipoSystem.Runtime.CompilerServices.ITuple
. - Si
P
es un patrón constante en el que la constante no es la constante nula y cuando la expresión no tiene una conversión de expresión constante al tipo de entrada , el tipo reducido es el tipo de la constante. - Si
P
es un patrón relacional en el que la expresión constante no tiene conversión de expresión constante al tipo de entrada, el tipo restringido es el tipo de la constante. - Si
P
es un patrón deor
, el tipo restringido es el tipo común del tipo restringido de los subpatrones si existe tal tipo común. Para este propósito, el algoritmo de tipo común solo considera las conversiones de identidad, boxing e implícitas, y tiene en cuenta todos los subpatrones de una secuencia de patrones deor
(ignorando los patrones entre paréntesis). - Si
P
es un patrón deand
, el tipo restringido es el tipo restringido del patrón de la derecha. Además, el tipo restringido del patrón izquierdo es el tipo de entrada del patrón derecho. - De lo contrario, el tipo restringido de
P
es el tipo de entrada deP
.
Resultado: se ha implementado la semántica limitada anterior.
Definiciones de variables y asignación definitiva
La adición de los patrones or
y not
crea algunos nuevos problemas interesantes en relación con las variables de patrón y la asignación definida. Dado que las variables normalmente se pueden declarar como máximo una vez, parecería que cualquier variable de patrón declarada en un lado de un patrón de or
no se asignaría definitivamente cuando coincida el patrón. Del mismo modo, no se espera que una variable declarada dentro de un patrón de not
se asigne definitivamente cuando el patrón coincida. La manera más sencilla de abordar esto es prohibir la declaración de variables de patrón en estos contextos. Sin embargo, esto puede ser demasiado restrictivo. Hay otros enfoques que se deben tener en cuenta.
Un escenario que vale la pena tener en cuenta es este
if (e is not int i) return;
M(i); // is i definitely assigned here?
Esto no funciona actualmente porque, para una is-pattern-expression, las variables de patrón se consideran definitivamente asignadas solo donde is-pattern-expression es true ("definitivamente asignadas cuando es verdadera").
Apoyar esto sería más sencillo (desde la perspectiva del programador) que agregar soporte para una instrucción if
de condición negada. Incluso si agregamos este soporte técnico, los programadores se preguntarían por qué el fragmento de código anterior no funciona. Por otro lado, el mismo escenario en un switch
tiene menos sentido, ya que no hay ningún punto correspondiente en el programa donde se asigne definitivamente en caso de que sea falso y eso tenga sentido. ¿Permitiríamos esto en un is-pattern-expression, pero no en otros contextos en los que se permiten patrones? Eso parece irregular.
El problema de la asignación definitiva en un patrón disyuntivo está relacionado con esto.
if (e is 0 or int i)
{
M(i); // is i definitely assigned here?
}
Únicamente esperamos que i
se asigne definitivamente cuando la entrada no sea cero. Pero como no sabemos si la entrada es cero o no dentro del bloque, i
no está asignada definitivamente. Sin embargo, ¿qué ocurre si permitimos que i
se declare en diferentes patrones mutuamente excluyentes?
if ((e1, e2) is (0, int i) or (int i, 0))
{
M(i);
}
Aquí, la variable i
se asigna definitivamente dentro del bloque y toma su valor del otro elemento de la tupla cuando se encuentra un elemento cero.
También se ha sugerido permitir que las variables se definan (multiplicar) en cada caso de un bloque de casos:
case (0, int x):
case (int x, 0):
Console.WriteLine(x);
Para realizar cualquiera de este trabajo, tendríamos que definir cuidadosamente dónde se permiten estas definiciones y en qué condiciones se considera que se asigna definitivamente dicha variable.
Si optamos por aplazar este trabajo hasta más tarde (lo que recomiendo), podríamos decir en C# 9
- debajo de una
not
oor
, es posible que no se declaren variables de patrón.
Entonces, tendríamos tiempo para adquirir experiencia que nos brindaría conocimiento sobre el posible valor de relajar esta restricción en el futuro.
Resultado: las variables de patrón no se pueden declarar debajo de un patrón de not
o or
.
Diagnóstico, subsumición y exhaustividad
Estas nuevas formas de patrón presentan muchas nuevas oportunidades para diagnosticar errores del programador. Tendremos que decidir qué tipos de errores diagnosticaremos y cómo hacerlo. Estos son algunos ejemplos:
case >= 0 and <= 100D:
Este caso nunca puede coincidir (porque la entrada no puede ser tanto un int
como un double
). Ya tenemos un error cuando detectamos un caso que nunca puede coincidir, pero su redacción ("El caso del switch ya ha sido gestionado por un caso anterior" y "El patrón ya ha sido gestionado por una rama anterior de la expresión switch") puede ser engañosa en nuevos escenarios. Es posible que tengamos que modificar el texto para simplemente decir que el patrón nunca coincidirá con la entrada.
case 1 and 2:
Del mismo modo, este sería un error porque un valor no puede ser 1
y 2
.
case 1 or 2 or 3 or 1:
Este caso se puede cotejar, pero el or 1
al final no agrega significado alguno al patrón. Sugiero que deberíamos intentar generar un error cada vez que alguna conjunción o disyunción de un patrón compuesto no defina una variable de patrón o afecte al conjunto de valores emparejados.
case < 2: break;
case 0 or 1 or 2 or 3 or 4 or 5: break;
Aquí, 0 or 1 or
no agrega nada al segundo caso, ya que esos valores habrían sido manejados por el primer caso. Esto también debería considerarse un error.
byte b = ...;
int x = b switch { <100 => 0, 100 => 1, 101 => 2, >101 => 3 };
Se debe considerar una expresión switch como esta exhaustiva (controla todos los valores de entrada posibles).
En C# 8.0, una expresión switch con una entrada de tipo byte
solo se considera exhaustiva si contiene un brazo final cuyo patrón coincide con todo (un patrón de descarte o un patrón var ). Incluso una expresión switch que tenga un brazo para cada valor de byte
distinto no se considera exhaustiva en C# 8. Para manejar correctamente la exhaustividad de los patrones relacionales, también tendremos que manejar este caso. Técnicamente, esto será un cambio importante, pero es probable que ningún usuario observe.
C# feature specifications