2009-11-18 17 views
35

Hay algo que no puedo entender en C#. Puede convertir int fuera de rango en enum y el compilador no parpadea. Imagínese esto enum:Casting ints a enumeraciones en C#

enum Colour 
{ 
    Red = 1, 
    Green = 2, 
    Blue = 3 
} 

Ahora, si escribe:

Colour eco; 
eco = (Colour)17; 

El compilador piensa que está bien. Y el tiempo de ejecución, también. ¿Uh?

¿Por qué el equipo de C# decidió hacerlo posible? Esta decisión se pierde el punto de utilizar enumeraciones, creo que, en escenarios como éste:

void DoSomethingWithColour(Colour eco) 
{ 
    //do something to eco. 
} 

En un lenguaje de tipos fuertes como C#, me gustaría asumir que eco siempre tendrá un valor legal Colour. Pero este no es el caso. Un programador podría llamar a mi método con un valor de 17 asignado a eco (como en el fragmento de código anterior), por lo que el código en mi método no debe suponer que eco tiene un valor legal Colour. Necesito probarlo explícitamente y manejar los valores excepcionales cuando me plazca. ¿Por qué es esto?

En mi humilde opinión, sería mucho mejor si el compilador emite un mensaje de error (o incluso una advertencia) al lanzar un fuera de rango int en un enum, si el valor int se conoce en tiempo de compilación. De lo contrario, el tiempo de ejecución arrojaría una excepción en la declaración de asignación.

¿Qué opinas? ¿Hay alguna razón por la cual esto es así?

(. Nota Esta es una pregunta I posted ages ago on my blog pero no obtuvo respuesta informativa.)

Respuesta

31

conjeturar sobre 'por qué' es siempre peligroso, pero considere esto:

enum Direction { North =1, East = 2, South = 4, West = 8 } 
Direction ne = Direction.North | Direction.East; 

int value = (int) ne; // value == 3 
string text = ne.ToString(); // text == "3" 

Cuando el atributo [Flags] se pone en frente a la enumeración, la última línea cambia a

string text = ne.ToString(); // text == "North, East" 
+0

Tu punto es? :-) Si desea que SouthEast sea una dirección válida, creo que debería incluirlo en la declaración enum como SouthEast = 10, por ejemplo. ¿De acuerdo? – CesarGon

+2

Se llaman enumeraciones de banderas. Diseño muy vaild –

+20

No, no estoy de acuerdo. Los campos de bits son un uso perfectamente válido de una enumeración, hasta el punto de que ni siquiera se requiere el atributo [Flags] para usarlos como tales. –

3

Cuando se define una enumeración que son esencialmente de dar nombres a los valores (s sintáctico ugar si quieres). Cuando transfiere 17 a Color, está almacenando un valor para un Color que no tiene nombre. Como probablemente sabrá al final, es solo un campo int de todos modos.

Esto es similar a declarar un número entero que aceptaría solo valores de 1 a 100; el único lenguaje que vi que admitía este nivel de comprobación fue CHILL.

+0

No estoy de acuerdo. Tal vez en los viejos tiempos de C++ era el azúcar sintáctico. Pero en .NET, las enumeraciones son tipos de primera clase. ¡Ese es el punto de tener tipos de enum! – CesarGon

+0

¿Puedes imaginar la sobrecarga para que el CLR verifique las enumeraciones válidas? Creo que al final tomaron la decisión de dejarlo sin control porque no valía la pena verificarlo. –

+0

¿Qué tal el hecho de que ya no puedes usar enumeraciones de banderas? –

4

No necesita lidiar con excepciones. La condición previa para el método es que las personas que llaman deben usar la enumeración, no emitir ninguna otra cosa que no sea int en dicha enumeración. Eso sería una locura. ¿No es el objetivo de las enumeraciones no utilizar los datos?

Cualquier desarrollador que arrojaría 17 a Color enum necesitaría 17 patadas en la espalda por lo que a mí respecta.

+0

De acuerdo sobre las patadas. :-D Pero la programación defensiva es una buena práctica. Y cuando estás haciendo interfaces públicas para tu biblioteca de clase, * sabes * que debes verificar los parámetros de entrada. – CesarGon

+0

Por lo tanto, establece un valor predeterminado en un conmutador y lanza una excepción o salta al depurador. Hay muchos casos vaild para estos valores enteros a partir de enumeraciones. –

+0

@CesarGon: así que compruébalos tú mismo, en el ejemplo que das todo lo que necesitas comprobar es que el valor está entre 1 y 3. CmdrTallen tiene un comentario que muestra cómo verificar en general. – tloach

4

Esa es una de las muchas razones por las que nunca debería estar asignando valores enteros a sus enumeraciones. Si tienen valores importantes que deben usarse en otras partes del código, conviértalo en un objeto.

+1

También hay muchas razones para incluir los valores. Podría estar interoperando con un sistema externo y simplemente usar la enumeración para hacer su código más limpio. –

