欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

CLR via C# 方法 实例构造器和类(引用类型)

程序员文章站 2022-05-22 20:35:34
...

//--

构造器是将类型的实例初始化为良好状态的特殊方法。构造器方法在“方法定义元数据表”中始终叫做.ctor(constructor的简称)。创建引用类型的实例时,首先为实例的数据字段分配内存,然后初始化对象的附加字段(类型对象指针和同步块索引),最后调用类型的实例构造器来设置对象的初始化状态。

//--

构造引用类型的对象时,在调用类型的实例构造器之前,为对象分配的内存总是先被归零。没有被构造器显式重写的所有字段都保证获得0或者null值。

//--

和其他方法不同,实例构造器永远不能被继承。也就是说,类只有类自己定义的实例构造器。由于永远不能继承实例构造器,所以实例构造器不能使用以下修饰符:virtual,new,override,sealed和abstract。如果类没有显式定义任何构造器,C#编译器将定义一个默认(无参)构造器。在它的实现中,只是简单地调用了基类的无参构造器。

下面这两种定义是等价的:

CLR via C# 方法 实例构造器和类(引用类型)

//--

如果类的修饰符为abstract,那么编译器生成的默认构造器的可访问性就为protected;否则,构造器会被赋予public可访问性。如果基类没有提供无参构造器,那么派生类必须显式调用一个基类构造器,否则编译器会报错。如果类的修饰符为static(sealed和abstract静态类在元数据中是抽象密封类),编译器根本不会在类的定义中生成默认构造器。

//--

一个类型可以定义多个实例构造器。每个构造器都必须有不同的签名,而且每个都可以有不同的可访问性。为了使代码“可验证”(verifiable),类的实例构造器在访问从基类继承的任何字段之前,必须先调用基类的构造器。如果派生类的构造器没有显式调用一个基类构造器,C#编译器会自动生成对默认的基类构造器的调用。最终,System.Object的公共无参构造器会得到调用。该构造器什么都不做,会直接返回。由于System.Object没有定义实例数据字段,所以它的构造器无事可做。

//--

极少数时候可以在不调用实例构造器的前提下创建类型的实例。一个典型的例子就是Object的MemberwiseClone方法。该方法的作用是分配内存,初始化对象的附加字段(类型对象指针和同步块索引),然后将源对象的字节数据复制到新对象中。另外,用运行时序列化器(runtime serializer)反序列化对象时,通常也不需要调用构造器。反序列化代码使用System.Runtime.Serialization.FormatterServices类型的GetUninitializedObject或者GetSafeUninitializedObject方法为对象分配内存,期间不会调用一个构造器。

//--

重要提示:不要在构造器中调用虚方法。原因是假如被实例化的类型重写了虚方法,就会执行派生类型对虚方法的实现,但在这个时候,尚未完成对继承层次结构的所有字段的初始化(被实例化的类型的构造器还没有运行呢)。所以,调用虚方法会导致无法预测的行为。归根到底,这是由于调用虚方法,直到运行时之前都不会选择执行该方法的实际类型。

//--

CLR via C# 方法 实例构造器和类(引用类型)

它的IL代码如下:

CLR via C# 方法 实例构造器和类(引用类型)

(可以搜索一下这个ildasm怎么用哈,还是自己写一写

可以看出,SomeType的构造器把值5存储到字段m_x,再调用基类的构造器。换句话说,C#编译器提供了一个简化的语法,允许以“内联”(其实就是嵌入)方式初始化实例字段。但在幕后,它会将这种语法转化成构造器方法中的代码来执行初始化。

//--

这同时提醒我们注意代码的膨胀效应。如下类定义:

CLR via C# 方法 实例构造器和类(引用类型)

编译器为这三个构造器方法生成代码时,在每个方法的开始位置,都会包含用于初始化m_x,m_s,m_d的代码。在这些初始化代码之后,编译器会插入对基类构造器的调用。再然后,会插入构造器自己的代码。

CLR via C# 方法 实例构造器和类(引用类型)

即使没有代码显式初始化m_b,m_b也保证会初始化为0。

//--

编译器在调用基类构造器前使用简化语法对所有字段进行初始化,以维持源代码给人留下的“这些字段总是有一个值”的印象。但假如基类构造器调用了虚方法并回调由派生类定义的方法,就可能出现问题。在这种情况下,使用简化语法初始化的字段在调用虚方法之前就初始化好了。

//--

由于有三个构造器,所以编译器生成三次初始化m_x,m_s,m_d的代码-----每个构造器一次。如果有几个已初始化的实例字段和许多重载的构造器方法,可以考虑不是在定义字段时初始化,而是创建单个构造器来执行这些公共的初始化。然后,让其他构造器都显式调用这个公共的初始化构造器。这样能减少生成的代码。

internal sealed class SomeType
{
    private int 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.14159;
        m_b = 0xff;
    }
    public SomeType(int x) : this()
    {
        m_x = x;
    }
    public SomeType(int x, string s) : this()
    {
        m_x = x;
        m_s = s;
    }
}

IL

CLR via C# 方法 实例构造器和类(引用类型)