C#中方法引數的引用傳遞、值傳遞。
一、值型別和引用型別
C# 中的型別一共分為兩類,一類是值型別(Value Type),一類是引用型別(Reference Type)。
值型別包括結構體(struct)和列舉(enum)。
引用型別包括類(class)、介面(interface)、委託(delegate)、陣列(array)等。常見的簡單型別如short、int、long、float、double、byte、char等其本質上都是結構體,對應struct System.Int16、System.Int32、System.Int64、System.Single、System.Double、Syetem.Byte、System.Char,因此它們都是值型別。但string和object例外,它們本質上是類,對應class System.String和System.Object,所以它們是引用型別。
1. 值型別
值型別變數本身儲存了該型別的全部資料,當宣告一個值型別的變數時,該變數會被分配到棧(Stack)上。
2. 引用型別
引用型別變數本身儲存的是位於堆(Heap)上的該型別的例項的記憶體地址,並不包含資料。當宣告一個引用型別變數時,該變數會被分配到棧上。如果僅僅只是宣告這樣一個變數,由於在堆上還沒有建立該型別的例項,因此,變數值為null,意思是不指向任何型別例項(堆上的物件)。對於變數的型別宣告,用於限制此變數可以儲存的型別。
二、值傳遞和引用傳遞
C#中方法的引數傳遞預設的是值傳遞,引用傳遞和輸出傳遞需要在引數型別前面對應加上ref、out限制符,由於輸出傳遞和引用傳遞類似,這裡只討論引用傳遞。
值傳遞引數是原變數的拷貝,值傳遞引數和原變數的記憶體地址不同,因此方法中對值傳遞引數的修改不會改變原變數。
引用傳遞引數是原變數的指標,引用傳遞引數和原變數的記憶體地址相同,相應的方法中對引用傳遞引數的修改會改變原變數。
三、傳遞值型別引數
1. 通過值傳遞值型別
class PassingValByVal
{
private static void Change(int x)
{
x = 10;
System.Console.WriteLine("The value inside the method: {0}" , x);
}
public static void Execute()
{
int n = 5;
System.Console.WriteLine("The value before calling the method: {0}", n);
Change(n);
System.Console.WriteLine("The value after calling the method: {0}", n);
System.Console.ReadLine();
}
}
- 原變數 n 是 int 值型別,系統為原變數 n 在堆疊Stack上分配的記憶體地址為:0x088aed2c,該記憶體地址儲存的資料值為5。
- 當呼叫 Change 方法時,由於是值傳遞,系統會為區域性引數變數 x 在堆疊Stack上分配一個新的記憶體區域, 記憶體地址為:0x088aecd0,並將 n 中的資料值 5 複製到區域性引數變數 x 中。這裡可以看到區域性引數變數 x 和原變數 n 的記憶體地址是不同的。
- 對區域性引數變數 x 的賦值操作是對變數記憶體地址中的資料值進行修改,此處是將區域性引數變數 x 的記憶體地址 0x088aecd0 中的資料值由原來的 5 改為 10。
- 因為原變數 n 和區域性引數變數 x 並不是同一塊記憶體區域(記憶體地址不同),所以 Change 方法中對區域性引數變數 x 的賦值操作不會影響到原變數 n。n 的值在呼叫 Change 方法前後是相同的。實際上,方法內發生的更改隻影響區域性引數變數 x。
2. 通過引用傳遞值型別
class PassingRefByVal
{
private static void Change(ref int x)
{
x = 10;
System.Console.WriteLine("The value inside the method: {0}", x);
}
public static void Execute()
{
int n = 5;
System.Console.WriteLine("The value before calling the method: {0}", n);
Change(ref n);
System.Console.WriteLine("The value after calling the method: {0}", n);
System.Console.ReadLine();
}
}
- 原變數 n 是 int 值型別,系統為原變數 n 在堆疊Stack上分配的記憶體地址為:0x0877eb4c,該記憶體地址儲存的資料值為5。
- 當呼叫 Change 方法時,由於是引用傳遞,區域性引數變數 x 的記憶體地址為原變數 n 的記憶體地址 0x0877eb4c,故區域性引數變數 x 的值即是原變數 n 的值。這裡可以看到區域性引數變數 x 和原變數 n 的記憶體地址是相同的。
- 對區域性引數變數 x 的賦值操作是對變數記憶體地址中的資料值進行修改,此處是將區域性引數變數 x 的記憶體地址 0x0877eb4c 中的資料值由原來的 5 改為 10。這裡監視視窗中 *&n 的值沒有改變是因為在 Change 方法程式碼塊中訪問不到原變數 n ,數值沒有重新整理。
- 因為原變數 n 和區域性引數變數 x 是同一塊記憶體區域(記憶體地址相同),所以呼叫 Change 方法後,可以看到變數 n 的記憶體地址 0x0877eb4c 中的資料值為 10。
四、傳遞引用型別引數
1. 通過值傳遞引用型別
class ClassA
{
public int a;
public ClassB classB;
}
class ClassB
{
public int b;
}
class PassingRefByVal
{
private static void Change(ClassA tempClassA)
{
tempClassA.a = 1;
tempClassA.classB.b = 1;
tempClassA = new ClassA();
tempClassA.a = 2;
tempClassA.classB = new ClassB();
tempClassA.classB.b = 2;
System.Console.WriteLine("The value inside the method: a = {0} ,b = {1}", tempClassA.a, tempClassA.classB.b);
}
public static void Execute()
{
ClassA classA = new ClassA();
classA.a = 0;
classA.classB = new ClassB();
classA.classB.b = 0;
System.Console.WriteLine("The value before calling the method: a = {0} ,b = {1}",
classA.a, classA.classB.b);
Change(classA);
System.Console.WriteLine("The value after calling the method: a = {0} ,b = {1}",
classA.a, classA.classB.b);
System.Console.ReadLine();
}
}
- 原變數 classA 是 ClassA 引用型別,系統為原變數 classA 在堆疊Stack上分配的記憶體地址為:0x086ce92c,該記憶體地址儲存的資料值為 37492360(十六進位制為 0x023c1688),該資料值是系統分配在託管堆Heap上的 ClassA 型別例項的記憶體地址。從圖中可以看到 classA.a 的記憶體地址是0X023c1690,0x023c1688~0x023c1690儲存了 ClassA 型別的方法表指標和 SyncBlockIndex 。
- 當呼叫 Change 方法時,由於是值傳遞,系統會為區域性引數變數 tempClassA 在堆疊Stack上分配一個新的記憶體區域, 記憶體地址為:0x086ce8c0,並將 classA 中的資料值 37492360(十六進位制為 0x023c1688) 複製到區域性引數變數 tempClassA 中。這裡可以看到區域性引數變數 tempClassA 和原變數 classA 的記憶體地址是不同的,但由於它們的記憶體地址中的資料值相同(均為 37492360,十六進位制為0x023c1688),所有它們指向了同一個記憶體地址,也就是同一個 ClassA 型別例項。
- 因為區域性引數變數 tempClassA 和原變數 classA 指向了同一個 ClassA型別例項,所以 tempClassA.a 和 classA.a、tempClassA.classB.b 和 classA.classB.b 是同一塊記憶體地址,因此對 tempClassA.a 和 tempClassA.classB.b 的賦值操作同時也是對 classA.a 和 classA.classB.b 的賦值操作。這裡監視視窗中 classA.a 和 classA.classB.b 的資料值還沒有重新整理。
- new運算子建立了一個新的 ClassA 型別的例項,系統為新例項在託管堆Heap上分配了另一塊記憶體區域,記憶體地址為 37631124(十六進位制為 0x023e3494),賦值操作將新例項的記憶體地址覆蓋 tempClassA 記憶體地址中的資料值,即 tempClassA 指向了新的物件。
- 因為區域性引數變數 tempClassA 指向了新建立的例項,後續對 tempClassA 的修改只會對新例項的記憶體區域的數值進行修改,而不會影響到原變數 classA。
2. 通過引用傳遞引用型別
class PassingRefByRef
{
private static void Change(ref ClassA tempClassA)
{
tempClassA.a = 1;
tempClassA.classB.b = 1;
tempClassA = new ClassA();
tempClassA.a = 2;
tempClassA.classB = new ClassB();
tempClassA.classB.b = 2;
System.Console.WriteLine("The value inside the method: a = {0} ,b = {1}",
tempClassA.a, tempClassA.classB.b);
}
public static void Execute()
{
ClassA classA = new ClassA();
classA.a = 0;
classA.classB = new ClassB();
classA.classB.b = 0;
System.Console.WriteLine("The value before calling the method: a = {0} ,b = {1}",
classA.a, classA.classB.b);
Change(ref classA);
System.Console.WriteLine("The value after calling the method: a = {0} ,b = {1}",
classA.a, classA.classB.b);
System.Console.ReadLine();
}
}
- 同理,引用傳遞的區域性引數變數的記憶體地址為原變數的記憶體地址,後續對區域性引數的修改同時也是對原變數的修改,故最後 classA.a 和 classA.classB.b 的值為2。
五、拷貝
拷貝即是通常所說的複製(Copy)或克隆(Clone),物件的拷貝也就是從當前物件複製一個“一模一樣”的新物件出來。雖然都是複製物件,但是不同的複製方法,複製出來的新物件卻並非完全一模一樣,物件內部存在著一些差異。通常的拷貝方法有兩種,即深拷貝和淺拷貝。
在淺拷貝中,拷貝物件會複製當前物件本身的資料(包括子物件的引用,拷貝物件和當前物件引用同一個子物件),但子物件的資料不會被複制。在深拷貝中,拷貝物件會複製當前物件所有的資料,包括當前物件子物件的資料;可以看出,深拷貝和淺拷貝之間的區別在於是否複製了子物件。
1. 淺拷貝
通過 MemberwiseClone 方法建立一個淺拷貝,該方法會建立一個新物件,然後將當前物件的非靜態欄位複製到新的物件。 如果一個欄位是值型別,則執行該欄位的逐位複製。 如果一個欄位是引用型別,則將引用複製但被引用的物件不會複製;因此,原始物件和其克隆引用同一物件。
class ClassA
{
public int a;
public ClassB classB;
public ClassA ShallowCopy()
{
return (ClassA)MemberwiseClone();
}
}
class TestShallowCopy
{
public static void Execute()
{
ClassA classA = new ClassA();
classA.a = 0;
classA.classB = new ClassB();
classA.classB.b = 0;
ClassA copy = classA.ShallowCopy();
System.Console.WriteLine("The value before change: a = {0} ,b = {1}",
copy.a, copy.classB.b);
classA.a = 1;
classA.classB.b = 1;
System.Console.WriteLine("The value after change: a = {0} ,b = {1}",
copy.a, copy.classB.b);
System.Console.ReadLine();
}
}
2. 深拷貝
class ClassA
{
public int a;
public ClassB classB;
public ClassA DeepCopy()
{
ClassA copy = new ClassA();
copy.a = a;
copy.classB = new ClassB();
copy.classB.b = classB.b;
return copy;
}
}
class TestDeepCopy
{
public static void Execute()
{
ClassA classA = new ClassA();
classA.a = 0;
classA.classB = new ClassB();
classA.classB.b = 0;
ClassA copy = classA.DeepCopy();
System.Console.WriteLine("The value before change: a = {0} ,b = {1}",
copy.a, copy.classB.b);
classA.a = 1;
classA.classB.b = 1;
System.Console.WriteLine("The value after change: a = {0} ,b = {1}",
copy.a, copy.classB.b);
System.Console.ReadLine();
}
}
3. ICloneable 介面
ICloneable 介面使您可以提供一個自定義的實現用於建立一個現有物件的拷貝。包含一個成員 Clone 方法,旨在提供超出 Object.MemberwiseClone 方法的克隆支援。
class ClassA : ICloneable
{
public int a;
public ClassB classB;
public object Clone()
{
ClassA copy = new ClassA();
copy.a = a;
copy.classB = new ClassB();
copy.classB.b = classB.b;
return copy;
}
}