处理枚举时不带默认值的 Switch 语句

发布于 2024-07-26 16:02:19 字数 1369 浏览 2 评论 0原文

自从我开始使用 .NET 以来,这一直是我的一个烦恼,但我很好奇,以防我遗漏了一些东西。 我的代码片段不会编译(请原谅示例的强制性质),因为(根据编译器)缺少返回语句:

public enum Decision { Yes, No}

    public class Test
    {
        public string GetDecision(Decision decision)
        {
            switch (decision)
            {
                case Decision.Yes:
                    return "Yes, that's my decision";
                case Decision.No:
                    return "No, that's my decision";

            }
        }
    }

现在我知道我可以简单地放置一个默认语句来摆脱编译器警告,但要我的脑海中不仅有那些多余的代码,还有危险的代码。 如果枚举位于另一个文件中,并且另一个开发人员出现并将Maybe添加到我的枚举中,它将由我的默认子句处理,该默认子句对Maybe一无所知,并且确实有一个我们很有可能引入逻辑错误。

然而,如果编译器允许我使用上面的代码,它就会识别出我们有问题,因为我的 case 语句将不再涵盖枚举中的所有值。 当然对我来说听起来更安全。

这对我来说是根本错误的,我想知道这是否只是我遗漏的东西,或者我们在 switch 语句中使用枚举时是否必须非常小心?

编辑: 我知道我可以在默认情况下引发异常或在开关之外添加返回,但这仍然是从根本上解决不应该是错误的编译器错误的黑客行为。

关于枚举实际上只是一个 int,这是 .NET 肮脏的小秘密之一,这确实非常令人尴尬。 让我声明一个具有有限数量可能性的枚举,并给我一个编译:

Decision fred = (Decision)123;

然后如果有人尝试以下操作,则抛出异常:

int foo = 123;
Decision fred = (Decision)foo;

编辑2:

一些人对当发生的情况发表了评论枚举位于不同的程序集中,这将如何导致问题。 我的观点是,这是我认为应该发生的行为。 如果我更改方法签名,这将导致问题,我的前提是更改枚举应该是相同的。 我的印象是很多人认为我不了解 .NET 中的枚举。 我确实认为这种行为是错误的,我希望有人可能知道一些非常晦涩的功能,这些功能会改变我对 .NET 枚举的看法。

This has been a pet peeve of mine since I started using .NET but I was curious in case I was missing something. My code snippet won't compile (please forgive the forced nature of the sample) because (according to the compiler) a return statement is missing:

public enum Decision { Yes, No}

    public class Test
    {
        public string GetDecision(Decision decision)
        {
            switch (decision)
            {
                case Decision.Yes:
                    return "Yes, that's my decision";
                case Decision.No:
                    return "No, that's my decision";

            }
        }
    }

Now I know I can simply place a default statement to get rid of the complier warning, but to my mind not only is that redundant code, its dangerous code. If the enumeration was in another file and another developer comes along and adds Maybe to my enumeration it would be handled by my default clause which knows nothing about Maybes and there's a really good chance we're introducing a logic error.

Whereas, if the compiler let me use my code above, it could then identify that we have a problem as my case statement would no longer cover all the values from my enumeration. Sure sounds a lot safer to me.

This is just so fundamentally wrong to me that I want to know if its just something I'm missing, or do we just have to be very careful when we use enumerations in switch statements?

EDIT:
I know I can raise exceptions in the default or add a return outside of the switch, but this are still fundamentally hacks to get round a compiler error that shouldn't be an error.

With regards an enum really just being an int, that's one of .NET's dirty little secrets which is quite embarassing really. Let me declare an enumeration with a finite number of possibilities please and give me a compilation for:

Decision fred = (Decision)123;

and then throw an exception if somebody tries something like:

int foo = 123;
Decision fred = (Decision)foo;

EDIT 2:

