ぼやきごと/2013-03-17/C#:値型リスト内の条件一致要素検索における Nullable 型の利用 の変更点


#blog2navi()
*C#:値型リスト内の条件一致要素検索における Nullable 型の利用 [#y0077144]

[[前回の記事>ぼやきごと/2013-03-16/C#: SortedList や SortedDictionary における特定値未満・以下・以上・超のキーを持つ要素の取得]]にも書いた通り、 @code{IEnumerable<T>}; オブジェクトの中から条件に一致する最初および最後の要素を取得するには拡張メソッド @code{FirstOrDefault}; および @code{LastOrDefault}; が利用できます。

これらのメソッドは、条件に一致する要素が見つからなかった場合は @code{default(T)}; を返します。~
@code{T}; が参照型であれば @code{default(T)}; は @code{null}; なので、 @code{null}; が検索対象でさえなければ問題なく利用できるでしょう。~
しかし @code{T}; が値型の場合はどうでしょうか?

例えば、 @code{IEnumerable<int>}; の中から @code{0}; 以上の最初の値を取得するメソッドを次のように書いたとします。

#code(csharp){{
int FindPositive(IEnumerable<int> values)
{
    return values.FirstOrDefault(i => i >= 0);
}
}}

このメソッドの引数に配列 @code(new[] { -1, -2, -3 }); を渡すとどうなるでしょうか?~
配列の中には条件に合致する要素が存在しないため、このメソッドは @code{default(int)}; の値である @code{0}; を返します。~
しかし @code{0}; はこのメソッドにとって有効な戻り値であるため、呼び出した側は「値が見つからなかった」のか「値 @code{0}; が見つかった」のかわからなくなってしまいます。

「値が見つからなかった場合に無効な値を返すようにしたい」と考えた時、このメソッドをどう書き直すべきでしょうか?

まず考え付くのは、値が見つからなかったら負数を返すという方法です。~
しかし拡張メソッド @code{FirstOrDefault}; では値が見つからなかった場合に @code{default(T)}; 以外の値を返すことはできません。~
そこで拡張メソッド @code{First}; を使うことにしてみます。

#code(csharp){{
// 値が見つからなければ -1 を返す。
int FindPositive(IEnumerable<int> values)
{
    try
    {
        return values.First(i => i >= 0);
    }
    catch
    {
        return -1;
    }
}
}}

確かにこれならば要件を満たせそうです。~
しかし、そもそも目的の値が存在しない可能性が十分ありうる状況なのに例外処理を用いるというのはあまり美しいコードとは言えません。~
また、逆に @code{0}; 以下の値を検索するようなメソッドを作った場合は無効値に正数を用いる必要があり、返り値の一貫性に欠けます。

そこで、返り値の型を @code{int?}; に変更し、値が見つからなかったら @code{null}; を返すようにしてみましょう。

#code(csharp){{
// 値が見つからなければ null を返す。
int? FindPositive(IEnumerable<int> values)
{
    return (
        from i in values
        where i >= 0
        select (int?)i)
        .FirstOrDefault();
}
}}

「クエリ式はよくわからん!」という人のためにメソッドだけで書くと次の通り。

#code(csharp){{
// 値が見つからなければ null を返す。
int? FindPositive(IEnumerable<int> values)
{
    return
        values
            .Where(i => i >= 0)
            .Select(i => (int?)i)
            .FirstOrDefault();
}
}}

上述のコードでは次の流れで処理が行われています。

+@code{Where}; によって条件に合致する要素のみを列挙する @code{IEnumerable<int>}; オブジェクトを取得します。
--ここで条件に合致する要素がなければ空になります。
+@code{Select}; によって @code{IEnumerable<int>}; オブジェクトを @code{IEnumerable<int?>}; オブジェクトに変換して取得します。
+拡張メソッド @code{FirstOrDefault}; によって一番最初の要素を取得して返します。
--空の場合は既定値である @code{default(int?)}; 、即ち @code{null}; を返します。

何度も @code{IEnumerable<T>}; オブジェクトを作成していて効率が悪そうに見えますが、 @code{Select}; や @code{Where}; が返す列挙子は遅延実行を使用して実装されており、拡張メソッド @code{FirstOrDefault}; によって列挙されるまでは実際の評価は行われません。~
@code{FirstOrDefault}; は最初の要素を取得した時点で列挙を打ち切るため、条件に合致する要素が存在するのであればその要素までの値しか評価されず、評価回数は最小限で済むことになります。

このコードの肝は @code{Select}; による値型列挙要素の @code{Nullable<T>}; 型への変換です。~
これによって例外処理を行う必要がなくなり、「値が見つからなければ @code{null}; を返す」という返り値の一貫性を保つこともできます。

:追記|
@code{Select}; での型変換ではなく @code{Cast}; を使う手もありました。~
どちらでも効率は大して変わらないと思いますが一応コードを載せておきます。
#code(csharp){{
// 値が見つからなければ null を返す。
int? FindPositive(IEnumerable<int> values)
{
    return (
        from int? i in values
        where i >= 0
        select i)
        .FirstOrDefault();
}
}}
#code(csharp){{
// 値が見つからなければ null を返す。
int? FindPositive(IEnumerable<int> values)
{
    return
        values
            .Cast<int?>()
            .Where(i => i >= 0)
            .FirstOrDefault();
}
}}

RIGHT:Category: &#x5b;[[プログラミング>ぼやきごと/カテゴリ/プログラミング]]&#x5d;&#x5b;[[C#>ぼやきごと/カテゴリ/C#]]&#x5d; - 2013-03-17 02:45:23
----
RIGHT:&blog2trackback();
#comment(above)
#blog2navi()