第四章 類型基礎

本章內(nèi)容:

1.所有類型都從System.Object派生
2.類型轉(zhuǎn)換
3.命名空間和程序集
4.運行時的相互關(guān)系

4.1 所有類型都從System.Object派生

“運行時”要求每個類型最終都從System.Object類型派生。以下類型定義完全一致。
class Employee{? ? ? ? ? ? ? ? ? ? }? ? // 隱式派生自Object
class Employee : System.Object {? ? ? ? ? ? ?}? ?//顯式派生自Object
由于所有類型最終都從System.Object派生,所以每個類型的每個對象都保證了一組最基本的方法。


公共方法? ? ? ? ? ? ? ? 說明

Equals? ? ? ? ? ? ? ? ? ? 如果兩個對象具有相同的值,就返回true(詳情5.3.2節(jié))

GetHashCode? ? ? ? 返回對象的值的哈希碼。

ToString? ? ? ? ? ? ? ? ? 默認返回類型的完整名稱(this.GetType().FullName)。但經(jīng)常重寫該方法來返? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?回包含對象狀態(tài)表示的String對象。例如核心類型(Boolean和Int32)重寫該方法? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?來返回它們的值的字符串表示。另外,經(jīng)常出于調(diào)試的目的而重寫該方法;調(diào)? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?用后獲得一個字符串,顯示對象各字段的值。

GetType? ? ? ? ? ? ? ? ?返回從Type派生的一個類型的實例,指出調(diào)用GetType的那個對象是什么類? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?型,返回的Type對象可以和反射類配合,獲取與對象有關(guān)的元數(shù)據(jù)信? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?息。 GetType是非虛方法,目的是防止類重寫該方法,隱瞞其類型,進而破壞? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?類型安全性


此外,從System.Object派生的類型能訪問


受保護方法? ? ? ? ? ? ?????????說明

MemberwiseClone? ? ? ? ? ? 這個非虛方法創(chuàng)建類型的實例,并將新對象的實例字段設與this對象的? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 實例字段完全一致,返回對新實例的引用

Finalize? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?在垃圾回收器判斷對象應該作為垃圾被回收之后,在對象的內(nèi)存被回? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 收之前,會調(diào)用這個虛方法。需要在回收內(nèi)存前執(zhí)行清理工作的類型? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 應重寫該方法。


CLR要求所有對象都有new操作符創(chuàng)建
new操作符所做事情:
1.計算類型及其所有基類型(一直到System.Object,雖然它沒有定義自己的實例字段)中定義的所有實例字段需要的字節(jié)數(shù),堆上每個對象都需要一些額外的成員(overhead成員,或者說”開銷成員“),包括”類型對象指針“(type object pointer)和”同步塊索引“(sync block index)。CLR利用這些成員管理對象,額外成員的字節(jié)數(shù)要計入對象大小。
2.從托管堆中分配類型要求的字節(jié)數(shù),從而分配對象的內(nèi)存,分配的所有字節(jié)都設為0。
3.初始化對象的”類型對象指針“和”同步塊索引“成員。
4.調(diào)用類型的實例構(gòu)造器,傳遞在new調(diào)用中指定的實參。大多數(shù)編譯器都在構(gòu)造器中自動生成代碼來調(diào)用基類構(gòu)造器。每個類型的構(gòu)造器都負責初始化該類型定義的實例字段,最終調(diào)System.Object的構(gòu)造器,該構(gòu)造器什么也不做,簡單的返回。
new執(zhí)行了所有這些操作之后,返回指向新建對象一個引用(或指針)。
沒有和new操作符對應的delete操作符;沒有辦法顯示的釋放對象分配的內(nèi)存,CLR采用了垃圾回收機制,能自動檢測到一個對象不再使用或訪問,并自動釋放對象的內(nèi)存。

4.2 類型轉(zhuǎn)換

