Éviter le vaudou `goto`?

47

J'ai une switchstructure qui a plusieurs cas à gérer. Le switchopère sur un enumqui pose le problème de la duplication de code via des valeurs combinées:

// All possible combinations of One - Eight.
public enum ExampleEnum {
    One,
    Two, TwoOne,
    Three, ThreeOne, ThreeTwo, ThreeOneTwo,
    Four, FourOne, FourTwo, FourThree, FourOneTwo, FourOneThree,
          FourTwoThree, FourOneTwoThree
    // ETC.
}

Actuellement, la switchstructure traite chaque valeur séparément:

// All possible combinations of One - Eight.
switch (enumValue) {
    case One: DrawOne; break;
    case Two: DrawTwo; break;
    case TwoOne:
        DrawOne;
        DrawTwo;
        break;
     case Three: DrawThree; break;
     ...
}

Vous avez l'idée là. J'ai actuellement cette ifstructure en pile pour gérer les combinaisons avec une seule ligne:

// All possible combinations of One - Eight.
if (One || TwoOne || ThreeOne || ThreeOneTwo)
    DrawOne;
if (Two || TwoOne || ThreeTwo || ThreeOneTwo)
    DrawTwo;
if (Three || ThreeOne || ThreeTwo || ThreeOneTwo)
    DrawThree;

Cela pose le problème des évaluations logiques incroyablement longues, qui déroutent à la lecture et sont difficiles à maintenir. Après avoir réexaminé cette réflexion, j'ai commencé à réfléchir à des alternatives et à l'idée d'une switchstructure avec des retombées entre les cas.

Je dois utiliser un gotodans ce cas car C#ne permet pas de tomber à travers. Cependant, cela évite les chaînes logiques incroyablement longues, même s'il saute dans la switchstructure, tout en permettant la duplication de code.

switch (enumVal) {
    case ThreeOneTwo: DrawThree; goto case TwoOne;
    case ThreeTwo: DrawThree; goto case Two;
    case ThreeOne: DrawThree; goto default;
    case TwoOne: DrawTwo; goto default;
    case Two: DrawTwo; break;
    default: DrawOne; break;
}

Ce n'est toujours pas une solution suffisamment propre et le gotomot clé que je voudrais éviter est associé à une honte . Je suis sûr qu'il doit y avoir un meilleur moyen de nettoyer cela.


Ma question

Existe-t-il une meilleure façon de gérer ce cas spécifique sans affecter la lisibilité et la maintenabilité?

Perpétuel
la source
28
on dirait que vous voulez avoir un grand débat. Mais vous aurez besoin d'un meilleur exemple de bon exemple pour utiliser goto. flag enum résout parfaitement celui-ci, même si votre exemple de déclaration if n'était pas acceptable et qu'il me
convient
5
@ Ewan Personne n'utilise le goto. À quand remonte la dernière fois que vous avez consulté le code source du noyau Linux?
John Douma
6
Vous utilisez gotolorsque la structure de haut niveau n'existe pas dans votre langue. Je souhaite parfois qu'il y ait un fallthru; mot-clé pour se débarrasser de cette utilisation particulière de gotomais bon.
Josué
25
De manière générale, si vous ressentez le besoin d'utiliser un gotolangage de haut niveau tel que C #, vous avez probablement négligé de nombreuses autres (et meilleures) alternatives de conception et / ou de mise en œuvre. Très découragé.
code_dredd
11
L'utilisation de "goto case" en C # diffère de l'utilisation d'un "goto" plus général, car c'est la façon dont vous accomplissez l'explosion explicite.
Hammerite

Réponses:

175

Je trouve le code difficile à lire avec les gotodéclarations. Je recommanderais de structurer votre enumdifféremment. Par exemple, si votre enumétait un champ de bits où chaque bit représentait l'un des choix, cela pourrait ressembler à ceci:

[Flags]
public enum ExampleEnum {
    One = 0b0001,
    Two = 0b0010,
    Three = 0b0100
};

