Skip to content

第13章 継承とポリモーフィズム

この章では、オブジェクト指向プログラミングの重要な考え方である 継承ポリモーフィズム を学習します。

これまでに作成してきたクラスは、基本的に独立した 1 つのクラスでした。 しかし、実際のプログラムでは、複数のクラスに共通する情報や処理が出てくることがあります。

たとえば、社員には次のような種類があるかもしれません。

正社員(月給制)
契約社員(時給制)

それぞれ違いはありますが、共通する情報もあります。

社員ID、氏名、部署名
社員情報を表示する処理

このような 共通部分を親となるクラスにまとめ、そこから別のクラスを派生させる仕組み継承 です。 さらに、共通の型として扱いつつ、実際のオブジェクトに応じて動きを変える考え方が ポリモーフィズム です。

補足:継承は強力だが万能ではない

継承は強力な機能ですが、何でも継承すればよいわけではありません。 本研修では、継承を多用した複雑な設計を目指すのではなく、まずは「読める」「基本的な動きが分かる」ことを重視します。 DB から取得した 1 行を表すような単純なクラスでは、無理に継承を使わないことも多いです。

補足:この章の例について

この章では「正社員」「契約社員」を例に継承を学びます。 SQL 研修の employees 表には正社員/契約社員という区分はありませんが、本章の説明用に拡張した想定として読んでください。 社員 ID と氏名は employees 表に整合しています。


この章でできるようになること

Section titled “この章でできるようになること”

この章を終えると、次のことができるようになります。

  • 継承の役割をおおまかに説明できる
  • 親クラス・子クラス(基底クラス・派生クラス)の関係を説明できる
  • : を使ってクラスを継承できる
  • 子クラスから親クラスのプロパティやメソッドを利用できる
  • : base(...) で親クラスのコンストラクターを呼び出せる
  • virtualoverride の役割を説明できる
  • 子クラスで親クラスのメソッドを上書きできる
  • ポリモーフィズムをおおまかに説明できる
  • 親クラス型の変数に子クラスのオブジェクトを代入できる
  • List<親クラス> で複数種類の子クラスをまとめて扱える
  • 抽象クラス(abstract)・抽象メソッドの役割を説明できる
  • 継承を使ったコードを読めるようになる

項目内容
開発環境Visual Studio 2022
プロジェクト種類コンソール アプリ
対象フレームワーク.NET 8
ソリューション名Chapter13
プロジェクト名Ch13_Inheritance

csproj の Nullable は disable に変更してください

プロジェクト作成後、Ch13_Inheritance.csproj を開き、<Nullable>disable</Nullable> に変更してください。

詳しい手順は、第 1 章「1-1 プロジェクトを作成する」を参照してください。


作業を始める前に、次の内容を確認してください。

  • クラス・プロパティ・メソッド・コンストラクターを書ける(第 7・10 章)
  • private set を使えるか、意味を理解している
  • List<T>foreach を使える(第 12 章)
  • 自作クラスを別ファイルに書ける
  • 第 12 章の内容を Git に提出済みである

共通部分があるクラスを 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}");
}
}

両方を比べると、共通部分と異なる部分が分かります。

共通部分異なる部分
EmployeeIdEmployeeNameDepartmentNameRegularEmployee は MonthlySalary
PrintProfile() メソッドContractEmployee は HourlyWage

共通部分を毎回書くと、コードが重複します。 この共通部分を親クラスにまとめる のが継承の出発点です。


社員に共通する情報・処理を EmployeeBase クラスにまとめます。

EmployeeBase.cs:

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」という名前は「基本」「土台」の意味で、親クラスにはよく使われる命名です。


EmployeeBase を継承して、RegularEmployee を作成します。

RegularEmployee.cs:

RegularEmployee.cs
namespace Ch13_Inheritance;
class RegularEmployee : EmployeeBase
{
public int MonthlySalary { get; set; }
}

クラス名の後ろに : を書き、続けて親クラス名を書くと 継承 になります。

class 子クラス : 親クラス
{
// 子クラス独自の追加メンバー
}

RegularEmployee には EmployeeId などを書いていませんが、EmployeeBase から 受け継いでいる ので利用できます。


Program.cs:

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()EmployeeIdRegularEmployee 自身には書いていませんが、親クラスから受け継いでいるため使えています。


継承では、次の用語が使われます。

