number - Casting ints a enumeraciones en C#
parse number to enum c# (12)
Hay algo que no puedo entender en C #. Puede convertir un int
fuera de rango en una enum
y el compilador no parpadea. Imagina esta enum
:
enum Colour
{
Red = 1,
Green = 2,
Blue = 3
}
Ahora, si escribes:
Colour eco;
eco = (Colour)17;
El compilador cree que está bien. Y el tiempo de ejecución, también. ¿Uh?
¿Por qué el equipo de C # decidió hacer esto posible? Esta decisión pierde el sentido de usar enumeraciones, creo, en escenarios como este:
void DoSomethingWithColour(Colour eco)
{
//do something to eco.
}
En un lenguaje fuerte como C #, me gustaría suponer que el eco
siempre tendrá un valor de Colour
legal. 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 de Colour
legal. Necesito probarlo explícitamente y manejar los valores excepcionales cuando me plazca. ¿Por qué es esto?
En mi humilde opinión, sería mucho más agradable si el compilador emitió un mensaje de error (o incluso una advertencia) al convertir un fuera de rango int
en una 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é piensas? ¿Hay alguna razón por la cual esto es así?
(Nota: esta es una pregunta que publiqué hace años en mi blog, pero no obtuve ninguna respuesta informativa).
Adivinar sobre ''por qué'' siempre es 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 coloca delante de la enumeración, la última línea cambia a
string text = ne.ToString(); // text == "North, East"
Arrojaría un error al usar Enum.Parse ();
Enum parsedColour = (Colour)Enum.Parse(typeof(Colour), "17");
Podría valer la pena utilizarlo para obtener un error de tiempo de ejecución arrojado, si lo desea.
Ciertamente veo el punto de César, y recuerdo que inicialmente también me confundió. 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 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.
Aún así, demasiado tarde ahora, creo que estamos estancados con la implementación actual para siempre. : /
Cuando defines una enumeración, esencialmente le das nombres a los valores (azúcar sintáctico, si lo prefieres). 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.
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.
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!
}
}
No estoy seguro de por qué, pero recientemente encontré esta "característica" increíblemente útil. Escribí algo como esto 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;
}
...
}
}
La pregunta es, ¿cómo pruebo esa última cláusula de caso? Es completamente razonable suponer que alguien puede extender TransmissionStatus
y no actualizar a sus consumidores, como la pequeña y pobre MyClass
anterior. Sin embargo, me gustaría verificar su comportamiento en este escenario. Una forma es usar el casting, como
[Test]
[ExpectedException (typeof (NotSupportedException))]
public void Test_ProcessTransmissionStatus_ExtendedEnum ()
{
MyClass myClass = new MyClass ();
myClass.ProcessTransmissionStatus ((TransmissionStatus)(10));
}
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 cualquier cosa, ni int en dicha enumeración. Eso sería una locura. ¿No es el objetivo de las enumeraciones no usar los enteros?
Cualquier desarrollador que arroje 17 a la Enum de Color necesitaría 17 patadas en la espalda por lo que a mí respecta.
Pensé que compartiría el código que terminé usando para validar Enums, ya que hasta ahora no parece que haya nada aquí que funcione ...
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>();
}
}
Una vieja pregunta, 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.
La esencia 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í está el corazón de un método de extensión que escribí para convertir / analizar cosas fácilmente en un Enum:
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...
El value
variable es de tipo 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. Al usar tanto TryParse
como IsDefined
en ese orden , puedo analizar todos los tipos que acabo de mencionar, incluidas las cadenas de casos mixtos, y es bastante robusto.
El uso es así (asume 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
}
Espero que ayude a alguien.
Descargo de responsabilidad: no usamos flags / bitmask enums ... esto no ha sido probado con ese escenario de uso y probablemente no funcionaría.
Version corta:
No hagas esto
Intentar cambiar las enumeraciones en entradas con solo permitir valores válidos (tal vez una alternativa predeterminada) requiere métodos de ayuda. En ese momento, no tienes una enumeración, realmente tienes una clase.
Doblemente si los ints son imperativos, como dijo Bryan Rowe.
if (!Enum.IsDefined(typeof(Colour), 17))
{
// Do something
}