[CLR via C#]4. 型別基礎及型別、物件、棧和堆執行時的相互聯絡

weixin_34377065發表於2014-06-11
原文:[CLR via C#]4. 型別基礎及型別、物件、棧和堆執行時的相互聯絡

  CLR要求所有型別最終都要從System.Object派生。也就是所,下面的兩個定義是完全相同的,

//隱式派生自System.Object
class Employee {
    .....
}
//顯示派生子 System.Object
class Employee : System.Object {
  .....  
}

  由於所有型別最終都是從System.Object派生的,所以可以保證每個型別的每個物件都有一組最基本的方法。

  System.Object提供瞭如下所示的公共例項方法。  

Equals(Object) 確定指定的物件是否等於當前物件。如果兩個物件具有相同值就返回ture.
GetHashCode 返回物件的值得一個雜湊碼。如果某個型別的物件要在雜湊表集合中作為key使用,該型別應該重寫這個方法。方法應該為不同的物件提供一個良好的分佈。
ToString 該方法預設返回型別的完整名稱(this.GetType().FullName)。
GetType 返回從Type派生的一個物件的例項,指出呼叫GetType的那個物件是什麼型別。返回的Type型別可以與反射類配合使用,從而獲取與物件的型別相關的後設資料資訊。

  

  System.Object的受保護方法  

MemberwiseClone 這個非虛方法能建立型別的一個新例項,並將物件的例項欄位設為與this物件的例項欄位完全一致。返回的是對新例項的一個引用
Finalize 在垃圾回收器判斷物件應該被作為垃圾收集之後,在物件的記憶體被實際回收之前,會呼叫這個虛方法。需要在回收之前執行一些清理工作的型別應該重寫這個方法。

  

  CLR要求所有物件都是用new操作符來建立。比如  

Employee e = new Employee("ConstructorParam1");

  以下是new操作符所做的事情:

  1)它計算型別及其所有基型別(一直到System.Object)中定義的所有例項需要的位元組數。堆上的每個物件都需要一些額外的開銷成員——"型別物件指標(type object pointer)"和"同步塊索引"(sync block index)。這些成員由CLR用於管理物件。這些額外成員的位元組數會計入物件大小。

  2)它從託管堆中分配指定型別要求的位元組數,從而分配物件的記憶體,分配的所有位元組都設為零(0)。

  3)它初始化物件的"型別物件指標"和"同步塊索引"成員。

  4)呼叫型別的例項構造器,向其傳入對new的呼叫中指定的任何實參(本例中是"ConstructorParam1")。大多數編譯器都在構造器中自動生成程式碼來呼叫一個基類的構造器。每個型別的構造器在被呼叫時,都要負責初始化這個型別定義的例項欄位。最後呼叫的是System.Object的構造器,該構造器只是簡單的返回,不會做其它任何事情。

  new 執行了所有的操作後,會返回執行新建物件的一個引用。在本例中,這個引用會儲存到變數e中,具有Employee型別。

  注意:上面提到過"型別物件指標",型別物件不是型別的物件/例項,這兩者是有區別的。

  ----------------------------------------------------------------------------------

  CLR最重要特性之一就是型別的安全性。在執行時,CLR始終知道一個物件的型別,可以呼叫GetType方法,得到物件的型別。

  CLR允許將一個物件轉換為它的實際型別或者它的任何基型別。

  C#不要求使用特殊語法即可將一個物件轉換為它的任何及型別,因為向基型別的轉換被認為是一種安全的隱式轉換。但是,將物件轉換為它的某個派生類時,C#要求開發人員只能進行顯示轉換,因為這樣的轉換在執行時可能失敗。

   public static void Main() {
      // 不需要轉型
      Object o = new Employee();
 
      // 需要進行強制型別轉換
      Employee e = (Employee) o;
   }

  在C#語言中進行型別轉換的另一種方式是使用is操作符。is操作符檢查一個物件是否相容指定的型別,並返回一個Boolean值(true和false)。注意,is操作符是不會返回異常資訊的。

  is操作符通常這樣使用:

  if ( o is Employe ){
       Employee e = (Employee) o;
  }

  在這段程式碼中,CLR實際是會檢查兩次物件的型別。is操作符首先核實o是否相容Employee型別。如果是,在if內部,CLR還會再次核實o是否引用一個Employee。CLR的型別檢查增強的安全性,但無疑也會對效能造成一定影響。

  C#專門提供了 as 操作符,目的就是簡化這種程式碼的寫法,同時提升效能。

  as操作符通常這樣使用:

  Employee e = o as Employee;
  if ( e != null ){
      //在if中使用e
  }
  as操作符的工作方式與強制型別轉換一樣,只是它是不會丟擲異常的,如果不能轉化,結果就是null。所以,正確的做法就是檢查最終生成的引用是否為null。如果企圖直接使用轉換後的引用,就會丟擲異常。
 
