1. 程式人生 > >ref以及傳值傳址的理解

ref以及傳值傳址的理解

  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型別是比較特殊的引用型別,重寫了一些方法和行為,這是另外一個話題)