1.概覽
較之以往任何一種開發語言來說,.NET在類型系統上的創新設計都是無與倫比的。強大的通用類型系統CTS(Common Type System)奠定了整個.NET體系的基石。這套類型系統是貫穿於.NET Framework和各種中間語言之間的。因此需要從兩個方面來理解.NET的類型系統。
總體來說,.NET的類型是一種完全的面向對象的類型。它由最底層的object類型開始,逐步擴展,上面再分支為值類型Value Type以及引用類型Reference Type。由值類型由分支出基礎值類型、用戶定義值類型以及枚舉類型。由引用類型分支出自描述類型、指針類型、接口類型。由自描述類型又分支出類類型、數組類型等。
下圖展示了.NET的類型體系分支:
這是一基於自演化的體系,由一個根類型逐漸分支。其結構體系完全符合自然發展規律,符合面向對象的思想。這種思想早在中國古代經典著作中就奠定了理論基礎,在《易經》中提到這樣的思想:太極生兩儀、兩儀生四象、四象生八卦、八卦演萬物。而.NET的類型體系正是符合這種發展的思想觀。它所帶來的優勢是不言而喻的:
架構清晰
整個樹形架構劃分明確,便於程序的設計,便於理解。
通用性強
這種明確的類型系統有效的保證了.NET實現的多語言開發,中間語言轉換,統一編譯的特性。
便於檢測
正是基於它清晰的架構,便於在程序出現錯誤時,按不同的類型需求檢測錯誤。
擴展性好
統一的設計保證了類型的可擴展性。
2.從源頭說起
前面已提到過,.NET類型系統全部來源於一個統一的基礎即System.Object類型。它定義於.NET Framework下,在C#中對應的類型為object類型。.NET實行了一種語言架構分離的機制,它的基本類型並沒有定義於語言中,而是內置在.NET Framework內。這樣的設計進一步保障了公共語言系統的成功及工作效率。而我們在語言中也可以方便的使用助記符來代替,例如System.Object在語言中可使用object替換,System.Int32在語言中可使用int替換。值得一提的是,這種替換名字雖有細小的差別,但仍是基於基本類型的。因此並不像網上某些文章提到的會損失性能。因為其在代碼編譯之前就會在MSIL中完成類型的轉換。請看以下示例。
static void Main(string[] args)
{
int intA = 123;
Console.WriteLine(intA.ToString());
}
這段程序描述了一個int型變量的定義。當該腳本轉換成IL後,其代碼如下:
其中紅色區域為int型變量在轉換後的類型,由此可見,它仍是.NET Framework中定義的基本類型。
System.Object中擁有幾個最基本的方法,包括實例方法:
靜態方法:
這幾個簡單的方法為object所有的分支類型所共有。其中使用最普遍的就是ToString()方法。用於返回對象的字符串形式。在調試程序時,經常會運用它來判斷當前對象是否正確。獲得當前值。< /p>
Equals和ReferenceEquals用於對對象的實例進行相等比較。
GetType:用於運行時獲取對象的運行時類型。
GetHashCode用於獲取對象的散列碼。
除此之外還包括了MemberwishClone方法,用於實現對象實例的淺拷貝,它是base基類中的一個受保護級別的方法。
最後還有一個非常特殊的Finalize方法,用於垃圾回收時處理資源的清理工作。該方法無法在自定義類中顯示重寫。要實現它,只需為類定義析構函數即可。但要注意,Finalize對系統的開銷非常大,因此請盡量少的使用它。
3.從內存結構談起
以上簡單的介紹了.NET類型系統的劃分和設計基礎。但要真正了解.NET類型的細節問題,就需要弄清楚類型在內存中的表現形式。因為類型最重要的作用,即它的核心價值就在於為應用程序的各個元素開辟相應的內存空間,指定其運行的位置。合理分配的內存空間可保證程序穩定有序的運行,也是決定程序性能的一項硬指標。
.NET類型系統的設計源自JAVA,其數據在內存的存儲區域被劃分成兩個不同的部分,堆棧區(Strack)和托管堆(Manage Heap),堆棧區用於存儲值類型,而引用類型則依賴於托管堆。這個過程是這樣進行的:
在32位的操作系統上,當用戶執行編譯好的應用程序時,操作系統會在內存中為程序創建一個進程,同時為其分配4GB的內存空間(此空間是通過內存地址映射實現的虛擬空間),這塊空間即為托管堆區,一般引用類型的實際數據都存儲在此,而在堆棧上存儲的則是引用類型的地址指針。
3.1值類型
而對於值類型來說,通常是存儲於線程的堆棧上。堆棧是一種先進後出,並從高地址向低地址擴展的數據結構,它是一塊連續的內存的區域。在系統分配時會被指定大小。若存儲的數據超出了這塊指定區域就會發生“溢出”錯誤。下圖表明了堆棧在內存中的存儲結構。< /p>
這個概念非常重要,理解了這一點,在後面談到數據類型轉換時的重重問題就可以迎刃而解了。打一個不恰當的比喻來說,堆棧就好比酒店內的房間,不同類型、不同數量的客人被安排在不同大小的房間內,有單間、雙人間、三人間、豪華間還有總統套房。酒店前台會根據客人的不同需要進行分配。這裡的酒店前台好比堆棧中的地址指針。此指針指向堆棧中下一個自由地址空間。
下面的程序使用.NET的指針,定義了3個int型變量,分別獲取它們的內存地址和值,從結果可以看出,值類型的內存分配方式:
using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApplication5
{
class Program
{
unsafe static void Main(string[] args)
{
int a = 1;
int* addInt = &a;
Console.WriteLine("指針地址為:{0},內容為{1}。", (uint)addInt, addInt->ToString());
byte b = 2;
byte* addByte = &b;
Console.WriteLine("指針地址為:{0},內容為{1}。", (uint)addByte, addByte->ToString());
decimal c = 3m;
decimal* addDecimal = &c;
Console.WriteLine("指針地址為:{0},內容為{1}。", (uint)addDecimal, addDecimal->ToString());
bool d = true;
bool* addBool = &d;
Console.WriteLine("指針地址為:{0},內容為{1}。", (uint)addBool, addBool->ToString());
}
}
}
運行結果如下:
這個程序很好的表明了值類型在地址中是如何進行存儲的。4個值從第1242220的高地址位開始一直向低地址位延伸。每次根據數據類型的不同分配不同長度的內存單元,用於存儲所需數據。當然地址的起始位置是根據系統當前的資源情況而分配的。
另外我們還需要了解的是值類型的作用域也有嚴格的規定。值類型的作用域被規范在一個代碼塊中,例如上面的例子程序中,a、b、c、d四個變量的作用域就只在main主函數中存在,當程序運行到主函數的最後一個}符號時,四個變量被依次釋放內存,這一操作是由系統自動完成的,並不需要人為去進行干預。< /p>
當然有些情況下值類型的作用域也被延伸。例如使用ref或out來按引用傳遞參數時,值類型的作用域則可被擴展到程序塊之外。
3.2引用類型
說完值類型,讓我們再回到引用類型上。首先要了解,為什麼需要引用類型。實際上,相對於引用類型來說,值類型的執行效率要高得多,並且後者的內存開銷也要比引用類型小。那麼是不是僅需要它就行啦?我們前面也談到,類型的核心價值就是提高程序的性能,這樣看來引用類型似乎是違背了這一原則。
事實是,我們不僅需要引用類型,而且它的作用往往比值類型顯得更加重要。因為值類型雖使用簡便,但最大的缺點就是受到語句塊作用域的限制,並且只能存儲一些小的數據類型,以至於使用上欠缺靈活性。而引用類型克服了這些缺點,首先是它的存儲位置被分為兩個區域。它在堆棧上聲明並被分配空間,但此空間存儲的僅是實際數據在托管堆中的地址的引用。真正的數據被存儲在托管堆中,托管堆的內存存放類似於堆棧,但它有一個專門的工具來負責內存的清理工作,這個工具就是垃圾收集器GC。垃圾收集器會定期檢查堆棧中的數據占用情況,若發現不用的對象(有一種算法來負責),或用戶提出了申請,則開動GC,回收內存中相應的資源。
引用類型使用運算符new進行創建,方法如下:
Test test = new Test();
Test是類型的名稱,這裡可以是用戶自定義類型、也可以是系統內置類型。這行語句與普通的值類型定義相比僅是等號右邊有所不同,但它本身包含了以下幾個步驟。
3.2.1 聲明類型,在堆棧開辟內存空間
等號左邊和值類型一樣,首先指明了數據的類型為Test類型,此時編譯器將會在同一命名空間下查找是否存在Test類型,若沒有則在引用中查找是否有using指向不同命名空間下的這一對象,若不存在則返回一個錯誤提示:“找不到類型或或命名空間名稱”。當然這一步驟會在源代碼編譯前就完成。但也有一種情況就是編譯成功後,系統中注冊的動鏈意外丟失,也會造成編譯後的錯誤。
若類型存在,則根據此類型的需要在內存的堆棧區開辟空間。因此,即使是引用類型,仍然需要消耗堆棧區的空間。和值類型不同的時,此時,堆棧空間中存儲的不是引用類型的數據,而是引用類型在托管堆中的地址。
3.2.2 在托管堆開辟內存空間
當運行到new操作符時,系統開始在托管堆上分配內存,用於存放引用類型的實際數據。New不僅是用於創建對象,還有一個重要的作用就是調用類構造函數。在IL中,new被newobj命名所定義,但new並不是為引用類型所獨有的。值類型也有使用new的情況,看下面的示例程序。
using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
//使用new聲明值類型
int intA = new int();
intA = 2;
Console.WriteLine(intA.ToString());
//聲明結構體時,並不一定需要new操作符
StrTest strTest;
strTest.intA = 123;
Console.WriteLine(strTest.intA.ToString());
//引用類型必須用new實例化
ClaTest claTest = new ClaTest();
claTest.intA = 123;
Console.WriteLine(claTest.intA.ToString());
}
}
struct StrTest
{
public int intA;
}
class ClaTest
{
public int intA;
}
}
在此程序中,定義了一個int型的值類型,一個結構體,一個類。我們可以看出,在聲明值類型時,也可以使用new操作符,也可以不使用 new。而聲明引用類型時,必須使用new操作符,因為需要new為引用類型在托管堆中分配資源。但new並不為值類型在托管堆中開辟內存區。
3.2.3 調用構造函數
new的最後一個作用就是調用類或結構體的構造函數。構造函數是與類名同名的方法成員,由類在初始化後自動運行,用來完成一些數據的初始化工作。