Using a Generic Type to Simplify Flags Enumeration Operations
Using a generic type taking an enumerated type to provide simple manipulation of the enumeration as an alternative to [Flags] attribute
Introduction
The introduction of the [Flags]
attribute made it very much easier to set up and use flag registers based on enumerated types. However, there are times when it can be cumbersome to query the state of individual flags or combinations of flags resulting in code that is difficult to understand without careful reading.
By providing a generic type that allows any enumeration, whether marked with the [Flags]
attribute or not, to be used as a flag type with methods to clear, set and interrogate flag state, we can make it easier to use such types and make the code easier to understand and therefore easier to develop and maintain.
Because we can use enumerations not marked with the [Flags]
attribute, we can use enumerations in third party DLLs, assuming it is appropriate and necessary, without having to wrap them in a marked in-house enumeration.
Using the Code
Outline
The developer defines an enumeration to meet the application's requirements. He or she then creates an instance of the generic type Register<Enumeration>
with the desired enumerated type and uses that in place of a variable of the enumerated type.
Class : Register<Enumeration>
Constructor | Comment |
Register<T>() |
Creates an instance based on enumerated type T with all flags cleared. |
Register<T>(T) |
Creates an instance based on enumerated type T with the specified flags set and the remainder clear. Use logical OR to set multiple flags. |
Method | Returns | Comment |
Set() |
T |
Sets all flags in the register. Returns the updated state of the register. |
Set(T) |
T |
Set the specified flags. Use logical OR to set multiple flags. Returns the updated state of the register. |
Clear() |
T |
Clears all flags in the register. Returns the updated state of the register. |
Clear(T) |
T |
Clear the specified flags. Use logical OR to set multiple flags. Returns the updated state of the register. |
IsSet(T) |
bool |
Query the state of a flag in the register. Multiple flags can be queried Using logical OR . Returns false if one or more of the queried flags is clear. |
Property |
Type |
Comment |
State |
T |
Returns the current state of the register as the instantiating enumerated type. |
Using : Register<Enumeration>
Example
Define our enumeration. Note that we no longer need to mark this with the [Flags]
attribute.
public enum Greek
{
Alpha = 1,
Beta = 2,
Gamma = 4,
Delta = 8,
Epsilon = 16,
}
Now create an instance of the Register
using the appropriate enumeration:
// We can either create an instance with all flags clear...
Register<Greek> startClear = new Register<Greek>();
// ... or create a flag register with one or more flags set.
Register<Greek> startSet = new Register<Greek>(Greek.Delta | Greek.Gamma);
And use it as needed:
// Set all flags in a single operation
startClear.Set();
// Clear flags either singly or in combination.
startClear.Clear(Greek.Alpha);
startClear.Clear(Greek.Gamma|Greek.Epsilon);
// Set flags either singly or in combination.
startClear.Set(Greek.Alpha);
startClear.Set(Greek.Gamma|Greek.Epsilon);
// Querying the state of an individual flag
Console.WriteLine("{0} set {1}", Greek.Alpha, startClear.IsSet(Greek.Alpha));
// Querying multiple flags in one operation.
Register<greek> startSet = new Register<greek>(Greek.Delta | Greek.Gamma);
Console.WriteLine("{0} set {1}",
Greek.Alpha.ToString() + Greek.Delta.ToString(),
startSet.IsSet(Greek.Alpha|Greek.Delta)); // Alpha flag is clear
// so returns false.
Console.WriteLine("{0} set {1}",
Greek.Gamma.ToString() + Greek.Delta.ToString(),
startSet.IsSet(Greek.Gamma|Greek.Delta)); // Both flags set returns true
We can also see that we have type safety if we try and assign the state of our Register<T>
instance to another enumerated type.
Greek g = startClear.State; // All hunky dory.
Island i = startClear.State; // Compile time error.
Notes
Internally the enumeration is held as int32
. This reduces the amount of casting required to persuade the compiler that type <T>
allows bitwise operations. We cast back to <T>
only if a return value is required.
For most cases where a flag register is required, int32
is probably overkill, but because we control the internal representation of the register, we could implement it any way we chose.
It would have been nice to be able to constrain the type parameter <T>
to allow the use of only enumerated types, e.g. class Y<T> where T : enum
unfortunately no such constraint exists. This means that a developer could instantiate a Register<T>
with pretty much any type so running the risk of a runtime error. The following will, alas, compile...
Register<string> r = new Register<string>();
...you have been warned.
Why?
Decades as a software developer creating and maintaining applications have made me appreciate anything, even something as trivial as reducing the effort required to manipulate bit flags, that makes it easier for me to write code that is correct the first time around and also allows anyone who inherits responsibility for the code to easily understand what I was trying to do when (inevitably) the business rules change and the code has to be revised in short order and with no access to the original design documentation.
Update
You might also like to have a look at an earlier article that covers the same ground and provides a workaround for constraint problem : EnumOperatorsHistory
- November 2010 - Convert type specific 1.1 code to type generic 3.5 code
- November 2010 - Add link to PIEBALDConsults article.