Pour compléter les réponses apportées ici, je pense qu'il est utile de se poser la question inverse, à savoir : pourquoi C a-t-il autorisé la chute en premier lieu ?
Tout langage de programmation a bien sûr deux objectifs :
- Fournir des instructions à l'ordinateur.
- Laisser une trace des intentions du programmeur.
La création d'un langage de programmation est donc un équilibre entre ces deux objectifs. D'une part, plus il est facile de transformer un langage en instructions informatiques (qu'il s'agisse de code machine, de bytecode comme IL, ou d'instructions interprétées lors de l'exécution), plus le processus de compilation ou d'interprétation sera efficace, fiable et compact. Poussé à l'extrême, cet objectif nous amène à n'écrire que de l'assemblage, de l'IL ou même des op-codes bruts, car la compilation la plus facile est celle où il n'y a pas de compilation du tout.
Inversement, plus le langage exprime l'intention du programmeur, plutôt que les moyens mis en œuvre pour y parvenir, plus le programme est compréhensible, tant lors de sa rédaction que lors de sa maintenance.
Aujourd'hui, switch
aurait toujours pu être compilé en le convertissant en une chaîne équivalente de if-else
ou similaire, mais il a été conçu pour permettre la compilation dans un modèle d'assemblage commun particulier où l'on prend une valeur et calcule un décalage à partir de cette valeur (soit en consultant une table indexée par un hachage parfait de la valeur, soit par une arithmétique réelle sur la valeur*). Il est intéressant de noter ici qu'aujourd'hui, la compilation C# peut parfois tourner à l'erreur. switch
en l'équivalent if-else
et utilisent parfois une approche de saut basée sur le hachage (de même pour le C, le C++ et d'autres langages ayant une syntaxe comparable).
Dans ce cas, il y a deux bonnes raisons d'autoriser les retombées :
-
De toute façon, cela se fait naturellement : si vous intégrez une table de sauts dans un ensemble d'instructions et que l'un des premiers lots d'instructions ne contient pas une sorte de saut ou de retour, l'exécution passera naturellement au lot suivant. Permettre le fall-through, c'est ce qui se passerait "tout simplement" si l'on mettait l'option switch
-en utilisant le C en code machine utilisant la table de saut.
-
Les codeurs qui écrivaient en assembleur étaient déjà habitués à l'équivalent : lorsqu'ils écrivaient une table de saut à la main en assembleur, ils devaient se demander si un bloc de code donné se terminerait par un retour, un saut en dehors de la table ou s'il continuerait simplement jusqu'au bloc suivant. En tant que tel, le fait de demander au codeur d'ajouter un break
lorsque c'est nécessaire était "naturel" pour le codeur également.
À l'époque, il s'agissait donc d'une tentative raisonnable d'équilibrer les deux objectifs d'un langage informatique en ce qui concerne à la fois le code machine produit et l'expressivité du code source.
Quatre décennies plus tard, les choses ne sont plus tout à fait les mêmes, et ce pour plusieurs raisons :
- Les codeurs en C d'aujourd'hui peuvent avoir peu ou pas d'expérience en matière d'assemblage. Les codeurs de nombreux autres langages de type C sont encore moins susceptibles de l'être (en particulier Javascript !). Tout concept de "ce à quoi les gens sont habitués avec l'assemblage" n'est plus pertinent.
- Grâce à l'amélioration des optimisations, la probabilité d'obtenir un résultat positif est plus élevée.
switch
soit être transformé en if-else
parce qu'elle était considérée comme l'approche susceptible d'être la plus efficace, ou qu'elle s'est transformée en une variante particulièrement ésotérique de l'approche de la table de saut sont plus élevés. La correspondance entre les approches de niveau supérieur et de niveau inférieur n'est plus aussi forte qu'elle l'était auparavant.
- L'expérience a montré que les retombées tendent à être un cas minoritaire plutôt que la norme (une étude du compilateur de Sun a révélé que 3 % des retombées étaient des retombées de l'utilisation du compilateur).
switch
Les blocs de l'UE ont utilisé une méthode autre que les étiquettes multiples sur le même bloc, et l'on a estimé que le cas d'utilisation ici signifiait que ces 3 % étaient en fait beaucoup plus élevés que la normale). Ainsi, la langue telle qu'elle a été étudiée permet de répondre plus facilement aux besoins inhabituels qu'aux besoins courants.
- L'expérience a montré que les retombées ont tendance à être la source de problèmes à la fois dans les cas où elles sont effectuées accidentellement et dans les cas où les retombées correctes ne sont pas prises en compte par quelqu'un qui maintient le code. Ce dernier cas est un ajout subtil aux bogues associés aux retombées, car même si votre code est parfaitement exempt de bogues, vos retombées peuvent toujours causer des problèmes.
En rapport avec ces deux derniers points, voici une citation tirée de l'édition actuelle de K&R :
Le passage d'un cas à l'autre n'est pas robuste, car il est susceptible de se désintégrer lorsque le programme est modifié. À l'exception des étiquettes multiples pour un seul calcul, les transitions doivent être utilisées avec parcimonie et commentées.
Pour des raisons de forme, il convient d'insérer une pause après le dernier cas (par défaut), même si cela n'est pas nécessaire d'un point de vue logique. Un jour, lorsqu'un autre cas sera ajouté à la fin, ce petit effort de programmation défensive vous sauvera.
Ainsi, de l'avis même des responsables, les retombées en C sont problématiques. Il est considéré comme une bonne pratique de toujours documenter les retombées avec des commentaires, ce qui est une application du principe général selon lequel il faut documenter les endroits où l'on fait quelque chose d'inhabituel, parce que c'est ce qui fera trébucher l'examen ultérieur du code et/ou donnera l'impression que votre code contient un bogue de novice alors qu'il est en fait correct.
Et quand on y pense, le code est comme ça :
switch(x)
{
case 1:
foo();
/* FALLTHRU */
case 2:
bar();
break;
}
Est ajouter quelque chose pour rendre le fall-through explicite dans le code, ce n'est tout simplement pas quelque chose qui peut être détecté (ou dont l'absence peut être détectée) par le compilateur.
En tant que tel, le fait que l'on doive être explicite avec le fall-through en C# n'ajoute aucune pénalité pour les personnes qui écrivent bien dans d'autres langages de style C de toute façon, puisqu'elles seraient déjà explicites dans leurs fall-throughs.†
Enfin, l'utilisation de goto
est déjà une norme du langage C et d'autres langages de ce type :
switch(x)
{
case 0:
case 1:
case 2:
foo();
goto below_six;
case 3:
bar();
goto below_six;
case 4:
baz();
/* FALLTHRU */
case 5:
below_six:
qux();
break;
default:
quux();
}
Dans ce genre de cas où l'on veut qu'un bloc soit inclus dans le code exécuté pour une valeur autre que celle qui amène au bloc précédent, il faut déjà utiliser goto
. (Bien sûr, il y a des moyens et des façons d'éviter cela avec des conditionnels différents, mais c'est vrai pour à peu près tout ce qui concerne cette question). En tant que tel, C# s'est appuyé sur la manière déjà normale de traiter une situation dans laquelle nous voulons frapper plus d'un bloc de code dans un switch
et l'a généralisé pour couvrir également les retombées. Cela a également rendu les deux cas plus pratiques et auto-documentés, puisque nous devons ajouter une nouvelle étiquette en C mais que nous pouvons utiliser la fonction case
comme étiquette en C#. En C#, nous pouvons nous débarrasser du below_six
étiquette et utilisation goto case 5
qui indique plus clairement ce que nous faisons. (Nous devrions également ajouter break
pour les default
que j'ai laissé de côté pour que le code C ci-dessus ne soit clairement pas du code C#).
En résumé donc :
- Le C# n'est plus lié à la sortie non optimisée du compilateur aussi directement que le code C l'était il y a 40 ans (et le C ne l'est pas non plus aujourd'hui), ce qui rend l'une des sources d'inspiration du fall-through hors de propos.
- C# reste compatible avec C en ne se contentant pas d'avoir des
break
L'utilisation de l'anglais comme langue de travail facilite l'apprentissage du langage par ceux qui sont familiers avec des langages similaires, ainsi que le portage.
- C# supprime une source possible de bogues ou de code mal compris qui a été bien documentée comme causant des problèmes au cours des quatre dernières décennies.
- C# rend les meilleures pratiques existantes en C (document fall through) applicables par le compilateur.
- Avec C#, le cas inhabituel est celui où le code est le plus explicite, et le cas habituel est celui où le code est écrit automatiquement.
- C# utilise le même
goto
-pour frapper le même bloc à partir de différents endroits. case
comme c'est le cas en C. Il le généralise simplement à d'autres cas.
- C# rend cela possible
goto
-plus pratique et plus claire qu'en C, en permettant à l'utilisateur d'accéder à l'ensemble des données de la base de données. case
pour servir d'étiquettes.
Dans l'ensemble, il s'agit d'une décision de conception assez raisonnable
*Certaines formes de BASIC permettent de faire des choses comme GOTO (x AND 7) * 50 + 240
qui, tout en étant fragile, constitue un argument particulièrement convaincant en faveur de l'interdiction. goto
En revanche, il sert à montrer un équivalent en langage supérieur de la manière dont le code de niveau inférieur peut effectuer un saut basé sur l'arithmétique d'une valeur, ce qui est beaucoup plus raisonnable lorsqu'il s'agit du résultat d'une compilation plutôt que de quelque chose qui doit être maintenu manuellement. Les implémentations du dispositif de Duff en particulier se prêtent bien au code machine équivalent ou à l'IL parce que chaque bloc d'instructions aura souvent la même longueur sans nécessiter l'ajout de nop
des produits de remplissage.
†Duff's Device revient ici, en tant qu'exception raisonnable. Le fait qu'il y ait une répétition des opérations dans ce cas et dans d'autres cas similaires permet de rendre l'utilisation des retombées relativement claire, même en l'absence d'un commentaire explicite à cet effet.