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


Estoy construyendo una función para extender el Enum.Parse concepto 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<T>(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;
}

Estoy consiguiendo una restricción de error no puede ser una clase especial System.Enum.

Bastante, pero ¿hay alguna solución para permitir un Enum genérico, o voy a tener que imitar el Parse Funciona y pasa 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<T>(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 un compilador impuso una solución genérica segura en MSIL o F # a continuación, que bien vale la pena mirar, y un voto positivo. Eliminaré esta edición si la solución aparece más arriba en la página.


945
2017-09-17 01:56


origen


Respuestas:


Ya que Enum Tipo de implementos IConvertible interfaz, una mejor implementación debería ser algo como esto:

public T GetEnumFromString<T>(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 tipos de valores implementando IConvertible. Sin embargo, las posibilidades son raras.


853
2017-09-17 04:13



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

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

public static Dictionary<int, string> EnumNamedValues<T>() where T : System.Enum
{
    var result = new Dictionary<int, string>();
    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<valuetype .ctor ([mscorlib]System.Enum) T>(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
  }
} 

Que genera una función que haría se ve así, si fuera válido C #:

T GetEnumFromString<T>(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 System.Enum. También es un poco fastidioso, porque se compila en un ensamblaje por separado. Sin embargo, esto no significa que deba implementarlo de esa manera.

Al quitar la línea .assembly MyThing{} e invocar 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 en que puede agregarlos como parte de su ensamblaje sería ejecutar csc.exe usted mismo usando el /addmodule:{files} argumento de línea de comando. No sería también 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 una restricción genérica en enum es posible 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 naturalmente IL considerablemente diferente (el código es muy diferente) y depende del FSharp.Core biblioteca, que, al igual que cualquier otra biblioteca externa, debe 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);

419
2017-11-10 21:46



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<TEnum>(string value)
where TEnum : struct, Enum { ... }

C # ≤ 7.2

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

public abstract class EnumClassUtils<TClass>
where TClass : class
{

    public static TEnum Parse<TEnum>(string value)
    where TEnum : struct, TClass
    {
        return (TEnum) Enum.Parse(typeof(TEnum), value);
    }

}

public class EnumUtils : EnumClassUtils<Enum>
{
}

Uso:

EnumUtils.Parse<SomeEnum>("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 para   S para tener la restricción de tipo de valor y T para tener el tipo de referencia   restricción. Efectivamente esto limita T a los tipos System.Object,   System.ValueType, System.Enum y cualquier tipo de interfaz.


149
2018-02-15 15:16



Editar

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

public abstract class ConstrainedEnumParser<TClass> 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 <System.Enum>-constrained type EnumUtils
    public static TEnum Parse<TEnum>(string value, bool ignoreCase = false) where TEnum : struct, TClass
    {
        return (TEnum)Enum.Parse(typeof(TEnum), value, ignoreCase);
    }
    public static bool TryParse<TEnum>(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<TEnum>(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<System.Enum>
// reference type constraint to any <System.Enum>
{
    // call to parse will then contain constraint to specific <System.Enum>-class
}

Ejemplos de uso:

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

Antiguo

Mis antiguas mejoras en La respuesta de Vivek mediante el uso de los comentarios y 'nuevos' desarrollos:

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

Resultando en:

public static class EnumUtils
{
    public static TEnum ParseEnum<TEnum>(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;
    }
}

29
2018-05-24 14:07



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<T> {
    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.


18
2017-09-17 02:32



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

public static T ParseEnum<T>(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);
    }
}

11
2017-09-17 02:24



Traté de mejorar el código un poco:

public T LoadEnum<T>(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;
}

9
2017-12-16 11:24



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<T>(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 sintaxis necesita al menos la versión 15.7. Puede encontrar eso también mencionado en las notas de lanzamiento de Microsoft, vea Notas de la versión de Visual Studio 2017 15.7. Gracias @MohamedElshawaf por señalar esta pregunta válida.

Pls en cuenta también que en mi caso ReSharper 2018.1 Al momento de redactar esta edición no admite todavía C # 7.3. Tener ReSharper activado resalta la restricción Enum como un error diciéndome No se puede utilizar 'System.Array', 'System.Delegate', 'System.Enum', 'System.ValueType', 'objeto' como tipo de parámetro de restricción. ReSharper sugiere una solución rápida para Eliminar la restricción 'Enum' de tipo parámetro T del método 

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


6
2018-05-10 09:55