Al pan Enum y al vino Flags

Mientras preparaba el material para el WebCast que di para el dotNet Club de la U. Lleida, me tope con enumeradores como marcadores de bit y quiero comentar lo útil que pueden llegar a ser.

Básicamente tenemos dos opciones o características que determinan el comportamiento de este tipo de dato constante y se basa en el uso o no del atributo FlagAttribute sobre el Enum. Es importante entender que dicho atributo será especialmente útil cuando necesitemos que los valores del enumerador se combinen a través de operaciones lógicas o bit a bit del tipo AND, OR, NOT y XOR.

Un ejemplo de típica declaración de enumeradores seria:

   1: public enum TypicalEnums
   2: {
   3:     Option1, // = 0x00
   4:     Option2, // = 0x01
   5:     Option3, // = 0x02
   6:     Option4, // = 0x03
   7:     Option5, // = 0x04
   8:     Option6  // = 0x05
   9: }

En este ejemplo, damos por supuesto que una variable de tipo TypicalEnums únicamente podrá contener un único valor, cuyo tipo subyacente es por defecto es int. Sin embargo el siguiente enumerador:

   1: [Flags]
   2: public enum FlagEnums
   3: {
   4:     Option0 = 0x00,     // 0 = 0x00
   5:     Option1 = 0x01,     // 1 = 0x01
   6:     Option2 = 0x01 << 1,// 2 = 0x02
   7:     Option3 = 0x01 << 2,// 4 = 0x04
   8:     Option4 = 0x01 << 3,// 8 = 0x08
   9:     Option5 = 0x01 << 4,//16 = 0x16
  10:     Option6 = 0x01 << 5 //32 = 0x32
  11: }

Nos permitirá tener varios valores dentro de una misma variable del tipo FlagsEnums.

En el primer caso podremos hacer por ejemplo:

Cliente.Tipo = TypicalEnums.Option1 | TypicalEnums.Option2

Pero no tendrá ninguna repercusión pues Cliente.Tipo almacenará el valor 1 (TypicalEnums.Option1), pero sin embargo la siguiente sentencia:

Cliente.Caracteristicas = FlagEnums.Option2 | FlagEnums.Option5,

Si que almacenará ambas opciones.

Fijaros que en la declaración de FlagsEnum hemos indicado explícitamente el valor de las opciones de tres formas, desplazando un bits a la izquierda en base al valor 0x01, con valores numéricos decimales y valores hexadecimales. Todos ellos tendrán la siguiente correspondencia en binario:

  • Option0 = 0000000
  • Option1 = 0000001
  • Option2 = 0000010
  • Option3 = 0000100
  • Option4 = 0001000
  • Option5 = 0010000
  • Option6 = 0100000

A partir de aquí todo lógica. Si en el anterior ejemplo asignamos FlagsEnum.Option2 y FlagsEnum.Option5 al campo Cliente.Característica el valor que almacenará será 7 es decir 2 + 5, y por tanto utilizaremos el operador lógico OR:

   1: Cliente cliente = new Cliente
   2:                       {
   3:                           IdProvincia = 1,
   4:                           Tipo = TypicalEnums.Option1, 
   5:                           Caracteristicas = FlagEnums.Option2 | FlagEnums.Option5,
   6:                           //Caracteristicas = 00000010        | 00001000           = 00001010
   7:                           Nombre = "Cliente 1",
   8:                           VolumenNegocio = 10.0m
   9:                       };

Ahora bien,¿cómo podemos sacar el máximo provecho de los enumeradores con marcadores de bit? pues aplicando lógica, es decir, si queremos saber si un marcador se ha establecido utilizaremos AND:

   1: if ((cliente.Caracteristicas & FlagEnums.Option5) == FlagEnums.Option5)
   2:     // (00001010 & 00001000) = 00001000 
   3:     //             00001000  = 00001000 => true
   4:     Console.WriteLine("{0}: nFlagEnum t[{1}] nOption 5 t[{2}] nHence: tt[{3}]",
   5:                       cliente.Nombre,
   6:                       Convert.ToString(((int)cliente.Caracteristicas), 2),
   7:                       Convert.ToString(((int)FlagEnums.Option5), 2),
   8:                       Convert.ToString((int)(cliente.Caracteristicas & FlagEnums.Option5), 2));

