CodeGym /コース /C# SELF /継承とオーバーライド時のエラー

継承とオーバーライド時のエラー

C# SELF
レベル 25, レッスン 1
使用可能

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.を呼ぶべきかを整理しよう(アナログだけど超有効!)。
本番プロジェクトでは、どのメソッドをオーバーライドしてほしいか、ドキュメントにもちゃんと書いておくのが大事だよ。

2
タスク
C# SELF, レベル 25, レッスン 1
ロック未解除
`virtual` の付け忘れによる継承メソッドのエラー
`virtual` の付け忘れによる継承メソッドのエラー
2
タスク
C# SELF, レベル 25, レッスン 1
ロック未解除
new を使ったメソッドの隠蔽のエラー
new を使ったメソッドの隠蔽のエラー
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION