C# Tips | コレクション・LINQ:FirstOrDefault安全版

C# C#
スポンサーリンク

はじめに:「FirstOrDefault安全版」は“意図しない 0 や null を潰すための仕掛け”

FirstOrDefault は LINQ の超定番メソッドですが、その「便利さ」の裏側で、初心者がよくハマる落とし穴があります。

要素が 1 件もなかったとき、値型なら 0、参照型なら null が返る
「0 や null が返る前提」でコードを書いていないと、後続でバグや例外になる
「本当は 0 件はおかしい」のに、FirstOrDefault がそれを隠してしまう

そこで出てくる発想が「FirstOrDefault安全版」です。
つまり、「0 件のときに“どう振る舞うか”を、もっと明示的にコントロールできるラッパー(拡張メソッド)」を用意してしまおう、という考え方です。

ここでは、まず First / FirstOrDefault の素の挙動を整理し、そのうえで

デフォルト値を強制的に指定できる版
「なかったら例外」にできる版
null 許容と組み合わせた“型レベルでの安全版”

を、例題付きで解説していきます。


まず整理:First と FirstOrDefault の素の挙動

First は「必ずある前提」、FirstOrDefault は「ないかもしれない前提」

基本形から確認します。

using System;
using System.Collections.Generic;
using System.Linq;

var numbers = new List<int> { 10, 20, 30 };

int a = numbers.First();         // 10
int b = numbers.FirstOrDefault(); // 10
C#

どちらも 1 件以上あれば、先頭要素を返します。
違いが出るのは「0 件」のときです。

var empty = new List<int>();

int x = empty.First();          // InvalidOperationException(例外)
int y = empty.FirstOrDefault(); // 0(int の既定値)
C#

参照型の場合は、FirstOrDefault は null を返します。

var names = new List<string>();

string? s = names.FirstOrDefault(); // null
C#

ここでの重要ポイントは、「FirstOrDefault は“0 件でも例外にしない代わりに、既定値を返す”」ということです。
この「既定値」が、業務的にはかなり曲者になります。


何が危ないのか:0 や null が「普通の値」に紛れ込む

「0 は本当に“見つからなかった”なのか?」

例えば、こんなコードを考えます。

var prices = new List<int> { 100, 200, 300 };

int price = prices
    .Where(p => p > 500)
    .FirstOrDefault();

Console.WriteLine(price); // 0
C#

p > 500 を満たす要素がないので、FirstOrDefault は 0 を返します。
でも、業務的に「価格 0 円」というのは、普通にあり得る値かもしれません。

「0 だから“見つからなかった”と判断する」のは、かなり危険です。
「本当に 0 円の商品」なのか、「見つからなかった結果の 0」なのか、区別がつかないからです。

null も同じ罠を持っている

参照型でも同じです。

var users = new List<User>();

User? u = users.FirstOrDefault(); // null
C#

null が返るのはいいとして、その後で

Console.WriteLine(u.Name); // NullReferenceException
C#

とやってしまうのは、初心者あるあるです。

ここでの重要ポイントは、「FirstOrDefault は“0 件でもとりあえず何か返す”ので、呼び出し側がちゃんと意識していないとバグの温床になる」ということです。
だからこそ、「安全版」を自分で用意しておく価値が出てきます。


パターン1:デフォルト値を明示的に指定できる FirstOrDefault 安全版

「なかったらこの値にしてほしい」をコードに刻む

一番シンプルな「安全版」は、「0 件のときに返す値を、呼び出し側で明示的に指定できる」ラッパーです。
拡張メソッドとして書いてみます。

using System;
using System.Collections.Generic;
using System.Linq;

public static class FirstOrDefaultSafeExtensions
{
    public static T FirstOrDefaultSafe<T>(
        this IEnumerable<T> source,
        T defaultValue)
    {
        if (source is null) throw new ArgumentNullException(nameof(source));

        return source.Any()
            ? source.First()
            : defaultValue;
    }

    public static T FirstOrDefaultSafe<T>(
        this IEnumerable<T> source,
        Func<T, bool> predicate,
        T defaultValue)
    {
        if (source is null) throw new ArgumentNullException(nameof(source));
        if (predicate is null) throw new ArgumentNullException(nameof(predicate));

        var filtered = source.Where(predicate);
        return filtered.Any()
            ? filtered.First()
            : defaultValue;
    }
}
C#

使い方はこうです。

var prices = new List<int> { 100, 200, 300 };

int price = prices
    .Where(p => p > 500)
    .FirstOrDefaultSafe(defaultValue: -1);

Console.WriteLine(price); // -1
C#

ここでの重要ポイントは、「“見つからなかったときの値”を、呼び出し側が意識的に決めている」ことです。
-1 なら、「これは“見つからなかった”印だな」と一目で分かります。

参照型でも同じです。

var users = new List<User>();

User unknown = new User { Id = "UNKNOWN", Name = "不明" };

User u = users.FirstOrDefaultSafe(unknown);
C#

「見つからなかったら“UNKNOWN ユーザー”にする」という業務ルールを、コードに刻み込めます。


パターン2:「なかったら例外」にする FirstOrDefault 安全版

「0 件は業務的におかしい」なら、あえて落とす

逆に、「0 件は絶対におかしい。あったらバグとして気づきたい」という場面もあります。
その場合は、「FirstOrDefault ではなく、業務用の“FirstOrThrow”を作る」という発想もアリです。