A few people have made comments about what happens when the enum is in a different assembly and how this would result in problems. My point is that this is the behaviour I think should happen. If I change a method signature this will lead to issues, my premis is that changing an enumeration should be this same. I get the impression that a lot of people don't think I understand about enums in .NET. I do I just think that the behaviour is wrong, and I'd hoped that someone might have known about some very obscure feature that would have altered my opinion about .NET enums.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(10

看轻我的陪伴 2024-08-02 16:02:19

哎呀,情况比仅仅处理枚举要糟糕得多。 我们甚至不为布尔值这样做!

public class Test {        
  public string GetDecision(bool decision) {
    switch (decision) {
       case true: return "Yes, that's my decision";                
       case false: return "No, that's my decision"; 
    }
  }
}

产生相同的错误。

即使您解决了枚举能够接受任何值的所有问题,您仍然会遇到这个问题。 该语言的流分析规则根本不认为没有默认值的开关是所有可能的代码路径的“详尽”,即使你和我都知道它们是这样的。

我非常想解决这个问题,但坦率地说,我们有比解决这个愚蠢的小问题更重要的优先事项,所以我们从来没有抽出时间来解决它。


更新:低性价比的功能本身并不能证明其努力的合理性,有时当另一种语言功能需要它时就会完成,这就是这里发生的情况。 C# 需要对改进的模式匹配和 switch 表达式进行详尽检查。 规则是:

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specation/patterns#114-pattern-exhaustiveness

Heck, the situation is far worse than just dealing with enums. We don't even do this for bools!

public class Test {        
  public string GetDecision(bool decision) {
    switch (decision) {
       case true: return "Yes, that's my decision";                
       case false: return "No, that's my decision"; 
    }
  }
}

Produces the same error.

Even if you solved all the problems with enums being able to take on any value, you'd still have this issue. The flow analysis rules of the language simply do not consider switches without defaults to be "exhaustive" of all possible code paths, even when you and I know they are.

I would like very much to fix that, but frankly, we have many higher priorities than fixing this silly little issue, so we've never gotten around to it.


UPDATE: Low-bang-for-buck features that do not justify the effort on their own merits sometimes get done when another language feature needs it, and that has happened here. C# needed exhaustivity checks for the improved pattern matching and the switch expression. The rules are:

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/patterns#114-pattern-exhaustiveness

三人与歌 2024-08-02 16:02:19

default 子句中抛出异常:

default:
    throw new ArgumentOutOfRangeException("decision");

这可以确保覆盖所有可能的路径,同时避免因添加新值而导致的逻辑错误。

Throw an exception in the default clause:

default:
    throw new ArgumentOutOfRangeException("decision");

This ensures that all possible paths are covered, while avoiding the logic errors resulting from adding a new value.

丶情人眼里出诗心の 2024-08-02 16:02:19

这是因为 decision 的值实际上可能是不属于枚举的值,例如:

string s = GetDecision((Decision)42);

编译器或 CLR 不会阻止这种情况。 该值也可以是枚举值的组合:(

string s = GetDecision(Decision.Yes | Decision.No);

即使枚举没有 Flags 属性)

因此,您应该始终放置一个 切换时的默认情况,因为您无法显式检查所有可能的值

That's because the value of decision could actually be a value that is not part of the enumeration, for instance :

string s = GetDecision((Decision)42);

This kind of thing is not prevented by the compiler or the CLR. The value could also be a combination of enum values :

string s = GetDecision(Decision.Yes | Decision.No);

(even if the enum doesn't have the Flags attribute)

Because of that, you should always put a default case in you switch, since you can't check all possible values explicitly

无畏 2024-08-02 16:02:19
public enum Decision { Yes, No}

public class Test
{
    public string GetDecision(Decision decision)
    {
        switch (decision)
        {
            case Decision.Yes:
                return "Yes, that's my decision";
            case Decision.No:
                return "No, that's my decision";
            default: throw new Exception(); // raise exception here.

        }
    }
}
public enum Decision { Yes, No}

public class Test
{
    public string GetDecision(Decision decision)
    {
        switch (decision)
        {
            case Decision.Yes:
                return "Yes, that's my decision";
            case Decision.No:
                return "No, that's my decision";
            default: throw new Exception(); // raise exception here.

        }
    }
}
是伱的 2024-08-02 16:02:19

默认设置是为了保护您。 抛出一个默认的异常,如果有人添加了一个额外的枚举,你就会被一些东西来标记它。

The default is there to protect you. Throw an exception from the default and if anyone adds an extra enum, you're covered with something to flag it.

小嗷兮 2024-08-02 16:02:19

我意识到这是一个线程复活...

我个人认为 switch 的工作方式是正确的,并且它按照我逻辑上想要的方式运行。

我很惊讶听到对默认标签的如此抱怨。

如果您只有一组严格的枚举或要测试的值,则不需要所有异常处理行或开关外部的返回等。

只需将默认标签放在其他标签之一上即可,也许这个标签是最常见的反应。 在您的示例中,哪一个可能并不重要。 简短、甜蜜,它满足了您消除编译器警告的需求:

switch (decision)
{
    default:
    case Decision.Yes:
        return "Yes, that's my decision";
    case Decision.No:
        return "No, that's my decision";
}

如果您不希望默认为“是”,请将默认标签放在“否”标签上方。

I realize that this is a thread resurrection...

I personally feel that the way that switch works is correct, and it operates how I would logically want it to.

I'm surprised to hear such complaining about the default label.

If you only have a strict set of enums or values that you are testing, you don't need all of the exception handling lines, or a return outside of the switch, etc.

Just put the default label over one of the other labels, perhaps the label that would be the most common response. In your example case it probably doesn't matter which one. Short, sweet, and it fulfills your needs to rid of the compiler warning:

switch (decision)
{
    default:
    case Decision.Yes:
        return "Yes, that's my decision";
    case Decision.No:
        return "No, that's my decision";
}

If you don't want the default to be Yes, put the default label above the No label.

黄昏下泛黄的笔记 2024-08-02 16:02:19

为了分享一个奇怪的想法,如果没有别的,这里是:

你总是可以实现你自己的强枚举

......并且自从引入 nameof 运算符后,你也可以在 switch-cases 中使用它们。
(并不是说您以前在技术上无法这样做,而是很难使此类代码可读且重构友好。)

public struct MyEnum : IEquatable<MyEnum>
{
    private readonly string name;
    private MyEnum(string name) { name = name; }

    public string Name
    {
        // ensure observable pureness and true valuetype behavior of our enum
        get { return name ?? nameof(Bork); } // <- by choosing a default here.
    }

    // our enum values:
    public static readonly MyEnum Bork;
    public static readonly MyEnum Foo;
    public static readonly MyEnum Bar;
    public static readonly MyEnum Bas;

    // automatic initialization:
    static MyEnum()
    {
        FieldInfo[] values = typeof(MyEnum).GetFields(BindingFlags.Static | BindingFlags.Public);
        foreach (var value in values)
            value.SetValue(null, new MyEnum(value.Name));
    }

    /* don't forget these: */
    public override bool Equals(object obj)
    {
        return obj is MyEnum && Equals((MyEnum)obj);
    }
    public override int GetHashCode()
    {
        return Name.GetHashCode();
    }
    public override string ToString()
    {
        return Name.ToString();
    }
    public bool Equals(MyEnum other)
    {
        return Name.Equals(other.Name);
    }
    public static bool operator ==(MyEnum left, MyEnum right)
    {
        return left.Equals(right);
    }
    public static bool operator !=(MyEnum left, MyEnum right)
    {
        return !left.Equals(right);
    }
}

并如此使用它:

public int Example(MyEnum value)
{
    switch(value.Name)
    {
        default: //case nameof(MyEnum.Bork):
            return 0;
        case nameof(MyEnum.Foo):
            return 1;
        case nameof(MyEnum.Bar):
            return 2;
        case nameof(MyEnum.Bas):
            return 3;
    }
}

您当然会像这样调用该方法:
int test = 示例(MyEnum.Bar); // returns 2

我们现在可以轻松获得 Name 基本上只是一个额外的好处,是的,一些读者可能会指出,这基本上是一个没有 null 情况的 Java 枚举(因为这不是一个类)。 就像在 Java 中一样,您可以添加任何您想要的额外数据和/或属性,例如序数值。

可读性:检查!
智能感知:检查!
可重构性:检查!
是 ValueType:检查!
真实枚举:检查!
...
性能好吗? 与原生枚举相比; 没有。
你应该使用这个吗? 嗯......

拥有真正的枚举对您来说有多重要,这样您就可以摆脱枚举运行时检查及其伴随的异常?
我不知道。 亲爱的读者,我无法真正回答这个问题; 给每个人自己的。

...实际上,当我写这篇文章时,我意识到让结构“包装”一个普通的枚举可能会更干净。 (静态结构体字段和相应的普通枚举在与上面类似的反射的帮助下相互镜像。)只要永远不要使用普通枚举作为参数就可以了。

更新:

是的,花了一整夜测试我的想法,我是对的:我现在在 C# 中拥有近乎完美的 Java 风格枚举。 使用干净并且性能得到提高。
最重要的是:所有讨厌的东西都封装在基类中,您自己的具体实现可以像这样干净:

// example java-style enum:
public sealed class Example : Enumeration<Example, Example.Const>, IEnumerationMarker
{
    private Example () {}

    /// <summary> Declare your enum constants here - and document them. </summary>
    public static readonly Example Foo = new Example ();
    public static readonly Example Bar = new Example ();
    public static readonly Example Bas = new Example ();

    // mirror your declaration here:
    public enum Const
    {
        Foo,
        Bar,
        Bas,
    }
}

这就是您可以做的:

  • 您可以添加您想要的任何私有字段。
  • 您可以添加任何您想要的公共非静态字段。
  • 您可以添加任何您想要的属性和方法。
  • 您可以按照自己的意愿设计构造函数,因为:
  • 您可以忘记基本构造函数的麻烦。 基本构造函数是无参数的!

这是您必须做的:

  1. 您的枚举必须是密封类。
  2. 所有的构造函数都必须是私有的。
  3. 您的枚举必须直接继承自 Enumeration并继承空的 IEnumerationMarker 接口。
  4. Enumeration的第一个泛型类型参数是 必须是你的枚举类。
  5. 对于每个公共静态字段,System.Enum 中必须存在一个名称相同的值(您指定为 Enumeration的第二个泛型类型参数)。
  6. 所有公共静态字段都必须是只读的并且属于枚举类型。
  7. 在类型初始化期间,必须为所有公共静态字段分配唯一的非空值。

目前,上述每个不变量都在类型初始化时断言。 稍后可能会尝试调整它,看看是否可以在编译时检测到其中的一些内容。

要求理由:

  1. 您的枚举必须是密封的,因为如果不是密封的,那么其他不变量会变得更加复杂,并且没有明显的好处。
  2. 允许公共构造函数是没有意义的。 它是一种枚举类型,基本上是一种单例类型,但具有一组固定的实例,而不是只有一个。
  3. 原因与第一个相同。 如果不是这样,反射和其他一些不变性和约束检查就会变得混乱。
  4. 我们需要这个泛型类型参数,以便类型系统可以用于通过高性能编译/Jit 时间绑定来唯一存储我们的枚举数据。 不使用哈希表或其他慢速机制! 理论上它可以被删除,但我认为这样做不值得增加复杂性和性能成本。
  5. 这一点应该是很明显的。 我们需要这些常量来制作优雅的 switch 语句。 当然,我可以创建第二个没有它们的枚举类型; 您仍然可以使用前面显示的 nameof 方法进行切换。 它只是性能不那么好。 我还在考虑是否应该放宽这个要求。 我将对此进行实验...
  6. 由于显而易见的原因,您的枚举常量必须是公共和静态的,并且只读字段,因为a)具有只读枚举实例意味着所有相等检查都简化为引用相等; b) 属性更加灵活和冗长,坦率地说,这对于枚举实现来说都是不受欢迎的属性。 最后,所有公共静态字段都必须是枚举类型,因为; a)它使您的枚举类型更干净,更少混乱; b) 使反射更简单; c) 无论如何你都可以自由地对属性做任何事情,所以这是一个非常软的限制。
  7. 这是因为我正在努力将“讨厌的反射魔法”保持在最低限度。 我不希望我的枚举实现需要完全信任执行。 这将严重限制它的实用性。 更准确地说,调用私有构造函数或写入只读字段可能会在低信任环境中引发安全异常。 因此,您的枚举必须在初始化时实例化您的枚举常量 - 然后我们可以“干净地”填充这些实例的(内部)基类数据。

无论如何,如何使用这些 java 风格的枚举?

我现在实现了这些东西:

int ordinal = Example.Bar.Ordinal; // will be in range: [0, Count-1]
string name = Example.Bas.Name; // "Bas"
int count = Enumeration.Count<Example>(); // 3
var value = Example.Foo.Value; // <-- Example.Const.Foo

Example[] values;
Enumeration.Values(out values);

foreach (var value in Enumeration.Values<Example>())
    Console.WriteLine(value); // "Foo", "Bar", "Bas"

public int Switching(Example value)
{
    if (value == null)
        return -1;

    // since we are switching on a System.Enum tabbing to autocomplete all cases works!
    switch (value.Value)
    {
        case Example.Const.Foo:
            return 12345;
        case Example.Const.Bar:
            return value.GetHasCode();
        case Example.Const.Bas:
            return value.Ordinal * 42;
        default:
            return 0;
    }
}

抽象 Enumeration 类还将为我们实现 IEquatable 接口,包括适用于 Example 实例的 == 和 != 运算符。

除了类型初始化期间所需的反射之外,一切都是干净且高性能的。 可能会继续实现 java 的枚举专用集合。

那么这段代码在哪里?

我想看看在发布之前是否可以对其进行更多清理,但到本周末它可能会出现在 GitHub 上的开发分支上- 除非我找到其他疯狂的项目可以做! ^_^

