C#のDistinct完全ガイド|LINQでListの重複削除からオブジェクト比較・DistinctByまで解説

はじめに

C#でListや配列の重複を削除したいとき、よく使われるのがLINQのDistinctです。

たとえば、次のようなリストがあるとします。

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

この中から重複を取り除き、1, 2, 3だけを取得したい場合は、Distinctを使うだけで簡単に実現できます。

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

ただし、Distinctは単純な数値や文字列では簡単に使える一方で、オブジェクトのListでは「同じ値なのに重複削除されない」「特定のプロパティだけで比較したい」「大文字小文字を無視したい」といった悩みが出やすいメソッドでもあります。

この記事では、C#のDistinctについて、基本的な使い方から、DistinctByGroupByIEqualityComparerを使った実践的な重複削除まで解説します。

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

1-1. Distinctの役割とできること

Distinctは、シーケンス内の重複する要素を取り除き、一意な要素だけを返すLINQメソッドです。

たとえば、数値のListから重複を削除する場合は次のように書きます。

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

var distinctNumbers = numbers.Distinct();

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

出力例は次のとおりです。

C#
1
2
3
4

Distinctは、主に次のような場面で使います。

C#
// IDの重複をなくす
var uniqueIds = ids.Distinct().ToList();

// 名前の重複をなくす
var uniqueNames = names.Distinct().ToList();

// オブジェクトの重複をなくす
var uniqueUsers = users.Distinct().ToList();

ただし、オブジェクトの重複削除では、後述するように比較条件を正しく理解する必要があります。

1-2. Distinctが返す値の型と遅延実行の仕組み

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

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

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

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

また、DistinctはLINQの多くのメソッドと同じく遅延実行です。つまり、Distinct()を呼び出した時点ではまだ処理は実行されず、foreachToList()などで列挙されたタイミングで実際に重複削除が行われます。Microsoftのドキュメントでも、DistinctByなどのLINQメソッドは列挙されるまで実行されない遅延実行であると説明されています。Microsoft Learn

C#
var query = numbers.Distinct(); // この時点ではまだ実行されない

var list = query.ToList(); // ここで実行される

1-3. Distinctを使うために必要なusing

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

C#
using System.Linq;

.NET 6以降のプロジェクトでは、テンプレートによって暗黙的なusingが有効になっていることが多いため、明示的に書かなくても使える場合があります。

ただし、古いプロジェクトや明示的なusing管理をしているプロジェクトでは、次のようにファイルの先頭に追加します。

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

DistinctByを使う場合も、同じくSystem.Linqが必要です。

1-4. DistinctとSQLのDISTINCTの違い

C#のDistinctとSQLのDISTINCTは、どちらも「重複を取り除く」という意味では似ています。

SQLでは、次のように指定した列の重複を削除します。

SQL
SELECT DISTINCT Name
FROM Users;

一方、C#のDistinctは、メモリ上のListや配列などのシーケンスに対して使います。

C#
var uniqueNames = names.Distinct().ToList();

大きな違いは、比較の基準です。

C#のDistinctは、既定ではEqualityComparer<T>.Defaultを使って値を比較します。MicrosoftのEnumerable.Distinctの説明でも、既定の等値比較子を使って重複しない値を返すとされています。Microsoft Learn

一方、SQLのDISTINCTは、データベース上の列や行の値を基準に重複を判定します。

また、Entity FrameworkなどでIQueryableに対してDistinctを使う場合は、SQLのDISTINCTに変換されることがあります。ただし、すべてのC#の比較ロジックがSQLに変換できるわけではないため、複雑な比較条件では注意が必要です。

2. Listや配列からDistinctで重複削除する基本的な使い方

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

数値のListから重複を削除する例です。

C#
var numbers = new List<int> { 10, 20, 10, 30, 20, 40 };

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

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

出力例です。

C#
10, 20, 30, 40

intのような値型は、値そのものを比較して重複を判定します。そのため、特別な設定をしなくても期待通りに動くことが多いです。

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

文字列のListでも、Distinctは簡単に使えます。

C#
var fruits = new List<string>
{
"apple",
"banana",
"apple",
"orange",
"banana"
};

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

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

出力例です。

C#
apple, banana, orange

ただし、文字列の場合は大文字・小文字の違いに注意が必要です。

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

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

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

この場合、通常は"Apple""apple""APPLE"は別の文字列として扱われます。

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

