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垃圾回收吗?
我昨天就这样做了。这是我必须添加的内容,以确保收集发生在您上次断言之前:
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)
涉及WeakReference
对象的单元测试比您想象的要棘手。正如您和其他人所指出的那样,GC.Collect()
大概可以“强制”进行垃圾收集,但这仍然取决于您的对象没有对其的引用。
不幸的是,如何构建你的代码可以改变对象是否仍然有它们的引用。更具体地说,无论您是在 Debug 模式还是 Release 模式下构建都可以并且会在对象仍然扎根时发生变化(更准确地说,取决于您是否打开了优化;Debug 默认关闭它们,而 Release 默认打开它们) . 调试模式关闭了很多优化,它甚至倾向于根植在当前正在执行的方法中创建/声明的对象。因此,您的单元测试可能会在 Debug 版本中失败,但在 Release 版本中会成功。
在您的示例中,即使您设置testObj
为 NULL,编译器也试图通过保持其先前值的根来帮助调试构建。这意味着无论您调用多少次GC.Collect()
,wr.IsAlive
都将始终返回 TRUE。
那么,你到底如何测试WeakReference
s 呢?简单:在另一种方法中创建它们和它们所基于的对象。只要该方法没有内联,并且在大多数情况下不会内联,编译器就不会根植您关心的对象,并且您可以在 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)
编辑:
还有一些其他的“问题”需要担心。一个常见的涉及String
s。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)