4

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

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

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

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

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

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

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

4

5 回答 5

6

作为变量,它是 C# 语言中的一个概念。在代码块之外没有任何“发生”,因为它在代码块内。这个句子之外的单词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
}

由于未使用该值,因此编译器将其作为无用的垃圾删除,并仅编译一个立即返回的方法。

如果编译器没有删除此类代码,我们可能会期望类似:

.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
}

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

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

然后将其转换为机器代码。它的工作方式类似于它会产生数字 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是引用类型,还会发生更多情况。如果它是一个值类型,那么所有它都保存在堆栈中(尽管它可能具有依次是引用类型的字段)。如果它是引用类型,那么堆栈中保存的是对堆中对象的引用。当堆栈上不再有任何此类引用时,垃圾收集器将不会在其扫描中找到它以查找它无法收集的对象,并且可以将其收集。

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

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

于 2015-06-30T12:13:03.783 回答
1

在 99% 的情况下,答案是“没关系”。唯一重要的是您不再可以访问它。

你不应该太在意剩下的 1%。要对 SO 做出合理的回答,要充分简化这一点并不容易。我唯一可以简单地说的是:

  • 只要将来不再使用变量,编译器或运行时就可以随心所欲地做任何事情是完全合法的:)

请注意,这并没有提到任何关于范围的内容——C# 实际上并不关心范围。范围可以帮助您编写代码,而不是帮助编译器(尽管方法和更高的范围肯定有助于编译时间)。

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

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

此外,如果您有时间玩这个,请注意,默认情况下,附加调试器会有点麻烦。例如,本地人将一直保持活动状态,直到其作用域结束——这当然是完全合法的,并且在调试时会有所帮助。

于 2015-06-30T11:41:02.057 回答
1

在第一种情况下,number是一个值类型,将存储在堆栈中。一旦您离开该方法,它将不再存在。并且没有释放,堆栈空间将仅用于其他事情。

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

于 2015-06-30T11:41:14.547 回答
1

假设您在 Debug 下运行,并且没有优化:

当它超出代码块(第 5 行)时,变量号会发生什么?

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

当它超出代码块(第 5 行)时,变量号会发生什么?

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

于 2015-06-30T11:41:21.410 回答
1

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

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

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

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

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

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

于 2015-06-30T11:45:37.133 回答