Czy istnieje sposób, aby sprawdzić, czy int jest prawnym wyliczeniem w C #?

167

Przeczytałem kilka postów SO i wydaje się, że brakuje najbardziej podstawowej operacji.

public enum LoggingLevel
{
    Off = 0,
    Error = 1,
    Warning = 2,
    Info = 3,
    Debug = 4,
    Trace = 5
};

if (s == "LogLevel")
{
    _log.LogLevel = (LoggingLevel)Convert.ToInt32("78");
    _log.LogLevel = (LoggingLevel)Enum.Parse(typeof(LoggingLevel), "78");
    _log.WriteDebug(_log.LogLevel.ToString());
}

To nie powoduje wyjątków, chętnie przechowuje 78. Czy istnieje sposób na walidację wartości przechodzącej do wyliczenia?

char m
źródło
2
Możliwy duplikat Validate Enum Values
Erik

Odpowiedzi:

271

Sprawdź Enum.IsDefined

Stosowanie:

if(Enum.IsDefined(typeof(MyEnum), value))
    MyEnum a = (MyEnum)value; 

Oto przykład z tej strony:

using System;    
[Flags] public enum PetType
{
   None = 0, Dog = 1, Cat = 2, Rodent = 4, Bird = 8, Reptile = 16, Other = 32
};

public class Example
{
   public static void Main()
   {
      object value;     
      // Call IsDefined with underlying integral value of member.
      value = 1;
      Console.WriteLine("{0}: {1}", value, Enum.IsDefined(typeof(PetType), value));
      // Call IsDefined with invalid underlying integral value.
      value = 64;
      Console.WriteLine("{0}: {1}", value, Enum.IsDefined(typeof(PetType), value));
      // Call IsDefined with string containing member name.
      value = "Rodent";
      Console.WriteLine("{0}: {1}", value, Enum.IsDefined(typeof(PetType), value));
      // Call IsDefined with a variable of type PetType.
      value = PetType.Dog;
      Console.WriteLine("{0}: {1}", value, Enum.IsDefined(typeof(PetType), value));
      value = PetType.Dog | PetType.Cat;
      Console.WriteLine("{0}: {1}", value, Enum.IsDefined(typeof(PetType), value));
      // Call IsDefined with uppercase member name.      
      value = "None";
      Console.WriteLine("{0}: {1}", value, Enum.IsDefined(typeof(PetType), value));
      value = "NONE";
      Console.WriteLine("{0}: {1}", value, Enum.IsDefined(typeof(PetType), value));
      // Call IsDefined with combined value
      value = PetType.Dog | PetType.Bird;
      Console.WriteLine("{0:D}: {1}", value, Enum.IsDefined(typeof(PetType), value));
      value = value.ToString();
      Console.WriteLine("{0:D}: {1}", value, Enum.IsDefined(typeof(PetType), value));
   }
}

Przykład wyświetla następujące dane wyjściowe:

//       1: True
//       64: False
//       Rodent: True
//       Dog: True
//       Dog, Cat: False
//       None: True
//       NONE: False
//       9: False
//       Dog, Bird: False
SwDevMan81
źródło
@matti: Zamień "78" na dowolną reprezentację liczbową LoggingLevelużywaną jako pamięć, a następnie przedstaw ją jako LoggingLevelwartość wyliczoną.
thecoop
9
Wygląda na IsDefinedto, że nie działa to dla członków wyliczenia bitwised.
Saeed Neamati
29

Powyższe rozwiązania nie dotyczą [Flags]sytuacji.

Poniższe rozwiązanie może mieć pewne problemy z wydajnością (jestem pewien, że można je zoptymalizować na różne sposoby), ale zasadniczo zawsze wykaże, czy wartość wyliczenia jest prawidłowa, czy nie .

Opiera się na trzech założeniach:

  • Wartości wyliczeniowe w C # mogą być tylko int, absolutnie niczym innym
  • Nazwy wyliczeń w C # muszą zaczynać się od znaku alfabetu
  • Żadna poprawna nazwa wyliczenia nie może zawierać znaku minusa: -

Wywołanie ToString()wyliczenia zwraca intwartość, jeśli żadne wyliczenie (flaga lub nie) nie jest dopasowane. Jeśli dopasowana zostanie dozwolona wartość wyliczenia, wypisze nazwę dopasowania (-ów).

