博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
深刻理解C#的传值调用和传引用调用
阅读量:6115 次
发布时间:2019-06-21

本文共 7461 字,大约阅读时间需要 24 分钟。

传值调用和传引用调用是几乎所有主流语言都会涉及到的问题,下面我谈谈我对C#中传值调用和传引用调用的理解。

1. 一般对C#中传值调用和传引用调用的理解

  • 如果传递的参数是基元类型(int,float等)或结构体(struct),那么就是传值调用。
  • 如果传递的参数是类(class)那么就是传引用调用。
  • 如果传递的参数前有ref或者out关键字,那么就是传引用调用。

验证示例的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
using 
System;
 
public 
class 
ArgsByRefOrValue
{
    
public 
static 
void 
Main(
string
[] args)
    
{
        
// 实验1. 传值调用--基元类型
        
int 
i = 10;
        
Console.WriteLine(
"before call ChangeByInt: i = " 
+ i.ToString());
        
ChangeByInt(i);
        
Console.WriteLine(
"after call ChangeByInt: i = " 
+ i.ToString());
 
        
Console.WriteLine(
"=============================================="
);
        
// 实验2. 传值调用--结构体
        
Person_val p_val =
new 
Person_val();
        
p_val.name =
"old val name"
;
        
Console.WriteLine(
"before call ChangeByStruct: p_val.name = " 
+ p_val.name);
        
ChangeByStruct(p_val);
        
Console.WriteLine(
"after call ChangeByStruct: p_val.name = " 
+ p_val.name);
 
        
Console.WriteLine(
"=============================================="
);
        
// 实验3. 传引用调用--类
        
Person_ref p_ref =
new 
Person_ref();
        
p_ref.name =
"old ref name"
;
        
Console.WriteLine(
"before call ChangeByClass: p_ref.name = " 
+ p_ref.name);
        
ChangeByClass(p_ref);
        
Console.WriteLine(
"after call ChangeByClass: p_ref.name = " 
+ p_ref.name);
 
        
Console.WriteLine(
"=============================================="
);
        
// 实验4. 传引用调用--利用ref
        
Person_ref p =
new 
Person_ref();
        
p.name =
"old ref name"
;
        
Console.WriteLine(
"before call ChangeByClassRef: p.name = " 
+ p.name);
        
ChangeByClassRef(
ref 
p);
        
Console.WriteLine(
"after call ChangeByClassRef: p.name = " 
+ p.name);
 
        
Console.ReadKey(
true
);
    
}
 
    
static 
void 
ChangeByInt(
int 
i)
    
{
        
i = i + 10;
        
Console.WriteLine(
"when calling ChangeByInt: i = " 
+ i.ToString());
    
}
 
    
static 
void 
ChangeByStruct(Person_val p_val)
    
{
        
p_val.name =
"new val name"
;
        
Console.WriteLine(
"when calling ChangeByStruct: p_val.name = " 
+ p_val.name);
    
}
 
    
static 
void 
ChangeByClass(Person_ref p_ref)
    
{
        
p_ref.name =
"new ref name"
;
        
Console.WriteLine(
"when calling ChangeByClass: p_ref.name = " 
+ p_ref.name);
    
}
 
    
static 
void 
ChangeByClassRef(
ref 
Person_ref p)
    
{
        
p.name =
"new ref name"
;
        
Console.WriteLine(
"when calling ChangeByClassRef: p.name = " 
+ p.name);
    
}
}
 
public 
struct 
Person_val
{
    
public 
string 
name;
}
 
public 
class 
Person_ref
{
    
public 
string 
name;
}

运行结果如下:

2011051823155221.png

看起来似乎上面代码中实验3实验4是一样的,即对于类(class)来说,不管加不加ref或out,都是传引用调用。

其实,这只是表面的现象,只要稍微改一下代码,结果就不一样了。

修改上面代码,再增加两个实验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
using 
System;
 
