常量
常量是值从不变化的符号。定义常量符号时,它的值必须能在编译时确定。确定后,编译器将常量值保存到程序集元数据中。这意味着只能定义编译器识别的基元类型的常量。在C#中,可用于定义常量:Boolean,Char,Byte,SByte,Int16,UInt16,Int32,UInt32,Int64,UInt64,Single,Double,Decimal,String。C#也允许定义非基元的常量变量,前提是把值设为null:
public class Program
{
public const Program program = null;
}
代码引用常量符号时,编译器在定义常量的程序集的元数据中查找该符号,提取常量的值,将值嵌入生成的IL代码中。由于常量的值直接嵌入生成的IL代码中,所以在运行时不需要为常量分配任何内存。除此之外,不能获取常量的地址,也不能以传引用的方式传递常量。
这也意味着没法做到很好的版本控制。例如我们首先编译一个dll,这个程序集内存在一个常量:
public sealed class SomeLibraryType
{
public const int MAX = 50;
}
接下来用以下代码生成一个应用程序程序集:
public class Program
{
static void Main(string[] args)
{
Console.WriteLine(SomeLibraryType.MAX);
}
}
编译器在生成代码时,会注意到MAX是值为50的常量符号,所以会将int值50嵌入应用程序的IL代码。事实上,在生成应用程序程序集后,运行根本不会加载DLL程序集,可以把它从磁盘上删除。
这个例子清楚的展示了版本控制问题。如果开发人员将常量MAX更改为1000,且只是重新生成dll,那么应用程序程序集不会有任何影响,依然输出50。想要输出新值,则需要重新编译。
字段
字段是一种数据类型,其中容纳了一个值类型的实例或者对一个引用类型的引用。CLR支持类型(静态)字段和实例(静态)字段。如果是类型字段,容纳字段数据所需的动态内存是在类型对象中分配的,而类型对象是在类型加载到一个AppDomain时创建的。将类型加载到一个AppDomain的时机通常是在引用了该类型的任何方法首次进行JIT编译的时候。如果是实例字段,容纳字段数据所需的动态内存是在构造类型的实例时分配的。
CLR支持readonly字段和read/write字段。大多数字段都是read/write字段,意味着在代码执行过程中,字段值可以多次改变。但readonly字段只能在构造器方法中写入(构造器方法只会在对象生成时,调用一次)。编译器和验证机制确保readonly字段不会被构造器以外的任何方法写入。不过我们仍然可以通过反射的方式修改readonly字段。
修改下之前的代码,举个例子,将SomeLibraryType类型更改如下,重新生成dll:
public sealed class SomeLibraryType
{
public static readonly int MAX = 50;
}
然后我们重新编译应用程序程序集,会输出50。我们将SomeLibraryType.MAX改为1000,然后重新编译dll,会发现能够输出1000了。使用ildasm.exe查看下IL代码,可以看到,它会去对应程序集中找MAX值:
以下表格稍微介绍一下字段修饰符
CLR术语 | C#术语 | 说明 |
---|---|---|
Static | static | 这种字段是类型状态的一部分,而不是对象状态的一部分 |
Instance | 默认 | 这种字段与类型的一个实例关联,而不是与类型本身关联 |
InitOnly | readonly | 这种字段只能由一个构造器方法中的代码写入 |
Volatile | volatile | 编译器,CLR和硬件不会对访问这种字段的代码执行“线程不安全”的优化措施。只有以下类型能标记为volatile:所有引用类型,Single,Boolean,Byte,SByte,Int16,UInt16,Int32,UInt32,Char,以及基础类型为Byte,SByte,Int16,UInt16,Int32,UInt32的所有枚举类型 |