C#构造器的那点事
1.实例构造器和类
构造器是允许将类型的实例化为良好的状态的一种特殊方法。
当创建一个类型的实例时:
1)为实例的字段分配内存。
2)初始化对象的附加字段(类型对象指针和同步块索引)。
3)调用类型的实例构造器来设置对象的初始状态。
构造引用类型对象时,在调用实例构造器之前,为对象分配的内存总是先被归零,构造器没有显式重写的所有字段保证只有一个0或null值。
和其他的方法不同,实例构造器永远不能被继承。
因为实例构造器不能被继承,类只有类自己定义的实例构造器,所以就不能用virtual,new,override,sealed,abstract修饰符来定义构造器。
如果定义的类没有显式的定义一个构造器,编译器会默认的定义一个无参的构造器。在默认构造器的实现中,它只是简单的调用了基类的无参构造器。
class Sometype
{
}
class Sometype//这两种定义是相等的。
{
public Sometype()
: base()
{
}
}
如果类的修饰符是abstract,那么编译器生成的默认构造器的可访问性就为proctected。
如果基类没有提供无参构造器,那么派生类必须显式调用一个基类构造器。
class Sometype
{
public Sometype(int x)
{
}
void DoSome()
{ }
}
class Some : Sometype
{
public Some()//编译出错
{
}
public Some():base(5)//通过
{
}
}
如果类的修饰符为static,编译器不会在类的定义中生成默认的构造器。
一个类型可以定义一个或多个实例构造器,每个构造器都必须有不同的签名。而且可以有不同的可访问性。
类的实例构造器在访问从基类继承的任何字段之前,必须首先调用基类的构造器。如果派生类的构造器没有显式调用一个基类构造器,编译器会自动生成对默认的基类构造器的调用。最终,System.Object的公共无参构造器会得到调用,这个构造器嘛都不干,直接返回。因为System.Object没有定义实例数据字段,所以它的构造器就没事干。
在极少的情况下可以在不调用基类构造器的前提下创建一个类型的实例,一个典型的例子是Object的MemberwiseClone方法。该方法的作用是分配内存,初始化对象的附加字段(你应该记得吧,类型对象指针和同步块索引)
然后将源对象的字节复制到新对象中。还有就是用运行时序列化器(runtime seializer)反序列化对象时,通常也不需要调用构造器。反序列化代码使用System.Runtime.Serialization.FormatterServices类型的GetUnintializedObject或者GetSafeuninitializedObject方法为对象分配内存,期间不会调用一个构造器。
宾果(不要在构造器中调用会影响所构造对象的任何虚方法,原因是假如这个虚方法在当前要实例化的类型的派生类型中进行了重写,就会调用重写的实现,但是在继承层次结构中,字段尚未完全初始化(派生类型的构造器还没有调用)。)
来看看下面这个例子中字段初始化是怎么发生的吧
sealed class Sometype
{
private Int32 x = 5;//sometype的构造器把5储存在字段x,接着调用基类的构造函数。C#编译器提供了一个简化的语法。允许以内联的方式初始化实例字段。但在幕后
//它会将这工语法转换成构造器中的代码来执行初始化。
}
sealed class Some
{
private Int32 x = 5;
private string s = "there";
private Int32 i;
public Some()
{
}//编译器在为这3个构造器方法生成代码时,在每个方法开始的位置,都会包含用于初始化x,s,i的代码。在这些
//代码初始化之后,编译器会插入对基类构造函数的调用,然后在插入自己的构造器代码。
//在这里,即使没有显式的初始化i,也会保证i会被初始化为0.
public Some(int x1) { x = 10; }
public Some(string s1) { s = s1; }
}
在上面的例子中,some类有3个构造器,所以编译器公生成3此初始化x,s,i的代码。如果有几个已初始化的实例字段和大量重载的构造器方法可以考虑下面的方法:
sealed class Some
{
private Int32 x ;
private string s ;
private Int32 i;
//该构造器将所有的字段都设为默认值。
//其他的所有构造器都显示调用这个构造器
public Some()
{
x = 30;
s = "there";
i = 300;
}
//该构造器将所有的字段都设为默认值,然后修改x的默认值
public Some(int x1)
: this()
{
x = x1;
}
//同上。。。
public Some(string s1)
: this()
{
s = s1;
}
}
2.实例构造器的结构(值类型)
值类型(struct)构造器的工作方式与引用类型的截然不同,CLR总是允许创建值类型的实例,并且阻止不了值类型的实例化。所以值类型不需要构造器,编译器也根本不会为值类型生成默认的构造器。
但是CLR允许为值类型定义构造器,但如果要执行定义的构造器,必须要显式的调用它们。
struct Point
{
private int x, y;
public Point(int x1, int y1)//如果显式声明了构造函数,就必须要为值类型的所有字段赋值。这是因为为了生成“可验证”的代码,在访问值类型的任何一个字段之前,要对所有的字段进行赋值。
{
x = x1;
y = y1;
}
public Point()//结构体不能包含显式的无参构造函数,这里会编译出错!
{//实际上,即使值类型提供哦你了无参构造器,许多编译器都不会去生成代码自动调用它,为了执行值类型的无参构造器,开发人员必须显式调用值类型的构造器。
x = 20; y = 30;
}//C#编译器故意不允许值类型定义无参构造器,就是为了避免开发人员对这种构造器在什么时候调用产生迷茫,由于不能定义无参构造器,所以编译器永远不会生成自动调用它的代码。
}
sealed class Rectangle
{
private Point p1, p2;//值类型的实例构造器只有在显式调用的时候才会执行,
public Rectangle()
{
p1 = new Point(100, 20);//如果没有用new操作符来调用Point的构造器,那么Point的字段x,y都将为0;这里p1的x=100,y=200.p2的都为0
}
}
当一个值类型有很多字段时,如果一个一个的赋值会不会很麻烦呢。。下面是对值类型全部字段赋值的一个替代方案:
struct Point
{
public int x, y, z, q, n, m;
public Some some;
public Point(int x1, int y1)//允许为值类型定义有参的构造器
{
//字段会将所有的字段初始化为0/null
this = new Point();
x = x1;//用x1的值覆盖x的0;
y = y1;//用y1的值覆盖y的0;
}
}
class Program
{
public const Program p=null;
static void Main()
{
Point p = new Point(200, 100);
Some som= p.some;//这里som为null
int x= p.x;//200
int y= p.y;//100
int z= p.z;//0
Console.ReadKey();
}
}
在值类型中,this代表值类型本身的一个实例,用new创建类型的一个实例可以赋给this。在new的过程中,所有字段为0/null。在引用类型中,this被认为是只读的,不能赋值。
3.类型构造器
也就是静态构造器,类构造器或者类型初始化器。类型构造器可用于接口(C#编译器不允许,CLR允许),引用类型和值类型。
实例构造器的作用是设置类型的实例初始状态。对应的,类型构造器的作用是设置类型的初始状态。类型默认没有定义类型构造器。如果自己定义,也只能定义一个。类型构造器永远没有参数!
sealed class Some
{
static Some()//可以看出,类型构造器类似于实例构造器,区别在于必须将它们标记为static。
//类型构造器没有访问你修饰符,总是私有的,但是如果显式的将类型标记为private 编译器会显示:静态构造函数不允许出现访问修饰符。
//之所以私有,是为了阻止任何用开发人员写的代码调用它,对类型构造器的调用总是由CLR负责的。
{
}
}
类型构造器的调用比较麻烦,JIT编译器在编译一个方法时,会查看代码中都引用了那些类型。任何一个定义了类型构造器的类型,JIT编译器都会检查针对当前AppDomain,是否已经执行了这个类型的构造器。如果构造器从未执行,JIT编译器会在它的本地(native)代码中添加对类型构造器的一个调用。如果类型构造器已经执行,JIT编译器就不添加对它的调用,因为JIT编译器知道类型已经初始化了。
现在,当方法被JIT编译完毕后,线程开始执行它,最终会执行到调用哪个类型构造器的代码。事实上,多个线程可能同时执行相同的方法。CLR希望确保在每个AppDomian(应用程序域)中,一个类型构造器只能执行一次,为了保证这点,在调用类型构造器时,调用线程要获取一个互斥同步锁。这样一来,在某一时间就会只用一个线程执行类型构造器中的代码了,第一个线程执行类型构造器中的代码,完事了第一个线程释放锁,当第一个线程离开构造器,正在等待的线程将被唤醒,然后发现类型构造器的代码已经被执行过,它就不会在执行这些代码,直接从构造器方法返回。
类型构造器中的代码只能访问类型的静态字段。并且它的常规用途就是初始化这些字段。和上面的实例字段一样,C#同样提供了一个简单的语法来初始化类型的静态字段。
sealed class Rectangle
{
private static int i = 200;
}
生成上诉代码时,编译器自动生成一个类型构造器,代码等于:
sealed class Rectangle
{
private static int i;
static Rectangle()
{
i = 200;
}
}
类型构造器不应调用基类型的类型构造器,因为类型不可能有静态字段是从基类型分享或继承的。
虽然说在值类型中也能定义类型构造器,但是永远都不要这么做:
struct Point
{
static Point()
{
Console.WriteLine("这句话永远都不会显示");//因为CLR不会调用值类型的静态构造器。
}
}
类型构造器的性能:
class Program
{
static void Main()
{
PerfTest1(1000000000);
PerfTest2(1000000000);
Console.ReadKey();
}
//这个方法被jit编译时,BeforeFieldInit和Precise类的类型构造器还没被执行,所以对这些构造器的调用将嵌入这个方法的代码中,使它的运行变得较慢。
private static void PerfTest1(int iterations)
{
Stopwatch sw = Stopwatch.StartNew();
for (int x = 0; x < iterations; x++)
{
//jit编译器优化调用BeforeFileldInit的类型构造器的代码,使它在循环之前执行。
BeforefieldInit.s_x = 1;
}
Console.WriteLine("PerfTest1:{0} BeforeFileldInit", sw.Elapsed);
sw = Stopwatch.StartNew();
for (int x = 0; x < iterations; x++)
{
//jit编译器在这里生成调用Precise类的类型构造器的代码,所以在每次循环迭代,都要核实一遍是否需要调用构造器。
Precise.s_x = 1;
}
Console.WriteLine("PerfTest1:{0} Precise", sw.Elapsed);
}
//这个方法被jit编译时,BeforeFieldInit和Precise类的类型构造器已经被执行,所以这个方法的代码中,不会在生成对这些构造器的调用,它运行的更快。
private static void PerfTest2(int iterations)
{
Stopwatch sw = Stopwatch.StartNew();
for (int x = 0; x < iterations; x++)
{
BeforefieldInit.s_x = 1;
}
Console.WriteLine("PerfTest2:{0} BeforeFileldInit", sw.Elapsed);
sw = Stopwatch.StartNew();
for (int x = 0; x < iterations; x++)
{
Precise.s_x = 1;
}
Console.WriteLine("PerfTest2:{0} Precise", sw.Elapsed);
}
}
//由于这个类没有显示定义类型的构造器,所以在C#
//元数据中用BeforeFieldInit(字段初始化前)来标记定义
internal sealed class BeforefieldInit
{
public static int s_x = 123;
}
//由于类中显式定义了类型的构造器,所以没有用BeforefieldInit来标记定义
internal sealed class Precise
{
public static int s_x;
static Precise()
{
s_x = 123;
}
}
C#编译器如果看到一个类(BeforeFieldInit)包含进行了内联初始化的静态字段,会在类型的类型定义表中生成一个添加了BeforeFieldInit元数据标记的记录项,
c#编译器如果看到一个类(Precise)包含显式的类型构造器,就不会添加BeforeFieldInit元数据标记了。它的基本原理是:静态字段只要在访问之前初始化就可以了,具体什么时候无所谓。而显式类型构造器可能
包含具有副作用的代码,所以需要在精确拿捏运行的时间。
从输出看,这个决定对性能影响极大。