ref(也包括out)关键字肯定都会用,传值调用和传址调用也是初学写代码时都已经历过的话题,与这相关的还有一些话题,比如值类型和引用类型有什么区别等,但是如果不仔细,可能有一些概念的混淆或者理解不够清晰(引用类型参数加ref关键字是多余的吗),本文试图以最简单的方式说明一下
有一些常见的说法:对于值类型传参就是传值调用,对于引用类型就是传址调用。如果加上ref关键字那就是传址调用,引用调用时,会改变原参数值,值调用时不会原改变参数值,看上去好像是的,那看一个例子:
public class MyClass{ public int Id { get; set; } } static void Invoke1(MyClass myClass) { myClass.Id = 0; } static void Invoke2(MyClass myClass) { myClass = new MyClass { Id = 50 }; } var myClass = new MyClass { Id = 100 };//原始值100 Invoke1(myClass); Console.WriteLine(myClass.Id); //100变为0 Invoke2(myClass); Console.WriteLine(myClass.Id); //依然是0
下面换一下将引用类型的参数加上ref关键字
public class MyClass{ public int Id { get; set; } } static void Invoke1(MyClass myClass) { myClass.Id = 0; } static void Invoke2(ref MyClass myClass) { myClass = new MyClass { Id = 50 }; } var myClass = new MyClass { Id = 100 };//原始值100 Invoke1(myClass); Console.WriteLine(myClass.Id); //100变为0 Invoke2(ref myClass); //这里加了ref Console.WriteLine(myClass.Id); //结果变了:0变为50
这里的现象是:
- 引用类型的参数,函数中的改变不一定会影响原来的参数
- 即使是引用类型,加上ref关键字以后也可能产生不一样的结果
那么ref关键字和传址调用还不是一回事,引用类型加传参ref关键字不是多余的(不考虑应用场景合理性),那怎么理解?
正常情况下(没有ref等关键字)的传参是怎么传的(包括引用类型和值类型)?
答案:传栈的副本
不管是值类型还是引用类型,传过去的都是栈的副本:
新的栈地址(栈的地址有改变) + 值副本(完全不变)
那么引用类型和值类型的参数传参行为是有区别的,区别在于值类型和引用类型的存储方式:
- 对于值类型:值副本就是原来的值
- 对于引用类型:值副本就是原来的托管堆地址
PS: 值类型栈上保存的值,引用类型栈上保存的托管堆的地址,真正的值在托管堆上
值类型传参对原参数无影响:栈地址和栈上的值都是副本,当然没影响
引用类型为什么有影响(不是所有情况都有影响):传过去的托管堆地址和原来的托管堆地址是同一个地址,引用类型数据在托管堆,所以操作是针对的同一个托管堆操作,托管堆值变了,原参数引用的也是这个托管堆,当然值也跟着变化。但是如果这种操作不是操作托管堆则不会影响以前的数据,比如把栈地址副本指向一个新的托管堆地址:
myClass = new MyClass { Id = 50 };
,这种操作是在托管堆上重新分配地址,然后把托管堆地址赋值给新栈副本,也就是副本栈的值不是原来的托管堆地址了,而是新的托管堆地址,那么这种改变对于原来的栈地址是没有任何影响的。
正常传参过程中值类型和引用类型内存示意图:那么ref关键字到底是有什么作用?
答案:传参数栈
PS: 不是传栈副本,而是参数栈,那么一切都好理解了,out也是一样的,只不过必须要赋值或者指向托管堆。但是这种情况又不一样 :Method(out var parameters),有兴趣可以看一下资料
为什么string类型是传值调用?
答案:string类型传参没任何特殊性,特殊性在于string类型的操作都是开辟新的托管堆,而不是改变原来托管堆值(string类型是比较特殊的引用类型,重写了一些方法和行为,这是另外一个话题)