Vermeiden Sie das "goto" -Voodoo?

47

Ich habe eine switchStruktur, die mehrere Fälle zu behandeln hat. Das switchfunktioniert über ein, enumdas die Ausgabe des doppelten Codes durch kombinierte Werte aufwirft:

// 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.
}

Derzeit behandelt die switchStruktur jeden Wert separat:

// 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;
     ...
}

Sie bekommen die Idee dort. Ich habe dies derzeit in eine gestapelte ifStruktur unterteilt, um stattdessen Kombinationen mit einer einzelnen Zeile zu behandeln:

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

Dies wirft das Problem von unglaublich langen logischen Auswertungen auf, die verwirrend zu lesen und schwierig zu warten sind. Nachdem ich dies überarbeitet hatte, begann ich über Alternativen nachzudenken und über die Idee einer switchStruktur mit Fall-Through-zwischen-Fällen nachzudenken .

Ich muss gotoin diesem Fall ein verwenden, da C#es kein Durchfallen zulässt. Es verhindert jedoch die unglaublich langen logischen Ketten, obwohl es in der switchStruktur herumspringt, und bringt immer noch Code-Duplizierung mit sich.

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;
}

Dies ist immer noch keine saubere Lösung und es gibt ein Stigma im Zusammenhang mit dem gotoKeyword, das ich vermeiden möchte. Ich bin mir sicher, dass es einen besseren Weg geben muss, das aufzuräumen.


Meine Frage

Gibt es eine bessere Möglichkeit, diesen speziellen Fall zu behandeln, ohne die Lesbarkeit und Wartbarkeit zu beeinträchtigen?

PerpetualJ
quelle
28
Es hört sich so an, als ob Sie eine große Debatte führen möchten. Sie benötigen jedoch ein besseres Beispiel für einen guten Fall, um goto verwenden zu können. flag enum löst dieses Problem ordentlich, auch wenn Ihr if-Statement-Beispiel nicht akzeptabel war und es für mich gut aussieht
Ewan
5
@Ewan Niemand benutzt den goto. Wann haben Sie sich das letzte Mal den Linux-Kernel-Quellcode angesehen?
John Douma
6
Sie verwenden, gotowenn die übergeordnete Struktur in Ihrer Sprache nicht vorhanden ist. Ich wünschte manchmal, es gäbe eine fallthru; Stichwort, um diese besondere Verwendung von loszuwerden, gotoaber na ja.
Joshua
25
Im Allgemeinen haben gotoSie wahrscheinlich viele andere (und bessere) Entwurfs- und / oder Implementierungsalternativen übersehen , wenn Sie das Bedürfnis haben, a in einer Hochsprache wie C # zu verwenden. Sehr entmutigt.
Code_dredd
11
Die Verwendung von "goto case" in C # unterscheidet sich von der Verwendung eines allgemeineren "goto", da auf diese Weise ein expliziter Fallthrough erzielt wird.
Hammerite

Antworten:

175

Ich finde den Code mit den gotoAussagen schwer zu lesen . Ich würde empfehlen, Ihre enumanders zu strukturieren . Wenn es sich bei Ihrem Beispiel enumum ein Bitfeld handelt, in dem jedes Bit eine der Auswahlmöglichkeiten darstellt, könnte dies folgendermaßen aussehen:

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

Das Attribut Flags teilt dem Compiler mit, dass Sie Werte einrichten, die sich nicht überlappen. Der Code, der diesen Code aufruft, könnte das entsprechende Bit setzen. Sie könnten dann so etwas tun, um zu verdeutlichen, was passiert:

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

Dies erfordert den Code, myEnumder eingerichtet wird, um die Bitfelder ordnungsgemäß festzulegen und mit dem Attribut Flags zu kennzeichnen. Sie können dies jedoch tun, indem Sie die Werte der Aufzählungen in Ihrem Beispiel wie folgt ändern:

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

Wenn Sie eine Zahl in das Formular schreiben 0bxxxx, geben Sie diese binär an. Sie können also sehen, dass wir Bit 1, 2 oder 3 setzen (technisch gesehen 0, 1 oder 2, aber Sie haben die Idee). Sie können Kombinationen auch mit einem bitweisen ODER benennen, wenn die Kombinationen häufig zusammengesetzt werden.

user1118321
quelle
5
Es scheint so ! Aber es muss C # 7.0 oder neuer sein, denke ich.
user1118321
31
Wie dem auch sei, so könnte man es auch tun , wie folgt: public enum ExampleEnum { One = 1 << 0, Two = 1 << 1, Three = 1 << 2, OneAndTwo = One | Two, OneAndThree = One | Three, TwoAndThree = Two | Three };. Sie müssen nicht auf C # 7 + bestehen.
Deduplicator
13
Das [Flags]Attribut signalisiert dem Compiler nichts. Aus diesem Grund müssen Sie die Aufzählungswerte immer noch explizit als Potenzen von 2 deklarieren.
Joe Sewell,
19
@ UKMonkey Ich bin vehement anderer Meinung. HasFlagist ein beschreibender und expliziter Begriff für die auszuführende Operation und abstrahiert die Implementierung von der Funktionalität. Die Verwendung von, &weil sie in anderen Sprachen üblich ist, ist nicht sinnvoller als die Verwendung eines byteTyps anstelle der Deklaration von enum.
BJ Myers
136

IMO ist die Wurzel des Problems, dass dieses Stück Code nicht einmal existieren sollte.

Anscheinend müssen Sie drei unabhängige Bedingungen und drei unabhängige Aktionen ausführen, wenn diese Bedingungen erfüllt sind. Warum wird all das in einem Code zusammengefasst, der drei Boolesche Flags benötigt, um zu bestimmen, was zu tun ist (ob Sie sie in eine Aufzählung verschleiern oder nicht), und führt dann eine Kombination aus drei unabhängigen Dingen aus? Das Prinzip der Einzelverantwortung scheint hier einen Ruhetag zu haben.

Rufen Sie die drei Funktionen auf, zu denen sie gehören (dh, Sie stellen fest, dass die Aktionen ausgeführt werden müssen), und geben Sie den Code in diesen Beispielen in den Papierkorb.

Wenn es zehn Flags und keine drei Aktionen gäbe, würden Sie diese Art von Code erweitern, um 1024 verschiedene Kombinationen zu verarbeiten? Ich hoffe nicht! Wenn 1024 zu viel ist, ist 8 aus dem gleichen Grund auch zu viel.

Alephzero
quelle
10
Tatsächlich gibt es viele gut gestaltete APIs, die alle Arten von Flags, Optionen und verschiedenen anderen Parametern enthalten. Einige können weitergegeben werden, einige haben direkte Auswirkungen und andere werden möglicherweise ignoriert, ohne die SRP zu beeinträchtigen. Wie auch immer, die Funktion könnte sogar einen externen Eingang dekodieren, wer weiß? Außerdem führt reductio ad absurdum oft zu absurden Ergebnissen, insbesondere wenn der Ausgangspunkt nicht einmal so solide ist.
Deduplizierer
9
Hat diese Antwort positiv bewertet. Wenn Sie nur über GOTO diskutieren möchten, ist das eine andere Frage als die, die Sie gestellt haben. Basierend auf den vorgelegten Beweisen haben Sie drei getrennte Anforderungen, die nicht aggregiert werden sollten. Aus Sicht der SE bin ich der Meinung, dass Alephzero die richtige Antwort ist.
8
@Deduplicator Ein Blick auf diesen Mischmasch einiger, aber nicht aller Kombinationen von 1,2 und 3 zeigt, dass dies keine "gut gestaltete API" ist.
user949300
4
Soviel dazu. Die anderen Antworten verbessern nicht wirklich das, was in der Frage steht. Dieser Code muss neu geschrieben werden. Es ist nichts falsch daran, mehr Funktionen zu schreiben.
only_pro
3
Ich liebe diese Antwort, besonders "Wenn 1024 zu viel ist, ist 8 auch zu viel". Aber es ist im Wesentlichen "Polymorphismus verwenden", nicht wahr? Und meine (schwächere) Antwort ist, dass ich viel Mist bekomme, wenn ich das sage. Kann ich Ihre PR-Person einstellen? :-)
user949300
29

Niemals gotos benutzen ist eine der "Lügen an Kinder" -Konzepte der Informatik. Es ist in 99% der Fälle der richtige Ratschlag, und die Zeiten, in denen es nicht so selten und spezialisiert ist, sind weitaus besser für alle, wenn es nur neuen Programmierern als "Verwenden Sie sie nicht" erklärt wird.

