ReactiveProperty v9.0.0 をリリースしました
ReactiveProperty v9.0.0 をリリースしました。
NuGet から導入できます。
更新概要
今回のアップデートはパフォーマンスの向上と System.Reactive
パッケージに依存していない ReactiveProperty.Core
パッケージでの開発出来るカバー範囲の拡大を行いました。
今までよりも ReactiveProperty
パッケージではなく ReactiveProperty.Core
パッケージだけで開発が出来るようになるケースが多くなると思います。
更新内容一覧
ReactiveProperty から ReactiveProperty.Core へ移動させたクラスやメソッド
-
AsyncReactiveCommand<T>
を移動 -
ObserveProperty
拡張メソッドを移動 -
ToReactivePropertySlimAsSynchronized
拡張メソッドを移動 - Notifier 系クラスの
BooleanNotifier
,BusyNotifier
,CountNotifier
,MessageBroker
,AsyncMessageBroker
を移動- これらのクラスは、今まではスレッドセーフでしたがスレッドセーフではなくなりました。どうしても従来の動作と同じにしたい場合は
ReactiveProperty
パッケージの中にある、クラス名の後ろにLegacy
を追加されたクラスがあるので、そちらを利用してください。まぎらわしいので、インテリセンスに出さないように設定していますが、今後出してほしいという要望があればインテリセンスに出すようにもしようと思います。
- これらのクラスは、今まではスレッドセーフでしたがスレッドセーフではなくなりました。どうしても従来の動作と同じにしたい場合は
ReactiveProperty.Core へ追加したクラスやメソッド
-
ReactiveCommandSlim<T>
を追加 -
CompositeDisposable
をReactive.Bindings.Disposables
名前空間 (新設) に追加 -
Reactive.Bindings.TinyLinq
名前空間を追加して以下のIObservable<T>
などへの拡張メソッドを追加-
Select
,Where
,CombineLatest
(CombineLatest
だけIEnumerable<IObservable<T>>
への拡張メソッド)
-
-
ValidatableReactiveProperty<T>
を追加
ReactiveProperty へ追加したメソッド
-
IReadOnlyCollection<T>
のすべての要素のプロパティの変更を監視するためのCollectionUtilities.ObserveElementProperty
メソッドとObserveElementObservableProperty
メソッドを追加しました。
更新内容詳細
ReactiveCommandSlim
ReactiveCommand
の軽量版になります。大きな違いは従来の ReactiveCommand
との違いは CanExecuteChanged
イベントを UI スレッド上にディスパッチをしなくなった点です。もし、UI スレッド上でイベントを必ず発生させる必要がある場合は ReactiveCommandSlim
のソースになった IObservable<bool>
側で ObserveOn
メソッドを使って明示的に設定をしてください。
さらに ReactivePropertySlim
と同じ形で実装することで各種パフォーマンスが大きく改善しています。ベンチマークは以下のようになります。
Method | Mean | Error | StdDev | Median |
---|---|---|---|---|
CreateReactiveCommand | 291.931 ns | 5.6965 ns | 10.5589 ns | 291.178 ns |
CreateReactiveCommandSlim | 4.313 ns | 0.1293 ns | 0.1080 ns | 4.269 ns |
BasicUsecaseForReactiveCommand | 1,187.294 ns | 22.8930 ns | 21.4141 ns | 1,179.896 ns |
BasicUsecaseForReactiveCommandSlim | 91.861 ns | 1.8934 ns | 3.5096 ns | 91.750 ns |
上から順に ReactiveCommand
を new
したもの、ReactiveCommandSlim
を new
したもの、ReactiveCommand
の基本的な機能を一通り使ったケース、ReactiveCommandSlim
の基本的な機能を一通り使ったケースになります。インスタンス化のケースで 70 倍以上、基本的な機能を使用したケースでも 13 倍の性能差があります。
また AsyncReactiveCommand
と同じように IReactiveProperty<bool>
を複数コマンドで共有することで実行可否状態を複数コマンド間で簡単に共有出来るようになっています。
// CanExecute のソースになる IObservable<bool>
IObservable<bool> someCanExecuteSource = ...;
// これを共有することで CanExecute の状態を共有可能にする
ReactivePropertySlim<bool> sharedStatus = new();
// command1 と command2 で CanExecute の状態が同期される
// AsyncReactiveCommand はコマンド実行中に自動的に CanExecute が false になるので command1 が実行中は sharedStatus 経由で command2 の CanExecute も false になる
AsyncReactiveCommand command1 = someCanExecuteSource.ToAsyncReactiveCommand(sharedStatus)
.WithSubscribe(async () =>
{
await Task.Delay(3000); // 何か時間のかかる処理
});
ReactiveCommandSlim command2 = someCanExecuteSource.ToReactiveCommandSlim(sharedStatus)
.WithSubscribe(() =>
{
// do something
});
ValidatableReactiveProperty<T>
バリデーション機能を持った IReactiveProperty<T>
の実装になります。バリデーション機能を持った軽量の ReactiveProperty<T>
を使用したい場合はこちらのクラスを利用してください。
従来はバリデーション機能が必要な場合は ReactiveProperty<T>
を使用して、そうじゃない場合は ReactivePropertySlim<T>
を使用するといった形でしたが、今後はバリデーション機能が必要な場合も軽量な実装の ValidatableReactiveProperty<T>
を使用することでパフォーマンス面のメリットを得ることが出来ます。
これで ReactiveProperty<T>
のメリットは PropertyChanged
イベントを必ず UI スレッド上で実行するようにするという点だけになりました。このメリットのためだけに性能劣化を受け入れるかどうかという判断基準になります。
ReactiveProperty<T>
と ReactivePropertySlim<T>
の性能差は以下のようになります。圧倒的に Slim
のほうが早くなります。
Method | Mean | Error | StdDev | Median |
---|---|---|---|---|
CreateReactivePropertyInstance | 67.878 ns | 1.3961 ns | 2.7230 ns | 68.221 ns |
CreateReactivePropertySlimInstance | 5.880 ns | 0.1770 ns | 0.5218 ns | 5.714 ns |
BasicForReactiveProperty | 2,708.497 ns | 53.9674 ns | 55.4206 ns | 2,715.572 ns |
BasicForReactivePropertySlim | 60.237 ns | 1.2479 ns | 2.3438 ns | 59.238 ns |
上から順に ReactiveProperty<T>
のインスタンス化、ReactivePropertySlim<T>
のインスタンス化、ReactiveProperty<T>
の基本的な機能を使用したケース、ReactivePropertySlim<T>
の基本的な機能を使用したケースになります。インスタンス化で 13 倍、基本的な機能を使用したケースでは 45 倍ほど Slim
のほうが早くなります。
バリデーションのためだけに、この性能劣化を受け入れるのはメリットに対してデメリットが大きいので、バリデーション機能に特化した軽量版の IReactiveProperty<T>
の実装を追加しました。以下のようにシンプルにバリデーションロジックを指定するケースのベンチマーク結果になります。
[Benchmark]
public ReactiveProperty<string> ReactivePropertyValidation()
{
var rp = new ReactiveProperty<string>("")
.SetValidateNotifyError(x => string.IsNullOrEmpty(x) ? "invalid" : null);
rp.Value = "xxx"; // valid
rp.Value = ""; // invalid
return rp;
}
[Benchmark]
public ValidatableReactiveProperty<string> ValidatableReactivePropertyValidation()
{
var rp = new ValidatableReactiveProperty<string>(
"",
x => string.IsNullOrEmpty(x) ? "invalid" : null);
rp.Value = "xxx"; // valid
rp.Value = ""; // invalid
return rp;
}
Method | Mean | Error | StdDev | Median |
---|---|---|---|---|
ReactivePropertyValidation | 4,954.138 ns | 93.2171 ns | 107.3490 ns | 4,942.650 ns |
ValidatableReactivePropertyValidation | 704.852 ns | 12.8322 ns | 10.7155 ns | 708.510 ns |
また、以下のような DataAnnotations
によるバリデーションと通常のクラスからの ReactiveProperty
の生成を行うよくあるケースでもベンチマークをとりました。以下のように ViewModel
にありがちなパターンを実装したクラスを 2 つ用意します。
// ReactiveProperty を使用したケース
public class ReactivePropertyVM : IDisposable
{
private Person _model = new();
[Required]
public ReactiveProperty<string> Name { get; }
public ReactivePropertyVM()
{
Name = _model.ToReactivePropertyAsSynchronized(x => x.Name,
ignoreValidationErrorValue: true)
.SetValidateAttribute(() => Name);
}
public void Dispose()
{
Name.Dispose();
}
}
// ValidatableReactiveProperty を使用したケース
public class ValidatableReactivePropertyVM : IDisposable
{
private Person _model = new();
[Required]
public ValidatableReactiveProperty<string> Name { get; }
public ValidatableReactivePropertyVM()
{
Name = _model.ToReactivePropertySlimAsSynchronized(x => x.Name)
.ToValidatableReactiveProperty(
() => Name,
disposeSource: true); // ToReactivePropertySlimAsSynchronized で生成された ReactivePropertySlim も まとめて Dispose するための引数
}
public void Dispose()
{
Name.Dispose();
}
}
ベンチマーク部分のコードは以下のようになります。
[Benchmark]
public ReactivePropertyVM ReactivePropertyValidationFromPoco()
{
var vm = new ReactivePropertyVM();
vm.Name.Value = "valid";
vm.Name.Value = "";
vm.Dispose();
return vm;
}
[Benchmark]
public ValidatableReactivePropertyVM ValidatableReactivePropertyValidationFromPoco()
{
var vm = new ValidatableReactivePropertyVM();
vm.Name.Value = "valid";
vm.Name.Value = "";
vm.Dispose();
return vm;
}
結果は以下のようになりました。
Method | Mean | Error | StdDev | Median |
---|---|---|---|---|
ReactivePropertyValidationFromPoco | 9,642.258 ns | 181.2835 ns | 208.7663 ns | 9,617.409 ns |
ValidatableReactivePropertyValidationFromPoco | 4,282.720 ns | 57.8372 ns | 54.1010 ns | 4,287.605 ns |
最初のシンプルなケースでは 7 倍、Poco
を使った ViewModel
の実装のケースでも倍近い性能改善があります。Poco
の方は DataAnnotations
の属性を取得するためにリフレクションを使用しているため、その部分の処理時間が大きくなるためこのような結果になっていると考えられます。
System.Reactive を追加しなくても本当に必要最低限のことが出来るようにするための機能
いくつかクラスを追加した後にサンプルを書こうと思ったら、やっぱり LINQ が恋しくなりました。あと CompositeDisposable
が無いと話にならないと思ったので本当に必要最低限だけ追加しました。
Reactive.Bindings.Disposables.CompositeDisposable
を追加しています。IObservable<T>
の拡張メソッドとして LINQ の基本のキの Select
と Where
を追加しました。Delay
とかみたいに処理を遅らせるようなものは見栄えはいいのですが、実際には Select
と Where
があれば軽いものであれば大体書けてしまうと思います。
また ReactiveCommand
系を生成するために CombineLatest
で IObservable<bool>
を束ねて ToAsyncReactiveCommand()
や ToReactiveCommandSlim()
をしたくなったので IEnumerable<IObservable<bool>>
に対しての拡張メソッドとして CombineLatest
を追加しています。
とりあえずのやっつけ実装なので本家と比べると遅いし効率もそんなに良くないと思いますが…。もし、それ以上が欲しい場合は追加で System.Reactive を追加するか ReactiveProperty の方のパッケージを追加して使ってくださいというコンセプトです。
IReadOnlyCollection<T>
向けの ObserveElementProperty
, ObserveElementObservableProperty
, ObserveElementPropertyChanged
メソッド
これまでは ObservableCollection<T>
などのように INotifyCollectionChanged
インターフェースを実装したコレクションをターゲットにしていましたが、読み取り専用の IReadOnlyCollection<T>
に対しても同様のことが出来るようにしました。以下のように Reactive.Bindings.Helpers.CollectionUtilities
の静的メソッドとして定義しています。
var people = new[]
{
new Person { FirstName = "Kazuki", LastName = "Ota" },
new Person { FirstName = "Taro", LastName = "Tanaka" },
new Person { FirstName = "Nobunaga", LastName = "Oda" },
};
var disposable = CollectionUtilities.ObserveElementProperty(people, x => x.FirstName)
.Subscribe(PropertyPack<Person, string> x =>
{
// Name プロパティの変更時の処理
});
まとめ
ReactiveProperty
の久しぶりのメジャーバージョンアップになります。
このバージョンアップからは Rx
の機能をヘビーに使うといった用途でない場合は、ほとんどのケースで ReactiveProperty.Core
パッケージで開発が出来るようになると思います。Rx
の機能をヘビーに使う場合には、これまで通り ReactiveProperty
のパッケージを参照してください。
まだドキュメント類が 9.0.0 向けに更新されていませんが、基本的な機能については以下のフォルダーに WPF で作ったサンプルがあるので参考にしてください。
Discussion