Więc:

[Flags]
enum WithFlags
{
    First = 1,
    Second = 2,
    Third = 4,
    Fourth = 8
}

((WithFlags)2).ToString() ==> "Second"
((WithFlags)(2 + 4)).ToString() ==> "Second, Third"
((WithFlags)20).ToString() ==> "20"

Mając na uwadze te dwie reguły, możemy założyć, że jeśli .NET Framework wykonuje swoje zadanie poprawnie, to każde wywołanie prawidłowej ToString()metody wyliczenia spowoduje powstanie czegoś, co ma znak alfabetu jako pierwszy znak:

public static bool IsValid<TEnum>(this TEnum enumValue)
    where TEnum : struct
{
    var firstChar = enumValue.ToString()[0];
    return (firstChar < '0' || firstChar > '9') && firstChar != '-';
}

Można to nazwać „włamaniem”, ale korzyści są takie, że polegając na własnej implementacji Microsoft Enumi standardach C #, nie polegasz na własnym, potencjalnie błędnym kodzie lub sprawdzeniach. W sytuacjach, w których wydajność nie jest wyjątkowo krytyczna, pozwoli to zaoszczędzić wiele nieprzyjemnych switchstwierdzeń lub innych kontroli!

Edytować

Podziękowania dla @ChaseMedallion za wskazanie, że moja oryginalna implementacja nie obsługiwała wartości ujemnych. Zostało to naprawione i zapewniono testy.

Oraz testy potwierdzające:

[TestClass]
public class EnumExtensionsTests
{
    [Flags]
    enum WithFlags
    {
        First = 1,
        Second = 2,
        Third = 4,
        Fourth = 8
    }

    enum WithoutFlags
    {
        First = 1,
        Second = 22,
        Third = 55,
        Fourth = 13,
        Fifth = 127
    }

    enum WithoutNumbers
    {
        First, // 1
        Second, // 2
        Third, // 3
        Fourth // 4
    }

    enum WithoutFirstNumberAssigned
    {
        First = 7,
        Second, // 8
        Third, // 9
        Fourth // 10
    }


    enum WithNagativeNumbers
    {
        First = -7,
        Second = -8,
        Third = -9,
        Fourth = -10
    }

