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 überholt hat. Andere gehen noch weiter, indem sie die Switch-Anweisung als Code-Smell bezeichnen.
1952 konzipierte Stephen Kleene die Switch-Anweisung in seinem Papier Introduction to Metamathematics. Die erste bemerkenswerte Implementierung war in ALGOL 58 im Jahr 1958. Später wurde die Switch-Anweisung in die unvergessliche C-Programmiersprache aufgenommen, die, wie wir wissen, die meisten modernen Programmiersprachen beeinflusst hat.
Spulen wir bis zur Gegenwart vor und praktisch jede Sprache hat eine Switch-Anweisung. Allerdings haben einige Sprachen die Switch-Anweisung weggelassen. Die bemerkenswerteste ist Smalltalk.
Dies weckte meine Neugier: Warum wurde die Switch-Anweisung aus Smalltalk ausgeschlossen?
Andy Bower, einer der Schöpfer/Befürworter von Dolphin Smalltalk, teilte seine Gedanken darüber, warum Smalltalk die Switch-Anweisung ausschloss:
Als ich zum ersten 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 zum ersten Mal von BASIC zur „strukturierten Programmierung” aufstieg, dass Switch eines der besten Dinge seit Erfindung des Schnellbrots war. Aber weil Smalltalk kein Switch unterstützte, musste ich nach Wegen suchen und verstehen, wie ich diesen Mangel beheben könnte. Die richtige Antwort ist natürlich, Polymorphismus zu verwenden und die Objekte selbst den richtigen Code-Teil aufrufen zu lassen. Dann realisierte ich, dass es überhaupt kein „Mangel” war, sondern dass Smalltalk mich zu einem viel feiner körnigen OOP-Design zwang, als ich mich in C++ gewöhnt hatte. Wenn es eine Switch-Anweisung gegeben hätte, hätte es viel länger gedauert, dies zu lernen, oder schlimmer noch, ich könnte immer noch im C++/Java-Pseudo-Objekt-Stil in Smalltalk programmieren.
Ich würde argumentieren, dass es in normalem OOP keinen wirklichen Bedarf für eine Switch-Anweisung gibt. Manchmal, wenn man sich mit einer nicht-OOP-Welt verbindet (wie das Empfangen und Versenden von WM_XXXX Windows-Meldungen, die keine Objekte sind, sondern nur Ganzzahlen), wäre eine Switch-Anweisung nützlich. In diesen Situationen gibt es Alternativen (wie das Versenden aus einem Dictionary) und die Häufigkeit, mit der sie auftreten, rechtfertigt nicht die Aufnahme zusätzlicher Syntax.
Hatte Andy recht? Geht es uns besser ohne die Switch-Anweisung? Würden andere Sprachen auch davon profitieren, die Switch-Anweisung auszuschließen?
Um 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 Wartbarkeitindex verwenden, um jede Implementierung zu untersuchen. Wir werden dann eine ganzheitliche Sicht auf alle drei Implementierungen nehmen.
Der Code.
Switch-Anweisung
| Wartbarkeitindex | 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
| Wartbarkeitindex | 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 Wartbarkeitindex | 94 |
|---|---|
| Gesamte zyklomatische Komplexität | 15 |
Schnittstelle
| Wartbarkeitindex | 100 |
|---|---|
| Zyklomatische Komplexität | 1 |
public interface IColor
{
string ColorName { get; }
}
Fabrik
| Wartbarkeitindex | 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
| Wartbarkeitindex | 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 mich in die Ergebnisse vertiefen, lassen Sie mich zyklomatische Komplexität und Wartbarkeitindex definieren:
- Zyklomatische Komplexität ist das Maß für die Logik-Verzweigung. Je niedriger die Zahl, desto besser.
- Wartbarkeitindex misst die Wartbarkeit des Codes. Er liegt auf einer Skala zwischen 0 und 100. Je höher die Zahl, desto besser.
| Zyklomatische Komplexität | Wartbarkeitindex | |
|---|---|---|
| Switch-Anweisung | 6 | 72 |
| Dictionary | 3 | 73 |
| Polymorphismus | 15 | 94 |
Wir werden zunächst die zyklomatische Komplexität untersuchen.
Die Ergebnisse für die zyklomatische Komplexität sind unkompliziert. Die Dictionary-Implementierung ist die einfachste. Bedeutet das, dass es die beste Lösung ist? Nein, wie wir sehen werden, wenn wir den Wartbarkeitindex bewerten.
Die meisten würden wie ich denken, dass die Implementierung mit der niedrigsten zyklomatischen Komplexität die wartbarste ist – 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! Unglaublich!
Wenn Sie sich erinnern, je höher der Wartbarkeitindex-Score, desto besser. Um zum Wesentlichen zu kommen, hat Polymorphismus den besten Wartbarkeitindex-Score – aber es hat auch die höchste zyklomatische Komplexität. Was ist da los? Das scheint nicht richtig zu sein.
Warum ist die komplexeste Implementierung die wartbarste? Um dies zu beantworten, müssen wir den Wartbarkeitindex verstehen.
Der Wartbarkeitindex besteht aus 4 Metriken: zyklomatische Komplexität, Codezeilen, die 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, die 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 höher die Komplexität. Je niedriger die Anzahl der beweglichen Teile, desto niedriger die Komplexität. Dies erklärt, warum die polymorphe Implementierung einen hohen Score im Wartbarkeitindex erreicht; die Klassen haben wenig bis keine beweglichen Teile. Eine andere Sichtweise auf das Halstead-Volumen ist, dass es die Dichte der „beweglichen Teile” misst.
Was ist Software, wenn sie sich nicht ändern soll? Um die reale Welt widerzuspiegeln, führen wir eine Änderung ein. Ich habe jeder Implementierung eine neue Farbe hinzugefügt.
Nachfolgend sind die überarbeiteten Ergebnisse aufgeführt.
| Zyklomatische Komplexität | Wartbarkeitindex | |
|---|---|---|
| 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 tat dies das Dictionary nicht. Zunächst war ich verwirrt darüber, aber dann realisierte ich, dass das Dictionary die Farben als Daten betrachtet und die anderen beiden Implementierungen die Farben als Code behandeln.Ich werde mich auf die Fakten konzentrieren.
Wenn wir unsere Aufmerksamkeit auf den Wartbarkeitindex richten, nur einer, die Switch-Anweisung, nahm an Wartbarkeit ab. Die Wartbarkeitsbewertung von Polymorphismus verbesserte sich, und doch stieg auch die Komplexität (wir würden es bevorzugen, dass sie sinkt). Wie ich oben erwähnt habe, ist dies kontraintuitiv.
Unser Vergleich zeigt, dass Dictionaries aus Komplexitätssicht unendlich skalierbar sind. Der polymorphe Ansatz ist bei weitem der wartbarste und scheint an Wartbarkeit zuzunehmen, wenn mehr Szenarien hinzugefügt werden. Die Switch-Anweisung nimmt an Komplexität zu und nimmt an Wartbarkeit ab, wenn das neue Szenario hinzugefügt wurde. Selbst bevor wir das neue Szenario hinzufügten, hatte es die schlechtesten Maße für zyklomatische Komplexität und Wartbarkeitindex.
Jem Finch von Google teilte seine Gedanken zu den Mängeln der Switch-Anweisungen:
1. Polymorphe Methodenimplementierungen sind lexikalisch voneinander isoliert. Variablen können hinzugefügt, entfernt, geändert usw. werden, ohne das Risiko, nicht verwandten Code in einem anderen Zweig der Switch-Anweisung zu beeinflussen.
2. Polymorphe Methodenimplementierungen kehren garantiert an die richtige Stelle zurück, vorausgesetzt, sie beenden. Switch-Anweisungen in einer Durchfallsprache wie C/C++/Java erfordern eine fehleranfällige „Break”-Anweisung, um sicherzustellen, dass sie zur Anweisung nach dem Switch und nicht zum nächsten Case-Block zurückkehren.
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 Methodendispatching ist erweiterbar, ohne Zugriff auf (oder Neukompilierung von) anderem Quellcode zu haben. Das Hinzufügen eines weiteren Falls zu einer Switch-Anweisung erfordert Zugriff auf den ursprünglichen Dispatching-Code, nicht nur an einer Stelle, sondern an jedem Ort, an dem das relevante Enum geschaltet wird.
5. … Sie können polymorphe Methoden unabhängig vom Switching-Apparat testen. Die meisten Funktionen, die wie das Beispiel des Autors schalten, enthalten anderen Code, der dann nicht separat getestet werden kann; virtuelle Methodenaufrufe hingegen können.
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 Durchfall), in ein konstantes Zeit-Konstrukt umzuwandeln.
Leider oder glücklicherweise, je nachdem in welchem Lager Sie sind, haben die meisten Sprachen eine Switch-Anweisung, und sie werden in absehbarer Zeit nicht verschwinden. Vor diesem Hintergrund ist es gut zu wissen, was unter der Haube beim Kompilieren von Switch-Anweisungen passiert.
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 (nicht-inkrementelle Werte wie 10, 250, 1000) hat, wird sie in eine if-elseif-Anweisung konvertiert.
- Jump Table – Bei größeren Sätzen benachbarter Fälle (1, 2, 3, 4, 5) konvertiert der Compiler die Switch-Anweisung in eine Jump Table. Eine Jump Table ist im Wesentlichen eine Hashtabelle mit einem Zeiger (denken Sie an die goto-Anweisung) auf die Funktion im Speicher.
- Binäre Suche – Für große Sätze spärlicher Fälle 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, in denen Fälle eine große Anzahl spärlicher und benachbarter Fälle sind, verwendet der Compiler eine Kombination der drei Optimierungen.
Zusammenfassung
In einer objektorientierten Welt ist die 1952 konzipierte Switch-Anweisung ein Grundpfeiler des Softwareingenieurs. Eine bemerkenswerte Ausnahme ist Smalltalk, wo sich die Designer dafür entschieden, die Switch-Anweisung auszuschließen.
Im Vergleich zu alternativen gleichwertigen Implementierungen, dem Dictionary und Polymorphismus, schnitt die Switch-Anweisung nicht so gut ab.
Die Switch-Anweisung wird 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 ein KI-Ingenieur mit fast 30 Jahren Erfahrung in der Softwareentwicklung. Er entwickelt praktische KI-Systeme – Content-Pipelines, Infrastruktur-Agenten und Tools, die echte Probleme lösen – und teilt seine Erkenntnisse unterwegs. Verbinden Sie sich mit ihm in den sozialen Medien: X (@chuckconway) oder besuchen Sie ihn auf YouTube und auf SubStack.