处理枚举时不带默认值的 Switch 语句
自从我开始使用 .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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(10)
哎呀,情况比仅仅处理枚举要糟糕得多。 我们甚至不为布尔值这样做!
产生相同的错误。
即使您解决了枚举能够接受任何值的所有问题,您仍然会遇到这个问题。 该语言的流分析规则根本不认为没有默认值的开关是所有可能的代码路径的“详尽”,即使你和我都知道它们是这样的。
我非常想解决这个问题,但坦率地说,我们有比解决这个愚蠢的小问题更重要的优先事项,所以我们从来没有抽出时间来解决它。
更新:低性价比的功能本身并不能证明其努力的合理性,有时当另一种语言功能需要它时就会完成,这就是这里发生的情况。 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!
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
在 default 子句中抛出异常:
这可以确保覆盖所有可能的路径,同时避免因添加新值而导致的逻辑错误。
Throw an exception in the default clause:
This ensures that all possible paths are covered, while avoiding the logic errors resulting from adding a new value.
这是因为
decision
的值实际上可能是不属于枚举的值,例如:编译器或 CLR 不会阻止这种情况。 该值也可以是枚举值的组合:(
即使枚举没有
Flags
属性)因此,您应该始终放置一个
切换时的默认情况,因为您无法显式检查所有可能的值
That's because the value of
decision
could actually be a value that is not part of the enumeration, for instance :This kind of thing is not prevented by the compiler or the CLR. The value could also be a combination of enum values :
(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默认设置是为了保护您。 抛出一个默认的异常,如果有人添加了一个额外的枚举,你就会被一些东西来标记它。
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.
我意识到这是一个线程复活...
我个人认为 switch 的工作方式是正确的,并且它按照我逻辑上想要的方式运行。
我很惊讶听到对默认标签的如此抱怨。
如果您只有一组严格的枚举或要测试的值,则不需要所有异常处理行或开关外部的返回等。
只需将默认标签放在其他标签之一上即可,也许这个标签是最常见的反应。 在您的示例中,哪一个可能并不重要。 简短、甜蜜,它满足了您消除编译器警告的需求:
如果您不希望默认为“是”,请将默认标签放在“否”标签上方。
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:
If you don't want the default to be Yes, put the default label above the No label.
为了分享一个奇怪的想法,如果没有别的,这里是:
你总是可以实现你自己的强枚举
......并且自从引入
nameof
运算符后,你也可以在 switch-cases 中使用它们。(并不是说您以前在技术上无法这样做,而是很难使此类代码可读且重构友好。)
并如此使用它:
您当然会像这样调用该方法:
int test = 示例(MyEnum.Bar); // returns 2
我们现在可以轻松获得 Name 基本上只是一个额外的好处,是的,一些读者可能会指出,这基本上是一个没有 null 情况的 Java 枚举(因为这不是一个类)。 就像在 Java 中一样,您可以添加任何您想要的额外数据和/或属性,例如序数值。
可读性:检查!
智能感知:检查!
可重构性:检查!
是 ValueType:检查!
真实枚举:检查!
...
性能好吗? 与原生枚举相比; 没有。
你应该使用这个吗? 嗯......
拥有真正的枚举对您来说有多重要,这样您就可以摆脱枚举运行时检查及其伴随的异常?
我不知道。 亲爱的读者,我无法真正回答这个问题; 给每个人自己的。
...实际上,当我写这篇文章时,我意识到让结构“包装”一个普通的枚举可能会更干净。 (静态结构体字段和相应的普通枚举在与上面类似的反射的帮助下相互镜像。)只要永远不要使用普通枚举作为参数就可以了。
更新:
是的,花了一整夜测试我的想法,我是对的:我现在在 C# 中拥有近乎完美的 Java 风格枚举。 使用干净并且性能得到提高。
最重要的是:所有讨厌的东西都封装在基类中,您自己的具体实现可以像这样干净:
这就是您可以做的:
这是您必须做的:
目前,上述每个不变量都在类型初始化时断言。 稍后可能会尝试调整它,看看是否可以在编译时检测到其中的一些内容。
要求理由:
nameof
方法进行切换。 它只是性能不那么好。 我还在考虑是否应该放宽这个要求。 我将对此进行实验...无论如何,如何使用这些 java 风格的枚举?
我现在实现了这些东西:
抽象 Enumeration 类还将为我们实现
IEquatable
接口,包括适用于 Example 实例的 == 和 != 运算符。除了类型初始化期间所需的反射之外,一切都是干净且高性能的。 可能会继续实现 java 的枚举专用集合。
那么这段代码在哪里?
我想看看在发布之前是否可以对其进行更多清理,但到本周末它可能会出现在 GitHub 上的开发分支上- 除非我找到其他疯狂的项目可以做! ^_^
现在在 GitHub
请参阅
Enumeration.cs
和Enumeration_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.)
and use it thusly:
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:
This is what you can do:
This is what you must do:
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:
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...So anyway, how can you use these java-style enums?
Well I implemented this stuff for now:
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
andEnumeration_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.)
我总是认为这种默认是失败/例外。
所以这里不会是“也许”,而是“无效决定,请联系支持人员”。
我不知道它会如何发展,但这将是包罗万象/例外的情况。
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.
除了这种情况之外,您还可以将任何 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.
不要抱怨 switch 语句的工作原理,而是通过在枚举上使用扩展方法来完全避免它,如所述 此处和此处。
这种方法的好处是,在添加新的枚举值时,您不会遇到忘记更新 GetDecision switch 语句的情况,因为它们都在同一个位置 - 在枚举声明中。
我不知道这种方法的效率,实际上,目前甚至没有考虑。 是的,我只是不在乎,因为这对我来说太容易了,我想,“去他妈的电脑吧,这就是它的作用——努力工作。” (当天网接管时,我可能会后悔这种态度。)
如果我需要从其中一个属性值返回到枚举值,我可以简单地构建一个反向字典并用一行代码填充它。
(我通常将“未设置”作为第一个枚举元素,因为正如OP所指出的,C#枚举实际上是一个int,所以未初始化的变量将为零或第一个枚举值)
使用内联,例如:
或者包装在函数中,如果您愿意的话:
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)
Use inline like:
Or wrapped in a function, if you feel like it: