C# LINQでIDリストに含まれるNameを抽出する方法|Where/Contains/SelectとHashSet高速化

C# LINQで「IDリストに一致する要素だけ取り出し、Name一覧を作る」処理は、画面表示や権限チェック、検索条件の反映などで頻出します。短く書ける反面、重複・速度・順序・DB連携でつまずきがちです。実務で困らない書き方を整理します。

目次

前提:やりたいことを具体化する

今回のゴールは次のとおりです。

  • Id と Name を持つクラスのリスト(例:List<MyClass>)がある
  • ID 文字列のリスト(例:List<string>)が別にある
  • classList の中から、Id が idList に含まれる要素だけを対象に、対応する Name を抽出したい

典型的なモデル例は次のようなものです。

public class MyClass
{
    public string Id { get; set; }
    public string Name { get; set; }
}

入力データのイメージも置いておきます(動作確認のときにそのまま使えます)。

var classList = new List<MyClass>
{
    new MyClass { Id = "A01", Name = "Alice" },
    new MyClass { Id = "A02", Name = "Bob"   },
    new MyClass { Id = "A02", Name = "Bobby" }, // 同じ Id が複数ある例
    new MyClass { Id = "A03", Name = "Carol" },
};

var idList = new List { "A02", "A03" };

C# LINQの基本形:Where + Contains + Select で Name を抽出する

最もシンプルで読みやすい解決策は、Where(絞り込み)→ Select(射影)です。

// Id が idList に含まれるものだけ残して、Name を取り出す
List<string> nameList =
    classList
        .Where(p => idList.Contains(p.Id))
        .Select(p => p.Name)
        .ToList();

この1本で目的は達成できます。メソッドの役割を整理すると次のとおりです。

メソッド役割今回の意味
Where条件に合う要素だけ残すId が idList に含まれる要素だけに絞る
Containsコレクション内に値があるか判定この Id は対象 ID か?を判定
Select必要な値に変換(射影)MyClass から Name だけを取り出す
ToList遅延実行を確定し List にする最終結果を List<string> にする

重複をどう扱うか:Distinct の入れどころ

実務でよくあるのが「同じ Id のデータが複数ある」ケースです。この場合、上の基本形は Name が複数返ります。

たとえば先ほどの例なら、"A02" に対して "Bob" と "Bobby" の両方が返る可能性があります。重複排除したい場合は Distinct() を挟みます。

var nameList = classList
    .Where(p => idList.Contains(p.Id))
    .Select(p => p.Name)
    .Distinct()
    .ToList();

注意点は、Distinct が排除するのは「Name の文字列としての重複」であることです。Id の重複を潰したい(Idごとに1件だけ欲しい)場合は、次の章の「辞書化」パターンが向きます。

速度が気になるなら:idList を HashSet にして Contains を高速化

idList.Contains(p.Id) は、idList が List<string> のままだと、内部的には先頭から順に探す形になりがちです。idList が大きい(数千〜数万以上)場合、ここがボトルネックになりやすくなります。

そこで、判定用の集合として HashSet<string> を作ると、平均的には高速化が期待できます。

var idSet = new HashSet<string>(idList);

var nameList = classList
.Where(p => idSet.Contains(p.Id))
.Select(p => p.Name)
.ToList();

比較のイメージ(ざっくり)は次のとおりです。

判定に使う型Contains のイメージ向いている状況
List<string>先頭から順に探すID が少ない/一回きりの処理/読みやすさ優先
HashSet<string>ハッシュで探すID が多い/何度も判定する/パフォーマンス重視

同じ idList を何度も使うなら「HashSet を使い回す」

画面のフィルタ処理やバッチで、同じ idList を複数回使う場合は、毎回 new HashSet するより 最初に1回だけ作って使い回すほうが効率的です。LINQ の前後で何度も判定が走る構成になっていないか、ついでに見直すと効果が出やすいです。

大文字小文字を区別しない判定が必要なとき

API から来る ID が a01 と A01 で揺れる、ユーザー入力を相手にしている、などの理由で「大文字小文字を区別しない」要件になることがあります。その場合は HashSet に比較方法を渡します。

