A Brief Discussion on the Application and Insights of Flag Enums
What is an Enum
Before discussing Flag Enums, let's first understand what an Enum (enumeration type) is. An Enum is a value type composed of a set of named integer constants. The default underlying type for enumeration members is int, but it can be specified as other integer types. If the value of an enumeration member is not specified, it starts at 0 and increments by one.
In software development, it is common to use a set of constants to represent specific states, types, or operations, especially when used as method parameters. While using numeric values can reduce the possibility of input errors, it is not intuitive to identify the specific meaning of each value. Although using meaningful strings can reduce the possibility of string input errors, defining constants for each value individually helps improve this issue but still cannot prevent the use of other undefined values.
Through Enums, we can assign meaningful names to each enumeration member, which improves code readability while preventing the possibility of using undefined values, thereby increasing the stability of the program. Even for developers who are not familiar with English and cannot understand the naming of Enum members, they can still clearly understand the meaning of each item through the comments displayed by the editor.
The following is an Enum example:
enum Action : ushort { // Specify member type as ushort; defaults to int if not specified
/// <summary>
/// None(0)
/// </summary>
None, // No value set, starts from 0
/// <summary>
/// Query(1)
/// </summary>
Query, // Value is 1
/// <summary>
/// Create(10)
/// </summary>
Create = 10,
/// <summary>
/// Update(11)
/// </summary>
Update, // Value is 11 because the previous item was set to 10
/// <summary>
/// Delete(12)
/// </summary>
Dalete // Value is 12
}
Action action = Action.Update;
string name = action.ToString(); // name = "Update"
ushort value = (ushort)action; // value = 11Introduction to Flag Enums
A Flag Enum can be viewed as an Enum that supports bitwise operations, where each enumeration value represents an independent flag. By combining these flags, multiple states or options can be represented effectively.
How to Define a Flag Enum
A Flag Enum is defined by adding the FlagsAttribute to the Enum and using powers of 2 to define each enumeration value. Other values are composite values, and you can also define your own composite enumeration members.
The following is an example:
[Flags]
enum Permissions {
None = 0,
CanQuery = 1,
CanCreate = 2,
CanUpdate = 4,
CanDelete = 8,
CanUpsert = CanCreate | CanUpdate, // Value is 6
ExcludeDelete = ~CanDelete, // Value is -9
All = CanQuery | CanCreate | CanUpdate | CanDelete
}
// Using a predefined composite value directly; ToString() returns the name of that value
Permissions permission = Permissions.CanUpsert;
string name = permission.ToString(); // name = "CanUpsert"
int value = (int)permission; // value = 6
// Using bitwise operations; the result is a predefined composite value, ToString() returns the name of that value
permission = Permissions.CanCreate | Permissions.CanUpdate;
name = permission.ToString(); // name = "CanUpsert"
value = (int)permission; // value = 6
// Using bitwise operations; the result is a non-predefined composite value, ToString() returns the names of each enumeration value
permission = Permissions.CanCreate | Permissions.CanDelete;
name = permission.ToString(); // name = "CanCreate, CanDelete"
value = (int)permission; // value = 10
// Using the ~ bitwise operator; the result is a predefined composite value, ToString() returns the name of that value
permission = ~Permissions.CanDelete;
name = permission.ToString(); // name = "ExcludeDelete"
value = (int)permission; // value = -9
// Using the ~ bitwise operator; the result is a non-predefined composite value, ToString() returns the numeric value
permission = ~Permissions.CanCreate;
name = permission.ToString(); // name = "-3"
value = (int)permission; // value = -3Or define values using bitwise shift operations:
enum Permissions {
None = 0,
CanQuery = 1 << 0,
CanCreate = 1 << 1,
CanUpdate = 1 << 2,
CanDelete = 1 << 3
}Microsoft recommends using plural names for Flag Enum types, such as RegexOptions; and plural names for general Enum types, such as DayOfWeek.
How to Use Flag Enums
Flag Enums can effectively simplify method parameters, making them more readable. The following case is very suitable for refactoring using a Flag Enum:
void Execute(bool canQuery, bool canCreate, bool canUpdate, bool canDelete) {
// Actual execution logic
}Using the Flag Enum approach:
void Execute(Permissions permiss) {
// Actual execution logic
}This refactoring makes the method parameters clearer and eliminates the confusion that can arise from using multiple boolean values. Using a Flag Enum not only improves readability but also makes it more convenient to extend permissions in the future.
Bitwise Operations
The following uses the concept of sets to explain bitwise operations related to Flag Enums.
- OR (
|) operator: Performs an OR operation on two enumeration values to form a set containing both enumeration items, i.e., a union.
- AND (
&) operator: Performs an AND operation on two enumeration values to form a set containing the overlapping enumeration items, i.e., an intersection.
- XOR (
^) operator: Performs an XOR operation on two enumeration values to form a set that does not contain the overlapping items, i.e., a symmetric difference.
- NOT (
~) operator: Performs a NOT operation on an enumeration value to produce a set of enumeration values that does not contain the original items, i.e., a complement.
There is no difference operator for bitwise operations, so you cannot simply remove a specific enumeration item. However, you can achieve the same effect using the following methods:
- Get the complement of the item to be removed, then perform an intersection with the original item. The code is
Permissions.CanUpsert & ~Permissions.CanCreate. - Form a union with the item to be removed, then perform a symmetric difference with the item to be removed. The code is
(Permissions.CanUpsert | Permissions.CanCreate) ^ Permissions.CanCreate.
Determining if a Specific Enumeration Value is Included
To determine if a specific enumeration item is included, you can use the following approaches:
- Perform an intersection with the enumeration item to be checked, then check if the result is equal to that item.
// has1 = true
bool has1 = (Permissions.CanUpsert & Permissions.CanCreate) == Permissions.CanCreate;
// has2 = false
bool has2 = (Permissions.CanUpsert & Permissions.CanDelete) == Permissions.CanDelete;
// has3 = true
bool has3 = (Permissions.CanUpsert & Permissions.None) == Permissions.None;
// has4 = false
bool has4 = (Permissions.ExcludeDelete & Permissions.CanDelete) == Permissions.CanDelete;
// has5 = true
bool has5 = (Permissions.ExcludeDelete & Permissions.CanCreate) == Permissions.CanCreate;
// has6 = false
bool has6 = (Permissions.All & Permissions.ExcludeDelete) == Permissions.ExcludeDelete;HasFlag: A method added to Enum in .NET Framework 4.0. It internally uses the logic described above to perform the check. The results are as follows:
// has1 = true
bool has1 = Permissions.CanUpsert.HasFlag(Permissions.CanCreate);
// has2 = false
bool has2 = Permissions.CanUpsert.HasFlag(Permissions.CanDelete);
// has3 = true
bool has3 = Permissions.CanUpsert.HasFlag(Permissions.None);
// has4 = false
bool has4 = Permissions.ExcludeDelete.HasFlag(Permissions.CanDelete);
// has5 = true
bool has5 = Permissions.ExcludeDelete.HasFlag(Permissions.CanCreate);
// has6 = false
bool has6 = Permissions.All.HasFlag(Permissions.ExcludeDelete);Concerns Regarding Bitwise Operations
In the examples above, you might have concerns about Permissions.CanUpsert.HasFlag(Permissions.None) and Permissions.All.HasFlag(Permissions.ExcludeDelete).
First, from the perspective of bitwise operations, Permissions.CanUpsert & Permissions.None == Permissions.None being true is correct. From the perspective of sets, None represents an empty set, and an empty set is a subset of any set. Even if None is defined, the result remains as shown in the figure below:
Instead of the figure below:
The concerns regarding Permissions.All.HasFlag(Permissions.ExcludeDelete) mainly stem from the naming of All and a potential misunderstanding of ExcludeDelete. Although named All, it actually only contains the defined enumeration items. In other words, if a new enumeration item is defined but the value of All is not updated, it will not contain the new definition. Therefore, for Enums that may be extended, you should be cautious about using the name All. ExcludeDelete is defined using the NOT (~) operator; it consists of defined enumeration items that do not include Delete and undefined values. Specifically, it is the orange area in the figure below:
Therefore, the result of Permissions.All.HasFlag(Permissions.ExcludeDelete) is false. This also explains why (~Permissions.CanCreate).ToString() returns a numeric value rather than the names of the contained enumeration items. Thus, it is recommended to avoid using the NOT (~) operator to define composite enumeration items.
Change Log
- 2023-12-05 Initial version created.