DistinctList<T>だけでなく、配列にも使えます。

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

var result = numbers.Distinct();

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

配列に対してDistinctを使っても、戻り値は配列ではなくIEnumerable<T>です。

配列に戻したい場合は、ToArray()を使います。

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

Listに戻したい場合は、ToList()を使います。

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

2-4. Distinct後にListへ戻すToListの使い方

Distinct()だけではIEnumerable<T>のままなので、Listとして使いたい場合はToList()を付けます。

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

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

ToList()を呼ぶことで、その時点でクエリが実行され、結果がListとして確定します。

次のように書くと、後続処理で何度も列挙されるのを避けられます。

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

Console.WriteLine(uniqueNumbers.Count);
Console.WriteLine(uniqueNumbers[0]);

IEnumerable<T>のまま何度も列挙すると、そのたびに処理が再実行される可能性があります。結果を使い回す場合は、ToList()で確定しておくとわかりやすいです。

2-5. 重複削除後の順序はどうなるのか

LINQ to ObjectsのDistinctでは、実際の挙動としては元のシーケンスで最初に現れた要素が残る形で使われることが多いです。

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

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

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

出力例です。

C#
3, 1, 2

ただし、MicrosoftのドキュメントではDistinctの結果は順序付けされていないシーケンスとして説明されています。Microsoft Learn

そのため、順序が重要な場合は、Distinctの前後で明示的にOrderByを使うのが安全です。

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

3. Distinctで文字列を比較するときの注意点

3-1. 大文字・小文字を区別する場合

通常のDistinctでは、大文字・小文字は区別されます。

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

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

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

この場合、3つは別々の文字列として残ります。

C#
Tanaka, tanaka, TANAKA

ユーザー名やコード値のように、大文字・小文字を厳密に区別したい場合は、この挙動で問題ありません。

3-2. 大文字・小文字を区別せず重複削除する方法

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

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

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

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

出力例です。

C#
Tanaka, Suzuki

この場合、"Tanaka""tanaka""TANAKA"は同じ値として扱われ、最初に登場した"Tanaka"が残ります。

文字列比較では、用途に応じて次のような比較子を選びます。

C#
StringComparer.Ordinal
StringComparer.OrdinalIgnoreCase
StringComparer.CurrentCulture
StringComparer.CurrentCultureIgnoreCase
StringComparer.InvariantCulture
StringComparer.InvariantCultureIgnoreCase

ID、コード、キーの比較では、カルチャの影響を受けにくいOrdinal系が使われることが多いです。

3-3. 空文字・nullを含むListでDistinctを使う場合

Distinctは、空文字やnullを含むListにも使えます。

C#
var values = new List<string?>
{
"A",
"",
null,
"A",
"",
null,
"B"
};

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

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

出力例です。

C#
A

null
B

空文字""は空文字として1つにまとめられ、nullも1つの値として扱われます。

ただし、Distinctの前後で次のような処理を入れる場合は注意が必要です。

C#
var result = values
.Select(x => x.Trim())
.Distinct()
.ToList();

このコードは、xnullの場合にNullReferenceExceptionになります。

安全に書くなら、次のようにします。

C#
var result = values
.Select(x => x?.Trim())
.Distinct()
.ToList();

また、nullや空文字を除外してから重複削除したい場合は、Whereを組み合わせます。

C#
var result = values
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct()
.ToList();

3-4. 日本語文字列や全角半角の扱い

日本語文字列では、大文字・小文字だけでなく、全角・半角、ひらがな・カタカナ、濁点の扱いなどに注意が必要です。

たとえば、次の文字列は見た目が似ていても、通常は別の文字列として扱われます。

C#
var words = new List<string>
{
"カタカナ",
"カタカナ"
};

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

全角・半角を同一視したい場合は、事前に正規化してからDistinctする方法があります。

C#
using System.Text;

var words = new List<string>
{
"カタカナ",
"カタカナ"
};

var result = words
.Select(x => x.Normalize(NormalizationForm.FormKC))
.Distinct()
.ToList();

ただし、正規化すると文字の意味や表現が変わる場合もあります。検索用、表示用、保存用のどこで正規化するのかを設計してから使うことが重要です。

4. オブジェクトのListでDistinctがうまく効かない理由

4-1. Distinctは何を基準に重複判定するのか

Distinctは、既定では型ごとの等値比較を使って重複を判定します。

