web-dev-qa-db-fra.com

Basculer l'instruction en C #?

L'instruction Switch est une des raisons principales pour lesquelles j'aime les constructions switch vs. if/else if. 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 sont en colère parce que les string[]s devraient ê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 est-il possible d’obtenir ce genre de comportement sans avoir trois ifs?

348
Matthew Scharley

(Copier/coller d'un réponse fournie ailleurs )

La chute de switch-cases peut être obtenue en n’ayant pas de code dans un case (voir case 0), ou en utilisant le spécial goto case (voir case 1) ou goto default (voir case 2) formes:

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;
}
617
Alex Lyman

Le "pourquoi" consiste à éviter les retombées accidentelles, et je les en remercie. C’est une source de bogues courante en C et en Java.

La solution de contournement 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 regrettable à mon avis. Cela collait trop près de C - il y a quelques changements utiles qui pourraient être apportés en termes de portée, etc. On pourrait soutenir qu'un commutateur plus intelligent qui pourrait faire la correspondance de modèle, etc. serait utile, mais cela change vraiment d'un commutateur à "vérifier une séquence de conditions" - à quel point un nom différent serait peut-être appelé.

40
Jon Skeet

Switch fallthrough est historiquement l’une des sources majeures de bugs dans les logiciels modernes. Le concepteur de langage a décidé de rendre obligatoire le saut à la fin du cas, à moins que vous ne passiez directement au cas suivant sans traitement.

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

Pour ajouter aux réponses ici, je pense qu’il est intéressant d’examiner la question opposée conjointement avec celle-ci, à savoir. pourquoi C a-t-il permis la chute en premier lieu?

Tout langage de programmation a bien sûr deux objectifs:

  1. Fournir des instructions à l'ordinateur.
  2. Laissez 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 transformer en instructions informatiques (qu’il s’agisse de code machine, de bytecode comme IL ou les instructions sont interprétées lors de l’exécution), plus le processus de compilation ou d’interprétation sera efficace, plus efficace et plus fiable. compact en sortie. Poussé à l'extrême, cet objectif se traduit par une simple écriture dans Assembly, IL, voire par des codes d'opération bruts, car la compilation la plus facile consiste à ne pas en compiler du tout.

Inversement, plus le langage exprime l'intention du programmeur et non les moyens utilisés à cette fin, plus le programme est compréhensible, tant en écriture qu'en maintenance.

Maintenant, switch aurait toujours pu être compilé en le convertissant en chaîne équivalente de blocs if-else ou similaire, mais elle a été conçue pour permettre la compilation dans un motif Assembly particulier, où l'on prend une valeur, calcule un son décalage (que ce soit en recherchant une table indexée 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 en C # transforme parfois switch en équivalent if-else, et utilise parfois une approche de saut basée sur le hachage (et également en C, C++ et d’autres langages avec des fonctions similaires). syntaxe).

Dans ce cas, il y a deux bonnes raisons d'autoriser les retombées:

  1. Quoi qu'il en soit, cela se produit naturellement de toute façon: si vous construisez une table de saut dans un ensemble d'instructions et si l'un des lots d'instructions précédents ne contient aucune sorte de saut ou de retour, l'exécution se poursuivra naturellement dans le prochain lot. Permettre une chute directe était ce qui "se produirait" si vous transformiez la switch- en utilisant C en table de saut, en utilisant le code machine.

  2. Les codeurs qui écrivaient dans Assembly étaient déjà habitués à l'équivalent: lorsqu'ils écrivaient une table de sauts à la main dans Assembly, ils devaient se demander si un bloc de code donné se terminerait par un retour, un saut en dehors de la table ou continuerait simplement. au bloc suivant. En tant que tel, le fait que le codeur ajoute un break explicite, le cas échéant, était également "naturel" pour le codeur.

À 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 pas tout à fait les mêmes, pour plusieurs raisons:

  1. Les codeurs en C actuels ont peut-être peu ou pas d'expérience de montage. Les codeurs dans beaucoup d'autres langages de style C ont encore moins de chances de le faire (en particulier le Javascript!). Toute notion de "à quoi les gens sont habitués de l'Assemblée" n'est plus pertinente.
  2. L'amélioration des optimisations signifie que la probabilité de switch soit d'être transformée en if-else parce qu'elle a été jugée l'approche la plus efficace, ou bien d'être transformée en une variante particulièrement ésotérique de l'approche par saut de table sont plus élevées. . La correspondance entre les approches de niveau supérieur et inférieur n'est pas aussi forte qu'auparavant.
  3. L’expérience a montré que les retombées tendaient à être le cas minoritaire plutôt que la norme (une étude du compilateur de Sun a révélé que 3% des blocs switch utilisaient une chute au-delà de plusieurs étiquettes sur le même bloc. le cas d’utilisation utilisé ici signifiait que ces 3% étaient en réalité beaucoup plus élevés que la normale). Ainsi, la langue étudiée rend l'inhabituel plus facile à satisfaire que le commun.
  4. L’expérience a montré que les retombées tendaient à être la source de problèmes à la fois dans les cas où cela se produirait accidentellement, mais aussi dans les cas où une personne responsable de la maintenance du code omettrait de passer au travers. Ce dernier est un ajout subtil aux bugs associés à fall-through, car même si votre code est parfaitement dépourvu de bugs, votre chute peut toujours causer des problèmes.

En rapport avec ces deux derniers points, considérons la citation suivante de l'édition actuelle de K & R:

