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! :)
2 Answers 2
Review
IEquatable<T>
protected bool Equals(Enumeration other) { return string.Equals(Name, other.Name) && Value == other.Value; }
Two things about this method...
If you implemented it, don't downgrade it to a simple
protected
helper method. It belongs to theIEquatable<T>
interface so implement this one too.I find it's easier to implement the
Equals
pair by forwarting the call fromEquals(object other)
toEquals(T other)
becasue the latter is strongly typed.
Bang! NullReferenceException!
This other.Name
will blow if other
is null
so make sure to check all parts of the expression.
bool throwEx
You shouldn't use parameters that switch exeception throwing on/off. Instead remove the ParseImpl
and implement the parsing by one of the TryParse
methods then reuse it elsewhere and throw exception by Parse
methods if necessasry.
readonly
Remember to make your public fields of the derived class readonly
.
Readability
partial class
Partial classes are a great way for organizing code and I find there should be more of them. In fact each interface should be implemented by its own partial class to avoid using so many region
s.
- Parameter lists
private static IEnumerable<TEnumeration> AddValueToCache<TEnumeration>(Type key, IEnumerable<TEnumeration> value) where TEnumeration : Enumeration
I've never liked the style where some parameters are left in one line somehere far to the right and others are put in the new line far to the left... they should either all be on new lines or none of them.
private static IEnumerable<TEnumeration> AddValueToCache<TEnumeration>(
Type key,
IEnumerable<TEnumeration> value
) where TEnumeration : Enumeration
I find this is much easier to read.
- LINQ chains
var value = GetValues<TEnumeration>() .FirstOrDefault(entry => StringComparisonPredicate(entry.Name, name, ignoreCase));
A similar rule applies to LINQ chanins. Either no line breaks or all line breaks. Mixed styles are difficult to read.
var value =
GetValues<TEnumeration>()
.FirstOrDefault(entry =>
StringComparisonPredicate(
entry.Name,
name,
ignoreCase
)
);
This is (an extreme example of) how I think long expressions should be formatted if a single line too long.
var value =
GetValues<TEnumeration>()
.FirstOrDefault(entry => StringComparisonPredicate(entry.Name, name, ignoreCase));
Or just like that so that you can read it easily and naturally from
left to
right
and not from
right to
left to
right
Naming
You name the properties Name
and Value
but the parameters inconsistently name
and id
. They should be name
and value
too.
Improvements
You can greatly simplify your code by implementing generics. If you make your base class Enumeration<T>
you can remove all boilerplate code from the derived class to the new generic Enumeration<T>
. The compiler will generate static
fields and properties for each T
separately so you don't have to write them yourself anymore. Here's a short example.
(I removed all interfaces or other helpers like Parse
and TryParse
or operators for simplicity but they should be included in the actual implementaion later.)
public abstract partial class Enumeration<T>
{
private static readonly IDictionary<string, int> _valueCache = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
private static readonly IDictionary<int, string> _nameCache = new Dictionary<int, string>();
public string Name { get; }
public int Value { get; }
protected Enumeration(string name, int value)
{
Value = value;
Name = name;
_valueCache.Add(name, value);
_nameCache.Add(value, name);
}
public static IEnumerable<string> Names => _nameCache.Values;
public static IEnumerable<int> Values => _valueCache.Values;
// Examples only! TODO: add null checks or redicrect `IComparable<T>`
public static bool operator >(Enumeration<T> left, Enumeration<T> right) => left.Value > right.Value;
public static bool operator <(Enumeration<T> left, Enumeration<T> right) => left.Value < right.Value;
}
This is how your new class could be implemented. You have now two dictionaries to cache both the names and values for faster lookups.
It's all static! No need to write this code again and again and agian...
You also no longer need reflection because when you create the properties on your derived class, the names and values will automatically by added to both dictionaries.
public class TestEnumeration : Enumeration<TestEnumeration>
{
public readonly static TestEnumeration A = new TestEnumeration(nameof(A), 0);
public readonly static TestEnumeration B = new TestEnumeration(nameof(B), 1);
protected TestEnumeration(string name, int value)
: base(name, value)
{
}
}
This is what remains from your original class. I swithched the order of parameters form value
and name to name
and value
because I find this order more natural.
I also suggest implementing the IEquatable<Enumerable<T>>
interface by redirecting it to a custom implementation of IEqualityComparer<Enumerable<T>>
becuase it not only better encapsulates the logic but also has a nicer Equals
method taking two parameters for left
and right
rather then working with the invisible this
and other
.
Usage examples:
TestEnumeration.A.Dump(); // (A, 0)
TestEnumeration.Names.Dump(); // A, B
(TestEnumeration.A > TestEnumeration.B).Dump(); // False
(TestEnumeration.B > TestEnumeration.A).Dump(); // True
I like the idea of the Enumeration
class and I use similar solutions myself a lot (now improved) because they are often better then ordinary enum
s:
- they can have a lot more custom features
- it's easier to write extensions
- they're faster and more efficient in scenarios where otherwise boxing would be involved e.g.
Dictionar<string, object>
-
\$\begingroup\$ Thank you for your input, I'd like to hear more if possible. \$\endgroup\$Denis– Denis2018年03月31日 16:28:49 +00:00Commented Mar 31, 2018 at 16:28
-
-
\$\begingroup\$ It looks very good, one thing that kinda breaks it tho is how would you retrieve an instance of
Enumeration
derived class by it's name and value? For example theParse
method. Instantiating a new object doesn't seem right to me, what are your thoughts? Perhaps another cache? \$\endgroup\$Denis– Denis2018年04月01日 17:17:13 +00:00Commented Apr 1, 2018 at 17:17 -
\$\begingroup\$ @Denis oh, crap. Sure, you're right, the instance isn't in the cache! :-o I'd make the
Value
of the dictionary a tuple with the value it has now and the instance. It should cover all needs. As far asParse
is concerned you call it viaEnumeration<YourEnum>.Parse(...)
then it has access to both caches and can retrieve everthing. \$\endgroup\$t3chb0t– t3chb0t2018年04月01日 17:23:39 +00:00Commented Apr 1, 2018 at 17:23 -
\$\begingroup\$ Let me play with it for a bit, I just woke up, I feel like there might be some problems with overlapping names, values between different enumerations. I will get back to you possibly with a github repository or something. \$\endgroup\$Denis– Denis2018年04月01日 17:29:06 +00:00Commented Apr 1, 2018 at 17:29
After incorporating the suggestions by t3chb0t, the class now looks like this:
[DebuggerDisplay("{Name} = {Value}")]
public abstract partial class Enumeration<T>
where T : Enumeration<T>
{
private static readonly IDictionary<int, Enumeration<T>> _valueCache =
new Dictionary<int, Enumeration<T>>();
public static IDictionary<int, Enumeration<T>> ValueCache
{
get
{
RuntimeHelpers.RunClassConstructor(typeof(T).TypeHandle);
return _valueCache;
}
}
private static readonly IDictionary<string, Enumeration<T>> _nameCache =
new Dictionary<string, Enumeration<T>>(StringComparer.OrdinalIgnoreCase);
public static IDictionary<string, Enumeration<T>> NameCache
{
get
{
RuntimeHelpers.RunClassConstructor(typeof(T).TypeHandle);
return _nameCache;
}
}
public string Name { get; }
public int Value { get; }
public static IEnumerable<string> Names => NameCache.Keys;
public static IEnumerable<int> Values => ValueCache.Keys;
protected Enumeration(string name, int value)
{
Name = name;
Value = value;
ValueCache.Add(value, this);
NameCache.Add(name, this);
}
public static T Parse(string name)
{
if (TryParse(name, out var value))
{
return value;
}
throw new InvalidOperationException($"Requested value {name} was not found.");
}
public static bool TryParse(string name, out T value)
{
if (NameCache.TryGetValue(name, out var containedValue))
{
value = (T)containedValue;
return true;
}
value = default(T);
return false;
}
public static string Format(T value, string format)
{
return value.ToString(format);
}
public static bool IsDefined(string name)
{
return NameCache.ContainsKey(name);
}
}
public abstract partial class Enumeration<T> : 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);
}
public abstract partial class Enumeration<T> : IComparable<Enumeration<T>>
{
public int CompareTo(Enumeration<T> other)
{
if (ReferenceEquals(this, other)) return 0;
if (ReferenceEquals(null, other)) return 1;
return Value.CompareTo(other.Value);
}
}
public abstract partial class Enumeration<T> : IFormattable
{
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);
}
public abstract partial class Enumeration<T> : IEquatable<Enumeration<T>>
{
public bool Equals(Enumeration<T> other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Value == other.Value;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((Enumeration<T>)obj);
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
public static bool operator >(Enumeration<T> item1, Enumeration<T> item2) => item1.CompareTo(item2) > 0;
public static bool operator <(Enumeration<T> item1, Enumeration<T> item2) => item1.CompareTo(item2) < 0;
public static explicit operator Enumeration<T>(int value)
{
return (Enumeration<T>)Activator.CreateInstance(
typeof(T),
BindingFlags.NonPublic | BindingFlags.Instance, null,
new object[] { value.ToString(), value }, null);
}
}
The base class is now generic, which allows the derived class to have an extremely simplified creation.
All of the helper methods no longer need to declare a generic type argument as they can use the class' one. Also most of the overloads have been removed, due to now having 2 private dictionary caches, which allows for a StringComparer
to be passed during initialization, which also means that all Name
s will be treated using the StringComparer.OrdinalIgnoreCase
.
All of the interfaces' implementations have been separated into partial files. IEquatable<Enumeration<T>>
has been inherited and implemented along with some changes to the equality members as a whole. Now they take only the Value
of an object in to consideration. As multiple repetitive values are NOT allowed. It's intended to work this way because from my experience with enums
it's quite a headache to work with duplicate values inside the same enum
.
2 operators have been overridden <
&& >
and an explicit cast from int
to Enumeration<T>
has be added.
That's pretty much all the changes, the class looks a lot cleaner and definitely faster, because reflection is no longer needed, and we can make use of the constant time complexity of the dictionary's look up.
-
\$\begingroup\$ @t3chb0t, click for future reference. \$\endgroup\$Denis– Denis2018年04月01日 20:20:40 +00:00Commented Apr 1, 2018 at 20:20
Explore related questions
See similar questions with these tags.
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\$