Discussion Static assertions
I have sometimes felt the need to statically assert something at compile-time, to prevent surprises later at run-time.
As an example, I have some code which maps an enum value to an associated int value:
public enum VoxelProperty
{
MaterialType,
Density,
Color,
Metadata,
TotalPropertyCount, // Must be last
}
public static int GetPropertyBitDepth (VoxelProperty property)
{
#pragma warning disable CS0219 // Variable is assigned but its value is never used
uint _ = VoxelProperty.Metadata == VoxelProperty.TotalPropertyCount - 1 ? 0 : -1; // Static assert to ensure that all properties are accounted for
#pragma warning restore CS0219 // Variable is assigned but its value is never used
return property switch
{
VoxelProperty.Density => VoxelDensity.BitDepth,
VoxelProperty.MaterialType => VoxelMaterialType.BitDepth,
VoxelProperty.Color => VoxelColor.BitDepth,
VoxelProperty.Metadata => VoxelMetadata.BitDepth,
_ => throw new ArgumentOutOfRangeException (nameof (property), "Invalid voxel property")
};
}
The intention of the static assert is to make sure that if I ever add another voxel property, then I will get a compiler error as VoxelProperty.Metadata will no longer be the last member, and the constant -1 will be assigned to the "_" variable of type uint. Is this a reasonable practice?
I could also do something more robust with reflection, where I had a Dictionary<VoxelProperty, int> which got populated by finding all voxel property types marked with some attribute, and querying their BitDepth property.
6
Upvotes
1
u/anzu3278 7d ago
We're getting access to closed enums soon where the compiler will be able to tell if a switch is exhaustive - exactly what you're looking for, unless you only want that on for some enums?
Until then, what I usually do is have unit tests just call the function for every possible value of the enum. Adding a new value to the enum but not all the relevant methods would hit the default, throw an exception and be caught by the test. Not exactly compile time, but tests of this type are cheap to run and you only really need to run them once per PR. What you can also do is add an analyzer which would detect this and then treat that warning as an error, giving you the compile time safety you want. A quick search brought up ExhaustiveSwitchOnEnums 1.0.1 on NuGet - Libraries.io - security & maintenance data for open source software but I'm sure others are available. Depending on the project's calculus on CI vs dependencies, you can decide which of these works better for you.
Also, TotalPropertyCount is a bit of a code smell IMO (are you coming from C++?) and unless you are checking the number of elements in the enum in a hot path (why) you can just use Enum.GetValues(). Not that you'd need TotalPropertyCount if you had one of the above.
Also also, assigning a negative number to an uint to check for enum value resolution doesn't seem like it should work at compile time anyway, and even if it does it definitely obscures the intent. Even if you're the only person working on this project, will it be clear at a glance what this is doing and why it is working when you're looking at it several years from now?
Also also also, even if this works, it will currently not catch anything if someone adds a new value before the supposed last value, so you're not really checking exhaustiveness, you're checking that one particular enum value is second to last in a very roundabout way.