深刻理解C#的传值调用和传引用调用
传值调用和传引用调用是几乎所有主流语言都会涉及到的问题,下面我谈谈我对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操作),那么执行结果类似传值调用
- 如果调用的函数中没有对参数重新进行了地址分配,直接就是使用了传递的参数,那么执行结果类似传引用调用
深刻理解C#的传值调用和传引用调用的更多相关文章
- Java中的形参和实参的区别以及传值调用和传引用调用
名词解析: 1.形参:用来接收调用该方法时传递的参数.只有在被调用的时候才分配内存空间,一旦调用结束,就释放内存空间.因此仅仅在方法内有效. 2.实参:传递给被调用方法的值,预先创建并赋予确定值. 3 ...
- 【Java-Method】读《重构》有感_Java方法到底是传值调用还是传引用调用(传钥匙调用)
今天读<重构>P279, Separate Query from Modifier,将查询函数和修改函数分离. 问题的产生 突然想到 Java 的传对象作为参数的方法到底是 传引用调用,还 ...
- golang的传值调用和传引用调用
传值还是传引用 调用函数时, 传入的参数的 传值 还是 传引用, 几乎是每种编程语言都会关注的问题. 最近在使用 golang 的时候, 由于 传值 和 传引用 的方式没有弄清楚, 导致了 BUG. ...
- Python FAQ1:传值,还是传引用?
在C/C++中,传值和传引用是函数参数传递的两种方式.由于思维定式,从C/C++转过来的Python初学者也经常会感到疑惑:在Python中,函数参数传递是传值,还是传引用呢? 看下面两段代码: de ...
- Python中参数是传值,还是传引用?
在 C/C++ 中,传值和传引用是函数参数传递的两种方式,在Python中参数是如何传递的?回答这个问题前,不如先来看两段代码. 代码段1: def foo(arg): arg = 2 print(a ...
- Python 函数中,参数是传值,还是传引用?
在 C/C++ 中,传值和传引用是函数参数传递的两种方式,在Python中参数是如何传递的?回答这个问题前,不如先来看两段代码. 代码段1: def foo(arg): arg = 2 print(a ...
- 拷贝构造函数不能传值,只能传引用,而且一般是传const引用
为什么呢?因为传值函数,需要调用拷贝构造函数,那就层层循环无止境了.
- php中传值与传引用的区别。什么时候传值什么时候传引用?
值传递: 函数范围内对值的任何改变在函数外部都会被忽略; 引用传递: 函数范围内对值的任何改变在函数外部也能反映出这些修改: 优缺点:按值传递时,php必须复制值.特别是对于大型的字符串和对象来说 ...
- Java内存管理-Stackoverflow问答-Java是传值还是传引用?(十一)
勿在流沙筑高台,出来混迟早要还的. 做一个积极的人 编码.改bug.提升自己 我有一个乐园,面向编程,春暖花开! 本文导图: 一.由一个提问引发的思考 在Stack Overflow 看到这样一个问题 ...
随机推荐
- 2.MySQL入门基本操作初体验
启动和关闭mysql服务器: 一.启动方式 1.使用 mysqld 脚本启动:/etc/inint.d/mysqld start 2.使用 守护进程safe_mysqld 启动:safe_mysqld ...
- print neatly 整齐打印 算法导论
作者:jostree 转载请注明出处 http://www.cnblogs.com/jostree/p/4098562.html 题目链接:print neatly 整齐打印 算法导论 考虑在一个打印 ...
- hdu 1576 A/B
原题链接:hdu 1576 A/B 同样是用扩展的欧几里得算法.A = 9973k+n = xB,从而转化为:xB-9973k=n求解x即可. 具体扩展欧几里得算法请参考:hdu 2669 Roman ...
- C#关于编码、解码相关问题
编码.解码技术是我们在程序中开发中经常使用到的,对一些敏感信息的存储,比如密码之类的,我们一般是不会直接以明文直接存储到数据库的,而是会通过各种算法,可以是现成的MD5(一种散列算法).或者是Hash ...
- PHP 5.6.6 上运行 ecshop 2.7.3 不兼容问题整合
在安装完php在自己的服务器上以后, 发现在静态网页上出现了很多 error. 在网上查找过后发现,大部分问题是因为 PHP发展到PHP5.5版本以后,有了很多细微的变化.而ECSHOP官方更新又太慢 ...
- Pascal、VB、C#、Java四种语法对照表
因为工作原因,自学会了vb后陆续接触了其它语言,在工作中经常需要与各家使用不同语言公司的开发人员做程序对接,初期特别需要一个各种语法的对照比,翻看了网络上已有高人做了整理,自己在他基础上也整理了一下, ...
- hive 中的Sort By、 Order By、Cluster By、Distribute By 区别
Order by: order by 会对输入做全局排序,因此只有一个reducer(多个reducer无法保证全局有序)只有一个reducer,会导致当输入规模较大时,需要较长的计算时间.在hive ...
- UNICODE编码表
UNICODE简介 Unicode(统一码.万国码.单一码)是一种在计算机上使用的字符编码.Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进 ...
- VisualStudio自定义代码段_方法一
在VisualStudio里,使用代码段会提高我们的编写速度.其实,就是给一段代码加个快捷方式,使用时,快捷方式按键+2次Tab键. 举个例子: 比如输入Console.WriteLine (); 传 ...
- Git权威指南 读笔(2)
第七章 Git重置: Git提供了一个挽救机制,通过.git/logs目录下日志文件记录了分支的变更. master分支的日志文件.git/logs/refs/heads/master,显示最后5行: ...