++C++; // 未確認飛行 C

Google
Web ufcpp.net

多態性

目次

キーワード

概要

多態性(polymorphism: ポリモーフィズム)とは、 同じメソッド呼び出し(オブジェクト指向用語的には「メッセージ」という)に対して異なるオブジェクトが異なる動作をすることを言います。

( ちなみに、polymorphism は多相性とか多様性と訳す場合もあります。 「poly(多)+morphism(射:形を変えるみたいな意味) → いろいろな姿を映し出す」という意味。 )

オブジェクト指向プログラミング言語には、 多態性を実現するために、仮想メソッドというものが用意されています。

静的な型、動的な型

継承」 で説明したとおり、 派生クラスのインスタンスは基底クラスの変数に格納することが出来ます。 このとき、変数の型を静的な型といい、 実際に格納されているインスタンスの型を動的な型といいます。

class Base{}
class Derived : Base{}

class DinamicTypeTest
{
  public static void Main()
  {
    // 変数の型
    // |         実際に格納するインスタンスの型
    // |         |
    // ↓         ↓              静的な型, 動的な型
    Base    a = new Base();    // Base    , Base
    Base    b = new Derived(); // Base    , Derived
    Derived c = new Derived(); // Derived , Derived
  }
}

“静的”とはコンパイル時に型が確定するという意味です。 変数(new で生成されるインスタンスではなく、単なる入れ物)の型は、 宣言時に決まっていますので、静的な型になります。 つまり、実行時に型が変わるということはありません。

静的な型の情報は以下のように typeof 演算子を用いて取得することが出来ます。 typeof 演算子は System.Type というクラスのインスタンスを返します。

typeof(クラス名)

逆に、“動的”とはコンパイル時には型が確定せず、 実行時に変化する可能性のあるもののことを指します。 (なので、動的な型のことを実行時型(run-time type)とも言います。) (単なる入れ物である)変数とは異なり、 (実行時に new で生成される)インスタンスの型は実行時に決まります。

動的な型の情報は以下のように GetType メソッドを用いて取得します。

変数名.GetType()

サンプル

using System;

class Base{}
class Derived : Base{}

class DinamicTypeTest
{
  public static void Main()
  {
    ShowDinamicType(new Base());
    ShowDinamicType(new Derived());
  }

  // Base 型の変数 b に格納されているインスタンスの動的な型の名前を表示する。
  static void ShowDinamicType(Base b)
  {
    Type t = b.GetType();
    Console.Write(t.Name + "\n");
  }
}
Base
Derived

ダウンキャスト

基底クラスの変数に派生クラスの変数を渡すことをアップキャスト(upcast)と呼び、 それとは逆に、 派生クラスの変数に基底クラスの変数を渡すことをダウンキャスト(downcast)と呼びます。

基底クラスの変数に派生クラスのインスタンスを格納することは何の問題もありませんので、 アップキャストは常に安全に行うことが出来ます。 ところが、ダウンキャストの場合は必ずしも安全には行うことが出来ません。 以下に危険なダウンキャストの例を挙げます。

class Base{}
class Derived1 : Base{}
class Derived2 : Base{}

class DowncastTest
{
  public static void Main()
  {
    Derived1 d1 = new Derived1(); // 当然、合法。
    Derived2 d2 = new Derived2(); // 同じく、合法。

    Base b;
    Derived1 d;

    b = d1;          // アップキャストは常に合法。明示的なキャスト不要。
    d = (Derived1)b; // ダウンキャストは明示的なキャストが必要。
    // Derived1 の変数に Derived1 のインスタンスを格納しているので、これはOK。

    b = d2;          // 同じ事を今度は d2 の方で繰り返す。
    d = (Derived1)b;
    // Derived1 の変数に Derived2 のインスタンスを格納しているので、これは問題あり。
    // コンパイルは通るが、実行時エラーになる。
  }
}

このプログラムを実行すると InvalidCastException という例外が発生します。 (例外については 「例外処理」 で説明します。)

このような問題があるため、ダウンキャストを行う際には動的な型情報を取得する必要があります。 そのための構文として C# には is 演算子as 演算子があります。

is 演算子はキャスト可能かどうかを調べるための演算子で以下のようにして使用します。

変数名 is 型名

is 演算子を適用した結果は bool 型になり、 左辺の変数が右辺の型にキャスト可能ならば true を、不能ならば false を返します。

using System;

class Base{}
class Derived1 : Base{}
class Derived2 : Base{}

