はじめに:「シャッフル」は“順番の意味を一度壊す”テクニック
シャッフルは、「コレクションの要素の順番をランダムに並べ替える」ことです。
トランプを切るイメージが一番近いです。
業務でも、実はちょこちょこ出番があります。
テストデータの順番をランダムにしたい
アンケートの選択肢を毎回違う順番で出したい
負荷テスト用にアクセス順をバラしたい
ここでは、C# でのシャッフルを、初心者向けにかみ砕いて説明します。
特に「LINQ でやりがちな危険な書き方」と「実務で使える正しいシャッフル」をしっかり区別して解説します。
よくあるけど危険なシャッフル:OrderBy(Guid.NewGuid())
一見スマートに見える LINQ シャッフル
ネットでよく見かけるシャッフルがこれです。
using System;
using System.Collections.Generic;
using System.Linq;
var list = new List<int> { 1, 2, 3, 4, 5 };
var shuffled = list
.OrderBy(_ => Guid.NewGuid())
.ToList();
Console.WriteLine(string.Join(", ", shuffled));
C#OrderBy のキーに Guid.NewGuid() を使って、「ランダムなキーでソートする → 結果としてシャッフルされる」という発想です。
動きとしては確かにシャッフルされます。
なぜ「業務ユーティリティ」としては微妙なのか
この書き方には、いくつか問題があります。
メモリと速度のコストが高い(全要素分 Guid を生成し、ソートする)
要素数が増えると、パフォーマンスが一気に悪くなる
「ソート」という本来別の目的の機能を、シャッフルに流用している
小さなリストなら問題になりませんが、業務で数万件・数十万件を扱うときには避けたい書き方です。
ここでの重要ポイントは、「OrderBy(Guid.NewGuid()) は“お手軽だけど重いシャッフル”」だと理解しておくことです。
サンプルコードならアリ、業務ユーティリティとして常用するのはナシ、くらいの感覚でいてください。
正統派シャッフル:フィッシャー–イェーツ(Fisher–Yates)法
アルゴリズムのイメージ
「ちゃんとしたシャッフル」として有名なのが、フィッシャー–イェーツ法です。
ざっくり言うと、
配列の末尾から順に、「そこに来るべき要素」をランダムに選んで入れ替えていく
というアルゴリズムです。
イメージとしては、こうです。
最後の位置に入る要素を、全体からランダムに選んで入れ替える
次に、最後から2番目の位置に入る要素を、残りの中からランダムに選んで入れ替える
…を先頭まで繰り返す
これで、「すべての並び順が同じ確率で出る」公平なシャッフルになります。
C# での実装例(拡張メソッド)
業務で使いやすいように、IList<T> に対する拡張メソッドとして書いてみます。
using System;
using System.Collections.Generic;
public static class ShuffleExtensions
{
private static readonly Random _random = new Random();
public static void ShuffleInPlace<T>(this IList<T> list)
{
if (list is null) throw new ArgumentNullException(nameof(list));
for (int i = list.Count - 1; i > 0; i--)
{
int j = _random.Next(i + 1); // 0 ~ i のどれか
// list[i] と list[j] を入れ替える
(list[i], list[j]) = (list[j], list[i]);
}
}
}
C#使い方はとてもシンプルです。
var list = new List<int> { 1, 2, 3, 4, 5 };
list.ShuffleInPlace();
Console.WriteLine(string.Join(", ", list));
C#ここでの重要ポイントは、次の3つです。
リストを「その場で」シャッフルしている(新しいリストを作らない)
ループは1回だけで、要素数 n に対して O(n) で終わるRandom を毎回 new せず、静的フィールドで共有している
特に Random を毎回 new するのは初心者がやりがちな罠で、
短時間に連続して呼ぶと「同じシードで初期化されて、同じ順番になる」ことがあります。
「新しいリストとしてシャッフル結果が欲しい」場合
元のリストを壊したくないとき
元のリストはそのまま残しておきたい、という場面も多いです。
その場合は、「コピーを作ってからシャッフル」します。
public static class ShuffleExtensions
{
private static readonly Random _random = new Random();
public static IList<T> Shuffled<T>(this IEnumerable<T> source)
{
if (source is null) throw new ArgumentNullException(nameof(source));
var list = new List<T>(source);
list.ShuffleInPlace();
return list;
}
public static void ShuffleInPlace<T>(this IList<T> list)
{
if (list is null) throw new ArgumentNullException(nameof(list));
for (int i = list.Count - 1; i > 0; i--)
{
int j = _random.Next(i + 1);
(list[i], list[j]) = (list[j], list[i]);
}
}
}
C#使い方はこうです。
var original = new List<int> { 1, 2, 3, 4, 5 };
var shuffled = original.Shuffled();
Console.WriteLine(string.Join(", ", original)); // 1, 2, 3, 4, 5
Console.WriteLine(string.Join(", ", shuffled)); // ランダムな順番
C#ここでの重要ポイントは、「ShuffleInPlace は破壊的変更、Shuffled は非破壊的」という役割分担です。
業務コードでは、「元データを壊してよいかどうか」を意識してメソッドを選ぶのが大事です。
実務でのシャッフルの使いどころと注意点
テストデータ・サンプリングでの利用
シャッフルは、テストや検証でとても役に立ちます。
大量データのうち、ランダムに 100 件だけ取りたい
テストケースの順番を毎回変えて、順序依存のバグをあぶり出したい
例えば、ランダムに 10 件だけ取りたいなら、こう書けます。
var sample = allData
.Shuffled()
.Take(10)
.ToList();
C#ここでの重要ポイントは、「シャッフル+Take で“ランダムサンプリング”が簡単に書ける」ということです。
ユーザー向け画面での利用(選択肢の順番など)
アンケートやクイズなどで、「選択肢の順番を毎回変えたい」という場面もあります。
var choices = new List<string>
{
"東京",
"大阪",
"名古屋",
"福岡"
};
choices.ShuffleInPlace();
// この順番で画面に表示する
C#ただし、ここで一つ注意があります。
「毎回違う順番になる」ことが本当に正しいかどうか、業務的に確認してください。
ログの追跡が難しくなる
ユーザーごとに順番が違うと、サポート時に話が噛み合わない
などの問題が出ることもあります。
乱数の「再現性」が必要な場合
負荷テストやシミュレーションでは、「同じシードで同じシャッフル結果を再現したい」ことがあります。
その場合は、Random を外から渡せるようにしておくと便利です。
public static void ShuffleInPlace<T>(this IList<T> list, Random random)
{
if (list is null) throw new ArgumentNullException(nameof(list));
if (random is null) throw new ArgumentNullException(nameof(random));
for (int i = list.Count - 1; i > 0; i--)
{
int j = random.Next(i + 1);
(list[i], list[j]) = (list[j], list[i]);
}
}
C#var rnd = new Random(12345);
var list1 = new List<int> { 1, 2, 3, 4, 5 };
var list2 = new List<int> { 1, 2, 3, 4, 5 };
list1.ShuffleInPlace(rnd);
list2.ShuffleInPlace(rnd); // 同じシードを使えば、同じ順番になる
C#ここでの重要ポイントは、「乱数のシードを制御すると、“同じランダム”を再現できる」ということです。
テストコードでは特に重要な考え方です。
まとめ:「シャッフルユーティリティ」は“順番に意味を持たせないための道具”
シャッフルの本質は、
「コレクションの順番に意味がある状態を、
あえて“意味のない順番”に壊すことで、公平さやランダム性を作る」
ことです。
押さえておきたいポイントは、
OrderBy(Guid.NewGuid()) はお手軽だが重いので、業務ユーティリティとしては避ける
フィッシャー–イェーツ法で O(n) の公平なシャッフルが書ける
破壊的シャッフル(ShuffleInPlace)と非破壊的シャッフル(Shuffled)を使い分ける
テストデータのサンプリングや、選択肢の順番ランダム化などに実務的な出番がある
乱数シードを制御すれば、「同じランダム」を再現できる
ここまで腹落ちしていれば、
「なんとなく OrderBy(Guid.NewGuid()) をコピペする」段階から卒業して、
“目的とコストを理解したうえで選べるシャッフルユーティリティ”を、自分のプロジェクトに組み込めるようになります。