Wann sollten sie verwendet werden? Es gibt einige Szenarien , aber die grundlegende, die Sie zu treffen scheinen, ist: Wenn Sie eine Zustandsmaschine codieren . Wenn es keinen besser organisierten und strukturierten Ausdruck Ihres Algorithmus gibt als einen Zustandsautomaten, dann beinhaltet sein natürlicher Ausdruck im Code unstrukturierte Verzweigungen, und es kann nicht viel getan werden, um die Struktur des Algorithmus nicht zu beeinflussen Zustandsmaschine selbst eher undurchsichtig als weniger.

Compiler-Autoren wissen dies, weshalb der Quellcode für die meisten Compiler, die LALR-Parser * implementieren, gotos enthält. Allerdings werden nur sehr wenige Menschen jemals ihre eigenen lexikalischen Analysatoren und Parser codieren.

* - IIRC, es ist möglich, LALL-Grammatiken vollständig rekursiv zu implementieren, ohne auf Sprungtabellen oder andere unstrukturierte Steueranweisungen zurückzugreifen. Wenn Sie also wirklich gegen Goto sind, ist dies ein Ausweg.


Nun lautet die nächste Frage: "Ist dieses Beispiel einer dieser Fälle?"

Was ich sehe, ist, dass Sie drei verschiedene mögliche nächste Zustände haben, abhängig von der Verarbeitung des aktuellen Zustands. Da einer von ihnen ("default") nur eine Codezeile ist, können Sie ihn technisch entfernen, indem Sie diese Codezeile am Ende der Zustände anheften, für die er gilt. Das würde Sie auf 2 mögliche nächste Zustände bringen.

Einer der verbleibenden ("Drei") ist nur von einer Stelle abgezweigt, die ich sehen kann. So können Sie es auf die gleiche Weise loswerden. Am Ende würden Sie Code haben, der so aussieht:

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;
}

Dies war jedoch wieder ein Spielzeugbeispiel, das Sie zur Verfügung gestellt haben. In Fällen, in denen "default" eine nicht triviale Menge an Code enthält, wird "three" aus mehreren Zuständen übernommen, oder (was am wichtigsten ist) eine weitere Wartung wird wahrscheinlich die Zustände ergänzen oder komplizieren , sind Sie ehrlich gesagt besser dran Verwenden von gotos (und vielleicht sogar die Enum-Case-Struktur loswerden, die die Zustandsmaschinen-Natur der Dinge verbirgt, es sei denn, es gibt einen guten Grund, warum es bleiben muss).

TED
quelle
2
@PerpetualJ - Nun, ich bin auf jeden Fall froh, dass Sie etwas Saubereres gefunden haben. Es könnte immer noch interessant sein, wenn das wirklich Ihre Situation ist, es mit den gotos (keine Groß- oder Kleinschreibung oder Aufzählung, nur mit Blöcken und gotos gekennzeichnet) auszuprobieren und zu sehen, wie sie in Bezug auf die Lesbarkeit verglichen werden. Abgesehen davon muss die Option "goto" nicht nur besser lesbar sein, sondern muss auch besser lesbar genug sein, um Argumente und von anderen Entwicklern ausgeführte "Fix" -Versuche abzuwehren, sodass Sie wahrscheinlich die beste Option gefunden haben.
TED
4
Ich glaube nicht, dass Sie mit gotos "besser dran sind", wenn "weitere Wartung die Zustände wahrscheinlich ergänzen oder erschweren wird". Behauptest du wirklich, dass Spaghetti-Code einfacher zu pflegen ist als strukturierter Code? Dies widerspricht einer 40-jährigen Praxis.
user949300
5
@ user949300 - Nicht gegen das Bauen von Zustandsautomaten üben, nicht. Sie können einfach meinen vorherigen Kommentar zu dieser Antwort lesen, um ein Beispiel aus der Praxis zu erhalten. Wenn Sie versuchen, C-Case-Fall-Throughs zu verwenden, die einem Algorithmus der Zustandsmaschine eine künstliche Struktur (ihre lineare Reihenfolge) auferlegen, für die es keine solche Einschränkung gibt, werden Sie feststellen, dass die geometrische Komplexität jedes Mal zunimmt, wenn Sie alle Ihre neu anordnen müssen Bestellungen, um einen neuen Zustand aufzunehmen. Wenn Sie nur Zustände und Übergänge codieren, wie es der Algorithmus verlangt, geschieht dies nicht. Es ist trivial, neue Bundesstaaten hinzuzufügen.
TED
8
@ user949300 - Wiederum zeigt "~ 40 Jahre Erfahrung" beim Erstellen von Compilern, dass gotos der beste Weg für Teile dieser Problemdomäne ist. Wenn Sie sich den Compiler-Quellcode ansehen, werden Sie fast immer gotos finden.
TED
3
@ user949300 Spaghetti-Code wird nicht nur bei der Verwendung bestimmter Funktionen oder Konventionen angezeigt. Es kann jederzeit in einer beliebigen Sprache, Funktion oder Konvention angezeigt werden. Die Ursache ist, dass die Sprache, Funktion oder Konvention in einer Anwendung verwendet wird, für die sie nicht vorgesehen ist, und dass sie sich nach hinten biegt, um sie abzunehmen. Verwenden Sie immer das richtige Werkzeug für den Job, und ja, manchmal bedeutet das, dass Sie verwenden goto.
Abion47
26

