我应该总是处理可观察对象的订阅吗?

3

ViewModel自动超出作用域并且没有对其他类的引用时,我是否应该始终处置可观察对象?

下面是一个小例子:

public class TestViewModel : ReactiveObject
{
    public TestViewModel()
    {
        MyList = new ReactiveList<string>();
        MyList.ChangeTrackingEnabled = true;

        //Do I have to dispose/unsubscribe this?
        MyList.ItemChanged.Subscribe(_ => ...);

        //Or this?
        this.WhenAnyValue(x => x.MyList).Subscribe(_ => ...);
    }

    ReactiveList<string> _myList;
    public ReactiveList<string> MyList
    {
        get => _myList;
        set => this.RaiseAndSetIfChanged(ref _myList, value);
    }
}

据我了解,订阅是普通的.NET对象。没有超出ViewModel类的引用。所以当我的TestViewModel超出范围(即该对象不再使用并被另一个对象替换)时,GarbageCollector应该清理ViewModel内部的所有内容,因此我不需要手动调用返回的IDisposables上的Dispose。
我这样说是否正确? 编辑
ReactiveList也可以持有其他.NET对象。此示例与不可变字符串类型无关。

谁知道调度程序会涉及什么,以及有哪些订阅可能会挂在您的“ViewModel”上?您应该始终处理可处置对象。这只是一个良好的编程实践。 - Enigmativity
但是ReactiveUiDocs指出,并不是每个Subscription都需要被处理? - senz
2
我同意,不是每个订阅都需要被处理。也不是每个 IDisposable 都需要被处理。但是很难总是知道哪些必须被处理。我想说的是,你不能确定你没有留下一个持有资源的订阅。如果你执行任何并发涉及计时器的连接或查询,那么你就会持有它们。你不知道你的客户端如何使用你的可观察对象。最好的做法是始终进行处理。 - Enigmativity
RxUI的开发人员持不同意见,特别是Kent Boogart发现如果Dispose所有内容,尤其是在移动设备上,它会降低性能。 - Glenn Watson
“这是必要的吗?为了回答这个问题,您需要考虑每个订阅所建立的对象链。如果该链中没有任何根源,它将不会阻止对象被收集。” 在您的问题中,除了ReactiveList被公开之外,没有任何东西超出了当前类的范围,WhenAnyValue(x => x.MyList)简化了对当前对象的INotifyPropertyChanged的订阅,并且仅在Subscribe中本地化,ItemChanged是本地的并且作用于不可变项。唯一可能的问题是您的ReactiveList和外部订阅者。 - Glenn Watson
3个回答

2
这是来自 ReactiveUI 维护者之一 Kent Boogart 对此问题的看法:
那么,假设你正在视图中使用 `WhenActivated`,那么何时处理它返回的可处理对象?你需要将其存储在本地字段中并使视图成为可处理对象。但是,谁来处理视图的处理呢?您需要平台钩子来知道何时适当地处理它-如果该视图在虚拟化场景中被重复使用,则这不是一个微不足道的问题。因此,有这个问题。
当执行反应式命令时怎么办?您是否存储了获取的可处理对象,以便稍后“清理”?我猜没有,并且有很好的原因。当执行完成时,所有观察者都会自动取消订阅。通常,对具有有限生命周期(例如通过超时)的管道进行订阅无需手动处理。处理此类订阅与处理 `MemoryStream` 的处理一样有用。
除此之外,我发现特别是在 VM 中,反应式代码往往需要处理大量的可处理对象。存储所有这些可处理对象并尝试处理会使代码混乱,并迫使 VM 自身成为可处理对象,进一步混淆事情。性能是另一个要考虑的因素,特别是在 Android 上。
因此,我的建议源于所有这些。我发现将那些需要处理的订阅包装在 `WhenActivated` 中是最实用的方法。

1
要回答这类与您具体情况相关的问题,您需要使用诊断工具来找出在您的情况下起作用的内容。
使用using块运行一次测试,再运行一次未使用该块的测试:
class Program
{
    static void Main(string[] args)
    {
        //warmup types
        var vm = new TestViewModel();

        Console.ReadLine(); //Snapshot #1
        for (int i = 0; i < 1000; i++)
            Model();

        GC.Collect();
        Console.ReadLine();  //Snapshot #2
    }

    private static void Model()
    {
        using (var vm = new TestViewModel())
        {                
        }
    }
}

没有手动处理:

No disposal

两个订阅已处理:

Both subscriptions disposed

对于1k次迭代,差异不大,垃圾回收正在发挥作用。这些差异主要是WeakReference类型。
最终,就像Glenn Watson所说的那样,您必须根据具体情况做出决定。使用周期性调度的可观察对象是手动处理的好选择。 ReactiveUI指南:何时处理订阅的释放

Kent Boogart在他的书《You, I和ReactiveUI》中有一章,讲述为什么总是像这样“处理”订阅是一个坏主意,特别是在移动设备上。你必须聪明地选择使用它的地方。 - Glenn Watson
基本上,您将大幅降低性能。实际上,即使文档也警告不要过度处置 https://reactiveui.net/docs/guidelines/framework/dispose-your-subscriptions - Glenn Watson
@GlennWatson 已经注意到了。我已经修改了答案。 - Asti

0

您永远不应该依赖垃圾回收器来清理对象。

终结器有最大的执行时间并且可能以任意顺序执行,如果正确实现了释放模式,则甚至不会调用Dispose

Dispose 应该清理托管资源,而终结器应该清理非托管资源以避免内存泄漏。 Disposing(bool) 模式将在显式调用Dispose时同时调用两者,在垃圾回收时只调用非托管资源清理。

显式调用Dispose 也可以让您更早地进行清理,而不必等待 GC 进行清理。

对于订阅、事件处理程序等,不清除引用可能意味着当 GC 收集时,仍存在引用,这种情况下对象根本不会被收集。

如果您知道没有到根路径,则可能不需要显式处理,但在这种情况下,通常会有人后来过来,无意中保留了图表中一个对象的引用,然后整个图表就永远无法被收集。


垃圾回收器(GC)永远不会在IDisposable对象上调用.Dispose()。绝不会。但是,它可能会调用终结器,而终结器可能会调用.Dispose()。除非您检查每个使用的IDisposable的源代码,否则您永远不知道它是否会被调用。 - Enigmativity
我并没有说GC会调用Dispose()。如果实现了正确的dispose模式,终结器就不应该处理托管对象的释放。Dispose调用Dispose(true),这将释放托管和非托管资源,并抑制终结器。终结器调用Dispose(false),这只释放非托管资源。在使用时仍然应该调用Dispose(),但如果忘记了,终结器至少应该防止非托管内存泄漏。 - George Helyar
是的,同意。只要实现终结器正确,它就能正常工作。 - Enigmativity

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接