类的实例构造器
构造器是将类型的实例化为良好状态的特殊方法。
创建引用类型的实例
首先会为实例的数据字段分配内存。
然后初始化对象的附加字段(类型对象指针和同步块索引)。
最后调用类型的实例构造器来设置对象的初始状态。
构造器初始化字段为0或null
构造引用类型的对象时,在调用类型的实例构造器之前,为对象分配的内存总是先被归零。
没有被构造器显式重写的所有字段保证都获得0或null值。
实例构造器永远不能被继承
类只有类自己定义的实例构造器,实例构造器不能使用以下修饰符:virtual,new,override,sealed和abstract。
默认生成无参构造器
如果类没有显式定义任何构造器,C#编译器将定义一个默认的无参构造器。在它的实现中,只是简单地调用了基类的无参构造器。
如果基类没有提供无参构造器,那么派生类必须显式调用一个基类构造器,否则编译器会报错。
抽象类的默认构造器可访问性为protected
如果类的修饰符为abstract,那么编译器生成的默认构造器的。否则,构造器会被赋予public可访问性。
静态类不会生成默认构造器
如果类的修饰符为static(sealed和abstract,静态类在元数据中是抽象密封类),编译器根本不会在类的定义中生成默认构造器。
基类的构造器先被调用
一个类型可以定义多个实例构造器。每个构造器都必须有不同的签名,而且每个都可以有不同的可访问性。
为了使代码可验证,类的实例构造器在访问从基类继承的任何字段之前,必须先调用基类的构造器。
如果派生类的构造器没有显式调用一个基类构造器,C#编译器会自动生成对默认构造器的调用。
最终,System.Object的公共无参构造器会得到调用。该构造器什么都不做,会直接返回。
不调用实例构造器就能创建类型的实例的情况
极少数时候可以在不调用实例构造器的前提下创建类型的实例。一个典型的例子是Object的MemeberwiseClone方法。
该方法的作用是分配内存,初始化对象的附加字段(类型对象指针和同步块索引),然后将源对象的字节数据赋值到新对象中。
另外,用运行时序列化器runtime serializer,反序列化对象时,通常也不需要调用构造器。
不要在构造器中调用虚方法
原因是加入被实例化的类型重写了虚方法,就会执行派生类型对虚方法的实现。
此时尚未完成对继承层次结构中的所有字段的初始化,被实例化的类型的构造器还没有运行,调用虚方法会导致无法徐预测的行为。
构造器实例化字段的IL代码与本质
C#语言用简单的语法在构造引用类型的实例时初始化类型中定义的字段。
internal sealed class SomeType{ private int m_x = 5; }
构造SomeType对象时m_x字段被初始化为5,查看IL代码可知SomeType的构造器把值5存储到字段m_x,再调用基类的构造器。
即C#编译器提供了一个简化的语法,允许以内联方式初始化实例字段,但在幕后,他将这种转换成构造器方法中的代码来执行初始化。
内联方式初始化实例字段
如以下类定义所示:
internal sealed class SomeType{ private int i=5; private string s="Hi there"; private double d=3.12159; private byte b; //构造器 public SomeType(){} public SomeType(int x){} public SomeType(string str){d=10;} }
编译器为这三个构造器方法生成代码时,在每个构造器方法的开始位置,都会包含用于初始化几个字段的代码。
在这些初始化代码之后,编译器会插入对基类构造器的调用,然后插入构造器自己的代码。
例如对于获取一个String参数的构造器,编译器生成的代码首先初始化这些字段,
再调用基类Object的构造器,再执行自己的代码,最后是用值10覆盖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; } }
结构的实例构造器
值类型-结构的构造器的工作方式与引用类型-类的构造器截然不同。
值类型没有默认构造器
CLR总是允许创建值类型的实例,并且没有办法阻止值类型的实例化。所以,值类型其实并不需要定义构造器。
C#编译器根本不会为值类型内联默认的无参构造器。
internal struct Point{ public int x, y; } internal sealed class Rectangle{ public Point topLeft, bottomRight; }
为了构造一个Rectangle,必须使用new操作符,而且必须指定构造器。
假设调用的是C#编译器自动生成的默认构造器,为Rectangle分配内存时,内存中包含Point值类型的两个实例。
考虑到性能,CLR不会为包含在引用类型中的每个值类型字段都主动调用构造器,不过值类型的字段会被初始化为0或null。
如何调用结构的构造器
CLR确实允许为值类型定义构造器,值类型的构造器只有显式调用时才会执行。
因此如果Rectangle的构造器没有使用new操作符来调用Point的构造器,从而初始化其的两个字段,那么两字段都将为0。
internal struct Point{ public Int32 x, y; public Point(int x, int y){ this.x = x; this.y = y; } } internal sealed class Rectangle{ public Point topLeft, bottomRight; public Rectangle(){ //在C#中,向一个值类型应用关键字new //可以调用构造器来初始化值类型的字段 topLeft = new Point(1, 2); bottomRight = new Point(100, 200); } }
值类型不允许定义无参构造器
前面展示的Point值类型没有定义默认的无参构造器。现在进行如下改写。
internal struct Point{ public Int32 x, y; public Point(){ x = y = 5; } } internal sealed class Rectangle{ public Point topLeft, bottomRight; public Rectangle(){} }
前面的代码是无法编译的,因为C#编译器故意不允许值类型定义无参构造器,目的是防止开发人员对这种构造器在什么时候调用产生疑惑。
由于不能定义无参构造器,所以编译器永远不会自动生成调用它的代码。没有无参构造器,值类型总是被初始化为0或null。
严格地说,只有当值类型的字段嵌套到引用类型时,才保证被初始化为0或null。基于栈的值类型字段则无此保证。
但是为了确保代码的可验证性,任何基于栈的值类型字段都必须在读取之前写入赋值。
值类型不能用内联的方式初始化字段
C#不允许为值类型定义无参构造器所以编译以下类型时,C#会报错结构中不能用实例字段初始值设定项。
internal struct SomeValType{ //不能在值类型中内联实例字段的初始化 private int x=5; }
有参构造器必须初始化所有字段
为了生成可验证代码,在访问值类型的任何字段之前,都需要对全部字段进行赋值。
所以值类型的任何构造器都必须初始化值类型的全部字段。
以下类型为值类型定义了一个构造器,但没有初始化值类型的全部字段。
internal struct SomeValType{ private int x, y; //C#允许为值类型定义有参构造器 public SomeValType(int x){ this.x = x; //注意m_y没有在这里初始化 } }
编译上述类型,C#会报错,需要在构造器中为y赋一个值,通常是0。下面是值类型的全部字段进行赋值的一个代替方案。
public SomeValType(Int32 x){ //看起来很奇怪,但编译没问题,会将所有字段初始化为0/null this = new SomrValType(); this.x = x; }
在值类型的构造器中,this代表值类型本身的一个实例,用new创建的值类型的一个实例可以赋给this。
在new的过程中,会将所有字段置为0,而在引用类型的构造器中,this被认为是只读的,所以不能对它进行赋值。