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傳外部變量的棧內存地址即指針地址本身。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章