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

推荐订阅源

TaoSecurity Blog
TaoSecurity Blog
博客园 - 司徒正美
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 【当耐特】
M
MIT News - Artificial intelligence
罗磊的独立博客
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Stack Overflow Blog
Stack Overflow Blog
The GitHub Blog
The GitHub Blog
Google DeepMind News
Google DeepMind News
Security Archives - TechRepublic
Security Archives - TechRepublic
宝玉的分享
宝玉的分享
N
News and Events Feed by Topic
The Hacker News
The Hacker News
Google DeepMind News
Google DeepMind News
C
CERT Recently Published Vulnerability Notes
F
Full Disclosure
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Security @ Cisco Blogs
H
Hacker News: Front Page
L
LangChain Blog
Microsoft Security Blog
Microsoft Security Blog
Y
Y Combinator Blog
B
Blog RSS Feed
H
Heimdal Security Blog
Google Online Security Blog
Google Online Security Blog
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 三生石上(FineUI控件)
V2EX - 技术
V2EX - 技术
V
Vulnerabilities – Threatpost
Help Net Security
Help Net Security
Hacker News - Newest:
Hacker News - Newest: "LLM"
T
Tailwind CSS Blog
W
WeLiveSecurity
T
Tenable Blog
D
DataBreaches.Net
Martin Fowler
Martin Fowler
Cyberwarzone
Cyberwarzone
Cisco Talos Blog
Cisco Talos Blog
S
Secure Thoughts
O
OpenAI News
L
LINUX DO - 热门话题
Vercel News
Vercel News
阮一峰的网络日志
阮一峰的网络日志
Jina AI
Jina AI
J
Java Code Geeks
Know Your Adversary
Know Your Adversary
IT之家
IT之家
Latest news
Latest news
Cloudbric
Cloudbric

博客园 - 水言木

好久没来了,发个招聘贴 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就是实实在在的同一个引用了。