【LINQ】FirstOrDefaultでリストの最初の要素を取得する

C#

こんにちは。働くC#プログラマーのさんさめです。

LINQの中でも使用率が高いFirstOrDefaultメソッドの紹介です。

いやそれくらい、
説明されなくても分かるよ!

…という声が聞こえてきそうですが、
あまり知られてなさそうな省コードな書き方があるので、
ぜひ軽い気持ちで目を通してみてください。

スポンサーリンク

FirstOrDefaultの概要とユースケース

Enumerable.FirstOrDefault
配列やコレクションから最初の1要素を取得する
拡張メソッドです。

要素が1つもないときは既定値(default(T))が返ってきます。
intだったら0ですし、stringやクラスなど参照型だったらnullですね。

こんな風に使います。

static void Main(string[] args)
{
    // 最初のコマンドライン引数を取得
    var firstCommand = args.FirstOrDefault();
    // そして取得できたかどうかで処理を分岐…など
    if (!string.IsNullOrEmpty(firstCommand)) { /* 省略 */ }
    else { /* 省略 */ }

    // 以下のような書き方でも取得できるが、
    // 配列に要素がないと例外になってしまう。
    // var firstCommand = args[0];
}

以下のようなユースケースがあります。

  • 要素があるかどうかわからないが、
    もしあったら、それを使用して別の処理を行う
    • 上記のコマンドラインの例など
  • 条件に合致する要素を1つだけ取得
    • あるModel要素を参照して作られたViewModelを得たい…など

Firstは使っちゃダメ

いきなり余談ですが、Firstメソッドは罠APIなので使わないようにしましょう。

何が罠かというと、要素が見つからない場合にいきなり例外を投げます。
しかも、例外メッセージで「見つかりませんでした」
というようなことを言われてしまうので、
全然本質的な原因を調べることができません。

詳しくは、以下の記事をご覧ください。
【LINQ】Firstは使ってはいけない!FirstOrDefaultを使おう

Whereの直後に使ってないですか?もっと楽に書けますよ

見ていただいた通りFirstOrDefault自体は単純なメソッドです。
ですが、単純すぎるが故に便利さを知らないコードを見かけることがあります。

よく見かけるのが次のようなコードです。

static void Main(string[] args)
{
    var intEnumerable = Enumerable.Range(0, 10);

    var firstOddNum = intEnumerable
        .Where(x => x % 2 == 1) // まずWhereで絞ってから…
        .FirstOrDefault(); // FirstOrDefaultで最初の要素を取得
    Console.WriteLine(firstOddNum);
}

Whereで絞りこみをしてから、
FirstOrDefaultで取得する、というコードです。

これ、実はコードとしては冗長です。
以下のように書き換えられます。

static void Main(string[] args)
{
    var intEnumerable = Enumerable.Range(0, 10);

    var firstOddNum = intEnumerable
        .FirstOrDefault(x => x % 2 == 1); // 実は直接条件を渡せる
    Console.WriteLine(firstOddNum);
}

FirstOrDefaultは要素の条件をFuncで渡すことができます。
Whereで一旦絞る必要がないということですね。

人のコードなんて読みたくないものです…。
読む立場になった時に負担を減らすためにも、
書かなくていいコードは書かないようにしましょう。

趣味で書いてるだけだし、
他人のコードを読むことなんてないけど…

いえいえ、
過去の自分が書いたコードなんて実質他人のコードですよ。
たとえ自分しか使わないコードであったとしても、
省コードにしておく癖をつけることは必ず未来に役立ちます。

FirstOrDefaultは線形探索なのでコストは気を付けるべき

ちなみに、LINQ to Objectの実装は賢いので、
前者の書き方をしたからと言って、
「Whereで絞る処理がシーケンス全体にかかってしまって処理が激重…」
みたいなことにはならないのでご安心ください。

以下のコードは同じ出力結果になります。
(WhereやFirstOrDefaultに渡す条件の処理の中で、
コンソール出力するコードです)

static void Main(string[] args)
{
    var intEnumerable = Enumerable.Range(0, 10);

    bool predicate(int x)
    {
        Console.WriteLine(x);
        return x % 2 == 1;
    }

    Console.WriteLine("Whereの後にFirstOrDefault");
    var firstOddNum = intEnumerable
        .Where(predicate)
        .FirstOrDefault();
    Console.WriteLine(firstOddNum);

    Console.WriteLine("\n条件渡しFirstOrDefault");
    firstOddNum = intEnumerable
        .FirstOrDefault(predicate);
    Console.WriteLine(firstOddNum);
}

実行結果は、以下の通りです。

どちらの書き方でも、
列挙の要素すべては走査していないことが分かります。
(もし全部走査してたら0~9まで出力されてから1が出力されるはず)

FirstOrDefaultは該当する要素が見つかったら、
そこですぐに打ち切ってくれる特徴があるということですね。

逆に言うとやってることはただの線形探索なので、
条件に該当する要素が末尾の方にある場合ばっかりな時は
毎回全要素なめてしまう可能性があるということです。

「該当する要素が後ろに集中しそう…」
とアタリがついている場合は、
LastOrDefaultに置き換えるだけでも意外と効くかもしれません。

まとめ

今回はFirstOrDefaultの紹介を行いました。

  • リストや配列の先頭の要素を取得する関数
  • 条件を渡すことでWhereを省略できることがある
  • 条件を渡す場合は不当に重くなっていないか意識する

LINQは気軽に使えますが、
その分実際にどんな処理が走っているかを
気にしづらいので、
意図せず重い処理になることがあります。

そのあたりの話も追々していきたいですね。

最後までお読みいただきありがとうございました。

関連記事です。

Firstを使うのだけは絶対にやめましょう。
FirstOrDefault一択です。

コメント

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