C#でクラスを作ろう(8)/クラスの継承

3月 25, 2020

このシリーズについて

C言語やC++言語などを学んではいるけどクラスをあまり作ったことが無い、という方を対象にしています。
このシリーズでは、C#でクラスを作るための基本的な構文を解説しています。C++やJavaなどと共通している概念も多いですが、サンプルコードは基本的にC#で解説します。ところどころ、C++特有の概念を解説することもあります。

クラスとは、構造体にメソッドが定義できるものであると解説しました。また、クラスには情報隠蔽をする機能が備わっていて、クラスを外側から使う人はクラスの内側の内部実装を詳しく知る必要が無く、関心事を外側と内側で分けられるということを解説しました。

しかし、クラスの有用な点はそれだけではありません。クラスには継承という概念があり、これこそがクラスの最も特徴的な性質と言えるでしょう。

スポンサーリンク

クラスを継承する

あるクラスを親クラスとして、親クラスの全ての機能を受け継ぎつつ新たなフィールド・メソッド・プロパティなどを追加する機能のことを、継承といいます。

//親クラス
class Person
{
    //人物を表わすクラス
    public string Name { get; set; }
    public string Address { get; set; }
    public DateTime Birthday { get; set; }
    public void EatOniku()
    {
        Console.WriteLine("おにくもぐもぐ");
    }
}
  
//子クラス
class Student : Person
{
    //小学生を表わすクラス。人物を表わす親クラスPersonを継承している
    public string SchoolName { get; set; }
    public int Grade { get; set; }
    public void GradeUp()
    {
        if (this.Grade < 6)
            this.Grade++;
        else
            Console.WriteLine("卒業おめでとう!");
    }
}
 
//使い方
Student student = new Student();
//親クラスのプロパティやメソッドにアクセスできる
student.Name = "○山×郎";
student.Address = "△△市☆☆町";
student.Birthday = new Date(2012, 5, 13);
student.EatOniku();
//子クラスのプロパティやメソッドにもアクセスできる
student.SchoolName = "☆☆小学校";
student.Grade = 1;
student.GradeUp();

このように、Studentクラスを"class Student : Person"とすることで、Personクラスから継承させることができます。子クラスのStudentは、親クラスのPersonのプロパティやメソッドを全て扱うことができます。

クラスの継承

親クラスには他に「基本クラス」「基底クラス」「スーパークラス」などの呼び方があります。子クラスにも「継承クラス」「派生クラス」「サブクラス」などの呼び方があり、いずれも同じ意味です。

クラスを継承すると、親クラスの機能を全て引き継いだ上に、さらに他の機能を追加することができます。クラスの継承は、このように親クラスの機能を拡張するときに使われる手法です。

他の拡張方法との違い

単純に機能を拡張するだけなら、次の2つの方法が考えられます。それぞれ、クラスの継承とどう違うのか、見ていきましょう。

プロパティやメソッドを追加する

元のPersonクラスに単純にプロパティやメソッドを追加する方法です。

class StudentPerson
{
    public string Name { get; set; }
    public string Address { get; set; }
    public DateTime Birthday { get; set; }
    public void EatOniku()
    {
        Console.WriteLine("おにくもぐもぐ");
    }
 
    public string SchoolName { get; set; }
    public int Grade { get; set; }
    public void GradeUp()
    {
        if (this.Grade < 6)
            this.Grade++;
        else
            Console.WriteLine("卒業おめでとう!");
    }
}

このようにして、「小学生の人物」を表わすStudentPersonクラスを作れば、機能を拡張したことになります。

しかしこれではもう、「小学生ではないただの人物」を表わす元のPersonクラスを使うことはできなくなります。逆に元のPersonクラスも残した場合は同じコードが重複して登場することになり、保守性が著しく低下します(修正時、一方だけ修正して、もう一方を修正し忘れるなど)。

さらに、親クラスが他人の作ったものでソースコードが存在しない場合(コンパイル済みのバイナリしか存在しない場合)、このような方法は不可能になります。

親クラスに相当する機能をメンバに持たせる

//親クラス
class Person
{
    //略...
}
 
//親クラスに相当する機能をメンバに持ったクラス
class Student
{
    //親クラス相当分
    public Person InnerPerson { get; set; } = new Person();
    //Studentクラスの独自部分
    public string SchoolName { get; set; }
    public int Grade { get; set; }
    public void GradeUp()
    {
        if (this.Grade < 6)
            this.Grade++;
        else
            Console.WriteLine("卒業おめでとう!");
    }
}
 
//使い方
Student student = new Student();
//親クラス相当分の機能へのアクセス
student.InnerPerson.Name = "○山×郎";
student.InnerPerson.Address = "△△市☆☆町";
student.InnerPerson.Birthday = new Date(2012, 5, 13);
student.InnerPerson.EatOniku();
//studentクラス独自部分へのアクセス
student.SchoolName = "☆☆小学校";
student.Grade = 1;
student.GradeUp();

このようにすれば、同じコードを重複させることなく、親クラスの機能を拡張することができます。このStudentクラスの中のInnerPersonをprivateなフィールドにし、さらに

