Switch instruction fallthrough en C #?


365

Instruction Switch fallthrough est l' une de mes principales raisons personnelles pour l' amour switchcontre les if/else ifconstructions. Un exemple est en ordre ici:

static string NumberToWords(int number)
{
    string[] numbers = new string[] 
        { "", "one", "two", "three", "four", "five", 
          "six", "seven", "eight", "nine" };
    string[] tens = new string[] 
        { "", "", "twenty", "thirty", "forty", "fifty", 
          "sixty", "seventy", "eighty", "ninety" };
    string[] teens = new string[]
        { "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen",
          "sixteen", "seventeen", "eighteen", "nineteen" };

    string ans = "";
    switch (number.ToString().Length)
    {
        case 3:
            ans += string.Format("{0} hundred and ", numbers[number / 100]);
        case 2:
            int t = (number / 10) % 10;
            if (t == 1)
            {
                ans += teens[number % 10];
                break;
            }
            else if (t > 1)
                ans += string.Format("{0}-", tens[t]);
        case 1:
            int o = number % 10;
            ans += numbers[o];

            break;
        default:
            throw new ArgumentException("number");
    }
    return ans;
}

Les gens intelligents grincent des dents parce que les string[]s doivent être déclarés en dehors de la fonction: eh bien, ils le sont, ce n'est qu'un exemple.

Le compilateur échoue avec l'erreur suivante:

Le contrôle ne peut pas passer d'une étiquette de cas («cas 3:») à une autre
Le contrôle ne peut pas passer d'une étiquette de cas («cas 2:») à une autre

Pourquoi? Et existe-t-il un moyen d'obtenir ce genre de comportement sans avoir trois ifs?

Réponses:


648

(Copier / coller d'une réponse que j'ai fournie ailleurs )

Vous pouvez passer à travers switch- cases en n'ayant pas de code dans un case(voir case 0), ou en utilisant les formes spéciales goto case(voir case 1) ou goto default(voir case 2):

switch (/*...*/) {
    case 0: // shares the exact same code as case 1
    case 1:
        // do something
        goto case 2;
    case 2:
        // do something else
        goto default;
    default:
        // do something entirely different
        break;
}

121
Je pense que, dans ce cas particulier, le goto n'est pas considéré comme nuisible.
Thomas Owens

78
Merde - Je programme avec C # depuis les premiers jours de 1.0, et je n'ai jamais vu ça jusqu'à présent. Va juste pour montrer, vous apprenez de nouvelles choses chaque jour.
Erik Forbes

37
Tout va bien, Erik. La seule raison / je le savais, c'est que je suis un nerd de la théorie du compilateur qui a lu les spécifications de l'ECMA-334 avec une loupe.
Alex Lyman

13
@Dancrumb: Au moment où la fonctionnalité a été écrite, C # n'avait pas encore ajouté de mots clés "logiciels" (comme 'yield', 'var', 'from' et 'select'), ils avaient donc trois options réelles: 1 ) faire de 'fallthrough' un mot-clé dur (vous ne pouvez pas l'utiliser comme nom de variable), 2) écrire le code nécessaire pour supporter un tel mot-clé soft, 3) utiliser des mots-clés déjà réservés. # 1 était un gros problème pour ceux qui portaient du code; # 2 était une tâche d'ingénierie assez importante, d'après ce que je comprends; et l'option avec laquelle ils sont allés, # 3 avait un avantage secondaire: d'autres développeurs lisant le code après coup pouvaient en apprendre davantage sur la fonctionnalité du concept de base de goto
Alex Lyman

10
Tout cela parle de mots clés nouveaux / spéciaux pour une solution explicite. Ne pourraient-ils pas simplement avoir utilisé le mot-clé «continuer»? En d'autres termes. Sortez de l'interrupteur ou passez au cas suivant (passez à travers).
Tal Even-Tov

44

Le "pourquoi" est d'éviter les chutes accidentelles, ce dont je suis reconnaissant. Il s'agit d'une source non rare de bogues en C et en Java.

La solution consiste à utiliser goto, par exemple

switch (number.ToString().Length)
{
    case 3:
        ans += string.Format("{0} hundred and ", numbers[number / 100]);
        goto case 2;
    case 2:
    // Etc
}

La conception générale de l'interrupteur / boîtier est un peu malheureuse à mon avis. Il est resté trop proche de C - il y a des changements utiles qui pourraient être apportés en termes de portée, etc. - à quel moment un nom différent serait peut-être demandé.


