.NET 中的方法----构造
.NET中的方法----构造
1、引用类型中的实例构造器
构造器是允许将类型的实例初始化为良好状态的一种特殊方法。构造器方法在"方法定义元数据表"中始终为.ctor。
创建一个引用类型的实例时,首先为实例的数据字段分配内存,然后初始化对象的附加字段(类型对象指针和同步块索引),最后调用类型的实例构造器来设置对象的初始状态。
构造引用类型的对象时,在调用类型的实例构造器之前,为对象分配的内存总是先被归零。构造器中没有显示重写的所有字段都保证有一个0或null值。
实例构造器不能被继承。如果没有显示定义任何构造器,C#编译器将定义一个默认无参构造器(其实现中,只是调用了基类的无参构造器),如果是抽象类的话,那么默认无参构造器的访问类型为protected,否则为public。如果是静态类,编译器不会创建默认构造器。
构造器可以有多个,但必须有不同的签名,每个构造器都可以有不同的可访问性。为了使代码"可验证",实例构造器在访问从基类继承的任何字段之前,必须先调用基类的构造器,如果没有显示的调用一个基类构造器,C#编译器会自动生成对默认的基类构造器的调用(如果基类没有默认构造器,则编译失败)。
在极少数情况下,可以在不调用实例构造器的前提下创建一个类型的实例。比如Object的MemberwiseClone方法,该方法的作用是分配内存,初始化对象的附加字段(类型对象指针和同步块索引),然后将源对象的字节数据复制到新对象中。另外,用运行时序列化器反序列化对象时,通常也不需要调用构造器,反序列化代码使用System.Runtime.Serialization.FormatterServices类型的GetUninitializedObject或者GetSafeUninitializedObject方法为对象分配内存,期间不会调用构造器。
重要:不要在构造器中调用任何虚方法,因为假设这个虚方法在派生类中进行重写,就会调用重写的实现,但是在继承层次结构中,派生类型的构造器还未被调用,所以,在构造器中调用虚方法会导致无法预测的行为。当使用声明时初始化方式时(private object obj = new object();),编译器会在调用基类的构造器之前初始化该字段。
2、值类型中的实例构造器
值类型(struct)构造器的工作方式与引用类型的构造器截然不同。CLR总是允许创建值类型的实例,并且没有办法阻止值类型的初始化。所以,值类型其实不需要定义构造器,C#编译器根本不会为值类型生成默认的无参构造函数。值类型的字段会被初始化为0或null。
CLR允许为值类型定义构造器,但是执行构造器的唯一方式是通过代码显示的调用。
严格的说,只有当值类型的字段嵌套到引用类型中时,才保证会被初始化为0或null。但是,基于栈的值类型字段不保证为0或null。为了确保代码的"可验证性",任何基于栈的值类型字段都必须在读取之前写入(赋值)。对于所有基于栈的值类型中的字段,C#编译器可以保证对它们进行"置零",或至少保证在读取之前赋值,从而确保不会再运行时因为验证失败而抛出异常。
重要:C#编译器不允许值类型定义无参构造器(也不允许字段声明时初始化操作,但允许静态字段声明时初始化操作),旨在避免开发人员对这种构造器在什么时候调用产生迷茫。但是CLR允许值类型定义无参构造器。为了生成"可验证"代码,在访问值类型的任何一个字段之前,都需要对全部字段进行赋值(也就是说,如果存在值类型的构造器,那么在构造器中必须对所有字段进行初始化操作)。
3、静态构造器
静态构造器可以用于接口(C#编译器不允许)、引用类型和值类型。其作用是设置类型的初始状态。静态构造器方法在"方法定义元素表"中始终为.cctor。
虽然能在值类型中定义静态构造器,但永远都不要这样做,因为CLR有时不会调用值类型的静态构造器。
类型不会默认定义静态构造器,如果需要定义,也只能定义一个无参静态构造器。静态构造器总是private的(不允许定义为其它可访问性),因为静态构造器总是由CLR负责调用,private可以避免开发人员调用它。
静态构造器的调用比较麻烦,JIT编译器在编译一个方法时,会查看代码中都引用了哪些类型,任何一个类型定义了静态构造器,JIT都会检查针对当前AppDomain是否已经执行了这个静态构造器,如果未执行,JIT会在它生成的本地代码中添加对静态构造器的一个调用。当JIT编译完成后,线程开始执行它,最终会执行到调用静态构造器的代码。事实上,多个线程可能同时执行相同的方法,CLR希望确保在每个AppDomain中,一个静态构造器只执行一次,为了保证这一点,在调用静态构造器时,调用线程要获取一个互斥线程同步锁。由于CLR保证一个静态构造器在每个AppDomain中只执行一次,而且是线程安全的,所以非常适合在静态构造器中初始化singleton对象。
在单个线程中,如果两个静态构造器包含了相互引用的代码,就会出现潜在的问题。在这种情况下,CLR要保证每个类型的静态构造器代码只被执行一次,但是那个静态构造器先被执行是由CLR控制的。所以任何代码都应要求以特定的顺序调用静态构造器。
如果在静态构造器中抛出一个未处理的异常,CLR会认为该类型不可用。视图访问该类型的任何字段和方法,都将导致抛出一个System.TypeInitializationException异常。
CLR不提供类似于静态的Finalize方法,如果需要在类型被卸载时做一些操作,可以使用System.AppDomain类型中的DomainUnload事件(因为只有当AppDomain被卸载时,类型才会被卸载)。
当编译一个方法时,JIT要决定是否在方法中生成一个对静态构造器的调用,如果JIT决定生成这个调用,它还必须决定将这个调用添加到什么位置,具体在什么位置,有下列两种情况:
- JIT可以刚好在创建类型的第一个实例之前,或者刚好在访问类的一个非继承成员之前生成这个调用。这称为"精确"语义,因为CLR调用静态构造器的时机拿捏的恰到好处。
- JIT可以在首次访问一个静态字段或者一个静态/实例方法之前,或者在调用一个实例构造器之前,随便找一个时间生成调用。这称为"字段初始化前"语义,因为CLR只保证访问成员之间会运行静态构造器,可能提前很早就运行了。
"字段初始化前"语义是首选的,因为它使CLR能够自由的选择调用静态构造器的时机,而且CLR会尽可能的利用这一点来生成运行更快的代码。
重要:当代码定义了静态构造器时,可能会引起性能问题。例如:在静态构造器还未执行的情况,在一个大循环中访问该类型的成员或方法时,JIT会在每次循环中都要检查一次是否需要调用构造器,所以会导致性能问题。