C# 未为一次性类型对象调用终结器

C# 未为一次性类型对象调用终结器,c#,.net,memory-leaks,garbage-collection,C#,.net,Memory Leaks,Garbage Collection,复制步骤: 这是我的代码: using (TestClass test = new TestClass()) { } GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); 这就是我定义TestClass的方式: public class TestClass : IDisposable { public void Dispose() { Dispos

复制步骤:

这是我的代码:

    using (TestClass test = new TestClass())
    {

    }

    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();
这就是我定义TestClass的方式:

public class TestClass : IDisposable
{
    public void Dispose()
    {
        Dispose(true);
        //GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            Console.WriteLine("Disposing TestClass.");
        }
    }

    ~TestClass()
    {
        Dispose(false);
        Console.WriteLine("TestClass Finalizer called");
    }
}
预期行为: 我确实看到“Disposing TestClass”按预期在using语句之后打印,但我也希望“TestClass Finalizer called”在我运行的GC命令之后打印。我确保跳过调用GC.SuppressFinalize(这个);在Dispose方法中。看起来处置的变量即使在超出范围后也不会最终确定。它们似乎在程序退出之前就完成了

实际行为: 我只看到在using语句之后按预期打印“Disposing TestClass”,而在GC命令之后没有看到“TestClass Finalizer called”。我只在程序退出前看到它

这不被认为是内存泄漏吗

如果我将其转换为一个非一次性类并像下面那样更新代码,我确实会看到在GC命令之后调用终结器

TestClass test = new TestClass();
test = null;

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();


public class TestClass
{
    ~TestClass()
    {   
        Console.WriteLine("TestClass Finalizer called");
    }
}

根据微软对

终结器执行的确切时间尚未定义。要确保类实例的资源得到确定性释放,请实现Close方法或提供IDisposable.Dispose实现

将终结器视为第二道防线。如果程序未能调用
Close
Dispose
,则终结器将获得更改以更正遗漏

这仍然可以确保在程序退出时关闭文件,例如,除非另一个终结器没有退出,因此阻止了其他终结器,或者如果灾难性异常正在残酷地终止程序


这不是内存泄漏。如果内存不足,垃圾收集器(GC)可以在程序退出之前很久决定释放资源并调用终结器。

调用
Dispose
方法不会影响对象的运行时间。GC仅收集不再引用的对象(例外情况适用)。在第一个示例中,
test
变量从不被垃圾收集(通过
GC.Collect
语句),因为它是在包含方法中声明的,而不在using块的范围内

给定以下C#输入(
A
实现
IDisposable
):

发出以下IL代码:

IL_0001: newobj       instance void Testit.A::.ctor()
IL_0006: stloc.0      // a
.try
{

  // [10 13 - 10 14]
  IL_0007: nop          

  // [12 13 - 12 14]
  IL_0008: nop          
  IL_0009: leave.s      IL_0016
} // end of .try
finally
{

  IL_000b: ldloc.0      // a
  IL_000c: brfalse.s    IL_0015
  IL_000e: ldloc.0      // a
  IL_000f: callvirt     instance void [mscorlib]System.IDisposable::Dispose()
  IL_0014: nop          
  IL_0015: endfinally   
} // end of finally
如果我们把它转换成C#,结果是:

A a;
try
{
}
catch
{
    if (a != null)
    {
        a.Dispose();
    }
}
变量
a
有一个方法作用域,因此只有在保留该方法的情况下才会被收集。这就是为什么没有调用终结器


在第二个示例中,您故意将实例设置为
null
从作用域中删除实例,并且由于没有活动指针,因此会收集实例

我猜,尽管您将测试变量设置为null,但仍然有对它的引用,它发生在调试模式下的某些.NET版本上。 试试这个代码

private void CreateAndRelease()
{
  new TestClass();
}

