Quelqu'un connaît-il une bonne solution de contournement pour l'absence d'une contrainte générique d'énumération?


89

Ce que je veux faire, c'est quelque chose comme ceci: j'ai des énumérations avec des valeurs marquées combinées.

public static class EnumExtension
{
    public static bool IsSet<T>( this T input, T matchTo ) 
        where T:enum //the constraint I want that doesn't exist in C#3
    {    
        return (input & matchTo) != 0;
    }
}

Alors je pourrais faire:

MyEnum tester = MyEnum.FlagA | MyEnum.FlagB

if( tester.IsSet( MyEnum.FlagA ) )
    //act on flag a

Malheureusement, le générique de C # où les contraintes n'ont aucune restriction d'énumération, seulement class et struct. C # ne voit pas les énumérations comme des structures (même s'il s'agit de types valeur), je ne peux donc pas ajouter de types d'extension comme celui-ci.

Quelqu'un connaît-il une solution de contournement?


2
Keith: téléchargez la version 0.0.0.2 de UnconstrainedMelody - J'ai implémenté HasAll et HasAny. Prendre plaisir.
Jon Skeet le

Qu'entendez-vous par «C # ne voit pas les énumérations comme des structures»? Vous pouvez utiliser des types enum comme paramètres de type qui sont contraints à structtrès bien.
Timwi

consultez cet article ici: codeproject.com/KB/cs/ExtendEnum.aspx « IsValidEnumValue » ou méthodes « IsFlagsEnumDefined » sont probablement la réponse à votre question.
dmihailescu

1
Votez pour cette idée uservoice , si vous souhaitez un jour la voir intégrée dans .net.
Matthieu

11
C # 7.3 introduit des contraintes d'énumération.
Marc Sigrist

Réponses:


48

EDIT: Ceci est maintenant en ligne dans la version 0.0.0.2 de UnconstrainedMelody.

(Comme demandé sur mon article de blog sur les contraintes d'énumération . J'ai inclus les faits de base ci-dessous pour une réponse autonome.)

La meilleure solution est d'attendre que je l'inclue dans UnconstrainedMelody 1 . Il s'agit d'une bibliothèque qui prend du code C # avec de "fausses" contraintes telles que

where T : struct, IEnumConstraint

et le transforme en

where T : struct, System.Enum

via une étape de post-construction.

Il ne devrait pas être trop difficile à écrire IsSet... même si la restauration pour les deux à Int64base et des UInt64drapeaux à base pourrait être la partie la plus délicate. (Je sens certaines méthodes d'aide arriver, ce qui me permet essentiellement de traiter toutes les énumérations d'indicateurs comme si elles avaient un type de base UInt64.)

Quel serait votre comportement si vous appeliez

tester.IsSet(MyFlags.A | MyFlags.C)

? Doit-il vérifier que tous les indicateurs spécifiés sont définis? Ce serait mon attente.

J'essaierai de le faire sur le chemin du retour ce soir ... J'espère avoir un éclair rapide sur les méthodes d'énumération utiles pour que la bibliothèque atteigne rapidement une norme utilisable, puis me détendre un peu.

EDIT: Je ne suis pas sûr du IsSetnom, au fait. Options:

  • Comprend
  • Contient
  • HasFlag (ou HasFlags)
  • IsSet (c'est certainement une option)

Les pensées sont les bienvenues. Je suis sûr que cela prendra du temps avant que quoi que ce soit ne soit gravé dans la pierre de toute façon


1 ou soumettez-le sous forme de patch, bien sûr ...


1
Il fallait aller mentionner PostSharp LOL: o postsharp.org/blog/generic-constraints-for-enums-and-delegates
Sam Harwell

1
Ou en fait plus simple HasAny () et HasAll ()
Keith

1
Oui, je suis d'accord que c'est encore mieux. colors.HasAny(Colors.Red | Colors.Blue)ressemble à du code très lisible. =)
Blixt du

1
Ouais, j'aime HasAny et HasAll aussi. Va avec ça.
Jon Skeet du

5
Depuis C # 7.3 (publié en mai 2018), il est possible d'utiliser la contrainte where T : System.Enum. Cela était déjà écrit ailleurs dans le fil; juste pensé que je le répéterais ici.
Jeppe Stig Nielsen


16

Darren, cela fonctionnerait si les types étaient des énumérations spécifiques - pour que les énumérations générales fonctionnent, vous devez les convertir en ints (ou plus probablement en uint) pour faire le calcul booléen:

public static bool IsSet( this Enum input, Enum matchTo )
{
    return ( Convert.ToUInt32( input ) & Convert.ToUInt32( matchTo ) ) != 0;
}

1
Et si vous avez un nombre ridicule de drapeaux, vous pouvez appeler GetTypeCode () sur les arguments et Convert.ToUint64 ()
Kit

Génial, la combinaison de «Enum» et Convert.ToUInt32je n'ai trouvé nulle part ailleurs. AFAIK, c'est la seule solution Pre-Net-4 décente qui fonctionne également en VB. BTW, si matchTopeut avoir plusieurs bits d'indicateur, remplacez-le != 0par == Convert.ToUInt32(matchTo).
ToolmakerSteve

1
Notez que Convert.ToUInt32utilisé avec une énumération utilisera la Convert.ToUInt32(object)surcharge, ce qui signifie que CLR encapsulera d'abord ces valeurs avant de passer ensuite à la ToUInt32méthode. Dans la plupart des cas, cela n'a pas d'importance, mais il est bon de savoir que vous occuperez le GC plutôt que si vous utilisez quelque chose comme celui-ci pour analyser des millions d'énumérations par seconde.
Groo

10

En fait, c'est possible, avec un truc laid. Cependant, il ne peut pas être utilisé pour les méthodes d'extension.

public abstract class Enums<Temp> where Temp : class {
    public static TEnum Parse<TEnum>(string name) where TEnum : struct, Temp {
        return (TEnum)Enum.Parse(typeof(TEnum), name); 
    }
}
public abstract class Enums : Enums<Enum> { }

Enums.IsSet<DateTimeKind>("Local")

Si vous le souhaitez, vous pouvez donner Enums<Temp>un constructeur privé et une classe héritée abstraite imbriquée publique avec Tempas Enum, pour empêcher les versions héritées pour les non-enums.


8

Vous pouvez y parvenir en utilisant IL Weaving et ExtraConstraints

Vous permet d'écrire ce code

public class Sample
{
    public void MethodWithDelegateConstraint<[DelegateConstraint] T> ()
    {        
    }
    public void MethodWithEnumConstraint<[EnumConstraint] T>()
    {
    }
}

Ce qui est compilé

public class Sample
{
    public void MethodWithDelegateConstraint<T>() where T: Delegate
    {
    }

    public void MethodWithEnumConstraint<T>() where T: struct, Enum
    {
    }
}

6

À partir de C # 7.3, vous pouvez utiliser la contrainte Enum sur les types génériques:

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

Si vous souhaitez utiliser une énumération Nullable, vous devez laisser la contrainte de structure orginial:

public static TEnum? TryParse<TEnum>(string value) where TEnum : struct, Enum
{
    if( Enum.TryParse(value, out TEnum res) )
        return res;
    else
        return null;
}

4

Cela ne répond pas à la question d'origine, mais il existe maintenant une méthode dans .NET 4 appelée Enum.HasFlag qui fait ce que vous essayez de faire dans votre exemple


Vote positif car à ce stade, presque tout le monde devrait utiliser .NET 4 (ou supérieur) et devrait donc utiliser cette méthode au lieu d'essayer de la pirater ensemble.
CptRobby

J'ai voté pour. Cependant leur solution utilise la boxe de l'argument flag. .NET 4.0 a maintenant cinq ans.
Jeppe Stig Nielsen

3

La façon dont je le fais est de mettre une contrainte de structure, puis de vérifier que T est une énumération à l'exécution. Cela n'élimine pas complètement le problème, mais le réduit quelque peu


7
où T: struct, IComparable, IFormattable, IConvertible - c'est le plus proche que vous pouvez obtenir d'énumération :)
Kit

1

En utilisant votre code d'origine, dans la méthode, vous pouvez également utiliser la réflexion pour tester que T est une énumération:

public static class EnumExtension
{
    public static bool IsSet<T>( this T input, T matchTo )
    {
        if (!typeof(T).IsEnum)
        {
            throw new ArgumentException("Must be an enum", "input");
        }
        return (input & matchTo) != 0;
    }
}

2
Merci, mais cela transforme un problème de compilation (la contrainte where) en un problème d'exécution (votre exception). De plus, vous devrez toujours convertir les entrées en entiers avant de pouvoir faire quoi que ce soit avec eux.
Keith du

1

Voici un code que je viens de créer et qui semble fonctionner comme vous le souhaitez sans avoir à faire quoi que ce soit de trop fou. Ce n'est pas limité aux énumérations définies comme Flags, mais il peut toujours y avoir une vérification si nécessaire.

public static class EnumExtensions
{
    public static bool ContainsFlag(this Enum source, Enum flag)
    {
        var sourceValue = ToUInt64(source);
        var flagValue = ToUInt64(flag);

        return (sourceValue & flagValue) == flagValue;
    }

    public static bool ContainsAnyFlag(this Enum source, params Enum[] flags)
    {
        var sourceValue = ToUInt64(source);

        foreach (var flag in flags)
        {
            var flagValue = ToUInt64(flag);

            if ((sourceValue & flagValue) == flagValue)
            {
                return true;
            }
        }

        return false;
    }

    // found in the Enum class as an internal method
    private static ulong ToUInt64(object value)
    {
        switch (Convert.GetTypeCode(value))
        {
            case TypeCode.SByte:
            case TypeCode.Int16:
            case TypeCode.Int32:
            case TypeCode.Int64:
                return (ulong)Convert.ToInt64(value, CultureInfo.InvariantCulture);

            case TypeCode.Byte:
            case TypeCode.UInt16:
            case TypeCode.UInt32:
            case TypeCode.UInt64:
                return Convert.ToUInt64(value, CultureInfo.InvariantCulture);
        }

        throw new InvalidOperationException("Unknown enum type.");
    }
}

