Compartir vía


Coincidencia de patrones recursiva

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 .

Resumen

Las extensiones de coincidencia de patrones para C# permiten muchas de las ventajas de los tipos de datos algebraicos y la coincidencia de patrones de lenguajes funcionales, pero de una manera que se integra sin problemas con la sensación del lenguaje subyacente. Los elementos de este enfoque se inspiran en características relacionadas de los lenguajes de programación F# y Scala.

Diseño detallado

Expresión Is

El operador is se extiende para probar una expresión con un patrón de .

relational_expression
    : is_pattern_expression
    ;
is_pattern_expression
    : relational_expression 'is' pattern
    ;

Esta forma de relational_expression se suma a los formularios existentes en la especificación de C#. Se trata de un error en tiempo de compilación si el relational_expression a la izquierda del token de is no designa un valor o no tiene un tipo.

Cada identificador del patrón introduce una nueva variable local que es asignada definitivamente después de que el operador is sea true (es decir, asignada definitivamente cuando es verdadero).

Nota: Técnicamente hay una ambigüedad entre el tipo en una is-expression y constant_pattern, cualquiera de los cuales podría ser una interpretación válida de un identificador calificado. Intentamos enlazarlo como un tipo para la compatibilidad con versiones anteriores del idioma; solo si se produce un error, lo resuelvemos como hacemos una expresión en otros contextos, a la primera cosa encontrada (que debe ser una constante o un tipo). Esta ambigüedad solo está presente en el lado derecho de una expresión is.

Patrones

Los patrones se utilizan en el operador is_pattern, en un switch_statement, y en una switch_expression para expresar la forma de los datos contra la que se compararán los datos entrantes, a los que llamamos el valor de entrada. Los patrones pueden ser recursivos para que las partes de los datos se puedan comparar con subprocesos.

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
    : '_'
    ;

Patrón de declaración

declaration_pattern
    : type simple_designation
    ;

El declaration_pattern prueba que una expresión es de un tipo determinado y la convierte en ese tipo si la prueba tiene éxito. Esto puede introducir una variable local del tipo dado nombrado por el identificador dado, si la designación es una designación_de_variable_única. Esa variable local se asigna definitivamente cuando el resultado de la operación de coincidencia de patrones es true.

La semántica en tiempo de ejecución de esta expresión es que prueba el tipo en tiempo de ejecución del operando relational_expression izquierdo contra el tipo en el patrón. Si es de ese tipo en tiempo de ejecución (o algún subtipo) y no null, el resultado del is operator es true.

Ciertas combinaciones de tipo estático del lado izquierdo y el tipo especificado se consideran incompatibles y generan un error en tiempo de compilación. Se dice que un valor de tipo estático E es compatibles con patrones con un tipo T si existe una conversión de identidad, una conversión de referencia implícita, una conversión boxing, una conversión de referencia explícita o una conversión de unboxing de E a T, o si uno de esos tipos es un tipo abierto. Se trata de un error en tiempo de compilación si una entrada de tipo E no es compatible con el patrón con el tipo en un patrón de tipo con el que se coteja.

El patrón de tipo es útil para realizar pruebas de tipos en tiempo de ejecución de tipos de referencia y reemplaza el idiom.

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

Con una versión ligeramente más concisa

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

Es un error si el tipo es un tipo de valor anulable.

El patrón de tipo se puede usar para probar valores de tipos que aceptan valores nulos: un valor de tipo Nullable<T> (o un Tencapsulado) coincide con un patrón de tipo T2 id si el valor no es nulo y el tipo de T2 es T, o algún tipo base o interfaz de T. Por ejemplo, en el fragmento de código

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

La condición de la instrucción if es true en tiempo de ejecución y la variable v contiene el valor 3 de tipo int dentro del bloque. Después del bloque, la variable v está en el ámbito, pero no está asignada definitivamente.

Patrón constante

constant_pattern
    : constant_expression
    ;

Un patrón constante comprueba el valor de una expresión con un valor constante. La constante puede ser cualquier expresión constante, como un literal, el nombre de una variable declarada const o una constante de enumeración. Cuando el valor de entrada no es un tipo abierto, la expresión constante se convierte implícitamente en el tipo de la expresión coincidente; si el tipo del valor de entrada no es compatibles con el patrón con el tipo de la expresión constante, la operación de coincidencia de patrones es un error.

El patrón c se considera que coincide con el valor de entrada convertido e si object.Equals(c, e) devolvería true.

Esperamos ver e is null como la manera más común de probar para null en código recién escrito, ya que no puede invocar un operator==definido por el usuario.

Patrón 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
    ;

