¿Alguien conoce una buena solución para la falta de una restricción genérica enum?

89

Lo que quiero hacer es algo como esto: tengo enumeraciones con valores marcados combinados.

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

Entonces podría hacer:

MyEnum tester = MyEnum.FlagA | MyEnum.FlagB

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

Desafortunadamente, C # es genérico donde las restricciones no tienen restricción de enumeración, solo clase y estructura. C # no ve las enumeraciones como estructuras (aunque son tipos de valor), por lo que no puedo agregar tipos de extensión como este.

¿Alguien sabe una solución?

Keith
fuente
2
Keith: descargue la versión 0.0.0.2 de UnconstrainedMelody - He implementado HasAll y HasAny. Disfrutar.
Jon Skeet
¿Qué quieres decir con "C # no ve las enumeraciones como estructuras"? Puede usar tipos de enumeración como parámetros de tipo que están limitados a structmuy bien.
Timwi
consulte este artículo aquí: codeproject.com/KB/cs/ExtendEnum.aspx Los métodos 'IsValidEnumValue' o 'IsFlagsEnumDefined' son probablemente la respuesta a su pregunta.
dmihailescu
1
Vote por esta idea de uservoice , si le gustaría verla incorporada en .net algún día.
Matthieu
11
C # 7.3 introduce restricciones de enumeración.
Marc Sigrist

Respuestas:

48

EDITAR: Esto ahora está disponible en la versión 0.0.0.2 de UnconstrainedMelody.

(Como se solicitó en la publicación de mi blog sobre las restricciones de enumeración . He incluido los datos básicos a continuación en aras de una respuesta independiente).

La mejor solución es esperar a que lo incluya en UnconstrainedMelody 1 . Esta es una biblioteca que toma código C # con restricciones "falsas" como

where T : struct, IEnumConstraint

y lo convierte en

where T : struct, System.Enum

a través de un paso posterior a la construcción.

No debería ser demasiado difícil de escribir IsSet... aunque atender tanto a las banderas Int64basadas como a las UInt64basadas en banderas podría ser la parte complicada. (Huelo algunos métodos de ayuda que se acercan, básicamente me permiten tratar cualquier enumeración de banderas como si tuviera un tipo base de UInt64).

¿Cuál le gustaría que fuera el comportamiento si llamara

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

? ¿Debería comprobar que todas las banderas especificadas están configuradas? Esa sería mi expectativa.

Trataré de hacer esto en el camino a casa esta noche ... Espero tener un bombardeo rápido sobre métodos de enumeración útiles para que la biblioteca alcance un estándar utilizable rápidamente, luego relajarme un poco.

EDITAR: No estoy seguro IsSetcomo nombre, por cierto. Opciones:

  • Incluye
  • Contiene
  • HasFlag (o HasFlags)
  • IsSet (ciertamente es una opción)

Los pensamientos son bienvenidos. Estoy seguro de que pasará un tiempo antes de que algo quede grabado en piedra de todos modos ...


1 o enviarlo como parche, por supuesto ...

Jon Skeet
fuente
1
Tenías que ir y mencionar PostSharp LOL: o postsharp.org/blog/generic-constraints-for-enums-and-delegates
Sam Harwell
1
O en realidad más simple HasAny () y HasAll ()
Keith
1
Sí, estoy de acuerdo en que eso es incluso mejor. colors.HasAny(Colors.Red | Colors.Blue)parece un código muy legible. =)
Blixt
1
Sí, también me gustan HasAny y HasAll. Irá con eso.
Jon Skeet
5
Desde C # 7.3 (publicado en mayo de 2018), es posible usar la restricción where T : System.Enum. Esto ya estaba escrito en otra parte del hilo; solo pensé que lo repetiría aquí.
Jeppe Stig Nielsen
16

Darren, eso funcionaría si los tipos fueran enumeraciones específicas; para que las enumeraciones generales funcionen, debe convertirlas en ints (o más probablemente uint) para hacer las matemáticas booleanas:

public static bool IsSet( this Enum input, Enum matchTo )
{
    return ( Convert.ToUInt32( input ) & Convert.ToUInt32( matchTo ) ) != 0;
}
Ronnie
fuente
1
Y si tiene una cantidad ridícula de banderas, puede llamar a GetTypeCode () en los argumentos y Convert.ToUint64 ()
Kit
Impresionante, la combinación de 'Enum' y Convert.ToUInt32no encontré en ningún otro lugar. AFAIK, es la única solución Pre-Net-4 decente que también funciona en VB. Por cierto, si matchTopuede tener varios bits de bandera, reemplácelos != 0con == Convert.ToUInt32(matchTo).
ToolmakerSteve
1
Tenga en cuenta que Convert.ToUInt32usado con una enumeración usará la Convert.ToUInt32(object)sobrecarga, lo que significa que CLR primero encuadrará estos valores antes de pasarlos al ToUInt32método. En la mayoría de los casos, esto no importará, pero es bueno saber que mantendrá el GC bastante ocupado si usa algo como esto para analizar millones de enumeraciones por segundo.
Groo
10

De hecho, es posible, con un truco feo. Sin embargo, no se puede utilizar para métodos de extensión.

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 lo desea, puede dar Enums<Temp>un constructor privado y una clase heredada abstracta anidada pública con Tempas Enum, para evitar versiones heredadas para no enumerados.

SLaks
fuente
8

Puede lograr esto usando IL Weaving y ExtraConstraints

Te permite escribir este código

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

Que se compila

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

    public void MethodWithEnumConstraint<T>() where T: struct, Enum
    {
    }
}
Simón
fuente
6

A partir de C # 7.3, puede usar la restricción Enum en tipos genéricos:

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

Si desea utilizar una enumeración que acepta valores NULL, debe dejar la restricción de estructura original:

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

Esto no responde a la pregunta original, pero ahora hay un método en .NET 4 llamado Enum.HasFlag que hace lo que está tratando de hacer en su ejemplo

Phil Devaney
fuente
Votado a favor porque en este punto, la mayoría de las personas deberían usar .NET 4 (o superior) y, por lo tanto, deberían usar este método en lugar de intentar piratearlo juntos.
CptRobby
Voto a favor. Sin embargo, su solución usa el boxeo del argumento flag. .NET 4.0 ya tiene cinco años.
Jeppe Stig Nielsen
3

La forma en que lo hago es poner una restricción de estructura, luego verificar que T es una enumeración en tiempo de ejecución. Esto no elimina el problema por completo, pero lo reduce un poco.

thecoop
fuente
7
donde T: struct, IComparable, IFormattable, IConvertible - esto es lo más cercano que puede llegar a enum :)
Kit
1

Usando su código original, dentro del método también puede usar la reflexión para probar que T es una enumeración:

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;
    }
}
Scott Dorman
fuente
2
Gracias, pero eso convierte un problema de tiempo de compilación (la restricción where) en uno de tiempo de ejecución (su excepción). Además, aún necesitaría convertir las entradas a ints antes de poder hacer algo con ellas.
Keith
1

Aquí hay un código que acabo de hacer y que parece funcionar como quieres sin tener que hacer nada demasiado loco. No está restringido solo a enumeraciones configuradas como banderas, pero siempre puede haber una marca de verificación si es necesario.

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.");
    }
}
Brian Surowiec
fuente
0

si alguien necesita IsSet genérico (creado fuera de la caja sobre la marcha podría mejorarse), yo cadena a la conversión de Enum onfly (que usa EnumConstraint que se presenta a continuación):

  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 alguien todavía necesita un ejemplo caliente para crear una restricción de codificación 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));
  }

}

Espero que esto ayude a alguien.

Solar
fuente
0

Solo quería agregar Enum como una restricción genérica.

Si bien esto es solo para un pequeño método auxiliar que usa ExtraConstraints es demasiado para mí.

Decidí simplemente crear una structrestricción y agregar una verificación de tiempo de ejecución para IsEnum. Para convertir una variable de T a Enum, primero la lanzo al objeto.

    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());
    }
Jürgen Steinblock
fuente