Die beste Antwort ist Polymorphismus .

Eine andere Antwort, die, IMO, das Wenn-Zeug klarer und wohl kürzer macht :

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

goto ist wahrscheinlich meine 58. Wahl hier ...

user949300
quelle
5
+1 für das Vorschlagen von Polymorphismus, obwohl dies im Idealfall den Client-Code bis auf "Call ();" vereinfachen könnte, indem tell not ask - verwendet wird, um die Entscheidungslogik vom konsumierenden Client in den Klassenmechanismus und die Klassenhierarchie zu verschieben.
Erik Eidt
12
Es ist zweifellos sehr mutig, etwas für das Beste zu erklären, was man nicht gesehen hat. Aber ohne zusätzliche Informationen schlage ich ein bisschen Skepsis und Offenheit vor. Vielleicht leidet es an einer kombinatorischen Explosion?
Deduplizierer
Wow, ich denke, dies ist das erste Mal, dass ich für die Andeutung von Polymorphismus herabgestuft wurde. :-)
user949300
25
Einige Leute sagen, wenn sie mit einem Problem konfrontiert werden "Ich werde nur Polymorphismus verwenden". Jetzt haben sie zwei Probleme und Polymorphismus.
Leichtigkeit Rennen mit Monica
8
Ich habe tatsächlich meine Masterarbeit über die Verwendung von Laufzeitpolymorphismus gemacht, um die Gotos in den Zustandsautomaten der Compiler loszuwerden. Das ist durchaus machbar, aber ich habe seitdem in der Praxis festgestellt, dass es in den meisten Fällen den Aufwand nicht wert ist. Es erfordert eine Tonne von OO-Setup-Code, die alle natürlich mit Fehlern enden können (und die diese Antwort weglässt).
TED
10