现在在 GitHub
请参阅 Enumeration.csEnumeration_T2.cs
它们目前是我正在开发的一个非常多的 wip 库的开发分支的一部分。
(还没有什么是“可发布的”,并且随时可能发生重大更改。)
...现在,库的其余部分主要是一堆样板文件,用于将所有数组方法扩展到多列数组,使多列数组可与 Linq 一起使用,以及用于公开(私有)的高性能 ReadOnlyArray 包装器(不可变结构)以安全的方式存储数组,而无需始终创建副本。

除了最新的开发提交之外,所有内容*都完全记录在案,并且 IntelliSense 友好。
(*java 枚举类型仍然是 wip,一旦我完成了它们的设计,就会正确记录它们。)

For the sake of sharing a quirky idea if nothing else, here goes:

You can always implement your own strong enums

...and since the introduction of the nameof operator you can also use them in switch-cases.
(Not that you couldn't technically do so previously, but it was difficult to make such code readable and refactor friendly.)

public struct MyEnum : IEquatable<MyEnum>
{
    private readonly string name;
    private MyEnum(string name) { name = name; }

    public string Name
    {
        // ensure observable pureness and true valuetype behavior of our enum
        get { return name ?? nameof(Bork); } // <- by choosing a default here.
    }

    // our enum values:
    public static readonly MyEnum Bork;
    public static readonly MyEnum Foo;
    public static readonly MyEnum Bar;
    public static readonly MyEnum Bas;

    // automatic initialization:
    static MyEnum()
    {
        FieldInfo[] values = typeof(MyEnum).GetFields(BindingFlags.Static | BindingFlags.Public);
        foreach (var value in values)
            value.SetValue(null, new MyEnum(value.Name));
    }

    /* don't forget these: */
    public override bool Equals(object obj)
    {
        return obj is MyEnum && Equals((MyEnum)obj);
    }
    public override int GetHashCode()
    {
        return Name.GetHashCode();
    }
    public override string ToString()
    {
        return Name.ToString();
    }
    public bool Equals(MyEnum other)
    {
        return Name.Equals(other.Name);
    }
    public static bool operator ==(MyEnum left, MyEnum right)
    {
        return left.Equals(right);
    }
    public static bool operator !=(MyEnum left, MyEnum right)
    {
        return !left.Equals(right);
    }
}

and use it thusly:

public int Example(MyEnum value)
{
    switch(value.Name)
    {
        default: //case nameof(MyEnum.Bork):
            return 0;
        case nameof(MyEnum.Foo):
            return 1;
        case nameof(MyEnum.Bar):
            return 2;
        case nameof(MyEnum.Bas):
            return 3;
    }
}

and you would of course call that method like so:
int test = Example(MyEnum.Bar); // returns 2

That we can now easily get the Name is basically just a bonus, and yeah some readers might point out that this is basically a Java enum without the null-case (since it's not a class). And just like in Java you can add whatever extra data and or properties you desire to it, e.g. an ordinal value.

Readability: Check!
Intellisense: Check!
Refactorability: Check!
Is a ValueType: Check!
True enumeration: Check!
...
Is it performant? Compared to native enums; no.
Should you use this? Hmmm....

How important is it for you to have true enumerations so you can getting rid of enum runtime checks and their accompanying exceptions?
I don't know. Can't really answer that for you dear reader; to each their own.

...Actually, as I wrote this I realized it would probably be cleaner to let the struct "wrap" a normal enum. (The static struct fields and the corresponding normal enum mirroring each other with the help of similar reflection as above.) Just never use the normal enum as a parameter and you're good.

UPDATE :

Yepp, spent the night testing out my ideas, and I was right: I now have near perfect java-style enums in c#. Usage is clean and performance is improved.
Best of all: all the nasty shit is encapsulated in the base-class, your own concrete implementation can be as clean as this:

// example java-style enum:
public sealed class Example : Enumeration<Example, Example.Const>, IEnumerationMarker
{
    private Example () {}

    /// <summary> Declare your enum constants here - and document them. </summary>
    public static readonly Example Foo = new Example ();
    public static readonly Example Bar = new Example ();
    public static readonly Example Bas = new Example ();

    // mirror your declaration here:
    public enum Const
    {
        Foo,
        Bar,
        Bas,
    }
}

This is what you can do:

  • You can add any private fields you want.
  • You can add any public non-static fields you want.
  • You can add any properties and methods you want.
  • You can design your constructors however you wish, because:
  • You can forget base constructor hassle. Base constructor is parameter-less!

This is what you must do:

  1. Your enum must be a sealed class.
  2. All your constructors must be private.
  3. Your enum must inherit directly from Enumeration<T, U> and inherit the empty IEnumerationMarker interface.
  4. The first generic type parameter to Enumeration<T, U> must be your enum class.
  5. For each public static field there must exist an identically named value in the System.Enum (that you specified as the second generic type parameter to Enumeration<T, U>).
  6. All your public static fields must be readonly and of your enum type.
  7. All your public static fields must be assigned a unique non-null value during type initialization.

At the moment every invariant above is asserted at type initialization. Might try to tweak it later to see if some of it can be detected at compile-time.

Requirements Rationale:

  1. Your enum must be sealed because if it isn't then other invariants become a lot more complicated for no obvious benefit.
  2. Allowing public constructors makes no sense. It's an enumeration type, which is basically a singleton type but with a fixed set of instances instead of just one.
  3. Same reason as the first one. The reflection and some of the other invariant and constraint checking just becomes messy if it isn't.
  4. We need this generic type parameter so the type-system can be used to uniquely store our enum data with performant compile/Jit time bindings. No hash-tables or other slow mechanisms are used! In theory it can be removed, but I don't think it's worth the added cost in complexity and performance to do so.
  5. This one should be quite obvious. We need these constants for making elegant switch-statements. Of course I could make a second enum-type that doesn't have them; you would still be able to switch using the nameof method shown earlier. It just would not be as performant. I'm still contemplating if a should relax this requirement or not. I'll experiment on it...
  6. Your enum constants must be public and static for obvious reasons, and readonly fields because a) having readonly enumeration instances means all equality checks simplifies to reference equality; b) properties are more flexible and verbose, and frankly both of those are undesirable properties for an enumeration implementation. Lastly all public static fields must be of your enum-type simply because; a) it keeps your enumeration type cleaner with less clutter; b) makes the reflection simpler; and c) you are free to do whatever with properties anyway so it's a very soft restriction.
  7. This is because I'm trying hard to keep "nasty reflection magic" to a minimum. I don't want my enum implementation to require full-trust execution. That would severely limit its usefulness. More precisely, calling private constructors or writing to readonly fields can throw security exceptions in a low-trust environment. Thus your enum must instantiate your enum constants at initialization time itself - then we can populate the (internal) base-class data of those instances "cleanly".

