C#中的值類型和引用類型


值類型和引用類型

 本篇筆記結合了《CLR Via C#》和《C# in Depth》兩本書中講述的值類型和引用類型的區別和特性、值類型的裝箱和拆箱這兩部分內容。

但我根據裝箱部分的理解所整理出來的配圖可能會有錯誤和遺漏,希望能有人來指正。


現實世界中的值和引用

報紙與值類型

先假設你正在讀的是一份真正的報紙。為了給朋友一份,需要影印報紙的全部內容並交給他。屆時,他將獲得屬於他自己的一份完整的報紙。

在這種情況下,我們處理的是值類型的行為。所有信息都在你的手上,不需要從任何其他地方獲得。制作了副本之后,你的這份信息和朋友的那份是各自獨立的。可以在自己的報紙上添加一些注解,他的報紙根本不會改變。

網址與值類型

再假設你正在讀的是一個網頁。你需要給朋友的就是網頁的URL。這是引用類型的行為,URL代替引用。為了真正讀到文檔,必須在瀏覽器中輸入URL,並要求它加載網頁來導航引用。

另一方面,假如網頁由於某種原因發生了變化,你和你的朋友下次載入頁面時,都會看到那個改變。

在C#和.NET中,值類型和引用類型的差異與現實世界中的差別類似。

誰是值類型或引用類型

.NET中的大多數類型都是引用類型,除了以下總結的特殊情況,類是引用類型,而結構是值類型。特殊情況包括如下方面:

①數組類型是引用類型,即使元素類型是值類型(所以即便 int 是值類型, int[] 仍是引用類型);

②枚舉(使用 enum 來聲明)是值類型;

③委托類型(使用 delegate 來聲明)是引用類型;

④接口類型(使用 interface 來聲明)是引用類型,但可由值類型實現。

 


值類型和引用類型的基礎知識

值類型

大多數表達式都有與其相關的靜態類型。對於值類型的表達式,它的值就是表達式的值。

值類型都隱式密封,目的是防止將值類型用作其他引用類型或值類型的基類型。

雖然不能在定義值類型時為他選擇基類型,但如果願意,值類型可實現一個或多個接口。

引用類型

對於引用類型的表達式,它的值是一個引用,是new操作符返回對象內存地址——即指向對象數據的內存地址。

使用引用類型的四個事實

使用引用類型必須留意性能問題。首先要認清楚以下四個事實。

1.內存必須從托管堆分配。

2.堆上分配的每個對象都有一些額外成員,這些成員必須初始化。

3.對象中的其他字節(為字段而設)總是設為零。

4.從托管堆分配對象時,可能強制執行一次垃圾回收。

 


二者在內存中的區別

Point 類型可以實現為結構或類。

Point p1 = new Point(10, 20);
Point p2 = p1;

左部分指出當 Point 是引用類型時所涉及的值,右部分展示了當Point 是一個值類型時的情形。

①在 Point 是引用類型的情況下,那個值是引用: p1 和 p2 都引用同一個對象。

②在 Point 是值類型的情況下, p1 的值是一個完整的數據,也就是 x 和 y 值。將 p1 的值賦給 p2 ,會復制 p1 的所有數據。

 


聲明值類型的條件

除非滿足以下全部條件,否則不應將類型聲明為值類型

①類型沒有提供會更改其字段的成員,也就是說該類型是不可變類型,建議將絕大多數的值類型的字段都編輯為readonly。

②類型不需要從其他任何類型繼承,類型也不派生出其他任何類型。

③類型的實例較小(16字節或更小);或者類型的實例較大(大於16字節),但不作為方法實參傳遞,也不從方法返回。

 


值類型和引用類型的區別

值類型的主要優勢是不作為對象在托管堆上分配。與引用類型相比,值類型也存在自身的一些局限。下面列出了二者的一些區別。

①值類型對象有兩種表示形式:未裝箱和已裝箱。引用類型則總是處於已裝箱形式。

②定義自己的值類型時應重寫Equals和GetHashCode,並提供他們的顯式實現。

