C#のDistinctで重複削除する方法|LINQの基本から自作クラス・複数条件まで解説

はじめに

C#で重複を取り除きたいときは、LINQ の Distinct が基本です。Distinct はシーケンスから重複のない要素を返し、既定の等値比較子を使って比較します。結果は 順序保証のない IEnumerable<T> で返り、元のコレクション自体は変更されません。

一方で、Distinct は「その型にとって等しい」と判断できるかどうかが重要です。値型や文字列は期待通り動きやすいですが、自作クラスでは比較ルールを定義しないと、同じ内容でも別物として扱われることがあります。

1. C#のDistinctとは?LINQで重複削除できる基本メソッド

1-1. Distinctでできること:コレクション内の重複要素を取り除く

Distinct は、List<T> や配列などのシーケンスに含まれる重複要素を取り除くための LINQ メソッドです。重複判定には既定の等値比較が使われるため、同じ値とみなされた要素だけがまとめられます。

1-2. Distinctを使うために必要なusing System.Linq

Distinct は LINQ の拡張メソッドなので、通常は using System.Linq; が必要です。これがないとメソッドが見つからないことがあります。

1-3. Distinctの戻り値はIEnumerable<T>になる

Distinct の戻り値は IEnumerable<T> です。つまり、その場で新しい List<T> が返るわけではなく、列挙して初めて結果を取り出せます。

1-4. Distinctは元のListや配列を直接変更しない

Distinct は元のコレクションを書き換えません。重複削除後の結果を使いたい場合は、ToList()ToArray() で別のコレクションに変換します。

2. C#のDistinctの基本的な使い方

2-1. List<int>の重複を削除するサンプル

C#
using System;
using System.Collections.Generic;
using System.Linq;

var numbers = new List<int> { 1, 2, 2, 3, 3, 3, 4 };

var distinctNumbers = numbers.Distinct();

foreach (var n in distinctNumbers)
{
Console.WriteLine(n);
}

int のような値型では、Distinct だけでシンプルに重複削除できます。

2-2. List<string>の重複を削除するサンプル

C#
using System;
using System.Collections.Generic;
using System.Linq;

var words = new List<string> { "apple", "orange", "apple", "banana" };

var distinctWords = words.Distinct();

Console.WriteLine(string.Join(", ", distinctWords));

文字列も Distinct の対象にできます。ただし、比較は既定の等値比較に従うため、大文字小文字の扱いは別途注意が必要です。

2-3. 配列にDistinctを使う方法

C#
using System;
using System.Linq;

var values = new[] { 10, 20, 20, 30, 30, 40 };

var result = values.Distinct();

Console.WriteLine(string.Join(", ", result));

配列でも Distinct はそのまま使えます。LINQ は IEnumerable<T> に対して動くため、配列も対象になります。

2-4. Distinctした結果をListや配列に戻す方法

C#
using System.Collections.Generic;
using System.Linq;

var list = new List<int> { 1, 1, 2, 3, 3 };

var asList = list.Distinct().ToList();
var asArray = list.Distinct().ToArray();

必要に応じて ToList()ToArray() を使うと、扱いやすい形式に戻せます。

2-5. foreachやstring.Joinで結果を確認する方法

Distinct の結果は IEnumerable<T> なので、foreach で確認するか、string.Join で一括表示するとわかりやすいです。遅延実行なので、実際に列挙したタイミングで評価されます。

3. Distinctの動作を正しく理解する

3-1. Distinctは「等しい」と判定された要素だけを重複として扱う

Distinct は、単純に見た目が同じかどうかではなく、等値比較の結果で重複を判断します。比較の基準が変われば、重複の判定結果も変わります。

3-2. 値型・文字列・自作クラスで比較方法が異なる

値型や文字列は既定の比較で扱いやすい一方、自作クラスでは Equals や比較子を定義しないと期待通りの重複削除にならないことがあります。IEqualityComparer<T> はコレクション向けに独自の等価比較を定義するための仕組みです。

3-3. Distinct後の順序はどうなるのか

Distinct の結果は 順序保証のないシーケンス として扱われます。表示順に依存したい場合は、Distinct の後に OrderBy を組み合わせるか、別の方法を選ぶのが安全です。

3-4. nullを含むコレクションでDistinctを使った場合