Como puedes ver, en la condición de la sentencia if estamos comprobando la existencia de FlagsEnums.Option5 de forma lógica. Realizamos la operación lógica a nivel de bit de Cliente.Característica AND FlagsEnums.Option5 e igualamos al valor de FlagsEnums.Option5. Por otro lado:

   1: if ((cliente.Caracteristicas & FlagEnums.Option4) != FlagEnums.Option4)
   2:     // (00001010 & 00000100) = 00000100 
   3:     //             000000000 = 00000100 => false
   4:     Console.WriteLine("Cliente with nFlagEnum t[{0}] nhasn't Option 4 t[{1}]",
   5:         Convert.ToString(((int)cliente.Caracteristicas), 2),
   6:         Convert.ToString(((int)FlagEnums.Option4), 2));

Si queremos comprobar FlagsEnums.Option4, el cual no está, el razonamiento será el mismo (fíjate en las líneas 2 y 3). Tras ejecutar ambos Snippets con la clase Cliente definida como:

   1: public class Cliente
   2: {
   3:     public int IdProvincia { get; set; }
   4:     public TypicalEnums Tipo { get; set; }
   5:     public FlagEnums Caracteristicas { get; set; }
   6:     public string Nombre { get; set; }
   7:     public decimal VolumenNegocio { get; set; }
   8:  
   9:     public override string ToString()
  10:     {
  11:         return string.Format("Cliente: {0} - Provincia:{1} - nTipo: {2} - Caract:{3} - Vol.:{4}en",
  12:                              Nombre, IdProvincia, (int) Tipo,Caracteristicas, VolumenNegocio);
  13:     }
  14: }

Obtendremos:

image

Por último, si pretendemos quitar un marcador ya establecido utilizaremos un XOR de forma que:

cliente.Caracteristicas = cliente.Caracteristicas ^ FlagEnums.Option5;

Desasignaría FlagsEnums.Option5 al campo Cliente.Características.

Uso de métodos extensores

Soy un auténtico fan de este tipo de característica y no puedo dejar pasar ni un solo enumerador de marcadores de bit sin extender el típico método HasFlag o como_queráis_llamarlo a la clase que los utiliza, siempre y cuando sea posible, y por tanto una forma de extender la clase Cliente para la comprobación de marcadores seria la que he utilizado para confeccionar el ejemplo de este post, es decir:

   1: public static class ClienteExtensions
   2: {
   3:     public static string HasFlagVerbose(this Program.Cliente cliente, Program.FlagEnums flag)
   4:     {
   5:         return string.Format("Checking {2} for {0}: nCaracteristica t[{1}] n{2} t[{3}] nHence tt[{4}]n",
   6:                              cliente.Nombre,
   7:                              Convert.ToString(((int)cliente.Caracteristicas), 2).PadLeft(8, '0'),
   8:                              flag,
   9:                              Convert.ToString(((int)flag), 2).PadLeft(8, '0'),
  10:                              Convert.ToString((int)(cliente.Caracteristicas & flag), 2).PadLeft(8, '0'));
  11:     }
  12:  
  13:     public static bool HasFlag(this Program.Cliente cliente, Program.FlagEnums flag)
  14:     {
  15:         return (cliente.Caracteristicas & flag) == flag;
  16:     }
  17: }

