かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

Reactive Extensions再入門 その22「単一の値を取得するメソッド」

過去記事インデックス

はじめに

2012年最初のBlogネタはReactive Extensions!!地味にいくよ!ということで今年もよろしくお願いします。

単一の値を取得するメソッド

ここでは、IObservableのシーケンスから単一の値を取得するために利用するメソッドについて説明します。

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の値を取得します。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);

5秒間隔で値を発行するIObservableから最初の値を取得しています。(正確にはSelectメソッドで発行された値を加工していますが)そして、Firstメソッドが呼び出される前と後にタイムスタンプを表示するコードを入れています。このコードの実行結果を下記に示します。

Timestamp 2012/01/03 18:25:45.783
Timestamp 2012/01/03 18:25:50.829
firstResult: value is 0

三行目に、Firstメソッドで取得した値が出力されていることが確認できます。このように、Firstメソッドでは、IObservableから最初に発行された値を取得できます。ここで注目したいのは、Firstメソッドの呼び出しの前後に入れているタイムスタンプを表示するメソッドの表示内容です。18:25:45から18:25:50となっていることからわかるようにFirstメソッドの呼び出し前と呼び出し後で5秒たっていることがわかります。
Firstメソッドの特徴として、最初の値がIObservableのシーケンスから発行されるまで、実行しているスレッドをブロックするという特徴があります。そのため非同期処理などの結果を待つのに使用することも可能です。(基本はブロックしないように作るのが一番いいです)また、下記のコードのように、永久に値が発行されないIObservableに対して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のシーケンスの最後の値を取得しています。Firstメソッドと同様にLastメソッドの呼び出し前と呼び出し後にタイムスタンプを表示しています。実行結果を下記に示します。

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秒間スレッドをブロックしています。

値が存在しない場合の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のシーケンス)場合はInvalidOperationExceptionを発行します。Lastメソッドについても同様です。以下にコード例と実行結果を示します。

// 1つも要素の無いIObservable
var noElementsSequence = new Subject<string>();
noElementsSequence.OnCompleted();

try
{
    // 最後の要素の取得
    var lastResult = noElementsSequence.Last();
}
catch (InvalidOperationException e)
{
    // 何もないので例外が発生する
    Console.WriteLine("Exception: {0}", e.Message);
}
Exception: Sequence contains no elements.
FirstメソッドとLastメソッドのオーバーロード

FirstメソッドとLastメソッドには、Func型のデリゲートを受け取るオーバーロードがあります。これは、デリゲートがtrueを返す最初または最後の要素を返すという点以外は、通常のFirstメソッドとLastメソッドと共通の動きをします。コードと実行結果については、割愛します。

FirstOrDefaultメソッドとLastOrDefaultメソッド

ここでは、FirstOrDefaultメソッドとLastOrDefaultメソッドについて説明します。このメソッドもFirstメソッドやLastメソッドと同様にIObservableのシーケンスから最初の値や最後の値を取得するために使用します。また、値が取得できるまでの間はスレッドをブロックする点も共通の動作になります。挙動が異なるのは、要素が存在しないIObservableのシーケンスに対して呼び出した場合になります。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
Timestamp 2012/01/03 18:55:13.623
firstResult: value is 0

実行結果もFirstメソッドと変わりません。次に、空のIObservableのシーケンスに対して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メソッドについても同様の動きとなるため、コード例と実行結果は割愛します。

FirstOrDefaultメソッドとLastOrDefaultメソッドのオーバーロード

FirstOrDefaultメソッドとLastOrDefaultメソッドにもFirstメソッドとLastメソッドと同様にFunc型のデリゲートを受け取るオーバーロードがあります。こちらも、デリゲートがtrueを返す要素のみに絞る点以外は、挙動は同じためコード例と実行結果は割愛します。

ElementAtメソッド

ここでは、ElementAtメソッドについて説明します。ElementAtメソッドはインデックスで指定した要素を取得するメソッドになります。FirstメソッドやLastメソッドが単一の値を返していたのに対してElementAtメソッドの戻り値はIObservableになります。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前のタイムスタンプを表示
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のシーケンスを作成して、ElementAt(3)で3番目の要素を取得しています。そして、ElementAtの結果のIObservableに対してSubscribeメソッドを呼び出して、結果を表示しています。

コードの本題とは関係ありませんが、EventWaitHandleクラスを使ってGenerateメソッドで作成したIObservableのシーケンスが終了するのを確実に待ち合わせています。これは、GenerateメソッドやElementAtメソッド, Subscribeメソッドの処理がバックグラウンドのスレッドで実行されているため、待ち合わせをしないと、Subscribeメソッドの処理が走る前にプログラムが終了してしまうためです。EventWaitHandleクラスについての説明はMSDNを参照してください。http://msdn.microsoft.com/ja-jp/library/system.threading.eventwaithandle.aspx
コードの実行結果を下記に示します。

Timestamp 2012/01/03 19:13:33.498
Timestamp 2012/01/03 19:13:33.538
elementAt3: value is 3
OnCompleted
Timestamp 2012/01/03 19:13:37.588

実行結果の3行目でvalue is 3と3番目の値が表示されていることが確認できます。また、最初の2つのタイムスタンプの表示は、ElementAtを含む一連のメソッドチェインの前後で出力していますが、ほとんど時間が経過していないことがわかります。そして、最後のタイムスタンプの出力までに4秒の時間が経過していることが確認できます。1つの値が1秒間隔で発行されているため3番目(0オリジンなので正確には4番目)の値が発行されるまでの時間と合致しています。

値が存在しない場合のElementAtメソッドの挙動

ElementAtで指定したインデックスに値が存在しない場合の挙動について説明します。この場合もFirstメソッドやLastメソッドと同様に例外が発生しますが、IObservableのシーケンス内で起きる例外なので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);

実行結果を下記に示します。

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が発生します。

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();
// gate.Setが呼ばれた後のタイムスタンプを表示する
Console.WriteLine("Timestamp {0:yyyy/MM/dd HH:mm:ss.FFF}", DateTime.Now);

0〜2の3つの要素しか発行していないIObservableのシーケンスに対して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と表示されていることから、範囲外の値を要求しても例外が発生せずにデフォルト値が発行されていることが確認できます。