    [TestMethod]
    public void IsValidEnumTests()
    {
        Assert.IsTrue(((WithFlags)(1 | 4)).IsValid());
        Assert.IsTrue(((WithFlags)(1 | 4)).IsValid());
        Assert.IsTrue(((WithFlags)(1 | 4 | 2)).IsValid());
        Assert.IsTrue(((WithFlags)(2)).IsValid());
        Assert.IsTrue(((WithFlags)(3)).IsValid());
        Assert.IsTrue(((WithFlags)(1 + 2 + 4 + 8)).IsValid());

        Assert.IsFalse(((WithFlags)(16)).IsValid());
        Assert.IsFalse(((WithFlags)(17)).IsValid());
        Assert.IsFalse(((WithFlags)(18)).IsValid());
        Assert.IsFalse(((WithFlags)(0)).IsValid());

        Assert.IsTrue(((WithoutFlags)1).IsValid());
        Assert.IsTrue(((WithoutFlags)22).IsValid());
        Assert.IsTrue(((WithoutFlags)(53 | 6)).IsValid());   // Will end up being Third
        Assert.IsTrue(((WithoutFlags)(22 | 25 | 99)).IsValid()); // Will end up being Fifth
        Assert.IsTrue(((WithoutFlags)55).IsValid());
        Assert.IsTrue(((WithoutFlags)127).IsValid());

        Assert.IsFalse(((WithoutFlags)48).IsValid());
        Assert.IsFalse(((WithoutFlags)50).IsValid());
        Assert.IsFalse(((WithoutFlags)(1 | 22)).IsValid());
        Assert.IsFalse(((WithoutFlags)(9 | 27 | 4)).IsValid());

        Assert.IsTrue(((WithoutNumbers)0).IsValid());
        Assert.IsTrue(((WithoutNumbers)1).IsValid());
        Assert.IsTrue(((WithoutNumbers)2).IsValid());
        Assert.IsTrue(((WithoutNumbers)3).IsValid());
        Assert.IsTrue(((WithoutNumbers)(1 | 2)).IsValid()); // Will end up being Third
        Assert.IsTrue(((WithoutNumbers)(1 + 2)).IsValid()); // Will end up being Third

        Assert.IsFalse(((WithoutNumbers)4).IsValid());
        Assert.IsFalse(((WithoutNumbers)5).IsValid());
        Assert.IsFalse(((WithoutNumbers)25).IsValid());
        Assert.IsFalse(((WithoutNumbers)(1 + 2 + 3)).IsValid());

        Assert.IsTrue(((WithoutFirstNumberAssigned)7).IsValid());
        Assert.IsTrue(((WithoutFirstNumberAssigned)8).IsValid());
        Assert.IsTrue(((WithoutFirstNumberAssigned)9).IsValid());
        Assert.IsTrue(((WithoutFirstNumberAssigned)10).IsValid());

        Assert.IsFalse(((WithoutFirstNumberAssigned)11).IsValid());
        Assert.IsFalse(((WithoutFirstNumberAssigned)6).IsValid());
        Assert.IsFalse(((WithoutFirstNumberAssigned)(7 | 9)).IsValid());
        Assert.IsFalse(((WithoutFirstNumberAssigned)(8 + 10)).IsValid());

        Assert.IsTrue(((WithNagativeNumbers)(-7)).IsValid());
        Assert.IsTrue(((WithNagativeNumbers)(-8)).IsValid());
        Assert.IsTrue(((WithNagativeNumbers)(-9)).IsValid());
        Assert.IsTrue(((WithNagativeNumbers)(-10)).IsValid());
        Assert.IsFalse(((WithNagativeNumbers)(-11)).IsValid());
        Assert.IsFalse(((WithNagativeNumbers)(7)).IsValid());
        Assert.IsFalse(((WithNagativeNumbers)(8)).IsValid());
    }
}
joshcomley
źródło
1
Dzięki za to, miałem podobny problem z prawidłowymi kombinacjami flag. Alternatywą do sprawdzenia pierwszego znaku wyliczenia może być również próba int.TryParse (enumValue.ToString ()) ... Jeśli to się nie powiedzie, masz prawidłowy zestaw flag. Może to jednak być wolniejsze niż twoje rozwiązanie.
MadHenchbot
Ta implementacja nie sprawdza poprawnie wartości ujemnych, ponieważ sprawdzanie dotyczy znaków
niecyfrowych
Dobry chwyt !! Zaktualizuję moją odpowiedź, aby uwzględnić takie, dziękuję @ChaseMedallion
joshcomley
Najbardziej podoba mi się to rozwiązanie, przedstawione sztuczki matematyczne działają tylko wtedy, gdy [Flags]mają rozsądne wartości całkowite.
MrLore
17

Kanoniczna odpowiedź brzmiałaby Enum.IsDefined, ale to jest: trochę powolne, jeśli jest używane w ciasnej pętli, oraz b: nie jest przydatne w przypadku [Flags]wyliczeń.

Osobiście przestałbym się tym martwić i po prostu switchodpowiednio pamiętając:

  • jeśli możesz nie rozpoznawać wszystkiego (i po prostu nic nie robić), nie dodawaj default:(lub miej puste default:wyjaśnienie dlaczego)
  • jeśli istnieje rozsądne domyślne zachowanie, umieść je w default:
  • w przeciwnym razie zajmij się tymi, o których wiesz, i zgłoś wyjątek dla pozostałych:

Tak jak to:

switch(someflag) {
    case TriBool.Yes:
        DoSomething();
        break;
    case TriBool.No:
        DoSomethingElse();
        break;
    case TriBool.FileNotFound:
        DoSomethingOther();
        break;
    default:
        throw new ArgumentOutOfRangeException("someflag");
}
Marc Gravell
źródło
nieznajomość [Flags] wyliczeń i wydajności nie jest problemem, więc twoja odpowiedź wydaje się być powodem dla którego wymyślono wyliczenia;) patrzenie na twoje "punkty" lub jakkolwiek się one nazywają, więc musisz mieć tam punkt . Założę się, że nie dostałeś ich za darmo, ale pomyśl o sytuacji odczytu pliku konfiguracyjnego, w którym jest 257 wartości w jednej definicji wyliczenia. Nie mówiąc już o dziesiątkach innych wyliczeń. Byłoby wiele rzędów przypadków ...
char m
@matti - to brzmi skrajnym przykładem; deserializacja i tak jest wyspecjalizowaną dziedziną - większość silników serializacji oferuje bezpłatną weryfikację wyliczenia.
Marc Gravell
@matti - na marginesie; Powiedziałbym, że traktuj odpowiedzi na podstawie ich indywidualnych zalet. Czasami coś mi się nie podoba, a ktoś z „rep 17” może równie dobrze udzielić doskonałej odpowiedzi.
Marc Gravell
Odpowiedź przełącznika jest szybka, ale nie jest ogólna.
Eldritch Conundrum
8

