連載:Reactive Extensions(Rx)入門

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

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

合成のためのメソッド

 Rxの代表的な合成のためのメソッドを紹介していこう。

SelectManyメソッド

 SelectManyメソッドはRxでは最もよく使われるメソッドの1つだ。例えばマウス・ダウンをきっかけにしてマウス・ムーブに差し替えてしまう、など、後へ流すシーケンス自体の差し替えを可能にする(次の図はそのイメージ)。また、1つ目の非同期の結果から2つ目の非同期を起動させる、といったように、特にRxで非同期プログラミングをする際にも重宝される。

SelectManyメソッドのイメージ図
Aのシーケンスでの値によって、後続のBのシーケンスの内容が差し替えられる。このイメージ図では、Aのシーケンスの1つ目の値によってBのシーケンスで1つの値に差し替えられ、Aの2つ目の値によってBで2つの値に差し替えられ、Aの3つ目の値によってBで3つの値に差し替えられている。

 メソッド構文でのSelectManyメソッドはクエリ構文の多重「from」に書き換えることができる。もし幾つものSelectManyメソッドを使うことがあったら、クエリ構文を使用することを検討すると、きれいなコードが書けるだろう。次のコードは、同じ内容をメソッド構文とクエリ構文で記述した例である。

// 別のObservableに差し替えるような挙動をする
// 結果:10, 10, 11, 10, 11, 12

Observable.Range(1, 3)
  .SelectMany(x => Observable.Range(10, x))
  .Subscribe(Console.WriteLine);

// クエリ構文を使うと、見栄えよく仕上げられることも
// { x = 1, y = 10 }
// { x = 2, y = 10 }
// { x = 2, y = 11 }
// { x = 3, y = 10 }
// { x = 3, y = 11 }
// { x = 3, y = 12 }

var query = from x in Observable.Range(1, 3)
            from y in Observable.Range(10, x)
            select new { x, y };
query.Subscribe(Console.WriteLine);
' 別のObservableに差し替えるような挙動をする
' 結果:10, 10, 11, 10, 11, 12
Observable.Range(1, 3).
  SelectMany(Function(x) Observable.Range(10, x)).
  Subscribe(AddressOf Console.WriteLine)

' クエリ構文を使うと、見栄えよく仕上げられることも
' { x = 1, y = 10 }
' { x = 2, y = 10 }
' { x = 2, y = 11 }
' { x = 3, y = 10 }
' { x = 3, y = 11 }
' { x = 3, y = 12 }
Dim query = From x In Observable.Range(1, 3)
            From y In Observable.Range(10, x)
            Select New With {.x = x, .y = y}
