• 从C#方法表看透方法调用的本质

    类、结构和接口可以拥有自己的方法。委托作为一个特殊的类,也有自己的方法。

    引用类型通过类型对象指针可以找到类型方法表,从而调用方法。

    对于值类型,也有方法表(任何类型都有方法表),但值类型的实例没有类型对象指针指向它,需要访问类型元数据获得方法表。

    类型方法表是在类型加载的过程中建立的,和类型对象的建立位于相同的阶段。

    我们使用下面的示例类型:

    interface ITest
    {
        string interfaceMethod();
    }
    class FatherClass : ITest
    {
        public static int i = 1;
        public string interfaceMethod()
        {
            Console.WriteLine("继承接口的方法");
            return "test";
        }
        public int NormalMethod(int a)
        {
            return a + 1;
        }
        public int NormalMethod2(int a)
        {
            return a + 2;
        }
        public int NormalMethod3(int a)
        {
            return a + 3;
        }
        public virtual void VirtualMethod1()
        {
            Console.WriteLine("VirtualMethod1");
        }
        public virtual void VirtualMethod2()
        {
            Console.WriteLine("VirtualMethod2");
        }
    }

    主程序:

    static void Main(string[] args)
    {
        var a = new FatherClass();
        Console.WriteLine("没调用方法");
        Console.ReadKey();
    
        a.NormalMethod(100);
        Console.WriteLine("调用方法");
        Console.ReadKey();
    }

    方法表

    类型对象中最重要的部分无疑是方法表,它是类加载过程中生成的。

    在上面的代码中,示例类型含有三个普通方法:两个虚方法,一个来自接口继承的方法,以及实例构造函数 .ctor 和静态构造函数 .cctor (因为包含了对静态字段的赋值),可以在方法表中找到它们。

    另外,方法表还含有类型所有父类的虚方法,父类的其他方法不出现在子类的方法表中。

    方法表的排列顺序严格按照方法定义的顺序,并从最高辈分开始往下排。因此,示例类型的方法表含有下面的成员(还有其他成员):

    • Object 的四个非虚方法
    • 自己继承自接口的方法
    • 自己的虚方法
    • 两个构造函数
    • 自己的普通方法

    这些成员组成了方法表的一部分——方法槽表(method slot table)。

    方法槽表按照如下顺序排列:继承的虚方法、自己继承自接口的方法、自己的虚方法、构造函数、自己的实例方法、自己的静态方法(不同版本的 CLR,顺序可能不同)。

    我们可以看到 Object 类中有一些方法不在其中,这是因为它们不是虚方法。

    方法槽表的每一个成员都包含着另一个表,即方法描述(MethodDesc)的其中一个位置,和那个表实现一一对应关系。

    方法调用

    方法的调用是 .NET 框架中最有趣的功能之一。简单来说,方法的调用是一个路由的过程。

    我们通过栈上的引用找到类型的方法表指针,它又指向方法表的开头。

    通过确定的偏移量,CLR 马上就可以定位到接口虚表的开头,或者方法槽表的开头。

    对前者来说,假设要调用的方法 X 位于接口 Y 中,接下来的事情就是查找到指向 Y 的方法表在接口虚表中的位置(不会顺序寻找,因为每个接口的偏移量都已经被索引好了)。

    然后,就可以定位到 Y 的方法表,之后,再次通过偏移量跳转到 Y 的方法槽表的开头,最后就和后者相同。

    对于后者来说,CLR 在方法槽表中找到方法,然后找到存根例程,根据它后面的 jmp 指令,就可以被引导到 JIT 编译器代码或者机器码,从而继续方法的调用。

    而实际上,每个方法都有自己的偏移量(在类型加载时,方法表的顺序就已经确定了,偏移量也就可以被计算出来了)。

    因此,CLR 是不会一个一个地寻找的,它总是一步到位。

    1) 方法的反射调用

    我们可以想象,如果直接访问类型对象获得了某个方法的地址,并传入必须的参数,那么我们是不是就可以进行方法调用了呢?答案是肯定的。

    这种类型的方法调用甚至不需要一个对应类型的实例,因为我们是直接从类型对象(元数据的一部分)出发的,而传统的方式是从栈上的引用(实例)出发的,这样的做法就叫做反射(reflection),如下图所示。

    方法的反射调用

更多...

加载中...