理解静态成员(static)的本质---类型对象
我们只知道静态成员的用法,比如调用一个静态方法要用类名去点出来,比如Math.Max(),但是为什么是这样调用呢?为什么静态构造函数不能有访问修饰符?为什么静态构造函数没有参数?静态函数的执行时间又是什么时候?为什么?
在面向对象中,万物皆对象。其中就有一种对象叫作类型对象,类型对象就是我们创建一个class时所代表的对象。
class Program
{
public static string name;//静态字段
public string tag;//实例字段
static void Main(string[] args)
{
Program p = new Program();//Program是类型对象,p是这个类型对象的实例对象
}
}
在C#中,每个AppDomain都保证了一个类型对象的唯一性(原理后面再解释)。
类型对象是实例对象的模板,具体来说,就是运用new操作符实例化一个对象的时候,CLR首先会计算类型对象中定义的实例字段所需要的字节数,然后会为对象分配两个额外的字段(类型对象指针和同步块索引)所需要的字节(顺便说下,C#中每个对象都有两个额外的字段),最后返回对象的引用。其中实例对象的类型对象指针指向的就是类型对象。
既然说了所有对象都有两个额外的字段,那么类型对象显然也是有的,类型对象指向的是Type类型,即所有的类型对象都相当于是Type类型对象的实例。而Type类型的类型对象指针指向的是它本身。这也就理解了下面这个代码的意义。
Type t1 = p.GetType();
Type t2 = typeof(Program);
同时也可以利用类型对象指针去思考下虚方法的调用原理。
那么类型对象什么时候初始化呢?它又是用什么来初始化呢?
要创建一个类型的实例或者调用类型对象的成员的时候,才需要初始化类型对象。因为毕竟只有有了模块才能实例化出来,有了对象才能使用它的成员,所有的静态成员都是属于类型对象而不属于类型的实例,所以调用的时候用的是类名直接点出来。就像类型的实例方法,是用类型的实例名点出来一样。在C#中,对象的初始化几乎都是通过调用构造函数来实现的(object的MemberwiseClone,以及反序列化除外)。要初始化一个类型对象必须调用类型对象的构造函数,它就是我们所知道的静态构造函数(类型构造器)。类型的静态构造函数是用来初始化类型对象的,而实例构造函数是用来初始化类型的实例对象的。
程序运行时,我们无法创建类型对象,类型对象的创建都是由CLR来执行的,所以静态构造函数是没有访问修饰符的,即它永远是private。因此它也没有参数,即使有,也没有什么意义,因为都是由CLR执行的,所以类型构造器是没有参数且不能有访问修饰符。在程序集的清单元数据文件中,记录了若干个表,其中就有方法定义表,大致就是记录方法的名称、标志、索引等。在CLR编译一个方法的时候,它会从元数据中看下这个方法中都引用了哪些类型,然后检查当前的AppDomain中是否已经执行了这个类型的构造器,如果没有执行,那么就执行它。当然方法可能被多个线程同时访问,所以调用静态构造函数的时候,调用线程会获得一个互斥锁,其它线程等待,当调用线程执行完成以后就会,其它线程再次进入方法时发现方法已经被执行了就会直接返回。
这就是类型对象。
其实泛型类也是类型对象。而且由泛型类型创建出的类型对象也是分别独立的。
namespace Static
{
class Program
{
static void Main(string[] args)
{
GenericType<int>.name = "Close Type";
// Console.WriteLine(GenericType<>.name); //C#不支持开放类型,所以会报错
Console.WriteLine(GenericType<int>.name); //打印结果 : Close Type
Console.WriteLine(GenericType<float>.name); //打印结果 : Open Type
Console.ReadKey();
}
}
class GenericType<T> //GenericType<>类型对象
{
public static string name = "Open Type";
}
}
可以看到GenericType<int>、GenericType<float>、GenericType<>是不同的类型对象。所以现在更好理解const、readonly、static readonly之间的区别了。