“扩展”Enum 类以使用 Flags 特性验证 Enum 值






4.58/5 (5投票s)
使用扩展方法增强枚举,以验证值等。

引言
每当有一组有序相关的值时,最好的做法是将它们放入一个枚举中,而不是将它们保留为常量值。
枚举至少自 C++ 以来就一直存在,并且 .NET 持续沿用,甚至还增加了新的功能,如类型的 Flags 特性或元素的描述。
背景
让我们来看一个简单的 C# 枚举
enum myenum : byte
{
a = 1,
b = 2,
c = 3
}
您可以轻松地为底层类型分配一个值,例如
myenum eval = default(myenum);//that is actually 0
eval = (myenum)4;
问题在于 0
和 4
不是 myenum
的一部分,编译器或运行时也不会对此进行抱怨,这可能会在您的代码中使用它们时导致问题。看起来唯一的检查是对底层类型的检查,而不是对 enum
的检查。
为了检查这些值,您可以使用 static Enum.IsDefined
方法来验证 enum
值。但是,当处理带有 Flags 的 enum
时,事情会变得更加复杂,如下所示
[Flags]// Define an Enum with FlagsAttribute.
enum MultiHue : sbyte
{
[Description("no Color")]Black = 0,
[Description("pure red")]Red = 1,
[Description("pure green")]Green = 2,
//[Description("pure yellow")]Yellow = 3,
//Green | Red number 3 is deliberately omitted
[Description("pure blue")]Blue = 4,
[Description("composite green + blue")]Cyan = Green | Blue,//6
[Description("composite red + blue")]Magenta = Red | Blue,//5
[Description("composite red + blue + green")]White = Red | Blue | Green,//7
[Description("not a valid color")]BadColor = 127
};
如果您运行 Enum.IsDefined(typeof(MultiHue),3)
,返回值是 false
。虽然这是 true
,但对于 Flags,您可能期望不同的结果,因为 3
等于 1 | 2
,所以它应该是一个有效值。即使 ToString()
实例方法也会返回 'Red
, Green
',而不是数字 3
作为未定义的值。
验证枚举的值
从 C# 3.0 开始,为了提高可用性,微软引入了扩展方法,我们也可以使用它们作为一种优雅的方式来验证 enum
的值。
对于没有 Flags 的枚举,验证功能已内置于框架中,但使用此语言功能可以使其更具可读性。
public static bool IsDefined(this System.Enum value)
{
return System.Enum.IsDefined(value.GetType(), value);
}
使用此扩展方法,验证变得非常优雅。
MultiHue mh = MultiHue.Blue;
bool isdefined = mh.IsDefined();
验证带有 Flags 的 enum
会稍微复杂一些,因为 IsDefined
方法不足以区分 Defined
和 Valid
。
public static bool IsValidEnumValue(this System.Enum value)
{
if (value.HasFlags())
return IsFlagsEnumDefined(value);
else
return value.IsDefined();
}
private static bool IsFlagsEnumDefined(System.Enum value)
{// modeled after Enum's InternalFlagsFormat
Type underlyingenumtype = Enum.GetUnderlyingType(value.GetType());
switch (Type.GetTypeCode(underlyingenumtype))
{
case TypeCode.Int16:
case TypeCode.Int32:
case TypeCode.Int64:
case TypeCode.SByte:
case TypeCode.Single:
{
object obj = Activator.CreateInstance(underlyingenumtype);
long svalue = System.Convert.ToInt64(value);
if (svalue < 0)
throw new ArgumentException(
string.Format("Can't process negative {0} as {1}
enum with flags", svalue, value.GetType().Name));
}
break;
default:
break;
}
ulong flagsset = System.Convert.ToUInt64(value);
Array values = Enum.GetValues(value.GetType());//.Cast().ToArray();
int flagno = values.Length - 1;
ulong initialflags = flagsset;
ulong flag = 0;
//start with the highest values
while (flagno >= 0)
{
flag = System.Convert.ToUInt64(values.GetValue(flagno));
if ((flagno == 0) && (flag == 0))
{
break;
}
//if the flags set contain this flag
if ((flagsset & flag) == flag)
{
//unset this flag
flagsset -= flag;
if (flagsset == 0)
return true;
}
flagno--;
}
if (flagsset != 0)
{
return false;
}
if (initialflags != 0 || flag == 0)
{
return true;
}
return false;
}
public static bool HasFlags(this System.Enum value)
{
return value.GetType().GetCustomAttributes(typeof(System.FlagsAttribute),
false).Length > 0;
}
验证的核心是 IsFlagsEnumDefined
方法,它是 Enum.InternalFlagsFormat
的修改版本,可以通过 Reflector 获取。
将变量转换为枚举
到目前为止,我一直处理在赋值后验证过的 enum
值。由于预防有时胜于治疗,让我们来看看一些转换问题。如果您在一个 `checked` 块中,并使用像下面这样的整数值进行强制类型转换
checked
{
//MultiHue has the underlying type of sbyte: -127...127
MultiHue mh = (MultiHue)(object)128;//InvalidCastException
int i = 128;
mh = (MultiHue)i;//OverflowException
}
如果无法执行到 enum
底层类型的转换,将抛出 OverflowException
或 InvalidCastException
。
如果块是 `unchecked`,则不会抛出异常,并且 mh
值将成为底层类型的默认值,该默认值始终是 0
。
如果您使用 static Enum.Parse
方法,并且参数越界,则无论是在 `checked` 还是 `unchecked` 模式下,都会抛出 OverflowException
。
//MultiHue has the underlying type of sbyte: -127...127
MultiHue mh = (MultiHue)Enum.Parse(typeof(MultiHue),"128");//OverflowException
因此,我创建了一个方法来处理 enum
转换,在通常情况下不会抛出异常。请注意,在不同 enum
类型之间进行转换并不容易,因为您无法利用原生的 Enum.Parse
和 object
的 ToString
方法。
public static bool SafeConvertToEnum(object value, out EnumType retv)
{
Type enumType = typeof(EnumType);
if (!enumType.IsEnum)
throw new System.ArgumentException(string.Format("{0} is not an Enum.",
enumType.Name));
if (value == null)
{
retv = default(EnumType);
return false;
}
Type valType = value.GetType();
bool isString = valType == typeof(string);
bool isOrdinal = valType.IsPrimitive ||
typeof(decimal) == valType || valType.IsEnum;
if (!isOrdinal && !isString)
throw new System.ArgumentException
(string.Format("{0} can not be converted to an enum", valType.Name));
try
{
checked
{
if (valType == Enum.GetUnderlyingType(enumType))
retv = (EnumType)value;
else
{
if(isString)
retv = (EnumType) Enum.Parse(typeof(EnumType), value as string);
else
if (valType.IsEnum)
{
Enum en = (Enum)value;
object zero = Activator.CreateInstance(valType);
value = (en.CompareTo(zero) >= 0)?
Convert.ToUInt64(value):Convert.ToUInt64(value);
}
retv = (EnumType)Enum.Parse(typeof(EnumType), value.ToString());
}
}
if (!((System.Enum)(object)retv).IsValidEnumValue())
{
retv = default(EnumType);
return false;
}
}
catch(ArgumentException)
{
retv = default(EnumType);
return false;
}
catch (OverflowException)
{
retv = default(EnumType);
return false;
}
catch (InvalidCastException)
{
retv = default(EnumType);
return false;
}
catch (Exception ex)
{
throw new System.ArgumentException(string.Format
("Can't convert value {0}\nfrom the type of {1}
into the underlying enum type of {2}\nbecause {3}",
value, valType.Name, Enum.GetUnderlyingType(enumType).Name,
ex.Message), ex);
}
return true;
}
由于我大量使用了反射来实现这种安全的转换,我认为性能是微软没有将枚举强制类型转换“标准化”的原因。该方法的使用应该很简单。
MultiHue mh;
//for strings
bool res = EnumHelper.SafeConvertToEnum("3", out mh);
//for ordinal values
res = EnumHelper.SafeConvertToEnum(3, out mh);
EnumHelper
是我创建的一个泛型 static
类,用于操作 enum
s。
您会发现它包含其他有用的方法,可以在使用泛型语法处理 enum
s 时使用。对于 enum
作为泛型参数没有支持,所以我尝试在约束子句中使用 abstract System.Enum
类。由于这是所有已定义的 enum
s 的基类,人们会期望它能起作用,但不幸的是,编译器会抱怨:Constraint cannot be special class 'System.Enum
'。
为了达成妥协,我在 where
子句中放置了任何 enum
实现的接口。
public static class EnumHelper < EnumType >
where EnumType : struct, IComparable, IConvertible, IFormattable
{........
其他扩展方法,如 GetDescription()
,为enum
s 添加了语法糖,位于一个名为 EnumExtenders
的非泛型 static
类中。
Using the Code
为了使用此代码,您需要将命名空间添加到您的代码中...
using CTSExtenders;
... 并添加对 CTSExtenders.dll 的引用,或者最好是引用 CTSExtenders
项目。如果您还没有使用 C# 3.0,扩展方法将无法编译,但您可以删除 'this
' 参数修饰符,并像任何经典的 static
方法一样使用此功能。
历史
- 这是版本 1.0.0.0