Crear un método genérico que limite T a un Enum

Estoy construyendo una función para extender el concepto Enum.Parse que

  • Permite analizar un valor predeterminado en caso de que no se encuentre un valor de Enum
  • Es insensible a mayúsculas

Entonces escribí lo siguiente:

 public static T GetEnumFromString(string value, T defaultValue) where T : Enum { if (string.IsNullOrEmpty(value)) return defaultValue; foreach (T item in Enum.GetValues(typeof(T))) { if (item.ToString().ToLower().Equals(value.Trim().ToLower())) return item; } return defaultValue; } 

Obtengo una Restricción de error no puede ser clase especial System.Enum .

Bastante, pero hay una solución alternativa para permitir un Enum genérico, o voy a tener que imitar la función Parse y pasar un tipo como un atributo, lo que obliga al feo requisito del boxeo a su código.

EDITAR Todas las sugerencias a continuación han sido muy apreciadas, gracias.

Me he asentado (he dejado el ciclo para mantener la insensibilidad de mayúsculas y minúsculas; lo estoy usando al analizar XML)

 public static class EnumUtils { public static T ParseEnum(string value, T defaultValue) where T : struct, IConvertible { if (!typeof(T).IsEnum) throw new ArgumentException("T must be an enumerated type"); if (string.IsNullOrEmpty(value)) return defaultValue; foreach (T item in Enum.GetValues(typeof(T))) { if (item.ToString().ToLower().Equals(value.Trim().ToLower())) return item; } return defaultValue; } } 

EDITAR: (16 de febrero de 2015) Julien Lebosquain ha publicado recientemente una solución genérica de seguridad tipo segura reforzada por comstackdor en MSIL o F # a continuación, que bien vale la pena echarle un vistazo, y una votación alternativa. Eliminaré esta edición si la solución aparece más arriba en la página.

