C#入門編

C#入門編(10)オブジェクト指向とは?「ポリモーフィズム(多態性)」 ~条件分岐を使わず型に応じた振る舞いをさせる~

今回は、「ポリモーフィズム(多態性)」について解説します。

ポリモーフィズムとは、あるインスタンスメソッドを呼び出したときに、そのインスタンスの型の種類に応じて実際に呼び出されるメソッドがプログラム実行時に切り替わる仕組みのことです。

例えば、動物型の変数animalがあるとしましょう。

animal.MakeSound()というメソッドを呼び出したとき、animalに代入されたインスタンスが、

  • 動物クラスを継承した犬型インスタンスであれば、「ワン」と出力する
  • 動物クラスを継承した猫型インスタンスであれば、「ニャア」と出力する

というふうに型に応じて処理を切り替えられます。

MakeSoundというメソッドを呼び出しただけで、animalへ代入された型に応じてプログラム実行時に処理を適切に選択できるわけですね。

これにより、条件分岐を使わずに型によって異なる振る舞いをさせることが可能となり、コードの可読性・変更容易性・再利用性が向上します。

本記事ではポリモーフィズムの考え方を「使わなかった場合」と「使った場合」で、どのように可読性・変更容易性・再利用性が変わるのかを、わかりやすく解説します。

プロ太
プロ太
オブジェクト指向の3本柱であるカプセル化・継承・ポリモーフィズムのうち、最後の1本である「ポリモーフィズム」についての内容です。

ここまで勉強すれば、オブジェクト指向の基本はバッチリです!

YouTubeの動画でも解説しているので、ぜひ御覧ください。

演習1:型による条件分岐の問題点

演習1では、型による条件分岐を行っているコードを機能拡張し、問題点についてみていきましょう。

前回演習コードの復習

前回の演習では、店舗で扱っているドリンクと食事のメニュー情報を登録し、HTML形式のテーブルにして出力するプログラムを作成しました。

詳しくは、以下の記事の演習2、3を御覧ください。

C#入門編(9)オブジェクト指向とは?「継承」 ~クラスを機能拡張して再利用する~ 前回の演習では、オブジェクト指向とカプセル化についての基本的な考え方を学びました。 https://prota-p.com/cs...

このコードは以下のような構成になっています。

  • メニューの基本情報を保持するMenuクラスを定義
  • Menuを継承し、DrinkMenu・MainMenuクラスを定義
  • Menu型配列からメニュー表(HTMLコード)を生成するMenuTableGeneratorクラスを定義
  • 上述したクラスを用いてMenu配列を作成し、HTMLコードを出力する処理を実行

このコードを実行すると例えば以下のようなHTMLコードが生成できます。

出力したメニュー一覧のHTML

「補足」の列では以下の情報を表示しています。

  • DrinkMenuクラスのIsColdプロパティの値に基づき、冷たいかどうかの情報を表示する
  • MainMenuクラスのIsVegetarianプロパティの値に基づき、ベジタリアン向けかどうかの情報を表示する

このような表示を行うため、MenuTableGeneratorのGenerateTableメソッドでは、以下のように処理を記述しています。

    public string GenerateTable()
    {
        …
        foreach (Menu menu in menus)
        {
        …
            string note;
            switch (menu)
            {
                case DrinkMenu drinkMenu when drinkMenu.IsCold:
                    note = "(冷)";
                    break;
                case MainMenu mainMenu when mainMenu.IsVegetarian:
                    note = "(菜食)";
                    break;
                default:
                    note = "";
                    break;
            }
            table += $"<td>{note}</td>";
            …
         }
       …
    }

デザートメニューの追加によるプログラムの拡張

ここで、メニュー一覧生成プログラムを次のように拡張することを考えてみましょう。

  • メニューにデザートを新たに追加する
  • デザートは甘さのレベルを独自の情報としてもつ(5段階)
  • メニュー一覧で、デザートの補足情報として甘さのレベルを表示する

