为什么我可以将0.0分配给枚举值,而不是1.0


90

出于好奇:为什么我可以将0.0分配给枚举类型的变量,而不是1.0?看下面的代码:

public enum Foo
{
    Bar,
    Baz
}

class Program
{
    static void Main()
    {
        Foo value1 = 0.0;
        Foo value2 = 1.0;   // This line does not compile
        Foo value3 = 4.2;   // This line does not compile
    }
}

我认为数值类型和枚举值之间的转换只能通过强制转换进行吗?那就是我可以写Foo value2 = (Foo) 1.0;以便Main编译第2行。为什么0.0C#中的值有例外?


17
对我来说,很奇怪,您可以将双字面量0.0分配给自定义枚举。不是说您不能1.0文字分配给自定义枚举。
伊利亚·伊万诺夫

2
我怀疑编译器将其视为0替代。我曾经有过类似的问题,罗林在这里发表了一个很好的答案。
2014年

2
IdeOne不会编译它。
约翰尼·莫普

Answers:


98

可以使用0.0的错误。编译器将所有值为零的常数表达式都隐式地视为0。

现在,按照C#5规范的第6.1.3节,编译器允许从0 的常量表达式到您的枚举的隐式转换是正确的int

隐式枚举转换允许将小数-整数-文字0转换为任何枚举类型和其基础类型为枚举类型的任何可空类型。在后一种情况下,通过转换为基础枚举类型并包装结果来评估转换(第4.1.10节)。

我之前曾与C#团队讨论过此事:他们希望将意外转换从0.0(实际上是0.0m和0.0f)删除为枚举值,但是不幸的是,我收集到它破坏了太多代码-尽管首先不应该允许它。

Mono mcs编译器禁止所有这些浮点转换,尽管它确实允许:

const int Zero = 0;
...

SomeEnum x = Zero;

尽管它Zero是一个常量表达式,但不是十进制整数。

将来看到C#规范发生更改以允许任何值为0的整数常量表达式(即模仿mcs),我不会感到惊讶,但是我不希望浮点转换正式地正确。(当然,在预测C#的未来之前,我错了。)


3
根据规范,它仅是字面值 0。因此,它应该拒绝1-1- int值为的常量表达式0。但是正如您观察到的,编译器与此处的规范不符。
Damien_The_Unbeliever

4
it broke too much code-很难想象有任何原因可以编写这样的代码。
伊利亚·伊万诺夫

1
@ObsidianPhoenix:我不确定你的意思。完全等同于:SomeEnum x = (SomeEnum) 0;。是否存在命名的零值就是这种情况。
乔恩·斯基特

2
@ObsidianPhoenix:不,因为值Test.Foo再次为1,而不是0 ...,这与您编写的完全相同Test v1 = (Test) 0;-并且该行为适用于在枚举中不是命名值的任何值。
乔恩·斯基特

2
@JonSkeet会在罗斯林修复吗?
2014年

98

乔恩的答案是正确的。我要补充以下几点。

  • 我造成了这个愚蠢而令人尴尬的错误。很多道歉。

  • 该错误是由我误解了编译器中“表达式为零”谓词的语义所致;我相信它只是在检查整数零相等性,而实际上它正在按照“这是该类型的默认值吗?”的方式检查更多内容。实际上,在早期版本的错误中,实际上可以将任何类型的默认值分配给枚举!现在仅是数字的默认值。(课程:请仔细命名您的辅助谓词。)

  • 实际上,我尝试实现的行为被弄乱了,这是一个针对稍有不同的错误的解决方法。您可以在此处阅读整个可怕的故事:https : //docs.microsoft.com/zh-cn/archive/blogs/ericlippert/the-root-of-all-evil-part-onehttps://docs.microsoft .com / zh-CN / archive / blogs / ericlippert / all-evil-part-two的根源 (课程:在修正旧错误的同时引入新的更差的错误非常容易。)

  • C#团队决定保留这种错误行为,而不是修复它,因为破坏现有代码而没有令人信服的好处的风险太大。(课程:第一次就做对了!)

  • 我在Roslyn中编写的用于保存此行为的代码可以IsConstantNumericZerohttps://github.com/dotnet/roslyn/blob/master/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionsBase.cs中的方法中找到-有关Roslyn行为的确切信息,请参见。我几乎在“转换”目录中编写了所有代码;我鼓励您阅读所有这些内容,因为关于C#与注释中的规范有何不同,有很多有趣的事实。我用SPEC VIOLATION装饰每个,以便于查找。