数値や文字列であれば、値が同じかどうかで比較されます。

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

一方、クラスのオブジェクトでは、既定では参照が同じかどうかが重要になります。

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

次のように同じ値を持つオブジェクトを作っても、それぞれ別インスタンスです。

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

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

Console.WriteLine(result.Count);

期待としては1件になってほしいかもしれませんが、結果は2件になることがあります。

4-2. 参照型オブジェクトでは同じ値でも重複削除されないケース

クラスは参照型です。既定では、同じプロパティ値を持っていても、別々に生成されたインスタンスは別物として扱われます。

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

Console.WriteLine(user1 == user2);

この結果は通常falseです。

MicrosoftのC#ドキュメントでも、通常のクラスは既定で参照等価を使い、2つの変数が同じオブジェクトを指しているかどうかを確認すると説明されています。Microsoft Learn

そのため、オブジェクトの中身で重複判定したい場合は、次のいずれかの方法を使います。

C#
// record型を使う
public record User(int Id, string Name);

// DistinctByを使う
var result = users.DistinctBy(x => x.Id).ToList();

// IEqualityComparerを使う
var result = users.Distinct(new UserComparer()).ToList();

// EqualsとGetHashCodeを実装する

4-3. EqualsとGetHashCodeの関係

Distinctでオブジェクトの重複判定を正しく行うには、EqualsGetHashCodeの関係が重要です。

Equalsは、2つのオブジェクトが等しいかどうかを判定します。

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

GetHashCodeは、ハッシュベースのコレクションで使われるハッシュ値を返します。

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

重要なのは、Equalsで等しいと判定されるオブジェクトは、同じハッシュコードを返す必要があるという点です。MicrosoftのIEqualityComparer<T>.Equalsの説明でも、Equalstrueを返す2つのオブジェクトは、GetHashCodeでも同じ値を返す必要があるとされています。Microsoft Learn

悪い例です。

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

// GetHashCodeを実装していない

このようにEqualsだけを変更してGetHashCodeを変更しないと、DistinctHashSetDictionaryで期待通りに動かない原因になります。

4-4. record型でDistinctが使いやすい理由

C#のrecord型は、値に基づく等価比較を標準でサポートします。

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

次のように同じ値を持つrecordをDistinctすると、重複が削除されます。

C#
var users = new List<User>
{
new User(1, "田中"),
new User(1, "田中"),
new User(2, "佐藤")
};

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

Console.WriteLine(result.Count);

出力例です。

C#
2

record型は、同じ型で各フィールドやプロパティの値が等しい場合に等しいと扱われます。C#のドキュメントでも、recordは値等価を持つ型として説明されています。Microsoft Learn

DTOや検索条件、値オブジェクトのように「中身が同じなら同じもの」と扱いたいデータでは、recordを使うとDistinctとの相性がよくなります。

5. オブジェクトを特定プロパティで重複削除する方法

5-1. DistinctByでプロパティを指定して重複削除する

オブジェクトのListを特定のプロパティで重複削除したい場合は、DistinctByが便利です。

C#
var users = new List<User>
{
new User { Id = 1, Name = "田中" },
new User { Id = 1, Name = "田中太郎" },
new User { Id = 2, Name = "佐藤" }
};

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

この場合、Idが同じユーザーは重複とみなされます。

出力すると、次のように最初に登場した要素が残ります。

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

出力例です。

C#
1: 田中
2: 佐藤

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

5-2. DistinctByで複数プロパティを比較する方法

複数のプロパティを基準に重複削除したい場合は、匿名型やタプルをキーにします。

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

または、タプルを使っても書けます。

C#
var result = users
.DistinctBy(x => (x.Id, x.Name))
.ToList();

たとえば、IdDepartmentIdの組み合わせで重複削除したい場合は次のように書きます。

C#
var result = employees
.DistinctBy(x => new { x.Id, x.DepartmentId })
.ToList();

「ユーザーIDだけなら同じ」「ユーザーIDと部署IDの組み合わせなら別」といった業務ルールに応じてキーを決めるのがポイントです。

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

DistinctByは.NET 6以降で使えます。古い.NET Frameworkや.NET Standard環境では使えない場合があります。

その場合は、GroupByで代用できます。

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

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

複数プロパティで重複削除したい場合も同じです。

C#
var result = users
.GroupBy(x => new { x.Id, x.Name })
.Select(g => g.First())
.ToList();