Warum nicht das:

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, ich stimme zu, das ist hackisch (ich bin übrigens kein C # -Entwickler, entschuldigen Sie mich für den Code), aber aus Sicht der Effizienz ist dies ein Muss? Die Verwendung von Enums als Array-Index ist in C # zulässig.

Laurent Grégoire
quelle
3
Ja. Ein weiteres Beispiel für das Ersetzen von Code durch Daten (was normalerweise aus Gründen der Geschwindigkeit, Struktur und Wartbarkeit wünschenswert ist).
Peter - Setzen Sie Monica
2
Das ist zweifellos eine hervorragende Idee für die letzte Ausgabe der Frage. Und es kann verwendet werden, um von der ersten Enumeration (einem dichten Wertebereich) zu den Flags mehr oder weniger unabhängiger Aktionen zu gelangen, die im Allgemeinen durchgeführt werden müssen.
Deduplikator
Ich habe keinen Zugriff auf einen C # Compiler, aber vielleicht kann der Code gemacht wird sicherer , diese Art von Code verwendet: int[ExempleEnum.length] COUNTS = { 1, 3, 4, 2, 5, 3 };?
Laurent Grégoire
7

Wenn Sie keine Flags verwenden können oder möchten, verwenden Sie eine rekursive Schwanzfunktion. Im 64-Bit-Release-Modus erzeugt der Compiler Code, der Ihrer gotoAnweisung sehr ähnlich ist . Du musst dich einfach nicht damit auseinandersetzen.

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
quelle
Das ist eine einzigartige Lösung! +1 Ich glaube nicht, dass ich das so machen muss, aber großartig für zukünftige Leser!
PerpetualJ
2

Die akzeptierte Lösung ist in Ordnung und eine konkrete Lösung für Ihr Problem. Ich möchte jedoch eine alternative, abstraktere Lösung vorschlagen.

Nach meiner Erfahrung ist die Verwendung von Aufzählungen zur Definition des Logikflusses ein Codegeruch, da dies oft ein Zeichen für ein schlechtes Klassendesign ist.

Ich bin auf ein Beispiel aus der Praxis gestoßen, das in Code vorkommt, an dem ich letztes Jahr gearbeitet habe. Der ursprüngliche Entwickler hatte eine einzelne Klasse erstellt, die sowohl Import- als auch Exportlogik ausführte, und anhand einer Aufzählung zwischen den beiden Klassen gewechselt. Jetzt war der Code ähnlich und hatte einige doppelte Codes, aber es war anders genug, dass dies das Lesen des Codes erheblich erschwerte und das Testen praktisch unmöglich machte. Am Ende habe ich das in zwei separate Klassen umgestaltet, die beide Klassen vereinfachten und es mir ermöglichten, eine Reihe von nicht gemeldeten Fehlern zu erkennen und zu beseitigen.

Ich muss noch einmal feststellen, dass die Verwendung von Aufzählungen zur Steuerung des Logikflusses häufig ein Entwurfsproblem darstellt. Im Allgemeinen sollten Enums hauptsächlich verwendet werden, um typsichere, verbraucherfreundliche Werte bereitzustellen, bei denen die möglichen Werte klar definiert sind. Sie werden besser als Eigenschaft (z. B. als Spalten-ID in einer Tabelle) als als Logiksteuerungsmechanismus verwendet.

Betrachten wir das in der Frage dargestellte Problem. Ich kenne den Kontext hier nicht wirklich, oder was diese Aufzählung darstellt. Zeichnet es Karten? Bilder malen? Blut abnehmen? Ist Ordnung wichtig? Ich weiß auch nicht, wie wichtig Leistung ist. Wenn Leistung oder Arbeitsspeicher kritisch sind, ist diese Lösung wahrscheinlich nicht die gewünschte.

Betrachten wir auf jeden Fall die Aufzählung:

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

Was wir hier haben, sind eine Reihe von verschiedenen Aufzählungswerten, die unterschiedliche Geschäftskonzepte darstellen.

Wir könnten stattdessen Abstraktionen verwenden, um die Dinge zu vereinfachen.

Betrachten wir die folgende Schnittstelle:

public interface IExample
{
  void Draw();
}

Wir können dies dann als abstrakte Klasse implementieren:

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

Wir können eine konkrete Klasse haben, um Zeichnung eins, zwei und drei darzustellen (die aus Gründen der Argumentation eine andere Logik haben). Diese könnten möglicherweise die oben definierte Basisklasse verwenden, aber ich gehe davon aus, dass sich das DrawOne-Konzept von dem durch die Aufzählung dargestellten Konzept unterscheidet:

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
  }
}

Und jetzt haben wir drei separate Klassen, die zusammengesetzt werden können, um die Logik für die anderen Klassen bereitzustellen.

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

Dieser Ansatz ist weitaus ausführlicher. Aber es hat Vorteile.

Betrachten Sie die folgende Klasse, die einen Fehler enthält:

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();
  }
}

Wie viel einfacher ist es, den fehlenden Aufruf von drawThree.Draw () zu erkennen? Und wenn die Reihenfolge wichtig ist, ist die Reihenfolge der Draw Calls auch sehr einfach zu sehen und zu befolgen.

Nachteile dieses Ansatzes:

  • Jede der acht vorgestellten Optionen erfordert eine eigene Klasse
  • Dadurch wird mehr Speicher benötigt
  • Dadurch wird Ihr Code oberflächlich größer
  • Manchmal ist dieser Ansatz nicht möglich, auch wenn es Abweichungen gibt

