¿Por qué la declaración de cambio fue diseñada para necesitar un descanso?


139

Dada una simple declaración de cambio

switch (int)
{
    case 1 :
    {
        printf("1\n");
        break;
    }

    case 2 : 
    {
        printf("2\n");
    }

    case 3 : 
    {
        printf("3\n");
    }
}

La ausencia de una declaración de interrupción en el caso 2 implica que la ejecución continuará dentro del código para el caso 3. Esto no es un accidente; Fue diseñado de esa manera. ¿Por qué se tomaron estas decisiones? ¿Qué beneficio ofrece esto en comparación con tener una ruptura semántica automática para los bloques? ¿Cuál fue la razón?

Respuestas:


150

Muchas respuestas parecen centrarse en la capacidad de fracasar como la razón para requerir la breakdeclaración.

Creo que fue simplemente un error, debido en gran medida a que cuando se diseñó C no había tanta experiencia sobre cómo se usarían estas construcciones.

Peter Van der Linden expone el caso en su libro "Expert C Programming":

Analizamos las fuentes del compilador Sun C para ver con qué frecuencia se usaba la falla predeterminada. El front-end del compilador Sun ANSI C tiene 244 instrucciones de cambio, cada una de las cuales tiene un promedio de siete casos. La caída ocurre en solo el 3% de todos estos casos.

En otras palabras, el comportamiento normal del interruptor es incorrecto el 97% del tiempo. No es solo en un compilador, por el contrario, cuando se utilizó la falla en este análisis, a menudo fue para situaciones que ocurren con más frecuencia en un compilador que en otro software, por ejemplo, cuando compila operadores que pueden tener uno o dos operandos :

switch (operator->num_of_operands) {
    case 2: process_operand( operator->operand_2);
              /* FALLTHRU */

    case 1: process_operand( operator->operand_1);
    break;
}

La omisión de casos es tan ampliamente reconocida como un defecto que incluso hay una convención de comentarios especial, que se muestra arriba, que le dice a la pelusa "este es realmente uno de esos 3% de los casos en que se deseaba la falla".

Creo que fue una buena idea que C # requiriera una declaración de salto explícita al final de cada bloque de casos (al tiempo que permite que se apilen varias etiquetas de casos, siempre y cuando solo haya un bloque de declaraciones). En C # todavía puede hacer que un caso se caiga a otro, solo tiene que hacer que la caída sea explícita saltando al siguiente caso usando a goto.

Es una lástima que Java no aprovechó la oportunidad para romper con la semántica de C.


De hecho, creo que fueron por la simplicidad de implementación. Algunos otros idiomas admitieron casos más sofisticados (rangos, valores múltiples, cadenas ...) a costa, tal vez, de la eficiencia.
PhiLho

Java probablemente no quería romper los hábitos y difundir la confusión. Para un comportamiento diferente, tendrían que usar semánticas diferentes. Los diseñadores de Java perdieron una serie de oportunidades para romper con C, de todos modos.
PhiLho

@PhiLho - Creo que probablemente estés más cerca de la verdad con "simplicidad de implementación".
Michael Burr el

Si está utilizando saltos explícitos a través de GOTO, ¿no es tan productivo usar solo una serie de declaraciones IF?
DevinB

3
incluso Pascal implementa su interruptor sin descanso. ¿Cómo podría C compilador-codificador no pensar en ello @@
Chan Le

30

En muchos sentidos, c es solo una interfaz limpia para modismos de ensamblaje estándar. Al escribir el control de flujo impulsado por la tabla de salto, el programador tiene la opción de caer o saltar de la "estructura de control", y un salto requiere una instrucción explícita.

Entonces, c hace lo mismo ...


1
Sé que mucha gente dice eso, pero creo que no es la imagen completa; C también es a menudo una interfaz fea para ensamblar antipatrones. 1. Feo: declaraciones en lugar de expresiones. "La declaración refleja el uso". Glorificado copiar y pegar como el mecanismo para la modularidad y portabilidad. Tipo de sistema de manera demasiado complicado; alienta a los insectos. NULL. (Continúa en el siguiente comentario.)
Harrison