Distinctnull を含むシーケンスにも使えます。比較は既定の等値比較に従うため、null は 1 件としてまとめられます。

3-5. 大文字小文字を区別して重複削除される点に注意

文字列の比較は、既定では文字列型の等値比較に従います。大文字小文字を無視したい場合は、StringComparer.OrdinalIgnoreCase のような比較子を渡します。IEqualityComparer<string> を使えば、比較ルールを明示できます。

C#
using System;
using System.Collections.Generic;
using System.Linq;

var words = new List<string> { "Apple", "apple", "APPLE" };

var result = words.Distinct(StringComparer.OrdinalIgnoreCase);

Console.WriteLine(string.Join(", ", result));

4. 自作クラス・オブジェクトのListでDistinctを使う方法

4-1. 自作クラスでDistinctが期待通りに動かない理由

自作クラスは、何をもって「同じ」とするかを自分で定義しないと、Distinct が参照の違いを基準に扱ってしまうことがあります。つまり、値が同じでも別インスタンスなら別物と見なされる場合があります。

4-2. 参照型では同じ値でも別インスタンスなら別物として扱われる

参照型は、内容ではなく参照の同一性が問題になることがあります。重複削除を「プロパティの値」で行いたいなら、比較方法を明示する必要があります。

4-3. EqualsとGetHashCodeをオーバーライドする方法

Distinct のような等値比較を使う処理では、EqualsGetHashCode を整合させるのが基本です。等しいとみなす条件を Equals に書き、同じ条件なら同じハッシュ値になるよう GetHashCode を合わせます。

C#
public class Person
{
public int Id { get; set; }
public string Name { get; set; } = "";

public override bool Equals(object? obj)
{
if (obj is not Person other) return false;
return Id == other.Id && Name == other.Name;
}

public override int GetHashCode()
{
return HashCode.Combine(Id, Name);
}
}

4-4. IEquatable<T>を実装して比較ルールを定義する方法

IEquatable<T> は、型固有の等価性判定を実装するためのインターフェースです。自作クラスで比較ルールを明確にしたいときに有効です。

C#
public class Person : IEquatable<Person>
{
public int Id { get; set; }
public string Name { get; set; } = "";

public bool Equals(Person? other)
{
if (other is null) return false;
return Id == other.Id && Name == other.Name;
}

public override bool Equals(object? obj) => Equals(obj as Person);

public override int GetHashCode() => HashCode.Combine(Id, Name);
}

4-5. IEqualityComparer<T>を作成してDistinctに渡す方法

IEqualityComparer<T> は、コレクションに対する独自の等価比較を提供するための仕組みです。既存クラスを変更できない場合や、比較ルールを使い分けたい場合に向いています。

C#
using System.Collections.Generic;

public class PersonComparer : IEqualityComparer<Person>
{
public bool Equals(Person? x, Person? y)
{
if (ReferenceEquals(x, y)) return true;
if (x is null || y is null) return false;
return x.Id == y.Id && x.Name == y.Name;
}

public int GetHashCode(Person obj)
{
return HashCode.Combine(obj.Id, obj.Name);
}
}

4-6. 自作クラスでDistinctを使うときの実装例

C#
using System;
using System.Collections.Generic;
using System.Linq;

var people = new List<Person>
{
new Person { Id = 1, Name = "A" },
new Person { Id = 1, Name = "A" },
new Person { Id = 2, Name = "B" }
};

var distinctPeople = people.Distinct(new PersonComparer()).ToList();

foreach (var p in distinctPeople)
{
Console.WriteLine($"{p.Id} {p.Name}");
}

自作クラスでは、「何を重複とみなすか」を比較子で明示すると、Distinct が安定して使えます。

5. 特定のプロパティや複数条件で重複削除する方法

5-1. 1つのプロパティだけを基準に重複削除する方法

Distinct 自体は対象オブジェクト全体を比較するため、1つのプロパティだけで重複削除したいなら、そのプロパティを比較基準にした別の方法を使います。DistinctBy はキー選択関数で重複を判定できるメソッドです。

5-2. SelectとDistinctで特定プロパティの一覧を取得する方法

C#
var ids = people.Select(p => p.Id).Distinct().ToList();

特定プロパティの一覧だけ欲しいなら、Select でキーを取り出してから Distinct を使うのが簡単です。

