简体   繁体   English

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

[英]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). 我有一个实体可以处于不同的状态之一(StateA,StateB和StateC),并且每个实体都有不同类型的相关数据(TStateA,TStateB,TStateC)。 Enums in Rust represent this perfectly . Rust中的枚举代表了这一点 What is the best way to implement something like this in C#? 在C#中实现这样的东西的最佳方法是什么?

This question may appear similar , but enums in Rust and unions in C are significantly different. 这个问题可能看似相似 ,但Rust中的枚举和C中的联合会有很大不同。

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. C#没有为此编写代码的好方法 ,也许正在为C#.next考虑的模式匹配会有所帮助。

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. 我认为你应该重新考虑你的设计来使用对象关系和遏制,试图采用一种rust的设计并强制它进入C#可能不是最好的选择。

This might be crazy, but if you are hard-up about emulating Rust-like enums in C#, you could do it with some generics. 这可能很疯狂,但是如果你很难在C#中模拟类似Rust的枚举,你可以用一些泛型来做。 Bonus: you keep type-safety and also get Intellisense out of the deal! 额外奖励:您保持类型安全并且还可以获得Intellisense! 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. 在C#中没有直接的支持,但是你可以为数据类型使用一个抽象类,为每个数据构造函数使用一个密封类。

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. 当然,没有什么禁止你以后添加StateZ : MyState类,并且编译器不会警告你,你的功能并不详尽。

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 . 现在,我定义属性类型MyTypeAttribute一个叫做物业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): 接下来,我需要编写一个扩展方法来为每个枚举项提取Type(首先在string中,然后再反映为实际类型):

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. 我最近一直在研究Rust,并一直在思考同样的问题。 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: 真正的问题是没有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
}

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 如果你添加一些额外的公共const字段,这可以减少到

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) ...但是您需要一个不同的名称来创建无价值的成员(在本例中为None)

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. 在我看来,如果C#编译器要在不改变CLR的情况下实现生锈枚举,那么这就是它将生成的那种代码。

It would be easy enough to create a .ttinclude to generate this. 创建一个.ttinclude来生成它很容易。

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 解构并不像Rust匹配那么好,但是没有其他方法可以有效和愚蠢地证明(效率低的方法是使用像

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 . 从来没有在Rust做过任何事情,但是看看它接触到的文档,你必须实现一本教科书C# class Since Rust enums even support functions and implementations of various types. 由于Rust枚举甚至支持各种类型的功能和实现。

Probabily an abstract class . Probabily是一个抽象类

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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