引用 https://www.c-sharpcorner.com/article/C-Sharp-heaping-vs-stacking-in-net-part-ii/
前言
使用.NET框架,我们不必主动考虑内存管理和垃圾回收(GC),但仍必须牢记内存管理和垃圾回收,以优化应用程序的性能。另外,对内存管理的工作原理有一个基本的了解将有助于解释我们在编写的每个程序中使用的变量的行为。在本文中,我将介绍将参数传递给方法时需要注意的一些行为。
参数,重点
这是代码执行过程中发生的情况的详细视图。在第一部分中,我们介绍了进行方法调用时发生的情况。现在让我们更详细的介绍一下。。。
当我们进行方法调用时,会发生以下的情况:
- 给堆栈上我们所执行的方法所需要的信息分配空间(称为堆栈框架)。这包括调用地址(指针),该地址基本上是GOTO指令,因此当线程完成运行我们的方法时,它知道要返回到哪里才能继续执行。
- 我们的方法参数被复制。这是我们需要更仔细研究的地方。
- 控制权传递给JIT'ted方法,线程开始执行代码,因此,我们有另外一种犯法,由“调用堆栈”上的堆栈帧表示。
public int AddFive(int pValue){
int result;
result = pValue + 5;
return result;
}
使用的堆栈如下图所示
注意:这个方法不存在与堆栈中,在这里只作为参考来说明堆栈框架的开始。
和第一部分讨论的那样,根据参数的类型是引用类型还是值类型,对堆栈上的参数的处理方式将有所不同。值类型将被整体复制,引用类型将被复制引用。
传递值类型
这是传递值类型
首先,当我们传递值类型时,将分配空间,并将类型中的值复制到堆栈上的新空间。
示例代码:
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是一个可以变得非常大的值类型,我们必须了解我们如何处理它。
这是一个很大的结构:
public struct MyStruct{
long a,b,c,d,e,f,g,h,i,j,k,l,m;
}
看一下我们执行Go()并转到下面的DoSomething()方法时会发生什么:
public void Go(){
MyStruct x = new MyStruct();
DoSomething(x);
}
public void DoSomeThing(MyStruct pValue){
//
}
这就能看到低效的地方了。想想一下,如果我们通过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){
//
}
这样,我们最终可以在内存中更加有效的分配对象。
通过引用传递值类型时,我们唯一需要注意的是,我们对值类型的值的访问。
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”,因为pValue.a实际上使用的是原始数据x变量里的内存空间。
传递引用类型
作为引用类型的传递参数类似于在上一个示例中通过引用传递值类型。
如果我们需要使用值类型
public class MyInt{
public int MyValue;
}
并且调用Go()方法,因为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
因此,当我们使用pValue更改堆中MyInt对象的MyValue属性并稍后使用x引用堆上对象时,我们得到的值是“12345”。
所以有一个有趣的地方,当我们通过引用传递引用类型的时候会发生什么呢?
看看这个。如果我们有Thing类,Animal和Vegetable继承自它
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。
如果不加ref那x还是Animal
让我们看看发生了什么;
- 从Go()方法调用开始,x指针进入堆栈
- Animal对象在堆上
- 对Switcharoo方法调用开始,pValue进入堆栈并指向x
- Vegetable在堆上
- x的值通过pValue更改为Vegetable的地址。
如果不通过ref传递Thing,则保留对Animal引用的并从代码获得相反的结果。