Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Note
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 capturées dans les notes de réunion de conception de langage (LDM) pertinentes .
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.
- Problème défendu : https://github.com/dotnet/csharplang/issues/9875
Résumé
Autorisez break et continue les instructions à spécifier éventuellement une étiquette qui identifie la boucle ou switch l’instruction à cibler, ce qui permet un flux de contrôle plus propre dans les constructions imbriquées sans nécessiter goto d’instructions ou d’autres contorsions telles que les fonctions imbriquées, les retours de tuple, etc.
Motivation
Lors de l’utilisation de boucles ou de boucles imbriquées contenant switch des instructions, les développeurs doivent souvent sortir ou continuer une boucle externe à partir d’un contexte interne. Actuellement, il existe deux approches principales pour y parvenir, à la fois avec des inconvénients importants :
Utilisation d’instructions goto
string foundValue = null;
for (int x = 0; x < xMax; x++)
{
for (int y = 0; y < yMax; y++)
{
foundValue = GetValue(x, y);
if (foundValue == target)
goto FOUND;
}
}
FOUND:
ProcessValue(foundValue);
Bien que goto cela fonctionne, il nécessite de placer des étiquettes après la construction de la boucle et ne communique pas clairement l’intention de s’arrêter d’une boucle spécifique. Pour poursuivre une boucle externe, l’approche devient encore plus maladroite :
for (int x = 0; x < xMax; x++)
{
for (int y = 0; y < yMax; y++)
{
if (ShouldSkipRest(x, y))
goto CONTINUE_OUTER;
}
CONTINUE_OUTER: ;
}
Ce modèle est confus, car l’étiquette doit être placée à la fin du corps de la boucle, juste avant l’accolade fermante, afin que l’incrémenteur et la vérification de la condition se produisent. Quand les deux break et continue sont nécessaires pour la même boucle externe, deux étiquettes distinctes sont requises :
for (int x = 0; x < xMax; x++)
{
for (int y = 0; y < yMax; y++)
{
if (ShouldSkipRest(x, y))
goto CONTINUE_OUTER;
if (ShouldExitAll(x, y))
goto BREAK_OUTER;
}
CONTINUE_OUTER: ;
}
BREAK_OUTER:
// Subsequent statements
Utilisation de variables d’indicateur
string foundValue = null;
bool shouldBreak = false;
for (int x = 0; x < xMax; x++)
{
for (int y = 0; y < yMax; y++)
{
foundValue = GetValue(x, y);
if (foundValue == target)
{
shouldBreak = true;
break;
}
}
if (shouldBreak)
break;
}
ProcessValue(foundValue);
Cette approche nécessite une gestion d’état supplémentaire, augmente la détail du code et masque l’intention du flux de contrôle.
Solution proposée
Avec étiqueté break et continue, le code devient plus clair et plus gérable :
string foundValue = null;
outer: for (int x = 0; x < xMax; x++)
{
for (int y = 0; y < yMax; y++)
{
foundValue = GetValue(x, y);
if (foundValue == target)
break outer;
}
}
ProcessValue(foundValue);
L’étiquette est placée directement sur la boucle qu’elle identifie, et l’instruction break/continue nomme explicitement sa cible. Pour continuer :
outer: for (int x = 0; x < xMax; x++)
{
for (int y = 0; y < yMax; y++)
{
if (ShouldSkipRest(x, y))
continue outer;
}
}
Cela exprime naturellement « continuer la boucle externe », sans confusion du placement d’étiquette associé à goto. Une seule étiquette peut être utilisée pour les deux opérations :
outer: for (int x = 0; x < xMax; x++)
{
for (int y = 0; y < yMax; y++)
{
if (ShouldSkipRest(x, y))
continue outer;
if (ShouldExitAll(x, y))
break outer;
}
}
Cette fonctionnalité a été demandée en grande partie dans la communauté C#, avec des discussions datant des décennies et le sujet réintroduite et reréquesté en continu. Des fonctionnalités similaires existent dans plusieurs autres langages modernes :
- Java : instructions de branchement (didacticiel Oracle)
- JavaScript : instruction étiquetée (MDN)
- Kotlin : Retours et sauts
- Swift : Flux de contrôle - Instructions étiquetées
- Rust : étiquettes de boucle
- Go : Instructions étiquetées
- Zig : boucles étiquetées
- Dart : Boucles
Dans tous ces cas, les langues fonctionnent de la même façon que dans cette spécification. À savoir, certaines constructions peuvent avoir une étiquette et il est possible de référencer cette étiquette à partir de leurs instructions continue respectives.break
Conception détaillée
Les mises à jour suivantes sont présentées sous forme de différences par rapport aux sections correspondantes de la norme C# 7 (statements.md).
Tout au long de cette section, la frappe indique que le texte est supprimé de la spécification existante et en gras indique que le texte est ajouté. La prose inchangée est entre guillemets verbatim pour le contexte.
§13.5 Instructions étiquetées
Insérez le paragraphe suivant immédiatement après le paragraphe existant « Une étiquette peut être référencée à partir d’instructions goto (§13.10.4) dans la portée de l’étiquette . » :
Si l’instruction est immédiatement imbriquée dans un labeled_statement est un switch_statement (§13.8.3) ou un iteration_statement (§13.9), l’instruction imbriquée est dite étiquetée avecl’identificateur du labeled_statement. Un break_statement (§13.10.2) ou continue_statement (§13.10.3) peut spécifier un tel identificateur pour référencer l’instruction étiquetée contenante.
Remarque : seule l’instruction qui est immédiatement imbriquée dans un labeled_statement est étiquetée avec cet identificateur. Par exemple, étant donné a: b: while (…) …, seules b les étiquettes de la iteration_statement ; a étiquettent le labeled_statementb: while (…) … interne, ce qui n’est pas lui-même un switch_statement ou iteration_statement. Par conséquent, break a; ou continue a; l’affichage dans le corps de la boucle ne cible pas l’instruction while .
fin de la remarque
§13.10.2 L’instruction break
break_statement
: 'break' identifier? ';'
;
L’instruction break quitte l’instruction englobante switchla plus proche, , whiledo, forou foreach l’instruction.
L’instruction break quitte l’switch_statement englobante la plus proche (§13.8.3) ou iteration_statement (§13.9), ou, si un identificateur est spécifié, le switch_statement le plus proche ou iteration_statement étiqueté avec cet identificateur (voir §13.5).
La cible d’une break instruction est le point de terminaison de l’instruction englobante la plus déterminée comme ci-dessus.
switchproche , while, do, forou foreach l’instruction d’instructionSi une instruction n’est pas entourée d’une Si aucune instruction englobante n’existe, une erreur au moment de la compilation se produit.breakswitchinstruction , ou foreachwhiledoford’une instruction, une erreur au moment de la compilation se produit.
Lorsque plusieurs switchinstructions sont whiledoforforeachimbriquées entre elles, une break instruction s’applique uniquement à l’instruction la plus interne. Pour transférer le contrôle sur plusieurs niveaux d’imbrication, une goto instruction (§13.10.4) doit être utilisée.
Une break instruction ne peut pas quitter un finally bloc (§13.11). Lorsqu’une break instruction se produit dans un finally bloc, la cible de l’instruction doit se trouver dans le même break bloc ; sinon, une erreur au moment de la finally compilation se produit.
Une break instruction est exécutée comme suit :
- Si l’instruction
breakquitte un ou plusieurstryblocs avec des blocs associésfinally, le contrôle est initialement transféré vers lefinallybloc de l’instruction la plustryinterne. Quand et si le contrôle atteint le point de terminaison d’unfinallybloc, le contrôle est transféré vers lefinallybloc de l’instruction englobantetrysuivante. Ce processus est répété jusqu’à ce que lesfinallyblocs de toutes les instructions intermédiairestryaient été exécutés. - Le contrôle est transféré vers la cible de l’instruction
break.
Étant donné qu’une break instruction transfère sans condition le contrôle ailleurs, le point de terminaison d’une break instruction n’est jamais accessible.
Exemple : une étiquette
breakest résolue en switch_statement ou iteration_statement englobante la plus proche avec l’étiquette correspondante :outer: for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { if (i * j > 20) break outer; // exits the outer for-loop } }exemple de fin
§13.10.3 L’instruction continue
continue_statement
: 'continue' identifier? ';'
;
L’instruction continue démarre une nouvelle itération de l’instruction englobante la plus whileproche, ou doforforeach de l’instruction.
L’instruction continue démarre une nouvelle itération du iteration_statement englobant le plus proche (§13.9) ou, si un identificateur est spécifié, le plus proche englobant iteration_statement étiqueté avec cet identificateur (voir §13.5).
La cible d’une continue instruction est le point de terminaison de l’instruction incorporée de l’instruction englobante la plus de l’instruction iteration_statement déterminée comme ci-dessus.
whileproche, doou forforeachSi une Si aucune instruction englobante n’existe, une erreur au moment de la compilation se produit.continue instruction n’est pas entourée d’une whileinstruction , doou forforeach d’une instruction, une erreur au moment de la compilation se produit.
Lorsque plusieurs whileinstructions sont doforforeach imbriquées entre elles, une continue instruction s’applique uniquement à l’instruction la plus interne. Pour transférer le contrôle sur plusieurs niveaux d’imbrication, une goto instruction (§13.10.4) doit être utilisée.
Une continue instruction ne peut pas quitter un finally bloc (§13.11). Lorsqu’une continue instruction se produit dans un finally bloc, la cible de l’instruction doit se trouver dans le même continue bloc ; sinon, une erreur au moment de la finally compilation se produit.
Une continue instruction est exécutée comme suit :
- Si l’instruction
continuequitte un ou plusieurstryblocs avec des blocs associésfinally, le contrôle est initialement transféré vers lefinallybloc de l’instruction la plustryinterne. Quand et si le contrôle atteint le point de terminaison d’unfinallybloc, le contrôle est transféré vers lefinallybloc de l’instruction englobantetrysuivante. Ce processus est répété jusqu’à ce que lesfinallyblocs de toutes les instructions intermédiairestryaient été exécutés. - Le contrôle est transféré vers la cible de l’instruction
continue.
Étant donné qu’une continue instruction transfère sans condition le contrôle ailleurs, le point de terminaison d’une continue instruction n’est jamais accessible.
Exemple : une étiquette
continueest résolue en iteration_statement englobante la plus proche avec l’étiquette correspondante :outer: for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { if (ShouldSkip(i, j)) continue outer; // continues the outer for-loop } }exemple de fin
Inconvénients/alternatives
Conserver l’utilisation des goto instructions
C# prend déjà en charge goto, ce qui peut accomplir le même flux de contrôle. Toutefois, goto il existe plusieurs inconvénients par rapport à l’arrêt/à la poursuite étiqueté :
- Nécessite des étiquettes distinctes pour les scénarios d’arrêt et de poursuite (les étiquettes d’arrêt suivent la boucle, continuez les étiquettes avant l’accolade fermante)
- Le placement des étiquettes est moins intuitif et diffère selon que vous êtes cassant ou continu
- Moins explicite sur l’intention (saut à un emplacement par rapport à la rupture/à la poursuite d’une boucle spécifique)
- Sujets à l’erreur et cassants : les développeurs doivent s’assurer qu’aucune instruction n’est placée accidentellement entre les étiquettes et leurs constructions cibles. Par exemple, avec
goto END_LOOP;suivi deEND_LOOP:, il est facile d’insérer par inadvertance une instruction entre elles pendant la maintenance, en cassant le flux de contrôle prévu. Les boucles étiquetées empêchent ce problème en liant l’étiquette directement à la construction. - Porte la stigmatisation historique qui a étiqueté rupture/continuer à éviter
Utiliser des variables d’indicateur
Comme indiqué dans la section motivation, les variables d’indicateur fonctionnent, mais ajoutent une réutilisable significative et masquent la logique de flux de contrôle.
Utiliser break N ou continue N avec des niveaux numériques
- Fragile lors de la refactorisation (ajout/suppression d’un niveau de boucle nécessite la mise à jour de toutes les références numériques)
- Plus difficile à lire (doit compter les niveaux pour comprendre la cible)
- Moins explicite que les étiquettes nommées
- Manque de clarté (basé sur 1 ? basé sur 0 ?)
Refactoriser en méthodes distinctes
Bien que cela soit souvent une bonne pratique, il n’est pas toujours possible ou approprié, et parfois introduit une complexité inutile pour ce qui doit être un flux de contrôle simple.
Discussions et questions connexes
Cette proposition consolide et aborde les discussions communautaires suivantes :
- Discussion #6634 : Boucle imbriquée C#
- Problème #869 : Discussion : Boucle imbriquée C#
- Discussion #5525 : [Proposition] Boucles étiquetées comme dans Java
- Problème #1597 : [Proposition] Boucles étiquetées comme dans Java
- Discussion #5521 : interruption des boucles imbriquées avec l’arrêt X, continuez X
- Problème #4109 : [Proposition] : Sucre syntactique pour la rupture ou la poursuite des boucles imbriquées
- Problème #3511 : [Proposition] « doublecontine », à la boucle externe contine
- Problème #2024 : arrêt et poursuite des inhancements
- Discussion #8434 : Instructions de flux de contrôle chaînées : arrêt [, arrêt]... [,continue]
Questions ouvertes
sémantique des étiquettes
La spécification actuelle définit la sémantique de break identifier/continue identifier recherche de la construction de boucle/commutateur applicable la plus interne étiquetée avec cet identificateur, puis de la distribuer avec la sémantique standard.break/continue Une autre formalisation consiste à dire que break identifier/continue identifier l’identification d’une étiquette utilise les mêmes règles que « goto ». Et si l’étiquette contient directement une construction de boucle/commutateur qui entoure l’arrêt/continue, il s’agit de la boucle/commutateur auquel l’arrêt/continue s’applique.
Les deux formalisations sont effectivement identiques, ce qui permet et désalloitent le même ensemble de programmes. L’approche choisie dans ce speclet a été effectuée pour la simplicité conceptuelle et littérale. Il n’est pas nécessaire de couvrir l’étendue des étiquettes, ni la liaison d’identificateurs comme goto il le fait, ou d’avoir à définir la logique de liaison vers l’extérieur pour résoudre la boucle/switch et continuer/arrêter l’instruction. Au lieu de cela, il développe simplement le langage de spécification simple qui trouve la boucle/commutateur englobant approprié en fonction de l’arrêt/continue, ce qui lui permet d’étendre au-delà de l’intérieur, à quelque chose au-dessus de cela.
Si LDM semble lier cette sémantique plus serrée à goto+label, il n’est pas difficile d’ajuster la spécification à cela. Garder cette question ouverte si le groupe estime que la dernière forme est plus naturelle que celle prise ici.
void M()
{
label:
Console.WriteLine();
foreach (var x in ...)
{
break label;
// should this scenario fail because:
// 1. the identifier lookup fails, or
// 2. the label is rejected (not a valid label for a `break` since not attached to a loop construct) after being found?
}
}
Spécifications sur la façon dont les étiquettes sont déclarées :
Chaque bloc ou switch_block crée un espace de déclaration distinct pour les étiquettes. Les noms sont introduits dans cet espace de déclaration via labeled_statements, et les noms sont référencés via goto_statements. L’espace de déclaration d’étiquette d’un bloc inclut tous les blocs imbriqués. Par conséquent, dans un bloc imbriqué, il n’est pas possible de déclarer une étiquette portant le même nom qu’une étiquette dans un bloc englobant.
Spécifications sur les instructions étiquetées :
L’étendue d’une étiquette est le bloc entier dans lequel l’étiquette est déclarée, y compris les blocs imbriqués. Il s’agit d’une erreur au moment de la compilation pour deux étiquettes portant le même nom pour avoir des étendues qui se chevauchent.
Une étiquette peut être référencée à partir d’instructions goto (§13.10.4) dans la portée de l’étiquette.
Étiquettes imbriquées
Doit-il a: b: while (true) continue a; être pris en charge ?
Recommandation : non. Aucun utilisateur n’a demandé cela. Aucun cas d’usage convaincant n’a été présenté. La plupart des langues standard ne l’autorisent pas, sans plainte de leurs communautés. Le langage (et l’impl) sont plus simples et plus clairs apportent strict que seules les étiquettes d’instruction étiquetées directes étiquettes de la boucle/commutateur.
Concevoir des réunions
À déterminer
C# feature specifications