はじめに
ReactiveProperty を使用して WPF アプリケーションを製作している時に見かけた怖いコード:
// 実際に見かけたものよりかなり簡易化してます。
// ViewModel: INotifyPropertyChanged, IDisposable を実装した VM 向けクラス
// Model: INotifyPropertyChanged を実装したシングルトンモデルクラス
// 他の VM を動的生成する VM クラス
public sealed class OneViewModel : ViewModel {
public ReadOnlyReactivePropertySlim<OtherViewModel?> Other { get; }
private CompositeDisposable Disposables { get; } = [];
public OneViewModel(Model model) {
// Model のプロパティ変化を監視して都度新しい VM を作成するよ。
this.Other = model.ObserveProperty(e => e.Foo)
.Select(x => new OtherViewModel(model))
.ToReadOnlyReactivePropertySlim()
.AddTo(this.Disposables);
}
protected override void Dispose(bool disposing) {
base.Dispose(disposing);
this.Disposables.Dispose();
}
}
// 他の VM から作成される VM クラス
public sealed class OtherViewModel : ViewModel {
public ReadOnlyReactivePropertySlim<int> Value { get; }
private CompositeDisposable Disposables { get; } = [];
public OtherViewModel(Model model) {
// 動的作成された VM でも Model のプロパティ変化を監視するよ。
this.Value = model.ObserveProperty(e => e.Bar)
.ToReadOnlyReactivePropertySlim()
.AddTo(this.Disposables);
}
protected override void Dispose(bool disposing) {
base.Dispose(disposing);
this.Disposables.Dispose();
}
}
このコードではプロパティの値変化に応じて新しい ViewModel が生成されていますが、恐ろしいことに「生成された ViewModel が一切破棄されていません」でした。
更に上記の OtherViewModel
は Model のプロパティ変化を監視している(= Model が OtherViewModel
を参照している)ため、Model が生存している限りこの VM が GC の対象になることはありません。
つまり、Model のプロパティが変更される度に破棄されることのない不要オブジェクトが量産される状況でした(これにより直ちにソフトに影響はありませんが…)。
本稿ではこういった状況に対処する方法について記載します。
確認時のバージョン情報
-
.NET
: 8.0.204 -
ReactiveProperty.WPF
: 9.5.0
確認用コード
動作確認用に以下の様なコードを用意してみます。
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Reactive.Bindings;
// この source が冒頭の `model.ObserveProperty(e => e.Foo)` 等に相当します。
var source = new Subject<string?>();
var property = source
.Select(name => Disposable.Create(() =>
Console.WriteLine($"{name} value is disposed.")))
.ToReadOnlyReactivePropertySlim();
Console.WriteLine("[action] next 1st");
source.OnNext("1st");
Console.WriteLine("[action] next 2nd");
source.OnNext("2nd");
Console.WriteLine("[action] next 3rd");
source.OnNext("3rd");
Console.WriteLine("[action] dispose");
property.Dispose();
[action] next 1st
[action] next 2nd
[action] next 3rd
[action] dispose
この実行結果からも、オブジェクトの破棄処理が実行されていないことが確認できます。
古い値を破棄する DisposePreviousValue
ReactiveProperty には古い値を破棄するための DisposePreviousValue
拡張メソッドが提供されています。
using Reactive.Bindings.Extensions;
var property = source
.Select(name => Disposable.Create(() =>
Console.WriteLine($"{name} value is disposed.")))
.DisposePreviousValue() // IDisposable オブジェクトを生成した後に追加。
.ToReadOnlyReactivePropertySlim();
このメソッドを追加しておくことで、値の変更時に過去オブジェクトが破棄されるようになり、また ReactiveProperty
が破棄されるタイミングで最終オブジェクトも破棄されるようになります。
[action] on next 1st
[action] on next 2nd
1st value is disposed.
[action] on next 3rd
2nd value is disposed.
[action] dispose
3rd value is disposed.
ReactiveProperty
に対して使用する時
この DisposePreviousValue
メソッドは ReactiveProperty
に対しても使用できますが、返り値が IObservable<T>
のため別途 Subscribe
メソッドを実行する必要があります(そのまま ReactiveProperty
として変数に設定することができません)。
var property = new ReactivePropertySlim<IDisposable?>();
_ = property.DisposePreviousValue().Subscribe();
// この返り値は property が破棄されるタイミングで自動的に破棄されるため対処不要。
そこで以下の様な拡張メソッドを作成しておけば便利(かもしれません)。
var property = new ReactivePropertySlim<IDisposable?>()
.WithDisposeValue();
public static class Extensions {
public static ReactivePropertySlim<T> WithDisposeValue<T>(this ReactivePropertySlim<T> self) {
_ = self.DisposePreviousValue().Subscribe();
return self;
}
}
おわりに
オブジェクトの破棄はしっかりしよう。