在探討本文的主題之前,先來介紹下C#中的實值型別和參考型別
眾所周知C#中有實值型別和參考型別,實值型別有基礎資料類型(諸如int,double,bool等)、結構體、枚舉,參考型別有介面、類、委託。
實值型別全部在作業系統的棧空間中申請,而參考型別則在作業系統的堆空間中建立對象,然後在棧空間中申請一個指標指向這個對象的地址。
因此C#的參考型別其實就如同C++的指標類型。
下面我再來看看函數傳參的問題。
早在C時代就有函數參數傳值和傳地址的概念,請記住在C#中函數參數預設都是傳值。
- 對於實值型別,函數是將實參變數的值在棧空間複製一份然後傳給形參變數。所以在函數中對形參變數的更改不會對實參變數造成任何影響,因為函數的形參只是實參的副本。
- 而對於參考型別,由於實參變數和形參變數都是參考型別,它們都指向記憶體堆中的某一對象的地址,函數是將實參變數指向的地址值複製了一份給形參變數,由於形參變數和實參變數指向堆中同一地址,所以在函數中使用形參變數對所指向對象所做的更改也會在實參變數中反映出來。
所以不管是實值型別還是參考型別在作為參數傳進函數時,其實都是傳的值,只不過參考型別傳的是對象在堆中的的地址罷了。
而且從上面的定義可以看出C#中參考型別的變數用C++來說就相當於是該參考型別的指標,比如有類(參考型別)RefClass:
RefClass rc就相當於是C++上的RefClass *rc
在C#中使用rc.IntValue++;時,相當於C++的rc->IntValue++;
因為參考型別在函數傳參時是傳地址的,所以我腦袋裡就形成了一種慣性思維,認為只要傳進函數的是參考型別,那麼在函數中做的任何更改都會反映到實參上。但是我發現並不完全是這樣,下面給出個例子(注釋內容為對應等效的C++代碼):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace RefWarn
{
class RefClass
{
public int IntValue
{
get;
set;
}
}
class Program
{
static void AddValue(RefClass prc)//RefClass *prc,prc和傳進來的rc指向同一個RefClass對象的地址
{
prc.IntValue++; //prc->IntValue++;
}
static void AddValue(ref RefClass prc)//RefClass **prc,prc指向傳進來的rc的地址
{
prc.IntValue++;//(*prc)->IntValue++;
}
static void ChangeRef(RefClass prc)//RefClass *prc,prc和傳進來的rc指向同一個RefClass對象的地址
{
prc = new RefClass() { IntValue = 1000 };
//prc=new RefClass() { IntValue = 1000 };請注意new關鍵字在C++中建立的是指向對象的指標不是對象
}
static void ChangeRef(ref RefClass prc)//RefClass **prc,prc指向傳進來的rc的地址
{
prc = new RefClass() { IntValue = 1000 };
//*prc=new RefClass() { IntValue = 1000 };請注意new關鍵字在C++中建立的是指向對象的指標不是對象
}
static void Main(string[] args)
{
RefClass rc = new RefClass() { IntValue = 1 };//RefClass *rc=new RefClass() { IntValue = 1 };請注意new關鍵字在C++中建立的是指向對象的指標不是對象
AddValue(rc);//rc,傳遞指向RefClass對象的指標
Console.WriteLine("調用AddValue(rc)後IntValue為:" + rc.IntValue);
rc.IntValue = 1;
AddValue(ref rc);//&rc,傳遞指向RefClass對象指標的指標
Console.WriteLine("調用AddValue(ref rc)後IntValue為:" + rc.IntValue);
rc.IntValue = 1;
ChangeRef(rc);//rc,傳遞指向RefClass對象的指標
Console.WriteLine("調用ChangeRef(rc)後IntValue為:" + rc.IntValue);
rc.IntValue = 1;
ChangeRef(ref rc);//&rc,傳遞指向RefClass對象指標的指標
Console.WriteLine("調用ChangeRef(ref rc)後IntValue為:" + rc.IntValue);
}
}
}
你會發現在Main函數中調用ChangeRef(rc)後,rc並沒有發生改變,其屬性IntValue的值還是1。
這是為什嗎?我們先來看看static void ChangeRef(RefClass prc)函數的結構,看看裡面都做了什麼
static void ChangeRef(RefClass prc)//RefClass *prc,prc和傳進來的rc指向同一個RefClass對象的地址
{
prc = new RefClass() { IntValue = 1000 };
//prc=new RefClass() { IntValue = 1000 };
}
可以看到函數裡就是對RefClass 類型的形參引用變數prc重新賦了值。但是最後我們看到這個賦值並沒有反應到實參引用變數rc上。原因其實很簡單就像本文開始所說的一樣,由於實參變數pc和形參變數rpc都是參考型別的變數,那麼它們實際上是在作業系統棧空間上的兩個指標,只不過指向的是作業系統堆空間上的同一個RefClass 對象。在函數ChangeRef中對引用變數rpc重新賦值,相當於是將棧中的rpc指標重新指向了堆中的另一個RefClass 對象。形參變數rpc指向的地址改變後,並不會對實參變數pc的指向發生改變,所以pc還是指向函數ChangeRef(RefClass prc)調用前的那個RefClass 對象。
但是也許你又會問為什麼AddValue(rc)執行後,函數對rc做了更改呢?我們來看看AddValue(RefClass prc)函數
static void AddValue(RefClass prc)//RefClass *prc,prc和傳進來的rc指向同一個RefClass對象的地址
{
prc.IntValue++; //prc->IntValue++;
}
請注意函數AddValue並不是更改了實參引用變數rc,它更改的是rc指向的RefClass 對象的屬性,是因為實參變數pc和形參變數rpc都指向同一個RefClass 對象的原理,所以在AddValue裡面rpc更改了它所指向RefClass 對象的屬性,也就等於更改了pc指向RefClass 對象的屬性。所以才在執行AddValue(rc)後給人一種好像rpc和pc是同一個變數,更改了rpc就等於更改了pc的錯覺。但是請記住這是絕對錯誤的,rpc和pc是兩個完全不同的引用變數,只不過指向的是記憶體中的同一個RefClass 對象。
最後我們來探討下有沒有辦法使函數在傳遞參考型別的參數時,讓形參完全等於實參呢?能否做到不管對形參是重新賦值還是做更改,都反映到實參上?
答案是肯定就是使用ref關鍵字
這個關鍵字用在實值型別上的時候,就相當於C++的指標類型,比如:
ref int param
就相當於C++的
int *param
且該指標指向的就是其對應的實參變數
所以在C#中使用聲明為ref的int形參變數param.ToString()時候,相當於C++上使用int指標*param.ToString()
所以在使用聲明為ref的int形參param時,就相當於是C++上的*param,其操作的就是param指向的那個int變數,即實參。
而當這個關鍵字用在參考型別前面的時候,就相當於是指向參考型別變數的地址,而前面說過C#參考型別的變數就相當於是C++的指標,那麼指向參考型別變數的地址也相當於就是指向指標的指標。
因為前面說了RefClass rc相當於C++的RefClass *rc
那麼ref RefClass rc相當於C++的RefClass **rc
在C#中使用聲明為ref的RefClass變數rc.ToString()時,當於C++上上使用RefClass指標的指標*rc->ToString()
所以在使用聲明為ref的RefClass類型形參rc時,就相當於是C++上的*rc(注意*rc還是指標,因為rc是指向指標的指標),其操作的是形參rc指向的那個RefClass類型的引用變數(即rc指向的是實參變數的地址,而不實參變數指向堆空間中對象的地址),即實參。
而實參前面的ref相當於是C++的&符號即取該變數的地址。
所以在函數形參前加上ref那麼形參變數指向的就是實參變數的地址,只不過如果實參類型是實值型別,那麼形參變數指向的就是該實參變數在作業系統棧中的地址。如果實參是參考型別,那麼形參變數指向的也是實參變數在作業系統棧中的地址,只不過該實參變數又指向對象在作業系統堆中的地址。所以無論是參考型別還是實值型別,只要在其作為形參時在前面加上ref,那麼形參變數都是指向實參變數的指標,則操作形參變數就等於是在操作實參變數。
最後一定要清楚在參考型別做函數形參時,加上ref和不加ref的不同。
還是拿RefClass rc來舉例:
- 不加ref時形參變數rc是指向作業系統堆空間中RefClass對象的指標,其和實參變數共同指向這個RefClass對象的地址,形參變數和實參變數之間無直接關係,通過形參變數rc對RefClass對象所做的更改,同樣也可以通過實參變數看到,但是對形參變數rc本身做更改(比如改變其指向的地址),並不會對實參變數產生任何影響。
- 加上ref時形參變數rc指向的是實參變數的地址,rc直接指向實參變數,由於實參本身就是指標,所以rc就是指向指標的指標,*rc就完全等於實參變數。對更改形參便變數*rc就是更改實參變數。