Modifications du modèle de correspondance pour C# 9.0
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 consignées dans les notes pertinentes de la réunion de conception linguistique (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 .
Nous envisageons une petite poignée d’améliorations apportées à la mise en correspondance des modèles pour C# 9.0 qui ont une synergie naturelle et fonctionnent bien pour résoudre un certain nombre de problèmes de programmation courants :
- https://github.com/dotnet/csharplang/issues/2925 Modèles de type
- https://github.com/dotnet/csharplang/issues/1350 Motifs entre parenthèses pour renforcer ou souligner la préséance des nouveaux combinateurs
- https://github.com/dotnet/csharplang/issues/1350 Modèles
and
conjonctifs qui nécessitent que deux modèles différents se correspondent tous les deux. - https://github.com/dotnet/csharplang/issues/1350Motifs
or
disjonctifs qui requièrent l'un ou l'autre de deux motifs différents pour correspondre ; - https://github.com/dotnet/csharplang/issues/1350 Modèle de correspondance négatif
not
qui exige qu'un modèle donné ne corresponde pas ; et - https://github.com/dotnet/csharplang/issues/812 modèles relationnels qui nécessitent que la valeur d’entrée soit inférieure, inférieure ou égale à, etc. une constante donnée.
Modèles entre parenthèses
Les modèles entre parenthèses permettent au programmeur de placer des parenthèses autour de n’importe quel modèle. Cela n’est pas si utile avec les modèles existants en C# 8.0, mais les nouveaux combinateurs de modèles introduisent une priorité que le programmeur peut vouloir remplacer.
primary_pattern
: parenthesized_pattern
| // all of the existing forms
;
parenthesized_pattern
: '(' pattern ')'
;
Modèles de type
Nous autorisons un type en tant que modèle.
primary_pattern
: type-pattern
| // all of the existing forms
;
type_pattern
: type
;
Cela réinterprète l’expression de type existante comme une expression de modèle dans laquelle le modèle est un motif de type , bien que nous ne modifiions pas l’arbre de syntaxe produit par le compilateur.
Un problème d’implémentation subtil est que cette grammaire est ambiguë. Une chaîne telle que a.b
peut être analysée sous la forme d’un nom qualifié (dans un contexte de type) ou d’une expression en pointillé (dans un contexte d’expression). Le compilateur est déjà capable de traiter un nom qualifié identique à une expression en pointillés afin de gérer quelque chose comme e is Color.Red
. L’analyse sémantique du compilateur serait plus étendue pour être capable de lier un modèle constant (syntactique) (par exemple, une expression en pointillé) en tant que type afin de le traiter comme un modèle de type lié afin de prendre en charge cette construction.
Après cette modification, vous serez en mesure d’écrire
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
}
}
Modèles relationnels
Les modèles relationnels permettent au programmeur d’exprimer qu’une valeur d’entrée doit satisfaire une contrainte relationnelle par rapport à une valeur 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,
};
Les modèles relationnels prennent en charge les opérateurs relationnels <
, <=
, >
et >=
sur tous les types intégrés qui prennent en charge ces opérateurs relationnels binaires avec deux opérandes du même type dans une expression. Plus précisément, nous prenons en charge tous ces modèles relationnels pour sbyte
, byte
, short
, ushort
, int
, uint
, long
, ulong
, char
, float
, double
, decimal
, nint
et nuint
.
primary_pattern
: relational_pattern
;
relational_pattern
: '<' relational_expression
| '<=' relational_expression
| '>' relational_expression
| '>=' relational_expression
;
L’expression est requise pour évaluer une valeur constante. Il s’agit d’une erreur si cette valeur constante est double.NaN
ou float.NaN
. Il s’agit d’une erreur si l’expression est une constante Null.
Lorsque l’entrée est un type pour lequel un opérateur relationnel binaire intégré approprié est défini qui s’applique à l’entrée en tant qu’opérande gauche et à la constante donnée comme opérande droit, l’évaluation de cet opérateur est prise comme signification du modèle relationnel. Sinon, nous convertissons l'entrée au type de l'expression en utilisant une conversion nullable ou de déballage explicite. Il s’agit d’une erreur au moment de la compilation si aucune conversion de ce type n’existe. Le modèle est considéré comme ne correspondant pas si la conversion échoue. Si la conversion réussit, le résultat de l’opération de correspondance de modèle est le résultat de l’évaluation de l’expression e OP v
où e
est l’entrée convertie, OP
est l’opérateur relationnel et v
est l’expression constante.
Combinateurs de motifs
Les combinateurs de modèles permettent de faire correspondre à la fois deux modèles différents en utilisant and
(ceci peut être étendu à un nombre quelconque de modèles par l'utilisation répétée de and
), l'un ou l'autre de deux modèles différents en utilisant or
(idem), ou la négation d'un modèle en utilisant not
.
Un usage courant d’un combinateur sera l’idiome
if (e is not null) ...
Plus lisible que l’idiome actuel e is object
, ce modèle exprime clairement qu’il s’agit de rechercher une valeur non null.
Les combinateurs and
et or
seront utiles pour tester les plages de valeurs
bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';
Cet exemple illustre que and
aura une priorité d’analyse plus élevée (c’est-à-dire liera plus étroitement) que or
. Le programmeur peut utiliser le modèle entre parenthèses pour rendre la priorité explicite :
bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');
Comme tous les motifs, ces combinateurs peuvent être utilisés dans n'importe quel contexte dans lequel un motif est attendu, y compris les motifs imbriqués, l'expression is-pattern, la switch-expression et le motif de l'étiquette case d'une instruction 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
;
Transition vers les ambiguïtés grammaticales 6.2.5
En raison de l’introduction du modèle de type , il est possible qu’un type générique apparaisse avant le jeton =>
. Nous ajoutons donc =>
à l’ensemble de jetons répertoriés dans §6.2.5 Ambiguïtés grammaticales pour faciliter la désambiguïsation du <
au début de la liste d’arguments de type. Voir aussi https://github.com/dotnet/roslyn/issues/47614.
Problèmes en cours avec les modifications proposées
Syntaxe pour les opérateurs relationnels
Est-ce que and
, or
et not
un mot clé contextuel ? Dans ce cas, existe-t-il un changement de rupture (par exemple, par rapport à leur utilisation en tant que désignateur dans un modèle de déclaration ).
Sémantique (par exemple, type) pour les opérateurs relationnels
Nous prévoyons de prendre en charge tous les types primitifs qui peuvent être comparés dans une expression à l’aide d’un opérateur relationnel. La signification dans les cas simples est claire
bool IsValidPercentage(int x) => x is >= 0 and <= 100;
Mais quand l’entrée n’est pas un type primitif, à quel type essayons-nous de convertir ?
bool IsValidPercentage(object x) => x is >= 0 and <= 100;
Nous avons proposé que lorsque le type d'entrée est déjà une primitive comparable, c'est le type de la comparaison. Toutefois, lorsque l’entrée n’est pas une primitive comparable, nous traitons le relationnel comme incluant un test de type implicite sur le type de la constante sur le côté droit du relationnel. Si le programmeur envisage de prendre en charge plusieurs types d’entrée, cela doit être effectué explicitement :
bool IsValidPercentage(object x) => x is
>= 0 and <= 100 or // integer tests
>= 0F and <= 100F or // float tests
>= 0D and <= 100D; // double tests
Résultat : L'expression relationnelle inclut un test de type implicite correspondant au type de la constante sur la partie droite de l'expression.
Flux d'informations de type de la gauche vers la droite de and
Il a été suggéré que lorsque vous écrivez un combinateur and
, les informations de type apprises à gauche sur le type de niveau supérieur pourraient circuler vers la droite. Par exemple
bool isSmallByte(object o) => o is byte and < 100;
Ici, le type d'entrée du deuxième motif est restreint par les exigences de restriction de type de la gauche du and
. Nous définissons la sémantique de rétrécissement de type pour tous les modèles comme suit. Le type étroit d’un modèle P
est défini comme suit :
- Si
P
est un modèle de type, le type réduit est le type du modèle de type. - Si
P
est un modèle de déclaration, le type étroit est le type du modèle de déclaration. - Si
P
est un modèle récursif qui donne un type explicite, le type étroit est ce type. - Si
P
correspond aux règles deITuple
, le type réduit est le typeSystem.Runtime.CompilerServices.ITuple
. - Si
P
est un modèle constant où la constante n’est pas la constante Null et où l’expression n’a pas de conversion d’expression constante au type d’entrée , le type étroit est le type de la constante. - Si
P
est un modèle relationnel où l’expression constante n’a pas de conversion d’expression constante vers le type d’entrée , le type étroit est le type de la constante. - Si
P
est un modèleor
, le type étroit est le type commun du type étroit des sous-modèles s’il existe un type commun. À cette fin, l'algorithme des types communs ne prend en compte que l'identité, la mise en boîte et les conversions de références implicites, et il considère tous les sous-motifs d'une séquence de motifsor
(en ignorant les motifs entre parenthèses). - Si
P
est un modèleand
, le type étroit est le type étroit du modèle droit. De plus, le type étroit du modèle gauche est le type d’entrée du modèle droit. - Dans le cas contraire, le type réduit de
P
est le type d'entrée deP
.
Résultat : la sémantique de rétrécissement ci-dessus a été implémentée.
Définitions de variables et affectation définie
L’ajout de modèles de or
et de not
crée des problèmes intéressants liés aux variables de modèle et à l’affectation définitive. Étant donné que les variables peuvent normalement être déclarées au plus une fois, il semblerait que toute variable de modèle déclarée d’un côté d’un modèle or
ne soit pas définitivement affectée lorsque le modèle correspond. De même, une variable déclarée à l'intérieur d'un motif not
ne devrait pas être définitivement affectée lorsque le motif correspond. La façon la plus simple de résoudre ce problème consiste à interdire la déclaration de variables de modèle dans ces contextes. Toutefois, cela peut être trop restrictif. Il existe d’autres approches à prendre en compte.
Un scénario qui vaut la peine d’être pris en compte est celui-ci
if (e is not int i) return;
M(i); // is i definitely assigned here?
Cela ne fonctionne pas aujourd'hui car, pour une expression is-pattern, les variables du motif sont considérées comme définitivement assignées uniquement lorsque l'expression is-pattern est vraie (« definitely assigned when true »).
Il serait plus simple (du point de vue du programmeur) de prendre en charge ce principe que d'ajouter également la prise en charge d'une instruction if
à condition négative. Même si nous ajoutons une telle prise en charge, les programmeurs se demandent pourquoi l’extrait de code ci-dessus ne fonctionne pas. D'un autre côté, le même scénario dans une switch
a moins de sens, car il n'y a pas de point correspondant dans le programme où definitely assigned when false serait significatif. Autoriserions-nous cela dans une expression is-pattern mais pas dans d'autres contextes où les patrons sont autorisés ? Ça semble irrégulier.
Cela est lié au problème de l’affectation définitive dans un modèle disjonctif .
if (e is 0 or int i)
{
M(i); // is i definitely assigned here?
}
Nous ne nous attendons à ce que i
soit définitivement assigné que lorsque l'entrée est différente de zéro. Toutefois, étant donné que nous ne savons pas si l’entrée est égale à zéro ou non à l’intérieur du bloc, i
n’est pas définitivement assignée. Cependant, que se passe-t-il si nous autorisons la déclaration de i
dans différents motifs mutuellement exclusifs ?
if ((e1, e2) is (0, int i) or (int i, 0))
{
M(i);
}
Ici, la variable i
est définitivement affectée à l’intérieur du bloc et prend la valeur de l’autre élément du tuple lorsqu’un élément zéro est trouvé.
Il a également été suggéré de permettre aux variables d’être définies (multiplier) dans chaque cas d’un bloc de cas :
case (0, int x):
case (int x, 0):
Console.WriteLine(x);
Pour effectuer l’un de ces travaux, nous devrons définir soigneusement l’endroit où de telles définitions sont autorisées et dans quelles conditions une telle variable est considérée comme définitivement affectée.
Si nous décidons de reporter ce travail à plus tard (ce que je conseille), nous pourrions dire en C# 9
- en dessous d’une
not
ou d’unor
, les variables de modèle peuvent ne pas être déclarées.
Ensuite, nous aurions le temps de développer une certaine expérience qui fournirait un aperçu de la valeur possible de la détente plus tard.
Résultat : les variables de modèle ne peuvent pas être déclarées sous un modèle not
ou or
.
Diagnostics, subsomption et exhaustivité
Ces nouveaux modèles de conception présentent de nombreuses nouvelles opportunités d'erreurs de programmeur détectables. Nous devons décider des types d’erreurs que nous diagnostiquerons et comment procéder. Voici quelques exemples :
case >= 0 and <= 100D:
Ce cas ne peut jamais correspondre (car l’entrée ne peut pas être à la fois une int
et une double
). Nous avons déjà une erreur lorsque nous détectons un cas qui ne peut jamais correspondre, mais sa formulation (« Le cas de commutation a déjà été traité par un cas précédent » et « Le modèle a déjà été traité par une branche précédente de l'expression de commutation ») peut être trompeuse dans de nouveaux scénarios. Nous devrons peut-être modifier la formulation pour simplement dire que le modèle ne correspondra jamais à l’entrée.
case 1 and 2:
De même, il s’agit d’une erreur, car une valeur ne peut pas être 1
et 2
.
case 1 or 2 or 3 or 1:
Ce cas peut correspondre, mais le or 1
à la fin n’apporte aucune signification au schéma. Je suggère que nous devrions viser à produire une erreur chaque fois qu’une certaine conjoncte ou disjonctive d’un modèle composé ne définit pas de variable de modèle ou n’affecte pas l’ensemble des valeurs correspondantes.
case < 2: break;
case 0 or 1 or 2 or 3 or 4 or 5: break;
Ici, 0 or 1 or
n’ajoute rien au deuxième cas, car ces valeurs auraient été gérées par le premier cas. Ceci mérite également d'être considéré comme une erreur.
byte b = ...;
int x = b switch { <100 => 0, 100 => 1, 101 => 2, >101 => 3 };
Une expression switch telle que celle-ci doit être considérée comme exhaustive (elle gère toutes les valeurs d’entrée possibles).
En C# 8.0, une expression switch avec une entrée de type byte
n'est considérée comme exhaustive que si elle contient un bras final dont le motif correspond à tout (discard-pattern ou var-pattern). Même une expression switch qui a un bras pour chaque valeur de byte
distincte n’est pas considérée comme exhaustive en C# 8. Afin de gérer correctement l’exhaustivité des modèles relationnels, nous devrons également gérer ce cas. Cela sera techniquement une modification majeure, mais il est peu probable qu'aucun utilisateur ne le remarque.
C# feature specifications