かずきのBlog@hatena

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

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

過去記事インデックス

はじめに

単一の値を取得する系メソッドの紹介の「その2」です!何故初回で全部やらなかったかというと、素直にここで紹介するSingleメソッドの存在を忘れていましたorz

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
singleResult: 1
End 2012/01/09 23:14:20.91

3行目で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.123
Dump 2012/01/09 23:21:25.133, Value = 0
Dump 2012/01/09 23:21:26.147, Value = 1
InvalidOperationException: Sequence contains more than one element.
End 2012/01/09 23:21:26.147

2つ目のDump後にInvalidOperationExceptionがスローされていることが確認できます。

Singleメソッドのオーバーロード

SingleメソッドにはFirstメソッドやLastメソッドやElementAtメソッドと同様に、値のフィルタリングを行うためのFunc型のデリゲートを渡すオーバーロードがあります。これをつかうことで、複数の値が発行されるIObservableのシーケンスから特定の条件に合致する単一の要素を取り出すことが出来ます。メソッドのシグネチャを下記に示します。

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が取得できています。
このオーバーロードを使った場合も、引数で指定した条件にあうものが複数ある場合や、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のシーケンスに対して10以上の値を1つ取得するようにSingleメソッドを呼び出しています。そのためInvalidOperationExceptionの例外が発生します。実行結果を下記に示します。

Start 2012/01/09 23:46:39.903
Dump 2012/01/09 23:46:40.917, Value = 0
Dump 2012/01/09 23:46:41.971, Value = 1
Dump 2012/01/09 23:46:42.976, Value = 2
Dump 2012/01/09 23:46:43.989, Value = 3
Dump 2012/01/09 23:46:45.013, Value = 4
InvalidOperationException: Sequence contains no elements.
End 2012/01/09 23:46:45.014

SingleOrDefaultメソッド

ここでは、SingleOrDefaultメソッドについて説明します。このメソッドはSingleメソッドと同様にIObservableのシーケンスから単一の値を取得するために使用します。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();

// 1つも値が取得できない場合はnullが返る
var singleResult = s.SingleOrDefault();
// 結果を出力
Console.WriteLine("singleResult: {0}", singleResult ?? "null");
このコードの実行結果を下記に示します。
singleResult: null

Singleメソッドでは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);

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つ目の値が発行されたタイミングで例外が発生していることが確認できます。

SingleOrDefaultメソッドのオーバーロード

SingleOrDefaultメソッドにもSingleメソッドと同様にFunc型のデリゲートを受け取るオーバーロードがあります。このメソッドの挙動は、Singleメソッドの時と同様に引数で渡したデリゲートでフィルタリングを行いつつ、SingleOrDefaultのコード例で示したように1つも値が取得できなかった場合にデフォルト値を返します。
動作はSingleOrDefaultとSingleメソッドのオーバーロードで示したものと、ほぼ同じような形になるため、コード例については割愛します。