Home / ぼやきごと / 2013-03-17
2013-03-17

C#:値型リスト内の条件一致要素検索における Nullable 型の利用

前回の記事にも書いた通り、 IEnumerable<T> オブジェクトの中から条件に一致する最初および最後の要素を取得するには拡張メソッド FirstOrDefault および LastOrDefault が利用できます。

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

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

すべて開くすべて閉じる
  1
  2
  3
  4
 
-
|
!
int FindPositive(IEnumerable<int> values)
{
    return values.FirstOrDefault(i => i >= 0);
}

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

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

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

すべて開くすべて閉じる
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
-
!
-
|
-
|
!
|
-
|
!
!
// 値が見つからなければ -1 を返す。
int FindPositive(IEnumerable<int> values)
{
    try
    {
        return values.First(i => i >= 0);
    }
    catch
    {
        return -1;
    }
}

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

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

すべて開くすべて閉じる
  1
  2
  3
  4
  5
  6
  7
  8
  9
-
!
-
|
|
|
|
|
!
// 値が見つからなければ null を返す。
int? FindPositive(IEnumerable<int> values)
{
    return (
        from i in values
        where i >= 0
        select (int?)i)
        .FirstOrDefault();
}

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

すべて開くすべて閉じる
  1
  2
  3
  4
  5
  6
  7
  8
  9
-
!
-
|
|
|
|
|
!
// 値が見つからなければ null を返す。
int? FindPositive(IEnumerable<int> values)
{
    return
        values
            .Where(i => i >= 0)
            .Select(i => (int?)i)
            .FirstOrDefault();
}

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

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

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

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

追記
Select での型変換ではなく Cast を使う手もありました。
どちらでも効率は大して変わらないと思いますが一応コードを載せておきます。
すべて開くすべて閉じる
  1
  2
  3
  4
  5
  6
  7
  8
  9
-
!
-
|
|
|
|
|
!
// 値が見つからなければ null を返す。
int? FindPositive(IEnumerable<int> values)
{
    return (
        from int? i in values
        where i >= 0
        select i)
        .FirstOrDefault();
}
すべて開くすべて閉じる
  1
  2
  3
  4
  5
  6
  7
  8
  9
-
!
-
|
|
|
|
|
!
// 値が見つからなければ null を返す。
int? FindPositive(IEnumerable<int> values)
{
    return
        values
            .Cast<int?>()
            .Where(i => i >= 0)
            .FirstOrDefault();
}
Category: [プログラミング][C#] - 2013-03-17 02:45:23