I am developing my own kind of Enumeration like SmartEnum
but trying to prevent the user to provide a recursive-ish type definition like below:
public sealed class TestEnum : SmartEnum<TestEnum> { public static readonly TestEnum One = new TestEnum(nameof(One), 1); public static readonly TestEnum Two = new TestEnum(nameof(Two), 2); public static readonly TestEnum Three = new TestEnum(nameof(Three), 3); private TestEnum(string name, int value) : base(name, value) { } }
I ended up on this code:
public static class EnumerationHelpers
{
public static TEnumeration ParseName<TEnumeration>(string name)
where TEnumeration : Enumeration
{
return EnumerationCacheProvider<TEnumeration>.Get().Names[name];
}
// Lack of inference from the compiler, cannot write
// public static TEnumeration ParseValue<TEnumeration, TValue>(TValue value)
// where TEnumeration : Enumeration
public static TEnumeration ParseValue<TEnumeration>(object value)
where TEnumeration : Enumeration
{
return EnumerationCacheProvider<TEnumeration>.Get().Values[value];
}
public static IEnumerable<TEnumeration> GetAll<TEnumeration>()
where TEnumeration : Enumeration
{
return EnumerationCacheProvider<TEnumeration>.Get().All;
}
public static IEnumerable<object> GetValues<TEnumeration>()
where TEnumeration : Enumeration
{
return EnumerationCacheProvider<TEnumeration>.Get().Values.Keys;
}
public static IEnumerable<string> GetNames<TEnumeration>()
where TEnumeration : Enumeration
{
return EnumerationCacheProvider<TEnumeration>.Get().Names.Keys;
}
}
internal class EnumerationCache<TEnumeration>
where TEnumeration : Enumeration
{
public readonly IReadOnlyCollection<TEnumeration> All;
public readonly IReadOnlyDictionary<string, TEnumeration> Names;
public readonly IReadOnlyDictionary<object, TEnumeration> Values;
public EnumerationCache(IEnumerable<TEnumeration> source)
{
var list = source.ToList();
var names = new Dictionary<string, TEnumeration>();
var values = new Dictionary<object, TEnumeration>();
foreach (var item in list)
{
// First definitions found are subsequently overriden
if (!names.ContainsKey(item.Name))
{
names[item.Name] = item;
}
if (!values.ContainsKey(item.Value))
{
values[item.Value] = item;
}
}
All = new ReadOnlyCollection<TEnumeration>(list);
Names = names;
Values = values;
}
}
internal static class EnumerationCacheFactory<TEnumeration>
where TEnumeration : Enumeration
{
public static EnumerationCache<TEnumeration> Create()
{
var enumerationType = typeof(TEnumeration);
var nameValues = enumerationType
.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)
.Where(field => field.FieldType == enumerationType)
.Select(field => (TEnumeration) field.GetValue(null));
return new EnumerationCache<TEnumeration>(nameValues);
}
}
internal class EnumerationCacheProvider<TEnumeration>
where TEnumeration : Enumeration
{
private static readonly Lazy<EnumerationCache<TEnumeration>> EnumerationCache;
static EnumerationCacheProvider()
{
EnumerationCache = new Lazy<EnumerationCache<TEnumeration>>(EnumerationCacheFactory<TEnumeration>.Create);
}
public static EnumerationCache<TEnumeration> Get()
{
return EnumerationCache.Value;
}
}
// Base class to not end up on multiple generic issue in helper methods
public abstract class Enumeration : IComparable
{
public string Name { get; }
public object Value { get; }
protected Enumeration(string name, object value)
{
Name = name;
Value = value;
}
public override bool Equals(object other)
{
if (!(other is Enumeration otherValue))
{
return false;
}
var typeMatches = GetType() == other.GetType();
var valueMatches = Value.Equals(otherValue.Value);
return typeMatches && valueMatches;
}
public override int GetHashCode() => Value.GetHashCode();
public int CompareTo(object other)
{
return Comparer.Default.Compare(Value, ((Enumeration) other).Value);
}
public static bool operator ==(Enumeration left, Enumeration right)
{
return Equals(left, right);
}
public static bool operator !=(Enumeration left, Enumeration right)
{
return !Equals(left, right);
}
}
public abstract class Enumeration<TValue> : Enumeration, IEquatable<Enumeration<TValue>>, IComparable<Enumeration<TValue>>
{
public new TValue Value => (TValue) base.Value;
protected Enumeration(string name, TValue value)
: base(name, value)
{
}
public bool Equals(Enumeration<TValue> other)
{
return Equals(other as object);
}
public override bool Equals(object other)
{
if (!(other is Enumeration<TValue> otherValue))
{
return false;
}
var typeMatches = GetType() == other.GetType();
var valueMatches = Value.Equals(otherValue.Value);
return typeMatches && valueMatches;
}
public override int GetHashCode() => Value.GetHashCode();
public int CompareTo(Enumeration<TValue> other)
{
return Comparer<TValue>.Default.Compare(Value, other.Value);
}
public static bool operator ==(Enumeration<TValue> left, Enumeration<TValue> right)
{
return Equals(left, right);
}
public static bool operator !=(Enumeration<TValue> left, Enumeration<TValue> right)
{
return !Equals(left, right);
}
}
Which can be used as:
public class Card : Enumeration<int>
{
public static readonly Card ClubKing = new Card(nameof(ClubKing), 1);
public static readonly Card ClubQueen = new Card(nameof(ClubQueen), 2);
public static readonly Card HeartKing = new Card(nameof(HeartKing), 1);
private Card(string name, int value)
: base(name, value)
{
}
}
public static class Program
{
public static void Main(params string[] args)
{
var cards = EnumerationHelpers.GetAll<Card>();
var cardNames = EnumerationHelpers.GetNames<Card>();
var cardValues = EnumerationHelpers.GetValues<Card>();
var clubKing = EnumerationHelpers.ParseName<Card>("ClubKing");
clubKing = EnumerationHelpers.ParseValue<Card>(1);
}
}
A few things:
- Yup there is a bit of boxing here and there in order to maintain a single root class since the inference for the helper methods do not work with several generic parameters.
- The first value or name found is not overridden by subsequent definition of the same value or name.
I am looking for feedbacks that can help to improve my implementation that aims to provide a good alternative to traditional C# enum
.
1 Answer 1
I'm not entirely convinced of the need for this class - in my experience, using extension methods for enum classes which need more than the basic functionality is sufficient. Also this lacks some things which native enums have, in particular an equivalent of [Flags]
(which is useful in parsing and ToString
).
public static TEnumeration ParseName<TEnumeration>(string name) where TEnumeration : Enumeration { return EnumerationCacheProvider<TEnumeration>.Get().Names[name]; }
(and similar methods). What exceptions can this throw? Are they what someone who is switching over from Enum.Parse
would expect?
// Lack of inference from the compiler, cannot write // public static TEnumeration ParseValue<TEnumeration, TValue>(TValue value) // where TEnumeration : Enumeration public static TEnumeration ParseValue<TEnumeration>(object value) where TEnumeration : Enumeration
What you can write is
public static TEnumeration ParseValue<TEnumeration, TValue>(TValue value)
where TEnumeration : Enumeration<TValue>
It's not clear from the comment whether you are aware of this and have rejected it, or whether you overlooked it.
foreach (var item in list) { // First definitions found are subsequently overriden if (!names.ContainsKey(item.Name)) { names[item.Name] = item; } if (!values.ContainsKey(item.Value)) { values[item.Value] = item; } }
The comment says the complete opposite of what the code actually does.
Would it not be more robust to throw an exception if the same name or value occurs twice? I can't tell, for example, whether it's an error that the Card
example defines two different cards with the same value, but I think it probably is and should be caught as early as possible.
The existence of EnumerationCacheFactory<TEnumeration>
and EnumerationCacheProvider<TEnumeration>
seems to me to be an "architecture astronaut" tendency. I see no reason why their fragments of code couldn't be included in EnumerationCache
, and as a bonus EnumerationCache
's constructor could be made private.
var nameValues = enumerationType .GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)
Why Public
? In my opinion you're missing an opportunity here.
public static EnumerationCache<TEnumeration> Get() { return EnumerationCache.Value; }
I would be tempted to replace this with some transparent properties
public static IReadOnlyDictionary<string, TEnumeration> Names => EnumerationCache.Value.Names
etc.
public override bool Equals(object other) { if (!(other is Enumeration otherValue)) { return false; } var typeMatches = GetType() == other.GetType(); var valueMatches = Value.Equals(otherValue.Value); return typeMatches && valueMatches; }
Why not simplify to
public override bool Equals(object other)
{
if (ReferenceEquals(other, null) || GetType() != other.GetType())
{
return false;
}
return Value.Equals(((TEnumeration)otherValue).Value);
}
Also, the constructor doesn't require value
to be non-null, so either that or various other methods are buggy.
Why does Enumeration<TValue>
override Enumeration.Equals
, Enumeration.GetHashCode
, etc? The overrides don't change the behaviour at all.
-
\$\begingroup\$ *
public static TEnumeration ParseValue<TEnumeration, TValue>(TValue value) where TEnumeration : Enumeration<TValue>
"It's not clear from the comment whether you are aware of this and have rejected it, or whether you overlooked it." CauseTValue
is not inferred by the compiler so that the call would be:Parse<Card, int>(1) and sadly, not Parse<Card>(1)
. * "The comment says the complete opposite of what the code actually does.": correct, I forgot to put a "not" in that comment. * "Why not simplify to [...]": agree with you \$\endgroup\$Natalie Perret– Natalie Perret2019年03月05日 16:00:44 +00:00Commented Mar 5, 2019 at 16:00
enum
and classes? \$\endgroup\$enum
s in .NET are just numbers and you cannot use anything else (except leveraging reflection on fields attributes). Unlike the SmartEnum library, my solution aims at providing simpler helper methods and simpler inheritance while trading those against more boxing allocations. \$\endgroup\$