为什么最后一个对象没有被垃圾收集器销毁?

Why isn't the last object destroyed by the garbage collector?

请耐心等待我在这里 "interesting stuff" 使用枚举器和 LINQ。在检查事情是否得到正确清理时,我注意到我创建的一个简单的控制台应用程序中有一些东西来测试一些东西。但首先,一些代码,从通用的 DoAll() 方法开始:

public static IEnumerable<T> DoAll<T>(this IEnumerable<T> data, Action<T> action)
{
    foreach (var item in data)
    {
        action(item);
        yield return item;
    }
}

这只是在将项目交给管道中的下一个方法之前对项目执行特定操作。接下来,一个仅用作示例的基础 IDisposable 对象:

public class Dummy : IDisposable
{
    private static int _count = 0;
    public int Count = ++_count;
    public Dummy() => Console.WriteLine($"Dummy {Count} created.");
    ~Dummy() => Console.WriteLine($"Dummy {Count} destroyed.");
    public void Dispose() => Console.WriteLine($"Dummy {Count} disposed.");
    private int _value = 0;
    public int Value => ++_value;
}

Dummy 只是一个对象,它计算一个对象的创建频率和一个跟踪它被调用频率的值。正如我所说,简单的例子。现在我需要一个枚举器!这个:

public static class DummyList
{
    public static IEnumerable<int> GetDummy()
    {
        using (var dummy = new Dummy())
        {
            while (true) { yield return dummy.Value; }
        }
    }
}

这里没有火箭科学。只是一个带有静态扩展方法的静态 class,它将在循环中永远调用 Dummy Value 方法,将每个值产生给管道中的下一个方法。
需要说明的是,虽然这看起来是一个无限循环,但循环将在下一个方法中被打破。下一个方法是测试方法:

public static void DummyTest()
    {
        for (var x = 0; x < 5; x++)
        {
            Console.WriteLine(
                string.Join(", ", 
                    DummyList.GetDummy()
                             .DoAll(i => Console.Write($"Got {i}! "))
                             .Take(10)
                             .Select(i => i.ToString())));
        }
        GC.Collect();
    }

好吧,上面的代码将使用我的枚举器 5 次,每次只取 10 个值,然后将其作为列表写入控制台。最后,我调用垃圾收集器来清理它。我只是在我的控制台应用程序中从我的主要方法调用 DummyTest(); 来获得这个结果:

Dummy 1 created.
Got 1! Got 2! Got 3! Got 4! Got 5! Got 6! Got 7! Got 8! Got 9! Got 10! Dummy 1 disposed.
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Dummy 2 created.
Got 1! Got 2! Got 3! Got 4! Got 5! Got 6! Got 7! Got 8! Got 9! Got 10! Dummy 2 disposed.
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Dummy 3 created.
Got 1! Got 2! Got 3! Got 4! Got 5! Got 6! Got 7! Got 8! Got 9! Got 10! Dummy 3 disposed.
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Dummy 4 created.
Got 1! Got 2! Got 3! Got 4! Got 5! Got 6! Got 7! Got 8! Got 9! Got 10! Dummy 4 disposed.
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Dummy 5 created.
Got 1! Got 2! Got 3! Got 4! Got 5! Got 6! Got 7! Got 8! Got 9! Got 10! Dummy 5 disposed.
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Dummy 4 destroyed.
Dummy 3 destroyed.
Dummy 2 destroyed.
Dummy 1 destroyed.

嗯,看起来差不多。每次创建和处置对象时,我都知道我的枚举将被清理,即使我在循环中将其中断。我想确保这会发生,我首先想在这里问一下是否会发生。但是这个测试表明确实如此。
然而,垃圾收集器只销毁了 5 个对象中的 4 个!这让我有点烦恼。所有这些用于显示我的枚举器的代码都会很好地清理,只是发现它不会清理所有内容。虽然它不是那么重要,但我确实想知道为什么它不会删除最后一个对象,以及在这个复杂的示例中我如何强制垃圾收集器将它们全部销毁。

那么,为什么最后一个对象没有被销毁,我如何才能在 DummyTest 方法中强制销毁它


事情变得有点复杂了。此错误仅发生在 "Debug" 模式下,而不是 "Release" 模式下。当它跳过 Dummy 5 时我没有调试它,但我已经编译了它,有和没有调试信息。没有调试信息,Dummy 5 被释放。当使用调试信息编译项目时,似乎有什么东西保留在那个 Dummy 上。
我想知道为什么,以及如何防止这种情况。

看来原因与垃圾收集器有关,垃圾收集器在调试模式下的行为与发布模式下的行为不同。正如某些人评论的那样,当 DummyTest() 方法关闭时,垃圾编译器可能仍保留对 Dummy 5 的引用,因为它会保留该值以用于调试目的。在释放模式下,垃圾收集器的行为更加积极。
这个问题与 and answered in Does garbage collection run during debug? 有相似之处,但这个问题仍然很有趣,因为它适用于资源是更复杂的无限枚举的一部分的情况。
不过,这不是内存泄漏!只是当项目在调试模式下编译时,垃圾收集器往往会在某些资源上停留更长的时间,直到方法关闭,因为任何 运行 调试器可能仍在评估该值。 (它不知道没有调试器运行。)