C# 超出范围的对象没有被收集

Jos*_*ams 1 c# garbage-collection unit-testing destructor mstest

我正在尝试构建我的第一个单元测试,并且有一个类可以分别在其构造函数和析构函数中递增和递减实例计数器。我有一个测试来确保它有效但它失败了,似乎我的其他测试中的其他类实例在超出范围时没有调用它们的析构函数。

public class Customer
{
    public Customer()
    {
        ++InstanceCount;
    }

    public Customer(int customerId)
    {
        CustomerId = customerId;
        ++InstanceCount;
    }

    ~Customer()
    {
        --InstanceCount;
    }

    public static int InstanceCount { get; private set; }
}
Run Code Online (Sandbox Code Playgroud)
[TestClass]
public class CustomerTest
    {
    [TestMethod]
    public void FullNameLastNameEmpty()
    {
        //--Arrange
        Customer customer = new Customer {FirstName = "John"};
        const string expected = "John";

        //--Act
        string actual = customer.FullName;

        //--Assert
        Assert.AreEqual(expected, actual);
    }

    [TestMethod]
    public void StaticTest()
    {
        //--Arrange
        Customer[] customers =
        {
            new Customer {FirstName = "John"},
            new Customer {FirstName = "Jane"},
            new Customer {FirstName = "Jeff"} 
        };

        //--Act

        //--Assert
        Assert.AreEqual(3, Customer.InstanceCount);
    }
}
Run Code Online (Sandbox Code Playgroud)

我没有在任何地方传递引用,所以客户应该被释放,但事实并非如此,我什至尝试在 StaticTest() 开始时调用 GC.Collect() 来强制调用析构函数,但仍然没有运气。

有人可以解释为什么会发生这种情况以及如何解决它。

V0l*_*dek 7

C# 不是 C++。析构函数,或者更确切地说是终结器,是一种用于非常特定目的的特性,即处理对象持有的非托管资源的处理。将程序逻辑填充到终结器中是您在 C# 中可以拥有的少数非常糟糕的想法之一。为什么?这是一个快速概述:

  • 无法保证对象超出范围时何时会被收集。
  • 哎呀,如果没有内存压力,就不能保证收集会发生。
  • 无法保证对象终结器的运行顺序。
  • 无法保证对象终结器何时运行 - 可能是在它超出范围之后,也可能是一分钟后。
  • 哎呀,有一个终结不能保证永远运行。
  • 不能保证,如果它运行,它会在您的 ctor 完成后运行。因此,在您的情况下,您最终可能会减少实例计数而不增加它。诚然,这不太可能真正发生,但可能会发生。
  • 哦,它也会对整个应用程序的性能产生负面影响,但与上面的邪恶相比,这是微不足道的。

基本上,在使用终结器时,您所知道的一切都是错误的。终结者是如此邪恶,以至于有第二部分

使用终结器的准则是 - 不要。即使您是一位经验丰富的 C# 开发人员,您实际需要终结器的可能性也非常小,而您在编写它时出错的可能性却很大。如果您被迫使用非托管资源,请遵循指南并使用SafeHandle.

如果您的要求是计算实例数,最好的办法是使用IDisposable模式。这是一种合作方式,这意味着调用代码必须实际Dispose()使用您的对象才能减少计数。但它不可能是任何其他方式。最重要的是,在 C# 中,没有可靠的方法可以在对对象的最后一次引用超出范围时执行某些操作

更新:

我希望我已经劝阻你永远不要编写终结器。说真的,除非您是专业人士并且确定您绝对需要终结器并且可以用它们描述在 CLR 级别上发生的所有事情,否则不要这样做。然而。您可以使用一个丑陋的黑客来获得您想要的东西。这两个咒语:

GC.Collect();
GC.WaitForPendingFinalizers();
Run Code Online (Sandbox Code Playgroud)

应该强制收集发生,这会将您的对象放在终结器队列中,并等待所有对象完成。如果你在每个测试用例之后运行它,它应该给你你期望的行为。但是,为了我们所有人,请不要在严肃的代码中使用它。我刚刚向你展示了一把非常锋利的双刃刀片。没有任何把手。那火了 第二种方法甚至不能保证终止。