+2

No creo que haya "muchas" razones. Entiendo el argumento de la interacción con sistemas externos, seguro. Pero no hay forma de que pueda argumentar que una enumeración con los valores asignados va a dar como resultado un código más limpio, más parecido al código más desagradable. Puede seguir un patrón de objetos de valor simple y crear objetos fáciles de entender ... y, ¿sabe, seguir todo el aspecto OOP? –

+3

Al transferir datos hacia y desde el DB, debe forzar las enumeraciones de int a enum y viceversa. – Guy

-1

Lanzaría un error al usar Enum.Parse();

Enum parsedColour = (Colour)Enum.Parse(typeof(Colour), "17"); 

Puede valer la pena utilizarlo para obtener un error de tiempo de ejecución arrojado, si lo desea.

+2

Por lo que puedo ver, esto en realidad no funciona. Simplemente establece parsedColour en 17, en lugar de lanzar una excepción. Se lanza una excepción si pasa una cadena al azar en ella (por ejemplo, "asdf"). ¿O me estoy perdiendo algo? –

+0

Después de probarlo, también puedo estar de acuerdo en que esto no funciona. Esto no arroja un error de tiempo de ejecución. Puede verificarlo usted mismo haciendo una prueba unitaria rápida. – Gilles

1

Versión corta:

No haga esto.

Tratar de cambiar las enumeraciones en entradas con solo permitir valores válidos (tal vez una alternativa predeterminada) requiere métodos auxiliares. En ese momento, no tienes una enumeración, realmente tienes una clase.

Doblemente así si los ints son impecables, como dijo Bryan Rowe.

15

No estoy seguro de por qué, pero recientemente encontré esta "función" increíblemente útil. Escribí algo así el otro día

// a simple enum 
public enum TransmissionStatus 
{ 
    Success = 0, 
    Failure = 1, 
    Error = 2, 
} 
// a consumer of enum 
public class MyClass 
{ 
    public void ProcessTransmissionStatus (TransmissionStatus status) 
    { 
     ... 
     // an exhaustive switch statement, but only if 
     // enum remains the same 
     switch (status) 
     { 
      case TransmissionStatus.Success: ... break; 
      case TransmissionStatus.Failure: ... break; 
      case TransmissionStatus.Error: ... break; 
      // should never be called, unless enum is 
      // extended - which is entirely possible! 
      // remember, code defensively! future proof! 
      default: 
       throw new NotSupportedException(); 
       break; 
     } 
     ... 
    } 
} 

pregunta es, ¿cómo puedo probar esa cláusula del último caso? Es completamente razonable suponer que alguien puede extender TransmissionStatus y no actualizar a sus consumidores, como el pequeño MyClass anterior. Sin embargo, me gustaría verificar su comportamiento en este escenario. Una forma es utilizar la fundición, tales como

[Test] 
[ExpectedException (typeof (NotSupportedException))] 
public void Test_ProcessTransmissionStatus_ExtendedEnum() 
{ 
    MyClass myClass = new MyClass(); 
    myClass.ProcessTransmissionStatus ((TransmissionStatus)(10)); 
} 
+1

¿Hablas en serio? Me parece un truco rápido y sucio, con todo el respeto. :-) – CesarGon

+4

Entonces ... suponiendo que las enumeraciones fueran completamente seguras con validación de valor, ¿cómo harías para probar el condicional anterior? ¿Prefiere agregar un valor "No admitido" en cada enumeración que escriba con el propósito expreso de probar las cláusulas predeterminadas? Si me preguntas, eso huele a mucho peor;) –

+1

De hecho, un valor No soportado en una enumeración es explícitamente claro. Creo que sería una forma mucho mejor de hacer las pruebas, sí. :-) – CesarGon

5

ciertamente veo el punto de César, y recuerdo que este principio me confundió también. En mi opinión, las enumeraciones, en su implementación actual, son de hecho un nivel demasiado bajo y con fugas. Me parece que habría dos soluciones al problema.

1) Solo permite almacenar valores arbitrarios en una enumeración si su definición tiene el atributo FlagsAttribute. De esta forma, podemos continuar usándolos para una máscara de bits cuando sea apropiado (y declarados explícitamente), pero cuando se usan simplemente como marcadores de posición para constantes, obtendríamos el valor verificado en tiempo de ejecución.

2) Introduzca un tipo primitivo separado llamado say, bitmask, que permita cualquier valor de ulong. De nuevo, restringimos las enumeraciones estándar a valores declarados solamente. Esto tendría el beneficio adicional de permitir que el compilador le asignara los valores de los bits. Así que esto:

[Flags] 
enum MyBitmask 
{ 
    FirstValue = 1, SecondValue = 2, ThirdValue = 4, FourthValue = 8 
} 

sería equivalente a esto:

bitmask MyBitmask 
{ 
    FirstValue, SecondValue, ThirdValue, FourthValue 
} 