public void Main()
{
  CreateAndRelease();
  GC.Collect();
  GC.WaitForPendingFinalizers();
  GC.Collect();
  GC.WaitForPendingFinalizers();

  Console.ReadLine();
}
请注意,GC.WaitForPendingFinalizers被调用两次-first GC.Collect不会完成对象,而是将其放入完成队列,只有在下一次垃圾收集时才会完成

编辑: 一些害羞的人否决了我的答案,但我在发布之前检查了一下。 .NET 4.5.1调试模式

编码和输出1

Output:
Disposing TestClass.
Finished

public static void Main()
{
  using (TestClass test = new TestClass())
  {}

  GC.Collect();
  GC.WaitForPendingFinalizers();
  GC.Collect();
  GC.WaitForPendingFinalizers();

  Console.WriteLine("Finished");
  Console.ReadLine();
}
Disposing TestClass.
TestClass Finalizer called
Finished

public static void Main()
{
  CreateAndRelease();

  GC.Collect();
  GC.WaitForPendingFinalizers();
  GC.Collect();
  GC.WaitForPendingFinalizers();

  Console.WriteLine("Finished");
  Console.ReadLine();
}

private static void CreateAndRelease()
{
  using (TestClass test = new TestClass())
  {}
}
编码和输出2

Output:
Disposing TestClass.
Finished

public static void Main()
{
  using (TestClass test = new TestClass())
  {}

  GC.Collect();
  GC.WaitForPendingFinalizers();
  GC.Collect();
  GC.WaitForPendingFinalizers();

  Console.WriteLine("Finished");
  Console.ReadLine();
}
Disposing TestClass.
TestClass Finalizer called
Finished

public static void Main()
{
  CreateAndRelease();

  GC.Collect();
  GC.WaitForPendingFinalizers();
  GC.Collect();
  GC.WaitForPendingFinalizers();

  Console.WriteLine("Finished");
  Console.ReadLine();
}

private static void CreateAndRelease()
{
  using (TestClass test = new TestClass())
  {}
}

因此,我的回答绝对正确,将有助于话题的开始。

以下是这种行为的原因:

以下代码:

using (TestClass test = new TestClass())
{

}
编译为以下内容:

TestClass test;
try
{
test = new TestClass();
}
finally
{
if(test != null)
      test.Dispose();
}
出于与中所述相同的原因,当您在调试模式下运行此函数时,“test”变量一直处于活动状态,直到方法结束,因此它不会被GC调用,也不会调用终结器。如果在发布模式下运行相同的东西,它将按预期工作


对于非一次性类型(在我的第二部分中),因为有一种方法可以将此“test”变量赋值为null,因此即使在调试模式下使用,GC也可以收集它,因此它可以按预期工作。

黄金法则:IDisposable不会影响垃圾收集器的行为方式。您的实现没有做任何有用的事情也不会影响它。选择发布配置并按Ctrl+F5,现在它可以工作了。在收集对象之前,终结器不会运行。没有理由怀疑它会在第一个例子中被收集(它可以,反过来也不能保证它会在第二个例子中被收集,但结果肯定是最有可能的行为)。谢谢Hans的参考!这不是一个范围问题。这是一个可达性问题。例如,如果你有一个链表,那么你的作用域中可能有一个变量指向列表的头部;但是,列表中间的对象将不在任何运行代码的范围内,但仍然是可到达的。@ OlivierJacot Descombes我指的是他的例子,其中<代码>测试< /COD>变量在<<代码>使用指令结束时不会超出范围。我猜我的措辞很糟糕。作用域是指你可以在程序中使用变量的地方。可达性取决于您在何处使用它,并且在应用了优化之后,因此在范围内时可以收集某些内容,而在范围外时则无法收集。您正确地理解了using语句如何被编译器转换,以及为什么它没有被GC’ed。正如我在回答中提到的,如果您在发布模式下运行相同的内容,它会被GC'ed和finalizer调用。我相信您不需要第二个GC.WaitForPendingFinalizers();第二个GC.Collect()将在GC.WaitForPendingFinalizers()完成引用后收集引用;第一次打电话。我一周前说的,你不相信我吗?:)