注意:如果你是一個初學者,對實例方法,虛方法的調用還不太清楚,強烈建議你不要閱讀本文,因為這里面的代碼會讓你完全崩潰掉。
如果你對實例方法,虛方法的運行機制已經了如指掌,並且,對方法和對象的內存布局也心中有數,那么本文可能會顛覆你以前對他們的認識。
閱讀本文的最佳方式就是親自演練一下,如果看完之后有疑惑,那么是正常的,但是稍加思考就會想明白。
我說,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。
本質:調用實例字段的本質是根據偏移量來取值。