A Little Enum Tip
I'm sure you all know how to use Enums. But have you ever tried to take it just that little bit further?
I've played a little bit, as I've posted before.
Last night, I had an interesting situation where a stored procedure was returning me an Integer value return code, which was supposed to map to a correct enum value. The twist here is that the database development is done in parallel to the code development, by a different team. We work closely together, but sometimes accidents happen, and things get a little bit out of whack.
Now, one thing that is always good to do is to do a verification that the value you have is a legitimate value - in this case, we want to be sure that it really does map into the enum it's supposed to match. The code, before I tweaked it, was not very well optimised. Sure, the execution path was always pretty straightforward, but it was an ugly method...a sample might help.
Private Function IsValid(ByVal retCode As Int32) As MyEnum
Select Case retCode
Case MyEnum.Val1
Return MyEnum.Val1
Case MyEnum.Val2
Return MyEnum.Val2
Case MyEnum.Val3
Return MyEnum.Val3
End Select
Return MyEnum.Failed
End Function
This is a really bad example, but it gets the point across (this is AirCode!). But the method I was fixing (which I didn't write!) did something along these lines (there was a bit more logic involved, for example). Oh, and the Enum actually had about 25 or 30 values, so the Case statement was huge.
I hate seeing things like this. If your case statement gets more than 4 or 5 cases long, perhaps there's a completely different approach you need to try. In this case, there's no room to grow. Creating a new value in the enum means updating the enum _and_ this function.
So I worked a little magic, and reduced it down to this little sucker:
Private Function IsValid(ByVal retCode As Int32) As MyEnum
If System.Enum.IsDefined(GetType(MyEnum), retCode) Then
Return CType(retCode, MyEnum)
End If
Return MyEnum.Failed
End Function
This is a little better. Now, if we go and add more values to the Enum, this function needs be none the wiser. However, there's still more future proofing that can be done.
What if the enum gets changed to be a a Flags enum? A flags enum is one where you define your enum values in such a way that you can use bitwise operations on them, to hold more than one value at once. You generally do this by defining your values as exact powers of two (1, 2, 4, 8, 16 etc) and adding an FlagsAttribute attribute to the declaration of the enum.
Does a flags enum work with my latest method? Yes and no.
Yes, it does when you've got exact value matches. But it doesn't if you pass in a combined value where more than one enum value has been ORd together.
So if we modify the enum to look like this:
<Flags()> _
Public Enum MyEnum
Failed = 0
Val1 = 1
Val2 = 2
Val3 = 4
End Enum
Then we want the call to validate the value to work when we pass in 3, 5, 6 and 7 (as well as 0, 1, 2, and 4) but to fail when we pass in 8 or above. Of course, a bit of help from reflection was the key. Since we really do want to use reflection as little as possible, I wrote it in such a way that it only used reflection if simple tests failed first.
Here's my final result, future proofing even further by not even hard-coding to a specific enum type, but accepting any.
Public Function IsValid(ByVal poType As System.Type, ByVal plValue As Int32) As Boolean
Dim bRet As Boolean = System.Enum.IsDefined(poType, plValue)
If bRet = False Then
If poType.GetCustomAttributes(GetType(System.FlagsAttribute), False).Length > 0 Then
Dim lRet As Int32 = SumValues(poType)
If (plValue And lRet) = plValue Then
bRet = True
End If
End If
End If
Return bRet
End Function
Private Function SumValues(ByVal poType As System.Type) As Int32
Dim lRet As Int32 = 0
For Each lVal As Int32 In System.Enum.GetValues(poType)
lRet += lVal
Next
Return lRet
End Function
First, do the simple test. By my careful calculations a good chunk of the majority of the time (that's correct to within 3 decimal places) the value passed in will be of a simple nature, and the call to System.Enum.IsDefined() will return true.
Next, if the first test failed, then it's time to get serious. We test for the FlagsAttribute attribute on the type, to ensure that they really are a Flags enum - if not, then we have to give up and say without a doubt that the value is not a member of the enum.
Having successfully tested for the existence of the FlagsAttribute, we then get the sum of all values defined in the enum. The call to System.Enum.GetValues() in the SumValues() method returns an array of all values defined, and we simply sum them up.
If you AND a value with the sum of all allowed values and get your original value back as a result, then you have a perfect match, and all flags set within the value are valid. Yay!
This method now also simply returns a Boolean response. If it returns true, we know it's safe to cast the integer without an exception being thrown:
Dim eVal As MyEnum
Dim lVal As Int32 = 3
If IsValid(GetType(MyEnum), 3) Then
eVal = CType(lVal, MyEnum)
End If
Pretty cool huh?
There's actually at least a couple of bugs still in there. I've fixed it in mine, but I thought I'd leave it open for people to fight over. Any suggestions on what else to do to the IsValid() method to ensure that there's no exceptions thrown?
Listening to: nothing compares to you - me first and the gimme gimmes - (2:39)