このような拡張を行うには、以下のようにコードを修正すればよさそうです。

  • Menuを継承してデザートメニューを表すDessertMenuを定義する
  • DessertMenuは甘さ度合いを表すSweetLevel(5段階)というプロパティをもつ
  • MenuTableGeneratorのGenerateTableメソッドで、Dessert型の場合には、SweetLevelを補足情報として出力するようにする

以下のように新規クラス追加と既存クラスへのコード追加を行うイメージです。

コード追加のイメージ

まず、DesserMenuクラスのコードは次のようになります。

//デザートメニューの情報を格納するクラス(Menuから派生した子クラス)
class DessertMenu : Menu
{
    private int sweetnessLevel;

    public DessertMenu(string name, string description, int price, int sweetnessLevel)
        : base(name, description, price)
    {
        this.sweetnessLevel = sweetnessLevel;
    }

    public int SweetnessLevel
    {
        get { return sweetnessLevel; }
    }
}

DessertMenuをHTMLテーブルに表示するためには、MenuTableGeneratorクラスに以下のようなコードを追加します。

    public string GenerateTable()
    {
        …
        foreach (Menu menu in menus)
        {
        …
            string note;
            switch (menu)
            {
                case DrinkMenu drinkMenu when drinkMenu.IsCold:
                    note = "(冷)";
                    break;
                case MainMenu mainMenu when mainMenu.IsVegetarian:
                    note = "(菜食)";
                    break;
                //★↓このcaseを追加
                case DessertMenu dessertMenu: 
                    note = $"(甘さレベル{dessertMenu.SweetnessLevel})";
                    break;
                default:
                    note = "";
                    break;
            }
            table += $"<td>{note}</td>";
            …
         }
       …
    }

これで、DessertMenu型の場合には補足情報に甘さのレベルを表示できます。

メニュー一覧へチョコレートケーキを追加してみます。

…
Menu[] menus = new Menu[] {
    new MainMenu("黒毛和牛ステーキ", "ジューシーで柔らかなステーキです。", 3000, false),
    new MainMenu("ベジタブルカレー", "野菜をたっぷりと使った、スパイシーなカレーです。", 2400, true),
    new DrinkMenu("メロンソーダ", "爽やかな甘さが楽しめるメロンソーダです。", 400, true),
    new DrinkMenu("ホットコーヒー", "丁寧に焙煎されたコーヒー豆を使用しています。", 500, false),
    //★↓新たに追加
    new DessertMenu("チョコレートケーキ", "しっとりとしたチョコレートケーキです。", 600, 4),
};
…

このプログラムを実行してHTMLコードを生成し、それをブラウザで表示すると以下のようになります。

演習1プログラムの実行結果をブラウザで表示

演習1コードの問題点

デザートメニューを追加するためにクラスを以下のように追加・修正しました。

  • 新規にMenuを継承したDessertMenuクラスを追加
  • 既存のManuTableGeneratorクラスのGenerateTableメソッドを修正

新規のクラス追加だけではなく、既存のクラスにも修正が必要となっていますね。

型を判別して条件分岐を行う方法では、以下のような問題点があります。

  1. 可読性の問題:型や各条件による複雑な条件分岐であることに加え、補足情報を表示する処理が各クラス(MainMenu等)の定義とは別の離れた場所に記述されてしまっている。
  2. 変更容易性の問題点:新しいメニュー(新しいクラス)を追加するたびに、既存コード(MenuTableGenerator)も併せて修正する必要があり、変更に手間がかかり誤りも混入しやすい。
  3. 再利用性の問題点:新しいメニューを追加するたびに既存クラスの修正が必要ということは、既存クラスをブラックボックスな部品として再利用することができない

プロ太
プロ太
この話題は、「C#入門編(7)クラス、メソッドによるコードの部品化」でも扱いましたね。

これから学ぶポリモーフィズムも、コードの可読性・変更容易性・再利用性を向上させるためにある部品化の仕組みの1つなのです。

C#入門編(7)クラス、メソッドによるコードの部品化 ~オブジェクト指向の土台を学ぶ~ 今回は、コードを部品化する演習を行います。 汚いコードを題材として用意して、そのコードを修正して綺麗にしながら、部品化について学...

