在C#中实现Rust枚举的最佳方法是什么?

14

我有一个涉及扩展方法和可能使用反射的想法,听起来已经很复杂了,所以我不认为这是实现的“最佳方式”。你还感兴趣吗? - nilbot
我认为如果您提供一个想要实现的例子,而不仅仅是引导人们查看文档,那将会很有用,这样您就有更多的机会得到答案。只是快速查看文档,我同意@tweellt的观点。 - Dzyann
请参见 https://dev59.com/C3A75IYBdhLWcg3wsraD。 - Alex
根据@tweellt的答案,没有内置机制,因此您必须从头开始自己编写它。更大的问题是实例的使用位置。由于没有匹配语句/表达式(例如Rust和F#),您必须手动测试和转换类型,这不太好。 - Giles
在C#中,枚举内部的方法可能会有用处。两种可能性:1)使用扩展方法从某个字典中访问数据。2)使用枚举类而不是枚举。 - dbc
11个回答

7
您需要一个类来表示您的实体。
class Entity {States state;}

然后你需要一组类来表示你的状态。
abstract class States {
   // maybe something in common
}
class StateA : MyState {
   // StateA's data and methods
}
class StateB : MyState {
   // ...
}

然后你需要编写类似以下的代码:
StateA maybeStateA = _state as StateA;
If (maybeStateA != null)
{
    - do something with the data in maybeStateA
}

C#目前还没有很好的编写此类代码的方式,也许正在考虑用于C#.next的模式匹配会有所帮助。

我认为您应该重新考虑设计,使用对象关系和包含,试图将在rust中有效的设计强制用于C#可能不是最佳选择。


喜欢模式匹配建议 - tweellt
1
如果你只是想进行类型检查,可以考虑使用 is 关键字 -- if (maybeStateA is StateA) - jocull
1
@jocull,那么在if语句内部需要进行强制类型转换,因此会稍微慢一些。我预计在实际应用中将同时使用“is”和“as”。或者在“States”类上使用抽象方法,例如“IsInStateA()”。 - Ian Ringrose
表面上看,这是一个很好的答案,但 maybeStateA != null 才是真正说明为什么它不适用于 C# 的原因。即使在 C# 中,枚举也不能为 null(这就是 Rust 特性如此出色的原因)。使用这种方法,你必须记住每次它可能为空,而在 Rust 中的 Option<T> 强制你考虑 None 状态。 - Gregor A. Lamche
@IanRingrose - 你可以结合记录的层次结构和模式匹配来实现一个状态转换引擎,其中每个状态可能有关联的数据。参见例如 https://dotnetfiddle.net/oD1AEg。这是你通过添加赏金来寻找的那种东西吗? - dbc
感谢 @IanRingrose 的悬赏。我非常感激您的善意。 - Peter Csala

2

这可能有些疯狂,但如果你想在C#中模拟类似于Rust的枚举,你可以使用一些泛型实现它。好处:您可以保留类型检查并获得Intellisense提示!您会失去一些与各种值类型相关的灵活性,但我认为安全性可能值得不便。

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;
    }
}

这是另一个示例,演示了自定义引用类型的使用。
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;
    }
}

你在所有的枚举中使用了相同的 TValue,这并不符合 Rust 枚举的预期。根据 OP 中链接的文档,其中一个构造函数没有值,另一个有 3 个整数,第三个有 2 个整数,最后一个则将字符串作为其值。 - Mephy
这些的一个好处是,如果您选择,可以将值设置为“dynamic”,或者您可以使用任何自定义类或结构作为值。虽然它不是Rust的完全匹配,但它可能会帮助您朝着正确的方向迈进 :) - jocull

1
虽然C#没有判别联合类型,但您可以通过为各种状态引入类型层次结构,然后使用C#版本8、9和10中引入的模式模式匹配功能来实现状态转换。
例如,考虑以下假设的状态机。该机器必须从用户那里获取三个输入,然后继续从用户那里获取输入,直到输入与某个终端字符串匹配。但无论如何,在获取一定数量的字符串后,机器都应以错误终止。
这可以通过以下方式实现,即将records作为类型层次结构进行定义。首先定义以下状态类型:
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);

使用这些类型,所需的状态机可以按照以下方式实现:
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);

关键要点:
使用is运算符否定模式来确定迭代是否已经终止:
while (state is not FinalState && state is not ErrorState) 

使用switch表达式匹配当前状态的类型,以及使用case guards指定额外条件:
var s when s.Count > maxIterations =>  

使用属性模式将具体状态的选定属性与案例保护中的已知值进行匹配:
StateA s when s is { InnerCount : > 3 } =>  

使用非破坏性变异从现有状态返回相同类型的修改后的状态:
s with { Count = s.Count + 1, Token = token, InnerCount = s.InnerCount + 1 },  

记录默认实现了值相等性,因此可以在when子句的案例保护中使用。
演示fiddle #1 here第二种类似的方法是,如果大多数状态没有内部数据,您可以创建一些通用接口供所有状态使用,对于具有内部数据的状态使用记录,对于没有内部数据的状态使用静态单例。
例如,您可以定义以下状态:
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;

然后按照上述定义的状态机进行实现:
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);