用語意味この章の例
親クラス / 基底クラス継承元のクラスEmployeeBase
子クラス / 派生クラス継承先のクラスRegularEmployeeContractEmployee

「親クラス」と「基底クラス」、「子クラス」と「派生クラス」はほぼ同じ意味です。 現場では両方の表現が出てくるので、両方覚えておきましょう。


13-2 親クラスと子クラスの関係

Section titled “13-2 親クラスと子クラスの関係”

EmployeeBase を継承して、もう 1 つ ContractEmployee を作ります。

ContractEmployee.cs:

ContractEmployee.cs
namespace Ch13_Inheritance;
class ContractEmployee : EmployeeBase
{
public int HourlyWage { get; set; }
public int WorkHours { get; set; }
}

両方を使うコード:

Program.cs
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 関係 で説明されます。

RegularEmployee is an EmployeeBase
→ 正社員は社員の一種である
ContractEmployee is an EmployeeBase
→ 契約社員は社員の一種である

「A は B の一種である」と自然に言えるとき、継承の候補になります。 逆に、次のような関係には継承を使いません。

× 社員は部署の一種である
× 商品は注文の一種である

このような場合は、プロパティで関係を表します(EmployeeDepartment プロパティを持つ、など)。


初学者が継承を学ぶと、何でも親子関係にしたくなることがあります。 しかし、実務では使いすぎると以下のような問題が起きます。

  • 親クラスを変えるだけで、すべての子クラスに影響が広がる
  • どこに何が書いてあるか追いにくくなる
  • 関係が不自然な継承は、後で外しにくい
継承の使いどころ
・「A は B の一種」と自然に言える
・共通部分が明確で、複数の子で本当に共有できる
・現場の既存コードを読むときに必要
無理に使わない方がよいケース
・DB の 1 行を表すだけのクラス
・共通点が少なく、無理に共通化している場合

13-3 base で親クラスのコンストラクターを呼び出す

Section titled “13-3 base で親クラスのコンストラクターを呼び出す”

親クラスにコンストラクターを用意する

Section titled “親クラスにコンストラクターを用意する”

第 10 章で学んだように、コンストラクターでオブジェクト作成時に必要な値を必ず受け取れます。 継承でも、親クラスにコンストラクターを用意できます。

EmployeeBase.cs(改造版):

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(改造版):

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円

base を使うと、初期化の役割分担がはっきりします。

共通の値(社員ID、氏名、部署)
→ EmployeeBase のコンストラクターで設定
子クラス独自の値(月給、時給など)
→ 子クラス自身のコンストラクターで設定

13-4 メソッドを上書きする(virtual / override)

Section titled “13-4 メソッドを上書きする(virtual / override)”

子クラスごとに処理を変えたい

Section titled “子クラスごとに処理を変えたい”

支給額の計算を考えます。

正社員 → 月給をそのまま支給
契約社員 → 時給 × 勤務時間

どちらも「支給額を求める」目的は同じですが、計算方法が違います。 このようなときに、親クラスのメソッドを子クラスで上書き します。


場所キーワード意味
親クラスvirtual「このメソッドは子クラスで上書きしてもよい」
子クラスoverride「親のメソッドを上書きする」

EmployeeBase に virtual メソッドを追加する

Section titled “EmployeeBase に virtual メソッドを追加する”
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 virtual int GetPaymentAmount()
{
return 0; // 既定値(子クラスで上書きされる想定)
}
}

RegularEmployee.cs:

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:

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;
}
}

Program.cs
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() という呼び方なのに、オブジェクトに応じて計算方法が変わっています。


ポリモーフィズム(多態性)は、次のような性質を指します。

同じ呼び出し方で、
オブジェクトの実際の型に応じて異なる動きをする

virtualoverride を使ったメソッドは、まさにこの動きをします。


親クラス型の変数に子クラスを代入できる

Section titled “親クラス型の変数に子クラスを代入できる”

継承関係があると、親クラス型の変数に子クラスのオブジェクトを入れられます

EmployeeBase employee1 = new RegularEmployee(1001, "山田二郎", "総務", 500000);
EmployeeBase employee2 = new ContractEmployee(1004, "田中浩介", "マーケティング", 1800, 120);

RegularEmployeeContractEmployee も「EmployeeBase の一種」なので、EmployeeBase 型の変数に代入できます。

変数の型 → EmployeeBase
中に入る実体 → RegularEmployee や ContractEmployee

