Partager via


Correspondance de motif récursif

Remarque

Cet article est une spécification de fonctionnalité. La spécification sert de document de conception pour la fonctionnalité. Il inclut les modifications de spécification proposées, ainsi que les informations nécessaires pendant la conception et le développement de la fonctionnalité. Ces articles sont publiés jusqu’à ce que les modifications de spécification proposées soient finalisées et incorporées dans la spécification ECMA actuelle.

Il peut y avoir des différences entre la spécification de la fonctionnalité et l’implémentation terminée. Ces différences sont capturées dans les notes pertinentes de la réunion de conception de langage (LDM).

Vous pouvez en savoir plus sur le processus d’adoption des speclets de fonctionnalités dans la norme de langage C# dans l’article sur les spécifications .

Résumé

Les extensions de correspondance de modèles pour C# permettent de tirer parti de nombreux avantages des types de données algébriques et de la correspondance de modèles à partir de langages fonctionnels, mais d’une manière qui s’intègre parfaitement à l'esprit du langage sous-jacent. Les éléments de cette approche sont inspirés par des fonctionnalités connexes dans les langages de programmation F# et Scala.

Conception détaillée

Est Expression

L’opérateur is est étendu pour tester une expression par rapport à un modèle .

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

Cette forme de relational_expression est en plus des formulaires existants dans la spécification C#. Il s’agit d’une erreur au moment de la compilation si le relational_expression à gauche du jeton is ne désigne pas de valeur ou n’a pas de type.

Chaque identificateur du modèle introduit une nouvelle variable locale qui est définitivement affectée après que l’opérateur de is est true (c’est-à-dire définitivement affecté quand true).

Remarque : Il existe techniquement une ambiguïté entre type dans un is-expression et constant_pattern, dont l’une peut être une analyse valide d’un identificateur qualifié. Nous essayons de le lier en tant que type pour la compatibilité avec les versions précédentes de la langue ; seulement si cela échoue, nous le résoudrons comme nous le faisons dans d’autres contextes, à la première chose trouvée (qui doit être une constante ou un type). Cette ambiguïté n’est présente que sur le côté droit d’une expression is.

Modèles

Les modèles sont utilisés dans l’opérateur is_pattern, dans un switch_statement, et dans un switch_expression pour exprimer la forme de données sur laquelle les données entrantes (que nous appelons la valeur d’entrée) doivent être comparées. Les modèles peuvent être récursifs afin que les parties des données puissent être mises en correspondance avec les sous-modèles.

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

Modèle de déclaration

declaration_pattern
    : type simple_designation
    ;

L'declaration_pattern teste à la fois qu’une expression est d’un type donné et la convertit en ce type si le test réussit. Cela peut introduire une variable locale du type donné nommé par l’identificateur donné, si la désignation est un single_variable_designation. Cette variable locale est définitivement affectée lorsque le résultat de l’opération de correspondance de modèle est true.

La sémantique runtime de cette expression est qu’elle teste le type d’exécution de l’opérande de gauche relational_expression par rapport au type dans le modèle. S’il s’agit de ce type d’exécution (ou d’un sous-type) et non null, le résultat de l'is operator est true.

Certaines combinaisons de type statique du côté gauche et du type donné sont considérées comme incompatibles et entraînent une erreur au moment de la compilation. Une valeur de type statique E est considérée comme compatible avec le motif avec un type T s’il existe une conversion d’identité, une conversion de référence implicite, une conversion par boxing, une conversion de référence explicite ou une conversion de déboxing de E à T, ou si l’un de ces types est un type ouvert. Il s’agit d’une erreur au moment de la compilation si une entrée de type E n’est pas compatible au modèle avec le type dans un modèle de type avec lequel elle est mise en correspondance.

Le modèle de type est utile pour effectuer des tests de type à l'exécution sur des types de référence et remplace l'expression courante.

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

Avec le légèrement plus concis

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

Il s’agit d’une erreur si type est un type valeur nullable.

Le modèle de type peut être utilisé pour tester les valeurs de types nullables : une valeur de type Nullable<T> (ou une Tboxée) correspond à un modèle de type T2 id si la valeur n’est pas null et que le type de T2 est T, ou un type de base ou une interface de T. Par exemple, dans le fragment de code

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

La condition de l’instruction if est true au moment de l’exécution et la variable v contient la valeur 3 de type int à l’intérieur du bloc. Après le bloc, la variable v est dans la portée mais n'est pas assignée définitivement.

Modèle constant

constant_pattern
    : constant_expression
    ;

Un modèle constant teste la valeur d’une expression par rapport à une valeur constante. La constante peut être n’importe quelle expression constante, telle qu’un littéral, le nom d’une variable const déclarée ou une constante d’énumération. Lorsque la valeur d’entrée n’est pas un type ouvert, l’expression constante est implicitement convertie en type de l’expression correspondante ; si le type de la valeur d’entrée n’est pas compatible avec le modèle avec le type de l’expression constante, l’opération de correspondance de modèle est une erreur.

