¿Evitar el vudú `goto`?

47

Tengo una switchestructura que tiene varios casos para manejar. El switchopera sobre un enumque 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 switchestructura 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 ifestructura 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 switchestructura con falla entre los casos.

Tengo que usar un gotoen 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 switchestructura, 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 gotopalabra 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?

PerpetualJ
fuente
28
Parece que quieres tener un gran debate. Pero necesitará un mejor ejemplo de un buen caso para usar goto. flag enum resuelve perfectamente este, incluso si su ejemplo de declaración if no era aceptable y me parece bien
Ewan
55
@Ewan Nadie usa el goto. ¿Cuándo fue la última vez que miró el código fuente del kernel de Linux?
John Douma
66
Se usa gotocuando la estructura de alto nivel no existe en su idioma. A veces desearía que hubiera un fallthru; palabra clave para deshacerse de ese uso particular de, gotopero bueno.
Joshua
25
En términos generales, si siente la necesidad de usar a gotoen 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.
code_dredd
11
Usar "goto case" en C # es diferente de usar un "goto" más general, porque así es como se logra un error explícito.
Hammerite

Respuestas:

175

Encuentro el código difícil de leer con las gotodeclaraciones. Recomendaría estructurar su enumdiferente. Por ejemplo, si tu enumfue un campo de bits donde cada bit representaba una de las opciones, podría verse así:

[Flags]
public enum ExampleEnum {
    One = 0b0001,
    Two = 0b0010,
    Three = 0b0100
};

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:

if (myEnum.HasFlag(ExampleEnum.One))
{
    CallOne();
}
if (myEnum.HasFlag(ExampleEnum.Two))
{
    CallTwo();
}
if (myEnum.HasFlag(ExampleEnum.Three))
{
    CallThree();
}

Esto requiere el código que se configura myEnumpara 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:

[Flags]
public enum ExampleEnum {
    One = 0b0001,
    Two = 0b0010,
    Three = 0b0100,
    OneAndTwo = One | Two,
    OneAndThree = One | Three,
    TwoAndThree = Two | Three
};

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.

usuario1118321
fuente
55
Se parece tan ! Pero debe ser C # 7.0 o posterior, supongo.
user1118321
31
De todos modos, también se podría hacerlo de esta manera: 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 +.
Deduplicador
8
Puede usar Enum.HasFlag
IMil
13
El [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.
Joe Sewell
19
@ UKMonkey Estoy totalmente en desacuerdo. HasFlages 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 un bytetipo en lugar de declarar un enum.
BJ Myers
136

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.

alephzero
fuente
10
De hecho, hay muchas API bien diseñadas que tienen todo tipo de indicadores, opciones y una variedad de otros parámetros. Algunos pueden transmitirse, algunos tienen efectos directos y otros pueden ser ignorados, sin romper el SRP. De todos modos, la función podría incluso decodificar alguna entrada externa, ¿quién sabe? Además, la reducción ad absurdum a menudo conduce a resultados absurdos, especialmente si el punto de partida ni siquiera es tan sólido.
Deduplicador
99
Votó esta respuesta. Si solo quieres debatir sobre GOTO, esa es una pregunta diferente a la que hiciste. Según la evidencia presentada, tiene tres requisitos disjuntos, que no deben agregarse. Desde un punto de vista SE, afirmo que alephzero es la respuesta correcta.
8
@Deduplicator Una mirada a esta mezcla de algunas combinaciones de 1,2 y 3, pero no todas, muestra que esta no es una "API bien diseñada".
user949300
44
Tanto esto Las otras respuestas realmente no mejoran lo que hay en la pregunta. Este código necesita una reescritura. No hay nada de malo en escribir más funciones.
Pide disculpas y reinstala a Monica el
3
Me encanta esta respuesta, especialmente "Si 1024 es demasiado, 8 también es demasiado". Pero es esencialmente "usar polimorfismo", ¿no? Y mi respuesta (más débil) está recibiendo mucha basura por decir eso. ¿Puedo contratar a tu persona de relaciones públicas? :-)
user949300
29

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í:

switch (exampleValue) {
    case OneAndTwo: i += 3 break;
    case OneAndThree: i += 4 break;
    case Two: i += 2 break;
    case TwoAndThree: i += 5 break;
    case Three: i += 3 break;
    default: i++ break;
}

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

