[译文]C# Heap(ing) Vs Stack(ing) in .NET: Part II
在PartI中,我们讨论了堆和栈的基本功能以及程序执行时,值类型和引用类型是如何被分配内存的。我们还讨论了什么是指针。
Parameters, the Big Picture
下面是当我们代码执行时,发生哪些事情的详细视图。PartI我们讨论了基本的内容,这部分我们将更加深入……
当我们调用一个方法时,下面是所发生的事情:
1. 为执行方法所需要的信息分配内存空间(称作栈帧(Stack Frame)),这其中包括调用地址(一个指针),该地址是一个GOTO指令,用于告诉程序,当这个方法调用完毕后,程序该从哪里继续执行。
2. 方法的参数将被拷贝。这是我们将进一步讨论的。
3. 控制将传递给JIT编译后的方法,然后线程开始执行代码。因此,我们有另外一个位于“调用栈”(call stack)上的代表栈帧(stack frame)的方法(原文: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; }
其对应的栈图是这样的:
正如PartI所讨论的,位于栈上的参数根据值类型和引用类型的不同将有不同的处理方式。
传递值类型
当我们传递值类型的时候,将分配新的内存空间,同时,我们的值将被拷贝到栈上的新内存空间。让我们看下下面的方法:
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就被完全的移除掉了:
因此,上述代码的返回值为5,对吧?这个问题的关键点是,我们传递给方法的值类型参数都只是一个副本,原始的变量得以保留。
我们需要记住的一点是,如果我们将一个很大的值类型(如,一个大的struct)传递到栈上,那么这将花费巨大的空间和处理周期。栈上的空间并不是无限的,正如我们从水龙头给杯子加水,它是会溢出来的。一个结构体(struct)可能是非常大的,我们需要注意如何处理它。
下面是一个大的结构体:
public struct MyStruct { long a, b, c, d, e, f, g, h, i, j, k, l, m; }
如果我们这样来处理该结构体:
public void Go() { MyStruct x = new MyStruct(); DoSomething(x); } public void DoSomething(MyStruct pValue) { // DO SOMETHING HERE.... }
这将是非常低效的。试想一下,如果我们将MyStruct传递上千次,那么我们的程序将被搞得无法动弹。
那么,我们该如何来解决这个问题呢?
我们可以通过传递原来值类型的一个引用,如下:
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.... }
这样,我们就可以更加有效的分配内存了。
当然,将值类型作为引用来传递参数,我们有一点需要注意的就是,我们可以访问该值类型了(译注:而不仅仅是它的副本)。我们队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; }
我们的输出结果就是12345,而不是5了。因为pValue和x事实上共享同一块内存空间,我们对pValue.a的改变,同样会改变x.a的值。
传递应用类型
传递引用类型和通过引用传递值类型是类似的.
如果我们使用引用类型:
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
因此,显然的,当我们通过pValue改变位于堆上的MyInt的属性MyValue后,再通过x去访问堆上的对象时,我们得到的值就是”12345”.
有趣的事情是:当我们通过引用传递引用类型的时候,会发生怎样的事情呢?
我们来检测一下。假设我们有一个Thing类,Animal和Vegetable都继承自Thing:
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将变成Vegetable类型,即输出结果为;
x is Animal : False
x is Vegetable : True
通过图来看看发生了什么情况:
1. 开始调用Go()方法,x位于栈上;
2. Animal位于堆上;
3.开始调用Switcharoo()方法,pValue位于栈上,并且指向x
4. Vegetable位于堆上;
5. x的值通过pValue变成指向Vegetable的地址
如果我们不是通过引用传递Thing,我们将得到相反的结果。
总结
我们已经讨论了参数传递在内存中是如何处理的,现在也知道该留心哪些东西了。
在下一节了,我们将探讨位于栈上的引用型变量(reference variables),以及如何攻克对象拷贝时的一些问题。
待续……