親クラス型のリストを使うと、異なる種類の子クラスを 1 つにまとめて扱えます

Program.cs
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() を呼ぶと 実際のオブジェクトに応じた処理 が動きます。

図の見方(シーケンス図の基本)

シーケンス図は、時間の流れを縦軸(上から下へ) で表す図です。

  • 横に並んだ (MainemployeesRegularEmployeeContractEmployee)が 登場人物
  • 実線の矢印 呼び出し(処理を依頼する。例:GetPaymentAmount() を呼ぶ)
  • 破線の矢印 返り値(結果が返ってくる。例:500000 を返す)
  • Note(箱で囲まれた黄色いメモ)は 補足説明(誰の番か、何の場面か)

この図の流れ:

  1. Mainemployees(List<EmployeeBase> 型のリスト)に「foreach で順に取り出す」と依頼
  2. 1 件目の 実体は RegularEmployee(山田二郎)RegularEmployeeGetPaymentAmount() が動き、500000 を返す
  3. 2 件目の 実体は ContractEmployee(田中浩介)ContractEmployeeGetPaymentAmount() が動き、216000 を返す

呼ぶ側のコードは employee.GetPaymentAmount() の 1 行だけ なのに、実体に応じて違う実装が動く ── これがポリモーフィズムです。

呼び方(employee.GetPaymentAmount())は同じ なのに、実体に応じて違う実装が動く ── これがポリモーフィズムの基本です。


ポリモーフィズムを使うと、呼び出し側が型を判定する必要がなくなります

ポリモーフィズムなしの場合:

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 つを 同時に 行います。

  1. その変数の 実体が指定した型かどうか を調べる(結果は true / false)
  2. 一致したら、その型として使える 新しい変数(ここでは 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)を追加しても、呼び出し側のコードは変えずに済みます。


EmployeeBase の GetPaymentAmount は本当に必要か?

Section titled “EmployeeBase の GetPaymentAmount は本当に必要か?”

ここまでの例では、EmployeeBase.GetPaymentAmountreturn 0; というほぼ空の実装でした。

public virtual int GetPaymentAmount()
{
return 0;
}

しかし、よく考えると次のことが言えます。

  • 「正社員でも契約社員でもない、ただの EmployeeBase」は、現実には存在しない
  • GetPaymentAmount の実装は、子クラスごとに必ず定義されるべき

このようなときは、抽象クラス(abstract class)にします。


抽象クラスは abstract キーワードを付けて定義します。 抽象クラスの中には、実装を持たない抽象メソッド を定義できます。

EmployeeBase.cs(抽象クラス版):

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 する必要 がある(そうでないとコンパイルエラー)

抽象クラスは「直接インスタンス化できない、子クラス専用の親クラス」です。

EmployeeBase employee = new EmployeeBase(...); // ← コンパイルエラー

「正社員でも契約社員でもない曖昧な社員」を作らせないための仕組みです。


抽象クラスを継承する子クラスは、抽象メソッドをすべて 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() を実装していません」とエラーを出してくれます。


種類親クラスでの実装子クラスでの override
通常メソッド普通の処理できない(隠蔽は別概念)
virtual メソッドデフォルトの処理あり任意(してもしなくてもよい)
abstract メソッド実装なし({ } がない)必須

13-7 継承を読むときのポイント

Section titled “13-7 継承を読むときのポイント”

現場では、自分で継承を設計するより、既存コードを読む 場面の方が多くなります。 継承を含むコードに出会ったら、次の順で確認するとスムーズです。

ステップ確認ポイント
1クラス定義の 1 行目で「: 親クラス」を確認(継承しているか)
2親クラスにどんなプロパティ・メソッドがあるか
3子クラスで追加されたメンバーは何か
4override が付いているメソッドはどれか(動きが変わる)
5変数や List<T>親クラス型 で扱われていないか(ポリモーフィズム)
EmployeeBase employee = new RegularEmployee(...);
employee.GetPaymentAmount(); // ← どの実装が動くかは「実体」で決まる

変数の 型は EmployeeBase ですが、実際に動くのは RegularEmployee の実装 です。 このパターンを見抜けると、現場のコードがぐっと読みやすくなります。


