この記事は C# Advent Calendar 2020 21日目の記事です。
#はじめに
僕はとある会社で働いているのですが、頑なにReactiveExtensions(以下Rx)に拒否反応を示す先輩がいます。
Rxの使い方と導入の利点を説明したのですが、なかなか理解してもらえず…。
そこで自分自身もRxの利点をきちんと理解し、胸を張って説明できるのか?と不安になってきたため、ここに整理してみます。
#なぜRxを導入すると幸せになれるのか
いきなり本題ですが、「なぜRxを導入すると幸せになれるのか」。
幸せになれるというのはただのキャッチフレーズであり言いすぎですが、なぜRxを導入するべきなのか。
それは、Rxとは__C#標準機能であるeventの完全上位互換1となる機能を提供するライブラリであるから__です。
つまり、Rxを導入すると標準のeventではできなかった、あんなことやこんなことが簡単にできるようになり、結果的に__ソースコードの可読性・保守性が飛躍的に高める__ことができます。
また、コード全体でRxを導入することで、イベント駆動型2のコーディングスタイルとなり、結果的に__クラス同士が疎結合となるような設計__がしやすくなります。
##Rxがeventに比べて追加で提供する機能
ここで重要なのは、eventに対してRxが追加で提供する機能が、__とんでもなく強力である__ということです。
具体的には、eventに比べて以下のような機能が追加で提供されます。
- イベント発行値に対して多彩なLINQオペレータを適用
- イベントのオブジェクト化
- 容易なイベント解除
一つずつ簡単な例を示します。
###1.イベント発行値に対してLINQオペレータを適用
この機能がRxのメインというか、要となる機能です。
IEnumerable<T>
でおなじみのLINQメソッド群を、ほぼそのままeventの発行値に対して適用することができます。
具体的には、以下のような事が可能になります。
eventPublisher.SomeIntEvent //Int型を発行する何らかのイベントに対して…
//発行された値のうち、偶数のものだけに絞り込み…
.Where(n => n % 2 == 0)
//それを文字列型に直して…
.Select(n => $"{n}が発行されました")
//購読する!
.Subscribe(str => Console.WriteLine(str));
Subscribe
というのは後で詳しく説明しますが、イベントを購読するという意味です。
要は、発行されたイベントをそのまま購読するのではなく、Where
やSelect
といったLINQメソッドチェーンを自由に適用し、都合の良い形に変形した上で、イベントを購読することができるというわけです。
これを同じことをeventでやろうとすると、LINQメソッドチェーンが使用できないため、次のようになります。
eventPublisher.SomeIntEvent += n =>
{
if(n % 2 == 0)
{
var str = $"{n}が発行されました";
Console.WriteLine(str);
}
}
LINQメソッドチェーンを利用した場合に比べ、ひと目でなにをやっているのかを理解することができず、可読性が低くなっています。
また、LINQのように処理がブロックごとに分離されていないため、変更に対して弱いという欠点もあります。
これは簡単な例なのでまだ良いのですが、複雑な処理になればなるほど、イベントの発行値に対してLINQオペレータが適用できるというRxの利点は、極めて大きく重要なものとなってきます。
###2.イベントのオブジェクト化
C#標準のeventはオブジェクトではないため、イベントを購読できる人はイベントに直接アクセスできる人だけとなります。
ところが、Rxの場合はイベントの購読権をIObservable
というオブジェクトとして表現されるため、__イベントの購読権を自由に別のクラスに引き渡したり__することができます。
具体例を見てみます。
まず、標準のeventの場合。eventがオブジェクトではないため、__eventを購読できるのはそのイベントを発行するクラスのインスタンスを保持しているクラスのみ__となります。
class EventPublisher
{
//..中略..
public event Action<int> SomeEvent;
//..中略..
}
//EventPublisherのインスタンスを直接保持しているクラス
class HogeClass
{
private EventPublisher publisher = new EventPublisher();
private MogeClass moge;
public HogeClass()
{
moge = new MogeClass();
//自分は当然イベント購読が可能
publisher.SomeEvent += n => { /* 何らかの処理 */ };
}
}
//EventPublisherのインスタンスを保持していないクラス
class MogeClass
{
public MogeClass()
{
//EventPublisherを保持していないので、EventPublisherが発行するイベントを購読できない
}
}
次に、Rxの場合。
Rxの場合は購読する権利をIObservable
というオブジェクトとして表現されるため、購読権を他のクラスに引き渡すことで、__イベントを発行するクラスのインスタンスを直接保持していなくてもイベントの購読が可能__となります。
class EventPublisher
{
//..中略..
public IObservable<int> SomeObservable { get; }
//..中略..
}
//EventPublisherのインスタンスを直接保持しているクラス
class HogeClass
{
private EventPublisher publisher = new EventPublisher();
private MogeClass moge;
public HogeClass()
{
//MogeClassにイベントの購読権を引き渡す
moge = new MogeClass(publisher.SomeObservable);
//自分は当然イベント購読が可能
publisher.SomeObservable.Subscribe(n => { /* 何らかの処理 */ });
}
}
//EventPublisherのインスタンスを保持していないクラス
class MogeClass
{
public MogeClass(IObservable<int> observable) //イベントの購読権を受けるようにする
{
//取得した購読権を使用して、イベントを購読可能!
observable.Subscribe(n => { /* 何らかの処理 */ });
}
}
###3.容易なイベントの解除
C#標準のイベントの場合、一度登録したイベントを解除するには-=
演算子を利用して、登録したときと全く同じメソッドを指定する必要があります。
つまり、__ラムダ式を登録した場合は解除できなくなってしまう__のです。
一方、Rxの場合は__Subscribe
したときに出てくるIDisposable
をDispose
するだけ__で済みます。
具体的には以下のような感じになります。
//イベント購読。購読時に解除用のIDisposableを取得する
IDisposable disposable = someObservable.Subscribe(x => /* 処理 */);
//..中略..
//購読を解除したいときはIDisposableをDisposeするだけ
disposable.Dispose();
Rxでは、イベントの購読権がIObservable
というオブジェクトとして表現されるのと同様に、__イベントの解除権もIDisposable
というオブジェクトで表現される__こともポイントです。
例えば、複数のイベントを購読して出てきたIDisposable
をList
にためておき、後でまとめて購読解除するといったことも容易に実現可能です。
//購読解除用IDisposableをためておくList
List<IDisposable> disposables = new List<IDisposable>();
//イベントを複数購読し、出てきたIDisposableをためておく
disposables.Add(observable1.Subscribe(x => /* 処理 */));
disposables.Add(observable2.Subscribe(x => /* 処理 */));
disposables.Add(observable3.Subscribe(x => /* 処理 */));
//..中略..
//購読解除したいときに一気に購読解除
foreach(var disposable in disposables)
{
disposable.Dispose();
}
ちなみに、上記のようなことが容易に行えるCompositeDisposable
クラスやAddTo
メソッドもあります。詳しくはググってみてください。
#Rxはどのような場面で役立つのか
このように様々な利点があるRxですが、どのような場面で利用できる・効果を発揮するのでしょうか?
大きく分けて、次の2つの場面で役立てることができると考えています。
##1.クラス間でメッセージのやり取りを行いたいとき
1つ目は、クラス間でメッセージ(データ)のやり取りを行いたい場面です。
これはつまるところ、__eventの代わりとして利用できます__という意味です。
Rxはeventの完全上位互換なので当然ですね。
すでに説明したように、Rxはeventに比べて非常に強力な機能が追加で利用できるため、わざわざeventを利用する意味はありません3。Rxを使いましょう。
##2.ある時間間隔で一定の処理を行いたい場合
2つ目は、一定、または不定の時間間隔である処理を行いたい場合です。
いわゆるタイマー処理です。Rxはこの場合にも、その威力を存分に発揮します。
というのも、Rxには一定、または不定の時間間隔でイベント(IObservable
)を発行するクラスを生成するファクトリメソッドが、予め用意されているのです。
そのため、時間間隔で何かをしたい場合、利用者はただ、その用意されたファクトリメソッドを呼び出し、イベントを購読して処理を登録するだけで済みます。
System.Windows.Forms.Timer
とかSystem.Threading.Timer
は不要です。Unityの場合はTime.deltaTime
をアレコレしたり、WaitForSecounds
をどうこうする必要はもうありません。
具体的には後で説明しますが、簡単に書くと次のように非常に簡潔に一定の時間間隔で処理を行うことができます。
Observable.Interval(100).Subscribe(_ => Console.WriteLine("100msごとにメッセージが表示されるよ!"));
やっていることとしては「『一定間隔でイベントを発行するクラス』からのメッセージを受け取って一定の処理を実行する」のと同じであるため、本質的には「1.クラス間でメッセージのやり取りを行いたいとき」に帰着します。
Rxを使う上で最低限必要な知識
Rxの利点と使い所がわかったところで、実際にどのように使うのかを説明したいところですが、その前にRxを使用する上で最低限必要な知識をここで紹介します。
##Observerパターン
Rxはデザインパターンの一つである__Observerパターン__ が基礎になっています。
Observerパターンについて詳しく知りたい方は、別記事を書きましたので↓を参照してください。
ところが、実際にRxを使う上ではObserverパターンそのものを意識する必要はあまりありません。
その上で、どうしてもRxを使う上で必要となる次の3つのクラスとインターフェイス、それからSubscribe
メソッドのオーバーロードについて簡単に説明していきます。
※一部↑の記事と内容が重複しています
-
IObserver<T>
インターフェイス -
IObservable<T>
インターフェイス -
Subject<T>
クラス - アクションを直接受け取る
Subscribe
メソッドのオーバーロード
※この記事では必要最低限の解説のみ行います。詳しい説明は↑の記事を参照ください。
IObserver<T>
インターフェイス
IObserver<T>
インターフェイスは、__発行された値を受け取るクラス__が実装するインターフェイスです。
定義は次のようになっています。
public interface IObserver<T>
{
/// <summary>
/// 値を通知する
/// </summary>
/// <param name="value"></param>
void OnNext(T value);
/// <summary>
/// 例外が発生したことを通知する
/// </summary>
/// <param name="e"></param>
void OnError(Exception e);
/// <summary>
/// 値の発行がすべて完了したことを通知する
/// </summary>
void OnCompleted();
}
IObserver<T>
に値を通知したい人は、この__OnNext
メソッドの引数に通知したい値を乗せて呼び出す__ことで、値を通知することができます。
また、「もう発行する値がない」ことを伝えるOnCompleted
メソッドや、値の発行元で何らかの例外が発生してしまったことを伝えるOnError
メソッドも用意されています。
IObservable<T>
インターフェイス
IObservable<T>
インターフェイスは、__値を発行するクラス__が実装するインターフェイスです。
定義は次のようになっています。
public interface IObservable<T>
{
/// <summary>
/// 値を購読する
/// </summary>
/// <param name="observer">値の発行先</param>
/// <returns></returns>
IDisposable Subscribe(IObserver<T> observer);
}
このクラスが発行する値を受け取りたいクラスは、この__Subscribe
メソッドに自分自身を引数で引き渡して呼び出す__ことで、発行先として登録することができます。
※値を受け取るクラスはIObserver<T>
である必要があります
発行先のIObserver<T>
を受け取ったObservableなクラスは、値を発行するタイミングで、受け取ったIObserver<T>
をOnNext
することによって、値を通知することができます。
ここまでが、__ベタなObserverパターン__の__簡易的な説明__になります。
繰り返しになりますが、きちんとObserverパターンを理解したい場合は下記記事を参照ください。
C#でObserverパターンをきちんと理解して実装する
####ベタなObserverパターンの問題点
ベタなObserverパターンでも値の発行と受け取りは可能なのですが、次のような問題が発生してしまい使い勝手が悪いです。
- 値を発行する側は、同じ型の値を複数種類発行できない。
- 値を受け取る側も同様に、同じ型の値を複数種類購読できない。
#####1.値を発行する側は、同じ型の値を複数種類発行できない。
値を発行する側のクラスは、同じ型の値を複数種類発行することができません。
例えば「キーボード」を例にとって考えてみます。
キーボードは、押されたキーをPCに送信します。このとき、同時押しを考慮すると、「押されたキー」だけではなく「離されたキー」の情報もPCに送信しなければなりません。
つまり、「押されたキー」「離されたキー」の2種類の値をPCに発行したいことになります。
これをそのまま実装しようと思っても、、
class KeyBoard : IObservable<KeyCode>
{
private IObserver<KeyCode> m_observer;
//値の発行先を受け取る
public IDisposable Subscribe(IObserver<KeyCode> observer)
{
m_observer = observer;
return null; //Disposeの処理は省略
}
//キーを謳歌されたときの処理
private void OnPushDown(KeyCode keyCode)
{
//押下されたキーを通知する
m_observer.OnNext(keyCode);
}
//押されたキーが離されたときの処理
private void OnPushUp(KeyCode keyCode)
{
//m_observer.OnNextすると押下通知を混ざってしまう
}
}
このように、押下されたキーの通知と、押下状態を解除されたキーの通知が混ざってしまい、それぞれを分けて通知することができません。
#####2.値を受け取る側も同様に、同じ型の値を複数種類購読できない。
この問題は受け取る側(Observer側)にも発生します。
理由は、IObserver<T>
インターフェイスを複数個実装できないため、受け取り口が同じになってしまい、これまたすべての通知が混ざってしまうからです。
例えば、次のようにPCにに2台のキーボードを接続し、キーボードからの入力を購読したとします。
しかし、両方のキーボードからの通知が同じOnNext
に来るので、どちらのキーボードからの入力なのか判別できません。
class PC : IObserver<KeyCode>
{
private KeyBoard keyBoard1, keyBoard2;
public PC()
{
//例えば、2種類のキーボードからの通知を購読したとしても…
keyBoard1.Subscribe(this);
keyBoard2.Subscribe(this);
}
public void OnCompleted()
{
//省略
}
public void OnError(Exception error)
{
//省略
}
public void OnNext(KeyCode value)
{
//両方ともここに通知がくるので、どちらのキーボードからの通知かわからない
}
}
これらの問題を解決するために、Rxには次に紹介するSubject<T>
クラスが用意されています。
Subject<T>
クラス
Subject<T>
クラスは、 IObservable<T>
インターフェイスとIObserver<T>
インターフェイスを両方実装したクラス です。
つまり、Subject<T>
は、「値を受け取る機能」と「値を発行する機能」を併せ持ったクラス、ということになります。
これがどう役立つのかというと、ちょうど 値の発行側と受け取り側の仲介役 のような役割を果たしてくれます。
####ObserverとObservableの間に立つ仲介役
今までのベタなObserverパターンでは、ObserverがObservableに自分自身を値の発行先として登録して、Observableは登録されたObserverに値を渡す…といったように、ObserverとObservableが直接繋がっていました。
直接つながっているが故、お互いが言わば密結合のような状態になり、ObserverはSubscribe
したただ1種類の値しか受け取れないし、Observableも自分が発行する値をただひとつに定めなければなりませんでした。
そこへ、仲介役となるSubject<T>
クラスを登場させることにより、互いの密結合を解消させることができます。
しかも、Subject<T>
クラスはIObserver<T>
とIObservable<T>
の機能を持っているので、もはや元の「値を受け取るクラス」と「値を発行するクラス」がそれらの機能を持つ必要はありません。
__購読者の管理、発行された値の通知といった煩雑な処理は、全部Subject<T>
が担ってくれる__ので、元のクラスはそのクラス本来の責務に専念することができるようになります。
しかも!Subject<T>
クラスは完全に独立した存在なので、発行したい値のぶんだけ、好きなだけ仲介役を生成することができます!
これによって、ベタなObserverパターンのデメリットとして挙げた「ただ一つの種類の値しか発行・購読ができない」を解消させる事が可能となります。
以下にSubject<T>
クラスによる利点を整理しておきます。
- 値を発行するクラス、値を受け取るクラスが
IObservable<T>
、IObserver<T>
を実装しなくても良くなる - 仲介役の役割をしてくれるので、値を発行するクラス、値を受け取るクラスが密結合しなくなる
- 好きなだけ仲介役を用意することで、何種類でも値の発行・受け取りが可能となる
実際にどのようにSubject<T>
クラスを使用するのかは、「Rxを使ってみる」の章で説明します。
###アクションを直接指定するSubscribe
のオーバーロード
Subject<T>
クラスの登場により、値を受け取るクラスはIObserver<T>
である必要がなくなりました。
そのため、__Subscribe
メソッドの引数で指定するIObserver<T>
をどうするか__という問題が発生します。
ここで、IObserver<T>
インターフェイスの役割について考えてみると、ただ「値を受け取ったときに任意の処理を実行できる」ということでした。
であれば、次のように任意の処理を直接指定できてもよいのでは?と考えることができます。
void Subscribe<T>(Action<T> onNext, //OnNext時の処理を直接指定
Action<Exception> onError, //OnError時の処理を直接指定
Action onCompleted); //OnCompleted時の処理を直接指定
Rxには、このSubscribe
メソッドのオーバーロードが用意されているため、値を受け取るクラスはIObserver<T>
である必要はなく、値を受け取ったときのコールバック処理を直接指定することができるようになっています。
実際にどのようにSubscribe
メソッドのオーバーロードを使用するのかは、「Rxを使ってみる」の章で説明します。
Rxの環境構築
Rxを使う前に、Rxを使えるように開発環境を用意する必要があります。
とはいっても、大したことは有りません。
Windowsアプリケーション開発の場合
VisualStudioによるWindowsアプリケーション開発の場合、VisualStudioのNuGetを使って~~System.Reactive.Linq
~~ System.Reactive
4をインストールすれば完了です。
↑画像はSystem.Reactive.Linq
を指していますが、System.Reactive
をインストールしてください。
Unity開発の場合
Unity開発の場合はAssetStoreからUniRxをインストールすれば完了です。
Rxを使ってみる
それでは、IObserver<T>
インターフェイス, IObservable<T>
インターフェイス, Subject<T>
クラス、Subscribe
メソッドのオーバーロードについて理解したところで、実際にRxを使ってみます。
##値を発行する方法
値を発行したいクラスが値を発行するには以下のようにします。
- 仲介役となる
Subject<T>
インスタンスをprivateフィールドで内部に持ちます - その
Subject<T>
をIObservable<T>
で公開します -
Subject<T>
にOnNext
して値を発行します
具体的なソースコードは以下のようになります。
//値を発行するクラス
class PublishClass
{
//1. 仲介役となるSubject<T>インスタンスをprivateフィールドで内部に持ちます
private Subject<int> m_someSubject = new Subject<int>();
//2. そのSubject<T>をIObservable<T>で公開します
public IObservable<int> SomeObservable => m_someSubject.AsObservable();
private void PublishValue(int num)
{
//3. Subject<T>にOnNextして値を発行します
m_someSubject.OnNext(num);
}
}
AsObservable
はなくても良いのですが、つけておくとIObservable<T>
をSubject<T>
にキャストされるのを防ぐことができます。
##値を受け取る方法
値を受け取りたいクラスが値を受け取るには以下のようにします。
- 値を発行するクラスのインスタンスを取得します
- 値を発行するクラスから公開された
IObservable<T>
インターフェイスのSubscribe
メソッドを呼び出し、引数に値を受け取ったときの処理を指定します
具体的なソースコードは以下のようになります。
class ReceiveClass
{
//1. 値を発行するクラスのインスタンスを取得します
private PublishClass publisher = new PublishClass();
public ReceiveClass()
{
//2. 値を発行するクラスから公開された`IObservable<T>`インターフェイスの`Subscribe`メソッドを呼び出し、引数に値を受け取ったときの処理を指定します
publisher.SomeObservable.Subscribe(n => Console.WriteLine($"{n}を受け取りました");
}
}
###IObservable<T>
はインターフェイス経由で引き渡すとReadOnlyが保証される
上記の例では、ReceiveClass
がPublishClass
のインスタンスを直接参照しています。
この方法でももちろん値の受け取りは可能なのですが、下記のように__IObservable<T>
を引き渡す専用のインターフェイス経由でアクセス__するように設計すると、__ReceiveClass
からPublishClass
へのアクセスが読み取り専用であることが保証__され5、より良い設計となります。
//SomeObservableの購読を提供する読み取り専用インターフェイス
public interface IObservableSome
{
IObservable<int> SomeObservable { get; }
}
class PublishClass : IObservableSome
{
//1. 仲介役となるSubject<T>インスタンスをprivateフィールドで内部に持ちます
private Subject<int> m_someSubject = new Subject<int>();
//2. そのSubject<T>をIObservable<T>で公開します
public IObservable<int> SomeObservable => m_someSubject.AsObservable();
private void PublishValue(int num)
{
//3. Subject<T>にOnNextして値を発行します
m_someSubject.OnNext(num);
}
}
class ReceiveClass
{
//1. 値を発行するクラスのインスタンスを取得します
private IObservableSome publisher = new PublishClass();
public ReceiveClass()
{
//2. 値を発行するクラスから公開された`IObservable<T>`インターフェイスの`Subscribe`メソッドを呼び出し、引数に値を受け取ったときの処理を指定します
publisher.SomeObservable.Subscribe(n => Console.WriteLine($"{n}を受け取りました");
}
}
このように、値の購読のみが可能な読み取り専用インターフェイス経由でアクセスさせることで、__例えPublishClass
に読み取り専用ではないメンバが存在したとしても、ReceiveClass
からは読み取り専用であることが保証される__ため、保守性の向上に繋がります。
インターフェイスを利用する利点については、下記記事に詳しく記載していますので、適宜参照ください。
【C#】インターフェイスの利点が理解できない人は「インターフェイスには3つのタイプがある」ことを理解しよう
##LINQを利用してデータを加工する方法
ここからがRxの本領発揮です。
これまで紹介した「値の発行・受け取り」は、C#標準のeventでも全く同じことができます。
しかし、Rxを使うと、以下のようにLINQを利用して__受け取ったデータを自分の都合の良いように自由に加工することができます__ 。
publisher.SomeObservable
//0以上の値に絞り込み
.Where(n => n > 0)
//受け取った値を秒としてTimeSpan構造体を生成
.Select(n => TimeSpan.FromSeconds(n))
//TimeSpan構造体を購読
.Subscribe(time => Console.WriteLine($"受け取った値を秒とすると{time.TotalHours}時間です"));
LINQオペレータがIEnumerable<T>
を受け取ってIEnumerable<T>
を返すように、RxオペレータもIObservable<T>
を受け取ってIObservable<T>
を返すように設計されているため、このようなことが可能となります。
他にもいろいろなオペレータが用意されています。詳しくは @toRisouPさんの下記のような記事が役立つと思いますので参照ください。
##ファクトリメソッドを利用してイベントソースを簡単に生成する方法
今までは、自作クラスの内部にSubject<T>
を持って値を発行したいタイミングでOnNext
を行うように、「値の発行側」を自前で作っていました。
しかしRxには、便利な「値の発行側」があらかじめ用意されています。これらは、IObservable
を生成することから、「ファクトリメソッド」と呼ばれています。
ここでは、Rxに搭載されているいくつかのファクトリメソッドのうち、個人的にもっともよく使用するObservable.Interval
ファクトリメソッドとObservable.FromEvent
ファクトリメソッドを紹介します。
Observable.Interval
ファクトリメソッド
Observable.Interval
ファクトリメソッドは、__一定時間間隔で値を発行するIObservable<T>
を生成するファクトリメソッド__です。
引数にTimeSpan
構造体を入れるだけで、その指定した時間間隔で値を発行してくれるIObservable<T>
が手に入ります。
発行される値は、0から順にインクリメントされたものになります。
次のように使用します。
Observable.Interval(TimeSpan.FromSeconds(1)) //1秒間隔で値を発行
.Take(5) //値を5回受け取る
.Timestamp() //タイムスタンプを付加
.Subscribe(t => Console.WriteLine($"Value:{t.Value} Time:{t.Timestamp}"),
() => Console.WriteLine("OnCompleted"));
このコードを順に追って説明します。
-
Observable.Interval(TimeSpan.FromSeconds(1))
で、1秒間隔で0,1,2,...と値を順番に発行するIObservable
を作り出します。 -
Take(5)
で発行された値を5個だけ受け取り、その後OnCompletedするように設定します。 -
Timestamp()
で受け取ったときのタイムスタンプを付加します。6 - 流れてきた値を購読して出力します。
出力結果は次のようになります。
Value:0 Time:2020/09/28 13:27:06 +00:00
Value:1 Time:2020/09/28 13:27:07 +00:00
Value:2 Time:2020/09/28 13:27:08 +00:00
Value:3 Time:2020/09/28 13:27:09 +00:00
Value:4 Time:2020/09/28 13:27:10 +00:00
OnCompleted
このように、1秒間隔で値が発行されていることがわかります。
Observable.Interval
ファクトリメソッドを使うと、このように簡単に一定時間間隔で値が発行されるIObservable<T>
を作り出すことができるので、一定周期で一位の処理を行いたい場合などに非常に便利です。
ちなみに、一定間隔ではなく「任意の時間間隔」で値を発行することもできます。
少しやり方が複雑になるのですが、興味がある方は下記記事も参照ください。
【ReactiveExtensions】任意の時間間隔で値を発行する2つの方法
###Observable.FromEvent
ファクトリメソッド
Observable.FromEvent
ファクトリメソッドは、その名の通り__「C#標準のイベント」をIObservable<T>
に変換するファクトリメソッド__です。
記事の冒頭で説明した__「イベントのオブジェクト化」を実現するファクトリメソッド__でもあります。
このファクトリメソッドを利用して、例えば__クラスライブラリから提供されるeventをIObservable<T>
に変換する__ことで、LINQオペレータを適用する、eventのオブジェクト化により別のクラスに購読権を引き渡すといった、__Rxの利点を活用する__ことが可能になります。
例えば、System.Windows.Forms.Button
のClick
イベントをIObservable<Unit>
に変換するには以下のようにします。
IObservable<Unit> clickEvent =
Observable.FromEvent<EventHandler, Unit>(
h => (sender, eventargs) => h(Unit.Default),
h => button1.Click += h,
h => button1.Click -= h);
引数が複雑なのですが、簡単に書くと第一引数には「eventから発行された値をどのようにIObservable<T>
シーケンスに伝達するか」、第二引数には「Subscribe
されたときの処理」、「第三引数にはDispose
されたときの処理」を記述します。
本当はもう少し詳しく書きたいのですが、アドベントカレンダーの公開まで時間がないので割愛させていただきます(残り5時間あまり。。)
他にもいろいろなファクトリメソッド
Rxには他にも多数のファクトリメソッドが用意されています。
詳しくは、@okazukiさんの下記記事などを参考にすると良いと思います。
Reactive Extensions再入門 その3「IObservableのファクトリメソッド」
#Rxを導入する利点まとめ
長くなってしまったので、以下にRxを導入する利点をまとめます。
##可読性の向上
Rxを導入するとソースコードの可読性の向上に繋がります。
具体的には、次の2つの要因が可読性の向上に寄与します。
- LINQによる可読性の向上
- イベント駆動型プログラミングによる可読性の向上
###LINQによる可読性の向上
これはもう言わずもがなと思いますが、__RxのLINQ機能を利用することで、可読性の向上が期待__できます。
C#標準のイベントを使い続け、以下のようなコードになっていたら目も当てられません。
int tmpValue;
hogeClass.MogeEvent += n =>
{
if(tmpValue != n) //前回と値が変わっていたら
{
tmpValue = n;
if(n > 100)
{
Console.WriteLine(n);
}
}
}
hogeClass.FugaEvent += n => Console.WriteLine(n);
RxとLINQを利用すると、以下のように簡潔に分かりやすく記述できます。
どちらが可読性が高いかは一目瞭然です。
hogeClass.MogeObservable
//前回と値が異なるものだけ通す
.DistinctUntilChanged()
//その中から100より大きいものに絞り込む
.Where(n => n > 100)
//FugaObservableと合成して次に流す
.Merge(hogeClass.FugaObservable)
//購読する
.Subscribe(n => Console.WriteLine(n));
###イベント駆動型プログラミングによる可読性の向上
Rxをプロジェクト全体に導入すると、自然とイベント駆動型のコーディングスタイルとなります。
そのため、「情報を欲しい人が取りに行く」という自然なソースコードになるため、可読性の向上が期待できます。
例えば、「情報を持っているクラス」と「その情報がほしいクラス」があるとします。
Rxを導入しない場合、「情報を必要とするクラス」は情報をもらう口となるようなメソッドを用意しておき、情報の獲得に関し受動的であるようなコードになることがあります。
//情報を持っているクラス
class HaveInformationClass
{
//情報を受ける口を用意しておく。情報は受動的にもらう。
public void ReceiveInformation(Information information)
{
//処理
}
}
//情報を必要とするクラス
class InformationRequiredClass
{
private HaveInformationClass destination;
private Information information;
private void SendInformation()
{
//「情報を必要とするクラス」が欲しい情報を渡しに行く
destination.ReceiveInformation(information);
}
}
しかし、このようなコーディングスタイルはいささか不自然に感じます。
本来、「情報は必要とする人が取りに行く」というのが自然であると考えるからです。
Rxは、__まさにこの「情報は必要とする人が取りに行く」コーディングスタイルが実現される__ため、各クラスが自然な行動になり、可読性が向上します。
//情報を持っているクラス
class HaveInformationClass
{
private InformationRequiredClass infoSource;
public HaveInformationClass()
{
//情報を必要とするクラスが能動的に貰いに行く
infoSource.ObservableInformation
.Subscribe(info => /* 処理 */);
}
}
//情報を必要とするクラス
class InformationRequiredClass
{
public IObservable<Information> ObservableInformation { get; }
//--省略--
}
##変更に強い
前項で紹介した「情報を欲しい人が取りに行く」という自然なコーディングスタイルは、変更に対しても強いです。
「情報を欲しい人」が情報の獲得に対し受動的なコーディングスタイルの場合、__「情報を欲しい人」が増えた場合に、情報の発信元にも変更が必要__になってきます。
ところが、「情報を欲しい人が取りに行く」場合、「情報を欲しい人」が増えたとしても、情報の発信側には何も影響がありません。
##密結合を回避できる
前述した通り、Rxを導入するとイベントをIObservable<T>
オブジェクトとして扱えるようになります。
IObservable<T>
オブジェクトは「情報の購読権」として自由に引き渡しが可能であるため、__情報の発信ソースを直接知らないクラスでも、IObservable<T>
オブジェクトを引き渡すことで情報の購読が可能__になります。
Rxを導入しなかった場合、イベントをオブジェクトとして扱えないため、情報を購読するためには、__その情報を発信しているクラスを直接知る必要__が出てきます。
それ故、__本来関わる必要がなかったクラス同士が関わってしまい、クラス同士の結合度が高くなる恐れ__があります。
##基礎部分だけなら、学習コストも高くない
学習コストが高いと思われがちなRxですが、この記事で紹介したような「値の発行」「値の購読」「LINQの適用」などの基本的な部分だけならそこまで学習コストは高くありません。
以下の通り再掲しておきます。
###「値の発行」3ステップ
- 仲介役となる
Subject<T>
インスタンスをprivateフィールドで内部に持ちます - その
Subject<T>
をIObservable<T>
で公開します -
Subject<T>
にOnNext
して値を発行します
###「値の購読」2ステップ
- 値を発行するクラスのインスタンスを取得します
- 値を発行するクラスから公開された
IObservable<T>
インターフェイスのSubscribe
メソッドを呼び出し、引数に値を受け取ったときの処理を指定します
###LINQの適用
IEnumerable<T>
のLINQと全く同じように利用可能です。
一部IEnumerable<T>
にはあるけどIObservable<T>
にはないオペレータや、その逆もありますが、調べながら使っていくうちにだんだん覚えていくと思われます。
###使い込むならば踏み込んだ理解も必要
ただ、Rxを本格的に使い込んでいくならば、IObservable<T>
のHot-Coldの違いなど、少し踏み込んだ部分の理解も必要になってきます。
この章で言いたいのは、「学習コストが高そう」という理由だけで、Rxを導入する多大なメリットを捨てないでほしいということです。
[追記]
RxのHot-Cold関係の記事を書きましたので、Rxを少し使い慣れてきた頃にご覧ください!
#さいごに
アドベントカレンダーの投稿期限ぎりぎりになってしまったため、最後駆け足な感じになってしまいましたが、Rxを導入する利点の説明になっているでしょうか。
普段Rxを多用していますが、「なぜ、Rxを導入するべきなのか?」という点について、他人にきちんと説明できるかが不安で整理してみました。
色々ツッコミどころがあるかと思いますが、もしよければコメント残していただけると幸いです。
また、下記に参考にさせて頂いた記事、及びRxの理解に役立った記事のリンクを掲載させていただきます。
-
イベント駆動型という表現が適切かどうかあまり自信がありません… ↩
-
Rxの利点をすべて利用しない(event標準機能だけで事足りる)ことが確定している場合はこの限りではありません。 ↩
-
@soi様より指摘いただきました。
System.Reactive.Linq
は互換性のために残っているパッケージだそうで、今はSystem.Reactive
をインストールすれば良いそうです。 ↩ -
IObservable<T>
はSubscribe
のみができるインターフェイスなので、IObservable<T>
は読み取り専用です。したがって、IObservable<T>
しか公開しないインターフェイスは、読み取り専用となります。 ↩ -
Timestamp()
を通過すると、流れてきた値はTimestamped<T>
という構造体にラップされます。この構造体はT
型のValue
プロパティとDateTimeOffset
型のTimestamp
プロパティを持ち、それぞれラップされた値とタイムスタンプを取得できるようになっています。 ↩