Posługiwać się:

Enum.IsDefined ( typeof ( Enum ), EnumValue );
n535
źródło
4

Aby sobie z tym poradzić [Flags], możesz również skorzystać z tego rozwiązania z C # Cookbook :

Najpierw dodaj nową ALLwartość do wyliczenia:

[Flags]
enum Language
{
    CSharp = 1, VBNET = 2, VB6 = 4, 
    All = (CSharp | VBNET | VB6)
}

Następnie sprawdź, czy wartość jest w ALL:

public bool HandleFlagsEnum(Language language)
{
    if ((language & Language.All) == language)
    {
        return (true);
    }
    else
    {
        return (false);
    }
}
Mario Levrero
źródło
2

Jednym ze sposobów byłoby poleganie na rzutowaniu i konwersji wyliczenia na ciąg. Podczas rzutowania int na typ Enum int jest konwertowany na odpowiednią wartość wyliczenia lub wynikowe wyliczenie zawiera po prostu int jako wartość, jeśli wartość wyliczenia nie jest zdefiniowana dla int.

enum NetworkStatus{
  Unknown=0,
  Active,
  Slow
}

int statusCode=2;
NetworkStatus netStatus = (NetworkStatus) statusCode;
bool isDefined = netStatus.ToString() != statusCode.ToString();

Nie testowane w żadnych skrajnych przypadkach.

maulik13
źródło
1

Jak powiedzieli inni, Enum.IsDefinedzwraca, falsenawet jeśli masz poprawną kombinację flag bitowych dla wyliczenia ozdobionego rozszerzeniem FlagsAttribute.

Niestety, jedyny sposób na utworzenie metody zwracającej wartość true dla prawidłowych flag bitowych jest trochę długi:

public static bool ValidateEnumValue<T>(T value) where T : Enum
{
    // Check if a simple value is defined in the enum.
    Type enumType = typeof(T);
    bool valid = Enum.IsDefined(enumType, value);
    // For enums decorated with the FlagsAttribute, allow sets of flags.
    if (!valid && enumType.GetCustomAttributes(typeof(FlagsAttribute), false)?.Any() == true)
    {
        long mask = 0;
        foreach (object definedValue in Enum.GetValues(enumType))
            mask |= Convert.ToInt64(definedValue);
        long longValue = Convert.ToInt64(value);
        valid = (mask & longValue) == longValue;
    }
    return valid;
}

Możesz chcieć zapisać wyniki GetCustomAttributew słowniku:

private static readonly Dictionary<Type, bool> _flagEnums = new Dictionary<Type, bool>();
public static bool ValidateEnumValue<T>(T value) where T : Enum
{
    // Check if a simple value is defined in the enum.
    Type enumType = typeof(T);
    bool valid = Enum.IsDefined(enumType, value);
    if (!valid)
    {
        // For enums decorated with the FlagsAttribute, allow sets of flags.
        if (!_flagEnums.TryGetValue(enumType, out bool isFlag))
        {
            isFlag = enumType.GetCustomAttributes(typeof(FlagsAttribute), false)?.Any() == true;
            _flagEnums.Add(enumType, isFlag);
        }
        if (isFlag)
        {
            long mask = 0;
            foreach (object definedValue in Enum.GetValues(enumType))
                mask |= Convert.ToInt64(definedValue);
            long longValue = Convert.ToInt64(value);
            valid = (mask & longValue) == longValue;
        }
    }
    return valid;
}

Należy zauważyć, że powyższy kod używa nowego Enumograniczenia, Tktóre jest dostępne tylko od wersji C # 7,3. Musisz zdać object valuew starszych wersjach i zadzwonić GetType()do niego.

Promień
źródło