つまずき原因対応
継承の意味が分からない親子関係のイメージが曖昧共通部分を親クラスにまとめると考える
子クラスに書いていないメンバーが使える理由が分からない親クラスから受け継いでいることを見落とし親クラスの定義を確認する
: base(...) を忘れてエラー親に引数付きコンストラクターがある子クラスから base(...) で値を渡す
virtual を付けずに override でエラー親メソッドが上書きを許可していない親に virtual(または abstract)を付ける
override を書き忘れる既定では親の処理が動くoverride を付けて上書き
親クラス型に子クラスを代入できる理由が分からないis-a 関係が曖昧子クラスは親クラスの一種
List<EmployeeBase> に複数種類を入れられる理由が分からない共通の親型として扱う考え方に慣れていない全員 EmployeeBase の一種
抽象クラスを new してエラー抽象クラスは直接インスタンス化できない子クラスを new する
抽象メソッドを override し忘れてエラー抽象メソッドは実装必須子クラスで override する
継承を使うべき場面が分からない何でも継承しようとしている「A は B の一種」と自然に言えるか確認

  • 継承とは何かを説明できる
  • 親クラスと子クラスの関係を説明できる
  • class 子クラス : 親クラス の書き方を理解している
  • 子クラスから親クラスのプロパティやメソッドを使える
  • : base(...) で親コンストラクターを呼び出せる
  • virtual の役割を説明できる
  • override の役割を説明できる
  • 同じメソッド呼び出しで実体に応じた動きをすることを説明できる
  • 親クラス型の変数に子クラスのオブジェクトを代入できる
  • List<親クラス> で複数種類の子クラスをまとめて扱える
  • 抽象クラスとは何かを説明できる
  • 抽象メソッドが子クラスで実装必須であることを説明できる
  • 継承を使ったコードを読むときの確認手順が分かる
  • 継承を使いすぎるべきではないことを理解している

研修の進め方によっては、隣の人またはチーム内で説明確認を行います。

次の内容を、自分の言葉で説明してください。

  1. 継承はどのようなときに使いますか。
  2. 親クラスと子クラスの違いは何ですか。
  3. : base(...) は何をしていますか。
  4. virtualoverride は何のために使いますか。
  5. ポリモーフィズムとは、ひとまずどのような考え方ですか。
  6. List<EmployeeBase>RegularEmployeeContractEmployee を入れられるのはなぜですか。
  7. 抽象クラスを直接 new できないのはなぜですか。
  8. 抽象メソッドと普通の virtual メソッドの違いは何ですか。

説明するときは、完全な答えでなくても構いません。 自分の言葉で説明しようとすることが大切です。


この章の演習課題に取り組みます。複数のクラスファイルを作る、歯ごたえのある演習です。

段階目安時間内容
① 準備10 分ペア確認 + 課題確認(評価対象外)
② ソロ作業35 分タイマーで計測。タイマー時点の commit が唯一の評価対象(別ファイルでクラスを複数作るため長めに設定)
③ チーム時間講師指定の発表開始時刻までレビュー + 発表者選出 + 実装続行(任意)。発表開始時刻は厳守

提出ルール(タイマー方式)

タイマー時点の commit が唯一の評価対象です。タイマー後の書き足しは評価されません。 コミットメッセージ形式:Chapter13 タイマー提出: <どこまで完成> / <詰まったポイント>(なければ「特になし」) 例:Chapter13 タイマー提出: 必須13-1・13-2完成、発展13-3途中 / List<DeliveryBase> の型でつまずいた

提出方法:Git が使えないときはサーバへコピー

講師の指示があったときは、push の代わりに Kadai13 フォルダを提出先サーバへコピーし、コピー先に 提出メモ.txt(「どこまで完成」「詰まったポイント」を記載)を作成してください。

タイマー後のチーム時間の使い方

レビュー・発表者選出・実装続行(任意)を自由配分してください。発表開始時刻は厳守です。


この章では、課題ごとにコンソールアプリのプロジェクトを作成 します。 各課題では、指定されたプロジェクト名を使ってください。

