Por que o C # não tem escopo local nos blocos de caso?

38

Eu estava escrevendo este código:

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 ficou surpreso ao encontrar um erro de compilação:

Uma variável local denominada 'ação' já está definida neste escopo

Foi uma questão bastante fácil de resolver; apenas se livrar do segundo varfez o truque.

Evidentemente, as variáveis ​​declaradas em caseblocos têm o escopo do pai switch, mas estou curioso para saber por que isso acontece. Dado que C # não permite a execução de cair através de outros casos (que requer break , return, throw, ou goto casedeclarações no final de cada casebloco), parece bastante estranho que permitiria declarações de variáveis dentro de um casepara ser usado ou conflito com as variáveis em qualquer outra case. Em outras palavras, as variáveis ​​parecem passar por caseinstruções, embora a execução não possa. O C # esforça-se ao máximo para promover a legibilidade, proibindo algumas construções de outros idiomas que sejam confusas ou que sejam facilmente abusadas. Mas isso parece apenas causar confusão. Considere os seguintes cenários:

  1. Se fosse alterá-lo para isso:

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

    Recebo " Uso da variável local não atribuída 'action' ". Isso é confuso, porque em qualquer outra construção em C # que eu possa pensar var action = ...inicializaria a variável, mas aqui ela simplesmente a declara.

  2. Se eu fosse trocar os casos assim:

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

    Recebo " Não é possível usar a variável local 'action' antes de ser declarada ". Portanto, a ordem dos blocos de casos parece ser importante aqui de uma maneira que não é totalmente óbvia - normalmente eu poderia escrevê-los em qualquer ordem que desejasse, mas como os varitens devem aparecer no primeiro bloco em que actioné usado, tenho que ajustar os caseblocos. adequadamente.

  3. Se fosse alterá-lo para isso:

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

    Não recebo nenhum erro, mas, em certo sentido, parece que a variável está recebendo um valor antes de ser declarada.
    (Embora eu não consiga pensar em nenhum momento em que você realmente queira fazer isso - eu nem sabia que goto caseexistia antes de hoje)

Então, minha pergunta é: por que os designers de C # não deram aos caseblocos seu próprio escopo local? Existem razões históricas ou técnicas para isso?

pswg
fonte
4
Declare sua actionvariável antes da switchdeclaração ou coloque cada caso em seu próprio aparelho, e você obterá um comportamento sensato.
Robert Harvey
3
@RobertHarvey sim, e eu poderia ir ainda mais longe e escrever isso sem usar um switch- eu só estou curioso sobre o raciocínio por trás desse design.
PSWG
se c # precisa de uma pausa após cada caso, então eu acho que tem que ser por razões históricas, como em c / java que não é o caso!
tgkprog
@tgkprog Eu acredito que não é por razões históricas e que os designers de C # fizeram isso de propósito para garantir que o erro comum de esquecer a breaknão seja possível em C #.
svick
12
Veja a resposta de Eric Lippert sobre esta questão SO relacionada. stackoverflow.com/a/1076642/414076 Você pode ou não ficar satisfeito. É lido como "porque foi assim que eles escolheram fazê-lo em 1999".
Anthony Pegram

Respostas:

24

Eu acho que uma boa razão é que, em todos os outros casos, o escopo de uma variável local "normal" é um bloco delimitado por chaves ( {}). As variáveis ​​locais que não são normais aparecem em uma construção especial antes de uma instrução (que geralmente é um bloco), como uma forvariável de loop ou uma variável declarada em using.

Mais uma exceção são as variáveis ​​locais nas expressões de consulta LINQ, mas essas são completamente diferentes das declarações de variáveis ​​locais normais, então não acho que exista uma chance de confusão lá.

Para referência, as regras estão em §3.7 Escopos da especificação C #:

  • O escopo de uma variável local declarada em uma declaração de variável local é o bloco no qual a declaração ocorre.

  • O escopo de uma variável local declarada no bloco de comutação de uma switchinstrução é o bloco de comutação .

  • O escopo de uma variável local declarada no inicializador de uma forinstrução é o inicializador , a condição de , o iterador e a instrução contida na forinstrução.

  • O âmbito de uma variável declarada como parte de um foreach-declaração , utilizando-declaração , trava-declaração ou consulta de expressão é determinada pela expansão da construo dada.

(Embora eu não esteja completamente certo de por que o switchbloco é mencionado explicitamente, pois ele não possui nenhuma sintaxe especial para declinações de variáveis ​​locais, diferentemente de todas as outras construções mencionadas).

svick
fonte
1
+1 Você pode fornecer um link para o escopo que está citando?
PSWG
@pswg Você pode encontrar um link para baixar a especificação no MSDN . (Clique no link "Microsoft Developer Network (MSDN)" nessa página.)
svick
Então, procurei as especificações do Java e switchespareço se comportar de maneira idêntica a esse respeito (exceto por exigir saltos no final de case). Parece que esse comportamento foi simplesmente copiado de lá. Então, acho que a resposta curta é - casedeclarações não criam blocos, elas simplesmente definem divisões de um switchbloco - e, portanto, não têm escopos por si mesmas.
precisa saber é
3
Re: Não sei bem por que o bloco de comutação é mencionado explicitamente - o autor da especificação está apenas sendo exigente, indicando implicitamente que um comutador de bloco tem uma gramática diferente de um bloco regular.
Eric Lippert
42

