返回

枚举类型和位标志

CLR和FCL结合起来之后, 枚举类型和位标志才真正成为面向对象的类型. 提供了一些强大的功能.大部分开发人员并不熟悉.

枚举类型

枚举类型(enumerated type) 定义了一组 “符号名称/值” 配对. 例如:

// 每个符号标识一种颜色, 也可以用0标识白色
internal enum Color
{
  White,    // 赋值0
  Red,      // 赋值1
  Green,    // 赋值2
  Blue,     // 赋值3
  Orange    // 赋值4
}

不应该将数字硬编码到代码中,而应该使用枚举类型.

  1. 更容易阅读和理解代表的含义. 调试程序能向开发人员显示有意义的符号名称.
  2. 枚举是强类型的. 比如Color.Orange和Fruit.Orange是不同的,虽然都有一个Orange.

在.Net Framework中, 枚举类型不只是编译器所关心的符号, 还是类型系统中的一等公民, 能实现很强大的操作.(在其他环境比如非托管C++中,枚举没有这个特点的).

每个枚举都直接从System.Enum派生 , System.EnumSystem.ValueType派生.System.ValueType又从System.Object派生.

枚举都是 值类型 . 可以用未装箱和已装箱的形式来表示,但是区别于其他值类型, 枚举类型不能定义任何方法,属性,事件. 可以用扩展方法功能模拟向枚举类型添加方法.(在最后会举例)

编译枚举类型时,C#编译器把每个符号转换成类型的一个 常量字段.

// 此代码是伪类型定义,用来了解内部的工作方式
// 实际上编译器不会编译以下代码,禁止定义从System.Enum派生的类型.
internal struct Color : System.Enum
{
  // 以下是一些公共常量,它们定义了Color的符号和值
  public const Color White  = (Color)0;
  public const Color Red    = (Color)1;
  ....

  // 以下是一个公共实例字段.包含Color变量的值
  // 不能写代码来直接引用该字段
  public Int32 value__;
}

简单地说,枚举类型只是一个结构,其中定义了:

  • 一组常量字段

    会嵌入程序集的元数据中,并可以通过反射来访问(有静态方法和实例方法可以操作,不是必须要用反射)

  • 一个实例字段

可以在运行时获得与枚举类型关联的所有符号及其值. 可以将字符串符号转换成对应的数值.System.Eunm基类型提供了这些操作, 还提供了几个静态和实例方法, 可利用它们操作枚举类型的实例,从而避免了必须使用反射的麻烦.

枚举类型定义的符号是常量值, 编译器发现代码引用了枚举类型的符号时,会在编译时用数值替换符号,代码不再引用定义了符号的枚举类型. 也就是说:运行时可能不需要定义了枚举类型的程序集,编译时才需要. 如果代码引用了枚举类型,而不是只引用了枚举类型定义的符号,那么运行时也需要包含定义枚举所在的程序集. 版本问题: 因为枚举类型符号是常量, 而不是只读的值. 常量值直接嵌入IL代码. 如果枚举常量所在的程序集改动了, 引用此程序集的应用程序没有重新编译,使用的就是旧值.而不会重新从那个程序集中获取常量的新值.

返回枚举基础类型的方法

例如,System.Enum类型有一个GetUnderlyingType的静态方法,而System.Type类型有一个 GetEnumUnderlyingType的实例方法。

public static Type GetUnderlyingType (Type enumType); //System.Enum中定义
public Type GetEnumUnderlyingType (Type enumType);    //System.Type中定义

这些方法返回用于容纳一个枚举类型的值的基础类型. 每个枚举类型都有一个基础类型. int最常用,也是C#默认选择的.

C#编译器要求只能指定基元类型名称. 使用FCL类型名称会报错. 应输入类型byte,sbyte,short,ushort,int,uint,long,ulong.

internal enum Color : byte
{
  White,
  Red,
  Green
}

// 以下代码都会显示 System.Byte
Console.WriteLine(Enum.GetUnderlyingType(typeof(Color)));
Console.WriteLine(typeof(Color).GetEnumUnderlyingType());

C#编译器将枚举类型视为基元类型. 所以可用操作符(==,!=,<,>….)来操纵枚举类型的实例.可以显式将枚举类型转型为不同的枚举类型,可以显式将枚举类型实例转换为数值类型.

