静态方法、实例方法和虚方法的区别

基础知识

对于面向对象的语言来说,所有类型都是从System.Object类型派生,正是由于这个原因,保证了每个类型都有一组最基本的方法,也就是从他们的父类System.Object继承来的方法,Object的定义如下面的代码所示,System.Object所定义的基本方法中基本包含了CLR所有的方法类型,静态方法(Static修饰,属于类成员)、虚方法(Virtural修饰,属于实例成员)、实例方法(普通的方法,属于实例成员)。可能会有人说还有抽象方法,其实抽象方法最后的编译也是一个虚方法。

CLR的最重要的特性之一就是类型安全性,在运行时,CLR总是知道一个对象是什么类型,我们看到Object中有一个GetType方法,这个方法总是能知道一个对象的确切类型,由于GetType是一个非虚实例方法,从而保证了派生类型不能重写它,所以一个类型不可能伪装成另一个类型,其实如果我们要有意的隐藏也是可以做到的(我们可以使用New关键字覆盖GetType方法),不过一般我们不推荐这样做。

那么GetType方法是如何返回一个对象的真实类型的呢?这就要引入一个新的概念,也是是“类型对象”,当我们使用New关键字在托管堆上创建一个对象的时候,大致做了一下几件事情:

 class Program
    {
        static void Main(string[] args)
        {

            Person p = new Person("Aseven");
          
            Console.ReadKey();

        }
    }
    public class Person
    {
        private string _name;
        private int _age;

        public string Name 
        {
            get { return _name; }
            set { _name = value; }
        }
        public virtual void Say()
        {
            Console.WriteLine("******");
        }
        public static Person Find(string name)
        {
            return new Person(name);//模拟数据库查找
        }
        public int GetAge()
        {
            return _age;
        }
        public Person() { }
        public Person(string name)
        {
            this._name = name;
        }
    }
View Code

1、计算类型和所有基类型(直到System.Object,虽然它没有定义实例字段)的所有实例字段(注意:没有静态字段和方法)所需要的字节数,堆上的每个对象都需要一些额外的成员---即类型对象指针和同步索引块,这些成员由CLR用于管理对象,也会计入对象大小。

2、从托管堆上分配对象的内存,并把字段初始化为零(0)。

3、初始化对象的“类型对象指针”和“同步索引块”。

4、执行实例构造函数,并向其传入在对new的调用中指定的实参(上例中的“Aseven”),大多数编译器会自动生成代码来调用基类的构造器,最终调用的而是System.Object的构造器。

在New执行了之后,会返回对堆中对象的一个引用(或指针),对上例来说,这个引用(地址)保存在变量e中,创建完这个Person对象之后,内存结构大致如下,可以看到类型对象指针指向的就是person的类型对象。

总结:一个实例对象创建之后,变量e保存了托管堆中的person对象的一个引用(指针)。而person对象指示保存了对象的一个实例字段(包括类型对象指针和同步索引块),至于静态字段、方法列表都保存在person的类型对象中,特别注意的是方法的列表,这个列表包含了静态方法、实例方法、虚方法,下面我们就来介绍对于这三种方法是如何调用的。

方法的调用

1、静态方法:当调用一个静态方法时,CLR会定位与定义静态方法对应的类型对应的类型对象(有点绕)。然后在类型对象的方法列表中查找对应的记录项,进行JIT编译(如果需要),然后调用。

2、实例方法:当调用一个非虚实例方法时,JIT编译器会找到发出调用的那个变量(p)对应的类型对应的类型对象,如果类型对象的方法列表中没有包含那个被调用的方法,JIT编译器会回溯类层次结构(一直回溯到Object),并在沿途的每个类型的方法集合中查找此方法,之所以能这样回溯,是因为每个类型对象都有一个字段引用了他的积累性,这个信息在途中没有显示。

3、虚方法:当调用一个虚方法时,会生成一些额外的代码,方法每次调用时,都会执行这些代码,这些代码首先检查发出调用的变量,然后会跟随地址(也就是我们说的p中保存的对象的指针)来到发出调用的对象,然后代码检查对象的内部的“类型对象指针”成员,这个成员指向对象的类型对象,然后代码在类型对象的方法集合中查找该方法,进行JIT编译(如果需要的话),在调用JIT编译的代码。如果没有则也会向上回溯查找基类中定义的方法。

下面用示例进行介绍:

class Program
    {
        static void Main(string[] args)
        {

            Person p = new Person("test1");
            p = Person.Find("Aseven");
            int Age = p.GetAge();
            p.Say();
            Console.ReadKey();

        }
    }
    public class Person
    {
        private string _name;
        private int _age;

        public string Name 
        {
            get { return _name; }
            set { _name = value; }
        }
        public virtual void Say()
        {
            Console.WriteLine("******");
        }
        public static Person Find(string name)
        {
            return new Chinese(name);//模拟数据库查找
        }
        public int GetAge()
        {
            return _age;
        }
        public Person() { }
        public Person(string name)
        {
            this._name = name;
        }
    }

    public class Chinese : Person
    {
        public Chinese(string name)
        {
            this.Name = name;
        }
        public override void Say()
        {
            Console.WriteLine("你好!");
        }
    }
    public class American : Person
    {
        public American(string name)
        {
            this.Name = name;
        }
        public override void Say()
        {
            Console.WriteLine("Hello!");
        }
    }
View Code

1、首先我们定义Person对象,Person p=new Person();这句代码执行之后和上面的内存分配基本类似。

2、我们调用Person的静态方法,p=Person.Find("Aseven");根据上面的定义,调用一个静态方法时会直接查找类型对象的方法列表,直接调用,调用之后我们看到Find方法中直接返回了一个Chinese对象,这会在托管堆中创建一个chinese对象,并且把地址存储在变量P中,这时P中保存的不在是Person对象的地址,而是Chinese对象的地址(当然也可能会是一个American对象,如果Find返回返回的是一个American对象)。

3、然后我们调用p.GetAge()的一个非虚实例方法,当CLR调用一个非虚的实例方法时,会根据发出调用者(P)的类型(Person)的类型对象(Person类型对象)去查找GetAge方法,如果找不到则回溯基类查找。这里由于Person的类型对象的方法集合中有这个方法,所以直接调用。将返回的结果(这里是0)存储到线程栈的一个Age的变量中。

4、调用P.Say()方法,Say方法是一个虚方法,CLR会根据发出调用者(P)的地址(这里是指向Chinese对象的一个指针)找到托管堆中真实的对象(chinese对象),然后根据托管堆中的对象去找到真实的类型对象(这里是Chinese类型对象),并且遍历方法集合查找Say方法,(如遇Chinese类重写了Say方法,所以Chinese类型对象的方法集合中有这个方法)进行调用。

这是内存的分配大致如下:由于person对象已经没有其它对象引用了,那么它将是下次垃圾回收的重点对象。

测试Demo

public class A

    {

        public void MethodF() 

        { 

            Console.WriteLine("A.F"); 

        }

        public virtual void MethodG() 

        { 

            Console.WriteLine("A.G"); 

        }

    }

    public class B : A

    {

        new public void MethodF() 

        { 

            Console.WriteLine("B.F"); 

        }

        public override void MethodG() 

        { 

            Console.WriteLine("B.G"); 

        }

    }

    class Test

    {

        static void Main()

        {

            B b;

            b = new B();

            A a = b;

            a.MethodF();

            b.MethodF();

            a.MethodG();

            b.MethodG();

        }
View Code

输出结果:A.F、B.F、B.G、B.G

1、首先MethodF是一个非虚实例方法,这时候我们用a.MethodF();由于a是A类型的实例,所以输出的是A.F。

2、接着调用b.MethodF(),因为b是一个B类型的实例,且B重写了A的MethodF方法,那么在B类型对象的类型对象的方法表中就已经有了MethodF方法,会直接调用,所以输出的是B.F

3、由于MethodG是一个虚方法,我们用a.MethodG调用的时候,首先会根据a中保存的地址(指针)找到托管堆中的具体对象,然后根据具体对象找到真实的类型对象,这里a中保存的是一个b的实例,所以对象的类型对象也就是B的类型对象的类型对象,

     调用的时候则会直接查找B类型对象的类型对象中的方法集合,查找MethodG方法并调用,所以输出的是B.G

4、对于b.MehtodG,首先会根据b中保存的地址(指针)找到托管堆中的具体对象,然后根据具体对象找到真实的类型对象,这里b中保存的是一个b的实例,所以对象的类型对象也就是B的类型对象的类型对象,调用的时候则会直接查找B类型对象的类型对象        中的方法集合,查找MethodG方法并调用,所以输出的是B.G

总结

1、方法的调用都是通过查找类型对象中的方法集合来实现的。

2、静态方法直接查找类型对象中方法集合进行调用。

3、非虚实例方法是根据发出调用者(对于上面的Demo,线程栈中的变量a、b是发出调用者)的类型去查找对应的类型对象,然后查找该类型对象的方法集合进行调用,没有找到则回溯基类进行查找。

4、虚方法是根据发出调用者(对于上面的Demo,线程栈中的变量a、b是发出调用者)的地址找到托管堆中的具体对象,然后根据对象去查找真实的类型对象,再根据类型对象去查找方法集合进行的。

原文地址:https://www.cnblogs.com/skm-blog/p/4176603.html