Vorteile dieses Ansatzes:

  • Jede dieser Klassen ist vollständig testbar; da
  • Die zyklomatische Komplexität der Zeichenmethoden ist gering (und theoretisch könnte ich die DrawOne-, DrawTwo- oder DrawThree-Klassen bei Bedarf verspotten).
  • Die Zeichenmethoden sind verständlich - ein Entwickler muss nicht sein Gehirn in Knoten binden, um herauszufinden, was die Methode tut
  • Bugs sind leicht zu erkennen und schwer zu schreiben
  • Klassen werden zu Klassen höherer Ebene zusammengefasst, was bedeutet, dass das Definieren einer ThreeThreeThree-Klasse einfach ist

Ziehen Sie diesen (oder einen ähnlichen) Ansatz in Betracht, wenn Sie das Gefühl haben, komplexen Logiksteuercode in case-Anweisungen schreiben zu müssen. Zukünftig wirst du froh sein, dass du es getan hast.

Stephen
quelle
Dies ist eine sehr gut geschriebene Antwort und ich stimme dem Ansatz voll und ganz zu. In meinem speziellen Fall wäre etwas, das so ausführlich ist, in Anbetracht des trivialen Codes, der sich dahinter verbirgt, unendlich übertrieben. Stellen Sie sich vor, der Code führt einfach eine Console.WriteLine (n) aus. Das entspricht praktisch dem Code, mit dem ich gearbeitet habe. In 90% aller anderen Fälle ist Ihre Lösung definitiv die beste Antwort, wenn Sie OOP folgen.
PerpetualJ
Ja klar, dieser Ansatz ist nicht in jeder Situation nützlich. Ich wollte es hier platzieren, weil es für Entwickler üblich ist, sich auf einen bestimmten Lösungsansatz zu konzentrieren, und manchmal lohnt es sich, einen Schritt zurückzutreten und nach Alternativen zu suchen.
Stephen
Ich stimme dem voll und ganz zu, es ist eigentlich der Grund, warum ich hier gelandet bin, weil ich versucht habe, ein Skalierbarkeitsproblem mit etwas zu lösen, das ein anderer Entwickler aus Gewohnheit geschrieben hat.
PerpetualJ
0

Wenn Sie beabsichtigen, hier einen Schalter zu verwenden, ist Ihr Code tatsächlich schneller, wenn Sie jeden Fall separat behandeln

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;
}

es wird jeweils nur eine einzige Rechenoperation durchgeführt

auch wie andere angegeben haben, sollten Sie, wenn Sie überlegen, gotos zu verwenden, wahrscheinlich Ihren Algorithmus überdenken (obwohl ich zugeben werde, dass C # mangelnde Groß- und Kleinschreibung ein Grund sein könnte, goto zu verwenden). Siehe Edgar Dijkstra berühmte Zeitung "Go To Statement als schädlich"

CaptianObvious
quelle
3
Ich würde sagen, dass dies eine großartige Idee für jemanden ist, der mit einer einfacheren Angelegenheit konfrontiert ist. Vielen Dank für das Posten. In meinem Fall ist es nicht so einfach.
PerpetualJ
Außerdem haben wir heute nicht genau die Probleme, die Edgar zum Zeitpunkt des Schreibens seiner Arbeit hatte. Die meisten Menschen haben heutzutage nicht einmal einen triftigen Grund, das Goto zu hassen, abgesehen davon, dass Code schwer zu lesen ist. Ehrlich gesagt, wenn es missbraucht wird, dann ja, sonst ist es auf die Containing-Methode beschränkt, so dass Sie nicht wirklich Spaghetti-Code erstellen können. Warum Aufwand betreiben, um ein Merkmal einer Sprache zu umgehen? Ich würde sagen, goto ist archaisch und Sie sollten in 99% der Anwendungsfälle über andere Lösungen nachdenken. aber wenn du es brauchst, ist es dafür da.
PerpetualJ
Dies lässt sich nicht gut skalieren, wenn es viele Boolesche Werte gibt.
user949300
0

Da für Ihr spezielles Beispiel von der Aufzählung nur ein Do / Do-Indikator für jeden der Schritte gewünscht wurde, ist die Lösung, die Ihre drei ifAnweisungen umschreibt , einer vorzuziehen switch, und es ist gut, dass Sie sie zur akzeptierten Antwort gemacht haben .

Aber wenn Sie eine komplexere Logik hatten, die nicht so sauber funktioniert hat, dann finde ich das gotos in der switchAnweisung immer noch verwirrend. Ich würde eher so etwas sehen:

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;
}