5-3. GroupByを使って特定プロパティごとに1件だけ取得する方法

C#
var uniquePeople = people
.GroupBy(p => p.Id)
.Select(g => g.First())
.ToList();

GroupBy はキーごとにグループ化するため、同じキーの中から 1 件だけ残したいときに便利です。GroupBy はキー選択関数に従って要素をグループ化します。

5-4. 匿名型を使って複数プロパティを条件にする方法

C#
var uniquePeople = people
.Select(p => new { p.Id, p.Name, Person = p })
.Distinct()
.Select(x => x.Person)
.ToList();

匿名型は、複数プロパティをまとめて比較したいときに使いやすい方法です。匿名型の値が同じなら、Distinct でも同じグループとして扱えます。

5-5. IdとNameなど複数条件でDistinctするサンプル

C#
var distinctPeople = people
.GroupBy(p => new { p.Id, p.Name })
.Select(g => g.First())
.ToList();

IdName の両方を条件にしたいなら、複合キーを作るのがわかりやすい書き方です。

5-6. 同じキーのうち最初の要素を残す場合の書き方

C#
var distinctPeople = people
.GroupBy(p => p.Id)
.Select(g => g.First())
.ToList();

「同じキーの最初の 1 件だけ欲しい」という要件なら、GroupBy(...).Select(g => g.First()) が読みやすく、安全です。Distinct は順序保証がないため、順序を意識するなら OrderByGroupBy で明示するのが無難です。

6. DistinctByを使って簡単にプロパティ指定で重複削除する方法

6-1. DistinctByとは何か

DistinctBy は、要素そのものではなく、指定したキーで重複を判定する LINQ メソッドです。Distinct よりも「このプロパティで重複削除したい」という意図が伝わりやすいのが強みです。

6-2. DistinctByが使える.NETのバージョン

DistinctBy は、現在の .NET の API ドキュメントに掲載されているメソッドで、.NET 6 系の参照情報でも確認できます。DistinctBy の公式ドキュメントは、キー選択関数に基づいて個別要素を返すメソッドとして案内されています。

6-3. DistinctByで1つのプロパティを基準にするサンプル

C#
using System.Linq;

var uniqueById = people.DistinctBy(p => p.Id).ToList();

1つのキーで重複を消したい場合は、DistinctBy が最もすっきり書けます。

6-4. DistinctByで複数プロパティを基準にするサンプル

C#
var uniqueByIdAndName = people
.DistinctBy(p => new { p.Id, p.Name })
.ToList();

複数条件を使いたい場合は、匿名型をキーとして渡すと実装しやすくなります。DistinctBy はキー比較に指定した比較子も使えます。

6-5. DistinctとDistinctByの使い分け

Distinct は「要素そのものの等価性」で比較するのに向いており、DistinctBy は「特定のキーで重複を消したい」場面に向いています。要件が明確なら DistinctBy、既存の等価性をそのまま使うなら Distinct が使いやすいです。

7. Distinctでよくあるエラー・つまずきポイント

7-1. Distinctが使えない・認識されない場合の原因

using System.Linq; が抜けていると、Distinct が認識されません。拡張メソッドなので、名前空間の読み込みを確認してください。

7-2. 自作クラスで重複が消えない原因

自作クラスで期待通り重複が消えない場合は、EqualsGetHashCode の実装、または IEqualityComparer<T> の指定漏れが原因になりやすいです。等価性の定義がないと、同じ内容でも別物として扱われます。

7-3. ToListしないと結果が反映されないと感じる原因

Distinct は遅延実行なので、すぐに元データが変わるわけではありません。結果を確定させたいなら、ToList() などで列挙してください。

7-4. 大文字小文字を無視して重複削除したい場合

文字列の比較を無視したいなら、StringComparer.OrdinalIgnoreCase を渡します。比較子を渡すことで、Distinct の重複判定を用途に合わせて変えられます。

7-5. 比較条件を変えても結果が変わらない場合の確認点

比較条件を変えたのに結果が変わらないときは、比較子を本当に Distinct に渡しているか、あるいは DistinctBy のキーが意図した値になっているかを確認します。IEqualityComparer<T> やキーセレクターが期待通り動いているかが重要です。

