Perché C # non ha un ambito locale in maiuscole?


38

Stavo scrivendo questo codice:

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.");
    }
}

Ed è stato sorpreso di trovare un errore di compilazione:

Una variabile locale denominata "azione" è già definita in questo ambito

È stato un problema abbastanza semplice da risolvere; solo sbarazzarsi del secondo ha varfatto il trucco.

Evidentemente le variabili dichiarate in caseblocchi hanno lo scopo del genitore switch, ma sono curioso di sapere perché. Dato che C # non consente l'esecuzione cadere attraverso altri casi (che richiede break , return, throwo goto caseistruzioni alla fine di ogni caseblocco), sembra abbastanza strano che consentirebbe dichiarazioni di variabili all'interno di uno caseda utilizzare o conflitto con le variabili in qualsiasi altra case. In altre parole, le variabili sembrano cadere nelle caseistruzioni anche se l'esecuzione non può. C # fa di tutto per promuovere la leggibilità vietando alcuni costrutti di altre lingue che sono confusi o o facilmente abusabili. Ma sembra che sia solo destinato a creare confusione. Considera i seguenti scenari:

  1. Se dovessi cambiarlo in questo:

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

    Ottengo " Utilizzo della variabile locale non assegnata 'action' ". Questo è confuso perché in ogni altro costrutto in C # che mi viene in mente var action = ...si inizializzerebbe la variabile, ma qui la dichiara semplicemente.

  2. Se dovessi scambiare i casi in questo modo:

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

    Ottengo " Impossibile utilizzare la variabile locale 'action' prima che venga dichiarata ". Quindi l'ordine dei case case sembra essere importante qui in un modo che non è del tutto ovvio - normalmente potrei scriverli nell'ordine che desidero, ma poiché il varmust deve apparire nel primo blocco dove actionviene usato, devo modificare i caseblocchi di conseguenza.

  3. Se dovessi cambiarlo in questo:

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

    Quindi non ricevo alcun errore, ma in un certo senso sembra che alla variabile venga assegnato un valore prima che venga dichiarato.
    (Anche se non riesco a pensare a quando vorresti davvero farlo, non sapevo nemmeno che goto caseesistesse prima di oggi)

Quindi la mia domanda è: perché i progettisti di C # non hanno dato ai caseblocchi il loro ambito locale? Ci sono ragioni storiche o tecniche per questo?


4
Dichiara la tua actionvariabile prima switchdell'istruzione o metti ogni caso tra parentesi graffe e otterrai un comportamento ragionevole.
Robert Harvey,

3
@RobertHarvey sì, e potrei andare ancora oltre e scrivere questo senza usare switchaffatto - Sono solo curioso di sapere il ragionamento alla base di questo disegno.
pswg,

se c # ha bisogno di una pausa dopo ogni caso, suppongo che debba essere per ragioni storiche come in c / java che non è il caso!
tgkprog,

@tgkprog Credo che non sia per ragioni storiche e che i progettisti di C # lo abbiano fatto apposta per assicurarsi che l'errore comune di dimenticare a breaknon sia possibile in C #.
svick

12
Vedi la risposta di Eric Lippert su questa relativa domanda SO. stackoverflow.com/a/1076642/414076 Potresti essere soddisfatto o meno. Si legge come "perché è così che hanno scelto di farlo nel 1999."
Anthony Pegram,

Risposte:


24

Penso che una buona ragione sia che in tutti gli altri casi, l'ambito di una variabile locale "normale" è un blocco delimitato da parentesi graffe ( {}). Le variabili locali che non sono normali appaiono in un costrutto speciale prima di un'istruzione (che di solito è un blocco), come una forvariabile di ciclo o una variabile dichiarata in using.

Un'altra eccezione sono le variabili locali nelle espressioni di query LINQ, ma sono completamente diverse dalle normali dichiarazioni delle variabili locali, quindi non credo che ci sia possibilità di confusione lì.

Per riferimento, le regole sono in §3.7 Ambiti della specifica C #:

  • L'ambito di una variabile locale dichiarata in una dichiarazione di variabile locale è il blocco in cui si verifica la dichiarazione.

  • L'ambito di una variabile locale dichiarata in un blocco switch di switchun'istruzione è il blocco switch .

  • L'ambito di una variabile locale dichiarata in un per-inizializzatore di una fordichiarazione è il per-inizializzatore , la per la condizione , il per-iteratore , e il contenuto dichiarazione della fordichiarazione.

  • L'ambito di una variabile dichiarata come parte di un'istruzione foreach , using-statement , lock-statement o query-expression è determinata dall'espansione del dato costrutto.

(Anche se non sono del tutto sicuro del motivo per cui il switchblocco è esplicitamente menzionato, dal momento che non ha alcuna sintassi speciale per le declinazioni delle variabili locali, a differenza di tutti gli altri costrutti menzionati.)


1
+1 Puoi fornire un link all'ambito che stai citando?
pswg

@pswg È possibile trovare un collegamento per scaricare la specifica su MSDN . (Fare clic sul collegamento "Microsoft Developer Network (MSDN)" in quella pagina.)
svick

Quindi ho cercato le specifiche Java e mi switchessembra che si comportino in modo identico a questo proposito (tranne che per richiedere salti alla fine di case). Sembra che questo comportamento sia stato semplicemente copiato da lì. Quindi suppongo che la risposta breve sia - le caseistruzioni non creano blocchi, semplicemente definiscono le divisioni di un switchblocco - e quindi non hanno ambiti da soli.
pswg

3
Ri: Non sono del tutto sicuro del motivo per cui il blocco switch sia esplicitamente menzionato - l'autore della specifica è solo schizzinoso, sottolineando implicitamente che un blocco switch ha una grammatica diversa rispetto a un blocco normale.
Eric Lippert,

42

Ma lo fa. È possibile creare ambiti locali ovunque avvolgendo le linee con{}

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.");
}

Yup l'ha usato e funziona come descritto
Mvision

1
+1 Questo è un buon trucco, ma accetterò la risposta di svick per avvicinarsi di più alla mia domanda originale.
pswg,

10

Citerò Eric Lippert, la cui risposta è abbastanza chiara sull'argomento:

Una domanda ragionevole è "perché questo non è legale?" Una risposta ragionevole è "bene, perché dovrebbe essere"? Puoi averlo in due modi. O questo è legale:

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

o questo è legale:

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

ma non puoi averlo in entrambi i modi. I progettisti di C # hanno scelto il secondo modo in cui sembra essere il modo più naturale per farlo.

Questa decisione fu presa il 7 luglio 1999, poco meno di dieci anni fa. I commenti nelle note di quel giorno sono estremamente brevi, affermando semplicemente "Una custodia non crea il proprio spazio di dichiarazione" e quindi fornisce un codice di esempio che mostra cosa funziona e cosa no.

Per saperne di più su ciò che era nella mente dei progettisti in quel particolare giorno, avrei dovuto infastidire molte persone su ciò che stavano pensando dieci anni fa - e infastidirli su ciò che alla fine è un problema banale; Non lo farò.

In breve, non esiste un motivo particolarmente convincente per scegliere in un modo o nell'altro; entrambi hanno dei meriti. Il team di progettazione del linguaggio ha scelto un modo perché doveva sceglierne uno; quello che hanno scelto mi sembra ragionevole.

Quindi, a meno che tu non sia più intime con il team di sviluppatori C # del 1999 di Eric Lippert, non saprai mai il motivo esatto!


Non un downvoter, ma questo è stato un commento fatto ieri alla domanda .
Jesse C. Slicer,

4
Lo vedo, ma dal momento che il commentatore non pubblica una risposta, ho pensato che potesse essere una buona idea creare esplicitamente la risposta citando il contenuto del link. E non mi interessa il downvotes! L'OP chiede la ragione storica, non una risposta "Come farlo".
Cyril Gandon,

5

La spiegazione è semplice - è perché è così in C. Lingue come C ++, Java e C # hanno copiato la sintassi e l'ambito dell'istruzione switch per motivi di familiarità.

(Come affermato in un'altra risposta, gli sviluppatori di C # non hanno documentazione su come sia stata presa questa decisione riguardo agli ambiti del caso. Ma il principio non dichiarato per la sintassi di C # era che, a meno che non avessero avuto motivi convincenti per farlo diversamente, hanno copiato Java. )

In C, le dichiarazioni dei casi sono simili alle etichette goto. L'istruzione switch è davvero una sintassi migliore per un goto calcolato . I casi definiscono i punti di ingresso nel blocco interruttori. Per impostazione predefinita, il resto del codice verrà eseguito, a meno che non esista un'uscita esplicita. Quindi ha senso usare solo lo stesso scopo.

(Più fondamentalmente. I Goto non sono strutturati - non definiscono o delimitano sezioni di codice, definiscono solo punti di salto. Pertanto un'etichetta goto non può introdurre un ambito.)

C # mantiene la sintassi, ma introduce una protezione contro il "fall through" richiedendo un'uscita dopo ogni clausola (non vuota). Ma questo cambia il modo in cui pensiamo al passaggio! I casi ora sono rami alternati , come i rami in un if-else. Ciò significa che ci aspetteremmo che ogni ramo definisca il proprio ambito proprio come le clausole if o le clausole di iterazione.

Quindi in breve: i casi condividono lo stesso scopo perché è così in C. Ma sembra strano e incoerente in C # perché pensiamo ai casi come rami alternati piuttosto che goto target.


1
" sembra strano e incoerente in C # perché pensiamo ai casi come rami alternativi piuttosto che andare a bersagli " Questa è precisamente la confusione che ho avuto. +1 per spiegare le ragioni estetiche alla base del motivo per cui ci si sente male in C #.
pswg,

4

Un modo semplificato di esaminare l'ambito è quello di considerare l'ambito per blocchi {}.

Poiché switchnon contiene alcun blocco, non può avere ambiti diversi.


3
Che dire for (int i = 0; i < n; i++) Write(i); /* legal */ Write(i); /* illegal */? Non ci sono blocchi, ma ci sono ambiti diversi.
svick,

@svick: come ho detto, semplificato, c'è un blocco, creato dall'istruzione for. switchnon crea blocchi per ogni caso, solo al livello più alto. La somiglianza è che ogni istruzione crea un blocco (contando switchcome l'istruzione).
Guvante,

1
Allora perché non puoi contare caseinvece?
svick

1
@svick: perché casenon contiene un blocco, a meno che tu non decida di aggiungerne uno facoltativamente. fordura per l'istruzione successiva a meno che non venga aggiunta, {}quindi ha sempre un blocco, ma casedura fino a quando qualcosa non ti fa abbandonare il blocco reale, l' switchistruzione.
Guvante,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.