class Program
{
    public static void Main(string[] args)
    {
        Color c = Color.Green;
        Console.WriteLine(c);                            // Green
        Console.WriteLine(c.ToString());                 // Green
        Console.WriteLine(c.ToString("G"));              // Green 常规格式
        Console.WriteLine(c.ToString("D"));              // 2     十进制
        // 使用十六进制是, ToStirng总是输出大写字母
        // 输出的位数取决于枚举的基础类型
        // byte/sbyte: 2位, short/ushort: 4位 int/uint:8位 long/ulong: 16位
        // 如果有必要会添加前导零
        Console.WriteLine(c.ToString("X"));              // 02    十六进制
        Console.WriteLine(Color2.Green.ToString("X"));   // 00000002 十六进制
    }
}

internal enum Color : byte
{
    White,
    Red,
    Green
}
internal enum Color2 : int
{
    White,
    Red,
    Green
}

枚举类型格式化输出的方法

除了ToString方法,System.Enum类型还提供了静态Fotmat方法.

  • ToSring方法: 代码少,容易调用.
  • Fotmat方法: 可以传递value值,而不需要获取枚举的实例. 代码多.
// 比ToString方法好的一个地方是,允许为value传递参数的值,这样就不需要获取枚举的实例
public static String Format(Type enumType,Object value, String format);

// 输出: Blue
Console.WriteLine(Enum.Format(typeof(Color), 3, "G"));

返回枚举名称(值)的Array数组方法

声明枚举类型时, 所有符号都可以有相同的数值. 使用常规格式将数值转换为符号时,Enum的方法会返回其中的一个符号,如果没有对应的数值定义的符号时,会返回包含该数值的字符串.

// 返回的数组中每个元素都对应枚举类型中的一个符号名称,每个元素都包含符号名称的数值
public static Array GetValues(Type enumType); //System.Enum中定义
public Array GetEnumValues(Type enumType);    //System.Type中定义

// 返回一个数组
Color[] colors = (Color[]) Enum.GetValues(typeof(Color));

Console.WriteLine("定义枚举的个数: " + colors.Length);
Console.WriteLine("Value\tSymbol\n-----\t------");

foreach (Color color in colors)
{
    // 以十进制和常规格式显示每个符号
    // 0代表第0个参数, {0,5:D} 5个占位符,color的十进制   {0:G} color的常规格式(符号名称)
    Console.WriteLine("{0,5:D}\t{0:G}", color);
}
//    输出:
//    定义枚举的个数: 3
//    Value Symbol
//    -----   ------
//        0   White
//        1   Red
//        2   Green

GetEnumValues和GetValues方法返回Array,必须转型成恰当的数组类型. 所以定义了一个自定义的方法:

// 使用下面自定义的泛型方法可以获得更好的编译时类型安全性
public static TEnum[] GetEnumValues<TEnum>() where TEnum :struct
 {
     return (TEnum[]) Enum.GetValues(typeof(TEnum));
 }

 // 使用如下
 Color[] colors = GetEnumValues<Color>();

返回枚举符号名称的方法

要显示符号名称时,ToString(常规格式)方法是经常使用的,前提是字符串不需要本地化(枚举类型没有提供本地化支持). 除了GetValues,还提供了以下方法来返回枚举类型的符号.

// 返回数值的字符串表示
public static String GetName(Type enumType, Object value); // System.Enum中定义
public String GetEnumName(Object value); // System.Type中定义

// 返回一个String数组,枚举中每个符号都对应一个String
public static String[] GetName(Type enumType);// System.Enum中定义
public String[] GetEnumName();  // System.Type中定义

返回与符号对应的值的方法

来查找与符号对应的值. 可以利用这个操作转换用户在文本框中输入的一个符号.利用Enum提供的静态Parse和TryParse方法.

public static Object Parse(Type enumType, String value);
public static Object Parse(Type enumType, String value, Boolean ignoreCase);
public static Boolean TryParse<TEnum>(String value, out TEnum result) where TEnum : struct;
public static Boolean TryParse<TEnum>(String value, Boolean ignoreCase, out TEnum result) where TEnum : struct;


// 使用方法
// 因为orange定义为4, 所有c被初始化为4
Color c = (Color) Enum.Parse(typeof(Color), "orange", true);

// 运行时异常, 没有定义此Brown符号
// Color c1 = (Color) Enum.Parse(typeof(Color), "Brown", false);

