zoukankan      html  css  js  c++  java
  • 静态方法、实例方法和虚方法的区别

    基础知识

    对于面向对象的语言来说,所有类型都是从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是发出调用者)的地址找到托管堆中的具体对象,然后根据对象去查找真实的类型对象,再根据类型对象去查找方法集合进行的。

  • 相关阅读:
    MySQL 8.0.11安装配置
    MySQL open_tables和opened_tables
    MongoDB 主从和Replica Set
    MySQL各类SQL语句的加锁机制
    MySQL锁机制
    MySQL事务隔离级别
    消除Warning: Using a password on the command line interface can be insecure的提示
    Error in Log_event::read_log_event(): 'Event too small', data_len: 0, event_type: 0
    Redis高可用 Sentinel
    PHP 的异步并行和协程 C 扩展 Swoole (附链接)
  • 原文地址:https://www.cnblogs.com/skm-blog/p/4176603.html
Copyright © 2011-2022 走看看