继承与多态
个人理解,欢迎讨论
using System;
public abstract class Animal
{
public string name = "Animal";
public abstract void ShowType();
}
public class Bird : Animal
{
public string name = "Bird";
public override void ShowType()
{
Console.WriteLine("Name is {0}", name);
}
}
public class TestInheritance
{
public static void Main()
{
Animal animal = new Bird();
Console.WriteLine("Name value is {0}", animal.name);
animal.ShowType();
Console.ReadKey();
}
}
看到上面小例子,或许你难以说出答案,或许你知道输出是什么,但你有没有这样的疑问:
1、Bird实例中到底有几个name呢,如果有两个,它们是如何区分的,关系又是怎样的?
2、animal.name,animal.ShowType()为何会输出‘Animal,Name is Bird’的结果呢。
我们关心的不是输出是什么,而是为什么会有那样的输出。
上面的问题归根结底是继承和多态如何实现的问题。了解了继承和多态的实现原理,上面的问题和疑惑就不复存在了。
先说继承,子继承父,看看子类和父类都有什么,继承便更好了解了。
用俺仅会的几个指令剖开父子看看(蓝色为指令):
父类Animal:
!dumpclass 00af1368
Class Name: Animal
mdToken: 02000002 (C:"Documents and Settings"Administrator"My Documents"Visual Studio 2005"Projects"ph"ph"bin"Debug"ph.exe)
Parent Class: 790c3ef0
Module: 00af2c5c
Method Table: 00af30a0
Vtable Slots: 5
Total Method Slots: 6
Class Attributes: 100081 Abstract,
NumInstanceFields: 1
NumStaticFields: 0
MT Field Offset Type VT Attr Value Name
793308ec 4000001 4 System.String 0 instance name
-----------------------------------------------------------------------------------------------------------
Parent Class: 790c3ef0----- Animal的父类,是谁?
!dumpclass 790c3ef0
Class Name: System.Object
mdToken: 02000002 (C:"WINDOWS"assembly"GAC_32"mscorlib"2.0.0.0__b77a5c561934e089"mscorlib.dll)
Parent Class: 00000000
Module: 790c1000
Method Table: 79330508
Vtable Slots: 4
Total Method Slots: a
Class Attributes: 102001
NumInstanceFields: 0
NumStaticFields: 0
根,本不用介绍
Method Table: 00af30a0------Animal方法表的地址,由此能看到Animal所有的方法
!dumpmt -md 00af30a0
EEClass: 00af1368
Module: 00af2c5c
Name: Animal
mdToken: 02000002 (C:"Documents and Settings"Administrator"My Documents"Visual Studio 2005"Projects"ph"ph"bin"Debug"ph.exe)
BaseSize: 0xc
ComponentSize: 0x0
Number of IFaces in IFaceMap: 0
Slots in VTable: 6
--------------------------------------
MethodDesc Table
Entry MethodDesc JIT Name
79286a70 79104934 PreJIT System.Object.ToString()
79286a90 7910493c PreJIT System.Object.Equals(System.Object)
79286b00 7910496c PreJIT System.Object.GetHashCode()
792f72f0 79104990 PreJIT System.Object.Finalize()
00afc030 00af307c NONE Animal.ShowType()
00afc038 00af3088 JIT Animal..ctor()
Vtable Slots: 5-----Animal方法表中虚方法的个数--5个(上面红的),前4个是从Object继承来的4个虚方法,ShowType是自定义的虚方法。
Total Method Slots: 6----Animal所有方法个数(上面5个虚的加上构造函数)
Class Attributes: 100081 Abstract,-----类属性,Abstract指明为抽象类
NumInstanceFields: 1----实例字段个数(1个),就是那个name
MT Field Offset Type VT Attr Value Name
793308ec 4000001 4 System.String 0 instance name----实例字段name
再者有必要注意的是Animal的构造函数:
.method hidebysig specialname rtspecialname instance void .ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldstr "Animal"
IL_0006: stfld string Animal::name
IL_000b: ldarg.0
IL_000c: call instance void [mscorlib]System.Object::.ctor()
IL_0011: nop
IL_0012: ret
}
如上,Animal被解剖了,有几个问题需要思考一下:
1、Animal为抽象类,不能被实例化,那构造函数(Animal..ctor())有何用?
2、既然不可能存在Animal类的实例,那实例字段name有什么用处?
3、实例字段name的OffSet为4是什么意思?
问题先留着,接着看Bird:
Bird的类型信息:
!dumpclass 00af13cc
Class Name: Bird
mdToken: 02000003 (C:"Documents and Settings"Administrator"My Documents"Visual Studio 2005"Projects"ph"ph"bin"Debug"ph.exe)
Parent Class: 00af1368
Module: 00af2c5c
Method Table: 00af3130
Vtable Slots: 5
Total Method Slots: 6
Class Attributes: 100001
NumInstanceFields: 2
NumStaticFields: 0
MT Field Offset Type VT Attr Value Name
793308ec 4000001 4 System.String 0 instance name
793308ec 4000002 8 System.String 0 instance name
-----------------------------------------------------------------------------------------------------------------------
由上可看出:Bird的父类是Animal(Parent Class: 00af1368),有5个虚方法(Vtable Slots: 5),一共有6个方法(Total Method Slots: 6),有2个实例字段(NumInstanceFields: 2),一个字段叫name,位移为4,一个字段也叫name,位移为8。
Bird的方法表信息:
!dumpmt -md 00af3130
EEClass: 00af13cc
Module: 00af2c5c
Name: Bird
mdToken: 02000003 (C:"Documents and Settings"Administrator"My Documents"Visual Studio 2005"Projects"ph"ph"bin"Debug"ph.exe)
BaseSize: 0x10
ComponentSize: 0x0
Number of IFaces in IFaceMap: 0
Slots in VTable: 6
--------------------------------------
MethodDesc Table
Entry MethodDesc JIT Name
79286a70 79104934 PreJIT System.Object.ToString()
79286a90 7910493c PreJIT System.Object.Equals(System.Object)
79286b00 7910496c PreJIT System.Object.GetHashCode()
792f72f0 79104990 PreJIT System.Object.Finalize()
00afc050 00af3104 JIT Bird.ShowType()
00afc058 00af3110 JIT Bird..ctor()
再看Bird的构造函数:
.method hidebysig specialname rtspecialname instance void .ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldstr "Bird"
IL_0006: stfld string Bird::name
IL_000b: ldarg.0
IL_000c: call instance void Animal::.ctor()
IL_0011: nop
IL_0012: ret
}
问题又来了:
1、Bird有两个实例字段name,一个是Bird本身的(public string name = "Bird";),另一个是从哪来的?(不难猜到,是从Animal继承过来的),但哪个name是Animal的,哪个又是Bird的?
2、两个name,一个位移为4,一个位移为8,是啥意思?
3、既然实例字段name被Bird继承来了,那Animal的方法会被Bird继承么?
将问题汇总一下:
1、Animal为抽象类,不能被实例化,那构造函数(Animal..ctor())有何用?
2、既然不可能存在Animal类的实例,那实例字段name有什么用处?
3、实例字段name的OffSet为4是什么意思?
4、Bird有两个实例字段name,一个是Bird本身的(public string name = "Bird";),另一个是从哪来的?(不难猜到,是从Animal继承过来的),但哪个name是Animal的,哪个又是Bird的?
5、两个name,一个位移为4,一个位移为8,是啥意思?
6、既然实例字段name被Bird继承来了,那Animal的方法会被Bird继承么?
通过问题3 4 5,大概能猜出来第一个name(位移为4)是Bird继承自Animal的,第二个name(位移为8)的是Bird自己的,猜对么?验证一下 找来Bird的实例看看:
!do 0x00c73528
Name: Bird
MethodTable: 00af3130
EEClass: 00af13cc
Size: 16(0x10) bytes
(C:"Documents and Settings"Administrator"My Documents"Visual Studio 2005"Projects"ph"ph"bin"Debug"ph.exe)
Fields:
MT Field Offset Type VT Attr Value Name
793308ec 4000001 4 System.String 0 instance 00c73554 name
793308ec 4000002 8 System.String 0 instance 00c73538 name
可以看到Bird实例有两个name,而且Value也不一样:
!do -nofields 00c73554
Name: System.String
MethodTable: 793308ec
EEClass: 790ed64c
Size: 30(0x1e) bytes
(C:"WINDOWS"assembly"GAC_32"mscorlib"2.0.0.0__b77a5c561934e089"mscorlib.dll)
String: Animal
!do -nofields 00c73538
Name: System.String
MethodTable: 793308ec
EEClass: 790ed64c
Size: 26(0x1a) bytes
(C:"WINDOWS"assembly"GAC_32"mscorlib"2.0.0.0__b77a5c561934e089"mscorlib.dll)
String: Bird
知道结果了,显然不够,还要知道过程:两个name是怎么被分别整成‘Animal’和‘Bird’的。
这要从对象的实例化说起,一切源于new Bird()。
new Bird()是怎样一个过程:
1、CLR计算并分配对象所需空间,包括:同步块索引,方法表地址,实例字段及其他一些信息,如下图:
并将所分配空间的地址压栈(ObjectInstance),在此过程,CLR会为实例字段赋默认值(值类型为0,引用类型为NULL)。
2、调用构造函数对实例字段进行初始化
过程2可通过Bird构造函数的IL代码粗略了解:
.method hidebysig specialname rtspecialname instance void .ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldstr "Bird" 将‘Bird’的地址入栈
IL_0006: stfld string Bird::name 将栈上‘Bird’地址存入name字段
IL_000b: ldarg.0
IL_000c: call instance void Animal::.ctor()
IL_0011: nop
IL_0012: ret
}
首先初始化字段name(绿色),再调用父类Animal的构造函数。
或许ldarg.0,Bird::name还是不够’真实‘,那看看它们对应的汇编:
说明:汇编我不懂,所以以下纯属瞎猜,不保证对
!u 00af3110
Normal JIT generated code
Bird..ctor()
Begin 003b00f8, size 47
003B00F8 55 push ebp
003B00F9 8BEC mov ebp,esp
003B00FB 57 push edi
003B00FC 56 push esi
003B00FD 53 push ebx
003B00FE 83EC30 sub esp,30h
003B0101 33C0 xor eax,eax
003B0103 8945F0 mov dword ptr [ebp-10h],eax
003B0106 33C0 xor eax,eax
003B0108 8945E4 mov dword ptr [ebp-1Ch],eax
003B010B 894DC4 mov dword ptr [ebp-3Ch],ecx
003B010E 833D142EAF0000 cmp dword ptr ds:[00AF2E14h],0
003B0115 7405 je 003B011C
003B0117 E865A3D179 call 7A0CA481 (JitHelp: CORINFO_HELP_DBG_IS_JUST_MY_CODE)
003B011C 8B053420C701 mov eax,dword ptr ds:[01C72034h] ("Bird")
003B0122 8B4DC4 mov ecx,dword ptr [ebp-3Ch]
003B0125 8D5108 lea edx,[ecx+8]
003B0128 E8632CAC79 call 79E72D90 (JitHelp: CORINFO_HELP_ASSIGN_REF_EAX)
003B012D 8B4DC4 mov ecx,dword ptr [ebp-3Ch]
003B0130 E803BF7400 call 00AFC038 (Animal..ctor(), mdToken: 06000002)
003B0135 90 nop
003B0136 90 nop
003B0137 8D65F4 lea esp,[ebp-0Ch]
003B013A 5B pop ebx
003B013B 5E pop esi
003B013C 5F pop edi
003B013D 5D pop ebp
003B013E C3 ret
通过看上面红色的指令,应该是将Bird的地址存入了[ecx+8]位置。
验证一下:
我们看看这个地址到底是指哪:
mov ecx,dword ptr [ebp-3Ch] ebp的值是0012F480,这条指令结果是将00c73528放入寄存器ecx中,00c73528又是什么呢?
!do 00c73528
Name: Bird
MethodTable: 00af3130
EEClass: 00af13cc
Size: 16(0x10) bytes
(C:"Documents and Settings"Administrator"My Documents"Visual Studio 2005"Projects"ph"ph"bin"Debug"ph.exe)
Fields:
MT Field Offset Type VT Attr Value Name
793308ec 4000001 4 System.String 0 instance 00c73554 name
793308ec 4000002 8 System.String 0 instance 00c73538 name
示意图大概:
00c73528再加上8([ecx+8])得00c73530,地址00c73530存放着00c73538,看看00c73538又是什么:
!do -nofields 00c73538
Name: System.String
MethodTable: 793308ec
EEClass: 790ed64c
Size: 26(0x1a) bytes
(C:"WINDOWS"assembly"GAC_32"mscorlib"2.0.0.0__b77a5c561934e089"mscorlib.dll)
String: Bird
终于找到了!!是Bird
这也就说明了上面红色汇编确实是把Bird存入了[ecx+8]位置,这个8就是字段的偏移量。
以上便是Bird构造函数中字段初始化部分,接着便调用父类Animal的构造函数:
.method hidebysig specialname rtspecialname instance void .ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldstr "Animal"
IL_0006: stfld string Animal::name
IL_000b: ldarg.0
IL_000c: call instance void [mscorlib]System.Object::.ctor()
IL_0011: nop
IL_0012: ret
}
找到对应函数的汇编,取其中我们关心的部分:
003B0174 8B053820C701 mov eax,dword ptr ds:[01C72038h] ("Animal")
003B017A 8B4DC4 mov ecx,dword ptr [ebp-3Ch]
003B017D 8D5104 lea edx,[ecx+4]
003B0180 E80B2CAC79 call 79E72D90 (JitHelp: CORINFO_HELP_ASSIGN_REF_EAX)
这次是吧’Animal‘存入‘[ecx+4]位置,所以上面那图在实例化完成后是这样的:
这下问题:
Animal为抽象类,不能被实例化,那构造函数(Animal..ctor())有何用?
既然不可能存在Animal类的实例,那实例字段name有什么用处?
两个问题似乎有答案了吧:
父类的构造函数负责初始化父类所定义的实例字段(在子类的构造函数中调用),在调用父类构造函数的时候,子类实例的地址是被当参数传递过去的,所以:
IL_000b: ldarg.0
IL_000c: call instance void Animal::.ctor()
ldarg.0正是Bird实例的地址,在Animal的构造函数中,拿Bird实例的地址加上Animal的实例字段的位移(4),完成了Animal定义的name的初始化工作。
所以事情大概是这样的(Bird构造函数执行过程):
ldarg.0
ldstr "Bird"
stfld string Bird::name
ldarg.0
call instance void Animal::.ctor()
取得Bird实例的地址,加上位移8,完成了Bird定义name的初始化,接着调用Animal的构造函数,在Animal的构造函数中,取得Bird实例的地址(当参数传过来的),加上位移4,完成Animal定义name的初始化,而位移值(4,8)是在CLR为实例分配空间的时候就规定好的,以后在读取字段时,只需要知道该用哪个位移值就行了。
当
Animal animal = new Bird();
Console.WriteLine("Name value is {0}", animal.name);这个animal.name会取出哪个name呢,关键是编译器按既定的规则取哪个位移值了。
上面的关键汇编是:00000052 mov edx,dword ptr [eax+4]
可见用的位移值4去取相应字段了,至于是取出哪个值,就看编译器的编译规则和CLR如何安排位移了。
我的意思是,对于计算机而言,初始化时CLR把两个字段的位移设成4 和8 ,并把位移4处初始化为Animal,8处为Bird。编译器看到Animal animal = new Bird();Animal.name时,会用位移4去取值,为啥不用8,你问写编译器的吧,相应的,看到Bird animal = new Bird();Animal.name时,会用位移8去取值。对于我们而言,规则变成了:实例字段的取值是按其声明类型的,所以变量aminal的声明类型是哪个,就去哪个的值。
下面该说多态了:
就是animal.ShowType();为啥会打印这个,而不打印出别的的问题。偷个懒,就不再写了,我文章‘实例化与多态’里面有还算详细的胡侃,都看到这里了,估计你也不介意找下那篇了。