Si la designación es una expresión simple_designation, una expresión e coincide con el patrón. Es decir, una coincidencia con un patrón var siempre se realiza correctamente con una simple_designation. Si la simple_designation es una single_variable_designation, el valor de e se enlaza a una variable local recién introducida. El tipo de la variable local es el tipo estático de e.

Si la designación es una tuple_designation, el patrón es equivalente a un positional_pattern con el formato (vardesignación, ... ) donde las designaciones son las que se encuentran dentro de la tuple_designation. Por ejemplo, el patrón var (x, (y, z)) es equivalente a (var x, (var y, var z)).

Se trata de un error si el nombre var se enlaza a un tipo.

Descartar patrón

discard_pattern
    : '_'
    ;

Una expresión e coincide con el patrón _ siempre. En otras palabras, cada expresión coincide con el patrón de descarte.

Un patrón de descarte no se puede usar como el patrón de una is_pattern_expression.

Patrón posicional

Un patrón posicional comprueba que el valor de entrada no es null, invoca un método Deconstruct adecuado y realiza una coincidencia de patrones adicional en los valores resultantes. También admite una sintaxis de patrón similar a la tupla (sin el tipo proporcionado) cuando el tipo del valor de entrada es el mismo que el tipo que contiene Deconstruct, o si el tipo del valor de entrada es un tipo de tupla, o si el tipo del valor de entrada es object o ITuple y el tipo en tiempo de ejecución de la expresión implementa ITuple.

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

Si se omite el tipo , se toma que sea el tipo estático del valor de entrada.

Dada una coincidencia de un valor de entrada con el patrón tipo(subpattern_list), se selecciona un método mediante la búsqueda en tipo para obtener declaraciones de Deconstruct accesibles y seleccionar una entre ellas con las mismas reglas que para la declaración de deconstrucción.

Es un error si un positional_pattern omite el tipo, tiene un solo subpattern sin un identifier, no tiene property_subpattern y no tiene simple_designation. Esto desambigua entre un constant_pattern entre paréntesis y un positional_pattern.

Para extraer los valores que deben coincidir con los patrones de la lista,

  • Si el tipo se omitió y el tipo del valor de entrada es un tipo de tupla, se requiere que el número de subpatrones sea el mismo que la cardinalidad de la tupla. Cada elemento de la tupla se compara con el subpatrón correspondiente subpattern y la coincidencia es correcta si todas son correctas. Si algún subpatrón tiene un identificador, debe nombrar un elemento de tupla en la posición correspondiente del tipo de tupla.
  • De lo contrario, si existe un Deconstruct adecuado como miembro de tipo, es un error en tiempo de compilación si el tipo del valor de entrada no es compatible con patrones con el tipo. En tiempo de ejecución, el valor de entrada se prueba contra el tipo. Si falla esto, entonces también fallará la coincidencia de patrones posicionales. Si se ejecuta correctamente, el valor de entrada se convierte en este tipo y Deconstruct se invoca con variables generadas por el compilador nuevas para recibir los parámetros de out. Cada valor recibido se compara con el subpatrón correspondiente, y la coincidencia es correcta si todas ellas lo son. Si cualquier subpatrón llamado tiene un identificador , entonces debe nombrar un parámetro en la posición correspondiente de Deconstruct.
  • De lo contrario, si se omitió el tipo y el valor de entrada es de tipo object o ITuple o de algún tipo que se pueda convertir a ITuple mediante una conversión de referencia implícita, y no aparece ningún identificador entre los subpatrones, hacemos coincidir utilizando ITuple.
  • De lo contrario, el patrón es un error en tiempo de compilación.

El orden en el que se emparejan los subpatrones en tiempo de ejecución no está especificado, y es posible que una coincidencia fallida no intente emparejar todos los subpatrones.

Ejemplo

En este ejemplo se usan muchas de las características descritas en esta especificación.

    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 };

Patrón de propiedades

Un patrón de propiedad comprueba que el valor de entrada no es null y coincide recursivamente con los valores extraídos por el uso de propiedades o campos accesibles.

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

Se trata de un error si algún subpattern de un property_pattern no contiene un identificador (debe tener el segundo formato, que tiene un identificador). Una coma final después del último subpattern es opcional.

Tenga en cuenta que un patrón de comprobación de valores NULL sale de un patrón de propiedad trivial. Para comprobar si la cadena s no es NULL, puede escribir cualquiera de los siguientes formularios.

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 una coincidencia de una expresión e al patrón type{property_pattern_list}, es un error en tiempo de compilación si la expresión e no es compatible con patrones con el tipo T designado por el tipo. Si el tipo está ausente, se toma que sea el tipo estático de e. Si el identificador de está presente, declara una variable de patrón de tipo tipo. Cada uno de los identificadores que aparecen en el lado izquierdo de su property_pattern_list debe designar una propiedad o campo legible accesible de T. Si la simple_designation de la property_pattern está presente, define una variable de patrón de tipo T.

