5

In order to use Enum's in combination with strings, I implemented a StringEnum class based on https://stackoverflow.com/a/424414/1293385.

However I run into problems when I try to implement the suggested user-defined conversion operations.

The StringEnum class is defined as follows:

public abstract class StringEnum
{
    private readonly String name;
    private readonly int value;

    protected static Dictionary<string, StringEnum> instances
        = new Dictionary<string, StringEnum>();

    protected StringEnum(int value, string name)
    {
        this.value = value;
        this.name = name;
        instances.Add(name.ToLower(), this);
    }

    public static explicit operator StringEnum(string name)
    {
        StringEnum se;
        if (instances.TryGetValue(name.ToLower(), out se))
        {
            return se;
        }
        throw new InvalidCastException();
    }

    public override string ToString()
    {
        return name;
    }
}

I use this class as a base like this:

public class DerivedStringEnum : StringEnum
{
    public static readonly DerivedStringEnum EnumValue1
        = new DerivedStringEnum (0, "EnumValue1");
    public static readonly DerivedStringEnum EnumValue2
        = new DerivedStringEnum (1, "EnumValue2");

    private DerivedStringEnum (int value, string name) : base(value, name) { }
}

However when I try to cast it using

string s = "EnumValue1"
DerivedStringEnum e = (DerivedStringEnum) s;

An InvalidCastException is returned. Inspection of the code shows that the instances attribute of the StringEnum class is never filled.

Is there an easy way to fix this?

I prefer not to use C# attribute "magic" such as [StringValue("EnumValue1")].

Thanks!

Community
  • 1
  • 1
Melle
  • 7,639
  • 1
  • 30
  • 31
  • 4
    Why are you reinventing this wheel? Description attribute defined in System.ComponentModel and a simple static class will do the job. – Tony Hopkinson Mar 26 '12 at 16:10
  • 1
    Not related directly to the question/answer (Andras Zoltan I think is correct), but that static dictionary on StringEnum is throwing up red flags to me. If you have two different derived enum classes, but they both happen to have an entry with the same "name", (e.g., Colour.Orange and Fruit.Orange) won't it cause a key-already-added ArgumentException since the dictionary is statically shared? Seems to me that the dictionary should be redeclared on each implementation or perhaps include the type information along with the name when building/looking up the key. – Chris Sinclair Mar 26 '12 at 16:12
  • @ChrisSinclair I was thinking the same thing - but I steared clear of it. And then there's a potential issue with thread-safety as well. – Andras Zoltan Mar 26 '12 at 16:14
  • 1
    `Dictionary>` is better to use for derived classes... also it is better to make such method thread safe. – ili Mar 26 '12 at 16:15
  • @TonyHopkinson You're right, I decided to use the description attribute and created an implementation based on http://stackoverflow.com/a/4367868/1293385 Thanks! – Melle Mar 27 '12 at 14:07

2 Answers2

6

You have to define an explicit cast operator on the derived class as well. The base class is not expected to know how to cast to a derived class.

Since operators are static they are not inherited - the explicit cast operator is only defined between string and StringEnum. You can do this rather ugly double-cast yourself:

DerivedStringEnum e = (DerivedStringEnum)(StringEnum)s

Or in your derived class you can put: (edited after @ili pointed out my own oversight)

public static explicit operator DerivedStringEnum(string name) 
{ 
  return (DerivedStringEnum)(StringEnum)name; 
} 
Andras Zoltan
  • 41,961
  • 13
  • 104
  • 160
  • `public static explicit operator DerivedStringEnum(string name) { return (DerivedStringEnum)(StringEnum)name; }` would not work? – ili Mar 26 '12 at 16:16
  • lol true - oh dear how could I have missed that! In my defense - can I just say I've been at work for 11 hours straight :$ – Andras Zoltan Mar 26 '12 at 16:22
  • @TonyHopkinson indeed! And with caffeine behind me, there's no program I can't totally screw up! – Andras Zoltan Mar 26 '12 at 19:55
  • @AndrasZoltan You were spot on with "The base class is not expected to know how to cast to a derived class.". I decided to go with an implementation that uses the Description attribute, as Tony Hopkinson suggested in his comment on the original question. Thanks! – Melle Mar 27 '12 at 14:03
  • 1
    No problem; and I agree: that is probably the best way forward :) – Andras Zoltan Mar 27 '12 at 20:21
  • This isn't actually correct. See my answer for the true explanation. – Jon Skeet Jul 28 '13 at 22:07
1

You don't need to add another operator. You've already identified the real problem:

Inspection of the code shows that the instances attribute of the StringEnum class is never filled.

That's because nothing is ever forcing the DerivedStringEnum class to be initialized. You're never referring to anything which would force it to be initialized. If you do so by adding a static constructor (to avoid type-initialization optimizations) and a static method which is then called to force the initialization, it works fine:

public class DerivedStringEnum : StringEnum
{
    // Other members as before.

    static DerivedStringEnum()
    {
    }

    public static void ForceInit()
    {
    }
}

class Test
{
    static void Main()
    {
        string s = "EnumValue1";
        DerivedStringEnum.ForceInit();
        DerivedStringEnum e = (DerivedStringEnum) s;
        Console.WriteLine(e); // EnumValue1
    }
}

It's not something I'd recommend doing - I dislike it when the state of a base class effectively depends on whether some derived type has been initialized - but it does explain things...

Note that the answer by Andras works (or at least can work, although I don't think it's guaranteed to) because by invoking the operator declared in the derived type, you might end up initializing the type. You might not though - type initialization can be very lazy. I believe you'd have to actually use a field within the operator (before the call to the base conversion operator) to really force initialization.

With just the StringEnum operator as per the original question, this line:

DerivedStringEnum e = (DerivedStringEnum) s;

is compiled to both the custom operator invocation and a cast:

IL_000d:  ldloc.0
IL_000e:  call       class StringEnum StringEnum::op_Explicit(string)
IL_0013:  castclass  DerivedStringEnum
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194