はじめに:「順序保持 Distinct」は“最初に出てきた順だけを残す”技
「重複を消したいけど、元の並び順は崩したくない」——業務でデータを扱っていると、かなり頻繁に出てきます。
ログの順番はそのままに、同じユーザー ID は 1 回だけにしたい
画面に表示するタグ一覧で、最初に出てきた順番をそのまま見せたい
履歴データから「初めて登場した順」にユニークな値を取りたい
こういうときに使えるのが「順序保持 Distinct」です。
実は、C# の LINQ の Distinct() は、もともと「最初に出てきた順」を保ったまま重複を消してくれる、という性質があります。
ここから、
Distinct() の基本と「順序がどうなるか」
「最初に出てきたものだけ残す」という意味をしっかり理解する
特定のキーで順序保持 Distinct したいときの書き方(DistinctBy / 自作)
業務でよくある使いどころ
を、初心者向けにかみ砕いて説明していきます。
Distinct の基本:最初に出てきた順番をそのまま残す
まずは動きをコードで確認する
using System;
using System.Linq;
var names = new[]
{
"Alice",
"Bob",
"Alice",
"Charlie",
"Bob",
"Alice"
};
var distinct = names.Distinct();
foreach (var name in distinct)
{
Console.WriteLine(name);
}
C#出力はこうなります。
Alice
Bob
Charlie
ここで注目してほしいのは、「ソートされていない」ということです。
アルファベット順なら Alice, Bob, Charlie なので一見同じに見えますが、Distinct() は「ソートしている」のではなく、
“最初に出てきた順に、重複を取り除いている”
だけです。
順番を変えてみると、もっと分かりやすくなります。
var names = new[]
{
"Charlie",
"Alice",
"Bob",
"Alice",
"Charlie",
"Bob"
};
var distinct = names.Distinct();
foreach (var name in distinct)
{
Console.WriteLine(name);
}
C#出力はこうなります。
Charlie
Alice
Bob
ここでの重要ポイントは、
「Distinct は“最初に登場した順番”を保ったまま、
“2 回目以降の同じ値”を落としているだけ」
ということです。
「順序保持 Distinct」のイメージをしっかりつかむ
頭の中でシミュレーションしてみる
さきほどの配列を、1 つずつ見ていくイメージで追ってみます。
入力: Charlie, Alice, Bob, Alice, Charlie, Bob
1 個目: Charlie → 初登場なので採用
2 個目: Alice → 初登場なので採用
3 個目: Bob → 初登場なので採用
4 個目: Alice → すでに出ているので捨てる
5 個目: Charlie → すでに出ているので捨てる
6 個目: Bob → すでに出ているので捨てる
結果:
Charlie, Alice, Bob
これが「順序保持 Distinct」の正体です。
ここでの重要ポイントは、
「Distinct は“ユニークな集合”を作るというより、
“最初に出てきたものだけを残すフィルター”だと考えると分かりやすい
ということです。
特定のプロパティで順序保持 Distinct したい(DistinctBy)
.NET 6 以降なら DistinctBy が使える
オブジェクトの「特定のプロパティ」で重複を消したい、というのも業務ではよくあります。
例えば、ユーザーのリストから「メールアドレスが重複しているものは 1 件にまとめたい。ただし、最初に出てきたユーザーを優先したい」というケース。
public class User
{
public int Id { get; set; }
public string Email { get; set; } = "";
}
var users = new[]
{
new User { Id = 1, Email = "a@example.com" },
new User { Id = 2, Email = "b@example.com" },
new User { Id = 3, Email = "a@example.com" }, // Email 重複
new User { Id = 4, Email = "c@example.com" },
new User { Id = 5, Email = "b@example.com" }, // Email 重複
};
C#.NET 6 以降なら、DistinctBy が使えます。
using System.Linq;
var distinctByEmail = users
.DistinctBy(u => u.Email);
foreach (var u in distinctByEmail)
{
Console.WriteLine($"Id={u.Id}, Email={u.Email}");
}
C#出力はこうなります。
Id=1, Email=a@example.com
Id=2, Email=b@example.com
Id=4, Email=c@example.com
ここでの重要ポイントは、
DistinctBy も「最初に出てきた順」を保つ
ということです。Email が同じユーザーが複数いても、「最初に登場したユーザー」が採用されます。
.NET 5 以前や DistinctBy がない環境なら自作する
DistinctBy が使えない環境では、自分で拡張メソッドを作るのが定番です。
using System;
using System.Collections.Generic;
public static class DistinctExtensions
{
public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector)
{
var seen = new HashSet<TKey>();
foreach (var item in source)
{
if (seen.Add(keySelector(item)))
{
yield return item;
}
}
}
}
C#使い方は同じです。
var distinctByEmail = users.DistinctBy(u => u.Email);
C#ここでの重要ポイントは、
HashSet を使って「すでに見たキー」を覚えながら、
“初めて見るキーの要素だけ yield する”ことで、
順序を保った DistinctBy を実現している
ということです。
GroupBy との違いをはっきりさせる
GroupBy は「グループごとに全部残す」
GroupBy も「キーごとにまとめる」ので、よく混同されますが、目的が違います。
var groups = users.GroupBy(u => u.Email);
foreach (var g in groups)
{
Console.WriteLine($"Email: {g.Key}");
foreach (var u in g)
{
Console.WriteLine($" Id={u.Id}");
}
}
C#これは「同じ Email のユーザーを全部グループにまとめる」処理です。
一方、DistinctBy は「グループの代表(最初の 1 件)だけを残す」イメージです。
ここでの重要ポイントは、
「全部見たいなら GroupBy、代表だけでいいなら Distinct / DistinctBy」
という使い分けを意識することです。
実務での具体例 1:タグ一覧を「初登場順」でユニークにする
記事に付いているタグから、表示用のタグ一覧を作る
public class Article
{
public string Title { get; set; } = "";
public string[] Tags { get; set; } = Array.Empty<string>();
}
var articles = new[]
{
new Article { Title = "A", Tags = new[] { "C#", "LINQ", "業務" } },
new Article { Title = "B", Tags = new[] { "LINQ", "設計" } },
new Article { Title = "C", Tags = new[] { "C#", "テスト" } },
};
C#「画面上部にタグ一覧を出したい。順番は“初めて登場した順”にしたい」という場合。
var allTags = articles
.SelectMany(a => a.Tags)
.Distinct(); // ここで順序保持 Distinct
foreach (var tag in allTags)
{
Console.WriteLine(tag);
}
C#出力はこうなります。
C#
LINQ
業務
設計
テスト
ここでの重要ポイントは、
「タグが最初に登場した順」がそのまま一覧の順番になっている
ということです。
ユーザーにとっても、「よく出てくる順」に近い自然な並びになります。
実務での具体例 2:履歴から「初回登場順」のユーザー一覧を作る
ログイン履歴から「初めてログインした順」にユーザーを並べる
public class LoginHistory
{
public DateTime Time { get; set; }
public int UserId { get; set; }
}
var logs = new[]
{
new LoginHistory { Time = new DateTime(2024, 1, 1, 9, 0, 0), UserId = 1 },
new LoginHistory { Time = new DateTime(2024, 1, 1, 9, 5, 0), UserId = 2 },
new LoginHistory { Time = new DateTime(2024, 1, 1, 9, 10, 0), UserId = 1 },
new LoginHistory { Time = new DateTime(2024, 1, 1, 9, 15, 0), UserId = 3 },
new LoginHistory { Time = new DateTime(2024, 1, 1, 9, 20, 0), UserId = 2 },
};
C#「その日、どのユーザーがどの順番で初めてログインしたか」を知りたいとき。
var orderedUsers = logs
.OrderBy(x => x.Time) // 念のため時間順に並べる
.Select(x => x.UserId)
.Distinct(); // 順序保持 Distinct
foreach (var userId in orderedUsers)
{
Console.WriteLine(userId);
}
C#出力はこうなります。
1
2
3
ここでの重要ポイントは、
「時間順に並べたあとで Distinct することで、
“初回登場順”のユニークなユーザー一覧が取れる
ということです。
「最初に出てきた順を保つ」という Distinct の性質が、業務ロジックときれいに噛み合っています。
まとめ:「順序保持 Distinct ユーティリティ」は“最初の 1 回だけを大事に残す道具”
順序保持 Distinct の本質は、
「同じ値が何度も出てきても、
最初に出てきた 1 回だけを残し、
その“登場順”をそのまま保つ」
ことです。
押さえておきたいポイントを整理すると、
LINQ の Distinct() は、もともと「最初に出てきた順」を保つ
「順序保持 Distinct」とは、「最初の登場だけ残すフィルター」だと考えると分かりやすい
オブジェクトの特定プロパティでやりたいときは DistinctBy(または自作拡張メソッド)
全部の要素を見たいなら GroupBy、代表だけでいいなら Distinct / DistinctBy
「タグ一覧」「初回ログイン順」「初登場順のユニーク値」など、業務での出番はかなり多い
ここまで腹落ちしていれば、
「とりあえず Distinct してから OrderBy でソートする」だけの世界から一歩進んで、
“最初の 1 回だけを大事に残す、順序保持 Distinct ユーティリティ”を、意図を持って使いこなせるようになります。
