即使使用.NET框架,我们也不必主动担心内存管理和垃圾回收(GC),但仍必须牢记内存管理和垃圾回收,以优化应用程序的性能。另外,对内存管理的工作原理有基本的了解将有助于解释我们在编写的每个程序中使用的变量的行为。在本文中,我将介绍将参数传递给方法时需要注意的一些行为。
在第一部分中,我们介绍了堆和堆栈功能的基础知识,以及在程序执行时分配变量类型和引用类型的位置。我们还介绍了指针是什么的基本概念。
参数,大图
这是我们的代码执行时的详细视图。我们在第一部分中介绍了进行方法调用时的基本情况。下面我们来了解更多的细节...
当我们进行方法调用时,会发生以下情况:
- 为在堆栈(stack)上执行我们的方法所需的信息分配了空间(称为堆栈框架)。这包括调用地址(指针),该地址基本上是GOTO指令,因此当线程运行完成我们的方法时,它知道要返回到哪里才能继续执行。
- 我们的方法参数被复制。这是我们想要更仔细地研究的。
- 控制权传递给JIT'ted方法,线程开始执行代码。因此,我们有另一种方法,由“调用堆栈”上的堆栈帧表示。
相关代码:
<code>public int AddFive(int pValue)
{
int result;
result = pValue + 5;
return result;
}
/<code>
堆栈如下所示:
注意:该方法不存在于堆栈(stack)中,在这里仅作为参考来说明作为堆栈框架的开始。
如第一部分所述,堆栈中参数的放置方式将根据其是值类型还是引用类型而有所不同。值类型被复制,引用类型的引用被复制。
传递值类型
这是值类型的陷阱...
首先,当我们传递值类型时,将分配空间,并将类型中的值复制到堆栈上的新空间。查看以下方法:
<code>class Class1
{
public void Go()
{
int x = 5;
AddFive(x);
Console.WriteLine(x.ToString());
}
public int AddFive(int pValue)
{
pValue += 5;
return pValue;
}
}
/<code>
执行该方法时,将“x”的空间放置在堆栈(stack)上,其值为5。
接下来,将AddFive()放置在堆栈上,并为其参数留出空间,并从x逐位复制值。
当AddFive()执行完成后,线程将被传递回Go(),并且由于AddFive()已经完成,因此pValue本质上是“已删除”:
因此,我们的代码的输出为“5”有意义,对吗?关键是,传递给方法的任何值类型参数都是复本,我们依靠原始变量的值来保存。
要记住的一件事是,如果我们有一个非常大的值类型(例如一个大的结构)并将其传递给堆栈(stack),则每次复制它的空间和处理器周期都会非常昂贵。堆栈(stack)没有无限的空间,就像从水龙头里倒满一杯水一样,它可能会溢出。struct是一个可以变得非常大的值类型,我们必须了解我们如何处理它。
这是一个很大的结构:
<code>public struct MyStruct
{
long a, b, c, d, e, f, g, h, i, j, k, l, m;
}
/<code>
看一下我们执行Go()并转到下面的DoSomething()方法时会发生什么:
<code>public void Go()
{
MyStruct x = new MyStruct();
DoSomething(x);
}
public void DoSomething(MyStruct pValue)
{
// DO SOMETHING HERE....
}
/<code>
这可能确实是低效的。想象一下,如果我们通过MyStruct数千次,您就可以了解它如何真正使事情陷入困境。
那么我们如何解决这个问题呢?通过传递对原始值类型的引用,如下所示:
<code>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....
}
/<code>
这样,我们最终可以在内存中更有效地分配对象。
通过引用传递值类型时,我们唯一需要注意的是,我们可以访问值类型的值。
pValue中的任何更改都将更改x。使用下面的代码,我们的结果将为“12345”,因为pValue.a实际上正在查看声明了原始x变量的内存空间。
<code>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;
}
/<code>
传递引用类型
传递引用类型的参数类似于上一个示例中的传递引用值类型。
如果我们使用值类型
<code>public class MyInt
{
public int MyValue;
}
/<code>
并调用Go()方法,由于它是引用类型,因此MyInt最终出现在堆上:
<code>public void Go()
{
MyInt x = new MyInt();
}
/<code>
如果我们按照以下代码执行Go()...
<code>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;
}
/<code>
这里发生了什么...
- 从对Go()的调用开始,变量x进入堆栈。
- 从对DoSomething()的调用开始,参数pValue进入堆栈。
- x的值(堆栈上MyInt的地址)被复制到pValue
因此,当我们使用pValue更改堆中MyInt对象的MyValue属性并稍后使用x引用堆中的对象时,我们得到的值为“12345”。
所以这就是有趣的地方。当我们通过引用传递引用类型时会发生什么?
一探究竟。如果我们有Thing类,而Animal和Vegetable都继承Thing:
<code>public class Thing
{
}
public class Animal:Thing
{
public int Weight;
}
public class Vegetable:Thing
{
public int Length;
}
/<code>
然后我们执行下面的Go()方法:
<code>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();
}
/<code>
我们的变量x变成了Vegetable。
<code>x is Animal : False
x is Vegetable : True
/<code>
让我们看看发生了什么:
- 从Go()方法调用开始,x指针进入堆栈
- Animal在堆上
- 从调用Switcharoo()方法开始,pValue进入堆栈并指向x
- Vegetable在堆上
- x的值通过pValue更改为Vegetable的地址
如果不通过ref传递Thing,则将保留Animal并从代码中获得相反的结果。
如果上面的代码没有意义,请查看我有关引用变量类型的文章,以更好地了解变量如何与引用类型一起使用。
结论
我们已经研究了如何在内存中处理参数传递,现在知道要注意什么。在本系列的下一部分中,我们将看看引用存在于堆栈中的变量会发生什么,以及如何克服复制对象时遇到的一些问题。
閱讀更多 非科班碼農 的文章