- はじめに:「FirstOrDefault安全版」は“意図しない 0 や null を潰すための仕掛け”
- まず整理:First と FirstOrDefault の素の挙動
- 何が危ないのか:0 や null が「普通の値」に紛れ込む
- パターン1:デフォルト値を明示的に指定できる FirstOrDefault 安全版
- パターン2:「なかったら例外」にする FirstOrDefault 安全版
- パターン3:null 許容と組み合わせた「型レベル安全版」
- 実務での設計の考え方:「0 件は普通か? 異常か?」を先に決める
- まとめ:「FirstOrDefault安全版」は“0 件という事実を曖昧にしないためのラッパー”
はじめに:「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 ユーティリティ”を自分で設計できるようになります。
