C#中參照型別的變數做為引數在方法呼叫時加不加 ref 關鍵字的不同之處

2022-07-31 21:00:44

一直以為對於參照型別做為引數在方法呼叫時加不加 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傳外部變數的棧記憶體地址即指標地址本身。

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