技术开发 频道

从微软CLR视角看.net用户自定义类型


“this”指针

    值类型实例方法里可用的“this”指针指向类型临时储存的第一个实例群组的地址。因此在实例方法中,访问一个实例群组,而且是从被“this”指针+由CLR分配的群组的偏移指向的地址直接访问。在下载类型过程中,偏移由CLR决定,而且在AppDomain生存期内,它将用于类型的整个实例。

    引用类型的实例方法中,可用的“this”指针指向这个类型临时储存的方法表信息块的地址。因此如果一个实例地址在实例方法内被访问,它将被由“this”+4字节+由CLR分配的群组的偏移地址访问。加一个4数值会把指针带到实例的第一个实例。在下载类型的过程中,这个群组的偏移由CLR决定,而且在应用程序域生存期内,它将用于类型的整个实例。

封箱和非封箱

    迭式存储器上,没有任何方法表的一个值类型实例被称为值类型 实例的取消装箱值。如果这个数值必须要分配到一个系统.目标变量上,我们就需要存储器上的值类型实例,存储器上的值类型实例在存储配置上有方法表。这是必须的,因为系统.目标是一个带有虚拟方法((相等功能, GetHashCode and 字符串)的引用类别。因此这些方法的任何调用要求有效的非零对象引用。但是我们可以说程序使用了值类型的群组数据来连接GetHashCode方法,并创造了一个属于它的特定的GetHashCode方法。

    现在,在GetHashCode覆盖方法内,如果使用了任何类型的群组,就可以通过使用”this”指针的起始地址来访问他们。但是如果我们封装他们,为值类型 实例创建一个对象引用,并使用它调用GetHashCode,当这个指针进入覆盖方法时,就会通过对象引用的起始地址(方法表的起始地址)。因为这个方法期待它的群组数值而不是方法表,所以在方法执行过程中会产生一些问题。为了避免产生问题,当封装值类型 实例时,CLR一定要保证被系统.对象变量调用的虚拟方法在这个类型实例中含有第一个群组项目的起始地址。也就是说CLR处理传输建立在类型实例基础上的正确的起始地址,对象引用和托管指针都可以,这个地址既可以是直接的值类型实例,封装的值类型 实例 或者直接的引用类型实例.

    装箱要求创建目标引用,这个目标引用指向建立在存储单元基础上的器堆,创建这个目标引用来保持值类型数据(群组);封装也要求从值类型 实例复制数据到类型实例的群组部分。取消装箱要求创建一个基于迭式存储器的类型实例,从目标引用复制群组数据到基于迭式存储器的类型实例。当系统.目标变量被安排到值类型 实例时,就取消装箱了。

非虚拟实例方法分派(方法调用)

    分配在值类型和引用类型的非虚拟实例方法是一样的。使用调用IL指令就可以完成非虚拟实例方法分派,但是要求类型实例指针,”this”指针作为第一个方法变元 在迭式存储器上可用,比其他方法变元都要快。在编译方法调用站点时,JIT将把群组方法储存在的方法主体(代码)转成机器代码。这个方法地址是从类型方法表结构中得到的。

    正如在存储配置里讨论的一样,在类型实例的开端,目标引用指向4字节地址,4字节地址包括类型方法表的地址。除了其他信息,方法表还包括以下信息。

• 应用程序域接口 偏移表的指针包括开始槽的地址,在那里,由类型分配的接口IIDs按序列的形式存在,而且在序列中也是由类型分配的。在基于接口的分派中就要使用这个。下一章节将讨论细节问题。
• 由类型分配的接口数量

• 每一个槽包括方法代码地址的方法表。被访问的地址内容包括一个标记(证明代码的类型),一个MSIL 或 JITTED 代码。在MSIL 事例中,在标记之后一个被访问的地址包括MSIL 代码。在JITTED 事例中,标记之后一个被访问的地址包括一个对内存的JMP语句,内存中存在JITTED代码。 The Method table is arranged in the order of inherited virtual methods, implemented virtual methods, 实例 methods and static methods. CLR determines the 偏移 of a method within this Method table based on its token, which is computed by the compiler during compilation of the assembly containing the type.按照继承虚拟方法分配方法表,执行虚拟方法,实例方法,静态方法。CLR 决定这个方法表中的方法偏移 ,而此方法表是建立在它自己的标记基础上的,在装配(包含类型)的编译中,由编译器计算。Call site (method call instruction in the code using the type) contains reference to the method token. Since CLR knows the method slot 偏移 based on its token, during JITTING phase all call sites are properly patched with method slot address based on the method token and the type of the variable (实例 method calls) or the type pointed to by the 目标引用 (virtual method calls)

• 调用站点(使用这一类型的代码中的方法调用指令)包括方法标记的一些内容。因为CLR知道方法槽 偏移建立在它自己的标记之上,在JITTING过程中,所有调用站点都正确地装饰有方法槽地址(建立在方法标记之上)和变量类型(实例方法调用)或者由目标引用(虚拟方法调用)指向的类型。

• Static fields. For primitive types each slot contains the value itself and for UDT the slot contains the address of the slot in the AppDomain wide Handle table. As described in the Variables and GC Roots section, each slot in the Handle table contains either Pinned 目标引用, 托管指针 or 目标引用

• Method slots for each interface implemented by the type

• 静态实例 对于原始类型,每一个槽都包含它自己的数值,而对于UDT每一个槽都包含由应用程序域句柄表里槽的地址。正如变量和垃圾回收根章节中描述的,句柄表中每一个槽都包含有连接的目标引用, 托管指针或者目标引用 。

    在创建非虚拟实例方法调用时,CLR 不检查实例指针的有效性。因此在现实中,可以使用类型变量调用实例方法,此类型变量还没有被示例化,或者此类型变量没有零目标引用 (你可以通过修改配置的IL来实践)。但是记住如果被调用调用的方法使用了零目标引用,包括到类型群组的途径或者调用其它方法(访问类型群组)的调用,在这些方法里执行这些调用的时候, CLR会引起系统.零引用异常。这是因为在访问实例群组时,CLR检查了目标引用的有效性。这一实例将再次发生,这是因为在存储单元(被分配给类型实例)里存在这些群组,对于零实例,则没有分配存储单元。

    在零目标引用上允许存在方法调用是很危险的,且不可预知的。正因为这个原因,许多.NET编译者(例如:C#)将发送callvirt IL调用,即使在调用非虚拟群组方法的时候也会这样做。Callvirt IL调用将指示CLR生成机械代码,此机械代码首先检查目标引用的有效性。如果目标引用是零,生成的机械代码将引起异常,或者是方法要求的其它东西。记住:即使在非虚拟实例方法中通过C#使用callvirt不会产生性能终结。这是因为非虚拟实例方法上的callvirt仅仅一个额外的指令,除了一个指令已经直接的跳入了方法执行的方法地址这个指令将目标引用和指令中的零比较开来,。在虚拟实例方法使用callvirt这一事例中,有额外的指令,这些指令试图计算出方法地址(此地址建立在类型的基础上,而事实上,此类型受目标引用的指示)。将在下章仔细讨论虚拟方法分配问题。

    CLR把调用IL 指令的调用转换成下面的机械代码。(伪码)

• 把目标引用/托管指针地址移到ECX寄存器里,这个指针总是被下载到ECX寄存器里,这个指针也是第一个隐藏的参数,而在调用任何一个实例方法(虚拟或非虚拟)时,需要经过此隐藏的参数。这是因为CLR使用快速调用惯例,快速调用惯例要求从寄存器,ECX 和 EDX里尽可能多的为快速访问使用前两个方法变元 。

• CLR 连接方法代码的地址,并向那个地址发送调用。在包括调用站点的方法JIT期间, CLR只为每一个调用做一次这样的工作。CLR为方法调用使用变量类型的方法表,在收集方法代码地址时,变量并没有指向对象的方法表。
 
0
相关文章