关键要点:
  • 使用静态单例来表示没有内部数据的状态,类似于Jimmy Bogard提出的枚举类概念,用于创建带有方法的枚举。

  • Case guards可以使用switch表达式之外的对象成员。在这里,迭代计数不包含在状态本身中,因此使用一个独立的计数器count

  • 接口IState没有成员,但你可以想象向其中添加一些内容,例如返回某个处理程序的方法:

    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("当前计数为 {0},当前令牌为 {1}", Count, Token);
    }
    

    该方法可以有一个虚拟默认实现,在某些状态下进行重写,而在其他状态下不进行重写。

演示fiddle #2 here

1

有几个NuGet包可以定义类似的行为,例如:OneOf

让我通过一个简单的示例向您展示它是如何工作的。
如果您对详细信息感兴趣,请查看这篇文章)。


假设我们需要基于以下要求创建一个折扣引擎:
  • 如果客户的生日未指定,则立即失败
  • 如果客户今天庆祝生日,则给予25%的折扣
  • 如果订单总费用超过10,000元,则给予15%的折扣
  • 如果没有订单(针对特定客户),则将其视为新用户
  • 如果他/她年龄小于21岁,则立即失败
  • 如果他/她年龄超过21岁,则给予5%的折扣

state diagram

让我们定义以下基类
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) {}
    }
}

从问题的角度来看,从OneOfBase类继承的内容是重要的。如果一个方法返回一个DiscountCalculationResult,那么你可以确定它是列出的类之一。OneOf提供了一个Switch方法来一次处理所有情况。
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)
);

为了简洁起见,我省略了引擎的实现细节,这对于问题来说也是无关紧要的。

0

最近我一直在研究Rust,并思考同样的问题。真正的问题是缺乏Rust解构模式匹配,但如果你愿意使用装箱,类型本身就很冗长但相对简单:

// 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
}

然后的用法是:

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;
}

如果您添加一些额外的公共常量字段,这可以被减少到。
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;
}

...但是你需要为创建无值成员(例如此示例中的None)使用不同的名称

在我看来,如果C#编译器要实现rust枚举而不改变CLR,那么它将生成这种类型的代码。

很容易创建一个.ttinclude来生成这个。

解构不像Rust match那样好,但没有既高效又白痴证明的替代方案(低效的方法是使用类似于

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

总结一下我的废话 - 这是可以做到的,但不太值得这么做。

0

简单来说,你不能这么做。即使你觉得你能,也不要这么做,否则会自食其果。我们必须等待C#团队提出一种带有以下功能的类型。

  • 结构体在大多数情况下存在于堆栈中,这意味着它在内存中具有固定大小

我们期望的是一种多重结构体,具有不同的布局,但仍然适合于一个确定的内存堆栈。 Rust的处理方式是使用最大组的内存大小,例如

# 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
这完全取决于你想如何使用实体。看起来这似乎可以采用状态模式。
假设你有一个名为MyEntity的实体,它可以有StateAStateBStateC
然后,你可以创建一个抽象类State,并让StateAStateBStateC实现这个抽象类。
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
    }

你可以在每个状态上添加任何属性。然后,在每个离散状态类中,string DoSomething()方法将被实现,并且对于每个状态可能是不同的。
你可以将State类添加到你的MyEntity中,并动态地改变状态。
    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;
        }
    }

0

这看起来很像函数式语言中的抽象数据类型。虽然C#没有直接支持,但您可以使用一个抽象类作为数据类型,再加上每个数据构造函数一个密封类。

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

当然,你可以随时添加一个 StateZ : MyState 类,编译器不会警告你的函数不够全面。

2
然而,这并不允许在对象创建后更改其状态。当然,您可以创建一个新对象,但身份将丢失。另一种选择是将所有内容封装在一个简单存储MyState的类中,但这会变得冗长。 - user395760

0

就我个人而言,作为一种快速实现的方法...

我会首先声明枚举类型并正常定义枚举项。

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

现在我定义了一个名为MyTypeAttribute的属性类型,并带有一个名为TypeString的属性。
接下来,我需要编写一个扩展方法来提取每个枚举项的类型(首先是字符串,然后反射到实际类型):
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;
}

最后,使用反射获取真实类型...


我认为这种方法的优点是在代码后期易于使用:

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

很不幸,属性的参数必须是数字常量、字符串常量、typeof表达式或枚举值。这减少了很多灵活性。 - Mephy
@Mephy 是的。但它符合要求,不是吗?我们只需要一个注入函数 f(item)->type。我认为每个定义的类型都可以通过类型名称(字符串)的反射获得。如果我错了,请纠正我... - nilbot

0
Rust的枚举可以通过继承和组合的方式在C#中模拟。一个实际应用的例子是创建一个具有不同状态和相关信息的Rust枚举的C#版本。
// 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");
        }
    }
}

继承自基本接口IState,我们定义了状态接口IStateA、IStateB和IStateC。对于每个状态接口,我们指定了与该特定状态相关的方法和属性。
实现具体状态类是我们流程的下一步。这些类被称为StateA、StateB和StateC,它们保存相关数据并遵循相应的状态接口。
您的实体可以由MyEntity类表示,其中包含一个类型为IState的字段,称为currentState。通过创建其相应状态类的实例,并将其分配给currentState,可以使用TransitionToStateX方法实现到任何所需状态的转换。
使用模式匹配,PerformAction方法执行适合当前具体状态的操作。
通过这种方法,您可以在C#中模拟Rust枚举与关联数据的行为 :)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接