Perché C # non ha scope locale in caso di blocchi?

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

E sono rimasto sorpreso nel trovare un errore di compilazione:

A local variable named 'action' is already defined in this scope

È stato un problema abbastanza facile da risolvere; solo eliminare il secondo var ha fatto il trucco.

Evidentemente le variabili dichiarate in case blocchi hanno lo scopo del genitore switch , ma sono curioso di sapere perché è così. Dato che C # non consente l'esecuzione di altri casi (è richiede break , return , throw , o goto case istruzioni alla fine di ogni blocco case ), sembra piuttosto strano che possa consentire l'uso di dichiarazioni variabili all'interno di un case o di conflitto con variabili in qualsiasi altro %codice%. In altre parole, le variabili sembrano cadere attraverso case dichiarazioni anche se l'esecuzione non può. C # si impegna a promuovere la leggibilità vietando alcuni costrutti di altre lingue che sono confusi o facilmente abusabili. Ma sembra che sia destinato a causare 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);
    

    Ricevo " Uso della variabile locale non assegnata 'azione' ". Questo è fonte di confusione perché in ogni altro costrutto in C # posso pensare a case inizializzare la variabile, ma qui semplicemente lo dichiara.

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

    I get " Impossibile usare la variabile locale 'action' prima che sia dichiarata ". Quindi l'ordine dei blocchi del caso sembra essere importante qui in un modo che non è del tutto ovvio - Normalmente potrei scriverli in qualsiasi ordine che desidero, ma perché var action = ... deve apparire nel primo blocco dove var è usato , Devo modificare action blocchi 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 ho alcun errore, ma in un certo senso, sembra come se alla variabile fosse assegnato un valore prima che fosse dichiarato.
    (Anche se non riesco a pensare a nessun tempo in cui vorresti davvero farlo - non sapevo nemmeno che esistesse case prima di oggi)

Quindi la mia domanda è: perché i progettisti di C # non hanno dato a goto case blocchi del loro ambito locale? Ci sono motivi storici o tecnici per questo?

    
posta p.s.w.g 16.04.2013 - 01:06
fonte

5 risposte

24

Penso che una buona ragione sia che in ogni altro caso, 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 variabile di ciclo for o una variabile dichiarata in using .

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

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

  • The scope of a local variable declared in a local-variable-declaration is the block in which the declaration occurs.

  • The scope of a local variable declared in a switch-block of a switch statement is the switch-block.

  • The scope of a local variable declared in a for-initializer of a for statement is the for-initializer, the for-condition, the for-iterator, and the contained statement of the for statement.

  • The scope of a variable declared as part of a foreach-statement, using-statement, lock-statement or query-expression is determined by the expansion of the given construct.

(Anche se non sono completamente sicuro del perché il blocco switch sia menzionato esplicitamente, poiché non ha alcuna sintassi speciale per le declinazioni di variabili locali, a differenza di tutti gli altri costrutti menzionati.)

    
risposta data 16.04.2013 - 01:59
fonte
41

Ma lo fa. Puoi 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.");
}
    
risposta data 16.04.2013 - 07:47
fonte
9

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

A reasonable question is "why is this not legal?" A reasonable answer is "well, why should it be"? You can have it one of two ways. Either this is legal:

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

or this is legal:

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

but you can't have it both ways. The designers of C# chose the second way as seeming to be the more natural way to do it.

This decision was made on July 7th, 1999, just shy of ten years ago. The comments in the notes from that day are extremely brief, simply stating "A switch-case does not create its own declaration space" and then giving some sample code that shows what works and what does not.

To find out more about what was in the designers minds on this particular day, I'd have to bug a lot of people about what they were thinking ten years ago -- and bug them about what is ultimately a trivial issue; I'm not going to do that.

In short, there is no particularly compelling reason to choose one way or the other; both have merits. The language design team chose one way because they had to pick one; the one they picked seems reasonable to me.

Quindi, se non sei più intimo con il team di sviluppatori C # del 1999 di Eric Lippert, non conoscerai mai il motivo esatto!

    
risposta data 17.04.2013 - 16:10
fonte
4

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

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

    
risposta data 16.04.2013 - 01:44
fonte
4

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 è stata presa questa decisione riguardo agli scope del caso, ma il principio non dichiarato per la sintassi C # era che, a meno che non avessero motivi convincenti per farlo in modo diverso, copiato Java.)

In C, le istruzioni case sono simili alle goto-label. L'istruzione switch è davvero una sintassi più bella per un goto calcolato . I casi definiscono i punti di ingresso nel blocco switch. Di default il resto del codice verrà eseguito, a meno che non ci sia un'uscita esplicita. Quindi ha senso solo che usano lo stesso scope.

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

C # mantiene la sintassi, ma introduce una salvaguardia contro il "fall through" richiedendo un'uscita dopo ogni caso (non vuoto). Ma questo cambia il modo in cui pensiamo all'interruttore! I casi sono ora rami alternativi , come i rami in un if-else. Ciò significa che ci aspetteremmo che ogni ramo definisca il proprio ambito proprio come se fossero clausole if o clausole di iterazione.

Quindi, in breve: i casi condividono lo stesso ambito perché questo è il modo in cui è in C. Ma sembra strano e incoerente in C # perché pensiamo ai casi come a diramazioni alternative piuttosto che a target goto.

    
risposta data 06.10.2017 - 09:59
fonte

Leggi altre domande sui tag