C# LINQ Select の3パターン — 単純射影 / 匿名型整形 / インデックス付きの使い分け

みなさんこんにちは!ヒロポンです!

集計画面を作ってると「このテーブルから ID と名前だけ欲しい」「画面に出す形に整えて渡したい」、こういう場面ってほぼ毎日来ますよね??

で、そのたびに for で回して詰め替えて……ってやってると、地味に時間が溶ける。俺も昔これを愚直に書いてて、レビューで「それ Select 一発でいけるよ」。顔から火が出た。

今回は C# LINQ Select の使い方を、業務SEが現場で一番使う3パターンに絞る。単純射影・匿名型へ整形・インデックス付き。全部コピペで動くコードで出すんで、急いでる人はコードだけ持って帰ってOKです。

💡 DataTable を GroupBy で分割したり Where で絞り込む 話は別記事 C# DataTable を LINQ でフィルタ・GroupBy・分割する3パターン にまとめてます。今回は Select で「列を取り出す・整形する」 ことだけに絞った話です。

目次

結論: LINQ Select は用途で3つ使い分ける

先に結論。Select「何を、どんな形で取り出したいか」で3パターンを使い分けるだけです。迷ったらこの表を見て選べばOK。

LINQ Select 3パターンの用途別早見表(取り出す列数 / 戻り値の型 / 主な使い所)

Select は、元のデータを1件ずつ受け取って「欲しい形」に作り直すメソッド。SQL でいう SELECT 句 とほぼ同じ役割です。元のコレクションの件数は変えず、返す値の形だけを変える。それだけ。

なぜ Select でつまずくのか

こんな単純なメソッドでなんでつまずくのか。理由は2つ。

1つは、Selectその場では実行されないこと。「遅延評価」って呼ばれるやつで、foreachToList() を呼ぶまで中身が走りません。知らないと「あれ、ログが出ない?」ってなる。

もう1つは、射影の途中で null を踏むやつ。e.Department.Name みたいにプロパティを辿ると、途中が null だと NullReferenceException で落ちる。SQL の感覚で書いてると、ここでやられます。

この2つは後半のハマりポイントでちゃんと潰します。まずは動くコードから。

最短対処: コピペで動く3つの Select

まずサンプルデータ。社員リストを使います。Department(部署)は参照型で、未所属だと null になりうる。業務でよくある形にしてます。

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public Department Department { get; set; } // 参照型・null になりうる
}

public class Department
{
    public string Name { get; set; }
}

パターン①: 単純射影(1列だけ・型付きで取り出す)

俺が一番使うのがこれ。「ID だけ」「名前だけ」の一覧が欲しい時に、for を回さず1行で取れます。

// List から名前だけ取り出す
var names = employees.Select(e => e.Name).ToList();

// DataTable から型付きで1列取り出す(業務SE頻出)
var ids = dt.AsEnumerable()
            .Select(r => r.Field<int>("Id"))
            .ToList();

Field<int>("Id") で型付きに取れるんで、(int)row["Id"] みたいなキャストはもう要りません。いい感じに短く書けます。

パターン②: 匿名型へ整形(複数列をまとめて画面/CSV用に)

複数の列を「画面に出す形」「CSV に書き出す形」へまとめたい時。匿名型(new { ... })に詰めます。

var view = employees.Select(e => new
{
    e.Id,
    名前 = e.Name,
    年齢 = e.Age
}).ToList();

プロパティ名は日本語でもいけます(名前 = e.Name)。グリッドの列名や CSV ヘッダーにそのまま使えるんで、整形がいい感じに楽になる。

DataTable からならこう。

var rows = dt.AsEnumerable().Select(r => new
{
    Id = r.Field<int>("Id"),
    Name = r.Field<string>("Name"),
    Dept = r.Field<string>("Dept")
}).ToList();

パターン③: インデックス付き Select(行番号・連番を振る)

意外と知られてないのがこれ。Select には 要素と一緒に 0 始まりのインデックスを受け取れるオーバーロードがあります。

var numbered = employees.Select((e, i) => new
{
    No = i + 1, // 1 始まりにしたいので +1
    名前 = e.Name
}).ToList();

(e, i)i が行番号。ん? これ for でカウンタ回さんでええやん、ってなりますよね。CSV の「No.」列とか画面の行番号が、これでいい感じにスッキリ片付く。

ハマりポイント: 知らないと一晩飛ぶやつ

コピペで動くのは確認した。ここからは、知らずに本番へ流すと夜中に呼び出されるやつを2つ。

ハマり①: Select は foreach するまで実行されない(遅延評価)

俺はこれで、仕込んだログが出力されなくて30分くらい無駄にしました。先に1個だけコピペで再現してみます。

class Program
{
    static void Main()
    {
        var names = new[] { "佐藤", "鈴木", "高橋" };

        var query = names.Select(n =>
        {
            Console.WriteLine("射影中: " + n);
            return n + " さん";
        });

        Console.WriteLine("--- foreach 前 ---");
        foreach (var x in query) { }
        Console.WriteLine("--- foreach 後 ---");
    }
}

実行するとこうなります。

--- foreach 前 ---
射影中: 佐藤
射影中: 鈴木
射影中: 高橋
--- foreach 後 ---

LINQ Select 遅延評価の実行結果(foreach 前は射影が走らず、foreach 時に佐藤・鈴木・高橋が射影される順序)

Select を書いた行では 射影中: が1つも出てない。foreach で初めて走ってるのが分かりますよね??「ログを仕込んだのに出ない」時は、たいてい「まだ評価されてない」が犯人です。ToList() で即時に確定させると安全。

ハマり②: 射影の途中で null を踏んで NullReferenceException

これはテスト中にヒヤッとしたやつ。参照型プロパティを辿る射影は、途中の null で落ちます。

class Dept { public string Name { get; set; } }
class Emp { public string Name { get; set; } public Dept Dept { get; set; } }

class Program
{
    static void Main()
    {
        var emps = new[]
        {
            new Emp { Name = "佐藤", Dept = new Dept { Name = "経理" } },
            new Emp { Name = "鈴木", Dept = null } // 部署 未設定
        };

        // Dept が null の行で落ちる
        var ng = emps.Select(e => e.Dept.Name).ToList(); // NullReferenceException
        Console.WriteLine(string.Join(", ", ng));
    }
}

エラー再現:

null 射影で NullReferenceException が発生する再現(Dept=null の行で例外)

2件目の「鈴木」で e.Dept.Name を辿った瞬間に NullReferenceException。対処は null 条件演算子(?.)と null 合体演算子(??)で逃がすだけ。

var ok = emps.Select(e => e.Dept?.Name ?? "(未所属)").ToList();
// → "経理", "(未所属)"

開発機のテストデータは全部きれいに埋まってる。本番の「部署 未設定」データで初めて踏む、これが業務系のあるあるパターンなんですよね。射影でプロパティを辿る時は、null が来る前提で ?. を癖にしておく。これだけで事故が減ります。

現場メモ: 一覧作成で時間を溶かさないために

流通系の基幹システムを保守してた頃の話。夜間バッチの出力 CSV を「画面でも確認したい」って要望が来たことがあって。数万件の取引履歴を、ID・取引日・金額の3列だけ匿名型に整形して、画面のグリッドに流すやつを Select 一発でやりました。

最初は愚直に DataRow を1件ずつ手で詰め替えてたんですよ。コードが縦に長くなるし、列が1つ増えるたびに直す箇所が3か所くらいある。Select(r => new { ... }) に置き換えたら、詰め替えループが丸ごと消えて、列追加も1行で済む。

ポイントは、画面に出す直前で ToList() して確定させること。遅延評価のまま画面のバインドに渡すと、スクロールのたびに再評価が走って「なんか重い?」になります。整形した結果は早めに確定。これだけで体感の重さが変わる。

まとめ

LINQ Select の3パターン、整理するとこう。

  • ① 単純射影 — 1列だけ欲しい時。Select(e => e.Name)
  • ② 匿名型へ整形 — 複数列を画面/CSV用にまとめたい時。Select(e => new { ... })
  • ③ インデックス付き — 連番・行番号を振りたい時。Select((e, i) => ...)

ハマりは「遅延評価」と「null 射影」の2つ。ToList() で確定、?. で null 回避。この2つを押さえとけば、一覧作成で詰まることはほぼ無くなります!!

よくある質問

Q1. Select と Where の違いは何ですか?

A. Where行を絞り込む(件数が減る)、Select列を作り直す(件数は変わらない)です。SQL でいう WHERE句 と SELECT句 の関係と同じ。両方使う時は Where(...).Select(...) の順で繋ぎます。

Q2. 匿名型で返した結果を、メソッドの戻り値にできますか?

A. 匿名型はそのメソッドの中でしか型名を書けないので、return で外に渡すのは基本できません。外に渡したいなら、ちゃんとしたクラス(DTO)か record を定義して Select(e => new 社員ViewDto { ... }) にするのが定石です。

Q3. Select((e, i) => …) の i は SQL の ROW_NUMBER() と同じですか?

A. 似てますが別物です。iメモリ上のコレクションの並び順で 0 始まりに振られるだけ。DB 側で並べ替えた順番が欲しいなら、SQL で ORDER BY してから取得するか、ROW_NUMBER() を使ってください。

Q4. ToList() と ToArray()、どっちを使えばいい?

A. 後から件数が変わる・追加削除するなら ToList()、件数が固定で読むだけなら ToArray() がわずかに軽いです。業務だと迷ったら ToList() でOK。困ることはほぼ無いです。

Q5. Select の中で DB アクセスなど重い処理を書いていい?

A. やめたほうがいいです。遅延評価のせいで、foreach や画面バインドのたびに何回も走る危険がある。重い処理は Select の外で済ませるか、ToList() で1回だけに確定させてからにしてください。


ここまでで LINQ Select の使い分けは押さえられたはず。

ただ、こういう「知ってれば3秒、知らないと30分」の小技って、現場で一個ずつ拾っていくしかないんですよね。で、この地味な積み重ねを続けてる業務SEほど、なぜか自分の市場価値を低く見積もりがち。「俺なんてただの保守要員だから」って。その辺の自己評価の話を、下の関連記事の最後に置いときます。技術の合間の箸休めにどうぞ。

関連記事

以上!

同じところで詰まってる人いたら、どんどんシェア待ってるぜ!!


執筆者

バイブス父さん — 業務 SE 7 年 (正社員 2 / フリーランス 5)。 現職は SEO 直轄部の AI アドバイザー兼 PL、 副業で中小 SIer の CTO。 SES 複数社・フリーランスエージェント複数経由の経験ベースで「業務 SE 視点」 の技術 + キャリア記事を書いています。

🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る


この記事が気に入ったら
いいねしてね!

どんどんシェア待ってるぜ!!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次