前回の演習では、オブジェクト指向とカプセル化についての基本的な考え方を学びました。
今回は、「継承」について解説します。
継承を使うとクラスのデータ構造と操作を再利用して新しいクラスを作成できます。
継承はオブジェクト指向における主要概念の1つである「多態性(ポリモーフィズム)」の土台にもなっています。
YouTubeの動画でも解説しているので、ぜひ御覧ください。
演習1:クラスのデータ構造と操作を再利用 ~継承~
演習1コード
今回はレストランやカフェなどのメニュー一覧を表示するためのプログラムを作ってみましょう。
店舗で扱っているドリンクと食事のメニュー情報を登録し、HTML形式のテーブルにして出力します。
具体的には以下ができるようなプログラムを作ってみます。
- メニュー情報を登録するためのデータ構造として、「ドリンクメニュー」と「主菜メニュー」の2つの種類を用意
- それぞれのメニューには、「メニュー名・説明、税抜き価格」を登録
- ドリンクメニューには「冷たいドリンクかどうか」の情報を追加で登録
- 主菜メニューには「ベジタリアン向けかどうか」の情報を追加で登録
- メニュー情報(メニュー名・説明・税込み価格)をHTMLのテーブル形式で出力
HTMLコードの出力例は以下です。
<html>
<head>
<title>メニュー</title>
</head>
<body>
<h1>メニュー一覧</h1>
<table border='1'>
<tr><th>メニュー名</th><th>価格(税込み)</th><th>説明</th><th>補足</th></tr>
<tr><td>黒毛和牛ステーキ</td><td>3300円</td><td>ジューシーで柔らかなステーキです。</td><td></td></tr>
<tr><td>ベジタブルカレー</td><td>2640円</td><td>野菜をたっぷりと使った、スパイシーなカレーです。</td><td>(菜食)</td></tr>
<tr><td>メロンソーダ</td><td>440円</td><td>爽やかな甘さが楽しめるメロンソーダです。</td><td>(冷)</td></tr>
<tr><td>ホットコーヒー</td><td>550円</td><td>丁寧に焙煎されたコーヒー豆を使用しています。</td><td></td></tr>
</table>
</body>
</html>
これをWebブラウザで表示すると以下のようになります。
これを実現するコード(演習1コード)を以下の順番で見ていき、それぞれポイントを説明します。
- メニュー関連のクラス
- HTMLコードを生成するクラス
- メニューを作成しHTMLコードを生成する処理
メニュー関連のクラスとHTMLコードを生成するクラスは、別々にしています。
ポイント1:メニュー関連のクラス ~継承の基本~
メニュー関連のクラス定義で「継承」を使ってみましょう。
オブジェクト指向プログラミングにおいて、継承とはあるクラスのデータ構造と操作を引き継ぎ、新しいクラスを定義するための機能です。
あるクラスを継承したクラスを子クラスまたは派生クラス・サブクラスなどと呼びます。
継承元のクラスを親クラスまたは基底クラス・スーパークラスと呼びます。
子クラスは親クラスが持つフィールド・メソッド・プロパティなどのメンバを自動的に引き継ぐことができます。
これによって、親クラスのデータ構造や操作を再利用し、効率よく新しいクラスを作成できます。
今回のコードでは、継承を用いて次のようにクラスを設計します。
- メニュー情報を格納するためのMenuクラスを作成
- Menuクラスを継承して、飲み物メニューと主菜メニューをそれぞれ表すDrinkMenuクラスとMainMenuクラスを作成
具体的なコードは以下のようになります。
//■===以下はクラス定義===■
//メニューの情報を格納するクラス(DrinkMenu、MainMenuの親クラス)
abstract class Menu
{
private string name;
private string description;
private int price;
public Menu(string name, string description, int price)
{
this.name = name;
this.description = description;
this.price = price;
}
public string Name
{
get { return name; }
}
public string Description
{
get { return description; }
}
public int Price
{
get { return price; }
}
public int GetPriceWithTax()
{
const double taxRate = 0.1; // 消費税率10%
return (int)Math.Round(Price * (1 + taxRate));
}
}
//飲み物メニューの情報を格納するクラス(Menuから派生した子クラス)
class DrinkMenu : Menu
{
private bool isCold;
public DrinkMenu(string name, string description, int price, bool isCold)
: base(name, description, price)
{
this.isCold = isCold;
}
public bool IsCold
{
get { return isCold; }
}
}
//主菜メニューの情報を格納するクラス(Menuから派生した子クラス)
class MainMenu : Menu
{
private bool isVegetarian;
public MainMenu(string name, string description, int price, bool isVegetarian)
: base(name, description, price)
{
this.isVegetarian = isVegetarian;
}
public bool IsVegetarian
{
get { return isVegetarian; }
}
}
クラスの継承
Menuクラスでは、Drinkクラス・MainMenuクラスで共通に使う以下のデータと操作を定義しています。
- データ:メニュー名、説明、税抜き価格
- 操作:税込み価格の計算
そして、DrinkMenuクラス・MainMenuクラスはそれぞれMenuクラスを継承することで、Menuクラスのデータと操作(フィールド、メソッド、プロパティ)を引き継いでいます。
継承を行うには、クラス定義で「class クラス名 : 親クラス名」というふうに、親クラスを指定します。(例:「class DrinkMenu : Menu」)
子クラスのコンストラクタでは、必ず親クラスのコンストラクタを呼ぶ必要があります。
コンストラクタについては以下の記事も参考にしてください。
親クラスのコンストラクタ呼び出し
親クラスのコンストラクタは、「子クラスのコンストラクタ(…) : base(…)」というふうに「base(…)」で呼び出します。
baseの引数には、子クラスのコンストラクタの引数を指定できます。例えば以下のようになります。
public DrinkMenu(string name, string description, int price, bool isCold)
: base(name, description, price)
{
…
}
親クラスの引数がないコンストラクタを呼びだす場合には省略も可能です。
抽象クラス
Menu・DrinkMenu・MainMenuの3つを定義していますが、Menuクラスは共通部分をまとめるためだけに作られたクラスですね。
つまり、Menuインスタンスについては生成する必要がないということです。(そもそもMenuインスタンスというのは存在していたらおかしいですね)
このような、子クラスを作るためだけに存在しているクラスは「abstract class クラス名」と、abstractキーワードをつけて抽象クラスとして定義するとよいでしょう。
抽象クラスとして定義されたクラスは、インスタンス生成ができなくなります。例えば、以下のコードはコンパイルエラーになります。
Menu menu = new Menu(…);
これで、Menuインスタンスを誤って生成してしまうことを防げますね。
子クラスの使用例
以下は、DrinkMenuクラスを使ってメロンソーダのインスタンスを作る例です。
DrinkMenu melonSoda = new DrinkMenu("メロンソーダ", "爽やかな甘さが楽しめるメロンソーダです。", 400, true);
Console.WriteLine(melonSoda.Name); //メロンソーダと表示 (親クラスで定義)
Console.WriteLine(melonSoda.GetPriceWithTax()); //440と表示 (親クラスで定義)
Console.WriteLine(melonSoda.IsCold); //Trueと表示 (子クラスで定義)
親クラス(Menuクラス)で定義されているデータ構造や操作が引き継がれていますね。
このように、複数のクラスで共通に使うデータや操作を親クラスにまとめることで、可読性・変更容易性・再利用性が向上します。
ポイント2:HTMLテーブルを生成するクラス ~子クラスを親クラスとして扱う~
次に、メニュー情報(メニュー名・説明・税込み価格)をHTMLのテーブル形式で出力するクラスを作ります。
MenuTableGeneratorは以下のようなクラスです。
- コンストラクタの引数として、出力対象のメニュー一覧(Menu型の配列)を与える
- GenerateTableメソッドでHTMLテーブルの文字列を生成する
コードは次のようになります。
//メニューデータ配列からHTMLテーブルを生成するクラス
class MenuTableGenerator
{
private Menu[] menus;
public MenuTableGenerator(Menu[] menus)
{
this.menus = menus;
}
public string GenerateTable()
{
string table = "<table border='1'>";
table += "<tr><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 += "</tr>\n";
//※ +演算子で多数の文字列を連結させていくのは効率が悪いため、
// 本当はStringBuilderクラスを使った方が良いです。
}
table += "</table>";
return table;
}
}
メニューを作成し、MenuTableGeneratorでHTMLコードを生成する例は以下のようになります。
//■===以下は処理===■
//メニューデータを定義し、Menu配列に格納
Menu[] menus = new Menu[] {
new MainMenu("黒毛和牛ステーキ", "ジューシーで柔らかなステーキです。", 3000, false),
new MainMenu("ベジタブルカレー", "野菜をたっぷりと使った、スパイシーなカレーです。", 2400, true),
new DrinkMenu("メロンソーダ", "爽やかな甘さが楽しめるメロンソーダです。", 400, true),
new DrinkMenu("ホットコーヒー", "丁寧に焙煎されたコーヒー豆を使用しています。", 500, false),
};
//メニュー一覧をHTMLコードとして出力
MenuTableGenerator generator = new MenuTableGenerator(menus);
string tableHtml = generator.GenerateTable();
Console.WriteLine("<html>");
Console.WriteLine("<head><title>メニュー</title></head>");
Console.WriteLine("<body>");
Console.WriteLine("<h1>メニュー一覧</h1>");
Console.WriteLine(tableHtml);
Console.WriteLine("</body>");
Console.WriteLine("</html>");
このコードを実行すると、メニュー一覧がHTMLテーブルとして出力されます。
ここでのポイントは、DrinkMenu型やMainMenu型をどちらもMenu型として扱っている点です。
Menu型配列の要素としてDrinkMenuやMainMenuのインスタンスを代入していますね。
飲み物メニューや主菜メニューはメニューの一種なので、メニューとして扱えるということですね。
以下のように考えるとわかりやすいかもしれません。
数学的に捉えると型は集合です。
DrinkMenu集合とMainMenu集合はそれぞれMenu集合の部分集合です。
そのため、DrinkMenu型の変数(正確にはその変数へ代入されているインスタンス)はMenu型としても扱えるわけですね。
あるクラス(型)を継承して新しいクラス(型)を作るということは、数学的には「ある集合の部分集合を定義する」ともみなせるでしょう。
型と集合については、以下の記事も参考にしてください。
演習のコードは配列なので少しわかりにくいかもしれませんが、例えば以下のようなコードが書けるわけですね。
Menu menu = new DrinkMenu("メロンソーダ", "爽やかな甘さが楽しめるメロンソーダです。", 400, true);
Console.WriteLine(menu.GetPriceWithTax()); //440と表示される
実際に、DrinkMenuやMainMenuはMenuのデータ構造・操作を全て引き継いでいるため、Menu型として振る舞えます。
このように、継承関係を使うと様々な子クラスがある場合に、それらを親クラス相当としてまとめて扱うことが可能になります。
継承についてはこちらの記事も参考にしてください。
privateメンバは子クラスからもアクセスすることはできません。
例えば、Menuのnameフィールドは、DrinkMenuからアクセスすることはできません。
もし、外部へは非公開だけど子クラスからはアクセスさせたいという場合にはprotectedを使いましょう。
アクセス修飾子についてはこちらの記事も参考にしてください。
この例も含めこれまでの演習コードでは、1つのファイルに全てのコードが書かれていますね。
実際にプログラムを開発する際には、クラスごとにファイルを適切に分けることが望ましいです。
ファイルをどのように分けて管理するかについては、また別の機会に説明します。
演習2:クラスの型によって条件分岐させる ~is演算子・キャスト演算子~
演習2コード
演習1のコードで、メニュー一覧のHTMLコードを出力できるようになりました。
演習2ではこのコードを発展させて、メニュー一覧のHTMLテーブルへ以下の情報も埋め込むようにしてみます。
- 飲み物が「冷たいドリンクかどうか」
- 主菜が「ベジタリアン向けかどうか」
以下のような表示を行うHTMLコードを出力するようにしてみましょう。
演習2コードは、演習1コードにおけるMenuTableGeneratorクラスのGenerateTableメソッドを一部修正したものとなります。
修正したGenerateTableメソッドは以下のようになります。
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>";
string note;
if(menu is DrinkMenu) //★(a1) is演算子による型判別
{
DrinkMenu drinkMenu = (DrinkMenu)menu; //★(b1) 型キャスト
note = drinkMenu.IsCold ? "(冷)" : "";
}
else if (menu is MainMenu) //★(a2) is演算子による型判別
{
MainMenu mainMenu = (MainMenu)menu; //★(b2) 型キャスト
note = mainMenu.IsVegetarian ? "(菜食)" : "";
}
else
{
note = "";
}
table += $"<td>{note}</td>";
table += "</tr>\n";
}
table += "</table>";
return table;
}
Menu型配列の各要素について、DrinkMenu型かMainMenu型のどちらであるかをis演算子により判断しています。
そして、キャスト演算子を使いMenu型からDrinkMenu型・MainMenu型へそれぞれ型変換し、必要な情報を取得しています。
それぞれ詳しくみていきましょう。
ポイント1:is演算子
is演算子は、オブジェクトが特定の型であるかどうかを判定するために使用されます。
例えばコードの(a1)では、menuインスタンスがDrinkMenu型かMainMenu型かを判定するために使用しています。
if(menu is DrinkMenu) //★(a1) is演算子による型判別
{
…
}
menuがDrinkMenu型であれば条件式がtrueとなり、if文の中身が実行されます。
ポイント2:キャスト演算子
キャスト演算子は、型の変換を行うために使用されます。
例えばコードの(b1)では、menuオブジェクトがDrinkMenu型であると判定された場合、DrinkMenu型へキャストしてisColdの値を取得しています。
if(menu is DrinkMenu) //★(a1) is演算子による型判別
{
DrinkMenu drinkMenu = (DrinkMenu)menu; //★(b1) 型キャスト
note = drinkMenu.IsCold ? "(冷)" : "";
}
このプログラムでは、Menu型を子クラスであるDrinkMenu型とMainMenu型へキャストしています。
この例のように親クラスから子クラスへ変換することをダウンキャストと呼びます。
ダウンキャストは失敗する可能性もあるため注意が必要です。
例えば、menu変数に格納されたインスタンスがDrinkMenu型でない場合にDrinkMenu型にキャストしようとすると、実行時エラーが発生します。
そのため、キャスト前にis演算子によってオブジェクトの型を確認しています。
is演算子と似た演算子として、指定した型に変換できる場合は変換を行い、できない場合はnullを返すas演算子もあります。
as演算子を使うことでより簡潔に型変換を記述できる場合があります。
is演算子、as演算子についてはこちらの記事も参考にしてください。
演習3:簡潔な条件分岐の書き方 ~パターンマッチング~
演習2のコードは、型を判別して型キャストを行うコードが煩雑ですね。
C#では、パターンマッチングという仕組みを使って、このような型による分岐を簡潔に記述できます。
パターンマッチングは非常に機能が豊富で奥深いです。
今回は、その機能の一部を見ていただき、雰囲気を掴んでもらえれば十分です。
switch文によるパターンマッチング
以下がパターンマッチングを使って修正したGenerateTableメソッド(演習3コード)です。(条件分岐に関する部分のみ記載しています)
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>";
…
}
…
}
switch文を用いたパターンマッチングを行い、DrinkMenu型・MainMenu型で条件分岐を行い、それぞれに対応する処理を記述しています。
switch文を用いたパターンマッチングの書き方は以下になります。
switch (確認対象の変数)
{
case 型1 変数1 when 条件式1:
処理1
break;
…
case 型N 変数N when 条件式N:
処理N
break;
default:
case1~Nにあてはまらない場合の処理
break;
}
「case 型名 変数」で確認対象の変数の型が一致し、かつ「when 条件式」の条件式が成立すれば処理を実行します。
演習コード2よりも簡潔に記述できていますね。
参考:switch式によるパターンマッチング
参考までに、swtich式という記法を用いると以下のように、さらに簡潔に記述することも可能です。
public string GenerateTable()
{
…
string note = menu switch
{
DrinkMenu drinkMenu when drinkMenu.IsCold => "(冷)",
MainMenu mainMenu when mainMenu.IsVegetarian => "(菜食)",
_ => "",
};
table += $"<td>{note}</td>";
…
}
_や=>など見慣れない記法ができていて少し難しいですね。
でも、なんとなく書いてある意味はわかるかと思います。
ここでは、このように簡潔に書ける方法があるということだけ覚えておいてもらえれば大丈夫です。
型による分岐をより簡潔に記述したいと思ったときには、パターンマッチングについて色々と調べてみるとよいでしょう。
C#のパターンマッチングは機能強化が続いており、簡潔に条件分岐を記述するための記法が次々に導入されています。
こちらの記事も参考にしてください。
パターンマッチングについては、また別の機会に改めて紹介したいと思います。
オブジェクト指向では、多態性(ポリモーフィズム)を使うことで、子クラスの型に応じて処理の内容を変えることができます。
多態性については、次の記事で解説します。
C#はオブジェクト指向がベースですが、関数型プログラミングの方法論なども取り込まれており、マルチパラダイムの言語と言われています。
講義1:継承とクラス階層
C#のクラス継承においては何段階でも継承が可能です。
Aクラスを継承したBクラスをさらにCクラスが継承する、ということができます。
これにより、クラス階層が形成されます。
C#のクラスはObject型を頂点として、以下のようなクラス階層を形成しています。
C#の型には値型と参照型がありましたね。
値型はObjectの子クラスであるValueTypeから派生しています。
参照型はValueType型から派生していない型ということになります。
クラス定義をするときに親クラスを指定しない場合、自動的にObject型が親クラスとなります。
演習で作成したMenuは、Objectの子クラスとなります。
まとめ
今回は、継承とクラス階層について説明しました。
継承を使うとクラスを機能拡張して再利用できます。
型の種類によって条件分岐を行う方法についても学びました。
is演算子や、パターンマッチングを用いると型の種類を判別し処理内容を変えることができます。
次回は、オブジェクト指向の3本柱における最後の1本「多態性(ポリモーフィズム)」について解説します。
多態性を用いると、条件分岐を用いずに型の種類に応じた処理を行えます。