7.1 实例构造器和类(引用类型)
构造器是将类型的实例化为良好状态的特殊方法。构造器方法在“方法定义元数据表”中始终叫做.cotor(constructor)的简称。
创建引用类型的实例时,首先为实力的数据字段分配内存,然后初始化对象的附加字段(类型对象指针和同步块索引),最后调用类型的实例构造器来设置对象的初始状态。
构造器引用类型的对象时,在调用类型的实例构造器之前,为对象分配的内存总是先被归零。
没有被构造器显示重写的所有字段保证都获得0或null值。
和其它方法不同,实例构造器永远不能被继承。也就是说,类只有类自己定义的实例构造器。
由于永远不能继承实例构造器,所以实例构造器不能使用以下修饰符:virtual,new,override,sealed和abstract。
默认生成无参构造器
如果类没有显式定义任何构造器,C#编译器将定义一个默认的无参构造器。在它的实现中,只是简单地调用了基类的无参构造器。
public class SomeType{} //等价于 public class SomeType { public SomeType():base(){} }
如果类的修饰符为abstract,那么编译器生成的默认构造器的可访问性就为protected。否则,构造器会被赋予public可访问性。
如果基类没有提供无参构造器,那么派生类必须显式调用一个基类构造器,否则编译器会报错。
如果类的修饰符为static(sealed和abstract,静态类在元数据中是抽象密封类),编译器根本不会在类的定义中生成默认构造器。
基类的构造器先被调用
一个类型可以定义多个实例构造器。每个构造器都必须有不同的签名,而且每个都可以有不同的可访问性。
为了使代码可验证verifiable,类的实例构造器在访问从基类继承的任何字段之前,必须先调用基类的构造器。
如果派生类的构造器没有显式调用一个基类构造器,C#编译器会自动生成对默认构造器的调用。
最终,System.Object的公共无参构造器会得到调用。该构造器什么都不做,会直接返回。
不调用实例构造器就能创建类型的实例的情况
极少数时候可以在不调用实例构造器的前提下创建类型的实例。
一个典型的例子是Object的MemeberwiseClone方法。该方法的作用是分配内存,初始化对象的附加字段(类型对象指针和同步块索引),然后将源对象的字节数据赋值到新对象中。
另外,用运行时序列化器runtime serializer,反序列化对象时,通常也不需要调用构造器。
反序列化代码使用System.Runtime.Sertialization.FormatterServices类型的GetUninitializedObject或者GetSafeUninitializedObject方法为对象分配内存,期间不会调用一个构造器。详情参见第二十四章,运行时序列化。
重要提示
不要早构造器中调用虚方法。原因是加入被实例化的类型重写了虚方法,就会执行派生类型对虚方法的实现。
但在这个时候,尚未完成对继承层次结构中的所有字段的初始化(被实例化的类型的构造器还没有运行)。
所以调用虚方法会导致无法徐预测的行为。归根到底,这是由于调用虚方法时,知道运行时之前都不会选择执行该方法的实际类型。
构造器实例化字段的IL代码与本质
C#语言用简单的语法在构造引用类型的实例时初始化类型中定义的字段。
internal sealed class SomeType { private Int32 m_x=5; }
构造SomeType的对象时,他的m_x字段被初始化为5,检查下SomeType的构造器方法的IL可知:
SomeType的构造器把值5存储到字段m_x,再调用基类的构造器。换句话说,C#编译器提供了一个简化的语法,允许以内联方式初始化实例字段。
但在幕后,他将这种转换成构造器方法中的代码来执行初始化。这同时提醒我们注意代码的膨胀效应。
内联方式初始化实例字段
如以下类定义所示:
internal sealed class SomeType { private Int32 m_x=5; private String m_s="Hi there"; private Double m_d=3.12159; private Byte m_b; //构造器 public SomeType(){} public SomeType(Int32 x){} public SomeType(String s){m_d=10;} }
编译器为这三个方法生成代码时,在每个方法的开始位置,都会包含用于初始化m_x,m_s,m_d的代码。在这些初始化代码之后,编译器会插入对基类构造器的调用。
再然后,会插入构造器自己的代码。例如对于获取一个String参数的构造器,编译器生成的到吗首先初始化m_x,m_s,m_d,再调用基类Object的构造器,在执行自己的代码(最后是用值10覆盖m_d原先的值)。
注意,即使没有代码显式初始化m_b,其也会保证被初始化为0;
提示
编译器在调用基类构造器之前使用简化语法对所有字段进行初始化,以维持原代码给人留下的“这些字段总是有一个值”的印象。但假如基类构造器调用了虚方法并回调由派生类定义的方法,就可能出问题。
在这种情况下,使用简化语法初始化的字段在调用虚方法之前就初始化好了。
其他构造器显式调用默认构造器
由于有三个构造器,所以编译器生成三次初始化m_x,m_s,m_d的代码——每个构造器一次。
如果有几个已初始化的实例字段和许多重载的构造器方法,可考虑不是在定义字段时初始化,
而是创建单个构造器来执行这些公共的初始化。然后让其他构造器都显式调用这个公共初始化构造器。
这样能减少生成的代码。下例演示了如何在C#中利用this关键字显式调用另一个构造器。
internal sealed class SomeType { //不显式初始化下面的字段 private Int32 m_x; private String m_s; private Double m_d; private Byte m_b; //该构造器构造器将所有字段都设为默认值 //其他所有构造器都显式调用该构造器 public SomeType(){ m_x=5; m_s="Hi there"; m_d=3.12159; m_b=0xff; } public SomeType(Int32 x):this() { m_x=x; } public SomeType(String s):this() { m_s=s; } public SomeType(Int32 x,String s):this() { m_x=x; m_s=s; } }