8.2 实例构造器和结构(值类型)
值类型struct构造器的工作方式与引用类型class的构造器截然不同。
CLR总是允许创建值类型的实例,并且没有办法阻止值类型的实例化。所以,值类型其实并不需要定义构造器。
C#编译器根本不会为值类型内联默认的无参构造器。来看下面的代码。
internal struct Point { public Int32 m_x,m_y; } internal sealed class Rectangle { public Point m_topLeft,m_bottomRight; }
为了构造一个Rectangle,必须使用new操作符,而且必须指定构造器。在各个例子中,调用的是C#编译器自动生成的默认构造器。
为Rectangle分配内存时,内存中包含Point值类型的两个实例。考虑到性能,CLR不会为包含在引用类型中的每个值类型字段都主动调用构造器。
但是,如前所述,值类型的字段会被初始化为0或null。
如何调用值类型结构的构造器
CLR确实允许为值类型定义构造器。但是必须显式调用才会执行。下面是一个例子。
internal struct Point { public Int32 m_x,m_y; public Point(Int32 x,Int y) { m_x=x; m_y=y; } } internal sealed class Rectangle { public Point m_topLeft,m_bottomRight; public Rectangle() { //在C#中,向一个值类型应用关键字new //可以调用构造器来初始化值类型的字段 m_topLeft=new Point(1,2); m_bottomRight=new Point(100,200); } }
值类型的构造器只有显式调用时才会执行。因此如果Rectangle的构造器没有使用new操作符来调用Point的构造器,从而初始化其的两个字段,那么两字段都将为0。
值类型不允许定义无参构造器
前面展示的Point值类型没有定义默认的无参构造器。现在进行如下改写。
internal struct Point { public Int32 m_x,m_y; public Point() { m_x=m_x=5; } } internal sealed class Rectangle { public Point m_topLeft,m_bottomRight; public Rectangle(){} }
现在构造新的Rectangle类时,当两个Point字段中的m_x,m_y会被初始化为多少?
为了增强应用程序的运行时性能,C#编译器不会自动生成为Rectangle的两个字段调用Point默认无参构造器的代码。
实际上,即使值类型提供了无参构造器,许多编译器也永远不会自动生成这种代码来自动调用它。
为了执行值类型的无参构造器,开发人员必须增加显式调用值类型构造器的代码。
基于前面的信息可以确定两个字段会被初始化为0。但实际上前面的代码是无法编译的,因为C#编译器故意不允许值类型定义无参构造器,目的是防止开发人员对这种构造器在什么时候调用产生疑惑。
由于不能定义无参构造器,所以编译器永远不会自动生成调用它的代码。没有无参构造器,值类型总是被初始化为0或null
提示
严格地说,只有当值类型的字段嵌套到引用类型时,才保证被初始化为0或null。基于栈的值类型字段则无此保证。
但是为了确保代码的可验证性verifiability,任何基于栈的值类型字段都必须在读取之前写入赋值。
值类型不能用内联的方式初始化字段
虽然C#不允许值类型定义无参构造器,但CLR允许。
由于C#不允许为值类型定义无参构造器所以编译以下类型时,C#会报错:结构中不能用实例字段初始值设定项。
internal struct SomeValType { //不能在值类型中内联实例字段的初始化 private Int32 m_x=5; }
有参构造器必须初始化所有字段
另外为了生成可验证代码,在访问值类型的任何字段之前,都需要对全部字段进行赋值。
所以值类型的任何构造器都必须初始化值类型的全部字段。以下类型为值类型定义了一个构造器,但没有初始化值类型的全部字段。
internal struct SomeValType { private Int32 m_x,m_y; //C#允许为值类型定义有参构造器 public SomeValType(Int32 x) { m_x=x; //注意m_y没有在这里初始化 } }
编译上述类型,C#会报错:在控制返回到调用方法之前,字段SomeValType.m_y必须完全赋值。
为了修正这个问题,需要早构造器中为y赋一个值,通常是0。下面是值类型的全部字段进行赋值的一个代替方案。
public SomeValType(Int32 x) { //看起来很奇怪,但编译没问题,会将所有字段初始化为0/null this=new SomrValType(); m_x=x; }
在值类型的构造器中,this代表值类型本身的一个实例,用new创建的值类型的一个实例可以赋给this。
在new的过程中,会将所有字段置为0,而在引用类型的构造器中,this被认为是只读的,所以不能对它进行赋值。