.NET值類型和引用類型101
1.1.1 摘要
什么是值類型?什么是引用類型?這問題大家很久以前就討論和研究過了,而且我相信很多人對值類型和引用類型的區別和用法都嫻熟于心。這里我給出自己的總結,而且也提供大家一個復習值類型和引用類型的機會。
熟悉C/C++的程序員都知道你可以為任何類型的對象建立指針來引用它們。這也不是java,任何類型都自動聲明為引用類型,然后C#中的類型提供了值類型和引用類型。
1.1.2 正文

圖1值類型和引用類型大綱
值類型是用來存放對象的值,那么引用類型就是存放對象的引用值,真的是這樣嗎?我看到很多人都說:“值類型就是用來存放值,而且不能或不應該。存放方法和行為”在我看來值類型不僅僅是存放值的,還是有存放方法和行為的,例如:DateTime和Decimal毫無疑問是值類型,而且.NET為該類型提供了許多方法和行為,以致DateTime和Decimal的功能強大。
值類型的值存放在棧中,引用類型的對象存放在堆中,真的是這樣嗎?我覺得這要視乎語言而定,就像我們前面提到C/C++類型都被定義為值類型,而Java中的類型都被定義為引用類型(前置條件一),而且我們不應該認為存儲區域是一成不變的,要看具體的語言,說不定猴年馬月C#會把一些引用類型放在棧中。
還有引用類型對象存放在堆中是沒有問題的,但是值類型的值就存放區域要根據值類型的定義來確定,例如我們把一個值類型分別定義為一個方法中局部變量和一個類中的字段,存放區域就不一樣了,前著存放在棧中,而后者存放在堆中(前置條件二)。而且我相信在上學的時候老師講的最多是值類型和引用類型存放區域—堆棧,以后要分別加上連個前置條件。
- 根據具體的語言
- 根據值類型具體定義
圖2值類型和引用類型存儲
上面簡單地描述了值類型和引用類型在內存中存儲的方式,值類型數據存儲在棧中,引用類型的引用值存放在棧,而對象和數據值存儲在堆中,大家都清楚.NET中提供的預定義類型中除了object和string是引用類型外其他類型都是值類型。讓我們通過具體的例子來講講值類型和引用類型。
OK首先我們定義一個Custom類,然后添加兩個字段_index和_description,為了簡潔起見我們沒有使用屬性,具體例子如下:
/// <summary> /// A custom clase. /// </summary> public class CustomType { /// <summary> /// _index is value type. /// _description is reference. /// </summary> private int _index; private string _description; }
假設我們實例化該類型的一個對象,然后讓我們分析一下該對象在內存中是如何分配的,好現是畫畫的時間了。
- 引用類型的對象和數據總是存儲在堆中
- 值類型和引用類型的引用值可以存儲在棧或堆視乎語言環境而定
圖3引用類型存儲方式
通過上圖我們可以發現引用類型的對象是存放在堆中這是毫無疑問的,而我們在CustomType中定義的值類型_index也是存放在堆中,這充分說明一點就是值類型的存儲區域要根據具體定義確定。
接下來我們介紹一個值類型--Struct,我們定義名為CustomType的結構體,然后添加兩個字段和之前類中的一樣,現在又是畫畫的時間了。
圖4值類型存儲方式
現在我們實例化一個CustomType對象,由于我們清楚地知道CustomType是一個值類型,所以它將被存儲在棧中,而且_index的值和_description的引用值都被存儲在棧中,_description具體的值或對象被存儲在堆中。
通過前面的引用類型和值類型對象在內存中存儲的例子,我們知道不能籠統地說:“值類型存儲在棧中,引用類型存儲在堆中”,而是要加上前置的條件(語言類型,定義類型)。
讓我們通過一段簡單的代碼向大家展示值類型和引用類型之間的使用上的區別,這里我們使用Struct和Class為例。
public class MyType { /// <summary> /// This class has two fields. /// </summary> private CustomType type1; private CustomType type2; /// <summary> /// Initializes a new instance of the <see cref="MyType"/> class. /// </summary> public MyType() { this.type1 = new CustomType(); this.type2 = new CustomType(); } }
//// Instantiates a object of MyType. MyType myType = new MyType();
現在讓大家分析一下當類型CustomType分別是結構體和類時內存分配空間的大小和分配次數(假設在32位CLR下)。
首先當CustomType是結構體時只需要一次內存分配,大小為CustomType類型大小的兩倍,由于CustomType大小為8Byte,則MyType分配空間大小為16Byte。如果CustomType是類時我們需要三次內存分配,一次是MyType對象的堆中分配,接著兩次分別是CustomType對象的堆中分配。
再舉一個簡單的例子:
MyType[] var = new MyType[888];
如果MyType是一個值類型,則只需要一次分配,大小為 MyType對象大小的888倍。但如果MyType是一個引用類型,剛開始需要一次分配,分配后數組的各元素值為 null。如果再初始化數組中的每個元素,我們總共將需要執行889次分配——889 次分配要比 1次分配耗費更多的時間。分配許多引用類型對象將在堆空間上造成很多碎片,從而降低系統的速度。
很多人都說:“結構體是一個輕量級類”,他們這樣認為是根據結構體的效率比類要好,但我覺得這樣理解不夠全面,通過前面的兩個例子我們的確看到結構體的確效率優于類,因為值類型不需要垃圾回收(除裝箱的值類型之外),而且沒有類型識別開銷。但如果我們要進行的是大量的數據copy時候,值類型要復制要初始化每個變量的值,而引用類型只需復制引用值就OK了。
MyType T1 = new MyType(); MyType T2 = T1;
圖5值類型引用類型賦值
通過上面的例子我們發現當MyTpye是值類型時我們需要復制N次而引用類型只需復制一次,如果復制數據量多時值類型效率比引用類型效率要低。因為引用類型只需復制引用值就OK了。
值類型和引用類型應用場合區別:
- 該類型的主要職責是否用于數據存儲?
- 該類型的公有接口是否完全由一些數據成員存取屬性定義?
- 是否確信該類型永遠不可能有子類?
- 是否確信該類型永遠不可能具有多態行為?
裝箱和拆箱
Boxing是.NET中提供一種機制使得根據值類型產生相應的對象,Unboxing就是把對象中的值取出來在放到相應的值類型中。然我們通過一段簡單的例子來說明。
int i = 5; object o = i; //boxing int j = (int)o; //unboxing
我們定義了一個值類型i和一個引用類型o,當由值類型i賦值給o時候發生了boxing,而由對象o賦值到j時候發生unboxing。但我們要注意的是如果我們boxing是long,而unboxing到int時候會拋出一個InvalidCastException異常。我們可以通過以下方式處理轉換異常問題,但這樣給代碼帶來了不必要的冗余和精度缺失,所以我們在進行unboxing時應該和boxing變量類型一致。
long i = 5;
object o = i; //boxing
int j = (int)(long)o; //unboxing
通過前面的例子我們發現boxing和unboxing的轉換并沒有什么標識,我們只可以根據值類型和引用類型之間轉換判斷boxing和unboxing發生。為了幫助大家理解這一點,考慮下面代碼boxing和unboxing操作的次數:
ArrayList list = new ArrayList(); list.Add(22); list.Add(23); list.Add((int)list[0] + (int)list[1]); foreach (int i in list) { Console.WriteLine("Number is: {0}.\n", i); }
首先我們往list數組中放入兩個值類型,由于Add()方法接受的參數是object類型,所以需要進行兩次boxing操作。
接著我們把list中的兩個對象取處理,然后轉換為值類型進行相加操作這需要兩次unboxing操作,最后把結果指存放到list中要進行boxing操作。
在遍歷list時候我們要把list中的值保存到i中發生unboxing操作,接著我們調用Console. WriteLine (string format, object arg)要對i進行boxing操作。
所以發生了4次boxing和3次unboxing操作。值得慶幸的是boxing和unboxing操作不多,對我們的程序效率暫時還沒有大的影響,但如果頻繁進行boxing和unboxing操作就會對我們程序產生不可估量的影響,這也是.NET提出要使用泛型的一個原因。
上面的例子我覺得發生比較隱秘是在WriteLine()方法,因為平時使用太多反而沒有注意該方法傳遞的參數類型。
接下來我們通過一些更加隱秘的例子說明什么時候會發生boxing操作。
int i = 23; i.ToString(); //A i.GetHashCode(); //B i.GetType(); //C i.GetTypeCode(); //D
請大家分析一下那個方法進行了boxing操作,激動人心的時刻又到了來讓我們公布答案--C。
原因很簡單Object.GetType()方法不可以重寫,所以值類型類Integer沒有實現自己的GetType方法,只能通過boxing操作調用Object的GetType()方法。
圖6 MSIL代碼
1.1.3 總結
本文主要介紹什么是值類型和引用類型,和它們在內存中的分配我們從中看到了值類型和引用類型的區別:
- 引用類型存放對象的引用值,而不是對象本身
- 值類型存放是具體的數據本身
- 很多時候值類型比引用類型高效,但也有例外情況
- 引用類型的對象存儲在堆中,而值類型數據可以存放在棧或堆中,要根據具體的定義
- 值類型值可以通過boxing轉換為引用類型,通過unboxing轉換為值類型
|
|
關于作者:[作者]:
JK_Rush從事.NET開發和熱衷于開源高性能系統設計,通過博文交流和分享經驗,歡迎轉載,請保留原文地址,謝謝。 |

摘要 什么是值類型?什么是引用類型?這問題大家很久以前就討論和研究過了,而且我相信很多人對值類型和引用類型的區別和用法都嫻熟于心。這里我給出自己的總結,而且也提供大家一個復習值類型和引用...


浙公網安備 33010602011771號