Reactive extensions入門v0.1
Upcoming SlideShare
Loading in...5
×
 

Reactive extensions入門v0.1

on

  • 4,735 views

 

Statistics

Views

Total Views
4,735
Views on SlideShare
2,299
Embed Views
2,436

Actions

Likes
5
Downloads
92
Comments
0

6 Embeds 2,436

http://d.hatena.ne.jp 1655
http://okazuki.hatenablog.com 765
https://twitter.com 8
http://webcache.googleusercontent.com 6
http://translate.googleusercontent.com 1
http://cache.yahoofs.jp 1

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

Reactive extensions入門v0.1 Reactive extensions入門v0.1 Document Transcript

  • Reactive Extensions 入門 2012/03/05 0.1 版 okazuk
  • 改版履歴 版数 内容 日付 0.1 基本メソッドの説明を書いた初版作成 2012/03/05 ii
  • 内容1. はじめに.............................................................................................................................................................. 1 1.1. 前提環境 ............................................................................................................................................................ 1 1.2. 対象読者 ............................................................................................................................................................ 1 1.3. 謝辞 ................................................................................................................................................................... 12. REACTIVE EXTENSIONS とは ........................................................................................................................ 2 2.1. OBSERVER パターン .......................................................................................................................................... 2 2.2. REACTIVE EXTENSIONS で提供されている機能 ................................................................................................ 2 2.3. IOBSERVALBE<T>゗ンターフェースと IOBSERVER<T>゗ンターフェース ..................................................... 3 2.4. REACTIVE EXTENSIONS の機能を使った書き直し ............................................................................................ 83. IOBSERVABLE<T>のファクトリメソッド ...................................................................................................... 11 3.1. 基本的なフゔクトリメソッドの使用 ............................................................................................................... 11 3.2. 指定された値を返す IOBSERVABLE<T>を返すフゔクトリメソッド .............................................................. 12 3.2.1. Observable.Return メソッド ................................................................................................................. 12 3.2.2. Observable.Repeat メソッド.................................................................................................................. 12 3.2.3. Observable.Range メソッド ................................................................................................................... 13 3.2.4. Observable.Generate メソッド .............................................................................................................. 14 3.2.5. Observable.Defer メソッド .................................................................................................................... 15 3.2.6. Observable.Create メソッド .................................................................................................................. 17 3.2.7. Observable.Throw メソッド .................................................................................................................. 18 3.3. ここまでに紹介した IOBSERVABLE<T>の動作 ............................................................................................... 19 3.4. リソースの確保を伴う IOBSERVABLE<T> ...................................................................................................... 19 3.4.1. Observable.Uting メソッド .................................................................................................................... 20 3.5. 時間と共に値を発行する IOBSERVABLE<T> .................................................................................................. 21 3.5.1. Observable.Timer メソッド ................................................................................................................... 21 3.5.2. Observable.Interval メソッド ................................................................................................................ 23 3.5.3. Observable.Generate メソッド .............................................................................................................. 24 3.6. COLD な IOBSERVABLE<T>と HOT な IOBSERVABLE<T> .............................................................................. 25 3.6.1. Observable.FromEvent メソッド .......................................................................................................... 28 i
  • 3.6.2. Observable.Start メソッド..................................................................................................................... 30 3.6.3. Observable.ToAsync メソッド ............................................................................................................... 32 3.6.4. Observable.FromAsyncPattern メソッド ............................................................................................. 334. IOBSERVABLE の拡張メソッド ...................................................................................................................... 35 4.1. LINQ のメソッド ............................................................................................................................................ 35 4.2. 単一の値を取得するメソッド ......................................................................................................................... 38 4.2.1. First メソッドと Last メソッド ............................................................................................................. 38 4.2.2. FirstOrDefault メソッドと LastOrDefault メソッド ........................................................................... 41 4.2.3. ElementAt メソッド ............................................................................................................................... 42 4.2.4. ElementAtOrDefault メソッド .............................................................................................................. 45 4.2.5. Single メソッド ....................................................................................................................................... 46 4.2.6. SingleOrDefault メソッド...................................................................................................................... 50 4.3. 値と飛ばす、値を拾うメソッド ...................................................................................................................... 52 4.3.1. Skip と Take メソッド ............................................................................................................................ 52 4.3.2. Repeat メソッドとの組み合わせ ............................................................................................................ 53 4.3.3. SkipWhile と TakeWhile メソッド ........................................................................................................ 54 4.3.4. SkipUntil と TakeUntil メソッド .......................................................................................................... 55 4.3.5. SkipUntil と TakeUntil を使ったドラッグの処理 ................................................................................. 56 4.3.6. SkipLast と TakeLast メソッド ............................................................................................................. 61 4.4. DO メソッド .................................................................................................................................................... 62 4.4.1. Do メソッド使用時の注意点 ................................................................................................................... 64 4.5. エラー処理関連のメソッド ............................................................................................................................. 64 4.5.1. Catch メソッド ....................................................................................................................................... 64 4.5.2. Finally メソッド ..................................................................................................................................... 67 4.5.3. OnErrorResumeNext メソッド ............................................................................................................. 70 4.5.4. Retry メソッド ........................................................................................................................................ 72 4.6. IOBSERVABLE<T>の値を収集するメソッド ................................................................................................... 75 4.6.1. ToArray メソッド ................................................................................................................................... 75 4.6.2. ToDictionary メソッド ........................................................................................................................... 76 4.6.3. ToList メソッド ...................................................................................................................................... 77 4.6.4. ToLookup メソッド ................................................................................................................................ 78 ii
  • 4.6.5. Max メソッドと Min メソッドと Average メソッド ............................................................................. 79 4.6.6. MaxBy メソッドと MinBy メソッド ...................................................................................................... 82 4.6.7. Count メソッドと LongCount メソッド ................................................................................................ 83 4.6.8. Any メソッド .......................................................................................................................................... 84 4.6.9. All メソッド ............................................................................................................................................ 85 4.6.10. Aggregate メソッド ................................................................................................................................ 86 4.6.11. Scan メソッド ......................................................................................................................................... 91 4.6.12. GroupBy メソッド .................................................................................................................................. 93 4.6.13. GroupByUntil メソッド ......................................................................................................................... 954.7. IOBSERVABLE<T>から IENUMERABLE<T>への変換メソッド ....................................................................... 99 4.7.1. ToEnumerable メソッド ........................................................................................................................ 99 4.7.2. Latest メソッド .................................................................................................................................... 101 4.7.3. MostRecent メソッド ........................................................................................................................... 104 4.7.4. Next メソッド ....................................................................................................................................... 1064.8. TOEVENT メソッド........................................................................................................................................ 1084.9. 重複を排除するメソッド ............................................................................................................................... 109 4.9.1. Distinct メソッド .................................................................................................................................. 109 4.9.2. Distinct メソッドのオーバーロード ..................................................................................................... 110 4.9.3. DistinctUntilChanged メソッド .......................................................................................................... 1134.10. BUFFER メソッドと WINDOW メソッド ........................................................................................................ 114 4.10.1. 数でまとめる Buffer メソッドのオーバーロード ................................................................................. 114 4.10.2. 時間でまとめる Buffer メソッドのオーバーロード ............................................................................. 118 4.10.3. 任意のタイミングで値をまとめる Buffer メソッドのオーバーロード ................................................ 121 4.10.4. 時間と数でまとめる Buffer メソッドのオーバーロード ...................................................................... 125 4.10.5. Window メソッド.................................................................................................................................. 1274.11. 発行された値にたいして時間でフゖルタ・操作するメソッド ..................................................................... 130 4.11.1. Sample メソッド ................................................................................................................................... 130 4.11.2. Throttle メソッド ................................................................................................................................. 133 4.11.3. Delay メソッド ..................................................................................................................................... 135 4.11.4. Timeout メソッド ................................................................................................................................. 1364.12. 時間に関する情報を付与する拡張メソッド .................................................................................................. 138 iii
  • 4.12.1. Timestamp メソッド ............................................................................................................................ 139 4.12.2. TimeInterval メソッド ......................................................................................................................... 140 4.13. 型変換を行う拡張メソッド ........................................................................................................................... 141 4.13.1. Cast メソッド ....................................................................................................................................... 141 4.13.2. OfType メソッド ................................................................................................................................... 142 4.14. COLD から HOT へ変換する拡張メソッド ..................................................................................................... 143 4.14.1. Publish メソッド .................................................................................................................................. 143 4.14.2. RefCount メソッド ............................................................................................................................... 148 4.14.3. 引数を受け取る Publish メソッドのオーバーロード ........................................................................... 151 4.14.4. PublishLast メソッド ........................................................................................................................... 152 4.14.5. Replay メソッド.................................................................................................................................... 153 4.14.6. Multicast メソッド ............................................................................................................................... 1555. SUBJECT 系クラス ........................................................................................................................................ 159 5.1. SUBJECT<T>クラス ...................................................................................................................................... 159 5.2. BEHAVIORSUBJECT<T>クラス...................................................................................................................... 161 5.3. ASYNCSUBJECT<T>クラス ........................................................................................................................... 162 5.4. REPLAYSUBJECT<T>クラス .......................................................................................................................... 1646. IOBSERVABLE の合成 .................................................................................................................................. 165 6.1. MERGE メソッド ........................................................................................................................................... 165 6.2. SELECTMANY メソッド ................................................................................................................................. 169 6.3. SWITCH メソッド........................................................................................................................................... 172 6.4. CONCAT メソッド .......................................................................................................................................... 176 6.5. ZIP メソッド .................................................................................................................................................. 178 6.6. AMB メソッド ................................................................................................................................................ 179 6.7. COMBINELATEST メソッド ............................................................................................................................ 180 6.8. STARTWITH メソッド .................................................................................................................................... 182 6.9. JOIN メソッド ............................................................................................................................................... 183 6.10. GROUPJOIN メソッド .................................................................................................................................... 186 6.11. WHEN メソッド ............................................................................................................................................. 189 6.11.1. Plan<TResult>クラスの作成 ............................................................................................................... 189 iv
  • 6.11.2. When メソッドの使用例 ....................................................................................................................... 190 6.11.3. まとめ .................................................................................................................................................... 1937. SCHEDULER ................................................................................................................................................ 193 7.1. 実行場所の切り替え ...................................................................................................................................... 194 7.2. IOBSERVABLE<T>生成時の SCHEDULER の指定方法 ................................................................................... 195 7.2.1. デフォルトの Scheduler ....................................................................................................................... 197 7.3. SCHEDULER の切り替え ................................................................................................................................ 197 7.3.1. ObserveOn メソッド ............................................................................................................................ 197 7.3.2. ObserveOn の使用用途 ......................................................................................................................... 198 7.4. 時間を制御する SCHEDULER ......................................................................................................................... 199 7.4.1. HistoricalScheduler クラス ................................................................................................................. 1998. 応用編 ............................................................................................................................................................. 200 8.1. センサー監視 ................................................................................................................................................. 2009. 参考サイト ...................................................................................................................................................... 203 v
  • 1. はじめに ここでは、Reactive Extensions(下記リンク)の著者自身の理解を深めるために著者自身の Reactive Extensions の理解している内容を記載します。 Dev Lab Reactive Extensions ホームページ1.1. 前提環境 ここでは、下記の環境を前提に説明を行います。  Visual Studio 2010 SP1 Ultimate  .NET Framework 4  Reactive Extensions 1.0.* Visual Studio は、Visual C# 2010 SP1 Express Edition でも動作可能です。1.2. 対象読者 Reactive Extensions によるプログラミングに興味がある方。1.3. 謝辞 本ドキュメントのもとになった著者の Blog エントリの記事に対してコメント等で指摘していただ いた方々に感謝いたします。特に@neuecc さんには、サ゗トの情報や私の理解不足による誤った 記載など丁寧に対応していただきとても感謝しています。 1
  • 2. Reactive Extensions とは Reactive Extensions は、公式ページに下記のように説明があります。 The Reactive Extensions (Rx)... ...is a library to compose asynchronous and event-based programs using observable collections and LINQ-style query operators. 私の拙い英語力で和訳を行うと「Reactive Extensions は、監視可能なコレクションと LINQ スタ ゗ルのオペレーションを使用して非同期と゗ベントベースのプログラムを合成するラ゗ブラリで す。」となります。 個人的な解釈としては、Reactive Extensions とは何かしらの値を 0 回以上通知するもの(C#の event や非同期処理やタ゗マーなど etc…)を統一的なプログラミングモデルで扱えるようにした ものです。 そして、この統一的なプログラミングモデルを提供するための要となる゗ンターフェー スが System 名前空間の下にある IObservable<T>と IObserver<T>です。2.1. Observer パターン Observer と Observable という名前からもわかる通り、この゗ンターフェースはデザ゗ンパター ンの Observer パターンのための機構を提供します。IObserver<T>゗ンターフェースが、 IObservable<T>゗ンターフェースの発行する゗ベントを監視するという構造になります。 IObserver<T>゗ンターフェースには下記の 3 つのメソッドが定義されています。 1. void OnNext(T value)メソッド IObservable<T>から発生した通知を受け取って処理を行います 2. void OnError(Exception ex)メソッド IObservable<T>で発生した例外を受け取って処理を行います。 3. void OnCompleted()メソッド IObservable<T>からの通知が終了した時の処理を行います。 対になる IObservable<T>には下記の 1 つのメソッドが定義されています。 1. IDisposable Subscribe(IObserver<T> observer)メソッド 引数で受け取った Observer に゗ベントの通知を行うようにします。戻り値の IDisposable の Dispose メソッドを呼び出すと通知を取り消します。 Reactive Extensions は、この Observer パターンを土台にして構築されたラ゗ブラリになります。2.2. Reactive Extensions で提供されている機能 Reactive Extensions は、この IObservable<T>と IObserver<T>をベースに下記のような機能 を提供しています。 1. IObservable<T>のフゔクトリメソッド Reactive Extensions には IObservable<T>を返すフゔクトリメソッドが多数用意されてい ます。 .NET の標準の゗ベントから IObservable<T>を生成するメソッドや、 非同期呼び出し、 2
  • タ゗マー、シーケンス、特定のルールで生成される値の集合 etc…さまざまなものが提供され ています。 2. IObservable<T>の拡張メソッド IObservable<T>と IObserver<T>だけでは゗ベントの発行と購読の関係にしかなりません。 Reactive Extensions では、ここに LINQ スタ゗ルの拡張メソッドを提供することで IObservable<T>から発行された値をフゖルタリングしたり、 発行された値を元に別の処理を 行ったり、発行された値の変換を行ったりすることが出来ます。 IObserver<T>生成へのショートカット IObserver<T>を実装しなくても、ラムダ式から IObserver<T>を内部的に生成してくれるため 実際に Reactive Extensions を使用するときには IObserver<T>゗ンターフェースを実装するケ ースは、ほとんどありません。 3. 柔軟なスレッドの切り替え機能 IObservable<T>から発行された値に対する処理を何処のスレッドで実行するのか柔軟に切 り替える機能が提供されています。このためバックグラウンドで時間のかかる処理を行い、UI スレッドに切り替えて画面の更新を行うといった処理が簡単に行えるようになります。 以上が、Reactive Extensions で提供されている機能の全体像になります。次からは、Reactive Extensions の基本となる IObservable<T>゗ンターフェースと IObserver<T>゗ンターフェー スを見ていこうと思います。2.3. IObservalbe<T>インターフェースと IObserver<T>インターフェース ここでは、Reactive Extensions の機能の要となる IObservable<T>と IObserver<T>を実装し て、その動作を確認します。まずは、Observer パターンでの監視役となる IObserver<T>から実 装を行います。IObserver<T>゗ンターフェースは、先に示したように OnNext と OnError と OnCompleted の3つのメソッドからなります。この3種類のメソッド内に IObservable<T>か ら通知された値を受け取った時の処理を行います。ここでは、IObservable<T>から通知された 値を単純にコンソールに出力するものを作成します。 namespace IObservableIObserverImpl { using System; // 監視する人 class PrintObserver : IObserver<int> { // 監視対象から通知が来たときの処理 public void OnNext(int value) { Console.WriteLine("OnNext({0}) called.", value); } 3
  • // 完了通知が来たときの処理 public void OnCompleted() { Console.WriteLine("OnCompleted called."); } // エラー通知が来たときの処理 public void OnError(Exception error) { Console.WriteLine("OnError({0}) called.", error.Message); } } }単純に、OnNext と OnCompleted と OnError で標準出力に値を出力しています。次にIObservable<T>を実装したコードを示します。 namespace IObservableIObserverImpl { using System; using System.Collections.Generic; /// <summary> /// 監視されるクラス /// </summary> class NumberObservable : IObservable<int> { // 自分を監視してる人を管理するリスト private List<IObserver<int>> observers = new List<IObserver<int>>(); // 自分を監視してる人に通知を行う // 0 を渡したらエラー通知 public void Execute(int value) { if (value == 0) { foreach (var obs in observers) { obs.OnError(new Exception("value is 0")); 4
  • } // エラーが起きたので処理は終了 this.observers.Clear(); return; } foreach (var obs in observers) { obs.OnNext(value); }}// 完了通知public void Completed(){ foreach (var obs in observers) { obs.OnCompleted(); } // 完了したので監視してる人たちをクリゕ this.observers.Clear();}// 監視してる人を追加する。// 戻り値の IDisposable を Dispose すると監視から外れる。public IDisposable Subscribe(IObserver<int> observer){ this.observers.Add(observer); return new RemoveListDisposable(observers, observer);}// Dispose が呼ばれたら observer を監視対象から削除するprivate class RemoveListDisposable : IDisposable{ private List<IObserver<int>> observers = new List<IObserver<int>>(); private IObserver<int> observer; 5
  • public RemoveListDisposable(List<IObserver<int>> observers, IObserver<int> observer) { this.observers = observers; this.observer = observer; } public void Dispose() { if (this.observers == null) { return; } if (observers.IndexOf(observer) != -1) { this.observers.Remove(observer); } this.observers = null; this.observer = null; } } } }IObservable<T>の実装は IObserver<T>に比べて複雑になっています。これは、IObservable<T>゗ンターフェースが IObserver<T>を自分自身の監視役として登録するSubscribe メソッドしか提供していないため、その他の監視役の IObserver<T>の保持や、Subscribe メソッドの戻り値の IDosposable の Dispose を呼び出したときの監視役解除の処理を作りこんでいるためです。どちらも一般的な C#によるプログラミングの範囲の内容になるので詳細は割愛します。 最期に、この PrintObserver クラスと NumberObservable クラスを使ったサンプルプログラムを以下に示します。 namespace IObservableIObserverImpl { using System; class Program 6
  • { static void Main(string[] args) { // 監視される人を作成 var source = new NumberObservable(); // 監視役を2つ登録 var sbscriber1 = source.Subscribe(new PrintObserver()); var sbscriber2 = source.Subscribe(new PrintObserver()); // 監視される人の処理を実行 Console.WriteLine("## Execute(1)"); source.Execute(1); // 片方を監視する人から解雇 Console.WriteLine("## Dispose"); sbscriber2.Dispose(); // 再度処理を実行 Console.WriteLine("## Execute(2)"); source.Execute(2); // エラーを起こしてみる Console.WriteLine("## Execute(0)"); source.Execute(0); // 完了通知 // もう 1 つ監視役を追加して完了通知を行う var sbscriber3 = source.Subscribe(new PrintObserver()); Console.WriteLine("## Completed"); source.Completed(); } }} 上記のプログラムは、NumberObservable 型の変数 source に PrintObserver を 2 つ登録してい ます。その状態で Execute メソッドを呼んだり Dispose を読んだりエラーを起こしたりして出力 を確認しています。このプログラムを動かすと下記のような結果になります。 ## Execute(1) OnNext(1) called. OnNext(1) called. ## Dispose 7
  • ## Execute(2) OnNext(2) called. ## Execute(0) OnError(value is 0) called. ## Completed OnCompleted called. 最初の Execute では、PrintObserver を 2 つ登録しているので 2 回 OnNext が呼ばれていること が確認出来ます。 次に片方を Dispose した後では Execute を読んでも 1 回しか OnNext が呼ばれ ません。 Execute メソッドの引数に 0 を渡してエラーを起こした場合と処理を完了させたときも、 PrintObserver に処理が伝わっていることが確認できます。2.4. Reactive Extensions の機能を使った書き直し ここまで書いてきたプログラムは、Reactive Extensions の説明というよりは IObservable<T> ゗ンターフェースと IObserver<T>゗ンターフェースを実装して使用しただけの Observer パタ ーンの1実装例です。ここでは、このプログラムを Reactive Extensions が提供する便利な拡張 メソッドやクラスを使って書き換えを行います。 Reactive Extensions を使ったプログラムでは、頻繁に IObservable<T>゗ンターフェースの Subscribe メソッドを使用して通知に対するゕクションを設定します。この時、実行したい処理の 単位でクラスを作成するのは現実的ではありません。そのため、Reactive Extensions では System.ObservableExtensions クラスで IObservable<T>゗ンターフェースの拡張メソッドを 定義しています。主な拡張メソッドは下記の 3 つになります。どれもデリゲートを受け取るタ゗ プになります。  void Subscribe<T>(this IObservable<T> source, Action<T> onNext)  void Subscribe<T>( this IObservable<T> source, Action<T> onNext, Action onCompleted)  void Subscribe<T>( this IObservable<T> source, Action<T> onNext, Action<Exception> onError, Action onCompleted) この拡張メソッドを使うことでデリゲートで IObserver<T>゗ンターフェースの OnNext メソッ ドと OnError メソッドと OnCompleted メソッドを指定するだけで内部で IObserver<T>を継承 したクラスを作成して IObservable<T>゗ンターフェースの Subscribe(IObserver<T>)メソッ ドへ渡してくれます。このため、Reactive Extensions を使う上では IObserver<T>゗ンターフ ェースを実装することは、ほぼ無くなります。 (私は今まで実際の処理を書いていて実装したこと は有りません)上記の拡張メソッドを使うためには Reactive Extensions をプロジェクトの参照 に追加します。 Nuget で Rx-Main という名前のパッケージをプロジェクトに追加します。 (Express Edition の方は nuget コマンドラ゗ンを゗ンストールして nuget install rx-main でダウンロード されるので手動で参照に追加してください)そして、 Main のプログラムを下記のように変更します。 // 監視される人を作成 var source = new NumberObservable(); // 2 つ監視役を登録 8
  • var subscriber1 = source.Subscribe( // OnNext value => Console.WriteLine("OnNext({0}) called.", value), // OnError ex => Console.WriteLine("OnError({0}) called.", ex.Message), // OnCompleted () => Console.WriteLine("OnCompleted() called."));var subscriber2 = source.Subscribe( // OnNext value => Console.WriteLine("OnNext({0}) called.", value), // OnError ex => Console.WriteLine("OnError({0}) called.", ex.Message), // OnCompleted () => Console.WriteLine("OnCompleted() called."));// 監視される人の処理を実行Console.WriteLine("## Execute(1)");source.Execute(1);// 1 つを監視する人から解雇Console.WriteLine("## Dispose");subscriber2.Dispose();// 再度処理を実行Console.WriteLine("## Execute(2)");source.Execute(2);// エラーを起こしてみるConsole.WriteLine("## Execute(0)");source.Execute(0);// もう 1 つ監視役を追加して完了通知を行うvar sbscriber3 = source.Subscribe( // OnNext value => Console.WriteLine("OnNext({0}) called.", value), // OnError ex => Console.WriteLine("OnError({0}) called.", ex.Message), // OnCompleted () => Console.WriteLine("OnCompleted() called."));Console.WriteLine("## Completed");source.Completed(); 9
  • 引数を 3 つ受け取るタ゗プの Subscribe 拡張メソッドを使用しているため PrintObserver クラスは不要になります。今回の例では、同じ処理を何度も Subscribe しているのでメソッドとして切り出すなりしたほうがコードの重複が無くなりますが、 サンプルとしての見通しのためにあえて 1メソッド内に重複コードを書いています。次に IObservable<T>の実装クラスですが、これも IObservable<T>を実装したクラスが提供されています。System.Reactive.Subjects.Subject<T>クラスがそれにあたります。このクラスはIObservable<T>゗ンターフェースと IObserver<T>゗ンターフェースの両方を実装していてOnNext や OnError や OnCompleted などの IObserver<T>゗ンターフェースで提供されているメソッドを呼び出すと IObservable<T>゗ンターフェースの Subscribe メソッドで監視対象として追加されている IObserver<T>に処理を通知します。このクラスをラップする形で使うと簡単に IObservable<T>゗ンターフェースを実装することが出来ます。 namespace UseSubscribeMethod { using System; using System.Reactive.Subjects; class NumberObservable : IObservable<int> { // IObservable<T>と IObserver<T>の両方を兼ねるクラス private Subject<int> source = new Subject<int>(); // 自分を監視してる人に通知を行う // 0 を渡したらエラー通知 public void Execute(int value) { if (value == 0) { this.source.OnError(new Exception("value is 0")); // エラー状態じゃないまっさらな Subject を再作成 this.source = new Subject<int>(); return; } this.source.OnNext(value); } // 完了通知 public void Completed() 10
  • { this.source.OnCompleted(); } // 監視してる人を追加する。 // 戻り値の IDisposable を Dispose すると監視から外れる。 public IDisposable Subscribe(IObserver<int> observer) { return this.source.Subscribe(observer); } } } これで、初期のプログラムと同じ動作を実装出来ました。ただし、Reactive Extensions を普通に 使っている範囲では、 Subject<T>クラスは使用することは少ないです。本来は、IObservable<T> を実装したクラスを生成するためのフゔクトリメソッドが用意されているので、 そちらを利用する ことのほうが多いです。 しかし、 動作確認をするためには自分で値の発行などが細かく制御できる Subject<T>クラスを、これから先のサンプルで使用するために紹介しました。3. IObservable<T>のファクトリメソッド ここでは、Reactive Extensions で提供される IObservable<T>を作成するフゔクトリメソッド を紹介します。通常の Reactive Extensions を使ったプログラミングでは、提供されている様々 なフゔクトリメソッドから IObservable<T>を生成して使用します。そのため、どのような IObservable<T>の生成方法があるかを把握していることは Reactive Extensions を使う上でと ても重要な要素になります。フゔクトリメソッドは基本的に System.Reactive.Linq.Observable クラスの static メソッドとして提供されています。メソッドの一覧は下記の MSDN のドキュメン トを参照してください。 MSDN Observable クラス http://msdn.microsoft.com/en-us/library/system.reactive.linq.observable(v=VS.103).as px3.1. 基本的なファクトリメソッドの使用 ここでは、数多くある Observable クラスのフゔクトリメソッドから基本的なものをいくつか紹介 します。 11
  • 3.2. 指定された値を返す IObservable<T>を返すファクトリメソッド3.2.1. Observable.Return メソッド まず、一番動作を理解しやすい指定した値を通知する IObservable<T>を作成するメソッドから 使用します。最初に紹介するメソッドは Return メソッドになります。これは引数に指定した値を 通知する IObservable<T>を作成します。コード例を以下に示します。 // 10 を発行する IObservable<int>を作成する var source = Observable.Return(10); // 購読 var subscription = source.Subscribe( i => Console.WriteLine("OnNext({0})", i), ex => Console.WriteLine("OnError({0})", ex.Message), () => Console.WriteLine("Completed()")); // 購読の停止(この場合意味はない) subscription.Dispose(); コメントにあるように、このコードは 10 という値を発行する IObservable<int>を作成していま す。実行結果は下記のようになります。 OnNext(10) Completed() Return で作成した IObservable<T>は値を 1 つ発行すると、それ以降発行する値が無いため終了 状態になります。そのため Subscribe をすると実行結果のように OnNext の後に Completed が 呼ばれます。また、ここでは示していませんが 2 回 Subscribe を行うと値の発行と終了の通知を 再び行います。上記のコード例で言うと、2 回目の Subscribe の呼び出しでも、OnNext(10)と Completed()が表示されます。3.2.2. Observable.Repeat メソッド 次は、同じ値を指定した回数発行する IObservable<T>を返すメソッドになります。コード例を 下記に示します。 // 2 を 5 回発行する IObservable<int>を作成する var source = Observable.Repeat(2, 5); // 購読 var subscription = source.Subscribe( i => Console.WriteLine("OnNext({0})", i), ex => Console.WriteLine("OnError({0})", ex.Message), () => Console.WriteLine("Completed()")); // 購読停止(この場合意味はない) subscription.Dispose(); 12
  • これを実行すると下記のようになります。 OnNext(2) OnNext(2) OnNext(2) OnNext(2) OnNext(2) Completed() 同じ値をひたすら発行しているのがわかります。3.2.3. Observable.Range メソッド このメソッドは、指定した値から 1 ずつカウントゕップした値を指定した個数だけ返します。コ ード例を下記に示します。 // 1 から始まる値を 10 個発行する IObservable<int>を作成する var source = Observable.Range(1, 10); // 購読 var subscription = source.Subscribe( i => Console.WriteLine("OnNext({0})", i), ex => Console.WriteLine("OnError({0})", ex.Message), () => Console.WriteLine("Completed()")); // 購読停止(この場合意味はない) subscription.Dispose(); 1 からカウントゕップする値を 10 個発行するので 1~10 の値を発行する IObservable<int>を作 成しています。実行例を下記に示します。 OnNext(1) OnNext(2) OnNext(3) OnNext(4) OnNext(5) OnNext(6) OnNext(7) OnNext(8) OnNext(9) OnNext(10) Completed() 13
  • 3.2.3.1. IObservable<T>の拡張メソッドの Repeat ここまでは Observable クラスに定義された IObservable<T>を返すメソッドを使ってきました が、ここで少し横道に逸れて IObservable<T>の拡張メソッドとして定義された Repeat メソッ ドを紹介したいと思います。この拡張メソッドも Observable クラスに定義されています。これま でのメソッドとの違いは純粋な static メソッドではなく、IObservable<T>の拡張メソッドとし て定義されている点です。 この Repeat 拡張メソッドは、 IObservable<T>が発行する値を指定し た回数繰り返す IObservable<T>を作成します。Range メソッドと組み合わせて使用した例を下 記に示します。 // 1 から始まる値を 3 個発行する IObservable<int>を作成する var source = Observable.Range(1, 3); // そして、それを 3 回繰り返す IObservable<int>を作成する source = source.Repeat(3); // 購読 var subscription = source.Subscribe( i => Console.WriteLine("OnNext({0})", i), ex => Console.WriteLine("OnError({0})", ex.Message), () => Console.WriteLine("Completed()")); // 購読停止(この場合意味はない) subscription.Dispose(); 実行結果は下記のようになります。1~3 の値が 3 回発行されているのがわかります。 OnNext(1) OnNext(2) OnNext(3) OnNext(1) OnNext(2) OnNext(3) OnNext(1) OnNext(2) OnNext(3) Completed()3.2.4. Observable.Generate メソッド 次は、Generate メソッドです。このメソッドは for 文に近い使用感のメソッドになっています。 第一引数で初期値状態、第二引数で継続の条件、第三引数で更新処理、第四引数で発行する値の生 成を行う処理を渡します。 コードを見ていただくと゗メージがわきやすいと思うので下記にコード 例を示します。 // 初期値 0, 値が 10 より小さい間, 値は 1 ずつ゗ンクリメントして, 値を二乗したものを発行する 14
  • // IObservable<int>を作成する。 // for (int i = 0; i < 10; i++) { yield return i * i; }のような゗メージ var source = Observable.Generate(0, i => i < 10, i => ++i, i => i * i); // 購読 var subscription = source.Subscribe( i => Console.WriteLine("OnNext({0})", i), ex => Console.WriteLine("OnError({0})", ex.Message), () => Console.WriteLine("Completed()")); // 購読停止(この場合意味はない) subscription.Dispose(); このコードを実行すると 0~9 までの値を二乗したものを発行する IObservable<int>が作成され ます。実行結果は下記のようになります。 OnNext(0) OnNext(1) OnNext(4) OnNext(9) OnNext(16) OnNext(25) OnNext(36) OnNext(49) OnNext(64) OnNext(81) Completed() 0~9 までの値を二乗した値が発行されているのが確認出来ます。3.2.5. Observable.Defer メソッド 次に紹介するメソッドは Defer メソッドです。このメソッドは IObservable<T>を直接返すラム ダ式を引数に渡します。Subscribe メソッドが呼ばれる度に、Defer メソッドが実行されて IObservable<T>が作成されます。コード例を以下に示します。 // 1, 2, 3 と順番に値を発行して終了する IObservable<int>を生成する var source = Observable.Defer<int>(() => { Console.WriteLine("# Defar method called."); // ReplaySubject<T>は Subject<T>の亜種で Subscribe されると // 今まで行われた操作を全てリプレ゗する。 var s = new ReplaySubject<int>(); s.OnNext(1); 15
  • s.OnNext(2); s.OnNext(3); s.OnCompleted(); // AsObservable で IObservable<T>へ変換できる。 return s.AsObservable(); }); // 購読(source は ReplaySubject で作っているので Defer メソッド内でした操作が再生される) var subscription1 = source.Subscribe( i => Console.WriteLine("OnNext({0})", i), ex => Console.WriteLine("OnError({0})", ex.Message), () => Console.WriteLine("Completed()")); var subscription2 = source.Subscribe( i => Console.WriteLine("OnNext({0})", i), ex => Console.WriteLine("OnError({0})", ex.Message), () => Console.WriteLine("Completed()")); // 購読停止(この場合意味はない) subscription1.Dispose(); subscription2.Dispose();実行結果は下記のようになります。2 回 Subscribe しているので 2 回 Defer メソッドが呼ばれていることが確認出来ます。# Defar method called.OnNext(1)OnNext(2)OnNext(3)Completed()# Defar method called.OnNext(1)OnNext(2)OnNext(3)Completed()Defer メソッド内で使用している ReplaySubject<T>クラスは、コメント内にあるようにSubject<T>クラスの親戚のクラスです。 Subject<T>クラスが Subscribe される前の操作は通知しないのに対して ReplaySubject<T>クラスは、Subscribe される前の操作も通知する点が異なります。試しに Defer メソッド内の ReplaySubject<T>を普通の Subject<T>に変更して実行すると下記のような結果になります。# Defar method called.Completed() 16
  • # Defar method called. Completed() Defer メソッドで返された IObservable<int>は既に通知する値が無く完了した状態になってい るため、OnCompleted だけが実行されます。3.2.6. Observable.Create メソッド 次は、Create メソッドを紹介します。このメソッドは、引数の形が特殊で IObserver<T>を受け 取って Action を返すラムダ式を引数に受け取ります。引数で受け取る IObserver<T>には OnNext や OnErrror や OnCompleted などの操作を行います。ここで行った操作に応じた値が Create メソッドの戻り値の IObservable<T>で発行される値になります。 最期に戻り値の Action ですが、これは Dispose された時に実行される処理になります。Create メソッド内でリソースや ゗ベントの購読などをしていた場合に解放する処理を行うと良いと思います。 では、コード例を下 記に示します。 // 1, 2, 3 と順番に値を発行して終了する IObservable<int>を生成する var source = Observable.Create<int>(observer => { Console.WriteLine("# Create method called."); // 引数の IObserver<int>に対して On****メソッドを呼ぶ observer.OnNext(1); observer.OnNext(2); observer.OnNext(3); observer.OnCompleted(); // Dispose が呼ばれた時の処理を返す。 // リソースを確保していた場合は、ここで解放すると良い。 return () => Console.WriteLine("Disposable action"); }); // 購読 var subscription1 = source.Subscribe( i => Console.WriteLine("OnNext({0})", i), ex => Console.WriteLine("OnError({0})", ex.Message), () => Console.WriteLine("Completed()")); var subscription2 = source.Subscribe( i => Console.WriteLine("OnNext({0})", i), ex => Console.WriteLine("OnError({0})", ex.Message), () => Console.WriteLine("Completed()")); // 購読停止(この場合意味はない) Console.WriteLine("# Dispose method call."); subscription1.Dispose(); 17
  • subscription2.Dispose(); 実行結果は、下記のようになります。 ## CreateSample # Create method called. OnNext(1) OnNext(2) OnNext(3) Completed() Disposable action # Create method called. OnNext(1) OnNext(2) OnNext(3) Completed() Disposable action # Dispose method call. 注意したいのは Disposable action の表示されているタ゗ミングです。通常の考えだと subscription1.Dispose();の呼び出しタ゗ミングで Disposable action と表示されるように思いま すが動作を確認すると Dispose メソッドを呼ぶ前に自動で呼び出されています。これは、 Completed の後で、もう購読していても値が来ないため Dispose が自動で行われていることを示 しています。 これまでのサンプルでも Dispose に特に意味がないと書いていたのはこのためです。3.2.7. Observable.Throw メソッド 一連の基本的な IObservable<T>を作成するメソッドの締めくくりとして最後に Throw メソッド を紹介します。これは引数に例外を渡します。 疑似的にエラーを起こしたいときに使う以外に使用 方法が思いつきません。コード例を下記に示します。 // エラーを発行するだけの IObservable<int>を生成 var source = Observable.Throw<int>(new Exception("Error message")); // 購読 var subscription = source.Subscribe( i => Console.WriteLine("OnNext({0})", i), ex => Console.WriteLine("OnError({0})", ex.Message), () => Console.WriteLine("Completed()")); // 購読停止(この場合意味はない) subscription.Dispose(); 実行結果は下記のようになります。OnError が呼ばれているのがわかります。 OnError(Error message) 18
  • 3.3. ここまでに紹介した IObservable<T>の動作 ここまで紹介してきた IObservable<T>は、全て下記のような特徴があります。 1. フゔクトリで発行する値を指定して IObservable<T>を作成する。 2. Subscribe が呼び出されると以下のような動きをする 1. 値を全て発行する 2. 終わったら OnCompleted を呼ぶ 3. 購読を解除する 3. Subscribe が行われる度に 2 の動作を行う。 この動作を表した図を以下に示します。 上記の図では OnError と二度目の Subscribe について書いていませんが、 基本的に OnError の時 には例外が IObserver<T>に通知されます。また、一度の Subscribe で IObservable<T>が空に なるように見えますがデータの流れを示すために空にしているだけで実際には再度 Subscribe を 行うと値が発行されます。3.4. リソースの確保を伴う IObservable<T> ここでは、IObservable<T>を取得する際にリソースの確保を行うケースに使用できるフゔクト リメソッドについて説明します。 19
  • 3.4.1. Observable.Uting メソッド Using メソッドは、 名前の通り using ブロックのようにリソースを確実に解放するために使用する メソッドになります。メソッドのシグネチャは、第一引数に Func<TResource>を渡して、リソ ースを確保するオブジェクトを作成する処理を指定します。TResource 型は IDisposable を実装 している必要があります。 第二引数に、 Func<TResource, IObservable<TSource>>を渡して、 リソースを使って IObservable<T>を取得する処理を指定します。実際には、外部リソースに依 存するものを使用する場合のコード例が適しているのですが、ここでのサンプルは、ダミーの IDisposable を実装した下記のようなクラスを使用して動作確認を行います。 // サンプルのダミーリソースクラス class SampleResource : IDisposable { // データを取得する public IObservable<string> GetData() { return Observable.Create<string>(o => { o.OnNext("one"); o.OnNext("two"); o.OnNext("three"); o.OnCompleted(); return Disposable.Empty; }); } // 解放処理 public void Dispose() { Console.WriteLine("Resource.Dispose called"); } } このクラスを使用して Using メソッドの動作確認を行います。Using メソッドの使用例を下記に 示します。 // SampleResource(IDisposable を実装)を使用してデータを取得する var source = Observable.Using( () => new SampleResource(), sr => sr.GetData()); // 購読 20
  • source.Subscribe( i => Console.WriteLine("OnNext({0})", i), ex => Console.WriteLine("OnError({0})", ex.Message), () => Console.WriteLine("Completed()")); Using メソッドで SampleResource クラスの作成と、SampleResource クラスを使って IObservable<T>を取得しています。このコードの実行結果を下記に示します。 OnNext(one) OnNext(two) OnNext(three) Completed() Resource.Dispose called 実行結果からもわかるように、最後に、SampleResource クラスの Dispose が呼ばれていること がわかります。3.5. 時間と共に値を発行する IObservable<T> ここでは時間と共に値を発行する IObservable<T>を返すフゔクトリメソッドを使って動作を確 認します。3.5.1. Observable.Timer メソッド まず、一番直感的に理解に理解できると思われる Timer メソッドを使用します。Timer メソッド は名前の通り一定時間ごとに値を発行します。 発行する値は Timer が実行された回数になります。 いくつかオーバーロードがありますが、 第一引数にタ゗マーを開始するまでの時間、 第二引数にタ ゗マーの゗ンターバルを TimeSpan 型で指定するオーバーロードが、一番使用頻度が高いと思い ます。コード例を下記に示します。 // 3 秒後から 1 秒間隔で値を発行する IObservable<long>を作成する var source = Observable.Timer( TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(1)); // 購読 var subscription = source.Subscribe( i => Console.WriteLine("OnNext({0})", i), ex => Console.WriteLine("OnError({0})", ex.Message), () => Console.WriteLine("Completed()")); // 3 秒後から OnNext(回数)が表示される Console.WriteLine("please enter key..."); Console.ReadLine(); 21
  • // Observable が発行する値の購読を停止 Console.WriteLine("dispose method call."); subscription.Dispose(); // Dispose をすると値が発行されなくなる。 Console.WriteLine("please enter key..."); Console.ReadLine();コメントにもある通り、 上記のコードは 3 秒後から 1 秒間隔で値を発行する IObservable<long>を作成して Subscribe で購読しています。そして、Console.ReadLine で待機しています。暫く待っていると OnNext が発行されます。適当なタ゗ミングで Enter キーを押すとsubscription.Dispose()が実行され、購読が解除されます。Dispose のあとは、OnNext が実行されないことを確認できます。実行結果を下記に示します。please enter key... // ここから 3 秒何も表示されないOnNext(0) // 3 秒たつと 1 秒間隔で OnNext が呼ばれるOnNext(1)OnNext(2)OnNext(3)OnNext(4)OnNext(5)dispose method call. // Enter を押して Dispose が呼ばれると OnNext も止まるplease enter key...Timer の実行゗メージを下図に示します。 22
  • 3.5.2. Observable.Interval メソッド 次も Timer メソッドと同じように使用できるメソッドを紹介します。こちらは Interval メソッド で TimeSpan を 1 つ渡すだけでシンプルに使用できます。Subscribe した時点から TimeSpan で 指定した間隔で値を発行します。発行する値は Timer と同じで何回値を発行したかという数字に なります。コード例を下記に示します。 // 500ms 間隔で値を発行する var source = Observable.Interval(TimeSpan.FromMilliseconds(500)); // 購読 var subscription = source.Subscribe( i => Console.WriteLine("OnNext({0})", i), ex => Console.WriteLine("OnError({0})", ex.Message), () => Console.WriteLine("Completed()")); Console.WriteLine("please enter key..."); Console.ReadLine(); // Observable が発行する値の購読を停止 Console.WriteLine("dispose method call."); subscription.Dispose(); // Dispose をすると値が発行されても受け取らなくなる。 Console.WriteLine("please enter key..."); Console.ReadLine(); 上記の例では 500ms 間隔で値を発行しています。Enter キーを押すと Dispose を読んで購読を停 止して再度 Enter キーが押されるまで待機します。実行結果を下記に示します。 please enter key... OnNext(0) OnNext(1) OnNext(2) OnNext(3) OnNext(4) OnNext(5) OnNext(6) dispose method call. please enter key... この実行結果では 6 が表示されたタ゗ミングで Enter キーを押して Dispose を実行しています。 実行結果からは読み取れませんが、上記の実行結果は Dispose が実行されたあとに数秒間待機し て購読が停止していることを確認しています。 23
  • 3.5.3. Observable.Generate メソッド 次は、3.2.4 でも使用した Generate メソッドを使用します。Generate メソッドには TimeSpan を受け取るオーバーロードがあり、これを使うことで指定した時間間隔で値を発行する IObservable<T>を作成できます。コード例を下記に示します。 var source = Observable.Generate( // 0 から 0, // i < 10 以下の間繰り返す i => i < 10, // i は 1 ずつ増える i => ++i, // 発行する値は i の二乗 i => i * i, // 値は(発行する値 * 100)ms 間隔で発行する i => TimeSpan.FromMilliseconds(i * 100)); // 購読 var subscription = source.Subscribe( i => Console.WriteLine("OnNext({0})", i), ex => Console.WriteLine("OnError({0})", ex.Message), () => Console.WriteLine("Completed()")); Console.WriteLine("please enter key..."); Console.ReadLine(); // Observable が発行する値の購読を停止 Console.WriteLine("dispose method call."); subscription.Dispose(); // Dispose をすると値が発行されても受け取らなくなる。 Console.WriteLine("please enter key..."); Console.ReadLine(); 上記の例は(0 * 0 * 100)ms, (1 * 1 * 100)ms, (2 * 2 * 100)ms, (3 * 3 * 100)ms…(9 * 9 * 100)ms と値の発行回数増える度に゗ンターバルを長くとるようにしています。 実行結果を下記に 示します。 please enter key... OnNext(0) OnNext(1) OnNext(4) 24
  • OnNext(9) OnNext(16) OnNext(25) OnNext(36) OnNext(49) OnNext(64) OnNext(81) Completed() dispose method call. please enter key... 上記実行例は、最後まで Enter キーを押さずに実行した場合になります。途中で Enter キーを押 した場合の実行例を下記に示します。 please enter key... OnNext(0) OnNext(1) OnNext(4) OnNext(9) OnNext(16) dispose method call. please enter key... このように Dispose を呼ぶと Generate メソッドの処理を途中から購読しなくなります。3.6. Cold な IObservable<T>と Hot な IObservable<T> ここまでに紹介した IObservable<T>のフゔクトリメソッドは全て共通の特徴があります。それ は複数回 Subscribe すると、それぞれの IObserver<T>に IObservable<T>が個別に値を発行し ます。IObservable<T>の作り方にもよりますが、基本的には同じ値が発行されていきます。文 章で説明するよりもコードの動作例で示します。 // 1 秒間隔で値を発行する IObservable<long>を作成する var source = Observable.Timer( TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)); // 購読 var subscription1 = source.Subscribe( i => Console.WriteLine("{0:yyyy/MM/dd HH:mm:ss.FFF} 1##OnNext({1})", DateTime.Now, i), ex => Console.WriteLine("1##OnError({0})", ex.Message), 25
  • () => Console.WriteLine("1##Completed()")); // 3 秒後にもう一度購読 Thread.Sleep(3000); // 購読 var subscription2 = source.Subscribe( i => Console.WriteLine("{0:yyyy/MM/dd HH:mm:ss.FFF} 2##OnNext({1})", DateTime.Now, i), ex => Console.WriteLine("2##OnError({0})", ex.Message), () => Console.WriteLine("2##Completed()")); Console.ReadLine(); subscription1.Dispose(); subscription2.Dispose();上記のコードは 1 秒間隔で値を発行する IObservable<long>を作成し、 回 Subscribe したあと 1に 3 秒待ってもう一度 Subscribe しています。この実行結果を下記に示します。2011/11/07 23:05:01.883 1##OnNext(0)2011/11/07 23:05:02.884 1##OnNext(1)2011/11/07 23:05:03.879 1##OnNext(2)2011/11/07 23:05:04.877 1##OnNext(3)2011/11/07 23:05:04.893 2##OnNext(0)2011/11/07 23:05:05.89 1##OnNext(4)2011/11/07 23:05:05.894 2##OnNext(1)2011/11/07 23:05:06.877 1##OnNext(5)2011/11/07 23:05:06.892 2##OnNext(2)2011/11/07 23:05:07.873 1##OnNext(6)2011/11/07 23:05:07.894 2##OnNext(3)最初に Subscribe した方には yyyy/MM/dd HH:mm:ss.FFF 1##OnNext(値)の形式で出力しています。二番目に Subscribe した方には yyyy/MM/dd HH:mm:ss.FFF 2##OnNext(値)の形式で出力しています。これを見ると、最初に Subscribe したものと二回目に Subscribe したものでは、タ゗ムスタンプが微妙にずれていることから、 内部的には別のタ゗マーが割り当てられていることが見て取れます。また、発行される値も Subscribe した時点で 0 からカウントされていて最初に Subscribe したものと二回目に Subscribe したものの間に関係性は全くありません。このように、Cold な Observable は「複数回 Subscribe した時に Observer 毎に独立した値を発行する」という特徴があります。では次に、Cold と対となるものとして Hot な IObservable<T>を見ていきます。Hot なObservable の代表として.NET 組み込みの Observer パターンである゗ベントからIObservable<T>を生成する Observable.FromEvent<TDelegate, TEventArgs>(…)というメソッドがあります。このメソッドのシグネチャを下記に示します。 26
  • public static IObservable<TEventArgs> FromEvent<TDelegate, TEventArgs>( // Action<TEventArgs>から゗ベントハンドラの形へと変換する処理 // Action<TEventArgs>は Subscribe したときの OnNext の処理にあたる。 Func<Action<TEventArgs>, TDelegate> conversion, // ゗ベントハンドラを登録する処理 Action<TDelegate> addHandler, // ゗ベントハンドラの登録を解除する処理 Action<TDelegate> removeHandler)今まで出てきたもののなかではかなり異質の引数ですが゗ベントハンドラの形にあったデリゲートを作成してハンドラの登録処理と削除処理を渡します。あとは IObservable<TEventArgs>をSubscribe したタ゗ミングや Dispose したタ゗ミングで適切に゗ベントの登録・登録解除が行われます。 自前でやると゗ベントハンドラの登録解除は忘れがちな処理なので地味に有りがたい機能です。コード例を下記に示します。 // 1 秒間隔で値を発行する Timer var timer = new System.Timers.Timer(1000); var source = Observable.FromEvent<ElapsedEventHandler, ElapsedEventArgs>( h => (s, e) => h(e), h => timer.Elapsed += h, h => timer.Elapsed -= h); // タ゗マー開始 timer.Start(); // 購読 var subscription1 = source.Subscribe( e => Console.WriteLine("{0:yyyy/MM/dd HH:mm:ss.FFF} 1##OnNext({1:yyyy/MM/dd HH:mm:ss.FFF})",DateTime.Now, e.SignalTime), ex => Console.WriteLine("1##OnError({0})", ex.Message), () => Console.WriteLine("1##Completed()")); // 3 秒後にもう一度購読 Thread.Sleep(3000); // 購読 var subscription2 = source.Subscribe( e => Console.WriteLine("{0:yyyy/MM/dd HH:mm:ss.FFF} 2##OnNext({1:yyyy/MM/dd HH:mm:ss.FFF})",DateTime.Now, e.SignalTime), ex => Console.WriteLine("2##OnError({0})", ex.Message), () => Console.WriteLine("2##Completed()")); 27
  • Console.ReadLine(); subscription1.Dispose(); subscription2.Dispose(); timer.Stop(); Cold な Observable で示した処理と基本的には同じ処理を行っています。実行結果を下記に示し ます。 2011/11/07 23:37:41.146 1##OnNext(2011/11/07 23:37:41.146) 2011/11/07 23:37:42.16 1##OnNext(2011/11/07 23:37:42.16) 2011/11/07 23:37:43.174 1##OnNext(2011/11/07 23:37:43.174) ← ここと 2011/11/07 23:37:43.174 2##OnNext(2011/11/07 23:37:43.174) ← ここ 以下 2 つずつ同じ値が表示されてい る 2011/11/07 23:37:44.188 1##OnNext(2011/11/07 23:37:44.188) 2011/11/07 23:37:44.188 2##OnNext(2011/11/07 23:37:44.188) 2011/11/07 23:37:45.202 1##OnNext(2011/11/07 23:37:45.202) 2011/11/07 23:37:45.202 2##OnNext(2011/11/07 23:37:45.202) 2011/11/07 23:37:46.216 1##OnNext(2011/11/07 23:37:46.216) 2011/11/07 23:37:46.216 2##OnNext(2011/11/07 23:37:46.216) 上記の結果で興味深い点は、最初に Subscribe したものと、二番目に Subscribe したものの出力 結果のタ゗ムスタンプと、OnNext に渡ってきている値が同じという点です。 つまり、Hot な IObservable<T>とは Cold な IObservable<T>と違って「複数回 Subscribe し たときに、全ての Observer に同じタ゗ミングで同じ値を発行するもの」ということになります。3.6.1. Observable.FromEvent メソッド では、Hot な IObservable<T>を作成するメソッドのトップバッターとして先ほど登場した FromEvent メソッドを紹介します。FromEvent メソッドのシグネチャは既に示したので、より 詳しく動作を確認するためのコードを下記に示します。 // ゗ベントを発行するクラス var eventSource = new EventSource(); var source = Observable.FromEvent<EventHandler, EventArgs>( h => (s, e) => h(e), // 普通は h => eventSource.Raised += h だけでいい h => { Console.WriteLine("add handler"); eventSource.Raised += h; }, // 普通は h => eventSource.Raised -= h だけでいい 28
  • h => { Console.WriteLine("remove handler"); eventSource.Raised -= h; }); // 2 回購読 var subscription1 = source.Subscribe( i => Console.WriteLine("1##OnNext({0})", i), ex => Console.WriteLine("1##OnError({0})", ex.Message), () => Console.WriteLine("1##Completed()")); var subscription2 = source.Subscribe( i => Console.WriteLine("2##OnNext({0})", i), ex => Console.WriteLine("2##OnError({0})", ex.Message), () => Console.WriteLine("2##Completed()")); // 2 回呼び出してみる // 合計 4 回の OnNext が呼ばれるはず eventSource.OnRaised(); eventSource.OnRaised(); // Observable が発行する値の購読を停止 Console.WriteLine("dispose method call."); subscription1.Dispose(); subscription2.Dispose();この例で使用している EventSource クラスは、Raised という゗ベントと OnRaised という゗ベントを発行するメソッドだけを持ったクラスで下記のように定義しています。 // ゗ベント発行クラス class EventSource { public event EventHandler Raised; public void OnRaised() { var h = this.Raised; if (h != null) { h(this, EventArgs.Empty); 29
  • } } } このコードの実行結果を下記に示します。 add handler add handler 1##OnNext(System.EventArgs) 2##OnNext(System.EventArgs) 1##OnNext(System.EventArgs) 2##OnNext(System.EventArgs) dispose method call. remove handler remove handler FromEvent メソッドの゗ベントハンドラ登録処理と゗ベントハンドラの登録解除処理にログを出 力するように仕込んだものが表示されています。 このことから、Dispose を呼ぶときちんと゗ベン トハンドラの登録解除が行われることがわかります。また、゗ベントが発行されたタ゗ミングで 2 つ登録した Observer の両方に対して通知がいっていることも確認できます。3.6.2. Observable.Start メソッド 次に、簡単にバックグラウンドの処理を記述できる Start メソッドについて説明します。Start メ ソッドは Action か Func<T>を引数に受け取り IObservable<Unit>か IObservable<T>を返し ます。引数で受け取ったデリゲートの処理が終わると IObservable から結果が発行されます。コ ード例を下記に示します。 // バックグラウンドで処理を開始 var source = Observable.Start(() => { Console.WriteLine("background task start."); Thread.Sleep(2000); Console.WriteLine("background task end."); return 1; }); // 購読 Console.WriteLine("subscribe1"); var subscription1 = source.Subscribe( i => Console.WriteLine("1##OnNext({0})", i), ex => Console.WriteLine("1##OnError({0})", ex.Message), () => Console.WriteLine("1##Completed()")); 30
  • // 処理が確実に終わるように 5 秒待つ Console.WriteLine("sleep 5sec."); Thread.Sleep(5000); // Observable が発行する値の購読を停止 Console.WriteLine("dispose method call."); subscription1.Dispose(); // 購読 Console.WriteLine("subscribe2"); var subscription2 = source.Subscribe( i => Console.WriteLine("2##OnNext({0})", i), ex => Console.WriteLine("2##OnError({0})", ex.Message), () => Console.WriteLine("2##Completed()")); subscription2.Dispose();このサンプルで特徴的なのが、 Start メソッド内の処理が 2 秒で終わるにも関わらず 5 秒スリープした後に Subscribe をしている点です。通常の感覚では、既に処理が完了して値が発行された後なので Subscribe しても何も起きないと考えられます。しかし、Start メソッドの戻り値のIObservable<T>は最後の処理結果をキャッシュしています。 そのため、Subscribe されるとキャッシュしている値と OnCompleted を発行します。この例の実行結果では、最初の Subscribe と二回目の Subscribe それぞれで、OnNext とOnCompleted の処理が呼ばれます。実行結果を下記に示します。subscribe1sleep 5sec. ← ここで 5 秒スリープしているbackground task start.background task end.1##OnNext(1)1##Completed()dispose method call.subscribe2 ← このタ゗ミングでは Start メソッドの処理は終了している2##OnNext(1) ← OnNext と OnCompleted が通知される2##Completed()このことから、Start メソッドで作成する IObservable<T>は、Start メソッドが完了するまではHot な Observable で処理が終了したあとは Cold な Observable になるという 2 面性をもつという特徴があることが確認できます。 31
  • 3.6.3. Observable.ToAsync メソッド 次は、ToAsync メソッドを紹介します。このメソッドも Start メソッドと同様に重たい処理をバ ックグラウンドでやるために使用できます。Start メソッドとの違いは ToAsync の戻り値にあら われています。シグネチャを下記に示します。 public static Func<IObservable<T>> ToAsync<T>(Func<T> function) 引数に重たいことをやる処理を渡して、戻り値が IObservable<T>を返すデリゲートになってい ます。この戻り値のデリゲートを呼び出すことで ToAsync の引数に渡した処理がはじめて実行さ れます。Start は、Start メソッドを呼び出した直後から処理が開始されましたが、ToAsync を使 うと処理の開始のタ゗ミングを柔軟に制御できます。 ここで示したメソッドのシグネチャは数十個あるオーバーロードの 1 つになります。ToAsync に はほかにも戻り値が無いケースや引数が大量にあるケースに備えて膨大な数のオーバーロードが あります。完全なオーバーロードのリストについては下記の MSDN のリフゔレンスを参照してく ださい。 Observable.ToAsync Method : http://msdn.microsoft.com/en-us/library/system.reactive.linq.observable.toasync(v=VS. 103).aspx コード例を下記に示します。 // 戻り値は Func<IObservable<T>> var source = Observable.ToAsync(() => { Console.WriteLine("background task start."); Thread.Sleep(2000); Console.WriteLine("background task end."); return 1; }); // ToAsync はデリゲートを返すので Invoke() or ()をしないと処理が開始されない Console.WriteLine("source() call."); var invokedSource = source.Invoke(); var subscription1 = invokedSource.Subscribe( i => Console.WriteLine("1##OnNext({0})", i), ex => Console.WriteLine("1##OnError({0})", ex.Message), () => Console.WriteLine("1##Completed()")); // 処理が確実に終わるように 5 秒待つ Console.WriteLine("sleep 5sec."); Thread.Sleep(5000); 32
  • // Observable が発行する値の購読を停止 Console.WriteLine("dispose method call."); subscription1.Dispose(); // 購読 Console.WriteLine("subscribe2"); var subscription2 = invokedSource.Subscribe( i => Console.WriteLine("2##OnNext({0})", i), ex => Console.WriteLine("2##OnError({0})", ex.Message), () => Console.WriteLine("2##Completed()")); subscription2.Dispose(); ポ゗ントは、ToAsync の戻り値に対して Invoke をしているところです。ここで初めて ToAsync に渡した処理の実行がはじまります。実行結果を下記に示します。 source() call. background task start. sleep 5sec. background task end. 1##OnNext(1) 1##Completed() dispose method call. subscribe2 2##OnNext(1) 2##Completed() ここでも Start メソッドの例と同じように ToAsync の処理が終わった後に Subscribe しているに も関わらず OnNext と OnCompleted が呼ばれていることがわかります。非同期で処理を行う IObservable<T>は全体的にこのような動きを行うので覚えておきましょう。3.6.4. Observable.FromAsyncPattern メソッド これで、一連のフゔクトリメソッドの紹介は最後になります。 最後を飾るのは FromAsyncPattern メソッドです。このメソッドは名前が示す通り.NET Framework で使われている非同期呼び出し のパターンから IObservable<T>を作成します。 // 重たい処理 Func<int, int, int> asyncProcess = (x, y) => { Console.WriteLine("process start."); Thread.Sleep(2000); Console.WriteLine("process end."); 33
  • return x + y;};// 因みに非同期呼び出しは普通に書くとこんな感じ// asyncProcess.BeginInvoke(// 10, 2,// ar =>// {// var ret = asyncProcess.EndInvoke(ar);// // do something// },// null);var asyncPattern = Observable.FromAsyncPattern<int, int, int>( asyncProcess.BeginInvoke, asyncProcess.EndInvoke);var source = asyncPattern(10, 2);// 処理中に購読開始Console.WriteLine("subscribe2");var subscription1 = source.Subscribe( i => Console.WriteLine("1##OnNext({0})", i), ex => Console.WriteLine("1##OnError({0})", ex.Message), () => Console.WriteLine("1##Completed()"));// 確実に処理が終わるように 5 秒待つConsole.WriteLine("sleep 5sec");Thread.Sleep(5000);// Observable が発行する値の購読を停止Console.WriteLine("dispose method call.");subscription1.Dispose();// 処理が完了したあとに購読Console.WriteLine("subscribe2");var subscription2 = source.Subscribe( i => Console.WriteLine("2##OnNext({0})", i), ex => Console.WriteLine("2##OnError({0})", ex.Message), 34
  • () => Console.WriteLine("2##Completed()")); // 購読解除 Console.WriteLine("dispose method call."); subscription2.Dispose(); 普通はコールバックで書く非同期呼び出しを IObservable<T>にラッピングします。このメソッ ドも ToAsync と同様に戻り値がデリゲートなので、デリゲートを呼び出すことで非同期処理が開 始されます。実行結果を下記に示します。 process start. subscribe2 sleep 5sec process end. 1##OnNext(12) 1##Completed() dispose method call. subscribe2 2##OnNext(12) 2##Completed() dispose method call. ここでも、処理が終わった後に Subscribe をしても OnNext と OnCompleted が呼ばれているこ とがわかります。4. IObservable の拡張メソッド ここまで IObservable<T>を作成するための様々なフゔクトリメソッドを見てきました。ここで は、視点を変えて IObservable<T>を作成したあとに使用できる IObservable<T>に定義された 拡張メソッドを紹介します。4.1. LINQ のメソッド IObservable<T>の拡張メソッドも、 ほとんどが System.Reactive.Linq.Observable クラスに定 義されています。その中でも LINQ でお馴染みの Where や Select メソッドも含まれています。 LINQ のメソッドは IObservable<T>が発行した値に対して Where でフゖルタリングしたり Select で変換したりできます。下図は、その゗メージを表しています。 35
  • 下図は Where でフゖルタリングされた場合を表しています。Where でフゖルタリングされた場合は Select や Subscribe まで処理はいきません。実際にコードで動きを確認してみます。 // 値を発行するための Subject var subject = new Subject<int>(); // AsObservable で IObservable<T>に変換(ゕップキャストで Subject<T>に戻せない var source = subject.AsObservable(); // 普通に Subscribe source.Subscribe( value => Console.WriteLine("1##OnNext({0})", value), ex => Console.WriteLine(ex.Message), () => Console.WriteLine("1##OnCompleted()")); // 奇数のみ通すようにフゖルタリングして source.Where(i => i % 2 == 1) // 文字列に加工して .Select(i => i + "は奇数です") // 表示する 36
  • .Subscribe( value => Console.WriteLine("2##OnNext({0})", value), ex => Console.WriteLine(ex.Message), () => Console.WriteLine("2##OnCompleted()")); // 1~10 の値を subject に対して発行する Observable.Range(1, 10).ForEach(i => subject.OnNext(i)); // 完了通知を行う subject.OnCompleted();上記のコードでは、1~10 の値を Subject<T>を使って発行しています。Subject<T>は、AsObservable メソッドで IObservable<T>に変換できます。AsObservable をしなくてもSubject<T>クラスは IObservable<T>を継承しているので差支えはないのですが、純粋なIObservable<T>に、なんとなくしたかったのでこの例では変換しています。通常は、内部にSubject<T>クラスを抱えたクラスが外部に IObservable<T>を公開するときに、ダウンキャストされても Subject<T>型に戻せない IObservable<T>を返すために使用します。その他に、今回初登場のメソッドとして ForEach メソッドがあります。これは引数に渡されたAction<T>を、IObservable<T>から発行された値を引数に渡して使用します。平たく言うと forループです。ここでは 1~10 の値を Observable.Range で作成して ForEach で Subject<T>に流し込んでいます。今回の本題である拡張メソッドは Where メソッドと Select メソッドになります。Where メソッドは引数で渡した Func<T, bool>が true を返す要素のみを通します。Select メソッドは引数で渡した Func<T, U>で値を変換します。上記の例では奇数以外の値をフゖルタリングして「X は奇数です」という文字列に変換して、Subscribe 内で標準出力に出力しています。動作の違いを見るために、Where や Select を使用しないで Subscribe もしています。このプログラムの実行結果を下記に示します。1##OnNext(1)2##OnNext(1 は奇数です)1##OnNext(2)1##OnNext(3)2##OnNext(3 は奇数です)1##OnNext(4)1##OnNext(5)2##OnNext(5 は奇数です)1##OnNext(6)1##OnNext(7)2##OnNext(7 は奇数です)1##OnNext(8)1##OnNext(9) 37
  • 2##OnNext(9 は奇数です) 1##OnNext(10) 1##OnCompleted() 2##OnCompleted() 実行結果から、Where によるフゖルタリングが行われていることと、Select による変換が行われ ていることがわかると思います。4.2. 単一の値を取得するメソッド ここでは、IObservable<T>のシーケンスから単一の値を取得するために利用するメソッドにつ いて説明します。4.2.1. First メソッドと Last メソッド まず、最初の値を取得する First メソッドと、最後の値を取得する Last メソッドについて説明し ます。各メソッドのシグネチャは以下のようになります。 // First メソッド public static TSource First<T>( this IObservable<T> source ) // Last メソッド public static TSource Last<T>( this IObservable<T> source ) どちらのメソッドも IObservable<T>から T の値を取得します。First メソッドのコード例を下記 に示します。 // Observable を作成前のタ゗ムスタンプを表示 Console.WriteLine("Timestamp {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now); var firstResult = Observable // 5 秒間隔で値を発行する .Interval(TimeSpan.FromSeconds(5)) .Select(i => "value is " + i) // 最初の値を取得 .First(); // First の実行が終わった後のタ゗ムスタンプを表示 Console.WriteLine("Timestamp {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now); // 取得した値を表示 Console.WriteLine("firstResult: {0}", firstResult); 38
  • 5 秒間隔で値を発行する IObservable<long>から最初の値を取得しています。(正確には Selectメソッドで発行された値を加工していますが)そして、First メソッドが呼び出される前と後にタ゗ムスタンプを表示するコードを入れています。このコードの実行結果を下記に示します。Timestamp 2012/01/03 18:25:45.783Timestamp 2012/01/03 18:25:50.829firstResult: value is 0三行目に、First メソッドで取得した値が出力されていることが確認できます。このように、Firstメソッドでは、IObservable<T>から最初に発行された値を取得できます。ここで注目したいのは、First メソッドの呼び出しの前後に入れているタ゗ムスタンプを表示するメソッドの表示内容です。18:25:45 から 18:25:50 となっていることからわかるように First メソッドの呼び出し前と呼び出し後で 5 秒たっていることがわかります。First メソッドの特徴として、最初の値が IObservable<T>のシーケンスから発行されるまで、実行しているスレッドをブロックするという特徴があります。 そのため非同期処理などの結果を待つのに使用することも可能です。 (基本はブロックしないように作るのが一番いいです)また、下記のコードのように、 永久に値が発行されない IObservable<T>に対して First メソッドを呼び出すと終了しないプログラムになるので注意してください。 var s = new Subject<int>(); s.First(); // 最初の値が発行されるまで処理が止まる s.OnNext(10); // ここには永久に到達しないLast メソッドも、 First メソッドと同じような動きをします。Last メソッドの場合は OnCompletedが呼ばれない限り値を返さないので、永遠に終わらないタ゗マーや゗ベントに対して呼び出すと、そこでプログラムがフリーズしてしまうので注意してください。Last メソッドのコード例を下記に示します。 // Observable を作成前のタ゗ムスタンプを表示 Console.WriteLine("Timestamp {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now); var lastResult = Observable // 1 秒間隔で値を 5 つ発行する IObservable .Generate(0, i => i < 5, i => ++i, i => "value is " + i, i => TimeSpan.FromSeconds(1)) // 最後の値を取得 .Last(); // Last の実行が終わった後のタ゗ムスタンプを表示 Console.WriteLine("Timestamp {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now); // Last の実行結果を取得 Console.WriteLine("lastResult: {0}", lastResult);このコードでは value is 0~value is 4 までの 5 つの値を 1 秒間隔で実行するIObservable<string>のシーケンスの最後の値を取得しています。First メソッドと同様に Lastメソッドの呼び出し前と呼び出し後にタ゗ムスタンプを表示しています。 実行結果を下記に示します。 39
  • Timestamp 2012/01/03 18:38:58.143 Timestamp 2012/01/03 18:39:03.257 lastResult: value is 4 Last メソッドもタ゗ムスタンプの結果を確認するとわかるとおり、実行しているスレッドをブロ ックして結果を待機します。今回の例では、Generate メソッドが発行する最後の値の value is 4 を待っているため 5 秒間スレッドをブロックしています。4.2.1.1. 値が存在しない場合の First メソッドと Last メソッドの挙動 First メソッドと Last メソッドですが、値が存在する場合は、その値を返しますが、値が存在しな い場合には例外を発生させます。First メソッドの場合のコード例を下記に示します。 // 1 つも要素の無い IObservable var noElementsSequence = new Subject<string>(); noElementsSequence.OnCompleted(); try { // 要素が無いものに対して最初の要素を要求 var firstResult = noElementsSequence.First(); } catch (InvalidOperationException e) { // 何もないので例外が発生する Console.WriteLine("Exception: {0}", e.Message); } コードの実行結果を下記に示します。 Exception: Sequence contains no elements. このように、取得する要素が無い(空の IObservable<T>のシーケンス)場合は InvalidOperationException を発行します。Last メソッドについても同様です。以下にコード例 と実行結果を示します。 // 1 つも要素の無い IObservable var noElementsSequence = new Subject<string>(); noElementsSequence.OnCompleted(); try { // 最後の要素の取得 var lastResult = noElementsSequence.Last(); 40
  • } catch (InvalidOperationException e) { // 何もないので例外が発生する Console.WriteLine("Exception: {0}", e.Message); } Exception: Sequence contains no elements.4.2.1.2. First メソッドと Last メソッドのオーバーロード First メソッドと Last メソッドには、Func<T, bool>型のデリゲートを受け取るオーバーロード があります。これは、デリゲートが true を返す最初または最後の要素を返すという点以外は、通 常の First メソッドと Last メソッドと共通の動きをします。コードと実行結果については、割愛 します。4.2.2. FirstOrDefault メソッドと LastOrDefault メソッド ここでは、FirstOrDefault メソッドと LastOrDefault メソッドについて説明します。このメソッ ドも First メソッドや Last メソッドと同様に IObservable<T>のシーケンスから最初の値や最後 の値を取得するために使用します。 また、 値が取得できるまでの間はスレッドをブロックする点も 共通の動作になります。挙動が異なるのは、要素が存在しない IObservable<T>のシーケンスに 対して呼び出した場合になります。 First メソッドと Last メソッドが例外を発生させるのに対して、 この FirstOrDefault メソッドと LastOrDefault メソッドはデフォルト値を返します。コード例を 下記に示します。 まずは、値が取得できるケースの FirstOrDefault メソッドのコードです。 // Observable を作成前のタ゗ムスタンプを表示 Console.WriteLine("Timestamp {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now); var firstResult = Observable // 5 秒間隔で値を発行する .Interval(TimeSpan.FromSeconds(5)) .Select(i => "value is " + i) // 最初の値を取得 .FirstOrDefault(); // First の実行が終わった後のタ゗ムスタンプを表示 Console.WriteLine("Timestamp {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now); // 取得した値を表示 Console.WriteLine("firstResult: {0}", firstResult); First メソッドのコード例と異なる点はメソッドの呼び出しを FirstOrDefault に変更した点だけで す。実行結果を下記に示します。 Timestamp 2012/01/03 18:55:08.572 41
  • Timestamp 2012/01/03 18:55:13.623 firstResult: value is 0 実行結果も First メソッドと変わりません。次に、空の IObservable<T>のシーケンスに対して FirstOrDefault メソッドを呼び出した場合のコード例を下記に示します。 // 1 つも要素の無い IObservable var noElementsSequence = new Subject<string>(); noElementsSequence.OnCompleted(); // 最初の値 or デフォルト値を取得 var firstResult = noElementsSequence.FirstOrDefault(); // 結果を出力。この場合は null が表示される。 Console.WriteLine("firstResult: {0}", firstResult ?? "null"); このコードでは、FirstOrDefault メソッドの戻り値は string 型のデフォルト値である null になり ます。そのため、実行結果にも null が表示されます。実行結果を下記に示します。 firstResult: null LastOrDefault メソッドについても同様の動きとなるため、コード例と実行結果は割愛します。4.2.2.1. FirstOrDefault メソッドと LastOrDefault メソッドのオーバーロード FirstOrDefault メソッドと LastOrDefault メソッドにも First メソッドと Last メソッドと同様に Func<T, bool>型のデリゲートを受け取るオーバーロードがあります。こちらも、デリゲートが true を返す要素のみに絞る点以外は、挙動は同じためコード例と実行結果は割愛します。4.2.3. ElementAt メソッド ここでは、ElementAt メソッドについて説明します。ElementAt メソッドは゗ンデックスで指定 した要素を取得するメソッドになります。 First メソッドや Last メソッドが単一の値を返していた のに対して ElementAt メソッドの戻り値は IObservable<T>になります。ElementAt メソッド のシグネチャを下記に示します。 public static IObservable<T> ElementAt<T>( this IObservable<T> source, int index ) このようなシグネチャになっているため ElementAt の結果の値を処理する場合は、別途 First メ ソッドを呼ぶか Subscribe して OnNext で値を処理する必要があります。ElementAt メソッドの コード例を下記に示します。 // 待機用の WaitHandle var gate = new EventWaitHandle(false, EventResetMode.AutoReset); // ElementAt 前のタ゗ムスタンプを表示 42
  • Console.WriteLine("Timestamp {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now); Observable // 1 秒間隔で 5 回値を発行する .Generate(0, i => i < 5, i => ++i, i => "value is " + i, i => TimeSpan.FromSeconds(1)) // 3 番目の要素を取得する .ElementAt(3) // この一連のシーケンスの最後で待機しているスレッドを解放する .Finally(() => gate.Set()) // 購読 .Subscribe( // 値を表示する(null の場合は null と表示する) i => Console.WriteLine("elementAt3: {0}", i ?? "null"), // 例外が発生した場合は例外のメッセージを表示する ex => Console.WriteLine("Exception: {0}, {1}", ex.GetType().Name, ex.Message), // 完了を示すメッセージを表示する () => Console.WriteLine("OnCompleted")); // 一連のメソッドチェ゗ンが終わった時のタ゗ムスタンプを表示する Console.WriteLine("Timestamp {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now); // gate.Set()が呼ばれるまで停止 gate.WaitOne(); // gate.Set が呼ばれた後のタ゗ムスタンプを表示する Console.WriteLine("Timestamp {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now);Generate メソッドを使って value is 0~value is 4 までの 5 つの値を発行するIObservable<string>のシーケンスを作成して、 ElementAt(3)で 3 番目の要素を取得しています。そして、ElementAt の結果の IObservable<string>に対して Subscribe メソッドを呼び出して、結果を表示しています。コードの本題とは関係ありませんが、EventWaitHandle クラスを使って Generate メソッドで作成した IObservable<string>のシーケンスが終了するのを確実に待ち合わせています。これは、Generate メソッドや ElementAt メソッド, Subscribe メソッドの処理がバックグラウンドのスレッドで実行されているため、待ち合わせをしないと、Subscribe メソッドの処理が走る前にプログラムが終了してしまうためです。 EventWaitHandle クラスについての説明は MSDN を参照してください。http://msdn.microsoft.com/ja-jp/library/system.threading.eventwaithandle.aspxコードの実行結果を下記に示します。Timestamp 2012/01/03 19:13:33.498Timestamp 2012/01/03 19:13:33.538elementAt3: value is 3OnCompleted 43
  • Timestamp 2012/01/03 19:13:37.588 実行結果の 3 行目で value is 3 と 3 番目の値が表示されていることが確認できます。また、最初 の 2 つのタ゗ムスタンプの表示は、ElementAt を含む一連のメソッドチェ゗ンの前後で出力して いますが、ほとんど時間が経過していないことがわかります。そして、最後のタ゗ムスタンプの出 力までに 4 秒の時間が経過していることが確認できます。1 つの値が 1 秒間隔で発行されている ため 3 番目(0 オリジンなので正確には 4 番目)の値が発行されるまでの時間と合致しています。4.2.3.1. 値が存在しない場合の ElementAt メソッドの挙動 ElementAt で指定した゗ンデックスに値が存在しない場合の挙動について説明します。この場合 も First メソッドや Last メソッドと同様に例外が発生しますが、IObservable<T>のシーケンス 内で起きる例外なので Subscribe の OnError や Catch メソッドで捕捉することが可能です。コー ド例を下記に示します。 // 待機用の WaitHandle var gate = new EventWaitHandle(false, EventResetMode.AutoReset); // ElementAt 前のタ゗ムスタンプを表示 Console.WriteLine("Timestamp {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now); Observable // 3 要素しか発行しない .Generate(0, i => i < 2, i => ++i, i => "value is " + i, i => TimeSpan.FromSeconds(1)) // 3 番目の要素を取得する .ElementAt(3) // この一連のシーケンスの最後で待機しているスレッドを解放する .Finally(() => gate.Set()) // 購読 .Subscribe( // 値を表示する(null の場合は null と表示する) i => Console.WriteLine("elementAt3: {0}", i ?? "null"), // 例外が発生した場合は例外のメッセージを表示する ex => Console.WriteLine("Exception: {0}, {1}", ex.GetType().Name, ex.Message), // 完了を示すメッセージを表示する () => Console.WriteLine("OnCompleted")); // 一連のメソッドチェ゗ンが終わった時のタ゗ムスタンプを表示する Console.WriteLine("Timestamp {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now); // gate.Set()が呼ばれるまで停止 gate.WaitOne(); // gate.Set が呼ばれた後のタ゗ムスタンプを表示する Console.WriteLine("Timestamp {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now); 実行結果を下記に示します。 44
  • Timestamp 2012/01/03 19:20:57.826 Timestamp 2012/01/03 19:20:57.866 Exception: ArgumentOutOfRangeException, 指定された引数は、有効な値の範囲内にありません。 パラメーター名: index Timestamp 2012/01/03 19:21:00.053 実行結果からわかるように、ArgumentOutOfRangeException が発生します。4.2.4. ElementAtOrDefault メソッド ここでは、ElementAtOrDefault メソッドについて説明します。ElementAtOrDefault メソッドは 通常時の動作は ElementAt メソッドと同じになります。挙動が異なるのは、引数に範囲外の値を 渡すと ElementAt メソッドでは例外が発生していたのに対して、ElementAtOrDefault メソッド ではデフォルト値を発行するという点です。 ゗ンデックスに範囲外の値を渡したときの ElementAtOrDefault メソッドのコード例について下 記に示します。 // 待機用の WaitHandle var gate = new EventWaitHandle(false, EventResetMode.AutoReset); // ElementAt 前のタ゗ムスタンプを表示 Console.WriteLine("Timestamp {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now); Observable // 3 要素しか発行しない .Generate(0, i => i < 2, i => ++i, i => "value is " + i, i => TimeSpan.FromSeconds(1)) // 3 番目の要素を取得する .ElementAtOrDefault(3) // この一連のシーケンスの最後で待機しているスレッドを解放する .Finally(() => gate.Set()) // 購読 .Subscribe( // 値を表示する(null の場合は null と表示する) i => Console.WriteLine("elementAt3: {0}", i ?? "null"), // 例外が発生した場合は例外のメッセージを表示する ex => Console.WriteLine("Exception: {0}, {1}", ex.GetType().Name, ex.Message), // 完了を示すメッセージを表示する () => Console.WriteLine("OnCompleted")); // 一連のメソッドチェ゗ンが終わった時のタ゗ムスタンプを表示する Console.WriteLine("Timestamp {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now); // gate.Set()が呼ばれるまで停止 gate.WaitOne(); 45
  • // gate.Set が呼ばれた後のタ゗ムスタンプを表示する Console.WriteLine("Timestamp {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now); 0~2 の 3 つの要素しか発行していない IObservable<string>のシーケンスに対して ElementAtOrDefault(3)のように範囲外の値を要求しています。実行結果を下記に示します。 Timestamp 2012/01/03 19:27:17.89 Timestamp 2012/01/03 19:27:17.929 elementAt3: null OnCompleted Timestamp 2012/01/03 19:27:19.965 三行目に elementAt3: null と表示されていることから、範囲外の値を要求しても例外が発生せず にデフォルト値が発行されていることが確認できます。4.2.5. Single メソッド ここでは、Single メソッドについて説明します。Single メソッドは、名前のとおり単一の値を返 すメソッドになります。メソッドのシグネチャを下記に示します。 public static TSource Single<TSource>(this IObservable<TSource> source) このメソッドの特徴は、単一の要素が返ることが確定するまで結果を返さないことです。コード例 を下記に示します。 // 実行開始時間を出力 Console.WriteLine("Start {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now); var singleResult = Observable // 1 秒後に 1 つだけ値を発行する .Generate(1, i => i == 1, i => ++i, i => i, _ => TimeSpan.FromSeconds(1)) // 発行された値をダンプ .Do(i => Console.WriteLine("Dump {0:yyyy/MM/dd HH:mm:ss.FFF}, Value = {1}", DateTime.Now, i)) // 単一の値を取得する .Single(); // 結果の出力 Console.WriteLine("singleResult: {0}", singleResult); // 終了時の時間を出力 Console.WriteLine("End {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now); Generate メソッドを使い、1 秒後に 1 つだけ値を発行しています。そして、Do メソッドで発行 された値とタ゗ムスタンプを表示して Single メソッドにつなげています。実行結果を下記に示し ます。 Start 2012/01/09 23:14:19.68 Dump 2012/01/09 23:14:20.906, Value = 1 46
  • singleResult: 1End 2012/01/09 23:14:20.913 行目で Single メソッドの戻り値が表示されています。注目する点は、Start と End の間で 1 秒の差があることです。このことから、Single メソッドは値を返すまで実行中のスレッドをブロックすることが確認できます。もう1つの Single メソッドの特徴として、単一の要素が返らない(2 個以上や 0 個の場合)ことが確定した時点で InvalidOperationException の例外をスローします。この動作を示すコード例を下記に示します。 // 実行開始時間を出力 Console.WriteLine("Start {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now); try { var singleResult = Observable // 1 秒間隔で 2 つの値を出力 .Generate(0, i => i < 2, i => ++i, i => i, i => TimeSpan.FromSeconds(1)) // 発行された値を出力 .Do(i => Console.WriteLine("Dump {0:yyyy/MM/dd HH:mm:ss.FFF}, Value = {1}", DateTime.Now, i)) // 単一の値を取得する .Single(); // 結果を出力 Console.WriteLine("singleResult: {0}", singleResult); } catch (InvalidOperationException ex) { // 単一の値を取得しようとしたら 2 つ以上値が流れてきたので例外になる Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message); } // 終了時の時間を出力 Console.WriteLine("End {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now);実行結果を下記に示します。Start 2012/01/09 23:21:24.123Dump 2012/01/09 23:21:25.133, Value = 0Dump 2012/01/09 23:21:26.147, Value = 1InvalidOperationException: Sequence contains more than one element.End 2012/01/09 23:21:26.1472 つ目の Dump 後に InvalidOperationException がスローされていることが確認できます。 47
  • 4.2.5.1. Single メソッドのオーバーロード Single メソッドには First メソッドや Last メソッドや ElementAt メソッドと同様に、値のフゖル タリングを行うための Func<T, bool>型のデリゲートを渡すオーバーロードがあります。これを つかうことで、複数の値が発行される IObservable<T>のシーケンスから特定の条件に合致する 単一の要素を取り出すことが出来ます。メソッドのシグネチャを下記に示します。 public static TSource Single<TSource>(this IObservable<TSource> source, Func<TSource, bool> predicate) このオーバーロードを使ったコード例を下記に示します。 // 実行開始時間を出力 Console.WriteLine("Start {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now); var singleResult = Observable // 1 秒間隔で 0~4 の値を発行する .Generate( 0, i => i < 5, i => ++i, i => i, i => TimeSpan.FromSeconds(1)) // 発行された値を出力 .Do(i => Console.WriteLine("Dump {0:yyyy/MM/dd HH:mm:ss.FFF}, Value = {1}", DateTime.Now, i)) // 値が 3 のものを 1 つだけ取得したい .Single(i => i == 3); // 結果を出力 Console.WriteLine("singleResult: {0}", singleResult); // 終了時の時間を出力 Console.WriteLine("End {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now); Generate メソッドを使って 0~4 の値を 1 秒間隔で発行しています。そして、Single メソッドの 引数で値が 3 のものを取得するように指定しています。 このコードの実行結果を下記に示します。 Start 2012/01/09 23:40:46.204 Dump 2012/01/09 23:40:47.215, Value = 0 Dump 2012/01/09 23:40:48.229, Value = 1 Dump 2012/01/09 23:40:49.243, Value = 2 Dump 2012/01/09 23:40:50.257, Value = 3 Dump 2012/01/09 23:40:51.271, Value = 4 singleResult: 3 End 2012/01/09 23:40:51.272 0~4 の 5 つの値が発行されていますが、Single メソッドでは例外がスローされずに引数で指定し た 3 が取得できています。 48
  • このオーバーロードを使った場合も、 引数で指定した条件にあうものが複数ある場合や、 1つも条件にあわない場合は InvalidOperationException の例外がスローされます。コード例を下記に示します。 // 実行開始時間を出力 Console.WriteLine("Start {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now); try { var singleResult = Observable // 1 秒間隔で 0~4 の値を発行する .Generate( 0, i => i < 5, i => ++i, i => i, i => TimeSpan.FromSeconds(1)) // 発行された値を出力 .Do(i => Console.WriteLine("Dump {0:yyyy/MM/dd HH:mm:ss.FFF}, Value = {1}", DateTime.Now, i)) // 値が 10 より大きいものを 1 つだけ取得したい .Single(i => i > 10); // 結果を出力 Console.WriteLine("singleResult: {0}", singleResult); } catch (InvalidOperationException ex) { // 単一の値を取得しようとしたら 1 つも値が無かったのでエラー Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message); } // 終了時の時間を出力 Console.WriteLine("End {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now);0~4 の値が発行される IObservable<int>のシーケンスに対して 10 以上の値を 1 つ取得するように Single メソッドを呼び出しています。そのため InvalidOperationException の例外が発生します。実行結果を下記に示します。Start 2012/01/09 23:46:39.903Dump 2012/01/09 23:46:40.917, Value = 0Dump 2012/01/09 23:46:41.971, Value = 1Dump 2012/01/09 23:46:42.976, Value = 2Dump 2012/01/09 23:46:43.989, Value = 3Dump 2012/01/09 23:46:45.013, Value = 4InvalidOperationException: Sequence contains no elements.End 2012/01/09 23:46:45.014 49
  • 4.2.6. SingleOrDefault メソッド ここでは、SingleOrDefault メソッドについて説明します。このメソッドは Single メソッドと同 様に IObservable<T>のシーケンスから単一の値を取得するために使用します。Single メソッド と異なる点は、1 つも値が取得できなかった場合に、デフォルト値を返すところです。まずは、正 常系の動作のコードを下記に示します。 // 実行開始時間を出力 Console.WriteLine("Start {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now); var singleResult = Observable // 1 秒間隔で 0~4 の値を発行する .Generate( 0, i => i < 5, i => ++i, i => i, i => TimeSpan.FromSeconds(1)) // デフォルト値を null にしたいので string 型に変換 .Select(i => i.ToString()) // 発行された値を出力 .Do(i => Console.WriteLine("Dump {0:yyyy/MM/dd HH:mm:ss.FFF}, Value = {1}", DateTime.Now, i)) // 値が”3"のものを 1 つだけ取得したい .SingleOrDefault(i => i == "3"); // 結果を出力 Console.WriteLine("singleResult: {0}", singleResult ?? "null"); // 終了時の時間を出力 Console.WriteLine("End {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now); 実行結果を下記に示します。 Start 2012/01/09 23:54:25.959 Dump 2012/01/09 23:54:26.971, Value = 0 Dump 2012/01/09 23:54:27.984, Value = 1 Dump 2012/01/09 23:54:28.998, Value = 2 Dump 2012/01/09 23:54:30.014, Value = 3 Dump 2012/01/09 23:54:31.028, Value = 4 singleResult: 3 End 2012/01/09 23:54:31.029 次に、1 つも値が取得できないケースのコード例を下記に示します。 // 空の IObservable シーケンス var s = new Subject<string>(); s.OnCompleted(); 50
  • // 1 つも値が取得できない場合は null が返る var singleResult = s.SingleOrDefault(); // 結果を出力 Console.WriteLine("singleResult: {0}", singleResult ?? "null");このコードの実行結果を下記に示します。singleResult: nullSingle メソッドでは InvalidOperationException が発生していたケースですが、SingleOrDefaultを使うとデフォルト値(今回の例では string 型なので null)を返すことが確認できます。注意点は、SingleOrDefault メソッドを使用した場合でも複数の値が取得できるケースではデフォルト値ではなく InvalidOperationException が発生するところです。この挙動が確認できるコード例を下記に示します。 // 実行開始時間を出力 Console.WriteLine("Start {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now); try { var singleResult = Observable // 1 秒間隔で 0~4 の値を発行する .Generate( 0, i => i < 5, i => ++i, i => i, i => TimeSpan.FromSeconds(1)) // デフォルト値を null にしたいので string 型に変換 .Select(i => i.ToString()) // 発行された値を出力 .Do(i => Console.WriteLine("Dump {0:yyyy/MM/dd HH:mm:ss.FFF}, Value = {1}", DateTime.Now, i)) // 値を 1 つだけ取得したい .SingleOrDefault(); // 結果を出力 Console.WriteLine("singleResult: {0}", singleResult ?? "null"); } catch (InvalidOperationException ex) { // SingleOrDefault メソッドを使っても複数の値が取得できてしまうケースでは例外になる Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message); } // 終了時の時間を出力 Console.WriteLine("End {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now); 51
  • 0~4 の値を 1 秒間隔で発生させて SingleOrDefault メソッドを呼び出しています。実行結果を下 記に示します。 Start 2012/01/10 00:02:36.047 Dump 2012/01/10 00:02:37.063, Value = 0 Dump 2012/01/10 00:02:38.077, Value = 1 InvalidOperationException: Sequence contains more than one element. End 2012/01/10 00:02:38.077 2 つ目の値が発行されたタ゗ミングで例外が発生していることが確認できます。4.2.6.1. SingleOrDefault メソッドのオーバーロード SingleOrDefault メソッドにも Single メソッドと同様に Func<T, bool>型のデリゲートを受け取 るオーバーロードがあります。このメソッドの挙動は、Single メソッドの時と同様に引数で渡し たデリゲートでフゖルタリングを行いつつ、 SingleOrDefault のコード例で示したように 1 つも値 が取得できなかった場合にデフォルト値を返します。 動作は「4.2.6 SingleOrDefault メソッド」と、「4.2.5.1 Single メソッドのオーバーロード」で 示したものと、ほぼ同じような形になるため、コード例については割愛します。4.3. 値と飛ばす、値を拾うメソッド ここでは、指定した数や条件に応じて IObservable<T>のシーケンスから値を飛ばしたり、拾っ たりするメソッドについて説明します。4.3.1. Skip と Take メソッド LINQ の基本のメソッドの Where と Select の動作を確認したので、次は Skip と Take を使用し たいと思います。Skip は、指定した数だけ値を読み飛ばして、Take は指定した数だけ値を通過さ せる゗メージです。コード例を下記に示します。 // 値の発行元になる Subject var s = new Subject<int>(); // 最初の 3 つをスキップして、その後 3 つを通知する s.Skip(3).Take(3).Subscribe( i => Console.WriteLine("OnNext({0})", i), ex => Console.WriteLine("OnError({0})", ex.Message), () => Console.WriteLine("OnCompleted()")); // 1-10 の値を流し込む Observable.Range(1, 10).ForEach(i => s.OnNext(i)); 52
  • Skip(3)をして Take(3)をしているところに 1~10 の値を発行しているので 4, 5, 6 が Subscribe しているところに通知されます。実行結果を下記に示します。 OnNext(4) OnNext(5) OnNext(6) OnCompleted() 注目するのは、6 が発行されたあとに OnCompleted が呼ばれている点です。Take(3)で値が 3 つ 通過した後には、もう値が発行されないので、完了通知が発行されます。4.3.2. Repeat メソッドとの組み合わせ Skip と Take の動作自体は非常にシンプルなので、 これに Repeat を組み合わせてみようと思いま す。Repeat 自体は 3.2.3.1 で紹介しているとおり、指定した回数繰り返しを行うメソッドです。 Skip と Take と組み合わせることで下記のような動作をさせることが出来ます。コードを下記に 示します。 // 値の発行元になる Subject var s = new Subject<int>(); // 最初の 3 つをスキップして、その後 3 つを通知することを繰り返す s.Skip(3).Take(3).Repeat().Subscribe( i => Console.WriteLine("OnNext({0})", i), ex => Console.WriteLine("OnError({0})", ex.Message), () => Console.WriteLine("OnCompleted()")); // 1-20 の値を流し込む Observable.Range(1, 20).ForEach(i => s.OnNext(i)); 先ほどのコードの Take の後に Repeat を追加しています。引数の無い Repeat メソッドは、無限 に繰り返しを行います。どのような動きになるのか実行結果を下記に示します。 OnNext(4) <- 最初の Skip(3).Take(3) OnNext(5) <- 最初の Skip(3).Take(3) OnNext(6) <- 最初の Skip(3).Take(3) OnNext(10) <- 2 回目の Skip(3).Take(3) OnNext(11) <- 2 回目の Skip(3).Take(3) OnNext(12) <- 2 回目の Skip(3).Take(3) OnNext(16) <- 3 回目の Skip(3).Take(3) OnNext(17) <- 3 回目の Skip(3).Take(3) OnNext(18) <- 3 回目の Skip(3).Take(3) 53
  • Repeat を追加することで 3 つ値をスキップして 3 つ値を発行するという動作を繰り返しています。 このように IObservable<T>の拡張メソッドを組み合わせることで様々な IObservable<T>を定 義することが出来ます。4.3.3. SkipWhile と TakeWhile メソッド 次は、SkipWhile と TakeWhile メソッドについてみていきます。このメソッドは Func<T, bool> のデリゲートを受け取ります。このデリゲートが true を返す間は Skip したり Take をします。動 作確認のためのコードを下記に示します。 // 値の発行元になる Subject var s = new Subject<int>(); // 発行される値が 5 より小さい間はスキップして 10 より小さい間は通過させる s.SkipWhile(i => i < 5).TakeWhile(i => i < 10).Subscribe( i => Console.WriteLine("OnNext({0})", i), ex => Console.WriteLine("OnError({0})", ex.Message), () => Console.WriteLine("OnCompleted()")); // 1-20 の値を流し込む Console.WriteLine("1-20 OnNext start."); Observable.Range(1, 20).ForEach(i => s.OnNext(i)); Console.WriteLine("1-20 OnNext end."); Console.WriteLine(); 5 より小さい間はスキップして 10 より小さい間は通過させるので 5,6,7,8,9 が Subscribe まで通 知されます。実行結果を下記に示します。 1-20 OnNext start. OnNext(5) OnNext(6) OnNext(7) OnNext(8) OnNext(9) OnCompleted() 1-20 OnNext end. この例も、 が発行された段階で値が発行されなくなるため OnCompleted が通知されていること 9 が確認できます。コードでは示しませんが、この例でも Repeat メソッドを組み合わせることで任 意の回数この挙動を繰り返すことが出来ます。 54
  • 4.3.4. SkipUntil と TakeUntil メソッド 次は LINQ には定義されていない Reactive Extensions 固有のメソッドの SkipUntil と TakeUntil メソッドについてみていきます。 SkipUntil と TakeUntil メソッドのシグネチャを下記に示します。 public IObservable<T> SkipUntil<T, TOther>(IObservable<TOther> other); public IObservable<T> TakeUntil<T, TOther>(IObservable<TOther> other); どちらも引数に IObservable<TOther>を受け取ります。SkipUntil は引数で渡された IObservable<TOther>から値が発行されるまで、値のスキップを行います。TakeUntil は引数で 渡された IObservable<TOther>から値が発行されるまで、値を通過させます。コード例を下記 に示します。 // 値の発行元になる Subject var s = new Subject<int>(); // 値のスキップを辞めるきっかけ var startTrigger = new Subject<Unit>(); // 値を後続に流すことを辞めるきっかけ var endTrigger = new Subject<Unit>(); // startTrigger の OnNext が呼ばれるまで値をスキップして endTrigger の OnNext が // 呼ばれるまで後続に値を流す。 s.SkipUntil(startTrigger).TakeUntil(endTrigger).Subscribe( i => Console.WriteLine("OnNext({0})", i), ex => Console.WriteLine("OnError({0})", ex.Message), () => Console.WriteLine("OnCompleted()")); // 1-5 の値を流し込む Console.WriteLine("1-5 OnNext start."); Observable.Range(1, 5).ForEach(i => s.OnNext(i)); Console.WriteLine("1-5 OnNext end."); Console.WriteLine(); // startTrigger の OnNext を発行してから 1-5 の値を流し込む Console.WriteLine("startTrigger.OnNext called."); startTrigger.OnNext(Unit.Default); Console.WriteLine("1-5 OnNext start."); Observable.Range(1, 5).ForEach(i => s.OnNext(i)); Console.WriteLine("1-5 OnNext end."); Console.WriteLine(); 55
  • // endTrigger の OnNext を発行してから 1-5 の値を流し込む Console.WriteLine("endTrigger.OnNext called."); endTrigger.OnNext(Unit.Default); Console.WriteLine("1-5 OnNext start."); Observable.Range(1, 5).ForEach(i => s.OnNext(i)); Console.WriteLine("1-5 OnNext end."); Console.WriteLine(); コードは長いですがポ゗ントは、startTrigger が SkipUntil に渡す IObservable<T>で、 endTrigger が TakeUntil に渡す IObservable<T>になる点です。そして、startTrigger と endTrigger の OnNext が呼ばれた際に発行された値が Subscribe に通知されるようになったり、 通知されなくなったりする点に注目してください。実行結果を下記に示します。 1-5 OnNext start. 1-5 OnNext end. startTrigger.OnNext called. 1-5 OnNext start. OnNext(1) OnNext(2) OnNext(3) OnNext(4) OnNext(5) 1-5 OnNext end. endTrigger.OnNext called. OnCompleted() 1-5 OnNext start. 1-5 OnNext end. startTrigger の OnNext を呼ぶ前に発行した値は全てスキップされて、startTrigger の OnNext を呼ぶと値が通知されて OnNext(1)~OnNext(5)までが表示されています。endTrigger の OnNext を呼ぶと OnCompleted が呼ばれて終了状態になり、以降に発行された値は Subscribe まで通知が届いていないことが確認できます。4.3.5. SkipUntil と TakeUntil を使ったドラッグの処理 ここでは、SkipUntil と TakeUntil を使って WPF ゕプリケーションでマウスのドラッグを処理し てみます。通常のゕプリケーションでは恐らく MouseDown, MouseUp, MouseMove の゗ベント を処理して実現すると思います。MouseDown でフラグ変数を立ててマウスをキャプチャし、 MouseUp でフラグ変数を下げてマウスのキャプチャを解放します。そして、MouseMove ゗ベン トではフラグ変数を見てマウスボタンが押されているかどうか判定してドラッグ中の処理を記述 56
  • します。このように単純なドラッグの処理を行うだけでも、 フラグを使用したプログラムになってしまいます。Reactive Extensions を使うと゗ベントを組み合わせて下記のように記述出来ます。まず、MouseDown, MouseUp, MouseMove の゗ベントを FromEvent メソッドを使ってIObservable に変換します。 // マウスダウン、マウスゕップ、マウスムーブの IObservable を作る var mouseDown = Observable.FromEvent<MouseButtonEventHandler, MouseButtonEventArgs>( h => (s, e) => h(e), h => this.MouseDown += h, h => this.MouseDown -= h); var mouseMove = Observable.FromEvent<MouseEventHandler, MouseEventArgs>( h => (s, e) => h(e), h => this.MouseMove += h, h => this.MouseMove -= h); var mouseUp = Observable.FromEvent<MouseButtonEventHandler, MouseButtonEventArgs>( h => (s, e) => h(e), h => this.MouseUp += h, h => this.MouseUp -= h);ドラッグ処理は、 マウスが押された状態でマウスが動いている間なので、 SkipUntil で MouseMove゗ベントを MouseDown が行われるまで読み飛ばし、TakeUntil で MouseUp が行われるまで Takeすることで表しています。これだけだと、一度きりの処理なので Repeat メソッドを使って繰り返し処理をするようにしています。コードを下記に示します。 var drag = mouseMove // マウスムーブをマウスダウンまでスキップ。マウスダウン時にマウスをキャプチャ .SkipUntil(mouseDown.Do(_ => this.CaptureMouse())) // マウスゕップが行われるまで Take。マウスゕップでマウスのキャプチャをリリース .TakeUntil(mouseUp.Do(_ => this.ReleaseMouseCapture())) // ドラッグが終了したタ゗ミングで Completed を表示 .Finally(() => textBlock.Text = "Completed") // これを繰り返す .Repeat();TakeUntil の後に Finally メソッドという使ったことの無いメソッドを使用していますが、このメソッドは IObservable<T>のシーケンスが終了したタ゗ミングでしたい処理を記載することが出来るメソッドです。こうすることで、一回のドラッグが終了したタ゗ミングで TextBlock にCompleted と表示するようにしています。その他に、mouseDown と mouseUp で使用しているDo メソッドは、 純粋に IObservable<T>のシーケンスに値が流れてきた時に処理を行うためのメソッドです。ここで、マウスのキャプチャとキャプチャのリリースを行っています。 57
  • 最後に、この drag を Subscribe してドラッグ中の処理を記述します。 // ドラッグ中は、゗ベント引数から座標を取り出して表示用に整えて TextBlock に設定 drag.Select(e => e.GetPosition(null)) .Select(p => string.Format("X: {0}, Y:{1}", p.X, p.Y)) .Subscribe(s => textBlock.Text = s); ゗ベント引数から座標を取り出し、座標を表示用文字列に整形して、TextBlock に表示させていま す。因みに、 上記の処理は Window1.xaml.cs のコンストラクタに記載しています。Window1.xaml の XAML は下記のようになっています。 <Window x:Class="DragSample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid Name="layoutRoot"> <TextBlock Name="textBlock" /> </Grid> </Window>4.3.5.1. 実行結果 このプログラムを実行すると、下記のように何も表示されないウゖンドウが表示されます。 このウゖンドウ上でドラッグを行うと下図のようにドラッグしている箇所の座標がリゕルタ゗ム で表示されます。 58
  • ドラッグを終了すると下図のように Completed と表示されます。このように SkipUntil と TakeUntil と Repeat, Select などのこれまで紹介してきたメソッドを組み合わせることで通常はフラグなどを使用して記載する処理を非常にシンプルに記載できました。最後に、MainWindow.xaml.cs のコードの全体を下記に示します。 namespace DragSample { using System; using System.Reactive.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; /// <summary> /// MainWindow.xaml の相互作用ロジック /// </summary> public partial class MainWindow : Window 59
  • { public MainWindow() { InitializeComponent(); // マウスダウン、マウスゕップ、マウスムーブの IObservable を作る var mouseDown = Observable.FromEvent<MouseButtonEventHandler,MouseButtonEventArgs>( h => (s, e) => h(e), h => this.MouseDown += h, h => this.MouseDown -= h); var mouseMove = Observable.FromEvent<MouseEventHandler, MouseEventArgs>( h => (s, e) => h(e), h => this.MouseMove += h, h => this.MouseMove -= h); var mouseUp = Observable.FromEvent<MouseButtonEventHandler,MouseButtonEventArgs>( h => (s, e) => h(e), h => this.MouseUp += h, h => this.MouseUp -= h); var drag = mouseMove // マウスムーブをマウスダウンまでスキップ。マウスダウン時にマウスをキャプチャ .SkipUntil(mouseDown.Do(_ => this.CaptureMouse())) // マウスゕップが行われるまで Take。マウスゕップでマウスのキャプチャをリリース .TakeUntil(mouseUp.Do(_ => this.ReleaseMouseCapture())) // ドラッグが終了したタ゗ミングで Completed を表示 .Finally(() => textBlock.Text = "Completed") // これを繰り返す .Repeat(); // ドラッグ中は、゗ベント引数から座標を取り出して表示用に整えて TextBlock に設定 drag.Select(e => e.GetPosition(null)) .Select(p => string.Format("X: {0}, Y:{1}", p.X, p.Y)) .Subscribe(s => textBlock.Text = s); 60
  • } } }4.3.6. SkipLast と TakeLast メソッド ここでは、SkipLast メソッドと TakeLast メソッドについて説明します。SkipLast メソッドと TakeLast メソッドは名前が示す通り IObservable<T>のシーケンスの最後から指定した数の値 を Skip したり Take したりするメソッドです。メソッドのシグネチャは下記に示す通り、どちら も何個の値を Skip するのか Take するのかを指定する数を渡します。 public static IObservable<T> SkipLast<T>(this IObservable<T> source, int count); public static IObservable<T> TakeLast<T>(this IObservable<T> source, int count); SkipLast メソッドの使用例を下記に示します。 Observable // 1~10 の値を発行する .Range(1, 10) // 最後 3 つを Skip .SkipLast(3) // 購読して表示 .Subscribe( i => Console.WriteLine("OnNext({0})", i), () => Console.WriteLine("OnCompleted()")); このコードは、1~10 の値を発行して最後の 3 つの値を Skip しています。実行結果を以下に示し ます。8,9,10 の最後の 3 つの値が表示されていないことが確認できます。 OnNext(1) OnNext(2) OnNext(3) OnNext(4) OnNext(5) OnNext(6) OnNext(7) OnCompleted() 次に TakeLast メソッドの使用例を下記に示します。 Observable // 1~10 の値を発行する .Range(1, 10) // 最後 3 つを Take 61
  • .TakeLast(3) // 購読して表示 .Subscribe( i => Console.WriteLine("OnNext({0})", i), () => Console.WriteLine("OnCompleted()")); このコードは、1~10 の値を発行して最後の 3 つを拾っています。実行結果を以下に示します。 SkipLast の例では飛ばされていた 8,9,10 が表示されていることが確認できます。 OnNext(8) OnNext(9) OnNext(10) OnCompleted() このように、SkipLast メソッドと TakeLast メソッドを使うことで IObservable<T>のシーケン スの最後を起点にして Skip したり Take をすることが出来ます。そのため、永遠に終わらない IObservable<T>のシーケンスに対して、このメソッドを呼び出すと永遠に結果が返ってこない ので注意が必要です。4.4. Do メソッド ここでは、Do メソッドについて説明します。Do メソッドは IObservable<T>から発行された値 を受けて処理を行うだけのメソッドです。4.3.5 の Drag 処理でも使用しましたが、要素を受け取 って処理を行い IObservable<T>の要素に対しては何も加工を行ったりしません。コード例を下 記に示します。 var subject = new Subject<int>(); subject // 途中に処理を挟む .Do(i => Console.WriteLine("Do : {0}", i)) // 購読(購読しないと OnNext をしても値が流れないね) .Subscribe(i => Console.WriteLine("OnNext : {0}", i)); // 値の発行 subject.OnNext(1); subject.OnNext(2); subject.OnNext(3); このコードを実行すると、Subscribe で値を購読している処理の前に Do の処理が行われているこ とが確認出来ます。 Do : 1 OnNext : 1 Do : 2 62
  • OnNext : 2Do : 3OnNext : 3Do メソッドには、 OnNext に対応する処理だけではなく OnError や OnCompleted 時の処理を指定することも出来ます。このオーバーロードは Subscribe と同じで OnError にはAction<Exception>を、OnCompleted には引数なしの Action を渡します。OnError の場合のコード例を下記に示します。 var subject = new Subject<int>(); subject // 途中に処理を挟む .Do( i => Console.WriteLine("Do : OnNext : {0}", i), ex => Console.WriteLine("Do : OnError : {0}", ex), () => Console.WriteLine("Do : OnCompleted")) // 購読(購読しないと OnNext をしても値が流れないね) .Subscribe( i => Console.WriteLine("OnNext : {0}", i), ex => Console.WriteLine("OnError : {0}", ex), () => Console.WriteLine("OnCompleted")); // 値の発行 subject.OnNext(1); subject.OnError(new Exception());実行結果を下記に示します。Do : OnNext : 1OnNext : 1Do : OnError : System.Exception: 種類 System.Exception の例外がスローされました。OnError : System.Exception: 種類 System.Exception の例外がスローされました。最後に OnCompleted の場合のコード例を下記に示します。 var subject = new Subject<int>(); subject .Do( i => Console.WriteLine("Do : OnNext : {0}", i), ex => Console.WriteLine("Do : OnError : {0}", ex), () => Console.WriteLine("Do : OnCompleted")) // 購読(購読しないと OnNext をしても値が流れないね) .Subscribe( i => Console.WriteLine("OnNext : {0}", i), 63
  • ex => Console.WriteLine("OnError : {0}", ex), () => Console.WriteLine("OnCompleted")); // 値の発行 subject.OnNext(1); subject.OnCompleted(); 実行結果を下記に示します。 Do : OnNext : 1 OnNext : 1 Do : OnCompleted OnCompleted このように、 メソッドを使うと IObservable<T>のシーケンスを処理する途中に任意のゕクシ Do ョンを実行できます。4.4.1. Do メソッド使用時の注意点 Do メソッドは Reactive Extensions の処理の中に任意のゕクションを実行できるという便利なメ ソッドですが、本来は外部に対して副作用を起こさない Reactive Extensions の処理の中で副作 用を起こすためのメソッドになります。そのため、Do メソッドの利用は必要最低限にとどめてく ださい。Do メソッド以外の代替メソッドがある場合はそれを使用するように心がけましょう。ま た、 複数の Do メソッドでフラグ変数を共有して挙動を変えるなど、 動作が複雑化してきた場合は、 一度立ち止まり本当にそれが必要か考えるようにすることをお勧めします。4.5. エラー処理関連のメソッド ここでは、IObservable<T>のシーケンス内で例外が発生したときの挙動に関わるメソッドにつ いて紹介します。4.5.1. Catch メソッド ここでは、例外処理を行うための Catch メソッドについて紹介します。例外処理については、こ れまでも OnError で処理をするという方法をとってきましたが、Catch メソッドを使うことでエ ラーの際にリトラ゗を行ったり、任意の値を後ろに対して流すといったことが出来ます。 Catch メソッドには、いくつかオーバーラ゗ドがありますが一番単純なものは、IObservable<T> を引数に渡すものになります。メソッドのシグネチャを下記に示します。 IObservable<TSource> Catch<TSource>(IObservable<TSource> ) 使用例を下記に示します。 var source = new Subject<string>(); source // source から例外が発生したら Error という文字列を後続へ流す 64
  • .Catch(Observable.Return("Error")) // 購読 .Subscribe( s => Console.WriteLine("OnNext {0}", s), ex => Console.WriteLine("OnError {0}", ex), () => Console.WriteLine("OnCompleted")); // 2 つ値を発行したあと、例外を流して、その後にもう1つ値を発行する。 source.OnNext("A"); source.OnNext("B"); source.OnError(new Exception("例外")); source.OnNext("C");実行結果を下記に示します。OnNext AOnNext BOnNext Error <- 例外が発生したので Error という文字列が Subscribe にわたってきているOnCompleted <- その後、正常終了。実行結果からもわかるとおり、Catch メソッドに Observable.Return(“Error”)を渡すことで、エラー時には Error という文字列を後続に対して流すようにしています。 ここでは固定の値を返していますが IObservable<T>(この場合は IObservable<string>)であれば何を返してもいいので、ここまでに紹介してきたメソッドを使って非同期処理を呼び出すことも、 別の゗ベントの発火を待つことも可能です。また、この Catch メソッドに渡す IObservable<T>が終了したタ゗ミングで OnCompleted が呼ばれることも上記サンプルから確認できます。 そのためエラー時に必ず終了させるといった処理を行いたい場合は、空の IObservable<T>を返します。コード零を下記に示します。 var source = new Subject<string>(); source // エラーが起きたら終了する .Catch(Observable.Empty<string>()) // 購読 .Subscribe( s => Console.WriteLine("OnNext {0}", s), ex => Console.WriteLine("OnError {0}", ex), () => Console.WriteLine("OnCompleted")); // 2 つ値を発行したあと、例外を流して、その後にもう1つ値を発行する。 source.OnNext("A"); 65
  • source.OnNext("B"); source.OnError(new Exception("例外")); source.OnNext("C");実行結果を下記に示します。OnNext AOnNext BOnCompletedObservable.Return を使用した時とは違い、エラーが発生したタ゗ミングで OnCompleted が呼ばれていることがわかります。Catch メソッドには、上記以外のシグネチャのメソッド以外に Func<TException,IObservable<TSource>>の形式のデリゲートを渡すオーバーラ゗ドがあります。このオーバーラ゗ドを使うと通常の例外処理の catch 句のように型指定で例外時の処理を振り分けることが出来ます。コード例を下記に示します。 var source = new Subject<string>(); source // ArgumentException 発生時には ArgumentException からの復帰という文字列を後続に流す .Catch((ArgumentException ex) => Observable.Return("ArgumentException から復帰")) // NullReferenceException 発生時には何もせず終了 .Catch((NullReferenceException ex) => Observable.Empty<string>()) // 購読 .Subscribe( s => Console.WriteLine("OnNext {0}", s), ex => Console.WriteLine("OnError {0}", ex), () => Console.WriteLine("OnCompleted")); // 2 つ値を発行したあと、例外を流して、その後にもう1つ値を発行する。 source.OnNext("A"); source.OnNext("B"); source.OnError(new ArgumentException("例外")); // ## 1 // source.OnError(new NullReferenceException("例外")); // ## 2 // source.OnError(new Exception("例外")); // ## 3 source.OnNext("C");上記の例では、2 つの Catch メソッドで ArgumentException と NullReferenceException の場合のエラー処理を行っています。 ArgumentException 発生時には ArgumentException が発生した旨を文字列で後続に対して流すようにしています。NullReferenceException の場合は、即座にシーケンスを終了するようにしています。コードの後半の## 1, ## 2, ## 3 のコメントのある行のどれか1行だけ有効にした状態で実行することで、 Catch メソッドの動作を確認できます。 ま 66
  • ず、## 1 の行をコメントゕウトして ArgumentException を発生させた場合の実行例を下記に示 します。 OnNext A OnNext B OnNext ArgumentException から復帰 OnCompleted 期待したとおり、 ArgumentException から復帰という文字列が OnNext にわたってきていること が確認できます。次に、## 1 の行をコメントにして## 2 の行のコメントを外して NullReferenceException を発生させたケースの実行例を下記に示します。 OnNext A OnNext B OnCompleted こちらは、Observable.Empty<string>()を返しているため、例外が発生したタ゗ミングで OnCompleted が呼ばれていることがわかります。最後に、## 3 のコメントを外して Exception を発生させたケースの実行例を下記に示します。 OnNext A OnNext B OnError System.Exception: 例外 この場合は、どの Catch メソッドでも処理されないため Subscribe の OnError まで例外が伝搬さ れています。このように Reactive Extensions では例外に対して複数の対処方法を用意していま す。この中から、状況に応じて最適な例外処理選択して使用します。4.5.2. Finally メソッド Catch メソッドの次は、Finally メソッドの紹介を行います。Finally メソッドは名前の通り例外処 理の finally 句と同じ動きをします。Reactive Extensions の IObservable<T>を起点とする一連 のシーケンスの処理が終わったタ゗ミングで呼び出される処理を記述することが出来ます。 引数に は Action を渡すだけのシンプルなシグネチャです。 IObservable<TSource> Finally(Action action) まずは、正常にシーケンスの処理が終わる場合のコード例を下記に示します。 var source = new Subject<string>(); source // 終了時に必ず呼ばれる処理 .Finally(() => Console.WriteLine("Finally")) // 購読 .Subscribe( s => Console.WriteLine("OnNext {0}", s), ex => Console.WriteLine("OnError {0}", ex), 67
  • () => Console.WriteLine("OnCompleted")); // 2 つ値を発行したあと、終了する。 source.OnNext("A"); source.OnNext("B"); source.OnCompleted();実行結果を下記に示します。OnNext AOnNext BOnCompletedFinallyFinally が、全ての処理の最後に呼ばれていることが確認出来ます。次に、Catch メソッドを使用して例外を処理したケースのコードを下記に示します。 var source = new Subject<string>(); source // source から例外が発生したら Error という文字列を後続へ流す .Catch(Observable.Return("Error")) // 終了時に必ず呼ばれる処理 .Finally(() => Console.WriteLine("Finally")) // 購読 .Subscribe( s => Console.WriteLine("OnNext {0}", s), ex => Console.WriteLine("OnError {0}", ex), () => Console.WriteLine("OnCompleted")); // 2 つ値を発行したあと、例外を発生させる。 source.OnNext("A"); source.OnNext("B"); source.OnError(new Exception("例外"));実行結果を下記に示します。OnNext AOnNext BOnNext ErrorOnCompletedFinally 68
  • この場合も Catch メソッドでエラーから復帰しているため、OnCompleted の後に Finally が呼ばれていることが確認できます。次に、Catch メソッドを使用せずに Subscribe の OnError で例外処理をした場合の動作確認をするコードを下記に示します。 var source = new Subject<string>(); source // 終了時に必ず呼ばれる処理 .Finally(() => Console.WriteLine("Finally")) // 購読 .Subscribe( s => Console.WriteLine("OnNext {0}", s), ex => Console.WriteLine("OnError {0}", ex), () => Console.WriteLine("OnCompleted")); // 2 つ値を発行したあと、例外を発生させる。 source.OnNext("A"); source.OnNext("B"); source.OnError(new Exception("例外"));実行結果を下記に示します。OnNext AOnNext BOnError System.Exception: 例外Finallyこの場合も、OnError などの Subscribe で行われる処理の後に Finally で指定した処理が実行されていることがわかります。最後に、OnError でも Catch メソッドでも例外を処理しなかった場合の動作を確認するコードを下記に示します。 var source = new Subject<string>(); var subscriber = source // 終了時に必ず呼ばれる処理 .Finally(() => Console.WriteLine("Finally")) // 購読(エラー処理無し) .Subscribe( s => Console.WriteLine("OnNext {0}", s)); // 2 つ値を発行したあと、例外を発生させる。 source.OnNext("A"); source.OnNext("B"); try 69
  • { // Reactive Extensions のシーケンスの処理内で例外が処理されないため例外がスローされる source.OnError(new Exception("例外")); } catch { // 例外が発生した場合はログを出して握りつぶす Console.WriteLine("catch 句"); } // 購読を解除する Console.WriteLine("subscriber.Dispose()"); subscriber.Dispose(); 今回は、OnError でも Catch メソッドでも例外を処理していないため source.OnError(new Exception(“例外”))の部分で例外が発生します。そのため try catch で例外処理を記載しています。 また、例外処理のあとで、Subscribe したものを Dispose して購読を解除しています。このコー ドの実行結果を下記に示します。 OnNext A OnNext B catch 句 subscriber.Dispose() Finally 上記の実行結果からわかるように、未処理の例外がある場合は、何もしないと Finally メソッドで 渡したゕクションが実行されません。明示的にコードで Dispose をすることで Finally で渡したゕ クションが実行されます。4.5.3. OnErrorResumeNext メソッド ここでは、OnErrorResumeNext メソッドについて説明します。このメソッドは IObservable<T> のシーケンスで例外が発生した際に、どのように IObservable<T>のシーケンスを再開するのか を指定します。IObservable<T>の拡張メソッドとして定義されているものは「4.5.1 Catch メソ ッド」で説明したものと同じように、例外発生時に別の IObservable<T>で処理を続けるという ことが出来ます。 コード例を下記に示します。 Observable // 例外を出す .Throw<string>(new Exception()) // OnErrorResumeNext でエラーになったときの代わりを指定しておく .OnErrorResumeNext(Observable.Return("OK")) // 購読 70
  • .Subscribe( s => Console.WriteLine("OnNext: {0}", s), ex => Console.WriteLine("OnError: {0}", ex), () => Console.WriteLine("OnCompleted"));最初に Throw メソッドを使って例外を発生させています。それに対して OnErrorResumeNextメソッドを使って OK という文字列を流し込んでいます。実行結果を以下に示します。OnNext: OKOnCompletedThrow メソッドで発生された例外は、Subscribe まで届いていないことが確認できます。このように例外発生時に、別の IObservable<T>で処理を再開することが出来ます。因みに Catch メソッドでは例外の種類を指定できましたが OnErrorResumeNext では例外の種類を指定できないので、この点だけを見ると Catch メソッドのほうが柔軟な対応が出来ます。OnErrorResumeNext は、IObservable<T>の拡張メソッドの他にIEnumerable<IObservable<T>>の拡張メソッドや、引数に params IObservable<T>[]を受け取るオーバーロードが定義されています。メソッドのシグネチャを下記に示します。 public static IObservable<T> OnErrorResumeNext<T>(this IEnumerable<IObservable<T>> sources); public static IObservable<T> OnErrorResumeNext<T>(params IObservable<T>[] sources);このメソッドを使うと、最初の IObservable<T>で例外が発生したら、次の IObservable<T>で再開して、そこでも例外が発生したら、次の IObservable<T>で再開して・・・ということが実現できます。コード例を下記に示します。 // 4 番目に OK new[] { "NG", "Error", "Abort", "OK" } // ゗ンデックスと値のペゕに変換 .Select((s, i) => new { index = i, value = s }) // OK 以外は例外を飛ばす IO<string>を返す(IEnumerable<IObservable<T>>へ変換) .Select(s => s.value != "OK" ? Observable.Throw<string>(new Exception(s.ToString())) : Observable.Return(s.ToString())) .OnErrorResumeNext() // 購読 .Subscribe( s => Console.WriteLine("OnNext: {0}", s), ex => Console.WriteLine("OnError: {0}", ex), () => Console.WriteLine("OnCompleted"));多少複雑ですが、{“NG”, “Error”, “Abort”, “OK”}という文字列の配列から、IEnumerable<IObservable<T>>へ変換して OnErrorResumeNext を呼び出しています。内容 71
  • としては”OK”という文字列以外が渡ってきた場合は Observable.Throw メソッドを使って例外を 発生させる IObservable<T>を作成しています。実行結果を以下に示します。 OnNext: { index = 3, value = OK } OnCompleted NG や Error, Abort の文字列から生成される例外は全て無視して OK が Subscribe に渡ってきて いることが確認できます。 動作確認のため OK の文字列の場所を下記のように 2 番目にくるように 書き直します。 // 2 番目に OK new[] { "NG", "OK", "Abort", "Error" } // ゗ンデックスと値のペゕに変換 .Select((s, i) => new { index = i, value = s }) // OK 以外は例外を飛ばす IO<string>を返す(IEnumerable<IObservable<T>>へ変換) .Select(s => s.value != "OK" ? Observable.Throw<string>(new Exception(s.ToString())) : Observable.Return(s.ToString())) .OnErrorResumeNext() // 購読 .Subscribe( s => Console.WriteLine("OnNext: {0}", s), ex => Console.WriteLine("OnError: {0}", ex), () => Console.WriteLine("OnCompleted")); 実行結果を以下に示します。 OnNext: { index = 1, value = OK } OnCompleted 今度は index が 1 になっていることが確認できます。全て例外で終わった場合(このプログラム 例では最初の文字列の配列に OK が無い場合)は以下のように OnCompleted になります。 OnCompleted これまでのサンプルでは OnErrorResumeNext の挙動を確認するために単純なプログラムを書い ていましたが、このメソッドを使うと例えば、 プラ゗マリのサーバに対して非同期にデータを要求 して例外が発生した場合にはセカンダリのサーバに対してデータを要求するというプログラムを 書くことができます。4.5.4. Retry メソッド ここでは Retry メソッドについて説明します。Retry メソッドは名前の通りエラーが起きた場合に、 繰り返し同じ処理を行うメソッドです。 メソッドのオーバーロードには、 成功するまで無限に繰り 返しリトラ゗をするものと、 引数でリトラ゗回数を指定するものがあります。 メソッドのシグネチ ャを下記に示します。 72
  • public static IObservable<T> Retry<T>(this IObservable<T> source); public static IObservable<T> Retry<T>(this IObservable<T> source, int retryCount);まず、無限に繰り返すメソッドのオーバーロードを使ったコード例を下記に示します。 var retryCount = 0; Observable // retryCount が 3 になるまでエラーになる .Create<string>(o => { Console.WriteLine("Create method called: {0}", retryCount); if (retryCount == 3) { o.OnNext(retryCount.ToString()); o.OnCompleted(); return Disposable.Empty; } retryCount++; o.OnError(new InvalidOperationException(retryCount.ToString())); return Disposable.Empty; }) // 成功するまでリトラ゗する .Retry() // 購読 .Subscribe( s => Console.WriteLine("OnNext: {0}", s), ex => Console.WriteLine("OnError: {0}", ex), () => Console.WriteLine("OnCompleted"));Create メソッドで 3 回目までエラーになる IObservable<string>を作成しています。それに対して Retry メソッドを呼び出して、結果を表示しています。実行結果を以下に示します。Create method called: 0Create method called: 1Create method called: 2Create method called: 3OnNext: 3OnCompleted 73
  • Create メソッドが 4 回呼ばれてエラーではない値が発行されると Subscribe まで値が渡っていることが確認できます。このようにエラーが起きても何度もリトラ゗します。次にリトラ゗回数を指定した場合の動作を示すコード例を下記に示します。 var retryCount = 0; Observable // retryCount が 3 になるまでエラーになる .Create<string>(o => { Console.WriteLine("Create method called: {0}", retryCount); if (retryCount == 3) { o.OnNext(retryCount.ToString()); o.OnCompleted(); return Disposable.Empty; } retryCount++; o.OnError(new InvalidOperationException(retryCount.ToString())); return Disposable.Empty; }) // 2 回リトラ゗する .Retry(2) // 購読 .Subscribe( s => Console.WriteLine("OnNext: {0}", s), ex => Console.WriteLine("OnError: {0}", ex), () => Console.WriteLine("OnCompleted"));先ほどと、ほぼ同じコードですが Retry メソッドの引数で 2 回までしかリトラ゗をしないように指定しています。このコードの実行結果を以下に示します。Create method called: 0Create method called: 1OnError: System.InvalidOperationException: 2実行結果からわかるように 2 回目のリトラ゗でも例外が発生しているため、Subscribe の OnErrorが呼び出されています。おそらく、 現実的にはリトラ゗回数を指定するオーバーロードを使うことが多くなると思います。 74
  • 4.6. IObservable<T>の値を収集するメソッド ここでは、IObservable<T>の値を収集するメソッドについて説明します。4.6.1. ToArray メソッド まず、 最初に IObservable<T>の値を収集して IObservable<T[]>へ変換する ToArray メソッド について説明します。これは、単純な Subject<T>を使ったシーケンスに対して適用すると OnNext~OnCompleted までの値を収集して配列にします。コード例を下記に示します。 var s = new Subject<int>(); // IO<T> -> IO<T[]>へ変換して購読 s.ToArray().Subscribe(array => { // 内容表示 Console.WriteLine("start array dump"); foreach (var i in array) { Console.WriteLine(" array value : {0}", i); } }); // 値の発行から Completed Console.WriteLine("OnNext(1)"); s.OnNext(1); Console.WriteLine("OnNext(2)"); s.OnNext(2); Console.WriteLine("OnNext(3)"); s.OnNext(3); Console.WriteLine("OnCompleted()"); s.OnCompleted(); 実行結果を下記に示します。 OnNext(1) OnNext(2) OnNext(3) OnCompleted() start array dump array value : 1 array value : 2 75
  • array value : 3 実行結果からもわかる通り、1 つの IObservable<T>シーケンスをまとめて配列にして IObservable<T[]>へ変換しています。4.6.2. ToDictionary メソッド この ToDictionary メソッドも、ToArray と同じような動きをします。IObservable<T>から IObservable<TKey, T>への変換を行います。TKey は ToDictionary のメソッド引数に Key を選 択するラムダ式を渡すことで指定します。コード例を下記に示します。 var s = new Subject<Tuple<string, int>>(); // IO<T> -> IO<IDictionary<TKey, T>>へ変換して購読 // Key を選択するラムダ式を渡す。 s.ToDictionary(t => t.Item1).Subscribe(dict => { Console.WriteLine("one : {0}", dict["one"]); Console.WriteLine("two : {0}", dict["two"]); Console.WriteLine("three : {0}", dict["three"]); }); // 値の発行から Completed Console.WriteLine("OnNext(one)"); s.OnNext(Tuple.Create("one", 1)); Console.WriteLine("OnNext(two)"); s.OnNext(Tuple.Create("two", 2)); Console.WriteLine("OnNext(three)"); s.OnNext(Tuple.Create("three", 3)); Console.WriteLine("OnCompleted()"); s.OnCompleted(); 実行結果を下記に示します。 OnNext(one) OnNext(two) OnNext(three) OnCompleted() one : (one, 1) two : (two, 2) three : (three, 3) Tuple を発行して、Tuple の Item1 をキーにして IDictionary を作成しています。実行結果から も、one, two, three などの Tuple の最初の要素がキーになっていることが確認できます。 76
  • 4.6.3. ToList メソッド ToList メソッドは ToArray メソッドと、ほぼ同様の動きを行います。ToArray が IObservable<T[]>を孵すのに B対して ToList メソッドハ IObservable<ILIST<T>>を返 します。コード例を下記に示します。 var s = new Subject<int>(); // IO<T> -> IO<IList<T>>へ変換 s.ToList().Subscribe(list => { // 値を表示 foreach (var i in list) { Console.WriteLine("value : {0}", i); } }); // 値の発行から Completed Console.WriteLine("OnNext(1)"); s.OnNext(1); Console.WriteLine("OnNext(2)"); s.OnNext(2); Console.WriteLine("OnNext(3)"); s.OnNext(3); Console.WriteLine("OnCompleted()"); s.OnCompleted(); 実行結果を下記に示します。 OnNext(1) OnNext(2) OnNext(3) OnCompleted() value : 1 value : 2 value : 3 ToArray と同様に OnNext~OnCompleted までの間の値を格納した IList<T>が作成されている ことが確認できます。 77
  • 4.6.4. ToLookup メソッド 次に、ToLookup メソッドについて説明します。これは ToDictionary メソッドと同様にキーを選 択するラムダ式を渡します。そうすると、キー値でグルーピングされた IObservable<ILookup<TKey, T>>が返されます。コード例を下記に示します。 var s = new Subject<Tuple<string, string>>(); // IO<T>から IO<ILookup<TKey, T>>へ変換 // Key を選択するラムダ式を渡す s.ToLookup(t => t.Item1).Subscribe(l => { // グループ単位に表示 foreach (var g in l) { Console.WriteLine("Key : {0}", g.Key); foreach (var i in g) { Console.WriteLine(" item : {0}", i); } } }); Console.WriteLine("OnNext(group A)"); s.OnNext(Tuple.Create("group A", "taro")); s.OnNext(Tuple.Create("group A", "jiro")); Console.WriteLine("OnNext(group B)"); s.OnNext(Tuple.Create("group B", "foo")); s.OnNext(Tuple.Create("group B", "hoge")); s.OnNext(Tuple.Create("group B", "bar")); Console.WriteLine("OnCompleted()"); s.OnCompleted(); 実行結果を下記に示します。 OnNext(group A) OnNext(group B) OnCompleted() Key : group A 78
  • item : (group A, taro) item : (group A, jiro) Key : group B item : (group B, foo) item : (group B, hoge) item : (group B, bar) IObservable<T>から発行された値が Key 値ごとにグルーピングされていることが確認できます。4.6.5. Max メソッドと Min メソッドと Average メソッド ここでは、IObservable<T>のシーケンスから最大、最小、平均の値を返す Max メソッド、Min メソッド、 Average メソッドを説明します。この 3 つのメソッドは、名前の通りそれぞれ最大値、 最小値、平均値のように単一の値を返す動作をしますが、これまでに説明した ToArray などのメ ソッドと同様に戻り値が IObservable<T>となっています。そのため、他のメソッドと同様に IObservable<T>の一連のメソッドチェ゗ンに対してシームレスに統合することが出来ます。こ れらのメソッドの使用例を下記に示します。 var s = new Subject<int>(); // 最大値を求めて表示 s.Max().Subscribe(max => { Console.WriteLine("Max {0}", max); }, () => Console.WriteLine("Max Completed")); // 最小値を求めて表示 s.Min().Subscribe(min => { Console.WriteLine("Min {0}", min); }, () => Console.WriteLine("Min Completed")); // 平均を求めて表示 s.Average().Subscribe(avg => { Console.WriteLine("Average {0}", avg); }, () => Console.WriteLine("Average Completed")); 79
  • // 値の発行~完了通知 Console.WriteLine("OnNext(1-3)"); s.OnNext(1); s.OnNext(2); s.OnNext(3); Console.WriteLine("OnCompleted()"); s.OnCompleted();実行結果を下記に示します。OnNext(1-3)OnCompleted()Max 3Max CompletedMin 1Min CompletedAverage 2Average Completed1~3 の値を発行しているので、最大値が 3、最小値が 1、平均が 2 という結果になっていることが確認できます。Max メソッドや Min メソッドや Average メソッドには数値を表す型と、それらの型の nullable 型に対応するオーバーロードが定義されています。Max と Min は、その型の最大値と最小値、Average は double 型で平均値の IObservable を返します。上記のコード例では、IObservable<int>に対してメソッドを呼び出しているので int 型の最大値と最小値、 double 型の平均値を返しています。また、Max と Min メソッドには、独自の比較方法を指定するための IComparer<T>を受け取るオーバーロードも定義されています。使用例を下記に示します。 // Tuple<int, int>の比較を行うクラス class TupleIntIntComparer : IComparer<Tuple<int, int>> { // Item1 + Item2 の結果で比較を行う public int Compare(Tuple<int, int> x, Tuple<int, int> y) { if (x == y) { return 0; } if (x == null) { 80
  • return -1; } if (y == null) { return 1; } var xValue = x.Item1 + x.Item2; var yValue = y.Item1 + y.Item2; return Comparer<int>.Default.Compare(xValue, yValue); }}// -----------------------------------------// 最大値を求めて表示s.Max(new TupleIntIntComparer()).Subscribe(max =>{ Console.WriteLine("Max {0}", max);},() => Console.WriteLine("Max Completed"));// 最小値を求めて表示s.Min(new TupleIntIntComparer()).Subscribe(min =>{ Console.WriteLine("Min {0}", min);},() => Console.WriteLine("Min Completed"));// 値の発行~完了通知Console.WriteLine("OnNext");s.OnNext(Tuple.Create(1, 1));s.OnNext(Tuple.Create(1, 2));s.OnNext(Tuple.Create(3, 1));Console.WriteLine("OnCompleted()");s.OnCompleted();実行結果を下記に示します。 81
  • OnNext OnCompleted() Max (3, 1) Max Completed Min (1, 1) Min Completed このようにして、最大、最小を求める際の比較ロジックをカスタマ゗ズすることが出来ます。4.6.6. MaxBy メソッドと MinBy メソッド ここでは、MaxBy メソッドと MinBy メソッドについて説明します。このメソッドは最大値、最小 値を求めるという意味では Max メソッドと Min メソッドと同じですが、引数に最大と最小を求め るためのキーとなる値を取得するための Func<T, TKey>型のデリゲートを受け取る点が異なり ます。さらに、MaxBy メソッドと MinBy メソッドの戻り値は IObservable<T>ではなく IObservable<IList<T>>のように複数の結果が返ることを想定したシグネチャになっています。 これは、 デリゲートで取得したキー値が最小となる値がシーケンス内に複数存在する可能性がある ためです。具体例を下記に示します。 var s = new Subject<Tuple<int, int>>(); // 最大値を求めて表示, 比較はタプルの Item1 を使用する s.MaxBy(t => t.Item1).Subscribe(max => { foreach (var i in max) { Console.WriteLine("MaxBy {0}", i); } }, () => Console.WriteLine("MaxBy Completed")); // 最小値を求めて表示, 比較はタプルの Item1 を使用する s.MinBy(t => t.Item1).Subscribe(min => { foreach (var i in min) { Console.WriteLine("MinBy {0}", i); } }, () => Console.WriteLine("MinBy Completed")); // 値の発行~完了通知 82
  • Console.WriteLine("OnNext"); s.OnNext(Tuple.Create(1, 1)); s.OnNext(Tuple.Create(1, 2)); s.OnNext(Tuple.Create(3, 1)); Console.WriteLine("OnCompleted()"); s.OnCompleted(); 実行結果を下記に示します。 OnNext OnCompleted() MaxBy (3, 1) MaxBy Completed MinBy (1, 1) MinBy (1, 2) MinBy Completed MinBy メソッドの結果が 2 つあることが確認できます。これは Item1 の値が 1 になるタプルが(1, 1)と(1, 2)の 2 種類あるためです。 このようなケースに対応するために、 IObservable<IList<T>> のように IList<T>型を返す仕組みになっています。 また、ManBy メソッドと MixBy メソッドにも、比較方法をカスタマ゗ズできる IComparer<T> ゗ンターフェースを受け取るオーバーロードがあります。 比較方法をカスタマ゗ズできる点以外は、 上記で示した MaxBy メソッドと MinBy メソッドと同様の動きをするため、コード例と実行結果 は省略します。4.6.7. Count メソッドと LongCount メソッド ここでは、Count メソッドと LongCount メソッドについて説明します。これは IObservable<T> のシーケンスが完了するまでに発行された値の数を数えます。コード例を下記に示します。 var s = new Subject<int>(); // 数を数える s.Count() // 購読 .Subscribe( i => Console.WriteLine("Count OnNext({0})", i), () => Console.WriteLine("Count OnCompleted()")); // 値の発行~完了通知 Console.WriteLine("OnNext(1)"); s.OnNext(1); Console.WriteLine("OnNext(10)"); 83
  • s.OnNext(10); Console.WriteLine("OnNext(100)"); s.OnNext(100); Console.WriteLine("OnCompleted()"); s.OnCompleted(); 値を3つ発行して OnCompleted メソッドを呼び出しています。実行結果を下記に示します。 OnNext(1) OnNext(10) OnNext(100) OnCompleted() Count OnNext(3) Count OnCompleted() 3 つの値を発行したので 3 が Subscribe で購読しているところに流れてきていることが確認でき ます。LongCount は、流れてくる値の型が int から long になるだけなのでコード例と実行例につ いては割愛します。4.6.8. Any メソッド 次に Any メソッドについて説明します。Any メソッドは、 引数で渡したデリゲートが true を返す 要素が1つでもあれば true を後続に流します。 IObservable<T>のシーケンスが完了した時点で、 どれも true にならなかった場合には false を返します。コード例を下記に示します。 var s = new Subject<int>(); // どれかが 0 以下かチェック s.Any(i => i <= 0) // 購読 .Subscribe( i => Console.WriteLine("Any(i => i <= 0) OnNext({0})", i), () => Console.WriteLine("Any(i => i <= 0) OnCompleted()")); // どれかが偶数かチェック s.Any(i => i % 2 == 0) // 購読 .Subscribe( i => Console.WriteLine("Any(i => i % 2 == 0) OnNext({0})", i), () => Console.WriteLine("Any(i => i % 2 == 0) OnCompleted()")); // 値の発行~完了通知 Console.WriteLine("OnNext(1)"); 84
  • s.OnNext(1); Console.WriteLine("OnNext(10)"); s.OnNext(10); Console.WriteLine("OnNext(100)"); s.OnNext(100); Console.WriteLine("OnCompleted()"); s.OnCompleted(); IObservable<T>に対して Any メソッドを 2 回呼び出しています。 最初の Any メソッドには渡っ てきた値が 0 以下かどうかを判定するデリゲートを渡しています。2 つ目の Any メソッドには渡 ってきた値が偶数かどうかを判定するデリゲートを渡しています。そして、1, 10, 100 という 3 つの値を IObservable<T>のシーケンスに流して OnCompleted でシーケンスを終了させていま す。実行結果を下記に示します。 OnNext(1) OnNext(10) Any(i => i % 2 == 0) OnNext(True) Any(i => i % 2 == 0) OnCompleted() OnNext(100) OnCompleted() Any(i => i <= 0) OnNext(False) Any(i => i <= 0) OnCompleted() 注目すべき点は、偶数の値が流れてきた時点で、True が後続に流れている点です。OnNext(10) の後にすぐログが出ていることが確認できます。一方、0 以下の値は1つも IObservable<T>の シーケンスに流していないため OnCompleted()が呼び出された後に False が流れていることが確 認できます。4.6.9. All メソッド 次は、All メソッドについて説明します。All メソッドは Any メソッドと異なり引数で渡したデリ ゲートが全て True になるかどうかを確認します。コード例を下記に示します。 var s = new Subject<int>(); // 全てが偶数かどうかをチェック s.All(i => i % 2 == 0) // 購読 .Subscribe( i => Console.WriteLine("All(i => i % 2 == 0) OnNext({0})", i), () => Console.WriteLine("All(i => i % 2 == 0) OnCompleted()")); // 全てが 1000 以下かどうかをチェック s.All(i => i <= 1000) 85
  • // 購読 .Subscribe( i => Console.WriteLine("All(i => i <= 1000) OnNext({0})", i), () => Console.WriteLine("All(i => i <= 1000) OnCompleted()")); // 値の発行~完了通知 Console.WriteLine("OnNext(1)"); s.OnNext(1); Console.WriteLine("OnNext(10)"); s.OnNext(10); Console.WriteLine("OnNext(100)"); s.OnNext(100); Console.WriteLine("OnCompleted()"); s.OnCompleted(); IObservable<T>のシーケンスに対して 2 回 All メソッドを呼び出しています。最初の呼び出しで は、偶数かどうかを判定するデリゲートを渡しています。2 つ目の呼び出しでは、1000 以下かど うかを判定するデリゲートを渡しています。この例も All メソッドがどの時点で後続に対して結果 を流しているかがポ゗ントになります。実行結果を下記に示します。 OnNext(1) All(i => i % 2 == 0) OnNext(False) All(i => i % 2 == 0) OnCompleted() OnNext(10) OnNext(100) OnCompleted() All(i => i <= 1000) OnNext(True) All(i => i <= 1000) OnCompleted() 偶数かどうかを判断するデリゲートを渡した All メソッドは、OnNext(1)が呼び出された段階で False になることが確定するので後続に False を流しています。1000 以下かどうかを判断するデ リゲートを渡した All メソッドは 1, 10, 100 の値では、どれも False にならないため OnCompleted()が呼び出された時点で True になることが確定して、 後続に True を流しています。 Any メソッドもそうですが All メソッドは、 流れてきた値にたいしてリゕルタ゗ムに反応できる点 がとても Reactive Extensions らしい特徴のメソッドになっています。4.6.10. Aggregate メソッド ここまでは、特定の収集・集計の方法を行うメソッドを見てきましたが、ここで紹介する Aggregate メソッドは汎用的に IObservable<T>のシーケンスに対して収集・集計するメソッド になります。Aggregate メソッドを使って IObservable<T>のシーケンスから最大の値を返す Max メソッドと同等のメソッドは、下記のようになります。 86
  • var s = new Subject<int>(); // x, y から値の大きい方を返す s.Aggregate((x, y) => x > y ? x : y).Subscribe( i => Console.WriteLine("Aggregate OnNext({0})", i), () => Console.WriteLine("Aggregate OnCompleted()")); // 値の発行~完了通知 Console.WriteLine("OnNext(1)"); s.OnNext(1); Console.WriteLine("OnNext(10)"); s.OnNext(10); Console.WriteLine("OnNext(100)"); s.OnNext(100); Console.WriteLine("OnNext(50)"); s.OnNext(50); Console.WriteLine("OnNext(1)"); s.OnNext(1); Console.WriteLine("OnCompleted()"); s.OnCompleted();実行結果を下記に示します。OnNext(1)OnNext(10)OnNext(100)OnNext(50)OnNext(1)OnCompleted()Aggregate OnNext(100)Aggregate OnCompleted()Aggregate メソッドは、2 つの値を受け取り1つの値を返すデリゲートを引数に受け取ります。2つの値のうちの第一引数が直前までの集計値で、第二引数が新たに IObservable<T>シーケンスから発行された値になります。 この2つの値を使って、任意の集計処理を行い集計結果を戻り値として返します。IObservable<T>から新たな値が発行されると、直前までの集計値と、新たに発行された値を使って集計処理を行います。 これをシーケンスの完了まで繰り返し、シーケンスが終了した時点の集計結果を後続に流します。動作確認をするためのコードを下記に示します。 var s = new Subject<int>(); 87
  • // IObservable<T>のシーケンスから渡される値の合計を求める s.Aggregate((x, y) => { // Aggregate の引数に渡されるデリゲートがどのように動くのか確認するためのログ Console.WriteLine("log({0}, {1})", x, y); // 今までの合計(x)と、新たな値(y)を足して新たな合計値として返す。 return x + y; }) // 結果を購読 .Subscribe( i => Console.WriteLine("Aggregate OnNext({0})", i), () => Console.WriteLine("Aggregate OnCompleted()")); // 値の発行~完了通知 Console.WriteLine("OnNext(1)"); s.OnNext(1); Console.WriteLine("OnNext(10)"); s.OnNext(10); Console.WriteLine("OnNext(100)"); s.OnNext(100); Console.WriteLine("OnCompleted()"); s.OnCompleted();Aggregate メソッドの引数に渡しているデリゲート内で log(x, y)の形式で値を標準出力に出力して、 どのようにメソッドが呼ばれているか確認できるようにしています。 実行結果を下記に示します。OnNext(1)OnNext(10)log(1, 10)OnNext(100)log(11, 100)OnCompleted()Aggregate OnNext(111)Aggregate OnCompleted()注意して見てほしい点として、最初の OnNext では Aggregate の引数で渡したデリゲートが実行されない点です。2 つ目の OnNext の呼び出しではじめて Aggregate の引数に渡したデリゲートが実行されています。そして、最初の OnNext で渡した値の1が第一引数に、2 つ目の OnNextで渡した値の 10 が第二引数に渡っていることが確認できます。次の OnNext で 100 を発行する 88
  • と、第一引数が 1 + 10 の結果の 11 で、第二引数に 100 が渡されてデリゲートが実行されていることが、 実行結果からわかります。 そして、シーケンスが終了(OnCompleted)すると、 Aggregateの返す IObservable<T>から値が後続に流れて Subscribe しているところに 1 + 10 + 100 の結果である 111 が渡ってきていることが確認できます。この Aggregate メソッドには初期値を受け取るオーバーロードがあります。初期値を受け取るオーバーロードでは、最初の OnNext で Aggregate に渡したデリゲートが呼ばれます。その時の第一引数は、初期値で渡した値で、第二引数が、OnNext で発行された値になります。コード例を下記に示します。 var s = new Subject<int>(); // IObservable<T>のシーケンスから渡される値の合計を求める // ただし、初期値として 5 を使用する s.Aggregate(5, (x, y) => { // Aggregate の引数に渡されるデリゲートがどのように動くのか確認するためのログ Console.WriteLine("log({0}, {1})", x, y); // 今までの合計(x)と、新たな値(y)を足して新たな合計値として返す。 return x + y; }) // 結果を購読 .Subscribe( i => Console.WriteLine("Aggregate OnNext({0})", i), () => Console.WriteLine("Aggregate OnCompleted()")); // 値の発行~完了通知 Console.WriteLine("OnNext(1)"); s.OnNext(1); Console.WriteLine("OnNext(10)"); s.OnNext(10); Console.WriteLine("OnNext(100)"); s.OnNext(100); Console.WriteLine("OnCompleted()"); s.OnCompleted();初期値に 5 を渡している点が 1 つ前のサンプルコードと異なります。実行結果を下記に示します。OnNext(1)log(5, 1)OnNext(10)log(6, 10) 89
  • OnNext(100)log(16, 100)OnCompleted()Aggregate OnNext(116)Aggregate OnCompleted()最初の OnNext からデリゲートが呼ばれていることが確認できます。因みに、この Aggregate メソッドを使うと数値の合計といった集計だけではなく、値の収集も可能です。コード例は下記のようになります。 var s = new Subject<int>(); // IObservable<T>のシーケンスから渡される値を集める // 初期値が空のリスト s.Aggregate(new List<int>(), (list, value) => { // 通知された値をリストの保持しておく list.Add(value); return list; }) // 購読。リストの内容を表示 .Subscribe(list => { foreach (var i in list) { Console.WriteLine("value : {0}", i); } }, () => Console.WriteLine("Aggregate OnCompleted()")); // 値の発行~完了通知 Console.WriteLine("OnNext(1)"); s.OnNext(1); Console.WriteLine("OnNext(10)"); s.OnNext(10); Console.WriteLine("OnNext(100)"); s.OnNext(100); Console.WriteLine("OnCompleted()"); s.OnCompleted(); 90
  • 実行結果を下記に示します。 OnNext(1) OnNext(10) OnNext(100) OnCompleted() value : 1 value : 10 value : 100 Aggregate OnCompleted() Aggregate メソッドで ToList メソッドと同様の処理ができていることが確認できます。4.6.11. Scan メソッド 次は、Scan メソッドについて説明します。Scan メソッドは Aggregate メソッドと同じシグネチ ャを持ちます。違いは、Aggregate が集計結果しか後続に流さないのに対して Scan メソッドは 集計経過を後続に流します。Aggregate と同じコードを Scan メソッドを使って書き換えたコー ド例を下記に示します。 var s = new Subject<int>(); // Aggregate で合計値を求めるのと同じ処理を Scan メソッドで行う。 s.Scan((x, y) => { Console.WriteLine("log({0}, {1})", x, y); return x + y; }) // 購読 .Subscribe( i => Console.WriteLine("Scan OnNext({0})", i), () => Console.WriteLine("Scan OnCompleted()")); // 値の発行~完了通知 Console.WriteLine("OnNext(1)"); s.OnNext(1); Console.WriteLine("OnNext(10)"); s.OnNext(10); Console.WriteLine("OnNext(100)"); s.OnNext(100); Console.WriteLine("OnCompleted()"); s.OnCompleted(); 91
  • 実行結果を下記に示します。OnNext(1)Scan OnNext(1)OnNext(10)log(1, 10)Scan OnNext(11)OnNext(100)log(11, 100)Scan OnNext(111)OnCompleted()Scan OnCompleted()OnNext が呼び出される度に、Subscribe で購読しているところに値が流れていることが確認できます。これが Scan メソッドの挙動になります。Aggregate と同様に初期値を渡すオーバーロードもあります。使用例を下記に示します。 var s = new Subject<int>(); // 初期値 5 で Scan を使い合計を求める s.Scan(5, (x, y) => { Console.WriteLine("log({0}, {1})", x, y); return x + y; }) // 購読 .Subscribe( i => Console.WriteLine("Scan OnNext({0})", i), () => Console.WriteLine("Scan OnCompleted()")); // 値の発行~完了通知 Console.WriteLine("OnNext(1)"); s.OnNext(1); Console.WriteLine("OnNext(10)"); s.OnNext(10); Console.WriteLine("OnNext(100)"); s.OnNext(100); Console.WriteLine("OnCompleted()"); s.OnCompleted();実行結果を下記に示します。OnNext(1) 92
  • log(5, 1) Scan OnNext(6) OnNext(10) log(6, 10) Scan OnNext(16) OnNext(100) log(16, 100) Scan OnNext(116) OnCompleted() Scan OnCompleted() 実行結果から、最初の OnNext で値が発行されたタ゗ミングで Scan メソッドに渡したデリゲート が実行されて、結果が Subscribe で購読しているところにながれていることが確認できます。4.6.12. GroupBy メソッド 次に、GroupBy メソッドについて説明します。これは、SQL 文の GROUP BY のように特定の値 でグルーピングしてくれる機能を提供します。 メソッドのシグネチャとしては、IObservable<T> の T からグルーピングのキーとなる値を抽出するデリゲートを渡します。戻り値は、 IObservable<IGroupedObservable<TKey, T>>になります。少し、 IGroupedObservable<TKey, T>について説明します。 IGroupedObservable<TKey, T>は、IObservable<T>を拡張した゗ンターフェースで TKey 型 の Key プロパテゖを追加しています。この Key プロパテゖで、何でグルーピングされたかを表す 以外は通常の IObservable<T>と変わりはありません。コード例を下記に示します。 var s = new Subject<int>(); // 10 で割った余りでグルーピングする。(つまり1の位の数字でグルーピング) s.GroupBy(i => i % 10) // IGroupedObservable<int, int>が Subscribe の OnNext に渡ってくる .Subscribe( g => { // GroupBy の OnNext の処理 Console.WriteLine("集計開始 {0}", g.Key); // 値を集計 配列にまとめて g.ToArray() // ,区切りの文字列にして .Select(array => string.Join(", ", array)) // 結果を表示 .Subscribe( values => Console.WriteLine("集計結果 Key: {0}, Values: {{{1}}}", g.Key, values), 93
  • () => Console.WriteLine("集計終了 Key: {0}", g.Key)); }, ex => { // エラー処理(今回の例ではエラーは起きないけど・・・ Console.WriteLine("GroupBy OnError {0}", ex.Message); }, () => { // GroupBy の結果が OnCompleted になった時の処理 Console.WriteLine("GroupBy OnCompleted"); }); // 値をいくつか発行して終了 Console.WriteLine("OnNext(13)"); s.OnNext(13); Console.WriteLine("OnNext(1)"); s.OnNext(1); Console.WriteLine("OnNext(11)"); s.OnNext(11); Console.WriteLine("OnNext(42)"); s.OnNext(42); Console.WriteLine("OnNext(21)"); s.OnNext(21); Console.WriteLine("OnNext(12)"); s.OnNext(12); Console.WriteLine("OnNext(23)"); s.OnNext(23); Console.WriteLine("OnCompleted()"); s.OnCompleted();多少複雑ですが、コードを理解するポ゗ントは GroupBy の結果がIObservable<IGroupedObservable<int, int>>になっている点です。これを Subscribe するとOnNext には IGroupedObservable<int, int>が渡ってきます。IGroupedObservable は、Keyプロパテゖがある以外は IObservable と同じなので、これまで使ってきた拡張メソッドがすべて使えます。ここの例では、発行された値をすべて”, ”区切りの文字列にして出力しています。実行結果を下記に示します。OnNext(13) 94
  • 集計開始 3 OnNext(1) 集計開始 1 OnNext(11) OnNext(42) 集計開始 2 OnNext(21) OnNext(12) OnNext(23) OnCompleted() 集計結果 Key: 3, Values: {13, 23} 集計終了 Key: 3 集計結果 Key: 1, Values: {1, 11, 21} 集計終了 Key: 1 集計結果 Key: 2, Values: {42, 12} 集計終了 Key: 2 GroupBy OnCompleted 新しいグループに属する値が発行される度に、“集計開始“という文字列が出力されます。これは GroupBy をした結果の OnNext 内で出力しているものです。新しいグループに属する値が出てく ると即座に IGroupedObservable が作成されて Subscribe している箇所に流れてくることがわか ります。そして、値を発行しおわったあとに、“集計結果”という文字列が出力されます。これは、 もとになる IObservable のシーケンスが完了したため、 IGroupedObservable の ToArray が値を まとめた結果を後続の処理に流し文字列として加工して出力しているものになります。最後に、 IGroupedObservable の OnCompleted の”集計終了”という文字列が出力されます。そして、最 後に GroupBy の結果を購読しているところの OnCompleted が呼ばれて”GroupBy OnCompleted”と表示されています。 このコードは Subscribe している中で、さらに Subscribe していたり ToArray などの他のメソッ ドの挙動もかかわってくるため若干複雑になっています。処理の流れが゗メージできない場合は、 コードを実際に書いてみてデバッガで止めて処理の流れを追うなどして動作を確認してみましょ う。4.6.13. GroupByUntil メソッド ここでは、GroupByUntil メソッドについて説明します。これは「4.3.4. SkipUntil と TakeUntil メソッド」で説明したメソッドと同じような動きをします。SkipUntil は引数で渡した IObservable<T>の OnNext が発行されるまで値をスキップします。TakeUntil は引数で渡した IObservable<T>の OnNext が発行されるまで値を後ろに流したりします。このように GroupByUntil メソッドでも、 IObservable<T>を使ってメソッドの処理の開始や終了のタ゗ミン グを制御することが出来ます。 動きの詳しい説明の前に GroupByUntil メソッドのシグネチャを下記に示します。 95
  • public static IObservable<IGroupedObservable<TKey, TSource>> GroupByUntil<TSource, TKey, TDuration>( this IObservable<TSource> source, Func<TSource, TKey> keySelector, Func<IGroupedObservable<TKey, TSource>, IObservable<TDuration>> durationSelector )第三引数に Func<IGroupedObservable<TKey, TSource>, IObservable<TDuration>>という型のデリゲートを受け取っているところが、理解に苦しむ点だと思います。これは、第二引数のkeySelector で選択された値で新しいグループが出来たときに呼び出されるデリゲートになります。 そのため引数がグループを表す IGroupedObservable<TKey, TSource>になります。 そして、戻り値で、このグループの集計を終了するきっかけを OnNext で通知するIObservable<TDuration>を返します。コード例を下記に示します。 // 値をランダムで出したいので var r = new Random(); // グループピングの終了を通知するための IObservable var duration = new Subject<Unit>(); // GroupByUntil の使用例 var subscriber = Observable.Interval(TimeSpan.FromMilliseconds(500)) // 500 ミリ秒間隔で乱数発生 .Select(_ => r.Next(1000)) .GroupByUntil( // 1 の位の数でグルーピング l => l % 10, // duration で OnNext が発行されるまでの間グルーピングをする。 // 全てのグループに対して、同じ IObservable を返しているので、同じタ゗ミングで // グルーピングが終わる。 l => duration.AsObservable()) // 購読 .Subscribe( g => { // GroupBy の OnNext の処理 Console.WriteLine("集計開始 {0}", g.Key); // 値を集計 配列にまとめて g.ToArray() // ,区切りの文字列にして .Select(array => string.Join(", ", array)) 96
  • // 結果を表示 .Subscribe( values => Console.WriteLine("集計結果 Key: {0}, Values: {{{1}}}", g.Key, values), () => Console.WriteLine("集計終了 Key: {0}", g.Key)); }, ex => { // エラー処理(今回の例ではエラーは起きないけど・・・ Console.WriteLine("GroupBy OnError {0}", ex.Message); }, () => { // GroupBy の結果が OnCompleted になった時の処理 Console.WriteLine("GroupBy OnCompleted"); }); while(true) { // 待ち Console.WriteLine("Enter を押すまで集計します。(終了したい場合は end と入力してください)"); if (Console.ReadLine() == "end") { break; } // duration の OnNext でいったんグルーピングの集計が一区切り duration.OnNext(Unit.Default); } // 後始末 subscriber.Dispose();このコードでは 0.5 秒(500ms)間隔でランダムに 0~999 の値を発行しています。そして、発行された値の 1 の位でグルーピングをしています。GroupByUntil では、グルーピングの終了のタ゗ミングを通知する IObservable を返すデリゲートで全て同じ IObservable を返しています。そのため、全てのグルーピングの終了のタ゗ミングが同じになります。 グルーピングの終了のタ゗ミングは、コンソールで Enter を押したタ゗ミングになります。Enter を押すと、今までのグルーピングの結果を表示して、新たにグルーピングを行います。end と入力して Enter を押すと処理が終了します。実行結果を下記に示します。Enter を押すまで集計します。(終了したい場合は end と入力してください) 97
  • 集計開始 8集計開始 4集計開始 1集計開始 6集計開始 9集計開始 7集計開始 0集計開始 5集計開始 2集計開始 3集計結果 Key: 8, Values: {358, 808, 48, 308}集計終了 Key: 8集計結果 Key: 4, Values: {724, 544, 594, 604}集計終了 Key: 4集計結果 Key: 1, Values: {371, 161, 81, 141, 221}集計終了 Key: 1集計結果 Key: 6, Values: {426, 666, 456}集計終了 Key: 6集計結果 Key: 9, Values: {309, 379, 689, 19}集計終了 Key: 9集計結果 Key: 7, Values: {967, 57, 557, 477}集計終了 Key: 7集計結果 Key: 0, Values: {650, 330, 590, 570, 780, 550}集計終了 Key: 0集計結果 Key: 5, Values: {545}集計終了 Key: 5集計結果 Key: 2, Values: {462, 532, 492, 902, 252, 542, 692, 852}集計終了 Key: 2集計結果 Key: 3, Values: {663, 433, 763}集計終了 Key: 3Enter を押すまで集計します。(終了したい場合は end と入力してください)集計開始 2集計開始 7集計開始 8集計開始 0集計開始 4 98
  • 集計開始 9 end 集計結果 Key: 2, Values: {112} 集計終了 Key: 2 集計結果 Key: 7, Values: {357} 集計終了 Key: 7 集計結果 Key: 8, Values: {598} 集計終了 Key: 8 集計結果 Key: 0, Values: {850} 集計終了 Key: 0 集計結果 Key: 4, Values: {644} 集計終了 Key: 4 集計結果 Key: 9, Values: {699} 集計終了 Key: 9 長い実行結果ですが、実行後暫く放置して Enter を押して、その後 end と入力して Enter を押し た実行結果になります。一度 Enter を押してグルーピングが終了したあとにも、新たにグルーピ ングが行われています。これはもとになる Observable.Interval が終了していないためです。グ ルーピングが一旦終了したあとに、 新たに値が発行されるため再度グルーピングが行われます。 こ のように、GroupBy では素直に書けない一定間隔でのグルーピングなどが GroupByUntil では行 えます。このように、GroupBy では素直に書けない一定間隔でのグルーピングなどが GroupByUntil では行えます。また、今回の例では全ての IGroupedObservable に対して同じ IObservable を返していたため、全てのグルーピングの終了タ゗ミングが同じになっていました が、IGroupedObservable ごとに異なる IObservable を返すことで、グルーピングの終了タ゗ミ ングをグループごとに指定することも出来ます。4.7. IObservable<T>から IEnumerable<T>への変換メソッド ここでは、IObservable<T>から IEnumerable<T>への変換メソッドについて説明します。既存 の IEnumerable<T>を受け取るメソッドとの連携に使えるかもしれませんが、使いどころは書い ている私もよくわかりません。4.7.1. ToEnumerable メソッド このメソッドは、単純に IObservable<T>から IEnumerable<T>へ変換します。 ToEnumerable<T>で IEnumerable<T>に変換した後に発行された値が IEnumerable<T>に流 れて、OnCompleted で IEnumerable<T>のシーケンスも終了します。コード例を下記に示しま す。 var s = new Subject<int>(); // 進捗状況を管理するための WaitHandle var gate = new EventWaitHandle(false, EventResetMode.AutoReset); Observable.Start(() => 99
  • { // IO<T> -> IE<T>への変換 var e = s.ToEnumerable(); // 待ち解除 gate.Set(); // 値の表示 foreach (var i in e) { Console.WriteLine("value : {0}", i); } }) // 最後に WaitHandle を発火 .Finally(() => gate.Set()) .Subscribe(); // ToEnumerable されるまで待つ gate.WaitOne(); // 値の発行から Completed Console.WriteLine("OnNext(1)"); s.OnNext(1); Console.WriteLine("OnNext(2)"); s.OnNext(2); Console.WriteLine("OnNext(3)"); s.OnNext(3); Console.WriteLine("OnCompleted()"); s.OnCompleted(); // 列挙が終わるのを待つ gate.WaitOne();実行結果を下記に示します。OnNext(1)OnNext(2)OnNext(3)OnCompleted()value : 1value : 2 100
  • value : 3 今回のプログラムは複数のスレッドから値の発行と列挙を行っているため、 毎回同じ実行結果にな るとは限りません出力が前後することがあります。複数スレッドに分けたのは ToEnumerable の 結果を IEnumerable<T>でループを行うと、値が発行されるまでブロックするという挙動のため です。因みに、この条件は Hot な IObservable<T>を使用している場合に起きる問題で Cold な IObservable<T>を使用している場合は問題になることは、ありません。コード例を下記に示し ます。 // Cold な IObservable<int>を作成 var s = Observable.Range(1, 5); // IEnumerable<int>に変換して列挙 foreach (var i in s.ToEnumerable()) { Console.WriteLine("value : {0}", i); } 実行結果を下記に示します。 value : 1 value : 2 value : 3 value : 4 value : 5 普通に値の列挙が行われていることが確認できます。4.7.2. Latest メソッド Latest メソッドについて説明します。 Latest メソッドは IObservable<T>から IEnumerable<T> へ変換を行うメソッドです。 変換後の IEnumerable<T>では MoveNext メソッドの呼び出し時に、 変換元の IObservable<T>からの値の発行を待ちます。既に値が発行されていたり、値が発行さ れると True を返し Current プロパテゖで、発行された値の取得ができるようになります。コード 例を下記に示します。 // 値の発行元 var s = new Subject<int>(); // Latest で取得した IEnumerable<int>の値を印字 Observable.Start(() => { Console.WriteLine("Start latest loop"); foreach (var i in s.Latest()) { Console.WriteLine("LatestValue : {0}", i); 101
  • } Console.WriteLine("End latest loop"); }); // 1 秒間隔で値 1~10 の値を発行 Observable.Start(() => { foreach (var i in Enumerable.Range(1, 10)) { Thread.Sleep(1000); Console.WriteLine("OnNext({0})", i); s.OnNext(i); } Console.WriteLine("OnCompleted()"); s.OnCompleted(); }); // 終了しないため待つ Console.ReadLine();実行結果を下記に示します。Start latest loopOnNext(1)LatestValue : 1OnNext(2)LatestValue : 2OnNext(3)LatestValue : 3OnNext(4)LatestValue : 4OnNext(5)LatestValue : 5OnNext(6)LatestValue : 6OnNext(7)LatestValue : 7OnNext(8)LatestValue : 8 102
  • OnNext(9)LatestValue : 9OnNext(10)OnCompleted()LatestValue : 10End latest loop動作結果から、OnNext で値が発行された後に、Latest で取得した IEnumerable<T>のループで値が取得できていることが確認できます。また、もとになる IObservable<T>のシーケンスが終了したタ゗ミングで Latest メソッドで変換した結果の IEnumerable<T>のループも抜けていることが確認できます。今回の例では、値の発行と Latest で変換した IEnumerable<T>から取得できる値が 1 対 1 の関係にあるように見えますが、 Latest で変換した IEnumerable<T>が返す値はあくまで最後に発行された値になります。例えば、IEnumerable<T>でのループが 1 周する間に 2 回値が発行された場合、IEnumerable<T>は最初に発行された値は無視して 2 回目に発行された値を返します。この挙動を確認するコードを下記に示します。 // 値の発行元 var s = new Subject<int>(); // Latest で変換した IEnumerable から IEnumerator を取得 var e = s.Latest().GetEnumerator(); // 1 を発行して IEnumerator から値を取得 Console.WriteLine("OnNext(1)"); s.OnNext(1); Console.WriteLine("MoveNext : {0}", e.MoveNext()); Console.WriteLine("Current : {0}", e.Current); // 10 と 100 を発行して IEnumerator から値を取得 Console.WriteLine("OnNext(10)"); s.OnNext(10); Console.WriteLine("OnNext(100)"); s.OnNext(100); Console.WriteLine("MoveNext : {0}", e.MoveNext()); Console.WriteLine("Current : {0}", e.Current); // IObservable<T>のシーケンスを終了した MoveNext を呼んでみる Console.WriteLine("OnCompleted()"); s.OnCompleted(); 103
  • Console.WriteLine("MoveNext : {0}", e.MoveNext()); 実行結果を下記に示します。 OnNext(1) MoveNext : True Current : 1 OnNext(10) ← ※1 OnNext(100) ← ※2 MoveNext : True Current : 100 ← ※3 OnCompleted() MoveNext : False 実行結果の※1と※2で 10 と 100 の値を 2 つ発行していますが、※3 で取得している値は最後 に発行された 100 の値を取得しています。以上が Latest メソッドの挙動になります。4.7.3. MostRecent メソッド Latest メソッドと同じような動きをするメソッドに MostRecent というメソッドがあります。こ のメソッドも IObservable<T>から IEnumerable<T>へ変換を行いますが、Latest メソッドが 最後に発行された値を返して、値が無い場合は値が発行されるまで待つのに対して、MostRecent は最後に発行された値をキャッシュしておいて、キャッシュを返します。そのため、 IEnumerable<T>から値を取得する際にブロックされることがありません。コード例を下記に示 します。 // 値の発行元 var s = new Subject<int>(); // 初期値を-1 にして IObservable<T>から IEnumerable に変換して IEnumerator を取得 var e = s.MostRecent(-1).GetEnumerator(); // 一度も値を発行してない状態 Console.WriteLine("一度も値を発行していない場合は初期値が返される"); Console.WriteLine("MoveNext : {0}", e.MoveNext()); Console.WriteLine("CurrentValue : {0}", e.Current); Console.WriteLine("MoveNext : {0}", e.MoveNext()); Console.WriteLine("CurrentValue : {0}", e.Current); // 値を発行した状態の確認 Console.WriteLine("-----"); Console.WriteLine("OnNext(10)"); s.OnNext(10); 104
  • Console.WriteLine("最後に発行した値が返されるようになる"); Console.WriteLine("MoveNext : {0}", e.MoveNext()); Console.WriteLine("CurrentValue : {0}", e.Current); Console.WriteLine("MoveNext : {0}", e.MoveNext()); Console.WriteLine("CurrentValue : {0}", e.Current); // IEnumerator で値を取得する前に 2 回値が発行された状態の確認 Console.WriteLine("-----"); Console.WriteLine("OnNext(100)"); s.OnNext(100); Console.WriteLine("OnNext(1000)"); s.OnNext(1000); Console.WriteLine("最後に発行した値が返されるようになる"); Console.WriteLine("MoveNext : {0}", e.MoveNext()); Console.WriteLine("CurrentValue : {0}", e.Current); Console.WriteLine("MoveNext : {0}", e.MoveNext()); Console.WriteLine("CurrentValue : {0}", e.Current); // OnCompleted の後の状態の確認 Console.WriteLine("-----"); Console.WriteLine("OnCompleted()"); s.OnCompleted(); Console.WriteLine("OnCompleted を呼ぶと、MoveNext が False を返すようになる"); Console.WriteLine("MoveNext : {0}", e.MoveNext());このコードの実行結果を下記に示します。一度も値を発行していない場合は初期値が返されるMoveNext : TrueCurrentValue : -1MoveNext : TrueCurrentValue : -1-----OnNext(10)最後に発行した値が返されるようになるMoveNext : TrueCurrentValue : 10MoveNext : TrueCurrentValue : 10 105
  • ----- OnNext(100) OnNext(1000) 最後に発行した値が返されるようになる MoveNext : True CurrentValue : 1000 MoveNext : True CurrentValue : 1000 ----- OnCompleted() OnCompleted を呼ぶと、MoveNext が False を返すようになる MoveNext : False このように Latest では MoveNext でブロックされるようなケースでも MostRecent ではブロック されずに最後の値のキャッシュを返すことが確認できます。4.7.4. Next メソッド 次に、 Latest メソッドと MostRecent メソッドと同様に IObservable<T>から IEnumerable<T> へ変換する Next メソッドについて説明します。Next メソッドは Latest メソッドと同じように MoveNext で値が発行されるのを待機する IEnumerable<T>を返します。 Latest メソッドが返す IEnumerable<T>との違いは、Latest は最後の値を 1 回だけキャッシュしますが Next メソッド が返す IEnumerable<T>は、キャッシュを行いません。この挙動の違いを示すプログラムを下記 に示します。 // 初期値 -1 の BehaviorSubject var s = new BehaviorSubject<int>(-1); // Latest の動作 Observable.Start(() => { Console.WriteLine("Latest start"); foreach (var i in s.Latest()) { Console.WriteLine("Latest : {0}", i); } }); // Next の動作 Observable.Start(() => { 106
  • Console.WriteLine("Next start"); foreach (var i in s.Next()) { Console.WriteLine("Next : {0}", i); } }); // 1 秒間隔で値を発行する Observable.Start(() => { var i = 0; while (true) { Thread.Sleep(1000); Console.WriteLine("OnNext({0})", ++i); s.OnNext(i); } }); // 待機 Console.WriteLine("Please any key."); Console.ReadLine(); Console.WriteLine("End NextSample");BehaviorSubject は初期値を持った Subject です。BehaviorSubject<T>クラスの詳細については、5.2 を参照してください。上記プログラムの実行結果を下記に示します。Please any key.Latest startNext startLatest : -1 <- ※1 Latest は最後の値をキャッシュしているので BehaviorSubject の初期値-1 を表示するOnNext(1)Next : 1 <- ※2 Next は最後の値をキャッシュしないので 1 から表示するLatest : 1OnNext(2)Next : 2Latest : 2OnNext(3)Next : 3 107
  • Latest : 3 OnNext(4) Latest : 4 Next : 4 End NextSample ※1 の箇所にある通り Latest では最後の値をキャッシュするため BehaviorSubject の初期値であ る-1 を表示しています。それに対して Next メソッドでは、初期値の-1 は表示せずに 1 からの表 示になっていることが確認できます。4.8. ToEvent メソッド ToEvent メソッドは、IObservable<T>から IEventSource<T>という型に変換を行います。こ の IEventSource<T>という型は OnNext ゗ベントを持つ型で、IObservable<T>から値が発行 される度に OnNext ゗ベントを発火させます。コード例を下記に示します。 var s = new Subject<int>(); // IO<T>から OnNext ゗ベントを発行する IEventSourct<T>へ変換 var evt = s.ToEvent(); // OnNext を゗ベントで受け取れる evt.OnNext += i => { Console.WriteLine("value : {0}", i); }; // 値の発行 Console.WriteLine("OnNext(1)"); s.OnNext(1); Console.WriteLine("OnNext(2)"); s.OnNext(2); Console.WriteLine("OnNext(3)"); s.OnNext(3); 実行結果を下記に示します。 OnNext(1) value : 1 OnNext(2) value : 2 OnNext(3) value : 3 108
  • Subject で OnNext が実行される度に OnNext ゗ベントが発行されていることが確認できます。4.9. 重複を排除するメソッド ここでは、IObservable<T>のシーケンスから重複要素を排除するメソッドについて紹介します。4.9.1. Distinct メソッド Distinct メソッドは、IObservable<T>のシーケンスから、一度通過した値を二度と通さないメソ ッドです。コード例を下記に示します。 // Distinct で重複を排除して購読 s.Distinct() .Subscribe( // 値を出力 i => Console.WriteLine("OnNext({0})", i), // OnCompleted したことを出力 () => Console.WriteLine("OnCompleted()")); // 1~3 の値を発行 Console.WriteLine("OnNext 1~3"); s.OnNext(1); s.OnNext(2); s.OnNext(3); // 繰り返し 1~3 の値を発行 Console.WriteLine("OnNext 1~3"); s.OnNext(1); s.OnNext(2); s.OnNext(3); // 2~4 の値を発行 Console.WriteLine("OnNext 2~4"); s.OnNext(2); s.OnNext(3); s.OnNext(4); Console.WriteLine("OnCompleted call."); s.OnCompleted(); 109
  • このコードでは、順番に 1, 2, 3, 1, 2, 3, 2, 3, 4 という値を発行する IObservable<int>のシー ケンスに対して Distinct を適用して Subscribe しています。実行結果を下記に示します。 OnNext 1~3 OnNext(1) OnNext(2) OnNext(3) OnNext 1~3 OnNext 2~4 OnNext(4) OnCompleted call. OnCompleted() 実行結果からわかるように、2 回目の 1, 2, 3 の値を発行している所では、Subscribe の OnNext が呼ばれていません。これは Distinct メソッドで一度出現した値をブロックしているためです。 次の 2~4 の値を発行している箇所では、既に発行されている 2 と 3 はブロックされていますが、 4 は Subscribe の OnNext に渡っていることが確認できます。4.9.2. Distinct メソッドのオーバーロード Distinct メソッドには、Func<T, TKey>という T から重複の判定を行う値を選択するためのデリ ゲートを渡すオーバーロードと、 IEqualityComparer<T>゗ンターフェースを実装したクラスで、 値が等しいかどうかの判定方法を指定するオーバーロードと、Func<T, TKey>型のデリゲートと IEqualityComparer<T>゗ンターフェースを実装したクラスの両方を指定するオーバーロードが あります。各オーバーロードのシグネチャを下記に示します。 // 比較方法を指定するオーバーロード public static IObservable<T> Distinct<T>( this IObservable<T> source, IEqualityComparer<T> comparer ) // 比較する値を選択するデリゲートを指定するオーバーロード public static IObservable<T> Distinct<T, TKey>( this IObservable<T> source, Func<T, TKey> keySelector ) // 比較する値を選択するデリゲートと、値の比較方法を指定するオーバーロード public static IObservable<T> Distinct<T, TKey>( this IObservable<T> source, Func<T, TKey> keySelector, 110
  • IEqualityComparer<TKey> comparer )この各種オーバーロードを使うことで、柔軟に重複値のフゖルタリングを行えます。ここでは、最後の Func<T, TKey>型のデリゲートと IEqualityComparer<T>゗ンターフェースの実装クラスの両方を指定するオーバーロードのコード例を示します。Func<T, TKey>型のデリゲートのみを指定するオーバーロードと、IEqualityComparer<T>゗ンターフェースの実装クラスのみを指定するオーバーロードのサンプルコードについては、 単純に引数が少ないだけなので、 ここでは割愛します。まず、サンプルコードで使用するクラスの定義を下記に示します。ここでは、名前と年齢をもった人を表すクラスと、int 型を 1 の位を除いた状態で比較するクラスの2つのコードを示します。 /// <summary> /// 人 /// </summary> class Person { public int Age { get; set; } public string Name { get; set; } public override string ToString() { return string.Format("{0}: {1}歳", this.Name, this.Age); } } /// <summary> /// 1 の位を省いた状態で比較を行う。 /// </summary> public class GenerationEqualityComparer : IEqualityComparer<int> { /// <summary> /// 1 の位を除いた数が等しければ true を返す /// </summary> public bool Equals(int x, int y) { return (x / 10) == (y / 10); } public int GetHashCode(int obj) 111
  • { return (obj / 10).GetHashCode(); } }上記の 2 つのクラスを使った、Distinct メソッドの使用例のコードを下記に示します。 var s = new Subject<Person>(); s.Distinct( // Person クラスの Age プロパテゖの値で比較する p => p.Age, // 比較方法は GenerationEqualityComparer を使用する new GenerationEqualityComparer()) // 購読 .Subscribe( // 値を出力 p => Console.WriteLine(p), // OnCompleted したことを出力 () => Console.WriteLine("OnCompleted()")); // 10 代, 20 代, 30 代の人を発行する Console.WriteLine("OnNext 10 代~30 代"); s.OnNext(new Person { Name = "田中 一郎", Age = 15 }); s.OnNext(new Person { Name = "田中 二郎", Age = 22 }); s.OnNext(new Person { Name = "田中 三郎", Age = 38 }); // 別の名前, 年齢の 10 代, 20 代, 30 代の人を発行する Console.WriteLine("OnNext 10 代~30 代"); s.OnNext(new Person { Name = "木村 一郎", Age = 12 }); s.OnNext(new Person { Name = "木村 二郎", Age = 28 }); s.OnNext(new Person { Name = "木村 三郎", Age = 31 }); // 40 代の人を発行する Console.WriteLine("OnNext 40 代"); s.OnNext(new Person { Name = "井上 エリザベス", Age = 49 }); Console.WriteLine("OnCompleted call."); s.OnCompleted(); 112
  • 上記のコードの実行結果を下記に示します。 OnNext 10 代~30 代 田中 一郎: 15 歳 田中 二郎: 22 歳 田中 三郎: 38 歳 OnNext 10 代~30 代 OnNext 40 代 井上 エリザベス: 49 歳 OnCompleted call. OnCompleted() 上記の結果からわかるとおり、Person クラスの Age プロパテゖの 10 の位の値で重複チェックが 行われていることが確認できます。4.9.3. DistinctUntilChanged メソッド DistinctUntilChanged メソッドは、直前の値と異なる値が出現するまで値をブロックするメソッ ドになります。Distinct メソッドが、今まで発行された値すべてに対して重複チェックを行ってい たのに対して DistinctUntilChanged メソッドは直前の値のみをチェック対象にする点が異なりま す。コード例を下記に示します。 var s = new Subject<int>(); // Distinct で重複を排除して購読 s.DistinctUntilChanged() .Subscribe( // 値を出力 i => Console.WriteLine("OnNext({0})", i), // OnCompleted したことを出力 () => Console.WriteLine("OnCompleted()")); // 1~3 の値を 2 回ずつ発行 Console.WriteLine("OnNext 1 -> 1 -> 2 -> 2 -> 3 -> 3"); s.OnNext(1); s.OnNext(1); s.OnNext(2); s.OnNext(2); s.OnNext(3); s.OnNext(3); // 1~3 の値を発行 113
  • Console.WriteLine("OnNext 1~3"); s.OnNext(1); s.OnNext(2); s.OnNext(3); Console.WriteLine("OnCompleted call."); s.OnCompleted(); 実行結果を下記に示します。 OnNext 1 -> 1 -> 2 -> 2 -> 3 -> 3 OnNext(1) OnNext(2) OnNext(3) OnNext 1~3 OnNext(1) OnNext(2) OnNext(3) OnCompleted call. OnCompleted() 最初に 1, 1, 2, 2, 3, 3 という値を発行している箇所では、Subscribe の OnNext に 1, 2, 3 とい う値しか渡っていないことから、値の変化が無い場合にブロックされていることが確認できます。 また、そのあとに 1, 2, 3 の値を発行している箇所で Subscribe の OnNext に 1, 2, 3 の値が渡っ ていることから、Distinct メソッドとの動作の違いである、直前の値しか比較の対象にしないとい う点が確認できます。 DistinctUntilChanged メソッドにも、比較対象の値を選択する Func<T, TKey>型のデリゲート を受け取るオーバーロードと、IEqualityComparer<T>゗ンターフェースを実装したクラスを受 け取るオーバーロードがありますが、 Distinct メソッドと基本的に同じためサンプルコードは割愛 します。4.10. Buffer メソッドと Window メソッド ここでは、Buffer メソッドと Window メソッドについて説明します。まず、最初に Buffer メソッ ドについて説明して、次に Window メソッドについて説明します。Buffer メソッドは、もとにな る IObservable<T>のシーケンスから指定した数、時間、タ゗ミングで値をまとめて IObservable<IList<T>>のシーケンスとして後続に流すメソッドになります。4.10.1. 数でまとめる Buffer メソッドのオーバーロード まず、最初に一番直感的に使える指定した数で値をまとめるオーバーロードについて説明します。 メソッドのシグネチャを以下に示します。 114
  • public static IObservable<IList<TSource>> Buffer<TSource>( this IObservable<TSource> source, int count)count で、値をまとめる個数を指定します。このメソッドの使用例を書きに示します。 // 1~10 の値を発行する IObservable<int>のシーケンス Observable.Range(1, 10) // 3 つずつの値に分ける .Buffer(3) .Subscribe( l => { // IList<int>の内容を出力 Console.WriteLine("-- Buffer start"); foreach (var i in l) { Console.WriteLine(i); } }, // 完了 () => Console.WriteLine("OnCompleted"));Observable.Range を使って 1~10 の 10 個の値を発行する IObservable<int>のシーケンスを作成して Buffer(3)で 3 つずつ値をまとめています。順当にいくと、1つ値が余ってしまいます。その値がどのように扱われるのかに注意して下記実行結果を確認してください。-- Buffer start123-- Buffer start456-- Buffer start789-- Buffer start10 115
  • OnCompleted結果からわかるように、(1,2,3), (4,5,6), (7,8,9), (10)のようにまとめられています。最後のあまった数は、無視されるのではなく、 残ったものだけを IList<int>にまとめて、 後続に流します。数を指定する Buffer メソッドのオーバーロードには、下記のシグネチャのように 2 つの int 型の数を受け取るものがあります。 public static IObservable<IList<TSource>> Buffer<TSource>( this IObservable<TSource> source, int count, int skip)引数の count は、値をまとめる数で skip は、Buffer でまとめ終わった後に次の値を収集するスタート地点を何処にするのか指定する引数です。skip で指定した数だけ、発行される値を無視したあとに count 個の値を集め始めます。最初に使用した引数が1つの Buffer メソッドは、count 個ずつ値をまとめたあとの、 次に値をまとめ始める起点が count 個の値を飛ばした個所になるため、count と skip に同じ値を指定した場合と等しくなります。このオーバーロードのコード例を下記に示します。 // 1~10 の値を発行する IObservable<int>のシーケンス Observable.Range(1, 10) // 3 つずつの値に分けて、値は 2 つ飛ばし .Buffer(3, 2) .Subscribe( l => { // IList<int>の内容を出力 Console.WriteLine("-- Buffer start"); foreach (var i in l) { Console.WriteLine(i); } }, // 完了 () => Console.WriteLine("OnCompleted"));Observable.Range メソッドを使用して 1~10 の 10 個の値を発行する IObservable<int>のシーケンスを作成して、Buffer(3, 2)の呼び出しで 3 こずつ、値は 2 つ飛ばしでまとめています。実行結果を下記に示します。-- Buffer start123 116
  • -- Buffer start345-- Buffer start567-- Buffer start789-- Buffer start910OnCompleted実行結果からわかるように(1,2,3), (3,4,5), (5,6,7), (7,8,9), (9, 10)という形で値がまとめられています。まとめられた値の最初の数を見ると 1, 3, 5, 7, 9 というように skip 引数で指定した 2が公差になっている数列になっていることが確認できます。skip 引数に count 引数より大きな値を指定することも可能です。コード例と実行結果を下記に示します。 // 1~10 の値を発行する IObservable<int>のシーケンス Observable.Range(1, 10) // 3 つずつの値に分けて、値は 5 つ飛ばし .Buffer(3, 5) .Subscribe( l => { // IList<int>の内容を出力 Console.WriteLine("-- Buffer start"); foreach (var i in l) { Console.WriteLine(i); } }, // 完了 () => Console.WriteLine("OnCompleted")); 117
  • このコードでは count に 3, skip に 5 を指定しています。実行結果を下記に示します。 -- Buffer start 1 2 3 -- Buffer start 6 7 8 OnCompleted4.10.2. 時間でまとめる Buffer メソッドのオーバーロード 次に、時間で値をまとめる Buffer メソッドのオーバーロードについて説明します。Reactive Extensions が得意とする時間を扱うオーバーロードなので、単純に数を指定するだけのよりも実 践では使う機会が多いかもしれません。 まず、一番単純な時間の間隔を指定するオーバーロードのメソッドのシグネチャを下記に示します。 public static IObservable<IList<T>> Buffer<TSource>( this IObservable<T> source, TimeSpan timeSpan); timeSpan 引数で指定した間隔で値をためて、たまった値の IList<T>を後続に流します。コード 例を下記に示します。 var gate = new EventWaitHandle(false, EventResetMode.AutoReset); Observable // 500ms ごとに値を発行する .Interval(TimeSpan.FromMilliseconds(500)) // 3 秒間値を溜める .Buffer(TimeSpan.FromSeconds(3)) // 最初の 3 つを後続に流す .Take(3) .Subscribe( l => { // 値を表示 Console.WriteLine("--Buffer {0:HH:mm:ss}", DateTime.Now); foreach (var i in l) { Console.WriteLine(i); 118
  • } }, () => { // 完了 Console.WriteLine("OnCompleted"); gate.Set(); }); // OnCompleted 待ち Console.WriteLine("WaitOne"); gate.WaitOne(); Console.WriteLine("WaitOne Completed");500ms ごとに値を発行して、それを 3 秒スパンでまとめて出力しています。実行結果を下記に示します。WaitOne--Buffer 20:35:26012345--Buffer 20:35:2967891011--Buffer 20:35:32121314151617 119
  • OnCompletedWaitOne Completed3 秒間隔で 6 個ずつ値がまとめられていることが確認できます。時間指定のオーバーロードにも、第二引数で次の値の塊を作るために待機する時間を指定するオーバーロードがあります。シグネチャを下記に示します。 public static IObservable<IList<TSource>> Buffer<T>(this IObservable<T> source, TimeSpan timeSpan, TimeSpan timeShift);timeShift 引数が、次の値を集め始めるまでの時間を指定する箇所になります。このオーバーロードを使用したコード例を下記に示します。 var gate = new EventWaitHandle(false, EventResetMode.AutoReset); Observable // 500ms ごとに値を発行する .Interval(TimeSpan.FromMilliseconds(500)) // 3 秒間値を溜める。次の値をためるための待機時間は 2 秒 .Buffer(TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(2)) // 最初の 3 つを後続に流す .Take(3) .Subscribe( l => { // 値を表示 Console.WriteLine("--Buffer {0:HH:mm:ss}", DateTime.Now); foreach (var i in l) { Console.WriteLine(i); } }, () => { // 完了 Console.WriteLine("OnCompleted"); gate.Set(); }); // OnCompleted 待ち Console.WriteLine("WaitOne"); 120
  • gate.WaitOne(); Console.WriteLine("WaitOne Completed"); このコードでは、3 秒間値を溜めるとともに次の値を溜め始めるまでの間隔を 2 秒にしています。 もととなる IObservable のシーケンスでは 500ms ごとに値が発行されているため、 秒分の値が 1 1 つ前の値の塊と重複します。実行結果を以下に示します。 WaitOne --Buffer 20:44:00 0 1 2 3 4 5 --Buffer 20:44:02 4 ← 1 秒分(500ms スパンなのでこの場合 2 個)の値がかぶってる 5 ← 6 7 8 9 --Buffer 20:44:04 8 ← 1 秒分(500ms スパンなのでこの場合 2 個)の値がかぶってる 9 ← 10 11 12 13 OnCompleted 実行結果内にも示していますが、1 秒分の値が重複していることが確認できます。4.10.3. 任意のタイミングで値をまとめる Buffer メソッドのオーバーロード これまで個数と時間で値をまとめる Buffer メソッドのオーバーロードを見てきましたが、任意の タ゗ミングで値をまとめるのを区切るオーバーロードもあります。シグネチャを下記に示します。 public static IObservable<IList<TSource>> Buffer<T, TBufferClosing>(this IObservable<T> source, Func<IObservable<TBufferClosing>> bufferClosingSelector); bufferClosingSelector デリゲートが返す IObservable<TBufferClosing>が OnNext を発行した タ゗ミングで値をまとめるのを辞めます。コード例を下記に示します。 121
  • var gate = new EventWaitHandle(false, EventResetMode.AutoReset); Observable // 500ms 間隔で値を発行 .Interval(TimeSpan.FromMilliseconds(500)) // 任意の値で値をまとめるのを辞める(この場合 3 秒間隔) .Buffer(() => Observable.Interval(TimeSpan.FromSeconds(3))) // 最初の 3 つだけ後続に流す .Take(3) .Subscribe( l => { // 値を表示 Console.WriteLine("--Buffer {0:HH:mm:ss}", DateTime.Now); foreach (var i in l) { Console.WriteLine(i); } }, () => { // 完了 Console.WriteLine("OnCompleted"); gate.Set(); }); // OnCompleted 待ち Console.WriteLine("WaitOne"); gate.WaitOne(); Console.WriteLine("WaitOne Completed");このコード例では、Buffer(() => Observable.Interval(TimeSpan.FromSeconds(3)))のようにして、3 秒で値をまとめるのを終了するようにしています。実行結果を下記に示します。WaitOne--Buffer 21:08:5501234 122
  • 5--Buffer 21:08:5867891011--Buffer 21:09:01121314151617OnCompletedWaitOne Completed指定した通り、3 秒間隔で値をまとめていることが確認できます。この Buffer の引数に渡したデリゲートは、Buffer を開始するタ゗ミングで毎回評価されるので、その時の状況に応じた終了タ゗ミングを表す IObservable<TBufferClosing>を返すことが出来ます。例えば、ボタンのクリック゗ベントを IObservable にしたものを渡すと、クリックのタ゗ミングで値のグルーピングを終了させるといった使い方も出来ます。また、IObservable を受け取るオーバーロードには値を集めるタ゗ミングと、値を集めるのを終了するタ゗ミングを任意に指定するオーバーロードも定義されています。 シグネチャを下記に示します。 public static IObservable<IList<T>> Buffer<T, TBufferOpening, TBufferClosing>( this IObservable<T> source, IObservable<TBufferOpening> bufferOpenings, Func<TBufferOpening, IObservable<TBufferClosing>> bufferClosingSelector);bufferOpenings は、もとになる IObservable<T>のシーケンスから値をあつめはじめるタ゗ミングを指定します。bufferOpenings から値が発行されると bufferClosingSelector に、その値が渡され、 値の収集を終了するタ゗ミングを通知する IObservable<TBufferClosing>が作成されます。このオーバーロードの使用例を下記に示します。 var gate = new EventWaitHandle(false, EventResetMode.AutoReset); // クリックをエミュレート var clickEmuration = new Subject<Unit>(); Observable // 500ms 間隔で値を発行 123
  • .Interval(TimeSpan.FromMilliseconds(500)) // 任意の値で値をまとめるのを辞める(この場合 3 秒間隔) .Buffer( // clickEmuration から通知がきたら clickEmuration.AsObservable(), // 2 秒間値を集める _ => Observable.Interval(TimeSpan.FromSeconds(2))) // 最初の 2 つだけ後続に流す .Take(2) .Subscribe( l => { // 値を表示 Console.WriteLine("--Buffer {0:HH:mm:ss}", DateTime.Now); foreach (var i in l) { Console.WriteLine(i); } }, () => { // 完了 Console.WriteLine("OnCompleted"); gate.Set(); });// Enter を押すとクリックを模した Subject から通知を上げるConsole.ReadLine();Console.WriteLine("{0:HH:mm:ss} Click emurate", DateTime.Now);clickEmuration.OnNext(Unit.Default);// Enter を押すとクリックを模した Subject から通知を上げるConsole.ReadLine();Console.WriteLine("{0:HH:mm:ss} Click emurate", DateTime.Now);clickEmuration.OnNext(Unit.Default);// OnCompleted 待ち 124
  • gate.WaitOne(); Console.WriteLine("WaitOne Completed"); このコードは、clickEmuration から値が発行されてから 2 秒間 IObservable<long>のシーケン スから値を収集して出力します。このコード例では Subject<Unit>を使用していますが、これを ボタンのクリック゗ベントや、何か別の契機を表す IObservable に変えることができることを考 えると、かなり柔軟性の高いオーバーロードだと言えます。実行結果を下記に示します。 21:22:02 Click emurate --Buffer 21:22:04 5 6 7 8 21:22:08 Click emurate --Buffer 21:22:10 17 18 19 20 OnCompleted WaitOne Completed4.10.4. 時間と数でまとめる Buffer メソッドのオーバーロード 最後に、 紹介するのは時間と数の両方でまとめる Buffer メソッドのオーバーロードです。 これは、 「4.10.1 数でまとめる Buffer メソッドのオーバーロード」と「4.10.2 時間でまとめる Buffer メソッドのオーバーロード」 で紹介した内容をあわせたもので、 指定した時間か指定した数のどち らかがそろったタ゗ミングで後続にまとめた値を流します。 メソッドのシグネチャを下記に示しま す。 public static IObservable<IList<T>> Buffer<T>(this IObservable<T> source, TimeSpan timeSpan, int count); timeSpan で時間間隔を指定し、count でまとめる個数を指定します。コード例を下記に示します。 var gate = new EventWaitHandle(false, EventResetMode.AutoReset); Observable // 0-9 の値を i * 200 ms 間隔で発行する .Generate( 0, i => i < 10, 125
  • i => ++i, i => i, i => TimeSpan.FromMilliseconds(i * 200)) // 2 秒間隔か 2 つ値がくるまでまとめる .Buffer(TimeSpan.FromSeconds(2), 2) .Subscribe( l => { // 値を表示 Console.WriteLine("--Buffer {0:HH:mm:ss}", DateTime.Now); foreach (var i in l) { Console.WriteLine(i); } }, () => { // 完了 Console.WriteLine("OnCompleted"); gate.Set(); }); gate.WaitOne();実行結果を下記に示します。--Buffer 21:44:3701--Buffer 21:44:3823--Buffer 21:44:4045--Buffer 21:44:426--Buffer 21:44:447--Buffer 21:44:46 126
  • 8 9 --Buffer 21:44:46 OnCompleted 最初の方は、値の発行間隔が短いため count 引数で指定した 2 つの値が流れてきていますが、後 半になってくると、値の発行間隔が長くなってくるため、timeSpan で指定した間隔で値がまとめ られていることが確認できます。4.10.5. Window メソッド ここでは Window メソッドについて説明します。Window メソッドは、ここまで説明してきた Buffer メソッドとほぼ同じ動きをします。指定した時間や数や間隔で柔軟に値をまとめることが 出来ます。Buffer メソッドとの違いを比べるために数でまとめる一番シンプルなオーバーロード を並べて以下に示します。 まずは、Buffer メソッドです。 public static IObservable<IList<T>> Buffer<T>( this IObservable<T> source, int count); 次に、Window メソッドを示します。 public static IObservable<IObservable<T>> Window<T>( this IObservable<T> source, int count); Buffer メソッドの戻り値が IObservable<IList<T>>なのに対して Window メソッドは IObservable<IObservable<T>>になります。このシグネチャの違いが挙動にどう影響している のかを比較するためのコードを下記に示します。 まずは、これまで使ってきた Buffer メソッドのコード例を下記に示します。 // 値の発行元 var source = new Subject<int>(); source // 3 つずつに纏める .Buffer(3) .Subscribe( l => { // 値を表示 Console.WriteLine("--Buffer"); // IList<T>なのでループを使って値を出力 foreach (var i in l) { 127
  • Console.WriteLine(i); } }, () => { // 完了通知 Console.WriteLine("Buffer Completed"); }); // 1~4 の値を発行して終了 Console.WriteLine("OnNext(1)"); source.OnNext(1); Console.WriteLine("OnNext(2)"); source.OnNext(2); Console.WriteLine("OnNext(3)"); source.OnNext(3); Console.WriteLine("OnNext(4)"); source.OnNext(4); Console.WriteLine("OnCompleted()"); source.OnCompleted();Subject<int>を使って、1~4 の値を発行しています。そして、Buffer メソッドで 3 つずつの値にまとめて表示しています。実行結果を以下に示します。OnNext(1)OnNext(2)OnNext(3)--Buffer123OnNext(4)OnCompleted()--Buffer4Buffer Completed実行結果からわかるように、IObservable<int>のシーケンスから値が 3 つ発行されると Bufferの Subscribe の OnNext が呼ばれて値が列挙されています。 IObservable<int>のシーケンスが終了すると、残った値をまとめて Buffer の Subscribe の OnNext が呼ばれます。これが Buffer メ 128
  • ソッドでの挙動です。次に、このコードの Buffer メソッドの部分を Window メソッドにしたコードを示します。 // 値の発行元 var source = new Subject<int>(); source // 3 つずつに纏める .Window(3) .Subscribe( o => { // 値を表示 Console.WriteLine("--Window"); // IO<T>なので Subscribe で OnNext を使って出力する o.Subscribe(Console.WriteLine); }, () => { // 完了通知 Console.WriteLine("Window Completed"); }); // 1~4 の値を発行して終了 Console.WriteLine("OnNext(1)"); source.OnNext(1); Console.WriteLine("OnNext(2)"); source.OnNext(2); Console.WriteLine("OnNext(3)"); source.OnNext(3); Console.WriteLine("OnNext(4)"); source.OnNext(4); Console.WriteLine("OnCompleted()"); source.OnCompleted();Buffer メソッドを Window メソッドに変えたのと、Subscribe の OnNext で渡ってくる型がIObservable<int>になっているためループから Subscribe に書き換えています。 このコードの実行結果を以下に示します。--WindowOnNext(1) 129
  • 1 OnNext(2) 2 OnNext(3) 3 --Window OnNext(4) 4 OnCompleted() Window Completed Buffer メソッドが、値が 3 つ揃ってから IList<int>という形で後続に値を流しているのに対して Window メソッドは、値が 3 つ揃う前から即座に後続に値を流していることが確認できます。 Window メソッドの戻り値が IObservable<IObservable<T>>になっているため、リゕルタ゗ム に値を監視して処理することが出来るようになっています。 そのため Buffer メソッドは、値が全て揃ってから後続に値を流したい場合に、Window メソッド は、値が発行されたら即座に後続に値を流したい場合に利用します。 その他のオーバーロードに関 しても挙動は同じになります。そのため、ここではその他の Window メソッドのオーバーロード についての説明は割愛します。4.11. 発行された値にたいして時間でフィルタ・操作するメソッド ここでは、IObservable<T>のシーケンスから発行される値に対して時間で何かしらの影響を与 えるメソッドの説明を行います。4.11.1. Sample メソッド Sample メソッドについて説明します。Sample メソッドは、指定した間隔(時間や任意のタ゗ミ ング)で最後に発行された値を後続に流すメソッドになります。大量の値が発行されるような IObservable<T>のシーケンスから任意の間隔で値を絞って後続の処理に渡すケースなどで利用 できます。 メソッドのシグネチャを下記に示します。 public static IObservable<T> Sample<T>( this IObservable<T> source, TimeSpan interval); このオーバーロードは、時間で値をフゖルタリングします。もう1つ任意の間隔で後続に最後の値 を流すオーバーロードは下記のようなシグネチャになっています。 public static IObservable<T> Sample<T, TSample>( this IObservable<T> source, IObservable<TSample> sampler); 130
  • このオーバーロードは、IObservable<TSample>の OnNext で値が発行されるタ゗ミングで後続に値を流します。このオーバーロードを使うことで、非同期処理が終わったタ゗ミングや、ボタンのクリック゗ベントなどをトリガーにして、一番最後に発行された値を後続に流せます。コード例を以下に示します。 var r = new Random(); var subscriber = Observable // 100ms の間隔で .Interval(TimeSpan.FromMilliseconds(100)) // 0~99 の乱数を発生させる .Select(_ => r.Next(100)) // 値が発行されたことを確認するためのダンプ .Do(i => Console.WriteLine("{0:HH:mm:ss} Dump {1}", DateTime.Now, i)) // 1 秒間隔で最後に発行された値を後続に流す .Sample(TimeSpan.FromMilliseconds(1000)) // 購読 .Subscribe( // 値を表示 i => Console.WriteLine("{0:HH:mm:ss} OnNext {1}", DateTime.Now, i)); Console.WriteLine("Please enter key"); Console.ReadLine(); // 購読終了 subscriber.Dispose();100 ミリ秒間隔で 0~99 の値を発行しています。この値を Sample メソッドを使って 1 秒間隔で最後に発行された値に絞り込んで Subscribe で結果を表示しています。実行結果を以下に示します。Please enter key23:13:23 Dump 6323:13:23 Dump 023:13:23 Dump 3623:13:23 Dump 1823:13:23 Dump 5323:13:24 Dump 6523:13:24 Dump 6323:13:24 Dump 9523:13:24 Dump 3923:13:24 Dump 4423:13:24 OnNext 44 131
  • 23:13:24 Dump 7223:13:24 Dump 9123:13:24 Dump 3623:13:24 Dump 3723:13:24 Dump 6823:13:25 Dump 6923:13:25 Dump 9823:13:25 Dump 6723:13:25 Dump 7923:13:25 Dump 2323:13:25 OnNext 2323:13:25 Dump 623:13:25 Dump 5323:13:25 Dump 2223:13:25 Dump 4723:13:25 Dump 9423:13:26 Dump 6723:13:26 Dump 9923:13:26 Dump 223:13:26 Dump 7023:13:26 Dump 7623:13:26 OnNext 7023:13:26 Dump 82長い実行結果になりますが、Dump が大量に表示されていることから 100ms ごとに乱数が発生していることが確認できます。その中で Subscribe の OnNext まで渡っている値(実行結果の中で太字の赤文字にしている箇所)は 1 秒間隔になっていることが確認できます。上記の例では、発行される値の間隔に比べて Sample で取得する値をフゖルタリングするように設定していましたが、逆に値は 10 秒間隔で発行されるのに対して Sample メソッドで 8 秒でフゖルタリングしたときに、どのような挙動になるのかを確認したいと思います。 コード例を下記に示します。 var r = new Random(); var subscriber = Observable // 10 秒間隔で .Interval(TimeSpan.FromSeconds(10)) // 乱数を発生させる .Select(_ => r.Next(100)) // 発生した乱数を表示 132
  • .Do(i => Console.WriteLine("{0:HH:mm:ss} Dump {1}", DateTime.Now, i)) // 8 秒間隔でフゖルタリングする .Sample(TimeSpan.FromSeconds(8)) .Subscribe( // 渡ってきた値を表示する i => Console.WriteLine("{0:HH:mm:ss} OnNext {1}", DateTime.Now, i)); Console.WriteLine("Please enter key"); Console.ReadLine(); // 購読解除 subscriber.Dispose(); 実行結果は以下のようになります。 Please enter key 23:23:20 Dump 1 23:23:20 OnNext 1 23:23:22 Dump 39 23:23:22 OnNext 39 23:23:24 Dump 91 23:23:24 OnNext 91 23:23:26 Dump 61 23:23:26 OnNext 61 結果からわかるように、Sample メソッドは、8 秒待っても値が発行されない(元のシーケンスは 10 秒間隔での値の発行のため)ので、値が発行されなかった場合は何もせず次のタ゗ミングを待 っています、次のタ゗ミングで値がきていたら後続に値を流す動きをします。 IObservable<TSample>を渡して任意のタ゗ミングで、後続に値を流すメソッドのオーバーロー ドについては、割愛します。4.11.2. Throttle メソッド ここでは、Throttle メソッドについて説明します。Throttle メソッドは指定した間、新たな値が 発行されなかったら最後に発行された値を後続に流すメソッドです。 メソッドのシグネチャを以下 に示します。 public static IObservable<T> Throttle<T>( this IObservable<T> source, TimeSpan dueTime); 第二引数の dueTime で後続に値を流すための判断基準になる間隔を指定します。このメソッドの 使用例を下記に示します。 var source = new Subject<int>(); // 500ms 値が発行されなかったら最後に発行された値を後続に流す 133
  • source .Throttle(TimeSpan.FromMilliseconds(500)) // 渡ってきた値を時間つきで表示 .Subscribe(i => Console.WriteLine("{0:HH:mm:ss.fff} {1}", DateTime.Now, i)); // 100ms 間隔で値を発行 foreach (var i in Enumerable.Range(1, 10)) { // 発行した値を出力しておく Console.WriteLine("{0:HH:mm:ss.fff} OnNext({1})",DateTime.Now, i); source.OnNext(i); Thread.Sleep(100); } // 2000ms sleep Console.WriteLine("{0:HH:mm:ss.fff} Sleep(2000)", DateTime.Now); Thread.Sleep(2000); // 100ms 間隔で値を発行 foreach (var i in Enumerable.Range(1, 5)) { // 発行した値を出力しておく Console.WriteLine("{0:HH:mm:ss.fff} OnNext({1})", DateTime.Now, i); source.OnNext(i); Thread.Sleep(100); } // 2000ms sleep Console.WriteLine("{0:HH:mm:ss.FFF} Sleep(2000)", DateTime.Now); Thread.Sleep(2000);Throttle メソッドで 500ms の間、 値が発行されなかった場合に後続に値を流すようにしています。そのあと、foreach で値を 10 個 100ms 間隔で発行して 2000ms スリープしています。次に、値を 5 個 100ms 間隔で発行して、2000ms スリープしています。実行結果を以下に示します。23:05:36.003 OnNext(1)23:05:36.116 OnNext(2)23:05:36.217 OnNext(3) 134
  • 23:05:36.317 OnNext(4) 23:05:36.417 OnNext(5) 23:05:36.517 OnNext(6) 23:05:36.617 OnNext(7) 23:05:36.717 OnNext(8) 23:05:36.817 OnNext(9) 23:05:36.917 OnNext(10) 23:05:37.023 Sleep(2000) 23:05:37.420 10 23:05:39.026 OnNext(1) 23:05:39.126 OnNext(2) 23:05:39.226 OnNext(3) 23:05:39.326 OnNext(4) 23:05:39.426 OnNext(5) 23:05:39.526 Sleep(2000) 23:05:39.926 5 実行結果で太字の赤文字にしている箇所が、Subscribe で出力している箇所です。直前の OnNext から 500ms 後に出力されていることが確認できます。4.11.3. Delay メソッド ここでは、Delay メソッドについて説明します。Delay メソッドは名前が示す通り IObservable<T>のシーケンスから発行された値を指定した時間だけ遅延させて後続に流すメソ ッドになります。 メソッドのシグネチャを以下に示します。 public static IObservable<T> Delay<T>( this IObservable<T> source, TimeSpan dueTime); 引数の dueTime で遅延させる時間を指定します。TimeSpan 型を受け取る以外にも、特定の時点 まで遅延させることも出来ます。このメソッドのコード例を下記に示します。 var source = new Subject<int>(); source // 10 秒遅延させる .Delay(TimeSpan.FromSeconds(10)) // 値を時間つきで表示させる .Subscribe(i => Console.WriteLine("{0:HH:mm:ss.fff} {1}", DateTime.Now, i)); // 1 秒間隔で 1~5 の値を発行 135
  • foreach (var i in Enumerable.Range(1, 5)) { Console.WriteLine("{0:HH:mm:ss.fff} OnNext({1})", DateTime.Now, i); source.OnNext(i); Thread.Sleep(1000); } // 終了待ち Console.WriteLine("Please enter key"); Console.ReadLine(); Delay メソッドを使って IObservable<int>のシーケンスから発行された値を 10 秒遅延させてい ます。実行結果を以下に示します。 21:58:12.174 OnNext(1) 21:58:13.202 OnNext(2) 21:58:14.202 OnNext(3) 21:58:15.202 OnNext(4) 21:58:16.202 OnNext(5) Please enter key 21:58:22.214 1 21:58:23.205 2 21:58:24.202 3 21:58:25.216 4 21:58:26.215 5 実行結果からわかるように、OnNext で発行した値が 10 秒後に表示されています。このように Delay メソッドを使うと、値を遅延させて後続に流すということが実現できます。4.11.4. Timeout メソッド ここでは、Timeout メソッドについて説明します。Timeout メソッドは名前の通り IObservable<T>のシーケンスから指定した時間、値が発行されなかった場合にエラーにするメ ソッドです。シグネチャを以下に示します。 public static IObservable<T> Timeout<T>( this IObservable<T> source, TimeSpan dueTime); dueTime でタ゗ムゕウトの時間を指定します。TimeSpan 以外にも DateTimeOffset でタ゗ムゕ ウトを指定するオーバーロードがありますが、 ここでは紹介を割愛します。 このメソッドのコード 例を下記に示します。 var subscriber = Observable // 0~4 の値を i 秒間隔で発行する 136
  • .Generate(0, i => i < 5, i => i + 1, i => i, i => TimeSpan.FromSeconds(i)) // 3500ms 以上間隔があくとタ゗ムゕウト .Timeout(TimeSpan.FromMilliseconds(3500)) // 購読 .Subscribe( i => Console.WriteLine("{0:HH:mm:ss} OnNext({1})", DateTime.Now, i), ex => Console.WriteLine("{0:HH:mm:ss} OnError({1})", DateTime.Now, ex), () => Console.WriteLine("{0:HH:mm:ss} OnCompleted()", DateTime.Now)); // Enter を押すと購読終了 Console.ReadLine(); subscriber.Dispose();このコードでは、Generate メソッドを使って 0, 1, 2, 3, 4 の値を 0s, 1s, 2s, 3s, 4s 間隔で発行しています。それに対して Timeout メソッドで 3.5s(3500ms)を指定しています。このコードの実行結果を以下に示します。12:39:48 OnNext(0)12:39:49 OnNext(1)12:39:51 OnNext(2)12:39:54 OnNext(3)12:39:58 OnError(System.TimeoutException: 操作がタ゗ムゕウトしました。)最後の 4 が発行されるのに 4 秒間が空いてしまうので、TimeoutException が発行され Subscribeの OnError に処理がいきます。このように、時間を要する処理のタ゗ムゕウトを簡単に指定することが出来ます。この Timeout メソッドにはタ゗ムゕウト時の動作をカスタマ゗ズすることが出来るオーバーロードがあります。そのメソッドのシグネチャを以下に示します。 public static IObservable<T> Timeout<T>( this IObservable<T> source, TimeSpan dueTime, IObservable<T> other);第三引数の other で、タ゗ムゕウトが起きたときに代わりに値を発行する IObservable<T>のシーケンスを指定します。このオーバーロードの使用例を下記に示します。 var subscriber = Observable // 0~4 の値を i 秒間隔で発行する .Generate(0, i => i < 5, i => i + 1, i => i, i => TimeSpan.FromSeconds(i)) // 3500ms 以上間隔があくとタ゗ムゕウト .Timeout( TimeSpan.FromMilliseconds(3500), // タ゗ムゕウトの時に流す値を指定 Observable.Create<int>(o => 137
  • { Console.WriteLine("{0:HH:mm:ss} Error Action", DateTime.Now); // -1 を流して完了 o.OnNext(-1); o.OnCompleted(); return Disposable.Empty; })) // 購読 .Subscribe( i => Console.WriteLine("{0:HH:mm:ss} OnNext({1})", DateTime.Now, i), ex => Console.WriteLine("{0:HH:mm:ss} OnError({1})", DateTime.Now, ex), () => Console.WriteLine("{0:HH:mm:ss} OnCompleted()", DateTime.Now)); // Enter を押すと購読終了 Console.ReadLine(); subscriber.Dispose(); Timeout メソッドで Observable.Create メソッドを使ってタ゗ムゕウトの時に-1 の値を流して IObservable<T>のシーケンスを完了させるようにしています。このコードの実行結果を以下に 示します。 13:08:09 OnNext(0) 13:08:10 OnNext(1) 13:08:12 OnNext(2) 13:08:15 OnNext(3) 13:08:19 Error Action 13:08:19 OnNext(-1) 13:08:19 OnCompleted() 最初のコード例では TimeoutException が発生していましたが、この例では Observable.Create で作成した IObservable から発行された値が後続に流れていることが確認できます。このように タ゗ムゕウトとタ゗ムゕウトに伴って発行する値を差し替えたりすることが出来ます。4.12. 時間に関する情報を付与する拡張メソッド ここでは、IObservable<T>のシーケンスに対して時間に関する情報を付与する IObservable<T>の拡張メソッドについて説明します。 138
  • 4.12.1. Timestamp メソッド ここでは、Timesptamp メソッドについて説明します。Timestamp メソッドは、IObservable<T> のシーケンスに対して、 その名の通りタ゗ムスタンプを追加するメソッドです。 メソッドのシグネ チャを以下に示します。 public static IObservable<Timestamped<T>> Timestamp<T>(this IObservable<T> source); 戻り値が IObservable<Timestamped<T>>型になっています。この Timestamped<T>型は以 下のように定義されています。(演算子のオーバーロード等は省いています) using System; namespace System.Reactive { public struct Timestamped<T> { public DateTimeOffset Timestamp { get; } public T Value { get; } } } Timestamp プロパテゖで、 時間を取得し Value プロパテゖで値を取得するシンプルな構造体です。 コード例を下記に示します。 var subscriber = Observable // 1 秒間隔で 0~4 の値を発行 .Generate(0, i => i < 5, i => i + 1, i => i, _ => TimeSpan.FromSeconds(1)) // タ゗ムスタンプをつける(Timestamped<T>型になる) .Timestamp() // 購読 .Subscribe( i => Console.WriteLine("Timestamp: {0}, Value: {1}", i.Timestamp, i.Value)); // 終了待ち Console.WriteLine("Please enter key."); Console.ReadLine(); subscriber.Dispose(); 1 秒間隔で 0~4 の値を発行して、それに Timestamp メソッドを呼び出して Timestamped<T> 型に変換しています。そして Subscribe で Timestamp と Value を表示しています。実行結果を 以下に示します。 Please enter key. 139
  • Timestamp: 2012/02/05 22:47:59 +09:00, Value: 0 Timestamp: 2012/02/05 22:48:00 +09:00, Value: 1 Timestamp: 2012/02/05 22:48:01 +09:00, Value: 2 Timestamp: 2012/02/05 22:48:02 +09:00, Value: 3 Timestamp: 2012/02/05 22:48:03 +09:00, Value: 4 値が発行された時の時間が表示されていることが確認できます。4.12.2. TimeInterval メソッド ここでは、TimeInterval メソッドについて説明します。TimeInterval メソッドは IObservable<T>のシーケンスから発行された値の間隔を TimeSpan 型で追加します。メソッド のシグネチャを以下に示します。 public static IObservable<TimeInterval<T>> TimeInterval<T>(this IObservable<T> source); 戻り値が、IObservable<TimeInterval<T>>型になっています。TimeInterval<T>型は以下の ように定義されています。(演算子のオーバーロード等は省いています) using System; namespace System.Reactive { public struct TimeInterval<T> { public TimeSpan Interval { get; } public T Value { get; } } } Interval プロパテゖで時間間隔を取得し、Value プロパテゖで値を取得するシンプルな構造体です。 コード例を下記に示します。 var subscriber = Observable // 1 秒間隔で 0~4 の値を発行 .Generate(0, i => i < 5, i => i + 1, i => i, _ => TimeSpan.FromSeconds(1)) // 値の発行間隔をつける(TimeInterval<T>型になる) .TimeInterval() // 購読 .Subscribe( i => Console.WriteLine("Interval: {0}, Value: {1}", i.Interval, i.Value)); // 終了待ち Console.WriteLine("Please enter key."); 140
  • Console.ReadLine(); subscriber.Dispose(); 1 秒間隔で 0~4 の値を発行して、それに TimeInterval メソッドを呼び出して TimeInterval<T> 型に変換しています。そして Subscribe で Interval と Value を表示しています。実行結果を以下 に示します。 Please enter key. Interval: 00:00:01.0050575, Value: 0 Interval: 00:00:01.0150581, Value: 1 Interval: 00:00:01.0140580, Value: 2 Interval: 00:00:01.0140580, Value: 3 Interval: 00:00:01.0120578, Value: 4 値が発行された間隔が表示されていることが確認できます。4.13. 型変換を行う拡張メソッド ここでは、IObservable<T>のシーケンスに対して型変換を行うメソッドについて説明します。4.13.1. Cast メソッド ここでは Cast メソッドについて説明します。Cast メソッドは名前の通り、もとになる IObservable<object>のシーケンスから発行された値をキャストして後続に流します。メソッド のシグネチャを以下に示します。 public static IObservable<T> Cast<T>(this IObservable<object> source); このメソッドの使用例を下記に示します。 var source = new Subject<object>(); source // 値を int 型にキャスト .Cast<int>() // 購読 .Subscribe( i => Console.WriteLine("OnNext({0})", i), ex => Console.WriteLine("OnError({0})", ex), () => Console.WriteLine("OnCompleted()")); // int 型の値を発行 source.OnNext(1); source.OnNext(2); source.OnNext(3); 141
  • // 文字列型を発行してみる source.OnNext("abc"); Cast メソッドを使って object 型から int 型に変換して Subscribe で購読しています。そして、1 ~3 の値を発行したあとに文字列型の”abc”を発行しています。実行結果を以下に示します。 OnNext(1) OnNext(2) OnNext(3) OnError(System.InvalidCastException: 指定されたキャストは有効ではありません。 場所 System.Reactive.Linq.Observable.<Cast>b__46e[TResult](Object x) 場所 System.Reactive.Linq.Observable.<>c__DisplayClass408`2.<>c__DisplayClass 40a.<Select>b__407(TSource x)) 実行結果からわかる通り、文字列型を発行すると InvalidCastException が発行されて OnError に処理がいっていることが確認できます。このように Cast メソッドは、あくまで単純なキャスト を行うためのメソッドであることがわかります。4.13.2. OfType メソッド ここでは、OfType メソッドについて説明します。OfType メソッドは、もとになる IObservable<object>のシーケンスから指定した型でフゖルタリングを行うメソッドになります。 メソッドのシグネチャを以下に示します。 public static IObservable<T> OfType<T>(this IObservable<object> source); このメソッドのコード例を下記に示します。 var source = new Subject<object>(); source // int 型のみを通過させる .OfType<int>() // 購読 .Subscribe( i => Console.WriteLine("OnNext({0})", i), ex => Console.WriteLine("OnError({0})", ex), () => Console.WriteLine("OnCompleted()")); // int 型の値を発行 source.OnNext(1); source.OnNext(2); source.OnNext(3); // 文字列型を発行してみる 142
  • source.OnNext("abc"); source.OnNext("4"); // もう一度 int 型の値を発行して終了 source.OnNext(100); source.OnCompleted(); OfType メソッドを使って、1~3 の値が発行されたあと文字列型で”abc”と”4”が発行されて、100 が発行されるシーケンスをフゖルタリングして、購読しています。実行結果を以下に示します。 OnNext(1) OnNext(2) OnNext(3) OnNext(100) OnCompleted() Cast メソッドとは異なり、文字列型が発行されても例外は発生しません。単純に後続に値が流れ ることなくフゖルタリングされます。4.14. Cold から Hot へ変換する拡張メソッド ここでは、Cold な IObservable<T>のシーケンスを Hot な IObservable<T>のシーケンスに変 換するメソッドについて説明します。Cold から Hot に変換することで、複数回 Subscribe したと きに、毎回値を発行するのか、 まとめて値を発行するのかといったことを細かく制御することが可 能になります。4.14.1. Publish メソッド ここでは、Publish メソッドについて説明します。Publish メソッドは、Cold な IObservable<T> を Hot な IObservable<T>に変換するメソッドです。Cold な IObservable<T>は、Subscribe で追加された Observer ごとに独立して値を流し込みます。しかし、この Publish メソッドを使う ことで、複数の Observer に対して同時に同一の値を流すことが出来ます。Publish メソッドのシ グネチャを以下に示します。 public static IConnectableObservable<T> Publish<T>(this IObservable<T> source); Publish メソッドの戻り値の IConnectableObservable<T>は、Connect というメソッドを持っ た IObservable<T>になります。この Connect メソッドを呼び出すまで、元になった IObservable<T>の値を発行するのを遅延させます。この Connect メソッドのシグネチャを以下 に示します。 IDisposable Connect(); Connect メソッドは、戻り値の IDisposable に対して Dispose メソッドが呼び出されるまで Hot な IObservable<T>として振る舞います。 コード例を下記に示します。 // cold な Observable の作成 143
  • var source = Observable.Range(1, 3);// 2 回購読Console.WriteLine("# Subscribe1");source.Subscribe( i => Console.WriteLine("Subscribe1#OnNext: {0}", i), () => Console.WriteLine("Subscribe1#OnCompleted"));Console.WriteLine("# Subscribe2");source.Subscribe( i => Console.WriteLine("Subscribe2#OnNext: {0}", i), () => Console.WriteLine("Subscribe2#OnCompleted"));Console.WriteLine("--");// Publish で Hot な Observable にConsole.WriteLine("# Publish");var connectableSource = source.Publish();// 2 回購読Console.WriteLine("# Subscribe1");connectableSource.Subscribe( i => Console.WriteLine("Subscribe1#OnNext: {0}", i), () => Console.WriteLine("Subscribe1#OnCompleted"));Console.WriteLine("# Subscribe2");connectableSource.Subscribe( i => Console.WriteLine("Subscribe2#OnNext: {0}", i), () => Console.WriteLine("Subscribe2#OnCompleted"));// Connect で購読している Observer に値を流すConsole.WriteLine("# Connect");connectableSource.Connect();// Connect 後に購読したときの動作確認Console.WriteLine("# Subscribe3");connectableSource.Subscribe( i => Console.WriteLine("Subscribe3#OnNext: {0}", i), () => Console.WriteLine("Subscribe3#OnCompleted")); 144
  • まず、Range メソッドで 0~2 の値を発行する Cold な IObservable<T>を作成しています。Publish をした時としてない時とで動きを比較するために、Publish をする前に 2 回 Subscribe をしています。 その後、Publish メソッドを呼び出して Hot な IObservable<T>へ変換しています。そして、2 回購読を行って Connect メソッドで値の発行を開始しています。上記コードの実行結果を以下に示します。# Subscribe1Subscribe1#OnNext: 1Subscribe1#OnNext: 2Subscribe1#OnNext: 3Subscribe1#OnCompleted# Subscribe2Subscribe2#OnNext: 1Subscribe2#OnNext: 2Subscribe2#OnNext: 3Subscribe2#OnCompleted--# Publish# Subscribe1# Subscribe2# ConnectSubscribe1#OnNext: 1Subscribe2#OnNext: 1Subscribe1#OnNext: 2Subscribe2#OnNext: 2Subscribe1#OnNext: 3Subscribe2#OnNext: 3Subscribe1#OnCompletedSubscribe2#OnCompleted# Subscribe3Subscribe3#OnCompletedIConnectableObservable<T>の Connect メソッドが呼ばれるまで、値を購読しているところまで値が流れていないことが確認できます。値の発行が終わった後に、Subscribe したときにはIObservable<T>は完了している状態のため、 OnCompleted が実行されていることが確認できます。Publish の動作を確認するために、 もう1つコード例を見ていきます。 今度は Interval メソッドを使って 500ms ごとに 0 からカウントゕップしていく Cold な IObservable<T>に対して Publishメソッドを呼び出しています。購読者のいない状態での Connect と、Connect したものの途中か 145
  • ら購読した場合の挙動について確認しています。 また、Connect メソッドの戻り値の IDisposableを使って Connect の解除を行っています。コードを下記に示します。 // 500ms 間隔で 0 からカウントゕップしていく値を発行する IObservable を作成 Console.WriteLine("# Create source(ConnectableObservable)"); var source = Observable .Interval(TimeSpan.FromMilliseconds(500)) .Publish(); // Connect で値の放流開始 Console.WriteLine("# Connect"); using (source.Connect()) { // 2 秒間何もせずに待つ Console.WriteLine("Sleep 2sec..."); Thread.Sleep(2000); // 2 秒間購読 Console.WriteLine("# Subscribe1"); using ( source.Subscribe( i => Console.WriteLine("Subscribe1#OnNext: {0}", i), () => Console.WriteLine("Subscribe1#OnCompleted"))) { Console.WriteLine("Sleep 2sec..."); Thread.Sleep(2000); } Console.WriteLine("# UnSubscribe1"); } // Dispose が呼ばれるので Connect が解除される Console.WriteLine("# DisConnect"); // Connect が解除された状態で 2 秒間購読する Console.WriteLine("# Subscribe2"); using ( source.Subscribe( i => Console.WriteLine("Subscribe2#OnNext: {0}", i), () => Console.WriteLine("Subscribe2#OnCompleted"))) 146
  • { Console.WriteLine("Sleep 2sec..."); Thread.Sleep(2000); } Console.WriteLine("# UnSubscribe2"); // 再接続 Console.WriteLine("# Connect"); using (source.Connect()) { // 再接続で発行される値を確認するため 2 秒間購読する Console.WriteLine("# Subscribe3"); using ( source.Subscribe( i => Console.WriteLine("Subscribe3#OnNext: {0}", i), () => Console.WriteLine("Subscribe3#OnCompleted"))) { Console.WriteLine("Sleep 2sec..."); Thread.Sleep(2000); } Console.WriteLine("# UnSubscribe3"); } // Dispose が呼ばれるので接続解除 Console.WriteLine("# DisConnect"); // 接続解除状態で 2 秒間待機して様子を見る Console.WriteLine("Sleep 2sec..."); Thread.Sleep(2000);実行結果を以下に示します。# Create source(ConnectableObservable)# ConnectSleep 2sec...# Subscribe1Sleep 2sec...Subscribe1#OnNext: 4Subscribe1#OnNext: 5Subscribe1#OnNext: 6 147
  • # UnSubscribe1 # DisConnect # Subscribe2 Sleep 2sec... # UnSubscribe2 # Connect # Subscribe3 Sleep 2sec... Subscribe3#OnNext: 0 Subscribe3#OnNext: 1 Subscribe3#OnNext: 2 # UnSubscribe3 # DisConnect Sleep 2sec... Connect をして 2 秒経過してからの購読(Subscribe1)で値が 4 から始まっていることが確認で きます。このことから、Connect を呼び出したタ゗ミングで Interval から値が発行されているこ とがわかります。また、Connect を解除してからの購読(Subscribe2)では値が発行されていな いことも確認できます。そして最後に、再度 Connect してから購読(Subscribe3)では、値が 0 から始まっていることが確認できます。このことから、再 Connect 時には元になる Cold な IObservable<T>から再度値が発行されていることがわかります。4.14.2. RefCount メソッド ここでは、IConnectableObservable<T>の拡張メソッドとして定義されている RefCount メソッ ドについて説明します。RefCount メソッドは、購読者が1つ以上いる時は Connect 状態を維持 して購読者が 0 になったら Connect を解除する IObservable<T>を返すメソッドです。 このメソ ッドを使うことで IConnectableObservable<T>の Connect メソッドと戻り値の IDisposable の 管理を省略することが出来ます。RefCount メソッドのシグネチャを以下に示します。 public static IObservable<T> RefCount<T>(this IConnectableObservable<T> source); このメソッドの使用例を下記に示します。 // 500ms 間隔で 0 から値をカウントゕップしていく IObservable を作成 Console.WriteLine("# Create source(RefCount)"); var source = Observable .Interval(TimeSpan.FromMilliseconds(500)) // Hot .Publish() // 購読者がいる間 Connect 状態を保つ .RefCount(); 148
  • // 2 秒待機して様子身Console.WriteLine("Sleep 2sec...");Thread.Sleep(2000);// 購読開始Console.WriteLine("# Subscribe1");using ( source.Subscribe( i => Console.WriteLine("Subscribe1#OnNext: {0}", i), () => Console.WriteLine("Subscribe1#OnCompleted"))){ // 購読状態で 2 秒待機 Console.WriteLine("Sleep 2sec..."); Thread.Sleep(2000); // 追加で 1 秒間購読 Console.WriteLine("# Subscribe2"); using (source.Subscribe( i => Console.WriteLine("Subscribe2#OnNext: {0}", i), () => Console.WriteLine("Subscribe2#OnCompleted"))) { Console.WriteLine("Sleep 1sec..."); Thread.Sleep(1000); } // 1 つ購読解除 Console.WriteLine("# UnSubscribe2"); // 1 つ購読解除した状態で 2 秒待機 Console.WriteLine("Sleep 2sec..."); Thread.Sleep(2000);}// 購読解除(ここで購読者数が 0 になるので Connect が解除される)Console.WriteLine("# UnSubscribe1");// 2 秒待機Console.WriteLine("Sleep 2sec...");Thread.Sleep(2000); 149
  • // 新たに購読開始(Connect 状態になる) Console.WriteLine("# Subscribe3"); using (source.Subscribe( i => Console.WriteLine("Subscribe3#OnNext: {0}", i), () => Console.WriteLine("Subscribe3#OnCompleted"))) { // 2 秒待機 Console.WriteLine("Sleep 2sec..."); Thread.Sleep(2000); } // 購読解除 Console.WriteLine("# UnSubscribe3");using ブロックを使って購読と購読解除を複数回行っています。最初の購読(Subscribe1)の後2秒待機して、次の購読(Subscribe2)をしています。Subscribe2 は1秒後に購読を解除して、その2秒後に Subscribe1 も購読を解除しています。そして、2秒待機したあと再び購読(Subscribe3)をしています。実行結果を以下に示します。# Create source(RefCount)Sleep 2sec...# Subscribe1Sleep 2sec...Subscribe1#OnNext: 0Subscribe1#OnNext: 1Subscribe1#OnNext: 2# Subscribe2Sleep 1sec...Subscribe1#OnNext: 3Subscribe2#OnNext: 3Subscribe1#OnNext: 4Subscribe2#OnNext: 4# UnSubscribe2Sleep 2sec...Subscribe1#OnNext: 5Subscribe1#OnNext: 6Subscribe1#OnNext: 7Subscribe1#OnNext: 8# UnSubscribe1 150
  • Sleep 2sec... # Subscribe3 Sleep 2sec... Subscribe3#OnNext: 0 Subscribe3#OnNext: 1 Subscribe3#OnNext: 2 # UnSubscribe3 Subscribe1 で値を出力している途中で Subscribe2 も購読を開始しています。このときの出力を 見ると同じ値が同時に複数の購読者に対して発行されているので Hot な IObservable<T>になっ ていることが確認できます。そして、Subscribe1 と Subscribe2 の購読を解除したあと、再度購 読(Subscribe3)をすると、値が 0 からはじまっていることが確認できます。このことから、購 読者数が0になったタ゗ミングで、Connect が解除されていることがわかります。4.14.3. 引数を受け取る Publish メソッドのオーバーロード ここでは、引数を受け取る Publish メソッドのオーバーロードについて説明します。このオーバー ロードは、Subscribe したタ゗ミングで引数で渡した値を流します。その後、の動作は引数無しの Publish メソッドと同様に Connect メソッドを呼び出したタ゗ミングで値を流します。このメソ ッドのシグネチャを以下に示します。 public static IConnectableObservable<T> Publish<T>( this IObservable<T> source, TSource initialValue); このメソッドの使用例を下記に示します。 // Publish の int の引数を受け取るオーバーロード var source = Observable // 1~3 の値を発行する Cold な Observable .Range(1, 3) // initialValue に 100 を指定して Publish を呼ぶ .Publish(100); // 購読 Console.WriteLine("# Subscribe"); source.Subscribe( i => Console.WriteLine("OnNext: {0}", i), () => Console.WriteLine("OnCompleted")); // 接続 Console.WriteLine("# Connect"); source.Connect(); 151
  • Publish メソッドの引数に 100 を渡して IConnectableObservable<T>を作成しています。その 後、購読して Connect メソッドを呼び出しています。Subscribe の OnNext の呼び出されるタ゗ ミングに注目して実行結果を確認してください。このコードの実行結果を以下に示します。 # Subscribe OnNext: 100 # Connect OnNext: 1 OnNext: 2 OnNext: 3 OnCompleted Connect メソッドを呼び出す前に OnNext: 100 が表示されていることが確認できます。このよう に、Publish メソッドの引数で渡した値が Subscribe 直後に発行されていることがわかります。4.14.4. PublishLast メソッド ここでは、PublishLast メソッドについて説明します。PublishLast メソッドは最後に発行された 値を Connect 後に流します。このメソッドのシグネチャを以下に示します。 public static IConnectableObservable<T> PublishLast<T>(this IObservable<T> source); このメソッドの使用例を下記に示します。 // PublishLast var source = Observable // 1~3 の値を発行する Cold な Observable .Range(1, 3) // PublishLast で Hot 化 .PublishLast(); // 購読 Console.WriteLine("# Subscribe1"); source.Subscribe( i => Console.WriteLine("Subscribe1#OnNext: {0}", i), () => Console.WriteLine("Subscribe1#OnCompleted")); // 接続 Console.WriteLine("# Connect"); source.Connect(); // 接続後にもう一度購読 Console.WriteLine("# Subscribe2"); 152
  • source.Subscribe( i => Console.WriteLine("Subscribe2#OnNext: {0}", i), () => Console.WriteLine("Subscribe2#OnCompleted")); Connect 前に1回購読(Subscribe1)をして、Connect 後にもう一度購読(Subscribe2)しています。 このコードの実行結果を以下に示します。 # Subscribe1 # Connect Subscribe1#OnNext: 3 Subscribe1#OnCompleted # Subscribe2 Subscribe2#OnNext: 3 Subscribe2#OnCompleted OnNext に、元になる IObservable<T>から発行される 1~3 のうち最後の 3 が表示されている ことが確認できます。このことから、最後(OnCompleted 直前)に発行された値のみ後続に流し ていることがわかります。また、このメソッドは Connect 後に購読しても最後の状態をキャッシ ュしているため、Connect 前に購読したものと同じ結果が表示されています。4.14.5. Replay メソッド ここでは Replay メソッドについて説明します。Replay メソッドは、購読した対象に今まで発行 した値を全て流すという特徴があります。このため、Connect 後暫く放置して Subscribe をする と一気に値が流れてきます。このメソッドのシグネチャを以下に示します。 public static IConnectableObservable<T> Replay<T>(this IObservable<T> source); このメソッドの使用例を下記に示します。 // 500ms 間隔で 0 から値をカウントゕップしていく var source = Observable .Interval(TimeSpan.FromMilliseconds(500)) // Replay で Hot に変換 .Replay(); // 購読 Console.WriteLine("# Subscribe1"); using ( source.Subscribe( i => Console.WriteLine("Subscribe1#OnNext: {0}", i))) { // 接続 Console.WriteLine("# Connect"); 153
  • source.Connect(); // 2 秒待機 Console.WriteLine("Sleep 2sec..."); Thread.Sleep(2000); // 購読2 Console.WriteLine("# Subscribe2"); using ( source.Subscribe( i => Console.WriteLine("Subscribe1#OnNext: {0}", i))) { // 1 秒待機 Console.WriteLine("Sleep 1sec..."); Thread.Sleep(1000); } // 購読2解除 Console.WriteLine("# UnSubscribe2"); // 2 秒待機 Console.WriteLine("Sleep 2sec..."); Thread.Sleep(2000); } // 購読解除 Console.WriteLine("# UnSubscribe1");Replay メソッドで作成した IConnectableObservable<T>に対して、 回 Subscribe を行ってい 2ます。実行結果を以下に示します。# Subscribe1# ConnectSleep 2sec...Subscribe1#OnNext: 0Subscribe1#OnNext: 1Subscribe1#OnNext: 2Subscribe1#OnNext: 3# Subscribe2Subscribe1#OnNext: 0Subscribe1#OnNext: 1 154
  • Subscribe1#OnNext: 2 Subscribe1#OnNext: 3 Sleep 1sec... Subscribe1#OnNext: 4 Subscribe1#OnNext: 4 Subscribe1#OnNext: 5 Subscribe1#OnNext: 5 # UnSubscribe2 Sleep 2sec... Subscribe1#OnNext: 6 Subscribe1#OnNext: 7 Subscribe1#OnNext: 8 Subscribe1#OnNext: 9 # UnSubscribe1 2 回目の Subscribe(# Subscribe2 が表示されている箇所)でどのように OnNext に値が渡ってい るかに注目をしてください。Subscribe と同時に一気に 0~3 の値が発行されていることが確認で きます。このことから、Replay メソッドの挙動である、発行済みの値を購読した時点で一気に流 すということが確認できます。4.14.6. Multicast メソッド ここでは、Multicast メソッドについて説明します。Multicast メソッドは引数に応じて、Publish メソッド、引数のある Publish メソッド、PublishLast メソッド、Replay メソッドと同じ動きを する IConnectableObservable<T>を返すメソッドになります。このメソッドのシグネチャを以 下に示します。 public static IConnectableObservable<T> Multicast<T, U>( this IObservable<T> source, ISubject<T, U> subject); 引数の subject に応じて Publish 系の各メソッドと同じ動作をさせることが出来ます。 ISubject<T, U>゗ンターフェースは Subject<T>クラス等の Subject 系クラスが実装している゗ンターフェー スで Subject<T>クラスは ISubject<T, T>を実装しています。そのため、基本的に型引数の T と U は同じケースが現実問題としては多いので下記のようなメソッドの定義だと思っていて差支 え無いと思います。 public static IConnectableObservable<T> Multicast<T >( this IObservable<T> source, ISubject<T > subject); このメソッドの使用例を下記に示します。 Console.WriteLine("# Multicast(new Subject<int>())"); var source = Observable .Range(1, 3) 155
  • // Publish と一緒 .Multicast(new Subject<int>()); // 購読 Console.WriteLine("# Subscribe"); source.Subscribe( i => Console.WriteLine("OnNext: {0}", i), () => Console.WriteLine("OnCompleted")); // 接続 Console.WriteLine("# Connect"); source.Connect();上記コードでは Multicast メソッドに Subject<T>クラスを渡しています。コメント内にもある通り、これは引数の無い Publish メソッドと同じ動きになります。実行結果を以下に示します。# Multicast(new Subject<int>())# Subscribe# ConnectOnNext: 1OnNext: 2OnNext: 3OnCompletedPublish メソッドと同様に Connect 後に、値が発行されていることが確認できます。次に、BehaviorSubject<T>を渡したコード例を下記に示します。 Console.WriteLine("# Multicast(new BehaviorSubject<int>(100))"); var source = Observable .Range(1, 3) // Publish(100)と一緒 .Multicast(new BehaviorSubject<int>(100)); // 購読 Console.WriteLine("# Subscribe"); source.Subscribe( i => Console.WriteLine("OnNext: {0}", i), () => Console.WriteLine("OnCompleted")); // 接続 156
  • Console.WriteLine("# Connect"); source.Connect();BehaviorSubject<T>は、 5. Subject 系クラス」 「 で説明するクラスで、初期値を持つ Subject<T>クラスになります。このコードの実行結果を以下に示します。# Multicast(new BehaviorSubject<int>(100))# SubscribeOnNext: 100# ConnectOnNext: 1OnNext: 2OnNext: 3OnCompleted引数のある Publish メソッドと同様に Subscribe 直後に値が発行されて、Connect 後に元になるIObservable<T>のシーケンスの値が発行されていることが確認できます。次に、AsyncSubject<T>を渡したコード例を下記に示します。 Console.WriteLine("# Multicast(new AsyncSubject<int>())"); var source = Observable .Range(1, 3) // PublishLast と一緒 .Multicast(new AsyncSubject<int>()); // 購読 Console.WriteLine("# Subscribe"); source.Subscribe( i => Console.WriteLine("OnNext: {0}", i), () => Console.WriteLine("OnCompleted")); // 接続 Console.WriteLine("# Connect"); source.Connect();AsyncSubject<T>クラスは、OnCompleted が発行される前の最後の値をキャッシュして購読者に流す Subject です。このコードの実行結果を以下に示します。# Multicast(new AsyncSubject<int>())# Subscribe# ConnectOnNext: 3 157
  • OnCompleted上記の実行結果からもわかるとおり、最後の値だけが発行されています。このことから、AsyncSubject<T>クラスを渡した場合は PublishLast と同様の動きを行います。次に ReplaySubject<T>を渡したコード例を下記に示します。 Console.WriteLine("# Multicast(new ReplaySubject<long>())"); var source = Observable .Interval(TimeSpan.FromMilliseconds(500)) // Replay と一緒 .Multicast(new ReplaySubject<long>()); // 接続 Console.WriteLine("# Connect"); source.Connect(); // 2 秒待機 Console.WriteLine("Sleep 2sec..."); Thread.Sleep(2000); // 購読 Console.WriteLine("# Subscribe"); source.Subscribe( i => Console.WriteLine("OnNext: {0}", i), () => Console.WriteLine("OnCompleted"));ReplaySubject<T>クラスは、今まで発行した値を全てキャッシュしていて購読時に一気に発行しなおす挙動をします。上記コードでは Connect 後に 2 秒待機して Interval メソッドから 0~3の 4 つの値が発行された状態で Subscribe しています。実行結果を以下に示します。# Multicast(new ReplaySubject<long>())# ConnectSleep 2sec...# SubscribeOnNext: 0OnNext: 1OnNext: 2OnNext: 3Subscribe をした時点で、これまで発行された値が一気に OnNext に渡っていることが確認できます。 158
  • この Multicast メソッドは、任意の Subject を渡すことによって Cold な IObservable<T>から Hot な IObservable<T>への変換を行います。その際の挙動を Subject クラスを差し替えること で様々な形にカスタマ゗ズできます。5. Subject 系クラス ここでは、System.Reactive.Subjects 名前空間に定義されている Subject<T>を代表する各種 Subject 系のクラスについて説明します。5.1. Subject<T>クラス Subject<T>クラスは IObservable<T>と IObserver<T>の両方の゗ンターフェースを実装する クラスです。これまでのサンプルプログラム内で使ってきた通り、OnNext, OnError, OnCompleted の各種メソッドを呼ぶことで、値の発行、エラーの発行、完了の通知を行います。 各種ある Subject 系のクラスの中でも一番素直な挙動を行うクラスです。コード例下記に示しま す。 var source = new Subject<int>(); // 購読 1 Console.WriteLine("# Subscribe1"); source.Subscribe( i => Console.WriteLine("Subscribe1#OnNext: {0}", i), ex => Console.WriteLine("Subscribe1#OnError: {0}", ex), () => Console.WriteLine("Subscribe1#OnCompleted")); // 購読 2 Console.WriteLine("# Subscribe2"); source.Subscribe( i => Console.WriteLine("Subscribe2#OnNext: {0}", i), ex => Console.WriteLine("Subscribe2#OnError: {0}", ex), () => Console.WriteLine("Subscribe2#OnCompleted")); // 値の発行~完了 Console.WriteLine("OnNext(1)"); source.OnNext(1); Console.WriteLine("OnNext(2)"); source.OnNext(2); Console.WriteLine("OnNext(3)"); source.OnNext(3); Console.WriteLine("OnCompleted()"); 159
  • source.OnCompleted();Subject<int>に対して 2 回 Subscribe をした状態で値の発行から完了を行っています。実行結果を以下に示します。# Subscribe1# Subscribe2OnNext(1)Subscribe1#OnNext: 1Subscribe2#OnNext: 1OnNext(2)Subscribe1#OnNext: 2Subscribe2#OnNext: 2OnNext(3)Subscribe1#OnNext: 3Subscribe2#OnNext: 3OnCompleted()Subscribe1#OnCompletedSubscribe2#OnCompletedSubject<int>から値が発行される度に 2 回 OnNext に値が渡っていることが確認できます。Subject<T>クラスではエラーを通知するために、 OnError で例外を発行することも出来ます。コード例を下記に示します。 var source = new Subject<int>(); // 購読 Console.WriteLine("# Subscribe"); source.Subscribe( i => Console.WriteLine("Subscribe1#OnNext: {0}", i), ex => Console.WriteLine("Subscribe1#OnError: {0}", ex), () => Console.WriteLine("Subscribe1#OnCompleted")); // 例外 source.OnError(new Exception("Error!!"));実行結果を以下に示します。# SubscribeSubscribe1#OnError: System.Exception: Error!! 160
  • 5.2. BehaviorSubject<T>クラス BehaviorSubject<T>クラスは、初期値を持つ Subject<T>クラスになります。 BehaviorSubject<T>クラスの゗ンスタンスを作成するときにコンストラクタの引数で初期値を 渡します。Subject<T>クラスでは Subscribe するだけでは値は発行されませんでしたが、 BehaviorSubject<T>クラスは Subscribe したタ゗ミングで、初期値が発行されます。コード例 を下記に示します。 // 初期値 0 の BehaviorSubject var behaviorSubject = new BehaviorSubject<int>(0); // 購読 Console.WriteLine("## Subscribe call"); behaviorSubject.Subscribe( i => Console.WriteLine("OnNext({0})", i), ex => Console.WriteLine("OnError({0})", ex.Message), () => Console.WriteLine("OnCompleted()")); // 順に値を発行して終了 Console.WriteLine("## OnNext(1) call"); behaviorSubject.OnNext(1); Console.WriteLine("## OnNext(2) call"); behaviorSubject.OnNext(2); Console.WriteLine("## OnNext(3) call"); behaviorSubject.OnNext(3); Console.WriteLine("## OnCompleted() call"); behaviorSubject.OnCompleted(); 上記コードの実行結果を下記に示します。 ## Subscribe call OnNext(0) <- ※Subscribe した時点で OnNext に通知がいってる ## OnNext(1) call OnNext(1) ## OnNext(2) call OnNext(2) ## OnNext(3) call OnNext(3) ## OnCompleted() call OnCompleted() 161
  • Subscribe をした時点で、コンストラクタで渡した初期値の 0 が通知されていることが確認でき ます。また、BehaviorSubject<T>は、Subscribe したタ゗ミングで最後の値を返すという特徴 もあります。この動作を確認するコードを下記に示します。 // 初期値 0 の BehaviorSubject var behaviorSubject = new BehaviorSubject<int>(0); // 値を発行 Console.WriteLine("## OnNext(1) call"); behaviorSubject.OnNext(1); Console.WriteLine("Subscribe"); // 値を発行した後に購読開始 behaviorSubject.Subscribe( i => Console.WriteLine("OnNext({0})", i), () => Console.WriteLine("OnCompleted()")); behaviorSubject.OnCompleted(); // 終了した後に購読開始 Console.WriteLine("Subscribe"); behaviorSubject.Subscribe( i => Console.WriteLine("OnNext({0})", i), () => Console.WriteLine("OnCompleted()")); 実行結果を下記に示します。 ## OnNext(1) call Subscribe OnNext(1) OnCompleted() Subscribe OnCompleted() OnNext(1)を呼び出した後に Subscribe すると 1 が発行されていることが確認できます。また、 OnCompleted()をしたあとに Subscribe すると OnCompleted が呼ばれていることが確認できま す。5.3. AsyncSubject<T>クラス ここでは、AsyncSubject<T>クラスについて説明します。AsyncSubject<T>クラスは名前の通 り非同期処理をラップする際によく使用します。このクラスの特徴は、OnNext を何回発行しても 162
  • OnCompleted を行うまで値が発行されないという点です。そして OnCompleted が呼ばれると、最後の OnNext の値と OnCompleted を Observer に通知します。また、OnCompleted が呼ばれた後に購読された場合にも、最後の OnNext の値と OnCompleted を Observer に通知します。コード例を下記に示します。 var source = new AsyncSubject<int>(); // 購読 Console.WriteLine("# Subscribe1"); source.Subscribe( i => Console.WriteLine("Subscribe1#OnNext: {0}", i), ex => Console.WriteLine("Subscribe1#OnError: {0}", ex), () => Console.WriteLine("Subscribe1#OnCompleted")); // 値の発行~完了 Console.WriteLine("OnNext(1)"); source.OnNext(1); Console.WriteLine("OnNext(2)"); source.OnNext(2); Console.WriteLine("OnNext(3)"); source.OnNext(3); Console.WriteLine("OnCompleted()"); source.OnCompleted(); // OnCompleted 後の購読 Console.WriteLine("# Subscribe2"); source.Subscribe( i => Console.WriteLine("Subscribe2#OnNext: {0}", i), ex => Console.WriteLine("Subscribe2#OnError: {0}", ex), () => Console.WriteLine("Subscribe2#OnCompleted"));実行結果を以下に示します。# Subscribe1OnNext(1)OnNext(2)OnNext(3)OnCompleted()Subscribe1#OnNext: 3Subscribe1#OnCompleted 163
  • # Subscribe2 Subscribe2#OnNext: 3 Subscribe2#OnCompleted OnCompleted メソッドが呼ばれたタ゗ミングでの値の発行と、OnCompleted メソッドが呼ばれ た後の Subscribe でも値が発行されていることが確認できます。5.4. ReplaySubject<T>クラス ここでは、ReplaySubject<T>クラスについて説明します。ReplaySubject<T>クラスは、発行 した全ての値をキャッシュして Subscribe されたタ゗ミングでキャッシュしている値を全て発行 するという特徴があります。それ以外は、通常の Subject<T>クラスと同じ動きをします。コー ド例を下記に示します。 var source = new ReplaySubject<int>(); // 購読 Console.WriteLine("# Subscribe1"); source.Subscribe( i => Console.WriteLine("Subscribe1#OnNext: {0}", i), ex => Console.WriteLine("Subscribe1#OnError: {0}", ex), () => Console.WriteLine("Subscribe1#OnCompleted")); // 値の発行~完了 Console.WriteLine("OnNext(1)"); source.OnNext(1); Console.WriteLine("OnNext(2)"); source.OnNext(2); Console.WriteLine("OnNext(3)"); source.OnNext(3); Console.WriteLine("OnCompleted()"); source.OnCompleted(); // OnCompleted 後の購読 Console.WriteLine("# Subscribe2"); source.Subscribe( i => Console.WriteLine("Subscribe2#OnNext: {0}", i), ex => Console.WriteLine("Subscribe2#OnError: {0}", ex), () => Console.WriteLine("Subscribe2#OnCompleted")); 実行結果を以下に示します。 164
  • # Subscribe1 OnNext(1) Subscribe1#OnNext: 1 OnNext(2) Subscribe1#OnNext: 2 OnNext(3) Subscribe1#OnNext: 3 OnCompleted() Subscribe1#OnCompleted # Subscribe2 Subscribe2#OnNext: 1 Subscribe2#OnNext: 2 Subscribe2#OnNext: 3 Subscribe2#OnCompleted 最初の購読から値の発行、完了通知の流れでは Subject<T>クラスと同様の動きをしています。 完了後の購読時に、今まで発行した値と完了通知が名前の通りリプレ゗されていることが確認でき ます。6. IObservable の合成 ここでは、IObservable<T>クラスの合成について説明します。いままで説明してきたメソッド は単一の IObservable<T>のシーケンスに対して操作を行うものでした。それだけでも Throttle や Skip, Take、Buffer メソッドや Distict メソッドなど Reactive Extensions を使う上で重要か つ特徴的なメソッドです。これから紹介するメソッドは、複数の IObservable<T>シーケンスを 合成して 1 つの IObservable<T>のシーケンスを作成するメソッド群です。 Reactive Extensions では、時間、゗ベント、非同期などを IObservable<T>のシーケンスとし て扱うことができます。つまり、時間、゗ベント、非同期などを合成して単一のシーケンスにした あと、Distinct で重複を排除したり Buffer などでまとめたり、Scan メソッドなどで集計をとった りすることが出来ます。これは、Reactive Extensions の特徴の 1 つになります。6.1. Merge メソッド ここでは、Merge メソッドについて説明します。Merge メソッドは名前の通り複数の IObservable<T>のシーケンスにするメソッドです。オーバーロードはいくつかありますが一番 シンプルな 2 つの IObservable<T>のシーケンスを合成するメソッドのシグネチャを以下に示し ます。 public static IObservable<T> Merge<T>( this IObservable<T> first, IObservable<T> second); 165
  • このメソッドは、左辺値の IObservable<T>のシーケンスと右辺値の IObservable<T>のシーケンスを 1 つのシーケンスとしてマージします。コード例を下記に示します。 Console.WriteLine("# 2 つの IObservable<T>の統合"); Observable // 0~2 の値を 1 秒間隔で発行 .Interval(TimeSpan.FromSeconds(1)).Take(3) // 1~3 の値に変換 .Select(i => i + 1) // マージ(統合) .Merge( Observable // 0~2 の値を 1 秒間隔で発行 .Interval(TimeSpan.FromSeconds(1)).Take(3) // 10, 20, 30 に変換 .Select(i => (i + 1) * 10)) // 購読 .Subscribe( i => Console.WriteLine("OnNext: {0}", i), () => Console.WriteLine("OnCompleted")); // Enter 押すまで待機 Console.ReadLine();1 秒間隔で 1, 2, 3 の値を発行する IObservable<T>のシーケンスと 1 秒間隔で 10, 20, 30 の値を発行する IObservable<T>のシーケンスを Merge メソッドで合成して購読しています。 実行結果を以下に示します。# 2 つの IObservable<T>の統合OnNext: 1OnNext: 10OnNext: 2OnNext: 20OnNext: 3OnNext: 30OnCompleted上記の結果から Merge メソッドでは left と right で渡した IObservable<T>のシーケンスから発行された値を単純に後続に流していることが確認出来ます。次に、複数の IObservable<T>のシーケンスを 1 つに統合する Merge メソッドのオーバーロードについてみていきます。メソッドのシグネチャを以下に示します。 public static IObservable<T> Merge<T>(params IObservable<T>[] sources); 166
  • 引数の params で渡した IObservable<T>を全てマージします。コード例を下記に示します。 Console.WriteLine("# 複数の IObservable<T>の統合"); // Observable.Merge<T>(params IObservable<T>[] sources) Observable.Merge( // 3 つの IObservable<T>をマージ Observable.Range(0, 3), Observable.Range(10, 3), Observable.Range(100, 3)) // 購読 .Subscribe( i => Console.WriteLine("OnNext: {0}", i), () => Console.WriteLine("OnCompleted")); // Enter 押すまで待機 Console.ReadLine();上記のコードは 3 つの IObservable<T>のシーケンスを Merge メソッドで合成して結果を出力しています。実行結果を以下に示します。# 複数の IObservable<T>の統合OnNext: 0OnNext: 10OnNext: 100OnNext: 1OnNext: 11OnNext: 101OnNext: 2OnNext: 12OnNext: 102OnCompleted3 つの IObservable<T>のシーケンスが 1 つに統合されていることが確認できます。このメソッドで興味深いオーバーロードとして以下のようなものがあります。 public static IObservable<T> Merge<T>(this IObservable<IObservable<T>> sources);IObservable<IObservable<T>>の拡張メソッドとして定義されています。このメソッドを使うと、下記のように Subject<int>などのような型から IObservable<IObservable<T>>を作成して Merge して 1 つにつなげるということが出来るようになります。コード例を下記に示します。 Console.WriteLine("# 複数の IObservable<T>の統合 2"); // IObservable<T> Merge<T>(IObservable<IObservable<T>> source) var source = new Subject<int>(); 167
  • source // IObservable<int>から IObservable<IObservable<int>>に変換 .Select(i => Observable // 0~2 の値を 1 秒間隔で 3 つ発行 .Interval(TimeSpan.FromSeconds(1)).Take(3) // 値を変換 .Select(l => (l + 1) * i)) // IObservable<IObservable<T>>から IObservable<T>へマージ .Merge() // 購読 .Subscribe( i => Console.WriteLine("OnNext: {0}", i), () => Console.WriteLine("OnCompleted")); // 値の発行から完了 Console.WriteLine("# OnNext(10)"); source.OnNext(10); Console.WriteLine("# OnNext(100)"); source.OnNext(100); Console.WriteLine("# OnNext(1000)"); source.OnNext(1000); Console.WriteLine("# OnCompleted"); source.OnCompleted(); // Enter 押すまで待機 Console.ReadLine();Subject<int>から発行された値を元に Interval メソッドを使って 3 つの値を発行するIObservable<T>型を生成しています。これに対して Merge メソッドを呼び出しIObservable<IObservable<T>>を 1 つの IObservable<T>にならしています。実行結果を以下に示します。# 複数の IObservable<T>の統合 2# OnNext(10)# OnNext(100)# OnNext(1000)# OnCompletedOnNext: 10OnNext: 100 168
  • OnNext: 1000 OnNext: 200 OnNext: 20 OnNext: 2000 OnNext: 300 OnNext: 3000 OnNext: 30 OnCompleted 3 つの IObservable<T>から発行された値が順不同(実行するごとに OnNext: ***の箇所の順番 が変わる)で表示されます。このように Merge メソッドを使用することで、例えば、ある゗ベン トが発行されたタ゗ミングで WebAPI を非同期に実行して結果を受け取って処理を行うという処 理を書くことが出来ます。6.2. SelectMany メソッド ここでは SelectMany メソッドについて説明します。SelectMany メソッドには様々なオーバーロ ードがありますが、一番使用頻度の高いオーバーロードは以下のものになります。 public static IObservable<R> SelectMany<T, R>( this IObservable<T> source, Func<T, IObservable<R>> selector); source から発行された値を受け取って selector で渡したデリゲート処理を行い IObservable<R>を取得します。source から複数の値が発行されると複数の IObservable<R> が内部で作成されますが、 それらをすべて 1 つの IObservable<R>にマージして後続に流します。 つまり、 Merge メソッドの最後のサンプルで示した IObservable<IObservable<T>>の引数を受 け取るオーバーロードと同様のことを内部でしています。コード例を下記に示します。 Console.WriteLine("# 複数の IObservable<T>の統合(SelectMany)"); // IObservable<T> Merge<T>(IObservable<IObservable<T>> source) var source = new Subject<int>(); source // 発行された値から IObservable<T>を作成してマージ(統合)する .SelectMany(i => Observable // 0~2 の値を 1 秒間隔で 3 つ発行 .Interval(TimeSpan.FromSeconds(1)).Take(3) // 値を変換 .Select(l => (l + 1) * i)) // 購読 .Subscribe( i => Console.WriteLine("OnNext: {0}", i), () => Console.WriteLine("OnCompleted")); 169
  • // 値の発行から完了 Console.WriteLine("# OnNext(10)"); source.OnNext(10); Console.WriteLine("# OnNext(100)"); source.OnNext(100); Console.WriteLine("# OnNext(1000)"); source.OnNext(1000); Console.WriteLine("# OnCompleted"); source.OnCompleted(); // Enter 押すまで待機 Console.ReadLine();IObservable<IObservable<T>>型の引数を受け取る Merge メソッドのオーバーロードと同様の処理を SelectMany メソッドで記述しています。実質 SelectMany は下記のように Select メソッドと Merge メソッドを使って置き換え出来ます。 SelectMany(i => Observable.Return(i)) → Select(i => Observable.Return(i)).Merge()実行結果を以下に示します。# 複数の IObservable<T>の統合(SelectMany)# OnNext(10)# OnNext(100)# OnNext(1000)# OnCompletedOnNext: 10OnNext: 100OnNext: 1000OnNext: 20OnNext: 200OnNext: 2000OnNext: 30OnNext: 3000OnNext: 300OnCompletedMerge メソッドと同様の結果になっていることが確認できます。ここで説明した SelectMany メソッドのオーバーラ゗ド以外に以下のようなオーバーラ゗ドがあります。 170
  • // IObservable<T>が IEnumerable<T>になった感じ public static IObservable<R> SelectMany<T, R>( this IObservable<T> source, Func<T, IEnumerable<R>> selector); // source から発行された値に関係なく IObservable<T>を作成する場合 public static IObservable<O> SelectMany<T, O>( this IObservable<T> source, IObservable<O> other); // source から発行された値を元に IEnumerable<TCollection>を作り、source から発行された値と // IEnumerable<TCollection>の値を元に結果を作成する。その結果を後続に流す。 public static IObservable<R> SelectMany<T, TCollection, R>( this IObservable<T> source, Func<T, IEnumerable<TCollection>> collectionSelector, Func<T, TCollection, R> resultSelector); // source から発行された値を元に IObservable<TCollection>を作り、source から発行された値と // IObservable<TCollection>の値を元に結果を作成する。その結果を後続に流す。 public static IObservable<R> SelectMany<T, TCollection, R>( this IObservable<T> source, Func<T, IObservable<TCollection>> collectionSelector, Func<T, TCollection, R> resultSelector); // onNext, onError, onCompleted のそれぞれで IObservable<R>を作成して、マージして返す。 public static IObservable<R> SelectMany<T, R>( this IObservable<T> source, Func<T, IObservable<R>> onNext, Func<Exception, IObservable<R>> onError, Func<IObservable<R>> onCompleted);全てのオーバーロードについてのコード例はここでは割愛します。ここでは上記のメソッドの 4番目のコードの使用例を下記に示します。 Console.WriteLine("# 複数の IObservable<T>の統合(SelectMany のオーバーロード)"); var source = new Subject<int>(); source .SelectMany( // source から発行された値を元に i, i+1, i+2 の値を発行する IObservable<int>を返す i => Observable.Range(i, 3), // source から発行された値と i => Observable.Range(i, 3)で生成された値を // 使って最終的に発行される値を作成する。ここでは匿名型にしただけ。 (x, y) => new { x, y }) .Subscribe(Console.WriteLine); // 値の発行から完了 Console.WriteLine("# OnNext(10)"); source.OnNext(10); 171
  • Console.WriteLine("# OnNext(100)"); source.OnNext(100); Console.WriteLine("# OnNext(1000)"); source.OnNext(1000); Console.WriteLine("# OnCompleted"); source.OnCompleted(); // Enter 押すまで待機 Console.ReadLine(); 上記のコードの実行結果を以下に示します。 # 複数の IObservable<T>の統合(SelectMany のオーバーロード) # OnNext(10) { x = 10, y = 10 } { x = 10, y = 11 } { x = 10, y = 12 } # OnNext(100) { x = 100, y = 100 } { x = 100, y = 101 } { x = 100, y = 102 } # OnNext(1000) { x = 1000, y = 1000 } { x = 1000, y = 1001 } { x = 1000, y = 1002 } # OnCompleted source から発行された値と、source から発行された値を元に作成した結果の IObservable<TCollection>から発行される値を組み合わせるケースで使用します。6.3. Switch メソッド ここでは Switch メソッドについて説明します。 Switch メソッドは、IObservable<T> Merge(self IObservable<IObservable<T>> source)と同じシグネチャを持ちます。メソッドの定義を以下 に示します。 public static IObservable<T> Switch<T>(this IObservable<IObservable<T>> sources); Merge メソッドとの違いを確認するために、Merge メソッドと Switch メソッドで同じ処理を書 いて比較します。まず、Merge メソッドのコード例を下記に示します。 Console.WriteLine("# 複数の IObservable<T>の統合(Merge)"); // IObservable<T> Merge<T>(IObservable<IObservable<T>> source) var source = new Subject<int>(); 172
  • source .Select(i => Observable // 1 * i, 2 * i, 3 * i の値を 1 秒間隔で発行 .Interval(TimeSpan.FromSeconds(1)).Take(3) // 値を変換 .Select(l => (l + 1) * i)) // 統合 .Merge() .Subscribe( i => Console.WriteLine("OnNext: {0}", i), () => Console.WriteLine("OnCompleted")); // 値の発行から完了 Console.WriteLine("# OnNext(10)"); source.OnNext(10); Console.WriteLine("Sleep 2000ms..."); Thread.Sleep(2000); Console.WriteLine("# OnNext(100)"); source.OnNext(100); Console.WriteLine("Sleep 2000ms..."); Thread.Sleep(2000); Console.WriteLine("# OnNext(1000)"); source.OnNext(1000); Console.WriteLine("Sleep 2000ms..."); Thread.Sleep(2000); Console.WriteLine("# OnCompleted"); source.OnCompleted(); Console.ReadLine();若干複雑ですが、時間間隔をあけて複数の IObservable<T>のシーケンスが入り乱れて値を発行するようにしています。実行結果を以下に示します。# 複数の IObservable<T>の統合(Merge)# OnNext(10)Sleep 2000ms... 173
  • OnNext: 10OnNext: 20# OnNext(100)Sleep 2000ms...OnNext: 30OnNext: 100# OnNext(1000)Sleep 2000ms...OnNext: 200OnNext: 1000OnNext: 300# OnCompletedOnNext: 2000OnNext: 3000OnCompleted10, 100, 1000 の 3 つの値を発行して、そこからさらに 3 つの値を発行する IObservable<T>のシーケンスを作成しているので、合計で 9 つの値が発行されます。このコードの Merge 部分をSwitch メソッドに書き換えたコードを下記に示します。 Console.WriteLine("# 複数の IObservable<T>の統合(Switch)"); // IObservable<T> Merge<T>(IObservable<IObservable<T>> source) var source = new Subject<int>(); source .Select(i => Observable // 1 * i, 2 * i, 3 * i の値を 1 秒間隔で発行 .Interval(TimeSpan.FromSeconds(1)).Take(3) .Select(l => (l + 1) * i)) // 最後 .Switch() .Subscribe( i => Console.WriteLine("OnNext: {0}", i), () => Console.WriteLine("OnCompleted")); // 値の発行から完了 Console.WriteLine("# OnNext(10)"); source.OnNext(10); Console.WriteLine("Sleep 2000ms..."); Thread.Sleep(2000); 174
  • Console.WriteLine("# OnNext(100)"); source.OnNext(100); Console.WriteLine("Sleep 2000ms..."); Thread.Sleep(2000); Console.WriteLine("# OnNext(1000)"); source.OnNext(1000); Console.WriteLine("Sleep 2000ms..."); Thread.Sleep(2000); Console.WriteLine("# OnCompleted"); source.OnCompleted(); Console.ReadLine();このコードの実行結果を以下に示します。# 複数の IObservable<T>の統合(Switch)# OnNext(10)Sleep 2000ms...OnNext: 10OnNext: 20# OnNext(100)Sleep 2000ms...OnNext: 100# OnNext(1000)Sleep 2000ms...OnNext: 1000# OnCompletedOnNext: 2000OnNext: 3000OnCompleted上記の実行結果から確認できるように、Merge の時と異なって 9 個の値を発行しているにも関わらず、最終的に OnNext まで値が流れているのが 6 つになっています。これは、Merge メソッドが元になる IObservable<T>が発行した値を全て後続に流すのに対して、Switch メソッドは、最後に値を発行した IObservable<T>の値を後続に流します。Switch メソッドは、このような特徴から一定間隔で非同期にデータを取得する処理を実行して、 最後に応答が返ってきたところからデータを抽出したいといったケースで利用できます。 175
  • 6.4. Concat メソッド ここでは、Concat メソッドについて説明します。Concat メソッドは、その名前の通り複数の IObservable<T>のシーケンスを直列で繋ぎます。例えば、3 つの IObservable<T>のシーケン スを渡すと 1 つ目の IObservable<T>のシーケンスが完了するまでの間は 1 つ目の IObservable<T>のシーケンスを後続に流します。1 つ目の IObservable<T>のシーケンスが完 了すると、2 つ目の IObservable<T>のシーケンスの値を流します。2 つ目の IObservable<T> のシーケンスが完了すると、3 つ目の IObservable<T>のシーケンスの値を流します。そして 3 つ目の IObservable<T>のシーケンスが完了すると、完了したということを後続に通知します。 Concat メソッドには 2 つのシーケンスを繋ぐことに特化したオーバーロードと、複数のシーケン スを繋ぐためのメソッドのオーバーロードがあります。 このメソッドのオーバーロードを以下に示 します。 // 引数で渡した全ての IObservable<T>のシーケンスを繋ぐ public static IObservable<T> Concat<T>(params IObservable<T>[] sources); // IEnumerable<IObservable<T>>から取得できるすべての IObservable<T>を繋ぐ public static IObservable<T> Concat<T>(this IEnumerable<IObservable<T>> sources); // IObservable<IObservable<T>>から発行されるすべての IObservable<T>を繋ぐ public static IObservable<T> Concat<T>(this IObservable<IObservable<T>> sources); // first と second を繋ぐ public static IObservable<T> Concat<T>(this IObservable<T> first, IObservable<T> second); Concat メソッドの動作を示すためのコード例を下記に示します。 Observable // IObservable<T>のシーケンスを直列につなげる .Concat( // "1st: 0" ~ "1st: 2"までの値を 1 秒間隔で発行する Observable.Interval(TimeSpan.FromSeconds(1)).Select(i => "1st: " + i).Take(3), // "2nd: 0" ~ "2nd: 2"までの値を 1 秒間隔で発行する Observable.Interval(TimeSpan.FromSeconds(1)).Select(i => "2nd: " + i).Take(3), // "3rd: 0" ~ "3rd: 2"までの値を 1 秒間隔で発行する Observable.Interval(TimeSpan.FromSeconds(1)).Select(i => "3rd: " + i).Take(3)) // 購読 .Subscribe( s => Console.WriteLine("OnNext: {0}", s), () => Console.WriteLine("OnCompleted")); Console.ReadLine(); Observable.Interval メソッドを使って 1 秒間隔で値を発行する IObservable<T>のシーケンス を 3 つ作成して、Concat メソッドで繋いでいます。1 秒間隔で値を発行するため、Merge メソッ 176
  • ドでは 1 つ目と 2 つ目と 3 つ目の IObservable<T>のシーケンスの発行する値が混在しますが、Concat は前の IObservable<T>のシーケンスが完了しない限り後ろの IObservable<T>のシーケンスが発行する値を、後続に流さないため上記のサンプルでは Subscribe の OnNext に値が渡る順番が保障されています。実行結果を以下に示します。OnNext: 1st: 0OnNext: 1st: 1OnNext: 1st: 2OnNext: 2nd: 0OnNext: 2nd: 1OnNext: 2nd: 2OnNext: 3rd: 0OnNext: 3rd: 1OnNext: 3rd: 2OnCompleted前の IObservable<T>のシーケンスが完了するまで次の IObservable<T>のシーケンスが発行される値が OnNext に流れていないことが確認できます。このプログラムは、可変長引数のオーバーロードを使いましたが、 下記のように別のオーバーロードを使っても同じように書くことができます。 // "1st: 0" ~ "1st: 2"までの値を 1 秒間隔で発行する Observable.Interval(TimeSpan.FromSeconds(1)) .Select(i => "1st: " + i) .Take(3) .Concat( // "2nd: 0" ~ "2nd: 2"までの値を 1 秒間隔で発行する Observable.Interval(TimeSpan.FromSeconds(1)) .Select(i => "2nd: " + i) .Take(3)) .Concat( // "3rd: 0" ~ "3rd: 2"までの値を 1 秒間隔で発行する Observable.Interval(TimeSpan.FromSeconds(1)) .Select(i => "3rd: " + i) .Take(3)) // 購読 .Subscribe( s => Console.WriteLine("OnNext: {0}", s), () => Console.WriteLine("OnCompleted")); 177
  • Console.ReadLine(); 実行結果は同じため省略します。6.5. Zip メソッド ここでは、Zip メソッドについて説明します。Zip メソッドは、2 つの IObservable<T>のシーケ ンスから値が発行されるのを待って、2 つの値を変換して、変換した結果を後ろに流すメソッドで す。メソッドのシグネチャを下記に示します。 public static IObservable<R> Zip<T, U, R>( this IObservable<T> first, IObservable<U> second, Func<T, U, R> resultSelector); first から発行された値と second から発行された値を resultSelector で変換して後ろに流します。 また、first か second のどちらかの IObservable<T>のシーケンスが完了状態になった段階で後 続に完了通知を発行します。このメソッドのコード例を下記に示します。 // 1 秒間隔で 0 から値をカウントゕップしていく IObservable<T>のシーケンスの最初の 3 つ Observable.Interval(TimeSpan.FromSeconds(1)).Take(3) // Zip でまとめる .Zip( // 100, 101, 102 を発行する IObservable<T>のシーケンス Observable.Range(100, 3), // 渡された値を元に文字列化 (l, r) => string.Format("{0}-{1}", l, r)) // 購読 .Subscribe( s => Console.WriteLine("OnNext: {0}", s), () => Console.WriteLine("OnCompleted")); Console.ReadLine(); 1 秒間隔で 0, 1, 2 の値を発行する IObservable<T>のシーケンスと 100, 101, 102 の値を発行 する IObservable<T>のシーケンスを Zip メソッドで合成しています。実行結果を以下に示しま す。 OnNext: 0-100 OnNext: 1-101 OnNext: 2-102 OnCompleted Zip メソッドで合成された 3 つの値が OnNext に渡ってきていることが確認できます。上記のプロ グラムを少し変更して片方の IObservable<T>のシーケンスが発行する値の数を 1 つにしたコー ドを下記に示します。 178
  • // 1 秒間隔で 0 から値をカウントゕップしていく IObservable<T>のシーケンスの最初の 3 つ Observable.Interval(TimeSpan.FromSeconds(1)).Take(3) // Zip でまとめる .Zip( // 100 を発行する IObservable<T>のシーケンス Observable.Return(100), // 渡された値を元に文字列化 (l, r) => string.Format("{0}-{1}", l, r)) // 購読 .Subscribe( s => Console.WriteLine("OnNext: {0}", s), () => Console.WriteLine("OnCompleted")); Console.ReadLine(); Interval メソッドから Take(3)で 3 つの値を発行する IObservable<T>のシーケンスと Return メソッドで 1 つの値しか発行しない IObservable<T>のシーケンスを Zip メソッドで合成してい ます。実行結果を下記に示します。 OnNext: 0-100 OnCompleted OnNext が 1 回しか実行されていないことが確認できます。 このことから Zip メソッドが、合成対 象の IObservable<T>のシーケンスのどちらか片方が終了した段階で、後続に完了通知を発行す ることが確認できます。6.6. Amb メソッド ここでは Amb メソッドについて説明します。Amb メソッドは複数の IObservable<T>のシーケ ンスの中から一番最初に値を発行した IObservable<T>のシーケンスの値を後続に流すメソッド です。このメソッドのオーバーロードを以下に示します。 // 引数で渡した全ての IObservable<T>から最初に値を発行した IObservable<T>の値を後ろに流す public static IObservable<T> Amb<T>(params IObservable<T>[] sources); // sources 内の全ての IObservable<T>から最初に値を発行した IObservable<T>の値を後ろに流す public static IObservable<T> Amb<T>(this IEnumerable<IObservable<T>> sources); // first と second のうち最初に値を発行したものの値を後ろに流す public static IObservable<T> Amb<T>(this IObservable<T> first, IObservable<T> second); 他の合成系メソッドと同様に可変長引数や IEnumerable<IObservable<T>>の拡張メソッドや 2 つの合成に特化したオーバーロードが用意されています。Amb メソッドのコード例を下記に示 します。 Observable.Amb( 179
  • // 3 秒後に値を発行する IO<T> Observable.Timer(TimeSpan.FromSeconds(3)).Select(_ => "3sec"), // 10 秒後に値を発行する IO<T> Observable.Timer(TimeSpan.FromSeconds(10)).Select(_ => "10sec"), // 2 秒後に値を発行する IO<T> Observable.Timer(TimeSpan.FromSeconds(2)).Select(_ => "2sec"), // 6 秒後に値を発行する IO<T> Observable.Timer(TimeSpan.FromSeconds(6)).Select(_ => "6sec"), // 22 秒後に値を発行する IO<T> Observable.Timer(TimeSpan.FromSeconds(22)).Select(_ => "22sec")) .Subscribe( s => Console.WriteLine("OnNext: {0}", s), () => Console.WriteLine("OnCompleted")); Timer メソッドを使って、指定した時間が経過した後に、値を発行する IObservable<T>のシー ケンスを Amb メソッドで合成しています。実行結果を以下に示します。 OnNext: 2sec OnCompleted 引数で渡した中で一番早く値を発行する Observable.Timer(TimeSpan.FromSecond(2)).Select(_ => “2sec”)の結果が OnNext や OnCompleted に渡っていることが確認できます。6.7. CombineLatest メソッド ここでは、 CombineLatest メソッドについて説明します。CombineLatest メソッドは Zip メソッ ドのように 2 つの IObservable<T>のシーケンスを合成するメソッドです。Zip メソッドが両方 の IObservable<T>のシーケンスから値が発行されるのを待つのに対して CombineLatest メソ ッドは、どちらか一方から値が発行されると、もう一方の最後に発行した値があるか確認し、あれ ばそれを使って合成処理を行い、 後続に値を流します。このメソッドのシグネチャを以下に示しま す。 public static IObservable<R> CombineLatest<F, S, R>( this IObservable<F> first, IObservable<S> second, Func<F, S, R> resultSelector); このメソッドのコード例を下記に示します。 var source1 = new Subject<string>(); var source2 = new Subject<int>(); source1 // source1 と source2 を合成 180
  • .CombineLatest( source2, // 発行された値を結合して文字列化 (l, r) => string.Format("{0}-{1}", l, r)) // 購読 .Subscribe( s => Console.WriteLine("OnNext: {0}", s), () => Console.WriteLine("OnCompleted")); // 適当に値を発行する Console.WriteLine("source1.OnNext(foo)"); source1.OnNext("foo"); Console.WriteLine("source2.OnNext(100)"); source2.OnNext(100); Console.WriteLine("source2.OnNext(200)"); source2.OnNext(200); Console.WriteLine("source1.OnNext(bar)"); source1.OnNext("bar"); Console.WriteLine("source1.OnNext(boo)"); source1.OnNext("boo"); // source1 完了 Console.WriteLine("source1.OnCompleted()"); source1.OnCompleted(); // source1 完了後に source2 から値を発行する Console.WriteLine("source2.OnNext(200)"); source2.OnNext(999); // source2 完了 Console.WriteLine("source2.OnCompleted()"); source2.OnCompleted();このコードの実行結果を以下に示します。source1.OnNext(foo)source2.OnNext(100)OnNext: foo-100source2.OnNext(200)OnNext: foo-200source1.OnNext(bar) 181
  • OnNext: bar-200 source1.OnNext(boo) OnNext: boo-200 source1.OnCompleted() source2.OnNext(200) OnNext: boo-999 source2.OnCompleted() OnCompleted 最初の OnNext では、source1 と source2 の値が両方発行されるまで実行されませんが、それ以 降は、source1 か source2 のどちらかから値が発行されると OnNext に値が発行されていること が確認できます。このことから、CombineLatest メソッドの特徴である、最後に発行された値を キャッシュしているということがわかります。そして、CombineLatest メソッドは両方のシーケ ンスが完了状態になった時点で完了通知を行うという動作も確認できます。 これは、片方が完了し ても、もう一方から値が発行されたら最後に発行された値を元に処理を継続できるためです。6.8. StartWith メソッド ここでは、StartWith メソッドについて説明します。StartWith メソッドのシグネチャを以下に示 します。 public static IObservable<T> StartWith<T>(this IObservable<T> source, params T[] values); このメソッドは、source で渡した IObservable<T>のシーケンスの先頭に values で指定した要 素を合成して 1 つの IObservable<T>のシーケンスにします。このメソッドの使用例を下記に示 します。 Observable // 1~3 の値を発行する IObservable<T>のシーケンス .Range(1, 3) // 頭に 10, 20, 30 をつける .StartWith(10, 20, 30) // 購読 .Subscribe( i => Console.WriteLine("OnNext: {0}", i)); Range メソッドを使って 1~3 の値を発行する IObservable<T>のシーケンスを作成して、 StartWith メソッドで 10, 20, 30 の値を先頭に追加して、Subscribe メソッドで購読をしていま す。このメソッドの実行例を以下に示します。 OnNext: 10 OnNext: 20 OnNext: 30 OnNext: 1 182
  • OnNext: 2 OnNext: 36.9. Join メソッド ここでは、Join メソッドについて説明します。Join メソッドは 2 つの IObservable<T>のシーケ ンスから発行された値の全ての組み合わせをもとに、値を生成して発行する IObservable<T>の シーケンスを作成します。このメソッドのシグネチャを以下に示します。 public static IObservable<TResult> Join<TLeft, TRight, TLeftDuration, TRightDuration, TResult>( this IObservable<TLeft> left, IObservable<TRight> right, Func<TLeft, IObservable<TLeftDuration>> leftDurationSelector, Func<TRight, IObservable<TRightDuration>> rightDurationSelector, Func<TLeft, TRight, TResult> resultSelector); Join メソッドは、left と right から値が発行されると、それぞれ leftDurationSelector 引数と rightDurationSelector 引数で指定したデリゲートが呼ばれて、その値の有効期間を示す IObservable<T>が生成されます。この有効期間を示す IObservable<T>から値が発行されたら left と right から発行された対象となる値は Join メソッドで使われなくなります。そして、left と right から値が発行されると、現在有効な値の全てを使って resultSelector が呼び出されます。例 えば left から発行された値で有効なものが(1, 2, 3)で right から発行された値で有効なものが(10, 20, 30)の場合は、resultSelector は(1, 10)(1, 20)(1, 30)(2, 10)(2, 20)(2, 30)(3, 10)(3, 20)(3, 30)の組み合わせの引数で呼び出されます。ここで生成された値が Join メソッドの結果の IObservable<TResult>から発行される値になります。 このメソッドのコード例を下記に示します。 // Join で合成する IObservable<T> var left = new Subject<int>(); var right = new Subject<int>(); left.Join( right, // left から発行される値の有効期間は永久 _ => Observable.Never<Unit>(), // right から発行される値の有効期間は永久 _ => Observable.Never<Unit>(), // 発行された値の組を作る Tuple.Create) // 購読 .Subscribe( // 組を表示 tuple => Console.WriteLine("Left: {0}, Right: {1}", tuple.Item1, tuple.Item2), 183
  • // 完了を表示 () => Console.WriteLine("OnCompleted")); // 値の発行 Console.WriteLine("left.OnNext(1)"); left.OnNext(1); Console.WriteLine("right.OnNext(10)"); right.OnNext(10); Console.WriteLine("right.OnNext(100)"); right.OnNext(100); Console.WriteLine("left.OnNext(2)"); left.OnNext(2); // 終了 Console.WriteLine("left.OnCompleted()"); left.OnCompleted(); Console.WriteLine("right.OnCompleted()"); right.OnCompleted();Join メソッドを使って 2 つの IObservable<T>のシーケンスから発行される値を組(Tuple)にしています。Join の引数で渡した IObservable<T>のシーケンスから発行される値の有効期間は、Observable.Never メソッドを使って永遠に OnNext の呼ばれない IObservable<T>のシーケンスを指定しているため、 永遠に無効にならないようにしています。Subscribe では組の値を表示しています。このコードの実行結果を以下に示します。left.OnNext(1) ← この段階では left に 1 があるだけright.OnNext(10) ← この段階で left に 1, right に 10 があるため(1, 10)の組が生成されるLeft: 1, Right: 10right.OnNext(100) ← この段階で left に 1, right に 10, 100 があるため (1, 100)の組が追加で生成されるLeft: 1, Right: 100left.OnNext(2) ← この段階で left に 1, 2, right に 10, 100 があるため(2, 10), (2, 100)の組が追加で生成されるLeft: 2, Right: 10Left: 2, Right: 100left.OnCompleted()right.OnCompleted()OnCompleted ← right と left が両方終了したため、これ以降新たな組み合わせが出来ないため終了この例のように値の有効期間に Observable.Never を使うと、left と right から発行された全ての組合わせを生成することが出来ます。Never 以外の値の有効期間を指定したときのコード例を下記に示します。 184
  • // Join で合成する IObservable<T>var left = new Subject<int>();var right = new Subject<int>();left.Join( right, // left から発行される値の有効期間は永久 _ => Observable.Never<Unit>(), // right から発行される値の有効期間は一瞬 _ => Observable.Empty<Unit>(), // 発行された値の組を作る Tuple.Create) // 購読 .Subscribe( // 組を表示 tuple => Console.WriteLine("Left: {0}, Right: {1}", tuple.Item1, tuple.Item2), // 完了を表示 () => Console.WriteLine("OnCompleted"));// 値の発行Console.WriteLine("left.OnNext(1)");left.OnNext(1);Console.WriteLine("right.OnNext(10)");right.OnNext(10);Console.WriteLine("right.OnNext(100)");right.OnNext(100);Console.WriteLine("left.OnNext(2)");left.OnNext(2);Console.WriteLine("right.OnNext(1000)");right.OnNext(1000);// 終了Console.WriteLine("left.OnCompleted()");left.OnCompleted();Console.WriteLine("right.OnCompleted()");right.OnCompleted(); 185
  • 上記のコードは right から発行された値の有効期間を Observable.Empty<Unit>()で指定してい るため、値が発行された瞬間しか有効期間が無いようにしています。 このプログラムの実行結果を 以下に示します。 left.OnNext(1) right.OnNext(10) Left: 1, Right: 10 right.OnNext(100) Left: 1, Right: 100 left.OnNext(2) right.OnNext(1000) Left: 1, Right: 1000 Left: 2, Right: 1000 left.OnCompleted() right.OnCompleted() OnCompleted right から値が発行されると、今まで left から発行された値との組み合わせが生成されていること が確認できます。 逆に left から値が発行されても right から発行された値には有効なものが 1 つも ないため、resultSelector で指定したデリゲートは呼ばれません。 ここでは、コード例を示しませんが、値の有効期限にタ゗マーや゗ベントなどから生成した IObservable<T>を指定することで、よりきめ細やかに値の有効期限を指定することが出来ます。6.10. GroupJoin メソッド ここでは、GroupJoin メソッドについて説明します。GroupJoin メソッドは、2 つの IObservable<T>のシーケンスのうち左辺の値に対して右辺の値の IObservable<T>のシーケン スの組み合わせを合成します。このメソッドのシグネチャを以下に示します。 public static IObservable<TResult> GroupJoin<TLeft, TRight, TLeftDuration, TRightDuration, TResult>( this IObservable<TLeft> left, IObservable<TRight> right, Func<TLeft, IObservable<TLeftDuration>> leftDurationSelector, Func<TRight, IObservable<TRightDuration>> rightDurationSelector, Func<TLeft, IObservable<TRight>, TResult> resultSelector); Join メソッドと、ほぼ同じシグネチャですが、resultSelector デリゲートの引数に違いがありま す。resultSelector デリゲートの第二引数が Join メソッドでは TRight 型だったのに対して GroupJoin メソッドでは IObservable<TRight>になります。GroupJoin メソッドの resultSelector は、left 引数から値が発行されたタ゗ミングで呼び出されます。この時の resultSelector の第一引数が、left から発行される値で、第二引数が right から発行された有効期 限内の値を発行する IObservable<TRight>になります。 このメソッドの使用例を下記に示します。 // センサー名 var sensors = new Subject<string>(); 186
  • // センサーが受信する値var values = new Subject<int>();// 値のリセット用 Subjectvar valueReset = new Subject<Unit>();sensors.GroupJoin( values, // センサーは有効期限無し _ => Observable.Never<Unit>(), // センサーの値は valueReset の OnNext で無効に出来る _ => valueReset, // センサーの名前と、センサーが受け取った値の現在の合計値を発行する Log にして後続に流す (l, r) => new { Name = l, Log = r.Scan((x, y) => x + y) }) .Subscribe( sensor => { // Log を表示する sensor .Log .Subscribe(i => Console.WriteLine("{0}: {1}", sensor.Name, i)); }, // 完了 () => Console.WriteLine("OnCompleted"));// センサーを 2 つ登録Console.WriteLine("sensors.OnNext(sensor1)");sensors.OnNext("sensor1");Console.WriteLine("sensors.OnNext(sensor2)");sensors.OnNext("sensor2");// 値を 3 つ発行Console.WriteLine("values.OnNext(100)");values.OnNext(100);Console.WriteLine("values.OnNext(10)");values.OnNext(10);Console.WriteLine("values.OnNext(1)"); 187
  • values.OnNext(1); // センサーの値を一旦リセット Console.WriteLine("valueReset.OnNext()"); valueReset.OnNext(Unit.Default); // 新しいセンサーを追加 Console.WriteLine("sensors.OnNext(sensor3)"); sensors.OnNext("sensor3"); // 値を 3 つ発行 Console.WriteLine("values.OnNext(1)"); values.OnNext(1); Console.WriteLine("values.OnNext(2)"); values.OnNext(2); Console.WriteLine("values.OnNext(3)"); values.OnNext(3); // 終了 Console.WriteLine("values.OnCompleted()"); values.OnCompleted(); Console.WriteLine("sensors.OnCompleted()"); sensors.OnCompleted();コードの゗メージはセンサーと、センサーが受信した値の合計値をリゕルタ゗ムで表示するプログラムです。センサーが受信する値は任意のタ゗ミング(valueReset 変数の OnNext)でリセットできるようにしています。このメソッドの実行結果を以下に示します。sensors.OnNext(sensor1)sensors.OnNext(sensor2)values.OnNext(100)sensor1: 100sensor2: 100values.OnNext(10)sensor1: 110sensor2: 110values.OnNext(1)sensor1: 111sensor2: 111valueReset.OnNext() 188
  • sensors.OnNext(sensor3) values.OnNext(1) sensor1: 112 sensor2: 112 sensor3: 1 values.OnNext(2) sensor1: 114 sensor2: 114 sensor3: 3 values.OnNext(3) sensor1: 117 sensor2: 117 sensor3: 6 values.OnCompleted() sensors.OnCompleted() OnCompleted sensor1 と sensor2 は、発行された値の合計を全て保持していることが確認できますが、 valueReset の OnNext を呼んだ後に追加した sensor3 は、途中からの値の合計しか集計していな いことが確認できます。6.11. When メソッド ここでは When メソッドについて説明します。When メソッドのシグネチャを以下に示します。 public static IObservable<TResult> When<TResult>(params Plan<TResult>[] plans); public static IObservable<TResult> When<TResult>(this IEnumerable<Plan<TResult>> plans); When メソッドには 2 つのオーバーロードがあり、片方が Plan<TResult>型の可変長引数で、も う一方が IEnumerable<Plan<TResult>>の拡張メソッドです。どちらも複数の Plan<TResult> を纏めるという意味合いになります。6.11.1. Plan<TResult>クラスの作成 When メソッドの引数に渡す Plan<TResult>クラスの作成方法について説明します。 Plan<TResult>クラスの゗ンスタンスを作成する前に、まず Pattern というクラスの゗ンスタン スを用意します。Pattern クラスは And というメソッドで作成します。And メソッドは IObservable<T>のシーケンスを引数に取る拡張メソッドとして定義されています。シグネチャ を以下に示します。 public static Pattern<TLeft, TRight> And<TLeft, TRight>( this IObservable<TLeft> left, IObservable<TRight> right); 189
  • この戻り値の Pattern<TLeft, TRight>クラスには IObservable<T>を受け取る And というメソ ッドが定義されています。シグネチャを以下に示します。 public Pattern<T1, T2, T3> And<T3>(IObservable<T3> other); このように、 この Pattern<T1, T2, T3>にも IObservable<T>を受け取って Pattern<T1, T2, T3, T4>型の値を返すメソッドが定義されています。このように、And メソッドを使って複数の IObservable<T>のシーケンスを繋げて Pattern クラスにまとめることが出来ます。 Pattern クラ スの型引数は、16 個まで定義されているので事実上数を気にすることなくつなげていくことが出 来ます。 Pattern クラスには型引数の数の引数を受け取るデリゲートを受け取る Then メソッドが定義され ています。 Pattern<T1, T2, T3>クラスの場合の Then メソッドのシグネチャを以下に示します。 public Plan<TResult> Then<TResult>(Func<T1, T2, T3, TResult> selector); このメソッドから Plan<TResult>クラスの゗ンスタンスが作成されます。Then メソッドで渡し たデリゲートは、それまでの And で繋いだ IObservable<T>のシーケンスの全てから値が発行さ れたタ゗ミングで実行されます。6.11.2. When メソッドの使用例 では、上記の And メソッドと Then メソッドを使って Plan<TResult>クラスの゗ンスタンスを作 成するコード例を下記に示します。 var plan = Observable // plan1 という文字列を 10 個発行する IObservable<string> .Return("plan1").Repeat(10) // 1 秒間隔で 0 からのカウントゕップにタ゗ムスタンプをつける IObservable<Timestamped<long>> .And(Observable.Interval(TimeSpan.FromSeconds(1)).Timestamp()) // 100~110 の値を発行する IObservable<int> .And(Observable.Range(100, 10)) // 3 つの値を文字列として纏める .Then((planName, timestamped, value) => string.Format("{0} {1} {2}", planName, timestamped, value)); // When で Plan<string>から IObservable<string>にして購読 Observable.When(plan).Subscribe( s => Console.WriteLine("OnNext: {0}", s), () => Console.WriteLine("OnCompleted")); Console.ReadLine(); コメントにある通りですが、無限に値を発行し続ける IObservable<T>のシーケンスから 10 個 の値を発行する IObservable<T>のシーケンスを And で繋いで Then メソッドで文字列化してい ます。そして、When メソッドに渡して購読しています。Then メソッドは、全ての And メソッ ドから値が発行されたタ゗ミングで実行されるため、もっとも短い 10 個しか値を発行しない IObservable<T>のシーケンスが終了したタ゗ミングで Then メソッドも呼ばれなくなります。 そ 190
  • のため、この Plan<string>から生成した IObservable<string>のシーケンスは 10 個しか値を発行しないということになります。実行結果を以下に示します。OnNext: plan1 0@2012/02/22 23:47:09 +09:00 100OnNext: plan1 1@2012/02/22 23:47:10 +09:00 101OnNext: plan1 2@2012/02/22 23:47:11 +09:00 102OnNext: plan1 3@2012/02/22 23:47:12 +09:00 103OnNext: plan1 4@2012/02/22 23:47:13 +09:00 104OnNext: plan1 5@2012/02/22 23:47:14 +09:00 105OnNext: plan1 6@2012/02/22 23:47:15 +09:00 106OnNext: plan1 7@2012/02/22 23:47:16 +09:00 107OnNext: plan1 8@2012/02/22 23:47:17 +09:00 108OnNext: plan1 9@2012/02/22 23:47:18 +09:00 109OnCompletedAnd で連結した IObservable<T>のシーケンスから発行された値を Then で加工した結果が表示されていることが確認できます。また、10 個の値が発行されたら OnCompleted が呼ばれていることも確認できます。次に、複数の Plan<TResult>を When メソッドに渡した場合のコード例を下記に示します。 var plan1 = Observable // plan1 という文字列を 10 個発行する IObservable<string> .Return("plan1").Repeat(10) // 1 秒間隔で 0 からのカウントゕップにタ゗ムスタンプをつける IObservable<Timestamped<long>> .And(Observable.Interval(TimeSpan.FromSeconds(1)).Timestamp()) // 100~110 の値を発行する IObservable<int> .And(Observable.Range(100, 10)) // 3 つの値を文字列として纏める .Then((planName, timestamped, value) => string.Format("{0} {1} {2}", planName, timestamped, value)); var plan2 = Observable // plan2 という文字列を 20 個発行する IObservable<string> .Return("plan2").Repeat(20) // 0.5s 間隔で 0 から値を発行していく IObservable<long> .And(Observable.Interval(TimeSpan.FromSeconds(0.5))) // Then で文字列に纏める .Then((s, l) => string.Format("{0} {1}", s, l)); Observable.When(plan1, plan2).Subscribe( 191
  • s => Console.WriteLine("OnNext: {0}", s), () => Console.WriteLine("OnCompleted"));plan1 は、最初の例と同じように作成しています。plan2 は、20 個の plan2 という文字列と、0.5秒間隔で 0 から値をカウントゕップしていく IObservable<long>を And で繋いで Then 内で文字列化しています。 この 2 つの When で繋いで IObservable<string>に変換して購読しています。実行結果を以下に示します。OnNext: plan2 0OnNext: plan1 0@2012/02/22 23:53:21 +09:00 100OnNext: plan2 1OnNext: plan2 2OnNext: plan2 3OnNext: plan1 1@2012/02/22 23:53:22 +09:00 101OnNext: plan2 4OnNext: plan2 5OnNext: plan1 2@2012/02/22 23:53:23 +09:00 102OnNext: plan2 6OnNext: plan2 7OnNext: plan1 3@2012/02/22 23:53:24 +09:00 103OnNext: plan2 8OnNext: plan2 9OnNext: plan1 4@2012/02/22 23:53:25 +09:00 104OnNext: plan2 10OnNext: plan2 11OnNext: plan1 5@2012/02/22 23:53:26 +09:00 105OnNext: plan2 12OnNext: plan2 13OnNext: plan1 6@2012/02/22 23:53:27 +09:00 106OnNext: plan2 14OnNext: plan2 15OnNext: plan1 7@2012/02/22 23:53:28 +09:00 107OnNext: plan2 16OnNext: plan2 17OnNext: plan1 8@2012/02/22 23:53:29 +09:00 108OnNext: plan2 18OnNext: plan1 9@2012/02/22 23:53:30 +09:00 109OnNext: plan2 19OnCompleted 192
  • plan1 から発行された値と、plan2 から発行された値が混在して表示されていることが確認できま す。このことから When で繋いだ Plan<TResult>から発行された値は Merge メソッドで合成し たように、値が発行された順番で後続に流されていることがわかります。6.11.3. まとめ このように And メソッドは使って Zip メソッドのように複数の IObservable<T>のシーケンスを 合成して Then メソッドで値の変換を行う Plan<TResult>クラスの゗ンスタンスを作成します。 そして When メソッドに Plan<TResult>を複数渡すことで And で繋いだものを Merge メソッド で合成したように繋げることが出来ます。 恐らく合成系のメソッドの中で一番自由度が高く複雑な合成が出来るメソッドになります。7. Scheduler ここでは、 Reactive Extensions の特徴的な機能の1つ Scheduler について説明します。Reactive Extensions では、 Scheduler という仕組みを使って IObservable<T>のシーケンスの処理の実行 場所と時間を自在に制御することができます。この機能を提供するために Reactive Extensions では、IScheduler という゗ンターフェースを定義しています。IScheduler ゗ンターフェースは下 記のような 1 つのプロパテゖと 3 つのメソッドを持つシンプルな゗ンターフェースです。 public interface IScheduler { DateTimeOffset Now { get; } public IDisposable Schedule<TState>( TState state, DateTimeOffset dueTime, Func<IScheduler, TState, IDisposable> action); public IDisposable Schedule<TState>( TState state, TimeSpan dueTime, Func<IScheduler, TState, IDisposable> action); public IDisposable Schedule<TState>( TState state, Func<IScheduler, TState, IDisposable> action); } IScheduler ゗ンターフェースの Now プロパテゖがスケジューラ上の現在の時間を表し、 Schedule メソッドで Scheduler で実行する処理を設定します。実際に IScheduler ゗ンターフェ 193
  • ースを実装することや、ここで定義されているメソッドを使用することは、ほとんどありませんが、 基本としては上記のようなものを使用しているという点はなんとなく頭の片隅に入れておいてく ださい。7.1. 実行場所の切り替え Reactive Extensions は実行場所を制御するための IScheduler ゗ンターフェースの実装クラスが いくつか定義されています。 Reactive Extensions で定義されている実装クラスを以下に示します。  ImmediateScheduler 現在のスレッド上で即座に処理を実行する Scheduler です。  CurrentThreadScheduler 現在のスレッド上で処理を逐次実行する Scheduler です。  ThreadPoolScheduler ThreadPool 上で処理を実行する Scheduler です。  NewThreadScheduler 新規スレッドを作り、その上で処理を実行する Scheduler です。  TaskPoolScheduler TPL の Task 上で処理を実行する Scheduler です。 上記の Scheduler は System.Reactive.Concurrency.Scheduler クラスの static プロパテゖとし て゗ンスタンスが提供されています。ImmediateScheduler と CurrentThreadScheduler は、ど ちらも現在のスレッド上で処理を実行しますが、ImmediateScheduler が、依頼された処理をそ の場で実行するのに対して、CurrentThreadScheduler は、依頼された処理をキューに追加して 順次実行していきます。動作の違いを示すコード例を下記に示します。 Console.WriteLine("CurrentThread"); // ChrrentThreadScheduler で処理を実行 Scheduler.CurrentThread.Schedule(() => { Console.WriteLine("Start"); // 処理の内部でさらに処理を CurrentThreadScheduler に依頼 Scheduler.CurrentThread.Schedule(() => { Console.WriteLine(" Start"); Console.WriteLine(" End"); }); Console.WriteLine("End"); }); Console.WriteLine("Immediate"); // ImmediateScheduler で処理を実行 194
  • Scheduler.Immediate.Schedule(() => { Console.WriteLine("Start"); // 処理の内部でさらに処理を ImmediateScheduler に依頼 Scheduler.Immediate.Schedule(() => { Console.WriteLine(" Start"); Console.WriteLine(" End"); }); Console.WriteLine("End"); }); CurrentThreadScheduler と ImmediateScheduler の双方で、Schedule メソッドを再起呼び出 ししています。このコードの実行結果を以下に示します。 CurrentThread Start End Start End Immediate Start Start End End CurrentThreadScheduler が、Schedule メソッドで登録した処理を現在実行中の処理が終了して から実行しているのに対して、 ImmediateScheduler が、Schedule メソッドで登録した処理を現 在実行中の処理を中断して実行している(通常のメソッド呼び出しをしたのと同じ)ことが確認で きます。7.2. IObservable<T>生成時の Scheduler の指定方法 IObservable<T>のシーケンスを生成するメソッドの大半には IScheduler ゗ンターフェースを受 け取るオーバーロードがあります。このオーバーロードに IScheduler の実装クラスの゗ンスタン スを渡すことで実行場所を設定できます。Timer メソッドを例にスケジューラの指定方法を示し ます。コード例を下記に示します。 Console.WriteLine("Normal"); // 開始のログ Console.WriteLine("{0:HH:mm:ss}: Start!!", DateTime.Now); // 1 秒後に値を発行する 195
  • Observable.Timer(TimeSpan.FromSeconds(1)) .Subscribe( i => Console.WriteLine("OnNext({0})", i), () => Console.WriteLine("OnCompleted")); // 終了のログ Console.WriteLine("{0:HH:mm:ss}: End!!", DateTime.Now); // Enter が押されるまで待つ Console.ReadLine(); Console.WriteLine("CurrentThreadScheduler"); // 開始のログ Console.WriteLine("{0:HH:mm:ss}: Start!!", DateTime.Now); // 1 秒後に値を発行する, 実行場所は現在のスレッド Observable.Timer(TimeSpan.FromSeconds(1), Scheduler.CurrentThread) .Subscribe( i => Console.WriteLine("OnNext({0})", i), () => Console.WriteLine("OnCompleted")); // 終了のログ Console.WriteLine("{0:HH:mm:ss}: End!!", DateTime.Now); // Enter が押されるまで待つ Console.ReadLine();Timer メソッドを 2 回呼び出して発行された値をコンソールに出力しています。最初の Timer メソッドの呼び出しは、Scheduler を指定しないものです。2 つ目の Timer メソッドの呼び出しでは Scheduler.CurrentThread を指定しています。このようにメソッドの最後の引数に Schedulerを渡すことで、 生成された IObservable<T>のシーケンスが何処で実行されるのか指定できます。実行結果を以下に示します。Normal20:25:22: Start!!20:25:22: End!!OnNext(0)OnCompletedCurrentThreadScheduler20:25:26: Start!!OnNext(0)OnCompleted20:25:27: End!! 196
  • CurrentThreadScheduler を指定したほうでは、OnNext と OnCompleted が現在のスレッド上 で実行されるため、Start!!と End!!の実行順序が Scheduler を指定しない場合と異なっているこ とが確認できます。7.2.1. デフォルトの Scheduler Timer メソッドや Interval メソッドのように、現在のスレッドをブロックしない形の IObservable<T>のシーケンスを生成するメソッドではデフォルトの Scheduler は ThreadPoolScheduler が指定されています。また、現在のスレッドをブロックする IObservable<T>のシーケンスを生成するメソッドでは ImmediateScheduler や CurrentThreadScheduler が指定されています。7.3. Scheduler の切り替え ここでは、IObservable<T>のシーケンスの処理中に Scheduler を切り替える方法について説明 します。7.3.1. ObserveOn メソッド IObservable<T>のシーケンスの処理中に Scheduler を切り替えるには ObserveOn メソッドを 使用します。ObserveOn メソッドは IObservable<T>の拡張メソッドとして定義されています。 メソッドのシグネチャを以下に示します。 public static IObservable<TSource> ObserveOn<T>( this IObservable<T> source, IScheduler scheduler); public static IObservable<T> ObserveOn<TSource>( this IObservable<T> source, SynchronizationContext context); IScheduler を受け取るオーバーロードは、後続の処理を指定した Scheduler 経由で実行するメソ ッドです。SynchronizationContext を受け取るオーバーロードは、SynchronizationContext 経 由で後続の処理を実行するメソッドです。実際には、SynchronizationContextScheduler という クラスが Reactive Extensions で定義されているので、このメソッドの意味合いは、 ObserveOn(new SynchronizationContextScheduler(<SynchronizationContext の゗ンスタン ス>))へのショートカットのメソッドです。ObserveOn メソッドのコード例を下記に示します。 // 1 秒間隔で 3 つ値を発行する Observable .Interval(TimeSpan.FromSeconds(1)) .Take(3) // 現在のスレッド ID を表示 .Do(_ => Console.WriteLine("ThreadId: {0}", Thread.CurrentThread.ManagedThreadId)) // 実行スレッドを新しいスレッドに切り替える .ObserveOn(Scheduler.NewThread) // 現在のスレッド ID を表示 197
  • .Do(_ => Console.WriteLine("ThreadId: {0}", Thread.CurrentThread.ManagedThreadId)) // 購読して、発行された値とスレッド ID を表示 .Subscribe( i => Console.WriteLine("OnNext({0}), ThreadId: {1}", i, Thread.CurrentThread.ManagedThreadId), () => Console.WriteLine("OnCompleted, ThreadId: {0}", Thread.CurrentThread.ManagedThreadId)); Console.ReadLine(); Timer メソッドと Take メソッドを使って 1 秒間隔で 3 つの値を発行しています。そして ObserveOn メソッドを使ってスレッドを新規スレッドに切り替えています。スレッドの切り替え 前後で Do メソッドを使ってスレッド ID を表示しています。 Subscribe メソッドでもスレッド ID を表示して実行スレッドがどうなっているのか確認しています。実行結果を以下に示します。 ThreadId: 4 ThreadId: 5 OnNext(0), ThreadId: 5 ThreadId: 4 ThreadId: 7 OnNext(1), ThreadId: 7 ThreadId: 6 ThreadId: 8 OnNext(2), ThreadId: 8 OnCompleted, ThreadId: 9 スレッド ID が切り替わっていることが確認できます。このことから、IObservable<T>のシーケ ンスの途中で実行スレッドの切り替えができることがわかります。7.3.2. ObserveOn の使用用途 実行場所(ここでの例では実行スレッド)の切り替えが、どのような用途で使えるのかについて説 明します。代表的な用途では、Windows Forms や WPF などのような UI を操作するケースです。 たとえば、ボタンクリックの゗ベントなどをトリガーにバックグラウンドで Web API を呼び出し て、結果を画面に反映させる操作があるとします。 このとき、゗ベントの購読と゗ベントの値の加工程度までは UI スレッドで行い、Web API を呼 び出す箇所はバックグラウンドで行うのが UI をフリーズさせないようにするため最近では一般的 な方法です。そして、最後の UI への結果の反映で、再び UI スレッドに切り替えて処理を行いま す。 これは、 フレームワークが一般的に UI スレッド以外からの操作に対応していないために必 UI 要になります。このように「゗ベントの発火→非同期での処理の呼び出し→結果の処理」といった Reactive Extensions で 1 シーケンスで記述できるような処理内でのスレッドの切り替えに ObserveOn メソッドは効果を発揮します。 198
  • 7.4. 時間を制御する Scheduler Scheduler には、実行場所を制御するもののほかに、時間を制御するタ゗プの Scheduler があり ます。実際の処理内部では、あまり出てくる頻度は高くないですがテスト時に 24 時間かかる処理 を 24 時間待たずに一瞬で 24 時間後の状態にするといったことも可能なため、このようなものも あるのだと把握しておいてください。7.4.1. HistoricalScheduler クラス 時間を制御する Scheduler クラスとして HistoricalScheduler を紹介します。 HistoricalScheduler は AdvancedBy メソッドや AdvancedTo メソッドを使って Scheduler 内の時間を制御します。 AdvancedBy が TimeSpan 型で AdvancedTo メソッドが DateTimeOffcet を使って時間を指定 します。HistoricalScheduler クラスの使用例を下記に示します。 var scheduler = new HistoricalScheduler(); // 1 秒間隔で値を発行する Observable.Interval(TimeSpan.FromSeconds(1), scheduler) // 購読 .Subscribe( i => Console.WriteLine("OnNext({0})", i), () => Console.WriteLine("OnCompleted")); // HistoricalScheduler を使ってプログラム内で Scheduler の時間を進める // 0.5sec 時間を進める Console.WriteLine("AdvanceBy(0.5sec)"); scheduler.AdvanceBy(TimeSpan.FromSeconds(0.5)); // 0.5sec 時間を進める Console.WriteLine("AdvanceBy(0.5sec)"); scheduler.AdvanceBy(TimeSpan.FromSeconds(0.5)); // 0.5sec 時間を進める Console.WriteLine("AdvanceBy(0.5sec)"); scheduler.AdvanceBy(TimeSpan.FromSeconds(0.5)); // 0.5sec 時間を進める Console.WriteLine("AdvanceBy(0.5sec)"); scheduler.AdvanceBy(TimeSpan.FromSeconds(0.5)); // 0.5sec 時間を進める Console.WriteLine("AdvanceBy(0.5sec)"); scheduler.AdvanceBy(TimeSpan.FromSeconds(0.5)); Observable.Interval メソッドを使って 1 秒間隔で値を発行する IObservable<T>のシーケンス を作成しています。このときの Scheduler に HistoricalScheduler を指定しています。そして 199
  • AdvanceBy メソッドを使ってプログラム内で Scheduler の時間を進めています。0.5 秒×5 回の 時間を進めているので合計で 2.5 秒の時間が進んでいることになります。実行結果を以下に示し ます。 AdvanceBy(0.5sec) AdvanceBy(0.5sec) OnNext(0) AdvanceBy(0.5sec) AdvanceBy(0.5sec) OnNext(1) AdvanceBy(0.5sec) AdvanceBy を 2 回呼んだ時点で値が発行され、さらに AdvanceBy を 2 回呼んだ時点で値が発行 されていることが確認できます。このように Reactive Extensions では時間を Scheduler 経由で 取得するようにすることで、 プログラムから柔軟に時間を制御することができるようになっていま す。8. 応用編 ここでは、これまでに紹介した Reactive Extensions のメソッドを組み合わせていくつか実用的 な例をプログラミングしてみようと思います。8.1. センサー監視 下記の要件を満たすプログラムを作成します。 10 個のセンサーがあります。10 個のセンサーは、それぞれに識別用の名前が与えられていて 1 秒間隔で数値(1 ~1000)を上げてきます。プログラムでは 10 秒間の間に一番大きな値を上げてきたセンサーの名前と値をコンソ ールに出力します。 ということで作成していきます。実際にセンサーは無いので下記のような疑似的なセンサーを表す クラスをセンサーに見立ててプログラムを書きます。 namespace SensorSample { using System; using System.Threading; // センサーを表すクラス class Sensor { // センサーの値はとりあえず乱数で発行する private static Random random = new Random(); // センサーの値を発行する間隔を制御するタ゗マー 200
  • 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)); } }} 201
  • // センサーが値を発行したときの゗ベント引数 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(); 202
  • } 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 行ずつ結果が表示されます。9. 参考サイト 本ドキュメントを書くにあたって参考にしたサ゗ト等を紹介します。  neue cc : http://neue.cc/ MS MVP for C#の neuecc さんのサ゗トです。日本語情報で一番濃い Reactive Extensions に関する情報が読めるサ゗トです。特に下記の勉強用リソースまとめは、参考になると思いま す。  Reactive Extensions を学習するためのリソースまとめ http://neue.cc/2011/01/26_300.html  連載:Reactive Extensions(Rx)入門 : http://www.atmarkit.co.jp/fdotnet/introrx/index/index.html @IT に neuecc さんが書いている Reactive Extensions の連載記事になります。  xin9le note Rx 入門゗ンデックス: http://xin9le.blogspot.com/2011/12/rx.html Hokuriku.NET などでスピーカーとして活躍されている@xin9le さんの入門記事です。 203
  •  Reactive Extensions : http://msdn.microsoft.com/en-us/data/gg577609 Reactive Extensions の公式サ゗トです。英語情報ですが、様々な情報ソースへのリンクがあ ります。  Reactive Extensions Team Blog : http://blogs.msdn.com/b/rxteam/ Reactive Extensions Team による公式ブログです。最新のリリース情報等が記載されて います。  Reactive Extensions Forum : http://social.msdn.microsoft.com/Forums/en-US/rx/threads MSDN Forum 内の Reactive Extensions Forum です。英語で活発なデゖスカッション が行われています。  Reactive Extensions for .NET Resources and Community : http://msdn.microsoft.com/ja-jp/data/gg577612 様々な情報へのリンクが集まっています。HoL から読むのがいいような気がします。  Rx on Channel 9 : http://channel9.msdn.com/tags/Rx/ Reactive Extensions について説明している Channel9 内の動画です。日本語だと非常に わかりやすいのですがいかんせん英語で説明されても 80%以上何を言っているのかわか らない・・・。 204