L'attribut Flags indique au compilateur que vous définissez des valeurs qui ne se chevauchent pas. Le code qui appelle ce code peut définir le bit approprié. Vous pouvez ensuite faire quelque chose comme ceci pour clarifier ce qui se passe:

if (myEnum.HasFlag(ExampleEnum.One))
{
    CallOne();
}
if (myEnum.HasFlag(ExampleEnum.Two))
{
    CallTwo();
}
if (myEnum.HasFlag(ExampleEnum.Three))
{
    CallThree();
}

Cela nécessite le code qui définit myEnumpour définir les champs de bits correctement et marqué avec l'attribut Flags. Mais vous pouvez le faire en modifiant les valeurs des enums de votre exemple pour:

[Flags]
public enum ExampleEnum {
    One = 0b0001,
    Two = 0b0010,
    Three = 0b0100,
    OneAndTwo = One | Two,
    OneAndThree = One | Three,
    TwoAndThree = Two | Three
};

Lorsque vous écrivez un nombre dans le formulaire 0bxxxx, vous le spécifiez en binaire. Vous pouvez donc voir que nous avons défini les bits 1, 2 ou 3 (bien, techniquement, 0, 1 ou 2, mais vous avez l’idée). Vous pouvez également nommer des combinaisons en utilisant un bit OU si les combinaisons peuvent être fréquemment définies ensemble.

utilisateur1118321
la source
5
Il semble que oui ! Mais ce doit être C # 7.0 ou plus tard, je suppose.
user1118321
31
Quoi qu'il en soit, on pourrait aussi le faire comme ceci: public enum ExampleEnum { One = 1 << 0, Two = 1 << 1, Three = 1 << 2, OneAndTwo = One | Two, OneAndThree = One | Three, TwoAndThree = Two | Three };. Pas besoin d'insister sur C # 7 +.
Déduplicateur
8
Vous pouvez utiliser Enum.HasFlag
IMil
13
L' [Flags]attribut ne signale rien au compilateur. C’est la raison pour laquelle vous devez toujours déclarer explicitement les valeurs d’énum en tant que puissances de 2.
Joe Sewell
19
@UKMonkey Je suis en désaccord avec véhémence. HasFlagest un terme descriptif et explicite pour l’opération en cours et extrait l’implémentation de la fonctionnalité. Utiliser &parce qu'il est commun à d'autres langages n'a pas plus de sens que d'utiliser un bytetype au lieu de déclarer un enum.
BJ Myers
136

OMI la racine du problème est que ce morceau de code ne devrait même pas exister.

Vous avez apparemment trois conditions indépendantes et trois actions indépendantes à prendre si ces conditions sont vraies. Alors, pourquoi tout cela est-il canalisé dans un code qui a besoin de trois drapeaux booléens pour lui dire quoi faire (que vous les masquiez ou non en une énumération) et ensuite une combinaison de trois choses indépendantes? Le principe de la responsabilité unique semble avoir une journée de congé ici.

Placez les appels sur les trois fonctions auxquelles vous appartenez (c’est-à-dire où vous découvrez la nécessité de faire les actions) et consignez le code de ces exemples dans la corbeille.

S'il y avait dix drapeaux et trois actions, étendriez-vous ce type de code pour traiter 1024 combinaisons différentes? J'espère que non! Si 1024 est trop, 8 est aussi trop, pour la même raison.

