¿Por qué puedo asignar 0.0 a los valores de enumeración, pero no 1.0?

90

Solo por curiosidad: ¿por qué puedo asignar 0.0 a una variable que es de tipo enumeración, pero no 1.0? Eche un vistazo al siguiente código:

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
    }
}

Pensé que las conversiones entre tipos numéricos y valores de enumeración solo se permiten a través de conversiones. Es decir, podría escribir Foo value2 = (Foo) 1.0;para que la línea 2 de Mainpueda compilarse. ¿Por qué hay una excepción para el valor 0.0en C #?

feO2x
fuente
17
Para mí es extraño que pueda asignar 0.0 doble literal a una enumeración personalizada. No es que no pueda asignar 1.0literal a una enumeración personalizada.
Ilya Ivanov
2
Sospecho que el compilador lo está tratando como en su 0lugar. Una vez tuve una pregunta similar y Rawling publicó una gran respuesta aquí .
Amistoso
2
IdeOne no lo compila.
Johnny Mopp

Respuestas:

98

Es un error que puedes usar 0.0. El compilador trata implícitamente todas las expresiones constantes con un valor de cero como solo 0.

Ahora, es correcto que el compilador permita una conversión implícita de una intexpresión constante de 0 a su enumeración según la sección 6.1.3 de la especificación C # 5:

Una conversión de enumeración implícita permite convertir el decimal-integer-literal 0 en cualquier tipo de enumeración y en cualquier tipo que acepta valores NULL cuyo tipo subyacente es un tipo de enumeración. En el último caso, la conversión se evalúa convirtiendo al tipo enum subyacente y envolviendo el resultado (§4.1.10).

Hablé con el equipo de C # sobre esto antes: les hubiera gustado haber eliminado la conversión accidental de 0.0 (y de hecho 0.0my 0.0f) a los valores de enumeración, pero desafortunadamente deduzco que rompió demasiado código, aunque en primer lugar, nunca debería haberse permitido.

El Mono mcscompilador prohíbe todas estas conversiones de punto flotante, aunque no permite:

const int Zero = 0;
...

SomeEnum x = Zero;

a pesar de que Zeroes una expresión constante pero no un decimal-entero-literal.

No me sorprendería ver que la especificación de C # cambie en el futuro para permitir cualquier expresión constante entera con un valor de 0 (es decir, para imitar mcs), pero no esperaría que las conversiones de punto flotante fueran oficialmente correctas. (Me he equivocado antes al predecir el futuro de C #, por supuesto ...)

Jon Skeet
fuente
3
Según la especificación, solo se supone que sea el 0 literal. Por lo tanto, debería rechazar 1-1una intexpresión constante con un valor de 0. Pero como puede observar, el compilador está fuera de línea con la especificación aquí.
Damien_The_Unbeliever
4
it broke too much code- Es realmente difícil imaginar alguna razón para escribir tal código.
Ilya Ivanov
1
@ObsidianPhoenix: No estoy seguro de lo que quieres decir. Es exactamente equivalente a: SomeEnum x = (SomeEnum) 0;. Ese es el caso, ya sea que haya un valor cero con nombre o no.
Jon Skeet
2
@ObsidianPhoenix: Bueno, no, porque el valor de Test.Fooes 1, no 0 ... de nuevo, eso es exactamente lo mismo que si escribiera Test v1 = (Test) 0;, y ese comportamiento es válido para cualquier valor que no sea un valor con nombre en la enumeración.
Jon Skeet
2
@JonSkeet, ¿se va a arreglar en Roslyn?
Max
98

La respuesta de Jon es correcta. Le agregaría los siguientes puntos.

  • Yo causé este error tonto y vergonzoso. Muchas disculpas.

  • El error se debió a que entendí mal la semántica de un predicado "la expresión es cero" en el compilador; Creí que solo estaba comprobando la igualdad de enteros cero, cuando de hecho estaba comprobando más cosas como "¿es este el valor predeterminado de este tipo?" De hecho, en una versión anterior del error, ¡era posible asignar el valor predeterminado de cualquier tipo a una enumeración! Ahora son solo valores predeterminados de números. (Lección: nombre sus predicados auxiliares con cuidado).

  • El comportamiento que estaba intentando implementar y que arruiné fue de hecho una solución para un error ligeramente diferente. Puede leer toda la terrible historia aquí: https://docs.microsoft.com/en-us/archive/blogs/ericlippert/the-root-of-all-evil-part-one y https://docs.microsoft .com / en-us / archive / blogs / ericlippert / the-root-of-all-evil-part-two (Lección: Es muy fácil introducir nuevos errores peores mientras se corrigen los antiguos).

  • El equipo de C # decidió consagrar este comportamiento defectuoso en lugar de corregirlo porque el riesgo de romper el código existente sin un beneficio convincente era demasiado alto. (Lección: ¡hazlo bien la primera vez!)

  • El código que escribí en Roslyn para preservar este comportamiento se puede encontrar en el método IsConstantNumericZeroen https://github.com/dotnet/roslyn/blob/master/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionsBase.cs - Véalo para obtener más detalles sobre cuál es exactamente el comportamiento de Roslyn. Escribí casi todo el código en el directorio de conversiones; Te animo a que lo leas todo, ya que hay muchos datos interesantes sobre cómo C # difiere de la especificación en los comentarios. Decoré cada uno con SPEC VIOLATION para que sean fáciles de encontrar.

Un punto más de interés: C # también permite que se use cualquier valor de enumeración en un inicializador de enumeración independientemente de su zeroness:

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

La especificación es algo vaga en cuanto a si esto debería ser legal o no, pero nuevamente, como ha estado en el compilador durante mucho tiempo, es probable que los nuevos compiladores mantengan el comportamiento.

Eric Lippert
fuente
10
Esto es realmente genial, finalmente puedo ver el código que escribiste. Es increíble que el código fuente de Roslyn sea de código abierto. Ahora entiendo completamente que existen razones válidas (técnicas / legales) para no proporcionar el historial de cambios, pero hubiera sido increíble ver el historial de cambios para ver cómo evolucionó el código.
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.No creo que haya muchas personas que dependan de este comportamiento, y es una de estas rarezas que quizás haya sido mejor arreglar. Sin embargo, tampoco hace mucho daño (a excepción de los proyectos que implementan la especificación).
Aidiakapi
5
@Aidiakapi: De hecho, el número de personas afectadas debería ser pequeño; no es cero. El equipo de C # se toma muy en serio los cambios importantes. Es fácil para usted decir que es mejor arreglarlo; no tiene que lidiar con clientes furiosos que llaman a su vicepresidente para quejarse de que su cambio trivial que no agrega ningún beneficio retrasó la integración de su sistema por un día.
Eric Lippert
3
Se pone peor. Todos estos cambios importantes se incluirán (idealmente) en la guía de migración de Framework de Microsoft. Cuanto más larga sea esta lista, más dudan los usuarios de migrar su aplicación. Por lo tanto, incluso un pequeño cambio importante provoca: 1. Un pequeño número de aplicaciones para interrumpir. 2. Que un pequeño número de usuarios se niegue a actualizar (incluso si el problema no les afecta). 3. Un pequeño número de usuarios desperdiciar recursos evaluando si el cambio radical los afecta. 4. Usuarios de los números 1, 2 y 3 para quejarse con todos los demás.
Brian
@EricLippert Si "La especificación es algo vaga", ¿no tendría sentido actualizar la especificación? (¡Pregunta genuina!)
James
10

Las enumeraciones en C # son, por definición, valores integrales. Por coherencia, C # no debería aceptar ninguna de estas asignaciones, pero 0.0se trata silenciosamente como integral 0. Esto es probablemente un vestigio de C, donde el literal 0fue tratado especialmente y esencialmente podría tomar cualquier tipo dado: entero, número de punto flotante, puntero nulo… lo que sea.

Konrad Rudolph
fuente
3
la pregunta es ¿por qué ? Si va a IL, está empujando el valor entero en la pilaIL_0001: ldc.i4.0
Ilya Ivanov
@IlyaIvanov Ver actualización. Pero, para ser honesto, la respuesta es "no hay una buena razón".
Konrad Rudolph
2
Creo que este es uno de esos casos en los que si observa la especificación de C # , no es legal, pero si observa cualquier compilador de C # que MS haya producido, lo hace.
Damien_The_Unbeliever
3

Enum está realmente destinado (en todos los lenguajes que lo admiten) a ser una forma de trabajar con cadenas (etiquetas) significativas y únicas en lugar de valores numéricos. Por lo tanto, en su ejemplo, solo debería usar Bar y Baz cuando se trata de un tipo de datos enumerado Foo . Nunca debe usar (comparar o asignar) un número entero, aunque muchos compiladores le permitirán salirse con la suya (las enumeraciones suelen ser números enteros internamente) y, en este caso, el compilador trata descuidadamente un 0.0 como un 0.

Conceptualmente, debería estar bien agregar un número entero n a un valor enumerado, para obtener n valores más abajo en la línea, o tomar val2 - val1 para ver qué tan separados están, pero a menos que la especificación del lenguaje lo permita explícitamente, I Lo evitaría. (Piense en un valor enumerado como un puntero C, en las formas en que puede usarlo). No hay razón para que las enumeraciones no se puedan implementar con números de punto flotante y un incremento fijo entre ellos, pero no he oído hablar de esto se hace en cualquier idioma.

Phil Perry
fuente
Sé que no debería usar enumeraciones de esa manera en C #, pero encontré este acertijo y quería saber por qué 0.0 funciona, pero 1.0 no. Sabía que tenía que ser algo con el compilador de C # porque puede ver que el código IL para Foo v1 = 0.0;es el mismo que para Foo v2 = Foo.Bar.
feO2x