C#中利用泛型扩展方法辅助枚举类型进行Flags处理

来源:互联网 发布:erp开源系统源码 编辑:程序博客网 时间:2024/05/16 14:49

Flags的按位或操作是在API设计中很常见的一种模式,十分直观和方便。C#中的枚举类型也提供了同样的机制来使用Flags模式。但是想要在代码中执行一些Flags操作需要频繁的使用按位或、按位与和按位求反等操作,十分不方便,代码可读性也不高。

最近在读CLR via C#讲述枚举类型的一章时,看到Jeff在书中为特定枚举类型实现了一组扩展方法来辅助枚举类型对Flags进行处理。

代码如下:

public static class MyEnumExtensions{    public static Boolean IsSet(this MyEnum enumValue,        MyEnum valueToTest)    {        if (valueToTest == 0)            throw new ArgumentOutOfRangeException("valueToTest",                "Value must not be 0");        return (enumValue & valueToTest) == valueToTest;    }        public static Boolean IsClear(this MyEnum enumValue,        MyEnum valueToTest)    {        if (valueToTest == 0)            throw new ArgumentOutOfRangeException("valueToTest",                "Value must not be 0");        return !IsSet(enumValue, valueToTest);    }        public static Boolean AnyFlagsSet(this MyEnum enumValue,        MyEnum testValues)    {        return ((enumValue & testValues) != 0);    }        public static MyEnum Set(this MyEnum enumValue,        MyEnum setValues)    {        return enumValue | setValues;    }        public static MyEnum Clear(this MyEnum enumValue,        MyEnum clearValues)    {        return enumValue & ~clearValues;    }        public static void ForEach(this MyEnum enumValue,        Action<MyEnum> processValue)    {        if (processValue == null)            throw new ArgumentNullException("processValue");        for (UInt32 bit = 1; bit != 0; bit <<= 1)        {            UInt32 temp = ((UInt32)enumValue) & bit;            if (temp != 0)                processValue((MyEnum)temp);        }    }}

 

虽然使用方便,但上述代码只能对某一种特定的枚举类型起作用。新的支持Flags的枚举类型还需要再编写同样的一组扩展方法。自然想到实现一组泛型的扩展方法应用到所有枚举类型。

着手实现的过程中逐渐发现,Jeff没有在书中给出一组泛型方法是有其考虑的。

 

首先,C#不支持枚举类型的泛型参数约束。

我们可以写:

void Func<T>() where T : class

void Func<T>() where T : struct

 

但我们不能写:

void Func<T>() where T : enum

void Func<T>() where T : System.Enum

既然不能约束类型参数为枚举,自然就会引出类型安全问题——Flags操作的操作数不一定来自于同一个枚举类型,甚至不是一个枚举类型。

所以要实现泛型就需要约束扩展方法只作用于枚举类型同时保证类型安全,在C#现有的限制下很难优美得实现这一特性。不过经过反复实验,我最终找到了一种实现方法。

 

代码如下:

/// <summary>/// Extensions help with handling enum with flags./// </summary>public static class FlagsExtensions{    /// <summary>    /// Test if the flag is set to this value.    /// </summary>    public static Boolean IsSet(this Enum enumValue,        Enum valueToTest)    {        if (enumValue.GetType() != valueToTest.GetType())            throw new ArgumentException("valueToTest",                "Value must share the same type.");        if (Convert.ToUInt64(valueToTest) == 0)            throw new ArgumentOutOfRangeException("valueToTest",                "Value must not be 0");        return (Convert.ToUInt64(enumValue) & Convert.ToUInt64(valueToTest))            == Convert.ToUInt64(valueToTest);    }    /// <summary>    /// Test if the value is clear with this flag.    /// </summary>    public static Boolean IsClear(this Enum enumValue,        Enum valueToTest)    {        if (enumValue.GetType() != valueToTest.GetType())            throw new ArgumentException("valueToTest",                "Value must share the same type.");        if (Convert.ToUInt64(valueToTest) == 0)            throw new ArgumentOutOfRangeException("valueToTest",                "Value must not be 0");        return !IsSet(enumValue, valueToTest);    }    /// <summary>    /// Test if one of these test flags is set to this value.    /// </summary>    public static Boolean AnyFlagsSet(this Enum enumValue,        Enum testValues)    {        if (enumValue.GetType() != testValues.GetType())            throw new ArgumentException("testValues",                "Value must share the same type.");        return ((Convert.ToUInt64(enumValue) & Convert.ToUInt64(testValues))            != 0);    }    /// <summary>    /// Return a new value that set with specific flags.    /// </summary>    public static TEnum Set<TEnum>(this Enum enumValue, TEnum setValues)    {        if (enumValue.GetType() != setValues.GetType())            throw new ArgumentException("setValues",                "Value must share the same type.");        return (TEnum)Enum.ToObject(enumValue.GetType(),            Convert.ToUInt64(enumValue) | Convert.ToUInt64(setValues));    }    /// <summary>    /// Return a new value with specific flags removed from this value.    /// </summary>    public static TEnum Clear<TEnum>(this Enum enumValue, TEnum clearValues)    {        if (enumValue.GetType() != clearValues.GetType())            throw new ArgumentException("clearValues",                "Value must share the same type.");        return (TEnum)Enum.ToObject(enumValue.GetType(),            Convert.ToUInt64(enumValue) & ~Convert.ToUInt64(clearValues));    }    /// <summary>    /// For each flag in the value, perform the specific action.    /// </summary>    public static void ForEach<TEnum>(this Enum enumValue, Action<TEnum> processValue)    {        if (processValue == null)            throw new ArgumentNullException("processValue");        if (enumValue.GetType() != typeof(TEnum))            throw new ArgumentException("type:TEnum",                "processValue's parameter must have the same type.");        for (UInt64 bit = 1; bit != 0; bit <<= 1)        {            UInt64 temp = Convert.ToUInt64(enumValue) & bit;            if (temp != 0)                processValue((TEnum)Enum.ToObject(typeof(TEnum), temp));        }    }}

使用示例:

[Flags]enum EnumWithFlags{    FlagA = 0x01,    FlagB = 0x02,    FlagC = 0x04,    FlagD = 0x08,    FlagE = 0x10,    FlagF = 0x20}class Program{    static void Main(string[] args)    {        EnumWithFlags value =            EnumWithFlags.FlagA            | EnumWithFlags.FlagB            | EnumWithFlags.FlagC;        value = value.Set(EnumWithFlags.FlagD | EnumWithFlags.FlagE);        value = value.Clear(EnumWithFlags.FlagA | EnumWithFlags.FlagC);        value.ForEach((EnumWithFlags v) => Console.WriteLine(v));        Console.ReadKey();    }}

 

 

这一实现主要有三个要点

  • 将扩展方法的this参数设置为System.Enum类型,因为全部枚举类型均派生自System.Enum,这样可以确保扩展方法只作用于枚举类型。
  • 对所有枚举类型进行类型检查,确保类型安全。
  • 将所有枚举类型转换到System.UInt64进行按位操作,确保足以容纳任何基类型的枚举值。

需要注意的是调用ForEach方法时如果使用Lambda表达式,那么编译器将无法推断泛型参数的类型,所以需要显式指定参数类型。

这一实现同样有一些缺点,例如频繁的类型检查和转换。密集地调用可能导致很差的性能,比较适合于一般Flags参数的传递,修改和检测。

—————————————————

Chobi-Q欢迎转载,转载请注明出处。

原创粉丝点击