LoginSignup
3
4

More than 3 years have passed since last update.

[C#] クラス継承とポリモーフィズム

Last updated at Posted at 2020-07-16

概要

  • 本稿では,C#初学者向けに「クラスの継承」についてしょうもない具体的な実装例を交えながら解説を行う.

継承とは

  • あるクラスから機能や性質(=メンバ)を受け継いで新しいクラスを作ること.
  • 「人間 ⊃ 社員」のように,包括関係にあるものに対し,「社員は人間を継承する」,「社員は人間から派生する」などと表現し,ここでの「人間」のことを「基底クラス(base class)」 または「スーパークラス(super class)」と呼び, 「社員」のことを「派生クラス(derived class)」 または「サブクラス(sub class)」と呼ぶ.

基底クラスの実装

  • 「人間」を表すPersonクラスに対し,氏名プロパティstring Name, 生年月日プロパティDateTime Birth,個人番号プロパティstring MyNumberreadonlyな年齢プロパティ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 Departmentreadonlyな勤続年数プロパティ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, "けつばん")});

実行結果

image.png

ToString()overrideする

  • ToString()overrideしない場合,既定の文字列変換が行われ,クラス名が表示される.
  • Pokomon.Nameプロパティを追加していけば表示は正せるが,追加アイテムがstring型になってしまい取得時に都合が悪い.
  • ToString()overrideすることで,指定した文字列を表示可能である.
  • ToString()overrideではEmptyまたはnull文字列を返さないようにする必要がある.また,例外をスローしない.

実装例

Pokomon.cs
/// <summary>
/// 現在のインスタンスを表す文字列を取得します.
/// </summary>
public override string ToString() => this.Name;

実行結果

image.png

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)の実装では,例外をスローすることはできず,常に値を返す必要がある.例えばObjectnullの場合,ArgumentNullExceptionthrowするのではなく,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
  • 抽象クラスではメンバの意味だけを与え,派生クラスでの実装を強要できることから,統一的なクラス設計が可能である.
  • ポリモーフィズムではインターフェース継承が必須となるが,これについては別記事で記載予定である.
  • デザインパターンとしての継承の使い方については,下記記事が非常に参考になる.

参考


  1. 派生クラスから基底クラスへの型変換をアップキャストといい,派生クラスが基底ークラスのすべてのメンバを保証できるため暗黙的な変換が可能である.その逆のダウンキャスト(基底クラスから派生クラスへの型変換)では,基底クラスが派生クラスのすべてのメンバーを保証できないため.明示的な型変換が必要となる. 

  2. CLR[Common Language Runtime、共通言語ランタイム]:.NET Frameworkなどを実行するための動作環境. 

  3. シグネチャ:メソッドの名前,引数の数やデータ型,返値の型などの組み合わせのこと. 

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4