单元测试WeakReference
的以下代码似乎无法正确/可靠地工作:
object realObject = new object();
WeakReference weakReference = new WeakReference(realObject);
Assert.NotNull(weakReference.Target);
realObject = null;
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Assert.Null(weakReference.Target);
针对DEBUG和RELEASE模式,针对net461和net5.0运行测试。最佳结果:
Targeting net461:以上代码成功运行,适用于DEBUG和RELEASE模式。
针对net5.0:上面的代码最初对DEBUG和RELEASE模式都失败。在阅读了这篇文章(.Net Framework 4.8和.Net 5之间的垃圾收集行为差异)之后,它通过将
<TieredCompilation>false</TieredCompilation>
添加到.csproj
来在RELEASE模式下运行。
因此问题缩小到针对net5.0调试模式。
我也看到了以下帖子,但它们似乎没有太大帮助,主要是因为它们已经过时了:
- 带WeakReference的单元测试代码
- 测试/验证弱点参考
值得一提的是@Eric Lippert对为什么C#垃圾回收行为在发布和调试可执行文件时不同的回答?says:您绝对不能依赖垃圾收集器在本地生存期内有任何特定的行为,这表明测试WeakReference是不可靠的。
编辑:关于为什么使用WeakReference
:的更多信息
我正在开发一个框架,提供类似于System.Data.DataRow
和System.Data.DataRowView
的类Entity<T>
和Record<T>
,最重要的是前一个是强类型的。实体是一个模型,它提供自己的事件来通知更改,Record<T>
是一个视图模型,它封装Entity<T>
,将更改事件转换为INotifyPropertyChanged
以及其他事情,如数据绑定、验证等。Entity<T>
不应该知道Record<T>
的存在。为了避免内存泄漏,当只有Entity仍然被引用时,Record<T>
应该被GC回收。
事件连接的示例代码:
IEntityListener
由Record<T>
类实现
internal interface IEntityListener
{
void OnFieldUpdated(FieldUpdatedEventArgs e);
}
WeakEntityListner
进行事件连接:
internal sealed class WeakEntityListener
{
private readonly WeakReference<IEntityListener> _weakListener;
private readonly Entity _entity;
internal WeakEntityListener(Entity entity, IEntityListener listener)
{
_entity = entity;
_weakListener = new (listener);
_entity.FieldUpdated += OnFieldUpdated;
}
private void OnFieldUpdated(object? sender, FieldUpdatedEventArgs e)
{
if (_weakListener.TryGetTarget(out var listener))
listener.OnFieldUpdated(e);
else
CleanUp();
}
private void CleanUp()
{
_entity.FieldUpdated -= OnFieldUpdated;
}
}
我想让方法OnFieldUpdated
和CleanUp
完全包含在单元测试中。
既然您已经解释了尝试使用WeakReference
的原因。您真正需要的解决方案是实现IDisposable
。这使调用者能够精确控制类的生存期,从而允许垃圾收集器在资源未使用时立即释放资源。
我认为使用WeakReference
来避免IDisposable
是一种反模式。垃圾收集是一项昂贵的操作。net运行时的每个版本都可以调整垃圾收集器的工作方式、发生频率以及清除哪些垃圾。在该集合发生之前,IEntityListener
的每个实例在其生存期结束后都将继续接收事件。强制任何实现都需要额外的保护代码来防止错误行为。
尽管实现IDisposable
有一种令人讨厌的趋势,即在每个类中传播。这种权衡是值得的。即使你正在构建一个供其他人重用的框架。我不认为使用IDisposable
作为客户端代码和框架之间的合同的一部分会阻止任何人使用您的库。然而,试图使用WeakReference
来推翻此合同,可能会给您的库带来负面声誉。
在DEBUG模式下失败的原因在我对https://stackoverflow.com/a/58662332/1261844(您链接到的)。如果您检查代码生成的IL,您会发现编译器生成了对您在临时变量的第一行中创建的对象的引用(也可能是在创建WeakReference时)。这个临时变量是你的对象的根。当启用优化时,编译器会生成不同的IL,从而尽可能避免使用临时变量。
简单地说,如果你想测试这样的东西,你必须在另一个方法中创建对象及其WeakReference。并非巧合的是,这也是你建议的答案中的代码(顺便说一句,这仍然是错误的)正在做的事情。
杰米·莱克曼的回答也是正确的。您不应该使用终结器来取消挂起事件。托管资源(如取消挂钩事件)应始终在Dispose()方法中清理。终结器用于非托管资源。为什么使用终结器来做这件事是一个糟糕的想法,这是有非常真实的原因的,尤其是当你的类被终结时,你不能保证它里面仍然有有效的引用。当您不知道对象何时真正空闲时,使用WeakEventManager
是最后的手段,即使在那时,这通常也是一种代码气味,表明您做错了什么。
我最终得到了以下解决方案,其灵感来自@Jeremy Lakeman建议的运行时WeakReference单元测试源代码(https://github.com/dotnet/runtime/blob/main/src/libraries/System.Runtime/tests/System/WeakReferenceTests.cs)
using System;
using Xunit;
using Xunit.Abstractions;
public class WeakReferenceTests
{
private sealed class Latch
{
public bool FinalizerRan { get; set; }
}
private sealed class TestObject
{
~TestObject()
{
Latch.FinalizerRan = true;
}
public Latch Latch { get; } = new();
}
private readonly ITestOutputHelper output;
public WeakReferenceTests(ITestOutputHelper output)
{
this.output = output;
}
[Fact]
public void Test1()
{
TestObject realObject = new TestObject();
Latch l = realObject.Latch;
Assert.False(l.FinalizerRan);
var weakReference = new WeakReference(realObject);
Assert.NotNull(weakReference.Target);
GC.KeepAlive(realObject);
realObject = null;
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
if (!l.FinalizerRan)
output.WriteLine("Attempted GC but could not force test object to finalize. Test skipped.");
else
Assert.Null(weakReference.Target);
}
}