なぜ、継承の理解が重要なのか、その理由
この記事では、当社の新人エンジニア研修の参考にC#を解説します。
前回は「インスタンスの活用」を学びました。今回は、継承(inheritance)について解説します。
継承を使うことで異なるクラスを「一つのグループ」として扱えるようになり、コードの再利用性や拡張性を高めます。
1. すべてのクラスの親クラス object
- すべてのクラスはデフォルトで
object
を継承するか、あるいは別のクラスを継承し、その先祖をたどれば最終的にobject
に行き着きます。
object
クラスが持っている代表的なメソッドには ToString()
, Equals()
, GetHashCode()
, GetType()
などがあります。すべてのクラスがこれらを使える(もしくはオーバーライドできる)のは、継承の仕組みのおかげです。
2. 継承(:
)とは何か?
C#ではあるクラスが他のクラスを継承するには、「:
」記号を使って
class 子クラス : 親クラス
と書きます。
親クラス(スーパークラス、基底クラス)のフィールドやメソッドを子クラス(サブクラス、派生クラス)が引き継ぐ仕組みです。
例: 社員クラス Employee
とエンジニアクラス Engineer
namespace Chap10
{
public class Employee
{
public int Id;
public string Name;
}
}
namespace Chap10
{
public class Engineer : Employee
{
public string Skill;
}
}
使ってみる
namespace Chap10
{
public class Example01
{
public static void Main()
{
Engineer se = new Engineer();
se.Id = 1; // `Employee`で定義したフィールドを利用
se.Name = "今井";
se.Skill = "C#";
Console.WriteLine($"私は{se.Name}。私のスキルは {se.Skill} です。");
}
}
}
<実行結果>
私は今井。私のスキルは C# です。
Engineer
がEmployee
を継承しているため、Engineer
はId
やName
を持っていないように見えても利用できます。
クラス図
継承関係を示すクラス図では、矢印や線を用いて示されることが多いです。
+-----------+ +-----------+
| Employee | | Engineer |
|-----------| |-----------|
| Id | <------ | (inherits Employee)
| Name | | Skill |
| (methods) | | (methods) |
+-----------+ +-----------+
Engineer
はEmployee
を継承
継承のメリット:
- 差分プログラミング: 子クラスは親クラスを再利用しつつ、新しい機能や変更のみを追加できる。
- 親クラスの変数で子クラスを扱える: 「エンジニアは一種の社員」(is-a関係) なら、
Employee e = new Engineer();
のように書けます。
3. 親クラスの変数で子クラスを扱う
「サブクラスは一種のスーパークラス」(is-a関係)
C#では、例えば Car : Vehicle
と定義した場合、車は一種の乗り物なので、Vehicle v = new Car();
が可能です。
namespace Chap10
{
public class Vehicle
{
public virtual void Run()
{
Console.WriteLine("I'm running.");
}
}
public class Car : Vehicle
{
}
public class Example02
{
public static void Main()
{
Vehicle v1 = new Car();
v1.Run(); // "I'm running."
}
}
}
ここで Car
クラスに特別な動作を追加していなければ、Vehicle
側の Run()
メソッドが呼び出されます。C#ではメソッドをオーバーライドするには override
と書き、親のメソッドに virtual
(または abstract
) としておく必要があります。
Vehicle v = new Airplane();
サブクラスをスーパークラスの参照で扱えるのは嬉しい点ですが、一方で、サブクラス固有のメソッドは呼び出せません。例えば、
namespace Chap10
{
public class Airplane : Vehicle
{
public void Fly()
{
Console.WriteLine("I'm flying.");
}
}
public class Example03
{
public static void Main()
{
Vehicle v = new Airplane();
// v.Fly(); // コンパイルエラー:VehicleにFlyがない
}
}
}
キャストしてサブクラスのメソッドを呼ぶ
実体は Airplane
でも、型が Vehicle
だと Fly()
は呼べません。キャストを使って「(Airplane)v
」に変換すれば使えます。
Vehicle v = new Airplane();
Airplane a = (Airplane)v;
a.Fly(); // "I'm flying."
ただし、実体が本当に Airplane
でないなら**InvalidCastException
** となります。
4. メソッドのオーバーライド (Override)
継承で最も重要な機能がオーバーライドです。オーバーライドとは、親クラスのメソッドを子クラスで“上書き”して、処理内容を変えること。C#では下記のように書きます。
namespace Chap10
{
public class Vehicle
{
public virtual void Run()
{
Console.WriteLine("I'm running.");
}
}
public class RacingCar : Vehicle
{
public override void Run()
{
Console.WriteLine("I'm running fast.");
}
}
public class Example05
{
public static void Main()
{
Vehicle v = new RacingCar();
v.Run(); // "I'm running fast."
}
}
}
<実行結果>
I'm running fast.
このように同じRun()
メソッドでも、子クラスでは別の動作をさせられます。
ここで親型変数 Vehicle v
で子のRacingCar
を扱いつつ、実行時には子クラスのRun()
を呼ぶ、これがオブジェクト指向のポリモーフィズム(多態性)です。
ポリモーフィズムの利点
たとえば乗り物というスーパークラスを定義し、各種サブクラス(車、飛行機、船、サーフィン…)をRun()
で走る・飛ぶ・泳ぐ… とオーバーライドさせれば、Vehicle[]
配列に全種類の乗り物を混在させても Run()
の呼び出しが可能です。
namespace Chap10
{
public class PatrolCar : Vehicle
{
public override void Run() => Console.WriteLine("I'm running.");
}
public class Rocket : Vehicle
{
public override void Run() => Console.WriteLine("I'm flying.");
}
public class Ship : Vehicle
{
public override void Run() => Console.WriteLine("I'm sailing.");
}
public class Surfing : Vehicle
{
public override void Run() => Console.WriteLine("I'm surfing.");
}
public class Example06
{
public static void Main()
{
Vehicle[] vs = { new PatrolCar(), new Rocket(), new Ship(), new Surfing() };
foreach (Vehicle v in vs)
{
v.Run();
}
}
}
}
<実行結果>
I'm running.
I'm flying.
I'm sailing.
I'm surfing.
このように同一のインターフェース (Run()
)を呼んでいるにも関わらず、実際の動作がインスタンスによって異なる。これが多態性 (polymorphism) の大きな強みです。
5. ToString()
オーバーライド
前に学んだ System.Object.ToString()
は、C#では「クラス名」や「名前空間.クラス名」を返すデフォルト実装になっています。string
や DateTime
などは自前で ToString()
をオーバーライドして、分かりやすい文字列を返しています。
自作クラスでも、ToString()
をオーバーライドすれば Console.WriteLine(myObj)
の表示内容を制御できます。
namespace Chap10
{
public class Bicycle : Vehicle
{
public int RegistryNumber;
public Bicycle(int registryNumber)
{
this.RegistryNumber = registryNumber;
}
public override string ToString()
{
return $"私の防犯登録番号は {RegistryNumber} です。";
}
}
public class Example07
{
public static void Main()
{
Bicycle bi = new Bicycle(1234);
Console.WriteLine(bi); // => "私の防犯登録番号は 1234 です。"
}
}
}
多くの場合、クラスを作成したら
ToString()
をオーバーライドしておくのが定番です。デバッグやログ出力で役立つからです。
6. コンストラクタ呼び出し順序 (初期化の流れ)
親クラスから順番に初期化
C#でも Java同様に、子クラスのコンストラクタが呼ばれるとき、先に親クラスのコンストラクタが呼ばれるというルールがあります(base(...)
で明示または暗黙的に呼び出し)。
namespace Chap10
{
public class Parent
{
public Parent()
{
Console.WriteLine("Hello from SuperClass");
}
}
public class Child : Parent
{
public Child()
{
Console.WriteLine("Hello from SubClass");
}
}
public class Example08
{
public static void Main()
{
Child c = new Child();
}
}
}
<実行結果>
Hello from SuperClass
Hello from SubClass
親 → 子 の順にコンストラクタが実行されました。
この仕組みをコンストラクタチェーンと呼ぶこともあります。
7. 継承は「最後の手段」
Java同様、C#も単一継承です。1つのクラスは1つの基底クラスしか持てません。
C++のような多重継承(複数クラスから継承)はできません。「diamond problem」などの複雑性を避けるためです。
is-a vs. has-a
Car
is aVehicle
→ 継承(:
)Car
has anEngine
→ フィールドやプロパティで持つ関係
オブジェクト指向設計で継承を使う場合、「子クラスは親クラスの一種か? (is-a)」をよく考え、単に他クラスのメソッドを使いたいから継承するのは避けるべきです。
もしメソッドを使うだけなら「委譲 (delegation)」や「has-a 関係」を使うほうが無理がありません。
public class SomeClass
{
private Engine engine = new Engine(); // has-a
public void Drive()
{
engine.Start();
// ...
}
}
継承を用いると、親クラスの変更が子クラスに波及したり、意図しないメンバーが子クラスに引き継がれるリスクがあります。
8. InvalidCastException
子クラスインスタンスを親クラスの変数に代入できるのが継承の特長ですが、逆に親クラスインスタンスを子クラス型にキャストするときは要注意です。Vehicle v = new Vehicle(); Car c = (Car)v;
のように「乗り物は一種の車?」は成立しないため、InvalidCastException
が発生します。
まとめができたら、アウトプットとして演習問題にチャレンジしましょう。
以上、今回は「継承を使ってクラスをグループ化する」方法について見てきました。