1
C'est la différence entre switch et if / elseif dans mon esprit. Le commutateur sert à vérifier divers états d'une seule variable, alors que if / elseif peut être utilisé pour vérifier n'importe quel nombre d'éléments connectés, mais pas nécessairement une seule ou la même variable.
Matthew Scharley

2
Si c'était pour éviter une chute accidentelle, je pense qu'un avertissement du compilateur aurait été mieux. Tout comme vous en avez un si votre déclaration if a une affectation:if (result = true) { }
Tal Even-Tov

3
@ TalEven-Tov: Les avertissements du compilateur devraient vraiment être pour les cas où vous pouvez presque toujours corriger le code pour être meilleur. Personnellement, je préférerais une rupture implicite, donc ce ne serait pas un problème pour commencer, mais c'est une autre affaire.
Jon Skeet

26

Le basculement des commutateurs est historiquement l'une des principales sources de bugs dans les logiciels modernes. Le concepteur de langage a décidé de rendre obligatoire le saut à la fin du cas, sauf si vous passez directement au cas suivant sans traitement.

switch(value)
{
    case 1:// this is still legal
    case 2:
}

23
Je ne comprends jamais pourquoi ce n'est pas "cas 1, 2:"
BCS

11
@David Pfeffer: Oui, c'est le cas, tout comme case 1, 2:dans les langues qui le permettent. Ce que je ne comprendrai jamais, c'est pourquoi aucune langue moderne ne choisirait de permettre cela.
BCS

@BCS avec l'instruction goto, plusieurs options séparées par des virgules pourraient être difficiles à gérer?
penguat

@pengut: il pourrait être plus précis de dire qu'il case 1, 2:s'agit d'une seule étiquette mais avec plusieurs noms. - FWIW, je pense que la plupart des langues qui interdisent la chute ne font pas cas particulier des "étiquettes de cas consécutives" indium mais traitent plutôt les étiquettes de cas comme une annotation sur la déclaration suivante et nécessitent la dernière déclaration avant une déclaration étiquetée avec (un ou plus) les étiquettes de cas seront un saut.
BCS

22

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:

  1. Fournissez des instructions à l'ordinateur.
  2. 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-elseblocs 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 switchen équivalent if-elseet 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:

  1. 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.

  2. 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 breaklorsque 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:

  1. 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.
  2. Des améliorations dans les optimisations signifient que la probabilité de switchdevenir soit if-elseparce 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.
  3. 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 switchblocs 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.
  4. 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' gotoici 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 casecomme é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 ajouterbreakdefault, que j'ai omis juste pour que le code C ci-dessus ne soit clairement pas un code C #).

En résumé donc:

  1. 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.
  2. 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.
  3. 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.
  4. C # rend les meilleures pratiques existantes avec C (chute de document) applicables par le compilateur.
  5. C # fait du cas inhabituel celui avec du code plus explicite, le cas habituel celui avec le code que l'on écrit juste automatiquement.
  6. C # utilise la même gotoapproche 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.
  7. C # rend cette gotoapproche basée sur plus pratique et plus claire qu'elle ne l'est en C, en permettant aux caseinstructions 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 + 240qui, 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 nopcharges.

† 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.


17

Vous pouvez 'goto case label' http://www.blackwasp.co.uk/CSharpGoto.aspx

L'instruction goto est une commande simple qui transfère inconditionnellement le contrôle du programme vers une autre instruction. La commande est souvent critiquée, certains développeurs préconisant sa suppression de tous les langages de programmation de haut niveau car elle peut conduire à du code spaghetti . Cela se produit lorsqu'il y a tellement d'instructions goto ou d'instructions de sauts similaires que le code devient difficile à lire et à maintenir. Cependant, certains programmeurs soulignent que l'instruction goto, lorsqu'elle est utilisée avec soin, fournit une solution élégante à certains problèmes ...


8

Ils ont omis ce comportement par conception pour éviter qu'il ne soit pas utilisé par testament mais qu'il cause des problèmes.

Il ne peut être utilisé que s'il n'y a pas de déclaration dans la partie de cas, comme:

switch (whatever)
{
    case 1:
    case 2:
    case 3: boo; break;
}

4

Ils ont changé le comportement de l'instruction switch (de C / Java / C ++) pour c #. Je suppose que le raisonnement était que les gens avaient oublié la chute et que des erreurs avaient été causées. Un livre que j'ai lu disait d'utiliser goto pour simuler, mais cela ne me semble pas être une bonne solution.


2
C # prend en charge goto, mais pas la solution? Sensationnel. Et ce n'est pas seulement ceux-là. C # est le seul langage que je connais qui se comporte de cette façon.
Matthew Scharley

