天天看點

C# 引用類型、值類型

在fcl中,所有稱為“結構”(struct)的類型都是值類型,所有稱為“類”(class)的類型都是引用類型。所有的struct都直接派生自抽象類system.valuetype,而system.valuetype直接從system.object派生。所有的枚舉都直接從system.enum派生,而後者又派生自system.valuetype,是以枚舉也是值類型。由于clr的單繼承規則,是以我們在定義值類型時,不能指定基類型,但可以實作接口。同時從下圖生成的il也可以看出,值類型是隐式密封的(sealed),也就是說也不能從值類型派生。

C# 引用類型、值類型

雖然引用類型與值類型實質隻是記憶體配置設定上的差異,但這種差異會導緻兩種類型在行為表現上有着明顯不同,比如下面的例子:

首先我們定義一個一值類型與一個引用類型,内部都隻有一個字段。用new操作符配置設定記憶體時,值類型v1的記憶體配置設定在了線程棧上,引用類型r1的記憶體配置設定在了托管堆上,在程式運作到第一次writeline輸出時,看到的結果是一緻的。但接下來聲明兩個新的對象并執行指派時,這裡的發生的事明顯不同:雖然指派操作都是拷貝線程棧上變量的内容,但由于值類型變量v1的棧内容就是valtype類型執行個體本身,而引用類型r1的棧内容是reftype對象執行個體在堆上的位址。是以指派後的結果就是,v1和v2各儲存了一份valtype類型執行個體,而r1和r2儲存了同一塊堆記憶體的位址。是以改變r2對象導緻了r1對象的随同改變。下面是記憶體示意圖:

C# 引用類型、值類型

圖1

C# 引用類型、值類型

圖2 

雖然值類型執行個體不需要垃圾回收,但由于值類型在傳遞時,傳遞的是内容本身,是以并不适合将所一些執行個體較大的類型定義為值類型。實作上除非滿足以下所有條件,否則不應該将一個類型聲明為值類型。

沒有更改其字段的成員,即該類型是不可變的。(建議所有字段為readonly)

類型不需要從其他任何類型繼承。(值類型不能選擇基類)

類型也不會派生出其他任何類型。(所有的值類型都是隐式密封sealed的)

執行個體較小(約<=16byte)或較大但不作為方法實參傳遞,也不從方法傳回。

将值類型轉換成一個引用類型的過程叫裝箱,整個過程看起來是這樣的:

在托管堆中配置設定好記憶體,配置設定的記憶體量=值類型的各個字段所需的記憶體量+所有堆上對象都有的兩個額外成員(類型對象指針和同步塊索引)所需的記憶體量。

值類型的字段複制到新配置設定的記憶體。

傳回對象的位址。

拆箱僅是擷取一個指針的過程,該指針指向包含在一個對象中的原始值類型(資料字段)。雖然拆箱比裝箱代價低,但實際在拆箱之後往往緊接着就是指派操作(記憶體複制)。顯然裝箱和拆箱/複制會對應用程式的速度與記憶體消耗上産生不利影響,是以應該了解到這一點,并盡量避免裝箱和拆箱操作。那麼什麼時候會發生裝箱和拆箱,最直覺的方法就是看生成的il代碼(il對應指令是分别是box與unbox),比如下面的例子:

C# 引用類型、值類型

示例中arraylist的add方法參數是object類型,也就是說一個引用類型(在堆上配置設定的記憶體),當我們傳遞int類型時,這裡便會将int執行個體裝箱,以傳回一個堆上的位址。在将array[0]強制轉型為int時,由于值類型int的對象是線上程棧上配置設定的,是以這裡拆箱并緊接着發生指派(記憶體複制)操作。同時為了對比,我加了引用類型的reference,可以看出引用類型是不會發生裝箱與拆箱的。

那麼如何避免(或減少)裝箱與拆箱:

盡量使用泛型集合。

盡量将裝箱與拆箱操作移到循環體之外。

定義一個方法如果可接收引用類型或值類型時,盡量不要将參數定義為object,可以考慮通過重載定義多個版本或定義泛型方法。

<a target="_blank" href="http://www.cnblogs.com/hecool/p/3149833.html">原文位址</a>