簡體   English   中英

有沒有辦法檢查 int 是否是 C# 中的合法枚舉?

[英]Is there a way to check if int is legal enum in C#?

我已經閱讀了一些 SO 帖子,似乎缺少最基本的操作。

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

這不會導致任何異常,很高興存儲78 有沒有辦法驗證進入枚舉的值?

查看Enum.IsDefined

用法:

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

這是該頁面的示例:

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

該示例顯示以下輸出:

//       1: True
//       64: False
//       Rodent: True
//       Dog: True
//       Dog, Cat: False
//       None: True
//       NONE: False
//       9: False
//       Dog, Bird: False

上述解決方案不處理[Flags]情況。

我下面的解決方案可能有一些性能問題(我相信可以通過各種方式進行優化),但本質上它總是會證明枚舉值是否有效

它依賴於三個假設:

  • C# 中的枚舉值只允許是int ,絕對沒有別的
  • C# 中的枚舉名稱必須以字母字符開頭
  • 沒有有效的枚舉名稱可以帶有減號: -

如果沒有匹配的枚舉(標志或不匹配ToString() ,則在枚舉上調用ToString()將返回int值。 如果匹配了允許的枚舉值,它將打印匹配項的名稱。

所以:

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

考慮到這兩條規則,我們可以假設,如果 .NET Framework 正確執行其工作,則對有效枚舉的ToString()方法的任何調用都將導致以字母字符作為其第一個字符的內容:

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

可以將其稱為“hack”,但優點是通過依賴 Microsoft 自己對Enum和 C# 標准的實現,您無需依賴自己的潛在錯誤代碼或檢查。 在性能不是特別關鍵的情況下,這將節省大量討厭的switch語句或其他檢查!

編輯

感謝@ChaseMedallion 指出我的原始實現不支持負值。 這已得到糾正並提供測試。

以及支持它的測試:

[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());
    }
}

規范的答案是Enum.IsDefined ,但這是 a: 如果在緊密循環中使用會有點慢,而 b: 對[Flags]枚舉沒有用。

就我個人而言,我不會擔心這個,只需適當switch ,記住:

  • 如果可以不識別所有內容(並且只是不做任何事情),那么不要添加default:或有一個空的default:解釋原因)
  • 如果有一個合理的默認行為,把它放在default:
  • 否則,處理您知道的那些並為其余的拋出異常:

像這樣:

switch(someflag) {
    case TriBool.Yes:
        DoSomething();
        break;
    case TriBool.No:
        DoSomethingElse();
        break;
    case TriBool.FileNotFound:
        DoSomethingOther();
        break;
    default:
        throw new ArgumentOutOfRangeException("someflag");
}

用:

Enum.IsDefined ( typeof ( Enum ), EnumValue );

使用Enum.IsDefined

為了處理[Flags]你也可以使用C# Cookbook 中的這個解決方案

首先,為您的枚舉添加一個新的ALL值:

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

然后,檢查該值是否在ALL

public bool HandleFlagsEnum(Language language)
{
    if ((language & Language.All) == language)
    {
        return (true);
    }
    else
    {
        return (false);
    }
}

一種方法是依靠強制轉換和枚舉到字符串的轉換。 將 int 轉換為 Enum 類型時,int 將轉換為相應的枚舉值,或者如果未為 int 定義枚舉值,則生成的枚舉僅包含 int 作為值。

enum NetworkStatus{
  Unknown=0,
  Active,
  Slow
}

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

未針對任何邊緣情況進行測試。

正如其他人所說, Enum.IsDefined返回false即使您有使用FlagsAttribute修飾的枚舉的位標志的有效組合。

可悲的是,創建一個為有效位標志返回 true 的方法的唯一方法有點冗長:

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

您可能希望在字典中緩存GetCustomAttribute的結果:

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

請注意,上面的代碼在T上使用了新的Enum約束,該約束僅自 C# 7.3 起可用。 您需要在舊版本中傳遞一個object value並對其調用GetType()

我知道這是一個老問題,但我今天遇到了這個問題,我想擴展 Josh Comley 的回答( https://stackoverflow.com/a/23177585/3403999

我想解決喬希的回答中有幾個錯誤的假設:

  1. 它假定“-”始終是負號。 我不知道是否有任何文化使用不同的符號,但 .Net 在 NumberFormatInfo ( https://docs.microsoft.com/en-us/dotnet/api/system.globalization.numberformatinfo .negativesign?view=net-5.0 )。 關於我能想到的唯一一個可能是常見的括號,即 (1) == -1。
  2. 枚舉成員必須以字母字符開頭。 具體來說,我知道您可以使用下划線作為第一個字符。 IE, enum MyEnum { _One = 1 }是有效的。
  3. 不確定這是否完全錯誤,但它假設“0”到“9”和“-”范圍之外的任何內容都是有效的字母字符。 這似乎是一個錯誤的假設,因為該范圍之外的控制字符會返回 true - 盡管我認為您無法將這些控制字符放入枚舉成員名稱中而不會引發編譯錯誤。

無論如何,這是我更新的解決方案:

public static bool IsValid<TEnum>(this TEnum value) where TEnum : System.Enum
{
    char first = value.ToString()[0];
    return (char.IsLetter(first) || first == '_');
}

我確實發現您可以在枚舉成員名稱中使用來自其他語言的 Unicode 字母( https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/inside-a-program/identifier-names )。 我的解決方案在這方面仍然通過。 我使用以下枚舉進行了測試: enum MyEnum { \א } 枚舉已編譯,並且 IsValid 返回 true。

我很好奇走這條路與使用帶有 HashSet 的靜態幫助器類相比,您會采取什么樣的性能打擊,該類中填充了Enum.GetValues(typeof(TEnum)) ,您可以在其中檢查 HashSet 是否包含枚舉值。 想法是 Enum.GetValues 和 Enum.IsDefined 只是昂貴的反射命中的包裝器,所以你用 GetValues 做一次反射,緩存結果,然后只檢查 HashSet 前進。

我使用 StopWatch 和 Random 運行了一個相當簡單的測試,它們將生成有效和無效的枚舉值,然后我通過 3 種不同的方法運行它們:ToString 方法、GetValues HashSet 方法和 IsDefined 方法。 我讓他們做每個方法 int.MaxValue 次。 結果:

  • ToString 每次運行 20 億次平均大約需要 2 分鍾。
  • 每次運行 20 億次 GetValues HashSet 大約需要 50 秒。
  • 每次運行 20 億次時,IsDefined 大約需要 5 分鍾。

因此,如果性能是一個問題,或者您正在執行循環,那么所有推薦 IsDefined 的解決方案都可能是一個壞主意。 如果您僅使用它以某種方式驗證單個實例上的用戶輸入,則可能無關緊要。

對於 HashSet,對於您通過它運行的每個不同的枚舉,它的性能都會受到很小的影響(因為第一次運行新的枚舉類型會生成一個新的靜態 HashSet)。 不科學,但在開始使用 ToString 方法執行之前,我的 PC 上的盈虧平衡點似乎是單個枚舉的大約 200k 到 300k 運行。

ToString 方法雖然不是最快的方法,但具有處理 IsDefined 和 HashSet 都無法容納的標志枚舉的額外好處。

如果性能確實是一個問題,請不要使用這 3 種方法中的任何一種。 而是編寫一個方法來驗證針對該枚舉優化的特定枚舉。

另請注意,我的測試使用相對較小的枚舉(5 個左右的元素)。 一旦您開始使用更大的枚舉,我不知道 ToString 與 HashSet 之間的性能如何。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM