测试/验证WeakReference

Ben*_*udo 16 c# unit-testing weak-references

我想验证代码设置a WeakReference不会意外地保持对引用对象的强引用.(这是一个很容易意外地做到这一点的例子.)

这看起来是检查无意中强引用的最佳方法吗?

TestObject testObj = new TestObject();
WeakReference wr = new WeakReference(testObj);

// Verify that the WeakReference actually points to the intended object instance.
Assert.Equals(wr.Target, testObject);

// Force disposal of testObj;
testObj = null;
GC.Collect();
// If no strong references are left to the wr.Target, wr.IsAlive will return false.
Assert.False(wr.IsAlive);
Run Code Online (Sandbox Code Playgroud)

Ben*_*udo 12

我与微软就这一点取得了联系,并了解到/确认:

  • GC.Collect() 强制阻止垃圾收集.
  • GC.Collect()运行时,它不会神秘跳过集合资格的对象.遵循可预测的规则来确定要收集的对象.只要您了解这些规则(即如何处理可终结对象),您就可以强制销毁特定对象,尽管被破坏对象使用的内存可能会被释放,也可能不会被释放.

有关我博客的更多信息:可以强制使用.Net垃圾回收吗?


n8w*_*wrl 7

我昨天就这样做了。这是我必须添加的内容,以确保收集发生在您上次断言之前:

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

如果在此之后 .IsAlive 仍然为真,则可能在某处仍然存在强引用。

顺便说一句 - 当你访问你的 WeakReference 目标时,一定不要检查 .IsAlive 。为避免在检查 .IsAlive 和 .Target 之间出现竞争条件,请执行以下操作:

var r = weakRef.Target AS Something;
if (r != null)
{
    ... do your thing
}
Run Code Online (Sandbox Code Playgroud)

  • GC.Collect() 执行阻塞收集——该方法仅在收集完成后返回,因此对 GC.WaitForFullGCComplete() 的调用应该是不必要的。GC.WaitForFullGCComplete() 旨在用于不同的场景。 (4认同)

aar*_*rro 7

涉及WeakReference对象的单元测试比您想象的要棘手。正如您和其他人所指出的那样,GC.Collect()大概可以“强制”进行垃圾收集,但这仍然取决于您的对象没有对其的引用。

不幸的是,如何构建你的代码可以改变对象是否仍然有它们的引用。更具体地说,无论您是在 Debug 模式还是 Release 模式下构建都可以并且会在对象仍然扎根时发生变化(更准确地说,取决于您是否打开了优化;Debug 默认关闭它们,而 Release 默认打开它们) . 调试模式关闭了很多优化,它甚至倾向于根植在当前正在执行的方法中创建/声明的对象。因此,您的单元测试可能会在 Debug 版本中失败,但在 Release 版本中会成功。

在您的示例中,即使您设置testObj为 NULL,编译器也试图通过保持其先前值的根来帮助调试构建。这意味着无论您调用多少次GC.Collect()wr.IsAlive都将始终返回 TRUE。

那么,你到底如何测试WeakReferences 呢?简单:在另一种方法中创建它们和它们所基于的对象。只要该方法没有内联,并且在大多数情况下不会内联,编译器就不会根植您关心的对象,并且您可以在 Debug 和 Release 构建中通过测试。

下面的函数为您提供了有关如何执行此操作的提示:

public static Tuple<WeakReference, ManualResetEvent, int> GetKillableWr(Func<object> func, bool useGetHashCode = false)
{
    var foo = func();
    var result = new Tuple<WeakReference, ManualResetEvent, int>(new WeakReference(foo), new ManualResetEvent(false), useGetHashCode ? (foo?.GetHashCode() ?? 0) : RuntimeHelpers.GetHashCode(foo));

    Task.Factory.StartNew(() =>
    {
        result.Item2.WaitOne();
        GC.KeepAlive(foo);  // need this here to make sure it doesn't get GC-ed ahead of time
        foo = null;
    });

    return result;
}
Run Code Online (Sandbox Code Playgroud)

使用此方法,只要您在参数创建对象func,您就可以创建一个WeakReference您选择的对象,该对象在您发出返回信号ManualResetEvent并调用后不会被植根GC.Collect()。正如其他人所指出的,调用以下代码以确保在您需要时进行清理会很有帮助...

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

编辑:

还有一些其他的“问题”需要担心。一个常见的涉及Strings。String文字和常量总是有根的,因为它们被编译为 DLL/EXE 的引用。因此,像这样的东西new WeakReference("foo")将始终显示为活动状态,因为“foo”已存储到您的 DLL 中,并且在编译代码中提供了对该存储文字的引用。解决此问题的一种简单方法是使用new StringBuilder("<your string here>").ToString()代替字符串文字。

再次编辑:

另一个“问题”是在 Release 构建中,优化会导致 GC 更加激进,与上述情况不同,这可能会导致对象比您预期的更快超出范围。在下面的代码中,wr.IsAlive有时会返回 FALSE,因为 GC 检测到该方法中myObject不会被其他任何东西使用,因此它使其符合垃圾收集的条件。解决这个问题的方法是放在GC.KeepAlive(myObject)方法的末尾。这将保持myObject扎根,直到至少执行该行。

public static void SomeTest()
{
    var myObject = new object();
    var wr = new WeakReference(myObject);
    GC.Collect();
    Assert.True(wr.IsAlive, "This could fail in Release Mode!");
}
Run Code Online (Sandbox Code Playgroud)