継承 -極める Part.2-

ホームC#プログラミング応用講義 > 極める Part.2

目次

ソース1

using System;

class Base_Drama
{
    public virtual void Show()
    {
        Console.WriteLine("ドラマベース");
    }
}

class TRICK : Base_Drama
{
    public override void Show()
    {
        Console.WriteLine("トリック");
    }
}

class Gokusen : TRICK
{
    public override void Show()
    {
        Console.WriteLine("ごくせん");
    }
}

class Yamato : Gokusen
{
    public override void Show()
    {
        Console.WriteLine("やまとなでしこ");
    }
}

class Drama
{
    public static void Main(string[] args)
    {
        Base_Drama bd = new Base_Drama();
        Base_Drama t = new TRICK();
        Base_Drama g = new Gokusen();
        Base_Drama y = new Yamato();
        
        bd.Show(); // ドラマベース
        t.Show(); // トリック
        g.Show(); // ごくせん
        y.Show(); // やまとなでしこ
        
        TRICK t2 = new TRICK();
        TRICK g2 = new Gokusen();
        TRICK y2 = new Yamato();
        
        t2.Show(); // トリック
        g2.Show(); // ごくせん
        y2.Show(); // やまとなでしこ
        
        Gokusen g3 = new Gokusen();
        Gokusen y3 = new Yamato();
        
        g3.Show(); // ごくせん
        y3.Show(); // やまとなでしこ
        
        Yamato y4 = new Yamato();
        y4.Show(); // やまとなでしこ
    }
}
今回はこのソースを解釈していきます。しかし、安心してください。新しいことはありません

まず、すべてのクラスの基本クラスである「Base_Drama」クラスを見ます。
その中に仮想メソッドが定義されています。

他の Show() メソッドはすべてこのメソッドを再定義したものです。こう見てみると結構単純ですね。ということで、一気に視点を Main() に移しましょう。

長々と説明する前に、ポイントを述べておきましょう。

変数に入っているオブジェクトのメンバが呼び出される

いろいろな型の変数がありますが、その中に入っているオブジェクトを見てください。t はトリック、g はごくせん、y はやまとなでしこをあらわします。
この文字は型ではなく、その中に入る実体(オブジェクト)にあわせています。(入るのは参照ですが)

表示結果はすべてこの変数にあっているのがわかります。

仮想メソッドと再定義メソッドというのは、「上乗せ(over ride)」の関係にあります。決して隠蔽ではなく、よりわかりやすく利用できるような仕様になっています。
少し詳しく説明すると仮想メソッドというのは「動的」であり、実行時に柔軟に対応するように作られています。

派生クラスは基本クラスをがっぽり飲み込む
オーバーライドでは変数に入っているオブジェクトのメンバが呼び出される

いくつかのポイントを自分で見つけていけば、ホイホイとわかり、実行結果が予想できるようになります。

ソース2

using System;

class Base_Drama
{
    public virtual void Show()
    {
        Console.WriteLine("ドラマベース");
    }
}

class TRICK : Base_Drama
{
    public override void Show()
    {
        Console.WriteLine("トリック");
    }
}

class Gokusen : TRICK
{
    new public virtual void Show()
    {
        Console.WriteLine("ごくせん");
    }
}

class Yamato : Gokusen
{
    public override void Show()
    {
        Console.WriteLine("やまとなでしこ");
    }
}

class Drama
{
    public static void Main(string[] args)
    {
        Base_Drama bd = new Base_Drama();
        Base_Drama t = new TRICK();
        Base_Drama g = new Gokusen();
        Base_Drama y = new Yamato();
        
        bd.Show(); // ドラマベース
        t.Show(); // トリック
        g.Show(); // トリック
        y.Show(); // トリック
        
        TRICK t2 = new TRICK();
        TRICK g2 = new Gokusen();
        TRICK y2 = new Yamato();
        
        t2.Show(); // トリック
        g2.Show(); // トリック
        y2.Show(); // トリック
        
        Gokusen g3 = new Gokusen();
        Gokusen y3 = new Yamato();
        
        g3.Show(); // ごくせん
        y3.Show(); // やまとなでしこ
        
        Yamato y4 = new Yamato();
        y4.Show(); // やまとなでしこ
    }
}
さてさて、今回は面倒ですよ。隠蔽は変な動きをするんです。

4つのクラスの Show() を見てみましょう。
Base_Drama は仮想メソッド、TRICK は Base_Drama の再定義、Gokusen は隠蔽+仮想メソッド、Yamato は Gukusen の再定義です。

派生クラスは基本クラスをがっぽり飲み込む
オーバーライドでは変数に入っているオブジェクトのメンバが呼び出される
new で隠蔽。キャストで切られる

これまで述べてきたのはこの3つの大原則です。複雑になればなるほど、ポイントをつかむというのは最も早い理解方法です。

Base_Drama 型に Base_Drama のオブジェクトに入れると、これは普通の動きをします。
そして Base_Drama型に TRICK のオブジェクトを入れても、キャストは暗黙的に発生しますが「仮想メソッド」「再定義メソッド」という上乗せの関係であるため、切られはせず、オーバーライドそのものの動きをします。

ところが、Base_Drama 型に Gokusen のオブジェクトを入れるときに「隠蔽+キャスト」で、Base_Drama 型に合わせるために Gokusen.Show() がカットされてしまいます。
では「ドラマベース」が表示されるのかと思うと、そうではありません。

