Pourquoi C # n'a-t-il pas de portée locale dans les blocs de cas?

38

J'écrivais ce code:

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

Et a été surpris de trouver une erreur de compilation:

Une variable locale nommée 'action' est déjà définie dans cette portée

C'était un problème assez facile à résoudre; juste se débarrasser de la seconde a varfait l'affaire.

Il est évident que les variables déclarées dans des caseblocs ont la portée du parent switch, mais je suis curieux de savoir pourquoi. Étant donné que C # ne permet pas l' exécution de tomber dans d' autres cas (il exige break , return, throwou des goto casedéclarations à la fin de chaque casebloc), il semble tout à fait étrange que cela permettrait des déclarations variables à l' intérieur d' un caseà utiliser ou de conflit avec des variables dans tout autre case. En d'autres termes, les variables semblent tomber à travers les caseinstructions, même si l'exécution ne peut pas. C # prend beaucoup de peine à améliorer la lisibilité en interdisant certaines constructions de langages confus ou faciles à abuser. Mais cela semble lié à la confusion. Considérez les scénarios suivants:

  1. Si devions le changer en ceci:

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

    Je reçois " Utilisation de la variable locale non affectée 'action' ". Ceci est déroutant parce que dans chaque construction en C # à laquelle je peux penser var action = ...initialiserait la variable, mais ici, il la déclare simplement.

  2. Si je devais échanger les cas comme ceci:

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

    J'obtiens " Impossible d'utiliser la variable locale 'action' avant qu'elle soit déclarée ". Donc, l'ordre des blocs de cas semble être important ici d'une manière qui n'est pas tout à fait évident - normalement je pourrais les écrire dans l'ordre que je souhaite, mais parce que le vardoit apparaître dans le premier bloc où actionest utilisé, je dois modifier des caseblocs en conséquence.

  3. Si devions le changer en ceci:

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

    Ensuite, je ne reçois aucune erreur, mais dans un sens, il semble que la variable se voit attribuer une valeur avant d'être déclarée.
    (Bien que je ne puisse penser à aucun moment où vous voudriez réellement faire cela - je ne savais même pas qu'il goto caseexistait avant aujourd'hui)

Ma question est donc la suivante: pourquoi les concepteurs de C # n’ont-ils pas donné aux caseblocs leur propre portée locale? Y a-t-il des raisons historiques ou techniques à cela?

pswg
la source
4
Déclarez votre actionvariable avant l' switchinstruction ou mettez chaque cas entre ses accolades pour obtenir un comportement judicieux.
Robert Harvey
3
@RobertHarvey oui, et je pourrais aller encore plus loin et écrire ceci sans utiliser switchdu tout - je suis simplement curieux de connaître le raisonnement derrière cette conception.
pswg
si c # a besoin d'une pause après chaque cas, alors je suppose que ce doit être pour des raisons historiques, comme dans c / java, ce n'est pas le cas!
tgkprog
@tgkprog Je crois que ce n'est pas pour des raisons historiques et que les concepteurs de C # l'ont fait exprès pour s'assurer que l'erreur commune d'oublier un breakn'est pas possible en C #.
svick
12
Voir la réponse d'Eric Lippert à cette question SO connexe. stackoverflow.com/a/1076642/414076 Vous pouvez ou non venir avec satisfaction. Cela se lit comme "parce que c'est comme ça qu'ils ont choisi de le faire en 1999."
Anthony Pegram

Réponses:

24

Je pense qu'une bonne raison est que, dans tous les autres cas, la portée d'une variable locale «normale» est un bloc délimité par des accolades ( {}). Les variables locales qui ne sont pas normales apparaissent dans une construction spéciale avant une instruction (qui est généralement un bloc), comme une forvariable de boucle ou une variable déclarée dans using.

Une autre exception concerne les variables locales dans les expressions de requête LINQ, mais celles-ci sont complètement différentes des déclarations de variables locales normales. Je ne pense donc pas qu’il y ait un risque de confusion.

Pour référence, les règles se trouvent au §3.7 Portées de la spécification C #:

  • La portée d'une variable locale déclarée dans une déclaration de variable locale est le bloc dans lequel la déclaration se produit.

  • L'étendue d'une variable locale déclarée dans un commutateur-bloc d'une switchinstruction est le commutateur-bloc .

  • La portée d'une variable locale déclarée dans un initialiseur for d'une forinstruction est l' initialiseur , la condition , le for et l' instruction contenue de l' forinstruction.

  • La portée d'une variable déclarée comme partie d'un foreach-instruction , à l' aide-instruction , lock-déclaration ou expression de requête est déterminée par l'expansion de la construction donnée.

(Bien que je ne sois pas tout à fait sûr de savoir pourquoi le switchbloc est explicitement mentionné, puisqu’il n’a pas de syntaxe particulière pour les déclinaisons de variables locales, contrairement à toutes les autres constructions mentionnées.)

svick
la source
1
+1 Pouvez-vous fournir un lien vers la portée que vous citez?
pswg
@ pswg Vous pouvez trouver un lien pour télécharger la spécification sur MSDN . (Cliquez sur le lien «Microsoft Developer Network (MSDN)» sur cette page.)
svick
J'ai donc regardé les spécifications Java et je switchessemble avoir un comportement identique à cet égard (sauf pour exiger des sauts à la fin de case). Il semble que ce comportement ait été simplement copié à partir de là. Donc, je suppose que la réponse courte est: les caseinstructions ne créent pas de blocs, elles définissent simplement les divisions d'un switchbloc et n'ont donc pas de portée par elles-mêmes.
pswg
3
Re: Je ne suis pas tout à fait sûr de savoir pourquoi le bloc de commutateur est explicitement mentionné - l'auteur de la spécification est juste pointilleux, soulignant implicitement qu'un bloc de commutateur a une grammaire différente de celle d'un bloc normal.
Eric Lippert
42

Mais c'est le cas. Vous pouvez créer des étendues locales n’importe où en enveloppant des lignes avec{}

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
la source
Yup a utilisé cela et cela fonctionne comme décrit
Mvision
1
+1 C’est un bon truc, mais je vais accepter la réponse de svick qui me rapproche le plus de ma question initiale.
pswg
10

Je citerai Eric Lippert, dont la réponse est assez claire sur le sujet:

Une question raisonnable est "pourquoi n'est-ce pas légal?" Une réponse raisonnable est "bon, pourquoi cela devrait-il être"? Vous pouvez l'avoir de deux manières. Soit c'est légal:

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

ou c'est légal:

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

mais vous ne pouvez pas avoir les deux. Les concepteurs de C # ont choisi la deuxième façon, qui semblait être la manière la plus naturelle de le faire.

Cette décision a été prise le 7 juillet 1999, il y a dix ans à peine. Les commentaires dans les notes de ce jour sont extrêmement brefs. Ils indiquent simplement "Un cas de commutation ne crée pas son propre espace de déclaration", puis donne un exemple de code qui montre ce qui fonctionne et ce qui ne fonctionne pas.

Pour en savoir plus sur les préoccupations des concepteurs ce jour-là, il faudrait que je bosse beaucoup de gens sur ce qu'ils pensaient il y a dix ans - et sur ce qui est finalement une question triviale; Je ne vais pas faire ça.

En bref, il n’ya pas de raison particulièrement convaincante de choisir l’un ou l’autre moyen; les deux ont des mérites. L'équipe de conception linguistique a choisi une méthode parce qu'elle devait en choisir une; celui qu'ils ont choisi me semble raisonnable.

Donc, à moins que vous ne connaissiez mieux l'équipe de développeurs C # de 1999 qu'Eric Lippert, vous ne saurez jamais la raison exacte!

Cyril Gandon
la source
Pas un vote négatif, mais c'était un commentaire sur la question d' hier .
Jesse C. Slicer
4
Je le vois bien, mais comme le commentateur n’a pas publié de réponse, j’ai pensé que ce pourrait être une bonne idée de créer explicitement la réponse en citant le contenu du lien. Et je me fous des votes négatifs! Le PO demande la raison historique, pas une réponse "Comment le faire".
Cyril Gandon
5

L'explication est simple - c'est parce que c'est comme ça en C. Des langages tels que C ++, Java et C # ont copié la syntaxe et la portée de l'instruction switch pour des raisons de familiarité.

(Comme indiqué dans une autre réponse, les développeurs de C # ne disposent pas de documentation sur la manière dont cette décision a été prise concernant les études de cas. Mais le principe non déclaré de la syntaxe C # était que, à moins d’avoir des raisons impérieuses de le faire différemment, ils copiaient Java. )

En C, les déclarations de cas sont similaires à celles de goto-labels. L'instruction switch est vraiment une syntaxe plus agréable pour un goto calculé . Les cas définissent les points d'entrée dans le bloc de commutation. Par défaut, le reste du code sera exécuté, sauf en cas de sortie explicite. Il est donc logique qu'ils utilisent la même portée.

(Plus fondamentalement. Les Goto ne sont pas structurés - ils ne définissent ni ne délimitent des sections de code, ils ne définissent que des points de saut. Par conséquent, une étiquette de goto ne peut pas introduire une portée.)

C # conserve la syntaxe, mais introduit une protection contre le "blocage" en exigeant une sortie après chaque clause de casse (non vide). Mais cela change notre façon de penser du commutateur! Les cas sont maintenant des branches alternatives , comme les branches d'un if-else. Cela signifie que nous nous attendons à ce que chaque branche définisse sa propre portée, comme des clauses if-ou itération.

En bref, les cas ont la même portée, car c’est ce qui se passe en C. Mais cela semble étrange et incohérent en C #, car nous considérons les cas comme des branches alternatives plutôt que comme des cibles.

JacquesB
la source
1
" Cela semble étrange et incohérent en C # parce que nous considérons les cas comme des branches alternatives plutôt que comme des cibles fixes " C'est précisément la confusion que j'ai eue. +1 pour expliquer les raisons esthétiques qui expliquent pourquoi on se sent mal en C #.
pswg
4

Une manière simplifiée de voir la portée consiste à examiner la portée par blocs {}.

Comme switchil ne contient aucun bloc, il ne peut pas avoir différentes portées.

Guvante
la source
3
Qu'en est- il for (int i = 0; i < n; i++) Write(i); /* legal */ Write(i); /* illegal */? Il n'y a pas de blocs, mais il y a différentes portées.
svick
@svick: Comme je l'ai dit, simplifié, il y a un bloc créé par la fordéclaration. switchne crée pas de blocs pour chaque cas, juste au plus haut niveau. La similitude est que chaque instruction crée un bloc (qui compte switchcomme une instruction).
Guvante
1
Alors pourquoi ne pas compter à la caseplace?
svick
1
@svick: Parce caseque ne contient pas de bloc, à moins que vous ne décidiez d'en ajouter éventuellement. fordure pour la déclaration suivante à moins que vous n'ayez ajouté {}un bloc, mais casedure jusqu'à ce que quelque chose vous oblige à quitter le bloc réel, la switchdéclaration.
Guvante