query.Subscribe(AddressOf Console.WriteLine)
SelectManyメソッドの例(上:C#、下:VB)
「Range(1, 3)」(以降、A)シーケンスの1つ目の値(x=1)によって後続の「Range(10, x)」(以降、B)シーケンスで「10」という1つの値に差し替えられ、Aの2つ目の値(x=2)によってBで「10」「11」という2つの値に差し替えられ、Aの3つ目の値(x=3)によってBで「10」「11」「12」という3つの値に差し替えられている。

 非同期プログラミングでの活用事例については、次回に詳しく解説する。

Concatメソッド

 Concatメソッドは2つのシーケンスを連結する。その際、1つ目のシーケンスが終了するまでは、仮に2つ目のシーケンスで値が発行されていたとしても値を無視する。次の図はそのイメージだ。

Concatメソッドのイメージ図(Aの末尾の四角はOnCompletedメソッドを表す)
Aのシーケンスが終了したら、Bのシーケンスに連結する。

 以下に単純なConcatメソッドの例を示す。

// 実行結果:1, 2, 3, -1, -1, -1
Observable.Range(1, 3)
  .Concat(Observable.Repeat(-1, 3))
  .Subscribe(Console.WriteLine);
' 実行結果:1, 2, 3, -1, -1, -1
Observable.Range(1, 3).
  Concat(Observable.Repeat(-1, 3)).
  Subscribe(AddressOf Console.WriteLine)
Concatメソッドの例(上:C#、下:VB)
「Range(1, 3)」シーケンスが終了したら、「Repeat(-1, 3)」シーケンスに連結する。

 2つのシーケンスだけではなく、複数(IEnumerable<IObservable<T>>オブジェクト)の連結にも対応しており、強く実行順序を規定したい場合に活用可能だ。

Mergeメソッド

 すべての値を、発行されると即座にそのまま流す(次の図はそのイメージ)。2つのみの連結ではなく、複数の連結にも対応している。幾つものコントロールに対して共通の処理をしたい、などのときに便利に使える。

Mergeメソッドのイメージ図
それぞれのシーケンスで発行されたすべての値を、即座にそのまま流す。

 次のコードはMergeメソッドの利用例。

// Windowsフォームで4つのTextBoxコントロール全てに
// 「DragDropEffects.All」を設定する

new[] { textBox1, textBox2, textBox3, textBox4 }
  .Select(x => Observable.FromEventPattern<DragEventArgs>(
                                                  x, "DragEnter"))
  .Merge()
  .Subscribe(x => x.EventArgs.Effect = DragDropEffects.All);

// 上のMergeメソッドは以下のコードの変形パターン。
// IEnumerable<IObservable<T>>オブジェクトでまとめてMergeすると
// コードがきれいに仕上がる

Observable.Merge(
  Observable.FromEventPattern<DragEventArgs>(textBox1, "DragEnter"),
  Observable.FromEventPattern<DragEventArgs>(textBox2, "DragEnter"),
  Observable.FromEventPattern<DragEventArgs>(textBox3, "DragEnter"),
  Observable.FromEventPattern<DragEventArgs>(textBox4, "DragEnter")
);
' Windowsフォームで4つのTextBoxコントロール全てに
' 「DragDropEffects.All」を設定する

Dim merge = {TextBox1, TextBox2, TextBox3, TextBox4}.
  Select(Function(x) Observable.FromEventPattern(Of DragEventArgs)(
                                                  x, "DragEnter")).
  Merge().
  Subscribe(Sub(x) x.EventArgs.Effect = DragDropEffects.All)

' 上のMergeメソッドは以下のコードの変形パターン。
' IEnumerable(Of IObservable(Of T))オブジェクトでまとめてMergeすると
' コードがきれいに仕上がる
Observable.Merge(
  Observable.FromEventPattern(Of DragEventArgs)(
                                              TextBox1, "DragEnter"),
  Observable.FromEventPattern(Of DragEventArgs)(
                                              TextBox2, "DragEnter"),
  Observable.FromEventPattern(Of DragEventArgs)(
                                              TextBox3, "DragEnter"),
  Observable.FromEventPattern(Of DragEventArgs)(
                                              TextBox4, "DragEnter")
)
Mergeメソッドの例(上:C#、下:VB)

 IEnumerable<IObservable<T>>オブジェクトからのMergeというのは一見奇妙に見えるかもしれないが、LINQ to Objectsと組み合わせてIEnumerable<IObservable<T>>オブジェクトをうまく作り上げることで、コードの重複をうまく消すことができる。

Zipメソッド

 ZipメソッドはAとB、2つの値がそろったときに値を流す。片方に値が偏ったときは、2つがそろうまでキューに古い値がためられていく。次の図はそのイメージである。

Zipメソッドのイメージ図
AとBのシーケンスで、2つの値がそろったときに値を流す。

 以下の例では、Zipメソッドの挙動をIntervalメソッド(=指定時間間隔で値を発行する)とTimestampメソッド(=通過した際の時間と値をくっつける)で確認している。

// 実行結果:
// { x = 0@2011/12/20 7:37:15 +09:00, y = 0@2011/12/20 7:37:17 +09:00, now = 2011/12/20 7:37:17 +09:00 }
// { x = 1@2011/12/20 7:37:16 +09:00, y = 1@2011/12/20 7:37:20 +09:00, now = 2011/12/20 7:37:20 +09:00 }
// { x = 2@2011/12/20 7:37:17 +09:00, y = 2@2011/12/20 7:37:23 +09:00, now = 2011/12/20 7:37:23 +09:00 }
// { x = 3@2011/12/20 7:37:18 +09:00, y = 3@2011/12/20 7:37:26 +09:00, now = 2011/12/20 7:37:26 +09:00 }
// { x = 4@2011/12/20 7:37:19 +09:00, y = 4@2011/12/20 7:37:29 +09:00, now = 2011/12/20 7:37:29 +09:00 }
// { x = 5@2011/12/20 7:37:20 +09:00, y = 5@2011/12/20 7:37:32 +09:00, now = 2011/12/20 7:37:32 +09:00 }
// { x = 6@2011/12/20 7:37:21 +09:00, y = 6@2011/12/20 7:37:35 +09:00, now = 2011/12/20 7:37:35 +09:00 }

Observable.Interval(TimeSpan.FromSeconds(1))
  .Timestamp()
  .Zip(Observable.Interval(TimeSpan.FromSeconds(3)).Timestamp(), (x, y) => new { x, y, now = DateTimeOffset.Now })
  .Subscribe(Console.WriteLine);
' 実行結果:
' { x = 0@2011/12/20 7:37:15 +09:00, y = 0@2011/12/20 7:37:17 +09:00, now = 2011/12/20 7:37:17 +09:00 }
' { x = 1@2011/12/20 7:37:16 +09:00, y = 1@2011/12/20 7:37:20 +09:00, now = 2011/12/20 7:37:20 +09:00 }
' { x = 2@2011/12/20 7:37:17 +09:00, y = 2@2011/12/20 7:37:23 +09:00, now = 2011/12/20 7:37:23 +09:00 }
' { x = 3@2011/12/20 7:37:18 +09:00, y = 3@2011/12/20 7:37:26 +09:00, now = 2011/12/20 7:37:26 +09:00 }
' { x = 4@2011/12/20 7:37:19 +09:00, y = 4@2011/12/20 7:37:29 +09:00, now = 2011/12/20 7:37:29 +09:00 }
' { x = 5@2011/12/20 7:37:20 +09:00, y = 5@2011/12/20 7:37:32 +09:00, now = 2011/12/20 7:37:32 +09:00 }
' { x = 6@2011/12/20 7:37:21 +09:00, y = 6@2011/12/20 7:37:35 +09:00, now = 2011/12/20 7:37:35 +09:00 }

Observable.Interval(TimeSpan.FromSeconds(1)).
  Timestamp().
  Zip(Observable.Interval(TimeSpan.FromSeconds(3)).Timestamp(), Function(x, y) New With {.x = x, .y = y, .now = DateTimeOffset.Now}).
  Subscribe(AddressOf Console.WriteLine)
Zipメソッドの例(上:C#、下:VB)
1秒間隔で作成されるタイムスタンプ値(Aシーケンス)と、3秒間隔で作成されるタイムスタンプ値(Bシーケンス)の両方がそろったときに値をSubscribeメソッドに流す。

 この場合、xが1秒間隔、yが3秒間隔なので、xの方が多く値が発行されるが、yの間隔に合わされて値が流されている。また、出力結果を見るとxのタイムスタンプ値は1秒間隔で古いものから順に出力されており、多く発行されたxの値はキューに入っているのが確認できる。

CombineLatestメソッド

 Zipメソッドと似ているが、これは、どちらかの値が発行された際に、最も新しい値とそろえて値を流す(次の図はそのイメージ)。例えばどちらかのイベントが発行されるたびに、結果を再計算してほしい、といった場合に有効活用できる。

CombineLatestメソッドのイメージ図
AとBのシーケンスで、どちらかの値が発行された際に2つの値がそろっていれば値を流す。ただし、各シーケンスにおける最新の値がそろえられて流される。

 以下は、2つのチェックボックスを、チェックが切り替わるたびにチェック状態を確認し、両方がチェック状態ならば実行する、というものだ。

public static class ToggleButtonExtensions
{
  // WPF/Silverlight/WP7のToggleButtonコントロール(チェックボックスなど)で
  // チェック状態が変化したらIsCheckedプロパティの値を送るように合成

  public static IObservable<bool> IsCheckedAsObservable(this ToggleButton button)
  {
    var checkedAsObservable = Observable.FromEvent<RoutedEventHandler, RoutedEventArgs>(
      h => (sender, e) => h(e),
      h => button.Checked += h, h => button.Checked -= h);

    var uncheckedAsObservable = Observable.FromEvent<RoutedEventHandler, RoutedEventArgs>(
      h => (sender, e) => h(e),
      h => button.Unchecked += h, h => button.Unchecked -= h);

    return Observable.Merge(checkedAsObservable, uncheckedAsObservable).Select(_ => button.IsChecked.Value);
  }
}

// checkBox1とcheckBox2という2つのチェックボックスが
// 両方ともチェックされていたらメッセージボックスを表示
checkBox1.IsCheckedAsObservable()
  .CombineLatest(checkBox2.IsCheckedAsObservable(),
    (isChecked1, isChecked2) => new { isChecked1, isChecked2 })
  .Where(x => x.isChecked1 && x.isChecked2)
  .Subscribe(_ => MessageBox.Show("両方チェックされている"));
Module ToggleButtonExtensions
  ' WPF/Silverlight/WP7のToggleButtonコントロール(チェックボックスなど)で
  ' チェック状態が変化したらIsCheckedプロパティの値を送るように合成

  <System.Runtime.CompilerServices.Extension()>
  Function IsCheckedAsObservable(button As ToggleButton) As IObservable(Of Boolean)
    Dim checkedAsObservable = Observable.FromEvent(Of RoutedEventHandler, RoutedEventArgs)(
      Function(h) Sub(sender, e) h.Invoke(e),
      Sub(h) AddHandler button.Checked, h, Sub(h) RemoveHandler button.Checked, h)

    Dim uncheckedAsObservable = Observable.FromEvent(Of RoutedEventHandler, RoutedEventArgs)(
      Function(h) Sub(sender, e) h.Invoke(e),
      Sub(h) AddHandler button.Unchecked, h, Sub(h) RemoveHandler button.Unchecked, h)

    Return Observable.Merge(checkedAsObservable, uncheckedAsObservable).Select(Function(x) button.IsChecked.Value)
  End Function
End Module

' checkBox1とcheckBox2という2つのチェックボックスが
' 両方ともチェックされていたらメッセージボックスを表示
CheckBox1.IsCheckedAsObservable().
  CombineLatest(CheckBox2.IsCheckedAsObservable(),
    Function(x, y) New With {.isChecked1 = x, .isChecked2 = y}).
  Where(Function(x) x.isChecked1 AndAlso x.isChecked2).
  Subscribe(Sub(x) MessageBox.Show("両方チェックされている"))
CombineLatestメソッドの例(上:C#、下:VB)
「checkBox1.IsCheckedAsObservable()」と「checkBox2.IsCheckedAsObservable()」のシーケンスで、どちらかのチェック状態が変更された際に、2つの値(最新の値)がチェック状態でそろっていれば、Subscribeメソッドに値を流す。

 しかし、この例は、こんな回りくどいことはせずに、XAMLコード上で、CheckBoxコントロールのIsCheckedプロパティにデータバインドすべきだろう。

 XAMLファミリでは、データ・バインディングの積極活用によりコントロールへのID付与やイベントの利用があまりされないため、GUIプログラミングでRxを活用可能な範囲はあまり広いとはいえない。しかし、RxをXAMLファミリで有効活用するための、サードパーティ製のライブラリが幾つか出ている。1つは「ReactiveUI」で、MVVMパターンにおけるViewModelの構築をRxで支援するというものだ。もう1つは筆者が開発している「ReactiveProperty」で、プロパティ自身がバインド可能かつIObservable<T>であるReactivePropertyや、宣言的にCanExecuteの条件を記述可能なReactiveCommandによって、Viewと切り離したGUIプログラミングを支援する。

Scanメソッド

 最後に、これは連結ではなく集計ではあるが、Scanメソッドを紹介しよう。Scanメソッドは1つ前の「結果」と現在の「値」を合成して値を流す。自分自身の1つ前の結果値を見ることが可能なので、差分を計算したりなどに活用できる。次の図はそのイメージ。

Scanメソッドのイメージ図
Aのシーケンスで、1つ前の「結果」(=中央にある茶色の横線)と現在の「値」(=上にある青色の横線)を合成して値を流す。

 Scanメソッドの挙動は、LINQ to ObjectsでのAggregateメソッドの計算結果の途中を全て列挙していることと等しい。

// 1, 3(=1+2), 6(=3+3), 10(=6+4), 15(=10+5)
Observable.Range(1, 5)
  .Scan((x, y) => x + y)
  .Subscribe(Console.WriteLine);
' 1, 3(=1+2), 6(=3+3), 10(=6+4), 15(=10+5)
Observable.Range(1, 5).
  Scan(Function(x, y) x + y).
  Subscribe(AddressOf Console.WriteLine)
Scanメソッドの例(上:C#、下:VB)
「Range(1, 5)」のシーケンスで、1つ目の現在値が「1」で1つ前の(x+yの)結果値がないので結果値は「1」、2つ目の現在値が「2」で1つ前の結果値が「1」なので結果値は「3」、3つ目の現在値が「3」で1つ前の結果値が「3」なので結果値は「6」、という結果値がSubscribeメソッドに流される。

 なお、LINQ to ObjectsでのAggregateメソッドと同じく、シード値を与えることも可能だ。

 今回はReactive Extensions(Rx)の基本的な流れと、イベントでの利用の仕方、合成のためのメソッド群を説明した。次回は非同期での使い方や、時間に関するメソッド群を解説していく予定だ。 End of Article

 

 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 記事ランキング

本日 月間