演習2:ポリモーフィズムを使ったコードの改善 ~抽象メソッド~

演習1のコードにおける問題点を解決するため、ポリモーフィズムの考え方を導入します。

ポリモーフィズムとは、同じ名前のメソッドが異なるクラスで異なる振る舞いをするという概念です。

つまり、同じメソッド名を使っていても、そのメソッドが呼ばれるインスタンスによって実行される内容が変わるということです。

ポリモーフィズムを実現するためには、親クラスで抽象メソッドと呼ばれるメソッドを宣言しておきます。

そして、各子クラスでそれぞれの具体的な振る舞いを具象メソッドとして実装します。

それでは、実際に演習1のコードを修正していきましょう。

ポイント1:抽象メソッドと具象メソッドの使い方

演習1コードのそれぞれのクラス定義において以下を追加します。

  • Menuクラスで、補足情報の文字列を取得する抽象メソッドGetNoteを宣言((a)の部分)
  • Menuの子クラスで、GetNoteの具体的な中身の実装を定義((b1)、(b2)、(b3)の部分)

追加したコードは以下のようになります。

//■===以下はクラス定義===■
//メニューの情報を格納するクラス(DrinkMenu、MainMenuの親クラス)
abstract class Menu
{
    …
    //★(a) 抽象メソッド
    public abstract string GetNote();
}

//飲み物メニューの情報を格納するクラス(Menuから派生した子クラス)
class DrinkMenu : Menu
{
    …
    //★(b1) 具象メソッドを実装
    public override string GetNote()
    {
        return IsCold ? "(冷)" : "";
    }
}

//主菜メニューの情報を格納するクラス(Menuから派生した子クラス)
class MainMenu : Menu
{
    …
    //★(b2) 具象メソッドを実装
    public override string GetNote()
    {
        return IsVegetarian ? "(菜食)" : "";
    }
}

//デザートメニューの情報を格納するクラス(Menuから派生した子クラス)
class DessertMenu : Menu
{
    …
     //★(b3) 具象メソッドを実装
    public override string GetNote()
    {
        return $"(甘さレベル{sweetnessLevel})";
    }
}

(a)では「public abstract string GetNote();」と、メソッドの戻り値、メソッド名、引数のみ宣言されていて、メソッドの中身はありませんね。

抽象メソッドを定義するときには「abstract」を修飾子としてつけます。

各子クラスの(b1)、(b2)、(b3)でこのメソッドの具体的な振る舞いである具象メソッドを実装しています。

「public override string GetNote(){…}」と定義することで、具象メソッドを作成できます。

親クラスで宣言された抽象メソッドへ具体的な実装を与え、具象メソッドを定義するには「override」を修飾子としてつけます。

具象メソッドを定義することをオーバーライドすると言います。

Menuの抽象メソッドGetNoteをオーバーライドして、DessertMenuの具象メソッドGetNoteを定義しています。

ポイント2:抽象メソッドの呼び出し方法

抽象メソッドを使うと、GenerateTableは以下のように修正できます。

(c)がMenu型のGetNote抽象メソッドを呼び出している部分です。

    public string GenerateTable()
    {
        string table = "<table border='1'>";
        table += "<tr><th>メニュー名</th><th>価格(税込み)</th><th>説明</th><th>補足</th></tr>\n";
        foreach (Menu menu in menus)
        {
            table += "<tr>";
            table += $"<td>{menu.Name}</td>";
            table += $"<td>{menu.GetPriceWithTax()}円</td>";
            table += $"<td>{menu.Description}</td>";
            table += $"<td>{menu.GetNote()}</td>"; //★(c) 抽象メソッドを呼び出す
            table += "</tr>\n";
            //※ +演算子で多数の文字列を連結させていくのは効率が悪いため、
            //    本当はStringBuilderクラスを使った方が良いです。
        }
        table += "</table>";
        return table;
    }

型により条件分岐を行うコードに比べて、だいぶ簡潔に書けていますね。

(c)の部分では、Menu型変数menuに対して、抽象メソッドGetNoteを呼び出しています。

