关于《警惕常量陷阱》之补充
关于《警惕常量陷阱》之补充
摘要:两年前曾写过一篇《警惕常量陷阱》(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 }
(完)