継承

継承とは

C#のクラスは、機能を拡張することができます。その中で藻、もっとも代表的なものが、C#のみならずオブジェクト指向言語に備わっている重要な特性の1つである継承(インヘリタンス)について説明します。これは、あるクラスのメンバを、他のクラスに引継ぐ(継承させる)という効果があります。

では、実際に継承とはどういうものか、さらに詳しく説明しましょう。すでに述べたように、クラスは、インスタンス、およびオブジェクトの「設計図」です。自動車の例を出すのならば、自動車の設計図がクラス、実際に工場で生産された自動車本体が、インスタンス、およびオブジェクトということになります。

親クラスと子クラス

さて、自動車といえば、普通は乗用車を想像してしまうかもしれませんが、実際、自動車と呼ばれるものは実に多くの種類が存在します。たとえば、警察車両であるパトロールカー、荷物を運ぶトラック、さらには緊急車両であるトラックなど、実に多種多様な種類が存在します。それらは、「自動車」でありながら、それぞれ機能に応じた独自の機能の拡張がなされています。

このように、基本となるクラスの性質を受け継ぎ、独自の拡張をすることを、オブジェクト指向では、継承(けいしょう)と呼びます。継承のもととなるクラスのことを、親クラス,スーパークラスなどと呼びます。それにたいし、親クラスの機能を継承し、独自の機能を実装したクラスのことを、子クラス、もしくは、サブクラスと呼びます。前述の自動車の例で言うのならば、車クラスが親クラス、トラックや救急車などが、サブクラスということになります。(図2-1.参照)

図3-1:継承のイメージ
継承のイメージ

C#での継承の実装

サンプルプログラム

は実際に、継承のサンプルを入力して、試してみましょう。以下のプログラムを入力・実行してみてください。

プロジェクトSampleEx301/Calculator.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SampleEx301
{
    class Calculator
    {
        //  一つ目の数値
        protected int num1;
        //  二つ目の数値
        protected int num2;
        //  num1のプロパティ
        public int Num1
        {
            set { num1 = value; }
            get { return num1; }
        }
        //  num2のプロパティ
        public int Num2
        {
            set { num2 = value; }
            get { return num2; }
        }
        //  足し算
        public void add()
        {
            Console.WriteLine("{0} + {1} = {2}",num1,num2,num1 + num2);
        }
        //  引き算
        public void sub()
        {
            Console.WriteLine("{0} - {1} = {2}", num1, num2, num1 - num2);
        }
    }
}
ExCalculator.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SampleEx301
{
    //  Calculatorクラスを継承した、ExCalculatorクラス
    class ExCalculator : Calculator
    {
        //  掛け算
        public void mul()
        {
            Console.WriteLine("{0} * {1} = {2}", num1, num2, num1 * num2);
        }
        //  割り算
        public void div()
        {
            Console.WriteLine("{0} / {1} = {2}", num1, num2, num1 / num2);
        }
    }
}
Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SampleEx301
{
    class Program
    {
        static void Main(string[] args)
        {
            //  Calculatorクラスのインスタンス
            Calculator c1 = new Calculator();
            c1.Num1 = 10;
            c1.Num2 = 3;
            //  足し算・引き算の結果を表示
            c1.add();
            c1.sub();
            ExCalculator c2 = new ExCalculator();
            c2.Num1 = 10;
            c2.Num2 = 3;
            //  足し算・引き算の結果を表示
            c2.add();
            c2.sub();
            //  掛け算・割り算の結果を表示
            c2.mul();
            c2.div();
        }
    }
}

実行結果
10 + 3 = 13
10 - 3 = 7
10 + 3 = 13
10 - 3 = 7
10 * 3 = 30
10 / 3 = 3

Calculationクラスは、フィールドnum1とnum2とそのプロパティ、メソッドとして、add()とsub()を持っています。これらメソッドは、二つのフィールドnum1およびnum2の和、差を求めるものです。

そのクラスの機能を受け継ぎ、さらに拡張したのが、ExCalculationクラスです。ExCalculation.csの10行目にあるクラスの定義で、クラス名の後に「:(コロン)」をつけて、クラス名を書くと、そのクラスの機能を受け受け継いだクラスを作ることができます。これを、継承と言います。このとき、基になるクラスを、親クラスもしくは、ベースクラスと呼び、その機能を継承し、新に機能を追加したクラスのことを、子クラスまたは、サブクラスと呼びます。

継承の実装

あるクラスが、別のクラスを親クラスとするときの定義は、

親クラスを継承した、子クラスの定義の方法
class 子クラス名 : 親クラス名

のように定義します。

つまり、このサンプルでは、ExCalculationクラスは、Calculationクラスを継承したクラスです。そのため、Calculationクラスが親クラス、ExCalculationクラスが、子クラスという組み合わせになります。

