【CLR】实例化顺序
实例化顺序题
这里用下其他博友的案例《一道关于实例化顺序的C#面试题》
static class Program { static void Main() { BaseB baseb = new BaseB(); baseb.MyFun(); } } public class BaseA { public static MyTest a1 = new MyTest("a1"); public MyTest a2 = new MyTest("a2"); static BaseA() { MyTest a3 = new MyTest("a3"); } public BaseA() { MyTest a4 = new MyTest("a4"); } public virtual void MyFun() { MyTest a5 = new MyTest("a5"); } } public class BaseB : BaseA { public static MyTest b1 = new MyTest("b1"); public MyTest b2 = new MyTest("b2"); static BaseB() { MyTest b3 = new MyTest("b3"); } public BaseB() { MyTest b4 = new MyTest("b4"); } public new void MyFun() { MyTest b5 = new MyTest("b5"); } } public class MyTest { public MyTest(string info) { Console.WriteLine(info); } }
正确答案是:
b1
b3
b2
a1
a3
a2
a4
b4
b5
我的总结:
1、 这里说下b2在bi和b3之后,静态变量和构造函数先执行,然后是类的实例变量地址赋值,然后才是实例构造函数。b1先于a1是因为程序需要在元数据列表中找到对应的数据类型,首先调用的是BaseB,然后根据类型对象指针去找到对应基类。
2、 类的实例构造器在访问从基类继承的任何字段之前,必须先调用基类的构造器,所以实例构造器的调用顺序必然是先基类再子类。
引用类型实例构造器
构造器是将类型的实例赋值初始化的特殊方法。构造器方法在“方法定义元数据表”中始终叫做.ctor(constructor的简称)。创建引用类型的实例时,首先为实例的数据字段分配内存,然后初始化对象的附加字段(类型对象指针和同步块索引),最后调用类型的实力构造器来设置对象的初始状态。
构造引用类型的对象时,在调用类型的实例构造器之前,为对象分配的内存总是先被归零。没有被构造器构造器显式重写的所有字段都保证获得0或者null值。
和其他方法不同,实例构造器永远不能被继承。也就是说,类只有类自己定义的实例构造器。由于永远不能被继承实力构造器,所以实例构造器不能使用以下修饰符:virtual,new,override,sealed和abstract。如果类没有显示定义任何构造器,c#编译器将定义一个默认无参构造器。在它的实现中,只是简单地调用了基类的无参构造器。
如果类的修饰符为abstract,那么编译器生成的默认构造器的可访问性就为protected;否则,构造器会被赋予public可访问性。如果基类没有提供无参构造器,那么派生类必须显式调用一个基类构造器,否则编译器会报错。如果类的修饰符为static(sealed和abstract),编译器根本不会再类的定义中生成默认构造器。
一个类型可以定义多个实参构造器。每个构造器都必须有不同的签名,而且每个都可以有不同的可访问性。为了使代码“可验证”,类的实例构造器在访问从基类继承的任何字段之前,必须先调用基类的构造器。如果派生类的构造器没有显式调用一个基类构造器,c#编译器会自动生成对默认的基类构造器的调用。最终,system.object的公共无参构造器会得到调用。该构造器什么都不做,会直接返回。
极少数时候可以在不调用实例构造器的前提下创建类型的实例。一个典型的例子是object的memberwiseClone方法。该方法的作用是分配内存,初始化对象的附加字段(类型对象指针和同步块索引),然后将源对象的字节数据复制到新对象中。另外,用运行时序列化器反序列化对象时,通常也不需要调用构造器。反序列化代码使用System.Runtime.Serialization.FormatterServices类型的GetUninitializeObject或者GetSafeUninitializeObject方法为对象分配内存,期间不会调用一个构造器。
重要提示:不要在构造器中调用虚方法。原因是假如被实例化的类型重写了虚方法,就会执行派生类型对虚方法的实现。但在这个时候,尚未完成对继承层次结构中所有字段的初始化(被实例化的类型的构造器还没有运行),所以,调用虚方法会导致无法预测的行为。归根结底,这是由于调用虚方法时,直到运行时之前都不会选择执行该方法的实际类型。
编译器为这三个构造器方法生成代码时,在每个方法的开始位置,都会包含用于初始化m_x,m_x和m_d的代码。这些代码初始化后,编译器会插入对基类构造器的调用,再然后会插入构造器自己的代码,最后m_d=10会覆盖掉之前内联初始化的值。
同一个类型中无参构造器可以被继承,可以把公共部分的构造器写成基方法。
值类型实例构造器
值类型(struct)构造器的工作方式与引用类型(classs)的构造器截然不同。clr总是允许创建值类型的实例,并且没有办法阻止值类型的实例化。所以,值类型其实并不需要定义构造器,c#编译器根本不会为值类型内联(嵌入)嵌入默认的无参构造器。(如果为值类型创建无参构造函数,编译器会报错)
类型构造器(静态构造器)
除了实例构造器,clr还还和支持类型构造器(type constructor),也称为静态构造器、类构造器或者类型初始化器。类型构造器可应用于接口(虽然C#编译器不允许)、引用类型和值类型。实例构造器的作用是设置类型的实例的初始状态。对应地,类型构造器的作用是设置类型的初始状态。类型默认没有定义类型构造器。如果定义,也只能定义一个。此外,类型构造器永远没有参数。以下代码演示了如何在c#中为引用类型和值类型定义一个类型构造器。
(虽然能在值类型中定义类型构造器,但永远不都要真的那么做,因为clr有时不会调用值类型的静态类型构造器。)
定义类型构造器类似于定义无参实例构造器,区别在于必须标记为为static。此外,类型构造器总是私有:c#自动把它们标记为private。(而且你无法修改)之所以必须私有,是为了防止任何由开发人员写的代码调用它,对它的调用总是有clr负责。
类型构造器的调用比较麻烦。jit编译器在编译一个方法时,会查看代码中都引用了哪些类型。任何一个类型定义了类型构造器,jit编译器都会检查针对当前AppDomain,是否已经执行了这个类型构造器。如果构造器从未执行,jit编译器会在它生成的本机代码中添加对类型构造器的调用。如果类型构造器已经执行,jit编译器就不添加对它的调用。
现在,当方法被jit编译完毕之后,线程开始执行它,最终会执行到调用类型构造器的代码。事实上,多个线程可能同时执行相同的方法。clr希望确保每个appdomain中,一个类型构造器只执行一次。为了确保这一点,在调用类型构造器时,调用线程要获取一个互斥线程同步锁。这样一来,如果多个线程视图同时调用某个类型的静态构造器,只有一个线程可以获得锁,其他线程线程会被阻塞,而且已经执行过,就不会再次被执行。
由于clr保证一个类型构造器在每个appdomain中只执行一次,而且这种执行是线程安全的,所以非常适合在类型构造器中初始化类型需要的任何单实例(singleton)对象
那个线程中的两个类型构造器包含相互引用的代码可能出现问题。假定classA的类型构造器包含了引用classB的代码,classB的类型构造器包含了引用ClassA的代码。在这种情况下,clr仍然保证每个类型构造器的代码只被执行一次,但是,完全有可能classA的类型构造器还没执行完毕的前提下,就开始执行classB的类型构造器。因此,应尽量避免写造成这种情况的代码。事实上,由于clr负责类型构造器的调用,所以任何代码都不应该要求以特定的顺序调用类型构造器。
最后,如果类型构造器抛出未处理的异常,clr会认为类型不可用。视图访问该类型的任何字段或方法都会抛出异常。
类型构造器中的代码只能访问类型的静态字段,并且它的常规用途就是初始化这些字段。和实例字段一样,c#提供了一个简单的语法来初始化类型的静态字段
internal sealed class SomeType{ private static Int32 s_x=5; }