継承(inheritance)を理解しよう!

[C#] 継承(inheritance)を理解しよう!

※ 当サイトは広告を含みます。

ここではC#の継承について学びます。これはクラスの延長線の話になりますが少し難しいです。
まずはクラスの基礎知識が必要なので、忘れてしまった人は以下で復習しましょう。

継承(inheritance)

継承とは、あるクラスを元に新しいクラスを定義することです。
こうすることで新しく定義したクラスは元になったクラスの全ての機能を有することができます。

簡単に言えば親子関係です。親(A)を元に子(B)を定義することができます。
人間なら全ての特性を引き継ぐことはありませんが、この世界では元の特性を完全に引き継いだ子孫が誕生します。

まずは以下の記述方法とサンプルコードを実行してください。

継承

namespace Sample
{
  // 親クラス
  public class Parent
  {
    // 喋る機能
    public void Speak()
    {
      System.Console.WriteLine("Hello");
    }
  }

  // 子クラス
  public class Child : Parent
  {
  }

  internal class Program
  {
    static void Main(string[] args)
    {
      Parent p = new Parent();
      p.Speak();

      Child c = new Child();
      c.Speak();
    }
  }
}

この様に、子クラスにはSpeak()を定義してないのに、子クラスはSpeak()を利用することができます。
今回は簡素化するためにメソッドを1つだけ記述しましたが、フィールドでもプロパティでも同じです。

また、呼び方も親クラスや子クラスとは言いません。

正しい名称は元になるクラスが基底(base)クラスで、それを元に定義するクラスが派生(derived)クラスです。
先程の例なら、Parentが基底クラス、Childが派生クラスになります。

ちなみに継承関係に制限はないので、無限に継承することができます。
つまりは基底クラスを継承した派生クラスを、さらに継承する派生クラスを作っても問題ありません。

object型の継承

以前、C#では全てのクラスはobject型を継承していると伝えたことを覚えてますでしょうか?
これはC#における絶対のルールとなり、それを証明するのが以下のサンプルコードです。


namespace Sample
{
  public class Base
  {
  }

  public class Derived : Base
  {
  }

  internal class Program
  {
    static void Main(string[] args)
    {
      var b = new Base();
      var d = new Derived();

      System.Console.WriteLine(b.ToString()); // 定義していないメソッド
      System.Console.WriteLine(d.ToString()); //   〃
    }
  }
}

この様に、どこにもToString()なんてメソッドは定義してないのに呼び出すことができます。
他にもobject型Equals()とかGetType()を所有しますが、これは本題とズレるので先に進みます。

気になる人は以下の公式ドキュメントを読むことをオススメします。
https://learn.microsoft.com/ja-jp/dotnet/api/system.object

派生クラスは基底クラスである

基底クラスの全てを有する派生クラスは、それ自身が基底クラスとも言えます。

りさ
りさ

難しく言ってるだけで当たり前では?

管理人
管理人

まぁ、聞きなさい。

何を当たり前のことを言ってんだと思うでしょうが、これは重要です。
C#のように型に厳密な言語ほど、この話は深く関係します。

まずは次のサンプルコードを実行しましょう。


namespace Sample
{
  // 乗り物
  public class Vehicle
  {
    public void Move()
    {
      System.Console.WriteLine("動く");
    }
  }

  // ロケット
  public class Rocket : Vehicle
  {
    public void Fly()
    {
      System.Console.WriteLine("飛ぶ");
    }
  }

  internal class Program
  {
    static void Main(string[] args)
    {
      // 派生クラスは基底クラスの型に代入することが可能
      Vehicle r = new Rocket();
      r.Move();

      // [ビルドエラー] これは呼び出しが不可能
      //r.Fly();
    }
  }
}

型に厳密なC#でも、派生クラスを基底クラスの型に代入することは許されます。
これは派生クラスが基底クラスの全てを有するので、代入しても動作上の問題が一切ないからです。

ただし、呼び出しは型に依存するため、先程の例のように基底クラスに存在しない機能は呼び出せません。
当然ですが、基底クラスは派生クラスを有するとは限らないからです。先程の例なら、乗り物が必ず飛べるわけではありません。

アップキャストとダウンキャスト

基底クラスと派生クラスでキャスト(代入も同じ)することに名前が付いてます。
両方とも名前のままで、派生クラスを基底クラスにするとアップキャスト、逆に基底クラスを派生クラスにするとダウンキャストです。

先程も伝えましたが、アップキャストに該当するキャストは絶対に安全が保証されます。
これは暗黙的キャストの扱いになり、特別なことをしなくてもキャストが可能です。

対してダウンキャストは危険です。

明示的キャストによりキャストが可能ですが、その時に情報が欠損(実際は基底クラスとか)してると例外が出てプログラムが死にます。
これを安全に行う方法は別の機会に解説するので、基本的に明示的キャストを利用したダウンキャストは使わないと思ってください。

りさ
りさ

明示的キャストいらなくない?

管理人
管理人

僕もそう思うんだけど、どうしても使わないと駄目な場面があって仕方ない感じ。

実際に動作する型を理解する

先程の話の延長ですが、派生クラスの型で生成したインスタンスを基底クラスにキャスト(代入)できることは説明しました。
では、その時に基底クラスになった派生クラスの情報は消えてしまうのでしょうか?

正解は消えません。あくまでアクセス用の型が変わっただけで派生クラスの情報は保持してます。
つまり、実際に動作している型と宣言されてる型に違いが生じてます。

では、これをコントロールするにはどうするのでしょうか?

コントロール自体は簡単で、既に何度もやってますがnewする時の型が実際に動作する型です。
要はインスタンスの生成に依存します。newした型が派生クラスなら派生クラス型、基底クラスなら基底クラス型になります。

この動作中の型が非常に重要で、この先に出てくるポリモーフィズムを理解するために必要になります。
自分が生成したインスタンスが実際に何の型で動作してるかは常に意識しましょう。

ポリモーフィズム(polymorphism)

現時点では理解するための情報が足りないので簡単に触れるだけにします。
これは継承と深い関係があり、この仕組みを利用しないならC#を選ぶ意味がありません。

記述が不明な部分は無視して次のサンプルコードを実行してください。


namespace Sample
{
  // 神のカード
  public abstract class GodCard
  {
    public abstract void Attack();
  }

  // オシリスの天空竜
  public class Osiris : GodCard
  {
    public override void Attack()
    {
      System.Console.WriteLine("超電導波サンダーフォース");
    }
  }

  // オベリスクの巨神兵
  public class Obelisk : GodCard
  {
    public override void Attack()
    {
      System.Console.WriteLine("ゴッド・ハンド・クラッシャー");
    }
  }

  // ラーの翼神竜
  public class Ra : GodCard
  {
    public override void Attack()
    {
      System.Console.WriteLine("ゴッド・ブレイズ・キャノン");
    }
  }

  internal class Program
  {
    static void Main(string[] args)
    {
      // 基底クラスの配列に派生クラスを入れる
      GodCard[] gods = new GodCard[]
      {
        new Osiris(),
        new Obelisk(),
        new Ra(),
      };

      // 基底クラスの型でアクセス
      foreach (GodCard g in gods)
        g.Attack();
    }
  }
}
りさ
りさ

もう少し普通のサンプルは無かったんですか?

管理人
管理人

神のカードは一般常識だが?

りさ
りさ

...

さて、この動作ですが凄いとは思いませんか?

メソッドへのアクセスは基底クラスの型なのに、実際に実行されてるのは派生クラスのAttack()メソッドです。
しかも、インスタンスを管理する配列は基底クラスの型となり、まるで基底クラスの集合に見えます。

ここでは深くは触れませんが、このようにインスタンス自体の型で動作が変わる仕組みをポリモーフィズムと言います。

コンストラクタの動作

継承した場合でもコンストラクタは呼び出されます。そして、その時の順番は基底クラスから順番に処理されます。
仮に自身より上位に複数の基底クラスが存在する場合、より上位の基底クラスから呼び出しが行われます。

ただし、基底クラスの引数ありコンストラクタは特殊な扱いになり、明示的に呼び出さないと実行されせん。

それを確認するために次のサンプルコードを実行してください。


namespace Sample
{
  public class Base
  {
    public Base()
    {
      System.Console.WriteLine($"Base()");
    }

    public Base(string name)
    {
      System.Console.WriteLine($"Base({name})");
    }
  }

  public class Derived : Base
  {
    public Derived()
    {
      System.Console.WriteLine($"Derived()");
    }

    public Derived(string name) : base(name) // base(引数)で基底クラスのコンストラクタを呼び出す
    {
      System.Console.WriteLine($"Derived({name})");
    }
  }

  internal class Program
  {
    static void Main(string[] args)
    {
      var d1 = new Derived();
      var d2 = new Derived("りさ");
    }
  }
}

該当する場所を抜き出すと以下です。


public Derived(string name) : base(name)

そして仕様を文字化するならこうなります。


アクセス修飾子 コンストラクタ(引数) : base(基底クラスの引数)

この様に、基底クラスの引数ありコンストラクタを呼び出したい場合は明示的に呼び出します。
明示的な指示をしない場合、引数なしコンストラクタが自動的に呼び出されます。

管理人
管理人

上位の基底クラスから順に実行される理由は、派生クラスが基底クラスの機能を使う時、基底クラスは初期化が完了してる必要があるからだね。

りさ
りさ

なるほど。先に派生クラスから初期化したら基底クラスの初期化が終わってる保証がないからですね。

多重継承

先に伝えますがC#では禁止されてます。はっきり言って闇の仕様なので無いほうがいいです。

これは継承して継承するって意味ではなく、1つのクラスが同時に複数のクラスを継承することを言います。
C#では動作しませんが、他の言語を参考に記述するならこんな感じになります。


public class Base1
{
}

public class Base2
{
}

public class Base3
{
}

public class Derived : Base1, Base2, Base3
{
}

さて、許可されてる言語もあるわけですが、これは何が問題なのでしょうか?
正直、誰でも気が付きそうですが、名前があってダイヤモンド継承問題と言います。

ダイヤモンド継承問題https://ja.wikipedia.org/wiki/%E8%8F%B1%E5%BD%A2%E7%B6%99%E6%89%BF%E5%95%8F%E9%A1%8C

リンク先の図を見れば分かりますが、こんな仕様を許可したら、どっかでフィールドやメソッドが被ります。
その時に上位の基底クラスの判断が難しく、複数の基底クラスに同じメソッドがあったりして崩壊します。

りさ
りさ

それでも同じような機能は継承したいと思いませんか?

管理人
管理人

安心してくれ。ここでは学ばないけど、それを叶えるインターフェイスって仕組みがC#にはある。

sealed修飾子

個人的にこの仕組み必要か?って思うんですが、クラスやメソッドにsealedを付けると継承禁止になります。
正確にはメソッドの場合はオーバーライド禁止と言います。このオーバーライドについては別枠で解説します。

つまり継承先で絶対に書き換えて欲しくない部分に付けます。

がっ、そもそもクラスを継承して書き換える時点で自己責任だと思うので、僕個人はsealedに必要性を感じません。
とは言え、そういう仕様もあるってことでサンプルコードを置いておきます。


namespace Sample
{
  public sealed class Base
  {
  }

  // 継承禁止だからビルドエラーになる
  public class Derived : Base
  {
  }

  internal class Program
  {
    static void Main(string[] args)
    {
    }
  }
}

あとがき

クラスや継承の理解がC#で1番難しいと思いますが、この機能を使わないC#では意味を成しません。
また、昨今の言語ではクラスに関する知識は基礎レベルとなり、どれを選んでも逃げることはできません。

◆ C#に関する学習コンテンツ

この記事は参考になりましたか?

関連記事

コメント

この記事へのコメントはありません。