Passer d'un cas à l'autre n'est pas robuste, il est sujet à la désintégration lorsque le programme est modifié. À l'exception de plusieurs étiquettes pour un même calcul, les retombées doivent être utilisées avec parcimonie et commentées.

En règle générale, mettez une pause après le dernier cas (le cas par défaut ici), même si c'est logiquement inutile. Un jour, lorsqu'un autre cas sera ajouté à la fin, cette programmation défensive vous épargnera.

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 il convient de documenter où l'on fait quelque chose d'inhabituel, car c'est ce qui déclenchera un examen ultérieur du code et/ou lui donnera l'aspect du code. a un bogue de novice en elle quand elle est en fait correcte.

Et quand vous y réfléchissez, codez comme ceci:

switch(x)
{
  case 1:
   foo();
   /* FALLTHRU */
  case 2:
    bar();
    break;
}

Is ajoutant quelque chose pour rendre la chute directe 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 qu’on doive être explicite avec une chute directe en C # n’ajoute aucune pénalité aux personnes qui écrivent bien dans d’autres langages de style C, car elles seraient déjà explicites lors de leurs retombées. †

Enfin, l'utilisation de goto est déjà une norme de C et d'autres langages similaires:

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 type de cas où nous souhaitons inclure un bloc dans le code exécuté pour une valeur autre que celle qui en amène un au bloc précédent, nous devons déjà utiliser goto. (Bien sûr, il existe des moyens et des moyens 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 # a construit sur la manière déjà normale de traiter une situation dans laquelle nous souhaitons utiliser plus d'un bloc de code dans un switch, et l'a simplement généralisé pour couvrir également les retombées. Cela a également rendu les deux cas plus pratiques et auto-documentés, car nous devons ajouter une nouvelle étiquette en C mais nous pouvons utiliser le case comme étiquette en C #. En C #, nous pouvons supprimer le libellé below_six et utiliser goto case 5, qui est plus clair quant à ce que nous faisons. (Nous devrons également ajouter break pour le default, ce que j'ai omis juste pour rendre le code C ci-dessus clairement pas le code C #).

En résumé donc:

  1. C # ne concerne plus la sortie non optimisée du compilateur aussi directement que le code C il y a 40 ans (et le C ne le fait pas non plus de nos jours), ce qui rend l'une des inspirations de fall-through non pertinente.
  2. C # reste compatible avec C sans avoir simplement break implicite, pour faciliter l'apprentissage du langage par ceux qui sont familiers avec des langages similaires et faciliter le portage.
  3. C # supprime une source possible de bogues ou de code incompris qui a été bien documentée comme causant des problèmes depuis quarante ans.
  4. C # rend les meilleures pratiques existantes avec C (passage de document) applicables par le compilateur.
  5. C # fait du cas inhabituel celui avec le code le plus explicite, le cas habituel avec le code que l’on écrit automatiquement.
  6. C # utilise la même approche basée sur goto pour frapper le même bloc à partir de différentes étiquettes case comme celle utilisée en C. Il la généralise simplement à d'autres cas.
  7. C # rend cette approche basée sur goto plus pratique et plus claire qu'en C, en permettant aux instructions case d'agir en tant qu'étiquettes.

Dans l'ensemble, une décision de conception assez raisonnable


* Certaines formes de BASIC permettraient de faire des choses comme GOTO (x AND 7) * 50 + 240 qui, bien qu’elle soit fragile et par conséquent un argument particulièrement convaincant en faveur de l’interdiction de goto, sert à montrer un équivalent en langage supérieur du type de 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 d'une compilation plutôt que d'un élément à gérer 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 nécessiter l'ajout de remplisseurs nop.

† Duff's Device revient ici à nouveau, à titre d'exception raisonnable. Le fait que, avec ce schéma et des schémas similaires, il y ait une répétition des opérations sert à rendre l'utilisation des retombées relativement claire, même sans commentaire explicite à cet effet.

20
Jon Hanna

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 à une autre instruction. La commande est souvent critiquée par certains développeurs qui préconisent sa suppression de tous les langages de programmation de haut niveau car elle peut entraîner code spaghetti . Cela se produit lorsqu'il y a tellement d'instructions goto ou d'invitations similaires que le code devient difficile à lire et à maintenir. Cependant, certains programmeurs soulignent que la déclaration goto, utilisée avec précaution, fournit une solution élégante à certains problèmes ...

15
kenny

Ils ont omis ce comportement de par leur conception pour éviter les problèmes lorsqu'ils étaient utilisés sans testament.

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

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

Ils ont modifié le comportement de l'instruction switch (à partir de C/Java/C++) pour c #. Je suppose que le raisonnement était que les gens ont oublié la chute et que des erreurs ont été commises. Un livre que j'ai lu m'a dit d'utiliser la méthode goto pour simuler, mais cela ne me semble pas une bonne solution.

4
Ken

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

0
Shilpa gulati

Vous pouvez obtenir des résultats similaires à c ++ à l'aide du 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;
}
0
Dai Tran

Juste une petite note pour ajouter que le compilateur de Xamarin s’est réellement trompé et qu’il permettait de s’écouler. Il a soi-disant été corrigé, mais n'a pas été publié. J'ai découvert cela dans un code qui était en train de tomber et le compilateur ne s'est pas plaint.

0
user486646

le commutateur (référence C #) dit

C # nécessite la fin des sections d’interrupteur, y compris la dernière,

Donc, vous devez également ajouter un break; à votre section default, sinon il y aura toujours une erreur de compilation.

0
gm2008

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

- documentation de C # switch ()

0
Powerlord