抽象クラス

インスタンス生成禁止のクラス

オーバーライドの項では仮想メソッドのオーバーライドについて説明しました。
仮想メソッドをオーバーライドするか否かは自由ですが、時にはオーバーライドを強制したい場合もあります。

例えば色々な国の人のクラスを定義して、それぞれの国の言語を話すプログラムを作ることを想定してみます。


class Human
{
    public string Name;

    public Human(string name)
    {
        Name = name;
    }

    public virtual void Speak()
    {
        //派生クラスで定義するので空
    }
}

class American : Human
{
    public American(string name)
        : base(name) { }

    public override void Speak()
    {
        Console.WriteLine("I'm {0}.", Name);
    }
}

class Japanese : Human
{
    public Japanese(string name)
        : base(name) { }

    public override void Speak()
    {
        Console.WriteLine("私は{0}です。", Name);
    }
}

言語の違いはフィールドで持つのが一般的かと思いますが、今回はメソッドに直接定義しています。

基底クラスのHumanは名前(Name)とそれを出力するSpeakメソッドを持っています。
Speakメソッドは、基底クラスでは使用することはないので空にしています。
使わないメソッドは普通は定義しませんが、このクラスの派生クラスにもSpeakメソッドを確実に持つようにしたいので敢えて定義しています。
(定義をしないとSpeakメソッドを持たない派生クラスが作れるが、それは避けたい)

しかし使わないメソッドを定義するのは無駄ですし、仮想メソッドをオーバーライドしなくても派生クラスは作れます。
そもそもHumanクラスは派生クラスを作るために必要ですが、この基底クラスはインスタンスが生成できなくても構いません。
むしろ意味のない動作をするようなインスタンスは生成できないほうが紛らわしくなく親切とも言えます。

このような場合は抽象クラス(ちゅうしょうクラス)と抽象メソッドという機能を使うとスッキリします。

抽象クラスと抽象メソッドの書式


//抽象クラス
abstract class Human
{
    public string Name;

    public Human(string name)
    {
        Name = name;
    }

    //抽象メソッド
    public abstract void Speak();
}

//Americanクラスと
//Japaneseクラスの定義は
//同じなので省略

抽象クラスはclassの前にabstractキーワードを付けて定義します。
(abstract=抽象的な)

抽象クラスはインスタンスが生成できないクラスになります。
つまり継承して派生クラスのインスタンスを生成して使うことを前提としたクラスになります。

抽象クラス内ではメソッドとプロパティにもabstractキーワードを付けることができます。
(抽象クラス以外のクラスでは不可)
これは抽象メソッドといい、派生クラスで必ずオーバーライドしなければならないメソッド(プロパティ)となります。
派生クラスでオーバーロードしないとコンパイルエラーになります。
必ずオーバーライドされるので、基底クラスではメソッドの処理は定義しません。
(ブロック{}さえも書かない)

抽象クラスと抽象メソッドを使用したコードの全体は以下になります。


//抽象クラス
abstract class Human
{
    public string Name;

    public Human(string name)
    {
        Name = name;
    }

    //抽象メソッド
    public abstract void Speak();
}

class American : Human
{
    public American(string name)
        : base(name) { }

    public override void Speak()
    {
        Console.WriteLine("I'm {0}.", Name);
    }
}

class Japanese : Human
{
    public Japanese(string name)
        : base(name) { }

    public override void Speak()
    {
        Console.WriteLine("私は{0}です。", Name);
    }
}

static void Main(string[] args)
{
    //Humanクラスのインスタンス生成不可
    //Human human = new Human();

    List<Human> lst = new List<Human>();
    lst.Add(new American("John"));
    lst.Add(new Japanese("太郎"));

    foreach (var e in lst)
        e.Speak();

    Console.ReadLine();
}
I'm John.
私は太郎です。

抽象クラスのインスタンスは生成できませんが、その派生クラスのインスタンスを基底クラス(抽象クラス)にアップキャストすることは可能です。
このサンプルコードでも派生クラスのインスタンスを基底クラス型のListクラスで受け取っています。

抽象的と具体的

抽象クラスは名前の通り「抽象的なクラス」です。
抽象的の反対語は具体的で、通常のクラスは具体的なクラスです。
(具象クラスなどと呼ばれます)

具体的なクラスは言い換えれば「抽象的な箇所がないクラス」で、実体であるインスタンスを生成できます。
抽象的なクラスは具体化できない箇所が含まれるクラスで、実体を作ることができません。

例えば「アメリカ人(American)」は「人間(Human)」よりも国籍の情報を持っているので具体度が高いと言えます。
今回のプログラムは、その人が話す言語がわかれば良いので「アメリカ人クラス」は通常のクラスとして定義できます。
(アメリカ人はすべて英語を話すわけではないですが、このプログラムでは無視します)
しかし「人間クラス」は、その人が話す言語の情報がないので具体的なクラスは作れません。
何らかの言語は話すということは分かっていても、それが「具体的に」何かが分からないので、分からない部分は「抽象的に」定義しておくのが抽象クラスの役割です。

抽象的な部分が残ったままでは実際に使えない(インスタンス化できない)ので、抽象クラスを継承する派生クラスでその抽象的な部分を具体的に定義して実際に使用するインスタンスを生成します。

抽象プロパティ

プロパティもクラス内から見ればメソッドのようなものなので、プロパティもabstractにできます。
プロパティの場合、抽象クラス内ではアクセサの指定のみが可能で、実装は派生クラスで行います。


abstract class Base
{
    //抽象プロパティ
    public abstract int Num { get; set; }
}

class Derived : Base
{
    public override int Num
    {
        get { return Num; }
        set
        {
            Num = Math.Max(0, value);
        }
    }
}

抽象プロパティのアクセサと派生クラスのアクセサが異なるとエラーになります。


abstract class Base
{
    public abstract int Num1 { get; }
    public abstract int Num2 { get; set; }
}

class Derived : Base
{
    //アクセサの指定が違うので両方エラー
    public override int Num1 { get; set; }
    public override int Num2 { get; }
}