前进中的蜗牛

番茄大叔

水滴穿石,非一日之功;没有量变,何来质变。

.net 方法的调用

本节主要讲.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在运行时的过程
  1. 字段分配
    对象创建首先为其父类字段分配空间,所以Chicken会找其父类Bird字段,Bird找其父类Animal一次递归到Object。所以对象字段在托管堆栈中的存储顺序是由上至下(先父类字段在子类字段),这样如果子类中出现同名字段,编译器自动认为这是两个不同的字段。
  2. 方法表的创建
    方法表的创建是类第一次加载到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.哪这个运行的为那个类型的方法表啊?

解答问题

  1. 通过IL语言分析newoverride
    通过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()的方法,因BirdChicken都新加了各自的Eat()方法。
再次论证(直接修改IL代码,调用Chicken类型方法表中的Chicken::Eat()的方法):

  1. 通过Developer Command Prompt工具反编译程序集ildasm 你的程序集.exe /out:test.il
  2. 打开test.il文件将IL_000f: callvirt instance void ConsoleApp.Inheritance.InheritDemo.Bird::Eat()修改为IL_000f: callvirt instance void ConsoleApp.Inheritance.InheritDemo.Chicken::Eat()
  3. 再编译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.
posted @ 2018-03-01 18:13  LoveTomato  阅读(354)  评论(0编辑  收藏  举报