C# の継承 - 抽象クラス(abstract class)の定義と活用

継承を前提としたクラス設計では、「親クラス自体はインスタンス化せず、子クラスに共通のインターフェースと部分的な実装を提供する」というパターンがよく登場します。C# ではこのようなクラスを abstract キーワードで定義します。

abstract クラスの基本

abstract を付けたクラスは、直接インスタンスを作ることができません。必ず子クラスを経由して使う前提の設計です。

abstract class Shape
{
    public string Color { get; set; }

    public abstract double GetArea();

    public void PrintInfo()
    {
        Console.WriteLine($"色: {Color}, 面積: {GetArea()}");
    }
}

Shape クラスには 3 つの要素があります。通常のプロパティ Color、抽象メソッド GetArea、そして具象メソッド PrintInfo です。抽象メソッドは本体({ } の中身)を持たず、子クラスで必ずオーバーライドしなければなりません。

// コンパイルエラー: 抽象クラスはインスタンス化できない
var shape = new Shape();

抽象メソッドの実装

子クラスは抽象メソッドを override で実装する義務があります。実装しなければコンパイルエラーになるため、「実装忘れ」が起こりません。

class Circle : Shape
{
    public double Radius { get; }

    public Circle(double radius)
    {
        Radius = radius;
    }

    public override double GetArea()
    {
        return Math.PI * Radius * Radius;
    }
}

class Rectangle : Shape
{
    public double Width { get; }
    public double Height { get; }

    public Rectangle(double width, double height)
    {
        Width = width;
        Height = height;
    }

    public override double GetArea()
    {
        return Width * Height;
    }
}

CircleRectangle はそれぞれ独自の面積計算を持ちつつ、PrintInfo メソッドは親クラスの実装をそのまま共有しています。これが抽象クラスの強みで、共通処理は親に集約し、個別の処理だけを子に委ねることができます。

Shape circle = new Circle(5) { Color = "赤" };
circle.PrintInfo();
// 出力: 色: 赤, 面積: 78.53981633974483

abstract と virtual の違い

どちらも子クラスでオーバーライドできるメソッドを定義しますが、性質は大きく異なります。

abstract メソッド

本体を持てない。子クラスでの override が必須。抽象クラス内でのみ宣言可能。

virtual メソッド

本体を持つ(デフォルトの実装がある)。子クラスでの override は任意。通常のクラスでも宣言可能。

「デフォルトの振る舞いが定義できるか」が判断基準になります。面積の計算方法は図形ごとに全く異なるため abstract が適切ですが、ToString のように「とりあえずの既定動作」があるものは virtual のほうが自然です。

抽象プロパティ

メソッドだけでなく、プロパティにも abstract を付けることができます。

abstract class Vehicle
{
    public abstract int MaxSpeed { get; }

    public void ShowSpec()
    {
        Console.WriteLine($"最高速度: {MaxSpeed} km/h");
    }
}

class Car : Vehicle
{
    public override int MaxSpeed => 180;
}

class Bicycle : Vehicle
{
    public override int MaxSpeed => 40;
}

抽象プロパティを使うことで、子クラスに対して「この値は必ず提供せよ」という制約を課すことができます。定数的な値を返すだけなら、上のようにラムダ式形式(=>)で簡潔に書けます。

抽象クラスを継承した抽象クラス

抽象クラスが別の抽象クラスを継承することも可能です。この場合、中間の抽象クラスは親の抽象メソッドを実装してもしなくても構いません。

abstract class Animal
{
    public abstract string GetSound();
    public abstract string GetCategory();
}

abstract class Pet : Animal
{
    public override string GetCategory()
    {
        return "ペット";
    }

    // GetSound は実装せず、さらに下の子クラスに委ねる
}

class Cat : Pet
{
    public override string GetSound()
    {
        return "ニャー";
    }
}

PetGetCategory だけ実装し、GetSound は未実装のまま残しています。Pet 自身が abstract なのでこれは許されます。最終的に具象クラスである Cat がすべての抽象メソッドを実装する責任を負います。

テンプレートメソッドパターン

抽象クラスの典型的な活用法として、テンプレートメソッドパターンがあります。処理の大枠を親クラスで定義し、細部を子クラスに実装させるデザインパターンです。

abstract class ReportGenerator
{
    public void Generate()
    {
        var data = CollectData();
        var formatted = Format(data);
        Output(formatted);
    }

    protected abstract string CollectData();
    protected abstract string Format(string data);

    protected virtual void Output(string content)
    {
        Console.WriteLine(content);
    }
}

Generate メソッドが処理の流れを固定し、CollectDataFormat の具体的な中身は子クラスに任せています。Outputvirtual にしているため、デフォルトではコンソール出力ですが、必要に応じてファイル出力などに差し替えることもできます。

class CsvReport : ReportGenerator
{
    protected override string CollectData()
    {
        return "Alice,30\nBob,25";
    }

    protected override string Format(string data)
    {
        return $"=== CSV レポート ===\n{data}";
    }
}

このパターンを使うと、新しいレポート形式を追加するときも ReportGenerator を継承して 2 つのメソッドを実装するだけで済みます。処理フローの一貫性を保ちつつ、拡張性を確保できる設計です。

abstract クラスについて正しい記述はどれですか?

  • abstract クラスは直接インスタンス化できる
  • abstract メソッドにはデフォルトの実装を書ける
  • abstract メソッドは子クラスで必ず override しなければならない
__RESULT__

abstract メソッドは本体を持たず、具象クラスで必ず実装する義務があります。実装しなければコンパイルエラーになります。