Question Créer une méthode générique contraignant T à un Enum


Je construis une fonction pour étendre la Enum.Parse concept que

  • Permet d'analyser une valeur par défaut dans le cas où une valeur Enum n'est pas trouvée
  • Est insensible à la casse

J'ai donc écrit ce qui suit:

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

Je reçois une contrainte d'erreur qui ne peut pas être une classe spéciale System.Enum.

Assez juste, mais existe-t-il une solution de contournement pour permettre un Enum générique, ou vais-je devoir imiter le Parse fonction et passer un type en tant qu'attribut, ce qui force l'exigence de boxe laide à votre code.

MODIFIER Toutes les suggestions ci-dessous ont été grandement appréciées, merci.

Je me suis installé (j'ai quitté la boucle pour conserver l'insensibilité à la casse - je l'utilise lors de l'analyse 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;
    }
}

MODIFIER: (16 février 2015) Julien Lebosquain a récemment posté une solution générique de type sécurisé sécurisée par compilateur dans MSIL ou F # ci-dessous, qui vaut bien un coup d'oeil, et un upvote. Je vais supprimer cette modification si la solution fait des bulles plus haut sur la page.


945
2017-09-17 01:56


origine


Réponses:


Depuis Enum Type d'outils IConvertible interface, une meilleure mise en œuvre devrait être quelque chose comme ceci:

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

   //...
}

Cela permettra toujours le passage de types de valeur mettant en œuvre IConvertible. Les chances sont rares cependant.


853
2017-09-17 04:13



Cette fonctionnalité est enfin supportée en C # 7.3!

L'extrait suivant (de les échantillons dotnet) démontre qu'il utilise:

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

Veillez à définir la version de votre langue dans votre projet C # sur la version 7.3.


Réponse originale ci-dessous:

Je suis en retard au jeu, mais j'ai pris comme un défi de voir comment cela pourrait être fait. Ce n'est pas possible en C # (ou VB.NET, mais faites défiler vers le bas pour F #), mais est possible dans MSIL. J'ai écrit cette petite chose ...

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

Qui génère une fonction qui aurait ressemble à ceci, s'il était valide C #:

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

Puis avec le code C # suivant:

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
}

Malheureusement, cela signifie que cette partie de votre code est écrite dans MSIL au lieu de C #, avec le seul avantage supplémentaire que vous êtes capable de contraindre cette méthode en System.Enum. C'est aussi un peu bête, car il est compilé dans un ensemble séparé. Cependant, cela ne signifie pas que vous devez le déployer de cette façon.

En enlevant la ligne .assembly MyThing{} et invoquant ilasm comme suit:

ilasm.exe /DLL /OUTPUT=MyThing.netmodule

vous obtenez un netmodule au lieu d'un assemblage.

Malheureusement, VS2010 (et plus tôt, évidemment) ne supporte pas l'ajout de références de netmodule, ce qui signifie que vous devrez le laisser dans deux assemblages séparés lorsque vous déboguez. La seule façon de les ajouter dans le cadre de votre assembly serait de lancer csc.exe en utilisant /addmodule:{files} argument de ligne de commande. Ce ne serait pas aussi douloureux dans un script MSBuild. Bien sûr, si vous êtes courageux ou stupide, vous pouvez exécuter manuellement le csc manuellement à chaque fois. Et cela devient certainement plus compliqué car plusieurs assemblages ont besoin d'y accéder.

Donc, cela peut être fait en .Net. Cela vaut-il l'effort supplémentaire? Euh, eh bien, je suppose que je te laisserai décider de ça.


F # Solution comme alternative

Crédit supplémentaire: Il s'avère qu'une restriction générique sur enum est possible dans au moins un autre langage .NET en plus 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

Celui-ci est plus facile à maintenir car il s'agit d'un langage bien connu avec la prise en charge complète de Visual Studio IDE, mais vous avez toujours besoin d'un projet distinct pour votre solution. Cependant, il produit naturellement des IL très différents (le code est très différent) et il repose sur le FSharp.Core bibliothèque, qui, comme toute autre bibliothèque externe, doit faire partie de votre distribution.

Voici comment vous pouvez l'utiliser (fondamentalement la même chose que la solution MSIL), et pour montrer qu'il échoue correctement sur les structs:

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

À partir de C # 7.3 (disponible avec Visual Studio 2017 ≥ v15.7), ce code est maintenant complètement valide:

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

C # ≤ 7.2

Vous pouvez avoir une véritable contrainte enum imposée par le compilateur en abusant de l'héritage des contraintes. Le code suivant spécifie à la fois class et un struct contraintes en même temps:

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

Usage:

EnumUtils.Parse<SomeEnum>("value");

Remarque: ceci est spécifiquement indiqué dans la spécification de langage C # 5.0:

Si le paramètre de type S dépend du paramètre de type T alors:   [...] Il est valable pour   S pour avoir la contrainte de type valeur et T pour avoir le type de référence   contrainte. Effectivement cela limite T aux types System.Object,   System.ValueType, System.Enum et tout type d'interface.


149
2018-02-15 15:16



modifier

La question a maintenant été superbement répondu par Julien Lebosquain. Je voudrais aussi étendre sa réponse avec ignoreCase, defaultValue et des arguments facultatifs, tout en ajoutant TryParse et 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
}

Exemples d'utilisation:

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);

Vieux

Mes anciennes améliorations sur La réponse de Vivek en utilisant les commentaires et les 'nouveaux' développements:

  • utilisation TEnum pour plus de clarté pour les utilisateurs
  • ajouter plus de contraintes d'interface pour un contrôle supplémentaire des contraintes
  • laisser TryParse manipuler ignoreCase avec le paramètre existant (introduit dans VS2010 / .Net 4)
  • utiliser éventuellement le générique default valeur (introduit dans VS2005 / .Net 2)
  • utilisation arguments optionnels(introduit dans VS2010 / .Net 4) avec des valeurs par défaut, pour defaultValue et ignoreCase

résultant 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



Vous pouvez définir un constructeur statique pour la classe qui vérifiera que le type T est une énumération et lancera une exception si ce n'est pas le cas. C'est la méthode mentionnée par Jeffery Richter dans son livre CLR via C #.

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

Ensuite, dans la méthode parse, vous pouvez simplement utiliser Enum.Parse (typeof (T), input, true) pour convertir de la chaîne en enum. Le dernier vrai paramètre est pour ignorer le cas de l'entrée.


18
2017-09-17 02:32



J'ai modifié l'échantillon par dimarzionist. Cette version ne fonctionnera qu'avec Enums et ne laissera pas passer les structures.

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



J'ai essayé d'améliorer le code un peu:

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