xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

Rx入門 (11) - イベントのシーケンス化

Rx入門 (2) - オブザーバーパターンの最後にもチラッと書きましたが、イベントはオブザーバーパターンの一実装です。なので、イベントをIObservable<T>に乗せ、イベントに対応する処理を記述することができます。そこで今回は、イベントをIObservable<T>シーケンスに変換する方法について紹介します。

IObservable<T>への変換

Rxのライブラリには、イベントをIObservable<T>に変換するためのファクトリーメソッドがふたつ用意されています。Observable.FromEventメソッドObservable.FromEventPatternメソッドです。早速、簡単に使い方を確認してみましょう。以下にObservable.FromEventメソッドを使った簡単なサンプルを示します。

まず、キー入力が行われる度にKeyPressイベントを発行するだけの簡単なクラスを作成します。イベントの型にはAction<T>デリゲートを利用しています。また、Ctrl + Qが押下されたらループを抜けるようになっています。

using System;
 
namespace Sample15_FromEvent
{
    class EventNotifier
    {
        public event Action<ConsoleKeyInfo> KeyPress = null;
        public void ObserveKeyInput()
        {
            while (true)
            {
                var info = Console.ReadKey(true);
                if (info.Modifiers == ConsoleModifiers.Control)
                if (info.Key == ConsoleKey.Q)
                    return;
 
                var handler = this.KeyPress;
                if (handler != null)
                    handler(info);
            }
        }
    }
}

次に、Observable.FromEventメソッドを利用してKeyPressイベントの購読を行ないます。ジェネリックの型には、Action<T>デリゲートで受ける型を記述します。第1引数にはイベントの関連付け処理を、第2引数にはイベントの関連付け解除処理を記述します。OnNextメソッドの引数には、イベントの引数が流れてきます。

using System;
using System.Reactive.Linq;
 
namespace Sample15_FromEvent
{
    class Program
    {
        static void Main()
        {
            var notifier = new EventNotifier();
            Observable.FromEvent<ConsoleKeyInfo>
            (
                handler => notifier.KeyPress += handler,
                handler => notifier.KeyPress -= handler
            )
            .Subscribe(value => Console.WriteLine("OnNext({0})", value.Key));
            notifier.ObserveKeyInput();
        }
    }
}
 
//----- 結果(例)
/*
OnNext(A)
OnNext(B)
OnNext(C)
OnNext(Enter)
*/

実行結果の通り、入力したキーが表示されるだけの簡単なサンプルです。すでにお分かりかと思いますが、イベントをIObservable<T>に変換することで、これまでイベントハンドラとして記述していたものをOnNextメソッドで処理するように書き換えることができます。また、IObservable<T>に変換できるということは、Rx入門 (8) - LINQスタイルでの記述で紹介したようにLINQスタイルの処理を間に挟むことができるので、通常のイベントよりも柔軟でわかりやすい処理ができるようになります。

独自デリゲートを持つイベントの変換

先の例ではAction<T>デリゲートに対する変換を行いました。Observable.FromEventメソッドにはいくつかのオーバーロードが用意されており、これらを利用すればEventHandler<T>デリゲートや独自デリゲートも変換可能です。

先ほど挙げた例を少し変形したサンプルを下に示します。イベント引数を3つに分解しただけの簡単なものです。(分解しているのはサンプルのためであって、それ以上の意味はありません)

using System;
 
namespace Sample16_FromEventUsingConversion
{
    class EventNotifier
    {
        public event Action<ConsoleKey, char, ConsoleModifiers> KeyPress = null;
        public void ObserveKeyInput()
        {
            while (true)
            {
                var info = Console.ReadKey(true);
                if (info.Modifiers == ConsoleModifiers.Control)
                if (info.Key == ConsoleKey.Q)
                    return;
 
                var handler = this.KeyPress;
                if (handler != null)
                    handler(info.Key, info.KeyChar, info.Modifiers);
            }
        }
    }
}

Observable.FromEventメソッドの別のオーバーロードを利用してIObservable<T>に変換します。今回は、イベントから渡される情報のうち、OnNextメソッドに通知したい情報がConsoleKey列挙体だけだったとして話を進めます。(というか、OnNextメソッドへ流すことができるのはひとつだけ、という制約があるのだけど)

