連載:Reactive Extensions(Rx)入門

第2回 イベント・プログラミングとRx

河合 宜文
2012/01/06
Page1 Page2 Page3

イベントとは何か? イベントをRxで扱うことの利点

 いよいよ、Rxならではの部分、イベントのObservable変換について見ていこう。しかしその前に、.NETにおいてイベントとはどこで使われているかを一度考えよう。

 最も代表的なのはGUIのイベントだろう。ボタンをクリックしたりマウスがムーブしたり、それらは全てイベントを介して処理されている。Windows Phone 7(以降、WP7)などタッチ・デバイスであればジェスチャ入力などもイベントだし、センサーからの入力の類もイベントである。また、タッチ機能やセンサーはデスクトップ・アプリケーションでも決して無縁ではない。タブレットPCはWP7と同様のタッチ機能やセンサーを備えるし、昨年登場し脚光を浴びたMicrosoft Kinectはセンサーの固まりである。

 次に、通知目的で使われるものがある。例えばINotifyPropertyChangedインターフェイスはプロパティ変更通知としてイベントが使用されている。また、FileSystemWatcherクラスはファイルやディレクトリの変更を監視し、イベントで通知する。ほかには、タイマーもイベントだし、非同期(WebClientクラスのDownloadStringAsyncメソッドなど)もイベントだ。

GUIのイベント:合成

 それらイベントをRxで扱えるようになると何がうれしいのか、というと、まず、「合成」が可能になる。例えばGUIイベントでマウス・ダウン/ムーブ/アップを組み合わせて何かをしたい場合に、従来では外部にフラグを立てて、そのフラグをやりくりして状態管理をする必要があった。これでは非常に込み入ったコードになってしまうし、それらの組み合わせで使う以外にダウン/ムーブ/アップのイベントで何か別の処理しようとすると、非常に破綻しやすい。

 しかし、イベントをRxで扱えば外部フラグを用意することなく、ダウン/ムーブ/アップを合成して1つの新しいイベントと見なせる。すると、フラグが不要なので使い回しが利くし、ダウン/ムーブ/アップで別の処理をしようとしても、コードが混じることがないので簡単に記述できる。また、1つのイベントと見ることができることは、分かりやすさにもつながる。

 次のコードは、ダウン/ムーブ/アップという3つのマウス・イベントを合成して、1つのドラッグ・イベントとして処理する場合のコード例である。

// WindowsフォームのFormクラスでドラッグ(=マウスの左ボタンを
// 押しながら移動している間)の座標イベントを生成

var drag = from down in this.MouseDownAsObservable()
           from move in this.MouseMoveAsObservable().TakeUntil(
                                         this.MouseUpAsObservable())
           select new { move.X, move.Y };
' WindowsフォームのFormクラスでドラッグ(=マウスの左ボタンを
' 押しながら移動している間)の座標イベントを生成
Dim drag = From down In Me.MouseDownAsObservable()
           From move In Me.MouseMoveAsObservable().TakeUntil(
                                           Me.MouseUpAsObservable())
           Select New With {move.X, move.Y}
3つのイベントを合成して1つのドラッグ・イベントを生成するコード例(上:C#、下:VB)
MouseDownAsObservable/MouseMoveAsObservable/MouseUpAsObservableメソッドは、ObservableクラスのFromEvent静的メソッドでイベントをラップした独自の拡張メソッドだ。詳細は後で述べる。

 また、通常のイベントはOnNextメソッドのみのIObservable<T>オブジェクトと考えることもできる。つまり、イベントがRxである場合、完了(=OnCompletedメソッド)とエラー(=OnErrorメソッド)の通達が可能であり、通常のイベントよりも表現力が高いといえる。

タイマー/通知イベント:フィルタリング

 タイマーでも考えてみよう。例えばポーリング、一定時間ごとに値を監視する場合はどうだろう。Rxでタイマーを扱うと1本のシーケンスになる。また、Selectメソッドで何かに変換することができる。これらを組み合わせると、一定時間ごとに監視対象の値が流れてくる一本のシーケンスという、非常に扱いやすい形状に変わる。さらにフィルタリングも容易なので、値が変化したときのみ値が流れてくる、といったものを簡単に作り上げられる。

 次のコードは、タイマーで発生する一定時間ごとの値により次の処理実行をフィルタリングする場合のコード例である。

// ある対象の値(=watchTarget.Valueプロパティ値)を1秒ごとに監視
var polling =
  Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(1))
  .Select(_ => watchTarget.Value)
  .DistinctUntilChanged(); // 値が変化したときのみ流す
