1
\$\begingroup\$

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.

asked Mar 4, 2019 at 19:49
\$\endgroup\$
11
  • \$\begingroup\$ You may find this to your interests: codeblog.jonskeet.uk/2009/09/10/… \$\endgroup\$ Commented Mar 4, 2019 at 19:58
  • \$\begingroup\$ @JesseC.Slicer I know that article but this is not really related to what I am trying to achieve. \$\endgroup\$ Commented Mar 4, 2019 at 20:00
  • 4
    \$\begingroup\$ I'm not sure I understand the concept and why it is necessary. Could you provide some more realistic use cases where you think this is more useful (smarter) than plain C# enum and classes? \$\endgroup\$ Commented Mar 5, 2019 at 5:39
  • \$\begingroup\$ @HenrikHansen the reason is the same as why github.com/ardalis/SmartEnum and github.com/HeadspringLabs/Enumeration: to provide java-like enumerations. enums 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\$ Commented Mar 5, 2019 at 7:39
  • \$\begingroup\$ Not really smart that other smart-enum... I'd like to join @HenrikHansen request and ask you too to summarize the features that your smarter-enum is about. You have just posted some code but we don't know how it is supposed to work. \$\endgroup\$ Commented Mar 5, 2019 at 8:08

1 Answer 1

6
\$\begingroup\$

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.

answered Mar 5, 2019 at 9:28
\$\endgroup\$
1
  • \$\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." Cause TValue 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\$ Commented Mar 5, 2019 at 16:00

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.