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 | habló del comportamiento de las enum en C#.

Mi recomendación con las enumeraciones es que no las uséis.

Leo Antoli ©

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.

[Flags]
public enum OperatingSystem
{
    Any = 0,
    Windows7 = 1,
    WindowsServer2008 = 2,
    Windows8 = 4,
    WindowsServer2012 = 8,
    All = 15,
}

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.

bool isDefined = Enum.IsDefined(typeof(OperatingSystem), registryValue);
if (!isDefined)
{
	throw new InvalidCastException();
}

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:

OperatingSystem operatingSystem = (OperatingSystem)valorRegistro; // valorRegistro es de tipo int
// ...

bool found = false;
foreach (OperatingSystem item in typeof(OperatingSystem).GetEnumValues())
{       
    found = operatingSystem.HasFlag(item);
    if (found)
    {
        break;
    }
}

if (!found)
{
    throw new InvalidCastException();
}

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.

bool isDefined = Enum.IsDefined(typeof(OperatingSystem), registryValue);
if (!isDefined)
{
	throw new InvalidCastException();
}

¡Ay que ver qué cosas!

Moraleja

¡Mucho cuidado con las enums, especialmente si tienen la propiedad [Flags] marcada!