可变寿命

时间:2015-06-30 11:38:28

标签: c# lifetime object-lifetime lifetime-scoping

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

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会发生什么?

我猜两种情况下都会分配变量,但是当它被解除分配时?

5 个答案:

答案 0 :(得分:4)

作为变量,它是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
}

由于未使用该值,编译器将其删除为无用的cruft,并且只编译一个立即返回的方法。

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

.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还没有运行。

答案 1 :(得分:1)

在99%的案例中,答案是“无所谓”。唯一重要的是它不再适合你。

你不应该经常关心剩余的1%。要简化这一点并不容易在SO上得到合理的答案。我唯一可以说的就是:

  • 一旦变量将来不再使用,编译器或运行时完全可以做任何他们喜欢的事情:)

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

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

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

另外,如果你四处闲逛,请注意默认情况下,连接一个调试器会让你有点麻烦。例如,本地人将保持活着,直到他们的范围结束 - 这当然是完全合法的,并且在调试时会有所帮助。

答案 2 :(得分:1)

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

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

答案 3 :(得分:1)

假设你在Debug下运行,没有优化:

  

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

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

  

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

假设Customerclass而不是struct,并且没有实现终结器(这改变了事情的过程),它将不再有任何对象在行后引用它9.一旦GC开始(在任意的,非确定的时间),它将看到它有资格收集并在标记阶段将其标记为如此。一旦扫描阶段开始,它将释放占用的内存。

答案 4 :(得分:1)

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

如果您不想等待GC自动运行它的方法,您可以通过调用GC.Collect()方法自行执行此操作

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

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

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

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