TED
fuente
2
@PerpetualJ - Bueno, ciertamente me alegra que hayas encontrado algo más limpio. Si esa es realmente su situación, podría ser interesante probarlo con los gotos (sin mayúsculas y minúsculas, solo bloques y gotos etiquetados) y ver cómo se comparan en la legibilidad. Dicho esto, la opción "goto" no solo tiene que ser más legible, sino lo suficientemente más legible para evitar argumentos e intentos de "reparación" de otros desarrolladores, por lo que parece probable que haya encontrado la mejor opción.
TED
44
No creo que sería "mejor usar gotos" si "es probable que un mantenimiento adicional agregue o complique los estados". ¿Realmente estás afirmando que el código de espagueti es más fácil de mantener que el código estructurado? Esto va en contra de ~ 40 años de práctica.
user949300
55
@ user949300 - No se practica contra la construcción de máquinas de estado, no lo hace. Simplemente puede leer mi comentario anterior sobre esta respuesta para un ejemplo del mundo real. Si intentas usar las caídas de mayúsculas y minúsculas en C, que imponen una estructura artificial (su orden lineal) en un algoritmo de máquina de estado que no tiene esa restricción inherentemente, te encontrarás con una complejidad geométricamente creciente cada vez que tengas que reorganizar todos tus pedidos para acomodar un nuevo estado. Si solo codifica estados y transiciones, como requiere el algoritmo, eso no sucede. Los nuevos estados son triviales para agregar.
TED
8
@ user949300 - Nuevamente, "~ 40 años de práctica" compilando compiladores muestra que los gotos son en realidad la mejor manera para partes de ese dominio problemático, por lo que si miras el código fuente del compilador, casi siempre encontrarás gotos.
TED
3
@ user949300 El código de espagueti no solo aparece inherentemente cuando se utilizan funciones o convenciones específicas. Puede aparecer en cualquier idioma, función o convención en cualquier momento. La causa es usar dicho lenguaje, función o convención en una aplicación para la que no fue diseñada y hacer que se doble hacia atrás para lograrlo. Siempre use la herramienta adecuada para el trabajo, y sí, a veces eso significa usar goto.
Abion47
26

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 :

if (One || OneAndTwo || OneAndThree)
  CallOne();
if (Two || OneAndTwo || TwoAndThree)
  CallTwo();
if (Three || OneAndThree || TwoAndThree)
  CallThree();

goto es probablemente mi 58a opción aquí ...

user949300
fuente
55
+1 por sugerir polimorfismo, aunque idealmente podría simplificar el código del cliente a solo "Call ();", usando tell don't ask - para alejar la lógica de decisión del cliente consumidor y llevarla al mecanismo de clase y la jerarquía.
Erik Eidt
12
Proclamar cualquier cosa que la mejor vista no haya visto es ciertamente muy valiente. Pero sin información adicional, sugiero un poco de escepticismo y mantener una mente abierta. ¿Quizás sufre una explosión combinatoria?
Deduplicador
Wow, creo que esta es la primera vez que me votan negativamente por sugerir polimorfismo. :-)
user949300
25
Algunas personas, cuando se enfrentan a un problema, dicen "Usaré el polimorfismo". Ahora tienen dos problemas, y el polimorfismo.
Lightness compite con Monica el
8
Realmente hice mi tesis de maestría sobre el uso del polimorfismo de tiempo de ejecución para deshacerme de los gotos en las máquinas de estado de los compiladores. Esto es bastante factible, pero desde entonces he descubierto en la práctica que no vale la pena el esfuerzo en la mayoría de los casos. Requiere una tonelada de código de configuración OO, todo lo cual, por supuesto, puede terminar con errores (y que esta respuesta omite).
TED
10

¿Por qué no esto?

public enum ExampleEnum {
    One = 0, // Why not?
    OneAndTwo,
    OneAndThree,
    Two,
    TwoAndThree,
    Three
}
int[] COUNTS = { 1, 3, 4, 2, 5, 3 }; // Whatever

int ComputeExampleValue(int i, ExampleEnum exampleValue) {
    return i + COUNTS[(int)exampleValue];
}

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 #.

Laurent Grégoire
fuente
3
Si. Otro ejemplo de reemplazo de código con datos (que generalmente es deseable, por velocidad, estructura y facilidad de mantenimiento).
Peter - Restablece a Mónica el
2
Esa es una excelente idea para la edición más reciente de la pregunta, sin duda. Y se puede utilizar para pasar de la primera enumeración (un rango denso de valores) a los indicadores de acciones más o menos independientes que deben realizarse en general.
Deduplicador
No tengo acceso a un compilador de C #, pero tal vez el código se pueden hacer más seguro el uso de este tipo de código: int[ExempleEnum.length] COUNTS = { 1, 3, 4, 2, 5, 3 };?
Laurent Grégoire
7

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 gotodeclaración. Simplemente no tienes que lidiar con eso.

