Tengo una switch
estructura que tiene varios casos para manejar. El switch
opera sobre un enum
que plantea el problema del código duplicado a través de valores combinados:
// All possible combinations of One - Eight.
public enum ExampleEnum {
One,
Two, TwoOne,
Three, ThreeOne, ThreeTwo, ThreeOneTwo,
Four, FourOne, FourTwo, FourThree, FourOneTwo, FourOneThree,
FourTwoThree, FourOneTwoThree
// ETC.
}
Actualmente la switch
estructura maneja cada valor por separado:
// All possible combinations of One - Eight.
switch (enumValue) {
case One: DrawOne; break;
case Two: DrawTwo; break;
case TwoOne:
DrawOne;
DrawTwo;
break;
case Three: DrawThree; break;
...
}
Tienes la idea allí. Actualmente tengo esto desglosado en una if
estructura apilada para manejar combinaciones con una sola línea:
// All possible combinations of One - Eight.
if (One || TwoOne || ThreeOne || ThreeOneTwo)
DrawOne;
if (Two || TwoOne || ThreeTwo || ThreeOneTwo)
DrawTwo;
if (Three || ThreeOne || ThreeTwo || ThreeOneTwo)
DrawThree;
Esto plantea el problema de evaluaciones lógicas increíblemente largas que son confusas de leer y difíciles de mantener. Después de refactorizar esto, comencé a pensar en alternativas y pensé en la idea de una switch
estructura con falla entre los casos.
Tengo que usar un goto
en ese caso ya C#
que no permite fall-through. Sin embargo, evita las cadenas lógicas increíblemente largas a pesar de que salta alrededor de la switch
estructura, y aún trae duplicación de código.
switch (enumVal) {
case ThreeOneTwo: DrawThree; goto case TwoOne;
case ThreeTwo: DrawThree; goto case Two;
case ThreeOne: DrawThree; goto default;
case TwoOne: DrawTwo; goto default;
case Two: DrawTwo; break;
default: DrawOne; break;
}
Esto todavía no es una solución lo suficientemente limpia y hay un estigma asociado con la goto
palabra clave que me gustaría evitar. Estoy seguro de que tiene que haber una mejor manera de limpiar esto.
Mi pregunta
¿Hay una mejor manera de manejar este caso específico sin afectar la legibilidad y la mantenibilidad?
fuente
goto
cuando la estructura de alto nivel no existe en su idioma. A veces desearía que hubiera unfallthru
; palabra clave para deshacerse de ese uso particular de,goto
pero bueno.goto
en un lenguaje de alto nivel como C #, entonces probablemente haya pasado por alto muchas otras (y mejores) alternativas de diseño y / o implementación. Muy desanimado.Respuestas:
Encuentro el código difícil de leer con las
goto
declaraciones. Recomendaría estructurar suenum
diferente. Por ejemplo, si tuenum
fue un campo de bits donde cada bit representaba una de las opciones, podría verse así:El atributo Flags le dice al compilador que está configurando valores que no se superponen. El código que llama a este código podría establecer el bit apropiado. Entonces podría hacer algo como esto para dejar en claro lo que está sucediendo:
Esto requiere el código que se configura
myEnum
para establecer los campos de bits correctamente y marcado con el atributo Banderas. Pero puede hacerlo cambiando los valores de las enumeraciones en su ejemplo a:Cuando escribe un número en el formulario
0bxxxx
, lo está especificando en binario. Entonces puede ver que establecemos el bit 1, 2 o 3 (bueno, técnicamente 0, 1 o 2, pero se entiende). También puede nombrar combinaciones usando un OR bit a bit si las combinaciones pueden establecerse juntas con frecuencia.fuente
public enum ExampleEnum { One = 1 << 0, Two = 1 << 1, Three = 1 << 2, OneAndTwo = One | Two, OneAndThree = One | Three, TwoAndThree = Two | Three };
. No es necesario insistir en C # 7 +.[Flags]
atributo no indica nada al compilador. Es por eso que todavía tiene que declarar explícitamente los valores de enumeración como potencias de 2.HasFlag
es un término descriptivo y explícito para la operación que se realiza y abstrae la implementación de la funcionalidad. Usarlo&
porque es común en otros idiomas no tiene más sentido que usar unbyte
tipo en lugar de declarar unenum
.En mi opinión, la raíz del problema es que este código ni siquiera debería existir.
Aparentemente tiene tres condiciones independientes y tres acciones independientes para tomar si esas condiciones son ciertas. Entonces, ¿por qué todo eso se canaliza en una sola pieza de código que necesita tres banderas booleanas para decirle qué hacer (si las ofuscas o no en una enumeración) y luego hace una combinación de tres cosas independientes? El principio de responsabilidad única parece estar teniendo un mal día aquí.
Coloque las llamadas en las tres funciones donde pertenecen (es decir, donde descubre la necesidad de realizar las acciones) y consigne el código de estos ejemplos a la papelera de reciclaje.
Si hubiera diez banderas y acciones no tres, ¿ampliaría este tipo de código para manejar 1024 combinaciones diferentes? ¡Espero que no! Si 1024 es demasiado, 8 también es demasiado, por la misma razón.
fuente
Nunca usar gotos es uno de los conceptos de "mentiras a los niños" de la informática. Es el consejo correcto el 99% de las veces, y las veces que no lo son son tan raras y especializadas que es mucho mejor para todos si solo se explica a los nuevos codificadores como "no los usen".
Entonces, ¿cuándo deberían usarse? Hay algunos escenarios , pero el más básico al que te estás enfrentando es: cuando estás codificando una máquina de estados . Si no hay una mejor expresión organizada y estructurada de su algoritmo que una máquina de estados, entonces su expresión natural en el código involucra ramas no estructuradas, y no hay mucho que se pueda hacer sobre lo que posiblemente no forma la estructura del algoritmo. La máquina de estado en sí misma más oscura, en lugar de menos.
Los escritores de compiladores lo saben, por eso el código fuente para la mayoría de los compiladores que implementan analizadores LALR * contienen gotos. Sin embargo, muy pocas personas codificarán sus propios analizadores y analizadores léxicos.
* - IIRC, es posible implementar gramáticas LALL completamente descendentes recursivas sin recurrir a tablas de salto u otras declaraciones de control no estructuradas, por lo que si realmente eres anti-goto, esta es una salida.
Ahora la siguiente pregunta es: "¿Es este ejemplo uno de esos casos?"
Lo que veo al revisarlo es que tiene tres estados próximos posibles diferentes dependiendo del procesamiento del estado actual. Dado que uno de ellos ("predeterminado") es solo una línea de código, técnicamente podría deshacerse de él al agregar esa línea de código al final de los estados a los que se aplica. Eso lo llevaría a 2 posibles estados siguientes.
Uno de los restantes ("Tres") solo se ramifica desde un lugar que puedo ver. Para que pueda deshacerse de él de la misma manera. Terminarías con un código que se ve así:
Sin embargo, nuevamente este fue un ejemplo de juguete que usted proporcionó. En los casos en que "predeterminado" tiene una cantidad de código no trivial, "tres" se transfiere de múltiples estados, o (lo más importante) es probable que un mantenimiento adicional agregue o complique los estados , honestamente estaría mejor usando gotos (y tal vez incluso deshaciéndose de la estructura enum-case que oculta la naturaleza de la máquina de estado de las cosas, a menos que haya alguna buena razón para que permanezca).
fuente
goto
.La mejor respuesta es usar polimorfismo .
Otra respuesta, que, en mi opinión, hace que las cosas sean más claras y posiblemente más cortas :
goto
es probablemente mi 58a opción aquí ...fuente
¿Por qué no esto?
OK, estoy de acuerdo, esto es hack (no soy un desarrollador de C # por cierto, así que discúlpeme por el código), pero desde el punto de vista de la eficiencia, ¿esto es obligatorio? Usar enumeraciones como índice de matriz es válido C #.
fuente
int[ExempleEnum.length] COUNTS = { 1, 3, 4, 2, 5, 3 };
?Si no puede o no quiere usar banderas, use una función recursiva de cola. En el modo de lanzamiento de 64 bits, el compilador producirá código que es muy similar a su
goto
declaración. Simplemente no tienes que lidiar con eso.fuente
La solución aceptada está bien y es una solución concreta a su problema. Sin embargo, me gustaría plantear una solución alternativa más abstracta.
En mi experiencia, el uso de enumeraciones para definir el flujo de la lógica es un olor a código, ya que a menudo es un signo de diseño de clase pobre.
Me encontré con un ejemplo del mundo real de esto sucediendo en el código en el que trabajé el año pasado. El desarrollador original había creado una sola clase que importaba y exportaba lógica, y cambió entre las dos basándose en una enumeración. Ahora el código era similar y tenía algún código duplicado, pero era lo suficientemente diferente como para que hacerlo hiciera que el código fuera significativamente más difícil de leer y prácticamente imposible de probar. Terminé refactorizando eso en dos clases separadas, lo que simplificó ambos y en realidad me permitió detectar y eliminar una serie de errores no reportados.
Una vez más, debo decir que usar enumeraciones para controlar el flujo de la lógica es a menudo un problema de diseño. En el caso general, las enumeraciones deben usarse principalmente para proporcionar valores seguros para los tipos y amigables para el consumidor donde los valores posibles estén claramente definidos. Se usan mejor como propiedad (por ejemplo, como ID de columna en una tabla) que como mecanismo de control lógico.
Consideremos el problema presentado en la pregunta. Realmente no sé el contexto aquí, o lo que representa esta enumeración. ¿Es dibujar cartas? Hacer dibujos? ¿Dibujando sangre? ¿Es importante el orden? Tampoco sé cuán importante es el rendimiento. Si el rendimiento o la memoria son críticos, entonces esta solución probablemente no sea la que desea.
En cualquier caso, consideremos la enumeración:
Lo que tenemos aquí son varios valores de enumeración diferentes que representan diferentes conceptos de negocio.
Lo que podríamos usar en su lugar son abstracciones para simplificar las cosas.
Consideremos la siguiente interfaz:
Entonces podemos implementar esto como una clase abstracta:
Podemos tener una clase concreta para representar el dibujo uno, dos y tres (que por el argumento tienen una lógica diferente). Potencialmente, podrían usar la clase base definida anteriormente, pero supongo que el concepto DrawOne es diferente del concepto representado por la enumeración:
Y ahora tenemos tres clases separadas que pueden estar compuestas para proporcionar la lógica para las otras clases.
Este enfoque es mucho más detallado. Pero tiene ventajas.
Considere la siguiente clase, que contiene un error:
¿Qué tan simple es detectar la llamada drawThree.Draw () que falta? Y si el orden es importante, el orden de las llamadas de sorteo también es muy fácil de ver y seguir.
Desventajas de este enfoque:
Ventajas de este enfoque:
Considere este enfoque (o similar) cuando sienta la necesidad de tener un código de control lógico complejo escrito en las declaraciones de casos. Futuro serás feliz de haberlo hecho.
fuente
Si tiene la intención de usar un interruptor aquí, su código en realidad será más rápido si maneja cada caso por separado
solo se realiza una operación aritmética en cada caso
también, como otros han dicho, si está considerando usar gotos, probablemente debería repensar su algoritmo (aunque admitiré que la falta de caída de mayúsculas y minúsculas de C # podría ser una razón para usar un goto). Ver el famoso artículo de Edgar Dijkstra "Ir a la declaración considerada perjudicial"
fuente
Para su ejemplo particular, dado que todo lo que realmente deseaba de la enumeración era un indicador de hacer / no hacer para cada uno de los pasos, la solución que reescribe sus tres
if
declaraciones es preferible a aswitch
, y es bueno que la haya aceptado. .Pero si tenía una lógica más compleja que no funcionó de manera tan limpia, entonces todavía encuentro confuso el
goto
s en laswitch
declaración. Prefiero ver algo como esto:Esto no es perfecto, pero creo que es mejor así que con el
goto
s. Si las secuencias de eventos son tan largas y se duplican tanto que realmente no tiene sentido deletrear la secuencia completa para cada caso, preferiría una subrutina en lugar degoto
reducir la duplicación de código.fuente
La razón para odiar la
goto
palabra clave es código como¡Uy! Eso claramente no va a funcionar. La
message
variable no está definida en esa ruta de código. Entonces C # no pasará eso. Pero puede ser implícito. ConsiderarY supongamos que
display
luego tiene laWriteLine
declaración.Este tipo de error puede ser difícil de encontrar porque
goto
oscurece la ruta del código.Este es un ejemplo simplificado. Suponga que un ejemplo real no sería tan obvio. Puede haber cincuenta líneas de código entre
label:
y el uso demessage
.El lenguaje puede ayudar a solucionar esto al limitar cómo
goto
se puede usar, solo descendiendo de bloques. Pero el C #goto
no está limitado así. Puede saltar sobre el código. Además, si va a limitargoto
, es mejor cambiar el nombre. Otros idiomas se usanbreak
para descender de bloques, ya sea con un número (de bloques para salir) o una etiqueta (en uno de los bloques).El concepto de
goto
es una instrucción de lenguaje de máquina de bajo nivel. Pero toda la razón por la que tenemos lenguajes de nivel superior es para limitarnos a las abstracciones de nivel superior, por ejemplo, alcance variable.Dicho todo esto, si usa el C #
goto
dentro de unaswitch
declaración para saltar de un caso a otro, es razonablemente inofensivo. Cada caso ya es un punto de entrada. Todavía creo que llamarlogoto
es una tontería en esa situación, ya que combina este uso inofensivogoto
con formas más peligrosas. Preferiría que usaran algo asícontinue
para eso. Pero por alguna razón, se olvidaron de preguntarme antes de escribir el idioma.fuente
C#
lenguaje no puede saltar sobre declaraciones variables. Como prueba, inicie una aplicación de consola e ingrese el código que proporcionó en su publicación. Obtendrá un error de compilación en su etiqueta que indica que el error es el uso de la variable local no asignada 'mensaje' . En los idiomas donde esto está permitido, es una preocupación válida, pero no en el lenguaje C #.Cuando tienes tantas opciones (e incluso más, como dices), entonces tal vez no sea código sino datos.
Cree un diccionario que asigne valores de enumeración a acciones, expresados como funciones o como un tipo de enumeración más simple que representa las acciones. Luego, su código puede reducirse a una simple búsqueda en el diccionario, seguido de llamar el valor de la función o cambiar las opciones simplificadas.
fuente
Use un bucle y la diferencia entre romper y continuar.
fuente