DistinctByが使える環境ならDistinctByの方が意図が明確ですが、GroupByは古い環境でも使いやすく、集計処理にも発展させやすい方法です。

5-4. SelectとDistinctを組み合わせて一部の値だけ取得する方法

オブジェクト全体ではなく、特定のプロパティの値だけを重複なしで取得したい場合は、SelectDistinctを組み合わせます。

C#
var names = users
.Select(x => x.Name)
.Distinct()
.ToList();

たとえば、注文一覧から顧客IDだけを重複なしで取得する場合は次のように書けます。

C#
var customerIds = orders
.Select(x => x.CustomerId)
.Distinct()
.ToList();

この方法では、元のオブジェクトではなく、選択した値だけが返ります。

C#
List<int> customerIds = orders
.Select(x => x.CustomerId)
.Distinct()
.ToList();

「ユーザーオブジェクトを残したい」のか、「ユーザーIDだけほしい」のかで、DistinctBySelect + Distinctを使い分けましょう。

5-5. 最初の要素ではなく任意の要素を残したい場合

DistinctByGroupBy(...).Select(g => g.First())では、基本的に最初に登場した要素を残します。

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

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

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

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

または、先に全体を並べ替えてからDistinctByを使う方法もあります。

C#
var result = users
.OrderByDescending(x => x.UpdatedAt)
.DistinctBy(x => x.Id)
.ToList();

この場合、更新日時が新しい順に並べたあとで、各Idの最初の要素を残します。

6. IEqualityComparerを使ってDistinctの比較条件をカスタマイズする

6-1. IEqualityComparerとは

IEqualityComparer<T>は、等価比較の条件を自分で定義するためのインターフェースです。

Distinctには、比較条件を指定するオーバーロードがあります。

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

IEqualityComparer<T>を使うと、次のような比較条件を自由に作れます。

C#
// IDが同じなら同一ユーザーとみなす
// メールアドレスを小文字化して比較する
// 姓名の組み合わせで比較する
// 商品コードと倉庫コードの組み合わせで比較する

Microsoftのドキュメントでも、IEqualityComparer<T>はコレクションで使う等価比較をカスタマイズするためのインターフェースとして説明されています。Microsoft Learn

6-2. IEqualityComparerを実装する基本手順

IEqualityComparer<T>を実装するには、EqualsGetHashCodeを定義します。

C#
public class UserComparer : 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 result = users
.Distinct(new UserComparer())
.ToList();

Equalsでは「何をもって同じとするか」を定義し、GetHashCodeではその比較条件に対応したハッシュコードを返します。

6-3. オブジェクトのIDで重複削除するサンプル

ユーザーIDが同じなら同じユーザーとみなす例です。

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

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 = "田中" },
new User { Id = 1, Name = "田中太郎" },
new User { Id = 2, Name = "佐藤" }
};

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

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

出力例です。

C#
1: 田中
2: 佐藤

Idが同じ2件のうち、最初に登場した田中が残っています。

6-4. 複数条件で重複判定するサンプル

複数条件で重複判定したい場合は、EqualsGetHashCodeの両方に同じ条件を反映します。

C#
public class EmployeeComparer : IEqualityComparer<Employee>
{
public bool Equals(Employee? x, Employee? y)
{
if (ReferenceEquals(x, y)) return true;
if (x is null || y is null) return false;

return x.CompanyId == y.CompanyId
&& x.EmployeeNo == y.EmployeeNo;
}

public int GetHashCode(Employee obj)
{
return HashCode.Combine(obj.CompanyId, obj.EmployeeNo);
}
}

使用例です。

C#
var result = employees
.Distinct(new EmployeeComparer())
.ToList();

HashCode.Combineを使うと、複数プロパティからハッシュコードを作りやすくなります。

C#
return HashCode.Combine(obj.CompanyId, obj.EmployeeNo);

このように、Equalsで比較しているプロパティと、GetHashCodeで使うプロパティを一致させることが重要です。

6-5. EqualityComparerを使うときの注意点

IEqualityComparerを使うときは、次の点に注意します。

まず、Equalsで等しいと判断する条件と、GetHashCodeで使う条件を必ずそろえます。

悪い例です。

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

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

この例では、EqualsIdで比較しているのに、GetHashCodeNameを使っています。これでは、同じIdでも異なるハッシュコードになる可能性があり、Distinctが正しく動かない原因になります。

