譯文---C#堆VS棧(Part Two)

前言

         在本系列的第一篇文章《C#堆棧對比(Part One)》中,介紹了堆棧的基本功能和值類型以及引用類型在程序運行時的表現,同時也包含了指針做用的講解。html

         本文爲文章的第二部分,主要講解參數在堆棧的做用。ui

 

         注:限於本人英文理解能力,以及技術經驗,文中若有錯誤之處,還請各位不吝指出。spa

目錄

C#堆棧對比(Part One)線程

C#堆棧對比(Part Two3d

C#堆棧對比(Part Three)指針

C#堆棧對比(Part Four)code

參數---重點討論事項

      這就是當咱們執行代碼時的詳細狀況。咱們在第一步已經講述了調用方法時所發生的狀況,如今讓咱們來看看更多細節…htm

      當咱們調用方法時,以下事情將發生:對象

  1. 當咱們執行一個方法時須要在棧上建立一個空間。這包含了一個GOTO指令的地址調用(指針),因此當線程執行完咱們的方法後它知道如何返回並繼續執行程序。
  2. 咱們方法的參數將被拷貝。這就是咱們要仔細去研究的東西。
  3. 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".

  代碼片斷: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;
}

  1. 開始執行Go方法,變量x進入棧中。
  2. 執行DoSomething方法,參數pValue進入棧中。
  3. 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

  接下來,讓咱們看看發生了什麼,以下圖:

  1. 開始執行Go方法,x指針在棧上被初始化。
  2. Animal類型在堆上被建立。
  3. 開始執行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關鍵字傳遞的是引用類型的指針,而非引用類型地址。

相關文章
相關標籤/搜索