CLR最重要的特性之一就是類型安全。在運行時,CLR總是知道對象的類型是什么。調(diào)用GetType方法即可知道對象的確切類型。由于它是非虛方法,所以一個類型不可能偽裝成另一個類型。
開發(fā)人員經(jīng)常需要將對象從一個類型轉(zhuǎn)換為另一種類型。CLR允許將對象轉(zhuǎn)換為它的(實際)類型或者它的任何基類型。以下C#代碼演示了向基類型和派生類型的轉(zhuǎn)換

//該類型隱式派生自System.Object
?internal class Employee{
}
?public sealed class Program{
?????public static void Main(){
?????????//不需要轉(zhuǎn)型,因為new返回一個Employee對象
?????????//而Object是Employee基類型
?????????Object o = new Employee();
?????????//需要轉(zhuǎn)型,因為Employee派生自Object。進行強轉(zhuǎn)。
?????????Employee e = (Employee) o;
?????}
}

這個例子展示了需要做什么,才能讓編譯器順利編譯這些代碼。在運行時,CLR檢查轉(zhuǎn)型操作,確定總是轉(zhuǎn)換為對象的實際類型或者它的任何基類型。例如,以下代碼雖然能通過編譯,但會在運行時拋出InvalidCastException異常:

class Employee{
}
class Manager :Employee{
}
public class Program{
? ? public static void Main(){
????????Manager m = new Manager();
? ? ? ? PromoteEmployee(m);
? ? ? ? DataTime c = new DataTime(2018,2,22);
???????? PromoteEmployee(c); //運行時拋出異常
????}
? ? ? public static void? PromoteEmployee (Object o){
????????//編譯器在編譯時無法準確獲知對象o引用的是什么類型,因此編譯器允許代碼通過編譯
????????//但在運行時,CLR知道了o引用的是什么類型,在每次執(zhí)行轉(zhuǎn)型的時候,會核實對象的
????????//類型是不是Employee類型或者從Employee派生的任何類型
????????Employee e = (Employee) o;
????}
}

使用C#的is和as操作符來轉(zhuǎn)型

4.3 命名空間和程序集

命名空間對相關(guān)的類型進行邏輯分組,開發(fā)人員可通過命名空間方便的定位類型。例如,System.Text命名空間定義了執(zhí)行字符串處理的類型,而System.IO命名空間定義了執(zhí)行IO操作的類型。

public void Main(){
????System.IO.FileStream fs = new? System.IO.FileStream ("...");
????System.Text.StringBuilder sb = new? System.Text.StringBuilder ("...");
}

像這樣寫代碼很繁瑣,應該有一種簡單直接引用FileStream和StringBuilder類型,減少打字量。C#通過using指令提供這個機制。

using? System.IO;
using?? System.Text;
public void Main(){ ???
?????FileStream fs = new? FileStream ("..."); ????
? ? .StringBuilder sb = new? StringBuilder ("...");
}

C#的using指令是可選的,如果愿意,完全可以輸入類型的完全限定名稱。C#的using指令指示編譯器嘗試為類型名稱附加不同的前綴,直至找到匹配項

重要提示 CLR對”命名空間“一無所知。訪問類型是時,CLR需要知道類型的完整名稱(可能是相當長的、包含句點符號的名稱)以及該類型的定義具體在哪個程序集中。這樣”運行時“才能加載正確程序集,找到目標類型,并對其進行操作。

在前面的示例代碼中,編譯器需要保證引用的每個類型都確實存在,而且代碼調(diào)用確實存在的方法,向方法傳遞正確數(shù)量的實參,保證實參具有正確類型,正確使用方法返回值等等。如果編譯器在源代碼文件或者引用的任何程序集中找不到指定名稱的類型,機會再類型名稱附加System.IO.前綴,檢查這樣生成的名稱是否與現(xiàn)有類型匹配。如果任然找不到匹配項,就繼續(xù)為類型名稱附加System.Text.前綴。在前面例子中的兩個using指令的幫助下,只需要在代碼中輸入FileStream和StringBuilder這兩個簡化的類型名稱,編譯器會自動將引用展開成System.IO.FileStream和System.Text.StringBuilder。

