かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Reactive Extensions再入門 番外編「センサー監視」

はじめに

基本的なことばかりでは飽きてきたので、ここらへんで息抜きとして、ここまででやっていないメソッドとかも駆使してReactive Extensionsを使うことで、どれだけプログラムがシンプルに書けるのか遊んでみようと思います。お題は以下の通りです。

センサー監視

下記の要件を満たすプログラムを作成します。
10個のセンサーがあります。10個のセンサーは、それぞれに識別用の名前が与えられていて1秒間隔で数値(1〜1000)を上げてきます。プログラムでは10秒間の間に一番大きな値を上げてきたセンサーの名前と値をコンソールに出力します。
ということで作成していきます。実際にセンサーは無いので下記のような疑似的なセンサーを表すクラスをセンサーに見立ててプログラムを書きます。

namespace SensorSample
{
    using System;
    using System.Threading;

    // センサーを表すクラス
    class Sensor
    {
        // センサーの値はとりあえず乱数で発行する
        private static Random random = new Random();
        // センサーの値を発行する間隔を制御するタイマー
        private Timer timer;

        // センサー名
        public string Name { get; private set; }

        // 名前をつけてセンサー作成
        public Sensor(string name)
        {
            this.Name = name;
        }

        // センサーの値の発行開始
        public void Start()
        {
            this.timer = new Timer(_ =>
            {
                this.OnPublish(random.Next(1001));
            },
            null,
            0,
            1000);
        }

        // センサーの値発行イベント
        public EventHandler<SensorEventArgs> Publish;

        // センサーから値を発行する
        private void OnPublish(int value)
        {
            var h = this.Publish;
            if (h != null)
            {
                h(this, new SensorEventArgs(this.Name, value));
            }
        }
    }

    // センサーが値を発行したときのイベント引数
    class SensorEventArgs : EventArgs
    {
        // 値を発行したセンサー名
        public string Name { get; private set; }
        // 発行した値
        public int Value { get; private set; }

        public SensorEventArgs(string name, int value)
        {
            this.Name = name;
            this.Value = value;
        }
    }
}

このプログラムをベースにReactive Extensionsを使ってプログラムを書いていきます。Mainメソッドの中身を下記に示します。

// 10個のセンサーを作成
var sensors = Enumerable.Range(1, 10).Select(i => new Sensor("Sensor#" + i)).ToArray();
// 10個のセンサーの値発行イベントをマージ
var subscription = Observable.Merge(
    sensors.Select(sensor => Observable.FromEvent<EventHandler<SensorEventArgs>, SensorEventArgs>(
        h => (s, e) => h(e),
        h => sensor.Publish += h,
        h => sensor.Publish -= h)))
    // 10秒ためて
    .Buffer(TimeSpan.FromSeconds(10))
    // その中から最大のものを探して
    .Select(values => values.Aggregate((x, y) => x.Value > y.Value ? x : y))
    // 表示する
    .Subscribe(e => Console.WriteLine("{0}: {1}", e.Name, e.Value));

// センサースタート
foreach (var sensor in sensors)
{
    sensor.Start();
}

Console.WriteLine("Sensor started.");
Console.ReadLine();
// 最後にセンサーのPublishイベントの購読解除
subscription.Dispose();

時間やイベントを扱うReactive Extensionsの得意分野なので非常にすっきりしたコードになっているのがわかります。恐らく普通にプログラムを書くとかなりの分量になると思われます。10秒間分の値を保持して最大値を探してetc…考えただけでも恐ろしくなります。
このプログラムの実行結果を下記に示します。

Sensor started.
Sensor#6: 994
Sensor#7: 997
Sensor#3: 1000
Sensor#1: 982
Sensor#4: 986
Sensor#4: 964

続行するには何かキーを押してください . . .

実行結果の字面上ではわかりませんが、10秒間隔で1行ずつ結果が表示されます。

まとめ

このようにReactive Extensionsを使えば時間、イベント、IObservableの合成、変換などなどを駆使して楽しく簡単?にコードを書くことが出来ます。私はReactive Extensionsのこういう所が大好きです。