10

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.

Community
  • 1
  • 1
Max Yankov
  • 11,341
  • 11
  • 62
  • 126
  • I have an idea that involves extension method and possibly reflection, sounds complicated already, so I don't think that qualify as 'best way' to implement. Still interested? – nilbot Jul 01 '15 at 16:03
  • I think it would be useful if you gave an example of what you want to achieve, instead of only directing people to the documentation, you will have more chances to get an answer. By just a quick look to the documentation, I agree with @tweellt. – Dzyann Jul 01 '15 at 16:05
  • See http://stackoverflow.com/questions/3151702/discriminated-union-in-c-sharp – Alex Jul 01 '15 at 16:05
  • As per @tweellt's answer, there's no built in mechanism so you'll have to cook it up from scratch. More of a problem is where your instances are used. As there's no match statement/expression (like Rust and F#), you'd have to manually test and cast the types, which isn't as nice. – Giles Jul 01 '15 at 16:08

6 Answers6

4

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.

Ian Ringrose
  • 50,487
  • 53
  • 210
  • 311
  • Liked the Pattern Matching suggestion – tweellt Jul 01 '15 at 16:40
  • 1
    Consider using the `is` keyword if you're just type-checking -- `if (maybeStateA is StateA)` – jocull Jul 01 '15 at 16:53
  • 1
    @jocull, then a cast would need to be done inside of the if, hence being a little slower. I expect that in real life a mix of "is" and "as" would be used. Or maybe abstract methods on the "States" class, like "IsInSateA()" – Ian Ringrose Jul 01 '15 at 16:58
  • This is good answer on the surface, but the `maybeStateA != null` is what really shows why this doesn't work for C#. That enums can't be null (even in C#) is what makes the Rust feature so great. With this you have to remember every time that it _could_ be null, with the Option in Rust you're forced to consider the None state. – Gregor A. Lamche Jul 14 '21 at 07:30
2

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;
    }
}
jocull
  • 18,749
  • 20
  • 100
  • 147
  • You use the same `TValue` for all the enums, and that's not what you expect from Rust's enum. From the linked documentation in OP, one constructor has no value, another has 3 ints, third has 2 ints and the last has a String as their values. – Mephy Jul 01 '15 at 17:24
  • The one nice part about these is that you could make the value a `dynamic` if you choose to, or you can use any custom class or struct as the value. It's not an exact match on Rust, but it might help you step in the right direction :) – jocull Jul 01 '15 at 17:37
0

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.

Mephy
  • 2,918
  • 3
  • 24
  • 30
  • 2
    However, this does not allow changing the state of an object after its creation. You can create a new one, of course, but the identity is lost. The alternative is wrapping it all up in a class that simply stores a `MyState`, though that gets wordy. –  Jul 01 '15 at 16:22
0

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());
nilbot
  • 345
  • 3
  • 13
  • Unfortunately, the attribute's parameters must be a number constant, string constant, a typeof expression or an enum value. This reduces a lot of flexibility. – Mephy Jul 01 '15 at 16:26
  • @Mephy yes. But it fits the requirement, no? We need only f(item)->type which is an injection. And I think every defined type can be obtained by reflection of type name (string). Correct me if I'm wrong... – nilbot Jul 01 '15 at 16:33
0

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.

-3

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.

tweellt
  • 2,103
  • 18
  • 26