Huristic/io

View Original

C# Advanced Enum Value Types!

C# and .NET framework generally try to stay true to the definitions of language concepts, warming your local computer scientist heart. Yet, sometimes this purist's attitude can get in the way of useful language constructs which otherwise don't quite fit into theoretical definitions. I'm talking about enums people! In C#; from eden to current version 7, enums have always remained true enumerated type definition: A data type consisting of a set of named values called elements, members, enumeral, or enumerators of the type. Most of not all languages support this, as a matter of fact you'd probably be very hard pressed to find a language that doesn't define an enum type. Though, many other modern languages, provide an expanded functionality enum type, one that does more than just enumerate values.


I'm not going to argue whether that's a good thing or not here; that's a topic of a major discussion. Rather, I'm going to show approaches of implementing what I call "advanced enums" in C#, since they aren't natively supported. With these techniques we will see, many advantages of more sophisticated enumerations, their use and implementations. Take this with a grain of salt if you are a language purist you probably won't like what you see. I'm going to try and change you mind.

What's an enum again?

It's actually more difficult to describe what an enum is than one can expect. Technical definitions aside, an enum is a type that represents a collection of something that can be "enumerated". For example, months of the year, or days of the week, or HTTP status codes, or HTTP methods. Hopefully you're starting to see a pattern. These values are finite, rarely change, and generally the collection is small. Some other properties of enums are: they are immutable, usually a value type vs. reference types (will talk about this later). Most important part of an enum is the "enumeration" part; these values must have some sort of implicit ordering. If you notice all the examples I mentioned above have an order associated with them. Months of the year: January comes before February which comes before March etc... For days of week, Sunday is first (in some countries) and Saturday is last. If there is no order to your collection of items, it's likely not good candidate to be represented by an enum. 

Immutability

Enums can change and there is no reason why they shouldn't. Generally however, they should change rarely and they can never change during runtime. Months of the year are not a good example, but HTTP status codes is. It's extremely rare to get a new HTTP status code, but not impossible, thus a new library may include an updated enum with the additional HTTP code. You know for a fact that these will never change during runtime, but if you upgrade to a library you may be surprised to see some new HTTP codes for your pleasure (or hatred). My point being is that whatever you decide to represent as an enum, is statically defined at the start of the application, it can change but only after re-compiling a new version of the enum. 

I tried my best to describe an enum without using math derived theoretical computer science lingo here, as I find those definitions more or less impractical. Though I concede that my explanation is probably quite vague as well; nothing like a good example to the rescue.

See this content in the original post

The ordering in this Month enum is implicit through the order of defined elements. January gets index 0, February 1 etc... Each enum also gets a name, which is just the name of the element. What I defined here is what an enumeration is theoretically supposed to be. Nothing too fancy, but these values are constants so they can be used in switch case statements, and anywhere constants are required. Performance is great, since these are value types like ints and bools, no references, everything is stored on the stack, no memory management required. You also get automatic copy-by-value by virtue of the value types. This brings me back to something I mentioned above, enums are simple data structures, so you want copy-by-value to preserve data integrity and avoid insidious reference bugs, especially in multi-threaded environments.

Now the advanced part

It can be quite useful to attach more data and even functionality to enums besides just the ordinal index and name. I know this is definitely not what enums are for and there are many arguments one can make about this being an anti-pattern ¯\_(ツ)_/¯ I could not disagree more, and I'll try to explain why throughout this blog. Before I get to that though, let me explain what I really mean, and as usual the best way to do this is to use an example. I'll focus on using the Month example I've been using above. 

A month is a natural enumerated type, we already saw that. However, if we had a few more data points in our Month enum it would be quite useful. We could for example add a 1 based instead of 0 based ordinal index to our Month implementation to be more consistent with western Calendars. We could add the season the month belongs to. Maybe you can come up with even more month meta-data that would be useful but I'll stick to just those I mentioned. Imagine being able to do something like this: 

See this content in the original post

Here we are given a method which determines if the supplied function should be executed on the with the month as the argument, if and only if the month is a winter month. This is pretty and clean code, anyone can read this code and know exactly what it does. What we need here is a field Season on our Month object to be able to run this code. However, if our Month is an enum this is not possible with the built-in language constructs of C#, because as I mentioned, C#'s enums are simple enumerations without providing us the ability to extend and add fields. This is just one simple example of a use case where custom enum fields would be great. If Month was a class then there would be no problem with this code. However, we lose the benefits of an enumeration if we implement month as a class. 

