为什么可以将0.0赋给枚举值,而不能赋给1.0

只是出于好奇: 为什么我可以把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行就可以编译了。为什么 C # 中的值 0.0有一个例外?

5055 次浏览

Enumerations in C# are by definition integral values. For consistency C# shouldn’t accept either of these assignments, but 0.0 is silently treated as integral 0. This is probably a holdover from C, where the literal 0 was treated specially and could essentially take any given type – integer, floating point number, null pointer … you name it.

It's a bug that you can use 0.0. The compiler implicitly treats all constant expressions with a value of zero as just 0.

Now, it's correct for the compiler to allow an implicit conversion from a constant int expression of 0 to your enum as per section 6.1.3 of the C# 5 specification:

An implicit enumeration conversion permits the decimal-integer-literal 0 to be converted to any enum-type and to any nullable-type whose underlying type is an enum-type. In the latter case the conversion is evaluated by converting to the underlying enum-type and wrapping the result (§4.1.10).

I've spoken with the C# team about this before: they'd have liked to have removed the accidental conversion from 0.0 (and indeed 0.0m and 0.0f) to enum values, but unfortunately I gather it broke too much code - even though it should never have been allowed in the first place.

The Mono mcs compiler prohibits all of these floating point conversions, although it does allow:

const int Zero = 0;
...


SomeEnum x = Zero;

despite the fact that Zero is a constant expression but not a decimal-integer-literal.

I wouldn't be surprised to see the C# specification change in the future to allow any integer constant expression with a value of 0 (i.e. to mimic mcs), but I wouldn't expect the floating point conversions to ever officially be correct. (I've been wrong before about predicting the future of C#, of course...)

enum is really intended (in all languages which support it) to be a way to work with meaningful and unique strings (labels) rather than a numeric values. Thus, in your example, you should only be using Bar and Baz when dealing with a Foo enumerated data type. You should never use (compare to, or assign) an integer, even though many compilers will let you get away with it (enums are usually integers internally), and in this case, a 0.0 is carelessly treated as a 0 by the compiler.

Conceptually, it should be all right to add an integer n to an enumerated value, to get n values further down the line, or to take val2-val1 to see how far apart they are, but unless the language specification explicitly allows this, I'd avoid it. (Think of an enumerated value as being like a C pointer, in the ways you can use it.) There's no reason enums couldn't be implemented with floating point numbers, and a fixed increment between them, but I haven't heard of this being done in any language.

Jon's answer is correct. I would add to it the following points.

  • I caused this silly and embarrassing bug. Many apologies.

  • The bug was caused by me misunderstanding the semantics of an "expression is zero" predicate in the compiler; I believed it was checking only for integer zero equality, when in fact it was checking for more along the lines of "is this the default value of this type?" In fact, in an earlier version of the bug it was actually possible to assign the default value of any type to an enum! It is now only default values of numbers. (Lesson: Name your helper predicates carefully.)

  • The behaviour I was attempting to implement that I messed up was in fact a workaround for a slightly different bug. You can read the whole terrible story here: https://learn.microsoft.com/en-us/archive/blogs/ericlippert/the-root-of-all-evil-part-one and https://learn.microsoft.com/en-us/archive/blogs/ericlippert/the-root-of-all-evil-part-two (Lesson: It is very easy to introduce new worse bugs while fixing old ones.)

  • 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. (Lesson: get it right the first time!)

  • The code I wrote in Roslyn to preserve this behaviour can be found in the method IsConstantNumericZero in https://github.com/dotnet/roslyn/blob/master/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionsBase.cs -- see it for more details of what exactly the Roslyn behaviour is. I wrote almost all the code in the Conversions directory; I encourage you to read all of it as there are many interesting facts about how C# diverges from the specification in the comments. I decorated each with SPEC VIOLATION to make them easy to find.

One more point of interest: C# also allows any enum value to be used in an enum initializer regardless of its zeroness:

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

The spec is somewhat vague as to whether this should be legal or not, but again, as this has been in the compiler for a long time, the new compilers are likely to maintain the behaviour.