menu変数にどの型のインスタンスが代入されているかで、実際にどのGetNoteメソッドが呼び出されるかはプログラム実行時に決定します。

例えば、menu変数にDessert型のインスタンスが代入されていれば、呼び出されるGetNoteメソッドはDessertクラスで定義された以下のものになります。

    public override string GetNote()
    {
        return $"(甘さレベル{sweetnessLevel})";
    }

条件分岐は存在しないのですが、型の種類によって呼び出されるGetNoteメソッドが切り替わっているのですね。

プロ太
プロ太
次のように考えるとわかりやすいかもしれません。

メニューというのは補足情報を取得できる。

具体的には、ドリンクメニューならば冷たいかどうかの補足情報を取得できる。

主菜メニューならばベジタリアン向けかの情報を取得できる。

…といった感じの意味合いが抽象メソッド・具象メソッドの関係です。

問題点がどのように解決されたか

ポリモーフィズムを導入した演習2コードで、型による条件分岐を行っていた演習1コードの問題点がどのように解決したかを確認していきましょう。

可読性

GeenrateTableメソッドにおいて、複雑な型による条件分岐がなくなり、コードが簡潔になりました。

補足情報を表示する処理が各クラス(MainMenu等)の定義にGetNoteメソッドとして記述されるようになり、各クラスにおける事柄が一箇所にまとまっています。

変更容易性

新しいメニュー(新しいクラス)を追加する場合でも、既存コード部分の修正は不要です。

MenuTableGeneratorクラスのGenerateTableメソッドを修正する必要はありません。

新しいメニューにGetNoteメソッドを定義しておけば、GetNote抽象メソッドの呼び出しがプログラム実行時に適切に切り替わります。

DessertMenuインスタンスであれば、DessertMenuのGetNoteメソッドが呼ばれるのです。

なので、コードの変更が容易になりました。

再利用性

既存クラスMenuTableGeneratorは、新しいメニューを追加したときに修正不要となりました。

そのため、既存クラスはブラックボックスな部品として扱うことができます

そのため、既存クラスを部品として取り回しがしやすくなり、再利用性が上がっています。

プロ太
プロ太
MenuTableGeneratorクラス、Menuクラスをメニュー表生成ライブラリとして提供する場合を考えてみましょう。

ポリモーフィズムの考え方を使えば、ライブラリ利用者が新規メニューを作るときには、Menuクラスを派生させてクラスを定義すれば良いだけですね。

なので、このライブラリは再利用性が高く、使い勝手が良いと言えるでしょう。

ポリモーフィズムについてはこちらの記事も参考にしてください。

抽象メソッドと、子クラスにおけるメソッドの具体的な実装については、abstract修飾子・override修飾子の記事もそれぞれ参考にしてください。

講義1:仮想メソッドとは?

親クラスで宣言した抽象メソッドは、子クラスで具象メソッドとして実装が必要です。

具象メソッドを実装しないとコンパイルエラーとなります。

具象メソッドが定義されていなければ、子クラスのインスタンスにおいてそのメソッドの呼び出しを行うことができないため、これは当然ですね。

プロ太
プロ太
子クラスが抽象クラスの場合には、抽象メソッドを実装しなくても問題ありません。

抽象クラスはインスタンス化できないため、そのメソッドが呼び出されることはないためです。

子クラスで具象メソッドを実装しなくても、あらかじめ親クラスでデフォルトの振る舞いを定義しておきたいということもあるかもしれませんね。

例えば演習2のMenuクラスにおいて、GetNoteメソッドにデフォルトの挙動として、”補足情報なし”という文字列を返させたいとします。

このようなデフォルトの振る舞いは、仮想メソッドを使うと以下のように実現できます。

abstract class Menu
{
    …
    //仮想メソッド
    public virtual string GetNote()
    {
        return "補足情報なし";
    }
}

仮想メソッドは「virtual」をメソッドの修飾子としてつけます。

Menuの子クラスでGetNodeをオーバーライドしない場合、この仮想メソッドGetNoteが呼びされます。

