传值调用和传引用调用是几乎所有主流语言都会涉及到的问题,下面我谈谈我对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关键字:
    1. 如果调用的函数中对参数重新进行了地址分配(new操作),那么执行结果类似传值调用
    2. 如果调用的函数中没有对参数重新进行了地址分配,直接就是使用了传递的参数,那么执行结果类似传引用调用

深刻理解C#的传值调用和传引用调用的更多相关文章

  1. Java中的形参和实参的区别以及传值调用和传引用调用

    名词解析: 1.形参:用来接收调用该方法时传递的参数.只有在被调用的时候才分配内存空间,一旦调用结束,就释放内存空间.因此仅仅在方法内有效. 2.实参:传递给被调用方法的值,预先创建并赋予确定值. 3 ...

  2. 【Java-Method】读《重构》有感_Java方法到底是传值调用还是传引用调用(传钥匙调用)

    今天读<重构>P279, Separate Query from Modifier,将查询函数和修改函数分离. 问题的产生 突然想到 Java 的传对象作为参数的方法到底是 传引用调用,还 ...

  3. golang的传值调用和传引用调用

    传值还是传引用 调用函数时, 传入的参数的 传值 还是 传引用, 几乎是每种编程语言都会关注的问题. 最近在使用 golang 的时候, 由于 传值 和 传引用 的方式没有弄清楚, 导致了 BUG. ...

  4. Python FAQ1:传值,还是传引用?

    在C/C++中,传值和传引用是函数参数传递的两种方式.由于思维定式,从C/C++转过来的Python初学者也经常会感到疑惑:在Python中,函数参数传递是传值,还是传引用呢? 看下面两段代码: de ...

  5. Python中参数是传值,还是传引用?

    在 C/C++ 中,传值和传引用是函数参数传递的两种方式,在Python中参数是如何传递的?回答这个问题前,不如先来看两段代码. 代码段1: def foo(arg): arg = 2 print(a ...

  6. Python 函数中,参数是传值,还是传引用?

    在 C/C++ 中,传值和传引用是函数参数传递的两种方式,在Python中参数是如何传递的?回答这个问题前,不如先来看两段代码. 代码段1: def foo(arg): arg = 2 print(a ...

  7. 拷贝构造函数不能传值,只能传引用,而且一般是传const引用

    为什么呢?因为传值函数,需要调用拷贝构造函数,那就层层循环无止境了.

  8. php中传值与传引用的区别。什么时候传值什么时候传引用?

    值传递:   函数范围内对值的任何改变在函数外部都会被忽略; 引用传递: 函数范围内对值的任何改变在函数外部也能反映出这些修改: 优缺点:按值传递时,php必须复制值.特别是对于大型的字符串和对象来说 ...

  9. Java内存管理-Stackoverflow问答-Java是传值还是传引用?(十一)

    勿在流沙筑高台,出来混迟早要还的. 做一个积极的人 编码.改bug.提升自己 我有一个乐园,面向编程,春暖花开! 本文导图: 一.由一个提问引发的思考 在Stack Overflow 看到这样一个问题 ...

随机推荐

  1. 再也不要看到Eclipse万恶的arg0,arg1提示

    不知道大家跟我是否一下,遇到arg的提示. @Override public void onDateChanged(DatePicker arg0, int arg1, int arg2, int a ...

  2. FlatBuffers

    1 What is FlatBuffers. FlatBuffers is a serialization library for games and other memory constrained ...

  3. 九度OJ 1528 最长回文子串 -- Manacher算法

    题目地址:http://ac.jobdu.com/problem.php?pid=1528 题目描述: 回文串就是一个正读和反读都一样的字符串,比如"level"或者"n ...

  4. PC软件-实用工具 True Launch Bar

    True Launch Bar 官网 增强及自定义window任务栏快捷方式管理. 有免费版的Free Launch Bar 官网

  5. 获取input标签的所有属性

    1.用jquery$("input[name='btnAdd']").attr("value") 获取value属性值,其它属性换attr的参数就OK 例1: ...

  6. Oracle datafile特殊字符处理

    1.发现数据库的数据文件有特殊字符: 2.尝试在sqlplus下用将tablespace offline后修改 SQL> alter tablespace WST_DATA rename dat ...

  7. JAVA技术体系发展路线

    JAVA技术体系 1.1 Java程序员 ·高级特性 反射.泛型.注释符.自动装箱和拆箱.枚举类.可变参数.可变返回类型.增强循环.静态导入 ·核心编程 IO.多线程.实体类.集合类.正则表达式.XM ...

  8. jcom2在win7 X86上操作Excel

    浅谈Java中利用JCOM实现仿Excel编程   在JAVA中使用JCOM和JXL注意要点: (1)在你的lib下要有jdom-1.0.jar,jxl-2.5.5.jar,jcom-2.2.4.ja ...

  9. Source Insight 显示中文乱码

    Source Insight 3.X utf8支持插件震撼发布 继上次SI多标签插件之后,因为公司内部编码改为utf8编码,因此特意做了这个Source Insight 3.X utf8插件. 下载地 ...

  10. button的相关属性

    设置自定义按钮的文字大小 [submit.titleLabel setFont:[UIFont boldSystemFontOfSize:16]]; 设置按钮选中状态的颜色 [btn setTintC ...