Pour ajouter aux réponses ici, je pense qu'il vaut la peine de considérer la question opposée conjointement avec cela, à savoir. pourquoi C a-t-il permis la chute en premier lieu?
Tout langage de programmation sert bien entendu deux objectifs:
- Fournissez des instructions à l'ordinateur.
- Laisser un enregistrement des intentions du programmeur.
La création de tout langage de programmation est donc un équilibre entre la meilleure façon de servir ces deux objectifs. D'une part, plus il est facile de se transformer en instructions informatiques (qu'il s'agisse de code machine, de bytecode comme IL ou que les instructions soient interprétées lors de l'exécution), plus le processus de compilation ou d'interprétation sera efficace, fiable et compact en sortie. Poussé à son extrême, cet objectif se traduit par notre simple écriture en assembleur, en IL ou même en op-codes bruts, car la compilation la plus simple est celle où il n'y a pas du tout de compilation.
Inversement, plus le langage exprime l'intention du programmeur, plutôt que les moyens utilisés à cette fin, plus le programme est compréhensible à la fois lors de l'écriture et pendant la maintenance.
Maintenant, switch
aurait toujours pu être compilé en le convertissant en une chaîne équivalente de if-else
blocs ou similaire, mais il a été conçu comme permettant la compilation dans un modèle d'assemblage commun particulier où l'on prend une valeur, en calcule un décalage (que ce soit en recherchant une table indexé par un hachage parfait de la valeur, ou par une arithmétique réelle sur la valeur *). Il convient de noter à ce stade qu'aujourd'hui, la compilation C # se transformera parfois switch
en équivalent if-else
et utilisera parfois une approche de saut basée sur le hachage (et de même avec C, C ++ et d'autres langages avec une syntaxe comparable).
Dans ce cas, il y a deux bonnes raisons pour autoriser les retards:
Cela se produit naturellement de toute façon: si vous créez une table de sauts dans un ensemble d'instructions et que l'un des premiers lots d'instructions ne contient pas de saut ou de retour, l'exécution progressera naturellement dans le lot suivant. Permettre les retombées était ce qui "arriverait" si vous tourniez laswitch
C en utilisant une table de saut à l'aide du code machine.
Les codeurs qui écrivaient dans l'assembly étaient déjà habitués à l'équivalent: lors de l'écriture manuelle d'une table de sauts dans l'assembly, ils devaient se demander si un bloc de code donné se terminerait par un retour, un saut en dehors de la table ou simplement continuer au bloc suivant. En tant que tel, avoir le codeur ajouter un explicite break
lorsque cela était nécessaire était "naturel" pour le codeur aussi.
Il s'agissait donc à l'époque 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.
Cependant, quatre décennies plus tard, les choses ne sont pas tout à fait les mêmes, pour plusieurs raisons:
- Les codeurs en C d'aujourd'hui peuvent avoir peu ou pas d'expérience en assemblage. Les codeurs dans de nombreux autres langages de style C sont encore moins susceptibles (en particulier Javascript!). Tout concept de «ce à quoi les gens sont habitués de l'assemblage» n'est plus pertinent.
- Des améliorations dans les optimisations signifient que la probabilité de
switch
devenir soit if-else
parce qu'elle a été considérée comme l'approche susceptible d'être la plus efficace, soit de devenir une variante particulièrement ésotérique de l'approche saut-table est plus élevée. La correspondance entre les approches de niveau supérieur et inférieur n'est pas aussi solide qu'auparavant.
- L'expérience a montré que les retombées ont tendance à être le cas minoritaire plutôt que la norme (une étude du compilateur de Sun a révélé que 3% des
switch
blocs utilisaient une jonction autre que plusieurs étiquettes sur le même bloc, et on pensait que l'utilisation - cas ici signifiait que ces 3% étaient en fait beaucoup plus élevés que la normale). Ainsi, la langue étudiée rend l'insolite plus facile à approvisionner que le commun.
- 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 accidentelles et également dans les cas où les retombées correctes sont manquées par quelqu'un qui maintient le code. Ce dernier est un ajout subtil aux bogues associés à la chute, car même si votre code est parfaitement exempt de bogues, votre chute peut toujours causer des problèmes.
Relativement à ces deux derniers points, considérez la citation suivante de l'édition actuelle de K&R:
Passer d'un cas à l'autre n'est pas robuste, étant susceptible de se désintégrer lorsque le programme est modifié. À l'exception de plusieurs étiquettes pour un seul calcul, les retombées doivent être utilisées avec parcimonie et commentées.
Par bonne forme, mettez une pause après le dernier cas (par défaut ici) même si c'est logiquement inutile. Un jour, quand un autre cas sera ajouté à la fin, ce morceau de programmation défensive vous sauvera.
Donc, de la bouche du cheval, la chute en C est problématique. 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 on doit documenter où l'on fait quelque chose d'inhabituel, car c'est ce qui déclenchera l'examen ultérieur du code et / ou fera ressembler votre code contient un bug de novice alors qu'il est correct.
Et quand on y pense, un code comme celui-ci:
switch(x)
{
case 1:
foo();
/* FALLTHRU */
case 2:
bar();
break;
}
Est d' ajouter quelque chose à faire tomber par-explicite dans le code, il est tout simplement pas quelque chose qui peut être détectée (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 les retombées en C # n'ajoute aucune pénalité aux personnes qui ont bien écrit dans d'autres langages de style C, car elles seraient déjà explicites dans leurs retombées. †
Enfin, l'utilisation d' goto
ici est déjà une norme de 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ù nous voulons qu'un bloc soit inclus dans le code exécuté pour une valeur autre que celle qui amène un au bloc précédent, alors nous devons déjà utiliser goto
. (Bien sûr, il existe des moyens et des façons d'éviter cela avec différentes conditions, mais c'est vrai pour à peu près tout ce qui concerne cette question). En tant que tel, C # s'est construit sur la manière déjà normale de traiter une situation où nous voulons frapper plus d'un bloc de code dans a switch
, et l'a simplement généralisé pour couvrir également les retombées. Cela a également rendu les deux cas plus pratiques et plus documentés, car nous devons ajouter une nouvelle étiquette en C, mais nous pouvons utiliser la case
comme étiquette en C #. En C #, nous pouvons nous débarrasser de l' below_six
étiquette et l'utiliser pourgoto case 5
qui est plus claire quant à ce que nous faisons. (Il faudrait également ajouterbreak
default
, que j'ai omis juste pour que le code C ci-dessus ne soit clairement pas un code C #).
En résumé donc:
- C # ne se rapporte plus à la sortie du compilateur non optimisé aussi directement que le code C le faisait il y a 40 ans (pas plus que C de nos jours), ce qui rend l'une des inspirations de la transition non pertinente.
- C # reste compatible avec C en ne se contentant pas d'avoir implicite
break
, pour un apprentissage plus facile de la langue par ceux qui connaissent des langages similaires, et un portage plus facile.
- C # supprime une source possible de bogues ou de code mal compris qui a été bien documenté comme causant des problèmes au cours des quatre dernières décennies.
- C # rend les meilleures pratiques existantes avec C (chute de document) applicables par le compilateur.
- C # fait du cas inhabituel celui avec du code plus explicite, le cas habituel celui avec le code que l'on écrit juste automatiquement.
- C # utilise la même
goto
approche pour frapper le même bloc à partir d' case
étiquettes différentes que celui utilisé en C. Il le généralise simplement à d'autres cas.
- C # rend cette
goto
approche basée sur plus pratique et plus claire qu'elle ne l'est en C, en permettant aux case
instructions d'agir comme des étiquettes.
Dans l'ensemble, une décision de conception assez raisonnable
* Certaines formes de BASIC permettraient de faire ce GOTO (x AND 7) * 50 + 240
qui, tout en étant fragile et donc un argument particulièrement convaincant pour l'interdiction goto
, sert à montrer un équivalent en langage plus élevé du type de manière dont le code de niveau inférieur peut faire un saut basé sur arithmétique sur une valeur, ce qui est beaucoup plus raisonnable quand elle est le résultat de la compilation plutôt que quelque chose qui doit être maintenu manuellement. Les implémentations de Duff's Device en particulier se prêtent bien au code machine équivalent ou IL car chaque bloc d'instructions aura souvent la même longueur sans avoir besoin d'ajouter des nop
charges.
† L'appareil de Duff revient ici, à titre d'exception raisonnable. Le fait qu'avec cela et des modèles similaires il y ait une répétition des opérations sert à rendre l'utilisation de la traversée relativement claire même sans commentaire explicite à cet effet.