開源 - Ideal庫 - 常用列舉擴充套件方法(二)

IT规划师發表於2024-11-14

書接上回,今天繼續和大家享一些關於列舉操作相關的常用擴充套件方法。

今天主要分享透過列舉值轉換成列舉、列舉名稱以及列舉描述相關實現。

我們首先修改一下上一篇定義用來測試的正常列舉,新增一個列舉項,程式碼如下:

//正常列舉
internal enum StatusEnum
{
    [Description("正常")]
    Normal = 0,
    [Description("待機")]
    Standby = 1,
    [Description("離線")]
    Offline = 2,
    Online = 3,
    Fault = 4,
}

01、根據列舉值轉換成列舉

該方法接收列舉值作為引數,並轉為對應列舉,轉換失敗則返回空。

列舉類Enum中自帶了兩種轉換方法,其一上篇文章使用過即Parse,這個方法可以接收string或Type型別作為引數,其二為ToObject方法,接收引數為整數型別。

因為列舉值本身就是整數型別,因此我們選擇ToObject方法作為最終實現,這樣就避免使用Parse方法時還需要把整數型別引數進行轉換。

同時我們透過上圖可以發現列舉值可能的型別有uint、ulong、ushort、sbyte、long、int、byte、short八種情況。

因此下面我們以int型別作為示例,進行說明,但同時考慮到後面通用性、擴充套件性,我們再封裝一個公共的泛型實現可以做到支援上面八種型別。因此本方法會呼叫一個內部私有方法,具體如下:

//根據列舉值轉換成列舉,轉換失敗則返回空
public static T? ToEnumByValue<T>(this int value) where T : struct, Enum
{
    //呼叫根據列舉值轉換成列舉方法
    return ToEnumByValue<int, T>(value);
}

而內部私有方法即透過泛型實現對多種型別支援,我們先看程式碼實現再詳細講解,具體程式碼如下:

//根據列舉值轉換成列舉,轉換失敗則返回空
private static TEnum? ToEnumByValue<TSource, TEnum>(this TSource value)
    where TSource : struct
    where TEnum : struct, Enum
{
    //檢查整數值是否是有效的列舉值並且是否是有效位標誌列舉組合項
    if (!Enum.IsDefined(typeof(TEnum), value) && !IsValidFlagsMask<TSource, TEnum>(value))
    {
        //非法資料則返回空
        return default;
    }
    //有效列舉值則進行轉換
    return (TEnum)Enum.ToObject(typeof(TEnum), value);
}

該方法首先驗證引數合法性,驗證透過直接使用ToObject方法進行轉換。

引數驗證首先透過Enum.IsDefined方法校驗引數是否是有效的列舉項,這是因為無論是ToObject方法還是Parse方法對於整數型別引數都是可以轉換成功的,無論這個引數是否是列舉中的項,因此我們需要首先排查掉非列舉中的項。

而該方法中IsValidFlagsMask方法主要是針對位標誌列舉組合情況,位標誌列舉特性導致即使我們列舉項中沒有定義相關項,但是可以透過組合得到而且是合法的,因此我們需要對位標誌列舉單獨處理,具體實現程式碼如下:

//儲存列舉是否為位標誌列舉
private static readonly ConcurrentDictionary<Type, bool> _flags = new();
//儲存列舉對應掩碼值
private static readonly ConcurrentDictionary<Type, long> _flagsMasks = new();
private static bool IsValidFlagsMask<TSource, TEnum>(TSource source)
    where TSource : struct
    where TEnum : struct, Enum
{
    var type = typeof(TEnum);
    //判斷是否為位標誌列舉,如果有快取直接獲取,否則計算後存入快取再返回
    var isFlags = _flags.GetOrAdd(type, (key) =>
    {
        //檢查列舉型別是否有Flags特性
        return Attribute.IsDefined(key, typeof(FlagsAttribute));
    });
    //如果不是位標誌列舉則返回false
    if (!isFlags)
    {
        return false;
    }
    //獲取列舉掩碼,如果有快取直接獲取,否則計算後存入快取再返回
    var mask = _flagsMasks.GetOrAdd(type, (key) =>
    {
        //初始化儲存掩碼變數
        var mask = 0L;
        //獲取列舉所有值
        var values = Enum.GetValues(key);
        //遍歷所有列舉值,透過位或運算合併所有列舉值
        foreach (Enum enumValue in values)
        {
            //將列舉值轉為long型別
            var valueLong = Convert.ToInt64(enumValue);
            // 過濾掉負數或無效的值,規範的位標誌列舉應該都為非負數
            if (valueLong >= 0)
            {
                //合併列舉值至mask
                mask |= valueLong;
            }
        }
        //返回包含所有列舉值的列舉掩碼
        return mask;
    });
    var value = Convert.ToInt64(source);
    //使用待驗證值value和列舉掩碼取反做與運算
    //結果等於0表示value為有效列舉值
    return (value & ~mask) == 0;
}

該方法首先是判斷當前列舉是否是位標誌列舉即列舉是否帶有Flags特性,可以透過Attribute.IsDefined實現,考慮到效能問題,因此我們把列舉是否為位標誌列舉存入快取中,當下次使用時就不必再次判斷了。

如果當前列舉不是位標誌列舉則之間返回false。

如果是位標誌列舉則進入關鍵點了,如何判斷一個值是否為一組值或一組值任意組合裡面的一個?

這裡用到了位掩碼技術,透過按位或對所有列舉項進行標記,達到合併所有列舉項的目的,同時還包括可能的組合情況。

這裡儲存掩碼的變數定義為long型別,因為我們需要相容上文提到的八種整數型別。同時一個符合規範的位標誌列舉設計列舉值是不會出現負數的因此也需要過濾掉。

同時考慮到效能問題,也需要把每個列舉對於的列舉掩碼記錄到快取中方便下次使用。

拿到列舉掩碼後我們需要對其進行取反,即表示所有符合要求的值,此值再與待驗證引數做按位與操作,如果不為0表示待驗證才是為無效列舉值,否則為有效列舉值。

關於位操作我們後面找機會再單獨詳解講解其中原理和奧秘。

講解完整個實現過程我們還需要對該方法進行詳細的單元測試,具體分為以下幾種情況:

(1) 正常列舉值,成功轉換成列舉;

(2) 不存在的列舉值,但是可以透過列舉項按位或合併得到,返回空;

(3) 不存在的列舉值,也不可以透過列舉項按位或合併得到,返回空;

(4) 正常位標誌列舉值,成功轉換成列舉;

(5) 不存在的列舉值,但是可以透過列舉項按位或合併得到,成功轉換成列舉;

(6) 不存在的列舉值,也不可以透過列舉項按位或合併得到,返回空;

具體實現程式碼如下:

[Fact]
public void ToEnumByValue()
{
    //正常列舉值,成功轉換成列舉
    var status = 1.ToEnumByValue<StatusEnum>();
    Assert.Equal(StatusEnum.Standby, status);
    //不存在的列舉值,但是可以透過列舉項按位或合併得到,返回空
    var isStatusNull = 5.ToEnumByValue<StatusEnum>();
    Assert.Null(isStatusNull);
    //不存在的列舉值,也不可以透過列舉項按位或合併得到,返回空
    var isStatusNull1 = 8.ToEnumByValue<StatusEnum>();
    Assert.Null(isStatusNull1);
    //正常位標誌列舉值,成功轉換成列舉
    var flags = 3.ToEnumByValue<TypeFlagsEnum>();
    Assert.Equal(TypeFlagsEnum.HttpAndUdp, flags);
    //不存在的列舉值,但是可以透過列舉項按位或合併得到,成功轉換成列舉
    var flagsGroup = 5.ToEnumByValue<TypeFlagsEnum>();
    Assert.Equal(TypeFlagsEnum.Http | TypeFlagsEnum.Tcp, flagsGroup);
    //不存在的列舉值,也不可以透過列舉項按位或合併得到,返回空
    var isFlagsNull = 8.ToEnumByValue<TypeFlagsEnum>();
    Assert.Null(isFlagsNull);
}

02、根據列舉值轉換成列舉或預設值

該方法是對上一個方法的補充,用於處理轉換不成功時,則返回一個指定預設列舉,具體程式碼如下:

//根據列舉值轉換成列舉,轉換失敗則返回預設列舉
public static T? ToEnumOrDefaultByValue<T>(this int value, T defaultValue) 
    where T : struct, Enum
{
    //呼叫根據列舉值轉換成列舉方法
    var result = value.ToEnumByValue<T>();
    if (result.HasValue)
    {
        //返回列舉
        return result.Value;
    }
    //轉換失敗則返回預設列舉
    return defaultValue;
}

然後我們進行一個簡單單元測試,程式碼如下:

[Fact]
public void ToEnumOrDefaultByValue()
{
    //正常列舉值,成功轉換成列舉
    var status = 1.ToEnumOrDefaultByValue(StatusEnum.Offline);
    Assert.Equal(StatusEnum.Standby, status);
    //不存在的列舉值,返回指定預設列舉
    var statusDefault = 5.ToEnumOrDefaultByValue(StatusEnum.Offline);
    Assert.Equal(StatusEnum.Offline, statusDefault);
}

03、根據列舉值轉換成列舉名稱

該方法接收列舉值作為引數,並轉為對應列舉名稱,轉換失敗則返回空。

實現則是透過根據列舉值轉換成列舉方法獲得列舉,然後透過列舉獲取列舉名稱,具體程式碼如下:

//根據列舉值轉換成列舉名稱,轉換失敗則返回空
public static string? ToEnumNameByValue<T>(this int value) where T : struct, Enum
{
    //呼叫根據列舉值轉換成列舉方法
    var result = value.ToEnumByValue<T>();
    if (result.HasValue)
    {
        //返回列舉名稱
        return result.Value.ToString();
    }
    //轉換失敗則返回空
    return default;
}

