前言
在本系列的第一篇文章《C#堆栈对比(Part One)》中,介绍了堆栈的基本功能和值类型以及引用类型在程序运行时的表现,同时也包含了指针作用的讲解。
本文为文章的第二部分,主要讲解参数在堆栈的作用。
注:限于本人英文理解能力,以及技术经验,文中如有错误之处,还请各位不吝指出。
目录
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关键字传递的是引用类型的指针,而非引用类型地址。