public static class FirstOrThrowExtensions
{
    public static T FirstOrThrow<T>(
        this IEnumerable<T> source,
        string? message = null)
    {
        if (source is null) throw new ArgumentNullException(nameof(source));

        if (source.Any())
        {
            return source.First();
        }

        throw new InvalidOperationException(
            message ?? "シーケンスに要素が存在しません。");
    }

    public static T FirstOrThrow<T>(
        this IEnumerable<T> source,
        Func<T, bool> predicate,
        string? message = null)
    {
        if (source is null) throw new ArgumentNullException(nameof(source));
        if (predicate is null) throw new ArgumentNullException(nameof(predicate));

        var filtered = source.Where(predicate);

        if (filtered.Any())
        {
            return filtered.First();
        }

        throw new InvalidOperationException(
            message ?? "条件を満たす要素が存在しません。");
    }
}
C#

使い方はこうです。

var users = new List<User>();

var u = users.FirstOrThrow("ユーザーが1件も取得できませんでした。");
C#

ここでの重要ポイントは、「“0 件は異常”という業務ルールを、例外という形で明示している」ことです。
素の First でも例外は出ますが、メッセージを業務寄りにできるのがメリットです。


パターン3:null 許容と組み合わせた「型レベル安全版」

参照型の場合:戻り値を T? にして、null チェックを強制する

C# 8 以降の「null 許容参照型」を使っている場合、
FirstOrDefault の戻り値は T? として扱われます。

List<User> users = GetUsers();

User? u = users.FirstOrDefault();
C#

このとき、「null チェックをしないと警告が出る」ようにしておくと、
「うっかり null のままプロパティにアクセスして例外」という事故を減らせます。

安全版を作るなら、あえて「null を返す」ことを前提にして、
呼び出し側に null チェックを強制するのも一つの設計です。

public static class FirstOrNullExtensions
{
    public static T? FirstOrNull<T>(
        this IEnumerable<T> source)
        where T : class
    {
        if (source is null) throw new ArgumentNullException(nameof(source));

        return source.FirstOrDefault();
    }

    public static T? FirstOrNull<T>(
        this IEnumerable<T> source,
        Func<T, bool> predicate)
        where T : class
    {
        if (source is null) throw new ArgumentNullException(nameof(source));
        if (predicate is null) throw new ArgumentNullException(nameof(predicate));

        return source.FirstOrDefault(predicate);
    }
}
C#

使い方はこうです。

User? u = users.FirstOrNull(x => x.Id == "U001");

if (u is null)
{
    // 見つからなかったときの処理
}
else
{
    Console.WriteLine(u.Name);
}
C#

ここでの重要ポイントは、「“null かもしれない”ことを型で表現し、コンパイラにチェックさせる」という発想です。
「安全版」と言いつつ、あえて null を返すことで、「null を無視させない」ようにしているわけです。


実務での設計の考え方:「0 件は普通か? 異常か?」を先に決める

まずは日本語でルールを言葉にする

FirstOrDefault安全版 をどう設計するかは、結局ここに行き着きます。

0 件なのは、業務的に普通にあり得るのか
0 件はおかしくて、あったらすぐに気づきたいのか
0 件のときは、どんな値(あるいは挙動)にしたいのか

例えば、こんな整理ができます。

0 件は普通にあり得る → null を返す or 明示的なデフォルト値を返す
0 件は異常 → 例外を投げる(FirstOrThrow 的なもの)

そして、「どのパターンをどこで使うか」をチームで揃えておくと、
コードを読む人が迷わなくなります。

「既定値に紛れ込ませない」ことが安全版の本質

FirstOrDefault の危険さは、「“見つからなかった”という情報が、0 や null の中に紛れ込んでしまう」ことにあります。

だからこそ、安全版では

業務的に意味のある“特別な値”を返す(-1、UNKNOWN など)
例外として表に出す
null として返し、型システムとコンパイラにチェックさせる

といった形で、「見つからなかった」という事実を隠さないようにするのが大事です。


まとめ:「FirstOrDefault安全版」は“0 件という事実を曖昧にしないためのラッパー”

FirstOrDefault 自体はとても便利ですが、そのまま乱用すると、

0 や null が「普通の値」に紛れ込む
「本当はおかしい 0 件」が、静かにスルーされる

という危険を常に抱えます。

だからこそ、業務ユーティリティとしての「FirstOrDefault安全版」は、

デフォルト値を明示的に指定できる版(FirstOrDefaultSafe)
0 件なら例外にする版(FirstOrThrow)
null 許容と組み合わせて、型レベルでチェックさせる版(FirstOrNull)

といった形で、「0 件のときどうするか」をコードに刻み込むための道具になります。

大事なのは、

この場面では 0 件は普通か、異常か
見つからなかったとき、何を返す(あるいはどう振る舞う)のが業務的に正しいか

を、まず日本語で言えるようにしてから、それを拡張メソッドの形に落とし込むことです。

ここまで腹落ちしていれば、
「とりあえず FirstOrDefault しておいて、後でなんとかする」スタイルから卒業して、
“0 件という現実をちゃんと扱う、安全な LINQ ユーティリティ”を自分で設計できるようになります。

タイトルとURLをコピーしました