継承

継承とは

継承はクラスの重要な機能のひとつです。
あるクラスの機能を受け継いで、新しいクラスを作るのが継承の機能です。

まずはサンプルコードから。


//基底クラス
class BaseClass
{
    public int ID;
    public string Name;
}

//派生クラス
class DerivedClass : BaseClass
{
    public int Age;
    public string Address;
}

static void Main(string[] args)
{
    //派生クラスの使用
    DerivedClass dc = new DerivedClass();

    dc.ID = 0;
    dc.Name = "○山×太";
    dc.Age = 20;
    dc.Address = "○県×市123";
}

上記コードの「BaseClass」は継承の元となるクラス(継承されるクラス)です。
これを基底クラス(サブクラス親クラス)といいます。

「DerivedClass」はBaseClassを継承したクラスです。
これを派生クラス(スーパークラス子クラス)といいます。

派生クラスDerivedClassではフィールドを二つしか宣言していませんが、基底クラスBaseClassで宣言したフィールドも使用できます。
DerivedClassはBaseClassを受け継いでいるからです。
これが継承の基本的な機能です。

継承の書式

継承は以下の書式で行います。


class 派生クラス名 : 基底クラス名
{
}

継承されたクラスをさらに継承することも可能です。


class BaseA
{ }

class BaseB : BaseA
{ }

class Derived : BaseB
{ }

多重継承の禁止

継承の元となる基底クラスはひとつしか持てません。
C++では多重継承といって許可されていますが、C#ではできません。


class BaseA
{ }

class BaseB
{ }

//二つ以上の基底クラスはエラー
class Derived : BaseA, BaseB
{ }

ただし、インターフェイスは複数指定することができます。
その場合、基底クラスはひとつまで指定でき、インターフェイスの指定数に制限はありません。
基底クラスの指定はインターフェイスよりも前に指定する必要があります。


class Base
{ }

interface IBaseA
{ }

interface IBaseB
{ }

class Derived : Base, IBaseA, IBaseB
{ }

詳しくはインターフェイスの項で説明します。

アクセス修飾子

派生クラスは基底クラスの機能を受け継ぎますが、派生クラスから使用できるのはアクセス修飾子private以外のメンバーのみです。


class BaseClass
{
    //アクセス修飾子がないと
    //privateになる
    int id;
    string name;
}

class DerivedClass : BaseClass
{
    public int Age;
    public string Address;
}

static void Main(string[] args)
{
    DerivedClass dc = new DerivedClass();

    //以下はエラー
    //基底クラスのprivateフィールドは
    //継承されていない
    dc.id = 0;
    dc.name = "○山×太";
}

privateなメンバーはそれを宣言したクラス内のみからアクセス可能なので、派生クラスでは使用できません。
protectedを指定すると、宣言したクラスとその派生クラス内で使用可能なメンバーとなります。


class BaseClass
{
    private int privateNum;
    protected int protectedNum;
    public int PublicNum;
}

class DerivedClass : BaseClass
{
    public void WriteLine()
    {
        //Console.WriteLine(privateNum); //NG
        Console.WriteLine(protectedNum); //OK
        Console.WriteLine(PublicNum);
    }
}

static void Main(string[] args)
{
    DerivedClass dc = new DerivedClass();

    //Console.WriteLine(dc.privateNum); //NG
    //Console.WriteLine(dc.protectedNum); //NG
    Console.WriteLine(dc.PublicNum);
}

protectedを指定したフィールドは(派生クラス以外の)外部からアクセスできないのはprivateと同じです。

継承とコンストラクター

派生クラスのインスタンスの生成時には派生クラスのコンストラクターが呼び出されますが、その際に基底クラスのコンストラクターも自動的に呼び出されます。


class BaseClass
{
    public BaseClass()
    {
        Console.WriteLine("BaseClass Constructor");
    }
}

class DerivedClass : BaseClass
{
    public DerivedClass()
    {
        Console.WriteLine("DerivedClass Constructor");
    }
}

static void Main(string[] args)
{
    DerivedClass dc = new DerivedClass();
    Console.ReadLine();
}
BaseClass Constructor
DerivedClass Constructor

呼び出しは「基底クラス→派生クラス」の順です。

基底クラスのコンストラクターの指定

基底クラスのコンストラクターは、何もしなければ引数なしのコンストラクター(既定のコンストラクター)が呼び出されます。
任意のコンストラクターを呼び出すにはbaseキーワードを使用します。