次に、変更可能なプロパティを比較条件にする場合も注意が必要です。

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

このようにEmailを基準にしている場合、DistinctHashSetで使っている途中にEmailが変更されると、挙動がわかりにくくなります。

比較条件には、できるだけ不変のIDやコードを使うのがおすすめです。

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

7-1. 単純な値の重複削除ならDistinct

数値、文字列、日付、enumなど、値そのものの重複を削除したい場合はDistinctを使います。

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

文字列で大文字・小文字を無視したい場合も、比較子を渡せば対応できます。

C#
var uniqueNames = names
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();

単純な値の重複削除では、まずDistinctを選ぶとよいでしょう。

7-2. オブジェクトの特定プロパティならDistinctBy

オブジェクトのListから、特定のプロパティを基準に重複削除したい場合はDistinctByが便利です。

C#
var uniqueUsers = users
.DistinctBy(x => x.Id)
.ToList();

複数プロパティを基準にする場合も、匿名型やタプルを使えば対応できます。

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

「オブジェクト全体は残したいが、比較は一部のプロパティで行いたい」という場合は、DistinctByが読みやすくなります。

7-3. 集計やグループ単位の処理ならGroupBy

重複削除だけでなく、グループごとの件数や合計も取得したい場合はGroupByを使います。

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

単に重複削除するだけならDistinctDistinctByの方が簡潔ですが、集計やグループ単位の処理がある場合はGroupByが適しています。

7-4. 比較条件を細かく制御するならIEqualityComparer

比較条件を再利用したい場合や、複雑なロジックで比較したい場合はIEqualityComparerを使います。

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

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

C#
// メールアドレスを小文字化して比較する
// nullや空白を同一扱いにする
// 複数プロパティを独自ルールで比較する
// 業務上の同一判定ロジックを共通化する

DistinctByで十分ならDistinctByの方が簡単ですが、比較ルールをクラスとして明示したい場合はIEqualityComparerが向いています。

7-5. 用途別の使い分け早見表

やりたいことおすすめ
数値や文字列の重複を削除したいDistinct
文字列の大文字・小文字を無視したいDistinct(StringComparer.OrdinalIgnoreCase)
オブジェクトをIDで重複削除したいDistinctBy(x => x.Id)
複数プロパティで重複削除したいDistinctBy(x => new { x.A, x.B })
古い環境でDistinctByが使えないGroupBy(...).Select(g => g.First())
件数や合計も出したいGroupBy
独自の比較ルールを再利用したいIEqualityComparer
オブジェクト全体の値で比較したいrecordまたはEquals/GetHashCode

迷った場合は、単純な値ならDistinct、オブジェクトのプロパティ指定ならDistinctBy、集計が必要ならGroupByと考えると整理しやすいです。

8. Distinctを使うときのパフォーマンスと注意点

8-1. Distinctの内部ではHashSetが使われる

Distinctは、重複判定のために内部的にハッシュベースの仕組みを使います。

そのため、通常は単純な二重ループで重複チェックするより効率的です。

悪い例です。

C#
var result = new List<int>();

foreach (var number in numbers)
{
if (!result.Contains(number))
{
result.Add(number);
}
}

このようなコードは、件数が増えるとContainsの繰り返しで遅くなりやすいです。

Distinctを使えば、簡潔でパフォーマンス面でも有利なケースが多くなります。

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

8-2. 大量データで気をつけたいメモリ使用量

Distinctは、すでに登場した値を記録しながら重複を判定します。そのため、大量のデータに対して使うと、記録用のメモリが必要になります。

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

このコードでは、重複判定用の内部データに加えて、最終的なListもメモリに保持します。

大量データを扱う場合は、次の点を検討します。

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

// DB側でDistinctする
var ids = await db.Users.Select(x => x.Id).Distinct().ToListAsync();

// 一度に全件をメモリに載せない設計にする

特にデータベースから取得する場合は、C#側で全件取得してからDistinctするより、可能であればSQL側で重複削除する方が効率的です。

8-3. OrderByとの実行順序による結果の違い

OrderByDistinctDistinctByの順序によって結果が変わることがあります。

たとえば、更新日時が新しいユーザーを残したい場合は、先に並べ替えてからDistinctByします。

C#
var result = users
.OrderByDescending(x => x.UpdatedAt)
.DistinctBy(x => x.Id)
.ToList();

一方、次のようにすると、先に重複削除され、その後で並べ替えられます。

