控制台应用程序和单元测试方法之间的不同垃圾收集行为

vos*_*d01 7 .net c# garbage-collection

我偶然发现垃圾收集似乎在运行与单元测试相同的代码与在Main控制台应用程序的方法中编写的相同代码之间表现不同.我想知道这种差异背后的原因.

在这种情况下,我和同事对于在垃圾收集上注册事件处理程序的效果存在分歧.我认为演示比仅仅向他发送一个高评级SO答案的链接更好.因此我写了一个简单的演示作为单元测试.

我的单元测试显示事情正常,因为我说他们应该.但是,我的同事编写了一个控制台应用程序,显示了他的工作方式,这意味着GC没有像我期望的那样在方法中的本地对象上发生Main.通过将我的测试中的代码移动到MainConsole Application项目的方法中,我能够重现他所看到的行为.

我想知道的是,为什么GC在Main控制台应用程序的方法中运行时似乎没有按预期收集对象.通过提取方法以便GC.Collect在不同方法中发生对范围的调用和对象超出范围,恢复了预期的行为.

这些是我用来定义测试的对象.只有一个带有事件的对象和一个为事件处理程序提供合适方法的对象.两者都有终结器设置一个全局变量,以便您可以知道它们何时被收集.

private static string Log;
public const string EventedObjectDisposed = "EventedObject disposed";
public const string HandlingObjectDisposed = "HandlingObject disposed";

private class EventedObject
{
    public event Action DoIt;

    ~EventedObject()
    {
        Log = EventedObjectDisposed;
    }

    protected virtual void OnDoIt()
    {
        Action handler = DoIt;
        if (handler != null) handler();
    }
}

private class HandlingObject
{

    ~HandlingObject()
    {
        Log = HandlingObjectDisposed;
    }

    public void Yeah()
    {
    }
}
Run Code Online (Sandbox Code Playgroud)

这是我的测试(NUnit),它通过:

[Test]
public void TestReference()
{
    {
        HandlingObject subscriber = new HandlingObject();

        {
            {
                EventedObject publisher = new EventedObject();
                publisher.DoIt += subscriber.Yeah;
            }

            GC.Collect(GC.MaxGeneration);
            GC.WaitForPendingFinalizers();
            Thread.MemoryBarrier();

            Assert.That(Log, Is.EqualTo(EventedObjectDisposed));
        }

        //Assertion needed for foo reference, else optimization causes it to already be collected.
        Assert.IsNotNull(subscriber);
    }

    GC.Collect(GC.MaxGeneration);
    GC.WaitForPendingFinalizers();
    Thread.MemoryBarrier();

    Assert.That(Log, Is.EqualTo(HandlingObjectDisposed));
}
Run Code Online (Sandbox Code Playgroud)

我将上面的主体粘贴到Main新控制台应用程序的方法中,并将调用转换AssertTrace.Assert调用.两个相等的断言失败然后失败.如果需要,可以在此处获得生成的Main方法代码.

我确实认识到,当GC发生时应该被视为非确定性的,并且通常应用程序不应该关注它何时发生.在所有情况下,代码都是在发布模式下编译的,目标是.NET 4.5.

编辑:我试过的其他事情

  • static自NUnit以来制作测试方法支持; 测试仍然有效.
  • 我还尝试将整个Main方法解压缩到程序中的实例方法并调用它.两个断言仍然失败.
  • 归因Main[STAThread]或者[MTAThread]如果这产生了影响.两个断言仍然失败.
  • 基于@ Moo-Juice的建议:
    • 我将NUnit引用到Console应用程序,以便我可以使用NUnit断言,但它们失败了.
    • 我尝试了对测试,测试类,Main方法和包含Main静态方法的类的可见性的各种更改.没变.
    • 我尝试将Test类设置为static,并将包含Main方法的类设置为static.没变.

Sam*_*ell 6

如果将以下代码提取到单独的方法,则测试将更有可能按预期运行.编辑:请注意,即使您将代码提取到单独的方法,C#语言规范的措辞也不需要通过此测试.

        {
            EventedObject publisher = new EventedObject();
            publisher.DoIt += subscriber.Yeah;
        }
Run Code Online (Sandbox Code Playgroud)

规范允许但不要求publisher在此块结束时立即有资格获得GC,因此您不应该以您假设可以在此处收集的方式编写代码.

编辑:来自ECMA-334(C#语言规范)§10.9自动内存管理(强调我的)

如果任何可能的继续执行都无法访问对象的任何部分,除了运行终结器之外,该对象被认为不再使用,并且它有资格进行最终确定.[注意:实现可能会选择分析代码以确定将来可以使用对对象的哪些引用.例如,如果作用域中的局部变量是对象的唯一现有引用,但该过程中当前执行点的任何可能的继续执行中从不引用该局部变量,则实现可能(但不是要求将对象视为不再使用.结束说明]