ref以及传值传址的理解

  ref(也包括out)关键字肯定都会用,传值调用和传址调用也是初学写代码时都已经历过的话题,与这相关的还有一些话题,比如值类型和引用类型有什么区别等,但是如果不仔细,可能有一些概念的混淆或者理解不够清晰。本文试图以最简单的方式说明一下
  比如:对于值类型传参就是传值调用,对于引用类型就是传址调用。如果加上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);
Console.WriteLine(myClass.Id); //0变为50
 
这里的现象是:
  • 引用类型的参数,函数中的改变不一定会影响原来的参数
  • 即使是引用类型,加上ref关键字以后也可能产生不一样的结果
那么ref关键字 和 传址调用还不是一回事,那怎么理解?
 
正常情况下(没有ref等关键字)的传参是怎么传的(包括引用类型和值类型)?
答案:传栈的副本
 
不管是值类型还是引用类型:

传过去的都是栈的副本:新的栈地址(栈的地址有改变) + 值副本(完全不变)

那么引用类型和值类型的参数传参行为是有区别的,区别在这里:

  • 对于值类型:值副本就是原来的值
  • 对于引用类型:值副本就是原来的堆栈地址

PS: 值类型栈上保存的值,引用类型栈上保存的托管堆的地址,真正的值在托管堆上

值类型传参对原参数无影响:栈地址和栈上的值都是副本,当然没影响

引用类型为什么有影响(不是所有情况都有影响):传过去的堆栈地址和原来的堆栈地址是同一个地址,引用类型数据在堆栈,所以操作是针对的同一个堆栈操作,堆栈值变了,原参数引用的也是这个堆栈,当然值也跟着变化。但是如果这种操作不是操作堆栈则不会影响以前的数据(比如把栈地址副本指向一个新的堆栈地址),

myClass = new MyClass { Id = 50 };

,这种操作是在堆栈上重新分配地址,然后把堆栈地址赋值给新栈副本,也就是副本栈的值不是原来的堆栈地址了,而是新的堆栈地址,那么这种改变对于原来的栈地址是没有任何影响的。

正常传参过程中值类型和引用类型内存示意图:

 

 

那么ref关键字到底是有什么作用?

答案:传参数栈
PS: 不是传栈副本,而是参数栈,那么一切都好理解了,out也是一样的,只不过必须要赋值或者指向堆栈。但是这种情况又不一样 :Method(out var parameters),有兴趣可以看一下资料
为什么string类型是传值调用?
答案:string类型传参没任何特殊性,特殊性在于string类型的操作都是开辟新的堆栈,而不是改变原来堆栈值(string类型是比较特殊的引用类型,重写了一些方法和行为,这是另外一个话题)