65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.58/5 (5投票s)

2007 年 12 月 28 日

CPOL

4分钟阅读

viewsIcon

63759

downloadIcon

333

使用扩展方法增强枚举,以验证值等。

Screenshot -

引言

每当有一组有序相关的值时,最好的做法是将它们放入一个枚举中,而不是将它们保留为常量值。

枚举至少自 C++ 以来就一直存在,并且 .NET 持续沿用,甚至还增加了新的功能,如类型的 Flags 特性或元素的描述。

背景

让我们来看一个简单的 C# 枚举

    enum myenum : byte
    {
        a = 1,
        b = 2,
        c = 3
    }

您可以轻松地为底层类型分配一个值,例如

myenum eval = default(myenum);//that is actually 0
eval = (myenum)4;

问题在于 04 不是 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 objectToString 方法。

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 类,用于操作 enums。
您会发现它包含其他有用的方法,可以在使用泛型语法处理 enums 时使用。对于 enum 作为泛型参数没有支持,所以我尝试在约束子句中使用 abstract System.Enum 类。由于这是所有已定义的 enums 的基类,人们会期望它能起作用,但不幸的是,编译器会抱怨:Constraint cannot be special class 'System.Enum'。
为了达成妥协,我在 where 子句中放置了任何 enum 实现的接口。

public static class EnumHelper < EnumType >
where EnumType : struct, IComparable, IConvertible, IFormattable
{........

其他扩展方法,如 GetDescription() ,为
enums 添加了语法糖,位于一个名为 EnumExtenders 的非泛型 static 类中。

Using the Code

为了使用此代码,您需要将命名空间添加到您的代码中...

using CTSExtenders;

... 并添加对 CTSExtenders.dll 的引用,或者最好是引用 CTSExtenders 项目。如果您还没有使用 C# 3.0,扩展方法将无法编译,但您可以删除 'this' 参数修饰符,并像任何经典的 static 方法一样使用此功能。

历史

  • 这是版本 1.0.0.0
© . All rights reserved.