Das ist nicht perfekt, aber ich denke, es ist besser so als mit dem gotos. Wenn die Sequenzen von Ereignissen so lang sind und sich so stark duplizieren, dass es wirklich keinen Sinn macht, die vollständige Sequenz für jeden Fall zu buchstabieren, bevorzuge ich eine Subroutine goto, um die Doppelung von Code zu reduzieren.

David K
quelle
0

Ich bin mir nicht sicher, ob irgendjemand in diesen Tagen wirklich einen Grund hat, das goto-Schlüsselwort zu hassen. Es ist definitiv archaisch und wird in 99% der Anwendungsfälle nicht benötigt, aber es ist aus einem bestimmten Grund ein Merkmal der Sprache.

Der Grund, das gotoSchlüsselwort zu hassen, ist Code wie

if (someCondition) {
    goto label;
}

string message = "Hello World!";

label:
Console.WriteLine(message);

Hoppla! Das wird natürlich nicht funktionieren. Die messageVariable ist in diesem Codepfad nicht definiert. Also wird C # das nicht bestehen. Aber es könnte implizit sein. Erwägen

object.setMessage("Hello World!");

label:
object.display();

Und davon ausgehen, dass displaydann die WriteLineAussage drin ist.

Diese Art von Fehler kann schwer zu finden sein, da der gotoCodepfad verdeckt wird.

Dies ist ein vereinfachtes Beispiel. Angenommen, ein reales Beispiel wäre bei weitem nicht so offensichtlich. Zwischen label:und der Verwendung von können fünfzig Codezeilen liegen message.

Die Sprache kann Abhilfe schaffen, indem sie die Verwendungsmöglichkeiten einschränkt gotound nur aus Blöcken heraus absteigt. Aber das C # gotoist nicht so begrenzt. Es kann über Code springen. Wenn Sie einschränken möchten goto, ist es außerdem gut, den Namen zu ändern. Andere Sprachen breakwerden zum Verlassen von Blöcken verwendet, entweder mit einer Nummer (Anzahl der zu beendenden Blöcke) oder einer Bezeichnung (auf einem der Blöcke).

Das Konzept von gotoist eine Maschinensprachenanweisung auf niedriger Ebene. Der ganze Grund, warum wir über höhere Sprachen verfügen, ist, dass wir uns auf die höheren Abstraktionen beschränken, z. B. den variablen Umfang.

Alles in allem, wenn Sie das C # gotoin einer switchAnweisung verwenden, um von Fall zu Fall zu springen, ist dies einigermaßen harmlos. Jeder Fall ist bereits ein Einstiegspunkt. Ich denke immer noch, dass es gotodumm ist , es in dieser Situation zu nennen, da es diesen harmlosen Gebrauch gotomit gefährlicheren Formen in Verbindung bringt . Mir wäre es viel lieber, wenn sie so etwas benutzen würden continue. Aber aus irgendeinem Grund haben sie vergessen, mich zu fragen, bevor sie die Sprache geschrieben haben.

mdfst13
quelle
2
In der C#Sprache können Sie nicht über Variablendeklarationen springen. Starten Sie zum Prüfen eine Konsolenanwendung und geben Sie den Code ein, den Sie in Ihrem Beitrag angegeben haben. In Ihrem Etikett wird ein Compilerfehler angezeigt, der besagt, dass der Fehler durch die Verwendung der nicht zugewiesenen lokalen Variablen "message" verursacht wird . In Sprachen, in denen dies zulässig ist, ist dies ein berechtigtes Problem, jedoch nicht in der C # -Sprache.
PerpetualJ
0

Wenn Sie so viele Möglichkeiten haben (und, wie Sie sagen, noch mehr), dann ist es vielleicht kein Code, sondern Daten.

Erstellen Sie ein Wörterbuch, in dem die Aufzählungswerte den Aktionen zugeordnet werden, entweder als Funktionen oder als einfacher Aufzählungstyp, der die Aktionen darstellt. Dann kann Ihr Code zu einem einfachen Nachschlagen des Wörterbuchs zusammengefasst werden, gefolgt vom Aufrufen des Funktionswerts oder dem Einschalten der vereinfachten Auswahlmöglichkeiten.

alexis
quelle
-7

Verwenden Sie eine Schleife und den Unterschied zwischen break und continue.

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
quelle