var idSet = new HashSet<string>(idList, StringComparer.OrdinalIgnoreCase);

var nameList = classList
.Where(p => p.Id != null && idSet.Contains(p.Id))
.Select(p => p.Name)
.ToList();

比較のルールを明示しておくと、環境やデータの揺れで「なぜ一致しないのか」を追う時間が減ります。

「Id ごとに 1件だけ欲しい」なら辞書化してから選ぶ

「同じ Id が複数あるのは分かっている。だけど画面には 1件しか出さない」「後勝ち(最後のデータを優先)にしたい」など、Id ごとに 1件が欲しい場面は多いです。

その場合、先に Id をキーに辞書を作ってしまうと意図が明確になります。

先勝ち(最初のデータを採用)

var idSet = new HashSet<string>(idList);

var dict = classList
.Where(p => p.Id != null && idSet.Contains(p.Id))
.GroupBy(p => p.Id)
.ToDictionary(g => g.Key, g => g.First().Name);

var nameList = dict.Values.ToList();

後勝ち(最後のデータを採用)

var idSet = new HashSet<string>(idList);

var dict = classList
.Where(p => p.Id != null && idSet.Contains(p.Id))
.GroupBy(p => p.Id)
.ToDictionary(g => g.Key, g => g.Last().Name);

var nameList = dict.Values.ToList();

「先勝ち/後勝ち」をコード上で見える形にすると、仕様変更のときも安全です(First と Last を入れ替えるだけで済みます)。

「IDごとに Name 一覧が欲しい」なら GroupBy で辞書化する

質問の派生で多いのが、「Id をキーにして Name の一覧を取りたい」という要望です。権限ID→権限名一覧、カテゴリID→表示名一覧などに使えます。

var idSet = new HashSet<string>(idList);

var dict = classList
.Where(p => p.Id != null && idSet.Contains(p.Id))
.GroupBy(p => p.Id)
.ToDictionary(
g => g.Key,
g => g.Select(x => x.Name).ToList()
);

この dict は次のように使えます。

if (dict.TryGetValue("A02", out var names))
{
    // names は List<string>
}

「辞書に無いキーを引くと例外になるのが怖い」なら TryGetValue を基本にすると堅いです。

出力の順序をコントロールしたい:idList の順番をそのまま使う

基本形は classList の順番で結果が並びます。しかし、実務では次のような要件が出がちです。

  • ユーザーが選んだ ID(idList)の順に表示したい
  • マスタの並びではなく、入力順で並べたい
  • ID が存在しない場合も「空で返す」「不明として返す」などをしたい

この場合は、idList を主(外側)として回すのが分かりやすいです。おすすめは ToLookup を使うパターンです。

idList の順に Name を列挙する(複数件にも対応)

var idSet = new HashSet<string>(idList);

var lookup = classList
.Where(p => p.Id != null && idSet.Contains(p.Id))
.ToLookup(p => p.Id, p => p.Name);

// idList の順に、対応する Name を全部つなげる
var nameList = idList
.SelectMany(id => lookup[id])
.ToList();

lookup[id] は「その id に紐づく Name の列挙」を返します。該当が 0件でも空の列挙になるので、例外になりにくいのが利点です。

idList の順に 1件だけ取りたい(該当が無ければ null)

var idSet = new HashSet<string>(idList);

var lookup = classList
.Where(p => p.Id != null && idSet.Contains(p.Id))
.ToLookup(p => p.Id, p => p.Name);

var nameList = idList
.Select(id => lookup[id].FirstOrDefault())
.ToList();

表示用にするなら、null を空文字に置き換えるなどもよく行います。

var nameList = idList
    .Select(id => lookup[id].FirstOrDefault() ?? "")
    .ToList();

Join で書く選択肢:読みやすさと順序を両立できる

もう少し SQL 的に「結合」として表現したいなら、Join も使えます。idList を外側にすることで、idList の順番を保ちやすいのもポイントです。