So anyway, how can you use these java-style enums?

Well I implemented this stuff for now:

int ordinal = Example.Bar.Ordinal; // will be in range: [0, Count-1]
string name = Example.Bas.Name; // "Bas"
int count = Enumeration.Count<Example>(); // 3
var value = Example.Foo.Value; // <-- Example.Const.Foo

Example[] values;
Enumeration.Values(out values);

foreach (var value in Enumeration.Values<Example>())
    Console.WriteLine(value); // "Foo", "Bar", "Bas"

public int Switching(Example value)
{
    if (value == null)
        return -1;

    // since we are switching on a System.Enum tabbing to autocomplete all cases works!
    switch (value.Value)
    {
        case Example.Const.Foo:
            return 12345;
        case Example.Const.Bar:
            return value.GetHasCode();
        case Example.Const.Bas:
            return value.Ordinal * 42;
        default:
            return 0;
    }
}

The abstract Enumeration class will also implement the IEquatable<Example> interface for us, including == and != operators that will work on Example instances.

Aside from the reflection needed during type initialization everything is clean and performant. Will probably move on to implement the specialized collections java has for enums.

So where is this code then?

I want to see if I can clean it up a bit more before I release it, but it will probably be up on a dev branch on GitHub by the end of the week - unless I find other crazy projects to work on! ^_^