What do we lose if Month is implemented as a class? We lose the ability to have constants for each available month, we lose the performance benefits of a value type, we lose the benefits of copy-by-value. It's not the end of the world, we can write our software without Month enum, but it won't be as elegant as it could be. This brings us to the proposed solution: implement Month as a sealed/final class with a set of constants defined for each of the enum elements with all the necessary meta-data fields, enforcing an explicit ordinal value and immutability, all statically initialized..phew! That was a mouthful, let's explore further.

Solution

This solution I'm proposing is not something I invented, as it turns out, it is kind of a pseudo standard way to work around the lack of advanced enum features in C#. I do have a small twist which I'm going to argue makes all the difference. 

The standard way to create a pseudo enum which exhibits many of the features of an enum without being limited by the language constraints is to create a class which can neither be inherited nor instantiated. No instance of this class can be created from outside the class itself. A private constructor is used to allow creating instances of this class. This is very similar to many factory patterns but not quite. Instead of providing a static factory method, we pre-create all the instances of this class during static initialization. Once initialized no more instances of this class can ever be created. The created instances are static and readonly and available anywhere the class is, but live inside the class itself as static class variables. 

For our Month example it would something like this:

See this content in the original post

This is a good start. We have a class which cannot be inherited or instantiated. We have enumerated all the allowed instances of this class as static readonly instances. No other instances of this class can ever be created. Additionally, each instance has 3 fields: Name, Index, Season. Name is the string representation of the month's name, index is the ordinal index, season is the season (I think you agree that Season should also be an enum!) 

This class satisfies our requirement of immutability, static initialization and it has explicit order. Additionally, we can refer to elements of our pseudo enum by name: Month.December . So almost all the benefits of an enum are satisfied, and in all honesty it's good enough for most applications. As a matter of fact I believe a lot of code exists witten this way. However, there are a few things missing. For one, this class is a reference type since it's a Class, so we don't get the benefits of having a value type. We also don't get automatic copy-by-value and implicit protection of reference leaks for multi-threads applications. Moreover, instances of this class can be null, so you do need to do null checks (nasty). Also, you could argue that someone using reflection could technically create more instances of this type which violates our enum requirements.

There is however a very simple way solve most (if not all) of these issues. Change the definition here from Class to struct, and viola you're done. OK fine, you're not quite done there are still issues still need addressing. Before we do that lets see, which issues from above we addressed. By switching the definition of Month to struct we turned it into a value type. We have effectively prevented inheritance and you can no longer use reflection to make new instances. We get automatic copy-by-value, we get stack storage instead of heap, we get protection against reference leakage and it's likely going to have (albeit minisculely) better performance. We also get to use the default() construct to create default values of this type, though this has issues which I'll get to in a bit. Oh and we get to use the crippled built-in C# optional. 

See this content in the original post

There is just one issue and that's how to deal with... defaults. With structs you can either use the new keyword or the default to create a new default instance of the struct. Structs do not allow explicit parameterless constructors and what actually happens behind the scenes when you use either of the above options, is that a new struct is created with each field of the struct set to it's natural default value. So in our case of Month we will get a new Month with null Name, 0 Index and null Season. This isn't ideal, but unfortunately there is no way around this.

What we can do however, is add a another property called IsDefault which returns a boolean and simply checks for the above condition:

See this content in the original post

So instead of doing null checks, which are now unnecessary because the struct can not be null you need to call the IsDefault property to see if this instance of the Month is initialized.

See this content in the original post

So our final struct based Month enum would look something like this:

See this content in the original post

I have added the source for this struct to a GitHub repo: https://github.com/dkhanaferov/cshartp-advanced-enums

There is a lot more code that shows the equality operators you need to add to your enum struct to make it really useful, which is out of scope of this post (it's already long enough). For example, you want to be able to do things like: 

See this content in the original post

In order to be able to that you need to provide custom Hashcode and Equals overrides which compare your enum values based on the private fields. And you also want to add operator overrides for ==, >, <, >=, <=.

Checkout the repo for examples of how to add the equality members, and there are also some unit tests which can be used as examples of usage of this struct. 

I hope this was useful to someone looking for examples of better enums in C#, and maybe some of this even made sense. If you liked this post, then stay tuned for more rants about C# and other topics. I'll be posting more about .NET Core and C# as I was mentioning earlier about being back in C# world, but without the Windows ecosystem.

 


If you want to be notified about these posts you can click the button below to sign up and you'll get a notification when a new post is available.

See this link in the original post

I promise, I won't send you spam :)