简体   繁体   中英

What is the best way to implement a Rust enum in C#?

I have an entity that can be in one of different states (StateA, StateB and StateC), and in each of them have relevant data of distinct types (TStateA, TStateB, TStateC). Enums in Rust represent this perfectly . What is the best way to implement something like this in C#?

This question may appear similar , but enums in Rust and unions in C are significantly different.

You need a class to represent your Entity

class Entity {States state;}

Then you need a set of classes to represent your states.

abstract class States {
   // maybe something in common
}
class StateA : MyState {
   // StateA's data and methods
}
class StateB : MyState {
   // ...
}

Then you need to write code like

StateA maybeStateA = _state as StateA;
If (maybeStateA != null)
{
    - do something with the data in maybeStateA
}

C# does not have a nice way of writing code for this yet , maybe the Pattern Matching that is being considered for C#.next would help.

I think you should rethink your design to use object relationships and containment, trying to take a design that works in rust and force it into C# may not be the best option.

This might be crazy, but if you are hard-up about emulating Rust-like enums in C#, you could do it with some generics. Bonus: you keep type-safety and also get Intellisense out of the deal! You'll lose a little flexibility with various value types, but I think the safety is probably worth the inconvenience.

enum Option
{
    Some,
    None
}

class RustyEnum<TType, TValue>
{
    public TType EnumType { get; set; }
    public TValue EnumValue { get; set; }
}

// This static class basically gives you type-inference when creating items. Sugar!
static class RustyEnum
{
    // Will leave the value as a null `object`. Not sure if this is actually useful.
    public static RustyEnum<TType, object> Create<TType>(TType e)
    {
        return new RustyEnum<TType, object>
        {
            EnumType = e,
            EnumValue = null
        };
    }

    // Will let you set the value also
    public static RustyEnum<TType, TValue> Create<TType, TValue>(TType e, TValue v)
    {
        return new RustyEnum<TType, TValue>
        {
            EnumType = e,
            EnumValue = v
        };
    }
}

void Main()
{
    var hasSome = RustyEnum.Create(Option.Some, 42);
    var hasNone = RustyEnum.Create(Option.None, 0);

    UseTheEnum(hasSome);
    UseTheEnum(hasNone);
}

void UseTheEnum(RustyEnum<Option, int> item)
{
    switch (item.EnumType)
    {
        case Option.Some:
            Debug.WriteLine("Wow, the value is {0}!", item.EnumValue);
            break;
        default:
            Debug.WriteLine("You know nuffin', Jon Snow!");
            break;
    }
}

Here's another sample demonstrating the use of a custom reference type.

class MyComplexValue
{
    public int A { get; set; }
    public int B { get; set; }
    public int C { get; set; }

    public override string ToString()
    {
        return string.Format("A: {0}, B: {1}, C: {2}", A, B, C);
    }
}

void Main()
{
    var hasSome = RustyEnum.Create(Option.Some, new MyComplexValue { A = 1, B = 2, C = 3});
    var hasNone = RustyEnum.Create(Option.None, null as MyComplexValue);

    UseTheEnum(hasSome);
    UseTheEnum(hasNone);
}

void UseTheEnum(RustyEnum<Option, MyComplexValue> item)
{
    switch (item.EnumType)
    {
        case Option.Some:
            Debug.WriteLine("Wow, the value is {0}!", item.EnumValue);
            break;
        default:
            Debug.WriteLine("You know nuffin', Jon Snow!");
            break;
    }
}

This looks a lot like Abstract Data Types in functional languages. There's no direct support for this in C#, but you can use one abstract class for the data type plus one sealed class for each data constructor.

abstract class MyState {
   // maybe something in common
}
sealed class StateA : MyState {
   // StateA's data and methods
}
sealed class StateB : MyState {
   // ...
}

Of course, there's nothing prohibiting you from adding a StateZ : MyState class later, and the compiler won't warn you that your functions are not exhaustive.

Just from the back of my head, as a quick implementation...

I would first declare the Enum type and define enumerate items normally.

enum MyEnum{
    [MyType('MyCustomIntType')]
    Item1,
    [MyType('MyCustomOtherType')]
    Item2,
}

Now I define the Attribute type MyTypeAttribute with a property called TypeString .

Next, I need to write an extension method to extract the Type for each enum item (first in string, then later reflect to real type):

public static string GetMyType(this Enum eValue){
    var _nAttributes = eValue.GetType().GetField(eValue.ToString()).GetCustomAttributes(typeof (MyTypeAttribute), false);
    // handle other stuff if necessary
    return ((MyTypeAttribute) _nAttributes.First()).TypeString;
}

Finally, get the real type using reflection...


I think the upside of this approach is easy to use later in the code:

var item = MyEnum.SomeItem;
var itemType = GetType(item.GetMyType());

I've been looking into Rust recently and been thinking the same questions. The real problem is the absence of the Rust deconstruction pattern matching but the type itself is long-winded but relatively straightforward if you are willing to use boxing:

// You need a new type with a lot of boilerplate for every
// Rust-like enum but they can all be implemented as a struct
// containing an enum discriminator and an object value.
// The struct is small and can be passed by value
public struct RustyEnum
{
    // discriminator type must be public so we can do a switch because there is no equivalent to Rust deconstructor
    public enum DiscriminatorType
    {
        // The 0 value doesn't have to be None 
        // but it must be something that has a reasonable default value 
        // because this  is a struct. 
        // If it has a struct type value then the access method 
        // must check for Value == null
        None=0,
        IVal,
        SVal,
        CVal,
    }

    // a discriminator for users to switch on
    public DiscriminatorType Discriminator {get;private set;}

    // Value is reference or box so no generics needed
    private object Value;

    // ctor is private so you can't create an invalid instance
    private RustyEnum(DiscriminatorType type, object value)
    {
        Discriminator = type;
        Value = value;
    }

    // union access methods one for each enum member with a value
    public int GetIVal() { return (int)Value; }
    public string GetSVal() { return (string)Value; }
    public C GetCVal() { return (C)Value; }

    // traditional enum members become static readonly instances
    public static readonly RustyEnum None = new RustyEnum(DiscriminatorType.None,null);

    // Rusty enum members that have values become static factory methods
    public static RustyEnum FromIVal(int i) 
    { 
        return  new RustyEnum(DiscriminatorType.IVal,i);
    }

    //....etc
}

Usage is then:

var x = RustyEnum::FromSVal("hello");
switch(x.Discriminator)
{
    case RustyEnum::DiscriminatorType::None:
    break;
    case RustyEnum::DiscriminatorType::SVal:
         string s = x.GetSVal();
    break;
    case RustyEnum::DiscriminatorType::IVal:
         int i = x.GetIVal();
    break;
}

If you add some extra public const fields this could be reduced to

var x = RustyEnum::FromSVal("hello");
switch(x.Discriminator)
{
    case RustyEnum::None:
    break;
    case RustyEnum::SVal:
         string s = x.GetSVal();
    break;
    case RustyEnum::IVal:
         int i = x.GetIVal();
    break;
}

... but you then need a different name for creating the valueless members (like None in this example)

It seems to me that if the C# compiler was to implement rust enums without changing the CLR then this is the sort of code that it would generate.

It would be easy enough to create a .ttinclude to generate this.

Deconstruction is not as nice as Rust match but there is no alternative that is both efficient and idiot proof (the inefficient way is to use something like

x.IfSVal(sval=> {....})

To summarize my rambling - It can be done but it's unlikely to be worth the effort.

Never did anything in Rust, but looking at the docs it seams to me that you would have to implement a textbook C# class . Since Rust enums even support functions and implementations of various types.

Probabily an abstract class .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM