C#中引用類型的變數做為參數在方法調用時加不加 ref 關鍵字的不同之處

一直以為對於引用類型做為參數在方法調用時加不加 ref 關鍵字是沒有區別的。但是今天一調試蹤了一下變數記憶體情況才發現大有不同。

直接上程式碼,結論是:以下程式碼是使用了 ref 關鍵字的版本,它輸出10;如果不使用ref 關鍵字則輸出 1,2,3 

 1    class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             int[] myArray = new int[] { 1, 2, 3 };
 6             new SetClass().SetArray(ref myArray);
 7             /*
 8              不加ref關鍵字的引用類型傳參情況                                    加上ref關鍵字後的引用類型傳參情況
 9              &myArray                                                           &myArray
10                 0x000000c088d7e3d0                                                  0x0000008151b7e6b0
11                 *&myArray: 0x0000029b3f84ae00                                       *&myArray: 0x000001c5e5a7ae00
12             */
13 
14             foreach (int i in myArray)
15                 Console.WriteLine(i);
16             /*
17              &myArray                                                           &myArray
18                 0x000000c088d7e3d0                                                  0x0000008151b7e6b0
19                 *&myArray: 0x0000029b3f84ae00                                       *&myArray: 0x000001c5e5a7ae00
20             */
21         }
22 
23     }
24 
25     class SetClass
26     {
27         //如果形參 array 是引用類型時(不論加不加 ref 關鍵字),則在方法執行時方法體內的局部變數 array 指向外部傳進來的實參所指向的記憶體空間。
28         //但是加上 ref 關鍵後在方法執行時方法體內接收傳進來的實參時,並不會給 array 變數分配記憶體空間,即 變數array就是變數myArray。 
29         internal void SetArray(ref int[] array)
30         {
31             /*
32              &array                                                           &myArray
33                 0x000000c088d7e388                                                0x0000008151b7e6b0
34                 *&array: 0x0000029b3f84ae00                                       *&myArray: 0x000001c5e5a7ae00
35             */
36             array = new int[] { 10 };
37             /*
38              &array                                                            &myArray
39                 0x000000c088d7e388                                                 0x0000008151b7e6b0
40                 *&array: 0x0000029b3f84bbf0                                        *&myArray: 0x000001c5e5a7ae00
41              */
42         }
43     }
44 }

 

一些說明:

  1. 以上程式碼中的注釋可縱向分隔為兩部分來看,左邊部分是不加ref關鍵字調試時查看的記憶體情況,右邊則是加上ref關鍵字後的情況。
  2. 每個/* */中注釋都是程式碼執行完注釋所在位置的上一語句後的記憶體情況。
  •              &myArray                         //表示獲取這個變數記憶體的指令

                0x000000c088d7e3d0            //表示這個變數在記憶體中的地址
                *&myArray: 0x0000029b3f84ae00  //表示這個變數指向的記憶體空間的對象的地址

  •         在visual studio 2019 中查看變數記憶體地址的方法:

        方法一:
        在即時窗口輸入取地址符+變數名如 &a 這是會輸出如下 兩行:
        0x000000325637e570
        *&a: 0x00000209ba0dad58
        第一行 0x000000325637e570 代表變數本身的記憶體地址,第二行 *&a: 0x00000209ba0dad58 表示變數指向的對象的記憶體地址

        方法二:
        【調試】-【窗口】-【記憶體】-從列出來的4個中選一個,然後會調出記憶體查看窗口。在記憶體查看地窗口中的【地址】里輸入[取地址符]+[變數名]如 &a ,這時地址中的&a會變成變數的十進位表示的記憶體地址,如:0x000000325637E570

 

補充幾張調試中斷在不同語句時的一些記憶體情況截圖:(加上ref關鍵字後的引用類型傳參情況圖)

1.


2.


3.


4.


 

2022-07-31再次總結:

 

我們知道,不論值類型還是引用類型,記憶體存儲單元中的數據是依靠存儲單元地址來訪問的。

對於值類型數據的記憶體模型就是直接把值放在記憶體單元里,需要訪問值時直接用記憶體地址就能獲取這個地址中存儲的數據了。這個模型直觀而簡單很好理解。

而引用類型的記憶體存儲模型是由棧記憶體+堆記憶體的結構共同實現的。具體細節就是:引用類型變數的數據內容(命名為content)放在堆記憶體(我們給這個堆記憶體地址一個名字叫H),然後還需要有一個棧記憶體(再把棧記憶體地址命名為S),這個地址為S的棧記憶體里存放的值就是H,是的 就是堆記憶體的地址,這樣就要訪問content就需要先訪問S,得到S中的內容才得到了地址H,最後才能訪問到H地址里的內容content。基於S中存放的值是另一個記憶體地址而不是數據內容本身的原因,所以人們常把S及其值叫做指針(引用類型數據使用的正是這種間接訪問數據的設計模型)。

接下來是在C#語言的方法中,對傳遞引用類型參數的設計及實現細節的說明。

先不考慮ref關鍵字,對於方法的引用類型參數,其在方法接收外部變數時的接收細節是這樣:方法內部會創建一個新的棧記憶體也就是個指針,其記憶體單元就是用來接收那個外部傳進來的變數所在的堆記憶體的地址,採用這樣的方式來實現對外部變數的接收也就是說本質上是傳遞堆記憶體地址。但是注意,外部變數原來那個棧記憶體指針也指向同樣的堆記憶體。即在方法內部和外部這兩個指針都指向同一塊堆記憶體但這兩個指針各是各,是不同的棧記憶體地址。基於這種設計,我們可以看出,在前面的示例中,如果不使用ref關鍵字,則在SetArray方法內部 array=new int[]…這行指令實際上是先按new int[]指令創建了一個新的堆記憶體(放新的數組),然後把指針array的存儲的值(賦值前它是原堆記憶體地址)更新為新的堆記憶體的地址,那麼原堆記憶體地址在方法內部也就無法再訪問了,而且這個地址值更新的進程也與方法外部的指針myArray無關,即方法外部的myArray依然指向它原先那個堆記憶體地址。

最後,我們來考慮ref關鍵字。一個方法的引用類型參數使用 ref 關鍵字後會使得在方法在接收外部變數時改變默認傳遞參數的行為。具體表現就是加上ref後,在方法內部不再在棧記憶體上創建一個新的指針用來接收外部變數其堆記憶體的地址,而是直接使用外部變數的指針,等於把外部變數的指針本身給傳進來了。

關鍵歸納:不加ref傳外部變數堆記憶體地址;加上ref傳外部變數的棧記憶體地址即指針地址本身。

我覺得這次總結的還不錯,希望對您有所幫助。也希望自己不要再忘記這些關鍵的知識點了。