----------------------------------------------------------------------------------
  
   名稱空間(namespace)用於對相關的型別進行邏輯分組,開發人員使用名稱空間來方便的定位一個型別。

  名稱空間和程式集不一定是相關的,也就是說它們之間沒有必然聯絡。

----------------------------------------------------------------------------------

  現在將解釋型別、物件、執行緒棧和託管堆在執行時的相互聯絡。此外,還將解釋呼叫靜態方法、例項方法和虛方法的區別。

  我們先從執行緒棧開始。

  1. 圖4-2展示了已載入了CLR的一個Windows程式。在這個程式中,可能存在多個執行緒。一個執行緒建立時,會分配到一個1MB大小的棧。這個棧的空間用於向方法傳遞實參,並用於方法內部定義的區域性變數。圖4-2展示了一個執行緒的棧記憶體(右側)。棧是從高地址向低地址構建的。在圖中,執行緒已執行了一些程式碼,現在,假定執行緒開始執行的程式碼要呼叫M1方法了。

  

  2. 在一個最基本的方法中,會有一些"序幕"程式碼,負責在方法開始時做它工作之前對其進行初始化。另外,還包括了"尾聲"程式碼,負責在方法完成工作之後對其進行清理,然後才返回至呼叫者。M1方法開始執行時,它的"序幕"程式碼就會線上程棧上分配區域性變數name的記憶體,如圖4-3所示。

  

  3. 然後,M1呼叫M2的方法,將區域性變數name作為一個實參來傳遞。這造成name區域性變數中的地址被壓入棧(參見圖4-4)。在M2方法內部,將使用名為s的引數變數來標識棧位置(有的CPU架構會通過暫存器來傳遞實參,以提高效能)。另外,呼叫一個方法時,還會將一個"返回地址"壓入棧中。被呼叫的方法在結束後,應該返回到這個位置(同樣參見圖4-4)。

  

  4. M2的方法開始執行時,它的"序幕"程式碼就是線上程棧中為區域性變數length和tally分配記憶體。如圖4-5所示。
  
  5. 然後,M2方法內部的程式碼開始執行。最後,M2抵達它的return語句,造成CPU的指令指標被設定成棧中的返回地址,而且M2的棧幀會展開,
使之看起來類似於圖4-3。之後,M1將繼續執行在M2呼叫之後的程式碼,M1的棧幀將準確反映M1需要的狀態。
   
  6. 最後,M1會返回到它的呼叫者。同樣的是通過CPU的指令指標設定成返回地址來實現的(這個返回地址在圖中未顯示,但它應該剛好在棧中的name實參上方),而且M1的棧幀會展開,使之看起來類似於圖4-2。之後,呼叫了M1的方法會繼續執行在M1之後的程式碼,那個方法的棧幀將準確反映它需要的狀態。
  
  CLR運作關係
  1. 假定現在有以下兩個類的定義:
  
