トランプで学ぶ!オブジェクト指向プログラミング入門(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 を生成し、それをソートキーとして返す関数です。
以下のような流れでシャッフル(ランダム並べ替え)が実現されます。
- キーの生成
- OrderBy は内部でシーケンスの各要素にキーを1回ずつ計算します。
- ここでは要素ごとに Guid.NewGuid() が呼ばれ、128ビットのほぼ一意な値(GUID)が生成される。
- ソート処理
- 生成された GUID を「昇順」で比較し、要素を並べ替える。
- GUID はランダム性が高いため、結果として元の順序とは関係のないランダムな並び順となる。
- 結果の確定
- 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();
- 仕組み
- 0,1,2… といったユニークなインデックス列を生成
- その列をランダムキーでソート → 重複のないシャッフル
- 元リストをインデックス指定で再配置
- メモリ:中間のインデックス配列が必要
- コスト: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 クラスに配布やシャッフルを分離 |
💡 おすすめの学習ステップ
- Cardクラス → Jokerクラス → List管理(OOP入門)
- Deckクラス導入 → 機能の分離(設計力の基礎)
- FaceCard・AceCard → 多態性と設計拡張
- 応用:ババ抜き、神経衰弱など簡単なカードゲームへ発展
📁 プロジェクト構成(参考)
TrumpOOP/
├── Program.cs // メイン処理
├── Card.cs // 基本カード
├── Joker.cs // 継承例
├── FaceCard.cs // 継承 + オーバーライド
├── AceCard.cs // 継承 + 表示変更
├── Deck.cs // デッキ管理
└── TrumpOOP.csproj
🏁 最後に
「クラスって何のためにあるの?」
「継承って本当に必要?」
そんな疑問を持つ人にこそ、「トランプをプログラムで再現してみる」ことは、OOPの本質を“自分の手”で感じる絶好のチャンスです。
カード1枚から、システム全体の構成へ。
シンプルな世界に、オブジェクト指向のすべてが詰まっています。
ディスカッション
コメント一覧
まだ、コメントがありません