今天想和大家分享關於列舉擴充套件設計思路和在實現過程中遇到的難點。
01、設計思路
設計思路說起來其實也很簡單,就是透過列舉相關資訊:列舉值、列舉名、列舉描述、列舉項、列舉型別,進行各種轉換,透過一個資訊獲取其他資訊。比如透過列舉項獲取列舉描述、透過列舉型別獲取列舉名稱-列舉描述鍵值對用於下拉選單等等。
主要包括以下幾類轉換實現:
(1)透過列舉值轉成:列舉、列舉名稱、舉描述;
(2)透過列舉名稱轉成:列舉、列舉值、列舉描述;
(3)透過列舉描述轉成:列舉、列舉值、列舉名稱;
(4)透過列舉項轉成:列舉值、列舉描述;
(5)透過列舉型別轉成:列舉值-列舉名稱、列舉值-列舉描述、列舉名稱-列舉值、列舉名稱-列舉描述、列舉描述-列舉值、列舉描述-列舉名稱等鍵值對集合;
02、實現難點
在實現過程中的確遇到一些難點以及需要注意的小細節。
1、列舉名稱轉列舉中的小細節
在實現列舉名稱轉列舉的過程中遇到了一個小坑。
列舉本身提供了Enum.TryParse方法用於把列舉名稱字串或者列舉值字串轉為列舉,因此我們選用了此方法實現列舉名稱轉列舉。但是我們自己這個介面設計目標是把列舉名稱轉為列舉,因此需要排除誤傳列舉值字串的情況。
方案:當轉換成功後,我們還需要呼叫Enum.IsDefined方法檢查列舉名稱字串是否是有效的列舉名稱字串,如果不是列舉中現有的有效列舉項,還需要考慮是否為位標誌組合情況。
程式碼如下:
//根據列舉名稱轉換成列舉,轉換失敗則返回空
public static TEnum? ToEnumByName<TEnum>(this string name)
where TEnum : struct, Enum
{
//轉換成功則返回結果,否則返回空
if (Enum.TryParse<TEnum>(name, out var result))
{
//檢查是否為有效的列舉名稱字串,
if (Enum.IsDefined(typeof(TEnum), name))
{
//返回列舉
return result;
}
else
{
//計算是否為有效的位標誌組合項
var isValidFlags = IsValidFlagsMask<ulong, TEnum>(result.ToEnumValue<ulong>());
//如果是有效的位標誌組合項則返回列舉,否則返回空
return isValidFlags ? result : default(TEnum?);
}
}
//返回空
return default;
}
2、列舉描述轉列舉中的小細節
在實現列舉描述轉列舉的過程中對於帶位標誌列舉處理需要特別小心。我們知道位標誌列舉有個特性是列舉項之間可以透過位操作進行組合得到一個有效的列舉項,而且這個列舉項還不存在於當前定義的列舉中。這就要求我們必須處理好組合情況。
方案:因此可以按照以下步驟進行處理。
(1)優先處理當列舉不帶位標誌的情況;
(2)再處理帶位標誌的情況;
位標誌組合是透過英文逗號[,]拼接的,因此還要考慮到如果一個描述字串中帶有英文逗號[,],但是本身又不是組合的情況。
(3)先把描述當作不是組合情況,先處理一遍,如果轉換成功則結束轉換;
(4)如果是組合的情況,則計算出組合的每項列舉值,透過位操作得到其對應的列舉項;
而在組合的情況中還需要考慮,如果有組合的項時無效的情況,則這個列舉描述轉換應該標記為無效。
(5)判斷組合的每一項都是正確的,否則返回空;
(6)將透過組合計算出來的列舉項轉為列舉;
因為透過列舉值轉列舉過程中還需要明確列舉值型別才行,因此最後一步還需要根據列舉型別實際的基礎型別呼叫不同的轉換方法。
具體程式碼如下:
//根據列舉描述轉換成列舉,轉換失敗返回空
public static TEnum? ToEnumByDesc<TEnum>(this string description)
where TEnum : struct, Enum
{
var type = typeof(TEnum);
var info = GetEnumTypeInfo(type);
//不是位標誌列舉的情況處理
if (!info.IsFlags)
{
return ToEnumDesc<TEnum>(description);
}
//是位標誌列舉的情況處理
//不是組合位的情況,本身可能就包含[,]
var tenum = ToEnumDesc<TEnum>(description);
if (tenum.HasValue)
{
return tenum;
}
//如果不包含[,],則直接返回
if (!description.Contains(','))
{
return default;
}
//是組合位的情況
var names = description.Split(',');
var values = Enum.GetValues(type);
//記錄有效列舉描述個數
var count = 0;
ulong mask = 0L;
//變數列舉所有項
foreach (var name in names)
{
foreach (Enum value in values)
{
//取列舉項描述與目標描述相比較,相同則返回該列舉項
if (value.ToEnumDesc() == name)
{
//有效列舉個數加1
count++;
//將列舉值轉為long型別
var valueLong = Convert.ToUInt64(value);
// 過濾掉負數或無效的值,規範的位標誌列舉應該都為非負數
if (valueLong >= 0)
{
//合併列舉值至mask
mask |= valueLong;
}
break;
}
}
}
//如果兩者不相等,說明描述字串不是一個有效組合項
if (count != names.Length)
{
return default;
}
var underlyingType = Enum.GetUnderlyingType(type);
if (underlyingType == typeof(byte))
{
return ((byte)mask).ToEnumByValue<TEnum>();
}
else if (underlyingType == typeof(sbyte))
{
return ((sbyte)mask).ToEnumByValue<TEnum>();
}
else if (underlyingType == typeof(short))
{
return ((short)mask).ToEnumByValue<TEnum>();
}
else if (underlyingType == typeof(ushort))
{
return ((ushort)mask).ToEnumByValue<TEnum>();
}
else if (underlyingType == typeof(int))
{
return ((int)mask).ToEnumByValue<TEnum>();
}
else if (underlyingType == typeof(uint))
{
return ((uint)mask).ToEnumByValue<TEnum>();
}
else if (underlyingType == typeof(long))
{
return ((long)mask).ToEnumByValue<TEnum>();
}
else if (underlyingType == typeof(ulong))
{
return mask.ToEnumByValue<TEnum>();
}
return default;
}
3、列舉轉列舉值中的小細節
我們知道在定義列舉的時候可以指定列舉值型別為以下八種型別sbyte、byte、short、ushort、int、uint、long、ulong,因此所有涉及到返回列舉值的方法,我們要考慮到支援不同型別的列舉值型別返回。
同時我們也知道相同的方法名和入參,並不能透過不同的返回型別區分出不同的過載方法。
方案:因此我們可以透過泛型的方法支援不同型別的列舉值型別返回。
4、透過列舉值轉換的小細節
透過上面我們知道列舉值型別有八種,因此涉及到列舉值轉換出其他資訊是需要同時相容這八種型別。
要實現這個功能,可以用上面提到的泛型,如果使用泛型我們可以使用struct型別來限制列舉值泛型TValue,因為struct可以覆蓋列舉值的八種型別,但是也引發了另一個問題就是float、double、DateTime都是struct型別,這樣就導致這些型別在編輯器中也可以點出ToEnumByValue等相關方法。
作為一個公共封裝方法,這種方式顯然是對使用者不友好的。
因此我們選擇另外一種方式:過載方法來實現,透過封裝方法多寫一些程式碼來實現對使用者友好呼叫。
5、如何高效返回鍵值對資料
在透過列舉型別轉成各種鍵值對集合時,有些方法需要用到反射,因此如何高效的獲取這些資訊就變成重中之重。
最直接的想法是直接把每種列舉型別對應的鍵值對集合直接快取起來,下次用到的時候直接從快取中獲取即可。
但是仔細詳細,一共有3個資料:列舉值、列舉名稱、列舉描述。兩兩組合有6種情況,也就是要6個快取儲存12個資料,這樣其實就相當於浪費了3倍的空間。
當然我們可以把這三個值存一份,然後在需要的是直接組合獲取,但是我們仔細分析一些是否有必要把所有資料都快取下來。
列舉值會涉及到型別轉換,列舉名稱涉及呼叫ToString方法,列舉描述需要用到反射。這其中最好效能的非列舉描述不可,因此列舉描述肯定需要快取,列舉值和列舉名稱可以考慮快取。
假如我們只快取列舉描述,那麼列舉值和列舉名稱在使用的時候直接轉換,那麼我們執行要一份記錄列舉描述的快取以及一份記錄列舉型別對應其所有列舉項的快取即可。另外因為列舉對應的是否帶位標誌標記以及掩碼可以同時記錄下來。
程式碼如下:
public static partial class EnumExtension
{
//列舉型別基礎資訊
private sealed class EnumTypeInfo
{
//是否是位標誌
public bool IsFlags { get; set; }
//列舉掩碼
public ulong Mask { get; set; }
//列舉項集合
public List<Enum> Items { get; set; } = [];
}
//儲存列舉項對應的描述
private static readonly ConcurrentDictionary<Enum, string> _descs = new();
//儲存列舉相關資訊
獲取列舉名稱-列舉描述鍵值對程式碼如下,首先獲取列舉型別基礎資訊,然後把列舉項集合轉為鍵值對集合。
//獲取列舉名稱+列舉描述
public static Dictionary<string, string> ToEnumNameDescs(this Type type)
{
//根據type獲取列舉型別基礎資訊
var info = GetEnumTypeInfo(type);
//透過列舉項集合轉為目標型別
return info.Items.ToDictionary(r => r.ToString(), r => r.ToEnumDesc());
}
6、如何識別一個列舉值是否為有效的位標誌組合
如何識別一個列舉值是否是一個有效的位標誌組合,可以說是整個程式碼中最難的部分了,其實前面也有提到,主要應用掩碼的思想,上一章節已經詳解講解了這裡就不在贅述了。
稍晚些時候我會把庫上傳至Nuget,大家可以直接使用Ideal.Core.Common。
注:測試方法程式碼以及示例原始碼都已經上傳至程式碼庫,有興趣的可以看看。https://gitee.com/hugogoos/Ideal