La maldad de Enum
Ayer surgió mi primera oportunidad de aplicar a la práctica parte de lo aprendido en la pasada #dotNetSpain2015.
Retomando el post anterior, durante su charla de Effective C#, Leo Antoli | @lantoli habló del comportamiento de las enum
en C#.
Mi recomendación con las enumeraciones es que no las uséis.
Pues bien… Ayer sufrí en primera persona uno de los motivos por los que hay que evitar las enumeraciones siempre que sea posible.
Situación inicial
Dentro de una librería de proyecto con la que trabajo habitualmente – pero que no puedo modificar –, me encontré con una enum
parecida a la siguiente.
A raíz de una nueva solicitud funcional recibida por parte del cliente, es necesario incluir un valor de registro de Windows donde se almacene un valor numérico que se corresponda con lo que el usuario definió como:
Un valor válido de sistema operativo.
Además, el usuario quería que se mostrara un mensaje en el visor de eventos del equipo que indicase el valor y advirtiera de que se utilizaría el valor por defecto Any
.
Pero… ¿qué es un valor válido?
En este caso, para el usuario un valor válido se corresponde con un valor simple o compuesto de la enumeración OperatingSystem
, donde:
-
Un valor simple se corresponde con un valor directo. Por ejemplo, el valor 1, que se corresponde con
OperatingSystem.Windows7
. -
Un valor compuesto se corresponde con aquel valor fruto de la combinación de dos o más valores. Por ejemplo, el valor 3, que se corresponde con
OperatingSystem.Windows7
+OperatingSystem.WindowsServer2008
.
Además, como requisito, al almacenar el valor en el registro de Windows, este valor tiene que ser obligatoriamente un valor numérico.
Problemas
¿Qué problema hay con esta solicitud que, en principio, parece sencilla de implementar?
El mayor problema aquí es la definición que da el usuario sobre lo que para él es un valor válido.
Mi primera aproximación fue utilizar el siguiente fragmento de código.
Este código no permite que la segunda premisa definida como valor válido se cumpla: no admite valores compuestos.
Por tanto, fui más allá e implementé esto:
Parece una solución correcta en principio, pero…
¿Qué pasa si ponemos un valor superior a 15? Por ejemplo, ¿qué sucede con el valor 16?
Pues lo que pasa es que si en el registro hay un 16
, que en teoría tendría que generar una excepción de tipo InvalidCastException
, en realidad está siendo tratada como el valor 0
por culpa del casting explícito.
Cuando el código revisa si el valor recuperado contiene el valor mediante Enum.HasFlag()
, en realidad sólo está comparando los bits significativos de los valores disponibles, que en este caso concreto son los 4 primeros bits.
Integer | Binary (8 bits) | Valor |
---|---|---|
0 | 0000 0000 | Any |
1 | 0000 0001 | Windows7 |
2 | 0000 0010 | WindowsServer2008 |
3 | 0000 0011 | Windows7 + WindowsServer2008 |
15 | 0000 1111 | All |
16 | 0001 0000 | Any |
17 | 0001 0001 | Windows7 |
Mola, ¿verdad?
Solución
Creo que la solución más adecuada para implementar este comportamiento de forma correcta es almacenar los nombres en vez de los valores numéricos de cada elemento de la enum
en un valor MULTI_SZ del registro de Windows. Pero el valor tenía que ser obligatoriamente numérico.
No obstante, en este caso, la solución ha pasado porque el usuario comprendiera que en realidad no necesitaba la composición, ya que en su modelo de negocio sólo se podrían dar tres de los casos directos que incluye la enum
: Any
, Windows7
y WindowsServer2008
.
Así, la solución adoptada ha sido finalmente utilizar la primera versión del código.
¡Ay que ver qué cosas!
Moraleja
¡Mucho cuidado con las
enums
, especialmente si tienen la propiedad[Flags]
marcada!