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.
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);//that is actually 0
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 enums with flags like below:
[Flags]// Define an Enum with FlagsAttribute.
enum MultiHue : sbyte
{
[Description("no Color")]Black = 0,
[Description("pure red")]Red = 1,
[Description("pure green")]Green = 2,
//[Description("pure yellow")]Yellow = 3,
//Green | Red number 3 is deliberately omitted
[Description("pure blue")]Blue = 4,
[Description("composite green + blue")]Cyan = Green | Blue,//6
[Description("composite red + blue")]Magenta = Red | Blue,//5
[Description("composite red + blue + green")]White = Red | Blue | Green,//7
[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.
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 enums 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)
{// modeled after Enum's InternalFlagsFormat
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());//.Cast ().ToArray ();
int flagno = values.Length - 1;
ulong initialflags = flagsset;
ulong flag = 0;
//start with the highest values
while (flagno >= 0)
{
flag = System.Convert.ToUInt64(values.GetValue(flagno));
if ((flagno == 0) && (flag == 0))
{
break;
}
//if the flags set contain this flag
if ((flagsset & flag) == flag)
{
//unset this 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.
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 has the underlying type of sbyte: -127...127
MultiHue mh = (MultiHue)(object)128;//InvalidCastException
int i = 128;
mh = (MultiHue)i;//OverflowException
}
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 has the underlying type of sbyte: -127...127
MultiHue mh = (MultiHue)Enum.Parse(typeof(MultiHue),"128");//OverflowException
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;
//for strings
bool res = EnumHelper .SafeConvertToEnum("3", out mh);
//for ordinal values
res = EnumHelper .SafeConvertToEnum(3, out mh);
EnumHelper is a generic static class I've created to manipulate enums.
You will find that it contains other useful methods that you can use when working with enums 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 enums, 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 theenums are in a non generic static class called EnumExtenders.
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.
| You must Sign In to use this message board. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||