C#/.NET值型別

2020-07-16 10:04:44
值型別 (Value Type) 包括兩個成員:結構體和列舉型別。

通常來說,值型別就是字面意義上的那種值,例如整數 int,小數 float/double,布林值等。

而實際上,整數、小數,布林值等全部都是結構體。

值型別的預設值一般為 0,例如整數和小數的預設值都是 0,列舉型別的預設值也為 0, char 的預設值為 ‘’。和參照型別相比,值型別的記憶體分配簡單得多。

基元型別

之前講過,C# 和其他 .NET 語言都是執行在通用型別系統(CTS)上的,而 CTS 提供 一些“基本的”型別一基元型別(Primitive Type)。

各個 .NET 語言分別使用不同的關鍵字, 但最終它們都會被對映到同一個 IL 型別。這樣的型別就叫做基元型別,它們由 CTS 定義,由編譯器與 BCL 直接支援,屬於 BCL 而非任何某個語言。

基元型別包括了幾乎所有的值型別(除了使用者定義的結構體和列舉)以及字串,object 和 dynamic。

Primitive 有原始的意思,可以將基元型別理解為基本的、原始的型別,少了它們就什麼都做不了。

有了基元型別,各個 .NET 語言的互操作性就可以實現了,例如,通過 ildasm 工具,我們可以檢視到 int i=1 對應的 IL 程式碼為(這裡省略了賦值的那一句 IL程式碼):

.locals init ([0] int32 i)

這說明了在 IL 中 int 對應的基元型別為 Int32。當然,在 C# 中也可以直接寫 Int32 i=1,不過,這樣並不會給你帶來任何好處。

而對於VB.NET,int 的關鍵字為 Integer,如果你在 VB.NET 中宣告了一個Integer,你也可以通過 ildasm 發現,它對應的型別仍然為 Int32。

值型別的記憶體分配

值型別的記憶體分配分為以下幾種情況:
  • 值型別作為區域性變數。
  • 值型別作為參照型別的成員。
  • 值型別中包含參照型別。

1) 值型別作為區域性變數

普通的值型別總是分配在棧上。例如以最簡單的 int 為例,inti=1 意味著我們在棧上開闢了一塊空間儲存這個值型別。

注意,int 實際上是一個結構體,它有 2 個值型別成員(最大值,最小值),它們是常數,所以是靜態的(const=static readonly)。

靜態的成員和 int 的方法均儲存在載入堆中。

值型別也沒有同步塊索引和型別物件指標。所以,新建一個 int,不會重新複製它的最大值和最小值,int 的開銷永遠是 4 個位元組(就是它自己)。

即使機器是 64 位機,int 的大小永遠是 32 位,因為 int 實質上是 Int32。Int64 這個基元型別在 C# 中對應 long。

對於區域性變數的複製來說,情況非常簡單。我們知道,值型別複製時,將只複製值的副本。所以更改原值對複製的新值不會有影響。
var i = 1;
var j = i;
i = 2 ;
//輸出1
Console.WriteLine(j);
當執行程式碼var j = i時,將會在棧上新建一個名為 j 的變數,然後將 i 的值複製給 j,它和 i 沒有任何關係。值型別也不可能有淺複製。

2) 值型別作為參照型別的成員

如果值型別為參照型別的成員,則遵從參照型別的記憶體分配和複製方式。例如:
public class AClass
{
    public int a;
    public string b;
}
在建立一個該類的範例時,遵從參照型別的記憶體分配方式。

下面的程式碼會範例化一個 AClass 物件:
var a = new AClass();
a. a = 1;
a.b = "hey";
執行完上面的程式碼之後,記憶體的分配如下圖所示。

值類型作為引用類型的成員