Después de todo, los valores para cualquier máscara de bits son completamente predecibles, ¿verdad? Como programador, estoy más que feliz de tener este detalle abstraído.

Todavía, demasiado tarde ahora, creo que estamos parados con la implementación actual para siempre. :/

+0

Gracias, Mikey. Eso es lo que quiero decir cuando digo que * real * enums y bitfields ("flag" enums) son de hecho dos tipos muy diferentes de cosas. C++ y los lenguajes relacionados los han tratado como lo mismo, y me temo que C# ha heredado ese defecto. Pero la semántica es claramente diferente. Tal vez Eric Lippert o Jon Skeet puedan venir y arrojar algo de luz sobre este tema. – CesarGon

0

pensé en compartir el código que terminé usando para validar enumeraciones, como hasta ahora no parecen tener nada aquí que funciona ...

public static class EnumHelper<T> 
{ 
    public static bool IsValidValue(int value) 
    { 
     try 
     { 
      Parse(value.ToString()); 
     } 
     catch 
     { 
      return false; 
     } 

     return true; 
    } 

    public static T Parse(string value) 
    { 
     var values = GetValues(); 
     int valueAsInt; 
     var isInteger = Int32.TryParse(value, out valueAsInt); 
     if(!values.Select(v => v.ToString()).Contains(value) 
      && (!isInteger || !values.Select(v => Convert.ToInt32(v)).Contains(valueAsInt))) 
     { 
      throw new ArgumentException("Value '" + value + "' is not a valid value for " + typeof(T)); 
     } 

     return (T)Enum.Parse(typeof(T), value); 
    } 

    public static bool TryParse(string value, out T p) 
    { 
     try 
     { 
      p = Parse(value); 
      return true; 
     } 
     catch (Exception) 
     { 
      p = default(T); 
      return false; 
     } 

    } 

    public static IEnumerable<T> GetValues() 
    { 
     return Enum.GetValues(typeof (T)).Cast<T>(); 
    } 
} 
4

Eso es inesperado .. . lo que realmente queremos es controlar el casting ... por ejemplo:

Colour eco; 
if(Enum.TryParse("17", out eco)) //Parse successfully?? 
{ 
    var isValid = Enum.GetValues(typeof (Colour)).Cast<Colour>().Contains(eco); 
    if(isValid) 
    { 
    //It is really a valid Enum Colour. Here is safe! 
    } 
} 
3
if (!Enum.IsDefined(typeof(Colour), 17)) 
{ 
    // Do something 
} 
0

pregunta antiguo, pero esto me confundió recientemente, y Google me trajo aquí.Encontré un artículo con más búsquedas que finalmente lo hizo clic para mí, y pensé en volver y compartirlo.

Lo esencial es que Enum es una estructura, lo que significa que es un tipo de valor (source). Pero, en esencia, actúa como un tipo derivado del tipo subyacente (int, byte, long, etc.). Entonces, si puede pensar que un tipo Enum es exactamente lo mismo que su tipo subyacente con alguna funcionalidad adicional/azúcar sintáctico (como dijo Otávio), entonces estará al tanto de este "problema" y podrá protegerse de él.

Hablando de eso, aquí es el corazón de un método de extensión que escribí para convertir fácilmente/analizar las cosas a una enumeración:

if (value != null) 
{ 
    TEnum result; 
    if (Enum.TryParse(value.ToString(), true, out result)) 
    { 
     // since an out-of-range int can be cast to TEnum, double-check that result is valid 
     if (Enum.IsDefined(typeof(TEnum), result.ToString())) 
     { 
      return result; 
     } 
    } 
} 

// deal with null and defaults... 

La variable value no es del tipo de objeto, ya que esta extensión tiene "sobrecargas" que aceptan int, int ?, string, Enum y Enum ?. Todos están encuadrados y enviados al método privado que realiza el análisis sintáctico. Mediante el uso de TryParse y IsDefineden ese orden, puedo analizar todos los tipos que acabo de mencionar, incluidas las cadenas de casos mixtos, y es bastante robusto.

uso es igual que (se supone NUnit):

[Test] 
public void MultipleInputTypeSample() 
{ 
    int source; 
    SampleEnum result; 

    // valid int value 
    source = 0; 
    result = source.ParseToEnum<SampleEnum>(); 
    Assert.That(result, Is.EqualTo(SampleEnum.Value1)); 

    // out of range int value 
    source = 15; 
    Assert.Throws<ArgumentException>(() => source.ParseToEnum<SampleEnum>()); 

    // out of range int with default provided 
    source = 30; 
    result = source.ParseToEnum<SampleEnum>(SampleEnum.Value2); 
    Assert.That(result, Is.EqualTo(SampleEnum.Value2)); 
} 

private enum SampleEnum 
{ 
    Value1, 
    Value2 
} 

la esperanza de que ayude a alguien.

Descargo de responsabilidad: no usamos flags/bitmask enums ... esto no se ha probado con ese escenario de uso y probablemente no funcionaría.