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.
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 }
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 }
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
.
We can use the bitwise operators to work with Flags
.
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;
We can add value by using |
operator:
flags |= UserType.Driver;
Now, the flags
variable equals Driver
.
We can remove value by use &
, ~
operators:
flags &= ~UserType.Driver;
Now, flags
variable 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.
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.
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.
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.
In this article, we will explore refactoring dispensables in C#. We'll focus on identifying code…
In software development, we constantly have to handle different data formats. The XML standard is…
In this article, we'll look at TimeProvider - a new feature coming to C# 12…
Issue #187 of the Code Maze weekly. Check out what's new this week and enjoy…
In the ever-evolving world of software development, writing clean and efficient code by using C#…
In this article, we'll explore EF Core Power Tools, a popular toolbox aimed at simplifying…