Rx の SelectMany について勘違いしていた

  • 3
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

Rxは便利すぎるのですが、まだ不慣れな私です。
ReactivePropertyを用いてMVVMパターンを組み立てていたところ、以下のようなシーンに遭遇しました。
以下、単純化したサンプルです。

example.cs
using System;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using Prism.Mvvm;
using Reactive.Bindings;
using Reactive.Bindings.Extensions;

namespace ConsoleExample
{
    class Model1 : BindableBase
    {
        private int _number = 0;
        public int Number
        {
            get { return _number; }
            set { SetProperty(ref _number, value); }
        }
    }

    class Model2 : BindableBase
    {
        private Model1 _model1 = null;
        public Model1 Model1
        {
            get { return _model1; }
            set { SetProperty(ref _model1, value); }
        }
    }

    class ViewModel
    {
        public Model2 Model2 { get; }
        public ReactiveProperty<int> Number { get; }

        public ViewModel()
        {
            Model2 = new Model2();
            Number = Model2
                .ToReactivePropertyAsSynchronized(m2 => m2.Model1)
                .Where(m1 => m1 != null)
                // ここのSelectMany
                .SelectMany(m1 => m1.ToReactivePropertyAsSynchronized(x => x.Number))
                .ToReactiveProperty(0);
        }
    }

    class View
    {
        private ViewModel _viewModel;

        public View(ViewModel viewModel)
        {
            _viewModel = viewModel;
            _viewModel.Number.Subscribe(x => Console.WriteLine(x));
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var viewModel = new ViewModel();
            var view = new View(viewModel);

            var model1_1 = new Model1();
            var model1_2 = new Model1();
            Observable.Timer(TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1)).Subscribe(x => model1_1.Number = (int)x);
            Observable.Timer(TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1)).Subscribe(x => model1_2.Number = (int)(x * 10));
            viewModel.Model2.Model1 = model1_1; // 1, 2, 3...
            Thread.Sleep(5500); // 5.5秒待つ
            viewModel.Model2.Model1 = model1_2; // 10, 20, 30...

            Console.ReadLine();
        }
    }
}

出力は以下のようになります。

0
1
2
3
4
5
50
60
6
70
7
80
8
90
9
100
10

このように、model1_1とmodel1_2の内容が混ざり合っています。
私は当初、以下のようにmodel1_1からmodel1_2へ切り替わると思っていました。

0
1
2
3
4
5
50
60
70
80
90
100

しかし、そうはなっていません。
SelectManyは、以下の処理と同じだったのです。

example.cs
Select(m1 => m1.ToReactivePropertyAsSynchronized(x => x.Number)).Merge()
// ↑SelectMany(m1 => m1.ToReactivePropertyAsSynchronized(x => x.Number))と同じ結果

model1_1とmodel1_2が混ざり合わずに切り替わるようにするためには、以下のようにします。

example.cs
Select(m1 => m1.ToReactivePropertyAsSynchronized(x => x.Number)).Switch()