internal class Employee {
    public               int32         GetYearsEmployed()       { ... }
    public    virtual    String        GenProgressReport()      { ... }
    public    static     Employee      Lookup(String name)      { ... }     
}
internal sealed class Manager : Employee {  
    public    override   String         GenProgressReport()    { ... }
}     

  2. 我們的Windows程式已啟動,CLR已載入到其中,託管堆已初始化,而且已建立一個執行緒(連同它的1MB的棧空間)。該執行緒已執行了一些程式碼,現在馬上就要呼叫M3的方法。圖4-6展示了目前的狀況。M3方法包含的程式碼演示了CLR是如何工作的。

   

  3. 當JIT編譯器將M3的IL程式碼轉換成為本地CPU指令時,會注意到M3的內部引用的所有型別:Employee、Int32、Manager以及String(因為"Joe")。這個時候,CLR要確保定義了這些型別的所有程式集都已經載入。然後,利用這些程式集的後設資料,CLR提取與這些型別有關的資訊,並建立一些資料結構表示型別本身。圖4-7展示了為Employee和Manager型別物件使用的資料結構。由於這個執行緒在呼叫M3之前已經執行了一些程式碼,所有不妨假定Int32和String型別物件已經建立好了,所以圖中沒有顯示它們。
   
  4. 先前提過,堆上的所有物件上都包含兩個額外的成員:"型別物件指標"和"同步塊索引"。如圖4-7所示,Employee和Manager型別物件都有這兩個成員。定義一個型別時,可以在型別的內部定義靜態資料欄位。為這些靜態欄位資料提供支援的位元組是在型別物件自身中分配到的。在每個型別物件中,都包含一個方法表。在方法表中,型別中定義的每個方法都有一個對應的記錄項。由於Employee有3個方法就有3個記錄項,Manager只有一個方法,也就只有一個記錄項。
  
   
  5. 現在,當CLR確定方法需要的所有型別物件都已經建立了,而且M3的程式碼也已經編譯好了,就允許執行緒開始執行M3的原生程式碼。M3的"序幕"程式碼執行時,必須線上程棧中為區域性變數分配記憶體,如4-8所示。作為方法的"序幕"程式碼的一部分,CLR會自定將所有區域性變數初始化為null或零(0)。
   
  6. 然後,M3執行它的程式碼來構造一個Manager物件。這就會在託管堆中建立Manager型別的一個例項(也就是Manager物件)。如4-9所示。和所有物件一樣,Manager物件也有一個"型別物件指標"和"同步塊索引"。該物件還包含必要的位元組來容納Manager型別定義的所有例項資料欄位,以及容納由Manager的任何基類(Employee和Object)定義的所有例項欄位。任何時候在堆上新建一個物件,CLR都會自動初始化內部"型別物件指標",讓它引用(或指向)與物件對應的型別物件(本例就是Manager型別物件)。此外,CLR會先初始化"同步塊索引",並將物件的所有例項欄位設為nll或為零(0),在呼叫型別的構造器(它本質上是可能修改某些例項欄位的一個方法)。new操作符會返回Manager物件的記憶體地址,該記憶體地址儲存在變數e中(e線上程棧上)。
   
  7. M3的下一行程式碼呼叫Employee的靜態方法Lookup。呼叫一個靜態方法時,CLR會定位到與定義靜態方法的型別對應的型別物件。然後,JIT編譯器在型別物件的方法表中查詢被呼叫的方法對應的記錄項,對該方法進行JIT編譯(如果需要的話),再呼叫JIT編譯後的程式碼。就本例,假定Enployee的Lookup方法要查詢資料中的Joe。另外,假定資料庫中指出Joe是為Manager,所以在內部,Lookup方法在堆上構造一個新的Manager物件,用Joe的資訊初始化它,然後返回該物件的地址。這個地址儲存在區域性變數e中。如圖4-10所示。值得注意的是,e不再引用第一個Manager物件。事實上,由於沒有變數引用第一個Manager物件,所以它是將來進行垃圾回收時的主要目標。
  
  8. M3的下一行呼叫Employee的非虛例項方法GetYearsEmployed。呼叫一個非虛例項方法時,JIT編譯器會找到與"發出呼叫的那個變數(e)的型別(Emplyee)"對應的型別物件(Employee型別物件)。在本例中,變數e被定義成為一個Employee。如果Employee型別沒有定義這個方法,JIT編譯器會回溯類層次結構(一直到Object),並在沿途的每個型別中查詢該方法。之所以能這樣回溯,是因為每個型別物件都有一個欄位引用了它的基型別,但在圖中沒有顯示。然後,JIT編譯器在型別物件的方法表中查詢引用了被呼叫方法的記錄項,對方法進行JIT編譯(如果需要的話),再呼叫JIT編譯後的呼叫。在本例中,假定Employee的GetYearsEmployed方法返回5,。這個整數就儲存在區域性變數year中。如圖4-11所示。
  
  9. M3的下一行程式碼呼叫Empolyee的虛例項方法GenProgressReport。呼叫一個虛例項方法時,JIT編譯器要在方法中生成一些額外程式碼;方法每次呼叫時,都會執行這些程式碼。這些程式碼首先檢查發出呼叫的變數,然後跟隨地址來到發出呼叫的物件。在本例中,變數e引用的是代表"Joe"的一個Manager物件。然後,程式碼檢查物件內出的"型別物件指標"成員,這個成員指向物件的實際型別。然後,程式碼在型別物件的方法表中查詢引用了被呼叫方法的記錄項,對方法進行JIT編譯(如果需要的話),再呼叫JIT編譯後的程式碼。在本例中,由於e引用了一個Manager物件,所以會呼叫Manager的GenProgressReport實現。如圖4-12所示。
  
  總結:
  注意,在Employee和Manager型別物件都包含"型別物件指標"成員。這是由於型別物件本質也是物件。CLR建立型別物件時,必須初始化這些成員。初始化成什麼呢?CLR開始在一個程式中執行時,會立即為MSCOrLib.dll中定義的System.Type型別建立一個特殊的型別物件。Employee和Manager型別物件都是該型別的"例項".因此,它們的型別物件指標成員會初始化成對System.Type型別物件的引用。如圖4-13。
  
  當然,System.Type型別物件本身也是一個物件,內部也有一個"型別物件指標"成員。那麼這個指標指向的是什麼呢?它指向它本身,因為System.Type型別物件本身就是一個型別物件的"例項"。
  現在,我們總算理解了CLR的整個型別系統及其工作方式。System.Object的GetType方法返回的是儲存在指定物件的"型別物件指標"成員中的地址。也就是說,GetType方法返回的是指向物件的型別物件的一個指標。這樣一來,就可以判斷系統中任何物件(包括型別物件本身)的真實型別。
  
 
 
 
 

GetType

相關文章