前言
在本系列的第一篇文章《C#堆棧對比(Part One)》中,介紹了堆棧的基本功能和值類型以及引用類型在程序運行時的表現,同時也包含了指針作用的講解。
本文為文章的第二部分,主要講解參數在堆棧的作用。
注:限於本人英文理解能力,以及技術經驗,文中如有錯誤之處,還請各位不吝指出。
目錄
參數---重點討論事項
這就是當我們執行代碼時的詳細情況。我們在第一步已經講述了調用方法時所發生的情況,現在讓我們來看看更多細節…
當我們調用方法時,如下事情將發生:
- 當我們執行一個方法時需要在棧上創建一個空間。這包含了一個GOTO指令的地址調用(指針),所以當線程執行完我們的方法后它知道如何返回並繼續執行程序。
- 我們方法的參數將被拷貝。這就是我們要仔細去研究的東西。
- Control is passed to the JIT'ted method and the thread starts executing code. Hence, we have another method represented by a stack frame on the "call stack".
代碼片段:
public int AddFive(int pValue) { int result; result = pValue + 5; return result; }
棧將會是這樣:
注:方法並不真正在棧上,這里只是舉例演示說明。
正如我們Part One中所討論的,棧上的參數將被不同的方式處理,處理的方式又取決於它是值類型,還是引用類型。值類型是復制拷貝,引用類型是在傳遞引用本身。(A value types is copied over and the reference of a reference type is copied over.ed over.)
注:值類型是完全拷貝(復制)對象,新對象的值改變與否與影響原值;引用類型則拷貝的僅僅是指向類型的指針,在內存中共享同一個對象。
值類型傳遞
下面我們將討論值類型…
首先,當我們傳遞值類型時,空間將被創建並且將復制我們的類型到棧中的一個新空間,讓我們來分析如下代碼:
class Class1 { public void Go() { int x = 5; AddFive(x); Console.WriteLine(x.ToString()); } public int AddFive(int pValue) { pValue += 5; return pValue; } }
在開始執行程序時,變量x=5在棧上被分配了一個空間,如下圖:
下一步,AddFive()攜帶其參數被放置在棧上,參數被一個字節一個字節的從變量x中拷貝,如下圖:
當AddFive()方法執行完畢后,線程(指針入口)會到Go()方法處,並且由於AddFive()方法已經執行完成,pValue自然會被回收,如下圖:
注:此處線程指針回退到Go方法后臨時變量pValue將被回收,即下圖中的灰色模塊。
所以,正確的輸出是5,對嗎?重點的是,任何值類型被作為參數傳遞到一個方法時要進行一個全拷貝復制(carbon copy)並且原變量的值被保存下來而不受影響(we count on the original variable's value to be preserved.)。
我們必須記住的是,如果我們有一個很大的值類型(例如很大的一個結構體)並且將它作為參數傳遞至方法時,每次它將被拷貝復制並且花費很大的內存和CPU時間。棧的空間是有限的,正如從水龍頭往杯里灌水一樣,它總會溢出的。結構體是值類型,可能會非常大,我們在使用時必須要注意。
注:這里可以將結構體理解為一種值類型,在其作為參數傳遞至方法時,必然會進行復制拷貝,這樣如果結構體很占空間的話,則必然引起空間上以及內存上的效率問題,這點必須引起重視。
下面就是一個很大的結構體:
public struct MyStruct { long a, b, c, d, e, f, g, h, i, j, k, l, m; }
接下來,讓我們看看當執行Go方法時發生了什么:
public void Go() { MyStruct x = new MyStruct(); DoSomething(x); } public void DoSomething(MyStruct pValue) { // DO SOMETHING HERE.... }
這將是非常沒有效率的。想象一下,如果我們傳遞12000次,你就能理解為什么效率如此低下。
那么,我們如何繞開這個問題呢?答案就是,傳遞一個指向值類型的引用。如下所示:
public void Go() { MyStruct x = new MyStruct(); DoSomething(ref x); } public struct MyStruct { long a, b, c, d, e, f, g, h, i, j, k, l, m; } public void DoSomething(ref MyStruct pValue) { // DO SOMETHING HERE.... }
這樣,通過ref引用結構體之后我們將有效率的使用內存。
當我們用引用的方式傳遞值類型時,我們僅需關注值類型值的改變。pValue改變,則x同時改變。用下面的代碼,結果將是“12345”,因為pValue取決於x所代表的內存空間。
public void Go() { MyStruct x = new MyStruct(); x.a = 5; DoSomething(ref x); Console.WriteLine(x.a.ToString()); } public void DoSomething(ref MyStruct pValue) { pValue.a = 12345; }
傳遞引用類型
引用類型的傳遞類似於包裝值類型的引用方式,正如前面所提到的例子。
如果我們使用引用類型:
public class MyInt { public int MyValue; }
並且調用Go方法,MyInt對象最終處於堆上,因為它是引用類型:
public void Go() { MyInt x = new MyInt(); }
如果我們依照下面的方式執行Go方法:
public void Go() { MyInt x = new MyInt(); x.MyValue = 2; DoSomething(x); Console.WriteLine(x.MyValue.ToString()); } public void DoSomething(MyInt pValue) { pValue.MyValue = 12345; }
- 開始執行Go方法,變量x進入棧中。
- 執行DoSomething方法,參數pValue進入棧中。
- x(堆上MyInt的指針)被傳遞給pValue。(Thanks To CityHunter,糾正了語言表達上的錯誤~)
所以,當我們改變堆上的MyValue內的pValue之后我們再調用x,將會得到“12345”。
這就是十分有趣的地方。用引用的方式傳遞引用類型時發生了什么?
仔細討論一下。如果我們有“物體”(Thing Class),動物,蔬菜這幾類事物:
public class Thing { } public class Animal:Thing { public int Weight; } public class Vegetable:Thing { public int Length; }
然后我們按如下的方式執行Go方法:
public void Go() { Thing x = new Animal(); Switcharoo(ref x); Console.WriteLine( "x is Animal : " + (x is Animal).ToString()); Console.WriteLine( "x is Vegetable : " + (x is Vegetable).ToString()); } public void Switcharoo(ref Thing pValue) { pValue = new Vegetable(); }
然后我們得到如下結果:
x is Animal : False
x is Vegetable : True
接下來,讓我們看看發生了什么,如下圖:
- 開始執行Go方法,x指針在棧上被初始化。
- Animal類型在堆上被創建。
- 開始執行Switchroo方法,pValue在棧上被創建並指向x
4. Vegetable類被創建在堆上。
5. 更改x指針並指向Vegetable類型。
如果我們沒有用ref關鍵字傳遞“事物”(Thing),我們將保持Animal並從代碼中得到想反的結果。
如果沒有理解以上代碼,請參考我的類型引用段落,這樣能更好的理解引用類型如何工作的。
注:當聲明參數帶有ref關鍵字時,引用類型傳遞的是引用類型的指針,相反如果沒有ref關鍵字,參數傳遞的是新的指向引用內容的指針(引用)。在作者的例子中當存在ref關鍵字時,傳遞的是x(指針),如果Swtichroo方法不使用ref關鍵字時,實際是直接指向Animal。
讀者可去掉ref關鍵字,編譯即可,輸出結果則為:
x is Animal : True
x is Vegetable : False
與原文答案正相反。
總結
Part Two關注參數傳遞時在內存中的不同,在下一個部分,讓我們看看在棧上的引用變量以及克服一些當我們拷貝對象時產生的問題。
1. 值類型當參數時,復制拷貝為一個棧上的新對象,使用后回收。
2. 值類型當參數時,會發生拷貝現象,所以對一些“很大”的結構體類型會產生很嚴重的效率問題,可嘗試用ref 關鍵字將結構體包裝成引用類型進行傳遞,節省空間及時間。
3. 引用類型傳遞的是引用地址,即多個事物指向同一個內存塊,如果更改內存中的值將同時反饋到所有其引用的對象上。
4. Ref關鍵字傳遞的是引用類型的指針,而非引用類型地址。