Now up on GitHub
See Enumeration.cs and Enumeration_T2.cs.
They are currently part of the dev branch of a very much wip library I'm working on.
(Nothing is "releasable" yet and subject to breaking changes at any moment.)
...For now the rest of the library is mostly a shit ton of boilerplate to extend all array methods to multi-rank arrays, make multi-rank arrays usable with Linq, and performant ReadOnlyArray wrappers (immutable structs) for exposing (private) arrays in a safe way without the cringy need to create copies all the time.

Everything* except the very latest dev commits is fully documented and IntelliSense friendly.
(*The java enum types are still wip and will be properly documented once I've finalized their design.)

帅气尐潴 2024-08-02 16:02:19

我总是认为这种默认是失败/例外。

所以这里不会是“也许”,而是“无效决定,请联系支持人员”。

我不知道它会如何发展,但这将是包罗万象/例外的情况。

I always think of that default as the fall through/exception.

So here it would not be maybe but instead would be "Invalid Decision, contact support".

I don't see how it would fall through to that but that would be the catchall/exception case.

冬天旳寂寞 2024-08-02 16:02:19

除了这种情况之外,您还可以将任何 int 转换为您的枚举,并拥有一个您不处理的枚举。 还有一种情况是,如果枚举位于外部 .dll 中,并且该 .dll 已更新,则向枚举添加其他选项(如“是”、“否”、“也许”)也不会破坏您的代码。 因此,为了处理这些未来的更改,您还需要默认情况。 无法保证在编译时您知道枚举在其生命周期中具有的每个值。

In addition to the case of, you can cast any int to your enum and have an enum you aren't handling. There is also the case where, if the enum is in an external .dll, and that .dll is updated, it does not break your code if an additional option is added to the enum (like Yes, No, Maybe). So, to handle those future changes you need the default case as well. There is no way to guarantee at compile time that you know every value that enum will have for it's life.

不爱素颜 2024-08-02 16:02:19

不要抱怨 switch 语句的工作原理,而是通过在枚举上使用扩展方法来完全避免它,如所述 此处此处

这种方法的好处是,在添加新的枚举值时,您不会遇到忘记更新 GetDecision switch 语句的情况,因为它们都在同一个位置 - 在枚举声明中。

我不知道这种方法的效率,实际上,目前甚至没有考虑。 是的,我只是不在乎,因为这对来说太容易了,我想,“去他妈的电脑吧,这就是它的作用——努力工作。” (当天网接管时,我可能会后悔这种态度。)

如果我需要从其中一个属性值返回到枚举值,我可以简单地构建一个反向字典并用一行代码填充它。

(我通常将“未设置”作为第一个枚举元素,因为正如OP所指出的,C#枚举实际上是一个int,所以未初始化的变量将为零或第一个枚举值

public enum Decision
{
  [DisplayText("Not Set")]
  NotSet,
  [DisplayText("Yes, that's my decision")]
  Yes,
  [DisplayText("No, that's my decision")]
  No
}

public static class XtensionMethods
{
  public static string ToDisplayText(this Enum Value)
  {
    try
    {
      Type type = Value.GetType();
      MemberInfo[] memInfo =
        type.GetMember(Value.ToString());

      if (memInfo != null && memInfo.Length > 0)
      {
        object[] attrs = memInfo[0]
          .GetCustomAttributes(typeof(DisplayText), false);
        if (attrs != null && attrs.Length > 0)
          return ((DisplayText)attrs[0]).DisplayedText;
      }
    }
    catch (Exception ex)
    {
      throw new Exception(
        "Error in XtensionMethods.ToDisplayText(Enum):\r\n" + ex.Message);
    }
    return Value.ToString();
  }

  [System.AttributeUsage(System.AttributeTargets.Field)]
  public class DisplayText : System.Attribute
  {
    public string DisplayedText;

    public DisplayText(string displayText)
    {
      DisplayedText = displayText;
    }
  }
}

使用内联,例如:

myEnum.ToDisplayText();

或者包装在函数中,如果您愿意的话:

public string GetDecision(Decision decision)
{
  return decision.ToDisplayText();
}

Instead of complaining about how the switch statement works, avoid it entirely by using an extension method on the enum as explained here and here.

The nice thing about this approach is, you don't get into a situation and forgetting to update your GetDecision switch statement when adding a new enum value because it's all together in the same place - in the enum declaration.

The efficiency of this approach is not known to me and, really, not even a consideration at this point. That's right, I just don't care because it's so much easier for me that I figure, "Phhhtt screw the computer that's what it's there for - to work hard." (I may regret that attitude when Skynet takes over.)

If I ever need to get from one of those attribute values back to the enum value, I can simply build a reverse dictionary of and fill it with a single line of code.

(I usually do a 'not set' as the first enum element because, as the OP notes, a C# enum is really an int so an uninitialized variable is going to be zero or the first enum value)

public enum Decision
{
  [DisplayText("Not Set")]
  NotSet,
  [DisplayText("Yes, that's my decision")]
  Yes,
  [DisplayText("No, that's my decision")]
  No
}

public static class XtensionMethods
{
  public static string ToDisplayText(this Enum Value)
  {
    try
    {
      Type type = Value.GetType();
      MemberInfo[] memInfo =
        type.GetMember(Value.ToString());

      if (memInfo != null && memInfo.Length > 0)
      {
        object[] attrs = memInfo[0]
          .GetCustomAttributes(typeof(DisplayText), false);
        if (attrs != null && attrs.Length > 0)
          return ((DisplayText)attrs[0]).DisplayedText;
      }
    }
    catch (Exception ex)
    {
      throw new Exception(
        "Error in XtensionMethods.ToDisplayText(Enum):\r\n" + ex.Message);
    }
    return Value.ToString();
  }

  [System.AttributeUsage(System.AttributeTargets.Field)]
  public class DisplayText : System.Attribute
  {
    public string DisplayedText;

    public DisplayText(string displayText)
    {
      DisplayedText = displayText;
    }
  }
}

Use inline like:

myEnum.ToDisplayText();

Or wrapped in a function, if you feel like it:

public string GetDecision(Decision decision)
{
  return decision.ToDisplayText();
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文