Optimize list of bool values in C#
Boolean data type represents the true
/false
value. If you create a class with multiple booleans, here are some tips and tricks to optimize them. This will include using bitwise operations and how to flatten them for display on UI.
- 1. The basic - Using bit to store multiple booleans
- 2. FlagsAttribute in C# - They already did it for you
- 3. Mind the limit of int
- 4. Flatten the value
1. The basic - Using bit to store multiple booleans
1.1. Hotel example
Let’s jump into an example.
It’s summer here in Montréal, and you want to book a hotel there for travel. The hotel’s rooms have multiple amenities. Some rooms have a TV; others have an oven, etc.
Naturally, when representing these rooms in the database, we would need some boolean values (or bit
data type in SQL). The class should look like this:
public class Room
{
public int RoomNumber { get; set; }
public bool HasTV { get; set; }
public bool HasMicrowave { get; set; }
public bool HasOven { get; set; }
}
All good if your hotel’s room only has that three amenities. What if the room has 10, 30, or even 100 amenities? Store each of them in a new bool
property? How about the database?
Suddenly, the manager wants to add a new amenity: A face mask dispenser. How would you deal with it? Would you add a new column in the database, a new property in the class, etc?
1.2. Bitwise operation comes to the rescue
We all know that:
AND operator
0 & 0 => 0
0 & 1 => 0
OR operator
0 | 0 => 0
0 | 1 => 1
We can exploit these bitwise to store our boolean values.
First, we set up an enum class. Notice the value of enum is the power of 2
public enum Facility
{
None = 0,
HasTV = 1,
HasMicrowave = 2,
HasOven = 4,
}
By doing this, when storing enum, these values will have the binary as follow:
None = 000
HasTV = 001
HasMicrowave = 010
HasOven = 100
The 1
character is in a different location on each value. When applying the AND
or OR
bitwise operator on these values, the combined value will have the 0
or 1
placed in the exact location.
For example:
HasTV 001
| HasMicrowave 010 <-- OR operator
-----------------------
011 <-- equal to 3 in decimal
We can store all the booleans in a single int
value using this.
The AND
operator with the targeted value will replace with 1
character with 0
:
value 011 <-- 3 in decimal, from above
& HasTV 001 <-- AND operator
-----------------------
Has TV? 001 <-- equal to enum value 1: HasTV
value 011 <-- 3 in decimal
& HasMicrowave 010 <-- AND operator
-----------------------
Has microwave? 010 <-- equal to enum value 2: HasMicrowave
How about HasOven
?
value 011 <-- 3 in decimal
& HasOven 100 <-- AND operator
-----------------------
Has oven? 000 <-- equal to enum value 0: None
1.3. Storing booleans
As you can guess, storing booleans is doing an OR
operator on the values.
var facilities = Facility.HasTV | Facility.HasMicrowave;
// facilities = 3
1.4. Retrieving booleans
To retrieve the bool values, we can do AND
operator with the targeted enum and test if the result > 0
var isHasTV = (facilities & Facility.HasTV) > 0 ? true : false;
2. FlagsAttribute in C# - They already did it for you
To make life easier for developers, Microsoft has a FlagsAttribute
that you can decorate your enum.
This will indicates that an enumeration can be treated as a bit field; that is, a set of flags.
[public enum Facility
{
None = 0,
HasTV = 1,
HasMicrowave = 2,
HasOven = 4,
}
]
2.1. Storing data
Using the same OR
bitwise operator to store the data
var roomFacility = Facility.HasTV | Facility.HasMicrowave;
2.2. Retrieving data
Use HasFlag
method to retrieve data
var hasTV = roomFacility.HasFlag(Facility.HasTV);
// hasTV = true
3. Mind the limit of int
There are 4 data type you can use to store int
value in SQL Server:
Data type | Range | Storage |
---|---|---|
bigint |
-2^63 (-9,223,372,036,854,775,808) to 2^63-1 (9,223,372,036,854,775,807) | 8 Bytes |
int |
-2^31 (-2,147,483,648) to 2^31-1 (2,147,483,647) | 4 Bytes |
smallint |
-2^15 (-32,768) to 2^15-1 (32,767) | 2 Bytes |
tinyint |
0 to 255 | 1 Byte |
8 bits = 1 byte. So for 1 byte of storage, you can store up to 8 boolean values (theoretically).
The default int
primitive type in C# is 32 bits or 4 bytes
What if you want to store more than 32 boolean values?
In this case, I would strongly recommend you to re-design your class and data structure to split it into smaller ones
4. Flatten the value
What if you want to flatten the boolean values compressed into a flat class with actual bool
properties?
Of course you can do it with HasFlag()
method, and a bunch of if
statements. However, writing if
10 times in a row is not very maintainable.
Luckily we have reflection
, a way to read the class meta data.
First, we need to create a new attribute class (full source code with comment here):
[public class FlattenedFlagAttribute : Attribute
{
public Enum enumProperty;
public FlattenedFlagAttribute(Facility facility)
{
this.enumProperty = facility;
}
// This method allow us "flatten" the compacted data into the property which is decorated with the same Facility data type
public static void Flatten(object o, Enum enumData)
{
var props = o.GetType().GetProperties();
foreach (var prop in props)
{
var attr = prop.GetCustomAttributes(typeof(FlattenedFlagAttribute), false);
if (attr.GetLength(0) > 0)
{
prop.SetValue(o, enumData.HasFlag(((FlattenedFlagAttribute)attr[0]).enumProperty));
}
}
}
}
]
Now, we can simply decorate the flatten properties with this attribute:
public class FlattenedRoom
{
public int RoomNumber { get; set; }
[ ]
public bool HasTV { get; set; }
[ ]
public bool HasMicrowave { get; set; }
[ ]
public bool HasOven { get; set; }
public FlattenedRoom(int roomNumber, Facility facility)
{
RoomNumber = roomNumber;
FlattenedFlagAttribute.Flatten(this, facility);
}
}
When you construct a new FlattenedRoom
, simple feed it with the compacted Facility
value, and everything will be correctly set.
Head over here for the full source code: https://github.com/huntertran/flag-flattener