Beim Schreiben einer switch-Anweisung scheint es zwei Einschränkungen zu geben, was Sie in case-Anweisungen aktivieren können.
Zum Beispiel (und ja, ich weiß, wenn Sie so etwas tun, bedeutet dies wahrscheinlich, dass Ihre objektorientierte (OO) Architektur fragwürdig ist - dies ist nur ein erfundenes Beispiel!),
Type t = typeof(int);
switch (t) {
case typeof(int):
Console.WriteLine("int!");
break;
case typeof(string):
Console.WriteLine("string!");
break;
default:
Console.WriteLine("unknown!");
break;
}
Hier schlägt die switch () - Anweisung mit 'Ein Wert eines erwarteten Integraltyps' fehl und die case-Anweisungen mit 'Ein konstanter Wert wird erwartet'.
Warum gibt es diese Einschränkungen und was ist die zugrunde liegende Rechtfertigung? Ich sehe keinen Grund, warum die switch-Anweisung nur einer statischen Analyse unterliegen muss und warum der eingeschaltete Wert ganzzahlig (dh primitiv) sein muss. Was ist die Rechtfertigung?
c#
switch-statement
ljs
quelle
quelle
Antworten:
Dies ist mein ursprünglicher Beitrag, der einige Debatten ausgelöst hat ... weil er falsch ist :
Tatsächlich ist die C # -Switch-Anweisung nicht immer ein konstanter Zeitzweig.
In einigen Fällen verwendet der Compiler eine CIL-switch-Anweisung, die in der Tat eine konstante Zeitverzweigung unter Verwendung einer Sprungtabelle ist. In spärlichen Fällen, auf die Ivan Hamilton hingewiesen hat, kann der Compiler jedoch etwas ganz anderes generieren.
Dies ist eigentlich recht einfach zu überprüfen, indem verschiedene C # -Schalteranweisungen geschrieben werden, einige spärlich, andere dicht, und die resultierende CIL mit dem Tool ildasm.exe betrachtet werden.
quelle
switch
Anweisung (der CIL), die nicht mit derswitch
Anweisung von C # identisch ist .Es ist wichtig, die Anweisung C # switch nicht mit der Anweisung CIL switch zu verwechseln.
Der CIL-Schalter ist eine Sprungtabelle, für die ein Index für eine Reihe von Sprungadressen erforderlich ist.
Dies ist nur nützlich, wenn die Fälle des C # -Schalters benachbart sind:
Aber von geringem Nutzen, wenn sie nicht:
(Sie benötigen eine Tabelle mit einer Größe von ca. 3000 Einträgen und nur 3 verwendeten Steckplätzen.)
Bei nicht benachbarten Ausdrücken kann der Compiler beginnen, lineare If-else-if-else-Prüfungen durchzuführen.
Bei größeren nicht benachbarten Ausdruckssätzen kann der Compiler mit einer binären Baumsuche und schließlich mit den letzten Elementen beginnen, wenn-sonst-wenn-sonst.
Bei Ausdruckssätzen, die Klumpen benachbarter Elemente enthalten, kann der Compiler eine binäre Baumsuche und schließlich einen CIL-Schalter durchführen.
Dies ist voll von "Mai" und "Macht" und hängt vom Compiler ab (kann bei Mono oder Rotor abweichen).
Ich habe Ihre Ergebnisse auf meinem Computer anhand benachbarter Fälle repliziert:
Dann habe ich auch nicht benachbarte Fallausdrücke verwendet:
Was hier lustig ist, ist, dass die binäre Baumsuche etwas (wahrscheinlich nicht statistisch) schneller erscheint als die CIL-Schalteranweisung.
Brian, Sie haben das Wort " Konstante " verwendet, das aus Sicht der rechnergestützten Komplexitätstheorie eine ganz bestimmte Bedeutung hat. Während das vereinfachte benachbarte Ganzzahlbeispiel CIL erzeugen kann, das als O (1) (konstant) betrachtet wird, ist ein spärliches Beispiel O (log n) (logarithmisch), gruppierte Beispiele liegen irgendwo dazwischen und kleine Beispiele sind O (n) (linear) ).
Dies betrifft nicht einmal die String-Situation, in der eine statische
Generic.Dictionary<string,int32>
Aufladung erstellt werden kann, und führt bei der ersten Verwendung zu einem deutlichen Overhead. Die Leistung hier hängt von der Leistung von abGeneric.Dictionary
.Wenn Sie die C # -Sprachenspezifikation (nicht die CIL-Spezifikation) überprüfen, finden Sie "15.7.2 Die switch-Anweisung" erwähnt "konstante Zeit" nicht oder dass die zugrunde liegende Implementierung sogar die CIL-switch-Anweisung verwendet (seien Sie sehr vorsichtig bei der Annahme solche Sachen).
Letztendlich ist ein C # -Schalter gegen einen ganzzahligen Ausdruck in einem modernen System eine Operation im Submikrosekundenbereich und normalerweise keine Sorge wert.
Natürlich hängen diese Zeiten von den Maschinen und Bedingungen ab. Ich würde diesen Timing-Tests keine Aufmerksamkeit schenken, die Mikrosekunden-Dauer, von der wir sprechen, wird durch die Ausführung von "echtem" Code in den Schatten gestellt (und Sie müssen "echten Code" einfügen, sonst optimiert der Compiler die Verzweigung) oder Jitter im System. Meine Antworten basieren auf der Verwendung von IL DASM , um die vom C # -Compiler erstellte CIL zu untersuchen. Dies ist natürlich nicht endgültig, da die tatsächlichen Anweisungen, die die CPU ausführt, dann von der JIT erstellt werden.
Ich habe die endgültigen CPU-Anweisungen überprüft, die tatsächlich auf meinem x86-Computer ausgeführt wurden, und kann einen einfachen benachbarten Set-Schalter bestätigen, der Folgendes ausführt:
Wo eine binäre Baumsuche voll ist von:
quelle
Der erste Grund, der mir in den Sinn kommt, ist historisch :
Da die meisten C-, C ++ - und Java-Programmierer an solche Freiheiten nicht gewöhnt sind, fordern sie diese nicht.
Ein weiterer, zutreffenderer Grund ist, dass die Sprachkomplexität zunehmen würde :
Sollten die Objekte zunächst mit
.Equals()
oder mit dem==
Bediener verglichen werden? Beide sind in einigen Fällen gültig. Sollten wir dazu eine neue Syntax einführen? Sollten wir dem Programmierer erlauben, seine eigene Vergleichsmethode einzuführen?Darüber hinaus würde das Zulassen des Einschaltens von Objekten die zugrunde liegenden Annahmen über die switch-Anweisung brechen . Es gibt zwei Regeln für die switch-Anweisung, die der Compiler nicht erzwingen kann, wenn Objekte eingeschaltet werden dürfen (siehe Sprachspezifikation für C # Version 3.0 , §8.7.2):
Betrachten Sie dieses Codebeispiel in dem hypothetischen Fall, dass nicht konstante Fallwerte zulässig waren:
Was wird der Code tun? Was ist, wenn die case-Anweisungen neu angeordnet werden? In der Tat ist einer der Gründe, warum C # das Durchfallen von Schaltern illegal gemacht hat, dass die Schalteranweisungen willkürlich neu angeordnet werden könnten.
Diese Regeln sind aus einem bestimmten Grund vorhanden, damit der Programmierer anhand eines Fallblocks sicher wissen kann, unter welchen genauen Bedingungen der Block eingegeben wird. Wenn die oben erwähnte switch-Anweisung auf 100 Zeilen oder mehr anwächst (und dies auch tun wird), ist dieses Wissen von unschätzbarem Wert.
quelle
Übrigens erlaubt VB mit der gleichen zugrunde liegenden Architektur viel flexiblere
Select Case
Anweisungen (der obige Code würde in VB funktionieren) und erzeugt dennoch effizienten Code, wo dies möglich ist, so dass das Argument durch technische Einschränkungen sorgfältig abgewogen werden muss.quelle
Select Case
en VB ist sehr flexibel und spart viel Zeit. Ich vermisse es sehr.Meistens bestehen diese Einschränkungen aufgrund von Sprachdesignern. Die zugrunde liegende Rechtfertigung kann die Kompatibilität mit der Sprachgeschichte, den Idealen oder die Vereinfachung des Compilerdesigns sein.
Der Compiler kann (und tut) wählen:
Die switch-Anweisung ist KEIN konstanter Zeitzweig. Der Compiler findet möglicherweise Verknüpfungen (unter Verwendung von Hash-Buckets usw.), aber kompliziertere Fälle erzeugen komplizierteren MSIL-Code, wobei einige Fälle früher verzweigen als andere.
Um den String-Fall zu behandeln, verwendet der Compiler (irgendwann) a.Equals (b) (und möglicherweise a.GetHashCode ()). Ich denke, es wäre für den Compiler trivial, jedes Objekt zu verwenden, das diese Einschränkungen erfüllt.
Was die Notwendigkeit statischer Fallausdrücke betrifft ... einige dieser Optimierungen (Hashing, Caching usw.) wären nicht verfügbar, wenn die Fallausdrücke nicht deterministisch wären. Aber wir haben bereits gesehen, dass der Compiler manchmal sowieso nur die vereinfachte Wenn-Sonst-Wenn-Sonst-Straße wählt ...
Bearbeiten: lomaxx - Ihr Verständnis des Operators "typeof" ist nicht korrekt. Der Operator "typeof" wird verwendet, um das System.Type-Objekt für einen Typ abzurufen (nichts mit seinen Supertypen oder Schnittstellen zu tun). Das Überprüfen der Laufzeitkompatibilität eines Objekts mit einem bestimmten Typ ist die Aufgabe des Operators "is". Die Verwendung von "typeof" hier, um ein Objekt auszudrücken, ist irrelevant.
quelle
Laut Jeff Atwood handelt es sich bei der switch-Anweisung um eine Programmier-Gräueltat . Verwenden Sie sie sparsam.
Sie können dieselbe Aufgabe häufig mithilfe einer Tabelle ausführen. Beispielsweise:
quelle
enum
Typs eignet . Es ist auch kein Zufall, dass Intellisense automatisch eine switch-Anweisung ausfüllt, wenn Sie eine Variable einesenum
Typs einschalten .switch
Anweisung zu verwenden. Er sagt nicht, dass Sie keine Zustandsmaschinen schreiben sollten, nur dass Sie dasselbe tun können, indem Sie nette spezifische Typen verwenden. Natürlich ist dies in Sprachen wie F # viel einfacher, die Typen haben, die leicht recht komplexe Zustände abdecken können. In Ihrem Beispiel könnten Sie diskriminierte Gewerkschaften verwenden, bei denen der Status Teil des Typs wird, und dieswitch
durch Mustervergleich ersetzen . Oder verwenden Sie zum Beispiel Schnittstellen.Dictionary
erheblich langsamer gewesen wäre als eine optimierteswitch
Aussage ...?Es stimmt, es nicht zu haben , und viele Sprachen verwenden dynamische switch - Anweisungen in der Tat. Dies bedeutet jedoch, dass eine Neuordnung der "case" -Klauseln das Verhalten des Codes ändern kann.
Es gibt einige interessante Informationen hinter den Entwurfsentscheidungen, die hier in "switch" eingegangen sind: Warum ist die C # -Switch-Anweisung so konzipiert, dass kein Durchfallen möglich ist, aber dennoch eine Pause erforderlich ist?
Das Zulassen dynamischer Fallausdrücke kann zu Monstrositäten wie diesem PHP-Code führen:
was ehrlich gesagt nur die
if-else
Aussage verwenden sollte.quelle
Microsoft hat dich endlich gehört!
Mit C # 7 können Sie jetzt:
quelle
Dies ist kein Grund dafür, aber in Abschnitt 8.7.2 der C # -Spezifikation heißt es:
Die C # 3.0-Spezifikation befindet sich unter: http://download.microsoft.com/download/3/8/8/388e7205-bc10-4226-b2a8-75351c669b09/CSharp%20Language%20Specification.doc
quelle
Judahs Antwort oben gab mir eine Idee. Sie können das oben beschriebene Schaltverhalten des OP mit folgenden Elementen "vortäuschen"
Dictionary<Type, Func<T>
:Auf diese Weise können Sie einem Typ Verhalten im selben Stil wie die switch-Anweisung zuordnen. Ich glaube, es hat den zusätzlichen Vorteil, dass es beim Kompilieren zu IL anstelle einer Sprungtabelle im Switch-Stil verschlüsselt wird.
quelle
Ich nehme an, es gibt keinen fundamentalen Grund, warum der Compiler Ihre switch-Anweisung nicht automatisch in Folgendes übersetzen konnte:
Aber das bringt nicht viel.
Eine case-Anweisung zu Integraltypen ermöglicht es dem Compiler, eine Reihe von Optimierungen vorzunehmen:
Es gibt keine Duplizierung (es sei denn, Sie duplizieren Fallbezeichnungen, die der Compiler erkennt). In Ihrem Beispiel könnte t aufgrund der Vererbung mehreren Typen entsprechen. Sollte das erste Match ausgeführt werden? Alle von ihnen?
Der Compiler kann eine switch-Anweisung über einen integralen Typ durch eine Sprungtabelle implementieren, um alle Vergleiche zu vermeiden. Wenn Sie eine Aufzählung mit ganzzahligen Werten von 0 bis 100 aktivieren, wird ein Array mit 100 Zeigern erstellt, einer für jede switch-Anweisung. Zur Laufzeit wird einfach die Adresse aus dem Array basierend auf dem eingeschalteten Ganzzahlwert nachgeschlagen. Dies führt zu einer viel besseren Laufzeitleistung als die Durchführung von 100 Vergleichen.
quelle
switch (t) { case typeof(int): ... }
da Ihre Übersetzung impliziert, dass die Variable zweimal aus dem Speicher abgerufen werdent
musst != typeof(int)
, während letzteres dies tun würde (mutmaßlich) immer den Wert vont
genau einmal lesen . Dieser Unterschied kann die Korrektheit von gleichzeitigem Code beeinträchtigen, der auf diesen hervorragenden Garantien beruht. Weitere Informationen hierzu finden Sie unter Joe Duffys Concurrent Programming unter WindowsLaut der Dokumentation der switch-Anweisung ist dies zulässig, wenn es eine eindeutige Möglichkeit gibt, das Objekt implizit in einen integralen Typ zu konvertieren. Ich denke, Sie erwarten ein Verhalten, bei dem für jede case-Anweisung diese ersetzt wird
if (t == typeof(int))
, aber das würde eine ganze Dose Würmer öffnen, wenn Sie diesen Operator überladen. Das Verhalten würde sich ändern, wenn sich die Implementierungsdetails für die switch-Anweisung ändern, wenn Sie Ihre == -Überschreibung falsch geschrieben haben. Indem die Vergleiche auf integrale Typen und Zeichenfolgen sowie auf Dinge reduziert werden, die auf integrale Typen reduziert werden können (und sollen), vermeiden sie potenzielle Probleme.quelle
Da die Sprache die Verwendung des Zeichenfolgentyps in einer switch-Anweisung zulässt , kann der Compiler vermutlich keinen Code für eine Implementierung mit konstanter Zeitverzweigung für diesen Typ generieren und muss einen Wenn-Dann-Stil generieren.
@mweerden - Ah ich verstehe. Vielen Dank.
Ich habe nicht viel Erfahrung mit C # und .NET, aber es scheint, dass die Sprachdesigner keinen statischen Zugriff auf das Typsystem zulassen, außer unter engen Umständen. Das Schlüsselwort typeof gibt ein Objekt zurück, sodass nur zur Laufzeit darauf zugegriffen werden kann .
quelle
Ich denke, Henk hat es mit der Sache "Kein statischer Zugang zum Typensystem" geschafft
Eine andere Option ist, dass es keine Reihenfolge für Typen gibt, bei denen Zahlen und Zeichenfolgen verwendet werden können. Ein Typschalter kann also keinen binären Suchbaum erstellen, sondern nur eine lineare Suche.
quelle
Ich stimme diesem Kommentar zu, dass die Verwendung eines tabellengesteuerten Ansatzes oft besser ist.
In C # 1.0 war dies nicht möglich, da es keine Generika und anonymen Delegaten gab. Neue Versionen von C # verfügen über das Gerüst, damit dies funktioniert. Es hilft auch, eine Notation für Objektliterale zu haben.
quelle
Ich habe praktisch keine Kenntnisse über C #, aber ich vermute, dass entweder der Wechsel einfach so vorgenommen wurde, wie er in anderen Sprachen vorkommt, ohne darüber nachzudenken, ihn allgemeiner zu gestalten, oder der Entwickler entschied, dass sich eine Erweiterung nicht lohnt.
Genau genommen haben Sie absolut Recht, dass es keinen Grund gibt, diese Einschränkungen aufzuerlegen. Man könnte vermuten, dass der Grund dafür ist, dass die Implementierung für die zulässigen Fälle sehr effizient ist (wie von Brian Ensink ( 44921 ) vorgeschlagen), aber ich bezweifle, dass die Implementierung sehr effizient ist (wrt if-Anweisungen), wenn ich Ganzzahlen und einige zufällige Fälle verwende (zB 345, -4574 und 1234203). Und auf jeden Fall, was schadet es, wenn man alles (oder zumindest mehr) zulässt und sagt, dass es nur für bestimmte Fälle (wie (fast) aufeinanderfolgende Zahlen) effizient ist?
Ich kann mir jedoch vorstellen, dass man Typen aus Gründen wie dem von lomaxx ( 44918) ausschließen möchte ) .
Bearbeiten: @Henk ( 44970 ): Wenn Zeichenfolgen maximal gemeinsam genutzt werden, sind Zeichenfolgen mit gleichem Inhalt auch Zeiger auf denselben Speicherort. Wenn Sie dann sicherstellen können, dass die in den Fällen verwendeten Zeichenfolgen nacheinander im Speicher gespeichert werden, können Sie den Switch sehr effizient implementieren (dh mit Ausführung in der Reihenfolge von 2 Vergleichen, einer Addition und zwei Sprüngen).
quelle
Mit C # 8 können Sie dieses Problem elegant und kompakt mithilfe eines Schalterausdrucks lösen:
Als Ergebnis erhalten Sie:
Weitere Informationen zur neuen Funktion finden Sie hier .
quelle