C#
var result = users
.DistinctBy(x => x.Id)
.OrderByDescending(x => x.UpdatedAt)
.ToList();

この場合、各Idでどの要素が残るかは、DistinctByが実行される時点の並びに依存します。

「どの要素を残したいか」を決めてから、OrderByDistinctByの順序を決めましょう。

8-4. ToListの呼び出しタイミングに注意する

LINQは遅延実行なので、ToList()をどこで呼ぶかによって処理内容やパフォーマンスが変わります。

次のコードでは、WhereSelectDistinctをまとめて実行した後にList化しています。

C#
var result = users
.Where(x => x.IsActive)
.Select(x => x.Email)
.Distinct()
.ToList();

一方、途中でToList()を呼ぶと、その時点で一度結果が確定します。

C#
var activeUsers = users
.Where(x => x.IsActive)
.ToList();

var result = activeUsers
.Select(x => x.Email)
.Distinct()
.ToList();

必ずしも悪いわけではありませんが、不要なToList()はメモリ使用量や処理回数を増やす原因になります。

基本的には、結果を確定したい最後のタイミングでToList()を呼ぶのがおすすめです。

8-5. LINQ to SQLやEntity Frameworkで使う場合の注意点

Entity FrameworkなどでDistinctを使う場合、クエリがSQLに変換されてデータベース側で実行されることがあります。

C#
var emails = await db.Users
.Select(x => x.Email)
.Distinct()
.ToListAsync();

このような単純なケースでは、SQLのDISTINCTに近い形で処理されることが多いです。

ただし、C#独自のメソッドや複雑な比較ロジックを含む場合、SQLに変換できないことがあります。EF Coreでは、サーバー側に変換できない式がトップレベルの射影以外にある場合、実行時例外になると説明されています。Microsoft Learn

注意が必要な例です。

C#
var result = await db.Users
.Distinct(new UserIdComparer())
.ToListAsync();

IEqualityComparerを使った比較は、データベース側のSQLに変換できないことが多いため、IQueryableではなくAsEnumerable()ToList()の後にメモリ上で処理する必要が出る場合があります。

C#
var users = await db.Users.ToListAsync();

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

ただし、この方法は全件をメモリに読み込むため、大量データでは注意が必要です。

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

9-1. オブジェクトの重複が削除されない

最もよくある原因は、クラスのオブジェクトをそのままDistinctしていることです。

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

Userが通常のclassで、EqualsGetHashCodeを実装していない場合、同じプロパティ値でも別インスタンスとして扱われることがあります。

対策は次のいずれかです。

C#
// IDで重複削除
var result = users.DistinctBy(x => x.Id).ToList();

// GroupByで代用
var result = users.GroupBy(x => x.Id).Select(g => g.First()).ToList();

// comparerを指定
var result = users.Distinct(new UserIdComparer()).ToList();

// record型にする
public record User(int Id, string Name);

9-2. 比較条件が意図したものと違う

文字列比較でよくあるのが、大文字・小文字の違いです。

C#
var values = new List<string> { "abc", "ABC" };

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

この場合、"abc""ABC"は別物として扱われます。

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

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

オブジェクトの場合も、「IDだけで比較したいのか」「名前も含めて比較したいのか」を明確にする必要があります。

C#
// IDだけで比較
var byId = users.DistinctBy(x => x.Id).ToList();

// IDと名前で比較
var byIdAndName = users.DistinctBy(x => new { x.Id, x.Name }).ToList();

9-3. DistinctByが使えない

DistinctByが使えない場合、主な原因は対象フレームワークが古いことです。

DistinctByは.NET 6以降で利用できます。古い.NET Frameworkなどでは、標準LINQにDistinctByが存在しないため、次のようなエラーになることがあります。

C#
'IEnumerable<User>' does not contain a definition for 'DistinctBy'

対策としては、次の方法があります。

C#
// .NET 6以降にする
var result = users.DistinctBy(x => x.Id).ToList();

// GroupByで代用する
var result = users.GroupBy(x => x.Id).Select(g => g.First()).ToList();

古い環境では、MoreLINQなどのライブラリにあるDistinctByを使う方法もありますが、標準機能だけで書くならGroupByがわかりやすいです。

9-4. 結果の順序が想定と違う

DistinctDistinctByの結果の順序に依存したコードを書くと、環境や処理内容によって意図が伝わりにくくなることがあります。

