让你一次性搞定堆、栈、值类型、引用类型…… (Part 2)
在Part 1但中,我们简单介绍了堆栈的功能以及值类型、引用类型在堆栈中的存储位置的问题,也简单介绍了指针是虾米。让我们沿着革命的步伐继续前进!
Parameters, the Big Picture.
我们的代码执行的时候,底层到底有哪些内幕交易在发生呢?当我们调用一个方法时:
- 栈顶分配控件用来存储执行我们的method所包含的信息,这部分空间叫做栈框(stack frame,详情见地板附录)。这里头有一个指针,指向调用地址。通常这是一个GOTO指令,这样线程执行完毕我们的方法后就知道应该回到哪儿去继续执行下一个栈里头的东东。(其实就是把stack frame删掉)
- 方法的参数被完全复制。这部分我们会做详细解释。
- 控制权被交给JIT编译好的方法指令集,开始真正的执行代码。实际上,在调用栈中还有另外一个方法,存在于栈框里。(见附录)
代码又来了:
public int AddFive(int pValue)
{
int result;
result = pValue + 5;
return result;
}
这时我们的栈看起来这酱紫滴:
就像前面说过的那样,参数如果是值类型,内容会被完整的复制过来。如果是引用类型,被复制的则只是指向堆里头实例的一个指针。
Passing Value Types.
首先,当我们传递一个值类型的时候,栈顶分配相应大小的空间,并且把值完整复制过去。例如:
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经常可能会变得很大,因此要小心使用。多大才算大?这就是个大大大的struct咯(有那么大嘛……*(*&@#¥%):
public struct MyStruct
{
long a, b, c, d, e, f, g, h, i, j, k, l, m;
}
看看使用这个struct时发生了虾米:
public void Go()
{
MyStruct x = new MyStruct();
DoSomething(x);
}
public void DoSomething(MyStruct pValue)
{
// DO SOMETHING HERE....
}
这可真是效率低下啊。想像一下,如果有数千个对这个struct的调用,那将会是多恐怖的事情!
那可咋办呢?我的程序就是要用到几千次的嘛?答案就是传递一个对这个值类型的引用而不是值类型本身:
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也跟着变了。看看以下的代码,我们得到的结果将是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;
}
Passing Reference Types.
传递引用类型跟使用引用传递值类型其实基本上差球不多。
如果我们在其中使用值类型:
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时,结果得到的是改变以后的12345了。
有意思的是,当我们使用引用传递一个引用类型的时候,会发生什么呢?
试试吧。假设有下列引用类型:
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
看看都怎么回事:
- 开始执行Go方法,这时x指针存在于栈中
- Animal存在于堆中
- 执行Switcharoo方法,pValue存在于栈中,同时指向x(引用传递)
- Vegetable在堆中分配(new Vegetable)
- pValue指向x,因此改变pValue的内容实际上是去改变x指向的内容,因此x收pValue的影响指向了Vegetable
如果我们不通过引用传递,那么我们将得到相反的结果,x仍然是Animal。(为什么?可以自个儿去画个图哈)
In Conclusion
接下来的章节里,我们会介绍栈里头的引用变量是怎么回事,以及如何克服复制对象时会遇到的一些麻烦。
Appendix
这里有两个概念,一个是call stack,一个是stack frame,在wikipedia中有详细的解释,日后有空我再简译一下,现在可以去读一下原文:http://en.wikipedia.org/wiki/Call_stack