継承とは
親クラスと子クラス
オブジェクト指向プログラミングには、継承という概念があります。継承とは、あるクラスのメンバを、他のクラスに引き継がせるという機能です。機能を引き継ぐ元となるクラスを親クラス(ベースクラス、スーパークラス)と呼び、その機能を受け継いで拡張したクラスを子クラス(サブクラス、派生クラス)と呼びます。
C#での継承は、クラス名の後に :(コロン)を付けて親クラス名を記述することで実現します。
図3-1. 継承のイメージ(Vehicle を親クラスとした場合)
C#では、一つのクラスが持てる親クラスは1つだけです(単一継承)。ただし、継承の連鎖は可能で、孫クラス・ひ孫クラス…と階層を深めることができます。
protected 修飾子
継承において重要なアクセス修飾子として、protected があります。private と同様に外部からのアクセスは禁止されますが、子クラスからはアクセスできるという点が異なります。
| 修飾子 | 同クラス内 | 子クラス | 外部クラス |
|---|---|---|---|
public | ○ | ○ | ○ |
protected | ○ | ○ | × |
private | ○ | × | × |
継承の基本
サンプルプログラム
以下のサンプルでは、加算・減算機能を持つ Calculator クラスを親クラスとして、乗算・除算機能を追加した ExCalculator クラスを定義しています。
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);
}
}
}
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);
}
}
}
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 = 7
10 + 3 = 13
10 - 3 = 7
10 * 3 = 30
10 / 3 = 3
ExCalculator は Calculator を継承しているので、ExCalculator のインスタンスから Add() や Subtract() を呼び出すことができます。また、子クラスのコンストラクタで base(a, b) と記述することで、親クラスのコンストラクタを呼び出し、protected フィールド a、b を初期化しています。
継承とコンストラクタ・デストラクタ
コンストラクタの呼び出し順
継承関係にあるクラスでインスタンスを生成すると、親クラスのコンストラクタが先に実行され、次に子クラスのコンストラクタが実行されます。デストラクタは逆の順序(子→親)で実行されます。
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クラスのデストラクタ");
}
}
}
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クラスのデストラクタ");
}
}
}
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);
}
}
}
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 キーワードを付けます。
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()");
}
}
}
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()");
}
}
}
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() が呼ばれる(ポリモーフィズム)
}
}
}
Childクラスの Foo()
Childクラスの Foo()
3行目の pc は Parent 型の変数ですが、実際には Child クラスのインスタンスを参照しています。pc.Foo() を呼び出すと、実際のインスタンスの型(Child)のメソッドが実行されます。このように、同じ呼び出し形式で異なる動作をすることをポリモーフィズム(多態性)と言います。
図3-4. オーバーライドとポリモーフィズムのしくみ
object クラス
すべてのクラスの親
C#では、すべてのクラスは暗黙的に object クラスを継承しています。object クラスは .NET Framework の基底クラスであり、すべてのクラスが共通して持つ基本的なメソッドを提供しています。
| メソッド | 機能 |
|---|---|
Equals(object) | オブジェクトの等価性を判定する |
ToString() | オブジェクトを表す文字列を返す |
GetType() | オブジェクトの型情報を返す |
GetHashCode() | オブジェクトのハッシュコードを返す |
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
42
System.Int32
int のような値型も object クラスを継承しているため、ToString() や GetType() を呼び出すことができます。