class DowncastTest
{
  public static void Main()
  {
    Base b;

    b = new Derived1();
    if(b is Derived1)
      Console.Write("b = new Derived1();\n");

    b = new Derived2();
    if(b is Derived1)
      Console.Write("b = new Derived2();\n");
  }
}
b = new Derived1();

as 演算子はキャストと同じような働きをする演算子で、以下のようにして使用します。

変換先の変数 = 変換元の変数 as 型名

キャストとの違いは、 もし型変換が出来ない場合には結果が null になるということです。

using System;

class Base{}
class Derived1 : Base{}
class Derived2 : Base{}

class DowncastTest
{
  public static void Main()
  {
    Base b;
    Derived1 d;

    b = new Derived1();
    d = b as Derived1;
    if(d != null)
      Console.Write("b = new Derived1();\n");

    b = new Derived2();
    d = b as Derived1;
    if(d != null)
      Console.Write("b = new Derived2();\n");
  }
}
b = new Derived1();

仮想メソッド

C# では、何も指定しない通常のメソッド呼び出し時、 基底クラスと派生クラスに同名のメソッドがある場合、 どちらのメソッドが呼び出されるかは静的な型によって決定されます。

using System;

class Base
{
  public void Test(){Console.Write("Base.Test()\n");}
}

class Derived : Base
{
  public new void Test(){Console.Write("Derived.Test()\n");}
}

class NonVirtualTest
{
  public static void Main()
  {
    Base    a = new Base();
    a.Test(); // Base の Test が呼ばれる。

    Base    b = new Derived();
    b.Test(); // Base の Test が呼ばれる。

    Derived c = new Derived();
    c.Test(); // Derived の Test が呼ばれる。
  }
}
Base.Test()
Base.Test()
Derived.Test()

しかし、動的な型に基づいて呼び出されるメソッドを決定したい場合があります。 (というより、ほとんどの場合、メソッド呼び出しは動的に決定した方が都合がいい。) 動的な型に基づいて呼び出されるメソッドを選びたい場合、 以下のように、 メソッドに virtual という修飾子を付けます。

using System;

class Base
{
  public virtual void Test(){Console.Write("Base.Test()\n");}
}

class Derived : Base
{
  public override void Test(){Console.Write("Derived.Test()\n");}
}

class VirtualTest
{
  public static void Main()
  {
    Base    a = new Base();
    a.Test(); // Base の Test が呼ばれる。

    Base    b = new Derived();
    b.Test(); // Derived の Test が呼ばれる。

    Derived c = new Derived();
    c.Test(); // Derived の Test が呼ばれる。
  }
}
Base.Test()
Derived.Test()
Derived.Test()

このような virtual 修飾子をつけたメソッドのことを仮想メソッド(virtual method)と呼びます。

また、仮想メソッドを派生クラスで再定義することをメソッドのオーバーライド(override: 上に重なる)と言います。 オーバーロード( 「関数」 のところにある「関数のオーバーロード」を参照)と混乱しそうになる名前ですが、別物です。

さらに、C#では、 「基底クラスのメンバーの隠蔽」 と同様に、 プログラマの意図しないところでメソッドがオーバーライドされてしまうのを防ぐため、 メソッドをオーバーライドする際には override 修飾子 を明示的に付ける必要があります。

仮想メソッドの利用例

仮想メソッド、すなわち、メソッドの動的呼び出しを用いると、 どのようなことが出来るのかを説明します。

ここではまた、例として Person クラスを使いましょう。 人間と一口に言ってもいろいろな人がいます。 例えば、年齢を聞いても、 正直に答える人、 鯖を読む人、 大体の年齢しか答えない人とさまざまなタイプの人がいます。

このようなさまざまなタイプの人をクラスで表現してみましょう。 まずは共通部分をまとめた基底クラス(Person)を定義します。 年齢を取得するプロパティ Age は、virtual にしておいて、 とりあえず意味のない値を返しておきます。

class Person
{
  protected string name;
  protected int age;

  public Person(string name, int age)
  {
    this.name = name;
    this.age  = age;
  }

  public string Name{get{return this.name;}}
  public virtual int Age{get{return 0;}} // 基底クラスでは特に意味のない値を返す。
}

次に正直者を表すクラス(Truepenny)を定義します。 TruepennyAge プロパティでは実年齢をそのまま返します。

/// <summary>
/// 正直者。
/// 年齢を偽らない。
/// </summary>
class Truepenny : Person
{
  public Truepenny(string name, int age) : base(name, age){}