En tiempo de ejecución, la expresión se evalúa frente a T. Si esto falla, entonces hay un fallo en la coincidencia del patrón de propiedad y el resultado es false. Si se ejecuta correctamente, cada property_subpattern campo o propiedad se lee y su valor coincide con su patrón correspondiente. El resultado de todo el partido es false solo si el resultado de cualquiera de estos es false. No se especifica el orden en el que se coinciden los subpatrones y es posible que una coincidencia con errores no coincida con todos los subpatrones en tiempo de ejecución. Si la coincidencia es correcta y la simple_designation de property_pattern es una single_variable_designation, define una variable de tipo T a la que se le asigna el valor coincidente.

Nota: El patrón de propiedad se puede usar para establecer coincidencias de patrones con tipos anónimos.

Ejemplo
if (o is string { Length: 5 } s)

Cambiar expresión

Se agrega un switch_expression para admitir semántica similar a switch en un contexto de expresión.

La sintaxis del lenguaje C# se amplía con las siguientes producciones sintácticas.

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
    ;

No se permite la switch_expression como expression_statement.

Estamos buscando relajar esto en una revisión futura.

El tipo de switch_expression es el mejor tipo común (§12.6.3.15) de las expresiones que aparecen a la derecha de los tokens de => de la switch_expression_arms si existe dicho tipo y la expresión de cada brazo de la expresión switch se puede convertir implícitamente en ese tipo. Además, agregamos una nueva conversión de expresión de conmutador, que consiste en una conversión implícita predefinida de una expresión de conmutador a cada tipo T para el cual existe una conversión implícita de la expresión de cada rama a T.

Es un error si algún patrón switch_expression_arms no puede afectar al resultado porque algún patrón anterior y la protección siempre coincidirán.

Se dice que una expresión switch es exhaustiva si algún brazo de la expresión switch controla cada valor de su entrada. El compilador generará una advertencia si una expresión switch no es exhaustiva.

En tiempo de ejecución, el resultado de la switch_expression es el valor de la expresión del primer switch_expression_arm para el que la expresión del lado izquierdo de la switch_expression coincide con el patrón de switch_expression_arms y para el que el case_guard de switch_expression_arm, si está presente, se evalúa como true. Si no hay tal switch_expression_arm, el switch_expression lanza una instancia de la excepción System.Runtime.CompilerServices.SwitchExpressionException.

Paréntesis opcionales al activar un literal de tupla

Para activar un literal de tupla con la switch_statement, debe escribir lo que parecen ser paréntesis redundantes.

switch ((a, b))
{

Para permitir

switch (a, b)
{

los paréntesis de la instrucción switch son opcionales cuando la expresión que se está activando es un literal de tupla.

Orden de evaluación en la coincidencia de patrones

Proporcionar al compilador flexibilidad para reordenar las operaciones ejecutadas durante la coincidencia de patrones puede permitir la flexibilidad que se puede usar para mejorar la eficacia de la coincidencia de patrones. El requisito (no aplicado) sería que las propiedades a las que se accede en un patrón y los métodos Deconstruct deben ser "puros" (libres de efectos secundarios, idempotentes, etc.). Esto no significa que agregaríamos pureza como concepto de lenguaje, solo que permitiríamos la flexibilidad del compilador en las operaciones de reordenación.

Resolución 2018-04-04 LDM: confirmado: el compilador puede reordenar llamadas a Deconstruct, accesos a propiedades e invocaciones de métodos en ITuple, y puede suponer que los valores devueltos son los mismos de varias llamadas. El compilador no debe invocar funciones que no puedan afectar al resultado y seremos muy cuidadosos antes de realizar cambios en el orden de evaluación generado por el compilador en el futuro.

Algunas optimizaciones posibles

La compilación de la coincidencia de patrones puede aprovechar las partes comunes de los patrones. Por ejemplo, si la prueba de tipo de nivel superior de dos patrones sucesivos en un switch_statement es el mismo tipo, el código generado puede omitir la prueba de tipos para el segundo patrón.

Cuando algunos de los patrones son enteros o cadenas, el compilador puede generar el mismo tipo de código que genera para una instrucción switch en versiones anteriores del lenguaje.

Para obtener más información sobre estos tipos de optimizaciones, consulte [Scott y Ramsey (2000)].