Warum hat C # keinen lokalen Gültigkeitsbereich für Fallblöcke?

38

Ich habe diesen Code geschrieben:

private static Expression<Func<Binding, bool>> ToExpression(BindingCriterion criterion)
{
    switch (criterion.ChangeAction)
    {
        case BindingType.Inherited:
            var action = (byte)ChangeAction.Inherit;
            return (x => x.Action == action);
        case BindingType.ExplicitValue:
            var action = (byte)ChangeAction.SetValue;
            return (x => x.Action == action);
        default:
            // TODO: Localize errors
            throw new InvalidOperationException("Invalid criterion.");
    }
}

Und war überrascht, einen Kompilierungsfehler zu finden:

In diesem Bereich ist bereits eine lokale Variable mit dem Namen 'action' definiert

Es war ein ziemlich leicht zu lösendes Problem. Nur den zweiten loszuwerden, war varder Trick.

In caseBlöcken deklarierte Variablen haben offensichtlich den Gültigkeitsbereich des übergeordneten Elements switch, aber ich bin gespannt, warum dies so ist. Da C # nicht Ausführung ermöglichen , durch andere Fälle zu fallen (es erfordert break , return, throw, oder goto caseAussagen am Ende eines jeden caseBlocks), so scheint es ziemlich merkwürdig , dass es Variablendeklarationen erlauben würde , innerhalb eines casezu verwendenden oder Konflikt mit Variablen in jedem anderen case. Mit anderen Worten, Variablen scheinen durch caseAnweisungen zu fallen , obwohl dies bei der Ausführung nicht möglich ist. C # ist sehr bemüht, die Lesbarkeit zu fördern, indem es einige Konstrukte anderer Sprachen verbietet, die verwirrend sind oder leicht missbraucht werden. Aber das scheint nur Verwirrung zu stiften. Stellen Sie sich die folgenden Szenarien vor:

  1. Wenn es so geändert werden soll:

    case BindingType.Inherited:
        var action = (byte)ChangeAction.Inherit;
        return (x => x.Action == action);
    case BindingType.ExplicitValue:
        return (x => x.Action == action);
    

    Ich erhalte die Meldung " Verwendung der nicht zugewiesenen lokalen Variablen 'action' ". Dies ist verwirrend, weil in jedem anderen Konstrukt in C #, das mir var action = ...einfällt, die Variable initialisiert wird, aber hier wird sie einfach deklariert.

  2. Wenn ich die Fälle so vertauschen würde:

    case BindingType.ExplicitValue:
        action = (byte)ChangeAction.SetValue;
        return (x => x.Action == action);
    case BindingType.Inherited:
        var action = (byte)ChangeAction.Inherit;
        return (x => x.Action == action);
    

    Ich erhalte die Meldung "Die lokale Variable 'action' kann nicht verwendet werden, bevor sie deklariert wurde ". Die Reihenfolge der Case-Blöcke scheint hier also in einer Weise wichtig zu sein, die nicht ganz offensichtlich ist. Normalerweise könnte ich diese in einer beliebigen Reihenfolge schreiben, aber da die varim ersten Block erscheinen müssen, in dem sie actionverwendet werden, muss ich die caseBlöcke anpassen entsprechend.

  3. Wenn es so geändert werden soll:

    case BindingType.Inherited:
        var action = (byte)ChangeAction.Inherit;
        return (x => x.Action == action);
    case BindingType.ExplicitValue:
        action = (byte)ChangeAction.SetValue;
        goto case BindingType.Inherited;
    

    Dann erhalte ich keine Fehlermeldung, aber in gewissem Sinne sieht es so aus, als würde der Variablen ein Wert zugewiesen, bevor sie deklariert wird.
    (Obwohl mir keine Zeit einfällt, zu der Sie das tatsächlich tun möchten - ich wusste nicht einmal, dass goto casees das schon heute gab)

Meine Frage ist also, warum haben die Designer von C # den caseBlöcken nicht ihren eigenen lokalen Geltungsbereich gegeben? Gibt es dafür historische oder technische Gründe?

pswg
quelle
4
Deklarieren Sie Ihre actionVariable vor der switchAnweisung, oder setzen Sie jeden Fall in eine eigene Klammer, und Sie erhalten ein vernünftiges Verhalten.
Robert Harvey
3
@RobertHarvey Ja, und ich könnte noch weiter gehen und dies schreiben, ohne überhaupt eine switchzu verwenden. Ich bin nur neugierig auf die Gründe für dieses Design.
Pswg
wenn c # nach jedem fall eine pause braucht dann muss es wohl aus historischen gründen sein wie in c / java das nicht der fall ist!
tgkprog
@tgkprog Ich glaube, es ist nicht aus historischen Gründen und dass die Designer von C # dies mit Absicht getan haben, um sicherzustellen, dass der häufige Fehler, a zu vergessen break, in C # nicht möglich ist.
SVICK
12
Siehe Eric Lipperts Antwort auf diese verwandte SO-Frage. stackoverflow.com/a/1076642/414076 Sie können zufrieden sein oder auch nicht. Es heißt "weil sie es 1999 so gewählt haben."
Anthony Pegram

Antworten:

24

Ich denke, ein guter Grund ist, dass in jedem anderen Fall der Gültigkeitsbereich einer „normalen“ lokalen Variablen ein durch geschweifte Klammern ( {}) begrenzter Block ist . Die lokalen Variablen, die nicht normal sind, werden in einem speziellen Konstrukt vor einer Anweisung (die normalerweise ein Block ist) angezeigt, wie z. B. eine forSchleifenvariable oder eine in deklarierte Variable using.

Eine weitere Ausnahme bilden lokale Variablen in LINQ-Abfrageausdrücken, die sich jedoch völlig von normalen Deklarationen lokaler Variablen unterscheiden, sodass hier keine Verwechslungsgefahr besteht.

Zu Referenzzwecken finden Sie die Regeln in §3.7 Geltungsbereich der C # -Spezifikation:

  • Der Gültigkeitsbereich einer in einer lokalen Variablendeklaration deklarierten lokalen Variablen ist der Block, in dem die Deklaration erfolgt.

  • Der Gültigkeitsbereich einer in einem switch-Block einer switchAnweisung deklarierten lokalen Variablen ist der switch-Block .

  • Der Gültigkeitsbereich einer in einem For-Initializer einer forAnweisung deklarierten lokalen Variablen ist der For-Initializer , die For-Bedingung , der For-Iterator und die enthaltene Anweisung der forAnweisung.

  • Der Gültigkeitsbereich einer Variablen, die als Teil einer foreach-Anweisung , using-Anweisung , lock-Anweisung oder eines Abfrageausdrucks deklariert wurde , wird durch die Erweiterung des angegebenen Konstrukts bestimmt.

(Obwohl ich nicht ganz sicher bin, warum der switchBlock explizit erwähnt wird, da er im Gegensatz zu allen anderen erwähnten Konstrukten keine spezielle Syntax für lokale Variablendeklarationen hat.)

svick
quelle
1
+1 Können Sie einen Link zu dem von Ihnen angegebenen Umfang angeben?
Pswg
@pswg Sie finden einen Link zum Herunterladen der Spezifikation auf MSDN . (Klicken Sie auf den Link "Microsoft Developer Network (MSDN)" auf dieser Seite.)
svick
Also habe ich die Java-Spezifikationen nachgeschlagen und mich switchesin dieser Hinsicht anscheinend identisch verhalten (abgesehen davon, dass am Ende von case's Sprünge erforderlich sind ). Es scheint, dass dieses Verhalten einfach von dort kopiert wurde. Ich denke, die kurze Antwort lautet: caseAnweisungen erzeugen keine Blöcke, sie definieren lediglich Teilungen eines switchBlocks und haben daher keinen eigenen Gültigkeitsbereich.
Pswg
3
Betreff: Ich bin nicht ganz sicher, warum der Schalterblock explizit erwähnt wird - der Autor der Spezifikation ist nur wählerisch und weist implizit darauf hin, dass ein Schalterblock eine andere Grammatik als ein regulärer Block hat.
Eric Lippert
42

Aber es tut. Sie können überall lokale Bereiche erstellen, indem Sie Zeilen mit umbrechen{}

switch (criterion.ChangeAction)
{
  case BindingType.Inherited:
    {
      var action = (byte)ChangeAction.Inherit;
      return (x => x.Action == action);
    }
  case BindingType.ExplicitValue:
    {
      var action = (byte)ChangeAction.SetValue;
      return (x => x.Action == action);
    }
  default:
    // TODO: Localize errors
    throw new InvalidOperationException("Invalid criterion.");
}
Mike Koder
quelle
Yup hat dies verwendet und es funktioniert wie beschrieben
Mvision
1
+1 Dies ist ein guter Trick, aber ich akzeptiere die Antwort von svick , weil sie meiner ursprünglichen Frage am nächsten kommt.
Pswg
10

Ich zitiere Eric Lippert, dessen Antwort zu diesem Thema ziemlich klar ist:

Eine vernünftige Frage lautet: "Warum ist das nicht legal?" Eine vernünftige Antwort lautet "nun, warum sollte es sein"? Sie können es auf zwei Arten haben. Entweder ist das legal:

switch(y) 
{ 
    case 1:  int x = 123; ...  break; 
    case 2:  int x = 456; ...  break; 
}

oder das ist legal:

switch(y) 
{
    case 1:  int x = 123; ... break; 
    case 2:  x = 456; ... break; 
}

aber man kann es nicht in beide Richtungen haben. Die Designer von C # entschieden sich für den zweiten Weg, da dies der natürlichere Weg zu sein schien.

Diese Entscheidung wurde am 7. Juli 1999 vor knapp zehn Jahren getroffen. Die Kommentare in den Notizen von diesem Tag sind äußerst kurz. Sie lauten lediglich "Ein Schalter erstellt keinen eigenen Deklarationsbereich" und geben anschließend einen Beispielcode an, der zeigt, was funktioniert und was nicht.

Um mehr darüber zu erfahren, was an diesem Tag in den Köpfen der Designer war, müsste ich viele Leute über das beunruhigen, was sie vor zehn Jahren dachten - und über das, was letztendlich ein triviales Problem ist. Ich werde das nicht tun.

Kurz gesagt, es gibt keinen besonders zwingenden Grund, sich für die eine oder andere Art zu entscheiden. beide haben Vorzüge. Das Sprachdesign-Team entschied sich für einen Weg, weil es einen auswählen musste. die, die sie ausgewählt haben, erscheint mir vernünftig.

Wenn Sie mit dem C # -Entwicklerteam von 1999 nicht so vertraut sind wie Eric Lippert, werden Sie den genauen Grund nie erfahren!

Cyril Gandon
quelle
Kein Downvoter, aber das war ein Kommentar, der gestern zu der Frage gemacht wurde .
Jesse C. Slicer
4
Ich sehe das, aber da der Kommentator keine Antwort veröffentlicht, hielt ich es für eine gute Idee, die Antwort explizit durch Zitieren des Inhalts des Links zu erstellen. Und die Abstimmungen sind mir egal! Das OP fragt nach dem historischen Grund, nicht nach einer Antwort auf die Frage, wie es gemacht wird.
Cyril Gandon
5

Die Erklärung ist einfach - es liegt daran, dass es in C so ist. Sprachen wie C ++, Java und C # haben die Syntax und den Gültigkeitsbereich der switch-Anweisung aus Gründen der Vertrautheit kopiert.

(Wie in einer anderen Antwort angegeben, haben die Entwickler von C # keine Dokumentation darüber, wie diese Entscheidung in Bezug auf die Anwendungsbereiche getroffen wurde. Das nicht angegebene Prinzip für die C # -Syntax war jedoch, dass sie Java kopierten, es sei denn, sie hatten zwingende Gründe, dies anders zu tun. )

In C ähneln die case-Anweisungen goto-labels. Die switch-Anweisung ist wirklich eine schönere Syntax für ein berechnetes goto . Die Fälle definieren die Eintrittspunkte in den Schaltblock. Standardmäßig wird der Rest des Codes ausgeführt, sofern es keinen expliziten Exit gibt. Daher ist es nur sinnvoll, dass sie denselben Bereich verwenden.

(. Grundsätzlicher Gotos sind nicht strukturiert - sie definieren nicht oder abgrenzen Abschnitte des Codes, sie definiert nur jump-Punkte Daher ist eine goto - Label. Nicht kann einen Rahmen vorstellen.)

C # behält die Syntax bei, bietet jedoch eine Absicherung gegen "Durchfallen", indem nach jeder (nicht leeren) case-Klausel ein Exit erforderlich ist. Aber das ändert unsere Einstellung zum Schalter! Die Fälle sind jetzt alternative Zweige , wie die Zweige in einem If-else. Dies bedeutet, dass wir erwarten, dass jeder Zweig seinen eigenen Bereich definiert, genau wie If-Klauseln oder Iterationsklauseln.

Kurz gesagt: Fälle haben den gleichen Gültigkeitsbereich, da dies in C der Fall ist. In C # erscheint dies jedoch seltsam und inkonsistent, da wir Fälle als alternative Verzweigungen und nicht als Ziele betrachten.

JacquesB
quelle
1
" Es scheint seltsam und inkonsistent in C # zu sein, weil wir uns Fälle als alternative Zweige vorstellen, anstatt zu Zielen zu gelangen. " Das ist genau die Verwirrung, die ich hatte. +1 für die Erklärung der ästhetischen Gründe, warum es sich in C # falsch anfühlt .
pswg
4

Eine vereinfachte Sichtweise auf den Geltungsbereich besteht darin, den Geltungsbereich blockweise zu betrachten {}.

Da switches keine Blöcke enthält, kann es keine unterschiedlichen Bereiche haben.

Guvante
quelle
3
Was ist for (int i = 0; i < n; i++) Write(i); /* legal */ Write(i); /* illegal */? Es gibt keine Blöcke, aber es gibt verschiedene Bereiche.
Svick
@svick: Wie gesagt, vereinfacht gibt es einen Block, der durch die forAnweisung erzeugt wird. switcherstellt keine Blöcke für jeden Fall, nur auf der obersten Ebene. Die Ähnlichkeit besteht darin, dass jede Anweisung einen Block erstellt (gilt switchals Anweisung).
Guvante
1
Warum konnten Sie dann nicht casestattdessen zählen ?
Svick
1
@svick: Weil casees keinen Block enthält, es sei denn, Sie möchten optional einen hinzufügen. forDauert für die nächste Anweisung, es sei denn, Sie fügen {}sie hinzu, sodass sie immer einen Block enthält. caseDauert jedoch so lange, bis Sie den eigentlichen Block, die switchAnweisung, verlassen.
Guvante