トランプで学ぶ!オブジェクト指向プログラミング入門(C#サンプル付き)

はじめに

オブジェクト指向(OOP)は、プログラミングの重要な考え方ですが、初心者には「抽象的でよくわからない」と感じられがちです。

そこで本記事では、誰もが知っている「トランプ」を題材にして、クラス・インスタンス・継承・ポリモーフィズムなどのOOPの概念を、実際にコードを動かしながら理解していきます。


🧠 この記事で学べること

  • クラスとインスタンスの関係(設計図と実物)
  • 継承の考え方(ジョーカーは特殊なカード)
  • ポリモーフィズム(共通型で異なる振る舞い)
  • 実践的なOOP設計(Deckクラスや表示処理)
  • 応用:FaceCardやAceCardの実装、クラス分離

プロジェクト名 DeckBuilder

1. クラス:カードという設計図

class Card
{
    public string Suit { get; set; }  // ♠, ♥, ♦, ♣
    public int Number { get; set; }   // 1〜13

    public virtual void Show()
    {
        Console.WriteLine($"{Suit} の {Number}");
    }
}
  • Card は「トランプの1枚」の設計図
  • まだこの時点では実物は存在しない

2. 実物を作る:インスタンス生成

Card card1 = new Card { Suit = "♠", Number = 1 };
Card card2 = new Card { Suit = "♦", Number = 13 };
  • card1 や card2 はクラスから作られた実体(インスタンス)
  • 52枚生成すれば、トランプ1組が完成

3. ジョーカーの追加:継承の活用

class Joker : Card
{
    public bool IsBlack { get; set; }

    public override void Show()
    {
        string color = IsBlack ? "黒" : "赤";
        Console.WriteLine($"{color}ジョーカー");
    }
}
  • Joker は Card を継承し、独自プロパティ動作を追加
  • 共通点を活かしつつ、特殊な振る舞いも実装できる

4. 全体のデッキを作る(Listで管理)

List<Card> deck = new List<Card>();
string[] suits = { "♠", "♥", "♦", "♣" };

// 通常カード52枚
foreach (var suit in suits)
{
    for (int i = 1; i <= 13; i++)
        deck.Add(new Card { Suit = suit, Number = i });
}

// ジョーカー2枚
deck.Add(new Joker { IsBlack = true });
deck.Add(new Joker { IsBlack = false });

5. ポリモーフィズム:共通の型でまとめて処理

foreach (var card in deck)
{
    card.Show(); // JokerでもCardでもOK
}
  • Card 型でまとめられているのに、実際には中身に応じて Show() の動作が切り替わる
  • これがポリモーフィズム(多態性)

6. Deckクラスで役割分離(SRP)

class Deck
{
    private List<Card> cards = new List<Card>();

    public Deck()
    {
        string[] suits = { "♠", "♥", "♦", "♣" };
        foreach (var suit in suits)
        {
            for (int i = 1; i <= 13; i++)
                cards.Add(new Card { Suit = suit, Number = i });
        }
        cards.Add(new Joker { IsBlack = true });
        cards.Add(new Joker { IsBlack = false });
    }

    public void Shuffle()
    {
        var rnd = new Random();
        cards = cards.OrderBy(c => rnd.Next()).ToList();
    }

    public Card Draw()
    {
        if (cards.Count == 0) return null;
        var top = cards[0];
        cards.RemoveAt(0);
        return top;
    }

    public int Count => cards.Count;
}

OrderBy(c => Guid.NewGuid()) のラムダ式 c => Guid.NewGuid() は、要素 c を受け取るものの 無視 して毎回新しい GUID を生成し、それをソートキーとして返す関数です。

以下のような流れでシャッフル(ランダム並べ替え)が実現されます。

  1. キーの生成
    • OrderBy は内部でシーケンスの各要素にキーを1回ずつ計算します。
    • ここでは要素ごとに Guid.NewGuid() が呼ばれ、128ビットのほぼ一意な値(GUID)が生成される。
  2. ソート処理
    • 生成された GUID を「昇順」で比較し、要素を並べ替える。
    • GUID はランダム性が高いため、結果として元の順序とは関係のないランダムな並び順となる。
  3. 結果の確定
    • ToList() を呼び出すと、並べ替え後のシーケンスが List<T> にコピーされ、シャッフル済みのリストが得られる。
var shuffled = cards
    .OrderBy(c => Guid.NewGuid())  // 各要素にランダムな GUID を割り当ててソート
    .ToList();                     // 結果をリスト化

ポイント

  • GUID の一意性
    • Guid.NewGuid() はほぼ重複しない値を生成するため、キーの衝突(重複)が極めて起こりにくい。
  • 実装の簡潔さ
    • 他のシャッフル実装(Fisher–Yates など)と比べてコードがシンプル。
  • コスト
    • GUID の生成コスト + ソート処理(O(n log n))がかかるため、大量要素ではやや重い。

まとめ

c => Guid.NewGuid() をキーセレクタに渡すことで、各要素にユニークかつランダムなキーを割り当て、そのキーでソートする――つまり 簡易的にシャッフル するトリックとしてよく使われます。

パフォーマンス重視なら Fisher–Yates アルゴリズム(O(n))も検討してみてください。

ランダムソート(シャッフル)とは、リストなどの要素を“ランダムな順序”に並べ替える操作です。LINQ の OrderBy を活用した方法から、古典的な Fisher–Yates アルゴリズムまで、代表的な手法をまとめて解説します。


1. LINQ+乱数キーでのシャッフル

1.1 rnd.Next() をキーに使う

var rnd = new Random();
cards = cards
    .OrderBy(c => rnd.Next())  // 各要素ごとに乱数をキーとして生成
    .ToList();
  • 仕組み
    • 要素ごとに rnd.Next() を呼び出し、その返り値(0 以上の乱数)をソートキーにする
    • ソート処理(O(n log n))によって並べ替え
  • メリット/デメリット
    • 簡潔に書ける
    • 乱数キーが重複する可能性がある(とはいえ偏りは小さい)
    • 大量要素だとソートコストが高い

1.2 Guid.NewGuid() をキーに使う

cards = cards
    .OrderBy(c => Guid.NewGuid()) // 毎回新規 GUID を生成してキーにする
    .ToList();
  • 仕組み
    • ラムダ式 c => Guid.NewGuid() は要素 c を無視して「一意な GUID」を毎回返す
    • GUID はほぼ重複しないため、キー衝突がほぼ起きず強力にシャッフル
  • メリット/デメリット
    • コードが最もシンプル
    • GUID 生成コスト+ソートコスト(O(n log n))がかかる
    • 小〜中規模のリストであれば実用的

2. インデックス列をシャッフルして再構築

var rnd = new Random();
var indices = Enumerable.Range(0, cards.Count)
                        .OrderBy(_ => rnd.Next())  // 重複なしのインデックスをソート
                        .ToArray();

cards = indices
    .Select(i => cards[i])       // シャッフル済みインデックス順に要素を取り出し
    .ToList();
  • 仕組み
    1. 0,1,2… といったユニークなインデックス列を生成
    2. その列をランダムキーでソート → 重複のないシャッフル
    3. 元リストをインデックス指定で再配置
  • メモリ:中間のインデックス配列が必要
  • コスト:O(n log n)

3. Fisher–Yates(Knuth)アルゴリズム

var rnd = new Random();
for (int i = cards.Count - 1; i > 0; i--)
{
    int j = rnd.Next(i + 1);   // 0 ~ i の範囲でランダム
    var tmp = cards[i];
    cards[i]  = cards[j];
    cards[j]  = tmp;
}
  • 仕組み
    • リストの末尾から順に、ランダムに選んだ前方の要素と交換していく
  • 特徴
    • 計算量 O(n):一度の走査で完了
    • インプレース(追加メモリ不要)
    • 均等で偏りの少ないシャッフル

4. 比較まとめ

方法実装の簡単さ時間計算量メモリ使用量シャッフル品質
OrderBy(rnd.Next())★★★O(n log n)
OrderBy(Guid.NewGuid())★★★O(n log n)非常に良い
インデックス列+OrderBy★★O(n log n)
Fisher–Yates★★O(n)低(インプレース)非常に良い

5. 選び方のポイント

  • 簡潔さ重視 → OrderBy(Guid.NewGuid())
  • パフォーマンス重視 → Fisher–Yates(大量要素やリアルタイム性が必要な場合)
  • メモリトレードオフ → インデックス列方式は中間配列が許容できるならあり

用途やデータ規模に応じて、上記のいずれかを使い分けてみてください。


使用例

Deck deck = new Deck();
deck.Shuffle();

for (int i = 0; i < 5; i++)
{
    var card = deck.Draw();
    card?.Show();
}
  • 責任の分離(カードとデッキは別のクラス)
  • 「シャッフル」「配布」などの機能は Deck 側に集約

7. 応用:FaceCard や AceCard を派生クラスで表現

class FaceCard : Card
{
    public string Face { get; set; }

    public override void Show()
    {
        Console.WriteLine($"{Suit} の {Face}(フェイスカード)");
    }
}

class AceCard : Card
{
    public override void Show()
    {
        Console.WriteLine($"{Suit} の A(エース)");
    }
}

デッキ作成時に使い分ける

for (int i = 1; i <= 13; i++)
{
    if (i == 1)
        cards.Add(new AceCard { Suit = suit, Number = i });
    else if (i >= 11)
    {
        string face = i switch
        {
            11 => "J", 12 => "Q", 13 => "K", _ => ""
        };
        cards.Add(new FaceCard { Suit = suit, Number = i, Face = face });
    }
    else
    {
        cards.Add(new Card { Suit = suit, Number = i });
    }
}

📝 まとめ

OOP用語トランプの比喩
クラスカードの設計図(SuitとNumber)
インスタンス実際のカード1枚
継承Joker や FaceCard などの特殊カード
オーバーライドShow() の動作をそれぞれのカードで変更
ポリモーフィズム共通型 Card としてまとめて扱える
責任分離Deck クラスに配布やシャッフルを分離

💡 おすすめの学習ステップ

  1. Cardクラス → Jokerクラス → List管理(OOP入門)
  2. Deckクラス導入 → 機能の分離(設計力の基礎)
  3. FaceCard・AceCard → 多態性と設計拡張
  4. 応用:ババ抜き、神経衰弱など簡単なカードゲームへ発展

📁 プロジェクト構成(参考)

TrumpOOP/
├── Program.cs         // メイン処理
├── Card.cs            // 基本カード
├── Joker.cs           // 継承例
├── FaceCard.cs        // 継承 + オーバーライド
├── AceCard.cs         // 継承 + 表示変更
├── Deck.cs            // デッキ管理
└── TrumpOOP.csproj

🏁 最後に

「クラスって何のためにあるの?」

「継承って本当に必要?」

そんな疑問を持つ人にこそ、「トランプをプログラムで再現してみる」ことは、OOPの本質を“自分の手”で感じる絶好のチャンスです。

カード1枚から、システム全体の構成へ。

シンプルな世界に、オブジェクト指向のすべてが詰まっています。


訪問数 15 回, 今日の訪問数 1回