  public override int Age
  {
    get
    {
      // 実年齢をそのまま返す。
      return this.age;
    }
  }
}

次は嘘つき(Liar)クラスの定義です。 LiarAge プロパティでは、 歳を取るにつれ大幅に鯖を読んだ値を返します。

/// <summary>
/// 嘘つき。
/// 鯖を読む(しかも、歳取るにつれ大幅に)。
/// </summary>
class Liar : Person
{
  public Liar(string name, int age) : base(name, age){}

  public override int Age
  {
    get
    {
      // 年齢を偽る。
      if(this.age < 20) return this.age;
      if(this.age < 25) return this.age - 1;
      if(this.age < 30) return this.age - 2;
      if(this.age < 35) return this.age - 3;
      if(this.age < 40) return this.age - 4;
      return this.age - 5;
    }
  }
}

次はいい加減な人(Equivocator)クラスの定義です。 EquivocatorAge プロパティでは、 実年齢を四捨五入した値を返します。

/// <summary>
/// いいかげん。
/// 大体の歳しか答えない。
/// </summary>
class Equivocator : Person
{
  public Equivocator(string name, int age) : base(name, age){}

  public override int Age
  {
    get
    {
      // 年齢を四捨五入した値を返す。
      return ((this.age + 5) / 10) * 10;
    }
  }
}

最後に、これらのクラスを利用したプログラムを作ってみます。 以下の例では、Person クラスを引数とし、 その人の自己紹介文を画面に表示するメソッドを用意し、 正直者、嘘つき、いい加減な人のそれぞれに自己紹介をしてもらいます。

using System;

class PolymorphismTest
{
  public static void Main()
  {
    Introduce(new Truepenny  ("Ky Kiske"  , 24)); //正直者のカイさん24歳。
    Introduce(new Liar       ("Axl Low"   , 24)); //嘘つきのアクセルさん24歳。
    Introduce(new Equivocator("Sol Badguy", 24)); //いい加減なソルさん24歳。
  }

  /// <summary>
  /// p さんの自己紹介をする。
  /// </summary>
  static void Introduce(Person p)
  {
    Console.Write("My name is {0}.\n", p.Name);
    Console.Write("I'm {0} years old.\n\n", p.Age);
  }
}
My name is Ky Kiske.
I'm 24 years old.

My name is Axl Low.
I'm 23 years old.

My name is Sol Badguy.
I'm 20 years old.

正直者、嘘つき、いい加減な人はいずれも実年齢24歳にしてあります。 しかし、画面に表示される自己紹介文では異なる年齢が表示されています。

Introduce メソッド中では、 PersonAge プロパティが呼び出されていますが、 実際には、動的型情報に基づき、 TruepennyLiarEquivocatorAge プロパティが呼び出されます。

多態性とは

仮想メソッドの利用例のところで示したとおり、 仮想メソッドを用いると、同じメソッドを呼び出しても、 変数に格納されているインスタンスの型によって異なる動作をします。 このように、同じメッセージ(メソッド呼び出し)に対し、 異なるオブジェクトが異なる動作をすることを多態性(polymorphism: ポリモーフィズム)と呼びます。

仮想メソッド呼び出しの他にも、 メソッドのオーバーロード (同じ名前のメソッドでも、引数が異なれば動作も異なる) なども多態性の一種であると考えられます。 しかし、メソッドのオーバーロードはその動作がコンパイル時に決定しますが、 仮想メソッド呼び出しの動作は実行時に決定するという違いがあります。 (前者を静的多態性、後者を動的多態性と言って区別する場合もあります。)

演習問題

問題 1

クラス問題 1Triangle クラスを元に、 以下のような継承構造を持つクラスを作成せよ。

まず、三角形や円等の共通の基底クラスとなる Figure クラスを以下のように作成。

/// <summary>
/// 2次元空間上の図形を表すクラス。
/// 三角形や円等の共通の基底クラス。
/// </summary>
class Figure
{
  virtual public double GetArea() { return 0; }
  virtual public double GetPerimeter() { return 0; }
}

そして、Figure クラスを継承して、 三角形 Triangle クラスと 円 Circle クラスを作成。

/// <summary>
/// 2次元空間上の三角形をあらわすクラス
/// </summary>
class Triangle : Figure
/// <summary>
/// 2次元空間上の円をあらわすクラス
/// </summary>
class Circle : Figure

解答

Transtation into English

[お問い合わせ](q)