MSDN 上,將 .Net 下所有的類別,分為以下三類︰

  • Value Type (實值型別)︰常見的型別是 structintchardouble
  • Reference Type (參考型別)︰典型的例子就是使用 class 關鍵字定義的型別
  • Pointer Type (指標型別)

其中 Pointer Type,主要是像 C/C++ 語言,可以用來對記憶體直接操作。但因為使用到它的機會比較特別,我自己使用到它的機會也沒有太多,所以本文暫時先不針對此型別做討論。

而 Value Type 與 Reference Type 兩者在 .Net 下的型別階層體系的差別上,最明顯的差別與區別方式在於,Value Type 皆繼承自 System.ValueType,因此如果不是繼承自 System.ValueType 的型別,都不是 Value Type。

針對 Value Type 與 Reference Type 的差異,我們可以先以以下兩句 ,簡單做個說明︰

  • 在 Value Type 變數中,儲存的值是「實值」(Value),像是整數、浮點數、布林、字元等
  • 在 Reference Type 變數中,儲存的值是「參考」(Reference),也就是記憶體的位址 ,指向儲存真實內容的記憶體區塊的開始位置。

也就是說,Value Type 與 Reference Type 最顯著的差別,是他們在記憶體中,儲存其變數值的方式。再往下繼續探討前,我覺得我們可以先建立以下的基礎觀念︰

  • C# (或說大部份的程式語言) 會將記憶體分為兩大用途︰Stack 與 Heap。
  • C# 中所有的區域變數 (不管是 Value Type 或是 Reference Type),其內容 (變數名稱、型別與與值) 都是儲存在 Stack 中。Value Type 變數儲存的內容是「實值」,Reference Type 變數儲存的內容是「參考」。
  • 使用 new 關鍵字實體化類別的物件,其物件內容,是儲存在 Heap 中。Reference Type 變數中所儲存的參考,其實是指向 Heap 中的記憶體位址。

墳墓大大之前有寫了一系列關於 C/Java 在記憶體管理上,非常好的系列文章,大家可以閱讀第一、二篇 (其實全系列 8 篇都很推薦閱讀),以建立上述觀念︰

為了測試兩者在記憶體儲存上的差別,我建立測試用 Reference Type 型別與 Value Type 型別如下︰

/// 
/// 測試用 Reference Type 型別
/// 
public class DemoReferenceType
{
    int _field;
    public int Field
    {
        get
        {
            return this._field;
        }

        set
        {
            this._field = value;
        }
    }

    public DemoReferenceType(int val)
    {
        this._field = val;
    }

    public override string ToString()
    {
        return "Field=" + this._field.ToString();
    }
}

另外建立測試用 Value Type 如下︰


/// 測試用 Value Type 型別
/// 
public struct DemoValueType
{
    int _field;
    public int Field 
    { 
        get
        {
            return this._field;
        }
        
        set
        {
            this._field = value;
        }
    }

    public DemoValueType(int val)
    {
        this._field = val;
    }

    public override string ToString()
    {
        return "Field=" + this._field.ToString();
    }
}

我們以下列程式進行測試︰

// 建立 Reference Type 與 Value Type 的測試物件
DemoReferenceType refVar1 = new DemoReferenceType(1);
DemoValueType valVar1 = new DemoValueType(1);

// 將變數 1 的值,指派給變數 2
DemoReferenceType refVar2 = refVar1;
DemoValueType valVar2 = valVar1;

// 變更變數 2 的欄位值
refVar1.Field = 2;
valVar2.Field = 2;

// 印出︰
// refVar1 Field=2
// refVar2 Field=2
// valVar1 Field=1
// valVar2 Field=2
Console.WriteLine("refVar1 " + refVar1);
Console.WriteLine("refVar2 " + refVar2);
Console.WriteLine("valVar1 " + valVar1);
Console.WriteLine("valVar2 " + valVar2);

可以發現,在 Reference Type 與 Value Type 下,我們都是將變數一 (refVar1、valVar1) 的值指派給變數二 (refVar2、valVar2),並修改變數二的欄位值。但最後的結果卻不相同。

直覺上我們會變得 Reference Type 的情形不符直覺,明明我已經變數一(refVar1) 「指派」(assign) 給變數二 (refVar2) 了,為什麼我修改變數二的欄位值,看起來好像也影響到變數一的欄位值了呢?

這是因為兩者間所交換儲存的值不同,在「指派」這件事上,其實兩種型別做的事都是相同的,即「將變數一儲存的值,建立一個複本並儲存到變數二中」,只是 Value Type 建立的複本是資料本身,而 Reference Type 的複本是參考。

我們以圖例來進行說明,下圖是我們建立變數一時,記憶體中的配置方式︰

image

可以看到,所有的區域變數,都是儲存於 Stack 中,Reference Type 中儲存的是參考,內含是 Heap 中的記憶體位址。

而在建立變數二,並將變數一 (refVar1、valVar1) 的值指派給變數二 (refVar2、valVar2)後,記憶體中的配置如下︰

image

如同先前所說,「指派」這件事,不管在 Reference Type 或是 Value Type 都是一樣的,都是將變數一的儲存的值,複製一份到變數二。

而因為 Reference Type 中儲存的是參考,因此變數二 (refVar2) 會儲存與變數一 (refVar2) 相同的記憶體位址。也就是其實 refVar2 與 refVar1 指向的是在 Heap 上,相同的物件內容。

最後我們可以看看,同樣針對變數二的欄位做變更,在記憶體上的儲存情形︰

image

在此我們可以看到,由於 Reference Type 中的 refVar1 與 refVar2 中儲存的參考值相同,也就是兩者背後指向的是相同的物件內容。因此使用 refVar2 變數修改 Field 欄位內容,再取出 refVar1.Field 欄位值,兩者數值是相等的也就是完全合理的了。

最後整理一下重點如下︰

  • 在 C# 中,記憶體用途分為 Stack 與 Heap 兩種,所有的區域變數 (不管是 Value Type 或是 Reference Type) 都儲存於 Stack 下,使用 new 關鍵字實體化的類別實體,則儲存於 Heap 中
  • Value Type 儲存的是內容 (實值),Reference 儲存的是位址 (參考)
  • 由於 Value Type 與 Reference Type 在記憶體儲存值上的差異,在使用上若不理解,有時會造成意料外的問題

References