14

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.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Max Yankov
  • 12,551
  • 12
  • 67
  • 135
  • 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
  • [Methods inside enum in C#](https://stackoverflow.com/q/5985661) might be of use. Two possibilities: 1) Use [extension methods](https://stackoverflow.com/a/5985710) to access the data from some dictionary. 2) Use [enumeration classes](https://stackoverflow.com/a/38985655) rather than enums. – dbc Jun 11 '23 at 18:45

11 Answers11

7

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
  • 51,220
  • 55
  • 213
  • 317
  • 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
  • @IanRingrose - You could combine hierarchies of records + pattern matching to implement a state transition engine where each state may have associated data. See e.g. https://dotnetfiddle.net/oD1AEg. Is this the sort of thing you were looking for by adding your bounty? – dbc Jun 12 '23 at 01:56
  • Thank you @IanRingrose for the bounty. I highly appreciate your kindness. – Peter Csala Jun 18 '23 at 09:15
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
  • 20,008
  • 22
  • 105
  • 149
  • 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
1

While c# does not have discriminated unions, you can do something similar by introducing a type hierarchy for your various states, then using the pattern and pattern matching features introduced in C# versions 8, 9 and 10 to implement your state transitions.

For instance, consider the following hypothetical state machine. The machine has to get three inputs from the user, then continue getting inputs from the user until the input matches some terminal string. But in any event, the machine should terminate with an error after getting some maximum number of strings.

One way this could be be implemented as a type hierarchy of records as follows. First define the following state types:

public abstract record State(int Count);  // An abstract base state that tracks shared information, here the total number of iterations.

public sealed record InitialState(int Count) : State(Count) { public InitialState() : this(0) {}}
public record StateA(int Count, string Token, int InnerCount) : State(Count) { }
public record StateB(int Count, string Token) : State(Count);
public sealed record FinalState(int Count) : State(Count);
public sealed record ErrorState(int Count) : State(Count);

Using these types, the required state machine could be implemented as follows:

string terminalString = "stop";
int maxIterations = 100;

State state = new InitialState();

// Negation pattern: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/patterns#logical-patterns
while (state is not FinalState && state is not ErrorState) 
{
    // Get the next token
    string token = GetNextToken();
    
    // Do some work with the current state + next token
    Console.WriteLine("State = {0}", state);

    // Transition to the new state
    state = state switch // Switch Expression: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/switch-expression
    {
        var s when s.Count > maxIterations => 
            new ErrorState(s.Count + 1),
        InitialState s => 
            new StateA(s.Count + 1, token, 0),
        StateA s when s is { InnerCount : > 3 } =>  //https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/patterns#property-pattern
            new StateB (s.Count + 1, token),
        StateA s =>
            s with { Count = s.Count + 1, Token = token, InnerCount = s.InnerCount + 1 },  // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record#nondestructive-mutation
        StateB  s when s.Token == terminalString =>
            new FinalState(s.Count + 1),
        StateB  s =>
            s with { Count = s.Count + 1, Token = token },
        _ => throw new Exception($"Unknown state {state}"),
    };
    
    // Do some additional work with the new state
}
Console.WriteLine("State = {0}", state);

Key points:

  • Use of the is operator and the negation pattern to determine whether iteration has terminated:

    while (state is not FinalState && state is not ErrorState) 
    
  • Use of the switch expression to match on the type of the current state along with case guards to specify additional conditions:

    var s when s.Count > maxIterations =>  
    
  • Use of property patterns to match selected properties of a concrete state against known values in a case guard:

    StateA s when s is { InnerCount : > 3 } =>  
    
  • Use of nondestructive mutation to return modified states of the same type from an existing state:

    s with { Count = s.Count + 1, Token = token, InnerCount = s.InnerCount + 1 },  
    
  • Records implement value equality by default so which could be used in a case guard when clause.

Demo fiddle #1 here.

A second, similar approach you could use if most of your states do not have internal data would be to create some common interface for all states, use records for states with internal data, and use static singletons for states with no internal data.

E.g., you could define the following states:

public interface IState { }

public sealed class State : IState
{
    private string state;
    private State(string state) => this.state = state;

    // Enum-like states with no internal data
    public static State Initial { get; } = new State(nameof(Initial));
    public static State Final { get; } = new State(nameof(Final));
    public static State Error { get; } = new State(nameof(Error));
    
    public override string ToString() => state;
}

// Record states with internal data
public record class StateA(string Token, int Count) : IState;
public record class StateB(string Token) : IState;

And then implement the state machine defined above as follows:

string terminalString = "stop";
int maxIterations = 100;

(int count, IState state) = (0, State.Initial);

while (state != State.Final && state != State.Error)
{
    // Get the next token
    string token = GetNextToken();
    
    // Do some work with the current state + next token
    Console.WriteLine("State = {0}", state);

    // Transition to the new state
    state = state switch // Switch Expression: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/switch-expression
    {
        _ when count > maxIterations =>
            state = State.Error,
        State s when s == State.Initial => 
            new StateA(token, 0),
        StateA s when s is { Count : > 3 } =>  //https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/patterns#property-pattern
            new StateB (token),
        StateA s =>
            s with { Token = token, Count = s.Count + 1 },  // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record#nondestructive-mutation
        StateB s when s.Token == terminalString =>
            State.Final,
        StateB s =>
            s with { Token = token },
        _ => throw new Exception($"Unknown state {state}"),
    };
    
    // Do some additional work with the new state
}
Console.WriteLine("State = {0}", state);

Key points:

  • The use of static singletons for states with no internal data resembles the enumeration class idea by Jimmy Bogard for creating enums with methods.

  • Case guards can use members outside the object used in the switch expression. Here the iteration count is not included in the states themselves so a separate counter count is used instead.

  • The interface IState has no members, but you could imagine adding something to it, e.g. a method to return some handler:

    public interface IState 
    {
        public virtual Action<string> GetTokenHandler() => (s) => Console.WriteLine(s);
    }
    
    public record class StateA(string Token, int Count) : IState
    {
        public Action<string> GetTokenHandler() => (s) => Console.WriteLine("The current count is {0} and the current token is {1}", Count, Token);
    }
    

    The method could have a virtual default implementation that is is overridden in some, but not all, states.

Demo fiddle #2 here.

dbc
  • 104,963
  • 20
  • 228
  • 340
1

There are couple of nuget packages to define somewhat similar behaviour, for example: OneOf

Let me show you how it works via a simple example.
(If you are interested more about the details please check this post).


Let's suppose we have to create a discount engine based on the following requirements:

  • If the customer’s birthday has not specified then fail immediately
  • If the customer celebrates his/her birthday today then give him/her 25% discount
  • If the orders' total fee is above 10k then give him/her 15% discount
  • If there is no order (for a given customer) then treat him/her as a newcomer
  • If he/she is under 21 then fail immediately
  • If he/she is above 21 then give him/her 5% discount

state diagram

Let's define the following base classes

public abstract class SucceededDiscountCalculation : DiscountCalculationResult
{
    public double Percentage { get; }
    protected SucceededDiscountCalculation(double percentage) => Percentage = percentage;
}

public abstract class FailedDiscountCalculation : DiscountCalculationResult
{
    public Dictionary<string, object> ErrorData { get; }
    protected FailedDiscountCalculation(params (string Key, object Value)[] errorData)
      => ErrorData = errorData.ToDictionary(item => item.Key, item => item.Value);
}

public abstract class DiscountCalculationResult
    : OneOfBase<
        DiscountCalculationResult.BirthdayDiscount,
        DiscountCalculationResult.BirthdayIsNotSet,
        DiscountCalculationResult.TotalFeeAbove10K,
        DiscountCalculationResult.Newcomer,
        DiscountCalculationResult.Under21,
        DiscountCalculationResult.Above21>
{
    public class BirthdayDiscount : SucceededDiscountCalculation
    {
        public BirthdayDiscount() : base(25) { }
    }

    public class BirthdayIsNotSet : FailedDiscountCalculation
    {
        public BirthdayIsNotSet(params (string Key, object Value)[] errorData) : base(errorData) { }
    }

    public class TotalFeeAbove10K : SucceededDiscountCalculation
    {
        public TotalFeeAbove10K() : base(15) { }
    }

    public class Newcomer : SucceededDiscountCalculation
    {
        public NewComer() : base(0) { }
    }

    public class Under21 : FailedDiscountCalculation
    {
        public Under21(params (string Key, object Value)[] errorData): base(errorData) { }
    }

    public class Above21 : SucceededDiscountCalculation
    {
        public Above21(): base(5) {}
    }
}

From the question perspective the inheritance from the OneOfBase class is the important stuff. If a method returns with an DiscountCalculationResult then you can be sure that it is one of the listed classes. OneOf provides a Switch method to handle all the cases at once.

var result = engine.CalculateDiscount(dateOfBirth, orderTotal);    
IActionResult actionResult = null;
result.Switch(
    bDayDiscount => actionResult = Ok(bDayDiscount.Percentage),
    bDayIsNotSet => {
        _logger.Log(LogLevel.Information, "BirthDay was not set");
        actionResult = StatusCode(StatusCodes.Status302Found, "Profile/Edit");
    },
    totalAbove10K => actionResult = Ok(totalAbove10K.Percentage),
    totalAbove20K => actionResult = Ok(totalAbove20K.Percentage),
    newcomer => actionResult = Ok(newcomer.Percentage),
    under21 => {
        _logger.Log(LogLevel.Information, $"Customer is under {under21.ErrorData.First().Value}");
        actionResult = StatusCode(StatusCodes.Status403Forbidden);
    },
    above21 => actionResult = Ok(above21.Percentage)
);

For the sake of brevity I've omitted the engine's implementation and it is also irrelevant from the question perspective.

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
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,978
  • 3
  • 25
  • 31
  • 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.

0

Short answer you can't. Even if you feel you can just don't do it you would shoot yourself in foot in doing so. We'll have to wait for the C# team to come up with a type with something like below

  • struct lives on stack in most cases this means it has a fixed size in memory

What we are expecting is sort of multiple struct with different layout but still fits in one decided stack of memory. The way rust handles this is by using the memory size of largest of the group for example

# Right now:
struct A { int a } # 4 bytes
struct B { int a, int b } # 8 bytes

# Can do but highly don't recommend would be waste of precious time, memory and cpu
struct AB {
 A a,
 B b
} # 12 bytes + 2 bytes to keep bool to check which struct should be used in code

# Future/Should be
super struct AB {
   A(int),
   B(int, int)
} # 8 bytes 
0

It all depends on how you want to use entity. This looks like something the State pattern can be used for.

Lets assume you have an Entity called MyEntity that can have StateA, StateB or StateC

You can then create an abstract class State and have StateA, StateB and StateC implement the abstract class.

public abstract class State
    {
        public StateType Type { get; protected set; }

        protected State(StateType type)
        {
            Type = type;
        }

        public abstract string DoSomething();
    }

    public class StateA : State
    {
        public string A { get; set; }

        public StateA() 
            : base(StateType.A)
        {
        }

        public override string DoSomething()
        {
            return $"A: {A}";
        }
    }

    public class StateB : State
    {
        public double B { get; set; }

        public StateB()
            : base(StateType.B)
        {
        }

        public override string DoSomething()
        {
            return $"B: {B}";
        }
    }
    public class StateC : State
    {
        public DateTime C { get; set; }

        public StateC()
            : base(StateType.C)
        {
        }

        public override string DoSomething()
        {
            return $"C: {C}";
        }
    }

    public enum StateType
    {
        A = 1,
        B = 2,
        C = 3
    }

You can then add any properties you want on each state. The string DoSomething() method will then be implemented in each discreet state class and can be different for each.

You can then add the State class to your MyEntity and change the state dynamically.

    public class MyEntity
    {
        public int Id { get; private set; }
        public State State { get; private set; }

        public MyEntity(int id, State state)
        {
            Id = id;
            State = state;
        }

        public void SetState(State state)
        {
            State = state;
        }
    }
Febre
  • 68
  • 4
0

Rust enums can be emulated on C# using a blend of inheritance and composition. An example of this in action is the creation of a C# version of a Rust enum with distinct states and connected information.

// Define the base state interface
public interface IState { }

// Define the specific state interfaces
public interface IStateA : IState
{
    // Define relevant methods and properties for StateA
}

public interface IStateB : IState
{
    // Define relevant methods and properties for StateB
}

public interface IStateC : IState
{
    // Define relevant methods and properties for StateC
}

// Define the concrete state classes
public class StateA : IStateA
{
    // Define relevant data for StateA
    public string DataA { get; set; }
}

public class StateB : IStateB
{
    // Define relevant data for StateB
    public int DataB { get; set; }
}

public class StateC : IStateC
{
    // Define relevant data for StateC
    public bool DataC { get; set; }
}

// Define your entity class
public class MyEntity
{
    private IState currentState;

    public void TransitionToStateA(string data)
    {
        currentState = new StateA { DataA = data };
    }

    public void TransitionToStateB(int data)
    {
        currentState = new StateB { DataB = data };
    }

    public void TransitionToStateC(bool data)
    {
        currentState = new StateC { DataC = data };
    }

    // Use pattern matching to work with specific states
    public void PerformAction()
    {
        switch (currentState)
        {
            case StateA stateA:
                // Perform actions specific to StateA
                Console.WriteLine("Performing action for StateA");
                Console.WriteLine(stateA.DataA);
                break;
            case StateB stateB:
                // Perform actions specific to StateB
                Console.WriteLine("Performing action for StateB");
                Console.WriteLine(stateB.DataB);
                break;
            case StateC stateC:
                // Perform actions specific to StateC
                Console.WriteLine("Performing action for StateC");
                Console.WriteLine(stateC.DataC);
                break;
            default:
                throw new InvalidOperationException("Invalid state");
        }
    }
}

Inheriting from the base interface IState, we define the state interfaces IStateA, IStateB, and IStateC. For each state interface, we specify the corresponding methods and properties that are pertinent to that specific state.

Implementing the concrete state classes is the next step in our process. These classes, known as StateA, StateB, and StateC, hold the relevant data and adhere to their corresponding state interfaces.

Your entity can be represented by the MyEntity class, featuring a field of type IState referred to as currentState. TransitionToStateX methods are provided to enable a transition to any desired state through the creation of an instance of its corresponding state class which can then be assigned to currentState.

Using pattern matching, the PerformAction method executes actions suited for the present concrete state.

Via this approach, you emulate the behavior of a Rust enum with associated data in C# :)

Qasim
  • 658
  • 1
  • 5
  • 10
-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,171
  • 20
  • 28