Para agregar a las respuestas aquí, creo que vale la pena considerar la pregunta opuesta junto con esto, a saber. ¿Por qué C permitió la caída en primer lugar?
Cualquier lenguaje de programación, por supuesto, tiene dos objetivos:
- Proporcionar instrucciones a la computadora.
- Deje un registro de las intenciones del programador.
La creación de cualquier lenguaje de programación es, por lo tanto, un equilibrio entre la mejor manera de servir estos dos objetivos. Por un lado, cuanto más fácil sea convertir las instrucciones de la computadora (ya sean códigos de máquina, bytecode como IL, o las instrucciones se interpretan en la ejecución), más capaz será ese proceso de compilación o interpretación para ser eficiente, confiable y compacto en salida. Llevado a su extremo, este objetivo da como resultado que solo escribamos en ensamblador, IL o incluso códigos de operación sin procesar, porque la compilación más fácil es donde no hay compilación en absoluto.
Por el contrario, cuanto más expresa el lenguaje la intención del programador, en lugar de los medios utilizados para ese fin, más comprensible es el programa tanto al escribir como durante el mantenimiento.
Ahora, switch
siempre podría haberse compilado convirtiéndolo en la cadena de if-else
bloques equivalente o similar, pero fue diseñado para permitir la compilación en un patrón de ensamblaje común particular donde uno toma un valor, calcula un desplazamiento de él (ya sea buscando una tabla indexado por un hash perfecto del valor, o por aritmética real en el valor *). Vale la pena señalar en este punto que hoy, la compilación de C # a veces se convertirá switch
en el equivalenteif-else
, y a veces utilizará un enfoque de salto basado en hash (y de la misma manera con C, C ++ y otros lenguajes con sintaxis comparable).
En este caso, hay dos buenas razones para permitir la caída:
De todos modos, sucede de forma natural: si construye una tabla de salto en un conjunto de instrucciones, y uno de los lotes de instrucciones anteriores no contiene algún tipo de salto o retorno, la ejecución progresará naturalmente al siguiente lote. Permitir la caída era lo que "sucedería" si giraba elswitch
C en una tabla de salto, utilizando el código de máquina.
Los codificadores que escribieron en conjunto ya estaban acostumbrados al equivalente: al escribir una tabla de salto a mano en conjunto, tendrían que considerar si un bloque de código dado terminaría con un retorno, un salto fuera de la tabla, o simplemente continuaría a la siguiente cuadra. Como tal, hacer que el codificador agregue un explícito break
cuando sea necesario también fue "natural" para el codificador.
En ese momento, por lo tanto, fue un intento razonable de equilibrar los dos objetivos de un lenguaje de computadora en lo que se refiere tanto al código de máquina producido como a la expresividad del código fuente.
Sin embargo, cuatro décadas después, las cosas no son exactamente iguales, por algunas razones:
- Los codificadores en C hoy pueden tener poca o ninguna experiencia en ensamblaje. Los codificadores en muchos otros lenguajes de estilo C son aún menos propensos (¡especialmente Javascript!). Cualquier concepto de "a lo que las personas están acostumbradas desde la asamblea" ya no es relevante.
- Las mejoras en las optimizaciones significan que la probabilidad de que
switch
cualquiera se convierta enif-else
porque se consideró que el enfoque es más eficiente, o que se convierta en una variante particularmente esotérica del enfoque de la tabla de salto es mayor. El mapeo entre los enfoques de nivel superior e inferior no es tan fuerte como lo era antes.
- La experiencia ha demostrado que la caída tiende a ser el caso minoritario más que la norma (un estudio del compilador de Sun encontró que el 3% de los
switch
bloques usaron una caída distinta de las etiquetas múltiples en el mismo bloque, y se pensó que el uso- caso aquí significaba que este 3% era, de hecho, mucho más alto de lo normal). Entonces, el lenguaje estudiado hace que lo inusual se atienda más fácilmente que lo común.
- La experiencia ha demostrado que la falla tiende a ser la fuente de problemas, tanto en los casos en que se realiza accidentalmente, como también en los casos en que alguien que mantiene el código pasa por alto la falla correcta. Esta última es una adición sutil a los errores asociados con la falla, porque incluso si su código está perfectamente libre de fallas, su falla puede causar problemas.
En relación con esos dos últimos puntos, considere la siguiente cita de la edición actual de K&R:
Caer de un caso a otro no es robusto, siendo propenso a la desintegración cuando se modifica el programa. Con la excepción de múltiples etiquetas para un solo cálculo, los fallos deben usarse con moderación y comentarse.
Como una buena forma, ponga un descanso después del último caso (el predeterminado aquí) aunque sea lógicamente innecesario. Algún día, cuando se agregue otro caso al final, este poco de programación defensiva lo salvará.
Entonces, desde la boca del caballo, la caída en C es problemática. Se considera una buena práctica documentar siempre las fallas con comentarios, lo cual es una aplicación del principio general de que uno debe documentar dónde se hace algo inusual, porque eso es lo que hará que el examen posterior del código y / o haga que su código se vea así tiene un error de novato cuando de hecho es correcto.
Y cuando lo piensas, codifica así:
switch(x)
{
case 1:
foo();
/* FALLTHRU */
case 2:
bar();
break;
}
Es agregando algo para hacer que la caída sea explícita en el código, simplemente no es algo que el compilador pueda detectar (o cuya ausencia se pueda detectar).
Como tal, el hecho de que on tiene que ser explícito con fall-through en C # no agrega ninguna penalización a las personas que escribieron bien en otros lenguajes de estilo C de todos modos, ya que ya serían explícitos en sus fallos. †
Finalmente, el uso de goto
here ya es una norma de C y otros lenguajes similares:
switch(x)
{
case 0:
case 1:
case 2:
foo();
goto below_six;
case 3:
bar();
goto below_six;
case 4:
baz();
/* FALLTHRU */
case 5:
below_six:
qux();
break;
default:
quux();
}
En este tipo de caso en el que queremos que se incluya un bloque en el código ejecutado para un valor distinto de aquel que trae uno al bloque anterior, entonces ya tenemos que usarlo goto
. (Por supuesto, hay medios y formas de evitar esto con diferentes condicionales, pero eso es cierto en casi todo lo relacionado con esta pregunta). Como tal, C # se basó en la forma ya normal de lidiar con una situación en la que queremos golpear más de un bloque de código en un switch
, y simplemente lo generalizó para cubrir también la falla. También hizo que ambos casos fueran más convenientes y autodocumentados, ya que tenemos que agregar una nueva etiqueta en C pero podemos usarla case
como etiqueta en C #. En C # podemos deshacernos de la below_six
etiqueta y usarlagoto case 5
que es más claro en cuanto a lo que estamos haciendo. (También tendríamos que agregarbreak
para el default
, que dejé fuera solo para hacer que el código C anterior claramente no sea el código C #).
En resumen, por lo tanto:
- C # ya no se relaciona con la salida no optimizada del compilador tan directamente como lo hizo el código C hace 40 años (ni tampoco lo hace C en estos días), lo que hace que una de las inspiraciones de la caída sea irrelevante.
- C # sigue siendo compatible con C no solo por tener implícito
break
, para que los que estén familiarizados con lenguajes más sencillos aprendan el idioma y porten más fácilmente.
- C # elimina una posible fuente de errores o código mal entendido que ha sido bien documentado como causante de problemas durante las últimas cuatro décadas.
- C # hace que el compilador ejecute las mejores prácticas existentes con C (caída del documento).
- C # hace que el caso inusual sea el que tiene un código más explícito, el caso habitual es el que tiene el código que solo se escribe automáticamente.
- C # usa el mismo
goto
enfoque basado en la base para golpear el mismo bloque desde diferentes case
etiquetas como se usa en C. Simplemente lo generaliza a otros casos.
- C # hace que ese
goto
enfoque basado en la base sea más conveniente y más claro que en C, al permitir que las case
declaraciones actúen como etiquetas.
En definitiva, una decisión de diseño bastante razonable
* Algunas formas de BASIC le permitirían a uno hacer cosas similares, GOTO (x AND 7) * 50 + 240
mientras que frágil y, por lo tanto, un caso particularmente persuasivo para la prohibición goto
, sirve para mostrar un equivalente en un idioma superior del tipo de forma en que el código de nivel inferior puede hacer un salto basado en aritmética sobre un valor, que es mucho más razonable cuando es el resultado de la compilación en lugar de algo que debe mantenerse manualmente. Las implementaciones del dispositivo de Duff en particular se prestan bien al código de máquina equivalente o IL porque cada bloque de instrucciones a menudo tendrá la misma longitud sin necesidad de agregar nop
rellenos.
† El dispositivo de Duff vuelve a aparecer aquí, como una excepción razonable. El hecho de que con eso y patrones similares haya una repetición de operaciones sirve para hacer que el uso de la caída sea relativamente claro, incluso sin un comentario explícito al respecto.