Alephzero
la source
10
En fait, il existe de nombreuses API bien conçues, dotées de toutes sortes d'indicateurs, d'options et de divers paramètres. Certains peuvent être transmis, certains ont des effets directs, d'autres encore peuvent être ignorés sans rompre le PRS. Quoi qu'il en soit, la fonction pourrait même décoder une entrée externe, qui sait? De plus, la réduction d'absurde conduit souvent à des résultats absurdes, surtout si le point de départ n'est même pas si solide.
Déduplicateur
9
Upvoted cette réponse. Si vous voulez juste débattre de GOTO, c'est une question différente de celle que vous avez posée. Sur la base des preuves présentées, vous avez trois exigences disjointes, qui ne doivent pas être agrégées. D'un point de vue SE, je soutiens que celui d'alephzero est la bonne réponse.
8
@DuDuplicator Un coup d'oeil à ce méli-mélo de certaines, mais pas toutes les combinaisons de 1,2 & 3, montre qu'il ne s'agit pas d'une "API bien conçue".
user949300
4
Tellement ça. Les autres réponses n'améliorent pas vraiment le contenu de la question. Ce code nécessite une réécriture. Il n'y a rien de mal à écrire plus de fonctions.
Faites des excuses et rétablissez Monica
3
J'aime cette réponse, en particulier "Si 1024, c'est trop, 8, c'est aussi trop". Mais c'est essentiellement du "polymorphisme d'utilisation", n'est-ce pas? Et ma réponse (la plus faible) me fait beaucoup de conneries pour avoir dit cela. Puis-je engager votre personne de relations publiques? :-)
user949300 le
29

Ne jamais utiliser gotos est l’un des concepts de «l’informatique » qui «ment aux enfants» . C'est le bon conseil 99% du temps, et les moments où il n'en est pas une sont tellement rares et spécialisés que c'est bien mieux pour tout le monde s'il est simplement expliqué aux nouveaux codeurs comme "ne les utilisez pas".

Alors, quand devraient- ils être utilisés? Il existe quelques scénarios , mais celui que vous semblez toucher est le suivant: lorsque vous codez une machine à états . S'il n'y a pas meilleure expression mieux organisée et structurée de votre algorithme qu'une machine à états, son expression naturelle dans le code implique des branches non structurées, et il n'y a pas grand chose à faire pour ce qui ne fait pas la structure du machine à états elle-même plus obscure, plutôt que moins.

Les rédacteurs de compilateurs le savent bien, c'est pourquoi le code source de la plupart des compilateurs qui implémentent des analyseurs LALR * contient des gotos. Cependant, très peu de gens coderont réellement leurs propres analyseurs et analyseurs syntaxiques lexicaux.

* - IIRC, il est possible de mettre en œuvre des grammaires LALL de descente entièrement récursive sans recourir à des tables de saut ou à d’autres instructions de contrôle non structurées. Par conséquent, si vous êtes vraiment anti-goto, c’est une solution.


Maintenant, la question suivante est: "Cet exemple est-il l'un de ces cas?"

Ce que je vois, c'est que vous avez trois états différents possibles en fonction du traitement de l'état actuel. Puisque l’un d’eux ("default") n’est qu’une ligne de code, techniquement, vous pouvez vous en débarrasser en pointant cette ligne de code à la fin des états auxquels elle s’applique. Cela vous mènerait à 2 états possibles.

L'un des autres ("Trois") n'est branché qu'à un endroit que je peux voir. Donc, vous pouvez vous en débarrasser de la même manière. Vous vous retrouveriez avec un code qui ressemble à ceci:

switch (exampleValue) {
    case OneAndTwo: i += 3 break;
    case OneAndThree: i += 4 break;
    case Two: i += 2 break;
    case TwoAndThree: i += 5 break;
    case Three: i += 3 break;
    default: i++ break;
}

Cependant, encore une fois, c’était un exemple de jouet que vous avez fourni. Dans les cas où "par défaut" contient une quantité de code non négligeable, "trois" passe de plusieurs états à une transition, ou (plus important encore ) une maintenance supplémentaire risque d'ajouter ou de compliquer les états , vous seriez honnêtement mieux. utiliser gotos (et peut-être même se débarrasser de la structure enumère qui cache la nature de la machine à états, à moins qu'il y ait une bonne raison pour qu'elle reste).

