C# Tips | コレクション・LINQ:シャッフル

C# C#
スポンサーリンク

はじめに:「シャッフル」は“順番の意味を一度壊す”テクニック

シャッフルは、「コレクションの要素の順番をランダムに並べ替える」ことです。
トランプを切るイメージが一番近いです。

業務でも、実はちょこちょこ出番があります。

テストデータの順番をランダムにしたい
アンケートの選択肢を毎回違う順番で出したい
負荷テスト用にアクセス順をバラしたい

ここでは、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()) をコピペする」段階から卒業して、
“目的とコストを理解したうえで選べるシャッフルユーティリティ”を、自分のプロジェクトに組み込めるようになります。

タイトルとURLをコピーしました