What's wrong with the enums we have now?
As I'm sure everyone reading this will know, enums are simple lightweight types
that enable you to define a set of named integral constants. An enum variable
can then be set to any one of these constants or (in the case of 'Flags' enums)
to a meaningful combination of them.
As such, enums perform a useful role in C# programming. However, they do have a
number of shortcomings:
- Their underlying types are always integers (int, long, byte etc). You can't have an enum with an underlying type of double, decimal, string or DateTime for example.
- You can assign any value of the underlying type to an enum variable, whether it corresponds to one of the defined constants or not.
- You can't add methods, properties or other types of member to an enum, though you can add methods to them indirectly using 'extension methods'. See my earlier article (http://www.c-sharpcorner.com/UploadFile/b942f9/5951/).
- Enum values often have to be cast back to their underlying types, which can be a nuisance in some scenarios - for example when they are used to index a collection.
What can we do about these shortcomings?
In this article, I'd like to demonstrate how we can build a 'generic' enum using
C# which:
- Can have any underlying type, except the type of the generic enum itself.
- Can only be assigned values which are actually defined.
- Can have other members apart from the enum constants, though needn't have.
- Has values which never have to be cast back to their underlying type, because they are in fact values of that type already.
All 'ordinary' enums inherit from an abstract
class called System.Enum which itself inherits from System.ValueType. Enums are
therefore value types themselves.
The System.Enum class contains a number of static methods which can be applied
to any enum, such as GetNames, GetValues, IsDefined, Parse and TryParse.
Enums also have some support from the C# language itself which conceals the way
they are actually implemented from the programmer. In particular, all enums have
a public instance field of their underlying type called 'value_' which contains
their current value but is hidden from view except when you use reflection:
using
System;
using
System.Reflection;
enum
Colors
{
Red,
Green,
Blue
}
class
Test
{
static void
Main()
{
Colors c =
Colors.Blue;
Type t =
typeof(Colors);
BindingFlags bf =
BindingFlags.Instance |
BindingFlags.Public;
FieldInfo fi = t.GetField("value__",
bf);
int val = (int)fi.GetValue(c);
Console.WriteLine((Colors)val);
// Blue
Console.ReadKey();
}
}
Our generic enum will also rely on an abstract class to provide basic
functionality and will itself need to be a class (i.e. a reference type),
because it's not possible for custom value types to inherit from anything other
than System.ValueType or System.Enum.
The abstract base class also needs to be a generic class which takes two type
parameters: the type of the 'enum' itself and its underlying type, so that it
can use reflection to obtain the names of the constants and their values.
However, the 'Value' property now needs to be explicit because, of course, our
generic enum will no longer have any language support. There are also explicit
'Name' and 'Index' properties. The latter records the position of each defined
value in the 'names' or 'values' collection and will normally correspond to the
order in which they're defined. The only instance field which a generic enum
needs and has is the _index field (exposed by the Index property) because the
other properties are deducible from this.
As with ordinary enums, it is possible for more than one constant to have the
same value. However, combinations of values are not supported.
How is the abstract base class implemented?
The code for the GenericEnum base class is included in the download which
accompanies this article but, for the benefit of those who don't like
downloading anything from the Internet, here it is 'in the flesh' :-
using
System;
using
System.Collections.Generic;
using
System.Reflection;
public
abstract class
GenericEnum<T, U>
where T : GenericEnum<T, U>,
new()
{
static readonly
List<string>
names;
static readonly
List<U> values;
static bool
allowInstanceExceptions;
static GenericEnum()
{
Type t =
typeof(T);
Type u =
typeof(U);
if (t == u)
throw new
InvalidOperationException(String.Format("{0}
and its underlying type cannot be the same", t.Name));
BindingFlags bf =
BindingFlags.Static |
BindingFlags.Public;
FieldInfo[] fia = t.GetFields(bf);
names = new
List<string>();
values = new
List<U>();
for (int
i = 0; i < fia.Length; i++)
{
if (fia[i].FieldType == u && (fia[i].IsLiteral
|| fia[i].IsInitOnly))
{
names.Add(fia[i].Name);
values.Add((U)fia[i].GetValue(null));
}
}
if (names.Count == 0)
throw new
InvalidOperationException(String.Format("{0}
has no suitable fields", t.Name));
}
public static
bool AllowInstanceExceptions
{
get { return
allowInstanceExceptions; }
set { allowInstanceExceptions =
value; }
}
public static
string[] GetNames()
{
return names.ToArray();
}
public static
string[] GetNames(U value)
{
List<string>
nameList = new List<string>();
for (int
i = 0; i < values.Count; i++)
{
if (values[i].Equals(value))
nameList.Add(names[i]);
}
return nameList.ToArray();
}
public static
U[] GetValues()
{
return values.ToArray();
}
public static
int[] GetIndices(U value)
{
List<int>
indexList = new List<int>();
for (int
i = 0; i < values.Count; i++)
{
if (values[i].Equals(value))
indexList.Add(i);
}
return indexList.ToArray();
}
public static
int IndexOf(string
name)
{
return names.IndexOf(name);
}
public static U
ValueOf(string name)
{
int index = names.IndexOf(name);
if (index >= 0)
{
return values[index];
}
throw new
ArgumentException(String.Format("'{0}'
is not a defined name of {1}", name, typeof(T).Name));
}
public static
string FirstNameWith(U value)
{
int index = values.IndexOf(value);
if (index >= 0)
{
return names[index];
}
throw new
ArgumentException(String.Format("'{0}'
is not a defined value of {1}", value, typeof(T).Name));
}
public static
int FirstIndexWith(U value)
{
int index = values.IndexOf(value);
if (index >= 0)
{
return index;
}
throw new
ArgumentException(String.Format("'{0}'
is not a defined value of {1}", value, typeof(T).Name));
}
public static
string NameAt(int
index)
{
if (index >= 0 && index < Count)
{
return names[index];
}
throw new
IndexOutOfRangeException(String.Format("Index
must be between 0 and {0}", Count - 1));
}
public static U
ValueAt(int index)
{
if (index >= 0 && index < Count)
{
return values[index];
}
throw new
IndexOutOfRangeException(String.Format("Index
must be between 0 and {0}", Count - 1));
}
public static
Type UnderlyingType
{
get { return
typeof(U); }
}
public static
int Count
{
get { return
names.Count; }
}
public static
bool IsDefinedName(string
name)
{
if (names.IndexOf(name) >= 0)
return true;
return false;
}
public static
bool IsDefinedValue(U value)
{
if (values.IndexOf(value) >= 0)
return true;
return false;
}
public static
bool IsDefinedIndex(int
index)
{
if (index >= 0 && index < Count)
return true;
return false;
}
public static T
ByName(string name)
{
if (!IsDefinedName(name))
{
if (allowInstanceExceptions)
throw new
ArgumentException(String.Format("'{0}'
is not a defined name of {1}", name, typeof(T).Name));
return null;
}
T t = new T();
t._index = names.IndexOf(name);
return t;
}
public static T
ByValue(U value)
{
if (!IsDefinedValue(value))
{
if (allowInstanceExceptions)
throw new
ArgumentException(String.Format("'{0}'
is not a defined value of {1}", value, typeof(T).Name));
return null;
}
T t = new T();
t._index = values.IndexOf(value);
return t;
}
public static T
ByIndex(int index)
{
if (index < 0 || index >= Count)
{
if (allowInstanceExceptions)
throw new
ArgumentException(String.Format("Index
must be between 0 and {0}", Count - 1));
return null;
}
T t = new T();
t._index = index;
return t;
}
protected int
_index;
public int Index
{
get { return
_index; }
set
{
if (value
< 0 || value >= Count)
{
if (allowInstanceExceptions)
throw new
ArgumentException(String.Format("Index
must be between 0 and {0}", Count - 1));
return;
}
_index = value;
}
}
public string
Name
{
get { return
names[_index]; }
set
{
int index = names.IndexOf(value);
if (index == -1)
{
if (allowInstanceExceptions)
throw new
ArgumentException(String.Format("'{0}'
is not a defined name of {1}", value,
typeof(T).Name));
return;
}
_index = index;
}
}
public U Value
{
get { return
values[_index]; }
set
{
int index = values.IndexOf(value);
if (index == -1)
{
if (allowInstanceExceptions)
throw new
ArgumentException(String.Format("'{0}'
is not a defined value of {1}", value,
typeof(T).Name));
return;
}
_index = index;
}
}
public override
string ToString()
{
return names[_index];
}
}
How can I use this base class to create and instantiate a generic enum?
You first need to declare your 'enum' class and specify that it inherits from
the GenericEnum base class. For example:
class
Fractions :
GenericEnum<Fractions,
double>
{
public static
readonly double
Sixth = 1.0 / 6.0;
public static
readonly double
Fifth = 0.2;
public static
readonly double
Quarter = 0.25;
public static
readonly double
Third = 1.0 / 3.0;
public static
readonly double
Half = 0.5;
public double
FractionOf(double amount)
{
return this.Value
* amount;
}
}
class
Seasons :
GenericEnum<Seasons,
DateTime>
{
public static
readonly DateTime
Spring = new
DateTime(2011, 3, 1);
public static
readonly DateTime
Summer = new
DateTime(2011, 6, 1);
public static
readonly DateTime
Autumn = new
DateTime(2011, 9, 1);
public static
readonly DateTime
Winter = new
DateTime(2011, 12, 1);
}
public
class Planets
: GenericEnum<Planets,
Planet>
{
public static
readonly Planet
Mercury = new Planet(3.303e+23,
2.4397e6);
public static
readonly Planet
Venus = new Planet(4.869e+24,
6.0518e6);
public static
readonly Planet
Earth = new Planet(5.976e+24,
6.37814e6);
public static
readonly Planet
Mars = new Planet(6.421e+23,
3.3972e6);
public static
readonly Planet
Jupiter = new Planet(1.9e+27,
7.1492e7);
public static
readonly Planet
Saturn = new Planet(5.688e+26,
6.0268e7);
public static
readonly Planet
Uranus = new Planet(8.686e+25,
2.5559e7);
public static
readonly Planet
Neptune = new Planet(1.024e+26,
2.4746e7);
public bool
IsCloserToSunThan(Planets p)
{
if (this.Index
< p.Index) return true;
return false;
}
}
public
class Planet
{
public double
Mass { get; private
set; } // in
kilograms
public
double Radius { get;
private set; }
// in meters
public Planet(double
mass, double radius)
{
Mass = mass;
Radius = radius;
}
// universal gravitational constant (m^3 kg^-1 s^-2)
public
static double G = 6.67300E-11;
public double
SurfaceGravity()
{
return G * Mass / (Radius * Radius);
}
public double
SurfaceWeight(double otherMass)
{
return otherMass * SurfaceGravity();
}
}
Incidentally, the code for the Planets and Planet classes follows closely the
code for a well known example of an enum in the Java programming language.
For the defined values, you can use either compile time constants (for those
types which support them) or static readonly fields which are not evaluated
until runtime and then remain constant. Either will be acceptable to the base
class though it's recommended that you use one or the other (but not both!) when
defining a particular class. All such constants must be public.
You can instantiate a generic enum either by name, index or value. As I dislike
throwing exceptions when there is a reasonably graceful alternative, if you try
to instantiate a generic enum with an invalid parameter, then null is returned.
Similarly, if you try to change the value of a generic enum to an invalid value,
then the existing value is left unchanged. Anyone who would prefer to throw
exceptions in these cases, can do so by setting the generic enum's static
property, AllowInstanceExceptions, to true; it is false by default.
You can also use the constructor to instantiate an enum in which case an enum
instance is created with an initial value corresponding to an index of zero.
Here's an example which illustrates these points and also shows how some of the
static members in the base class are used:
class
Test
{
static void
Main()
{
Console.Clear();
Fractions f =
new Fractions();
Console.WriteLine(f);
// Sixth
f.Value = Fractions.Fifth;
Console.WriteLine(f.Index);
// 1
Fractions f2 =
Fractions.ByName("Third");
Console.WriteLine(f2.FractionOf(30));
// 10
f2.Index = 4;
Console.WriteLine(f2);
// Half
string name =
Fractions.FirstNameWith(0.25);
Console.WriteLine(name);
// Quarter
Fractions f3 =
Fractions.ByName("Tenth");
// no exception by default
Console.WriteLine(f3 ==
null); // true
f3 = Fractions.ByValue(1.0 /
3.0);
Console.WriteLine(f3);
// Third
Console.WriteLine();
foreach (string
season in Seasons.GetNames())
{
Console.WriteLine("{0}
starts on {1}", season, Seasons.ValueOf(season).ToString("d
MMMM"));
}
Console.WriteLine();
double earthWeight = 80;
double mass = earthWeight /
Planets.Earth.SurfaceGravity();
foreach (Planet
p in Planets.GetValues())
{
Console.WriteLine("Weight
on {0} is {1:F2} kg", Planets.FirstNameWith(p),
p.SurfaceWeight(mass));
}
Console.WriteLine();
Planets mercury =
Planets.ByName("Mercury");
Planets earth =
Planets.ByIndex(2);
Planets jupiter =
new Planets();
jupiter.Value = Planets.Jupiter;
Console.WriteLine("It
is {0} that Mercury is closer to the Sun than the Earth",
mercury.IsCloserToSunThan(earth)); // True
Console.WriteLine("It
is {0} that Jupiter is closer to the Sun than the Earth",
jupiter.IsCloserToSunThan(earth)); // False
Console.ReadKey();
}
}
Do generic enums have any shortcomings or limitations?
Inevitably, they do though I don't regard any of them as being particularly
serious:
Although they have a small memory footprint (4 bytes for the _index field), they have to be reference types rather than value types for the reasons already discussed. This means that they will require heap memory, including overhead, of 12 bytes (32 bit system) or 20 bytes (64 bit system) and will need to be tracked by the garbage collector. Memory will also be needed for the static fields in the abstract base class.
Due to the lack of built-in language support, instantiation is more awkward than one would like; if you instantiate by name, you have to use a string.
There is no 'Flags' enum support where the underlying type is integral, as I concluded that trying to replicate this would be too messy, given that it would be meaningless for non-integral types.
It's not possible to use the type of the generic enum itself as its underlying type due to initialization problems. The base class's static constructor inevitably runs before the enum's static readonly members have been initialized which means that they are all still null when they are being reflected upon.
Even though any other type can be used as the underlying type, in practice you might want to restrict this to structs and immutable reference types. If you use a mutable reference type, then the user could read the 'constant value' and change the state of the object itself.
Any kind of member can be added to a generic enum except, of course, for public constants or static readonly fields of the underlying type.
Should I always prefer generic enums to ordinary enums
in future?
No, ordinary enums are fine for most purposes and are very lightweight.
However, if you wish to use a non-integral underlying type or to include some
other members, then I hope that you will consider using a generic enum instead.