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 | 判定コストを下げやすい |
| 重複を排除したい | Distinct | Name の重複か Id の重複かを意識 |
| Id ごとに Name 一覧が欲しい | GroupBy + ToDictionary | 後段の参照が楽になる |
| idList の順に並べたい | ToLookup or Join | 「外側の順序」が結果に出る |
LINQ は短く書ける反面、データの前提(重複、順序、比較ルール、データ量)が曖昧だと意図と結果がズレやすいです。今回のパターンをベースに、要件に合わせて Distinct、HashSet、GroupBy、ToLookup を組み合わせると、読みやすさと性能の両方を取りやすくなります。

コメント