このことから、publicメンバである、move()メソッド、および、supply()メソッドを利用することができます。ただ、Carクラスのprivateなフィールドである、fuelおよび、migrationには、直接アクセスすることはできません。

また、ExCalculationクラスは、さらに独自のメソッドである、mulおよび、subを持っています。

protected

Calculator.csの12行目、および14行目を見てください。フィールドの先頭に、protectedと書かれています。このprotectedは、private同様、外部からはアクセスはできませんが、子クラスからはアクセスできることを意味する修飾子です。

図3-2:protected修飾子
protected修飾子

したがって、Calculatorを継承した、ExCalculatorクラスでは、このフィールドを利用することができます。

C#は単一継承

ここまで、C#の継承について説明してきました。基本的に、一つのクラスのサブクラスはいくらでもできますが、サブクラス一つに対し、スーパークラスは一つしか存在しません。このように、親クラスが一つしかないような継承の仕方を、単一継承(たんいつけいしょう)と言います。

ただ、言語によってはひとつのクラスに複数の親クラスを設定することができます。これを、多重継承(たじゅうけいしょう)と言います。

図3-3:単一継承と多重継承
単一継承と多重継承

継承とコンストラクタ・デストラクタ

サンプルプログラム

継承関係にあるクラス間のコンストラクタやデストラクタの関係はどうなっているのでしょうか。まずは以下のサンプルを実行してみてください。

プロジェクトSampleEx302/Super.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SampleEx302
{
    //  スーパークラス
    class Super
    {
        //  パラメータ
        private int param = 0;
        //  コンストラクタ(引数なし)
        public Super()
        {
            Console.WriteLine("Superクラスのコンストラクタ(引数なし)");
        }
        //  コンストラクタ(引数あり)
        public Super(int param)
        {
            Console.WriteLine("Superクラスのコンストラクタ(引数:param={0})", param);
            this.param = param;
        }
        //  デストラクタ
        ~Super()
        {
            Console.WriteLine("Superクラスのデストラクタ");
        }
        public void showParam()
        {
            Console.WriteLine("param = {0}", param);
        }
    }
}
Sub.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SampleEx302
{
    //  サブクラス(Superクラスを継承
    class Sub : Super
    {
        //  Subクラスのコンストラクタ
        public Sub()
        {
            Console.WriteLine("Subのコンストラクタ(引数なし)");
        }
        //  Subクラスのコンストラクタ
        public Sub(int param) : base(param)
        {
            Console.WriteLine("Subのコンストラクタ(引数:param={0})",param);
        }
        //  Subクラスのデストラクタ
        ~Sub()
        {
            Console.WriteLine("Subクラスのデストラクタ");
        }
    }
}
Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SampleEx302
{
    class Program
    {
        static void Main(string[] args)
        {
            Sub s1 = new Sub();
            s1.showParam();
            Sub s2 = new Sub(100);
            s2.showParam();
        }
    }
}
実行結果
Superクラスのコンストラクタ(引数なし)
Subのコンストラクタ(引数なし)
param = 0
Superクラスのコンストラクタ(引数:param=100)
Subのコンストラクタ(引数:param=100)
param = 100
Subクラスのデストラクタ
Superクラスのデストラクタ
Subクラスのデストラクタ
Superクラスのデストラクタ

コンストラクタの呼び出し順番

プログラムを実行すると、Subクラスをnewすると、Superクラスのコンストラクタが呼び出され、子クラスを生成した場合、親クラスのコンストラクタが呼ばれ、そののちに子クラスのコンストラクタが呼び出されます。

Program.csの13行目で、Subクラスのインスタンスを生成していますが、このとき、Sub.csのコンストラクタが呼び出されますが、その前に親クラスである、Superクラスのコンストラクタが呼び出されます。(図3-4.)

図3-4:コンストラクタの呼び出し順位①
コンストラクタの呼び出し順位①

続いて、Program.csの15行目に出ているもう一つのインスタンスの生成を見てみましょう。この場合は、引数付きのコンストラクタを呼び出しています。このとき、ポイントになるのは、18行目に出ている、Sub.csのbase(param)です。この、コンストラクタの後に:をつけてbaseをつけると、親クラスのコンストラクタの呼び出しを意味します。

()内に、引数として、param、つまりint型の値を与えていますが、これにより、親クラスの、引数つきのコンストラクタを呼び出しています。(図3-5.参照)

図3-5:コンストラクタの呼び出し順位②
コンストラクタの呼び出し順位②

子クラスのコンストラクタを呼び出すと、必ず親クラスのコンストラクタが呼び出されます。特に指定がなければ、パラメータのないコンストラクタが呼び出されますが、このように、baseを用いて、呼び出すコンストラクタの種類を明示的に選択することができます。

オーバーライド

