关于《警惕常量陷阱》之补充

关于《警惕常量陷阱》之补充

摘要:两年前曾写过一篇《警惕常量陷阱》(http://www.cnblogs.com/AndersLiu/archive/2008/11/23/csharp-via-il-constant-a.html),时至今日,又遇到类似问题,在此做一些补充。内容包括使用枚举时的类似问题,以及关于使用常量的一些最佳实践。

首先,我需要明确一下问题出现的场景。常量陷阱会存在于类库和应用程序需要单独编译的情况下。有些朋友并未遇到过类似问题,或者使用Visual Studio在一个解决方案下建立两个项目进行试验时,无法重现问题。这是因为Visual Studio会检查项目之间的依赖,当依赖项目修改后,会重新编译所有受影响的项目。而对于需要分别编译的项目,或是为客户提供升级版本的类库时,这些问题才会浮出水面。

在使用没有显式赋值的枚举值时,同样存在常量陷阱,因为枚举其实就是常量的一种特殊表示形式。例如,下面的类库包含了一个简单的枚举类型:

//
// Library.cs
//
namespace AndersLiu.ConstantTrap.Library
{
    public enum SampleEnum
    {
        Zero,  // 0
        One,  // 1
        Two,  // 2
    }
}

这个简单的枚举有三个成员,均没有显式赋值。但是根据C#语言规范,我们可以明确地得知,它们的值依次是0、1和2。使用下面的命令行可以将其编译为一个类库:

csc /t:library Library.cs

然后,我们让另外一个应用程序来访问这个枚举:

//
// Program.cs
//
namespace AndersLiu.ConstantTrap
{
    using System;
    using AndersLiu.ConstantTrap.Library;

    class Program
    {
        static void Main()
        {
            Console.WriteLine(SampleEnum.One);
        }
    }
}

这个程序简单得可以,只是打印出SampleEnum.One这个成员。使用下面的命令行进行编译:

csc /r:Library.dll Program.cs

运行程序,不出所料,程序应该打印出枚举成员的名字:

One

接下来,我们对类库进行一次升级。我们计划为枚举添加一个InsertANewOne成员,因为我发现这个新值和One是如此地息息相关,为了程序的美观并且提高代码的可读性,所以我把它添加在了One成员的前面:

//
// Library.cs
//
namespace AndersLiu.ConstantTrap.Library
{
    public enum SampleEnum
    {
        Zero,  // 0
        InsertANewOne,  // !!! Now it's 1
        One,  // !!! Changed to 2
        Two,  // !!! Changed to 3
    }
}

然后我重新编译了类库,替换掉了原有的版本。再次运行Program.exe,问题出现了。虽然Program.cs并没有改变,引用的依然是SampleEnum.One,但显示的结果却发生了变化:

InsertANewOne

很明显,SampleEnum中值为1的成员被InsertANewOne占据了,而One变成了2,但是客户端应用程序却没有感知到这种变化。这是因为在使用枚举值的时候,编译器会将其替换为常量。老规矩,上IL:

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       14 (0xe)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldc.i4.1
  IL_0002:  box        [Library]AndersLiu.ConstantTrap.Library.SampleEnum
  IL_0007:  call       void [mscorlib]System.Console::WriteLine(object)
  IL_000c:  nop
  IL_000d:  ret
} // end of method Program::Main

以上是Program中Main方法的IL,可以很清楚地看到,在需要使用枚举值SampleEnum.One的地方,只是使用ldc.i4.1指令加载了一个常数1,然后装箱为SampleEnum对象再进行打印的。

那么,如何避免常量陷阱和使用枚举时的相应问题呢?

1. 将所有常量定义为private,或者至少是internal;通过只读属性或者readonly字段暴露常量值。

例如:

public class Library
{
    private const int _version = 1;

    public readonly in VersionField = _version;
    public int VersionProperty { get { return _version; } }
}

除非遇到重大性能瓶颈,尽量不要将常量定义为public,而实践证明,性能瓶颈往往不会出现在这一细节上。

2. 为枚举成员进行显式赋值。

例如:

public enum SampleEnum
{
    Zero = 0,
    InsertANewOne = 3,  // The new one
    One = 1,
    Two = 2,
}

3. 对于已有的、没有显式赋值的枚举,只能在枚举定义末尾追加成员,不要插入到现有成员之前。

例如:

public enum SampleEnum
{
    Zero,
    One,
    Two,

    InsertANewOne,  // The new one
}

(完)

posted @ 2010-10-28 17:59 Anders Liu 阅读(...) 评论(...) 编辑 收藏