Mas sim. Você pode criar escopos locais em qualquer lugar, envolvendo linhas com{}

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
fonte
Yup usado isso e ele funciona como descrito
Mvision
1
+1 Este é um bom truque, mas vou aceitar a resposta de svick por ter se aproximado mais da minha pergunta original.
precisa
10

Vou citar Eric Lippert, cuja resposta é bastante clara sobre o assunto:

Uma pergunta razoável é "por que isso não é legal?" Uma resposta razoável é "bem, por que deveria ser"? Você pode ter uma de duas maneiras. Ou isso é legal:

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

ou isso é legal:

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

mas você não pode ter os dois lados. Os designers de C # escolheram a segunda maneira como parecendo ser a maneira mais natural de fazê-lo.

Esta decisão foi tomada em 7 de julho de 1999, a apenas dez anos atrás. Os comentários nas notas daquele dia são extremamente breves, simplesmente afirmando "Um caso de opção não cria seu próprio espaço de declaração" e fornecendo um código de exemplo que mostra o que funciona e o que não funciona.

Para descobrir mais sobre o que estava na mente dos designers nesse dia em particular, eu teria que incomodar muitas pessoas sobre o que eles estavam pensando há dez anos - e incomodá-las sobre o que é, em última análise, uma questão trivial; Eu não vou fazer isso.

Em suma, não há razão particularmente convincente para escolher uma maneira ou de outra; ambos têm méritos. A equipe de design de idiomas escolheu um caminho porque precisou escolher um; o que eles escolheram me parece razoável.

Portanto, a menos que você seja mais íntimo da equipe de desenvolvedores de C # de 1999 do que Eric Lippert, nunca saberá o motivo exato!

Cyril Gandon
fonte
Não é um downvoter, mas este foi um comentário feito sobre a questão ontem .
Jesse C. Slicer
4
Entendo isso, mas como o comentarista não postou uma resposta, achei que seria uma boa ideia criar explicitamente a resposta, citando o conteúdo do link. E eu não ligo para os votos negativos! O OP pergunta a razão histórica, não a resposta "Como fazer".
Cyril Gandon
5

A explicação é simples - é porque é assim em C. Linguagens como C ++, Java e C # copiaram a sintaxe e o escopo da instrução switch por uma questão de familiaridade.

(Conforme declarado em outra resposta, os desenvolvedores do C # não têm documentação sobre como essa decisão foi tomada. Mas o princípio não declarado da sintaxe do C # era que, a menos que eles tivessem razões convincentes para fazê-lo de maneira diferente, eles copiavam o Java. )

Em C, as instruções de caso são semelhantes aos rótulos de goto. A instrução switch é realmente uma sintaxe melhor para um goto computado . Os casos definem os pontos de entrada no bloco de chaves. Por padrão, o restante do código será executado, a menos que haja uma saída explícita. Portanto, faz sentido que eles usem o mesmo escopo.

(Mais fundamentalmente. Os Goto não são estruturados - eles não definem ou delimitam seções de código, apenas definem pontos de salto. Portanto, um rótulo Goto não pode introduzir um escopo.)

O C # mantém a sintaxe, mas introduz uma proteção contra "falhas", exigindo uma saída após cada cláusula de caso (não vazia). Mas isso muda a forma como pensamos na mudança! Os casos agora são ramificações alternativas , como as ramificações em um if-else. Isso significa que esperamos que cada ramo defina seu próprio escopo, como cláusulas se ou cláusulas de iteração.

Em resumo: os casos compartilham o mesmo escopo, porque é assim que é em C. Mas parece estranho e inconsistente em C #, porque pensamos nos casos como ramificações alternativas e não como alvos.

JacquesB
fonte
1
" parece estranho e inconsistente em C # porque pensamos nos casos como ramos alternativos ao invés de ir para alvos " Essa é precisamente a confusão que tive. +1 por explicar as razões estéticas por trás do motivo de parecer errado em C #.
PSWG
4

Uma maneira simplificada de examinar o escopo é considerar o escopo por blocos {}.

Como switchnão contém nenhum bloco, ele não pode ter escopos diferentes.

Guvante
fonte
3
Que tal for (int i = 0; i < n; i++) Write(i); /* legal */ Write(i); /* illegal */? Não há blocos, mas existem escopos diferentes.
svick
@svick: Como eu disse, simplificado, existe um bloco, criado pela fordeclaração. switchnão cria blocos para cada caso, apenas no nível superior. A semelhança é que cada instrução cria um bloco (contando switchcomo a instrução).
Guvante
1
Então, por que você não contou case?
svick
1
@svick: porque casenão contém um bloco, a menos que você decida adicionar um opcional. fordura a próxima instrução, a menos que você a adicione {}, sempre tem um bloco, mas casedura até que algo faça com que você deixe o bloco real, a switchinstrução.
Guvante