¿Por qué C # no tiene alcance local en los bloques de casos?

38

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

Y se sorprendió al encontrar un error de compilación:

  

Una variable local llamada 'acción' ya está definida en este ámbito

Fue un problema bastante fácil de resolver; Simplemente deshacerse del segundo var hizo el truco.

Evidentemente, las variables declaradas en bloques case tienen el alcance del switch padre, pero tengo curiosidad por saber por qué esto es así. Dado que C # no permite que la ejecución caiga en otros casos ( requiere break , return , throw , o goto case de las declaraciones al final de cada bloque case ), parece bastante extraño que permita que se utilicen declaraciones de variables dentro de un case o que entren en conflicto con las variables en cualquier otro %código%. En otras palabras, las variables parecen caer en case de las declaraciones, aunque la ejecución no puede. C # hace grandes esfuerzos para promover la legibilidad al prohibir algunas construcciones de otros lenguajes que son confusos o que se abusan fácilmente. Pero esto parece que está destinado a causar confusión. Considere los siguientes escenarios:

  1. Si fuera a cambiarlo a esto:

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

    Obtengo " Uso de la variable local sin asignar 'acción' ". Esto es confuso porque en cualquier otra construcción en C # que se me ocurra, case inicializaría la variable, pero aquí simplemente la declara.

  2. Si tuviera que cambiar los casos de esta manera:

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

    Obtengo " No puedo usar la variable 'acción' local antes de que se declare ". Así que el orden de los bloques de casos parece ser importante aquí de una manera que no es del todo obvia. Normalmente, podría escribirlos en el orden que desee, pero porque var action = ... debe aparecer en el primer bloque donde se usa var , Tengo que modificar action bloques en consecuencia.

  3. Si fuera a cambiarlo a esto:

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

    Entonces no obtengo ningún error, pero en cierto sentido, parece que se asigna un valor a la variable antes de que se declare.
    (Aunque no puedo pensar en ningún el momento en el que realmente desea hacer esto (ni siquiera sabía que case existía antes de hoy)

Entonces, mi pregunta es, ¿por qué los diseñadores de C # give goto case bloquean su alcance local? ¿Hay razones históricas o técnicas para esto?

    
pregunta p.s.w.g 16.04.2013 - 01:06

5 respuestas

24

Creo que una buena razón es que, en cualquier otro caso, el alcance de una variable local "normal" es un bloque delimitado por llaves ( {} ). Las variables locales que no son normales aparecen en una construcción especial antes de una declaración (que generalmente es un bloque), como una variable de bucle for o una variable declarada en using .

Una excepción más son las variables locales en las expresiones de consulta LINQ, pero son completamente diferentes de las declaraciones de variables locales normales, por lo que no creo que exista una posibilidad de confusión.

Para referencia, las reglas están en §3.7 Ámbitos de la especificación de C #:

  
  • El alcance de una variable local declarada en una local-variable-declaraciones es el bloque en el que se produce la declaración.

  •   
  • El alcance de una variable local declarada en un switch-block de una declaración switch es el switch-block .

  •   
  • El alcance de una variable local declarada en un for-initializer de una declaración for es el for-initializer , el for- condicion , el for-iterator y la sentencia contenida de la sentencia for .

  •   
  • El alcance de una variable declarada como parte de una foreach-statement , using-statement , lock-statement o < em> query-expression está determinada por la expansión de la construcción dada.

  •   

(Aunque no estoy completamente seguro de por qué se menciona explícitamente el bloque switch , ya que no tiene una sintaxis especial para las declaciones de variables locales, a diferencia de todas las otras construcciones mencionadas).

    
respondido por el svick 16.04.2013 - 01:59
41

Pero lo hace. Puedes crear ámbitos locales en cualquier lugar envolviendo líneas 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.");
}
    
respondido por el Mika Kolari 16.04.2013 - 07:47
9

Citaré a Eric Lippert, cuya respuesta es bastante clara sobre el tema:

  

Una pregunta razonable es "¿por qué esto no es legal?" Una respuesta razonable   es "bien, ¿por qué debería ser"? Puedes tenerlo de una de dos maneras. Ya sea   esto es legal:

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

o esto es legal:

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

pero no puedes tenerlo de ambas maneras. Los diseñadores de C # eligieron.   La segunda forma parece ser la forma más natural de hacerlo.

     

Esta decisión se tomó el 7 de julio de 1999, hace apenas diez años.   Los comentarios en las notas de ese día son extremadamente breves, simplemente   indicando "Un caso de conmutación no crea su propio espacio de declaración" y   luego, proporcione un código de ejemplo que muestre qué funciona y qué no.

     

Para obtener más información sobre lo que estaba en la mente de los diseñadores sobre esto   Un día en particular, tendría que molestar a mucha gente sobre lo que eran   Hace diez años, pensé en lo que en última instancia es un error.   cuestión trivial; No voy a hacer eso.

     

En resumen, no hay una razón particularmente convincente para elegir una forma   o el otro; ambos tienen méritos. El equipo de diseño de idiomas eligió una forma.   porque tenían que escoger uno; el que escogieron parece razonable   yo.

Por lo tanto, a menos que esté más involucrado con el equipo de desarrolladores de C # de 1999 que con Eric Lippert, ¡nunca sabrá la razón exacta!

    
respondido por el Cyril Gandon 17.04.2013 - 16:10
4

Una forma simplificada de ver el alcance es considerar el alcance por bloques {} .

Dado que switch no contiene ningún bloque, no puede tener diferentes ámbitos.

    
respondido por el Guvante 16.04.2013 - 01:44
4

La explicación es simple: se debe a que es así en C. Lenguajes como C ++, Java y C # han copiado la sintaxis y el alcance de la declaración del conmutador en aras de la familiaridad.

(Como se indicó en otra respuesta, los desarrolladores de C # no tienen documentación sobre cómo se tomó esta decisión con respecto a los alcances de los casos. Pero el principio no declarado para la sintaxis de C # era que, a menos que tuvieran razones convincentes para hacerlo de manera diferente, Java copiado.)

En C, las declaraciones de casos son similares a goto-labels. La declaración de cambio es realmente una sintaxis más agradable para un goto computado . Los casos definen los puntos de entrada en el bloque de interruptores. Por defecto, el resto del código se ejecutará, a menos que haya una salida explícita. Así que solo tiene sentido que usen el mismo ámbito.

(Más fundamentalmente. Los Goto no están estructurados, no definen ni delimitan secciones de código, solo definen puntos de salto. Por lo tanto, una etiqueta goto no puede introducir una alcance.)

C # conserva la sintaxis, pero introduce una protección contra "caídas" al requerir una salida después de cada cláusula de caso (no vacía). ¡Pero esto cambia la forma en que pensamos en el cambio! Los casos ahora son ramas alternativas , como las ramas en if-else. Esto significa que esperaríamos que cada rama defina su propio alcance, como las cláusulas if y las cláusulas de iteración.

En resumen: los casos comparten el mismo alcance porque así es como está en C. Pero parece extraño e inconsistente en C # porque consideramos los casos como ramas alternativas en lugar de ir a objetivos.

    
respondido por el JacquesB 06.10.2017 - 09:59

Lea otras preguntas en las etiquetas