抽象(abstract)クラスを理解しよう!

[C#] 抽象(abstract)クラスを理解しよう!

※ 当サイトは広告を含みます。

ここではC#の抽象クラスについて学びます。これはクラスの継承に関する話の延長になります。
理解するためには継承の仕組みの理解が必須なので、忘れてしまった人は以下で復習しましょう。

抽象クラス(abstract class)

抽象クラスとは、抽象的で不完全なクラスを意味します。

りさ

全く分からないよ。

管理人

僕も同じ気持ちです。よくある日本語に翻訳しないほうが良くねって感じの言葉だね。

簡単に言ってしまうとインタンス化できないクラスです。
別の表現をするならインタンス化する必要の無いクラスとも言えます。

色々と機能を実装して継承関係を作るわけですが、その過程でどう考えてもインスタンス化する必要のないクラスが出現します。
よく例題に利用されるのは、動物って概念を継承した犬、猫、鳥みたいなクラスを作るパターンです。

でも、それだと例として面白くないので遊戯王にします。
ぶっちゃけ何でも良いんですが、個人的には身近なゲームとかを例にしたほうが理解しやすいと思います。

りさ

汎用例ってのは誰にでも通じて便利だから存在するんだよ。

管理人

おいおい、流石にカードの種類くらい知ってるだろ。

遊戯王のカードは大きく、[モンスターカード、魔法カード、罠カード]に分類されます。
さらにモンスターカードは通常モンスターとか効果モンスターに分類されます。
もちろん魔法や罠も同じです。と言っても、今回は最上位の3種類(モンスター、魔法、罠)に話を絞ります。

これを表現するクラス設計ですが、全てのカードで共通する項目としてカード名とか説明があります。
他にもあるかもですが、ここでは名前だけにしましょう。そして、これをプロパティで所有することにします。

そして、これらをモンスター、魔法、罠のクラスに個別に実装するのは無駄です。
よって最上位の基底クラスにしましょう。普通に実装するとこうなります。


internal class BaseCard
{
  public string Name { get; set; } // カード名

  public BaseCard()
  {
    this.Name = "";
  }
}
管理人

さて、ここで問題です。このBaseCardクラスってインスタンス化(実体)する必要ありますか?

りさ

無いと思います。

管理人

何故?

りさ

全てのカードは[モンスター、魔法、罠]のいずれかだからです。

管理人

正解です。

つまり、BaseCardとして実体化することはないのです。必ずBaseCardを継承した派生クラスをインスタンス化します。
そんな時に抽象クラスの仕組みが役に立ちます。抽象とは概念みたいなもの、今回の場合はカードっていう概念ですね。

仕組みは簡単です。抽象化するクラスにabstractってキーワードを付けます。以下が構文とサンプルコードです。

抽象クラス

namespace Sample
{
  internal abstract class BaseCard
  {
    public string Name { get; set; } // カード名

    public BaseCard()
    {
      this.Name = "";
    }
  }

  internal class Program
  {
    static void Main(string[] args)
    {
      // インスタンス化できないからビルドエラー
      BaseCard card = new BaseCard();
    }
  }
}

このようにabstractを付けたクラスはインスタンス化が不可能となり、それを試みるとビルドエラーになります。
とは言え、単にインスタンス化を拒否するためにabstractを付けてるわけではありません。これはあくまで抽象クラスの一部の機能です。

管理人

もっと汎用的な例にするなら、敵って概念があって、その配下にラスボス、中ボス、ザコ敵ABC ...って感じの関係です。

抽象メソッドと抽象プロパティ

最初の方で抽象とは不完全と言いました。その機能を証明するように抽象クラスは不完全な実装が許されます。
これを抽象メソッド(またはプロパティ)と呼ばれる仕組みで実装します。難しくないので先に構文とサンプルコードを確認しましょう。

抽象メソッド / 抽象プロパティ

internal abstract class BaseCard
{
  public string Name { get; set; }

  // 抽象プロパティ
  public abstract string Description { get; }

  // 抽象メソッド
  public abstract void Action();

  public BaseCard()
  {
    this.Name = "";
  }
}

抽象メソッドとしてAction()を宣言しました。これはカードを使う時に呼び出すメソッドです。
また、抽象プロパティはあまり良い例ではないですね。正直、プロパティ化してまで返したい値がない。

ポイントはabstractを付けたメソッドやプロパティは実装が半端でも許されます。寧ろ実装することがNGになります。
プロパティに関しては自動実装プロパティのせいで分かりづらいのですが、abstractが付いてると実装されません。

この中途半端な状態が許されるのが抽象クラスの特徴です。これはインスタンス化できないから中途半端でもOKってことですね。
その代わり、抽象クラスを継承した派生クラスは未実装部分の全実装が強制され、そうしないとビルドエラーになります。
ちなみに抽象クラスから抽象クラスって継承も可能です。その場合に実装を強制されるのは最初の非抽象クラスです。

Tips抽象クラスにはフィールドとかメソッドなど、普通のメンバを実装をすることも可能です。

それでは実際に先程の例を派生クラスで実装してみましょう。


internal abstract class BaseCard
{
  public string Name { get; set; }

  // 抽象プロパティ
  public abstract string Description { get; }

  // 抽象メソッド
  public abstract void Action();

  public BaseCard()
  {
    this.Name = "";
  }
}

internal class MonsterCard : BaseCard
{
  // 抽象プロパティの実装
  public override string Description
  {
    get
    {
      return "説明";
    }
  }

  // 抽象メソッドの実装
  public override void Action()
  {
    System.Console.WriteLine("モンスターカードで攻撃!");
  }
}

キーワードのoverrideは元々のメソッドやプロパティを書き換えることを明示的に表現してます。
そして、そういった元々の機能を書き換えることをオーバーライドって言います。
このオーバーライドは抽象クラスに限定した機能ではないので、別枠で解説をしたいと思います。

ポリモーフィズム(polymorphism)

以前に軽く触れましたね。抽象クラスを理解したことでポリモーフィズムについて理解できるようになりました。
これは簡単に言うなら、実際に生成されたインスタンスの型で動作(メソッドやプロパティ)が変わる仕組みです。

このポリモーフィズム自体はとても難しい言葉です。ちなみに日本語だと多態性(または多様性)って呼ばれます。
ただ、そんな言葉を覚えることに意味は無いので、仕組みとして機能を理解しましょう。

とりあえず、これが完成形のサンプルコードです。実行して動作を確認してください。


namespace Sample
{
  internal abstract class BaseCard
  {
    public string Name { get; set; }

    // 抽象プロパティ (※ 分かりづらいので削除)
    //public abstract string Description { get; }

    // 抽象メソッド
    public abstract void Action();

    public BaseCard()
    {
      this.Name = "";
    }
  }

  internal class MonsterCard : BaseCard
  {
    // 抽象メソッドの実装
    public override void Action()
    {
      System.Console.WriteLine("モンスターカードで攻撃!");
    }
  }

  internal class MagicCard : BaseCard
  {
    // 抽象メソッドの実装
    public override void Action()
    {
      System.Console.WriteLine("マジックカード発動!");
    }
  }

  internal class TrapCard : BaseCard
  {
    // 抽象メソッドの実装
    public override void Action()
    {
      System.Console.WriteLine("トラップカード発動!");
    }
  }

  internal class Program
  {
    static void Main(string[] args)
    {
      // 手札 = 複数のカードの集合
      BaseCard[] hand = new BaseCard[]
      {
        new MonsterCard()
        {
          Name = "1枚目",
        },
        new MonsterCard()
        {
          Name = "2枚目",
        },
        new MonsterCard()
        {
          Name = "3枚目",
        },
        new MagicCard()
        {
          Name = "4枚目",
        },
        new TrapCard()
        {
          Name = "5枚目",
        },
      };

      // 全部使ってみる
      foreach (BaseCard card in hand)
      {
        System.Console.WriteLine($"==================================================");
        System.Console.WriteLine($"Name: {card.Name}");

        // ポリモーフィズム
        card.Action();
      }
    }
  }
}

ついでに前に使った神のカードを例にしたサンプルコードも置いておきます。
こっちはポリモーフィズムに絞って記述してるので、より分かりやすいかも。


namespace Sample
{
  // 神のカード (抽象クラス)
  public abstract class GodCard
  {
    // 抽象メソッド
    public abstract void Attack();
  }

  // オシリスの天空竜
  public class Osiris : GodCard
  {
    public override void Attack()
    {
      System.Console.WriteLine("超電導波サンダーフォース");
    }
  }

  // オベリスクの巨神兵
  public class Obelisk : GodCard
  {
    public override void Attack()
    {
      System.Console.WriteLine("ゴッド・ハンド・クラッシャー");
    }
  }

  // ラーの翼神竜
  public class Ra : GodCard
  {
    public override void Attack()
    {
      System.Console.WriteLine("ゴッド・ブレイズ・キャノン");
    }
  }

  internal class Program
  {
    static void Main(string[] args)
    {
      // 基底クラスの配列に派生クラスを入れる
      GodCard[] gods = new GodCard[]
      {
        new Osiris(),
        new Obelisk(),
        new Ra(),
      };

      // 基底クラスの型でアクセス
      foreach (GodCard g in gods)
        g.Attack(); // ポリモーフィズム
    }
  }
}

呼び出し自体は基底クラスの型なのに、どうして派生クラスのメソッドが実行されるのでしょうか?

その理由はこれです。

  1. 実際に生成されてる型が派生クラスの型だから。
  2. 生成された派生クラスが抽象クラスを継承してるから。

特に2つ目が重要です。抽象クラスを継承したクラスは抽象メソッドの実装を強制されます。
つまり、インスタンス化が可能な非抽象クラスとなった時点で、絶対にメソッドが実装されてるんです。
その結果、基底クラスまたは派生クラスの型でメソッドを呼び出しても、何ら問題が起きないことが約束されます。

その代わり制約もあり、抽象クラスと派生クラスでメソッドの引数や戻り値を変更することはできません。
全てが同じ名前、同じ型で存在してる必要があります。その結果、基底クラスの型でも呼び出し処理が実行できるのです。

あとがき

プログラムの設計にオブジェクト指向って考え方があり、それの難しい単語3選みたいなのにポリモーフィズムが入ってます。
残りの2つは継承とカプセル化だった気がします。どれにも言えることですが、機能として見れば全く難しくないですよ。

◆ C#に関する学習コンテンツ

この記事は参考になりましたか?

👆このブログを支援する

関連記事

コメント

この記事へのコメントはありません。