还有一个有趣的地方:C#还允许在枚举初始值设定项中使用任何枚举值,而不管其零度如何:

enum E { A = 1 }
enum F { B = E.A }  // ???

规范对于是否应该合法还有些含糊,但是同样,由于在编译器中已经存在很长时间了,因此新的编译器可能会保持这种行为。


10
这真的很酷,我终于可以看到您编写的代码。Roslyn源代码是开源的,这真棒。现在我完全理解,存在不提供变更历史记录的正当理由(技术/法律上的理由),但是查看变更历史记录以了解代码如何演变将是非常棒的。
SolutionYogi

The C# team decided to enshrine this buggy behaviour rather than fixing it because the risk of breaking existing code for no compelling benefit was too high.我认为不会有很多人依赖这种行为,而其中之一可能就是更好地解决。但是,它实际上也没有太大危害(除了实现该规范的项目之外)。
Aidiakapi 2014年

5
@Aidiakapi:确实,受影响的人数应该很少;不为零。C#团队非常重视重大更改。可以轻松地说出解决办法更好;您不必与恼怒的客户打交道,而这些客户会打电话给您的副总裁,他们抱怨您琐碎的更改没有任何好处,却使他们的系统集成延迟了一天。
埃里克·利珀特

3
情况变得更糟。所有这些重大更改都将(理想情况下)列在Microsoft的Framework迁移指南中。该列表越长,用户迁移其应用程序的犹豫就越多。因此,即使是很小的重大更改也会导致:1.少数应用程序中断。2.少数用户拒绝升级(即使问题不影响他们)。3.少数用户浪费资源来评估重大更改是否影响他们。4.#1,#2和#3中的用户向其他所有人抱怨。
2014年

@EricLippert如果“规范有些模糊”,更新规范是否有意义?(真正的问题!)
詹姆斯

10

根据定义,C#中的枚举是整数值。为了保持一致,C#不应接受这些分配中的任何一个,而应将0.0其默默地视为integral 0。这可能是C的遗留物,在C中,文字0被特殊对待,并且可以采用任何给定的类型-整数,浮点数,空指针…命名。


3
问题是为什么?如果你去IL-这是推动整数值压入堆栈IL_0001: ldc.i4.0
伊利亚·伊万诺夫

@IlyaIvanov参见更新。但老实说,答案是“没有充分的理由”。
康拉德·鲁道夫2014年

2
我认为这是其中一种情况,如果您查看C#规范,这是不合法的,但是如果您查看MS生成的任何C#编译器,它便会执行此操作。
Damien_The_Unbeliever

3

枚举实际上是打算(在支持它的所有语言中)是一种使用有意义且唯一的字符串(标签)而不是数字值的方法。因此,在您的示例中,仅应在处理Foo枚举数据类型时使用BarBaz。即使许多编译器会让您舍弃它(整数通常在内部整数),也永远不要使用(比较或分配)整数(在这种情况下,编译器会不小心将0.0视为0)。

从概念上讲,可以将整数n添加到枚举值,以使n个值更远,或者采用val2 - val1来查看它们之间有多远,但是除非语言规范明确允许,否则我应该没问题。会避免的。(以您可以使用的方式,将枚举值视为类似于C指针。)没有理由不能用浮点数实现枚举,并且它们之间有固定的增量,但是我没有听说过这可以用任何语言完成。


我知道我不应该在C#中以这种方式使用枚举-但是我发现了这个难题,想知道为什么0.0有效,而1.0无效。我知道这一定与C#编译器有关,因为您可以看到IL代码与for Foo v1 = 0.0;相同Foo v2 = Foo.Bar
feO2x 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.