Le modèle c est considéré comme correspondant à la valeur d’entrée convertie e si object.Equals(c, e) retournerait true.

Nous nous attendons à voir e is null comme le moyen le plus courant de tester null dans le code nouvellement écrit, car il ne peut pas appeler un operator==défini par l’utilisateur.

Modèle 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 désignation est un simple_designation, une expression e correspond au modèle. En d’autres termes, une correspondance à un modèle de var réussit toujours avec un simple_designation. Si le simple_designation est un single_variable_designation, la valeur de e est liée à une variable locale nouvellement introduite. Le type de la variable locale est le type statique de e.

Si la désignation est un tuple_designation, le modèle équivaut à un positional_pattern de la forme désignation, ... où lesde désignation sont celles trouvées dans le tuple_designation. Par exemple, le modèle var (x, (y, z)) équivaut à (var x, (var y, var z)).

Il s’agit d’une erreur si le nom var est lié à un type.

Jeter le modèle

discard_pattern
    : '_'
    ;

Une expression e correspond toujours au modèle _. En d'autres termes, chaque expression correspond au motif de rejet.

Un modèle de rejet ne peut pas être utilisé en tant que modèle d'un is_pattern_expression.

Modèle positionnel

Un modèle positionnel vérifie que la valeur d’entrée n’est pas null, appelle une méthode Deconstruct appropriée et effectue une correspondance de modèle supplémentaire sur les valeurs résultantes. Il prend également en charge une syntaxe de modèle de type tuple (sans le type fourni) lorsque le type de la valeur d’entrée est identique au type contenant Deconstruct, ou si le type de la valeur d’entrée est un type tuple, ou si le type de la valeur d’entrée est object ou ITuple et que le type d’exécution de l’expression implémente ITuple.

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

Si le type est omis, il s’agit du type statique de la valeur d’entrée.

Étant donné une correspondance d’une valeur d’entrée au modèle type(subpattern_list), une méthode est sélectionnée en recherchant dans type pour les déclarations accessibles de Deconstruct et en sélectionnant une parmi elles à l’aide des mêmes règles que pour la déclaration de déconstruction.

Il s’agit d’une erreur si un positional_pattern omet le type, a un seul sous-modèle sans identificateur, n’a pas de property_subpattern et n’a pas de simple_designation. Cela désambiguïse entre un constant_pattern placé entre parenthèses et un positional_pattern.

Pour extraire les valeurs à mettre en correspondance avec les modèles de la liste,

  • Si type a été omis et que le type de la valeur d’entrée est un type tuple, le nombre de sous-modèles doit être identique à la cardinalité du tuple. Chaque élément tuple est mis en correspondance avec le sous-modèle correspondant, et la correspondance réussit si toutes ces correspondances réussissent. Si un sous-modèle a un identificateur , il doit nommer un élément tuple à la position correspondante dans le type de tuple.
  • Sinon, si un Deconstruct approprié existe en tant que membre de type, il s’agit d’une erreur au moment de la compilation si le type de la valeur d’entrée n’est pas compatible avec type. Lors de l’exécution, la valeur d’entrée est testée par rapport à type. Si cela échoue, le modèle de correspondance positionnelle échoue. Si elle réussit, la valeur d’entrée est convertie en ce type et Deconstruct est appelée avec de nouvelles variables générées par le compilateur pour recevoir les paramètres de out. Chaque valeur reçue est mise en correspondance avec le sous-modèle correspondant, et la correspondance réussit si toutes ces correspondances réussissent. Si un sous-modèle a l'identificateur , il doit alors désigner un paramètre à la position correspondante de Deconstruct.
  • Sinon, si le type de a été omis, et que la valeur d’entrée est de type ou ou d’un type qui peut être converti en par une conversion de référence implicite, et qu’aucun identificateur de n’apparaît parmi les sous-modèles, alors nous établissons une correspondance à l’aide de .
  • Sinon, le modèle est une erreur au moment de la compilation.

L’ordre dans lequel les sous-modèles sont mis en correspondance au moment de l’exécution n’est pas spécifié, et une correspondance ayant échoué peut ne pas tenter de faire correspondre tous les sous-modèles.

Exemple

Cet exemple utilise de nombreuses fonctionnalités décrites dans cette spécification

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

Modèle de propriété

Un modèle de propriété vérifie que la valeur d’entrée n’est pas null et correspond de manière récursive aux valeurs extraites par l’utilisation de propriétés ou de champs accessibles.

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

Il s’agit d’une erreur si une de sous-modèle d’un property_pattern ne contient pas d’identificateur (il doit s’agir du deuxième formulaire, qui a un identificateur ). Une virgule de fin après le dernier sous-modèle est facultative.

Notez qu’un modèle de vérification null est hors d’un modèle de propriété trivial. Pour vérifier si la chaîne s n’est pas null, vous pouvez écrire l’un des formulaires suivants

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 {}) ...

