传值调用和传引用调用是几乎所有主流语言都会涉及到的问题,下面我谈谈我对C#中传值调用和传引用调用的理解。
 
1. 一般对C#中传值调用和传引用调用的理解
 如果传递的参数是基元类型(int,float等)或结构体(struct),那么就是传值调用。
 如果传递的参数是类(class)那么就是传引用调用。
 如果传递的参数前有ref或者out关键字,那么就是传引用调用。
 
验证示例的代码如下:
 
 
 
 
 
 
 
view sourceprint?
 
01    using System;
 
02   
 
03    public class ArgsByRefOrValue
 
04    {
 
05        public static void Main(string[] args)
 
06        {
 
07            // 实验1. 传值调用--基元类型
 
08            int i = 10;
 
09            Console.WriteLine("before call ChangeByInt: i = " + i.ToString());
 
10            ChangeByInt(i);
 
11            Console.WriteLine("after call ChangeByInt: i = " + i.ToString());
 
12   
 
13            Console.WriteLine("==============================================");
 
14            // 实验2. 传值调用--结构体
 
15            Person_val p_val = new Person_val();
 
16            p_val.name = "old val name";
 
17            Console.WriteLine("before call ChangeByStruct: p_val.name = " + p_val.name);
 
18            ChangeByStruct(p_val);
 
19            Console.WriteLine("after call ChangeByStruct: p_val.name = " + p_val.name);
 
20   
 
21            Console.WriteLine("==============================================");
 
22            // 实验3. 传引用调用--类
 
23            Person_ref p_ref = new Person_ref();
 
24            p_ref.name = "old ref name";
 
25            Console.WriteLine("before call ChangeByClass: p_ref.name = " + p_ref.name);
 
26            ChangeByClass(p_ref);
 
27            Console.WriteLine("after call ChangeByClass: p_ref.name = " + p_ref.name);
 
28   
 
29            Console.WriteLine("==============================================");
 
30            // 实验4. 传引用调用--利用ref
 
31            Person_ref p = new Person_ref();
 
32            p.name = "old ref name";
 
33            Console.WriteLine("before call ChangeByClassRef: p.name = " + p.name);
 
34            ChangeByClassRef(ref p);
 
35            Console.WriteLine("after call ChangeByClassRef: p.name = " + p.name);
 
36   
 
37            Console.ReadKey(true);
 
38        }
 
39   
 
40        static void ChangeByInt(int i)
 
41        {
 
42            i = i + 10;
 
43            Console.WriteLine("when calling ChangeByInt: i = " + i.ToString());
 
44        }
 
45   
 
46        static void ChangeByStruct(Person_val p_val)
 
47        {
 
48            p_val.name = "new val name";
 
49            Console.WriteLine("when calling ChangeByStruct: p_val.name = " + p_val.name);
 
50        }
 
51   
 
52        static void ChangeByClass(Person_ref p_ref)
 
53        {
 
54            p_ref.name = "new ref name";
 
55            Console.WriteLine("when calling ChangeByClass: p_ref.name = " + p_ref.name);
 
56        }
 
57   
 
58        static void ChangeByClassRef(ref Person_ref p)
 
59        {
 
60            p.name = "new ref name";
 
61            Console.WriteLine("when calling ChangeByClassRef: p.name = " + p.name);
 
62        }
 
63    }
 
64   
 
65    public struct Person_val
 
66    {
 
67        public string name;
 
68    }
 
69   
 
70    public class Person_ref
 
71    {
 
72        public string name;
 
73    }
 
 
 
运行结果如下:
 
 
  
 
看起来似乎上面代码中实验3和实验4是一样的,即对于类(class)来说,不管加不加ref或out,都是传引用调用。
 
其实,这只是表面的现象,只要稍微改一下代码,结果就不一样了。
 
修改上面代码,再增加两个实验。
 
 001  using System; 002  003  public class ArgsByRefOrValue 004  { 005      public static void Main(string[] args) 006      { 007          // 实验1. 传值调用--基元类型008          int i = 10; 009          Console.WriteLine("before call ChangeByInt: i = " + i.ToString()); 010          ChangeByInt(i); 011          Console.WriteLine("after call ChangeByInt: i = " + i.ToString()); 012  013          Console.WriteLine("=============================================="); 014          // 实验2. 传值调用--结构体015          Person_val p_val = new Person_val(); 016          p_val.name = "old val name"; 017          Console.WriteLine("before call ChangeByStruct: p_val.name = " + p_val.name); 018          ChangeByStruct(p_val); 019          Console.WriteLine("after call ChangeByStruct: p_val.name = " + p_val.name); 020  021          Console.WriteLine("=============================================="); 022          // 实验3. 传引用调用--类023          Person_ref p_ref = new Person_ref(); 024          p_ref.name = "old ref name"; 025          Console.WriteLine("before call ChangeByClass: p_ref.name = " + p_ref.name); 026          ChangeByClass(p_ref); 027          Console.WriteLine("after call ChangeByClass: p_ref.name = " + p_ref.name); 028  029          Console.WriteLine("=============================================="); 030          // 实验4. 传引用调用--利用ref 031          Person_ref p = new Person_ref(); 032          p.name = "old ref name"; 033          Console.WriteLine("before call ChangeByClassRef: p.name = " + p.name); 034          ChangeByClassRef(ref p); 035          Console.WriteLine("after call ChangeByClassRef: p.name = " + p.name); 036  037          Console.WriteLine("=============================================="); 038          // 实验5. 传引用调用--类 在调用的函数重新new一个对象039          Person_ref p_ref_new = new Person_ref(); 040          p_ref_new.name = "old new ref name"; 041          Console.WriteLine("before call ChangeByClassNew: p_ref_new.name = " + p_ref_new.name); 042          ChangeByClassNew(p_ref_new); 043          Console.WriteLine("after call ChangeByClassNew: p_ref_new.name = " + p_ref_new.name); 044  045          Console.WriteLine("=============================================="); 046          // 实验6. 传引用调用--利用ref 在调用的函数重新new一个对象047          Person_ref p_new = new Person_ref(); 048          p_new.name = "old new ref name"; 049          Console.WriteLine("before call ChangeByClassRefNew: p_new.name = " + p_new.name); 050          ChangeByClassRefNew(ref p_new); 051          Console.WriteLine("after call ChangeByClassRefNew: p_new.name = " + p_new.name); 052  053          Console.ReadKey(true); 054      } 055  056      static void ChangeByInt(int i) 057      { 058          i = i + 10; 059          Console.WriteLine("when calling ChangeByInt: i = " + i.ToString()); 060      } 061  062      static void ChangeByStruct(Person_val p_val) 063      { 064          p_val.name = "new val name"; 065          Console.WriteLine("when calling ChangeByStruct: p_val.name = " + p_val.name); 066      } 067  068      static void ChangeByClass(Person_ref p_ref) 069      { 070          p_ref.name = "new ref name"; 071          Console.WriteLine("when calling ChangeByClass: p_ref.name = " + p_ref.name); 072      } 073  074      static void ChangeByClassRef(ref Person_ref p) 075      { 076          p.name = "new ref name"; 077          Console.WriteLine("when calling ChangeByClassRef: p.name = " + p.name); 078      } 079  080      static void ChangeByClassNew(Person_ref p_ref_new) 081      { 082          p_ref_new = new Person_ref(); 083          p_ref_new.name = "new ref name"; 084          Console.WriteLine("when calling ChangeByClassNew: p_ref_new.name = " + p_ref_new.name); 085      } 086  087      static void ChangeByClassRefNew(ref Person_ref p_new) 088      { 089          p_new = new Person_ref(); 090          p_new.name = "new ref name"; 091          Console.WriteLine("when calling ChangeByClassRefNew: p_new.name = " + p_new.name); 092      } 093  } 094  095  public struct Person_val 096  { 097      public string name; 098  } 099  100  public class Person_ref 101  { 102      public string name; 103  }  则运行结果为: 
 
实验5的运行结果似乎说明即使参数是类(class),只要不加ref,也是传值调用。
 
下面就引出了我的理解。
 
2. 没有ref时,即使参数为引用类型(class)时,也可算是一种传值调用
 
参数为引用类型时,传递的是该引用类型的地址的一份拷贝,“该引用类型的地址的一份拷贝”即为传值调用的“值”。
 
注意这里说传递的是该引用类型的地址的一份拷贝,而不是引用类型的地址。
 
下面将用图的形式来说明以上实验3,实验5和实验6中内存的情况。
 
2.1 首先是实验3
 
实验3的内存图如下,实参是函数ChangeByClass外的Person_ref对象,形参是函数ChangeByClass内的Person_ref对象。
 
从图中我们可以看出实参new出来之后就在托管堆上分配了内存,并且在栈上保存了对象的指针。
 
调用函数ChangeByClass后,由于没有ref参数,所以将栈上的实参p_val拷贝了一份作为形参,注意这里p_val(实参)和p_val(形参)是指向托管堆上的同一地址。
 
所以说没有ref时,即使参数为引用类型(class)时,也可算是一种传值调用,这里的值就是托管堆中对象的地址(0x1000)。
 
调用函数ChangeByClass后,通过p_val(形参)修改了name属性的值,由于p_val(实参)和p_val(形参)是指向托管堆上的同一地址,所以函数外的p_val(实参)的name属性也被修改了。
 
2.2 然后是实验5
 
上面的实验3从执行结果来看似乎是传引用调用,因为形参的改变导致了实参的改变。
 
下面的实验5就可以看出,p_val(形参)和p_val(实参)并不是同一个变量,而是p_val(实参)的一个拷贝。
 
从图中可以看出第一步还是和实验3一样,但是在调用函数ChangeByClassNew后,就不一样了。
 
函数ChangeByClassNew中,对p_val(形参)重新分配了内存(new操作),使其指向了新的地址(0x1100),如下图:
 
所以p_val(形参)的name属性改了时候,p_val(实参)的name属性还是没变。
 
2.3 最后是实验6
 
我觉得实验6是真正的传引用调用。不废话了,直接上第一个图。
 
参数中加了ref关键字之后,其实传递的不是托管堆中对象的地址(0x1000),而是栈上p_val(实参)的地址(0x0001)。
 
所以这里实参和形参都是栈上的同一个东西,没有什么区别了。我觉得这才是真正的传引用调用。
 
然后调用了函数ChangeByClassRefNew,函数中对p_val(形参)重新分配了内存(new操作),使其指向了新的地址(0x1100)。
 
由于p_val(形参)就是p_val(实参),所以p_val(形参)的name属性改变后,函数ChangeByClassRefNew外的p_val(实参)的name属性也被改变了。
 
而原先分配的对象(地址0x1000)其实已经没有被引用了,随时会被GC回收。
 
3. 结论
 
 
 
 
 如果传递的参数是基元类型(int,float等)或结构体(struct),那么就是传值调用。
  
 
 
 
 
 
 
 如果传递的参数前有ref或者out关键字,那么就是传引用调用。
  
 
 
 
 
 
 
 如果传递的参数是类(class)并且没有ref或out关键字: 如果调用的函数中对参数重新进行了地址分配(new操作),那么执行结果类似传值调用
 如果调用的函数中没有对参数重新进行了地址分配,直接就是使用了传递的参数,那么执行结果类似传引用调用