C# Tips | コレクション・LINQ:マージ

C# C#
スポンサーリンク

はじめに:「マージ」は“バラバラの情報を一つの流れにまとめる技”

業務システムでは、こういう状況がよく出てきます。

「今年分のデータと昨年分のデータを一緒に扱いたい」
「マスタと明細をくっつけて、見やすい形にしたい」
「複数のリストを一つにまとめて処理したい」

こういう「別々のコレクションを一つにまとめる」ことを、ここではざっくり「マージ」と呼びます。
C# / LINQ では、目的に応じていくつかのマージ方法があります。

単純に「くっつける」
重複を除いて「統合する」
キーを使って「結合する」

この3つを押さえると、業務で必要なマージのほとんどは書けるようになります。


単純マージ:Concat と AddRange で「ただくっつける」

List 同士をそのままつなげるイメージ

まずは一番シンプルな「マージ」です。
「A のリスト」と「B のリスト」を、順番に並べて一つにします。

using System;
using System.Collections.Generic;
using System.Linq;

var list1 = new List<int> { 1, 2, 3 };
var list2 = new List<int> { 4, 5 };

var merged = list1.Concat(list2).ToList();

Console.WriteLine(string.Join(", ", merged)); // 1, 2, 3, 4, 5
C#

ここでの重要ポイントは、「Concat は“単に後ろにくっつけるだけ”で、重複もそのまま残る」ということです。
「今年分のログ+昨年分のログ」のように、「全部まとめて時系列で処理したい」ときに向いています。

List のメソッド AddRange との違い

同じことを AddRange でも書けます。

var merged = new List<int>(list1);
merged.AddRange(list2);
C#

Concat は「新しい列を返す LINQ メソッド」、
AddRange は「既存の List に要素を追加するメソッド」です。

「元のリストを変えたくない」なら Concat
「この List にどんどん足していきたい」なら AddRange、という使い分けになります。


統合マージ:Union で「重複を除いて一つにする」

重複を除いたマージ

「A と B の両方に同じ値があるときは、1回だけでいい」というマージもよくあります。
このときに使うのが Union です。

var list1 = new List<int> { 1, 2, 3 };
var list2 = new List<int> { 3, 4, 5 };

var merged = list1.Union(list2).ToList();

Console.WriteLine(string.Join(", ", merged)); // 1, 2, 3, 4, 5
C#

Concat なら 1,2,3,3,4,5 になりますが、
Union は「集合」として扱うので、3 が一つにまとまります。

ここでの重要ポイントは、「Union は“重複を除いたマージ”」だということです。
「顧客コード一覧を統合したい」「権限一覧を統合したい」など、
「同じものが複数あっても意味がない」場面で使います。

オブジェクトの Union で気をつけること

オブジェクトのリストに対して Union を使うときは、
「何をもって同じとみなすか」を決める必要があります。

public class Customer
{
    public string Code { get; set; } = "";
    public string Name { get; set; } = "";
}
C#

この場合、単純に Union すると「参照が同じかどうか」で判定されてしまうので、
IEqualityComparer<T> を実装するか、Select でコードだけにしてから Union する、などの工夫が必要になります。

初心者のうちは、「値型や string の Union はそのまま使える」「オブジェクトの Union は一段難しい」と覚えておくと安全です。


キーマージ:Join で「マスタと明細をくっつける」

マスタと明細を結合するイメージ

業務で一番「マージっぽい」のは、
「コードだけ持っている明細」と「コード→名称を持っているマスタ」をくっつけるパターンです。

商品マスタと売上明細を例にします。

public class Product
{
    public string Code { get; set; } = "";
    public string Name { get; set; } = "";
}

public class Sale
{
    public string ProductCode { get; set; } = "";
    public int Quantity { get; set; }
}
C#
var products = new List<Product>
{
    new Product { Code = "A001", Name = "りんご" },
    new Product { Code = "B002", Name = "バナナ" },
};

var sales = new List<Sale>
{
    new Sale { ProductCode = "A001", Quantity = 3 },
    new Sale { ProductCode = "B002", Quantity = 2 },
};
C#

「売上明細に商品名をくっつけたい」とき、Join を使います。

using System.Linq;

var merged = sales
    .Join(
        products,
        s => s.ProductCode, // 明細側のキー
        p => p.Code,        // マスタ側のキー
        (s, p) => new       // 結合後の形
        {
            ProductCode = s.ProductCode,
            ProductName = p.Name,
            Quantity    = s.Quantity
        })
    .ToList();
C#

ここでの重要ポイントは、「Join は“キーを指定して、2つのコレクションを結合する”」ということです。

第1引数:結合したい相手のコレクション
第2引数:左側(呼び出し元)のキーを取り出す式
第3引数:右側(引数)のキーを取り出す式
第4引数:結合結果をどう形にするか

という並びになっています。

Join の注意点:「片方にしかないデータ」は落ちる

Join は「内部結合(inner join)」なので、
両方にキーが存在するものだけが結果に出てきます。

「売上にはあるけど、マスタにない商品コード」
「マスタにはあるけど、売上には出てこない商品」

こういうものは、Join の結果には含まれません。

業務的に「マスタにないコードは異常」とみなすならそれでよく、
「マスタにないコードも一応見たい」なら、GroupJoin や「左外部結合」の書き方が必要になります。


左外部マージ:GroupJoin+SelectMany で「片方だけのデータも残す」

「売上にはあるがマスタにない」も見たい場合

「売上明細は全部見たい。商品名が取れないものは“名称不明”にしたい」
こういうときは、GroupJoin を使って「左外部結合」を書きます。

var merged = sales
    .GroupJoin(
        products,
        s => s.ProductCode,
        p => p.Code,
        (s, ps) => new { Sale = s, Products = ps })
    .SelectMany(
        x => x.Products.DefaultIfEmpty(),
        (x, p) => new
        {
            ProductCode = x.Sale.ProductCode,
            ProductName = p?.Name ?? "名称不明",
            Quantity    = x.Sale.Quantity
        })
    .ToList();
C#

少し難しく見えますが、やっていることはこうです。

売上1件に対して、「対応する商品マスタの集合」をくっつける(GroupJoin)
商品が見つからない場合は「空集合」になるので、DefaultIfEmpty() で null 1件にする
売上×商品(または売上×null)に展開して、最終形に投影する

ここでの重要ポイントは、「GroupJoin + SelectMany + DefaultIfEmpty で“左外部結合”を表現できる」ということです。
最初は丸暗記で構いません。「売上は全部残したいときの定型句」として覚えておくと、実務でかなり役立ちます。


実務で意識してほしい「マージ設計」の考え方

まずは日本語で「どうマージしたいか」を言葉にする

コードを書く前に、必ず言葉で整理してみてください。

単に A と B をつなげたいだけなのか
A と B の重複を除いて統合したいのか
A のコードと B のコードを使って、情報をくっつけたいのか
A は全部残したいのか、両方にあるものだけでいいのか

これが決まれば、選ぶメソッドもほぼ決まります。

ただつなげる → Concat / AddRange
重複を除いて統合 → Union
キーで結合(両方にあるものだけ) → Join
キーで結合(片方は全部残したい) → GroupJoin + SelectMany

という対応です。

「片方にしかないデータ」をどう扱うかを最初に決める

マージで一番事故りやすいのが、「片方にしかないデータ」の扱いです。

マスタにないコードは、業務的に異常として落とすのか
それとも、「名称不明」として残すのか
売上に出てこないマスタは、今回の集計では無視していいのか

ここを決めずに Join だけで書くと、「たまたまマスタにないコードがあったときに、気づかないまま集計から消える」という怖い状態になります。


まとめ:「マージユーティリティ」は“バラバラな世界を一つにするための道具箱”

コレクション・LINQ の「マージ」の本質は、

「別々に存在しているデータを、
 業務の意図に沿った一つの流れに組み立て直す」

ことです。

押さえておきたいのは、

単純マージ:Concat(順番にくっつける)
統合マージ:Union(重複を除いてまとめる)
キーマージ:Join(両方にあるキーだけ結合)
左外部マージ:GroupJoin + SelectMany(片方は全部残す)

そして何より、

「片方にしかないデータをどう扱うか」を、
コードを書く前に日本語で決めておくこと。

ここまで腹落ちしていれば、
「とりあえず for で2つのリストを回して if で頑張る」スタイルから卒業して、
“業務の意図がそのまま読める LINQ ベースのマージ処理”を書けるようになります。

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