从
C#规范,10.4章 - 常量:
常量是表示常量值的类成员:可以在编译时计算的值。
这意味着您只能使用完全由文字量组成的表达式。任何调用任何方法、构造函数(无法表示为纯IL字面值的)都不能使用,因为编译器没有执行它的执行方式,从而计算结果,就没有办法在编译时进行计算。另外,由于没有办法将方法标记为不变的(即输入和输出之间存在一对一映射),编译器唯一的做法就是分析IL,看是否依赖于除了输入参数之外的其他内容,特殊处理某些类型(如IntPtr),或禁止对任何代码的每个调用。
例如,IntPtr虽然是值类型,但仍然是结构,而不是内置文字。因此,任何使用IntPtr的表达式都需要调用IntPtr结构中的代码,而这就是常量声明中不合法的内容。
我能想到的唯一的合法常量值类型示例是仅通过声明以零初始化的类型,这几乎没有用处。
至于编译器如何处理/使用常量,它将在代码中使用计算的值代替常量名称。
因此,您会得到以下效果:
- 在此位置编译的代码中没有对原始常量名称、声明它的类或命名空间的引用
- 如果反汇编代码,它将具有魔术数字,只是因为原始对常量的“引用”,如上所述,不存在,只有常量的值
- 编译器可以使用这个来优化,甚至删除不必要的代码。例如,
if (SomeClass.Version == 1)
,当SomeClass.Version的值为1时,实际上将删除if语句,并保留正被执行的代码块。如果常量的值不是1,则整个if语句及其块将被删除。
- 由于常量的值编译到代码中而不是引用常量,因此从其他程序集使用常量不会自动更新已编译的代码,如果常量的值发生更改(它不应该!)
换句话说,具有以下情况:
- 程序集A 包含名称为“版本”的常量,其值为1
- 程序集B 包含一个表达式,该表达式分析程序集A的版本号并与1进行比较,以确保它可以使用该程序集
- 有人修改程序集A,将常量的值增加到2,并重新构建A(但不包括B)
在这种情况下,程序集B 在其编译形式中仍将比较1的值与1,因为当B被编译时,常量的值为1。
事实上,如果这是从程序集A 到程序集B 中任何内容的唯一用法,则程序集B 将被编译而无需依赖于程序集A。执行包含该表达式的代码的程序集B 不会加载程序集A。
因此,常量应仅用于永远不会更改的东西。如果它是可能或将来会更改的值,而您无法保证所有其他程序集同时重构,则只读字段比常量更合适。
public class Test
{
public const decimal Value = 10.123M;
}
我们来看看使用ildasm工具查看这个类真正的样子:
.field public static initonly valuetype [mscorlib]System.Decimal X
.custom instance void [mscorlib]System.Runtime.CompilerServices.DecimalConstantAttribute::.ctor(int8, uint8, uint32, uint32, uint32) = ( 01 00 01 00 00 00 00 00 00 00 00 00 64 00 00 00 00 00 )
让我为您详细解释一下:
.field public static initonly
对应于:
public static readonly
没错,const decimal
实际上是一个 readonly decimal
。
真正的问题在于编译器会使用 DecimalConstantAttribute
来实现它的魔法。
我只知道这是 C# 编译器中唯一的这样的魔法,但我认为值得一提。