【问题标题】:Unit Testing WeakReference单元测试弱参考
【发布时间】:2021-12-31 17:11:59
【问题描述】:

似乎以下用于单元测试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);

测试针对 net461 和 net5.0 运行,适用于 DEBUG 和 RELEASE 模式。最好的结果:

  • 以 net461 为目标:以上代码运行成功,无论是 DEBUG 还是 RELEASE 模式。

  • 以 net5.0 为目标:上面的代码一开始在 DEBUG 和 RELEASE 模式下都失败了。阅读这篇文章 (Garbage collection behaviour difference between .NetFramework 4.8 and .Net 5) 后,它可以通过将 <TieredCompilation>false</TieredCompilation> 添加到 .csproj 以在 RELEASE 模式下运行。

所以问题缩小到针对 net5.0 调试模式

我也看到了以下帖子,但它们似乎不是很有帮助,主要是因为它们已经过时了:

值得一提的是@Eric Lippert 对Why C# Garbage Collection behavior differs for Release and Debug executables? 的回答说:您绝对不能依赖垃圾收集器在生命周期方面具有任何特定行为本地的,这表明无法可靠地测试 WeakReference。

编辑:有关为什么使用WeakReference 的上下文的更多信息:

我正在开发一个框架,提供类似于System.Data.DataRowSystem.Data.DataRowView 的类Entity<T>Record<T>,最重要的是前一个是强类型的。实体是模型提供自己的事件来通知更改,Record<T> 是一个视图模型,它包装了Entity<T> 以将更改事件转换为INotifyPropertyChanged 以及其他内容,例如数据绑定、验证等。Entity<T> 不应该知道Record<T> 的存在。为避免内存泄漏,Record<T> 应在仅 Entity 仍被引用时进行 GC 回收。

事件连接的示例代码:

IEntityListenerRecord<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;
    }
}

我希望方法 OnFieldUpdatedCleanUp 被单元测试完全覆盖。

【问题讨论】:

  • 恕我直言,您似乎已经回答了自己的问题。但是为什么你要测试弱引用的行为呢?您的应用程序依赖于什么行为?
  • 实际场景是使用弱引用的自定义弱事件实现。我需要测试当侦听器被垃圾收集时事件处理程序将被取消连接。这个问题没有完全回答,因为测试代码在调试模式下无法针对 net5.0 工作。
  • 使测试用例完全确定可能是不可能的。即使github.com/dotnet/runtime/blob/main/src/libraries/… 似乎也只有在PlatformDetection.IsPreciseGcSupported 时才会运行。我会退一步问你为什么使用弱引用,没有它们你能实现你的要求吗?
  • WeakReference 是我所知道的实现弱事件的唯一实用选项:codeproject.com/Articles/29922/Weak-Events-in-C。基本上在 CLR 世界中,WeakReference 是允许引用对象同时仍允许垃圾收集回收该对象的唯一选项。如果我错了,请纠正我。
  • @DekuDesu 我已经用事件连接示例更新了问题。

标签: c# .net .net-core garbage-collection weak-references


【解决方案1】:

现在您已经解释了尝试使用WeakReference 的原因。您真正需要的解决方案是实现IDisposable。这使调用者可以精确控制类的生命周期,从而允许垃圾收集器在资源未使用时立即释放它们。

我会考虑使用WeakReference 来避免IDisposable 成为反模式。垃圾收集是一项昂贵的操作。每个版本的 .net 运行时都可能会调整垃圾收集器的工作方式、发生频率以及删除的垃圾。在该收集发生之前,IEntityListener 的每个实例都将在其生命周期结束后继续接收事件。强制任何实现都需要额外的保护代码以防止行为不端。

虽然实现IDisposable 有一种令人讨厌的趋势,它会传播到每个班级。权衡是值得的。即使您正在构建一个框架供其他人重用。我不相信使用IDisposable 作为客户端代码和框架之间合同的一部分会阻止任何人使用您的库。但是,尝试使用 WeakReference 来反转此合同可能会给您的图书馆带来负面声誉。

【讨论】:

  • 非常感谢您的回答。与其考虑使用WeakReference 来避免IDisposable 成为反模式,我更愿意将其视为性能和易用性之间的权衡。 WPF 有一个WeakEventManager 可以准确地做到这一点,我相信它在内部使用了WeakReference
【解决方案2】:

受@Jeremy Lakeman (https://github.com/dotnet/runtime/blob/main/src/libraries/System.Runtime/tests/System/WeakReferenceTests.cs) 建议的运行时 WeakReference 单元测试源代码的启发,我最终得到了以下解决方案

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);
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-11-16
    • 1970-01-01
    • 2010-09-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-12
    • 2017-01-19
    相关资源
    最近更新 更多