第13章 継承とポリモーフィズム
この章の目的
Section titled “この章の目的”この章では、オブジェクト指向プログラミングの重要な考え方である 継承 と ポリモーフィズム を学習します。
これまでに作成してきたクラスは、基本的に独立した 1 つのクラスでした。 しかし、実際のプログラムでは、複数のクラスに共通する情報や処理が出てくることがあります。
たとえば、社員には次のような種類があるかもしれません。
正社員(月給制)契約社員(時給制)それぞれ違いはありますが、共通する情報もあります。
社員ID、氏名、部署名社員情報を表示する処理このような 共通部分を親となるクラスにまとめ、そこから別のクラスを派生させる仕組み が 継承 です。 さらに、共通の型として扱いつつ、実際のオブジェクトに応じて動きを変える考え方が ポリモーフィズム です。
補足:継承は強力だが万能ではない
継承は強力な機能ですが、何でも継承すればよいわけではありません。 本研修では、継承を多用した複雑な設計を目指すのではなく、まずは「読める」「基本的な動きが分かる」ことを重視します。 DB から取得した 1 行を表すような単純なクラスでは、無理に継承を使わないことも多いです。
補足:この章の例について
この章では「正社員」「契約社員」を例に継承を学びます。 SQL 研修の
employees表には正社員/契約社員という区分はありませんが、本章の説明用に拡張した想定として読んでください。 社員 ID と氏名はemployees表に整合しています。
この章でできるようになること
Section titled “この章でできるようになること”この章を終えると、次のことができるようになります。
- 継承の役割をおおまかに説明できる
- 親クラス・子クラス(基底クラス・派生クラス)の関係を説明できる
:を使ってクラスを継承できる- 子クラスから親クラスのプロパティやメソッドを利用できる
: base(...)で親クラスのコンストラクターを呼び出せるvirtualとoverrideの役割を説明できる- 子クラスで親クラスのメソッドを上書きできる
- ポリモーフィズムをおおまかに説明できる
- 親クラス型の変数に子クラスのオブジェクトを代入できる
List<親クラス>で複数種類の子クラスをまとめて扱える- 抽象クラス(
abstract)・抽象メソッドの役割を説明できる - 継承を使ったコードを読めるようになる
本章で使用する環境
Section titled “本章で使用する環境”| 項目 | 内容 |
|---|---|
| 開発環境 | Visual Studio 2022 |
| プロジェクト種類 | コンソール アプリ |
| 対象フレームワーク | .NET 8 |
| ソリューション名 | Chapter13 |
| プロジェクト名 | Ch13_Inheritance |
csproj の Nullable は disable に変更してください
プロジェクト作成後、
Ch13_Inheritance.csprojを開き、<Nullable>disable</Nullable>に変更してください。詳しい手順は、第 1 章「1-1 プロジェクトを作成する」を参照してください。
作業前チェック
Section titled “作業前チェック”作業を始める前に、次の内容を確認してください。
- クラス・プロパティ・メソッド・コンストラクターを書ける(第 7・10 章)
-
private setを使えるか、意味を理解している -
List<T>とforeachを使える(第 12 章) - 自作クラスを別ファイルに書ける
- 第 12 章の内容を Git に提出済みである
13-1 継承の基礎
Section titled “13-1 継承の基礎”共通部分があるクラスを 2 つ書いてみる
Section titled “共通部分があるクラスを 2 つ書いてみる”正社員と契約社員を、それぞれ別々のクラスとして書くとどうなるかを見てみます。
RegularEmployee クラスの仮イメージ:
class RegularEmployee{ public int EmployeeId { get; set; } public string EmployeeName { get; set; } public string DepartmentName { get; set; } public int MonthlySalary { get; set; }
public void PrintProfile() { Console.WriteLine($"社員ID:{EmployeeId}"); Console.WriteLine($"氏名:{EmployeeName}"); Console.WriteLine($"部署:{DepartmentName}"); }}ContractEmployee クラスの仮イメージ:
class ContractEmployee{ public int EmployeeId { get; set; } public string EmployeeName { get; set; } public string DepartmentName { get; set; } public int HourlyWage { get; set; }
public void PrintProfile() { Console.WriteLine($"社員ID:{EmployeeId}"); Console.WriteLine($"氏名:{EmployeeName}"); Console.WriteLine($"部署:{DepartmentName}"); }}両方を比べると、共通部分と異なる部分が分かります。
| 共通部分 | 異なる部分 |
|---|---|
EmployeeId、EmployeeName、DepartmentName | RegularEmployee は MonthlySalary |
PrintProfile() メソッド | ContractEmployee は HourlyWage |
共通部分を毎回書くと、コードが重複します。 この共通部分を親クラスにまとめる のが継承の出発点です。
親クラス EmployeeBase を作る
Section titled “親クラス EmployeeBase を作る”社員に共通する情報・処理を EmployeeBase クラスにまとめます。
EmployeeBase.cs:
namespace Ch13_Inheritance;
class EmployeeBase{ public int EmployeeId { get; set; } public string EmployeeName { get; set; } public string DepartmentName { get; set; }
public void PrintProfile() { Console.WriteLine($"社員ID:{EmployeeId}"); Console.WriteLine($"氏名:{EmployeeName}"); Console.WriteLine($"部署:{DepartmentName}"); }}「Base」という名前は「基本」「土台」の意味で、親クラスにはよく使われる命名です。
子クラスで継承する
Section titled “子クラスで継承する”EmployeeBase を継承して、RegularEmployee を作成します。
RegularEmployee.cs:
namespace Ch13_Inheritance;
class RegularEmployee : EmployeeBase{ public int MonthlySalary { get; set; }}クラス名の後ろに : を書き、続けて親クラス名を書くと 継承 になります。
class 子クラス : 親クラス{ // 子クラス独自の追加メンバー}RegularEmployee には EmployeeId などを書いていませんが、EmployeeBase から 受け継いでいる ので利用できます。
動かしてみる
Section titled “動かしてみる”Program.cs:
namespace Ch13_Inheritance;
internal class Program{ static void Main(string[] args) { RegularEmployee employee = new RegularEmployee { EmployeeId = 1001, EmployeeName = "山田二郎", DepartmentName = "総務", MonthlySalary = 500000 };
employee.PrintProfile(); Console.WriteLine($"月給:{employee.MonthlySalary}円"); }}実行結果:
社員ID:1001氏名:山田二郎部署:総務月給:500000円PrintProfile() も EmployeeId も RegularEmployee 自身には書いていませんが、親クラスから受け継いでいるため使えています。
継承では、次の用語が使われます。
| 用語 | 意味 | この章の例 |
|---|---|---|
| 親クラス / 基底クラス | 継承元のクラス | EmployeeBase |
| 子クラス / 派生クラス | 継承先のクラス | RegularEmployee、ContractEmployee |
「親クラス」と「基底クラス」、「子クラス」と「派生クラス」はほぼ同じ意味です。 現場では両方の表現が出てくるので、両方覚えておきましょう。
13-2 親クラスと子クラスの関係
Section titled “13-2 親クラスと子クラスの関係”複数の子クラスを作る
Section titled “複数の子クラスを作る”EmployeeBase を継承して、もう 1 つ ContractEmployee を作ります。
ContractEmployee.cs:
namespace Ch13_Inheritance;
class ContractEmployee : EmployeeBase{ public int HourlyWage { get; set; } public int WorkHours { get; set; }}両方を使うコード:
namespace Ch13_Inheritance;
internal class Program{ static void Main(string[] args) { RegularEmployee regular = new RegularEmployee { EmployeeId = 1001, EmployeeName = "山田二郎", DepartmentName = "総務", MonthlySalary = 500000 };
ContractEmployee contract = new ContractEmployee { EmployeeId = 1004, EmployeeName = "田中浩介", DepartmentName = "マーケティング", HourlyWage = 1800, WorkHours = 120 };
regular.PrintProfile(); Console.WriteLine($"月給:{regular.MonthlySalary}円");
contract.PrintProfile(); Console.WriteLine($"時給:{contract.HourlyWage}円 / 勤務時間:{contract.WorkHours}時間"); }}実行結果:
社員ID:1001氏名:山田二郎部署:総務月給:500000円社員ID:1004氏名:田中浩介部署:マーケティング時給:1800円 / 勤務時間:120時間どちらの子クラスも、共通部分は EmployeeBase から受け継ぎ、独自部分だけ追加しています。
3 クラスの関係を図にすると、次のようになります。
矢印 <|-- は 「親 → 子」の継承関係(is-a) を表します。
親クラス EmployeeBase の共通メンバーは、両方の子クラスから利用できます。
is-a 関係
Section titled “is-a 関係”継承は is-a 関係 で説明されます。
RegularEmployee is an EmployeeBase → 正社員は社員の一種である
ContractEmployee is an EmployeeBase → 契約社員は社員の一種である「A は B の一種である」と自然に言えるとき、継承の候補になります。 逆に、次のような関係には継承を使いません。
× 社員は部署の一種である× 商品は注文の一種であるこのような場合は、プロパティで関係を表します(Employee が Department プロパティを持つ、など)。
継承を使いすぎない
Section titled “継承を使いすぎない”初学者が継承を学ぶと、何でも親子関係にしたくなることがあります。 しかし、実務では使いすぎると以下のような問題が起きます。
- 親クラスを変えるだけで、すべての子クラスに影響が広がる
- どこに何が書いてあるか追いにくくなる
- 関係が不自然な継承は、後で外しにくい
継承の使いどころ ・「A は B の一種」と自然に言える ・共通部分が明確で、複数の子で本当に共有できる ・現場の既存コードを読むときに必要
無理に使わない方がよいケース ・DB の 1 行を表すだけのクラス ・共通点が少なく、無理に共通化している場合13-3 base で親クラスのコンストラクターを呼び出す
Section titled “13-3 base で親クラスのコンストラクターを呼び出す”親クラスにコンストラクターを用意する
Section titled “親クラスにコンストラクターを用意する”第 10 章で学んだように、コンストラクターでオブジェクト作成時に必要な値を必ず受け取れます。 継承でも、親クラスにコンストラクターを用意できます。
EmployeeBase.cs(改造版):
namespace Ch13_Inheritance;
class EmployeeBase{ public int EmployeeId { get; private set; } public string EmployeeName { get; private set; } public string DepartmentName { get; private set; }
public EmployeeBase(int employeeId, string employeeName, string departmentName) { EmployeeId = employeeId; EmployeeName = employeeName; DepartmentName = departmentName; }
public void PrintProfile() { Console.WriteLine($"社員ID:{EmployeeId}"); Console.WriteLine($"氏名:{EmployeeName}"); Console.WriteLine($"部署:{DepartmentName}"); }}親クラスに引数付きコンストラクターを置くと、子クラスからも引数を渡してあげる必要 が出てきます。
: base(…) で親コンストラクターを呼び出す
Section titled “: base(…) で親コンストラクターを呼び出す”子クラスのコンストラクター宣言の後ろに : base(...) を書くと、親クラスのコンストラクターを呼び出せます。
RegularEmployee.cs(改造版):
namespace Ch13_Inheritance;
class RegularEmployee : EmployeeBase{ public int MonthlySalary { get; private set; }
public RegularEmployee(int employeeId, string employeeName, string departmentName, int monthlySalary) : base(employeeId, employeeName, departmentName) { MonthlySalary = monthlySalary; }}ポイント:
| 部分 | 役割 |
|---|---|
: base(employeeId, employeeName, departmentName) | 親クラス EmployeeBase のコンストラクターを呼ぶ |
MonthlySalary = monthlySalary; | 子クラス独自の値を設定する |
呼び出し:
RegularEmployee employee = new RegularEmployee(1001, "山田二郎", "総務", 500000);
employee.PrintProfile();Console.WriteLine($"月給:{employee.MonthlySalary}円");実行結果:
社員ID:1001氏名:山田二郎部署:総務月給:500000円共通部分は親、個別部分は子
Section titled “共通部分は親、個別部分は子”base を使うと、初期化の役割分担がはっきりします。
共通の値(社員ID、氏名、部署) → EmployeeBase のコンストラクターで設定
子クラス独自の値(月給、時給など) → 子クラス自身のコンストラクターで設定13-4 メソッドを上書きする(virtual / override)
Section titled “13-4 メソッドを上書きする(virtual / override)”子クラスごとに処理を変えたい
Section titled “子クラスごとに処理を変えたい”支給額の計算を考えます。
正社員 → 月給をそのまま支給契約社員 → 時給 × 勤務時間どちらも「支給額を求める」目的は同じですが、計算方法が違います。 このようなときに、親クラスのメソッドを子クラスで上書き します。
virtual と override
Section titled “virtual と override”| 場所 | キーワード | 意味 |
|---|---|---|
| 親クラス | virtual | 「このメソッドは子クラスで上書きしてもよい」 |
| 子クラス | override | 「親のメソッドを上書きする」 |
EmployeeBase に virtual メソッドを追加する
Section titled “EmployeeBase に virtual メソッドを追加する”namespace Ch13_Inheritance;
class EmployeeBase{ public int EmployeeId { get; private set; } public string EmployeeName { get; private set; } public string DepartmentName { get; private set; }
public EmployeeBase(int employeeId, string employeeName, string departmentName) { EmployeeId = employeeId; EmployeeName = employeeName; DepartmentName = departmentName; }
public virtual int GetPaymentAmount() { return 0; // 既定値(子クラスで上書きされる想定) }}各子クラスで override する
Section titled “各子クラスで override する”RegularEmployee.cs:
namespace Ch13_Inheritance;
class RegularEmployee : EmployeeBase{ public int MonthlySalary { get; private set; }
public RegularEmployee(int employeeId, string employeeName, string departmentName, int monthlySalary) : base(employeeId, employeeName, departmentName) { MonthlySalary = monthlySalary; }
public override int GetPaymentAmount() { return MonthlySalary; }}ContractEmployee.cs:
namespace Ch13_Inheritance;
class ContractEmployee : EmployeeBase{ public int HourlyWage { get; private set; } public int WorkHours { get; private set; }
public ContractEmployee(int employeeId, string employeeName, string departmentName, int hourlyWage, int workHours) : base(employeeId, employeeName, departmentName) { HourlyWage = hourlyWage; WorkHours = workHours; }
public override int GetPaymentAmount() { return HourlyWage * WorkHours; }}動かしてみる
Section titled “動かしてみる”namespace Ch13_Inheritance;
internal class Program{ static void Main(string[] args) { RegularEmployee regular = new RegularEmployee(1001, "山田二郎", "総務", 500000); ContractEmployee contract = new ContractEmployee(1004, "田中浩介", "マーケティング", 1800, 120);
Console.WriteLine($"{regular.EmployeeName}:{regular.GetPaymentAmount()}円"); Console.WriteLine($"{contract.EmployeeName}:{contract.GetPaymentAmount()}円"); }}実行結果:
山田二郎:500000円田中浩介:216000円同じ GetPaymentAmount() という呼び方なのに、オブジェクトに応じて計算方法が変わっています。
13-5 ポリモーフィズム
Section titled “13-5 ポリモーフィズム”ポリモーフィズムとは
Section titled “ポリモーフィズムとは”ポリモーフィズム(多態性)は、次のような性質を指します。
同じ呼び出し方で、オブジェクトの実際の型に応じて異なる動きをするvirtual と override を使ったメソッドは、まさにこの動きをします。
親クラス型の変数に子クラスを代入できる
Section titled “親クラス型の変数に子クラスを代入できる”継承関係があると、親クラス型の変数に子クラスのオブジェクトを入れられます。
EmployeeBase employee1 = new RegularEmployee(1001, "山田二郎", "総務", 500000);EmployeeBase employee2 = new ContractEmployee(1004, "田中浩介", "マーケティング", 1800, 120);RegularEmployee も ContractEmployee も「EmployeeBase の一種」なので、EmployeeBase 型の変数に代入できます。
変数の型 → EmployeeBase中に入る実体 → RegularEmployee や ContractEmployeeList<EmployeeBase> でまとめる
Section titled “List<EmployeeBase> でまとめる”親クラス型のリストを使うと、異なる種類の子クラスを 1 つにまとめて扱えます。
namespace Ch13_Inheritance;
internal class Program{ static void Main(string[] args) { List<EmployeeBase> employees = new List<EmployeeBase> { new RegularEmployee(1001, "山田二郎", "総務", 500000), new ContractEmployee(1004, "田中浩介", "マーケティング", 1800, 120) };
foreach (EmployeeBase employee in employees) { Console.WriteLine($"{employee.EmployeeName}:{employee.GetPaymentAmount()}円"); } }}実行結果:
山田二郎:500000円田中浩介:216000円foreach の中で employee の型は EmployeeBase ですが、GetPaymentAmount() を呼ぶと 実際のオブジェクトに応じた処理 が動きます。
図の見方(シーケンス図の基本)
シーケンス図は、時間の流れを縦軸(上から下へ) で表す図です。
- 横に並んだ 柱(
Main・employees・RegularEmployee・ContractEmployee)が 登場人物- 実線の矢印
→は 呼び出し(処理を依頼する。例:GetPaymentAmount()を呼ぶ)- 破線の矢印
⇠は 返り値(結果が返ってくる。例:500000を返す)- Note(箱で囲まれた黄色いメモ)は 補足説明(誰の番か、何の場面か)
この図の流れ:
Mainがemployees(List<EmployeeBase>型のリスト)に「foreachで順に取り出す」と依頼- 1 件目の 実体は
RegularEmployee(山田二郎) →RegularEmployeeのGetPaymentAmount()が動き、500000を返す- 2 件目の 実体は
ContractEmployee(田中浩介) →ContractEmployeeのGetPaymentAmount()が動き、216000を返す呼ぶ側のコードは
employee.GetPaymentAmount()の 1 行だけ なのに、実体に応じて違う実装が動く ── これがポリモーフィズムです。
呼び方(employee.GetPaymentAmount())は同じ なのに、実体に応じて違う実装が動く ── これがポリモーフィズムの基本です。
ポリモーフィズムのメリット
Section titled “ポリモーフィズムのメリット”ポリモーフィズムを使うと、呼び出し側が型を判定する必要がなくなります。
ポリモーフィズムなしの場合:
foreach (EmployeeBase employee in employees){ if (employee is RegularEmployee regular) { Console.WriteLine($"{regular.EmployeeName}:{regular.MonthlySalary}円"); } else if (employee is ContractEmployee contract) { Console.WriteLine($"{contract.EmployeeName}:{contract.HourlyWage * contract.WorkHours}円"); }}補足:
is演算子(型を調べる)上の悪い例で使った
employee is RegularEmployee regularが、ここで初めて出てくるis演算子 です。基底クラス型の変数 is 派生型 変数名と書くと、次の 2 つを 同時に 行います。
- その変数の 実体が指定した型かどうか を調べる(結果は
true/false)- 一致したら、その型として使える 新しい変数(ここでは
regular)に入れる// employee の実体が RegularEmployee なら true。// true のとき、regular に RegularEmployee 型として入るif (employee is RegularEmployee regular){// regular は RegularEmployee 型なので、固有のメンバーを使えるConsole.WriteLine(regular.MonthlySalary);}
isは「基底クラス型でまとめて扱っているが、実体ごとに違う処理をしたい」ときに型を見分ける道具です。 ただし、isで型を分岐するコードが増えてきたら、まずポリモーフィズムに置き換えられないかを考える のがコツです(この後の「ポリモーフィズムありの場合」のように、型が増えても呼び出し側を変えずに済みます)。なお
isは、例外処理で「この例外はどの種類か」を調べるときにも使います(第 24・25 章のex is SqlExceptionなど)。
ポリモーフィズムありの場合:
foreach (EmployeeBase employee in employees){ Console.WriteLine($"{employee.EmployeeName}:{employee.GetPaymentAmount()}円");}新しい子クラス(例:PartTimeEmployee)を追加しても、呼び出し側のコードは変えずに済みます。
13-6 抽象クラス
Section titled “13-6 抽象クラス”EmployeeBase の GetPaymentAmount は本当に必要か?
Section titled “EmployeeBase の GetPaymentAmount は本当に必要か?”ここまでの例では、EmployeeBase.GetPaymentAmount は return 0; というほぼ空の実装でした。
public virtual int GetPaymentAmount(){ return 0;}しかし、よく考えると次のことが言えます。
- 「正社員でも契約社員でもない、ただの
EmployeeBase」は、現実には存在しない GetPaymentAmountの実装は、子クラスごとに必ず定義されるべき
このようなときは、抽象クラス(abstract class)にします。
abstract class と abstract method
Section titled “abstract class と abstract method”抽象クラスは abstract キーワードを付けて定義します。
抽象クラスの中には、実装を持たない抽象メソッド を定義できます。
EmployeeBase.cs(抽象クラス版):
namespace Ch13_Inheritance;
abstract class EmployeeBase{ public int EmployeeId { get; private set; } public string EmployeeName { get; private set; } public string DepartmentName { get; private set; }
public EmployeeBase(int employeeId, string employeeName, string departmentName) { EmployeeId = employeeId; EmployeeName = employeeName; DepartmentName = departmentName; }
public abstract int GetPaymentAmount(); // 実装なし、子クラスで必ず override}3 クラスの関係を図にすると、次のようになります。
EmployeeBaseの<<abstract>>ステレオタイプは「直接インスタンス化できない」を示しますGetPaymentAmount()*の末尾*は 抽象メソッド(実装なし、子で必須実装)を示します- 子クラス側の
GetPaymentAmount()(*なし)は具体実装を持つことを表します
抽象メソッドは:
- メソッド名と引数だけを書き、
{ }の中身は書かない - 末尾はセミコロンで終わる
- 子クラスで 必ず
overrideする必要 がある(そうでないとコンパイルエラー)
抽象クラスは new できない
Section titled “抽象クラスは new できない”抽象クラスは「直接インスタンス化できない、子クラス専用の親クラス」です。
EmployeeBase employee = new EmployeeBase(...); // ← コンパイルエラー「正社員でも契約社員でもない曖昧な社員」を作らせないための仕組みです。
子クラスは override 必須
Section titled “子クラスは override 必須”抽象クラスを継承する子クラスは、抽象メソッドをすべて override する必要があります。
書き方は、これまでの virtual の場合と同じです。
class RegularEmployee : EmployeeBase{ public int MonthlySalary { get; private set; }
public RegularEmployee(int employeeId, string employeeName, string departmentName, int monthlySalary) : base(employeeId, employeeName, departmentName) { MonthlySalary = monthlySalary; }
public override int GetPaymentAmount() { return MonthlySalary; }}override を書き忘れると、コンパイラが「EmployeeBase.GetPaymentAmount() を実装していません」とエラーを出してくれます。
virtual と abstract の違い
Section titled “virtual と abstract の違い”| 種類 | 親クラスでの実装 | 子クラスでの override |
|---|---|---|
| 通常メソッド | 普通の処理 | できない(隠蔽は別概念) |
virtual メソッド | デフォルトの処理あり | 任意(してもしなくてもよい) |
abstract メソッド | 実装なし({ } がない) | 必須 |
13-7 継承を読むときのポイント
Section titled “13-7 継承を読むときのポイント”現場では、自分で継承を設計するより、既存コードを読む 場面の方が多くなります。 継承を含むコードに出会ったら、次の順で確認するとスムーズです。
| ステップ | 確認ポイント |
|---|---|
| 1 | クラス定義の 1 行目で「: 親クラス」を確認(継承しているか) |
| 2 | 親クラスにどんなプロパティ・メソッドがあるか |
| 3 | 子クラスで追加されたメンバーは何か |
| 4 | override が付いているメソッドはどれか(動きが変わる) |
| 5 | 変数や List<T> が 親クラス型 で扱われていないか(ポリモーフィズム) |
EmployeeBase employee = new RegularEmployee(...);employee.GetPaymentAmount(); // ← どの実装が動くかは「実体」で決まる変数の 型は EmployeeBase ですが、実際に動くのは RegularEmployee の実装 です。 このパターンを見抜けると、現場のコードがぐっと読みやすくなります。
よくあるつまずき
Section titled “よくあるつまずき”| つまずき | 原因 | 対応 |
|---|---|---|
| 継承の意味が分からない | 親子関係のイメージが曖昧 | 共通部分を親クラスにまとめると考える |
| 子クラスに書いていないメンバーが使える理由が分からない | 親クラスから受け継いでいることを見落とし | 親クラスの定義を確認する |
: base(...) を忘れてエラー | 親に引数付きコンストラクターがある | 子クラスから base(...) で値を渡す |
virtual を付けずに override でエラー | 親メソッドが上書きを許可していない | 親に virtual(または abstract)を付ける |
override を書き忘れる | 既定では親の処理が動く | override を付けて上書き |
| 親クラス型に子クラスを代入できる理由が分からない | is-a 関係が曖昧 | 子クラスは親クラスの一種 |
List<EmployeeBase> に複数種類を入れられる理由が分からない | 共通の親型として扱う考え方に慣れていない | 全員 EmployeeBase の一種 |
抽象クラスを new してエラー | 抽象クラスは直接インスタンス化できない | 子クラスを new する |
抽象メソッドを override し忘れてエラー | 抽象メソッドは実装必須 | 子クラスで override する |
| 継承を使うべき場面が分からない | 何でも継承しようとしている | 「A は B の一種」と自然に言えるか確認 |
学んだことチェック
Section titled “学んだことチェック”- 継承とは何かを説明できる
- 親クラスと子クラスの関係を説明できる
-
class 子クラス : 親クラスの書き方を理解している - 子クラスから親クラスのプロパティやメソッドを使える
-
: base(...)で親コンストラクターを呼び出せる -
virtualの役割を説明できる -
overrideの役割を説明できる - 同じメソッド呼び出しで実体に応じた動きをすることを説明できる
- 親クラス型の変数に子クラスのオブジェクトを代入できる
-
List<親クラス>で複数種類の子クラスをまとめて扱える - 抽象クラスとは何かを説明できる
- 抽象メソッドが子クラスで実装必須であることを説明できる
- 継承を使ったコードを読むときの確認手順が分かる
- 継承を使いすぎるべきではないことを理解している
研修の進め方によっては、隣の人またはチーム内で説明確認を行います。
次の内容を、自分の言葉で説明してください。
- 継承はどのようなときに使いますか。
- 親クラスと子クラスの違いは何ですか。
: base(...)は何をしていますか。virtualとoverrideは何のために使いますか。- ポリモーフィズムとは、ひとまずどのような考え方ですか。
List<EmployeeBase>にRegularEmployeeとContractEmployeeを入れられるのはなぜですか。- 抽象クラスを直接
newできないのはなぜですか。 - 抽象メソッドと普通の
virtualメソッドの違いは何ですか。
説明するときは、完全な答えでなくても構いません。 自分の言葉で説明しようとすることが大切です。
この章の演習課題に取り組みます。複数のクラスファイルを作る、歯ごたえのある演習です。
本章の進め方
Section titled “本章の進め方”| 段階 | 目安時間 | 内容 |
|---|---|---|
| ① 準備 | 10 分 | ペア確認 + 課題確認(評価対象外) |
| ② ソロ作業 | 35 分 | タイマーで計測。タイマー時点の commit が唯一の評価対象(別ファイルでクラスを複数作るため長めに設定) |
| ③ チーム時間 | 講師指定の発表開始時刻まで | レビュー + 発表者選出 + 実装続行(任意)。発表開始時刻は厳守 |
提出ルール(タイマー方式)
タイマー時点の commit が唯一の評価対象です。タイマー後の書き足しは評価されません。 コミットメッセージ形式:
Chapter13 タイマー提出: <どこまで完成> / <詰まったポイント>(なければ「特になし」) 例:Chapter13 タイマー提出: 必須13-1・13-2完成、発展13-3途中 / List<DeliveryBase> の型でつまずいた
提出方法:Git が使えないときはサーバへコピー
講師の指示があったときは、
pushの代わりにKadai13フォルダを提出先サーバへコピーし、コピー先に提出メモ.txt(「どこまで完成」「詰まったポイント」を記載)を作成してください。
タイマー後のチーム時間の使い方
レビュー・発表者選出・実装続行(任意)を自由配分してください。発表開始時刻は厳守です。
この章の演習の進め方
Section titled “この章の演習の進め方”この章では、課題ごとにコンソールアプリのプロジェクトを作成 します。 各課題では、指定されたプロジェクト名を使ってください。
| 課題 | 必須/発展 | プロジェクト名 | 作成する主なファイル |
|---|---|---|---|
| 課題 13-1 | 必須 | Kd13_01_DeliveryFee | Program.cs、DeliveryBase.cs、RegularDelivery.cs、ExpressDelivery.cs |
| 課題 13-2 | 必須 | Kd13_02_Polymorphism | Program.cs、3 ファイル(課題 13-1 と同じ構成) |
| 課題 13-3 | 発展 | Kd13_03_AbstractClass | Program.cs、3 ファイル(課題 13-1 を発展) |
| 課題 13-4 | 発展 | Kd13_04_ReportBase | Program.cs、ReportBase.cs、SalesReport.cs、AttendanceReport.cs |
| 課題 13-5 | 発展 | Kd13_05_RegionSummary | Program.cs、3 ファイル(課題 13-3 を発展) |
課題用プロジェクトは、課題用ソリューション Kadai13 の中にまとめます(講義用の Chapter13 ソリューションとは分けます)。
Kadai13/ Kadai13.sln Kd13_01_DeliveryFee/ Kd13_02_Polymorphism/ Kd13_03_AbstractClass/ Kd13_04_ReportBase/ Kd13_05_RegionSummary/最初の課題で
Kadai13ソリューションとKd13_01_DeliveryFeeプロジェクトを同時に作成し、2 つ目以降はそのソリューションに プロジェクトを追加 していきます。各プロジェクトの csproj は忘れず<Nullable>disable</Nullable>に変更してください。クラスファイルを自動生成すると
namespace プロジェクト名 { ... }(ブロック形式)で作られます。本研修はファイルスコープ形式に統一するため、namespace Kd13_01_InheritanceBasic;のように;形式へ書き換え、同じプロジェクト内の各ファイルの namespace を揃えてください。
まずは、全員が必須課題に取り組んでください。
この章の演習は「配送料金システム」を題材にします
本文では社員(
EmployeeBase)を題材に継承を学びました。演習では 別の題材=配送(DeliveryBase) で、同じ仕組みを自分で組み立てます。 本文のコードをそのまま写しても解けません。本文で学んだ「継承・base・virtual/override・ポリモーフィズム」の考え方を、配送の仕様に当てはめて 実装してください。
課題 13-1 配送料金を継承で計算する
Section titled “課題 13-1 配送料金を継承で計算する”配送の共通情報を持つ DeliveryBase クラスと、それを継承する 2 つの配送クラスを別ファイルで作成してください。
配送の種類によって 料金の計算式が違う ので、virtual / override で料金計算メソッドを上書きします。
DeliveryBase クラス仕様
- ファイル名:
DeliveryBase.cs - プロパティ(すべて
get; private set;):DeliveryId(int):配送番号Region(string):宛先地域Weight(int):重量(kg)
- 引数付きコンストラクター(3 引数):上記 3 プロパティを設定する
- メソッド
int GetFee():virtualとし、ひとまずreturn 0;を返す(子クラスで上書きする前提) - メソッド
PrintInfo():"配送1 / 東京 / 2kg / 700円"の形式で 1 行表示する(料金はGetFee()を呼んで求める)
RegularDelivery クラス仕様(通常便)
- ファイル名:
RegularDelivery.cs - 継承元:
DeliveryBase - 引数付きコンストラクター(3 引数):
: base(...)で親コンストラクターを呼ぶ GetFee()をoverride:基本料金 500 円 + 重量 × 100 円
ExpressDelivery クラス仕様(速達便)
- ファイル名:
ExpressDelivery.cs - 継承元:
DeliveryBase - 引数付きコンストラクター(3 引数):
: base(...)で親コンストラクターを呼ぶ GetFee()をoverride:基本料金 800 円 + 重量 × 150 円
Main メソッドの処理
new RegularDelivery(1, "東京", 2)とnew ExpressDelivery(2, "大阪", 3)を作成- それぞれ
PrintInfo()を呼び出して表示
実行結果例:
配送1 / 東京 / 2kg / 700円配送2 / 大阪 / 3kg / 1250円通常便:500 + 2×100 = 700 円。速達便:800 + 3×150 = 1250 円。
条件:
- 各クラスを別ファイルに分けて書く
: base(...)で親コンストラクターを呼び出すGetFee()は親でvirtual、子でoverride(計算式は子クラスごとに違う)
課題 13-2 List<DeliveryBase> でポリモーフィズム
Section titled “課題 13-2 List<DeliveryBase> でポリモーフィズム”課題 13-1 で作った 3 つのクラスを使い、複数の配送をまとめて扱ってポリモーフィズムを確認してください。
Main メソッドの処理
List<DeliveryBase>を作成- 次の 4 件を入れる(オブジェクト初期化子は使わず、コンストラクターで作成)
| 種別 | (配送番号, 地域, 重量) |
|---|---|
| 通常便 | (1, “東京”, 2) |
| 速達便 | (2, “大阪”, 3) |
| 通常便 | (3, “東京”, 5) |
| 速達便 | (4, “東京”, 1) |
foreachですべての配送についてPrintInfo()を呼び出して表示- 最後に「料金の合計 を
Sumで求めて表示」(LINQ のSumを使い、GetFee()を合計する)
実行結果例:
配送1 / 東京 / 2kg / 700円配送2 / 大阪 / 3kg / 1250円配送3 / 東京 / 5kg / 1000円配送4 / 東京 / 1kg / 950円合計:3900円条件:
List<DeliveryBase>を使う(RegularDeliveryとExpressDeliveryを同じリストに入れる)foreachの中で型判定のifは書かない(PrintInfo()の中で実体ごとのGetFee()が動く)- LINQ の
Sumを使って合計を算出(deliveries.Sum(d => d.GetFee()))
必須課題が終わった人は、発展課題に取り組んでください。 発展課題からは、仕様だけが提示されます。実装方法は自分で考えてください。
課題 13-3 DeliveryBase を抽象クラスに変更する
Section titled “課題 13-3 DeliveryBase を抽象クラスに変更する”課題 13-1・13-2 の DeliveryBase を、以下の仕様で 抽象クラス に変更してください。
仕様
DeliveryBaseをabstract classに変更するGetFee()をabstractメソッドに変更する({ }を書かずセミコロンで終わる。return 0;の仮実装は削除)RegularDeliveryとExpressDeliveryのGetFeeのoverride実装はそのままPrintInfo()はDeliveryBaseに実装を残してよい(抽象メソッドはGetFee()だけ)Mainメソッドで、new DeliveryBase(...)を コメントアウトして書いておく(コメントを外すとコンパイルエラーになることを確認)
Main メソッドで確認する内容
- 抽象クラスは
newできないこと - 子クラスは普通に
newできること List<DeliveryBase>でまとめて扱えること
実行結果は課題 13-2 と同じでよい。
配送1 / 東京 / 2kg / 700円配送2 / 大阪 / 3kg / 1250円配送3 / 東京 / 5kg / 1000円配送4 / 東京 / 1kg / 950円合計:3900円課題 13-4 ReportBase を作成する(別題材)
Section titled “課題 13-4 ReportBase を作成する(別題材)”社員以外の題材で、抽象クラスとポリモーフィズムを確認してください。
まず、作るクラスの関係を クラス図 で示します。この図を手がかりに、下の仕様に沿って実装 してください (現場では、こうしたクラス図を見ながらコードに起こす場面がよくあります)。
ReportBaseは<<abstract>>(直接newできない親クラス)、Print()*の末尾*は 抽象メソッド(子で必ず実装)を表しますSalesReport・AttendanceReportはReportBaseを継承し、それぞれPrint()をoverrideで実装します- 矢印
<|--は「親 → 子」の継承(is-a)関係です
ReportBase クラス仕様
abstract class ReportBase- ファイル名:
ReportBase.cs - プロパティ:
Title(string、get; private set;) - コンストラクター:
ReportBase(string title) - 抽象メソッド:
public abstract void Print();
SalesReport クラス仕様
- ファイル名:
SalesReport.cs - 継承元:
ReportBase - コンストラクター:
SalesReport()で: base("売上レポート")を呼ぶ Print実装:"=== {Title} ==="の後に「売上レポートを出力します。」を表示
AttendanceReport クラス仕様
- ファイル名:
AttendanceReport.cs - 継承元:
ReportBase - コンストラクター:
AttendanceReport()で: base("勤怠レポート")を呼ぶ Print実装:"=== {Title} ==="の後に「勤怠レポートを出力します。」を表示
Main メソッドの処理
List<ReportBase>にnew SalesReport()とnew AttendanceReport()を入れるforeachでPrint()を呼び出す
実行結果例:
=== 売上レポート ===売上レポートを出力します。=== 勤怠レポート ===勤怠レポートを出力します。課題 13-5 地域別の配送料金集計
Section titled “課題 13-5 地域別の配送料金集計”課題 13-3 を発展させ、宛先地域ごと の配送料金合計を表示してください。
仕様
- 課題 13-2 と同じ 4 件のデータを
List<DeliveryBase>に入れる DeliveryBaseには宛先地域(Region)があるので、地域ごとにグループ化 して料金合計を表示する- グループ化には LINQ の
GroupByを使う
GroupBy のヒント
GroupByは第 12 章では扱っていない初出のメソッドです。 この発展課題で初めて登場します。下のヒントコードをそのまま真似て構いません。GroupBy(キー)は「同じキーを持つ要素どうしをまとめる」メソッドで、まとめた各グループはgroup.Key(キーの値=ここでは地域名)と、そのグループに属する要素の集まりを持ちます。各グループに対してSumで料金を合計します。
var grouped = deliveries.GroupBy(d => d.Region);
foreach (var group in grouped){ int total = group.Sum(d => d.GetFee()); Console.WriteLine($"{group.Key}:{total}円");}実行結果例:
東京:2650円大阪:1250円東京:700 + 1000 + 950 = 2650 円(配送1・3・4)。大阪:1250 円(配送2)。
条件:
List<DeliveryBase>をそのままGroupByで地域別にまとめる- 各グループ内で
Sumを使って料金合計を計算 - ポリモーフィズムにより
GetFee()が型ごとに正しく動くことを確認
提出前チェックリスト
Section titled “提出前チェックリスト”- プログラムを Visual Studio から実行できる
- 親クラスを別ファイルに作成できている
- 子クラスで
: 親クラスの継承を書けている -
: base(...)で親コンストラクターを呼び出せている -
virtualとoverrideを使えている -
List<親クラス>に複数種類の子クラスを入れている -
foreachで型を判定せずに同じメソッドを呼べている - 抽象クラス・抽象メソッドを使えている(発展)
-
private setで親クラスのプロパティを守れている - インデントが整っている
- タイマー時点で commit 済み(または
提出メモ.txtを書いた)
Git への提出
Section titled “Git への提出”タイマーが鳴ったら、その時点の状態を Git に提出します。
git statusgit add .git commit -m "Chapter13 タイマー提出: 必須13-1・13-2完成、発展13-3途中 / 特になし"git push origin mainGit が使えないときは、上記コミットの代わりに Kadai13 フォルダを提出先サーバへコピーし、コピー先に 提出メモ.txt を作成してください(演習課題の「提出方法:Git が使えないときはサーバへコピー」参照)。
Git の詳しい操作は、付録 C「Git のインストールと提出ルール」 を参照してください。
この章のまとめ
Section titled “この章のまとめ”この章では、継承とポリモーフィズムを学習しました。
- 継承は、共通部分を親クラスにまとめ、子クラスで独自部分を追加する仕組み
class 子クラス : 親クラスの形で継承する- 子クラスは親クラスのプロパティ・メソッドを利用できる
: base(...)で親クラスのコンストラクターを呼び出せるvirtual(親)+override(子)で、メソッドを上書きできる- 親クラス型の変数に子クラスのオブジェクトを代入できる
List<親クラス>で複数種類の子クラスをまとめて扱える- 同じメソッド呼び出しでも、実体の型に応じた動きをする(ポリモーフィズム)
- ポリモーフィズムにより、呼び出し側の型判定を減らせる
abstract classは直接newできない、子クラス専用の親クラスabstractメソッドは実装を持たず、子クラスでoverride必須- 継承は便利だが、関係が自然な場合に絞って使う
次章では、インターフェイス を学習します。
インターフェイスは、クラスに「この機能を持っていること」を約束させる仕組みです。 継承や抽象クラスと同じくポリモーフィズムを支えますが、より柔軟な設計に使えます。 現場の既存コード(特に DB 接続や Web フレームワーク)では、インターフェイスが頻繁に登場します。