Lately I've been using a lot of enums in my project and the lack of generic Enum
constraint seems to be quite problematic. This combined with few other headaches caused by the enums, made look for alternative solutions.
I came up with the following abstract Enumeration class:
Enumeration
public abstract partial class Enumeration : IConvertible, IComparable, IFormattable
{
public string Name { get; }
public int Value { get; }
protected Enumeration(int id, string name)
{
Value = id;
Name = name;
}
#region Equality members
public override bool Equals(object obj)
{
var otherValue = obj as Enumeration;
if (otherValue == null)
{
return false;
}
var typeMatches = GetType() == obj.GetType();
var valueMatches = Value.Equals(otherValue.Value);
return typeMatches && valueMatches;
}
protected bool Equals(Enumeration other)
{
return string.Equals(Name, other.Name) && Value == other.Value;
}
public override int GetHashCode()
{
unchecked
{
return ((Name != null ? Name.GetHashCode() : 0) * 397) ^ Value;
}
}
#endregion
#region Implementation of IComparable
public int CompareTo(object other)
{
return Value.CompareTo(((Enumeration)other).Value);
}
#endregion
#region ToString methods
public string ToString(string format)
{
if (string.IsNullOrEmpty(format))
{
format = "G";
}
if (string.Compare(format, "G", StringComparison.OrdinalIgnoreCase) == 0)
{
return Name;
}
if (string.Compare(format, "D", StringComparison.OrdinalIgnoreCase) == 0)
{
return Value.ToString();
}
if (string.Compare(format, "X", StringComparison.OrdinalIgnoreCase) == 0)
{
return Value.ToString("X8");
}
throw new FormatException("Invalid format");
}
public override string ToString() => ToString("G");
public string ToString(string format, IFormatProvider formatProvider) => ToString(format);
#endregion
#region Implementation of IConvertible
TypeCode IConvertible.GetTypeCode() => TypeCode.Int32;
bool IConvertible.ToBoolean(IFormatProvider provider) => Convert.ToBoolean(Value, provider);
char IConvertible.ToChar(IFormatProvider provider) => Convert.ToChar(Value, provider);
sbyte IConvertible.ToSByte(IFormatProvider provider) => Convert.ToSByte(Value, provider);
byte IConvertible.ToByte(IFormatProvider provider) => Convert.ToByte(Value, provider);
short IConvertible.ToInt16(IFormatProvider provider) => Convert.ToInt16(Value, provider);
ushort IConvertible.ToUInt16(IFormatProvider provider) => Convert.ToUInt16(Value, provider);
int IConvertible.ToInt32(IFormatProvider provider) => Value;
uint IConvertible.ToUInt32(IFormatProvider provider) => Convert.ToUInt32(Value, provider);
long IConvertible.ToInt64(IFormatProvider provider) => Convert.ToInt64(Value, provider);
ulong IConvertible.ToUInt64(IFormatProvider provider) => Convert.ToUInt64(Value, provider);
float IConvertible.ToSingle(IFormatProvider provider) => Convert.ToSingle(Value, provider);
double IConvertible.ToDouble(IFormatProvider provider) => Convert.ToDouble(Value, provider);
decimal IConvertible.ToDecimal(IFormatProvider provider) => Convert.ToDecimal(Value, provider);
DateTime IConvertible.ToDateTime(IFormatProvider provider) => throw new InvalidCastException("Invalid cast.");
string IConvertible.ToString(IFormatProvider provider) => ToString();
object IConvertible.ToType(Type conversionType, IFormatProvider provider)
=> Convert.ChangeType(this, conversionType, provider);
#endregion
}
public abstract partial class Enumeration
{
private static readonly Dictionary<Type, IEnumerable<Enumeration>> _allValuesCache =
new Dictionary<Type, IEnumerable<Enumeration>>();
#region Parse overloads
public static TEnumeration Parse<TEnumeration>(string name)
where TEnumeration : Enumeration
{
return Parse<TEnumeration>(name, false);
}
public static TEnumeration Parse<TEnumeration>(string name, bool ignoreCase)
where TEnumeration : Enumeration
{
return ParseImpl<TEnumeration>(name, ignoreCase, true);
}
private static TEnumeration ParseImpl<TEnumeration>(string name, bool ignoreCase, bool throwEx)
where TEnumeration : Enumeration
{
var value = GetValues<TEnumeration>()
.FirstOrDefault(entry => StringComparisonPredicate(entry.Name, name, ignoreCase));
if (value == null && throwEx)
{
throw new InvalidOperationException($"Requested value {name} was not found.");
}
return value;
}
#endregion
#region TryParse overloads
public static bool TryParse<TEnumeration>(string name, out TEnumeration value)
where TEnumeration : Enumeration
{
return TryParse(name, false, out value);
}
public static bool TryParse<TEnumeration>(string name, bool ignoreCase, out TEnumeration value)
where TEnumeration : Enumeration
{
value = ParseImpl<TEnumeration>(name, ignoreCase, false);
return value != null;
}
#endregion
#region Format overloads
public static string Format<TEnumeration>(TEnumeration value, string format)
where TEnumeration : Enumeration
{
return value.ToString(format);
}
#endregion
#region GetNames
public static IEnumerable<string> GetNames<TEnumeration>()
where TEnumeration : Enumeration
{
return GetValues<TEnumeration>().Select(e => e.Name);
}
#endregion
#region GetValues
public static IEnumerable<TEnumeration> GetValues<TEnumeration>()
where TEnumeration : Enumeration
{
var enumerationType = typeof(TEnumeration);
if (_allValuesCache.TryGetValue(enumerationType, out var value))
{
return value.Cast<TEnumeration>();
}
return AddValueToCache(enumerationType, enumerationType
.GetFields(BindingFlags.Public | BindingFlags.Static)
.Select(p => p.GetValue(enumerationType)).Cast<TEnumeration>());
}
private static IEnumerable<TEnumeration> AddValueToCache<TEnumeration>(Type key,
IEnumerable<TEnumeration> value)
where TEnumeration : Enumeration
{
_allValuesCache.Add(key, value);
return value;
}
#endregion
#region IsDefined overloads
public static bool IsDefined<TEnumeration>(string name)
where TEnumeration : Enumeration
{
return IsDefined<TEnumeration>(name, false);
}
public static bool IsDefined<TEnumeration>(string name, bool ignoreCase)
where TEnumeration : Enumeration
{
return GetValues<TEnumeration>().Any(e => StringComparisonPredicate(e.Name, name, ignoreCase));
}
#endregion
#region Helpers
private static bool StringComparisonPredicate(string item1, string item2, bool ignoreCase)
{
var comparison = ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
return string.Compare(item1, item2, comparison) == 0;
}
#endregion
}
It's separated into 2 files -
1 responsible for the core logic, interface implementations, protected members, etc. and 1 responsible for the static helper methods such as Parse
, TryParse
, etc.
I have implemented all of the interfaces that the Enum
class usually has. There are 2 properties to access the Name
and the Value
of the entry. Currently there is no support for the Flags
attribute, but I might work this in if I find it necessary.
For the GetValues
method I decided to use private cache to speed further lookups, as the method uses reflection which tends to be slow and I've tried to limit it's usage to only the first time for each enumeration type.
Some methods that are present in the Enum
class have been omitted, since they don't make much sense, e.g Enum.GetName()
my enumeration entries already have such property, unless the idea was to pass some value equal to the underlying type and retrieve the name, but still I find it rather pointless and easily achievable through the other available methods.
Speaking of underlying type, there currently isn't any besides Int32
. This could be easily changed by adding a generic type argument to the class itself, but I don't feel like it's necessary, because I rarely find the need to use different underlying type for an enum, perhaps a Int64
would serve me better, but for now it's just Int32
.
Example implementation
public class TestEnumeration : Enumeration
{
public static TestEnumeration A = new TestEnumeration(0, nameof(A));
public static TestEnumeration B = new TestEnumeration(1, nameof(B));
//...
private static readonly IEnumerable<TestEnumeration> _test;
protected TestEnumeration(int id, string name)
: base(id, name)
{
}
static TestEnumeration()
{
_test = GetValues<TestEnumeration>();
}
public static IEnumerable<TestEnumeration> Values()
{
foreach (var entry in _test)
{
yield return entry;
}
}
}
Notice the usage of the static constructor. It allows the Enumeration
class to cache the values as soon as possible, resulting in slightly better performance.
A method template would be extremely useful in this situation, as we can get rid of most of the reflection if we can guide the derived classes' usage, but since some of the logic is in static context this doesn't seem to be possible to me.
ANY feedback is welcome! :)
Enumeration
is a reference type, so code that uses it may need to take null-references into account. \$\endgroup\$Value
which is an integer type, null check wont be required, there is no point of checking if the whole object itself is null or not as if it is null, it's simply a mistake on the supplier's end. That's not the intended usage of the class. It is indeed possible to screw with it, but avoiding that from the class itself would add way too much redundant code for the intended, average user. \$\endgroup\$null
instead of anEnumeration
instance may not be intended usage, but it can - and in practice, likely will - happen. It introduces a potential source of problems that you don't have with enums. I'm not saying that such a trade-off isn't worth it - that depends on how and where you intend to use this - but it's certainly something I would take into account. \$\endgroup\$