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