Partage 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é. Elle inclut les changements de spécification proposés, ainsi que les informations nécessaires à la conception et au développement de la fonctionnalité. Ces articles sont publiés jusqu'à ce que les changements proposés soient finalisés et incorporés dans la spécification ECMA actuelle.

Il peut y avoir des divergences entre la spécification de la fonctionnalité et l'implémentation réalisée. Ces différences sont consignées dans les notes pertinentes de la réunion de conception linguistique (LDM).

Pour en savoir plus sur le processus d'adoption des speclets de fonctionnalité dans la norme du langage C#, consultez l'article sur les spécifications.

Problème de champion : https://github.com/dotnet/csharplang/issues/45

Récapitulatif

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 d'expression relationnelle s'ajoute aux formes existantes dans la spécification C#. C'est une erreur compile-time si l'expression relationnelle à gauche du jeton is ne désigne pas une valeur ou n'a pas de type.

Chaque identificateur du motif introduit une nouvelle variable locale qui est définitivement assignée après que l'opérateur is est true (c'est-à-dire définitivement assignée lorsqu'elle est vraie).

Remarque : il existe techniquement une ambiguïté entre type in an is-expression et constant_pattern, l'un ou l'autre pouvant être une analyse correcte d'un identificateur qualifié. Nous essayons de la lier à un type pour des raisons de compatibilité avec les versions précédentes du langage ; ce n'est qu'en cas d'échec que nous la résolvons comme nous le faisons pour une expression dans d'autres contextes, à la première chose trouvée (qui doit être soit une constante, soit un type). Cette ambiguïté n'existe que du côté droit d'une expression is.

Modèles

Les motifs sont utilisés dans l'opérateur is_pattern, dans une switch_statement et dans une switch_expression pour exprimer la forme des données auxquelles les données entrantes (que nous appelons la valeur d'entrée) doivent être comparées. Les modèles peuvent être récursifs, de sorte que certaines parties des données peuvent être comparées à des 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
    ;

Le declaration_pattern teste qu'une expression est d'un type donné et la transforme en ce type si le test réussit. Cela peut introduire une variable locale du type donné, nommée par l'identifiant donné, si la désignation est une single_variable_designation. Cette variable locale est définitivement affectée lorsque le résultat de l'opération de recherche de motifs est true.

