【.NET版月经问题】之二【引用类型参数就是按引用传递吗?】

  1. 云栖社区>
  2. 博客>
  3. 正文

【.NET版月经问题】之二【引用类型参数就是按引用传递吗?】

技术小甜 2017-11-09 13:58:00 浏览939
展开阅读全文

这个问题其实算不得月经帖问题,已经成周经甚至日经了...隔几天就有人问,然后总是大部分人站在自以为正确的角度大谈引用与指针的相似性,假如不幸这参数是string类型无一例外会被人扯到string的“不可变性”上去,而且每次都有扛着红星星的高人大撒烟雾弹,让提问者一头雾水...

这问题产生的来源是可恶的C/C++指针...很多人指点新手(包括很多培训机构的讲师)讲到“引用”这一.NET的特殊数据类型时,都直接一句:相当于(甚至说“就是”)C/C++的指针——其实我也说过,惭愧...不幸的是两者看似相似,本质却不同...事实上这个引用和Java的引用才是非常相似的东西,几乎一样...

这就带来一个问题...凡是学过C/C++指针的,总有很多人想当然地认为传递一个引用类型的参数就是传递这个引用类型实例的引用——我还没说指针呢——看起来似乎是这样...大错特错!

在.NET中,除非显式以ref或out声明传递参数,否则所有类型的参数都是按值传递的!引用类型也不会例外!

 

 

这句话我不知道回答过多少遍,不过看起来没什么效果...每次一出此类问题,照旧大堆人往按引用传递和string不可变上引...

那么先来看看实例吧...值类型一般不会有疑义,就不提了...

view plaincopy to clipboardprint?

  1. class test 
  2. public string s = null; 
  3. public int i = 0; 
  4.  
  5. public void run() 
  6.     test instance = new test(); 
  7.     instance.s = "first"; 
  8.     instance.i = 1; 
  9.     callByValue(instance); 
  10.  
  11. public void callByValue(test t) 
  12.     t.s = "changed"; 
  13.     t.i = 2; 

class test { public string s = null; public int i = 0; } public void run() { test instance = new test(); instance.s = "first"; instance.i = 1; callByValue(instance); } public void callByValue(test t) { t.s = "changed"; t.i = 2; }

这段代码就是广泛会被误认为按引用传递实际上是按值传递的参数传递方式...它的执行结果非常明显,参数引用类型test的实例instance的成员s和i一定会被更改,所以看起来它似乎确实是按引用传递的...但是,错的!这个参数传递的是该参数实例的一个副本!

引用类型实例的副本是什么呢?就是这个instance的引用的副本...也就是说,这个时候在栈上,原来的instance的引用还在,传递给callByValue方法的参数t是栈上instance的引用的copy...这个copy引用指向托管堆上instance值的地址,所以一改俱改...所以表象似乎一样,但和C/C++传递指针的方式本质是差别巨大的...

我们把callByValue方法稍作修改,如下...

view plaincopy to clipboardprint?

  1. public void callByValue(test t) 
  2.     test tmp=new test(); 
  3.     tmp.s = "changed"; 
  4.     tmp.i = 2; 
  5.     t = tmp; 

public void callByValue(test t) { test tmp=new test(); tmp.s = "changed"; tmp.i = 2; t = tmp; }

结果很显然,不会变...instance和instance的成员都不会变,原因呢?最常见的解释是作用域不同,tmp只在callByValue方法体中存活,所以呢,出了这个方法体就不起作用了...胡说八道!出了方法体tmp废弃了,那instance应该是null才对啊?!什么思维逻辑...其实很简单,上面说了,这个传递进来的引用只是个副本,修改这个副本不会对在栈上的instance引用有丝毫影响,新构造的实例也跟托管堆上instance的值毫不相干...当退出方法体时,这个副本随即被当作垃圾废弃,instance和它的成员自然不会变...

看看另一个方法...

view plaincopy to clipboardprint?

  1. public void callByReference(ref test t) 
  2.     test tmp=new test(); 
  3.     tmp.s = "changed"; 
  4.     tmp.i = 2; 
  5.     t = tmp; 

public void callByReference(ref test t) { test tmp=new test(); tmp.s = "changed"; tmp.i = 2; t = tmp; }

这就改变了...tmp被废弃了,但是instance却改变了...因为这时传递进去的是instance的引用本身,自然在t=tmp;时instance的引用被修改指向tmp在托管堆上的值...

最后来看看“特殊”的string...在被当做参数传递时,string一点也不特殊...

view plaincopy to clipboardprint?

  1. public void run() 
  2. string instance = "first"; 
  3.     callByValue(instance); 
  4.     callByReference(instance); 
  5.  
  6. public void callByValue(string s) 
  7.     s = "changed"; 
  8.  
  9. public void callByReference(ref string s) 
  10.     s = "changed"; 

public void run() { string instance = "first"; callByValue(instance); callByReference(instance); } public void callByValue(string s) { s = "changed"; } public void callByReference(ref string s) { s = "changed"; }

callByValue方法一定不会更改instance的值,而callByReference方法一定会更改instance的值...原因和上面一样,前者传递的是instance的引用的副本,后者传递的是instance的引用本身...跟什么“不可变性”、“字符串驻留”毫不相干...

此处存疑:

此处有个问题,string传到下面方法的时候,确实是传递了一个引用的副本。 
public void callByValue(string s)  
{  
    s = "changed";  
}

那么既然是引用的副本,那么指向的实际内容就必定是是同一个地址,同一个数据。然后再对这个引用副本赋值,理论上是指向的内容会被更改,但是实际上,正是由于string的特殊性,他不能直接在该地址空间上做修改,而只能开辟新空间,所以导致引用副本指向新开辟的空间。

所以这里和string的特殊性是有关系的。
















本文转自cnn23711151CTO博客,原文链接:http://blog.51cto.com/cnn237111/601458 ,如需转载请自行联系原作者



网友评论

登录后评论
0/500
评论
技术小甜
+ 关注