課題必須/発展プロジェクト名作成する主なファイル
課題 13-1必須Kd13_01_DeliveryFeeProgram.csDeliveryBase.csRegularDelivery.csExpressDelivery.cs
課題 13-2必須Kd13_02_PolymorphismProgram.cs、3 ファイル(課題 13-1 と同じ構成)
課題 13-3発展Kd13_03_AbstractClassProgram.cs、3 ファイル(課題 13-1 を発展)
課題 13-4発展Kd13_04_ReportBaseProgram.csReportBase.csSalesReport.csAttendanceReport.cs
課題 13-5発展Kd13_05_RegionSummaryProgram.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) で、同じ仕組みを自分で組み立てます。 本文のコードをそのまま写しても解けません。本文で学んだ「継承・basevirtual/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> を使う(RegularDeliveryExpressDelivery を同じリストに入れる)
  • foreach の中で型判定の if は書かない(PrintInfo() の中で実体ごとの GetFee() が動く)
  • LINQ の Sum を使って合計を算出(deliveries.Sum(d => d.GetFee()))

必須課題が終わった人は、発展課題に取り組んでください。 発展課題からは、仕様だけが提示されます。実装方法は自分で考えてください。


課題 13-3 DeliveryBase を抽象クラスに変更する

Section titled “課題 13-3 DeliveryBase を抽象クラスに変更する”

課題 13-1・13-2 の DeliveryBase を、以下の仕様で 抽象クラス に変更してください。

仕様

  • DeliveryBaseabstract class に変更する
  • GetFee()abstract メソッドに変更する({ } を書かずセミコロンで終わる。return 0; の仮実装は削除)
  • RegularDeliveryExpressDeliveryGetFeeoverride 実装はそのまま
  • 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()* の末尾 *抽象メソッド(子で必ず実装)を表します
  • SalesReportAttendanceReportReportBase を継承し、それぞれ Print()override で実装します
  • 矢印 <|-- は「親 → 子」の継承(is-a)関係です

ReportBase クラス仕様

  • abstract class ReportBase
  • ファイル名:ReportBase.cs
  • プロパティ:Title (stringget; 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() を入れる
  • foreachPrint() を呼び出す

実行結果例:

=== 売上レポート ===
売上レポートを出力します。
=== 勤怠レポート ===
勤怠レポートを出力します。

課題 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() が型ごとに正しく動くことを確認

  • プログラムを Visual Studio から実行できる
  • 親クラスを別ファイルに作成できている
  • 子クラスで : 親クラス の継承を書けている
  • : base(...) で親コンストラクターを呼び出せている
  • virtualoverride を使えている
  • List<親クラス> に複数種類の子クラスを入れている
  • foreach で型を判定せずに同じメソッドを呼べている
  • 抽象クラス・抽象メソッドを使えている(発展)
  • private set で親クラスのプロパティを守れている
  • インデントが整っている
  • タイマー時点で commit 済み(または 提出メモ.txt を書いた)

タイマーが鳴ったら、その時点の状態を Git に提出します。

Terminal window
git status
git add .
git commit -m "Chapter13 タイマー提出: 必須13-1・13-2完成、発展13-3途中 / 特になし"
git push origin main

Git が使えないときは、上記コミットの代わりに Kadai13 フォルダを提出先サーバへコピーし、コピー先に 提出メモ.txt を作成してください(演習課題の「提出方法:Git が使えないときはサーバへコピー」参照)。

Git の詳しい操作は、付録 C「Git のインストールと提出ルール」 を参照してください。


この章では、継承とポリモーフィズムを学習しました。

  • 継承は、共通部分を親クラスにまとめ、子クラスで独自部分を追加する仕組み
  • class 子クラス : 親クラス の形で継承する
  • 子クラスは親クラスのプロパティ・メソッドを利用できる
  • : base(...) で親クラスのコンストラクターを呼び出せる
  • virtual(親)+ override(子)で、メソッドを上書きできる
  • 親クラス型の変数に子クラスのオブジェクトを代入できる
  • List<親クラス> で複数種類の子クラスをまとめて扱える
  • 同じメソッド呼び出しでも、実体の型に応じた動きをする(ポリモーフィズム)
  • ポリモーフィズムにより、呼び出し側の型判定を減らせる
  • abstract class は直接 new できない、子クラス専用の親クラス
  • abstract メソッドは実装を持たず、子クラスで override 必須
  • 継承は便利だが、関係が自然な場合に絞って使う

次章では、インターフェイス を学習します。

インターフェイスは、クラスに「この機能を持っていること」を約束させる仕組みです。 継承や抽象クラスと同じくポリモーフィズムを支えますが、より柔軟な設計に使えます。 現場の既存コード(特に DB 接続や Web フレームワーク)では、インターフェイスが頻繁に登場します。