In this article, we are going to learn about the Flags attribute for enum in C#, what its benefits are, and how to use it in a practical example.
Let’s start.
Define the Problem
Let’s define an enum that represents the types of users:
public enum UserType { Customer = 1, Driver = 2, Admin = 3, }
We define the UserType
enum that contains three values: Customer
, Driver
, and Admin
.
But what if we need to represent a collection of values?
For example, at a delivery company, we know that both the Admin
and the Driver
are employees. So let’s add a new enumeration item Employee
. Later on, we will show you how we can represent both the admin and the driver with it:
public enum UserType { Customer = 1, Driver = 2, Admin = 3, Employee = 4 }
Define and Declare a Flags Attribute
A Flags
is an attribute that allows us to represent an enum as a collection of values rather than a single value. So, let’s see how we can implement the Flags
attribute on enumeration:
[Flags] public enum UserType { Customer = 1, Driver = 2, Admin = 4, }
We add the Flags
attribute and number the values with powers of 2. Without both, this won’t work.
Now going back to our previous problem, we can represent Employee
using the |
operator:
var employee = UserType.Driver | UserType.Admin;
Also, we can define it as a constant inside the enum to use it directly:
[Flags] public enum UserType { Customer = 1, Driver = 2, Admin = 4, Employee = Driver | Admin }
Behind the Scenes
To understand the Flags
attribute better, we must go back to the binary representation of the number. For example, we can represent 1 as binary 0b_0001 and 2 as 0b_0010:
[Flags] public enum UserType { Customer = 0b_0001, Driver = 0b_0010, Admin = 0b_0100, Employee = Driver | Admin, //0b_0110 }
We can see that each value is represented in an active bit. And this is where the idea of numbering the values with the power of 2 came from. We can also note that Employee
contains two active bits, that is, it is a composite of two values Driver
and Admin
.
Operations on Flags Attribute
We can use the bitwise operators to work with Flags
.
Initialize a Value
For the initialization, we should use the value 0 named None, which means the collection is empty:
[Flags] public enum UserType { None = 0, Customer = 1, Driver = 2, Admin = 4, Employee = Driver | Admin }
Now, we can define a variable:
var flags = UserType.None;
Add a Value
We can add value by using |
operator:
flags |= UserType.Driver;
Now, the flags
variable equals Driver
.
Remove a Value
We can remove value by use &
, ~
operators:
flags &= ~UserType.Driver;
Now, flags
variable equals None
.
Check if the Value Exists
We can check if the value exists by using &
operator:
Console.WriteLine((flags & UserType.Driver) == UserType.Driver);
The result is False.
Also, we can do this by using the HasFlag
method:
Console.WriteLine(flags.HasFlag(UserType.Driver));
Also, the result will be False.
As we can see, both ways, using the &
operator and the HasFlag
method, give the same result, but which one should we use? To find out, we will test the performance on several frameworks.
Measure the Performance
First, we will create a Console App, and in the .csproj file we will replace the TargetFramwork tag with the TargetFramworks tag:
<TargetFrameworks>net48;netcoreapp3.1;net6.0</TargetFrameworks>
We use the TargetFramworks tag to support multiple frameworks: .NET Framework 4.8, .Net Core 3.1, and .Net 6.0.
Secondly, let’s introduce the BenchmarkDotNet library to get the benchmark results:
[Benchmark] public bool HasFlag() { var result = false; for (int i = 0; i < 100000; i++) { result = UserType.Employee.HasFlag(UserType.Driver); } return result; } [Benchmark] public bool BitOperator() { var result = false; for (int i = 0; i < 100000; i++) { result = (UserType.Employee & UserType.Driver) == UserType.Driver; } return result; }
We add [SimpleJob(RuntimeMoniker.Net48)]
, [SimpleJob(RuntimeMoniker.NetCoreApp31)]
, and [SimpleJob(RuntimeMoniker.Net60)]
attributes to the HasFlagBenchmarker
class to see the performance differences between different versions of .NET Framework / .NET Core:
| Method | Job | Runtime | Mean | Error | StdDev | Median | |------------ |------------------- |------------------- |------------:|-----------:|------------:|------------:| | HasFlag | .NET 6.0 | .NET 6.0 | 37.79 us | 3.781 us | 11.15 us | 30.30 us | | BitOperator | .NET 6.0 | .NET 6.0 | 38.17 us | 3.853 us | 11.36 us | 30.38 us | | HasFlag | .NET Core 3.1 | .NET Core 3.1 | 38.31 us | 3.939 us | 11.61 us | 30.37 us | | BitOperator | .NET Core 3.1 | .NET Core 3.1 | 38.07 us | 3.819 us | 11.26 us | 30.33 us | | HasFlag | .NET Framework 4.8 | .NET Framework 4.8 | 2,893.10 us | 342.563 us | 1,010.06 us | 2,318.93 us | | BitOperator | .NET Framework 4.8 | .NET Framework 4.8 | 38.04 us | 3.920 us | 11.56 us | 30.17 us |
So, in .NET Framework 4.8 a HasFlag
method was much slower than the BitOperator
. But, the performance improves in .Net Core 3.1 and .Net 6.0. So in newer versions, we can use both ways.
A Practical Example of Flags Attribute
Let’s say we want to send messages to users by their type.
We can define previous operations as Extension Methods:
public static UserType Add(this UserType userType, params UserType[] typesToAdd) { foreach (var flag in typesToAdd) { userType |= flag; } return userType; } public static UserType Remove(this UserType userType, params UserType[] typesToRemove) { foreach (var item in typesToRemove) { userType &= ~item; } return userType; } public static bool CustomHasFlag(this UserType userType, UserType typeToCompare) => (userType & typeToCompare) == typeToCompare; public static void Print(this UserType userType) => Console.WriteLine("This message is for users of type: {0}.", userType);
Now, we can use them easily:
var audience = UserType.None; audience = audince.Add(UserType.Employee, UserType.Customer); audience.Print(); audience = audience.Remove(UserType.Driver); audience.Print(); if (audience.CustomHasFlag(UserType.Driver)) // or use HasFlag { Console.WriteLine("Driver exists."); } else { Console.WriteLine("Driver doesn't exist."); Console.WriteLine("Adding Driver..."); audience = audience.Add(UserType.Driver); } audience.Print();
And the output will be:
This message is for users of type: Customer, Employee. This message is for users of type: Customer, Admin. Driver doesn't exist. Adding Driver... This message is for users of type: Customer, Employee.
Conclusion
In this article, we’ve learned about the Flags
attribute in C# and operations we can do with it. We’ve also compared two ways to check if a value exists. Finally, we’ve used our knowledge in a single example to see the Flags attribute in action.