派生クラスは基本クラスをがっぽり飲み込む
Gokusen クラスは「Base_Drama」クラスと「TRICK」クラスをがっぽり飲み込んでいます。そのため、いつのまにか Base_Drama.Show() は TRICK.Show() によって再定義・上乗せされているのです。
それで「トリック」が表示されるのです。

Base_Drama 型に Yamato のオブジェクトを入れる際も同じです。
Yamato は Base_Drama と TRICK と Gokusen をすべてがっぽり飲み込んでいますが、new の付いている TRICK.Show() はカットされ、トリックが表示されます。

TRICK 型にオブジェクトを入れる場合はすべて「トリック」が表示されます。これも「がっぽり飲み込んでいる」を考えると当然のことです。

Gokusen 型にオブジェクトを入れる時は、意外とシンプルな動きをしています。
Gokusen.Show() は new がつけられ、全く新しいものとなっていますし、キャストでカットされるという弊害もありません。

なぜなら Gokusen.Show() 以降に new がないため、隠蔽+キャストがありえないためです。
Gokusen 型に Gokusen のオブジェクトを入れると、Base_Drama と TRICK の Show() は Gokusen.Show() によって隠蔽されますから、「ごくせん」が表示されます。

Gokusen 型に Yamato のオブジェクトを入れると、オーバーライドされた「Yamato.Show()」が呼び出されます。
Yamato 型に Yamato のオブジェクトを入れると、当然ながらオーバーライドされた「Yamato.Show()」が呼び出されます。

ここまで理解できれば、もはや十分です。ここまで詳しく継承・オーバーライド・隠蔽を勉強できるサイト・書籍はないと思ってください。

派生クラスは基本クラスをがっぽり飲み込む
オーバーライドでは変数に入っているオブジェクトのメンバが呼び出される
new で隠蔽。キャストで切られる

virtual / override / new のキーワードを見つけたら、上の原則を思い出してください。そうすればきっと解釈できるはずです。

抽象クラス

残念ながら、継承というのは幅が広いため、まだ習得すべき項目が残っています。その残りの部分を習得していきます。

基本クラス
に、共通性の高いメンバをせっせと定義していくわけですが、ときには
完全でなく、おぼつかない基本クラス
完全で、いじられたくないクラス
があります。いわば、「普通でないクラス」もあるはずです。異常なクラスではありませんが、普通に扱うより特別扱いしたほうがいいものです。

そこで、完全でなく、おぼつかない基本クラスは「抽象クラス」として定義します。抽象クラスは派生クラスで再定義されることが前提となります。
抽象クラスには抽象メソッドを含めるのが一般的です。(なくても隠蔽の new を使って再定義できます)

抽象というからには、抽象メソッドは中身を持ちません。すなわち、名前はあるのに実装はしていないということです。
/*
    抽象クラスと抽象メソッド
*/

using System;

abstract class Base_Class
{
    public abstract void Show();
}

class Test : Base_Class
{
    public override void Show()
    {
        Console.WriteLine("抽象メソッド Show() の再定義");
    }
    
    public static void Main(string[] args)
    {
        // Base_Class b = new Base_Class();
        // b.Show();
        
        Base_Class b = new Test();
        b.Show();
        
        Test t = new Test();
        t.Show();
    }
}
抽象クラス・抽象メソッドを定義するには「abstract」キーワードを利用します。
このキーワードは virtual の働きもあるので、Base_Class を基本クラスとする派生クラス Test でオーバーライドすることができます。

抽象クラスは完全ではなく、おぼつかないため、自分でオブジェクトを作ることはできません。だからコメントをかけています。
しかし、基本クラスは基本クラスなので、クラス型として変数を宣言することはできます。

抽象クラスはオブジェクト指向の中では異質な存在です。オブジェクトはそれ単体で完全なデータと動作を持った塊で、基本的にクラスから生成されます。
しかし抽象クラスは完全ではないため、オブジェクトを形成することはできないのです。そのために継承を使うので「継承」の分野で説明しました。

抽象クラスはおぼつかなくて、必ず派生クラスで定義されるということを確認しておけば十分です。それと、抽象メソッドは実装できないということも重要ですね。

継承禁止

抽象クラスの逆、といえば「完全で、いじられたくないクラス」です。それはすなわち、継承できないクラスということになります。

継承できないクラスは非常に簡単です。ただ、継承しなければいいだけです。
変更しなければ言いだけなのに「const」を変更禁止変数につけるように、継承禁止クラスもあるキーワードを使って明示的に禁止します。

そのキーワードが「sealed」です。適当なクラスにこのキーワードを付け、派生させてコンパイルするとエラーがでますから確認しておいてください。

このキーワードをメソッドに使えば「オーバーライドの限定」をすることができます。

このページの1番上のサンプルを見てください。基本クラスからオーバーライドが多段階でなされていますが、この中に「sealed」を入れると、それ以降のオーバーライドができなくなります。
実際にそれを入れてみるとエラーが出ますから確認してください。

「sealed」はシールドと読みますが、映画のミサイルを打つ際などに聞く言葉を想像すればわかりやすいでしょう。「シールド完了!!」、ロックと同じような意味ですね。

さて、これで継承の内容をすべて終えました。もう、いつ何時継承がきても迷わないでしょう。
さらに、継承とは関係のない C# 言語のソース、ひいてはオブジェクト指向という考え方に強くなったのではないでしょうか。

C# プログラミングの中でも山場ですので、わからなければじっくり読み返してください。

[ ステップアップC# ] [ C#プログラミング応用講義 ]