可变寿命

Variable lifetime

当执行行超出代码块时,变量会发生什么变化? 例如:

1  public void myMethod()
2  {
3     int number;
4     number = 5;
5  }

所以,我们声明并设置变量。当它超出代码块(第 5 行)时,变量号会发生什么变化?

这是创建 class 实例的另一个示例:

7   public void myMethod()
8   {
9      Customer myClient;
10     myClient = new Customer();
11  }

当它超出代码块(第 11 行)时,对象引用 myClient 会发生什么?

我想在这两种情况下变量都被分配了,但是什么时候被释放了?

在 99% 的情况下,答案是 "it doesn't matter"。唯一重要的是您无法再访问它。

你不应该太在意剩下的1%。将其简化到足以对 SO 做出合理的回答并不容易。我唯一能简单地说的是:

  • 一旦将来不再使用某个变量,编译器或运行时就可以为所欲为是完全合法的:)

请注意,这并没有提及任何关于作用域的内容——C# 实际上并不那么关心作用域。作用域是用来帮助你编码的,而不是帮助编译器(尽管方法和更高的作用域肯定有助于编译时间)。

同样,在大多数情况下,您不关心接下来会发生什么。主要例外情况是:

  • 使用非托管资源。确定性地处理非托管资源通常是个好主意。所有封装非托管资源的 类 都有一个 Dispose 方法来处理这个问题。您可以使用 using 语句来帮助解决这个问题。
  • 性能瓶颈 - 如果分析表明您在不切实际的重新分配上输了 memory/CPU,您可能需要提供一些帮助。
  • 保持对范围外部对象的引用。很容易不小心阻止收集不再使用但仍有参考的东西。这是托管应用程序内存泄漏的主要原因。

此外,如果您有空尝试一下,请注意,默认情况下,附加调试器会给您带来一些麻烦。例如,局部变量将一直保持活动状态,直到它们的作用域结束——这当然是完全合法的,并且在调试时有帮助。

在第一种情况下,number是一个值类型,将存储在堆栈中。一旦你离开这个方法,它就不再存在。并且没有重新分配,堆栈 space 将简单地用于其他事情。

在第二种情况下,由于 Customer(我假设)是引用类型,myClient 将在堆栈上存储对实例的引用。一旦您离开该方法,该引用将不再存在。这意味着该实例最终将被垃圾回收。

假设您 运行 处于调试状态,没有优化:

When it goes outside of code block (line 5) what happends to variable number?

一旦方法退出,该值就会从堆栈中弹出。值类型存在于堆栈中这一事实是 实现细节 ,您不应该依赖它。如果这是一个值类型,它是 class 中的一个字段,它就不会存在于堆栈中,而是存在于堆中。

When it goes outside of code block (line 5) what happends to variable number?

假设 Customerclass 而不是 struct,并且没有实现终结器(这改变了事情的进程),它将不再有任何对象引用它在第 9 行之后。一旦 GC 启动(在任意的、不确定的时间),它将认为它符合收集条件,并在标记阶段将其标记为符合条件。一旦扫描阶段开始,它将释放占用的内存。

当对结构类型字段的引用丢失时 - 内存被释放(在堆栈中)。对于引用类型,它更复杂。如果对象 (class) 不再使用并且对它的引用丢失,那么它会被垃圾收集器标记为删除。如果没有任何变化,将在下次垃圾回收时删除此对象。

如果您不想等待 GC 运行 它是自动方法,您可以通过调用 GC.Collect() 方法自行完成

P.S。 在您的 class 对象被破坏并释放内存之前(如果它实现了 IDisposable 接口),它会依次调用三个方法:

 1. Dispose() 2. Finalize() 3. ~ctor()

在 C# 中,您可以使用其中的两个:dispose() 和 finalize()。 Dispose 通常用于释放托管资源(例如 FileStreamThreads),而 Finalize 更适合编写非托管资源释放逻辑。

要更改 object.Finalize() 方法的逻辑 - 将您的逻辑放入 ~ctor() 但要小心,因为它可能会导致一些严重的故障。

作为一个变量它是C#语言中的一个概念。在代码块之外没有任何东西 "happens" ,因为它在代码块内部。此句外的 word 字词没有任何变化。

当然,你的意思是当代码为 运行 时变量会变成什么因为它们在 C# 中。

两种情况下代码都转成CIL,然后在运行时转成机器码。

CIL 可能会有很大差异。例如,这是在调试模式下编译时的第一个外观:

.method public hidebysig instance void myMethod () cil managed 
{
  .locals init ([0] int32) // Set-up space for a 32-bit value to be stored
  nop                      // Do nothing
  ldc.i4.5                 // Push the number 5 onto the stack
  stloc.0                  // Store the number 5 in the first slot of locals
  ret                      // Return
}

下面是编译发布时的样子:

.method public hidebysig instance void myMethod () cil managed 
{
  ret                      // Return
}

由于未使用该值,编译器将其作为无用的垃圾删除,并仅编译一个立即 returns.

的方法

如果编译器没有删除这样的代码,我们可能会期待这样的东西:

.method public hidebysig instance void myMethod () cil managed 
{
  ldc.i4.5                 // Push the number 5 onto the stack
  pop                      // Remove value from stack
  ret                      // Return
}

调试版本存储东西的时间更长,因为检查它们对调试很有用。

当发布版本确实将内容存储在本地数组中时,它们也更有可能在方法中重用槽。

然后将其转换为机器代码。它的工作方式类似,它要么产生数字 5,将其存储在本地(在堆栈或寄存器中),然后再次删除它,要么什么都不做,因为未使用的变量已被删除. (也许甚至不执行该方法;可以内联该方法,然后由于它不执行任何操作而被有效地完全删除)。

对于带有构造函数的类型,还有更多的事情要做:

.method public hidebysig instance void myMethod () cil managed 
{
  .locals init ([0] class Temp.Program/Customer)       // Set-up space for a reference to a Customer

  nop                                                  // Do nothing.
  newobj instance void SomeNamespace/Customer::.ctor() // Call Customer constructor (results in Customer on the stack)
  stloc.0                                              // Store the customer in the frist slot in locals
  ret                                                  // Return
}

.method public hidebysig instance void myMethod () cil managed 
{
  newobj instance void SomeNamespace/Customer::.ctor() // Call Customer constructor (results in Customer on the stack)
  pop                                                  // Remove value from stack
  ret                                                  // Return
}

这里都调用构造函数,甚至发布版本也会调用构造函数,因为它必须确保任何副作用仍然发生。

如果 Customer 是引用类型,还会发生更多情况。如果它是一个值类型,那么它的所有内容都保存在堆栈中(尽管它可能具有依次为引用类型的字段)。如果它是引用类型,那么堆栈中保存的是对堆中对象的引用。当堆栈上不再有任何此类引用时,垃圾收集器将不会在其扫描中找到它以查找它无法收集的对象,并且可以收集它。

在发布版本中,一旦构造函数returns,可能永远不会有存储该引用的内存位置或寄存器。事实上,即使构造函数是 运行ning 也可能没有(如果没有字段访问或其他隐式或显式使用 this 发生)或者它可能已被擦除部分-通过它(一旦此类访问完成),因此垃圾收集可能在构造函数完成之前发生。

更有可能在方法返回后它会在堆内存中停留一段时间,因为 GC 还没有 运行。