Creating Generic Enums using C#

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 (https://www.c-sharpcorner.com/uploadfile/b942f9/how-to-add-methods-to-an-enum-in-C-Sharp/).
  • 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.


Recommended Free Ebook
Similar Articles