// 创建值为1的Color枚举类型的实例.
Enum.TryParse<Color>("1", false, out c);
Console.WriteLine(c.ToString("D")  + ":" + typeof(Color).GetEnumName(c));

// 创建值23的Color枚举类型的实例.
Enum.TryParse<Color>("23", false, out c);
Console.WriteLine(c + ":" + typeof(Color).GetEnumName(c));

判断数值对于某枚举类型是否合法

IsDefined方法经常用于参数校验.

public static Boolean IsDefined(Type enumType, Object value); // System.Enum中定义
public Boolean IsEnumDefined(Object value);// System.Type中定义


// 用法
// 输出 true , Red定义为1
Console.WriteLine(Enum.IsDefined(typeof(Color),1));
// 输出 true , white定义为0
Console.WriteLine(Enum.IsDefined(typeof(Color),"White"));

// 输出 false , 会检查区分大小写,并没有定义小写的white
Console.WriteLine(Enum.IsDefined(typeof(Color),"white"));
// 输出 false , 没有和值10对应的符号
Console.WriteLine(Enum.IsDefined(typeof(Color),10));

// IsDefined方法经常用于参数校验.
public void SetColor(Color c)
{
    if (!Enum.IsDefined(typeof(Color),c))
    {
        throw (new ArgumentOutOfRangeException("c",c,"无效颜色值."));
    }
    //.....
}

参数校验是很有用的一个功能,防止别人这样调用SetColor((Color) 100); 并没有对应100的颜色,抛出异常指出参数无效.

IsDefined 需要慎用

  1. 总是执行 区分大小写 的查找,没有办法让它执行不区分大小写的查找
  2. 执行速度慢,因为内部使用了 反射.(如果写代码来手动检查每一个可能的值,性能极有可能变得更好)
  3. 只有枚举类型本身在调用IsDefined在同一个 程序集 时才可以使用.(枚举类型是常量,内联到IL代码中的,版本控制问题)
  4. 不要对位标志枚举使用IsDefined方法. 因为如果传递字符串,它不会把字符串拆分为单独的token来进行查找,而是查找整个字符串, 把它看成是一个包含逗号的更大的符号.由 于不能再枚举中定义包含逗号的符号,所以永远找不到. 传递数值,它会检查枚举类型是否定义了其数值和传入的数值匹配,位标志不能简单的这样匹配,所以不要用这个方法.

ToObject方法

将byte,sbyte,Int16,UInt16,Int32,UInt32,Int64,UInt64等类型的实例转换为 枚举类型 的实例.

枚举一般来说应该定义在和需要它的类型同级.

位标志

位标志(bit flag), System.IO.File类型的GetAttributes方法,会返回一个FileAttributes的实例, FileAttributes是Int32类型的枚举, 每一个位都反映了文件的一个特性.

[Flags]
// Int32 有4个字节 每个字节8个位
public enum FileAttributes
{
    //                十进制        二进制
    ReadOnly          = 1,      // 0001
    Hidden            = 2,      // 0010
    System            = 4,      // 0100
    Directory         = 16,     // 0001 0000
    Archive           = 32,     // 0010 0000
    Device            = 64,     // 0100 0000
    Normal            = 128,    // 1000 0000
    Temporary         = 256,    // 0001 0000 0000
    SparseFile        = 512,    // 0010 0000 0000
    ReparsePoint      = 1024,   // 0100 0000 0000
    Compressed        = 2048,   // 1000 0000 0000
    Offline           = 4096,   // 0001 0000 0000 0000
    NotContentIndexed = 8192,   // 0010 0000 0000 0000
    Encrypted         = 16384,  // 0100 0000 0000 0000
    IntegrityStream   = 32768,  // 1000 0000 0000 0000
    NoScrubData       = 131072, // 0010 0000 0000 0000 0000
}
// 判断文件是否隐藏可以执行以下代码:
String file = Assembly.GetEntryAssembly().Location;
// 获取文件的特性的枚举位标志
FileAttributes attributes = File.GetAttributes(file);
// 用& 与操作进行判断,如果attributes对应的Hidden位标志是1,那么1&1 就是true.
Console.WriteLine("Is {0} hidden? {1}", file, (attributes & FileAttributes.Hidden) != 0);
// HasFlag方法获取Enum参数,Enum参数是引用类型.
// Console.WriteLine("Is {0} hidden? {1}", file, attributes.HasFlag(FileAttributes.Hidden) );