' ある対象の値(=watchTarget.Valueプロパティ値)を1秒ごとに監視
Dim polling =
  Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(1)).
  Select(Function(x) watchTarget.Value).
  DistinctUntilChanged() ' 値が変化したときのみ流す
Rxでのポーリングによる値監視の例(上:C#、下:VB)

 フィルタリングが容易という性質によって、ジェスチャやセンサーなどのように、大量の通知がやってくるがそれら全てが必要ではなく、取捨・整形しなければならないタイプのイベントの取り扱いを簡略化できる。また、Rxにおけるフィルタリングはただ単にif文で除去するだけにとどまらず、時間を用いたフィルタリングも可能になっている(これは、Rxのシーケンスの横軸が時間であるため、フィルタリングだけじゃなく合成などにおいても時間を使った操作ができるということだ)。次のコードは、時間によるフィルタリングの例である。

// FileSystemWatcherクラスのChangedイベントは
// 一度の変更で複数のイベントが発行される

var watcher =
  new FileSystemWatcher("C:\\", "test.txt")
    { EnableRaisingEvents = true };

// 「1秒以内に連続して発生してきたイベントは無視して、
// 通すのは最後の1つだけにする」
// という時間を使ったフィルタ処理を行うことで、
// 扱いやすい形に変換できる
var changed =
  Observable.FromEventPattern<FileSystemEventArgs>(
                                                 watcher, "Changed")
  .Throttle(TimeSpan.FromSeconds(1)); // Throttleメソッドは指定時間、値の通過がなかった場合に、最後の1つを通す
' FileSystemWatcherクラスのChangedイベントは
' 一度の変更で複数のイベントが発行される

Dim watcher =
  New FileSystemWatcher("C:\\", "test.txt") With
    {.EnableRaisingEvents = True}

' 「1秒以内に連続して発生してきたイベントは無視して、
' 通すのは最後の1つだけにする」
' という時間を使ったフィルタ処理を行うことで、
' 扱いやすい形に変換できる
Dim changed =
  Observable.FromEventPattern(Of FileSystemEventArgs)(
                                                 watcher, "Changed").
  Throttle(TimeSpan.FromSeconds(1)) ' Throttleメソッドは指定時間、値の通過がなかった場合に、最後の1つを通す
Rxでの時間を使ったフィルタリングの例(上:C#、下:VB)

非同期イベント

 最後の非同期イベントだが、これは次回以降でほかの非同期パターンとともに解説する。

ユニット・テストとの親和性

 また、時間を自由に扱えるというRxの特徴は、従来では困難だったイベントのユニット・テスト(=単体テスト)を圧倒的に記述しやすくする。例えば「3分30秒に『10』という値が発火、その後の4分0秒に『20』という値が発火」というような、時間と値をセットにしたダミーのイベントを生成することができる。それらのテスト用の便利なメソッドはMicrosoft.Reactive.Testingアセンブリ(NuGetでは「Rx-Testing」)にまとめられている。Rxにおけるユニット・テストも、詳しくは次回以降に解説する。

FromEventメソッド&FromEventPatternメソッド

 イベントをRxで扱うためのIObservable<T>オブジェクトへと変換するにはFromEventメソッド、もしくはFromEventPatternメソッドを用いる。FromEventメソッドはAction<T>デリゲートからの変換を可能にし、シーケンスの要素の型は「T」となる。FromEventPatternメソッドはEventHandlerデリゲートからの変換を可能にし、シーケンスの要素の型は「EventPattern<TEventArgs>」という、Object型の引数「sender」とTEventArgs型の引数「e」をラップした型になっている。

 この部分はWP7版RxであるMicrosoft.Phone.Reactiveアセンブリがリリースされた後に、Data Developer Center版で大きく変更が加えられたため、両者を比較すると非常に紛らわしいことになっているので注意が必要だ。具体的には、WP7版のFromEventメソッドがData Developer Center版のFromEventPatternメソッドに相当し(シーケンスの要素の型が「IEvent<TEventArgs>」であるが、これは「EventPattern<TEventArgs>」とほぼ同じ)、Data Developer Center版のFromEventメソッドに相当するものはWP7版にはない。

イベントのRx変換:イベント名の文字列指定

 それではまず、FromEventPatternメソッドから見てみよう。以下のコード例ではData Developer Center版を扱うが、WP7版ではFromEventメソッドに置き換えてほしい。

// WPF/Silverlight/WP7のButtonコントロール(変数名「button1」)のClickイベントをRx化
Observable.FromEventPattern<RoutedEventArgs>(button1, "Click");
' WPF/Silverlight/WP7のButtonコントロール(変数名「button1」)のClickイベントをRx化
Observable.FromEventPattern(Of RoutedEventArgs)(Button1, "Click")
イベント名の文字列指定での変換(上:C#、下:VB)

 イベントをRx変換する最も簡単な方法が、FromEventPatternメソッドの型引数にEventArgsの型(この例では「RoutedEventArgs」)を指定し、第1パラメータにコントロールを渡し、第2パラメータにイベント名(この例では「Click」)を文字列で与える方法だ。これは文字列で与えていることから分かるように、内部ではリフレクションを使用してイベントをアタッチしている。後述する非リフレクション・バージョンに比べると記述が平易であるものの、パフォーマンス面では劣る。とはいえ、イベントのアタッチなど頻繁に繰り返すものではないため、パフォーマンスにそこまで深刻な影響はなく、簡易さから、これを採用するのはアリだろう。

イベントのRx変換(非リフレクション・バージョン):イベント・ハンドラ指定

 リフレクションを使わないでイベントをRx変換する場合は、次のように記述する。

// 第1引数にデリゲートの変換子(これは h => h.Invokeが定型句)
// 第2引数でアタッチ、第3引数にデタッチを登録する
Observable.FromEventPattern<RoutedEventHandler, RoutedEventArgs>(
  h => h.Invoke,
  h => button1.Click += h, h => button1.Click -= h);
' 第1引数にデリゲートの変換子
' 第2引数でアタッチ、第3引数にデタッチを登録する
Observable.FromEventPattern(Of RoutedEventHandler, RoutedEventArgs)(
  Function(h) AddressOf h.Invoke,
  Sub(h) AddHandler Button1.Click, h, Sub(h) RemoveHandler Button1.Click, h)
イベント・ハンドラ指定での変換(上:C#、下:VB)

 記述量はかなり増加するが、パフォーマンスのうえではベストな選択肢となる。FromEventPatternメソッドにはほかにも幾つかのオーバーロードがあるが、この2つだけを押さえておけば問題ない。

イベントをRx変換するコードをすっきりさせる方法

 イベント・ハンドラ指定はもとより、文字列指定であっても記述量がかなり多いので、実際のアプリケーション・コードに頻繁に出てくると可読性を落とすだろう。そこで、このイベントのRx変換部分は拡張メソッドで分離してしまうことをお勧めする。

public static class ButtonBaseExtensions
{
  // このような拡張メソッドを定義することで
  public static IObservable<RoutedEventArgs> ClickAsObservable(this ButtonBase button)
  {
    return Observable.FromEventPattern<RoutedEventHandler, RoutedEventArgs>(
        h => h.Invoke,
        h => button.Click += h, h => button.Click -= h)
      .Select(x => x.EventArgs);
  }
}

// アプリケーション・コード上ではすっきりと記述できるようになる
button1.ClickAsObservable().Subscribe(_ => MessageBox.Show("Clicked!"));
Module ButtonBaseExtensions
  ' このような拡張メソッドを定義することで
  <System.Runtime.CompilerServices.Extension()>
  Public Function ClickAsObservable(button As ButtonBase) As IObservable(Of RoutedEventArgs)
    Return Observable.FromEventPattern(Of RoutedEventHandler, RoutedEventArgs)(
        Function(h) AddressOf h.Invoke,
        Sub(h) AddHandler button.Click, h, Sub(h) RemoveHandler button.Click, h).
      Select(Function(x) x.EventArgs)
  End Function
End Module

' アプリケーション・コード上ではすっきりと記述できるようになる
Button1.ClickAsObservable().Subscribe(Sub(x) MessageBox.Show("Clicked"))
イベントのRx変換を拡張メソッドに分離する(上:C#、下:VB)

 上記の例ではEventPattern<TEventArgs>オブジェクトからSelectメソッドでEventArgsオブジェクトのみにしている。これは、Rxでのイベントへの登録のやり方だと、Object型のsender引数は拡張メソッドの呼び出し元自身であるので必要なく、EventArgs型のe引数のみの方が、利便性が高いと判断したからであるが、sender引数を残しておくかは好みで決めるとよいだろう。

 (Data Developer Center版の)FromEventメソッドもFromEventPatternメソッドと同様だが、こちらは対象のイベントの型が「Action<T>」となる。.NET Frameworkの中で、標準でそのように定義されているイベントはないため、直接使うことは少ないだろうが、ユーザー定義でAction<T>デリゲート型のイベントを作っている場合にはこれが使える。

 また、デリゲートの変換子を工夫することで、上のコード例のようなSelectメソッドでsender引数を省く場合に、最初からsender引数を省いた変換が可能だ。例えば次のコードのようになる。

public static class ButtonBaseExtensions
{
  // FromEventメソッドを使うと、EventPatternオブジェクト生成と
  // Selectメソッドによるオーバーヘッドを省ける
  public static IObservable<RoutedEventArgs> ClickAsObservable(this ButtonBase button)
  {
    return Observable.FromEvent<RoutedEventHandler, RoutedEventArgs>(
      h => (sender, e) => h(e),
      h => button.Click += h, h => button.Click -= h);
  }
}
Module ButtonBaseExtensions
  ' FromEventメソッドを使うと、EventPatternオブジェクト生成と
  ' Selectメソッドによるオーバーヘッドを省ける
  <System.Runtime.CompilerServices.Extension()>
  Public Function ClickAsObservable(button As ButtonBase) As IObservable(Of RoutedEventArgs)
    Return Observable.FromEvent(Of RoutedEventHandler, RoutedEventArgs)(
      Function(h) Sub(sender, e) h.Invoke(e),
      Sub(h) AddHandler button.Click, h, Sub(h) RemoveHandler button.Click, h)
  End Function
End Module
FromEventメソッドでsenderを省いた変換をする(上:C#、下:VB)

 もしsender引数が必要な場合は、Selectメソッドで別途くっつければよいだろう。例えば「button1.ClickAsObservable().Select(ev => new { Sender = button1, EventArgs = ev })」のように。

 最後に次のページでは、Rxの代表的な合成のためのメソッドを紹介する。


 INDEX
  [連載]Reactive Extensions(Rx)入門
  第2回 イベント・プログラミングとRx
    1.基本的な記述方法/IObserver<T>オブジェクトの詳細/“Dispose”の必要性/Observableオブジェクトの生成子
  2.イベントとは何か? イベントをRxで扱うことの利点/FromEventメソッド&FromEventPatternメソッド
    3.合成のためのメソッド

インデックス・ページヘ  「連載:Reactive Extensions(Rx)入門」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間