1. はじめに
抽象クラスってのは、newで直接インスタンス化できないクラスのこと。これは他の、もっと具体的なクラスを作るためのベースになるやつ。いわばテンプレートとか「設計図」みたいなもので、派生クラスに共通の構造や振る舞いを決めるけど、細かい実装は子クラスに任せる感じ。
イメージしやすいように例えると、「乗り物」っていうコンセプトがあるとする。現実世界で「抽象的な乗り物」に乗る人はいないよね(想像の中なら別だけど)。誰かは車に乗るし、誰かは船で泳ぐし、誰かは飛行機で飛ぶ。抽象クラスは「乗り物」っていう設計図で、どんな乗り物にも「移動できる」(Move)っていうキーな機能が必要だよ、って決めてる。でもどうやって移動するかは子クラスで決めるってわけ。
// 抽象クラスの例
public abstract class Transport
{
public string Model { get; set; }
// 抽象メソッド - 実装なし
public abstract void Move();
}
Transport型のオブジェクトを作ろうとすると、コンパイルエラーになるよ!試してみて、コンパイラが「抽象クラスのインスタンスは作れません」って怒ってくるから。
抽象クラスっていつ・なんで使うの?
- いろんな似てるけど違うオブジェクトの共通の特徴や振る舞いをまとめたいとき。
- 「みんなこれだけはできてね」っていう「共通の契約」を決めたいとき。例:「全員移動できること」
- 一部のメソッドは共通実装したいけど、細かいロジックは子クラスで決めたいとき。
抽象クラスは、大きなシステムの設計で「みんなに共通の機能はここ」「細かい部分は各自で」って分けたいときにめっちゃ便利なツールだよ。
2. 抽象クラスと普通のクラスの違い
ここはよく質問が出るから、ちゃんと整理しとくね!
抽象クラス vs 普通のクラス
- 普通のクラスはnewでインスタンス化できるし、全部のメソッドに実装がある。
- 抽象クラスは:
- 直接作れない(クラス宣言にabstractが必要)
- 実装済みメソッドも抽象メソッド(本体なし)も持てる
- フィールド、プロパティ、コンストラクタ、staticメソッドも持てる
public abstract class Animal
{
public string Name { get; set; }
public void Sleep()
{
Console.WriteLine("動物が寝てるよ!");
}
public abstract void MakeSound();
}
3. 抽象メソッドとプロパティ
抽象メソッド
抽象メソッドは抽象クラスの中で宣言するけど、本体(実装)は書かない。子クラスが自分なりに必ず実装しなきゃダメ。
public abstract class Animal
{
public string Name { get; set; }
// 抽象メソッド
public abstract void MakeSound();
}
抽象プロパティ
抽象プロパティも宣言できて、全ての子クラスで必ず実装しなきゃいけないってルールにできるよ:
public abstract class Shape
{
// 抽象プロパティ
public abstract double Area { get; }
}
4. 抽象クラスの継承
抽象クラスは抽象メソッドも実装済みメソッドも持てる。子クラスは全部の抽象メソッド・プロパティを実装しないと、自分も抽象クラスになっちゃう。
じゃあ、前のレクチャーで作った「動物アプリ」を発展させてみよう:
public abstract class Animal
{
public string Name { get; set; }
public void Sleep()
{
Console.WriteLine($"{Name}はぐっすり寝てるよ!");
}
public abstract void MakeSound();
}
今度は普通の(抽象じゃない)クラスを作ってみる:
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine($"{Name}が言う:ワン!");
}
}
MakeSound()を実装しないでみて—コンパイルエラーになるのが分かるよ。
図:抽象クラスを使ったクラスの階層
+---------------------+
| Animal (abstract) |
+---------------------+
| Name: string |
| + Sleep() |
| + MakeSound()* |
+---------------------+
^
|
+------+-------+
| |
Dog Cat
* MakeSound() — 抽象メソッド
5. ポリモーフィズム実践:抽象化がどう動くか
じゃあ、これをミニアプリで使ってみよう。動物のリストがあって、犬と猫がいるとする。
List<Animal> animals = new List<Animal>
{
new Dog { Name = "シャリク" },
new Cat { Name = "バルシク" }
};
foreach (var animal in animals)
{
animal.MakeSound(); // ポリモーフィックな呼び出し:それぞれの実装が動く
animal.Sleep(); // 基底クラスで実装されたメソッドが呼ばれる
}
Catクラスも同じ感じで作るよ:
public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine($"{Name}が言う:ニャー!");
}
}
実際のプロジェクトでのメリット
抽象クラスは大規模アプリのアーキテクチャでよく使われるよ。例えばグラフィックエディタだと、Shapeって抽象クラスがあって、Draw()とかAreaみたいなメソッドやプロパティがある。具体的な図形(四角とか円とか)はそれぞれ自分の計算式で抽象メソッドを実装する。こうやっておけば、いろんな図形を追加してもプログラムのロジックを全部書き直さなくて済むんだ。
6. 抽象クラスの特徴と制限
インスタンス化できない
コンパイラはこんなこと絶対許してくれないよ:
Animal a = new Animal(); // エラー!
インスタンス化できるのは派生(具体的な)クラスだけ。
抽象クラスに含められるもの:
- 抽象メソッドの宣言(実装なし)
- 実装済みメソッド(本体あり)
- プロパティやフィールド(抽象でも実装済みでもOK)
- コンストラクタ — 抽象クラスにもコンストラクタは書けるけど、呼べるのは子クラスだけ。直接インスタンス化はできない。
抽象クラスは継承できる
抽象化の階層をどんどん作れるよ:
public abstract class Creature
{
public abstract void Live();
}
public abstract class Animal : Creature
{
public abstract override void Live();
public abstract void MakeSound();
}
継承チェーンの中で、全部の抽象メソッドを実装しないクラスは、自分も抽象クラスにしなきゃダメ!
7. 抽象クラスとコンストラクタ
抽象クラスにもコンストラクタは書けて、それは子クラスのコンストラクタから呼ばれるよ。
ポイント: 抽象クラスのコンストラクタはたいていprotectedで宣言する。これは「継承先だけが使うもの」って意味をはっきりさせるため。
public abstract class Animal
{
public string Name { get; set; }
// コンストラクタはprotectedで宣言
protected Animal(string name)
{
Name = name;
Console.WriteLine($"{name}という動物が作られた");
}
public abstract void MakeSound();
}
public class Dog : Animal
{
public Dog(string name) : base(name)
{
// base(name)でAnimalのコンストラクタを呼ぶ
Console.WriteLine("犬が作られた");
}
public override void MakeSound()
{
Console.WriteLine($"{Name}が言う:ワン!");
}
}
なんでprotectedコンストラクタを使うの?
- 意図が明確: protectedだと「これは継承先専用」って分かりやすい
- ミス防止: newでインスタンス化はそもそもできないけど、protectedにしておくとAPIレベルでも意図が伝わるし、誤解の余地がなくなる
- 良い習慣: カプセル化や明確なAPI設計のためにもおすすめ
publicコンストラクタも書けるけど、protectedの方が用途に合ってることが多いよ。
8. 抽象メソッドとvirtualメソッドの比較
抽象メソッドはvirtualメソッドの一種(実際は常にvirtualみたいなものだけど、キーワードは使わない)。でも実装は書けない。もし「子クラスでできればオーバーライドしてほしい(でも必須じゃない)」ならvirtualを使う。「絶対オーバーライドして!」ならabstractだけ。
public abstract class Worker
{
// 全員必ず実装
public abstract void Work();
// オーバーライドしてもいいし、しなくてもいい
public virtual void Rest()
{
Console.WriteLine("デフォルトの休憩!");
}
}
| 抽象メソッド | virtualメソッド | |
|---|---|---|
| 基底クラスでの実装有無 | なし(シグネチャだけ、実装なし) | あり(デフォルト実装) |
| どこで宣言できるか | 抽象クラスだけ | sealedじゃないクラスならどこでも(抽象クラスもOK) |
| 子クラスでのオーバーライド必須か | 最初の非抽象クラスで必須 | 必須じゃない。オーバーライドしてもしなくてもOK |
| 直接呼び出せるか | 不可(本体がない) | 可(基底バージョンが動く) |
| 主な目的 | 子クラスに独自実装を必ずさせる。「契約」を決める | 必要なら子クラスで振る舞いを変えられるようにする |
9. プロジェクトで抽象クラスを使う
学習アプリをさらに発展させよう。今度はユーザー入力を処理する抽象クラス(いろんな種類のフォーム用)を作るとする。
public abstract class InputForm
{
public string Title { get; set; }
// 入力処理を実装する抽象メソッド
public abstract void ShowAndHandleInput();
}
public class LoginForm : InputForm
{
public override void ShowAndHandleInput()
{
Console.WriteLine($"=== {Title} ===");
Console.Write("ログインを入力して: ");
string login = Console.ReadLine();
Console.Write("パスワードを入力して: ");
string password = Console.ReadLine();
Console.WriteLine("認証が完了したよ!");
}
}
public class RegistrationForm : InputForm
{
public override void ShowAndHandleInput()
{
Console.WriteLine($"=== {Title} ===");
Console.Write("ログインを考えて: ");
string login = Console.ReadLine();
Console.Write("パスワードを考えて: ");
string password = Console.ReadLine();
Console.WriteLine("登録が完了したよ!");
}
}
使い方:
InputForm[] forms = new InputForm[]
{
new LoginForm { Title = "ログイン" },
new RegistrationForm { Title = "登録" }
};
foreach (var form in forms)
{
form.ShowAndHandleInput();
}
このやり方は、プログラムが大きくなるほど強力になるよ。抽象クラスは拡張しやすいシステムの土台になるし、DRYやSOLID(繰り返し禁止、モジュール化、拡張しやすさ重視)っていう原則とも相性バッチリ!
10. 抽象クラス・メソッドでよくあるミス
ミスその1:抽象メソッドの実装忘れ。
子クラスで全部の抽象メソッド・プロパティを実装しないと、コンパイラがエラー出すよ。特に他人のコードや大きな階層でよくやりがち。大事なやつを見落としてないかちゃんとチェックしよう。
ミスその2:abstractとstaticを同時に使おうとする。
これは無理。抽象メソッドはインスタンスで実装するもの、staticはインスタンス不要。両立できないから、コンパイラが絶対許してくれない。
ミスその3:「全部実装するのめんどい」って抽象メンバーを放置。
全部の抽象メソッドを実装したくないなら、そのクラスもabstractにしなきゃダメ。これはエラーじゃないけど、設計がごちゃごちゃしてるサインかも。部分実装を中間の抽象クラスにまとめると、子クラスが楽になることもあるよ。
GO TO FULL VERSION