C# の LINQ で重複を除く Distinct

Distinct はシーケンスから重複を除去するメソッドだ。ユニークな値だけを取り出したいときに使う。

基本的な使い方

同じ値を持つ要素を1つにまとめる。

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

var unique = numbers.Distinct();

foreach (var n in unique)
{
    Console.Write($"{n} ");  // 1 2 3 4
}

出現順は維持される。最初に出現したものが残り、2回目以降は除外される。

文字列の重複除去

文字列でも同様に使える。

string[] words = { "apple", "banana", "apple", "cherry", "banana" };

var unique = words.Distinct();

foreach (var word in unique)
{
    Console.WriteLine(word);
}
// apple
// banana
// cherry

大文字小文字を区別しない比較

デフォルトでは大文字小文字を区別する。区別したくない場合は StringComparer を指定する。

string[] names = { "Alice", "alice", "Bob", "ALICE" };

// デフォルト: 大文字小文字を区別
var distinct1 = names.Distinct();
// Alice, alice, Bob, ALICE(4件)

// 大文字小文字を区別しない
var distinct2 = names.Distinct(StringComparer.OrdinalIgnoreCase);
// Alice, Bob(2件)

オブジェクトの重複除去

参照型のオブジェクトでは、デフォルトで参照の等価性が使われる。

var people = new List<Person>
{
    new Person { Name = "Alice", Age = 25 },
    new Person { Name = "Alice", Age = 25 },
    new Person { Name = "Bob", Age = 30 }
};

var distinct = people.Distinct();
Console.WriteLine(distinct.Count());  // 3(すべて別オブジェクト)

同じ内容でも別のインスタンスなら重複とみなされない。

IEqualityComparer を使う

オブジェクトの特定のプロパティで重複判定したい場合は、カスタムの比較クラスを作る。

public class PersonNameComparer : IEqualityComparer<Person>
{
    public bool Equals(Person? x, Person? y)
    {
        if (x == null || y == null) return false;
        return x.Name == y.Name;
    }

    public int GetHashCode(Person obj)
    {
        return obj.Name?.GetHashCode() ?? 0;
    }
}

このコンペアラを Distinct に渡す。

var people = new List<Person>
{
    new Person { Name = "Alice", Age = 25 },
    new Person { Name = "Alice", Age = 30 },
    new Person { Name = "Bob", Age = 30 }
};

var distinct = people.Distinct(new PersonNameComparer());
Console.WriteLine(distinct.Count());  // 2(Alice と Bob)

DistinctBy(.NET 6 以降)

.NET 6 以降では DistinctBy が使え、カスタムコンペアラなしでプロパティ指定ができる。

var people = new List<Person>
{
    new Person { Name = "Alice", Age = 25 },
    new Person { Name = "Alice", Age = 30 },
    new Person { Name = "Bob", Age = 30 }
};

// 名前で重複除去
var distinctByName = people.DistinctBy(p => p.Name);
Console.WriteLine(distinctByName.Count());  // 2

// 年齢で重複除去
var distinctByAge = people.DistinctBy(p => p.Age);
Console.WriteLine(distinctByAge.Count());  // 2(25歳と30歳)

非常に便利なので、.NET 6 以降なら積極的に使おう。

GroupBy で代用

DistinctBy がない環境では、GroupBy で代用できる。

var people = new List<Person>
{
    new Person { Name = "Alice", Age = 25 },
    new Person { Name = "Alice", Age = 30 },
    new Person { Name = "Bob", Age = 30 }
};

// 名前でグループ化し、各グループの最初の要素を取る
var distinctByName = people
    .GroupBy(p => p.Name)
    .Select(g => g.First());

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

実用的な例

タグの一覧取得

var posts = new List<Post>
{
    new Post { Title = "記事1", Tags = new[] { "C#", "LINQ" } },
    new Post { Title = "記事2", Tags = new[] { "C#", "ASP.NET" } },
    new Post { Title = "記事3", Tags = new[] { "LINQ", "Entity Framework" } }
};

var allTags = posts
    .SelectMany(p => p.Tags)
    .Distinct()
    .OrderBy(t => t);

foreach (var tag in allTags)
{
    Console.WriteLine(tag);
}
// ASP.NET
// C#
// Entity Framework
// LINQ

すべての記事からタグを集め、重複を除去して一覧化している。

重複チェック

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

bool hasDuplicates = ids.Count() != ids.Distinct().Count();
Console.WriteLine(hasDuplicates);  // True

元の数と Distinct 後の数が違えば重複がある。

Union / Intersect / Except

集合演算のメソッドも重複除去と関連している。

int[] a = { 1, 2, 3, 4 };
int[] b = { 3, 4, 5, 6 };

var union = a.Union(b);      // 1, 2, 3, 4, 5, 6(和集合)
var intersect = a.Intersect(b); // 3, 4(積集合)
var except = a.Except(b);    // 1, 2(差集合)

Union は両方のシーケンスを結合し、自動的に重複を除去する。Concat + Distinct と同じ結果だが、Union の方が意図が明確だ。