応用編

応用編3日目:継承

既存クラスの機能を引き継いで拡張する、継承の仕組みを学びましょう

応用編: 1 2 3 4 5 6 7

継承とは

親クラスと子クラス

オブジェクト指向プログラミングには、継承という概念があります。継承とは、あるクラスのメンバを、他のクラスに引き継がせるという機能です。機能を引き継ぐ元となるクラスを親クラス(ベースクラス、スーパークラス)と呼び、その機能を受け継いで拡張したクラスを子クラス(サブクラス、派生クラス)と呼びます。

C#での継承は、クラス名の後に :(コロン)を付けて親クラス名を記述することで実現します。

継承の書式
class (子クラス名) : (親クラス名) { … }
継承のイメージ

図3-1. 継承のイメージ(Vehicle を親クラスとした場合)

C#では、一つのクラスが持てる親クラスは1つだけです(単一継承)。ただし、継承の連鎖は可能で、孫クラス・ひ孫クラス…と階層を深めることができます。

protected 修飾子

継承において重要なアクセス修飾子として、protected があります。private と同様に外部からのアクセスは禁止されますが、子クラスからはアクセスできるという点が異なります。

アクセス修飾子の比較
修飾子同クラス内子クラス外部クラス
public
protected×
private××

継承の基本

サンプルプログラム

以下のサンプルでは、加算・減算機能を持つ Calculator クラスを親クラスとして、乗算・除算機能を追加した ExCalculator クラスを定義しています。

プロジェクト: 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 a;
        protected int b;

        public Calculator(int a, int b)
        {
            this.a = a;
            this.b = b;
        }

        public void Add()
        {
            Console.WriteLine("{0} + {1} = {2}", a, b, a + b);
        }

        public void Subtract()
        {
            Console.WriteLine("{0} - {1} = {2}", a, b, a - b);
        }
    }
}
プロジェクト: SampleEx301 / ファイル名: ExCalculator.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SampleEx301
{
    class ExCalculator : Calculator
    {
        public ExCalculator(int a, int b) : base(a, b)
        {
        }

        public void Multiply()
        {
            Console.WriteLine("{0} * {1} = {2}", a, b, a * b);
        }

        public void Divide()
        {
            Console.WriteLine("{0} / {1} = {2}", a, b, a / b);
        }
    }
}
プロジェクト: SampleEx301 / ファイル名: 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 c = new Calculator(10, 3);
            c.Add();
            c.Subtract();

            ExCalculator ec = new ExCalculator(10, 3);
            ec.Add();       //  親クラスのメソッドを呼び出せる
            ec.Subtract();  //  親クラスのメソッドを呼び出せる
            ec.Multiply();
            ec.Divide();
        }
    }
}
実行結果
10 + 3 = 13
10 - 3 = 7
10 + 3 = 13
10 - 3 = 7
10 * 3 = 30
10 / 3 = 3

ExCalculator は Calculator を継承しているので、ExCalculator のインスタンスから Add()Subtract() を呼び出すことができます。また、子クラスのコンストラクタで base(a, b) と記述することで、親クラスのコンストラクタを呼び出し、protected フィールド ab を初期化しています。

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

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

継承関係にあるクラスでインスタンスを生成すると、親クラスのコンストラクタが先に実行され、次に子クラスのコンストラクタが実行されます。デストラクタは逆の順序(子→親)で実行されます。

プロジェクト: SampleEx302 / ファイル名: Super.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SampleEx302
{
    class Super
    {
        public Super()
        {
            Console.WriteLine("Superクラスのコンストラクタ(引数なし)");
        }

        public Super(int a)
        {
            Console.WriteLine("Superクラスのコンストラクタ(引数あり): a={0}", a);
        }

        ~Super()
        {
            Console.WriteLine("Superクラスのデストラクタ");
        }
    }
}
プロジェクト: SampleEx302 / ファイル名: Sub.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SampleEx302
{
    class Sub : Super
    {
        public Sub() : base()
        {
            Console.WriteLine("Subクラスのコンストラクタ(引数なし)");
        }

        public Sub(int a) : base(a)
        {
            Console.WriteLine("Subクラスのコンストラクタ(引数あり): a={0}", a);
        }

        ~Sub()
        {
            Console.WriteLine("Subクラスのデストラクタ");
        }
    }
}
プロジェクト: SampleEx302 / ファイル名: 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();
            Sub s2 = new Sub(10);
        }
    }
}
実行結果
Superクラスのコンストラクタ(引数なし)
Subクラスのコンストラクタ(引数なし)
Superクラスのコンストラクタ(引数あり): a=10
Subクラスのコンストラクタ(引数あり): a=10
Subクラスのデストラクタ
Superクラスのデストラクタ
Subクラスのデストラクタ
Superクラスのデストラクタ

実行結果からわかるとおり、コンストラクタは親(Super)→子(Sub)の順で実行され、デストラクタは子(Sub)→親(Super)の順で実行されています。子クラスのコンストラクタで base() を用いることで、親クラスの対応するコンストラクタを明示的に呼び出しています。

次の図は、引数なし・引数ありそれぞれのケースでコンストラクタがどの順番で呼ばれるかを表しています。

コンストラクタの呼び出し順序①(引数なし)

図3-2. コンストラクタの呼び出し順序①(Sub s1 = new Sub())

コンストラクタの呼び出し順序②(引数あり)

図3-3. コンストラクタの呼び出し順序②(Sub s2 = new Sub(10))

オーバーライド

メソッドのオーバーライド

子クラスでは、親クラスで定義されたメソッドを上書き(オーバーライド)することができます。オーバーライドするには、親クラス側のメソッドに virtual キーワードを付け、子クラス側のメソッドに override キーワードを付けます。

プロジェクト: SampleEx303 / ファイル名: Parent.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SampleEx303
{
    class Parent
    {
        public virtual void Foo()
        {
            Console.WriteLine("Parentクラスの Foo()");
        }
    }
}
プロジェクト: SampleEx303 / ファイル名: Child.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SampleEx303
{
    class Child : Parent
    {
        public override void Foo()
        {
            Console.WriteLine("Childクラスの Foo()");
        }
    }
}
プロジェクト: 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 p  = new Parent();
            Child  c  = new Child();
            Parent pc = new Child();  //  親クラス型の変数に子クラスを代入

            p.Foo();   //  Parentクラスの Foo() が呼ばれる
            c.Foo();   //  Childクラスの Foo() が呼ばれる
            pc.Foo();  //  Childクラスの Foo() が呼ばれる(ポリモーフィズム)
        }
    }
}
実行結果
Parentクラスの Foo()
Childクラスの Foo()
Childクラスの Foo()

3行目の pc は Parent 型の変数ですが、実際には Child クラスのインスタンスを参照しています。pc.Foo() を呼び出すと、実際のインスタンスの型(Child)のメソッドが実行されます。このように、同じ呼び出し形式で異なる動作をすることをポリモーフィズム(多態性)と言います。

オーバーライドとポリモーフィズムのしくみ

図3-4. オーバーライドとポリモーフィズムのしくみ

💡 オーバーロード(同じメソッド名で引数が異なる)とオーバーライド(親クラスのメソッドを子クラスで上書き)を合わせてポリモーフィズムと呼びます。

object クラス

すべてのクラスの親

C#では、すべてのクラスは暗黙的に object クラスを継承しています。object クラスは .NET Framework の基底クラスであり、すべてのクラスが共通して持つ基本的なメソッドを提供しています。

object クラスの主なメソッド
メソッド機能
Equals(object)オブジェクトの等価性を判定する
ToString()オブジェクトを表す文字列を返す
GetType()オブジェクトの型情報を返す
GetHashCode()オブジェクトのハッシュコードを返す
プロジェクト: SampleEx304 / ファイル名: Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SampleEx304
{
    class Program
    {
        static void Main(string[] args)
        {
            object obj = new object();
            Console.WriteLine(obj.ToString());
            Console.WriteLine(obj.GetType());

            int n = 42;
            Console.WriteLine(n.ToString());
            Console.WriteLine(n.GetType());
        }
    }
}
実行結果
System.Object
System.Object
42
System.Int32

int のような値型も object クラスを継承しているため、ToString()GetType() を呼び出すことができます。

練習問題(応用編)3 へ →