// 设置文件只读和隐藏特性
// 用 |
File.SetAttributes(file, FileAttributes.Hidden | FileAttributes.ReadOnly);

避免使用Enum提供的HasFlag方法, 理由是,由于它获取Enum类型的参数,所以传给它的任何值都必须装箱,产生一次内存分配.

关于为什么传入结构会进行装箱的问题

在本例中,在进入HasFlags方法之前,需要两个装箱调用。

class Program
{
    static void Main(string[] args)
    {
        var f = Fruit.Apple;
        var result = f.HasFlag(Fruit.Apple);

        Console.ReadLine();
    }
}

[Flags]
enum Fruit
{
    Apple
}

.method private hidebysig static
    void Main (
        string[] args
    ) cil managed
{
    // Method begins at RVA 0x2050
    // Code size 28 (0x1c)
    .maxstack 2
    .entrypoint
    .locals init (
        [0] valuetype ConsoleApplication1.Fruit f,
        [1] bool result
    )

    IL_0000: nop
    IL_0001: ldc.i4.0
    IL_0002: stloc.0
    IL_0003: ldloc.0
    IL_0004: box ConsoleApplication1.Fruit
    IL_0009: ldc.i4.0
    IL_000a: box ConsoleApplication1.Fruit
    IL_000f: call instance bool [mscorlib]System.Enum::HasFlag(class [mscorlib]System.Enum)
    IL_0014: stloc.1
    IL_0015: call string [mscorlib]System.Console::ReadLine()
    IL_001a: pop
    IL_001b: ret
} // end of method Program::Main
  • 第一次是将值类型上的方法调用解析为基类型方法,

    调用基类型的方法会导致装箱

  • 第二次是参数时引用类型Enum, 要对枚举值类型进行装箱.

调用非虚的,继承的方法时(比如GetType或者MemberwiseClone),无论如何都要对值类型进行装箱, 因为这些方法由System.Object定义,要求this实参是指向堆对象的指针.

“值类型都是System.ValueType的后代”,但System.ValueType的后代不全是值类型System.Enum就是唯一的特例!在System.ValueType的所有后代中,除了System.Enum之外其它都是值类型。事实上,我们可以在.NET的源代码中找到System.Enum的声明.

public abstract class Enum : ValueType, IComparable, IFormattable, IConvertible

C#枚举类型、System.Enum、System.ValueType、值类型和引用类型之间存在着什么样的关系?

  1. 所有枚举类型(enum type)都是值类型。
  2. System.EnumSystem.ValueType本身是引用类型。
  3. 枚举类型(enum type)都是隐式的直接继承自System.Enum,并且这种继承关系只能由编译器自动展开。但System.Enum本身 不是枚举类型(enum type)
  4. System.Enum是一个特例,它直接继承自System.ValueType,但本身却是一个引用类型。
// 结果是什么都没输出
static void Main()  
{  
    Type t = typeof(System.Enum);  

    if (t.IsEnum)  
        Console.WriteLine("I'm enum type.");  

    if (t.IsValueType)  
        Console.WriteLine("I'm value type.");  
}

对于第一个判断,我们很清楚System.Enum并不是枚举类型。但第二个判断呢?System.Enum明明继承自System.ValueType,却不承认是System.ValueType的后代!这是.NET上的一个特例,恰恰体现出System.Enum是特殊性。

Q:既然枚举类型是值类型,自然会涉及到装箱和拆箱(boxing and unboxing)的问题,那么枚举类型会被装箱成什么呢?

A:枚举类型可以被装箱成System.EnumSystem.ValueTypeSystem.Object或者System.IConvertibleSystem.IFormattableSystem.IComparable

关于枚举的种种 C#, IL, BCL(博客地址)

Flag特性

位标志可以用来组合. 虽然枚举类型和位标志相似,但它们语义不尽相同.

  • 枚举类型表示单个数值

  • 位标志表示位集合

    一些位处于On状态(代表1),一些处于off状态 (代表0)

  • 强烈建议向枚举类型应用定制特性类型[Flag]

public static void Main(string[] args)
{
    // 现在数值为0x0005
    Actions actions = Actions.Read | Actions.Delete;
    // 输出: Read, Delete
    Console.WriteLine(actions.ToString());
}

