1446 votes

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

Je suis en train de construire une fonction pour étendre la fonction Enum.Parse concept qui

  • Permet d'analyser une valeur par défaut au cas où une valeur d'énumération ne serait 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;
}

J'obtiens une erreur La contrainte ne peut pas être une classe spéciale. System.Enum .

C'est vrai, mais existe-t-il une solution pour permettre l'utilisation d'un Enum générique, ou vais-je devoir imiter la méthode Parse et de passer un type en tant qu'attribut, ce qui impose à votre code l'exigence de la boîte laide.

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

J'ai choisi (j'ai laissé la boucle pour maintenir l'insensibilité à la casse - je l'utilise pour analyser le 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;
    }
}

EDIT : (16 février 2015) Christopher Currens a posté une solution générique à sécurité de type imposée par le compilateur en MSIL ou F# ci-dessous, qui mérite un coup d'œil et un vote positif. Je supprimerai cette modification si la solution apparaît plus haut sur la page.

EDIT 2 : (13 avr. 2021) Comme ce problème a été résolu et pris en charge depuis C# 7.3, j'ai modifié la réponse acceptée, bien qu'une lecture complète des meilleures réponses en vaille la peine pour des raisons académiques et historiques :)

12 votes

Peut-être que vous devrait utiliser ToUpperInvariant() au lieu de ToLower()...

0 votes

Pourquoi les méthodes d'extension sont-elles réservées aux types de référence ?

34 votes

@Shimmy : Dès que vous passez un type de valeur à la méthode d'extension, vous travaillez sur une copie de ce type de valeur, et vous ne pouvez donc pas modifier son état.

1104voto

Vivek Points 7254

Depuis Enum Le type met en œuvre IConvertible une meilleure implémentation 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 de transmettre des types de valeurs mettant en œuvre IConvertible . Les chances sont toutefois rares.

0 votes

Il semble que cela ne concerne que vs2008 et les versions plus récentes, n'est-ce pas ? ou peut-être que ce n'est pas dans vb2005 ?

2 votes

Les génériques sont disponibles depuis .NET 2.0. Ils sont donc également disponibles dans vb 2005.

51 votes

Eh bien, rendez-le encore plus contraignant, si vous choisissez de suivre cette voie... utilisez "class TestClass<T> where T : struct, IComparable, IFormattable, IConvertible"

940voto

Cette fonctionnalité est enfin prise en charge dans C# 7.3 !

L'extrait suivant (tiré de les échantillons dotnet ) montre comment :

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 à ce que la version du langage dans votre projet C# soit la version 7.3.


Réponse originale ci-dessous :

Je suis en retard dans le jeu, mais j'ai pris cela comme un défi pour voir comment cela pouvait ê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 ce petit....thing

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

Ce qui génère une fonction qui serait ressemblerait à ceci, s'il s'agissait d'un code C# valide :

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 en MSIL plutôt qu'en C#, le seul avantage étant que vous pouvez contraindre cette méthode en System.Enum . C'est aussi un peu dommage, parce qu'il est compilé dans un assemblage séparé. Cependant, cela ne signifie pas que vous devez le déployer de cette façon.

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

ilasm.exe /DLL /OUTPUT=MyThing.netmodule

vous obtenez un netmodule au lieu d'un assemblage.

Malheureusement, VS2010 (et les versions antérieures, évidemment) ne prend pas en charge l'ajout de références netmodule, ce qui signifie que vous devez le laisser dans deux assemblages distincts lorsque vous déboguez. La seule façon de les ajouter à votre assemblage serait d'exécuter csc.exe vous-même en utilisant la commande /addmodule:{files} en ligne de commande. Il ne serait pas aussi douloureux dans un script de MSBuild. Bien sûr, si vous êtes courageux ou stupide, vous pouvez exécuter csc vous-même manuellement à chaque fois. Et cela devient certainement plus compliqué lorsque plusieurs assemblages ont besoin d'y accéder.

Il est donc possible de le faire en .Net. Cela vaut-il la peine de faire un effort supplémentaire ? Hum, eh bien, je suppose que je vous laisse décider de cela.


Solution F# comme alternative

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

Ce dernier est plus facile à maintenir car il s'agit d'un langage bien connu avec un support complet de l'IDE Visual Studio, mais vous avez toujours besoin d'un projet séparé dans votre solution pour ce langage. Cependant, il produit naturellement un IL considérablement différent (le code es très différente) et elle s'appuie sur la FSharp.Core qui, comme toute autre bibliothèque externe, doit être intégrée à votre distribution.

Voici comment vous pouvez l'utiliser (en gros, la même chose que la solution MSIL), et pour montrer qu'elle échoue correctement sur des structs autrement synonymes :

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

79 votes

Oui, c'est très dur. J'ai le plus grand respect pour quelqu'un qui peut coder en IL, y savoir comment les fonctionnalités sont prises en charge au niveau supérieur du langage - un niveau que beaucoup d'entre nous considèrent encore comme un niveau inférieur aux applications, aux règles commerciales, aux interfaces utilisateur, aux bibliothèques de composants, etc.

2 votes

@ruslan - il est dit que vous ne pouvez pas accomplir cela en c# dans le premier paragraphe de la réponse. C'est en fait ce que montre cette réponse : très possible en cil (puisque le code ci-dessus fonctionne avec succès lorsqu'il est utilisé dans d'autres langages .net), mais pas possible en C# en soi.

13 votes

Ce que j'aimerais vraiment savoir, c'est pourquoi l'équipe C# n'a pas encore commencé à autoriser cela, puisque c'est déjà supporté par MSIL.

35voto

Yahoo Serious Points 964

Editer

La question a été superbement résolue par Julien Lebosquain . Je voudrais également compléter sa réponse par ignoreCase , defaultValue et des arguments facultatifs, tout en ajoutant 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
}

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

Ancienne

Mes anciennes améliorations sur Réponse de Vivek en utilisant les commentaires et les "nouveaux" développements :

  • utiliser TEnum par souci de clarté pour les utilisateurs
  • ajout de contraintes d'interface pour une vérification supplémentaire des contraintes
  • laisser TryParse poignée ignoreCase avec le paramètre existant (introduit dans VS2010/.Net 4)
  • utiliser éventuellement l'option générique default valeur (introduit dans VS2005/.Net 2)
  • utiliser arguments facultatifs (introduit dans VS2010/.Net 4) avec des valeurs par défaut, par exemple defaultValue y ignoreCase

qui en résulte :

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

20voto

Karg Points 585

Vous pouvez définir un constructeur statique pour la classe qui vérifiera que le type T est un enum et lèvera 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 de parse, vous pouvez simplement utiliser Enum.Parse(typeof(T), input, true) pour convertir la chaîne en enum. Le dernier paramètre true permet d'ignorer le cas de l'entrée.

2 votes

C'est une bonne option pour les classes génériques - mais bien sûr, cela ne sert à rien pour les méthodes génériques.

1 votes

De plus, ceci n'est pas non plus appliqué au moment de la compilation, vous sauriez seulement que vous avez fourni une valeur non Enum T lors de l'exécution du constructeur. Mais c'est bien plus agréable que d'attendre un constructeur d'instance.

19voto

Nescio Points 12613

Vous pouvez contraindre un paramètre de type générique à être un type de valeur (tel qu'un int, un bool et un enum) ou une structure personnalisée à l'aide de la contrainte struct :

public class MyClass<T> where T : struct
{
   //...
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X