public 
class 
ArgsByRefOrValue
{
    
public 
static 
void 
Main(
string
[] args)
    
{
        
// 实验1. 传值调用--基元类型
        
int 
i = 10;
        
Console.WriteLine(
"before call ChangeByInt: i = " 
+ i.ToString());
        
ChangeByInt(i);
        
Console.WriteLine(
"after call ChangeByInt: i = " 
+ i.ToString());
 
        
Console.WriteLine(
"=============================================="
);
        
// 实验2. 传值调用--结构体
        
Person_val p_val =
new 
Person_val();
        
p_val.name =
"old val name"
;
        
Console.WriteLine(
"before call ChangeByStruct: p_val.name = " 
+ p_val.name);
        
ChangeByStruct(p_val);
        
Console.WriteLine(
"after call ChangeByStruct: p_val.name = " 
+ p_val.name);
 
        
Console.WriteLine(
"=============================================="
);
        
// 实验3. 传引用调用--类
        
Person_ref p_ref =
new 
Person_ref();
        
p_ref.name =
"old ref name"
;
        
Console.WriteLine(
"before call ChangeByClass: p_ref.name = " 
+ p_ref.name);
        
ChangeByClass(p_ref);
        
Console.WriteLine(
"after call ChangeByClass: p_ref.name = " 
+ p_ref.name);
 
        
Console.WriteLine(
"=============================================="
);
        
// 实验4. 传引用调用--利用ref
        
Person_ref p =
new 
Person_ref();
        
p.name =
"old ref name"
;
        
Console.WriteLine(
"before call ChangeByClassRef: p.name = " 
+ p.name);
        
ChangeByClassRef(
ref 
p);
        
Console.WriteLine(
"after call ChangeByClassRef: p.name = " 
+ p.name);
 
        
Console.WriteLine(
"=============================================="
);
        
// 实验5. 传引用调用--类 在调用的函数重新new一个对象
        
Person_ref p_ref_new =
new 
Person_ref();
        
p_ref_new.name =
"old new ref name"
;
        
Console.WriteLine(
"before call ChangeByClassNew: p_ref_new.name = " 
+ p_ref_new.name);
        
ChangeByClassNew(p_ref_new);
        
Console.WriteLine(
"after call ChangeByClassNew: p_ref_new.name = " 
+ p_ref_new.name);
 
        
Console.WriteLine(
"=============================================="
);
        
// 实验6. 传引用调用--利用ref 在调用的函数重新new一个对象
        
Person_ref p_new =
new 
Person_ref();
        
p_new.name =
"old new ref name"
;
        
Console.WriteLine(
"before call ChangeByClassRefNew: p_new.name = " 
+ p_new.name);
        
ChangeByClassRefNew(
ref 
p_new);
        
Console.WriteLine(
"after call ChangeByClassRefNew: p_new.name = " 
+ p_new.name);
 
        
Console.ReadKey(
true
);
    
}
 
    
static 
void 
ChangeByInt(
int 
i)
    
{
        
i = i + 10;
        
Console.WriteLine(
"when calling ChangeByInt: i = " 
+ i.ToString());
    
}
 
    
static 
void 
ChangeByStruct(Person_val p_val)
    
{
        
p_val.name =
"new val name"
;
        
Console.WriteLine(
"when calling ChangeByStruct: p_val.name = " 
+ p_val.name);
    
}
 
    
static 
void 
ChangeByClass(Person_ref p_ref)
    
{
        
p_ref.name =
"new ref name"
;
        
Console.WriteLine(
"when calling ChangeByClass: p_ref.name = " 
+ p_ref.name);
    
}
 
    
static 
void 
ChangeByClassRef(
ref 
Person_ref p)
    
{
        
p.name =
"new ref name"
;
        
Console.WriteLine(
"when calling ChangeByClassRef: p.name = " 
+ p.name);
    
}
 
    
static 
void 
ChangeByClassNew(Person_ref p_ref_new)
    
{
        
p_ref_new =
new 
Person_ref();
        
p_ref_new.name =
"new ref name"
;
        
Console.WriteLine(
"when calling ChangeByClassNew: p_ref_new.name = " 
+ p_ref_new.name);
    
}
 
    
static 
void 
ChangeByClassRefNew(
ref 
Person_ref p_new)
    
{
        
p_new =
new 
Person_ref();
        
p_new.name =
"new ref name"
;
        
Console.WriteLine(
"when calling ChangeByClassRefNew: p_new.name = " 
+ p_new.name);
    
}
}
 
public 
struct 
Person_val
{
    
public 
string 
name;
}
 
public 
class 
Person_ref
{
    
public 
string 
name;
}

则运行结果为:

2011051823171086.png

实验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. 如果调用的函数中没有对参数重新进行了地址分配,直接就是使用了传递的参数,那么执行结果类似传引用调用
本文转自wang_yb博客园博客,原文链接:http://www.cnblogs.com/wang_yb/archive/2011/05/18/2050574.html,如需转载请自行联系原作者
你可能感兴趣的文章
AMD64与IA64的区别-64位操作系统
查看>>
我的友情链接
查看>>
配置远程桌面服务会话的超时设置和重新连接设置
查看>>
linux硬盘安装的方法
查看>>
Android判断、创建和删除快捷方式
查看>>
云平台编程与开发(二):X5Cloud云平台SDK包概述
查看>>
Android图片失真问题
查看>>
我的友情链接
查看>>
通过路由配置提高Wi-Fi速度和距离
查看>>
使用Gradle构建Java项目
查看>>
Leetcode PHP题解--D26 766. Toeplitz Matrix
查看>>
爬虫简单入门-接口寻找调用
查看>>
mysql常用语句
查看>>
程序员随想-关于优雅
查看>>
爱加密联合应用之星(APPSTAR)为开发者提供免费云加密服务
查看>>
部署基于Centos7的Zimbra邮件系统-之一系统规划及DNS服务配置
查看>>
如何理解比特币的底层协议
查看>>
cocos集成科大讯飞语音识别
查看>>
The Reactive Manifesto(响应式宣言)
查看>>
R语言笔记 attach()、detach()和with()
查看>>