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:
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:
http://msdn.microsoft.com/en-us/library/system.enum.hasflag(VS.100).aspx
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!!!
@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!!!!
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!
jejeje, Lluis, o bajas tu o subo yo a Andorra con la parienta a esquiar, que se que te encanta ;-)))