檢查類型定義時,編譯器必須要知道在什么程序集中檢查。第2.3章講過,這通過/reference編譯器開關(guān)實現(xiàn)。百年一起掃描引用的所有程序集,在其中查找類型定義,一旦找到正確的程序集,程序集信息和類型信息就嵌入生成的托管模塊的元數(shù)據(jù)中,為了獲取程序集信息,必須將定義了被引用類型的程序集傳給編譯器。c#編譯器自動在MSCorLib.dll程序集中查找被引用類型,即使沒有顯示告訴它要這么做,MSCorLib.dll程序集包含所有核心Framework類型(FCL)類型定義。

4.4 運行時的相互關(guān)系

圖4-2展示了已加載CLR的一個Windows進程。該進程可能有多個線程。線程創(chuàng)建時會分配到1MB的棧。棧空間用于向方法傳遞實參,方法內(nèi)部定義的局部變量也在棧上。圖4-2展示了線程的棧內(nèi)存(右側(cè))。棧從高位內(nèi)存向低位內(nèi)存地址構(gòu)建。圖中線程已執(zhí)行了一些代碼,棧上已經(jīng)有一些數(shù)據(jù)了?,F(xiàn)在,假定線程執(zhí)行的代碼要調(diào)用M1方法。

圖 4-2 一個線程的棧,當前準備調(diào)用M1方法

最簡單的方法包含”序幕“(prologue)代碼,在方法開始做工作前對其進行初始化;還包含”尾聲“(epilogue)代碼,在方法做完工作后對其進行清理,以便返回至調(diào)用者。M1方法開始執(zhí)行時,它的序幕代碼在線程棧上分配局部變量name的內(nèi)存,如圖4-3所示。


圖4-3 在線程棧上分配M1的局部變量

然后,M1調(diào)用M2方法,將局部變量name作為實參傳遞。這造成name局部變量中的地址被壓入棧(參見圖4-4)。M2方法內(nèi)部使用參數(shù)變量s標識棧位置(注意,有的CPU架構(gòu)用寄存器傳遞實參以提高性能,但這個區(qū)別對于當前討論來說并不重要)。另外,調(diào)用方法時還會將”返回地址“壓入棧。被調(diào)用的方法在結(jié)束之后應返回至該位置。


圖4-4 M1調(diào)用M2時,將實參和返回地址壓入線程棧

M2方法開始執(zhí)行時,它的序幕代碼在線程棧中為局部變量length和tally分配內(nèi)存,如圖4-5所示。然后,M2方法內(nèi)部的代碼開始執(zhí)行。最終,M2抵達它的return語句,造成CPU的指令指針被設置成棧中的返回地址,M2的棧幀展開,恢復成圖4-3的樣子。之后,M1繼續(xù)執(zhí)行M2調(diào)用之后的代碼,M1的棧幀將準確反映M1需要的狀態(tài)。

棧幀:代表當前線程的調(diào)用棧中的一個方法調(diào)用。執(zhí)行線程的過程中,進行的每個方法調(diào)用都會在調(diào)用棧中創(chuàng)建并壓入一個StackFrame。

展開:unwind一般翻譯成“展開”,但這并不是一個很好的翻譯。wind和unwind源于生活。把線纏到線圈上稱為wind,從線圈松開成為unwind。同樣地,調(diào)用方法時壓入棧幀,成為wind,方法執(zhí)行完畢,彈出棧幀,稱為unwind。把這幾張圖的線程看成一個線圈,就很容易理解了。


圖 4-5 在線程棧上分配M2的局部變量

最終,M1會返回到它的調(diào)用者。這同樣通過將CPU的指令指針設置成返回地址來實現(xiàn)(這個返回地址在圖中未顯示,但它應該在棧中的name變量上方),M1的棧幀展開,恢復成圖4-2的樣子。之后,調(diào)用M1的方法將繼續(xù)執(zhí)行M1調(diào)用之后的代碼,那個方法的棧幀將準確放映它需要的狀態(tài)。


圍繞CLR討論。如下類定義

class Employee{
public Int32 GetYearsEmployed() {...}
public virtual String GetProgressReport() {...}
public static Employee Lookup(String name) {...}
}
class Manager : Employee{
public override String? GetProgressReport() {...}
}

