枚举类型和位标志
CLR和FCL结合起来之后, 枚举类型和位标志才真正成为面向对象的类型. 提供了一些强大的功能.大部分开发人员并不熟悉.
枚举类型
枚举类型(enumerated type) 定义了一组 “符号名称/值” 配对. 例如:
// 每个符号标识一种颜色, 也可以用0标识白色
internal enum Color
{
White, // 赋值0
Red, // 赋值1
Green, // 赋值2
Blue, // 赋值3
Orange // 赋值4
}
不应该将数字硬编码到代码中,而应该使用枚举类型.
- 更容易阅读和理解代表的含义. 调试程序能向开发人员显示有意义的符号名称.
- 枚举是强类型的. 比如Color.Orange和Fruit.Orange是不同的,虽然都有一个Orange.
在.Net Framework中, 枚举类型不只是编译器所关心的符号, 还是类型系统中的一等公民, 能实现很强大的操作.(在其他环境比如非托管C++中,枚举没有这个特点的).
每个枚举都直接从System.Enum
派生 , System.Enum
从System.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
需要慎用
- 总是执行 区分大小写 的查找,没有办法让它执行不区分大小写的查找
- 执行速度慢,因为内部使用了 反射.(如果写代码来手动检查每一个可能的值,性能极有可能变得更好)
- 只有枚举类型本身在调用IsDefined在同一个 程序集 时才可以使用.(枚举类型是常量,内联到IL代码中的,版本控制问题)
- 不要对位标志枚举使用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、值类型和引用类型之间存在着什么样的关系?
- 所有
枚举类型(enum type)
都是值类型。 System.Enum
和System.ValueType
本身是引用类型。枚举类型(enum type)
都是隐式的直接继承自System.Enum
,并且这种继承关系只能由编译器自动展开。但System.Enum
本身 不是枚举类型(enum type) 。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.Enum
、System.ValueType
、System.Object
或者System.IConvertible
、System.IFormattable
、System.IComparable
。
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()允许三种方式格式化输出:
- “G” 常规
- 首先会检测类型,是否应用了[Flags]这个特性
- 没有应用就查找与该数值匹配的符号,返回 符号
- “D” 十进制
- “X” 十六进制
- “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);
}
}
}
}