web-dev-qa-db-fra.com

Éviter le vaudou `goto`?

J'ai une structure switch qui a plusieurs cas à gérer. switch fonctionne sur un enum ce qui pose le problème du code en double à travers 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 structure switch gère 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à-bas. Je l'ai actuellement décomposé en une structure empilée if pour gérer les combinaisons avec une seule ligne à la place:

// 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 d'évaluations logiques incroyablement longues qui sont difficiles à lire et difficiles à maintenir. Après avoir refactorisé cela, j'ai commencé à réfléchir à des alternatives et à l'idée d'une structure switch avec passage entre les cas.

Je dois utiliser un goto dans ce cas puisque C# ne permet pas les retombées. Cependant, il empêche les chaînes logiques incroyablement longues même s'il saute dans la structure switch, et il apporte toujours une 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 assez propre et il y a un stigmate associé au mot clé goto que j'aimerais éviter. Je suis sûr qu'il doit y avoir une meilleure façon de nettoyer cela.


Ma question

Existe-t-il un meilleur moyen de gérer ce cas spécifique sans affecter la lisibilité et la maintenabilité?

47
Emma - PerpetualJ

Je trouve le code difficile à lire avec les instructions goto. Je recommanderais de structurer votre enum différemment. Par exemple, si votre enum était un champ de bits où chaque bit représentait l'un des choix, il 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 alors faire quelque chose comme ça pour faire comprendre 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 configure myEnum pour définir correctement les champs binaires et marqué avec l'attribut Flags. Mais vous pouvez le faire en modifiant les valeurs des énumérations dans 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 sous la forme 0bxxxx, vous le spécifiez en binaire. Vous pouvez donc voir que nous avons défini le bit 1, 2 ou 3 (enfin, techniquement 0, 1 ou 2, mais vous avez l'idée). Vous pouvez également nommer des combinaisons en utilisant un bit OR si les combinaisons peuvent être fréquemment définies ensemble.

174
user1118321

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 à entreprendre si ces conditions sont vraies. Alors pourquoi tout cela est-il canalisé en un seul morceau de code qui a besoin de trois drapeaux booléens pour lui dire quoi faire (que vous les obscurcissiez ou non en une énumération) et fait ensuite une combinaison de trois choses indépendantes? Le principe de la responsabilité unique semble avoir un jour de repos ici.

Placez les appels aux trois fonctions où elles appartiennent (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 indicateurs et actions pas trois, étendriez-vous ce type de code pour gérer 1024 combinaisons différentes? J'espère que non! Si 1024 est trop, 8 est aussi trop, pour la même raison.

136
alephzero

N'utilisez jamais gotos est l'un des concepts "mensonges aux enfants" de l'informatique. C'est le bon conseil 99% du temps, et les fois où ce n'est pas le cas sont si rares et spécialisés que c'est bien mieux pour tout le monde si cela est expliqué aux nouveaux codeurs comme "ne les utilisez pas".

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

Les auteurs de compilateurs le savent, c'est pourquoi le code source de la plupart des compilateurs qui implémentent les analyseurs LALR* contiennent des gotos. Cependant, très peu de gens coderont jamais leurs propres analyseurs et analyseurs lexicaux.

* - IIRC, il est possible d'implémenter des grammaires LALL entièrement récursives-descentes sans avoir recours à des tables de sauts ou à d'autres instructions de contrôle non structurées, donc si vous êtes vraiment anti-goto, c'est une solution.


Maintenant, la question suivante est: "Cet exemple fait-il partie de ces cas?"

Ce que je vois en regardant, c'est que vous avez trois différents états possibles selon le traitement de l'état actuel. Étant donné que l'un d'eux ("par défaut") n'est qu'une ligne de code, vous pouvez techniquement vous en débarrasser en plaçant cette ligne de code à la fin des états auxquels elle s'applique. Cela vous ramènerait à 2 prochains états possibles.

L'un des autres ("Trois") n'est ramifié qu'à un seul endroit que je peux voir. Vous pouvez donc 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 triviale, "trois" est transféré de plusieurs états, ou (plus important encore) une maintenance supplémentaire est susceptible d'ajouter ou de compliquer le , vous feriez vraiment mieux d'utiliser gotos (et peut-être même de vous débarrasser de la structure enum-case qui cache la nature de la machine à états des choses, à moins qu'il n'y ait une bonne raison rester).

29
T.E.D.

La meilleure réponse est tilisez le polymorphisme.

Une autre réponse, qui, l'OMI, rend les choses if plus claires et sans doute plus courtes:

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

goto est probablement mon 58e choix ici ...

26
user949300

Pourquoi pas ça:

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 hackish (je ne suis pas un développeur C # btw, alors excusez-moi pour le code), mais du point de vue de l'efficacité c'est incontournable? tiliser des énumérations comme index de tablea est un C # valide.

10
Laurent Grégoire

Si vous ne pouvez pas ou ne voulez pas utiliser d'indicateurs, utilisez une fonction récursive de queue. En mode de sortie 64 bits, le compilateur produira un code très similaire à votre instruction goto. 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;
   }
}
7
Philipp

La solution acceptée est très bien et est une solution concrète à votre problème. Cependant, je voudrais proposer une solution alternative plus abstraite.

D'après mon expérience, l'utilisation d'énumérations pour définir le flux de la logique est une odeur de code car elle est souvent un signe de mauvaise conception de classe.

Je suis tombé sur 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 seule classe qui faisait à la fois la logique d'importation et d'exportation, et basculait entre les deux sur la base d'une énumération. Maintenant, le code était similaire et avait du code en double, mais il était suffisamment différent pour que cela rende le code beaucoup plus difficile à lire et pratiquement impossible à tester. J'ai fini par refactoriser cela en deux classes distinctes, ce qui a simplifié les deux 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'énumérations pour contrôler le flux de la logique est souvent un problème de conception. Dans le cas général, les énumérations doivent être utilisées principalement pour fournir des valeurs respectueuses du type et conviviales 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.

Examinons 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? Tu as du sang? La commande est-elle importante? Je ne sais pas non plus à quel point les performances sont importantes. Si les performances ou la mémoire sont critiques, 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 d'énumération différentes qui représentent différents concepts commerciaux.

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 ensuite implémenter cela comme une 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 pour des raisons 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 des 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 est également très facile à voir et à suivre.

Inconvénients de cette approche:

  • Chacune des huit options présentées nécessite une classe distincte
  • Cela utilisera plus de mémoire
  • Cela rendra votre code superficiellement plus grand
  • Parfois, cette approche n'est pas possible, même si certaines variations

Avantages de cette approche:

  • Chacune de ces classes est entièrement testable; parce que
  • La complexité cyclomatique des méthodes de dessin est faible (et en théorie je pourrais moquer les classes DrawOne, DrawTwo ou DrawThree si nécessaire)
  • Les méthodes de dessin sont compréhensibles - un développeur n'a pas besoin de faire des nœuds pour déterminer ce que fait la méthode
  • Les bogues 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

Envisagez cette approche (ou une approche similaire) chaque fois que vous ressentez le besoin d'avoir un code de contrôle logique complexe écrit dans des instructions de cas. À l'avenir, vous serez heureux de l'avoir fait.

2
Stephen

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

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

0
alexis

Si vous avez l'intention d'utiliser un commutateur ici, votre code sera en fait plus rapide si vous gérez 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 indiqué si vous envisagez d'utiliser gotos, vous devriez probablement repenser votre algorithme (bien que je concède que l'absence de casse de C # pourrait être une raison d'utiliser un goto). Voir le célèbre article d'Edgar Dijkstra "Aller à la déclaration considérée comme nuisible"

0
CaptianObvious

Pour votre exemple particulier, puisque tout ce que vous vouliez vraiment de l'énumération était un indicateur do/do-not pour chacune des étapes, la solution qui réécrit vos trois instructions if est préférable à une switch et il est bon que vous en ayez fait la réponse acceptée.

Mais si vous aviez une logique plus complexe qui ne pas fonctionnait si bien, alors je trouve toujours les gotos dans l'instruction switch confus. Je préfère 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 les gotos. Si les séquences d'événements sont si longues et se dupliquent tellement que cela n'a vraiment pas de sens d'énoncer la séquence complète pour chaque cas, je préférerais un sous-programme plutôt que goto afin de réduire duplication de code.

0
David K

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

La raison de détester le mot-clé goto est du type code

if (someCondition) {
    goto label;
}

string message = "Hello World!";

label:
Console.WriteLine(message);

Oops! Cela ne fonctionnera clairement pas. La variable message 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 que display contient alors l'instruction WriteLine.

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

Ceci est un exemple simplifié. Supposons qu'un exemple réel ne serait 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 la façon dont goto peut être utilisé, en descendant uniquement des blocs. Mais le C # goto n'est pas limité comme ça. Il peut sauter par-dessus le code. De plus, si vous allez limiter goto, il est tout aussi bien de changer le nom. D'autres langues utilisent break pour descendre des blocs, soit avec un nombre (de blocs à quitter) soit avec une étiquette (sur l'un des blocs).

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

Cela dit, si vous utilisez la C # goto dans une instruction switch 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 l'appeler goto est idiot dans cette situation, car il confond cette utilisation inoffensive de goto avec des formes plus dangereuses. Je préférerais de beaucoup qu'ils utilisent quelque chose comme continue pour cela. Mais pour une raison quelconque, ils ont oublié de me demander avant d'écrire la langue.

0
mdfst13