2. Antipatterns: no proporciona uniones etiquetadas "limpias", uno de los dos modismos fundamentales de ensamblaje para la representación de datos. (Knuth, vol. 1, cap. 2) En cambio, "uniones sin etiquetar", un truco no idiomático. Esta elección ha obstaculizado la forma en que las personas piensan en los datos durante décadas. Las NULcadenas terminadas son la peor idea de la historia.
Harrison

@HarrisonKlaperman: Ningún método para almacenar cadenas es perfecto. La gran mayoría de los problemas asociados con las cadenas terminadas en nulo no ocurrirían si las rutinas que aceptaran cadenas también aceptaran un parámetro de longitud máxima, y ​​ocurrirían con rutinas que almacenan cadenas marcadas en longitud en buffers de tamaño fijo sin aceptar un parámetro de tamaño de buffer . El diseño de la declaración de cambio donde los casos son simplemente etiquetas puede parecer extraño para los ojos modernos, pero no es peor que el diseño del DObucle Fortran .
supercat

Si decido escribir una tabla de salto en conjunto, tomaré el valor del caso y lo convertiré mágicamente en el subíndice de la tabla de salto, saltaré a esa ubicación y ejecutaré el código. Después de eso no saltaré al siguiente caso. Saltaré a una dirección uniforme que es la salida de todos los casos. La idea de que saltaría o caería en el cuerpo del próximo caso es una tontería. No hay caso de uso para ese patrón.
EvilTeach

2
Si bien hoy en día las personas están más acostumbradas a limpiar y proteger los idos y las características del lenguaje que evitan que uno se dispare al pie, esto es una reminiscencia de un área donde los bytes habían sido caros (C comenzó antes de 1970). Si su código necesita caber dentro de 1024 bytes, experimentará una gran medida para reutilizar fragmentos de código. Reutilizar el código comenzando en diferentes puntos de entrada que comparten el mismo extremo es un mecanismo para lograr esto.
rpy

23

Para implementar el dispositivo de Duff, obviamente:

dsend(to, from, count)
char *to, *from;
int count;
{
    int n = (count + 7) / 8;
    switch (count % 8) {
    case 0: do { *to = *from++;
    case 7:      *to = *from++;
    case 6:      *to = *from++;
    case 5:      *to = *from++;
    case 4:      *to = *from++;
    case 3:      *to = *from++;
    case 2:      *to = *from++;
    case 1:      *to = *from++;
               } while (--n > 0);
    }
}

3
Me encanta el dispositivo de Duff. Muy elegante y malvado rápido.
dicroce

77
sí, pero ¿tiene que aparecer cada vez que hay una declaración de cambio en SO?
billjamesdev

Te perdiste dos llaves de cierre ;-).
Toon Krijthe

18

Si los casos fueron diseñados para romperse implícitamente, entonces no podrías haber sufrido un fracaso.

case 0:
case 1:
case 2:
    // all do the same thing.
    break;
case 3:
case 4:
    // do something different.
    break;
default:
    // something else entirely.

Si el interruptor se diseñó para explotar implícitamente después de cada caso, no tendría elección. La estructura de la caja del interruptor se diseñó de la manera que debe ser más flexible.


12
Puede crear una imagen de un interruptor que se rompe implícitamente, pero que tiene una palabra clave "fallthrough". Torpe, pero viable.
dmckee --- ex gatito moderador el

¿Cómo sería eso mejor? Me imagino que una declaración de caso funciona de esta manera con más frecuencia que "bloque de código por caso" ... ese es un bloque if / then / else.
billjamesdev

Agregaría que en la pregunta, los recintos de alcance {} en cada bloque de caso se suman a la confusión ya que parece el estilo de una declaración "while".
Matthew Smith el

1
@Bill: Creo que sería peor, pero abordaría la queja presentada por Mike B: ese fracaso (aparte de varios casos iguales) es un evento raro y no debería ser el comportamiento predeterminado.
dmckee --- ex gatito moderador el

