Czy ktoś zna dobre obejście braku ograniczenia ogólnego wyliczenia?

90

Chcę zrobić coś takiego: mam wyliczenia z połączonymi oflagowanymi wartościami.

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

Więc mogłem zrobić:

MyEnum tester = MyEnum.FlagA | MyEnum.FlagB

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

Niestety, C # jest generyczny, w którym ograniczenia nie mają ograniczenia wyliczeniowego, tylko klasa i struktura. C # nie widzi wyliczeń jako struktur (mimo że są to typy wartości), więc nie mogę dodawać takich typów rozszerzeń.

Czy ktoś zna obejście?

Keith
źródło
2
Keith: pobierz wersję 0.0.0.2 UnconstrainedMelody - zaimplementowałem HasAll i HasAny. Cieszyć się.
Jon Skeet,
Co masz na myśli, mówiąc „C # nie widzi wyliczeń jako struktur”? Możesz użyć typów wyliczenia jako parametrów typu, które są ograniczone do structdobrego.
Timwi,
sprawdź ten artykuł tutaj: codeproject.com/KB/cs/ExtendEnum.aspx Metody „IsValidEnumValue” lub „IsFlagsEnumDefined” są prawdopodobnie odpowiedzią na Twoje pytanie.
dmihailescu
1
Zagłosuj na ten pomysł na głosowanie użytkownika , jeśli chcesz go kiedyś zobaczyć w domenie .net.
Matthieu
11
C # 7.3 wprowadza ograniczenia wyliczeniowe.
Marc Sigrist,

Odpowiedzi:

49

EDYCJA: To jest teraz dostępne w wersji 0.0.0.2 UnconstrainedMelody.

(Zgodnie z prośbą zawartą w moim poście na blogu dotyczącym ograniczeń wyliczeniowych . Poniżej zamieściłem podstawowe fakty, aby udzielić niezależnej odpowiedzi.

Najlepszym rozwiązaniem jest zaczekanie, aż dołączę go do UnconstrainedMelody 1 . Jest to biblioteka, która przyjmuje kod C # z „fałszywymi” ograniczeniami, takimi jak

where T : struct, IEnumConstraint

i zamienia go w

where T : struct, System.Enum

poprzez krok po kompilacji.

Nie powinno być zbyt trudno to napisać IsSet... chociaż obsługa flag zarówno Int64opartych, jak i UInt64opartych może być trudną częścią. (Wyczuwam nadchodzące metody pomocnicze, w zasadzie pozwalające mi traktować wyliczenie wszystkich flag tak, jakby miały podstawowy typ UInt64.)

Jakie zachowanie byś chciał, gdybyś zadzwonił

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

? Czy powinien sprawdzić, czy wszystkie określone flagi są ustawione? Takie byłoby moje oczekiwanie.

Spróbuję to zrobić dziś wieczorem w drodze do domu ... Mam nadzieję, że będę miał szybki rzut oka na użyteczne metody wyliczania, aby szybko doprowadzić bibliotekę do użytecznego standardu, a potem trochę się zrelaksować.

EDYCJA: Nawiasem mówiąc, nie jestem pewien IsSetco do nazwy. Opcje:

  • Zawiera
  • Zawiera
  • HasFlag (lub HasFlags)
  • IsSet (z pewnością jest to opcja)

Myśli mile widziane. Jestem pewien, że minie trochę czasu, zanim cokolwiek zostanie osadzone w kamieniu ...


1 lub oczywiście prześlij jako łatkę ...

Jon Skeet
źródło
1
Trzeba było wspomnieć o PostSharp LOL: o postsharp.org/blog/generic-constraints-for-enums-and-delegates
Sam Harwell
1
A właściwie prostsze HasAny () i HasAll ()
Keith
1
Tak, zgadzam się, że to jeszcze lepsze. colors.HasAny(Colors.Red | Colors.Blue)wygląda na bardzo czytelny kod. =)
Blixt
1
Tak, lubię też HasAny i HasAll. Pójdzie z tym.
Jon Skeet,
5
Od wersji C # 7,3 (wydanej w maju 2018 r.) Można użyć ograniczenia where T : System.Enum. To było już napisane w innym miejscu wątku; pomyślałem, że powtórzę to tutaj.
Jeppe Stig Nielsen
16

Darren, to zadziałałoby, gdyby typy były specyficznymi wyliczeniami - aby ogólne wyliczenia działały, musisz rzutować je na ints (lub bardziej prawdopodobne uint), aby wykonać obliczenia boolowskie:

