在本系列的第一篇文章《C#堆棧對比(Part One)》中,介紹了堆棧的基本功能和值類型以及引用類型在程序運行時的表現,同時也包含了指針做用的講解。html
本文爲文章的第二部分,主要講解參數在堆棧的做用。ui
注:限於本人英文理解能力,以及技術經驗,文中若有錯誤之處,還請各位不吝指出。spa
這就是當咱們執行代碼時的詳細狀況。咱們在第一步已經講述了調用方法時所發生的狀況,如今讓咱們來看看更多細節…htm
當咱們調用方法時,以下事情將發生:對象
代碼片斷:blog
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; }
因此,當咱們改變堆上的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
接下來,讓咱們看看發生了什麼,以下圖:
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關鍵字傳遞的是引用類型的指針,而非引用類型地址。