zoukankan      html  css  js  c++  java
  • 颠覆你对方法调用的看法!

    注意:如果你是一个初学者,对实例方法,虚方法的调用还不太清楚,强烈建议你不要阅读本文,因为这里面的代码会让你完全崩溃掉。

             如果你对实例方法,虚方法的运行机制已经了如指掌,并且,对方法和对象的内存布局也心中有数,那么本文可能会颠覆你以前对他们的认识。

             阅读本文的最佳方式就是亲自演练一下,如果看完之后有疑惑,那么是正常的,但是稍加思考就会想明白。

    我说,string变量可以直接引用一个object对象!

    我说,派生类型的变量可以直接引用基类型的对象!

    你会说,老兄,别开玩笑了,派生类型怎么可以指向一个基类型的对象呢!

    我会让你见证一下奇迹,并在文章的结尾再给你一个更加不可思议的例子。

    首先,请看下面的代码:

        class Program {
            static void Main(string[] args) {
                Derived d=(Derived)new Base();
                d.Print();
                Console.Read();
            }
        }
                class Base {
            public void Print() {
                Console.Write("in base");
            }
        }
    
        class Derived : Base {
            public new void Print() {
                Console.WriteLine("in derived");
            }
        }

    毫无疑问,在运行时一定会抛出一个异常,因为Base对象无法转换为Derived对象。

    但是,现在,我就想让d指向Base对象,并且可以调用Base中的Print方法,该怎么做呢?

    用FiledOffset可以做到这一点,但首先需要定义一个叫做Manager的类,里面包含两个实例字段,一个为Derived,一个为Base。如下:

        [StructLayout(LayoutKind.Explicit)]
        class Manager {
            [FieldOffset(0)]
            public  Base b = new Base();
    
            [FieldOffset(0)]
            public Derived derived;
        }

    现在,通过为b和derived都指定了相同的偏移,所以,b和derived都指向了同一个对象,Base对象。

    由于derived现在指向了Base对象,那么如果我调用d.Print方法,调用的是Base的Printf还是Derived的Print方法,还是抛出一个异常。请看如下代码:

        class Program {
            static void Main(string[] args) {
                Manager m = new Manager();
                m.derived.Print();
                Console.Read();
            }
        }

    运行上面代码,会输出什么呢?

    答案是,“In Derived”。

    这很不可思议,因为derived指向的是Base对象,现在调用的确实Derived的方法。想要了解原因,请看下图:

    这里,尽管derived指向的是一个Base对象,但是,CLR发现Print是一个非虚方法,所以CLR并不关心derived变量指向什么对象,CLR根据derived变量的类型来调用Print方法,这里derived是一个Derived类型,所以CLR会调用Derived中的Print,最终输出In Derived。

    第二个例子:

    下面的这个例子也很不可思议,同样会颠覆你传统的观点。

    让我们将上面的print方法改为virtual方法,最终如下:

     [StructLayout(LayoutKind.Explicit)]
        class Manager {
            [FieldOffset(0)]
            public  Base b = new Base();
    
            [FieldOffset(0)]
            public Derived derived;
        }
    
        class Base {
            public virtual void Print() {
                Console.Write("in base");
            }
        }
    
        class Derived : Base {
            public override void Print() {
                Console.WriteLine("in derived");
            }
        }

    现在,运行如下测试代码:

        class Program {
            static void Main(string[] args) {
                Manager m = new Manager();
                m.derived.Print();
                Console.Read();
            }
        }
            

    这次结果会是什么呢?强烈建议你自己思考答案。

    结果是,In Base!

    是不是及其不可思议!为了更清楚的理解原因,请看下图:

    这里,尽管derived指向的是Base对象,但是,当CLR看到derived.Print这行代码时,由于Print是虚方法,所以CLR会查看derived所指向的Base对象。

    CLR发现Base对象里的type object pointer指向一个Base type object,于是就调用Base Type object中的Print方法,所以最终会输出InBase。

     总结:

    没有总结可不好。

    本质上,子类型是不能引用父类型对象的。但是,我们可以通过FieldOffset绕过这一限制。通过子类型的变量来调用父对象的方法,这很是不可思议,但更不思议的是,当子类型的变量指向父对象时,竟然可以调用子方法!

    那么上面的本质是什么呢?当CLR调用一个非虚方法时,不会关心变量具体指向的是什么,因为CLR此时是通过变量的类型来调用方法。如果方法时虚方法,那么CLR为了实现多态,需要查看这个变量指向的是什么对象,然后在通过对象的type object pointer找到对应的Type Object,然后调用Type Object中的方法。

    修改:

    这篇文章的评论无非有两种,一种是在方法的调用上,一个是在字段的调用上。还有一些表示看不懂的,抱歉,这可能是我表达的不是很清楚。

    首先,方法的调用和实例字段的调用是完全不一样的,注意,我这里说的是实例字段,至于为什么,因为实例字段和方法根本就存在不同的地方,其次,CLR调用方法看的是方法表,而调用字段看的是字段的偏移量,不可相提并论。

    1.实例字段的调用

    首先,请看下面的例子,我保证,这里的结果一定出乎你的意料。

      class Program {
            static void Main(string[] args) {
                Manager m = new Manager();
                Console.WriteLine(m.b.A);
                Console.WriteLine(m.derived.A);
                Console.WriteLine(m.derived.B); //你觉得这个输出会是什么?
           
                Console.Read();
            }
        }
        [StructLayout(LayoutKind.Explicit)]
        class Manager {
            [FieldOffset(0)]
            public Base b = new Base();
    
            [FieldOffset(0)]
            public Derived derived;
        }
    
        class Base {
            public int A = 65537;
            public void Print() {
                Console.WriteLine("in base");
            }
        }
        //注意这里没有了继承关系,一样也可以通过
        class Derived {
            public short A = 2;
            public short B = 2;
            public void Print() {
                Console.WriteLine("in derived");
            }
        }
    复制代码

    你能猜出,结果是什么么?如果猜对了,后面的可以完全跳过。

    答案分别是:65537,1,1

    之所以会出现这个结果,是因为我举了一个非常非常特殊的数65537,他是short的最大值+1。至于为什么举这个数,后面会说。

    原理:

    当调用manager.b.A时,取得就是65537。我想这不用多说。

    当调用manager.derived.A时,取得就是字段的前两个字节,由于我们存的是65537,他比short的最大值大1,所以,前两个字的二进制都是1,所以当你调用manager.derived.A时,值就是65536!!!

    当调用manager.derived.B时,取得就是后两个字节的值,后两个字节的二进制只是一个1,所以,这里的结果是1。

    本质:调用实例字段的本质是根据偏移量来取值。

  • 相关阅读:
    关于sql的对称性密钥和非对称性密钥(基础)
    Thinking in life(1)
    java集合类(三)About Iterator & Vector(Stack)
    java集合类(二)List学习
    How does java technology relate to cloud computing?
    Java 集合类(一)
    Snapchat
    Oppotunity land---China
    Learn know more about big data
    About the Storage allocation
  • 原文地址:https://www.cnblogs.com/francisYoung/p/3371106.html
Copyright © 2011-2022 走看看