Beiträge
Untersuchung des Falls für Switch-Anweisungen
6. Dezember 2015 • 9 Min. Lesezeit

Seit fast 50 Jahren ist die Switch-Anweisung (auch als Case-Anweisung bekannt) ein integraler Bestandteil der Programmierung. In den letzten Jahren behaupten jedoch einige, dass die Switch-Anweisung ihre Nützlichkeit überlebt hat. Andere gehen sogar noch weiter und bezeichnen die Switch-Anweisung als Code-Smell.
1952 konzipierte Stephen Kleene die Switch-Anweisung in seinem Papier Introduction to Metamathematics. Die erste bemerkenswerte Implementierung erfolgte in ALGOL 58 im Jahr 1958. Später wurde die Switch-Anweisung in die unauslöschliche Programmiersprache C aufgenommen, die, wie wir wissen, die meisten modernen Programmiersprachen beeinflusst hat.
Springen wir in die Gegenwart vor, und praktisch jede Sprache hat eine Switch-Anweisung. Jedoch haben einige wenige Sprachen die Switch-Anweisung weggelassen. Die bemerkenswerteste ist Smalltalk.
Das weckte meine Neugier: Warum wurde die Switch-Anweisung von Smalltalk ausgeschlossen?
Andy Bower, einer der Schöpfer/Befürworter hinter Dolphin Smalltalk, teilte seine Gedanken darüber mit, warum Smalltalk die Switch-Anweisung ausschloss:
Als ich das erste Mal von C++ zu Smalltalk kam, konnte ich nicht verstehen, wie eine angeblich vollwertige Sprache kein Switch/Case-Konstrukt unterstützte. Schließlich dachte ich, als ich das erste Mal von BASIC zur “strukturierten Programmierung” wechselte, dass Switch eines der besten Dinge seit geschnittenem Brot war. Da Smalltalk jedoch keinen Switch unterstützte, musste ich nach Wegen suchen und verstehen, wie ich diesen Mangel überwinden konnte. Die richtige Antwort ist natürlich, Polymorphismus zu verwenden und die Objekte selbst zum korrekten Code-Stück dispatchen zu lassen. Dann erkannte ich, dass es überhaupt kein “Mangel” war, sondern dass Smalltalk mich zu einem viel feinkörnigeren OOP-Design zwang, als ich es in C++ gewohnt war. Wenn eine Switch-Anweisung verfügbar gewesen wäre, hätte es viel länger gedauert, dies zu lernen, oder schlimmer noch, ich würde vielleicht immer noch C++/Java-Pseudo-Objekt-Stil in Smalltalk programmieren.
Ich würde behaupten, dass es in normaler OOP keinen wirklichen Bedarf für eine Switch-Anweisung gibt. Manchmal, beim Interface zu einer Nicht-OOP-Welt (wie dem Empfangen und Dispatchen von WM_XXXX Windows-Nachrichten, die keine Objekte, sondern nur Ganzzahlen sind), wäre eine Switch-Anweisung nützlich. In diesen Situationen gibt es Alternativen (wie das Dispatchen aus einem Dictionary), und die Anzahl der Male, wo sie auftreten, rechtfertigt nicht die Aufnahme zusätzlicher Syntax.
Hatte Andy recht? Sind wir ohne die Switch-Anweisung besser dran? Würden andere Sprachen auch davon profitieren, die Switch-Anweisung auszuschließen?
Um etwas Licht auf diese Frage zu werfen, habe ich einen Vergleich zwischen einer Switch-Anweisung, einem Dictionary und Polymorphismus zusammengestellt. Nennen wir es einen Showdown. Möge die beste Implementierung gewinnen!
Jede Implementierung hat eine Methode, die einen Parameter, eine Ganzzahl, nimmt und einen String zurückgibt. Wir werden zyklomatische Komplexität und Wartbarkeitsindex verwenden, um jede Implementierung zu untersuchen. Dann werden wir eine ganzheitliche Sicht auf alle drei Implementierungen werfen.
Der Code.
Switch-Anweisung
Wartbarkeitsindex | 72 |
---|---|
Zyklomatische Komplexität | 6 |
public class SwitchWithFourCases
{
public string SwitchStatment(int color)
{
var colorString = "Red";
switch (color)
{
case 1:
colorString = "Green";
break;
case 2:
colorString = "Blue";
break;
case 3:
colorString = "Violet";
break;
case 4:
colorString = "Orange";
break;
}
return colorString;
}
}
Dictionary
Wartbarkeitsindex | 73 |
---|---|
Zyklomatische Komplexität | 3 |
public class DictionaryWithFourItems
{
public string Dictionary(int color)
{
var colorString = "Red";
var colors = new Dictionary<int, string> {{1, "Green"}, {2, "Blue"}, {3, "Violet"}, {4, "Orange"}};
var containsKey = colors.ContainsKey(color);
if (containsKey)
{
colorString = colors[color];
}
return colorString;
}
}
Polymorphismus
Gesamter Wartbarkeitsindex | 94 |
---|---|
Gesamte zyklomatische Komplexität | 15 |
Interface
Wartbarkeitsindex | 100 |
---|---|
Zyklomatische Komplexität | 1 |
public interface IColor
{
string ColorName { get; }
}
Factory
Wartbarkeitsindex | 76 |
---|---|
Zyklomatische Komplexität | 4 |
public class ColorFactory
{
public string GetColor(int color)
{
IColor defaultColor = new RedColor();
var colors = GetColors();
var containsKey = colors.ContainsKey(color);
if (containsKey)
{
var c = colors[color];
return c.ColorName;
}
return defaultColor.ColorName;
}
private static IDictionary<int, IColor> GetColors()
{
return new Dictionary<int, IColor>
{
{1, new GreenColor()},
{2, new BlueColor()},
{3, new VioletColor()},
{4, new OrangeColor()},
{5, new MagentaColor()}
};
}
}
Implementierung
Wartbarkeitsindex | 97 |
---|---|
Zyklomatische Komplexität | 2 |
public class BlueColor : IColor
{
public string ColorName => "Blue";
}
public class RedColor : IColor
{
public string ColorName => "Red";
}
public class GreenColor : IColor
{
public string ColorName => "Green";
}
public class MagentaColor : IColor
{
public string ColorName => "Magenta";
}
public class VioletColor : IColor
{
public string ColorName => "Violet";
}
Die Ergebnisse
Bevor ich in die Ergebnisse eintauche, lassen Sie uns zyklomatische Komplexität und Wartbarkeitsindex definieren:
- Zyklomatische Komplexität ist das Maß für Logik-Verzweigungen. Je niedriger die Zahl, desto besser.
- Wartbarkeitsindex misst die Wartbarkeit des Codes. Er liegt auf einer Skala zwischen 0 und 100. Je höher die Zahl, desto besser.
Zyklomatische Komplexität | Wartbarkeitsindex | |
---|---|---|
Switch-Anweisung | 6 | 72 |
Dictionary | 3 | 73 |
Polymorphismus | 15 | 94 |
Wir werden zuerst die zyklomatische Komplexität untersuchen.
Die Ergebnisse für die zyklomatische Komplexität sind eindeutig. Die Dictionary-Implementierung ist die einfachste. Bedeutet das, dass sie die beste Lösung ist? Nein, wie wir sehen werden, wenn wir den Wartbarkeitsindex bewerten.
Die meisten würden denken wie ich, die Implementierung mit der niedrigsten zyklomatischen Komplexität ist die wartbarste — wie könnte es anders sein?
In unserem Szenario ist die Implementierung mit der niedrigsten zyklomatischen Komplexität nicht die wartbarste. Tatsächlich ist es in unserem Szenario das Gegenteil. Die komplexeste Implementierung ist die wartbarste! Verblüffend!
Wenn Sie sich erinnern, je höher der Wartbarkeitsindex-Score, desto besser. Um es auf den Punkt zu bringen, Polymorphismus hat den besten Wartbarkeitsindex-Score — aber er hat auch die höchste zyklomatische Komplexität. Was ist los? Das scheint nicht richtig zu sein.
Warum ist die komplexeste Implementierung die wartbarste? Um das zu beantworten, müssen wir den Wartbarkeitsindex verstehen.
Der Wartbarkeitsindex besteht aus 4 Metriken: zyklomatische Komplexität, Codezeilen, Anzahl der Kommentare und das Halstead-Volumen. Die ersten drei Metriken sind relativ bekannt, aber die letzte, das Halstead-Volumen, ist relativ unbekannt. Wie die zyklomatische Komplexität versucht das Halstead-Volumen, Code-Komplexität objektiv zu messen.
Einfach ausgedrückt misst das Halstead-Volumen die Anzahl der beweglichen Teile (Variablen, Systemaufrufe, Arithmetik, Coding-Konstrukte usw.) im Code. Je höher die Anzahl der beweglichen Teile, desto mehr Komplexität. Je niedriger die Anzahl der beweglichen Teile, desto geringer die Komplexität. Das erklärt, warum die polymorphe Implementierung beim Wartbarkeitsindex hoch punktet; die Klassen haben wenig bis keine beweglichen Teile. Eine andere Art, das Halstead-Volumen zu betrachten, ist, dass es die Dichte der “beweglichen Teile” misst.
Was ist Software, wenn sie sich nicht ändern soll? Um die reale Welt zu reflektieren, führen wir Änderungen ein. Ich habe jeder Implementierung eine neue Farbe hinzugefügt.
Unten sind die überarbeiteten Ergebnisse.
Zyklomatische Komplexität | Wartbarkeitsindex | |
---|---|---|
Switch-Anweisung | 7 | 70 |
Dictionary | 3 | 73 |
Polymorphismus | 17 | 95 |
Die Switch-Anweisung und die polymorphen Ansätze erhöhten beide die zyklomatische Komplexität um eine Einheit, aber interessanterweise erhöhte sich das Dictionary nicht. Zuerst war ich davon verwirrt, aber dann erkannte ich, dass das Dictionary die Farben als Daten betrachtet und die anderen beiden Implementierungen die Farben als Code behandeln. Ich komme zur Sache.
Wenn wir unsere Aufmerksamkeit auf den Wartbarkeitsindex richten, verringerte sich nur einer, die Switch-Anweisung, in der Wartbarkeit. Der Wartbarkeitsscore des Polymorphismus verbesserte sich, und dennoch steigt auch die Komplexität (wir würden es vorziehen, dass sie abnimmt). Wie ich oben erwähnte, ist das kontraintuitiv.
Unser Vergleich zeigt, dass Dictionaries aus Komplexitätssicht unendlich skalieren können. Der polymorphe Ansatz ist bei weitem der wartbarste und scheint in der Wartbarkeit zu steigen, wenn mehr Szenarien hinzugefügt werden. Die Switch-Anweisung steigt in der Komplexität und nimmt in der Wartbarkeit ab, als das neue Szenario hinzugefügt wurde. Schon bevor wir das neue Szenario hinzufügten, hatte sie die schlechtesten Maße für zyklomatische Komplexität und Wartbarkeitsindex.
Jem Finch von Google teilte seine Gedanken zu den Mängeln der Switch-Anweisungen mit:
1. Polymorphe Methodenimplementierungen sind lexikalisch voneinander isoliert. Variablen können hinzugefügt, entfernt, modifiziert werden usw., ohne jedes Risiko, unabhängigen Code in einem anderen Zweig der Switch-Anweisung zu beeinträchtigen.
2. Polymorphe Methodenimplementierungen sind garantiert an den richtigen Ort zurückzukehren, vorausgesetzt sie terminieren. Switch-Anweisungen in einer Fall-durch-Sprache wie C/C++/Java erfordern eine fehleranfällige “break”-Anweisung, um sicherzustellen, dass sie zur Anweisung nach dem Switch zurückkehren und nicht zum nächsten Case-Block.
3. Die Existenz einer polymorphen Methodenimplementierung kann vom Compiler erzwungen werden, der sich weigert, das Programm zu kompilieren, wenn eine polymorphe Methodenimplementierung fehlt. Switch-Anweisungen bieten keine solche Vollständigkeitsprüfung.
4. Polymorphes Methoden-Dispatching ist erweiterbar ohne Zugang zu (oder Neukompilierung von) anderem Quellcode. Das Hinzufügen eines weiteren Falls zu einer Switch-Anweisung erfordert Zugang zum ursprünglichen Dispatching-Code, nicht nur an einer Stelle, sondern an jeder Stelle, wo das relevante Enum geswitcht wird.
5. … Sie können polymorphe Methoden unabhängig vom Switching-Apparat testen. Die meisten Funktionen, die wie das Beispiel des Autors switchen, enthalten anderen Code, der dann nicht separat getestet werden kann; virtuelle Methodenaufrufe hingegen können es.
6. Polymorphe Methodenaufrufe garantieren konstante Zeit-Dispatch. Kein ausreichend intelligenter Compiler ist notwendig, um das, was natürlich ein lineares Zeit-Konstrukt ist (die Switch-Anweisung mit Fall-durch), in ein konstantes Zeit-Konstrukt zu konvertieren.
Unglücklicherweise oder glücklicherweise, je nach Ihrem Lager, haben die meisten Sprachen eine Switch-Anweisung, und sie werden so bald nicht verschwinden. Mit diesem im Hinterkopf ist es gut zu wissen, was unter der Haube passiert, wenn Switch-Anweisungen kompiliert werden.
Es gibt drei Switch-Anweisungs-Optimierungen, die auftreten können:
- If-elseif-Anweisungen – Wenn eine Switch-Anweisung eine kleine Anzahl von Fällen oder spärliche Fälle hat (nicht-inkrementelle Werte, wie 10, 250, 1000), wird sie in eine if-elseif-Anweisung konvertiert.
- Jump Table – Bei größeren Mengen von benachbarten Fällen (1, 2, 3, 4, 5) konvertiert der Compiler die Switch-Anweisung in eine Jump Table. Eine Jump Table ist im Wesentlichen eine Hashtable mit einem Zeiger (denken Sie an goto-Anweisung) zur Funktion im Speicher.
- Binäre Suche – Für große Mengen von spärlichen Fällen kann der Compiler eine binäre Suche implementieren, um den Fall schnell zu identifizieren, ähnlich wie ein Index in einer Datenbank funktioniert. In außergewöhnlichen Fällen, wo Fälle eine große Anzahl von spärlichen und benachbarten Fällen sind, wird der Compiler eine Kombination der drei Optimierungen verwenden.
Zusammenfassung
In einer objektorientierten Welt ist die Switch-Anweisung, konzipiert 1952, ein Grundpfeiler des Software-Ingenieurs. Eine bemerkenswerte Ausnahme ist Smalltalk, wo die Designer sich entschieden, die Switch-Anweisung auszuschließen.
Verglichen mit alternativen äquivalenten Implementierungen, dem Dictionary und Polymorphismus, schnitt die Switch-Anweisung nicht so gut ab.
Die Switch-Anweisung ist da, um zu bleiben, aber wie unser Vergleich gezeigt hat, gibt es bessere Alternativen zur Switch-Anweisung.
Die Implementierungen sind auf Github verfügbar.
Autor: Chuck Conway ist spezialisiert auf Software-Engineering und Generative KI. Verbinden Sie sich mit ihm in den sozialen Medien: X (@chuckconway) oder besuchen Sie ihn auf YouTube.