Con lo que el cuerpo del programa que he ejecutado para mostrar los resultados seria:

   1: public class Program
   2:     {
   3:         
   4:         static void Main(string[] args)
   5:         {
   6:             Cliente cliente = new Cliente
   7:                                   {
   8:                                       IdProvincia = 1,
   9:                                       Tipo = TypicalEnums.Option1, 
  10:                                       Caracteristicas = FlagEnums.Option2 | FlagEnums.Option5,
  11:                                       //Caracteristicas = 00000010        | 00001000           = 00001010
  12:                                       Nombre = "Cliente 1",
  13:                                       VolumenNegocio = 10.0m
  14:                                   };
  15:  
  16:             //comprobamos q realmente tiene FlagEnums.Option5 
  17:             if ((cliente.HasFlag(FlagEnums.Option5)))
  18:                 Console.WriteLine(cliente.HasFlagVerbose(FlagEnums.Option5));
  19:  
  20:             //comprobamos q realmente NO tiene FlagEnums.Option4
  21:             if (!(cliente.HasFlag(FlagEnums.Option4)))
  22:                 Console.WriteLine(cliente.HasFlagVerbose(FlagEnums.Option4));
  23:  
  24:             //quitamos FlagsEnums.Option5
  25:             cliente.Caracteristicas = cliente.Caracteristicas ^ FlagEnums.Option5;
  26:  
  27:             //comprobamos q hemos quitado FlagEnums.Option5
  28:             if (!(cliente.HasFlag(FlagEnums.Option5)))
  29:                 Console.WriteLine(cliente.HasFlagVerbose(FlagEnums.Option5));
  30:  
  31:             Console.ReadKey();
  32:         }
  33:     }

 

A partir de aquí… posibilidades infinitas ;-))

Más info:

5 comentarios en “Al pan Enum y al vino Flags”

  1. Buenas!

    Un temilla: dices que sin [Flags] no se puede usar bitwise en mis enums, pero eso no es cierto.
    Prueba el siguiente código:

    enum Foo
    {
    Bar =0x1,
    Baz =0x2,
    FooBar =0x4,
    FooBaz =0x8
    }

    class Program
    {
    static void Main(string[] args)
    {
    Foo foo1 = Foo.Bar;
    Foo foo2 = Foo.FooBar| Foo.FooBaz;

    Console.WriteLine((foo2 & Foo.Baz) == Foo.Baz);
    Console.WriteLine((foo2 & Foo.FooBaz) == Foo.FooBaz);
    Console.ReadLine();
    }

    Verás como imprime false y true… exactamente igual que si tuvieses [Flags] aplicado.

    Que hace [Flags]? Pues una muy buena pregunta: yo creo que modificar ToString() y poca cosa más… pero de verdad lo desconozco.

    Saludos!!!

  2. @Ramón: gracias por la info. No lo conocia y sin duda es una buena noticia 😉

    @Edu: Al fin y al cabo esta tratando valores que son potencia de dos con lo que también nos serviria no utilizar enum. Si:

    int foo = 4 | 8 = 12;

    entonces siempre:

    bool res = (12 & 8) == 8;

    será true pues no está mas que resolviendo (00001100 & 0001000) = 0001000.

    El verdadera meta del uso de FlagsAttribute, además de hacer las cosas bien, es la de los valores implícitos asignados a los marcadores del enum, es decir:

    enum Foo
    {
    Bar, //=0x1
    Baz, // =0x2
    FooBar, // =0x3
    FooBaz // =0x4
    }

    mientras que:

    [Flags]
    enum Foo
    {
    Bar, //=0x1
    Baz, // =0x2
    FooBar, // =0x4
    FooBaz // =0x8
    }

    Y evitar lapsus como:

    [Flags]
    enum Foo
    {
    Bar =0x1,
    Baz =0x2,
    FooBar =0x3,
    FooBaz =0x4
    }

    Y, efectivamente, sobreescribir el ToString() para que te salgan los valores separados por coma.

    En fin, ya tenemos tema de discusión para el jueves en CatDotNet 😉

    Gracias por el comentario!!!!

  3. Efestivamente, la ventaja del atributo [Flags] estriba en no tener que preocuparte tu mismo por los valores de cada elemento de la enumeración.
    Así que a esto os dedicáis en las renuiones de CatDotNet? Y seguro que delante de una cerveza, verdad?
    Aisss… a ver si me puedo escapar algún dia, ya sabéis, para culturizarme un poco 😛

    Salut!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *