Alguém conhece uma boa solução alternativa para a falta de uma restrição genérica de enum?

89

O que quero fazer é algo assim: tenho enums com valores sinalizados 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;
    }
}

Então eu poderia fazer:

MyEnum tester = MyEnum.FlagA | MyEnum.FlagB

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

Infelizmente, o C # é genérico em que as restrições não têm restrição de enum, apenas classe e estrutura. C # não vê enums como structs (embora sejam tipos de valor), portanto, não posso adicionar tipos de extensão como este.

Alguém conhece uma solução alternativa?

Keith
fonte
2
Keith: baixe a versão 0.0.0.2 de UnconstrainedMelody - implementei HasAll e HasAny. Aproveitar.
Jon Skeet
O que você quer dizer com “C # não vê enums como structs”? Você pode usar tipos enum como parâmetros de tipo que são restritos a structapenas multa.
Timwi,
verifique este artigo aqui: codeproject.com/KB/cs/ExtendEnum.aspx 'IsValidEnumValue' ou 'IsFlagsEnumDefined' métodos são provavelmente a resposta para sua pergunta.
dmihailescu
1
Vote nesta ideia de uservoice , se quiser vê-la embutida em .net um dia.
Matthieu
11
C # 7.3 apresenta restrições de enum.
Marc Sigrist,

Respostas:

48

EDITAR: Agora está disponível na versão 0.0.0.2 do UnconstrainedMelody.

(Conforme solicitado em minha postagem do blog sobre restrições de enum . Incluí os fatos básicos abaixo para obter uma resposta autônoma.)

A melhor solução é esperar que eu inclua no UnconstrainedMelody 1 . Esta é uma biblioteca que usa código C # com restrições "falsas", como

where T : struct, IEnumConstraint

e o transforma em

where T : struct, System.Enum

por meio de uma etapa de postbuild.

Não deve ser muito difícil de escrever IsSet... embora atender às bandeiras baseadas Int64e UInt64baseadas em ambos possa ser a parte complicada. (Sinto o cheiro de alguns métodos auxiliares chegando, basicamente me permitindo tratar qualquer enum sinalizador como se ele tivesse um tipo base de UInt64.)

Qual você gostaria que fosse o comportamento se ligasse

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

? Ele deve verificar se todos os sinalizadores especificados estão definidos? Essa seria a minha expectativa.

Vou tentar fazer isso no caminho para casa esta noite ... Espero ter uma rápida blitz sobre métodos de enum úteis para colocar a biblioteca em um padrão utilizável rapidamente e, em seguida, relaxar um pouco.

EDIT: Não tenho certeza sobre IsSetcomo um nome, a propósito. Opções:

  • Inclui
  • Contém
  • HasFlag (ou HasFlags)
  • IsSet (certamente é uma opção)

Os pensamentos são bem-vindos. Tenho certeza de que vai demorar um pouco antes de qualquer coisa ser gravada em pedra de qualquer maneira ...


1 ou enviá-lo como um patch, é claro ...

Jon Skeet
fonte
1
Você tinha que ir e mencionar PostSharp LOL: o postsharp.org/blog/generic-constraints-for-enums-and-delegates
Sam Harwell
1
Ou, na verdade, HasAny () e HasAll () mais simples
Keith
1
Sim, concordo que é ainda melhor. colors.HasAny(Colors.Red | Colors.Blue)parece um código muito legível. =)
Blixt
1
Sim, eu gosto de HasAny e HasAll também. Irá com isso.
Jon Skeet
5
Desde C # 7.3 (lançado em maio de 2018), é possível usar a restrição where T : System.Enum. Isso já foi escrito em outro lugar no tópico; apenas pensei em repetir aqui.
Jeppe Stig Nielsen
16

Darren, isso funcionaria se os tipos fossem enumerações específicas - para enumerações gerais funcionarem, você tem que convertê-las em ints (ou mais provavelmente uint) para fazer a matemática booleana:

public static bool IsSet( this Enum input, Enum matchTo )
{
    return ( Convert.ToUInt32( input ) & Convert.ToUInt32( matchTo ) ) != 0;
}
Ronnie
fonte
1
E se você tiver um número ridículo de sinalizadores, você pode chamar GetTypeCode () nos argumentos e Convert.ToUint64 ()
Kit de
Incrível, a combinação de 'Enum' e Convert.ToUInt32eu não encontrei em nenhum outro lugar. AFAIK, é a única solução Pre-Net-4 decente que também funciona em VB. BTW, se houver matchTovários bits de sinalizador, substitua != 0por == Convert.ToUInt32(matchTo).
Toolmaker Steve
1
Observe que Convert.ToUInt32usado com um enum usará a Convert.ToUInt32(object)sobrecarga, o que significa que o CLR primeiro encaixotará esses valores antes de passá-los para o ToUInt32método. Na maioria dos casos, isso não importa, mas é bom saber que você manterá o GC bastante ocupado se estiver usando algo assim para analisar milhões de enums por segundo.
Groo
10

Na verdade, é possível, com um truque feio. No entanto, não pode ser usado para métodos de extensão.

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

Se desejar, você pode fornecer Enums<Temp>um construtor privado e uma classe herdada abstrata aninhada pública com Tempas Enum, para evitar versões herdadas de não enums.

SLaks
fonte
8

Você pode conseguir isso usando IL Weaving e ExtraConstraints

Permite que você escreva este código

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

O que é compilado

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

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

A partir do C # 7.3, você pode usar a restrição Enum em tipos genéricos:

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

Se quiser usar um enum Nullable, você deve deixar a restrição de estrutura organizacional:

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

Isso não responde à pergunta original, mas agora existe um método no .NET 4 chamado Enum.HasFlag que faz o que você está tentando fazer no seu exemplo

Phil Devaney
fonte
Votado porque, neste ponto, quase todo mundo deve estar usando o .NET 4 (ou superior) e, portanto, deve usar este método em vez de tentar hackea-lo juntos.
CptRobby de
Votado. No entanto, sua solução usa o encaixotamento do argumento flag. O .NET 4.0 já tem cinco anos.
Jeppe Stig Nielsen
3

A maneira como faço isso é colocar uma restrição de estrutura e verificar se T é um enum em tempo de execução. Isso não elimina o problema completamente, mas o reduz um pouco

theecoop
fonte
7
onde T: struct, IComparable, IFormattable, IConvertible - este é o mais próximo que você pode chegar de enum :)
Kit de
1

Usando seu código original, dentro do método, você também pode usar reflexão para testar se T é um enum:

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
fonte
2
Obrigado, mas isso transforma um problema de tempo de compilação (a restrição where) em um de tempo de execução (sua exceção). Além disso, você ainda precisa converter as entradas em ints antes de fazer qualquer coisa com elas.
Keith
1

Aqui está um código que acabei de criar que parece funcionar como você deseja, sem ter que fazer nada muito louco. Não está restrito apenas a enums definidos como Sinalizadores, mas sempre pode haver uma verificação inserida, se necessário.

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
fonte
0

se alguém precisa de IsSet genérico (criado imediatamente e pode ser melhorado), e / ou string para conversão onfly Enum (que usa EnumConstraint apresentado abaixo):

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

    }

  }

Se alguém ainda precisa do exemplo quente para criar a restrição de codificação 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 isso ajude alguém.

Solar
fonte
0

Eu só queria adicionar Enum como uma restrição genérica.

Embora isso seja apenas para um pequeno método auxiliar, o uso ExtraConstraintsé um pouco sobrecarregado para mim.

Decidi apenas criar uma structrestrição e adicionar uma verificação de tempo de execução para IsEnum. Para converter uma variável de T em Enum, eu a lanço em objeto primeiro.

    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
fonte