c#类的⽅法表的建⽴和⽅法的调⽤
对于⽅法的调⽤,很是令我头疼,什么静态⽅法,实例⽅法,实例虚⽅法,这⾥查了很多资料,总结如下:
这⾥声明,我也是菜鸟,这⾥只讨论⽅法的调⽤相关的技术,属于个⼈理解,如有错误,请指正
思路:
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 ()
00320190 001d3818 JIT MethodInvoke.Father.DoWork()
Father类的⽅法表
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 ()//Son类的构造函数
00320238 001d3898 JIT MethodInvoke.Son.DoStaticWork()//Son类的静态⽅法
001dc055 001d38a4 NONE MethodInvoke.Son.DoWork()//Son类的实例⽅法
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 ()
Grandson类的⽅法表
⼆、⽅法表中⽅法的确定:
1 最简单的是⾮虚的⽅法
这个只有⼀种情况,⽅法在哪个类中,该⽅法就是在那个类中定义的。因为这些⾮虚的⽅法,并不会在其⼦类中复制其⽅法。Son 类的⽅法表中最后两个⽅法
MethodInvoke.Son.DoStaticWork()
writeline翻译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 在程序调⽤⽅法时是怎样确定⽅法在⽅法表中的位置的(位于⽅法表的第⼏个⽅法)?
即“引⽤变量类型”的⽅法表中该⽅法的位置(由于虚⽅法表的特点决定了虚⽅法的位置在类层次结构中的各个类的⽅法表中是相同的,也即虚⽅法的位置被其⼦类继承了下来)
完
后记:这篇⽂章查了⽹上很多⽜⼈的博客,还有参考了《你必须知道的》王涛等资料,感谢这些⽜⼈的⾟勤劳动成果,⾃⼰写了才知道来之不易啊。由于笔者⾸次写,有很多不⾜之处,希望指正
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论