Introduction
Every time you have a set of ordinal values that are related, it is a good practice to put them in an enumeration rather than leave them as constant values.
Enumerations have been around at least since C++ and .NET continues to do so and has even added new features like flags attribute for the type or description for the elements.
Background
Let's consider a simple C# enumeration:
enum myenum : byte
{
a = 1,
b = 2,
c = 3
}
You can easily assign a value that is the type of the underlying type like:
myenum eval = default(myenum);
eval = (myenum)4;
The problem lies in the fact that 0
and 4
are not part of myenum
, and the compiler or the runtime won't complain about it and that can cause trouble when they are used in your code. It appears that the only check is against the underlying type, not the enum
.
In order to check for the values, you can use the static Enum.IsDefined
method that would validate the enum
value. But things get more complicated when dealing with enum
s with flags like below:
[Flags]
enum MultiHue : sbyte
{
[Description("no Color")]Black = 0,
[Description("pure red")]Red = 1,
[Description("pure green")]Green = 2,
[Description("pure blue")]Blue = 4,
[Description("composite green + blue")]Cyan = Green | Blue,
[Description("composite red + blue")]Magenta = Red | Blue,
[Description("composite red + blue + green")]White = Red | Blue | Green,
[Description("not a valid color")]BadColor = 127
};
If you run Enum.IsDefined(typeof(MultiHue),3)
, the return value is false
, but while this is true
, for flags you might expect a different outcome since 3
is 1 | 2
, so it should be a valid value. Even the ToString()
instance method would return 'Red
, Green
', not the number 3
as an undefined value.
Validate the Value of the Enumeration
Starting with C# 3.0, in its quest to improve usability, Microsoft introduced the extension methods that we can also use as an elegant way to validate the enum
s value.
For an enumeration with no flags, the validation is built in the framework, but using this language feature makes it more readable:
public static bool IsDefined(this System.Enum value)
{
return System.Enum.IsDefined(value.GetType(), value);
}
Using this extension method, validation becomes very elegant:
MultiHue mh = MultiHue.Blue;
bool isdefined = mh.IsDefined();
Validating an enum
with flags is a little more complicated since the IsDefined
method is not enough to make the difference between Defined
and Valid
:
public static bool IsValidEnumValue(this System.Enum value)
{
if (value.HasFlags())
return IsFlagsEnumDefined(value);
else
return value.IsDefined();
}
private static bool IsFlagsEnumDefined(System.Enum value)
{
Type underlyingenumtype = Enum.GetUnderlyingType(value.GetType());
switch (Type.GetTypeCode(underlyingenumtype))
{
case TypeCode.Int16:
case TypeCode.Int32:
case TypeCode.Int64:
case TypeCode.SByte:
case TypeCode.Single:
{
object obj = Activator.CreateInstance(underlyingenumtype);
long svalue = System.Convert.ToInt64(value);
if (svalue < 0)
throw new ArgumentException(
string.Format("Can't process negative {0} as {1}
enum with flags", svalue, value.GetType().Name));
}
break;
default:
break;
}
ulong flagsset = System.Convert.ToUInt64(value);
Array values = Enum.GetValues(value.GetType());
int flagno = values.Length - 1;
ulong initialflags = flagsset;
ulong flag = 0;
while (flagno >= 0)
{
flag = System.Convert.ToUInt64(values.GetValue(flagno));
if ((flagno == 0) && (flag == 0))
{
break;
}
if ((flagsset & flag) == flag)
{
flagsset -= flag;
if (flagsset == 0)
return true;
}
flagno--;
}
if (flagsset != 0)
{
return false;
}
if (initialflags != 0 || flag == 0)
{
return true;
}
return false;
}
public static bool HasFlags(this System.Enum value)
{
return value.GetType().GetCustomAttributes(typeof(System.FlagsAttribute),
false).Length > 0;
}
The workhorse of the validation is the IsFlagsEnumDefined
method which is a modified Enum.InternalFlagsFormat
that can be obtained using the reflector.
Converting a Variable to an Enum
So far, I've been dealing with enum
values that were validated after a value has been assigned. Since prevention sometimes is better than a cure, let's look at some conversion issues. If you are in a checked block and use casting for an integral value like below:
checked
{
MultiHue mh = (MultiHue)(object)128;
int i = 128;
mh = (MultiHue)i;
}
An OverflowException
or InvalidCastException
will be thrown if a conversion to the underlying type of the enum
cannot be performed.
If the block was unchecked, there will be no exception and the mh
value will become the default value of the underlying type, and that is always 0
.
If you use the static Enum.Parse
method and the parameter is out of bounds, you will have the OverflowException
thrown regardless of the checked or uncheck mode.
MultiHue mh = (MultiHue)Enum.Parse(typeof(MultiHue),"128");
Hence I've created a method to deal with enum
conversion without throwing exceptions for usual situations. Notice that converting between different enum
types is not a slam dunk because you cannot take advantage of the native Enum.Parse
and object
's ToString
methods.
public static bool SafeConvertToEnum(object value, out EnumType retv)
{
Type enumType = typeof(EnumType);
if (!enumType.IsEnum)
throw new System.ArgumentException(string.Format("{0} is not an Enum.",
enumType.Name));
if (value == null)
{
retv = default(EnumType);
return false;
}
Type valType = value.GetType();
bool isString = valType == typeof(string);
bool isOrdinal = valType.IsPrimitive ||
typeof(decimal) == valType || valType.IsEnum;
if (!isOrdinal && !isString)
throw new System.ArgumentException
(string.Format("{0} can not be converted to an enum", valType.Name));
try
{
checked
{
if (valType == Enum.GetUnderlyingType(enumType))
retv = (EnumType)value;
else
{
if(isString)
retv = (EnumType) Enum.Parse(typeof(EnumType), value as string);
else
if (valType.IsEnum)
{
Enum en = (Enum)value;
object zero = Activator.CreateInstance(valType);
value = (en.CompareTo(zero) >= 0)?
Convert.ToUInt64(value):Convert.ToUInt64(value);
}
retv = (EnumType)Enum.Parse(typeof(EnumType), value.ToString());
}
}
if (!((System.Enum)(object)retv).IsValidEnumValue())
{
retv = default(EnumType);
return false;
}
}
catch(ArgumentException)
{
retv = default(EnumType);
return false;
}
catch (OverflowException)
{
retv = default(EnumType);
return false;
}
catch (InvalidCastException)
{
retv = default(EnumType);
return false;
}
catch (Exception ex)
{
throw new System.ArgumentException(string.Format
("Can't convert value {0}\nfrom the type of {1}
into the underlying enum type of {2}\nbecause {3}",
value, valType.Name, Enum.GetUnderlyingType(enumType).Name,
ex.Message), ex);
}
return true;
}
Since I've used reflection quite intensely to make this safe conversion, I think that performance was the reason why Microsoft did not make enumeration casting 'boiler plate'. The method usage should be straight forward:
MultiHue mh;
bool res = EnumHelper.SafeConvertToEnum("3", out mh);
res = EnumHelper.SafeConvertToEnum(3, out mh);
EnumHelper
is a generic static
class I've created to manipulate enum
s.
You will find that it contains other useful methods that you can use when working with enum
s using generics syntax. There is no support for enum
as a generic parameter, so I've tried to use the abstract System.Enum
class in the constraint clause. Since that is the base class for all of the defined enum
s, one would expect it should work, but unfortunately the compiler complains about it: Constraint cannot be special class 'System.Enum
'.
To reach a compromise, I put the interfaces any enum
implements in the where
clause:
public static class EnumHelper < EnumType >
where EnumType : struct, IComparable, IConvertible, IFormattable
{........
The other extension methods like GetDescription()
that add syntactical sugar to the
enum
s are in a non generic static
class called EnumExtenders
.
Using the Code
In order to use this code, you have to add the namespace to your code...
using CTSExtenders;
... and add a reference to CTSExtenders.dll, or better yet to the project CTSExtenders
. If you don't use C# 3.0 yet, extension methods won't compile, but you can remove the 'this
' parameter modifier and still use this functionality like any classic static
method.
History