// 加上特性类型[Flag],ToString方法会试图转换为对应的符号,
// 但是0x0005没有对应的值,不过方法检测到[Flag]标志,不会把他视为单独的值
// 会视为一组位标志 0x0005由0x0001和0x0004组合而成,会输出Read, Delete字符串
[Flags]
internal enum Actions
{
    None      = 0b0,
    Read      = 0b1,
    Write     = 0b10,  // 0x2
    ReadWrite = Actions.Read | Actions.Write, //0x5
    Delete    = 0b100, // 0x4
    Query     = 0b1000,// 0x8
    Sync      = 0b10000//0x10
}

加上特性类型[Flag],ToString方法会试图转换为对应的符号,但是0x0005没有对应的值,不过 方法检测到[Flag]标志,不会把他视为单独的值,会视为一组位标志 0x0005由0x0001和0x0004组合而成,会输出Read, Delete字符串,去掉[flag]特性,则输出5.

枚举的ToString()允许三种方式格式化输出:

  1. “G” 常规
    1. 首先会检测类型,是否应用了[Flags]这个特性
    2. 没有应用就查找与该数值匹配的符号,返回 符号
  2. “D” 十进制
  3. “X” 十六进制
  4. “F” 获得正确的字符串

如果应用了[Flags]这个特性,ToString的工作流程如下:

  • 获取枚举类型定义的集合,降序排列这些数值
  • 每个数值都和枚举实例中的值进行按位与计算
  • 结果等于数值,与该数值关联的字符串就附加到输出字符串上. 对应的位会被关闭(设为0),认为已经考虑过了.
  • 重复上一步,直到检查完所有的值(或者所有位都为0)
  • 检查完数值后,如果枚举实例仍有不为0,表示处于On位的位不对应任何已经定义的符号.这种情况下,ToString返回枚举实例中的 原始数值作为字符串返回.
  • 如果实例不为0,就返回符号之间以逗号分隔的字符串.例如:Read, Delete
  • 如果实例原始值是0,并且有对应0值的符号. 返回这个符号
  • 到达这一步就返回”0”.

永远不要对位标志枚举使用IsDefined方法.

  • 向方法传递字符串,它不会把字符串拆分为单独的token来进行查找,而是查找整个字符串, 把它看成是一个包含逗号的更大的符号.由于不能在枚举中定义包含逗号的符号,所以永远找不到.
  • 向方法传递数值,它会检查枚举类型是否定义了其数值和传入的数值匹配,位标志不能简单的这样匹配,所以不要用这个方法.

向枚举类型添加方法

利用C#的扩展方法 模拟 向枚举类型添加方法.

class Program
{
    public static void Main(string[] args)
    {
        FileAttributes fa = FileAttributes.System;
        fa = fa.Set(FileAttributes.ReadOnly);
        fa = fa.Clear(FileAttributes.System);
        // 输出: ReadOnly
        fa.ForEach(f=>Console.WriteLine(f));

    }
}

// 先定义一个包含了扩展方法的静态类
internal static class FileAttributesExtensionMethod
{
    public static Boolean IsSet(this FileAttributes flags, FileAttributes flagToTest)
    {
        if (flagToTest == 0)
        {
            throw new ArgumentOutOfRangeException("flagToTest","值不能为0.");
        }
        return (flags & flagToTest) == flagToTest;
    }

    public static Boolean IsClear(this FileAttributes flags, FileAttributes flagToTest)
    {
        if (flagToTest == 0)
        {
            throw new ArgumentOutOfRangeException("flagToTest","值不能为0.");
        }
        return  ! IsSet(flags,flagToTest);
    }

    public static Boolean AnyFlagSet(this FileAttributes flags, FileAttributes testFlag)
    {
        return ((flags & testFlag) != 0);
    }

    public static FileAttributes Set(this FileAttributes flags, FileAttributes setFlag)
    {
        return flags | setFlag;
    }

    public static FileAttributes Clear(this FileAttributes flags, FileAttributes clearFlag)
    {
        return flags & ~clearFlag;
    }

    public static void ForEach(this FileAttributes flags, Action<FileAttributes> processFlag)
    {
        if (processFlag == null)
        {
            throw new ArgumentNullException("processFlag");
        }

        for (UInt32 bit = 1; bit != 0; bit <<= 1)
        {
            UInt32 temp = ((UInt32) flags) & bit;
            if (temp!=0)
            {
                processFlag((FileAttributes) temp);
            }
        }
    }
}
Licensed under CC BY-NC-SA 4.0
0