La sémantique d'exécution de cette expression est qu'elle teste le type d'exécution de l'opérande de l'expression relationnelle de gauche par rapport au type du motif. Si elle est de ce type d'exécution (ou d'un sous-type) et non de null, le résultat de is operator est true.

Certaines combinaisons du 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 le type est un type de valeur nullable.

Le modèle de correspondance peut être utilisé pour tester les valeurs de types nullables : une valeur de type Nullable<T> (ou un T encadré) correspond à un modèle de correspondance T2 id si la valeur est non nulle et si le type de T2 est T, ou un certain 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 à 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.

Motif constant

constant_pattern
    : constant_expression
    ;

Un modèle de constante teste la valeur d'une expression par rapport à une valeur de 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 dans le type de l'expression correspondante ; si le type de la valeur d'entrée n'est pas pattern-compatible avec le type de l'expression constante, l'opération de correspondance de modèle est une erreur.

Le motif c est considéré comme correspondant à la valeur d'entrée convertie e si object.Equals(c, e) renvoie true.

Nous nous attendons à ce que e is null soit le moyen le plus courant de tester null dans un code nouvellement écrit, car il ne peut pas invoquer 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 une simple_designation, une expression e correspond au motif. En d'autres termes, une correspondance avec un modèle var réussit toujours avec une simple_designation. Si la simple_designation est une 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 une tuple_designation, le motif est équivalent à un motif positionnel de la forme (vardésignation,.... ), où les désignations sont celles que l'on trouve dans le tuple_designation. Par exemple, le motif var (x, (y, z)) est équivalent à (var x, (var y, var z)).

Une erreur se produit si le nom var est lié à un type.

Jeter le modèle

discard_pattern
    : '_'
    ;

Une expression e correspond toujours au motif _. 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.

Motif positionnel

Un modèle de correspondance vérifie que la valeur d'entrée n'est pas null, invoque une méthode Deconstruct appropriée et effectue d'autres correspondances sur les valeurs résultantes. Il prend également en charge une syntaxe de motif de type tuple (sans que le type soit fourni) lorsque le type de la valeur d'entrée est le même que le 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, nous considérons qu'il s'agit du type statique de la valeur d'entrée.

Si une valeur d'entrée correspond au motif type(subpattern_list), une méthode est sélectionnée en recherchant dans type les déclarations accessibles de Deconstruct et en sélectionnant l'une d'entre elles à l'aide des mêmes règles que pour la déclaration de déconstruction.

Une erreur est commise si un positional_pattern omet le type, a un seul sous-motif sans identifiant, n'a pas de sous-motif de propriété et n'a pas de simple_designation. Cela désambiguïse entre un constant_pattern placé entre parenthèses et un positional_pattern.

Afin d'extraire les valeurs à comparer aux modèles de la liste,

  • Si le type a été omis et que le type de la valeur d'entrée est un type de tuple, le nombre de sous-motifs doit être identique à la cardinalité du tuple. Chaque élément du n-uplet est comparé au sous-modèle correspondant, et la comparaison est réussie si tous les éléments sont réussis. Si un sous-modèle possède un identifiant, celui-ci doit nommer un élément de n-uplet à la position correspondante dans le type de n-uplet.
  • Sinon, si un Deconstruct approprié existe en tant que membre de type, il s'agit d'une erreur compile-time si le type de la valeur d'entrée n'est pas compatible avec le motif type. Au moment de l'exécution, la valeur d'entrée est testée par rapport au type. Si cela échoue, le modèle de correspondance positionnelle échoue. En cas de succès, la valeur d'entrée est convertie en ce type et Deconstruct est invoqué avec de nouvelles variables générées par le compilateur pour recevoir les paramètres out. Chaque valeur reçue est comparée au sous-motif correspondant, et la correspondance est réussie si tous les sous-motifs 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 .
  • Dans le cas contraire, le motif est une erreur compile-time.

L'ordre dans lequel les sous-motifs sont comparés au moment de l'exécution n'est pas spécifié, et une correspondance échouée peut ne pas essayer de correspondre à tous les sous-motifs.

Exemple

Cet exemple utilise plusieurs des fonctionnalités décrites dans la présente 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 correspondance vérifie que la valeur d'entrée n'est pas null et fait correspondre de manière récursive les 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 un sous-motif d'un motif de propriété ne contient pas d'identificateur (il doit être de la deuxième forme, qui a un identificateur). La virgule qui suit le dernier sous-motif est facultative.

Notez qu'un motif de vérification de la nullité ne fait pas partie d'un motif de propriété trivial. Pour vérifier si la chaîne s est non nulle, vous pouvez écrire l'une des formes suivantes

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, nous considérons qu'il s'agit du type statique de e. Si l'identificateur est présent, il déclare une variable motif 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. En cas d'échec, la correspondance des propriétés échoue et le résultat est false. En cas de succès, chaque champ ou propriété de property_subpattern est lu et sa valeur est comparée au motif 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-motifs sont comparés n'est pas spécifié, et une correspondance échouée peut ne pas correspondre à tous les sous-motifs au moment de l'exécution. Si la correspondance réussit et que la simple_designation du property_pattern est une single_variable_designation, elle définit une variable de type T à laquelle est attribuée la valeur correspondante.

Remarque : le modèle de propriété peut être utilisé pour la recherche de motifs avec des types anonymes.

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

Expression de commutation

Une switch_expression est ajoutée pour prendre en charge la sémantique switch pour un contexte d'expression.

La syntaxe du langage C# est enrichie des 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 de commutation, qui est une conversion implicite prédéfinie d'une expression de commutation vers chaque type T pour lequel il existe une conversion implicite de l'expression de chaque bras vers 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 émet un avertissement si une expression de commutation n'est pas exhaustive.

Au moment de l'exécution, le résultat de la switch_expression est la valeur de l'expression de la première branche de la switch_expression pour laquelle l'expression du côté gauche de la switch_expression correspond au modèle de la branche de la switch_expression, et pour laquelle la case_guard de la branche de la switch_expression, si elle est présente, est évaluée à true. S'il n'existe pas de switch_expression_arm, le switch_expression lance 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 permettre

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 recherche de motifs

Donner au compilateur la possibilité de réordonner les opérations exécutées lors du pattern-matching peut permettre une flexibilité qui peut être utilisée pour améliorer l'efficacité du pattern-matching. 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é comme concept de langage, mais seulement que nous donnerions au compilateur une certaine flexibilité dans le réordonnancement des opérations.

Résolution 2018-04-04 LDM : confirmé : le compilateur est autorisé à réordonner les appels à Deconstruct, les accès aux propriétés et les invocations de méthodes dans ITuple, et peut supposer que les valeurs retournées sont les mêmes à partir d'appels multiples. Le compilateur ne devrait pas invoquer des fonctions qui ne peuvent pas affecter le résultat, et nous serons très prudents avant d'apporter des changements à l'ordre d'évaluation généré par le compilateur à l'avenir.

Quelques optimisations possibles

La compilation de la correspondance de motifs peut tirer parti de parties communes des motifs. Par exemple, si le test de type de premier niveau de deux motifs successifs dans un switch_statement est du même type, le code généré peut sauter le test de type pour le deuxième motif.

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

Pour plus d'informations sur ce type d'optimisations, voir [Scott and Ramsey (2000)].