class Student
{
    private Person _innerPerson = new InnerPerson();
    public string Name
    {
        get { return _innerPerson.Name; }
        set { _innerPerson.Name = value; }
    }
    public string Address
    {
        get { return _innerPerson.Address; }
        set { _innerPerson.Address = value; }
    }
    public DateTime Birthday
    {
        get { return _innerPerson.Birthday; }
        set { _innerPerson.Birthday = value; }
    }
    public void EatOniku()
    {
        _innerPerson.Oniku();
    }
    //以下略...
}

とすれば、継承したときと同じような構文で外側から内部の親クラスにアクセスできます。このように、内部に親クラスをメンバとして持たせるやり方を合成(または包含)、親クラスのプロパティやメソッドへのアクセスを再定義することを委譲といいます。

継承を使うか合成を使うか

先の節の2つ目の例で、継承の他にクラスを拡張するやり方として、合成という方法を紹介しました。では、継承を使うか合成を使うかの線引きはどこにあるのでしょうか?

一般的には、次のようなガイドラインに沿うと良いとされています。

【継承】
「子クラス is a 親クラス」のように、「is a」の関係であるとき。つまり、Student(小学生)はPerson(人物)でもあるので、「is a」の関係になっている。

【合成】
「親クラス has a 子クラス」のように、「has a」の関係であるとき。例えばランドセルを表わすBugクラスがあり、「ランドセルを持った小学生」という新しいクラスを作りたいとき、Student(小学生)はBug(ランドセル)を持っているので、「has a」の関係になっている。ランドセルという物体は小学生ではないので、「ランドセル is a 小学生」ではない。

従って、例のStudentクラスの場合は「is a」の関係なので、継承を使うほうが適切です。

継承を避ける傾向

クラスの継承を使うと、親クラスと子クラスの間にある種の「関係」が生じることになります。先の章で説明するようなポリモーフィズムが複雑に絡み合った状況だと、親クラスに変更を加えたら子クラスにどのような影響があるだろうか、子クラスに変更を加えるためには親クラスの設計も変更しなければならないだろうか、と、関心事が余計に多く増えてしまいます。
そこで近年は、クラスの継承をできるだけ避け、委譲(前述)のためのコードがそれほど大きくならないようであれば、「is a」の関係であっても合成を使うという流儀もあります。

子クラスのコンストラクタ

継承された子クラスにもコンストラクタを定義することができます。インスタンスを作成した際は、親クラスのコンストラクタ→子クラスのコンストラクタの順に呼び出されます。

//親クラス
class Person
{
    //中略...
    //親クラスのコンストラクタ
    public Person(string name)
    {
        this.Name = name;
    }
}
 
//子クラス
class Student : Person
{
    //中略...
    //子クラスのコンストラクタ
    public Student(string name, string schoolName}
        : base(name)
    {
        this.SchoolName = schoolName;
    }
}
 
//使い方
Student student = new Student("○山×郎", "△△市☆☆町");

子クラスのコンストラクタの定義のところで、先に呼び出される親クラスのコンストラクタへの引数をbase初期化子によって" : base(name)"のように指定します。

親クラスがコンストラクタを持たないとき(デフォルトコンストラクタしかないとき)や、引数無しのコンストラクタが定義されているときは、base初期化子による記法は必ずしも必要ではありません。

protectedアクセス修飾子

C#でクラスを作ろう(3)/アクセス修飾子で、プロパティやメソッドに対するアクセス制限を規定するためのpublicアクセス修飾子、privateアクセス修飾子を紹介しました。

クラスの継承が絡むとき、publicとprivateの中間にあたるprotectedアクセス修飾子を指定することができます。

//親クラス
class Parent
{
    private string PrivateMember { get; set; }
    protected string ProtectedMember { get; set; }
    public string PublicMember { get; set; }
}
 
//子クラス
class Child : Parent
{
    public void SomeMethod()
    {
        PrivateMember = "あいうえお";  //※エラー! アクセスできない
        ProtectedMember = "あいうえお";  //アクセスできる
    }
}
 
//外側から使うとき
Child child = new Child();
child.PrivateMember = "あいうえお";  //※エラー! アクセスできない
child.ProtectedMember = "あいうえお";  //※エラー! アクセスできない
child.PublicMember = "あいうえお";  //アクセスできる

protected修飾子が付けられたメンバーは基本的にはprivateと同じように振る舞います。つまり、クラスの外側からアクセスすることはできません。

ただし例外的に、継承された子クラスの中からは、protectedメンバーにアクセスすることができます。

まずは継承してみよう

どのような場合にクラスを継承すべきかという問題は、しばしば宗教論争に発展します。特に、継承という仕組みの仕様が言語ごとに微妙に違うため、C++では継承すべきだけどC#では継承すべきでない、みたいな事例もあったりします。

言語に拠らない「オブジェクト指向とは何ぞや」という学術的な視点からは、継承は極めて限定的な場面でしか使ってはいけないという意見さえあります。

いろいろな考え方はありますが、まずはとにかくクラスの継承をしてみて、その雰囲気を掴んでみましょう。学術的な意見に捉われず、まずは手を動かして実感してみて、時には他の人の意見も取り入れながら、クラスの継承というものを自分の知識の中に取り込んでみてください。

スポンサーリンク