サンプルプログラム

通常、public及びprotectedメンバであれば、子クラスは、親クラスのメソッドをそのまま使うことができます。しかし、場合によっては、同じメソッドでも、親クラスと子クラスで結果の異なるものを出すことができます。以下のサンプルを実行してみてください。

プロジェクトSampleEx303/Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SampleEx303
{
    class Program
    {
        static void Main(string[] args)
        {
            //  Parentクラスのインスタンス生成
            Parent p = new Parent();
            //  Childクラスのインスタンス生成
            Parent c = new Child();
            //  それぞれのクラスのfoo、barメソッドを実行
            p.Foo();
            c.Foo();
        }
    }
}
Parent.cs(変更前)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SampleEx303
{
    //  親クラス
    class Parent
    {
        public void Foo()
        {
            Console.WriteLine("親クラスのFoo()メソッド");
        }
    }
}
Child.cs(変更前)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SampleEx303
{
    //  子クラス
    class Child : Parent
    {
        public void Foo()
        {
            Console.WriteLine("子クラスのFoo()メソッド");
        }
    }
}
実行結果(変更前)
親クラスのFoo()メソッド
親クラスのFoo()メソッド

ParentおよびChildクラスには、戻り値引数共にないメソッドFooが存在します。このような場合、Mainにおいて、サブクラスであるChildのインスタンスの参照をPareng型の変数に代入しこれを実行すると、「親クラスのFoo()メソッド」と表示され、あくまでもParentクラスのメソッドが呼び出されることが分かります。

virtualとoverride

実行結果(変更後)

しかし、ParentおよびChildに以下のように変更を加えるとプログラムの結果がわかります。

Parent.cs(変更前)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SampleEx303
{
    //  親クラス
    class Parent
    {
        public void virtual Foo()
        {
            Console.WriteLine("親クラスのFoo()メソッド");
        }
    }
}
Child.cs(変更前)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SampleEx303
{
    //  子クラス
    class Child : Parent
    {
        public void override Foo()
        {
            Console.WriteLine("子クラスのFoo()メソッド");
        }
    }
}
親クラスのFoo()メソッド
子クラスのFoo()メソッド

まず、スーパークラスである、Parent.csの12行目を見てください。このメソッドfoo()は、先頭にvirtual(バーチャル)修飾子がついています。続いてChild.csの12行目を見てください。Parentクラス同様、foo()メソッドが定義されていますが、ここには、override(オーバーライド)修飾子がついています。

これにより、同じFoo()メソッドを呼び出しても、同じParent型の変数に代入したとしても親クラスの場合と、子クラスの場合では、実行結果が違うことが分かります。このように、子クラスで親クラスと名前、引数の組み合わせ、および戻り値の型がまったく同じメソッドが定義されると、子クラスと親クラスでは、同じメソッドの呼び出し方をしても、違う動作をします。これを、オーバーライドと言います。(図3-6.参照)

通常はインスタンスがサブクラスのものでも、その参照を代入する変数の型がスーパークラスのものであれば、同名のメソッドがあればスーパークラスのものが呼び出されますが、この方法を用いると、サブクラスのメソッドを呼び出すことが可能です。

図3-6:オーバーライド
オーバーライド

ポリモーフィズム

すでに説明した、オーバーロードおよび、ここで取り上げたオーバーライドのことを、総称して、ポリモーフィズムと言います。日本語で、多様性、多態性、などとも呼ばれます。

ポリモーフィズムは、C#に限らず、広く他のオブジェクト指向言語でも用いられており、非常に大事な概念です。

objectクラス

サンプルプログラム

実はC#のすべてのクラスは、Objectオブジェクトを暗黙の内に継承しています。以下のプログラムを実行してみてください。

プロジェクトSampleEx304/Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SampleEx304
{
    class Program
    {
        public void Foo()
        {
            Console.WriteLine("Foo");
        }
        static void Main(string[] args)
        {
            Program s = new Program();
            Console.WriteLine(s.ToString());
        }
    }
}
実行結果
SampleEx303.Program

Programクラスには、foo()というメソッドはついていますが、ToStringというクラスはありません。しかし、「SampleEx303.Program」という文字列が表示されています。これは、実はobjectクラスのメソッドなのです。このメソッドが実行できるは、このクラスがobjectクラスを継承しているからなのです。

Objectクラスの主要なメソッド

なお、Objectクラスの主なメソッドには、以下のようなものがあります。処理によっては、ベースクラスであるObjectと、サブクラスでは結果が違うものも存在します。(表3-1)

表3-1.Objectクラスの主要なメソッド
メソッド 働き
Equarls(object) 引数内のオブジェクトと等しいかどうか調べる。

ToString() ブジェクトを表す文字列を返す。

GetType() オブジェクトのタイプを返す。


練習問題 : 問題3.