class BaseClass
{
    public string FirstName;
    public string LastName;

    public BaseClass() { }

    public BaseClass(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }
}

class DerivedClass : BaseClass
{
    public DerivedClass() { }

    //基底クラスの任意のコンストラクターの呼び出し
    public DerivedClass(string firstName, string lastName)
        : base (firstName, lastName)
    { }
}

これはコンストラクター初期化子の一種で、指定した引数を持つ基底クラスのコンストラクターを呼び出すことができます。

なお、基底クラスに引数なしのコンストラクターがない場合、必ずコンストラクター初期化子を使用して引数付きのコンストラクターを呼び出す必要があります。

基底クラスのメンバーの隠蔽

派生クラスでは基底クラスの非privateメンバーを使用できますが、派生クラスでも同じ名前のメンバーを定義することができます。


class BaseClass
{
    public int Num = 10;
}

class DerivedClass : BaseClass
{
    //基底クラスのフィールドNumを隠蔽
    public int Num = 20;
}

static void Main(string[] args)
{
    DerivedClass dc = new DerivedClass();
    
    Console.WriteLine(dc.Num);

    Console.ReadLine();
}
20

このようにすると外部からは基底クラスのフィールドNumにアクセスすることができなくなります。
これを基底クラスのメンバーの隠蔽(いんぺい)といいます。

ただ、メンバーの隠蔽は意図せず行ってしまう場合があり、これは好ましくないのでメンバーを隠蔽する場合は意図的なものであることを明示するためにnewキーワードを付けます。
(newがないとコンパイラが警告を出します)


class BaseClass
{
    public int Num = 10;
}

class DerivedClass : BaseClass
{
    public new int Num = 20;
}

base

メンバーの隠蔽は基底クラスのフィールドNumを再定義するのではなく、基底クラスと派生クラスとで同じ名前のメンバーを同時に持っている状態になります。
派生クラスからはbaseキーワードを使用して基底クラスの隠蔽したメンバーにアクセスすることができます。


class BaseClass
{
    public int Num = 10;
}

class DerivedClass : BaseClass
{
    //基底クラスのフィールドNumを隠蔽
    public new int Num = 20;

    public void WriteLine()
    {
        //基底クラスのフィールドNumにアクセス
        Console.WriteLine(base.Num);
        Console.WriteLine(Num);
    }
}

static void Main(string[] args)
{
    DerivedClass dc = new DerivedClass();

    dc.WriteLine();

    Console.ReadLine();
}
10
20

継承禁止のクラス

継承は基本的にどのようなクラスであっても基底クラスにできます。
(.NET標準ライブラリのクラスでも可能)
しかし派生クラスを作らせたくない場合はクラスにsealedキーワードを付けます。
(sealed=封印された)


//継承できないクラス
sealed class SealedClass
{ }

//エラー
class DerivedClass : SealedClass
{ }

自作クラスでsealedなクラスを作ることはあまりないと思いますが、ライブラリにはたまに継承禁止のクラスがあります。

is-a関係とhas-a関係

継承は「クラスAからクラスBを作る」という機能です。
言い換えれば「BはA(の一部)である」という関係が成り立ちます。
これをis-a関係といいます。
(B is a A)

例えば「動物」を定義するクラスを継承して「犬」という新しいクラスを作ります。
この時「犬は動物である」という関係が成り立ちます。
「動物」という漠然としたモノに対して「哺乳類」「四本足」「ワンワンと鳴く」などの特徴となるモノを加えて「犬」を定義するのです。

これに似た概念としてhas-a関係というものもあります。
これは「BはAを持っている」というものです。
(B has a A)

例えば「尻尾」というクラスを定義してそれを「犬」というクラスで利用します。
この時「犬は尻尾を持っている」という関係が成り立ちます。

has-a関係は継承ではなく、クラスのフィールドとして定義します。
継承よりも元となるクラスへの依存度が低く、簡単に扱うことができます。
しかし継承ではないのでprotectedメンバーを扱えないなど、自由度が低くなります。

どちらが優れているという事ではなく、どちらを使用するほうが自然か、で考えると良いでしょう。
例えば「犬は尻尾である」は成り立たないので、尻尾クラスを継承して犬クラスを作るのは不自然と考えられます。
動物クラスを継承したクラスに尻尾クラスを持たせて犬クラスを作るのは自然な定義といえます。