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再次总结:

?

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传外部变量的栈内存地址即指针地址本身。

我觉得这次总结的还不错,希望对您有所帮助。也希望自己不要再忘记这些关键的知识点了。