Today’s story is about how careful we should be while coding. So, I’m writing this text because a strange number occurred on the test results list. And because I’d like to tell a bit about enums definitions in C#.
Strange numbers on the test results list
While testing I noticed that all test cases passed, but the execution times are a bit strange. Most cases run below 0ms but one was a lot slower and took over 25ms. The method under test was supposed to be run all over again, so its performance is maybe not critical, but certainly important.
Quickly I realized that this one method is run longer just because it’s first in the row, so the testing framework probably has to warm up and that’s why delay occurs. But at the same time I recalled that some time ago, during the discussion with Eric Lippert (PL), I noticed the slow behaviour of some Enum static methods. I decided to go deeper and after investigation I’m ready to share results with you.
The tested method was an extension for InvoicePaymentMethods enum. This type is a bit special because instead of defining its members and corresponding integers/shorts, characters are assigned.
The characters correspond with values in the database, so previously we had to compare chars, now we can compare enums. Great readability improvement.
How to say if the enum is defined?
Let’s say that we want to check whether the value from the database corresponds with one of our enum members. Easy as “valueFromDb == InvoicePaymentMethods.Undefined”, right?
Not exactly. This will work only if the valueFromDb is ‘/0’, which may be sometimes not true. It should be, but the practice shows that sometimes value may be, for example ‘Ż’. Or ‘8’, ‘[‘ etc. What will happen then?
As we can see, when an undefined value is cast to enum, no exception occurs. The variable will hold an integer value which may cause troubles in further processing. Same with comparison – it’s possible to compare an unexpected value with our Undefined member. It will return “false”, which may lead to the feeling that the value is OK. How to prevent that?
Enum extension method
Enums are value types that behave as a set of constants with the underlying integer values. There is no option to extend this type with methods or overload their operators. But one thing that we can do is to define extension methods for them. And this is what I initially did:
Simple and elegant. And works too. I could skip the initial check, as it’s not critical, but it was useful for debugging purposes.
Now you know what was the background of the issue. I have an enum with underlying characters, I use Enum.IsDefined to make checks and I remember that some time ago I’ve heard something bad about them. What’s the plan then?
First I have to figure out what’s the other possible options to check whether the enum value is defined. Then I have to compare them somehow. Possibly the best by measure the execution times 😊
Three options to check if the enum is (un)defined,
The first option is implemented. Second, the simplest is to check variables against all defined values. The third defines collection that holds all defined types and then use it to check the variable. Of course, the best collection type for this purpose is generic HashSet with access complexity O(1).
Implementation was quite straightforward:
Some time ago I used a simple framework Stopwatch class to measure performance. But later I found an interesting topic on StackOverflow and since that time I use this piece of code for this purpose. Of course, sometimes I have to modify it (like today).
… and the measurements themselves
When I run the code now, I can clearly see the results:In short, the fastest option is to directly compare with all defined members – about 4.2 units. HashSet is 1.37 times slower, and Enum.IsDefined is stunning 11.3 times slower.
And the winner is…
Do you expect that direct comparison is a winner? Not exactly. If we consider times only, then yes. But remember about code maintenance. If the enum is old and well defined, the probability that it will be extended with a new member is low. But for new types, it may happen sometimes.
HashSet approach first loads all defined members automatically, when direct comparison must be defined manually and if new member arrives it must be added to the comparison.
We may minimalize the chance of forgetting about this with unit tests. This is the example of a test that checks whether method IsUndefined checks all members correctly:
Such a guard let us use the fastest method without worries that we will miss to update the method in the future.