8. Distinctと他の重複削除方法の違い

8-1. DistinctとGroupByの違い

Distinct は重複のない要素を返すことに特化しています。GroupBy はグループ化が主目的で、同じキーの中から First() を選ぶことで重複削除のように使えます。

8-2. DistinctとHashSetの違い

Distinct は LINQ の流れの中で使いやすく、HashSet<T> は集合そのものを扱う用途に向いています。どちらも等値比較に基づきますが、使う場面が少し異なります。

8-3. DistinctとUnionの違い

Union は 2 つのシーケンスをまとめて重複を取り除くメソッドです。1 つのコレクション内だけを処理するなら Distinct、複数シーケンスを合わせるなら Union が自然です。

8-4. RemoveAllやループ処理で重複削除する方法との違い

手書きのループでも重複削除はできますが、Distinct のほうが意図が伝わりやすく、比較ルールも明確にできます。可読性を優先するなら LINQ、細かな制御が必要なら手続き的な処理、という考え方がしやすいです。

8-5. 可読性・処理速度・用途別の選び方

一般には、単純な重複削除なら Distinct、プロパティ指定なら DistinctBy、複雑な選別なら GroupBy が使いやすいです。比較の定義が必要な自作クラスでは、IEqualityComparer<T>Equals / GetHashCode の整備が先です。

9. Distinctを使うときの実務的な注意点

9-1. 大量データでDistinctを使う場合のパフォーマンス

大量データでは、比較ロジックやハッシュコードの質が結果に影響します。IEqualityComparer<T>GetHashCode の実装が適切だと、重複判定が安定しやすくなります。

9-2. GetHashCodeの実装ミスによる不具合

EqualsGetHashCode の整合が取れていないと、重複判定が不安定になります。等しいオブジェクトは同じハッシュ値になるように実装するのが基本です。

9-3. LINQ to SQLやEntity Frameworkで使う場合の注意

DistinctByIQueryable<T> 向けにも用意されていますが、クエリプロバイダーによっては意図どおりに変換されない場合があります。データベース問い合わせに流すか、メモリ上で列挙してから使うかは確認が必要です。

9-4. DB側で重複削除するべきケース

件数が多い場合や、DB の索引を使ったほうが効率的な場合は、SQL 側で重複排除する設計も有効です。アプリ側で Distinct を使うか、DB 側で集約するかは、処理場所と転送量で判断します。

9-5. 重複削除前にOrderByが必要になるケース

結果順を安定させたいときは、Distinct の前後で OrderBy を組み合わせます。Distinct の結果は順序保証がないため、表示順や優先順位が重要なら並べ替えを明示してください。

10. C#のDistinctに関するよくある質問

10-1. Distinctで元のListから重複要素は削除される?

削除されません。Distinct は元の List を変更せず、重複のない新しい列挙結果を返します。

10-2. Distinctで最初の要素と最後の要素のどちらが残る?

Distinct の結果は順序保証がないため、残る要素の順番に依存した設計は避けるべきです。特定の要素を残したいなら、GroupBy(...).Select(g => g.First()) のように明示します。

10-3. Distinctで大文字小文字を無視できる?

できます。StringComparer.OrdinalIgnoreCase などの比較子を渡します。

10-4. Distinctで複数条件を指定できる?

Distinct そのものに複数条件を直接渡すのではなく、複合キーを作るか、DistinctBy で匿名型をキーにします。GroupBy を使う方法も定番です。

10-5. DistinctByが使えない場合はどうすればいい?

GroupBy(...).Select(g => g.First()) で代用できます。特定のキーで 1 件だけ残したい処理は、この書き方でも十分実現できます。

まとめ

C# の Distinct は、LINQ で重複削除を行うときの基本メソッドです。using System.Linq; を追加し、Distinct() を呼び出すだけで使えますが、比較は等値判定に依存するため、自作クラスや文字列の大小文字には注意が必要です。Distinct は元のコレクションを変更せず、結果は順序保証のない IEnumerable<T> として返されます。

特定のプロパティで重複を消したいなら DistinctBy、複数条件や最初の 1 件を明示したいなら GroupBy が便利です。実務では、比較ルールの定義、順序の扱い、DB 側で処理するかどうかまで含めて選ぶと、Distinct をより安全に使えます。