Je n'aimais pas exactement au début, mais "fall-thru" est vraiment une recette pour un désastre (en particulier chez les programmeurs juniors.) Comme beaucoup l'ont souligné, C # permet toujours le fall-thru pour les lignes vides (qui est la majorité des cas.) "Kenny" a publié un lien qui met en évidence l'utilisation élégante de Goto avec switch-case.
Pretzel

Ce n'est pas une grosse affaire à mon avis. 99% du temps, je ne veux pas de chute et j'ai été brûlé par des bugs dans le passé.
Ken

1
"cela ne me semble pas être une bonne solution" - désolé d'entendre cela à votre sujet, car c'est à cela que goto casesert. Son avantage par rapport à la retombée est qu'il est explicite. Le fait que certaines personnes s'y goto caseopposent montre simplement qu'elles ont été endoctrinées contre le «goto» sans aucune compréhension du problème et sont incapables de penser par elles-mêmes. Lorsque Dijkstra a écrit "GOTO considéré comme nuisible", il s'adressait à des langues qui n'avaient aucun autre moyen de modifier le flux de contrôle.
Jim Balter

@JimBalter, puis combien de personnes qui citent Dijkstra à ce sujet citeront Knuth que "l'optimisation prématurée est la racine de tout mal" bien que cette citation ait été lorsque Knuth écrivait explicitement sur l'utilité de l' gotooptimisation du code?
Jon Hanna du

1

Vous pouvez effectuer une chute comme c ++ par le mot-clé goto.

EX:

switch(num)
{
   case 1:
      goto case 3;
   case 2:
      goto case 3;
   case 3:
      //do something
      break;
   case 4:
      //do something else
      break;
   case default:
      break;
}

9
Si seulement quelqu'un l'avait posté deux ans plus tôt!

0

Une instruction de saut telle qu'une pause est requise après chaque bloc de cas, y compris le dernier bloc, qu'il s'agisse d'une instruction de cas ou d'une instruction par défaut. À une exception près (contrairement à l'instruction switch C ++), C # ne prend pas en charge une chute implicite d'une étiquette de cas à l'autre. La seule exception est si une instruction case n'a pas de code.

- Documentation C # switch ()


1
Je me rends compte que ce comportement est documenté, je veux savoir POURQUOI c'est ainsi et toutes les alternatives pour obtenir l'ancien comportement.
Matthew Scharley

0

Après chaque instruction de cas, il faut une instruction break ou goto , même s'il s'agit d'un cas par défaut.


2
Si seulement quelqu'un l'avait posté deux ans plus tôt!

1
@Poldie c'était drôle la première fois ... Shilpa vous n'avez pas besoin d'une pause ou d'un goto pour chaque cas, juste pour chaque cas avec son propre code. Vous pouvez avoir plusieurs cas qui partagent très bien le code.
Maverick

0

Juste une petite note pour ajouter que le compilateur pour Xamarin s'est vraiment trompé et qu'il permet une solution. Il aurait été corrigé, mais n'a pas été rendu public. J'ai découvert cela dans un code qui était en train de tomber et le compilateur ne s'est pas plaint.



-1

C # ne prend pas en charge les instructions Fall / Switch. Je ne sais pas pourquoi, mais il n'y a vraiment aucun support pour cela. Lien


C'est tellement faux. Voir les autres réponses ... l'effet d'une chute implicite peut être obtenu avec une explicite goto case . C'était un choix de conception intentionnel et judicieux par les concepteurs C #.
Jim Balter

-11

Vous avez oublié d'ajouter la "pause"; dans le cas 3. Dans le cas 2, vous l'avez écrit dans le bloc if. Essayez donc ceci:

case 3:            
{
    ans += string.Format("{0} hundred and ", numbers[number / 100]);
    break;
}


case 2:            
{
    int t = (number / 10) % 10;            
    if (t == 1)            
    {                
        ans += teens[number % 10];                
    }            
    else if (t > 1)                
    {
        ans += string.Format("{0}-", tens[t]);        
    }
    break;
}

case 1:            
{
    int o = number % 10;            
    ans += numbers[o];            
    break;        
}

default:            
{
    throw new ArgumentException("number");
}

2
Cela produit une sortie très erronée. J'ai omis les instructions switch par conception. La question est de savoir pourquoi le compilateur C # considère cela comme une erreur alors que presque aucun autre langage n'a cette restriction.
Matthew Scharley

5
Quel étonnant échec à comprendre. Et vous avez eu 5 ans pour le supprimer et vous ne l'avez toujours pas fait? Époustouflant.
Jim Balter
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.