TED
la source
2
@PerpetualJ - Eh bien, je suis certainement heureux que vous ayez trouvé quelque chose de plus propre. Il pourrait être intéressant, si tel est vraiment votre cas, de l’essayer avec les gotos (pas de cas, ni d’énum. Simplement des blocs étiquetés et des gotos) et de voir comment ils se comparent en termes de lisibilité. Cela étant dit, l'option "goto" ne doit pas seulement être plus lisible, mais suffisamment lisible pour éviter les arguments et les tentatives de "réparation" impitoyables d'autres développeurs. Il semble donc probable que vous ayez trouvé la meilleure option.
TED
4
Je ne crois pas qu'il serait "préférable d'utiliser gotos" si "un entretien supplémentaire risque d'ajouter ou de compliquer les choses". Êtes-vous vraiment en train de dire que le code spaghetti est plus facile à gérer que le code structuré? Cela va à l'encontre de ~ 40 ans de pratique.
user949300
5
@ user949300 - Ce n'est pas pratique contre la construction de machines d'état. Vous pouvez simplement lire mon commentaire précédent sur cette réponse pour un exemple concret. Si vous essayez d’utiliser des retombées de casse C, qui imposent une structure artificielle (leur ordre linéaire) à un algorithme de machine à états dépourvu de telles restrictions, vous vous retrouverez avec une complexité croissante du point de vue géométrique chaque fois que vous devez réorganiser tous vos paramètres. commandes pour accueillir un nouvel état. Si vous ne codez que des états et des transitions, comme le demande l'algorithme, cela n'arrivera pas. Les nouveaux états sont triviaux à ajouter.
TED
8
@ user949300 - Encore une fois, la compilation de compilateurs "~ 40 ans de pratique" montre que les gotos sont en fait le meilleur moyen de résoudre certains problèmes, c'est pourquoi, si vous examinez le code source du compilateur, vous trouverez presque toujours des gotos.
TED
3
@ user949300 Le code spaghetti n'apparaît pas de manière inhérente lors de l'utilisation de fonctions ou de conventions spécifiques. Il peut apparaître dans n'importe quelle langue, fonction ou convention à tout moment. La cause utilise ce langage, cette fonction ou cette convention dans une application à laquelle il n'était pas destiné et le fait pencher en arrière pour le retirer. Toujours utiliser le bon outil pour le travail, et oui, parfois, cela signifie utiliser goto.
Abion47 le
26

La meilleure réponse est d' utiliser le polymorphisme .

Une autre réponse, qui, IMO, rend les choses si plus claires et peut-être plus courtes :

if (One || OneAndTwo || OneAndThree)
  CallOne();
if (Two || OneAndTwo || TwoAndThree)
  CallTwo();
if (Three || OneAndThree || TwoAndThree)
  CallThree();

goto est probablement mon 58ème choix ici ...

utilisateur949300
la source
5
+1 pour suggérer un polymorphisme, bien qu'idéalement, cela simplifierait le code client en appelant simplement "Call ();", en utilisant tell not ask - pour écarter la logique de décision du client consommateur vers le mécanisme et la hiérarchie de la classe.
Erik Eidt le
12
Proclamer la meilleure vision invisible est certainement très courageux. Mais sans informations supplémentaires, je suggère un peu de scepticisme et de garder l’esprit ouvert. Peut-être souffre-t-il d'une explosion combinatoire?
Déduplicateur
Wow, je pense que c'est la première fois que je suis voté pour avoir suggéré un polymorphisme. :-)
user949300
25
Certaines personnes, face à un problème, disent "Je vais utiliser un polymorphisme". Maintenant, ils ont deux problèmes et polymorphisme.
Courses de légèreté avec Monica
8
J'ai en fait rédigé mon mémoire de maîtrise sur l'utilisation du polymorphisme à l'exécution pour supprimer les gotos dans les machines d'état des compilateurs. C'est tout à fait faisable, mais j'ai depuis constaté que, dans la plupart des cas, l'effort en valait la chandelle. Cela nécessite une tonne de code OO-setup, qui peut bien sûr se retrouver avec des bugs (et que cette réponse omet).
TED
10