using System;
using System.Reactive.Linq;
 
namespace Sample16_FromEventUsingConversion
{
    class Program
    {
        static void Main()
        {
            var notifier = new EventNotifier();
            Observable.FromEvent<Action<ConsoleKey, char, ConsoleModifiers>, ConsoleKey>
            (
            //--- ↓↓ 第1引数はコレの省略記法
            //  handlerA => new Action<ConsoleKey, char, ConsoleModifiers>((key, letter, modifiers) => handlerA(key)),
                handlerA => (key, letter, modifiers) => handlerA(key),
                handlerB => notifier.KeyPress += handlerB, //--- 第2引数と第3引数の
                handlerB => notifier.KeyPress -= handlerB  //--- handlerB変数は第1引数の戻り値
            )
            .Subscribe(value => Console.WriteLine("OnNext({0})", value));
            notifier.ObserveKeyInput();
        }
    }
}
 
//----- 結果(例)
/*
OnNext(X)
OnNext(Y)
OnNext(Z)
OnNext(Escape)
*/

Observable.FromEventメソッドに指定するひとつめのジェネリック型はイベントのデリゲート型です。ふたつめのジェネリック型には、OnNextメソッドで通知する情報の型を指定します。

前の例では出て来なかった第1引数が、今回の重要なポイントです。イベント引数と通知する情報の型が異なるため、その差を埋めるためのコンバートを行っています。ラムダ式のGoes To記号 "=>" がふたつ続いているためわかりにくいですが、サンプルコードの中にもコメントで記載している通り、第1引数は省略記法となっています。第1引数で返したデリゲートが、第2/第3引数のhandlerB変数として渡され、イベントに関連付けられます。つまり、KeyPressイベントが発生した際には、第1引数で返したデリゲート (handlerB) を介してhandlerAデリデートが呼び出される、ということです。そして、handlerAデリゲートの内部でOnNextメソッドの呼び出しが行われます。

関連付けの挙動確認

次に、Observable.FromEventメソッドに渡す各引数がどのように呼び出されるかを見てみます。先ほどの例をトレースするために画面出力を行うようにしたサンプルを示します。EventNotifierの内容は名前空間を除いて同じなので割愛します。

using System;
using System.Reactive.Linq;
 
namespace Sample17_AnalyzeFromEvent
{
   static class Program
   {
       static void Main()
       {
           var notifier = new EventNotifier();
           var sequence = Observable.FromEvent<Action<ConsoleKey, char, ConsoleModifiers>, ConsoleKey>
           (
               handlerA =>
               {
                   Console.WriteLine("Conversion");
                   return (key, letter, modifiers) =>
                   {
                       Console.WriteLine("OnConvert");
                       handlerA(key);
                   };
               },
               handlerB =>
               {
                   Console.WriteLine("AddHandler");
                   notifier.KeyPress += handlerB;
               },
               handlerB =>
               {
                   Console.WriteLine("RemoveHandler");
                   notifier.KeyPress -= handlerB;
               }
           );
           var disposerA = sequence.SubscribeTracer("A");
           var disposerB = sequence.SubscribeTracer("B");
           notifier.ObserveKeyInput();
           disposerA.DisposeTracer("A");
           notifier.ObserveKeyInput();
       }
 
       static IDisposable SubscribeTracer<T>(this IObservable<T> source, string name)
       {
           Console.WriteLine("----- {0} : Subscribe Before -----", name);
           var disposer = source.Subscribe
           (
               value => Console.WriteLine("{0} : OnNext({1})", name, value),
               ()    => Console.WriteLine("{0} : OnCompleted", name)
           );
           Console.WriteLine("----- {0} : Subscribe After -----", name);
           return disposer;
       }
 
       static void DisposeTracer(this IDisposable source, string name)
       {
           Console.WriteLine("----- {0} : Dispose Before -----", name);
           source.Dispose();
           Console.WriteLine("----- {0} : Dispose After -----", name);
       }
   }
}
 
