派生クラス型・インターフェイス型・基底クラス型の違いとは?
ポリモーフィズムを引数設計から学ぼう
はじめに
C#でオブジェクトを扱うとき、次のように変数を宣言できます。
FoodiePet pet1 = new FoodiePet("エイミー"); // 具体クラス型
IVirtualPet pet2 = new FoodiePet("クー"); // インターフェイス型
VirtualPet pet3 = new FoodiePet("ライアン"); // 基底クラス型(※例として)
このように、変数の型をどこまで抽象化するかで、コードの柔軟性や設計の拡張性が大きく変わってきます。
今回は、ポリモーフィズムを理解する上で重要な「引数として渡すときの型指定」に注目し、インターフェイス型と基底クラス型の違いも含めて解説します。
1. まず押さえたい:変数の静的型の違い
型の種類 | 例 | 説明 |
---|---|---|
具体クラス型 | FoodiePet pet = … | クラス特有の機能も使えるが、他の型と混ぜにくい |
インターフェイス型 | IVirtualPet pet = … | 実装クラスを意識せず共通処理ができる |
基底クラス型 | VirtualPetBase pet = … | 実装の一部を共通化しつつ、共通処理も可能 |
2. よくある誤解:「varを使うとポリモーフィズムが効かない?」
var pet = new FoodiePet("エイミー"); // 型は FoodiePet に推論される
これは実質次と同じです:
FoodiePet pet = new FoodiePet("エイミー");
つまり、varはただの省略記法にすぎません。ポリモーフィズムにおいて重要なのは、右辺の型ではなく、変数の「静的型」です。
3. メソッドの引数で違いが出る
次のような共通メソッドがあったとしましょう。
void InteractWithPet(IVirtualPet pet)
{
pet.Play();
pet.Eat();
Console.WriteLine($"{pet.Name} の機嫌: {pet.Mood}, エネルギー: {pet.Energy}");
}
これは IVirtualPet を実装しているクラスであれば、すべて同じ処理で扱えます。つまり、ポリモーフィズムの力が引数設計に現れます。
✔ クラス特有の型(具象型)を使うと…
FoodiePet pet = new FoodiePet("エイミー");
pet.SniffFood(); // OK:FoodiePetにしかないメソッド
InteractWithPet(pet); // 暗黙的にIVirtualPetに変換されて渡される
具象型では特有の操作もできますが、他の種類と共通の操作で扱うには不向きです。
✔ インターフェイス型にすると…
IVirtualPet pet = new FoodiePet("クー");
// pet.SniffFood(); // コンパイルエラー:IVirtualPetに定義されていない
インターフェイスにない操作は封印され、「共通の使い方」だけが可能になります。
4. インターフェイスか基底クラスか:どう選ぶ?
🔷 インターフェイスを使うと…
- クラス間の共通の契約(=必ず持つメソッド)だけを定義
- 実装の共通部分は持てない(=すべてのクラスで書く必要あり)
- 多重継承が可能(複数のインターフェイスを実装できる)
🔷 抽象基底クラスを使うと…
abstract class VirtualPet
{
public string Name { get; protected set; }
public int Mood { get; set; }
public int Energy { get; set; }
public abstract void Eat();
public abstract void Play();
public virtual void Sleep()
{
Energy += 2;
Mood -= 1;
}
}
- 共通プロパティや実装の一部を基底クラスにまとめられる
- ただし継承は1つだけ(他のクラスを同時に継承できない)
5. ポリモーフィズム設計の実例
List<IVirtualPet> pets = new List<IVirtualPet>
{
new FoodiePet("エイミー"),
new SleepyPet("ライアン"),
new CheerfulPet("クー")
};
foreach (var pet in pets)
{
InteractWithPet(pet); // 見た目は同じ、動作は異なる
}
InteractWithPet の引数が IVirtualPet であることで、種類の違うペットも一貫して扱えるのです。
6. まとめ
比較項目 | 具体クラス型 | インターフェイス型 | 基底クラス型 |
---|---|---|---|
特有メソッドの使用 | できる | できない | できない(抽象型に依存) |
共通処理の利用 | △(制限あり) | ◎ | ◎ |
実装の共通化 | × | × | ◎ |
複数継承の可否 | ― | ◎ | × |
柔軟性・拡張性 | 低 | 高 | 中〜高 |
✅ 練習課題
- IVirtualPet を抽象クラス VirtualPet に置き換えてみよう
- InteractWithPet をオーバーロードして、基底クラス版も用意してみよう
- 特定のペットクラスに専用機能(例:SniffFood())を加え、それが使える型と使えない型を確認しよう
以下は ✅ 練習課題 1〜3 をすべて満たす「完成形サンプル」です。
Main メソッドを含む 1 ファイルにまとめてあるので、そのまま dotnet run/Visual Studio でビルド→実行できます。
using System;
using System.Collections.Generic;
#region 1. インターフェイスを抽象基底クラスへ置き換え
// ― 共通のデータと振る舞いを持たせたいので abstract class に変更
abstract class VirtualPet
{
public string Name { get; protected set; }
public int Mood { get; protected set; } // 0(不機嫌)~10(ご機嫌)
public int Energy { get; protected set; } // 0(疲労) ~10(元気)
protected VirtualPet(string name)
{
Name = name;
Mood = 5;
Energy = 5;
}
public abstract void Eat(); // 各派生クラスごとに具体実装
public abstract void Play(); // 同上
// 全ペット共通の既定動作は virtual で用意
public virtual void Sleep()
{
Energy = Math.Min(Energy + 2, 10);
Mood = Math.Max(Mood - 1, 0);
Console.WriteLine($"{Name} は眠って回復した(Energy↑ / Mood↓)");
}
}
#endregion
#region 派生クラス
class FoodiePet : VirtualPet
{
public FoodiePet(string name) : base(name) { }
public override void Eat () { Energy = Math.Min(Energy + 3, 10); }
public override void Play() { Energy--; Mood++; }
// 3. 専用機能(SniffFood)は FoodiePet だけが持つ
public void SniffFood()
{
Console.WriteLine($"{Name} は食べ物の匂いをかいでワクワクしている!");
Mood = Math.Min(Mood + 1, 10);
}
}
class SleepyPet : VirtualPet
{
public SleepyPet(string name) : base(name) { }
public override void Eat () { Energy += 2; }
public override void Play() { Energy--; Mood--; }
}
class CheerfulPet : VirtualPet
{
public CheerfulPet(string name) : base(name) { }
public override void Eat () { Energy += 1; Mood += 2; }
public override void Play() { Energy--; Mood += 2; }
}
#endregion
class Program
{
// 2-A. 基底クラス版(共通ロジック)
static void InteractWithPet(VirtualPet pet)
{
pet.Play();
pet.Eat();
Console.WriteLine(
$"[{pet.GetType().Name}] {pet.Name} → Mood:{pet.Mood}, Energy:{pet.Energy}");
}
// 2-B. オーバーロード:FoodiePet 専用ロジックを追加
static void InteractWithPet(FoodiePet pet)
{
pet.SniffFood(); // 派生クラス特有の機能
InteractWithPet((VirtualPet)pet); // 共通処理へフォールバック
}
static void Main()
{
// ★ 動作確認シナリオ
var pets = new List<VirtualPet>
{
new FoodiePet ("エイミー"), // FoodiePet
new SleepyPet ("ライアン"), // SleepyPet
new CheerfulPet("クー") // CheerfulPet
};
foreach (var p in pets)
{
// 静的型が VirtualPet なので SniffFood は見えない
// p.SniffFood(); // ← コンパイル エラーになることを確認 ◎
// 動的型に応じて最適なオーバーロードが選択される
InteractWithPet(p);
Console.WriteLine();
}
// 静的型を FoodiePet にすると SniffFood が呼べる
FoodiePet foodie = new FoodiePet("ミント");
foodie.SniffFood(); // OK
InteractWithPet(foodie); // FoodiePet 版 → 基底クラス版へ
}
}
使い方と確認ポイント
課題 | 具体的にやったこと | 期待できる学習効果 |
---|---|---|
1 | IVirtualPet → VirtualPet (abstract class) に変更し、共通プロパティを集約 | 「共通実装を基底クラスにまとめる利点」を体感 |
2 | InteractWithPet(VirtualPet pet) と InteractWithPet(FoodiePet pet) の オーバーロード を追加 | 「静的型解決 vs 動的ディスパッチ」「メソッド選択のルール」を実践で確認 |
3 | FoodiePet.SniffFood() を独自追加し、変数の静的型違いで呼べる/呼べないを比較 | 「型を抽象化すると専用機能は隠れる」= 設計上のトレードオフを理解 |
実行例(抜粋)
エイミー は食べ物の匂いをかいでワクワクしている!
[FoodiePet] エイミー → Mood:7, Energy:7
[SleepyPet] ライアン → Mood:4, Energy:6
[CheerfulPet] クー → Mood:8, Energy:5
ミント は食べ物の匂いをかいでワクワクしている!
[FoodiePet] ミント → Mood:7, Energy:7
- InteractWithPet には 静的型 が VirtualPet の変数しか渡していないため SniffFood は呼べません。
- FoodiePet 専用オーバーロードがあることで、実際のオブジェクトが FoodiePet のときだけ追加処理 を注入できています。
✍ まとめ
ポリモーフィズムの基本は「共通の使い方」で異なる中身を動かすこと。
そのためにインターフェイスや抽象基底クラスで型を設計することが大切です。
varを使うかどうかではなく、変数の静的型をどうするかが、設計の分かれ道です。
ディスカッション
コメント一覧
まだ、コメントがありません