原本是想寫String Pool,但感覺可以先寫這篇當前置知識。

目錄

  1. 什麼是Call By Value
  2. 什麼是Call By Reference
  3. 物件預設就是傳Ref嗎?
  4. 關於相等比較 == (Equals)
  5. 結語
  6. 參考資源

因為我認為傳參考只是隱藏指標的存在,但它本質上就是指標,所以會用比較偏指標的觀點來講。

 

MSDN對於value type與reference type的解說:

C# 中有兩種類型:參考類型和實值類型。 參考類型的變數會儲存期資料 (物件) 的參考,而實值類型的變數則會直接包含其資料。 使用參考類型時,這兩種變數可以參考相同的物件,因此對其中一個變數進行的作業可能會影響另一個變數所參考的物件。 使用實值型別時,每個變數都有自己的資料複本,因此對某一個變數進行的作業,不可能會影響其他變數。

 

什麼是Call By Value

通常Function在傳遞參數的時候,只要是實值型別(像是int、float、char這類),就是單純的把value複製一份到Function中,傳遞方與接收方互不影響

以基本的數值交換當範例 :

//
void swap(int a, int b){
    int temp = a;
    a = b;
    b = temp;
}

int t1 = 1, 
    t2 = 7;

swap(t1, t2);

/*
output:

t1 = 1, 
t2 = 7;
*/

 

所以在swap()中不管怎麼修改a, b,都不會改變原本的值t1, t2。

 

 

什麼是Call By Reference

參考型別(如string、array等)可以直接取得變數或物件的位址,並間接透過參考來操作物件,作用類似於指標,但卻不必使用指標語法,也就是不必使用*或&運算子來提取值,在C#中則是使用ref來做參考傳遞。

 

簡單來說,傳參考的意思就是,讓傳遞方與接收方的位置跟一樣,這樣修改接收方時,傳遞方也會跟著改變。

以剛剛的swap為例:

//
void swap(ref int a, ref int b){ 
    int temp = a; 
    a = b; 
    b = temp; 
} 

int t1 = 1, 
    t2 = 7; 

swap(ref t1, ref t2); 

/*
output: 

t1 = 7, 
t2 = 1; 
*/

因為在傳遞時a, b 的參考位置跟t1, t2一樣,所以修改a, b的時候,相當於在修改t1, t2,最後成功達成了數值交換的功能。

 

 

物件預設就是傳Ref嗎?

這邊以Array為例,Array沒用ref傳參考,但效果卻類似ref?

因為Array中的一串資料,本身就是參考,所以在正常傳遞的時候是傳value沒錯,但那個value本身就是陣列的實際位置。

//
void changeArray(int[] array){
    array[0] = 10;
    array[1] = 30;
    array[2] = 50;
}

int[] arr = new int[]{ 1, 3, 5};

changeArray(arr);

/*

output:

arr = {10, 30, 50}

*/

 

看起來有類似的效果,但本質不太一樣,因為物件本身就是各種資料的集合體,在傳遞的的時候,如果將所有資料當value一起傳遞非常消耗資源,因為會把所有資料各自再複製一份,所以參考型別的變數會以第一個資料的所在位置來代表這個物件,類似下圖:

從此圖可以看到,arr這個變數本身有一個位置,而他存的值就是陣列的第一個元素所在位置,當在在傳遞value的時候,其實就只是把0x10這個陣列起始位置傳進去,這就是為什麼不加ref也能在函式內修改時,也會影響到原本的arr內容。

(至於電腦如何知道這個記憶體值是陣列,我想這部分可能就要研究.Net編譯器原理了)

 

那麼對物件(參考型別)來說,pass by reference跟pass by value到底差在哪裡呢? 透過檢視記憶體位置來看看:

//
static void changeArray(int[] array) { }

static void changeArray(ref int[] array) { }

static void Main(string[] args){
    int[] arr = new int[] { 1, 3, 5 };
    /*             
    &arr       address   0x010ff488
     arr       value     0x03242420
    */

    changeArray(arr);
    /* 
    pass by value
    In function:
    &array     address   0x010ff3f0
     array     value     0x03242420
    */

    changeArray(ref arr);
    /*         
    pass by reference
    In function:                 
    &array     address   0x010ff488
     array     value     0x03242420
    */

    Console.Read();
}

可以清楚看到,透過pass by value傳遞,array變數是另開一個空間(0x20),來存這個陣列的所在位置(0x10),但透過pass by referencs,ref array變數的位置就是跟原本arr變數一樣(0x00),所以value也一樣(0x10)。

 

差別就是,如果你在函式內想new一個陣列給array(等於給array變數新的參考),就會發現pass by value完全沒有改變,因為array跟原本的arr完全沒有關係,而pass by reference就成功new了,範例:

static void changeArray(int[] array){
    array = new int[] { 10, 30, 50 };
}

static void changeArray(ref int[] array){
    array = new int[] { 10, 30, 50 };
}

static void Main(string[] args){
    int[] arr = new int[] { 1, 3, 5 };

    changeArray(arr);
    Console.WriteLine($"changeArray(arr)     : {arr[0]}");

    changeArray(ref arr);
    Console.WriteLine($"changeArray(ref arr) : {arr[0]}");

    Console.Read();
}

/*
output:

changeArray(arr)     : 1
changeArray(ref arr) : 10

*/

 

而最明顯的用途就是交換兩個物件,以string為例:

static void SwapStrings_PassbyValue(string s1, string s2)
{
    string temp = s1;
    s1 = s2;
    s2 = temp;
    Console.WriteLine("Inside the SwapStrings_PassbyValue     : {0} {1}", s1, s2);
}

static void SwapStrings_PassbyReference(ref string s1, ref string s2)
{
    string temp = s1;
    s1 = s2;
    s2 = temp;
    Console.WriteLine("Inside the SwapStrings_PassbyReference : {0} {1}", s1, s2);
}

static void Main(string[] args)
{
    string str1 = "Daivd",
           str2 = "Hello";

    Console.WriteLine($"str1, str2 : {str1} {str2}");

    Console.WriteLine(new string('-', 50));

    Console.WriteLine("Inside Main, before swapping           : {0} {1}", str1, str2);
    SwapStrings_PassbyValue(str1, str2);
    Console.WriteLine("Inside Main, artef swapping            : {0} {1}", str1, str2);

    Console.WriteLine(new string('-', 50));

    Console.WriteLine("Inside Main, before swapping           : {0} {1}", str1, str2);
    SwapStrings_PassbyReference(ref str1, ref str2);
    Console.WriteLine("Inside Main, after swapping            : {0} {1}", str1, str2);

    Console.Read();
}

/*
output:

str1, str2 : Daivd Hello
--------------------------------------------------
Inside Main, before swapping           : Daivd Hello
Inside the SwapStrings_PassbyValue     : Hello Daivd
Inside Main, artef swapping            : Daivd Hello
--------------------------------------------------
Inside Main, before swapping           : Daivd Hello
Inside the SwapStrings_PassbyReference : Hello Daivd
Inside Main, after swapping            : Hello Daivd

*/

 

從結果可以看到,如果要交換兩個物件,則必須透過pass by reference的方式來處理。

 

 

關於相等比較 == (Equals)

在C#中,其實 == 就是在比較兩個變數的value是否相等,講到這應該就能更清楚瞭解,為什麼兩個同樣內容的物件或陣列等等,直接透過 == 比較的結果都是False,而實值型別(int、double…)都能正常比較的原因了。

因為這些物件在建立的時候,就算是同樣的內容,但裡面的每個資料都是另外分配空間儲存,所以這些物件變數的value,都不會指向同一個東西。

int[] arr1 = new int[] { 1, 3, 5 };
int[] arr2 = new int[] { 1, 3, 5 };

Console.WriteLine($"arr1 == arr2 : {arr1 == arr2}");

/*
output:

arr1 == arr2 : False

*/

 

相等比較的部分自己稍微有些研究,但還沒辦法很清楚的講解,所以先停在這邊,有興趣可以參考最後附上的參考資源-關於C# 4種比較方式。

 

 

結語

關於Call by value、Call by reference大概就是這樣。

但還是有些例外,像是string:

string a1 = "hello";
string b1 = "hello";

Console.WriteLine(a1 == b1);
Console.WriteLine(Object.ReferenceEquals(a1, b1));
Console.WriteLine($"{a1.Address():X}, {b1.Address():X}");

/*
output:

a1 == b1                       : True
Object.ReferenceEquals(a1, b1) : True
a1,b1 address                  : 32423C8, 32423C8
*/

奇怪,明明string也是物件阿,但為什麼兩個物件變數的value會一樣?

因為C# 為了節省效能,特別設計了一個叫做String Intern Pool的機制,這部分等之後有空會在研究看看。

 

 

參考資源

ref (C# 參考)

傳遞參考類型的參數 (C# 程式設計手冊)

什麼是傳值call by value、傳址call by address、傳參考call by reference

 

關於C# 4種比較方式的延伸閱讀

C# ==、Equals、ReferenceEquals 区别与联系

More Effective C#中文版 | 寫出良好C#程式的50個具體做法 第二版 P43~P52

Object.ReferenceEquals(Object, Object)

Object.Equals

== Operator

最後修改日期: 2019-01-19

留言

撰寫回覆或留言

發佈留言必須填寫的電子郵件地址不會公開。