Windows進程已啟動,CLR已加載到其中,托管堆已初始化,而且已創(chuàng)建一個線程。線程已執(zhí)行了一些代碼,馬上調(diào)用M3方法。狀態(tài)如圖4-6。


圖 4-6 CLR已加載到進程中,堆已初始化,線程棧已創(chuàng)建,馬上調(diào)用M3方法

JIT編譯器將M3的IL代碼轉(zhuǎn)換成本機CPU指令時,會注意到M3內(nèi)部引用的所有類型,包括Employee,Int32,Manager,String。這時CLR要確認定義了這些類型的所有程序集都已加載。然后。利用程序集的元數(shù)據(jù),CLR提取與這些類型有關(guān)的信息,創(chuàng)建一些數(shù)據(jù)結(jié)構(gòu)來表示類型本身。圖4-7展示了Employee和Manager類型對象使用的數(shù)據(jù)結(jié)構(gòu)。


圖 4-7 Employee和Manager類型對象在M3被調(diào)用時創(chuàng)建

本章前面講過,堆上所有對象都包含兩個額外成員:類型對象指針和同步塊索引。如圖,Manager和Employee都有這兩個成員。定義類型時,可在類型內(nèi)部定義靜態(tài)數(shù)據(jù)字段。為這些靜態(tài)數(shù)據(jù)字段提供資源的字節(jié)在類型對象自身中分配。每個類型對象最后都包含一個方法表。在方法表中,類型定義的每個方法都有對應的記錄項。Employee定義了三個方法,所有方法表有三個記錄項。Manager中有一個記錄項。

當CLR確認方法所需要的所有類型對象都已創(chuàng)建,M3的代碼編譯完成后,就允許線程執(zhí)行M3的本機代碼。M3的序幕代碼執(zhí)行時必須在線程棧中為局部變量分配內(nèi)存,如圖4-8所示。CLR自動將所有局部變量初始化為null或0。(試圖訪問未顯示初始化的局部變量,會報錯)


圖 4-8 在線程棧上分配M3的局部變量

然后,M3執(zhí)行代碼創(chuàng)建了一個Manager對象。這造成在托管堆上創(chuàng)建Manager類型的一個實例(也就是一個Manager對象),如圖4-9所示。


圖 4-9 分配并初始化Manager對象

可以看出,和所有對象一樣,Manager對象也有類型對象指針和同步塊索引。該對象還包括必要的字節(jié)來容納Manager類型定義的所有實例字段,以及容納由Manager任何基類定義的所有實例字段。任何時候在堆上新建對象,CLR都自動初始化內(nèi)部的“類型對象指針”成員來引用和對象對應的類型對象。此外,在調(diào)用類型的構(gòu)造器(本質(zhì)是可能修改某些實例字段的方法)之前,CLR會先初始化同步塊索引,并將對象的所有實例字段設為null或0.new操作符返回Manager對象的內(nèi)存地址,該地址就保存到變量e中。

M3的下一行代碼是調(diào)用Employee的靜態(tài)方法Lookup。調(diào)用靜態(tài)方法時,CLR會定位與定義靜態(tài)方法的類型對應的類型對象。然后,JIT編譯器在類型對象的方法表中查找與被調(diào)用方法對應的記錄項,對方法進行JIT編譯,再調(diào)用JIT編譯好的代碼。在Lookup方法內(nèi)部,假設參數(shù)“chen”在數(shù)據(jù)庫查詢到是Manager,就在堆上構(gòu)造一個新的Manager對象,返回該對象的地址。該地址保存到局部變量e中。如圖4-10。


圖 4-10 Employee的靜態(tài)方法Lookup初始化Manager對象

注意,e不再引用第一個Manager對象。事實上,由于沒有變量引用該對象,所以它是垃圾回收的主要目標,垃圾回收機制將自動回收(釋放)該對象占用的內(nèi)存。

