对于方法的调用,很是令我头疼,什么静态方法,实例方法,实例虚方法,这里查了很多资料,总结如下:
这里声明,我也是菜鸟,这里只讨论方法的调用相关的技术,属于个人理解,如有错误,请指正
思路:
1 clr在加载类型的过程中方法表是怎么样构建的?
2 在程序调用方法时是怎样确定使用哪个类型的方法表的?
3 在程序调用方法时是怎样确定方法在方法表中的位置的(位于方法表的第几个方法)?
一 、方法在方法表中的排列顺序:
继承的实例虚方法、实例虚方法、构造函数、静态方法、实例方法
方法表排列原则:
1 在类的方法表的构造过程中:虚方法总是在子类的方法表中被复制的;实例方法,构造函数,静态方法等其他方法则在子类的方法表中不继承的
2 在类的方法表中:虚方法总是排在方法表的开头位置;继承的虚方法在最前面,新建的虚方法紧随其后(如图)
3 虚方法后边依次排列的是构造函数、静态方法、实例方法
为什么把“继承的实例虚方法”和“实例虚方法”放在方法表的开头位置?
在这种情况下每个虚方法在 相关的类的 方法表中 的位置都是不变的(无论是在其在创建方法的类中还是在派生类中):比如一个虚方法在类中的次序是第k个,那么他在其子类或父类(如果父类中有这个方法)中的位置都是第k个。
如果子类中新添加了虚方法,因为在新填的虚方法之前,已经把父类的方法表中的虚方法都复制到了子类的方法表最前面,所以父类中所有的方法在其子类中的位置序号都是不变的。
如果子类中新添加了除了虚方法之外的其他方法(实例方法,构造函数,静态方法等),这些方法也都是排在虚方法之后
以上两点就保证了虚方法无论是在其自身的类、父类、子类中其在方法表中的位置(位于方法表的第几个)都是不变的
结论:方法表中虚方法的排序,可以在类的层次结构中保持虚方法的层次结构,这是实现多态的基础,也就是为什么说继承是实现多态的基础了。
例子:
类的定义代码如下:
class Program { static void Main(string[] args) { Father son = new Son(); son.DoWork(); son.DoVirtualWork(); son.DoVirtualAll(); Son.DoStaticWork(); Father aGrandson = new Grandson(); aGrandson.DoWork(); aGrandson.DoVirtualWork(); aGrandson.DoVirtualAll(); Console.ReadKey(); } } public class Father { public void DoWork() { Console.WriteLine("Father.DoWork()"); } public virtual void DoVirtualWork() { Console.WriteLine("Father.DoVirtualWork()"); } public virtual void DoVirtualAll() { Console.WriteLine("Father.DoVirtualAll()"); } } public class Son : Father { public static void DoStaticWork() { Console.WriteLine("Son.DoStaticWork()"); } public new void DoWork() { Console.WriteLine("Son.DoWork()"); } public new virtual void DoVirtualWork() { Console.WriteLine("Son.DoVirtualWork()"); } public override void DoVirtualAll() { Console.WriteLine("Son.DoVirtualAll()"); } } public class Grandson : Son { public override void DoVirtualWork() { Console.WriteLine("Grandson.DoVirtualWork()"); } public override void DoVirtualAll() { Console.WriteLine("Grandson.DoVirtualAll()"); } } public class GrandGrandson : Grandson { public new virtual void DoVirtualWork() { Console.WriteLine("GGson.DovirtualWork()"); } public override void DoVirtualAll() { Console.WriteLine("GGson.DoVirtualAll()"); } }
Entry MethodDe JIT Name 6751cd88 672360bc PreJIT System.Object.ToString() 67516a90 672360c4 PreJIT System.Object.Equals(System.Object) 67516660 672360e4 PreJIT System.Object.GetHashCode() 675967c0 672360f8 PreJIT System.Object.Finalize() 003201c8 001d3824 JIT MethodInvoke.Father.DoVirtualWork() 001dc035 001d382c NONE MethodInvoke.Father.DoVirtualAll() 00320158 001d3834 JIT MethodInvoke.Father..ctor() 00320190 001d3818 JIT MethodInvoke.Father.DoWork()
Entry MethodDe JIT Name 6751cd88 672360bc PreJIT System.Object.ToString() 67516a90 672360c4 PreJIT System.Object.Equals(System.Object) 67516660 672360e4 PreJIT System.Object.GetHashCode() 675967c0 672360f8 PreJIT System.Object.Finalize()////前四个方法是继承自Object类的方法 003201c8 001d3824 JIT MethodInvoke.Father.DoVirtualWork() 00320200 001d38b8 JIT MethodInvoke.Son.DoVirtualAll()//这也是继承的Father类的虚方法,只不过在Son类中重写的方法覆盖了//这两个类是继承自Father类的方法 001dc059 001d38b0 NONE MethodInvoke.Son.DoVirtualWork()//这个是Son类中新建的方法 00320120 001d38c0 JIT MethodInvoke.Son..ctor()//Son类的构造函数 00320238 001d3898 JIT MethodInvoke.Son.DoStaticWork()//Son类的静态方法 001dc055 001d38a4 NONE MethodInvoke.Son.DoWork()//Son类的实例方法
Entry MethodDe JIT Name 6751cd88 672360bc PreJIT System.Object.ToString() 67516a90 672360c4 PreJIT System.Object.Equals(System.Object) 67516660 672360e4 PreJIT System.Object.GetHashCode() 675967c0 672360f8 PreJIT System.Object.Finalize() 003201c8 001d3824 JIT MethodInvoke.Father.DoVirtualWork() 003202a8 001d3930 JIT MethodInvoke.Grandson.DoVirtualAll() 001dc079 001d3928 NONE MethodInvoke.Grandson.DoVirtualWork() 00320270 001d3938 JIT MethodInvoke.Grandson..ctor()
二、方法表中方法的确定:
1 最简单的是非虚的方法
这个只有一种情况,方法在哪个类中,该方法就是在那个类中定义的。因为这些非虚的方法,并不会在其子类中复制其方法。Son类的方法表中最后两个方法
MethodInvoke.Son.DoStaticWork()
MethodInvoke.Son.DoWork()这两个类是Son类中定义的,因此也只有Son类的方法表中才会有这两个方法
就验证了这个说法。
2 对于虚方法
方法表中的方法有可能有三种来源
a 来自其所在类新建的虚方法,这种情况会在方法表中适当的位置新加一个方法表槽(使用new virtual 和virtual关键字)
例如:public new virtual void DoVirtualWork()和public virtual void NewMethod()
b 通过继承父类并且在类中重新定义的虚方法,这种情况在把父类的方法复制到该类的方法表中后,使用重新定义的方法将其覆盖掉,不会新建方法表槽(使用override关键字)
例如:public override void DoVirtualWork()
c 通过继承父类的虚方法,这种情况不用使用任何关键字,他只是把父类的方法复制到该类的方法表中。
b c两种其实都继承了父类中该方法在方法表中的位置,而a则是在该类的方法表中新添加了位置(新见了方法表槽)
三、方法的调用:
要讲明白方法的调用,先要解释几个名词(自己理解的名词)
引用变量:是指在声明时的那个变量,如object a;这里的a就是引用变量
对象、实例:在实例化中建立的那个对象,如new object(),会创建一个object对象(并且返回一个对象的引用)
从C#到IL:
首先看看从C#语言到IL语言,C#编译器是怎么翻译的
在调用方法的时候,C#编译器关注的是引用变量的类型,它并不会关心实例类型是什么。C#编译器会从引用变量的类型开始向其父类逐层查找:
a 对于实例虚方法,直到查找到virtual关键字的时候,就会翻译为该方法的调用。如:
对于上面代码中的类型,如果我有代码Father gd=new Grandson();gd.DoVirtualWork(),那么在IL中会翻译成callvirt/call Father::DoVirtualWork()
如果有代码Grandson gd=new Grandson();gd.DoVirtualWork(),那么在IL中就会翻译成callvirt/call Son::DoVirtualWork()
这里通常情况用的是callvirt,但在有些情况是会用call的:
-比如一个密封类引用的虚方法就可以用call,因为可以确定没有派生类,不会调用派生类中的方法了,使用call可以避免进行类型检查,提高性能
-值类型调用虚方法时也会用call,值类型首先是一个密封类型,其次call调用可以阻止值类型被执行装箱
-在类型定义中,调用基类的虚方法时,采用call可以避免callvirt递归调用本身引起的堆栈溢出,如
class call_callvirt { public override bool Equals(object obj) { return base.Equals(obj); } }
b 对于非虚方法,直到查找到第一个含该方法的定义类时,就会调用该方法。如:
对于上面代码中的类型,如果我有代码Father gd=new Grandson();gd.DoWork(),那么在IL中会翻译成call/callvirt Father::DoWork()
如果有代码Grandson gd=new Grandson();gd.DoWork(),那么在IL中就会翻译成call/callvirt Son::DoVirtualWork()
这里通常用的用的是call,但也有使用callvirt的情况:
-常见的在引用类型中使用callvirt,因为引用变量为null时会抛出异常NullReferenceException,而call则不会抛出任何异常,为类型安全起见在C#中会调用callvirt来完成非虚类型的调用
总的来说,在C#中对方法的调用在IL中基本都翻译成了call/callvirt(还有calli在C#中我很少见倒)指令调用方法,虽然call和callvirt用法比较乱,但是骨子里还是有区别的:
call用来调用静态类型、声明类型的方法,而callvirt调用动态类型、实际(实例)类型的方法
从IL到localcode
这里首先要讲的就是IL中call和callvirt的区别了:
call 直接调用函数(由上一部分知道,这里调用的函数是由引用变量的类型决定的);
执行静态调度:在编译期间就可以确定其执行的操作(编译的时候就可以确定使用哪个类型的方法表)
callvirt会检查引用变量所指向的实例的类型(包括是否是null引用),并且在实例的类型的方法表中调用对应位置的方法(这个命令实际上就是说知道了方法在方法表中的位置,由实例的类型决定使用哪个方法表中对应位置的方法)
执行动态调度:在运行时才能确定执行的操作(需要运行时判断引用变量所指向的实例的类型,进而确定该实例类型的方法表为要使用的方法表)
另外的方法调用:
基于反射技术的动态调度机制,基本原理是在运行时,查找方法表的信息来实施调用的方式。常见的方式有:MethodInfo.Invoke()方式和动态方法委托(Dynamic Method Delegate)方式。
四、贴出去的代码的结果如下图:
回答问题:
假定有ABC三个类型,且A<--B<--C
1 clr在加载类型的过程中方法表是怎么样构建的?
这里只关心方法表,类的其他部分忽略:clr在实例化类型的实例的时候,需要在之前加载好类型:
加载object类(如果还未加载,下同);
然后加载类A,在这个过程中先将object类的虚方法复制在A类的方法表中,然后依次排列A类的虚方法,构造函数,静态方法,实例方法;
然后加载类B,在这个过程中先将A类的虚方法复制在A类的方法表中,然后依次排列B类的虚方法,构造函数,静态方法,实例方法;
然后加载类C,在这个过程中先将B类的虚方法复制在C类的方法表中,然后依次排列C类的虚方法,构造函数,静态方法,实例方法;
.....依次类推,这就构成了各个类自己的方法表,方法表中包括了继承的虚方法、虚方法、构造函数、静态方法、实例方法
2 在程序调用方法时是怎样确定使用哪个类型的方法表的?
对于非虚方法:“引用变量”是哪个类型,就使用哪个类型的方法表
对于虚方法:“引用变量指向的对象类型” 是什么类型,就使用哪个类型的方法表
3 在程序调用方法时是怎样确定方法在方法表中的位置的(位于方法表的第几个方法)?
即“引用变量类型”的方法表中该方法的位置(由于虚方法表的特点决定了 虚方法的位置 在 类层次结构中的各个类的方法表 中 是相同的,也即虚方法的位置被其子类继承了下来)
完
后记:这篇文章查了网上很多牛人的博客,还有参考了《你必须知道的.net》王涛 等资料,感谢这些牛人的辛勤劳动成果,自己写了才知道来之不易啊。由于笔者首次写,有很多不足之处,希望指正