標籤:
原文:譯文---C#堆VS棧(Part Two)
前言
在本系列的第一篇文章《C#堆棧對比(Part One)》中,介紹了堆棧的準系統和實值型別以及參考型別在程式運行時的表現,同時也包含了指標作用的講解。
本文為文章的第二部分,主要講解參數在堆棧的作用。
註:限於本人英文理解能力,以及技術經驗,文中如有錯誤之處,還請各位不吝指出。
目錄
C#堆棧對比(Part One)
C#堆棧對比(Part Two)
C#堆棧對比(Part Three)
C#堆棧對比(Part Four)
參數---重點討論事項
這就是當我們執行代碼時的詳細情況。我們在第一步已經講述了調用方法時所發生的情況,現在讓我們來看看更多細節…
當我們調用方法時,如下事情將發生:
- 當我們執行一個方法時需要在棧上建立一個空間。這包含了一個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。
所以,當我們改變堆上的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關鍵字傳遞的是參考型別的指標,而非參考型別地址。
譯文---C#堆VS棧(Part Two)