Publicaciones
Examinando el caso de las sentencias Switch
6 de diciembre de 2015 • 10 min de lectura
Durante casi 50 años, la sentencia switch (también conocida como sentencia case) ha sido una parte integral de la programación. En años recientes, sin embargo, algunos afirman que la sentencia switch ha dejado de ser útil. Otros van aún más lejos etiquetando la sentencia switch como un code-smell.
En 1952, Stephen Kleene concibió la sentencia switch en su artículo, Introduction to Metamathematics. La primera implementación notable fue en ALGOL 58 en 1958. Posteriormente, la sentencia switch fue incluida en el influyente lenguaje de programación C, que, como sabemos, ha influido en la mayoría de los lenguajes de programación modernos.
Avanzando hasta el presente, prácticamente todos los lenguajes tienen una sentencia switch. Sin embargo, algunos lenguajes han omitido la sentencia switch. El más notable es Smalltalk.
Esto despertó mi curiosidad, ¿por qué fue excluida la sentencia switch de Smalltalk?
Andy Bower, uno de los creadores/proponentes detrás de Dolphin Smalltalk compartió sus pensamientos sobre por qué Smalltalk excluyó la sentencia switch:
Cuando llegué por primera vez a Smalltalk desde C++, no podía entender cómo un lenguaje supuestamente completo no soportaba una construcción switch/case. Después de todo, cuando me mudé por primera vez a la “programación estructurada” desde BASIC pensé que switch era una de las mejores cosas desde el pan de molde. Sin embargo, porque Smalltalk no soportaba un switch tuve que buscar y entender cómo superar esta deficiencia. La respuesta correcta es, por supuesto, usar polimorfismo y hacer que los objetos mismos se envíen al código correcto. Entonces me di cuenta de que no era una “deficiencia” en absoluto, sino que Smalltalk me estaba forzando a un diseño OOP mucho más granular de lo que había llegado a acostumbrarme en C++. Si hubiera habido una sentencia switch disponible me habría tomado mucho más tiempo aprender esto o, peor aún, podría seguir programando en estilo pseudo-objeto C++/Java en Smalltalk.
Yo argumentaría que en OOP normal no hay una necesidad real de una sentencia switch. A veces, cuando se interfaz con un mundo no-OOP (como recibir y enviar mensajes WM_XXXX de Windows que no son objetos sino solo enteros), entonces una sentencia switch sería útil. En estas situaciones, hay alternativas (como enviar desde un Dictionary) y el número de veces que surgen no justifica la inclusión de sintaxis adicional.
¿Tenía razón Andy? ¿Estamos mejor sin la sentencia switch? ¿Se beneficiarían otros lenguajes también de excluir la sentencia switch?
Para arrojar luz sobre esta pregunta, he reunido una comparación entre una sentencia switch, un diccionario, y polimorfismo. Llamémoslo un enfrentamiento. ¡Que gane la mejor implementación!
Cada implementación tiene un método que toma un parámetro, un entero, y devuelve una cadena. Usaremos complejidad ciclomática e índice de mantenibilidad para examinar cada implementación. Luego tomaremos una vista holística de las tres implementaciones.
El código.
Sentencia Switch
| Índice de Mantenibilidad | 72 |
|---|---|
| Complejidad Ciclomática | 6 |
public class SwitchWithFourCases
{
public string SwitchStatment(int color)
{
var colorString = "Red";
switch (color)
{
case 1:
colorString = "Green";
break;
case 2:
colorString = "Blue";
break;
case 3:
colorString = "Violet";
break;
case 4:
colorString = "Orange";
break;
}
return colorString;
}
}
Diccionario
| Índice de Mantenibilidad | 73 |
|---|---|
| Complejidad Ciclomática | 3 |
public class DictionaryWithFourItems
{
public string Dictionary(int color)
{
var colorString = "Red";
var colors = new Dictionary<int, string> {{1, "Green"}, {2, "Blue"}, {3, "Violet"}, {4, "Orange"}};
var containsKey = colors.ContainsKey(color);
if (containsKey)
{
colorString = colors[color];
}
return colorString;
}
}
Polimorfismo
| Índice de Mantenibilidad Total | 94 |
|---|---|
| Complejidad Ciclomática Total | 15 |
Interfaz
| Índice de Mantenibilidad | 100 |
|---|---|
| Complejidad Ciclomática | 1 |
public interface IColor
{
string ColorName { get; }
}
Factory
| Índice de Mantenibilidad | 76 |
|---|---|
| Complejidad Ciclomática | 4 |
public class ColorFactory
{
public string GetColor(int color)
{
IColor defaultColor = new RedColor();
var colors = GetColors();
var containsKey = colors.ContainsKey(color);
if (containsKey)
{
var c = colors[color];
return c.ColorName;
}
return defaultColor.ColorName;
}
private static IDictionary<int, IColor> GetColors()
{
return new Dictionary<int, IColor>
{
{1, new GreenColor()},
{2, new BlueColor()},
{3, new VioletColor()},
{4, new OrangeColor()},
{5, new MagentaColor()}
};
}
}
Implementación
| Índice de Mantenibilidad | 97 |
|---|---|
| Complejidad Ciclomática | 2 |
public class BlueColor : IColor
{
public string ColorName => "Blue";
}
public class RedColor : IColor
{
public string ColorName => "Red";
}
public class GreenColor : IColor
{
public string ColorName => "Green";
}
public class MagentaColor : IColor
{
public string ColorName => "Magenta";
}
public class VioletColor : IColor
{
public string ColorName => "Violet";
}
Los Resultados
Antes de profundizar en los resultados, definamos Complejidad Ciclomática e Índice de Mantenibilidad:
- Complejidad Ciclomática es la medida de ramificación lógica. Cuanto menor sea el número, mejor.
- Índice de Mantenibilidad mide la mantenibilidad del código. Está en una escala entre 0 y 100. Cuanto mayor sea el número, mejor.
| Complejidad Ciclomática | Índice de Mantenibilidad | |
|---|---|---|
| Sentencia Switch | 6 | 72 |
| Diccionario | 3 | 73 |
| Polimorfismo | 15 | 94 |
Examinaremos primero la complejidad ciclomática.
Los resultados para la complejidad ciclomática son directos. La implementación del diccionario es la más simple. ¿Significa esto que es la mejor solución? No, como veremos cuando evaluemos el índice de mantenibilidad.
La mayoría pensaría como yo lo hice, que la implementación con la menor complejidad ciclomática es la más mantenible — ¿cómo podría ser de otra manera?
En nuestro escenario, la implementación con la menor complejidad ciclomática no es la más mantenible. De hecho, en nuestro escenario, es lo opuesto. ¡La implementación más compleja es la más mantenible! ¡Mente volada!
Si recuerdas, cuanto mayor sea la puntuación del índice de mantenibilidad, mejor. Yendo al grano, el polimorfismo tiene la mejor puntuación del índice de mantenibilidad — pero también tiene la mayor complejidad ciclomática. ¿Qué pasa? Eso no parece correcto.
¿Por qué la implementación más compleja es la más mantenible? Para responder esto, debemos entender el índice de mantenibilidad.
El índice de mantenibilidad consta de 4 métricas: complejidad ciclomática, líneas de código, el número de comentarios y el volumen de Halstead. Las primeras tres métricas son relativamente bien conocidas, pero la última, el Volumen de Halstead, es relativamente desconocida. Como la complejidad ciclomática, el Volumen de Halstead intenta medir objetivamente la complejidad del código.
En términos simples, el Volumen de Halstead mide el número de partes móviles (variables, llamadas del sistema, aritmética, construcciones de código, etc.) en el código. Cuanto mayor sea el número de partes móviles, más complejidad. Cuanto menor sea el número de partes móviles, menor será la complejidad. Esto explica por qué la implementación polimórfica obtiene una puntuación alta en el índice de mantenibilidad; las clases tienen pocas o ninguna parte móvil. Otra forma de ver el Volumen de Halstead es que mide la densidad de “partes móviles”.
¿Qué es el software, si no es para cambiar? Para reflejar el mundo real, estamos introduciendo cambio. He añadido un nuevo color a cada implementación.
A continuación se muestran los resultados revisados.
| Complejidad Ciclomática | Índice de Mantenibilidad | |
|---|---|---|
| Sentencia Switch | 7 | 70 |
| Diccionario | 3 | 73 |
| Polimorfismo | 17 | 95 |
Los enfoques de sentencia switch y polimórficos ambos aumentaron en complejidad ciclomática en una unidad, pero interesantemente, el diccionario no aumentó. Al principio estaba desconcertado por esto, pero luego me di cuenta de que el diccionario considera los colores como datos y las otras dos implementaciones tratan los colores como código.Voy a ir al grano.
Dirigiendo nuestra atención al índice de mantenibilidad, solo uno, la sentencia switch, disminuyó en mantenibilidad. La puntuación de mantenibilidad del polimorfismo mejoró y sin embargo la complejidad también aumenta (preferiríamos que disminuyera). Como mencioné arriba, esto es contraintuitivo.
Nuestra comparación muestra que los diccionarios pueden, desde un punto de vista de complejidad, escalar infinitamente. El enfoque polimórfico es, con mucho, el más mantenible y parece aumentar en mantenibilidad a medida que se añaden más escenarios. La sentencia switch aumenta en complejidad y disminuye en mantenibilidad cuando se añadió el nuevo escenario. Incluso antes de que añadiéramos el nuevo escenario, tenía la peor complejidad ciclomática y medidas del índice de mantenibilidad.
Jem Finch de Google compartió sus pensamientos sobre las deficiencias de las sentencias switch:
1. Las implementaciones de métodos polimórficos están aisladas léxicamente una de la otra. Las variables pueden ser añadidas, removidas, modificadas, y así sucesivamente sin ningún riesgo de impactar código no relacionado en otra rama de la sentencia switch.
2. Las implementaciones de métodos polimórficos están garantizadas de retornar al lugar correcto, asumiendo que terminan. Las sentencias switch en un lenguaje con fall through como C/C++/Java requieren una sentencia “break” propensa a errores para asegurar que retornen a la sentencia después del switch en lugar del siguiente bloque case.
3. La existencia de una implementación de método polimórfico puede ser reforzada por el compilador, que se negará a compilar el programa si falta una implementación de método polimórfico. Las sentencias switch no proporcionan tal verificación de exhaustividad.
4. El envío de métodos polimórficos es extensible sin acceso a (o recompilación de) otro código fuente. Añadir otro case a una sentencia switch requiere acceso al código de envío original, no solo en un lugar sino en cada lugar donde el enum relevante está siendo switcheado.
5. … puedes probar métodos polimórficos independientemente del aparato de switching. La mayoría de funciones que hacen switch como el ejemplo que dio el autor contendrán otro código que no puede ser probado por separado; las llamadas de métodos virtuales, por otro lado, pueden.
6. Las llamadas de métodos polimórficos garantizan envío en tiempo constante. Ningún compilador suficientemente inteligente es necesario para convertir lo que es naturalmente una construcción de tiempo lineal (la sentencia switch con fall through) en una construcción de tiempo constante.
Desafortunadamente, o afortunadamente, dependiendo de tu bando, la mayoría de lenguajes tienen una sentencia switch, y no van a desaparecer pronto. Con esto en mente, es bueno saber qué está pasando bajo el capó cuando se compilan sentencias switch.
Hay tres optimizaciones de sentencias switch que pueden ocurrir:
- Sentencias If-elseif – Cuando una sentencia switch tiene un pequeño número de cases o cases dispersos (valores no incrementales, como 10, 250, 1000) se convierte en una sentencia if-elseif.
- Tabla de Saltos – En conjuntos más grandes de cases adyacentes (1, 2, 3, 4, 5) el compilador convierte la sentencia switch a una tabla de saltos. Una Tabla de Saltos es esencialmente una Hashtable con un puntero (piensa en la sentencia goto) a la función en memoria.
- Búsqueda Binaria – Para conjuntos grandes de cases dispersos el compilador puede implementar una búsqueda binaria para identificar el case rápidamente, similar a cómo funciona un índice en una base de datos. En casos extraordinarios donde los cases son un gran número de cases dispersos y adyacentes, el compilador usará una combinación de las tres optimizaciones.
Resumen
En un mundo orientado a objetos la sentencia switch, concebida en 1952, es un pilar del ingeniero de software. Una excepción notable es Smalltalk donde los diseñadores optaron por excluir la sentencia switch.
Cuando se compara con implementaciones equivalentes alternativas, el diccionario, y el polimorfismo, la sentencia switch no se desempeñó tan bien.
La sentencia switch está aquí para quedarse, pero como nuestra comparación ha mostrado hay mejores alternativas a la sentencia switch.
Las implementaciones están disponibles en Github.
Autor: Chuck Conway es un Ingeniero de IA con casi 30 años de experiencia en ingeniería de software. Construye sistemas de IA prácticos—canalizaciones de contenido, agentes de infraestructura y herramientas que resuelven problemas reales—y comparte lo que está aprendiendo en el camino. Conéctate con él en redes sociales: X (@chuckconway) o visítalo en YouTube y en SubStack.