CLR via C 5 3 值型別的裝箱和拆箱

2021-09-06 13:36:00 字數 3516 閱讀 3165

在clr中為了將乙個值型別轉換成乙個引用型別,要使用乙個名為裝箱的機制。

下面總結了對值型別的乙個例項進行裝箱操作時內部發生的事:

1)在託管堆中分配好記憶體。分配的記憶體量是值型別的各個字段需要的記憶體量加上託管堆上的所有物件都有的兩個額外成員(型別物件指標同步塊索引)需要的記憶體量。

2)值型別的字段複製到新的分配的堆記憶體。

3)返回物件的位址。現在,這個位址是對乙個物件的引用,值型別現在是乙個引用型別。

拆箱不是直接將裝箱過程倒過來。拆箱的代價比裝箱低得多。拆箱其實就是乙個獲取乙個指標的過程,該指標指向包含在乙個物件中的原始值型別(資料字段)。事實上,指標指向的是已裝箱例項中的未裝箱部分。所以,和裝箱不同,拆箱不要求在記憶體中複製位元組。還有乙個重點就是,拆箱之後,往往會緊接著發生一次欄位的複製操作。

乙個已裝箱的值型別例項在拆箱時,內部會發生下面這些事情。

1.如果包含了"對已裝箱值型別例項的引用"的變數為null,就丟擲乙個nullreferenceexception異常。

2.如果引用指向的物件不是所期待的值型別的乙個已裝箱例項,就丟擲乙個invalidcastexception異常。

上面第二條意味著一下**不會如你預期的那樣工作:

public

static

void

main()

在對乙個物件進行拆箱的時候,只能將其轉型為原始未裝箱時的值型別——int32,下面是正確的寫法:

public

static

void

main()

前面說過,在進行一次拆箱後,經常會緊接著一次欄位的複製。以下演示了拆箱和複製操作:

public

static

void

main()

在最後一行,c#編譯器會生成一條il指令對o進行拆箱,並生成另一條il指令將這些欄位從堆複製到基於棧的變數p中。

再看看一下**:

public

static

void

main()

最後三行**唯一的目的就是將point的x欄位從1變成2.為此,首先要執行一次拆箱,在執行一次字段複製,在更改字段(在棧上),最後執行一次裝箱(從而在託管堆上建立乙個全新的已裝箱例項)。希望你能體會到裝箱和拆箱/複製操作對應用程式效能的影響。

在看個演示裝箱和拆箱的例子:  

private

static

void main(string

args)

你可以看出上述**進行了幾次裝箱操作?如果說是3次,你會不會意味呢?我們來看下生成的il**。  

.method private hidebysig static

void main(string

args) cil managed

console.writeline方法要求獲取乙個string物件,為了建立乙個string物件,c#編譯器生成的**來呼叫string物件的靜態方法concate。該方法有幾個過載的版本,唯一區別就是引數數量,在本例中需要連線三個資料項來建立乙個字串,所以編譯器會選擇以下concat方法來呼叫:

public

static string concat(objetc arg0, object arg1, onject arg2);

所以,如果像下面寫對writeline的呼叫,生成的il**將具有更高的執行效率:

console.writeline(v + "

," + o); //

顯示"123,5"

這只是移除了變數o之前的(int32)強制轉換。就避免了一次拆箱和一次裝箱。

我們還可以這樣呼叫writeline,進一步提公升上述**的效能:

console.writeline(v.tostring() + "

," + o); //

顯示"123,5"

現在,會為未裝箱的值型別例項v呼叫tostring方法,它返回乙個string。string型別已經是引用型別,所以能直接傳給concat方法,不需要任何裝箱操作。

下面在演示乙個裝箱和拆箱操作:  

private

static

void main(string

args)

上述**發生了多少次裝箱呢?答案是一次。因為system.console類定義了獲取乙個int32作為引數的writeline方法的過載版本:  

public

static string concat(int32 value);

在writeline方法內部也許會發生裝箱操作,但這已經不是我們能控制的。我們已經盡可能地從自己的**中消除了裝箱操作。

最後,如果知道自己寫的**會造成編譯器反覆對乙個值型別進行裝箱,請改用手動方式對值型別進行裝箱。

物件相等性和同一性。

system.object型別提供了乙個名為equals的虛方法,它的作用是在兩個物件包含相同的值得前提下返回true。如:

public

class

object

}

對於object的equals方法的預設實現來說,它實現的實際是同一性,而非相等性。

下面展示了如何在內部正確實現乙個equals方法。

1)如果obj實參為null,就返回false,因為在呼叫非靜態的equals方法時,this所標識的當前物件顯然不為null.

2)如果this和obj實參引用同乙個物件,就返回true。在比較包含大量欄位的物件時,這一步有助效能提公升。

3)如果this和obj實參引用不同型別的物件,就返回false。乙個string物件顯然不等於乙個filestream物件。

4)針對型別定義的每個例項字段,將this物件中的值與obj物件中的值進行比較。任何欄位不相等,就返回false。

5)呼叫基類的equals方法,以便比較它定義的任何字段。如果基類的equals方法返回false,就返回false;否則返回true;

例如:

public

class

object

}

由於,乙個型別能重寫object的equals方法,所以不能再呼叫這個equals方法來測試同一性。為了修正這一問題,object提供了乙個靜態方法referenequals,其原型如下:

public

class

object

}

如果想檢查同一性,務必呼叫

referenceequals,而不應該使用c#的== 操作符,因為==操作符可能被過載。

system.valuetype(所有值型別的基類)重寫了object的equals方法,並進行了正確的實現來執行值得相等性檢查。

值型別的裝箱,拆箱

裝箱發生的事情 1.在託管堆中分配記憶體。分配的記憶體量是值型別各字段所需的記憶體量,還要加上託管堆所有物件都有的兩個額外成員 型別物件指標和同步塊索引 所需的記憶體量。2.值型別的字段複製到新分配的堆記憶體。拆箱發生的事情 1.獲取已裝箱物件中各個欄位的位址。2.將字段包含的值從堆複製到基於棧的值...

值型別 引用型別,裝箱 拆箱

值型別 宣告乙個值型別變數,會在棧上分配乙個空間,空間裡儲存的就是變數的值 引用型別 宣告乙個引用型別變數,會在棧中分配乙個空間,儲存乙個引用,這個引用指向了乙個託管堆。值型別 struct,列舉,數值型別,bool型別 引用型別 陣列,類,介面,委託 delegate object,string ...

值型別的拆箱與裝箱

在之前文章中提到了,值型別具有兩種表現形式 已裝箱和未裝箱,這兩種狀態的轉換過程稱之為裝箱和拆箱。從記憶體分配的角度來說,裝箱就是將值型別經過處理從執行緒棧複製到託管堆 拆箱則是將已裝箱的值型別例項從託管堆複製到執行緒棧。裝箱流程 在託管堆中分配記憶體,記憶體大小 值型別大小 物件指標 同步塊索引。...