var nameList = idList
    .Join(
        classList,
        id => id,
        p => p.Id,
        (id, p) => p.Name
    )
    .ToList();

重複を避けたいなら Distinct() を足す、Id ごとにまとめたいなら GroupBy を組み合わせる、といった拡張もしやすいです。

入力データが汚れているときの実務テクニック

「ロジックは合っているのに一致しない」原因の多くは、実はデータの揺れです。特に ID は 余計な空白 や 全角半角、大文字小文字で躓きがちです。

トリムしてから比較する

var idSet = new HashSet<string>(
    idList.Where(x => !string.IsNullOrWhiteSpace(x))
          .Select(x => x.Trim())
);

var nameList = classList
.Where(p => !string.IsNullOrWhiteSpace(p.Id) && idSet.Contains(p.Id.Trim()))
.Select(p => p.Name)
.ToList();

「入力側で正規化してから処理する」のがコツです。比較するたびに Trim() するより、先に揃えておく方が意図が明確になります。

Name 側も null・空を除外したい

var idSet = new HashSet<string>(idList);

var nameList = classList
.Where(p => p.Id != null && idSet.Contains(p.Id))
.Select(p => p.Name)
.Where(name => !string.IsNullOrWhiteSpace(name))
.Distinct()
.ToList();

表示やCSV出力など、後工程の扱いやすさが上がります。

IQueryable(Entity Framework など)での注意点

ここまでの例は「メモリ上の List」を想定しています。一方で、classList が IQueryable(Entity Framework のクエリなど)になっている場合、同じ LINQ でも内部では SQL に変換されます。

  • Where(p => idList.Contains(p.Id)) は、多くの場合 SQL の IN (...) 相当になります
  • ID 件数が極端に多いと、SQL のパラメータ数上限やクエリ肥大化で失速することがあります
  • HashSet にしても「SQL への変換」という観点では効きにくいことがあります(最終的に IN 句になるため)

DB 側で巨大な ID リストを扱う必要がある場合は、状況に応じて次のような代替案を検討します。

状況考え方例
ID が少ない素直に Contains(IN)でOK検索条件の選択肢が数十程度
ID が多い(数千以上)DB に渡す方法を再設計一時テーブル/テーブル値パラメータ/バルク投入
どうしてもアプリ側で処理したい必要な列だけ取得してからメモリで絞るId と Name だけ Select して ToList し、その後に HashSet で判定

「どこでフィルタするのが最適か」はデータ量と要件で変わるため、クエリの実行計画や実測も合わせて判断するのが安全です。

よくあるつまずきとチェックリスト

症状原因の候補対処
コンパイルで LINQ メソッドが見つからないusing System.Linq; が無いファイル先頭に using System.Linq; を追加
一致するはずなのに 0 件になる空白、大小文字、全角半角、null が混ざっているTrim / StringComparer / nullチェックで正規化
処理が遅いidList が大きく List の Contains を何度も呼んでいるHashSet 化、または Join / Lookup の利用を検討
順序が期待と違う外側にしているコレクションの順序がそのまま出力されるidList を外側に回す(ToLookup / Join)
Id と型が違って比較できないId が int、idList が string など型を揃える(変換は片側でまとめて行う)

実務でのおすすめパターンまとめ

最後に、用途別に「まずはこれ」をまとめます。迷ったらこのどれかで大抵解決できます。

やりたいことおすすめポイント
ID に一致する Name を全部取りたいWhere + Contains + Select短くて読みやすい
ID が大きくて速くしたいHashSet 化して Contains判定コストを下げやすい
重複を排除したいDistinctName の重複か Id の重複かを意識
Id ごとに Name 一覧が欲しいGroupBy + ToDictionary後段の参照が楽になる
idList の順に並べたいToLookup or Join「外側の順序」が結果に出る

LINQ は短く書ける反面、データの前提(重複、順序、比較ルール、データ量)が曖昧だと意図と結果がズレやすいです。今回のパターンをベースに、要件に合わせて Distinct、HashSet、GroupBy、ToLookup を組み合わせると、読みやすさと性能の両方を取りやすくなります。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次