# 一起學 Reference type 和 Value type (實作篇) ###### tags: `C#` `ReferenceType/ValueType` `Stack/Heap` ## 前言 這一篇是由[一起學 Class and Struct (C#)](/obn5psF6SjyIPbRicUsvwg)的擴充研究,裡面提到了`Stack`和`Heap`兩個記憶體互相配合的方式,這邊想要實際測試看看能不能透過實作來觀看其結果。 ## 學習目標 透過實作來更加認識`ReferenceType`和`ValueType`,並觀測`Stack`和`Heap`的資料儲存方式。 ## 目錄 [TOC] # 介紹 這邊就不多做介紹了不知道可以回去看[一起學 Class and Struct (C#)](/obn5psF6SjyIPbRicUsvwg)了解一下差異。 在進入實作之前說明一下今天會用到工具,主要會使用Visual Studio進行記憶體查看,不知道的人怎麼啟用的可以參考官方文件[[在 Visual Studio 偵錯工具中使用記憶體視窗]](https://docs.microsoft.com/zh-tw/visualstudio/debugger/memory-windows?view=vs-2022#open-a-memory-window),接著進入實作環節。 ## 實值類型(ValueType) ### 位置查看 #### 程式碼 這邊`ValueType`使用`char`來操作,我們建立一個變數`c`並指定一個字元`'X'`,之後取得其指標且將位置輸出,程式碼如下: ```csharp= { char c = 'X'; char* cp = &c; IntPtr TypePtr = (IntPtr)cp; Console.WriteLine($"Address of Type : {TypePtr.FormatToMemberAddress()}"); } ``` :::info :bulb: 這邊的`FormatToMemberAddress`是自己寫的擴充 ::: #### 執行結果 這邊可以看指向的位子就是其資料`'X'`很符合符合`ValueType`的儲存特性(Stack)  接著來看使用`=`的操作會發生什麼事吧 ### 使用`=`的操作 #### 程式碼 其實程式碼大致上差不多,只是變數變成`c0`和`c1`並將`c1 = c0`,接著一樣把位置輸出 ```csharp= { char c0 = 'A'; char c1 = c0; char* c0p = &c0; char* c1p = &c1; // 檢視記憶體 IntPtr TypePtr0 = (IntPtr)c0p; IntPtr TypePtr1 = (IntPtr)c1p; Console.WriteLine($"Address of `TypePtr0` : {TypePtr0.FormatToMemberAddress()}"); Console.WriteLine($"Address of `TypePtr0` : {TypePtr1.FormatToMemberAddress()}"); } ``` #### 執行結果 這邊看到這`c0`和`c1`是不同的位置內容也是個別儲存,在這個情況下<font class='red'>修改其`c0`不會改變`c1`</font>反之亦然  ## 參考類型(ReferenceType) ### 位置查看 #### 程式碼 這邊`ReferenceType`使用`string`來操作,我們建立一個變數`Str`並指定一個字串`"X"`,之後取得其指標且將位置輸出,稍微不同的是因為`Heap`才是資料儲存,`Stack`會是`Heap`的位置資訊,直接看程式碼應該會比較明白如下: ```csharp= { string Str = "X"; // 取得 TypeReferences TypedReference tr = __makeref(Str); // Type pointer IntPtr TypePtr = *(IntPtr*)(&tr); // Ref pointer IntPtr RefPtr = **(IntPtr**)(&tr); Console.WriteLine($"Address of Type : {TypePtr.FormatToMemberAddress()}"); Console.WriteLine($"Address of Ref : {RefPtr.FormatToMemberAddress()}"); } ``` #### 執行結果 這邊可以看到`Type`的位置存放的資料是指向`Ref`的位置,則`Ref`該位置內的資料可以看到存放字串`"X"`,也很符合`ReferenceType`的儲存特性(Heap)  簡單做個Table描述其關係 | Memory | Address | Data | |:------------:|:------------------:|:----:| | Stack Memory | 0x000000693937EA98 | <font class='red'>0x000001A88EED89B8</font> | | Heap Memory | <font class='red'>0x000001A88EED89B8</font> | X<br/>(字串"X") | 一樣來看看使用`=`的操作會發生什麼事 ### 使用`=`的操作 #### 程式碼 其實程式碼大致上差不多,只是變數變成`c0`和`c1`並將`c1 = c0`,接著一樣把位置輸出 ```csharp= { string Str0 = "X"; string Str1 = Str0; // 取得 TypeReferences TypedReference tr0 = __makeref(Str0); TypedReference tr1 = __makeref(Str1); // Type pointer IntPtr TypePtr0 = *(IntPtr*)(&tr0); IntPtr TypePtr1 = *(IntPtr*)(&tr1); // Ref pointer IntPtr RefPtr0 = **(IntPtr**)(&tr0); IntPtr RefPtr1 = **(IntPtr**)(&tr1); Console.WriteLine($"Address of Type0 : {TypePtr0.FormatToMemberAddress()}"); Console.WriteLine($"Address of Ref0 : {RefPtr0.FormatToMemberAddress()}"); Console.WriteLine($"Address of Type1 : {TypePtr1.FormatToMemberAddress()}"); Console.WriteLine($"Address of Ref1 : {RefPtr1.FormatToMemberAddress()}"); } ``` #### 執行結果 這邊可以看到`Stack`(Type0、Type1)儲存位置是不同,但是其資料都是相同的,表示都是指向同一個位置(Heap Address),其地址內容當然只會有一份而已。 在此情況<font class='red'>修改`Str0`會改變`Str1`</font>反之亦然  一樣做個Table描述其關係 | Memory | Address | Data | |:------------:|:------------------:|:----:| | Type0(Stack) | 0x0000007B81F7E4D8 | <font class='red'>0x0000022A81FBA260</font> | | Type1(Stack) | 0x0000007B81F7E4D0 | <font class='red'>0x0000022A81FBA260</font> | | Ref0(Heap)<br/>Ref1(Heap) | <font class='red'>0x0000022A81FBA260</font> | X<br/>(字串"X") | # 結論 雖然透過以往編寫上的經驗已經知道哪些情況下修改參數時會互相影響,但透過實作更加熟悉其核心概念,我們可以得出以下結論: * `ValueType`是直接將**值**儲存於`Stack`,複製時也會完整複製,修改時互相不影響。 * `ReferenceType`是將**參考位址**存放於`Stack`資料則是存放於`Heap`,複製時只會將位置進行複製,但還是指向同一份資料,修改時互相受影響。 最後不確定對寫程式上是否有幫助,但至少對所使用的框架有更深一層的認識。 <br/> --- <style> .red{color: red;} </style>
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up