1. 程式人生 > >C#中方法引數的引用傳遞、值傳遞。

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;
    }
}