1
Dejé los corchetes por brevedad, con múltiples declaraciones que deberían estar allí. Estoy de acuerdo en que la falla debe usarse solo cuando los casos son exactamente iguales. Es confuso como el infierno cuando los casos se basan en casos anteriores usando fallthrough.
Bill the Lizard

15

Las declaraciones de caso en las declaraciones de un interruptor son simplemente etiquetas.

Al encender un valor, la instrucción switch hace esencialmente un Goto a la etiqueta con el valor coincidente.

Esto significa que el descanso es necesario para evitar pasar al código bajo la siguiente etiqueta.

En cuanto a la razón por la que se implementó de esta manera, la naturaleza de caída de una declaración de cambio puede ser útil en algunos escenarios. Por ejemplo:

case optionA:
    // optionA needs to do its own thing, and also B's thing.
    // Fall-through to optionB afterwards.
    // Its behaviour is a superset of B's.
case optionB:
    // optionB needs to do its own thing
    // Its behaviour is a subset of A's.
    break;
case optionC:
    // optionC is quite independent so it does its own thing.
    break;

2
Hay dos grandes problemas: 1) Olvidar breakdónde se necesita. 2) Si se cambia el orden de las declaraciones de caso, el error puede resultar en la ejecución del caso incorrecto. Por lo tanto, encuentro que el manejo de C # es mucho mejor (explícito goto casepara fallthrough, excepto para las etiquetas de mayúsculas y minúsculas vacías).
Daniel Rose

@DanielRose: 1) también hay formas de olvidar breaken C #: la más simple es cuando no quieres casehacer nada más que olvidar agregar un break(tal vez te has quedado tan envuelto en el comentario explicativo o te han llamado por alguna otra tarea): la ejecución solo caerá a través de lo casesiguiente. 2) alentar goto casees alentar la codificación de "espagueti" no estructurada: puede terminar con ciclos accidentales ( case A: ... goto case B; case B: ... ; goto case A;), especialmente cuando los casos están separados en el archivo y son difíciles de asimilar en combinación. En C ++, la caída está localizada.
Tony Delroy

8

Para permitir cosas como:

switch(foo) {
case 1:
    /* stuff for case 1 only */
    if (0) {
case 2:
    /* stuff for case 2 only */
    }
    /* stuff for cases 1 and 2 */
case 3:
    /* stuff for cases 1, 2, and 3 */
}

Piense en la casepalabra clave como una gotoetiqueta y es mucho más natural.


8
Eso if(0)al final del primer caso es malo, y solo debe usarse si el objetivo es ofuscar el código.
Daniel Rose

11
Toda esta respuesta fue un ejercicio de ser malvado, creo. :-)
R .. GitHub DEJA DE AYUDAR AL HIELO

3

Elimina la duplicación de código cuando varios casos necesitan ejecutar el mismo código (o el mismo código en secuencia).

Dado que en el nivel del lenguaje ensamblador no le importa si se divide entre cada uno o no, no hay sobrecarga para los casos de caída, de todos modos, ¿por qué no permitirlos, ya que ofrecen ventajas significativas en ciertos casos?


2

Me encontré con un caso de asignación de valores en vectores a estructuras: tenía que hacerse de tal manera que si el vector de datos fuera más corto que el número de miembros de datos en la estructura, el resto de los miembros permanecerían en su valor predeterminado En ese caso, omitir breakfue bastante útil.

switch (nShorts)
{
case 4: frame.leadV1    = shortArray[3];
case 3: frame.leadIII   = shortArray[2];
case 2: frame.leadII    = shortArray[1];
case 1: frame.leadI     = shortArray[0]; break;
default: TS_ASSERT(false);
}

0

Como muchos han especificado aquí, es permitir que un solo bloque de código funcione para múltiples casos. Esto debería ser una ocurrencia más común para sus declaraciones de cambio que el "bloque de código por caso" que especifique en su ejemplo.

Si tiene un bloque de código por caso sin fall-through, tal vez debería considerar usar un bloque if-elseif-else, ya que eso parecería más apropiado.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.