Pourquoi pas ceci:

public enum ExampleEnum {
    One = 0, // Why not?
    OneAndTwo,
    OneAndThree,
    Two,
    TwoAndThree,
    Three
}
int[] COUNTS = { 1, 3, 4, 2, 5, 3 }; // Whatever

int ComputeExampleValue(int i, ExampleEnum exampleValue) {
    return i + COUNTS[(int)exampleValue];
}

OK, je suis d’accord, c’est du hack (je ne suis pas un développeur C #, excusez-moi donc pour le code), mais du point de vue de l’efficacité, c’est un must? L'utilisation d'énums en tant qu'index de tableau est valide C #.

Laurent Grégoire
la source
3
Oui. Un autre exemple de remplacement de code par des données (ce qui est généralement souhaitable pour la rapidité, la structure et la facilité de maintenance).
Peter - Réintégrer Monica le
2
C'est une excellente idée pour l'édition la plus récente de la question, sans aucun doute. Et il peut être utilisé pour aller du premier enum (une gamme dense de valeurs) aux drapeaux d’actions plus ou moins indépendantes qui doivent être faites en général.
Déduplicateur
Je n'ai pas accès à un compilateur C #, mais peut-être que le code peut être rendu plus sûr en utilisant ce type de code int[ExempleEnum.length] COUNTS = { 1, 3, 4, 2, 5, 3 };:?
Laurent Grégoire le
7

Si vous ne pouvez pas ou ne voulez pas utiliser les indicateurs, utilisez une fonction récursive. En mode de publication 64 bits, le compilateur produira un code très similaire à votre gotodéclaration. Vous n'avez simplement pas à vous en occuper.

int ComputeExampleValue(int i, ExampleEnum exampleValue) {
    switch (exampleValue) {
        case One: return i + 1;
        case OneAndTwo: return ComputeExampleValue(i + 2, ExampleEnum.One);
        case OneAndThree: return ComputeExampleValue(i + 3, ExampleEnum.One);
        case Two: return i + 2;
        case TwoAndThree: return ComputeExampleValue(i + 2, ExampleEnum.Three);
        case Three: return i + 3;
   }
}
Philipp
la source
C'est une solution unique! +1 Je ne pense pas avoir besoin de le faire de cette façon, mais c'est bien pour les futurs lecteurs!
PerpetualJ
2

La solution acceptée est correcte et constitue une solution concrète à votre problème. Cependant, je voudrais poser une solution alternative, plus abstraite.

D'après mon expérience, l'utilisation d'énums pour définir le flux de la logique constitue une odeur de code, car elle est souvent le signe d'une conception de classe médiocre.

J'ai rencontré un exemple concret de ce qui se passe dans le code sur lequel j'ai travaillé l'année dernière. Le développeur d'origine avait créé une classe unique, chargée de la logique d'importation et d'exportation, et basculant entre les deux sur la base d'une énumération. À présent, le code était similaire et comportait un code dupliqué, mais il était suffisamment différent pour rendre le code beaucoup plus difficile à lire et pratiquement impossible à tester. J'ai fini par refactoriser cela en deux classes distinctes, ce qui les a simplifiées et m'a permis de repérer et d'éliminer un certain nombre de bogues non signalés.

Encore une fois, je dois dire que l'utilisation d'énums pour contrôler le flux de la logique est souvent un problème de conception. Dans le cas général, Enums devrait être principalement utilisé pour fournir des valeurs adaptées au type et sécurisées pour le consommateur, lorsque les valeurs possibles sont clairement définies. Ils sont mieux utilisés en tant que propriété (par exemple, en tant qu'ID de colonne dans une table) qu'en tant que mécanisme de contrôle logique.

Considérons le problème présenté dans la question. Je ne connais pas vraiment le contexte ici, ni ce que représente cette énumération. Est-ce que c'est dessiner des cartes? Dessiner des images? Tirer du sang? L'ordre est-il important? Je ne sais pas non plus à quel point la performance est importante. Si les performances ou la mémoire sont essentielles, cette solution ne sera probablement pas celle que vous souhaitez.

Dans tous les cas, considérons l'énumération:

// All possible combinations of One - Eight.
public enum ExampleEnum {
    One,
    Two,
    TwoOne,
    Three,
    ThreeOne,
    ThreeTwo,
    ThreeOneTwo
}

Nous avons ici un certain nombre de valeurs enum différentes représentant différents concepts d’entreprise.

Ce que nous pourrions utiliser à la place, ce sont des abstractions pour simplifier les choses.

Considérons l'interface suivante:

public interface IExample
{
  void Draw();
}

Nous pouvons alors implémenter cela en tant que classe abstraite:

public abstract class ExampleClassBase : IExample
{
  public abstract void Draw();
  // other common functionality defined here
}

Nous pouvons avoir une classe concrète pour représenter les dessins un, deux et trois (qui, par souci d'argument, ont une logique différente). Ceux-ci pourraient potentiellement utiliser la classe de base définie ci-dessus, mais je suppose que le concept DrawOne est différent du concept représenté par l'énumération:

public class DrawOne
{
  public void Draw()
  {
    // Drawing logic here
  }
}

public class DrawTwo
{
  public void Draw()
  {
    // Drawing two logic here
  }
}

public class DrawThree
{
  public void Draw()
  {
    // Drawing three logic here
  }
}

Et maintenant, nous avons trois classes distinctes qui peuvent être composées pour fournir la logique aux autres classes.

public class One : ExampleClassBase
{
  private DrawOne drawOne;

  public One(DrawOne drawOne)
  {
    this.drawOne = drawOne;
  }

  public void Draw()
  {
    this.drawOne.Draw();
  }
}

public class TwoOne : ExampleClassBase
{
  private DrawOne drawOne;
  private DrawTwo drawTwo;

  public One(DrawOne drawOne, DrawTwo drawTwo)
  {
    this.drawOne = drawOne;
    this.drawTwo = drawTwo;
  }

  public void Draw()
  {
    this.drawOne.Draw();
    this.drawTwo.Draw();
  }
}

// the other six classes here

Cette approche est beaucoup plus verbeuse. Mais cela a des avantages.

Considérez la classe suivante, qui contient un bogue:

public class ThreeTwoOne : ExampleClassBase
{
  private DrawOne drawOne;
  private DrawTwo drawTwo;
  private DrawThree drawThree;

  public One(DrawOne drawOne, DrawTwo drawTwo, DrawThree drawThree)
  {
    this.drawOne = drawOne;
    this.drawTwo = drawTwo;
    this.drawThree = drawThree;
  }

  public void Draw()
  {
    this.drawOne.Draw();
    this.drawTwo.Draw();
  }
}

À quel point est-il plus simple de repérer l'appel drawThree.Draw () manquant? Et si l'ordre est important, l'ordre des appels de tirage au sort est également très facile à voir et à suivre.

Inconvénients de cette approche:

  • Chacune des huit options présentées nécessite un cours séparé
  • Cela utilisera plus de mémoire
  • Cela rendra votre code superficiellement plus grand
  • Parfois, cette approche n’est pas possible, bien que certaines variations puissent être possibles.

Avantages de cette approche:

  • Chacune de ces classes est complètement testable; parce que
  • La complexité cyclomatique des méthodes de tirage est faible (et en théorie, je pourrais me moquer des classes DrawOne, DrawTwo ou DrawThree si nécessaire)
  • Les méthodes de tirage au sort sont compréhensibles - un développeur n'a pas à se lier le cerveau en travaillant à déterminer ce que la méthode fait.
  • Les bugs sont faciles à repérer et difficiles à écrire
  • Les classes se composent en classes de plus haut niveau, ce qui signifie qu'il est facile de définir une classe ThreeThreeThree

Considérez cette approche (ou une approche similaire) chaque fois que vous ressentez le besoin de rédiger un code de contrôle logique complexe dans les instructions de cas. Future, vous serez heureux de l'avoir fait.

Stephen
la source
C'est une réponse très bien écrite et je suis entièrement d'accord avec l'approche. Dans mon cas particulier, quelque chose de ce genre serait excessivement excessif compte tenu du code trivial derrière chaque code. Pour le mettre en perspective, imaginez que le code exécute simplement un objet Console.WriteLine (n). C'est pratiquement l'équivalent de ce que faisait le code avec lequel je travaillais. Dans 90% des autres cas, votre solution est certainement la meilleure réponse lorsque vous suivez la POO.
PerpetualJ
Ouais bon point, cette approche n'est pas utile dans toutes les situations. Je voulais le dire ici car il est courant que les développeurs se concentrent sur une approche particulière pour résoudre un problème et il est parfois payant de prendre du recul et de chercher des alternatives.
Stephen
Je suis tout à fait d’accord avec ça, c’est en fait la raison pour laquelle je me suis retrouvé ici parce que j’essayais de résoudre un problème d’évolutivité avec quelque chose que d’autres développeurs ont écrit par habitude.
PerpetualJ
0

Si vous souhaitez utiliser un commutateur ici, votre code sera réellement plus rapide si vous traitez chaque cas séparément.

switch(exampleValue)
{
    case One:
        i++;
        break;
    case Two:
        i += 2;
        break;
    case OneAndTwo:
    case Three:
        i+=3;
        break;
    case OneAndThree:
        i+=4;
        break;
    case TwoAndThree:
        i+=5;
        break;
}

une seule opération arithmétique est effectuée dans chaque cas

également, comme d’autres l’ont déjà dit, si vous envisagez d’utiliser gotos, vous devriez probablement repenser votre algorithme (bien que je concède que le manque de cas de C # peut être une raison pour utiliser un goto). Voir le célèbre article d'Edgar Dijkstra "Aller à la déclaration considérée comme nuisible"

CaptianOvvious
la source
3
Je dirais que c'est une excellente idée pour quelqu'un confronté à un problème plus simple, alors merci de l'avoir postée. Dans mon cas, ce n'est pas si simple.
PerpetualJ
De plus, nous n'avons pas exactement les problèmes actuels d'Edgar au moment de la rédaction de son article; De nos jours, la plupart des gens n'ont même pas de raison valable de haïr le goto, sinon le code est difficile à lire. En toute honnêteté, si c'est abusé, alors oui, sinon, il est pris au piège de la méthode du conteneur, vous ne pouvez donc pas créer de code spaghetti. Pourquoi dépenser des efforts pour travailler autour d'une fonctionnalité d'une langue? Je dirais que goto est archaïque et que vous devriez penser à d’autres solutions dans 99% des cas d’utilisation; mais si vous en avez besoin, c'est pour cela qu'il est.
PerpetualJ
Cela ne va pas bien quand il y a beaucoup de booléens.
user949300 le
0

Pour votre exemple particulier, puisque tout ce que vous vouliez vraiment de l'énumération était un indicateur "faire / ne pas-ne" pour chacune des étapes, la solution qui réécrit vos trois ifdéclarations est préférable à un switchet il est bon que vous en fassiez la réponse acceptée. .

Mais si vous aviez une logique plus complexe qui ne pas fonctionner de manière proprement, je trouve encore le gotos dans la switchdéclaration confusion. Je préférerais voir quelque chose comme ça:

switch (enumVal) {
    case ThreeOneTwo: DrawThree; DrawTwo; DrawOne; break;
    case ThreeTwo:    DrawThree; DrawTwo; break;
    case ThreeOne:    DrawThree; DrawOne; break;
    case TwoOne:      DrawTwo; DrawOne; break;
    case Two:         DrawTwo; break;
    default:          DrawOne; break;
}

Ce n'est pas parfait, mais je pense que c'est mieux ainsi qu'avec le gotos. Si les séquences d'événements sont si longues et se dupliquent tellement qu'il n'a pas vraiment de sens d'épeler la séquence complète pour chaque cas, je préférerais un sous-programme plutôt que gotopour réduire la duplication de code.

David K
la source
0

Je ne suis pas sûr que quiconque ait vraiment une raison de haïr le mot-clé goto de nos jours. C'est définitivement archaïque et inutile dans 99% des cas d'utilisation, mais c'est une caractéristique du langage pour une raison.

La raison pour détester le gotomot clé est un code comme

if (someCondition) {
    goto label;
}

string message = "Hello World!";

label:
Console.WriteLine(message);

Oops! Cela ne va clairement pas au travail. La messagevariable n'est pas définie dans ce chemin de code. Donc C # ne passera pas ça. Mais cela pourrait être implicite. Considérer

object.setMessage("Hello World!");

label:
object.display();

Et supposons qu’il displayy ait ensuite la WriteLinedéclaration.

Ce type de bogue peut être difficile à trouver car gotoobscurcit le chemin du code.

Ceci est un exemple simplifié. Supposons qu'un exemple réel ne soit pas aussi évident. Il peut y avoir cinquante lignes de code entre label:et l’utilisation de message.

Le langage peut aider à résoudre ce problème en limitant l'utilisation qui gotopeut être faite, en descendant seulement des blocs. Mais le C # goton'est pas limité comme ça. Il peut sauter par-dessus le code. De plus, si vous voulez limiter goto, il est également bon de changer le nom. D'autres langues sont utilisées breakpour descendre des blocs, avec un nombre (de blocs à quitter) ou une étiquette (sur l'un des blocs).

Le concept de gotoest une instruction de bas niveau en langage machine. Mais la raison principale pour laquelle nous avons des langages de niveau supérieur est que nous sommes limités aux abstractions de niveau supérieur, par exemple la portée variable.

Cela étant dit, si vous utilisez le C # gotodans une switchinstruction pour passer d'un cas à l'autre, c'est raisonnablement inoffensif. Chaque cas est déjà un point d'entrée. Je pense toujours que le qualifier de gotostupide dans cette situation, car il confond cet usage inoffensif de gotoavec des formes plus dangereuses. Je préférerais de beaucoup qu'ils utilisent quelque chose comme continueça pour ça. Mais pour une raison quelconque, ils ont oublié de me demander avant d’écrire la langue.

mdfst13
la source
2
Dans la C#langue, vous ne pouvez pas sauter par-dessus les déclarations de variables. Pour preuve, lancez une application console et entrez le code que vous avez fourni dans votre message. Vous obtiendrez une erreur de compilateur dans votre étiquette indiquant que l'erreur est l' utilisation de la variable locale non affectée 'message' . Dans les langues où cela est autorisé, c'est une préoccupation valable, mais pas dans le langage C #.
PerpetualJ
0

Lorsque vous avez autant de choix (et même plus, comme vous dites), alors peut-être que ce n'est pas du code mais des données.

Créez un dictionnaire mappant des valeurs d'énumération en actions, exprimé sous forme de fonctions ou sous la forme d'un type d'énum plus simple représentant les actions. Ensuite, votre code peut être réduit à une simple recherche dans un dictionnaire, suivi de l’appel de la valeur de la fonction ou de l’activation des choix simplifiés.

alexis
la source
-7

Utilisez une boucle et la différence entre pause et continuer.

do {
  switch (exampleValue) {
    case OneAndTwo: i += 2; break;
    case OneAndThree: i += 3; break;
    case Two: i += 2; continue;
    case TwoAndThree: i += 2;
    // dropthrough
    case Three: i += 3; continue;
    default: break;
  }
  i++;
} while(0);
QuentinUK
la source