③不應在值類型中引入任何新的虛方法。所有方法都不能是抽象的,所有方法都隱式密封。

④引用類型的變量包含堆中對象的地址。引用類型的變量創建時默認初始化為null。值類型的變量總是包含其基礎類型的一個值,所有成員都初始化為0。

⑥值類型變量賦值給另一個值類型變量,逐字段地復制。引用類型賦值,只復制地址。

⑦未裝箱的值類型不在堆上分配。一旦定義了該類型的一個實例的方法不再活動,
為他們分配的存儲就會被釋放,而不是等着進行垃圾回收。

 


值類型的裝箱和拆箱

值類型比引用類型輕,是他們不作為對象在托管堆中分配,不被垃圾回收,也不通過指針進行引用。但許多時候都需要獲取對值類型實例的引用。

舉個例子

例如假定要創建ArrayList對象來容納一組Point結構。

struct Point{
    public Int32 x,y;
}
//測試類
ArrayList list = new ArrayList();
Point p;//分配一個Point,不再堆中分配
for(Int32 i = 0;i<10;i++){
    p.x = p.y = i;//初始化值類型中的成員
    list.Add(p);//對值類型裝箱,將引用添加到ArrayList中
}
//本例的Add方法原型
public virtual Int32 Add(Object value);

可以看出Add獲取的是一個Object參數,也就是說Add獲取對托管堆上的一個對象的引用來作為參數。

但代碼傳遞的是Point是值類型。為了使代碼正確工作,Point值類型必須轉換成真正的、在堆中托管的對象,而且必須獲取對該對象的引用。

裝箱機制

將值類型轉換成引用類型要使用裝箱機制。下面總結了對值類型的實例進行裝箱時發生的事情。

①在托管堆中分配內存。分配的內存量是值類型各字段所需的內存量,還要加上托管堆所有對象都有的兩個額外成員所需的內存量。

②值類型的字段復制到新分配的堆內存。

③返回對象地址。現在該地址是對象引用,值類型成了引用類型。

在運行時,當前存在於Point值類型實例p中的字段復制到新分配的Point對象中。已裝箱Point對象的地址返回並傳給Add方法。

Point對象一直存在於堆中,直到被垃圾回收。Point值類型變量可被重用,因為ArrayList不知道關於他的任何事情。

在這種情況,已裝箱值類型的生存期超過了未裝箱值類型的生存期。

拆箱

假定要用以下代碼獲取ArrayList的第一個元素。

Point p=(Point) a[0];

獲取ArrayList的元素0包含的引用或指針,試圖將其放到Point值類型的實例p中。

為此已裝箱Point對象中的所有字段都必須賦值到值類型變量p中,后者在線程棧上。CLR分兩步完成復制。

①獲取已裝箱Point對象中的各個Point字段的地址。這個過程稱為拆箱。

②第二步將字段包含的值從堆復制到基於棧的值類型實例中。

拆箱不是直接將裝箱過程倒過來,其代價比裝箱低得多。拆箱就是獲取指針的過程,該指針指向包含在一個對象中的原始值類型(數據字段)。

指針指向的是已裝箱實例中的未裝箱部分。所以和裝箱不同,拆箱不要求在內存中復制任何字節。往往緊接着拆箱發生一次字段復制。

已裝箱值類型實例在拆箱時,內部發生這些事。

①如果包含“對已裝箱值類型實例的引用”的變量變為null,拋出NullReferenceException異常。

②如果引用的對象不是所需值類型的已裝箱實例,拋出InvalidCastException異常。

第二條意味着在對象進行拆箱時,只能轉型為最初未裝箱的值類型。

//錯誤
Int32 x=5;
Object o=x;//對x裝箱,o引用已裝箱對象
Int16 y=(Int16)o;//拋出InvalidCastExcrption異常
//正確
Int32 x=5;
Object o=x;//對x裝箱,o引用已裝箱對象
Int16 y=(Int16)(Int32)o;//先拆箱為爭取類型,再轉型

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM