C# LINQ Distinctの使い方完全ガイド|Listの重複削除から自作クラス・複数プロパティの比較まで

はじめに

C#でListや配列の重複を削除したいとき、LINQのDistinctは非常によく使われるメソッドです。

たとえば、次のようなケースで役立ちます。

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

var result = numbers.Distinct().ToList();

結果は次のようになります。

C#
// 1, 2, 3

一見すると簡単なDistinctですが、実務では次のような疑問がよく出てきます。

「自作クラスのListでDistinctしても重複が消えない」

「IDだけで重複削除したい」

「複数プロパティで比較したい」

「DistinctByとGroupByはどう使い分ける?」

この記事では、C# LINQのDistinctについて、Listの基本的な重複削除から、自作クラス、複数プロパティ、DistinctByGroupByとの使い分けまで、実務で使える形で解説します。

1. C# LINQ Distinctとは?重複削除の基本を理解する

1-1. Distinctでできること:重複しない要素だけを取得する

Distinctは、シーケンスの中から重複する要素を取り除き、重複しない要素だけを返すLINQメソッドです。

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

var distinctNumbers = numbers.Distinct();

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

出力結果は次のようになります。

C#
1
2
3
4

Microsoftの公式ドキュメントでも、Distinct<TSource>は重複値を含まないシーケンスを返し、既定の等値比較子を使って値を比較すると説明されています。Microsoft Learn

1-2. Distinctの戻り値はIEnumerable<T>である

Distinctの戻り値はList<T>ではなく、IEnumerable<T>です。

C#
IEnumerable<int> result = numbers.Distinct();

そのため、結果をListとして扱いたい場合は、ToList()を使います。

C#
List<int> result = numbers.Distinct().ToList();

この違いは重要です。Distinct()だけでは「重複削除された結果を表すLINQクエリ」が返るだけで、Listそのものに変換されるわけではありません。

1-3. Distinctは元のListを直接変更しない

Distinctは、元のListを直接変更しません。

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

var result = numbers.Distinct().ToList();

Console.WriteLine(string.Join(", ", numbers)); // 1, 1, 2, 3
Console.WriteLine(string.Join(", ", result)); // 1, 2, 3

元のnumbersには重複が残ったままです。

元のList自体を重複削除後の状態にしたい場合は、結果を再代入します。

C#
numbers = numbers.Distinct().ToList();

1-4. Distinctの比較にはEqualityComparer<T>.Defaultが使われる

Distinctは、要素が同じかどうかを判断するときにEqualityComparer<T>.Defaultを使います。

C#
var names = new List<string> { "Tanaka", "Tanaka", "Sato" };

var result = names.Distinct().ToList();

intstringのような基本的な型では、通常は期待どおりに重複削除できます。

一方、自作クラスでは「何をもって同じと判断するか」を明示しないと、期待どおりに重複削除できないことがあります。EqualityComparer<T>.Defaultは、対象の型がIEquatable<T>を実装していればそれを使う、という仕組みを持っています。Microsoft Learn

1-5. Distinctは遅延実行される

Distinctは遅延実行されます。

つまり、Distinct()を呼び出した瞬間に重複削除が完了するわけではありません。foreachで列挙したり、ToList()ToArray()を呼び出したりしたタイミングで実行されます。

C#
var numbers = new List<int> { 1, 1, 2 };

var query = numbers.Distinct();

numbers.Add(3);

foreach (var number in query)
{
Console.WriteLine(number);
}

出力結果は次のようになります。

C#
1
2
3

queryを作った後に追加した3も結果に含まれます。これは、実際の列挙がforeachのタイミングで行われるためです。

2. List・配列でDistinctを使う基本例

2-1. int型のListから重複を削除する

数値の重複削除は、Distinctのもっとも基本的な使い方です。

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

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

var result = numbers.Distinct().ToList();

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

出力結果です。

C#
1, 2, 3, 4, 5

intは値そのものを比較できるため、特別な設定をしなくても重複削除できます。

2-2. string型のListから重複を削除する

文字列のListでも同じように使えます。

C#
var names = new List<string>
{
"Alice",
"Bob",
"Alice",
"Charlie",
"Bob"
};

var result = names.Distinct().ToList();

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

出力結果です。

C#
Alice, Bob, Charlie

ただし、既定では大文字・小文字は区別されます。

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

var result = words.Distinct().ToList();

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

この場合、appleAppleAPPLEは別の文字列として扱われます。

2-3. 配列にDistinctを使う

DistinctはListだけでなく、配列にも使えます。

C#
int[] numbers = { 1, 1, 2, 3, 3 };

var result = numbers.Distinct();

foreach (var number in result)
{
Console.WriteLine(number);
}

出力結果です。

C#
1
2
3

配列に対してDistinctを使っても、戻り値は配列ではなくIEnumerable<T>です。配列として受け取りたい場合はToArray()を使います。

C#
int[] resultArray = numbers.Distinct().ToArray();

2-4. Distinct後にToListでListへ変換する

実務では、Distinct().ToList()の形で使うことが多いです。

C#
var uniqueIds = ids.Distinct().ToList();

ToList()を付けることで、重複削除後の結果をListとして扱えます。

C#
uniqueIds.Add(999);

Distinct()だけの場合はIEnumerable<T>なので、AddのようなList専用メソッドは使えません。

2-5. 大文字・小文字を区別せずに文字列を重複削除する

大文字・小文字を区別せずに重複削除したい場合は、StringComparer.OrdinalIgnoreCaseを渡します。

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

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

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

出力結果です。

C#
apple, banana

StringComparer.OrdinalIgnoreCaseを使うことで、appleAppleAPPLEを同じ値として扱えます。

3. Distinctの基本構文と実践コード

3-1. Distinct()の基本構文

Distinctの基本構文は次のとおりです。

C#
source.Distinct()

比較方法を指定する場合は、IEqualityComparer<T>を渡します。

C#
source.Distinct(comparer)

たとえば、文字列の大文字・小文字を無視する場合は次のようになります。

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

3-2. Distinct().ToList()の使い方

Listの重複削除では、次の形がよく使われます。

C#
var result = source.Distinct().ToList();

具体例です。

C#
var ids = new List<int> { 101, 102, 101, 103 };

var uniqueIds = ids.Distinct().ToList();

uniqueIdsには、重複を除いたIDだけが入ります。

C#
101, 102, 103

3-3. foreachで結果を確認する

Distinctの結果はforeachで確認できます。

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

foreach (var number in numbers.Distinct())
{
Console.WriteLine(number);
}

ToList()しなくても、列挙するだけなら問題ありません。

ただし、結果を何度も使う場合やListとして加工する場合は、先にToList()しておくと扱いやすくなります。

C#
var uniqueNumbers = numbers.Distinct().ToList();

foreach (var number in uniqueNumbers)
{
Console.WriteLine(number);
}

3-4. SelectやWhereと組み合わせる

Distinctは、SelectWhereと組み合わせて使えます。

C#
var users = new List<User>
{
new User { Id = 1, Name = "Alice", IsActive = true },
new User { Id = 2, Name = "Bob", IsActive = false },
new User { Id = 1, Name = "Alice", IsActive = true }
};

var activeUserIds = users
.Where(u => u.IsActive)
.Select(u => u.Id)
.Distinct()
.ToList();

このコードでは、アクティブなユーザーのIDだけを取り出し、重複を削除しています。

C#
public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
public bool IsActive { get; set; }
}

3-5. OrderByと組み合わせるときの注意点

DistinctOrderByを組み合わせる場合、順番に注意が必要です。

重複削除してから並び替える場合です。

C#
var result = numbers
.Distinct()
.OrderBy(x => x)
.ToList();

並び替えてから重複削除する場合です。

C#
var result = numbers
.OrderBy(x => x)
.Distinct()
.ToList();

単純な数値であれば結果が同じに見えることもありますが、オブジェクトを扱う場合は意味が変わります。

特に「どの要素を残したいか」が重要な場合は、OrderByGroupByFirstLastを組み合わせて明示的に書く方が安全です。

C#
var result = users
.OrderByDescending(u => u.UpdatedAt)
.GroupBy(u => u.Id)
.Select(g => g.First())
.ToList();

この例では、同じIDのユーザーのうち、更新日時が新しいものを残しています。

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

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

自作クラスでDistinctを使うと、値が同じに見えても重複削除されないことがあります。

C#
var users = new List<User>
{
new User { Id = 1, Name = "Alice" },
new User { Id = 1, Name = "Alice" }
};

var result = users.Distinct().ToList();

Console.WriteLine(result.Count); // 2

IdNameも同じなので1件になりそうですが、結果は2件です。

理由は、クラスが参照型だからです。既定では、それぞれ別のインスタンスとして扱われます。

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

次の2つのオブジェクトは、プロパティの値は同じですが、別々にnewされたインスタンスです。

C#
var user1 = new User { Id = 1, Name = "Alice" };
var user2 = new User { Id = 1, Name = "Alice" };

Console.WriteLine(user1 == user2); // False

自作クラスでDistinctを期待どおりに動かすには、「どのプロパティが同じなら同一とみなすのか」を定義する必要があります。

代表的な方法は次の4つです。

C#
// 1. EqualsとGetHashCodeをオーバーライドする
// 2. IEquatable<T>を実装する
// 3. IEqualityComparer<T>を作ってDistinctに渡す
// 4. record型を使う

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

クラス内でEqualsGetHashCodeをオーバーライドすると、Distinctがその比較ルールを使えるようになります。

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

public override bool Equals(object? obj)
{
if (obj is not User other)
{
return false;
}

return Id == other.Id && Name == other.Name;
}

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

使用例です。

C#
var users = new List<User>
{
new User { Id = 1, Name = "Alice" },
new User { Id = 1, Name = "Alice" },
new User { Id = 2, Name = "Bob" }
};

var result = users.Distinct().ToList();

Console.WriteLine(result.Count); // 2

Equalsで等価性を判断し、GetHashCodeでハッシュ値を返します。

この2つはセットで実装するのが基本です。

4-4. IEquatable<T>を実装する方法

型安全に比較ルールを定義したい場合は、IEquatable<T>を実装します。

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

public bool Equals(User? other)
{
if (other is null)
{
return false;
}

return Id == other.Id && Name == other.Name;
}

public override bool Equals(object? obj)
{
return Equals(obj as User);
}

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

IEquatable<T>を実装すると、型に特化した等価比較を定義できます。MicrosoftのC#ガイドでも、型固有のEqualsを提供する方法としてIEquatable<T>の実装が説明されています。Microsoft Learn

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

クラス本体を変更したくない場合や、比較ルールを場面によって変えたい場合は、IEqualityComparer<T>を作成します。

C#
public class UserIdComparer : IEqualityComparer<User>
{
public bool Equals(User? x, User? y)
{
if (ReferenceEquals(x, y))
{
return true;
}

if (x is null || y is null)
{
return false;
}

return x.Id == y.Id;
}

public int GetHashCode(User obj)
{
return obj.Id.GetHashCode();
}
}

使用例です。

C#
var users = new List<User>
{
new User { Id = 1, Name = "Alice" },
new User { Id = 1, Name = "Alicia" },
new User { Id = 2, Name = "Bob" }
};

var result = users
.Distinct(new UserIdComparer())
.ToList();

この場合、Nameが違っていてもIdが同じなら重複とみなされます。

4-6. record型を使って簡潔に重複判定する方法

C#のrecord型を使うと、値ベースの等価比較を簡潔に書けます。

C#
public record UserRecord(int Id, string Name);

使用例です。

C#
var users = new List<UserRecord>
{
new UserRecord(1, "Alice"),
new UserRecord(1, "Alice"),
new UserRecord(2, "Bob")
};

var result = users.Distinct().ToList();

Console.WriteLine(result.Count); // 2

recordは、プロパティ単位の値等価性が生成されるため、DTOや読み取り中心のデータを扱う場合に便利です。MicrosoftのC#ドキュメントでも、recordは値等価性を持つ型として説明されています。Microsoft Learn

5. 複数プロパティでDistinctする方法

5-1. IDとNameなど複数条件で重複判定する

実務では、1つのプロパティだけでなく、複数プロパティの組み合わせで重複判定したいことがあります。

たとえば、次のようなデータです。

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

IdNameが同じなら重複」としたい場合、いくつかの書き方があります。

5-2. 匿名型を使って複数プロパティを比較する

特定のプロパティの組み合わせだけを取得したい場合は、匿名型を使う方法が簡単です。

C#
var result = users
.Select(u => new { u.Id, u.Name })
.Distinct()
.ToList();

この場合、結果はUserではなく匿名型のListになります。

C#
foreach (var user in result)
{
Console.WriteLine($"{user.Id}: {user.Name}");
}

「重複しないキーの一覧がほしい」だけなら、この方法で十分です。

5-3. ValueTupleを使って複数プロパティを比較する

ValueTupleを使って複数プロパティを比較することもできます。

C#
var result = users
.Select(u => (u.Id, u.Name))
.Distinct()
.ToList();

結果はタプルのListです。

C#
foreach (var item in result)
{
Console.WriteLine($"{item.Id}: {item.Name}");
}

匿名型よりも軽く書けるため、一時的なキー比較では便利です。

5-4. IEqualityComparer<T>で複数プロパティを比較する

元のUserオブジェクトを残したまま、複数プロパティで重複判定したい場合は、IEqualityComparer<T>を使います。

C#
public class UserIdNameComparer : IEqualityComparer<User>
{
public bool Equals(User? x, User? 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(User obj)
{
return HashCode.Combine(obj.Id, obj.Name);
}
}

使用例です。

C#
var result = users
.Distinct(new UserIdNameComparer())
.ToList();

この方法なら、User型のまま結果を取得できます。

5-5. 複数プロパティ比較でGetHashCodeを書くときの注意点

複数プロパティで比較する場合、Equalsで使うプロパティとGetHashCodeで使うプロパティを一致させることが重要です。

悪い例です。

C#
public bool Equals(User? x, User? y)
{
return x?.Id == y?.Id && x?.Name == y?.Name;
}

public int GetHashCode(User obj)
{
return obj.Id.GetHashCode(); // Nameを使っていない
}

この例では、EqualsではIdNameを見ているのに、GetHashCodeではIdしか見ていません。

基本的には、次のようにHashCode.Combineを使うのがおすすめです。

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

比較対象にしたプロパティをすべて含めることで、意図した重複判定になりやすくなります。

6. 特定のプロパティだけで重複削除する方法

6-1. IDだけで重複削除する

「ユーザーIDだけで重複削除したい」というケースはよくあります。

.NET 6以降であれば、DistinctByを使うのが簡単です。

C#
var result = users
.DistinctBy(u => u.Id)
.ToList();

Idが同じユーザーは重複とみなされます。

C#
var users = new List<User>
{
new User { Id = 1, Name = "Alice" },
new User { Id = 1, Name = "Alicia" },
new User { Id = 2, Name = "Bob" }
};

var result = users.DistinctBy(u => u.Id).ToList();

この場合、Id = 1のユーザーは1件だけ残ります。

6-2. Nameだけで重複削除する

Nameだけで重複削除したい場合も同じです。

C#
var result = users
.DistinctBy(u => u.Name)
.ToList();

大文字・小文字を無視したい場合は、比較子を渡します。

C#
var result = users
.DistinctBy(u => u.Name, StringComparer.OrdinalIgnoreCase)
.ToList();

6-3. DistinctByを使う方法

DistinctByは、キーセレクターで指定した値をもとに重複削除するメソッドです。

C#
source.DistinctBy(x => x.Key)

Microsoftのドキュメントでも、DistinctByは指定したキーセレクター関数に従ってシーケンスから個別の要素を返すメソッドとして説明されています。Microsoft Learn

通常のDistinctは「要素全体」を比較します。

C#
users.Distinct()

一方、DistinctByは「指定したキー」を比較します。

C#
users.DistinctBy(u => u.Id)

自作クラスで「IDだけ」「コードだけ」「メールアドレスだけ」のように重複削除したい場合、DistinctByは非常に読みやすい方法です。

6-4. DistinctByが使えない環境でGroupByを使う方法

古い.NET環境などでDistinctByが使えない場合は、GroupByで代用できます。

C#
var result = users
.GroupBy(u => u.Id)
.Select(g => g.First())
.ToList();

GroupByは指定したキーで要素をグループ化するLINQメソッドです。Microsoft Learn

このコードでは、Idごとにグループ化し、各グループの最初の1件を取得しています。

C#
var result = users
.GroupBy(u => u.Id)
.Select(g => g.First())
.ToList();

DistinctBy(u => u.Id)に近い動きを実現できます。

6-5. 最初の1件ではなく最後の1件を残す方法

DistinctByGroupBy(...).Select(g => g.First())では、基本的に最初に出てきた要素を残す用途で使われます。

最後の1件を残したい場合は、GroupByLastを使います。

C#
var result = users
.GroupBy(u => u.Id)
.Select(g => g.Last())
.ToList();

また、更新日時が新しいものを残したい場合は、グループ内で並び替えます。

C#
var result = users
.GroupBy(u => u.Id)
.Select(g => g.OrderByDescending(u => u.UpdatedAt).First())
.ToList();

このように、「どの1件を残すか」が重要な場合は、GroupByを使うと意図を明確にできます。

7. Distinct・DistinctBy・GroupByの使い分け

7-1. 値そのものの重複削除ならDistinct

値そのものを比較して重複削除したい場合は、Distinctを使います。

C#
var result = numbers.Distinct().ToList();

次のようなケースに向いています。

C#
var ids = new List<int> { 1, 1, 2, 3 };
var uniqueIds = ids.Distinct().ToList();

var tags = new List<string> { "C#", "LINQ", "C#" };
var uniqueTags = tags.Distinct().ToList();

intstringDateTimeなど、値そのものの重複削除ならDistinctが最もシンプルです。

7-2. 1つのキーで重複削除するならDistinctBy

オブジェクトの特定プロパティだけで重複削除したい場合は、DistinctByが向いています。

C#
var result = users
.DistinctBy(u => u.Email)
.ToList();

次のようなケースです。

C#
// IDだけで重複削除
users.DistinctBy(u => u.Id);

// メールアドレスだけで重複削除
users.DistinctBy(u => u.Email);

// 商品コードだけで重複削除
products.DistinctBy(p => p.ProductCode);

可読性が高く、意図も明確です。

7-3. 集計やグループ処理も必要ならGroupBy

重複削除だけでなく、件数集計やグループ単位の処理も必要ならGroupByを使います。

C#
var result = orders
.GroupBy(o => o.CustomerId)
.Select(g => new
{
CustomerId = g.Key,
OrderCount = g.Count(),
TotalAmount = g.Sum(o => o.Amount)
})
.ToList();

重複削除だけならDistinctByで十分ですが、「グループごとに集計したい」「グループ内から条件に合う1件を選びたい」という場合はGroupByの方が適しています。

7-4. 複数条件なら匿名型・タプル・Comparerを使い分ける

複数条件で重複削除したい場合は、目的に応じて使い分けます。

キーだけ取得したい場合です。

C#
var result = users
.Select(u => new { u.Id, u.Name })
.Distinct()
.ToList();

タプルで簡潔に書きたい場合です。

C#
var result = users
.Select(u => (u.Id, u.Name))
.Distinct()
.ToList();

元のオブジェクトを残したい場合です。

C#
var result = users
.Distinct(new UserIdNameComparer())
.ToList();

また、DistinctByに匿名型を渡す方法も使えます。

C#
var result = users
.DistinctBy(u => new { u.Id, u.Name })
.ToList();

この書き方なら、元のUserオブジェクトを残したまま、IdNameの組み合わせで重複削除できます。

7-5. 可読性と保守性を重視した選び方

実務では、短く書けることよりも、後から読んだときに意図が伝わることが重要です。

おすすめの選び方は次のとおりです。

C#
// 値そのものの重複削除
Distinct()

// 1つのキーで重複削除
DistinctBy(x => x.Key)

// 複数キーで重複削除
DistinctBy(x => new { x.Key1, x.Key2 })

// 古い環境や集計も必要な場合
GroupBy(x => x.Key)

// 比較ルールを再利用したい場合
Distinct(new CustomComparer())

チーム開発では、複雑な比較ロジックをLINQの中に直接書きすぎるより、IEqualityComparer<T>やメソッドに切り出した方が保守しやすくなります。

8. Distinctでよくあるエラー・期待通りに動かない原因

8-1. using System.Linqがない

Distinctを使うには、通常System.Linq名前空間が必要です。

C#
using System.Linq;

これがないと、次のようなエラーが出ることがあります。

C#
'List<int>' does not contain a definition for 'Distinct'

最近のプロジェクトテンプレートでは暗黙的なusingにより省略できる場合もありますが、エラーが出たらまずusing System.Linq;を確認しましょう。

8-2. ToListしないとListとして扱えない

Distinctの戻り値はIEnumerable<T>です。

そのため、次のように書くと型が合わないことがあります。

C#
List<int> result = numbers.Distinct(); // エラー

正しくは次のように書きます。

C#
List<int> result = numbers.Distinct().ToList();

配列にしたい場合はToArray()です。

C#
int[] result = numbers.Distinct().ToArray();

8-3. 自作クラスで重複が削除されない

自作クラスでは、同じ値を持っていても別インスタンスなら別物として扱われることがあります。

C#
var users = new List<User>
{
new User { Id = 1, Name = "Alice" },
new User { Id = 1, Name = "Alice" }
};

var result = users.Distinct().ToList();

Console.WriteLine(result.Count); // 2

この場合は、次のいずれかで対応します。

C#
// EqualsとGetHashCodeを実装する
// IEquatable<T>を実装する
// IEqualityComparer<T>をDistinctに渡す
// record型を使う
// DistinctByでキーを指定する

特定のプロパティだけで重複削除したいなら、DistinctByが簡潔です。

C#
var result = users.DistinctBy(u => u.Id).ToList();

8-4. Equalsだけ実装してGetHashCodeを実装していない

Equalsだけを実装して、GetHashCodeを実装していないのは避けるべきです。

C#
public override bool Equals(object? obj)
{
return obj is User other && Id == other.Id;
}

このようにEqualsだけ実装しても、ハッシュベースのコレクションやDistinctで期待どおりに動かない原因になります。

正しくは、GetHashCodeも実装します。

C#
public override int GetHashCode()
{
return Id.GetHashCode();
}

複数プロパティを使う場合は、HashCode.Combineを使うと書きやすいです。

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

8-5. nullを含むListでの挙動

Distinctは、nullを含むListにも使えます。

C#
var names = new List<string?>
{
"Alice",
null,
"Bob",
null,
"Alice"
};

var result = names.Distinct().ToList();

foreach (var name in result)
{
Console.WriteLine(name ?? "(null)");
}

出力結果です。

C#
Alice
(null)
Bob

nullも1つの値として扱われ、複数のnullは1つにまとめられます。

ただし、自作のIEqualityComparer<T>を書く場合は、nullチェックを忘れないようにしましょう。

C#
public bool Equals(User? x, User? y)
{
if (ReferenceEquals(x, y))
{
return true;
}

if (x is null || y is null)
{
return false;
}

return x.Id == y.Id;
}

8-6. 順序が保証されると思い込んでいる

Distinctの結果の順序には注意が必要です。

LINQ to Objectsでは、実装上は元のシーケンスを前から列挙しながら重複を除くため、最初に出てきた値が残るように見えることが多いです。

しかし、公式ドキュメントではDistinctDistinctByの戻り値は「順序付けされていないシーケンス」と説明されています。Microsoft Learn+1

順序が重要な場合は、最後にOrderByなどで明示的に並び替えましょう。

C#
var result = numbers
.Distinct()
.OrderBy(x => x)
.ToList();

また、「最初の1件を残す」「最後の1件を残す」「更新日時が最新の1件を残す」などの要件がある場合は、GroupByで明示的に書く方が安全です。

9. Distinctのパフォーマンスと実務での注意点

9-1. Distinctは内部的にハッシュベースで重複判定する

Distinctは、要素がすでに登場したかどうかを判定しながら結果を返します。

そのため、比較にはEqualsGetHashCodeが重要です。

C#
public override bool Equals(object? obj)
{
return obj is User other && Id == other.Id;
}

public override int GetHashCode()
{
return Id.GetHashCode();
}

GetHashCodeの実装が不適切だと、パフォーマンスが悪くなったり、意図しない結果になったりする可能性があります。

9-2. 大量データで注意すべきメモリ使用量

Distinctは、重複判定のために「すでに登場した値」を保持する必要があります。

そのため、非常に大量のデータを扱う場合はメモリ使用量に注意が必要です。

C#
var result = veryLargeList
.Distinct()
.ToList();

このようなコードでは、元データに加えて、重複判定用の内部データと結果Listのメモリが必要になります。

大量データでは、次のような対策を検討します。

C#
// 必要な列だけに絞ってからDistinctする
var uniqueIds = users
.Select(u => u.Id)
.Distinct()
.ToList();

// 条件で絞ってからDistinctする
var activeUserIds = users
.Where(u => u.IsActive)
.Select(u => u.Id)
.Distinct()
.ToList();

先にWhereSelectで対象データを減らすと、メモリ使用量を抑えやすくなります。

9-3. GetHashCodeの品質がパフォーマンスに影響する

GetHashCodeが常に同じ値を返すような実装は避けましょう。

悪い例です。

C#
public override int GetHashCode()
{
return 0;
}

このような実装でも動く場合はありますが、ハッシュ値による効率的な分類ができず、パフォーマンスが悪化しやすくなります。

よい例です。

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

比較に使うプロパティをもとに、適切なハッシュコードを返すようにしましょう。

9-4. DB検索ならLINQ to ObjectsとLINQ to Entitiesの違いに注意する

C#のLINQには、主に次のような種類があります。

C#
// LINQ to Objects
var result = list.Distinct().ToList();

// LINQ to Entities
var result = dbContext.Users.Distinct().ToList();

LINQ to Objectsは、メモリ上のListや配列に対して処理します。

一方、Entity FrameworkなどのLINQ to Entitiesでは、LINQがSQLに変換されてデータベース側で実行されることがあります。

そのため、同じDistinctでも、実行場所や変換されるSQLが異なります。

C#
var result = dbContext.Users
.Select(u => u.Email)
.Distinct()
.ToList();

このようなコードは、SQLのDISTINCTに変換されることがあります。

ただし、自作のIEqualityComparer<T>はSQLに変換できないことが多いため、DBクエリでは使えない場合があります。

9-5. SQLのDISTINCTとの違い

C# LINQのDistinctとSQLのDISTINCTは似ていますが、完全に同じではありません。

C# LINQ to ObjectsのDistinctは、メモリ上のオブジェクトに対してEqualsGetHashCodeを使って重複判定します。

C#
var result = users.Distinct().ToList();

SQLのDISTINCTは、データベース上の行や列の値に対して重複を排除します。

SQL
SELECT DISTINCT Email
FROM Users;

Entity Frameworkなどを使っている場合、どのLINQがSQLに変換され、どの処理がメモリ上で行われるのかを意識することが重要です。

10. 実務で使えるDistinctのサンプル集

10-1. ユーザー一覧から重複ユーザーを削除する

ユーザーIDで重複削除する例です。

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

var users = new List<User>
{
new User { Id = 1, Name = "Alice", Email = "alice@example.com" },
new User { Id = 1, Name = "Alice", Email = "alice@example.com" },
new User { Id = 2, Name = "Bob", Email = "bob@example.com" }
};

var uniqueUsers = users
.DistinctBy(u => u.Id)
.ToList();

Idが同じユーザーを1件にまとめられます。

10-2. 商品一覧から商品コードの重複を削除する

商品コードで重複削除する例です。

C#
public class Product
{
public string ProductCode { get; set; } = "";
public string Name { get; set; } = "";
public decimal Price { get; set; }
}

var products = new List<Product>
{
new Product { ProductCode = "A001", Name = "Pen", Price = 100 },
new Product { ProductCode = "A001", Name = "Pen", Price = 100 },
new Product { ProductCode = "B002", Name = "Notebook", Price = 200 }
};

var uniqueProducts = products
.DistinctBy(p => p.ProductCode)
.ToList();

商品コードが一意であれば、DistinctBy(p => p.ProductCode)が読みやすいです。

10-3. CSV読み込み後に重複行を削除する

CSVを読み込んだ後、完全一致する行を削除する例です。

C#
var lines = File.ReadAllLines("users.csv");

var uniqueLines = lines
.Distinct()
.ToList();

File.WriteAllLines("users_unique.csv", uniqueLines);

ヘッダーを除いてデータ部分だけ重複削除したい場合です。

C#
var lines = File.ReadAllLines("users.csv");

var header = lines.First();
var dataLines = lines.Skip(1);

var uniqueLines = new[] { header }
.Concat(dataLines.Distinct())
.ToList();

File.WriteAllLines("users_unique.csv", uniqueLines);

特定の列だけで重複削除したい場合は、CSVをオブジェクトに変換してからDistinctByする方が安全です。

C#
var uniqueUsers = users
.DistinctBy(u => u.Email)
.ToList();

10-4. APIレスポンスの重複データを整理する

APIから重複したデータが返ってくる場合にも、DistinctByが便利です。

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

var items = await GetItemsFromApiAsync();

var uniqueItems = items
.DistinctBy(x => x.Code)
.ToList();

更新日時がある場合は、最新データを残すこともあります。

C#
var uniqueItems = items
.GroupBy(x => x.Code)
.Select(g => g.OrderByDescending(x => x.UpdatedAt).First())
.ToList();

単純に最初の1件でよいならDistinctBy、条件付きで残すデータを選ぶならGroupByが向いています。

10-5. 重複チェックだけを行う方法

重複削除ではなく、重複があるかどうかだけをチェックしたい場合もあります。

C#
var hasDuplicates = numbers.Count != numbers.Distinct().Count();

ただし、Count()を複数回呼ぶと全体を列挙するため、大量データでは注意が必要です。

キーで重複チェックする場合です。

C#
var hasDuplicateEmails = users
.GroupBy(u => u.Email)
.Any(g => g.Count() > 1);

重複している値を一覧で取得する場合です。

C#
var duplicateEmails = users
.GroupBy(u => u.Email)
.Where(g => g.Count() > 1)
.Select(g => g.Key)
.ToList();

重複しているユーザー自体を取得する場合です。

C#
var duplicatedUsers = users
.GroupBy(u => u.Email)
.Where(g => g.Count() > 1)
.SelectMany(g => g)
.ToList();

11. C# LINQ Distinctに関するよくある質問

11-1. Distinctは元のListを変更しますか?

いいえ、Distinctは元のListを変更しません。

C#
var numbers = new List<int> { 1, 1, 2 };

var result = numbers.Distinct().ToList();

Console.WriteLine(string.Join(", ", numbers)); // 1, 1, 2
Console.WriteLine(string.Join(", ", result)); // 1, 2

元のListを変更したい場合は、結果を再代入します。

C#
numbers = numbers.Distinct().ToList();

11-2. Distinctで最初に出てきた要素は残りますか?

LINQ to Objectsでは、実際には先に列挙された要素が結果に出るように見えることが多いです。

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

var result = numbers.Distinct().ToList();

Console.WriteLine(string.Join(", ", result)); // 3, 1, 2

ただし、公式ドキュメント上はDistinctの結果は順序付けされていないシーケンスとして説明されています。順序に依存する処理では、OrderByなどで明示的に並び替えるのが安全です。Microsoft Learn

C#
var result = numbers
.Distinct()
.OrderBy(x => x)
.ToList();

11-3. DistinctとExceptの違いは何ですか?

Distinctは、1つのシーケンス内の重複を削除します。

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

var result = numbers.Distinct().ToList();
// 1, 2, 3

Exceptは、あるシーケンスから別のシーケンスに含まれる要素を除外します。

C#
var source = new List<int> { 1, 2, 3, 4 };
var exclude = new List<int> { 2, 4 };

var result = source.Except(exclude).ToList();
// 1, 3

使い分けは次のとおりです。

C#
// 同じList内の重複を消す
Distinct()

// 別のListに含まれる値を除外する
Except()

11-4. DistinctByはどのバージョンから使えますか?

DistinctByは、.NET 6以降の環境で使えるLINQメソッドです。

C#
var result = users.DistinctBy(u => u.Id).ToList();

古い.NET FrameworkなどでDistinctByが使えない場合は、GroupByで代用できます。

C#
var result = users
.GroupBy(u => u.Id)
.Select(g => g.First())
.ToList();

または、MoreLINQなどの外部ライブラリを使う選択肢もありますが、現在の.NETでは標準のDistinctByを使える環境ならそれを優先するとよいでしょう。

11-5. 自作クラスではIEquatable<T>とIEqualityComparer<T>のどちらを使うべきですか?

クラスの標準的な等価性を定義したいなら、IEquatable<T>を使うのがおすすめです。

C#
public class User : IEquatable<User>
{
public int Id { get; set; }

public bool Equals(User? other)
{
return other is not null && Id == other.Id;
}

public override bool Equals(object? obj)
{
return Equals(obj as User);
}

public override int GetHashCode()
{
return Id.GetHashCode();
}
}

一方、比較ルールを場面によって変えたい場合は、IEqualityComparer<T>が向いています。

C#
public class UserEmailComparer : IEqualityComparer<User>
{
public bool Equals(User? x, User? y)
{
return x?.Email == y?.Email;
}

public int GetHashCode(User obj)
{
return obj.Email.GetHashCode();
}
}

使い分けの目安です。

C#
// その型の標準的な比較ルールを定義したい
IEquatable<T>

// 特定の処理だけで比較ルールを変えたい
IEqualityComparer<T>

// 特定プロパティだけで簡単に重複削除したい
DistinctBy

実務では、まずDistinctByで解決できるかを考え、比較ルールを再利用したい場合にIEqualityComparer<T>、型そのものの等価性として定義したい場合にIEquatable<T>を検討するとよいです。

まとめ

C# LINQのDistinctは、Listや配列から重複を削除するための基本的で強力なメソッドです。

単純な値の重複削除であれば、次のように書くだけで十分です。

C#
var result = numbers.Distinct().ToList();

文字列の大文字・小文字を無視したい場合は、比較子を渡します。

C#
var result = words
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();

自作クラスでは、既定の比較が参照ベースになりやすいため、期待どおりに重複削除されないことがあります。その場合は、EqualsGetHashCodeIEquatable<T>IEqualityComparer<T>recordDistinctByなどを使って比較ルールを明確にします。

特定のプロパティだけで重複削除したい場合は、DistinctByが最も読みやすい選択肢です。

C#
var result = users.DistinctBy(u => u.Id).ToList();

複数プロパティで比較したい場合は、匿名型やタプルを使えます。

C#
var result = users
.DistinctBy(u => new { u.Id, u.Name })
.ToList();

また、集計や「最新の1件を残す」といった処理が必要な場合は、GroupByが適しています。

C#
var result = users
.GroupBy(u => u.Id)
.Select(g => g.OrderByDescending(u => u.UpdatedAt).First())
.ToList();

DistinctDistinctByGroupByを正しく使い分けることで、C#のListやオブジェクトコレクションの重複処理を、シンプルかつ保守しやすく書けるようになります。