惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
GbyAI
GbyAI
SecWiki News
SecWiki News
Project Zero
Project Zero
C
Cisco Blogs
Simon Willison's Weblog
Simon Willison's Weblog
P
Privacy International News Feed
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Scott Helme
Scott Helme
A
Arctic Wolf
Security Latest
Security Latest
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
P
Privacy & Cybersecurity Law Blog
Apple Machine Learning Research
Apple Machine Learning Research
T
Tailwind CSS Blog
The Hacker News
The Hacker News
T
Tenable Blog
雷峰网
雷峰网
有赞技术团队
有赞技术团队
V
V2EX
C
CXSECURITY Database RSS Feed - CXSecurity.com
T
Threat Research - Cisco Blogs
T
Threatpost
AWS News Blog
AWS News Blog
L
LINUX DO - 热门话题
Application and Cybersecurity Blog
Application and Cybersecurity Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
S
SegmentFault 最新的问题
月光博客
月光博客
Spread Privacy
Spread Privacy
S
Secure Thoughts
宝玉的分享
宝玉的分享
博客园 - 三生石上(FineUI控件)
Forbes - Security
Forbes - Security
T
The Exploit Database - CXSecurity.com
G
GRAHAM CLULEY
The Last Watchdog
The Last Watchdog
Y
Y Combinator Blog
I
Intezer
博客园 - 【当耐特】
B
Blog RSS Feed
Attack and Defense Labs
Attack and Defense Labs
I
InfoQ
博客园 - 叶小钗
Cyberwarzone
Cyberwarzone
V2EX - 技术
V2EX - 技术
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Hugging Face - Blog
Hugging Face - Blog
H
Help Net Security
C
CERT Recently Published Vulnerability Notes

博客园 - 水言木

好久没来了,发个招聘贴 RavenDB Tips: 巧妙设计文档的Id格式以提升查询性能 CQRS实践(4): 领域事件 CQRS实践(3): Command执行结果的返回 CQRS实践(2): Command的实现 CQRS实践(1): 什么是CQRS 准备重新开始写点东西 不要让编写set访问器成为习惯 [PoEAA] Learning Notes of Data Transfer Object 再谈“引用” 使用UrlRewriter进行Url重写的完整解决方案 迅速搭建SVN环境 小议GridView中的删除按钮 工厂的那些事儿 关于Ajax的只言片语[2008.7.31最后更新] 重温:从aspx到dll 关于ASP.NET的只言片语[2008.8.12最后更新] .ctor,.cctor 以及 对象的构造过程 汶川地震小感
小记一次让我误会的引用传参
水言木 · 2008-06-18 · via 博客园 - 水言木

大部分时候,引用类型和值类型的区别是显而易见的,但在用引用类型作为方法参数时,却多次引起了我的误解,每次都得对着电脑抓狂一下才又一次发现问题所在。

问题在于如下代码段:

Code Segment 1

此时会输出20,这很明显,现在加上了一个方法AddAgeB:

public static void AddAgeB(Person p)

当把Main方法中的AddAgeA(p)改成AddAgeB(p)后,就输出了10,而不再是20。

于是我自言自语:不是说class都是引用类型么?AddAgeB一样是接收一个引用类型Person,虽然方法体中新new了一个Person,但是紧跟在后面的一句一样对这个引用类型的age字段赋值了啊?
在Main方法中Person p = new Person() (#1处)时,在堆中就分配了一块空间,下一句向这块空间中的age所在的内存空间存入了10,然后把p这个引用传递给AddAgeB()方法,在AddAgeB()中又在堆中开辟了一块新的空间(#2处),然后把新开辟的内存地址赋给p,下一句把新空间中的age赋为20,这么说来,Main()中的输出应该变成20了啊?难道这样不对么?

我抓狂了一下后发现,上面我的自言自语中缺失了一个很重要的细节,而这个细节,就是关键所在。问题出在这一句:“然后把p这个引用传递给AddAgeB()方法”,这太含糊了,我忘记了这里的“引用传递”其实是一个“复制”的过程。在上面两个AddAge方法体中的p,其实只是Main方法中的p的一个副本,当执行:Person p = new Person() 时,会在托管堆上开辟一块地址空间,存放Person的一个实例,这个实例的age字段就存在这个空间里头,同时,在栈上还会分配一块内存空间,它用来存放Person实例所在的堆内存地址,而p的值,就是栈上的这个地址。当执行 AddAgeA(p) 或 AddAgeB(p)时,栈上p的值(内存地址)会被复制,而在AddAge方法体中所使用的p,就是这个复制出来的副本,而不是原来的p,但是,它们的值(地址)是一样的,都是指向托管堆中的Person实例地址,所以,在AddAgeA()中,当执行p.age = 20时,托管堆中的那个Person实例的age字段的值就变成了20,于是,在Main方法中输出时,因为Main方法中的p也是指向这块堆空间地址,所以输出了20。

而在AddAgeB()中,有关键的一句:p = new Person(),这一句会在托管堆中再开辟一块新的内存空间,把后把它的地址赋给栈上的p,在这个时候,Main方法中的p和AddAgeB方法中的p不仅在栈上的位置不一样,值也不一样,当在执行下一句:p.age = 20;时,新开辟的堆空间中的age的值的的确确是改变了,但是在出了方法体的右花括号之后,方法体中的p(在栈上)就因为作用域的原因而被释放,那么,AddAgeB方法中创建的那个Person实例就成了一个等待GC来回收的垃圾,因为此时堆中存在了两个Person实例,在Main方法中的那个p所指向的堆空间中的age并没有被改变,所以在Main方法中输出了10。

再回头看看装箱:

Box Main

很显然,此时输出了 a = 5 , obj = 10。
在第二行中,有一个装箱操作,a的值为被“复制”一份到obj中,所以a的值不变,obj的值在执行obj = 10之后变了。

现在改一下:

public static void Main() {
    
int a = 5;
    
object obj = a;
    Change(obj);
    Console.WriteLine(
"a = " + a);
    Console.WriteLine(
"obj = " + obj);
}

public void Change(object obj) {
    obj 
= 10;
}

此时会输出什么呢?
正确答案是a = 5, obj = 5

由此也可得知,当在Change方法体中执行obj = 10时,10会被装到托管堆中的箱子里面去,然后obj的值被重新赋为这个箱子所在的起始地址,由于方法体中的obj引用的值跟Main方法中的obj引用的值并不一样,所以,输出的obj还是5。

总结一下,其实就是几个字:

如果没有用ref或out关键字的话,在给方法传递参数时,会有一个复制的过程,不管是值类型还是引用类型。

当然,用上了ref的话,就没有了这个复制过程,Main方法中的p和AddAge方法中的p就是实实在在的同一个引用了。