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, flagsvariable equals None.
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.
CatAndDog = Cat | Dog
(the logical OR instead of the Conditional), I assume? – Heroin