Dado que Enum Type implementa la interfaz IConvertible , una mejor implementación debería ser algo como esto:

 public T GetEnumFromString(string value) where T : struct, IConvertible { if (!typeof(T).IsEnum) { throw new ArgumentException("T must be an enumerated type"); } //... } 

Esto aún permitirá el paso de los tipos de valor que implementan IConvertible . Sin embargo, las posibilidades son raras.

¡Esta característica finalmente es compatible con C # 7.3!

El siguiente fragmento (de las muestras de dotnet ) demuestra su uso:

 public static Dictionary EnumNamedValues() where T : System.Enum { var result = new Dictionary(); var values = Enum.GetValues(typeof(T)); foreach (int item in values) result.Add(item, Enum.GetName(typeof(T), item)); return result; } 

Asegúrese de configurar su versión de idioma en su proyecto de C # a la versión 7.3.


Respuesta original a continuación:

Llego tarde al juego, pero lo tomé como un desafío para ver cómo se podía hacer. No es posible en C # (o VB.NET, pero desplácese hacia abajo para F #), pero es posible en MSIL. Escribí esta pequeña … cosa

 // license: http://www.apache.org/licenses/LICENSE-2.0.html .assembly MyThing{} .class public abstract sealed MyThing.Thing extends [mscorlib]System.Object { .method public static !!T GetEnumFromString(string strValue, !!T defaultValue) cil managed { .maxstack 2 .locals init ([0] !!T temp, [1] !!T return_value, [2] class [mscorlib]System.Collections.IEnumerator enumerator, [3] class [mscorlib]System.IDisposable disposer) // if(string.IsNullOrEmpty(strValue)) return defaultValue; ldarg strValue call bool [mscorlib]System.String::IsNullOrEmpty(string) brfalse.s HASVALUE br RETURNDEF // return default it empty // foreach (T item in Enum.GetValues(typeof(T))) HASVALUE: // Enum.GetValues.GetEnumerator() ldtoken !!T call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) call class [mscorlib]System.Array [mscorlib]System.Enum::GetValues(class [mscorlib]System.Type) callvirt instance class [mscorlib]System.Collections.IEnumerator [mscorlib]System.Array::GetEnumerator() stloc enumerator .try { CONDITION: ldloc enumerator callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext() brfalse.s LEAVE STATEMENTS: // T item = (T)Enumerator.Current ldloc enumerator callvirt instance object [mscorlib]System.Collections.IEnumerator::get_Current() unbox.any !!T stloc temp ldloca.s temp constrained. !!T // if (item.ToString().ToLower().Equals(value.Trim().ToLower())) return item; callvirt instance string [mscorlib]System.Object::ToString() callvirt instance string [mscorlib]System.String::ToLower() ldarg strValue callvirt instance string [mscorlib]System.String::Trim() callvirt instance string [mscorlib]System.String::ToLower() callvirt instance bool [mscorlib]System.String::Equals(string) brfalse.s CONDITION ldloc temp stloc return_value leave.s RETURNVAL LEAVE: leave.s RETURNDEF } finally { // ArrayList's Enumerator may or may not inherit from IDisposable ldloc enumerator isinst [mscorlib]System.IDisposable stloc.s disposer ldloc.s disposer ldnull ceq brtrue.s LEAVEFINALLY ldloc.s disposer callvirt instance void [mscorlib]System.IDisposable::Dispose() LEAVEFINALLY: endfinally } RETURNDEF: ldarg defaultValue stloc return_value RETURNVAL: ldloc return_value ret } } 

Lo que genera una función que se vería así, si fuera válida C #:

 T GetEnumFromString(string valueString, T defaultValue) where T : Enum 

Luego con el siguiente código C #:

 using MyThing; // stuff... private enum MyEnum { Yes, No, Okay } static void Main(string[] args) { Thing.GetEnumFromString("No", MyEnum.Yes); // returns MyEnum.No Thing.GetEnumFromString("Invalid", MyEnum.Okay); // returns MyEnum.Okay Thing.GetEnumFromString("AnotherInvalid", 0); // compiler error, not an Enum } 

Desafortunadamente, esto significa tener esta parte de tu código escrita en MSIL en lugar de C #, con el único beneficio adicional de que puedes restringir este método por System.Enum . También es un poco fastidioso, porque se comstack en un ensamblaje por separado. Sin embargo, esto no significa que deba implementarlo de esa manera.

.assembly MyThing{} la línea .assembly MyThing{} e invocando ilasm de la siguiente manera:

 ilasm.exe /DLL /OUTPUT=MyThing.netmodule 

obtienes un netmodule en lugar de un ensamblado.

Desafortunadamente, VS2010 (y antes, obviamente) no admite agregar referencias de netmodule, lo que significa que tendrías que dejarlo en 2 ensamblajes separados cuando estés depurando. La única manera de agregarlos como parte de su ensamblaje sería ejecutar csc.exe usted mismo usando el argumento de línea de comando /addmodule:{files} . No sería demasiado doloroso en un script de MSBuild. Por supuesto, si eres valiente o estúpido, puedes ejecutar csc tu mismo manualmente cada vez. Y ciertamente se vuelve más complicado ya que múltiples ensambles necesitan acceder a él.

Entonces, PUEDE hacerse en .Net. ¿Vale la pena el esfuerzo extra? Um, bueno, supongo que te dejaré decidir sobre eso.


F # Solución como alternativa

Crédito adicional: resulta que es posible una restricción genérica en la enum en al menos otro idioma .NET además de MSIL: F #.

 type MyThing = static member GetEnumFromString< 'T when 'T :> Enum> str defaultValue: 'T = /// protect for null (only required in interop with C#) let str = if isNull str then String.Empty else str Enum.GetValues(typedefof< 'T>) |> Seq.cast<_> |> Seq.tryFind(fun v -> String.Compare(v.ToString(), str.Trim(), true) = 0) |> function Some x -> x | None -> defaultValue 

Este es más fácil de mantener ya que es un lenguaje bien conocido con soporte completo de Visual Studio IDE, pero aún necesita un proyecto separado en su solución para ello. Sin embargo, produce IL considerablemente diferente (el código es muy diferente) y depende de la biblioteca FSharp.Core , que, al igual que cualquier otra biblioteca externa, necesita formar parte de su distribución.

