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: \t\t[{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}e\n",
  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 \t\t[{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:

Published 16/11/2009 0:06 por José Miguel Torres
Comparte este post:
http://geeks.ms/blogs/jmtorres/archive/2009/11/16/al-pan-enum-y-al-vino-flag.aspx

Comentarios

# re: Al pan Enum y al vino Flags

Ciertamente, es fácil cometer un pequeño error en las comparaciones con los operadores lógicos. Hace poco leí que la clase Enum de .NET 4 a partir de la Beta 2 incorpora ya un método HasFlag:

msdn.microsoft.com/.../system.enum.hasflag(VS.100).aspx

Monday, November 16, 2009 12:44 AM por Ramón Sola

# re: Al pan Enum y al vino Flags

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!!!

Monday, November 16, 2009 10:00 AM por Eduard Tomàs i Avellana

# re: Al pan Enum y al vino Flags

@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!!!!

Monday, November 16, 2009 10:37 AM por José Miguel Torres

# re: Al pan Enum y al vino Flags

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 :-P

Salut!

Tuesday, November 17, 2009 9:38 AM por Lluis Franco

# re: Al pan Enum y al vino Flags

jejeje, Lluis, o bajas tu o subo yo a Andorra con la parienta a esquiar, que se que te encanta ;-)))

Tuesday, November 17, 2009 9:59 AM por José Miguel Torres