¿Por qué C # no tiene alcance local en 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 me sorprendió encontrar un error de compilación:

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

Era un problema bastante fácil de resolver; solo deshacerme del segundo varhizo el truco.

Evidentemente, las variables declaradas en casebloques tienen el alcance del padre switch, pero tengo curiosidad de por qué esto es así. Dado que C # no permite la ejecución de caer a través de otros casos (que requiere break , return, throw, o goto casedeclaraciones al final de cada casebloque), parece bastante extraño que permitiría a las declaraciones de variables dentro de una casea usarse o conflicto con las variables en cualquier otro case. En otras palabras, las variables parecen caer a través de las casedeclaraciones aunque la ejecución no puede. C # se esfuerza mucho para promover la legibilidad al prohibir algunas construcciones de otros lenguajes que son confusos o de abuso fácil. 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 no asignada 'acción' ". Esto es confuso porque en cualquier otra construcción en C # que se me ocurra var action = ...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 se puede usar la variable local 'acción' antes de que se declare ". Entonces, 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 debido a que vardebe aparecer en el primer bloque donde actionse usa, tengo que ajustar los casebloques 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 a la variable se le está asignando un valor antes de que se declare.
    (Aunque no puedo pensar en ningún momento en el que realmente quieras hacer esto, ni siquiera sabía que goto caseexistía antes de hoy)

Entonces mi pregunta es, ¿por qué los diseñadores de C # no le dieron a los casebloques su propio alcance local? ¿Hay alguna razón histórica o técnica para esto?

pswg
fuente
44
Declare su actionvariable antes de la switchdeclaración, o ponga cada caso en sus propios corchetes, y obtendrá un comportamiento sensato.
Robert Harvey
3
@RobertHarvey, sí, y podría ir aún más lejos y escribir esto sin usar ninguno, switchsolo tengo curiosidad sobre el razonamiento detrás de este diseño.
pswg
si c # necesita un descanso después de cada caso, entonces supongo que tiene que ser por razones históricas, como en c / java, ¡ese no es el caso!
tgkprog
@tgkprog Creo que no es por razones históricas y que los diseñadores de C # lo hicieron a propósito para asegurarse de que el error común de olvidar a breakno sea posible en C #.
svick
12
Vea la respuesta de Eric Lippert sobre esta pregunta SO relacionada. stackoverflow.com/a/1076642/414076 Puede o no salir satisfecho. Se lee como "porque así es como decidieron hacerlo en 1999".
Anthony Pegram

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 forvariable de bucle 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 haya una posibilidad de confusión allí.

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

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

  • El alcance de una variable local declarada en un bloque de conmutación de una switchinstrucción es el bloque de conmutación .

  • El alcance de una variable local declarada en una para-inicializador de un forcomunicado es la -inicializador para , el para-estado , el de-iterador , y la contenida declaración de la fordeclaración.

  • El alcance de una variable declarada como parte de un foreach-declaración , utilizando-declaración , lock-declaración o consulta de expresión está determinada por la expansión de la construcción dada.

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

svick
fuente
1
+1 ¿Puede proporcionar un enlace al alcance que está citando?
pswg
@pswg Puede encontrar un enlace para descargar la especificación en MSDN . (Haga clic en el enlace que dice "Microsoft Developer Network (MSDN)" en esa página.)
svick
Así que busqué las especificaciones de Java, y switchesparece que me comporto de manera idéntica a este respecto (excepto por requerir saltos al final de case's). Parece que este comportamiento simplemente se copió desde allí. Entonces, supongo que la respuesta corta es: las casedeclaraciones no crean bloques, simplemente definen las divisiones de un switchbloque y, por lo tanto, no tienen ámbitos por sí mismos.
pswg
3
Re: No estoy completamente seguro de por qué se menciona explícitamente el bloque de conmutación : el autor de la especificación solo es exigente, señalando implícitamente que un bloque de conmutación tiene una gramática diferente que un bloque normal.
Eric Lippert
42

Pero lo hace. Puede crear ámbitos locales en cualquier lugar ajustando 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.");
}
Mike Koder
fuente
Sí, usó esto y funciona como se describe
Mvision
1
+1 Este es un buen truco, pero voy a aceptar la respuesta de svick por estar más cerca de abordar mi pregunta original.
pswg
10

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 "bueno, ¿por qué debería serlo?" Puedes tenerlo de dos maneras. O 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 en ambos sentidos. Los diseñadores de C # eligieron la segunda forma, ya que parece ser la forma más natural de hacerlo.

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

Para saber más sobre lo que estaba en la mente de los diseñadores en este día en particular, tendría que molestar a muchas personas sobre lo que estaban pensando hace diez años, y sobre lo que en última instancia es un problema trivial; No voy a hacer eso.

En resumen, no hay una razón particularmente convincente para elegir un camino u otro; Ambos tienen méritos. El equipo de diseño del lenguaje eligió un camino porque tenían que elegir uno; el que escogieron me parece razonable.

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

Cyril Gandon
fuente
No es un votante negativo, pero este fue un comentario hecho sobre la pregunta ayer .
Jesse C. Slicer
44
Lo veo, pero como el comentarista no publica una respuesta, pensé que podría ser una buena idea crear explícitamente la respuesta citando el contenido del enlace. ¡Y no me importan los votos negativos! El OP pregunta la razón histórica, no una respuesta de "Cómo hacerlo".
Cyril Gandon
5

La explicación es simple: es así en C. Los lenguajes como C ++, Java y C # han copiado la sintaxis y el alcance de la instrucción switch por razones de 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 al alcance de los casos. Pero el principio no declarado de la sintaxis de C # era que, a menos que tuvieran razones convincentes para hacerlo de manera diferente, copiaban Java. )

En C, las declaraciones de caso son similares a las etiquetas de goto. La declaración de cambio es realmente una sintaxis más agradable para un goto calculado . 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. Por lo tanto, solo tiene sentido que usen el mismo alcance.

(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 un alcance).

C # retiene la sintaxis, pero introduce una protección contra el "fallo" 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 alternas , como las ramas en un if-else. Esto significa que esperaríamos que cada rama defina su propio alcance al igual que las cláusulas if o 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 pensamos en los casos como ramas alternativas en lugar de ir a objetivos.

JacquesB
fuente
1
" Parece extraño e inconsistente en C # porque pensamos en los casos como ramas alternativas en lugar de ir a objetivos " Esa es precisamente la confusión que tuve. +1 por explicar las razones estéticas detrás de por qué se siente mal en C #.
pswg
4

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

Como switchno contiene ningún bloque, no puede tener ámbitos diferentes.

Guvante
fuente
3
¿Qué hay de for (int i = 0; i < n; i++) Write(i); /* legal */ Write(i); /* illegal */? No hay bloques, pero hay diferentes ámbitos.
svick
@svick: Como dije, simplificado, hay un bloque creado por la fordeclaración. switchno crea bloques para cada caso, solo en el nivel superior. La similitud es que cada declaración crea un bloque (contando switchcomo la declaración).
Guvante
1
Entonces, ¿por qué no podrías contar en su caselugar?
svick
1
@svick: Porque caseno contiene un bloque, a menos que decidas agregar uno opcionalmente. fordura la siguiente declaración a menos que agregue, {}por lo que siempre tiene un bloque, pero casedura hasta que algo haga que abandone el bloque real, la switchdeclaración.
Guvante