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 var
hizo el truco.
Evidentemente, las variables declaradas en case
bloques 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 case
declaraciones al final de cada case
bloque), parece bastante extraño que permitiría a las declaraciones de variables dentro de una case
a usarse o conflicto con las variables en cualquier otro case
. En otras palabras, las variables parecen caer a través de las case
declaraciones 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:
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.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
var
debe aparecer en el primer bloque dondeaction
se usa, tengo que ajustar loscase
bloques en consecuencia.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 quegoto case
existía antes de hoy)
Entonces mi pregunta es, ¿por qué los diseñadores de C # no le dieron a los case
bloques su propio alcance local? ¿Hay alguna razón histórica o técnica para esto?
fuente
action
variable antes de laswitch
declaración, o ponga cada caso en sus propios corchetes, y obtendrá un comportamiento sensato.switch
solo tengo curiosidad sobre el razonamiento detrás de este diseño.break
no sea posible en C #.Respuestas:
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 unafor
variable de bucle o una variable declarada enusing
.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 #:
(Aunque no estoy completamente seguro de por qué se
switch
menciona 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).fuente
switches
parece que me comporto de manera idéntica a este respecto (excepto por requerir saltos al final decase
's). Parece que este comportamiento simplemente se copió desde allí. Entonces, supongo que la respuesta corta es: lascase
declaraciones no crean bloques, simplemente definen las divisiones de unswitch
bloque y, por lo tanto, no tienen ámbitos por sí mismos.Pero lo hace. Puede crear ámbitos locales en cualquier lugar ajustando líneas con
{}
fuente
Citaré a Eric Lippert, cuya respuesta es bastante clara sobre el tema:
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!
fuente
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.
fuente
Una forma simplificada de ver el alcance es considerar el alcance por bloques
{}
.Como
switch
no contiene ningún bloque, no puede tener ámbitos diferentes.fuente
for (int i = 0; i < n; i++) Write(i); /* legal */ Write(i); /* illegal */
? No hay bloques, pero hay diferentes ámbitos.for
declaración.switch
no crea bloques para cada caso, solo en el nivel superior. La similitud es que cada declaración crea un bloque (contandoswitch
como la declaración).case
lugar?case
no contiene un bloque, a menos que decidas agregar uno opcionalmente.for
dura la siguiente declaración a menos que agregue,{}
por lo que siempre tiene un bloque, perocase
dura hasta que algo haga que abandone el bloque real, laswitch
declaración.