##概要
- 本稿では,C#初学者向けに「クラスの継承」について
しょうもない具体的な実装例を交えながら解説を行う.
##継承とは
- あるクラスから機能や性質(=メンバ)を受け継いで新しいクラスを作ること.
- 「人間 ⊃ 社員」のように,包括関係にあるものに対し,「社員は人間を継承する」,「社員は人間から派生する」などと表現し,ここでの「人間」のことを「基底クラス(base class)」 または「スーパークラス(super class)」と呼び, 「社員」のことを「派生クラス(derived class)」 または「サブクラス(sub class)」と呼ぶ.
###基底クラスの実装
- 「人間」を表す
Person
クラスに対し,氏名プロパティstring Name
, 生年月日プロパティDateTime Birth
,個人番号プロパティstring MyNumber
,readonly
な年齢プロパティint Age
,年齢計算用の静的メソッドprotected static int GetAge(DateTime birth, DateTime? today = null)
を与える. - ここで,
protected
は,クラス内またはそのクラスを継承する派生クラス内からのみアクセス可能なレベルである.
/// <summary>
/// 人間を表します.
/// </summary>
public class Person
{
/// <summary>
/// 氏名
/// </summary>
public string Name { get; set; }
/// <summary>
/// 生年月日
/// </summary>
public DateTime Birth { get; set; }
/// <summary>
/// 個人番号
/// </summary>
public string MyNumber { get; set; }
/// <summary>
/// 年齢
/// </summary>
public int Age => GetAge(this.Birth);
/// <summary>
/// 年齢を計算します.
/// </summary>
/// <param name="birth">生年月日</param>
/// <param name="today">計算の基準となる日付 ※指定しない場合はシステム日付</param>
/// <returns>年齢</returns>
protected static int GetAge(DateTime birth, DateTime? today = null)
{
if (!today.HasValue) today = DateTime.Today;
var age = today.Value.Year - birth.Year;
if (birth.AddYears(age) > today.Value) age--;
return age;
}
}
###派生クラスの実装
- 「社員」を表す
Employee
クラスを作成し,Person
クラスを継承させる.- クラス宣言部分を
class Derived : Base
の形式で記載.
- クラス宣言部分を
-
Employee
クラスに対し,社員番号プロパティint Id
, 入社日プロパティDateTime Entry
,部署プロパティstring Department
,readonly
な勤続年数プロパティint ServiceYears
を与える. - ここで,
Employee
クラスはPerson
クラスを継承しているため,基底クラスであるPerson
クラスの年齢計算用の静的メソッドprotected static int GetAge(...)
を利用できる.
/// <summary>
/// 社員を表します.
/// </summary>
public class Employee : Person
{
/// <summary>
/// 社員番号
/// </summary>
public int Id { get; private set; }
/// <summary>
/// 入社日
/// </summary>
public DateTime Entry { get; private set; }
/// <summary>
/// 部署
/// </summary>
public string Department { get; private set; }
/// <summary>
/// 勤続年数
/// </summary>
public int ServiceYears => GetAge(this.Entry);
}
###テスト
-
Employee
クラスはPerson
クラスを継承しているので,基底クラスであるPerson
のメンバを持つことが分かる.
var employee = new Employee();
employee.Name = "夜神 月";
employee.Birth = new DateTime(1986, 2, 28);
employee.MyNumber = "000000000000";
employee.Id = 8536110;
employee.Entry = new DateTime(2009, 4, 1);
employee.Department = "警察庁情報通信局情報管理課";
var age = employee.Age;
var serviceYears = employee.ServiceYears;
##アクセシビリティ
- 変数やメソッドに対して,どこからアクセスできるかという制限のレベル.
- ここでの「アセンブリ」とは,「プロジェクト」「exe」「dll」などを指す.
アクセシビリティ | レベル |
---|---|
public | どこからでもアクセス可能 |
protected | クラス内部と派生クラスの内部からのみアクセス可能 |
internal | 同一アセンブリ内のクラスからのみアクセス可能 |
protected internal | 同一アセンブリ内のクラス内部,または派生クラスの内部からのみアクセス可能 |
private protected | 同一アセンブリ内のクラス内部,かつ派生クラスの内部からのみアクセス可能 |
private | クラス内部からのみアクセス可能 |
##コンストラクタ
- 派生クラスのインスタンスが生成されるとき,まず基底クラスのコンストラクタが呼び出され,その後派生クラスのコンストラクタが呼び出される.
- 派生クラスで,基底クラスの引数つきコンストラクタを呼び出すためには, 明示的に基底クラスのコンストラクタを呼び出す必要がある.
###基底クラスのコンストラクタ
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="name">氏名</param>
/// <param name="birth">生年月日</param>
/// <param name="myNumber">個人番号</param>
public Person(string name, DateTime birth, string myNumber)
=> (this.Name, this.Birth, this.MyNumber) = (name, birth, myNumber)
###派生クラスのコンストラクタ
- コンストラクタ定義部分に
public Derived(...) : base(...) { ... }
の形式で記載.
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="name">氏名</param>
/// <param name="birth">生年月日</param>
/// <param name="myNumber">個人番号</param>
/// <param name="id">社員番号</param>
/// <param name="entry">入社日</param>
/// <param name="department">部署</param>
public Employee(string name, DateTime birth, string myNumber, int id, DateTime entry, string department) : base(name, birth, myNumber)
=> (this.Id, this.Entry, this.Department) = (id, entry, department);
##継承の禁止(sealed
)
- クラス宣言部分に
sealed
修飾子を付与した場合,継承不可となる.
##基底クラスのメンバを上書き(override
)する
- 基底クラスで定義されたメンバを,派生クラスで独自機能に上書きすること.
###基底クラスの実装
- メソッドやプロパティに対して
virtual
修飾子を付与することで,派生クラスでのoverride
が可能となる.
/// <summary>
/// 方言を表す基底クラスです.
/// </summary>
public class Dialect
{
/// <summary>
/// 方言を取得します.
/// </summary>
protected virtual string GetDialect() => "サイゼリアへ行きませんか?";
/// <summary>
/// 方言を発言します.
/// </summary>
public void Say() => Console.WriteLine($"{this.GetType().Name}:{this.GetDialect()}");
}
###派生クラスの実装
-
override
修飾子を付与することで,基底クラスのvirtual
なメソッドやプロパティを上書き可能となる. - 基底クラス側の処理を呼び出す場合は,
base.Member
の形式で記載する.
/// <summary>
/// 標準語を表す派生クラスです.※本来は継承しなくて良い
/// </summary>
public class StandardLanguage : Dialect
{
// 本来は override しなくて良い
protected override string GetDialect() => base.GetDialect();
}
/// <summary>
/// 関西弁を表す派生クラスです.
/// </summary>
public class KansaiDialect : Dialect
{
protected override string GetDialect() => "サイゼ行かへん?";
}
/// <summary>
/// 関東弁を表す派生クラスです.
/// </summary>
public class KantoDialect : Dialect
{
protected override string GetDialect() => "ゼリア行こうじゃん?";
}
###テスト
- 派生クラスのインスタンスは基底クラスのインスタンスとして扱うことができる.1
- 基底クラスでメソッドを呼び出したときでも,CLRによってオブジェクトの実行時の型が検索され,そのメソッドの派生クラス版が実行される.2
var dialects = new List<Dialect>(){ new Dialect(),
new StandardLanguage(),
new KansaiDialect(),
new KantoDialect() };
dialects.ForEach(d => d.Say());
###実行結果
Dialect:サイゼリアへ行きませんか?
StandardLanguage:サイゼリアへ行きませんか?
KansaiDialect:サイゼ行かへん?
KantoDialect:ゼリア行こうじゃん?
##上書き(override
)の禁止
- 基底クラスで
override
を禁止するには,virtual
を不要しなければよい. - 派生クラスで基底クラスから
override
したメンバを,さらに派生させたクラスでoverride
禁止するには,sealed
修飾子を付与する.
/// <summary>
/// 讃岐弁を表す派生クラスです.
/// </summary>
public class SanukiDialect : Dialect
{
sealed protected override string GetDialect() => "おなか起きた";
}
/// <summary>
/// ヒンディー語を表す(讃岐弁の)派生クラスです.
/// </summary>
public class HindiDialect : SanukiDialect
{
// エラー:継承されたメンバ'SanukiDialect.GetDialect()'はシールドされているため,オーバーライドできません.
protected override string GetDialect() => "पेट से भरा";
}
##System.Object
のメソッドをoverride
する
- C#ではすべてのクラスまたは構造体が暗黙的に
Object
クラスを継承する. - そのため,C# のすべてのオブジェクトが以下のメソッドを得る.
-
ToString()
:現在のオブジェクトを表す文字列を返す. -
Equals()
:2 つのオブジェクトインスタンスが等しいかどうかを判断する.
-
- ここでは例として,図鑑番号と種族名の属性を持つ
Pokomon
クラスを実装し,System.Windows.Forms.Form
上でSystem.Windows.Forms.ListBox
へのアイテム追加を考える.
###クラスとフォームの実装
クラス[Pokomon]の実装
/// <summary>
/// ポ〇モンを表します.
/// </summary>
public class Pokomon
{
/// <summary>
/// 図鑑番号
/// </summary>
public int Id { get; private set; }
/// <summary>
/// 種別名
/// </summary>
public string Name { get; private set; }
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="id">図鑑番号</param>
/// <param name="name">種別名</param>
public Pokomon(int id, string name) => (this.Id, this.Name) = (id, name);
}
Formの実装
// サンプルポコモンを追加
this.ListBox.Items.AddRange(new Pokomon[] { new Pokomon(0, "ィ゛ゃゾ┛A"),
new Pokomon(6, "アネ゙デパミ゙"),
new Pokomon(135, "ベアビヲ9"),
new Pokomon(253, "ダメタマゴ"),
new Pokomon(255, "けつばん")});
###実行結果
###ToString()
をoverride
する
-
ToString()
をoverride
しない場合,既定の文字列変換が行われ,クラス名が表示される. -
Pokomon.Name
プロパティを追加していけば表示は正せるが,追加アイテムがstring
型になってしまい取得時に都合が悪い. -
ToString()
をoverride
することで,指定した文字列を表示可能である. -
ToString()
のoverride
ではEmpty
またはnull
文字列を返さないようにする必要がある.また,例外をスローしない.
####実装例
Pokomon.cs
/// <summary>
/// 現在のインスタンスを表す文字列を取得します.
/// </summary>
public override string ToString() => this.Name;
####実行結果
###Equals(Object)
をoverride
する
- 既定の
Equals(Object)
では参照の等価を判断するため,以下のように判断される.
####実行結果
Form1.cs
var pokomon1 = new Pokomon(6, "アネ゙デパミ゙");
var pokomon2 = new Pokomon(6, "アネ゙デパミ゙");
var judge1 = pokomon1.Equals(pokomon1); //実行結果:true
var judge2 = pokomon1.Equals(pokomon2); //実行結果:false
var judge3 = this.ListBox.Items.Contains(pokomon1); //実行結果:false
- ここで,
pokomon
とは図鑑番号によって一意に決定されることとし,Equals(Object)
をoverride
して図鑑番号での等価性を確保する. -
Equals(Object)
の実装では,例外をスローすることはできず,常に値を返す必要がある.例えばObject
がnull
の場合,ArgumentNullException
をthrow
するのではなく,false
を返す必要がある. -
Equals(Object)
をoverride
する型は,ハッシュテーブルを正しく機能させるため,GetHashCode()
もオーバーライドする必要がある. - 値型の場合,さらに下記の実装が必要となるが,本稿では割愛する(Microsoft Docsを参照).
-
IEquatable<T>
インターフェイス - 等値演算子
==
のオーバーロード
-
####実装例
Pokomon.cs
/// <summary>
/// 指定したオブジェクトが,現在のオブジェクトと等しいかどうか判断します.
/// </summary>
public override bool Equals(object obj) => (obj as Pokomon)?.Id == this.Id;
/// <summary>
/// 既定のハッシュ関数として機能します.
/// </summary>
public override int GetHashCode() => this.Id;
####実行結果
Form1.cs
var pokomon1 = new Pokomon(6, "アネ゙デパミ゙");
var pokomon2 = new Pokomon(6, "アネ゙デパミ゙");
var judge1 = pokomon1.Equals(pokomon1); //実行結果:true
var judge2 = pokomon1.Equals(pokomon2); //実行結果:true
var judge3 = this.ListBox.Items.Contains(pokomon1); //実行結果:true
##基底クラスのメンバを隠蔽(new
)する
- 派生クラスで,基底クラスと同じシグネチャのメンバを再定義すること.3
- メンバの実体が 1 つに収束するわけではなく,基底クラスと派生クラスで同じメンバが重複して存在するようになる.
-
new
修飾子を付与することで,基底クラスのメソッドやプロパティを隠蔽して再定義できる.このメンバはvirtual
でなくてもよい.
###実装例
-
sealed
でないクラスLight_Yagami
に対し,「私はキラです」と自己紹介を行うvirtual
でないプロパティを与える.
/// <summary>
/// キラを表します.
/// </summary>
public class Light_Yagami
{
/// <summary>
/// 自己紹介します.
/// </summary>
public string SelfIntroduction => "私はキラです";
}
-
Light_Yagami
を継承したL_Lawliet
クラスで,自分がキラであることを隠蔽するため,自己紹介プロパティをnew
して再定義し,「私はLです」などと宣うことにする.
/// <summary>
/// (2代目)Lを表します.
/// </summary>
public class L_Lawliet : Light_Yagami
{
/// <summary>
/// 自己紹介します.
/// </summary>
public new string SelfIntroduction => "私はLです";
}
###実行結果
var K = new Light_Yagami();
var L = new L_Lawliet();
Console.WriteLine(K.SelfIntroduction); // 実行結果:私はキラです
Console.WriteLine(L.SelfIntroduction); // 実行結果:私はLです
##上書き(override
)と隠蔽(new
)の相違
-
new
はフィールドの型に応じて呼び出しメソッドが決められる. -
override
はインスタンスの型に応じて呼び出しメソッドが決められる.
###override
の動作
####実装例
/// <summary>
/// キラを表します.
/// </summary>
public class Light_Yagami
{
public virtual string SelfIntroduction => "私はキラです";
}
/// <summary>
/// (2代目)Lを表します.
/// </summary>
public class L_Lawliet : Light_Yagami
{
public override string SelfIntroduction => "私はLです";
}
####実行結果
DeathNoteTester.cs
Light_Yagami K = new Light_Yagami(); // フィールド:Light_Yagami,インスタンス:Light_Yagami
L_Lawliet L1 = new L_Lawliet(); // フィールド:L_Lawliet,インスタンス:L_Lawliet
Light_Yagami L2 = new L_Lawliet(); // フィールド:Light_Yagami,インスタンス:L_Lawliet
Console.WriteLine(K.SelfIntroduction); // 実行結果:私はキラです
Console.WriteLine(L1.SelfIntroduction); // 実行結果:私はLです
Console.WriteLine(L2.SelfIntroduction); // 実行結果:私はLです
###new
の動作
####実装例
Light_Yagami.cs
/// <summary>
/// キラを表します.
/// </summary>
public class Light_Yagami
{
public string SelfIntroduction => "私はキラです";
}
L_Lawliet.cs
/// <summary>
/// (2代目)Lを表します.
/// </summary>
public class L_Lawliet : Light_Yagami
{
public new string SelfIntroduction => "私はLです";
}
####実行結果
Light_Yagami K = new Light_Yagami(); // フィールド:Light_Yagami,インスタンス:Light_Yagami
L_Lawliet L1 = new L_Lawliet(); // フィールド:L_Lawliet,インスタンス:L_Lawliet
Light_Yagami L2 = new L_Lawliet(); // フィールド:Light_Yagami,インスタンス:L_Lawliet
Console.WriteLine(K.SelfIntroduction); // 実行結果:私はキラです
Console.WriteLine(L1.SelfIntroduction); // 実行結果:私はLです
Console.WriteLine(L2.SelfIntroduction); // 実行結果:私はキラです
##抽象クラス
- 継承して派生クラスでインスタンス生成して使うことを前提としたクラス.
- クラスに
abstract
修飾子を付与して宣言する.
- クラスに
- 抽象クラス自身は実体を持たず,インスタンス化できない.
- 基底クラスでメソッドの意味だけを定義して実装を持たず,派生クラス上での実装を強制する抽象メソッドを定義できる.
- メソッドに
abstract
修飾子を付与して宣言する.
- メソッドに
###抽象クラスの実装
/// <summary>
/// 方言を表す抽象クラスです.
/// </summary>
public abstract class Dialect
{
/// <summary>
/// 方言を取得します. ※抽象メソッド:実装を持たず,派生クラスでの実装を強制する.
/// </summary>
protected abstract string GetDialect();
/// <summary>
/// 方言を発言します.
/// </summary>
public void Say() => Console.WriteLine($"{this.GetType().Name}:{this.GetDialect()}");
}
###派生クラスの実装
/// <summary>
/// 標準語を表す派生クラスです.
/// </summary>
public class StandardLanguage : Dialect
{
protected override string GetDialect() => "サイゼリアへ行きませんか?";
}
/// <summary>
/// 関西弁を表す派生クラスです.
/// </summary>
public class KansaiDialect : Dialect
{
protected override string GetDialect() => "サイゼ行かへん?";
}
/// <summary>
/// 関東弁を表す派生クラスです.
/// </summary>
public class KantoDialect : Dialect
{
protected override string GetDialect() => "ゼリア行こうじゃん?";
}
###テスト
var dialects = new List<Dialect>(){ new StandardLanguage(), new KansaiDialect(), new KantoDialect() };
dialects.ForEach(d => d.Say());
###実行結果
StandardLanguage:サイゼリアへ行きませんか?
KansaiDialect:サイゼ行かへん?
KantoDialect:ゼリア行こうじゃん?
##ポリモーフィズム
- クラスメンバがシグネチャに応じて異なる複数の実装を持ち,呼び出し時に使い分けできることで,操作が統一的であること.3
- 抽象クラスではメンバの意味だけを与え,派生クラスでの実装を強要できることから,統一的なクラス設計が可能である.
- ポリモーフィズムではインターフェース継承が必須となるが,これについては別記事で記載予定である.
- デザインパターンとしての継承の使い方については,下記記事が非常に参考になる.
##参考
- 継承 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
- 実装の隠蔽 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
- Object.ToString メソッド (System) | Microsoft Docs
- Object.Equals メソッド (System) | Microsoft Docs
- Override キーワードと New キーワードを使用する場合について - C# プログラミング ガイド | Microsoft Docs
- ポリモーフィズム - C# プログラミング ガイド | Microsoft Docs
- 君の継承の使い方は間違っている - Qiita
- 夜神月 - Wikipedia
- L (DEATH NOTE) - Wikipedia
- サイゼリヤトップページ|サイゼリヤ
- バグポケモン一覧 - ポケモンWiki