关于枚举的种种 (Enumeration FAQ) [C#, IL, BCL]

关于枚举的种种 [C#, IL, BCL]

Enumeration FAQ [C#, IL, BCL]

 

Updated on Tuesday, January 11, 2005

 

Written by Allen Lee

 

Q:在C#里,我们如何表达枚举类型?

A:你可以使用enum关键字(keyword)来声明一个枚举类型(enum type):

  // Code #01
  public enum Alignment
  
{
      Left,
      Center,
      Right
  }


Q:枚举类型是值类型(value type)还是引用类型(reference type)?

A:枚举类型都是值类型。


Q:System.Enum是枚举类型么?

A:不是。


Q:System.Enum与枚举类型(enum type)有什么关系?

A:System.Enum是一个抽象类(abstract class),所有枚举类型都直接继承自它,当然也同时继承了它的所有成员。


Q:那么System.Enum属于引用类型啦?

A:是的。


Q:既然System.Enum是引用类型,而枚举类型又是直接继承自System.Enum的,那为什么枚举类型却不是引用类型?

A:这种继承关系是隐式的并由编译器负责展开,上面Code #1的Alignment枚举被展开后的IL代码如下:

  // Code #02
  .class public auto ansi sealed Aligment
         extends [mscorlib]System.Enum
  
{
      .field 
public static literal Aligment Left = int32(0x00000000)
      .field 
public static literal Aligment Center = int32(0x00000001)
      .field 
public static literal Aligment Right = int32(0x00000002)

      .field 
public specialname rtspecialname int32 value__
  }

从声明中,你可以看到Aligment的确是继承自System.Enum的,只是你不能在C#里显式声明这种继承关系。


Q:但你好像没有回答为什么枚举类型继承自一个引用类型后,却还是值类型!

A:你知道,所有的值类型都是System.ValueType的后代,枚举类型也不例外,枚举类型直接继承自System.Enum,而System.Enum却又直接继承自System.ValueType的,所以,枚举类型也是System.ValueType的后代。


Q:慢着!从System.ValueType派生出来的类型不都应该是值类型吗?为什么System.Enum会是引用类型?

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

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

请注意,.NET Framework SDK v2.0.3600.0 Documentation中的Enum声明是错的:

public abstract struct Enum : IComparable, IFormattable, IConvertible


Q:开始头晕了,究竟枚举类型、System.Enum、System.ValueType、值类型和引用类型之间存在着什么样的关系?

A:简单的说,

  • 1. 所有枚举类型(enum type)都是值类型。
  • 2. System.Enum和System.ValueType本身是引用类型。
  • 3. 枚举类型(enum type)都是隐式的直接继承自System.Enum,并且这种继承关系只能由编译器自动展开。但System.Enum本身不是枚举类型(enum type)。
  • 4. System.Enum是一个特例,它直接继承自System.ValueType(参见Code #03),但本身却是一个引用类型。

好吧,现在来看看下面代码,你能猜得出它的输出结果吗?

  // Code #04
  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)的问题,那么枚举类型会被装箱成什么呢?[Updated]

A:枚举类型可以被装箱成System.Enum、System.ValueType、System.Object或者System.IConvertible、System.IFormattable、System.IComparable。

注意:在.NET 1.1上,枚举类型只能被装箱到System.Enum、System.ValueType、System.Object;而在.NET 2.0上,枚举类型还能被装箱到System.Enum所实现的三个接口:System.IConvertible、System.IComparable、System.IFormattable。对应的装箱操作既可以为隐式的也可以是显式的。

下面的C#代码:

  // Code #05
  
// See Code #01 for Alignment.
  static void Main()
  
{
      Alignment a 
= Alignment.Center;

      Console.WriteLine(a.ToString());

      Console.WriteLine(a);
  }

对应的IL代码是:

  // Code #06
  .method private hidebysig static void Main() cil managed
  
{
      
.entrypoint
      
// Code Size: 32 byte(s)
      .maxstack 1
      
.locals (
            EnumerationFaq.Alignment alignment1)
      L_0000: ldc.i4.1 
      L_0001: stloc.0 
      L_0002: ldloc.0 
      L_0003: box EnumerationFaq.Alignment
      L_0008: call instance 
string [mscorlib]System.Enum::ToString()
      L_000d: call 
void [mscorlib]System.Console::WriteLine(string)
      L_0012: nop 
      L_0013: ldloc.0 
      L_0014: box EnumerationFaq.Alignment
      L_0019: call 
void [mscorlib]System.Console::WriteLine(object)
      L_001e: nop 
      L_001f: ret 
  }

从IL代码中我们可以看到枚举类型被装箱两次。第一次(L_0003)被装箱成System.Enum,而第二次(L_0014)就被装箱成System.Object。

但如果你让编译器自动为你选择装箱类型的话,它会优先考虑System.Enum:

  // Code #07
  
// See Code #01 for Alignment.
  class Program
  
{
      
static void Main()
      
{
          Alignment a 
= Alignment.Center;

          Print(a);
      }


      
static void Print(IConvertible c)
      
{
          Console.WriteLine(c);
      }


      
static void Print(IFormattable f)
      
{
          Console.WriteLine(f);
      }


      
static void Print(IComparable c)
      
{
          Console.WriteLine(c);
      }


      
static void Print(Object o)
      
{
          Console.WriteLine(o);
      }


      
static void Print(ValueType v)
      
{
          Console.WriteLine(v);
      }


      
static void Print(Enum e)
      
{
          Console.WriteLine(e);
      }

  }

上面的代码将被编译成如下的IL:

  // Code #08
  .method private hidebysig static void Main(string[] args) cil managed
  
{
      .entrypoint
      
// Code Size: 15 byte(s)
      .maxstack 1
      .locals (
            EnumerationFaq.Alignment alignment1)
      L_0000: ldc.i4.
1 
      L_0001: stloc.
0 
      L_0002: ldloc.
0 
      L_0003: box EnumerationFaq.Alignment
      
// 调用static void Print(Enum e);
      L_0008: call void EnumerationFaq.Program::Print([mscorlib]System.Enum)
      L_000d: nop 
      L_000e: ret 
  }


Q:我留意到Code #02中的

.field public static literal Aligment Center = int32(0x00000001)

该语句明显是整数赋值,这是否说明枚举类型实质上是整数类型?

A:这说明枚举类型与整数类型的确有一定的关系。事实上,每一个枚举类型都有与之相对应的整数类型,我们称该整数类型为底层类型(underlying type),默认的情况下使用,.NET使用System.Int32。当然,你可以手动将其指定为其他的整数类型:

  // Code #09
  public enum Alignment : byte
  
{
      Left,
      Center,
      Right
  }

注意,能被指定为枚举的底层类型的只能是如下所列的整数类型:byte, sbyte, short, ushort, int, uint, long, ulong


Q:为何我们需要指定枚举类型的底层类型?

A:你完全可以让它接受默认的底层类型。请留意Code #08,你完全找不到“Center”这个字眼,然而在C#代码中,它却是存在的,为什么呢?这是因为代码在编译的时候,编译器把枚举类型转换为与之对应的底层类型的数值来处理。Code #08的L_0000实际上就是把类型为System.Int32的数值1推入堆栈,而不是把“Center”推入堆栈。事实上,底层类型说明了如何为枚举类型分配空间,不同的底层类型所占用的资源不同,大概当你在受限系统上进行开发的话,你就可能需要注意一下了。


Q:枚举成员的值是怎样规定的?

A:如果你没有手动指定成员的值的话,从上往下看,各成员的值为:0, 1, 2, ...。说罢了,就是一个非负整数等差数列,其初值为0,步长为1。例如:

  // Code #10
  public enum Alignment
  
{
      Left,    
// 0
      Center,    // 1
      Right    // 2
  }


Q:如果我有手动指定某些成员的值呢?

A:那么被赋值的成员的值就是你所指定的值。当然,无论你是否手动指定枚举成员的值,递增步长都不会变,总是为1。为了测试你是否理解,请说出下面枚举个成员的值以及你的判断理由(请用人脑而不是电脑来运行以下代码):

  // Code #11
  public enum DriveType : sbyte
  
{
      CDRom,
      Fixed 
= -2,
      Network,
      NoRootDirectory 
= -1,
      Ram,
      Removable 
= Network * NoRootDirectory,
      Unknown
  }


Q:我们如何获取枚举成员的值,无论成员是否被手动赋值?

A:你可以使用System.Enum的

public static Array GetValues(Type enumType);

该方法返回一个包含所有枚举成员的数组:

  // Code #12
  
// See Code #01 for Alignment.
  public static void Main()
  
{
      Alignment[] alignments 
= (Alignment[])Enum.GetValues(typeof(Alignment));
      Console.WriteLine(
"Wanna see the values of Alignment's menbers?");
      
foreach (Alignment a in alignments)
          Console.WriteLine(
"{0:G} = {0:D}", a);
  }


  
// Output:
  
// Wanna see the values of Alignment's menbers?
  
// Left = 0
  
// Center = 1
  
// Right = 2


Q:如果我只需要其中某些枚举成员的值呢?

A:那么你可以把枚举转换为IConvertible接口,再调用对应的方法:

  // Code #12
  
// See Code #01 for Alignment.
  public static void Main()
  
{
      IConvertible ic 
= (IConvertible)Alignment.Center;
      
int i = ic.ToInt32(null);
      Console.WriteLine(
"The value of Alignment.Center is {0}.", i);
  }


  
// Output:
  
// The value of Alignment.Center is 1.


Q:为什么需要手动指定枚举成员的值?

A:一般情况下,使用默认的赋值规则就足够了,但某些情况下,为枚举成员指定一个与实际情况(模型)相符的值可能更有意义,这要视你具体所建的模型而定。

还是让我们来一个实际的例子:

  // Code #13
  public enum CustomerKind
  
{
      Normal 
= 90,
      Vip 
= 80,
      SuperVip 
= 70,
      InActive 
= 100
  }


  
public class Customer
  
{
      
public readonly CustomerKind Kind;

      
private double m_Payment;
      
public double Payment
      
{
          
return m_Payment * (int)Kind / 100;
      }


      
// Code here
  }

我为枚举CustomerKind的每个成员都赋了一个特定的值,该值其实就是顾客购物折扣百分率。而在Customer类中,Payment属性就通过强类型转换来获取枚举成员的值(也就是购物折扣率),并用于货款计算。从这里可以看出,获取枚举成员的值还可以通过强类型转换方式。


Q:既然枚举类型可以强制转换为整数,那么整数是否也可以强制转换为枚举类型?

A:答案是肯定的。

  // Code #14
  
// See Code #01 for Alignment.
  Alignment a = (Alignment)1;

但这种机制可能使你遇到一些麻烦:

  // Code #15
  
// See Code #01 for Alignment.
  class Program
  
{
      
static void Main()
      
{
          Foo((Alignment)
12345);
      }


      
static void Foo(Alignment a)
      
{
          
// Code here
      }

  }

你无法避免有人进行这样的恶作剧!!


Q:那么是否有办法对付这些恶作剧的人?

A:Sure!我们总不能假设人人都那么守规矩,所以,我们需要System.Enum的

public static bool IsDefined(Type enumType, object value);

现在我们把Code #15的Foo方法改进一下:

  // Code #16
  
// See Code #01 for Alignment.
  static void Foo(Alignment a)
  
{
      
if (!Enum.IsDefined(typeof(Alignment), a))
          
throw new ArgumentException("DO NOT MAKE MISCHIEF!");

      
// Code here
  }

这样,恶作剧的人将会收到一个警告(异常消息)。当然,我们不排除有人是由于一时大意才造成这样的“恶作剧”,那么IsDefined方法同样可以帮助你处理好这些情况。


Q:我认为我们还可以使用条件判断语句来处理这种情况:

  // Code #17
  
// See Code #01 for Alignment.
  static void Foo(Alignment a)
  
{
      
if (a != Alignment.Left &&
          a 
!= Alignment.Center &&
          a 
!= Alignment.Right)
          
throw new ArgumentException("DO NOT MAKE MISCHIEF!");

      
// Code here
  }

或者

  // Code #18
  
// See Code #01 for Alignment.
  static void Foo(Alignment a)
  
{
      
switch(a)
      
{
          
case Alignment.Left:
              Console.WriteLine(
"Cool~");
              
break;
          
case Alignment.Center:
              Console.WriteLine(
"Well~");
              
break;
          
case Alignment.Right:
              Console.WriteLine(
"Good~");
              
break;
          
default:
              Console.WriteLine(
"DO NOT MAKE MISCHIEF!");
              
break;
      }

  }

A:你绝对可以这样做!事实上,如果你处于以下情况之一的话:

  • 1. Alignment枚举代码不会被修改
  • 2. 你不希望使用Alignment枚举新增的特性

那么我会推荐使用你的处理方式。而且,你还可以为自己的代码定义一个这样的方法:

  // Code #19
  
// See Code #01 for Alignment.
  public static bool IsAlignment(Alignment a)
  
{
      
switch(a)
      
{
          
case Alignment.Left:
              
return true;
          
case Alignment.Center:
              
return true;
          
case Alignment.Right:
              
return true;
          
default:
              
return false;
      }

  }

这个方法比起IsDefine方法高效多了。


Q:我定义了一个这样的枚举:

  // Code #20
  public enum FontStyle
  
{
      Bold,
      Italic,
      Regular,
      Strikethrough,
      Underline
  }

我用它来指定字体的风格,但我遇到了麻烦。你知道,字体可以同时拥有枚举里面所列举的一种或者多种风格,那么,我如何为字体同时指定多种风格呢?

A:这个时候你就需要位枚举(Bit Flags),把Code #20修改一下:

  // Code #21
  
// I am using the FlagsAttribute to identify a bit flags.
  [Flags]
  
public enum FontStyle
  
{
      Bold        
= 0x0001,
      Italic        
= 0x0002,
      Regular        
= 0x0004,
      Strikethrough    
= 0x0010,
      Underline        
= 0x0020
  }

现在,你可以通过按位或运算来为字体指定多种风格了:

  // Code #22
  
// See Code #21 for FontStyle.
  Font f = new Font(
        FontFamily.GenericSansSerif,
        
12.0F,
        FontStyle.Italic 
| FontStyle.Underline
        );


Q:位枚举同样存在类似于Code #15的恶作剧吧?

A:是的,例如:

  // Code #23
  
// See Code #21 for FontStyle.
  class Program
  
{
      
static void Main()
      
{
          Bar(FontStyle.Regular 
| (FontStyle)0x0400);
      }


      
static void Bar(FontStyle fs)
      
{
          
// Code here
      }

  }


Q:那么,System.Enum.IsDefine方法是否还能应对呢?

A:不能。位枚举成员并不具备排他性,多个成员可以通过按位或运算组合起来。而System.Enum.IsDefine方法只能判断枚举变量的值是否为某一已定义的枚举成员。请看如下代码:

  // Code #24
  
// See Code #21 for FontStyle.
  FontStyle fs1 = FontStyle.Bold | FontStyle.Italic | FontStyle.Underline;
  Console.WriteLine(Enum.IsDefine(
typeof(FontStyle), fs1));

  FontStyle fs2 
= FontStyle.Regular | (FontStyle)0x0400;
  Console.WriteLine(Enum.IsDefine(
typeof(FontStyle), fs2));

  
// Output:
  
// false
  
// false

我们对代码的输出毫无疑问,因为fs1和fs2都不是一个单独的枚举成员。但这不是我们所追求的答案,我们希望区别对待fs1和fs2,至少我们不希望fs2中的捣蛋家伙——(FontStyle)0x0400——在我们的程序中搞破坏!


Q:那么,我们是否有办法隔离这些捣蛋鬼呢?

A:Of course!我们同样可以使用条件判断语句来处理,但做法将与Code #17和Code #18有所不同。现在我们把Code #23改进如下:

  // Code #25
  
// See Code #21 for FontStyle.
  class Program
  
{
      
static void Main()
      
{
          Bar(FontStyle.Regular 
| (FontStyle)0x0400);
      }


      
static void Bar(FontStyle fs)
      
{
          
if ((fs & FontStyle.Bold) != 0)
          
{
              
// Do something associated with bold
          }


          
if ((fs & FontStyle.Italic) != 0)
          
{
              
// Do something associated with italic
          }


          
// Other conditional code continues here
      }

  }

我们把枚举变量与某一特定的位枚举成员进行按位与运算,若结果不为0则表明枚举变量中包含着该位枚举成员。当然,你也可以为自己的代码写一组这样的方法:

  // Code #26
  
// See Code #21 for FontStyle.
  static bool ContainsBold(FontStyle fs)
  
{
      
if ((fs & FontStyle.Bold) != 0)
          
return true;
      
else
          
return false;
  }


  
static bool ContainsItalic(FontStyle fs)
  
{
      
if ((fs & FontStyle.Italic) != 0)
          
return true;
      
else
          
return false;
  }


  
// Other similar methods continue here

又或者你可以写一个这样的方法:

  // Code #27
  
// See Code #21 for FontStyle.
  static bool ContainsMember(FontStyle fs, FontStyle menber)
  
{
      
if ((fs & member) != 0)
          
return true;
      
else
          
return false;
  }

如果你只希望判断某一个枚举变量里面是否包含捣蛋鬼,你可以写一个这样的方法:

  // Code #28
  // See Code #21 for FontStyle.
  static bool ContainsPranksters(FontStyle fs)
  
{
      
if ((fs < FontStyle.Bold) || (fs > (FontStyle)0x0037))
          
return true;

      
if (fs == (FontStyle)0x0008 ||
          fs == (FontStyle)0x0009 ||
          fs == (FontStyle)0x0018 ||
          fs == (FontStyle)0x0019 ||
          fs == (FontStyle)0x0028 ||
          fs == (FontStyle)0x0029)
          
return true;

      
return false;
  }

留个“作业”吧,知道为何这样可以判断出是否有捣蛋鬼吗?当然,如果你想到了更好的方法,记住要告诉我哟!


Q:慢着!你那个“我们把枚举变量与某一特定的位枚举成员进行按位与运算,若结果不为0则表明枚举变量中包含着该位枚举成员”,在以下的情况显然不成立的:

// Code #35
[Flags]
enum Music
{
    Jazz 
= 0x00,
    Rock 
= 0x01,
    Country 
= 0x02,
    Classic 
= 0x03
}


static void Main()
{
    Music m 
= Music.Rock | Music.Jazz;
    
int r = (int)(m & Music.Classic);
    Console.WriteLine(r);
}

该代码的输出恰恰就为0,然而m却不包含Music.Classic!

A:Good question!也正如你所看到的,这种做法其实与位枚举成员的值是如何被赋予息息相关的。那么,我们应该如何为位枚举的成员赋值呢?由于位枚举成员的值和位运算都直接与二进制相关,所以,我们不妨从二进制的角度去探索一下如何恰当的为位枚举的成员赋值。

试想一下,如果我们能把二进制值的字面特征与位枚举成员关联起来,使得我们能够直接从二进制值的字面特征判断位枚举成员的存在与否该多好呀!

考察你的Music枚举,它有4个成员,那么我们把二进制值的数位设定为4,即[D][C][B][A]型;并规定每一个数位代表该一个枚举成员,即A代表Music.Jazz、B代表Music.Rock、C代表Music.Country、D代表Music.Classic;那么,某个数位的值为1就代表其对应的枚举成员存在,为0则不存在。现在,假如Music的某个变量m的二进制值为0110,我们就可以肯定的说,m中包含着Music.Rock和Music.Country,因其B、C数位的值均为1。

那么这些跟为位枚举成员赋值有什么关系呢?从上面的讨论可以知道,Music各个成员的二进制值分别为:Music.Jazz为0001、Music.Rock为0010、Music.Country为0100、Music.Classic为1000。把这些值转换为十六进制值并赋予对应的成员:

// Code #36
[Flags]
enum Music
{
    Jazz 
= 0x01,
    Rock 
= 0x02,
    Country 
= 0x04,
    Classic 
= 0x08
}

这样,你就可以采用我所提到的方法来验证某个枚举变量中是否包含着特定的枚举成员了。


Q:如何把枚举类型转换(解析)成字符串类型?

A:最简单的方法就是使用System.Enum的

public override string ToString();

方法,或者把枚举类型转换为IConvertible接口,再调用该接口的

string ToString(IFormatProvider provider);

方法。此时你将得到枚举成员的字面值的字符串:

  // Code #29
  // See Code #01 for Alignment.
  // See Code #21 for FontStyle.
  static void Main()
  
{
      Alignment a = Alignment.Right;
      Console.WriteLine("Alignment is {0}.", a.ToString());

      FontStyle fs = FontStyle.Bold | FontStyle.Underline;
      Console.WriteLine("FontStyle is {0}.", fs.ToString());
  }


  
// Output:
  // Alignment is Right.
  // FontStyle is Bold, Underline.

如果你希望输出枚举成员的值,那么你可以手动指定格式参数:

  // Code #30
  // See Code #01 for Alignment.
  // See Code #21 for FontStyle.
  static void Main()
  
{
      Alignment a = Alignment.Right;
      
// Represents Alignment in decimal form.
      Console.WriteLine("Alignment is {0}.", a.ToString("d"));
      
// Represents Alignment in hexadecimal without a leading "0x".
      Console.WriteLine("Alignment is {0}.", a.ToString("x"));

      FontStyle fs = FontStyle.Bold | FontStyle.Underline;
      
// Represents FontStyle in decimal form.
      Console.WriteLine("FontStyle is {0}.", fs.ToString("d"));
      
// Represents FontStyle in hexadecimal without a leading "0x".
      Console.WriteLine("FontStyle is {0}.", fs.ToString("x"));
  }


  
// Output:
  // Alignment is 2.
  // Alignment is 00000002.
  // FontStyle is 33.
  // FontStyle is 00000021.

除此之外,你还可以使用System.Enum的

public static string Format(
            Type enumType,
            
object value,
            
string format
            );

方法:

  // Code #31
  // See Code #01 for Alignment.
  static void Main()
  
{
      Alignment a = Alignment.Right;
      Console.WriteLine(
          "Alignment is 0x{0}.",
        System.Enum.Format(
typeof(Alignment), a, "x")
          );
      Console.WriteLine("Alignment is 0x{0:x}.", a);
  }


  
// Output:
  // Alignment is 0x00000002.
  // Alignment is 0x00000002.

另外,你还可以通过System.Enum的

public static string[] GetNames(Type enumType);

来获取枚举所有成员的字面值:

  // Code #32
  // See Code #01 for Alignment.
  static void Main()
  
{
      
string[] names = Enum.GetNames(typeof(Alignment));
      
foreach(string name in names)
          Console.WriteLine(name);
  }


  
// Output:
  // Left
  // Center
  // Right


Q:如果我得到一个表示枚举成员的字符串,我如何将其解析为对应枚举类型呢?

A:这时你就需要System.Enum的

public static object Parse(
            Type enumType,
            
string value,
            
bool ignoreCase
            );

方法了:

  // Code #33
  // See Code #01 for Alignment.
  // See Code #21 for FontStyle.
  static void Main()
  
{
      
string name = "Right";
      Alignment a = (Alignment)Enum.Parse(
typeof(Alignment), name, false);

      Console.WriteLine(a.ToString());

      
string names = "Bold, Italic, Underline";
      FontStyle fs = (FontStyle)Enum.Parse(
typeof(FontStyle), names, false);

      Console.WriteLine(fs.ToString());
  }


  
// Output:
  // Right
  // Bold, Italic, Underline


Q:枚举类型为我们编码提供了巨大的便利,什么情况下我们不应该使用枚举呢?

A:首先你应该清楚枚举类型在编程中充当一个什么样的角色。在我看来,枚举类型表达了一种稳定的分类标准。当你查看.NET Framework BCL中的枚举类型,你会发现它们几乎没有任何改变的可能或者趋势,表现出一种稳定性。所以,当你所要表达的分类标准也同样具备这种稳定性时,你就可以考虑枚举类型了。那么什么情况下不使用枚举呢?一般说来,当分类标准不闭合时——即新的子分类随时有可能产生或者现有子分类随时有可能被替换——你就应该考虑使用其他的方式来表达了。

下面让我们来看一个薪酬自动管理系统的一部分,假设某公司现有雇员种类为:

  • 1. Programmer
  • 2. Salesman
  • 3. Manager

相关代码如下:

  // Code #34
  public enum EmployeeKind
  
{
      Programmer,
      Salesman,
      Manager
  }


  
public class Employee
  
{
      
public Employee(string name, EmployeeKind kind)
      
{
          Kind = kind;
      }


      
private string m_Name;
      
public string Name
      
{
          
get return m_Name; }
      }


      
public readonly EmployeeKind Kind;

      
public double GetPayment()
      
{
          
switch(Kind)
          
{
              
case EmployeeKind.Programmer:
                  
// Return payment
              case EmployeeKind.Salesman:
                  
// Return payment
              case EmployeeKind.Manager:
                  
// Return payment
          }

      }

  }

假如该公司正处于成长期,那么公司的组织结构发生改变是家常便饭之事。但公司每一次改变组织结构,这样的系统就要经历源代码修改、重新编译然后再部署的过程!而且,如果公司实行了新的绩效评估方案,并把薪酬计算与绩效追踪挂钩,那么GetPayment方法将进一步壮大,以至于最终其代码晦涩难懂。你当然可以把GetPayment分割为子方法,但并没有什么实质的改变。再想一想,如果将来公司打算把薪酬系统与银行挂钩,提供薪资直接银行划拨,那将又会是怎么一番局面呢?

好吧,我承认我不太愿意接手维护这样的系统,但讨论如何改进这个系统会使本文进一步壮大,以至于最终其内容晦涩难懂,不过你应该可以从我的《今天你多态了吗?》中找到一些改进灵感。

 

posted @ 2004-12-19 01:17 Allen Lee 阅读(17235) 评论(47) 编辑 收藏

 回复 引用 查看   
#1楼 2004-11-29 10:04 aierong      
very good
 回复 引用 查看   
#2楼 2004-11-29 10:38 dudu      
好文章!看了这篇文章, 对枚举有了更深的了解!
 回复 引用 查看   
#3楼 2004-11-29 10:38 montaque      
呵呵,关于枚举 ,不得不说得故事。
 回复 引用 查看   
#4楼 2004-11-29 10:42 小新0574      
Allen,我发现你越来越强了~~
 回复 引用 查看   
#5楼 2004-11-29 10:58 网际飞狐      
好文章
 回复 引用 查看   
#6楼 2004-11-29 11:02 行知      
非常好,谢谢
 回复 引用 查看   
#7楼 2004-11-29 12:13 活靶子的靶子      
Excellent~!
 回复 引用 查看   
#8楼 2004-11-29 13:48 小陆      
好东西,加入本公司的C#编程规约
 回复 引用   
#9楼 2004-11-29 15:45 Ninputer[未注册用户]
我已经做了很多试验,包括阻止继承自ValueType的类型变成sealed,以及手工书写IL产生如System.Enum这样继承自ValueType又不是值类型的类型。ILASM竟然也检查我的语法!

为了彻底破除限制,看来我只能用Emit自己生成一个类?呵呵

 回复 引用   
#10楼 2004-11-29 15:48 Ninputer[未注册用户]
我还试验过直接继承Delegate和Array,似乎都没能成功。比如尽管Array是抽象类,但若用IL写一个直接继承它的类,调用的时候会说它的基类是sealed的,所以不能实例化??
 回复 引用   
#11楼 2004-11-29 15:57 Ninputer[未注册用户]
如果你有空可以研究一下Array(不能算我出题哦,呵呵)
这个很有趣,虽然是抽象类,但整个.NET世界没有他的以IL“代码实体”形式存在的任何子类。所有的子类都只存在于JIT编译器生成的世界中。

 回复 引用 查看   
#12楼[楼主] 2004-11-29 17:45 Allen Lee      

To Ninputer:

事实上,我在写这篇文章的时候,也试着动手用IL来写一个类似于System.Enum的特殊类,可是我未能成功,ILASM在编译的时候无论如何都会自动为你添上sealed

当你反编译System.Enum的Assembly时,你可以发现它的IL声明是:

.class public abstract auto ansi serializable beforefieldinit Enum
      extends [mscorlib]System.ValueType
      implements [mscorlib]System.IComparable,
                 [mscorlib]System.IFormattable,
                 [mscorlib]System.IConvertible

我试着写一个这样的类:

.class public abstract auto ansi serializable beforefieldinit MyEnum
      extends [mscorlib]System.ValueType
      implements [mscorlib]System.IComparable,
                 [mscorlib]System.IFormattable,
                 [mscorlib]System.IConvertible

然后用ILASM编译IL代码,最后我得到一个这样的“怪物”:(使用Reflector反编译)

[Serializable]
public static class MyEnum : ValueType, IComparable, IConvertible, IFormattable

我们都很清楚MyEnum无法使用的,它甚至无法编译!之所以说它是“怪物”,是由于这个东西根本无法实现我预期的效果。声明中的static其实就是对应着abstract sealed这个组合!static类不能被继承,也不能被实例化,这种类只有在.NET 2.0或以上才有。(当然,static类自有它的用途。

我也就此问过Eric Gunnerson和Kit George(BCL Team member),他们都说要实现这种效果需要BCL Team对System.Enum进行特殊处理(我猜这种特殊处理是一种特别的编译过程),而且这是.NET的唯一特例!

 


 回复 引用   
#13楼 2004-11-30 08:37 Ninputer[未注册用户]
所以我说不要用ilasm,而是要用System.Reflection.Emit来实现。如果还是不行,我们可以用Mono的System.Reflection.Emit来实现!
呵呵,总会有办法的。

 回复 引用 查看   
#14楼 2004-12-01 14:58 明月伴我行      
Good
 回复 引用   
#15楼 2004-12-23 21:18 cboy
非常棒
 回复 引用   
#16楼 2005-02-23 13:40 lsq[未注册用户]
说明:
1.定义了一个枚举如下:
enum _jb_prog_mode {
PROGRAM_ONE = 0,
PROGRAM_TWO
};
enum _jb_prog_mode e_prog_index;

2.疑惑:
e_prog_index = (enum _jb_prog_mode)-1;
e_prog_index等于-1?为什么?

 回复 引用 查看   
#17楼[楼主] 2005-03-06 09:51 Allen Lee      
To lsp:

关于你的问题,我已在《我只负责转换![C/C++]》一文给出我的想法,希望对你有点儿帮助。

 回复 引用   
#18楼 2005-09-26 17:58 Johnsuna[未注册用户]
现在我想做一个多版本的带农历的中国万年历,月历中有星期日、星期一至六,我想使用"星期一","一"或"Monday", "Mon",或"M",但也可能使用其组合,如“星期一Mon”, 于是我定义一个公共Style枚举,里面有ChineseFullName, ChineseShortName, EnglishFullName,EnglishShortName, EnglishSigleLetter等,然后又定义一个公共的ChineseFullName枚举,里面有:星期一,星期二等等,类似的EnglishShortName:Mon,Tue等(类推)。
因为可能要进行组合,比如Style为:ChineseFullName|EnglishShortName以便得到“星期一MON”的结果,所以考虑使用枚举而未使用数组。
我的问题是:
(1)如果现在选择的是星期五,Style样式为EnglishShortName,如何取得“Fri”的字符串?如果样式Style选择的是EnglishShortName | ChineseShortName,又如何得到“Fri一”呢?
(2)如果EnglishSigleLetter枚举,则其枚举值为:M,T,W,T,F,S,S,很明显,T(Tuesday)与T(Thursday)重复,S(Saturday)与S(Sunday)重复,这在枚举中是不允许的。那么,如何才能正确实现呢?

 回复 引用 查看   
#19楼 2005-11-12 22:02 空间/IV      

// Code #19
// See Code #01 for Alignment.
public static bool IsAlignment(Alignment a)
{
  switch(a)
  {
    case Alignment.Left:
      return true;
    case Alignment.Center:
      return true;
    case Alignment.Right:
      return true;
    default:
      return false;
  }
}

可以简化为:

public static bool IsAlignment(Alignment a)
{
  switch(a)
  {
    case Alignment.Left:
    case Alignment.Center:
    case Alignment.Right:
      return true;
    default:
      return false;
  }
}


 回复 引用 查看   
#20楼 2005-11-14 06:12 晓风残月      
空间/IV :
C# 中的 switch 是不支持一个case贯穿多个case,不知道你编译过这段代码没有?
但是对于枚举确可以这样(不管枚举是否定义成位枚举):
public static bool IsAlignment(Alignment a)
{
switch(a)
{
case Alignment.Left | case Alignment.Center :
return true; //该句是必须的,每个case语句都必须要有跳转语句,我们常用break
default :
return false;
}
}

同时,switch可以接受 string 类型(c/C++只能接受整型),假如你想支持多个case贯穿,只能利用goto语句了


 回复 引用 查看   
#21楼[楼主] 2005-11-14 10:43 Allen Lee      
To 晓风残月:

C# 确实支持 case 的贯穿,条件是 case 内没有任何代码,例如:

static bool IsAlignment(string a)
{
switch(a.ToLower())
{
case "left":
case "center":
case "right":
return true;
default:
return false;
}
}

只要 case 内包含一点儿代码,break 等跳转语句就是必须加上,而且不再有隐式的 case 贯穿效果。

 回复 引用 查看   
#22楼 2006-02-04 23:48 rocman      
很感谢您的文章,解答了我很多疑问,在阅读时发现有一处不太妥当,希望以下对你有用

// Code #04
static void Main()
{
Type t = typeof(System.Enum);

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

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

这里边t.GetType()得到的是System.RuntimeType,因为t已经是System.Enum的Type了,再GetType一次得到的并不能作为我们实验对象。

// Code #04
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.");
}

 回复 引用 查看   
#23楼[楼主] 2006-02-05 19:47 Allen Lee      
To rocman:

谢谢你的细心,原文我已经修改过来了 ^-^,很奇怪文章放在这里这么久了居然没有人提出来 -_-!。

 回复 引用   
#24楼 2006-02-28 17:13 alexfffffff[未注册用户]
enum Range :long {max=3434L,min=15541L};
public static void Main(string[] args)
{
long x =(long)Range.max;
long y=(long)Range.min;
Console.WriteLine("max={0}",x);
Console.WriteLine("min={0}",y);
枚举类型中的Range.max;Range.min;已经是long型的了
为什么还要强制转化如 long y=(long)Range.min;

 回复 引用 查看   
#25楼[楼主] 2006-03-02 10:55 Allen Lee      
To alexfffffff:

“enum Range : long”中的 long 指定了枚举的底层类型(Underlying Type),即 Range 内部的 value_ 的类型。C# 允许你手动为 Range 的成员赋值,但该值必须与底层类型兼容,而这些值将会由编译器隐式转换为 Range,这一转换的实质是把对应的值赋予 Range 内部的 value_。也就是说 Range.max 并不是 long 而是 Range,这样的话,要从 Range.max 中取出内部的 value_ 的值,你就必须使用强制转换/显式转换,当然,目标类型必须是 Range 的底层类型或其兼容类型。

Hope that helps!

 回复 引用 查看   
#26楼 2006-07-15 13:09 林子      
好文!
 回复 引用   
#27楼 2006-09-02 04:51 UK[未注册用户]
枚举的成员类型都是数值型的,如果想做一个字符型的枚举有什么办法?
enum colors :string{
red='#ff0000',
...
}

 回复 引用 查看   
#28楼 2006-09-07 08:57 壮志      
好文章
 回复 引用 查看   
#29楼 2006-10-16 21:05 晓风残月      
@UK
无法定义字符型的枚举,从C语言开始枚举都是数值整形常量值。

 回复 引用 查看   
#30楼 2007-10-12 09:31 1-2-3      
太精彩了,收藏。
 回复 引用   
#31楼 2008-03-13 13:03 天秤座虫子[未注册用户]
@Johnsuna

是否考虑下,用FormatProvider来处理,而不是枚举?

 回复 引用 查看   
#32楼 2008-04-09 13:04 没有昵称      
请教个问题。

这里是这些枚举的值。
public enum DriveType : sbyte
{
CDRom, // 0
Fixed = -2,
Network,// -1
NoRootDirectory = -1,
Ram,// 0
Removable = Network * NoRootDirectory,// 1
Unknown// 2
}

我用下面的代码调用:
DriveType a;
a = (DriveType)(-1);
Console.WriteLine(a);
a = (DriveType)0;
Console.WriteLine(a);


输出结果是:
NoRootDirectory
Ram

为何 (DriveType)(-1)得到的是NoRootDirectory 而不是 Network?
为何 (DriveType)0 得到的是 Ram 而不是 CDRom?

 回复 引用 查看   
#33楼[楼主] 2008-04-10 09:43 Allen Lee      
@没有昵称

虽然NoRootDirectory和Network具有相同的值,但输出并不总是NoRootDirectory的,你把枚举里NoRootDirectory和Network的位置交换一下试试看(注意,只交换这两个词的位置,原先NoRootDirectory后面的“= -1”还是留在原处),你会发现这次输出的是Network而不是NoRootDirectory。同样的做法也适用于Ram和CDRom。

如果把枚举成员和它的值看作某种函数关系y=f(x),我们很清楚一点,对于任意给定的枚举成员,我们总可以确定它的值,但反过来就不行,因为对于任意给定的值,将存在以下三种情况:
1、能找到唯一对应的枚举成员;
2、能找到多个对应的枚举成员;
3、找不到对应的枚举成员。

而强制转换运算符(explicit)只能返回一个结果,这对于第1、3种情况都没问题,对于第2种情况就出现大问题了,因为确定枚举成员的值的规则和强制转换运算符之前出现了某种不协调。如果你足够细心的话,你会发现强制转换运算符和Enum.ToObject方法返回的结果是一样的,我猜他们使用了相同的实现。由于现在只能返回一个结果,而候选结果有多个,.NET不可能对多个候选结果进行比较,并根据自身的喜好选出优胜者,所以我认为它采用的策略是最先碰到谁就返回谁,这意味着内部有一个数据结构用来储存枚举成员,成员在里面的先后顺序将会影响到返回的结果,而这个先后顺序又受到枚举成员在代码里定义时的先后顺序的影响。至于这些顺序如何影响到最终的返回结果,我也没有找到任何线索。

 回复 引用 查看   
#34楼 2008-05-09 09:25 果果’er      
Q&A的文章不错啊。

太长了,mark,需要用到的时候再来读。

 回复 引用   
#35楼 2008-10-29 16:45 Questing[未注册用户]
非常不错的文章,受益匪浅
 回复 引用 查看   
#36楼 2009-02-03 16:06 一线风      
确实不错,好文章呀!

 回复 引用   
#37楼 2009-04-20 10:46 王者之怒[未注册用户]
很不错。
 回复 引用   
#38楼 2009-08-29 15:23 maomaocongcong[未注册用户]
相当好的文章。
通俗易懂。
一万个赞!!!!!!!!!!!!!!!!!!

 回复 引用 查看   
#39楼 2010-06-14 13:57 numega      
非常好的文章,不过有两点疑问提出:
1. System.Enum 是一个 abstract class 且继承自 System.ValueType,但是它并不是 value type,也不是 enum,更进一步的是,它也不是一个 class !?
运行 typeof(System.Enum).IsClass 的结果将是 false !
请问此点该如何解释之 ?

2. 对于具有 Bit Flags 属性的 enum,无法用 IsDefined 方法来判断枚举变量的值是否为某一已定义的枚举成员,文中提出一些变通的方案来判断,这里提出另一个有趣的方法 (从C# 4.0 in a Nutshell p.100摘录)

static bool IsFlagDefined (Enum e)
{
decimal d;
return !decimal.TryParse(e.ToString(), out d);
}

[Flags]
public enum BorderSides { Left=1, Right=2, Top=4, Bottom=8 }

static void Main()
{
for (int i = 0; i <= 16; i++)
{
BorderSides side = (BorderSides)i;
Console.WriteLine (IsFlagDefined (side) + " " + side);
}
}

这样就可以通用,做为 flagged enum 的 type safety 的检查了,其输出为:

False 0
True Left
True Right
True Left, Right
True Top
True Left, Top
True Right, Top
True Left, Right, Top
True Bottom
True Left, Bottom
True Right, Bottom
True Left, Right, Bottom
True Top, Bottom
True Left, Top, Bottom
True Right, Top, Bottom
True Left, Right, Top, Bottom
False 16

 回复 引用 查看   
#40楼[楼主] 2010-06-14 19:21 Allen Lee      
@numega
1. 老实说,我也觉得typeof(System.Enum).IsClass返回false实在很不可思议,如果你没有提出来,我大概也没有想到这点。看到你这个问题,我马上打开VS2010,创建一个C# Project运行这句代码,令我感到意外的是,我看到那句话返回true……嗯,我猜你肯定是用VS2008或者之前的版本来运行那句话,幸亏我的机器上也同时安装了VS2008,打开,运行一下,噢,果然如你所言。好,我再做个实验证实一下我的猜想,我在VS2010里另外创建3个新的C# Project,分别对应.NET 2.0、3.0和3.5,然后分别运行那句话,哈,结果出来了,它们都显示false,看来,这个问题在.NET 4.0被揪了出来,并被修正了。

2. 我一开始没看明白你贴出来的代码为何可以判断位枚举是否合法,幸亏我手头上也有C# 4.0 in a Nutshell :),我看到原文提到利用Enum.ToString方法的行为,于是去查了一下MSDN,现在明白了:
引用如果应用了 FlagsAttribute,且存在与此实例的值相等的一个或多个已命名常数的组合,则返回值是一个字符串,该字符串包含用分隔符分隔的常数名称列表。其他情况下,返回值是此实例的数值的字符串表示形式。


最后感谢你为本文增色,也让我学到了新的东西 :)

 回复 引用 查看   
#41楼 2010-06-14 20:30 numega      
感谢 Allen 如此迅速的回覆。
确实我所用的是 VS2008,没想到在 .NET 4.0 之后,typeof(System.Enum).IsClass 被修正为 true !

不过我替 .NET 3.5(含)以前的 System.Enum 为何不是一个 class 提出可能的解释(也许是错的,但是合理):

1. 首先看 Type.IsClass 的定义 (.NET 4.0 的MSDN也是同样的定义)
Type.IsClass Property
Gets a value indicating whether the Type is a class; that is, not a value type or interface.
注意最后的一句话 "not a value type or interface",也就是非 value type 或 interface 就会返回 true。

2. System.Enum 的宣告如下:
public abstract class Enum : ValueType, IComparable, IFormattable, IConvertible
因此可知 System.Enum 继承自 System.ValueType,根据Type.IsClass 的定义,所以 Type.IsClass 返回 false
也就是 All value types return false for IsClass.
到这里,就会变得很奇怪
System.Enum 不是 enum
System.Enum 不是 value type
System.Enum 也不是 class
那它究竟是什么?
它是一个 "value type class" !

3. 为何 System.Enum 要弄得什么都不是?
因为System.Enum 要成为一个抽象类别,以使得所有枚舉類型都直接繼承自它,但是它又要具备 value type 的特性。
当我们想要自定义一个 value type 的时候,一定会考虑使用 struct,但是并没有 abstract struct 这样的东西,所以就只好使用 abstract class System.Enum : System.ValueType 来完成

以上是小弟的一点浅见,有错误请指正,谢谢。

 回复 引用 查看   
#42楼[楼主] 2010-06-15 00:08 Allen Lee      
@numega
你解释的很好啊。你使用繁体中文吗?

 回复 引用 查看   
#43楼 2010-06-15 12:37 numega      
是的,人在台湾,最近在学习C#,偶然发现楼主的文章对语言的描述深入浅出甚感佩服。因为使用繁体中文,所以简体中文是转换过去的,可能会有混用的情形,请见谅。
 回复 引用 查看   
#44楼[楼主] 2010-06-15 12:46 Allen Lee      
@numega
不用转换啊,我从小就看香港的电视,所以看繁体中文是没问题的,嘿嘿~

 回复 引用 查看   
#45楼 2010-12-09 10:25 Devil_Zhang      
真是太棒了!支持一下!