Por que o C # não possui escopo local em blocos de casos?

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:

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

Foi uma questão bastante fácil de resolver; apenas se livrando do segundo var fez o truque.

Evidentemente, as variáveis declaradas em case blocks têm o escopo do pai switch , mas estou curioso para saber o motivo. Como o C # não permite que a execução caia em outros casos ( requer break , return , throw ou goto case declarações no final de cada case de blocos), parece bastante estranho que permitiria que as declarações de variáveis dentro de um case fossem usadas ou entrassem em conflito com variáveis em qualquer outra %código%. Em outras palavras, as variáveis parecem cair através de case declarações, mesmo que a execução não possa. O C # se esforça muito para promover a legibilidade, proibindo algumas construções de outras linguagens que são confusas ou facilmente violáveis. Mas isso parece apenas causar confusão. Considere os seguintes cenários:

  1. Se fosse mudá-lo para isso:

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

    Eu recebo " Uso da variável local não atribuída 'action' ". Isso é confuso porque em todas as outras construções em C # que eu posso pensar em case 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);
    

    Eu recebo " Não é possível usar a variável local 'action' antes de ser declarada ". Assim, a ordem dos blocos de casos parece ser importante aqui de uma forma que não é totalmente óbvia - normalmente eu poderia escrevê-los em qualquer ordem que desejar, mas porque o var action = ... deve aparecer no primeiro bloco em que var é usado , Tenho que ajustar action blocos de acordo.

  3. Se fosse mudá-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;
    

    Então eu não recebo nenhum erro, mas de certo modo, parece que a variável recebe um valor antes de ser declarada.
    (Embora eu não consiga pensar em nenhum vez que você realmente quer fazer isso - eu nem sabia que case existia antes de hoje)

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

    
por p.s.w.g 16.04.2013 / 01:06
fonte

5 respostas

24

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 variável de loop for ou uma variável declarada em using .

Mais uma exceção são as variáveis locais nas expressões de consulta LINQ, mas elas são completamente diferentes das declarações de variáveis locais normais, portanto, não creio que haja uma chance de confusão.

Para referência, as regras estão em §3.7 Escopos da especificação 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.

(Embora eu não esteja completamente certo do porque o bloco switch foi explicitamente mencionado, uma vez que ele não possui nenhuma sintaxe especial para declinações de variáveis locais, ao contrário de todas as outras construções mencionadas.)

    
por 16.04.2013 / 01:59
fonte
41

Mas isso acontece. Você pode criar escopos locais em qualquer lugar agrupando 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.");
}
    
por 16.04.2013 / 07:47
fonte
9

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

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.

Então, a menos que você esteja mais atento com a equipe de desenvolvedores de C # de 1999 do que Eric Lippert, você nunca saberá o motivo exato!

    
por 17.04.2013 / 16:10
fonte
4

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

Como switch não contém blocos, não pode ter escopos diferentes.

    
por 16.04.2013 / 01:44
fonte
4

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 causa da familiaridade.

(Como afirmado em outra resposta, os desenvolvedores do C # não têm documentação sobre como essa decisão foi tomada em relação aos escopos de casos. Mas o princípio não declarado da sintaxe do C # era que, a menos que eles tivessem razões para fazer isso de forma diferente, eles Java copiado.)

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

(Mais fundamentalmente. Os Goto não são estruturados - eles não definem ou delimitam seções de código, eles 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 salvaguarda contra o "fall through", exigindo uma saída após cada cláusula (não vazia) do caso. 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 esperaríamos que cada ramificação definisse seu próprio escopo como cláusulas if ou cláusulas de iteração.

Em suma: os casos compartilham o mesmo escopo, porque é assim em C. Mas parece estranho e inconsistente em C # porque pensamos em casos como ramificações alternadas em vez de metas goto.

    
por 06.10.2017 / 09:59
fonte