C# Tips | コレクション・LINQ:差集合

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

はじめに:「差集合」は“どっちにだけあるか?”をはっきりさせる技

業務でデータを扱っていると、こういう問いがよく出てきます。

「昨日はあったけど、今日はなくなったデータはどれ?」
「マスタにはあるけど、インポートファイルには入っていないコードは?」
「A システムにはあるが、B システムにはまだ登録されていないユーザーは?」

こういう「片方にだけ存在するもの」を取り出すのが、集合でいう「差集合」です。
C# / LINQ では Except を使うことで、差集合をシンプルに書けます。

ここから、初心者向けに

差集合のイメージ
Except の基本(値型・string)
オブジェクトでの差集合
実務での差集合の使いどころ

を、例題付きでかみ砕いて説明していきます。


差集合のイメージをざっくりつかむ

A − B とは「A にだけあるもの」

集合の世界では、A - B(A マイナス B)は

「A にはあるけど、B にはないもの全部」

という意味です。

例えば、

A = {1, 2, 3, 4}
B = {3, 4, 5}

なら、A − B = {1, 2} です。
3 と 4 は両方にあるので除外され、「A にだけある 1 と 2」だけが残ります。

LINQ の Except は、まさにこの「A − B」をやってくれるメソッドです。


基本:数値・文字列の差集合を Except で求める

int の差集合

まずは一番シンプルな例から。

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

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

var diff = a.Except(b).ToList();

Console.WriteLine(string.Join(", ", diff)); // 1, 2
C#

ここでの重要ポイントは、「a.Except(b) は“a にだけある要素”を返す」ということです。
b.Except(a) と書けば、逆に「b にだけある要素」になります。

var diff2 = b.Except(a).ToList(); // 5
C#

順番を変えると結果も変わる、というのが差集合の特徴です。

string の差集合(コードの差分など)

商品コードの差分を取るイメージで見てみます。

var masterCodes = new[] { "A001", "A002", "A003" };
var fileCodes   = new[] { "A002", "A003" };

var notInFile = masterCodes.Except(fileCodes).ToList();

Console.WriteLine(string.Join(", ", notInFile)); // A001
C#

「マスタにはあるが、ファイルにはないコード」を取り出しています。
業務でよくある「取り込み漏れチェック」そのものです。

ここでの重要ポイントは、「Except は“重複を考えない集合としての差”」だということです。
同じ値が何回出てきても、「あるかないか」だけを見ます。


オブジェクトの差集合:何をもって「同じ」とみなすか

そのまま Except すると「参照が同じかどうか」になる

クラスのリストに対して Except を使うときは注意が必要です。

public class User
{
    public string Id { get; set; } = "";
    public string Name { get; set; } = "";
}
C#
var list1 = new List<User>
{
    new User { Id = "U001", Name = "Sato" },
    new User { Id = "U002", Name = "Suzuki" },
};

var list2 = new List<User>
{
    new User { Id = "U002", Name = "Suzuki" },
};
C#

見た目は同じ U002 ですが、別インスタンスです。
このまま Except すると、両方に U002 がいても「別物」とみなされてしまいます。

var diff = list1.Except(list2).ToList(); // 実は U001 も U002 も残ってしまう
C#

ここでの重要ポイントは、「クラスの Except はデフォルトでは“参照が同じかどうか”で比較される」ということです。
業務的には「Id が同じなら同じユーザー」とみなしたいので、そのままでは使いづらいです。

プロジェクションしてから Except する

初心者向けに一番分かりやすいのは、「比較したいキーだけに変換してから差集合を取る」方法です。

var ids1 = list1.Select(x => x.Id);
var ids2 = list2.Select(x => x.Id);

var diffIds = ids1.Except(ids2).ToList(); // U001
C#

これで、「list1 にだけいるユーザーの Id 一覧」が取れます。
必要なら、そこから元のリストに戻すこともできます。

var onlyInList1 = list1
    .Where(u => diffIds.Contains(u.Id))
    .ToList();
C#

ここでの重要ポイントは、「差集合を取りたいときは、“何をもって同一とみなすか”を先に決める」ことです。
Id なのか、コードなのか、複数項目の組み合わせなのか――それを Select で取り出してから Except する、という流れです。


実務でよくある差集合のパターン

「マスタにあるが、ファイルにない」チェック

典型的な業務シナリオをもう少し具体的に書いてみます。

var masterCodes = new[] { "A001", "A002", "A003", "A004" };
var fileCodes   = new[] { "A002", "A004" };

var missingInFile = masterCodes.Except(fileCodes).ToList(); // A001, A003
C#

「マスタには登録されているのに、今回のインポートファイルには出てこなかったコード」を洗い出しています。

逆に、「ファイルにはあるが、マスタにないコード」もよくチェックします。

var unknownCodes = fileCodes.Except(masterCodes).ToList();
C#

ここでの重要ポイントは、「Except の向きを変えるだけで、“どっちにだけあるか”を切り替えられる」ことです。
A − B は「A にだけある」、B − A は「B にだけある」です。

「前回から今回で増えたもの・減ったもの」を見る

前回の状態と今回の状態を比べる差分チェックも、差集合の得意分野です。

var prev = new[] { "U001", "U002", "U003" };
var curr = new[] { "U002", "U003", "U004" };

var added   = curr.Except(prev).ToList(); // 新しく増えた: U004
var removed = prev.Except(curr).ToList(); // いなくなった: U001
C#

「増えたユーザー」「いなくなったユーザー」が一目で分かります。


差集合を使うときに意識してほしいこと

「順番」ではなく「集合」として考える

Except は「集合」として扱うメソッドなので、順番は基本的に意識しません。
「順番も大事」な場合は、差集合を取ったあとに OrderBy などで並べ替えるのが普通です。

var diff = a.Except(b)
            .OrderBy(x => x)
            .ToList();
C#

「順番で比較したい(1行目と1行目を比べたい)」という話になると、
それはもう差集合ではなく「レコード比較」の世界になるので、別の設計になります。

「何をもって同じとみなすか」を最初に決める

差集合で一番大事なのは、ここです。

Id が同じなら同じとみなすのか
コードが同じなら同じとみなすのか
コード+日付の組み合わせが同じなら同じとみなすのか

これが決まれば、Select でそのキーを取り出して Except するだけです。

var keys1 = list1.Select(x => new { x.Code, x.Date });
var keys2 = list2.Select(x => new { x.Code, x.Date });

var diffKeys = keys1.Except(keys2).ToList();
C#

匿名型は「全プロパティが同じなら同一」とみなされるので、
複数項目の組み合わせで差集合を取りたいときに便利です。


まとめ:「差集合ユーティリティ」は“ズレているところだけを浮かび上がらせるレンズ”

差集合の本質は、

「2つの世界のうち、“片方にだけ存在するもの”をあぶり出す」

ことです。

押さえておきたいポイントは、

a.Except(b) は「a にだけあるもの」
順番を変えると結果も変わる(b.Except(a) は「b にだけあるもの」)
オブジェクトの差集合では、「何をもって同じとみなすか」を決めてから Select する
マスタ vs ファイル、前回 vs 今回の差分チェックにとても相性がいい

ここまで腹落ちしていれば、
「for で2重ループを回して if で頑張る」差分チェックから卒業して、
“意図がそのまま読める LINQ ベースの差集合処理”を、落ち着いて書けるようになります。

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