常量

常量是从不变化的符号. 它的值必须能在编译时确定. 编译器将常量值保存到程序集的元数据中. 这意味着只能定义编译器识别的基元类型常量.

  • Boolean
  • Char
  • Byte
  • SByte
  • Int16
  • UInt16
  • Int32
  • UInt32
  • Int64
  • UInt64
  • Single (float)
  • Double
  • Decimal
  • String

特殊的是: C#允许定义非基元类型的常量变量,但是必须为null;

public const SomeType Empty = null;

定义常量将导致创建元数据,总是被视为静态成员,而不是实例成员.

C#不允许为常量指定static关键字,因为常量是总是隐式的static.

常量是怎么保存的?

  1. 代码引用常量符号时, 编译器在定义常量的程序集的元数据中查找符号,提取常量的值,将值嵌入生成的IL代码中.
  2. 由于常量的值直接嵌入代码,所以运行时不需要为常量分配任何内存.
  3. 不能获取常量的地址,也不能以传引用的方式传递常量
  4. 不能很好的支持跨程序集的版本控制

常量的跨程序集问题

// 程序集A 类TypeA
public const A = 50;
// 程序集B,引用了程序集A
Console.WriteLine(TypeA.A);

程序集B生成应用程序B之后, 用IL代码看出, 常量值直接嵌入IL代码.

如果程序集A,开发人员改变常量值为1000,只重新生成程序集A,应用程序B不会被影响,常量值还是50. 只有重新编译程序集B并生成才可以.

如果希望在运行时从一个程序集中提取另一个程序集的值,不应该使用常量,而应该使用readonly字段.

字段

字段是一种数据成员,其中容纳了一个值类型的实例或者对一个引用类型的引用.

volatile 翻译为可变的, 其实它是短暂存在,易变的意思,因为可能有多个线程都想对这个字段进行修改. “易变/易失” 翻译更佳.

CLR支持

  • 类型字段.(静态)
    • 容纳字段数据所需的动态内存是在类型对象中创建时分配的. 类型对象是在类型加载到AppDomain时创建的.

类型对象通常是在引用了 该类型的任何方法首次进行JIT编译 的时候,将该类型加载到AppDomain中.

  • 实例字段.(非静态)
    • 容纳字段数据所需的动态内存是在 构造类型的实例时分配 的.

字段存储在动态内存中,它们的值在运行时才能获取.(字段还解决了常量存在版本控制问题). 此外字段可以使任何数据类型,不像常量仅仅局限于编译器内置的基元类型.

CLR支持readonly字段read/write字段.

  • readonly字段
    • 只能在构造器方法中写入.(构造器方法只调用一次,即对象首次创建的时.)
    • 编译器和验证机制确保readonly字段不会被构造器以外的任何方法写入.
    • 可以利用反射来修改readonly字段(非常规手段反射,操作内存).

更改上一段代码,程序集B的代码不需要改,这样如果修改程序集A的值为1000,程序集B不需要重新编译就能获取到新值.

// 程序集A 类TypeA
// 字段和类型关联需要用static关键字
public static readonly A = 50;

当某个字段是引用类型,并且该字段被标记为readonly时,不可变的是引用,而不是字段引用的对象.


public static readonly Char[] cc = new Char[]{'a','b','c'};
...
// 合法的,可以通过编译
cc[0] = 'A';

// 非法的,无法通过编译,因为不能让cc引用别的东西
cc = new Char[]{'x','y','z'};

如何定义一个与类型本身关联的字段

// 这是一个静态的readonly字段; 在运行时对它初始化
// 它的值会被计算并存储到内存中
public static readonly Random s_random = new Random();

// 静态的可读可写字段
private static Int32 s_numberOfWrites = 0;

// 实例只读的字段
public readonly String Pathname = "Untitled";

// 实例可读可写的字段
private System.IO.FileStream m_fs;

public SomeType(String pathname)
{
    // 修改只读字段Pathname
    // 在构造器中可以这样做
    this.Pathname = pathname;
}

上述代码许多字段都是内联初始化的. C#允许使用这种内联初始化语法来初始化类的常量,可读可写字段readonly字段.

c#其实是在构造器对字段进行初始化的, 字段的内联初始化只是一种语法上的简化.(有些情况用内联语法需要考虑性能问题)

内联初始化: 代码中直接赋值来初始化,而不是将对构造器的调用写出来.