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?


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ę ...


1

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

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ń.


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

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

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


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


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

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.");
    }
}

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.


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());
    }
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.