本节主要讲.net实体方法在继承下的调用。参考《你必须知道的.Net》又参考了很多博客,加上自己的理解,因讨论比较深如有不当之处,还望指正。
实例代码
public abstract class Animal
{
public abstract void ShowType();
public virtual void Eat()
{
Console.WriteLine("Animal always eat.");
}
}
public class Bird : Animal
{
private string type = "Bird";
public override void ShowType()
{
Console.WriteLine("Type is {0}", type);
}
public new void Eat()
{
Console.WriteLine("Bird can eat.");
}
}
public class Chicken : Bird
{
private string type = "Chicken";
public override void ShowType()
{
Console.WriteLine("Type is {0}", type);
}
public new void Eat()
{
Console.WriteLine("Chicken can eat.");
}
}
代码调用一:
static void Main(string[] args)
{
Bird bird = new Chicken();
bird.ShowType();
bird.Eat();
Console.ReadKey();
}
大家猜下运行结果,结果如下:
理论基础
- CLR层次上理解继承
Bird bird = new Bird(); Chicken chicken = new Chicken();
Bird bird
创建的是一个Bird类型的引用,而new Bird()完成的是创建Bird对象(在托管堆栈上分配内存空间)。我们以Chicken
对象的创建为例,分析CLR在运行时的过程
- 字段分配
对象创建首先为其父类字段分配空间,所以Chicken
会找其父类Bird
字段,Bird
找其父类Animal
一次递归到Object
。所以对象字段在托管堆栈中的存储顺序是由上至下(先父类字段在子类字段),这样如果子类中出现同名字段,编译器自动认为这是两个不同的字段。 - 方法表的创建
方法表的创建是类第一次加载到AppDomain
时完成(在对象创建之前)。在对象创建时将其TypeHandle
指向方法表在Loader Heap
上的地址。方法表的创建也是由上至下(先创建父类方法,在创建子类方法),这样子类方法与父类方法重名编译器自动认为是两个不同的方法,还有一点override
重写虚方法(或抽象方法)时,将在已分配的父类方法上重写,不在添加新的方法。
- 引用类型与对象类型
Bird bird; bird = new Chicken();
Bird bird;
声明引用类型为Bird
的变量,分配在栈上(占4个字节)
bird = new Chicken();
为创建类型为Chicken
的对象,并堆栈地址赋值给bird
变量。
所以bird
的引用类型为Bird
,而对象类型(变量指向的实体)为Chicken
类型对象。
问题讨论
回过来看 实例代码 ,不知道大家是否猜中了上面的运行结果。
那么运行时bird
到底时调用哪个类型上的方法表啊,是 Bird
类型,还是Chicken
类型?bird.ShowType()
运行结果Type is Chicken
,可知运行的为Chicken
类型的方法表。但是bird.Eat();
运行结果为Bird can eat.
哪这个运行的为那个类型的方法表啊?
解答问题
- 通过IL语言分析
new
和override
通过ILSpy工具查看程序集中间代码。Bird
类型下的ShowType()
和Eat()
方法
.method public hidebysig virtual
instance void ShowType () cil managed
{
// Method begins at RVA 0x208f
// Code size 19 (0x13)
.maxstack 8
IL_0000: nop
IL_0001: ldstr "Type is {0}"
IL_0006: ldarg.0
IL_0007: ldfld string ConsoleApp.Inheritance.InheritDemo.Bird::'type'
IL_000c: call void [mscorlib]System.Console::WriteLine(string, object)
IL_0011: nop
IL_0012: ret
} // end of method Bird::ShowType
.method public hidebysig
instance void Eat () cil managed
{
// Method begins at RVA 0x20a3
// Code size 13 (0xd)
.maxstack 8
IL_0000: nop
IL_0001: ldstr "Bird can eat."
IL_0006: call void [mscorlib]System.Console::WriteLine(string)
IL_000b: nop
IL_000c: ret
} // end of method Bird::Eat
关键字virtual
表明重写ShowType()
方法。其实Eat()
方法是为Bird
类型新建了一个Eat()
方法(你可以在Bird
类下添加新方法Eat2()
指令将同样)则Bird与Chicken的方法表如下
2.方法调用的IL代码
bird.ShowType();
反编译为:
IL_0007: ldloc.0
IL_0008: callvirt instance void ConsoleApp.Inheritance.InheritDemo.Animal::ShowType()
IL_000d: nop
callvirt
调用被Chicken
重写ShowType()
方法。
bird.Eat();
反编译为:
IL_000e: ldloc.0
IL_000f: callvirt instance void ConsoleApp.Inheritance.InheritDemo.Bird::Eat()
IL_0014: nop
这里先假设是调用的是Chicken
类型方法表中的Bird::Eat()
的方法,因Bird
与Chicken
都新加了各自的Eat()
方法。
再次论证(直接修改IL代码,调用Chicken
类型方法表中的Chicken::Eat()
的方法):
- 通过Developer Command Prompt工具反编译程序集
ildasm 你的程序集.exe /out:test.il
- 打开test.il文件将
IL_000f: callvirt instance void ConsoleApp.Inheritance.InheritDemo.Bird::Eat()
修改为IL_000f: callvirt instance void ConsoleApp.Inheritance.InheritDemo.Chicken::Eat()
- 再编译
ilasm test.il /res:test.res /out:test.exe
运行test.exe结果为:
总结
- 编译时,依照引用类型。所以
bird.Eat();
编译为:Bird::Eat()
。 - 运行时调用的对象实体类型的方法。也可通过反射调用调用对象类型上的方法
bird.GetType().GetMethod("Eat").Invoke(bird, null);
输出为Chicken can eat.