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