int ComputeExampleValue(int i, ExampleEnum exampleValue) {
    switch (exampleValue) {
        case One: return i + 1;
        case OneAndTwo: return ComputeExampleValue(i + 2, ExampleEnum.One);
        case OneAndThree: return ComputeExampleValue(i + 3, ExampleEnum.One);
        case Two: return i + 2;
        case TwoAndThree: return ComputeExampleValue(i + 2, ExampleEnum.Three);
        case Three: return i + 3;
   }
}
Philipp
fuente
¡Esa es una solución única! +1 No creo que deba hacerlo de esta manera, ¡pero es genial para futuros lectores!
PerpetualJ
2

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:

// All possible combinations of One - Eight.
public enum ExampleEnum {
    One,
    Two,
    TwoOne,
    Three,
    ThreeOne,
    ThreeTwo,
    ThreeOneTwo
}

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:

public interface IExample
{
  void Draw();
}

Entonces podemos implementar esto como una clase abstracta:

public abstract class ExampleClassBase : IExample
{
  public abstract void Draw();
  // other common functionality defined here
}

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:

public class DrawOne
{
  public void Draw()
  {
    // Drawing logic here
  }
}

public class DrawTwo
{
  public void Draw()
  {
    // Drawing two logic here
  }
}

public class DrawThree
{
  public void Draw()
  {
    // Drawing three logic here
  }
}

Y ahora tenemos tres clases separadas que pueden estar compuestas para proporcionar la lógica para las otras clases.

public class One : ExampleClassBase
{
  private DrawOne drawOne;

  public One(DrawOne drawOne)
  {
    this.drawOne = drawOne;
  }

  public void Draw()
  {
    this.drawOne.Draw();
  }
}

public class TwoOne : ExampleClassBase
{
  private DrawOne drawOne;
  private DrawTwo drawTwo;

  public One(DrawOne drawOne, DrawTwo drawTwo)
  {
    this.drawOne = drawOne;
    this.drawTwo = drawTwo;
  }

  public void Draw()
  {
    this.drawOne.Draw();
    this.drawTwo.Draw();
  }
}

// the other six classes here

Este enfoque es mucho más detallado. Pero tiene ventajas.

Considere la siguiente clase, que contiene un error:

public class ThreeTwoOne : ExampleClassBase
{
  private DrawOne drawOne;
  private DrawTwo drawTwo;
  private DrawThree drawThree;

  public One(DrawOne drawOne, DrawTwo drawTwo, DrawThree drawThree)
  {
    this.drawOne = drawOne;
    this.drawTwo = drawTwo;
    this.drawThree = drawThree;
  }

  public void Draw()
  {
    this.drawOne.Draw();
    this.drawTwo.Draw();
  }
}

¿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:

  • Cada una de las ocho opciones presentadas requiere una clase separada
  • Esto usará más memoria
  • Esto hará que tu código sea superficialmente más grande
  • A veces, este enfoque no es posible, aunque puede haber alguna variación.

Ventajas de este enfoque:

  • Cada una de estas clases es completamente comprobable; porque
  • La complejidad ciclomática de los métodos de dibujo es baja (y en teoría podría burlarme de las clases DrawOne, DrawTwo o DrawThree si es necesario)
  • Los métodos de dibujo son comprensibles: un desarrollador no tiene que atar su cerebro en nudos para resolver lo que hace el método
  • Los errores son fáciles de detectar y difíciles de escribir.
  • Las clases se componen en más clases de alto nivel, lo que significa que definir una clase ThreeThreeThree es fácil de hacer.

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.

Stephen
fuente
Esta es una respuesta muy bien escrita y estoy totalmente de acuerdo con el enfoque. En mi caso particular, algo tan detallado sería excesivo hasta el grado infinito considerando el código trivial detrás de cada uno. Para ponerlo en perspectiva, imagine que el código simplemente realiza una Console.WriteLine (n). Eso es prácticamente el equivalente de lo que estaba haciendo el código con el que estaba trabajando. En el 90% de todos los demás casos, su solución es definitivamente la mejor respuesta al seguir OOP.
PerpetualJ
Sí, claro, este enfoque no es útil en todas las situaciones. Quería ponerlo aquí porque es común que los desarrolladores se obsesionen con un enfoque particular para resolver un problema y, a veces, vale la pena retroceder y buscar alternativas.
Stephen
Estoy completamente de acuerdo con eso, en realidad es la razón por la que terminé aquí porque estaba tratando de resolver un problema de escalabilidad con algo que otro desarrollador escribió por costumbre.
PerpetualJ
0

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

switch(exampleValue)
{
    case One:
        i++;
        break;
    case Two:
        i += 2;
        break;
    case OneAndTwo:
    case Three:
        i+=3;
        break;
    case OneAndThree:
        i+=4;
        break;
    case TwoAndThree:
        i+=5;
        break;
}

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"

CaptianObvious
fuente
3
Diría que esta es una gran idea para alguien que se enfrenta a un asunto más simple, así que gracias por publicarlo. En mi caso no es tan simple.
PerpetualJ
Además, hoy no tenemos exactamente los problemas que tenía Edgar al momento de escribir su artículo; La mayoría de las personas hoy en día ni siquiera tienen una razón válida para odiar el goto, aparte de que hace que el código sea difícil de leer. Bueno, con toda honestidad, si se abusa de él, entonces sí, de lo contrario está atrapado en el método de contención, por lo que realmente no puede hacer código de espagueti. ¿Por qué gastar esfuerzo para solucionar una característica de un idioma? Diría que goto es arcaico y que debería pensar en otras soluciones en el 99% de los casos de uso; pero si lo necesitas, para eso está ahí.
PerpetualJ
Esto no escalará bien cuando hay muchos booleanos.
user949300
0

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 ifdeclaraciones es preferible a a switch, 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 gotos en la switchdeclaración. Prefiero ver algo como esto:

switch (enumVal) {
    case ThreeOneTwo: DrawThree; DrawTwo; DrawOne; break;
    case ThreeTwo:    DrawThree; DrawTwo; break;
    case ThreeOne:    DrawThree; DrawOne; break;
    case TwoOne:      DrawTwo; DrawOne; break;
    case Two:         DrawTwo; break;
    default:          DrawOne; break;
}

Esto no es perfecto, pero creo que es mejor así que con el gotos. 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 de gotoreducir la duplicación de código.

David K
fuente
0

No estoy seguro de que alguien realmente tenga una razón para odiar la palabra clave goto en estos días. Definitivamente es arcaico y no es necesario en el 99% de los casos de uso, pero es una característica del lenguaje por una razón.

La razón para odiar la gotopalabra clave es código como

if (someCondition) {
    goto label;
}

string message = "Hello World!";

label:
Console.WriteLine(message);

¡Uy! Eso claramente no va a funcionar. La messagevariable no está definida en esa ruta de código. Entonces C # no pasará eso. Pero puede ser implícito. Considerar

object.setMessage("Hello World!");

label:
object.display();

Y supongamos que displayluego tiene la WriteLinedeclaración.

Este tipo de error puede ser difícil de encontrar porque gotooscurece 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 de message.

El lenguaje puede ayudar a solucionar esto al limitar cómo gotose puede usar, solo descendiendo de bloques. Pero el C # gotono está limitado así. Puede saltar sobre el código. Además, si va a limitar goto, es mejor cambiar el nombre. Otros idiomas se usan breakpara descender de bloques, ya sea con un número (de bloques para salir) o una etiqueta (en uno de los bloques).

El concepto de gotoes 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 # gotodentro de una switchdeclaración para saltar de un caso a otro, es razonablemente inofensivo. Cada caso ya es un punto de entrada. Todavía creo que llamarlo gotoes una tontería en esa situación, ya que combina este uso inofensivo gotocon formas más peligrosas. Preferiría que usaran algo así continuepara eso. Pero por alguna razón, se olvidaron de preguntarme antes de escribir el idioma.

mdfst13
fuente
2
En el 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 #.
PerpetualJ
0

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.

alexis
fuente
-7

Use un bucle y la diferencia entre romper y continuar.

do {
  switch (exampleValue) {
    case OneAndTwo: i += 2; break;
    case OneAndThree: i += 3; break;
    case Two: i += 2; continue;
    case TwoAndThree: i += 2;
    // dropthrough
    case Three: i += 3; continue;
    default: break;
  }
  i++;
} while(0);
QuentinUK
fuente