順序が重要な場合は、明示的にOrderByを書きます。

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

また、どの要素を残すかが重要な場合は、先に並べ替えます。

C#
var result = users
.OrderByDescending(x => x.UpdatedAt)
.DistinctBy(x => x.Id)
.ToList();

「重複削除してから並べ替える」のか、「並べ替えてから重複削除する」のかを明確にしましょう。

9-5. nullを含むデータでエラーになる

Distinct自体はnullを含むデータでも使えます。

C#
var values = new List<string?> { null, "A", null, "B" };

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

ただし、Distinctの前にメソッド呼び出しをしている場合は注意が必要です。

C#
var result = values
.Select(x => x.Trim())
.Distinct()
.ToList();

xnullだとエラーになります。

安全に書くなら、null条件演算子を使います。

C#
var result = values
.Select(x => x?.Trim())
.Distinct()
.ToList();

nullを除外したい場合は、先にWhereを使います。

C#
var result = values
.Where(x => x is not null)
.Select(x => x!.Trim())
.Distinct()
.ToList();

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

10-1. Distinctは元のListを変更する?

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

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

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

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

出力例です。

C#
1, 1, 2
1, 2

元のListを重複削除後の内容に置き換えたい場合は、結果を再代入します。

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

10-2. Distinctで重複件数も取得できる?

Distinctだけでは重複件数は取得できません。

重複件数を取得したい場合は、GroupByを使います。

C#
var result = values
.GroupBy(x => x)
.Select(g => new
{
Value = g.Key,
Count = g.Count()
})
.ToList();

重複している値だけを取得したい場合は、次のようにします。

C#
var duplicates = values
.GroupBy(x => x)
.Where(g => g.Count() > 1)
.Select(g => new
{
Value = g.Key,
Count = g.Count()
})
.ToList();

10-3. Distinctで最後の要素を残すには?

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

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

または、順序を逆にしてからDistinctByする方法もあります。

C#
var result = users
.AsEnumerable()
.Reverse()
.DistinctBy(x => x.Id)
.Reverse()
.ToList();

ただし、読みやすさを考えると、GroupBy(...).Select(g => g.Last())の方が意図が伝わりやすいです。

更新日時が最新の要素を残したいなら、次のように書きます。

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

10-4. 複数列・複数プロパティでDistinctできる?

できます。

DistinctByを使う場合は、匿名型やタプルをキーにします。

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

タプルでも書けます。

C#
var result = users
.DistinctBy(x => (x.Id, x.Name))
.ToList();

GroupByでも同じように書けます。

C#
var result = users
.GroupBy(x => new { x.Id, x.Name })
.Select(g => g.First())
.ToList();

データベースに対して使う場合は、SQLに変換できる形かどうかにも注意しましょう。

10-5. DistinctとHashSetはどちらを使うべき?

単にListや配列から重複を削除したいだけなら、Distinctが簡潔です。

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

一方、重複チェックをしながら要素を追加していきたい場合は、HashSetが向いています。

C#
var set = new HashSet<int>();

set.Add(1);
set.Add(1);
set.Add(2);

Console.WriteLine(set.Count);

出力例です。

C#
2

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

用途おすすめ
既存のListから重複削除したいDistinct
重複なしのコレクションを保持したいHashSet
追加時に重複を防ぎたいHashSet
LINQの流れの中で処理したいDistinct
オブジェクトの特定プロパティで重複削除したいDistinctBy

一度きりの重複削除ならDistinct、データ構造として一意性を保ちたいならHashSetと考えるとわかりやすいです。

まとめ

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

数値や文字列のような単純な値であれば、次のように簡単に使えます。

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

文字列の大文字・小文字を無視したい場合は、StringComparerを指定します。

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

オブジェクトのListでは、通常のclassをそのままDistinctしても、期待通りに重複削除されないことがあります。特定のプロパティで重複削除したい場合は、DistinctByを使うのが便利です。

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

DistinctByが使えない環境では、GroupByで代用できます。

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

比較条件を細かく制御したい場合は、IEqualityComparerを実装します。

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

Distinctを正しく使うポイントは、「何を基準に同じと判断するのか」を明確にすることです。

単純な値ならDistinct、オブジェクトの特定プロパティならDistinctBy、集計や任意の要素を残す処理ならGroupBy、複雑な比較条件ならIEqualityComparerを選ぶと、C#での重複削除をわかりやすく安全に実装できます。