//----- 結果(例)
/*
----- A : Subscribe Before -----
Conversion
AddHandler
----- A : Subscribe After -----
----- B : Subscribe Before -----
Conversion
AddHandler
----- B : Subscribe After -----
OnConvert
A : OnNext(F1)
OnConvert
B : OnNext(F1)
OnConvert
A : OnNext(F2)
OnConvert
B : OnNext(F2)
----- A : Dispose Before -----
RemoveHandler
----- A : Dispose After -----
OnConvert
B : OnNext(F3)
OnConvert
B : OnNext(F4)
*/

上記の結果より、Subscribeメソッドによる関連付けのタイミングで第1引数と第2引数が連続して呼び出されていることがわかります。また、KeyPressイベント発生時に第1引数で返したデリゲートが実行されていることが確認できます。Disposeメソッドにより購読の解除を行うと、第3引数のデリゲートが呼び出され、イベント解除が行われます。

何をやっているのか、何が起こっているのかはわかりにくく、あまり理解が進まない場合は、各引数にブレークポイントを置くなどして挙動をトレースしてみると良いです。私も最初は混乱していたのですが、トレースしたらすぐに理解できました。

FromEventとFromEventPatternの違い

Observable.FromEventPatternメソッドは、EventHandlerEventHandler<T>に代表される典型的なイベントに特化したファクトリーメソッドです。基本的な使い方はObservable.FromEventメソッドとほとんど同じです。これにも多数のオーバーロードがあり、独自イベントハンドラへの対応も可能です。また、リフレクションを利用した記述も可能になっています。

以下に簡単なサンプルを示します。まず、イベント引数を定義します。ConsoleKeyInfo構造体をラップしているだけの非常に簡単なものです。

using System;
 
namespace Sample18_FromEventPattern
{
    class KeyPressEventArgs : EventArgs
    {
        public ConsoleKeyInfo Info{ get; private set; }
        public KeyPressEventArgs(ConsoleKeyInfo info)
        {
            this.Info = info;
        }
    }
}

次に、前の例で何度も出てきているEventNotifierのイベントを、EventHandler<T>を利用した形に書き換えます。

using System;
 
namespace Sample18_FromEventPattern
{
    class EventNotifier
    {
        public event EventHandler<KeyPressEventArgs> KeyPress = null;
        public void ObserveKeyInput()
        {
            while (true)
            {
                var info = Console.ReadKey(true);
                if (info.Modifiers == ConsoleModifiers.Control)
                if (info.Key == ConsoleKey.Q)
                    return;
 
                var handler = this.KeyPress;
                if (handler != null)
                    handler(this, new KeyPressEventArgs(info));
            }
        }
    }
}

準備ができたので、最後に利用するコードを記述します。今回はリフレクションを使用するオーバーロードを使ってみました。

using System;
using System.Reactive.Linq;
 
namespace Sample18_FromEventPattern
{
    class Program
    {
        static void Main()
        {
            var notifier = new EventNotifier();
            Observable.FromEventPattern<KeyPressEventArgs>(notifier, "KeyPress")
            .Subscribe(value => Console.WriteLine("OnNext({0})", value.EventArgs.Info.Key));
            notifier.ObserveKeyInput();
        }
    }
}
 
//----- 結果(例)
/*
OnNext(LeftArrow)
OnNext(UpArrow)
OnNext(RightArrow)
OnNext(DownArrow)
*/

リフレクションを利用すると記述が非常に簡潔になりますが、そこはやはりリフレクションなので、イベント名を間違えたりしてもコンパイルエラーにならないという問題点があります。ですので、面倒がらずにリフレクションを利用しない書き方をするのが安全で良いと思います。

OnNextメソッドの引数として渡されるデータはSystem.Reactive名前空間にあるEventPattern<T>クラスで、イベント引数であるSenderとEventArgsをひとまとめにして渡してくれます。

参考記事

今回の内容は、@neueccさんのblog記事にほぼほぼすべてに載っています。多くのオーバーロードに対する解説がされているので、非常に参考になります。また、@okazukiさんのblogではこれらを応用してドラッグ処理をしています。ぜひ、ご一読ください。

次回予告

今回はイベントのIObservable<T>に変換する方法について見てきました。@okazukiさんのblog記事にもあるように、ドラッグ処理をひとつのシーケンスとして扱えたりするのは、もうある種の革命です。Rx万歳!次回は非同期処理をIObservable<T>シーケンスに変換する方法について触れてみたいと思います。