我們進行如下單元測試:

[Fact]
public void ToEnumNameByValue()
{
    //正常列舉值,成功轉換成列舉名稱
    var status = 1.ToEnumNameByValue<StatusEnum>();
    Assert.Equal("Standby", status);
    //不存在的列舉值,返回空
    var isStatusNull = 10.ToEnumNameByValue<StatusEnum>();
    Assert.Null(isStatusNull);
    //正常位標誌列舉值,成功轉換成列舉名稱
    var flags = 3.ToEnumNameByValue<TypeFlagsEnum>();
    Assert.Equal("HttpAndUdp", flags);
    //不存在的位標誌列舉值,返回空
    var isFlagsNull = 20.ToEnumNameByValue<TypeFlagsEnum>();
    Assert.Null(isFlagsNull);
}

04、根據列舉值轉換成列舉名稱預設值

該方法是對上一個方法的補充,用於處理轉換不成功時,則返回一個指定預設列舉名稱,具體程式碼如下:

//根據列舉值轉換成列舉名稱,轉換失敗則返回預設列舉名稱
public static string? ToEnumNameOrDefaultByValue<T>(this int value, string defaultValue) 
    where T : struct, Enum
{
    //呼叫根據列舉值轉換成列舉名稱方法
    var result = value.ToEnumNameByValue<T>();
    if (!string.IsNullOrWhiteSpace(result))
    {
        //返回列舉名稱
        return result;
    }
    //轉換失敗則返回預設列舉名稱
    return defaultValue;
}

進行簡單的單元測試,具體程式碼如下:

[Fact]
public void ToEnumNameOrDefaultByValue()
{
    //正常列舉值,成功轉換成列舉名稱
    var status = 1.ToEnumNameOrDefaultByValue<StatusEnum>("離線");
    Assert.Equal("Standby", status);
    //不存在的列舉名值,返回指定預設列舉名稱
    var statusDefault = 12.ToEnumNameOrDefaultByValue<StatusEnum>("離線");
    Assert.Equal("離線", statusDefault);
}

05、根據列舉值轉換成列舉描述

該方法接收列舉值作為引數,並轉為對應列舉名稱,轉換失敗則返回空。

實現則是透過根據列舉值轉換成列舉方法獲得列舉,然後透過列舉獲取列舉描述,具體程式碼如下:

//根據列舉值轉換成列舉描述,轉換失敗則返回空
public static string? ToEnumDescByValue<T>(this int value) where T : struct, Enum
{
    //呼叫根據列舉值轉換成列舉方法
    var result = value.ToEnumByValue<T>();
    if (result.HasValue)
    {
        //返回列舉描述
        return result.Value.ToEnumDesc();
    }
    //轉換失敗則返回空
    return default;
}

單元測試如下:

[Fact]
public void ToEnumDescByValue()
{
    //正常位標誌列舉值,成功轉換成列舉描述
    var flags = 3.ToEnumDescByValue<TypeFlagsEnum>();
    Assert.Equal("Http協議,Udp協議", flags);
    //正常的位標誌列舉值,組合項不存在,成功轉換成列舉描述
    var flagsGroup1 = 5.ToEnumDescByValue<TypeFlagsEnum>();
    Assert.Equal("Http協議,Tcp協議", flagsGroup1);
}

06、根據列舉值轉換成列舉描述預設值

該方法是對上一個方法的補充,用於處理轉換不成功時,則返回一個指定預設列舉描述,具體程式碼如下:

//根據列舉值轉換成列舉描述,轉換失敗則返回預設列舉描述
public static string? ToEnumDescOrDefaultByValue<T>(this int value, string defaultValue) 
    where T : struct, Enum
{
    //呼叫根據列舉值轉換成列舉描述方法
    var result = value.ToEnumDescByValue<T>();
    if (!string.IsNullOrWhiteSpace(result))
    {
        //返回列舉描述
        return result;
    }
    //轉換失敗則返回預設列舉描述
    return defaultValue;
}

單元測試程式碼如下:

[Fact]
public void ToEnumDescOrDefaultByValue()
{
    //正常列舉值,成功轉換成列舉描述
    var status = 1.ToEnumDescOrDefaultByValue<StatusEnum>("測試");
    Assert.Equal("待機", status);
    //不存在的列舉值,返回指定預設列舉描述
    var statusDefault = 11.ToEnumDescOrDefaultByValue<StatusEnum>("測試");
    Assert.Equal("測試", statusDefault);
}

稍晚些時候我會把庫上傳至Nuget,大家可以直接使用Ideal.Core.Common。

:測試方法程式碼以及示例原始碼都已經上傳至程式碼庫,有興趣的可以看看。https://gitee.com/hugogoos/Ideal

相關文章