A continuación, le mostramos cómo puede usarlo (básicamente lo mismo que la solución MSIL) y para demostrar que falla correctamente, ya que de lo contrario se estructura:

 // works, result is inferred to have type StringComparison var result = MyThing.GetEnumFromString("OrdinalIgnoreCase", StringComparison.Ordinal); // type restriction is recognized by C#, this fails at compile time var result = MyThing.GetEnumFromString("OrdinalIgnoreCase", 42); 

C # ≥ 7.3

Comenzando con C # 7.3 (disponible con Visual Studio 2017 ≥ v15.7), este código ahora es completamente válido:

 public static TEnum Parse(string value) where TEnum : struct, Enum { ... } 

C # ≤ 7.2

Puede tener una restricción de enumeración forzada por el comstackdor real al abusar de la herencia de restricciones. El siguiente código especifica restricciones de class y de struct al mismo tiempo:

 public abstract class EnumClassUtils where TClass : class { public static TEnum Parse(string value) where TEnum : struct, TClass { return (TEnum) Enum.Parse(typeof(TEnum), value); } } public class EnumUtils : EnumClassUtils { } 

Uso:

 EnumUtils.Parse("value"); 

Nota: esto se establece específicamente en la especificación del lenguaje C # 5.0:

Si el parámetro de tipo S depende del parámetro de tipo T, entonces: […] Es válido que S tenga la restricción de tipo de valor y T tenga la restricción de tipo de referencia. Efectivamente esto limita T a los tipos System.Object, System.ValueType, System.Enum y cualquier tipo de interfaz.

Editar

La pregunta ha sido magníficamente respondida por Julien Lebosquain . También me gustaría extender su respuesta con ignoreCase , ignoreCase y argumentos opcionales, al tiempo que agrego TryParse y ParseOrDefault .

 public abstract class ConstrainedEnumParser where TClass : class // value type constraint S ("TEnum") depends on reference type T ("TClass") [and on struct] { // internal constructor, to prevent this class from being inherited outside this code internal ConstrainedEnumParser() {} // Parse using pragmatic/adhoc hard cast: // - struct + class = enum // - 'guaranteed' call from derived -constrained type EnumUtils public static TEnum Parse(string value, bool ignoreCase = false) where TEnum : struct, TClass { return (TEnum)Enum.Parse(typeof(TEnum), value, ignoreCase); } public static bool TryParse(string value, out TEnum result, bool ignoreCase = false, TEnum defaultValue = default(TEnum)) where TEnum : struct, TClass // value type constraint S depending on T { var didParse = Enum.TryParse(value, ignoreCase, out result); if (didParse == false) { result = defaultValue; } return didParse; } public static TEnum ParseOrDefault(string value, bool ignoreCase = false, TEnum defaultValue = default(TEnum)) where TEnum : struct, TClass // value type constraint S depending on T { if (string.IsNullOrEmpty(value)) { return defaultValue; } TEnum result; if (Enum.TryParse(value, ignoreCase, out result)) { return result; } return defaultValue; } } public class EnumUtils: ConstrainedEnumParser // reference type constraint to any  { // call to parse will then contain constraint to specific -class } 

Ejemplos de uso:

 WeekDay parsedDayOrArgumentException = EnumUtils.Parse("monday", ignoreCase:true); WeekDay parsedDayOrDefault; bool didParse = EnumUtils.TryParse("clubs", out parsedDayOrDefault, ignoreCase:true); parsedDayOrDefault = EnumUtils.ParseOrDefault("friday", ignoreCase:true, defaultValue:WeekDay.Sunday); 

Antiguo

Mis antiguas mejoras en la respuesta de Vivek al usar los comentarios y los ‘nuevos’ desarrollos:

  • use TEnum para mayor claridad para los usuarios
  • agregue más restricciones de interfaz para una comprobación de restricciones adicional
  • deje que TryParse maneje ignoreCase con el parámetro existente (introducido en VS2010 / .Net 4)
  • opcionalmente use el valor default genérico (introducido en VS2005 / .Net 2)
  • utilice argumentos opcionales (introducidos en VS2010 / .Net 4) con valores predeterminados, para ignoreCase e ignoreCase

Resultando en:

 public static class EnumUtils { public static TEnum ParseEnum(this string value, bool ignoreCase = true, TEnum defaultValue = default(TEnum)) where TEnum : struct, IComparable, IFormattable, IConvertible { if ( ! typeof(TEnum).IsEnum) { throw new ArgumentException("TEnum must be an enumerated type"); } if (string.IsNullOrEmpty(value)) { return defaultValue; } TEnum lResult; if (Enum.TryParse(value, ignoreCase, out lResult)) { return lResult; } return defaultValue; } } 

Puede definir un constructor estático para la clase que verificará que el tipo T sea una enumeración y arroje una excepción si no lo es. Este es el método mencionado por Jeffery Richter en su libro CLR a través de C #.

 internal sealed class GenericTypeThatRequiresAnEnum { static GenericTypeThatRequiresAnEnum() { if (!typeof(T).IsEnum) { throw new ArgumentException("T must be an enumerated type"); } } } 

Luego, en el método de análisis, puede usar Enum.Parse (typeof (T), input, true) para convertir de cadena a la enumeración. El último parámetro verdadero es para ignorar el caso de la entrada.

Modifiqué la muestra por dimarzionista. Esta versión solo funcionará con Enums y no permitirá que las estructuras se transmitan.

 public static T ParseEnum(string enumString) where T : struct // enum { if (String.IsNullOrEmpty(enumString) || !typeof(T).IsEnum) throw new Exception("Type given must be an Enum"); try { return (T)Enum.Parse(typeof(T), enumString, true); } catch (Exception ex) { return default(T); } } 

Traté de mejorar el código un poco:

 public T LoadEnum(string value, T defaultValue = default(T)) where T : struct, IComparable, IFormattable, IConvertible { if (Enum.IsDefined(typeof(T), value)) { return (T)Enum.Parse(typeof(T), value, true); } return defaultValue; } 

También se debe considerar que ya que el lanzamiento de C # 7.3 usando restricciones de Enum es compatible desde el primer momento sin tener que hacer comprobaciones adicionales.

De modo que, en adelante, y dado que ha cambiado la versión de idioma de su proyecto a C # 7.3, el siguiente código funcionará perfectamente bien:

  private static T GetEnumFromString(string value, T defaultValue) where T : Enum { // Your code goes here... } 

En caso de que no sepa cómo cambiar la versión del idioma a C # 7.3, consulte la siguiente captura de pantalla: enter image description here

EDIT 1 – Versión requerida de Visual Studio y considerando ReSharper

Para que Visual Studio reconozca la nueva syntax necesita al menos la versión 15.7. Puede encontrar que también se menciona en las notas de la versión de Microsoft, consulte Visual Studio 2017 15.7 Release Notes . Gracias @MohamedElshawaf por señalar esta pregunta válida.

Pls también nota que en mi caso ReSharper 2018.1 al momento de escribir este EDIT aún no es compatible con C # 7.3. Tener ReSharper activado resalta la restricción Enum como un error que me dice No se pueden usar ‘System.Array’, ‘System.Delegate’, ‘System.Enum’, ‘System.ValueType’, ‘object’ como tipo de restricción de parámetro . ReSharper sugiere una solución rápida para eliminar la restricción ‘Enum’ del tipo T parámetro del método

Sin embargo, si apaga ReSharper temporalmente en Herramientas -> Opciones -> ReSharper Ultimate -> General verá que la syntax está perfectamente bien dado que usa VS 15.7 o superior y C # 7.3 o superior.

Tengo un requisito específico donde requiero usar enum con el texto asociado con el valor enum. Por ejemplo, cuando uso enum para especificar el tipo de error, es necesario para describir los detalles del error.

 public static class XmlEnumExtension { public static string ReadXmlEnumAttribute(this Enum value) { if (value == null) throw new ArgumentNullException("value"); var attribs = (XmlEnumAttribute[]) value.GetType().GetField(value.ToString()).GetCustomAttributes(typeof (XmlEnumAttribute), true); return attribs.Length > 0 ? attribs[0].Name : value.ToString(); } public static T ParseXmlEnumAttribute(this string str) { foreach (T item in Enum.GetValues(typeof(T))) { var attribs = (XmlEnumAttribute[])item.GetType().GetField(item.ToString()).GetCustomAttributes(typeof(XmlEnumAttribute), true); if(attribs.Length > 0 && attribs[0].Name.Equals(str)) return item; } return (T)Enum.Parse(typeof(T), str, true); } } public enum MyEnum { [XmlEnum("First Value")] One, [XmlEnum("Second Value")] Two, Three } static void Main() { // Parsing from XmlEnum attribute var str = "Second Value"; var me = str.ParseXmlEnumAttribute(); System.Console.WriteLine(me.ReadXmlEnumAttribute()); // Parsing without XmlEnum str = "Three"; me = str.ParseXmlEnumAttribute(); System.Console.WriteLine(me.ReadXmlEnumAttribute()); me = MyEnum.One; System.Console.WriteLine(me.ReadXmlEnumAttribute()); } 

Espero que esto sea útil:

 public static TValue ParseEnum(string value, TValue defaultValue) where TValue : struct // enum { try { if (String.IsNullOrEmpty(value)) return defaultValue; return (TValue)Enum.Parse(typeof (TValue), value); } catch(Exception ex) { return defaultValue; } } 

Curiosamente, aparentemente esto es posible en otros lenguajes (Managed C ++, IL directamente).

Citar:

… Ambas restricciones en realidad producen una IL válida y también pueden ser consumidas por C # si están escritas en otro idioma (puede declarar esas restricciones en C ++ administrado o en IL).

Quién sabe

Este es mi punto de vista. Combinado a partir de las respuestas y MSDN

 public static TEnum ParseToEnum(this string text) where TEnum : struct, IConvertible, IComparable, IFormattable { if (string.IsNullOrEmpty(text) || !typeof(TEnum).IsEnum) throw new ArgumentException("TEnum must be an Enum type"); try { var enumValue = (TEnum)Enum.Parse(typeof(TEnum), text.Trim(), true); return enumValue; } catch (Exception) { throw new ArgumentException(string.Format("{0} is not a member of the {1} enumeration.", text, typeof(TEnum).Name)); } } 

MSDN Source

Las respuestas existentes son verdaderas a partir de C # < = 7.2. Sin embargo, hay una solicitud de función de lenguaje C # (vinculada a una solicitud de función corefx ) para permitir lo siguiente;

 public class MyGeneric where TEnum : System.Enum { } 

Al momento de escribir, la función es “En discusión” en las Juntas de Desarrollo del Idioma.

EDITAR

Según la información de nawfal , esto se está introduciendo en C # 7.3 .

Siempre me gustó esto (puedes modificarlo como sea apropiado):

 public static IEnumerable GetEnumValues() { Type enumType = typeof(TEnum); if(!enumType.IsEnum) throw new ArgumentException("Type argument must be Enum type"); Array enumValues = Enum.GetValues(enumType); return enumValues.Cast(); } 

Me encantó la solución de Christopher Currens usando IL, pero para aquellos que no quieren lidiar con el complicado negocio de incluir MSIL en su proceso de comstackción, escribí una función similar en C #.

Sin embargo, tenga en cuenta que no puede usar la restricción genérica como where T : Enum porque Enum es un tipo especial. Por lo tanto, debo verificar si el tipo genérico es enum.

Mi función es:

 public static T GetEnumFromString(string strValue, T defaultValue) { // Check if it realy enum at runtime if (!typeof(T).IsEnum) throw new ArgumentException("Method GetEnumFromString can be used with enums only"); if (!string.IsNullOrEmpty(strValue)) { IEnumerator enumerator = Enum.GetValues(typeof(T)).GetEnumerator(); while (enumerator.MoveNext()) { T temp = (T)enumerator.Current; if (temp.ToString().ToLower().Equals(strValue.Trim().ToLower())) return temp; } } return defaultValue; } 

He encapsulado la solución de Vivek en una clase de utilidad que puedes reutilizar. Tenga en cuenta que todavía debe definir las restricciones de tipo “donde T: struct, IConvertible” en su tipo.

 using System; internal static class EnumEnforcer { ///  /// Makes sure that generic input parameter is of an enumerated type. ///  /// Type that should be checked. /// Name of the type parameter. /// Name of the method which accepted the parameter. public static void EnforceIsEnum(string typeParameterName, string methodName) where T : struct, IConvertible { if (!typeof(T).IsEnum) { string message = string.Format( "Generic parameter {0} in {1} method forces an enumerated type. Make sure your type parameter {0} is an enum.", typeParameterName, methodName); throw new ArgumentException(message); } } ///  /// Makes sure that generic input parameter is of an enumerated type. ///  /// Type that should be checked. /// Name of the type parameter. /// Name of the method which accepted the parameter. /// Name of the input parameter of this page. public static void EnforceIsEnum(string typeParameterName, string methodName, string inputParameterName) where T : struct, IConvertible { if (!typeof(T).IsEnum) { string message = string.Format( "Generic parameter {0} in {1} method forces an enumerated type. Make sure your input parameter {2} is of correct type.", typeParameterName, methodName, inputParameterName); throw new ArgumentException(message); } } ///  /// Makes sure that generic input parameter is of an enumerated type. ///  /// Type that should be checked. /// Message to show in case T is not an enum. public static void EnforceIsEnum(string exceptionMessage) where T : struct, IConvertible { if (!typeof(T).IsEnum) { throw new ArgumentException(exceptionMessage); } } } 

Creé un método de extensión to get integer value from enum eche un vistazo a la implementación del método

 public static int ToInt(this T soure) where T : IConvertible//enum { if (typeof(T).IsEnum) { return (int) (IConvertible)soure;// the tricky part } //else // throw new ArgumentException("T must be an enumerated type"); return soure.ToInt32(CultureInfo.CurrentCulture); } 

esto es uso

 MemberStatusEnum.Activated.ToInt()// using extension Method (int) MemberStatusEnum.Activated //the ordinary way 

Como se dijo en otras respuestas antes; aunque esto no se puede express en código fuente, en realidad se puede hacer en el nivel IL. La respuesta de @Christopher Currens muestra cómo IL lo hace.

Con Fody s Add-In ExtraConstraints.Fody hay una manera muy simple, completa con herramientas de construcción, para lograr esto. Simplemente agregue sus paquetes Nuget ( Fody , ExtraConstraints.Fody ) a su proyecto y agregue las restricciones de la siguiente manera (Extracto del archivo Léame de ExtraConstraints):

 public void MethodWithEnumConstraint< [EnumConstraint] T>() {...} public void MethodWithTypeEnumConstraint< [EnumConstraint(typeof(ConsoleColor))] T>() {...} 

y Fody agregará el IL necesario para que la restricción esté presente. También tenga en cuenta la característica adicional de restringir a los delegates:

 public void MethodWithDelegateConstraint< [DelegateConstraint] T> () {...} public void MethodWithTypeDelegateConstraint< [DelegateConstraint(typeof(Func))] T> () {...} 

Con respecto a los Enums, es posible que también desee tomar nota de Enums.NET, que es muy interesante.

Si está bien usar el casting directo luego, supongo que puedes usar la clase base System.Enum en tu método, donde sea necesario. Solo necesita reemplazar los parámetros de tipo con cuidado. Entonces la implementación del método sería como:

 public static class EnumUtils { public static Enum GetEnumFromString(string value, Enum defaultValue) { if (string.IsNullOrEmpty(value)) return defaultValue; foreach (Enum item in Enum.GetValues(defaultValue.GetType())) { if (item.ToString().ToLower().Equals(value.Trim().ToLower())) return item; } return defaultValue; } } 

Entonces puedes usarlo como:

 var parsedOutput = (YourEnum)EnumUtils.GetEnumFromString(someString, YourEnum.DefaultValue); 

In java, you would use…

  SomeClass { } 

Pretty straightforward, that.