public static bool IsSet( this Enum input, Enum matchTo )
{
    return ( Convert.ToUInt32( input ) & Convert.ToUInt32( matchTo ) ) != 0;
}
Ronnie
źródło
1
A jeśli masz absurdalną liczbę flag, możesz wywołać GetTypeCode () na argumentach i Convert.ToUint64 ()
zestaw
Niesamowite, połączenie "Enum" i Convert.ToUInt32nie znalazłem nigdzie indziej. AFAIK, to jedyne przyzwoite rozwiązanie Pre-Net-4, które działa również w VB. BTW, jeśli matchTomoże mieć wiele bitów flaga, a następnie zastąpić != 0z == Convert.ToUInt32(matchTo).
ToolmakerSteve
1
Należy zauważyć, że Convert.ToUInt32użyte z wyliczeniem użyje Convert.ToUInt32(object)przeciążenia, co oznacza, że ​​środowisko CLR najpierw zapakuje te wartości przed przekazaniem ich do ToUInt32metody. W większości przypadków nie ma to znaczenia, ale dobrze jest wiedzieć, że GC będzie raczej zajęty, jeśli używasz czegoś takiego do analizowania milionów wyliczeń na sekundę.
Groo
10

Właściwie jest to możliwe za pomocą brzydkiej sztuczki. Nie można go jednak używać w przypadku metod rozszerzających.

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

Jeśli chcesz, możesz podać Enums<Temp>prywatny konstruktor i publiczną dziedziczoną klasę abstrakcyjną zagnieżdżoną z Tempas Enum, aby zapobiec dziedziczeniu wersji dla nie-wyliczeń.

SLaks
źródło
8

Możesz to osiągnąć za pomocą IL Weaving i ExtraConstraints

Pozwala napisać ten kod

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

Co jest kompilowane

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

    public void MethodWithEnumConstraint<T>() where T: struct, Enum
    {
    }
}
Szymon
źródło
6

Począwszy od C # 7,3, można użyć ograniczenia wyliczenia w typach ogólnych:

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

Jeśli chcesz użyć wyliczenia o wartości Nullable, musisz zostawić ograniczenie struktury oryginalnej:

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

To nie odpowiada na pierwotne pytanie, ale w .NET 4 jest teraz metoda o nazwie Enum.HasFlag, która robi to, co próbujesz zrobić w twoim przykładzie

Phil Devaney
źródło
Głos za pozytywnymi, ponieważ w tym momencie prawie każdy powinien używać .NET 4 (lub nowszego), więc powinni używać tej metody zamiast próbować ją razem zhakować.
CptRobby
Głosowano za. Jednak ich rozwiązanie opiera się na argumentacji flag. NET 4.0 ma już pięć lat.
Jeppe Stig Nielsen
3

Sposób, w jaki to robię, polega na umieszczeniu ograniczenia struct, a następnie sprawdzeniu, czy T jest wyliczeniem w czasie wykonywania. Nie eliminuje to całkowicie problemu, ale w pewnym stopniu go zmniejsza

thecoop
źródło
7
gdzie T: struct, IComparable, IFormattable, IConvertible - to jest najbliższe wyliczenie :)
Zestaw
1

Używając oryginalnego kodu, wewnątrz metody możesz również użyć odbicia, aby sprawdzić, czy T jest wyliczeniem:

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
źródło
2
Dzięki, ale to zmienia problem czasu kompilacji (ograniczenie Where) w problem w czasie wykonywania (twój wyjątek). Ponadto nadal musiałbyś przekonwertować dane wejściowe na wartości int, zanim będziesz mógł cokolwiek z nimi zrobić.
Keith
1

Oto kod, który właśnie stworzyłem, który wydaje się działać tak, jak chcesz, bez konieczności robienia niczego zbyt szalonego. Nie ogranicza się tylko do wyliczeń ustawionych jako flagi, ale w razie potrzeby zawsze można wprowadzić czek.

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
źródło
0

jeśli ktoś potrzebuje generycznego IsSet (utworzonego po wyjęciu z pudełka, można go ulepszyć), lub string do konwersji Enum onfly (który wykorzystuje EnumConstraint przedstawiony poniżej):

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

    }

  }

Jeśli ktoś nadal potrzebuje przykładu gorącego, aby utworzyć ograniczenie kodowania 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));
  }

}

mam nadzieję, że to komuś pomoże.

Słoneczny
źródło
0

Chciałem tylko dodać Enum jako ogólne ograniczenie.

Chociaż jest to tylko dla małej metody pomocniczej, użycie ExtraConstraintsjest dla mnie trochę za dużo.

Postanowiłem po prostu utworzyć structograniczenie i dodać sprawdzanie w czasie wykonywania IsEnum. Aby przekonwertować zmienną z T na Enum, najpierw rzutuję ją na obiekt.

    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
źródło