Étant donné une correspondance d’une expression e au modèle de type {property_pattern_list}, il s’agit d’une erreur au moment de la compilation si l’expression e n’est pas compatible avec le type T désigné par type. Si le type est absent, il faut qu’il s’agit du type statique de e. Si l’identificateur est présent, il déclare une variable de modèle de type type. Chacun des identificateurs apparaissant sur la partie gauche de la property_pattern_list doit désigner une propriété ou un champ accessible en lecture de T. Si la simple_designation du property_pattern est présente, elle définit une variable de modèle de type T.

Au moment de l’exécution, l’expression est testée par rapport à T. Si cela échoue, la correspondance du modèle de propriété échoue et le résultat est false. Si elle réussit, chaque champ ou propriété property_subpattern est lu et sa valeur correspond à son modèle correspondant. Le résultat du match entier n’est false que si le résultat de l'une d'elles est false. L’ordre dans lequel les sous-modèles sont mis en correspondance n’est pas spécifié et une correspondance ayant échoué peut ne pas correspondre à tous les sous-modèles au moment de l’exécution. Si la correspondance réussit et que la simple_designation de l'property_pattern est un single_variable_designation, elle définit une variable de type T affectée à la valeur correspondante.

Remarque : Le modèle de propriété peut être utilisé pour faire correspondre des modèles avec des types anonymes.

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

Changer d’expression

Une switch_expression est ajoutée pour prendre en charge switchsémantique semblable à celle d’un contexte d’expression.

La syntaxe du langage C# est augmentée avec les productions syntaxiques suivantes :

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
    ;

La switch_expression n’est pas autorisée comme expression_statement.

Nous envisageons d'assouplir cela lors d'une révision future.

Le type de l'switch_expression est le meilleur type commun (§12.6.3.15) des expressions apparaissant à droite des jetons => du switch_expression_arm, s’il existe un tel type et que chaque expression dans chaque bras de l’expression switch peut être convertie implicitement en ce type. En outre, nous ajoutons une nouvelle conversion d’expression switch, qui est une conversion implicite prédéfinie d’une expression switch vers chaque type T pour laquelle il existe une conversion implicite de l’expression de chaque bras en T.

Il s’agit d’une erreur si certains motifs de switch_expression_armne peuvent pas affecter le résultat, car certains motifs et garde précédents correspondent toujours.

Une expression de commutateur est considérée comme exhaustive si un bras de l'expression de commutateur traite chaque valeur de son entrée. Le compilateur génère un avertissement si une expression switch n’est pas exhaustive.

Au moment de l’exécution, le résultat de l'switch_expression est la valeur de l’expression du premier switch_expression_arm pour lequel l’expression sur le côté gauche de la switch_expression correspond au modèle de switch_expression_armet pour lequel le case_guard du switch_expression_arm, s’il est présent, prend la valeur true. S’il n’existe aucun tel switch_expression_arm, le switch_expression lève une instance de l’exception System.Runtime.CompilerServices.SwitchExpressionException.

Parenthèses optionnelles lors de l'utilisation d'un littéral de tuple

Pour basculer un littéral de tuple à l'aide du switch_statement, vous devez écrire ce qui semble être des parenthèses redondantes.

switch ((a, b))
{

Pour autoriser

switch (a, b)
{

les parenthèses de l’instruction switch sont facultatives lorsque l’expression de commutation est un tuple littéral.

Ordre d’évaluation dans la mise en correspondance des modèles

La possibilité pour le compilateur de réorganiser les opérations exécutées pendant la mise en correspondance des modèles peut permettre une flexibilité qui peut être utilisée pour améliorer l’efficacité de la mise en correspondance des modèles. L’exigence (non appliquée) serait que les propriétés accessibles dans un modèle, et les méthodes Déconstruire, doivent être sans effet secondaire, idempotentes, etc., et par conséquent, « pures ». Cela ne signifie pas que nous ajouterions la pureté en tant que concept de langage, seulement que nous autoriserions la flexibilité du compilateur dans la réorganisation des opérations.

Résolution 2018-04-04 LDM: confirmé : le compilateur est autorisé à réorganiser les appels vers Deconstruct, les accès aux propriétés et les appels de méthodes dans ITuple, et peut supposer que les valeurs retournées sont identiques à partir de plusieurs appels. Le compilateur ne doit pas appeler les fonctions qui ne peuvent pas affecter le résultat, et nous serons très prudents avant d’apporter des modifications à l’ordre d’évaluation généré par le compilateur à l’avenir.

Certaines optimisations possibles

La compilation de la mise en correspondance des modèles peut tirer parti des parties communes des modèles. Par exemple, si le test de type de niveau supérieur de deux modèles successifs dans un switch_statement est le même type, le code généré peut ignorer le test de type pour le deuxième modèle.

Lorsque certains des modèles sont des entiers ou des chaînes, le compilateur peut générer le même type de code qu’il génère pour une instruction switch dans les versions antérieures du langage.

Pour plus d’informations sur ces types d’optimisations, consultez [Scott et Ramsey (2000)].