1. はじめに
初心者が継承って聞くと、「なんか簡単そう!既存クラスをちょっと拡張すればOKでしょ?」って思いがち。でも実際は細かい罠がいっぱい。継承でのミスは色々な形で現れるよ:シグネチャ間違い、キーワードの付け忘れ、イケてない階層設計…全部バグの温床になる。
場合によってはすぐエラーでコンパイルできないこともあるし、逆にランタイムでしか気づかないバグもある。たとえば新しいSuperMegaLoggerが全然違うことを書き出したり、何も出力しなかったり。悲しみ、ガッカリ、デバッグ地獄…あるあるだよね?
一緒に継承やオーバーライドでやりがちなミスを見ていって、コードを「健康」にしていこう!
2. virtualの付け忘れ:なぜメソッドがオーバーライドできないの?
問題
C#では、overrideできるのは、親クラスでvirtualかabstract、または継承チェーンでoverrideされてるメソッドだけ。もしそのキーワードがなかったら、子クラスでoverrideしようとするとコンパイルエラーになる。
class Animal
{
public void Speak()
{
Console.WriteLine("動物が何かを話している。");
}
}
class Cat : Animal
{
// コンパイルエラー!親クラスのメソッドがvirtualでもabstractでもoverrideでもない。
public override void Speak()
{
Console.WriteLine("ニャー!");
}
}
エラー例: "'Cat.Speak()': cannot override inherited member 'Animal.Speak()' because it is not marked virtual, abstract, or override"
どうやって回避する?
メソッドをオーバーライドしたいなら、親クラスで必ずvirtualを付けよう。クラスを拡張する設計をするなら、どのメソッドをオーバーライドさせたいかもちゃんと考えておくといいよ。
class Animal
{
public virtual void Speak()
{
Console.WriteLine("動物が音を出す。");
}
}
class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("ニャー!");
}
}
なんで必要なの?
virtualを付けることで、子クラスがそのメソッドの動作を変えていいよって明示できるし、コンパイラも間違ったオーバーライドを防いでくれる。
3. overrideとnewのキーワードミス
逆に、子クラスで「オーバーライドしたい!」と思っても、親クラスのメソッドがvirtualじゃなかったら、overrideは使えない。でもnewは使える。ただし、これは全然違う動作になる!
class Dog : Animal
{
// これはoverrideじゃなくて、親クラスのメソッドを隠す(hiding)だけ。
public new void Speak()
{
Console.WriteLine("ワン!");
}
}
このメソッドをDog型の参照で呼ぶとOK:
Dog dog = new Dog();
dog.Speak(); // "ワン!"
でも親クラス型の参照で呼ぶと、親のメソッドが呼ばれる:
Animal dog2 = new Dog();
dog2.Speak(); // "動物が音を出す。"
解説:
newはメソッドをオーバーライドするんじゃなくて、親のメソッドを隠すだけ。これを"hiding"って呼ぶ。ポリモーフィズムが期待通りに動かなくて混乱しやすいから注意!
newとoverrideの罠を避けるには?
ポリモーフィズムをちゃんと使いたいなら、virtual/overrideを使おう。まったく新しい機能を追加したい(または意図的に親の動作を隠したい)場合だけnewを使う(でも慎重に!)。
| 状況 | キーワード | ポリモーフィズム動作? | 親型参照での動作 |
|---|---|---|---|
| 動作を変えたい | override | はい | 子クラスのメソッドが呼ばれる |
| メソッドを隠す/置き換える | new | いいえ | 親クラスのメソッドが呼ばれる |
4. メソッドシグネチャの不一致
初心者だけじゃなくてベテランでも、子クラスでパラメータや戻り値の型を変えちゃうミスはよくある。たとえば親でpublic virtual void Print(string message)なのに、子でpublic override void Print(object message)ってやると、これはもうoverrideじゃなくて新しいメソッド定義になっちゃう。
class Printer
{
public virtual void Print(string msg)
{
Console.WriteLine("ベースプリンター: " + msg);
}
}
class SmartPrinter : Printer
{
// コンパイルエラー!シグネチャが親と一致してない。
public override void Print(object msg)
{
Console.WriteLine("スマートプリンター: " + msg);
}
}
ヒント:
メソッド名、戻り値の型、パラメータ(型・数・順番)全部一致してないとダメ。
もしパラメータの型をうっかり変えたり、名前をミスったら、コンパイラがちゃんと警告してくれるよ。
5. アクセス修飾子の不一致
もう一つの落とし穴はアクセス修飾子。子クラスのメソッドは、親よりも「閉じた」アクセスにはできない。ダメな例:
public class Vehicle
{
public virtual void StartEngine() { /* ... */ }
}
public class Car : Vehicle
{
// エラー!'private'は親の'public'より厳しい。
private override void StartEngine() { /* ... */ }
}
どうする?
子クラスのメソッドのアクセス修飾子は、親と同じか、もっと「開いた」ものにしよう。たいていはpublicかprotectedになる。
6. 抽象メソッドの未実装/余計なabstract
親クラスでabstractメソッドを宣言したら、子クラスで必ずoverrideしないといけない。しないと、そのクラスも抽象クラスになってインスタンス化できなくなる。
abstract class Shape
{
public abstract double Area();
}
class Circle : Shape
{
// エラー!抽象メソッドArea()が未実装
}
解決:
ちゃんと実装しよう:
class Circle : Shape
{
public override double Area()
{
return 3.14 * 2 * 2; // だいたいね…
}
}
7. 親の実装呼び出し:base.Method()
メソッドの実装を完全に置き換えるんじゃなくて、追加したいだけのとき、子クラスで親の実装をbaseで呼べるって知らない人も多い。
class Logger
{
public virtual void Log(string msg)
{
Console.WriteLine("ベースログ: " + msg);
}
}
class FancyLogger : Logger
{
public override void Log(string msg)
{
// ちょっと装飾してから親のメソッドを呼ぶ:
Console.WriteLine("[FANCY] " + msg);
base.Log(msg);
}
}
解説:
base.Log(msg)を呼ばないと、親クラスのロジックが完全に消えちゃうから注意。
8. 親コンストラクタの呼び出し(base)
親クラスのコンストラクタに必須パラメータがある場合、子クラスでbaseを使って明示的に呼ばないといけない。
class Engine
{
public Engine(int cylinders)
{
Console.WriteLine("シリンダー数: " + cylinders + "のエンジン");
}
}
class RaceEngine : Engine
{
// コンパイルエラー!Engineにデフォルトコンストラクタがない。
public RaceEngine() { }
}
// 修正版:
class RaceEngine2 : Engine
{
public RaceEngine2() : base(8) // 親コンストラクタを明示的に呼ぶ
{
Console.WriteLine("レース用エンジン準備OK!");
}
}
9. sealedの付け忘れ
子クラスでこれ以上オーバーライドさせたくないときは、C#のsealedキーワードをoverrideと一緒に使う。これを忘れると、誰かがさらにオーバーライドして、想定外の動作になるかも。
class Hero
{
public virtual void Attack() => Console.WriteLine("ヒーローが攻撃!");
}
class Warrior : Hero
{
public sealed override void Attack() => Console.WriteLine("戦士が一撃!");
}
class Mutant : Warrior
{
// エラー!Attackはsealedされてる。
// public override void Attack() { ... }
}
解説:
こうやって、そのレベルで実装を「封印」できて、下のクラスで変更できなくなる。
10. オーバーロードとオーバーライドの混同
たまにオーバーロード(overloading)とオーバーライド(overriding)を混同しちゃう人がいる。オーバーロードは同じ名前でシグネチャ違いのメソッドを作ること。ポリモーフィズムとは関係ないよ。
class Animal
{
public virtual void Eat()
{
Console.WriteLine("動物が食べる。");
}
}
class Panda : Animal
{
// これはoverrideじゃない!ただの新しいオーバーロード。
public void Eat(string what)
{
Console.WriteLine("パンダが食べる: " + what);
}
}
...
Animal a = new Panda();
a.Eat(); // オーバーライドされてれば子クラスの実装が呼ばれる。なければ親のが使われる
// a.Eat("バンブー"); // コンパイルエラー:Animalにそんなメソッドはない
ポリモーフィックな動作を追加したいなら、ちゃんとオーバーライドしよう。
11. 設計上の形式的・非形式的ミス
クラス設計がイケてないと、継承リストがカオスになる:
- 内部でbase.をちゃんと使わずにoverrideがグルグル回る
- 修飾子の使い方がバラバラ
- 階層が深すぎたり、分かりにくい継承構造
- ロジックが継承階層に分散しすぎてる
- virtual/abstractメソッドにコメントがない
- 副作用が分かりにくい(たとえば親コンストラクタでvirtualメソッドを呼ぶと、子がまだ初期化されてないのに動いちゃうとか)
アドバイス:
もし混乱してきたら、紙に継承図を書いて、どのメソッドがどこにあるか、誰がオーバーライドしてるか、どこでbase.を呼ぶべきかを整理しよう(アナログだけど超有効!)。
本番プロジェクトでは、どのメソッドをオーバーライドしてほしいか、ドキュメントにもちゃんと書いておくのが大事だよ。
GO TO FULL VERSION