かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Reactive Extensions再入門 その13「最後の値を取得するLatestとMostRecentメソッド」

はじめに

今回は、これまでにないタイプのメソッドについて説明します。IObservableからIEnumerableへの変換を行うメソッドです。用途は最後にIObservableから発行された値を取得するです。

Latestメソッド

ここでは、Latestメソッドについて説明します。LatestメソッドはIObservableからIEnumerableへ変換を行うメソッドです。変換後のIEnumerableではMoveNextメソッドの呼び出し時に、変換元のIObservableからの値の発行を待ちます。既に値が発行されていたり、値が発行されると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);
    }
    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 loop
OnNext(1)
LatestValue : 1
OnNext(2)
LatestValue : 2
OnNext(3)
LatestValue : 3
OnNext(4)
LatestValue : 4
OnNext(5)
LatestValue : 5
OnNext(6)
LatestValue : 6
OnNext(7)
LatestValue : 7
OnNext(8)
LatestValue : 8
OnNext(9)
LatestValue : 9
OnNext(10)
OnCompleted()
LatestValue : 10
End latest loop

動作結果から、OnNextで値が発行された後に、Latestで取得したIEnumerableのループで値が取得できていることが確認できます。また、もとになるIObservableのシーケンスが終了したタイミングでLatestメソッドで変換した結果のIEnumerableのループも抜けていることが確認できます。
今回の例では、値の発行とLatestで変換したIEnumerableから取得できる値が1対1の関係にあるように見えますが、Latestで変換したIEnumerableが返す値はあくまで最後に発行された値になります。例えば、IEnumerableでのループが1周する間に2回値が発行された場合、IEnumerableは最初に発行された値は無視して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();
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メソッドの挙動になります。

MostRecentメソッド

Latestメソッドと同じような動きをするメソッドにMostRecentというメソッドがあります。このメソッドもIObservableからIEnumerableへ変換を行いますが、Latestメソッドが最後に発行された値を返して、値が無い場合は値が発行されるまで待つのに対して、MostRecentは最後に発行された値をキャッシュしておいて、キャッシュを返します。そのため、IEnumerableから値を取得する際にブロックされることがありません。コード例を下記に示します。

// 値の発行元
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);
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 : True
CurrentValue : -1
MoveNext : True
CurrentValue : -1
-----
OnNext(10)
最後に発行した値が返されるようになる
MoveNext : True
CurrentValue : 10
MoveNext : True
CurrentValue : 10
-----
OnNext(100)
OnNext(1000)
最後に発行した値が返されるようになる
MoveNext : True
CurrentValue : 1000
MoveNext : True
CurrentValue : 1000
-----
OnCompleted()
OnCompletedを呼ぶと、MoveNextがFalseを返すようになる
MoveNext : False

このようにLatestではMoveNextでブロックされるようなケースでもMostRecentではブロックされずに最後の値のキャッシュを返すことが確認できます。