Introduction
There's so many Enum definition in a real world C#/.NET project. And I found
that in many case we need each enum item has distinct value from others, but
the real truth is that chances exists developer define more than one enum
item by assign them the same value. That's the source of bug. And, to make
things further interesting, there's [Flags] Enum, which value should be 0x1,
0x2, 0x4 etc, while not something 0x7. This article illustrate the reusable
way to check these Enum-definition related things automatically by Reflection.
And apply these rules to an unit test framework is easy, such as NUnit.
Background
Before you can use NUnit test in the code, you should include
using NUnit.Framework;
in the preamble. And
Add a reference to the NUnit dll: nunit-framework.dll
, just this one
is really needed.
Test on by C# on .NET 1.1
The demo project itself is not runnable, just use NUnit to test it.
Motivation
Reflection on .NET open a door to many interesting things, in fact, the motivation
of this article lies in another story:
Localization for all the name of an enum:
public enum Unit
{
mm,
cm,
pt,
Inch,
}
That's very common in a drawing software. And I want to localize these item in
a string table, just keep the string table nearest to the enum is not sufficient for
sync the two things. I need to ensure that my software will notify the
developer if someone add a new supported unit to enum Unit while leave the corresponding
localization not-done.
public enum StringID
{
Unit_UI_mm,
Unit_UI_cm,
Unit_UI_pt,
Unit_UI_Inch,
}
I use the naming convention between the two enum. So I can check it by
foreach(string unit_name in Enum.GetNames( typeof(Unit) ) )
{
if( Enum.IsDefined( typeof(Unit), "Unit_UI_" + unit_name ) == false)
{
Debug.Assert(false, string.Format("need to add Localization for [{0}]", unit_name) );
}
}
Using the code
To use the code, just add the EnumTester.cs
file to your project,
then re-compile your assembly and then throw it to NUnit to test.
using System;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Collections;
using NUnit.Framework;
namespace EnumTester
{
[TestFixture(Description="Test Enum definition: duplicated value, or error-prone value for [Flags]ed Enum such as 3")]
public class EnumTest
{
[Test(Description="Test All the Enum types defined in this assembly")]
public void Enum_Check_All()
{
Assembly assem = Assembly.GetExecutingAssembly();
EnumTester_Common.check_assembly_enum_type( assem, EnumTest_Type.All );
}
}
The basic idea is to detect all the Enum types in the Assembly using reflection,
then check each Enum name definition and the values. If you known well about
your Enum and simply want to Skip the test, just add the following Attribute to your Enum
or to individual enum Item:
[Skip_Enum_CheckAttribute]
public enum I_Know_My_Enum_Well_Even_Its_error_prone
{
Name,
name,
s0,
sO,
others = 0,
}
public enum MyEnum
{
Name,
name,
[Skip_Enum_CheckAttribute]
s0,
sO,
others = 0,
}
Note the Attribute postfix for Skip_Enum_CheckAttribute is optional.
For the sake of refined control of the test, I include a Enum named
EnumTest_Type
to define precisely which type of Test should be
performed on the assembly. E.g., to detect value duplications, using
[Test(Description="Test All the Enum types defined in this assembly")]
public void Enum_Check_All()
{
Assembly assem = Assembly.GetExecutingAssembly();
EnumTester_Common.check_assembly_enum_type( assem, EnumTest_Type.Duplicated_Value );
}
Points of Interest
Check constant name different only by case
internal enum Enum_Diff_Only_Case
{
abcdefghijklmnopqrstuvwxyz,
ABCDEFGHIJKLMNOPQRSTUVWXYZ,
}
Check constan name different only by digit 0 and lower/upper character o
internal enum Enum_Digit_0_Upper_Char_O
{
Guess_digit_or_char_0,
Guess_digit_or_char_O,
}
Check constant name different only by digit 1 and lower character l
internal enum Enum_Digit_1_Lower_Char_l
{
Guess_digit_or_char_1,
Guess_digit_or_char_l,
}
Tricks, Tips and pitfalls of Enum
There's several article on the CodeProject talks about how to operation Enum,
especially by Reflection, here's the partial collection of the tricks and tips, and
of my own:
It's possible to add a comma after the last item in enum definition.
I think it's very convenient compared to C's rule:
int ia[] = {1, 2, 3, 4, };
The last comma only allowed in the above construct, not in enum. This little
improvement did make sense for enum, since mostly you want to re-group the enum
items.
value for the item derived from it's latest previous sibling
public enum WeekDay
{
SunDay ,
MonDay = 2,
TuesDay = 2,
Wednesday ,
Thursday ,
Friday = 100,
Saturday,
}
The above commented rules is same as C/C++.
Enum.GetNames( Type enumType) will return the names in the order of definition
Enum.GetName(Type enumType, object value) will also return first item in the above order in case of more than one item defined the same value
public enum EnumTest: int
{
None = 0,
One = 0,
}
Console.WriteLine( Enum.GetName(typeof(EnumTest), (int)0 ) );
public enum EnumTest: int
{
One = 0,
None = 0,
}
Console.WriteLine( Enum.GetName(typeof(EnumTest), (int)0 ) );
0 is special Enum value
You can assign 0 to a Enum variable directly, other value not allowed:
MyEnum val = 0;
val = 1;
When cast is needed, always use (MyEnum) syntax, GetObject() as MyEnum not allowed
Enum is value type. "as" only applies to reference type. This is a general rule
for all the value types, not limited to Enum.
Define the underlying type of enum
public enum MyColors: long
{
...,
}
int is the default at least for C#, it's ok in the most case. I want to repeat
myself here that char is *NOT* the legal underlying type for enum.
Get the underlying type of enum, and Unmanaged sizeof of enum
Type underlying_type = MyEnum.GetUnderlyingType();
int unmanaged_size = System.Runtime.InteropServices.Marshal.SizeOf(underlying_type);
int unmanaged_size = System.Runtime.InteropServices.Marshal.SizeOf( MyEnum );
C# compiler won't give any warning about an error-prone [Flags] Enum value even csc /w:4 is specified
[Flags]
public enum MyEnum
{
First = 0x1;
Second = 0x3;
}
Commandments from [Effective C#](Item 8): Make 0 a valid value for enum
Simply saying a value type will be initialized to 0 by default. For more
discuss please refer the book.
Get all the defined Enum constants:
string[] all_names = Enum.GetNames( typeof(MyEnum) );
or, there's harder way to do the same thing:
ArrayList all_names = new ArrayList();
Type t = typeof(MyEnum);
FieldInfo[] fields = t.GetFields( BindingFlags.Public | BindingFlags.Static);
foreach(FieldInfo f in fields)
{
all_names.Add( f.Name );
}
To detect whether a given type is a Enum type
Type t = null;
if( t.BaseType == typeof(Enum) ) {...}
The legal underlying types for Enum:
error CS1008: Type byte, sbyte, short, ushort, int, uint, long, or ulong expected
I get it by try-error, so you needn't. Note that char is *NOT* a valid underlying
type for Enum.
How to get an Enum variable's instance by it's Name?
enum_val = (MyEnum_WeekDay) TypeDescriptor.GetConverter(enum_val).ConvertFrom("Sunday");
I customed to the following way for a long time, but don't know the above way,
fortunatly, suggested by Dactyl (thank you!), the later is 3-times faster
than using the TypeConverter class.
Enum.Parse( typeof(MyEnum_WeekDay), "Sunday") ;
How to get an Enum variable's instance by a int value?
If you can access the Enum type at compile time, that's easier:
MyEnum val = (MyEnum)300;
Console.WriteLine( ((int)val).ToString() );
No exception will be thrown even 300 is not a defined value.
It's harder when not knowing the compile time Enum type:
Convert.ChangeType( (int)300, runtime_enum_type );
But this will result in the following exception:
System.InvalidCastException: Invalid cast from System.Int32 to Client+TestEnum.
Even the underlying type of runtime_enum_type is exactly int.
Note it's ok in the reverse conversion:
Convert.ChangeType( enum_val, typeof(long) );
Even that long is not the underlying type of the Enum.
Maybe there's an easy way and I just miss it(tell me if there's), here's my own:
Type underlying_type = t.GetUnderlyingType();
string enum_name =
Enum.GetName(t, Convert.ChangeType( int_value, underlying_type ) );
Enum.Parse(t, enum_name );
Note that change the raw decimal number to the real underlying type is a must,
otherwise the following exception will be thrown:
System.ArgumentException: Enum underlying type and the object must be same type or object
must be a String. Type passed in was System.Int64; the enum underlying type was System.In
t16.
The same rule also apply to the following method:
static Enum.IsDefined( Type enum_type, object obj)
The valid type of the obj is either string or the exact underlying type of the
Enum. Of course for the string it's should be the Enum Constant. The above
snippet can be used to detect whethter a given string or int/short etc is a
defined item in the Enum.
History
This is my first post on CodeProject. Hope this will be useful to you.