0

si quelqu'un a besoin d'un IsSet générique (créé hors de la boîte à la volée pourrait être amélioré), et / ou d'une chaîne en conversion à la volée Enum (qui utilise EnumConstraint présentée ci-dessous):

  public class TestClass
  { }

  public struct TestStruct
  { }

  public enum TestEnum
  {
    e1,    
    e2,
    e3
  }

  public static class TestEnumConstraintExtenssion
  {

    public static bool IsSet<TEnum>(this TEnum _this, TEnum flag)
      where TEnum : struct
    {
      return (((uint)Convert.ChangeType(_this, typeof(uint))) & ((uint)Convert.ChangeType(flag, typeof(uint)))) == ((uint)Convert.ChangeType(flag, typeof(uint)));
    }

    //public static TestClass ToTestClass(this string _this)
    //{
    //  // #generates compile error  (so no missuse)
    //  return EnumConstraint.TryParse<TestClass>(_this);
    //}

    //public static TestStruct ToTestStruct(this string _this)
    //{
    //  // #generates compile error  (so no missuse)
    //  return EnumConstraint.TryParse<TestStruct>(_this);
    //}

    public static TestEnum ToTestEnum(this string _this)
    {
      // #enum type works just fine (coding constraint to Enum type)
      return EnumConstraint.TryParse<TestEnum>(_this);
    }

    public static void TestAll()
    {
      TestEnum t1 = "e3".ToTestEnum();
      TestEnum t2 = "e2".ToTestEnum();
      TestEnum t3 = "non existing".ToTestEnum(); // default(TestEnum) for non existing 

      bool b1 = t3.IsSet(TestEnum.e1); // you can ommit type
      bool b2 = t3.IsSet<TestEnum>(TestEnum.e2); // you can specify explicite type

      TestStruct t;
      // #generates compile error (so no missuse)
      //bool b3 = t.IsSet<TestEnum>(TestEnum.e1);

    }

  }

Si quelqu'un a encore besoin d'exemple chaud pour créer la contrainte de codage Enum:

using System;

/// <summary>
/// would be same as EnumConstraint_T&lt;Enum>Parse&lt;EnumType>("Normal"),
/// but writen like this it abuses constrain inheritence on System.Enum.
/// </summary>
public class EnumConstraint : EnumConstraint_T<Enum>
{

}

/// <summary>
/// provides ability to constrain TEnum to System.Enum abusing constrain inheritence
/// </summary>
/// <typeparam name="TClass">should be System.Enum</typeparam>
public abstract class EnumConstraint_T<TClass>
  where TClass : class
{

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

  public static bool TryParse<TEnum>(string value, out TEnum evalue)
    where TEnum : struct, TClass // struct is required to ignore non nullable type error
  {
    evalue = default(TEnum);
    return Enum.TryParse<TEnum>(value, out evalue);
  }

  public static TEnum TryParse<TEnum>(string value, TEnum defaultValue = default(TEnum))
    where TEnum : struct, TClass // struct is required to ignore non nullable type error
  {    
    Enum.TryParse<TEnum>(value, out defaultValue);
    return defaultValue;
  }

  public static TEnum Parse<TEnum>(string value, TEnum defaultValue = default(TEnum))
    where TEnum : struct, TClass // struct is required to ignore non nullable type error
  {
    TEnum result;
    if (Enum.TryParse<TEnum>(value, out result))
      return result;
    return defaultValue;
  }

  public static TEnum Parse<TEnum>(ushort value)
  {
    return (TEnum)(object)value;
  }

  public static sbyte to_i1<TEnum>(TEnum value)
  {
    return (sbyte)(object)Convert.ChangeType(value, typeof(sbyte));
  }

  public static byte to_u1<TEnum>(TEnum value)
  {
    return (byte)(object)Convert.ChangeType(value, typeof(byte));
  }

  public static short to_i2<TEnum>(TEnum value)
  {
    return (short)(object)Convert.ChangeType(value, typeof(short));
  }

  public static ushort to_u2<TEnum>(TEnum value)
  {
    return (ushort)(object)Convert.ChangeType(value, typeof(ushort));
  }

  public static int to_i4<TEnum>(TEnum value)
  {
    return (int)(object)Convert.ChangeType(value, typeof(int));
  }

  public static uint to_u4<TEnum>(TEnum value)
  {
    return (uint)(object)Convert.ChangeType(value, typeof(uint));
  }

}

j'espère que cela aide quelqu'un.


0

Je voulais juste ajouter Enum comme contrainte générique.

Bien que ce ne soit que pour une petite méthode d'aide utilisant ExtraConstraints est un peu trop lourde pour moi.

J'ai décidé de simplement créer une structcontrainte et d'ajouter une vérification à l'exécution IsEnum. Pour convertir une variable de T en Enum, je l'ai d'abord convertie en objet.

    public static Converter<T, string> CreateConverter<T>() where T : struct
    {
        if (!typeof(T).IsEnum) throw new ArgumentException("Given Type is not an Enum");
        return new Converter<T, string>(x => ((Enum)(object)x).GetEnumDescription());
    }
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.