M3的下一行代碼調(diào)用Employee的非虛實例方法GetYearsEmployee。調(diào)用非虛實例方法時,JIT編譯器會找到與“發(fā)出調(diào)用的那個變量(e)的類型(Employee)”對應的類型對象(Employee類型對象)。這時的變量e被定義成一個Employee。如果Emoloyee類型沒有定義正在調(diào)用的那個方法,JIT編譯器會回溯類層次結(jié)構(gòu)(一直回溯到Object),并在沿途的每個類型中查找該方法。之所以能這樣回溯,是因為在每個類型對象都有一個字段引用了它的基類型,這個信息在圖中沒有顯示。

然后,JIT編譯器在類型對象的方法表中查找引用了被調(diào)用方法的記錄項,對方法進行JIT編譯,再調(diào)用JIT編譯好的代碼。本例假定方法返回值為5,這個整數(shù)就會保存到局部變量year中。如圖4-11.


圖 4-11 Employee的非虛實例方法GetYearsEmployee調(diào)用后返回5

M3的下一行代碼是調(diào)用Employee的虛實例方法GetProgressReport。調(diào)用虛實例方法時,JIT編譯器要在方法中生成一些額外的代碼;方法每次調(diào)用都會執(zhí)行這些代碼。這些代碼首先檢查發(fā)出調(diào)用的變量(e),并跟隨地址來到發(fā)出調(diào)用的對象。變量e當前引用的是代表“chen”的Manager對象。然后,代碼檢查對象內(nèi)部的“類型對象指針”成員,該成員指向?qū)ο蟮膶嶋H類型。然后,代碼在類型對象的方法表中查找引用了被調(diào)用方法的記錄項,對方法進行JIT編譯,再調(diào)用JIT編譯好的代碼。由于e引用一個Manager對象,所以會調(diào)用Manager的GetProgressReport實現(xiàn)。如圖4-12.


圖 4-12 調(diào)用Employee的虛實例方法GetProgressReport,最終執(zhí)行Manager重寫的版本

注意,如果Employee的Lookup方法發(fā)現(xiàn)chen是Emoloyee而不是Manager,Lookup在內(nèi)部構(gòu)造一個Employee對象,它的類型指針將引用Employee類型對象,最終執(zhí)行的也將是Employee的GetProgressRrport實現(xiàn),而不是Manager的。

Employee類型對象和Manager類型對象都包含“類型對象指針”成員,這是由于類型對象本質(zhì)上也是對象。CLR創(chuàng)建類型對象時,必須初始化這些成員,初始化成什么呢?CLR開始在一個進程中運行時,會立即為MSCorLib.dll中定義的System.Type類型創(chuàng)建一個特殊的類型對象。Employee和Manager類型對象都是該類型的“實例”。因此,它們的類型對象指針成員會初始化成對System.Type類型對象的引用,如圖4-13.


圖 4-13 Employee和Manager類型對象是System.Type類型的實例

當然,System.Type類型對象本身也是對象,也有類型對象指針成員。這個指針指向它本身,因為System.Type類型對象本身是一個類型對象的“實例”。

System.Object的GetType方法返回存儲在指定對象的"類型對象指針“成員中的地址。也就是說,GetType方法返回指向?qū)ο蟮念愋蛯ο蟮闹羔?。這樣就可以判斷系統(tǒng)中任何對象(包括類型對象本身)的真實類型。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,692評論 18 399
  • 大概每個人的心中,都有一個地方。在這里,什么也不用去想,什么也不用猜,沒有憂慮,沒有煩惱,在這里我們還是那個小孩...
    沒事做個夢吧閱讀 323評論 0 1
  • 跟很多人一樣,心里有一個夢想是周游世界,看各種風景,品五味人生。 前幾年, “世界那么大,我想去看看”,一張辭職單...
    云在波心閱讀 611評論 2 4
  • 出發(fā)上班路上,發(fā)現(xiàn)一片銀杏樹葉,覺得很搭畫,然后隨手放在了花壇上擺拍了一張。這次頭發(fā)畫得還算是有進步的,準備這幾天...
    夏暖心閱讀 225評論 0 4
  • 有太多雙手抓不住的東西
    天天的哈哈笑閱讀 187評論 0 0

友情鏈接更多精彩內(nèi)容