Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Casting string to type-safe-enum using user-defined conversion

Tags:

c#

dictionary

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!

like image 429
Melle Avatar asked Mar 26 '12 16:03

Melle


2 Answers

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; 
} 
like image 88
Andras Zoltan Avatar answered Oct 27 '22 19:10

Andras Zoltan


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
like image 30
Jon Skeet Avatar answered Oct 27 '22 21:10

Jon Skeet