Al escribir una declaración de cambio, parece haber dos limitaciones sobre lo que puede activar en las declaraciones de caso.
Por ejemplo (y sí, lo sé, si estás haciendo este tipo de cosas, probablemente significa que tu arquitectura orientada a objetos (OO) es dudosa, ¡este es solo un ejemplo artificial!),
Type t = typeof(int);
switch (t) {
case typeof(int):
Console.WriteLine("int!");
break;
case typeof(string):
Console.WriteLine("string!");
break;
default:
Console.WriteLine("unknown!");
break;
}
Aquí la instrucción switch () falla con 'Se espera un valor de tipo integral' y las declaraciones de caso fallan con 'Se espera un valor constante'.
¿Por qué existen estas restricciones y cuál es la justificación subyacente? No veo ninguna razón por la cual la declaración del interruptor tiene que sucumbir solo al análisis estático, y por qué el valor que se debe activar debe ser integral (es decir, primitivo). ¿Cuál es la justificación?
c#
switch-statement
ljs
fuente
fuente
Respuestas:
Esta es mi publicación original, que provocó un debate ... porque está mal :
De hecho, la declaración de cambio de C # no siempre es una rama de tiempo constante.
En algunos casos, el compilador usará una declaración de cambio CIL que es, de hecho, una rama de tiempo constante usando una tabla de salto. Sin embargo, en casos escasos como lo señaló Ivan Hamilton, el compilador puede generar algo completamente distinto.
En realidad, esto es bastante fácil de verificar escribiendo varias instrucciones de cambio de C #, algunas dispersas, otras densas y mirando el CIL resultante con la herramienta ildasm.exe.
fuente
switch
instrucción (del CIL) que no es lo mismo que laswitch
declaración de C #.Es importante no confundir la declaración de cambio de C # con la instrucción de cambio de CIL.
El interruptor CIL es una tabla de salto, que requiere un índice en un conjunto de direcciones de salto.
Esto solo es útil si los casos del interruptor C # son adyacentes:
Pero de poca utilidad si no lo son:
(Necesitaría una tabla de ~ 3000 entradas de tamaño, con solo 3 ranuras utilizadas)
Con expresiones no adyacentes, el compilador puede comenzar a realizar comprobaciones lineales if-else-if-else.
Con conjuntos de expresiones no adyacentes más grandes, el compilador puede comenzar con una búsqueda de árbol binario, y finalmente los últimos elementos if-else-if-else.
Con conjuntos de expresiones que contienen grupos de elementos adyacentes, el compilador puede buscar en árbol binario y finalmente un interruptor CIL.
Esto está lleno de "mays" y "mights", y depende del compilador (puede diferir con Mono o Rotor).
Repliqué sus resultados en mi máquina usando casos adyacentes:
Luego también lo hice usando expresiones de casos no adyacentes:
Lo curioso aquí es que la búsqueda de árbol binario parece un poco (probablemente no estadísticamente) más rápida que la instrucción de cambio de CIL.
Brian, has usado la palabra " constante ", que tiene un significado muy definido desde la perspectiva de la teoría de la complejidad computacional. Mientras que el ejemplo entero adyacente simplista puede producir CIL que se considera O (1) (constante), un ejemplo disperso es O (log n) (logarítmico), los ejemplos agrupados se encuentran en algún punto intermedio, y los ejemplos pequeños son O (n) (lineal )
Esto ni siquiera aborda la situación de String, en la que
Generic.Dictionary<string,int32>
se puede crear una estática , y sufrirá una sobrecarga definitiva en el primer uso. El rendimiento aquí dependerá del rendimiento deGeneric.Dictionary
.Si marca la Especificación del lenguaje C # (no la especificación CIL), encontrará "15.7.2 La declaración de cambio" no menciona el "tiempo constante" o que la implementación subyacente incluso utiliza la instrucción de cambio CIL (tenga mucho cuidado de asumir tales cosas).
Al final del día, un cambio de C # contra una expresión entera en un sistema moderno es una operación de menos de un microsegundo, y normalmente no vale la pena preocuparse.
Por supuesto, estos tiempos dependerán de las máquinas y las condiciones. No prestaría atención a estas pruebas de tiempo, las duraciones de microsegundos de las que estamos hablando están eclipsadas por cualquier código "real" que se esté ejecutando (y debe incluir algún "código real", de lo contrario, el compilador optimizará la ramificación), o nerviosismo en el sistema. Mis respuestas se basan en el uso de IL DASM para examinar el CIL creado por el compilador de C #. Por supuesto, esto no es definitivo, ya que las instrucciones reales que ejecuta la CPU son creadas por el JIT.
Verifiqué las instrucciones finales de la CPU realmente ejecutadas en mi máquina x86, y puedo confirmar que un simple interruptor de configuración adyacente haga algo como:
Donde una búsqueda de árbol binario está llena de:
fuente
La primera razón que viene a la mente es histórica :
Como la mayoría de los programadores de C, C ++ y Java no están acostumbrados a tener tales libertades, no los exigen.
Otra razón más válida es que la complejidad del lenguaje aumentaría :
En primer lugar, ¿se deben comparar los objetos con
.Equals()
o con el==
operador? Ambos son válidos en algunos casos. ¿Deberíamos introducir una nueva sintaxis para hacer esto? ¿Deberíamos permitir que el programador introduzca su propio método de comparación?Además, permitir encender objetos rompería los supuestos subyacentes sobre la declaración de cambio . Hay dos reglas que rigen la declaración de cambio que el compilador no podría aplicar si se permitiera encender los objetos (consulte la especificación del lenguaje C # versión 3.0 , §8.7.2):
Considere este ejemplo de código en el caso hipotético de que se permitieron valores de caso no constantes:
¿Qué hará el código? ¿Qué pasa si se reordenan las declaraciones del caso? De hecho, una de las razones por las cuales C # hizo ilegal la interrupción del cambio es que las declaraciones de cambio podrían reorganizarse arbitrariamente.
Estas reglas están en su lugar por una razón, para que el programador pueda, al mirar un bloque de casos, saber con certeza la condición precisa bajo la cual se ingresa el bloque. Cuando la declaración de cambio antes mencionada crece en 100 líneas o más (y lo hará), dicho conocimiento es invaluable.
fuente
Por cierto, VB, que tiene la misma arquitectura subyacente, permite
Select Case
declaraciones mucho más flexibles (el código anterior funcionaría en VB) y aún produce código eficiente donde esto es posible, por lo que el argumento por restricción técnica debe considerarse cuidadosamente.fuente
Select Case
en VB es muy flexible y ahorra mucho tiempo. Lo extraño mucho.En su mayoría, esas restricciones están vigentes debido a los diseñadores de idiomas. La justificación subyacente puede ser la compatibilidad con la historia, los ideales o la simplificación del diseño del compilador.
El compilador puede (y lo hace) elegir:
La declaración de cambio NO ES una rama de tiempo constante. El compilador puede encontrar atajos (usando cubos hash, etc.), pero los casos más complicados generarán un código MSIL más complicado con algunos casos que se ramifican antes que otros.
Para manejar el caso de String, el compilador terminará (en algún momento) usando a.Equals (b) (y posiblemente a.GetHashCode ()). Creo que sería una complicidad para el compilador usar cualquier objeto que satisfaga estas restricciones.
En cuanto a la necesidad de expresiones de caso estáticas ... algunas de esas optimizaciones (hashing, almacenamiento en caché, etc.) no estarían disponibles si las expresiones de caso no fueran deterministas. Pero ya hemos visto que a veces el compilador simplemente elige el camino simplista if-else-if-else de todos modos ...
Editar: lomaxx : su comprensión del operador "typeof" no es correcta. El operador "typeof" se usa para obtener el objeto System.Type para un tipo (nada que ver con sus supertipos o interfaces). Verificar la compatibilidad en tiempo de ejecución de un objeto con un tipo dado es el trabajo del operador "es". El uso de "typeof" aquí para expresar un objeto es irrelevante.
fuente
Mientras habla sobre el tema, según Jeff Atwood, la declaración de cambio es una atrocidad de programación . Úsalos con moderación.
A menudo puede realizar la misma tarea usando una tabla. Por ejemplo:
fuente
enum
tipo. Tampoco es una coincidencia que intellisense llene automáticamente una declaración de cambio cuando se activa una variable de unenum
tipo.switch
declaración. No está diciendo que no debas escribir máquinas de estado, solo que puedes hacer lo mismo usando tipos específicos agradables. Por supuesto, esto es mucho más fácil en lenguajes como F # que tienen tipos que pueden cubrir fácilmente estados bastante complejos. Para su ejemplo, podría usar uniones discriminadas donde el estado se convierte en parte del tipo y reemplazar elswitch
con coincidencia de patrones. O use interfaces, por ejemplo.Dictionary
habría sido considerablemente más lento que unaswitch
declaración optimizada ...?Es cierto que no tiene a, y muchos lenguajes de hecho emplean sentencias switch dinámico. Sin embargo, esto significa que reordenar las cláusulas de "caso" puede cambiar el comportamiento del código.
Hay alguna información interesante detrás de las decisiones de diseño que entraron en "cambiar" aquí: ¿Por qué la declaración de cambio C # está diseñada para no permitir fallos , pero aún así requiere un descanso?
Permitir expresiones de caso dinámicas puede conducir a monstruosidades como este código PHP:
que francamente solo debería usar la
if-else
declaración.fuente
¡Microsoft finalmente te escuchó!
Ahora con C # 7 puedes:
fuente
Esta no es una razón, pero la sección 8.7.2 de la especificación de C # establece lo siguiente:
La especificación C # 3.0 se encuentra en: http://download.microsoft.com/download/3/8/8/388e7205-bc10-4226-b2a8-75351c669b09/CSharp%20Language%20Specification.doc
fuente
La respuesta de Judá anterior me dio una idea. Puede "fingir" el comportamiento de cambio del OP usando un
Dictionary<Type, Func<T>
:Esto le permite asociar el comportamiento con un tipo en el mismo estilo que la instrucción switch. Creo que tiene el beneficio adicional de estar codificado en lugar de una tabla de salto de estilo de conmutador cuando se compila en IL.
fuente
Supongo que no hay una razón fundamental por la cual el compilador no pudo traducir automáticamente su declaración de cambio a:
Pero no se gana mucho con eso.
Una declaración de caso sobre tipos integrales permite al compilador realizar una serie de optimizaciones:
No hay duplicación (a menos que duplique etiquetas de caso, que el compilador detecta). En su ejemplo, t podría coincidir con varios tipos debido a la herencia. ¿Se debe ejecutar el primer partido? ¿Todos ellos?
El compilador puede optar por implementar una instrucción switch sobre un tipo integral mediante una tabla de salto para evitar todas las comparaciones. Si está activando una enumeración que tiene valores enteros de 0 a 100, crea una matriz con 100 punteros, uno para cada instrucción de cambio. En tiempo de ejecución, simplemente busca la dirección de la matriz en función del valor entero que se activa. Esto hace que el rendimiento en tiempo de ejecución sea mucho mejor que realizar 100 comparaciones.
fuente
switch (t) { case typeof(int): ... }
porque su traducción implica que la variablet
debe recuperarse de la memoria dos veces sit != typeof(int)
, mientras que este último sería (supuestamente) siempre lea el valor det
exactamente una vez . Esta diferencia puede romper la corrección del código concurrente que se basa en esas excelentes garantías. Para obtener más información sobre esto, vea Programación concurrente deDe acuerdo con la documentación de la declaración de cambio, si hay una forma inequívoca de convertir implícitamente el objeto a un tipo integral, entonces se permitirá. Creo que está esperando un comportamiento en el que para cada enunciado de caso se reemplazaría
if (t == typeof(int))
, pero eso abriría una lata completa de gusanos cuando se sobrecarga ese operador. El comportamiento cambiaría cuando los detalles de implementación para la instrucción switch cambiaran si escribiera su == override incorrectamente. Al reducir las comparaciones a tipos integrales y cadenas y aquellas cosas que pueden reducirse a tipos integrales (y están destinadas a hacerlo) evitan posibles problemas.fuente
Dado que el lenguaje permite que el tipo de cadena se use en una declaración de cambio, supongo que el compilador no puede generar código para una implementación de rama de tiempo constante para este tipo y necesita generar un estilo if-then.
@mweerden - Ah, ya veo. Gracias.
No tengo mucha experiencia en C # y .NET, pero parece que los diseñadores de lenguaje no permiten el acceso estático al sistema de tipos, excepto en circunstancias limitadas. La palabra clave typeof devuelve un objeto, por lo que solo se puede acceder en tiempo de ejecución.
fuente
Creo que Henk lo logró con la cuestión de "no tener acceso estático al sistema de tipos"
Otra opción es que no hay un orden de tipos donde pueden estar los números y las cadenas. Por lo tanto, un interruptor de tipo no podría construir un árbol de búsqueda binario, solo una búsqueda lineal.
fuente
Estoy de acuerdo con este comentario en que usar un enfoque basado en tablas suele ser mejor.
En C # 1.0 esto no fue posible porque no tenía delegados genéricos y anónimos. Las nuevas versiones de C # tienen el andamiaje para que esto funcione. Tener una notación para literales de objetos también es útil.
fuente
Prácticamente no tengo conocimiento de C #, pero sospecho que cualquiera de los cambios simplemente se tomó como ocurre en otros idiomas sin pensar en hacerlo más general o el desarrollador decidió que extenderlo no valía la pena.
Estrictamente hablando, tiene toda la razón en que no hay razón para ponerle restricciones. Uno podría sospechar que la razón es que, para los casos permitidos, la implementación es muy eficiente (como lo sugiere Brian Ensink ( 44921 )), pero dudo que la implementación sea muy eficiente (wrt if-declaraciones) si uso números enteros y algunos casos aleatorios (por ejemplo, 345, -4574 y 1234203). Y, en cualquier caso, cuál es el daño al permitirlo para todo (o al menos más) y decir que solo es eficiente para casos específicos (como (casi) números consecutivos).
Sin embargo, puedo imaginar que uno podría excluir tipos debido a razones como la que da lomaxx ( 44918 ).
Editar: @Henk ( 44970 ): si las cadenas se comparten al máximo, las cadenas con el mismo contenido también serán punteros a la misma ubicación de memoria. Luego, si puede asegurarse de que las cadenas utilizadas en los casos se almacenan consecutivamente en la memoria, puede implementar el conmutador de manera muy eficiente (es decir, con la ejecución en el orden de 2 comparaciones, una suma y dos saltos).
fuente
C # 8 le permite resolver este problema de manera elegante y compacta usando una expresión de interruptor:
Como resultado, obtienes:
Puede leer más sobre la nueva característica aquí .
fuente