顛覆你對方法調用的見解!

注意:若是你是一個初學者,對實例方法,虛方法的調用還不太清楚,強烈建議你不要閱讀本文,由於這裏面的代碼會讓你徹底崩潰掉。ide

         若是你對實例方法,虛方法的運行機制已經瞭如指掌,而且,對方法和對象的內存佈局也心中有數,那麼本文可能會顛覆你之前對他們的認識。佈局

         閱讀本文的最佳方式就是親自演練一下,若是看完以後有疑惑,那麼是正常的,可是稍加思考就會想明白。測試

 

我說,string變量能夠直接引用一個object對象!spa

我說,派生類型的變量能夠直接引用基類型的對象!code

你會說,老兄,別開玩笑了,派生類型怎麼能夠指向一個基類型的對象呢!對象

 

我會讓你見證一下奇蹟,並在文章的結尾再給你一個更加難以想象的例子。blog

 

首先,請看下面的代碼:繼承

    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方法,該怎麼作呢?ci

 

用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。

本質:調用實例字段的本質是根據偏移量來取值。

相關文章
相關標籤/搜索