オーバーライドするというのは、親クラスの仮想メソッドや抽象メソッドを上書きする(新しい定義に置き換える)という意味となります。

仮想メソッドについては、こちらの記事も参考にしてください。

メソッドだけでなく、プロパティやインデクサといったインスタンスメンバについてもabstract・virtualの修飾子をつけることで、ポリモーフィズムを適用できます。

おまけ:Visual Studioによる抽象メソッド・具象メソッドの確認方法

演習2で説明したように、ポリモーフィズムをうまく使うとコードをより良くできます。

しかし、ポリモーフィズムを使って書かれたコードは、プログラム実行時に呼び出されるメソッドが決定するため、一見するとプログラムの振る舞いがわかりずらいと思うこともあるでしょう。

Visual Studioの支援機能を使うとこのような振る舞いを理解しやすくなります。

ここでは、すぐに使える2つの機能を紹介します。

コードエディタで抽象メソッド・具象メソッドの関係を確認

Visual Studio 2022の最新版では、コードエディタ上で、抽象メソッド(もしくは仮想メソッド)のオーバーライドしている具象メソッド一覧を表示して確認できます。

演習2コードのMenuクラスの抽象メソッドGetNoteをVisual Studioコードエディタで見ると、以下のように「◯↓」のアイコンが左側に表示されています。

コードエディタ上のアイコン

このアイコンをクリックすると、以下のようにGetNoteメソッドをオーバーライドしたメソッド一覧が表示されます。

オーバーライドしたメソッド一覧を表示

ここでメソッドを選ぶと、そのメソッド定義へジャンプすることも可能です。

コードエディタ上で、具象メソッド定義の右側にも「◯↑」のアイコンがあり、親クラスの抽象メソッド(もしくは仮想メソッド)へジャンプできます。

これらの機能を使うと、抽象メソッド・仮想メソッドとオーバーライドした具象メソッドの間を容易に行き来することができ、とても便利です。

プロ太
プロ太
「◯↑」、「◯↓」アイコンはクラス名の左側にもでてきます。

そして、親クラス、子クラスを相互に行き来できます。

デバッガで実際に呼びだされるメソッドを確認

ポリモーフィズムが用いられているコードを理解したい場合、ひとまずコードを動かしてみて、どのメソッドが呼ばれるかを確認するという方法もあります。

以下のように、メソッド呼び出しが行われる直前にブレークポイントを設置します。

ブレークポイントデバッグ

プログラムが停止したらステップ実行を行えば、どのメソッドが呼びだされるかを確認できます。

このような方法を使うと、ポリモーフィズムを用いたコードの理解を効率よく行える可能性があります。

プロ太
プロ太
私は、他人の書いたコードを理解したいときに、この方法でひとまずプログラムの動きをざっくり把握する場合が多いです。

まとめ

今回は、ポリモーフィズムについて説明しました。

ポリモーフィズムを使うと、型による条件分岐を行わずに、型ごとに特化した処理を記述できます。

抽象メソッド・仮想メソッド・オーバーライドを使って、ポリモーフィズムを具体的に実現する方法について学びました。

ポリモーフィズムを使うことでコードの可読性・変更容易性・再利用性が向上します。

カプセル化、継承、ポリモーフィズムという3本柱について一通り説明しました。

今回で、オブジェクト指向についての基本的な話はおしまいです。

次回以降は、名前空間・外部ライブラリ・コレクションなど、アプリケーションを作っていくために必要な事柄を学んでいきましょう。

プロ太
プロ太
引続き、C#やプログラミングの考え方を一緒に学んでいきましょう!

ABOUT ME
プロ太
プログラミングを勉強している人へ情報を発信していきます! ・情報工学分野で博士(工学)の学位取得 ・言語:C# 、Java、C/C++、Python、JavaScript/TypeScript等 ・仕事は主に上流工程(WF開発・Agile開発、OSS開発経験あり) ・趣味で開発:3Dゲーム、Webアプリ、言語処理系等

ご依頼・ご相談について

プログラミング学習のご相談、お仕事のご依頼については、
こちらのお問い合わせページをご確認ください。