译文---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关键字传递的是引用类型的指针,而非引用类型地址。

相关文章
相关标签/搜索