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.

\$\endgroup\$
11
  • \$\begingroup\$ You may find this to your interests: codeblog.jonskeet.uk/2009/09/10/… \$\endgroup\$ Mar 4 '19 at 19:58
  • \$\begingroup\$ @JesseC.Slicer I know that article but this is not really related to what I am trying to achieve. \$\endgroup\$ Mar 4 '19 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\$
    – user73941
    Mar 5 '19 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\$ Mar 5 '19 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\$
    – t3chb0t
    Mar 5 '19 at 8:08
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.

\$\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\$ Mar 5 '19 at 16:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.